diff --git a/public/test/24609.glb b/public/test/24609.glb new file mode 100644 index 000000000..636a3b032 Binary files /dev/null and b/public/test/24609.glb differ diff --git a/scripts/buildAssets.ts b/scripts/buildAssets.ts index 0a6d69627..1b5d0efdb 100644 --- a/scripts/buildAssets.ts +++ b/scripts/buildAssets.ts @@ -17,14 +17,13 @@ import { toUniqueId } from '../src/core/blitz/toUniqueId'; import commitMultipleFiles, { FileChange, } from '../src/core/blitzkrieg/commitMultipleFiles'; +import { ModelDefinitions } from '../src/core/blitzkrieg/modelDefinitions'; import { GunDefinition, - ShellDefinition, ShellType, TankDefinitionPrice, TankDefinitions, Tier, - TurretDefinition, } from '../src/core/blitzkrieg/tankDefinitions'; config(); @@ -113,6 +112,11 @@ interface VehicleDefinitions { [key: string]: { reloadTime: number; maxAmmo: number; + extraPitchLimits?: { + front?: string; + back?: string; + transition?: number | number[]; + }; pitchLimits?: string | string[]; pumpGunMode?: boolean; pumpGunReloadTimes?: string; @@ -174,6 +178,7 @@ if (allTargets || targets?.includes('definitions')) { console.log('Building tank definitions...'); const tankDefinitions: TankDefinitions = {}; + const modelDefinitions: ModelDefinitions = {}; const nations = await readdir(`${DATA}/${DOI.vehicleDefinitions}`).then( (nations) => nations.filter((nation) => nation !== 'common'), ); @@ -216,8 +221,40 @@ if (allTargets || targets?.includes('definitions')) { .map(Number) as Vector3Tuple; const tankId = toUniqueId(nation, tank.id); const tankTags = tank.tags.split(' '); - const tankTurrets = Object.keys(tankDefinition.root.turrets0).map( - (turretKey) => { + + tankDefinitions[tankId] = { + id: tankId, + name: + (tank.shortUserString + ? strings[tank.shortUserString] + : undefined) ?? + strings[tank.userString] ?? + tankKey.replaceAll('_', ' '), + nation, + tree_type: (tank.sellPrice ? 'gold' in tank.sellPrice : false) + ? 'collector' + : (typeof tank.price === 'number' ? false : 'gold' in tank.price) + ? 'premium' + : 'researchable', + tier: tank.level as Tier, + type: blitzTankTypeToBlitzkrieg[tankTags[0] as BlitzTankType], + testing: tankTags.includes('testTank') ? true : undefined, + price: tankPrice, + camouflage: { + still: tankDefinition.root.invisibility.still, + moving: tankDefinition.root.invisibility.moving, + firing: tankDefinition.root.invisibility.firePenalty, + }, + turrets: [], + }; + + modelDefinitions[tankId] = { + turretOrigin, + turrets: {}, + }; + + Object.keys(tankDefinition.root.turrets0).forEach( + (turretKey, turretIndex) => { const turret = tankDefinition.root.turrets0[turretKey]; const turretModel = Number( parse(turret.models.undamaged).name.split('_')[1], @@ -237,7 +274,29 @@ if (allTargets || targets?.includes('definitions')) { ) .split(' ') .map(Number) as Vector3Tuple; - const turretGuns = Object.keys(turret.guns).map((gunKey) => { + + tankDefinitions[tankId].turrets.push({ + id: turretId, + name: + strings[turret.userString] ?? + turretKey + .replaceAll('_', ' ') + .replace(/^(Turret ([0-9] )?)+/, ''), + tier: turret.level as Tier, + guns: [], + }); + + modelDefinitions[tankId].turrets[turretId] = { + gunOrigin, + model: turretModel, + yaw: + -turretYaw[0] + turretYaw[1] === 360 + ? undefined + : { min: turretYaw[0], max: turretYaw[1] }, + guns: {}, + }; + + Object.keys(turret.guns).forEach((gunKey, gunIndex) => { const turretGunEntry = turret.guns[gunKey]; const gunId = toUniqueId(nation, gunList.root.ids[gunKey]); const gun = gunList.root.shared[gunKey]; @@ -271,14 +330,63 @@ if (allTargets || targets?.includes('definitions')) { gunType === 'regular' ? undefined : 60 / turretGunEntry.clip!.rate; - const gunShells = Object.keys(gun.shots).map((shellKey) => { + const front = turretGunEntry.extraPitchLimits?.front + ? turretGunEntry.extraPitchLimits.front.split(' ').map(Number) + : undefined; + const back = turretGunEntry.extraPitchLimits?.back + ? turretGunEntry.extraPitchLimits.back.split(' ').map(Number) + : undefined; + const transition = turretGunEntry.extraPitchLimits?.transition + ? typeof turretGunEntry.extraPitchLimits.transition === 'number' + ? turretGunEntry.extraPitchLimits.transition + : turretGunEntry.extraPitchLimits.transition.at(-1)! + : undefined; + + tankDefinitions[tankId].turrets[turretIndex].guns.push({ + id: gunId, + name: gunName, + tier: gun.level as Tier, + shells: [], + type: gunType, + reload: gunReload, + count: gunClipCount, + interClip: gunInterClip, + } as GunDefinition); + + modelDefinitions[tankId].turrets[turretId].guns[gunId] = { + model: gunModel, + pitch: { + min: gunPitch[0], + max: gunPitch[1], + + front: front + ? { + min: front[0], + max: front[1], + range: front[2], + } + : undefined, + back: back + ? { + min: back[0], + max: back[1], + range: back[2], + } + : undefined, + transition, + }, + }; + + Object.keys(gun.shots).forEach((shellKey) => { const turretShellEntry = gun.shots[shellKey]; const shell = shellList.root[shellKey]; const shellId = toUniqueId(nation, shell.id); const shellName = strings[shell.userString] ?? shellKey.replaceAll('_', ' '); - return { + tankDefinitions[tankId].turrets[turretIndex].guns[ + gunIndex + ].shells.push({ id: shellId, name: shellName, speed: turretShellEntry.speed, @@ -291,65 +399,11 @@ if (allTargets || targets?.includes('definitions')) { ricochet: shell.ricochetAngle, type: blitzShellKindToBLitzkrieg[shell.kind], icon: shell.icon, - } satisfies ShellDefinition; + }); }); - - return { - id: gunId, - pitch: gunPitch, - name: gunName, - tier: gun.level as Tier, - shells: gunShells, - type: gunType, - reload: gunReload, - count: gunClipCount, - interClip: gunInterClip, - model: gunModel, - } as GunDefinition; }); - - return { - id: turretId, - name: - strings[turret.userString] ?? - turretKey - .replaceAll('_', ' ') - .replace(/^(Turret ([0-9] )?)+/, ''), - tier: turret.level as Tier, - yaw: -turretYaw[0] + turretYaw[1] === 360 ? undefined : turretYaw, - guns: turretGuns, - gunOrigin, - model: turretModel, - } satisfies TurretDefinition; }, ); - - tankDefinitions[tankId] = { - id: tankId, - name: - (tank.shortUserString - ? strings[tank.shortUserString] - : undefined) ?? - strings[tank.userString] ?? - tankKey.replaceAll('_', ' '), - nation, - turretOrigin, - tree_type: (tank.sellPrice ? 'gold' in tank.sellPrice : false) - ? 'collector' - : (typeof tank.price === 'number' ? false : 'gold' in tank.price) - ? 'premium' - : 'researchable', - tier: tank.level as Tier, - type: blitzTankTypeToBlitzkrieg[tankTags[0] as BlitzTankType], - testing: tankTags.includes('testTank') ? true : undefined, - turrets: tankTurrets, - price: tankPrice, - camouflage: { - still: tankDefinition.root.invisibility.still, - moving: tankDefinition.root.invisibility.moving, - firing: tankDefinition.root.invisibility.firePenalty, - }, - }; } }), ); @@ -366,6 +420,11 @@ if (allTargets || targets?.includes('definitions')) { encoding: 'utf-8', path: 'definitions/tanks.json', }, + { + content: JSON.stringify(modelDefinitions), + encoding: 'utf-8', + path: 'definitions/models.json', + }, ], true, ); @@ -527,13 +586,13 @@ if (allTargets || targets?.includes('tankModels')) { const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1; // if (id !== 15697) continue; // chieftain TODO: investigate vertices stream over read - // if (id !== 24609) continue; // concept 1b + if (id !== 24609) continue; // concept 1b // if (id !== 4417) continue; // amx m4 mle // if (id !== 7297) continue; // 60tp // if (id !== 1) continue; // t-34 // if (id !== 6753) continue; // type 71 // if (id !== 5137) continue; // tiger ii - if (id !== 11633) continue; // forest witch + // if (id !== 11633) continue; // forest witch // if (id !== 6225) continue; // fv215b console.log(`Building model ${id} @ ${nation}/${tankIndex}`); diff --git a/src/app/tools/tankopedia/[id]/page.tsx b/src/app/tools/tankopedia/[id]/page.tsx index b3c865b6e..30968cbda 100644 --- a/src/app/tools/tankopedia/[id]/page.tsx +++ b/src/app/tools/tankopedia/[id]/page.tsx @@ -31,6 +31,7 @@ import InfiniteGridHelper from '../../../../components/InfiniteGridHelper'; import { ModuleButtons } from '../../../../components/ModuleButton'; import PageWrapper from '../../../../components/PageWrapper'; import { asset } from '../../../../core/blitzkrieg/asset'; +import { modelDefinitions } from '../../../../core/blitzkrieg/modelDefinitions'; import { resolveJsxTree } from '../../../../core/blitzkrieg/resolveJsxTree'; import { TIER_ROMAN_NUMERALS, @@ -41,13 +42,19 @@ import { normalizeAngle180 } from '../../../../core/math/normalizeAngle180'; import * as styles from '../page.css'; import { TankAlignment } from './components/tankAlignment'; +const X_AXIS = new Vector3(1, 0, 0); + export default function Page({ params }: { params: { id: string } }) { const id = parseInt(params.id); const awaitedTankDefinitions = use(tankDefinitions); + const awaitedModelDefinitions = use(modelDefinitions); const awaitedTankNamesDiacritics = use(tankNamesDiacritics); const tank = awaitedTankDefinitions[id]; + const tankModelDefinition = awaitedModelDefinitions[id]; const [turret, setTurret] = useState(tank.turrets.at(-1)!); + const turretModelDefinition = tankModelDefinition.turrets[turret.id]; const [gun, setGun] = useState(turret.guns.at(-1)!); + const gunModelDefinition = turretModelDefinition.guns[gun.id]; const [crew, setCrew] = useState(100); const [mode, setMode] = useState('model'); const versusTankSearchInput = useRef(null); @@ -63,15 +70,15 @@ export default function Page({ params }: { params: { id: string } }) { const canvas = useRef(null); const gltf = useLoader(GLTFLoader, `/test/${id}.glb`); const turretOrigin = new Vector3( - tank.turretOrigin[0], - tank.turretOrigin[1], - -tank.turretOrigin[2], - ).applyAxisAngle(new Vector3(1, 0, 0), Math.PI / 2); + tankModelDefinition.turretOrigin[0], + tankModelDefinition.turretOrigin[1], + -tankModelDefinition.turretOrigin[2], + ).applyAxisAngle(X_AXIS, Math.PI / 2); const gunOrigin = new Vector3( - turret.gunOrigin[0], - turret.gunOrigin[1], - -turret.gunOrigin[2], - ).applyAxisAngle(new Vector3(1, 0, 0), Math.PI / 2); + turretModelDefinition.gunOrigin[0], + turretModelDefinition.gunOrigin[1], + -turretModelDefinition.gunOrigin[2], + ).applyAxisAngle(X_AXIS, Math.PI / 2); const [turretYaw, setTurretYaw] = useState(0); const [hullYaw, setHullYaw] = useState(0); const [mantletPitch, setMantletPitch] = useState(0); @@ -266,7 +273,7 @@ export default function Page({ params }: { params: { id: string } }) { const isCurrentTurret = isTurret && child.name === - `turret_${turret.model + `turret_${turretModelDefinition.model .toString() .padStart(2, '0')}`; const isVisible = isCurrentTurret; @@ -358,13 +365,17 @@ export default function Page({ params }: { params: { id: string } }) { child.name.endsWith('_mask'); const isCurrentMantlet = child.name === - `gun_${gun.model.toString().padStart(2, '0')}_mask`; + `gun_${gunModelDefinition.model + .toString() + .padStart(2, '0')}_mask`; const isGun = child.name.startsWith('gun_') && !isMantlet; const isCurrentGun = isGun && child.name === - `gun_${gun.model.toString().padStart(2, '0')}`; + `gun_${gunModelDefinition.model + .toString() + .padStart(2, '0')}`; const isVisible = isCurrentGun || isCurrentMantlet; let draftMantletPitch = 0; @@ -397,8 +408,8 @@ export default function Page({ params }: { params: { id: string } }) { draftMantletPitch += delta; draftMantletPitch = clamp( draftMantletPitch, - -gun.pitch[1] * (Math.PI / 180), - -gun.pitch[0] * (Math.PI / 180), + -gunModelDefinition.pitch.max * (Math.PI / 180), + -gunModelDefinition.pitch.min * (Math.PI / 180), ); if (mantletContainer.current) { @@ -539,8 +550,8 @@ export default function Page({ params }: { params: { id: string } }) { Number(gunRotationInput.current?.value) * (Math.PI / 180), ), - -gun.pitch[1] * (Math.PI / 180), - -gun.pitch[0] * (Math.PI / 180), + -gunModelDefinition.pitch.min * (Math.PI / 180), + -gunModelDefinition.pitch.max * (Math.PI / 180), ), ); }} diff --git a/src/core/blitzkrieg/modelDefinitions.ts b/src/core/blitzkrieg/modelDefinitions.ts new file mode 100644 index 000000000..6fe3d1ffa --- /dev/null +++ b/src/core/blitzkrieg/modelDefinitions.ts @@ -0,0 +1,29 @@ +import { Vector3Tuple } from 'three'; +import { asset } from './asset'; + +export type ModelDefinitions = Record; +interface ModelDefinition { + turretOrigin: Vector3Tuple; + turrets: Record; +} +interface TurretModelDefinition { + model: number; + gunOrigin: Vector3Tuple; + guns: Record; + yaw?: { min: number; max: number }; +} +interface GunModelDefinition { + model: number; + pitch: { + min: number; + max: number; + + front?: { min: number; max: number; range: number }; + back?: { min: number; max: number; range: number }; + transition?: number; + }; +} + +export const modelDefinitions = fetch(asset('definitions/models.json'), { + cache: 'no-cache', +}).then(async (response) => response.json() as Promise); diff --git a/src/core/blitzkrieg/tankDefinitions.ts b/src/core/blitzkrieg/tankDefinitions.ts index 58c791d74..d047584a8 100644 --- a/src/core/blitzkrieg/tankDefinitions.ts +++ b/src/core/blitzkrieg/tankDefinitions.ts @@ -1,5 +1,4 @@ import { deburr } from 'lodash'; -import { Vector3Tuple } from 'three'; import { TankType, TreeTypeString } from '../../components/Tanks'; import { asset } from './asset'; @@ -10,7 +9,6 @@ export interface TankDefinition { id: number; nation: string; name: string; - turretOrigin: Vector3Tuple; tree_type: TreeTypeString; tier: Tier; type: TankType; @@ -26,20 +24,15 @@ export interface TurretDefinition { id: number; name: string; tier: Tier; - yaw?: [number, number]; guns: GunDefinition[]; - gunOrigin: Vector3Tuple; - model: number; } export type GunDefinition = GunDefinitionBase & (GunDefinitionRegular | GunDefinitionAutoLoader | GunDefinitionAutoReloader); -interface GunDefinitionBase { +interface GunDefinitionBase { id: number; name: string; tier: Tier; - pitch: [number, number]; shells: ShellDefinition[]; - model: number; } interface GunDefinitionRegular { type: 'regular';