@adatechnology/game-vehicles-three 0.7.0 → 0.8.1

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"),
@@ -1273,6 +1327,8 @@ var VEHICLE_SOUNDS = [
1273
1327
  ];
1274
1328
  var CAR_RECORDED_RPM = { idle: 1e3, high: 2520 };
1275
1329
  var CAR_START_RUN_STOP = {
1330
+ /** The take the times below were measured on. */
1331
+ take: "_01.",
1276
1332
  /** The starter and the engine catching (at 0.58 s) and flaring; the loops take over at the crank time of the vehicle. */
1277
1333
  start: { offset: 0, duration: 1.3 },
1278
1334
  /** The ignition off: the engine running down to the end of the file. */
@@ -1292,11 +1348,13 @@ var VEHICLE_SAMPLE_ALIASES = [
1292
1348
  carLayer("engine_car_mid", "engine_loop_car_high", CAR_RECORDED_RPM.high, 1),
1293
1349
  // the same file as the mid layer, read from two seconds in, so the two do not sum in phase where they cross
1294
1350
  carLayer("engine_car_high", "engine_loop_car_high", CAR_RECORDED_RPM.high, 2, 2),
1295
- { id: "engine_start", file: "engine_start_run_stop", ...CAR_START_RUN_STOP.start, falloff: VEHICLE_LOOP_FALLOFF },
1296
- { id: "engine_stop", file: "engine_start_run_stop", ...CAR_START_RUN_STOP.stop, falloff: VEHICLE_LOOP_FALLOFF },
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 },
1297
1354
  { id: "car_door_close", file: "car_door_shut" },
1298
1355
  { id: "car_trunk_close", file: "car_trunk_shut" }
1299
1356
  ];
1357
+ var VEHICLE_PRELOAD_SOUNDS = [...VEHICLE_SAMPLE_ALIASES.map((alias) => alias.id), "car_door_open"];
1300
1358
  var VEHICLE_SOUND_BANK = { id: "vehicles_three", sounds: VEHICLE_SOUNDS, loops: VEHICLE_LOOPS, sampleAliases: VEHICLE_SAMPLE_ALIASES };
1301
1359
 
1302
1360
  // src/test-area/module.ts
@@ -1641,7 +1699,8 @@ function createVehiclesTestModule(options = {}) {
1641
1699
  speed,
1642
1700
  throttle: input.throttle,
1643
1701
  ...semiState ? { rpm: semiState.rpm, slip: Math.max(0, ...semiState.slip.map(Math.abs)) } : { slip: braking && speed > 3 ? 0.6 : 0 },
1644
- 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
1645
1704
  });
1646
1705
  }
1647
1706
  state.exhaust -= dt;
