@adatechnology/game-vehicles-three 0.6.3 → 0.7.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.
@@ -1271,7 +1271,33 @@ var VEHICLE_SOUNDS = [
1271
1271
  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
1272
  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
1273
  ];
1274
- var VEHICLE_SOUND_BANK = { id: "vehicles_three", sounds: VEHICLE_SOUNDS, loops: VEHICLE_LOOPS };
1274
+ var CAR_RECORDED_RPM = { idle: 1e3, high: 2520 };
1275
+ var CAR_START_RUN_STOP = {
1276
+ /** The starter and the engine catching (at 0.58 s) and flaring; the loops take over at the crank time of the vehicle. */
1277
+ start: { offset: 0, duration: 1.3 },
1278
+ /** The ignition off: the engine running down to the end of the file. */
1279
+ stop: { offset: 9.96, duration: 0.64 }
1280
+ };
1281
+ var carLayer = (id, file, rpm, harmonic, offset) => ({
1282
+ id,
1283
+ file,
1284
+ loop: true,
1285
+ // the layer's oscillator plays the firing frequency times its harmonic: the file sounds at that frequency for the RPM it was recorded at
1286
+ baseFrequency: firingHz(CAR_ENGINE, rpm) * harmonic,
1287
+ falloff: VEHICLE_LOOP_FALLOFF,
1288
+ ...offset === void 0 ? {} : { offset }
1289
+ });
1290
+ var VEHICLE_SAMPLE_ALIASES = [
1291
+ carLayer("engine_car_low", "engine_loop_car_idle", CAR_RECORDED_RPM.idle, 1),
1292
+ carLayer("engine_car_mid", "engine_loop_car_high", CAR_RECORDED_RPM.high, 1),
1293
+ // the same file as the mid layer, read from two seconds in, so the two do not sum in phase where they cross
1294
+ 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 },
1297
+ { id: "car_door_close", file: "car_door_shut" },
1298
+ { id: "car_trunk_close", file: "car_trunk_shut" }
1299
+ ];
1300
+ var VEHICLE_SOUND_BANK = { id: "vehicles_three", sounds: VEHICLE_SOUNDS, loops: VEHICLE_LOOPS, sampleAliases: VEHICLE_SAMPLE_ALIASES };
1275
1301
 
1276
1302
  // src/test-area/module.ts
1277
1303
  var VEHICLES_AREA_ID = "vehicles_area";
