@adatechnology/game-vehicles-three 0.6.4 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -76,7 +76,8 @@ function fuse(THREE, c, pivot, paintColor, primary, hidden) {
76
76
  const tmp = new THREE.Box3();
77
77
  let pruned = 0;
78
78
  c.vehicle.updateMatrixWorld(true);
79
- c.vehicle.traverse((o) => {
79
+ c.vehicle.traverse((node) => {
80
+ const o = node;
80
81
  if (!o.isMesh || o.isInstancedMesh || o.isSkinnedMesh || !o.geometry || !o.geometry.attributes.position) return;
81
82
  for (let p = o; p; p = p.parent) if (p.visible === false) return;
82
83
  if (o.userData && (o.userData.ghost || o.userData.skipLite)) return;
@@ -179,7 +180,8 @@ function liteMeshes(THREE, fusion, materials) {
179
180
  function count(root) {
180
181
  let triangles = 0;
181
182
  let drawCalls = 0;
182
- root.traverse((o) => {
183
+ root.traverse((node) => {
184
+ const o = node;
183
185
  if (!o.isMesh || !o.geometry) return;
184
186
  for (let p = o; p; p = p.parent) if (p.visible === false) return;
185
187
  const g = o.geometry;
@@ -194,7 +196,8 @@ function primaryPaint(c) {
194
196
  let best = null;
195
197
  let bestCount = -1;
196
198
  const counts = /* @__PURE__ */ new Map();
197
- c.vehicle.traverse((o) => {
199
+ c.vehicle.traverse((node) => {
200
+ const o = node;
198
201
  if (!o.isMesh || !o.geometry) return;
199
202
  const mats = Array.isArray(o.material) ? o.material : [o.material];
200
203
  const g = o.geometry;
@@ -239,7 +242,8 @@ function createVehiclesThree(THREE, options) {
239
242
  const bounds = new THREE.Box3();
240
243
  const piece = new THREE.Box3();
241
244
  const turned = new THREE.Matrix4();
242
- c.vehicle.traverse((m) => {
245
+ c.vehicle.traverse((node) => {
246
+ const m = node;
243
247
  if (!m.isMesh || !m.geometry || m.userData.ghost) return;
244
248
  for (let p = m; p; p = p.parent) if (p.visible === false) return;
245
249
  m.geometry.computeBoundingBox();
@@ -268,7 +272,8 @@ function createVehiclesThree(THREE, options) {
268
272
  if (color && primary) {
269
273
  const repainted = primary.clone();
270
274
  repainted.color.set(color);
271
- c.vehicle.traverse((m) => {
275
+ c.vehicle.traverse((node) => {
276
+ const m = node;
272
277
  if (!m.isMesh) return;
273
278
  if (Array.isArray(m.material)) m.material = m.material.map((x) => x === primary ? repainted : x);
274
279
  else if (m.material === primary) m.material = repainted;
@@ -304,7 +309,8 @@ function createVehiclesThree(THREE, options) {
304
309
  context: quality === "high" ? c : null,
305
310
  driverEye,
306
311
  dispose() {
307
- group.traverse((m) => {
312
+ group.traverse((node) => {
313
+ const m = node;
308
314
  if (m.geometry) m.geometry.dispose();
309
315
  });
310
316
  }
@@ -349,7 +355,8 @@ function createVehicleLod(kit, context, root, full, options = {}) {
349
355
  culled.clear();
350
356
  modelKey = keyOf();
351
357
  isUploaded = false;
352
- full.traverse((object) => {
358
+ full.traverse((node) => {
359
+ const object = node;
353
360
  if (!object.isMesh || culled.has(object)) return;
354
361
  culled.set(object, object.frustumCulled);
355
362
  object.frustumCulled = false;
@@ -376,7 +383,8 @@ function createVehicleLod(kit, context, root, full, options = {}) {
376
383
  const shadowProxyOf = (fused) => {
377
384
  const positions = [];
378
385
  const indices = [];
379
- for (const mesh of fused.children) {
386
+ for (const child of fused.children) {
387
+ const mesh = child;
380
388
  if (!mesh.isMesh || !mesh.castShadow) continue;
381
389
  const geometry2 = mesh.geometry.clone().applyMatrix4(mesh.matrix);
382
390
  const offset = positions.length / 3;
@@ -415,7 +423,8 @@ function createVehicleLod(kit, context, root, full, options = {}) {
415
423
  lite.visible = isFused;
416
424
  shadow ?? (shadow = shadowProxyOf(lite));
417
425
  shadow.visible = !isFused;
418
- full.traverse((object) => {
426
+ full.traverse((node) => {
427
+ const object = node;
419
428
  if (!object.isMesh || !object.castShadow) return;
420
429
  castsShadow.add(object);
421
430
  object.castShadow = false;
@@ -437,7 +446,8 @@ function createVehicleLod(kit, context, root, full, options = {}) {
437
446
  root.updateMatrixWorld(true);
438
447
  const inverse = new THREE.Matrix4().copy(root.matrixWorld).invert();
439
448
  const textured = [];
440
- full.traverse((object) => {
449
+ full.traverse((node) => {
450
+ const object = node;
441
451
  if (!object.isMesh || object.userData.skipLite || !isShown(object, full)) return;
442
452
  const materials = Array.isArray(object.material) ? object.material : [object.material];
443
453
  if (materials.some((material) => material?.map && !/plate/i.test(material.name ?? "")) && !/plate/i.test(object.name ?? "")) textured.push(object);
@@ -998,7 +1008,11 @@ var DEFAULT_VEHICLE_SOUND_KEYS = {
998
1008
  crashHeavy: "crash_heavy",
999
1009
  skidAsphalt: "tire_skid_asphalt",
1000
1010
  skidDirt: "tire_skid_dirt",
1001
- roll: "tire_roll"
1011
+ roll: "tire_roll",
1012
+ brakeSqueal: "brake_squeal",
1013
+ hitBody: "vehicle_hit_pedestrian",
1014
+ siren: "siren",
1015
+ rotor: "helicopter_rotor_loop"
1002
1016
  };
1003
1017
  var CRASH_SPEEDS = { medium: 6, heavy: 14 };
1004
1018
  var layers = (id, cutoffs, gains) => [
@@ -1111,8 +1125,10 @@ function createVehicleSound(audio, options = {}) {
1111
1125
  const crankSeconds = options.crankSeconds ?? 0.75;
1112
1126
  const gears = createGearGuess(profile);
1113
1127
  const loops = /* @__PURE__ */ new Map();
1128
+ const budget = options.budget;
1129
+ const self = {};
1114
1130
  const levels = {};
1115
- const state = { running: false, cranking: 0, catches: false, flare: 0, rpm: 0 };
1131
+ const state = { running: false, cranking: 0, catches: false, flare: 0, rpm: 0, sirenPhase: 0 };
1116
1132
  const loopOf = (sound) => {
1117
1133
  let handle = loops.get(sound);
1118
1134
  if (!handle && audio?.loop) {
@@ -1146,6 +1162,11 @@ function createVehicleSound(audio, options = {}) {
1146
1162
  play(kind === "heavy" ? keys.crashHeavy : kind === "medium" ? keys.crashMedium : keys.crashLight, position, Math.min(1, 0.35 + v / 20));
1147
1163
  return kind;
1148
1164
  },
1165
+ hit(speed, position, sound) {
1166
+ const v = Math.abs(speed);
1167
+ if (v < 0.5) return;
1168
+ play(sound ?? keys.hitBody, position, Math.min(1, 0.3 + v / 15));
1169
+ },
1149
1170
  update(frame) {
1150
1171
  if (state.cranking > 0) {
1151
1172
  state.cranking -= frame.dt;
@@ -1159,20 +1180,33 @@ function createVehicleSound(audio, options = {}) {
1159
1180
  const driven = frame.rpm ?? gears.rpm(frame.speed, throttle);
1160
1181
  const rpm = state.running ? Math.max(driven, profile.idleRpm * (1 + 1.2 * state.flare * state.flare)) : 0;
1161
1182
  state.rpm = rpm;
1183
+ const heard = budget ? budget.admit(self, frame.position) ? 1 : 0 : 1;
1162
1184
  for (const layer of engineMix(profile, { rpm, throttle: Math.max(throttle, state.flare * 0.6) })) {
1163
- setLoop(layer.sound, layer.volume, frame.position, { ...layer.frequency === void 0 ? {} : { frequency: layer.frequency }, cutoff: layer.cutoff });
1185
+ setLoop(layer.sound, heard * layer.volume, frame.position, { ...layer.frequency === void 0 ? {} : { frequency: layer.frequency }, cutoff: layer.cutoff });
1164
1186
  }
1165
1187
  const speed = Math.abs(frame.speed);
1166
1188
  const slide = clamp012(((frame.slip ?? 0) - 0.15) / 0.5) * clamp012(speed / 4);
1167
1189
  const dirt = frame.surface === "dirt";
1168
- setLoop(keys.skidAsphalt, dirt ? 0 : 0.16 * slide, frame.position, { cutoff: 1700 + 600 * slide });
1169
- setLoop(keys.skidDirt, dirt ? 0.2 * slide : 0, frame.position, { cutoff: 500 + 500 * slide });
1170
- setLoop(keys.roll, 0.06 * clamp012(speed / 25), frame.position, { cutoff: 250 + 45 * Math.min(speed, 40) });
1190
+ setLoop(keys.skidAsphalt, heard * (dirt ? 0 : 0.16 * slide), frame.position, { cutoff: 1700 + 600 * slide });
1191
+ setLoop(keys.skidDirt, heard * (dirt ? 0.2 * slide : 0), frame.position, { cutoff: 500 + 500 * slide });
1192
+ setLoop(keys.roll, heard * 0.06 * clamp012(speed / 25), frame.position, { cutoff: 250 + 45 * Math.min(speed, 40) });
1193
+ const squeal = clamp012(((frame.brake ?? 0) - 0.3) / 0.4) * clamp012(speed / 0.8) * clamp012((5 - speed) / 2) * (slide > 0 ? 0 : 1);
1194
+ setLoop(keys.brakeSqueal, heard * 0.05 * squeal, frame.position, { cutoff: 3200 + 400 * squeal });
1195
+ const siren = frame.siren ?? "off";
1196
+ if (siren !== "off" || loops.has(keys.siren)) {
1197
+ const period = siren === "yelp" ? 0.25 : 4;
1198
+ state.sirenPhase = (state.sirenPhase + frame.dt / period) % 1;
1199
+ const tri = state.sirenPhase < 0.5 ? state.sirenPhase * 2 : 2 - state.sirenPhase * 2;
1200
+ setLoop(keys.siren, heard * (siren === "off" ? 0 : 0.07), frame.position, { frequency: 650 + 850 * tri, cutoff: 2600 });
1201
+ }
1202
+ const rotor = clamp012(frame.rotor ?? 0);
1203
+ setLoop(keys.rotor, heard * 0.12 * rotor, frame.position, { cutoff: 180 + 220 * rotor });
1171
1204
  },
1172
1205
  get reading() {
1173
1206
  return { running: state.running, rpm: state.rpm, levels: { ...levels } };
1174
1207
  },
1175
1208
  dispose() {
1209
+ budget?.release(self);
1176
1210
  for (const handle of loops.values()) handle.stop();
1177
1211
  loops.clear();
1178
1212
  }
@@ -1218,7 +1252,16 @@ var VEHICLE_LOOPS = [
1218
1252
  ...engineLoops("bike", 600, 1400, 2800, 1200),
1219
1253
  noiseLoop("tire_skid_asphalt", "bandpass", 1900, 5, "rubber squealing on asphalt: a narrow band of noise near 2 kHz"),
1220
1254
  noiseLoop("tire_skid_dirt", "lowpass", 700, 0.8, "tyres sliding on dirt and gravel: a dull, wide hiss"),
1221
- noiseLoop("tire_roll", "lowpass", 400, 0.7, "road noise of the tyres rolling, brighter with the speed")
1255
+ noiseLoop("tire_roll", "lowpass", 400, 0.7, "road noise of the tyres rolling, brighter with the speed"),
1256
+ // a pad on a disc: a thin, nearly pure whistle near 3.4 kHz (a narrow band of noise, so it wavers as a real squeal does)
1257
+ // a siren: a square through a low-pass, its pitch swept by the vehicle every frame (wail: a slow rise and fall; yelp: a fast one)
1258
+ toneLoop("siren", "square", "lowpass", 2600, 0.8, "emergency siren, the sweep driven by the vehicle"),
1259
+ // the blades: a low rumble chopped at the blade-pass rate (≈ 18 Hz for a light helicopter's main rotor)
1260
+ {
1261
+ ...noiseLoop("helicopter_rotor_loop", "lowpass", 320, 0.9, "helicopter rotor: low rumble chopped by the blades"),
1262
+ modulators: [{ target: "gain", wave: "sine", rate: 18, depth: 0.45, base: 0.55 }]
1263
+ },
1264
+ noiseLoop("brake_squeal", "bandpass", 3400, 28, "brake pads squealing on the discs as the car comes to a stop")
1222
1265
  ];
1223
1266
  var recipe = (id, layers2, line) => ({ ...sceneRecipe(id, layers2, line), sources: [SDK, line] });
1224
1267
  var crank = (count2, spacing) => [
@@ -1262,6 +1305,17 @@ var VEHICLE_SOUNDS = [
1262
1305
  ],
1263
1306
  "crash above 50 km/h: metal folding, the glass going, debris falling"
1264
1307
  ),
1308
+ // a body hit by a car: the dull slap of the bumper on a person, the body folding over the bonnet, at speed a crack of the windscreen
1309
+ recipe(
1310
+ "vehicle_hit_pedestrian",
1311
+ [
1312
+ tone({ f: 95, f2: 50, d: 0.14, vol: 0.9 }),
1313
+ noise({ type: "lowpass", f: 600, d: 0.12, vol: 0.8 }),
1314
+ noise({ type: "bandpass", f: 280, q: 1.2, d: 0.18, vol: 0.55, delay: 0.09 }),
1315
+ noise({ type: "highpass", f: 2600, d: 0.05, vol: 0.18, delay: 0.1 })
1316
+ ],
1317
+ "a person hit by a car: the bumper slap, the body on the bonnet"
1318
+ ),
1265
1319
  // the game's horn (two squares at 415 and 523 Hz, jogo L70272) heard from where the car is; a truck's air horn lower and longer
1266
1320
  recipe("horn_car", [tone({ wave: "square", f: 415, d: 0.6, vol: 0.09 }), tone({ wave: "square", f: 523, d: 0.6, vol: 0.09 })], "car horn: the game chord, placed and panned"),
1267
1321
  recipe("horn_truck", [tone({ wave: "sawtooth", f: 220, d: 1.1, vol: 0.1 }), tone({ wave: "sawtooth", f: 277, d: 1.1, vol: 0.08 }), noise({ type: "bandpass", f: 900, q: 2, d: 1.1, vol: 0.05 })], "truck air horn: low reedy chord"),
@@ -1271,7 +1325,37 @@ var VEHICLE_SOUNDS = [
1271
1325
  recipe("car_trunk_close", [tone({ f: 90, f2: 55, d: 0.15, vol: 0.65 }), noise({ type: "bandpass", f: 500, q: 1, d: 0.12, vol: 0.5 }), ...latch(0.01, 0.28), noise({ type: "bandpass", f: 1800, q: 3, d: 0.04, vol: 0.1, delay: 0.06 })], "trunk or hatch shut: a lighter thump, the latch and a small rattle"),
1272
1326
  recipe("car_hood_close", [tone({ f: 70, f2: 40, d: 0.22, vol: 0.8 }), noise({ type: "bandpass", f: 850, q: 1.4, d: 0.2, vol: 0.6, wet: 0.3 }), tone({ f: 900, f2: 860, d: 0.18, vol: 0.05 }), ...latch(0.015, 0.3)], "hood slammed: a big sheet of metal ringing a little, the latch")
1273
1327
  ];
1274
- var VEHICLE_SOUND_BANK = { id: "vehicles_three", sounds: VEHICLE_SOUNDS, loops: VEHICLE_LOOPS };
1328
+ var CAR_RECORDED_RPM = { idle: 1e3, high: 2520 };
1329
+ var CAR_START_RUN_STOP = {
1330
+ /** The take the times below were measured on. */
1331
+ take: "_01.",
1332
+ /** The starter and the engine catching (at 0.58 s) and flaring; the loops take over at the crank time of the vehicle. */
1333
+ start: { offset: 0, duration: 1.3 },
1334
+ /** The ignition off: the engine running down to the end of the file. */
1335
+ stop: { offset: 9.96, duration: 0.64 }
1336
+ };
1337
+ var carLayer = (id, file, rpm, harmonic, offset) => ({
1338
+ id,
1339
+ file,
1340
+ loop: true,
1341
+ // the layer's oscillator plays the firing frequency times its harmonic: the file sounds at that frequency for the RPM it was recorded at
1342
+ baseFrequency: firingHz(CAR_ENGINE, rpm) * harmonic,
1343
+ falloff: VEHICLE_LOOP_FALLOFF,
1344
+ ...offset === void 0 ? {} : { offset }
1345
+ });
1346
+ var VEHICLE_SAMPLE_ALIASES = [
1347
+ carLayer("engine_car_low", "engine_loop_car_idle", CAR_RECORDED_RPM.idle, 1),
1348
+ carLayer("engine_car_mid", "engine_loop_car_high", CAR_RECORDED_RPM.high, 1),
1349
+ // the same file as the mid layer, read from two seconds in, so the two do not sum in phase where they cross
1350
+ carLayer("engine_car_high", "engine_loop_car_high", CAR_RECORDED_RPM.high, 2, 2),
1351
+ // the cut points are of take _01 (10.6 s); _02 (21.9 s) starts already running and idles at 9.96 s, so it is left out of both
1352
+ { id: "engine_start", file: "engine_start_run_stop", only: CAR_START_RUN_STOP.take, ...CAR_START_RUN_STOP.start, falloff: VEHICLE_LOOP_FALLOFF },
1353
+ { id: "engine_stop", file: "engine_start_run_stop", only: CAR_START_RUN_STOP.take, ...CAR_START_RUN_STOP.stop, falloff: VEHICLE_LOOP_FALLOFF },
1354
+ { id: "car_door_close", file: "car_door_shut" },
1355
+ { id: "car_trunk_close", file: "car_trunk_shut" }
1356
+ ];
1357
+ var VEHICLE_PRELOAD_SOUNDS = [...VEHICLE_SAMPLE_ALIASES.map((alias) => alias.id), "car_door_open"];
1358
+ var VEHICLE_SOUND_BANK = { id: "vehicles_three", sounds: VEHICLE_SOUNDS, loops: VEHICLE_LOOPS, sampleAliases: VEHICLE_SAMPLE_ALIASES };
1275
1359
 
1276
1360
  // src/test-area/module.ts
1277
1361
  var VEHICLES_AREA_ID = "vehicles_area";
@@ -1280,6 +1364,7 @@ var ITEM_OF = { compact_hatch: "hatch", midsize_sedan: "sedan", midsize_pickup:
1280
1364
  var itemId = (key) => ITEM_OF[key] ?? key;
1281
1365
  var LANE = { z: 11, from: -12, wall: 13 };
1282
1366
  var SPARK_EVERY_S = 0.08;
1367
+ var TYRE_DAMAGE = /* @__PURE__ */ new Set(["flat_tyre", "torn_tyre", "run_flat"]);
1283
1368
  var LEAF_REST_RADIANS = 0.02;
1284
1369
  var MATERIAL_COLOURS = { metal: 13421772, glass: 4500223, rubber: 16750848, fuel: 16724736 };
1285
1370
  var LANE_START_S = 1.2;
@@ -1575,7 +1660,8 @@ function createVehiclesTestModule(options = {}) {
1575
1660
  state.time += dt;
1576
1661
  const braking = settings.damage !== "into_wall" && gap() < brakeAt() && state.time > 0.5;
1577
1662
  const moving = state.time > LANE_START_S;
1578
- const input = { ...idle, throttle: braking || !moving ? 0 : 1, brake: braking ? 1 : 0, steer: settings.damage === "into_wall" || !moving ? 0 : Math.sin(state.time * 1.4) * 0.15 };
1663
+ const straight = settings.damage === "into_wall" || TYRE_DAMAGE.has(settings.damage) || !moving;
1664
+ const input = { ...idle, throttle: braking || !moving ? 0 : 1, brake: braking ? 1 : 0, steer: straight ? 0 : Math.sin(state.time * 1.4) * 0.15 };
1579
1665
  if (mode === "arcade") {
1580
1666
  const { events } = arcade.step(input, dt, { blocked: (x, z, yaw) => vehicleCorners2(arcade.spec, x, z, yaw).some(([px]) => px >= LANE.wall) });
1581
1667
  for (const e of events) {
@@ -1613,7 +1699,8 @@ function createVehiclesTestModule(options = {}) {
1613
1699
  speed,
1614
1700
  throttle: input.throttle,
1615
1701
  ...semiState ? { rpm: semiState.rpm, slip: Math.max(0, ...semiState.slip.map(Math.abs)) } : { slip: braking && speed > 3 ? 0.6 : 0 },
1616
- surface: /dirt|gravel|sand|grass|mud/.test(settings.surface) ? "dirt" : "asphalt"
1702
+ surface: /dirt|gravel|sand|grass|mud/.test(settings.surface) ? "dirt" : "asphalt",
1703
+ brake: input.brake
1617
1704
  });
1618
1705
  }
1619
1706
  state.exhaust -= dt;
@@ -1789,6 +1876,7 @@ function createVehiclesTestModule(options = {}) {
1789
1876
  if (isRenderPools(lent.pools)) lent.pools.materials.register("vehicles.cracked-glass", () => crackedGlassMaterial(three, new three.DataTexture(new Uint8Array(4), 1, 1)));
1790
1877
  lent.audio?.register?.(LIBRARY_BANK);
1791
1878
  lent.audio?.register?.(VEHICLE_SOUND_BANK);
1879
+ lent.audio?.preload?.(VEHICLE_PRELOAD_SOUNDS);
1792
1880
  return () => {
1793
1881
  stopChange?.();
1794
1882
  stopFrame();
@@ -1814,16 +1902,139 @@ function loadVehiclesTestModule(room, options = {}) {
1814
1902
  }
1815
1903
 
1816
1904
  // src/test-area/proving/module.ts
1817
- import { createVehicleCamera, mirrorPoses } from "@adatechnology/game-camera";
1905
+ import { createVehicleCamera, defaultEye, mirrorPoses } from "@adatechnology/game-camera";
1818
1906
  import { markKitOrigin as markKitOrigin3 } from "@adatechnology/game-contracts";
1819
- import { CABIN_BANK, LIBRARY_BANK as LIBRARY_BANK2 } from "@adatechnology/game-audio";
1907
+ import { CABIN_BANK, LIBRARY_BANK as LIBRARY_BANK2, RADIO_BANK } from "@adatechnology/game-audio";
1820
1908
  import { createSemiSimDriver as createSemiSimDriver4 } from "@adatechnology/game-vehicles-driving";
1821
1909
  import { certify, createProvingTests, startRun, statusOf, subjectSpec as subjectSpec2, withResult } from "@adatechnology/game-vehicles-proving";
1822
1910
  import { discardObject3D as discardObject3D3 } from "@adatechnology/game-shapes-three";
1823
1911
 
1824
1912
  // src/test-area/proving/cockpit.ts
1825
- import { createSemiSimDriver as createSemiSimDriver3, createVehicleControls } from "@adatechnology/game-vehicles-driving";
1913
+ import { RADIO_TRACKS } from "@adatechnology/game-audio";
1914
+ import { bumperDistance, createSemiSimDriver as createSemiSimDriver3, createVehicleControls, createVehicleStorage, createVehicleSystems, createWindshieldFog, GLOVEBOX, parkingBeepInterval, trunkFor } from "@adatechnology/game-vehicles-driving";
1826
1915
  import { subjectSpec } from "@adatechnology/game-vehicles-proving";
1916
+ import { createVehicleDestructible as createVehicleDestructible3 } from "@adatechnology/game-vehicles";
1917
+
1918
+ // src/body.ts
1919
+ var OPEN_SOUND = "car_door_open";
1920
+ var SHUT_SOUND = { door: "car_door_close", hood: "car_hood_close", lid: "car_trunk_close" };
1921
+ var clamp = (x, a, b) => Math.max(a, Math.min(b, x));
1922
+ function createVehicleBody(built, options) {
1923
+ const swing = options.swingSeconds ?? 1.2;
1924
+ const windowSeconds = options.windowSeconds ?? 3.5;
1925
+ const travel = options.windowTravel ?? 0.92;
1926
+ const root = built.context?.vehicle ?? built.group;
1927
+ const poses = /* @__PURE__ */ new Map();
1928
+ for (const clip of options.clips) {
1929
+ if (!clip.id.startsWith("open_")) continue;
1930
+ const last = clip.keys[clip.keys.length - 1];
1931
+ for (const [id, pose] of Object.entries(last?.parts ?? {})) if (pose.rotation) poses.set(id, pose.rotation);
1932
+ }
1933
+ const hinges = /* @__PURE__ */ new Map();
1934
+ for (const [id, open] of poses) {
1935
+ const pivot = root.getObjectByName(id);
1936
+ if (pivot) hinges.set(id, { pivot, open, now: 0, target: 0 });
1937
+ }
1938
+ const kind = (part) => part.startsWith("door") ? "door" : part === "hood" ? "hood" : "lid";
1939
+ const panes = /* @__PURE__ */ new Map();
1940
+ for (const glass of built.context?.glass ?? []) {
1941
+ const g = glass.userData.glass;
1942
+ if (!g || g.key !== "window_front" && g.key !== "window_rear") continue;
1943
+ const id = `${g.key === "window_front" ? "front" : "rear"}_${(g.side ?? 1) < 0 ? "left" : "right"}`;
1944
+ if (panes.has(id) || !glass.geometry) continue;
1945
+ glass.geometry.computeBoundingBox();
1946
+ const box = glass.geometry.boundingBox;
1947
+ if (box) panes.set(id, { pane: glass, drop: (box.max.y - box.min.y) * travel, now: 0, target: 0, applied: 0 });
1948
+ }
1949
+ const sunroofs = [];
1950
+ const wipers = [];
1951
+ const T = options.THREE;
1952
+ const turn = new T.Quaternion();
1953
+ root.traverse((o) => {
1954
+ const sun = o.userData.sunroof;
1955
+ if (sun) sunroofs.push({ obj: o, slide: sun.slide, base: o.position.x });
1956
+ });
1957
+ for (const { part, tag } of built.context?.wipers ?? [])
1958
+ wipers.push({ obj: part, arm: tag.arm, sweep: tag.sweep, spindle: new T.Vector3(...tag.spindle), axis: new T.Vector3(...tag.axis), rest: part.position.clone(), restQ: part.quaternion.clone() });
1959
+ let sunNow = 0;
1960
+ let sunTarget = 0;
1961
+ let wipePhase = 0;
1962
+ let locked = false;
1963
+ const placeWiper = (w, angle) => {
1964
+ if (angle === 0) {
1965
+ w.obj.position.copy(w.rest);
1966
+ w.obj.quaternion.copy(w.restQ);
1967
+ return;
1968
+ }
1969
+ turn.setFromAxisAngle(w.axis, angle);
1970
+ w.obj.position.copy(w.rest).sub(w.spindle).applyQuaternion(turn).add(w.spindle);
1971
+ w.obj.quaternion.copy(turn).multiply(w.restQ);
1972
+ };
1973
+ const ease = (now, target, seconds, dt) => now + clamp(target - now, -dt / seconds, dt / seconds);
1974
+ return {
1975
+ parts: [...hinges.keys()],
1976
+ windows: [...panes.keys()],
1977
+ hasSunroof: sunroofs.length > 0,
1978
+ wipers: new Set(wipers.map((w) => w.arm)).size,
1979
+ get locked() {
1980
+ return locked;
1981
+ },
1982
+ openness: (part) => hinges.get(part)?.now ?? 0,
1983
+ toggle(part) {
1984
+ const h = hinges.get(part);
1985
+ if (!h) return [{ type: "refused", part, reason: "missing" }];
1986
+ if (locked && kind(part) !== "hood" && h.target === 0) return [{ type: "refused", part, reason: "locked" }];
1987
+ h.target = h.target > 0 ? 0 : 1;
1988
+ return h.target > 0 ? [{ type: "sound", key: OPEN_SOUND, part }] : [];
1989
+ },
1990
+ window: (id) => panes.get(id)?.now ?? 0,
1991
+ toggleWindow(id) {
1992
+ const w = panes.get(id);
1993
+ if (w) w.target = w.target > 0.5 ? 0 : 1;
1994
+ },
1995
+ setWindow(id, open) {
1996
+ const w = panes.get(id);
1997
+ if (w) w.target = clamp(open, 0, 1);
1998
+ },
1999
+ toggleSunroof() {
2000
+ sunTarget = sunTarget > 0.5 ? 0 : 1;
2001
+ },
2002
+ lock(on) {
2003
+ locked = on;
2004
+ return [{ type: "sound", key: "blinker_tick", part: "doors" }];
2005
+ },
2006
+ update(dt, wipe = 0) {
2007
+ const events = [];
2008
+ for (const [id, h] of hinges) {
2009
+ if (h.now === h.target) continue;
2010
+ h.now = ease(h.now, h.target, swing, dt);
2011
+ h.pivot.rotation.set(h.open[0] * h.now, h.open[1] * h.now, h.open[2] * h.now);
2012
+ if (h.now === 0) events.push({ type: "sound", key: SHUT_SOUND[kind(id)] ?? "car_door_close", part: id });
2013
+ }
2014
+ for (const w of panes.values()) {
2015
+ if (w.now === w.target) continue;
2016
+ w.now = ease(w.now, w.target, windowSeconds, dt);
2017
+ const offset = -w.drop * w.now;
2018
+ w.pane.position.y += offset - w.applied;
2019
+ w.applied = offset;
2020
+ }
2021
+ if (sunNow !== sunTarget) {
2022
+ sunNow = ease(sunNow, sunTarget, 2.5, dt);
2023
+ for (const s of sunroofs) s.obj.position.x = s.base - s.slide * sunNow;
2024
+ }
2025
+ wipePhase = wipe > 0 ? (wipePhase + dt * wipe) % 1 : wipePhase > 0 ? Math.min(1, wipePhase + dt * 1.5) % 1 : 0;
2026
+ const sweep = Math.sin(Math.PI * wipePhase);
2027
+ for (const w of wipers) placeWiper(w, w.sweep * sweep);
2028
+ return events;
2029
+ },
2030
+ dispose() {
2031
+ for (const h of hinges.values()) h.pivot.rotation.set(0, 0, 0);
2032
+ for (const w of panes.values()) w.pane.position.y -= w.applied;
2033
+ for (const s of sunroofs) s.obj.position.x = s.base;
2034
+ for (const w of wipers) placeWiper(w, 0);
2035
+ }
2036
+ };
2037
+ }
1827
2038
 
1828
2039
  // src/dashboard.ts
1829
2040
  import { discardObject3D as discardObject3D2 } from "@adatechnology/game-shapes-three";
@@ -2000,6 +2211,8 @@ function createVehicleDashboard(THREE, built, options) {
2000
2211
  lamp(g, 104, 214, r.highBeam, DASHBOARD_LOOK.blue, beam);
2001
2212
  lamp(g, 152, 214, r.lowBeam || r.fog, r.fog ? DASHBOARD_LOOK.amber : DASHBOARD_LOOK.green, beam);
2002
2213
  bar(g, 40, r.fuel, false, "F");
2214
+ lamp(g, 104, 244, !!r.cruise, DASHBOARD_LOOK.green, letter("CRZ"));
2215
+ if (r.profile && r.profile !== "normal") lamp(g, 152, 244, true, r.profile === "eco" ? DASHBOARD_LOOK.green : DASHBOARD_LOOK.amber, letter(r.profile === "eco" ? "E" : "S"));
2003
2216
  } else {
2004
2217
  dial(g, r.rpm / 1e3, Math.ceil(redline / 1e3) + 1, 1, redline / 1e3, (v) => String(Math.round(v)));
2005
2218
  g.fillStyle = DASHBOARD_LOOK.marks;
@@ -2008,7 +2221,9 @@ function createVehicleDashboard(THREE, built, options) {
2008
2221
  g.fillText(r.gear < 0 ? "R" : r.gear === 0 ? "N" : String(r.gear), SIZE / 2, SIZE / 2 + 54);
2009
2222
  lamp(g, 86, 178, r.parkingBrake, DASHBOARD_LOOK.red, ring("P"));
2010
2223
  lamp(g, 128, 214, r.ignition && !r.seatbelt, DASHBOARD_LOOK.red, belt);
2011
- lamp(g, 170, 178, warn, DASHBOARD_LOOK.red, letter("+\u2212"));
2224
+ lamp(g, 170, 178, warn || r.engineOn && r.charging === false, DASHBOARD_LOOK.red, letter("+\u2212"));
2225
+ lamp(g, 104, 244, r.ignition && !!r.tractionOff, DASHBOARD_LOOK.amber, letter("TC"));
2226
+ lamp(g, 152, 244, r.ignition && !!r.absOff, DASHBOARD_LOOK.amber, letter("ABS"));
2012
2227
  lamp(g, 86, 214, warn, DASHBOARD_LOOK.red, letter("OIL"));
2013
2228
  lamp(g, 170, 214, r.fourWheel, DASHBOARD_LOOK.green, letter("4x4"));
2014
2229
  bar(g, SIZE - 40, r.temperature, true, "T");
@@ -2045,7 +2260,7 @@ function createVehicleDashboard(THREE, built, options) {
2045
2260
  // src/lamps.ts
2046
2261
  import { isLightBudget, LIGHT_PRIORITY } from "@adatechnology/game-render-three";
2047
2262
  var LAMP_RULES = {
2048
- glow: { head: { off: 0.04, position: 0.3, low: 1, high: 1.35 }, tail: { off: 0.15, on: 0.7, brake: 1.6 }, fog: { off: 0.04, on: 1.1 }, amber: { off: 0.1, on: 2.2 } },
2263
+ glow: { head: { off: 0.04, position: 0.3, low: 1, high: 1.35 }, tail: { off: 0.15, on: 0.7, brake: 1.6 }, fog: { off: 0.04, on: 1.1 }, amber: { off: 0.1, on: 2.2 }, beacon: { off: 0.35, dark: 0.12, lit: 2.8 } },
2049
2264
  low: { color: 16773584, intensity: 2.4, distance: 32, angle: 0.42, penumbra: 0.55, decay: 1.6, drop: 0.09 },
2050
2265
  high: { color: 16774882, intensity: 3.6, distance: 70, angle: 0.3, penumbra: 0.45, decay: 1.4, drop: 0.015 },
2051
2266
  fog: { color: 16773320, intensity: 1.6, distance: 16, angle: 0.75, penumbra: 0.7, decay: 1.8, drop: 0.12 },
@@ -2062,7 +2277,11 @@ function createVehicleLamps(THREE, built, options = {}) {
2062
2277
  if (o.userData?.lamp === "fog" && o.material && !bulbs.includes(o)) bulbs.push(o);
2063
2278
  });
2064
2279
  if (bulbs.length) lamps.push({ kind: "fog", inner: bulbs });
2065
- const has = { head: lamps.some((l) => l.kind === "headlight"), tail: lamps.some((l) => l.kind === "taillight"), fog: lamps.some((l) => l.kind === "fog"), indicators: false };
2280
+ const beacons = [];
2281
+ built.group.traverse((o) => {
2282
+ if (o.userData?.lamp === "beacon" && o.material) beacons.push({ mesh: o, side: o.userData.beaconSide === 1 ? 1 : 0 });
2283
+ });
2284
+ const has = { head: lamps.some((l) => l.kind === "headlight"), tail: lamps.some((l) => l.kind === "taillight"), fog: lamps.some((l) => l.kind === "fog"), indicators: false, beacon: beacons.length > 0 };
2066
2285
  const originals = /* @__PURE__ */ new Map();
2067
2286
  const copies = /* @__PURE__ */ new Map();
2068
2287
  const base = /* @__PURE__ */ new Map();
@@ -2077,6 +2296,7 @@ function createVehicleLamps(THREE, built, options = {}) {
2077
2296
  mesh.material = copy;
2078
2297
  return copy;
2079
2298
  };
2299
+ for (const b of beacons) own(b.mesh);
2080
2300
  const haloBase = /* @__PURE__ */ new Map();
2081
2301
  for (const l of lamps) {
2082
2302
  for (const m of l.inner) own(m);
@@ -2152,6 +2372,8 @@ function createVehicleLamps(THREE, built, options = {}) {
2152
2372
  else if (l.kind === "fog") glow(l, state.fog ? g.fog.on : g.fog.off);
2153
2373
  }
2154
2374
  for (const m of amber) glowMesh(m, state[sideOf.get(m) ?? "left"] ? g.amber.on : g.amber.off);
2375
+ const lit = state.beacon ?? null;
2376
+ for (const b of beacons) glowMesh(b.mesh, lit === null ? g.beacon.off : lit === b.side ? g.beacon.lit : g.beacon.dark);
2155
2377
  const beamId = state.mode === "low" || state.mode === "high" ? state.mode : null;
2156
2378
  for (const id of ["low", "high"]) beam(id, beamId === id, () => spots(front, LAMP_RULES[id]));
2157
2379
  beam("fog", state.fog && has.fog, () => spots(fogAnchors, LAMP_RULES.fog));
@@ -2165,7 +2387,7 @@ function createVehicleLamps(THREE, built, options = {}) {
2165
2387
  return { ...state };
2166
2388
  },
2167
2389
  set(next) {
2168
- if (next.mode === state.mode && next.fog === state.fog && next.interior === state.interior && next.brake === state.brake && next.reverse === state.reverse && next.left === state.left && next.right === state.right) return;
2390
+ if (next.mode === state.mode && next.fog === state.fog && next.interior === state.interior && next.brake === state.brake && next.reverse === state.reverse && next.left === state.left && next.right === state.right && (next.beacon ?? null) === (state.beacon ?? null)) return;
2169
2391
  Object.assign(state, next);
2170
2392
  apply();
2171
2393
  },
@@ -2174,15 +2396,17 @@ function createVehicleLamps(THREE, built, options = {}) {
2174
2396
  sources.clear();
2175
2397
  for (const a of anchors) a.removeFromParent();
2176
2398
  anchors.length = 0;
2399
+ const giveBack = (m) => {
2400
+ const copy = m.material;
2401
+ const original = originals.get(m);
2402
+ if (!original) return;
2403
+ originals.delete(m);
2404
+ m.material = original;
2405
+ if (copy && !Array.isArray(copy)) copy.dispose?.();
2406
+ };
2407
+ for (const b of beacons) giveBack(b.mesh);
2177
2408
  for (const l of lamps) {
2178
- for (const m of l.inner) {
2179
- const copy = m.material;
2180
- const original = originals.get(m);
2181
- if (original) {
2182
- m.material = original;
2183
- if (copy && !Array.isArray(copy)) copy.dispose?.();
2184
- }
2185
- }
2409
+ for (const m of l.inner) giveBack(m);
2186
2410
  for (const h of l.halo ?? []) {
2187
2411
  h.visible = true;
2188
2412
  const s = haloBase.get(h) ?? 1;
@@ -2196,14 +2420,85 @@ function createVehicleLamps(THREE, built, options = {}) {
2196
2420
  };
2197
2421
  }
2198
2422
 
2423
+ // src/windshield-mist.ts
2424
+ var MIST_RULES = { color: 15001836, opacity: 0.72, others: 0.8 };
2425
+ var DOUBLE_SIDE = 2;
2426
+ function createWindshieldMist(THREE, built) {
2427
+ const glass = (built.context?.glass ?? []).filter((p) => p?.geometry);
2428
+ const films = [];
2429
+ for (const pane of glass) {
2430
+ const side = !Array.isArray(pane.material) && pane.material?.side || DOUBLE_SIDE;
2431
+ const material = new THREE.MeshBasicMaterial({ color: MIST_RULES.color, transparent: true, opacity: 0, depthWrite: false, side, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 });
2432
+ const mesh = new THREE.Mesh(pane.geometry, material);
2433
+ mesh.name = "windshield_mist";
2434
+ mesh.raycast = () => {
2435
+ };
2436
+ mesh.userData.mist = true;
2437
+ mesh.renderOrder = pane.renderOrder + 1;
2438
+ mesh.visible = false;
2439
+ pane.add(mesh);
2440
+ films.push({ mesh, material, share: pane.userData.glass?.key === "windshield" ? 1 : MIST_RULES.others });
2441
+ }
2442
+ let fog = 0;
2443
+ return {
2444
+ panes: films.length,
2445
+ get fog() {
2446
+ return fog;
2447
+ },
2448
+ set(next) {
2449
+ const share = Math.max(0, Math.min(1, next));
2450
+ if (Math.abs(share - fog) < 5e-3 && share > 0 === fog > 0) return;
2451
+ fog = share;
2452
+ for (const f of films) {
2453
+ f.material.opacity = MIST_RULES.opacity * fog * f.share;
2454
+ f.mesh.visible = fog > 0.01;
2455
+ }
2456
+ },
2457
+ dispose() {
2458
+ for (const f of films) {
2459
+ f.mesh.removeFromParent();
2460
+ f.material.dispose();
2461
+ }
2462
+ films.length = 0;
2463
+ }
2464
+ };
2465
+ }
2466
+
2199
2467
  // src/test-area/proving/cockpit.ts
2200
- var DEFAULT_COCKPIT_SOUNDS = { horn: "horn", blinker: "blinker_tick", starter: "starter_crank", alarm: "car_alarm", chirp: "alarm_chirp", wiper: "wiper_swish", chime: "seatbelt_chime", radio: "radio_static" };
2468
+ var DEFAULT_COCKPIT_SOUNDS = {
2469
+ horn: "horn",
2470
+ blinker: "blinker_tick",
2471
+ starter: "starter_crank",
2472
+ alarm: "car_alarm",
2473
+ chirp: "alarm_chirp",
2474
+ wiper: "wiper_swish",
2475
+ chime: "seatbelt_chime",
2476
+ radio: "radio_static",
2477
+ beep: "parking_beep",
2478
+ wail: "siren_wail",
2479
+ yelp: "siren_yelp",
2480
+ spark: "hotwire_spark",
2481
+ glovebox: "glovebox_latch"
2482
+ };
2483
+ var BODY_KEYS = {
2484
+ Digit1: "door_front_left",
2485
+ Digit2: "door_front_right",
2486
+ Digit3: "door_rear_left",
2487
+ Digit4: "door_rear_right",
2488
+ Digit5: "hood",
2489
+ Digit6: "lid"
2490
+ };
2491
+ var WINDOW_KEYS = { Digit1: "front_left", Digit2: "front_right", Digit3: "rear_left", Digit4: "rear_right" };
2492
+ var LOCK_KEY = "KeyV";
2493
+ var WIPE_RATE = { off: 0, intermittent: 0.25, slow: 0.7, fast: 1.4 };
2201
2494
  function createCockpit(o) {
2202
2495
  const sounds = { ...DEFAULT_COCKPIT_SOUNDS, ...o.sounds };
2203
2496
  const spec = subjectSpec(o.subject);
2204
2497
  const driver = createSemiSimDriver3(spec);
2205
2498
  driver.place(o.place[0], o.place[1], o.place[2]);
2206
2499
  const lamps = createVehicleLamps(o.THREE, o.built, { lights: o.host.lights, owner: "proving-ground" });
2500
+ const mist = createWindshieldMist(o.THREE, o.built);
2501
+ const fog = createWindshieldFog();
2207
2502
  const audio = o.host.audio;
2208
2503
  const top = o.subject.stats.maxSpeedKmh;
2209
2504
  const dashboard = o.gauge ? createVehicleDashboard(o.THREE, o.built, { gauge: o.gauge, needle: o.needle, redlineRpm: spec.engine.redlineRpm, maxKmh: top ? Math.ceil((top + 10) / 20) * 20 : void 0 }) : null;
@@ -2214,27 +2509,77 @@ function createCockpit(o) {
2214
2509
  let wipeAgain = 0;
2215
2510
  let chimeAgain = 0;
2216
2511
  let radioAgain = 0;
2512
+ let beepAgain = 0;
2513
+ let bar = 0;
2217
2514
  let exitAsked = false;
2515
+ let sirenAgain = 0;
2516
+ let sparkAgain = 0;
2517
+ let wiresBefore = 0;
2218
2518
  let gas = 0;
2519
+ const systems = createVehicleSystems();
2520
+ const context = o.built.context;
2521
+ const destructible = o.kit && context ? createVehicleDestructible3(o.kit, context, { id: "driven", frame: o.built.group }) : null;
2522
+ const body = destructible ? createVehicleBody(o.built, { THREE: o.THREE, clips: destructible.animations() }) : null;
2523
+ const compartments = [];
2524
+ if (!body || body.parts.some((p) => p.startsWith("door"))) compartments.push(GLOVEBOX);
2525
+ if (body?.parts.includes("lid")) compartments.push(trunkFor(o.trunkLitres));
2526
+ const storage = createVehicleStorage({ compartments });
2527
+ for (const c of compartments) storage.stock(c.id, o.stock?.(c.id) ?? []);
2528
+ const access = () => ({
2529
+ seated: true,
2530
+ frontDoorOpen: (body?.openness("door_front_left") ?? 0) >= 0.5 || (body?.openness("door_front_right") ?? 0) >= 0.5,
2531
+ gloveboxOpen: controls.state.glovebox,
2532
+ lid: body?.openness("lid") ?? 0
2533
+ });
2534
+ const bodyEvents = (events) => {
2535
+ for (const event of events) {
2536
+ if (event.type === "sound") play(event.key, 0.9);
2537
+ o.onEvent?.(event);
2538
+ }
2539
+ };
2219
2540
  const controls = createVehicleControls({
2541
+ canCrank: systems.canCrank,
2542
+ onCrank: systems.crank,
2543
+ powerLimit: systems.powerLimit,
2220
2544
  gears: spec.gears?.length ?? 5,
2221
2545
  fourWheelDrive: !!o.subject.body.fourWheelDrive,
2222
2546
  fogLamps: lamps.has.fog,
2547
+ clutchPedal: true,
2548
+ radioTracks: RADIO_TRACKS.length,
2549
+ hasKey: o.hasKey,
2550
+ emergency: lamps.has.beacon,
2223
2551
  onEvent: (event) => {
2224
2552
  if (event.type === "command") {
2225
- if (event.command === "ignition" && event.state.ignition && sounds.starter) play(sounds.starter);
2226
2553
  if (event.command === "ignition" && !event.state.ignition) driver.state.engineOn = false;
2227
2554
  if (event.command === "alarm" && event.state.alarm === "armed") play(sounds.chirp);
2228
2555
  if (event.command === "alarm") alarmAgain = 0;
2229
- if (event.command === "radio" || event.command === "radio_source") radioAgain = 0;
2556
+ if (event.command === "radio" || event.command === "radio_source" || event.command === "radio_track") {
2557
+ radioAgain = 0;
2558
+ bar = 0;
2559
+ }
2230
2560
  if (event.command === "exit") exitAsked = true;
2561
+ if (event.command === "siren") sirenAgain = 0;
2562
+ if (event.command === "glovebox") play(sounds.glovebox, 0.8);
2231
2563
  }
2232
2564
  o.onEvent?.(event);
2233
2565
  }
2234
2566
  });
2235
2567
  driver.state.engineOn = false;
2236
2568
  const target = typeof window !== "undefined" ? window : null;
2569
+ const bodyKey = (e) => {
2570
+ if (!body) return false;
2571
+ if (e.code === LOCK_KEY) bodyEvents(body.lock(!body.locked));
2572
+ else if (e.shiftKey && e.code === "Digit5") body.toggleSunroof();
2573
+ else if (e.shiftKey && WINDOW_KEYS[e.code]) body.toggleWindow(WINDOW_KEYS[e.code]);
2574
+ else if (!e.shiftKey && BODY_KEYS[e.code]) bodyEvents(body.toggle(BODY_KEYS[e.code]));
2575
+ else return false;
2576
+ return true;
2577
+ };
2237
2578
  const down = (e) => {
2579
+ if (!e.repeat && bodyKey(e)) {
2580
+ e.stopImmediatePropagation();
2581
+ return;
2582
+ }
2238
2583
  if (!controls.key(e.code, true)) return;
2239
2584
  e.stopImmediatePropagation();
2240
2585
  if (e.code.startsWith("Arrow") || e.code === "Space") e.preventDefault();
@@ -2246,9 +2591,19 @@ function createCockpit(o) {
2246
2591
  target?.addEventListener("keydown", down, true);
2247
2592
  target?.addEventListener("keyup", up, true);
2248
2593
  target?.addEventListener("blur", blur);
2249
- return {
2594
+ const cockpit = {
2595
+ radiator: 1,
2596
+ storage,
2597
+ access,
2598
+ fog,
2599
+ mist,
2600
+ weather: { ...o.weather ?? { outsideC: 25, rain: 0 } },
2601
+ emergency: lamps.has.beacon,
2250
2602
  driver,
2251
2603
  controls,
2604
+ systems,
2605
+ body,
2606
+ destructible,
2252
2607
  lamps,
2253
2608
  dashboard,
2254
2609
  get throttle() {
@@ -2257,18 +2612,39 @@ function createCockpit(o) {
2257
2612
  step(dt) {
2258
2613
  const input = controls.input(dt, driver);
2259
2614
  gas = input.throttle;
2260
- driver.step(input, dt);
2615
+ if (input.start && sounds.starter) play(sounds.starter);
2616
+ const engine = driver.step(input, dt).events;
2261
2617
  controls.after(driver, dt);
2262
2618
  const s = controls.state;
2619
+ const report = [...engine.filter((e) => e.type !== "shift"), ...systems.step({ driver, controls: s, throttle: input.throttle, dt, radiator: cockpit.radiator })];
2620
+ for (const event of report) o.onEvent?.(event);
2621
+ const powered = driver.state.engineOn || systems.state.charge > 0;
2622
+ const high = s.lamps.mode === "high" || s.lamps.flash;
2263
2623
  lamps.set({
2264
- mode: s.lamps.mode,
2624
+ mode: !powered ? "off" : s.lamps.flash ? "high" : s.lamps.mode,
2265
2625
  fog: s.lamps.fog,
2266
2626
  interior: s.lamps.interior,
2267
2627
  brake: s.brakeLights,
2268
2628
  reverse: s.reverseLights,
2269
2629
  left: s.blinkOn && (s.indicator === "left" || s.indicator === "hazard"),
2270
- right: s.blinkOn && (s.indicator === "right" || s.indicator === "hazard")
2630
+ right: s.blinkOn && (s.indicator === "right" || s.indicator === "hazard"),
2631
+ beacon: s.beacon && powered ? s.beaconSide : null
2271
2632
  });
2633
+ const w = cockpit.weather;
2634
+ const windows = body ? body.windows.reduce((sum, id) => sum + body.window(id), 0) / Math.max(1, body.windows.length) : 0;
2635
+ const warm = systems.state.temperature;
2636
+ const cabinC = s.climate.on ? 22 : w.outsideC + (driver.state.engineOn ? 3 + 5 * Math.min(1, warm * 2) : 2);
2637
+ mist.set(fog.step({ dt, occupants: 1, outsideC: w.outsideC, cabinC, rain: w.rain, speed: Math.abs(driver.state.u), windows, climate: s.climate, engineWarm: driver.state.engineOn ? Math.min(1, warm * 2) : 0 }).fog);
2638
+ if (s.siren !== "off" && powered && (sirenAgain -= dt) <= 0) {
2639
+ play(s.siren === "wail" ? sounds.wail : sounds.yelp);
2640
+ sirenAgain = s.siren === "wail" ? 2.95 : 1.95;
2641
+ }
2642
+ const working = s.hotwire.progress > wiresBefore;
2643
+ wiresBefore = s.hotwire.progress;
2644
+ if (working && (sparkAgain -= dt) <= 0) {
2645
+ play(sounds.spark, 0.7);
2646
+ sparkAgain = 0.45 + 0.4 * (s.hotwire.progress * 7 % 1);
2647
+ }
2272
2648
  dashboard?.update(
2273
2649
  {
2274
2650
  kmh: Math.abs(driver.state.u) * 3.6,
@@ -2278,7 +2654,14 @@ function createCockpit(o) {
2278
2654
  engineOn: driver.state.engineOn,
2279
2655
  left: s.blinkOn && (s.indicator === "left" || s.indicator === "hazard"),
2280
2656
  right: s.blinkOn && (s.indicator === "right" || s.indicator === "hazard"),
2281
- highBeam: s.lamps.mode === "high",
2657
+ highBeam: high,
2658
+ fuel: driver.state.fuel / spec.tank.capacity,
2659
+ temperature: systems.state.temperature,
2660
+ cruise: s.cruise.on,
2661
+ profile: s.profile,
2662
+ tractionOff: !s.tractionControl,
2663
+ absOff: !s.abs,
2664
+ charging: systems.state.netW >= 0,
2282
2665
  lowBeam: s.lamps.mode === "low" || s.lamps.mode === "position",
2283
2666
  fog: s.lamps.fog,
2284
2667
  parkingBrake: s.parkingBrake,
@@ -2287,7 +2670,19 @@ function createCockpit(o) {
2287
2670
  },
2288
2671
  dt
2289
2672
  );
2673
+ if (body) bodyEvents(body.update(dt, s.washer ? 1.2 : WIPE_RATE[s.wipers] ?? 0));
2290
2674
  if (s.tick) play(sounds.blinker, 0.8);
2675
+ const near = o.obstacles?.() ?? [];
2676
+ if (s.ignition && near.length && Math.abs(driver.state.u) < 3) {
2677
+ const g = o.built.group;
2678
+ const reversing = driver.state.gear < 0 || driver.state.u < -0.05;
2679
+ const distance = bumperDistance({ x: g.position.x, z: g.position.z, heading: [Math.sin(g.rotation.y), Math.cos(g.rotation.y)], length: o.built.length, width: o.built.width, forward: !reversing, obstacles: near });
2680
+ const every = parkingBeepInterval(distance);
2681
+ if (every != null && (beepAgain -= dt) <= 0) {
2682
+ play(sounds.beep, 0.7);
2683
+ beepAgain = every === 0 ? 0.09 : every;
2684
+ }
2685
+ }
2291
2686
  if (s.horn && (hornAgain -= dt) <= 0) {
2292
2687
  play(sounds.horn);
2293
2688
  hornAgain = 0.5;
@@ -2296,9 +2691,9 @@ function createCockpit(o) {
2296
2691
  play(sounds.alarm);
2297
2692
  alarmAgain = 2.9;
2298
2693
  }
2299
- if (s.wipers !== "off" && (wipeAgain -= dt) <= 0) {
2694
+ if ((s.wipers !== "off" || s.washer) && (wipeAgain -= dt) <= 0) {
2300
2695
  play(sounds.wiper, 0.7);
2301
- wipeAgain = s.wipers === "fast" ? 0.6 : 1.4;
2696
+ wipeAgain = s.washer || s.wipers === "fast" ? 0.6 : s.wipers === "slow" ? 1.4 : 4;
2302
2697
  }
2303
2698
  const kmh = Math.abs(driver.state.u) * 3.6;
2304
2699
  if (!s.seatbelt && s.ignition && kmh > 10 && (chimeAgain -= dt) <= 0) {
@@ -2306,8 +2701,13 @@ function createCockpit(o) {
2306
2701
  chimeAgain = 6;
2307
2702
  }
2308
2703
  if (s.radio.on && (radioAgain -= dt) <= 0) {
2309
- play(sounds.radio, s.radio.source === "tape" ? 0.25 : 0.45);
2310
- radioAgain = 0.55;
2704
+ const song = RADIO_TRACKS[s.radio.track % RADIO_TRACKS.length];
2705
+ if (song) {
2706
+ play(song.bars[bar % song.bars.length], s.radio.source === "tape" ? 0.8 : 0.7);
2707
+ bar++;
2708
+ }
2709
+ if (s.radio.source === "radio") play(sounds.radio, 0.12);
2710
+ radioAgain = song?.barSeconds ?? 2;
2311
2711
  }
2312
2712
  if (exitAsked) {
2313
2713
  exitAsked = false;
@@ -2320,11 +2720,203 @@ function createCockpit(o) {
2320
2720
  target?.removeEventListener("blur", blur);
2321
2721
  controls.release();
2322
2722
  lamps.dispose();
2723
+ mist.dispose();
2724
+ storage.clear();
2323
2725
  dashboard?.dispose();
2726
+ body?.dispose();
2727
+ destructible?.dispose?.();
2728
+ }
2729
+ };
2730
+ return cockpit;
2731
+ }
2732
+
2733
+ // src/audio/engine-budget.ts
2734
+ var distanceSq = (a, b) => (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2;
2735
+ function createEngineBudget(options) {
2736
+ const max = Math.max(1, options.max ?? 6);
2737
+ const places = /* @__PURE__ */ new Map();
2738
+ return {
2739
+ admit(vehicle, position) {
2740
+ const own = distanceSq(position, options.listener());
2741
+ places.set(vehicle, own);
2742
+ if (places.size <= max) return true;
2743
+ let nearer = 0;
2744
+ for (const [other, d] of places) {
2745
+ if (other === vehicle) continue;
2746
+ if (d < own) nearer++;
2747
+ if (nearer >= max) return false;
2748
+ }
2749
+ return true;
2750
+ },
2751
+ release(vehicle) {
2752
+ places.delete(vehicle);
2753
+ },
2754
+ get size() {
2755
+ return places.size;
2756
+ }
2757
+ };
2758
+ }
2759
+
2760
+ // src/wheel-roll.ts
2761
+ var DEFAULT_RADIUS = 0.32;
2762
+ function createWheelRoll(THREE, built) {
2763
+ const wheels = (built.context?.wheels ?? []).map((wheel) => ({
2764
+ wheel,
2765
+ rest: wheel.quaternion.clone(),
2766
+ radius: Number(wheel.userData.r) || DEFAULT_RADIUS,
2767
+ // in the kit's frame the front is +x: the wheels ahead of the middle steer
2768
+ front: wheel.position.x > 0,
2769
+ angle: 0
2770
+ }));
2771
+ const axle = new THREE.Vector3(0, 0, 1);
2772
+ const up = new THREE.Vector3(0, 1, 0);
2773
+ const spin = new THREE.Quaternion();
2774
+ const steering = new THREE.Quaternion();
2775
+ return {
2776
+ count: wheels.length,
2777
+ roll(distance, steer) {
2778
+ for (const w of wheels) {
2779
+ w.angle = (w.angle - distance / w.radius) % (Math.PI * 2);
2780
+ spin.setFromAxisAngle(axle, w.angle);
2781
+ steering.setFromAxisAngle(up, w.front ? steer : 0);
2782
+ w.wheel.quaternion.copy(w.rest).premultiply(spin).premultiply(steering);
2783
+ }
2784
+ },
2785
+ dispose() {
2786
+ for (const w of wheels) w.wheel.quaternion.copy(w.rest);
2324
2787
  }
2325
2788
  };
2326
2789
  }
2327
2790
 
2791
+ // src/test-area/proving/crash.ts
2792
+ import { launchBody } from "@adatechnology/game-contracts";
2793
+ import { ejectionOf } from "@adatechnology/game-vehicles-driving";
2794
+ var GLASS_BLOW = 6e3;
2795
+ function createProvingCrash(host, occupant) {
2796
+ const thrown = [];
2797
+ const breakGlass = (destructible, exit, forward) => {
2798
+ if (!destructible) return;
2799
+ const want = exit === "windshield" ? "windshield" : exit === "rear_window" ? "rear" : exit.startsWith("left") ? "left" : "right";
2800
+ const pane = destructible.describe().parts.find((p) => p.id.includes(exit === "windshield" || exit === "rear_window" ? want : "window") && (exit === "windshield" || exit === "rear_window" || p.id.includes(want)));
2801
+ if (!pane) return;
2802
+ destructible.hit({ type: "blunt", partId: pane.id, point: destructible.partWorldCenter(pane.id), direction: forward, energy: GLASS_BLOW, radius: 0.35, source: "occupant" });
2803
+ };
2804
+ return {
2805
+ crash({ car, u, w, belted, destructible }) {
2806
+ const ejection = ejectionOf({ before: { u, w }, after: { u: 0, w: 0 } }, belted);
2807
+ if (!ejection) return null;
2808
+ const s = Math.sin(car.rotationY);
2809
+ const c = Math.cos(car.rotationY);
2810
+ const forward = [s, 0, c];
2811
+ const right = [-c, 0, s];
2812
+ breakGlass(destructible, ejection.exit, forward);
2813
+ const [vf, vr, vu] = ejection.velocity;
2814
+ const start = [car.position.x + forward[0] * 1.2, 1.1, car.position.z + forward[2] * 1.2];
2815
+ const placed = occupant ? host.addObject?.(occupant, { position: start, rotationY: car.rotationY }) : null;
2816
+ if (placed) {
2817
+ launchBody(placed.root, [forward[0] * vf + right[0] * vr, vu, forward[2] * vf + right[2] * vr]);
2818
+ thrown.push(placed.remove);
2819
+ }
2820
+ return ejection;
2821
+ },
2822
+ dispose() {
2823
+ for (const remove of thrown.splice(0)) remove();
2824
+ }
2825
+ };
2826
+ }
2827
+
2828
+ // src/test-area/proving/systems-text.ts
2829
+ var SYSTEMS_TEXT = {
2830
+ "pt-BR": {
2831
+ keys: "C embreagem (c\xE2mbio manual) \xB7 X lampejo do farol alto \xB7 Z esguicho do limpador \xB7 7 modo econ\xF4mico/normal/esporte \xB7 8 controle de tra\xE7\xE3o \xB7 9 ABS \xB7 0 piloto autom\xE1tico \xB7 - ar-condicionado \xB7 = desemba\xE7ador \xB7 ] troca a faixa do r\xE1dio ou da fita \xB7 ; sirene \xB7 ' giroflex (ve\xEDculo de emerg\xEAncia) \xB7 / porta-luvas \xB7 sem a chave, segure I: liga\xE7\xE3o direta",
2832
+ profile: "Modo de condu\xE7\xE3o",
2833
+ profiles: { eco: "econ\xF4mico", normal: "normal", sport: "esporte" },
2834
+ traction: "Controle de tra\xE7\xE3o",
2835
+ abs: "ABS",
2836
+ cruise: "Piloto autom\xE1tico",
2837
+ cruiseSet: "Piloto autom\xE1tico: segurando {0} km/h",
2838
+ climate: "Ar-condicionado",
2839
+ demist: "Desemba\xE7ador",
2840
+ wiperModes: { off: "desligado", intermittent: "intermitente", slow: "lento", fast: "r\xE1pido" },
2841
+ refused: { battery_flat: "Bateria descarregada: o motor de partida n\xE3o gira", too_slow: "Piloto autom\xE1tico s\xF3 acima de 30 km/h", locked: "Porta travada", missing: "Este ve\xEDculo n\xE3o tem essa pe\xE7a", no_key: "Sem a chave: segure I para fazer a liga\xE7\xE3o direta", no_siren: "Este ve\xEDculo n\xE3o tem sirene nem giroflex" },
2842
+ events: {
2843
+ stall: "O motor morreu",
2844
+ out_of_fuel: "Acabou o combust\xEDvel",
2845
+ bogged: "Atolou",
2846
+ aquaplaning: "Aquaplanagem",
2847
+ battery_flat: "A bateria descarregou",
2848
+ overheating: "Motor superaquecendo: perdendo pot\xEAncia",
2849
+ seized: "O motor fundiu de tanto esquentar",
2850
+ spare_fitted: "Estepe colocado",
2851
+ no_spare: "N\xE3o h\xE1 estepe",
2852
+ cruise_cancelled: "Piloto autom\xE1tico desligado",
2853
+ hotwire_started: "Fios descascados sob a coluna: continue segurando I",
2854
+ hotwire_joined: "Liga\xE7\xE3o direta feita: a igni\xE7\xE3o pegou e o motor de partida gira"
2855
+ },
2856
+ actions: { puncture: "Furar pneu (dianteiro esquerdo)", fit_spare: "Trocar pelo estepe", refuel: "Abastecer", drain_battery: "Descarregar bateria", jump_start: "Chupeta (carregar bateria)", break_radiator: "Furar o radiador", empty_tank: "Deixar o tanque na reserva", remote_lock: "Controle: travar (arma o alarme)", remote_unlock: "Controle: destravar", force_door: "For\xE7ar a porta do motorista", crash_wall: "Bater no muro (parada seca)", park_behind: "Estacionar um carro atr\xE1s (sensor de r\xE9)", take_key: "Tirar a chave (liga\xE7\xE3o direta) / devolver", cold_rain: "Tempo: frio e chuva / bom", glovebox_contents: "Ver o porta-luvas", trunk_contents: "Ver o porta-malas", take_from_trunk: "Tirar um item do porta-malas para o ch\xE3o" },
2857
+ siren: "Sirene",
2858
+ sirenModes: { off: "desligada", wail: "cont\xEDnua", yelp: "r\xE1pida (cruzamento)" },
2859
+ beacon: "Giroflex",
2860
+ glovebox: "Porta-luvas",
2861
+ lid: { open: "aberto", shut: "fechado" },
2862
+ storage: { names: { glovebox: "Porta-luvas", trunk: "Porta-malas" }, empty: "{0}: vazio", shut: "{0} fechado: abra antes (porta-luvas com / sentado, porta-malas com 6)", list: "{0} ({1} kg): {2}", none: "Este ve\xEDculo n\xE3o tem {0}", taken: "{0} tirado do porta-malas e posto no ch\xE3o", ground: "{0} tirado do porta-malas (sem cat\xE1logo de itens nesta sala para p\xF4-lo no ch\xE3o)" },
2863
+ fog: { misted: "O para-brisa emba\xE7ou por dentro: = desemba\xE7ador, ar-condicionado ou um vidro aberto", clear: "Para-brisa desemba\xE7ado", weather: "L\xE1 fora: 8 \xB0C e chuva", fair: "L\xE1 fora: 25 \xB0C, sem chuva", metric: "Vidro emba\xE7ado" },
2864
+ keyTaken: "Sem a chave no contato: segure I para a liga\xE7\xE3o direta",
2865
+ keyBack: "Chave de volta no contato",
2866
+ parking: { placed: "Carro parado 3 m atr\xE1s: d\xEA r\xE9 e escute o sensor", none: "Sem cat\xE1logo de ve\xEDculos nesta sala: o sensor n\xE3o tem o que ver" },
2867
+ track: "faixa {0}",
2868
+ adjust: { seat_forward: "Banco: frente/tr\xE1s (m)", seat_height: "Banco: altura (m)", mirror_left: "Retrovisor esquerdo: \xE2ngulo", mirror_right: "Retrovisor direito: \xE2ngulo" },
2869
+ crash: { mild: "Batida a {0} km/h: fraca demais para lan\xE7ar quem est\xE1 sem cinto", stayed: "Batida a {0} km/h: o cinto segurou o motorista", ejected: "Batida a {0} km/h sem cinto: o motorista saiu pelo {1}", exits: { windshield: "para-brisa", left_window: "vidro esquerdo", right_window: "vidro direito", rear_window: "vidro traseiro" } },
2870
+ bodyKeys: "1 a 4 portas \xB7 5 cap\xF4 \xB7 6 porta-malas \xB7 Shift+1 a 4 vidros \xB7 Shift+5 teto solar \xB7 V trava as portas",
2871
+ remote: { locked: "Travado pelo controle: alarme armado", unlocked: "Destravado pelo controle", forced: "Porta for\xE7ada" },
2872
+ done: { puncture: "Pneu dianteiro esquerdo furado", refuel: "Tanque cheio", drain_battery: "Bateria quase sem carga", jump_start: "Bateria carregada", break_radiator: "Radiador furado: o motor vai esquentar", empty_tank: "Tanque na reserva" }
2873
+ },
2874
+ en: {
2875
+ keys: "C clutch (manual gearbox) \xB7 X flash the high beam \xB7 Z washer \xB7 7 eco/normal/sport \xB7 8 traction control \xB7 9 ABS \xB7 0 cruise control \xB7 - air conditioning \xB7 = demister \xB7 ] next radio or tape track \xB7 ; siren \xB7 ' beacon (emergency vehicle) \xB7 / glovebox \xB7 without the key, hold I: hotwire",
2876
+ profile: "Drive profile",
2877
+ profiles: { eco: "eco", normal: "normal", sport: "sport" },
2878
+ traction: "Traction control",
2879
+ abs: "ABS",
2880
+ cruise: "Cruise control",
2881
+ cruiseSet: "Cruise control: holding {0} km/h",
2882
+ climate: "Air conditioning",
2883
+ demist: "Demister",
2884
+ wiperModes: { off: "off", intermittent: "intermittent", slow: "slow", fast: "fast" },
2885
+ refused: { battery_flat: "Flat battery: the starter does not turn", too_slow: "Cruise control only above 30 km/h", locked: "The door is locked", missing: "This vehicle has no such part", no_key: "No key: hold I to hotwire it", no_siren: "This vehicle has no siren or beacon" },
2886
+ events: {
2887
+ stall: "The engine stalled",
2888
+ out_of_fuel: "Out of fuel",
2889
+ bogged: "Bogged down",
2890
+ aquaplaning: "Aquaplaning",
2891
+ battery_flat: "The battery went flat",
2892
+ overheating: "Engine overheating: losing power",
2893
+ seized: "The engine seized from the heat",
2894
+ spare_fitted: "Spare tyre fitted",
2895
+ no_spare: "No spare tyre left",
2896
+ cruise_cancelled: "Cruise control off",
2897
+ hotwire_started: "Wires stripped under the column: keep holding I",
2898
+ hotwire_joined: "Hotwired: the ignition is live and the starter turns"
2899
+ },
2900
+ actions: { puncture: "Puncture a tyre (front left)", fit_spare: "Fit the spare", refuel: "Refuel", drain_battery: "Drain the battery", jump_start: "Jump start (charge the battery)", break_radiator: "Puncture the radiator", empty_tank: "Leave the tank on reserve", remote_lock: "Remote: lock (arms the alarm)", remote_unlock: "Remote: unlock", force_door: "Force the driver's door", crash_wall: "Hit a wall (dead stop)", park_behind: "Park a car behind (rear sensor)", take_key: "Take the key out (hotwire) / give it back", cold_rain: "Weather: cold and rain / fair", glovebox_contents: "Look in the glovebox", trunk_contents: "Look in the trunk", take_from_trunk: "Take an item out of the trunk to the ground" },
2901
+ siren: "Siren",
2902
+ sirenModes: { off: "off", wail: "wail", yelp: "yelp (junction)" },
2903
+ beacon: "Beacon",
2904
+ glovebox: "Glovebox",
2905
+ lid: { open: "open", shut: "shut" },
2906
+ storage: { names: { glovebox: "Glovebox", trunk: "Trunk" }, empty: "{0}: empty", shut: "{0} shut: open it first (glovebox with / while seated, trunk with 6)", list: "{0} ({1} kg): {2}", none: "This vehicle has no {0}", taken: "{0} taken out of the trunk and put on the ground", ground: "{0} taken out of the trunk (no item catalogue in this room to put it on the ground)" },
2907
+ fog: { misted: "The windshield misted up inside: = demister, air conditioning or a window down", clear: "Windshield clear", weather: "Outside: 8 \xB0C and rain", fair: "Outside: 25 \xB0C, no rain", metric: "Glass misted" },
2908
+ keyTaken: "No key in the ignition: hold I to hotwire",
2909
+ keyBack: "Key back in the ignition",
2910
+ parking: { placed: "A car parked 3 m behind: reverse and listen to the sensor", none: "No vehicle catalogue in this room: the sensor has nothing to see" },
2911
+ track: "track {0}",
2912
+ adjust: { seat_forward: "Seat: forward/back (m)", seat_height: "Seat: height (m)", mirror_left: "Left mirror: angle", mirror_right: "Right mirror: angle" },
2913
+ crash: { mild: "Crash at {0} km/h: too mild to throw an unbelted occupant out", stayed: "Crash at {0} km/h: the belt held the driver", ejected: "Crash at {0} km/h without a belt: the driver went out through the {1}", exits: { windshield: "windshield", left_window: "left window", right_window: "right window", rear_window: "rear window" } },
2914
+ bodyKeys: "1 to 4 doors \xB7 5 hood \xB7 6 trunk \xB7 Shift+1 to 4 windows \xB7 Shift+5 sunroof \xB7 V locks the doors",
2915
+ remote: { locked: "Locked by the remote: alarm armed", unlocked: "Unlocked by the remote", forced: "Door forced" },
2916
+ done: { puncture: "Front left tyre punctured", refuel: "Tank full", drain_battery: "Battery almost flat", jump_start: "Battery charged", break_radiator: "Radiator punctured: the engine will heat up", empty_tank: "Tank on reserve" }
2917
+ }
2918
+ };
2919
+
2328
2920
  // src/test-area/proving/text.ts
2329
2921
  var PROVING_TEXT = {
2330
2922
  "pt-BR": {
@@ -2469,9 +3061,11 @@ var MIRROR_RECTS = {
2469
3061
  right: { x: 0.79, y: 0.72, width: 0.19, height: 0.13, rounded: 0.3 },
2470
3062
  reverse: { x: 0.36, y: 0.13, width: 0.28, height: 0.18, rounded: 0.06 }
2471
3063
  };
3064
+ var ADJUST_KEYS = { seat_forward: "seatForward", seat_height: "seatHeight", mirror_left: "mirrorLeft", mirror_right: "mirrorRight" };
2472
3065
  function createProvingModule(options) {
2473
3066
  const locale = options.locale ?? "pt-BR";
2474
3067
  const text = PROVING_TEXT[locale] ?? PROVING_TEXT["pt-BR"];
3068
+ const sysText = SYSTEMS_TEXT[locale] ?? SYSTEMS_TEXT["pt-BR"];
2475
3069
  const tests = options.tests ?? createProvingTests();
2476
3070
  const vehicles = options.vehicles;
2477
3071
  const sounds = { ...defaultSoundIds, ...options.sounds };
@@ -2488,7 +3082,19 @@ function createProvingModule(options) {
2488
3082
  let run = null;
2489
3083
  let parking = null;
2490
3084
  let driving = null;
3085
+ let crashes = null;
3086
+ let wheels = null;
3087
+ let lastAt = null;
3088
+ let parked = null;
3089
+ const adjust = { seatForward: 0, seatHeight: 0, mirrorLeft: 0, mirrorRight: 0 };
3090
+ const aimed = (pose2) => {
3091
+ const turn = pose2.id === "left" ? adjust.mirrorLeft : pose2.id === "right" ? -adjust.mirrorRight : 0;
3092
+ return turn ? { ...pose2, look: [pose2.look[0] + turn, pose2.look[1], pose2.look[2]] } : pose2;
3093
+ };
2491
3094
  let gearbox = "automatic";
3095
+ let hasKey = true;
3096
+ let weather = { outsideC: 25, rain: 0 };
3097
+ let misted = false;
2492
3098
  let lastGear = null;
2493
3099
  let driveClock = 0;
2494
3100
  let queue = [];
@@ -2649,6 +3255,8 @@ function createProvingModule(options) {
2649
3255
  };
2650
3256
  const removeCar = () => {
2651
3257
  if (!car) return;
3258
+ wheels = null;
3259
+ lastAt = null;
2652
3260
  discardObject3D3(car.built.group);
2653
3261
  car = null;
2654
3262
  };
@@ -2662,6 +3270,7 @@ function createProvingModule(options) {
2662
3270
  built.group.add(guides);
2663
3271
  host.scene.add(built.group);
2664
3272
  car = { built, guides, subject };
3273
+ wheels = createWheelRoll(three, built);
2665
3274
  pose({ x: 0, z: TRACK.start, yaw: 0, pitch: 0, roll: 0, height: 0 }, 0);
2666
3275
  rig.vehicle?.reset();
2667
3276
  drawBoard();
@@ -2712,11 +3321,18 @@ function createProvingModule(options) {
2712
3321
  car.built.group.position.set(TRACK.x0 + wrapped, 0, TRACK.z + s.z);
2713
3322
  car.built.group.rotation.set(-s.pitch, s.yaw + Math.PI / 2, s.roll, "YXZ");
2714
3323
  bendGuides(steer);
3324
+ if (lastAt) {
3325
+ const along = (s.x - lastAt.x) * Math.cos(s.yaw) - (s.z - lastAt.z) * Math.sin(s.yaw);
3326
+ if (Math.abs(along) < 5) wheels?.roll(along, steer);
3327
+ }
3328
+ lastAt = { x: s.x, z: s.z };
2715
3329
  };
2716
3330
  const frameOf = (speed) => {
2717
3331
  if (!car) return null;
2718
3332
  const g = car.built.group;
2719
- return { x: g.position.x, y: g.position.y, z: g.position.z, yaw: g.rotation.y, pitch: -g.rotation.x, roll: g.rotation.z, speed, length: car.built.length, width: car.built.width, height: car.built.height, ...car.built.driverEye ? { eye: car.built.driverEye } : {} };
3333
+ const size = { length: car.built.length, width: car.built.width, height: car.built.height };
3334
+ const [ex, ey, ez] = car.built.driverEye ?? defaultEye(size);
3335
+ return { x: g.position.x, y: g.position.y, z: g.position.z, yaw: g.rotation.y, pitch: -g.rotation.x, roll: g.rotation.z, speed, ...size, eye: [ex, ey + adjust.seatHeight, ez + adjust.seatForward] };
2720
3336
  };
2721
3337
  const applyCamera = () => {
2722
3338
  if (!host?.setViewCamera) return;
@@ -2798,26 +3414,42 @@ function createProvingModule(options) {
2798
3414
  run = null;
2799
3415
  queue = [];
2800
3416
  parking = null;
2801
- setSurface("dry_asphalt");
3417
+ setSurface(weather.rain > 0 ? "wet_asphalt" : "dry_asphalt");
3418
+ misted = false;
2802
3419
  driving = createCockpit({
2803
3420
  THREE: three,
2804
3421
  host,
2805
3422
  built: car.built,
2806
3423
  subject: car.subject,
2807
3424
  place: [0, 0, 0],
3425
+ kit: kitFor(three),
3426
+ obstacles: () => parked ? [[parked.root.position.x, parked.root.position.z]] : [],
2808
3427
  gauge: kitFor(three).materials?.gauge,
2809
3428
  needle: kitFor(three).materials?.amber,
2810
3429
  // the engine's own sound turns the key (starter, catch, idle by RPM)
2811
3430
  sounds: { starter: "" },
3431
+ hasKey,
3432
+ weather,
3433
+ stock: (compartment) => options.stock?.(compartment, settings.vehicle) ?? [],
2812
3434
  onExit: () => stopDriving(),
2813
3435
  onEvent: (event) => {
2814
- if (event.type === "refused") return host?.log(text.cabin.refused[event.reason] ?? event.reason);
3436
+ if (event.type === "hotwire") {
3437
+ host?.log(sysText.events[event.stage === "joined" ? "hotwire_joined" : "hotwire_started"], { vehicle: settings.vehicle, stage: event.stage }, "proving_system");
3438
+ if (event.stage === "joined" && car && driving?.systems.canCrank() !== false) engine?.start([car.built.group.position.x, 0.5, car.built.group.position.z]);
3439
+ return;
3440
+ }
3441
+ if (event.type === "refused") return host?.log(text.cabin.refused[event.reason] ?? sysText.refused[event.reason] ?? event.reason);
3442
+ if (event.type === "sound") return;
3443
+ if (event.type !== "command") {
3444
+ if (event.type === "out_of_fuel" || event.type === "stall" || event.type === "seized") engine?.stop([car?.built.group.position.x ?? 0, 0.5, car?.built.group.position.z ?? 0]);
3445
+ return host?.log(sysText.events[event.type] ?? event.type, { vehicle: settings.vehicle, ...event }, "proving_system");
3446
+ }
2815
3447
  const st = event.state;
2816
3448
  const line = cabinLine(event.command, st);
2817
3449
  if (line) host?.log(line);
2818
3450
  if (event.command === "ignition" && car) {
2819
3451
  const at = [car.built.group.position.x, 0.5, car.built.group.position.z];
2820
- if (st.ignition) engine?.start(at);
3452
+ if (st.ignition && driving?.systems.canCrank() !== false) engine?.start(at);
2821
3453
  else engine?.stop(at);
2822
3454
  }
2823
3455
  if (event.command === "gearbox") {
@@ -2836,7 +3468,7 @@ function createProvingModule(options) {
2836
3468
  rig.vehicle?.reset();
2837
3469
  stopEngine();
2838
3470
  engine = createVehicleSound(host.audio, { sounds });
2839
- host.bench?.sheet?.({ kicker: text.name, title: fillText(text.log.drive, name(settings.vehicle)), body: text.cabin.keys });
3471
+ host.bench?.sheet?.({ kicker: text.name, title: fillText(text.log.drive, name(settings.vehicle)), body: `${text.cabin.keys} \xB7 ${sysText.keys} \xB7 ${sysText.bodyKeys}` });
2840
3472
  host.log(fillText(text.log.drive, name(settings.vehicle)));
2841
3473
  };
2842
3474
  const onOff = (on) => on ? text.cabin.on : text.cabin.off;
@@ -2865,19 +3497,156 @@ function createProvingModule(options) {
2865
3497
  return `${c.alarm}: ${c.alarmStates[st.alarm]}`;
2866
3498
  case "radio":
2867
3499
  case "radio_source":
2868
- return `${c.radio}: ${st.radio.on ? c.sources[st.radio.source] : c.off}`;
3500
+ case "radio_track":
3501
+ return `${c.radio}: ${st.radio.on ? `${c.sources[st.radio.source]} \xB7 ${fillText(sysText.track, st.radio.track + 1)}` : c.off}`;
2869
3502
  case "indicator_left":
2870
3503
  case "indicator_right":
2871
3504
  case "hazard":
2872
3505
  return `${c.indicator}: ${c.indicators[st.indicator]}`;
2873
3506
  case "wipers":
2874
- return `${c.wipers}: ${c.wiperModes[st.wipers]}`;
3507
+ return `${c.wipers}: ${sysText.wiperModes[st.wipers]}`;
3508
+ case "drive_profile":
3509
+ return `${sysText.profile}: ${sysText.profiles[st.profile]}`;
3510
+ case "traction_control":
3511
+ return `${sysText.traction}: ${onOff(st.tractionControl)}`;
3512
+ case "abs":
3513
+ return `${sysText.abs}: ${onOff(st.abs)}`;
3514
+ case "cruise":
3515
+ return st.cruise.on ? fillText(sysText.cruiseSet, Math.round(st.cruise.speed * 3.6)) : `${sysText.cruise}: ${c.off}`;
3516
+ case "climate":
3517
+ return `${sysText.climate}: ${onOff(st.climate.on)}`;
3518
+ case "demist":
3519
+ return `${sysText.demist}: ${onOff(st.climate.demist)}`;
2875
3520
  case "seatbelt":
2876
3521
  return `${c.seatbelt}: ${onOff(st.seatbelt)}`;
3522
+ case "siren":
3523
+ return `${sysText.siren}: ${sysText.sirenModes[st.siren]}`;
3524
+ case "beacon":
3525
+ return `${sysText.beacon}: ${onOff(st.beacon)}`;
3526
+ case "glovebox":
3527
+ return `${sysText.glovebox}: ${sysText.lid[st.glovebox ? "open" : "shut"]}`;
2877
3528
  default:
2878
3529
  return null;
2879
3530
  }
2880
3531
  };
3532
+ const itemName = (id) => options.itemName?.(id) ?? id;
3533
+ const listCompartment = (cockpit, id) => {
3534
+ const label = sysText.storage.names[id];
3535
+ const data = { vehicle: settings.vehicle, compartment: id };
3536
+ if (!cockpit.storage.compartments.some((c) => c.id === id)) return host?.log(fillText(sysText.storage.none, label.toLowerCase()), data, "proving_storage");
3537
+ if (!cockpit.storage.reachable(id, cockpit.access())) return host?.log(fillText(sysText.storage.shut, label), data, "proving_storage");
3538
+ const stacks = cockpit.storage.contents(id).filter((x) => x !== null);
3539
+ if (!stacks.length) return host?.log(fillText(sysText.storage.empty, label), data, "proving_storage");
3540
+ host?.log(fillText(sysText.storage.list, label, cockpit.storage.loadKg(id).toFixed(1), stacks.map((x) => x.count > 1 ? `${x.count}\xD7 ${itemName(x.itemId)}` : itemName(x.itemId)).join(", ")), { ...data, stacks: stacks.map((x) => ({ item: x.itemId, count: x.count })) }, "proving_storage");
3541
+ };
3542
+ const systemsAction = (id) => {
3543
+ if (id === "take_key") {
3544
+ hasKey = !hasKey;
3545
+ if (driving) stopDriving();
3546
+ startDriving();
3547
+ host?.log(hasKey ? sysText.keyBack : sysText.keyTaken, { vehicle: settings.vehicle, hasKey }, "proving_system");
3548
+ return;
3549
+ }
3550
+ if (id === "cold_rain") {
3551
+ weather = weather.rain > 0 ? { outsideC: 25, rain: 0 } : { outsideC: 8, rain: 1 };
3552
+ if (driving) driving.weather = { ...weather };
3553
+ setSurface(weather.rain > 0 ? "wet_asphalt" : "dry_asphalt");
3554
+ host?.log(weather.rain > 0 ? sysText.fog.weather : sysText.fog.fair, { vehicle: settings.vehicle, ...weather }, "proving_system");
3555
+ return;
3556
+ }
3557
+ if (!driving) startDriving();
3558
+ const cockpit = driving;
3559
+ if (!cockpit) return;
3560
+ const d = cockpit.driver;
3561
+ const done = (key) => host?.log(sysText.done[key], { vehicle: settings.vehicle, action: key }, "proving_system");
3562
+ switch (id) {
3563
+ case "puncture": {
3564
+ const tyres = d.spec.wheels.map((_, i) => d.condition.tyres?.[i] ?? 1);
3565
+ tyres[0] = 0;
3566
+ d.condition = { ...d.condition, tyres };
3567
+ return done("puncture");
3568
+ }
3569
+ case "fit_spare": {
3570
+ const flat = d.spec.wheels.findIndex((_, i) => (d.condition.tyres?.[i] ?? 1) < 0.2);
3571
+ const event = cockpit.systems.fitSpare(d, Math.max(0, flat));
3572
+ host?.log(sysText.events[event.type], { vehicle: settings.vehicle, ...event }, "proving_system");
3573
+ return;
3574
+ }
3575
+ case "refuel":
3576
+ d.refuel(d.spec.tank.capacity);
3577
+ return done("refuel");
3578
+ case "empty_tank":
3579
+ d.state.fuel = Math.min(d.state.fuel, d.spec.tank.capacity * 0.04);
3580
+ return done("empty_tank");
3581
+ case "drain_battery":
3582
+ cockpit.systems.charge(-(cockpit.systems.state.charge - 0.15));
3583
+ return done("drain_battery");
3584
+ case "jump_start":
3585
+ cockpit.systems.charge(1);
3586
+ return done("jump_start");
3587
+ case "break_radiator":
3588
+ cockpit.radiator = 0.05;
3589
+ return done("break_radiator");
3590
+ case "remote_lock":
3591
+ case "remote_unlock": {
3592
+ const lock = id === "remote_lock";
3593
+ cockpit.body?.lock(lock);
3594
+ cockpit.controls.state.alarm = lock ? "armed" : "off";
3595
+ host?.audio?.play?.({ sound: "alarm_chirp", position: [car?.built.group.position.x ?? 0, 1, car?.built.group.position.z ?? 0] });
3596
+ host?.log(sysText.remote[lock ? "locked" : "unlocked"], { vehicle: settings.vehicle, locked: lock }, "proving_system");
3597
+ return;
3598
+ }
3599
+ case "crash_wall": {
3600
+ const g = car?.built.group;
3601
+ if (!g || !host) return;
3602
+ crashes ?? (crashes = createProvingCrash(host, options.occupant));
3603
+ const speed = d.state.u;
3604
+ const ejection = crashes.crash({ car: { position: g.position, rotationY: g.rotation.y }, u: d.state.u, w: d.state.w, belted: cockpit.controls.state.seatbelt, destructible: cockpit.destructible });
3605
+ d.state.u = d.state.w = d.state.yawRate = 0;
3606
+ const kmh = Math.round(Math.abs(speed) * 3.6);
3607
+ const belted = cockpit.controls.state.seatbelt;
3608
+ host.log(ejection ? fillText(sysText.crash.ejected, kmh, sysText.crash.exits[ejection.exit]) : fillText(belted ? sysText.crash.stayed : sysText.crash.mild, kmh), { vehicle: settings.vehicle, kmh, belted: cockpit.controls.state.seatbelt, ejection }, "proving_crash");
3609
+ return;
3610
+ }
3611
+ case "park_behind": {
3612
+ const g = car?.built.group;
3613
+ if (!g || !host) return;
3614
+ parked?.remove();
3615
+ const back = (car?.built.length ?? 4.5) / 2 + 3 + 2.3;
3616
+ const at = [g.position.x - Math.sin(g.rotation.y) * back, 0, g.position.z - Math.cos(g.rotation.y) * back];
3617
+ const placed = host.addObject?.(options.parkedCar ?? "sedan", { position: at, rotationY: g.rotation.y });
3618
+ parked = placed ? { root: placed.root, remove: placed.remove } : null;
3619
+ host.log(placed ? sysText.parking.placed : sysText.parking.none, { vehicle: settings.vehicle }, "proving_system");
3620
+ return;
3621
+ }
3622
+ case "glovebox_contents":
3623
+ return listCompartment(cockpit, "glovebox");
3624
+ case "trunk_contents":
3625
+ return listCompartment(cockpit, "trunk");
3626
+ case "take_from_trunk": {
3627
+ const storage = cockpit.storage;
3628
+ if (!storage.compartments.some((c) => c.id === "trunk")) return listCompartment(cockpit, "trunk");
3629
+ const slot = storage.contents("trunk").findIndex((x) => x !== null);
3630
+ if (slot < 0 || !storage.reachable("trunk", cockpit.access())) return listCompartment(cockpit, "trunk");
3631
+ const stack = storage.take("trunk", slot, cockpit.access());
3632
+ if (!stack) return;
3633
+ const g = car?.built.group;
3634
+ const back = (car?.built.length ?? 4.5) / 2 + 0.8;
3635
+ const placed = g ? host?.addObject?.(stack.itemId, { position: [g.position.x - Math.sin(g.rotation.y) * back, 0, g.position.z - Math.cos(g.rotation.y) * back] }) : null;
3636
+ host?.log(fillText(placed ? sysText.storage.taken : sysText.storage.ground, itemName(stack.itemId)), { vehicle: settings.vehicle, item: stack.itemId, count: stack.count, placed: !!placed }, "proving_storage");
3637
+ return;
3638
+ }
3639
+ case "force_door": {
3640
+ const body = cockpit.body;
3641
+ if (!body) return;
3642
+ if (body.locked) body.lock(false);
3643
+ body.toggle("door_front_left");
3644
+ if (cockpit.controls.state.alarm === "armed") cockpit.controls.state.alarm = "ringing";
3645
+ host?.log(sysText.remote.forced, { vehicle: settings.vehicle, alarm: cockpit.controls.state.alarm }, "proving_system");
3646
+ return;
3647
+ }
3648
+ }
3649
+ };
2881
3650
  const gearName = (gear) => gear < 0 ? "R" : gear === 0 ? "N" : String(gear);
2882
3651
  const onChange = (change) => {
2883
3652
  if (change.kind === "layer") {
@@ -2886,6 +3655,7 @@ function createProvingModule(options) {
2886
3655
  }
2887
3656
  if (change.kind === "action") {
2888
3657
  if (change.id === "drive") return driving ? stopDriving() : startDriving();
3658
+ if (change.id in sysText.actions) return systemsAction(change.id);
2889
3659
  if (change.id === "run_test" || change.id === "run_all" || change.id === "reverse_park") stopDriving();
2890
3660
  if (change.id === "run_test") {
2891
3661
  queue = [];
@@ -2932,6 +3702,12 @@ function createProvingModule(options) {
2932
3702
  settings.camera = String(change.value);
2933
3703
  applyCamera();
2934
3704
  break;
3705
+ case "seat_forward":
3706
+ case "seat_height":
3707
+ case "mirror_left":
3708
+ case "mirror_right":
3709
+ adjust[ADJUST_KEYS[change.id]] = Number(change.value);
3710
+ break;
2935
3711
  case "time_scale":
2936
3712
  settings.timeScale = Number(change.value);
2937
3713
  break;
@@ -2966,6 +3742,13 @@ function createProvingModule(options) {
2966
3742
  const d = driving;
2967
3743
  d.step(dt);
2968
3744
  if (!driving) return;
3745
+ if (!misted && d.fog.state.fog > 0.6) {
3746
+ misted = true;
3747
+ host.log(sysText.fog.misted, { vehicle: settings.vehicle, fog: +d.fog.state.fog.toFixed(2), humidity: +d.fog.state.humidity.toFixed(2) }, "proving_system");
3748
+ } else if (misted && d.fog.state.fog < 0.1) {
3749
+ misted = false;
3750
+ host.log(sysText.fog.clear, { vehicle: settings.vehicle }, "proving_system");
3751
+ }
2969
3752
  pose(d.driver.state, d.driver.state.steerAngle);
2970
3753
  speed = d.driver.state.u;
2971
3754
  steer = d.driver.state.steerAngle;
@@ -2981,7 +3764,7 @@ function createProvingModule(options) {
2981
3764
  if (car) engine?.update({ dt, position: [car.built.group.position.x, 0.5, car.built.group.position.z], speed, throttle: run ? 1 : driving?.throttle ?? 0, rpm: driving ? driving.driver.state.rpm : void 0 });
2982
3765
  const f = frameOf(speed);
2983
3766
  if (f && rig.vehicle && settings.camera !== "room") rig.vehicle.update(rig.camera, f, dt);
2984
- if (f && rig.vehicle) for (const m of rig.mirrors) rig.vehicle.placeMirror(m.camera, m.pose, f);
3767
+ if (f && rig.vehicle) for (const m of rig.mirrors) rig.vehicle.placeMirror(m.camera, aimed(m.pose), f);
2985
3768
  void steer;
2986
3769
  const key = `${host.bench?.layer("mirrors")}|${host.bench?.layer("reverse_camera")}`;
2987
3770
  if (key !== mirrorKey) {
@@ -3000,6 +3783,10 @@ function createProvingModule(options) {
3000
3783
  const speedNow = () => Math.abs(run?.driver.state.u ?? driving?.driver.state.u ?? parking?.driver.state.u ?? 0) * 3.6;
3001
3784
  const counts = (verdict) => certificateOf(settings.vehicle)?.results.filter((r) => r.verdict === verdict).length ?? 0;
3002
3785
  const teardown = () => {
3786
+ crashes?.dispose();
3787
+ crashes = null;
3788
+ parked?.remove();
3789
+ parked = null;
3003
3790
  stopDriving();
3004
3791
  stopEngine();
3005
3792
  for (const m of rig.mirrors) m.remove();
@@ -3030,9 +3817,14 @@ function createProvingModule(options) {
3030
3817
  { kind: "choice", id: "vehicle", name: text.controls.vehicle, hint: text.hints.vehicle, options: vehicles.map((id) => ({ id, name: name(id) })), default: settings.vehicle },
3031
3818
  { kind: "choice", id: "test", name: text.controls.test, hint: text.hints.test, options: measured.map((id) => ({ id, name: text.tests[id] ?? id })), default: settings.test },
3032
3819
  { kind: "choice", id: "camera", name: text.controls.camera, hint: text.hints.camera, options: ["room", "chase", "cockpit", "hood"].map((id) => ({ id, name: text.cameras[id] })), default: settings.camera },
3820
+ { kind: "number", id: "seat_forward", name: sysText.adjust.seat_forward, min: -0.2, max: 0.2, step: 0.02, default: 0 },
3821
+ { kind: "number", id: "seat_height", name: sysText.adjust.seat_height, min: -0.1, max: 0.1, step: 0.01, default: 0 },
3822
+ { kind: "number", id: "mirror_left", name: sysText.adjust.mirror_left, min: -0.3, max: 0.3, step: 0.02, default: 0 },
3823
+ { kind: "number", id: "mirror_right", name: sysText.adjust.mirror_right, min: -0.3, max: 0.3, step: 0.02, default: 0 },
3033
3824
  { kind: "number", id: "time_scale", name: text.controls.time_scale, hint: text.hints.time_scale, min: 1, max: 20, step: 1, default: settings.timeScale },
3034
3825
  { kind: "choice", id: "visual_check", name: text.controls.visual_check, hint: text.hints.visual_check, options: VISUAL_CHECKS.map((id) => ({ id, name: text.tests[id] ?? id })), default: settings.visual },
3035
3826
  { kind: "action", id: "drive", name: text.actions.drive },
3827
+ ...Object.keys(sysText.actions).map((id) => ({ kind: "action", id, name: sysText.actions[id] })),
3036
3828
  { kind: "choice", id: "gearbox", name: text.controls.gearbox, hint: text.hints.gearbox, options: ["automatic", "manual"].map((id) => ({ id, name: text.gearbox[id] })), default: gearbox },
3037
3829
  { kind: "action", id: "run_test", name: text.actions.run_test },
3038
3830
  { kind: "action", id: "run_all", name: text.actions.run_all },
@@ -3052,6 +3844,7 @@ function createProvingModule(options) {
3052
3844
  { id: "gear", name: text.metrics.gear, read: () => driving?.driver.state.gear ?? run?.driver.state.gear ?? 0 },
3053
3845
  { id: "rpm", name: text.metrics.rpm, unit: "rpm", digits: 0, read: () => driving?.driver.state.rpm ?? run?.driver.state.rpm ?? 0 },
3054
3846
  { id: "time", name: text.metrics.time, unit: "s", digits: 1, read: () => run?.time ?? 0 },
3847
+ { id: "fog", name: sysText.fog.metric, unit: "%", digits: 0, read: () => (driving?.fog.state.fog ?? 0) * 100 },
3055
3848
  { id: "value", name: text.metrics.value, digits: 1, read: () => run ? run.trace[run.trace.length - 1]?.[1] ?? 0 : 0 },
3056
3849
  { id: "passed", name: text.metrics.passed, read: () => counts("pass") },
3057
3850
  { id: "failed", name: text.metrics.failed, read: () => counts("fail") },
@@ -3066,6 +3859,8 @@ function createProvingModule(options) {
3066
3859
  rig.vehicle = createVehicleCamera(three);
3067
3860
  lent.audio?.register?.(LIBRARY_BANK2);
3068
3861
  lent.audio?.register?.(CABIN_BANK);
3862
+ lent.audio?.register?.(VEHICLE_SOUND_BANK);
3863
+ lent.audio?.register?.(RADIO_BANK);
3069
3864
  const stopChange = lent.bench?.onChange(onChange);
3070
3865
  const stopFrame = lent.onFrame(frame);
3071
3866
  const stopClear = lent.onClear?.(() => {
@@ -3098,10 +3893,6 @@ export {
3098
3893
  liteMeshes,
3099
3894
  createVehiclesThree,
3100
3895
  createCabinSmoke,
3101
- VEHICLE_LOOP_FALLOFF,
3102
- VEHICLE_LOOPS,
3103
- VEHICLE_SOUNDS,
3104
- VEHICLE_SOUND_BANK,
3105
3896
  firingHz,
3106
3897
  rpmShare,
3107
3898
  layerWeight,
@@ -3113,6 +3904,15 @@ export {
3113
3904
  TRUCK_ENGINE,
3114
3905
  BIKE_ENGINE,
3115
3906
  ENGINE_PROFILES,
3907
+ VEHICLE_LOOP_FALLOFF,
3908
+ VEHICLE_LOOPS,
3909
+ VEHICLE_SOUNDS,
3910
+ CAR_RECORDED_RPM,
3911
+ CAR_START_RUN_STOP,
3912
+ VEHICLE_SAMPLE_ALIASES,
3913
+ VEHICLE_PRELOAD_SOUNDS,
3914
+ VEHICLE_SOUND_BANK,
3915
+ createEngineBudget,
3116
3916
  createVehicleSound,
3117
3917
  VEHICLE_LOD_DISTANCE,
3118
3918
  createVehicleLod,
@@ -3126,13 +3926,17 @@ export {
3126
3926
  DEFAULT_TEST_VEHICLES,
3127
3927
  createVehiclesTestModule,
3128
3928
  loadVehiclesTestModule,
3929
+ createVehicleBody,
3129
3930
  DASHBOARD_LOOK,
3130
3931
  createVehicleDashboard,
3131
3932
  LAMP_RULES,
3132
3933
  createVehicleLamps,
3934
+ MIST_RULES,
3935
+ createWindshieldMist,
3936
+ createWheelRoll,
3133
3937
  PROVING_TEXT,
3134
3938
  PROVING_AREA_ID,
3135
3939
  createProvingModule,
3136
3940
  loadProvingModule
3137
3941
  };
3138
- //# sourceMappingURL=chunk-MRUPKE7J.js.map
3942
+ //# sourceMappingURL=chunk-NPS4X64L.js.map