@adatechnology/game-vehicles-three 0.6.0 → 0.6.1

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