feat: multiple lines

This commit is contained in:
TrèsAbhi committed 2024-08-20 13:49:14 -05:00
1 parent cfe0060121
commit 781d2da933
4 files changed
+259 -218

No files matched your search

@@ -1,218 +0,0 @@
import { CaretRightIcon, PlusIcon } from '@radix-ui/react-icons';
import { Box, Flex, Heading, ScrollArea, Text } from '@radix-ui/themes';
import Link from 'next/link';
import { use, useEffect, useMemo, useRef } from 'react';
import { asset } from '../../../../../../core/blitzkit/asset';
import {
TankDefinition,
tankDefinitions,
} from '../../../../../../core/blitzkit/tankDefinitions';
import { TIER_ROMAN_NUMERALS } from '../../../../../../core/blitzkit/tankDefinitions/constants';
import { formatCompact } from '../../../../../../core/math/formatCompact';
import * as Duel from '../../../../../../stores/duel';
function Card({ id, highlight }: { id: number; highlight?: boolean }) {
const awaitedTankDefinitions = use(tankDefinitions);
const tank = awaitedTankDefinitions[id];
return (
<Link
href={`/tools/tankopedia/${id}`}
key={id}
style={{
textDecoration: 'none',
color: 'inherit',
position: 'relative',
}}
>
<Box
mx={highlight ? '2' : '0'}
style={{
background: highlight
? `url(${asset(`flags/fade_small/${tank.nation}.webp`)})`
: undefined,
backgroundPosition: 'center',
backgroundSize: 'cover',
boxShadow: highlight ? 'var(--shadow-4)' : undefined,
borderRadius: 'var(--radius-4)',
overflow: 'hidden',
}}
>
<Flex
direction="column"
align="center"
py="4"
px={highlight ? '4' : '2'}
style={{
backdropFilter: highlight ? 'brightness(0.25)' : undefined,
WebkitBackdropFilter: highlight ? 'brightness(0.25)' : undefined,
}}
>
<img
alt={tank.name}
src={asset(`icons/tanks/big/${id}.webp`)}
width={64}
height={64}
style={{
objectFit: 'contain',
}}
/>
<Flex direction="column" align="center">
<Flex gap="2" align="center">
<Text color="gray" size="1">
{TIER_ROMAN_NUMERALS[tank.tier]}
</Text>
<Text wrap="nowrap">{tank.name}</Text>
</Flex>
<Flex gap="2" align="center">
<Text color="gray" size="1" wrap="nowrap">
<Flex gap="1" align="center">
<img
alt="XP"
src={asset('icons/currencies/xp.webp')}
style={{
width: '1em',
height: '1em',
objectFit: 'contain',
objectPosition: 'center',
}}
/>
{tank.xp === undefined || tank.tier === 1
? 0
: formatCompact(tank.xp!)}
</Flex>
</Text>
<Text color="gray" size="1" wrap="nowrap">
<Flex gap="1" align="center">
<img
alt="Silver"
src={asset('icons/currencies/silver.webp')}
style={{
width: '1em',
height: '1em',
objectFit: 'contain',
objectPosition: 'center',
}}
/>
{formatCompact(tank.price.value)}
</Flex>
</Text>
</Flex>
</Flex>
</Flex>
</Box>
</Link>
);
}
function Arrow() {
return (
<Text color="gray">
<CaretRightIcon />
</Text>
);
}
export function TechTreeSection() {
const tank = Duel.use((state) => state.protagonist.tank);
const awaitedTankDefinitions = use(tankDefinitions);
const container = useRef<HTMLDivElement>(null);
if (
tank.treeType !== 'researchable' ||
tank.ancestors === undefined ||
tank.ancestors.length === 0
) {
return null;
}
const ancestors = useMemo(() => {
const treeBackwards: number[] = [];
let ancestor: TankDefinition | undefined = tank;
while (ancestor !== undefined) {
if (
ancestor.tier === 1 ||
ancestor.ancestors === undefined ||
ancestor.ancestors.length === 0
) {
ancestor = undefined;
break;
} else if (ancestor.ancestors.length === 1) {
ancestor = awaitedTankDefinitions[ancestor.ancestors[0]];
} else {
let bestAncestor = awaitedTankDefinitions[ancestor.ancestors[0]];
ancestor.ancestors.forEach((thisAncestorId) => {
const possibleAncestor = awaitedTankDefinitions[thisAncestorId];
if (possibleAncestor.xp! < bestAncestor.xp!) {
bestAncestor = possibleAncestor;
}
});
ancestor = bestAncestor as TankDefinition;
}
treeBackwards.push(ancestor.id);
}
return treeBackwards.reverse();
}, []);
useEffect(() => {
if (!container.current) return;
container.current.scrollLeft = container.current.scrollWidth;
});
return (
<Flex
my="6"
direction="column"
align="center"
gap="4"
style={{
backgroundColor: 'var(--color-surface)',
}}
py="4"
>
<Heading size="6" style={{ position: 'absolute' }} mt="3">
Tech tree
</Heading>
<ScrollArea type="hover" scrollbars="horizontal" ref={container}>
<Flex align="center" gap="2" justify="center" p="6" pt="9">
{ancestors.map((id, index) => (
<>
{index > 0 && <Arrow />}
<Card key={id} id={id} />
</>
))}
<Arrow />
<Card highlight id={tank.id} />
{tank.tier < 10 && (
<>
<Arrow />
{tank.successors!.map((id, index) => (
<>
{index > 0 && (
<Text color="gray">
<PlusIcon />
</Text>
)}
<Card key={id} id={id} />
</>
))}
</>
)}
</Flex>
</ScrollArea>
</Flex>
);
}
@@ -0,0 +1,10 @@
import { CaretRightIcon } from '@radix-ui/react-icons';
import { Text } from '@radix-ui/themes';
export function Arrow() {
return (
<Text color="gray">
<CaretRightIcon />
</Text>
);
}
@@ -0,0 +1,104 @@
import { Box, Flex, Text } from '@radix-ui/themes';
import Link from 'next/link';
import { use } from 'react';
import { asset } from '../../../../../../../../core/blitzkit/asset';
import { tankDefinitions } from '../../../../../../../../core/blitzkit/tankDefinitions';
import { TIER_ROMAN_NUMERALS } from '../../../../../../../../core/blitzkit/tankDefinitions/constants';
import { formatCompact } from '../../../../../../../../core/math/formatCompact';
export function Node({ id, highlight }: { id: number; highlight?: boolean }) {
const awaitedTankDefinitions = use(tankDefinitions);
const tank = awaitedTankDefinitions[id];
return (
<Link
href={`/tools/tankopedia/${id}`}
key={id}
style={{
textDecoration: 'none',
color: 'inherit',
position: 'relative',
}}
>
<Box
mx={highlight ? '2' : '0'}
style={{
background: highlight
? `url(${asset(`flags/fade_small/${tank.nation}.webp`)})`
: undefined,
backgroundPosition: 'center',
backgroundSize: 'cover',
boxShadow: highlight ? 'var(--shadow-4)' : undefined,
borderRadius: 'var(--radius-4)',
overflow: 'hidden',
}}
>
<Flex
direction="column"
align="center"
py="4"
px={highlight ? '4' : '2'}
style={{
backdropFilter: highlight ? 'brightness(0.25)' : undefined,
WebkitBackdropFilter: highlight ? 'brightness(0.25)' : undefined,
}}
>
<img
alt={tank.name}
src={asset(`icons/tanks/big/${id}.webp`)}
width={64}
height={64}
style={{
objectFit: 'contain',
}}
/>
<Flex direction="column" align="center">
<Flex gap="2" align="center">
<Text color="gray" size="1">
{TIER_ROMAN_NUMERALS[tank.tier]}
</Text>
<Text wrap="nowrap">{tank.name}</Text>
</Flex>
<Flex gap="2" align="center">
<Text color="gray" size="1" wrap="nowrap">
<Flex gap="1" align="center">
<img
alt="XP"
src={asset('icons/currencies/xp.webp')}
style={{
width: '1em',
height: '1em',
objectFit: 'contain',
objectPosition: 'center',
}}
/>
{tank.xp === undefined || tank.tier === 1
? 0
: formatCompact(tank.xp!)}
</Flex>
</Text>
<Text color="gray" size="1" wrap="nowrap">
<Flex gap="1" align="center">
<img
alt="Silver"
src={asset('icons/currencies/silver.webp')}
style={{
width: '1em',
height: '1em',
objectFit: 'contain',
objectPosition: 'center',
}}
/>
{formatCompact(tank.price.value)}
</Flex>
</Text>
</Flex>
</Flex>
</Flex>
</Box>
</Link>
);
}
@@ -0,0 +1,145 @@
import { CaretLeftIcon, CaretRightIcon, PlusIcon } from '@radix-ui/react-icons';
import { Flex, Heading, IconButton, ScrollArea, Text } from '@radix-ui/themes';
import { use, useEffect, useMemo, useRef, useState } from 'react';
import { tankDefinitions } from '../../../../../../../core/blitzkit/tankDefinitions';
import * as Duel from '../../../../../../../stores/duel';
import { Arrow } from './components/Arrow';
import { Node } from './components/Node';
// function Ancestry({ line }: { line: Line }) {
// return (
// <Card>
// <Flex align="center">
// {line.map((node) => {
// if (typeof node === 'number') {
// return <Node key={node} id={node} />;
// } else {
// return (
// <Flex direction="column">
// {node.map((line) => (
// <Ancestry line={line} />
// ))}
// </Flex>
// );
// }
// })}
// </Flex>
// </Card>
// );
// }
type Line = number[];
export function TechTreeSection() {
const master = Duel.use((state) => state.protagonist.tank);
const awaitedTankDefinitions = use(tankDefinitions);
const container = useRef<HTMLDivElement>(null);
const lines = useMemo(() => {
function extend(line: Line): Line[] {
const root = awaitedTankDefinitions[line.at(-1)!];
if (root.ancestors === undefined || root.tier === 1) {
return [line];
} else {
if (root.ancestors.length === 1 || root.tier === 2) {
line.push(root.ancestors[0]);
return extend(line);
} else {
return root.ancestors
.map((ancestor) => {
const newLine = [...line, ancestor];
return extend(newLine);
})
.flat();
}
}
}
return extend([master.id]);
}, [master]);
const [lineIndex, setLineIndex] = useState(0);
const line = useMemo(
() => [...lines[lineIndex]].toReversed(),
[master, lineIndex],
);
if (
master.treeType !== 'researchable' ||
master.ancestors === undefined ||
master.ancestors.length === 0
) {
return null;
}
useEffect(() => {
if (!container.current) return;
container.current.scrollLeft = container.current.scrollWidth;
});
return (
<Flex
my="6"
direction="column"
align="center"
gap="4"
style={{
backgroundColor: 'var(--color-surface)',
}}
py="6"
>
<Flex gap="0" direction="column" align="center">
<Heading size="6">Tech tree</Heading>
{lines.length > 1 && (
<Flex align="center" gap="2">
<IconButton
variant="soft"
onClick={() =>
setLineIndex((lines.length + lineIndex - 1) % lines.length)
}
>
<CaretLeftIcon />
</IconButton>
<Text>
Route {lineIndex + 1} of {lines.length}
</Text>
<IconButton
variant="soft"
onClick={() => setLineIndex((lineIndex + 1) % lines.length)}
>
<CaretRightIcon />
</IconButton>
</Flex>
)}
</Flex>
<ScrollArea type="hover" scrollbars="horizontal" ref={container}>
<Flex align="center" gap="2" justify="center" p="4">
{line.map((id, index) => (
<>
{index > 0 && <Arrow />}
<Node key={id} id={id} highlight={index === line.length - 1} />
</>
))}
{master.tier < 10 && (
<>
<Arrow />
{master.successors!.map((id, index) => (
<>
{index > 0 && (
<Text color="gray">
<PlusIcon />
</Text>
)}
<Node key={id} id={id} />
</>
))}
</>
)}
</Flex>
</ScrollArea>
</Flex>
);
}