@@ -1817,6 +1876,7 @@ function createVehiclesTestModule(options = {}) {
1817
1876
  if (isRenderPools(lent.pools)) lent.pools.materials.register("vehicles.cracked-glass", () => crackedGlassMaterial(three, new three.DataTexture(new Uint8Array(4), 1, 1)));
1818
1877
  lent.audio?.register?.(LIBRARY_BANK);
1819
1878
  lent.audio?.register?.(VEHICLE_SOUND_BANK);
1879
+ lent.audio?.preload?.(VEHICLE_PRELOAD_SOUNDS);
1820
1880
  return () => {
1821
1881
  stopChange?.();
1822
1882
  stopFrame();
@@ -1851,7 +1911,7 @@ import { discardObject3D as discardObject3D3 } from "@adatechnology/game-shapes-
1851
1911
 
1852
1912
  // src/test-area/proving/cockpit.ts
1853
1913
  import { RADIO_TRACKS } from "@adatechnology/game-audio";
1854
- import { bumperDistance, createSemiSimDriver as createSemiSimDriver3, createVehicleControls, createVehicleSystems, parkingBeepInterval } from "@adatechnology/game-vehicles-driving";
1914
+ import { bumperDistance, createSemiSimDriver as createSemiSimDriver3, createVehicleControls, createVehicleStorage, createVehicleSystems, createWindshieldFog, GLOVEBOX, parkingBeepInterval, trunkFor } from "@adatechnology/game-vehicles-driving";
1855
1915
  import { subjectSpec } from "@adatechnology/game-vehicles-proving";
1856
1916
  import { createVehicleDestructible as createVehicleDestructible3 } from "@adatechnology/game-vehicles";
1857
1917
 
@@ -2200,7 +2260,7 @@ function createVehicleDashboard(THREE, built, options) {
2200
2260
  // src/lamps.ts
2201
2261
  import { isLightBudget, LIGHT_PRIORITY } from "@adatechnology/game-render-three";
2202
2262
  var LAMP_RULES = {
2203
- 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 } },
2204
2264
  low: { color: 16773584, intensity: 2.4, distance: 32, angle: 0.42, penumbra: 0.55, decay: 1.6, drop: 0.09 },
2205
2265
  high: { color: 16774882, intensity: 3.6, distance: 70, angle: 0.3, penumbra: 0.45, decay: 1.4, drop: 0.015 },
2206
2266
  fog: { color: 16773320, intensity: 1.6, distance: 16, angle: 0.75, penumbra: 0.7, decay: 1.8, drop: 0.12 },
@@ -2217,7 +2277,11 @@ function createVehicleLamps(THREE, built, options = {}) {
2217
2277
  if (o.userData?.lamp === "fog" && o.material && !bulbs.includes(o)) bulbs.push(o);
2218
2278
  });
2219
2279
  if (bulbs.length) lamps.push({ kind: "fog", inner: bulbs });
2220
- 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 };
2221
2285
  const originals = /* @__PURE__ */ new Map();
2222
2286
  const copies = /* @__PURE__ */ new Map();
2223
2287
  const base = /* @__PURE__ */ new Map();
@@ -2232,6 +2296,7 @@ function createVehicleLamps(THREE, built, options = {}) {
2232
2296
  mesh.material = copy;
2233
2297
  return copy;
2234
2298
  };
2299
+ for (const b of beacons) own(b.mesh);
2235
2300
  const haloBase = /* @__PURE__ */ new Map();
2236
2301
  for (const l of lamps) {
2237
2302
  for (const m of l.inner) own(m);
@@ -2307,6 +2372,8 @@ function createVehicleLamps(THREE, built, options = {}) {
2307
2372
  else if (l.kind === "fog") glow(l, state.fog ? g.fog.on : g.fog.off);
2308
2373
  }
2309
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);
2310
2377
  const beamId = state.mode === "low" || state.mode === "high" ? state.mode : null;
2311
2378
  for (const id of ["low", "high"]) beam(id, beamId === id, () => spots(front, LAMP_RULES[id]));
2312
2379
  beam("fog", state.fog && has.fog, () => spots(fogAnchors, LAMP_RULES.fog));
@@ -2320,7 +2387,7 @@ function createVehicleLamps(THREE, built, options = {}) {
2320
2387
  return { ...state };
2321
2388
  },
2322
2389
  set(next) {
2323
- 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;
2324
2391
  Object.assign(state, next);
2325
2392
  apply();
2326
2393
  },