@@ -1280,6 +1306,7 @@ var ITEM_OF = { compact_hatch: "hatch", midsize_sedan: "sedan", midsize_pickup:
1280
1306
  var itemId = (key) => ITEM_OF[key] ?? key;
1281
1307
  var LANE = { z: 11, from: -12, wall: 13 };
1282
1308
  var SPARK_EVERY_S = 0.08;
1309
+ var TYRE_DAMAGE = /* @__PURE__ */ new Set(["flat_tyre", "torn_tyre", "run_flat"]);
1283
1310
  var LEAF_REST_RADIANS = 0.02;
1284
1311
  var MATERIAL_COLOURS = { metal: 13421772, glass: 4500223, rubber: 16750848, fuel: 16724736 };
1285
1312
  var LANE_START_S = 1.2;
@@ -1575,7 +1602,8 @@ function createVehiclesTestModule(options = {}) {
1575
1602
  state.time += dt;
1576
1603
  const braking = settings.damage !== "into_wall" && gap() < brakeAt() && state.time > 0.5;
1577
1604
  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 };
1605
+ const straight = settings.damage === "into_wall" || TYRE_DAMAGE.has(settings.damage) || !moving;
1606
+ const input = { ...idle, throttle: braking || !moving ? 0 : 1, brake: braking ? 1 : 0, steer: straight ? 0 : Math.sin(state.time * 1.4) * 0.15 };
1579
1607
  if (mode === "arcade") {
1580
1608
  const { events } = arcade.step(input, dt, { blocked: (x, z, yaw) => vehicleCorners2(arcade.spec, x, z, yaw).some(([px]) => px >= LANE.wall) });
1581
1609
  for (const e of events) {
@@ -1814,16 +1842,139 @@ function loadVehiclesTestModule(room, options = {}) {
1814
1842
  }
1815
1843
 
1816
1844
  // src/test-area/proving/module.ts
1817
- import { createVehicleCamera, mirrorPoses } from "@adatechnology/game-camera";
1845
+ import { createVehicleCamera, defaultEye, mirrorPoses } from "@adatechnology/game-camera";
1818
1846
  import { markKitOrigin as markKitOrigin3 } from "@adatechnology/game-contracts";
1819
- import { CABIN_BANK, LIBRARY_BANK as LIBRARY_BANK2 } from "@adatechnology/game-audio";
1847
+ import { CABIN_BANK, LIBRARY_BANK as LIBRARY_BANK2, RADIO_BANK } from "@adatechnology/game-audio";
1820
1848
  import { createSemiSimDriver as createSemiSimDriver4 } from "@adatechnology/game-vehicles-driving";
1821
1849
  import { certify, createProvingTests, startRun, statusOf, subjectSpec as subjectSpec2, withResult } from "@adatechnology/game-vehicles-proving";
1822
1850
  import { discardObject3D as discardObject3D3 } from "@adatechnology/game-shapes-three";
1823
1851
 
1824
1852
  // src/test-area/proving/cockpit.ts
1825
- import { createSemiSimDriver as createSemiSimDriver3, createVehicleControls } from "@adatechnology/game-vehicles-driving";
1853
+ import { RADIO_TRACKS } from "@adatechnology/game-audio";
1854
+ import { bumperDistance, createSemiSimDriver as createSemiSimDriver3, createVehicleControls, createVehicleSystems, parkingBeepInterval } from "@adatechnology/game-vehicles-driving";
1826
1855
  import { subjectSpec } from "@adatechnology/game-vehicles-proving";
1856
+ import { createVehicleDestructible as createVehicleDestructible3 } from "@adatechnology/game-vehicles";
1857
+
1858
+ // src/body.ts
1859
+ var OPEN_SOUND = "car_door_open";
1860
+ var SHUT_SOUND = { door: "car_door_close", hood: "car_hood_close", lid: "car_trunk_close" };
1861
+ var clamp = (x, a, b) => Math.max(a, Math.min(b, x));
1862
+ function createVehicleBody(built, options) {
1863
+ const swing = options.swingSeconds ?? 1.2;
1864
+ const windowSeconds = options.windowSeconds ?? 3.5;
1865
+ const travel = options.windowTravel ?? 0.92;
1866
+ const root = built.context?.vehicle ?? built.group;
1867
+ const poses = /* @__PURE__ */ new Map();
1868
+ for (const clip of options.clips) {
1869
+ if (!clip.id.startsWith("open_")) continue;
1870
+ const last = clip.keys[clip.keys.length - 1];
1871
+ for (const [id, pose] of Object.entries(last?.parts ?? {})) if (pose.rotation) poses.set(id, pose.rotation);
1872
+ }
1873
+ const hinges = /* @__PURE__ */ new Map();
1874
+ for (const [id, open] of poses) {
1875
+ const pivot = root.getObjectByName(id);
1876
+ if (pivot) hinges.set(id, { pivot, open, now: 0, target: 0 });
1877
+ }
1878
+ const kind = (part) => part.startsWith("door") ? "door" : part === "hood" ? "hood" : "lid";
1879
+ const panes = /* @__PURE__ */ new Map();
1880
+ for (const glass of built.context?.glass ?? []) {
1881
+ const g = glass.userData.glass;
1882
+ if (!g || g.key !== "window_front" && g.key !== "window_rear") continue;
1883
+ const id = `${g.key === "window_front" ? "front" : "rear"}_${(g.side ?? 1) < 0 ? "left" : "right"}`;
1884
+ if (panes.has(id) || !glass.geometry) continue;
1885
+ glass.geometry.computeBoundingBox();
1886
+ const box = glass.geometry.boundingBox;
1887
+ if (box) panes.set(id, { pane: glass, drop: (box.max.y - box.min.y) * travel, now: 0, target: 0, applied: 0 });
1888
+ }
1889
+ const sunroofs = [];
1890
+ const wipers = [];
1891
+ const T = options.THREE;
1892
+ const turn = new T.Quaternion();
1893
+ root.traverse((o) => {
1894
+ const sun = o.userData.sunroof;
1895
+ if (sun) sunroofs.push({ obj: o, slide: sun.slide, base: o.position.x });
1896
+ });
1897
+ for (const { part, tag } of built.context?.wipers ?? [])
1898
+ 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() });
1899
+ let sunNow = 0;
1900
+ let sunTarget = 0;
1901
+ let wipePhase = 0;
1902
+ let locked = false;
1903
+ const placeWiper = (w, angle) => {
1904
+ if (angle === 0) {
1905
+ w.obj.position.copy(w.rest);
1906
+ w.obj.quaternion.copy(w.restQ);
1907
+ return;
1908
+ }
1909
+ turn.setFromAxisAngle(w.axis, angle);
1910
+ w.obj.position.copy(w.rest).sub(w.spindle).applyQuaternion(turn).add(w.spindle);
1911
+ w.obj.quaternion.copy(turn).multiply(w.restQ);
1912
+ };
1913
+ const ease = (now, target, seconds, dt) => now + clamp(target - now, -dt / seconds, dt / seconds);
1914
+ return {
1915
+ parts: [...hinges.keys()],
1916
+ windows: [...panes.keys()],
1917
+ hasSunroof: sunroofs.length > 0,
1918
+ wipers: new Set(wipers.map((w) => w.arm)).size,
1919
+ get locked() {
1920
+ return locked;
1921
+ },
1922
+ openness: (part) => hinges.get(part)?.now ?? 0,
1923
+ toggle(part) {
1924
+ const h = hinges.get(part);
1925
+ if (!h) return [{ type: "refused", part, reason: "missing" }];
1926
+ if (locked && kind(part) !== "hood" && h.target === 0) return [{ type: "refused", part, reason: "locked" }];
1927
+ h.target = h.target > 0 ? 0 : 1;
1928
+ return h.target > 0 ? [{ type: "sound", key: OPEN_SOUND, part }] : [];
1929
+ },
1930
+ window: (id) => panes.get(id)?.now ?? 0,
1931
+ toggleWindow(id) {
1932
+ const w = panes.get(id);
1933
+ if (w) w.target = w.target > 0.5 ? 0 : 1;
1934
+ },
1935
+ setWindow(id, open) {
1936
+ const w = panes.get(id);
1937
+ if (w) w.target = clamp(open, 0, 1);
1938
+ },
1939
+ toggleSunroof() {
1940
+ sunTarget = sunTarget > 0.5 ? 0 : 1;
1941
+ },
1942
+ lock(on) {
1943
+ locked = on;
1944
+ return [{ type: "sound", key: "blinker_tick", part: "doors" }];
1945
+ },
1946
+ update(dt, wipe = 0) {
1947
+ const events = [];
1948
+ for (const [id, h] of hinges) {
1949
+ if (h.now === h.target) continue;
1950
+ h.now = ease(h.now, h.target, swing, dt);
1951
+ h.pivot.rotation.set(h.open[0] * h.now, h.open[1] * h.now, h.open[2] * h.now);
1952
+ if (h.now === 0) events.push({ type: "sound", key: SHUT_SOUND[kind(id)] ?? "car_door_close", part: id });
1953
+ }
1954
+ for (const w of panes.values()) {
1955
+ if (w.now === w.target) continue;
1956
+ w.now = ease(w.now, w.target, windowSeconds, dt);
1957
+ const offset = -w.drop * w.now;
1958
+ w.pane.position.y += offset - w.applied;
1959
+ w.applied = offset;
1960
+ }
1961
+ if (sunNow !== sunTarget) {
1962
+ sunNow = ease(sunNow, sunTarget, 2.5, dt);
1963
+ for (const s of sunroofs) s.obj.position.x = s.base - s.slide * sunNow;
1964
+ }
1965
+ wipePhase = wipe > 0 ? (wipePhase + dt * wipe) % 1 : wipePhase > 0 ? Math.min(1, wipePhase + dt * 1.5) % 1 : 0;
1966
+ const sweep = Math.sin(Math.PI * wipePhase);
1967
+ for (const w of wipers) placeWiper(w, w.sweep * sweep);
1968
+ return events;
1969
+ },
1970
+ dispose() {
1971
+ for (const h of hinges.values()) h.pivot.rotation.set(0, 0, 0);
1972
+ for (const w of panes.values()) w.pane.position.y -= w.applied;
1973
+ for (const s of sunroofs) s.obj.position.x = s.base;
1974
+ for (const w of wipers) placeWiper(w, 0);
1975
+ }
1976
+ };
1977
+ }
1827
1978
 
1828
1979
  // src/dashboard.ts
1829
1980
  import { discardObject3D as discardObject3D2 } from "@adatechnology/game-shapes-three";
@@ -2000,6 +2151,8 @@ function createVehicleDashboard(THREE, built, options) {
2000
2151
  lamp(g, 104, 214, r.highBeam, DASHBOARD_LOOK.blue, beam);
2001
2152
  lamp(g, 152, 214, r.lowBeam || r.fog, r.fog ? DASHBOARD_LOOK.amber : DASHBOARD_LOOK.green, beam);
2002
2153
  bar(g, 40, r.fuel, false, "F");
2154
+ lamp(g, 104, 244, !!r.cruise, DASHBOARD_LOOK.green, letter("CRZ"));
2155
+ 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
2156
  } else {
2004
2157
  dial(g, r.rpm / 1e3, Math.ceil(redline / 1e3) + 1, 1, redline / 1e3, (v) => String(Math.round(v)));
2005
2158
  g.fillStyle = DASHBOARD_LOOK.marks;
@@ -2008,7 +2161,9 @@ function createVehicleDashboard(THREE, built, options) {
2008
2161
  g.fillText(r.gear < 0 ? "R" : r.gear === 0 ? "N" : String(r.gear), SIZE / 2, SIZE / 2 + 54);
2009
2162
  lamp(g, 86, 178, r.parkingBrake, DASHBOARD_LOOK.red, ring("P"));
2010
2163
  lamp(g, 128, 214, r.ignition && !r.seatbelt, DASHBOARD_LOOK.red, belt);
2011
- lamp(g, 170, 178, warn, DASHBOARD_LOOK.red, letter("+\u2212"));
2164
+ lamp(g, 170, 178, warn || r.engineOn && r.charging === false, DASHBOARD_LOOK.red, letter("+\u2212"));
2165
+ lamp(g, 104, 244, r.ignition && !!r.tractionOff, DASHBOARD_LOOK.amber, letter("TC"));
2166
+ lamp(g, 152, 244, r.ignition && !!r.absOff, DASHBOARD_LOOK.amber, letter("ABS"));
2012
2167
  lamp(g, 86, 214, warn, DASHBOARD_LOOK.red, letter("OIL"));
2013
2168
  lamp(g, 170, 214, r.fourWheel, DASHBOARD_LOOK.green, letter("4x4"));
2014
2169
  bar(g, SIZE - 40, r.temperature, true, "T");
@@ -2197,7 +2352,18 @@ function createVehicleLamps(THREE, built, options = {}) {
2197
2352
  }
2198
2353
 
2199
2354
  // 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" };
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" };
2356
+ var BODY_KEYS = {
2357
+ Digit1: "door_front_left",
2358
+ Digit2: "door_front_right",
2359
+ Digit3: "door_rear_left",
2360
+ Digit4: "door_rear_right",
2361
+ Digit5: "hood",
2362
+ Digit6: "lid"
2363
+ };
2364
+ var WINDOW_KEYS = { Digit1: "front_left", Digit2: "front_right", Digit3: "rear_left", Digit4: "rear_right" };
2365
+ var LOCK_KEY = "KeyV";
2366
+ var WIPE_RATE = { off: 0, intermittent: 0.25, slow: 0.7, fast: 1.4 };
2201
2367
  function createCockpit(o) {
2202
2368
  const sounds = { ...DEFAULT_COCKPIT_SOUNDS, ...o.sounds };
2203
2369
  const spec = subjectSpec(o.subject);
@@ -2214,19 +2380,38 @@ function createCockpit(o) {
2214
2380
  let wipeAgain = 0;
2215
2381
  let chimeAgain = 0;
2216
2382
  let radioAgain = 0;
2383
+ let beepAgain = 0;
2384
+ let bar = 0;
2217
2385
  let exitAsked = false;
2218
2386
  let gas = 0;
2387
+ const systems = createVehicleSystems();
2388
+ const context = o.built.context;
2389
+ const destructible = o.kit && context ? createVehicleDestructible3(o.kit, context, { id: "driven", frame: o.built.group }) : null;
2390
+ const body = destructible ? createVehicleBody(o.built, { THREE: o.THREE, clips: destructible.animations() }) : null;
2391
+ const bodyEvents = (events) => {
2392
+ for (const event of events) {
2393
+ if (event.type === "sound") play(event.key, 0.9);
2394
+ o.onEvent?.(event);
2395
+ }
2396
+ };
2219
2397
  const controls = createVehicleControls({
2398
+ canCrank: systems.canCrank,
2399
+ onCrank: systems.crank,
2400
+ powerLimit: systems.powerLimit,
2220
2401
  gears: spec.gears?.length ?? 5,
2221
2402
  fourWheelDrive: !!o.subject.body.fourWheelDrive,
2222
2403
  fogLamps: lamps.has.fog,
2404
+ clutchPedal: true,
2405
+ radioTracks: RADIO_TRACKS.length,
2223
2406
  onEvent: (event) => {
2224
2407
  if (event.type === "command") {
2225
- if (event.command === "ignition" && event.state.ignition && sounds.starter) play(sounds.starter);
2226
2408
  if (event.command === "ignition" && !event.state.ignition) driver.state.engineOn = false;
2227
2409
  if (event.command === "alarm" && event.state.alarm === "armed") play(sounds.chirp);
2228
2410
  if (event.command === "alarm") alarmAgain = 0;
2229
- if (event.command === "radio" || event.command === "radio_source") radioAgain = 0;
2411
+ if (event.command === "radio" || event.command === "radio_source" || event.command === "radio_track") {
2412
+ radioAgain = 0;
2413
+ bar = 0;
2414
+ }
2230
2415
  if (event.command === "exit") exitAsked = true;
2231
2416
  }
2232
2417
  o.onEvent?.(event);
@@ -2234,7 +2419,20 @@ function createCockpit(o) {
2234
2419
  });
2235
2420
  driver.state.engineOn = false;
2236
2421
  const target = typeof window !== "undefined" ? window : null;
2422
+ const bodyKey = (e) => {
2423
+ if (!body) return false;
2424
+ if (e.code === LOCK_KEY) bodyEvents(body.lock(!body.locked));
2425
+ else if (e.shiftKey && e.code === "Digit5") body.toggleSunroof();
2426
+ else if (e.shiftKey && WINDOW_KEYS[e.code]) body.toggleWindow(WINDOW_KEYS[e.code]);
2427
+ else if (!e.shiftKey && BODY_KEYS[e.code]) bodyEvents(body.toggle(BODY_KEYS[e.code]));
2428
+ else return false;
2429
+ return true;
2430
+ };
2237
2431
  const down = (e) => {
2432
+ if (!e.repeat && bodyKey(e)) {
2433
+ e.stopImmediatePropagation();
2434
+ return;
2435
+ }
2238
2436
  if (!controls.key(e.code, true)) return;
2239
2437
  e.stopImmediatePropagation();
2240
2438
  if (e.code.startsWith("Arrow") || e.code === "Space") e.preventDefault();
@@ -2246,9 +2444,13 @@ function createCockpit(o) {
2246
2444
  target?.addEventListener("keydown", down, true);
2247
2445
  target?.addEventListener("keyup", up, true);
2248
2446
  target?.addEventListener("blur", blur);
2249
- return {
2447
+ const cockpit = {
2448
+ radiator: 1,
2250
2449
  driver,
2251
2450
  controls,
2451
+ systems,
2452
+ body,
2453
+ destructible,
2252
2454
  lamps,
2253
2455
  dashboard,
2254
2456
  get throttle() {
@@ -2257,11 +2459,16 @@ function createCockpit(o) {
2257
2459
  step(dt) {
2258
2460
  const input = controls.input(dt, driver);
2259
2461
  gas = input.throttle;
2260
- driver.step(input, dt);
2462
+ if (input.start && sounds.starter) play(sounds.starter);
2463
+ const engine = driver.step(input, dt).events;
2261
2464
  controls.after(driver, dt);
2262
2465
  const s = controls.state;
2466
+ const report = [...engine.filter((e) => e.type !== "shift"), ...systems.step({ driver, controls: s, throttle: input.throttle, dt, radiator: cockpit.radiator })];
2467
+ for (const event of report) o.onEvent?.(event);
2468
+ const powered = driver.state.engineOn || systems.state.charge > 0;
2469
+ const high = s.lamps.mode === "high" || s.lamps.flash;
2263
2470
  lamps.set({
2264
- mode: s.lamps.mode,
2471
+ mode: !powered ? "off" : s.lamps.flash ? "high" : s.lamps.mode,
2265
2472
  fog: s.lamps.fog,
2266
2473
  interior: s.lamps.interior,
2267
2474
  brake: s.brakeLights,
@@ -2278,7 +2485,14 @@ function createCockpit(o) {
2278
2485
  engineOn: driver.state.engineOn,
2279
2486
  left: s.blinkOn && (s.indicator === "left" || s.indicator === "hazard"),
2280
2487
  right: s.blinkOn && (s.indicator === "right" || s.indicator === "hazard"),
2281
- highBeam: s.lamps.mode === "high",
2488
+ highBeam: high,
2489
+ fuel: driver.state.fuel / spec.tank.capacity,
2490
+ temperature: systems.state.temperature,
2491
+ cruise: s.cruise.on,
2492
+ profile: s.profile,
2493
+ tractionOff: !s.tractionControl,
2494
+ absOff: !s.abs,
2495
+ charging: systems.state.netW >= 0,
2282
2496
  lowBeam: s.lamps.mode === "low" || s.lamps.mode === "position",
2283
2497
  fog: s.lamps.fog,
2284
2498
  parkingBrake: s.parkingBrake,
@@ -2287,7 +2501,19 @@ function createCockpit(o) {
2287
2501
  },
2288
2502
  dt
2289
2503
  );
2504
+ if (body) bodyEvents(body.update(dt, s.washer ? 1.2 : WIPE_RATE[s.wipers] ?? 0));
2290
2505
  if (s.tick) play(sounds.blinker, 0.8);
2506
+ const near = o.obstacles?.() ?? [];
2507
+ if (s.ignition && near.length && Math.abs(driver.state.u) < 3) {
2508
+ const g = o.built.group;
2509
+ const reversing = driver.state.gear < 0 || driver.state.u < -0.05;
2510
+ 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 });
2511
+ const every = parkingBeepInterval(distance);
2512
+ if (every != null && (beepAgain -= dt) <= 0) {
2513
+ play(sounds.beep, 0.7);
2514
+ beepAgain = every === 0 ? 0.09 : every;
2515
+ }
2516
+ }
2291
2517
  if (s.horn && (hornAgain -= dt) <= 0) {
2292
2518
  play(sounds.horn);
2293
2519
  hornAgain = 0.5;
@@ -2296,9 +2522,9 @@ function createCockpit(o) {
2296
2522
  play(sounds.alarm);
2297
2523
  alarmAgain = 2.9;
2298
2524
  }
2299
- if (s.wipers !== "off" && (wipeAgain -= dt) <= 0) {
2525
+ if ((s.wipers !== "off" || s.washer) && (wipeAgain -= dt) <= 0) {
2300
2526
  play(sounds.wiper, 0.7);
2301
- wipeAgain = s.wipers === "fast" ? 0.6 : 1.4;
2527
+ wipeAgain = s.washer || s.wipers === "fast" ? 0.6 : s.wipers === "slow" ? 1.4 : 4;
2302
2528
  }
2303
2529
  const kmh = Math.abs(driver.state.u) * 3.6;
2304
2530
  if (!s.seatbelt && s.ignition && kmh > 10 && (chimeAgain -= dt) <= 0) {
@@ -2306,8 +2532,13 @@ function createCockpit(o) {
2306
2532
  chimeAgain = 6;
2307
2533
  }
2308
2534
  if (s.radio.on && (radioAgain -= dt) <= 0) {
2309
- play(sounds.radio, s.radio.source === "tape" ? 0.25 : 0.45);
2310
- radioAgain = 0.55;
2535
+ const song = RADIO_TRACKS[s.radio.track % RADIO_TRACKS.length];
2536
+ if (song) {
2537
+ play(song.bars[bar % song.bars.length], s.radio.source === "tape" ? 0.8 : 0.7);
2538
+ bar++;
2539
+ }
2540
+ if (s.radio.source === "radio") play(sounds.radio, 0.12);
2541
+ radioAgain = song?.barSeconds ?? 2;
2311
2542
  }
2312
2543
  if (exitAsked) {
2313
2544
  exitAsked = false;
@@ -2321,10 +2552,151 @@ function createCockpit(o) {
2321
2552
  controls.release();
2322
2553
  lamps.dispose();
2323
2554
  dashboard?.dispose();
2555
+ body?.dispose();
2556
+ destructible?.dispose?.();
2557
+ }
2558
+ };
2559
+ return cockpit;
2560
+ }
2561
+
2562
+ // src/wheel-roll.ts
2563
+ var DEFAULT_RADIUS = 0.32;
2564
+ function createWheelRoll(THREE, built) {
2565
+ const wheels = (built.context?.wheels ?? []).map((wheel) => ({
2566
+ wheel,
2567
+ rest: wheel.quaternion.clone(),
2568
+ radius: Number(wheel.userData.r) || DEFAULT_RADIUS,
2569
+ // in the kit's frame the front is +x: the wheels ahead of the middle steer
2570
+ front: wheel.position.x > 0,
2571
+ angle: 0
2572
+ }));
2573
+ const axle = new THREE.Vector3(0, 0, 1);
2574
+ const up = new THREE.Vector3(0, 1, 0);
2575
+ const spin = new THREE.Quaternion();
2576
+ const steering = new THREE.Quaternion();
2577
+ return {
2578
+ count: wheels.length,
2579
+ roll(distance, steer) {
2580
+ for (const w of wheels) {
2581
+ w.angle = (w.angle - distance / w.radius) % (Math.PI * 2);
2582
+ spin.setFromAxisAngle(axle, w.angle);
2583
+ steering.setFromAxisAngle(up, w.front ? steer : 0);
2584
+ w.wheel.quaternion.copy(w.rest).premultiply(spin).premultiply(steering);
2585
+ }
2586
+ },
2587
+ dispose() {
2588
+ for (const w of wheels) w.wheel.quaternion.copy(w.rest);
2589
+ }
2590
+ };
2591
+ }
2592
+
2593
+ // src/test-area/proving/crash.ts
2594
+ import { launchBody } from "@adatechnology/game-contracts";
2595
+ import { ejectionOf } from "@adatechnology/game-vehicles-driving";
2596
+ var GLASS_BLOW = 6e3;
2597
+ function createProvingCrash(host, occupant) {
2598
+ const thrown = [];
2599
+ const breakGlass = (destructible, exit, forward) => {
2600
+ if (!destructible) return;
2601
+ const want = exit === "windshield" ? "windshield" : exit === "rear_window" ? "rear" : exit.startsWith("left") ? "left" : "right";
2602
+ 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)));
2603
+ if (!pane) return;
2604
+ destructible.hit({ type: "blunt", partId: pane.id, point: destructible.partWorldCenter(pane.id), direction: forward, energy: GLASS_BLOW, radius: 0.35, source: "occupant" });
2605
+ };
2606
+ return {
2607
+ crash({ car, u, w, belted, destructible }) {
2608
+ const ejection = ejectionOf({ before: { u, w }, after: { u: 0, w: 0 } }, belted);
2609
+ if (!ejection) return null;
2610
+ const s = Math.sin(car.rotationY);
2611
+ const c = Math.cos(car.rotationY);
2612
+ const forward = [s, 0, c];
2613
+ const right = [-c, 0, s];
2614
+ breakGlass(destructible, ejection.exit, forward);
2615
+ const [vf, vr, vu] = ejection.velocity;
2616
+ const start = [car.position.x + forward[0] * 1.2, 1.1, car.position.z + forward[2] * 1.2];
2617
+ const placed = occupant ? host.addObject?.(occupant, { position: start, rotationY: car.rotationY }) : null;
2618
+ if (placed) {
2619
+ launchBody(placed.root, [forward[0] * vf + right[0] * vr, vu, forward[2] * vf + right[2] * vr]);
2620
+ thrown.push(placed.remove);
2621
+ }
2622
+ return ejection;
2623
+ },
2624
+ dispose() {
2625
+ for (const remove of thrown.splice(0)) remove();
2324
2626
  }
2325
2627
  };
2326
2628
  }
2327
2629
 
2630
+ // src/test-area/proving/systems-text.ts
2631
+ var SYSTEMS_TEXT = {
2632
+ "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",
2634
+ profile: "Modo de condu\xE7\xE3o",
2635
+ profiles: { eco: "econ\xF4mico", normal: "normal", sport: "esporte" },
2636
+ traction: "Controle de tra\xE7\xE3o",
2637
+ abs: "ABS",
2638
+ cruise: "Piloto autom\xE1tico",
2639
+ cruiseSet: "Piloto autom\xE1tico: segurando {0} km/h",
2640
+ climate: "Ar-condicionado",
2641
+ demist: "Desemba\xE7ador",
2642
+ 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" },
2644
+ events: {
2645
+ stall: "O motor morreu",
2646
+ out_of_fuel: "Acabou o combust\xEDvel",
2647
+ bogged: "Atolou",
2648
+ aquaplaning: "Aquaplanagem",
2649
+ battery_flat: "A bateria descarregou",
2650
+ overheating: "Motor superaquecendo: perdendo pot\xEAncia",
2651
+ seized: "O motor fundiu de tanto esquentar",
2652
+ spare_fitted: "Estepe colocado",
2653
+ no_spare: "N\xE3o h\xE1 estepe",
2654
+ cruise_cancelled: "Piloto autom\xE1tico desligado"
2655
+ },
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)" },
2657
+ 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
+ track: "faixa {0}",
2659
+ adjust: { seat_forward: "Banco: frente/tr\xE1s (m)", seat_height: "Banco: altura (m)", mirror_left: "Retrovisor esquerdo: \xE2ngulo", mirror_right: "Retrovisor direito: \xE2ngulo" },
2660
+ 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" } },
2661
+ 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",
2662
+ remote: { locked: "Travado pelo controle: alarme armado", unlocked: "Destravado pelo controle", forced: "Porta for\xE7ada" },
2663
+ 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
+ },
2665
+ 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",
2667
+ profile: "Drive profile",
2668
+ profiles: { eco: "eco", normal: "normal", sport: "sport" },
2669
+ traction: "Traction control",
2670
+ abs: "ABS",
2671
+ cruise: "Cruise control",
2672
+ cruiseSet: "Cruise control: holding {0} km/h",
2673
+ climate: "Air conditioning",
2674
+ demist: "Demister",
2675
+ 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" },
2677
+ events: {
2678
+ stall: "The engine stalled",
2679
+ out_of_fuel: "Out of fuel",
2680
+ bogged: "Bogged down",
2681
+ aquaplaning: "Aquaplaning",
2682
+ battery_flat: "The battery went flat",
2683
+ overheating: "Engine overheating: losing power",
2684
+ seized: "The engine seized from the heat",
2685
+ spare_fitted: "Spare tyre fitted",
2686
+ no_spare: "No spare tyre left",
2687
+ cruise_cancelled: "Cruise control off"
2688
+ },
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)" },
2690
+ 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
+ track: "track {0}",
2692
+ adjust: { seat_forward: "Seat: forward/back (m)", seat_height: "Seat: height (m)", mirror_left: "Left mirror: angle", mirror_right: "Right mirror: angle" },
2693
+ 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" } },
2694
+ 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",
2695
+ remote: { locked: "Locked by the remote: alarm armed", unlocked: "Unlocked by the remote", forced: "Door forced" },
2696
+ 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" }
2697
+ }
2698
+ };
2699
+
2328
2700
  // src/test-area/proving/text.ts
2329
2701
  var PROVING_TEXT = {
2330
2702
  "pt-BR": {
@@ -2469,9 +2841,11 @@ var MIRROR_RECTS = {
2469
2841
  right: { x: 0.79, y: 0.72, width: 0.19, height: 0.13, rounded: 0.3 },
2470
2842
  reverse: { x: 0.36, y: 0.13, width: 0.28, height: 0.18, rounded: 0.06 }
2471
2843
  };
2844
+ var ADJUST_KEYS = { seat_forward: "seatForward", seat_height: "seatHeight", mirror_left: "mirrorLeft", mirror_right: "mirrorRight" };
2472
2845
  function createProvingModule(options) {
2473
2846
  const locale = options.locale ?? "pt-BR";
2474
2847
  const text = PROVING_TEXT[locale] ?? PROVING_TEXT["pt-BR"];
2848
+ const sysText = SYSTEMS_TEXT[locale] ?? SYSTEMS_TEXT["pt-BR"];
2475
2849
  const tests = options.tests ?? createProvingTests();
2476
2850
  const vehicles = options.vehicles;
2477
2851
  const sounds = { ...defaultSoundIds, ...options.sounds };
@@ -2488,6 +2862,15 @@ function createProvingModule(options) {
2488
2862
  let run = null;
2489
2863
  let parking = null;
2490
2864
  let driving = null;
2865
+ let crashes = null;
2866
+ let wheels = null;
2867
+ let lastAt = null;
2868
+ let parked = null;
2869
+ const adjust = { seatForward: 0, seatHeight: 0, mirrorLeft: 0, mirrorRight: 0 };
2870
+ const aimed = (pose2) => {
2871
+ const turn = pose2.id === "left" ? adjust.mirrorLeft : pose2.id === "right" ? -adjust.mirrorRight : 0;
2872
+ return turn ? { ...pose2, look: [pose2.look[0] + turn, pose2.look[1], pose2.look[2]] } : pose2;
2873
+ };
2491
2874
  let gearbox = "automatic";
2492
2875
  let lastGear = null;
2493
2876
  let driveClock = 0;
@@ -2649,6 +3032,8 @@ function createProvingModule(options) {
2649
3032
  };
2650
3033
  const removeCar = () => {
2651
3034
  if (!car) return;
3035
+ wheels = null;
3036
+ lastAt = null;
2652
3037
  discardObject3D3(car.built.group);
2653
3038
  car = null;
2654
3039
  };
@@ -2662,6 +3047,7 @@ function createProvingModule(options) {
2662
3047
  built.group.add(guides);
2663
3048
  host.scene.add(built.group);
2664
3049
  car = { built, guides, subject };
3050
+ wheels = createWheelRoll(three, built);
2665
3051
  pose({ x: 0, z: TRACK.start, yaw: 0, pitch: 0, roll: 0, height: 0 }, 0);
2666
3052
  rig.vehicle?.reset();
2667
3053
  drawBoard();
@@ -2712,11 +3098,18 @@ function createProvingModule(options) {
2712
3098
  car.built.group.position.set(TRACK.x0 + wrapped, 0, TRACK.z + s.z);
2713
3099
  car.built.group.rotation.set(-s.pitch, s.yaw + Math.PI / 2, s.roll, "YXZ");
2714
3100
  bendGuides(steer);
3101
+ if (lastAt) {
3102
+ const along = (s.x - lastAt.x) * Math.cos(s.yaw) - (s.z - lastAt.z) * Math.sin(s.yaw);
3103
+ if (Math.abs(along) < 5) wheels?.roll(along, steer);
3104
+ }
3105
+ lastAt = { x: s.x, z: s.z };
2715
3106
  };
2716
3107
  const frameOf = (speed) => {
2717
3108
  if (!car) return null;
2718
3109
  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 } : {} };
3110
+ const size = { length: car.built.length, width: car.built.width, height: car.built.height };
3111
+ const [ex, ey, ez] = car.built.driverEye ?? defaultEye(size);
3112
+ 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
3113
  };
2721
3114
  const applyCamera = () => {
2722
3115
  if (!host?.setViewCamera) return;
@@ -2805,19 +3198,26 @@ function createProvingModule(options) {
2805
3198
  built: car.built,
2806
3199
  subject: car.subject,
2807
3200
  place: [0, 0, 0],
3201
+ kit: kitFor(three),
3202
+ obstacles: () => parked ? [[parked.root.position.x, parked.root.position.z]] : [],
2808
3203
  gauge: kitFor(three).materials?.gauge,
2809
3204
  needle: kitFor(three).materials?.amber,
2810
3205
  // the engine's own sound turns the key (starter, catch, idle by RPM)
2811
3206
  sounds: { starter: "" },
2812
3207
  onExit: () => stopDriving(),
2813
3208
  onEvent: (event) => {
2814
- if (event.type === "refused") return host?.log(text.cabin.refused[event.reason] ?? event.reason);
3209
+ if (event.type === "refused") return host?.log(text.cabin.refused[event.reason] ?? sysText.refused[event.reason] ?? event.reason);
3210
+ if (event.type === "sound") return;
3211
+ if (event.type !== "command") {
3212
+ 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]);
3213
+ return host?.log(sysText.events[event.type] ?? event.type, { vehicle: settings.vehicle, ...event }, "proving_system");
3214
+ }
2815
3215
  const st = event.state;
2816
3216
  const line = cabinLine(event.command, st);
2817
3217
  if (line) host?.log(line);
2818
3218
  if (event.command === "ignition" && car) {
2819
3219
  const at = [car.built.group.position.x, 0.5, car.built.group.position.z];
2820
- if (st.ignition) engine?.start(at);
3220
+ if (st.ignition && driving?.systems.canCrank() !== false) engine?.start(at);
2821
3221
  else engine?.stop(at);
2822
3222
  }
2823
3223
  if (event.command === "gearbox") {
@@ -2836,7 +3236,7 @@ function createProvingModule(options) {
2836
3236
  rig.vehicle?.reset();
2837
3237
  stopEngine();
2838
3238
  engine = createVehicleSound(host.audio, { sounds });
2839
- host.bench?.sheet?.({ kicker: text.name, title: fillText(text.log.drive, name(settings.vehicle)), body: text.cabin.keys });
3239
+ 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
3240
  host.log(fillText(text.log.drive, name(settings.vehicle)));
2841
3241
  };
2842
3242
  const onOff = (on) => on ? text.cabin.on : text.cabin.off;
@@ -2865,19 +3265,109 @@ function createProvingModule(options) {
2865
3265
  return `${c.alarm}: ${c.alarmStates[st.alarm]}`;
2866
3266
  case "radio":
2867
3267
  case "radio_source":
2868
- return `${c.radio}: ${st.radio.on ? c.sources[st.radio.source] : c.off}`;
3268
+ case "radio_track":
3269
+ return `${c.radio}: ${st.radio.on ? `${c.sources[st.radio.source]} \xB7 ${fillText(sysText.track, st.radio.track + 1)}` : c.off}`;
2869
3270
  case "indicator_left":
2870
3271
  case "indicator_right":
2871
3272
  case "hazard":
2872
3273
  return `${c.indicator}: ${c.indicators[st.indicator]}`;
2873
3274
  case "wipers":
2874
- return `${c.wipers}: ${c.wiperModes[st.wipers]}`;
3275
+ return `${c.wipers}: ${sysText.wiperModes[st.wipers]}`;
3276
+ case "drive_profile":
3277
+ return `${sysText.profile}: ${sysText.profiles[st.profile]}`;
3278
+ case "traction_control":
3279
+ return `${sysText.traction}: ${onOff(st.tractionControl)}`;
3280
+ case "abs":
3281
+ return `${sysText.abs}: ${onOff(st.abs)}`;
3282
+ case "cruise":
3283
+ return st.cruise.on ? fillText(sysText.cruiseSet, Math.round(st.cruise.speed * 3.6)) : `${sysText.cruise}: ${c.off}`;
3284
+ case "climate":
3285
+ return `${sysText.climate}: ${onOff(st.climate.on)}`;
3286
+ case "demist":
3287
+ return `${sysText.demist}: ${onOff(st.climate.demist)}`;
2875
3288
  case "seatbelt":
2876
3289
  return `${c.seatbelt}: ${onOff(st.seatbelt)}`;
2877
3290
  default:
2878
3291
  return null;
2879
3292
  }
2880
3293
  };
3294
+ const systemsAction = (id) => {
3295
+ if (!driving) startDriving();
3296
+ const cockpit = driving;
3297
+ if (!cockpit) return;
3298
+ const d = cockpit.driver;
3299
+ const done = (key) => host?.log(sysText.done[key], { vehicle: settings.vehicle, action: key }, "proving_system");
3300
+ switch (id) {
3301
+ case "puncture": {
3302
+ const tyres = d.spec.wheels.map((_, i) => d.condition.tyres?.[i] ?? 1);
3303
+ tyres[0] = 0;
3304
+ d.condition = { ...d.condition, tyres };
3305
+ return done("puncture");
3306
+ }
3307
+ case "fit_spare": {
3308
+ const flat = d.spec.wheels.findIndex((_, i) => (d.condition.tyres?.[i] ?? 1) < 0.2);
3309
+ const event = cockpit.systems.fitSpare(d, Math.max(0, flat));
3310
+ host?.log(sysText.events[event.type], { vehicle: settings.vehicle, ...event }, "proving_system");
3311
+ return;
3312
+ }
3313
+ case "refuel":
3314
+ d.refuel(d.spec.tank.capacity);
3315
+ return done("refuel");
3316
+ case "empty_tank":
3317
+ d.state.fuel = Math.min(d.state.fuel, d.spec.tank.capacity * 0.04);
3318
+ return done("empty_tank");
3319
+ case "drain_battery":
3320
+ cockpit.systems.charge(-(cockpit.systems.state.charge - 0.15));
3321
+ return done("drain_battery");
3322
+ case "jump_start":
3323
+ cockpit.systems.charge(1);
3324
+ return done("jump_start");
3325
+ case "break_radiator":
3326
+ cockpit.radiator = 0.05;
3327
+ return done("break_radiator");
3328
+ case "remote_lock":
3329
+ case "remote_unlock": {
3330
+ const lock = id === "remote_lock";
3331
+ cockpit.body?.lock(lock);
3332
+ cockpit.controls.state.alarm = lock ? "armed" : "off";
3333
+ host?.audio?.play?.({ sound: "alarm_chirp", position: [car?.built.group.position.x ?? 0, 1, car?.built.group.position.z ?? 0] });
3334
+ host?.log(sysText.remote[lock ? "locked" : "unlocked"], { vehicle: settings.vehicle, locked: lock }, "proving_system");
3335
+ return;
3336
+ }
3337
+ case "crash_wall": {
3338
+ const g = car?.built.group;
3339
+ if (!g || !host) return;
3340
+ crashes ?? (crashes = createProvingCrash(host, options.occupant));
3341
+ const speed = d.state.u;
3342
+ 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 });
3343
+ d.state.u = d.state.w = d.state.yawRate = 0;
3344
+ const kmh = Math.round(Math.abs(speed) * 3.6);
3345
+ const belted = cockpit.controls.state.seatbelt;
3346
+ 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");
3347
+ return;
3348
+ }
3349
+ case "park_behind": {
3350
+ const g = car?.built.group;
3351
+ if (!g || !host) return;
3352
+ parked?.remove();
3353
+ const back = (car?.built.length ?? 4.5) / 2 + 3 + 2.3;
3354
+ const at = [g.position.x - Math.sin(g.rotation.y) * back, 0, g.position.z - Math.cos(g.rotation.y) * back];
3355
+ const placed = host.addObject?.(options.parkedCar ?? "sedan", { position: at, rotationY: g.rotation.y });
3356
+ parked = placed ? { root: placed.root, remove: placed.remove } : null;
3357
+ host.log(placed ? sysText.parking.placed : sysText.parking.none, { vehicle: settings.vehicle }, "proving_system");
3358
+ return;
3359
+ }
3360
+ case "force_door": {
3361
+ const body = cockpit.body;
3362
+ if (!body) return;
3363
+ if (body.locked) body.lock(false);
3364
+ body.toggle("door_front_left");
3365
+ if (cockpit.controls.state.alarm === "armed") cockpit.controls.state.alarm = "ringing";
3366
+ host?.log(sysText.remote.forced, { vehicle: settings.vehicle, alarm: cockpit.controls.state.alarm }, "proving_system");
3367
+ return;
3368
+ }
3369
+ }
3370
+ };
2881
3371
  const gearName = (gear) => gear < 0 ? "R" : gear === 0 ? "N" : String(gear);
2882
3372
  const onChange = (change) => {
2883
3373
  if (change.kind === "layer") {
@@ -2886,6 +3376,7 @@ function createProvingModule(options) {
2886
3376
  }
2887
3377
  if (change.kind === "action") {
2888
3378
  if (change.id === "drive") return driving ? stopDriving() : startDriving();
3379
+ if (change.id in sysText.actions) return systemsAction(change.id);
2889
3380
  if (change.id === "run_test" || change.id === "run_all" || change.id === "reverse_park") stopDriving();
2890
3381
  if (change.id === "run_test") {
2891
3382
  queue = [];
@@ -2932,6 +3423,12 @@ function createProvingModule(options) {
2932
3423
  settings.camera = String(change.value);
2933
3424
  applyCamera();
2934
3425
  break;
3426
+ case "seat_forward":
3427
+ case "seat_height":
3428
+ case "mirror_left":
3429
+ case "mirror_right":
3430
+ adjust[ADJUST_KEYS[change.id]] = Number(change.value);
3431
+ break;
2935
3432
  case "time_scale":
2936
3433
  settings.timeScale = Number(change.value);
2937
3434
  break;
@@ -2981,7 +3478,7 @@ function createProvingModule(options) {
2981
3478
  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
3479
  const f = frameOf(speed);
2983
3480
  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);
3481
+ if (f && rig.vehicle) for (const m of rig.mirrors) rig.vehicle.placeMirror(m.camera, aimed(m.pose), f);
2985
3482
  void steer;
2986
3483
  const key = `${host.bench?.layer("mirrors")}|${host.bench?.layer("reverse_camera")}`;
2987
3484
  if (key !== mirrorKey) {
@@ -3000,6 +3497,10 @@ function createProvingModule(options) {
3000
3497
  const speedNow = () => Math.abs(run?.driver.state.u ?? driving?.driver.state.u ?? parking?.driver.state.u ?? 0) * 3.6;
3001
3498
  const counts = (verdict) => certificateOf(settings.vehicle)?.results.filter((r) => r.verdict === verdict).length ?? 0;
3002
3499
  const teardown = () => {
3500
+ crashes?.dispose();
3501
+ crashes = null;
3502
+ parked?.remove();
3503
+ parked = null;
3003
3504
  stopDriving();
3004
3505
  stopEngine();
3005
3506
  for (const m of rig.mirrors) m.remove();
@@ -3030,9 +3531,14 @@ function createProvingModule(options) {
3030
3531
  { kind: "choice", id: "vehicle", name: text.controls.vehicle, hint: text.hints.vehicle, options: vehicles.map((id) => ({ id, name: name(id) })), default: settings.vehicle },
3031
3532
  { 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
3533
  { 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 },
3534
+ { kind: "number", id: "seat_forward", name: sysText.adjust.seat_forward, min: -0.2, max: 0.2, step: 0.02, default: 0 },
3535
+ { kind: "number", id: "seat_height", name: sysText.adjust.seat_height, min: -0.1, max: 0.1, step: 0.01, default: 0 },
3536
+ { kind: "number", id: "mirror_left", name: sysText.adjust.mirror_left, min: -0.3, max: 0.3, step: 0.02, default: 0 },
3537
+ { kind: "number", id: "mirror_right", name: sysText.adjust.mirror_right, min: -0.3, max: 0.3, step: 0.02, default: 0 },
3033
3538
  { 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
3539
  { 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
3540
  { kind: "action", id: "drive", name: text.actions.drive },
3541
+ ...Object.keys(sysText.actions).map((id) => ({ kind: "action", id, name: sysText.actions[id] })),
3036
3542
  { 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
3543
  { kind: "action", id: "run_test", name: text.actions.run_test },
3038
3544
  { kind: "action", id: "run_all", name: text.actions.run_all },
@@ -3066,6 +3572,8 @@ function createProvingModule(options) {
3066
3572
  rig.vehicle = createVehicleCamera(three);
3067
3573
  lent.audio?.register?.(LIBRARY_BANK2);
3068
3574
  lent.audio?.register?.(CABIN_BANK);
3575
+ lent.audio?.register?.(VEHICLE_SOUND_BANK);
3576
+ lent.audio?.register?.(RADIO_BANK);
3069
3577
  const stopChange = lent.bench?.onChange(onChange);
3070
3578
  const stopFrame = lent.onFrame(frame);
3071
3579
  const stopClear = lent.onClear?.(() => {
@@ -3098,10 +3606,6 @@ export {
3098
3606
  liteMeshes,
3099
3607
  createVehiclesThree,
3100
3608
  createCabinSmoke,
3101
- VEHICLE_LOOP_FALLOFF,
3102
- VEHICLE_LOOPS,
3103
- VEHICLE_SOUNDS,
3104
- VEHICLE_SOUND_BANK,
3105
3609
  firingHz,
3106
3610
  rpmShare,
3107
3611
  layerWeight,
@@ -3113,6 +3617,13 @@ export {
3113
3617
  TRUCK_ENGINE,
3114
3618
  BIKE_ENGINE,
3115
3619
  ENGINE_PROFILES,
3620
+ VEHICLE_LOOP_FALLOFF,
3621
+ VEHICLE_LOOPS,
3622
+ VEHICLE_SOUNDS,
3623
+ CAR_RECORDED_RPM,
3624
+ CAR_START_RUN_STOP,
3625
+ VEHICLE_SAMPLE_ALIASES,
3626
+ VEHICLE_SOUND_BANK,
3116
3627
  createVehicleSound,
3117
3628
  VEHICLE_LOD_DISTANCE,
3118
3629
  createVehicleLod,
@@ -3126,13 +3637,15 @@ export {
3126
3637
  DEFAULT_TEST_VEHICLES,
3127
3638
  createVehiclesTestModule,
3128
3639
  loadVehiclesTestModule,
3640
+ createVehicleBody,
3129
3641
  DASHBOARD_LOOK,
3130
3642
  createVehicleDashboard,
3131
3643
  LAMP_RULES,
3132
3644
  createVehicleLamps,
3645
+ createWheelRoll,
3133
3646
  PROVING_TEXT,
3134
3647
  PROVING_AREA_ID,
3135
3648
  createProvingModule,
3136
3649
  loadProvingModule
3137
3650
  };
3138
- //# sourceMappingURL=chunk-MRUPKE7J.js.map
3651
+ //# sourceMappingURL=chunk-L63R2JQA.js.map