@adatechnology/game-vehicles-three 0.3.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,9 +28,12 @@ var GameVehiclesThree = (() => {
28
28
  PAGE_SCENARIO_ALIASES: () => PAGE_SCENARIO_ALIASES,
29
29
  PRUNE_BELOW: () => PRUNE_BELOW,
30
30
  VEHICLES_AREA_ID: () => VEHICLES_AREA_ID,
31
+ VEHICLE_LOD_DISTANCE: () => VEHICLE_LOD_DISTANCE,
31
32
  VEHICLE_SCENARIO_IDS: () => VEHICLE_SCENARIO_IDS,
32
33
  classColor: () => classColor,
33
34
  classify: () => classify,
35
+ createCabinSmoke: () => createCabinSmoke,
36
+ createVehicleLod: () => createVehicleLod,
34
37
  createVehiclesTestModule: () => createVehiclesTestModule,
35
38
  createVehiclesThree: () => createVehiclesThree,
36
39
  defaultEffectIds: () => defaultEffectIds,
@@ -42,6 +45,7 @@ var GameVehiclesThree = (() => {
42
45
  });
43
46
 
44
47
  // ../game-contracts/dist/index.js
48
+ var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
45
49
  var DEFAULT_IGNITION_RULES = {
46
50
  fillCurve: [
47
51
  [0, 0.6],
@@ -85,6 +89,38 @@ var GameVehiclesThree = (() => {
85
89
  const flame = flameBase * (1 - rules.flameFillInfluence * (1 - k));
86
90
  return 1 - (1 - Math.min(1, Math.max(0, spark))) * (1 - Math.min(1, Math.max(0, flame)));
87
91
  };
92
+ var DEFERRED_QUEUE_ID = {
93
+ DESTRUCTION_EVENTS: "destruction.events",
94
+ BREACH_PAINT: "floorplan.breach-paint",
95
+ AUDIO_VOICES: "audio.voices",
96
+ VEHICLE_CREASE: "vehicles.crease"
97
+ };
98
+ var DEFERRED_PRIORITY = {
99
+ [DEFERRED_QUEUE_ID.DESTRUCTION_EVENTS]: { priority: 300, phase: "present" },
100
+ [DEFERRED_QUEUE_ID.BREACH_PAINT]: { priority: 250, phase: "present" },
101
+ [DEFERRED_QUEUE_ID.AUDIO_VOICES]: { priority: 200, phase: "idle", reserveMs: 1 },
102
+ [DEFERRED_QUEUE_ID.VEHICLE_CREASE]: { priority: 100, phase: "idle" }
103
+ };
104
+ var WARNED_KEY = /* @__PURE__ */ Symbol.for("@adatechnology/game-contracts.warned");
105
+ var warnedKeys = () => {
106
+ const holder = globalThis;
107
+ return holder[WARNED_KEY] ?? (holder[WARNED_KEY] = /* @__PURE__ */ new Set());
108
+ };
109
+ var isProduction = () => {
110
+ try {
111
+ return process.env.NODE_ENV === "production";
112
+ } catch {
113
+ return false;
114
+ }
115
+ };
116
+ var isQuiet = () => globalThis.__GAME_KIT_QUIET__ === true;
117
+ var warnOnce = (key, message) => {
118
+ if (isQuiet() || isProduction()) return;
119
+ const seen = warnedKeys();
120
+ if (seen.has(key)) return;
121
+ seen.add(key);
122
+ console.warn(`[game-kit] ${message}`);
123
+ };
88
124
  var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
89
125
  var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
90
126
  var deepMerge = (base, patch) => {
@@ -106,10 +142,13 @@ var GameVehiclesThree = (() => {
106
142
  for (const [id, patch] of Object.entries(patches ?? {})) {
107
143
  const entry = entries.get(id);
108
144
  if (entry) entries.set(id, deepMerge(entry, patch));
145
+ else warnOnce(`registry.patch.${id}`, `patch for "${id}" ignored: no entry has that id`);
109
146
  }
110
147
  };
111
148
  var removeIds = (entries, ids) => {
112
- for (const id of ids ?? []) entries.delete(id);
149
+ for (const id of ids ?? []) {
150
+ if (!entries.delete(id)) warnOnce(`registry.remove.${id}`, `remove of "${id}" ignored: no entry has that id`);
151
+ }
113
152
  };
114
153
  var KIT_ORIGIN_KEY = "kitOrigin";
115
154
  function markKitOrigin(object2, origin) {
@@ -134,6 +173,43 @@ var GameVehiclesThree = (() => {
134
173
  return lx > fp.minX - radius && lx < fp.maxX + radius && lz > fp.minZ - radius && lz < fp.maxZ + radius;
135
174
  };
136
175
  }
176
+ var SURFACE_IDS = ["concrete", "asphalt", "wood", "tile", "dirt", "grass", "gravel", "sand", "metal", "water", "glass", "carpet"];
177
+ var GAIT_IDS = ["walk", "run"];
178
+ var LANDING_IDS = ["light", "heavy"];
179
+ var BODY_SOUND_IDS = [
180
+ "jump",
181
+ "cloth_rustle",
182
+ "body_fall",
183
+ "fall_damage",
184
+ "player_hurt",
185
+ "player_breath_run",
186
+ "player_death",
187
+ "bite",
188
+ "grab",
189
+ "punch_swing",
190
+ "punch_hit_body",
191
+ "punch_hit_face",
192
+ "punch_hit_object",
193
+ "kick_swing",
194
+ "kick_hit",
195
+ "kick_hit_object",
196
+ "shove_two_hands",
197
+ "knife_swing",
198
+ "knife_stab",
199
+ "knife_slash",
200
+ "knife_hit_object",
201
+ "blunt_swing",
202
+ "blunt_hit",
203
+ "blunt_hit_object",
204
+ "axe_swing",
205
+ "axe_chop",
206
+ "axe_hit_object"
207
+ ];
208
+ var footstepSoundId = (surface, gait) => `footstep_${surface}_${gait}`;
209
+ var FOOTSTEP_SOUND_IDS = SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstepSoundId(surface, gait)));
210
+ var LAND_SOUND_IDS = SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((landing2) => `land_${surface}_${landing2}`));
211
+ var BODY_FOLEY_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
212
+ var SWING_LEAD_SECONDS = { punch: 0.07, kick: 0.09, shove: 0, blade: 0.05, blunt: 0.1, chop: 0.11 };
137
213
 
138
214
  // ../game-vehicles-fleet/dist/index.js
139
215
  var military = [
@@ -24323,7 +24399,7 @@ var GameVehiclesThree = (() => {
24323
24399
  c2.cylinder(0.03, 0.04, 0.08, MT.chrome, sx + 2.35, fl + 0.99, -0.75);
24324
24400
  c2.part("fuel_tank", trl("labels.oxygen_cylinder"), null, new V3(sx + 2.35, fl + 0.5, -0.75), 0.5);
24325
24401
  c2.roundBox(0.04, 0.26, 0.34, 0.02, MT.screen, 0.1, 1.7, 0.5);
24326
- const light = c2.roundBox(
24402
+ const lightPanel = c2.roundBox(
24327
24403
  2.3,
24328
24404
  0.02,
24329
24405
  W - 0.3,
@@ -24333,7 +24409,7 @@ var GameVehiclesThree = (() => {
24333
24409
  m.top - 0.06,
24334
24410
  0
24335
24411
  );
24336
- light.castShadow = false;
24412
+ lightPanel.castShadow = false;
24337
24413
  m.roofItems = beaconBar;
24338
24414
  }
24339
24415
  });
@@ -27892,15 +27968,17 @@ var GameVehiclesThree = (() => {
27892
27968
  }
27893
27969
  function createVehiclesThree(THREE, options) {
27894
27970
  const kit = options.kit;
27971
+ const fleet = options.fleet ?? { colorFromSeed, fleetDims, paintPalettes, paletteOf };
27972
+ const { colorFromSeed: colorFromSeed2, fleetDims: fleetDims2, paintPalettes: paintPalettes2, paletteOf: paletteOf2 } = fleet;
27895
27973
  const groupOf = options.groupOf || ((key) => kit.find(key)?.group === kit.config.defaultGroup ? "cars" : String(kit.find(key)?.group));
27896
27974
  const militaryKeys = /* @__PURE__ */ new Set();
27897
- const isMilitary = (key) => militaryKeys.has(key) || paletteOf(groupOf(key)) === paintPalettes.military;
27975
+ const isMilitary = (key) => militaryKeys.has(key) || paletteOf2(groupOf(key)) === paintPalettes2.military;
27898
27976
  const build = (key, o = {}) => {
27899
27977
  const quality = o.quality === "lite" ? "lite" : "high";
27900
27978
  const spec = kit.find(key);
27901
27979
  if (!spec) throw new Error(`createVehiclesThree: unknown vehicle "${key}"`);
27902
27980
  const seed = o.seed == null ? null : o.seed;
27903
- const color2 = o.color || (seed != null ? colorFromSeed(seed, isMilitary(key) ? paintPalettes.military : paintPalettes.civil) : null);
27981
+ const color2 = o.color || (seed != null ? colorFromSeed2(seed, isMilitary(key) ? paintPalettes2.military : paintPalettes2.civil) : null);
27904
27982
  const c = kit.build(key, o.state || "intact", void 0, { lod: quality === "lite" });
27905
27983
  c.root.updateMatrixWorld(true);
27906
27984
  const primary = primaryPaint(c);
@@ -27966,7 +28044,7 @@ var GameVehiclesThree = (() => {
27966
28044
  width: +size.x.toFixed(3),
27967
28045
  height: +size.y.toFixed(3),
27968
28046
  pruned,
27969
- expected: fleetDims(key),
28047
+ expected: fleetDims2(key),
27970
28048
  context: quality === "high" ? c : null,
27971
28049
  dispose() {
27972
28050
  group.traverse((m) => {
@@ -27991,7 +28069,7 @@ var GameVehiclesThree = (() => {
27991
28069
  }
27992
28070
  }
27993
28071
 
27994
- // ../game-shapes/dist/chunk-R4RY23RH.js
28072
+ // ../game-shapes/dist/chunk-IFXQIDSA.js
27995
28073
  function canonicalJson(value) {
27996
28074
  if (value === null) return "null";
27997
28075
  if (typeof value === "number") return Object.is(value, -0) ? "-0" : Number.isFinite(value) ? String(value) : JSON.stringify(String(value));
@@ -29286,10 +29364,10 @@ var GameVehiclesThree = (() => {
29286
29364
  return cached;
29287
29365
  }
29288
29366
  stats.misses++;
29289
- const built = make(node.kind, parameters);
29290
- templates.set(key, built);
29367
+ const built2 = make(node.kind, parameters);
29368
+ templates.set(key, built2);
29291
29369
  if (templates.size > templateLimit) templates.delete(templates.keys().next().value);
29292
- return built;
29370
+ return built2;
29293
29371
  }
29294
29372
  var num = (v) => typeof v === "number" && Number.isFinite(v) ? null : "expected a finite number";
29295
29373
  var bool = (v) => typeof v === "boolean" ? null : "expected a boolean";
@@ -30467,7 +30545,7 @@ var GameVehiclesThree = (() => {
30467
30545
  }
30468
30546
  return ti.builtBlocks === total;
30469
30547
  }
30470
- function start(geo) {
30548
+ function start(geo, store = cache) {
30471
30549
  const pos = geo.attributes.position;
30472
30550
  const triangles = trianglesOf(geo);
30473
30551
  const ti = {
@@ -30480,7 +30558,7 @@ var GameVehiclesThree = (() => {
30480
30558
  blocks: new Float64Array(blockCount(triangles) * 6),
30481
30559
  builtBlocks: 0
30482
30560
  };
30483
- cache.set(geo, ti);
30561
+ store.set(geo, ti);
30484
30562
  return ti;
30485
30563
  }
30486
30564
  function triIndexOf(geo) {
@@ -30532,6 +30610,106 @@ var GameVehiclesThree = (() => {
30532
30610
  }
30533
30611
  return { box: box2, version: ti.positionVersion };
30534
30612
  }
30613
+ var rayCache = /* @__PURE__ */ new WeakMap();
30614
+ function rayIndexOf(geo) {
30615
+ const shared2 = cache.get(geo);
30616
+ if (shared2 && isCurrent(geo, shared2) && shared2.builtBlocks === blockCount(shared2.triangles)) return shared2;
30617
+ let ti = rayCache.get(geo);
30618
+ if (!ti || !isCurrent(geo, ti)) ti = start(geo, rayCache);
30619
+ advance(geo, ti, Infinity);
30620
+ return ti;
30621
+ }
30622
+ var BLOCK_SLACK = 1e-6;
30623
+ var scratch = /* @__PURE__ */ new WeakMap();
30624
+ function scratchOf(THREE) {
30625
+ let s = scratch.get(THREE);
30626
+ if (!s) {
30627
+ s = {
30628
+ inverse: new THREE.Matrix4(),
30629
+ local: new THREE.Ray(),
30630
+ box: new THREE.Box3(),
30631
+ a: new THREE.Vector3(),
30632
+ b: new THREE.Vector3(),
30633
+ c: new THREE.Vector3(),
30634
+ point: new THREE.Vector3(),
30635
+ world: new THREE.Vector3(),
30636
+ sphere: new THREE.Sphere()
30637
+ };
30638
+ scratch.set(THREE, s);
30639
+ }
30640
+ return s;
30641
+ }
30642
+ function indexable(THREE, mesh2) {
30643
+ const geo = mesh2.geometry;
30644
+ if (mesh2.raycast !== THREE.Mesh.prototype.raycast) return false;
30645
+ if (!mesh2.isMesh || mesh2.isSkinnedMesh || mesh2.isInstancedMesh || mesh2.isBatchedMesh || !geo || !geo.isBufferGeometry) return false;
30646
+ if (!mesh2.material || Array.isArray(mesh2.material)) return false;
30647
+ if (geo.morphAttributes && geo.morphAttributes.position && geo.morphAttributes.position.length) return false;
30648
+ return supports(geo);
30649
+ }
30650
+ function raycastMesh(THREE, raycaster, mesh2, out) {
30651
+ const geo = mesh2.geometry;
30652
+ const s = scratchOf(THREE);
30653
+ if (geo.boundingSphere === null) geo.computeBoundingSphere();
30654
+ s.sphere.copy(geo.boundingSphere).applyMatrix4(mesh2.matrixWorld);
30655
+ if (!raycaster.ray.intersectsSphere(s.sphere)) return;
30656
+ const ti = rayIndexOf(geo);
30657
+ if (!ti) return;
30658
+ s.inverse.copy(mesh2.matrixWorld).invert();
30659
+ s.local.copy(raycaster.ray).applyMatrix4(s.inverse);
30660
+ const material = mesh2.material;
30661
+ const back = material.side === THREE.BackSide;
30662
+ const cull = material.side === THREE.FrontSide;
30663
+ const P = geo.attributes.position;
30664
+ const ix = geo.index;
30665
+ const count2 = ix ? ix.count : P.count;
30666
+ const first = Math.max(0, geo.drawRange.start);
30667
+ const last = Math.min(count2, geo.drawRange.start + geo.drawRange.count);
30668
+ const t0 = Math.ceil(first / 3);
30669
+ const t1 = Math.floor(last / 3);
30670
+ const B = ti.boxes;
30671
+ const K = ti.blocks;
30672
+ for (let blk = Math.floor(t0 / TRI_BLOCK); blk * TRI_BLOCK < t1; blk++) {
30673
+ const o = blk * 6;
30674
+ s.box.min.set(K[o] - BLOCK_SLACK, K[o + 1] - BLOCK_SLACK, K[o + 2] - BLOCK_SLACK);
30675
+ s.box.max.set(K[o + 3] + BLOCK_SLACK, K[o + 4] + BLOCK_SLACK, K[o + 5] + BLOCK_SLACK);
30676
+ if (!s.local.intersectsBox(s.box)) continue;
30677
+ const end = Math.min(t1, (blk + 1) * TRI_BLOCK);
30678
+ for (let t = Math.max(t0, blk * TRI_BLOCK); t < end; t++) {
30679
+ const j = t * 6;
30680
+ s.box.min.set(B[j] - BLOCK_SLACK, B[j + 1] - BLOCK_SLACK, B[j + 2] - BLOCK_SLACK);
30681
+ s.box.max.set(B[j + 3] + BLOCK_SLACK, B[j + 4] + BLOCK_SLACK, B[j + 5] + BLOCK_SLACK);
30682
+ if (!s.local.intersectsBox(s.box)) continue;
30683
+ const a = ix ? ix.getX(t * 3) : t * 3;
30684
+ const b = ix ? ix.getX(t * 3 + 1) : t * 3 + 1;
30685
+ const c = ix ? ix.getX(t * 3 + 2) : t * 3 + 2;
30686
+ s.a.fromBufferAttribute(P, a);
30687
+ s.b.fromBufferAttribute(P, b);
30688
+ s.c.fromBufferAttribute(P, c);
30689
+ const hit = back ? s.local.intersectTriangle(s.c, s.b, s.a, true, s.point) : s.local.intersectTriangle(s.a, s.b, s.c, cull, s.point);
30690
+ if (hit === null) continue;
30691
+ s.world.copy(s.point).applyMatrix4(mesh2.matrixWorld);
30692
+ const distance = raycaster.ray.origin.distanceTo(s.world);
30693
+ if (distance < raycaster.near || distance > raycaster.far) continue;
30694
+ const normal = new THREE.Vector3();
30695
+ THREE.Triangle.getNormal(s.a, s.b, s.c, normal);
30696
+ out.push({ distance, point: s.world.clone(), object: mesh2, face: { a, b, c, normal, materialIndex: 0 }, faceIndex: Math.floor(t) });
30697
+ }
30698
+ }
30699
+ }
30700
+ function collect2(THREE, raycaster, object2, recursive, out) {
30701
+ if (object2.layers.test(raycaster.layers)) {
30702
+ if (indexable(THREE, object2)) raycastMesh(THREE, raycaster, object2, out);
30703
+ else object2.raycast(raycaster, out);
30704
+ }
30705
+ if (recursive) for (const child of object2.children) collect2(THREE, raycaster, child, true, out);
30706
+ }
30707
+ function raycastIndexed(THREE, raycaster, objects, recursive = false) {
30708
+ const out = [];
30709
+ for (const object2 of objects) collect2(THREE, raycaster, object2, recursive, out);
30710
+ out.sort((x, y) => x.distance - y.distance);
30711
+ return out;
30712
+ }
30535
30713
  function installGeometry(kit) {
30536
30714
  const THREE = kit.THREE;
30537
30715
  const V3 = THREE.Vector3;
@@ -31856,6 +32034,7 @@ var GameVehiclesThree = (() => {
31856
32034
  }
31857
32035
  kit.profX = profX;
31858
32036
  const _sr = new THREE.Raycaster();
32037
+ _sr.layers.enableAll();
31859
32038
  kit.surfaceAt = function(c, org, dir, o) {
31860
32039
  o = o || {};
31861
32040
  c.root.updateMatrixWorld(true);
@@ -31872,7 +32051,7 @@ var GameVehiclesThree = (() => {
31872
32051
  _sr.set(p0.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
31873
32052
  _sr.near = 0;
31874
32053
  _sr.far = o.max || 20;
31875
- const h = _sr.intersectObjects(cands, false)[0];
32054
+ const h = raycastIndexed(THREE, _sr, cands)[0];
31876
32055
  if (!h) return null;
31877
32056
  const n = h.face ? h.face.normal.clone().transformDirection(h.object.matrixWorld).transformDirection(inv) : d.clone().negate();
31878
32057
  if (n.dot(d) > 0) n.negate();
@@ -31892,6 +32071,7 @@ var GameVehiclesThree = (() => {
31892
32071
  const V3 = THREE.Vector3;
31893
32072
  const I = kit._;
31894
32073
  const { MATERIALS, crease, cutGeometry, pathShape, subdivide, tumble, uvProject } = I;
32074
+ I.paneSeals = /* @__PURE__ */ new WeakMap();
31895
32075
  kit.addGlass = function(c, geo, label, type, o) {
31896
32076
  o = o || {};
31897
32077
  if (!geo.attributes.uv || o.reuv) o.aspect = uvProject(geo, o.uvAxes);
@@ -31958,7 +32138,7 @@ var GameVehiclesThree = (() => {
31958
32138
  cy /= poly.length;
31959
32139
  const base = I.resolveLabel(label ? label(i, polys.length) : { key: "window" });
31960
32140
  const lab = { key: base.key, label: kit.t("parts.glass_side", { label: base.label, s }) };
31961
- kit.addGlass(c, geo, lab, o.type || "tempered", {
32141
+ const pane = kit.addGlass(c, geo, lab, o.type || "tempered", {
31962
32142
  mat: o.mat || (o.see === false ? MATERIALS.glassDark : MATERIALS.glass),
31963
32143
  pos: new V3(cx, cy, s * I.halfAt(Wg, T, cy)),
31964
32144
  outline: shp.getSpacedPoints(56),
@@ -31974,6 +32154,7 @@ var GameVehiclesThree = (() => {
31974
32154
  pts.pop();
31975
32155
  const seal = c.tube(pts, 0.011, MATERIALS.trim, { closed: true, seg: 72, rs: 5 });
31976
32156
  seal.castShadow = false;
32157
+ I.paneSeals.set(pane, seal);
31977
32158
  });
31978
32159
  });
31979
32160
  };
@@ -33670,6 +33851,42 @@ var GameVehiclesThree = (() => {
33670
33851
  };
33671
33852
  Object.assign(I, {});
33672
33853
  }
33854
+ var ROUTED_PARTICLE_TYPES = { smoke: "particle_puff", dust: "particle_puff", fire: "particle_glow" };
33855
+ var PUFF_LOOKS = {
33856
+ smokewhite: [10133927, 0.42],
33857
+ smokeblack: [1776412, 0.62],
33858
+ smokegray: [5593436, 0.5],
33859
+ smokesteam: [11975873, 0.3],
33860
+ dust: [7235160, 0.34],
33861
+ fire: [16777215, 0.9]
33862
+ };
33863
+ var LIFE = { dust: 1.6, smoke: 2.8, fire: 0.7 };
33864
+ var FIRE_COLOR = 16753968;
33865
+ var SMOKE_START_SCALE = 0.18;
33866
+ function routedParticleRequest(input) {
33867
+ const { type } = input;
33868
+ const look = PUFF_LOOKS[type + (input.variant ?? "")] ?? PUFF_LOOKS[type] ?? PUFF_LOOKS.smokewhite;
33869
+ const life = input.life ?? LIFE[type] ?? 99;
33870
+ const size = (input.size ?? 1) * (input.grow ?? 1);
33871
+ const isFire = type === "fire";
33872
+ const growth = isFire ? 1 : (SMOKE_START_SCALE + 1.5 * (1 - Math.exp(-life * 1.1)) + life * 0.12) / SMOKE_START_SCALE;
33873
+ return {
33874
+ effect: ROUTED_PARTICLE_TYPES[type],
33875
+ point: input.position,
33876
+ values: {
33877
+ vx: input.velocity[0],
33878
+ vy: input.velocity[1],
33879
+ vz: input.velocity[2],
33880
+ life,
33881
+ size: isFire ? size * 0.85 : size * SMOKE_START_SCALE,
33882
+ grow: growth,
33883
+ color: input.color ?? (isFire ? FIRE_COLOR : look[0]),
33884
+ alpha: input.alpha ?? look[1],
33885
+ gravity: isFire ? 0 : type === "dust" ? 0 : -0.08,
33886
+ drag: type === "dust" ? 2.5 : isFire ? 1.5 : 0.9
33887
+ }
33888
+ };
33889
+ }
33673
33890
  function installParticles(kit) {
33674
33891
  const THREE = kit.THREE;
33675
33892
  const V3 = THREE.Vector3;
@@ -33760,7 +33977,7 @@ var GameVehiclesThree = (() => {
33760
33977
  });
33761
33978
  }
33762
33979
  const list2 = fire ? FIRE_TEXTURES : SMOKE_TEXTURES;
33763
- const mat = new THREE.SpriteMaterial({
33980
+ const build = () => new THREE.SpriteMaterial({
33764
33981
  map: list2[Math.floor(Math.random() * list2.length)],
33765
33982
  color: new THREE.Color(col),
33766
33983
  transparent: true,
@@ -33768,6 +33985,7 @@ var GameVehiclesThree = (() => {
33768
33985
  opacity: op,
33769
33986
  blending: fire ? THREE.AdditiveBlending : THREE.NormalBlending
33770
33987
  });
33988
+ const mat = kit.particleMaterials ? kit.particleMaterials.own(build) : build();
33771
33989
  const dis = { value: 0 };
33772
33990
  mat.userData.dis = dis;
33773
33991
  mat.onBeforeCompile = function(sh) {
@@ -33795,11 +34013,37 @@ var GameVehiclesThree = (() => {
33795
34013
  function killParticle(c, i) {
33796
34014
  const P = c.particles[i];
33797
34015
  c.effects.remove(P.m);
33798
- if (P.own) P.m.material.dispose();
34016
+ if (P.own) {
34017
+ if (kit.particleMaterials) kit.particleMaterials.release(P.m.material);
34018
+ else P.m.material.dispose();
34019
+ }
33799
34020
  c.particles.splice(i, 1);
33800
34021
  }
34022
+ function routeParticle(c, effects, type, pos, vel, o) {
34023
+ c.effects.updateWorldMatrix(true, false);
34024
+ const at = pos.clone().applyMatrix4(c.effects.matrixWorld);
34025
+ const to = vel.clone().transformDirection(c.effects.matrixWorld).multiplyScalar(vel.length());
34026
+ const colour = o.col == null ? void 0 : new THREE.Color(o.col).getHex();
34027
+ effects.play(
34028
+ routedParticleRequest({
34029
+ type,
34030
+ position: [at.x, at.y, at.z],
34031
+ velocity: [to.x, to.y, to.z],
34032
+ ...o.variant ? { variant: o.variant } : {},
34033
+ ...colour !== void 0 ? { color: colour } : {},
34034
+ ...o.op != null ? { alpha: o.op } : {},
34035
+ ...o.s ? { size: o.s } : {},
34036
+ ...o.life ? { life: o.life } : {},
34037
+ ...o.grow ? { grow: o.grow } : {}
34038
+ })
34039
+ );
34040
+ return null;
34041
+ }
33801
34042
  kit.spawn = function(c, type, pos, vel, o) {
33802
34043
  o = o || {};
34044
+ const effects = ROUTED_PARTICLE_TYPES[type] ? kit.effects?.() : void 0;
34045
+ if (effects) return routeParticle(c, effects, type, pos, vel, o);
34046
+ if (ROUTED_PARTICLE_TYPES[type] && !kit.effects) warnOnce("game-vehicles.own-particles", "game-vehicles drew its smoke, fire and dust with sprites of its own because the host lent no effects world (options.effects); pass the host effects so the puffs share one budget");
33803
34047
  if (c.particles.length >= PARTICLE_LIMIT) {
33804
34048
  const idx = c.particles.findIndex(function(q) {
33805
34049
  return TRANSIENT_TYPES[q.type];
@@ -34069,6 +34313,7 @@ var GameVehiclesThree = (() => {
34069
34313
  };
34070
34314
  kit.defineFluid("stain", { fresh: "#4a4a44", dry: "#26241f", wipeable: false });
34071
34315
  const _fr = new THREE.Raycaster();
34316
+ _fr.layers.enableAll();
34072
34317
  kit.splat = function(c, p, n, r, o) {
34073
34318
  o = o || {};
34074
34319
  n = n.clone().normalize();
@@ -34508,31 +34753,55 @@ var GameVehiclesThree = (() => {
34508
34753
  };
34509
34754
  Object.assign(I, { _fr, buildDrip, DOWNHILL, PN, puddleGeometry, rimGeometry, puddle, shapePuddle, updateFluids });
34510
34755
  }
34756
+ function crackedGlassMaterial(THREE, texture) {
34757
+ return new THREE.MeshStandardMaterial({
34758
+ color: 16777215,
34759
+ map: texture,
34760
+ emissive: 6976118,
34761
+ emissiveMap: texture,
34762
+ transparent: true,
34763
+ roughness: 0.25,
34764
+ metalness: 0.1,
34765
+ side: THREE.DoubleSide,
34766
+ depthWrite: false
34767
+ });
34768
+ }
34511
34769
  function installGlassDamage(kit) {
34512
34770
  const THREE = kit.THREE;
34513
34771
  const V3 = THREE.Vector3;
34514
34772
  const PI = Math.PI;
34515
34773
  const I = kit._;
34516
34774
  const document2 = kit.document;
34517
- const { DOUBLE_SIDE, MATERIALS, crease, cutGeometry, mergeGeometries, subdivide, tumble } = I;
34518
- function glassUV(msh, pLocal) {
34775
+ const { MATERIALS, crease, cutGeometry, mergeGeometries, subdivide, tumble } = I;
34776
+ function glassUV(c, msh, pLocal) {
34519
34777
  const pos = msh.geometry.attributes.position;
34520
34778
  const uv = msh.geometry.attributes.uv;
34521
34779
  if (!uv) return [0.5, 0.5];
34522
- const inv = new THREE.Matrix4().copy(msh.matrix).invert();
34523
- const q = pLocal.clone().applyMatrix4(inv);
34524
- let best = 1e9;
34525
- let bi = 0;
34526
- const v = new V3();
34527
- for (let i = 0; i < pos.count; i++) {
34528
- v.fromBufferAttribute(pos, i);
34529
- const d = v.distanceToSquared(q);
34530
- if (d < best) {
34531
- best = d;
34532
- bi = i;
34533
- }
34780
+ c.root.updateMatrixWorld(true);
34781
+ const q = msh.worldToLocal(c.vehicle.localToWorld(pLocal.clone()));
34782
+ const idx = msh.geometry.index;
34783
+ const tri = new THREE.Triangle();
34784
+ const near = new V3();
34785
+ const bary = new V3();
34786
+ let best = Infinity;
34787
+ const out = [0.5, 0.5];
34788
+ const count2 = idx ? idx.count : pos.count;
34789
+ for (let f = 0; f + 2 < count2; f += 3) {
34790
+ const ia = idx ? idx.getX(f) : f;
34791
+ const ib = idx ? idx.getX(f + 1) : f + 1;
34792
+ const ic = idx ? idx.getX(f + 2) : f + 2;
34793
+ tri.a.fromBufferAttribute(pos, ia);
34794
+ tri.b.fromBufferAttribute(pos, ib);
34795
+ tri.c.fromBufferAttribute(pos, ic);
34796
+ tri.closestPointToPoint(q, near);
34797
+ const d = near.distanceToSquared(q);
34798
+ if (d >= best) continue;
34799
+ best = d;
34800
+ tri.getBarycoord(near, bary);
34801
+ out[0] = uv.getX(ia) * bary.x + uv.getX(ib) * bary.y + uv.getX(ic) * bary.z;
34802
+ out[1] = uv.getY(ia) * bary.x + uv.getY(ib) * bary.y + uv.getY(ic) * bary.z;
34534
34803
  }
34535
- return [uv.getX(bi), uv.getY(bi)];
34804
+ return out;
34536
34805
  }
34537
34806
  function drawWeb(x, cx, cy, s, ax, rnd) {
34538
34807
  const n = 16 + Math.floor(rnd() * 8);
@@ -34609,19 +34878,9 @@ var GameVehiclesThree = (() => {
34609
34878
  x0.fillStyle = "rgba(48,66,72,0.5)";
34610
34879
  x0.fillRect(0, 0, 256, 256);
34611
34880
  G2.tex = new THREE.CanvasTexture(G2.cv);
34612
- msh.material = new THREE.MeshStandardMaterial({
34613
- color: 16777215,
34614
- map: G2.tex,
34615
- emissive: 6976118,
34616
- emissiveMap: G2.tex,
34617
- transparent: true,
34618
- roughness: 0.25,
34619
- metalness: 0.1,
34620
- side: DOUBLE_SIDE,
34621
- depthWrite: false
34622
- });
34881
+ msh.material = crackedGlassMaterial(THREE, G2.tex);
34623
34882
  }
34624
- const uv = glassUV(msh, p);
34883
+ const uv = glassUV(c, msh, p);
34625
34884
  const x = G2.cv.getContext("2d");
34626
34885
  drawWeb(x, uv[0] * 256, (1 - uv[1]) * 256, sc != null ? sc : 0.6 + force * 0.5, Math.max(0.4, G2.aspect || 1), c.rand);
34627
34886
  G2.tex.needsUpdate = true;
@@ -34766,6 +35025,13 @@ var GameVehiclesThree = (() => {
34766
35025
  }
34767
35026
  const seatGeo = [];
34768
35027
  const d = dir.clone().normalize();
35028
+ if (msh.parent && msh.parent !== c.vehicle) {
35029
+ c.root.updateMatrixWorld(true);
35030
+ const toVehicle = new THREE.Matrix4().copy(c.vehicle.matrixWorld).invert().multiply(msh.parent.matrixWorld);
35031
+ cubesAt.forEach(function(q) {
35032
+ q.applyMatrix4(toVehicle);
35033
+ });
35034
+ }
34769
35035
  cubesAt.forEach(function(p, i) {
34770
35036
  if (i % 3 === 0 && c.seatTops.length && !c.rollState && !c.flipped) {
34771
35037
  let best = null;
@@ -34966,6 +35232,7 @@ var GameVehiclesThree = (() => {
34966
35232
  polygonOffsetUnits: -2
34967
35233
  });
34968
35234
  const _ray = new THREE.Raycaster();
35235
+ _ray.layers.enableAll();
34969
35236
  function findSurface(c, p, dir) {
34970
35237
  c.root.updateMatrixWorld(true);
34971
35238
  const Mw = c.vehicle.matrixWorld;
@@ -35047,6 +35314,7 @@ var GameVehiclesThree = (() => {
35047
35314
  const v0 = new V3();
35048
35315
  const w = new V3();
35049
35316
  const sc = new V3();
35317
+ const local = new V3();
35050
35318
  const cab = m.cabin && !c.flipped && !c.rollState ? m.cabin : null;
35051
35319
  c.deform.forEach(function(msh) {
35052
35320
  if (!msh.parent || !msh.visible) return;
@@ -35061,7 +35329,15 @@ var GameVehiclesThree = (() => {
35061
35329
  irel.copy(rel).invert();
35062
35330
  if (!g.userData.p0) g.userData.p0 = new Float32Array(pos.array);
35063
35331
  const p0 = g.userData.p0;
35332
+ local.copy(p).applyMatrix4(irel);
35333
+ const reach = H.R / Math.max(1e-6, Math.min(sc.x, sc.y, sc.z));
35334
+ const reach2 = reach * reach;
35335
+ const arr = pos.array;
35064
35336
  for (let i = 0; i < pos.count; i++) {
35337
+ const dx = arr[3 * i] - local.x;
35338
+ const dy = arr[3 * i + 1] - local.y;
35339
+ const dz = arr[3 * i + 2] - local.z;
35340
+ if (dx * dx + dy * dy + dz * dz >= reach2) continue;
35065
35341
  v.fromBufferAttribute(pos, i).applyMatrix4(rel);
35066
35342
  const a = dentField(v, H);
35067
35343
  if (a <= 0) continue;
@@ -35077,10 +35353,15 @@ var GameVehiclesThree = (() => {
35077
35353
  }
35078
35354
  if (changed) {
35079
35355
  pos.needsUpdate = true;
35080
- if (g.userData.crease) crease(g, 42);
35356
+ if (g.userData.crease && c.smoothLater) c.smoothLater(msh);
35357
+ else if (g.userData.crease) crease(g, 42);
35081
35358
  else g.computeVertexNormals();
35082
- g.computeBoundingSphere();
35083
35359
  g.computeBoundingBox();
35360
+ const sphere2 = g.boundingSphere ?? (g.boundingSphere = new THREE.Sphere());
35361
+ g.boundingBox.getCenter(sphere2.center);
35362
+ let far2 = 0;
35363
+ for (let i = 0; i < pos.count; i++) far2 = Math.max(far2, sphere2.center.distanceToSquared(v.fromBufferAttribute(pos, i)));
35364
+ sphere2.radius = Math.sqrt(far2);
35084
35365
  }
35085
35366
  });
35086
35367
  const dset = new Set(c.deform);
@@ -35164,13 +35445,14 @@ var GameVehiclesThree = (() => {
35164
35445
  kit.impact = impact;
35165
35446
  kit.partEffects = {};
35166
35447
  const _pr = new THREE.Raycaster();
35448
+ _pr.layers.enableAll();
35167
35449
  function probe(c, o, d, far) {
35168
35450
  c.root.updateMatrixWorld(true);
35169
35451
  const Mw = c.vehicle.matrixWorld;
35170
35452
  const inv = new THREE.Matrix4().copy(Mw).invert();
35171
35453
  _pr.set(o.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
35172
35454
  _pr.far = far || 10;
35173
- const hits = _pr.intersectObject(c.vehicle, true);
35455
+ const hits = raycastIndexed(THREE, _pr, [c.vehicle], true);
35174
35456
  for (let i = 0; i < hits.length; i++) {
35175
35457
  const h = hits[i];
35176
35458
  const ob = h.object;
@@ -36524,6 +36806,7 @@ var GameVehiclesThree = (() => {
36524
36806
  return hole;
36525
36807
  }
36526
36808
  const _zr = new THREE.Raycaster();
36809
+ _zr.layers.enableAll();
36527
36810
  const PROXY_MIN_TRIANGLES = 500;
36528
36811
  const STROKE_REACH = 0.6;
36529
36812
  const _pv = new V3();
@@ -36758,6 +37041,7 @@ var GameVehiclesThree = (() => {
36758
37041
  return out;
36759
37042
  }
36760
37043
  const _rt = new THREE.Raycaster();
37044
+ _rt.layers.enableAll();
36761
37045
  function scratchPath(c, s, len, ang, cands, B) {
36762
37046
  const n = s.n.clone().normalize();
36763
37047
  const t = Math.abs(n.y) < 0.9 ? new V3().crossVectors(new V3(0, 1, 0), n) : new V3().crossVectors(new V3(1, 0, 0), n);
@@ -40017,6 +40301,8 @@ var GameVehiclesThree = (() => {
40017
40301
  V3: THREE.Vector3,
40018
40302
  PI: Math.PI,
40019
40303
  document: doc,
40304
+ particleMaterials: opts.particleMaterials,
40305
+ effects: opts.effects,
40020
40306
  strings,
40021
40307
  t,
40022
40308
  matchers: createMatchers(strings),
@@ -40272,7 +40558,9 @@ var GameVehiclesThree = (() => {
40272
40558
  assignDeep(strings, resolveStrings(opts.locale));
40273
40559
  return kit;
40274
40560
  };
40275
- const { THREE: _three, lod: _lod, locale: _locale, document: _document, plugins, ...config } = opts;
40561
+ const { THREE: _three, lod: _lod, locale: _locale, document: _document, particleMaterials: _particleMaterials, effects: _effects, plugins, ...config } = opts;
40562
+ void _particleMaterials;
40563
+ void _effects;
40276
40564
  void _three;
40277
40565
  void _lod;
40278
40566
  void _locale;
@@ -40366,6 +40654,14 @@ var GameVehiclesThree = (() => {
40366
40654
  center: [b.center[0], b.center[1] - b.size[1] / 2 + Math.min(0.14, b.size[1]) / 2 + 0.04, b.center[2]],
40367
40655
  size: [b.size[0] * 0.5, Math.min(0.14, b.size[1]), b.size[2] * 0.95]
40368
40656
  });
40657
+ function paneBox(c, pane, inverse) {
40658
+ pane.geometry.computeBoundingBox();
40659
+ const box2 = pane.geometry.boundingBox.clone().applyMatrix4(pane.matrixWorld);
40660
+ box2.applyMatrix4(inverse);
40661
+ const center = [(box2.min.x + box2.max.x) / 2, (box2.min.y + box2.max.y) / 2, (box2.min.z + box2.max.z) / 2];
40662
+ const size = [box2.max.x - box2.min.x, box2.max.y - box2.min.y, box2.max.z - box2.min.z];
40663
+ return { center, size };
40664
+ }
40369
40665
  function buildVehicleModel(kit, c, options = {}) {
40370
40666
  const kind = kindOf(c);
40371
40667
  const m = c.meta;
@@ -40506,6 +40802,16 @@ var GameVehiclesThree = (() => {
40506
40802
  tags: [role, ...tags]
40507
40803
  });
40508
40804
  }
40805
+ const claimed = new Set(zones.filter((z) => roleOfZone(z) === "glass").flatMap((z) => z.glass ?? []));
40806
+ c.root.updateMatrixWorld(true);
40807
+ const inverse = c.vehicle.matrixWorld.clone().invert();
40808
+ for (const pane of c.glass ?? []) {
40809
+ if (claimed.has(pane)) continue;
40810
+ const G2 = pane.userData.glass;
40811
+ const bb = paneBox(c, pane, inverse);
40812
+ const laminated = G2?.type === "laminated" || m.bullet;
40813
+ add2({ id: `pane_${G2?.key ?? "glass"}`, role: "glass", name: String(G2?.label ?? G2?.key ?? "glass"), material: "glass", box: bb, mass: 10 * areaOf(bb.size), hp: laminated ? 4e3 : 450, panes: [pane], visual: false, tags: ["glass"] });
40814
+ }
40509
40815
  if (!hubZone) {
40510
40816
  const h = Math.max(0.15, belt - clearance);
40511
40817
  add2({
@@ -40839,6 +41145,76 @@ var GameVehiclesThree = (() => {
40839
41145
  void has;
40840
41146
  return { parts, joints, hub: hubId, hitch };
40841
41147
  }
41148
+ var HOLE_DECAL_SCALE = 2.4;
41149
+ var DOOR_DEPTH = 0.07;
41150
+ var DOOR_EDGE_PIECES = 16;
41151
+ var DOOR_CARD_COLOR = "#3b3e42";
41152
+ var DOOR_LINER_COLOR = "#5a5e64";
41153
+ var DOOR_LINER_DARK = 0.15;
41154
+ var DOOR_STRIP_THIN = 0.05;
41155
+ var DOOR_STRIP_NEAR = 0.04;
41156
+ var DOOR_STRIP_OVER = 0.3;
41157
+ var DOOR_TOP_OVER_PANE = 0.03;
41158
+ var DOOR_STRIP_SUB = 0.02;
41159
+ var HOLE_TEXTURE_SIZE = 64;
41160
+ var DOOR_HANDLE_INSET = 0.12;
41161
+ var DOOR_HANDLE_MIN = 0.07;
41162
+ var DOOR_HANDLE_MAX = 0.24;
41163
+ var DOOR_HANDLE_THIN = 0.07;
41164
+ var DOOR_HANDLE_OUTSIDE = 0.1;
41165
+ var PANE_SEAT_OUT = 0.05;
41166
+ var PANE_SEAT_IN = -0.05;
41167
+ var PANE_SEAT_REACH = 0.14;
41168
+ var PANE_SEAT_SAMPLES = 24;
41169
+ var PANE_SEAT_LIFT = 4e-3;
41170
+ var PANE_FRAME_NEAR = 0.07;
41171
+ var DRAWN_MIRROR_SIZE = 0.3;
41172
+ var DRAWN_MIRROR_REACH = 0.4;
41173
+ var DRAWN_MIRROR_MOUNT = 0.2;
41174
+ var MIRROR_GLASS_FIT = 0.76;
41175
+ var MIRROR_GLASS_THIN = 0.04;
41176
+ var MIRROR_GLASS_MIN = 0.05;
41177
+ var MIRROR_GLASS_COLOR = "#59636a";
41178
+ var HOLE_RIM = { steel: "#b7bcc0", aluminium: "#d3d6d8", plastic: "#d9d6cc", rubber: "#141414", glass: "#eef3f5" };
41179
+ var holeMaterials = /* @__PURE__ */ new WeakMap();
41180
+ var holeDisc = null;
41181
+ function holeDiscGeometry(THREE) {
41182
+ holeDisc = holeDisc ?? new THREE.CircleGeometry(1, 12);
41183
+ return holeDisc;
41184
+ }
41185
+ function holeMaterial(kit, material) {
41186
+ const key = kit;
41187
+ const cache2 = holeMaterials.get(key) ?? /* @__PURE__ */ new Map();
41188
+ holeMaterials.set(key, cache2);
41189
+ const known = cache2.get(material);
41190
+ if (known) return known;
41191
+ const I = kit._;
41192
+ const THREE = kit.THREE;
41193
+ const canvas = kit.document?.createElement?.("canvas");
41194
+ const x = canvas?.getContext?.("2d");
41195
+ let made = I.MATERIALS.dark;
41196
+ if (x) {
41197
+ canvas.width = canvas.height = HOLE_TEXTURE_SIZE;
41198
+ const mid = HOLE_TEXTURE_SIZE / 2;
41199
+ x.fillStyle = HOLE_RIM[material] ?? HOLE_RIM.steel;
41200
+ x.beginPath();
41201
+ for (let i = 0; i < 18; i++) {
41202
+ const a = i / 18 * Math.PI * 2;
41203
+ const r = mid * (i % 2 ? 0.98 : 0.76);
41204
+ x.lineTo(mid + Math.cos(a) * r, mid + Math.sin(a) * r);
41205
+ }
41206
+ x.closePath();
41207
+ x.fill();
41208
+ x.fillStyle = "#050505";
41209
+ x.beginPath();
41210
+ x.arc(mid, mid, mid * 0.42, 0, Math.PI * 2);
41211
+ x.fill();
41212
+ const map = new THREE.CanvasTexture(canvas);
41213
+ made = new THREE.MeshStandardMaterial({ map, transparent: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -2, roughness: 0.55, metalness: material === "steel" || material === "aluminium" ? 0.6 : 0 });
41214
+ }
41215
+ cache2.set(material, made);
41216
+ return made;
41217
+ }
40842
41218
  var VehicleVisuals = class {
40843
41219
  constructor(kit, c, model) {
40844
41220
  __publicField(this, "kit");
@@ -40852,11 +41228,17 @@ var GameVehiclesThree = (() => {
40852
41228
  __publicField(this, "gone", /* @__PURE__ */ new Set());
40853
41229
  __publicField(this, "syncPlates", /* @__PURE__ */ new Map());
40854
41230
  __publicField(this, "skins", /* @__PURE__ */ new Map());
41231
+ __publicField(this, "doorTops", /* @__PURE__ */ new Map());
40855
41232
  __publicField(this, "lightsOn", true);
40856
41233
  __publicField(this, "warming");
40857
41234
  __publicField(this, "tornThisFrame", false);
41235
+ __publicField(this, "queued", false);
41236
+ __publicField(this, "holdArmed", false);
41237
+ __publicField(this, "holdFrame", -1);
40858
41238
  __publicField(this, "smoothing", /* @__PURE__ */ new Map());
40859
41239
  __publicField(this, "hood");
41240
+ ;
41241
+ c.smoothLater = (mesh2) => this.smoothLater(mesh2);
40860
41242
  this.kit = kit;
40861
41243
  this.c = c;
40862
41244
  this.model = model;
@@ -40865,6 +41247,8 @@ var GameVehiclesThree = (() => {
40865
41247
  for (const p of model.parts) this.byId.set(p.id, p);
40866
41248
  c.root.updateMatrixWorld(true);
40867
41249
  this.hood = this.nameHood();
41250
+ this.seatPanes();
41251
+ this.seatDoorHandles();
40868
41252
  this.warming = c.deform.filter((msh) => msh.geometry && msh.geometry.attributes.position && msh.geometry.attributes.position.count >= 1500);
40869
41253
  if (!this.warming.length) this.warming = null;
40870
41254
  for (const p of model.parts) {
@@ -40885,6 +41269,7 @@ var GameVehiclesThree = (() => {
40885
41269
  const id = z ? z.id : this.model.parts.find((p) => p.role === "hood")?.id;
40886
41270
  if (!id) return null;
40887
41271
  c.hood.piv.name = id;
41272
+ c.hood.piv.userData.plateOf = id;
40888
41273
  return { id, open: c.hood.open, sign: c.hood.sign || 1 };
40889
41274
  }
40890
41275
  mirrorObjects(p) {
@@ -40972,6 +41357,7 @@ var GameVehiclesThree = (() => {
40972
41357
  recess.userData.noFollow = true;
40973
41358
  const piv = new THREE.Group();
40974
41359
  piv.name = p.id;
41360
+ piv.userData.plateOf = p.id;
40975
41361
  piv.position.set(hx, cy, sk + s * 0.016);
40976
41362
  piv.userData.rigid = true;
40977
41363
  this.c.vehicle.add(piv);
@@ -40988,6 +41374,7 @@ var GameVehiclesThree = (() => {
40988
41374
  recess.userData.noFollow = true;
40989
41375
  const piv = new THREE.Group();
40990
41376
  piv.name = p.id;
41377
+ piv.userData.plateOf = p.id;
40991
41378
  piv.position.set(sk - 0.016, B.y1 - 0.05, 0);
40992
41379
  piv.userData.rigid = true;
40993
41380
  this.c.vehicle.add(piv);
@@ -41051,10 +41438,19 @@ var GameVehiclesThree = (() => {
41051
41438
  try {
41052
41439
  if (p.role === "door") {
41053
41440
  const dirty = /* @__PURE__ */ new Set();
41054
- const rect = [B.x0 + 0.03, B.x1 - 0.03, B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1), B.y1 - 0.02];
41055
- const pcs = I.cutPanel(this.c, z, rect, true, dirty).map((x) => x.mesh).filter((m) => !!m);
41441
+ const glassBox = new this.THREE.Box3();
41442
+ for (const g of I.windowsOver(this.c, z, inv)) glassBox.union(I.localBox(this.c, g, inv));
41443
+ const top = glassBox.isEmpty() ? B.y1 - 0.02 : Math.max(B.y1 - 0.02, glassBox.min.y + DOOR_TOP_OVER_PANE);
41444
+ this.doorTops.set(id, top);
41445
+ const rect = [B.x0 + 0.03, B.x1 - 0.03, B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1), top];
41446
+ const pcs = I.cutPanel(this.c, z, rect, true, dirty, { anyN: true }).map((x) => x.mesh).filter((m) => !!m);
41056
41447
  this.mend(dirty);
41057
- if (pcs.length) objs = [...pcs, ...I.followersIn(this.c, z, inv), ...I.windowsOver(this.c, z, inv)];
41448
+ if (pcs.length) {
41449
+ const panes = I.windowsOver(this.c, z, inv);
41450
+ const seals = panes.map((g) => I.paneSeals.get(g)).filter((s) => !!s);
41451
+ const mirrors = this.mirrorsOn(z);
41452
+ objs = [...pcs, ...this.doorCards(pcs, z), ...I.followersIn(this.c, z, inv), ...panes, ...seals, ...mirrors];
41453
+ }
41058
41454
  } else {
41059
41455
  const pcs = this.cutZone(z);
41060
41456
  if (pcs.length) objs = [...pcs, ...I.followersIn(this.c, z, inv)];
@@ -41072,10 +41468,124 @@ var GameVehiclesThree = (() => {
41072
41468
  const di = deform.indexOf(o);
41073
41469
  if (di >= 0) deform.splice(di, 1);
41074
41470
  }
41471
+ if (p.role === "door" && objs.length > 0) {
41472
+ this.doorEdges(piv, z, this.doorTops.get(id) ?? B.y1 - 0.02);
41473
+ this.liftDoorTrim(objs, z);
41474
+ this.cutStripsAcross(piv, z, inv);
41475
+ }
41075
41476
  piv.quaternion.copy(q);
41076
41477
  this.c.root.updateMatrixWorld(true);
41077
41478
  return objs.length > 0;
41078
41479
  }
41480
+ /** A door has edges: a band that follows the skin all round closes it to its card and hides where the cut followed the triangles of the shell; the same band, in the same paint as on a real car, lines the opening. */
41481
+ doorEdges(piv, z, y1) {
41482
+ const THREE = this.THREE;
41483
+ const B = z.box;
41484
+ const side2 = z.side < 0 ? -1 : 1;
41485
+ const flat = piv.position.z - side2 * 0.016;
41486
+ const x0 = B.x0 + 0.03;
41487
+ const x1 = B.x1 - 0.03;
41488
+ const y0 = B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1);
41489
+ const loop = [];
41490
+ const run = (ax, ay, bx, by) => {
41491
+ for (let i = 0; i < DOOR_EDGE_PIECES; i++) loop.push([ax + (bx - ax) * i / DOOR_EDGE_PIECES, ay + (by - ay) * i / DOOR_EDGE_PIECES]);
41492
+ };
41493
+ run(x0, y0, x1, y0);
41494
+ run(x1, y0, x1, y1);
41495
+ run(x1, y1, x0, y1);
41496
+ run(x0, y1, x0, y0);
41497
+ const heights = loop.map(([x, y]) => this.surface("z", side2, B, [[x, y]]) ?? flat);
41498
+ const band = (lift) => {
41499
+ const position = [];
41500
+ const index = [];
41501
+ loop.forEach(([x, y], i) => {
41502
+ position.push(x, y, heights[i] + side2 * lift, x, y, heights[i] + side2 * (lift - DOOR_DEPTH));
41503
+ const a = i * 2;
41504
+ const b = (i + 1) % loop.length * 2;
41505
+ index.push(a, a + 1, b, b, a + 1, b + 1);
41506
+ });
41507
+ const g = new THREE.BufferGeometry();
41508
+ g.setAttribute("position", new THREE.Float32BufferAttribute(position, 3));
41509
+ g.setIndex(index);
41510
+ g.computeVertexNormals();
41511
+ return g;
41512
+ };
41513
+ const paint = this.paint().clone();
41514
+ paint.side = THREE.DoubleSide;
41515
+ const edges = new THREE.Mesh(band(2e-3), paint);
41516
+ const lining = new THREE.Mesh(band(2e-3), paint);
41517
+ for (const m of [edges, lining]) {
41518
+ m.userData.noFollow = true;
41519
+ m.castShadow = false;
41520
+ this.c.vehicle.add(m);
41521
+ }
41522
+ piv.attach(edges);
41523
+ }
41524
+ /** A front door carries the mirror drawn at its corner. */
41525
+ mirrorsOn(z) {
41526
+ if (!z.mirror) return [];
41527
+ const own = (z.mirrors ?? []).filter((o) => o.parent);
41528
+ if (own.length) return own;
41529
+ const part = this.model.parts.find((q) => q.role === "mirror" && Math.sign(q.box.center[2]) === Math.sign(z.side));
41530
+ return part ? (this.extra.get(part.id) ?? []).filter((o) => o.parent) : [];
41531
+ }
41532
+ /** The panel drawn inside the door is lit like the rest of a cabin, not a black slab from end to end. */
41533
+ liftDoorTrim(objs, z) {
41534
+ const card = this.I.makeMaterial(DOOR_LINER_COLOR, { roughness: 0.85 });
41535
+ const inv = this.I.worldInverseOf(this.c);
41536
+ for (const o of objs) {
41537
+ if (!o.isMesh || o.userData.glass) continue;
41538
+ const box2 = this.I.localBox(this.c, o, inv);
41539
+ const size = box2.getSize(new this.THREE.Vector3());
41540
+ if (size.x > (z.box.x1 - z.box.x0) * 0.4 && size.y > (z.box.y1 - z.box.y0) * 0.3 && size.z < DOOR_DEPTH * 1.5 && o.material?.color?.getHSL({ h: 0, s: 0, l: 0 }).l < DOOR_LINER_DARK) o.material = card;
41541
+ }
41542
+ }
41543
+ /** A long strip drawn along the side of the car (the chrome of the belt): the part over the door goes with the door, and nothing stays across the opening. */
41544
+ cutStripsAcross(piv, z, inv) {
41545
+ const THREE = this.THREE;
41546
+ const B = z.box;
41547
+ const x0 = B.x0 + 0.03;
41548
+ const x1 = B.x1 - 0.03;
41549
+ const y0 = B.y0 - DOOR_STRIP_NEAR;
41550
+ const y1 = B.y1 + DOOR_STRIP_NEAR;
41551
+ const deform = this.c.deform ?? [];
41552
+ for (const o of [...this.c.vehicle.children]) {
41553
+ if (!o.isMesh || o === piv || deform.includes(o) || o.userData.glass || o.userData.decal || !o.visible) continue;
41554
+ const box2 = this.I.localBox(this.c, o, inv);
41555
+ const size = box2.getSize(new THREE.Vector3());
41556
+ const mid = box2.getCenter(new THREE.Vector3());
41557
+ if (Math.sign(mid.z) !== Math.sign(z.side) || size.z > DOOR_STRIP_THIN || size.y > DOOR_STRIP_THIN || Math.min(box2.max.x, x1) - Math.max(box2.min.x, x0) < (x1 - x0) * DOOR_STRIP_OVER || mid.y < y0 || mid.y > y1) continue;
41558
+ const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld);
41559
+ const v = new THREE.Vector3();
41560
+ const sdf = (x, y, zz) => {
41561
+ v.set(x, y, zz).applyMatrix4(toVehicle);
41562
+ return Math.max(x0 - v.x, v.x - x1);
41563
+ };
41564
+ this.kit.cutMesh(o, [{ sdf }], 42, { sub: DOOR_STRIP_SUB });
41565
+ const from = Math.max(box2.min.x, x0);
41566
+ const to = Math.min(box2.max.x, x1);
41567
+ const piece = this.box(to - from, size.y, size.z, o.material, (from + to) / 2, mid.y, mid.z);
41568
+ piece.userData.noFollow = true;
41569
+ piv.attach(piece);
41570
+ }
41571
+ }
41572
+ /** The inside of a door: its skin brought in by the depth of the door, in the trim's dark plastic, so the open door is a door and not one sheet of paint. */
41573
+ doorCards(skins, z) {
41574
+ const cardMaterial = this.I.makeMaterial(DOOR_CARD_COLOR, { roughness: 0.8 });
41575
+ return skins.map((skin) => {
41576
+ const card = new this.THREE.Mesh(skin.geometry, cardMaterial);
41577
+ card.position.copy(skin.position);
41578
+ card.quaternion.copy(skin.quaternion);
41579
+ card.scale.copy(skin.scale);
41580
+ const side2 = z.side < 0 ? -1 : 1;
41581
+ const inward = -side2 * DOOR_DEPTH;
41582
+ card.position.z += inward;
41583
+ card.userData.noFollow = true;
41584
+ card.castShadow = false;
41585
+ this.c.vehicle.add(card);
41586
+ return card;
41587
+ });
41588
+ }
41079
41589
  mend(dirty) {
41080
41590
  dirty.forEach((msh) => {
41081
41591
  const g = msh.geometry;
@@ -41085,7 +41595,191 @@ var GameVehiclesThree = (() => {
41085
41595
  g.computeBoundingBox();
41086
41596
  });
41087
41597
  }
41598
+ /**
41599
+ * A windshield or rear window is drawn as a flat quad, and the body under its edge is curved: the pane and its frame stand off the shell or rise past the roof. Each one
41600
+ * comes to the shell along its own normal, by how far the shell is just outside each of its vertices.
41601
+ */
41602
+ seatPanes() {
41603
+ const THREE = this.THREE;
41604
+ const c = this.c;
41605
+ c.root.updateMatrixWorld(true);
41606
+ const shell = c.deform.filter((o) => o.isMesh && !o.userData.glass && c.paints?.has(o.material));
41607
+ if (!shell.length) return;
41608
+ const ray = new THREE.Raycaster();
41609
+ ray.layers.enableAll();
41610
+ for (const pane of c.glass) {
41611
+ if (pane.userData.glass?.outline) continue;
41612
+ const seat = this.paneSeat(pane, shell, ray);
41613
+ if (!seat) continue;
41614
+ const near = c.vehicle.children.filter((o) => o.isMesh && o !== pane && !o.userData.glass && !shell.includes(o) && this.onPane(o, pane, seat));
41615
+ for (const m of [pane, ...near]) this.movePane(m, seat);
41616
+ const past = this.shellPast(pane, shell, ray, seat.normal);
41617
+ if (past > 0) for (const m of [pane, ...near]) this.movePane(m, { normal: seat.normal, samples: [{ at: seat.samples[0].at, by: past }] });
41618
+ }
41619
+ }
41620
+ /** The outward normal of a pane and how far the shell is at each sampled point (world). */
41621
+ paneSeat(pane, shell, ray) {
41622
+ const THREE = this.THREE;
41623
+ const pos = pane.geometry.attributes.position;
41624
+ const box2 = new THREE.Box3().setFromObject(pane);
41625
+ const centre = box2.getCenter(new THREE.Vector3());
41626
+ const vehicleCentre = new THREE.Box3().setFromObject(this.c.root).getCenter(new THREE.Vector3());
41627
+ const tri = new THREE.Triangle();
41628
+ const normal = new THREE.Vector3();
41629
+ let largest = 0;
41630
+ const idx = pane.geometry.index;
41631
+ const count2 = idx ? idx.count : pos.count;
41632
+ for (let f = 0; f + 2 < count2; f += 3) {
41633
+ tri.a.fromBufferAttribute(pos, idx ? idx.getX(f) : f);
41634
+ tri.b.fromBufferAttribute(pos, idx ? idx.getX(f + 1) : f + 1);
41635
+ tri.c.fromBufferAttribute(pos, idx ? idx.getX(f + 2) : f + 2);
41636
+ if (tri.getArea() > largest) {
41637
+ largest = tri.getArea();
41638
+ tri.getNormal(normal);
41639
+ }
41640
+ }
41641
+ normal.transformDirection(pane.matrixWorld);
41642
+ if (normal.dot(centre.clone().sub(vehicleCentre)) < 0) normal.negate();
41643
+ const samples = [];
41644
+ const seen = /* @__PURE__ */ new Set();
41645
+ const v = new THREE.Vector3();
41646
+ const step = Math.max(1, Math.floor(pos.count / PANE_SEAT_SAMPLES));
41647
+ for (let i = 0; i < pos.count; i += step) {
41648
+ v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
41649
+ const key = `${Math.round(v.x * 100)},${Math.round(v.y * 100)},${Math.round(v.z * 100)}`;
41650
+ if (seen.has(key)) continue;
41651
+ seen.add(key);
41652
+ const away = v.clone().sub(centre);
41653
+ away.addScaledVector(normal, -away.dot(normal)).normalize();
41654
+ let by = -Infinity;
41655
+ for (const off of [PANE_SEAT_OUT, PANE_SEAT_IN, PANE_SEAT_IN * 3]) {
41656
+ const at = v.clone().addScaledVector(away, off);
41657
+ ray.set(at.addScaledVector(normal, PANE_SEAT_REACH), normal.clone().negate());
41658
+ ray.far = PANE_SEAT_REACH * 2;
41659
+ const hit = raycastIndexed(THREE, ray, shell)[0];
41660
+ if (hit) by = Math.max(by, PANE_SEAT_REACH - hit.distance);
41661
+ }
41662
+ if (by > -Infinity) samples.push({ at: v.clone(), by });
41663
+ }
41664
+ return samples.length >= 3 ? { normal, samples } : null;
41665
+ }
41666
+ /** How far the shell still stands past a seated pane, over its triangles. */
41667
+ shellPast(pane, shell, ray, normal) {
41668
+ const THREE = this.THREE;
41669
+ const pos = pane.geometry.attributes.position;
41670
+ const idx = pane.geometry.index;
41671
+ const count2 = idx ? idx.count : pos.count;
41672
+ const tri = new THREE.Triangle();
41673
+ const step = Math.max(1, Math.floor(count2 / 3 / PANE_SEAT_SAMPLES)) * 3;
41674
+ let past = 0;
41675
+ for (let f = 0; f + 2 < count2; f += step) {
41676
+ tri.a.fromBufferAttribute(pos, idx ? idx.getX(f) : f);
41677
+ tri.b.fromBufferAttribute(pos, idx ? idx.getX(f + 1) : f + 1);
41678
+ tri.c.fromBufferAttribute(pos, idx ? idx.getX(f + 2) : f + 2);
41679
+ const mid = tri.getMidpoint(new THREE.Vector3()).applyMatrix4(pane.matrixWorld);
41680
+ ray.set(mid.addScaledVector(normal, PANE_SEAT_REACH), normal.clone().negate());
41681
+ ray.far = PANE_SEAT_REACH;
41682
+ const hit = raycastIndexed(THREE, ray, shell)[0];
41683
+ if (hit) past = Math.max(past, PANE_SEAT_REACH - hit.distance + PANE_SEAT_LIFT);
41684
+ }
41685
+ return past;
41686
+ }
41687
+ /** A frame or a seal drawn on the pane: small, and close to its plane. */
41688
+ onPane(o, pane, seat) {
41689
+ const THREE = this.THREE;
41690
+ const box2 = new THREE.Box3().setFromObject(o);
41691
+ if (box2.isEmpty() || !new THREE.Box3().setFromObject(pane).expandByScalar(PANE_FRAME_NEAR).containsBox(box2)) return false;
41692
+ const at = box2.getCenter(new THREE.Vector3());
41693
+ return Math.abs(at.clone().sub(seat.samples[0].at).dot(seat.normal)) < PANE_SEAT_REACH;
41694
+ }
41695
+ movePane(mesh2, seat) {
41696
+ const THREE = this.THREE;
41697
+ const g = mesh2.geometry.clone();
41698
+ const pos = g.attributes.position;
41699
+ const inverse = mesh2.matrixWorld.clone().invert();
41700
+ const v = new THREE.Vector3();
41701
+ for (let i = 0; i < pos.count; i++) {
41702
+ v.fromBufferAttribute(pos, i).applyMatrix4(mesh2.matrixWorld);
41703
+ let weight = 0;
41704
+ let by = 0;
41705
+ for (const s of seat.samples) {
41706
+ const w = 1 / (s.at.distanceToSquared(v) + 1e-4);
41707
+ weight += w;
41708
+ by += s.by * w;
41709
+ }
41710
+ v.addScaledVector(seat.normal, by / weight + PANE_SEAT_LIFT).applyMatrix4(inverse);
41711
+ pos.setXYZ(i, v.x, v.y, v.z);
41712
+ }
41713
+ pos.needsUpdate = true;
41714
+ g.computeBoundingBox();
41715
+ g.computeBoundingSphere();
41716
+ mesh2.geometry = g;
41717
+ }
41718
+ /** A handle is at the free edge of its door, the end away from the hinge. */
41719
+ seatDoorHandles() {
41720
+ const THREE = this.THREE;
41721
+ const c = this.c;
41722
+ const inv = this.I.worldInverseOf(c);
41723
+ for (const p of this.model.parts) {
41724
+ if (p.role !== "door") continue;
41725
+ const z = this.zone(p);
41726
+ if (!z) continue;
41727
+ const B = z.box;
41728
+ const free = z.hinge === "rear" ? B.x1 - DOOR_HANDLE_INSET : B.x0 + DOOR_HANDLE_INSET;
41729
+ for (const o of c.vehicle.children) {
41730
+ if (!o.isMesh || o.material !== this.I.MATERIALS.chrome || o.userData.glass) continue;
41731
+ const box2 = this.I.localBox(c, o, inv);
41732
+ const size = box2.getSize(new THREE.Vector3());
41733
+ const at = box2.getCenter(new THREE.Vector3());
41734
+ const isHandle = size.x > DOOR_HANDLE_MIN && size.x < DOOR_HANDLE_MAX && size.y < DOOR_HANDLE_THIN && size.z < DOOR_HANDLE_THIN;
41735
+ if (!isHandle || Math.sign(at.z) !== Math.sign(z.side) || at.x < B.x0 || at.x > B.x1 || at.y < B.y0 || at.y > B.y1) continue;
41736
+ const here = this.surface("z", z.side < 0 ? -1 : 1, B, [[at.x, at.y]]);
41737
+ if (here != null && Math.abs(here - at.z) > DOOR_HANDLE_OUTSIDE) continue;
41738
+ const skin = this.surface("z", z.side < 0 ? -1 : 1, B, [[free, at.y]]);
41739
+ o.position.x += free - at.x;
41740
+ if (skin != null) o.position.z += skin + (z.side < 0 ? -1 : 1) * (size.z / 2) - at.z;
41741
+ }
41742
+ }
41743
+ c.root.updateMatrixWorld(true);
41744
+ }
41745
+ /** The glass of a drawn mirror sits inside its rounded housing, without corners standing past it. */
41746
+ seatMirrorGlass(glass) {
41747
+ const g = glass.geometry.clone();
41748
+ g.computeBoundingBox();
41749
+ const mid = g.boundingBox.getCenter(new this.THREE.Vector3());
41750
+ g.translate(-mid.x, -mid.y, -mid.z);
41751
+ g.scale(MIRROR_GLASS_FIT, MIRROR_GLASS_FIT, MIRROR_GLASS_FIT);
41752
+ g.translate(mid.x, mid.y, mid.z);
41753
+ glass.geometry = g;
41754
+ glass.material = this.I.makeMaterial(MIRROR_GLASS_COLOR, { metalness: 0.95, roughness: 0.12 });
41755
+ }
41756
+ /** The mirror the model already draws around where the part says, if it draws one. */
41757
+ drawnMirror(p) {
41758
+ const THREE = this.THREE;
41759
+ const [cx, cy, cz] = p.box.center;
41760
+ this.c.root.updateMatrixWorld(true);
41761
+ const inv = this.I.worldInverseOf(this.c);
41762
+ const small = this.c.vehicle.children.flatMap((o) => {
41763
+ if (o.userData.glass || o.userData.decal || !(o.isMesh || o.isGroup) || !o.visible || this.c.deform?.includes(o)) return [];
41764
+ const box2 = this.I.localBox(this.c, o, inv);
41765
+ if (box2.isEmpty()) return [];
41766
+ const size = box2.getSize(new THREE.Vector3());
41767
+ return Math.max(size.x, size.y, size.z) < DRAWN_MIRROR_SIZE ? [{ o, at: box2.getCenter(new THREE.Vector3()) }] : [];
41768
+ });
41769
+ const housing = small.filter(({ at }) => Math.hypot(at.x - cx, at.y - cy, at.z - cz) < DRAWN_MIRROR_REACH && Math.abs(at.z) > Math.abs(cz) - DRAWN_MIRROR_REACH / 2);
41770
+ return small.filter(({ at }) => housing.some((h) => h.at.distanceTo(at) < DRAWN_MIRROR_MOUNT)).map(({ o }) => o);
41771
+ }
41088
41772
  makeMirror(p) {
41773
+ const drawn = this.drawnMirror(p);
41774
+ if (drawn.length) {
41775
+ for (const o of drawn) {
41776
+ if (!o.isMesh || o.material !== this.I.MATERIALS.chrome) continue;
41777
+ const size = new this.THREE.Box3().setFromObject(o).getSize(new this.THREE.Vector3());
41778
+ if (size.x < MIRROR_GLASS_THIN && size.y > MIRROR_GLASS_MIN && size.z > MIRROR_GLASS_MIN) this.seatMirrorGlass(o);
41779
+ }
41780
+ this.extra.set(p.id, drawn);
41781
+ return;
41782
+ }
41089
41783
  const [cx, cy, cz] = p.box.center;
41090
41784
  const s = cz < 0 ? -1 : 1;
41091
41785
  const mats = this.I.MATERIALS;
@@ -41126,17 +41820,28 @@ var GameVehiclesThree = (() => {
41126
41820
  }
41127
41821
  // ---- marks -------------------------------------------------------------------------------------------------------------------------------
41128
41822
  /** A bullet hole: a dark disc lying on the surface at `point` (`c.vehicle` coordinates). */
41129
- holeDecal(point, normal, diameter) {
41823
+ holeDecal(point, normal, diameter, part) {
41130
41824
  const THREE = this.THREE;
41131
- const m = new THREE.Mesh(new THREE.CircleGeometry(Math.max(6e-3, diameter / 2), 10), this.I.MATERIALS.dark);
41825
+ const pane = part?.role === "glass" ? this.panesOf(part).find((g) => g.visible) : void 0;
41826
+ if (part?.role === "glass" && (!pane || pane.userData.glass?.broken)) return;
41827
+ const radius = Math.max(6e-3, diameter / 2) * HOLE_DECAL_SCALE;
41828
+ const m = new THREE.Mesh(holeDiscGeometry(THREE), holeMaterial(this.kit, part?.material ?? "steel"));
41829
+ m.scale.set(radius, radius, 1);
41132
41830
  const n = new THREE.Vector3(...normal);
41133
- m.position.set(...point).addScaledVector(n, 4e-3);
41134
- m.lookAt(m.position.clone().add(n));
41831
+ m.position.set(...point).addScaledVector(n, 3e-3);
41832
+ this.c.vehicle.add(m);
41833
+ m.lookAt(this.c.vehicle.localToWorld(m.position.clone().add(n)));
41135
41834
  m.userData.decal = true;
41136
41835
  m.userData.noFollow = true;
41836
+ m.renderOrder = 3;
41137
41837
  m.raycast = () => {
41138
41838
  };
41139
- this.c.vehicle.add(m);
41839
+ const carrier = (part && (this.plates.get(part.id) ?? (part.id === this.hood?.id ? this.c.hood?.piv : void 0))) ?? (pane && pane.parent !== this.c.vehicle ? pane.parent : null);
41840
+ if (carrier) carrier.attach(m);
41841
+ }
41842
+ /** The door or lid a hinge group stands for. */
41843
+ plateIdOf(o) {
41844
+ return o.userData.plateOf;
41140
41845
  }
41141
41846
  spark(point, dir, count2 = 8) {
41142
41847
  const V3 = this.THREE.Vector3;
@@ -41176,10 +41881,8 @@ var GameVehiclesThree = (() => {
41176
41881
  }
41177
41882
  /** Glass: cracks (laminated) or shatters (tempered); returns whether it broke. */
41178
41883
  hitGlass(p, point, dir, force) {
41179
- const z = this.zone(p);
41180
- if (!z) return false;
41181
41884
  const V3 = this.THREE.Vector3;
41182
- const list2 = z.glass.filter((g) => g.parent && g.visible);
41885
+ const list2 = this.panesOf(p).filter((g) => g.parent && g.visible);
41183
41886
  if (!list2.length) return false;
41184
41887
  const pt = new V3(...point);
41185
41888
  let best = list2[0];
@@ -41196,9 +41899,16 @@ var GameVehiclesThree = (() => {
41196
41899
  this.kit.hitGlass(this.c, best, pt, new V3(...dir), force);
41197
41900
  return !!best.userData.glass?.broken;
41198
41901
  }
41902
+ panesOf(p) {
41903
+ return p.panes ?? this.zone(p)?.glass ?? [];
41904
+ }
41199
41905
  glassBreak(p) {
41200
41906
  const z = this.zone(p);
41201
- if (!z) return;
41907
+ if (!z) {
41908
+ for (const g of p.panes ?? []) if (g.parent && g.visible) this.kit.hitGlass(this.c, g, g.position.clone(), new this.THREE.Vector3(0, -1, 0), 0.6);
41909
+ this.gone.add(p.id);
41910
+ return;
41911
+ }
41202
41912
  for (const g of z.glass) if (g.parent && g.visible) this.I.glassBand(this.c, z, g, 4);
41203
41913
  this.gone.add(p.id);
41204
41914
  }
@@ -41226,6 +41936,7 @@ var GameVehiclesThree = (() => {
41226
41936
  smoothLater(msh) {
41227
41937
  this.smoothing.set(msh, this.I.creaseTask(msh.geometry, 42));
41228
41938
  this.tornThisFrame = true;
41939
+ this.holdArmed = true;
41229
41940
  }
41230
41941
  /** Spends at most `budgetMs` finishing the pending normal smoothing. Call once per frame. */
41231
41942
  pump(budgetMs = 2.5) {
@@ -41233,6 +41944,7 @@ var GameVehiclesThree = (() => {
41233
41944
  this.tornThisFrame = false;
41234
41945
  return;
41235
41946
  }
41947
+ if (this.queued) return;
41236
41948
  if (!this.smoothing.size && !this.warming) return;
41237
41949
  const deadline = performance.now() + budgetMs;
41238
41950
  for (const [msh, task] of this.smoothing) {
@@ -41241,6 +41953,34 @@ var GameVehiclesThree = (() => {
41241
41953
  }
41242
41954
  this.warmIndexes(deadline);
41243
41955
  }
41956
+ setQueued(isQueued) {
41957
+ this.queued = isQueued;
41958
+ }
41959
+ /** Units the shared queue may run now: 0 in the frame of the cut (the first ask after it holds the work for that frame, or `pump` closes it when there is no frame). */
41960
+ pendingUnits(frame) {
41961
+ if (this.holdArmed && frame !== void 0) {
41962
+ this.holdArmed = false;
41963
+ this.holdFrame = frame;
41964
+ }
41965
+ if (frame !== void 0 && frame === this.holdFrame) return 0;
41966
+ if (frame === void 0 && this.tornThisFrame) return 0;
41967
+ return this.smoothing.size + (this.warming ? 1 : 0);
41968
+ }
41969
+ /** One slice of the pending work, for the shared queue. */
41970
+ runStep(deadline) {
41971
+ const next = this.smoothing.entries().next();
41972
+ if (!next.done) {
41973
+ const [msh, task] = next.value;
41974
+ if (msh.geometry !== task.geo || task.step(deadline)) this.smoothing.delete(msh);
41975
+ return true;
41976
+ }
41977
+ if (!this.warming) return false;
41978
+ this.warmIndexes(deadline);
41979
+ return true;
41980
+ }
41981
+ finishAll() {
41982
+ while (this.runStep(Infinity)) ;
41983
+ }
41244
41984
  /** Measures the triangles of the heavy body meshes once, a slice per frame, so the first panel torn off finds them ready (see `triIndexOf`). */
41245
41985
  warmIndexes(deadline) {
41246
41986
  if (!this.warming) return;
@@ -41412,6 +42152,7 @@ var GameVehiclesThree = (() => {
41412
42152
  }
41413
42153
  pieceTasks.forEach((task, mesh2) => this.smoothing.set(mesh2, task));
41414
42154
  this.tornThisFrame = true;
42155
+ this.holdArmed = true;
41415
42156
  dirty.forEach((msh) => {
41416
42157
  const g = msh.geometry;
41417
42158
  if (g.userData.crease || !g.index) this.smoothLater(msh);
@@ -41514,6 +42255,7 @@ var GameVehiclesThree = (() => {
41514
42255
  let clock = 0;
41515
42256
  let model;
41516
42257
  let vis = null;
42258
+ let releaseCrease;
41517
42259
  const ps = /* @__PURE__ */ new Map();
41518
42260
  let js = [];
41519
42261
  let leaks = /* @__PURE__ */ new Map();
@@ -41598,7 +42340,9 @@ var GameVehiclesThree = (() => {
41598
42340
  rand = rng(seedBase);
41599
42341
  igniteRand = rng(strHash(`${c.key}|${id}|ignition|${options.seed ?? 0}`));
41600
42342
  steerSign = rand() < 0.5 ? -1 : 1;
42343
+ releaseCrease?.();
41601
42344
  vis = headless ? null : new VehicleVisuals(kit, c, model);
42345
+ releaseCrease = vis && options.creaseQueue ? options.creaseQueue.track(vis) : void 0;
41602
42346
  recomputeGroups();
41603
42347
  };
41604
42348
  const recomputeGroups = () => {
@@ -41909,13 +42653,13 @@ var GameVehiclesThree = (() => {
41909
42653
  sync();
41910
42654
  const M = toFrameMatrix();
41911
42655
  const v = toLocal(h.point);
41912
- const p = locate(v, 0.2);
42656
+ const p = (h.part ? ps.get(h.part) : void 0) ?? locate(v, 0.2);
41913
42657
  if (!p) return;
41914
42658
  const d = p.def;
41915
42659
  const E = h.energy ?? 0;
41916
42660
  const cal = h.caliber ?? 9;
41917
42661
  const dir = dirToLocal(h.direction ?? [0, 0, -1]).normalize();
41918
- const n = normalOf(d, v);
42662
+ const n = h.normal ? dirToLocal(h.normal).normalize() : normalOf(d, v);
41919
42663
  const res = resist(d, p);
41920
42664
  const through = E >= res;
41921
42665
  const cosInc = Math.abs(dir.dot(n));
@@ -41934,7 +42678,7 @@ var GameVehiclesThree = (() => {
41934
42678
  const hp = { point: r3(fromVehicle(v, M)), normal: r3(fromVehicle(n, new THREE.Matrix4().extractRotation(M))), diameter: r2(holeDiam), through };
41935
42679
  p.holes.push(hp);
41936
42680
  out.push(ev({ type: "hole", part: d.id, hole: hp, worldPoint, ...through ? { exitPoint: add3(worldPoint, r3(dirToWorld(dir.clone().multiplyScalar(Math.max(0.02, Math.min(d.box.size[0], d.box.size[1], d.box.size[2])))))) } : {} }));
41937
- vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam);
42681
+ vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam, d);
41938
42682
  startLeak(p, v, cal, out, dir);
41939
42683
  };
41940
42684
  switch (d.material) {
@@ -42235,6 +42979,46 @@ var GameVehiclesThree = (() => {
42235
42979
  for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT)) return true;
42236
42980
  return false;
42237
42981
  };
42982
+ const surfaceRays = new THREE.Raycaster();
42983
+ surfaceRays.layers.enableAll();
42984
+ surfaceRays.camera = new THREE.PerspectiveCamera();
42985
+ let meshParts = null;
42986
+ const paneParts = () => {
42987
+ if (meshParts) return meshParts;
42988
+ meshParts = /* @__PURE__ */ new Map();
42989
+ for (const d of model.parts) {
42990
+ if (d.role !== "glass") continue;
42991
+ const z = kit.zonesOf(c).find((q) => q.id === d.zoneId);
42992
+ for (const g of z?.glass ?? d.panes ?? []) meshParts.set(g, d.id);
42993
+ }
42994
+ return meshParts;
42995
+ };
42996
+ const isDrawnSurface = (o) => {
42997
+ for (let q = o; q && q !== c.vehicle; q = q.parent) if (!q.visible || q.userData.decal || q.userData.ghost) return false;
42998
+ return true;
42999
+ };
43000
+ const partOfSurface = (o, v) => {
43001
+ const pane = paneParts().get(o);
43002
+ if (pane) return ps.get(pane) ?? null;
43003
+ for (let q = o; q && q !== c.vehicle; q = q.parent) {
43004
+ if (vis?.plateIdOf(q)) return ps.get(vis.plateIdOf(q)) ?? null;
43005
+ }
43006
+ return locate(v, 0.08, (p) => p.def.role === "glass");
43007
+ };
43008
+ const surfaceRay = (ray) => {
43009
+ sync();
43010
+ surfaceRays.set(new V3(ray.origin[0], ray.origin[1], ray.origin[2]), new V3(ray.direction[0], ray.direction[1], ray.direction[2]));
43011
+ surfaceRays.far = ray.maxDistance;
43012
+ for (const h of surfaceRays.intersectObject(c.vehicle, true)) {
43013
+ if (!h.face || !isDrawnSurface(h.object)) continue;
43014
+ const p = partOfSurface(h.object, c.vehicle.worldToLocal(h.point.clone()));
43015
+ if (!p || p.lost || ray.ignored?.(p.def.id)) continue;
43016
+ const n = h.face.normal.clone().transformDirection(h.object.matrixWorld);
43017
+ if (n.dot(surfaceRays.ray.direction) > 0) n.negate();
43018
+ return { part: p.def.id, point: [h.point.x, h.point.y, h.point.z], normal: [n.x, n.y, n.z], distance: h.distance, pathLen: 0.02 };
43019
+ }
43020
+ return null;
43021
+ };
42238
43022
  const hit = (h) => {
42239
43023
  stateCache = void 0;
42240
43024
  const out = [];
@@ -42357,9 +43141,14 @@ var GameVehiclesThree = (() => {
42357
43141
  },
42358
43142
  describe,
42359
43143
  hit,
43144
+ surfaceRay,
42360
43145
  update,
42361
43146
  state,
42362
43147
  reset,
43148
+ dispose: () => {
43149
+ releaseCrease?.();
43150
+ releaseCrease = void 0;
43151
+ },
42363
43152
  disassemble,
42364
43153
  extinguish,
42365
43154
  serialize: () => ({
@@ -42395,6 +43184,387 @@ var GameVehiclesThree = (() => {
42395
43184
  root.userData.vehicle = vehicle;
42396
43185
  return { root, vehicle };
42397
43186
  }
43187
+ function createCreaseQueue() {
43188
+ const tracked = [];
43189
+ let cursor = 0;
43190
+ let lastFrame;
43191
+ let attached = false;
43192
+ const deferred = {
43193
+ id: DEFERRED_QUEUE_ID.VEHICLE_CREASE,
43194
+ pending(frame) {
43195
+ lastFrame = frame;
43196
+ let units = 0;
43197
+ for (const work of tracked) units += work.pendingUnits(frame);
43198
+ return units;
43199
+ },
43200
+ run(deadline, clock) {
43201
+ let ran = 0;
43202
+ let skipped = 0;
43203
+ while (tracked.length > 0 && skipped < tracked.length) {
43204
+ if (ran > 0 && clock() >= deadline) break;
43205
+ const work = tracked[cursor % tracked.length];
43206
+ cursor = (cursor + 1) % tracked.length;
43207
+ if (work.pendingUnits(lastFrame) > 0 && work.runStep(deadline)) {
43208
+ ran++;
43209
+ skipped = 0;
43210
+ } else skipped++;
43211
+ }
43212
+ return ran;
43213
+ },
43214
+ flush() {
43215
+ for (const work of tracked) work.finishAll();
43216
+ },
43217
+ attach() {
43218
+ attached = true;
43219
+ for (const work of tracked) work.setQueued(true);
43220
+ },
43221
+ detach() {
43222
+ attached = false;
43223
+ for (const work of tracked) work.setQueued(false);
43224
+ }
43225
+ };
43226
+ return {
43227
+ deferredQueue: () => deferred,
43228
+ track(work) {
43229
+ tracked.push(work);
43230
+ work.setQueued(attached);
43231
+ return () => {
43232
+ const at = tracked.indexOf(work);
43233
+ if (at < 0) return;
43234
+ tracked.splice(at, 1);
43235
+ work.setQueued(false);
43236
+ };
43237
+ }
43238
+ };
43239
+ }
43240
+ var CABIN_LEAK = 0.02;
43241
+ var HOLE_RATE_PER_AREA = 3e3;
43242
+ var CRACK_GAP_PER_METRE = 0.5;
43243
+ var THROUGH = 0.18;
43244
+ var HOLE_PATCH = 0.12;
43245
+ var FULL_SWING = 1.15;
43246
+ var SHELL_ROLES = /* @__PURE__ */ new Set(["door", "glass", "roof", "lid", "panel", "pillar"]);
43247
+ var GONE = /* @__PURE__ */ new Set(["broken", "melted", "ashes"]);
43248
+ function cabinBoxOf(vehicle) {
43249
+ const cabin = vehicle.meta.cabin;
43250
+ if (!cabin || !(cabin.x1 > cabin.x0) || !(cabin.y1 > cabin.y0) || !(cabin.z > 0)) return null;
43251
+ return cabin;
43252
+ }
43253
+ function swingOf(vehicle, partId) {
43254
+ const pivot = vehicle.vehicle?.getObjectByName?.(partId);
43255
+ const q = pivot?.quaternion;
43256
+ if (!q) return 0;
43257
+ const angle = 2 * Math.acos(Math.min(1, Math.abs(q.w)));
43258
+ return Math.min(1, angle / FULL_SWING);
43259
+ }
43260
+ var overlaps = (b, c, margin) => Math.abs(b.center[0] - (c.x0 + c.x1) / 2) <= b.size[0] / 2 + (c.x1 - c.x0) / 2 + margin && Math.abs(b.center[1] - (c.y0 + c.y1) / 2) <= b.size[1] / 2 + (c.y1 - c.y0) / 2 + margin && Math.abs(b.center[2]) <= b.size[2] / 2 + c.z + margin;
43261
+ function cabinEnclosure(source, options = {}) {
43262
+ const c = source.vehicle;
43263
+ const cabin = cabinBoxOf(c);
43264
+ const node = c.vehicle;
43265
+ if (!cabin || !node?.localToWorld) return null;
43266
+ const state = source.state();
43267
+ if (state.destroyed) return null;
43268
+ const V3 = node.position.constructor;
43269
+ const Quat = node.quaternion.constructor;
43270
+ const mid = [(cabin.x0 + cabin.x1) / 2, (cabin.y0 + cabin.y1) / 2, 0];
43271
+ const half = [(cabin.x1 - cabin.x0) / 2, (cabin.y1 - cabin.y0) / 2, cabin.z];
43272
+ const ajar = options.ajar ?? 0.06;
43273
+ const byId = new Map(state.parts.map((p) => [p.id, p]));
43274
+ const mainGroup = state.groups[0] ? new Set(state.groups[0]) : null;
43275
+ const openings = [];
43276
+ let crackLength = 0;
43277
+ const toCabin = (b) => ({
43278
+ center: [b.center[0] - mid[0], b.center[1] - mid[1], b.center[2] - mid[2]],
43279
+ halfSize: [b.size[0] / 2 + THROUGH, b.size[1] / 2 + THROUGH, b.size[2] / 2 + THROUGH]
43280
+ });
43281
+ node.updateWorldMatrix?.(true, false);
43282
+ const frame = options.frame ?? c.root;
43283
+ frame?.updateWorldMatrix?.(true, false);
43284
+ const inverse = node.matrixWorld.clone().invert();
43285
+ for (const def of source.model.parts) {
43286
+ if (!SHELL_ROLES.has(def.role) || !overlaps(def.box, cabin, 0.12)) continue;
43287
+ const part = byId.get(def.id);
43288
+ if (!part) continue;
43289
+ const gone = GONE.has(part.status) || mainGroup !== null && !mainGroup.has(def.id);
43290
+ const swung = (def.role === "door" || def.role === "lid") && swingOf(c, def.id) > ajar;
43291
+ if (gone || swung) {
43292
+ openings.push(toCabin(def.box));
43293
+ continue;
43294
+ }
43295
+ for (const hole of part.holes) {
43296
+ if (!hole.through && def.material !== "glass") continue;
43297
+ const area3 = Math.PI * (hole.diameter / 2) ** 2;
43298
+ const p = new V3(hole.point[0], hole.point[1], hole.point[2]);
43299
+ if (frame?.localToWorld) frame.localToWorld(p);
43300
+ p.applyMatrix4(inverse);
43301
+ openings.push({
43302
+ center: [p.x - mid[0], p.y - mid[1], p.z - mid[2]],
43303
+ halfSize: [HOLE_PATCH, HOLE_PATCH, HOLE_PATCH],
43304
+ // the patch is far bigger than the hole: only a share of what crosses it passes, more for a bigger hole
43305
+ rate: Math.min(4, area3 * HOLE_RATE_PER_AREA)
43306
+ });
43307
+ }
43308
+ for (const crack of part.cracks) crackLength += crack.length;
43309
+ }
43310
+ const center = node.localToWorld(new V3(mid[0], mid[1], mid[2]));
43311
+ const q = node.getWorldQuaternion(new Quat());
43312
+ const scale = node.getWorldScale(new V3());
43313
+ const leak = (options.leak ?? CABIN_LEAK) * (1 + crackLength * CRACK_GAP_PER_METRE);
43314
+ return {
43315
+ center: [center.x, center.y, center.z],
43316
+ halfSize: [half[0] * Math.abs(scale.x), half[1] * Math.abs(scale.y), half[2] * Math.abs(scale.z)],
43317
+ orientation: [q.x, q.y, q.z, q.w],
43318
+ openings: openings.map((o) => ({ ...o, center: [o.center[0] * scale.x, o.center[1] * scale.y, o.center[2] * scale.z], halfSize: [o.halfSize[0] * Math.abs(scale.x), o.halfSize[1] * Math.abs(scale.y), o.halfSize[2] * Math.abs(scale.z)] })),
43319
+ leak
43320
+ };
43321
+ }
43322
+
43323
+ // src/cabin-smoke.ts
43324
+ function createCabinSmoke() {
43325
+ const tracked = /* @__PURE__ */ new Map();
43326
+ const end = (id, entry, effects) => {
43327
+ if (entry.declared) effects?.enclose?.(id, null);
43328
+ tracked.delete(id);
43329
+ };
43330
+ return {
43331
+ track(id, source, frame) {
43332
+ tracked.set(id, { source, frame, declared: false, placed: false });
43333
+ },
43334
+ untrack(id, effects) {
43335
+ const entry = tracked.get(id);
43336
+ if (entry) end(id, entry, effects);
43337
+ },
43338
+ sync(effects) {
43339
+ if (!effects?.enclose) return;
43340
+ for (const [id, entry] of tracked) {
43341
+ if (!entry.frame.parent) {
43342
+ if (entry.placed) end(id, entry, effects);
43343
+ continue;
43344
+ }
43345
+ entry.placed = true;
43346
+ const enclosure = cabinEnclosure(entry.source, { frame: entry.frame });
43347
+ if (enclosure || entry.declared) effects.enclose(id, enclosure);
43348
+ entry.declared = enclosure !== null;
43349
+ }
43350
+ },
43351
+ clear(effects) {
43352
+ for (const [id, entry] of [...tracked]) end(id, entry, effects);
43353
+ },
43354
+ ids: () => [...tracked.keys()]
43355
+ };
43356
+ }
43357
+
43358
+ // ../game-render-three/dist/index.js
43359
+ var DEFAULT_ANGLE = Math.PI / 3;
43360
+ var EFFECT_STEP_SECONDS = 1 / 50;
43361
+ function isRenderPools(value) {
43362
+ const candidate = value;
43363
+ return typeof candidate === "object" && candidate !== null && typeof candidate.geometries?.acquire === "function" && typeof candidate.materials?.acquire === "function";
43364
+ }
43365
+ var BATCHED_LAYER = 30;
43366
+ var MATRIX_EPSILON = 1e-6;
43367
+ var ATTRIBUTES = ["position", "normal", "uv", "color"];
43368
+ var isShown = (object2, root) => {
43369
+ for (let node = object2; node; node = node.parent) {
43370
+ if (node === root) return true;
43371
+ if (!node.visible) return false;
43372
+ }
43373
+ return false;
43374
+ };
43375
+ var isBatchable = (mesh2) => {
43376
+ const flags = mesh2;
43377
+ if (flags.isInstancedMesh === true || flags.isSkinnedMesh === true || Array.isArray(mesh2.material)) return false;
43378
+ const geometry = mesh2.geometry;
43379
+ return geometry.attributes.position !== void 0 && Object.keys(geometry.morphAttributes).length === 0;
43380
+ };
43381
+ var signatureOf = (mesh2) => {
43382
+ const { attributes } = mesh2.geometry;
43383
+ const present = ATTRIBUTES.filter((name) => attributes[name] !== void 0).map((name) => `${name}${attributes[name].itemSize}`);
43384
+ return `${mesh2.material.uuid}|${present.join(",")}|${mesh2.castShadow ? 1 : 0}${mesh2.receiveShadow ? 1 : 0}`;
43385
+ };
43386
+ var sameMatrix = (a, b) => a.elements.every((value, index) => Math.abs(value - b.elements[index]) < MATRIX_EPSILON);
43387
+ function createStaticBatcher(THREE, root, options = {}) {
43388
+ const include = options.include ?? (() => true);
43389
+ const follow = options.followMeshes === true;
43390
+ root.updateMatrixWorld(true);
43391
+ const inverse = new THREE.Matrix4().copy(root.matrixWorld).invert();
43392
+ const groups = /* @__PURE__ */ new Map();
43393
+ root.traverse((object2) => {
43394
+ const mesh2 = object2;
43395
+ if (!mesh2.isMesh || !isShown(mesh2, root) || !isBatchable(mesh2) || !include(mesh2)) return;
43396
+ const key = signatureOf(mesh2);
43397
+ groups.set(key, [...groups.get(key) ?? [], mesh2]);
43398
+ });
43399
+ const group = new THREE.Group();
43400
+ group.name = "static-batches";
43401
+ const sources = [];
43402
+ const pending = [...groups.values()].filter((meshes) => meshes.length >= 2);
43403
+ const mergeSome = (deadline) => {
43404
+ while (pending.length > 0) {
43405
+ const meshes = pending.shift();
43406
+ if (meshes.every((mesh2) => isShown(mesh2, root))) sources.push(...mergeGroup(THREE, { meshes, inverse: new THREE.Matrix4().copy(root.matrixWorld).invert(), group, follow }));
43407
+ if (performance.now() >= deadline) return;
43408
+ }
43409
+ };
43410
+ if (options.budgetMs === void 0) mergeSome(Number.POSITIVE_INFINITY);
43411
+ root.add(group);
43412
+ const scratch2 = new THREE.Matrix4();
43413
+ let released = 0;
43414
+ const release = (source) => {
43415
+ source.released = true;
43416
+ source.mesh.layers.mask = source.mask;
43417
+ const { index } = source.batch;
43418
+ const first = index.array[source.start];
43419
+ for (let at = source.start; at < source.start + source.count; at++) index.array[at] = first;
43420
+ index.needsUpdate = true;
43421
+ if (--source.batch.live === 0) source.batch.mesh.visible = false;
43422
+ released++;
43423
+ };
43424
+ const hasChanged = (source) => {
43425
+ const { mesh: mesh2 } = source;
43426
+ if (mesh2.geometry !== source.geometry || mesh2.material !== source.material || !isShown(mesh2, root)) return true;
43427
+ if (mesh2.geometry.attributes.position.version !== source.version) return true;
43428
+ if (follow || sameMatrix(mesh2.matrixWorld, source.world)) return false;
43429
+ if (!sameMatrix(scratch2.multiplyMatrices(inverse, mesh2.matrixWorld), source.local)) return true;
43430
+ source.world.copy(mesh2.matrixWorld);
43431
+ return false;
43432
+ };
43433
+ return {
43434
+ group,
43435
+ update() {
43436
+ if (pending.length > 0) mergeSome(performance.now() + (options.budgetMs ?? Number.POSITIVE_INFINITY));
43437
+ const before = released;
43438
+ inverse.copy(root.matrixWorld).invert();
43439
+ for (const source of sources) if (!source.released && hasChanged(source)) release(source);
43440
+ return released - before;
43441
+ },
43442
+ stats: () => ({ batches: group.children.length, batched: sources.filter((source) => !source.released).length, released }),
43443
+ dispose() {
43444
+ for (const source of sources) if (!source.released) source.mesh.layers.mask = source.mask;
43445
+ group.removeFromParent();
43446
+ for (const child of group.children) {
43447
+ ;
43448
+ child.geometry.dispose();
43449
+ child.skeleton?.dispose();
43450
+ }
43451
+ }
43452
+ };
43453
+ }
43454
+ function copyAttribute(name, attribute, target, offset, matrix, normalMatrix2) {
43455
+ const size = attribute.itemSize;
43456
+ const source = attribute.array;
43457
+ const flat = !attribute.isInterleavedBufferAttribute && source.length === attribute.count * size;
43458
+ if (name !== "position" && name !== "normal") {
43459
+ if (flat) target.set(source, offset * size);
43460
+ else for (let at = 0; at < attribute.count; at++) for (let k = 0; k < size; k++) target[(offset + at) * size + k] = attribute.getComponent(at, k);
43461
+ return;
43462
+ }
43463
+ const e = (name === "position" ? matrix : normalMatrix2).elements;
43464
+ const isNormal = name === "normal";
43465
+ for (let at = 0; at < attribute.count; at++) {
43466
+ const x = attribute.getX(at);
43467
+ const y = attribute.getY(at);
43468
+ const z = attribute.getZ(at);
43469
+ const out = (offset + at) * 3;
43470
+ if (isNormal) {
43471
+ const nx = e[0] * x + e[3] * y + e[6] * z;
43472
+ const ny = e[1] * x + e[4] * y + e[7] * z;
43473
+ const nz = e[2] * x + e[5] * y + e[8] * z;
43474
+ const length = Math.hypot(nx, ny, nz) || 1;
43475
+ target[out] = nx / length;
43476
+ target[out + 1] = ny / length;
43477
+ target[out + 2] = nz / length;
43478
+ } else {
43479
+ target[out] = e[0] * x + e[4] * y + e[8] * z + e[12];
43480
+ target[out + 1] = e[1] * x + e[5] * y + e[9] * z + e[13];
43481
+ target[out + 2] = e[2] * x + e[6] * y + e[10] * z + e[14];
43482
+ }
43483
+ }
43484
+ }
43485
+ function skinnedBatch(THREE, merged, material, meshes) {
43486
+ const vertices = merged.attributes.position.count;
43487
+ const skinIndex = new Uint16Array(vertices * 4);
43488
+ const skinWeight = new Float32Array(vertices * 4);
43489
+ let at = 0;
43490
+ meshes.forEach((mesh22, bone) => {
43491
+ for (let k = 0; k < mesh22.geometry.attributes.position.count; k++, at++) {
43492
+ skinIndex[at * 4] = bone;
43493
+ skinWeight[at * 4] = 1;
43494
+ }
43495
+ });
43496
+ merged.setAttribute("skinIndex", new THREE.BufferAttribute(skinIndex, 4));
43497
+ merged.setAttribute("skinWeight", new THREE.BufferAttribute(skinWeight, 4));
43498
+ const mesh2 = new THREE.SkinnedMesh(merged, material);
43499
+ const identities = meshes.map(() => new THREE.Matrix4());
43500
+ mesh2.bind(new THREE.Skeleton(meshes, identities), new THREE.Matrix4());
43501
+ mesh2.frustumCulled = false;
43502
+ return mesh2;
43503
+ }
43504
+ function mergeGroup(THREE, { meshes, inverse: rootInverse, group, follow }) {
43505
+ const identity = new THREE.Matrix4();
43506
+ const inverse = follow ? identity : rootInverse;
43507
+ const first = meshes[0];
43508
+ const names = ATTRIBUTES.filter((name) => first.geometry.attributes[name] !== void 0);
43509
+ const pieces = meshes.map((mesh22) => {
43510
+ const local = new THREE.Matrix4().multiplyMatrices(inverse, mesh22.matrixWorld);
43511
+ const vertices = mesh22.geometry.attributes.position.count;
43512
+ const indexCount = mesh22.geometry.index ? mesh22.geometry.index.count : vertices;
43513
+ return { mesh: mesh22, local, vertices, indexCount };
43514
+ });
43515
+ const totalVertices = pieces.reduce((sum, piece) => sum + piece.vertices, 0);
43516
+ const totalIndices = pieces.reduce((sum, piece) => sum + piece.indexCount, 0);
43517
+ const merged = new THREE.BufferGeometry();
43518
+ const normalMatrix2 = new THREE.Matrix3();
43519
+ for (const name of names) {
43520
+ const size = first.geometry.attributes[name].itemSize;
43521
+ const array = new Float32Array(totalVertices * size);
43522
+ let offset = 0;
43523
+ for (const piece of pieces) {
43524
+ const matrix = follow ? identity : piece.local;
43525
+ copyAttribute(name, piece.mesh.geometry.attributes[name], array, offset, matrix, normalMatrix2.getNormalMatrix(matrix));
43526
+ offset += piece.vertices;
43527
+ }
43528
+ merged.setAttribute(name, new THREE.BufferAttribute(array, size));
43529
+ }
43530
+ const indexArray2 = totalVertices > 65535 ? new Uint32Array(totalIndices) : new Uint16Array(totalIndices);
43531
+ const indexAttribute = new THREE.BufferAttribute(indexArray2, 1);
43532
+ merged.setIndex(indexAttribute);
43533
+ const mesh2 = follow ? skinnedBatch(THREE, merged, first.material, meshes) : new THREE.Mesh(merged, first.material);
43534
+ mesh2.name = `batch:${first.material.name || first.material.type}`;
43535
+ mesh2.castShadow = first.castShadow;
43536
+ mesh2.receiveShadow = first.receiveShadow;
43537
+ mesh2.raycast = () => void 0;
43538
+ merged.computeBoundingBox();
43539
+ merged.computeBoundingSphere();
43540
+ group.add(mesh2);
43541
+ const batch = { mesh: mesh2, live: pieces.length, index: indexAttribute };
43542
+ const sources = [];
43543
+ let vertexOffset = 0;
43544
+ let indexOffset = 0;
43545
+ for (const piece of pieces) {
43546
+ const index = piece.mesh.geometry.index;
43547
+ if (index) for (let at = 0; at < piece.indexCount; at++) indexArray2[indexOffset + at] = index.getX(at) + vertexOffset;
43548
+ else for (let at = 0; at < piece.indexCount; at++) indexArray2[indexOffset + at] = at + vertexOffset;
43549
+ sources.push({
43550
+ mesh: piece.mesh,
43551
+ geometry: piece.mesh.geometry,
43552
+ material: piece.mesh.material,
43553
+ version: piece.mesh.geometry.attributes.position.version,
43554
+ local: piece.local,
43555
+ world: piece.mesh.matrixWorld.clone(),
43556
+ mask: piece.mesh.layers.mask,
43557
+ batch,
43558
+ start: indexOffset,
43559
+ count: piece.indexCount,
43560
+ released: false
43561
+ });
43562
+ piece.mesh.layers.set(BATCHED_LAYER);
43563
+ vertexOffset += piece.vertices;
43564
+ indexOffset += piece.indexCount;
43565
+ }
43566
+ return sources;
43567
+ }
42398
43568
 
42399
43569
  // ../game-vehicles-driving/dist/index.js
42400
43570
  var defaultArcadeSpec = {
@@ -42811,6 +43981,169 @@ var GameVehiclesThree = (() => {
42811
43981
  return driver;
42812
43982
  }
42813
43983
 
43984
+ // src/vehicle-lod.ts
43985
+ var VEHICLE_LOD_DISTANCE = 14;
43986
+ var BATCH_BUDGET_MS = 2;
43987
+ var isShown2 = (object2, top) => {
43988
+ for (let node = object2; node && node !== top; node = node.parent) if (node.visible === false) return false;
43989
+ return true;
43990
+ };
43991
+ function createVehicleLod(kit, context, root, full) {
43992
+ const THREE = kit.THREE;
43993
+ let isUploaded = false;
43994
+ let modelKey = "";
43995
+ const culled = /* @__PURE__ */ new Map();
43996
+ const keyOf = () => full.children.filter((child) => child.name !== "static-batches").map((child) => child.uuid).join();
43997
+ const armUpload = () => {
43998
+ for (const [mesh2, wasCulled] of culled) mesh2.frustumCulled = wasCulled;
43999
+ culled.clear();
44000
+ modelKey = keyOf();
44001
+ isUploaded = false;
44002
+ full.traverse((object2) => {
44003
+ if (!object2.isMesh || culled.has(object2)) return;
44004
+ culled.set(object2, object2.frustumCulled);
44005
+ object2.frustumCulled = false;
44006
+ });
44007
+ const probe = [...culled.keys()][0];
44008
+ if (!probe) {
44009
+ isUploaded = true;
44010
+ return;
44011
+ }
44012
+ const before = probe.onAfterRender;
44013
+ probe.onAfterRender = function(...args) {
44014
+ isUploaded = true;
44015
+ probe.onAfterRender = before;
44016
+ before?.apply(this, args);
44017
+ };
44018
+ };
44019
+ armUpload();
44020
+ let lite = null;
44021
+ let isFused = false;
44022
+ let isLocked = false;
44023
+ let batcher = null;
44024
+ let shadow = null;
44025
+ const castsShadow = /* @__PURE__ */ new Set();
44026
+ const shadowProxyOf = (fused) => {
44027
+ const positions = [];
44028
+ const indices = [];
44029
+ for (const mesh2 of fused.children) {
44030
+ if (!mesh2.isMesh || !mesh2.castShadow) continue;
44031
+ const geometry2 = mesh2.geometry.clone().applyMatrix4(mesh2.matrix);
44032
+ const offset = positions.length / 3;
44033
+ const position = geometry2.attributes.position;
44034
+ for (let at = 0; at < position.count; at++) positions.push(position.getX(at), position.getY(at), position.getZ(at));
44035
+ const index = geometry2.index ? Array.from(geometry2.index.array) : Array.from({ length: position.count }, (_, at) => at);
44036
+ for (const vertex of index) indices.push(vertex + offset);
44037
+ geometry2.dispose();
44038
+ }
44039
+ const geometry = new THREE.BufferGeometry();
44040
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
44041
+ geometry.setIndex(indices);
44042
+ geometry.computeBoundingSphere();
44043
+ const proxy = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ colorWrite: false, depthWrite: false }));
44044
+ proxy.name = "vehicle-shadow";
44045
+ markKitOrigin(proxy, { owner: "game-vehicles-three", kind: "vehicle", id: root.name });
44046
+ proxy.castShadow = true;
44047
+ proxy.userData.skipLite = true;
44048
+ proxy.raycast = () => void 0;
44049
+ root.add(proxy);
44050
+ return proxy;
44051
+ };
44052
+ const drawFull = () => {
44053
+ if (!batcher && isLocked) return;
44054
+ if (!batcher) {
44055
+ lite ?? (lite = build());
44056
+ lite.visible = isFused;
44057
+ shadow ?? (shadow = shadowProxyOf(lite));
44058
+ shadow.visible = !isFused;
44059
+ full.traverse((object2) => {
44060
+ if (!object2.isMesh || !object2.castShadow) return;
44061
+ castsShadow.add(object2);
44062
+ object2.castShadow = false;
44063
+ });
44064
+ batcher = createStaticBatcher(THREE, full, { budgetMs: BATCH_BUDGET_MS });
44065
+ batcher.group.traverse((object2) => object2.userData.skipLite = true);
44066
+ markKitOrigin(batcher.group, { owner: "game-vehicles-three", kind: "vehicle", id: root.name });
44067
+ return;
44068
+ }
44069
+ batcher.update();
44070
+ if (!isLocked && batcher.stats().batched === 0 && batcher.stats().released > 0) {
44071
+ batcher.dispose();
44072
+ batcher = null;
44073
+ }
44074
+ };
44075
+ const build = () => {
44076
+ root.updateMatrixWorld(true);
44077
+ const inverse = new THREE.Matrix4().copy(root.matrixWorld).invert();
44078
+ const textured = [];
44079
+ full.traverse((object2) => {
44080
+ if (!object2.isMesh || object2.userData.skipLite || !isShown2(object2, full)) return;
44081
+ const materials = Array.isArray(object2.material) ? object2.material : [object2.material];
44082
+ if (materials.some((material) => material?.map && !/plate/i.test(material.name ?? "")) && !/plate/i.test(object2.name ?? "")) textured.push(object2);
44083
+ });
44084
+ for (const mesh2 of textured) mesh2.userData.skipLite = true;
44085
+ const fusion = fuse(THREE, { ...context, vehicle: full }, inverse, null, primaryPaint(context), kit.lodHiddenMaterials());
44086
+ for (const mesh2 of textured) delete mesh2.userData.skipLite;
44087
+ const group = new THREE.Group();
44088
+ group.name = "vehicle-lite";
44089
+ markKitOrigin(group, { owner: "game-vehicles-three", kind: "vehicle", id: root.name });
44090
+ for (const mesh2 of liteMeshes(THREE, fusion, liteMaterials(THREE, kit.studioEnvironment()))) group.add(mesh2);
44091
+ for (const mesh2 of textured) {
44092
+ const copy = new THREE.Mesh(mesh2.geometry, mesh2.material);
44093
+ copy.matrixAutoUpdate = false;
44094
+ copy.matrix.multiplyMatrices(inverse, mesh2.matrixWorld);
44095
+ copy.castShadow = mesh2.castShadow;
44096
+ copy.receiveShadow = mesh2.receiveShadow;
44097
+ group.add(copy);
44098
+ }
44099
+ root.add(group);
44100
+ return group;
44101
+ };
44102
+ const show = (shouldFuse) => {
44103
+ if (shouldFuse === isFused) return;
44104
+ isFused = shouldFuse;
44105
+ full.visible = !shouldFuse;
44106
+ if (lite) lite.visible = shouldFuse;
44107
+ if (shadow) shadow.visible = !shouldFuse;
44108
+ };
44109
+ const api = {
44110
+ update(distance, touched, posed = false) {
44111
+ isLocked || (isLocked = touched);
44112
+ if (keyOf() !== modelKey) {
44113
+ api.dispose();
44114
+ armUpload();
44115
+ }
44116
+ if (!isUploaded) return show(false);
44117
+ if (culled.size > 0) {
44118
+ for (const [mesh2, wasCulled] of culled) mesh2.frustumCulled = wasCulled;
44119
+ culled.clear();
44120
+ }
44121
+ const shouldFuse = !isLocked && !posed && distance > (isFused ? VEHICLE_LOD_DISTANCE * 0.9 : VEHICLE_LOD_DISTANCE);
44122
+ if (shouldFuse && !lite) lite = build();
44123
+ show(shouldFuse);
44124
+ if (!shouldFuse) drawFull();
44125
+ },
44126
+ get fused() {
44127
+ return isFused;
44128
+ },
44129
+ dispose() {
44130
+ show(false);
44131
+ batcher?.dispose();
44132
+ batcher = null;
44133
+ for (const mesh2 of castsShadow) mesh2.castShadow = true;
44134
+ castsShadow.clear();
44135
+ shadow?.removeFromParent();
44136
+ shadow?.geometry.dispose();
44137
+ shadow = null;
44138
+ if (!lite) return;
44139
+ lite.removeFromParent();
44140
+ lite.traverse((object2) => object2.geometry?.dispose());
44141
+ lite = null;
44142
+ }
44143
+ };
44144
+ return api;
44145
+ }
44146
+
42814
44147
  // src/test-area/text.ts
42815
44148
  var scenarioIds = [
42816
44149
  "vehicles_9mm_windshield",
@@ -43202,7 +44535,7 @@ var GameVehiclesThree = (() => {
43202
44535
  ];
43203
44536
  };
43204
44537
 
43205
- // ../game-audio/dist/index.js
44538
+ // ../game-audio/dist/chunk-DZITVFMJ.js
43206
44539
  var DISTANCE_CURVES = {
43207
44540
  /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
43208
44541
  game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
@@ -43217,31 +44550,6 @@ var GameVehiclesThree = (() => {
43217
44550
  /** The game's animal calls: `max(0, 1 − d / 70) × 0.25`, silent at or below 0.005 (jogo L66627–66628). */
43218
44551
  animals: { model: "linear", range: 70, scale: 0.25, minGain: 5e-3 }
43219
44552
  };
43220
- var mix_profiles_default = [
43221
- {
43222
- id: "game",
43223
- master: 0.5,
43224
- noise: { seconds: 1.5, create: "lazy" },
43225
- sources: ["jogo:L62077-62081 master 0.5 to the output", "jogo:L62082-62088 noise 1.5 s made by the first sound that needs it"]
43226
- },
43227
- {
43228
- id: "art_scene",
43229
- master: 0.85,
43230
- compressor: { threshold: -12, ratio: 5 },
43231
- reverb: { seconds: 2.2, decay: 3.4, wet: 0.4 },
43232
- noise: { seconds: 2, create: "eager" },
43233
- sources: [
43234
- "cena:L1308 compressor -12 dB, ratio 5",
43235
- "cena:L1309 master 0.85",
43236
- "cena:L1310 convolver, impulse 2.2 s of (1 - i/len)^3.4",
43237
- "cena:L1311 wet 0.4",
43238
- "cena:L1312 noise 2 s made with the chain"
43239
- ]
43240
- }
43241
- ];
43242
- var MIX_PROFILES = Object.fromEntries(
43243
- mix_profiles_default.map((profile) => [profile.id, profile])
43244
- );
43245
44553
  var game_sfx_default = {
43246
44554
  sources: ["jogo:L62089-62107"],
43247
44555
  note: "The game's sfx table P: [lowpass cutoff Hz, peak gain, seconds]. Played as a noise burst with the game's distance curve (falloff game_sfx) and a 0.05 s tail. Team names in `team`.",
@@ -43374,6 +44682,31 @@ var GameVehiclesThree = (() => {
43374
44682
  var LIBRARY_SOUNDS = [horn_default, radio_static_default, siren_default];
43375
44683
  var LIBRARY_LOOPS = [engine_saw_default, rotor_blades_default, crowd_rumble_default];
43376
44684
  var LIBRARY_BANK = { id: "library", sounds: LIBRARY_SOUNDS, loops: LIBRARY_LOOPS };
44685
+ var mix_profiles_default = [
44686
+ {
44687
+ id: "game",
44688
+ master: 0.5,
44689
+ noise: { seconds: 1.5, create: "lazy" },
44690
+ sources: ["jogo:L62077-62081 master 0.5 to the output", "jogo:L62082-62088 noise 1.5 s made by the first sound that needs it"]
44691
+ },
44692
+ {
44693
+ id: "art_scene",
44694
+ master: 0.85,
44695
+ compressor: { threshold: -12, ratio: 5 },
44696
+ reverb: { seconds: 2.2, decay: 3.4, wet: 0.4 },
44697
+ noise: { seconds: 2, create: "eager" },
44698
+ sources: [
44699
+ "cena:L1308 compressor -12 dB, ratio 5",
44700
+ "cena:L1309 master 0.85",
44701
+ "cena:L1310 convolver, impulse 2.2 s of (1 - i/len)^3.4",
44702
+ "cena:L1311 wet 0.4",
44703
+ "cena:L1312 noise 2 s made with the chain"
44704
+ ]
44705
+ }
44706
+ ];
44707
+ var MIX_PROFILES = Object.fromEntries(
44708
+ mix_profiles_default.map((profile) => [profile.id, profile])
44709
+ );
43377
44710
  var BENCH_BANK = {
43378
44711
  id: "audio_bench",
43379
44712
  sounds: [
@@ -43420,6 +44753,236 @@ var GameVehiclesThree = (() => {
43420
44753
  ]
43421
44754
  };
43422
44755
 
44756
+ // ../game-audio/dist/index.js
44757
+ var ATTACK = 2e-3;
44758
+ function lengthOf(d, attack) {
44759
+ if (typeof d === "number") return attack + d;
44760
+ if (Array.isArray(d)) return [attack + d[0], attack + d[1]];
44761
+ return { ...d, add: (d.add ?? 0) + attack };
44762
+ }
44763
+ var envelope = (attack) => [
44764
+ [0, 1e-4, "set", "fixed"],
44765
+ [attack, 1, "exp"],
44766
+ ["1d", 1e-4, "exp", "fixed"]
44767
+ ];
44768
+ function sceneNoise(o) {
44769
+ const a = o.a ?? ATTACK;
44770
+ const over = typeof o.d === "number" ? o.d : 0;
44771
+ return {
44772
+ filters: {
44773
+ mode: "serial",
44774
+ bank: [{ type: o.type ?? "lowpass", frequency: o.f2 === void 0 ? { set: o.f } : { set: o.f, to: o.f2, curve: "exp", over }, q: o.q ?? 0.7 }]
44775
+ },
44776
+ duration: lengthOf(o.d, a),
44777
+ gain: o.vol,
44778
+ ...o.delay === void 0 ? {} : { at: o.delay },
44779
+ ...o.wet === void 0 ? {} : { wet: o.wet },
44780
+ source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
44781
+ envelope: envelope(a)
44782
+ };
44783
+ }
44784
+ function sceneTone(o) {
44785
+ return {
44786
+ source: { kind: "osc", wave: o.wave ?? "sine", frequency: { from: o.f, to: o.f2 ?? "from", curve: "exp", over: "duration" } },
44787
+ duration: lengthOf(o.d, ATTACK),
44788
+ gain: o.vol,
44789
+ ...o.delay === void 0 ? {} : { at: o.delay },
44790
+ ...o.wet === void 0 ? {} : { wet: o.wet },
44791
+ envelope: envelope(ATTACK)
44792
+ };
44793
+ }
44794
+ function sceneVoice(o) {
44795
+ return {
44796
+ duration: lengthOf(o.d, 0),
44797
+ gain: o.vol,
44798
+ wet: 0.45,
44799
+ source: {
44800
+ kind: "formant_voice",
44801
+ wave: o.wave ?? "sawtooth",
44802
+ frequency: { from: o.f0, to: o.f1, curve: "linear", over: "duration" },
44803
+ vibrato: { rate: [4, 10], depth: 0.08 },
44804
+ formants: [
44805
+ { frequency: o.fA, q: 4 },
44806
+ { frequency: o.fB, q: 6 }
44807
+ ],
44808
+ breath: { frequency: o.fB, q: 1.1, gain: o.noise },
44809
+ mix: 3.2
44810
+ },
44811
+ envelope: [
44812
+ [0, 1e-4, "set", "fixed"],
44813
+ [0.1, 1, "exp"],
44814
+ ["0.6d", 1, "set"],
44815
+ ["1d", 1e-4, "exp", "fixed"]
44816
+ ]
44817
+ };
44818
+ }
44819
+ var TAKES = [
44820
+ { f: 1, t: 1, v: 1 },
44821
+ { f: 1.08, t: 0.9, v: 0.92 },
44822
+ { f: 0.93, t: 1.12, v: 1.05 },
44823
+ { f: 1.03, t: 1.05, v: 0.88 }
44824
+ ];
44825
+ var FOOTSTEP_FALLOFF = { model: "inverse_square", k: 0.03, cutoffK: 0.03, minGain: 4e-3 };
44826
+ var BODY_FALLOFF = { model: "inverse_square", k: 0.012, cutoffK: 0.015, minGain: 4e-3 };
44827
+ var SURFACES = {
44828
+ concrete: { heel: { f: 95, d: 0.05, vol: 0.55 }, roll: { type: "bandpass", f: 2300, q: 1.1, d: 0.05, vol: 0.38 }, grains: { count: 2, f: 4200, q: 2, spread: 0.04, vol: 0.08 } },
44829
+ asphalt: { heel: { f: 90, d: 0.05, vol: 0.5 }, roll: { type: "bandpass", f: 1700, q: 0.9, d: 0.06, vol: 0.36 }, grains: { count: 4, f: 3600, q: 2.5, spread: 0.06, vol: 0.1 } },
44830
+ wood: { heel: { f: 130, d: 0.08, vol: 0.55 }, roll: { type: "lowpass", f: 1300, q: 0.8, d: 0.06, vol: 0.3 }, ring: [{ f: 210, d: 0.1, vol: 0.18 }, { f: 470, d: 0.06, vol: 0.06 }] },
44831
+ tile: { heel: { f: 110, d: 0.04, vol: 0.45 }, roll: { type: "bandpass", f: 3600, q: 2, d: 0.03, vol: 0.42 }, ring: [{ f: 1900, d: 0.03, vol: 0.05 }] },
44832
+ dirt: { heel: { f: 75, d: 0.06, vol: 0.45 }, roll: { type: "lowpass", f: 750, q: 0.7, d: 0.09, vol: 0.4 }, grains: { count: 3, f: 1800, q: 1.5, spread: 0.07, vol: 0.06 } },
44833
+ grass: { heel: { f: 70, d: 0.05, vol: 0.32 }, roll: { type: "highpass", f: 2600, q: 0.7, d: 0.13, vol: 0.28 }, grains: { count: 4, f: 5200, q: 1.2, spread: 0.1, vol: 0.07 } },
44834
+ gravel: { heel: { f: 80, d: 0.05, vol: 0.4 }, roll: { type: "lowpass", f: 950, q: 0.8, d: 0.1, vol: 0.3 }, grains: { count: 8, f: 2600, q: 3, spread: 0.13, vol: 0.2 } },
44835
+ sand: { heel: { f: 65, d: 0.06, vol: 0.3 }, roll: { type: "bandpass", f: 1500, q: 0.6, d: 0.16, vol: 0.3 }, grains: { count: 5, f: 4800, q: 1.5, spread: 0.12, vol: 0.05 } },
44836
+ metal: { heel: { f: 120, d: 0.05, vol: 0.5 }, roll: { type: "bandpass", f: 2800, q: 1.5, d: 0.03, vol: 0.25 }, ring: [{ f: 420, d: 0.25, vol: 0.16 }, { f: 1130, d: 0.18, vol: 0.09 }, { f: 2470, d: 0.1, vol: 0.04 }] },
44837
+ water: { heel: { f: 70, d: 0.04, vol: 0.25 }, roll: { type: "bandpass", f: 3e3, q: 1.2, d: 0.1, vol: 0.18 }, splash: { f: 1100, f2: 380, d: 0.22, vol: 0.55 }, grains: { count: 4, f: 2400, q: 6, spread: 0.18, vol: 0.12 } },
44838
+ glass: { heel: { f: 90, d: 0.04, vol: 0.4 }, roll: { type: "highpass", f: 4500, q: 0.8, d: 0.06, vol: 0.2 }, grains: { count: 9, f: 6200, q: 5, spread: 0.12, vol: 0.22 }, ring: [{ f: 3900, d: 0.04, vol: 0.04 }, { f: 5300, d: 0.03, vol: 0.03 }] },
44839
+ carpet: { heel: { f: 70, d: 0.05, vol: 0.32 }, roll: { type: "lowpass", f: 420, q: 0.7, d: 0.07, vol: 0.3 } }
44840
+ };
44841
+ var GAITS = {
44842
+ walk: { vol: 1, bright: 1, toe: 0.075, short: 1 },
44843
+ run: { vol: 1.35, bright: 1.25, toe: 0.035, short: 0.75 }
44844
+ };
44845
+ var grainLayers = (g, take, start2, scale) => Array.from(
44846
+ { length: g.count },
44847
+ (_, i) => sceneNoise({ type: "bandpass", f: [g.f * take.f * 0.85, g.f * take.f * 1.15], q: g.q, d: [8e-3, 0.02], vol: g.vol * scale * take.v, delay: start2 + (i + 0.5) / g.count * g.spread * take.t })
44848
+ );
44849
+ var ringLayers = (rings, take, scale, at = 0) => rings.map((r) => sceneTone({ f: r.f * take.f, d: r.d * take.t, vol: r.vol * scale * take.v, delay: at }));
44850
+ function footLayers(surface, take, gait, weight = 1) {
44851
+ const s = SURFACES[surface];
44852
+ const toe = gait.toe * take.t;
44853
+ return [
44854
+ sceneTone({ f: s.heel.f * take.f * weight ** -0.3, f2: s.heel.f * take.f * 0.6, d: s.heel.d * gait.short * take.t * weight ** 0.5, vol: s.heel.vol * gait.vol * take.v * weight ** 0.5 }),
44855
+ sceneNoise({ type: "lowpass", f: 500 * gait.bright, d: 0.02 * take.t, vol: 0.18 * gait.vol * take.v * weight ** 0.5 }),
44856
+ sceneNoise({ type: s.roll.type, f: [s.roll.f * take.f * gait.bright * 0.92, s.roll.f * take.f * gait.bright * 1.08], q: s.roll.q, d: s.roll.d * gait.short * take.t * weight ** 0.3, vol: s.roll.vol * gait.vol * take.v, delay: toe }),
44857
+ ...s.grains ? grainLayers(s.grains, take, toe * 0.5, gait.vol * weight ** 0.5) : [],
44858
+ ...s.ring ? ringLayers(s.ring, take, gait.vol * weight ** 0.5) : [],
44859
+ ...s.splash ? [sceneNoise({ type: "bandpass", f: s.splash.f * take.f, f2: s.splash.f2 * take.f, q: 1.4, d: s.splash.d * take.t * weight ** 0.4, vol: s.splash.vol * gait.vol * take.v * weight ** 0.5 })] : []
44860
+ ];
44861
+ }
44862
+ var quieter = (level, layers) => layers.map((layer) => {
44863
+ const gain = layer.gain ?? 1;
44864
+ if (typeof gain === "number") return { ...layer, gain: gain * level };
44865
+ if (Array.isArray(gain)) return { ...layer, gain: [gain[0] * level, gain[1] * level] };
44866
+ return layer;
44867
+ });
44868
+ var recipe = (id, layers, falloff, bus = "world") => ({ id, bus, pan: true, layers, falloff, sources: ["sdk"] });
44869
+ var takeId = (id, index) => `${id}_${String(index + 1).padStart(2, "0")}`;
44870
+ function takes(id, layersOf, falloff, rate = [0.96, 1.04], bus) {
44871
+ const sounds = TAKES.map((take, index) => recipe(takeId(id, index), layersOf(take, index), falloff, bus));
44872
+ return { sounds, set: { id, variants: sounds.map((sound) => sound.id), rate } };
44873
+ }
44874
+ var footstep = (surface, gait) => takes(footstepSoundId(surface, gait), (take) => footLayers(surface, take, GAITS[gait]), FOOTSTEP_FALLOFF);
44875
+ var LANDINGS = { light: { weight: 1.4, cloth: 0.12 }, heavy: { weight: 2.4, cloth: 0.22 } };
44876
+ var LANDING_LEVEL = 0.65;
44877
+ var landing = (surface, landing2) => takes(
44878
+ `land_${surface}_${landing2}`,
44879
+ (take) => quieter(LANDING_LEVEL, [
44880
+ // both feet, a hair apart, then the knees taking the weight (a low body thump) and the clothes settling
44881
+ ...footLayers(surface, take, GAITS.run, LANDINGS[landing2].weight),
44882
+ ...footLayers(surface, { ...take, v: take.v * 0.6 }, GAITS.run, LANDINGS[landing2].weight * 0.6).map((layer) => ({ ...layer, at: (typeof layer.at === "number" ? layer.at : 0) + 0.018 * take.t })),
44883
+ sceneTone({ f: 60 * take.f, f2: 38, d: 0.14 * LANDINGS[landing2].weight * take.t, vol: 0.22 * LANDINGS[landing2].weight ** 0.7 * take.v, delay: 0.02 }),
44884
+ sceneNoise({ type: "bandpass", f: [1800, 2600], q: 0.8, d: 0.18, vol: LANDINGS[landing2].cloth * take.v, delay: 0.04 })
44885
+ ]),
44886
+ FOOTSTEP_FALLOFF
44887
+ );
44888
+ var whoosh = (blow, from, to, d, vol, q = 1.6) => (take) => [
44889
+ // the air: a band sweeping up as the limb speeds up; it peaks `SWING_LEAD_SECONDS` in, the moment before the contact, then dies away
44890
+ sceneNoise({ type: "bandpass", f: from * take.f, f2: to * take.f, q, a: SWING_LEAD_SECONDS[blow], d: d * take.t, vol: vol * take.v }),
44891
+ sceneNoise({ type: "lowpass", f: 600 * take.f, a: SWING_LEAD_SECONDS[blow], d: d * take.t * 0.8, vol: vol * 0.35 * take.v })
44892
+ ];
44893
+ var fleshHit = (o) => (take) => [
44894
+ sceneNoise({ type: "bandpass", f: o.slapF * take.f, q: 1.2, d: 0.03 * take.t, vol: o.slap * take.v }),
44895
+ sceneNoise({ type: "highpass", f: 3500 * take.f, d: 0.012, vol: o.slap * 0.4 * take.v }),
44896
+ sceneTone({ f: o.thudF * take.f, f2: o.thudF * 0.5, d: o.thudD * take.t, vol: o.thud * take.v }),
44897
+ sceneNoise({ type: "lowpass", f: 600 * take.f, d: o.thudD * 1.2 * take.t, vol: o.thud * 0.5 * take.v }),
44898
+ ...o.crack ? [sceneNoise({ type: "highpass", f: 2500, d: 0.015, vol: o.crack * take.v, delay: 6e-3 }), sceneTone({ wave: "square", f: 900 * take.f, f2: 300, d: 0.02, vol: o.crack * 0.15 * take.v, delay: 6e-3 })] : [],
44899
+ ...o.wet ? [sceneNoise({ type: "bandpass", f: 1400 * take.f, f2: 500, q: 2.5, d: 0.12 * take.t, vol: o.wet * take.v, delay: 0.01 })] : []
44900
+ ];
44901
+ var knock = (o) => (take) => [
44902
+ sceneNoise({ type: "bandpass", f: o.clickF * take.f, q: 1.5, d: 0.018, vol: o.click * take.v }),
44903
+ ...o.tones.map(([f, d, vol]) => sceneTone({ f: f * take.f, d: d * take.t, vol: vol * take.v }))
44904
+ ];
44905
+ var voice = (o) => (take) => [
44906
+ sceneVoice({ f0: o.f0 * take.f, f1: o.f1 * take.f, d: o.d * take.t, fA: o.fA * take.f, fB: o.fB * take.f, noise: o.noise, vol: o.vol * take.v })
44907
+ ];
44908
+ var BODY = {
44909
+ jump: (take) => [
44910
+ sceneNoise({ type: "bandpass", f: 900 * take.f, f2: 1700 * take.f, q: 1, a: 0.03, d: 0.12, vol: 0.18 * take.v }),
44911
+ sceneNoise({ type: "bandpass", f: 2200 * take.f, q: 0.8, d: 0.04, vol: 0.12 * take.v }),
44912
+ sceneTone({ f: 85 * take.f, f2: 60, d: 0.04, vol: 0.2 * take.v })
44913
+ ],
44914
+ cloth_rustle: (take) => [
44915
+ { ...sceneNoise({ type: "bandpass", f: 2400 * take.f, q: 0.8, a: 0.06, d: 0.22 * take.t, vol: 0.12 * take.v }), modulators: [{ target: "gain", wave: "triangle", rate: [9, 14], depth: 0.5 }] }
44916
+ ],
44917
+ body_fall: (take) => [
44918
+ sceneTone({ f: 58 * take.f, f2: 35, d: 0.28 * take.t, vol: 0.9 * take.v }),
44919
+ sceneNoise({ type: "lowpass", f: 480 * take.f, d: 0.3 * take.t, vol: 0.55 * take.v }),
44920
+ sceneNoise({ type: "bandpass", f: 1300 * take.f, q: 1, d: 0.05, vol: 0.3 * take.v }),
44921
+ // the limbs a moment later, and the clothes
44922
+ sceneTone({ f: 75 * take.f, f2: 45, d: 0.12, vol: 0.4 * take.v, delay: 0.13 * take.t }),
44923
+ sceneNoise({ type: "bandpass", f: 2200, q: 0.8, d: 0.2, vol: 0.12 * take.v, delay: 0.05 })
44924
+ ],
44925
+ fall_damage: (take) => [
44926
+ ...BODY.body_fall(take),
44927
+ sceneNoise({ type: "highpass", f: 2800, d: 0.02, vol: 0.35 * take.v, delay: 0.01 }),
44928
+ ...voice({ f0: 170, f1: 110, d: 0.32, fA: 650, fB: 1150, noise: 0.25, vol: 0.32 })(take).map((layer) => ({ ...layer, at: 0.08 }))
44929
+ ],
44930
+ player_hurt: voice({ f0: 150, f1: 105, d: 0.26, fA: 620, fB: 1100, noise: 0.3, vol: 0.35 }),
44931
+ player_breath_run: (take) => [
44932
+ sceneNoise({ type: "bandpass", f: 1200 * take.f, q: 1, a: 0.08, d: 0.22 * take.t, vol: 0.25 * take.v }),
44933
+ sceneNoise({ type: "bandpass", f: 900 * take.f, q: 1, a: 0.06, d: 0.26 * take.t, vol: 0.3 * take.v, delay: 0.3 * take.t })
44934
+ ],
44935
+ player_death: voice({ f0: 135, f1: 68, d: 1, fA: 560, fB: 980, noise: 0.35, vol: 0.4 }),
44936
+ bite: (take) => [
44937
+ sceneNoise({ type: "highpass", f: 2600, d: 0.015, vol: 0.4 * take.v }),
44938
+ sceneNoise({ type: "bandpass", f: 1600 * take.f, f2: 450, q: 2.2, d: 0.16 * take.t, vol: 0.55 * take.v, delay: 0.01 }),
44939
+ sceneNoise({ type: "bandpass", f: 3200 * take.f, f2: 1800, q: 1.2, d: 0.22 * take.t, vol: 0.25 * take.v, delay: 0.09 }),
44940
+ sceneTone({ f: 120 * take.f, f2: 70, d: 0.06, vol: 0.3 * take.v })
44941
+ ],
44942
+ grab: (take) => [
44943
+ sceneNoise({ type: "bandpass", f: 1900 * take.f, q: 0.9, a: 4e-3, d: 0.13 * take.t, vol: 0.32 * take.v }),
44944
+ sceneTone({ f: 95 * take.f, f2: 60, d: 0.07, vol: 0.3 * take.v }),
44945
+ sceneNoise({ type: "lowpass", f: 700, d: 0.08, vol: 0.2 * take.v })
44946
+ ],
44947
+ punch_swing: whoosh("punch", 450, 1500, 0.16, 0.9),
44948
+ punch_hit_body: fleshHit({ slap: 0.55, slapF: 1500, thud: 0.6, thudF: 115, thudD: 0.09 }),
44949
+ punch_hit_face: fleshHit({ slap: 0.75, slapF: 2300, thud: 0.45, thudF: 150, thudD: 0.07, crack: 0.25 }),
44950
+ punch_hit_object: knock({ clickF: 1800, click: 0.3, tones: [[210, 0.04, 0.25]] }),
44951
+ kick_swing: whoosh("kick", 280, 950, 0.22, 1, 1.3),
44952
+ kick_hit: fleshHit({ slap: 0.45, slapF: 1100, thud: 0.85, thudF: 82, thudD: 0.14 }),
44953
+ kick_hit_object: knock({ clickF: 1e3, click: 0.3, tones: [[92, 0.08, 0.5]] }),
44954
+ shove_two_hands: (take) => [
44955
+ sceneNoise({ type: "bandpass", f: 950 * take.f, q: 0.9, a: 0.01, d: 0.13 * take.t, vol: 0.38 * take.v }),
44956
+ sceneTone({ f: 100 * take.f, f2: 60, d: 0.1 * take.t, vol: 0.45 * take.v }),
44957
+ sceneNoise({ type: "bandpass", f: 2300, q: 0.8, d: 0.16, vol: 0.12 * take.v, delay: 0.03 })
44958
+ ],
44959
+ knife_swing: whoosh("blade", 1900, 4600, 0.12, 1.1, 2.2),
44960
+ knife_stab: fleshHit({ slap: 0.2, slapF: 1200, thud: 0.4, thudF: 160, thudD: 0.06, wet: 0.5 }),
44961
+ knife_slash: (take) => [
44962
+ sceneNoise({ type: "highpass", f: 3200 * take.f, f2: 1800, d: 0.13 * take.t, vol: 0.35 * take.v }),
44963
+ sceneNoise({ type: "bandpass", f: 1300 * take.f, f2: 600, q: 2.5, d: 0.12 * take.t, vol: 0.35 * take.v, delay: 0.02 })
44964
+ ],
44965
+ knife_hit_object: knock({ clickF: 3200, click: 0.25, tones: [[2800, 0.08, 0.07], [4150, 0.06, 0.05]] }),
44966
+ blunt_swing: whoosh("blunt", 220, 760, 0.28, 1.1, 1.5),
44967
+ blunt_hit: fleshHit({ slap: 0.35, slapF: 900, thud: 1, thudF: 70, thudD: 0.18, crack: 0.3 }),
44968
+ blunt_hit_object: knock({ clickF: 1400, click: 0.35, tones: [[300, 0.12, 0.25], [760, 0.08, 0.12]] }),
44969
+ axe_swing: whoosh("chop", 190, 620, 0.3, 1.2, 1.4),
44970
+ axe_chop: fleshHit({ slap: 0.3, slapF: 1400, thud: 0.75, thudF: 140, thudD: 0.1, crack: 0.35, wet: 0.45 }),
44971
+ axe_hit_object: knock({ clickF: 2200, click: 0.4, tones: [[1500, 0.15, 0.1], [2300, 0.1, 0.06], [140, 0.1, 0.35]] })
44972
+ };
44973
+ var BODY_BUS = { player_hurt: "voices", player_death: "voices", player_breath_run: "voices", fall_damage: "voices" };
44974
+ var built = [
44975
+ ...SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstep(surface, gait))),
44976
+ ...SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((l) => landing(surface, l))),
44977
+ ...BODY_SOUND_IDS.map((id) => takes(id, BODY[id], BODY_FALLOFF, id.endsWith("_swing") ? [0.94, 1.06] : [0.96, 1.04], BODY_BUS[id]))
44978
+ ];
44979
+ var BODY_FOLEY_BANK = {
44980
+ id: "body_foley",
44981
+ sounds: built.flatMap((entry) => entry.sounds),
44982
+ sets: built.map((entry) => entry.set)
44983
+ };
44984
+ var BODY_FOLEY_SET_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
44985
+
43423
44986
  // src/test-area/types.ts
43424
44987
  var defaultEffectIds = {
43425
44988
  ignite: "vehicle_fire",
@@ -43442,10 +45005,12 @@ var GameVehiclesThree = (() => {
43442
45005
  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" };
43443
45006
  var itemId = (key) => ITEM_OF[key] ?? key;
43444
45007
  var LANE = { z: 11, from: -12, wall: 13 };
45008
+ var LEAF_REST_RADIANS = 0.02;
43445
45009
  var MATERIAL_COLOURS = { metal: 13421772, glass: 4500223, rubber: 16750848, fuel: 16724736 };
43446
45010
  var ZONE_COLOURS = { door: 5627221, hood: 14540083, lid: 14522675, bumper: 14505437, glass: 4500223, light: 16777215, wheel: 16742195 };
43447
45011
  var BODY_HEIGHT = 1.7;
43448
45012
  function createVehiclesTestModule(options = {}) {
45013
+ const fleet = options.fleet ?? { colorFromSeed, fleetDims, installFleet, paintPalettes, paletteOf };
43449
45014
  const locale = options.locale ?? "pt-BR";
43450
45015
  const text = TEXT[locale] ?? TEXT["pt-BR"];
43451
45016
  const keys = options.vehicles ?? DEFAULT_TEST_VEHICLES;
@@ -43456,6 +45021,7 @@ var GameVehiclesThree = (() => {
43456
45021
  const settings = { quality: "high", paint: false, seed: 1, palette: "auto", driving: "arcade", surface: "dry_asphalt" };
43457
45022
  const kits = /* @__PURE__ */ new WeakMap();
43458
45023
  const placed = /* @__PURE__ */ new Map();
45024
+ const cabins = createCabinSmoke();
43459
45025
  let host = null;
43460
45026
  let three = null;
43461
45027
  let counter = 0;
@@ -43469,23 +45035,29 @@ var GameVehiclesThree = (() => {
43469
45035
  engineLoop?.stop();
43470
45036
  engineLoop = void 0;
43471
45037
  };
45038
+ const particleMaterials = {
45039
+ own: (build) => isRenderPools(host?.pools) ? host.pools.materials.own(build) : build(),
45040
+ release: (material) => isRenderPools(host?.pools) ? host.pools.materials.release(material) : material.dispose?.()
45041
+ };
43472
45042
  const kitFor = (THREE) => {
43473
45043
  three = THREE;
43474
45044
  let kit = kits.get(THREE);
43475
45045
  if (!kit) {
43476
45046
  if (options.createKit) kit = options.createKit(THREE);
43477
45047
  else {
43478
- kit = createVehicleKit({ THREE, locale, ...options.document ? { document: options.document } : {} });
43479
- installFleet(kit);
45048
+ kit = createVehicleKit({ THREE, locale, particleMaterials, effects: () => host?.effects, ...options.document ? { document: options.document } : {} });
45049
+ fleet.installFleet(kit);
43480
45050
  }
43481
45051
  kits.set(THREE, kit);
43482
45052
  }
43483
45053
  return kit;
43484
45054
  };
43485
- const paletteFor = (key) => settings.palette === "civil" ? paintPalettes.civil : settings.palette === "military" ? paintPalettes.military : paletteOf(key);
45055
+ const paletteFor = (key) => settings.palette === "civil" ? fleet.paintPalettes.civil : settings.palette === "military" ? fleet.paintPalettes.military : fleet.paletteOf(key);
43486
45056
  const paintOf = (entry) => {
43487
45057
  if (!settings.paint) return;
43488
- const color2 = colorFromSeed(settings.seed + entry.index, paletteFor(entry.key));
45058
+ entry.lod?.dispose();
45059
+ delete entry.lod;
45060
+ const color2 = fleet.colorFromSeed(settings.seed + entry.index, paletteFor(entry.key));
43489
45061
  if (!entry.paint) {
43490
45062
  const vehicle = entry.item.vehicle;
43491
45063
  const primary = primaryPaint(entry.item.vehicle);
@@ -43507,19 +45079,41 @@ var GameVehiclesThree = (() => {
43507
45079
  slow: true,
43508
45080
  build: (THREE) => {
43509
45081
  const kit = kitFor(THREE);
43510
- const built = buildVehicleItem(kit, key, { lod: settings.quality === "lite", ...scenario ? { scenario } : {} });
43511
- built.root.userData.kit = kit;
43512
- const entry = { root: built.root, key, ...scenario ? { scenario } : {}, item: built, index: counter++, helpers: [] };
43513
- placed.set(built.root.uuid, entry);
45082
+ const built2 = buildVehicleItem(kit, key, { lod: settings.quality === "lite", ...scenario ? { scenario } : {} });
45083
+ built2.root.userData.kit = kit;
45084
+ const entry = { root: built2.root, key, ...scenario ? { scenario } : {}, item: built2, index: counter++, helpers: [] };
45085
+ placed.set(built2.root.uuid, entry);
43514
45086
  measure(entry);
43515
45087
  paintOf(entry);
43516
- return built.root;
45088
+ return built2.root;
43517
45089
  },
43518
45090
  impl: (root, id) => {
43519
45091
  const r = root;
43520
45092
  const kit = r.userData.kit;
43521
- const d = createVehicleDestructible(kit, r.userData.vehicle, { id, frame: root, name: text.items[itemKey] ?? key });
45093
+ const d = createVehicleDestructible(kit, r.userData.vehicle, { id, frame: root, name: text.items[itemKey] ?? key, creaseQueue });
43522
45094
  tapEffects(d);
45095
+ const setPartPose = d.setPartPose?.bind(d);
45096
+ {
45097
+ const rests = new Map(d.describe().parts.map((part) => [part.id, part.rotation ?? [0, 0, 0]]));
45098
+ d.setPartPose = (partId, position, rotation) => {
45099
+ const entry = [...placed.values()].find((candidate) => candidate.root === root);
45100
+ const rest = rests.get(partId) ?? [0, 0, 0];
45101
+ const isOpen = (rotation ?? [0, 0, 0]).some((angle, axis) => Math.abs(angle - (rest[axis] ?? 0)) > LEAF_REST_RADIANS);
45102
+ if (entry) {
45103
+ entry.posedParts ?? (entry.posedParts = /* @__PURE__ */ new Set());
45104
+ if (isOpen) entry.posedParts.add(partId);
45105
+ else entry.posedParts.delete(partId);
45106
+ }
45107
+ return setPartPose?.(partId, position, rotation);
45108
+ };
45109
+ }
45110
+ const hitBody = d.hit.bind(d);
45111
+ d.hit = (h) => {
45112
+ const entry = [...placed.values()].find((candidate) => candidate.root === root);
45113
+ if (entry) entry.touched = true;
45114
+ return hitBody(h);
45115
+ };
45116
+ cabins.track(id, d, root);
43523
45117
  r.userData.destructible = d;
43524
45118
  return d;
43525
45119
  },
@@ -43531,10 +45125,10 @@ var GameVehiclesThree = (() => {
43531
45125
  if (!THREE) return void 0;
43532
45126
  if (!clipCache.has(key)) {
43533
45127
  const kit = kitFor(THREE);
43534
- const built = buildVehicleItem(kit, key);
43535
- const d = createVehicleDestructible(kit, built.vehicle, { id: `${itemId(key)}_clips`, frame: built.root, headless: true });
45128
+ const built2 = buildVehicleItem(kit, key);
45129
+ const d = createVehicleDestructible(kit, built2.vehicle, { id: `${itemId(key)}_clips`, frame: built2.root, headless: true });
43536
45130
  clipCache.set(key, d.animations());
43537
- kit.dispose(built.vehicle);
45131
+ kit.dispose(built2.vehicle);
43538
45132
  }
43539
45133
  return clipCache.get(key);
43540
45134
  };
@@ -43659,33 +45253,33 @@ var GameVehiclesThree = (() => {
43659
45253
  if (!host || !three) return;
43660
45254
  clearLane();
43661
45255
  const kit = kitFor(three);
43662
- const vehicles = createVehiclesThree(three, { kit });
43663
- const built = vehicles.build("compact_hatch", { quality: "lite", seed: settings.seed });
45256
+ const vehicles = createVehiclesThree(three, { kit, fleet });
45257
+ const built2 = vehicles.build("compact_hatch", { quality: "lite", seed: settings.seed });
43664
45258
  const wall = new three.Mesh(new three.BoxGeometry(0.6, 2, 6), new three.MeshStandardMaterial({ color: 7829367 }));
43665
45259
  wall.position.set(LANE.wall + 0.3, 1, LANE.z);
43666
45260
  const scene = host.scene;
43667
- scene.add(built.group, wall);
45261
+ scene.add(built2.group, wall);
43668
45262
  const mode = settings.driving === "semi" ? "semi" : "arcade";
43669
45263
  const surfaces = createSurfaces();
43670
45264
  const idle = { throttle: 0, brake: 0, steer: 0, handbrake: false };
43671
45265
  const state = { time: 0, exhaust: 0, ended: false, wasBlocked: false };
43672
- const arcade = createArcadeDriver({ length: built.length, width: built.width });
45266
+ const arcade = createArcadeDriver({ length: built2.length, width: built2.width });
43673
45267
  const semi = createSemiSimDriver({}, { surfaces });
43674
45268
  const driver = mode === "semi" ? semi : arcade;
43675
45269
  driver.place(LANE.from, LANE.z, 0);
43676
- const gap = () => LANE.wall - (driver.state.x + built.length / 2);
45270
+ const gap = () => LANE.wall - (driver.state.x + built2.length / 2);
43677
45271
  const decel = () => mode === "semi" ? 9 * surfaces.get(settings.surface).grip : arcade.spec.brakeAgainstMotion;
43678
45272
  const brakeAt = () => lane ? lane.speed() * lane.speed() / (2 * decel()) + 3 : 3;
43679
45273
  const pose = () => {
43680
- built.group.position.set(driver.state.x, 0, driver.state.z);
43681
- built.group.rotation.y = driver.state.yaw + Math.PI / 2;
45274
+ built2.group.position.set(driver.state.x, 0, driver.state.z);
45275
+ built2.group.rotation.y = driver.state.yaw + Math.PI / 2;
43682
45276
  };
43683
45277
  pose();
43684
45278
  engineLoop = audio()?.loop?.(soundIds.engine, { position: [driver.state.x, 0.5, driver.state.z] });
43685
45279
  const log = (key, value) => host?.log(fill(text, locale, text.lane[key], value.toFixed(1)));
43686
45280
  lane = {
43687
- car: built,
43688
- group: built.group,
45281
+ car: built2,
45282
+ group: built2.group,
43689
45283
  wall,
43690
45284
  mode,
43691
45285
  done: false,
@@ -43721,7 +45315,7 @@ var GameVehiclesThree = (() => {
43721
45315
  const fx = effects();
43722
45316
  if (fx && state.exhaust <= 0 && Math.abs(lane?.speed() ?? 0) > 0.5) {
43723
45317
  state.exhaust = 0.4;
43724
- 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) });
45318
+ fx.play({ effect: effectIds.exhaust, point: [driver.state.x - Math.cos(driver.state.yaw) * (built2.length / 2), 0.3, driver.state.z + Math.sin(driver.state.yaw) * (built2.length / 2)], scale: Math.min(1, Math.abs(lane?.speed() ?? 0) / 17) });
43725
45319
  }
43726
45320
  if (!state.ended && state.time > 1 && Math.abs(lane?.speed() ?? 0) < 0.05) {
43727
45321
  state.ended = true;
@@ -43765,6 +45359,16 @@ var GameVehiclesThree = (() => {
43765
45359
  break;
43766
45360
  }
43767
45361
  };
45362
+ const creaseQueue = createCreaseQueue();
45363
+ const updateLod = (entry) => {
45364
+ if (!host || settings.quality === "lite") return;
45365
+ entry.lod ?? (entry.lod = createVehicleLod(entry.root.userData.kit, entry.item.vehicle, entry.root, entry.root.children[0]));
45366
+ const at = host.player().position;
45367
+ const distance = Math.hypot(at[0] - entry.root.position.x, at[2] - entry.root.position.z);
45368
+ const c = entry.item.vehicle;
45369
+ if (c && (c.rollState || c.flipped || c.blast || c.exploded)) entry.touched = true;
45370
+ entry.lod.update(distance, entry.touched === true, entry.scenario !== void 0 || (entry.posedParts?.size ?? 0) > 0);
45371
+ };
43768
45372
  const frame = (dt) => {
43769
45373
  for (const [id, entry] of placed) {
43770
45374
  if (!entry.root.parent) {
@@ -43772,9 +45376,11 @@ var GameVehiclesThree = (() => {
43772
45376
  continue;
43773
45377
  }
43774
45378
  measure(entry);
45379
+ updateLod(entry);
43775
45380
  }
43776
45381
  if (host?.bench && boxKey() !== helpersShown && (host.bench.layer("damageables") || host.bench.layer("zones") || helpersShown !== "")) refreshLayers();
43777
45382
  lane?.step(dt);
45383
+ cabins.sync(effects());
43778
45384
  };
43779
45385
  const count2 = () => {
43780
45386
  let drawCalls = 0;
@@ -43801,7 +45407,7 @@ var GameVehiclesThree = (() => {
43801
45407
  const sizeError = () => {
43802
45408
  let worst = 0;
43803
45409
  for (const entry of placed.values()) {
43804
- const want = fleetDims(entry.key);
45410
+ const want = fleet.fleetDims(entry.key);
43805
45411
  const got = entry.size;
43806
45412
  if (!want || !got || entry.scenario) continue;
43807
45413
  worst = Math.max(worst, Math.abs(got.length - want.length), Math.abs(got.width - want.width), Math.abs(got.height - want.height));
@@ -43823,15 +45429,15 @@ var GameVehiclesThree = (() => {
43823
45429
  const rowOf = (list2, z) => {
43824
45430
  let cursor = 0;
43825
45431
  const spots = list2.map((entry) => {
43826
- const width = fleetDims(entry.key)?.width ?? 2;
45432
+ const width = fleet.fleetDims(entry.key)?.width ?? 2;
43827
45433
  const x = cursor + width / 2;
43828
45434
  cursor += width + gap;
43829
45435
  return { id: entry.id, x };
43830
45436
  });
43831
45437
  return { spots, z, width: Math.max(0, cursor - gap) };
43832
45438
  };
43833
- const front = rowOf(entries.filter((e) => (fleetDims(e.key)?.width ?? 2) <= WIDE), 0);
43834
- const back = rowOf(entries.filter((e) => (fleetDims(e.key)?.width ?? 2) > WIDE), -16);
45439
+ const front = rowOf(entries.filter((e) => (fleet.fleetDims(e.key)?.width ?? 2) <= WIDE), 0);
45440
+ const back = rowOf(entries.filter((e) => (fleet.fleetDims(e.key)?.width ?? 2) > WIDE), -16);
43835
45441
  const placements = [front, back].flatMap((row) => row.spots.map(({ id, x }) => ({ item: id, position: [x - row.width / 2, 0, row.z], rotationY: 0 })));
43836
45442
  return { placements, view: { eye: [0, 5, Math.max(14, front.width * 0.6)], target: [0, 1, -2] } };
43837
45443
  },
@@ -43870,11 +45476,15 @@ var GameVehiclesThree = (() => {
43870
45476
  const stopChange = lent.bench?.onChange(onChange);
43871
45477
  const stopFrame = lent.onFrame(frame);
43872
45478
  const stopBlocker = lent.addBlocker?.(blocked);
45479
+ const stopCrease = lent.defer?.(creaseQueue.deferredQueue(), DEFERRED_PRIORITY[DEFERRED_QUEUE_ID.VEHICLE_CREASE]);
45480
+ if (isRenderPools(lent.pools)) lent.pools.materials.register("vehicles.cracked-glass", () => crackedGlassMaterial(three, new three.DataTexture(new Uint8Array(4), 1, 1)));
43873
45481
  lent.audio?.register?.(LIBRARY_BANK);
43874
45482
  return () => {
43875
45483
  stopChange?.();
43876
45484
  stopFrame();
43877
45485
  stopBlocker?.();
45486
+ stopCrease?.();
45487
+ cabins.clear(effects());
43878
45488
  clearHelpers();
43879
45489
  clearLane();
43880
45490
  host = null;