fix imports

This commit is contained in:
tresabhi committed 2026-10-27 17:13:01 -05:00
1 parent 10f8fa28f9
commit 45811b5c18
10 files changed
+106 -110

No files matched your search

@@ -1,328 +0,0 @@
import { literals } from "@blitzkit/i18n/src/literals";
import { ShellType } from "@blitzkit/protos";
import {
Button,
Flex,
IconButton,
Popover,
Select,
Slider,
Text,
TextField,
} from "@radix-ui/themes";
import { throttle } from "lodash-es";
import { type ChangeEvent } from "react";
import { useLocale } from "../../../../../../hooks/useLocale";
import { Duel } from "../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../stores/tankopedia";
export function CustomShellButton() {
const customShell = Tankopedia.use((state) => state.customShell);
const gun = Duel.use((state) => state.protagonist.gun);
const antagonistShell = Duel.use((state) => state.antagonist.shell);
const { strings } = useLocale();
return (
<Popover.Root
onOpenChange={(open) => {
if (!open || customShell) return;
Tankopedia.mutate((draft) => {
draft.customShell = antagonistShell;
});
}}
>
<Popover.Trigger>
<IconButton
color={customShell ? undefined : "gray"}
variant="soft"
size={{ initial: "2", sm: "3" }}
radius="none"
>
<img
alt="custom shell"
src="/assets/images/icons/custom-shell.png"
style={{
width: "50%",
height: "50%",
}}
/>
</IconButton>
</Popover.Trigger>
<Popover.Content>
{customShell && (
<>
<Flex direction="column" gap="2">
<Flex align="center" justify="between" gap="4">
<Text>
{
strings.website.tools.tankopedia.sandbox.dynamic
.custom_shell.type
}
</Text>
<Select.Root
value={`${customShell.type}`}
onValueChange={(value) => {
const type = Number(value) as ShellType;
const modified = { ...customShell, type };
switch (type) {
case ShellType.SHELL_TYPE_AP: {
modified.normalization = 5;
modified.ricochet = 70;
modified.explosion_radius = undefined;
break;
}
case ShellType.SHELL_TYPE_APCR: {
modified.normalization = 2;
modified.ricochet = 70;
modified.explosion_radius = undefined;
break;
}
case ShellType.SHELL_TYPE_HEAT: {
modified.normalization = undefined;
modified.ricochet = undefined;
modified.explosion_radius = undefined;
}
case ShellType.SHELL_TYPE_HE: {
modified.normalization = undefined;
modified.ricochet = undefined;
modified.explosion_radius =
gun.shells.find(
(shell) => shell.type === ShellType.SHELL_TYPE_HE,
)?.explosion_radius ?? 1;
break;
}
}
Tankopedia.mutate((draft) => {
draft.customShell = modified;
});
}}
>
<Select.Trigger variant="classic" />
<Select.Content>
<Select.Item value={`${ShellType.SHELL_TYPE_AP}`}>
{strings.common.shells.ap}
</Select.Item>
<Select.Item value={`${ShellType.SHELL_TYPE_APCR}`}>
{strings.common.shells.ap_cr}
</Select.Item>
<Select.Item value={`${ShellType.SHELL_TYPE_HEAT}`}>
{strings.common.shells.hc}
</Select.Item>
<Select.Item value={`${ShellType.SHELL_TYPE_HE}`}>
{strings.common.shells.he}
</Select.Item>
</Select.Content>
</Select.Root>
</Flex>
<Flex align="center" justify="between" gap="4">
<Text>
{
strings.website.tools.tankopedia.sandbox.dynamic
.custom_shell.caliber
}
</Text>
<TextField.Root
variant="classic"
style={{ maxWidth: "6rem" }}
defaultValue={customShell.caliber}
type="number"
onChange={(event) => {
Tankopedia.mutate((draft) => {
draft.customShell!.caliber = event.target.valueAsNumber;
});
}}
onFocus={(event) => event.target.select()}
onKeyDown={(event) => {
event.stopPropagation();
if (event.key === "Enter") {
(event.target as HTMLInputElement).blur();
} else if (event.key === "Escape") {
(event.target as HTMLInputElement).valueAsNumber =
customShell.caliber;
(event.target as HTMLInputElement).blur();
}
}}
>
<TextField.Slot side="right">
{literals(strings.common.units.mm, { value: "" })}
</TextField.Slot>
</TextField.Root>
</Flex>
<Flex align="center" justify="between" gap="4">
<Text>
{
strings.website.tools.tankopedia.sandbox.dynamic
.custom_shell.penetration
}
</Text>
<TextField.Root
variant="classic"
style={{ maxWidth: "6rem" }}
defaultValue={customShell.penetration!.near}
type="number"
onChange={(event: ChangeEvent<HTMLInputElement>) => {
Tankopedia.mutate((draft) => {
draft.customShell!.penetration!.near =
event.target.valueAsNumber;
});
}}
onFocus={(event) => event.target.select()}
onKeyDown={(event) => {
event.stopPropagation();
if (event.key === "Enter") {
(event.target as HTMLInputElement).blur();
} else if (event.key === "Escape") {
(event.target as HTMLInputElement).valueAsNumber =
customShell.penetration!.near;
(event.target as HTMLInputElement).blur();
}
}}
>
<TextField.Slot side="right">
{literals(strings.common.units.mm, { value: "" })}
</TextField.Slot>
</TextField.Root>
</Flex>
{(customShell.type === ShellType.SHELL_TYPE_AP ||
customShell.type === ShellType.SHELL_TYPE_APCR) && (
<>
<Flex align="center" justify="between" gap="4" py="1">
<Text>
{
strings.website.tools.tankopedia.sandbox.dynamic
.custom_shell.normalization
}
</Text>
<Flex
align="center"
gap="2"
flexGrow="1"
maxWidth="15rem"
minWidth="8rem"
>
<Text color="gray">
{literals(strings.common.units.deg, {
value: (customShell.normalization ?? 0).toFixed(0),
})}
</Text>
<Slider
variant="classic"
min={0}
max={90}
step={Number.EPSILON}
defaultValue={[customShell.normalization ?? 0]}
onValueChange={([value]) => {
console.log("asd");
Tankopedia.mutate((draft) => {
draft.customShell!.normalization = value;
});
}}
/>
</Flex>
</Flex>
<Flex align="center" justify="between" gap="4" py="1">
<Text>
{
strings.website.tools.tankopedia.sandbox.dynamic
.custom_shell.ricochet
}
</Text>
<Flex align="center" gap="2" flexGrow="1" maxWidth="15rem">
<Text color="gray">
{literals(strings.common.units.deg, {
value: (customShell.ricochet ?? 90).toFixed(0),
})}
</Text>
<Slider
variant="classic"
min={0}
max={90}
step={Number.EPSILON}
defaultValue={[customShell.ricochet ?? 90]}
onValueChange={throttle(([value]) => {
Tankopedia.mutate((draft) => {
draft.customShell!.ricochet = value;
});
}, 1000)}
/>
</Flex>
</Flex>
</>
)}
{customShell.type === ShellType.SHELL_TYPE_HE && (
<Flex align="center" justify="between" gap="4">
<Text>Explosion radius</Text>
<TextField.Root
variant="classic"
style={{ maxWidth: "6rem" }}
defaultValue={customShell.explosion_radius ?? 0}
type="number"
onChange={(event: ChangeEvent<HTMLInputElement>) => {
Tankopedia.mutate((draft) => {
draft.customShell!.explosion_radius =
event.target.valueAsNumber;
});
}}
onFocus={(event) => event.target.select()}
onKeyDown={(event) => {
event.stopPropagation();
if (event.key === "Enter") {
(event.target as HTMLInputElement).blur();
} else if (event.key === "Escape") {
(event.target as HTMLInputElement).valueAsNumber =
customShell.explosion_radius ?? 1;
(event.target as HTMLInputElement).blur();
}
}}
>
<TextField.Slot side="right">m</TextField.Slot>
</TextField.Root>
</Flex>
)}
</Flex>
<Flex justify="end" mt="6" gap="2">
<Popover.Close>
<Button color="red" variant="outline">
{
strings.website.tools.tankopedia.sandbox.dynamic
.custom_shell.exit
}
</Button>
</Popover.Close>
<Button
color="red"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.customShell = antagonistShell;
});
}}
>
{
strings.website.tools.tankopedia.sandbox.dynamic.custom_shell
.reset
}
</Button>
</Flex>
</>
)}
</Popover.Content>
</Popover.Root>
);
}
@@ -1,49 +0,0 @@
import { alias } from "@blitzkit/core";
import { IconButton } from "@radix-ui/themes";
import { useArmor } from "../../../../../../hooks/useArmor";
import { Duel } from "../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../stores/tankopedia";
export function DynamicArmorSwitcher() {
const tank = Duel.use((state) => state.protagonist.tank);
const isDynamicArmorActive = Duel.use((state) =>
state.protagonist.consumables.includes(73),
);
const { hasDynamicArmor } = useArmor(tank.id);
if (!hasDynamicArmor) return null;
return (
<IconButton
color={isDynamicArmorActive ? undefined : "gray"}
variant="soft"
size={{ initial: "2", sm: "3" }}
onClick={() => {
Duel.mutate((draft) => {
if (draft.protagonist.consumables.includes(73)) {
draft.protagonist.consumables =
draft.protagonist.consumables.filter((id) => id !== 73);
} else {
if (draft.protagonist.consumables.length === tank.max_consumables) {
draft.protagonist.consumables[tank.max_consumables - 1] = 73;
} else {
draft.protagonist.consumables.push(73);
}
}
});
Tankopedia.mutate((draft) => {
draft.shot = undefined;
});
}}
>
<img
alt="Dynamic Armor"
src={alias("api", "/icons/consumables/73.webp")}
style={{
width: "50%",
height: "50%",
}}
/>
</IconButton>
);
}
@@ -1,8 +0,0 @@
.quick-inputs {
left: -5rem;
}
.quick-inputs:focus-within,
.tank-sandbox-container:hover .quick-inputs {
left: 1rem;
}
@@ -1,160 +0,0 @@
import { HeightIcon, WidthIcon } from "@radix-ui/react-icons";
import { Flex, TextField } from "@radix-ui/themes";
import type { QuicklimeEvent } from "quicklime";
import { useEffect, useRef } from "react";
import { degToRad, radToDeg } from "three/src/math/MathUtils.js";
import { applyPitchYawLimits } from "../../../../../../../core/blitz/applyPitchYawLimits";
import {
type ModelTransformEventData,
modelTransformEvent,
} from "../../../../../../../core/blitzkit/modelTransform";
import { useEquipment } from "../../../../../../../hooks/useEquipment";
import { useTankModelDefinition } from "../../../../../../../hooks/useTankModelDefinition";
import { Duel } from "../../../../../../../stores/duel";
import "./index.css";
export function QuickInputs() {
const protagonist = Duel.use((state) => state.protagonist);
const yawInput = useRef<HTMLInputElement>(null);
const pitchInput = useRef<HTMLInputElement>(null);
const tankModelDefinition = useTankModelDefinition();
const turretModelDefinition =
tankModelDefinition.turrets[protagonist.turret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
const initialTurretPitch =
tankModelDefinition.initial_turret_rotation?.pitch ?? 0;
const hasImprovedVerticalStabilizer = useEquipment("protagonist", 122);
const hasDownImprovedVerticalStabilizer = useEquipment("protagonist", 124);
function displayPitch(pitch: number) {
return radToDeg(pitch) - initialTurretPitch;
}
useEffect(() => {
function syncInputValues(data: ModelTransformEventData) {
if (!pitchInput.current || !yawInput.current) return;
pitchInput.current.value = displayPitch(data.pitch).toFixed(1);
yawInput.current.value = radToDeg(data.yaw).toFixed(1);
}
function handleTransformEvent(
event: QuicklimeEvent<ModelTransformEventData>,
) {
syncInputValues(event.data);
}
modelTransformEvent.on(handleTransformEvent);
syncInputValues(modelTransformEvent.last!);
return () => {
modelTransformEvent.off(handleTransformEvent);
};
}, [initialTurretPitch]);
return (
<Flex
align="center"
gap="2"
width="4rem"
direction="column"
style={{
pointerEvents: "auto",
position: "absolute",
top: "50%",
transform: "translateY(-50%)",
transitionDuration: "0.2s",
}}
className="quick-inputs"
>
<TextField.Root
variant="classic"
size="1"
radius="full"
style={{ flex: 1, textAlign: "right" }}
defaultValue={radToDeg(modelTransformEvent.last!.yaw).toFixed(1)}
onBlur={() => {
const value = Number(yawInput.current!.value);
if (isNaN(value)) {
yawInput.current!.value = radToDeg(
modelTransformEvent.last!.yaw,
).toFixed(1);
return;
}
const [pitch, yaw] = applyPitchYawLimits(
modelTransformEvent.last!.pitch,
degToRad(value),
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
yawInput.current!.value = radToDeg(
modelTransformEvent.last!.yaw,
).toFixed(1);
}}
onKeyDown={(event) => {
if (event.key === "Enter") {
yawInput.current?.blur();
}
}}
onFocus={() => yawInput.current?.focus()}
ref={yawInput}
>
<TextField.Slot style={{ userSelect: "none" }}>
<WidthIcon />
</TextField.Slot>
<TextField.Slot />
</TextField.Root>
<TextField.Root
variant="classic"
size="1"
radius="full"
style={{ flex: 1, textAlign: "right" }}
defaultValue={displayPitch(modelTransformEvent.last!.pitch).toFixed(1)}
onBlur={() => {
const value = Number(pitchInput.current!.value);
if (isNaN(value)) {
pitchInput.current!.value = displayPitch(
modelTransformEvent.last!.pitch,
).toFixed(1);
return;
}
const [pitch, yaw] = applyPitchYawLimits(
degToRad(value + initialTurretPitch),
modelTransformEvent.last!.yaw,
gunModelDefinition.pitch!,
turretModelDefinition.yaw,
hasImprovedVerticalStabilizer,
hasDownImprovedVerticalStabilizer,
);
modelTransformEvent.dispatch({ pitch, yaw });
pitchInput.current!.value = displayPitch(
modelTransformEvent.last!.pitch,
).toFixed(1);
}}
onKeyDown={(event) => {
if (event.key === "Enter") {
pitchInput.current?.blur();
}
}}
onFocus={() => pitchInput.current?.focus()}
ref={pitchInput}
>
<TextField.Slot
style={{
userSelect: "none",
}}
>
<HeightIcon />
</TextField.Slot>
<TextField.Slot />
</TextField.Root>
</Flex>
);
}
@@ -1,128 +0,0 @@
import { alias, availableProvisions } from "@blitzkit/core";
import { ShuffleIcon } from "@radix-ui/react-icons";
import { Flex, IconButton, Popover } from "@radix-ui/themes";
import { api } from "../../../../../../core/blitzkit/api";
import { SPALL_LINER_PROVISION_ID } from "../../../../../../core/blitzkit/spallLiner";
import { useLocale } from "../../../../../../hooks/useLocale";
import { Duel } from "../../../../../../stores/duel";
import { Tankopedia } from "../../../../../../stores/tankopedia";
const provisionDefinitions = await api.provisionDefinitions();
export function SpallLinerSwitcher() {
const { tank, gun, provisions } = Duel.use((state) => state.protagonist);
const { unwrap } = useLocale();
const isSpallLinerActive = provisions.includes(SPALL_LINER_PROVISION_ID);
const hasSlotAvailable = provisions.length < tank.max_provisions;
const canEquipSpallLiner = availableProvisions(
tank,
gun,
provisionDefinitions,
).some((provision) => provision.id === SPALL_LINER_PROVISION_ID);
if (!canEquipSpallLiner) return null;
function equip(replacing?: number) {
Duel.mutate((draft) => {
if (replacing !== undefined) {
const provisions = draft.protagonist.provisions;
provisions[provisions.indexOf(replacing)] = SPALL_LINER_PROVISION_ID;
} else {
draft.protagonist.provisions.push(SPALL_LINER_PROVISION_ID);
}
});
Tankopedia.mutate((draft) => {
draft.shot = undefined;
});
}
function unequip() {
Duel.mutate((draft) => {
draft.protagonist.provisions = draft.protagonist.provisions.filter(
(id) => id !== SPALL_LINER_PROVISION_ID,
);
});
Tankopedia.mutate((draft) => {
draft.shot = undefined;
});
}
const button = (
<IconButton
color={isSpallLinerActive ? undefined : "gray"}
variant="soft"
size={{ initial: "2", sm: "3" }}
radius="medium"
onClick={() => {
if (isSpallLinerActive) {
unequip();
} else if (hasSlotAvailable) {
equip();
}
}}
>
<img
alt={unwrap(
provisionDefinitions.provisions[SPALL_LINER_PROVISION_ID].name!,
)}
src={alias("api", `/icons/provisions/${SPALL_LINER_PROVISION_ID}.webp`)}
style={{
width: "50%",
height: "50%",
}}
/>
</IconButton>
);
if (isSpallLinerActive || hasSlotAvailable) return button;
return (
<Popover.Root>
<Popover.Trigger>{button}</Popover.Trigger>
<Popover.Content>
<Flex gap="2" align="center">
<img
alt={unwrap(
provisionDefinitions.provisions[SPALL_LINER_PROVISION_ID].name!,
)}
src={alias(
"api",
`/icons/provisions/${SPALL_LINER_PROVISION_ID}.webp`,
)}
style={{
width: "1.25em",
height: "1.25em",
}}
/>
<ShuffleIcon color="var(--gray-9)" />
<Flex gap="2">
{provisions.map((id) => (
<Popover.Close key={id}>
<IconButton
color="gray"
variant="soft"
size="3"
radius="medium"
onClick={() => equip(id)}
>
<img
alt={unwrap(provisionDefinitions.provisions[id].name!)}
src={alias("api", `/icons/provisions/${id}.webp`)}
style={{
width: "50%",
height: "50%",
}}
/>
</IconButton>
</Popover.Close>
))}
</Flex>
</Flex>
</Popover.Content>
</Popover.Root>
);
}
@@ -1,204 +0,0 @@
import { literals } from "@blitzkit/i18n/src/literals";
import { EyeClosedIcon, EyeOpenIcon, ReloadIcon } from "@radix-ui/react-icons";
import { Button, Checkbox, Flex, Text } from "@radix-ui/themes";
import { Suspense } from "react";
import { useLocale } from "../../../../../../hooks/useLocale";
import { Tankopedia } from "../../../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../../../stores/tankopediaPersistent";
import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
import type { MaybeSkeletonComponentProps } from "../../../../../../types/maybeSkeletonComponentProps";
import type { ThicknessRange } from "../../../../../StaticArmor";
import { DynamicArmorSwitcher } from "./DynamicArmorSwitcher";
type ThicknessesProps = MaybeSkeletonComponentProps & {
thicknessRange: ThicknessRange;
};
export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
const display = Tankopedia.use((state) => state.display);
const showExternalModules = TankopediaPersistent.use(
(state) => state.showExternalModules
);
const showSpacedArmor = TankopediaPersistent.use(
(state) => state.showSpacedArmor
);
const showPrimaryArmor = TankopediaPersistent.use(
(state) => state.showPrimaryArmor
);
const editStatic = Tankopedia.use((state) => state.editStatic);
const { strings } = useLocale();
return (
<Flex
position="absolute"
right="0"
top="50%"
mr="4"
style={{
transform: `translate(${display === TankopediaDisplay.StaticArmor ? "0" : "calc(100% + var(--space-4))"}, -50%)`,
userSelect: "none",
transitionDuration: "200ms",
}}
direction="column"
gap="5"
align="end"
>
<Flex direction="column" align="end" gap="3">
<Flex
height="64px"
gap="2"
style={{ opacity: showPrimaryArmor ? 1 : 0.5, cursor: "pointer" }}
onClick={() => {
TankopediaPersistent.mutate((draft) => {
draft.showPrimaryArmor = !draft.showPrimaryArmor;
});
Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined;
});
}}
>
<Flex direction="column" align="end" justify="between">
<Text color="gray" size="1">
{literals(strings.common.units.mm, {
value: (thicknessRange.value * 1.5).toFixed(0),
})}
</Text>
<Text size="1">
{strings.website.tools.tankopedia.sandbox.static.primary}
</Text>
<Text color="gray" size="1">
{literals(strings.common.units.mm, { value: 0 })}
</Text>
</Flex>
<Flex
align="center"
width="1rem"
style={{
borderRadius: "var(--radius-full)",
overflow: "hidden",
background: `linear-gradient(rgb(64, 0, 0), rgb(255, 0, 0) 33%, rgb(255, 255, 0) 66%, rgb(0, 255, 0))`,
color: "black",
}}
>
{showPrimaryArmor ? <EyeOpenIcon /> : <EyeClosedIcon />}
</Flex>
</Flex>
<Flex
height="64px"
gap="2"
style={{ opacity: showSpacedArmor ? 1 : 0.5, cursor: "pointer" }}
onClick={() => {
TankopediaPersistent.mutate((draft) => {
draft.showSpacedArmor = !draft.showSpacedArmor;
});
Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined;
});
}}
>
<Flex direction="column" align="end" justify="between">
<Text color="gray" size="1">
{literals(strings.common.units.mm, {
value: thicknessRange.value.toFixed(0),
})}
</Text>
<Text size="1">
{strings.website.tools.tankopedia.sandbox.static.spaced}
</Text>
<Text color="gray" size="1">
{literals(strings.common.units.mm, { value: 0 })}
</Text>
</Flex>
<Flex
align="center"
width="1rem"
style={{
borderRadius: "var(--radius-full)",
overflow: "hidden",
background: `linear-gradient(rgb(32, 0, 225), rgb(255, 0, 255))`,
}}
>
{showSpacedArmor ? <EyeOpenIcon /> : <EyeClosedIcon />}
</Flex>
</Flex>
<Flex
gap="2"
style={{ opacity: showExternalModules ? 1 : 0.5, cursor: "pointer" }}
onClick={() => {
TankopediaPersistent.mutate((draft) => {
draft.showExternalModules = !draft.showExternalModules;
});
Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined;
});
}}
>
<Flex direction="column" align="end" justify="between">
<Text size="1">
{strings.website.tools.tankopedia.sandbox.static.external}
</Text>
</Flex>
<Flex
justify="center"
width="1rem"
height="1rem"
style={{
borderRadius: "var(--radius-full)",
overflow: "hidden",
backgroundColor: "#c0c0c0",
color: "black",
}}
>
{showExternalModules ? <EyeOpenIcon /> : <EyeClosedIcon />}
</Flex>
</Flex>
</Flex>
<Flex direction="column" align="end" gap="2">
<Flex align="center" gap="2">
<Flex
gap="2"
style={{ cursor: "pointer" }}
onClick={() => {
Tankopedia.mutate((draft) => {
draft.editStatic = !draft.editStatic;
draft.highlightArmor = undefined;
});
}}
>
<Text size="1">
{strings.website.tools.tankopedia.sandbox.static.edit.label}
</Text>
<Checkbox variant="classic" size="2" checked={editStatic} />
</Flex>
</Flex>
<Button
color="red"
size="1"
variant="ghost"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.editStatic = false;
draft.model = Tankopedia.initial.model;
});
}}
>
<ReloadIcon />
{strings.website.tools.tankopedia.sandbox.static.edit.reset}
</Button>
</Flex>
{!skeleton && (
<Suspense>
<DynamicArmorSwitcher />
</Suspense>
)}
</Flex>
);
}
@@ -1,508 +0,0 @@
import {
alias,
resolvePenetrationCoefficient,
romanize,
uniqueGuns,
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n/src/literals";
import {
Crosshair1Icon,
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 { useEquipment } from "../../../../../hooks/useEquipment";
import { useFullScreen } from "../../../../../hooks/useFullScreen";
import { useFullscreenAvailability } from "../../../../../hooks/useFullscreenAvailability";
import { useLocale } from "../../../../../hooks/useLocale";
import { Duel } from "../../../../../stores/duel";
import { Tankopedia } from "../../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../../stores/tankopediaPersistent";
import { TankopediaDisplay } from "../../../../../stores/tankopediaPersistent/constants";
import type { MaybeSkeletonComponentProps } from "../../../../../types/maybeSkeletonComponentProps";
import { EqualIcon } from "../../../../EqualIcon";
import { ModuleButton } from "../../../../ModuleButtons/ModuleButton";
import { ScreenshotButton } from "../../../../ScreenshotButton";
import { SmallTankIcon } from "../../../../SmallTankIcon";
import type { ThicknessRange } from "../../../../StaticArmor";
import {
SidebarIconButton,
SidebarIconGroup,
} from "../../../../TankopediaSidebarIconButtons";
import { SkinSwitcher } from "../../../../TankopediaSkinSwitcher";
import { TankSearch } from "../../../../TankSearch";
import { CustomShellButton } from "./components/CustomShellButton";
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
import { QuickInputs } from "./components/QuickInputs";
import { SpallLinerSwitcher } from "./components/SpallLinerSwitcher";
import { Thicknesses } from "./components/Thicknesses";
type OptionsProps = MaybeSkeletonComponentProps & {
thicknessRange: ThicknessRange;
canvas: RefObject<HTMLCanvasElement>;
};
export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const hasCustomShell = Tankopedia.use(
(state) => state.customShell !== undefined,
);
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const isFullScreen = useFullScreen();
const advancedHighlighting = TankopediaPersistent.use(
(state) => state.advancedHighlighting,
);
const fullScreenAvailable = useFullscreenAvailability(true);
const protagonistTank = Duel.use((state) => state.protagonist.tank);
const antagonistGun = Duel.use((state) => state.antagonist.gun);
const antagonistShell = Duel.use((state) => state.antagonist.shell);
const [antagonistSelectorOpen, setAntagonistSelectorOpen] = useState(false);
const antagonistTank = Duel.use((state) => state.antagonist.tank);
const hasCalibratedShells = useEquipment("protagonist", 103, true);
const [tab, setTab] = useState("search");
const hasEnhancedArmor = useEquipment("protagonist", 110);
const antagonistUniqueGuns = uniqueGuns(antagonistTank.turrets);
const { strings, unwrap } = useLocale();
const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = Tankopedia.use((state) => state.disturbed);
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
const modelRequested = Tankopedia.use((state) => state.modelRequested);
const equalize = Duel.use((state) => state.equalize);
const equalizer = Duel.use((state) => state.antagonist.tank.equalizer);
return (
<>
{disturbed && <QuickInputs />}
<Thicknesses skeleton={skeleton} thicknessRange={thicknessRange} />
<SkinSwitcher />
<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),
})}
</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(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={() =>
Duel.mutate((draft) => {
draft.antagonist.turret = turret;
draft.antagonist.gun = gun;
draft.antagonist.shell = gun.shells[0];
})
}
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={() => {
Duel.mutate((draft) => {
draft.antagonist.shell = thisShell;
});
Tankopedia.mutate((draft) => {
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={() => {
Duel.mutate((draft) => {
draft.antagonist.equipmentMatrix[0][0] = hasCalibratedShells
? 0
: 1;
});
Tankopedia.mutate((draft) => {
draft.shot = undefined;
});
}}
>
<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={() => {
Duel.mutate((draft) => {
draft.protagonist.equipmentMatrix[1][1] = hasEnhancedArmor
? 0
: -1;
});
Tankopedia.mutate((draft) => {
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={() => {
Duel.mutate((draft) => {
draft.equalize = !draft.equalize;
});
Tankopedia.mutate((draft) => {
draft.shot = undefined;
});
}}
>
<EqualIcon />
</IconButton>
</Flex>
<Flex
direction="column"
align="center"
position="absolute"
bottom={revealed ? "4" : "-100%"}
left="50%"
style={{ transform: "translateX(-50%)", transitionDuration: "200ms" }}
gap="2"
>
<Flex
gap="2"
align="center"
style={{ transitionDuration: "200ms" }}
position="relative"
>
{disturbed && (
<>
{requestedDisplay === TankopediaDisplay.DynamicArmor && (
<IconButton
size="2"
highContrast
onClick={() => {
TankopediaPersistent.mutate((draft) => {
draft.advancedHighlighting = !draft.advancedHighlighting;
});
}}
color={advancedHighlighting ? undefined : "gray"}
variant={advancedHighlighting ? "solid" : "surface"}
>
{advancedHighlighting ? (
<ShadowInnerIcon />
) : (
<ShadowNoneIcon />
)}
</IconButton>
)}
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<IconButton
size="2"
variant="surface"
highContrast
color="gray"
>
<EyeOpenIcon />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item
onClick={() => poseEvent.emit(Pose.HullDown)}
>
{strings.website.tools.tankopedia.sandbox.poses.hull_down}
</DropdownMenu.Item>
<DropdownMenu.Item
onClick={() => poseEvent.emit(Pose.FaceHug)}
>
{strings.website.tools.tankopedia.sandbox.poses.face_hug}
</DropdownMenu.Item>
<DropdownMenu.Item
onClick={() => poseEvent.emit(Pose.Default)}
>
{strings.website.tools.tankopedia.sandbox.poses.default}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<IconButton
size="2"
highContrast
onClick={() => {
TankopediaPersistent.mutate((draft) => {
draft.highGraphics = !draft.highGraphics;
});
}}
color={highGraphics ? undefined : "gray"}
variant={highGraphics ? "solid" : "surface"}
>
<SunIcon />
</IconButton>
<ScreenshotButton
color="gray"
size="2"
variant="surface"
highContrast
canvas={canvas}
name={protagonistTank.slug}
/>
{fullScreenAvailable && (
<IconButton
size="2"
highContrast
onClick={() => {
if (isFullScreen) {
document.exitFullscreen();
} else document.body.requestFullscreen();
}}
color={isFullScreen ? undefined : "gray"}
variant={isFullScreen ? "solid" : "surface"}
>
{isFullScreen ? (
<ExitFullScreenIcon />
) : (
<EnterFullScreenIcon />
)}
</IconButton>
)}
</>
)}
</Flex>
<Flex gap="3" align="center" mt="2">
<SegmentedControl.Root
variant="classic"
value={`${disturbed ? requestedDisplay : -1}`}
onValueChange={(value) => {
Tankopedia.mutate((draft) => {
draft.disturbed = true;
draft.requestedDisplay = Number(value);
});
}}
>
<SegmentedControl.Item value={`${TankopediaDisplay.Model}`}>
<Tooltip
content={strings.website.tools.tankopedia.sandbox.model.name}
>
<Flex height="100%" align="center">
<img
src="/assets/images/icons/tankopedia-model.png"
style={{ height: "1.25em" }}
/>
</Flex>
</Tooltip>
</SegmentedControl.Item>
<SegmentedControl.Item value={`${TankopediaDisplay.DynamicArmor}`}>
<Tooltip
content={strings.website.tools.tankopedia.sandbox.dynamic.name}
>
<Flex height="100%" align="center">
<img
src="/assets/images/icons/tankopedia-dynamic-armor.png"
style={{ height: "1.25em" }}
/>
</Flex>
</Tooltip>
</SegmentedControl.Item>
<SegmentedControl.Item value={`${TankopediaDisplay.StaticArmor}`}>
<Tooltip
content={strings.website.tools.tankopedia.sandbox.static.name}
>
<Flex height="100%" align="center">
<img
src="/assets/images/icons/tankopedia-static-armor.png"
style={{ height: "1.25em" }}
/>
</Flex>
</Tooltip>
</SegmentedControl.Item>
</SegmentedControl.Root>
{requestedDisplay === TankopediaDisplay.DynamicArmor && (
<Dialog.Root
open={antagonistSelectorOpen}
onOpenChange={setAntagonistSelectorOpen}
>
<Dialog.Trigger>
<Button variant="solid" highContrast>
<Crosshair1Icon />
{unwrap(antagonistTank.name!)}
<SmallTankIcon id={antagonistTank.id} size={16} />
</Button>
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Title align="center">
{strings.website.common.tank_search.select_dialog_title}
</Dialog.Title>
<Tabs.Root
value={tab}
onValueChange={setTab}
style={{ position: "relative" }}
>
<TankSearch
compact
onSelect={(tank) => {
Duel.mutate((draft) => {
draft.antagonist.tank = tank;
draft.antagonist.engine = tank.engines.at(-1)!;
draft.antagonist.track = tank.tracks.at(-1)!;
draft.antagonist.turret = tank.turrets.at(-1)!;
draft.antagonist.gun =
draft.antagonist.turret.guns.at(-1)!;
draft.antagonist.shell = draft.antagonist.gun.shells[0];
});
setAntagonistSelectorOpen(false);
}}
/>
</Tabs.Root>
</Dialog.Content>
</Dialog.Root>
)}
</Flex>
</Flex>
</>
);
}
@@ -8,7 +8,7 @@ import { Duel } from "../../../stores/duel";
import { Tankopedia } from "../../../stores/tankopedia";
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
import type { ThicknessRange } from "../../StaticArmor";
import { Options } from "./components/Options";
import { TankopediaOptions } from "../../TankopediaOptions";
import { TankSandbox } from "./components/TankSandbox";
import { Title } from "./components/TankSandbox/Title";
@@ -142,7 +142,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
<Title skeleton={skeleton} />
<Options
<TankopediaOptions
skeleton={skeleton}
canvas={canvas}
thicknessRange={thicknessRange}