Files
blitzkit/packages/website/src/components/Tankopedia/TechTreeSection/index.tsx
T

242 lines
7.4 KiB
TypeScript
Raw Normal View History

2026-08-06 20:01:50 -05:00
import { alias, TankType } from "@blitzkit/core";
2025-09-01 22:56:39 -05:00
import { literals } from "@blitzkit/i18n/src/literals";
import { CaretLeftIcon, CaretRightIcon, PlusIcon } from "@radix-ui/react-icons";
import { Flex, Heading, IconButton, ScrollArea, Text } from "@radix-ui/themes";
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
2026-08-04 14:02:52 -05:00
import { api } from "../../../core/blitzkit/api";
2025-09-01 22:56:39 -05:00
import { useLocale } from "../../../hooks/useLocale";
import { Duel } from "../../../stores/duel";
import { Tankopedia } from "../../../stores/tankopedia";
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
import { Arrow } from "./components/Arrow";
import { Node } from "./components/Node";
2024-08-20 13:49:14 -05:00
type Line = number[];
2026-08-04 14:02:52 -05:00
const tankDefinitions = await api.tankDefinitions();
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-11-09 18:38:04 -06:00
export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
2025-01-14 14:13:00 -06:00
const { locale, strings } = useLocale();
const master = Duel.use((state) => state.protagonist.tank);
2025-09-01 22:56:39 -05:00
const xpMultiplier = Tankopedia.use((state) => state.xpMultiplier);
2024-08-20 13:49:14 -05:00
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
2026-03-31 14:11:45 -05:00
if (
root.ancestors === undefined ||
root.ancestors.length === 0 ||
root.tier === 1
) {
2024-08-20 13:49:14 -05:00
return [line];
} else {
2024-11-20 10:26:55 -06:00
if (root.ancestors.length === 1 || root.tier === 2) {
2024-09-01 23:06:40 -05:00
line.push(
root.ancestors.find(
2026-03-31 14:11:45 -05:00
(ancestor) => !tankDefinitions.tanks[ancestor].deprecated,
) ?? root.ancestors[0],
2024-09-01 23:06:40 -05:00
);
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-10-09 10:49:59 -05:00
() => (lines.length === 0 ? [master.id] : [...lines[lineIndex]].reverse()),
2026-03-31 14:11:45 -05:00
[master, lineIndex],
2024-08-20 13:49:14 -05:00
);
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-07 18:45:11 -05:00
(tankDefinitions.tanks[id].research_cost === undefined ||
2024-11-20 10:26:55 -06:00
tankDefinitions.tanks[id].tier === 1
2024-08-21 12:08:02 -05:00
? 0
2024-10-07 18:45:11 -05:00
: (tankDefinitions.tanks[id].research_cost.research_cost_type!
2024-10-07 10:26:13 -05:00
.value as number)),
2026-03-31 14:11:45 -05:00
0,
2024-08-20 13:57:22 -05:00
);
const totalCredits = line.reduce(
2026-03-31 14:11:45 -05:00
(credits, id) => credits + tankDefinitions.tanks[id].price!.value,
0,
2024-08-20 13:57:22 -05:00
);
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 (
2026-03-31 14:11:45 -05:00
master.type !== TankType.TANK_TYPE_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"
2025-09-01 22:56:39 -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">
2025-01-14 14:13:00 -06:00
<Heading size="6">
{strings.website.tools.tankopedia.tech_tree.title}
</Heading>
2024-08-20 13:49:14 -05:00
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"
2026-08-06 20:01:50 -05:00
src={alias("api", "/icons/currencies/xp.webp")}
2024-08-21 12:08:02 -05:00
style={{
2025-09-01 22:56:39 -05:00
width: "1em",
height: "1em",
objectFit: "contain",
objectPosition: "center",
2024-08-21 12:08:02 -05:00
}}
/>
2025-01-07 12:47:55 -06:00
{Math.round(totalXp / xpMultiplier).toLocaleString(locale)}
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"
2026-08-06 20:01:50 -05:00
src={alias("api", "/icons/currencies/silver.webp")}
2024-08-21 12:08:02 -05:00
style={{
2025-09-01 22:56:39 -05:00
width: "1em",
height: "1em",
objectFit: "contain",
objectPosition: "center",
2024-08-21 12:08:02 -05:00
}}
/>
2025-01-07 12:47:55 -06:00
{totalCredits.toLocaleString(locale)}
2024-08-21 12:08:02 -05:00
</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={{
2025-09-01 22:56:39 -05:00
background: "url(/assets/images/icons/xp-multiplier.png)",
backgroundSize: "contain",
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
2024-08-25 21:06:56 -05:00
opacity: selected ? 1 : 0.5,
2025-09-01 22:56:39 -05:00
cursor: "pointer",
2024-08-25 21:06:56 -05:00
}}
onClick={() => {
2025-09-01 22:56:39 -05:00
Tankopedia.mutate((draft) => {
2024-08-25 21:06:56 -05:00
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">
2025-10-02 10:15:38 -05:00
{literals(strings.common.units.x, { value: multiplier })}
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-11-09 18:38:04 -06:00
skeleton={skeleton}
2024-08-25 21:06:56 -05:00
/>
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
2026-10-05 18:18:19 +13:00
{master.successors.length > 0 && (
2024-08-20 13:49:14 -05:00
<>
<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-11-09 18:38:04 -06:00
<Node key={`${id}-node}`} id={id} skeleton={skeleton} />
2024-09-01 17:41:50 -05:00
</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>
2025-10-02 10:15:38 -05:00
{literals(strings.website.tools.tankopedia.tech_tree.route, {
index: lineIndex + 1,
count: lines.length,
})}
2024-08-20 13:57:22 -05:00
</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>
);
}