@adatechnology/game-vehicles-three 0.5.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.
- package/dist/chunk-MRUPKE7J.js +3138 -0
- package/dist/chunk-MRUPKE7J.js.map +1 -0
- package/dist/index.d.ts +262 -4
- package/dist/index.global.js +5743 -1699
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +51 -3
- package/dist/{testing-DmGv7Gpb.d.ts → testing-C-BgV7gj.d.ts} +170 -9
- package/dist/testing.d.ts +2 -1
- package/dist/testing.js +9 -1
- package/package.json +10 -8
- package/dist/chunk-GAO7ZJP7.js +0 -1456
- package/dist/chunk-GAO7ZJP7.js.map +0 -1
|
@@ -0,0 +1,3138 @@
|
|
|
1
|
+
// src/test-area/module.ts
|
|
2
|
+
import { DEFERRED_PRIORITY, DEFERRED_QUEUE_ID, footprintBlocker } from "@adatechnology/game-contracts";
|
|
3
|
+
import { discardObject3D } from "@adatechnology/game-shapes-three";
|
|
4
|
+
import { buildVehicleItem, crackedGlassMaterial, createCreaseQueue, createVehicleDestructible as createVehicleDestructible2, createVehicleKit } from "@adatechnology/game-vehicles";
|
|
5
|
+
import { isRenderPools } from "@adatechnology/game-render-three";
|
|
6
|
+
import { createArcadeDriver as createArcadeDriver2, createSemiSimDriver as createSemiSimDriver2, createSurfaces as createSurfaces2, vehicleCorners as vehicleCorners2 } from "@adatechnology/game-vehicles-driving";
|
|
7
|
+
import { colorFromSeed, fleetDims, installFleet, paintPalettes, paletteOf } from "@adatechnology/game-vehicles-fleet";
|
|
8
|
+
|
|
9
|
+
// src/build.ts
|
|
10
|
+
import { markKitOrigin } from "@adatechnology/game-contracts";
|
|
11
|
+
import { colorFromSeed as defaultColorFromSeed, fleetDims as defaultFleetDims, paintPalettes as defaultPaintPalettes, paletteOf as defaultPaletteOf } from "@adatechnology/game-vehicles-fleet";
|
|
12
|
+
|
|
13
|
+
// src/lite.ts
|
|
14
|
+
var LITE_CLASSES = ["paint", "trim", "metal", "glass", "light", "plate"];
|
|
15
|
+
var PRUNE_BELOW = 0.03;
|
|
16
|
+
var createAccumulators = () => {
|
|
17
|
+
const acc = {};
|
|
18
|
+
for (const name of LITE_CLASSES) acc[name] = { P: [], N: [], C: [], U: [], I: [], nv: 0, alpha: name === "glass", uv: name === "plate", map: null };
|
|
19
|
+
return acc;
|
|
20
|
+
};
|
|
21
|
+
function classify(mat, mesh, c) {
|
|
22
|
+
if (mat.map) {
|
|
23
|
+
if (mesh.userData.plate) return "plate";
|
|
24
|
+
if (mat.userData && mat.userData.grille || mat.name === "grille" || mat.metalness != null && mat.roughness != null && !mat.transparent) return "trim";
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (mat.transparent && mat.opacity < 0.99) {
|
|
28
|
+
const col = mat.color;
|
|
29
|
+
if (col.r > col.g + col.b * 0.9 && col.r > 0.35) return "light";
|
|
30
|
+
if (col.r > 0.8 && col.g > 0.8 && mat.opacity < 0.3) return null;
|
|
31
|
+
return "glass";
|
|
32
|
+
}
|
|
33
|
+
if (mat.isMeshBasicMaterial) return null;
|
|
34
|
+
const e = mat.emissive;
|
|
35
|
+
const ei = mat.emissiveIntensity == null ? 1 : mat.emissiveIntensity;
|
|
36
|
+
if (e && (e.r + e.g + e.b) / 3 * ei > 0.25 && !mat.metalness) return "light";
|
|
37
|
+
if (e && (e.r + e.g + e.b) / 3 * ei > 0.45) return "light";
|
|
38
|
+
if (mat.metalness >= 0.5) return "metal";
|
|
39
|
+
if (c.paints && c.paints.has(mat)) return "paint";
|
|
40
|
+
if (mat.opacity < 1 && mat.transparent) return null;
|
|
41
|
+
return "trim";
|
|
42
|
+
}
|
|
43
|
+
function classColor(mat, cls, primary, paintColor, out) {
|
|
44
|
+
if (cls === "paint" && paintColor && mat === primary) return out.copy(paintColor);
|
|
45
|
+
if (cls === "light") {
|
|
46
|
+
const e = mat.emissive;
|
|
47
|
+
const ei = mat.emissiveIntensity == null ? 1 : mat.emissiveIntensity;
|
|
48
|
+
if (e && e.r + e.g + e.b > 0.05) {
|
|
49
|
+
out.copy(e).multiplyScalar(Math.max(1, ei) * 1);
|
|
50
|
+
out.r = Math.min(out.r, 1);
|
|
51
|
+
out.g = Math.min(out.g, 1);
|
|
52
|
+
out.b = Math.min(out.b, 1);
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
return out.copy(mat.color);
|
|
56
|
+
}
|
|
57
|
+
if (mat.map) return out.setRGB(0.12, 0.13, 0.14);
|
|
58
|
+
return out.copy(mat.color);
|
|
59
|
+
}
|
|
60
|
+
function liteMaterials(THREE, environment) {
|
|
61
|
+
return {
|
|
62
|
+
paint: new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.42, metalness: 0.3, side: THREE.DoubleSide, envMap: environment, envMapIntensity: 0.35 }),
|
|
63
|
+
trim: new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.78, metalness: 0.08, side: THREE.DoubleSide }),
|
|
64
|
+
metal: new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.32, metalness: 0.85, envMap: environment, envMapIntensity: 1.1, side: THREE.DoubleSide }),
|
|
65
|
+
glass: new THREE.MeshStandardMaterial({ vertexColors: true, transparent: true, depthWrite: false, roughness: 0.06, metalness: 0.3, side: THREE.DoubleSide, envMap: environment, envMapIntensity: 0.9 }),
|
|
66
|
+
light: new THREE.MeshBasicMaterial({ vertexColors: true, side: THREE.DoubleSide }),
|
|
67
|
+
plate: new THREE.MeshStandardMaterial({ roughness: 0.5, metalness: 0.1, side: THREE.DoubleSide })
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function fuse(THREE, c, pivot, paintColor, primary, hidden) {
|
|
71
|
+
const acc = createAccumulators();
|
|
72
|
+
const mw = new THREE.Matrix4();
|
|
73
|
+
const nm = new THREE.Matrix3();
|
|
74
|
+
const v = new THREE.Vector3();
|
|
75
|
+
const col = new THREE.Color();
|
|
76
|
+
const tmp = new THREE.Box3();
|
|
77
|
+
let pruned = 0;
|
|
78
|
+
c.vehicle.updateMatrixWorld(true);
|
|
79
|
+
c.vehicle.traverse((o) => {
|
|
80
|
+
if (!o.isMesh || o.isInstancedMesh || o.isSkinnedMesh || !o.geometry || !o.geometry.attributes.position) return;
|
|
81
|
+
for (let p = o; p; p = p.parent) if (p.visible === false) return;
|
|
82
|
+
if (o.userData && (o.userData.ghost || o.userData.skipLite)) return;
|
|
83
|
+
const mats = Array.isArray(o.material) ? o.material : [o.material];
|
|
84
|
+
const geo = o.geometry;
|
|
85
|
+
mw.multiplyMatrices(pivot, o.matrixWorld);
|
|
86
|
+
nm.getNormalMatrix(mw);
|
|
87
|
+
const flip = mw.determinant() < 0;
|
|
88
|
+
geo.computeBoundingBox();
|
|
89
|
+
tmp.copy(geo.boundingBox).applyMatrix4(mw);
|
|
90
|
+
const ext = Math.max(tmp.max.x - tmp.min.x, tmp.max.y - tmp.min.y, tmp.max.z - tmp.min.z);
|
|
91
|
+
const pa = geo.attributes.position;
|
|
92
|
+
const na = geo.attributes.normal;
|
|
93
|
+
const ua = geo.attributes.uv;
|
|
94
|
+
const ca = geo.attributes.color;
|
|
95
|
+
const ix = geo.index;
|
|
96
|
+
const tot = ix ? ix.count : pa.count;
|
|
97
|
+
const groups = Array.isArray(o.material) && geo.groups.length ? geo.groups : [{ start: 0, count: tot, materialIndex: 0 }];
|
|
98
|
+
groups.forEach((gr) => {
|
|
99
|
+
const mat = mats[gr.materialIndex] || mats[0];
|
|
100
|
+
if (!mat || hidden.has(mat)) return;
|
|
101
|
+
const cls = classify(mat, o, c);
|
|
102
|
+
if (!cls) return;
|
|
103
|
+
if (ext < PRUNE_BELOW && cls !== "plate") {
|
|
104
|
+
pruned++;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const A = acc[cls];
|
|
108
|
+
const a0 = Math.min(gr.start, tot);
|
|
109
|
+
let n0 = Math.min(gr.count, tot - a0);
|
|
110
|
+
n0 -= n0 % 3;
|
|
111
|
+
if (n0 < 3) return;
|
|
112
|
+
const cc = classColor(mat, cls, primary, paintColor, col);
|
|
113
|
+
const alpha = cls === "glass" ? mat.transparent ? mat.opacity : 1 : 1;
|
|
114
|
+
if (cls === "plate" && A.map && mat.map !== A.map) return;
|
|
115
|
+
if (cls === "plate") A.map = mat.map;
|
|
116
|
+
const remap = /* @__PURE__ */ new Map();
|
|
117
|
+
for (let i = 0; i < n0; i++) {
|
|
118
|
+
const vi = ix ? ix.getX(a0 + i) : a0 + i;
|
|
119
|
+
if (remap.has(vi)) continue;
|
|
120
|
+
remap.set(vi, A.nv++);
|
|
121
|
+
v.fromBufferAttribute(pa, vi).applyMatrix4(mw);
|
|
122
|
+
A.P.push(v.x, v.y, v.z);
|
|
123
|
+
if (na) {
|
|
124
|
+
v.fromBufferAttribute(na, vi).applyMatrix3(nm).normalize();
|
|
125
|
+
A.N.push(v.x, v.y, v.z);
|
|
126
|
+
} else A.N.push(0, 1, 0);
|
|
127
|
+
let cr = cc.r;
|
|
128
|
+
let cg = cc.g;
|
|
129
|
+
let cb = cc.b;
|
|
130
|
+
if (ca && cls !== "plate") {
|
|
131
|
+
cr *= ca.getX(vi);
|
|
132
|
+
cg *= ca.getY(vi);
|
|
133
|
+
cb *= ca.getZ(vi);
|
|
134
|
+
}
|
|
135
|
+
if (A.alpha) A.C.push(cr, cg, cb, alpha);
|
|
136
|
+
else A.C.push(cr, cg, cb);
|
|
137
|
+
if (A.uv) A.U.push(ua ? ua.getX(vi) : 0, ua ? ua.getY(vi) : 0);
|
|
138
|
+
}
|
|
139
|
+
for (let t = 0; t < n0; t += 3) {
|
|
140
|
+
const i0 = remap.get(ix ? ix.getX(a0 + t) : a0 + t);
|
|
141
|
+
const i1 = remap.get(ix ? ix.getX(a0 + t + 1) : a0 + t + 1);
|
|
142
|
+
const i2 = remap.get(ix ? ix.getX(a0 + t + 2) : a0 + t + 2);
|
|
143
|
+
if (flip) A.I.push(i0, i2, i1);
|
|
144
|
+
else A.I.push(i0, i1, i2);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
return { acc, pruned };
|
|
149
|
+
}
|
|
150
|
+
function liteMeshes(THREE, fusion, materials) {
|
|
151
|
+
const meshes = [];
|
|
152
|
+
for (const name of LITE_CLASSES) {
|
|
153
|
+
const A = fusion.acc[name];
|
|
154
|
+
if (!A.I.length) continue;
|
|
155
|
+
const g = new THREE.BufferGeometry();
|
|
156
|
+
g.setAttribute("position", new THREE.Float32BufferAttribute(A.P, 3));
|
|
157
|
+
g.setAttribute("normal", new THREE.Float32BufferAttribute(A.N, 3));
|
|
158
|
+
g.setAttribute("color", new THREE.Float32BufferAttribute(A.C, A.alpha ? 4 : 3));
|
|
159
|
+
if (A.uv) g.setAttribute("uv", new THREE.Float32BufferAttribute(A.U, 2));
|
|
160
|
+
g.setIndex(A.nv > 65535 ? new THREE.Uint32BufferAttribute(A.I, 1) : new THREE.Uint16BufferAttribute(A.I, 1));
|
|
161
|
+
g.computeBoundingBox();
|
|
162
|
+
g.computeBoundingSphere();
|
|
163
|
+
let mt = materials[name];
|
|
164
|
+
if (name === "plate") {
|
|
165
|
+
mt = mt.clone();
|
|
166
|
+
mt.map = A.map;
|
|
167
|
+
if (mt.map) mt.map.needsUpdate = true;
|
|
168
|
+
}
|
|
169
|
+
const mesh = new THREE.Mesh(g, mt);
|
|
170
|
+
mesh.name = name;
|
|
171
|
+
mesh.castShadow = name !== "glass" && name !== "light";
|
|
172
|
+
mesh.receiveShadow = name !== "light";
|
|
173
|
+
meshes.push(mesh);
|
|
174
|
+
}
|
|
175
|
+
return meshes;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/build.ts
|
|
179
|
+
function count(root) {
|
|
180
|
+
let triangles = 0;
|
|
181
|
+
let drawCalls = 0;
|
|
182
|
+
root.traverse((o) => {
|
|
183
|
+
if (!o.isMesh || !o.geometry) return;
|
|
184
|
+
for (let p = o; p; p = p.parent) if (p.visible === false) return;
|
|
185
|
+
const g = o.geometry;
|
|
186
|
+
const n = g.index ? g.index.count : g.attributes.position ? g.attributes.position.count : 0;
|
|
187
|
+
if (Array.isArray(o.material) && g.groups.length) drawCalls += g.groups.length;
|
|
188
|
+
else drawCalls++;
|
|
189
|
+
triangles += n / 3;
|
|
190
|
+
});
|
|
191
|
+
return { triangles: Math.round(triangles), drawCalls };
|
|
192
|
+
}
|
|
193
|
+
function primaryPaint(c) {
|
|
194
|
+
let best = null;
|
|
195
|
+
let bestCount = -1;
|
|
196
|
+
const counts = /* @__PURE__ */ new Map();
|
|
197
|
+
c.vehicle.traverse((o) => {
|
|
198
|
+
if (!o.isMesh || !o.geometry) return;
|
|
199
|
+
const mats = Array.isArray(o.material) ? o.material : [o.material];
|
|
200
|
+
const g = o.geometry;
|
|
201
|
+
const n = (g.index ? g.index.count : g.attributes.position.count) / 3;
|
|
202
|
+
for (const m of mats) if (c.paints.has(m)) counts.set(m, (counts.get(m) || 0) + n);
|
|
203
|
+
});
|
|
204
|
+
counts.forEach((n, m) => {
|
|
205
|
+
if (n > bestCount) {
|
|
206
|
+
bestCount = n;
|
|
207
|
+
best = m;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
return best;
|
|
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
|
+
}
|
|
222
|
+
function createVehiclesThree(THREE, options) {
|
|
223
|
+
const kit = options.kit;
|
|
224
|
+
const fleet = options.fleet ?? { colorFromSeed: defaultColorFromSeed, fleetDims: defaultFleetDims, paintPalettes: defaultPaintPalettes, paletteOf: defaultPaletteOf };
|
|
225
|
+
const { colorFromSeed: colorFromSeed2, fleetDims: fleetDims2, paintPalettes: paintPalettes2, paletteOf: paletteOf2 } = fleet;
|
|
226
|
+
const groupOf = options.groupOf || ((key) => kit.find(key)?.group === kit.config.defaultGroup ? "cars" : String(kit.find(key)?.group));
|
|
227
|
+
const militaryKeys = /* @__PURE__ */ new Set();
|
|
228
|
+
const isMilitary = (key) => militaryKeys.has(key) || paletteOf2(groupOf(key)) === paintPalettes2.military;
|
|
229
|
+
const build = (key, o = {}) => {
|
|
230
|
+
const quality = o.quality === "lite" ? "lite" : "high";
|
|
231
|
+
const spec = kit.find(key);
|
|
232
|
+
if (!spec) throw new Error(`createVehiclesThree: unknown vehicle "${key}"`);
|
|
233
|
+
const seed = o.seed == null ? null : o.seed;
|
|
234
|
+
const color = o.color || (seed != null ? colorFromSeed2(seed, isMilitary(key) ? paintPalettes2.military : paintPalettes2.civil) : null);
|
|
235
|
+
const c = kit.build(key, o.state || "intact", void 0, { lod: quality === "lite" });
|
|
236
|
+
c.root.updateMatrixWorld(true);
|
|
237
|
+
const primary = primaryPaint(c);
|
|
238
|
+
const rotation = new THREE.Matrix4().makeRotationY(-Math.PI / 2);
|
|
239
|
+
const bounds = new THREE.Box3();
|
|
240
|
+
const piece = new THREE.Box3();
|
|
241
|
+
const turned = new THREE.Matrix4();
|
|
242
|
+
c.vehicle.traverse((m) => {
|
|
243
|
+
if (!m.isMesh || !m.geometry || m.userData.ghost) return;
|
|
244
|
+
for (let p = m; p; p = p.parent) if (p.visible === false) return;
|
|
245
|
+
m.geometry.computeBoundingBox();
|
|
246
|
+
piece.copy(m.geometry.boundingBox).applyMatrix4(turned.multiplyMatrices(rotation, m.matrixWorld));
|
|
247
|
+
bounds.union(piece);
|
|
248
|
+
});
|
|
249
|
+
const cx = (bounds.min.x + bounds.max.x) / 2;
|
|
250
|
+
const cz = (bounds.min.z + bounds.max.z) / 2;
|
|
251
|
+
const pivotMatrix = new THREE.Matrix4().makeTranslation(-cx, -bounds.min.y, -cz).multiply(rotation);
|
|
252
|
+
const group = new THREE.Group();
|
|
253
|
+
group.name = `vehicle:${key}`;
|
|
254
|
+
markKitOrigin(group, { owner: "game-vehicles-three", kind: "vehicle", id: key });
|
|
255
|
+
const damageables = kit.damageables(c, pivotMatrix);
|
|
256
|
+
let pruned = 0;
|
|
257
|
+
let driverEye = null;
|
|
258
|
+
if (quality === "high") {
|
|
259
|
+
const pivot = new THREE.Group();
|
|
260
|
+
pivot.name = "vehicle";
|
|
261
|
+
pivot.matrixAutoUpdate = false;
|
|
262
|
+
pivot.matrix.copy(pivotMatrix);
|
|
263
|
+
pivot.matrixWorldNeedsUpdate = true;
|
|
264
|
+
c.vehicle.matrixAutoUpdate = true;
|
|
265
|
+
pivot.add(c.vehicle);
|
|
266
|
+
group.add(pivot);
|
|
267
|
+
c.root = group;
|
|
268
|
+
if (color && primary) {
|
|
269
|
+
const repainted = primary.clone();
|
|
270
|
+
repainted.color.set(color);
|
|
271
|
+
c.vehicle.traverse((m) => {
|
|
272
|
+
if (!m.isMesh) return;
|
|
273
|
+
if (Array.isArray(m.material)) m.material = m.material.map((x) => x === primary ? repainted : x);
|
|
274
|
+
else if (m.material === primary) m.material = repainted;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
markDamageables(c, damageables);
|
|
278
|
+
group.updateMatrixWorld(true);
|
|
279
|
+
driverEye = eyeOf(THREE, c);
|
|
280
|
+
} else {
|
|
281
|
+
const fusion = fuse(THREE, c, pivotMatrix, color ? new THREE.Color(color) : null, primary, kit.lodHiddenMaterials());
|
|
282
|
+
for (const mesh of liteMeshes(THREE, fusion, liteMaterials(THREE, kit.studioEnvironment()))) group.add(mesh);
|
|
283
|
+
pruned = fusion.pruned;
|
|
284
|
+
}
|
|
285
|
+
const size = new THREE.Box3().setFromObject(group).getSize(new THREE.Vector3());
|
|
286
|
+
const counted = count(group);
|
|
287
|
+
group.userData.damageables = damageables;
|
|
288
|
+
group.userData.key = key;
|
|
289
|
+
group.userData.quality = quality;
|
|
290
|
+
return {
|
|
291
|
+
key,
|
|
292
|
+
quality,
|
|
293
|
+
color,
|
|
294
|
+
seed,
|
|
295
|
+
group,
|
|
296
|
+
damageables,
|
|
297
|
+
triangles: counted.triangles,
|
|
298
|
+
drawCalls: counted.drawCalls,
|
|
299
|
+
length: +size.z.toFixed(3),
|
|
300
|
+
width: +size.x.toFixed(3),
|
|
301
|
+
height: +size.y.toFixed(3),
|
|
302
|
+
pruned,
|
|
303
|
+
expected: fleetDims2(key),
|
|
304
|
+
context: quality === "high" ? c : null,
|
|
305
|
+
driverEye,
|
|
306
|
+
dispose() {
|
|
307
|
+
group.traverse((m) => {
|
|
308
|
+
if (m.geometry) m.geometry.dispose();
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
};
|
|
313
|
+
return { THREE, kit, build };
|
|
314
|
+
}
|
|
315
|
+
function markDamageables(c, damageables) {
|
|
316
|
+
const byZone = {};
|
|
317
|
+
for (const d of damageables) byZone[d.zone] = d;
|
|
318
|
+
for (const z of c.zones || []) {
|
|
319
|
+
const d = byZone[z.id];
|
|
320
|
+
if (!d) continue;
|
|
321
|
+
for (const o of z.objs || z.meshes || []) if (o && o.userData) o.userData.damageable = { material: d.material, zone: d.zone };
|
|
322
|
+
}
|
|
323
|
+
for (const w of c.wheels || []) {
|
|
324
|
+
const z = (c.zones || []).find((zz) => zz.type === "wheel" && zz.objs && zz.objs.indexOf(w) >= 0);
|
|
325
|
+
w.userData.damageable = { material: "rubber", zone: z ? z.id : "wheel" };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/vehicle-lod.ts
|
|
330
|
+
import { markKitOrigin as markKitOrigin2 } from "@adatechnology/game-contracts";
|
|
331
|
+
import { createStaticBatcher } from "@adatechnology/game-render-three";
|
|
332
|
+
var VEHICLE_LOD_DISTANCE = 14;
|
|
333
|
+
var BATCH_BUDGET_MS = 2;
|
|
334
|
+
var VEHICLE_REBATCH_AFTER_MS = 1500;
|
|
335
|
+
var isShown = (object, top) => {
|
|
336
|
+
for (let node = object; node && node !== top; node = node.parent) if (node.visible === false) return false;
|
|
337
|
+
return true;
|
|
338
|
+
};
|
|
339
|
+
function createVehicleLod(kit, context, root, full, options = {}) {
|
|
340
|
+
const THREE = kit.THREE;
|
|
341
|
+
const now = options.now ?? (() => performance.now());
|
|
342
|
+
let changedAt = Number.NEGATIVE_INFINITY;
|
|
343
|
+
let isUploaded = false;
|
|
344
|
+
let modelKey = "";
|
|
345
|
+
const culled = /* @__PURE__ */ new Map();
|
|
346
|
+
const keyOf = () => full.children.filter((child) => child.name !== "static-batches").map((child) => child.uuid).join();
|
|
347
|
+
const armUpload = () => {
|
|
348
|
+
for (const [mesh, wasCulled] of culled) mesh.frustumCulled = wasCulled;
|
|
349
|
+
culled.clear();
|
|
350
|
+
modelKey = keyOf();
|
|
351
|
+
isUploaded = false;
|
|
352
|
+
full.traverse((object) => {
|
|
353
|
+
if (!object.isMesh || culled.has(object)) return;
|
|
354
|
+
culled.set(object, object.frustumCulled);
|
|
355
|
+
object.frustumCulled = false;
|
|
356
|
+
});
|
|
357
|
+
const probe = [...culled.keys()][0];
|
|
358
|
+
if (!probe) {
|
|
359
|
+
isUploaded = true;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const before = probe.onAfterRender;
|
|
363
|
+
probe.onAfterRender = function(...args) {
|
|
364
|
+
isUploaded = true;
|
|
365
|
+
probe.onAfterRender = before;
|
|
366
|
+
before?.apply(this, args);
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
armUpload();
|
|
370
|
+
let lite = null;
|
|
371
|
+
let isFused = false;
|
|
372
|
+
let isLocked = false;
|
|
373
|
+
let batcher = null;
|
|
374
|
+
let shadow = null;
|
|
375
|
+
const castsShadow = /* @__PURE__ */ new Set();
|
|
376
|
+
const shadowProxyOf = (fused) => {
|
|
377
|
+
const positions = [];
|
|
378
|
+
const indices = [];
|
|
379
|
+
for (const mesh of fused.children) {
|
|
380
|
+
if (!mesh.isMesh || !mesh.castShadow) continue;
|
|
381
|
+
const geometry2 = mesh.geometry.clone().applyMatrix4(mesh.matrix);
|
|
382
|
+
const offset = positions.length / 3;
|
|
383
|
+
const position = geometry2.attributes.position;
|
|
384
|
+
for (let at = 0; at < position.count; at++) positions.push(position.getX(at), position.getY(at), position.getZ(at));
|
|
385
|
+
const index = geometry2.index ? Array.from(geometry2.index.array) : Array.from({ length: position.count }, (_, at) => at);
|
|
386
|
+
for (const vertex of index) indices.push(vertex + offset);
|
|
387
|
+
geometry2.dispose();
|
|
388
|
+
}
|
|
389
|
+
const geometry = new THREE.BufferGeometry();
|
|
390
|
+
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
|
391
|
+
geometry.setIndex(indices);
|
|
392
|
+
geometry.computeBoundingSphere();
|
|
393
|
+
const proxy = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ colorWrite: false, depthWrite: false }));
|
|
394
|
+
proxy.name = "vehicle-shadow";
|
|
395
|
+
markKitOrigin2(proxy, { owner: "game-vehicles-three", kind: "vehicle", id: root.name });
|
|
396
|
+
proxy.castShadow = true;
|
|
397
|
+
proxy.userData.skipLite = true;
|
|
398
|
+
proxy.raycast = () => void 0;
|
|
399
|
+
root.add(proxy);
|
|
400
|
+
return proxy;
|
|
401
|
+
};
|
|
402
|
+
const batchLocked = () => {
|
|
403
|
+
batcher = createStaticBatcher(THREE, full, { budgetMs: BATCH_BUDGET_MS });
|
|
404
|
+
batcher.group.traverse((object) => object.userData.skipLite = true);
|
|
405
|
+
batcher.group.userData.noSurfaceRay = true;
|
|
406
|
+
markKitOrigin2(batcher.group, { owner: "game-vehicles-three", kind: "vehicle", id: root.name });
|
|
407
|
+
};
|
|
408
|
+
const drawFull = () => {
|
|
409
|
+
if (!batcher && isLocked) {
|
|
410
|
+
if (now() - changedAt >= VEHICLE_REBATCH_AFTER_MS) batchLocked();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (!batcher) {
|
|
414
|
+
lite ?? (lite = build());
|
|
415
|
+
lite.visible = isFused;
|
|
416
|
+
shadow ?? (shadow = shadowProxyOf(lite));
|
|
417
|
+
shadow.visible = !isFused;
|
|
418
|
+
full.traverse((object) => {
|
|
419
|
+
if (!object.isMesh || !object.castShadow) return;
|
|
420
|
+
castsShadow.add(object);
|
|
421
|
+
object.castShadow = false;
|
|
422
|
+
});
|
|
423
|
+
batcher = createStaticBatcher(THREE, full, { budgetMs: BATCH_BUDGET_MS });
|
|
424
|
+
batcher.group.traverse((object) => object.userData.skipLite = true);
|
|
425
|
+
batcher.group.userData.noSurfaceRay = true;
|
|
426
|
+
markKitOrigin2(batcher.group, { owner: "game-vehicles-three", kind: "vehicle", id: root.name });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (!batcher) return;
|
|
430
|
+
if (batcher.update() > 0) changedAt = now();
|
|
431
|
+
if (batcher.stats().batched === 0 && batcher.stats().released > 0) {
|
|
432
|
+
batcher.dispose();
|
|
433
|
+
batcher = null;
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
const build = () => {
|
|
437
|
+
root.updateMatrixWorld(true);
|
|
438
|
+
const inverse = new THREE.Matrix4().copy(root.matrixWorld).invert();
|
|
439
|
+
const textured = [];
|
|
440
|
+
full.traverse((object) => {
|
|
441
|
+
if (!object.isMesh || object.userData.skipLite || !isShown(object, full)) return;
|
|
442
|
+
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
|
443
|
+
if (materials.some((material) => material?.map && !/plate/i.test(material.name ?? "")) && !/plate/i.test(object.name ?? "")) textured.push(object);
|
|
444
|
+
});
|
|
445
|
+
for (const mesh of textured) mesh.userData.skipLite = true;
|
|
446
|
+
const fusion = fuse(THREE, { ...context, vehicle: full }, inverse, null, primaryPaint(context), kit.lodHiddenMaterials());
|
|
447
|
+
for (const mesh of textured) delete mesh.userData.skipLite;
|
|
448
|
+
const group = new THREE.Group();
|
|
449
|
+
group.name = "vehicle-lite";
|
|
450
|
+
markKitOrigin2(group, { owner: "game-vehicles-three", kind: "vehicle", id: root.name });
|
|
451
|
+
for (const mesh of liteMeshes(THREE, fusion, liteMaterials(THREE, kit.studioEnvironment()))) group.add(mesh);
|
|
452
|
+
for (const mesh of textured) {
|
|
453
|
+
const copy = new THREE.Mesh(mesh.geometry, mesh.material);
|
|
454
|
+
copy.matrixAutoUpdate = false;
|
|
455
|
+
copy.matrix.multiplyMatrices(inverse, mesh.matrixWorld);
|
|
456
|
+
copy.castShadow = mesh.castShadow;
|
|
457
|
+
copy.receiveShadow = mesh.receiveShadow;
|
|
458
|
+
group.add(copy);
|
|
459
|
+
}
|
|
460
|
+
root.add(group);
|
|
461
|
+
return group;
|
|
462
|
+
};
|
|
463
|
+
const show = (shouldFuse) => {
|
|
464
|
+
if (shouldFuse === isFused) return;
|
|
465
|
+
isFused = shouldFuse;
|
|
466
|
+
full.visible = !shouldFuse;
|
|
467
|
+
if (lite) lite.visible = shouldFuse;
|
|
468
|
+
if (shadow) shadow.visible = !shouldFuse;
|
|
469
|
+
};
|
|
470
|
+
const api = {
|
|
471
|
+
update(distance, touched, posed = false) {
|
|
472
|
+
if (touched && !isLocked) changedAt = now();
|
|
473
|
+
isLocked || (isLocked = touched);
|
|
474
|
+
if (keyOf() !== modelKey) {
|
|
475
|
+
changedAt = now();
|
|
476
|
+
api.dispose();
|
|
477
|
+
armUpload();
|
|
478
|
+
}
|
|
479
|
+
if (!isUploaded) return show(false);
|
|
480
|
+
if (culled.size > 0) {
|
|
481
|
+
for (const [mesh, wasCulled] of culled) mesh.frustumCulled = wasCulled;
|
|
482
|
+
culled.clear();
|
|
483
|
+
}
|
|
484
|
+
const shouldFuse = !isLocked && !posed && distance > (isFused ? VEHICLE_LOD_DISTANCE * 0.9 : VEHICLE_LOD_DISTANCE);
|
|
485
|
+
if (shouldFuse && !lite) lite = build();
|
|
486
|
+
show(shouldFuse);
|
|
487
|
+
if (!shouldFuse) drawFull();
|
|
488
|
+
},
|
|
489
|
+
get fused() {
|
|
490
|
+
return isFused;
|
|
491
|
+
},
|
|
492
|
+
dispose() {
|
|
493
|
+
show(false);
|
|
494
|
+
batcher?.dispose();
|
|
495
|
+
batcher = null;
|
|
496
|
+
for (const mesh of castsShadow) mesh.castShadow = true;
|
|
497
|
+
castsShadow.clear();
|
|
498
|
+
shadow?.removeFromParent();
|
|
499
|
+
shadow?.geometry.dispose();
|
|
500
|
+
shadow = null;
|
|
501
|
+
if (!lite) return;
|
|
502
|
+
lite.removeFromParent();
|
|
503
|
+
lite.traverse((object) => object.geometry?.dispose());
|
|
504
|
+
lite = null;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
return api;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/cabin-smoke.ts
|
|
511
|
+
import { cabinEnclosure } from "@adatechnology/game-vehicles";
|
|
512
|
+
function createCabinSmoke() {
|
|
513
|
+
const tracked = /* @__PURE__ */ new Map();
|
|
514
|
+
const end = (id, entry, effects) => {
|
|
515
|
+
if (entry.declared) effects?.enclose?.(id, null);
|
|
516
|
+
tracked.delete(id);
|
|
517
|
+
};
|
|
518
|
+
return {
|
|
519
|
+
track(id, source, frame) {
|
|
520
|
+
tracked.set(id, { source, frame, declared: false, placed: false });
|
|
521
|
+
},
|
|
522
|
+
untrack(id, effects) {
|
|
523
|
+
const entry = tracked.get(id);
|
|
524
|
+
if (entry) end(id, entry, effects);
|
|
525
|
+
},
|
|
526
|
+
sync(effects) {
|
|
527
|
+
if (!effects?.enclose) return;
|
|
528
|
+
for (const [id, entry] of tracked) {
|
|
529
|
+
if (!entry.frame.parent) {
|
|
530
|
+
if (entry.placed) end(id, entry, effects);
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
entry.placed = true;
|
|
534
|
+
const enclosure = cabinEnclosure(entry.source, { frame: entry.frame });
|
|
535
|
+
if (enclosure || entry.declared) effects.enclose(id, enclosure);
|
|
536
|
+
entry.declared = enclosure !== null;
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
clear(effects) {
|
|
540
|
+
for (const [id, entry] of [...tracked]) end(id, entry, effects);
|
|
541
|
+
},
|
|
542
|
+
ids: () => [...tracked.keys()]
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/test-area/lane-damage.ts
|
|
547
|
+
import { crashVehicle, createVehicleDestructible } from "@adatechnology/game-vehicles";
|
|
548
|
+
import { conditionFromDamage } from "@adatechnology/game-vehicles-driving";
|
|
549
|
+
var LANE_DAMAGE = ["none", "into_wall", "flat_tyre", "torn_tyre", "run_flat", "no_brake_fluid"];
|
|
550
|
+
var RIFLE = { energy: 3500, caliber: 7.8, penetration: { steel: 18 } };
|
|
551
|
+
var TEAR_OFF_M = 5e3;
|
|
552
|
+
var LANE_CAR_KG = 1150;
|
|
553
|
+
var LANE_WALL_CONTACT = 1.6;
|
|
554
|
+
var WALL_REACH = 0.15;
|
|
555
|
+
var DRAIN_S = 600;
|
|
556
|
+
function createLaneDamage(params) {
|
|
557
|
+
const d = createVehicleDestructible(params.kit, params.context, { id: "lane", frame: params.frame, armour: params.damage === "run_flat" ? "IIIA" : null });
|
|
558
|
+
const part = (prefix) => d.describe().parts.find((p) => p.id.startsWith(prefix))?.id;
|
|
559
|
+
const shoot = (id) => {
|
|
560
|
+
if (id) d.hit({ type: "ballistic", point: d.partWorldCenter(id), part: id, direction: [0, 0, 1], ...RIFLE });
|
|
561
|
+
};
|
|
562
|
+
if (params.damage === "into_wall") {
|
|
563
|
+
} else if (params.damage === "no_brake_fluid") {
|
|
564
|
+
shoot(part("brakes"));
|
|
565
|
+
for (let t = 0; t < DRAIN_S; t++) d.update?.(1);
|
|
566
|
+
} else {
|
|
567
|
+
shoot(part("tyre_front_left"));
|
|
568
|
+
if (params.damage === "torn_tyre") d.rollFlat(TEAR_OFF_M, 8);
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
step({ dt, speed, throttle, driver }) {
|
|
572
|
+
d.rollFlat(speed * dt, speed);
|
|
573
|
+
d.runEngine(dt, throttle);
|
|
574
|
+
const events = d.update?.(dt) ?? [];
|
|
575
|
+
driver.condition = conditionFromDamage(driver.spec, d.driveReport());
|
|
576
|
+
return events.flatMap((e) => e.type === "debris" && e.kind === "sparks" ? [e.point] : []);
|
|
577
|
+
},
|
|
578
|
+
settle(dt) {
|
|
579
|
+
d.update?.(dt);
|
|
580
|
+
},
|
|
581
|
+
crash({ speed, direction }) {
|
|
582
|
+
const front = part("bumper_front");
|
|
583
|
+
if (!front) return [];
|
|
584
|
+
const at = d.partWorldCenter(front);
|
|
585
|
+
return crashVehicle(d, { point: [at[0] + direction[0] * WALL_REACH, at[1], at[2] + direction[2] * WALL_REACH], direction, speed, massKg: LANE_CAR_KG, otherMassKg: Infinity, contactWidth: LANE_WALL_CONTACT }).events;
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/test-area/scenarios.ts
|
|
591
|
+
import { createArcadeDriver, createSemiSimDriver, createSurfaces, vehicleCorners } from "@adatechnology/game-vehicles-driving";
|
|
592
|
+
|
|
593
|
+
// src/test-area/text.ts
|
|
594
|
+
var scenarioIds = [
|
|
595
|
+
"vehicles_9mm_windshield",
|
|
596
|
+
"vehicles_shotgun_door",
|
|
597
|
+
"vehicles_ram_front",
|
|
598
|
+
"vehicles_engine_fire",
|
|
599
|
+
"vehicles_fuel_tank",
|
|
600
|
+
"vehicles_unbolt_wheel",
|
|
601
|
+
"vehicles_tear_bumper",
|
|
602
|
+
"vehicles_break_mirror",
|
|
603
|
+
"vehicles_kill_battery",
|
|
604
|
+
"vehicles_rollover",
|
|
605
|
+
"vehicles_melt",
|
|
606
|
+
"vehicles_burnt_wreck",
|
|
607
|
+
"vehicles_drive_arcade",
|
|
608
|
+
"vehicles_drive_semi"
|
|
609
|
+
];
|
|
610
|
+
var VEHICLE_SCENARIO_IDS = scenarioIds;
|
|
611
|
+
var PAGE_SCENARIO_ALIASES = {
|
|
612
|
+
veiculos_9mm_parabrisa: "vehicles_9mm_windshield",
|
|
613
|
+
veiculos_espingarda_porta: "vehicles_shotgun_door",
|
|
614
|
+
veiculos_bater_frente: "vehicles_ram_front",
|
|
615
|
+
veiculos_fogo_motor: "vehicles_engine_fire",
|
|
616
|
+
veiculos_tanque: "vehicles_fuel_tank",
|
|
617
|
+
veiculos_soltar_roda: "vehicles_unbolt_wheel",
|
|
618
|
+
veiculos_arrancar_parachoque: "vehicles_tear_bumper",
|
|
619
|
+
veiculos_quebrar_retrovisor: "vehicles_break_mirror",
|
|
620
|
+
veiculos_matar_bateria: "vehicles_kill_battery",
|
|
621
|
+
veiculos_capotar: "vehicles_rollover",
|
|
622
|
+
veiculos_derreter: "vehicles_melt"
|
|
623
|
+
};
|
|
624
|
+
var PAGE_ITEM_ALIASES = {
|
|
625
|
+
hatch: "hatch",
|
|
626
|
+
sedan: "sedan",
|
|
627
|
+
picape: "pickup",
|
|
628
|
+
moto: "sport_bike",
|
|
629
|
+
caminhao: "box_truck",
|
|
630
|
+
onibus: "city_bus",
|
|
631
|
+
tanque: "battle_tank",
|
|
632
|
+
helicoptero: "attack_helicopter"
|
|
633
|
+
};
|
|
634
|
+
var TEXT = {
|
|
635
|
+
en: {
|
|
636
|
+
group: "Vehicles",
|
|
637
|
+
name: "Vehicles area",
|
|
638
|
+
items: { hatch: "Compact hatch", sedan: "Midsize sedan", pickup: "Midsize pickup", sport_bike: "Sport bike", box_truck: "Box truck", city_bus: "City bus", battle_tank: "Battle tank", attack_helicopter: "Attack helicopter", sedan_burnt: "Burnt sedan", hatch_burnt: "Burnt hatch" },
|
|
639
|
+
controls: { quality: "Quality", paint_by_seed: "Paint by seed", color_seed: "Colour seed", palette: "Palette", driving: "Driving", surface: "Surface", lane_damage: "Lane damage" },
|
|
640
|
+
hints: {
|
|
641
|
+
quality: "High: the kit at full detail. Light: fewer segments and no interior (the light mode of the fleet); applies to the vehicles placed from now on",
|
|
642
|
+
paint_by_seed: "Off: every vehicle keeps its own paint. On: the body colour comes from the seed and the palette",
|
|
643
|
+
palette: "Auto: the palette of the vehicle group (civil, or military for the army vehicles)",
|
|
644
|
+
driving: "Arcade: the game car, simple and forgiving. Semi: a rigid body with springs, tyre friction, gears and surfaces",
|
|
645
|
+
surface: "The ground under the wheels in the semi mode (grip and aquaplaning)",
|
|
646
|
+
lane_damage: "What the lane car starts with; it drives in the semi mode and the damage model sets its tyres, brakes and engine"
|
|
647
|
+
},
|
|
648
|
+
options: {
|
|
649
|
+
quality: { high: "High", lite: "Light" },
|
|
650
|
+
palette: { auto: "Auto", civil: "Civil", military: "Military" },
|
|
651
|
+
driving: { arcade: "Arcade", semi: "Semi-simulation" },
|
|
652
|
+
surface: { dry_asphalt: "Dry asphalt", wet_asphalt: "Wet asphalt", dirt: "Dirt", grass: "Grass", mud: "Mud", blood: "Blood" },
|
|
653
|
+
lane_damage: { none: "None", into_wall: "Into the wall, no braking", flat_tyre: "Flat tyre (rifle round)", torn_tyre: "Tyre torn off: rim on the road", run_flat: "Armoured, flat tyre (run-flat)", no_brake_fluid: "No brake fluid" }
|
|
654
|
+
},
|
|
655
|
+
actions: { repaint: "Repaint with the seed", drive_lane: "Drive the lane", clear_lane: "Remove the lane car", honk: "Honk the horn" },
|
|
656
|
+
layers: { damageables: "Damageable boxes by material", zones: "Zone boxes by type", axes: "Output frame axes" },
|
|
657
|
+
metrics: { vehicles: "Vehicles", triangles: "Triangles", draw_calls: "Draw calls", size_error_mm: "Size against DIMS", metal: "Metal boxes", glass: "Glass boxes", rubber: "Rubber boxes", fuel: "Fuel boxes", speed: "Lane speed", lane: "Lane distance" },
|
|
658
|
+
words: { yes: "true", no: "false", open: "open", tight: "tight", on: "on", off: "off" },
|
|
659
|
+
lane: { crash: "Lane: the car hit the wall at {0} m/s", end: "Lane: the car stopped {0} m from the wall", damage: "Lane: the crash dented {0} and broke {1}" },
|
|
660
|
+
scenarios: {
|
|
661
|
+
vehicles_9mm_windshield: "Vehicles: 9 mm through the windshield",
|
|
662
|
+
vehicles_shotgun_door: "Vehicles: shotgun on the door",
|
|
663
|
+
vehicles_ram_front: "Vehicles: ram the front",
|
|
664
|
+
vehicles_engine_fire: "Vehicles: fire on the engine",
|
|
665
|
+
vehicles_fuel_tank: "Vehicles: shoot the fuel tank (leaks, ignites, explodes)",
|
|
666
|
+
vehicles_unbolt_wheel: "Vehicles: unbolt the wheel with the wrench",
|
|
667
|
+
vehicles_tear_bumper: "Vehicles: tear off the bumper",
|
|
668
|
+
vehicles_break_mirror: "Vehicles: break the mirror",
|
|
669
|
+
vehicles_kill_battery: "Vehicles: kill the battery (lights go out)",
|
|
670
|
+
vehicles_rollover: "Vehicles: roll over",
|
|
671
|
+
vehicles_melt: "Vehicles: melt (long fire burns the tyres)",
|
|
672
|
+
vehicles_drive_arcade: "Vehicles: arcade driving down a lane that ends in a wall",
|
|
673
|
+
vehicles_drive_semi: "Vehicles: semi-simulation braking on dry, wet and mud",
|
|
674
|
+
vehicles_burnt_wreck: "Vehicles: the burnt sedan comes out of the kit burnt, with the glass gone"
|
|
675
|
+
},
|
|
676
|
+
checks: {
|
|
677
|
+
windshield_holes: "Windshield: {0} hole(s), went through {1:b} (expected: 1, true)",
|
|
678
|
+
windshield_cracks: "Windshield cracked: {0} crack(s), state {1} (expected: damaged)",
|
|
679
|
+
door_marks: "Front left door: {0} hole(s), {1} dent(s), hp {2}/{3} (expected: 2 marks or more)",
|
|
680
|
+
front_crush: "Front crushed: bumper {0} dent(s), hood {1} (expected: 1 or more each)",
|
|
681
|
+
bumper_off: "The bumper came off: {0} (expected: bumper_front)",
|
|
682
|
+
engine_burning: "Engine: burning {0:b}, {1} \xB0C (expected: true, above 250)",
|
|
683
|
+
ignite_lines: "Ignite lines in the log: {0} (expected: 1 or more)",
|
|
684
|
+
tank_leaking: "Tank leaking: {0} leak(s), {1} L left (expected: 1 or more, below 55)",
|
|
685
|
+
ignited: "Ignited: {0} ignite line(s) (expected: 1 or more)",
|
|
686
|
+
exploded: "Exploded: {0} explosion(s) (expected: 1)",
|
|
687
|
+
wheel_wrench: "Wrench on the wheel: bolts {0:open}, {1} group(s), {2} detach line(s) (expected: open, 2 or more, 1)",
|
|
688
|
+
loose_parts: "Loose parts: {0} (expected: bumper_front)",
|
|
689
|
+
mirror_broken: "Left mirror: {0} (expected: broken)",
|
|
690
|
+
battery_before: "Before: lights on and it starts (expected: yes)",
|
|
691
|
+
battery_after: "No battery: lights {0:on}, start {1:b} (expected: off, false)",
|
|
692
|
+
rolled_over: "Rolled over: {0:b} (expected: true)",
|
|
693
|
+
roof: "Roof: hp {0}/{1}, {2} dent(s) (expected: below max, 1 or more)",
|
|
694
|
+
tyre_melt: "Tyre: {0}, melted {1}%, {2} melt line(s) (expected: melted or above 50%)",
|
|
695
|
+
arcade_crash: "Arcade: the wall is hit at {0} m/s (expected: the crash above 4 m/s, at most {1})",
|
|
696
|
+
arcade_bounce: "Arcade: the car bounces back at {0} m/s (expected: below 0)",
|
|
697
|
+
arcade_noise: "Arcade: {0} engine noise(s) and {1} crash noise(s) (expected: 1 or more each)",
|
|
698
|
+
semi_order: "Semi: braking from 80 km/h takes dry {0} m, wet {1} m, mud {2} m (expected: dry shorter than wet shorter than mud)",
|
|
699
|
+
semi_mud: "Semi: a 2WD car on mud {0:on} (expected: bogged on, a bog event)",
|
|
700
|
+
wreck_burnt: "Burnt sedan: burnt flag {0:b} (expected: yes)",
|
|
701
|
+
wreck_glass: "Burnt sedan: {0} glass zone(s), {1} still above 0 (expected: 1 or more, none above 0)"
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
"pt-BR": {
|
|
705
|
+
group: "Ve\xEDculos",
|
|
706
|
+
name: "\xC1rea de ve\xEDculos",
|
|
707
|
+
items: { hatch: "Hatch compacto", sedan: "Sed\xE3 m\xE9dio", pickup: "Picape m\xE9dia", sport_bike: "Moto esportiva", box_truck: "Caminh\xE3o ba\xFA", city_bus: "\xD4nibus urbano", battle_tank: "Tanque de guerra", attack_helicopter: "Helic\xF3ptero de ataque", sedan_burnt: "Sed\xE3 queimado", hatch_burnt: "Hatch queimado" },
|
|
708
|
+
controls: { quality: "Qualidade", paint_by_seed: "Pintar pela semente", color_seed: "Semente da cor", palette: "Paleta", driving: "Dire\xE7\xE3o", surface: "Piso", lane_damage: "Dano na pista" },
|
|
709
|
+
hints: {
|
|
710
|
+
quality: "Alta: o kit com todo o detalhe. Leve: menos segmentos e sem interior (o modo leve da frota); vale para os ve\xEDculos postos daqui em diante",
|
|
711
|
+
paint_by_seed: "Desligado: cada ve\xEDculo mant\xE9m a pr\xF3pria pintura. Ligado: a cor da carroceria vem da semente e da paleta",
|
|
712
|
+
palette: "Auto: a paleta do grupo do ve\xEDculo (civil, ou militar para os do ex\xE9rcito)",
|
|
713
|
+
driving: "Arcade: o carro do jogo, simples e tolerante. Semi: corpo r\xEDgido com molas, atrito do pneu, marchas e pisos",
|
|
714
|
+
surface: "O ch\xE3o sob as rodas no modo semi (ader\xEAncia e aquaplanagem)",
|
|
715
|
+
lane_damage: "Com o que o carro da pista sai; ele anda no modo semi e o modelo de dano define pneus, freio e motor"
|
|
716
|
+
},
|
|
717
|
+
options: {
|
|
718
|
+
quality: { high: "Alta", lite: "Leve" },
|
|
719
|
+
palette: { auto: "Auto", civil: "Civil", military: "Militar" },
|
|
720
|
+
driving: { arcade: "Arcade", semi: "Semissimula\xE7\xE3o" },
|
|
721
|
+
surface: { dry_asphalt: "Asfalto seco", wet_asphalt: "Asfalto molhado", dirt: "Terra", grass: "Grama", mud: "Lama", blood: "Sangue" },
|
|
722
|
+
lane_damage: { none: "Nenhum", into_wall: "Contra o muro, sem frear", flat_tyre: "Pneu furado (tiro de fuzil)", torn_tyre: "Pneu desfeito: aro no ch\xE3o", run_flat: "Blindado, pneu furado (run-flat)", no_brake_fluid: "Sem fluido de freio" }
|
|
723
|
+
},
|
|
724
|
+
actions: { repaint: "Repintar com a semente", drive_lane: "Percorrer a pista", clear_lane: "Tirar o carro da pista", honk: "Buzinar" },
|
|
725
|
+
layers: { damageables: "Caixas danific\xE1veis por material", zones: "Caixas das zonas por tipo", axes: "Eixos do quadro de sa\xEDda" },
|
|
726
|
+
metrics: { vehicles: "Ve\xEDculos", triangles: "Tri\xE2ngulos", draw_calls: "Chamadas de desenho", size_error_mm: "Medida contra DIMS", metal: "Caixas de metal", glass: "Caixas de vidro", rubber: "Caixas de borracha", fuel: "Caixas de combust\xEDvel", speed: "Velocidade na pista", lane: "Dist\xE2ncia na pista" },
|
|
727
|
+
words: { yes: "sim", no: "n\xE3o", open: "soltos", tight: "presos", on: "acesos", off: "apagados" },
|
|
728
|
+
lane: { crash: "Pista: o carro bateu no muro a {0} m/s", end: "Pista: o carro parou a {0} m do muro", damage: "Pista: a batida amassou {0} e quebrou {1}" },
|
|
729
|
+
scenarios: {
|
|
730
|
+
vehicles_9mm_windshield: "Ve\xEDculos: 9 mm atravessa o para-brisa",
|
|
731
|
+
vehicles_shotgun_door: "Ve\xEDculos: espingarda na porta",
|
|
732
|
+
vehicles_ram_front: "Ve\xEDculos: bater de frente",
|
|
733
|
+
vehicles_engine_fire: "Ve\xEDculos: fogo no motor",
|
|
734
|
+
vehicles_fuel_tank: "Ve\xEDculos: atirar no tanque (vaza, pega fogo, explode)",
|
|
735
|
+
vehicles_unbolt_wheel: "Ve\xEDculos: soltar a roda com a chave inglesa",
|
|
736
|
+
vehicles_tear_bumper: "Ve\xEDculos: arrancar o para-choque",
|
|
737
|
+
vehicles_break_mirror: "Ve\xEDculos: quebrar o retrovisor",
|
|
738
|
+
vehicles_kill_battery: "Ve\xEDculos: matar a bateria (far\xF3is apagam)",
|
|
739
|
+
vehicles_rollover: "Ve\xEDculos: capotar",
|
|
740
|
+
vehicles_melt: "Ve\xEDculos: derreter (fogo longo queima os pneus)",
|
|
741
|
+
vehicles_drive_arcade: "Ve\xEDculos: dire\xE7\xE3o arcade numa pista que acaba num muro",
|
|
742
|
+
vehicles_drive_semi: "Ve\xEDculos: frenagem da semissimula\xE7\xE3o no seco, no molhado e na lama",
|
|
743
|
+
vehicles_burnt_wreck: "Ve\xEDculos: o sed\xE3 queimado sai do kit queimado, sem os vidros"
|
|
744
|
+
},
|
|
745
|
+
checks: {
|
|
746
|
+
windshield_holes: "Para-brisa: {0} furo(s), atravessou {1:b} (esperado: 1, sim)",
|
|
747
|
+
windshield_cracks: "Para-brisa trincou: {0} trinca(s), estado {1} (esperado: damaged)",
|
|
748
|
+
door_marks: "Porta dianteira esquerda: {0} furo(s), {1} amassado(s), vida {2}/{3} (esperado: 2 marcas ou mais)",
|
|
749
|
+
front_crush: "Frente amassada: para-choque {0} amassado(s), cap\xF4 {1} (esperado: 1 ou mais cada)",
|
|
750
|
+
bumper_off: "O para-choque soltou: {0} (esperado: bumper_front)",
|
|
751
|
+
engine_burning: "Motor: pegando fogo {0:b}, {1} \xB0C (esperado: sim, acima de 250)",
|
|
752
|
+
ignite_lines: "Linhas de igni\xE7\xE3o no log: {0} (esperado: 1 ou mais)",
|
|
753
|
+
tank_leaking: "Tanque vazando: {0} vazamento(s), {1} L restantes (esperado: 1 ou mais, menos de 55)",
|
|
754
|
+
ignited: "Pegou fogo: {0} linha(s) de igni\xE7\xE3o (esperado: 1 ou mais)",
|
|
755
|
+
exploded: "Explodiu: {0} explos\xE3o (esperado: 1)",
|
|
756
|
+
wheel_wrench: "Chave na roda: parafusos {0:open}, {1} grupo(s), {2} linha(s) de soltura (esperado: soltos, 2 ou mais, 1)",
|
|
757
|
+
loose_parts: "Pe\xE7as soltas: {0} (esperado: bumper_front)",
|
|
758
|
+
mirror_broken: "Retrovisor esquerdo: {0} (esperado: broken)",
|
|
759
|
+
battery_before: "Antes: far\xF3is acesos e d\xE1 partida (esperado: sim)",
|
|
760
|
+
battery_after: "Sem bateria: far\xF3is {0:on}, partida {1:b} (esperado: apagados, n\xE3o)",
|
|
761
|
+
rolled_over: "Capotou: {0:b} (esperado: sim)",
|
|
762
|
+
roof: "Teto: vida {0}/{1}, {2} amassado(s) (esperado: menos que o m\xE1ximo, 1 ou mais)",
|
|
763
|
+
tyre_melt: "Pneu: {0}, derretido {1}%, {2} linha(s) de derretimento (esperado: melted ou mais de 50%)",
|
|
764
|
+
arcade_crash: "Arcade: bate no muro a {0} m/s (esperado: a batida acima de 4 m/s, no m\xE1ximo {1})",
|
|
765
|
+
arcade_bounce: "Arcade: o carro volta a {0} m/s (esperado: abaixo de 0)",
|
|
766
|
+
arcade_noise: "Arcade: {0} ru\xEDdo(s) de motor e {1} de batida (esperado: 1 ou mais de cada)",
|
|
767
|
+
semi_order: "Semi: frear de 80 km/h leva no seco {0} m, no molhado {1} m, na lama {2} m (esperado: seco menor que molhado menor que lama)",
|
|
768
|
+
semi_mud: "Semi: um carro 2WD na lama {0:on} (esperado: atolado, com evento de atolamento)",
|
|
769
|
+
wreck_burnt: "Sed\xE3 queimado: marca de queimado {0:b} (esperado: sim)",
|
|
770
|
+
wreck_glass: "Sed\xE3 queimado: {0} zona(s) de vidro, {1} ainda acima de 0 (esperado: 1 ou mais, nenhuma acima de 0)"
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
function fill(text, locale, template, ...values) {
|
|
775
|
+
return template.replace(/\{(\d+)(?::(b|open|on))?\}/g, (_match, index, kind) => {
|
|
776
|
+
const value = values[Number(index)];
|
|
777
|
+
if (kind === "b") return locale === "en" ? String(value) : value ? text.words.yes : text.words.no;
|
|
778
|
+
if (kind === "open") return value ? text.words.open : text.words.tight;
|
|
779
|
+
if (kind === "on") return value ? text.words.on : text.words.off;
|
|
780
|
+
return String(value);
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/test-area/scenarios.ts
|
|
785
|
+
var add = (p, dx, dy, dz) => [p[0] + dx, p[1] + dy, p[2] + dz];
|
|
786
|
+
var createScenarios = (deps) => {
|
|
787
|
+
const { text, locale } = deps;
|
|
788
|
+
const say = (id, ...values) => fill(text, locale, text.checks[id], ...values);
|
|
789
|
+
const scenario = (id, run) => ({ id, name: text.scenarios[id] ?? id, run });
|
|
790
|
+
const put = (room, item, z = -7) => {
|
|
791
|
+
const spawned = room.spawn(item, [0, 0, z], 0);
|
|
792
|
+
return { id: spawned.objectId, d: spawned.root.userData.destructible };
|
|
793
|
+
};
|
|
794
|
+
const part = (room, v, id) => room.world.part(v.id, id);
|
|
795
|
+
const events = (room, type, v) => room.log.entries({ type, objectId: v.id }).length;
|
|
796
|
+
return [
|
|
797
|
+
scenario("vehicles_9mm_windshield", ({ room, step, expect }) => {
|
|
798
|
+
const v = put(room, "hatch");
|
|
799
|
+
const c = v.d.partWorldCenter("windshield");
|
|
800
|
+
room.setTool("pistol_9mm");
|
|
801
|
+
room.waitUntilReady();
|
|
802
|
+
room.look(add(c, 0, 0.5, 3), c);
|
|
803
|
+
step(0.1);
|
|
804
|
+
room.use();
|
|
805
|
+
step(0.4);
|
|
806
|
+
const w = part(room, v, "windshield");
|
|
807
|
+
expect(w.holes.length >= 1 && !!w.holes[0]?.through, say("windshield_holes", w.holes.length, w.holes[0]?.through));
|
|
808
|
+
expect(w.cracks.length >= 1 && w.status === "damaged", say("windshield_cracks", w.cracks.length, w.status));
|
|
809
|
+
}),
|
|
810
|
+
scenario("vehicles_shotgun_door", ({ room, step, expect }) => {
|
|
811
|
+
const v = put(room, "sedan");
|
|
812
|
+
const c = v.d.partWorldCenter("door_front_left");
|
|
813
|
+
room.setTool("shotgun_pump_12");
|
|
814
|
+
room.waitUntilReady();
|
|
815
|
+
room.look(add(c, 2.5, 0, 0), c);
|
|
816
|
+
step(0.1);
|
|
817
|
+
room.use();
|
|
818
|
+
step(0.4);
|
|
819
|
+
const p = part(room, v, "door_front_left");
|
|
820
|
+
const marks = p.holes.length + p.dents.length;
|
|
821
|
+
expect(marks >= 2 && p.hp < p.maxHp, say("door_marks", p.holes.length, p.dents.length, Math.round(p.hp), p.maxHp));
|
|
822
|
+
}),
|
|
823
|
+
scenario("vehicles_ram_front", ({ room, step, expect }) => {
|
|
824
|
+
const v = put(room, "hatch");
|
|
825
|
+
const c = v.d.partWorldCenter("bumper_front");
|
|
826
|
+
room.world.hit(v.id, { type: "blunt", point: c, direction: [0, 0, -1], energy: 45e3, radius: 0.6, source: "teste" });
|
|
827
|
+
step(2.5);
|
|
828
|
+
const b = part(room, v, "bumper_front");
|
|
829
|
+
const h = part(room, v, "hood");
|
|
830
|
+
expect(b.dents.length >= 1 && h.dents.length >= 1, say("front_crush", b.dents.length, h.dents.length));
|
|
831
|
+
const det = v.d.detachedParts();
|
|
832
|
+
expect(det.indexOf("bumper_front") >= 0, say("bumper_off", det.join(", ")));
|
|
833
|
+
}),
|
|
834
|
+
scenario("vehicles_engine_fire", ({ room, step, expect }) => {
|
|
835
|
+
const v = put(room, "pickup");
|
|
836
|
+
const c = v.d.partWorldCenter("engine");
|
|
837
|
+
for (let i = 0; i < 80; i++) {
|
|
838
|
+
room.world.hit(v.id, { type: "fire", point: c, temperature: 900, radius: 0.5, duration: 0.1, source: "teste" });
|
|
839
|
+
step(0.1);
|
|
840
|
+
}
|
|
841
|
+
const e = part(room, v, "engine");
|
|
842
|
+
expect(e.burning && e.temperature > 250, say("engine_burning", e.burning, Math.round(e.temperature)));
|
|
843
|
+
expect(events(room, "ignite", v) >= 1, say("ignite_lines", events(room, "ignite", v)));
|
|
844
|
+
}),
|
|
845
|
+
scenario("vehicles_fuel_tank", ({ room, step, expect }) => {
|
|
846
|
+
const v = put(room, "hatch");
|
|
847
|
+
const spec = v.d.describe().parts.filter((p) => p.id === "fuel_tank")[0];
|
|
848
|
+
const c = v.d.partWorldCenter("fuel_tank");
|
|
849
|
+
const low = [c[0], c[1] - spec.shape.size[1] / 2 + 0.06, c[2]];
|
|
850
|
+
room.setTool("pistol_9mm");
|
|
851
|
+
room.waitUntilReady();
|
|
852
|
+
room.look(add(low, 0, 0, -2.5), low);
|
|
853
|
+
step(0.1);
|
|
854
|
+
room.use();
|
|
855
|
+
step(1);
|
|
856
|
+
const t = part(room, v, "fuel_tank");
|
|
857
|
+
const nl = v.d.status().leaking.length;
|
|
858
|
+
expect(nl >= 1 && t.liters < 55, say("tank_leaking", nl, t.liters.toFixed(1)));
|
|
859
|
+
for (let i = 0; i < 160 && !events(room, "explode", v); i++) {
|
|
860
|
+
room.world.hit(v.id, { type: "fire", point: c, temperature: 900, radius: 0.6, duration: 0.1, source: "teste" });
|
|
861
|
+
step(0.1);
|
|
862
|
+
}
|
|
863
|
+
expect(events(room, "ignite", v) >= 1, say("ignited", events(room, "ignite", v)));
|
|
864
|
+
step(2);
|
|
865
|
+
expect(events(room, "explode", v) === 1 && v.d.status().exploded, say("exploded", events(room, "explode", v)));
|
|
866
|
+
const vc = v.d.partWorldCenter("engine");
|
|
867
|
+
room.look(add(vc, 4, 0, -3), vc);
|
|
868
|
+
}),
|
|
869
|
+
scenario("vehicles_unbolt_wheel", ({ room, step, expect }) => {
|
|
870
|
+
const v = put(room, "sedan");
|
|
871
|
+
const joint = "wheel_front_left~underbody";
|
|
872
|
+
const c = v.d.partWorldCenter("wheel_front_left");
|
|
873
|
+
room.setTool("wrench");
|
|
874
|
+
for (let k = 0; k < 120 && room.world.disassemble(v.id, joint, "wrench", 0.1) < 1; k++) step(0.1);
|
|
875
|
+
step(1);
|
|
876
|
+
const st = room.world.state(v.id);
|
|
877
|
+
const open = !!st.joints.filter((j) => j.id === joint)[0]?.open;
|
|
878
|
+
expect(open && st.groups.length >= 2 && events(room, "detach", v) >= 1, say("wheel_wrench", open, st.groups.length, events(room, "detach", v)));
|
|
879
|
+
step(2);
|
|
880
|
+
room.look(add(c, 2.2, 0, 0.2), add(c, 0, -0.1, 0));
|
|
881
|
+
}),
|
|
882
|
+
scenario("vehicles_tear_bumper", ({ room, step, expect }) => {
|
|
883
|
+
const v = put(room, "hatch");
|
|
884
|
+
const c = v.d.partWorldCenter("bumper_front");
|
|
885
|
+
room.world.hit(v.id, { type: "blunt", point: c, direction: [0, 0, -1], energy: 3e4, radius: 0.6, source: "teste" });
|
|
886
|
+
step(2.5);
|
|
887
|
+
const det = v.d.detachedParts();
|
|
888
|
+
expect(det.indexOf("bumper_front") >= 0, say("loose_parts", det.join(", ")));
|
|
889
|
+
room.look(add(c, 2, 0, 3.5), add(c, 0, 0, -0.5));
|
|
890
|
+
}),
|
|
891
|
+
scenario("vehicles_break_mirror", ({ room, step, expect }) => {
|
|
892
|
+
const v = put(room, "sedan");
|
|
893
|
+
const c = v.d.partWorldCenter("mirror_left");
|
|
894
|
+
room.world.hit(v.id, { type: "blunt", point: c, direction: [-1, 0, 0], energy: 2500, radius: 0.1, source: "teste" });
|
|
895
|
+
step(2);
|
|
896
|
+
const m = part(room, v, "mirror_left");
|
|
897
|
+
expect(m.status === "broken" && v.d.state().groups.length >= 1, say("mirror_broken", m.status));
|
|
898
|
+
}),
|
|
899
|
+
scenario("vehicles_kill_battery", ({ room, step, expect }) => {
|
|
900
|
+
const v = put(room, "pickup");
|
|
901
|
+
expect(v.d.status().lightsOn && v.d.status().canStart, say("battery_before"));
|
|
902
|
+
room.playAnimation("open_hood", v.id);
|
|
903
|
+
step(1.5);
|
|
904
|
+
for (let i = 0; i < 80 && room.world.disassemble(v.id, "battery~underbody", "wrench", 0.1) < 1; i++) step(0.1);
|
|
905
|
+
step(0.5);
|
|
906
|
+
const s = v.d.status();
|
|
907
|
+
expect(!s.lightsOn && !s.canStart && !s.canDrive, say("battery_after", s.lightsOn, s.canStart));
|
|
908
|
+
}),
|
|
909
|
+
scenario("vehicles_rollover", ({ room, step, expect }) => {
|
|
910
|
+
const v = put(room, "sedan");
|
|
911
|
+
const maxRoof = v.d.describe().parts.filter((p) => p.id === "roof")[0].hp;
|
|
912
|
+
v.d.rollover();
|
|
913
|
+
step(8);
|
|
914
|
+
const roof = part(room, v, "roof");
|
|
915
|
+
expect(!!v.d.vehicle.flipped, say("rolled_over", !!v.d.vehicle.flipped));
|
|
916
|
+
expect(roof.hp < maxRoof && roof.dents.length >= 1, say("roof", Math.round(roof.hp), maxRoof, roof.dents.length));
|
|
917
|
+
}),
|
|
918
|
+
scenario("vehicles_melt", ({ room, step, expect }) => {
|
|
919
|
+
const v = put(room, "hatch");
|
|
920
|
+
const c = v.d.partWorldCenter("tyre_front_left");
|
|
921
|
+
for (let i = 0; i < 60; i++) {
|
|
922
|
+
room.world.hit(v.id, { type: "fire", point: c, temperature: 900, radius: 0.7, duration: 0.1, source: "teste" });
|
|
923
|
+
step(0.1);
|
|
924
|
+
}
|
|
925
|
+
step(30);
|
|
926
|
+
const t = part(room, v, "tyre_front_left");
|
|
927
|
+
expect(events(room, "melt", v) >= 1 && (t.status === "melted" || t.melt > 0.5), say("tyre_melt", t.status, Math.round(t.melt * 100), events(room, "melt", v)));
|
|
928
|
+
}),
|
|
929
|
+
// new in the kit: the burnt wreck comes ready from the kit (the fleet's `burnt` scenario built into the vehicle), no damage applied by the area
|
|
930
|
+
scenario("vehicles_burnt_wreck", ({ room, expect }) => {
|
|
931
|
+
const c = room.spawn("sedan_burnt", [0, 0, -7], 0).root.userData.vehicle;
|
|
932
|
+
expect(c.zoneState.flags.burnt === true, say("wreck_burnt", c.zoneState.flags.burnt === true));
|
|
933
|
+
const glass = c.zones.filter((z) => z.type === "glass");
|
|
934
|
+
const up = glass.filter((z) => (c.zoneState.health[z.id] ?? 100) > 0);
|
|
935
|
+
expect(glass.length >= 1 && up.length === 0, say("wreck_glass", glass.length, up.length));
|
|
936
|
+
}),
|
|
937
|
+
// new in the kit: the drivers, headless (no vehicle is spawned)
|
|
938
|
+
scenario("vehicles_drive_arcade", ({ expect }) => {
|
|
939
|
+
const driver = createArcadeDriver();
|
|
940
|
+
const wallX = 60;
|
|
941
|
+
driver.place(0, 0, 0);
|
|
942
|
+
const world = { blocked: (x, z, yaw) => vehicleCorners(driver.spec, x, z, yaw).some(([px]) => px >= wallX) };
|
|
943
|
+
let crashSpeed = 0;
|
|
944
|
+
let engine = 0;
|
|
945
|
+
let crashNoise = 0;
|
|
946
|
+
let after = 0;
|
|
947
|
+
for (let t = 0; t < 12; t += 1 / 60) {
|
|
948
|
+
const { events: out } = driver.step({ throttle: 1, brake: 0, steer: 0, handbrake: false }, 1 / 60, world);
|
|
949
|
+
for (const e of out) {
|
|
950
|
+
if (e.type === "crash" && !crashSpeed) {
|
|
951
|
+
crashSpeed = e.speed;
|
|
952
|
+
after = driver.state.v;
|
|
953
|
+
}
|
|
954
|
+
if (e.type === "noise" && e.source === "engine") engine++;
|
|
955
|
+
if (e.type === "noise" && e.source === "crash") crashNoise++;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
expect(crashSpeed > driver.spec.crash.damageSpeed && crashSpeed <= driver.spec.maxForward + 1e-6, say("arcade_crash", crashSpeed.toFixed(1), driver.spec.maxForward));
|
|
959
|
+
expect(after < 0, say("arcade_bounce", after.toFixed(1)));
|
|
960
|
+
expect(engine >= 1 && crashNoise >= 1, say("arcade_noise", engine, crashNoise));
|
|
961
|
+
}),
|
|
962
|
+
scenario("vehicles_drive_semi", ({ expect }) => {
|
|
963
|
+
const surfaces = createSurfaces();
|
|
964
|
+
const coast = { throttle: 0, brake: 0, steer: 0, handbrake: false };
|
|
965
|
+
const dt = 1 / 60;
|
|
966
|
+
const world = (surface) => ({ ground: () => ({ surface }) });
|
|
967
|
+
const braking = (surface) => {
|
|
968
|
+
const driver = createSemiSimDriver({}, { surfaces });
|
|
969
|
+
driver.state.u = 80 / 3.6;
|
|
970
|
+
for (let t = 0; t < 1; t += dt) driver.step({ ...coast, throttle: 0.3 }, dt, world(surface));
|
|
971
|
+
driver.state.u = 80 / 3.6;
|
|
972
|
+
const start = driver.state.x;
|
|
973
|
+
for (let t = 0; t < 30 && driver.state.u > 0.1; t += dt) driver.step({ ...coast, brake: 1 }, dt, world(surface));
|
|
974
|
+
return driver.state.x - start;
|
|
975
|
+
};
|
|
976
|
+
const dry = braking("dry_asphalt");
|
|
977
|
+
const wet = braking("wet_asphalt");
|
|
978
|
+
const mud = braking("mud");
|
|
979
|
+
expect(dry < wet && wet < mud, say("semi_order", dry.toFixed(1), wet.toFixed(1), mud.toFixed(1)));
|
|
980
|
+
const bog = createSemiSimDriver({}, { surfaces });
|
|
981
|
+
let bogged = 0;
|
|
982
|
+
for (let t = 0; t < 8; t += dt) bogged += bog.step({ ...coast, throttle: 1 }, dt, world("mud")).events.filter((e) => e.type === "bogged").length;
|
|
983
|
+
expect(bog.state.bogged && bogged >= 1, say("semi_mud", bog.state.bogged));
|
|
984
|
+
})
|
|
985
|
+
];
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
// src/test-area/module.ts
|
|
989
|
+
import { LIBRARY_BANK } from "@adatechnology/game-audio";
|
|
990
|
+
|
|
991
|
+
// src/audio/profiles.ts
|
|
992
|
+
var DEFAULT_VEHICLE_SOUND_KEYS = {
|
|
993
|
+
start: "engine_start",
|
|
994
|
+
stop: "engine_stop",
|
|
995
|
+
fail: "engine_fail",
|
|
996
|
+
crashLight: "crash_light",
|
|
997
|
+
crashMedium: "crash_medium",
|
|
998
|
+
crashHeavy: "crash_heavy",
|
|
999
|
+
skidAsphalt: "tire_skid_asphalt",
|
|
1000
|
+
skidDirt: "tire_skid_dirt",
|
|
1001
|
+
roll: "tire_roll"
|
|
1002
|
+
};
|
|
1003
|
+
var CRASH_SPEEDS = { medium: 6, heavy: 14 };
|
|
1004
|
+
var layers = (id, cutoffs, gains) => [
|
|
1005
|
+
{ sound: `engine_${id}_low`, centre: 0, width: 0.5, harmonic: 1, gain: gains[0], cutoff: cutoffs[0] },
|
|
1006
|
+
{ sound: `engine_${id}_mid`, centre: 0.45, width: 0.5, harmonic: 1, gain: gains[1], cutoff: cutoffs[1] },
|
|
1007
|
+
{ sound: `engine_${id}_high`, centre: 1, width: 0.6, harmonic: 2, gain: gains[2], cutoff: cutoffs[2] }
|
|
1008
|
+
];
|
|
1009
|
+
var CAR_ENGINE = {
|
|
1010
|
+
id: "car",
|
|
1011
|
+
cylinders: 4,
|
|
1012
|
+
idleRpm: 800,
|
|
1013
|
+
redlineRpm: 6500,
|
|
1014
|
+
// a 5-speed hatch: 1st tops at ~13 m/s, 5th at ~50 m/s
|
|
1015
|
+
rpmPerMps: [500, 300, 205, 155, 125],
|
|
1016
|
+
layers: layers("car", [[220, 520], [420, 1300], [900, 2600]], [0.11, 0.075, 0.04]),
|
|
1017
|
+
load: { sound: "engine_car_load", gain: 0.05, cutoff: [350, 1600] },
|
|
1018
|
+
closedThrottle: 0.55
|
|
1019
|
+
};
|
|
1020
|
+
var TRUCK_ENGINE = {
|
|
1021
|
+
id: "truck",
|
|
1022
|
+
cylinders: 6,
|
|
1023
|
+
idleRpm: 650,
|
|
1024
|
+
redlineRpm: 2600,
|
|
1025
|
+
rpmPerMps: [420, 260, 170, 120, 90, 72],
|
|
1026
|
+
layers: layers("truck", [[160, 380], [300, 800], [600, 1500]], [0.13, 0.08, 0.035]),
|
|
1027
|
+
load: { sound: "engine_truck_load", gain: 0.06, cutoff: [250, 900] },
|
|
1028
|
+
closedThrottle: 0.6
|
|
1029
|
+
};
|
|
1030
|
+
var BIKE_ENGINE = {
|
|
1031
|
+
id: "bike",
|
|
1032
|
+
cylinders: 4,
|
|
1033
|
+
idleRpm: 1300,
|
|
1034
|
+
redlineRpm: 12e3,
|
|
1035
|
+
rpmPerMps: [900, 640, 500, 410, 350, 310],
|
|
1036
|
+
layers: layers("bike", [[350, 900], [700, 2200], [1500, 4200]], [0.08, 0.07, 0.05]),
|
|
1037
|
+
load: { sound: "engine_bike_load", gain: 0.045, cutoff: [600, 2600] },
|
|
1038
|
+
closedThrottle: 0.5
|
|
1039
|
+
};
|
|
1040
|
+
var ENGINE_PROFILES = { car: CAR_ENGINE, truck: TRUCK_ENGINE, bike: BIKE_ENGINE };
|
|
1041
|
+
|
|
1042
|
+
// src/test-area/types.ts
|
|
1043
|
+
var defaultEffectIds = {
|
|
1044
|
+
ignite: "vehicle_fire",
|
|
1045
|
+
explode: "car_fuel_tank",
|
|
1046
|
+
glass: "glass_burst",
|
|
1047
|
+
sparks: "impact_metal",
|
|
1048
|
+
leak: "vehicle_leak",
|
|
1049
|
+
leakIgnited: "vehicle_leak_ignited",
|
|
1050
|
+
exhaust: "vehicle_exhaust"
|
|
1051
|
+
};
|
|
1052
|
+
var defaultSoundIds = { horn: "horn_car", ...DEFAULT_VEHICLE_SOUND_KEYS };
|
|
1053
|
+
var DEFAULT_TEST_WRECKS = [
|
|
1054
|
+
{ key: "sedan_burnt", vehicle: "midsize_sedan", scenario: "burnt" },
|
|
1055
|
+
{ key: "hatch_burnt", vehicle: "compact_hatch", scenario: "burnt" }
|
|
1056
|
+
];
|
|
1057
|
+
|
|
1058
|
+
// src/audio/engine-mix.ts
|
|
1059
|
+
var clamp01 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
|
|
1060
|
+
var firingHz = (profile, rpm) => rpm / 60 * (profile.cylinders / 2);
|
|
1061
|
+
var rpmShare = (profile, rpm) => clamp01((rpm - profile.idleRpm) / (profile.redlineRpm - profile.idleRpm));
|
|
1062
|
+
var layerWeight = (centre, width, n) => {
|
|
1063
|
+
const d = Math.abs(n - centre) / width;
|
|
1064
|
+
return d >= 1 ? 0 : Math.cos(d * Math.PI / 2);
|
|
1065
|
+
};
|
|
1066
|
+
function engineMix(profile, input) {
|
|
1067
|
+
const throttle = clamp01(input.throttle);
|
|
1068
|
+
const on = input.rpm > 0;
|
|
1069
|
+
const n = rpmShare(profile, input.rpm);
|
|
1070
|
+
const level = on ? profile.closedThrottle + (1 - profile.closedThrottle) * throttle : 0;
|
|
1071
|
+
const open = 0.5 * n + 0.5 * throttle;
|
|
1072
|
+
const hz = firingHz(profile, input.rpm);
|
|
1073
|
+
const tones = profile.layers.map((layer) => ({
|
|
1074
|
+
sound: layer.sound,
|
|
1075
|
+
volume: layer.gain * level * layerWeight(layer.centre, layer.width, n),
|
|
1076
|
+
frequency: hz * layer.harmonic,
|
|
1077
|
+
cutoff: layer.cutoff[0] + (layer.cutoff[1] - layer.cutoff[0]) * open
|
|
1078
|
+
}));
|
|
1079
|
+
const load = profile.load;
|
|
1080
|
+
return [
|
|
1081
|
+
...tones,
|
|
1082
|
+
{ 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 }
|
|
1083
|
+
];
|
|
1084
|
+
}
|
|
1085
|
+
function createGearGuess(profile) {
|
|
1086
|
+
let gear = 0;
|
|
1087
|
+
return {
|
|
1088
|
+
rpm(speed, throttle) {
|
|
1089
|
+
const v = Math.abs(speed);
|
|
1090
|
+
const ratios = profile.rpmPerMps;
|
|
1091
|
+
if (v < 0.3) {
|
|
1092
|
+
gear = 0;
|
|
1093
|
+
return profile.idleRpm + (profile.redlineRpm - profile.idleRpm) * 0.12 * clamp01(throttle);
|
|
1094
|
+
}
|
|
1095
|
+
const at = (g) => v * ratios[g];
|
|
1096
|
+
while (gear < ratios.length - 1 && at(gear) > profile.redlineRpm * 0.85) gear++;
|
|
1097
|
+
while (gear > 0 && at(gear) < profile.redlineRpm * 0.4) gear--;
|
|
1098
|
+
return Math.max(profile.idleRpm, Math.min(profile.redlineRpm, at(gear)));
|
|
1099
|
+
},
|
|
1100
|
+
get gear() {
|
|
1101
|
+
return gear;
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// src/audio/vehicle-sound.ts
|
|
1107
|
+
var clamp012 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
|
|
1108
|
+
function createVehicleSound(audio, options = {}) {
|
|
1109
|
+
const profile = options.profile ?? CAR_ENGINE;
|
|
1110
|
+
const keys = { ...DEFAULT_VEHICLE_SOUND_KEYS, ...options.sounds };
|
|
1111
|
+
const crankSeconds = options.crankSeconds ?? 0.75;
|
|
1112
|
+
const gears = createGearGuess(profile);
|
|
1113
|
+
const loops = /* @__PURE__ */ new Map();
|
|
1114
|
+
const levels = {};
|
|
1115
|
+
const state = { running: false, cranking: 0, catches: false, flare: 0, rpm: 0 };
|
|
1116
|
+
const loopOf = (sound) => {
|
|
1117
|
+
let handle = loops.get(sound);
|
|
1118
|
+
if (!handle && audio?.loop) {
|
|
1119
|
+
handle = audio.loop(sound);
|
|
1120
|
+
loops.set(sound, handle);
|
|
1121
|
+
}
|
|
1122
|
+
return handle;
|
|
1123
|
+
};
|
|
1124
|
+
const setLoop = (sound, volume, position, extra = {}) => {
|
|
1125
|
+
levels[sound] = volume;
|
|
1126
|
+
if (volume <= 0 && !loops.has(sound)) return;
|
|
1127
|
+
loopOf(sound)?.set({ volume, position, ...extra.frequency === void 0 ? {} : { frequency: extra.frequency }, ...extra.cutoff === void 0 ? {} : { params: { cutoff: extra.cutoff } } });
|
|
1128
|
+
};
|
|
1129
|
+
const play = (sound, position, volume = 1) => void audio?.play({ sound, position, volume });
|
|
1130
|
+
return {
|
|
1131
|
+
start(position, startOptions = {}) {
|
|
1132
|
+
if (state.running || state.cranking > 0) return;
|
|
1133
|
+
state.catches = !startOptions.fail;
|
|
1134
|
+
state.cranking = state.catches ? crankSeconds : 1.7;
|
|
1135
|
+
play(state.catches ? keys.start : keys.fail, position);
|
|
1136
|
+
},
|
|
1137
|
+
stop(position) {
|
|
1138
|
+
if (!state.running && state.cranking <= 0) return;
|
|
1139
|
+
state.running = false;
|
|
1140
|
+
state.cranking = 0;
|
|
1141
|
+
play(keys.stop, position);
|
|
1142
|
+
},
|
|
1143
|
+
crash(speed, position) {
|
|
1144
|
+
const v = Math.abs(speed);
|
|
1145
|
+
const kind = v >= CRASH_SPEEDS.heavy ? "heavy" : v >= CRASH_SPEEDS.medium ? "medium" : "light";
|
|
1146
|
+
play(kind === "heavy" ? keys.crashHeavy : kind === "medium" ? keys.crashMedium : keys.crashLight, position, Math.min(1, 0.35 + v / 20));
|
|
1147
|
+
return kind;
|
|
1148
|
+
},
|
|
1149
|
+
update(frame) {
|
|
1150
|
+
if (state.cranking > 0) {
|
|
1151
|
+
state.cranking -= frame.dt;
|
|
1152
|
+
if (state.cranking <= 0 && state.catches) {
|
|
1153
|
+
state.running = true;
|
|
1154
|
+
state.flare = 1;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
state.flare = Math.max(0, state.flare - frame.dt / 0.9);
|
|
1158
|
+
const throttle = clamp012(frame.throttle);
|
|
1159
|
+
const driven = frame.rpm ?? gears.rpm(frame.speed, throttle);
|
|
1160
|
+
const rpm = state.running ? Math.max(driven, profile.idleRpm * (1 + 1.2 * state.flare * state.flare)) : 0;
|
|
1161
|
+
state.rpm = rpm;
|
|
1162
|
+
for (const layer of engineMix(profile, { rpm, throttle: Math.max(throttle, state.flare * 0.6) })) {
|
|
1163
|
+
setLoop(layer.sound, layer.volume, frame.position, { ...layer.frequency === void 0 ? {} : { frequency: layer.frequency }, cutoff: layer.cutoff });
|
|
1164
|
+
}
|
|
1165
|
+
const speed = Math.abs(frame.speed);
|
|
1166
|
+
const slide = clamp012(((frame.slip ?? 0) - 0.15) / 0.5) * clamp012(speed / 4);
|
|
1167
|
+
const dirt = frame.surface === "dirt";
|
|
1168
|
+
setLoop(keys.skidAsphalt, dirt ? 0 : 0.16 * slide, frame.position, { cutoff: 1700 + 600 * slide });
|
|
1169
|
+
setLoop(keys.skidDirt, dirt ? 0.2 * slide : 0, frame.position, { cutoff: 500 + 500 * slide });
|
|
1170
|
+
setLoop(keys.roll, 0.06 * clamp012(speed / 25), frame.position, { cutoff: 250 + 45 * Math.min(speed, 40) });
|
|
1171
|
+
},
|
|
1172
|
+
get reading() {
|
|
1173
|
+
return { running: state.running, rpm: state.rpm, levels: { ...levels } };
|
|
1174
|
+
},
|
|
1175
|
+
dispose() {
|
|
1176
|
+
for (const handle of loops.values()) handle.stop();
|
|
1177
|
+
loops.clear();
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// src/audio/bank.ts
|
|
1183
|
+
import { sceneMany as many, sceneNoise as noise, sceneRecipe, sceneTone as tone } from "@adatechnology/game-audio";
|
|
1184
|
+
var VEHICLE_LOOP_FALLOFF = { model: "inverse_square", k: 4e-3, cutoffK: 0.01 };
|
|
1185
|
+
var SDK = "sdk: vehicle sounds (sons/checklist-sons.md 5.6)";
|
|
1186
|
+
var SMOOTHING = { volume: 0.06, frequency: 0.04 };
|
|
1187
|
+
var toneLoop = (id, wave, filter, cutoff, q, line) => ({
|
|
1188
|
+
id,
|
|
1189
|
+
bus: "world",
|
|
1190
|
+
order: "game_loop",
|
|
1191
|
+
source: { kind: "osc", wave, loop: true },
|
|
1192
|
+
filters: { mode: "serial", bank: [{ type: filter, frequency: { set: cutoff }, q }] },
|
|
1193
|
+
smoothing: SMOOTHING,
|
|
1194
|
+
falloff: VEHICLE_LOOP_FALLOFF,
|
|
1195
|
+
pan: true,
|
|
1196
|
+
sources: [SDK, line]
|
|
1197
|
+
});
|
|
1198
|
+
var noiseLoop = (id, filter, cutoff, q, line) => ({
|
|
1199
|
+
id,
|
|
1200
|
+
bus: "world",
|
|
1201
|
+
order: "game_loop",
|
|
1202
|
+
source: { kind: "noise", offset: "random", loop: true },
|
|
1203
|
+
filters: { mode: "serial", bank: [{ type: filter, frequency: { set: cutoff }, q }] },
|
|
1204
|
+
smoothing: SMOOTHING,
|
|
1205
|
+
falloff: VEHICLE_LOOP_FALLOFF,
|
|
1206
|
+
pan: true,
|
|
1207
|
+
sources: [SDK, line]
|
|
1208
|
+
});
|
|
1209
|
+
var engineLoops = (id, low, mid, high, load) => [
|
|
1210
|
+
toneLoop(`engine_${id}_low`, "sawtooth", "lowpass", low, 1.4, "idle and low RPM: the firing frequency, a saw through a low-pass"),
|
|
1211
|
+
toneLoop(`engine_${id}_mid`, "square", "lowpass", mid, 0.9, "mid RPM: odd harmonics, the bark"),
|
|
1212
|
+
toneLoop(`engine_${id}_high`, "sawtooth", "bandpass", high, 1.2, "high RPM: the second harmonic through a band-pass, the scream near the redline"),
|
|
1213
|
+
noiseLoop(`engine_${id}_load`, "bandpass", load, 0.8, "intake and exhaust roar, opened by the throttle")
|
|
1214
|
+
];
|
|
1215
|
+
var VEHICLE_LOOPS = [
|
|
1216
|
+
...engineLoops("car", 400, 900, 1600, 700),
|
|
1217
|
+
...engineLoops("truck", 280, 600, 1e3, 500),
|
|
1218
|
+
...engineLoops("bike", 600, 1400, 2800, 1200),
|
|
1219
|
+
noiseLoop("tire_skid_asphalt", "bandpass", 1900, 5, "rubber squealing on asphalt: a narrow band of noise near 2 kHz"),
|
|
1220
|
+
noiseLoop("tire_skid_dirt", "lowpass", 700, 0.8, "tyres sliding on dirt and gravel: a dull, wide hiss"),
|
|
1221
|
+
noiseLoop("tire_roll", "lowpass", 400, 0.7, "road noise of the tyres rolling, brighter with the speed")
|
|
1222
|
+
];
|
|
1223
|
+
var recipe = (id, layers2, line) => ({ ...sceneRecipe(id, layers2, line), sources: [SDK, line] });
|
|
1224
|
+
var crank = (count2, spacing) => [
|
|
1225
|
+
{ ...noise({ type: "bandpass", f: [300, 380], q: 1.5, d: 0.07, vol: 0.45 }), repeat: { count: count2, spacing } },
|
|
1226
|
+
tone({ wave: "sawtooth", f: 95, f2: count2 > 8 ? 70 : 110, d: count2 * spacing, vol: 0.035 })
|
|
1227
|
+
];
|
|
1228
|
+
var latch = (delay, vol) => [
|
|
1229
|
+
noise({ type: "bandpass", f: [2600, 3200], q: 3, d: 0.018, vol, delay }),
|
|
1230
|
+
tone({ f: 1300, f2: 1180, d: 0.025, vol: vol * 0.15, delay: delay + 4e-3 })
|
|
1231
|
+
];
|
|
1232
|
+
var VEHICLE_SOUNDS = [
|
|
1233
|
+
// the engine catches at 0.75 s: the loops take over from there (the vehicle flares the RPM)
|
|
1234
|
+
recipe("engine_start", [...crank(7, 0.105), tone({ wave: "sawtooth", f: 45, f2: 27, d: 0.35, vol: 0.3, delay: 0.72 }), noise({ f: 260, d: 0.3, vol: 0.35, delay: 0.72 })], "starter cranking, then the engine catches"),
|
|
1235
|
+
recipe("engine_fail", [...crank(13, 0.125), ...latch(1.65, 0.2)], "starter cranking slower and slower, the engine never catches, the key released"),
|
|
1236
|
+
recipe("engine_stop", [tone({ wave: "sawtooth", f: 27, f2: 8, d: 0.45, vol: 0.3 }), noise({ f: 200, f2: 60, d: 0.4, vol: 0.32 }), noise({ type: "bandpass", f: 120, q: 2, d: 0.12, vol: 0.3, delay: 0.32 })], "ignition off: the engine runs down and shudders to a stop"),
|
|
1237
|
+
// light: a dent and plastic giving; medium: the body folding and a pane cracking; heavy: metal folding, glass going, debris falling
|
|
1238
|
+
recipe(
|
|
1239
|
+
"crash_light",
|
|
1240
|
+
[tone({ f: 110, f2: 55, d: 0.16, vol: 0.7 }), noise({ type: "bandpass", f: 1100, q: 1, d: 0.18, vol: 0.85, wet: 0.2 }), noise({ type: "highpass", f: 2500, d: 0.05, vol: 0.45 }), ...many(3, () => noise({ type: "bandpass", f: [1800, 3200], q: 3, d: 0.03, vol: 0.2, delay: [0.03, 0.2] }))],
|
|
1241
|
+
"crash below 22 km/h: bumper and plastic, a dull dent"
|
|
1242
|
+
),
|
|
1243
|
+
recipe(
|
|
1244
|
+
"crash_medium",
|
|
1245
|
+
[
|
|
1246
|
+
tone({ f: 85, f2: 38, d: 0.3, vol: 1.1 }),
|
|
1247
|
+
noise({ type: "bandpass", f: 700, q: 0.9, d: 0.5, vol: 0.8, wet: 0.3 }),
|
|
1248
|
+
...many(5, () => noise({ type: "bandpass", f: [500, 1300], q: 2, d: [0.08, 0.18], vol: 0.55, delay: [0, 0.35] })),
|
|
1249
|
+
...many(6, () => tone({ f: [3500, 7e3], d: 0.06, vol: 0.08, delay: [0.05, 0.5] }))
|
|
1250
|
+
],
|
|
1251
|
+
"crash at 22\u201350 km/h: the body folding, a pane cracking"
|
|
1252
|
+
),
|
|
1253
|
+
recipe(
|
|
1254
|
+
"crash_heavy",
|
|
1255
|
+
[
|
|
1256
|
+
tone({ f: 65, f2: 28, d: 0.6, vol: 1.5 }),
|
|
1257
|
+
noise({ f: 900, f2: 150, d: 0.9, vol: 1.3, wet: 0.5 }),
|
|
1258
|
+
...many(9, () => noise({ type: "bandpass", f: [300, 1200], q: 2, d: [0.1, 0.25], vol: 0.6, delay: [0, 0.7] })),
|
|
1259
|
+
noise({ type: "highpass", f: 3200, d: 0.5, vol: 0.7, delay: 0.03, wet: 0.4 }),
|
|
1260
|
+
...many(14, () => tone({ f: [3e3, 7500], d: [0.05, 0.12], vol: 0.12, delay: [0.05, 1.2] })),
|
|
1261
|
+
...many(6, () => noise({ type: "bandpass", f: [1500, 3e3], q: 2, d: 0.04, vol: 0.25, delay: [0.5, 1.5] }))
|
|
1262
|
+
],
|
|
1263
|
+
"crash above 50 km/h: metal folding, the glass going, debris falling"
|
|
1264
|
+
),
|
|
1265
|
+
// the game's horn (two squares at 415 and 523 Hz, jogo L70272) heard from where the car is; a truck's air horn lower and longer
|
|
1266
|
+
recipe("horn_car", [tone({ wave: "square", f: 415, d: 0.6, vol: 0.09 }), tone({ wave: "square", f: 523, d: 0.6, vol: 0.09 })], "car horn: the game chord, placed and panned"),
|
|
1267
|
+
recipe("horn_truck", [tone({ wave: "sawtooth", f: 220, d: 1.1, vol: 0.1 }), tone({ wave: "sawtooth", f: 277, d: 1.1, vol: 0.08 }), noise({ type: "bandpass", f: 900, q: 2, d: 1.1, vol: 0.05 })], "truck air horn: low reedy chord"),
|
|
1268
|
+
// a car door is a padded "tum" with a latch, not a hollow metal box (the fridge's shut_metal)
|
|
1269
|
+
recipe("car_door_open", [...latch(0, 0.32), noise({ f: 400, d: 0.06, vol: 0.14, delay: 0.035 })], "car door: the handle pulled, the latch released, the seal letting go"),
|
|
1270
|
+
recipe("car_door_close", [tone({ f: 75, f2: 48, d: 0.16, vol: 0.75 }), noise({ f: 320, d: 0.1, vol: 0.6 }), ...latch(0.012, 0.3)], "car door shut: a dull padded thump and the latch catching"),
|
|
1271
|
+
recipe("car_trunk_close", [tone({ f: 90, f2: 55, d: 0.15, vol: 0.65 }), noise({ type: "bandpass", f: 500, q: 1, d: 0.12, vol: 0.5 }), ...latch(0.01, 0.28), noise({ type: "bandpass", f: 1800, q: 3, d: 0.04, vol: 0.1, delay: 0.06 })], "trunk or hatch shut: a lighter thump, the latch and a small rattle"),
|
|
1272
|
+
recipe("car_hood_close", [tone({ f: 70, f2: 40, d: 0.22, vol: 0.8 }), noise({ type: "bandpass", f: 850, q: 1.4, d: 0.2, vol: 0.6, wet: 0.3 }), tone({ f: 900, f2: 860, d: 0.18, vol: 0.05 }), ...latch(0.015, 0.3)], "hood slammed: a big sheet of metal ringing a little, the latch")
|
|
1273
|
+
];
|
|
1274
|
+
var VEHICLE_SOUND_BANK = { id: "vehicles_three", sounds: VEHICLE_SOUNDS, loops: VEHICLE_LOOPS };
|
|
1275
|
+
|
|
1276
|
+
// src/test-area/module.ts
|
|
1277
|
+
var VEHICLES_AREA_ID = "vehicles_area";
|
|
1278
|
+
var DEFAULT_TEST_VEHICLES = ["compact_hatch", "midsize_sedan", "midsize_pickup", "sport_bike", "box_truck", "city_bus", "battle_tank", "attack_helicopter"];
|
|
1279
|
+
var ITEM_OF = { compact_hatch: "hatch", midsize_sedan: "sedan", midsize_pickup: "pickup", sport_bike: "sport_bike", box_truck: "box_truck", city_bus: "city_bus", battle_tank: "battle_tank", attack_helicopter: "attack_helicopter" };
|
|
1280
|
+
var itemId = (key) => ITEM_OF[key] ?? key;
|
|
1281
|
+
var LANE = { z: 11, from: -12, wall: 13 };
|
|
1282
|
+
var SPARK_EVERY_S = 0.08;
|
|
1283
|
+
var LEAF_REST_RADIANS = 0.02;
|
|
1284
|
+
var MATERIAL_COLOURS = { metal: 13421772, glass: 4500223, rubber: 16750848, fuel: 16724736 };
|
|
1285
|
+
var LANE_START_S = 1.2;
|
|
1286
|
+
var ZONE_COLOURS = { door: 5627221, hood: 14540083, lid: 14522675, bumper: 14505437, glass: 4500223, light: 16777215, wheel: 16742195 };
|
|
1287
|
+
var BODY_HEIGHT = 1.7;
|
|
1288
|
+
function createVehiclesTestModule(options = {}) {
|
|
1289
|
+
const fleet = options.fleet ?? { colorFromSeed, fleetDims, installFleet, paintPalettes, paletteOf };
|
|
1290
|
+
const locale = options.locale ?? "pt-BR";
|
|
1291
|
+
const text = TEXT[locale] ?? TEXT["pt-BR"];
|
|
1292
|
+
const keys = options.vehicles ?? DEFAULT_TEST_VEHICLES;
|
|
1293
|
+
const wrecks = options.wrecks ?? DEFAULT_TEST_WRECKS;
|
|
1294
|
+
const entries = [...keys.map((key) => ({ key, id: itemId(key) })), ...wrecks.map((w) => ({ key: w.vehicle, id: w.key, scenario: w.scenario }))];
|
|
1295
|
+
const effectIds = { ...defaultEffectIds, ...options.effects };
|
|
1296
|
+
const soundIds = { ...defaultSoundIds, ...options.sounds };
|
|
1297
|
+
const settings = { quality: "high", paint: false, seed: 1, palette: "auto", driving: "arcade", surface: "dry_asphalt", damage: "none" };
|
|
1298
|
+
const kits = /* @__PURE__ */ new WeakMap();
|
|
1299
|
+
const placed = /* @__PURE__ */ new Map();
|
|
1300
|
+
const cabins = createCabinSmoke();
|
|
1301
|
+
let host = null;
|
|
1302
|
+
let three = null;
|
|
1303
|
+
let counter = 0;
|
|
1304
|
+
let helpersShown = "";
|
|
1305
|
+
let axes = [];
|
|
1306
|
+
let lane = null;
|
|
1307
|
+
const effects = () => host?.effects;
|
|
1308
|
+
const audio = () => host?.audio;
|
|
1309
|
+
let carSound;
|
|
1310
|
+
const stopEngine = () => {
|
|
1311
|
+
carSound?.dispose();
|
|
1312
|
+
carSound = void 0;
|
|
1313
|
+
};
|
|
1314
|
+
const particleMaterials = {
|
|
1315
|
+
own: (build) => isRenderPools(host?.pools) ? host.pools.materials.own(build) : build(),
|
|
1316
|
+
release: (material) => isRenderPools(host?.pools) ? host.pools.materials.release(material) : material.dispose?.()
|
|
1317
|
+
};
|
|
1318
|
+
const kitFor = (THREE) => {
|
|
1319
|
+
three = THREE;
|
|
1320
|
+
let kit = kits.get(THREE);
|
|
1321
|
+
if (!kit) {
|
|
1322
|
+
if (options.createKit) kit = options.createKit(THREE);
|
|
1323
|
+
else {
|
|
1324
|
+
kit = createVehicleKit({ THREE, locale, particleMaterials, effects: () => host?.effects, ...options.document ? { document: options.document } : {} });
|
|
1325
|
+
fleet.installFleet(kit);
|
|
1326
|
+
}
|
|
1327
|
+
kits.set(THREE, kit);
|
|
1328
|
+
}
|
|
1329
|
+
return kit;
|
|
1330
|
+
};
|
|
1331
|
+
const paletteFor = (key) => settings.palette === "civil" ? fleet.paintPalettes.civil : settings.palette === "military" ? fleet.paintPalettes.military : fleet.paletteOf(key);
|
|
1332
|
+
const paintOf = (entry) => {
|
|
1333
|
+
if (!settings.paint) return;
|
|
1334
|
+
entry.lod?.dispose();
|
|
1335
|
+
delete entry.lod;
|
|
1336
|
+
const color = fleet.colorFromSeed(settings.seed + entry.index, paletteFor(entry.key));
|
|
1337
|
+
if (!entry.paint) {
|
|
1338
|
+
const vehicle = entry.item.vehicle;
|
|
1339
|
+
const primary = primaryPaint(entry.item.vehicle);
|
|
1340
|
+
if (!primary) return;
|
|
1341
|
+
entry.paint = primary.clone();
|
|
1342
|
+
vehicle.vehicle.traverse((mesh) => {
|
|
1343
|
+
if (!mesh.isMesh) return;
|
|
1344
|
+
if (Array.isArray(mesh.material)) mesh.material = mesh.material.map((m) => m === primary ? entry.paint : m);
|
|
1345
|
+
else if (mesh.material === primary) mesh.material = entry.paint;
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
entry.paint.color.set(color);
|
|
1349
|
+
};
|
|
1350
|
+
const item = ({ key, id: itemKey, scenario }) => ({
|
|
1351
|
+
id: itemKey,
|
|
1352
|
+
name: text.items[itemKey] ?? key,
|
|
1353
|
+
movable: false,
|
|
1354
|
+
// the first build of a vehicle takes most of a second
|
|
1355
|
+
slow: true,
|
|
1356
|
+
build: (THREE) => {
|
|
1357
|
+
const kit = kitFor(THREE);
|
|
1358
|
+
const built = buildVehicleItem(kit, key, { lod: settings.quality === "lite", ...scenario ? { scenario } : {} });
|
|
1359
|
+
built.root.userData.kit = kit;
|
|
1360
|
+
const entry = { root: built.root, key, ...scenario ? { scenario } : {}, item: built, index: counter++, helpers: [] };
|
|
1361
|
+
placed.set(built.root.uuid, entry);
|
|
1362
|
+
measure(entry);
|
|
1363
|
+
paintOf(entry);
|
|
1364
|
+
return built.root;
|
|
1365
|
+
},
|
|
1366
|
+
impl: (root, id) => {
|
|
1367
|
+
const r = root;
|
|
1368
|
+
const kit = r.userData.kit;
|
|
1369
|
+
const d = createVehicleDestructible2(kit, r.userData.vehicle, { id, frame: root, name: text.items[itemKey] ?? key, creaseQueue });
|
|
1370
|
+
tapEffects(d);
|
|
1371
|
+
const setPartPose = d.setPartPose?.bind(d);
|
|
1372
|
+
{
|
|
1373
|
+
const rests = new Map(d.describe().parts.map((part) => [part.id, part.rotation ?? [0, 0, 0]]));
|
|
1374
|
+
d.setPartPose = (partId, position, rotation) => {
|
|
1375
|
+
const entry = [...placed.values()].find((candidate) => candidate.root === root);
|
|
1376
|
+
const rest = rests.get(partId) ?? [0, 0, 0];
|
|
1377
|
+
const isOpen = (rotation ?? [0, 0, 0]).some((angle, axis) => Math.abs(angle - (rest[axis] ?? 0)) > LEAF_REST_RADIANS);
|
|
1378
|
+
if (entry) {
|
|
1379
|
+
entry.posedParts ?? (entry.posedParts = /* @__PURE__ */ new Set());
|
|
1380
|
+
if (isOpen) entry.posedParts.add(partId);
|
|
1381
|
+
else entry.posedParts.delete(partId);
|
|
1382
|
+
}
|
|
1383
|
+
return setPartPose?.(partId, position, rotation);
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
const hitBody = d.hit.bind(d);
|
|
1387
|
+
d.hit = (h) => {
|
|
1388
|
+
const entry = [...placed.values()].find((candidate) => candidate.root === root);
|
|
1389
|
+
if (entry) entry.touched = true;
|
|
1390
|
+
return hitBody(h);
|
|
1391
|
+
};
|
|
1392
|
+
cabins.track(id, d, root);
|
|
1393
|
+
r.userData.destructible = d;
|
|
1394
|
+
return d;
|
|
1395
|
+
},
|
|
1396
|
+
...clipsOf(key) ? { animations: clipsOf(key) } : {}
|
|
1397
|
+
});
|
|
1398
|
+
const clipCache = /* @__PURE__ */ new Map();
|
|
1399
|
+
const clipsOf = (key) => {
|
|
1400
|
+
const THREE = options.three ?? globalThis.THREE;
|
|
1401
|
+
if (!THREE) return void 0;
|
|
1402
|
+
if (!clipCache.has(key)) {
|
|
1403
|
+
const kit = kitFor(THREE);
|
|
1404
|
+
const built = buildVehicleItem(kit, key);
|
|
1405
|
+
const d = createVehicleDestructible2(kit, built.vehicle, { id: `${itemId(key)}_clips`, frame: built.root, headless: true });
|
|
1406
|
+
clipCache.set(key, d.animations());
|
|
1407
|
+
kit.dispose(built.vehicle);
|
|
1408
|
+
}
|
|
1409
|
+
return clipCache.get(key);
|
|
1410
|
+
};
|
|
1411
|
+
const tapEffects = (d) => {
|
|
1412
|
+
const request = (events) => {
|
|
1413
|
+
const fx = effects();
|
|
1414
|
+
if (!fx) return;
|
|
1415
|
+
for (const e of events) {
|
|
1416
|
+
const position = e.point ?? e.worldPoint;
|
|
1417
|
+
if (!position) continue;
|
|
1418
|
+
if (e.type === "ignite") fx.play({ effect: /fuel_tank|tank/.test(e.part ?? "") ? effectIds.leakIgnited : effectIds.ignite, point: position });
|
|
1419
|
+
else if (e.type === "explode") fx.play({ effect: effectIds.explode, point: position });
|
|
1420
|
+
else if (e.type === "leak") fx.play({ effect: effectIds.leak, point: position });
|
|
1421
|
+
else if (e.type === "fracture" && /glass|window|windshield/.test(e.part ?? "")) fx.play({ effect: effectIds.glass, point: position });
|
|
1422
|
+
else if (e.type === "hole") fx.play({ effect: /glass|window|windshield/.test(e.part ?? "") ? effectIds.glass : effectIds.sparks, point: position });
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
const hit = d.hit.bind(d);
|
|
1426
|
+
d.hit = (h) => {
|
|
1427
|
+
const events = hit(h);
|
|
1428
|
+
request(events);
|
|
1429
|
+
return events;
|
|
1430
|
+
};
|
|
1431
|
+
if (d.update) {
|
|
1432
|
+
const update = d.update.bind(d);
|
|
1433
|
+
d.update = (dt) => {
|
|
1434
|
+
const events = update(dt);
|
|
1435
|
+
request(events);
|
|
1436
|
+
return events;
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
const rootMatrix = (entry) => {
|
|
1441
|
+
const m = new three.Matrix4().copy(entry.root.matrixWorld).invert();
|
|
1442
|
+
return m.multiply(entry.item.vehicle.vehicle.matrixWorld);
|
|
1443
|
+
};
|
|
1444
|
+
const measure = (entry) => {
|
|
1445
|
+
if (entry.size) return entry.size;
|
|
1446
|
+
entry.root.updateMatrixWorld(true);
|
|
1447
|
+
const inverse = new three.Matrix4().copy(entry.root.matrixWorld).invert();
|
|
1448
|
+
const bounds = new three.Box3();
|
|
1449
|
+
const low = new three.Box3();
|
|
1450
|
+
const piece = new three.Box3();
|
|
1451
|
+
const matrix = new three.Matrix4();
|
|
1452
|
+
entry.root.traverse((mesh) => {
|
|
1453
|
+
if (!mesh.isMesh || !mesh.geometry || mesh.userData.ghost) return;
|
|
1454
|
+
for (let p = mesh; p; p = p.parent) if (p.visible === false) return;
|
|
1455
|
+
if (mesh.userData.helper) return;
|
|
1456
|
+
mesh.geometry.computeBoundingBox();
|
|
1457
|
+
piece.copy(mesh.geometry.boundingBox).applyMatrix4(matrix.multiplyMatrices(inverse, mesh.matrixWorld));
|
|
1458
|
+
bounds.union(piece);
|
|
1459
|
+
if (piece.min.y < BODY_HEIGHT) low.union(piece);
|
|
1460
|
+
});
|
|
1461
|
+
const size = bounds.getSize(new three.Vector3());
|
|
1462
|
+
entry.size = { length: size.z, width: size.x, height: size.y };
|
|
1463
|
+
if (!low.isEmpty()) entry.footprint = { minX: low.min.x, maxX: low.max.x, minZ: low.min.z, maxZ: low.max.z };
|
|
1464
|
+
return entry.size;
|
|
1465
|
+
};
|
|
1466
|
+
const clearHelpers = () => {
|
|
1467
|
+
for (const entry of placed.values()) {
|
|
1468
|
+
discardObject3D(entry.helpers);
|
|
1469
|
+
entry.helpers = [];
|
|
1470
|
+
}
|
|
1471
|
+
discardObject3D(axes);
|
|
1472
|
+
axes = [];
|
|
1473
|
+
helpersShown = "";
|
|
1474
|
+
};
|
|
1475
|
+
const boxKey = () => `${host?.bench?.layer("damageables")}|${host?.bench?.layer("zones")}|${host?.bench?.layer("axes")}|${placed.size}`;
|
|
1476
|
+
const refreshLayers = () => {
|
|
1477
|
+
if (!host?.bench || !three) return;
|
|
1478
|
+
clearHelpers();
|
|
1479
|
+
const kit = [...placed.values()][0] && ([...placed.values()][0].root.userData.kit ?? void 0);
|
|
1480
|
+
const mark = (entry, box, color) => {
|
|
1481
|
+
const helper = new three.Box3Helper(box, color);
|
|
1482
|
+
helper.userData.helper = true;
|
|
1483
|
+
entry.root.add(helper);
|
|
1484
|
+
entry.helpers.push(helper);
|
|
1485
|
+
};
|
|
1486
|
+
if (kit) {
|
|
1487
|
+
for (const entry of placed.values()) {
|
|
1488
|
+
if (!entry.root.parent) continue;
|
|
1489
|
+
entry.root.updateMatrixWorld(true);
|
|
1490
|
+
const matrix = rootMatrix(entry);
|
|
1491
|
+
if (host.bench.layer("damageables")) for (const d of kit.damageables(entry.item.vehicle, matrix)) mark(entry, d.box, MATERIAL_COLOURS[d.material] ?? 16777215);
|
|
1492
|
+
if (host.bench.layer("zones")) {
|
|
1493
|
+
for (const zone of entry.item.vehicle.zones ?? []) {
|
|
1494
|
+
if (!zone.box || zone.type === "internal") continue;
|
|
1495
|
+
const box = new three.Box3(new three.Vector3(zone.box.x0, zone.box.y0, zone.box.z0), new three.Vector3(zone.box.x1, zone.box.y1, zone.box.z1)).applyMatrix4(matrix);
|
|
1496
|
+
mark(entry, box, ZONE_COLOURS[zone.type] ?? 16777215);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (host.bench.layer("axes")) {
|
|
1502
|
+
const front = new three.ArrowHelper(new three.Vector3(0, 0, 1), new three.Vector3(0, 0.05, 0), 3, 3368703);
|
|
1503
|
+
const right = new three.ArrowHelper(new three.Vector3(-1, 0, 0), new three.Vector3(0, 0.05, 0), 3, 16724787);
|
|
1504
|
+
const up = new three.ArrowHelper(new three.Vector3(0, 1, 0), new three.Vector3(0, 0.05, 0), 3, 3394611);
|
|
1505
|
+
for (const arrow of [front, right, up]) {
|
|
1506
|
+
;
|
|
1507
|
+
host.scene.add(arrow);
|
|
1508
|
+
axes.push(arrow);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
helpersShown = boxKey();
|
|
1512
|
+
};
|
|
1513
|
+
const laneBlocked = footprintBlocker(
|
|
1514
|
+
() => lane?.group,
|
|
1515
|
+
() => lane ? { minX: -lane.car.width / 2, maxX: lane.car.width / 2, minZ: -lane.car.length / 2, maxZ: lane.car.length / 2 } : null
|
|
1516
|
+
);
|
|
1517
|
+
const blocked = (x, z, radius) => {
|
|
1518
|
+
for (const entry of placed.values()) if (entry.footprint && footprintBlocker(entry.root, entry.footprint)(x, z, radius)) return true;
|
|
1519
|
+
return laneBlocked(x, z, radius);
|
|
1520
|
+
};
|
|
1521
|
+
const clearLane = () => {
|
|
1522
|
+
stopEngine();
|
|
1523
|
+
if (!lane) return;
|
|
1524
|
+
lane.group.parent?.remove(lane.group);
|
|
1525
|
+
lane.wall.parent?.remove(lane.wall);
|
|
1526
|
+
lane = null;
|
|
1527
|
+
};
|
|
1528
|
+
const startLane = () => {
|
|
1529
|
+
if (!host || !three) return;
|
|
1530
|
+
clearLane();
|
|
1531
|
+
const kit = kitFor(three);
|
|
1532
|
+
const vehicles = createVehiclesThree(three, { kit, fleet });
|
|
1533
|
+
const built = vehicles.build("compact_hatch", { quality: settings.damage === "none" ? "lite" : "high", seed: settings.seed });
|
|
1534
|
+
const wall = new three.Mesh(new three.BoxGeometry(0.6, 2, 6), new three.MeshStandardMaterial({ color: 7829367 }));
|
|
1535
|
+
wall.position.set(LANE.wall + 0.3, 1, LANE.z);
|
|
1536
|
+
const scene = host.scene;
|
|
1537
|
+
scene.add(built.group, wall);
|
|
1538
|
+
const mode = settings.driving === "semi" || settings.damage !== "none" ? "semi" : "arcade";
|
|
1539
|
+
const damage = settings.damage === "none" || !built.context ? null : createLaneDamage({ kit, context: built.context, frame: built.group, damage: settings.damage });
|
|
1540
|
+
const surfaces = createSurfaces2();
|
|
1541
|
+
const idle = { throttle: 0, brake: 0, steer: 0, handbrake: false };
|
|
1542
|
+
const state = { time: 0, exhaust: 0, sparks: 0, ended: false, wasBlocked: false };
|
|
1543
|
+
const arcade = createArcadeDriver2({ length: built.length, width: built.width });
|
|
1544
|
+
const semi = createSemiSimDriver2({}, { surfaces });
|
|
1545
|
+
const driver = mode === "semi" ? semi : arcade;
|
|
1546
|
+
driver.place(LANE.from, LANE.z, 0);
|
|
1547
|
+
const gap = () => LANE.wall - (driver.state.x + built.length / 2);
|
|
1548
|
+
const decel = () => mode === "semi" ? 9 * surfaces.get(settings.surface).grip : arcade.spec.brakeAgainstMotion;
|
|
1549
|
+
const brakeAt = () => lane ? lane.speed() * lane.speed() / (2 * decel()) + 3 : 3;
|
|
1550
|
+
const pose = () => {
|
|
1551
|
+
built.group.position.set(driver.state.x, 0, driver.state.z);
|
|
1552
|
+
built.group.rotation.y = driver.state.yaw + Math.PI / 2;
|
|
1553
|
+
};
|
|
1554
|
+
pose();
|
|
1555
|
+
carSound = createVehicleSound(audio(), { sounds: soundIds });
|
|
1556
|
+
carSound.start([driver.state.x, 0.5, driver.state.z]);
|
|
1557
|
+
const log = (key, value) => host?.log(fill(text, locale, text.lane[key], value.toFixed(1)));
|
|
1558
|
+
lane = {
|
|
1559
|
+
car: built,
|
|
1560
|
+
group: built.group,
|
|
1561
|
+
wall,
|
|
1562
|
+
mode,
|
|
1563
|
+
done: false,
|
|
1564
|
+
stopped: 0,
|
|
1565
|
+
speed: () => "u" in driver.state ? driver.state.u : driver.state.v,
|
|
1566
|
+
distance: () => driver.state.x - LANE.from,
|
|
1567
|
+
step(dt) {
|
|
1568
|
+
if (state.ended) {
|
|
1569
|
+
damage?.settle(dt);
|
|
1570
|
+
const at = [driver.state.x, 0.5, driver.state.z];
|
|
1571
|
+
if (carSound?.reading.running) carSound.stop(at);
|
|
1572
|
+
carSound?.update({ dt, position: at, speed: 0, throttle: 0 });
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
state.time += dt;
|
|
1576
|
+
const braking = settings.damage !== "into_wall" && gap() < brakeAt() && state.time > 0.5;
|
|
1577
|
+
const moving = state.time > LANE_START_S;
|
|
1578
|
+
const input = { ...idle, throttle: braking || !moving ? 0 : 1, brake: braking ? 1 : 0, steer: settings.damage === "into_wall" || !moving ? 0 : Math.sin(state.time * 1.4) * 0.15 };
|
|
1579
|
+
if (mode === "arcade") {
|
|
1580
|
+
const { events } = arcade.step(input, dt, { blocked: (x, z, yaw) => vehicleCorners2(arcade.spec, x, z, yaw).some(([px]) => px >= LANE.wall) });
|
|
1581
|
+
for (const e of events) {
|
|
1582
|
+
if (e.type !== "crash") continue;
|
|
1583
|
+
log("crash", e.speed);
|
|
1584
|
+
carSound?.crash(e.speed, [driver.state.x + built.length / 2, 0.5, driver.state.z]);
|
|
1585
|
+
}
|
|
1586
|
+
} else {
|
|
1587
|
+
semi.step(input, dt, { ground: () => ({ surface: settings.surface }) });
|
|
1588
|
+
const grinding = damage?.step({ dt, speed: Math.abs(lane?.speed() ?? 0), throttle: input.throttle, driver: semi }) ?? [];
|
|
1589
|
+
state.sparks -= dt;
|
|
1590
|
+
const fx2 = effects();
|
|
1591
|
+
if (fx2 && state.sparks <= 0 && Math.abs(lane?.speed() ?? 0) > 0.5) {
|
|
1592
|
+
state.sparks = grinding.length ? SPARK_EVERY_S : 0;
|
|
1593
|
+
for (const point of grinding) fx2.play({ effect: effectIds.sparks, point });
|
|
1594
|
+
}
|
|
1595
|
+
if (gap() <= 0) {
|
|
1596
|
+
state.ended = true;
|
|
1597
|
+
const crashed = damage?.crash({ speed: Math.abs(semi.state.u), direction: [Math.cos(driver.state.yaw), 0, -Math.sin(driver.state.yaw)] });
|
|
1598
|
+
if (crashed) {
|
|
1599
|
+
const parts = (type) => [...new Set(crashed.flatMap((e) => e.type === type && "part" in e ? [e.part] : []))].join(", ") || "-";
|
|
1600
|
+
host?.log(fill(text, locale, text.lane.damage, parts("dent"), parts("fracture")));
|
|
1601
|
+
}
|
|
1602
|
+
log("crash", Math.abs(semi.state.u));
|
|
1603
|
+
carSound?.crash(semi.state.u, [driver.state.x + built.length / 2, 0.5, driver.state.z]);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
pose();
|
|
1607
|
+
{
|
|
1608
|
+
const speed = Math.abs(lane?.speed() ?? 0);
|
|
1609
|
+
const semiState = mode === "semi" ? semi.state : null;
|
|
1610
|
+
carSound?.update({
|
|
1611
|
+
dt,
|
|
1612
|
+
position: [driver.state.x, 0.5, driver.state.z],
|
|
1613
|
+
speed,
|
|
1614
|
+
throttle: input.throttle,
|
|
1615
|
+
...semiState ? { rpm: semiState.rpm, slip: Math.max(0, ...semiState.slip.map(Math.abs)) } : { slip: braking && speed > 3 ? 0.6 : 0 },
|
|
1616
|
+
surface: /dirt|gravel|sand|grass|mud/.test(settings.surface) ? "dirt" : "asphalt"
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
state.exhaust -= dt;
|
|
1620
|
+
const fx = effects();
|
|
1621
|
+
if (fx && state.exhaust <= 0 && Math.abs(lane?.speed() ?? 0) > 0.5) {
|
|
1622
|
+
state.exhaust = 0.4;
|
|
1623
|
+
fx.play({ effect: effectIds.exhaust, point: [driver.state.x - Math.cos(driver.state.yaw) * (built.length / 2), 0.3, driver.state.z + Math.sin(driver.state.yaw) * (built.length / 2)], scale: Math.min(1, Math.abs(lane?.speed() ?? 0) / 17) });
|
|
1624
|
+
}
|
|
1625
|
+
if (!state.ended && state.time > LANE_START_S + 1 && Math.abs(lane?.speed() ?? 0) < 0.05) {
|
|
1626
|
+
state.ended = true;
|
|
1627
|
+
log("end", Math.max(0, gap()));
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
};
|
|
1632
|
+
const onChange = (change) => {
|
|
1633
|
+
if (change.kind === "layer") return refreshLayers();
|
|
1634
|
+
if (change.kind === "action") {
|
|
1635
|
+
if (change.id === "repaint") for (const entry of placed.values()) paintOf(entry);
|
|
1636
|
+
else if (change.id === "drive_lane") startLane();
|
|
1637
|
+
else if (change.id === "clear_lane") clearLane();
|
|
1638
|
+
else if (change.id === "honk") audio()?.play({ sound: soundIds.horn, position: lane ? [lane.group.position.x, 1, lane.group.position.z] : void 0 });
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
switch (change.id) {
|
|
1642
|
+
case "quality":
|
|
1643
|
+
settings.quality = String(change.value);
|
|
1644
|
+
break;
|
|
1645
|
+
case "paint_by_seed":
|
|
1646
|
+
settings.paint = Boolean(change.value);
|
|
1647
|
+
for (const entry of placed.values()) paintOf(entry);
|
|
1648
|
+
break;
|
|
1649
|
+
case "color_seed":
|
|
1650
|
+
settings.seed = Number(change.value);
|
|
1651
|
+
for (const entry of placed.values()) paintOf(entry);
|
|
1652
|
+
break;
|
|
1653
|
+
case "palette":
|
|
1654
|
+
settings.palette = String(change.value);
|
|
1655
|
+
for (const entry of placed.values()) paintOf(entry);
|
|
1656
|
+
break;
|
|
1657
|
+
case "driving":
|
|
1658
|
+
settings.driving = String(change.value);
|
|
1659
|
+
break;
|
|
1660
|
+
case "surface":
|
|
1661
|
+
settings.surface = String(change.value);
|
|
1662
|
+
break;
|
|
1663
|
+
case "lane_damage":
|
|
1664
|
+
settings.damage = String(change.value);
|
|
1665
|
+
break;
|
|
1666
|
+
default:
|
|
1667
|
+
break;
|
|
1668
|
+
}
|
|
1669
|
+
};
|
|
1670
|
+
const creaseQueue = createCreaseQueue();
|
|
1671
|
+
const updateLod = (entry) => {
|
|
1672
|
+
if (!host || settings.quality === "lite") return;
|
|
1673
|
+
entry.lod ?? (entry.lod = createVehicleLod(entry.root.userData.kit, entry.item.vehicle, entry.root, entry.root.children[0]));
|
|
1674
|
+
const at = host.player().position;
|
|
1675
|
+
const distance = Math.hypot(at[0] - entry.root.position.x, at[2] - entry.root.position.z);
|
|
1676
|
+
const c = entry.item.vehicle;
|
|
1677
|
+
if (c && (c.rollState || c.flipped || c.blast || c.exploded)) entry.touched = true;
|
|
1678
|
+
entry.lod.update(distance, entry.touched === true, entry.scenario !== void 0 || (entry.posedParts?.size ?? 0) > 0);
|
|
1679
|
+
};
|
|
1680
|
+
const frame = (dt) => {
|
|
1681
|
+
for (const [id, entry] of placed) {
|
|
1682
|
+
if (!entry.root.parent) {
|
|
1683
|
+
placed.delete(id);
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1686
|
+
measure(entry);
|
|
1687
|
+
updateLod(entry);
|
|
1688
|
+
}
|
|
1689
|
+
if (host?.bench && boxKey() !== helpersShown && (host.bench.layer("damageables") || host.bench.layer("zones") || helpersShown !== "")) refreshLayers();
|
|
1690
|
+
lane?.step(dt);
|
|
1691
|
+
cabins.sync(effects());
|
|
1692
|
+
};
|
|
1693
|
+
const count2 = () => {
|
|
1694
|
+
let drawCalls = 0;
|
|
1695
|
+
let triangles = 0;
|
|
1696
|
+
for (const entry of placed.values()) {
|
|
1697
|
+
entry.root.traverse((mesh) => {
|
|
1698
|
+
if (!mesh.isMesh || mesh.userData.helper) return;
|
|
1699
|
+
for (let p = mesh; p; p = p.parent) if (p.visible === false) return;
|
|
1700
|
+
drawCalls += 1;
|
|
1701
|
+
const g = mesh.geometry;
|
|
1702
|
+
triangles += (g.index ? g.index.count : g.attributes.position.count) / 3;
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
return { drawCalls, triangles: Math.round(triangles) };
|
|
1706
|
+
};
|
|
1707
|
+
const boxesOf = (material) => {
|
|
1708
|
+
let total = 0;
|
|
1709
|
+
for (const entry of placed.values()) {
|
|
1710
|
+
const kit = entry.root.userData.kit;
|
|
1711
|
+
if (kit) total += kit.damageables(entry.item.vehicle).filter((d) => d.material === material).length;
|
|
1712
|
+
}
|
|
1713
|
+
return total;
|
|
1714
|
+
};
|
|
1715
|
+
const sizeError = () => {
|
|
1716
|
+
let worst = 0;
|
|
1717
|
+
for (const entry of placed.values()) {
|
|
1718
|
+
const want = fleet.fleetDims(entry.key);
|
|
1719
|
+
const got = entry.size;
|
|
1720
|
+
if (!want || !got || entry.scenario) continue;
|
|
1721
|
+
worst = Math.max(worst, Math.abs(got.length - want.length), Math.abs(got.width - want.width), Math.abs(got.height - want.height));
|
|
1722
|
+
}
|
|
1723
|
+
return Math.round(worst * 1e3);
|
|
1724
|
+
};
|
|
1725
|
+
return {
|
|
1726
|
+
id: VEHICLES_AREA_ID,
|
|
1727
|
+
name: text.name,
|
|
1728
|
+
ground: { surface: "road" },
|
|
1729
|
+
group: text.group,
|
|
1730
|
+
get placed() {
|
|
1731
|
+
return placed;
|
|
1732
|
+
},
|
|
1733
|
+
items: () => entries.map(item),
|
|
1734
|
+
area: () => {
|
|
1735
|
+
const gap = 1.6;
|
|
1736
|
+
const WIDE = 6;
|
|
1737
|
+
const rowOf = (list, z) => {
|
|
1738
|
+
let cursor = 0;
|
|
1739
|
+
const spots = list.map((entry) => {
|
|
1740
|
+
const width = fleet.fleetDims(entry.key)?.width ?? 2;
|
|
1741
|
+
const x = cursor + width / 2;
|
|
1742
|
+
cursor += width + gap;
|
|
1743
|
+
return { id: entry.id, x };
|
|
1744
|
+
});
|
|
1745
|
+
return { spots, z, width: Math.max(0, cursor - gap) };
|
|
1746
|
+
};
|
|
1747
|
+
const front = rowOf(entries.filter((e) => (fleet.fleetDims(e.key)?.width ?? 2) <= WIDE), 0);
|
|
1748
|
+
const back = rowOf(entries.filter((e) => (fleet.fleetDims(e.key)?.width ?? 2) > WIDE), -16);
|
|
1749
|
+
const placements = [front, back].flatMap((row) => row.spots.map(({ id, x }) => ({ item: id, position: [x - row.width / 2, 0, row.z], rotationY: 0 })));
|
|
1750
|
+
return { placements, view: { eye: [0, 5, Math.max(14, front.width * 0.6)], target: [0, 1, -2] } };
|
|
1751
|
+
},
|
|
1752
|
+
controls: () => {
|
|
1753
|
+
const surfaceIds = createSurfaces2().surfaces.map((s) => s.id);
|
|
1754
|
+
return [
|
|
1755
|
+
{ kind: "choice", id: "quality", name: text.controls.quality, hint: text.hints.quality, options: ["high", "lite"].map((id) => ({ id, name: text.options.quality[id] })), default: "high" },
|
|
1756
|
+
{ kind: "boolean", id: "paint_by_seed", name: text.controls.paint_by_seed, hint: text.hints.paint_by_seed, default: false },
|
|
1757
|
+
{ kind: "number", id: "color_seed", name: text.controls.color_seed, min: 0, max: 999, step: 1, default: 1 },
|
|
1758
|
+
{ kind: "choice", id: "palette", name: text.controls.palette, hint: text.hints.palette, options: ["auto", "civil", "military"].map((id) => ({ id, name: text.options.palette[id] })), default: "auto" },
|
|
1759
|
+
{ kind: "choice", id: "driving", name: text.controls.driving, hint: text.hints.driving, options: ["arcade", "semi"].map((id) => ({ id, name: text.options.driving[id] })), default: "arcade" },
|
|
1760
|
+
{ kind: "choice", id: "surface", name: text.controls.surface, hint: text.hints.surface, options: surfaceIds.map((id) => ({ id, name: text.options.surface[id] ?? id })), default: "dry_asphalt" },
|
|
1761
|
+
{ kind: "choice", id: "lane_damage", name: text.controls.lane_damage, hint: text.hints.lane_damage, options: LANE_DAMAGE.map((id) => ({ id, name: text.options.lane_damage[id] ?? id })), default: "none" },
|
|
1762
|
+
{ kind: "action", id: "repaint", name: text.actions.repaint },
|
|
1763
|
+
{ kind: "action", id: "drive_lane", name: text.actions.drive_lane },
|
|
1764
|
+
{ kind: "action", id: "clear_lane", name: text.actions.clear_lane },
|
|
1765
|
+
{ kind: "action", id: "honk", name: text.actions.honk }
|
|
1766
|
+
];
|
|
1767
|
+
},
|
|
1768
|
+
layers: () => ["damageables", "zones", "axes"].map((id) => ({ id, name: text.layers[id], default: false })),
|
|
1769
|
+
metrics: () => [
|
|
1770
|
+
{ id: "vehicles", name: text.metrics.vehicles, read: () => placed.size },
|
|
1771
|
+
{ id: "triangles", name: text.metrics.triangles, read: () => count2().triangles },
|
|
1772
|
+
{ id: "draw_calls", name: text.metrics.draw_calls, read: () => count2().drawCalls },
|
|
1773
|
+
{ id: "size_error_mm", name: text.metrics.size_error_mm, unit: "mm", read: sizeError },
|
|
1774
|
+
{ id: "metal", name: text.metrics.metal, read: () => boxesOf("metal") },
|
|
1775
|
+
{ id: "glass", name: text.metrics.glass, read: () => boxesOf("glass") },
|
|
1776
|
+
{ id: "rubber", name: text.metrics.rubber, read: () => boxesOf("rubber") },
|
|
1777
|
+
{ id: "fuel", name: text.metrics.fuel, read: () => boxesOf("fuel") },
|
|
1778
|
+
{ id: "speed", name: text.metrics.speed, unit: "km/h", digits: 0, read: () => Math.round(Math.abs(lane?.speed() ?? 0) * 3.6) },
|
|
1779
|
+
{ id: "lane", name: text.metrics.lane, unit: "m", digits: 1, read: () => lane?.distance() ?? 0 }
|
|
1780
|
+
],
|
|
1781
|
+
scenarios: () => createScenarios({ text, locale }),
|
|
1782
|
+
attach(lent) {
|
|
1783
|
+
host = lent;
|
|
1784
|
+
three = lent.THREE;
|
|
1785
|
+
const stopChange = lent.bench?.onChange(onChange);
|
|
1786
|
+
const stopFrame = lent.onFrame(frame);
|
|
1787
|
+
const stopBlocker = lent.addBlocker?.(blocked);
|
|
1788
|
+
const stopCrease = lent.defer?.(creaseQueue.deferredQueue(), DEFERRED_PRIORITY[DEFERRED_QUEUE_ID.VEHICLE_CREASE]);
|
|
1789
|
+
if (isRenderPools(lent.pools)) lent.pools.materials.register("vehicles.cracked-glass", () => crackedGlassMaterial(three, new three.DataTexture(new Uint8Array(4), 1, 1)));
|
|
1790
|
+
lent.audio?.register?.(LIBRARY_BANK);
|
|
1791
|
+
lent.audio?.register?.(VEHICLE_SOUND_BANK);
|
|
1792
|
+
return () => {
|
|
1793
|
+
stopChange?.();
|
|
1794
|
+
stopFrame();
|
|
1795
|
+
stopBlocker?.();
|
|
1796
|
+
stopCrease?.();
|
|
1797
|
+
cabins.clear(effects());
|
|
1798
|
+
clearHelpers();
|
|
1799
|
+
clearLane();
|
|
1800
|
+
host = null;
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
function loadVehiclesTestModule(room, options = {}) {
|
|
1806
|
+
let three = options.three;
|
|
1807
|
+
if (!three) {
|
|
1808
|
+
room.loadModule({ id: "vehicles_probe", name: "Vehicles probe", group: "Vehicles", items: () => [], attach: (lent) => {
|
|
1809
|
+
three = lent.THREE;
|
|
1810
|
+
return () => void 0;
|
|
1811
|
+
} });
|
|
1812
|
+
}
|
|
1813
|
+
return room.loadModule(createVehiclesTestModule({ ...options, three }));
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// src/test-area/proving/module.ts
|
|
1817
|
+
import { createVehicleCamera, mirrorPoses } from "@adatechnology/game-camera";
|
|
1818
|
+
import { markKitOrigin as markKitOrigin3 } from "@adatechnology/game-contracts";
|
|
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
|
|
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
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// src/test-area/proving/text.ts
|
|
2329
|
+
var PROVING_TEXT = {
|
|
2330
|
+
"pt-BR": {
|
|
2331
|
+
name: "Banco de provas",
|
|
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" },
|
|
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",
|
|
2336
|
+
vehicle: "Ve\xEDculo da frota que vai para a pista",
|
|
2337
|
+
test: "Prova medida que o bot\xE3o Rodar prova executa",
|
|
2338
|
+
camera: "Sala: a c\xE2mera do jogador. Persegui\xE7\xE3o, cabine e cap\xF4 seguem o ve\xEDculo",
|
|
2339
|
+
time_scale: "Quantas vezes mais r\xE1pido que o tempo real a prova roda",
|
|
2340
|
+
visual_check: "O que voc\xEA olhou e vai aprovar ou reprovar"
|
|
2341
|
+
},
|
|
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" },
|
|
2343
|
+
layers: { mirrors: "Retrovisores", reverse_camera: "C\xE2mera de r\xE9", scoreboard: "Painel da ficha" },
|
|
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.)" },
|
|
2345
|
+
cameras: { room: "Sala", chase: "Persegui\xE7\xE3o (3\xAA pessoa)", cockpit: "Cabine (1\xAA pessoa)", hood: "Cap\xF4" },
|
|
2346
|
+
tests: {
|
|
2347
|
+
accel_0_100: "0 a 100 km/h",
|
|
2348
|
+
top_speed: "Velocidade m\xE1xima",
|
|
2349
|
+
brake_100_0: "Frenagem 100 a 0",
|
|
2350
|
+
turn_circle: "Di\xE2metro de giro",
|
|
2351
|
+
wet_brake_80_0: "Frenagem molhada 80 a 0",
|
|
2352
|
+
mud_traction: "Tra\xE7\xE3o na lama",
|
|
2353
|
+
cabin: "Cabine e personagem",
|
|
2354
|
+
cameras: "C\xE2meras",
|
|
2355
|
+
mirrors: "Retrovisores e r\xE9",
|
|
2356
|
+
terrain: "Terrenos",
|
|
2357
|
+
wind_tunnel: "T\xFAnel de vento",
|
|
2358
|
+
crash_test: "Crash test",
|
|
2359
|
+
pedestrian: "Atropelamento",
|
|
2360
|
+
performance: "Desempenho",
|
|
2361
|
+
buoyancy: "Flutua\xE7\xE3o",
|
|
2362
|
+
flight: "Voo"
|
|
2363
|
+
},
|
|
2364
|
+
status: { approved: "APROVADO", rejected: "REPROVADO", pending: "PENDENTE" },
|
|
2365
|
+
verdict: { pass: "ok", fail: "falhou", pending: "\u2026" },
|
|
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
|
+
}
|
|
2393
|
+
},
|
|
2394
|
+
en: {
|
|
2395
|
+
name: "Proving ground",
|
|
2396
|
+
controls: { vehicle: "Vehicle", test: "Test", camera: "Camera", time_scale: "Simulation speed", visual_check: "Visual check", gearbox: "Gearbox" },
|
|
2397
|
+
gearbox: { automatic: "Automatic", manual: "Manual" },
|
|
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",
|
|
2400
|
+
vehicle: "Fleet vehicle that goes on the track",
|
|
2401
|
+
test: "Measured test the Run test button runs",
|
|
2402
|
+
camera: "Room: the player camera. Chase, cockpit and hood follow the vehicle",
|
|
2403
|
+
time_scale: "How many times faster than real time the test runs",
|
|
2404
|
+
visual_check: "What you looked at and will approve or refuse"
|
|
2405
|
+
},
|
|
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" },
|
|
2407
|
+
layers: { mirrors: "Mirrors", reverse_camera: "Reversing camera", scoreboard: "Certificate board" },
|
|
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)" },
|
|
2409
|
+
cameras: { room: "Room", chase: "Chase (third person)", cockpit: "Cockpit (first person)", hood: "Hood" },
|
|
2410
|
+
tests: {
|
|
2411
|
+
accel_0_100: "0 to 100 km/h",
|
|
2412
|
+
top_speed: "Top speed",
|
|
2413
|
+
brake_100_0: "Braking 100 to 0",
|
|
2414
|
+
turn_circle: "Turning circle",
|
|
2415
|
+
wet_brake_80_0: "Wet braking 80 to 0",
|
|
2416
|
+
mud_traction: "Mud traction",
|
|
2417
|
+
cabin: "Cabin and character",
|
|
2418
|
+
cameras: "Cameras",
|
|
2419
|
+
mirrors: "Mirrors and reversing",
|
|
2420
|
+
terrain: "Terrain",
|
|
2421
|
+
wind_tunnel: "Wind tunnel",
|
|
2422
|
+
crash_test: "Crash test",
|
|
2423
|
+
pedestrian: "Pedestrian impact",
|
|
2424
|
+
performance: "Performance",
|
|
2425
|
+
buoyancy: "Buoyancy",
|
|
2426
|
+
flight: "Flight"
|
|
2427
|
+
},
|
|
2428
|
+
status: { approved: "APPROVED", rejected: "REJECTED", pending: "PENDING" },
|
|
2429
|
+
verdict: { pass: "ok", fail: "failed", pending: "\u2026" },
|
|
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
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
};
|
|
2459
|
+
var fillText = (template, ...values) => template.replace(/\{(\d+)\}/g, (_, i) => String(values[Number(i)] ?? ""));
|
|
2460
|
+
|
|
2461
|
+
// src/test-area/proving/module.ts
|
|
2462
|
+
var PROVING_AREA_ID = "vehicles_proving";
|
|
2463
|
+
var TRACK = { x0: -30, z: 22, tile: 260, width: 14, start: 4 };
|
|
2464
|
+
var SURFACE_LOOK = { dry_asphalt: { color: 2829617, roughness: 0.95 }, wet_asphalt: { color: 1842980, roughness: 0.18 }, mud: { color: 4863268, roughness: 1 } };
|
|
2465
|
+
var VISUAL_CHECKS = ["cabin", "cameras", "mirrors"];
|
|
2466
|
+
var MIRROR_RECTS = {
|
|
2467
|
+
rear_view: { x: 0.38, y: 0.02, width: 0.24, height: 0.09, rounded: 0.35 },
|
|
2468
|
+
left: { x: 0.02, y: 0.72, width: 0.19, height: 0.13, rounded: 0.3 },
|
|
2469
|
+
right: { x: 0.79, y: 0.72, width: 0.19, height: 0.13, rounded: 0.3 },
|
|
2470
|
+
reverse: { x: 0.36, y: 0.13, width: 0.28, height: 0.18, rounded: 0.06 }
|
|
2471
|
+
};
|
|
2472
|
+
function createProvingModule(options) {
|
|
2473
|
+
const locale = options.locale ?? "pt-BR";
|
|
2474
|
+
const text = PROVING_TEXT[locale] ?? PROVING_TEXT["pt-BR"];
|
|
2475
|
+
const tests = options.tests ?? createProvingTests();
|
|
2476
|
+
const vehicles = options.vehicles;
|
|
2477
|
+
const sounds = { ...defaultSoundIds, ...options.sounds };
|
|
2478
|
+
const measured = tests.tests.filter((t) => t.kind === "measured").map((t) => t.id);
|
|
2479
|
+
const settings = { vehicle: vehicles.includes("compact_hatch") ? "compact_hatch" : vehicles[0] ?? "compact_hatch", test: measured[0] ?? "accel_0_100", camera: "chase", timeScale: 4, visual: "cameras" };
|
|
2480
|
+
const certificates = /* @__PURE__ */ new Map();
|
|
2481
|
+
const kits = /* @__PURE__ */ new WeakMap();
|
|
2482
|
+
let host = null;
|
|
2483
|
+
let three = null;
|
|
2484
|
+
let track = null;
|
|
2485
|
+
let strip = null;
|
|
2486
|
+
let board = null;
|
|
2487
|
+
let car = null;
|
|
2488
|
+
let run = null;
|
|
2489
|
+
let parking = null;
|
|
2490
|
+
let driving = null;
|
|
2491
|
+
let gearbox = "automatic";
|
|
2492
|
+
let lastGear = null;
|
|
2493
|
+
let driveClock = 0;
|
|
2494
|
+
let queue = [];
|
|
2495
|
+
let open = false;
|
|
2496
|
+
let lastWrap = 0;
|
|
2497
|
+
let engine;
|
|
2498
|
+
const rig = { camera: null, vehicle: null, mirrors: [] };
|
|
2499
|
+
const name = (id) => options.nameOf?.(id) ?? id.replace(/_/g, " ");
|
|
2500
|
+
const kitFor = (THREE) => {
|
|
2501
|
+
let kit = kits.get(THREE);
|
|
2502
|
+
if (!kit) {
|
|
2503
|
+
kit = options.createKit(THREE);
|
|
2504
|
+
kits.set(THREE, kit);
|
|
2505
|
+
}
|
|
2506
|
+
return kit;
|
|
2507
|
+
};
|
|
2508
|
+
const subjectOf = (id) => {
|
|
2509
|
+
const mass = three ? kitFor(three).find(id)?.mass : void 0;
|
|
2510
|
+
return options.subject(id, mass);
|
|
2511
|
+
};
|
|
2512
|
+
const blank = (subject) => {
|
|
2513
|
+
const results = tests.forDomain(subject.domain).map((t) => ({ test: t.id, kind: t.kind, unit: t.unit, range: t.range?.(subject) ?? {}, value: null, verdict: "pending", note: t.kind === "measured" ? "not run" : t.kind === "visual" ? "awaiting sign-off" : "room not built yet" }));
|
|
2514
|
+
return { vehicle: subject.id, domain: subject.domain, category: subject.category, status: statusOf(results), results };
|
|
2515
|
+
};
|
|
2516
|
+
const certificateOf = (id) => {
|
|
2517
|
+
if (!certificates.has(id)) {
|
|
2518
|
+
const subject = subjectOf(id);
|
|
2519
|
+
if (subject) certificates.set(id, blank(subject));
|
|
2520
|
+
}
|
|
2521
|
+
return certificates.get(id);
|
|
2522
|
+
};
|
|
2523
|
+
const store = (certificate) => {
|
|
2524
|
+
certificates.set(certificate.vehicle, certificate);
|
|
2525
|
+
options.onCertificate?.(certificate);
|
|
2526
|
+
drawBoard();
|
|
2527
|
+
};
|
|
2528
|
+
const buildTrack = () => {
|
|
2529
|
+
if (!host || track) return;
|
|
2530
|
+
const THREE = three;
|
|
2531
|
+
track = markKitOrigin3(new THREE.Group(), { owner: "@adatechnology/game-vehicles-three", kind: "room", id: "proving_track" });
|
|
2532
|
+
track.name = "proving_track";
|
|
2533
|
+
const look = SURFACE_LOOK.dry_asphalt;
|
|
2534
|
+
strip = new THREE.Mesh(new THREE.PlaneGeometry(TRACK.tile + 80, TRACK.width), new THREE.MeshStandardMaterial({ color: look.color, roughness: look.roughness, metalness: 0.05 }));
|
|
2535
|
+
strip.rotation.x = -Math.PI / 2;
|
|
2536
|
+
strip.position.set(TRACK.x0 + TRACK.tile / 2, 0.012, TRACK.z);
|
|
2537
|
+
strip.receiveShadow = true;
|
|
2538
|
+
track.add(strip);
|
|
2539
|
+
const paint = new THREE.MeshBasicMaterial({ color: 15263970 });
|
|
2540
|
+
const yellow = new THREE.MeshBasicMaterial({ color: 15909424 });
|
|
2541
|
+
const dash = new THREE.PlaneGeometry(3, 0.14);
|
|
2542
|
+
const edge = new THREE.PlaneGeometry(TRACK.tile + 80, 0.16);
|
|
2543
|
+
for (const side of [-1, 1]) {
|
|
2544
|
+
const line = new THREE.Mesh(edge, paint);
|
|
2545
|
+
line.rotation.x = -Math.PI / 2;
|
|
2546
|
+
line.position.set(TRACK.x0 + TRACK.tile / 2, 0.016, TRACK.z + side * (TRACK.width / 2 - 0.4));
|
|
2547
|
+
track.add(line);
|
|
2548
|
+
}
|
|
2549
|
+
for (let x = TRACK.x0 - 40; x < TRACK.x0 + TRACK.tile + 40; x += 9) {
|
|
2550
|
+
const d = new THREE.Mesh(dash, paint);
|
|
2551
|
+
d.rotation.x = -Math.PI / 2;
|
|
2552
|
+
d.position.set(x, 0.016, TRACK.z - 1.5);
|
|
2553
|
+
track.add(d);
|
|
2554
|
+
}
|
|
2555
|
+
const post = new THREE.BoxGeometry(0.12, 1.4, 0.12);
|
|
2556
|
+
const plate = new THREE.BoxGeometry(0.06, 0.5, 0.9);
|
|
2557
|
+
for (let m = 0; m <= TRACK.tile; m += 50) {
|
|
2558
|
+
const p = new THREE.Mesh(post, yellow);
|
|
2559
|
+
p.position.set(TRACK.x0 + m, 0.7, TRACK.z - TRACK.width / 2 - 0.6);
|
|
2560
|
+
const q = new THREE.Mesh(plate, m === 0 ? yellow : paint);
|
|
2561
|
+
q.position.set(TRACK.x0 + m, 1.45, TRACK.z - TRACK.width / 2 - 0.6);
|
|
2562
|
+
track.add(p, q);
|
|
2563
|
+
}
|
|
2564
|
+
const startLine = new THREE.Mesh(new THREE.PlaneGeometry(0.5, TRACK.width - 1), yellow);
|
|
2565
|
+
startLine.rotation.x = -Math.PI / 2;
|
|
2566
|
+
startLine.position.set(TRACK.x0, 0.017, TRACK.z);
|
|
2567
|
+
track.add(startLine);
|
|
2568
|
+
host.scene.add(track);
|
|
2569
|
+
buildBoard();
|
|
2570
|
+
};
|
|
2571
|
+
const setSurface = (surface) => {
|
|
2572
|
+
if (!strip) return;
|
|
2573
|
+
const look = SURFACE_LOOK[surface] ?? SURFACE_LOOK.dry_asphalt;
|
|
2574
|
+
strip.material.color.setHex(look.color);
|
|
2575
|
+
strip.material.roughness = look.roughness;
|
|
2576
|
+
};
|
|
2577
|
+
const buildBoard = () => {
|
|
2578
|
+
if (!host || board || typeof document === "undefined") return;
|
|
2579
|
+
const THREE = three;
|
|
2580
|
+
const canvas = document.createElement("canvas");
|
|
2581
|
+
canvas.width = 1024;
|
|
2582
|
+
canvas.height = 640;
|
|
2583
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
2584
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
2585
|
+
const mesh = markKitOrigin3(new THREE.Mesh(new THREE.PlaneGeometry(9.6, 6), new THREE.MeshBasicMaterial({ map: texture, toneMapped: false })), { owner: "@adatechnology/game-vehicles-three", kind: "room", id: "proving_board" });
|
|
2586
|
+
mesh.position.set(TRACK.x0 + 6, 3.6, TRACK.z - TRACK.width / 2 - 3);
|
|
2587
|
+
const legs = new THREE.Mesh(new THREE.BoxGeometry(9.8, 0.2, 0.2), new THREE.MeshStandardMaterial({ color: 1842981 }));
|
|
2588
|
+
legs.position.set(0, -3.1, -0.05);
|
|
2589
|
+
const stand = new THREE.Mesh(new THREE.BoxGeometry(0.25, 3.4, 0.25), legs.material);
|
|
2590
|
+
stand.position.set(0, -4.6, -0.1);
|
|
2591
|
+
mesh.add(legs, stand);
|
|
2592
|
+
track.add(mesh);
|
|
2593
|
+
board = { mesh, canvas, texture };
|
|
2594
|
+
drawBoard();
|
|
2595
|
+
};
|
|
2596
|
+
const fmtRange = (r) => r.min == null && r.max == null ? "\u2014" : `${r.min ?? "\u2026"}\u2013${r.max ?? "\u2026"}`;
|
|
2597
|
+
const drawBoard = () => {
|
|
2598
|
+
if (!board) return;
|
|
2599
|
+
const cert = certificateOf(settings.vehicle);
|
|
2600
|
+
const g = board.canvas.getContext("2d");
|
|
2601
|
+
if (!g || !cert) return;
|
|
2602
|
+
const W = board.canvas.width;
|
|
2603
|
+
g.fillStyle = "#16181c";
|
|
2604
|
+
g.fillRect(0, 0, W, board.canvas.height);
|
|
2605
|
+
g.fillStyle = "#f2c230";
|
|
2606
|
+
g.fillRect(0, 0, W, 10);
|
|
2607
|
+
g.font = "600 26px monospace";
|
|
2608
|
+
g.fillStyle = "#8d95a3";
|
|
2609
|
+
g.fillText(text.board.title.toUpperCase(), 32, 50);
|
|
2610
|
+
g.font = "700 46px sans-serif";
|
|
2611
|
+
g.fillStyle = "#e7e9ec";
|
|
2612
|
+
g.fillText(name(cert.vehicle), 32, 102);
|
|
2613
|
+
const colour = { approved: "#5cc98a", rejected: "#ef6a5b", pending: "#a7a0f0" }[cert.status];
|
|
2614
|
+
g.font = "700 34px sans-serif";
|
|
2615
|
+
const status = text.status[cert.status];
|
|
2616
|
+
const sw = g.measureText(status).width + 32;
|
|
2617
|
+
g.strokeStyle = colour;
|
|
2618
|
+
g.lineWidth = 3;
|
|
2619
|
+
g.strokeRect(W - sw - 32, 66, sw, 48);
|
|
2620
|
+
g.fillStyle = colour;
|
|
2621
|
+
g.fillText(status, W - sw - 16, 102);
|
|
2622
|
+
g.font = "22px monospace";
|
|
2623
|
+
g.fillStyle = "#8d95a3";
|
|
2624
|
+
const cols = [32, 430, 640, 860];
|
|
2625
|
+
[text.board.test, text.board.value, text.board.range, text.board.verdict].forEach((h, i) => g.fillText(h.toUpperCase(), cols[i], 150));
|
|
2626
|
+
let y = 190;
|
|
2627
|
+
for (const r of cert.results) {
|
|
2628
|
+
const live2 = run && run.test.id === r.test && !run.done;
|
|
2629
|
+
g.fillStyle = "#e7e9ec";
|
|
2630
|
+
g.font = "26px sans-serif";
|
|
2631
|
+
g.fillText(text.tests[r.test] ?? r.test, cols[0], y);
|
|
2632
|
+
g.font = "26px monospace";
|
|
2633
|
+
g.fillStyle = "#5fd4e0";
|
|
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}`;
|
|
2635
|
+
g.fillText(value, cols[1], y);
|
|
2636
|
+
g.fillStyle = "#8d95a3";
|
|
2637
|
+
g.fillText(r.kind === "measured" ? fmtRange(r.range) : r.kind === "visual" ? "visual" : "sala", cols[2], y);
|
|
2638
|
+
g.fillStyle = { pass: "#5cc98a", fail: "#ef6a5b", pending: "#a7a0f0" }[r.verdict];
|
|
2639
|
+
g.fillText(r.verdict === "pass" ? "\u2713 " + text.verdict.pass : r.verdict === "fail" ? "\u2717 " + text.verdict.fail : "\u2026", cols[3], y);
|
|
2640
|
+
y += 30;
|
|
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
|
+
}
|
|
2648
|
+
board.texture.needsUpdate = true;
|
|
2649
|
+
};
|
|
2650
|
+
const removeCar = () => {
|
|
2651
|
+
if (!car) return;
|
|
2652
|
+
discardObject3D3(car.built.group);
|
|
2653
|
+
car = null;
|
|
2654
|
+
};
|
|
2655
|
+
const placeCar = () => {
|
|
2656
|
+
if (!host || !three) return;
|
|
2657
|
+
const subject = subjectOf(settings.vehicle);
|
|
2658
|
+
if (!subject) return;
|
|
2659
|
+
removeCar();
|
|
2660
|
+
const built = createVehiclesThree(three, { kit: kitFor(three) }).build(settings.vehicle, { quality: "high" });
|
|
2661
|
+
const guides = reverseGuides(built);
|
|
2662
|
+
built.group.add(guides);
|
|
2663
|
+
host.scene.add(built.group);
|
|
2664
|
+
car = { built, guides, subject };
|
|
2665
|
+
pose({ x: 0, z: TRACK.start, yaw: 0, pitch: 0, roll: 0, height: 0 }, 0);
|
|
2666
|
+
rig.vehicle?.reset();
|
|
2667
|
+
drawBoard();
|
|
2668
|
+
};
|
|
2669
|
+
const reverseGuides = (built) => {
|
|
2670
|
+
const THREE = three;
|
|
2671
|
+
const group = new THREE.Group();
|
|
2672
|
+
group.name = "reverse_guides";
|
|
2673
|
+
const half = built.width / 2 - 0.05;
|
|
2674
|
+
const tail = -built.length / 2;
|
|
2675
|
+
const bands = [
|
|
2676
|
+
[0.2, 1, 15682107],
|
|
2677
|
+
[1, 2, 15909424],
|
|
2678
|
+
[2, 3.2, 6080906]
|
|
2679
|
+
];
|
|
2680
|
+
for (const [a, b, colour] of bands) {
|
|
2681
|
+
const material = new THREE.MeshBasicMaterial({ color: colour, transparent: true, opacity: 0.85, depthTest: false });
|
|
2682
|
+
for (const side of [-1, 1]) {
|
|
2683
|
+
const bar = new THREE.Mesh(new THREE.PlaneGeometry(0.06, b - a), material);
|
|
2684
|
+
bar.rotation.x = -Math.PI / 2;
|
|
2685
|
+
bar.position.set(side * half, 0.03, tail - (a + b) / 2);
|
|
2686
|
+
bar.userData.side = side;
|
|
2687
|
+
bar.userData.mid = (a + b) / 2;
|
|
2688
|
+
group.add(bar);
|
|
2689
|
+
}
|
|
2690
|
+
const cross = new THREE.Mesh(new THREE.PlaneGeometry(half * 2, 0.05), material);
|
|
2691
|
+
cross.rotation.x = -Math.PI / 2;
|
|
2692
|
+
cross.position.set(0, 0.03, tail - a);
|
|
2693
|
+
cross.userData.mid = a;
|
|
2694
|
+
group.add(cross);
|
|
2695
|
+
}
|
|
2696
|
+
group.traverse((o) => o.layers.set(1));
|
|
2697
|
+
return group;
|
|
2698
|
+
};
|
|
2699
|
+
const bendGuides = (steer) => {
|
|
2700
|
+
if (!car) return;
|
|
2701
|
+
for (const bar of car.guides.children) {
|
|
2702
|
+
const d = bar.userData.mid;
|
|
2703
|
+
const offset = -steer * 0.18 * d * d;
|
|
2704
|
+
bar.position.x = (bar.userData.side ?? 0) * (car.built.width / 2 - 0.05) + offset;
|
|
2705
|
+
}
|
|
2706
|
+
};
|
|
2707
|
+
const pose = (s, steer) => {
|
|
2708
|
+
if (!car) return;
|
|
2709
|
+
const wrapped = (s.x % TRACK.tile + TRACK.tile) % TRACK.tile;
|
|
2710
|
+
if (Math.abs(wrapped - lastWrap) > TRACK.tile / 2) rig.vehicle?.reset();
|
|
2711
|
+
lastWrap = wrapped;
|
|
2712
|
+
car.built.group.position.set(TRACK.x0 + wrapped, 0, TRACK.z + s.z);
|
|
2713
|
+
car.built.group.rotation.set(-s.pitch, s.yaw + Math.PI / 2, s.roll, "YXZ");
|
|
2714
|
+
bendGuides(steer);
|
|
2715
|
+
};
|
|
2716
|
+
const frameOf = (speed) => {
|
|
2717
|
+
if (!car) return null;
|
|
2718
|
+
const g = car.built.group;
|
|
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 } : {} };
|
|
2720
|
+
};
|
|
2721
|
+
const applyCamera = () => {
|
|
2722
|
+
if (!host?.setViewCamera) return;
|
|
2723
|
+
host.setViewCamera(settings.camera === "room" ? null : rig.camera);
|
|
2724
|
+
if (settings.camera !== "room") rig.vehicle?.setMode(settings.camera);
|
|
2725
|
+
};
|
|
2726
|
+
const refreshMirrors = () => {
|
|
2727
|
+
for (const m of rig.mirrors) m.remove();
|
|
2728
|
+
rig.mirrors = [];
|
|
2729
|
+
if (!host?.addView || !car) return;
|
|
2730
|
+
const poses = mirrorPoses(car.built);
|
|
2731
|
+
const want = (id) => id === "reverse" ? !!host?.bench?.layer("reverse_camera") : !!host?.bench?.layer("mirrors");
|
|
2732
|
+
for (const pose2 of poses) {
|
|
2733
|
+
if (!want(pose2.id)) continue;
|
|
2734
|
+
const camera = new three.PerspectiveCamera(pose2.fov, 2, 0.05, 400);
|
|
2735
|
+
if (pose2.id === "reverse") camera.layers.enable(1);
|
|
2736
|
+
const rect = MIRROR_RECTS[pose2.id];
|
|
2737
|
+
const remove = host.addView({ camera, rect, mirror: pose2.mirror, rounded: rect.rounded, label: pose2.id, resolution: pose2.id === "reverse" ? 0.6 : 0.45 });
|
|
2738
|
+
rig.mirrors.push({ pose: pose2, camera, remove });
|
|
2739
|
+
}
|
|
2740
|
+
};
|
|
2741
|
+
let mirrorKey = "";
|
|
2742
|
+
const stopEngine = () => {
|
|
2743
|
+
engine?.dispose();
|
|
2744
|
+
engine = void 0;
|
|
2745
|
+
};
|
|
2746
|
+
const startTest = (id) => {
|
|
2747
|
+
const test = tests.get(id);
|
|
2748
|
+
if (!host || !test || test.kind !== "measured") return;
|
|
2749
|
+
if (!car || car.subject.id !== settings.vehicle) placeCar();
|
|
2750
|
+
if (!car) return;
|
|
2751
|
+
parking = null;
|
|
2752
|
+
run = startRun(test, car.subject);
|
|
2753
|
+
setSurface(run.surface);
|
|
2754
|
+
if (id === "turn_circle") run.driver.place(0, TRACK.start, 0);
|
|
2755
|
+
rig.vehicle?.reset();
|
|
2756
|
+
stopEngine();
|
|
2757
|
+
engine = createVehicleSound(host.audio, { sounds });
|
|
2758
|
+
engine.start([car.built.group.position.x, 0.5, car.built.group.position.z]);
|
|
2759
|
+
host.log(fillText(text.log.start, name(settings.vehicle), text.tests[id] ?? id));
|
|
2760
|
+
drawBoard();
|
|
2761
|
+
};
|
|
2762
|
+
const finishTest = () => {
|
|
2763
|
+
if (!run || !car) return;
|
|
2764
|
+
const result = run.result();
|
|
2765
|
+
const cert = certificateOf(car.subject.id);
|
|
2766
|
+
if (cert) store(withResult(cert, result));
|
|
2767
|
+
host?.log(fillText(text.log.result, name(car.subject.id), text.tests[result.test] ?? result.test, result.value ?? "\u2014", result.unit, fmtRange(result.range), result.verdict), { vehicle: car.subject.id, ...result, trace: void 0 }, "proving_result");
|
|
2768
|
+
run = null;
|
|
2769
|
+
stopEngine();
|
|
2770
|
+
const next = queue.shift();
|
|
2771
|
+
if (next) startTest(next);
|
|
2772
|
+
else setSurface("dry_asphalt");
|
|
2773
|
+
};
|
|
2774
|
+
const startParking = () => {
|
|
2775
|
+
if (!host) return;
|
|
2776
|
+
if (!car || car.subject.id !== settings.vehicle) placeCar();
|
|
2777
|
+
if (!car) return;
|
|
2778
|
+
run = null;
|
|
2779
|
+
queue = [];
|
|
2780
|
+
setSurface("dry_asphalt");
|
|
2781
|
+
const driver = createSemiSimDriver4(subjectSpec2(car.subject));
|
|
2782
|
+
driver.place(40, 0, 0);
|
|
2783
|
+
parking = { driver, time: 0 };
|
|
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);
|
|
2882
|
+
const onChange = (change) => {
|
|
2883
|
+
if (change.kind === "layer") {
|
|
2884
|
+
if (change.id === "scoreboard" && board) board.mesh.visible = !!host?.bench?.layer("scoreboard");
|
|
2885
|
+
return refreshMirrors();
|
|
2886
|
+
}
|
|
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();
|
|
2890
|
+
if (change.id === "run_test") {
|
|
2891
|
+
queue = [];
|
|
2892
|
+
startTest(settings.test);
|
|
2893
|
+
} else if (change.id === "run_all") {
|
|
2894
|
+
queue = measured.slice(1);
|
|
2895
|
+
startTest(measured[0]);
|
|
2896
|
+
} else if (change.id === "certify_now") {
|
|
2897
|
+
const subject = subjectOf(settings.vehicle);
|
|
2898
|
+
if (!subject) return;
|
|
2899
|
+
const prior = certificateOf(subject.id);
|
|
2900
|
+
const signOffs = (prior?.results ?? []).filter((r) => r.kind !== "measured" && r.verdict !== "pending").map((r) => ({ vehicle: subject.id, test: r.test, ok: r.verdict === "pass", by: r.signedBy ?? "test room" }));
|
|
2901
|
+
const cert = certify(subject, { tests, signOffs });
|
|
2902
|
+
store(cert);
|
|
2903
|
+
host?.log(fillText(text.log.certified, name(subject.id), text.status[cert.status]), { vehicle: subject.id, status: cert.status }, "proving_certificate");
|
|
2904
|
+
} else if (change.id === "reverse_park") startParking();
|
|
2905
|
+
else if (change.id === "sign_ok" || change.id === "sign_fail") {
|
|
2906
|
+
const cert = certificateOf(settings.vehicle);
|
|
2907
|
+
if (!cert) return;
|
|
2908
|
+
const ok = change.id === "sign_ok";
|
|
2909
|
+
store(withResult(cert, { test: settings.visual, kind: "visual", unit: "", range: {}, value: null, verdict: ok ? "pass" : "fail", signedBy: "test room" }));
|
|
2910
|
+
host?.log(fillText(text.log.signed, name(settings.vehicle), text.tests[settings.visual] ?? settings.visual, ok ? text.verdict.pass : text.verdict.fail));
|
|
2911
|
+
} else if (change.id === "reset") {
|
|
2912
|
+
const subject = subjectOf(settings.vehicle);
|
|
2913
|
+
if (subject) store(blank(subject));
|
|
2914
|
+
}
|
|
2915
|
+
return;
|
|
2916
|
+
}
|
|
2917
|
+
switch (change.id) {
|
|
2918
|
+
case "vehicle":
|
|
2919
|
+
settings.vehicle = String(change.value);
|
|
2920
|
+
stopDriving();
|
|
2921
|
+
run = null;
|
|
2922
|
+
queue = [];
|
|
2923
|
+
parking = null;
|
|
2924
|
+
stopEngine();
|
|
2925
|
+
placeCar();
|
|
2926
|
+
refreshMirrors();
|
|
2927
|
+
break;
|
|
2928
|
+
case "test":
|
|
2929
|
+
settings.test = String(change.value);
|
|
2930
|
+
break;
|
|
2931
|
+
case "camera":
|
|
2932
|
+
settings.camera = String(change.value);
|
|
2933
|
+
applyCamera();
|
|
2934
|
+
break;
|
|
2935
|
+
case "time_scale":
|
|
2936
|
+
settings.timeScale = Number(change.value);
|
|
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;
|
|
2942
|
+
case "visual_check":
|
|
2943
|
+
settings.visual = String(change.value);
|
|
2944
|
+
break;
|
|
2945
|
+
default:
|
|
2946
|
+
break;
|
|
2947
|
+
}
|
|
2948
|
+
};
|
|
2949
|
+
const frame = (dt) => {
|
|
2950
|
+
if (!host || !open) return;
|
|
2951
|
+
if (!track) buildTrack();
|
|
2952
|
+
if (!car) {
|
|
2953
|
+
placeCar();
|
|
2954
|
+
refreshMirrors();
|
|
2955
|
+
}
|
|
2956
|
+
let speed = 0;
|
|
2957
|
+
let steer = 0;
|
|
2958
|
+
if (run) {
|
|
2959
|
+
run.step(dt * settings.timeScale);
|
|
2960
|
+
const s = run.driver.state;
|
|
2961
|
+
pose(s, s.steerAngle);
|
|
2962
|
+
speed = s.u;
|
|
2963
|
+
steer = s.steerAngle;
|
|
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;
|
|
2972
|
+
} else if (parking) {
|
|
2973
|
+
parking.time += dt;
|
|
2974
|
+
const p = parking;
|
|
2975
|
+
p.driver.step({ throttle: 0, brake: p.time < 7 ? 0.35 : 1, steer: p.time > 2 ? -0.6 : 0, handbrake: false }, dt);
|
|
2976
|
+
pose(p.driver.state, p.driver.state.steerAngle);
|
|
2977
|
+
speed = p.driver.state.u;
|
|
2978
|
+
steer = p.driver.state.steerAngle;
|
|
2979
|
+
if (p.time > 9) parking = null;
|
|
2980
|
+
}
|
|
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 });
|
|
2982
|
+
const f = frameOf(speed);
|
|
2983
|
+
if (f && rig.vehicle && settings.camera !== "room") rig.vehicle.update(rig.camera, f, dt);
|
|
2984
|
+
if (f && rig.vehicle) for (const m of rig.mirrors) rig.vehicle.placeMirror(m.camera, m.pose, f);
|
|
2985
|
+
void steer;
|
|
2986
|
+
const key = `${host.bench?.layer("mirrors")}|${host.bench?.layer("reverse_camera")}`;
|
|
2987
|
+
if (key !== mirrorKey) {
|
|
2988
|
+
mirrorKey = key;
|
|
2989
|
+
refreshMirrors();
|
|
2990
|
+
}
|
|
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;
|
|
2999
|
+
};
|
|
3000
|
+
const speedNow = () => Math.abs(run?.driver.state.u ?? driving?.driver.state.u ?? parking?.driver.state.u ?? 0) * 3.6;
|
|
3001
|
+
const counts = (verdict) => certificateOf(settings.vehicle)?.results.filter((r) => r.verdict === verdict).length ?? 0;
|
|
3002
|
+
const teardown = () => {
|
|
3003
|
+
stopDriving();
|
|
3004
|
+
stopEngine();
|
|
3005
|
+
for (const m of rig.mirrors) m.remove();
|
|
3006
|
+
rig.mirrors = [];
|
|
3007
|
+
removeCar();
|
|
3008
|
+
mirrorKey = "";
|
|
3009
|
+
if (track) discardObject3D3(track);
|
|
3010
|
+
track = null;
|
|
3011
|
+
strip = null;
|
|
3012
|
+
board = null;
|
|
3013
|
+
run = null;
|
|
3014
|
+
parking = null;
|
|
3015
|
+
};
|
|
3016
|
+
return {
|
|
3017
|
+
id: PROVING_AREA_ID,
|
|
3018
|
+
name: text.name,
|
|
3019
|
+
group: text.name,
|
|
3020
|
+
ground: { surface: "road" },
|
|
3021
|
+
certificates: () => [...certificates.values()],
|
|
3022
|
+
certificate: (id) => certificateOf(id),
|
|
3023
|
+
items: () => [],
|
|
3024
|
+
area: () => {
|
|
3025
|
+
open = true;
|
|
3026
|
+
applyCamera();
|
|
3027
|
+
return { placements: [], view: { eye: [TRACK.x0 - 4, 3.2, TRACK.z + 9], target: [TRACK.x0 + 6, 2.2, TRACK.z - TRACK.width / 2 - 3] } };
|
|
3028
|
+
},
|
|
3029
|
+
controls: () => [
|
|
3030
|
+
{ kind: "choice", id: "vehicle", name: text.controls.vehicle, hint: text.hints.vehicle, options: vehicles.map((id) => ({ id, name: name(id) })), default: settings.vehicle },
|
|
3031
|
+
{ 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 },
|
|
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 },
|
|
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 },
|
|
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 },
|
|
3037
|
+
{ kind: "action", id: "run_test", name: text.actions.run_test },
|
|
3038
|
+
{ kind: "action", id: "run_all", name: text.actions.run_all },
|
|
3039
|
+
{ kind: "action", id: "certify_now", name: text.actions.certify_now },
|
|
3040
|
+
{ kind: "action", id: "reverse_park", name: text.actions.reverse_park },
|
|
3041
|
+
{ kind: "action", id: "sign_ok", name: text.actions.sign_ok },
|
|
3042
|
+
{ kind: "action", id: "sign_fail", name: text.actions.sign_fail },
|
|
3043
|
+
{ kind: "action", id: "reset", name: text.actions.reset }
|
|
3044
|
+
],
|
|
3045
|
+
layers: () => [
|
|
3046
|
+
{ id: "mirrors", name: text.layers.mirrors, default: false },
|
|
3047
|
+
{ id: "reverse_camera", name: text.layers.reverse_camera, default: false },
|
|
3048
|
+
{ id: "scoreboard", name: text.layers.scoreboard, default: true }
|
|
3049
|
+
],
|
|
3050
|
+
metrics: () => [
|
|
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 },
|
|
3054
|
+
{ id: "time", name: text.metrics.time, unit: "s", digits: 1, read: () => run?.time ?? 0 },
|
|
3055
|
+
{ id: "value", name: text.metrics.value, digits: 1, read: () => run ? run.trace[run.trace.length - 1]?.[1] ?? 0 : 0 },
|
|
3056
|
+
{ id: "passed", name: text.metrics.passed, read: () => counts("pass") },
|
|
3057
|
+
{ id: "failed", name: text.metrics.failed, read: () => counts("fail") },
|
|
3058
|
+
{ id: "pending", name: text.metrics.pending, read: () => counts("pending") },
|
|
3059
|
+
{ id: "status", name: text.metrics.status, read: () => ({ approved: 1, rejected: -1, pending: 0 })[certificateOf(settings.vehicle)?.status ?? "pending"] }
|
|
3060
|
+
],
|
|
3061
|
+
scenarios: () => [],
|
|
3062
|
+
attach(lent) {
|
|
3063
|
+
host = lent;
|
|
3064
|
+
three = lent.THREE;
|
|
3065
|
+
rig.camera = new three.PerspectiveCamera(60, 1.6, 0.05, 900);
|
|
3066
|
+
rig.vehicle = createVehicleCamera(three);
|
|
3067
|
+
lent.audio?.register?.(LIBRARY_BANK2);
|
|
3068
|
+
lent.audio?.register?.(CABIN_BANK);
|
|
3069
|
+
const stopChange = lent.bench?.onChange(onChange);
|
|
3070
|
+
const stopFrame = lent.onFrame(frame);
|
|
3071
|
+
const stopClear = lent.onClear?.(() => {
|
|
3072
|
+
open = false;
|
|
3073
|
+
teardown();
|
|
3074
|
+
lent.setViewCamera?.(null);
|
|
3075
|
+
});
|
|
3076
|
+
return () => {
|
|
3077
|
+
stopChange?.();
|
|
3078
|
+
stopFrame();
|
|
3079
|
+
stopClear?.();
|
|
3080
|
+
teardown();
|
|
3081
|
+
lent.setViewCamera?.(null);
|
|
3082
|
+
host = null;
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
};
|
|
3086
|
+
}
|
|
3087
|
+
function loadProvingModule(room, options) {
|
|
3088
|
+
return room.loadModule(createProvingModule(options));
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
export {
|
|
3092
|
+
LITE_CLASSES,
|
|
3093
|
+
PRUNE_BELOW,
|
|
3094
|
+
classify,
|
|
3095
|
+
classColor,
|
|
3096
|
+
liteMaterials,
|
|
3097
|
+
fuse,
|
|
3098
|
+
liteMeshes,
|
|
3099
|
+
createVehiclesThree,
|
|
3100
|
+
createCabinSmoke,
|
|
3101
|
+
VEHICLE_LOOP_FALLOFF,
|
|
3102
|
+
VEHICLE_LOOPS,
|
|
3103
|
+
VEHICLE_SOUNDS,
|
|
3104
|
+
VEHICLE_SOUND_BANK,
|
|
3105
|
+
firingHz,
|
|
3106
|
+
rpmShare,
|
|
3107
|
+
layerWeight,
|
|
3108
|
+
engineMix,
|
|
3109
|
+
createGearGuess,
|
|
3110
|
+
DEFAULT_VEHICLE_SOUND_KEYS,
|
|
3111
|
+
CRASH_SPEEDS,
|
|
3112
|
+
CAR_ENGINE,
|
|
3113
|
+
TRUCK_ENGINE,
|
|
3114
|
+
BIKE_ENGINE,
|
|
3115
|
+
ENGINE_PROFILES,
|
|
3116
|
+
createVehicleSound,
|
|
3117
|
+
VEHICLE_LOD_DISTANCE,
|
|
3118
|
+
createVehicleLod,
|
|
3119
|
+
VEHICLE_SCENARIO_IDS,
|
|
3120
|
+
PAGE_SCENARIO_ALIASES,
|
|
3121
|
+
PAGE_ITEM_ALIASES,
|
|
3122
|
+
defaultEffectIds,
|
|
3123
|
+
defaultSoundIds,
|
|
3124
|
+
DEFAULT_TEST_WRECKS,
|
|
3125
|
+
VEHICLES_AREA_ID,
|
|
3126
|
+
DEFAULT_TEST_VEHICLES,
|
|
3127
|
+
createVehiclesTestModule,
|
|
3128
|
+
loadVehiclesTestModule,
|
|
3129
|
+
DASHBOARD_LOOK,
|
|
3130
|
+
createVehicleDashboard,
|
|
3131
|
+
LAMP_RULES,
|
|
3132
|
+
createVehicleLamps,
|
|
3133
|
+
PROVING_TEXT,
|
|
3134
|
+
PROVING_AREA_ID,
|
|
3135
|
+
createProvingModule,
|
|
3136
|
+
loadProvingModule
|
|
3137
|
+
};
|
|
3138
|
+
//# sourceMappingURL=chunk-MRUPKE7J.js.map
|