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 type { ComponentProps, ReactNode } from 'react';
import { Discriminator } from '../Discriminator';
export interface TankComponentButtonProps export interface TankComponentButtonProps
extends ComponentProps<typeof Button> { extends ComponentProps<typeof Button> {
@@ -61,35 +62,13 @@ export function TankComponentButton({
{children} {children}
{discriminator !== undefined && ( {discriminator !== undefined && (
<Text <Discriminator>{discriminator}</Discriminator>
size="1"
style={{
textShadow: '0 0 4px black',
position: 'absolute',
top: '50%',
right: 8,
textAlign: 'right',
}}
color={variant === 'ghost' ? 'gray' : undefined}
>
{discriminator}
</Text>
)} )}
{secondaryDiscriminator !== undefined && ( {secondaryDiscriminator !== undefined && (
<Text <Discriminator>
size="1"
style={{
textShadow: '0 0 4px black',
position: 'absolute',
bottom: '50%',
right: 8,
textAlign: 'right',
}}
color={variant === 'ghost' ? 'gray' : undefined}
>
{secondaryDiscriminator} {secondaryDiscriminator}
</Text> </Discriminator>
)} )}
</Button> </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 { .antagonist-control {
pointer-events: auto;
align-items: flex-end;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: end; justify-content: end;
@@ -17,3 +19,12 @@
transform: translate(0, -50%); 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 { alias, romanize, uniqueGuns } from "@blitzkit/core";
import { literals } from "@blitzkit/i18n"; import { literals } from "@blitzkit/i18n";
import { Popover } from "radix-ui";
import { useMemo } from "react"; import { useMemo } from "react";
import { DuelModule, useDuel } from "../../hooks/useDuel"; import { DuelModule, useDuel } from "../../hooks/useDuel";
import { useEquipment } from "../../hooks/useEquipment"; import { useEquipment } from "../../hooks/useEquipment";
@@ -9,10 +8,13 @@ import { useUnwrapper } from "../../hooks/useUnwrapper";
import { Tankopedia } from "../../stores/tankopedia"; import { Tankopedia } from "../../stores/tankopedia";
import { TankopediaDisplay } from "../../stores/tankopediaPersistent/constants"; import { TankopediaDisplay } from "../../stores/tankopediaPersistent/constants";
import type { ComputedCharacteristics } from "../../types/characteristics"; import type { ComputedCharacteristics } from "../../types/characteristics";
import { CustomShellButton } from "../CustomShellButton";
import { EqualIcon } from "../EqualIcon"; import { EqualIcon } from "../EqualIcon";
import { IconButton } from "../IconButton"; import { IconButton } from "../IconButton";
import { CustomShellButton } from "../TankopediaOptions/components/CustomShellButton";
import { Discriminator } from "../Discriminator";
import { Text } from "../Text"; import { Text } from "../Text";
import { Tooltip } from "../Tooltip";
import styles from "./index.module.css"; import styles from "./index.module.css";
interface TankopediaAntagonistControlProps { interface TankopediaAntagonistControlProps {
@@ -25,14 +27,15 @@ export function TankopediaAntagonistControl({
const strings = useStrings(); const strings = useStrings();
const unwrap = useUnwrapper(); 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( const hasCustomShell = Tankopedia.use(
(state) => state.customShell !== undefined, (state) => state.customShell !== undefined,
); );
const equalize = Tankopedia.use((state) => state.environment.equalize); const equalize = Tankopedia.use((state) => state.environment.equalize);
const hasCalibratedShells = useEquipment("protagonist", 103); const hasCalibratedShells = useEquipment("antagonist", 103);
const hasEnhancedArmor = useEquipment("protagonist", 110); const hasEnhancedArmor = useEquipment("antagonist", 110);
const tank = useDuel("antagonist", DuelModule.Tank); const tank = useDuel("antagonist", DuelModule.Tank);
const gun = useDuel("antagonist", DuelModule.Gun); const gun = useDuel("antagonist", DuelModule.Gun);
@@ -43,7 +46,7 @@ export function TankopediaAntagonistControl({
[tank.turrets], [tank.turrets],
); );
const isVisible = requestedDisplay === TankopediaDisplay.DynamicArmor; const isVisible = showControls && display === TankopediaDisplay.DynamicArmor;
return ( return (
<div className={styles["antagonist-control"]} data-visible={isVisible}> <div className={styles["antagonist-control"]} data-visible={isVisible}>
@@ -56,166 +59,143 @@ export function TankopediaAntagonistControl({
</Text> </Text>
{antagonistUniqueGuns.length > 1 && ( {antagonistUniqueGuns.length > 1 && (
<Popover.Root> <div className={styles.group}>
<Popover.Trigger> {[...antagonistUniqueGuns].reverse().map((entry) => {
<IconButton const selected = entry.gun.id === gun.id;
radius="full" const name = unwrap(entry.gun.name!);
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 return (
alt="Antagonist Gun" <Tooltip key={entry.gun.id} tooltip={name} side="left">
src={alias("api", "/icons/modules/gun.webp")} <IconButton
style={{ className={styles["gun-button"]}
width: "65%", parentListChild="vertical"
height: "65%", color={selected ? undefined : "gray"}
objectFit: "contain", variant={selected ? "solid" : "soft"}
}} highContrast={selected}
/> size="major"
</IconButton> onClick={() => {
</Popover.Trigger> 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"> <Discriminator>{romanize(entry.gun.tier)}</Discriminator>
<div>
{[...antagonistUniqueGuns].reverse().map(({ gun, turret }) => (
<IconButton key={gun.id}>
<img src={alias("api", "/icons/modules/gun.webp")} />
</IconButton> </IconButton>
// <ModuleButton </Tooltip>
// module="gun" );
// key={gun.id} })}
// onClick={() => { </div>
// 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> <div className={styles.group}>
{gun.shells.map((thisShell) => ( {gun.shells.map((thisShell) => {
<IconButton const selected = thisShell.id === shell.id && !hasCustomShell;
key={thisShell.id} const name = unwrap(thisShell.name!);
selected={thisShell.id === shell.id && !hasCustomShell}
alt={unwrap(thisShell.name!)} return (
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)} <Tooltip key={thisShell.id} tooltip={name} side="left">
onClick={() => { <IconButton
Tankopedia.mutate((draft) => { parentListChild="vertical"
draft.antagonist.shell = thisShell.id; color={selected ? undefined : "gray"}
draft.shot = undefined; variant={selected ? "solid" : "soft"}
draft.customShell = undefined; 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 /> <CustomShellButton />
</div> </div>
<div <div className={styles.group}>
direction="column" <Tooltip tooltip="Calibrated Shells" side="left">
style={{ <IconButton
pointerEvents: "auto", parentListChild="vertical"
borderRadius: "var(--radius-full)", color={hasCalibratedShells ? undefined : "gray"}
}} variant={hasCalibratedShells ? "solid" : "soft"}
overflow="hidden" highContrast={hasCalibratedShells}
> size="major"
<IconButton onClick={() => {
color={hasCalibratedShells ? undefined : "gray"} Tankopedia.mutate((draft) => {
variant="soft" draft.shot = undefined;
size={{ initial: "2", sm: "3" }}
radius="none"
onClick={() => {
Tankopedia.mutate((draft) => {
draft.shot = undefined;
if (hasCalibratedShells) { if (hasCalibratedShells) {
delete draft.protagonist.equipment[0]; delete draft.antagonist.equipment[0];
} else { } else {
draft.antagonist.equipment[0] = 1; draft.antagonist.equipment[0] = 1;
} }
}); });
}}
>
<img
src={alias("api", "/icons/equipment/103.webp")}
style={{
width: "50%",
height: "50%",
}} }}
/> >
</IconButton> <img
<IconButton alt="Calibrated Shells"
color={hasEnhancedArmor ? undefined : "gray"} src={alias("api", "/icons/equipment/103.webp")}
variant="soft" />
size={{ initial: "2", sm: "3" }} </IconButton>
radius="none" </Tooltip>
onClick={() => {
Tankopedia.mutate((draft) => {
if (hasEnhancedArmor) {
delete draft.protagonist.equipment[4];
} else {
draft.antagonist.equipment[4] = 0;
}
draft.shot = undefined; <Tooltip tooltip="Enhanced Armor" side="left">
}); <IconButton
}} parentListChild="vertical"
> color={hasEnhancedArmor ? undefined : "gray"}
<img variant={hasEnhancedArmor ? "solid" : "soft"}
alt="Enhanced Armor" highContrast={hasEnhancedArmor}
src={alias("api", "/icons/equipment/110.webp")} size="major"
style={{ onClick={() => {
width: "50%", Tankopedia.mutate((draft) => {
height: "50%", 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> </div>
<IconButton <Tooltip tooltip="Equalize" side="left">
variant="soft" <IconButton
size={{ initial: "2", sm: "3" }} color={equalize ? undefined : "gray"}
color={equalize ? undefined : "gray"} variant={equalize ? "solid" : "soft"}
onClick={() => { highContrast={equalize}
Tankopedia.mutate((draft) => { size="major"
draft.environment.equalize = !draft.environment.equalize; onClick={() => {
draft.shot = undefined; Tankopedia.mutate((draft) => {
}); draft.environment.equalize = !draft.environment.equalize;
}} draft.shot = undefined;
> });
<EqualIcon /> }}
</IconButton> >
<EqualIcon />
</IconButton>
</Tooltip>
</div> </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 { createDefaultEnvironment, type TankEnvironment } from "@blitzkit/core";
import type { TankDefinition } from "@blitzkit/protos"; import type { ShellDefinition, TankDefinition } from "@blitzkit/protos";
import { Soapstone } from "soapstone"; import { Soapstone } from "soapstone";
import type { Vector3 } from "three"; import type { Vector3 } from "three";
import { api } from "../api/dynamic"; import { api } from "../api/dynamic";
@@ -121,7 +121,7 @@ interface Tankopedia {
// editStatic: boolean; // editStatic: boolean;
// xpMultiplier: (typeof XP_MULTIPLIERS)[number]; // xpMultiplier: (typeof XP_MULTIPLIERS)[number];
// customShell?: ShellDefinition; customShell?: ShellDefinition;
// statSearch?: string; // statSearch?: string;
} }