@adatechnology/game-vehicles-three 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -209,6 +209,16 @@ function primaryPaint(c) {
209
209
  });
210
210
  return best;
211
211
  }
212
+ var SEATED_EYE = { up: 0.55, back: 0.28 };
213
+ function eyeOf(THREE, c) {
214
+ const tops = c.seatTops ?? [];
215
+ if (!tops.length) return null;
216
+ const points = tops.map((t) => c.vehicle.localToWorld(new THREE.Vector3(t.x, t.y, t.z)));
217
+ const front = Math.max(...points.map((p) => p.z));
218
+ const row = points.filter((p) => p.z > front - 0.25);
219
+ const seat = row.reduce((a, b) => b.x > a.x ? b : a);
220
+ return [+seat.x.toFixed(3), +(seat.y + SEATED_EYE.up).toFixed(3), +(seat.z - SEATED_EYE.back).toFixed(3)];
221
+ }
212
222
  function createVehiclesThree(THREE, options) {
213
223
  const kit = options.kit;
214
224
  const fleet = options.fleet ?? { colorFromSeed: defaultColorFromSeed, fleetDims: defaultFleetDims, paintPalettes: defaultPaintPalettes, paletteOf: defaultPaletteOf };
@@ -244,6 +254,7 @@ function createVehiclesThree(THREE, options) {
244
254
  markKitOrigin(group, { owner: "game-vehicles-three", kind: "vehicle", id: key });
245
255
  const damageables = kit.damageables(c, pivotMatrix);
246
256
  let pruned = 0;
257
+ let driverEye = null;
247
258
  if (quality === "high") {
248
259
  const pivot = new THREE.Group();
249
260
  pivot.name = "vehicle";
@@ -265,6 +276,7 @@ function createVehiclesThree(THREE, options) {
265
276
  }
266
277
  markDamageables(c, damageables);
267
278
  group.updateMatrixWorld(true);
279
+ driverEye = eyeOf(THREE, c);
268
280
  } else {
269
281
  const fusion = fuse(THREE, c, pivotMatrix, color ? new THREE.Color(color) : null, primary, kit.lodHiddenMaterials());
270
282
  for (const mesh of liteMeshes(THREE, fusion, liteMaterials(THREE, kit.studioEnvironment()))) group.add(mesh);
@@ -290,6 +302,7 @@ function createVehiclesThree(THREE, options) {
290
302
  pruned,
291
303
  expected: fleetDims2(key),
292
304
  context: quality === "high" ? c : null,
305
+ driverEye,
293
306
  dispose() {
294
307
  group.traverse((m) => {
295
308
  if (m.geometry) m.geometry.dispose();
@@ -1803,26 +1816,532 @@ function loadVehiclesTestModule(room, options = {}) {
1803
1816
  // src/test-area/proving/module.ts
1804
1817
  import { createVehicleCamera, mirrorPoses } from "@adatechnology/game-camera";
1805
1818
  import { markKitOrigin as markKitOrigin3 } from "@adatechnology/game-contracts";
1806
- import { LIBRARY_BANK as LIBRARY_BANK2 } from "@adatechnology/game-audio";
1807
- import { createSemiSimDriver as createSemiSimDriver3 } from "@adatechnology/game-vehicles-driving";
1808
- import { certify, createProvingTests, startRun, statusOf, subjectSpec, withResult } from "@adatechnology/game-vehicles-proving";
1819
+ import { CABIN_BANK, LIBRARY_BANK as LIBRARY_BANK2 } from "@adatechnology/game-audio";
1820
+ import { createSemiSimDriver as createSemiSimDriver4 } from "@adatechnology/game-vehicles-driving";
1821
+ import { certify, createProvingTests, startRun, statusOf, subjectSpec as subjectSpec2, withResult } from "@adatechnology/game-vehicles-proving";
1822
+ import { discardObject3D as discardObject3D3 } from "@adatechnology/game-shapes-three";
1823
+
1824
+ // src/test-area/proving/cockpit.ts
1825
+ import { createSemiSimDriver as createSemiSimDriver3, createVehicleControls } from "@adatechnology/game-vehicles-driving";
1826
+ import { subjectSpec } from "@adatechnology/game-vehicles-proving";
1827
+
1828
+ // src/dashboard.ts
1809
1829
  import { discardObject3D as discardObject3D2 } from "@adatechnology/game-shapes-three";
1830
+ var DASHBOARD_LOOK = {
1831
+ face: "#0b1110",
1832
+ ring: "#2f3a36",
1833
+ marks: "#cfe8dc",
1834
+ needle: "#ff6a2a",
1835
+ red: "#e2402f",
1836
+ amber: "#f2a630",
1837
+ green: "#46d16f",
1838
+ blue: "#3f8cff",
1839
+ dim: "rgba(207,232,220,0.12)",
1840
+ /** How far in front of the kit's dial the face sits, metres (the kit's needle sticks out 6 mm). */
1841
+ lift: 0.011
1842
+ };
1843
+ var SIZE = 256;
1844
+ function createVehicleDashboard(THREE, built, options) {
1845
+ const maxKmh = options.maxKmh ?? 220;
1846
+ const redline = options.redlineRpm ?? 6500;
1847
+ const doc = options.document ?? (typeof document !== "undefined" ? document : null);
1848
+ const dials = [];
1849
+ built.group.traverse((o) => {
1850
+ if (o.isMesh && o.material === options.gauge) dials.push(o);
1851
+ });
1852
+ const eye = built.driverEye;
1853
+ const hidden = [];
1854
+ const faces = [];
1855
+ if (doc && eye && dials.length) {
1856
+ built.group.updateMatrixWorld(true);
1857
+ const eyeWorld = built.group.localToWorld(new THREE.Vector3(eye[0], eye[1], eye[2]));
1858
+ const near = dials.map((d) => ({ d, at: d.getWorldPosition(new THREE.Vector3()) })).sort((a, b) => a.at.distanceTo(eyeWorld) - b.at.distanceTo(eyeWorld)).slice(0, 2).sort((a, b) => built.group.worldToLocal(b.at.clone()).x - built.group.worldToLocal(a.at.clone()).x);
1859
+ near.forEach(({ d, at }, i) => {
1860
+ const canvas = doc.createElement("canvas");
1861
+ canvas.width = canvas.height = SIZE;
1862
+ const g = canvas.getContext("2d");
1863
+ if (!g) return;
1864
+ const texture = new THREE.CanvasTexture(canvas);
1865
+ const radius = d.geometry.parameters?.radiusTop ?? 0.055;
1866
+ const mesh = new THREE.Mesh(new THREE.CircleGeometry(radius * 1.12, 40), new THREE.MeshBasicMaterial({ map: texture, transparent: true, toneMapped: false, depthWrite: false }));
1867
+ mesh.name = "dashboard_face";
1868
+ const axis = new THREE.Vector3(0, 1, 0).applyQuaternion(d.getWorldQuaternion(d.quaternion.clone())).normalize();
1869
+ if (axis.dot(eyeWorld.clone().sub(at)) < 0) axis.negate();
1870
+ const front = at.clone().addScaledVector(axis, DASHBOARD_LOOK.lift);
1871
+ mesh.position.copy(built.group.worldToLocal(front.clone()));
1872
+ built.group.add(mesh);
1873
+ mesh.updateMatrixWorld(true);
1874
+ mesh.lookAt(front.add(axis));
1875
+ d.visible = false;
1876
+ hidden.push(d);
1877
+ if (options.needle)
1878
+ built.group.traverse((o) => {
1879
+ if (o.isMesh && o.material === options.needle && o.visible && o.getWorldPosition(new THREE.Vector3()).distanceTo(at) < radius * 1.2) {
1880
+ o.visible = false;
1881
+ hidden.push(o);
1882
+ }
1883
+ });
1884
+ faces.push({ mesh, canvas, g, texture, kind: near.length === 1 || i === 0 ? "speed" : "rpm" });
1885
+ });
1886
+ }
1887
+ const lamp = (g, x, y, on, colour, draw2) => {
1888
+ g.save();
1889
+ g.translate(x, y);
1890
+ g.fillStyle = g.strokeStyle = on ? colour : DASHBOARD_LOOK.dim;
1891
+ g.lineWidth = 4;
1892
+ draw2(g);
1893
+ g.restore();
1894
+ };
1895
+ const arrow = (dir) => (g) => {
1896
+ g.beginPath();
1897
+ g.moveTo(dir * 14, 0);
1898
+ g.lineTo(-dir * 2, -12);
1899
+ g.lineTo(-dir * 2, -5);
1900
+ g.lineTo(-dir * 14, -5);
1901
+ g.lineTo(-dir * 14, 5);
1902
+ g.lineTo(-dir * 2, 5);
1903
+ g.lineTo(-dir * 2, 12);
1904
+ g.closePath();
1905
+ g.fill();
1906
+ };
1907
+ const beam = (g) => {
1908
+ g.beginPath();
1909
+ g.arc(-4, 0, 10, Math.PI / 2, -Math.PI / 2, true);
1910
+ g.closePath();
1911
+ g.fill();
1912
+ for (let k = -1; k <= 1; k++) g.fillRect(8, k * 6 - 1.5, 10, 3);
1913
+ };
1914
+ const belt = (g) => {
1915
+ g.beginPath();
1916
+ g.arc(0, -10, 5, 0, Math.PI * 2);
1917
+ g.fill();
1918
+ g.fillRect(-6, -3, 12, 16);
1919
+ g.strokeStyle = DASHBOARD_LOOK.face;
1920
+ g.lineWidth = 3;
1921
+ g.beginPath();
1922
+ g.moveTo(-7, -3);
1923
+ g.lineTo(7, 12);
1924
+ g.stroke();
1925
+ };
1926
+ const letter = (text) => (g) => {
1927
+ g.font = "bold 22px sans-serif";
1928
+ g.textAlign = "center";
1929
+ g.textBaseline = "middle";
1930
+ g.fillText(text, 0, 1);
1931
+ };
1932
+ const ring = (text) => (g) => {
1933
+ g.beginPath();
1934
+ g.arc(0, 0, 13, 0, Math.PI * 2);
1935
+ g.stroke();
1936
+ letter(text)(g);
1937
+ };
1938
+ const dial = (g, value, top, step, redFrom, label) => {
1939
+ const c = SIZE / 2;
1940
+ g.clearRect(0, 0, SIZE, SIZE);
1941
+ g.fillStyle = DASHBOARD_LOOK.face;
1942
+ g.beginPath();
1943
+ g.arc(c, c, c - 2, 0, Math.PI * 2);
1944
+ g.fill();
1945
+ g.strokeStyle = DASHBOARD_LOOK.ring;
1946
+ g.lineWidth = 6;
1947
+ g.stroke();
1948
+ const angle = (v) => Math.PI * 0.75 + Math.max(0, Math.min(top, v)) / top * Math.PI * 1.5;
1949
+ for (let v = 0; v <= top + 1e-6; v += step) {
1950
+ const a2 = angle(v);
1951
+ g.strokeStyle = redFrom !== null && v >= redFrom ? DASHBOARD_LOOK.red : DASHBOARD_LOOK.marks;
1952
+ g.lineWidth = 4;
1953
+ g.beginPath();
1954
+ g.moveTo(c + Math.cos(a2) * (c - 12), c + Math.sin(a2) * (c - 12));
1955
+ g.lineTo(c + Math.cos(a2) * (c - 30), c + Math.sin(a2) * (c - 30));
1956
+ g.stroke();
1957
+ g.fillStyle = DASHBOARD_LOOK.marks;
1958
+ g.font = "18px sans-serif";
1959
+ g.textAlign = "center";
1960
+ g.textBaseline = "middle";
1961
+ g.fillText(label(v), c + Math.cos(a2) * (c - 48), c + Math.sin(a2) * (c - 48));
1962
+ }
1963
+ const a = angle(value);
1964
+ g.strokeStyle = DASHBOARD_LOOK.needle;
1965
+ g.lineWidth = 6;
1966
+ g.beginPath();
1967
+ g.moveTo(c - Math.cos(a) * 14, c - Math.sin(a) * 14);
1968
+ g.lineTo(c + Math.cos(a) * (c - 22), c + Math.sin(a) * (c - 22));
1969
+ g.stroke();
1970
+ g.fillStyle = DASHBOARD_LOOK.ring;
1971
+ g.beginPath();
1972
+ g.arc(c, c, 10, 0, Math.PI * 2);
1973
+ g.fill();
1974
+ };
1975
+ const bar = (g, x, share, warnAbove, label) => {
1976
+ if (share === void 0) return;
1977
+ const h = 60;
1978
+ g.strokeStyle = DASHBOARD_LOOK.marks;
1979
+ g.lineWidth = 2;
1980
+ g.strokeRect(x - 5, 150, 10, h);
1981
+ const s = Math.max(0, Math.min(1, share));
1982
+ g.fillStyle = (warnAbove ? s > 0.85 : s < 0.12) ? DASHBOARD_LOOK.red : DASHBOARD_LOOK.marks;
1983
+ g.fillRect(x - 4, 150 + h * (1 - s), 8, h * s);
1984
+ g.font = "14px sans-serif";
1985
+ g.textAlign = "center";
1986
+ g.fillText(label, x, 222);
1987
+ };
1988
+ const draw = (r) => {
1989
+ const warn = r.ignition && !r.engineOn;
1990
+ for (const f of faces) {
1991
+ const g = f.g;
1992
+ if (f.kind === "speed") {
1993
+ dial(g, r.kmh, maxKmh, maxKmh > 160 ? 20 : 10, null, (v) => String(Math.round(v)));
1994
+ g.fillStyle = DASHBOARD_LOOK.marks;
1995
+ g.font = "bold 30px sans-serif";
1996
+ g.textAlign = "center";
1997
+ g.fillText(String(Math.round(r.kmh)), SIZE / 2, SIZE / 2 + 52);
1998
+ lamp(g, 92, 178, r.left, DASHBOARD_LOOK.green, arrow(-1));
1999
+ lamp(g, 164, 178, r.right, DASHBOARD_LOOK.green, arrow(1));
2000
+ lamp(g, 104, 214, r.highBeam, DASHBOARD_LOOK.blue, beam);
2001
+ lamp(g, 152, 214, r.lowBeam || r.fog, r.fog ? DASHBOARD_LOOK.amber : DASHBOARD_LOOK.green, beam);
2002
+ bar(g, 40, r.fuel, false, "F");
2003
+ } else {
2004
+ dial(g, r.rpm / 1e3, Math.ceil(redline / 1e3) + 1, 1, redline / 1e3, (v) => String(Math.round(v)));
2005
+ g.fillStyle = DASHBOARD_LOOK.marks;
2006
+ g.font = "bold 34px sans-serif";
2007
+ g.textAlign = "center";
2008
+ g.fillText(r.gear < 0 ? "R" : r.gear === 0 ? "N" : String(r.gear), SIZE / 2, SIZE / 2 + 54);
2009
+ lamp(g, 86, 178, r.parkingBrake, DASHBOARD_LOOK.red, ring("P"));
2010
+ lamp(g, 128, 214, r.ignition && !r.seatbelt, DASHBOARD_LOOK.red, belt);
2011
+ lamp(g, 170, 178, warn, DASHBOARD_LOOK.red, letter("+\u2212"));
2012
+ lamp(g, 86, 214, warn, DASHBOARD_LOOK.red, letter("OIL"));
2013
+ lamp(g, 170, 214, r.fourWheel, DASHBOARD_LOOK.green, letter("4x4"));
2014
+ bar(g, SIZE - 40, r.temperature, true, "T");
2015
+ }
2016
+ f.texture.needsUpdate = true;
2017
+ }
2018
+ };
2019
+ let wait = 0;
2020
+ let last = "";
2021
+ return {
2022
+ dials: faces.length,
2023
+ update(reading, dt) {
2024
+ if (!faces.length) return;
2025
+ wait -= dt;
2026
+ const key = JSON.stringify(reading);
2027
+ if (wait > 0 || key === last) return;
2028
+ wait = 1 / (options.fps ?? 15);
2029
+ last = key;
2030
+ draw(reading);
2031
+ },
2032
+ dispose() {
2033
+ for (const f of faces) {
2034
+ discardObject3D2(f.mesh);
2035
+ f.texture.dispose();
2036
+ f.canvas.width = f.canvas.height = 0;
2037
+ }
2038
+ faces.length = 0;
2039
+ for (const o of hidden) o.visible = true;
2040
+ hidden.length = 0;
2041
+ }
2042
+ };
2043
+ }
2044
+
2045
+ // src/lamps.ts
2046
+ import { isLightBudget, LIGHT_PRIORITY } from "@adatechnology/game-render-three";
2047
+ var LAMP_RULES = {
2048
+ glow: { head: { off: 0.04, position: 0.3, low: 1, high: 1.35 }, tail: { off: 0.15, on: 0.7, brake: 1.6 }, fog: { off: 0.04, on: 1.1 }, amber: { off: 0.1, on: 2.2 } },
2049
+ low: { color: 16773584, intensity: 2.4, distance: 32, angle: 0.42, penumbra: 0.55, decay: 1.6, drop: 0.09 },
2050
+ high: { color: 16774882, intensity: 3.6, distance: 70, angle: 0.3, penumbra: 0.45, decay: 1.4, drop: 0.015 },
2051
+ fog: { color: 16773320, intensity: 1.6, distance: 16, angle: 0.75, penumbra: 0.7, decay: 1.8, drop: 0.12 },
2052
+ reverse: { color: 16054015, intensity: 0.9, distance: 7, decay: 2 },
2053
+ interior: { color: 16769712, intensity: 0.5, distance: 2.4, decay: 2 }
2054
+ };
2055
+ function createVehicleLamps(THREE, built, options = {}) {
2056
+ const budget = isLightBudget(options.lights) ? options.lights : null;
2057
+ const owner = options.owner ?? "vehicle-lamps";
2058
+ const context = built.context;
2059
+ const lamps = (context?.lights ?? []).filter((l) => l && Array.isArray(l.inner)).map((l) => l.fog ? { ...l, kind: "fog" } : l);
2060
+ const bulbs = (context?.fogBulbs ?? []).filter((o) => o?.material);
2061
+ built.group.traverse((o) => {
2062
+ if (o.userData?.lamp === "fog" && o.material && !bulbs.includes(o)) bulbs.push(o);
2063
+ });
2064
+ if (bulbs.length) lamps.push({ kind: "fog", inner: bulbs });
2065
+ const has = { head: lamps.some((l) => l.kind === "headlight"), tail: lamps.some((l) => l.kind === "taillight"), fog: lamps.some((l) => l.kind === "fog"), indicators: false };
2066
+ const originals = /* @__PURE__ */ new Map();
2067
+ const copies = /* @__PURE__ */ new Map();
2068
+ const base = /* @__PURE__ */ new Map();
2069
+ const own = (mesh) => {
2070
+ const mat = mesh.material;
2071
+ if (!mat || Array.isArray(mat)) return null;
2072
+ if (copies.has(mat) || base.has(mat)) return mat;
2073
+ const copy = mat.clone();
2074
+ originals.set(mesh, mat);
2075
+ copies.set(copy, mat);
2076
+ base.set(copy, mat.emissiveIntensity ?? 1);
2077
+ mesh.material = copy;
2078
+ return copy;
2079
+ };
2080
+ const haloBase = /* @__PURE__ */ new Map();
2081
+ for (const l of lamps) {
2082
+ for (const m of l.inner) own(m);
2083
+ for (const h of l.halo ?? []) haloBase.set(h, h.scale.x);
2084
+ }
2085
+ const amber = /* @__PURE__ */ new Set();
2086
+ const sideOf = /* @__PURE__ */ new Map();
2087
+ for (const l of lamps) {
2088
+ const x = l.group?.position?.x ?? 0;
2089
+ for (const m of l.inner) {
2090
+ const mat = Array.isArray(m.material) ? null : m.material;
2091
+ const c = (mat && (copies.get(mat) ?? mat))?.color;
2092
+ if (c && c.r > 0.5 && c.r - c.b > 0.35 && c.g < c.r * 0.8 && c.g > c.b) {
2093
+ amber.add(m);
2094
+ sideOf.set(m, x >= 0 ? "left" : "right");
2095
+ }
2096
+ }
2097
+ }
2098
+ has.indicators = amber.size > 0;
2099
+ const glowMesh = (m, share) => {
2100
+ const mat = m.material;
2101
+ if (mat && !Array.isArray(mat) && base.has(mat) && "emissiveIntensity" in mat) mat.emissiveIntensity = base.get(mat) * share;
2102
+ };
2103
+ const glow = (l, share) => {
2104
+ for (const m of l.inner) {
2105
+ if (amber.has(m)) continue;
2106
+ glowMesh(m, share);
2107
+ }
2108
+ for (const h of l.halo ?? []) {
2109
+ h.visible = share > 0.5;
2110
+ const s = (haloBase.get(h) ?? 1) * Math.min(1.4, share);
2111
+ h.scale.set(s, s, 1);
2112
+ }
2113
+ };
2114
+ const L = built.length;
2115
+ const W = built.width;
2116
+ const H = built.height;
2117
+ const anchors = [];
2118
+ const anchor = (x, y, z, yaw, drop) => {
2119
+ const a = new THREE.Object3D();
2120
+ a.position.set(x, y, z);
2121
+ a.rotation.set(-drop, yaw, 0, "YXZ");
2122
+ built.group.add(a);
2123
+ anchors.push(a);
2124
+ return a;
2125
+ };
2126
+ const front = [-1, 1].map((s) => anchor(s * W * 0.34, Math.min(0.9, H * 0.48), L / 2 - 0.05, Math.PI, 0));
2127
+ const fogAnchors = [-1, 1].map((s) => anchor(s * W * 0.38, Math.min(0.45, H * 0.25), L / 2 - 0.03, Math.PI, 0));
2128
+ const rear = anchor(0, Math.min(0.9, H * 0.45), -L / 2 - 0.3, 0, 0);
2129
+ const cabin = anchor(0, H * 0.85, 0, 0, 0);
2130
+ const sources = /* @__PURE__ */ new Map();
2131
+ const beam = (id, on, make) => {
2132
+ const now = sources.get(id);
2133
+ if (on && !now && budget) sources.set(id, make());
2134
+ if (!on && now) {
2135
+ for (const s of now) s.remove();
2136
+ sources.delete(id);
2137
+ }
2138
+ };
2139
+ const spots = (at, rule) => at.map((a) => {
2140
+ a.rotation.x = -rule.drop;
2141
+ return budget.addSource({ owner, kind: "spot", priority: LIGHT_PRIORITY.headlight, color: rule.color, intensity: rule.intensity, distance: rule.distance, decay: rule.decay, angle: rule.angle, penumbra: rule.penumbra, position: [0, 0, 0], follow: a });
2142
+ });
2143
+ const point = (a, rule) => [
2144
+ budget.addSource({ owner, kind: "point", priority: LIGHT_PRIORITY.lamp, color: rule.color, intensity: rule.intensity, distance: rule.distance, decay: rule.decay, position: [0, 0, 0], follow: a })
2145
+ ];
2146
+ const state = { mode: "off", fog: false, interior: false, brake: false, reverse: false, left: false, right: false };
2147
+ const apply = () => {
2148
+ const g = LAMP_RULES.glow;
2149
+ for (const l of lamps) {
2150
+ if (l.kind === "headlight") glow(l, g.head[state.mode]);
2151
+ else if (l.kind === "taillight") glow(l, state.brake ? g.tail.brake : state.mode === "off" ? g.tail.off : g.tail.on);
2152
+ else if (l.kind === "fog") glow(l, state.fog ? g.fog.on : g.fog.off);
2153
+ }
2154
+ for (const m of amber) glowMesh(m, state[sideOf.get(m) ?? "left"] ? g.amber.on : g.amber.off);
2155
+ const beamId = state.mode === "low" || state.mode === "high" ? state.mode : null;
2156
+ for (const id of ["low", "high"]) beam(id, beamId === id, () => spots(front, LAMP_RULES[id]));
2157
+ beam("fog", state.fog && has.fog, () => spots(fogAnchors, LAMP_RULES.fog));
2158
+ beam("reverse", state.reverse, () => point(rear, LAMP_RULES.reverse));
2159
+ beam("interior", state.interior, () => point(cabin, LAMP_RULES.interior));
2160
+ };
2161
+ apply();
2162
+ return {
2163
+ has,
2164
+ get state() {
2165
+ return { ...state };
2166
+ },
2167
+ set(next) {
2168
+ if (next.mode === state.mode && next.fog === state.fog && next.interior === state.interior && next.brake === state.brake && next.reverse === state.reverse && next.left === state.left && next.right === state.right) return;
2169
+ Object.assign(state, next);
2170
+ apply();
2171
+ },
2172
+ dispose() {
2173
+ for (const list of sources.values()) for (const s of list) s.remove();
2174
+ sources.clear();
2175
+ for (const a of anchors) a.removeFromParent();
2176
+ anchors.length = 0;
2177
+ for (const l of lamps) {
2178
+ for (const m of l.inner) {
2179
+ const copy = m.material;
2180
+ const original = originals.get(m);
2181
+ if (original) {
2182
+ m.material = original;
2183
+ if (copy && !Array.isArray(copy)) copy.dispose?.();
2184
+ }
2185
+ }
2186
+ for (const h of l.halo ?? []) {
2187
+ h.visible = true;
2188
+ const s = haloBase.get(h) ?? 1;
2189
+ h.scale.set(s, s, 1);
2190
+ }
2191
+ }
2192
+ originals.clear();
2193
+ copies.clear();
2194
+ base.clear();
2195
+ }
2196
+ };
2197
+ }
2198
+
2199
+ // src/test-area/proving/cockpit.ts
2200
+ var DEFAULT_COCKPIT_SOUNDS = { horn: "horn", blinker: "blinker_tick", starter: "starter_crank", alarm: "car_alarm", chirp: "alarm_chirp", wiper: "wiper_swish", chime: "seatbelt_chime", radio: "radio_static" };
2201
+ function createCockpit(o) {
2202
+ const sounds = { ...DEFAULT_COCKPIT_SOUNDS, ...o.sounds };
2203
+ const spec = subjectSpec(o.subject);
2204
+ const driver = createSemiSimDriver3(spec);
2205
+ driver.place(o.place[0], o.place[1], o.place[2]);
2206
+ const lamps = createVehicleLamps(o.THREE, o.built, { lights: o.host.lights, owner: "proving-ground" });
2207
+ const audio = o.host.audio;
2208
+ const top = o.subject.stats.maxSpeedKmh;
2209
+ const dashboard = o.gauge ? createVehicleDashboard(o.THREE, o.built, { gauge: o.gauge, needle: o.needle, redlineRpm: spec.engine.redlineRpm, maxKmh: top ? Math.ceil((top + 10) / 20) * 20 : void 0 }) : null;
2210
+ const at = () => [o.built.group.position.x, 1, o.built.group.position.z];
2211
+ const play = (sound, volume = 1) => void audio?.play?.({ sound, position: at(), volume });
2212
+ let alarmAgain = 0;
2213
+ let hornAgain = 0;
2214
+ let wipeAgain = 0;
2215
+ let chimeAgain = 0;
2216
+ let radioAgain = 0;
2217
+ let exitAsked = false;
2218
+ let gas = 0;
2219
+ const controls = createVehicleControls({
2220
+ gears: spec.gears?.length ?? 5,
2221
+ fourWheelDrive: !!o.subject.body.fourWheelDrive,
2222
+ fogLamps: lamps.has.fog,
2223
+ onEvent: (event) => {
2224
+ if (event.type === "command") {
2225
+ if (event.command === "ignition" && event.state.ignition && sounds.starter) play(sounds.starter);
2226
+ if (event.command === "ignition" && !event.state.ignition) driver.state.engineOn = false;
2227
+ if (event.command === "alarm" && event.state.alarm === "armed") play(sounds.chirp);
2228
+ if (event.command === "alarm") alarmAgain = 0;
2229
+ if (event.command === "radio" || event.command === "radio_source") radioAgain = 0;
2230
+ if (event.command === "exit") exitAsked = true;
2231
+ }
2232
+ o.onEvent?.(event);
2233
+ }
2234
+ });
2235
+ driver.state.engineOn = false;
2236
+ const target = typeof window !== "undefined" ? window : null;
2237
+ const down = (e) => {
2238
+ if (!controls.key(e.code, true)) return;
2239
+ e.stopImmediatePropagation();
2240
+ if (e.code.startsWith("Arrow") || e.code === "Space") e.preventDefault();
2241
+ };
2242
+ const up = (e) => {
2243
+ if (controls.key(e.code, false)) e.stopImmediatePropagation();
2244
+ };
2245
+ const blur = () => controls.release();
2246
+ target?.addEventListener("keydown", down, true);
2247
+ target?.addEventListener("keyup", up, true);
2248
+ target?.addEventListener("blur", blur);
2249
+ return {
2250
+ driver,
2251
+ controls,
2252
+ lamps,
2253
+ dashboard,
2254
+ get throttle() {
2255
+ return gas;
2256
+ },
2257
+ step(dt) {
2258
+ const input = controls.input(dt, driver);
2259
+ gas = input.throttle;
2260
+ driver.step(input, dt);
2261
+ controls.after(driver, dt);
2262
+ const s = controls.state;
2263
+ lamps.set({
2264
+ mode: s.lamps.mode,
2265
+ fog: s.lamps.fog,
2266
+ interior: s.lamps.interior,
2267
+ brake: s.brakeLights,
2268
+ reverse: s.reverseLights,
2269
+ left: s.blinkOn && (s.indicator === "left" || s.indicator === "hazard"),
2270
+ right: s.blinkOn && (s.indicator === "right" || s.indicator === "hazard")
2271
+ });
2272
+ dashboard?.update(
2273
+ {
2274
+ kmh: Math.abs(driver.state.u) * 3.6,
2275
+ rpm: driver.state.engineOn ? driver.state.rpm : 0,
2276
+ gear: driver.state.gear,
2277
+ ignition: s.ignition,
2278
+ engineOn: driver.state.engineOn,
2279
+ left: s.blinkOn && (s.indicator === "left" || s.indicator === "hazard"),
2280
+ right: s.blinkOn && (s.indicator === "right" || s.indicator === "hazard"),
2281
+ highBeam: s.lamps.mode === "high",
2282
+ lowBeam: s.lamps.mode === "low" || s.lamps.mode === "position",
2283
+ fog: s.lamps.fog,
2284
+ parkingBrake: s.parkingBrake,
2285
+ seatbelt: s.seatbelt,
2286
+ fourWheel: s.fourWheelEngaged
2287
+ },
2288
+ dt
2289
+ );
2290
+ if (s.tick) play(sounds.blinker, 0.8);
2291
+ if (s.horn && (hornAgain -= dt) <= 0) {
2292
+ play(sounds.horn);
2293
+ hornAgain = 0.5;
2294
+ } else if (!s.horn) hornAgain = 0;
2295
+ if (s.alarm === "ringing" && (alarmAgain -= dt) <= 0) {
2296
+ play(sounds.alarm);
2297
+ alarmAgain = 2.9;
2298
+ }
2299
+ if (s.wipers !== "off" && (wipeAgain -= dt) <= 0) {
2300
+ play(sounds.wiper, 0.7);
2301
+ wipeAgain = s.wipers === "fast" ? 0.6 : 1.4;
2302
+ }
2303
+ const kmh = Math.abs(driver.state.u) * 3.6;
2304
+ if (!s.seatbelt && s.ignition && kmh > 10 && (chimeAgain -= dt) <= 0) {
2305
+ play(sounds.chime, 0.6);
2306
+ chimeAgain = 6;
2307
+ }
2308
+ if (s.radio.on && (radioAgain -= dt) <= 0) {
2309
+ play(sounds.radio, s.radio.source === "tape" ? 0.25 : 0.45);
2310
+ radioAgain = 0.55;
2311
+ }
2312
+ if (exitAsked) {
2313
+ exitAsked = false;
2314
+ o.onExit();
2315
+ }
2316
+ },
2317
+ stop() {
2318
+ target?.removeEventListener("keydown", down, true);
2319
+ target?.removeEventListener("keyup", up, true);
2320
+ target?.removeEventListener("blur", blur);
2321
+ controls.release();
2322
+ lamps.dispose();
2323
+ dashboard?.dispose();
2324
+ }
2325
+ };
2326
+ }
1810
2327
 
1811
2328
  // src/test-area/proving/text.ts
1812
2329
  var PROVING_TEXT = {
1813
2330
  "pt-BR": {
1814
2331
  name: "Banco de provas",
1815
- controls: { vehicle: "Ve\xEDculo", test: "Prova", camera: "C\xE2mera", time_scale: "Velocidade da simula\xE7\xE3o", visual_check: "Checagem visual" },
2332
+ controls: { vehicle: "Ve\xEDculo", test: "Prova", camera: "C\xE2mera", time_scale: "Velocidade da simula\xE7\xE3o", visual_check: "Checagem visual", gearbox: "C\xE2mbio" },
2333
+ gearbox: { automatic: "Autom\xE1tico", manual: "Manual" },
1816
2334
  hints: {
2335
+ gearbox: "Autom\xE1tico troca sozinho. Manual: E sobe e Q desce a marcha (abaixo da 1\xAA v\xEAm o neutro e a r\xE9). M alterna dirigindo",
1817
2336
  vehicle: "Ve\xEDculo da frota que vai para a pista",
1818
2337
  test: "Prova medida que o bot\xE3o Rodar prova executa",
1819
2338
  camera: "Sala: a c\xE2mera do jogador. Persegui\xE7\xE3o, cabine e cap\xF4 seguem o ve\xEDculo",
1820
2339
  time_scale: "Quantas vezes mais r\xE1pido que o tempo real a prova roda",
1821
2340
  visual_check: "O que voc\xEA olhou e vai aprovar ou reprovar"
1822
2341
  },
1823
- actions: { run_test: "Rodar prova", run_all: "Rodar todas na pista", certify_now: "Homologar agora (sem anima\xE7\xE3o)", reverse_park: "Dar r\xE9 (c\xE2mera de r\xE9)", sign_ok: "Aprovar checagem visual", sign_fail: "Reprovar checagem visual", reset: "Limpar ficha" },
2342
+ actions: { drive: "Dirigir (entrar / sair)", run_test: "Rodar prova", run_all: "Rodar todas na pista", certify_now: "Homologar agora (sem anima\xE7\xE3o)", reverse_park: "Dar r\xE9 (c\xE2mera de r\xE9)", sign_ok: "Aprovar checagem visual", sign_fail: "Reprovar checagem visual", reset: "Limpar ficha" },
1824
2343
  layers: { mirrors: "Retrovisores", reverse_camera: "C\xE2mera de r\xE9", scoreboard: "Painel da ficha" },
1825
- metrics: { speed: "Velocidade", time: "Tempo da prova", value: "Medi\xE7\xE3o", passed: "Aprovadas", failed: "Reprovadas", pending: "Pendentes", status: "Situa\xE7\xE3o (0 pend., 1 aprov., -1 reprov.)" },
2344
+ metrics: { gear: "Marcha (-1 r\xE9, 0 neutro)", rpm: "Rota\xE7\xE3o", speed: "Velocidade", time: "Tempo da prova", value: "Medi\xE7\xE3o", passed: "Aprovadas", failed: "Reprovadas", pending: "Pendentes", status: "Situa\xE7\xE3o (0 pend., 1 aprov., -1 reprov.)" },
1826
2345
  cameras: { room: "Sala", chase: "Persegui\xE7\xE3o (3\xAA pessoa)", cockpit: "Cabine (1\xAA pessoa)", hood: "Cap\xF4" },
1827
2346
  tests: {
1828
2347
  accel_0_100: "0 a 100 km/h",
@@ -1844,22 +2363,49 @@ var PROVING_TEXT = {
1844
2363
  },
1845
2364
  status: { approved: "APROVADO", rejected: "REPROVADO", pending: "PENDENTE" },
1846
2365
  verdict: { pass: "ok", fail: "falhou", pending: "\u2026" },
1847
- board: { title: "Ficha de homologa\xE7\xE3o", test: "Prova", value: "Medido", range: "Faixa", verdict: "Resultado", notRun: "n\xE3o rodada", running: "rodando" },
1848
- log: { start: "Prova {0}: {1}", result: "{0} \xB7 {1}: {2} {3} (faixa {4}) \u2192 {5}", signed: "{0} \xB7 {1}: {2} na sala", certified: "{0}: {1}" }
2366
+ board: { title: "Ficha de homologa\xE7\xE3o", test: "Prova", value: "Medido", range: "Faixa", verdict: "Resultado", notRun: "n\xE3o rodada", running: "rodando", gear: "Marcha {0} \xB7 {1} rpm \xB7 {2} km/h" },
2367
+ log: { start: "Prova {0}: {1}", result: "{0} \xB7 {1}: {2} {3} (faixa {4}) \u2192 {5}", signed: "{0} \xB7 {1}: {2} na sala", certified: "{0}: {1}", drive: "Dirigindo {0}: I liga o motor, F sai do carro", leave: "Saiu do volante", shift: "Marcha {0} \u2192 {1} a {2} km/h" },
2368
+ cabin: {
2369
+ keys: "W ou \u2191 acelera \xB7 S ou \u2193 freia (parado, d\xE1 r\xE9) \xB7 A/D ou \u2190/\u2192 vira \xB7 Espa\xE7o freio de m\xE3o \xB7 P puxa o freio de m\xE3o \xB7 I liga e desliga \xB7 M c\xE2mbio manual/autom\xE1tico \xB7 E sobe e Q desce marcha \xB7 G tra\xE7\xE3o 4x2/4x4/4x4 autom\xE1tica \xB7 L luzes (posi\xE7\xE3o, baixa) \xB7 K farol alto \xB7 N neblina \xB7 U luz interna \xB7 v\xEDrgula e ponto setas \xB7 B pisca-alerta \xB7 O limpador \xB7 J cinto \xB7 H buzina \xB7 Y alarme \xB7 R r\xE1dio \xB7 T r\xE1dio/fita \xB7 F sai do carro",
2370
+ on: "ligado",
2371
+ off: "desligado",
2372
+ ignition: "Motor",
2373
+ gearbox: "C\xE2mbio",
2374
+ gear: "Marcha",
2375
+ parkingBrake: "Freio de m\xE3o",
2376
+ driveMode: "Tra\xE7\xE3o",
2377
+ modes: { "4x2": "4x2", "4x4": "4x4", auto: "4x4 autom\xE1tica" },
2378
+ lights: "Luzes",
2379
+ lampModes: { off: "apagadas", position: "posi\xE7\xE3o", low: "farol baixo", high: "farol alto" },
2380
+ fog: "Neblina",
2381
+ interior: "Luz interna",
2382
+ alarm: "Alarme",
2383
+ alarmStates: { off: "desligado", armed: "armado", ringing: "disparado" },
2384
+ radio: "Som",
2385
+ sources: { radio: "r\xE1dio", tape: "toca-fitas" },
2386
+ indicator: "Setas",
2387
+ indicators: { off: "desligadas", left: "esquerda", right: "direita", hazard: "pisca-alerta" },
2388
+ wipers: "Limpador",
2389
+ wiperModes: { off: "desligado", slow: "lento", fast: "r\xE1pido" },
2390
+ seatbelt: "Cinto",
2391
+ refused: { no_four_wheel_drive: "Este ve\xEDculo n\xE3o tem tra\xE7\xE3o 4x4", no_fog_lamps: "Este ve\xEDculo n\xE3o tem farol de neblina", ignition_off: "Ligue o motor primeiro" }
2392
+ }
1849
2393
  },
1850
2394
  en: {
1851
2395
  name: "Proving ground",
1852
- controls: { vehicle: "Vehicle", test: "Test", camera: "Camera", time_scale: "Simulation speed", visual_check: "Visual check" },
2396
+ controls: { vehicle: "Vehicle", test: "Test", camera: "Camera", time_scale: "Simulation speed", visual_check: "Visual check", gearbox: "Gearbox" },
2397
+ gearbox: { automatic: "Automatic", manual: "Manual" },
1853
2398
  hints: {
2399
+ gearbox: "Automatic shifts by itself. Manual: E shifts up and Q down (below 1st come neutral and reverse). M switches while driving",
1854
2400
  vehicle: "Fleet vehicle that goes on the track",
1855
2401
  test: "Measured test the Run test button runs",
1856
2402
  camera: "Room: the player camera. Chase, cockpit and hood follow the vehicle",
1857
2403
  time_scale: "How many times faster than real time the test runs",
1858
2404
  visual_check: "What you looked at and will approve or refuse"
1859
2405
  },
1860
- actions: { run_test: "Run test", run_all: "Run all on the track", certify_now: "Certify now (no animation)", reverse_park: "Reverse (reversing camera)", sign_ok: "Approve visual check", sign_fail: "Refuse visual check", reset: "Clear certificate" },
2406
+ actions: { drive: "Drive (get in / out)", run_test: "Run test", run_all: "Run all on the track", certify_now: "Certify now (no animation)", reverse_park: "Reverse (reversing camera)", sign_ok: "Approve visual check", sign_fail: "Refuse visual check", reset: "Clear certificate" },
1861
2407
  layers: { mirrors: "Mirrors", reverse_camera: "Reversing camera", scoreboard: "Certificate board" },
1862
- metrics: { speed: "Speed", time: "Test time", value: "Measured", passed: "Passed", failed: "Failed", pending: "Pending", status: "Status (0 pending, 1 approved, -1 rejected)" },
2408
+ metrics: { gear: "Gear (-1 reverse, 0 neutral)", rpm: "Engine speed", speed: "Speed", time: "Test time", value: "Measured", passed: "Passed", failed: "Failed", pending: "Pending", status: "Status (0 pending, 1 approved, -1 rejected)" },
1863
2409
  cameras: { room: "Room", chase: "Chase (third person)", cockpit: "Cockpit (first person)", hood: "Hood" },
1864
2410
  tests: {
1865
2411
  accel_0_100: "0 to 100 km/h",
@@ -1881,8 +2427,33 @@ var PROVING_TEXT = {
1881
2427
  },
1882
2428
  status: { approved: "APPROVED", rejected: "REJECTED", pending: "PENDING" },
1883
2429
  verdict: { pass: "ok", fail: "failed", pending: "\u2026" },
1884
- board: { title: "Certificate", test: "Test", value: "Measured", range: "Range", verdict: "Result", notRun: "not run", running: "running" },
1885
- log: { start: "Test {0}: {1}", result: "{0} \xB7 {1}: {2} {3} (range {4}) \u2192 {5}", signed: "{0} \xB7 {1}: {2} in the room", certified: "{0}: {1}" }
2430
+ board: { title: "Certificate", test: "Test", value: "Measured", range: "Range", verdict: "Result", notRun: "not run", running: "running", gear: "Gear {0} \xB7 {1} rpm \xB7 {2} km/h" },
2431
+ log: { start: "Test {0}: {1}", result: "{0} \xB7 {1}: {2} {3} (range {4}) \u2192 {5}", signed: "{0} \xB7 {1}: {2} in the room", certified: "{0}: {1}", drive: "Driving {0}: I starts the engine, F gets out", leave: "Left the wheel", shift: "Gear {0} \u2192 {1} at {2} km/h" },
2432
+ cabin: {
2433
+ keys: "W or \u2191 accelerates \xB7 S or \u2193 brakes (stopped, reverses) \xB7 A/D or \u2190/\u2192 steer \xB7 Space handbrake \xB7 P pulls the handbrake \xB7 I ignition \xB7 M manual/automatic gearbox \xB7 E shifts up, Q down \xB7 G drive 4x2/4x4/auto 4x4 \xB7 L lamps (position, low) \xB7 K high beam \xB7 N fog \xB7 U cabin light \xB7 comma and period indicators \xB7 B hazard \xB7 O wipers \xB7 J seat belt \xB7 H horn \xB7 Y alarm \xB7 R radio \xB7 T radio/tape \xB7 F gets out",
2434
+ on: "on",
2435
+ off: "off",
2436
+ ignition: "Engine",
2437
+ gearbox: "Gearbox",
2438
+ gear: "Gear",
2439
+ parkingBrake: "Handbrake",
2440
+ driveMode: "Drive",
2441
+ modes: { "4x2": "4x2", "4x4": "4x4", auto: "automatic 4x4" },
2442
+ lights: "Lamps",
2443
+ lampModes: { off: "off", position: "position", low: "low beam", high: "high beam" },
2444
+ fog: "Fog lamps",
2445
+ interior: "Cabin light",
2446
+ alarm: "Alarm",
2447
+ alarmStates: { off: "off", armed: "armed", ringing: "ringing" },
2448
+ radio: "Sound",
2449
+ sources: { radio: "radio", tape: "tape" },
2450
+ indicator: "Indicators",
2451
+ indicators: { off: "off", left: "left", right: "right", hazard: "hazard" },
2452
+ wipers: "Wipers",
2453
+ wiperModes: { off: "off", slow: "slow", fast: "fast" },
2454
+ seatbelt: "Seat belt",
2455
+ refused: { no_four_wheel_drive: "This vehicle has no 4x4", no_fog_lamps: "This vehicle has no fog lamps", ignition_off: "Start the engine first" }
2456
+ }
1886
2457
  }
1887
2458
  };
1888
2459
  var fillText = (template, ...values) => template.replace(/\{(\d+)\}/g, (_, i) => String(values[Number(i)] ?? ""));
@@ -1916,6 +2487,10 @@ function createProvingModule(options) {
1916
2487
  let car = null;
1917
2488
  let run = null;
1918
2489
  let parking = null;
2490
+ let driving = null;
2491
+ let gearbox = "automatic";
2492
+ let lastGear = null;
2493
+ let driveClock = 0;
1919
2494
  let queue = [];
1920
2495
  let open = false;
1921
2496
  let lastWrap = 0;
@@ -2050,13 +2625,13 @@ function createProvingModule(options) {
2050
2625
  [text.board.test, text.board.value, text.board.range, text.board.verdict].forEach((h, i) => g.fillText(h.toUpperCase(), cols[i], 150));
2051
2626
  let y = 190;
2052
2627
  for (const r of cert.results) {
2053
- const live = run && run.test.id === r.test && !run.done;
2628
+ const live2 = run && run.test.id === r.test && !run.done;
2054
2629
  g.fillStyle = "#e7e9ec";
2055
2630
  g.font = "26px sans-serif";
2056
2631
  g.fillText(text.tests[r.test] ?? r.test, cols[0], y);
2057
2632
  g.font = "26px monospace";
2058
2633
  g.fillStyle = "#5fd4e0";
2059
- const value = live ? text.board.running : r.value == null ? r.kind === "measured" && r.note === "not run" ? text.board.notRun : "\u2014" : `${r.value} ${r.unit}`;
2634
+ const value = live2 ? text.board.running : r.value == null ? r.kind === "measured" && r.note === "not run" ? text.board.notRun : "\u2014" : `${r.value} ${r.unit}`;
2060
2635
  g.fillText(value, cols[1], y);
2061
2636
  g.fillStyle = "#8d95a3";
2062
2637
  g.fillText(r.kind === "measured" ? fmtRange(r.range) : r.kind === "visual" ? "visual" : "sala", cols[2], y);
@@ -2064,11 +2639,17 @@ function createProvingModule(options) {
2064
2639
  g.fillText(r.verdict === "pass" ? "\u2713 " + text.verdict.pass : r.verdict === "fail" ? "\u2717 " + text.verdict.fail : "\u2026", cols[3], y);
2065
2640
  y += 30;
2066
2641
  }
2642
+ const live = run?.driver.state ?? driving?.driver.state;
2643
+ if (live) {
2644
+ g.font = "24px monospace";
2645
+ g.fillStyle = "#f2c230";
2646
+ g.fillText(fillText(text.board.gear, gearName(live.gear), Math.round(live.rpm), Math.round(Math.abs(live.u) * 3.6)), cols[0], y + 16);
2647
+ }
2067
2648
  board.texture.needsUpdate = true;
2068
2649
  };
2069
2650
  const removeCar = () => {
2070
2651
  if (!car) return;
2071
- discardObject3D2(car.built.group);
2652
+ discardObject3D3(car.built.group);
2072
2653
  car = null;
2073
2654
  };
2074
2655
  const placeCar = () => {
@@ -2135,7 +2716,7 @@ function createProvingModule(options) {
2135
2716
  const frameOf = (speed) => {
2136
2717
  if (!car) return null;
2137
2718
  const g = car.built.group;
2138
- return { x: g.position.x, y: g.position.y, z: g.position.z, yaw: g.rotation.y, pitch: -g.rotation.x, roll: g.rotation.z, speed, length: car.built.length, width: car.built.width, height: car.built.height };
2719
+ return { x: g.position.x, y: g.position.y, z: g.position.z, yaw: g.rotation.y, pitch: -g.rotation.x, roll: g.rotation.z, speed, length: car.built.length, width: car.built.width, height: car.built.height, ...car.built.driverEye ? { eye: car.built.driverEye } : {} };
2139
2720
  };
2140
2721
  const applyCamera = () => {
2141
2722
  if (!host?.setViewCamera) return;
@@ -2197,16 +2778,115 @@ function createProvingModule(options) {
2197
2778
  run = null;
2198
2779
  queue = [];
2199
2780
  setSurface("dry_asphalt");
2200
- const driver = createSemiSimDriver3(subjectSpec(car.subject));
2781
+ const driver = createSemiSimDriver4(subjectSpec2(car.subject));
2201
2782
  driver.place(40, 0, 0);
2202
2783
  parking = { driver, time: 0 };
2203
2784
  };
2785
+ const stopDriving = () => {
2786
+ if (!driving) return;
2787
+ driving.stop();
2788
+ driving = null;
2789
+ host?.holdPlayer?.(false);
2790
+ host?.bench?.sheet?.(null);
2791
+ stopEngine();
2792
+ host?.log(text.log.leave);
2793
+ };
2794
+ const startDriving = () => {
2795
+ if (!host || !three) return;
2796
+ if (!car || car.subject.id !== settings.vehicle) placeCar();
2797
+ if (!car) return;
2798
+ run = null;
2799
+ queue = [];
2800
+ parking = null;
2801
+ setSurface("dry_asphalt");
2802
+ driving = createCockpit({
2803
+ THREE: three,
2804
+ host,
2805
+ built: car.built,
2806
+ subject: car.subject,
2807
+ place: [0, 0, 0],
2808
+ gauge: kitFor(three).materials?.gauge,
2809
+ needle: kitFor(three).materials?.amber,
2810
+ // the engine's own sound turns the key (starter, catch, idle by RPM)
2811
+ sounds: { starter: "" },
2812
+ onExit: () => stopDriving(),
2813
+ onEvent: (event) => {
2814
+ if (event.type === "refused") return host?.log(text.cabin.refused[event.reason] ?? event.reason);
2815
+ const st = event.state;
2816
+ const line = cabinLine(event.command, st);
2817
+ if (line) host?.log(line);
2818
+ if (event.command === "ignition" && car) {
2819
+ const at = [car.built.group.position.x, 0.5, car.built.group.position.z];
2820
+ if (st.ignition) engine?.start(at);
2821
+ else engine?.stop(at);
2822
+ }
2823
+ if (event.command === "gearbox") {
2824
+ gearbox = st.gearbox;
2825
+ host?.bench?.patch("gearbox", { value: st.gearbox });
2826
+ }
2827
+ }
2828
+ });
2829
+ if (gearbox !== driving.controls.state.gearbox) driving.controls.command("gearbox");
2830
+ host.holdPlayer?.(true);
2831
+ if (settings.camera === "room") {
2832
+ settings.camera = "cockpit";
2833
+ host.bench?.patch("camera", { value: "cockpit" });
2834
+ applyCamera();
2835
+ }
2836
+ rig.vehicle?.reset();
2837
+ stopEngine();
2838
+ engine = createVehicleSound(host.audio, { sounds });
2839
+ host.bench?.sheet?.({ kicker: text.name, title: fillText(text.log.drive, name(settings.vehicle)), body: text.cabin.keys });
2840
+ host.log(fillText(text.log.drive, name(settings.vehicle)));
2841
+ };
2842
+ const onOff = (on) => on ? text.cabin.on : text.cabin.off;
2843
+ const cabinLine = (command, st) => {
2844
+ const c = text.cabin;
2845
+ switch (command) {
2846
+ case "ignition":
2847
+ return `${c.ignition}: ${onOff(st.ignition)}`;
2848
+ case "gearbox":
2849
+ return `${c.gearbox}: ${text.gearbox[st.gearbox]}`;
2850
+ case "gear_up":
2851
+ case "gear_down":
2852
+ return `${c.gear}: ${gearName(st.gear)}`;
2853
+ case "parking_brake":
2854
+ return `${c.parkingBrake}: ${onOff(st.parkingBrake)}`;
2855
+ case "drive_mode":
2856
+ return `${c.driveMode}: ${c.modes[st.driveMode]}`;
2857
+ case "lights":
2858
+ case "high_beam":
2859
+ return `${c.lights}: ${c.lampModes[st.lamps.mode]}`;
2860
+ case "fog_lights":
2861
+ return `${c.fog}: ${onOff(st.lamps.fog)}`;
2862
+ case "interior_light":
2863
+ return `${c.interior}: ${onOff(st.lamps.interior)}`;
2864
+ case "alarm":
2865
+ return `${c.alarm}: ${c.alarmStates[st.alarm]}`;
2866
+ case "radio":
2867
+ case "radio_source":
2868
+ return `${c.radio}: ${st.radio.on ? c.sources[st.radio.source] : c.off}`;
2869
+ case "indicator_left":
2870
+ case "indicator_right":
2871
+ case "hazard":
2872
+ return `${c.indicator}: ${c.indicators[st.indicator]}`;
2873
+ case "wipers":
2874
+ return `${c.wipers}: ${c.wiperModes[st.wipers]}`;
2875
+ case "seatbelt":
2876
+ return `${c.seatbelt}: ${onOff(st.seatbelt)}`;
2877
+ default:
2878
+ return null;
2879
+ }
2880
+ };
2881
+ const gearName = (gear) => gear < 0 ? "R" : gear === 0 ? "N" : String(gear);
2204
2882
  const onChange = (change) => {
2205
2883
  if (change.kind === "layer") {
2206
2884
  if (change.id === "scoreboard" && board) board.mesh.visible = !!host?.bench?.layer("scoreboard");
2207
2885
  return refreshMirrors();
2208
2886
  }
2209
2887
  if (change.kind === "action") {
2888
+ if (change.id === "drive") return driving ? stopDriving() : startDriving();
2889
+ if (change.id === "run_test" || change.id === "run_all" || change.id === "reverse_park") stopDriving();
2210
2890
  if (change.id === "run_test") {
2211
2891
  queue = [];
2212
2892
  startTest(settings.test);
@@ -2237,6 +2917,7 @@ function createProvingModule(options) {
2237
2917
  switch (change.id) {
2238
2918
  case "vehicle":
2239
2919
  settings.vehicle = String(change.value);
2920
+ stopDriving();
2240
2921
  run = null;
2241
2922
  queue = [];
2242
2923
  parking = null;
@@ -2254,6 +2935,10 @@ function createProvingModule(options) {
2254
2935
  case "time_scale":
2255
2936
  settings.timeScale = Number(change.value);
2256
2937
  break;
2938
+ case "gearbox":
2939
+ gearbox = change.value === "manual" ? "manual" : "automatic";
2940
+ if (driving && driving.controls.state.gearbox !== gearbox) driving.controls.command("gearbox");
2941
+ break;
2257
2942
  case "visual_check":
2258
2943
  settings.visual = String(change.value);
2259
2944
  break;
@@ -2277,6 +2962,13 @@ function createProvingModule(options) {
2277
2962
  speed = s.u;
2278
2963
  steer = s.steerAngle;
2279
2964
  if (run.done) finishTest();
2965
+ } else if (driving) {
2966
+ const d = driving;
2967
+ d.step(dt);
2968
+ if (!driving) return;
2969
+ pose(d.driver.state, d.driver.state.steerAngle);
2970
+ speed = d.driver.state.u;
2971
+ steer = d.driver.state.steerAngle;
2280
2972
  } else if (parking) {
2281
2973
  parking.time += dt;
2282
2974
  const p = parking;
@@ -2286,7 +2978,7 @@ function createProvingModule(options) {
2286
2978
  steer = p.driver.state.steerAngle;
2287
2979
  if (p.time > 9) parking = null;
2288
2980
  }
2289
- if (car) engine?.update({ dt, position: [car.built.group.position.x, 0.5, car.built.group.position.z], speed, throttle: run ? 1 : 0 });
2981
+ 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 });
2290
2982
  const f = frameOf(speed);
2291
2983
  if (f && rig.vehicle && settings.camera !== "room") rig.vehicle.update(rig.camera, f, dt);
2292
2984
  if (f && rig.vehicle) for (const m of rig.mirrors) rig.vehicle.placeMirror(m.camera, m.pose, f);
@@ -2296,17 +2988,25 @@ function createProvingModule(options) {
2296
2988
  mirrorKey = key;
2297
2989
  refreshMirrors();
2298
2990
  }
2299
- if (run && Math.floor(run.time * 2) !== Math.floor((run.time - dt * settings.timeScale) * 2)) drawBoard();
2991
+ const geared = run?.driver.state ?? driving?.driver.state;
2992
+ if (geared && geared.gear !== lastGear) {
2993
+ if (lastGear !== null && geared.gear > 0) host.log(fillText(text.log.shift, gearName(lastGear), gearName(geared.gear), Math.round(Math.abs(geared.u) * 3.6)), { from: lastGear, to: geared.gear, kmh: +(Math.abs(geared.u) * 3.6).toFixed(1), rpm: Math.round(geared.rpm) }, "proving_shift");
2994
+ lastGear = geared.gear;
2995
+ drawBoard();
2996
+ } else if (!geared) lastGear = null;
2997
+ if (run && Math.floor(run.time * 2) !== Math.floor((run.time - dt * settings.timeScale) * 2) || driving && Math.floor(driveClock * 2) !== Math.floor((driveClock - dt) * 2)) drawBoard();
2998
+ if (driving) driveClock += dt;
2300
2999
  };
2301
- const speedNow = () => Math.abs(run?.driver.state.u ?? parking?.driver.state.u ?? 0) * 3.6;
3000
+ const speedNow = () => Math.abs(run?.driver.state.u ?? driving?.driver.state.u ?? parking?.driver.state.u ?? 0) * 3.6;
2302
3001
  const counts = (verdict) => certificateOf(settings.vehicle)?.results.filter((r) => r.verdict === verdict).length ?? 0;
2303
3002
  const teardown = () => {
3003
+ stopDriving();
2304
3004
  stopEngine();
2305
3005
  for (const m of rig.mirrors) m.remove();
2306
3006
  rig.mirrors = [];
2307
3007
  removeCar();
2308
3008
  mirrorKey = "";
2309
- if (track) discardObject3D2(track);
3009
+ if (track) discardObject3D3(track);
2310
3010
  track = null;
2311
3011
  strip = null;
2312
3012
  board = null;
@@ -2332,6 +3032,8 @@ function createProvingModule(options) {
2332
3032
  { kind: "choice", id: "camera", name: text.controls.camera, hint: text.hints.camera, options: ["room", "chase", "cockpit", "hood"].map((id) => ({ id, name: text.cameras[id] })), default: settings.camera },
2333
3033
  { kind: "number", id: "time_scale", name: text.controls.time_scale, hint: text.hints.time_scale, min: 1, max: 20, step: 1, default: settings.timeScale },
2334
3034
  { kind: "choice", id: "visual_check", name: text.controls.visual_check, hint: text.hints.visual_check, options: VISUAL_CHECKS.map((id) => ({ id, name: text.tests[id] ?? id })), default: settings.visual },
3035
+ { kind: "action", id: "drive", name: text.actions.drive },
3036
+ { kind: "choice", id: "gearbox", name: text.controls.gearbox, hint: text.hints.gearbox, options: ["automatic", "manual"].map((id) => ({ id, name: text.gearbox[id] })), default: gearbox },
2335
3037
  { kind: "action", id: "run_test", name: text.actions.run_test },
2336
3038
  { kind: "action", id: "run_all", name: text.actions.run_all },
2337
3039
  { kind: "action", id: "certify_now", name: text.actions.certify_now },
@@ -2347,6 +3049,8 @@ function createProvingModule(options) {
2347
3049
  ],
2348
3050
  metrics: () => [
2349
3051
  { id: "speed", name: text.metrics.speed, unit: "km/h", digits: 0, read: speedNow },
3052
+ { id: "gear", name: text.metrics.gear, read: () => driving?.driver.state.gear ?? run?.driver.state.gear ?? 0 },
3053
+ { id: "rpm", name: text.metrics.rpm, unit: "rpm", digits: 0, read: () => driving?.driver.state.rpm ?? run?.driver.state.rpm ?? 0 },
2350
3054
  { id: "time", name: text.metrics.time, unit: "s", digits: 1, read: () => run?.time ?? 0 },
2351
3055
  { id: "value", name: text.metrics.value, digits: 1, read: () => run ? run.trace[run.trace.length - 1]?.[1] ?? 0 : 0 },
2352
3056
  { id: "passed", name: text.metrics.passed, read: () => counts("pass") },
@@ -2361,6 +3065,7 @@ function createProvingModule(options) {
2361
3065
  rig.camera = new three.PerspectiveCamera(60, 1.6, 0.05, 900);
2362
3066
  rig.vehicle = createVehicleCamera(three);
2363
3067
  lent.audio?.register?.(LIBRARY_BANK2);
3068
+ lent.audio?.register?.(CABIN_BANK);
2364
3069
  const stopChange = lent.bench?.onChange(onChange);
2365
3070
  const stopFrame = lent.onFrame(frame);
2366
3071
  const stopClear = lent.onClear?.(() => {
@@ -2421,9 +3126,13 @@ export {
2421
3126
  DEFAULT_TEST_VEHICLES,
2422
3127
  createVehiclesTestModule,
2423
3128
  loadVehiclesTestModule,
3129
+ DASHBOARD_LOOK,
3130
+ createVehicleDashboard,
3131
+ LAMP_RULES,
3132
+ createVehicleLamps,
2424
3133
  PROVING_TEXT,
2425
3134
  PROVING_AREA_ID,
2426
3135
  createProvingModule,
2427
3136
  loadProvingModule
2428
3137
  };
2429
- //# sourceMappingURL=chunk-4SUQMRS3.js.map
3138
+ //# sourceMappingURL=chunk-MRUPKE7J.js.map