port antagonist control

This commit is contained in:
tresabhi committed 2026-10-27 18:39:56 -05:00
1 parent 45811b5c18
commit d251c31b92
8 files changed
+293 -282

No files matched your search

+10
View File
@@ -20,6 +20,16 @@
}, },
"tanks": { "tanks": {
"custom_shell": {
"type": "Type",
"caliber": "Caliber",
"penetration": "Penetration",
"normalization": "Normalization",
"ricochet": "Ricochet",
"exit": "Exit",
"reset": "Reset"
},
"tabs": { "tabs": {
"model": "Model", "model": "Model",
"dynamic_armor": "Dynamic armor", "dynamic_armor": "Dynamic armor",
@@ -0,0 +1,19 @@
.antagonist-control {
display: flex;
flex-direction: column;
justify-content: end;
gap: var(--space-4);
transition: var(--duration-brief);
position: absolute;
top: 50%;
right: var(--space-4);
transform: translate(calc(100% + var(--space-4)), -50%);
&[data-visible="true"] {
transform: translate(0, -50%);
}
}
@@ -0,0 +1,221 @@
import { alias, romanize, uniqueGuns } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n";
import { Popover } from "radix-ui";
import { useMemo } from "react";
import { DuelModule, useDuel } from "../../hooks/useDuel";
import { useEquipment } from "../../hooks/useEquipment";
import { useStrings } from "../../hooks/useStrings";
import { useUnwrapper } from "../../hooks/useUnwrapper";
import { Tankopedia } from "../../stores/tankopedia";
import { TankopediaDisplay } from "../../stores/tankopediaPersistent/constants";
import type { ComputedCharacteristics } from "../../types/characteristics";
import { EqualIcon } from "../EqualIcon";
import { IconButton } from "../IconButton";
import { CustomShellButton } from "../TankopediaOptions/components/CustomShellButton";
import { Text } from "../Text";
import styles from "./index.module.css";
interface TankopediaAntagonistControlProps {
antagonistCharacteristics: ComputedCharacteristics;
}
export function TankopediaAntagonistControl({
antagonistCharacteristics,
}: TankopediaAntagonistControlProps) {
const strings = useStrings();
const unwrap = useUnwrapper();
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const hasCustomShell = Tankopedia.use(
(state) => state.customShell !== undefined,
);
const equalize = Tankopedia.use((state) => state.environment.equalize);
const hasCalibratedShells = useEquipment("protagonist", 103);
const hasEnhancedArmor = useEquipment("protagonist", 110);
const tank = useDuel("antagonist", DuelModule.Tank);
const gun = useDuel("antagonist", DuelModule.Gun);
const shell = useDuel("antagonist", DuelModule.Shell);
const antagonistUniqueGuns = useMemo(
() => uniqueGuns(tank.turrets),
[tank.turrets],
);
const isVisible = requestedDisplay === TankopediaDisplay.DynamicArmor;
return (
<div className={styles["antagonist-control"]} data-visible={isVisible}>
<Text lowContrast>
{literals(strings.common.units.mm, {
value: (
antagonistCharacteristics.get("penetration") as number
).toFixed(0),
})}
</Text>
{antagonistUniqueGuns.length > 1 && (
<Popover.Root>
<Popover.Trigger>
<IconButton
radius="full"
variant="soft"
color="gray"
size={{ initial: "2", sm: "3" }}
style={{ position: "relative" }}
>
<Text
size="1"
color="gray"
style={{
position: "absolute",
right: 0,
bottom: 0,
}}
mr="2"
mb="1"
>
{romanize(gun.tier)}
</Text>
<img
alt="Antagonist Gun"
src={alias("api", "/icons/modules/gun.webp")}
style={{
width: "65%",
height: "65%",
objectFit: "contain",
}}
/>
</IconButton>
</Popover.Trigger>
<Popover.Content side="left" align="center">
<div>
{[...antagonistUniqueGuns].reverse().map(({ gun, turret }) => (
<IconButton key={gun.id}>
<img src={alias("api", "/icons/modules/gun.webp")} />
</IconButton>
// <ModuleButton
// module="gun"
// key={gun.id}
// onClick={() => {
// Tankopedia.mutate((draft) => {
// draft.antagonist.turret = turret.id;
// draft.antagonist.gun = gun.id;
// draft.antagonist.shell = gun.shells[0].id;
// });
// }}
// selected={gun.id === antagonistGun.id}
// discriminator={romanize(gun.tier)}
// secondaryDiscriminator={
// <Text style={{ fontSize: "0.75em" }}>
// {literals(strings.common.units.mm, {
// value: gun.shells[0].caliber.toFixed(0),
// })}
// </Text>
// }
// />
))}
</div>
</Popover.Content>
</Popover.Root>
)}
<div>
{gun.shells.map((thisShell) => (
<IconButton
key={thisShell.id}
selected={thisShell.id === shell.id && !hasCustomShell}
alt={unwrap(thisShell.name!)}
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.antagonist.shell = thisShell.id;
draft.shot = undefined;
draft.customShell = undefined;
});
}}
/>
))}
<CustomShellButton />
</div>
<div
direction="column"
style={{
pointerEvents: "auto",
borderRadius: "var(--radius-full)",
}}
overflow="hidden"
>
<IconButton
color={hasCalibratedShells ? undefined : "gray"}
variant="soft"
size={{ initial: "2", sm: "3" }}
radius="none"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.shot = undefined;
if (hasCalibratedShells) {
delete draft.protagonist.equipment[0];
} else {
draft.antagonist.equipment[0] = 1;
}
});
}}
>
<img
src={alias("api", "/icons/equipment/103.webp")}
style={{
width: "50%",
height: "50%",
}}
/>
</IconButton>
<IconButton
color={hasEnhancedArmor ? undefined : "gray"}
variant="soft"
size={{ initial: "2", sm: "3" }}
radius="none"
onClick={() => {
Tankopedia.mutate((draft) => {
if (hasEnhancedArmor) {
delete draft.protagonist.equipment[4];
} else {
draft.antagonist.equipment[4] = 0;
}
draft.shot = undefined;
});
}}
>
<img
alt="Enhanced Armor"
src={alias("api", "/icons/equipment/110.webp")}
style={{
width: "50%",
height: "50%",
}}
/>
</IconButton>
</div>
<IconButton
variant="soft"
size={{ initial: "2", sm: "3" }}
color={equalize ? undefined : "gray"}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.environment.equalize = !draft.environment.equalize;
draft.shot = undefined;
});
}}
>
<EqualIcon />
</IconButton>
</div>
);
}
@@ -12,15 +12,15 @@ import {
} from "@radix-ui/themes"; } from "@radix-ui/themes";
import { throttle } from "lodash-es"; import { throttle } from "lodash-es";
import { type ChangeEvent } from "react"; import { type ChangeEvent } from "react";
import { Duel } from "../../../../../../stores/duel"; import { DuelModule, useDuel } from "../../../hooks/useDuel";
import { useLocale } from "../../../hooks/useLocale"; import { useStrings } from "../../../hooks/useStrings";
import { Tankopedia } from "../../../stores/tankopedia"; import { Tankopedia } from "../../../stores/tankopedia";
export function CustomShellButton() { export function CustomShellButton() {
const customShell = Tankopedia.use((state) => state.customShell); const customShell = Tankopedia.use((state) => state.customShell);
const gun = Duel.use((state) => state.protagonist.gun); const gun = useDuel("protagonist", DuelModule.Gun);
const antagonistShell = Duel.use((state) => state.antagonist.shell); const antagonistShell = useDuel("antagonist", DuelModule.Shell);
const { strings } = useLocale(); const strings = useStrings();
return ( return (
<Popover.Root <Popover.Root
@@ -55,12 +55,7 @@ export function CustomShellButton() {
<> <>
<Flex direction="column" gap="2"> <Flex direction="column" gap="2">
<Flex align="center" justify="between" gap="4"> <Flex align="center" justify="between" gap="4">
<Text> <Text>{strings.tanks.custom_shell.type}</Text>
{
strings.website.tools.tankopedia.sandbox.dynamic
.custom_shell.type
}
</Text>
<Select.Root <Select.Root
value={`${customShell.type}`} value={`${customShell.type}`}
onValueChange={(value) => { onValueChange={(value) => {
@@ -121,12 +116,7 @@ export function CustomShellButton() {
</Flex> </Flex>
<Flex align="center" justify="between" gap="4"> <Flex align="center" justify="between" gap="4">
<Text> <Text>{strings.tanks.custom_shell.caliber}</Text>
{
strings.website.tools.tankopedia.sandbox.dynamic
.custom_shell.caliber
}
</Text>
<TextField.Root <TextField.Root
variant="classic" variant="classic"
style={{ maxWidth: "6rem" }} style={{ maxWidth: "6rem" }}
@@ -157,12 +147,7 @@ export function CustomShellButton() {
</Flex> </Flex>
<Flex align="center" justify="between" gap="4"> <Flex align="center" justify="between" gap="4">
<Text> <Text>{strings.tanks.custom_shell.penetration}</Text>
{
strings.website.tools.tankopedia.sandbox.dynamic
.custom_shell.penetration
}
</Text>
<TextField.Root <TextField.Root
variant="classic" variant="classic"
style={{ maxWidth: "6rem" }} style={{ maxWidth: "6rem" }}
@@ -197,12 +182,7 @@ export function CustomShellButton() {
customShell.type === ShellType.SHELL_TYPE_APCR) && ( customShell.type === ShellType.SHELL_TYPE_APCR) && (
<> <>
<Flex align="center" justify="between" gap="4" py="1"> <Flex align="center" justify="between" gap="4" py="1">
<Text> <Text>{strings.tanks.custom_shell.normalization}</Text>
{
strings.website.tools.tankopedia.sandbox.dynamic
.custom_shell.normalization
}
</Text>
<Flex <Flex
align="center" align="center"
@@ -234,12 +214,7 @@ export function CustomShellButton() {
</Flex> </Flex>
<Flex align="center" justify="between" gap="4" py="1"> <Flex align="center" justify="between" gap="4" py="1">
<Text> <Text>{strings.tanks.custom_shell.ricochet}</Text>
{
strings.website.tools.tankopedia.sandbox.dynamic
.custom_shell.ricochet
}
</Text>
<Flex align="center" gap="2" flexGrow="1" maxWidth="15rem"> <Flex align="center" gap="2" flexGrow="1" maxWidth="15rem">
<Text color="gray"> <Text color="gray">
@@ -300,10 +275,7 @@ export function CustomShellButton() {
<Flex justify="end" mt="6" gap="2"> <Flex justify="end" mt="6" gap="2">
<Popover.Close> <Popover.Close>
<Button color="red" variant="outline"> <Button color="red" variant="outline">
{ {strings.tanks.custom_shell.exit}
strings.website.tools.tankopedia.sandbox.dynamic
.custom_shell.exit
}
</Button> </Button>
</Popover.Close> </Popover.Close>
<Button <Button
@@ -314,10 +286,7 @@ export function CustomShellButton() {
}); });
}} }}
> >
{ {strings.tanks.custom_shell.reset}
strings.website.tools.tankopedia.sandbox.dynamic.custom_shell
.reset
}
</Button> </Button>
</Flex> </Flex>
</> </>
@@ -1,28 +1,7 @@
import { alias, romanize, uniqueGuns } from "@blitzkit/core"; import { alias, romanize, uniqueGuns } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n/src/literals"; import { literals } from "@blitzkit/i18n/src/literals";
import { import { Flex, IconButton, Popover, Text } from "@radix-ui/themes";
Crosshair1Icon, import { useState, type RefObject } from "react";
EnterFullScreenIcon,
ExitFullScreenIcon,
EyeOpenIcon,
ShadowInnerIcon,
ShadowNoneIcon,
SunIcon,
} from "@radix-ui/react-icons";
import {
Button,
Dialog,
DropdownMenu,
Flex,
IconButton,
Popover,
SegmentedControl,
Tabs,
Text,
Tooltip,
} from "@radix-ui/themes";
import { Suspense, useState, type RefObject } from "react";
import { Pose, poseEvent } from "../../core/blitzkit/pose";
import { DuelModule, useDuel } from "../../hooks/useDuel"; import { DuelModule, useDuel } from "../../hooks/useDuel";
import { useEquipment } from "../../hooks/useEquipment"; import { useEquipment } from "../../hooks/useEquipment";
import { useFullScreen } from "../../hooks/useFullScreen"; import { useFullScreen } from "../../hooks/useFullScreen";
@@ -32,25 +11,26 @@ import { useUnwrapper } from "../../hooks/useUnwrapper";
import { Tankopedia } from "../../stores/tankopedia"; import { Tankopedia } from "../../stores/tankopedia";
import { TankopediaPersistent } from "../../stores/tankopediaPersistent"; import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
import { TankopediaDisplay } from "../../stores/tankopediaPersistent/constants"; import { TankopediaDisplay } from "../../stores/tankopediaPersistent/constants";
import type { ComputedCharacteristics } from "../../types/characteristics";
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps"; import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
import { EqualIcon } from "../EqualIcon"; import { EqualIcon } from "../EqualIcon";
import { ModuleButton } from "../ModuleButtons/ModuleButton"; import { ModuleButton } from "../ModuleButtons/ModuleButton";
import { ScreenshotButton } from "../ScreenshotButton";
import { SmallTankIcon } from "../SmallTankIcon";
import { import {
SidebarIconButton, SidebarIconButton,
SidebarIconGroup, SidebarIconGroup,
} from "../TankopediaSidebarIconButtons"; } from "../TankopediaSidebarIconButtons";
import { TankSearch } from "../TankSearch";
import { CustomShellButton } from "./components/CustomShellButton"; import { CustomShellButton } from "./components/CustomShellButton";
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
import { SpallLinerSwitcher } from "./components/SpallLinerSwitcher";
type OptionsProps = MaybeSkeletonComponentProps & { type OptionsProps = MaybeSkeletonComponentProps & {
canvas: RefObject<HTMLCanvasElement>; canvas: RefObject<HTMLCanvasElement>;
antagonistCharacteristics: ComputedCharacteristics;
}; };
export function TankopediaOptions({ canvas, skeleton }: OptionsProps) { export function TankopediaOptions({
canvas,
skeleton,
antagonistCharacteristics,
}: OptionsProps) {
const strings = useStrings(); const strings = useStrings();
const unwrap = useUnwrapper(); const unwrap = useUnwrapper();
@@ -80,206 +60,9 @@ export function TankopediaOptions({ canvas, skeleton }: OptionsProps) {
return ( return (
<> <>
<Flex
gap="2"
direction="column"
top="50%"
right={
requestedDisplay === TankopediaDisplay.DynamicArmor ? "3" : "-4rem"
}
style={{
position: "absolute",
transform: "translateY(-50%)",
transitionDuration: "200ms",
}}
align="end"
>
{!hasCustomShell && (
<Text color="gray" size={{ initial: "1", sm: "2" }}>
{/* {literals(strings.common.units.mm, {
value: (
resolvePenetrationCoefficient(
hasCalibratedShells,
equalize,
antagonistShell.type,
equalizer,
) * antagonistShell.penetration!.near
).toFixed(0),
})} */}
what
</Text>
)}
{antagonistUniqueGuns.length > 1 && ( {/* <Flex
<Popover.Root>
<Popover.Trigger>
<IconButton
radius="full"
variant="soft"
color="gray"
size={{ initial: "2", sm: "3" }}
style={{ position: "relative" }}
>
<Text
size="1"
color="gray"
style={{
position: "absolute",
right: 0,
bottom: 0,
}}
mr="2"
mb="1"
>
{romanize(antagonistGun.tier)}
</Text>
<img
alt="Antagonist Gun"
src={alias("api", "/icons/modules/gun.webp")}
style={{
width: "65%",
height: "65%",
objectFit: "contain",
}}
/>
</IconButton>
</Popover.Trigger>
<Popover.Content side="left" align="center">
<Flex gap="2">
{[...antagonistUniqueGuns].reverse().map(({ gun, turret }) => (
<ModuleButton
module="gun"
key={gun.id}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.antagonist.turret = turret.id;
draft.antagonist.gun = gun.id;
draft.antagonist.shell = gun.shells[0].id;
});
}}
selected={gun.id === antagonistGun.id}
discriminator={romanize(gun.tier)}
secondaryDiscriminator={
<Text style={{ fontSize: "0.75em" }}>
{literals(strings.common.units.mm, {
value: gun.shells[0].caliber.toFixed(0),
})}
</Text>
}
/>
))}
</Flex>
</Popover.Content>
</Popover.Root>
)}
<SidebarIconGroup>
{antagonistGun.shells.map((thisShell) => (
<SidebarIconButton
key={thisShell.id}
selected={thisShell.id === antagonistShell.id && !hasCustomShell}
alt={unwrap(thisShell.name!)}
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.antagonist.shell = thisShell.id;
draft.shot = undefined;
draft.customShell = undefined;
});
}}
/>
))}
<CustomShellButton />
</SidebarIconGroup>
<Flex
direction="column"
style={{
pointerEvents: "auto",
borderRadius: "var(--radius-full)",
}}
overflow="hidden"
>
<IconButton
color={hasCalibratedShells ? undefined : "gray"}
variant="soft"
size={{ initial: "2", sm: "3" }}
radius="none"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.shot = undefined;
if (hasCalibratedShells) {
delete draft.protagonist.equipment[0];
} else {
draft.antagonist.equipment[0] = 1;
}
});
}}
>
<img
alt="Calibrated Shells"
src={alias("api", "/icons/equipment/103.webp")}
style={{
width: "50%",
height: "50%",
}}
/>
</IconButton>
<IconButton
color={hasEnhancedArmor ? undefined : "gray"}
variant="soft"
size={{ initial: "2", sm: "3" }}
radius="none"
onClick={() => {
Tankopedia.mutate((draft) => {
if (hasEnhancedArmor) {
delete draft.protagonist.equipment[4];
} else {
draft.antagonist.equipment[4] = 0;
}
draft.shot = undefined;
});
}}
>
<img
alt="Enhanced Armor"
src={alias("api", "/icons/equipment/110.webp")}
style={{
width: "50%",
height: "50%",
}}
/>
</IconButton>
<SpallLinerSwitcher />
</Flex>
{!skeleton && modelRequested && (
<Suspense>
<DynamicArmorSwitcher />
</Suspense>
)}
<IconButton
variant="soft"
size={{ initial: "2", sm: "3" }}
color={equalize ? undefined : "gray"}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.environment.equalize = !draft.environment.equalize;
draft.shot = undefined;
});
}}
>
<EqualIcon />
</IconButton>
</Flex>
<Flex
direction="column" direction="column"
align="center" align="center"
position="absolute" position="absolute"
@@ -407,7 +190,7 @@ export function TankopediaOptions({ canvas, skeleton }: OptionsProps) {
}} }}
> >
<SegmentedControl.Item value={`${TankopediaDisplay.Model}`}> <SegmentedControl.Item value={`${TankopediaDisplay.Model}`}>
<Tooltip content={strings.tanks.tabs.model}> <Tooltip tooltip={strings.tanks.tabs.model}>
<Flex height="100%" align="center"> <Flex height="100%" align="center">
<img <img
src="/assets/images/icons/tankopedia-model.png" src="/assets/images/icons/tankopedia-model.png"
@@ -417,7 +200,7 @@ export function TankopediaOptions({ canvas, skeleton }: OptionsProps) {
</Tooltip> </Tooltip>
</SegmentedControl.Item> </SegmentedControl.Item>
<SegmentedControl.Item value={`${TankopediaDisplay.DynamicArmor}`}> <SegmentedControl.Item value={`${TankopediaDisplay.DynamicArmor}`}>
<Tooltip content={strings.tanks.tabs.dynamic_armor}> <Tooltip tooltip={strings.tanks.tabs.dynamic_armor}>
<Flex height="100%" align="center"> <Flex height="100%" align="center">
<img <img
src="/assets/images/icons/tankopedia-dynamic-armor.png" src="/assets/images/icons/tankopedia-dynamic-armor.png"
@@ -427,7 +210,7 @@ export function TankopediaOptions({ canvas, skeleton }: OptionsProps) {
</Tooltip> </Tooltip>
</SegmentedControl.Item> </SegmentedControl.Item>
<SegmentedControl.Item value={`${TankopediaDisplay.StaticArmor}`}> <SegmentedControl.Item value={`${TankopediaDisplay.StaticArmor}`}>
<Tooltip content={strings.tanks.tabs.model}> <Tooltip tooltip={strings.tanks.tabs.model}>
<Flex height="100%" align="center"> <Flex height="100%" align="center">
<img <img
src="/assets/images/icons/tankopedia-static-armor.png" src="/assets/images/icons/tankopedia-static-armor.png"
@@ -484,7 +267,7 @@ export function TankopediaOptions({ canvas, skeleton }: OptionsProps) {
</Dialog.Root> </Dialog.Root>
)} )}
</Flex> </Flex>
</Flex> </Flex> */}
</> </>
); );
} }
@@ -9,6 +9,7 @@ import { useModel } from "../../hooks/useModel";
import { Tankopedia } from "../../stores/tankopedia"; import { Tankopedia } from "../../stores/tankopedia";
import { TankopediaPersistent } from "../../stores/tankopediaPersistent"; import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
import { TankopediaDisplay } from "../../stores/tankopediaPersistent/constants"; import { TankopediaDisplay } from "../../stores/tankopediaPersistent/constants";
import type { ComputedCharacteristics } from "../../types/characteristics";
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps"; import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
import { ArmorPlateDisplay } from "../ArmorPlateDisplay"; import { ArmorPlateDisplay } from "../ArmorPlateDisplay";
import { CanvasAutoClear } from "../CanvasAutoClear"; import { CanvasAutoClear } from "../CanvasAutoClear";
@@ -21,17 +22,19 @@ import { ShotDisplay } from "../ShotDisplay";
import { SmartCanvas } from "../SmartCanvas"; import { SmartCanvas } from "../SmartCanvas";
import { StaticArmor, type ThicknessRange } from "../StaticArmor"; import { StaticArmor, type ThicknessRange } from "../StaticArmor";
import { TankModel } from "../TankModel"; import { TankModel } from "../TankModel";
import { TankopediaOptions } from "../TankopediaOptions"; import { TankopediaAntagonistControl } from "../TankopediaAntagonistControl";
import { TankopediaSkinSwitcher } from "../TankopediaSkinSwitcher"; import { TankopediaSkinSwitcher } from "../TankopediaSkinSwitcher";
import styles from "./index.module.css"; import styles from "./index.module.css";
type TankSandboxProps = MaybeSkeletonComponentProps & { type TankSandboxProps = MaybeSkeletonComponentProps & {
thicknessRange: ThicknessRange; thicknessRange: ThicknessRange;
antagonistCharacteristics: ComputedCharacteristics;
}; };
export function TankopediaSandbox({ export function TankopediaSandbox({
skeleton, skeleton,
thicknessRange, thicknessRange,
antagonistCharacteristics,
}: TankSandboxProps) { }: TankSandboxProps) {
const wrapper = useRef<HTMLDivElement>(null); const wrapper = useRef<HTMLDivElement>(null);
const canvas = useRef<HTMLCanvasElement>(null); const canvas = useRef<HTMLCanvasElement>(null);
@@ -172,7 +175,9 @@ export function TankopediaSandbox({
</SmartCanvas> </SmartCanvas>
<TankopediaSkinSwitcher /> <TankopediaSkinSwitcher />
<TankopediaOptions /> <TankopediaAntagonistControl
antagonistCharacteristics={antagonistCharacteristics}
/>
</div> </div>
); );
} }
@@ -191,12 +191,14 @@ export function computeCharacteristics(
return computed; return computed;
} }
export function useCharacteristics() { export function useCharacteristics(side: DuelSide) {
const protagonist = Tankopedia.use((state) => state.protagonist); const A = Tankopedia.use((state) => state[side]);
const antagonist = Tankopedia.use((state) => state.antagonist); const B = Tankopedia.use(
(state) => state[side === "antagonist" ? "protagonist" : "antagonist"],
);
const environment = Tankopedia.use((state) => state.environment); const environment = Tankopedia.use((state) => state.environment);
return computeCharacteristics({ protagonist, antagonist }, environment); return computeCharacteristics({ protagonist: A, antagonist: B }, environment);
} }
export function computeDefaultCharacteristics(tank: TankDefinition) { export function computeDefaultCharacteristics(tank: TankDefinition) {
@@ -30,7 +30,8 @@ export const Page = withErrorWrapper(
const protagonist = Tankopedia.use((state) => state.protagonist); const protagonist = Tankopedia.use((state) => state.protagonist);
const equalize = Tankopedia.use((state) => state.environment.equalize); const equalize = Tankopedia.use((state) => state.environment.equalize);
const characteristics = useCharacteristics(); const characteristics = useCharacteristics("protagonist");
const antagonistCharacteristics = useCharacteristics("antagonist");
const thicknessRange = useMemo(() => { const thicknessRange = useMemo(() => {
const entries = Object.values(tanks.tanks); const entries = Object.values(tanks.tanks);
@@ -62,6 +63,7 @@ export const Page = withErrorWrapper(
<div className={styles.sandbox}> <div className={styles.sandbox}>
<TankopediaSandbox <TankopediaSandbox
antagonistCharacteristics={antagonistCharacteristics}
skeleton={skeleton} skeleton={skeleton}
thicknessRange={thicknessRange} thicknessRange={thicknessRange}
/> />