static card

This commit is contained in:
tresabhi committed 2026-11-09 22:58:55 -06:00
1 parent e6158dec8d
commit 263475ec3c
7 files changed
+249 -218

No files matched your search

@@ -1,199 +0,0 @@
import { ReloadIcon } from "@radix-ui/react-icons";
import {
Card,
Code,
Flex,
IconButton,
Text,
TextField,
} from "@radix-ui/themes";
import { Html } from "@react-three/drei";
import { useRef } from "react";
import { radToDeg } from "three/src/math/MathUtils.js";
import { resolveArmorIndex } from "../core/blitzkit/resolveArmorIndex";
import { LocaleProvider, useLocale } from "../hooks/useLocale";
import { useStrings } from "../hooks/useStrings";
import { App } from "../stores/app";
import { Tankopedia } from "../stores/tankopedia";
import { layerTypeNames } from "./ShotCard";
import { ArmorType } from "./SpacedArmorScene";
export function ArmorPlateDisplay() {
const highlightArmor = Tankopedia.use((state) => state.highlightArmor);
const developerMode = App.useDeferred((state) => state.developerMode, false);
const input = useRef<HTMLInputElement>(null);
const locale = useLocale();
const strings = useStrings();
if (highlightArmor === undefined) return null;
return (
<group position={highlightArmor.point}>
<Html
center
style={{ pointerEvents: highlightArmor.editingPlate ? "auto" : "none" }}
>
<LocaleProvider locale={locale}>
<Card
variant="classic"
mb="4"
style={{
whiteSpace: "nowrap",
color: highlightArmor.color,
transform: "translateY(-50%)",
}}
>
<Flex direction="column">
<Text weight="bold">
{
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
.element[layerTypeNames[highlightArmor.type]]
}{" "}
{highlightArmor.thickness.toFixed(0)}
<Text size="1" weight="regular">
mm
</Text>
</Text>
{highlightArmor.type !== ArmorType.External && (
<Text size="2" color="gray">
{highlightArmor.thicknessAngled.toFixed(0)}
<Text size="1">
mm {radToDeg(highlightArmor.angle).toFixed(0)}°
</Text>
</Text>
)}
{developerMode && (
<>
<Text mt="2" color="gray" size="2">
<b>DEV:</b>{" "}
<Code variant="soft" highContrast color="gray">
{highlightArmor.name}
</Code>
</Text>
</>
)}
{highlightArmor.editingPlate && (
<Flex gap="2" align="center">
<TextField.Root
variant="classic"
style={{ width: 96 }}
mt="2"
ref={input}
key={highlightArmor.name}
type="number"
autoFocus
onFocus={(event) => event.target.select()}
onKeyDown={(event) => {
if (!highlightArmor.editingPlate) return;
const index = resolveArmorIndex(highlightArmor.name);
if (index === undefined) return;
const thickness = event.currentTarget.valueAsNumber;
if (isNaN(thickness) || thickness < 0) return;
if (event.key === "Enter") {
input.current?.blur();
Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined;
const { protagonist } = draft;
const tank = protagonist.model;
const turret = tank.turrets[protagonist.turret];
const gun = turret.guns[protagonist.gun];
const track = tank.tracks[protagonist.track];
if (highlightArmor.name.startsWith("hull_")) {
tank.armor!.thickness[index] = thickness;
} else if (
highlightArmor.name.startsWith("turret_")
) {
turret.armor!.thickness[index] = thickness;
} else if (highlightArmor.name.startsWith("gun_")) {
if (highlightArmor.name.includes("_armor_")) {
gun.armor!.thickness[index] = thickness;
} else {
gun.thickness = thickness;
}
} else if (
highlightArmor.name.startsWith("chassis_")
) {
track.thickness = thickness;
}
});
} else if (event.key === "Escape") {
input.current?.blur();
Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined;
});
}
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
}}
defaultValue={highlightArmor.thickness.toFixed(0)}
>
<TextField.Slot side="right">mm</TextField.Slot>
</TextField.Root>
<IconButton
mt="2"
color="red"
onClick={() => {
Tankopedia.mutate((draft) => {
if (!highlightArmor.editingPlate) return;
const index = resolveArmorIndex(highlightArmor.name);
if (index === undefined) return;
draft.highlightArmor = undefined;
const initialState = Tankopedia.initial;
const { protagonist } = draft;
const tank = protagonist.model;
const initialTank = initialState.protagonist.model;
const turret = tank.turrets[protagonist.turret];
const initialTurret =
initialTank.turrets[protagonist.turret];
const gun = turret.guns[protagonist.gun];
const initialGun = initialTurret.guns[protagonist.gun];
const track = tank.tracks[protagonist.track];
const initialTrack =
initialTank.tracks[protagonist.track];
if (highlightArmor.name.startsWith("hull_")) {
tank.armor!.thickness[index] =
initialTank.armor!.thickness[index];
} else if (highlightArmor.name.startsWith("turret_")) {
turret.armor!.thickness[index] =
initialTurret.armor!.thickness[index];
} else if (highlightArmor.name.startsWith("gun_")) {
if (highlightArmor.name.includes("_armor_")) {
gun.armor!.thickness[index] =
initialGun.armor!.thickness[index];
} else {
gun.thickness = initialGun.thickness;
}
} else if (highlightArmor.name.startsWith("chassis_")) {
track.thickness = initialTrack.thickness;
}
});
}}
>
<ReloadIcon />
</IconButton>
</Flex>
)}
</Flex>
</Card>
</LocaleProvider>
</Html>
</group>
);
}
@@ -0,0 +1,28 @@
.card {
white-space: nowrap;
margin-bottom: var(--space-4);
transform: translateY(-50%);
}
.title {
display: flex;
align-items: baseline;
gap: var(--space-3);
}
.info {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.edit {
display: flex;
align-items: center;
gap: var(--space-3);
}
.input {
width: 6rem;
}
@@ -0,0 +1,191 @@
import { literals } from "@blitzkit/i18n/src/literals";
import { ReloadIcon } from "@radix-ui/react-icons";
import { Html } from "@react-three/drei";
import { useEffect, useRef, useState, type KeyboardEvent } from "react";
import { radToDeg } from "three/src/math/MathUtils.js";
import { resolveArmorIndex } from "../../core/blitzkit/resolveArmorIndex";
import { LocaleProvider, useLocale } from "../../hooks/useLocale";
import { useStrings } from "../../hooks/useStrings";
import { App } from "../../stores/app";
import { Tankopedia } from "../../stores/tankopedia";
import { Card } from "../Card";
import { Code } from "../Code";
import { IconButton } from "../IconButton";
import { layerTypeNames } from "../ShotCard";
import { ArmorType } from "../SpacedArmorScene";
import { Text } from "../Text";
import { TextField } from "../TextField";
import styles from "./index.module.css";
export function ArmorPlateDisplay() {
const highlightArmor = Tankopedia.use((state) => state.highlightArmor);
const developerMode = App.useDeferred((state) => state.developerMode, false);
const input = useRef<HTMLInputElement>(null);
const locale = useLocale();
const strings = useStrings();
const [draft, setDraft] = useState("");
const editingName = highlightArmor?.editingPlate
? highlightArmor.name
: undefined;
// start every edit from the plate's current thickness, focused and selected
useEffect(() => {
if (editingName === undefined) return;
setDraft(Tankopedia.state.highlightArmor!.thickness.toFixed(0));
input.current?.focus();
input.current?.select();
}, [editingName]);
if (highlightArmor === undefined) return null;
function handleKeyDown(event: KeyboardEvent) {
if (!highlightArmor?.editingPlate) return;
const index = resolveArmorIndex(highlightArmor.name);
if (index === undefined) return;
if (event.key === "Escape") {
input.current?.blur();
Tankopedia.mutate((draft) => {
draft.highlightArmor = undefined;
});
} else if (event.key === "Enter") {
const thickness = Number(draft);
if (draft.trim() === "" || isNaN(thickness) || thickness < 0) return;
input.current?.blur();
Tankopedia.mutate((state) => {
state.highlightArmor = undefined;
const { protagonist } = state;
const tank = protagonist.model;
const turret = tank.turrets[protagonist.turret];
const gun = turret.guns[protagonist.gun];
const track = tank.tracks[protagonist.track];
if (highlightArmor.name.startsWith("hull_")) {
tank.armor!.thickness[index] = thickness;
} else if (highlightArmor.name.startsWith("turret_")) {
turret.armor!.thickness[index] = thickness;
} else if (highlightArmor.name.startsWith("gun_")) {
if (highlightArmor.name.includes("_armor_")) {
gun.armor!.thickness[index] = thickness;
} else {
gun.thickness = thickness;
}
} else if (highlightArmor.name.startsWith("chassis_")) {
track.thickness = thickness;
}
});
}
}
function handleReset() {
Tankopedia.mutate((state) => {
if (!highlightArmor?.editingPlate) return;
const index = resolveArmorIndex(highlightArmor.name);
if (index === undefined) return;
state.highlightArmor = undefined;
const { protagonist } = state;
const initial = Tankopedia.initial.protagonist;
const tank = protagonist.model;
const initialTank = initial.model;
const turret = tank.turrets[protagonist.turret];
const initialTurret = initialTank.turrets[protagonist.turret];
const gun = turret.guns[protagonist.gun];
const initialGun = initialTurret.guns[protagonist.gun];
const track = tank.tracks[protagonist.track];
const initialTrack = initialTank.tracks[protagonist.track];
if (highlightArmor.name.startsWith("hull_")) {
tank.armor!.thickness[index] = initialTank.armor!.thickness[index];
} else if (highlightArmor.name.startsWith("turret_")) {
turret.armor!.thickness[index] = initialTurret.armor!.thickness[index];
} else if (highlightArmor.name.startsWith("gun_")) {
if (highlightArmor.name.includes("_armor_")) {
gun.armor!.thickness[index] = initialGun.armor!.thickness[index];
} else {
gun.thickness = initialGun.thickness;
}
} else if (highlightArmor.name.startsWith("chassis_")) {
track.thickness = initialTrack.thickness;
}
});
}
return (
<group position={highlightArmor.point}>
<Html
center
style={{ pointerEvents: highlightArmor.editingPlate ? "auto" : "none" }}
>
<LocaleProvider locale={locale}>
<Card className={styles.card} style={{ color: highlightArmor.color }}>
<div className={styles.info}>
<div className={styles.title}>
<Text weight="bold" style={{ color: "inherit" }}>
{
strings.tanks.shot_card.element[
layerTypeNames[highlightArmor.type]
]
}{" "}
{literals(strings.common.units.mm, {
value: highlightArmor.thickness.toFixed(0),
})}
</Text>
{highlightArmor.type !== ArmorType.External && (
<Text lowContrast>
{literals(strings.common.units.mm, {
value: highlightArmor.thicknessAngled.toFixed(0),
})}{" "}
{literals(strings.common.units.deg, {
value: radToDeg(highlightArmor.angle).toFixed(0),
})}
</Text>
)}
</div>
{developerMode && (
<Text lowContrast>
<b>DEV:</b> <Code lowContrast>{highlightArmor.name}</Code>
</Text>
)}
</div>
{highlightArmor.editingPlate && (
<div className={styles.edit}>
<TextField
ref={input}
className={styles.input}
variant="form"
align="right"
inputMode="numeric"
value={draft}
onChange={(event) => setDraft(event.target.value)}
onKeyDown={handleKeyDown}
onClick={(event) => event.stopPropagation()}
/>
<IconButton
color="red"
variant="soft"
size="minor"
evenPadding
onClick={handleReset}
>
<ReloadIcon />
</IconButton>
</div>
)}
</Card>
</LocaleProvider>
</Html>
</group>
);
}
@@ -0,0 +1,12 @@
.card {
display: flex;
flex-direction: column;
gap: var(--space-4);
box-sizing: border-box;
padding: var(--space-4);
border-radius: var(--space-3);
border: 1px solid var(--gray-a4);
background-color: var(--gray-2);
}
@@ -0,0 +1,7 @@
import type { ComponentProps } from "react";
import { classNames } from "../../ui/classNames";
import styles from "./index.module.css";
export function Card({ className, ...props }: ComponentProps<"div">) {
return <div className={classNames(styles.card, className)} {...props} />;
}
@@ -21,15 +21,6 @@
}
.card {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding: var(--space-4);
border-radius: var(--space-3);
border: 1px solid var(--gray-a4);
background-color: var(--gray-2);
white-space: nowrap;
}
@@ -28,6 +28,7 @@ import {
type ShotStatus,
} from "../../stores/tankopedia";
import { classNames } from "../../ui/classNames";
import { Card } from "../Card";
import { ArmorType } from "../SpacedArmorScene";
import { Text, type TextProps } from "../Text";
import styles from "./index.module.css";
@@ -187,7 +188,7 @@ function ShotCardHtml({ shot, className, ...props }: ShotCardHtmlProps) {
);
return (
<div className={classNames(styles.card, className)} {...props}>
<Card className={classNames(styles.card, className)} {...props}>
<div className={styles.section}>
<div className={styles.header}>
<Text color={shotStatusColors[shot.in.status]} weight="bold">
@@ -230,7 +231,7 @@ function ShotCardHtml({ shot, className, ...props }: ShotCardHtmlProps) {
</div>
</>
)}
</div>
</Card>
);
}
@@ -250,7 +251,7 @@ export function ShotCard() {
useEffect(() => {
if (shot?.splashRadius !== undefined) {
const outlineGeometry = new BufferGeometry().setFromPoints(
new Path().absarc(0, 0, 1, 0, Math.PI * 2).getSpacedPoints(50),
new Path().absarc(0, 0, 1, 0, Math.PI * 2).getSpacedPoints(50)
);
const outlineMaterial = new LineDashedMaterial({
color: "orange",
@@ -259,7 +260,7 @@ export function ShotCard() {
});
const outline = new Line(
outlineGeometry,
outlineMaterial,
outlineMaterial
).computeLineDistances();
splashRadiusWrapper.current?.add(outline);
@@ -294,11 +295,11 @@ export function ShotCard() {
const outLength =
shot.out && shot.out.layers.length > 0
? (shot.in.layers.at(-1) as ShotLayerNonExternal).point.distanceTo(
(shot.out.layers.at(-1) as ShotLayerNonExternal).point,
(shot.out.layers.at(-1) as ShotLayerNonExternal).point
)
: SHOT_DISPLAY_LENGTH_INFINITY;
const inLast = shot.in.layers.findLast(
(layer) => layer.type !== null,
(layer) => layer.type !== null
) as ShotLayerNonExternal;
const outTitleColor = shot.out
? shotStatusColors[shot.out.status]
@@ -323,7 +324,7 @@ export function ShotCard() {
position={inLast.point}
ref={splashRadiusWrapper}
rotation={new Euler().setFromQuaternion(
new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal),
new Quaternion().setFromUnitVectors(K_HAT, shot.in.surfaceNormal)
)}
>
<mesh>
@@ -379,7 +380,7 @@ export function ShotCard() {
<group
position={inLast.point}
rotation={new Euler().setFromQuaternion(
new Quaternion().setFromUnitVectors(J_HAT, shot.in.surfaceNormal),
new Quaternion().setFromUnitVectors(J_HAT, shot.in.surfaceNormal)
)}
>
<mesh position={[0, inLength / 2, 0]} renderOrder={2}>
@@ -407,8 +408,8 @@ export function ShotCard() {
rotation={new Euler().setFromQuaternion(
new Quaternion().setFromUnitVectors(
J_HAT,
shot.out.surfaceNormal.clone(),
),
shot.out.surfaceNormal.clone()
)
)}
>
<mesh position={[0, outLength / 2, 0]} renderOrder={2}>