@adatechnology/game-vehicles-three 0.6.0 → 0.6.2

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.
@@ -24,10 +24,12 @@ var GameVehiclesThree = (() => {
24
24
  BIKE_ENGINE: () => BIKE_ENGINE,
25
25
  CAR_ENGINE: () => CAR_ENGINE,
26
26
  CRASH_SPEEDS: () => CRASH_SPEEDS,
27
+ DASHBOARD_LOOK: () => DASHBOARD_LOOK,
27
28
  DEFAULT_TEST_VEHICLES: () => DEFAULT_TEST_VEHICLES,
28
29
  DEFAULT_TEST_WRECKS: () => DEFAULT_TEST_WRECKS,
29
30
  DEFAULT_VEHICLE_SOUND_KEYS: () => DEFAULT_VEHICLE_SOUND_KEYS,
30
31
  ENGINE_PROFILES: () => ENGINE_PROFILES,
32
+ LAMP_RULES: () => LAMP_RULES,
31
33
  LITE_CLASSES: () => LITE_CLASSES,
32
34
  PAGE_ITEM_ALIASES: () => PAGE_ITEM_ALIASES,
33
35
  PAGE_SCENARIO_ALIASES: () => PAGE_SCENARIO_ALIASES,
@@ -47,6 +49,8 @@ var GameVehiclesThree = (() => {
47
49
  createCabinSmoke: () => createCabinSmoke,
48
50
  createGearGuess: () => createGearGuess,
49
51
  createProvingModule: () => createProvingModule,
52
+ createVehicleDashboard: () => createVehicleDashboard,
53
+ createVehicleLamps: () => createVehicleLamps,
50
54
  createVehicleLod: () => createVehicleLod,
51
55
  createVehicleSound: () => createVehicleSound,
52
56
  createVehiclesTestModule: () => createVehiclesTestModule,
@@ -65,6 +69,7 @@ var GameVehiclesThree = (() => {
65
69
  });
66
70
 
67
71
  // ../game-contracts/dist/index.js
72
+ var DEFAULT_BLAST_IMPULSE = { bodyArea: 0.7, lift: 0.15, minSpeed: 0.3, tntPerJoule: 0.24 / 2e5 };
68
73
  var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
69
74
  var DEFAULT_IGNITION_RULES = {
70
75
  fillCurve: [
@@ -6137,7 +6142,9 @@ var GameVehiclesThree = (() => {
6137
6142
  function fogLamp(c, s, r) {
6138
6143
  onSurf(c.cylinder(r * 1.4, r * 1.4, 0.014, LH, 0, 0, 0), s, 6e-3).castShadow = false;
6139
6144
  onSurf(c.cylinder(r, r, 0.012, REFL, 0, 0, 0), s, 0.012).castShadow = false;
6140
- onSurf(c.sphere(r * 0.45, MT.head, 0, 0, 0, 1, 0.5, 1), s, 0.016).castShadow = false;
6145
+ const bulb = onSurf(c.sphere(r * 0.45, MT.head, 0, 0, 0, 1, 0.5, 1), s, 0.016);
6146
+ bulb.castShadow = false;
6147
+ (c.fogBulbs ?? (c.fogBulbs = [])).push(bulb);
6141
6148
  }
6142
6149
  function crease(c, I, sd, pts, o) {
6143
6150
  o = o || {};
@@ -15139,7 +15146,9 @@ var GameVehiclesThree = (() => {
15139
15146
  function fogLamp(c, s, r) {
15140
15147
  onSurf(c.cylinder(r * 1.4, r * 1.4, 0.014, LH, 0, 0, 0), s, 6e-3).castShadow = false;
15141
15148
  onSurf(c.cylinder(r, r, 0.012, REFL, 0, 0, 0), s, 0.012).castShadow = false;
15142
- onSurf(c.sphere(r * 0.45, MT.head, 0, 0, 0, 1, 0.5, 1), s, 0.016).castShadow = false;
15149
+ const bulb = onSurf(c.sphere(r * 0.45, MT.head, 0, 0, 0, 1, 0.5, 1), s, 0.016);
15150
+ bulb.castShadow = false;
15151
+ (c.fogBulbs ?? (c.fogBulbs = [])).push(bulb);
15143
15152
  }
15144
15153
  function crease(c, I, sd, pts, o) {
15145
15154
  o = o || {};
@@ -22348,7 +22357,7 @@ var GameVehiclesThree = (() => {
22348
22357
  const x = kit.profX(s.lower, s.bottomY + 0.14, true) + 0.09;
22349
22358
  [1, -1].forEach(function(sd) {
22350
22359
  c.roundBox(0.04, r * 2.6, r * 3.2, 0.015, MT.housing, x, y, sd * z);
22351
- kit.headlight(c, {
22360
+ const lamp = kit.headlight(c, {
22352
22361
  style: "round",
22353
22362
  x: x + 0.01,
22354
22363
  y,
@@ -22360,6 +22369,7 @@ var GameVehiclesThree = (() => {
22360
22369
  label: sd > 0 ? trl("labels.right_fog_light") : trl("labels.left_fog_light"),
22361
22370
  reach: 0.5
22362
22371
  });
22372
+ lamp.fog = true;
22363
22373
  });
22364
22374
  }
22365
22375
  function roofBars(c, z, h, r) {
@@ -27987,6 +27997,16 @@ var GameVehiclesThree = (() => {
27987
27997
  });
27988
27998
  return best;
27989
27999
  }
28000
+ var SEATED_EYE = { up: 0.55, back: 0.28 };
28001
+ function eyeOf(THREE, c) {
28002
+ const tops = c.seatTops ?? [];
28003
+ if (!tops.length) return null;
28004
+ const points = tops.map((t) => c.vehicle.localToWorld(new THREE.Vector3(t.x, t.y, t.z)));
28005
+ const front = Math.max(...points.map((p) => p.z));
28006
+ const row = points.filter((p) => p.z > front - 0.25);
28007
+ const seat = row.reduce((a, b) => b.x > a.x ? b : a);
28008
+ return [+seat.x.toFixed(3), +(seat.y + SEATED_EYE.up).toFixed(3), +(seat.z - SEATED_EYE.back).toFixed(3)];
28009
+ }
27990
28010
  function createVehiclesThree(THREE, options) {
27991
28011
  const kit = options.kit;
27992
28012
  const fleet = options.fleet ?? { colorFromSeed, fleetDims, paintPalettes, paletteOf };
@@ -28022,6 +28042,7 @@ var GameVehiclesThree = (() => {
28022
28042
  markKitOrigin(group, { owner: "game-vehicles-three", kind: "vehicle", id: key });
28023
28043
  const damageables = kit.damageables(c, pivotMatrix);
28024
28044
  let pruned = 0;
28045
+ let driverEye = null;
28025
28046
  if (quality === "high") {
28026
28047
  const pivot = new THREE.Group();
28027
28048
  pivot.name = "vehicle";
@@ -28043,6 +28064,7 @@ var GameVehiclesThree = (() => {
28043
28064
  }
28044
28065
  markDamageables(c, damageables);
28045
28066
  group.updateMatrixWorld(true);
28067
+ driverEye = eyeOf(THREE, c);
28046
28068
  } else {
28047
28069
  const fusion = fuse(THREE, c, pivotMatrix, color2 ? new THREE.Color(color2) : null, primary, kit.lodHiddenMaterials());
28048
28070
  for (const mesh2 of liteMeshes(THREE, fusion, liteMaterials(THREE, kit.studioEnvironment()))) group.add(mesh2);
@@ -28068,6 +28090,7 @@ var GameVehiclesThree = (() => {
28068
28090
  pruned,
28069
28091
  expected: fleetDims2(key),
28070
28092
  context: quality === "high" ? c : null,
28093
+ driverEye,
28071
28094
  dispose() {
28072
28095
  group.traverse((m) => {
28073
28096
  if (m.geometry) m.geometry.dispose();
@@ -44012,7 +44035,7 @@ var GameVehiclesThree = (() => {
44012
44035
  };
44013
44036
  }
44014
44037
 
44015
- // ../game-audio/dist/chunk-7TLFY7VO.js
44038
+ // ../game-audio/dist/chunk-KBTUANNG.js
44016
44039
  var DISTANCE_CURVES = {
44017
44040
  /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
44018
44041
  game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
@@ -44313,7 +44336,11 @@ var GameVehiclesThree = (() => {
44313
44336
  ["casing_drop_terra_pistol", 1],
44314
44337
  ["casing_drop_terra_rifle", 1],
44315
44338
  ["casing_drop_terra_shell", 1],
44316
- ["rocket_fire", 2]
44339
+ ["rocket_fire", 2],
44340
+ ["smg_45_auto", 2, "sfx_pistol"],
44341
+ ["weapon_draw", 2],
44342
+ ["suppressor_attach", 3],
44343
+ ["suppressor_detach", 3]
44317
44344
  ]),
44318
44345
  ...entries("explosoes-fogo", [
44319
44346
  ["explosion_grenade", 1, "boom"],
@@ -44327,7 +44354,11 @@ var GameVehiclesThree = (() => {
44327
44354
  ["fire_loop_large", 1],
44328
44355
  ["fire_extinguish", 2, "hiss"],
44329
44356
  ["gas_leak_hiss", 2],
44330
- ["structure_collapse", 2, "crumble"]
44357
+ ["structure_collapse", 2, "crumble"],
44358
+ ["grenade_bounce", 1],
44359
+ ["c4_plant", 2],
44360
+ ["c4_beep", 2],
44361
+ ["c4_disarm", 2]
44331
44362
  ]),
44332
44363
  ...entries("personagem", [
44333
44364
  ["footstep_concrete_walk", 1, "sfx_step"],
@@ -44343,7 +44374,19 @@ var GameVehiclesThree = (() => {
44343
44374
  ["blunt_hit", 1],
44344
44375
  ["bite", 1, "sfx_flesh"],
44345
44376
  ["body_fall", 1],
44346
- ["player_hurt", 2]
44377
+ ["player_hurt", 2],
44378
+ ["footstep_tile_walk", 1, "sfx_step"],
44379
+ ["footstep_water_walk", 1, "sfx_step"],
44380
+ ["footstep_grate_walk", 2, "sfx_step"],
44381
+ ["footstep_duct_walk", 3, "sfx_step"],
44382
+ ["ladder_climb", 2],
44383
+ ["swim", 2],
44384
+ ["zombie_groan", 1],
44385
+ ["zombie_alert", 1],
44386
+ ["zombie_attack", 1],
44387
+ ["zombie_pain", 1],
44388
+ ["zombie_claw_hit", 1, "sfx_flesh"],
44389
+ ["zombie_claw_miss", 2]
44347
44390
  ]),
44348
44391
  ...entries("veiculos", [
44349
44392
  ["engine_loop_car", 1],
@@ -44371,7 +44414,8 @@ var GameVehiclesThree = (() => {
44371
44414
  ["wind_loop", 2],
44372
44415
  ["thunder_near", 2],
44373
44416
  ["thunder_far", 2],
44374
- ["water_splash", 2]
44417
+ ["water_splash", 2],
44418
+ ["helicopter_loop", 3]
44375
44419
  ])
44376
44420
  ];
44377
44421
  var SFX_MATERIALS = [
@@ -44460,6 +44504,7 @@ var GameVehiclesThree = (() => {
44460
44504
  category: "1 \xB7 Categoria",
44461
44505
  sound: "1 \xB7 Som",
44462
44506
  play: "1 \xB7 \u25B6 Tocar",
44507
+ play_user: "1 \xB7 \u25B6 Tocar o seu",
44463
44508
  material: "2 \xB7 Material",
44464
44509
  blow: "2 \xB7 Golpe",
44465
44510
  strike: "2 \xB7 \u25B6 Tocar golpe",
@@ -44481,6 +44526,7 @@ var GameVehiclesThree = (() => {
44481
44526
  approve: "5 \xB7 Aprovo (C10)"
44482
44527
  },
44483
44528
  hints: {
44529
+ play_user: "Toca a sua grava\xE7\xE3o deste som (dos seus pacotes, fora do git), mesmo quando o SDK tem uma melhor e toca na frente.",
44484
44530
  sound: "A = arquivo, S = sintetizado, F = faltando. O que tem arquivo toca o arquivo; o resto toca o som atual.",
44485
44531
  strike: "Toca impact_<material>_<golpe>: o arquivo se existir, sen\xE3o o som atual daquele material.",
44486
44532
  wall: "P\xF5e a parede de casa entre voc\xEA e a fonte: o som fica abafado (C4).",
@@ -44525,9 +44571,10 @@ var GameVehiclesThree = (() => {
44525
44571
  strengths: { light: "leve", medium: "m\xE9dio", heavy: "forte" },
44526
44572
  states: { file: "A", synth: "S", missing: "F" },
44527
44573
  criteria: { C1: "C1 Sincronia", C2: "C2 Certo p/ a\xE7\xE3o", C3: "C3 Dist\xE2ncia", C4: "C4 Posi\xE7\xE3o", C5: "C5 Varia\xE7\xE3o", C6: "C6 Volume", C7: "C7 Intensidade", C8: "C8 Cauda", C9: "C9 Rajada", C10: "C10 Escuta" },
44528
- metrics: { chosen: "1 \xB7 Escolhido", matrix: "2 \xB7 Matriz", last: "\xDAltimo tocado", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Prefer\xEAncias", state: "5 \xB7 Medi\xE7\xE3o", count: "1 \xB7 Cat\xE1logo" },
44574
+ metrics: { source: "1 \xB7 Fonte do que tocou", chosen: "1 \xB7 Escolhido", matrix: "2 \xB7 Matriz", last: "\xDAltimo tocado", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Prefer\xEAncias", state: "5 \xB7 Medi\xE7\xE3o", count: "1 \xB7 Cat\xE1logo" },
44529
44575
  run: { idle: "parada", measuring: "medindo\u2026", done: "pronta", failed: "falhou", recording: "gravando\u2026", recorded: "grava\xE7\xE3o baixada" },
44530
44576
  words: { none: "\u2014", hidden: "? (revele)", prefers: "prefere", approved: "aprovado pelo dono" },
44577
+ sources: { sdk: "SDK", user: "seu som", synth: "sintetizado" },
44531
44578
  probe: "Fonte do som",
44532
44579
  wall: "Parede (oclus\xE3o)",
44533
44580
  scenarios: { pipeline: "Arquivo \u2192 decodifica \u2192 toca (round-robin, cauda)", criteria: "Crit\xE9rios C1\u2013C9 no som de calibra\xE7\xE3o", loop: "Loop de arquivo: emenda sem buraco (crossfade)", panel: "Painel: categorias, matriz, A/B e parede" }
@@ -44540,6 +44587,7 @@ var GameVehiclesThree = (() => {
44540
44587
  category: "1 \xB7 Category",
44541
44588
  sound: "1 \xB7 Sound",
44542
44589
  play: "1 \xB7 \u25B6 Play",
44590
+ play_user: "1 \xB7 \u25B6 Play yours",
44543
44591
  material: "2 \xB7 Material",
44544
44592
  blow: "2 \xB7 Blow",
44545
44593
  strike: "2 \xB7 \u25B6 Play blow",
@@ -44561,6 +44609,7 @@ var GameVehiclesThree = (() => {
44561
44609
  approve: "5 \xB7 Approve (C10)"
44562
44610
  },
44563
44611
  hints: {
44612
+ play_user: "Plays your own take of this sound (from your packs, kept out of git), even where the SDK has a better one that plays first.",
44564
44613
  sound: "A = file, S = synthesized, F = missing. A file plays the file; the rest plays the current sound.",
44565
44614
  strike: "Plays impact_<material>_<blow>: the file if there is one, else the current sound of that material.",
44566
44615
  wall: "Puts the house wall between you and the source: the sound is muffled (C4).",
@@ -44595,9 +44644,10 @@ var GameVehiclesThree = (() => {
44595
44644
  environments: { indoor: "indoor", outdoor: "outdoor" },
44596
44645
  strengths: { light: "light", medium: "medium", heavy: "heavy" },
44597
44646
  criteria: { C1: "C1 Sync", C2: "C2 Right sound", C3: "C3 Distance", C4: "C4 Position", C5: "C5 Variation", C6: "C6 Level", C7: "C7 Strength", C8: "C8 Tail", C9: "C9 Burst", C10: "C10 Listening" },
44598
- metrics: { chosen: "1 \xB7 Chosen", matrix: "2 \xB7 Matrix", last: "Last played", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Preferences", state: "5 \xB7 Measurement", count: "1 \xB7 Catalogue" },
44647
+ metrics: { source: "1 \xB7 Source of what played", chosen: "1 \xB7 Chosen", matrix: "2 \xB7 Matrix", last: "Last played", ab: "4 \xB7 A \xD7 B", blind: "4 \xB7 X", verdicts: "4 \xB7 Preferences", state: "5 \xB7 Measurement", count: "1 \xB7 Catalogue" },
44599
44648
  run: { idle: "idle", measuring: "measuring\u2026", done: "done", failed: "failed", recording: "recording\u2026", recorded: "recording downloaded" },
44600
44649
  words: { none: "\u2014", hidden: "? (reveal)", prefers: "prefers", approved: "approved by the owner" },
44650
+ sources: { sdk: "SDK", user: "your sound", synth: "synthesized" },
44601
44651
  probe: "Sound source",
44602
44652
  wall: "Wall (occlusion)",
44603
44653
  scenarios: { pipeline: "File \u2192 decode \u2192 play (round-robin, tail)", criteria: "Criteria C1\u2013C9 on the calibration sound", loop: "File loop: seamless joins (crossfade)", panel: "Panel: categories, matrix, A/B and wall" }
@@ -44679,6 +44729,80 @@ var GameVehiclesThree = (() => {
44679
44729
  sources: [`cena:${line}`]
44680
44730
  };
44681
44731
  }
44732
+ var CABIN_SOUNDS = [
44733
+ {
44734
+ id: "blinker_tick",
44735
+ bus: "world",
44736
+ duration: 0.03,
44737
+ layers: [
44738
+ // the relay: a click of bright noise and a short low knock
44739
+ { source: { kind: "noise", offset: "random" }, filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 2600 } }] }, envelope: [[0, 0.22, "set"], [0.025, 1e-4, "exp", "fixed"]] },
44740
+ { source: { kind: "osc", wave: "triangle", frequency: { set: 180 } }, envelope: [[0, 0.12, "set"], [0.03, 1e-4, "exp", "fixed"]] }
44741
+ ],
44742
+ falloff: { model: "none" }
44743
+ },
44744
+ {
44745
+ id: "starter_crank",
44746
+ bus: "world",
44747
+ duration: 0.9,
44748
+ layers: [
44749
+ // the starter motor turning the engine over: low noise chopped at the compression strokes
44750
+ {
44751
+ source: { kind: "noise", offset: "random" },
44752
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 420 } }] },
44753
+ modulators: [{ target: "gain", wave: "square", rate: 11, depth: 0.08, base: 0.1 }],
44754
+ envelope: [[0, 1e-4, "set", "fixed"], [0.05, 0.25, "linear"], [0.8, 0.22, "set"], [0.9, 1e-4, "exp", "fixed"]]
44755
+ },
44756
+ { source: { kind: "osc", wave: "sawtooth", frequency: { set: 95, to: 120, curve: "linear", over: 0.9 } }, filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 300 } }] }, envelope: [[0, 0.04, "set"], [0.9, 1e-4, "exp", "fixed"]] }
44757
+ ],
44758
+ falloff: { model: "none" }
44759
+ },
44760
+ {
44761
+ id: "car_alarm",
44762
+ bus: "world",
44763
+ order: "siren",
44764
+ duration: 3,
44765
+ tail: 0.1,
44766
+ layers: [
44767
+ // the whoop of a car alarm: a square swept up and down three times a second
44768
+ {
44769
+ source: { kind: "osc", wave: "square", frequency: { set: 1100 } },
44770
+ modulators: [{ target: "frequency", wave: "triangle", rate: 3, depth: 420 }],
44771
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 2600 } }] },
44772
+ envelope: [[0, 1e-4, "set", "fixed"], [0.05, 0.05, "linear"], [2.9, 0.05, "set"], [3, 1e-4, "exp", "fixed"]]
44773
+ }
44774
+ ],
44775
+ falloff: { model: "none" }
44776
+ },
44777
+ {
44778
+ id: "alarm_chirp",
44779
+ bus: "world",
44780
+ duration: 0.1,
44781
+ layers: [{ source: { kind: "osc", wave: "square", frequency: { set: 2300 } }, repeat: { count: 2, spacing: 0.16 }, envelope: [[0, 0.05, "set"], [0.09, 1e-4, "exp", "fixed"]] }],
44782
+ falloff: { model: "none" }
44783
+ },
44784
+ {
44785
+ id: "wiper_swish",
44786
+ bus: "world",
44787
+ duration: 0.45,
44788
+ layers: [
44789
+ {
44790
+ source: { kind: "noise", offset: "random" },
44791
+ filters: { mode: "serial", bank: [{ type: "bandpass", frequency: { set: 700, to: 1100, curve: "linear", over: 0.45 }, q: 1.2 }] },
44792
+ envelope: [[0, 1e-4, "set", "fixed"], [0.2, 0.06, "linear"], [0.45, 1e-4, "exp", "fixed"]]
44793
+ }
44794
+ ],
44795
+ falloff: { model: "none" }
44796
+ },
44797
+ {
44798
+ id: "seatbelt_chime",
44799
+ bus: "world",
44800
+ duration: 0.5,
44801
+ layers: [{ source: { kind: "osc", wave: "sine", frequency: { set: 880 } }, repeat: { count: 3, spacing: 0.7 }, envelope: [[0, 0.06, "set"], [0.5, 1e-4, "exp", "fixed"]] }],
44802
+ falloff: { model: "none" }
44803
+ }
44804
+ ];
44805
+ var CABIN_BANK = { id: "vehicle_cabin", sounds: CABIN_SOUNDS, loops: [] };
44682
44806
  var FOLEY_FALLOFF = {
44683
44807
  model: "inverse_square",
44684
44808
  k: 0.02,
@@ -45679,6 +45803,21 @@ var GameVehiclesThree = (() => {
45679
45803
 
45680
45804
  // ../game-render-three/dist/index.js
45681
45805
  var DEFAULT_ANGLE = Math.PI / 3;
45806
+ var LIGHT_PRIORITY = {
45807
+ blast: 900,
45808
+ rocket: 800,
45809
+ muzzle: 750,
45810
+ fire: 600,
45811
+ flare: 500,
45812
+ headlight: 400,
45813
+ weaponLight: 400,
45814
+ lamp: 200,
45815
+ street: 150
45816
+ };
45817
+ function isLightBudget(value) {
45818
+ const candidate = value;
45819
+ return typeof candidate === "object" && candidate !== null && typeof candidate.acquire === "function" && typeof candidate.addSource === "function" && typeof candidate.lease === "function" && typeof candidate.present === "function";
45820
+ }
45682
45821
  var EFFECT_STEP_SECONDS = 1 / 50;
45683
45822
  function isRenderPools(value) {
45684
45823
  const candidate = value;
@@ -46343,6 +46482,205 @@ var GameVehiclesThree = (() => {
46343
46482
  const brakeForce = base.brakeForce ?? body.mass * 9.81 * (carRatio + (0.6 - carRatio) * heavyShare);
46344
46483
  return { ...spec, mass: body.mass, wheels, fourWheelDrive: driveAll, engine, gears, drag, brakeForce, tyre: { ...spec.tyre, friction: 0.8 + 0.03 * stats2.handling } };
46345
46484
  }
46485
+ var DEFAULT_CONTROL_KEYS = {
46486
+ throttle: ["KeyW", "ArrowUp"],
46487
+ brake: ["KeyS", "ArrowDown"],
46488
+ left: ["KeyA", "ArrowLeft"],
46489
+ right: ["KeyD", "ArrowRight"],
46490
+ handbrake: ["Space"],
46491
+ horn: ["KeyH"],
46492
+ ignition: ["KeyI"],
46493
+ gearbox: ["KeyM"],
46494
+ gear_up: ["KeyE"],
46495
+ gear_down: ["KeyQ"],
46496
+ parking_brake: ["KeyP"],
46497
+ drive_mode: ["KeyG"],
46498
+ lights: ["KeyL"],
46499
+ high_beam: ["KeyK"],
46500
+ fog_lights: ["KeyN"],
46501
+ interior_light: ["KeyU"],
46502
+ alarm: ["KeyY"],
46503
+ radio: ["KeyR"],
46504
+ radio_source: ["KeyT"],
46505
+ exit: ["KeyF"],
46506
+ indicator_left: ["Comma"],
46507
+ indicator_right: ["Period"],
46508
+ hazard: ["KeyB"],
46509
+ wipers: ["KeyO"],
46510
+ seatbelt: ["KeyJ"]
46511
+ };
46512
+ function createVehicleControls(options) {
46513
+ const keys = { ...DEFAULT_CONTROL_KEYS, ...options.keys };
46514
+ const auto4 = options.autoFourWheel ?? { slip: 0.18, releaseSeconds: 2 };
46515
+ const hasFog = options.fogLamps !== false;
46516
+ const held = /* @__PURE__ */ new Set();
46517
+ const any = (codes) => codes.some((c) => held.has(c));
46518
+ const commandOf = /* @__PURE__ */ new Map();
46519
+ for (const [command2, codes] of Object.entries(keys)) if (!["throttle", "brake", "left", "right", "handbrake", "horn"].includes(command2)) for (const code of codes) commandOf.set(code, command2);
46520
+ const driving = new Set(Object.values(keys).flat());
46521
+ const state = {
46522
+ ignition: !!options.running,
46523
+ gearbox: options.gearbox ?? "automatic",
46524
+ gear: 1,
46525
+ parkingBrake: false,
46526
+ driveMode: options.fourWheelDrive ? "auto" : "4x2",
46527
+ fourWheelEngaged: false,
46528
+ lamps: { mode: "off", fog: false, interior: false },
46529
+ brakeLights: false,
46530
+ reverseLights: false,
46531
+ horn: false,
46532
+ alarm: "off",
46533
+ radio: { on: false, source: "radio" },
46534
+ indicator: "off",
46535
+ blinkOn: false,
46536
+ tick: false,
46537
+ wipers: "off",
46538
+ seatbelt: false
46539
+ };
46540
+ const blinkHz = options.blinkHz ?? 1.5;
46541
+ const cancelAt = options.indicatorCancel ?? 0.15;
46542
+ let blink = 0;
46543
+ let turned = 0;
46544
+ let throttle = 0;
46545
+ let brake = 0;
46546
+ let steer = 0;
46547
+ let calm = 0;
46548
+ const emit = (event) => options.onEvent?.(event);
46549
+ const LAMPS = ["off", "position", "low"];
46550
+ const MODES = ["4x2", "4x4", "auto"];
46551
+ const command = (c) => {
46552
+ switch (c) {
46553
+ case "ignition":
46554
+ state.ignition = !state.ignition;
46555
+ if (state.ignition && state.alarm !== "off") state.alarm = "off";
46556
+ break;
46557
+ case "gearbox":
46558
+ state.gearbox = state.gearbox === "automatic" ? "manual" : "automatic";
46559
+ state.gear = Math.max(1, state.gear);
46560
+ break;
46561
+ case "gear_up":
46562
+ case "gear_down":
46563
+ if (state.gearbox !== "manual") return;
46564
+ state.gear = Math.max(-1, Math.min(options.gears, state.gear + (c === "gear_up" ? 1 : -1)));
46565
+ break;
46566
+ case "parking_brake":
46567
+ state.parkingBrake = !state.parkingBrake;
46568
+ break;
46569
+ case "drive_mode": {
46570
+ if (!options.fourWheelDrive) return emit({ type: "refused", command: c, reason: "no_four_wheel_drive" });
46571
+ state.driveMode = MODES[(MODES.indexOf(state.driveMode) + 1) % MODES.length];
46572
+ state.fourWheelEngaged = state.driveMode === "4x4";
46573
+ break;
46574
+ }
46575
+ case "lights":
46576
+ state.lamps.mode = state.lamps.mode === "high" ? "off" : LAMPS[(LAMPS.indexOf(state.lamps.mode) + 1) % LAMPS.length];
46577
+ break;
46578
+ case "high_beam":
46579
+ state.lamps.mode = state.lamps.mode === "high" ? "low" : "high";
46580
+ break;
46581
+ case "fog_lights":
46582
+ if (!hasFog) return emit({ type: "refused", command: c, reason: "no_fog_lamps" });
46583
+ state.lamps.fog = !state.lamps.fog;
46584
+ if (state.lamps.fog && state.lamps.mode === "off") state.lamps.mode = "position";
46585
+ break;
46586
+ case "interior_light":
46587
+ state.lamps.interior = !state.lamps.interior;
46588
+ break;
46589
+ case "alarm":
46590
+ state.alarm = state.alarm === "off" ? "armed" : state.alarm === "armed" ? "ringing" : "off";
46591
+ break;
46592
+ case "radio":
46593
+ state.radio.on = !state.radio.on;
46594
+ break;
46595
+ case "radio_source":
46596
+ state.radio.source = state.radio.source === "radio" ? "tape" : "radio";
46597
+ break;
46598
+ case "exit":
46599
+ break;
46600
+ case "indicator_left":
46601
+ case "indicator_right": {
46602
+ const side2 = c === "indicator_left" ? "left" : "right";
46603
+ if (state.indicator === "hazard") return;
46604
+ state.indicator = state.indicator === side2 ? "off" : side2;
46605
+ turned = 0;
46606
+ blink = 0;
46607
+ break;
46608
+ }
46609
+ case "hazard":
46610
+ state.indicator = state.indicator === "hazard" ? "off" : "hazard";
46611
+ blink = 0;
46612
+ break;
46613
+ case "wipers":
46614
+ state.wipers = state.wipers === "off" ? "slow" : state.wipers === "slow" ? "fast" : "off";
46615
+ break;
46616
+ case "seatbelt":
46617
+ state.seatbelt = !state.seatbelt;
46618
+ break;
46619
+ }
46620
+ emit({ type: "command", command: c, state });
46621
+ };
46622
+ const ease = (from, to, rate, dt) => from + Math.sign(to - from) * Math.min(Math.abs(to - from), rate * dt);
46623
+ return {
46624
+ keys,
46625
+ state,
46626
+ key(code, down) {
46627
+ if (!driving.has(code)) return false;
46628
+ const was = held.has(code);
46629
+ if (down) held.add(code);
46630
+ else held.delete(code);
46631
+ const c = commandOf.get(code);
46632
+ if (c && down && !was) command(c);
46633
+ return true;
46634
+ },
46635
+ command,
46636
+ input(dt, driver) {
46637
+ const gas = any(keys.throttle) ? 1 : 0;
46638
+ const stop = any(keys.brake) ? 1 : 0;
46639
+ const wheel = (any(keys.left) ? 1 : 0) - (any(keys.right) ? 1 : 0);
46640
+ throttle = ease(throttle, gas, gas > throttle ? 4 : 8, dt);
46641
+ brake = ease(brake, stop, stop > brake ? 6 : 10, dt);
46642
+ steer = ease(steer, wheel, wheel !== 0 && Math.sign(wheel) === Math.sign(steer || wheel) ? 3 : 6, dt);
46643
+ const handbrake = state.parkingBrake || any(keys.handbrake);
46644
+ state.tick = false;
46645
+ if (state.indicator !== "off") {
46646
+ const before = Math.floor(blink * blinkHz * 2);
46647
+ blink += dt;
46648
+ const now = Math.floor(blink * blinkHz * 2);
46649
+ state.blinkOn = now % 2 === 0;
46650
+ state.tick = now !== before;
46651
+ if (state.indicator !== "hazard") {
46652
+ const way = state.indicator === "left" ? steer : -steer;
46653
+ if (way > 0.5) turned = 1;
46654
+ else if (turned && way < cancelAt) state.indicator = "off";
46655
+ }
46656
+ } else state.blinkOn = false;
46657
+ state.horn = any(keys.horn);
46658
+ const gasNow = state.ignition ? throttle : 0;
46659
+ const reversing = state.gearbox === "manual" ? state.gear < 0 : (driver?.state.gear ?? 1) < 0;
46660
+ state.reverseLights = reversing;
46661
+ state.brakeLights = handbrake || (reversing ? gasNow > 0.05 && (driver?.state.u ?? 0) < -0.3 : brake > 0.05);
46662
+ if (state.gearbox === "automatic") return { throttle: gasNow, brake, steer, handbrake, horn: state.horn };
46663
+ return state.gear < 0 ? { throttle: brake, brake: gasNow, steer, handbrake, horn: state.horn, gear: state.gear } : { throttle: gasNow, brake, steer, handbrake, horn: state.horn, gear: state.gear };
46664
+ },
46665
+ after(driver, dt) {
46666
+ if (driver.state.engineOn && !state.ignition && driver.state.u !== 0) state.ignition = true;
46667
+ if (!state.ignition && driver.state.engineOn) driver.state.engineOn = false;
46668
+ if (state.driveMode === "auto" && options.fourWheelDrive) {
46669
+ const slip = Math.max(0, ...driver.state.slip);
46670
+ if (slip > auto4.slip) {
46671
+ state.fourWheelEngaged = true;
46672
+ calm = 0;
46673
+ } else if (state.fourWheelEngaged && (calm += dt) > auto4.releaseSeconds) state.fourWheelEngaged = false;
46674
+ } else state.fourWheelEngaged = state.driveMode === "4x4";
46675
+ driver.spec.fourWheelDrive = !!options.fourWheelDrive && state.fourWheelEngaged;
46676
+ },
46677
+ release() {
46678
+ held.clear();
46679
+ throttle = brake = steer = 0;
46680
+ state.horn = false;
46681
+ }
46682
+ };
46683
+ }
46346
46684
  function conditionFromDamage(spec, report) {
46347
46685
  const tyres = spec.wheels.map((wheel) => {
46348
46686
  let best;
@@ -47949,21 +48287,521 @@ var GameVehiclesThree = (() => {
47949
48287
  return { ...certificate, results, status: statusOf(results) };
47950
48288
  }
47951
48289
 
48290
+ // src/dashboard.ts
48291
+ var DASHBOARD_LOOK = {
48292
+ face: "#0b1110",
48293
+ ring: "#2f3a36",
48294
+ marks: "#cfe8dc",
48295
+ needle: "#ff6a2a",
48296
+ red: "#e2402f",
48297
+ amber: "#f2a630",
48298
+ green: "#46d16f",
48299
+ blue: "#3f8cff",
48300
+ dim: "rgba(207,232,220,0.12)",
48301
+ /** How far in front of the kit's dial the face sits, metres (the kit's needle sticks out 6 mm). */
48302
+ lift: 0.011
48303
+ };
48304
+ var SIZE = 256;
48305
+ function createVehicleDashboard(THREE, built2, options) {
48306
+ const maxKmh = options.maxKmh ?? 220;
48307
+ const redline = options.redlineRpm ?? 6500;
48308
+ const doc = options.document ?? (typeof document !== "undefined" ? document : null);
48309
+ const dials = [];
48310
+ built2.group.traverse((o) => {
48311
+ if (o.isMesh && o.material === options.gauge) dials.push(o);
48312
+ });
48313
+ const eye = built2.driverEye;
48314
+ const hidden = [];
48315
+ const faces = [];
48316
+ if (doc && eye && dials.length) {
48317
+ built2.group.updateMatrixWorld(true);
48318
+ const eyeWorld = built2.group.localToWorld(new THREE.Vector3(eye[0], eye[1], eye[2]));
48319
+ const near = dials.map((d) => ({ d, at: d.getWorldPosition(new THREE.Vector3()) })).sort((a, b) => a.at.distanceTo(eyeWorld) - b.at.distanceTo(eyeWorld)).slice(0, 2).sort((a, b) => built2.group.worldToLocal(b.at.clone()).x - built2.group.worldToLocal(a.at.clone()).x);
48320
+ near.forEach(({ d, at }, i) => {
48321
+ const canvas = doc.createElement("canvas");
48322
+ canvas.width = canvas.height = SIZE;
48323
+ const g = canvas.getContext("2d");
48324
+ if (!g) return;
48325
+ const texture = new THREE.CanvasTexture(canvas);
48326
+ const radius = d.geometry.parameters?.radiusTop ?? 0.055;
48327
+ const mesh2 = new THREE.Mesh(new THREE.CircleGeometry(radius * 1.12, 40), new THREE.MeshBasicMaterial({ map: texture, transparent: true, toneMapped: false, depthWrite: false }));
48328
+ mesh2.name = "dashboard_face";
48329
+ const axis = new THREE.Vector3(0, 1, 0).applyQuaternion(d.getWorldQuaternion(d.quaternion.clone())).normalize();
48330
+ if (axis.dot(eyeWorld.clone().sub(at)) < 0) axis.negate();
48331
+ const front = at.clone().addScaledVector(axis, DASHBOARD_LOOK.lift);
48332
+ mesh2.position.copy(built2.group.worldToLocal(front.clone()));
48333
+ built2.group.add(mesh2);
48334
+ mesh2.updateMatrixWorld(true);
48335
+ mesh2.lookAt(front.add(axis));
48336
+ d.visible = false;
48337
+ hidden.push(d);
48338
+ if (options.needle)
48339
+ built2.group.traverse((o) => {
48340
+ if (o.isMesh && o.material === options.needle && o.visible && o.getWorldPosition(new THREE.Vector3()).distanceTo(at) < radius * 1.2) {
48341
+ o.visible = false;
48342
+ hidden.push(o);
48343
+ }
48344
+ });
48345
+ faces.push({ mesh: mesh2, canvas, g, texture, kind: near.length === 1 || i === 0 ? "speed" : "rpm" });
48346
+ });
48347
+ }
48348
+ const lamp = (g, x, y, on, colour, draw2) => {
48349
+ g.save();
48350
+ g.translate(x, y);
48351
+ g.fillStyle = g.strokeStyle = on ? colour : DASHBOARD_LOOK.dim;
48352
+ g.lineWidth = 4;
48353
+ draw2(g);
48354
+ g.restore();
48355
+ };
48356
+ const arrow = (dir) => (g) => {
48357
+ g.beginPath();
48358
+ g.moveTo(dir * 14, 0);
48359
+ g.lineTo(-dir * 2, -12);
48360
+ g.lineTo(-dir * 2, -5);
48361
+ g.lineTo(-dir * 14, -5);
48362
+ g.lineTo(-dir * 14, 5);
48363
+ g.lineTo(-dir * 2, 5);
48364
+ g.lineTo(-dir * 2, 12);
48365
+ g.closePath();
48366
+ g.fill();
48367
+ };
48368
+ const beam = (g) => {
48369
+ g.beginPath();
48370
+ g.arc(-4, 0, 10, Math.PI / 2, -Math.PI / 2, true);
48371
+ g.closePath();
48372
+ g.fill();
48373
+ for (let k = -1; k <= 1; k++) g.fillRect(8, k * 6 - 1.5, 10, 3);
48374
+ };
48375
+ const belt = (g) => {
48376
+ g.beginPath();
48377
+ g.arc(0, -10, 5, 0, Math.PI * 2);
48378
+ g.fill();
48379
+ g.fillRect(-6, -3, 12, 16);
48380
+ g.strokeStyle = DASHBOARD_LOOK.face;
48381
+ g.lineWidth = 3;
48382
+ g.beginPath();
48383
+ g.moveTo(-7, -3);
48384
+ g.lineTo(7, 12);
48385
+ g.stroke();
48386
+ };
48387
+ const letter = (text) => (g) => {
48388
+ g.font = "bold 22px sans-serif";
48389
+ g.textAlign = "center";
48390
+ g.textBaseline = "middle";
48391
+ g.fillText(text, 0, 1);
48392
+ };
48393
+ const ring2 = (text) => (g) => {
48394
+ g.beginPath();
48395
+ g.arc(0, 0, 13, 0, Math.PI * 2);
48396
+ g.stroke();
48397
+ letter(text)(g);
48398
+ };
48399
+ const dial = (g, value, top, step, redFrom, label) => {
48400
+ const c = SIZE / 2;
48401
+ g.clearRect(0, 0, SIZE, SIZE);
48402
+ g.fillStyle = DASHBOARD_LOOK.face;
48403
+ g.beginPath();
48404
+ g.arc(c, c, c - 2, 0, Math.PI * 2);
48405
+ g.fill();
48406
+ g.strokeStyle = DASHBOARD_LOOK.ring;
48407
+ g.lineWidth = 6;
48408
+ g.stroke();
48409
+ const angle = (v) => Math.PI * 0.75 + Math.max(0, Math.min(top, v)) / top * Math.PI * 1.5;
48410
+ for (let v = 0; v <= top + 1e-6; v += step) {
48411
+ const a2 = angle(v);
48412
+ g.strokeStyle = redFrom !== null && v >= redFrom ? DASHBOARD_LOOK.red : DASHBOARD_LOOK.marks;
48413
+ g.lineWidth = 4;
48414
+ g.beginPath();
48415
+ g.moveTo(c + Math.cos(a2) * (c - 12), c + Math.sin(a2) * (c - 12));
48416
+ g.lineTo(c + Math.cos(a2) * (c - 30), c + Math.sin(a2) * (c - 30));
48417
+ g.stroke();
48418
+ g.fillStyle = DASHBOARD_LOOK.marks;
48419
+ g.font = "18px sans-serif";
48420
+ g.textAlign = "center";
48421
+ g.textBaseline = "middle";
48422
+ g.fillText(label(v), c + Math.cos(a2) * (c - 48), c + Math.sin(a2) * (c - 48));
48423
+ }
48424
+ const a = angle(value);
48425
+ g.strokeStyle = DASHBOARD_LOOK.needle;
48426
+ g.lineWidth = 6;
48427
+ g.beginPath();
48428
+ g.moveTo(c - Math.cos(a) * 14, c - Math.sin(a) * 14);
48429
+ g.lineTo(c + Math.cos(a) * (c - 22), c + Math.sin(a) * (c - 22));
48430
+ g.stroke();
48431
+ g.fillStyle = DASHBOARD_LOOK.ring;
48432
+ g.beginPath();
48433
+ g.arc(c, c, 10, 0, Math.PI * 2);
48434
+ g.fill();
48435
+ };
48436
+ const bar = (g, x, share, warnAbove, label) => {
48437
+ if (share === void 0) return;
48438
+ const h = 60;
48439
+ g.strokeStyle = DASHBOARD_LOOK.marks;
48440
+ g.lineWidth = 2;
48441
+ g.strokeRect(x - 5, 150, 10, h);
48442
+ const s = Math.max(0, Math.min(1, share));
48443
+ g.fillStyle = (warnAbove ? s > 0.85 : s < 0.12) ? DASHBOARD_LOOK.red : DASHBOARD_LOOK.marks;
48444
+ g.fillRect(x - 4, 150 + h * (1 - s), 8, h * s);
48445
+ g.font = "14px sans-serif";
48446
+ g.textAlign = "center";
48447
+ g.fillText(label, x, 222);
48448
+ };
48449
+ const draw = (r) => {
48450
+ const warn = r.ignition && !r.engineOn;
48451
+ for (const f of faces) {
48452
+ const g = f.g;
48453
+ if (f.kind === "speed") {
48454
+ dial(g, r.kmh, maxKmh, maxKmh > 160 ? 20 : 10, null, (v) => String(Math.round(v)));
48455
+ g.fillStyle = DASHBOARD_LOOK.marks;
48456
+ g.font = "bold 30px sans-serif";
48457
+ g.textAlign = "center";
48458
+ g.fillText(String(Math.round(r.kmh)), SIZE / 2, SIZE / 2 + 52);
48459
+ lamp(g, 92, 178, r.left, DASHBOARD_LOOK.green, arrow(-1));
48460
+ lamp(g, 164, 178, r.right, DASHBOARD_LOOK.green, arrow(1));
48461
+ lamp(g, 104, 214, r.highBeam, DASHBOARD_LOOK.blue, beam);
48462
+ lamp(g, 152, 214, r.lowBeam || r.fog, r.fog ? DASHBOARD_LOOK.amber : DASHBOARD_LOOK.green, beam);
48463
+ bar(g, 40, r.fuel, false, "F");
48464
+ } else {
48465
+ dial(g, r.rpm / 1e3, Math.ceil(redline / 1e3) + 1, 1, redline / 1e3, (v) => String(Math.round(v)));
48466
+ g.fillStyle = DASHBOARD_LOOK.marks;
48467
+ g.font = "bold 34px sans-serif";
48468
+ g.textAlign = "center";
48469
+ g.fillText(r.gear < 0 ? "R" : r.gear === 0 ? "N" : String(r.gear), SIZE / 2, SIZE / 2 + 54);
48470
+ lamp(g, 86, 178, r.parkingBrake, DASHBOARD_LOOK.red, ring2("P"));
48471
+ lamp(g, 128, 214, r.ignition && !r.seatbelt, DASHBOARD_LOOK.red, belt);
48472
+ lamp(g, 170, 178, warn, DASHBOARD_LOOK.red, letter("+\u2212"));
48473
+ lamp(g, 86, 214, warn, DASHBOARD_LOOK.red, letter("OIL"));
48474
+ lamp(g, 170, 214, r.fourWheel, DASHBOARD_LOOK.green, letter("4x4"));
48475
+ bar(g, SIZE - 40, r.temperature, true, "T");
48476
+ }
48477
+ f.texture.needsUpdate = true;
48478
+ }
48479
+ };
48480
+ let wait = 0;
48481
+ let last = "";
48482
+ return {
48483
+ dials: faces.length,
48484
+ update(reading, dt) {
48485
+ if (!faces.length) return;
48486
+ wait -= dt;
48487
+ const key = JSON.stringify(reading);
48488
+ if (wait > 0 || key === last) return;
48489
+ wait = 1 / (options.fps ?? 15);
48490
+ last = key;
48491
+ draw(reading);
48492
+ },
48493
+ dispose() {
48494
+ for (const f of faces) {
48495
+ discardObject3D(f.mesh);
48496
+ f.texture.dispose();
48497
+ f.canvas.width = f.canvas.height = 0;
48498
+ }
48499
+ faces.length = 0;
48500
+ for (const o of hidden) o.visible = true;
48501
+ hidden.length = 0;
48502
+ }
48503
+ };
48504
+ }
48505
+
48506
+ // src/lamps.ts
48507
+ var LAMP_RULES = {
48508
+ glow: { head: { off: 0.04, position: 0.3, low: 1, high: 1.35 }, tail: { off: 0.15, on: 0.7, brake: 1.6 }, fog: { off: 0.04, on: 1.1 }, amber: { off: 0.1, on: 2.2 } },
48509
+ low: { color: 16773584, intensity: 2.4, distance: 32, angle: 0.42, penumbra: 0.55, decay: 1.6, drop: 0.09 },
48510
+ high: { color: 16774882, intensity: 3.6, distance: 70, angle: 0.3, penumbra: 0.45, decay: 1.4, drop: 0.015 },
48511
+ fog: { color: 16773320, intensity: 1.6, distance: 16, angle: 0.75, penumbra: 0.7, decay: 1.8, drop: 0.12 },
48512
+ reverse: { color: 16054015, intensity: 0.9, distance: 7, decay: 2 },
48513
+ interior: { color: 16769712, intensity: 0.5, distance: 2.4, decay: 2 }
48514
+ };
48515
+ function createVehicleLamps(THREE, built2, options = {}) {
48516
+ const budget = isLightBudget(options.lights) ? options.lights : null;
48517
+ const owner = options.owner ?? "vehicle-lamps";
48518
+ const context = built2.context;
48519
+ const lamps = (context?.lights ?? []).filter((l) => l && Array.isArray(l.inner)).map((l) => l.fog ? { ...l, kind: "fog" } : l);
48520
+ const bulbs = (context?.fogBulbs ?? []).filter((o) => o?.material);
48521
+ built2.group.traverse((o) => {
48522
+ if (o.userData?.lamp === "fog" && o.material && !bulbs.includes(o)) bulbs.push(o);
48523
+ });
48524
+ if (bulbs.length) lamps.push({ kind: "fog", inner: bulbs });
48525
+ const has = { head: lamps.some((l) => l.kind === "headlight"), tail: lamps.some((l) => l.kind === "taillight"), fog: lamps.some((l) => l.kind === "fog"), indicators: false };
48526
+ const originals = /* @__PURE__ */ new Map();
48527
+ const copies = /* @__PURE__ */ new Map();
48528
+ const base = /* @__PURE__ */ new Map();
48529
+ const own = (mesh2) => {
48530
+ const mat = mesh2.material;
48531
+ if (!mat || Array.isArray(mat)) return null;
48532
+ if (copies.has(mat) || base.has(mat)) return mat;
48533
+ const copy = mat.clone();
48534
+ originals.set(mesh2, mat);
48535
+ copies.set(copy, mat);
48536
+ base.set(copy, mat.emissiveIntensity ?? 1);
48537
+ mesh2.material = copy;
48538
+ return copy;
48539
+ };
48540
+ const haloBase = /* @__PURE__ */ new Map();
48541
+ for (const l of lamps) {
48542
+ for (const m of l.inner) own(m);
48543
+ for (const h of l.halo ?? []) haloBase.set(h, h.scale.x);
48544
+ }
48545
+ const amber = /* @__PURE__ */ new Set();
48546
+ const sideOf = /* @__PURE__ */ new Map();
48547
+ for (const l of lamps) {
48548
+ const x = l.group?.position?.x ?? 0;
48549
+ for (const m of l.inner) {
48550
+ const mat = Array.isArray(m.material) ? null : m.material;
48551
+ const c = (mat && (copies.get(mat) ?? mat))?.color;
48552
+ if (c && c.r > 0.5 && c.r - c.b > 0.35 && c.g < c.r * 0.8 && c.g > c.b) {
48553
+ amber.add(m);
48554
+ sideOf.set(m, x >= 0 ? "left" : "right");
48555
+ }
48556
+ }
48557
+ }
48558
+ has.indicators = amber.size > 0;
48559
+ const glowMesh = (m, share) => {
48560
+ const mat = m.material;
48561
+ if (mat && !Array.isArray(mat) && base.has(mat) && "emissiveIntensity" in mat) mat.emissiveIntensity = base.get(mat) * share;
48562
+ };
48563
+ const glow = (l, share) => {
48564
+ for (const m of l.inner) {
48565
+ if (amber.has(m)) continue;
48566
+ glowMesh(m, share);
48567
+ }
48568
+ for (const h of l.halo ?? []) {
48569
+ h.visible = share > 0.5;
48570
+ const s = (haloBase.get(h) ?? 1) * Math.min(1.4, share);
48571
+ h.scale.set(s, s, 1);
48572
+ }
48573
+ };
48574
+ const L = built2.length;
48575
+ const W = built2.width;
48576
+ const H = built2.height;
48577
+ const anchors = [];
48578
+ const anchor = (x, y, z, yaw, drop) => {
48579
+ const a = new THREE.Object3D();
48580
+ a.position.set(x, y, z);
48581
+ a.rotation.set(-drop, yaw, 0, "YXZ");
48582
+ built2.group.add(a);
48583
+ anchors.push(a);
48584
+ return a;
48585
+ };
48586
+ const front = [-1, 1].map((s) => anchor(s * W * 0.34, Math.min(0.9, H * 0.48), L / 2 - 0.05, Math.PI, 0));
48587
+ const fogAnchors = [-1, 1].map((s) => anchor(s * W * 0.38, Math.min(0.45, H * 0.25), L / 2 - 0.03, Math.PI, 0));
48588
+ const rear = anchor(0, Math.min(0.9, H * 0.45), -L / 2 - 0.3, 0, 0);
48589
+ const cabin = anchor(0, H * 0.85, 0, 0, 0);
48590
+ const sources = /* @__PURE__ */ new Map();
48591
+ const beam = (id, on, make2) => {
48592
+ const now = sources.get(id);
48593
+ if (on && !now && budget) sources.set(id, make2());
48594
+ if (!on && now) {
48595
+ for (const s of now) s.remove();
48596
+ sources.delete(id);
48597
+ }
48598
+ };
48599
+ const spots = (at, rule) => at.map((a) => {
48600
+ a.rotation.x = -rule.drop;
48601
+ return budget.addSource({ owner, kind: "spot", priority: LIGHT_PRIORITY.headlight, color: rule.color, intensity: rule.intensity, distance: rule.distance, decay: rule.decay, angle: rule.angle, penumbra: rule.penumbra, position: [0, 0, 0], follow: a });
48602
+ });
48603
+ const point = (a, rule) => [
48604
+ budget.addSource({ owner, kind: "point", priority: LIGHT_PRIORITY.lamp, color: rule.color, intensity: rule.intensity, distance: rule.distance, decay: rule.decay, position: [0, 0, 0], follow: a })
48605
+ ];
48606
+ const state = { mode: "off", fog: false, interior: false, brake: false, reverse: false, left: false, right: false };
48607
+ const apply = () => {
48608
+ const g = LAMP_RULES.glow;
48609
+ for (const l of lamps) {
48610
+ if (l.kind === "headlight") glow(l, g.head[state.mode]);
48611
+ else if (l.kind === "taillight") glow(l, state.brake ? g.tail.brake : state.mode === "off" ? g.tail.off : g.tail.on);
48612
+ else if (l.kind === "fog") glow(l, state.fog ? g.fog.on : g.fog.off);
48613
+ }
48614
+ for (const m of amber) glowMesh(m, state[sideOf.get(m) ?? "left"] ? g.amber.on : g.amber.off);
48615
+ const beamId = state.mode === "low" || state.mode === "high" ? state.mode : null;
48616
+ for (const id of ["low", "high"]) beam(id, beamId === id, () => spots(front, LAMP_RULES[id]));
48617
+ beam("fog", state.fog && has.fog, () => spots(fogAnchors, LAMP_RULES.fog));
48618
+ beam("reverse", state.reverse, () => point(rear, LAMP_RULES.reverse));
48619
+ beam("interior", state.interior, () => point(cabin, LAMP_RULES.interior));
48620
+ };
48621
+ apply();
48622
+ return {
48623
+ has,
48624
+ get state() {
48625
+ return { ...state };
48626
+ },
48627
+ set(next) {
48628
+ if (next.mode === state.mode && next.fog === state.fog && next.interior === state.interior && next.brake === state.brake && next.reverse === state.reverse && next.left === state.left && next.right === state.right) return;
48629
+ Object.assign(state, next);
48630
+ apply();
48631
+ },
48632
+ dispose() {
48633
+ for (const list2 of sources.values()) for (const s of list2) s.remove();
48634
+ sources.clear();
48635
+ for (const a of anchors) a.removeFromParent();
48636
+ anchors.length = 0;
48637
+ for (const l of lamps) {
48638
+ for (const m of l.inner) {
48639
+ const copy = m.material;
48640
+ const original = originals.get(m);
48641
+ if (original) {
48642
+ m.material = original;
48643
+ if (copy && !Array.isArray(copy)) copy.dispose?.();
48644
+ }
48645
+ }
48646
+ for (const h of l.halo ?? []) {
48647
+ h.visible = true;
48648
+ const s = haloBase.get(h) ?? 1;
48649
+ h.scale.set(s, s, 1);
48650
+ }
48651
+ }
48652
+ originals.clear();
48653
+ copies.clear();
48654
+ base.clear();
48655
+ }
48656
+ };
48657
+ }
48658
+
48659
+ // src/test-area/proving/cockpit.ts
48660
+ 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" };
48661
+ function createCockpit(o) {
48662
+ const sounds = { ...DEFAULT_COCKPIT_SOUNDS, ...o.sounds };
48663
+ const spec = subjectSpec(o.subject);
48664
+ const driver = createSemiSimDriver(spec);
48665
+ driver.place(o.place[0], o.place[1], o.place[2]);
48666
+ const lamps = createVehicleLamps(o.THREE, o.built, { lights: o.host.lights, owner: "proving-ground" });
48667
+ const audio = o.host.audio;
48668
+ const top = o.subject.stats.maxSpeedKmh;
48669
+ const dashboard = o.gauge ? createVehicleDashboard(o.THREE, o.built, { gauge: o.gauge, needle: o.needle, redlineRpm: spec.engine.redlineRpm, maxKmh: top ? Math.ceil((top + 10) / 20) * 20 : void 0 }) : null;
48670
+ const at = () => [o.built.group.position.x, 1, o.built.group.position.z];
48671
+ const play = (sound, volume = 1) => void audio?.play?.({ sound, position: at(), volume });
48672
+ let alarmAgain = 0;
48673
+ let hornAgain = 0;
48674
+ let wipeAgain = 0;
48675
+ let chimeAgain = 0;
48676
+ let radioAgain = 0;
48677
+ let exitAsked = false;
48678
+ let gas = 0;
48679
+ const controls = createVehicleControls({
48680
+ gears: spec.gears?.length ?? 5,
48681
+ fourWheelDrive: !!o.subject.body.fourWheelDrive,
48682
+ fogLamps: lamps.has.fog,
48683
+ onEvent: (event) => {
48684
+ if (event.type === "command") {
48685
+ if (event.command === "ignition" && event.state.ignition && sounds.starter) play(sounds.starter);
48686
+ if (event.command === "ignition" && !event.state.ignition) driver.state.engineOn = false;
48687
+ if (event.command === "alarm" && event.state.alarm === "armed") play(sounds.chirp);
48688
+ if (event.command === "alarm") alarmAgain = 0;
48689
+ if (event.command === "radio" || event.command === "radio_source") radioAgain = 0;
48690
+ if (event.command === "exit") exitAsked = true;
48691
+ }
48692
+ o.onEvent?.(event);
48693
+ }
48694
+ });
48695
+ driver.state.engineOn = false;
48696
+ const target = typeof window !== "undefined" ? window : null;
48697
+ const down = (e) => {
48698
+ if (!controls.key(e.code, true)) return;
48699
+ e.stopImmediatePropagation();
48700
+ if (e.code.startsWith("Arrow") || e.code === "Space") e.preventDefault();
48701
+ };
48702
+ const up = (e) => {
48703
+ if (controls.key(e.code, false)) e.stopImmediatePropagation();
48704
+ };
48705
+ const blur = () => controls.release();
48706
+ target?.addEventListener("keydown", down, true);
48707
+ target?.addEventListener("keyup", up, true);
48708
+ target?.addEventListener("blur", blur);
48709
+ return {
48710
+ driver,
48711
+ controls,
48712
+ lamps,
48713
+ dashboard,
48714
+ get throttle() {
48715
+ return gas;
48716
+ },
48717
+ step(dt) {
48718
+ const input = controls.input(dt, driver);
48719
+ gas = input.throttle;
48720
+ driver.step(input, dt);
48721
+ controls.after(driver, dt);
48722
+ const s = controls.state;
48723
+ lamps.set({
48724
+ mode: s.lamps.mode,
48725
+ fog: s.lamps.fog,
48726
+ interior: s.lamps.interior,
48727
+ brake: s.brakeLights,
48728
+ reverse: s.reverseLights,
48729
+ left: s.blinkOn && (s.indicator === "left" || s.indicator === "hazard"),
48730
+ right: s.blinkOn && (s.indicator === "right" || s.indicator === "hazard")
48731
+ });
48732
+ dashboard?.update(
48733
+ {
48734
+ kmh: Math.abs(driver.state.u) * 3.6,
48735
+ rpm: driver.state.engineOn ? driver.state.rpm : 0,
48736
+ gear: driver.state.gear,
48737
+ ignition: s.ignition,
48738
+ engineOn: driver.state.engineOn,
48739
+ left: s.blinkOn && (s.indicator === "left" || s.indicator === "hazard"),
48740
+ right: s.blinkOn && (s.indicator === "right" || s.indicator === "hazard"),
48741
+ highBeam: s.lamps.mode === "high",
48742
+ lowBeam: s.lamps.mode === "low" || s.lamps.mode === "position",
48743
+ fog: s.lamps.fog,
48744
+ parkingBrake: s.parkingBrake,
48745
+ seatbelt: s.seatbelt,
48746
+ fourWheel: s.fourWheelEngaged
48747
+ },
48748
+ dt
48749
+ );
48750
+ if (s.tick) play(sounds.blinker, 0.8);
48751
+ if (s.horn && (hornAgain -= dt) <= 0) {
48752
+ play(sounds.horn);
48753
+ hornAgain = 0.5;
48754
+ } else if (!s.horn) hornAgain = 0;
48755
+ if (s.alarm === "ringing" && (alarmAgain -= dt) <= 0) {
48756
+ play(sounds.alarm);
48757
+ alarmAgain = 2.9;
48758
+ }
48759
+ if (s.wipers !== "off" && (wipeAgain -= dt) <= 0) {
48760
+ play(sounds.wiper, 0.7);
48761
+ wipeAgain = s.wipers === "fast" ? 0.6 : 1.4;
48762
+ }
48763
+ const kmh2 = Math.abs(driver.state.u) * 3.6;
48764
+ if (!s.seatbelt && s.ignition && kmh2 > 10 && (chimeAgain -= dt) <= 0) {
48765
+ play(sounds.chime, 0.6);
48766
+ chimeAgain = 6;
48767
+ }
48768
+ if (s.radio.on && (radioAgain -= dt) <= 0) {
48769
+ play(sounds.radio, s.radio.source === "tape" ? 0.25 : 0.45);
48770
+ radioAgain = 0.55;
48771
+ }
48772
+ if (exitAsked) {
48773
+ exitAsked = false;
48774
+ o.onExit();
48775
+ }
48776
+ },
48777
+ stop() {
48778
+ target?.removeEventListener("keydown", down, true);
48779
+ target?.removeEventListener("keyup", up, true);
48780
+ target?.removeEventListener("blur", blur);
48781
+ controls.release();
48782
+ lamps.dispose();
48783
+ dashboard?.dispose();
48784
+ }
48785
+ };
48786
+ }
48787
+
47952
48788
  // src/test-area/proving/text.ts
47953
48789
  var PROVING_TEXT = {
47954
48790
  "pt-BR": {
47955
48791
  name: "Banco de provas",
47956
- controls: { vehicle: "Ve\xEDculo", test: "Prova", camera: "C\xE2mera", time_scale: "Velocidade da simula\xE7\xE3o", visual_check: "Checagem visual" },
48792
+ controls: { vehicle: "Ve\xEDculo", test: "Prova", camera: "C\xE2mera", time_scale: "Velocidade da simula\xE7\xE3o", visual_check: "Checagem visual", gearbox: "C\xE2mbio" },
48793
+ gearbox: { automatic: "Autom\xE1tico", manual: "Manual" },
47957
48794
  hints: {
48795
+ gearbox: "Autom\xE1tico troca sozinho. Manual: E sobe e Q desce a marcha (abaixo da 1\xAA v\xEAm o neutro e a r\xE9). M alterna dirigindo",
47958
48796
  vehicle: "Ve\xEDculo da frota que vai para a pista",
47959
48797
  test: "Prova medida que o bot\xE3o Rodar prova executa",
47960
48798
  camera: "Sala: a c\xE2mera do jogador. Persegui\xE7\xE3o, cabine e cap\xF4 seguem o ve\xEDculo",
47961
48799
  time_scale: "Quantas vezes mais r\xE1pido que o tempo real a prova roda",
47962
48800
  visual_check: "O que voc\xEA olhou e vai aprovar ou reprovar"
47963
48801
  },
47964
- 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" },
48802
+ actions: { drive: "Dirigir (entrar / sair)", 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" },
47965
48803
  layers: { mirrors: "Retrovisores", reverse_camera: "C\xE2mera de r\xE9", scoreboard: "Painel da ficha" },
47966
- 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.)" },
48804
+ metrics: { gear: "Marcha (-1 r\xE9, 0 neutro)", rpm: "Rota\xE7\xE3o", 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.)" },
47967
48805
  cameras: { room: "Sala", chase: "Persegui\xE7\xE3o (3\xAA pessoa)", cockpit: "Cabine (1\xAA pessoa)", hood: "Cap\xF4" },
47968
48806
  tests: {
47969
48807
  accel_0_100: "0 a 100 km/h",
@@ -47985,22 +48823,49 @@ var GameVehiclesThree = (() => {
47985
48823
  },
47986
48824
  status: { approved: "APROVADO", rejected: "REPROVADO", pending: "PENDENTE" },
47987
48825
  verdict: { pass: "ok", fail: "falhou", pending: "\u2026" },
47988
- board: { title: "Ficha de homologa\xE7\xE3o", test: "Prova", value: "Medido", range: "Faixa", verdict: "Resultado", notRun: "n\xE3o rodada", running: "rodando" },
47989
- 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}" }
48826
+ board: { title: "Ficha de homologa\xE7\xE3o", test: "Prova", value: "Medido", range: "Faixa", verdict: "Resultado", notRun: "n\xE3o rodada", running: "rodando", gear: "Marcha {0} \xB7 {1} rpm \xB7 {2} km/h" },
48827
+ 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}", drive: "Dirigindo {0}: I liga o motor, F sai do carro", leave: "Saiu do volante", shift: "Marcha {0} \u2192 {1} a {2} km/h" },
48828
+ cabin: {
48829
+ keys: "W ou \u2191 acelera \xB7 S ou \u2193 freia (parado, d\xE1 r\xE9) \xB7 A/D ou \u2190/\u2192 vira \xB7 Espa\xE7o freio de m\xE3o \xB7 P puxa o freio de m\xE3o \xB7 I liga e desliga \xB7 M c\xE2mbio manual/autom\xE1tico \xB7 E sobe e Q desce marcha \xB7 G tra\xE7\xE3o 4x2/4x4/4x4 autom\xE1tica \xB7 L luzes (posi\xE7\xE3o, baixa) \xB7 K farol alto \xB7 N neblina \xB7 U luz interna \xB7 v\xEDrgula e ponto setas \xB7 B pisca-alerta \xB7 O limpador \xB7 J cinto \xB7 H buzina \xB7 Y alarme \xB7 R r\xE1dio \xB7 T r\xE1dio/fita \xB7 F sai do carro",
48830
+ on: "ligado",
48831
+ off: "desligado",
48832
+ ignition: "Motor",
48833
+ gearbox: "C\xE2mbio",
48834
+ gear: "Marcha",
48835
+ parkingBrake: "Freio de m\xE3o",
48836
+ driveMode: "Tra\xE7\xE3o",
48837
+ modes: { "4x2": "4x2", "4x4": "4x4", auto: "4x4 autom\xE1tica" },
48838
+ lights: "Luzes",
48839
+ lampModes: { off: "apagadas", position: "posi\xE7\xE3o", low: "farol baixo", high: "farol alto" },
48840
+ fog: "Neblina",
48841
+ interior: "Luz interna",
48842
+ alarm: "Alarme",
48843
+ alarmStates: { off: "desligado", armed: "armado", ringing: "disparado" },
48844
+ radio: "Som",
48845
+ sources: { radio: "r\xE1dio", tape: "toca-fitas" },
48846
+ indicator: "Setas",
48847
+ indicators: { off: "desligadas", left: "esquerda", right: "direita", hazard: "pisca-alerta" },
48848
+ wipers: "Limpador",
48849
+ wiperModes: { off: "desligado", slow: "lento", fast: "r\xE1pido" },
48850
+ seatbelt: "Cinto",
48851
+ refused: { no_four_wheel_drive: "Este ve\xEDculo n\xE3o tem tra\xE7\xE3o 4x4", no_fog_lamps: "Este ve\xEDculo n\xE3o tem farol de neblina", ignition_off: "Ligue o motor primeiro" }
48852
+ }
47990
48853
  },
47991
48854
  en: {
47992
48855
  name: "Proving ground",
47993
- controls: { vehicle: "Vehicle", test: "Test", camera: "Camera", time_scale: "Simulation speed", visual_check: "Visual check" },
48856
+ controls: { vehicle: "Vehicle", test: "Test", camera: "Camera", time_scale: "Simulation speed", visual_check: "Visual check", gearbox: "Gearbox" },
48857
+ gearbox: { automatic: "Automatic", manual: "Manual" },
47994
48858
  hints: {
48859
+ gearbox: "Automatic shifts by itself. Manual: E shifts up and Q down (below 1st come neutral and reverse). M switches while driving",
47995
48860
  vehicle: "Fleet vehicle that goes on the track",
47996
48861
  test: "Measured test the Run test button runs",
47997
48862
  camera: "Room: the player camera. Chase, cockpit and hood follow the vehicle",
47998
48863
  time_scale: "How many times faster than real time the test runs",
47999
48864
  visual_check: "What you looked at and will approve or refuse"
48000
48865
  },
48001
- 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" },
48866
+ actions: { drive: "Drive (get in / out)", 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" },
48002
48867
  layers: { mirrors: "Mirrors", reverse_camera: "Reversing camera", scoreboard: "Certificate board" },
48003
- metrics: { speed: "Speed", time: "Test time", value: "Measured", passed: "Passed", failed: "Failed", pending: "Pending", status: "Status (0 pending, 1 approved, -1 rejected)" },
48868
+ metrics: { gear: "Gear (-1 reverse, 0 neutral)", rpm: "Engine speed", speed: "Speed", time: "Test time", value: "Measured", passed: "Passed", failed: "Failed", pending: "Pending", status: "Status (0 pending, 1 approved, -1 rejected)" },
48004
48869
  cameras: { room: "Room", chase: "Chase (third person)", cockpit: "Cockpit (first person)", hood: "Hood" },
48005
48870
  tests: {
48006
48871
  accel_0_100: "0 to 100 km/h",
@@ -48022,8 +48887,33 @@ var GameVehiclesThree = (() => {
48022
48887
  },
48023
48888
  status: { approved: "APPROVED", rejected: "REJECTED", pending: "PENDING" },
48024
48889
  verdict: { pass: "ok", fail: "failed", pending: "\u2026" },
48025
- board: { title: "Certificate", test: "Test", value: "Measured", range: "Range", verdict: "Result", notRun: "not run", running: "running" },
48026
- 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}" }
48890
+ board: { title: "Certificate", test: "Test", value: "Measured", range: "Range", verdict: "Result", notRun: "not run", running: "running", gear: "Gear {0} \xB7 {1} rpm \xB7 {2} km/h" },
48891
+ 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}", drive: "Driving {0}: I starts the engine, F gets out", leave: "Left the wheel", shift: "Gear {0} \u2192 {1} at {2} km/h" },
48892
+ cabin: {
48893
+ keys: "W or \u2191 accelerates \xB7 S or \u2193 brakes (stopped, reverses) \xB7 A/D or \u2190/\u2192 steer \xB7 Space handbrake \xB7 P pulls the handbrake \xB7 I ignition \xB7 M manual/automatic gearbox \xB7 E shifts up, Q down \xB7 G drive 4x2/4x4/auto 4x4 \xB7 L lamps (position, low) \xB7 K high beam \xB7 N fog \xB7 U cabin light \xB7 comma and period indicators \xB7 B hazard \xB7 O wipers \xB7 J seat belt \xB7 H horn \xB7 Y alarm \xB7 R radio \xB7 T radio/tape \xB7 F gets out",
48894
+ on: "on",
48895
+ off: "off",
48896
+ ignition: "Engine",
48897
+ gearbox: "Gearbox",
48898
+ gear: "Gear",
48899
+ parkingBrake: "Handbrake",
48900
+ driveMode: "Drive",
48901
+ modes: { "4x2": "4x2", "4x4": "4x4", auto: "automatic 4x4" },
48902
+ lights: "Lamps",
48903
+ lampModes: { off: "off", position: "position", low: "low beam", high: "high beam" },
48904
+ fog: "Fog lamps",
48905
+ interior: "Cabin light",
48906
+ alarm: "Alarm",
48907
+ alarmStates: { off: "off", armed: "armed", ringing: "ringing" },
48908
+ radio: "Sound",
48909
+ sources: { radio: "radio", tape: "tape" },
48910
+ indicator: "Indicators",
48911
+ indicators: { off: "off", left: "left", right: "right", hazard: "hazard" },
48912
+ wipers: "Wipers",
48913
+ wiperModes: { off: "off", slow: "slow", fast: "fast" },
48914
+ seatbelt: "Seat belt",
48915
+ refused: { no_four_wheel_drive: "This vehicle has no 4x4", no_fog_lamps: "This vehicle has no fog lamps", ignition_off: "Start the engine first" }
48916
+ }
48027
48917
  }
48028
48918
  };
48029
48919
  var fillText = (template, ...values) => template.replace(/\{(\d+)\}/g, (_, i) => String(values[Number(i)] ?? ""));
@@ -48057,6 +48947,10 @@ var GameVehiclesThree = (() => {
48057
48947
  let car = null;
48058
48948
  let run = null;
48059
48949
  let parking = null;
48950
+ let driving = null;
48951
+ let gearbox = "automatic";
48952
+ let lastGear = null;
48953
+ let driveClock = 0;
48060
48954
  let queue = [];
48061
48955
  let open = false;
48062
48956
  let lastWrap = 0;
@@ -48191,13 +49085,13 @@ var GameVehiclesThree = (() => {
48191
49085
  [text.board.test, text.board.value, text.board.range, text.board.verdict].forEach((h, i) => g.fillText(h.toUpperCase(), cols[i], 150));
48192
49086
  let y = 190;
48193
49087
  for (const r of cert.results) {
48194
- const live = run && run.test.id === r.test && !run.done;
49088
+ const live2 = run && run.test.id === r.test && !run.done;
48195
49089
  g.fillStyle = "#e7e9ec";
48196
49090
  g.font = "26px sans-serif";
48197
49091
  g.fillText(text.tests[r.test] ?? r.test, cols[0], y);
48198
49092
  g.font = "26px monospace";
48199
49093
  g.fillStyle = "#5fd4e0";
48200
- const value = live ? text.board.running : r.value == null ? r.kind === "measured" && r.note === "not run" ? text.board.notRun : "\u2014" : `${r.value} ${r.unit}`;
49094
+ const value = live2 ? text.board.running : r.value == null ? r.kind === "measured" && r.note === "not run" ? text.board.notRun : "\u2014" : `${r.value} ${r.unit}`;
48201
49095
  g.fillText(value, cols[1], y);
48202
49096
  g.fillStyle = "#8d95a3";
48203
49097
  g.fillText(r.kind === "measured" ? fmtRange(r.range) : r.kind === "visual" ? "visual" : "sala", cols[2], y);
@@ -48205,6 +49099,12 @@ var GameVehiclesThree = (() => {
48205
49099
  g.fillText(r.verdict === "pass" ? "\u2713 " + text.verdict.pass : r.verdict === "fail" ? "\u2717 " + text.verdict.fail : "\u2026", cols[3], y);
48206
49100
  y += 30;
48207
49101
  }
49102
+ const live = run?.driver.state ?? driving?.driver.state;
49103
+ if (live) {
49104
+ g.font = "24px monospace";
49105
+ g.fillStyle = "#f2c230";
49106
+ g.fillText(fillText(text.board.gear, gearName(live.gear), Math.round(live.rpm), Math.round(Math.abs(live.u) * 3.6)), cols[0], y + 16);
49107
+ }
48208
49108
  board.texture.needsUpdate = true;
48209
49109
  };
48210
49110
  const removeCar = () => {
@@ -48276,7 +49176,7 @@ var GameVehiclesThree = (() => {
48276
49176
  const frameOf = (speed) => {
48277
49177
  if (!car) return null;
48278
49178
  const g = car.built.group;
48279
- 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 };
49179
+ 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 } : {} };
48280
49180
  };
48281
49181
  const applyCamera = () => {
48282
49182
  if (!host?.setViewCamera) return;
@@ -48342,12 +49242,111 @@ var GameVehiclesThree = (() => {
48342
49242
  driver.place(40, 0, 0);
48343
49243
  parking = { driver, time: 0 };
48344
49244
  };
49245
+ const stopDriving = () => {
49246
+ if (!driving) return;
49247
+ driving.stop();
49248
+ driving = null;
49249
+ host?.holdPlayer?.(false);
49250
+ host?.bench?.sheet?.(null);
49251
+ stopEngine();
49252
+ host?.log(text.log.leave);
49253
+ };
49254
+ const startDriving = () => {
49255
+ if (!host || !three) return;
49256
+ if (!car || car.subject.id !== settings.vehicle) placeCar();
49257
+ if (!car) return;
49258
+ run = null;
49259
+ queue = [];
49260
+ parking = null;
49261
+ setSurface("dry_asphalt");
49262
+ driving = createCockpit({
49263
+ THREE: three,
49264
+ host,
49265
+ built: car.built,
49266
+ subject: car.subject,
49267
+ place: [0, 0, 0],
49268
+ gauge: kitFor(three).materials?.gauge,
49269
+ needle: kitFor(three).materials?.amber,
49270
+ // the engine's own sound turns the key (starter, catch, idle by RPM)
49271
+ sounds: { starter: "" },
49272
+ onExit: () => stopDriving(),
49273
+ onEvent: (event) => {
49274
+ if (event.type === "refused") return host?.log(text.cabin.refused[event.reason] ?? event.reason);
49275
+ const st = event.state;
49276
+ const line = cabinLine(event.command, st);
49277
+ if (line) host?.log(line);
49278
+ if (event.command === "ignition" && car) {
49279
+ const at = [car.built.group.position.x, 0.5, car.built.group.position.z];
49280
+ if (st.ignition) engine?.start(at);
49281
+ else engine?.stop(at);
49282
+ }
49283
+ if (event.command === "gearbox") {
49284
+ gearbox = st.gearbox;
49285
+ host?.bench?.patch("gearbox", { value: st.gearbox });
49286
+ }
49287
+ }
49288
+ });
49289
+ if (gearbox !== driving.controls.state.gearbox) driving.controls.command("gearbox");
49290
+ host.holdPlayer?.(true);
49291
+ if (settings.camera === "room") {
49292
+ settings.camera = "cockpit";
49293
+ host.bench?.patch("camera", { value: "cockpit" });
49294
+ applyCamera();
49295
+ }
49296
+ rig.vehicle?.reset();
49297
+ stopEngine();
49298
+ engine = createVehicleSound(host.audio, { sounds });
49299
+ host.bench?.sheet?.({ kicker: text.name, title: fillText(text.log.drive, name(settings.vehicle)), body: text.cabin.keys });
49300
+ host.log(fillText(text.log.drive, name(settings.vehicle)));
49301
+ };
49302
+ const onOff = (on) => on ? text.cabin.on : text.cabin.off;
49303
+ const cabinLine = (command, st) => {
49304
+ const c = text.cabin;
49305
+ switch (command) {
49306
+ case "ignition":
49307
+ return `${c.ignition}: ${onOff(st.ignition)}`;
49308
+ case "gearbox":
49309
+ return `${c.gearbox}: ${text.gearbox[st.gearbox]}`;
49310
+ case "gear_up":
49311
+ case "gear_down":
49312
+ return `${c.gear}: ${gearName(st.gear)}`;
49313
+ case "parking_brake":
49314
+ return `${c.parkingBrake}: ${onOff(st.parkingBrake)}`;
49315
+ case "drive_mode":
49316
+ return `${c.driveMode}: ${c.modes[st.driveMode]}`;
49317
+ case "lights":
49318
+ case "high_beam":
49319
+ return `${c.lights}: ${c.lampModes[st.lamps.mode]}`;
49320
+ case "fog_lights":
49321
+ return `${c.fog}: ${onOff(st.lamps.fog)}`;
49322
+ case "interior_light":
49323
+ return `${c.interior}: ${onOff(st.lamps.interior)}`;
49324
+ case "alarm":
49325
+ return `${c.alarm}: ${c.alarmStates[st.alarm]}`;
49326
+ case "radio":
49327
+ case "radio_source":
49328
+ return `${c.radio}: ${st.radio.on ? c.sources[st.radio.source] : c.off}`;
49329
+ case "indicator_left":
49330
+ case "indicator_right":
49331
+ case "hazard":
49332
+ return `${c.indicator}: ${c.indicators[st.indicator]}`;
49333
+ case "wipers":
49334
+ return `${c.wipers}: ${c.wiperModes[st.wipers]}`;
49335
+ case "seatbelt":
49336
+ return `${c.seatbelt}: ${onOff(st.seatbelt)}`;
49337
+ default:
49338
+ return null;
49339
+ }
49340
+ };
49341
+ const gearName = (gear) => gear < 0 ? "R" : gear === 0 ? "N" : String(gear);
48345
49342
  const onChange = (change) => {
48346
49343
  if (change.kind === "layer") {
48347
49344
  if (change.id === "scoreboard" && board) board.mesh.visible = !!host?.bench?.layer("scoreboard");
48348
49345
  return refreshMirrors();
48349
49346
  }
48350
49347
  if (change.kind === "action") {
49348
+ if (change.id === "drive") return driving ? stopDriving() : startDriving();
49349
+ if (change.id === "run_test" || change.id === "run_all" || change.id === "reverse_park") stopDriving();
48351
49350
  if (change.id === "run_test") {
48352
49351
  queue = [];
48353
49352
  startTest(settings.test);
@@ -48378,6 +49377,7 @@ var GameVehiclesThree = (() => {
48378
49377
  switch (change.id) {
48379
49378
  case "vehicle":
48380
49379
  settings.vehicle = String(change.value);
49380
+ stopDriving();
48381
49381
  run = null;
48382
49382
  queue = [];
48383
49383
  parking = null;
@@ -48395,6 +49395,10 @@ var GameVehiclesThree = (() => {
48395
49395
  case "time_scale":
48396
49396
  settings.timeScale = Number(change.value);
48397
49397
  break;
49398
+ case "gearbox":
49399
+ gearbox = change.value === "manual" ? "manual" : "automatic";
49400
+ if (driving && driving.controls.state.gearbox !== gearbox) driving.controls.command("gearbox");
49401
+ break;
48398
49402
  case "visual_check":
48399
49403
  settings.visual = String(change.value);
48400
49404
  break;
@@ -48418,6 +49422,13 @@ var GameVehiclesThree = (() => {
48418
49422
  speed = s.u;
48419
49423
  steer = s.steerAngle;
48420
49424
  if (run.done) finishTest();
49425
+ } else if (driving) {
49426
+ const d = driving;
49427
+ d.step(dt);
49428
+ if (!driving) return;
49429
+ pose(d.driver.state, d.driver.state.steerAngle);
49430
+ speed = d.driver.state.u;
49431
+ steer = d.driver.state.steerAngle;
48421
49432
  } else if (parking) {
48422
49433
  parking.time += dt;
48423
49434
  const p = parking;
@@ -48427,7 +49438,7 @@ var GameVehiclesThree = (() => {
48427
49438
  steer = p.driver.state.steerAngle;
48428
49439
  if (p.time > 9) parking = null;
48429
49440
  }
48430
- if (car) engine?.update({ dt, position: [car.built.group.position.x, 0.5, car.built.group.position.z], speed, throttle: run ? 1 : 0 });
49441
+ 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 });
48431
49442
  const f = frameOf(speed);
48432
49443
  if (f && rig.vehicle && settings.camera !== "room") rig.vehicle.update(rig.camera, f, dt);
48433
49444
  if (f && rig.vehicle) for (const m of rig.mirrors) rig.vehicle.placeMirror(m.camera, m.pose, f);
@@ -48437,11 +49448,19 @@ var GameVehiclesThree = (() => {
48437
49448
  mirrorKey = key;
48438
49449
  refreshMirrors();
48439
49450
  }
48440
- if (run && Math.floor(run.time * 2) !== Math.floor((run.time - dt * settings.timeScale) * 2)) drawBoard();
49451
+ const geared = run?.driver.state ?? driving?.driver.state;
49452
+ if (geared && geared.gear !== lastGear) {
49453
+ if (lastGear !== null && geared.gear > 0) host.log(fillText(text.log.shift, gearName(lastGear), gearName(geared.gear), Math.round(Math.abs(geared.u) * 3.6)), { from: lastGear, to: geared.gear, kmh: +(Math.abs(geared.u) * 3.6).toFixed(1), rpm: Math.round(geared.rpm) }, "proving_shift");
49454
+ lastGear = geared.gear;
49455
+ drawBoard();
49456
+ } else if (!geared) lastGear = null;
49457
+ if (run && Math.floor(run.time * 2) !== Math.floor((run.time - dt * settings.timeScale) * 2) || driving && Math.floor(driveClock * 2) !== Math.floor((driveClock - dt) * 2)) drawBoard();
49458
+ if (driving) driveClock += dt;
48441
49459
  };
48442
- const speedNow = () => Math.abs(run?.driver.state.u ?? parking?.driver.state.u ?? 0) * 3.6;
49460
+ const speedNow = () => Math.abs(run?.driver.state.u ?? driving?.driver.state.u ?? parking?.driver.state.u ?? 0) * 3.6;
48443
49461
  const counts = (verdict) => certificateOf(settings.vehicle)?.results.filter((r) => r.verdict === verdict).length ?? 0;
48444
49462
  const teardown = () => {
49463
+ stopDriving();
48445
49464
  stopEngine();
48446
49465
  for (const m of rig.mirrors) m.remove();
48447
49466
  rig.mirrors = [];
@@ -48473,6 +49492,8 @@ var GameVehiclesThree = (() => {
48473
49492
  { 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 },
48474
49493
  { kind: "number", id: "time_scale", name: text.controls.time_scale, hint: text.hints.time_scale, min: 1, max: 20, step: 1, default: settings.timeScale },
48475
49494
  { 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 },
49495
+ { kind: "action", id: "drive", name: text.actions.drive },
49496
+ { kind: "choice", id: "gearbox", name: text.controls.gearbox, hint: text.hints.gearbox, options: ["automatic", "manual"].map((id) => ({ id, name: text.gearbox[id] })), default: gearbox },
48476
49497
  { kind: "action", id: "run_test", name: text.actions.run_test },
48477
49498
  { kind: "action", id: "run_all", name: text.actions.run_all },
48478
49499
  { kind: "action", id: "certify_now", name: text.actions.certify_now },
@@ -48488,6 +49509,8 @@ var GameVehiclesThree = (() => {
48488
49509
  ],
48489
49510
  metrics: () => [
48490
49511
  { id: "speed", name: text.metrics.speed, unit: "km/h", digits: 0, read: speedNow },
49512
+ { id: "gear", name: text.metrics.gear, read: () => driving?.driver.state.gear ?? run?.driver.state.gear ?? 0 },
49513
+ { id: "rpm", name: text.metrics.rpm, unit: "rpm", digits: 0, read: () => driving?.driver.state.rpm ?? run?.driver.state.rpm ?? 0 },
48491
49514
  { id: "time", name: text.metrics.time, unit: "s", digits: 1, read: () => run?.time ?? 0 },
48492
49515
  { id: "value", name: text.metrics.value, digits: 1, read: () => run ? run.trace[run.trace.length - 1]?.[1] ?? 0 : 0 },
48493
49516
  { id: "passed", name: text.metrics.passed, read: () => counts("pass") },
@@ -48502,6 +49525,7 @@ var GameVehiclesThree = (() => {
48502
49525
  rig.camera = new three.PerspectiveCamera(60, 1.6, 0.05, 900);
48503
49526
  rig.vehicle = createVehicleCamera(three);
48504
49527
  lent.audio?.register?.(LIBRARY_BANK);
49528
+ lent.audio?.register?.(CABIN_BANK);
48505
49529
  const stopChange = lent.bench?.onChange(onChange);
48506
49530
  const stopFrame = lent.onFrame(frame);
48507
49531
  const stopClear = lent.onClear?.(() => {