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