@@ -2329,15 +2396,17 @@ function createVehicleLamps(THREE, built, options = {}) {
2329
2396
  sources.clear();
2330
2397
  for (const a of anchors) a.removeFromParent();
2331
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);
2332
2408
  for (const l of lamps) {
2333
- for (const m of l.inner) {
2334
- const copy = m.material;
2335
- const original = originals.get(m);
2336
- if (original) {
2337
- m.material = original;
2338
- if (copy && !Array.isArray(copy)) copy.dispose?.();
2339
- }
2340
- }
2409
+ for (const m of l.inner) giveBack(m);
2341
2410
  for (const h of l.halo ?? []) {
2342
2411
  h.visible = true;
2343
2412
  const s = haloBase.get(h) ?? 1;
@@ -2351,8 +2420,66 @@ function createVehicleLamps(THREE, built, options = {}) {
2351
2420
  };
2352
2421
  }
2353
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
+
2354
2467
  // src/test-area/proving/cockpit.ts
2355
- 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", beep: "parking_beep" };
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
+ };
2356
2483
  var BODY_KEYS = {
2357
2484
  Digit1: "door_front_left",
2358
2485
  Digit2: "door_front_right",
@@ -2370,6 +2497,8 @@ function createCockpit(o) {
2370
2497
  const driver = createSemiSimDriver3(spec);
2371
2498
  driver.place(o.place[0], o.place[1], o.place[2]);
2372
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();
2373
2502
  const audio = o.host.audio;
2374
2503
  const top = o.subject.stats.maxSpeedKmh;
2375
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;
@@ -2383,11 +2512,25 @@ function createCockpit(o) {
2383
2512
  let beepAgain = 0;
2384
2513
  let bar = 0;
2385
2514
  let exitAsked = false;
2515
+ let sirenAgain = 0;
2516
+ let sparkAgain = 0;
2517
+ let wiresBefore = 0;
2386
2518
  let gas = 0;
2387
2519
  const systems = createVehicleSystems();
2388
2520
  const context = o.built.context;
2389
2521
  const destructible = o.kit && context ? createVehicleDestructible3(o.kit, context, { id: "driven", frame: o.built.group }) : null;
2390
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
+ });
2391
2534
  const bodyEvents = (events) => {
2392
2535
  for (const event of events) {
2393
2536
  if (event.type === "sound") play(event.key, 0.9);
@@ -2403,6 +2546,8 @@ function createCockpit(o) {
2403
2546
  fogLamps: lamps.has.fog,
2404
2547
  clutchPedal: true,
2405
2548
  radioTracks: RADIO_TRACKS.length,
2549
+ hasKey: o.hasKey,
2550
+ emergency: lamps.has.beacon,
2406
2551
  onEvent: (event) => {
2407
2552
  if (event.type === "command") {
2408
2553
  if (event.command === "ignition" && !event.state.ignition) driver.state.engineOn = false;
@@ -2413,6 +2558,8 @@ function createCockpit(o) {
2413
2558
  bar = 0;
2414
2559
  }
2415
2560
  if (event.command === "exit") exitAsked = true;
2561
+ if (event.command === "siren") sirenAgain = 0;
2562
+ if (event.command === "glovebox") play(sounds.glovebox, 0.8);
2416
2563
  }
2417
2564
  o.onEvent?.(event);
2418
2565
  }
@@ -2446,6 +2593,12 @@ function createCockpit(o) {
2446
2593
  target?.addEventListener("blur", blur);
2447
2594
  const cockpit = {
2448
2595
  radiator: 1,
2596
+ storage,
2597
+ access,
2598
+ fog,
2599
+ mist,
2600
+ weather: { ...o.weather ?? { outsideC: 25, rain: 0 } },
2601
+ emergency: lamps.has.beacon,
2449
2602
  driver,
2450
2603
  controls,
2451
2604
  systems,
@@ -2474,8 +2627,24 @@ function createCockpit(o) {
2474
2627
  brake: s.brakeLights,
2475
2628
  reverse: s.reverseLights,
2476
2629
  left: s.blinkOn && (s.indicator === "left" || s.indicator === "hazard"),
2477
- 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
2478
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
+ }
2479
2648
  dashboard?.update(
2480
2649
  {
2481
2650
  kmh: Math.abs(driver.state.u) * 3.6,
@@ -2551,6 +2720,8 @@ function createCockpit(o) {
2551
2720
  target?.removeEventListener("blur", blur);
2552
2721
  controls.release();
2553
2722
  lamps.dispose();
2723
+ mist.dispose();
2724
+ storage.clear();
2554
2725
  dashboard?.dispose();
2555
2726
  body?.dispose();
2556
2727
  destructible?.dispose?.();
@@ -2559,6 +2730,33 @@ function createCockpit(o) {
2559
2730
  return cockpit;
2560
2731
  }
2561
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
+
2562
2760
  // src/wheel-roll.ts
2563
2761
  var DEFAULT_RADIUS = 0.32;
2564
2762
  function createWheelRoll(THREE, built) {
@@ -2630,7 +2828,7 @@ function createProvingCrash(host, occupant) {
2630
2828
  // src/test-area/proving/systems-text.ts
2631
2829
  var SYSTEMS_TEXT = {
2632
2830
  "pt-BR": {
2633
- 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",
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",
2634
2832
  profile: "Modo de condu\xE7\xE3o",
2635
2833
  profiles: { eco: "econ\xF4mico", normal: "normal", sport: "esporte" },
2636
2834
  traction: "Controle de tra\xE7\xE3o",
@@ -2640,7 +2838,7 @@ var SYSTEMS_TEXT = {
2640
2838
  climate: "Ar-condicionado",
2641
2839
  demist: "Desemba\xE7ador",
2642
2840
  wiperModes: { off: "desligado", intermittent: "intermitente", slow: "lento", fast: "r\xE1pido" },
2643
- 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" },
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" },
2644
2842
  events: {
2645
2843
  stall: "O motor morreu",
2646
2844
  out_of_fuel: "Acabou o combust\xEDvel",
@@ -2651,9 +2849,20 @@ var SYSTEMS_TEXT = {
2651
2849
  seized: "O motor fundiu de tanto esquentar",
2652
2850
  spare_fitted: "Estepe colocado",
2653
2851
  no_spare: "N\xE3o h\xE1 estepe",
2654
- cruise_cancelled: "Piloto autom\xE1tico desligado"
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"
2655
2855
  },
2656
- 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)" },
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",
2657
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" },
2658
2867
  track: "faixa {0}",
2659
2868
  adjust: { seat_forward: "Banco: frente/tr\xE1s (m)", seat_height: "Banco: altura (m)", mirror_left: "Retrovisor esquerdo: \xE2ngulo", mirror_right: "Retrovisor direito: \xE2ngulo" },
@@ -2663,7 +2872,7 @@ var SYSTEMS_TEXT = {
2663
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" }
2664
2873
  },
2665
2874
  en: {
2666
- 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",
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",
2667
2876
  profile: "Drive profile",
2668
2877
  profiles: { eco: "eco", normal: "normal", sport: "sport" },
2669
2878
  traction: "Traction control",
@@ -2673,7 +2882,7 @@ var SYSTEMS_TEXT = {
2673
2882
  climate: "Air conditioning",
2674
2883
  demist: "Demister",
2675
2884
  wiperModes: { off: "off", intermittent: "intermittent", slow: "slow", fast: "fast" },
2676
- 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" },
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" },
2677
2886
  events: {
2678
2887
  stall: "The engine stalled",
2679
2888
  out_of_fuel: "Out of fuel",
@@ -2684,9 +2893,20 @@ var SYSTEMS_TEXT = {
2684
2893
  seized: "The engine seized from the heat",
2685
2894
  spare_fitted: "Spare tyre fitted",
2686
2895
  no_spare: "No spare tyre left",
2687
- cruise_cancelled: "Cruise control off"
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"
2688
2899
  },
2689
- 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)" },
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",
2690
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" },
2691
2911
  track: "track {0}",
2692
2912
  adjust: { seat_forward: "Seat: forward/back (m)", seat_height: "Seat: height (m)", mirror_left: "Left mirror: angle", mirror_right: "Right mirror: angle" },
@@ -2872,6 +3092,9 @@ function createProvingModule(options) {
2872
3092
  return turn ? { ...pose2, look: [pose2.look[0] + turn, pose2.look[1], pose2.look[2]] } : pose2;
2873
3093
  };
2874
3094
  let gearbox = "automatic";
3095
+ let hasKey = true;
3096
+ let weather = { outsideC: 25, rain: 0 };
3097
+ let misted = false;
2875
3098
  let lastGear = null;
2876
3099
  let driveClock = 0;
2877
3100
  let queue = [];
@@ -3191,7 +3414,8 @@ function createProvingModule(options) {
3191
3414
  run = null;
3192
3415
  queue = [];
3193
3416
  parking = null;
3194
- setSurface("dry_asphalt");
3417
+ setSurface(weather.rain > 0 ? "wet_asphalt" : "dry_asphalt");
3418
+ misted = false;
3195
3419
  driving = createCockpit({
3196
3420
  THREE: three,
3197
3421
  host,
@@ -3204,8 +3428,16 @@ function createProvingModule(options) {
3204
3428
  needle: kitFor(three).materials?.amber,
3205
3429
  // the engine's own sound turns the key (starter, catch, idle by RPM)
3206
3430
  sounds: { starter: "" },
3431
+ hasKey,
3432
+ weather,
3433
+ stock: (compartment) => options.stock?.(compartment, settings.vehicle) ?? [],
3207
3434
  onExit: () => stopDriving(),
3208
3435
  onEvent: (event) => {
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
+ }
3209
3441
  if (event.type === "refused") return host?.log(text.cabin.refused[event.reason] ?? sysText.refused[event.reason] ?? event.reason);
3210
3442
  if (event.type === "sound") return;
3211
3443
  if (event.type !== "command") {
@@ -3287,11 +3519,41 @@ function createProvingModule(options) {
3287
3519
  return `${sysText.demist}: ${onOff(st.climate.demist)}`;
3288
3520
  case "seatbelt":
3289
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"]}`;
3290
3528
  default:
3291
3529
  return null;
3292
3530
  }
3293
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
+ };
3294
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
+ }
3295
3557
  if (!driving) startDriving();
3296
3558
  const cockpit = driving;
3297
3559
  if (!cockpit) return;
@@ -3357,6 +3619,23 @@ function createProvingModule(options) {
3357
3619
  host.log(placed ? sysText.parking.placed : sysText.parking.none, { vehicle: settings.vehicle }, "proving_system");
3358
3620
  return;
3359
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
+ }
3360
3639
  case "force_door": {
3361
3640
  const body = cockpit.body;
3362
3641
  if (!body) return;
@@ -3463,6 +3742,13 @@ function createProvingModule(options) {
3463
3742
  const d = driving;
3464
3743
  d.step(dt);
3465
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
+ }
3466
3752
  pose(d.driver.state, d.driver.state.steerAngle);
3467
3753
  speed = d.driver.state.u;
3468
3754
  steer = d.driver.state.steerAngle;
@@ -3558,6 +3844,7 @@ function createProvingModule(options) {
3558
3844
  { id: "gear", name: text.metrics.gear, read: () => driving?.driver.state.gear ?? run?.driver.state.gear ?? 0 },
3559
3845
  { id: "rpm", name: text.metrics.rpm, unit: "rpm", digits: 0, read: () => driving?.driver.state.rpm ?? run?.driver.state.rpm ?? 0 },
3560
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 },
3561
3848
  { id: "value", name: text.metrics.value, digits: 1, read: () => run ? run.trace[run.trace.length - 1]?.[1] ?? 0 : 0 },
3562
3849
  { id: "passed", name: text.metrics.passed, read: () => counts("pass") },
3563
3850
  { id: "failed", name: text.metrics.failed, read: () => counts("fail") },
@@ -3623,7 +3910,9 @@ export {
3623
3910
  CAR_RECORDED_RPM,
3624
3911
  CAR_START_RUN_STOP,
3625
3912
  VEHICLE_SAMPLE_ALIASES,
3913
+ VEHICLE_PRELOAD_SOUNDS,
3626
3914
  VEHICLE_SOUND_BANK,
3915
+ createEngineBudget,
3627
3916
  createVehicleSound,
3628
3917
  VEHICLE_LOD_DISTANCE,
3629
3918
  createVehicleLod,
@@ -3642,10 +3931,12 @@ export {
3642
3931
  createVehicleDashboard,
3643
3932
  LAMP_RULES,
3644
3933
  createVehicleLamps,
3934
+ MIST_RULES,
3935
+ createWindshieldMist,
3645
3936
  createWheelRoll,
3646
3937
  PROVING_TEXT,
3647
3938
  PROVING_AREA_ID,
3648
3939
  createProvingModule,
3649
3940
  loadProvingModule
3650
3941
  };
3651
- //# sourceMappingURL=chunk-L63R2JQA.js.map
3942
+ //# sourceMappingURL=chunk-NPS4X64L.js.map