diff --git a/scripts/buildAssets/definitions.ts b/scripts/buildAssets/definitions.ts index 928a1fc09..ec898bd47 100644 --- a/scripts/buildAssets/definitions.ts +++ b/scripts/buildAssets/definitions.ts @@ -6,7 +6,10 @@ import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL'; import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL'; import { toUniqueId } from '../../src/core/blitz/toUniqueId'; import commitMultipleFiles from '../../src/core/blitzkrieg/commitMultipleFiles'; -import { ModelDefinitions } from '../../src/core/blitzkrieg/modelDefinitions'; +import { + ModelArmor, + ModelDefinitions, +} from '../../src/core/blitzkrieg/modelDefinitions'; import { GunDefinition, ShellType, @@ -40,14 +43,20 @@ type ShellDefinitionsList = Record< > & { icons: Record; }; +type VehicleDefinitionArmor = Record< + string, + number | { vehicleDamageFactor: 0; '#text': number } +>; interface VehicleDefinitions { invisibility: { moving: number; still: number; firePenalty: number }; hull: { + armor: VehicleDefinitionArmor; turretPositions: { turret: string }; turretInitialRotation?: { yaw: 0; pitch: 6.5; roll: 0 }; }; turrets0: { [key: string]: { + armor: VehicleDefinitionArmor; userString: number; level: number; yawLimits: string | string[]; @@ -55,6 +64,7 @@ interface VehicleDefinitions { models: { undamaged: string }; guns: { [key: string]: { + armor: VehicleDefinitionArmor; reloadTime: number; maxAmmo: number; extraPitchLimits?: { @@ -174,6 +184,28 @@ export async function buildDefinitions() { .map(Number) as Vector3Tuple; const tankId = toUniqueId(nation, tank.id); const tankTags = tank.tags.split(' '); + const hullArmor: ModelArmor = { thickness: {} }; + + Object.keys(tankDefinition.root.hull.armor) + .filter((name) => name.startsWith('armor_')) + .forEach((name) => { + const armorIdString = name.match(/armor_(\d+)/)?.[1]; + + if (armorIdString === undefined) { + throw new SyntaxError(`Invalid armor id: ${name}`); + } + + const armorId = parseInt(armorIdString); + const armorRaw = tankDefinition.root.hull.armor[name]; + + if (typeof armorRaw === 'number') { + hullArmor.thickness[armorId] = armorRaw; + } else { + if (!hullArmor.spaced) hullArmor.spaced = []; + hullArmor.thickness[armorId] = armorRaw['#text']; + hullArmor.spaced.push(armorId); + } + }); tankDefinitions[tankId] = { id: tankId, @@ -202,6 +234,7 @@ export async function buildDefinitions() { }; modelDefinitions[tankId] = { + armor: hullArmor, turretOrigin, turretRotation: tankDefinition.root.hull.turretInitialRotation ? { @@ -234,6 +267,28 @@ export async function buildDefinitions() { ) .split(' ') .map(Number) as Vector3Tuple; + const turretArmor: ModelArmor = { thickness: {} }; + + Object.keys(turret.armor) + .filter((name) => name.startsWith('armor_')) + .forEach((name) => { + const armorIdString = name.match(/armor_(\d+)/)?.[1]; + + if (armorIdString === undefined) { + throw new SyntaxError(`Invalid armor id: ${name}`); + } + + const armorId = parseInt(armorIdString); + const armorRaw = turret.armor[name]; + + if (typeof armorRaw === 'number') { + turretArmor.thickness[armorId] = armorRaw; + } else { + if (!turretArmor.spaced) turretArmor.spaced = []; + turretArmor.thickness[armorId] = armorRaw['#text']; + turretArmor.spaced.push(armorId); + } + }); tankDefinitions[tankId].turrets.push({ id: turretId, @@ -247,6 +302,7 @@ export async function buildDefinitions() { }); modelDefinitions[tankId].turrets[turretId] = { + armor: turretArmor, gunOrigin, model: turretModel, yaw: @@ -301,6 +357,25 @@ export async function buildDefinitions() { ? turretGunEntry.extraPitchLimits.transition : turretGunEntry.extraPitchLimits.transition.at(-1)! : undefined; + const gunArmor: ModelArmor = { thickness: {} }; + + Object.keys(turretGunEntry.armor) + .filter((name) => name.startsWith('armor_')) + .forEach((name) => { + const armorIdString = name.match(/armor_(\d+)/)?.[1]; + if (armorIdString === undefined) { + throw new SyntaxError(`Invalid armor id: ${name}`); + } + const armorId = parseInt(armorIdString); + const armorRaw = turretGunEntry.armor[name]; + if (typeof armorRaw === 'number') { + gunArmor.thickness[armorId] = armorRaw; + } else { + if (!gunArmor.spaced) gunArmor.spaced = []; + gunArmor.thickness[armorId] = armorRaw['#text']; + gunArmor.spaced.push(armorId); + } + }); tankDefinitions[tankId].turrets[turretIndex].guns.push({ id: gunId, @@ -314,6 +389,7 @@ export async function buildDefinitions() { } as GunDefinition); modelDefinitions[tankId].turrets[turretId].guns[gunId] = { + armor: gunArmor, model: gunModel, pitch: { min: gunPitch[0], diff --git a/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx b/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx index 06a5571de..abd477c6e 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/TankDisplay.tsx @@ -19,6 +19,7 @@ import { Loader } from '../../../../components/Loader'; import { Controls } from '../Control'; import { RotationInputs } from '../RotationInputs'; import { SceneProps } from '../SceneProps'; +import { TankArmor } from './components/TankArmor'; import { TankModel } from './components/TankModel'; export function TankDisplay() { @@ -131,6 +132,7 @@ export function TankDisplay() { } > + {mode === 'armor' && } diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor.tsx new file mode 100644 index 000000000..a0e1e29fa --- /dev/null +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankArmor.tsx @@ -0,0 +1,81 @@ +import { useLoader } from '@react-three/fiber'; +import { + forwardRef, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react'; +import { Group, Mesh, Vector3 } from 'three'; +import { GLTFLoader } from 'three-stdlib'; +import { X_AXIS } from '../../../../../../../constants/axis'; +import { asset } from '../../../../../../../core/blitzkrieg/asset'; +import { + ModelDefinitions, + modelDefinitions, +} from '../../../../../../../core/blitzkrieg/modelDefinitions'; +import { useTankopedia } from '../../../../../../../stores/tankopedia'; + +export const TankArmor = forwardRef((_props, ref) => { + const hullContainer = useRef(null); + const protagonist = useTankopedia((state) => { + if (!state.areTanksAssigned) return; + return state.protagonist; + }); + const [awaitedModelDefinitions, setAwaitedModelDefinitions] = useState< + ModelDefinitions | undefined + >(undefined); + + useEffect(() => { + (async () => { + setAwaitedModelDefinitions(await modelDefinitions); + })(); + }, []); + + useImperativeHandle(ref, () => hullContainer.current!); + + if (!awaitedModelDefinitions) return null; + if (!protagonist) return null; + + const tankModelDefinition = awaitedModelDefinitions[protagonist.tank.id]; + const turretModelDefinition = + tankModelDefinition.turrets[protagonist.turret.id]; + const turretOrigin = new Vector3( + tankModelDefinition.turretOrigin[0], + tankModelDefinition.turretOrigin[1], + -tankModelDefinition.turretOrigin[2], + ).applyAxisAngle(X_AXIS, Math.PI / 2); + const gunOrigin = new Vector3( + turretModelDefinition.gunOrigin[0], + turretModelDefinition.gunOrigin[1], + -turretModelDefinition.gunOrigin[2], + ).applyAxisAngle(X_AXIS, Math.PI / 2); + const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id]; + const gltf = useLoader( + GLTFLoader, + asset(`3d/tanks/armor/${protagonist.tank.id}.glb`), + ); + + return ( + + {Object.values(gltf.nodes).map((node) => { + const armor = Number(node.name.match(/.+_armor_(\d+)/)?.[1]); + const position = new Vector3(); + + if (node.name.startsWith('turret_')) position.add(turretOrigin); + if (node.name.startsWith('gun_')) + position.add(turretOrigin).add(gunOrigin); + + return ( + + + + ); + })} + + ); +}); diff --git a/src/app/tools/tankopedia/[id]/components/GunContainer.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/GunContainer.tsx similarity index 93% rename from src/app/tools/tankopedia/[id]/components/GunContainer.tsx rename to src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/GunContainer.tsx index 455e5c1e8..e6d345681 100644 --- a/src/app/tools/tankopedia/[id]/components/GunContainer.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/GunContainer.tsx @@ -1,14 +1,14 @@ import { ThreeEvent, useThree } from '@react-three/fiber'; import { RefObject, forwardRef, useImperativeHandle, useRef } from 'react'; import { Group, Mesh, Object3D, Vector3 } from 'three'; -import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits'; +import { applyPitchYawLimits } from '../../../../../../../../../core/blitz/applyPitchYawLimits'; import { InitialTurretRotation, PitchLimits, YawLimits, -} from '../../../../../core/blitzkrieg/modelDefinitions'; -import { resolveJsxTree } from '../../../../../core/blitzkrieg/resolveJsxTree'; -import { normalizeAnglePI } from '../../../../../core/math/normalizeAngle180'; +} from '../../../../../../../../../core/blitzkrieg/modelDefinitions'; +import { resolveJsxTree } from '../../../../../../../../../core/blitzkrieg/resolveJsxTree'; +import { normalizeAnglePI } from '../../../../../../../../../core/math/normalizeAngle180'; interface GunContainerProps { turretOrigin: Vector3; diff --git a/src/app/tools/tankopedia/[id]/components/HullContainer.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/HullContainer.tsx similarity index 95% rename from src/app/tools/tankopedia/[id]/components/HullContainer.tsx rename to src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/HullContainer.tsx index 31bd1d96a..a681ad0ed 100644 --- a/src/app/tools/tankopedia/[id]/components/HullContainer.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/HullContainer.tsx @@ -1,8 +1,8 @@ import { ThreeEvent, useThree } from '@react-three/fiber'; import { ReactNode, forwardRef, useImperativeHandle, useRef } from 'react'; import { Group, Mesh, MeshStandardMaterial, Object3D, Vector2 } from 'three'; -import { resolveJsxTree } from '../../../../../core/blitzkrieg/resolveJsxTree'; -import { normalizeAnglePI } from '../../../../../core/math/normalizeAngle180'; +import { resolveJsxTree } from '../../../../../../../../../core/blitzkrieg/resolveJsxTree'; +import { normalizeAnglePI } from '../../../../../../../../../core/math/normalizeAngle180'; interface HullContainerProps { children: ReactNode; diff --git a/src/app/tools/tankopedia/[id]/components/TurretContainer.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/TurretContainer.tsx similarity index 93% rename from src/app/tools/tankopedia/[id]/components/TurretContainer.tsx rename to src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/TurretContainer.tsx index c4bc80e1a..1c55c4e78 100644 --- a/src/app/tools/tankopedia/[id]/components/TurretContainer.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/components/TurretContainer.tsx @@ -7,14 +7,14 @@ import { useRef, } from 'react'; import { Euler, Group, Mesh, Object3D, Vector3 } from 'three'; -import { applyPitchYawLimits } from '../../../../../core/blitz/applyPitchYawLimits'; +import { applyPitchYawLimits } from '../../../../../../../../../core/blitz/applyPitchYawLimits'; import { InitialTurretRotation, PitchLimits, YawLimits, -} from '../../../../../core/blitzkrieg/modelDefinitions'; -import { resolveJsxTree } from '../../../../../core/blitzkrieg/resolveJsxTree'; -import { normalizeAnglePI } from '../../../../../core/math/normalizeAngle180'; +} from '../../../../../../../../../core/blitzkrieg/modelDefinitions'; +import { resolveJsxTree } from '../../../../../../../../../core/blitzkrieg/resolveJsxTree'; +import { normalizeAnglePI } from '../../../../../../../../../core/math/normalizeAngle180'; interface TurretContainerProps { turretOrigin: Vector3; diff --git a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/index.tsx similarity index 91% rename from src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx rename to src/app/tools/tankopedia/[id]/components/Model/components/TankModel/index.tsx index e315dd18f..427bf1ebf 100644 --- a/src/app/tools/tankopedia/[id]/components/Model/components/TankModel.tsx +++ b/src/app/tools/tankopedia/[id]/components/Model/components/TankModel/index.tsx @@ -8,18 +8,18 @@ import { } from 'react'; import { Group, Vector3 } from 'three'; import { GLTFLoader } from 'three-stdlib'; -import { X_AXIS } from '../../../../../../../constants/axis'; -import { asset } from '../../../../../../../core/blitzkrieg/asset'; +import { X_AXIS } from '../../../../../../../../constants/axis'; +import { asset } from '../../../../../../../../core/blitzkrieg/asset'; import { ModelDefinitions, modelDefinitions, -} from '../../../../../../../core/blitzkrieg/modelDefinitions'; +} from '../../../../../../../../core/blitzkrieg/modelDefinitions'; import mutateTankopedia, { useTankopedia, -} from '../../../../../../../stores/tankopedia'; -import { GunContainer } from '../../GunContainer'; -import { HullContainer } from '../../HullContainer'; -import { TurretContainer } from '../../TurretContainer'; +} from '../../../../../../../../stores/tankopedia'; +import { GunContainer } from './components/GunContainer'; +import { HullContainer } from './components/HullContainer'; +import { TurretContainer } from './components/TurretContainer'; export const TankModel = forwardRef((_props, ref) => { // "TypeError: dispatcher.use is not a function" diff --git a/src/core/blitzkrieg/modelDefinitions.ts b/src/core/blitzkrieg/modelDefinitions.ts index 4ff4556cf..7ec27447c 100644 --- a/src/core/blitzkrieg/modelDefinitions.ts +++ b/src/core/blitzkrieg/modelDefinitions.ts @@ -2,7 +2,14 @@ import { Vector3Tuple } from 'three'; import { asset } from './asset'; export type ModelDefinitions = Record; + +export interface ModelArmor { + thickness: Record; + spaced?: number[]; +} + interface ModelDefinition { + armor: ModelArmor; turretOrigin: Vector3Tuple; turretRotation?: InitialTurretRotation; turrets: Record; @@ -13,6 +20,7 @@ export interface InitialTurretRotation { roll: number; } interface TurretModelDefinition { + armor: ModelArmor; model: number; gunOrigin: Vector3Tuple; guns: Record; @@ -23,6 +31,7 @@ export interface YawLimits { max: number; } interface GunModelDefinition { + armor: ModelArmor; model: number; pitch: PitchLimits; }