2024-10-05 20:55:44 -05:00
|
|
|
import { asset, fetchTankDefinitions, imgur, TankType } from '@blitzkit/core';
|
2024-08-20 13:49:14 -05:00
|
|
|
import { CaretLeftIcon, CaretRightIcon, PlusIcon } from '@radix-ui/react-icons';
|
|
|
|
|
import { Flex, Heading, IconButton, ScrollArea, Text } from '@radix-ui/themes';
|
2024-09-21 20:26:58 -05:00
|
|
|
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
2024-09-22 22:04:03 -05:00
|
|
|
import { Duel } from '../../../stores/duel';
|
|
|
|
|
import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
2024-08-20 13:49:14 -05:00
|
|
|
import { Arrow } from './components/Arrow';
|
|
|
|
|
import { Node } from './components/Node';
|
|
|
|
|
|
|
|
|
|
type Line = number[];
|
|
|
|
|
|
2024-10-05 20:55:44 -05:00
|
|
|
const tankDefinitions = await fetchTankDefinitions();
|
2024-09-21 20:26:58 -05:00
|
|
|
|
2024-08-25 21:06:56 -05:00
|
|
|
export const XP_MULTIPLIERS = [1, 2, 3, 4, 5, 10];
|
|
|
|
|
|
2024-08-20 13:49:14 -05:00
|
|
|
export function TechTreeSection() {
|
2024-08-25 21:06:56 -05:00
|
|
|
const xpMultiplier = TankopediaEphemeral.use((state) => state.xpMultiplier);
|
|
|
|
|
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
2024-08-20 13:49:14 -05:00
|
|
|
const master = Duel.use((state) => state.protagonist.tank);
|
|
|
|
|
const container = useRef<HTMLDivElement>(null);
|
|
|
|
|
const lines = useMemo(() => {
|
|
|
|
|
function extend(line: Line): Line[] {
|
2024-10-05 20:55:44 -05:00
|
|
|
const root = tankDefinitions.tanks[line.at(-1)!];
|
2024-08-20 13:49:14 -05:00
|
|
|
|
|
|
|
|
if (root.ancestors === undefined || root.tier === 1) {
|
|
|
|
|
return [line];
|
|
|
|
|
} else {
|
|
|
|
|
if (root.ancestors.length === 1 || root.tier === 2) {
|
2024-09-01 23:06:40 -05:00
|
|
|
line.push(
|
|
|
|
|
root.ancestors.find(
|
2024-10-05 20:55:44 -05:00
|
|
|
(ancestor) => !tankDefinitions.tanks[ancestor].deprecated,
|
2024-09-01 23:06:40 -05:00
|
|
|
) ?? root.ancestors[0],
|
|
|
|
|
);
|
2024-08-20 13:49:14 -05:00
|
|
|
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(
|
2024-09-04 22:17:32 -05:00
|
|
|
() => [...lines[lineIndex]].reverse(),
|
2024-08-20 13:49:14 -05:00
|
|
|
[master, lineIndex],
|
|
|
|
|
);
|
2024-08-20 13:57:22 -05:00
|
|
|
const totalXp = line.reduce(
|
2024-08-21 12:08:02 -05:00
|
|
|
(xp, id) =>
|
|
|
|
|
xp +
|
2024-10-05 20:55:44 -05:00
|
|
|
(tankDefinitions.tanks[id].xp === undefined ||
|
|
|
|
|
tankDefinitions.tanks[id].tier === 1
|
2024-08-21 12:08:02 -05:00
|
|
|
? 0
|
2024-10-05 20:55:44 -05:00
|
|
|
: tankDefinitions.tanks[id].xp),
|
2024-08-20 13:57:22 -05:00
|
|
|
0,
|
|
|
|
|
);
|
|
|
|
|
const totalCredits = line.reduce(
|
2024-10-05 20:55:44 -05:00
|
|
|
(credits, id) => credits + tankDefinitions.tanks[id].price.value,
|
2024-08-20 13:57:22 -05:00
|
|
|
0,
|
|
|
|
|
);
|
2024-08-20 13:49:14 -05:00
|
|
|
|
2024-09-01 21:18:50 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!container.current) return;
|
|
|
|
|
container.current.scrollLeft = container.current.scrollWidth;
|
|
|
|
|
});
|
|
|
|
|
|
2024-08-20 13:49:14 -05:00
|
|
|
if (
|
2024-10-05 20:55:44 -05:00
|
|
|
master.type !== TankType.RESEARCHABLE ||
|
2024-08-20 13:49:14 -05:00
|
|
|
master.ancestors === undefined ||
|
|
|
|
|
master.ancestors.length === 0
|
|
|
|
|
) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Flex
|
|
|
|
|
direction="column"
|
|
|
|
|
align="center"
|
2024-08-20 13:57:22 -05:00
|
|
|
gap="0"
|
2024-08-25 18:48:57 -05:00
|
|
|
style={{ backgroundColor: 'var(--color-surface)' }}
|
2024-08-20 13:49:14 -05:00
|
|
|
py="6"
|
|
|
|
|
>
|
2024-08-20 13:57:22 -05:00
|
|
|
<Flex direction="column" align="center">
|
2024-08-20 13:49:14 -05:00
|
|
|
<Heading size="6">Tech tree</Heading>
|
|
|
|
|
|
2024-08-21 12:08:02 -05:00
|
|
|
{master.tier > 1 && (
|
|
|
|
|
<Flex gap="4" align="center">
|
|
|
|
|
<Text color="gray" 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',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2024-08-25 21:06:56 -05:00
|
|
|
{Math.round(totalXp / xpMultiplier).toLocaleString()}
|
2024-08-21 12:08:02 -05:00
|
|
|
</Flex>
|
|
|
|
|
</Text>
|
2024-08-20 13:57:22 -05:00
|
|
|
|
2024-08-21 12:08:02 -05:00
|
|
|
<Text color="gray" 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',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
{totalCredits.toLocaleString()}
|
|
|
|
|
</Flex>
|
|
|
|
|
</Text>
|
|
|
|
|
</Flex>
|
|
|
|
|
)}
|
2024-08-25 21:06:56 -05:00
|
|
|
|
|
|
|
|
<Flex gap="1" mt="2">
|
|
|
|
|
{XP_MULTIPLIERS.map((multiplier) => {
|
|
|
|
|
const selected = xpMultiplier === multiplier;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Flex
|
2024-09-01 17:41:50 -05:00
|
|
|
key={multiplier}
|
2024-08-25 21:06:56 -05:00
|
|
|
style={{
|
|
|
|
|
background: `url(${imgur('7hDltb4')})`,
|
|
|
|
|
backgroundSize: 'contain',
|
|
|
|
|
backgroundRepeat: 'no-repeat',
|
|
|
|
|
backgroundPosition: 'center',
|
|
|
|
|
opacity: selected ? 1 : 0.5,
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
}}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
mutateTankopediaEphemeral((draft) => {
|
|
|
|
|
draft.xpMultiplier = multiplier;
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
width="2.5rem"
|
|
|
|
|
justify="center"
|
|
|
|
|
align="center"
|
|
|
|
|
pt="1"
|
|
|
|
|
pl="1"
|
|
|
|
|
>
|
2024-08-25 21:09:07 -05:00
|
|
|
<Text size="2">
|
|
|
|
|
x<b>{multiplier}</b>
|
2024-08-25 21:08:41 -05:00
|
|
|
</Text>
|
2024-08-25 21:06:56 -05:00
|
|
|
</Flex>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</Flex>
|
2024-08-20 13:49:14 -05:00
|
|
|
</Flex>
|
|
|
|
|
|
|
|
|
|
<ScrollArea type="hover" scrollbars="horizontal" ref={container}>
|
|
|
|
|
<Flex align="center" gap="2" justify="center" p="4">
|
2024-08-25 21:06:56 -05:00
|
|
|
{line.map((id, index) => {
|
|
|
|
|
const last = index === line.length - 1;
|
2024-10-05 20:55:44 -05:00
|
|
|
const tank = tankDefinitions.tanks[id];
|
2024-08-25 21:06:56 -05:00
|
|
|
|
|
|
|
|
return (
|
2024-09-01 17:41:50 -05:00
|
|
|
<Fragment key={id}>
|
|
|
|
|
{index > 0 && <Arrow key={`${id}-arrow`} />}
|
2024-09-22 22:04:03 -05:00
|
|
|
|
2024-08-25 21:06:56 -05:00
|
|
|
<Node
|
2024-09-01 17:41:50 -05:00
|
|
|
key={`${id}-node`}
|
2024-08-25 21:06:56 -05:00
|
|
|
id={id}
|
|
|
|
|
nextIds={last ? tank.successors : [line[index + 1]]}
|
|
|
|
|
highlight={last}
|
|
|
|
|
/>
|
2024-09-01 17:41:50 -05:00
|
|
|
</Fragment>
|
2024-08-25 21:06:56 -05:00
|
|
|
);
|
|
|
|
|
})}
|
2024-08-20 13:49:14 -05:00
|
|
|
|
|
|
|
|
{master.tier < 10 && (
|
|
|
|
|
<>
|
|
|
|
|
<Arrow />
|
|
|
|
|
{master.successors!.map((id, index) => (
|
2024-09-01 17:41:50 -05:00
|
|
|
<Fragment key={id}>
|
2024-08-20 13:49:14 -05:00
|
|
|
{index > 0 && (
|
2024-09-01 17:41:50 -05:00
|
|
|
<Text color="gray" key={`${id}-plus`}>
|
2024-08-20 13:49:14 -05:00
|
|
|
<PlusIcon />
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
2024-09-01 17:41:50 -05:00
|
|
|
<Node key={`${id}-node}`} id={id} />
|
|
|
|
|
</Fragment>
|
2024-08-20 13:49:14 -05:00
|
|
|
))}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Flex>
|
|
|
|
|
</ScrollArea>
|
2024-08-20 13:57:22 -05:00
|
|
|
|
|
|
|
|
<Flex>
|
|
|
|
|
{lines.length > 1 && (
|
|
|
|
|
<Flex align="center" gap="2">
|
|
|
|
|
<IconButton
|
|
|
|
|
size="2"
|
|
|
|
|
variant="soft"
|
|
|
|
|
onClick={() =>
|
|
|
|
|
setLineIndex((lines.length + lineIndex - 1) % lines.length)
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<CaretLeftIcon />
|
|
|
|
|
</IconButton>
|
|
|
|
|
<Text>
|
|
|
|
|
Route {lineIndex + 1} of {lines.length}
|
|
|
|
|
</Text>
|
|
|
|
|
<IconButton
|
|
|
|
|
size="2"
|
|
|
|
|
variant="soft"
|
|
|
|
|
onClick={() => setLineIndex((lineIndex + 1) % lines.length)}
|
|
|
|
|
>
|
|
|
|
|
<CaretRightIcon />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Flex>
|
|
|
|
|
)}
|
|
|
|
|
</Flex>
|
2024-08-20 13:49:14 -05:00
|
|
|
</Flex>
|
|
|
|
|
);
|
|
|
|
|
}
|