antagonist settings

This commit is contained in:
tresabhi committed 2026-11-09 23:23:04 -06:00
1 parent aa982d73de
commit fe545c415e
15 files changed
+654 -475

No files matched your search

@@ -0,0 +1,36 @@
.form {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
}
.field {
max-width: 7rem;
}
.slider {
display: flex;
align-items: center;
gap: var(--space-3);
flex-grow: 1;
min-width: 8rem;
max-width: 15rem;
> :last-child {
flex-grow: 1;
}
}
.actions {
display: flex;
justify-content: flex-end;
gap: var(--space-3);
}
@@ -0,0 +1,306 @@
import { literals } from "@blitzkit/i18n/src/literals";
import { ShellType } from "@blitzkit/protos";
import { throttle } from "lodash-es";
import { useEffect, useMemo, useState, type KeyboardEvent } from "react";
import { DuelModule, useDuel } from "../../hooks/useDuel";
import { useStrings } from "../../hooks/useStrings";
import { Tankopedia } from "../../stores/tankopedia";
import { Button } from "../Button";
import { IconButton } from "../IconButton";
import { Popover } from "../Popover";
import { Select } from "../Select";
import { Slider } from "../Slider";
import { Text } from "../Text";
import { TextField } from "../TextField";
import styles from "./index.module.css";
interface NumberFieldProps {
value: number;
suffix: string;
onValueChange(value: number): void;
}
/** Text field that only commits valid numbers and reverts on Escape */
function NumberField({ value, suffix, onValueChange }: NumberFieldProps) {
const [text, setText] = useState(`${value}`);
useEffect(() => setText(`${value}`), [value]);
function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {
// keep tankopedia hotkeys from firing while typing
event.stopPropagation();
const input = event.target as HTMLInputElement;
if (event.key === "Enter") {
input.blur();
} else if (event.key === "Escape") {
setText(`${value}`);
onValueChange(value);
input.blur();
}
}
return (
<TextField
className={styles.field}
variant="form"
align="right"
value={text}
onFocus={(event) => (event.target as HTMLInputElement).select?.()}
onKeyDown={handleKeyDown}
onChange={(event) => {
setText(event.target.value);
const parsed = Number(event.target.value);
if (event.target.value.trim() !== "" && Number.isFinite(parsed)) {
onValueChange(parsed);
}
}}
>
<Text lowContrast>{suffix}</Text>
</TextField>
);
}
export function CustomShellButton() {
const customShell = Tankopedia.use((state) => state.customShell);
const gun = useDuel("protagonist", DuelModule.Gun);
const antagonistShell = useDuel("antagonist", DuelModule.Shell);
const strings = useStrings();
const mm = literals(strings.common.units.mm, { value: "" });
const setRicochet = useMemo(
() =>
throttle((value: number) => {
Tankopedia.mutate((draft) => {
draft.customShell!.ricochet = value;
});
}, 1000),
[],
);
return (
<Popover.Root
onOpenChange={(open) => {
if (!open || customShell) return;
Tankopedia.mutate((draft) => {
draft.customShell = antagonistShell;
});
}}
>
<Popover.Trigger asChild>
<IconButton
listChild="vertical"
color={customShell ? undefined : "gray"}
variant={customShell ? "solid" : "soft"}
highContrast={customShell !== undefined}
size="major"
>
<img alt="custom shell" src="/assets/images/icons/custom-shell.png" />
</IconButton>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content side="left" align="center">
{customShell && (
<>
<div className={styles.form}>
<div className={styles.row}>
<Text>{strings.tanks.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;
break;
}
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>
<Select.Value />
</Select.Trigger>
<Select.Portal>
<Select.Content position="popper" sideOffset={8}>
<Select.Viewport>
{[
[ShellType.SHELL_TYPE_AP, strings.common.shells.ap],
[
ShellType.SHELL_TYPE_APCR,
strings.common.shells.ap_cr,
],
[
ShellType.SHELL_TYPE_HEAT,
strings.common.shells.hc,
],
[ShellType.SHELL_TYPE_HE, strings.common.shells.he],
].map(([type, label]) => (
<Select.Item key={type} value={`${type}`}>
<Select.ItemText>{label}</Select.ItemText>
</Select.Item>
))}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
</div>
<div className={styles.row}>
<Text>{strings.tanks.custom_shell.caliber}</Text>
<NumberField
value={customShell.caliber}
suffix={mm}
onValueChange={(value) => {
Tankopedia.mutate((draft) => {
draft.customShell!.caliber = value;
});
}}
/>
</div>
<div className={styles.row}>
<Text>{strings.tanks.custom_shell.penetration}</Text>
<NumberField
value={customShell.penetration!.near}
suffix={mm}
onValueChange={(value) => {
Tankopedia.mutate((draft) => {
draft.customShell!.penetration!.near = value;
});
}}
/>
</div>
{(customShell.type === ShellType.SHELL_TYPE_AP ||
customShell.type === ShellType.SHELL_TYPE_APCR) && (
<>
<div className={styles.row}>
<Text>{strings.tanks.custom_shell.normalization}</Text>
<div className={styles.slider}>
<Text lowContrast>
{literals(strings.common.units.deg, {
value: (customShell.normalization ?? 0).toFixed(0),
})}
</Text>
<Slider.Root
min={0}
max={90}
step={0.1}
defaultValue={[customShell.normalization ?? 0]}
onValueChange={([value]) => {
Tankopedia.mutate((draft) => {
draft.customShell!.normalization = value;
});
}}
>
<Slider.Track>
<Slider.Range />
</Slider.Track>
<Slider.Thumb />
</Slider.Root>
</div>
</div>
<div className={styles.row}>
<Text>{strings.tanks.custom_shell.ricochet}</Text>
<div className={styles.slider}>
<Text lowContrast>
{literals(strings.common.units.deg, {
value: (customShell.ricochet ?? 90).toFixed(0),
})}
</Text>
<Slider.Root
min={0}
max={90}
step={0.1}
defaultValue={[customShell.ricochet ?? 90]}
onValueChange={([value]) => setRicochet(value)}
>
<Slider.Track>
<Slider.Range />
</Slider.Track>
<Slider.Thumb />
</Slider.Root>
</div>
</div>
</>
)}
{customShell.type === ShellType.SHELL_TYPE_HE && (
<div className={styles.row}>
<Text>Explosion radius</Text>
<NumberField
value={customShell.explosion_radius ?? 0}
suffix="m"
onValueChange={(value) => {
Tankopedia.mutate((draft) => {
draft.customShell!.explosion_radius = value;
});
}}
/>
</div>
)}
</div>
<div className={styles.actions}>
<Popover.Close asChild>
<Button color="red" variant="outline">
{strings.tanks.custom_shell.exit}
</Button>
</Popover.Close>
<Button
color="red"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.customShell = antagonistShell;
});
}}
>
{strings.tanks.custom_shell.reset}
</Button>
</div>
</>
)}
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
}
@@ -0,0 +1,14 @@
.discriminator {
position: absolute;
right: var(--space-3);
font-size: var(--font-size-minor);
line-height: 1.2;
text-align: right;
text-shadow: 0 0 4px black;
color: var(--white-a12);
pointer-events: none;
top: 50%;
}
@@ -0,0 +1,13 @@
import type { ComponentProps } from "react";
import { classNames } from "../../ui/classNames";
import styles from "./index.module.css";
/** Small label overlaid on the right edge of a `position: relative` parent */
export function Discriminator({
className,
...props
}: ComponentProps<"span">) {
return (
<span className={classNames(styles.discriminator, className)} {...props} />
);
}
@@ -1,5 +1,6 @@
import { Button, Text, Tooltip } from '@radix-ui/themes';
import { Button, Tooltip } from '@radix-ui/themes';
import type { ComponentProps, ReactNode } from 'react';
import { Discriminator } from '../Discriminator';
export interface TankComponentButtonProps
extends ComponentProps<typeof Button> {
@@ -61,35 +62,13 @@ export function TankComponentButton({
{children}
{discriminator !== undefined && (
<Text
size="1"
style={{
textShadow: '0 0 4px black',
position: 'absolute',
top: '50%',
right: 8,
textAlign: 'right',
}}
color={variant === 'ghost' ? 'gray' : undefined}
>
{discriminator}
</Text>
<Discriminator>{discriminator}</Discriminator>
)}
{secondaryDiscriminator !== undefined && (
<Text
size="1"
style={{
textShadow: '0 0 4px black',
position: 'absolute',
bottom: '50%',
right: 8,
textAlign: 'right',
}}
color={variant === 'ghost' ? 'gray' : undefined}
>
<Discriminator>
{secondaryDiscriminator}
</Text>
</Discriminator>
)}
</Button>
);
@@ -0,0 +1,16 @@
.Content {
z-index: 1;
display: flex;
flex-direction: column;
gap: var(--space-4);
margin: var(--space-4);
padding: var(--space-4);
max-width: calc(100dvw - 2 * var(--space-4));
background-color: var(--gray-2);
box-shadow: var(--shadow-subtle);
border-radius: var(--space-4);
outline: var(--outline-interactive) !important;
}
@@ -0,0 +1,5 @@
import { Popover as _ } from "radix-ui";
import { composeRadixComponents } from "../../ui/compose";
import styles from "./index.module.css";
export const Popover = composeRadixComponents(_, styles);
@@ -0,0 +1,62 @@
.Trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border: none;
border-radius: var(--max-size);
background-color: var(--gray-1);
outline: var(--outline-interactive);
color: var(--gray-12);
font: inherit;
cursor: pointer;
&:focus-visible {
outline: var(--outline-focus);
box-shadow: var(--shadow-focus);
}
}
.Content {
/* above popovers */
z-index: 2;
overflow: hidden;
background-color: var(--gray-2);
box-shadow: var(--shadow-subtle);
border-radius: var(--space-4);
outline: var(--outline-interactive) !important;
}
.Item {
position: relative;
display: flex;
align-items: center;
padding: var(--space-3) var(--space-4) var(--space-3) var(--space-5);
background-color: var(--gray-3);
font-size: var(--font-size-minor);
cursor: pointer;
user-select: none;
&[data-highlighted] {
outline: none;
background-color: var(--gray-5);
}
&:not(:last-child) {
border-bottom: var(--space-1) solid var(--gray-2);
}
}
.ItemIndicator {
position: absolute;
left: var(--space-3);
display: flex;
}
@@ -0,0 +1,5 @@
import { Select as _ } from "radix-ui";
import { composeRadixComponents } from "../../ui/compose";
import styles from "./index.module.css";
export const Select = composeRadixComponents(_, styles);
@@ -0,0 +1,42 @@
.Root {
position: relative;
display: flex;
align-items: center;
height: var(--space-5);
touch-action: none;
user-select: none;
}
.Track {
position: relative;
flex: 1;
height: var(--space-2);
border-radius: var(--max-size);
background-color: var(--gray-5);
}
.Range {
position: absolute;
height: 100%;
border-radius: var(--max-size);
background-color: var(--accent-8);
}
.Thumb {
display: block;
width: var(--space-5);
height: var(--space-5);
border-radius: var(--max-size);
background-color: var(--gray-12);
box-shadow: var(--shadow-subtle);
&:focus-visible {
outline: var(--outline-focus);
box-shadow: var(--shadow-focus);
}
}
@@ -0,0 +1,5 @@
import { Slider as _ } from "radix-ui";
import { composeRadixComponents } from "../../ui/compose";
import styles from "./index.module.css";
export const Slider = composeRadixComponents(_, styles);
@@ -1,4 +1,6 @@
.antagonist-control {
pointer-events: auto;
align-items: flex-end;
display: flex;
flex-direction: column;
justify-content: end;
@@ -17,3 +19,12 @@
transform: translate(0, -50%);
}
}
.group {
display: flex;
flex-direction: column;
}
.gun-button {
position: relative;
}
@@ -1,6 +1,5 @@
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";
@@ -9,10 +8,13 @@ import { useUnwrapper } from "../../hooks/useUnwrapper";
import { Tankopedia } from "../../stores/tankopedia";
import { TankopediaDisplay } from "../../stores/tankopediaPersistent/constants";
import type { ComputedCharacteristics } from "../../types/characteristics";
import { CustomShellButton } from "../CustomShellButton";
import { EqualIcon } from "../EqualIcon";
import { IconButton } from "../IconButton";
import { CustomShellButton } from "../TankopediaOptions/components/CustomShellButton";
import { Discriminator } from "../Discriminator";
import { Text } from "../Text";
import { Tooltip } from "../Tooltip";
import styles from "./index.module.css";
interface TankopediaAntagonistControlProps {
@@ -25,14 +27,15 @@ export function TankopediaAntagonistControl({
const strings = useStrings();
const unwrap = useUnwrapper();
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const showControls = Tankopedia.use((state) => state.showControls);
const display = Tankopedia.use((state) => state.loaders.display.loaded);
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 hasCalibratedShells = useEquipment("antagonist", 103);
const hasEnhancedArmor = useEquipment("antagonist", 110);
const tank = useDuel("antagonist", DuelModule.Tank);
const gun = useDuel("antagonist", DuelModule.Gun);
@@ -43,7 +46,7 @@ export function TankopediaAntagonistControl({
[tank.turrets],
);
const isVisible = requestedDisplay === TankopediaDisplay.DynamicArmor;
const isVisible = showControls && display === TankopediaDisplay.DynamicArmor;
return (
<div className={styles["antagonist-control"]} data-visible={isVisible}>
@@ -56,166 +59,143 @@ export function TankopediaAntagonistControl({
</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>
<div className={styles.group}>
{[...antagonistUniqueGuns].reverse().map((entry) => {
const selected = entry.gun.id === gun.id;
const name = unwrap(entry.gun.name!);
<img
alt="Antagonist Gun"
src={alias("api", "/icons/modules/gun.webp")}
style={{
width: "65%",
height: "65%",
objectFit: "contain",
}}
/>
</IconButton>
</Popover.Trigger>
return (
<Tooltip key={entry.gun.id} tooltip={name} side="left">
<IconButton
className={styles["gun-button"]}
parentListChild="vertical"
color={selected ? undefined : "gray"}
variant={selected ? "solid" : "soft"}
highContrast={selected}
size="major"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.antagonist.turret = entry.turret.id;
draft.antagonist.gun = entry.gun.id;
draft.antagonist.shell = entry.gun.shells[0].id;
draft.shot = undefined;
draft.customShell = undefined;
});
}}
>
<img alt={name} src={alias("api", "/icons/modules/gun.webp")} />
<Popover.Content side="left" align="center">
<div>
{[...antagonistUniqueGuns].reverse().map(({ gun, turret }) => (
<IconButton key={gun.id}>
<img src={alias("api", "/icons/modules/gun.webp")} />
<Discriminator>{romanize(entry.gun.tier)}</Discriminator>
</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>
</Tooltip>
);
})}
</div>
)}
<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;
});
}}
/>
))}
<div className={styles.group}>
{gun.shells.map((thisShell) => {
const selected = thisShell.id === shell.id && !hasCustomShell;
const name = unwrap(thisShell.name!);
return (
<Tooltip key={thisShell.id} tooltip={name} side="left">
<IconButton
parentListChild="vertical"
color={selected ? undefined : "gray"}
variant={selected ? "solid" : "soft"}
highContrast={selected}
size="major"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.antagonist.shell = thisShell.id;
draft.shot = undefined;
draft.customShell = undefined;
});
}}
>
<img
alt={name}
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
/>
</IconButton>
</Tooltip>
);
})}
<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;
<div className={styles.group}>
<Tooltip tooltip="Calibrated Shells" side="left">
<IconButton
parentListChild="vertical"
color={hasCalibratedShells ? undefined : "gray"}
variant={hasCalibratedShells ? "solid" : "soft"}
highContrast={hasCalibratedShells}
size="major"
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%",
if (hasCalibratedShells) {
delete draft.antagonist.equipment[0];
} else {
draft.antagonist.equipment[0] = 1;
}
});
}}
/>
</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;
}
>
<img
alt="Calibrated Shells"
src={alias("api", "/icons/equipment/103.webp")}
/>
</IconButton>
</Tooltip>
draft.shot = undefined;
});
}}
>
<img
alt="Enhanced Armor"
src={alias("api", "/icons/equipment/110.webp")}
style={{
width: "50%",
height: "50%",
<Tooltip tooltip="Enhanced Armor" side="left">
<IconButton
parentListChild="vertical"
color={hasEnhancedArmor ? undefined : "gray"}
variant={hasEnhancedArmor ? "solid" : "soft"}
highContrast={hasEnhancedArmor}
size="major"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.shot = undefined;
if (hasEnhancedArmor) {
delete draft.antagonist.equipment[4];
} else {
draft.antagonist.equipment[4] = 0;
}
});
}}
/>
</IconButton>
>
<img
alt="Enhanced Armor"
src={alias("api", "/icons/equipment/110.webp")}
/>
</IconButton>
</Tooltip>
</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>
<Tooltip tooltip="Equalize" side="left">
<IconButton
color={equalize ? undefined : "gray"}
variant={equalize ? "solid" : "soft"}
highContrast={equalize}
size="major"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.environment.equalize = !draft.environment.equalize;
draft.shot = undefined;
});
}}
>
<EqualIcon />
</IconButton>
</Tooltip>
</div>
);
}
@@ -1,295 +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 { DuelModule, useDuel } from "../../../hooks/useDuel";
import { useStrings } from "../../../hooks/useStrings";
import { Tankopedia } from "../../../stores/tankopedia";
export function CustomShellButton() {
const customShell = Tankopedia.use((state) => state.customShell);
const gun = useDuel("protagonist", DuelModule.Gun);
const antagonistShell = useDuel("antagonist", DuelModule.Shell);
const strings = useStrings();
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.tanks.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.tanks.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.tanks.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.tanks.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]) => {
Tankopedia.mutate((draft) => {
draft.customShell!.normalization = value;
});
}}
/>
</Flex>
</Flex>
<Flex align="center" justify="between" gap="4" py="1">
<Text>{strings.tanks.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.tanks.custom_shell.exit}
</Button>
</Popover.Close>
<Button
color="red"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.customShell = antagonistShell;
});
}}
>
{strings.tanks.custom_shell.reset}
</Button>
</Flex>
</>
)}
</Popover.Content>
</Popover.Root>
);
}
+2 -2
View File
@@ -1,5 +1,5 @@
import { createDefaultEnvironment, type TankEnvironment } from "@blitzkit/core";
import type { TankDefinition } from "@blitzkit/protos";
import type { ShellDefinition, TankDefinition } from "@blitzkit/protos";
import { Soapstone } from "soapstone";
import type { Vector3 } from "three";
import { api } from "../api/dynamic";
@@ -121,7 +121,7 @@ interface Tankopedia {
// editStatic: boolean;
// xpMultiplier: (typeof XP_MULTIPLIERS)[number];
// customShell?: ShellDefinition;
customShell?: ShellDefinition;
// statSearch?: string;
}