@adatechnology/game-vehicles-three 0.5.0 → 0.6.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.
@@ -1,7 +1,7 @@
1
1
  // src/test-area/module.ts
2
2
  import { DEFERRED_PRIORITY, DEFERRED_QUEUE_ID, footprintBlocker } from "@adatechnology/game-contracts";
3
3
  import { discardObject3D } from "@adatechnology/game-shapes-three";
4
- import { buildVehicleItem, crackedGlassMaterial, createCreaseQueue, createVehicleDestructible, createVehicleKit } from "@adatechnology/game-vehicles";
4
+ import { buildVehicleItem, crackedGlassMaterial, createCreaseQueue, createVehicleDestructible as createVehicleDestructible2, createVehicleKit } from "@adatechnology/game-vehicles";
5
5
  import { isRenderPools } from "@adatechnology/game-render-three";
6
6
  import { createArcadeDriver as createArcadeDriver2, createSemiSimDriver as createSemiSimDriver2, createSurfaces as createSurfaces2, vehicleCorners as vehicleCorners2 } from "@adatechnology/game-vehicles-driving";
7
7
  import { colorFromSeed, fleetDims, installFleet, paintPalettes, paletteOf } from "@adatechnology/game-vehicles-fleet";
@@ -253,6 +253,7 @@ function createVehiclesThree(THREE, options) {
253
253
  c.vehicle.matrixAutoUpdate = true;
254
254
  pivot.add(c.vehicle);
255
255
  group.add(pivot);
256
+ c.root = group;
256
257
  if (color && primary) {
257
258
  const repainted = primary.clone();
258
259
  repainted.color.set(color);
@@ -317,12 +318,15 @@ import { markKitOrigin as markKitOrigin2 } from "@adatechnology/game-contracts";
317
318
  import { createStaticBatcher } from "@adatechnology/game-render-three";
318
319
  var VEHICLE_LOD_DISTANCE = 14;
319
320
  var BATCH_BUDGET_MS = 2;
321
+ var VEHICLE_REBATCH_AFTER_MS = 1500;
320
322
  var isShown = (object, top) => {
321
323
  for (let node = object; node && node !== top; node = node.parent) if (node.visible === false) return false;
322
324
  return true;
323
325
  };
324
- function createVehicleLod(kit, context, root, full) {
326
+ function createVehicleLod(kit, context, root, full, options = {}) {
325
327
  const THREE = kit.THREE;
328
+ const now = options.now ?? (() => performance.now());
329
+ let changedAt = Number.NEGATIVE_INFINITY;
326
330
  let isUploaded = false;
327
331
  let modelKey = "";
328
332
  const culled = /* @__PURE__ */ new Map();
@@ -382,8 +386,17 @@ function createVehicleLod(kit, context, root, full) {
382
386
  root.add(proxy);
383
387
  return proxy;
384
388
  };
389
+ const batchLocked = () => {
390
+ batcher = createStaticBatcher(THREE, full, { budgetMs: BATCH_BUDGET_MS });
391
+ batcher.group.traverse((object) => object.userData.skipLite = true);
392
+ batcher.group.userData.noSurfaceRay = true;
393
+ markKitOrigin2(batcher.group, { owner: "game-vehicles-three", kind: "vehicle", id: root.name });
394
+ };
385
395
  const drawFull = () => {
386
- if (!batcher && isLocked) return;
396
+ if (!batcher && isLocked) {
397
+ if (now() - changedAt >= VEHICLE_REBATCH_AFTER_MS) batchLocked();
398
+ return;
399
+ }
387
400
  if (!batcher) {
388
401
  lite ?? (lite = build());
389
402
  lite.visible = isFused;
@@ -396,11 +409,13 @@ function createVehicleLod(kit, context, root, full) {
396
409
  });
397
410
  batcher = createStaticBatcher(THREE, full, { budgetMs: BATCH_BUDGET_MS });
398
411
  batcher.group.traverse((object) => object.userData.skipLite = true);
412
+ batcher.group.userData.noSurfaceRay = true;
399
413
  markKitOrigin2(batcher.group, { owner: "game-vehicles-three", kind: "vehicle", id: root.name });
400
414
  return;
401
415
  }
402
- batcher.update();
403
- if (!isLocked && batcher.stats().batched === 0 && batcher.stats().released > 0) {
416
+ if (!batcher) return;
417
+ if (batcher.update() > 0) changedAt = now();
418
+ if (batcher.stats().batched === 0 && batcher.stats().released > 0) {
404
419
  batcher.dispose();
405
420
  batcher = null;
406
421
  }
@@ -441,8 +456,10 @@ function createVehicleLod(kit, context, root, full) {
441
456
  };
442
457
  const api = {
443
458
  update(distance, touched, posed = false) {
459
+ if (touched && !isLocked) changedAt = now();
444
460
  isLocked || (isLocked = touched);
445
461
  if (keyOf() !== modelKey) {
462
+ changedAt = now();
446
463
  api.dispose();
447
464
  armUpload();
448
465
  }
@@ -513,6 +530,50 @@ function createCabinSmoke() {
513
530
  };
514
531
  }
515
532
 
533
+ // src/test-area/lane-damage.ts
534
+ import { crashVehicle, createVehicleDestructible } from "@adatechnology/game-vehicles";
535
+ import { conditionFromDamage } from "@adatechnology/game-vehicles-driving";
536
+ var LANE_DAMAGE = ["none", "into_wall", "flat_tyre", "torn_tyre", "run_flat", "no_brake_fluid"];
537
+ var RIFLE = { energy: 3500, caliber: 7.8, penetration: { steel: 18 } };
538
+ var TEAR_OFF_M = 5e3;
539
+ var LANE_CAR_KG = 1150;
540
+ var LANE_WALL_CONTACT = 1.6;
541
+ var WALL_REACH = 0.15;
542
+ var DRAIN_S = 600;
543
+ function createLaneDamage(params) {
544
+ const d = createVehicleDestructible(params.kit, params.context, { id: "lane", frame: params.frame, armour: params.damage === "run_flat" ? "IIIA" : null });
545
+ const part = (prefix) => d.describe().parts.find((p) => p.id.startsWith(prefix))?.id;
546
+ const shoot = (id) => {
547
+ if (id) d.hit({ type: "ballistic", point: d.partWorldCenter(id), part: id, direction: [0, 0, 1], ...RIFLE });
548
+ };
549
+ if (params.damage === "into_wall") {
550
+ } else if (params.damage === "no_brake_fluid") {
551
+ shoot(part("brakes"));
552
+ for (let t = 0; t < DRAIN_S; t++) d.update?.(1);
553
+ } else {
554
+ shoot(part("tyre_front_left"));
555
+ if (params.damage === "torn_tyre") d.rollFlat(TEAR_OFF_M, 8);
556
+ }
557
+ return {
558
+ step({ dt, speed, throttle, driver }) {
559
+ d.rollFlat(speed * dt, speed);
560
+ d.runEngine(dt, throttle);
561
+ const events = d.update?.(dt) ?? [];
562
+ driver.condition = conditionFromDamage(driver.spec, d.driveReport());
563
+ return events.flatMap((e) => e.type === "debris" && e.kind === "sparks" ? [e.point] : []);
564
+ },
565
+ settle(dt) {
566
+ d.update?.(dt);
567
+ },
568
+ crash({ speed, direction }) {
569
+ const front = part("bumper_front");
570
+ if (!front) return [];
571
+ const at = d.partWorldCenter(front);
572
+ return crashVehicle(d, { point: [at[0] + direction[0] * WALL_REACH, at[1], at[2] + direction[2] * WALL_REACH], direction, speed, massKg: LANE_CAR_KG, otherMassKg: Infinity, contactWidth: LANE_WALL_CONTACT }).events;
573
+ }
574
+ };
575
+ }
576
+
516
577
  // src/test-area/scenarios.ts
517
578
  import { createArcadeDriver, createSemiSimDriver, createSurfaces, vehicleCorners } from "@adatechnology/game-vehicles-driving";
518
579
 
@@ -562,25 +623,27 @@ var TEXT = {
562
623
  group: "Vehicles",
563
624
  name: "Vehicles area",
564
625
  items: { hatch: "Compact hatch", sedan: "Midsize sedan", pickup: "Midsize pickup", sport_bike: "Sport bike", box_truck: "Box truck", city_bus: "City bus", battle_tank: "Battle tank", attack_helicopter: "Attack helicopter", sedan_burnt: "Burnt sedan", hatch_burnt: "Burnt hatch" },
565
- controls: { quality: "Quality", paint_by_seed: "Paint by seed", color_seed: "Colour seed", palette: "Palette", driving: "Driving", surface: "Surface" },
626
+ controls: { quality: "Quality", paint_by_seed: "Paint by seed", color_seed: "Colour seed", palette: "Palette", driving: "Driving", surface: "Surface", lane_damage: "Lane damage" },
566
627
  hints: {
567
628
  quality: "High: the kit at full detail. Light: fewer segments and no interior (the light mode of the fleet); applies to the vehicles placed from now on",
568
629
  paint_by_seed: "Off: every vehicle keeps its own paint. On: the body colour comes from the seed and the palette",
569
630
  palette: "Auto: the palette of the vehicle group (civil, or military for the army vehicles)",
570
631
  driving: "Arcade: the game car, simple and forgiving. Semi: a rigid body with springs, tyre friction, gears and surfaces",
571
- surface: "The ground under the wheels in the semi mode (grip and aquaplaning)"
632
+ surface: "The ground under the wheels in the semi mode (grip and aquaplaning)",
633
+ lane_damage: "What the lane car starts with; it drives in the semi mode and the damage model sets its tyres, brakes and engine"
572
634
  },
573
635
  options: {
574
636
  quality: { high: "High", lite: "Light" },
575
637
  palette: { auto: "Auto", civil: "Civil", military: "Military" },
576
638
  driving: { arcade: "Arcade", semi: "Semi-simulation" },
577
- surface: { dry_asphalt: "Dry asphalt", wet_asphalt: "Wet asphalt", dirt: "Dirt", grass: "Grass", mud: "Mud", blood: "Blood" }
639
+ surface: { dry_asphalt: "Dry asphalt", wet_asphalt: "Wet asphalt", dirt: "Dirt", grass: "Grass", mud: "Mud", blood: "Blood" },
640
+ lane_damage: { none: "None", into_wall: "Into the wall, no braking", flat_tyre: "Flat tyre (rifle round)", torn_tyre: "Tyre torn off: rim on the road", run_flat: "Armoured, flat tyre (run-flat)", no_brake_fluid: "No brake fluid" }
578
641
  },
579
642
  actions: { repaint: "Repaint with the seed", drive_lane: "Drive the lane", clear_lane: "Remove the lane car", honk: "Honk the horn" },
580
643
  layers: { damageables: "Damageable boxes by material", zones: "Zone boxes by type", axes: "Output frame axes" },
581
644
  metrics: { vehicles: "Vehicles", triangles: "Triangles", draw_calls: "Draw calls", size_error_mm: "Size against DIMS", metal: "Metal boxes", glass: "Glass boxes", rubber: "Rubber boxes", fuel: "Fuel boxes", speed: "Lane speed", lane: "Lane distance" },
582
645
  words: { yes: "true", no: "false", open: "open", tight: "tight", on: "on", off: "off" },
583
- lane: { crash: "Lane: the car hit the wall at {0} m/s", end: "Lane: the car stopped {0} m from the wall" },
646
+ lane: { crash: "Lane: the car hit the wall at {0} m/s", end: "Lane: the car stopped {0} m from the wall", damage: "Lane: the crash dented {0} and broke {1}" },
584
647
  scenarios: {
585
648
  vehicles_9mm_windshield: "Vehicles: 9 mm through the windshield",
586
649
  vehicles_shotgun_door: "Vehicles: shotgun on the door",
@@ -629,25 +692,27 @@ var TEXT = {
629
692
  group: "Ve\xEDculos",
630
693
  name: "\xC1rea de ve\xEDculos",
631
694
  items: { hatch: "Hatch compacto", sedan: "Sed\xE3 m\xE9dio", pickup: "Picape m\xE9dia", sport_bike: "Moto esportiva", box_truck: "Caminh\xE3o ba\xFA", city_bus: "\xD4nibus urbano", battle_tank: "Tanque de guerra", attack_helicopter: "Helic\xF3ptero de ataque", sedan_burnt: "Sed\xE3 queimado", hatch_burnt: "Hatch queimado" },
632
- controls: { quality: "Qualidade", paint_by_seed: "Pintar pela semente", color_seed: "Semente da cor", palette: "Paleta", driving: "Dire\xE7\xE3o", surface: "Piso" },
695
+ controls: { quality: "Qualidade", paint_by_seed: "Pintar pela semente", color_seed: "Semente da cor", palette: "Paleta", driving: "Dire\xE7\xE3o", surface: "Piso", lane_damage: "Dano na pista" },
633
696
  hints: {
634
697
  quality: "Alta: o kit com todo o detalhe. Leve: menos segmentos e sem interior (o modo leve da frota); vale para os ve\xEDculos postos daqui em diante",
635
698
  paint_by_seed: "Desligado: cada ve\xEDculo mant\xE9m a pr\xF3pria pintura. Ligado: a cor da carroceria vem da semente e da paleta",
636
699
  palette: "Auto: a paleta do grupo do ve\xEDculo (civil, ou militar para os do ex\xE9rcito)",
637
700
  driving: "Arcade: o carro do jogo, simples e tolerante. Semi: corpo r\xEDgido com molas, atrito do pneu, marchas e pisos",
638
- surface: "O ch\xE3o sob as rodas no modo semi (ader\xEAncia e aquaplanagem)"
701
+ surface: "O ch\xE3o sob as rodas no modo semi (ader\xEAncia e aquaplanagem)",
702
+ lane_damage: "Com o que o carro da pista sai; ele anda no modo semi e o modelo de dano define pneus, freio e motor"
639
703
  },
640
704
  options: {
641
705
  quality: { high: "Alta", lite: "Leve" },
642
706
  palette: { auto: "Auto", civil: "Civil", military: "Militar" },
643
707
  driving: { arcade: "Arcade", semi: "Semissimula\xE7\xE3o" },
644
- surface: { dry_asphalt: "Asfalto seco", wet_asphalt: "Asfalto molhado", dirt: "Terra", grass: "Grama", mud: "Lama", blood: "Sangue" }
708
+ surface: { dry_asphalt: "Asfalto seco", wet_asphalt: "Asfalto molhado", dirt: "Terra", grass: "Grama", mud: "Lama", blood: "Sangue" },
709
+ lane_damage: { none: "Nenhum", into_wall: "Contra o muro, sem frear", flat_tyre: "Pneu furado (tiro de fuzil)", torn_tyre: "Pneu desfeito: aro no ch\xE3o", run_flat: "Blindado, pneu furado (run-flat)", no_brake_fluid: "Sem fluido de freio" }
645
710
  },
646
711
  actions: { repaint: "Repintar com a semente", drive_lane: "Percorrer a pista", clear_lane: "Tirar o carro da pista", honk: "Buzinar" },
647
712
  layers: { damageables: "Caixas danific\xE1veis por material", zones: "Caixas das zonas por tipo", axes: "Eixos do quadro de sa\xEDda" },
648
713
  metrics: { vehicles: "Ve\xEDculos", triangles: "Tri\xE2ngulos", draw_calls: "Chamadas de desenho", size_error_mm: "Medida contra DIMS", metal: "Caixas de metal", glass: "Caixas de vidro", rubber: "Caixas de borracha", fuel: "Caixas de combust\xEDvel", speed: "Velocidade na pista", lane: "Dist\xE2ncia na pista" },
649
714
  words: { yes: "sim", no: "n\xE3o", open: "soltos", tight: "presos", on: "acesos", off: "apagados" },
650
- lane: { crash: "Pista: o carro bateu no muro a {0} m/s", end: "Pista: o carro parou a {0} m do muro" },
715
+ lane: { crash: "Pista: o carro bateu no muro a {0} m/s", end: "Pista: o carro parou a {0} m do muro", damage: "Pista: a batida amassou {0} e quebrou {1}" },
651
716
  scenarios: {
652
717
  vehicles_9mm_windshield: "Ve\xEDculos: 9 mm atravessa o para-brisa",
653
718
  vehicles_shotgun_door: "Ve\xEDculos: espingarda na porta",
@@ -910,6 +975,57 @@ var createScenarios = (deps) => {
910
975
  // src/test-area/module.ts
911
976
  import { LIBRARY_BANK } from "@adatechnology/game-audio";
912
977
 
978
+ // src/audio/profiles.ts
979
+ var DEFAULT_VEHICLE_SOUND_KEYS = {
980
+ start: "engine_start",
981
+ stop: "engine_stop",
982
+ fail: "engine_fail",
983
+ crashLight: "crash_light",
984
+ crashMedium: "crash_medium",
985
+ crashHeavy: "crash_heavy",
986
+ skidAsphalt: "tire_skid_asphalt",
987
+ skidDirt: "tire_skid_dirt",
988
+ roll: "tire_roll"
989
+ };
990
+ var CRASH_SPEEDS = { medium: 6, heavy: 14 };
991
+ var layers = (id, cutoffs, gains) => [
992
+ { sound: `engine_${id}_low`, centre: 0, width: 0.5, harmonic: 1, gain: gains[0], cutoff: cutoffs[0] },
993
+ { sound: `engine_${id}_mid`, centre: 0.45, width: 0.5, harmonic: 1, gain: gains[1], cutoff: cutoffs[1] },
994
+ { sound: `engine_${id}_high`, centre: 1, width: 0.6, harmonic: 2, gain: gains[2], cutoff: cutoffs[2] }
995
+ ];
996
+ var CAR_ENGINE = {
997
+ id: "car",
998
+ cylinders: 4,
999
+ idleRpm: 800,
1000
+ redlineRpm: 6500,
1001
+ // a 5-speed hatch: 1st tops at ~13 m/s, 5th at ~50 m/s
1002
+ rpmPerMps: [500, 300, 205, 155, 125],
1003
+ layers: layers("car", [[220, 520], [420, 1300], [900, 2600]], [0.11, 0.075, 0.04]),
1004
+ load: { sound: "engine_car_load", gain: 0.05, cutoff: [350, 1600] },
1005
+ closedThrottle: 0.55
1006
+ };
1007
+ var TRUCK_ENGINE = {
1008
+ id: "truck",
1009
+ cylinders: 6,
1010
+ idleRpm: 650,
1011
+ redlineRpm: 2600,
1012
+ rpmPerMps: [420, 260, 170, 120, 90, 72],
1013
+ layers: layers("truck", [[160, 380], [300, 800], [600, 1500]], [0.13, 0.08, 0.035]),
1014
+ load: { sound: "engine_truck_load", gain: 0.06, cutoff: [250, 900] },
1015
+ closedThrottle: 0.6
1016
+ };
1017
+ var BIKE_ENGINE = {
1018
+ id: "bike",
1019
+ cylinders: 4,
1020
+ idleRpm: 1300,
1021
+ redlineRpm: 12e3,
1022
+ rpmPerMps: [900, 640, 500, 410, 350, 310],
1023
+ layers: layers("bike", [[350, 900], [700, 2200], [1500, 4200]], [0.08, 0.07, 0.05]),
1024
+ load: { sound: "engine_bike_load", gain: 0.045, cutoff: [600, 2600] },
1025
+ closedThrottle: 0.5
1026
+ };
1027
+ var ENGINE_PROFILES = { car: CAR_ENGINE, truck: TRUCK_ENGINE, bike: BIKE_ENGINE };
1028
+
913
1029
  // src/test-area/types.ts
914
1030
  var defaultEffectIds = {
915
1031
  ignite: "vehicle_fire",
@@ -920,20 +1036,240 @@ var defaultEffectIds = {
920
1036
  leakIgnited: "vehicle_leak_ignited",
921
1037
  exhaust: "vehicle_exhaust"
922
1038
  };
923
- var defaultSoundIds = { engine: "engine_saw", horn: "horn", crash: "sfx_impact" };
1039
+ var defaultSoundIds = { horn: "horn_car", ...DEFAULT_VEHICLE_SOUND_KEYS };
924
1040
  var DEFAULT_TEST_WRECKS = [
925
1041
  { key: "sedan_burnt", vehicle: "midsize_sedan", scenario: "burnt" },
926
1042
  { key: "hatch_burnt", vehicle: "compact_hatch", scenario: "burnt" }
927
1043
  ];
928
1044
 
1045
+ // src/audio/engine-mix.ts
1046
+ var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
1047
+ var firingHz = (profile, rpm) => rpm / 60 * (profile.cylinders / 2);
1048
+ var rpmShare = (profile, rpm) => clamp01((rpm - profile.idleRpm) / (profile.redlineRpm - profile.idleRpm));
1049
+ var layerWeight = (centre, width, n) => {
1050
+ const d = Math.abs(n - centre) / width;
1051
+ return d >= 1 ? 0 : Math.cos(d * Math.PI / 2);
1052
+ };
1053
+ function engineMix(profile, input) {
1054
+ const throttle = clamp01(input.throttle);
1055
+ const on = input.rpm > 0;
1056
+ const n = rpmShare(profile, input.rpm);
1057
+ const level = on ? profile.closedThrottle + (1 - profile.closedThrottle) * throttle : 0;
1058
+ const open = 0.5 * n + 0.5 * throttle;
1059
+ const hz = firingHz(profile, input.rpm);
1060
+ const tones = profile.layers.map((layer) => ({
1061
+ sound: layer.sound,
1062
+ volume: layer.gain * level * layerWeight(layer.centre, layer.width, n),
1063
+ frequency: hz * layer.harmonic,
1064
+ cutoff: layer.cutoff[0] + (layer.cutoff[1] - layer.cutoff[0]) * open
1065
+ }));
1066
+ const load = profile.load;
1067
+ return [
1068
+ ...tones,
1069
+ { sound: load.sound, volume: on ? load.gain * (0.15 + 0.85 * throttle) * (0.35 + 0.65 * n) : 0, cutoff: load.cutoff[0] + (load.cutoff[1] - load.cutoff[0]) * open }
1070
+ ];
1071
+ }
1072
+ function createGearGuess(profile) {
1073
+ let gear = 0;
1074
+ return {
1075
+ rpm(speed, throttle) {
1076
+ const v = Math.abs(speed);
1077
+ const ratios = profile.rpmPerMps;
1078
+ if (v < 0.3) {
1079
+ gear = 0;
1080
+ return profile.idleRpm + (profile.redlineRpm - profile.idleRpm) * 0.12 * clamp01(throttle);
1081
+ }
1082
+ const at = (g) => v * ratios[g];
1083
+ while (gear < ratios.length - 1 && at(gear) > profile.redlineRpm * 0.85) gear++;
1084
+ while (gear > 0 && at(gear) < profile.redlineRpm * 0.4) gear--;
1085
+ return Math.max(profile.idleRpm, Math.min(profile.redlineRpm, at(gear)));
1086
+ },
1087
+ get gear() {
1088
+ return gear;
1089
+ }
1090
+ };
1091
+ }
1092
+
1093
+ // src/audio/vehicle-sound.ts
1094
+ var clamp012 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
1095
+ function createVehicleSound(audio, options = {}) {
1096
+ const profile = options.profile ?? CAR_ENGINE;
1097
+ const keys = { ...DEFAULT_VEHICLE_SOUND_KEYS, ...options.sounds };
1098
+ const crankSeconds = options.crankSeconds ?? 0.75;
1099
+ const gears = createGearGuess(profile);
1100
+ const loops = /* @__PURE__ */ new Map();
1101
+ const levels = {};
1102
+ const state = { running: false, cranking: 0, catches: false, flare: 0, rpm: 0 };
1103
+ const loopOf = (sound) => {
1104
+ let handle = loops.get(sound);
1105
+ if (!handle && audio?.loop) {
1106
+ handle = audio.loop(sound);
1107
+ loops.set(sound, handle);
1108
+ }
1109
+ return handle;
1110
+ };
1111
+ const setLoop = (sound, volume, position, extra = {}) => {
1112
+ levels[sound] = volume;
1113
+ if (volume <= 0 && !loops.has(sound)) return;
1114
+ loopOf(sound)?.set({ volume, position, ...extra.frequency === void 0 ? {} : { frequency: extra.frequency }, ...extra.cutoff === void 0 ? {} : { params: { cutoff: extra.cutoff } } });
1115
+ };
1116
+ const play = (sound, position, volume = 1) => void audio?.play({ sound, position, volume });
1117
+ return {
1118
+ start(position, startOptions = {}) {
1119
+ if (state.running || state.cranking > 0) return;
1120
+ state.catches = !startOptions.fail;
1121
+ state.cranking = state.catches ? crankSeconds : 1.7;
1122
+ play(state.catches ? keys.start : keys.fail, position);
1123
+ },
1124
+ stop(position) {
1125
+ if (!state.running && state.cranking <= 0) return;
1126
+ state.running = false;
1127
+ state.cranking = 0;
1128
+ play(keys.stop, position);
1129
+ },
1130
+ crash(speed, position) {
1131
+ const v = Math.abs(speed);
1132
+ const kind = v >= CRASH_SPEEDS.heavy ? "heavy" : v >= CRASH_SPEEDS.medium ? "medium" : "light";
1133
+ play(kind === "heavy" ? keys.crashHeavy : kind === "medium" ? keys.crashMedium : keys.crashLight, position, Math.min(1, 0.35 + v / 20));
1134
+ return kind;
1135
+ },
1136
+ update(frame) {
1137
+ if (state.cranking > 0) {
1138
+ state.cranking -= frame.dt;
1139
+ if (state.cranking <= 0 && state.catches) {
1140
+ state.running = true;
1141
+ state.flare = 1;
1142
+ }
1143
+ }
1144
+ state.flare = Math.max(0, state.flare - frame.dt / 0.9);
1145
+ const throttle = clamp012(frame.throttle);
1146
+ const driven = frame.rpm ?? gears.rpm(frame.speed, throttle);
1147
+ const rpm = state.running ? Math.max(driven, profile.idleRpm * (1 + 1.2 * state.flare * state.flare)) : 0;
1148
+ state.rpm = rpm;
1149
+ for (const layer of engineMix(profile, { rpm, throttle: Math.max(throttle, state.flare * 0.6) })) {
1150
+ setLoop(layer.sound, layer.volume, frame.position, { ...layer.frequency === void 0 ? {} : { frequency: layer.frequency }, cutoff: layer.cutoff });
1151
+ }
1152
+ const speed = Math.abs(frame.speed);
1153
+ const slide = clamp012(((frame.slip ?? 0) - 0.15) / 0.5) * clamp012(speed / 4);
1154
+ const dirt = frame.surface === "dirt";
1155
+ setLoop(keys.skidAsphalt, dirt ? 0 : 0.16 * slide, frame.position, { cutoff: 1700 + 600 * slide });
1156
+ setLoop(keys.skidDirt, dirt ? 0.2 * slide : 0, frame.position, { cutoff: 500 + 500 * slide });
1157
+ setLoop(keys.roll, 0.06 * clamp012(speed / 25), frame.position, { cutoff: 250 + 45 * Math.min(speed, 40) });
1158
+ },
1159
+ get reading() {
1160
+ return { running: state.running, rpm: state.rpm, levels: { ...levels } };
1161
+ },
1162
+ dispose() {
1163
+ for (const handle of loops.values()) handle.stop();
1164
+ loops.clear();
1165
+ }
1166
+ };
1167
+ }
1168
+
1169
+ // src/audio/bank.ts
1170
+ import { sceneMany as many, sceneNoise as noise, sceneRecipe, sceneTone as tone } from "@adatechnology/game-audio";
1171
+ var VEHICLE_LOOP_FALLOFF = { model: "inverse_square", k: 4e-3, cutoffK: 0.01 };
1172
+ var SDK = "sdk: vehicle sounds (sons/checklist-sons.md 5.6)";
1173
+ var SMOOTHING = { volume: 0.06, frequency: 0.04 };
1174
+ var toneLoop = (id, wave, filter, cutoff, q, line) => ({
1175
+ id,
1176
+ bus: "world",
1177
+ order: "game_loop",
1178
+ source: { kind: "osc", wave, loop: true },
1179
+ filters: { mode: "serial", bank: [{ type: filter, frequency: { set: cutoff }, q }] },
1180
+ smoothing: SMOOTHING,
1181
+ falloff: VEHICLE_LOOP_FALLOFF,
1182
+ pan: true,
1183
+ sources: [SDK, line]
1184
+ });
1185
+ var noiseLoop = (id, filter, cutoff, q, line) => ({
1186
+ id,
1187
+ bus: "world",
1188
+ order: "game_loop",
1189
+ source: { kind: "noise", offset: "random", loop: true },
1190
+ filters: { mode: "serial", bank: [{ type: filter, frequency: { set: cutoff }, q }] },
1191
+ smoothing: SMOOTHING,
1192
+ falloff: VEHICLE_LOOP_FALLOFF,
1193
+ pan: true,
1194
+ sources: [SDK, line]
1195
+ });
1196
+ var engineLoops = (id, low, mid, high, load) => [
1197
+ toneLoop(`engine_${id}_low`, "sawtooth", "lowpass", low, 1.4, "idle and low RPM: the firing frequency, a saw through a low-pass"),
1198
+ toneLoop(`engine_${id}_mid`, "square", "lowpass", mid, 0.9, "mid RPM: odd harmonics, the bark"),
1199
+ toneLoop(`engine_${id}_high`, "sawtooth", "bandpass", high, 1.2, "high RPM: the second harmonic through a band-pass, the scream near the redline"),
1200
+ noiseLoop(`engine_${id}_load`, "bandpass", load, 0.8, "intake and exhaust roar, opened by the throttle")
1201
+ ];
1202
+ var VEHICLE_LOOPS = [
1203
+ ...engineLoops("car", 400, 900, 1600, 700),
1204
+ ...engineLoops("truck", 280, 600, 1e3, 500),
1205
+ ...engineLoops("bike", 600, 1400, 2800, 1200),
1206
+ noiseLoop("tire_skid_asphalt", "bandpass", 1900, 5, "rubber squealing on asphalt: a narrow band of noise near 2 kHz"),
1207
+ noiseLoop("tire_skid_dirt", "lowpass", 700, 0.8, "tyres sliding on dirt and gravel: a dull, wide hiss"),
1208
+ noiseLoop("tire_roll", "lowpass", 400, 0.7, "road noise of the tyres rolling, brighter with the speed")
1209
+ ];
1210
+ var recipe = (id, layers2, line) => ({ ...sceneRecipe(id, layers2, line), sources: [SDK, line] });
1211
+ var crank = (count2, spacing) => [
1212
+ { ...noise({ type: "bandpass", f: [300, 380], q: 1.5, d: 0.07, vol: 0.45 }), repeat: { count: count2, spacing } },
1213
+ tone({ wave: "sawtooth", f: 95, f2: count2 > 8 ? 70 : 110, d: count2 * spacing, vol: 0.035 })
1214
+ ];
1215
+ var latch = (delay, vol) => [
1216
+ noise({ type: "bandpass", f: [2600, 3200], q: 3, d: 0.018, vol, delay }),
1217
+ tone({ f: 1300, f2: 1180, d: 0.025, vol: vol * 0.15, delay: delay + 4e-3 })
1218
+ ];
1219
+ var VEHICLE_SOUNDS = [
1220
+ // the engine catches at 0.75 s: the loops take over from there (the vehicle flares the RPM)
1221
+ recipe("engine_start", [...crank(7, 0.105), tone({ wave: "sawtooth", f: 45, f2: 27, d: 0.35, vol: 0.3, delay: 0.72 }), noise({ f: 260, d: 0.3, vol: 0.35, delay: 0.72 })], "starter cranking, then the engine catches"),
1222
+ recipe("engine_fail", [...crank(13, 0.125), ...latch(1.65, 0.2)], "starter cranking slower and slower, the engine never catches, the key released"),
1223
+ recipe("engine_stop", [tone({ wave: "sawtooth", f: 27, f2: 8, d: 0.45, vol: 0.3 }), noise({ f: 200, f2: 60, d: 0.4, vol: 0.32 }), noise({ type: "bandpass", f: 120, q: 2, d: 0.12, vol: 0.3, delay: 0.32 })], "ignition off: the engine runs down and shudders to a stop"),
1224
+ // light: a dent and plastic giving; medium: the body folding and a pane cracking; heavy: metal folding, glass going, debris falling
1225
+ recipe(
1226
+ "crash_light",
1227
+ [tone({ f: 110, f2: 55, d: 0.16, vol: 0.7 }), noise({ type: "bandpass", f: 1100, q: 1, d: 0.18, vol: 0.85, wet: 0.2 }), noise({ type: "highpass", f: 2500, d: 0.05, vol: 0.45 }), ...many(3, () => noise({ type: "bandpass", f: [1800, 3200], q: 3, d: 0.03, vol: 0.2, delay: [0.03, 0.2] }))],
1228
+ "crash below 22 km/h: bumper and plastic, a dull dent"
1229
+ ),
1230
+ recipe(
1231
+ "crash_medium",
1232
+ [
1233
+ tone({ f: 85, f2: 38, d: 0.3, vol: 1.1 }),
1234
+ noise({ type: "bandpass", f: 700, q: 0.9, d: 0.5, vol: 0.8, wet: 0.3 }),
1235
+ ...many(5, () => noise({ type: "bandpass", f: [500, 1300], q: 2, d: [0.08, 0.18], vol: 0.55, delay: [0, 0.35] })),
1236
+ ...many(6, () => tone({ f: [3500, 7e3], d: 0.06, vol: 0.08, delay: [0.05, 0.5] }))
1237
+ ],
1238
+ "crash at 22\u201350 km/h: the body folding, a pane cracking"
1239
+ ),
1240
+ recipe(
1241
+ "crash_heavy",
1242
+ [
1243
+ tone({ f: 65, f2: 28, d: 0.6, vol: 1.5 }),
1244
+ noise({ f: 900, f2: 150, d: 0.9, vol: 1.3, wet: 0.5 }),
1245
+ ...many(9, () => noise({ type: "bandpass", f: [300, 1200], q: 2, d: [0.1, 0.25], vol: 0.6, delay: [0, 0.7] })),
1246
+ noise({ type: "highpass", f: 3200, d: 0.5, vol: 0.7, delay: 0.03, wet: 0.4 }),
1247
+ ...many(14, () => tone({ f: [3e3, 7500], d: [0.05, 0.12], vol: 0.12, delay: [0.05, 1.2] })),
1248
+ ...many(6, () => noise({ type: "bandpass", f: [1500, 3e3], q: 2, d: 0.04, vol: 0.25, delay: [0.5, 1.5] }))
1249
+ ],
1250
+ "crash above 50 km/h: metal folding, the glass going, debris falling"
1251
+ ),
1252
+ // 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
1253
+ 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"),
1254
+ 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"),
1255
+ // a car door is a padded "tum" with a latch, not a hollow metal box (the fridge's shut_metal)
1256
+ recipe("car_door_open", [...latch(0, 0.32), noise({ f: 400, d: 0.06, vol: 0.14, delay: 0.035 })], "car door: the handle pulled, the latch released, the seal letting go"),
1257
+ recipe("car_door_close", [tone({ f: 75, f2: 48, d: 0.16, vol: 0.75 }), noise({ f: 320, d: 0.1, vol: 0.6 }), ...latch(0.012, 0.3)], "car door shut: a dull padded thump and the latch catching"),
1258
+ 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"),
1259
+ 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")
1260
+ ];
1261
+ var VEHICLE_SOUND_BANK = { id: "vehicles_three", sounds: VEHICLE_SOUNDS, loops: VEHICLE_LOOPS };
1262
+
929
1263
  // src/test-area/module.ts
930
1264
  var VEHICLES_AREA_ID = "vehicles_area";
931
1265
  var DEFAULT_TEST_VEHICLES = ["compact_hatch", "midsize_sedan", "midsize_pickup", "sport_bike", "box_truck", "city_bus", "battle_tank", "attack_helicopter"];
932
1266
  var ITEM_OF = { compact_hatch: "hatch", midsize_sedan: "sedan", midsize_pickup: "pickup", sport_bike: "sport_bike", box_truck: "box_truck", city_bus: "city_bus", battle_tank: "battle_tank", attack_helicopter: "attack_helicopter" };
933
1267
  var itemId = (key) => ITEM_OF[key] ?? key;
934
1268
  var LANE = { z: 11, from: -12, wall: 13 };
1269
+ var SPARK_EVERY_S = 0.08;
935
1270
  var LEAF_REST_RADIANS = 0.02;
936
1271
  var MATERIAL_COLOURS = { metal: 13421772, glass: 4500223, rubber: 16750848, fuel: 16724736 };
1272
+ var LANE_START_S = 1.2;
937
1273
  var ZONE_COLOURS = { door: 5627221, hood: 14540083, lid: 14522675, bumper: 14505437, glass: 4500223, light: 16777215, wheel: 16742195 };
938
1274
  var BODY_HEIGHT = 1.7;
939
1275
  function createVehiclesTestModule(options = {}) {
@@ -945,7 +1281,7 @@ function createVehiclesTestModule(options = {}) {
945
1281
  const entries = [...keys.map((key) => ({ key, id: itemId(key) })), ...wrecks.map((w) => ({ key: w.vehicle, id: w.key, scenario: w.scenario }))];
946
1282
  const effectIds = { ...defaultEffectIds, ...options.effects };
947
1283
  const soundIds = { ...defaultSoundIds, ...options.sounds };
948
- const settings = { quality: "high", paint: false, seed: 1, palette: "auto", driving: "arcade", surface: "dry_asphalt" };
1284
+ const settings = { quality: "high", paint: false, seed: 1, palette: "auto", driving: "arcade", surface: "dry_asphalt", damage: "none" };
949
1285
  const kits = /* @__PURE__ */ new WeakMap();
950
1286
  const placed = /* @__PURE__ */ new Map();
951
1287
  const cabins = createCabinSmoke();
@@ -957,10 +1293,10 @@ function createVehiclesTestModule(options = {}) {
957
1293
  let lane = null;
958
1294
  const effects = () => host?.effects;
959
1295
  const audio = () => host?.audio;
960
- let engineLoop;
1296
+ let carSound;
961
1297
  const stopEngine = () => {
962
- engineLoop?.stop();
963
- engineLoop = void 0;
1298
+ carSound?.dispose();
1299
+ carSound = void 0;
964
1300
  };
965
1301
  const particleMaterials = {
966
1302
  own: (build) => isRenderPools(host?.pools) ? host.pools.materials.own(build) : build(),
@@ -1017,7 +1353,7 @@ function createVehiclesTestModule(options = {}) {
1017
1353
  impl: (root, id) => {
1018
1354
  const r = root;
1019
1355
  const kit = r.userData.kit;
1020
- const d = createVehicleDestructible(kit, r.userData.vehicle, { id, frame: root, name: text.items[itemKey] ?? key, creaseQueue });
1356
+ const d = createVehicleDestructible2(kit, r.userData.vehicle, { id, frame: root, name: text.items[itemKey] ?? key, creaseQueue });
1021
1357
  tapEffects(d);
1022
1358
  const setPartPose = d.setPartPose?.bind(d);
1023
1359
  {
@@ -1053,7 +1389,7 @@ function createVehiclesTestModule(options = {}) {
1053
1389
  if (!clipCache.has(key)) {
1054
1390
  const kit = kitFor(THREE);
1055
1391
  const built = buildVehicleItem(kit, key);
1056
- const d = createVehicleDestructible(kit, built.vehicle, { id: `${itemId(key)}_clips`, frame: built.root, headless: true });
1392
+ const d = createVehicleDestructible2(kit, built.vehicle, { id: `${itemId(key)}_clips`, frame: built.root, headless: true });
1057
1393
  clipCache.set(key, d.animations());
1058
1394
  kit.dispose(built.vehicle);
1059
1395
  }
@@ -1181,15 +1517,16 @@ function createVehiclesTestModule(options = {}) {
1181
1517
  clearLane();
1182
1518
  const kit = kitFor(three);
1183
1519
  const vehicles = createVehiclesThree(three, { kit, fleet });
1184
- const built = vehicles.build("compact_hatch", { quality: "lite", seed: settings.seed });
1520
+ const built = vehicles.build("compact_hatch", { quality: settings.damage === "none" ? "lite" : "high", seed: settings.seed });
1185
1521
  const wall = new three.Mesh(new three.BoxGeometry(0.6, 2, 6), new three.MeshStandardMaterial({ color: 7829367 }));
1186
1522
  wall.position.set(LANE.wall + 0.3, 1, LANE.z);
1187
1523
  const scene = host.scene;
1188
1524
  scene.add(built.group, wall);
1189
- const mode = settings.driving === "semi" ? "semi" : "arcade";
1525
+ const mode = settings.driving === "semi" || settings.damage !== "none" ? "semi" : "arcade";
1526
+ const damage = settings.damage === "none" || !built.context ? null : createLaneDamage({ kit, context: built.context, frame: built.group, damage: settings.damage });
1190
1527
  const surfaces = createSurfaces2();
1191
1528
  const idle = { throttle: 0, brake: 0, steer: 0, handbrake: false };
1192
- const state = { time: 0, exhaust: 0, ended: false, wasBlocked: false };
1529
+ const state = { time: 0, exhaust: 0, sparks: 0, ended: false, wasBlocked: false };
1193
1530
  const arcade = createArcadeDriver2({ length: built.length, width: built.width });
1194
1531
  const semi = createSemiSimDriver2({}, { surfaces });
1195
1532
  const driver = mode === "semi" ? semi : arcade;
@@ -1202,7 +1539,8 @@ function createVehiclesTestModule(options = {}) {
1202
1539
  built.group.rotation.y = driver.state.yaw + Math.PI / 2;
1203
1540
  };
1204
1541
  pose();
1205
- engineLoop = audio()?.loop?.(soundIds.engine, { position: [driver.state.x, 0.5, driver.state.z] });
1542
+ carSound = createVehicleSound(audio(), { sounds: soundIds });
1543
+ carSound.start([driver.state.x, 0.5, driver.state.z]);
1206
1544
  const log = (key, value) => host?.log(fill(text, locale, text.lane[key], value.toFixed(1)));
1207
1545
  lane = {
1208
1546
  car: built,
@@ -1214,29 +1552,56 @@ function createVehiclesTestModule(options = {}) {
1214
1552
  speed: () => "u" in driver.state ? driver.state.u : driver.state.v,
1215
1553
  distance: () => driver.state.x - LANE.from,
1216
1554
  step(dt) {
1217
- if (state.ended) return;
1555
+ if (state.ended) {
1556
+ damage?.settle(dt);
1557
+ const at = [driver.state.x, 0.5, driver.state.z];
1558
+ if (carSound?.reading.running) carSound.stop(at);
1559
+ carSound?.update({ dt, position: at, speed: 0, throttle: 0 });
1560
+ return;
1561
+ }
1218
1562
  state.time += dt;
1219
- const braking = gap() < brakeAt() && state.time > 0.5;
1220
- const input = { ...idle, throttle: braking ? 0 : 1, brake: braking ? 1 : 0, steer: Math.sin(state.time * 1.4) * 0.15 };
1563
+ const braking = settings.damage !== "into_wall" && gap() < brakeAt() && state.time > 0.5;
1564
+ const moving = state.time > LANE_START_S;
1565
+ 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 };
1221
1566
  if (mode === "arcade") {
1222
1567
  const { events } = arcade.step(input, dt, { blocked: (x, z, yaw) => vehicleCorners2(arcade.spec, x, z, yaw).some(([px]) => px >= LANE.wall) });
1223
1568
  for (const e of events) {
1224
1569
  if (e.type !== "crash") continue;
1225
1570
  log("crash", e.speed);
1226
- audio()?.play({ sound: soundIds.crash, position: [driver.state.x, 0.5, driver.state.z], volume: Math.min(1, e.speed / 15) });
1571
+ carSound?.crash(e.speed, [driver.state.x + built.length / 2, 0.5, driver.state.z]);
1227
1572
  }
1228
1573
  } else {
1229
1574
  semi.step(input, dt, { ground: () => ({ surface: settings.surface }) });
1575
+ const grinding = damage?.step({ dt, speed: Math.abs(lane?.speed() ?? 0), throttle: input.throttle, driver: semi }) ?? [];
1576
+ state.sparks -= dt;
1577
+ const fx2 = effects();
1578
+ if (fx2 && state.sparks <= 0 && Math.abs(lane?.speed() ?? 0) > 0.5) {
1579
+ state.sparks = grinding.length ? SPARK_EVERY_S : 0;
1580
+ for (const point of grinding) fx2.play({ effect: effectIds.sparks, point });
1581
+ }
1230
1582
  if (gap() <= 0) {
1231
1583
  state.ended = true;
1584
+ const crashed = damage?.crash({ speed: Math.abs(semi.state.u), direction: [Math.cos(driver.state.yaw), 0, -Math.sin(driver.state.yaw)] });
1585
+ if (crashed) {
1586
+ const parts = (type) => [...new Set(crashed.flatMap((e) => e.type === type && "part" in e ? [e.part] : []))].join(", ") || "-";
1587
+ host?.log(fill(text, locale, text.lane.damage, parts("dent"), parts("fracture")));
1588
+ }
1232
1589
  log("crash", Math.abs(semi.state.u));
1233
- audio()?.play({ sound: soundIds.crash, position: [driver.state.x, 0.5, driver.state.z], volume: Math.min(1, Math.abs(semi.state.u) / 15) });
1590
+ carSound?.crash(semi.state.u, [driver.state.x + built.length / 2, 0.5, driver.state.z]);
1234
1591
  }
1235
1592
  }
1236
1593
  pose();
1237
1594
  {
1238
1595
  const speed = Math.abs(lane?.speed() ?? 0);
1239
- engineLoop?.set({ volume: state.ended ? 0 : 0.035 + speed * 3e-3, frequency: 38 + speed * 7, position: [driver.state.x, 0.5, driver.state.z] });
1596
+ const semiState = mode === "semi" ? semi.state : null;
1597
+ carSound?.update({
1598
+ dt,
1599
+ position: [driver.state.x, 0.5, driver.state.z],
1600
+ speed,
1601
+ throttle: input.throttle,
1602
+ ...semiState ? { rpm: semiState.rpm, slip: Math.max(0, ...semiState.slip.map(Math.abs)) } : { slip: braking && speed > 3 ? 0.6 : 0 },
1603
+ surface: /dirt|gravel|sand|grass|mud/.test(settings.surface) ? "dirt" : "asphalt"
1604
+ });
1240
1605
  }
1241
1606
  state.exhaust -= dt;
1242
1607
  const fx = effects();
@@ -1244,7 +1609,7 @@ function createVehiclesTestModule(options = {}) {
1244
1609
  state.exhaust = 0.4;
1245
1610
  fx.play({ effect: effectIds.exhaust, point: [driver.state.x - Math.cos(driver.state.yaw) * (built.length / 2), 0.3, driver.state.z + Math.sin(driver.state.yaw) * (built.length / 2)], scale: Math.min(1, Math.abs(lane?.speed() ?? 0) / 17) });
1246
1611
  }
1247
- if (!state.ended && state.time > 1 && Math.abs(lane?.speed() ?? 0) < 0.05) {
1612
+ if (!state.ended && state.time > LANE_START_S + 1 && Math.abs(lane?.speed() ?? 0) < 0.05) {
1248
1613
  state.ended = true;
1249
1614
  log("end", Math.max(0, gap()));
1250
1615
  }
@@ -1282,6 +1647,9 @@ function createVehiclesTestModule(options = {}) {
1282
1647
  case "surface":
1283
1648
  settings.surface = String(change.value);
1284
1649
  break;
1650
+ case "lane_damage":
1651
+ settings.damage = String(change.value);
1652
+ break;
1285
1653
  default:
1286
1654
  break;
1287
1655
  }
@@ -1377,6 +1745,7 @@ function createVehiclesTestModule(options = {}) {
1377
1745
  { kind: "choice", id: "palette", name: text.controls.palette, hint: text.hints.palette, options: ["auto", "civil", "military"].map((id) => ({ id, name: text.options.palette[id] })), default: "auto" },
1378
1746
  { kind: "choice", id: "driving", name: text.controls.driving, hint: text.hints.driving, options: ["arcade", "semi"].map((id) => ({ id, name: text.options.driving[id] })), default: "arcade" },
1379
1747
  { kind: "choice", id: "surface", name: text.controls.surface, hint: text.hints.surface, options: surfaceIds.map((id) => ({ id, name: text.options.surface[id] ?? id })), default: "dry_asphalt" },
1748
+ { kind: "choice", id: "lane_damage", name: text.controls.lane_damage, hint: text.hints.lane_damage, options: LANE_DAMAGE.map((id) => ({ id, name: text.options.lane_damage[id] ?? id })), default: "none" },
1380
1749
  { kind: "action", id: "repaint", name: text.actions.repaint },
1381
1750
  { kind: "action", id: "drive_lane", name: text.actions.drive_lane },
1382
1751
  { kind: "action", id: "clear_lane", name: text.actions.clear_lane },
@@ -1406,6 +1775,7 @@ function createVehiclesTestModule(options = {}) {
1406
1775
  const stopCrease = lent.defer?.(creaseQueue.deferredQueue(), DEFERRED_PRIORITY[DEFERRED_QUEUE_ID.VEHICLE_CREASE]);
1407
1776
  if (isRenderPools(lent.pools)) lent.pools.materials.register("vehicles.cracked-glass", () => crackedGlassMaterial(three, new three.DataTexture(new Uint8Array(4), 1, 1)));
1408
1777
  lent.audio?.register?.(LIBRARY_BANK);
1778
+ lent.audio?.register?.(VEHICLE_SOUND_BANK);
1409
1779
  return () => {
1410
1780
  stopChange?.();
1411
1781
  stopFrame();
@@ -1430,6 +1800,589 @@ function loadVehiclesTestModule(room, options = {}) {
1430
1800
  return room.loadModule(createVehiclesTestModule({ ...options, three }));
1431
1801
  }
1432
1802
 
1803
+ // src/test-area/proving/module.ts
1804
+ import { createVehicleCamera, mirrorPoses } from "@adatechnology/game-camera";
1805
+ import { markKitOrigin as markKitOrigin3 } from "@adatechnology/game-contracts";
1806
+ import { LIBRARY_BANK as LIBRARY_BANK2 } from "@adatechnology/game-audio";
1807
+ import { createSemiSimDriver as createSemiSimDriver3 } from "@adatechnology/game-vehicles-driving";
1808
+ import { certify, createProvingTests, startRun, statusOf, subjectSpec, withResult } from "@adatechnology/game-vehicles-proving";
1809
+ import { discardObject3D as discardObject3D2 } from "@adatechnology/game-shapes-three";
1810
+
1811
+ // src/test-area/proving/text.ts
1812
+ var PROVING_TEXT = {
1813
+ "pt-BR": {
1814
+ name: "Banco de provas",
1815
+ controls: { vehicle: "Ve\xEDculo", test: "Prova", camera: "C\xE2mera", time_scale: "Velocidade da simula\xE7\xE3o", visual_check: "Checagem visual" },
1816
+ hints: {
1817
+ vehicle: "Ve\xEDculo da frota que vai para a pista",
1818
+ test: "Prova medida que o bot\xE3o Rodar prova executa",
1819
+ camera: "Sala: a c\xE2mera do jogador. Persegui\xE7\xE3o, cabine e cap\xF4 seguem o ve\xEDculo",
1820
+ time_scale: "Quantas vezes mais r\xE1pido que o tempo real a prova roda",
1821
+ visual_check: "O que voc\xEA olhou e vai aprovar ou reprovar"
1822
+ },
1823
+ actions: { run_test: "Rodar prova", run_all: "Rodar todas na pista", certify_now: "Homologar agora (sem anima\xE7\xE3o)", reverse_park: "Dar r\xE9 (c\xE2mera de r\xE9)", sign_ok: "Aprovar checagem visual", sign_fail: "Reprovar checagem visual", reset: "Limpar ficha" },
1824
+ layers: { mirrors: "Retrovisores", reverse_camera: "C\xE2mera de r\xE9", scoreboard: "Painel da ficha" },
1825
+ metrics: { speed: "Velocidade", time: "Tempo da prova", value: "Medi\xE7\xE3o", passed: "Aprovadas", failed: "Reprovadas", pending: "Pendentes", status: "Situa\xE7\xE3o (0 pend., 1 aprov., -1 reprov.)" },
1826
+ cameras: { room: "Sala", chase: "Persegui\xE7\xE3o (3\xAA pessoa)", cockpit: "Cabine (1\xAA pessoa)", hood: "Cap\xF4" },
1827
+ tests: {
1828
+ accel_0_100: "0 a 100 km/h",
1829
+ top_speed: "Velocidade m\xE1xima",
1830
+ brake_100_0: "Frenagem 100 a 0",
1831
+ turn_circle: "Di\xE2metro de giro",
1832
+ wet_brake_80_0: "Frenagem molhada 80 a 0",
1833
+ mud_traction: "Tra\xE7\xE3o na lama",
1834
+ cabin: "Cabine e personagem",
1835
+ cameras: "C\xE2meras",
1836
+ mirrors: "Retrovisores e r\xE9",
1837
+ terrain: "Terrenos",
1838
+ wind_tunnel: "T\xFAnel de vento",
1839
+ crash_test: "Crash test",
1840
+ pedestrian: "Atropelamento",
1841
+ performance: "Desempenho",
1842
+ buoyancy: "Flutua\xE7\xE3o",
1843
+ flight: "Voo"
1844
+ },
1845
+ status: { approved: "APROVADO", rejected: "REPROVADO", pending: "PENDENTE" },
1846
+ verdict: { pass: "ok", fail: "falhou", pending: "\u2026" },
1847
+ board: { title: "Ficha de homologa\xE7\xE3o", test: "Prova", value: "Medido", range: "Faixa", verdict: "Resultado", notRun: "n\xE3o rodada", running: "rodando" },
1848
+ log: { start: "Prova {0}: {1}", result: "{0} \xB7 {1}: {2} {3} (faixa {4}) \u2192 {5}", signed: "{0} \xB7 {1}: {2} na sala", certified: "{0}: {1}" }
1849
+ },
1850
+ en: {
1851
+ name: "Proving ground",
1852
+ controls: { vehicle: "Vehicle", test: "Test", camera: "Camera", time_scale: "Simulation speed", visual_check: "Visual check" },
1853
+ hints: {
1854
+ vehicle: "Fleet vehicle that goes on the track",
1855
+ test: "Measured test the Run test button runs",
1856
+ camera: "Room: the player camera. Chase, cockpit and hood follow the vehicle",
1857
+ time_scale: "How many times faster than real time the test runs",
1858
+ visual_check: "What you looked at and will approve or refuse"
1859
+ },
1860
+ actions: { run_test: "Run test", run_all: "Run all on the track", certify_now: "Certify now (no animation)", reverse_park: "Reverse (reversing camera)", sign_ok: "Approve visual check", sign_fail: "Refuse visual check", reset: "Clear certificate" },
1861
+ layers: { mirrors: "Mirrors", reverse_camera: "Reversing camera", scoreboard: "Certificate board" },
1862
+ metrics: { speed: "Speed", time: "Test time", value: "Measured", passed: "Passed", failed: "Failed", pending: "Pending", status: "Status (0 pending, 1 approved, -1 rejected)" },
1863
+ cameras: { room: "Room", chase: "Chase (third person)", cockpit: "Cockpit (first person)", hood: "Hood" },
1864
+ tests: {
1865
+ accel_0_100: "0 to 100 km/h",
1866
+ top_speed: "Top speed",
1867
+ brake_100_0: "Braking 100 to 0",
1868
+ turn_circle: "Turning circle",
1869
+ wet_brake_80_0: "Wet braking 80 to 0",
1870
+ mud_traction: "Mud traction",
1871
+ cabin: "Cabin and character",
1872
+ cameras: "Cameras",
1873
+ mirrors: "Mirrors and reversing",
1874
+ terrain: "Terrain",
1875
+ wind_tunnel: "Wind tunnel",
1876
+ crash_test: "Crash test",
1877
+ pedestrian: "Pedestrian impact",
1878
+ performance: "Performance",
1879
+ buoyancy: "Buoyancy",
1880
+ flight: "Flight"
1881
+ },
1882
+ status: { approved: "APPROVED", rejected: "REJECTED", pending: "PENDING" },
1883
+ verdict: { pass: "ok", fail: "failed", pending: "\u2026" },
1884
+ board: { title: "Certificate", test: "Test", value: "Measured", range: "Range", verdict: "Result", notRun: "not run", running: "running" },
1885
+ log: { start: "Test {0}: {1}", result: "{0} \xB7 {1}: {2} {3} (range {4}) \u2192 {5}", signed: "{0} \xB7 {1}: {2} in the room", certified: "{0}: {1}" }
1886
+ }
1887
+ };
1888
+ var fillText = (template, ...values) => template.replace(/\{(\d+)\}/g, (_, i) => String(values[Number(i)] ?? ""));
1889
+
1890
+ // src/test-area/proving/module.ts
1891
+ var PROVING_AREA_ID = "vehicles_proving";
1892
+ var TRACK = { x0: -30, z: 22, tile: 260, width: 14, start: 4 };
1893
+ var SURFACE_LOOK = { dry_asphalt: { color: 2829617, roughness: 0.95 }, wet_asphalt: { color: 1842980, roughness: 0.18 }, mud: { color: 4863268, roughness: 1 } };
1894
+ var VISUAL_CHECKS = ["cabin", "cameras", "mirrors"];
1895
+ var MIRROR_RECTS = {
1896
+ rear_view: { x: 0.38, y: 0.02, width: 0.24, height: 0.09, rounded: 0.35 },
1897
+ left: { x: 0.02, y: 0.72, width: 0.19, height: 0.13, rounded: 0.3 },
1898
+ right: { x: 0.79, y: 0.72, width: 0.19, height: 0.13, rounded: 0.3 },
1899
+ reverse: { x: 0.36, y: 0.13, width: 0.28, height: 0.18, rounded: 0.06 }
1900
+ };
1901
+ function createProvingModule(options) {
1902
+ const locale = options.locale ?? "pt-BR";
1903
+ const text = PROVING_TEXT[locale] ?? PROVING_TEXT["pt-BR"];
1904
+ const tests = options.tests ?? createProvingTests();
1905
+ const vehicles = options.vehicles;
1906
+ const sounds = { ...defaultSoundIds, ...options.sounds };
1907
+ const measured = tests.tests.filter((t) => t.kind === "measured").map((t) => t.id);
1908
+ const settings = { vehicle: vehicles.includes("compact_hatch") ? "compact_hatch" : vehicles[0] ?? "compact_hatch", test: measured[0] ?? "accel_0_100", camera: "chase", timeScale: 4, visual: "cameras" };
1909
+ const certificates = /* @__PURE__ */ new Map();
1910
+ const kits = /* @__PURE__ */ new WeakMap();
1911
+ let host = null;
1912
+ let three = null;
1913
+ let track = null;
1914
+ let strip = null;
1915
+ let board = null;
1916
+ let car = null;
1917
+ let run = null;
1918
+ let parking = null;
1919
+ let queue = [];
1920
+ let open = false;
1921
+ let lastWrap = 0;
1922
+ let engine;
1923
+ const rig = { camera: null, vehicle: null, mirrors: [] };
1924
+ const name = (id) => options.nameOf?.(id) ?? id.replace(/_/g, " ");
1925
+ const kitFor = (THREE) => {
1926
+ let kit = kits.get(THREE);
1927
+ if (!kit) {
1928
+ kit = options.createKit(THREE);
1929
+ kits.set(THREE, kit);
1930
+ }
1931
+ return kit;
1932
+ };
1933
+ const subjectOf = (id) => {
1934
+ const mass = three ? kitFor(three).find(id)?.mass : void 0;
1935
+ return options.subject(id, mass);
1936
+ };
1937
+ const blank = (subject) => {
1938
+ const results = tests.forDomain(subject.domain).map((t) => ({ test: t.id, kind: t.kind, unit: t.unit, range: t.range?.(subject) ?? {}, value: null, verdict: "pending", note: t.kind === "measured" ? "not run" : t.kind === "visual" ? "awaiting sign-off" : "room not built yet" }));
1939
+ return { vehicle: subject.id, domain: subject.domain, category: subject.category, status: statusOf(results), results };
1940
+ };
1941
+ const certificateOf = (id) => {
1942
+ if (!certificates.has(id)) {
1943
+ const subject = subjectOf(id);
1944
+ if (subject) certificates.set(id, blank(subject));
1945
+ }
1946
+ return certificates.get(id);
1947
+ };
1948
+ const store = (certificate) => {
1949
+ certificates.set(certificate.vehicle, certificate);
1950
+ options.onCertificate?.(certificate);
1951
+ drawBoard();
1952
+ };
1953
+ const buildTrack = () => {
1954
+ if (!host || track) return;
1955
+ const THREE = three;
1956
+ track = markKitOrigin3(new THREE.Group(), { owner: "@adatechnology/game-vehicles-three", kind: "room", id: "proving_track" });
1957
+ track.name = "proving_track";
1958
+ const look = SURFACE_LOOK.dry_asphalt;
1959
+ strip = new THREE.Mesh(new THREE.PlaneGeometry(TRACK.tile + 80, TRACK.width), new THREE.MeshStandardMaterial({ color: look.color, roughness: look.roughness, metalness: 0.05 }));
1960
+ strip.rotation.x = -Math.PI / 2;
1961
+ strip.position.set(TRACK.x0 + TRACK.tile / 2, 0.012, TRACK.z);
1962
+ strip.receiveShadow = true;
1963
+ track.add(strip);
1964
+ const paint = new THREE.MeshBasicMaterial({ color: 15263970 });
1965
+ const yellow = new THREE.MeshBasicMaterial({ color: 15909424 });
1966
+ const dash = new THREE.PlaneGeometry(3, 0.14);
1967
+ const edge = new THREE.PlaneGeometry(TRACK.tile + 80, 0.16);
1968
+ for (const side of [-1, 1]) {
1969
+ const line = new THREE.Mesh(edge, paint);
1970
+ line.rotation.x = -Math.PI / 2;
1971
+ line.position.set(TRACK.x0 + TRACK.tile / 2, 0.016, TRACK.z + side * (TRACK.width / 2 - 0.4));
1972
+ track.add(line);
1973
+ }
1974
+ for (let x = TRACK.x0 - 40; x < TRACK.x0 + TRACK.tile + 40; x += 9) {
1975
+ const d = new THREE.Mesh(dash, paint);
1976
+ d.rotation.x = -Math.PI / 2;
1977
+ d.position.set(x, 0.016, TRACK.z - 1.5);
1978
+ track.add(d);
1979
+ }
1980
+ const post = new THREE.BoxGeometry(0.12, 1.4, 0.12);
1981
+ const plate = new THREE.BoxGeometry(0.06, 0.5, 0.9);
1982
+ for (let m = 0; m <= TRACK.tile; m += 50) {
1983
+ const p = new THREE.Mesh(post, yellow);
1984
+ p.position.set(TRACK.x0 + m, 0.7, TRACK.z - TRACK.width / 2 - 0.6);
1985
+ const q = new THREE.Mesh(plate, m === 0 ? yellow : paint);
1986
+ q.position.set(TRACK.x0 + m, 1.45, TRACK.z - TRACK.width / 2 - 0.6);
1987
+ track.add(p, q);
1988
+ }
1989
+ const startLine = new THREE.Mesh(new THREE.PlaneGeometry(0.5, TRACK.width - 1), yellow);
1990
+ startLine.rotation.x = -Math.PI / 2;
1991
+ startLine.position.set(TRACK.x0, 0.017, TRACK.z);
1992
+ track.add(startLine);
1993
+ host.scene.add(track);
1994
+ buildBoard();
1995
+ };
1996
+ const setSurface = (surface) => {
1997
+ if (!strip) return;
1998
+ const look = SURFACE_LOOK[surface] ?? SURFACE_LOOK.dry_asphalt;
1999
+ strip.material.color.setHex(look.color);
2000
+ strip.material.roughness = look.roughness;
2001
+ };
2002
+ const buildBoard = () => {
2003
+ if (!host || board || typeof document === "undefined") return;
2004
+ const THREE = three;
2005
+ const canvas = document.createElement("canvas");
2006
+ canvas.width = 1024;
2007
+ canvas.height = 640;
2008
+ const texture = new THREE.CanvasTexture(canvas);
2009
+ texture.colorSpace = THREE.SRGBColorSpace;
2010
+ const mesh = markKitOrigin3(new THREE.Mesh(new THREE.PlaneGeometry(9.6, 6), new THREE.MeshBasicMaterial({ map: texture, toneMapped: false })), { owner: "@adatechnology/game-vehicles-three", kind: "room", id: "proving_board" });
2011
+ mesh.position.set(TRACK.x0 + 6, 3.6, TRACK.z - TRACK.width / 2 - 3);
2012
+ const legs = new THREE.Mesh(new THREE.BoxGeometry(9.8, 0.2, 0.2), new THREE.MeshStandardMaterial({ color: 1842981 }));
2013
+ legs.position.set(0, -3.1, -0.05);
2014
+ const stand = new THREE.Mesh(new THREE.BoxGeometry(0.25, 3.4, 0.25), legs.material);
2015
+ stand.position.set(0, -4.6, -0.1);
2016
+ mesh.add(legs, stand);
2017
+ track.add(mesh);
2018
+ board = { mesh, canvas, texture };
2019
+ drawBoard();
2020
+ };
2021
+ const fmtRange = (r) => r.min == null && r.max == null ? "\u2014" : `${r.min ?? "\u2026"}\u2013${r.max ?? "\u2026"}`;
2022
+ const drawBoard = () => {
2023
+ if (!board) return;
2024
+ const cert = certificateOf(settings.vehicle);
2025
+ const g = board.canvas.getContext("2d");
2026
+ if (!g || !cert) return;
2027
+ const W = board.canvas.width;
2028
+ g.fillStyle = "#16181c";
2029
+ g.fillRect(0, 0, W, board.canvas.height);
2030
+ g.fillStyle = "#f2c230";
2031
+ g.fillRect(0, 0, W, 10);
2032
+ g.font = "600 26px monospace";
2033
+ g.fillStyle = "#8d95a3";
2034
+ g.fillText(text.board.title.toUpperCase(), 32, 50);
2035
+ g.font = "700 46px sans-serif";
2036
+ g.fillStyle = "#e7e9ec";
2037
+ g.fillText(name(cert.vehicle), 32, 102);
2038
+ const colour = { approved: "#5cc98a", rejected: "#ef6a5b", pending: "#a7a0f0" }[cert.status];
2039
+ g.font = "700 34px sans-serif";
2040
+ const status = text.status[cert.status];
2041
+ const sw = g.measureText(status).width + 32;
2042
+ g.strokeStyle = colour;
2043
+ g.lineWidth = 3;
2044
+ g.strokeRect(W - sw - 32, 66, sw, 48);
2045
+ g.fillStyle = colour;
2046
+ g.fillText(status, W - sw - 16, 102);
2047
+ g.font = "22px monospace";
2048
+ g.fillStyle = "#8d95a3";
2049
+ const cols = [32, 430, 640, 860];
2050
+ [text.board.test, text.board.value, text.board.range, text.board.verdict].forEach((h, i) => g.fillText(h.toUpperCase(), cols[i], 150));
2051
+ let y = 190;
2052
+ for (const r of cert.results) {
2053
+ const live = run && run.test.id === r.test && !run.done;
2054
+ g.fillStyle = "#e7e9ec";
2055
+ g.font = "26px sans-serif";
2056
+ g.fillText(text.tests[r.test] ?? r.test, cols[0], y);
2057
+ g.font = "26px monospace";
2058
+ g.fillStyle = "#5fd4e0";
2059
+ const value = live ? text.board.running : r.value == null ? r.kind === "measured" && r.note === "not run" ? text.board.notRun : "\u2014" : `${r.value} ${r.unit}`;
2060
+ g.fillText(value, cols[1], y);
2061
+ g.fillStyle = "#8d95a3";
2062
+ g.fillText(r.kind === "measured" ? fmtRange(r.range) : r.kind === "visual" ? "visual" : "sala", cols[2], y);
2063
+ g.fillStyle = { pass: "#5cc98a", fail: "#ef6a5b", pending: "#a7a0f0" }[r.verdict];
2064
+ g.fillText(r.verdict === "pass" ? "\u2713 " + text.verdict.pass : r.verdict === "fail" ? "\u2717 " + text.verdict.fail : "\u2026", cols[3], y);
2065
+ y += 30;
2066
+ }
2067
+ board.texture.needsUpdate = true;
2068
+ };
2069
+ const removeCar = () => {
2070
+ if (!car) return;
2071
+ discardObject3D2(car.built.group);
2072
+ car = null;
2073
+ };
2074
+ const placeCar = () => {
2075
+ if (!host || !three) return;
2076
+ const subject = subjectOf(settings.vehicle);
2077
+ if (!subject) return;
2078
+ removeCar();
2079
+ const built = createVehiclesThree(three, { kit: kitFor(three) }).build(settings.vehicle, { quality: "high" });
2080
+ const guides = reverseGuides(built);
2081
+ built.group.add(guides);
2082
+ host.scene.add(built.group);
2083
+ car = { built, guides, subject };
2084
+ pose({ x: 0, z: TRACK.start, yaw: 0, pitch: 0, roll: 0, height: 0 }, 0);
2085
+ rig.vehicle?.reset();
2086
+ drawBoard();
2087
+ };
2088
+ const reverseGuides = (built) => {
2089
+ const THREE = three;
2090
+ const group = new THREE.Group();
2091
+ group.name = "reverse_guides";
2092
+ const half = built.width / 2 - 0.05;
2093
+ const tail = -built.length / 2;
2094
+ const bands = [
2095
+ [0.2, 1, 15682107],
2096
+ [1, 2, 15909424],
2097
+ [2, 3.2, 6080906]
2098
+ ];
2099
+ for (const [a, b, colour] of bands) {
2100
+ const material = new THREE.MeshBasicMaterial({ color: colour, transparent: true, opacity: 0.85, depthTest: false });
2101
+ for (const side of [-1, 1]) {
2102
+ const bar = new THREE.Mesh(new THREE.PlaneGeometry(0.06, b - a), material);
2103
+ bar.rotation.x = -Math.PI / 2;
2104
+ bar.position.set(side * half, 0.03, tail - (a + b) / 2);
2105
+ bar.userData.side = side;
2106
+ bar.userData.mid = (a + b) / 2;
2107
+ group.add(bar);
2108
+ }
2109
+ const cross = new THREE.Mesh(new THREE.PlaneGeometry(half * 2, 0.05), material);
2110
+ cross.rotation.x = -Math.PI / 2;
2111
+ cross.position.set(0, 0.03, tail - a);
2112
+ cross.userData.mid = a;
2113
+ group.add(cross);
2114
+ }
2115
+ group.traverse((o) => o.layers.set(1));
2116
+ return group;
2117
+ };
2118
+ const bendGuides = (steer) => {
2119
+ if (!car) return;
2120
+ for (const bar of car.guides.children) {
2121
+ const d = bar.userData.mid;
2122
+ const offset = -steer * 0.18 * d * d;
2123
+ bar.position.x = (bar.userData.side ?? 0) * (car.built.width / 2 - 0.05) + offset;
2124
+ }
2125
+ };
2126
+ const pose = (s, steer) => {
2127
+ if (!car) return;
2128
+ const wrapped = (s.x % TRACK.tile + TRACK.tile) % TRACK.tile;
2129
+ if (Math.abs(wrapped - lastWrap) > TRACK.tile / 2) rig.vehicle?.reset();
2130
+ lastWrap = wrapped;
2131
+ car.built.group.position.set(TRACK.x0 + wrapped, 0, TRACK.z + s.z);
2132
+ car.built.group.rotation.set(-s.pitch, s.yaw + Math.PI / 2, s.roll, "YXZ");
2133
+ bendGuides(steer);
2134
+ };
2135
+ const frameOf = (speed) => {
2136
+ if (!car) return null;
2137
+ const g = car.built.group;
2138
+ 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 };
2139
+ };
2140
+ const applyCamera = () => {
2141
+ if (!host?.setViewCamera) return;
2142
+ host.setViewCamera(settings.camera === "room" ? null : rig.camera);
2143
+ if (settings.camera !== "room") rig.vehicle?.setMode(settings.camera);
2144
+ };
2145
+ const refreshMirrors = () => {
2146
+ for (const m of rig.mirrors) m.remove();
2147
+ rig.mirrors = [];
2148
+ if (!host?.addView || !car) return;
2149
+ const poses = mirrorPoses(car.built);
2150
+ const want = (id) => id === "reverse" ? !!host?.bench?.layer("reverse_camera") : !!host?.bench?.layer("mirrors");
2151
+ for (const pose2 of poses) {
2152
+ if (!want(pose2.id)) continue;
2153
+ const camera = new three.PerspectiveCamera(pose2.fov, 2, 0.05, 400);
2154
+ if (pose2.id === "reverse") camera.layers.enable(1);
2155
+ const rect = MIRROR_RECTS[pose2.id];
2156
+ const remove = host.addView({ camera, rect, mirror: pose2.mirror, rounded: rect.rounded, label: pose2.id, resolution: pose2.id === "reverse" ? 0.6 : 0.45 });
2157
+ rig.mirrors.push({ pose: pose2, camera, remove });
2158
+ }
2159
+ };
2160
+ let mirrorKey = "";
2161
+ const stopEngine = () => {
2162
+ engine?.dispose();
2163
+ engine = void 0;
2164
+ };
2165
+ const startTest = (id) => {
2166
+ const test = tests.get(id);
2167
+ if (!host || !test || test.kind !== "measured") return;
2168
+ if (!car || car.subject.id !== settings.vehicle) placeCar();
2169
+ if (!car) return;
2170
+ parking = null;
2171
+ run = startRun(test, car.subject);
2172
+ setSurface(run.surface);
2173
+ if (id === "turn_circle") run.driver.place(0, TRACK.start, 0);
2174
+ rig.vehicle?.reset();
2175
+ stopEngine();
2176
+ engine = createVehicleSound(host.audio, { sounds });
2177
+ engine.start([car.built.group.position.x, 0.5, car.built.group.position.z]);
2178
+ host.log(fillText(text.log.start, name(settings.vehicle), text.tests[id] ?? id));
2179
+ drawBoard();
2180
+ };
2181
+ const finishTest = () => {
2182
+ if (!run || !car) return;
2183
+ const result = run.result();
2184
+ const cert = certificateOf(car.subject.id);
2185
+ if (cert) store(withResult(cert, result));
2186
+ host?.log(fillText(text.log.result, name(car.subject.id), text.tests[result.test] ?? result.test, result.value ?? "\u2014", result.unit, fmtRange(result.range), result.verdict), { vehicle: car.subject.id, ...result, trace: void 0 }, "proving_result");
2187
+ run = null;
2188
+ stopEngine();
2189
+ const next = queue.shift();
2190
+ if (next) startTest(next);
2191
+ else setSurface("dry_asphalt");
2192
+ };
2193
+ const startParking = () => {
2194
+ if (!host) return;
2195
+ if (!car || car.subject.id !== settings.vehicle) placeCar();
2196
+ if (!car) return;
2197
+ run = null;
2198
+ queue = [];
2199
+ setSurface("dry_asphalt");
2200
+ const driver = createSemiSimDriver3(subjectSpec(car.subject));
2201
+ driver.place(40, 0, 0);
2202
+ parking = { driver, time: 0 };
2203
+ };
2204
+ const onChange = (change) => {
2205
+ if (change.kind === "layer") {
2206
+ if (change.id === "scoreboard" && board) board.mesh.visible = !!host?.bench?.layer("scoreboard");
2207
+ return refreshMirrors();
2208
+ }
2209
+ if (change.kind === "action") {
2210
+ if (change.id === "run_test") {
2211
+ queue = [];
2212
+ startTest(settings.test);
2213
+ } else if (change.id === "run_all") {
2214
+ queue = measured.slice(1);
2215
+ startTest(measured[0]);
2216
+ } else if (change.id === "certify_now") {
2217
+ const subject = subjectOf(settings.vehicle);
2218
+ if (!subject) return;
2219
+ const prior = certificateOf(subject.id);
2220
+ const signOffs = (prior?.results ?? []).filter((r) => r.kind !== "measured" && r.verdict !== "pending").map((r) => ({ vehicle: subject.id, test: r.test, ok: r.verdict === "pass", by: r.signedBy ?? "test room" }));
2221
+ const cert = certify(subject, { tests, signOffs });
2222
+ store(cert);
2223
+ host?.log(fillText(text.log.certified, name(subject.id), text.status[cert.status]), { vehicle: subject.id, status: cert.status }, "proving_certificate");
2224
+ } else if (change.id === "reverse_park") startParking();
2225
+ else if (change.id === "sign_ok" || change.id === "sign_fail") {
2226
+ const cert = certificateOf(settings.vehicle);
2227
+ if (!cert) return;
2228
+ const ok = change.id === "sign_ok";
2229
+ store(withResult(cert, { test: settings.visual, kind: "visual", unit: "", range: {}, value: null, verdict: ok ? "pass" : "fail", signedBy: "test room" }));
2230
+ host?.log(fillText(text.log.signed, name(settings.vehicle), text.tests[settings.visual] ?? settings.visual, ok ? text.verdict.pass : text.verdict.fail));
2231
+ } else if (change.id === "reset") {
2232
+ const subject = subjectOf(settings.vehicle);
2233
+ if (subject) store(blank(subject));
2234
+ }
2235
+ return;
2236
+ }
2237
+ switch (change.id) {
2238
+ case "vehicle":
2239
+ settings.vehicle = String(change.value);
2240
+ run = null;
2241
+ queue = [];
2242
+ parking = null;
2243
+ stopEngine();
2244
+ placeCar();
2245
+ refreshMirrors();
2246
+ break;
2247
+ case "test":
2248
+ settings.test = String(change.value);
2249
+ break;
2250
+ case "camera":
2251
+ settings.camera = String(change.value);
2252
+ applyCamera();
2253
+ break;
2254
+ case "time_scale":
2255
+ settings.timeScale = Number(change.value);
2256
+ break;
2257
+ case "visual_check":
2258
+ settings.visual = String(change.value);
2259
+ break;
2260
+ default:
2261
+ break;
2262
+ }
2263
+ };
2264
+ const frame = (dt) => {
2265
+ if (!host || !open) return;
2266
+ if (!track) buildTrack();
2267
+ if (!car) {
2268
+ placeCar();
2269
+ refreshMirrors();
2270
+ }
2271
+ let speed = 0;
2272
+ let steer = 0;
2273
+ if (run) {
2274
+ run.step(dt * settings.timeScale);
2275
+ const s = run.driver.state;
2276
+ pose(s, s.steerAngle);
2277
+ speed = s.u;
2278
+ steer = s.steerAngle;
2279
+ if (run.done) finishTest();
2280
+ } else if (parking) {
2281
+ parking.time += dt;
2282
+ const p = parking;
2283
+ p.driver.step({ throttle: 0, brake: p.time < 7 ? 0.35 : 1, steer: p.time > 2 ? -0.6 : 0, handbrake: false }, dt);
2284
+ pose(p.driver.state, p.driver.state.steerAngle);
2285
+ speed = p.driver.state.u;
2286
+ steer = p.driver.state.steerAngle;
2287
+ if (p.time > 9) parking = null;
2288
+ }
2289
+ if (car) engine?.update({ dt, position: [car.built.group.position.x, 0.5, car.built.group.position.z], speed, throttle: run ? 1 : 0 });
2290
+ const f = frameOf(speed);
2291
+ if (f && rig.vehicle && settings.camera !== "room") rig.vehicle.update(rig.camera, f, dt);
2292
+ if (f && rig.vehicle) for (const m of rig.mirrors) rig.vehicle.placeMirror(m.camera, m.pose, f);
2293
+ void steer;
2294
+ const key = `${host.bench?.layer("mirrors")}|${host.bench?.layer("reverse_camera")}`;
2295
+ if (key !== mirrorKey) {
2296
+ mirrorKey = key;
2297
+ refreshMirrors();
2298
+ }
2299
+ if (run && Math.floor(run.time * 2) !== Math.floor((run.time - dt * settings.timeScale) * 2)) drawBoard();
2300
+ };
2301
+ const speedNow = () => Math.abs(run?.driver.state.u ?? parking?.driver.state.u ?? 0) * 3.6;
2302
+ const counts = (verdict) => certificateOf(settings.vehicle)?.results.filter((r) => r.verdict === verdict).length ?? 0;
2303
+ const teardown = () => {
2304
+ stopEngine();
2305
+ for (const m of rig.mirrors) m.remove();
2306
+ rig.mirrors = [];
2307
+ removeCar();
2308
+ mirrorKey = "";
2309
+ if (track) discardObject3D2(track);
2310
+ track = null;
2311
+ strip = null;
2312
+ board = null;
2313
+ run = null;
2314
+ parking = null;
2315
+ };
2316
+ return {
2317
+ id: PROVING_AREA_ID,
2318
+ name: text.name,
2319
+ group: text.name,
2320
+ ground: { surface: "road" },
2321
+ certificates: () => [...certificates.values()],
2322
+ certificate: (id) => certificateOf(id),
2323
+ items: () => [],
2324
+ area: () => {
2325
+ open = true;
2326
+ applyCamera();
2327
+ return { placements: [], view: { eye: [TRACK.x0 - 4, 3.2, TRACK.z + 9], target: [TRACK.x0 + 6, 2.2, TRACK.z - TRACK.width / 2 - 3] } };
2328
+ },
2329
+ controls: () => [
2330
+ { kind: "choice", id: "vehicle", name: text.controls.vehicle, hint: text.hints.vehicle, options: vehicles.map((id) => ({ id, name: name(id) })), default: settings.vehicle },
2331
+ { 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 },
2332
+ { 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 },
2333
+ { kind: "number", id: "time_scale", name: text.controls.time_scale, hint: text.hints.time_scale, min: 1, max: 20, step: 1, default: settings.timeScale },
2334
+ { 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 },
2335
+ { kind: "action", id: "run_test", name: text.actions.run_test },
2336
+ { kind: "action", id: "run_all", name: text.actions.run_all },
2337
+ { kind: "action", id: "certify_now", name: text.actions.certify_now },
2338
+ { kind: "action", id: "reverse_park", name: text.actions.reverse_park },
2339
+ { kind: "action", id: "sign_ok", name: text.actions.sign_ok },
2340
+ { kind: "action", id: "sign_fail", name: text.actions.sign_fail },
2341
+ { kind: "action", id: "reset", name: text.actions.reset }
2342
+ ],
2343
+ layers: () => [
2344
+ { id: "mirrors", name: text.layers.mirrors, default: false },
2345
+ { id: "reverse_camera", name: text.layers.reverse_camera, default: false },
2346
+ { id: "scoreboard", name: text.layers.scoreboard, default: true }
2347
+ ],
2348
+ metrics: () => [
2349
+ { id: "speed", name: text.metrics.speed, unit: "km/h", digits: 0, read: speedNow },
2350
+ { id: "time", name: text.metrics.time, unit: "s", digits: 1, read: () => run?.time ?? 0 },
2351
+ { id: "value", name: text.metrics.value, digits: 1, read: () => run ? run.trace[run.trace.length - 1]?.[1] ?? 0 : 0 },
2352
+ { id: "passed", name: text.metrics.passed, read: () => counts("pass") },
2353
+ { id: "failed", name: text.metrics.failed, read: () => counts("fail") },
2354
+ { id: "pending", name: text.metrics.pending, read: () => counts("pending") },
2355
+ { id: "status", name: text.metrics.status, read: () => ({ approved: 1, rejected: -1, pending: 0 })[certificateOf(settings.vehicle)?.status ?? "pending"] }
2356
+ ],
2357
+ scenarios: () => [],
2358
+ attach(lent) {
2359
+ host = lent;
2360
+ three = lent.THREE;
2361
+ rig.camera = new three.PerspectiveCamera(60, 1.6, 0.05, 900);
2362
+ rig.vehicle = createVehicleCamera(three);
2363
+ lent.audio?.register?.(LIBRARY_BANK2);
2364
+ const stopChange = lent.bench?.onChange(onChange);
2365
+ const stopFrame = lent.onFrame(frame);
2366
+ const stopClear = lent.onClear?.(() => {
2367
+ open = false;
2368
+ teardown();
2369
+ lent.setViewCamera?.(null);
2370
+ });
2371
+ return () => {
2372
+ stopChange?.();
2373
+ stopFrame();
2374
+ stopClear?.();
2375
+ teardown();
2376
+ lent.setViewCamera?.(null);
2377
+ host = null;
2378
+ };
2379
+ }
2380
+ };
2381
+ }
2382
+ function loadProvingModule(room, options) {
2383
+ return room.loadModule(createProvingModule(options));
2384
+ }
2385
+
1433
2386
  export {
1434
2387
  LITE_CLASSES,
1435
2388
  PRUNE_BELOW,
@@ -1440,6 +2393,22 @@ export {
1440
2393
  liteMeshes,
1441
2394
  createVehiclesThree,
1442
2395
  createCabinSmoke,
2396
+ VEHICLE_LOOP_FALLOFF,
2397
+ VEHICLE_LOOPS,
2398
+ VEHICLE_SOUNDS,
2399
+ VEHICLE_SOUND_BANK,
2400
+ firingHz,
2401
+ rpmShare,
2402
+ layerWeight,
2403
+ engineMix,
2404
+ createGearGuess,
2405
+ DEFAULT_VEHICLE_SOUND_KEYS,
2406
+ CRASH_SPEEDS,
2407
+ CAR_ENGINE,
2408
+ TRUCK_ENGINE,
2409
+ BIKE_ENGINE,
2410
+ ENGINE_PROFILES,
2411
+ createVehicleSound,
1443
2412
  VEHICLE_LOD_DISTANCE,
1444
2413
  createVehicleLod,
1445
2414
  VEHICLE_SCENARIO_IDS,
@@ -1451,6 +2420,10 @@ export {
1451
2420
  VEHICLES_AREA_ID,
1452
2421
  DEFAULT_TEST_VEHICLES,
1453
2422
  createVehiclesTestModule,
1454
- loadVehiclesTestModule
2423
+ loadVehiclesTestModule,
2424
+ PROVING_TEXT,
2425
+ PROVING_AREA_ID,
2426
+ createProvingModule,
2427
+ loadProvingModule
1455
2428
  };
1456
- //# sourceMappingURL=chunk-GAO7ZJP7.js.map
2429
+ //# sourceMappingURL=chunk-4SUQMRS3.js.map