From c243ebd19a872ecf610da7ce3bb7a596998c376d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sun, 10 Aug 2025 14:03:13 -0500 Subject: [PATCH] feat: embedded canvas --- packages/scripts/src/buildAssets/tankIcons.ts | 1 + .../scripts/src/buildAssets/tankModels.ts | 3 +- .../Characteristics/components/Firepower.tsx | 8 +- .../components/FlexibilityCanvas.tsx | 46 +++ .../components/FlexibilityCard.tsx | 298 +++++++++++++++++ .../GunFlexibilityVisualizer/index.tsx | 302 ++---------------- .../components/Characteristics/index.tsx | 5 +- .../CharacteristicsSection/index.tsx | 7 +- packages/website/src/hooks/useModel.ts | 1 + .../pages/[...locale]/tanks/[slug]/_index.tsx | 4 +- 10 files changed, 382 insertions(+), 293 deletions(-) create mode 100644 packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/components/FlexibilityCanvas.tsx create mode 100644 packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/components/FlexibilityCard.tsx diff --git a/packages/scripts/src/buildAssets/tankIcons.ts b/packages/scripts/src/buildAssets/tankIcons.ts index 42d4ba309..95cc898c3 100644 --- a/packages/scripts/src/buildAssets/tankIcons.ts +++ b/packages/scripts/src/buildAssets/tankIcons.ts @@ -62,6 +62,7 @@ export async function tankIcons() { const nationVehicleId = tank.id; const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1; + const parameters = await readYAMLDVPL( `${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`, ); diff --git a/packages/scripts/src/buildAssets/tankModels.ts b/packages/scripts/src/buildAssets/tankModels.ts index 5a5895ca2..aeeb0fe70 100644 --- a/packages/scripts/src/buildAssets/tankModels.ts +++ b/packages/scripts/src/buildAssets/tankModels.ts @@ -1,7 +1,7 @@ import { toUniqueId } from '@blitzkit/core'; import { NodeIO } from '@gltf-transform/core'; import { ALL_EXTENSIONS } from '@gltf-transform/extensions'; -import { readdir, writeFile } from 'fs/promises'; +import { readdir } from 'fs/promises'; import { extractModel } from '../core/blitz/extractModel'; import { readXMLDVPL } from '../core/blitz/readXMLDVPL'; import { readYAMLDVPL } from '../core/blitz/readYAMLDVPL'; @@ -32,6 +32,7 @@ export async function tankModels() { if (tankKey.includes('tutorial_bot')) continue; const id = toUniqueId(nation, tank.id); + const parameters = await readYAMLDVPL( `${DATA}/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`, ); diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/Firepower.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/Firepower.tsx index 4f2dd23a2..3ee20f2cb 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/Firepower.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/Firepower.tsx @@ -21,6 +21,7 @@ import { useEquipment } from '../../../../../../hooks/useEquipment'; import { useLocale } from '../../../../../../hooks/useLocale'; import { useTankModelDefinition } from '../../../../../../hooks/useTankModelDefinition'; import { Duel } from '../../../../../../stores/duel'; +import type { MaybeSkeletonComponentProps } from '../../../../../../types/maybeSkeletonComponentProps'; import { AssaultRangesVisualizer } from './AssaultRangesVisualizer'; import { GunFlexibilityVisualizer } from './GunFlexibilityVisualizer'; import { Info } from './Info'; @@ -30,7 +31,10 @@ import { RicochetVisualizer } from './RicochetVisualizer'; import { StatsTableWrapper } from './StatsTableWrapper'; import type { StatsAcceptorProps } from './TraverseVisualizer'; -export function Firepower({ stats }: StatsAcceptorProps) { +export function Firepower({ + stats, + skeleton, +}: StatsAcceptorProps & MaybeSkeletonComponentProps) { const mutateDuel = Duel.useMutation(); const { strings, unwrap } = useLocale(); const [penetrationDistance, setPenetrationDistance] = useState(250); @@ -405,7 +409,7 @@ export function Firepower({ stats }: StatsAcceptorProps) { /> )} - + ); } diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/components/FlexibilityCanvas.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/components/FlexibilityCanvas.tsx new file mode 100644 index 000000000..5cb19ef4e --- /dev/null +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/components/FlexibilityCanvas.tsx @@ -0,0 +1,46 @@ +import { Canvas } from '@react-three/fiber'; +import { awaitableModelDefinitions } from '../../../../../../../../core/awaitables/modelDefinitions'; +import { useModel } from '../../../../../../../../hooks/useModel'; +import { Duel } from '../../../../../../../../stores/duel'; + +const modelDefinitions = await awaitableModelDefinitions; + +export function FlexibilityCanvas() { + const tank = Duel.use((state) => state.protagonist.tank); + const turret = Duel.use((state) => state.protagonist.turret); + + const tankModel = modelDefinitions.models[tank.id]; + const turretModel = tankModel.turrets[turret.id]; + + const { gltf } = useModel(tank.id); + + const zoom = 60; + const originY = tankModel.turret_origin.z + turretModel.gun_origin.z + 1.5; + const originZ = tankModel.turret_origin.y + turretModel.gun_origin.y; + + console.log(originY, originZ); + + return ( + + + + + {/* + + + */} + + + + + + ); +} diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/components/FlexibilityCard.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/components/FlexibilityCard.tsx new file mode 100644 index 000000000..dfc13199c --- /dev/null +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/components/FlexibilityCard.tsx @@ -0,0 +1,298 @@ +import { literals } from '@blitzkit/i18n'; +import { Box } from '@radix-ui/themes'; +import { useRef, useState } from 'react'; +import { radToDeg } from 'three/src/math/MathUtils.js'; +import { awaitableModelDefinitions } from '../../../../../../../../core/awaitables/modelDefinitions'; +import { + DEFAULT_PITCH_TRANSITION, + applyPitchYawLimits, +} from '../../../../../../../../core/blitz/applyPitchYawLimits'; +import { hasEquipment } from '../../../../../../../../core/blitzkit/hasEquipment'; +import { Var } from '../../../../../../../../core/radix/var'; +import { useLocale } from '../../../../../../../../hooks/useLocale'; +import { Duel } from '../../../../../../../../stores/duel'; +import { VisualizerCard } from '../../VisualizerCard'; +import { VisualizerCornerStat } from '../../VisualizerCornerStat'; + +const ANGLE_COEFFICIENT = 1 / 10; + +const modelDefinition = await awaitableModelDefinitions; + +function mag(x: number) { + return ((2 / Math.PI) * Math.atan(ANGLE_COEFFICIENT * x) + 1) / 2; +} + +function c(thetaDeg: number, m = 1) { + const theta = thetaDeg * (Math.PI / 180); + return `${m * Math.cos(theta)} ${-m * Math.sin(theta)}`; +} + +export function FlexibilityCard() { + const { strings } = useLocale(); + + const [yaw, setYaw] = useState(0); + const [minPitch, setMinPitch] = useState(0); + const [maxPitch, setMaxPitch] = useState(0); + + const container = useRef(null); + const highlighter = useRef(null); + const pointer = useRef(null); + + const duelStore = Duel.useStore(); + const tank = Duel.use((state) => state.protagonist.tank); + const turret = Duel.use((state) => state.protagonist.turret); + const gun = Duel.use((state) => state.protagonist.gun); + + const tankModel = modelDefinition.models[tank.id]; + const turretModel = tankModel.turrets[turret.id]; + const gunModel = turretModel.guns[gun.gun_type!.value.base.id]; + + const p = gunModel.pitch; + const y = turretModel.yaw; + const f = p.front; + const b = p.back; + const t = gunModel.pitch.transition ?? DEFAULT_PITCH_TRANSITION; + + let d = ''; + + // right + const mo = mag(p.max); + if (y) { + d += `M ${c(90 - y.max, mo)}`; + } else if (b) { + d += `M ${c(-90 + b.range / 2 + t, mo)}`; + } else { + d += `M ${c(-90, mo)}`; + } + if (f) { + d += `A ${mo} ${mo} 0 0 0 ${c(90 - f.range / 2 - t, mo)}`; + } else { + d += `A ${mo} ${mo} 0 0 0 ${c(90, mo)}`; + } + + // front + if (f) { + const m = mag(f.max); + d += `L ${c(90 - f.range / 2, m)}`; + d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`; + } + + // left + if (f) { + d += `L ${c(90 + f.range / 2 + t, mo)}`; + } else { + d += `L ${c(90, mo)}`; + } + if (y) { + d += `A ${mo} ${mo} 0 0 0 ${c(90 - y.min, mo)}`; + } else if (b) { + d += `A ${mo} ${mo} 0 0 0 ${c(-90 - b.range / 2 - t, mo)}`; + } else { + d += `A ${mo} ${mo} 0 0 0 ${c(-90, mo)}`; + } + + // back + if (!y && b) { + const m = mag(b.max); + d += `L ${c(-90 - b.range / 2, m)}`; + d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`; + } + + d += `Z`; + + // right + const mi = mag(p.min); + if (y) { + d += `M ${c(90 - y.max, mi)}`; + } else if (b) { + d += `M ${c(-90 + b.range / 2 + t, mi)}`; + } else { + d += `M ${c(-90, mi)}`; + } + if (f) { + d += `A ${mi} ${mi} 0 0 0 ${c(90 - f.range / 2 - t, mi)}`; + } else { + d += `A ${mi} ${mi} 0 0 0 ${c(90, mi)}`; + } + + // front + if (f) { + const m = mag(f.min); + d += `L ${c(90 - f.range / 2, m)}`; + d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`; + } + + // left + if (f) { + d += `L ${c(90 + f.range / 2 + t, mi)}`; + } else { + d += `L ${c(90, mi)}`; + } + if (y) { + d += `A ${mi} ${mi} 0 0 0 ${c(90 - y.min, mi)}`; + } else if (b) { + d += `A ${mi} ${mi} 0 0 0 ${c(-90 - b.range / 2 - t, mi)}`; + } else { + d += `A ${mi} ${mi} 0 0 0 ${c(-90, mi)}`; + } + + // back + if (!y && b) { + const m = mag(b.min); + d += `L ${c(-90 - b.range / 2, m)}`; + d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`; + } + + d += `Z`; + + return ( + { + if (!container.current || !highlighter.current || !pointer.current) + return; + + const rect = container.current.getBoundingClientRect(); + const u = event.clientX - rect.left - rect.width / 2; + const v = event.clientY - rect.top - rect.height / 2; + const yaw = Math.atan2(u, -v); + + const { + equipmentMatrix, + tank: { equipment_preset }, + } = duelStore.getState().protagonist; + const hasImprovedVerticalStabilizer = hasEquipment( + 122, + equipment_preset, + equipmentMatrix, + ); + const hasDownImprovedVerticalStabilizer = hasEquipment( + 124, + equipment_preset, + equipmentMatrix, + ); + + const min = applyPitchYawLimits( + -Math.PI, + yaw, + p, + y, + hasImprovedVerticalStabilizer, + hasDownImprovedVerticalStabilizer, + ); + const max = applyPitchYawLimits( + Math.PI, + yaw, + p, + y, + hasImprovedVerticalStabilizer, + hasDownImprovedVerticalStabilizer, + ); + + setYaw(min[1]); + setMinPitch(min[0]); + setMaxPitch(max[0]); + }} + mb="0" + > + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/index.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/index.tsx index 1f1c006d0..99787c426 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/index.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/components/GunFlexibilityVisualizer/index.tsx @@ -1,297 +1,31 @@ -import { literals } from '@blitzkit/i18n'; -import { Box } from '@radix-ui/themes'; -import { useRef, useState } from 'react'; -import { radToDeg } from 'three/src/math/MathUtils.js'; -import { awaitableModelDefinitions } from '../../../../../../../core/awaitables/modelDefinitions'; -import { - applyPitchYawLimits, - DEFAULT_PITCH_TRANSITION, -} from '../../../../../../../core/blitz/applyPitchYawLimits'; -import { hasEquipment } from '../../../../../../../core/blitzkit/hasEquipment'; -import { Var } from '../../../../../../../core/radix/var'; -import { useLocale } from '../../../../../../../hooks/useLocale'; -import { Duel } from '../../../../../../../stores/duel'; -import { VisualizerCard } from '../VisualizerCard'; -import { VisualizerCornerStat } from '../VisualizerCornerStat'; - -const ANGLE_COEFFICIENT = 1 / 10; - -const modelDefinition = await awaitableModelDefinitions; - -function mag(x: number) { - return ((2 / Math.PI) * Math.atan(ANGLE_COEFFICIENT * x) + 1) / 2; -} - -function c(thetaDeg: number, m = 1) { - const theta = thetaDeg * (Math.PI / 180); - return `${m * Math.cos(theta)} ${-m * Math.sin(theta)}`; -} - -export function GunFlexibilityVisualizer() { - const { strings } = useLocale(); - - const [yaw, setYaw] = useState(0); - const [minPitch, setMinPitch] = useState(0); - const [maxPitch, setMaxPitch] = useState(0); - - const container = useRef(null); - const highlighter = useRef(null); - const pointer = useRef(null); - - const duelStore = Duel.useStore(); - const tank = Duel.use((state) => state.protagonist.tank); - const turret = Duel.use((state) => state.protagonist.turret); - const gun = Duel.use((state) => state.protagonist.gun); - - const tankModel = modelDefinition.models[tank.id]; - const turretModel = tankModel.turrets[turret.id]; - const gunModel = turretModel.guns[gun.gun_type!.value.base.id]; - - const p = gunModel.pitch; - const y = turretModel.yaw; - const f = p.front; - const b = p.back; - const t = gunModel.pitch.transition ?? DEFAULT_PITCH_TRANSITION; - - let d = ''; - - // right - const mo = mag(p.max); - if (y) { - d += `M ${c(90 - y.max, mo)}`; - } else if (b) { - d += `M ${c(-90 + b.range / 2 + t, mo)}`; - } else { - d += `M ${c(-90, mo)}`; - } - if (f) { - d += `A ${mo} ${mo} 0 0 0 ${c(90 - f.range / 2 - t, mo)}`; - } else { - d += `A ${mo} ${mo} 0 0 0 ${c(90, mo)}`; - } - - // front - if (f) { - const m = mag(f.max); - d += `L ${c(90 - f.range / 2, m)}`; - d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`; - } - - // left - if (f) { - d += `L ${c(90 + f.range / 2 + t, mo)}`; - } else { - d += `L ${c(90, mo)}`; - } - if (y) { - d += `A ${mo} ${mo} 0 0 0 ${c(90 - y.min, mo)}`; - } else if (b) { - d += `A ${mo} ${mo} 0 0 0 ${c(-90 - b.range / 2 - t, mo)}`; - } else { - d += `A ${mo} ${mo} 0 0 0 ${c(-90, mo)}`; - } - - // back - if (!y && b) { - const m = mag(b.max); - d += `L ${c(-90 - b.range / 2, m)}`; - d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`; - } - - d += `Z`; - - // right - const mi = mag(p.min); - if (y) { - d += `M ${c(90 - y.max, mi)}`; - } else if (b) { - d += `M ${c(-90 + b.range / 2 + t, mi)}`; - } else { - d += `M ${c(-90, mi)}`; - } - if (f) { - d += `A ${mi} ${mi} 0 0 0 ${c(90 - f.range / 2 - t, mi)}`; - } else { - d += `A ${mi} ${mi} 0 0 0 ${c(90, mi)}`; - } - - // front - if (f) { - const m = mag(f.min); - d += `L ${c(90 - f.range / 2, m)}`; - d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`; - } - - // left - if (f) { - d += `L ${c(90 + f.range / 2 + t, mi)}`; - } else { - d += `L ${c(90, mi)}`; - } - if (y) { - d += `A ${mi} ${mi} 0 0 0 ${c(90 - y.min, mi)}`; - } else if (b) { - d += `A ${mi} ${mi} 0 0 0 ${c(-90 - b.range / 2 - t, mi)}`; - } else { - d += `A ${mi} ${mi} 0 0 0 ${c(-90, mi)}`; - } - - // back - if (!y && b) { - const m = mag(b.min); - d += `L ${c(-90 - b.range / 2, m)}`; - d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`; - } - - d += `Z`; +import { Box, Flex } from '@radix-ui/themes'; +import type { MaybeSkeletonComponentProps } from '../../../../../../../types/maybeSkeletonComponentProps'; +import { FlexibilityCanvas } from './components/FlexibilityCanvas'; +import { FlexibilityCard } from './components/FlexibilityCard'; +export function GunFlexibilityVisualizer({ + skeleton, +}: MaybeSkeletonComponentProps) { return ( - { - if (!container.current || !highlighter.current || !pointer.current) - return; + + - const rect = container.current.getBoundingClientRect(); - const u = event.clientX - rect.left - rect.width / 2; - const v = event.clientY - rect.top - rect.height / 2; - const yaw = Math.atan2(u, -v); + + + {!skeleton && } + - const { - equipmentMatrix, - tank: { equipment_preset }, - } = duelStore.getState().protagonist; - const hasImprovedVerticalStabilizer = hasEquipment( - 122, - equipment_preset, - equipmentMatrix, - ); - const hasDownImprovedVerticalStabilizer = hasEquipment( - 124, - equipment_preset, - equipmentMatrix, - ); - - const min = applyPitchYawLimits( - -Math.PI, - yaw, - p, - y, - hasImprovedVerticalStabilizer, - hasDownImprovedVerticalStabilizer, - ); - const max = applyPitchYawLimits( - Math.PI, - yaw, - p, - y, - hasImprovedVerticalStabilizer, - hasDownImprovedVerticalStabilizer, - ); - - setYaw(min[1]); - setMinPitch(min[0]); - setMaxPitch(max[0]); - }} - > - - - - - - - - - - - - - - - - - - + ); } diff --git a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/index.tsx b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/index.tsx index 5e5de9e70..8d5a5e283 100644 --- a/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/index.tsx +++ b/packages/website/src/components/Tankopedia/CharacteristicsSection/components/Characteristics/index.tsx @@ -8,6 +8,7 @@ import { useEquipment } from '../../../../../hooks/useEquipment'; import { useTankModelDefinition } from '../../../../../hooks/useTankModelDefinition'; import { Duel } from '../../../../../stores/duel'; import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral'; +import type { MaybeSkeletonComponentProps } from '../../../../../types/maybeSkeletonComponentProps'; import { Crew } from './components/Crew'; import { Firepower } from './components/Firepower'; import { Maneuverability } from './components/Maneuverability'; @@ -18,7 +19,7 @@ const [equipmentDefinitions, provisionDefinitions] = await Promise.all([ awaitableProvisionDefinitions, ]); -export function Characteristics() { +export function Characteristics({ skeleton }: MaybeSkeletonComponentProps) { const mutateDuel = Duel.useMutation(); const crewSkills = TankopediaEphemeral.use((state) => state.skills); const provisions = Duel.use((state) => state.protagonist.provisions); @@ -93,7 +94,7 @@ export function Characteristics() { align={{ initial: 'center', sm: 'start' }} width={{ initial: '100%', sm: 'auto' }} > - + - + diff --git a/packages/website/src/hooks/useModel.ts b/packages/website/src/hooks/useModel.ts index c6036b21f..525cb051a 100644 --- a/packages/website/src/hooks/useModel.ts +++ b/packages/website/src/hooks/useModel.ts @@ -25,5 +25,6 @@ export function useModel(id: number) { ), }; } + return cache[id]; } diff --git a/packages/website/src/pages/[...locale]/tanks/[slug]/_index.tsx b/packages/website/src/pages/[...locale]/tanks/[slug]/_index.tsx index b7961104a..2dfae6d74 100644 --- a/packages/website/src/pages/[...locale]/tanks/[slug]/_index.tsx +++ b/packages/website/src/pages/[...locale]/tanks/[slug]/_index.tsx @@ -54,9 +54,9 @@ export function Page({ id, skeleton, locale, guide }: PageProps) { {tank.type === TankType.RESEARCHABLE && !tank.deprecated && ( )} - + - {guide && } + {guide && }