diff --git a/packages/website/src/components/ArmorPlateDisplay.tsx b/packages/website/src/components/ArmorPlateDisplay.tsx deleted file mode 100644 index d445ba217..000000000 --- a/packages/website/src/components/ArmorPlateDisplay.tsx +++ /dev/null @@ -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(null); - const locale = useLocale(); - const strings = useStrings(); - - if (highlightArmor === undefined) return null; - - return ( - - - - - - - { - strings.website.tools.tankopedia.sandbox.dynamic.shot_card - .element[layerTypeNames[highlightArmor.type]] - }{" "} - {highlightArmor.thickness.toFixed(0)} - - mm - - - - {highlightArmor.type !== ArmorType.External && ( - - {highlightArmor.thicknessAngled.toFixed(0)} - - mm {radToDeg(highlightArmor.angle).toFixed(0)}° - - - )} - - {developerMode && ( - <> - - DEV:{" "} - - {highlightArmor.name} - - - - )} - - {highlightArmor.editingPlate && ( - - 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)} - > - mm - - - { - 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; - } - }); - }} - > - - - - )} - - - - - - ); -} diff --git a/packages/website/src/components/ArmorPlateDisplay/index.module.css b/packages/website/src/components/ArmorPlateDisplay/index.module.css new file mode 100644 index 000000000..3e6215390 --- /dev/null +++ b/packages/website/src/components/ArmorPlateDisplay/index.module.css @@ -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; +} diff --git a/packages/website/src/components/ArmorPlateDisplay/index.tsx b/packages/website/src/components/ArmorPlateDisplay/index.tsx new file mode 100644 index 000000000..c779c4cc9 --- /dev/null +++ b/packages/website/src/components/ArmorPlateDisplay/index.tsx @@ -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(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 ( + + + + +
+
+ + { + strings.tanks.shot_card.element[ + layerTypeNames[highlightArmor.type] + ] + }{" "} + {literals(strings.common.units.mm, { + value: highlightArmor.thickness.toFixed(0), + })} + + + {highlightArmor.type !== ArmorType.External && ( + + {literals(strings.common.units.mm, { + value: highlightArmor.thicknessAngled.toFixed(0), + })}{" "} + {literals(strings.common.units.deg, { + value: radToDeg(highlightArmor.angle).toFixed(0), + })} + + )} +
+ + {developerMode && ( + + DEV: {highlightArmor.name} + + )} +
+ + {highlightArmor.editingPlate && ( +
+ setDraft(event.target.value)} + onKeyDown={handleKeyDown} + onClick={(event) => event.stopPropagation()} + /> + + + + +
+ )} +
+
+ +
+ ); +} diff --git a/packages/website/src/components/Card/index.module.css b/packages/website/src/components/Card/index.module.css new file mode 100644 index 000000000..5f78cedb1 --- /dev/null +++ b/packages/website/src/components/Card/index.module.css @@ -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); +} diff --git a/packages/website/src/components/Card/index.tsx b/packages/website/src/components/Card/index.tsx new file mode 100644 index 000000000..6079df80b --- /dev/null +++ b/packages/website/src/components/Card/index.tsx @@ -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
; +} diff --git a/packages/website/src/components/ShotCard/index.module.css b/packages/website/src/components/ShotCard/index.module.css index 32a901d56..cce710123 100644 --- a/packages/website/src/components/ShotCard/index.module.css +++ b/packages/website/src/components/ShotCard/index.module.css @@ -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; } diff --git a/packages/website/src/components/ShotCard/index.tsx b/packages/website/src/components/ShotCard/index.tsx index 0a107c634..42722fdc1 100644 --- a/packages/website/src/components/ShotCard/index.tsx +++ b/packages/website/src/components/ShotCard/index.tsx @@ -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 ( -
+
@@ -230,7 +231,7 @@ function ShotCardHtml({ shot, className, ...props }: ShotCardHtmlProps) {
)} -
+
); } @@ -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) )} > @@ -379,7 +380,7 @@ export function ShotCard() { @@ -407,8 +408,8 @@ export function ShotCard() { rotation={new Euler().setFromQuaternion( new Quaternion().setFromUnitVectors( J_HAT, - shot.out.surfaceNormal.clone(), - ), + shot.out.surfaceNormal.clone() + ) )} >