mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: multiple lines
This commit is contained in:
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user