mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-10 11:16:10 +01:00
antagonist settings
This commit is contained in:
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user