@adatechnology/game-vehicles-three 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,8 @@ var GameVehiclesThree = (() => {
23
23
  __export(index_exports, {
24
24
  BIKE_ENGINE: () => BIKE_ENGINE,
25
25
  CAR_ENGINE: () => CAR_ENGINE,
26
+ CAR_RECORDED_RPM: () => CAR_RECORDED_RPM,
27
+ CAR_START_RUN_STOP: () => CAR_START_RUN_STOP,
26
28
  CRASH_SPEEDS: () => CRASH_SPEEDS,
27
29
  DASHBOARD_LOOK: () => DASHBOARD_LOOK,
28
30
  DEFAULT_TEST_VEHICLES: () => DEFAULT_TEST_VEHICLES,
@@ -41,6 +43,7 @@ var GameVehiclesThree = (() => {
41
43
  VEHICLE_LOD_DISTANCE: () => VEHICLE_LOD_DISTANCE,
42
44
  VEHICLE_LOOPS: () => VEHICLE_LOOPS,
43
45
  VEHICLE_LOOP_FALLOFF: () => VEHICLE_LOOP_FALLOFF,
46
+ VEHICLE_SAMPLE_ALIASES: () => VEHICLE_SAMPLE_ALIASES,
44
47
  VEHICLE_SCENARIO_IDS: () => VEHICLE_SCENARIO_IDS,
45
48
  VEHICLE_SOUNDS: () => VEHICLE_SOUNDS,
46
49
  VEHICLE_SOUND_BANK: () => VEHICLE_SOUND_BANK,
@@ -49,12 +52,14 @@ var GameVehiclesThree = (() => {
49
52
  createCabinSmoke: () => createCabinSmoke,
50
53
  createGearGuess: () => createGearGuess,
51
54
  createProvingModule: () => createProvingModule,
55
+ createVehicleBody: () => createVehicleBody,
52
56
  createVehicleDashboard: () => createVehicleDashboard,
53
57
  createVehicleLamps: () => createVehicleLamps,
54
58
  createVehicleLod: () => createVehicleLod,
55
59
  createVehicleSound: () => createVehicleSound,
56
60
  createVehiclesTestModule: () => createVehiclesTestModule,
57
61
  createVehiclesThree: () => createVehiclesThree,
62
+ createWheelRoll: () => createWheelRoll,
58
63
  defaultEffectIds: () => defaultEffectIds,
59
64
  defaultSoundIds: () => defaultSoundIds,
60
65
  engineMix: () => engineMix,
@@ -70,6 +75,10 @@ var GameVehiclesThree = (() => {
70
75
 
71
76
  // ../game-contracts/dist/index.js
72
77
  var DEFAULT_BLAST_IMPULSE = { bodyArea: 0.7, lift: 0.15, minSpeed: 0.3, tntPerJoule: 0.24 / 2e5 };
78
+ var LAUNCHED = "launched";
79
+ function launchBody(root, velocity) {
80
+ root.userData[LAUNCHED] = [velocity[0], velocity[1], velocity[2]];
81
+ }
73
82
  var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
74
83
  var DEFAULT_IGNITION_RULES = {
75
84
  fillCurve: [
@@ -3861,9 +3870,9 @@ var GameVehiclesThree = (() => {
3861
3870
  let x = bx + (Hd[0] - bx) * v;
3862
3871
  const y = by + (Hd[1] - by) * v;
3863
3872
  let z = bz + (Hd[2] - bz) * v;
3864
- const chord = Math.hypot(C[0] - T[0], C[1] - T[1]) * (1 - v);
3873
+ const chord2 = Math.hypot(C[0] - T[0], C[1] - T[1]) * (1 - v);
3865
3874
  x -= roach * u * Math.sin(v * PI) * (u > 0.7 ? 1 : u / 0.7);
3866
- z += cam * chord * 4 * u * (1 - u) * (0.6 + 0.4 * (1 - v));
3875
+ z += cam * chord2 * 4 * u * (1 - u) * (0.6 + 0.4 * (1 - v));
3867
3876
  pos.push(x, y, z);
3868
3877
  }
3869
3878
  }
@@ -5092,12 +5101,24 @@ var GameVehiclesThree = (() => {
5092
5101
  }
5093
5102
  );
5094
5103
  }
5104
+ var WIPER_SWEEP = 1.5;
5105
+ function tagWiper(params) {
5106
+ var _a;
5107
+ const tag = { arm: params.index, sweep: WIPER_SWEEP, spindle: [params.spindle.x, params.spindle.y, params.spindle.z], axis: [params.normal.x, params.normal.y, params.normal.z] };
5108
+ (_a = params.context).wipers ?? (_a.wipers = []);
5109
+ for (const part of params.parts) params.context.wipers.push({ part, tag });
5110
+ }
5111
+ function markSunroof(pane, slide) {
5112
+ pane.name = "sunroof";
5113
+ pane.userData.sunroof = { slide };
5114
+ return pane;
5115
+ }
5095
5116
  function installCarsExtra(fx) {
5096
5117
  const kit = fx.kit;
5097
5118
  const THREE = kit.THREE;
5098
5119
  const trl = fx.t;
5099
5120
  const lbl = fx.label;
5100
- const V3 = kit.V3, PI = Math.PI, MT = kit.materials, CM = kit.makeMaterial, G2 = kit.geo, profX = kit.profX, halfAt = kit.halfAt, clamp3 = kit.clamp, smooth2 = kit.smooth;
5121
+ const V3 = kit.V3, PI = Math.PI, MT = kit.materials, CM = kit.makeMaterial, G2 = kit.geo, profX = kit.profX, halfAt = kit.halfAt, clamp5 = kit.clamp, smooth2 = kit.smooth;
5101
5122
  const GLOSS = CM("#07080a", { roughness: 0.18, metalness: 0.5 });
5102
5123
  const ALLOY = CM("#b9bec2", { metalness: 0.75, roughness: 0.3 });
5103
5124
  const ALLOYD = CM("#3a3e42", { metalness: 0.6, roughness: 0.35 });
@@ -5111,7 +5132,7 @@ var GameVehiclesThree = (() => {
5111
5132
  const a = (y - o.ys) / (o.belt - o.ys);
5112
5133
  return o.kt * a * a;
5113
5134
  }
5114
- const b = clamp3((o.ys - y) / (o.ys - o.yb), 0, 1.25);
5135
+ const b = clamp5((o.ys - y) / (o.ys - o.yb), 0, 1.25);
5115
5136
  return o.kb * b * b;
5116
5137
  }
5117
5138
  const f = function(x, y, z) {
@@ -5191,13 +5212,13 @@ var GameVehiclesThree = (() => {
5191
5212
  });
5192
5213
  }
5193
5214
  function roofAt(gh, x) {
5194
- const rR = gh.roofR || gh.roof, t = clamp3((x - gh.rt) / (gh.ft - gh.rt), 0, 1);
5215
+ const rR = gh.roofR || gh.roof, t = clamp5((x - gh.rt) / (gh.ft - gh.rt), 0, 1);
5195
5216
  return rR + (gh.roof - rR) * t + (gh.crown || 0) * 4 * t * (1 - t);
5196
5217
  }
5197
5218
  function windows(gh) {
5198
5219
  const inn = 0.07, rise = gh.rise || 0;
5199
5220
  function ybAt(x) {
5200
- return gh.belt + 0.035 + rise * clamp3((gh.fb - x) / (gh.fb - gh.rb), 0, 1);
5221
+ return gh.belt + 0.035 + rise * clamp5((gh.fb - x) / (gh.fb - gh.rb), 0, 1);
5201
5222
  }
5202
5223
  function lx(x0, x1, y, top) {
5203
5224
  return x0 + (x1 - x0) * (y - gh.belt) / (top - gh.belt);
@@ -5400,7 +5421,7 @@ var GameVehiclesThree = (() => {
5400
5421
  }
5401
5422
  const G22 = { P, N, nu, nv, y0: o.y0, y1: o.y1, len: tot * (full ? 2 : 1), E, sd };
5402
5423
  G22.at = function(u, y) {
5403
- const fi = clamp3(u, 0, 1) * nu, fj = clamp3((y - o.y0) / (o.y1 - o.y0), 0, 1) * nv, i0 = Math.min(nu - 1, Math.floor(fi)), j0 = Math.min(nv - 1, Math.floor(fj)), a = fi - i0, b = fj - j0;
5424
+ const fi = clamp5(u, 0, 1) * nu, fj = clamp5((y - o.y0) / (o.y1 - o.y0), 0, 1) * nv, i0 = Math.min(nu - 1, Math.floor(fi)), j0 = Math.min(nv - 1, Math.floor(fj)), a = fi - i0, b = fj - j0;
5404
5425
  function L(M) {
5405
5426
  return M[i0][j0].clone().multiplyScalar((1 - a) * (1 - b)).addScaledVector(M[i0 + 1][j0], a * (1 - b)).addScaledVector(M[i0][j0 + 1], (1 - a) * b).addScaledVector(M[i0 + 1][j0 + 1], a * b);
5406
5427
  }
@@ -5588,7 +5609,7 @@ var GameVehiclesThree = (() => {
5588
5609
  off: 3e-3,
5589
5610
  nv: 4
5590
5611
  });
5591
- skin(c, Gd, o.refl || REFL, { u0: pu, u1: 1 - pu, yb, yt, off: 6e-3, nv: 4 });
5612
+ const reflector = skin(c, Gd, o.refl || REFL, { u0: pu, u1: 1 - pu, yb, yt, off: 6e-3, nv: 4 });
5592
5613
  (o.proj || []).forEach(function(q) {
5593
5614
  const r = sh(q[0]), y = (r[0] + r[1]) / 2 + (q[1] || 0), s = Gd.at(q[0], y), rad = q[2] || Math.min(0.034, (r[1] - r[0]) * 0.4);
5594
5615
  const cup = onSurf(c.cylinder(rad * 1.35, rad * 1.2, 0.012, LH, 0, 0, 0), s, 8e-3);
@@ -5619,7 +5640,7 @@ var GameVehiclesThree = (() => {
5619
5640
  lens.push(ln);
5620
5641
  if (!inner.length) inner.push(ln);
5621
5642
  const cs = Gd.at(0.5, (rg[0] + rg[1]) / 2).p;
5622
- regLight(c, "headlight", o.label || (sd > 0 ? trl("labels.right_headlight") : trl("labels.left_headlight")), inner, lens, cs);
5643
+ Object.assign(regLight(c, "headlight", o.label || (sd > 0 ? trl("labels.right_headlight") : trl("labels.left_headlight")), inner, lens, cs), { reflector: [reflector] });
5623
5644
  return Gd;
5624
5645
  };
5625
5646
  K.tail = function(sd, o) {
@@ -5980,7 +6001,7 @@ var GameVehiclesThree = (() => {
5980
6001
  c.roundBox(x1 - x0, 0.08, 0.035, 0.015, MT.trim, (x0 + x1) / 2, by + 0.05, sd * (hw - 8e-3));
5981
6002
  if (s.clad)
5982
6003
  s.arches.forEach(function(a) {
5983
- const a0 = Math.asin(clamp3((by - a[1]) / a[2], -1, 1)), tor = c.add(new THREE.TorusGeometry(a[2] + 0.02, 0.035, 6, 26, PI - 2 * a0), MT.trim, a[0], a[1], sd * (hw - 0.01));
6004
+ const a0 = Math.asin(clamp5((by - a[1]) / a[2], -1, 1)), tor = c.add(new THREE.TorusGeometry(a[2] + 0.02, 0.035, 6, 26, PI - 2 * a0), MT.trim, a[0], a[1], sd * (hw - 0.01));
5984
6005
  tor.rotation.z = a0;
5985
6006
  tor.scale.z = 0.8;
5986
6007
  });
@@ -6005,12 +6026,18 @@ var GameVehiclesThree = (() => {
6005
6026
  (s.wiper === 1 ? [[0.12, 0.66, -0.5]] : [
6006
6027
  [-0.3, 0.42, -0.36],
6007
6028
  [0.2, 0.42, -0.36]
6008
- ]).forEach(function(q) {
6029
+ ]).forEach(function(q, index) {
6009
6030
  const p0 = new V3(gh.fb, gh.belt, q[0]).addScaledVector(wd, 0.07).addScaledVector(wn, 0.035), p1 = p0.clone().addScaledVector(wd, q[1] * 0.42);
6010
6031
  p1.z += q[2];
6011
- c.rod(p0, p1, 9e-3, MT.trim);
6012
- c.cylinder(0.018, 0.018, 0.03, MT.trim, p0.x, p0.y, p0.z);
6032
+ const blade = c.rod(p0, p1, 9e-3, MT.trim);
6033
+ const cap = c.cylinder(0.018, 0.018, 0.03, MT.trim, p0.x, p0.y, p0.z);
6034
+ tagWiper({ context: c, parts: [blade, cap], spindle: p0, normal: wn, index });
6013
6035
  });
6036
+ if (s.sunroof) {
6037
+ const sx = gh.rt + (gh.ft - gh.rt) * 0.62, sl = 0.62, sw = 2 * (halfAt(gh.Wg, T, gh.roof) - 0.24), sy = roofAt(gh, sx), tilt = Math.atan2(roofAt(gh, sx + 0.3) - roofAt(gh, sx - 0.3), 0.6);
6038
+ c.roundBox(sl, 8e-3, sw, 0.01, PLASTIC, sx, sy + 4e-3, 0, { rot: [0, 0, tilt] }).castShadow = false;
6039
+ markSunroof(c.roundBox(sl + 0.03, 0.012, sw + 0.03, 0.01, MT.tint, sx, sy + 0.013, 0, { rot: [0, 0, tilt] }), sl * 0.85);
6040
+ }
6014
6041
  if (s.fin !== false)
6015
6042
  c.sphere(0.1, s.roofCol ? c.paint(s.roofCol) : GLOSS, gh.rt + 0.2, roofAt(gh, gh.rt + 0.2) + 0.045, 0, 1, 0.45, 0.28, {
6016
6043
  tl: PI / 2
@@ -6111,7 +6138,7 @@ var GameVehiclesThree = (() => {
6111
6138
  c.cylinder(0.06, 0.06, 0.012, MT.head, x + 0.055, y, z, "x");
6112
6139
  }
6113
6140
  function sm(t) {
6114
- t = clamp3(t, 0, 1);
6141
+ t = clamp5(t, 0, 1);
6115
6142
  return t * t * (3 - 2 * t);
6116
6143
  }
6117
6144
  function sideGrid(S, sd, o) {
@@ -6128,7 +6155,7 @@ var GameVehiclesThree = (() => {
6128
6155
  }
6129
6156
  const G22 = { P, N, nu, nv, y0: o.y0, y1: o.y1, len: Math.abs(o.x1 - o.x0) };
6130
6157
  G22.at = function(u, y) {
6131
- const fi = clamp3(u, 0, 1) * nu, fj = clamp3((y - o.y0) / (o.y1 - o.y0), 0, 1) * nv, i0 = Math.min(nu - 1, Math.floor(fi)), j0 = Math.min(nv - 1, Math.floor(fj)), a = fi - i0, b = fj - j0;
6158
+ const fi = clamp5(u, 0, 1) * nu, fj = clamp5((y - o.y0) / (o.y1 - o.y0), 0, 1) * nv, i0 = Math.min(nu - 1, Math.floor(fi)), j0 = Math.min(nv - 1, Math.floor(fj)), a = fi - i0, b = fj - j0;
6132
6159
  function L(M) {
6133
6160
  return M[i0][j0].clone().multiplyScalar((1 - a) * (1 - b)).addScaledVector(M[i0 + 1][j0], a * (1 - b)).addScaledVector(M[i0][j0 + 1], (1 - a) * b).addScaledVector(M[i0 + 1][j0 + 1], a * b);
6134
6161
  }
@@ -6986,6 +7013,7 @@ var GameVehiclesThree = (() => {
6986
7013
  function(c) {
6987
7014
  const hex = "#8c2b2b";
6988
7015
  car(c, {
7016
+ sunroof: fx.hasSunroof("turbo_hatch"),
6989
7017
  W: 1.751,
6990
7018
  col: hex,
6991
7019
  shelf: 0.42,
@@ -7263,6 +7291,7 @@ var GameVehiclesThree = (() => {
7263
7291
  function(c) {
7264
7292
  const hex = "#5a5f66";
7265
7293
  car(c, {
7294
+ sunroof: fx.hasSunroof("fastback_sedan"),
7266
7295
  W: 1.799,
7267
7296
  col: hex,
7268
7297
  bottomY: 0.2,
@@ -7557,6 +7586,8 @@ var GameVehiclesThree = (() => {
7557
7586
  fx.register(
7558
7587
  {
7559
7588
  key: "classic_six_sedan",
7589
+ // glass headlight lenses, as on cars of its time: they shatter (a modern car's are polycarbonate)
7590
+ headlightLens: "glass",
7560
7591
  group: "cars",
7561
7592
  name: trl("vehicles.classic_six_sedan.name"),
7562
7593
  inspired: trl("vehicles.classic_six_sedan.inspired"),
@@ -7858,7 +7889,7 @@ var GameVehiclesThree = (() => {
7858
7889
  const g = K.grid(1, 0, { z1: 0.66, y0: 0.27, y1: 0.61, du: 0.03, dv: 0.03 }), zt = 0.6, zb = 0.43, yB0 = 0.3, yT = 0.585;
7859
7890
  const gyb = function(u) {
7860
7891
  const z = Math.abs(g.zu(u));
7861
- return z <= zb ? yB0 : yB0 + (yT - 0.02 - yB0) * clamp3((z - zb) / (zt - zb), 0, 1);
7892
+ return z <= zb ? yB0 : yB0 + (yT - 0.02 - yB0) * clamp5((z - zb) / (zt - zb), 0, 1);
7862
7893
  };
7863
7894
  K.skin(g, grilleMat("mesh"), { u0: g.uz(-zt), u1: g.uz(zt), yb: gyb, yt: yT, off: 0.022, tile: 0.05, nv: 6 });
7864
7895
  outline(c2, g, { u0: g.uz(-zt), u1: g.uz(zt), yb: gyb, yt: yT }, 0.012, LH, 0.024);
@@ -8667,7 +8698,7 @@ var GameVehiclesThree = (() => {
8667
8698
  lightsY: 0.82
8668
8699
  });
8669
8700
  const tx = -1.25, len2 = 6.2, ty = 2.08, sy = 0.82, sz = 1.16;
8670
- function hz(y) {
8701
+ function hz2(y) {
8671
8702
  const d = (y - ty) / sy;
8672
8703
  return sz * Math.sqrt(Math.max(0, 1 - d * d));
8673
8704
  }
@@ -8721,12 +8752,12 @@ var GameVehiclesThree = (() => {
8721
8752
  [1, -1].forEach(function(s) {
8722
8753
  c.cylinder(0.12, 0.12, 2.3, ALU, 0.25, 1.36, s * 1, "x");
8723
8754
  c.cylinder(0.1, 0.1, 0.02, MT.dark, -0.91, 1.36, s * 1, "x");
8724
- c.roundBox(0.42, 0.32, 0.012, 0.01, CM("#d9771c"), tx + 1.6, ty - 0.25, s * (hz(ty - 0.1) + 6e-3));
8725
- c.text("33", 0.36, 0.13, tx + 1.6, ty - 0.18, s * (hz(ty - 0.1) + 0.014), s > 0 ? "+z" : "-z", { fg: "#111", size: 0.85 });
8726
- c.text("1203", 0.36, 0.13, tx + 1.6, ty - 0.32, s * (hz(ty - 0.1) + 0.014), s > 0 ? "+z" : "-z", { fg: "#111", size: 0.85 });
8727
- const dia = c.roundBox(0.3, 0.3, 0.012, 0.01, CM("#c3271c"), tx + 2.15, ty - 0.25, s * (hz(ty - 0.05) + 6e-3));
8755
+ c.roundBox(0.42, 0.32, 0.012, 0.01, CM("#d9771c"), tx + 1.6, ty - 0.25, s * (hz2(ty - 0.1) + 6e-3));
8756
+ c.text("33", 0.36, 0.13, tx + 1.6, ty - 0.18, s * (hz2(ty - 0.1) + 0.014), s > 0 ? "+z" : "-z", { fg: "#111", size: 0.85 });
8757
+ c.text("1203", 0.36, 0.13, tx + 1.6, ty - 0.32, s * (hz2(ty - 0.1) + 0.014), s > 0 ? "+z" : "-z", { fg: "#111", size: 0.85 });
8758
+ const dia = c.roundBox(0.3, 0.3, 0.012, 0.01, CM("#c3271c"), tx + 2.15, ty - 0.25, s * (hz2(ty - 0.05) + 6e-3));
8728
8759
  dia.rotation.z = PI / 4;
8729
- c.text("3", 0.18, 0.18, tx + 2.15, ty - 0.27, s * (hz(ty - 0.05) + 0.014), s > 0 ? "+z" : "-z", { fg: "#fff", size: 0.9 });
8760
+ c.text("3", 0.18, 0.18, tx + 2.15, ty - 0.27, s * (hz2(ty - 0.05) + 0.014), s > 0 ? "+z" : "-z", { fg: "#fff", size: 0.9 });
8730
8761
  c.text(trl("texts.flammable"), 1.12, 0.3, tx - 0.625, ty + 0.12, s * (sz + 0.012), s > 0 ? "+z" : "-z", { fg: "#b3261e" });
8731
8762
  for (let k = 0; k < 11; k++)
8732
8763
  c.roundBox(
@@ -8737,7 +8768,7 @@ var GameVehiclesThree = (() => {
8737
8768
  k % 2 ? REFL_W : REFL_R,
8738
8769
  tx - 2.8 + k * 0.55,
8739
8770
  ty - sy * 0.72,
8740
- s * (hz(ty - sy * 0.62) + 6e-3)
8771
+ s * (hz2(ty - sy * 0.62) + 6e-3)
8741
8772
  );
8742
8773
  c.roundBox(0.42, 0.42, 0.25, 0.04, CM("#b3261e"), 2.2, 1, s * (W / 2 - 0.15));
8743
8774
  });
@@ -11753,7 +11784,7 @@ var GameVehiclesThree = (() => {
11753
11784
  m.castShadow = true;
11754
11785
  m.receiveShadow = false;
11755
11786
  for (let k = 0; k < 14; k++) {
11756
- const hx = x0 + 0.2 + (x1 - x0 - 0.4) * kit.hash(k, 1, 7), hz = (kit.hash(k, 2, 7) - 0.5) * w * 0.85, n = 3 + Math.floor(kit.hash(k, 4, 7) * 3);
11787
+ const hx = x0 + 0.2 + (x1 - x0 - 0.4) * kit.hash(k, 1, 7), hz2 = (kit.hash(k, 2, 7) - 0.5) * w * 0.85, n = 3 + Math.floor(kit.hash(k, 4, 7) * 3);
11757
11788
  for (let q = 0; q < n; q++) {
11758
11789
  const r = 0.07 + kit.hash(k, q, 11) * 0.07, geo = new THREE.IcosahedronGeometry(r, 1), p = geo.attributes.position;
11759
11790
  for (let vi = 0; vi < p.count; vi++) {
@@ -11761,7 +11792,7 @@ var GameVehiclesThree = (() => {
11761
11792
  p.setXYZ(vi, p.getX(vi) * f, p.getY(vi) * f * 0.8, p.getZ(vi) * f);
11762
11793
  }
11763
11794
  geo.computeVertexNormals();
11764
- const ang = q / n * PI * 2 + k, tf = c.add(geo, (k + q) % 2 ? NET : NET2, hx + Math.cos(ang) * 0.1, prof(hz) + 0.1 + r * 0.5, hz + Math.sin(ang) * 0.1);
11795
+ const ang = q / n * PI * 2 + k, tf = c.add(geo, (k + q) % 2 ? NET : NET2, hx + Math.cos(ang) * 0.1, prof(hz2) + 0.1 + r * 0.5, hz2 + Math.sin(ang) * 0.1);
11765
11796
  tf.rotation.set(kit.hash(k, q, 1) * 3, kit.hash(k, q, 2) * 3, 0);
11766
11797
  }
11767
11798
  }
@@ -13644,7 +13675,7 @@ var GameVehiclesThree = (() => {
13644
13675
  }
13645
13676
  };
13646
13677
  }
13647
- function militaryRotor(c, x, y, z, n, len, chord, mat, o) {
13678
+ function militaryRotor(c, x, y, z, n, len, chord2, mat, o) {
13648
13679
  o = o || {};
13649
13680
  const g = new THREE.Group();
13650
13681
  g.position.set(x, y, z);
@@ -13652,7 +13683,7 @@ var GameVehiclesThree = (() => {
13652
13683
  for (let i = 0; i < n; i++) {
13653
13684
  const piv = new THREE.Group();
13654
13685
  piv.rotation[o.axis === "z" ? "z" : "y"] = i / n * PI * 2 + (o.phase || 0);
13655
- const geo = G2.roundedBox(len, o.th || 0.06, chord, 0.02);
13686
+ const geo = G2.roundedBox(len, o.th || 0.06, chord2, 0.02);
13656
13687
  geo.translate(len / 2 + (o.hub || 0.3), 0, 0);
13657
13688
  if (o.droop) {
13658
13689
  const p = geo.attributes.position;
@@ -13667,7 +13698,7 @@ var GameVehiclesThree = (() => {
13667
13698
  if (o.axis === "z") b.rotation.x = PI / 2;
13668
13699
  piv.add(b);
13669
13700
  if (o.tip) {
13670
- const tp = new THREE.Mesh(G2.roundedBox(0.25, (o.th || 0.06) + 5e-3, chord + 5e-3, 0.01), o.tip);
13701
+ const tp = new THREE.Mesh(G2.roundedBox(0.25, (o.th || 0.06) + 5e-3, chord2 + 5e-3, 0.01), o.tip);
13671
13702
  tp.position.set(len + (o.hub || 0.3) - 0.12, -(o.droop || 0), 0);
13672
13703
  if (o.axis === "z") tp.rotation.x = PI / 2;
13673
13704
  piv.add(tp);
@@ -14017,7 +14048,7 @@ var GameVehiclesThree = (() => {
14017
14048
  const THREE = kit.THREE;
14018
14049
  const trl = fx.t;
14019
14050
  const lbl = fx.label;
14020
- const V3 = kit.V3, PI = Math.PI, MT = kit.materials, CM = kit.makeMaterial, G2 = kit.geo, profX = kit.profX, halfAt = kit.halfAt, clamp3 = kit.clamp, smooth2 = kit.smooth;
14051
+ const V3 = kit.V3, PI = Math.PI, MT = kit.materials, CM = kit.makeMaterial, G2 = kit.geo, profX = kit.profX, halfAt = kit.halfAt, clamp5 = kit.clamp, smooth2 = kit.smooth;
14021
14052
  const GLOSS = CM("#07080a", { roughness: 0.18, metalness: 0.5 });
14022
14053
  const ALLOY = CM("#b9bec2", { metalness: 0.75, roughness: 0.3 });
14023
14054
  const ALLOYD = CM("#3a3e42", { metalness: 0.6, roughness: 0.35 });
@@ -14036,7 +14067,7 @@ var GameVehiclesThree = (() => {
14036
14067
  const a = (y - o.ys) / (o.belt - o.ys);
14037
14068
  return o.kt * a * a;
14038
14069
  }
14039
- const b = clamp3((o.ys - y) / (o.ys - o.yb), 0, 1.25);
14070
+ const b = clamp5((o.ys - y) / (o.ys - o.yb), 0, 1.25);
14040
14071
  return o.kb * b * b;
14041
14072
  }
14042
14073
  const f = function(x, y, z) {
@@ -14216,13 +14247,13 @@ var GameVehiclesThree = (() => {
14216
14247
  return L;
14217
14248
  }
14218
14249
  function roofAt(gh, x) {
14219
- const rR = gh.roofR || gh.roof, t = clamp3((x - gh.rt) / (gh.ft - gh.rt), 0, 1);
14250
+ const rR = gh.roofR || gh.roof, t = clamp5((x - gh.rt) / (gh.ft - gh.rt), 0, 1);
14220
14251
  return rR + (gh.roof - rR) * t + (gh.crown || 0) * 4 * t * (1 - t);
14221
14252
  }
14222
14253
  function windows(gh) {
14223
14254
  const inn = 0.07, rise = gh.rise || 0;
14224
14255
  function ybAt(x) {
14225
- return gh.belt + 0.035 + rise * clamp3((gh.fb - x) / (gh.fb - gh.rb), 0, 1);
14256
+ return gh.belt + 0.035 + rise * clamp5((gh.fb - x) / (gh.fb - gh.rb), 0, 1);
14226
14257
  }
14227
14258
  function lx(x0, x1, y, top) {
14228
14259
  return x0 + (x1 - x0) * (y - gh.belt) / (top - gh.belt);
@@ -14433,7 +14464,7 @@ var GameVehiclesThree = (() => {
14433
14464
  }
14434
14465
  const G22 = { P, N, nu, nv, y0: o.y0, y1: o.y1, len: tot * (full ? 2 : 1), E, sd };
14435
14466
  G22.at = function(u, y) {
14436
- const fi = clamp3(u, 0, 1) * nu, fj = clamp3((y - o.y0) / (o.y1 - o.y0), 0, 1) * nv, i0 = Math.min(nu - 1, Math.floor(fi)), j0 = Math.min(nv - 1, Math.floor(fj)), a = fi - i0, b = fj - j0;
14467
+ const fi = clamp5(u, 0, 1) * nu, fj = clamp5((y - o.y0) / (o.y1 - o.y0), 0, 1) * nv, i0 = Math.min(nu - 1, Math.floor(fi)), j0 = Math.min(nv - 1, Math.floor(fj)), a = fi - i0, b = fj - j0;
14437
14468
  function L(M) {
14438
14469
  return M[i0][j0].clone().multiplyScalar((1 - a) * (1 - b)).addScaledVector(M[i0 + 1][j0], a * (1 - b)).addScaledVector(M[i0][j0 + 1], (1 - a) * b).addScaledVector(M[i0 + 1][j0 + 1], a * b);
14439
14470
  }
@@ -14994,7 +15025,7 @@ var GameVehiclesThree = (() => {
14994
15025
  c.roundBox(x1 - x0, 0.08, 0.035, 0.015, MT.trim, (x0 + x1) / 2, by + 0.05, sd * (hw - 8e-3));
14995
15026
  if (s.clad)
14996
15027
  s.arches.forEach(function(a) {
14997
- const a0 = Math.asin(clamp3((by - a[1]) / a[2], -1, 1)), tor = c.add(new THREE.TorusGeometry(a[2] + 0.02, 0.035, 6, 26, PI - 2 * a0), MT.trim, a[0], a[1], sd * (hw - 0.01));
15028
+ const a0 = Math.asin(clamp5((by - a[1]) / a[2], -1, 1)), tor = c.add(new THREE.TorusGeometry(a[2] + 0.02, 0.035, 6, 26, PI - 2 * a0), MT.trim, a[0], a[1], sd * (hw - 0.01));
14998
15029
  tor.rotation.z = a0;
14999
15030
  tor.scale.z = 0.8;
15000
15031
  });
@@ -15012,8 +15043,11 @@ var GameVehiclesThree = (() => {
15012
15043
  );
15013
15044
  });
15014
15045
  const wy = gh.belt + 0.06, wx = gh.fb - 0.03;
15015
- [-0.3, 0.2].forEach(function(z) {
15016
- c.rod(new V3(wx + 0.02, wy, z + 0.02), new V3(wx - 0.13, wy + 0.13, z - 0.36), 9e-3, MT.trim);
15046
+ [-0.3, 0.2].forEach(function(z, index) {
15047
+ const spindle = new V3(wx + 0.02, wy, z + 0.02);
15048
+ const blade = c.rod(spindle, new V3(wx - 0.13, wy + 0.13, z - 0.36), 9e-3, MT.trim);
15049
+ const along = new V3(gh.ft - gh.fb, gh.roof - gh.belt, 0).normalize();
15050
+ tagWiper({ context: c, parts: [blade], spindle, normal: new V3(along.y, -along.x, 0), index });
15017
15051
  });
15018
15052
  if (s.fin !== false)
15019
15053
  c.sphere(0.1, s.roofCol ? c.paint(s.roofCol) : GLOSS, gh.rt + 0.2, roofAt(gh, gh.rt + 0.2) + 0.045, 0, 1, 0.45, 0.28, {
@@ -15115,7 +15149,7 @@ var GameVehiclesThree = (() => {
15115
15149
  c.cylinder(0.06, 0.06, 0.012, MT.head, x + 0.055, y, z, "x");
15116
15150
  }
15117
15151
  function sm(t) {
15118
- t = clamp3(t, 0, 1);
15152
+ t = clamp5(t, 0, 1);
15119
15153
  return t * t * (3 - 2 * t);
15120
15154
  }
15121
15155
  function sideGrid(S, sd, o) {
@@ -15132,7 +15166,7 @@ var GameVehiclesThree = (() => {
15132
15166
  }
15133
15167
  const G22 = { P, N, nu, nv, y0: o.y0, y1: o.y1, len: Math.abs(o.x1 - o.x0) };
15134
15168
  G22.at = function(u, y) {
15135
- const fi = clamp3(u, 0, 1) * nu, fj = clamp3((y - o.y0) / (o.y1 - o.y0), 0, 1) * nv, i0 = Math.min(nu - 1, Math.floor(fi)), j0 = Math.min(nv - 1, Math.floor(fj)), a = fi - i0, b = fj - j0;
15169
+ const fi = clamp5(u, 0, 1) * nu, fj = clamp5((y - o.y0) / (o.y1 - o.y0), 0, 1) * nv, i0 = Math.min(nu - 1, Math.floor(fi)), j0 = Math.min(nv - 1, Math.floor(fj)), a = fi - i0, b = fj - j0;
15136
15170
  function L(M) {
15137
15171
  return M[i0][j0].clone().multiplyScalar((1 - a) * (1 - b)).addScaledVector(M[i0 + 1][j0], a * (1 - b)).addScaledVector(M[i0][j0 + 1], (1 - a) * b).addScaledVector(M[i0 + 1][j0 + 1], a * b);
15138
15172
  }
@@ -15293,7 +15327,7 @@ var GameVehiclesThree = (() => {
15293
15327
  return function(c, I) {
15294
15328
  const gh = I.gh, col = c.meta.color;
15295
15329
  function yb(x) {
15296
- return gh.belt + 0.035 + (gh.rise || 0) * clamp3((gh.fb - x) / (gh.fb - gh.rb), 0, 1);
15330
+ return gh.belt + 0.035 + (gh.rise || 0) * clamp5((gh.fb - x) / (gh.fb - gh.rb), 0, 1);
15297
15331
  }
15298
15332
  ;
15299
15333
  [1, -1].forEach(function(sd) {
@@ -18089,6 +18123,8 @@ var GameVehiclesThree = (() => {
18089
18123
  fx.register(
18090
18124
  {
18091
18125
  key: "beetle",
18126
+ // glass headlight lenses, as on cars of its time: they shatter (a modern car's are polycarbonate)
18127
+ headlightLens: "glass",
18092
18128
  group: "cars",
18093
18129
  name: trl("vehicles.beetle.name"),
18094
18130
  inspired: trl("vehicles.beetle.inspired"),
@@ -18164,7 +18200,7 @@ var GameVehiclesThree = (() => {
18164
18200
  return n.y > 0.3 && ce.x > 0.9 && ce.y > 0.6;
18165
18201
  },
18166
18202
  sdf: function(x, y, z) {
18167
- const t = clamp3((x - 1.17) / 0.5, 0, 1);
18203
+ const t = clamp5((x - 1.17) / 0.5, 0, 1);
18168
18204
  return Math.max(0.94 - x, x - 1.69, Math.abs(z) - (0.44 - 0.16 * t * t));
18169
18205
  }
18170
18206
  });
@@ -18868,8 +18904,8 @@ var GameVehiclesThree = (() => {
18868
18904
  ];
18869
18905
  body(c, RB, col);
18870
18906
  [1, -1].forEach(function(s) {
18871
- const x = -0.3, y = 0.6, hz = loftHalf(RB, x, y) + 4e-3;
18872
- c.text(trl("texts.bis_125"), 0.2, 0.05, x, y, s * hz, s > 0 ? "+z" : "-z", { fg: "#f2f0ea", size: 0.85 });
18907
+ const x = -0.3, y = 0.6, hz2 = loftHalf(RB, x, y) + 4e-3;
18908
+ c.text(trl("texts.bis_125"), 0.2, 0.05, x, y, s * hz2, s > 0 ? "+z" : "-z", { fg: "#f2f0ea", size: 0.85 });
18873
18909
  body(
18874
18910
  c,
18875
18911
  [
@@ -22162,7 +22198,7 @@ var GameVehiclesThree = (() => {
22162
22198
  const THREE = kit.THREE;
22163
22199
  const trl = fx.t;
22164
22200
  const lbl = fx.label;
22165
- const V3 = kit.V3, PI = Math.PI, MT = kit.materials, CM = kit.makeMaterial, clamp3 = kit.clamp, smooth2 = kit.smooth;
22201
+ const V3 = kit.V3, PI = Math.PI, MT = kit.materials, CM = kit.makeMaterial, clamp5 = kit.clamp, smooth2 = kit.smooth;
22166
22202
  const CLAD = CM("#1a1b1d", { roughness: 0.82 }), LINER = CM("#1c1d1f", { roughness: 0.92 }), RIMDARK = CM("#2a2d30", { metalness: 0.6, roughness: 0.4 });
22167
22203
  const RIMSIL = CM("#b9bec2", { metalness: 0.75, roughness: 0.3 }), RIMGUN = CM("#55595d", { metalness: 0.7, roughness: 0.35 });
22168
22204
  function axles(WB, oF, L, dF, dR) {
@@ -22193,16 +22229,16 @@ var GameVehiclesThree = (() => {
22193
22229
  const m = c.meta, W = m.width, by = o.bottom != null ? o.bottom : m.clearance, top = o.top != null ? o.top : m.belt;
22194
22230
  const Wg = m.glasshouse && m.glasshouse.Wg || W, front = m.front, rear = m.rear;
22195
22231
  const tuck = o.tuck == null ? 0.045 : o.tuck, peak = o.peak || 0.45;
22196
- const B = o.sh != null ? o.sh : clamp3(1 - (Wg + 0.06) / W, 0, 0.06);
22232
+ const B = o.sh != null ? o.sh : clamp5(1 - (Wg + 0.06) / W, 0, 0.06);
22197
22233
  const Rf = o.Rf || 0.55, Rr = o.Rr || 0.42, pf = o.pf == null ? 0.08 : o.pf, pr = o.pr == null ? 0.05 : o.pr;
22198
22234
  const bowF = o.bowF == null ? 0.07 : o.bowF, bowR = o.bowR == null ? 0.04 : o.bowR, Rb = o.Rb || 0.35, hw = W / 2;
22199
22235
  const crown = o.crown == null ? 0.045 : o.crown, xc = o.crownX != null ? o.crownX : m.glasshouse ? m.glasshouse.fb : front - 1, y0 = by + 0.55 * (top - by);
22200
22236
  function sc(y) {
22201
- const t = clamp3((y - by) / (top - by), 0, 1);
22237
+ const t = clamp5((y - by) / (top - by), 0, 1);
22202
22238
  return t < peak ? 1 - tuck * Math.pow(1 - t / peak, 2) : 1 - B * Math.pow((t - peak) / (1 - peak), 2);
22203
22239
  }
22204
22240
  function F(v2) {
22205
- const a = smooth2((v2.x - (front - Rf)) / Rf), b = smooth2((rear + Rr - v2.x) / Rr), zz = clamp3(v2.z / hw, -1, 1) * clamp3(v2.z / hw, -1, 1);
22241
+ const a = smooth2((v2.x - (front - Rf)) / Rf), b = smooth2((rear + Rr - v2.x) / Rr), zz = clamp5(v2.z / hw, -1, 1) * clamp5(v2.z / hw, -1, 1);
22206
22242
  const bf = smooth2((v2.x - (front - Rb)) / Rb), br = smooth2((rear + Rb - v2.x) / Rb);
22207
22243
  const cr = crown * (1 - zz) * smooth2((v2.y - y0) / (top - y0)) * smooth2((v2.x - xc) / 0.3);
22208
22244
  v2.x += -bowF * zz * bf + bowR * zz * br;
@@ -22279,7 +22315,7 @@ var GameVehiclesThree = (() => {
22279
22315
  o = o || {};
22280
22316
  const wd = o.w || 0.075, dp = o.d || 0.045, mat = o.mat || CLAD, out = [];
22281
22317
  arches.forEach(function(a) {
22282
- const a0 = Math.asin(clamp3((by - a[1]) / a[2], -1, 1)) - 0.02, r0 = a[2] - (o.inner == null ? 0.055 : o.inner), r1 = a[2] + wd;
22318
+ const a0 = Math.asin(clamp5((by - a[1]) / a[2], -1, 1)) - 0.02, r0 = a[2] - (o.inner == null ? 0.055 : o.inner), r1 = a[2] + wd;
22283
22319
  const sh = new THREE.Shape();
22284
22320
  if (o.trap) {
22285
22321
  const yb0 = Math.sin(a0) * r1, xb0 = Math.cos(a0) * r1, top = r1 * 0.93, xt = r1 * 0.52;
@@ -22629,7 +22665,7 @@ var GameVehiclesThree = (() => {
22629
22665
  function roof(c, drop, x0, x1) {
22630
22666
  const m = c.meta, b = m.glasshouse.belt, h = m.top - b;
22631
22667
  function dy(x, y) {
22632
- const t = clamp3((y - b) / h, 0, 1);
22668
+ const t = clamp5((y - b) / h, 0, 1);
22633
22669
  return t <= 0 ? 0 : drop * smooth2((x0 - x) / (x0 - x1)) * t;
22634
22670
  }
22635
22671
  c.root.updateMatrixWorld(true);
@@ -23116,17 +23152,17 @@ var GameVehiclesThree = (() => {
23116
23152
  }
23117
23153
  beltline(c2, -0.35, 0.035, col);
23118
23154
  pillarC(c2, -1.12, [-1.36, 1.25]);
23119
- const E = glasshouse(c2), hz = function(y) {
23155
+ const E = glasshouse(c2), hz2 = function(y) {
23120
23156
  return kit.halfAt(m.glasshouse.Wg, m.tumble, y) + 0.03;
23121
23157
  };
23122
23158
  [1, -1].forEach(function(sd) {
23123
23159
  c2.tube(
23124
23160
  [
23125
- [m.glasshouse.fb - 0.08, E.yb - 0.03, sd * hz(E.yb)],
23126
- [-0.35, E.yb - 0.03, sd * hz(E.yb)],
23127
- [-1.45, E.yb + 0.01, sd * hz(E.yb)],
23128
- [E.back(E.yb + 0.15) + 0.05, E.yb + 0.15, sd * hz(E.yb + 0.15)],
23129
- [E.back(E.top - 0.05) + 0.06, E.top - 0.05, sd * hz(E.top - 0.05)]
23161
+ [m.glasshouse.fb - 0.08, E.yb - 0.03, sd * hz2(E.yb)],
23162
+ [-0.35, E.yb - 0.03, sd * hz2(E.yb)],
23163
+ [-1.45, E.yb + 0.01, sd * hz2(E.yb)],
23164
+ [E.back(E.yb + 0.15) + 0.05, E.yb + 0.15, sd * hz2(E.yb + 0.15)],
23165
+ [E.back(E.top - 0.05) + 0.06, E.top - 0.05, sd * hz2(E.top - 0.05)]
23130
23166
  ],
23131
23167
  9e-3,
23132
23168
  MT.chrome,
@@ -23823,8 +23859,8 @@ var GameVehiclesThree = (() => {
23823
23859
  setOnHit(pt, function(c2, p0) {
23824
23860
  const o = p0.obj;
23825
23861
  if (o && o.userData.hung && o.parent && o.parent !== c2.vehicle) {
23826
- o.parent.rotation.x = clamp3(o.parent.rotation.x, -0.22, 0.22);
23827
- o.parent.rotation.y = clamp3(o.parent.rotation.y, -0.08, 0.08);
23862
+ o.parent.rotation.x = clamp5(o.parent.rotation.x, -0.22, 0.22);
23863
+ o.parent.rotation.y = clamp5(o.parent.rotation.y, -0.08, 0.08);
23828
23864
  }
23829
23865
  });
23830
23866
  });
@@ -24475,7 +24511,7 @@ var GameVehiclesThree = (() => {
24475
24511
  const shared2 = fx.shared;
24476
24512
  const trl = fx.t;
24477
24513
  const lbl = fx.label;
24478
- const V3 = kit.V3, PI = Math.PI, MT = kit.materials, CM = kit.makeMaterial, _paintCol = kit.paintColor, G2 = kit.geo, DS = THREE.DoubleSide, clamp3 = kit.clamp;
24514
+ const V3 = kit.V3, PI = Math.PI, MT = kit.materials, CM = kit.makeMaterial, _paintCol = kit.paintColor, G2 = kit.geo, DS = THREE.DoubleSide, clamp5 = kit.clamp;
24479
24515
  function colorize(geo, fn) {
24480
24516
  const p = geo.attributes.position, col = new Float32Array(p.count * 3);
24481
24517
  for (let i = 0; i < p.count; i++) {
@@ -24491,7 +24527,7 @@ var GameVehiclesThree = (() => {
24491
24527
  return a + (b - a) * t;
24492
24528
  }
24493
24529
  function sstep(a, b, x) {
24494
- const t = clamp3((x - a) / (b - a), 0, 1);
24530
+ const t = clamp5((x - a) / (b - a), 0, 1);
24495
24531
  return t * t * (3 - 2 * t);
24496
24532
  }
24497
24533
  function tab(T) {
@@ -24568,7 +24604,7 @@ var GameVehiclesThree = (() => {
24568
24604
  o,
24569
24605
  sec,
24570
24606
  t: function(x) {
24571
- return clamp3((x - o.x0) / o.L, 0, 1);
24607
+ return clamp5((x - o.x0) / o.L, 0, 1);
24572
24608
  },
24573
24609
  /* half beam of the hull at height y (inside, so floors and decks do not leak) */
24574
24610
  half: function(x, y) {
@@ -24742,7 +24778,7 @@ var GameVehiclesThree = (() => {
24742
24778
  for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
24743
24779
  const a = poly[i], b = poly[j];
24744
24780
  if (a[1] > y !== b[1] > y && x < (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]) + a[0]) inside = !inside;
24745
- const vx = b[0] - a[0], vy = b[1] - a[1], wx = x - a[0], wy = y - a[1], t = clamp3((wx * vx + wy * vy) / (vx * vx + vy * vy), 0, 1);
24781
+ const vx = b[0] - a[0], vy = b[1] - a[1], wx = x - a[0], wy = y - a[1], t = clamp5((wx * vx + wy * vy) / (vx * vx + vy * vy), 0, 1);
24746
24782
  d = Math.min(d, Math.hypot(wx - vx * t, wy - vy * t));
24747
24783
  }
24748
24784
  return inside ? -d : d;
@@ -27290,6 +27326,7 @@ var GameVehiclesThree = (() => {
27290
27326
  shared: shared2,
27291
27327
  t,
27292
27328
  label: (key) => ({ key, label: t("labels." + key) }),
27329
+ hasSunroof: (key) => !!options.sunroofs?.includes(key),
27293
27330
  register(spec, build) {
27294
27331
  if (!groups.has(spec.group) || only && !only.has(spec.key) || skip.has(spec.key)) return;
27295
27332
  kit.register({ ...spec, seedKey: legacyVehicleSeeds[spec.key] }, build);
@@ -29267,8 +29304,8 @@ var GameVehiclesThree = (() => {
29267
29304
  const face = faces[i];
29268
29305
  f3(face[2] + offset, face[1] + offset, face[0] + offset);
29269
29306
  }
29270
- const layer = steps + bevelSegments * 2;
29271
- offset = vlen * layer;
29307
+ const layer2 = steps + bevelSegments * 2;
29308
+ offset = vlen * layer2;
29272
29309
  for (let i = 0; i < flen; i++) {
29273
29310
  const face = faces[i];
29274
29311
  f3(face[0] + offset, face[1] + offset, face[2] + offset);
@@ -30771,7 +30808,7 @@ var GameVehiclesThree = (() => {
30771
30808
  const nr = g.attributes.normal;
30772
30809
  const hx = w / 2 - r;
30773
30810
  const hy = h / 2 - r;
30774
- const hz = d / 2 - r;
30811
+ const hz2 = d / 2 - r;
30775
30812
  function map(v, hi) {
30776
30813
  const i = Math.round((v + 0.5) * n);
30777
30814
  return i <= nb ? -hi - r * (nb - i) / nb : hi + r * (i - nb - 1) / nb;
@@ -30780,8 +30817,8 @@ var GameVehiclesThree = (() => {
30780
30817
  const c = new V3();
30781
30818
  const dir = new V3();
30782
30819
  for (let i = 0; i < p.count; i++) {
30783
- q.set(map(p.getX(i), hx), map(p.getY(i), hy), map(p.getZ(i), hz));
30784
- c.set(clamp(q.x, -hx, hx), clamp(q.y, -hy, hy), clamp(q.z, -hz, hz));
30820
+ q.set(map(p.getX(i), hx), map(p.getY(i), hy), map(p.getZ(i), hz2));
30821
+ c.set(clamp(q.x, -hx, hx), clamp(q.y, -hy, hy), clamp(q.z, -hz2, hz2));
30785
30822
  dir.subVectors(q, c);
30786
30823
  if (dir.lengthSq() > 1e-12) {
30787
30824
  dir.normalize();
@@ -33504,13 +33541,13 @@ var GameVehiclesThree = (() => {
33504
33541
  );
33505
33542
  const hy = hl.y || s.headY || s.noseY - 0.06;
33506
33543
  const hx = I.profX(it, hy, true);
33507
- const hz = W / 2 - hl.in - hl.w / 2;
33544
+ const hz2 = W / 2 - hl.in - hl.w / 2;
33508
33545
  [1, -1].forEach(function(sd) {
33509
33546
  kit.headlight(c, {
33510
33547
  style: hl.style,
33511
33548
  x: hx + 0.02 + (hl.dx || 0),
33512
33549
  y: hy,
33513
- z: sd * hz,
33550
+ z: sd * hz2,
33514
33551
  w: hl.w,
33515
33552
  h: hl.h,
33516
33553
  side: sd,
@@ -33518,7 +33555,7 @@ var GameVehiclesThree = (() => {
33518
33555
  mat: hl.mat
33519
33556
  });
33520
33557
  });
33521
- if (hl.style === "ledbar") kit.lightBar(c, hx + 0.03, hy + hl.h * 0.3, -hz + hl.w / 2, hz - hl.w / 2, 0.018, 1);
33558
+ if (hl.style === "ledbar") kit.lightBar(c, hx + 0.03, hy + hl.h * 0.3, -hz2 + hl.w / 2, hz2 - hl.w / 2, 0.018, 1);
33522
33559
  if (s.grille !== false && !s.noGrille) {
33523
33560
  const gr = Object.assign({ style: "bars", w: W * 0.44, h: Math.max(0.08, hy - bumperY - 0.12) }, s.grille || {});
33524
33561
  const gy = gr.y || (bumperY + hy) / 2 + 0.02;
@@ -35135,8 +35172,20 @@ var GameVehiclesThree = (() => {
35135
35172
  if (!L || L.broken) return;
35136
35173
  L.broken = true;
35137
35174
  L.lens.forEach(function(m) {
35138
- m.visible = false;
35175
+ const rim = L.lensMaterial ? glassRim(m.geometry, c.rand) : null;
35176
+ if (!rim) {
35177
+ m.visible = false;
35178
+ return;
35179
+ }
35180
+ m.geometry.dispose();
35181
+ m.geometry = rim;
35182
+ m.material = MATERIALS.glassEdge;
35183
+ m.userData.lensRim = true;
35139
35184
  });
35185
+ if (L.lensMaterial)
35186
+ (L.reflector || []).forEach(function(m) {
35187
+ m.material = MATERIALS.broken;
35188
+ });
35140
35189
  L.inner.forEach(function(m) {
35141
35190
  m.material = MATERIALS.bulbBroken;
35142
35191
  });
@@ -35144,11 +35193,38 @@ var GameVehiclesThree = (() => {
35144
35193
  h.visible = false;
35145
35194
  });
35146
35195
  const p = L.group ? L.group.position.clone() : L.inner[0] ? L.inner[0].position.clone() : new V3();
35147
- I.burst(c, "lens", c.toEffects(p), 10, { dir: c.dirToEffects(dir.clone().negate()), speed: 1.4, variant: L.tint === "red" ? "red" : "clear" });
35148
- I.burst(c, "plastic", c.toEffects(p), 2, { dir: c.dirToEffects(dir.clone().negate()), speed: 1 });
35196
+ const glass = L.lensMaterial !== "plastic";
35197
+ I.burst(c, "lens", c.toEffects(p), glass ? 10 : 5, { dir: c.dirToEffects(dir.clone().negate()), speed: glass ? 1.4 : 1.1, variant: L.tint === "red" ? "red" : "clear" });
35198
+ I.burst(c, "plastic", c.toEffects(p), glass ? 2 : 5, { dir: c.dirToEffects(dir.clone().negate()), speed: 1 });
35149
35199
  }
35150
35200
  kit.breakLight = breakLight;
35151
- Object.assign(I, { glassUV, drawWeb, crackGlass, glassRim, shatterGlass, hitGlass, breakLight });
35201
+ const crazedCache = /* @__PURE__ */ new Map();
35202
+ const CRAZED_OPACITY = 0.6;
35203
+ function crazed(material) {
35204
+ const known = crazedCache.get(material);
35205
+ if (known) return known;
35206
+ const m = I.crackLens(material).clone();
35207
+ m.opacity = Math.max(m.opacity ?? 1, CRAZED_OPACITY);
35208
+ m.transparent = true;
35209
+ m.userData.shared = true;
35210
+ crazedCache.set(material, m);
35211
+ return m;
35212
+ }
35213
+ function crackLight(c, L, dir, pierced) {
35214
+ if (!L || L.broken) return;
35215
+ if (!L.cracked) {
35216
+ L.cracked = true;
35217
+ for (const m of L.lens) if (m.material && !Array.isArray(m.material)) m.material = crazed(m.material);
35218
+ }
35219
+ const p = L.group ? L.group.position.clone() : L.inner[0] ? L.inner[0].position.clone() : new V3();
35220
+ I.burst(c, "lens", c.toEffects(p), 3, { dir: c.dirToEffects(dir.clone().negate()), speed: 0.9, variant: L.tint === "red" ? "red" : "clear" });
35221
+ if (!pierced || L.dark) return;
35222
+ L.dark = true;
35223
+ for (const m of L.inner) m.material = MATERIALS.bulbBroken;
35224
+ for (const h of L.halo ?? []) h.visible = false;
35225
+ }
35226
+ kit.crackLight = crackLight;
35227
+ Object.assign(I, { glassUV, drawWeb, crackGlass, glassRim, shatterGlass, hitGlass, breakLight, crackLight });
35152
35228
  }
35153
35229
  function installImpact(kit) {
35154
35230
  const THREE = kit.THREE;
@@ -40658,6 +40734,8 @@ var GameVehiclesThree = (() => {
40658
40734
  heli: ["battery", "alternator", "gearbox"]
40659
40735
  };
40660
40736
  var HITCH_CHANCE = { car: 30, truck: 60, bus: 0, tractor: 100, quad: 40, bike: 0, tank: 0, boat: 0, plane: 0, heli: 0 };
40737
+ var GLASS_LENS_HP = 320;
40738
+ var PLASTIC_LENS_HP = 900;
40661
40739
  var STEEL_KG_M2 = 7;
40662
40740
  var ALU_KG_M2 = 3.2;
40663
40741
  var kindOf = (c) => {
@@ -40815,7 +40893,9 @@ var GameVehiclesThree = (() => {
40815
40893
  continue;
40816
40894
  }
40817
40895
  if (role === "light") {
40818
- add2({ id, role, name: label, material: "glass", box: box2, mass: 1.2 + 4 * area3, hp: 320, zoneId: id, visual: true, tags: ["light", ...tags] });
40896
+ const lens = /^head/.test(id) ? spec.headlightLens ?? "polycarbonate" : "acrylic";
40897
+ const glassLens = lens === "glass";
40898
+ add2({ id, role, name: label, material: glassLens ? "glass" : "plastic", box: box2, mass: 1.2 + 4 * area3, hp: glassLens ? GLASS_LENS_HP : PLASTIC_LENS_HP, zoneId: id, visual: true, tags: ["light", `lens_${lens}`, ...tags] });
40819
40899
  continue;
40820
40900
  }
40821
40901
  if (role === "bumper") {
@@ -41269,20 +41349,185 @@ var GameVehiclesThree = (() => {
41269
41349
  mesh2.geometry.dispose();
41270
41350
  mesh2.geometry = g;
41271
41351
  }
41352
+ var STRIP_SMOOTH_DEG = 180;
41353
+ var SKIN_BAND = 0.05;
41354
+ var BODY_FACING = 0.5;
41355
+ var FIELDS = 4;
41356
+ var SEAM_THIN = 0.03;
41357
+ var SEAM_AT_EDGE = 0.04;
41358
+ var SEAM_TALL = 0.4;
41359
+ var SLIVER_AREA = 1e-10;
41360
+ function cutStripOverDoor(vehicleKit, params) {
41361
+ const kit = vehicleKit;
41362
+ const strip = params.strip;
41363
+ const toVehicle = params.toVehicle;
41364
+ const { x0, x1 } = params;
41365
+ const THREE = kit.THREE;
41366
+ const at = new THREE.Vector3();
41367
+ const along = (x, y, z) => at.set(x, y, z).applyMatrix4(toVehicle).x;
41368
+ const piece = (tests) => {
41369
+ const mesh2 = new THREE.Mesh(strip.geometry.clone(), strip.material);
41370
+ kit.cutMesh(mesh2, tests, STRIP_SMOOTH_DEG, {});
41371
+ return mesh2;
41372
+ };
41373
+ const over = piece([{ sdf: (x, y, z) => along(x, y, z) - x0 }, { sdf: (x, y, z) => x1 - along(x, y, z) }]);
41374
+ const after = piece([{ sdf: (x, y, z) => along(x, y, z) - x1 }]);
41375
+ kit.cutMesh(strip, [{ sdf: (x, y, z) => x0 - along(x, y, z) }], STRIP_SMOOTH_DEG, {});
41376
+ const joined = joinTriangles(THREE, strip.geometry, after.geometry);
41377
+ strip.geometry.dispose();
41378
+ after.geometry.dispose();
41379
+ strip.geometry = joined;
41380
+ return over;
41381
+ }
41382
+ function joinTriangles(THREE, first, second) {
41383
+ const a = first.index ? first.toNonIndexed() : first;
41384
+ const b = second.index ? second.toNonIndexed() : second;
41385
+ const flags = [...solidTriangles(THREE, a), ...solidTriangles(THREE, b)];
41386
+ const both = new THREE.BufferGeometry();
41387
+ for (const name of Object.keys(a.attributes)) {
41388
+ const p = a.getAttribute(name);
41389
+ const q = b.getAttribute(name);
41390
+ if (!q || q.itemSize !== p.itemSize) continue;
41391
+ const array = new Float32Array(p.array.length + q.array.length);
41392
+ array.set(p.array, 0);
41393
+ array.set(q.array, p.array.length);
41394
+ both.setAttribute(name, new THREE.Float32BufferAttribute(array, p.itemSize));
41395
+ }
41396
+ if (a !== first) a.dispose();
41397
+ if (b !== second) b.dispose();
41398
+ const out = pickTriangles(THREE, both, flags, true) ?? new THREE.BufferGeometry();
41399
+ both.dispose();
41400
+ return out;
41401
+ }
41402
+ function solidTriangles(THREE, geometry) {
41403
+ const pos = geometry.getAttribute("position");
41404
+ const triangle = new THREE.Triangle();
41405
+ const flags = [];
41406
+ for (let t = 0; t * 3 + 2 < pos.count; t++) {
41407
+ triangle.a.fromBufferAttribute(pos, t * 3);
41408
+ triangle.b.fromBufferAttribute(pos, t * 3 + 1);
41409
+ triangle.c.fromBufferAttribute(pos, t * 3 + 2);
41410
+ flags.push(triangle.getArea() > SLIVER_AREA);
41411
+ }
41412
+ return flags;
41413
+ }
41414
+ function splitDoorSkin(three, meshes, toVehicleOf, side2, depth) {
41415
+ const THREE = three;
41416
+ const pieces = meshes;
41417
+ const toVehicle = toVehicleOf;
41418
+ const v = new THREE.Vector3();
41419
+ const corners = [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()];
41420
+ const triangle = new THREE.Triangle();
41421
+ const centres = pieces.map((mesh2) => {
41422
+ const geometry = mesh2.geometry.index ? mesh2.geometry.toNonIndexed() : mesh2.geometry;
41423
+ const matrix = toVehicle(mesh2);
41424
+ const pos = geometry.getAttribute("position");
41425
+ const out = new Float32Array(pos.count / 3 * FIELDS);
41426
+ for (let t = 0; t * 3 + 2 < pos.count; t++) {
41427
+ let y = 0;
41428
+ let across = 0;
41429
+ for (let k = 0; k < 3; k++) {
41430
+ corners[k].fromBufferAttribute(pos, t * 3 + k).applyMatrix4(matrix);
41431
+ y += corners[k].y / 3;
41432
+ across += side2 * corners[k].z / 3;
41433
+ }
41434
+ out[t * FIELDS] = y;
41435
+ out[t * FIELDS + 1] = across;
41436
+ triangle.set(corners[0], corners[1], corners[2]).getNormal(v);
41437
+ out[t * FIELDS + 2] = v.y;
41438
+ out[t * FIELDS + 3] = v.x;
41439
+ }
41440
+ return { geometry, out };
41441
+ });
41442
+ const outer = /* @__PURE__ */ new Map();
41443
+ for (const { out } of centres)
41444
+ for (let t = 0; t * FIELDS < out.length; t++) {
41445
+ const band = Math.floor(out[t * FIELDS] / SKIN_BAND);
41446
+ outer.set(band, Math.max(outer.get(band) ?? -Infinity, out[t * FIELDS + 1]));
41447
+ }
41448
+ const surfaceAt = (y) => {
41449
+ const band = Math.floor(y / SKIN_BAND);
41450
+ return Math.max(outer.get(band - 1) ?? -Infinity, outer.get(band) ?? -Infinity, outer.get(band + 1) ?? -Infinity);
41451
+ };
41452
+ return centres.map(({ geometry, out }, i) => {
41453
+ const near = [];
41454
+ for (let t = 0; t * FIELDS < out.length; t++) {
41455
+ const f = t * FIELDS;
41456
+ const body = out[f + 1] < surfaceAt(out[f]) - depth && (out[f + 2] < -BODY_FACING || Math.abs(out[f + 3]) > BODY_FACING);
41457
+ near.push(!body);
41458
+ }
41459
+ const result = { skin: pickTriangles(THREE, geometry, near, true), deep: pickTriangles(THREE, geometry, near, false) };
41460
+ if (geometry !== pieces[i].geometry) geometry.dispose();
41461
+ return result;
41462
+ });
41463
+ }
41464
+ function leaveDeepSkinOnBody(three, meshes, params) {
41465
+ const THREE = three;
41466
+ const pieces = meshes;
41467
+ const toVehicle = params.toVehicle;
41468
+ const { side: side2, depth } = params;
41469
+ const split = splitDoorSkin(THREE, pieces, (mesh2) => new THREE.Matrix4().multiplyMatrices(toVehicle, mesh2.matrixWorld), side2, depth);
41470
+ split.forEach(({ skin, deep }, i) => {
41471
+ const piece = pieces[i];
41472
+ if (!deep || !skin || !piece.parent) return;
41473
+ const body = new THREE.Mesh(deep, piece.material);
41474
+ body.position.copy(piece.position);
41475
+ body.quaternion.copy(piece.quaternion);
41476
+ body.scale.copy(piece.scale);
41477
+ body.castShadow = piece.castShadow;
41478
+ body.receiveShadow = piece.receiveShadow;
41479
+ piece.parent.add(body);
41480
+ piece.geometry.dispose();
41481
+ piece.geometry = skin;
41482
+ });
41483
+ }
41484
+ function pickTriangles(THREE, geometry, flags, wanted) {
41485
+ const count2 = flags.filter((flag) => flag === wanted).length;
41486
+ if (!count2) return null;
41487
+ const out = new THREE.BufferGeometry();
41488
+ for (const name of Object.keys(geometry.attributes)) {
41489
+ const attribute = geometry.getAttribute(name);
41490
+ const size = attribute.itemSize;
41491
+ const source = attribute.array;
41492
+ const array = new Float32Array(count2 * 3 * size);
41493
+ let w = 0;
41494
+ flags.forEach((flag, t) => {
41495
+ if (flag !== wanted) return;
41496
+ array.set(source.subarray(t * 3 * size, (t + 1) * 3 * size), w);
41497
+ w += 3 * size;
41498
+ });
41499
+ out.setAttribute(name, new THREE.Float32BufferAttribute(array, size));
41500
+ }
41501
+ out.computeBoundingBox();
41502
+ out.computeBoundingSphere();
41503
+ return out;
41504
+ }
41505
+ function isDoorSeam(three, params) {
41506
+ const THREE = three;
41507
+ const box2 = params.box;
41508
+ const { zone, side: side2, dark } = params;
41509
+ if (!dark || box2.isEmpty()) return false;
41510
+ const size = box2.getSize(new THREE.Vector3());
41511
+ const centre = box2.getCenter(new THREE.Vector3());
41512
+ if (Math.sign(centre.z) !== Math.sign(side2) || size.x > SEAM_THIN || size.z > SEAM_THIN || size.y < (zone.y1 - zone.y0) * SEAM_TALL) return false;
41513
+ if (centre.y < zone.y0 || centre.y > zone.y1) return false;
41514
+ return Math.abs(centre.x - zone.x0) < SEAM_AT_EDGE || Math.abs(centre.x - zone.x1) < SEAM_AT_EDGE;
41515
+ }
41272
41516
  var HOLE_DECAL_SCALE = 2.4;
41273
41517
  var DOOR_DEPTH = 0.07;
41518
+ var DOOR_SKIN_DEPTH = 0.09;
41519
+ var DOOR_FLOOR_DEPTH = 0.2;
41274
41520
  var DOOR_EDGE_PIECES = 8;
41275
41521
  var DOOR_EDGE_REACH = 0.08;
41276
41522
  var DOOR_CARD_COLOR = "#3b3e42";
41277
41523
  var DOOR_LINER_COLOR = "#5a5e64";
41278
41524
  var DOOR_LINER_DARK = 0.15;
41279
- var DOOR_STRIP_THIN = 0.08;
41525
+ var DOOR_STRIP_THIN = 0.13;
41280
41526
  var DOOR_STRIP_NEAR = 0.04;
41281
41527
  var DOOR_STRIP_TALL = 0.22;
41282
- var STRIP_SMOOTH_DEG = 180;
41283
- var DOOR_STRIP_VERTICES = 2e4;
41528
+ var DOOR_STRIP_VERTICES = 6e4;
41284
41529
  var DOOR_SILL = 0.25;
41285
- var DOOR_STRIP_OVER = 0.3;
41530
+ var DOOR_STRIP_OVER = 0.05;
41286
41531
  var DOOR_TOP_OVER_PANE = 0.03;
41287
41532
  var HOLE_TEXTURE_SIZE = 64;
41288
41533
  var LAMINATED_HP = 1e3;
@@ -41386,6 +41631,8 @@ var GameVehiclesThree = (() => {
41386
41631
  __publicField(this, "syncPlates", /* @__PURE__ */ new Map());
41387
41632
  __publicField(this, "skins", /* @__PURE__ */ new Map());
41388
41633
  __publicField(this, "doorTops", /* @__PURE__ */ new Map());
41634
+ __publicField(this, "seams", /* @__PURE__ */ new Map());
41635
+ __publicField(this, "openNow", /* @__PURE__ */ new Set());
41389
41636
  __publicField(this, "lightsOn", true);
41390
41637
  __publicField(this, "warming");
41391
41638
  __publicField(this, "tornThisFrame", false);
@@ -41562,6 +41809,11 @@ var GameVehiclesThree = (() => {
41562
41809
  }
41563
41810
  panel.visible = open && !this.skins.get(id);
41564
41811
  recess.visible = open && !this.skins.get(id);
41812
+ if (open) this.openNow.add(id);
41813
+ else this.openNow.delete(id);
41814
+ this.seams.forEach((doors, seam) => {
41815
+ if (doors.has(id)) seam.visible = ![...doors].some((door) => this.openNow.has(door));
41816
+ });
41565
41817
  };
41566
41818
  const euler = piv.rotation;
41567
41819
  const quaternion = piv.quaternion;
@@ -41604,11 +41856,13 @@ var GameVehiclesThree = (() => {
41604
41856
  const pcs = I.cutPanel(this.c, z, rect, true, dirty, { anyN: true }).map((x) => x.mesh).filter((m) => !!m);
41605
41857
  this.mend(dirty);
41606
41858
  if (pcs.length) {
41859
+ leaveDeepSkinOnBody(this.THREE, pcs, { toVehicle: inv, side: z.side < 0 ? -1 : 1, depth: DOOR_SKIN_DEPTH });
41607
41860
  const panes = I.windowsOver(this.c, z, inv);
41608
41861
  const seals = panes.map((g) => I.paneSeals.get(g)).filter((s) => !!s);
41609
41862
  const mirrors = this.mirrorsOn(z);
41610
41863
  const frames = this.framesAround(panes, inv);
41611
- objs = [...pcs, ...I.followersIn(this.c, z, inv), ...panes, ...seals, ...mirrors, ...frames];
41864
+ const followers = I.followersIn(this.c, z, inv).filter((o) => !this.takeSeam(o, id, z, inv) && !this.inFloor(o, piv, z, inv));
41865
+ objs = [...pcs, ...followers, ...panes, ...seals, ...mirrors, ...frames];
41612
41866
  }
41613
41867
  } else {
41614
41868
  const pcs = this.cutZone(z);
@@ -41636,6 +41890,25 @@ var GameVehiclesThree = (() => {
41636
41890
  this.c.root.updateMatrixWorld(true);
41637
41891
  return objs.length > 0;
41638
41892
  }
41893
+ /** A thin part at the foot of the opening, well inside the skin (a step, a sill trim, a rail on the floor), is the floor's: it does not swing with the door. */
41894
+ inFloor(o, piv, z, inv) {
41895
+ const side2 = z.side < 0 ? -1 : 1;
41896
+ const centre = this.I.localBox(this.c, o, inv).getCenter(new this.THREE.Vector3());
41897
+ const skin = side2 * (piv.position.z - side2 * 0.016);
41898
+ return centre.y < z.box.y0 + DOOR_SILL && side2 * centre.z < skin - DOOR_FLOOR_DEPTH;
41899
+ }
41900
+ /** A shut line drawn at the door's edge stays on the body, hidden while a door it marks stands open; answers whether `o` is one. */
41901
+ takeSeam(o, id, z, inv) {
41902
+ const B = z.box;
41903
+ const box2 = this.I.localBox(this.c, o, inv);
41904
+ const color2 = o.material?.color;
41905
+ const dark = !!color2 && color2.getHSL({ h: 0, s: 0, l: 0 }).l < DOOR_LINER_DARK;
41906
+ if (!o.isMesh || !isDoorSeam(this.THREE, { box: box2, zone: { x0: B.x0, x1: B.x1, y0: B.y0, y1: B.y1 }, side: z.side, dark })) return false;
41907
+ const doors = this.seams.get(o) ?? /* @__PURE__ */ new Set();
41908
+ doors.add(id);
41909
+ this.seams.set(o, doors);
41910
+ return true;
41911
+ }
41639
41912
  /** A door has edges: a band that follows the skin all round closes it to its card and hides where the cut followed the triangles of the shell; the same band, in the same paint as on a real car, lines the opening. */
41640
41913
  doorEdges(piv, z, y1) {
41641
41914
  const THREE = this.THREE;
@@ -41657,13 +41930,15 @@ var GameVehiclesThree = (() => {
41657
41930
  const at = this.surface("z", side2, B, [[x, y]]);
41658
41931
  return at != null && Math.abs(at - flat) < DOOR_EDGE_REACH ? at : flat;
41659
41932
  });
41660
- const band = (lift) => {
41933
+ const band = (lift, path, closed) => {
41661
41934
  const position = [];
41662
41935
  const index = [];
41663
- loop.forEach(([x, y], i) => {
41664
- position.push(x, y, heights[i] + side2 * lift, x, y, heights[i] + side2 * (lift - DOOR_DEPTH));
41936
+ path.forEach((at, i) => {
41937
+ const [x, y] = loop[at];
41938
+ position.push(x, y, heights[at] + side2 * lift, x, y, heights[at] + side2 * (lift - DOOR_DEPTH));
41939
+ if (!closed && i === path.length - 1) return;
41665
41940
  const a = i * 2;
41666
- const b = (i + 1) % loop.length * 2;
41941
+ const b = (i + 1) % path.length * 2;
41667
41942
  index.push(a, a + 1, b, b, a + 1, b + 1);
41668
41943
  });
41669
41944
  const g = new THREE.BufferGeometry();
@@ -41691,8 +41966,11 @@ var GameVehiclesThree = (() => {
41691
41966
  card.castShadow = false;
41692
41967
  this.c.vehicle.add(card);
41693
41968
  piv.attach(card);
41694
- const edges = new THREE.Mesh(band(2e-3), paint);
41695
- const lining = new THREE.Mesh(band(2e-3), paint);
41969
+ const all = loop.map((_, i) => i);
41970
+ const top = 2 * DOOR_EDGE_PIECES;
41971
+ const sides = [...all.slice(3 * DOOR_EDGE_PIECES), ...all.slice(0, top + 1)];
41972
+ const edges = new THREE.Mesh(band(2e-3, all, true), paint);
41973
+ const lining = new THREE.Mesh(band(2e-3, sides, false), paint);
41696
41974
  for (const m of [edges, lining]) {
41697
41975
  m.userData.noFollow = true;
41698
41976
  m.castShadow = false;
@@ -41749,26 +42027,14 @@ var GameVehiclesThree = (() => {
41749
42027
  const box2 = this.I.localBox(this.c, o, inv);
41750
42028
  const size = box2.getSize(new THREE.Vector3());
41751
42029
  const mid = box2.getCenter(new THREE.Vector3());
41752
- if (Math.sign(mid.z) !== Math.sign(z.side) || size.z > DOOR_STRIP_THIN || size.y > DOOR_STRIP_TALL || Math.min(box2.max.x, x1) - Math.max(box2.min.x, x0) < (x1 - x0) * DOOR_STRIP_OVER || mid.y < y0 || mid.y > y1) continue;
42030
+ if (Math.sign(mid.z) !== Math.sign(z.side) || size.z > DOOR_STRIP_THIN || size.y > DOOR_STRIP_TALL || Math.min(box2.max.x, x1) - Math.max(box2.min.x, x0) < DOOR_STRIP_OVER || mid.y < y0 || mid.y > y1) continue;
41753
42031
  const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld);
41754
- const v = new THREE.Vector3();
41755
- const sdf = (x, y, zz) => {
41756
- v.set(x, y, zz).applyMatrix4(toVehicle);
41757
- return Math.max(x0 - v.x, v.x - x1);
41758
- };
41759
- const over = new THREE.Mesh(o.geometry.clone(), o.material);
42032
+ const over = cutStripOverDoor(this.kit, { strip: o, toVehicle, x0, x1 });
41760
42033
  over.position.copy(o.position);
41761
42034
  over.quaternion.copy(o.quaternion);
41762
42035
  over.scale.copy(o.scale);
41763
42036
  this.c.vehicle.add(over);
41764
42037
  over.updateMatrixWorld(true);
41765
- const overToVehicle = new THREE.Matrix4().multiplyMatrices(inv, over.matrixWorld);
41766
- const w = new THREE.Vector3();
41767
- this.kit.cutMesh(over, [{ sdf: (x, y, zz) => {
41768
- w.set(x, y, zz).applyMatrix4(overToVehicle);
41769
- return -Math.max(x0 - w.x, w.x - x1);
41770
- } }], STRIP_SMOOTH_DEG, {});
41771
- this.kit.cutMesh(o, [{ sdf }], STRIP_SMOOTH_DEG, {});
41772
42038
  over.userData.noFollow = true;
41773
42039
  piv.attach(over);
41774
42040
  }
@@ -42349,11 +42615,18 @@ var GameVehiclesThree = (() => {
42349
42615
  }
42350
42616
  }
42351
42617
  }
42618
+ /** The lens of a light is holed or cracked and stays; `pierced`: a round went through it to the bulb, and the lamp goes dark. */
42619
+ crackLight(p, pierced) {
42620
+ const z = this.zone(p);
42621
+ if (!z) return;
42622
+ for (const L of z.lights) this.kit.crackLight(this.c, L, new this.THREE.Vector3(p.box.center[0] > 0 ? -1 : 1, 0, 0), pierced);
42623
+ }
42352
42624
  /** The lens of a light breaks and its unit stays (damaged) or goes (destroyed). */
42353
42625
  breakLight(p, gone) {
42354
42626
  const z = this.zone(p);
42355
42627
  if (!z) return;
42356
42628
  for (const L of z.lights) {
42629
+ L.lensMaterial = p.material;
42357
42630
  this.kit.breakLight(this.c, L, new this.THREE.Vector3(p.box.center[0] > 0 ? -1 : 1, 0, 0));
42358
42631
  if (gone) {
42359
42632
  if (L.group) L.group.visible = false;
@@ -42995,6 +43268,7 @@ var GameVehiclesThree = (() => {
42995
43268
  wear(p, amount, out);
42996
43269
  syncZone(p);
42997
43270
  if (p.hp <= 0) lose(p, "broken", out, cause);
43271
+ else if (p.def.role === "light") vis?.crackLight(p.def, dtype === "ballistic");
42998
43272
  if (p.def.role === "tyre" && !p.flat && p.hp < p.def.hp * 0.6 && !p.lost) {
42999
43273
  p.flat = true;
43000
43274
  vis?.tyreFlat(p.def, 0.24);
@@ -43807,7 +44081,7 @@ var GameVehiclesThree = (() => {
43807
44081
  if (!list2.length) return 1;
43808
44082
  return Math.min(...list2.map((p) => p.lost || !attached(p.def.id) ? 0 : p.hp / p.def.hp));
43809
44083
  };
43810
- const tyres = [...ps.values()].filter((p) => p.def.role === "tyre").map((p) => ({ x: p.def.box.center[0], z: p.def.box.center[2], health: tyreHealth(p) }));
44084
+ const tyres = [...ps.values()].filter((p) => p.def.role === "tyre").map((p) => ({ x: p.def.box.center[0], z: p.def.box.center[2], health: tyreHealth(p), rim: p.lost }));
43811
44085
  const status = computeStatus();
43812
44086
  const brakeSystem = [...ps.values()].find((p) => p.def.role === "brakes" && p.def.contains);
43813
44087
  const fluidLeft = brakeSystem ? clamp((brakeSystem.liters ?? 0) / brakeSystem.def.contains.liters / BRAKE_FLUID_NEEDED, 0, 1) : 1;
@@ -44035,7 +44309,7 @@ var GameVehiclesThree = (() => {
44035
44309
  };
44036
44310
  }
44037
44311
 
44038
- // ../game-audio/dist/chunk-BXUGSOPY.js
44312
+ // ../game-audio/dist/chunk-C2K7XN2K.js
44039
44313
  var DISTANCE_CURVES = {
44040
44314
  /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
44041
44315
  game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
@@ -44558,6 +44832,7 @@ var GameVehiclesThree = (() => {
44558
44832
  burst: 20,
44559
44833
  burstSpacing: 0.06
44560
44834
  };
44835
+ var CATALOG = new Map([...SFX_CATALOG, ...SFX_IMPACTS].map((entry) => [entry.id, entry]));
44561
44836
  var PT = {
44562
44837
  group: "Som",
44563
44838
  name: "Painel de som (arquivos e C1\u2013C10)",
@@ -44632,9 +44907,9 @@ var GameVehiclesThree = (() => {
44632
44907
  strengths: { light: "leve", medium: "m\xE9dio", heavy: "forte" },
44633
44908
  states: { file: "A", synth: "S", missing: "F" },
44634
44909
  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" },
44635
- 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" },
44910
+ 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", unlicensed: "1 \xB7 Arquivo sem licen\xE7a" },
44636
44911
  run: { idle: "parada", measuring: "medindo\u2026", done: "pronta", failed: "falhou", recording: "gravando\u2026", recorded: "grava\xE7\xE3o baixada" },
44637
- words: { none: "\u2014", hidden: "? (revele)", prefers: "prefere", approved: "aprovado pelo dono" },
44912
+ words: { none: "\u2014", hidden: "? (revele)", prefers: "prefere", approved: "aprovado pelo dono", local_file: "arquivo do pacote local", sdk_file: "grava\xE7\xE3o do SDK", synth: "s\xEDntese do SDK" },
44638
44913
  sources: { sdk: "SDK", user: "seu som", synth: "sintetizado" },
44639
44914
  probe: "Fonte do som",
44640
44915
  wall: "Parede (oclus\xE3o)",
@@ -44705,9 +44980,9 @@ var GameVehiclesThree = (() => {
44705
44980
  environments: { indoor: "indoor", outdoor: "outdoor" },
44706
44981
  strengths: { light: "light", medium: "medium", heavy: "heavy" },
44707
44982
  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" },
44708
- 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" },
44983
+ 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", unlicensed: "1 \xB7 File without licence" },
44709
44984
  run: { idle: "idle", measuring: "measuring\u2026", done: "done", failed: "failed", recording: "recording\u2026", recorded: "recording downloaded" },
44710
- words: { none: "\u2014", hidden: "? (reveal)", prefers: "prefers", approved: "approved by the owner" },
44985
+ words: { none: "\u2014", hidden: "? (reveal)", prefers: "prefers", approved: "approved by the owner", local_file: "local pack file", sdk_file: "SDK recording", synth: "SDK synthesis" },
44711
44986
  sources: { sdk: "SDK", user: "your sound", synth: "synthesized" },
44712
44987
  probe: "Sound source",
44713
44988
  wall: "Wall (occlusion)",
@@ -44861,9 +45136,77 @@ var GameVehiclesThree = (() => {
44861
45136
  duration: 0.5,
44862
45137
  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"]] }],
44863
45138
  falloff: { model: "none" }
45139
+ },
45140
+ {
45141
+ // the parking sensor: a short high beep, repeated by the cabin faster as the obstacle comes close
45142
+ id: "parking_beep",
45143
+ bus: "world",
45144
+ duration: 0.09,
45145
+ layers: [{ source: { kind: "osc", wave: "sine", frequency: { set: 2300 } }, envelope: [[0, 0.07, "set"], [0.08, 1e-4, "exp", "fixed"]] }],
45146
+ falloff: { model: "none" }
44864
45147
  }
44865
45148
  ];
44866
45149
  var CABIN_BANK = { id: "vehicle_cabin", sounds: CABIN_SOUNDS, loops: [] };
45150
+ var hz = (midi) => 440 * 2 ** ((midi - 69) / 12);
45151
+ var layer = (note, beat, wave, level) => {
45152
+ const [at, midi, beats] = note;
45153
+ const length = beats * beat;
45154
+ return {
45155
+ at: at * beat,
45156
+ duration: length,
45157
+ source: { kind: "osc", wave, frequency: { set: hz(midi) } },
45158
+ envelope: [[0, 1e-4, "set"], [0.01, level, "linear"], [Math.max(0.02, length * 0.9), level * 0.35, "linear"], [length, 1e-4, "exp", "fixed"]]
45159
+ };
45160
+ };
45161
+ function radioTrack(spec) {
45162
+ const beat = 60 / spec.bpm;
45163
+ const recipes = Array.from({ length: spec.bars }, (_, bar) => {
45164
+ const inBar = (notes) => notes.filter((n) => n[0] >= bar * 4 && n[0] < bar * 4 + 4).map((n) => [n[0] - bar * 4, n[1], n[2]]);
45165
+ const layers2 = [
45166
+ ...inBar(spec.bass).map((n) => layer(n, beat, "triangle", 0.16)),
45167
+ ...inBar(spec.chords).map((n) => layer(n, beat, "sine", 0.05)),
45168
+ ...inBar(spec.melody).map((n) => layer(n, beat, spec.wave, 0.07))
45169
+ ];
45170
+ return { id: `${spec.id}_${bar}`, bus: "world", duration: 4 * beat, layers: layers2, falloff: { model: "none" } };
45171
+ });
45172
+ return { song: { id: spec.id, barSeconds: 4 * beat, bars: recipes.map((r) => r.id) }, recipes };
45173
+ }
45174
+ var chord = (bar, root, minor = false) => [[bar * 4, root, 4], [bar * 4, root + (minor ? 3 : 4), 4], [bar * 4, root + 7, 4]];
45175
+ var bassLine = (roots, pattern) => roots.flatMap((root, bar) => pattern.map((b) => [bar * 4 + b, root - 12, 0.9]));
45176
+ var TRACKS = [
45177
+ // a bright pop loop: I–V–vi–IV in C
45178
+ radioTrack({
45179
+ id: "radio_track_pop",
45180
+ bpm: 112,
45181
+ bars: 4,
45182
+ wave: "square",
45183
+ bass: bassLine([48, 55, 57, 53], [0, 1.5, 2, 3]),
45184
+ chords: [...chord(0, 60), ...chord(1, 55), ...chord(2, 57, true), ...chord(3, 53)],
45185
+ melody: [[0, 76, 1], [1, 74, 0.5], [1.5, 72, 1.5], [4, 74, 1], [5, 76, 1], [6, 79, 2], [8, 76, 1], [9, 74, 1], [10, 72, 2], [12, 72, 1], [13, 74, 1], [14, 72, 2]]
45186
+ }),
45187
+ // a syncopated loop in A minor, the bass on the off-beats
45188
+ radioTrack({
45189
+ id: "radio_track_groove",
45190
+ bpm: 100,
45191
+ bars: 4,
45192
+ wave: "triangle",
45193
+ bass: bassLine([57, 62, 64, 57], [0, 0.75, 1.5, 2.5, 3.25]),
45194
+ chords: [...chord(0, 57, true), ...chord(1, 62, true), ...chord(2, 64), ...chord(3, 57, true)],
45195
+ melody: [[0.5, 72, 0.5], [1.25, 76, 0.75], [2.5, 74, 1], [4.5, 77, 0.5], [5.25, 74, 0.75], [6.5, 72, 1.5], [8.5, 71, 0.5], [9.25, 74, 0.75], [10.5, 76, 1.5], [12.5, 72, 0.5], [13.25, 69, 2]]
45196
+ }),
45197
+ // a slow blues-like loop in F with a swung melody
45198
+ radioTrack({
45199
+ id: "radio_track_slow",
45200
+ bpm: 76,
45201
+ bars: 4,
45202
+ wave: "sine",
45203
+ bass: bassLine([53, 58, 53, 60], [0, 2]),
45204
+ chords: [...chord(0, 65), ...chord(1, 58), ...chord(2, 65), ...chord(3, 60)],
45205
+ melody: [[0, 72, 1.5], [1.67, 75, 0.33], [2, 77, 2], [4, 74, 1], [5, 72, 1], [6, 70, 2], [8, 72, 1.5], [9.67, 69, 0.33], [10, 65, 2], [12, 67, 1], [13, 70, 1], [14, 72, 2]]
45206
+ })
45207
+ ];
45208
+ var RADIO_TRACKS = TRACKS.map((t) => t.song);
45209
+ var RADIO_BANK = { id: "vehicle_radio", sounds: TRACKS.flatMap((t) => t.recipes), loops: [] };
44867
45210
  var FOLEY_FALLOFF = {
44868
45211
  model: "inverse_square",
44869
45212
  k: 0.02,
@@ -45470,11 +45813,11 @@ var GameVehiclesThree = (() => {
45470
45813
  ...s.splash ? [sceneNoise({ type: "bandpass", f: s.splash.f * take.f, f2: s.splash.f2 * take.f, q: 1.4, d: s.splash.d * take.t * weight ** 0.4, vol: s.splash.vol * gait.vol * take.v * weight ** 0.5 })] : []
45471
45814
  ];
45472
45815
  }
45473
- var quieter = (level, layers2) => layers2.map((layer) => {
45474
- const gain = layer.gain ?? 1;
45475
- if (typeof gain === "number") return { ...layer, gain: gain * level };
45476
- if (Array.isArray(gain)) return { ...layer, gain: [gain[0] * level, gain[1] * level] };
45477
- return layer;
45816
+ var quieter = (level, layers2) => layers2.map((layer2) => {
45817
+ const gain = layer2.gain ?? 1;
45818
+ if (typeof gain === "number") return { ...layer2, gain: gain * level };
45819
+ if (Array.isArray(gain)) return { ...layer2, gain: [gain[0] * level, gain[1] * level] };
45820
+ return layer2;
45478
45821
  });
45479
45822
  var recipe = (id, layers2, falloff, bus = "world") => ({ id, bus, pan: true, layers: layers2, falloff, sources: ["sdk"] });
45480
45823
  var takeId = (id, index) => `${id}_${String(index + 1).padStart(2, "0")}`;
@@ -45490,7 +45833,7 @@ var GameVehiclesThree = (() => {
45490
45833
  (take) => quieter(LANDING_LEVEL, [
45491
45834
  // both feet, a hair apart, then the knees taking the weight (a low body thump) and the clothes settling
45492
45835
  ...footLayers(surface, take, GAITS.run, LANDINGS[landing2].weight),
45493
- ...footLayers(surface, { ...take, v: take.v * 0.6 }, GAITS.run, LANDINGS[landing2].weight * 0.6).map((layer) => ({ ...layer, at: (typeof layer.at === "number" ? layer.at : 0) + 0.018 * take.t })),
45836
+ ...footLayers(surface, { ...take, v: take.v * 0.6 }, GAITS.run, LANDINGS[landing2].weight * 0.6).map((layer2) => ({ ...layer2, at: (typeof layer2.at === "number" ? layer2.at : 0) + 0.018 * take.t })),
45494
45837
  sceneTone({ f: 60 * take.f, f2: 38, d: 0.14 * LANDINGS[landing2].weight * take.t, vol: 0.22 * LANDINGS[landing2].weight ** 0.7 * take.v, delay: 0.02 }),
45495
45838
  sceneNoise({ type: "bandpass", f: [1800, 2600], q: 0.8, d: 0.18, vol: LANDINGS[landing2].cloth * take.v, delay: 0.04 })
45496
45839
  ]),
@@ -45536,7 +45879,7 @@ var GameVehiclesThree = (() => {
45536
45879
  fall_damage: (take) => [
45537
45880
  ...BODY.body_fall(take),
45538
45881
  sceneNoise({ type: "highpass", f: 2800, d: 0.02, vol: 0.35 * take.v, delay: 0.01 }),
45539
- ...voice({ f0: 170, f1: 110, d: 0.32, fA: 650, fB: 1150, noise: 0.25, vol: 0.32 })(take).map((layer) => ({ ...layer, at: 0.08 }))
45882
+ ...voice({ f0: 170, f1: 110, d: 0.32, fA: 650, fB: 1150, noise: 0.25, vol: 0.32 })(take).map((layer2) => ({ ...layer2, at: 0.08 }))
45540
45883
  ],
45541
45884
  player_hurt: voice({ f0: 150, f1: 105, d: 0.26, fA: 620, fB: 1100, noise: 0.3, vol: 0.35 }),
45542
45885
  player_breath_run: (take) => [
@@ -45594,6 +45937,105 @@ var GameVehiclesThree = (() => {
45594
45937
  };
45595
45938
  var BODY_FOLEY_SET_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
45596
45939
 
45940
+ // src/audio/engine-mix.ts
45941
+ var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
45942
+ var firingHz = (profile, rpm) => rpm / 60 * (profile.cylinders / 2);
45943
+ var rpmShare = (profile, rpm) => clamp01((rpm - profile.idleRpm) / (profile.redlineRpm - profile.idleRpm));
45944
+ var layerWeight = (centre, width, n) => {
45945
+ const d = Math.abs(n - centre) / width;
45946
+ return d >= 1 ? 0 : Math.cos(d * Math.PI / 2);
45947
+ };
45948
+ function engineMix(profile, input) {
45949
+ const throttle = clamp01(input.throttle);
45950
+ const on = input.rpm > 0;
45951
+ const n = rpmShare(profile, input.rpm);
45952
+ const level = on ? profile.closedThrottle + (1 - profile.closedThrottle) * throttle : 0;
45953
+ const open = 0.5 * n + 0.5 * throttle;
45954
+ const hz2 = firingHz(profile, input.rpm);
45955
+ const tones = profile.layers.map((layer2) => ({
45956
+ sound: layer2.sound,
45957
+ volume: layer2.gain * level * layerWeight(layer2.centre, layer2.width, n),
45958
+ frequency: hz2 * layer2.harmonic,
45959
+ cutoff: layer2.cutoff[0] + (layer2.cutoff[1] - layer2.cutoff[0]) * open
45960
+ }));
45961
+ const load = profile.load;
45962
+ return [
45963
+ ...tones,
45964
+ { sound: load.sound, volume: on ? load.gain * (0.15 + 0.85 * throttle) * (0.35 + 0.65 * n) : 0, cutoff: load.cutoff[0] + (load.cutoff[1] - load.cutoff[0]) * open }
45965
+ ];
45966
+ }
45967
+ function createGearGuess(profile) {
45968
+ let gear = 0;
45969
+ return {
45970
+ rpm(speed, throttle) {
45971
+ const v = Math.abs(speed);
45972
+ const ratios = profile.rpmPerMps;
45973
+ if (v < 0.3) {
45974
+ gear = 0;
45975
+ return profile.idleRpm + (profile.redlineRpm - profile.idleRpm) * 0.12 * clamp01(throttle);
45976
+ }
45977
+ const at = (g) => v * ratios[g];
45978
+ while (gear < ratios.length - 1 && at(gear) > profile.redlineRpm * 0.85) gear++;
45979
+ while (gear > 0 && at(gear) < profile.redlineRpm * 0.4) gear--;
45980
+ return Math.max(profile.idleRpm, Math.min(profile.redlineRpm, at(gear)));
45981
+ },
45982
+ get gear() {
45983
+ return gear;
45984
+ }
45985
+ };
45986
+ }
45987
+
45988
+ // src/audio/profiles.ts
45989
+ var DEFAULT_VEHICLE_SOUND_KEYS = {
45990
+ start: "engine_start",
45991
+ stop: "engine_stop",
45992
+ fail: "engine_fail",
45993
+ crashLight: "crash_light",
45994
+ crashMedium: "crash_medium",
45995
+ crashHeavy: "crash_heavy",
45996
+ skidAsphalt: "tire_skid_asphalt",
45997
+ skidDirt: "tire_skid_dirt",
45998
+ roll: "tire_roll"
45999
+ };
46000
+ var CRASH_SPEEDS = { medium: 6, heavy: 14 };
46001
+ var layers = (id, cutoffs, gains) => [
46002
+ { sound: `engine_${id}_low`, centre: 0, width: 0.5, harmonic: 1, gain: gains[0], cutoff: cutoffs[0] },
46003
+ { sound: `engine_${id}_mid`, centre: 0.45, width: 0.5, harmonic: 1, gain: gains[1], cutoff: cutoffs[1] },
46004
+ { sound: `engine_${id}_high`, centre: 1, width: 0.6, harmonic: 2, gain: gains[2], cutoff: cutoffs[2] }
46005
+ ];
46006
+ var CAR_ENGINE = {
46007
+ id: "car",
46008
+ cylinders: 4,
46009
+ idleRpm: 800,
46010
+ redlineRpm: 6500,
46011
+ // a 5-speed hatch: 1st tops at ~13 m/s, 5th at ~50 m/s
46012
+ rpmPerMps: [500, 300, 205, 155, 125],
46013
+ layers: layers("car", [[220, 520], [420, 1300], [900, 2600]], [0.11, 0.075, 0.04]),
46014
+ load: { sound: "engine_car_load", gain: 0.05, cutoff: [350, 1600] },
46015
+ closedThrottle: 0.55
46016
+ };
46017
+ var TRUCK_ENGINE = {
46018
+ id: "truck",
46019
+ cylinders: 6,
46020
+ idleRpm: 650,
46021
+ redlineRpm: 2600,
46022
+ rpmPerMps: [420, 260, 170, 120, 90, 72],
46023
+ layers: layers("truck", [[160, 380], [300, 800], [600, 1500]], [0.13, 0.08, 0.035]),
46024
+ load: { sound: "engine_truck_load", gain: 0.06, cutoff: [250, 900] },
46025
+ closedThrottle: 0.6
46026
+ };
46027
+ var BIKE_ENGINE = {
46028
+ id: "bike",
46029
+ cylinders: 4,
46030
+ idleRpm: 1300,
46031
+ redlineRpm: 12e3,
46032
+ rpmPerMps: [900, 640, 500, 410, 350, 310],
46033
+ layers: layers("bike", [[350, 900], [700, 2200], [1500, 4200]], [0.08, 0.07, 0.05]),
46034
+ load: { sound: "engine_bike_load", gain: 0.045, cutoff: [600, 2600] },
46035
+ closedThrottle: 0.5
46036
+ };
46037
+ var ENGINE_PROFILES = { car: CAR_ENGINE, truck: TRUCK_ENGINE, bike: BIKE_ENGINE };
46038
+
45597
46039
  // src/audio/bank.ts
45598
46040
  var VEHICLE_LOOP_FALLOFF = { model: "inverse_square", k: 4e-3, cutoffK: 0.01 };
45599
46041
  var SDK = "sdk: vehicle sounds (sons/checklist-sons.md 5.6)";
@@ -45685,106 +46127,33 @@ var GameVehiclesThree = (() => {
45685
46127
  recipe2("car_trunk_close", [sceneTone({ f: 90, f2: 55, d: 0.15, vol: 0.65 }), sceneNoise({ type: "bandpass", f: 500, q: 1, d: 0.12, vol: 0.5 }), ...latch(0.01, 0.28), sceneNoise({ type: "bandpass", f: 1800, q: 3, d: 0.04, vol: 0.1, delay: 0.06 })], "trunk or hatch shut: a lighter thump, the latch and a small rattle"),
45686
46128
  recipe2("car_hood_close", [sceneTone({ f: 70, f2: 40, d: 0.22, vol: 0.8 }), sceneNoise({ type: "bandpass", f: 850, q: 1.4, d: 0.2, vol: 0.6, wet: 0.3 }), sceneTone({ f: 900, f2: 860, d: 0.18, vol: 0.05 }), ...latch(0.015, 0.3)], "hood slammed: a big sheet of metal ringing a little, the latch")
45687
46129
  ];
45688
- var VEHICLE_SOUND_BANK = { id: "vehicles_three", sounds: VEHICLE_SOUNDS, loops: VEHICLE_LOOPS };
45689
-
45690
- // src/audio/engine-mix.ts
45691
- var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
45692
- var firingHz = (profile, rpm) => rpm / 60 * (profile.cylinders / 2);
45693
- var rpmShare = (profile, rpm) => clamp01((rpm - profile.idleRpm) / (profile.redlineRpm - profile.idleRpm));
45694
- var layerWeight = (centre, width, n) => {
45695
- const d = Math.abs(n - centre) / width;
45696
- return d >= 1 ? 0 : Math.cos(d * Math.PI / 2);
46130
+ var CAR_RECORDED_RPM = { idle: 1e3, high: 2520 };
46131
+ var CAR_START_RUN_STOP = {
46132
+ /** The starter and the engine catching (at 0.58 s) and flaring; the loops take over at the crank time of the vehicle. */
46133
+ start: { offset: 0, duration: 1.3 },
46134
+ /** The ignition off: the engine running down to the end of the file. */
46135
+ stop: { offset: 9.96, duration: 0.64 }
45697
46136
  };
45698
- function engineMix(profile, input) {
45699
- const throttle = clamp01(input.throttle);
45700
- const on = input.rpm > 0;
45701
- const n = rpmShare(profile, input.rpm);
45702
- const level = on ? profile.closedThrottle + (1 - profile.closedThrottle) * throttle : 0;
45703
- const open = 0.5 * n + 0.5 * throttle;
45704
- const hz = firingHz(profile, input.rpm);
45705
- const tones = profile.layers.map((layer) => ({
45706
- sound: layer.sound,
45707
- volume: layer.gain * level * layerWeight(layer.centre, layer.width, n),
45708
- frequency: hz * layer.harmonic,
45709
- cutoff: layer.cutoff[0] + (layer.cutoff[1] - layer.cutoff[0]) * open
45710
- }));
45711
- const load = profile.load;
45712
- return [
45713
- ...tones,
45714
- { sound: load.sound, volume: on ? load.gain * (0.15 + 0.85 * throttle) * (0.35 + 0.65 * n) : 0, cutoff: load.cutoff[0] + (load.cutoff[1] - load.cutoff[0]) * open }
45715
- ];
45716
- }
45717
- function createGearGuess(profile) {
45718
- let gear = 0;
45719
- return {
45720
- rpm(speed, throttle) {
45721
- const v = Math.abs(speed);
45722
- const ratios = profile.rpmPerMps;
45723
- if (v < 0.3) {
45724
- gear = 0;
45725
- return profile.idleRpm + (profile.redlineRpm - profile.idleRpm) * 0.12 * clamp01(throttle);
45726
- }
45727
- const at = (g) => v * ratios[g];
45728
- while (gear < ratios.length - 1 && at(gear) > profile.redlineRpm * 0.85) gear++;
45729
- while (gear > 0 && at(gear) < profile.redlineRpm * 0.4) gear--;
45730
- return Math.max(profile.idleRpm, Math.min(profile.redlineRpm, at(gear)));
45731
- },
45732
- get gear() {
45733
- return gear;
45734
- }
45735
- };
45736
- }
45737
-
45738
- // src/audio/profiles.ts
45739
- var DEFAULT_VEHICLE_SOUND_KEYS = {
45740
- start: "engine_start",
45741
- stop: "engine_stop",
45742
- fail: "engine_fail",
45743
- crashLight: "crash_light",
45744
- crashMedium: "crash_medium",
45745
- crashHeavy: "crash_heavy",
45746
- skidAsphalt: "tire_skid_asphalt",
45747
- skidDirt: "tire_skid_dirt",
45748
- roll: "tire_roll"
45749
- };
45750
- var CRASH_SPEEDS = { medium: 6, heavy: 14 };
45751
- var layers = (id, cutoffs, gains) => [
45752
- { sound: `engine_${id}_low`, centre: 0, width: 0.5, harmonic: 1, gain: gains[0], cutoff: cutoffs[0] },
45753
- { sound: `engine_${id}_mid`, centre: 0.45, width: 0.5, harmonic: 1, gain: gains[1], cutoff: cutoffs[1] },
45754
- { sound: `engine_${id}_high`, centre: 1, width: 0.6, harmonic: 2, gain: gains[2], cutoff: cutoffs[2] }
46137
+ var carLayer = (id, file, rpm, harmonic, offset) => ({
46138
+ id,
46139
+ file,
46140
+ loop: true,
46141
+ // the layer's oscillator plays the firing frequency times its harmonic: the file sounds at that frequency for the RPM it was recorded at
46142
+ baseFrequency: firingHz(CAR_ENGINE, rpm) * harmonic,
46143
+ falloff: VEHICLE_LOOP_FALLOFF,
46144
+ ...offset === void 0 ? {} : { offset }
46145
+ });
46146
+ var VEHICLE_SAMPLE_ALIASES = [
46147
+ carLayer("engine_car_low", "engine_loop_car_idle", CAR_RECORDED_RPM.idle, 1),
46148
+ carLayer("engine_car_mid", "engine_loop_car_high", CAR_RECORDED_RPM.high, 1),
46149
+ // the same file as the mid layer, read from two seconds in, so the two do not sum in phase where they cross
46150
+ carLayer("engine_car_high", "engine_loop_car_high", CAR_RECORDED_RPM.high, 2, 2),
46151
+ { id: "engine_start", file: "engine_start_run_stop", ...CAR_START_RUN_STOP.start, falloff: VEHICLE_LOOP_FALLOFF },
46152
+ { id: "engine_stop", file: "engine_start_run_stop", ...CAR_START_RUN_STOP.stop, falloff: VEHICLE_LOOP_FALLOFF },
46153
+ { id: "car_door_close", file: "car_door_shut" },
46154
+ { id: "car_trunk_close", file: "car_trunk_shut" }
45755
46155
  ];
45756
- var CAR_ENGINE = {
45757
- id: "car",
45758
- cylinders: 4,
45759
- idleRpm: 800,
45760
- redlineRpm: 6500,
45761
- // a 5-speed hatch: 1st tops at ~13 m/s, 5th at ~50 m/s
45762
- rpmPerMps: [500, 300, 205, 155, 125],
45763
- layers: layers("car", [[220, 520], [420, 1300], [900, 2600]], [0.11, 0.075, 0.04]),
45764
- load: { sound: "engine_car_load", gain: 0.05, cutoff: [350, 1600] },
45765
- closedThrottle: 0.55
45766
- };
45767
- var TRUCK_ENGINE = {
45768
- id: "truck",
45769
- cylinders: 6,
45770
- idleRpm: 650,
45771
- redlineRpm: 2600,
45772
- rpmPerMps: [420, 260, 170, 120, 90, 72],
45773
- layers: layers("truck", [[160, 380], [300, 800], [600, 1500]], [0.13, 0.08, 0.035]),
45774
- load: { sound: "engine_truck_load", gain: 0.06, cutoff: [250, 900] },
45775
- closedThrottle: 0.6
45776
- };
45777
- var BIKE_ENGINE = {
45778
- id: "bike",
45779
- cylinders: 4,
45780
- idleRpm: 1300,
45781
- redlineRpm: 12e3,
45782
- rpmPerMps: [900, 640, 500, 410, 350, 310],
45783
- layers: layers("bike", [[350, 900], [700, 2200], [1500, 4200]], [0.08, 0.07, 0.05]),
45784
- load: { sound: "engine_bike_load", gain: 0.045, cutoff: [600, 2600] },
45785
- closedThrottle: 0.5
45786
- };
45787
- var ENGINE_PROFILES = { car: CAR_ENGINE, truck: TRUCK_ENGINE, bike: BIKE_ENGINE };
46156
+ var VEHICLE_SOUND_BANK = { id: "vehicles_three", sounds: VEHICLE_SOUNDS, loops: VEHICLE_LOOPS, sampleAliases: VEHICLE_SAMPLE_ALIASES };
45788
46157
 
45789
46158
  // src/audio/vehicle-sound.ts
45790
46159
  var clamp012 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
@@ -45842,8 +46211,8 @@ var GameVehiclesThree = (() => {
45842
46211
  const driven = frame.rpm ?? gears.rpm(frame.speed, throttle);
45843
46212
  const rpm = state.running ? Math.max(driven, profile.idleRpm * (1 + 1.2 * state.flare * state.flare)) : 0;
45844
46213
  state.rpm = rpm;
45845
- for (const layer of engineMix(profile, { rpm, throttle: Math.max(throttle, state.flare * 0.6) })) {
45846
- setLoop(layer.sound, layer.volume, frame.position, { ...layer.frequency === void 0 ? {} : { frequency: layer.frequency }, cutoff: layer.cutoff });
46214
+ for (const layer2 of engineMix(profile, { rpm, throttle: Math.max(throttle, state.flare * 0.6) })) {
46215
+ setLoop(layer2.sound, layer2.volume, frame.position, { ...layer2.frequency === void 0 ? {} : { frequency: layer2.frequency }, cutoff: layer2.cutoff });
45847
46216
  }
45848
46217
  const speed = Math.abs(frame.speed);
45849
46218
  const slide = clamp012(((frame.slip ?? 0) - 0.15) / 0.5) * clamp012(speed / 4);
@@ -46245,7 +46614,15 @@ var GameVehiclesThree = (() => {
46245
46614
  var clamp22 = (x, a, b) => Math.max(a, Math.min(b, x));
46246
46615
  var MAX_SUBSTEP = 1 / 240;
46247
46616
  var FLAT_BELOW = 0.2;
46617
+ var RIM_RADIUS_SHARE = 0.7;
46618
+ var RIM_FRICTION = 0.4;
46619
+ var RIM_ROLLING = 0.25;
46620
+ var FLAT_SAG = 0.05;
46621
+ var FLAT_ROLLING = 6;
46248
46622
  var STALL_HEALTH = 0.3;
46623
+ var CLUTCH_BITE = 0.7;
46624
+ var STALL_RPM = 0.5;
46625
+ var STALL_GAS = 0.35;
46249
46626
  var defaultSemiSpec = {
46250
46627
  mass: 1300,
46251
46628
  centreHeight: 0.5,
@@ -46386,7 +46763,14 @@ var GameVehiclesThree = (() => {
46386
46763
  const accelInput = reversing ? input.brake : input.throttle;
46387
46764
  const brakeInput = clamp22(reversing ? input.throttle : input.brake, 0, 1);
46388
46765
  const ratio = ratioOf(state.gear) * spec.finalDrive;
46389
- state.rpm = clamp22(wheelRpm * ratio, spec.engine.idleRpm, spec.engine.redlineRpm + 200);
46766
+ const engaged = input.clutch == null || state.gear === 0 ? 1 : 1 - clamp22(input.clutch, 0, 1);
46767
+ const coupled = wheelRpm * ratio;
46768
+ const free = spec.engine.idleRpm + clamp22(accelInput, 0, 1) * (spec.engine.redlineRpm * 0.7 - spec.engine.idleRpm);
46769
+ state.rpm = clamp22(coupled * engaged + Math.max(coupled, free) * (1 - engaged), spec.engine.idleRpm, spec.engine.redlineRpm + 200);
46770
+ if (input.clutch != null && state.gear !== 0 && state.engineOn && engaged > CLUTCH_BITE && coupled < spec.engine.idleRpm * STALL_RPM && accelInput < STALL_GAS) {
46771
+ state.engineOn = false;
46772
+ events.push({ type: "stall" });
46773
+ }
46390
46774
  if (!state.engineOn || state.fuel <= 0) state.rpm = 0;
46391
46775
  let throttle = clamp22(accelInput, 0, 1);
46392
46776
  if (engineHealth < STALL_HEALTH && throttle > 0.8) {
@@ -46397,12 +46781,13 @@ var GameVehiclesThree = (() => {
46397
46781
  }
46398
46782
  throttle *= 0.5;
46399
46783
  } else stallWatch.throttleTime = 0;
46400
- if (!state.engineOn && accelInput > 0 && Math.abs(state.u) < 0.5 && engineHealth >= STALL_HEALTH && state.fuel > 0) state.engineOn = true;
46784
+ const starting = input.start ?? (accelInput > 0 && Math.abs(state.u) < 0.5);
46785
+ if (!state.engineOn && starting && engineHealth >= STALL_HEALTH && state.fuel > 0) state.engineOn = true;
46401
46786
  if (state.fuel <= 0 && state.engineOn) {
46402
46787
  state.engineOn = false;
46403
46788
  events.push({ type: "out_of_fuel" });
46404
46789
  }
46405
- const engineTorque = state.engineOn ? torqueAt(spec, state.rpm) * throttle * (0.55 + 0.45 * engineHealth) : 0;
46790
+ const engineTorque = state.engineOn ? torqueAt(spec, state.rpm) * throttle * (0.55 + 0.45 * engineHealth) * clamp22(input.powerLimit ?? 1, 0, 1) * engaged : 0;
46406
46791
  const driveForceTotal = state.gear === 0 ? 0 : (reversing ? -1 : 1) * engineTorque * ratio * spec.efficiency / spec.wheelRadius;
46407
46792
  if (state.engineOn) {
46408
46793
  const burned = fuelRate(fuelModel, state.rpm / spec.engine.redlineRpm, throttle, cargo / baseMass, engineHealth) * dt;
@@ -46420,19 +46805,21 @@ var GameVehiclesThree = (() => {
46420
46805
  const wz = state.z - w.x * sn + w.z * cs;
46421
46806
  const ground = world.ground ? world.ground(wx, wz) : {};
46422
46807
  const surface = surfaces.get(contamination && contamination.left > 0 && surfaces.get(contamination.id).grip < surfaces.get(ground.surface ?? "dry_asphalt").grip ? contamination.id : ground.surface ?? "dry_asphalt");
46808
+ const tyreHealth = cond.tyres ? clamp22(cond.tyres[i] ?? 1, 0, 1) : 1;
46809
+ const rim = !!cond.rims?.[i];
46810
+ const flat = rim || tyreHealth < FLAT_BELOW;
46811
+ const radius = rim ? spec.rimRadius ?? spec.wheelRadius * RIM_RADIUS_SHARE : flat ? spec.wheelRadius - FLAT_SAG : spec.wheelRadius;
46423
46812
  const top = state.height + w.x * state.pitch - w.z * state.roll;
46424
- const length = top - (ground.height ?? 0) - spec.wheelRadius;
46813
+ const length = top - (ground.height ?? 0) - radius;
46425
46814
  const compression = ride - length;
46426
46815
  const rate = -(state.heightRate + w.x * state.pitchRate - w.z * state.rollRate);
46427
46816
  const k = spec.suspension.spring * (0.5 + 0.5 * suspensionHealth);
46428
46817
  const c = spec.suspension.damper * (0.5 + 0.5 * suspensionHealth);
46429
46818
  const load = Math.max(0, k * compression + c * rate);
46430
- const tyreHealth = cond.tyres ? clamp22(cond.tyres[i] ?? 1, 0, 1) : 1;
46431
- const flat = tyreHealth < FLAT_BELOW;
46432
46819
  const bog = !!surface.bogs && !spec.fourWheelDrive;
46433
46820
  if (bog) bogging = true;
46434
- const grip = surfaceGrip(surface, state.u) * (flat ? 0.5 : 1);
46435
- wheelState.push({ n: load, grip, surface: surface.id, bog, flat });
46821
+ const grip = surfaceGrip(surface, state.u) * (rim ? RIM_FRICTION / spec.tyre.friction : flat ? 0.5 : 1);
46822
+ wheelState.push({ n: load, grip, surface: surface.id, bog, flat, rim });
46436
46823
  sumN += load;
46437
46824
  sumNA += load * w.x;
46438
46825
  sumNB += load * w.z;
@@ -46468,7 +46855,7 @@ var GameVehiclesThree = (() => {
46468
46855
  if (locked || lockedByBrake) fl += moving ? -Math.sign(vl) * capacity * (lockedByBrake ? SLIDING_GRIP : 1) : 0;
46469
46856
  else if (brakeShare > 0 && moving) fl += -Math.sign(vl) * brakeShare;
46470
46857
  if (brakeShare > 0 && moving) brakeWork += Math.min(brakeShare, capacity) * Math.abs(vl) * dt;
46471
- const rolling = ws.bog && accelInput > 0 ? spec.bogResistance : spec.rolling * (ws.flat ? 6 : 1);
46858
+ const rolling = ws.bog && accelInput > 0 ? spec.bogResistance : ws.rim ? RIM_ROLLING : spec.rolling * (ws.flat ? FLAT_ROLLING : 1);
46472
46859
  const alpha = Math.atan2(vc, Math.abs(vl) + 0.5);
46473
46860
  let fc = -Math.tanh(spec.tyre.corneringStiffness * alpha) * capacity;
46474
46861
  if (locked) fc *= 0.6;
@@ -46543,6 +46930,21 @@ var GameVehiclesThree = (() => {
46543
46930
  const brakeForce = base.brakeForce ?? body.mass * 9.81 * (carRatio + (0.6 - carRatio) * heavyShare);
46544
46931
  return { ...spec, mass: body.mass, wheels, fourWheelDrive: driveAll, engine, gears, drag, brakeForce, tyre: { ...spec.tyre, friction: 0.8 + 0.03 * stats2.handling } };
46545
46932
  }
46933
+ var DRIVE_PROFILES = {
46934
+ eco: { throttle: 0.7, upshift: 0.55, downshift: 0.25 },
46935
+ normal: { throttle: 1, upshift: 0.85, downshift: 0.35 },
46936
+ sport: { throttle: 1, upshift: 0.97, downshift: 0.55 }
46937
+ };
46938
+ var DEFAULT_TRACTION = { slip: 0.97, floor: 0.35 };
46939
+ function tractionLimit(slips, options = DEFAULT_TRACTION) {
46940
+ const worst = Math.max(0, ...slips);
46941
+ if (worst <= options.slip) return 1;
46942
+ return Math.max(options.floor, 1 - (worst - options.slip) / (1 - options.slip));
46943
+ }
46944
+ var DEFAULT_CRUISE = { gain: 0.35, minSpeed: 8.3 };
46945
+ function cruiseThrottle(target, speed, options = DEFAULT_CRUISE) {
46946
+ return Math.max(0, Math.min(1, (target - speed) * options.gain + 0.12));
46947
+ }
46546
46948
  var DEFAULT_CONTROL_KEYS = {
46547
46949
  throttle: ["KeyW", "ArrowUp"],
46548
46950
  brake: ["KeyS", "ArrowDown"],
@@ -46550,6 +46952,9 @@ var GameVehiclesThree = (() => {
46550
46952
  right: ["KeyD", "ArrowRight"],
46551
46953
  handbrake: ["Space"],
46552
46954
  horn: ["KeyH"],
46955
+ clutch: ["KeyC"],
46956
+ flash: ["KeyX"],
46957
+ washer: ["KeyZ"],
46553
46958
  ignition: ["KeyI"],
46554
46959
  gearbox: ["KeyM"],
46555
46960
  gear_up: ["KeyE"],
@@ -46563,13 +46968,21 @@ var GameVehiclesThree = (() => {
46563
46968
  alarm: ["KeyY"],
46564
46969
  radio: ["KeyR"],
46565
46970
  radio_source: ["KeyT"],
46971
+ radio_track: ["BracketRight"],
46566
46972
  exit: ["KeyF"],
46567
46973
  indicator_left: ["Comma"],
46568
46974
  indicator_right: ["Period"],
46569
46975
  hazard: ["KeyB"],
46570
46976
  wipers: ["KeyO"],
46571
- seatbelt: ["KeyJ"]
46977
+ seatbelt: ["KeyJ"],
46978
+ drive_profile: ["Digit7"],
46979
+ traction_control: ["Digit8"],
46980
+ abs: ["Digit9"],
46981
+ cruise: ["Digit0"],
46982
+ climate: ["Minus"],
46983
+ demist: ["Equal"]
46572
46984
  };
46985
+ var HELD_KEYS = ["throttle", "brake", "left", "right", "handbrake", "horn", "clutch", "flash", "washer"];
46573
46986
  function createVehicleControls(options) {
46574
46987
  const keys = { ...DEFAULT_CONTROL_KEYS, ...options.keys };
46575
46988
  const auto4 = options.autoFourWheel ?? { slip: 0.18, releaseSeconds: 2 };
@@ -46577,7 +46990,7 @@ var GameVehiclesThree = (() => {
46577
46990
  const held = /* @__PURE__ */ new Set();
46578
46991
  const any = (codes) => codes.some((c) => held.has(c));
46579
46992
  const commandOf = /* @__PURE__ */ new Map();
46580
- 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);
46993
+ for (const [command2, codes] of Object.entries(keys)) if (!HELD_KEYS.includes(command2)) for (const code of codes) commandOf.set(code, command2);
46581
46994
  const driving = new Set(Object.values(keys).flat());
46582
46995
  const state = {
46583
46996
  ignition: !!options.running,
@@ -46586,18 +46999,29 @@ var GameVehiclesThree = (() => {
46586
46999
  parkingBrake: false,
46587
47000
  driveMode: options.fourWheelDrive ? "auto" : "4x2",
46588
47001
  fourWheelEngaged: false,
46589
- lamps: { mode: "off", fog: false, interior: false },
47002
+ lamps: { mode: "off", fog: false, interior: false, flash: false },
46590
47003
  brakeLights: false,
46591
47004
  reverseLights: false,
46592
47005
  horn: false,
46593
47006
  alarm: "off",
46594
- radio: { on: false, source: "radio" },
47007
+ radio: { on: false, source: "radio", track: 0 },
46595
47008
  indicator: "off",
46596
47009
  blinkOn: false,
46597
47010
  tick: false,
46598
47011
  wipers: "off",
46599
- seatbelt: false
46600
- };
47012
+ washer: false,
47013
+ seatbelt: false,
47014
+ clutch: 0,
47015
+ profile: "normal",
47016
+ tractionControl: true,
47017
+ abs: true,
47018
+ cruise: { on: false, speed: 0 },
47019
+ climate: { on: false, demist: false },
47020
+ cranking: false
47021
+ };
47022
+ let crankAsked = false;
47023
+ let lastSpeed = 0;
47024
+ let engineRunning = !!options.running;
46601
47025
  const blinkHz = options.blinkHz ?? 1.5;
46602
47026
  const cancelAt = options.indicatorCancel ?? 0.15;
46603
47027
  let blink = 0;
@@ -46609,15 +47033,19 @@ var GameVehiclesThree = (() => {
46609
47033
  const emit = (event) => options.onEvent?.(event);
46610
47034
  const LAMPS = ["off", "position", "low"];
46611
47035
  const MODES = ["4x2", "4x4", "auto"];
47036
+ const WIPERS = ["off", "intermittent", "slow", "fast"];
47037
+ const PROFILES = ["eco", "normal", "sport"];
46612
47038
  const command = (c) => {
46613
47039
  switch (c) {
46614
47040
  case "ignition":
46615
47041
  state.ignition = !state.ignition;
46616
47042
  if (state.ignition && state.alarm !== "off") state.alarm = "off";
47043
+ if (state.ignition) crankAsked = true;
47044
+ else state.cruise.on = false;
46617
47045
  break;
46618
47046
  case "gearbox":
46619
47047
  state.gearbox = state.gearbox === "automatic" ? "manual" : "automatic";
46620
- state.gear = Math.max(1, state.gear);
47048
+ state.gear = options.clutchPedal && state.gearbox === "manual" && Math.abs(lastSpeed) < 0.5 ? 0 : Math.max(1, state.gear);
46621
47049
  break;
46622
47050
  case "gear_up":
46623
47051
  case "gear_down":
@@ -46656,6 +47084,9 @@ var GameVehiclesThree = (() => {
46656
47084
  case "radio_source":
46657
47085
  state.radio.source = state.radio.source === "radio" ? "tape" : "radio";
46658
47086
  break;
47087
+ case "radio_track":
47088
+ state.radio.track = (state.radio.track + 1) % Math.max(1, options.radioTracks ?? 3);
47089
+ break;
46659
47090
  case "exit":
46660
47091
  break;
46661
47092
  case "indicator_left":
@@ -46672,7 +47103,30 @@ var GameVehiclesThree = (() => {
46672
47103
  blink = 0;
46673
47104
  break;
46674
47105
  case "wipers":
46675
- state.wipers = state.wipers === "off" ? "slow" : state.wipers === "slow" ? "fast" : "off";
47106
+ state.wipers = WIPERS[(WIPERS.indexOf(state.wipers) + 1) % WIPERS.length];
47107
+ break;
47108
+ case "drive_profile":
47109
+ state.profile = PROFILES[(PROFILES.indexOf(state.profile) + 1) % PROFILES.length];
47110
+ break;
47111
+ case "traction_control":
47112
+ state.tractionControl = !state.tractionControl;
47113
+ break;
47114
+ case "abs":
47115
+ state.abs = !state.abs;
47116
+ break;
47117
+ case "cruise":
47118
+ if (state.cruise.on) state.cruise.on = false;
47119
+ else if (!engineRunning) return emit({ type: "refused", command: c, reason: "ignition_off" });
47120
+ else if (lastSpeed < DEFAULT_CRUISE.minSpeed) return emit({ type: "refused", command: c, reason: "too_slow" });
47121
+ else state.cruise = { on: true, speed: lastSpeed };
47122
+ break;
47123
+ case "climate":
47124
+ state.climate.on = !state.climate.on;
47125
+ if (!state.climate.on) state.climate.demist = false;
47126
+ break;
47127
+ case "demist":
47128
+ state.climate.demist = !state.climate.demist;
47129
+ if (state.climate.demist) state.climate.on = true;
46676
47130
  break;
46677
47131
  case "seatbelt":
46678
47132
  state.seatbelt = !state.seatbelt;
@@ -46716,16 +47170,48 @@ var GameVehiclesThree = (() => {
46716
47170
  }
46717
47171
  } else state.blinkOn = false;
46718
47172
  state.horn = any(keys.horn);
46719
- const gasNow = state.ignition ? throttle : 0;
47173
+ state.lamps.flash = any(keys.flash);
47174
+ state.washer = any(keys.washer);
47175
+ state.clutch = ease(state.clutch, any(keys.clutch) ? 1 : 0, any(keys.clutch) ? 8 : 2.5, dt);
47176
+ const speed = driver?.state.u ?? 0;
47177
+ lastSpeed = speed;
47178
+ if (state.cruise.on && (brake > 0.05 || state.clutch > 0.5 || handbrake)) {
47179
+ state.cruise.on = false;
47180
+ emit({ type: "cruise_cancelled" });
47181
+ }
47182
+ const tune = DRIVE_PROFILES[state.profile];
47183
+ let gasNow = state.ignition ? throttle * tune.throttle : 0;
47184
+ if (state.cruise.on && state.ignition) gasNow = Math.max(gasNow, cruiseThrottle(state.cruise.speed, speed));
47185
+ const gasStart = state.gearbox === "automatic" && state.ignition && !engineRunning && throttle > 0 && Math.abs(speed) < 0.5;
47186
+ state.cranking = false;
47187
+ if (crankAsked || gasStart) {
47188
+ crankAsked = false;
47189
+ if (options.canCrank && !options.canCrank()) emit({ type: "refused", command: "ignition", reason: "battery_flat" });
47190
+ else {
47191
+ state.cranking = true;
47192
+ options.onCrank?.();
47193
+ }
47194
+ }
47195
+ const driven = (driver?.state.slip ?? []).filter((_, i) => driver?.spec.wheels?.[i]?.drive ?? true);
47196
+ const powerLimit = (state.tractionControl ? tractionLimit(driven) : 1) * (options.powerLimit?.() ?? 1);
46720
47197
  const reversing = state.gearbox === "manual" ? state.gear < 0 : (driver?.state.gear ?? 1) < 0;
46721
47198
  state.reverseLights = reversing;
46722
- state.brakeLights = handbrake || (reversing ? gasNow > 0.05 && (driver?.state.u ?? 0) < -0.3 : brake > 0.05);
46723
- if (state.gearbox === "automatic") return { throttle: gasNow, brake, steer, handbrake, horn: state.horn };
46724
- 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 };
47199
+ state.brakeLights = handbrake || (reversing ? gasNow > 0.05 && speed < -0.3 : brake > 0.05);
47200
+ const common = { steer, handbrake, horn: state.horn, start: state.cranking, powerLimit };
47201
+ if (state.gearbox === "automatic") return { ...common, throttle: gasNow, brake };
47202
+ const pedals = state.gear < 0 ? { throttle: brake, brake: gasNow } : { throttle: gasNow, brake };
47203
+ return { ...common, ...pedals, gear: state.gear, ...options.clutchPedal ? { clutch: state.clutch } : {} };
46725
47204
  },
46726
47205
  after(driver, dt) {
46727
- if (driver.state.engineOn && !state.ignition && driver.state.u !== 0) state.ignition = true;
46728
47206
  if (!state.ignition && driver.state.engineOn) driver.state.engineOn = false;
47207
+ engineRunning = driver.state.engineOn;
47208
+ if (!engineRunning) state.cruise.on = false;
47209
+ const spec = driver.spec;
47210
+ spec.abs = state.abs;
47211
+ if (spec.upshiftFraction != null) {
47212
+ spec.upshiftFraction = DRIVE_PROFILES[state.profile].upshift;
47213
+ spec.downshiftFraction = DRIVE_PROFILES[state.profile].downshift;
47214
+ }
46729
47215
  if (state.driveMode === "auto" && options.fourWheelDrive) {
46730
47216
  const slip = Math.max(0, ...driver.state.slip);
46731
47217
  if (slip > auto4.slip) {
@@ -46739,6 +47225,10 @@ var GameVehiclesThree = (() => {
46739
47225
  held.clear();
46740
47226
  throttle = brake = steer = 0;
46741
47227
  state.horn = false;
47228
+ state.lamps.flash = false;
47229
+ state.washer = false;
47230
+ state.clutch = 0;
47231
+ state.cruise.on = false;
46742
47232
  }
46743
47233
  };
46744
47234
  }
@@ -46753,15 +47243,126 @@ var GameVehiclesThree = (() => {
46753
47243
  best = tyre;
46754
47244
  }
46755
47245
  }
46756
- return best?.health ?? 1;
47246
+ return best;
46757
47247
  });
46758
47248
  return {
46759
- tyres,
47249
+ tyres: tyres.map((tyre) => tyre?.health ?? 1),
47250
+ rims: tyres.map((tyre) => !!tyre?.rim),
46760
47251
  ...report.brakes != null ? { brakes: report.brakes } : {},
46761
47252
  ...report.engine != null ? { engine: report.engine } : {},
46762
47253
  ...report.suspension != null ? { suspension: report.suspension } : {}
46763
47254
  };
46764
47255
  }
47256
+ var DEFAULT_LOADS = { position: 20, low: 110, high: 130, fog: 110, interior: 10, radio: 25, wipers: 60, indicators: 42, horn: 60, alarm: 5, brakeLights: 42, climate: 180 };
47257
+ var clamp3 = (x, a, b) => Math.max(a, Math.min(b, x));
47258
+ function createVehicleSystems(options = {}) {
47259
+ const loads = { ...DEFAULT_LOADS, ...options.loads };
47260
+ const capacity = options.batteryWh ?? 720;
47261
+ const alternator = options.alternatorW ?? { idle: 350, full: 1100 };
47262
+ const crankCharge = options.crankCharge ?? 0.25;
47263
+ const startWh = options.startWh ?? 0.6;
47264
+ const ambient = options.ambientC ?? 25;
47265
+ const thermostat = options.thermostatC ?? 90;
47266
+ const limp = options.limpC ?? 115;
47267
+ const seize = options.seizeC ?? 130;
47268
+ const heat = options.heatPerSecond ?? 0.5;
47269
+ const cooling = options.cooling ?? { stopped: 4e-3, fast: 0.03 };
47270
+ const state = { charge: clamp3(options.charge ?? 1, 0, 1), coolantC: ambient, temperature: 0, overheating: false, seized: false, spares: options.spares ?? 1, netW: 0 };
47271
+ const draw = (c) => {
47272
+ const lamps = c.lamps;
47273
+ let w = lamps.mode === "off" ? 0 : loads.position;
47274
+ if (lamps.mode === "low") w += loads.low;
47275
+ if (lamps.mode === "high") w += loads.low + loads.high;
47276
+ if (lamps.flash) w += loads.high;
47277
+ if (lamps.fog) w += loads.fog;
47278
+ if (lamps.interior) w += loads.interior;
47279
+ if (c.radio.on) w += loads.radio;
47280
+ if (c.wipers !== "off" || c.washer) w += loads.wipers;
47281
+ if (c.indicator !== "off") w += loads.indicators * (c.indicator === "hazard" ? 2 : 1) * 0.5;
47282
+ if (c.horn) w += loads.horn;
47283
+ if (c.alarm !== "off") w += loads.alarm;
47284
+ if (c.brakeLights) w += loads.brakeLights;
47285
+ if (c.climate.on) w += loads.climate;
47286
+ return w;
47287
+ };
47288
+ return {
47289
+ state,
47290
+ canCrank: () => state.charge >= crankCharge && !state.seized,
47291
+ crank() {
47292
+ state.charge = clamp3(state.charge - startWh / capacity, 0, 1);
47293
+ },
47294
+ powerLimit: () => state.coolantC <= limp ? 1 : clamp3(1 - (state.coolantC - limp) / (seize - limp), 0.25, 1),
47295
+ step({ driver, controls, throttle, dt, radiator = 1, coolantC }) {
47296
+ const events = [];
47297
+ const running = driver.state.engineOn;
47298
+ const charging = running ? alternator.idle + (alternator.full - alternator.idle) * clamp3((driver.state.rpm - 900) / 1100, 0, 1) : 0;
47299
+ const net = charging - draw(controls);
47300
+ state.netW = net;
47301
+ const before = state.charge;
47302
+ state.charge = clamp3(state.charge + net * dt / 3600 / capacity, 0, 1);
47303
+ if (before >= crankCharge && state.charge < crankCharge && !running) events.push({ type: "battery_flat" });
47304
+ const work = running ? clamp3(0.15 + 0.85 * throttle * (driver.state.rpm / 6500), 0, 1) : 0;
47305
+ const air = cooling.stopped + (cooling.fast - cooling.stopped) * clamp3(Math.abs(driver.state.u) / 27.8, 0, 1);
47306
+ const cool = air * clamp3(radiator, 0.05, 1) * (running ? 1 + (controls.climate.on ? 0.15 : 0) : 1);
47307
+ const opened = state.coolantC >= thermostat ? 1 : 0.15;
47308
+ state.coolantC += (heat * work - cool * opened * (state.coolantC - ambient)) * dt;
47309
+ state.coolantC = coolantC ?? Math.max(ambient, state.coolantC);
47310
+ state.temperature = clamp3((state.coolantC - ambient) / (seize - ambient), 0, 1);
47311
+ const hot = state.coolantC > limp;
47312
+ if (hot && !state.overheating) events.push({ type: "overheating" });
47313
+ state.overheating = hot;
47314
+ if (state.coolantC >= seize && !state.seized) {
47315
+ state.seized = true;
47316
+ events.push({ type: "seized" });
47317
+ }
47318
+ if (state.seized) driver.state.engineOn = false;
47319
+ return events;
47320
+ },
47321
+ fitSpare(driver, wheel) {
47322
+ if (state.spares <= 0) return { type: "no_spare" };
47323
+ state.spares--;
47324
+ const tyres = [...driver.condition.tyres ?? []];
47325
+ tyres[wheel] = 1;
47326
+ driver.condition = { ...driver.condition, tyres };
47327
+ return { type: "spare_fitted", wheel };
47328
+ },
47329
+ charge(share) {
47330
+ state.charge = clamp3(state.charge + share, 0, 1);
47331
+ }
47332
+ };
47333
+ }
47334
+ var DEFAULT_EJECTION = { frontal: 8.3, side: 6, keep: 0.8, lift: 2.5 };
47335
+ function ejectionOf(pulse, belted, options = DEFAULT_EJECTION) {
47336
+ if (belted) return null;
47337
+ const du = pulse.after.u - pulse.before.u;
47338
+ const dw = pulse.after.w - pulse.before.w;
47339
+ const deltaV = Math.hypot(du, dw);
47340
+ const frontal = Math.abs(du) >= Math.abs(dw);
47341
+ if (frontal && Math.abs(du) < options.frontal && !pulse.rolled) return null;
47342
+ if (!frontal && Math.abs(dw) < options.side && !pulse.rolled) return null;
47343
+ const exit = frontal ? du < 0 ? "windshield" : "rear_window" : dw < 0 ? "right_window" : "left_window";
47344
+ return { exit, velocity: [pulse.before.u * options.keep, pulse.before.w * options.keep, options.lift], deltaV };
47345
+ }
47346
+ var DEFAULT_PARKING_SENSOR = { range: 1.5, solid: 0.3, slowest: 0.8, fastest: 0.12 };
47347
+ function parkingBeepInterval(distance, options = DEFAULT_PARKING_SENSOR) {
47348
+ if (distance > options.range) return null;
47349
+ if (distance <= options.solid) return 0;
47350
+ const t = (distance - options.solid) / (options.range - options.solid);
47351
+ return options.fastest + (options.slowest - options.fastest) * t;
47352
+ }
47353
+ function bumperDistance(params) {
47354
+ const [hx, hz2] = params.heading;
47355
+ const sign2 = params.forward ? 1 : -1;
47356
+ let best = Infinity;
47357
+ for (const [ox, oz] of params.obstacles) {
47358
+ const dx = ox - params.x;
47359
+ const dz = oz - params.z;
47360
+ const along = (dx * hx + dz * hz2) * sign2 - params.length / 2;
47361
+ const side2 = Math.abs(-dx * hz2 + dz * hx);
47362
+ if (along >= 0 && side2 <= params.width / 2 + 0.2) best = Math.min(best, along);
47363
+ }
47364
+ return best;
47365
+ }
46765
47366
 
46766
47367
  // src/vehicle-lod.ts
46767
47368
  var VEHICLE_LOD_DISTANCE = 14;
@@ -47402,6 +48003,7 @@ var GameVehiclesThree = (() => {
47402
48003
  var itemId = (key) => ITEM_OF[key] ?? key;
47403
48004
  var LANE = { z: 11, from: -12, wall: 13 };
47404
48005
  var SPARK_EVERY_S = 0.08;
48006
+ var TYRE_DAMAGE = /* @__PURE__ */ new Set(["flat_tyre", "torn_tyre", "run_flat"]);
47405
48007
  var LEAF_REST_RADIANS = 0.02;
47406
48008
  var MATERIAL_COLOURS = { metal: 13421772, glass: 4500223, rubber: 16750848, fuel: 16724736 };
47407
48009
  var LANE_START_S = 1.2;
@@ -47697,7 +48299,8 @@ var GameVehiclesThree = (() => {
47697
48299
  state.time += dt;
47698
48300
  const braking = settings.damage !== "into_wall" && gap() < brakeAt() && state.time > 0.5;
47699
48301
  const moving = state.time > LANE_START_S;
47700
- const input = { ...idle2, throttle: braking || !moving ? 0 : 1, brake: braking ? 1 : 0, steer: settings.damage === "into_wall" || !moving ? 0 : Math.sin(state.time * 1.4) * 0.15 };
48302
+ const straight = settings.damage === "into_wall" || TYRE_DAMAGE.has(settings.damage) || !moving;
48303
+ const input = { ...idle2, throttle: braking || !moving ? 0 : 1, brake: braking ? 1 : 0, steer: straight ? 0 : Math.sin(state.time * 1.4) * 0.15 };
47701
48304
  if (mode === "arcade") {
47702
48305
  const { events } = arcade.step(input, dt, { blocked: (x, z, yaw) => vehicleCorners(arcade.spec, x, z, yaw).some(([px]) => px >= LANE.wall) });
47703
48306
  for (const e of events) {
@@ -48348,6 +48951,127 @@ var GameVehiclesThree = (() => {
48348
48951
  return { ...certificate, results, status: statusOf(results) };
48349
48952
  }
48350
48953
 
48954
+ // src/body.ts
48955
+ var OPEN_SOUND = "car_door_open";
48956
+ var SHUT_SOUND = { door: "car_door_close", hood: "car_hood_close", lid: "car_trunk_close" };
48957
+ var clamp4 = (x, a, b) => Math.max(a, Math.min(b, x));
48958
+ function createVehicleBody(built2, options) {
48959
+ const swing = options.swingSeconds ?? 1.2;
48960
+ const windowSeconds = options.windowSeconds ?? 3.5;
48961
+ const travel = options.windowTravel ?? 0.92;
48962
+ const root = built2.context?.vehicle ?? built2.group;
48963
+ const poses = /* @__PURE__ */ new Map();
48964
+ for (const clip of options.clips) {
48965
+ if (!clip.id.startsWith("open_")) continue;
48966
+ const last = clip.keys[clip.keys.length - 1];
48967
+ for (const [id, pose] of Object.entries(last?.parts ?? {})) if (pose.rotation) poses.set(id, pose.rotation);
48968
+ }
48969
+ const hinges = /* @__PURE__ */ new Map();
48970
+ for (const [id, open] of poses) {
48971
+ const pivot = root.getObjectByName(id);
48972
+ if (pivot) hinges.set(id, { pivot, open, now: 0, target: 0 });
48973
+ }
48974
+ const kind = (part) => part.startsWith("door") ? "door" : part === "hood" ? "hood" : "lid";
48975
+ const panes = /* @__PURE__ */ new Map();
48976
+ for (const glass of built2.context?.glass ?? []) {
48977
+ const g = glass.userData.glass;
48978
+ if (!g || g.key !== "window_front" && g.key !== "window_rear") continue;
48979
+ const id = `${g.key === "window_front" ? "front" : "rear"}_${(g.side ?? 1) < 0 ? "left" : "right"}`;
48980
+ if (panes.has(id) || !glass.geometry) continue;
48981
+ glass.geometry.computeBoundingBox();
48982
+ const box2 = glass.geometry.boundingBox;
48983
+ if (box2) panes.set(id, { pane: glass, drop: (box2.max.y - box2.min.y) * travel, now: 0, target: 0, applied: 0 });
48984
+ }
48985
+ const sunroofs = [];
48986
+ const wipers = [];
48987
+ const T = options.THREE;
48988
+ const turn = new T.Quaternion();
48989
+ root.traverse((o) => {
48990
+ const sun = o.userData.sunroof;
48991
+ if (sun) sunroofs.push({ obj: o, slide: sun.slide, base: o.position.x });
48992
+ });
48993
+ for (const { part, tag } of built2.context?.wipers ?? [])
48994
+ wipers.push({ obj: part, arm: tag.arm, sweep: tag.sweep, spindle: new T.Vector3(...tag.spindle), axis: new T.Vector3(...tag.axis), rest: part.position.clone(), restQ: part.quaternion.clone() });
48995
+ let sunNow = 0;
48996
+ let sunTarget = 0;
48997
+ let wipePhase = 0;
48998
+ let locked = false;
48999
+ const placeWiper = (w, angle) => {
49000
+ if (angle === 0) {
49001
+ w.obj.position.copy(w.rest);
49002
+ w.obj.quaternion.copy(w.restQ);
49003
+ return;
49004
+ }
49005
+ turn.setFromAxisAngle(w.axis, angle);
49006
+ w.obj.position.copy(w.rest).sub(w.spindle).applyQuaternion(turn).add(w.spindle);
49007
+ w.obj.quaternion.copy(turn).multiply(w.restQ);
49008
+ };
49009
+ const ease = (now, target, seconds, dt) => now + clamp4(target - now, -dt / seconds, dt / seconds);
49010
+ return {
49011
+ parts: [...hinges.keys()],
49012
+ windows: [...panes.keys()],
49013
+ hasSunroof: sunroofs.length > 0,
49014
+ wipers: new Set(wipers.map((w) => w.arm)).size,
49015
+ get locked() {
49016
+ return locked;
49017
+ },
49018
+ openness: (part) => hinges.get(part)?.now ?? 0,
49019
+ toggle(part) {
49020
+ const h = hinges.get(part);
49021
+ if (!h) return [{ type: "refused", part, reason: "missing" }];
49022
+ if (locked && kind(part) !== "hood" && h.target === 0) return [{ type: "refused", part, reason: "locked" }];
49023
+ h.target = h.target > 0 ? 0 : 1;
49024
+ return h.target > 0 ? [{ type: "sound", key: OPEN_SOUND, part }] : [];
49025
+ },
49026
+ window: (id) => panes.get(id)?.now ?? 0,
49027
+ toggleWindow(id) {
49028
+ const w = panes.get(id);
49029
+ if (w) w.target = w.target > 0.5 ? 0 : 1;
49030
+ },
49031
+ setWindow(id, open) {
49032
+ const w = panes.get(id);
49033
+ if (w) w.target = clamp4(open, 0, 1);
49034
+ },
49035
+ toggleSunroof() {
49036
+ sunTarget = sunTarget > 0.5 ? 0 : 1;
49037
+ },
49038
+ lock(on) {
49039
+ locked = on;
49040
+ return [{ type: "sound", key: "blinker_tick", part: "doors" }];
49041
+ },
49042
+ update(dt, wipe = 0) {
49043
+ const events = [];
49044
+ for (const [id, h] of hinges) {
49045
+ if (h.now === h.target) continue;
49046
+ h.now = ease(h.now, h.target, swing, dt);
49047
+ h.pivot.rotation.set(h.open[0] * h.now, h.open[1] * h.now, h.open[2] * h.now);
49048
+ if (h.now === 0) events.push({ type: "sound", key: SHUT_SOUND[kind(id)] ?? "car_door_close", part: id });
49049
+ }
49050
+ for (const w of panes.values()) {
49051
+ if (w.now === w.target) continue;
49052
+ w.now = ease(w.now, w.target, windowSeconds, dt);
49053
+ const offset = -w.drop * w.now;
49054
+ w.pane.position.y += offset - w.applied;
49055
+ w.applied = offset;
49056
+ }
49057
+ if (sunNow !== sunTarget) {
49058
+ sunNow = ease(sunNow, sunTarget, 2.5, dt);
49059
+ for (const s of sunroofs) s.obj.position.x = s.base - s.slide * sunNow;
49060
+ }
49061
+ wipePhase = wipe > 0 ? (wipePhase + dt * wipe) % 1 : wipePhase > 0 ? Math.min(1, wipePhase + dt * 1.5) % 1 : 0;
49062
+ const sweep = Math.sin(Math.PI * wipePhase);
49063
+ for (const w of wipers) placeWiper(w, w.sweep * sweep);
49064
+ return events;
49065
+ },
49066
+ dispose() {
49067
+ for (const h of hinges.values()) h.pivot.rotation.set(0, 0, 0);
49068
+ for (const w of panes.values()) w.pane.position.y -= w.applied;
49069
+ for (const s of sunroofs) s.obj.position.x = s.base;
49070
+ for (const w of wipers) placeWiper(w, 0);
49071
+ }
49072
+ };
49073
+ }
49074
+
48351
49075
  // src/dashboard.ts
48352
49076
  var DASHBOARD_LOOK = {
48353
49077
  face: "#0b1110",
@@ -48522,6 +49246,8 @@ var GameVehiclesThree = (() => {
48522
49246
  lamp(g, 104, 214, r.highBeam, DASHBOARD_LOOK.blue, beam);
48523
49247
  lamp(g, 152, 214, r.lowBeam || r.fog, r.fog ? DASHBOARD_LOOK.amber : DASHBOARD_LOOK.green, beam);
48524
49248
  bar(g, 40, r.fuel, false, "F");
49249
+ lamp(g, 104, 244, !!r.cruise, DASHBOARD_LOOK.green, letter("CRZ"));
49250
+ if (r.profile && r.profile !== "normal") lamp(g, 152, 244, true, r.profile === "eco" ? DASHBOARD_LOOK.green : DASHBOARD_LOOK.amber, letter(r.profile === "eco" ? "E" : "S"));
48525
49251
  } else {
48526
49252
  dial(g, r.rpm / 1e3, Math.ceil(redline / 1e3) + 1, 1, redline / 1e3, (v) => String(Math.round(v)));
48527
49253
  g.fillStyle = DASHBOARD_LOOK.marks;
@@ -48530,7 +49256,9 @@ var GameVehiclesThree = (() => {
48530
49256
  g.fillText(r.gear < 0 ? "R" : r.gear === 0 ? "N" : String(r.gear), SIZE / 2, SIZE / 2 + 54);
48531
49257
  lamp(g, 86, 178, r.parkingBrake, DASHBOARD_LOOK.red, ring2("P"));
48532
49258
  lamp(g, 128, 214, r.ignition && !r.seatbelt, DASHBOARD_LOOK.red, belt);
48533
- lamp(g, 170, 178, warn, DASHBOARD_LOOK.red, letter("+\u2212"));
49259
+ lamp(g, 170, 178, warn || r.engineOn && r.charging === false, DASHBOARD_LOOK.red, letter("+\u2212"));
49260
+ lamp(g, 104, 244, r.ignition && !!r.tractionOff, DASHBOARD_LOOK.amber, letter("TC"));
49261
+ lamp(g, 152, 244, r.ignition && !!r.absOff, DASHBOARD_LOOK.amber, letter("ABS"));
48534
49262
  lamp(g, 86, 214, warn, DASHBOARD_LOOK.red, letter("OIL"));
48535
49263
  lamp(g, 170, 214, r.fourWheel, DASHBOARD_LOOK.green, letter("4x4"));
48536
49264
  bar(g, SIZE - 40, r.temperature, true, "T");
@@ -48718,7 +49446,18 @@ var GameVehiclesThree = (() => {
48718
49446
  }
48719
49447
 
48720
49448
  // src/test-area/proving/cockpit.ts
48721
- 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" };
49449
+ var DEFAULT_COCKPIT_SOUNDS = { horn: "horn", blinker: "blinker_tick", starter: "starter_crank", alarm: "car_alarm", chirp: "alarm_chirp", wiper: "wiper_swish", chime: "seatbelt_chime", radio: "radio_static", beep: "parking_beep" };
49450
+ var BODY_KEYS = {
49451
+ Digit1: "door_front_left",
49452
+ Digit2: "door_front_right",
49453
+ Digit3: "door_rear_left",
49454
+ Digit4: "door_rear_right",
49455
+ Digit5: "hood",
49456
+ Digit6: "lid"
49457
+ };
49458
+ var WINDOW_KEYS = { Digit1: "front_left", Digit2: "front_right", Digit3: "rear_left", Digit4: "rear_right" };
49459
+ var LOCK_KEY = "KeyV";
49460
+ var WIPE_RATE = { off: 0, intermittent: 0.25, slow: 0.7, fast: 1.4 };
48722
49461
  function createCockpit(o) {
48723
49462
  const sounds = { ...DEFAULT_COCKPIT_SOUNDS, ...o.sounds };
48724
49463
  const spec = subjectSpec(o.subject);
@@ -48735,19 +49474,38 @@ var GameVehiclesThree = (() => {
48735
49474
  let wipeAgain = 0;
48736
49475
  let chimeAgain = 0;
48737
49476
  let radioAgain = 0;
49477
+ let beepAgain = 0;
49478
+ let bar = 0;
48738
49479
  let exitAsked = false;
48739
49480
  let gas = 0;
49481
+ const systems = createVehicleSystems();
49482
+ const context = o.built.context;
49483
+ const destructible = o.kit && context ? createVehicleDestructible(o.kit, context, { id: "driven", frame: o.built.group }) : null;
49484
+ const body = destructible ? createVehicleBody(o.built, { THREE: o.THREE, clips: destructible.animations() }) : null;
49485
+ const bodyEvents = (events) => {
49486
+ for (const event of events) {
49487
+ if (event.type === "sound") play(event.key, 0.9);
49488
+ o.onEvent?.(event);
49489
+ }
49490
+ };
48740
49491
  const controls = createVehicleControls({
49492
+ canCrank: systems.canCrank,
49493
+ onCrank: systems.crank,
49494
+ powerLimit: systems.powerLimit,
48741
49495
  gears: spec.gears?.length ?? 5,
48742
49496
  fourWheelDrive: !!o.subject.body.fourWheelDrive,
48743
49497
  fogLamps: lamps.has.fog,
49498
+ clutchPedal: true,
49499
+ radioTracks: RADIO_TRACKS.length,
48744
49500
  onEvent: (event) => {
48745
49501
  if (event.type === "command") {
48746
- if (event.command === "ignition" && event.state.ignition && sounds.starter) play(sounds.starter);
48747
49502
  if (event.command === "ignition" && !event.state.ignition) driver.state.engineOn = false;
48748
49503
  if (event.command === "alarm" && event.state.alarm === "armed") play(sounds.chirp);
48749
49504
  if (event.command === "alarm") alarmAgain = 0;
48750
- if (event.command === "radio" || event.command === "radio_source") radioAgain = 0;
49505
+ if (event.command === "radio" || event.command === "radio_source" || event.command === "radio_track") {
49506
+ radioAgain = 0;
49507
+ bar = 0;
49508
+ }
48751
49509
  if (event.command === "exit") exitAsked = true;
48752
49510
  }
48753
49511
  o.onEvent?.(event);
@@ -48755,7 +49513,20 @@ var GameVehiclesThree = (() => {
48755
49513
  });
48756
49514
  driver.state.engineOn = false;
48757
49515
  const target = typeof window !== "undefined" ? window : null;
49516
+ const bodyKey = (e) => {
49517
+ if (!body) return false;
49518
+ if (e.code === LOCK_KEY) bodyEvents(body.lock(!body.locked));
49519
+ else if (e.shiftKey && e.code === "Digit5") body.toggleSunroof();
49520
+ else if (e.shiftKey && WINDOW_KEYS[e.code]) body.toggleWindow(WINDOW_KEYS[e.code]);
49521
+ else if (!e.shiftKey && BODY_KEYS[e.code]) bodyEvents(body.toggle(BODY_KEYS[e.code]));
49522
+ else return false;
49523
+ return true;
49524
+ };
48758
49525
  const down = (e) => {
49526
+ if (!e.repeat && bodyKey(e)) {
49527
+ e.stopImmediatePropagation();
49528
+ return;
49529
+ }
48759
49530
  if (!controls.key(e.code, true)) return;
48760
49531
  e.stopImmediatePropagation();
48761
49532
  if (e.code.startsWith("Arrow") || e.code === "Space") e.preventDefault();
@@ -48767,9 +49538,13 @@ var GameVehiclesThree = (() => {
48767
49538
  target?.addEventListener("keydown", down, true);
48768
49539
  target?.addEventListener("keyup", up, true);
48769
49540
  target?.addEventListener("blur", blur);
48770
- return {
49541
+ const cockpit = {
49542
+ radiator: 1,
48771
49543
  driver,
48772
49544
  controls,
49545
+ systems,
49546
+ body,
49547
+ destructible,
48773
49548
  lamps,
48774
49549
  dashboard,
48775
49550
  get throttle() {
@@ -48778,11 +49553,16 @@ var GameVehiclesThree = (() => {
48778
49553
  step(dt) {
48779
49554
  const input = controls.input(dt, driver);
48780
49555
  gas = input.throttle;
48781
- driver.step(input, dt);
49556
+ if (input.start && sounds.starter) play(sounds.starter);
49557
+ const engine = driver.step(input, dt).events;
48782
49558
  controls.after(driver, dt);
48783
49559
  const s = controls.state;
49560
+ const report = [...engine.filter((e) => e.type !== "shift"), ...systems.step({ driver, controls: s, throttle: input.throttle, dt, radiator: cockpit.radiator })];
49561
+ for (const event of report) o.onEvent?.(event);
49562
+ const powered = driver.state.engineOn || systems.state.charge > 0;
49563
+ const high = s.lamps.mode === "high" || s.lamps.flash;
48784
49564
  lamps.set({
48785
- mode: s.lamps.mode,
49565
+ mode: !powered ? "off" : s.lamps.flash ? "high" : s.lamps.mode,
48786
49566
  fog: s.lamps.fog,
48787
49567
  interior: s.lamps.interior,
48788
49568
  brake: s.brakeLights,
@@ -48799,7 +49579,14 @@ var GameVehiclesThree = (() => {
48799
49579
  engineOn: driver.state.engineOn,
48800
49580
  left: s.blinkOn && (s.indicator === "left" || s.indicator === "hazard"),
48801
49581
  right: s.blinkOn && (s.indicator === "right" || s.indicator === "hazard"),
48802
- highBeam: s.lamps.mode === "high",
49582
+ highBeam: high,
49583
+ fuel: driver.state.fuel / spec.tank.capacity,
49584
+ temperature: systems.state.temperature,
49585
+ cruise: s.cruise.on,
49586
+ profile: s.profile,
49587
+ tractionOff: !s.tractionControl,
49588
+ absOff: !s.abs,
49589
+ charging: systems.state.netW >= 0,
48803
49590
  lowBeam: s.lamps.mode === "low" || s.lamps.mode === "position",
48804
49591
  fog: s.lamps.fog,
48805
49592
  parkingBrake: s.parkingBrake,
@@ -48808,7 +49595,19 @@ var GameVehiclesThree = (() => {
48808
49595
  },
48809
49596
  dt
48810
49597
  );
49598
+ if (body) bodyEvents(body.update(dt, s.washer ? 1.2 : WIPE_RATE[s.wipers] ?? 0));
48811
49599
  if (s.tick) play(sounds.blinker, 0.8);
49600
+ const near = o.obstacles?.() ?? [];
49601
+ if (s.ignition && near.length && Math.abs(driver.state.u) < 3) {
49602
+ const g = o.built.group;
49603
+ const reversing = driver.state.gear < 0 || driver.state.u < -0.05;
49604
+ const distance = bumperDistance({ x: g.position.x, z: g.position.z, heading: [Math.sin(g.rotation.y), Math.cos(g.rotation.y)], length: o.built.length, width: o.built.width, forward: !reversing, obstacles: near });
49605
+ const every = parkingBeepInterval(distance);
49606
+ if (every != null && (beepAgain -= dt) <= 0) {
49607
+ play(sounds.beep, 0.7);
49608
+ beepAgain = every === 0 ? 0.09 : every;
49609
+ }
49610
+ }
48812
49611
  if (s.horn && (hornAgain -= dt) <= 0) {
48813
49612
  play(sounds.horn);
48814
49613
  hornAgain = 0.5;
@@ -48817,9 +49616,9 @@ var GameVehiclesThree = (() => {
48817
49616
  play(sounds.alarm);
48818
49617
  alarmAgain = 2.9;
48819
49618
  }
48820
- if (s.wipers !== "off" && (wipeAgain -= dt) <= 0) {
49619
+ if ((s.wipers !== "off" || s.washer) && (wipeAgain -= dt) <= 0) {
48821
49620
  play(sounds.wiper, 0.7);
48822
- wipeAgain = s.wipers === "fast" ? 0.6 : 1.4;
49621
+ wipeAgain = s.washer || s.wipers === "fast" ? 0.6 : s.wipers === "slow" ? 1.4 : 4;
48823
49622
  }
48824
49623
  const kmh2 = Math.abs(driver.state.u) * 3.6;
48825
49624
  if (!s.seatbelt && s.ignition && kmh2 > 10 && (chimeAgain -= dt) <= 0) {
@@ -48827,8 +49626,13 @@ var GameVehiclesThree = (() => {
48827
49626
  chimeAgain = 6;
48828
49627
  }
48829
49628
  if (s.radio.on && (radioAgain -= dt) <= 0) {
48830
- play(sounds.radio, s.radio.source === "tape" ? 0.25 : 0.45);
48831
- radioAgain = 0.55;
49629
+ const song = RADIO_TRACKS[s.radio.track % RADIO_TRACKS.length];
49630
+ if (song) {
49631
+ play(song.bars[bar % song.bars.length], s.radio.source === "tape" ? 0.8 : 0.7);
49632
+ bar++;
49633
+ }
49634
+ if (s.radio.source === "radio") play(sounds.radio, 0.12);
49635
+ radioAgain = song?.barSeconds ?? 2;
48832
49636
  }
48833
49637
  if (exitAsked) {
48834
49638
  exitAsked = false;
@@ -48842,10 +49646,149 @@ var GameVehiclesThree = (() => {
48842
49646
  controls.release();
48843
49647
  lamps.dispose();
48844
49648
  dashboard?.dispose();
49649
+ body?.dispose();
49650
+ destructible?.dispose?.();
49651
+ }
49652
+ };
49653
+ return cockpit;
49654
+ }
49655
+
49656
+ // src/wheel-roll.ts
49657
+ var DEFAULT_RADIUS = 0.32;
49658
+ function createWheelRoll(THREE, built2) {
49659
+ const wheels = (built2.context?.wheels ?? []).map((wheel) => ({
49660
+ wheel,
49661
+ rest: wheel.quaternion.clone(),
49662
+ radius: Number(wheel.userData.r) || DEFAULT_RADIUS,
49663
+ // in the kit's frame the front is +x: the wheels ahead of the middle steer
49664
+ front: wheel.position.x > 0,
49665
+ angle: 0
49666
+ }));
49667
+ const axle = new THREE.Vector3(0, 0, 1);
49668
+ const up = new THREE.Vector3(0, 1, 0);
49669
+ const spin = new THREE.Quaternion();
49670
+ const steering = new THREE.Quaternion();
49671
+ return {
49672
+ count: wheels.length,
49673
+ roll(distance, steer) {
49674
+ for (const w of wheels) {
49675
+ w.angle = (w.angle - distance / w.radius) % (Math.PI * 2);
49676
+ spin.setFromAxisAngle(axle, w.angle);
49677
+ steering.setFromAxisAngle(up, w.front ? steer : 0);
49678
+ w.wheel.quaternion.copy(w.rest).premultiply(spin).premultiply(steering);
49679
+ }
49680
+ },
49681
+ dispose() {
49682
+ for (const w of wheels) w.wheel.quaternion.copy(w.rest);
48845
49683
  }
48846
49684
  };
48847
49685
  }
48848
49686
 
49687
+ // src/test-area/proving/crash.ts
49688
+ var GLASS_BLOW = 6e3;
49689
+ function createProvingCrash(host, occupant) {
49690
+ const thrown = [];
49691
+ const breakGlass = (destructible, exit, forward) => {
49692
+ if (!destructible) return;
49693
+ const want = exit === "windshield" ? "windshield" : exit === "rear_window" ? "rear" : exit.startsWith("left") ? "left" : "right";
49694
+ const pane = destructible.describe().parts.find((p) => p.id.includes(exit === "windshield" || exit === "rear_window" ? want : "window") && (exit === "windshield" || exit === "rear_window" || p.id.includes(want)));
49695
+ if (!pane) return;
49696
+ destructible.hit({ type: "blunt", partId: pane.id, point: destructible.partWorldCenter(pane.id), direction: forward, energy: GLASS_BLOW, radius: 0.35, source: "occupant" });
49697
+ };
49698
+ return {
49699
+ crash({ car, u, w, belted, destructible }) {
49700
+ const ejection = ejectionOf({ before: { u, w }, after: { u: 0, w: 0 } }, belted);
49701
+ if (!ejection) return null;
49702
+ const s = Math.sin(car.rotationY);
49703
+ const c = Math.cos(car.rotationY);
49704
+ const forward = [s, 0, c];
49705
+ const right = [-c, 0, s];
49706
+ breakGlass(destructible, ejection.exit, forward);
49707
+ const [vf, vr, vu] = ejection.velocity;
49708
+ const start2 = [car.position.x + forward[0] * 1.2, 1.1, car.position.z + forward[2] * 1.2];
49709
+ const placed = occupant ? host.addObject?.(occupant, { position: start2, rotationY: car.rotationY }) : null;
49710
+ if (placed) {
49711
+ launchBody(placed.root, [forward[0] * vf + right[0] * vr, vu, forward[2] * vf + right[2] * vr]);
49712
+ thrown.push(placed.remove);
49713
+ }
49714
+ return ejection;
49715
+ },
49716
+ dispose() {
49717
+ for (const remove of thrown.splice(0)) remove();
49718
+ }
49719
+ };
49720
+ }
49721
+
49722
+ // src/test-area/proving/systems-text.ts
49723
+ var SYSTEMS_TEXT = {
49724
+ "pt-BR": {
49725
+ keys: "C embreagem (c\xE2mbio manual) \xB7 X lampejo do farol alto \xB7 Z esguicho do limpador \xB7 7 modo econ\xF4mico/normal/esporte \xB7 8 controle de tra\xE7\xE3o \xB7 9 ABS \xB7 0 piloto autom\xE1tico \xB7 - ar-condicionado \xB7 = desemba\xE7ador \xB7 ] troca a faixa do r\xE1dio ou da fita",
49726
+ profile: "Modo de condu\xE7\xE3o",
49727
+ profiles: { eco: "econ\xF4mico", normal: "normal", sport: "esporte" },
49728
+ traction: "Controle de tra\xE7\xE3o",
49729
+ abs: "ABS",
49730
+ cruise: "Piloto autom\xE1tico",
49731
+ cruiseSet: "Piloto autom\xE1tico: segurando {0} km/h",
49732
+ climate: "Ar-condicionado",
49733
+ demist: "Desemba\xE7ador",
49734
+ wiperModes: { off: "desligado", intermittent: "intermitente", slow: "lento", fast: "r\xE1pido" },
49735
+ refused: { battery_flat: "Bateria descarregada: o motor de partida n\xE3o gira", too_slow: "Piloto autom\xE1tico s\xF3 acima de 30 km/h", locked: "Porta travada", missing: "Este ve\xEDculo n\xE3o tem essa pe\xE7a" },
49736
+ events: {
49737
+ stall: "O motor morreu",
49738
+ out_of_fuel: "Acabou o combust\xEDvel",
49739
+ bogged: "Atolou",
49740
+ aquaplaning: "Aquaplanagem",
49741
+ battery_flat: "A bateria descarregou",
49742
+ overheating: "Motor superaquecendo: perdendo pot\xEAncia",
49743
+ seized: "O motor fundiu de tanto esquentar",
49744
+ spare_fitted: "Estepe colocado",
49745
+ no_spare: "N\xE3o h\xE1 estepe",
49746
+ cruise_cancelled: "Piloto autom\xE1tico desligado"
49747
+ },
49748
+ actions: { puncture: "Furar pneu (dianteiro esquerdo)", fit_spare: "Trocar pelo estepe", refuel: "Abastecer", drain_battery: "Descarregar bateria", jump_start: "Chupeta (carregar bateria)", break_radiator: "Furar o radiador", empty_tank: "Deixar o tanque na reserva", remote_lock: "Controle: travar (arma o alarme)", remote_unlock: "Controle: destravar", force_door: "For\xE7ar a porta do motorista", crash_wall: "Bater no muro (parada seca)", park_behind: "Estacionar um carro atr\xE1s (sensor de r\xE9)" },
49749
+ parking: { placed: "Carro parado 3 m atr\xE1s: d\xEA r\xE9 e escute o sensor", none: "Sem cat\xE1logo de ve\xEDculos nesta sala: o sensor n\xE3o tem o que ver" },
49750
+ track: "faixa {0}",
49751
+ adjust: { seat_forward: "Banco: frente/tr\xE1s (m)", seat_height: "Banco: altura (m)", mirror_left: "Retrovisor esquerdo: \xE2ngulo", mirror_right: "Retrovisor direito: \xE2ngulo" },
49752
+ crash: { mild: "Batida a {0} km/h: fraca demais para lan\xE7ar quem est\xE1 sem cinto", stayed: "Batida a {0} km/h: o cinto segurou o motorista", ejected: "Batida a {0} km/h sem cinto: o motorista saiu pelo {1}", exits: { windshield: "para-brisa", left_window: "vidro esquerdo", right_window: "vidro direito", rear_window: "vidro traseiro" } },
49753
+ bodyKeys: "1 a 4 portas \xB7 5 cap\xF4 \xB7 6 porta-malas \xB7 Shift+1 a 4 vidros \xB7 Shift+5 teto solar \xB7 V trava as portas",
49754
+ remote: { locked: "Travado pelo controle: alarme armado", unlocked: "Destravado pelo controle", forced: "Porta for\xE7ada" },
49755
+ done: { puncture: "Pneu dianteiro esquerdo furado", refuel: "Tanque cheio", drain_battery: "Bateria quase sem carga", jump_start: "Bateria carregada", break_radiator: "Radiador furado: o motor vai esquentar", empty_tank: "Tanque na reserva" }
49756
+ },
49757
+ en: {
49758
+ keys: "C clutch (manual gearbox) \xB7 X flash the high beam \xB7 Z washer \xB7 7 eco/normal/sport \xB7 8 traction control \xB7 9 ABS \xB7 0 cruise control \xB7 - air conditioning \xB7 = demister \xB7 ] next radio or tape track",
49759
+ profile: "Drive profile",
49760
+ profiles: { eco: "eco", normal: "normal", sport: "sport" },
49761
+ traction: "Traction control",
49762
+ abs: "ABS",
49763
+ cruise: "Cruise control",
49764
+ cruiseSet: "Cruise control: holding {0} km/h",
49765
+ climate: "Air conditioning",
49766
+ demist: "Demister",
49767
+ wiperModes: { off: "off", intermittent: "intermittent", slow: "slow", fast: "fast" },
49768
+ refused: { battery_flat: "Flat battery: the starter does not turn", too_slow: "Cruise control only above 30 km/h", locked: "The door is locked", missing: "This vehicle has no such part" },
49769
+ events: {
49770
+ stall: "The engine stalled",
49771
+ out_of_fuel: "Out of fuel",
49772
+ bogged: "Bogged down",
49773
+ aquaplaning: "Aquaplaning",
49774
+ battery_flat: "The battery went flat",
49775
+ overheating: "Engine overheating: losing power",
49776
+ seized: "The engine seized from the heat",
49777
+ spare_fitted: "Spare tyre fitted",
49778
+ no_spare: "No spare tyre left",
49779
+ cruise_cancelled: "Cruise control off"
49780
+ },
49781
+ actions: { puncture: "Puncture a tyre (front left)", fit_spare: "Fit the spare", refuel: "Refuel", drain_battery: "Drain the battery", jump_start: "Jump start (charge the battery)", break_radiator: "Puncture the radiator", empty_tank: "Leave the tank on reserve", remote_lock: "Remote: lock (arms the alarm)", remote_unlock: "Remote: unlock", force_door: "Force the driver's door", crash_wall: "Hit a wall (dead stop)", park_behind: "Park a car behind (rear sensor)" },
49782
+ parking: { placed: "A car parked 3 m behind: reverse and listen to the sensor", none: "No vehicle catalogue in this room: the sensor has nothing to see" },
49783
+ track: "track {0}",
49784
+ adjust: { seat_forward: "Seat: forward/back (m)", seat_height: "Seat: height (m)", mirror_left: "Left mirror: angle", mirror_right: "Right mirror: angle" },
49785
+ crash: { mild: "Crash at {0} km/h: too mild to throw an unbelted occupant out", stayed: "Crash at {0} km/h: the belt held the driver", ejected: "Crash at {0} km/h without a belt: the driver went out through the {1}", exits: { windshield: "windshield", left_window: "left window", right_window: "right window", rear_window: "rear window" } },
49786
+ bodyKeys: "1 to 4 doors \xB7 5 hood \xB7 6 trunk \xB7 Shift+1 to 4 windows \xB7 Shift+5 sunroof \xB7 V locks the doors",
49787
+ remote: { locked: "Locked by the remote: alarm armed", unlocked: "Unlocked by the remote", forced: "Door forced" },
49788
+ done: { puncture: "Front left tyre punctured", refuel: "Tank full", drain_battery: "Battery almost flat", jump_start: "Battery charged", break_radiator: "Radiator punctured: the engine will heat up", empty_tank: "Tank on reserve" }
49789
+ }
49790
+ };
49791
+
48849
49792
  // src/test-area/proving/text.ts
48850
49793
  var PROVING_TEXT = {
48851
49794
  "pt-BR": {
@@ -48990,9 +49933,11 @@ var GameVehiclesThree = (() => {
48990
49933
  right: { x: 0.79, y: 0.72, width: 0.19, height: 0.13, rounded: 0.3 },
48991
49934
  reverse: { x: 0.36, y: 0.13, width: 0.28, height: 0.18, rounded: 0.06 }
48992
49935
  };
49936
+ var ADJUST_KEYS = { seat_forward: "seatForward", seat_height: "seatHeight", mirror_left: "mirrorLeft", mirror_right: "mirrorRight" };
48993
49937
  function createProvingModule(options) {
48994
49938
  const locale = options.locale ?? "pt-BR";
48995
49939
  const text = PROVING_TEXT[locale] ?? PROVING_TEXT["pt-BR"];
49940
+ const sysText = SYSTEMS_TEXT[locale] ?? SYSTEMS_TEXT["pt-BR"];
48996
49941
  const tests = options.tests ?? createProvingTests();
48997
49942
  const vehicles = options.vehicles;
48998
49943
  const sounds = { ...defaultSoundIds, ...options.sounds };
@@ -49009,6 +49954,15 @@ var GameVehiclesThree = (() => {
49009
49954
  let run = null;
49010
49955
  let parking = null;
49011
49956
  let driving = null;
49957
+ let crashes = null;
49958
+ let wheels = null;
49959
+ let lastAt = null;
49960
+ let parked = null;
49961
+ const adjust = { seatForward: 0, seatHeight: 0, mirrorLeft: 0, mirrorRight: 0 };
49962
+ const aimed = (pose2) => {
49963
+ const turn = pose2.id === "left" ? adjust.mirrorLeft : pose2.id === "right" ? -adjust.mirrorRight : 0;
49964
+ return turn ? { ...pose2, look: [pose2.look[0] + turn, pose2.look[1], pose2.look[2]] } : pose2;
49965
+ };
49012
49966
  let gearbox = "automatic";
49013
49967
  let lastGear = null;
49014
49968
  let driveClock = 0;
@@ -49170,6 +50124,8 @@ var GameVehiclesThree = (() => {
49170
50124
  };
49171
50125
  const removeCar = () => {
49172
50126
  if (!car) return;
50127
+ wheels = null;
50128
+ lastAt = null;
49173
50129
  discardObject3D(car.built.group);
49174
50130
  car = null;
49175
50131
  };
@@ -49183,6 +50139,7 @@ var GameVehiclesThree = (() => {
49183
50139
  built2.group.add(guides);
49184
50140
  host.scene.add(built2.group);
49185
50141
  car = { built: built2, guides, subject };
50142
+ wheels = createWheelRoll(three, built2);
49186
50143
  pose({ x: 0, z: TRACK.start, yaw: 0, pitch: 0, roll: 0, height: 0 }, 0);
49187
50144
  rig.vehicle?.reset();
49188
50145
  drawBoard();
@@ -49233,11 +50190,18 @@ var GameVehiclesThree = (() => {
49233
50190
  car.built.group.position.set(TRACK.x0 + wrapped, 0, TRACK.z + s.z);
49234
50191
  car.built.group.rotation.set(-s.pitch, s.yaw + Math.PI / 2, s.roll, "YXZ");
49235
50192
  bendGuides(steer);
50193
+ if (lastAt) {
50194
+ const along = (s.x - lastAt.x) * Math.cos(s.yaw) - (s.z - lastAt.z) * Math.sin(s.yaw);
50195
+ if (Math.abs(along) < 5) wheels?.roll(along, steer);
50196
+ }
50197
+ lastAt = { x: s.x, z: s.z };
49236
50198
  };
49237
50199
  const frameOf = (speed) => {
49238
50200
  if (!car) return null;
49239
50201
  const g = car.built.group;
49240
- 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 } : {} };
50202
+ const size = { length: car.built.length, width: car.built.width, height: car.built.height };
50203
+ const [ex, ey, ez] = car.built.driverEye ?? defaultEye(size);
50204
+ return { x: g.position.x, y: g.position.y, z: g.position.z, yaw: g.rotation.y, pitch: -g.rotation.x, roll: g.rotation.z, speed, ...size, eye: [ex, ey + adjust.seatHeight, ez + adjust.seatForward] };
49241
50205
  };
49242
50206
  const applyCamera = () => {
49243
50207
  if (!host?.setViewCamera) return;
@@ -49326,19 +50290,26 @@ var GameVehiclesThree = (() => {
49326
50290
  built: car.built,
49327
50291
  subject: car.subject,
49328
50292
  place: [0, 0, 0],
50293
+ kit: kitFor(three),
50294
+ obstacles: () => parked ? [[parked.root.position.x, parked.root.position.z]] : [],
49329
50295
  gauge: kitFor(three).materials?.gauge,
49330
50296
  needle: kitFor(three).materials?.amber,
49331
50297
  // the engine's own sound turns the key (starter, catch, idle by RPM)
49332
50298
  sounds: { starter: "" },
49333
50299
  onExit: () => stopDriving(),
49334
50300
  onEvent: (event) => {
49335
- if (event.type === "refused") return host?.log(text.cabin.refused[event.reason] ?? event.reason);
50301
+ if (event.type === "refused") return host?.log(text.cabin.refused[event.reason] ?? sysText.refused[event.reason] ?? event.reason);
50302
+ if (event.type === "sound") return;
50303
+ if (event.type !== "command") {
50304
+ if (event.type === "out_of_fuel" || event.type === "stall" || event.type === "seized") engine?.stop([car?.built.group.position.x ?? 0, 0.5, car?.built.group.position.z ?? 0]);
50305
+ return host?.log(sysText.events[event.type] ?? event.type, { vehicle: settings.vehicle, ...event }, "proving_system");
50306
+ }
49336
50307
  const st = event.state;
49337
50308
  const line = cabinLine(event.command, st);
49338
50309
  if (line) host?.log(line);
49339
50310
  if (event.command === "ignition" && car) {
49340
50311
  const at = [car.built.group.position.x, 0.5, car.built.group.position.z];
49341
- if (st.ignition) engine?.start(at);
50312
+ if (st.ignition && driving?.systems.canCrank() !== false) engine?.start(at);
49342
50313
  else engine?.stop(at);
49343
50314
  }
49344
50315
  if (event.command === "gearbox") {
@@ -49357,7 +50328,7 @@ var GameVehiclesThree = (() => {
49357
50328
  rig.vehicle?.reset();
49358
50329
  stopEngine();
49359
50330
  engine = createVehicleSound(host.audio, { sounds });
49360
- host.bench?.sheet?.({ kicker: text.name, title: fillText(text.log.drive, name(settings.vehicle)), body: text.cabin.keys });
50331
+ host.bench?.sheet?.({ kicker: text.name, title: fillText(text.log.drive, name(settings.vehicle)), body: `${text.cabin.keys} \xB7 ${sysText.keys} \xB7 ${sysText.bodyKeys}` });
49361
50332
  host.log(fillText(text.log.drive, name(settings.vehicle)));
49362
50333
  };
49363
50334
  const onOff = (on) => on ? text.cabin.on : text.cabin.off;
@@ -49386,19 +50357,109 @@ var GameVehiclesThree = (() => {
49386
50357
  return `${c.alarm}: ${c.alarmStates[st.alarm]}`;
49387
50358
  case "radio":
49388
50359
  case "radio_source":
49389
- return `${c.radio}: ${st.radio.on ? c.sources[st.radio.source] : c.off}`;
50360
+ case "radio_track":
50361
+ return `${c.radio}: ${st.radio.on ? `${c.sources[st.radio.source]} \xB7 ${fillText(sysText.track, st.radio.track + 1)}` : c.off}`;
49390
50362
  case "indicator_left":
49391
50363
  case "indicator_right":
49392
50364
  case "hazard":
49393
50365
  return `${c.indicator}: ${c.indicators[st.indicator]}`;
49394
50366
  case "wipers":
49395
- return `${c.wipers}: ${c.wiperModes[st.wipers]}`;
50367
+ return `${c.wipers}: ${sysText.wiperModes[st.wipers]}`;
50368
+ case "drive_profile":
50369
+ return `${sysText.profile}: ${sysText.profiles[st.profile]}`;
50370
+ case "traction_control":
50371
+ return `${sysText.traction}: ${onOff(st.tractionControl)}`;
50372
+ case "abs":
50373
+ return `${sysText.abs}: ${onOff(st.abs)}`;
50374
+ case "cruise":
50375
+ return st.cruise.on ? fillText(sysText.cruiseSet, Math.round(st.cruise.speed * 3.6)) : `${sysText.cruise}: ${c.off}`;
50376
+ case "climate":
50377
+ return `${sysText.climate}: ${onOff(st.climate.on)}`;
50378
+ case "demist":
50379
+ return `${sysText.demist}: ${onOff(st.climate.demist)}`;
49396
50380
  case "seatbelt":
49397
50381
  return `${c.seatbelt}: ${onOff(st.seatbelt)}`;
49398
50382
  default:
49399
50383
  return null;
49400
50384
  }
49401
50385
  };
50386
+ const systemsAction = (id) => {
50387
+ if (!driving) startDriving();
50388
+ const cockpit = driving;
50389
+ if (!cockpit) return;
50390
+ const d = cockpit.driver;
50391
+ const done = (key) => host?.log(sysText.done[key], { vehicle: settings.vehicle, action: key }, "proving_system");
50392
+ switch (id) {
50393
+ case "puncture": {
50394
+ const tyres = d.spec.wheels.map((_, i) => d.condition.tyres?.[i] ?? 1);
50395
+ tyres[0] = 0;
50396
+ d.condition = { ...d.condition, tyres };
50397
+ return done("puncture");
50398
+ }
50399
+ case "fit_spare": {
50400
+ const flat = d.spec.wheels.findIndex((_, i) => (d.condition.tyres?.[i] ?? 1) < 0.2);
50401
+ const event = cockpit.systems.fitSpare(d, Math.max(0, flat));
50402
+ host?.log(sysText.events[event.type], { vehicle: settings.vehicle, ...event }, "proving_system");
50403
+ return;
50404
+ }
50405
+ case "refuel":
50406
+ d.refuel(d.spec.tank.capacity);
50407
+ return done("refuel");
50408
+ case "empty_tank":
50409
+ d.state.fuel = Math.min(d.state.fuel, d.spec.tank.capacity * 0.04);
50410
+ return done("empty_tank");
50411
+ case "drain_battery":
50412
+ cockpit.systems.charge(-(cockpit.systems.state.charge - 0.15));
50413
+ return done("drain_battery");
50414
+ case "jump_start":
50415
+ cockpit.systems.charge(1);
50416
+ return done("jump_start");
50417
+ case "break_radiator":
50418
+ cockpit.radiator = 0.05;
50419
+ return done("break_radiator");
50420
+ case "remote_lock":
50421
+ case "remote_unlock": {
50422
+ const lock = id === "remote_lock";
50423
+ cockpit.body?.lock(lock);
50424
+ cockpit.controls.state.alarm = lock ? "armed" : "off";
50425
+ host?.audio?.play?.({ sound: "alarm_chirp", position: [car?.built.group.position.x ?? 0, 1, car?.built.group.position.z ?? 0] });
50426
+ host?.log(sysText.remote[lock ? "locked" : "unlocked"], { vehicle: settings.vehicle, locked: lock }, "proving_system");
50427
+ return;
50428
+ }
50429
+ case "crash_wall": {
50430
+ const g = car?.built.group;
50431
+ if (!g || !host) return;
50432
+ crashes ?? (crashes = createProvingCrash(host, options.occupant));
50433
+ const speed = d.state.u;
50434
+ const ejection = crashes.crash({ car: { position: g.position, rotationY: g.rotation.y }, u: d.state.u, w: d.state.w, belted: cockpit.controls.state.seatbelt, destructible: cockpit.destructible });
50435
+ d.state.u = d.state.w = d.state.yawRate = 0;
50436
+ const kmh2 = Math.round(Math.abs(speed) * 3.6);
50437
+ const belted = cockpit.controls.state.seatbelt;
50438
+ host.log(ejection ? fillText(sysText.crash.ejected, kmh2, sysText.crash.exits[ejection.exit]) : fillText(belted ? sysText.crash.stayed : sysText.crash.mild, kmh2), { vehicle: settings.vehicle, kmh: kmh2, belted: cockpit.controls.state.seatbelt, ejection }, "proving_crash");
50439
+ return;
50440
+ }
50441
+ case "park_behind": {
50442
+ const g = car?.built.group;
50443
+ if (!g || !host) return;
50444
+ parked?.remove();
50445
+ const back = (car?.built.length ?? 4.5) / 2 + 3 + 2.3;
50446
+ const at = [g.position.x - Math.sin(g.rotation.y) * back, 0, g.position.z - Math.cos(g.rotation.y) * back];
50447
+ const placed = host.addObject?.(options.parkedCar ?? "sedan", { position: at, rotationY: g.rotation.y });
50448
+ parked = placed ? { root: placed.root, remove: placed.remove } : null;
50449
+ host.log(placed ? sysText.parking.placed : sysText.parking.none, { vehicle: settings.vehicle }, "proving_system");
50450
+ return;
50451
+ }
50452
+ case "force_door": {
50453
+ const body = cockpit.body;
50454
+ if (!body) return;
50455
+ if (body.locked) body.lock(false);
50456
+ body.toggle("door_front_left");
50457
+ if (cockpit.controls.state.alarm === "armed") cockpit.controls.state.alarm = "ringing";
50458
+ host?.log(sysText.remote.forced, { vehicle: settings.vehicle, alarm: cockpit.controls.state.alarm }, "proving_system");
50459
+ return;
50460
+ }
50461
+ }
50462
+ };
49402
50463
  const gearName = (gear) => gear < 0 ? "R" : gear === 0 ? "N" : String(gear);
49403
50464
  const onChange = (change) => {
49404
50465
  if (change.kind === "layer") {
@@ -49407,6 +50468,7 @@ var GameVehiclesThree = (() => {
49407
50468
  }
49408
50469
  if (change.kind === "action") {
49409
50470
  if (change.id === "drive") return driving ? stopDriving() : startDriving();
50471
+ if (change.id in sysText.actions) return systemsAction(change.id);
49410
50472
  if (change.id === "run_test" || change.id === "run_all" || change.id === "reverse_park") stopDriving();
49411
50473
  if (change.id === "run_test") {
49412
50474
  queue = [];
@@ -49453,6 +50515,12 @@ var GameVehiclesThree = (() => {
49453
50515
  settings.camera = String(change.value);
49454
50516
  applyCamera();
49455
50517
  break;
50518
+ case "seat_forward":
50519
+ case "seat_height":
50520
+ case "mirror_left":
50521
+ case "mirror_right":
50522
+ adjust[ADJUST_KEYS[change.id]] = Number(change.value);
50523
+ break;
49456
50524
  case "time_scale":
49457
50525
  settings.timeScale = Number(change.value);
49458
50526
  break;
@@ -49502,7 +50570,7 @@ var GameVehiclesThree = (() => {
49502
50570
  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 });
49503
50571
  const f = frameOf(speed);
49504
50572
  if (f && rig.vehicle && settings.camera !== "room") rig.vehicle.update(rig.camera, f, dt);
49505
- if (f && rig.vehicle) for (const m of rig.mirrors) rig.vehicle.placeMirror(m.camera, m.pose, f);
50573
+ if (f && rig.vehicle) for (const m of rig.mirrors) rig.vehicle.placeMirror(m.camera, aimed(m.pose), f);
49506
50574
  void steer;
49507
50575
  const key = `${host.bench?.layer("mirrors")}|${host.bench?.layer("reverse_camera")}`;
49508
50576
  if (key !== mirrorKey) {
@@ -49521,6 +50589,10 @@ var GameVehiclesThree = (() => {
49521
50589
  const speedNow = () => Math.abs(run?.driver.state.u ?? driving?.driver.state.u ?? parking?.driver.state.u ?? 0) * 3.6;
49522
50590
  const counts = (verdict) => certificateOf(settings.vehicle)?.results.filter((r) => r.verdict === verdict).length ?? 0;
49523
50591
  const teardown = () => {
50592
+ crashes?.dispose();
50593
+ crashes = null;
50594
+ parked?.remove();
50595
+ parked = null;
49524
50596
  stopDriving();
49525
50597
  stopEngine();
49526
50598
  for (const m of rig.mirrors) m.remove();
@@ -49551,9 +50623,14 @@ var GameVehiclesThree = (() => {
49551
50623
  { kind: "choice", id: "vehicle", name: text.controls.vehicle, hint: text.hints.vehicle, options: vehicles.map((id) => ({ id, name: name(id) })), default: settings.vehicle },
49552
50624
  { kind: "choice", id: "test", name: text.controls.test, hint: text.hints.test, options: measured.map((id) => ({ id, name: text.tests[id] ?? id })), default: settings.test },
49553
50625
  { 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 },
50626
+ { kind: "number", id: "seat_forward", name: sysText.adjust.seat_forward, min: -0.2, max: 0.2, step: 0.02, default: 0 },
50627
+ { kind: "number", id: "seat_height", name: sysText.adjust.seat_height, min: -0.1, max: 0.1, step: 0.01, default: 0 },
50628
+ { kind: "number", id: "mirror_left", name: sysText.adjust.mirror_left, min: -0.3, max: 0.3, step: 0.02, default: 0 },
50629
+ { kind: "number", id: "mirror_right", name: sysText.adjust.mirror_right, min: -0.3, max: 0.3, step: 0.02, default: 0 },
49554
50630
  { kind: "number", id: "time_scale", name: text.controls.time_scale, hint: text.hints.time_scale, min: 1, max: 20, step: 1, default: settings.timeScale },
49555
50631
  { 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 },
49556
50632
  { kind: "action", id: "drive", name: text.actions.drive },
50633
+ ...Object.keys(sysText.actions).map((id) => ({ kind: "action", id, name: sysText.actions[id] })),
49557
50634
  { kind: "choice", id: "gearbox", name: text.controls.gearbox, hint: text.hints.gearbox, options: ["automatic", "manual"].map((id) => ({ id, name: text.gearbox[id] })), default: gearbox },
49558
50635
  { kind: "action", id: "run_test", name: text.actions.run_test },
49559
50636
  { kind: "action", id: "run_all", name: text.actions.run_all },
@@ -49587,6 +50664,8 @@ var GameVehiclesThree = (() => {
49587
50664
  rig.vehicle = createVehicleCamera(three);
49588
50665
  lent.audio?.register?.(LIBRARY_BANK);
49589
50666
  lent.audio?.register?.(CABIN_BANK);
50667
+ lent.audio?.register?.(VEHICLE_SOUND_BANK);
50668
+ lent.audio?.register?.(RADIO_BANK);
49590
50669
  const stopChange = lent.bench?.onChange(onChange);
49591
50670
  const stopFrame = lent.onFrame(frame);
49592
50671
  const stopClear = lent.onClear?.(() => {