-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathM48TankSpawn.ts
More file actions
268 lines (245 loc) · 10.3 KB
/
Copy pathM48TankSpawn.ts
File metadata and controls
268 lines (245 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (c) 2025-2026 Matthew Kissinger
import * as THREE from 'three';
import { Faction } from '../combat/types';
import { Tank } from './Tank';
import { M48_HULL_DIMENSIONS, M48_PHYSICS_CONFIG, M48_SPAWN_OFFSETS } from '../../config/vehicles/m48-config';
import { applyM48TankGlbVisual } from './VehicleGlbVisuals';
import type { VehicleManager } from './VehicleManager';
/**
* M48 Patton chassis mesh + scenario-spawn glue. Mirrors the shape of
* `M2HBEmplacementSpawn`:
*
* - Procedural mesh shipped in source so the scenario spawn is not
* blocked on the GLB loader; `applyM48TankGlbVisual` then swaps in
* `m48-patton.glb` asynchronously (hull on the chassis, Joint_Turret /
* Joint_MainGun re-seated on the TankTurret rig), keeping the
* procedural mesh only as a load-failure fallback.
* - Static spawn table per scenario (Open Frontier airfield Main
* Motor Pool bay + A Shau valley road).
* - `resolvePosition` callback so the caller can snap the spawn
* anchor to terrain through the runtime terrain provider.
*
* Hierarchy built by `buildM48ChassisMesh`:
* chassisRoot (positioned in world)
* ├── hull box (the main armored body)
* ├── track L / track R boxes
*
* The turret + barrel meshes are NOT part of the chassis mesh. They are
* mounted onto the `TankTurret` rig nodes (yaw + pitch) by
* `mountM48TurretMeshes` after the `Tank` (and its turret) is constructed,
* so the turret traverses and the barrel elevates with crew aim instead of
* being a static cosmetic stand-in.
*
* The mesh has no physics meaning — `TrackedVehiclePhysics` owns the
* simulation; the mesh is just a visible proxy.
*/
export function buildM48ChassisMesh(): THREE.Group {
const root = new THREE.Group();
root.name = 'm48_chassis_root';
root.userData.perfCategory = 'ground_vehicles';
const { length, width, height } = M48_HULL_DIMENSIONS;
// Hull: rough armored box, olive-drab. Sits on its tracks with the
// chassis origin at the track contact patch (matches the
// TrackedVehiclePhysics axleOffset convention — origin Y on the
// ground, hull body lifted half its height + a small clearance).
const hullGeom = new THREE.BoxGeometry(width, height * 0.55, length);
const hullMat = new THREE.MeshStandardMaterial({ color: 0x3d4631, flatShading: true });
const hull = new THREE.Mesh(hullGeom, hullMat);
hull.position.y = height * 0.275 + 0.45;
hull.name = 'm48_hull';
root.add(hull);
// Tracks: two long boxes flanking the hull, darker.
const trackGeom = new THREE.BoxGeometry(width * 0.16, 0.65, length * 1.02);
const trackMat = new THREE.MeshStandardMaterial({ color: 0x1a1a18, flatShading: true });
const trackL = new THREE.Mesh(trackGeom, trackMat);
trackL.position.set(-width * 0.42, 0.325, 0);
root.add(trackL);
const trackR = new THREE.Mesh(trackGeom, trackMat);
trackR.position.set(+width * 0.42, 0.325, 0);
root.add(trackR);
return root;
}
/**
* Mount the rotating turret + barrel geometry onto a Tank's `TankTurret`
* rig. The turret bulk parents under the yaw node (traverses with yaw) and
* the barrel + mantlet parent under the pitch node (elevates with the
* barrel). The TankTurret's `barrelTipLocalOffset` points -5 m along
* pitchNode-local -Z, so the barrel mesh is centred at -2.5 m to put its
* muzzle at that tip — keeping the rendered barrel aligned with the
* cannon's fire origin + aim direction.
*
* Idempotent enough for spawn use: it adds named meshes once per Tank. The
* meshes follow the rig on `dispose()` because the turret detaches its
* nodes (which carry these children) from the chassis.
*/
export function mountM48TurretMeshes(tank: Tank): void {
const turretRig = tank.getTurret();
const yawNode = turretRig.getYawNode();
const pitchNode = turretRig.getPitchNode();
const turretMat = new THREE.MeshStandardMaterial({ color: 0x3d4631, flatShading: true });
const steelMat = new THREE.MeshStandardMaterial({ color: 0x1a1a18, flatShading: true });
// Turret bulk: a rounded cast-steel body. A flattened sphere reads as the
// M48's distinctive elliptical turret far better than a plain cylinder,
// and a low cylindrical ring under it seats it on the turret ring.
const turretBody = new THREE.Mesh(
new THREE.SphereGeometry(1.55, 14, 10),
turretMat,
);
turretBody.scale.set(1.0, 0.55, 1.25); // squashed + elongated fore-aft
turretBody.position.set(0, 0.35, -0.1);
turretBody.name = 'm48_turret';
turretBody.userData.perfCategory = 'ground_vehicles';
yawNode.add(turretBody);
const turretRing = new THREE.Mesh(
new THREE.CylinderGeometry(1.45, 1.6, 0.4, 16),
turretMat,
);
turretRing.position.set(0, 0.05, 0);
turretRing.name = 'm48_turret_ring';
turretRing.userData.perfCategory = 'ground_vehicles';
yawNode.add(turretRing);
// Commander cupola on the turret roof (cosmetic detail, traverses w/ turret).
const cupola = new THREE.Mesh(
new THREE.CylinderGeometry(0.45, 0.5, 0.45, 10),
turretMat,
);
cupola.position.set(0.5, 0.85, 0.45);
cupola.name = 'm48_cupola';
cupola.userData.perfCategory = 'ground_vehicles';
yawNode.add(cupola);
// Mantlet: the gun shield where the barrel exits the turret face. Parents
// under the pitch node so it tilts with the barrel.
const mantlet = new THREE.Mesh(
new THREE.BoxGeometry(0.9, 0.7, 0.5),
steelMat,
);
mantlet.position.set(0, 0, -0.9);
mantlet.name = 'm48_mantlet';
mantlet.userData.perfCategory = 'ground_vehicles';
pitchNode.add(mantlet);
// Barrel: 90mm M41 main gun. Centred at -2.5 m along pitch-local -Z so
// the muzzle lands at the turret's -5 m barrel-tip offset.
const barrel = new THREE.Mesh(
new THREE.CylinderGeometry(0.11, 0.13, 5.0, 12),
steelMat,
);
barrel.rotation.x = Math.PI * 0.5; // cylinder +Y -> local -Z
barrel.position.set(0, 0, -2.5);
barrel.name = 'm48_barrel';
barrel.userData.perfCategory = 'ground_vehicles';
pitchNode.add(barrel);
// Muzzle brake: short fatter sleeve at the barrel tip.
const muzzleBrake = new THREE.Mesh(
new THREE.CylinderGeometry(0.18, 0.18, 0.5, 12),
steelMat,
);
muzzleBrake.rotation.x = Math.PI * 0.5;
muzzleBrake.position.set(0, 0, -4.85);
muzzleBrake.name = 'm48_muzzle_brake';
muzzleBrake.userData.perfCategory = 'ground_vehicles';
pitchNode.add(muzzleBrake);
}
export interface CreateM48TankOptions {
vehicleId: string;
position: THREE.Vector3;
faction: Faction;
/** Override yaw (radians) for the chassis placement. */
initialYaw?: number;
}
/**
* Build a complete M48 tank (procedural mesh + Tank IVehicle) and
* register it with the VehicleManager.
*/
export function createM48Tank(
scene: THREE.Scene,
vehicleManager: VehicleManager,
options: CreateM48TankOptions,
): { tank: Tank; root: THREE.Group } {
const root = buildM48ChassisMesh();
root.position.copy(options.position);
if (options.initialYaw !== undefined) root.rotation.y = options.initialYaw;
scene.add(root);
const tank = new Tank(options.vehicleId, root, options.faction, undefined, M48_PHYSICS_CONFIG);
// Mount the rotating turret + barrel onto the Tank's turret rig so the
// gunner's aim visibly traverses + elevates the cannon.
mountM48TurretMeshes(tank);
vehicleManager.register(tank);
// Fire-and-forget visual upgrade; the procedural meshes stay on failure.
void applyM48TankGlbVisual(root, tank);
return { tank, root };
}
/**
* Primary M48 scenario spawn table. Coordinates come from
* `M48_SPAWN_OFFSETS`; Y is left at 0 and the spawn caller should
* snap-to-terrain via the runtime terrain provider before handing off
* to `createM48Tank`.
*
* The Open Frontier entry now lands inside the airfield Main Motor
* Pool bay (per cycle-motor-pool-reflow-and-tank-dedup), not the
* West FOB anchor used by the cycle-VEKHIKL-3 initial drop. The
* sibling `motor-pool-heavy-reflow` task removes the dressing M48
* prop from the prefab, so this Tank IVehicle is the only M48
* rendered in the US motor pool.
*/
export interface M48ScenarioSpawnDefinition {
vehicleId: string;
position: THREE.Vector3;
faction: Faction;
initialYaw: number;
}
export const M48_SCENARIO_SPAWNS: Record<'open_frontier' | 'a_shau_valley', M48ScenarioSpawnDefinition> = {
open_frontier: {
vehicleId: 'm48_tank_of_us_fob',
position: new THREE.Vector3(M48_SPAWN_OFFSETS.open_frontier.x, 0, M48_SPAWN_OFFSETS.open_frontier.z),
faction: Faction.US,
initialYaw: M48_SPAWN_OFFSETS.open_frontier.yaw,
},
a_shau_valley: {
vehicleId: 'm48_tank_ashau_valley_road',
position: new THREE.Vector3(M48_SPAWN_OFFSETS.a_shau_valley.x, 0, M48_SPAWN_OFFSETS.a_shau_valley.z),
faction: Faction.US,
initialYaw: M48_SPAWN_OFFSETS.a_shau_valley.yaw,
},
};
export type M48ScenarioMode = keyof typeof M48_SCENARIO_SPAWNS;
/**
* Full per-mode tank fleet. The US fields the M48 in each large scenario; the
* NVA armor is the period-correct T-54 (`T54TankSpawn.T54_SCENARIO_SPAWN_GROUPS`),
* spawned at the OPFOR anchors this table used to field NVA Pattons at — so a
* single tank sits at each anchor with no NVA M48 / T-54 overlap.
*/
export const M48_SCENARIO_SPAWN_GROUPS: Record<M48ScenarioMode, readonly M48ScenarioSpawnDefinition[]> = {
open_frontier: [
M48_SCENARIO_SPAWNS.open_frontier,
],
a_shau_valley: [
M48_SCENARIO_SPAWNS.a_shau_valley,
],
};
export function spawnScenarioM48Tanks(args: {
modes: M48ScenarioMode[];
scene: THREE.Scene;
vehicleManager: VehicleManager;
/**
* Optional resolver to translate the spawn-table's logical position
* into a final world-space point (e.g. snap to terrain height).
* Defaults to returning the table position unchanged.
*/
resolvePosition?: (mode: M48ScenarioMode, base: THREE.Vector3) => THREE.Vector3;
}): Array<{ vehicleId: string; tank: Tank; root: THREE.Group }> {
const spawned: Array<{ vehicleId: string; tank: Tank; root: THREE.Group }> = [];
for (const mode of args.modes) {
for (const def of M48_SCENARIO_SPAWN_GROUPS[mode]) {
const position = args.resolvePosition ? args.resolvePosition(mode, def.position) : def.position;
const parts = createM48Tank(args.scene, args.vehicleManager, {
vehicleId: def.vehicleId,
position,
faction: def.faction,
initialYaw: def.initialYaw,
});
spawned.push({ vehicleId: def.vehicleId, ...parts });
}
}
return spawned;
}