Files
blitzkit/packages/website/src/components/Compare/CompareTable.tsx
T

590 lines
24 KiB
TypeScript
Raw Normal View History

2025-09-01 23:05:22 -05:00
import { asset, availableProvisions, permanentSkills } from "@blitzkit/core";
import { checkConsumableProvisionInclusivity } from "@blitzkit/core/src/blitzkit/checkConsumableProvisionInclusivity";
import { ComponentPlaceholderIcon } from "@radix-ui/react-icons";
import { Button, Flex, IconButton, Popover, Table } from "@radix-ui/themes";
import { times } from "lodash-es";
import { awaitableConsumableDefinitions } from "../../core/awaitables/consumableDefinitions";
import { awaitableEquipmentDefinitions } from "../../core/awaitables/equipmentDefinitions";
import { awaitableProvisionDefinitions } from "../../core/awaitables/provisionDefinitions";
import { awaitableSkillDefinitions } from "../../core/awaitables/skillDefinitions";
import type { TankCharacteristics } from "../../core/blitzkit/tankCharacteristics";
import { Var } from "../../core/radix/var";
import { useLocale } from "../../hooks/useLocale";
import { CompareEphemeral } from "../../stores/compareEphemeral";
import type { EquipmentMatrix } from "../../stores/duel";
import { BlitzkitButtonGrayIcon } from "../BlitzkitButtonGrayIcon";
import { ConsumablesManager } from "../ConsumablesManager";
import { CrewSkillManager } from "../CrewSkillManager";
import { EquipmentManager } from "../EquipmentManager";
import { ModuleManager } from "../ModuleManager";
import { ProvisionsManager } from "../ProvisionsManager";
import { StickyColumnHeaderCell } from "../StickyColumnHeaderCell";
import { StickyTableRoot } from "../StickyTableRoot";
import { Body } from "./Body";
import { InsertionMarker } from "./IntersectionMarker";
import { TankCard } from "./TankCard";
2024-07-12 20:38:19 -05:00
interface CompareTableProps {
stats: TankCharacteristics[];
}
2024-11-29 13:20:27 -06:00
const [
skillDefinitions,
equipmentDefinitions,
consumableDefinitions,
provisionDefinitions,
] = await Promise.all([
awaitableSkillDefinitions,
awaitableEquipmentDefinitions,
awaitableConsumableDefinitions,
awaitableProvisionDefinitions,
]);
2024-09-23 10:49:54 -05:00
2024-07-12 20:38:19 -05:00
export function CompareTable({ stats }: CompareTableProps) {
const crewSkills = CompareEphemeral.use((state) => state.crewSkills);
const members = CompareEphemeral.use((state) => state.members);
2025-01-16 20:14:34 -06:00
const { unwrap, strings } = useLocale();
2024-07-12 20:38:19 -05:00
return (
<StickyTableRoot
size="1"
variant="surface"
2024-10-13 20:49:02 -05:00
style={{
2025-09-01 23:05:22 -05:00
position: "absolute",
2025-10-02 10:39:36 -05:00
width: "100%",
2025-09-01 23:05:22 -05:00
height: "100%",
2025-10-02 10:39:36 -05:00
borderRadius: 0,
2024-10-13 20:49:02 -05:00
}}
2024-07-12 20:38:19 -05:00
>
<Table.Header>
<Table.Row>
2025-09-01 23:05:22 -05:00
<StickyColumnHeaderCell width="0" style={{ height: "100%" }}>
2024-07-12 20:38:19 -05:00
<Flex
style={{
2025-09-01 23:05:22 -05:00
width: "100%",
height: "100%",
2024-07-12 20:38:19 -05:00
}}
align="center"
justify="center"
>
2024-09-07 21:45:00 -05:00
<BlitzkitButtonGrayIcon opacity="0.25" width={64} height={64} />
2024-07-12 20:38:19 -05:00
<InsertionMarker index={0} />
</Flex>
</StickyColumnHeaderCell>
{members.map(({ tank, key }, index) => {
return <TankCard index={index} key={key} tank={tank} />;
})}
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
2025-09-01 23:05:22 -05:00
<Table.Cell style={{ height: "100%" }}>
2024-07-12 20:38:19 -05:00
<Flex
style={{
2025-09-01 23:05:22 -05:00
width: "100%",
height: "100%",
2024-07-12 20:38:19 -05:00
}}
align="center"
justify="center"
>
<Popover.Root>
<Popover.Trigger>
<Button variant="ghost" radius="large">
2024-09-07 23:00:56 -05:00
<Flex direction="column" style={{ gap: 2 }}>
2024-10-05 16:44:07 -05:00
{Object.entries(skillDefinitions.classes).map(
2024-07-12 20:38:19 -05:00
([tankClass, skills]) => (
<Flex key={tankClass} style={{ gap: 2 }}>
2024-10-05 16:44:07 -05:00
{skills.skills.map((skill) => (
2024-07-12 20:38:19 -05:00
<div
key={skill}
style={{
width: 6,
height: 6,
borderRadius: 2,
backgroundColor:
crewSkills[skill] === 0
2025-09-01 23:05:22 -05:00
? Var("gray-11")
2024-07-12 20:38:19 -05:00
: permanentSkills.includes(skill)
2025-09-01 23:05:22 -05:00
? Var("crimson-11")
: Var("amber-11"),
2024-07-12 20:38:19 -05:00
}}
/>
))}
</Flex>
2025-09-01 23:05:22 -05:00
)
2024-07-12 20:38:19 -05:00
)}
</Flex>
</Button>
</Popover.Trigger>
<Popover.Content>
<CrewSkillManager
skillLevels={crewSkills}
onChange={(skills) => {
2025-09-01 23:05:22 -05:00
CompareEphemeral.mutate((draft) => {
2024-07-12 20:38:19 -05:00
draft.crewSkills = skills;
});
}}
/>
<Flex justify="end" mt="4" gap="4">
<Button
variant="ghost"
onClick={() => {
2025-09-01 23:05:22 -05:00
CompareEphemeral.mutate((draft) => {
2024-07-12 20:38:19 -05:00
Object.keys(draft.crewSkills).forEach((skill) => {
draft.crewSkills[skill] = 0;
});
});
}}
>
2025-01-16 20:14:34 -06:00
{strings.website.tools.compare.table.skills.clear}
2024-07-12 20:38:19 -05:00
</Button>
<Button
variant="ghost"
onClick={() => {
2025-09-01 23:05:22 -05:00
CompareEphemeral.mutate((draft) => {
2024-07-12 20:38:19 -05:00
Object.keys(draft.crewSkills).forEach((skill) => {
draft.crewSkills[skill] = 7;
});
});
}}
>
2025-01-16 20:14:34 -06:00
{strings.website.tools.compare.table.skills.maximize}
2024-07-12 20:38:19 -05:00
</Button>
</Flex>
</Popover.Content>
</Popover.Root>
</Flex>
</Table.Cell>
{members.map(
(
{
equipmentMatrix,
tank,
key,
provisions,
gun,
shell,
turret,
engine,
track,
consumables,
2024-11-09 19:30:50 -06:00
camouflage,
2024-07-12 20:38:19 -05:00
},
2025-09-01 23:05:22 -05:00
index
2024-07-12 20:38:19 -05:00
) => {
const equipmentPreset =
2024-10-07 18:45:11 -05:00
equipmentDefinitions.presets[tank.equipment_preset];
2024-07-12 20:38:19 -05:00
const provisionsList = availableProvisions(
tank,
gun,
2025-09-01 23:05:22 -05:00
provisionDefinitions
2024-07-12 20:38:19 -05:00
);
const consumablesList = Object.values(
2025-09-01 23:05:22 -05:00
consumableDefinitions.consumables
2024-07-12 20:38:19 -05:00
).filter((consumable) =>
2025-09-01 23:05:22 -05:00
checkConsumableProvisionInclusivity(consumable, tank, gun)
2024-07-12 20:38:19 -05:00
);
return (
<Table.Cell key={key}>
<Flex
align="center"
justify="center"
direction="column"
gap="2"
>
<Popover.Root>
<Popover.Trigger>
<Button variant="ghost">
<Flex align="center">
{(
2025-09-01 23:05:22 -05:00
["turret", "gun", "engine", "chassis"] as const
2024-07-12 20:38:19 -05:00
).map((module, index) => (
<img
2024-10-18 11:23:39 -05:00
key={module}
2024-07-12 20:38:19 -05:00
alt={module}
width={24}
height={24}
src={asset(`icons/modules/${module}.webp`)}
style={{
marginLeft: index > 0 ? -8 : 0,
2025-09-01 23:05:22 -05:00
objectFit: "contain",
2024-07-12 20:38:19 -05:00
}}
/>
))}
</Flex>
</Button>
</Popover.Trigger>
<Popover.Content>
<ModuleManager
tank={tank}
turret={turret}
gun={gun}
shell={shell}
engine={engine}
track={track}
onChange={(modules) => {
2025-09-01 23:05:22 -05:00
CompareEphemeral.mutate((draft) => {
2024-07-12 20:38:19 -05:00
draft.members[index] = {
...draft.members[index],
...modules,
};
});
}}
/>
<Flex justify="end" mt="4" gap="4">
<Button
variant="ghost"
onClick={() => {
2025-09-01 23:05:22 -05:00
CompareEphemeral.mutate((draft) => {
2024-07-12 20:38:19 -05:00
const member = draft.members[index];
member.turret = member.tank.turrets[0];
member.gun = member.turret.guns[0];
2025-08-18 12:06:14 -05:00
member.shell = member.gun.shells[0];
2024-07-12 20:38:19 -05:00
member.engine = member.tank.engines[0];
member.track = member.tank.tracks[0];
});
}}
>
2025-01-16 20:14:34 -06:00
{strings.website.tools.compare.table.modules.stock}
2024-07-12 20:38:19 -05:00
</Button>
<Button
variant="ghost"
onClick={() => {
2025-09-01 23:05:22 -05:00
CompareEphemeral.mutate((draft) => {
2024-07-12 20:38:19 -05:00
const member = draft.members[index];
member.turret = member.tank.turrets.at(-1)!;
member.gun = member.turret.guns.at(-1)!;
2025-08-18 12:06:14 -05:00
member.shell = member.gun.shells[0];
2024-07-12 20:38:19 -05:00
member.engine = member.tank.engines.at(-1)!;
member.track = member.tank.tracks.at(-1)!;
});
}}
>
2025-01-16 20:14:34 -06:00
{
strings.website.tools.compare.table.modules
.upgrade
}
2024-07-12 20:38:19 -05:00
</Button>
</Flex>
</Popover.Content>
</Popover.Root>
<Flex align="center" justify="center" gap="3">
<Popover.Root>
<Popover.Trigger>
<Button
variant="ghost"
radius="large"
style={{
2025-09-01 23:05:22 -05:00
height: "100%",
2024-07-12 20:38:19 -05:00
width: 12,
2025-09-01 23:05:22 -05:00
position: "relative",
2024-07-12 20:38:19 -05:00
}}
>
<Flex direction="column" justify="center">
{provisions.map((provision, index) => (
<img
2025-01-14 19:22:41 -06:00
alt={unwrap(
2024-10-05 16:44:07 -05:00
provisionDefinitions.provisions[provision]
2025-09-01 23:05:22 -05:00
.name
2025-01-14 19:22:41 -06:00
)}
2024-07-12 20:38:19 -05:00
key={provision}
src={asset(
2025-09-01 23:05:22 -05:00
`/icons/provisions/${provision}.webp`
2024-07-12 20:38:19 -05:00
)}
style={{
2025-09-01 23:05:22 -05:00
left: "50%",
2024-07-12 20:38:19 -05:00
/**
* max = 100% - 16px - 4px
* min = 4px
* diff = max - min
* = 100% - 16px - 4px - 4px
* = 100% - 24px
* coefficient = index / length - 1
* position = min + coefficient * diff
*/
top: `calc(4px + ${
provisions.length === 1
? 0.5
: index / (provisions.length - 1)
} * (100% - 24px))`,
2025-09-01 23:05:22 -05:00
transform: "translateX(-50%)",
position: "absolute",
2024-07-12 20:38:19 -05:00
width: 16,
height: 16,
2025-09-01 23:05:22 -05:00
objectFit: "contain",
2024-07-12 20:38:19 -05:00
}}
/>
))}
{provisions.length === 0 && (
<ComponentPlaceholderIcon />
)}
</Flex>
</Button>
</Popover.Trigger>
<Popover.Content>
<ProvisionsManager
provisions={provisionsList.map(
2025-09-01 23:05:22 -05:00
(provision) => provision.id
2024-07-12 20:38:19 -05:00
)}
selected={provisions}
2024-10-07 18:45:11 -05:00
disabled={tank.max_provisions === provisions.length}
2024-07-12 20:38:19 -05:00
onChange={(provisions) => {
2025-09-01 23:05:22 -05:00
CompareEphemeral.mutate((draft) => {
2024-07-12 20:38:19 -05:00
draft.members[index].provisions = provisions;
});
}}
/>
<Flex justify="end" mt="4">
<Button
variant="ghost"
onClick={() => {
2025-09-01 23:05:22 -05:00
CompareEphemeral.mutate((draft) => {
2024-07-12 20:38:19 -05:00
draft.members[index].provisions = [];
});
}}
>
2025-01-16 20:14:34 -06:00
{
strings.website.tools.compare.table.provisions
.clear
}
2024-07-12 20:38:19 -05:00
</Button>
</Flex>
</Popover.Content>
</Popover.Root>
<Popover.Root>
<Popover.Trigger>
<Button variant="ghost" radius="large">
<Flex gap="1" direction="column">
{times(3, (y) => (
<Flex
key={y}
style={{
gap: 6,
}}
>
{times(3, (x) => {
const equipment = equipmentMatrix[y][x];
return (
<Flex
key={x}
style={{
gap: 2,
}}
>
<div
style={{
width: 4,
height: 8,
backgroundColor:
equipment === -1
2025-09-01 23:05:22 -05:00
? Var("crimson-11")
: Var("gray-11"),
2024-07-12 20:38:19 -05:00
borderRadius: 2,
}}
/>
<div
style={{
width: 4,
height: 8,
backgroundColor:
equipment === 1
2025-09-01 23:05:22 -05:00
? Var("crimson-11")
: Var("gray-11"),
2024-07-12 20:38:19 -05:00
borderRadius: 2,
}}
/>
</Flex>
);
})}
</Flex>
))}
</Flex>
</Button>
</Popover.Trigger>
<Popover.Content>
<EquipmentManager
matrix={equipmentMatrix}
preset={equipmentPreset}
onChange={(matrix) => {
2025-09-01 23:05:22 -05:00
CompareEphemeral.mutate((draft) => {
2024-07-12 20:38:19 -05:00
draft.members[index].equipmentMatrix = matrix;
draft.sorting = undefined;
});
}}
/>
<Flex justify="end" gap="4" mt="4">
<Button
variant="ghost"
color="red"
onClick={() => {
2025-09-01 23:05:22 -05:00
CompareEphemeral.mutate((draft) => {
2024-07-12 20:38:19 -05:00
draft.members[index].equipmentMatrix = times(
3,
2025-09-01 23:05:22 -05:00
() => times(3, () => 0)
2024-07-12 20:38:19 -05:00
) as EquipmentMatrix;
draft.sorting = undefined;
});
}}
>
2025-01-16 20:14:34 -06:00
{
strings.website.tools.compare.table.equipment
.clear
}
2024-07-12 20:38:19 -05:00
</Button>
<Button
variant="ghost"
onClick={() => {
2025-09-01 23:05:22 -05:00
CompareEphemeral.mutate((draft) => {
2024-07-12 20:38:19 -05:00
draft.members.forEach((member) => {
member.equipmentMatrix =
draft.members[index].equipmentMatrix;
draft.sorting = undefined;
});
});
}}
>
2025-01-16 20:14:34 -06:00
{
strings.website.tools.compare.table.equipment
.apply_to_all
}
2024-07-12 20:38:19 -05:00
</Button>
</Flex>
</Popover.Content>
</Popover.Root>
<Popover.Root>
<Popover.Trigger>
<Button
variant="ghost"
radius="large"
style={{
2025-09-01 23:05:22 -05:00
height: "100%",
2024-07-12 20:38:19 -05:00
width: 12,
2025-09-01 23:05:22 -05:00
position: "relative",
2024-07-12 20:38:19 -05:00
}}
>
<Flex direction="column" justify="center">
{consumables.map((consumable, index) => (
<img
2025-01-14 19:22:41 -06:00
alt={unwrap(
2024-10-05 16:44:07 -05:00
consumableDefinitions.consumables[
consumable
2025-09-01 23:05:22 -05:00
].name
2025-01-14 19:22:41 -06:00
)}
2024-07-12 20:38:19 -05:00
key={consumable}
src={asset(
2025-09-01 23:05:22 -05:00
`/icons/consumables/${consumable}.webp`
2024-07-12 20:38:19 -05:00
)}
style={{
2025-09-01 23:05:22 -05:00
left: "50%",
2024-07-12 20:38:19 -05:00
/**
* max = 100% - 16px - 4px
* min = 4px
* diff = max - min
* = 100% - 16px - 4px - 4px
* = 100% - 24px
* coefficient = index / length - 1
* position = min + coefficient * diff
*/
top: `calc(4px + ${
consumables.length === 1
? 0.5
: index / (consumables.length - 1)
} * (100% - 24px))`,
2025-09-01 23:05:22 -05:00
transform: "translateX(-50%)",
position: "absolute",
2024-07-12 20:38:19 -05:00
width: 16,
height: 16,
2025-09-01 23:05:22 -05:00
objectFit: "contain",
2024-07-12 20:38:19 -05:00
}}
/>
))}
{consumables.length === 0 && (
<ComponentPlaceholderIcon />
)}
</Flex>
</Button>
</Popover.Trigger>
<Popover.Content>
<ConsumablesManager
consumables={consumablesList}
selected={consumables}
2024-08-21 19:45:33 -05:00
onConsumablesChange={(consumables) => {
2025-09-01 23:05:22 -05:00
CompareEphemeral.mutate((draft) => {
2024-07-12 20:38:19 -05:00
draft.members[index].consumables = consumables;
});
}}
2024-10-05 16:44:07 -05:00
disabled={
2024-10-07 18:45:11 -05:00
tank.max_consumables === consumables.length
2024-10-05 16:44:07 -05:00
}
2024-07-12 20:38:19 -05:00
/>
<Flex justify="end" mt="4">
<Button
variant="ghost"
onClick={() => {
2025-09-01 23:05:22 -05:00
CompareEphemeral.mutate((draft) => {
2024-07-12 20:38:19 -05:00
draft.members[index].consumables = [];
});
}}
>
2025-01-16 20:14:34 -06:00
{
strings.website.tools.compare.table.consumables
.clear
}
2024-07-12 20:38:19 -05:00
</Button>
</Flex>
</Popover.Content>
</Popover.Root>
</Flex>
2024-11-09 19:30:50 -06:00
{!tank.fixed_camouflage && (
<IconButton
onClick={() => {
2025-09-01 23:05:22 -05:00
CompareEphemeral.mutate((draft) => {
draft.members[index].camouflage = !camouflage;
});
2024-11-09 19:30:50 -06:00
}}
2025-09-01 23:05:22 -05:00
variant={camouflage ? "solid" : "outline"}
>
<img
2025-09-01 23:05:22 -05:00
src={asset("icons/camo.webp")}
style={{
2025-09-01 23:05:22 -05:00
width: "1em",
height: "1em",
transform: "scale(2) translate(15%, 15%)",
}}
/>
</IconButton>
)}
2024-07-12 20:38:19 -05:00
</Flex>
</Table.Cell>
);
2025-09-01 23:05:22 -05:00
}
2024-07-12 20:38:19 -05:00
)}
</Table.Row>
</Table.Body>
<Body stats={stats} />
</StickyTableRoot>
);
}