@adatechnology/game-vehicles 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -892,7 +892,7 @@ function advance(geo, ti, deadline) {
892
892
  }
893
893
  return ti.builtBlocks === total;
894
894
  }
895
- function start(geo) {
895
+ function start(geo, store = cache) {
896
896
  const pos = geo.attributes.position;
897
897
  const triangles = trianglesOf(geo);
898
898
  const ti = {
@@ -905,7 +905,7 @@ function start(geo) {
905
905
  blocks: new Float64Array(blockCount(triangles) * 6),
906
906
  builtBlocks: 0
907
907
  };
908
- cache.set(geo, ti);
908
+ store.set(geo, ti);
909
909
  return ti;
910
910
  }
911
911
  function triIndexOf(geo) {
@@ -957,6 +957,106 @@ function boundsFromIndex(geo) {
957
957
  }
958
958
  return { box, version: ti.positionVersion };
959
959
  }
960
+ var rayCache = /* @__PURE__ */ new WeakMap();
961
+ function rayIndexOf(geo) {
962
+ const shared = cache.get(geo);
963
+ if (shared && isCurrent(geo, shared) && shared.builtBlocks === blockCount(shared.triangles)) return shared;
964
+ let ti = rayCache.get(geo);
965
+ if (!ti || !isCurrent(geo, ti)) ti = start(geo, rayCache);
966
+ advance(geo, ti, Infinity);
967
+ return ti;
968
+ }
969
+ var BLOCK_SLACK = 1e-6;
970
+ var scratch = /* @__PURE__ */ new WeakMap();
971
+ function scratchOf(THREE) {
972
+ let s = scratch.get(THREE);
973
+ if (!s) {
974
+ s = {
975
+ inverse: new THREE.Matrix4(),
976
+ local: new THREE.Ray(),
977
+ box: new THREE.Box3(),
978
+ a: new THREE.Vector3(),
979
+ b: new THREE.Vector3(),
980
+ c: new THREE.Vector3(),
981
+ point: new THREE.Vector3(),
982
+ world: new THREE.Vector3(),
983
+ sphere: new THREE.Sphere()
984
+ };
985
+ scratch.set(THREE, s);
986
+ }
987
+ return s;
988
+ }
989
+ function indexable(THREE, mesh) {
990
+ const geo = mesh.geometry;
991
+ if (mesh.raycast !== THREE.Mesh.prototype.raycast) return false;
992
+ if (!mesh.isMesh || mesh.isSkinnedMesh || mesh.isInstancedMesh || mesh.isBatchedMesh || !geo || !geo.isBufferGeometry) return false;
993
+ if (!mesh.material || Array.isArray(mesh.material)) return false;
994
+ if (geo.morphAttributes && geo.morphAttributes.position && geo.morphAttributes.position.length) return false;
995
+ return supports(geo);
996
+ }
997
+ function raycastMesh(THREE, raycaster, mesh, out) {
998
+ const geo = mesh.geometry;
999
+ const s = scratchOf(THREE);
1000
+ if (geo.boundingSphere === null) geo.computeBoundingSphere();
1001
+ s.sphere.copy(geo.boundingSphere).applyMatrix4(mesh.matrixWorld);
1002
+ if (!raycaster.ray.intersectsSphere(s.sphere)) return;
1003
+ const ti = rayIndexOf(geo);
1004
+ if (!ti) return;
1005
+ s.inverse.copy(mesh.matrixWorld).invert();
1006
+ s.local.copy(raycaster.ray).applyMatrix4(s.inverse);
1007
+ const material = mesh.material;
1008
+ const back = material.side === THREE.BackSide;
1009
+ const cull = material.side === THREE.FrontSide;
1010
+ const P = geo.attributes.position;
1011
+ const ix = geo.index;
1012
+ const count = ix ? ix.count : P.count;
1013
+ const first = Math.max(0, geo.drawRange.start);
1014
+ const last = Math.min(count, geo.drawRange.start + geo.drawRange.count);
1015
+ const t0 = Math.ceil(first / 3);
1016
+ const t1 = Math.floor(last / 3);
1017
+ const B = ti.boxes;
1018
+ const K = ti.blocks;
1019
+ for (let blk = Math.floor(t0 / TRI_BLOCK); blk * TRI_BLOCK < t1; blk++) {
1020
+ const o = blk * 6;
1021
+ s.box.min.set(K[o] - BLOCK_SLACK, K[o + 1] - BLOCK_SLACK, K[o + 2] - BLOCK_SLACK);
1022
+ s.box.max.set(K[o + 3] + BLOCK_SLACK, K[o + 4] + BLOCK_SLACK, K[o + 5] + BLOCK_SLACK);
1023
+ if (!s.local.intersectsBox(s.box)) continue;
1024
+ const end = Math.min(t1, (blk + 1) * TRI_BLOCK);
1025
+ for (let t = Math.max(t0, blk * TRI_BLOCK); t < end; t++) {
1026
+ const j = t * 6;
1027
+ s.box.min.set(B[j] - BLOCK_SLACK, B[j + 1] - BLOCK_SLACK, B[j + 2] - BLOCK_SLACK);
1028
+ s.box.max.set(B[j + 3] + BLOCK_SLACK, B[j + 4] + BLOCK_SLACK, B[j + 5] + BLOCK_SLACK);
1029
+ if (!s.local.intersectsBox(s.box)) continue;
1030
+ const a = ix ? ix.getX(t * 3) : t * 3;
1031
+ const b = ix ? ix.getX(t * 3 + 1) : t * 3 + 1;
1032
+ const c = ix ? ix.getX(t * 3 + 2) : t * 3 + 2;
1033
+ s.a.fromBufferAttribute(P, a);
1034
+ s.b.fromBufferAttribute(P, b);
1035
+ s.c.fromBufferAttribute(P, c);
1036
+ 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);
1037
+ if (hit === null) continue;
1038
+ s.world.copy(s.point).applyMatrix4(mesh.matrixWorld);
1039
+ const distance = raycaster.ray.origin.distanceTo(s.world);
1040
+ if (distance < raycaster.near || distance > raycaster.far) continue;
1041
+ const normal = new THREE.Vector3();
1042
+ THREE.Triangle.getNormal(s.a, s.b, s.c, normal);
1043
+ out.push({ distance, point: s.world.clone(), object: mesh, face: { a, b, c, normal, materialIndex: 0 }, faceIndex: Math.floor(t) });
1044
+ }
1045
+ }
1046
+ }
1047
+ function collect(THREE, raycaster, object, recursive, out) {
1048
+ if (object.layers.test(raycaster.layers)) {
1049
+ if (indexable(THREE, object)) raycastMesh(THREE, raycaster, object, out);
1050
+ else object.raycast(raycaster, out);
1051
+ }
1052
+ if (recursive) for (const child of object.children) collect(THREE, raycaster, child, true, out);
1053
+ }
1054
+ function raycastIndexed(THREE, raycaster, objects, recursive = false) {
1055
+ const out = [];
1056
+ for (const object of objects) collect(THREE, raycaster, object, recursive, out);
1057
+ out.sort((x, y) => x.distance - y.distance);
1058
+ return out;
1059
+ }
960
1060
 
961
1061
  // src/geometry.ts
962
1062
  function installGeometry(kit) {
@@ -2290,6 +2390,7 @@ function installBody(kit) {
2290
2390
  }
2291
2391
  kit.profX = profX;
2292
2392
  const _sr = new THREE.Raycaster();
2393
+ _sr.layers.enableAll();
2293
2394
  kit.surfaceAt = function(c, org, dir, o) {
2294
2395
  o = o || {};
2295
2396
  c.root.updateMatrixWorld(true);
@@ -2306,7 +2407,7 @@ function installBody(kit) {
2306
2407
  _sr.set(p0.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
2307
2408
  _sr.near = 0;
2308
2409
  _sr.far = o.max || 20;
2309
- const h = _sr.intersectObjects(cands, false)[0];
2410
+ const h = raycastIndexed(THREE, _sr, cands)[0];
2310
2411
  if (!h) return null;
2311
2412
  const n = h.face ? h.face.normal.clone().transformDirection(h.object.matrixWorld).transformDirection(inv) : d.clone().negate();
2312
2413
  if (n.dot(d) > 0) n.negate();
@@ -2328,6 +2429,7 @@ function installGlass(kit) {
2328
2429
  const V3 = THREE.Vector3;
2329
2430
  const I = kit._;
2330
2431
  const { MATERIALS, crease, cutGeometry, pathShape, subdivide, tumble, uvProject } = I;
2432
+ I.paneSeals = /* @__PURE__ */ new WeakMap();
2331
2433
  kit.addGlass = function(c, geo, label, type, o) {
2332
2434
  o = o || {};
2333
2435
  if (!geo.attributes.uv || o.reuv) o.aspect = uvProject(geo, o.uvAxes);
@@ -2394,7 +2496,7 @@ function installGlass(kit) {
2394
2496
  cy /= poly.length;
2395
2497
  const base = I.resolveLabel(label ? label(i, polys.length) : { key: "window" });
2396
2498
  const lab = { key: base.key, label: kit.t("parts.glass_side", { label: base.label, s }) };
2397
- kit.addGlass(c, geo, lab, o.type || "tempered", {
2499
+ const pane = kit.addGlass(c, geo, lab, o.type || "tempered", {
2398
2500
  mat: o.mat || (o.see === false ? MATERIALS.glassDark : MATERIALS.glass),
2399
2501
  pos: new V3(cx, cy, s * I.halfAt(Wg, T, cy)),
2400
2502
  outline: shp.getSpacedPoints(56),
@@ -2410,6 +2512,7 @@ function installGlass(kit) {
2410
2512
  pts.pop();
2411
2513
  const seal = c.tube(pts, 0.011, MATERIALS.trim, { closed: true, seg: 72, rs: 5 });
2412
2514
  seal.castShadow = false;
2515
+ I.paneSeals.set(pane, seal);
2413
2516
  });
2414
2517
  });
2415
2518
  };
@@ -3085,6 +3188,7 @@ function installInterior(kit) {
3085
3188
  }
3086
3189
 
3087
3190
  // src/hood.ts
3191
+ var HOOD_LIFT = 8e-3;
3088
3192
  function installHood(kit) {
3089
3193
  const THREE = kit.THREE;
3090
3194
  const V3 = THREE.Vector3;
@@ -3107,7 +3211,7 @@ function installHood(kit) {
3107
3211
  for (let i = 0; i < p.count; i++) {
3108
3212
  p.setX(i, cx + (p.getX(i) - cx) * sx);
3109
3213
  p.setZ(i, p.getZ(i) * sz);
3110
- p.setY(i, p.getY(i) + 8e-3);
3214
+ p.setY(i, p.getY(i) + HOOD_LIFT);
3111
3215
  }
3112
3216
  geo.translate(-hx, -hy, 0);
3113
3217
  const under = geo.clone();
@@ -3136,7 +3240,8 @@ function installHood(kit) {
3136
3240
  yMin: bb.min.y,
3137
3241
  zw: bb.max.z,
3138
3242
  label: o.label,
3139
- noLatch: !!o.noLatch
3243
+ noLatch: !!o.noLatch,
3244
+ lift: HOOD_LIFT
3140
3245
  };
3141
3246
  c.part("hood", { key: "hood" }, piv, new V3((hx + bb.max.x) / 2, hy, 0), 0.9);
3142
3247
  return c.hood;
@@ -4119,6 +4224,47 @@ function installWheels(kit) {
4119
4224
  Object.assign(I, {});
4120
4225
  }
4121
4226
 
4227
+ // src/particles.ts
4228
+ import { warnOnce } from "@adatechnology/game-contracts";
4229
+
4230
+ // src/routed-particles.ts
4231
+ var ROUTED_PARTICLE_TYPES = { smoke: "particle_puff", dust: "particle_puff", fire: "particle_glow" };
4232
+ var PUFF_LOOKS = {
4233
+ smokewhite: [10133927, 0.42],
4234
+ smokeblack: [1776412, 0.62],
4235
+ smokegray: [5593436, 0.5],
4236
+ smokesteam: [11975873, 0.3],
4237
+ dust: [7235160, 0.34],
4238
+ fire: [16777215, 0.9]
4239
+ };
4240
+ var LIFE = { dust: 1.6, smoke: 2.8, fire: 0.7 };
4241
+ var FIRE_COLOR = 16753968;
4242
+ var SMOKE_START_SCALE = 0.18;
4243
+ function routedParticleRequest(input) {
4244
+ const { type } = input;
4245
+ const look = PUFF_LOOKS[type + (input.variant ?? "")] ?? PUFF_LOOKS[type] ?? PUFF_LOOKS.smokewhite;
4246
+ const life = input.life ?? LIFE[type] ?? 99;
4247
+ const size = (input.size ?? 1) * (input.grow ?? 1);
4248
+ const isFire = type === "fire";
4249
+ const growth = isFire ? 1 : (SMOKE_START_SCALE + 1.5 * (1 - Math.exp(-life * 1.1)) + life * 0.12) / SMOKE_START_SCALE;
4250
+ return {
4251
+ effect: ROUTED_PARTICLE_TYPES[type],
4252
+ point: input.position,
4253
+ values: {
4254
+ vx: input.velocity[0],
4255
+ vy: input.velocity[1],
4256
+ vz: input.velocity[2],
4257
+ life,
4258
+ size: isFire ? size * 0.85 : size * SMOKE_START_SCALE,
4259
+ grow: growth,
4260
+ color: input.color ?? (isFire ? FIRE_COLOR : look[0]),
4261
+ alpha: input.alpha ?? look[1],
4262
+ gravity: isFire ? 0 : type === "dust" ? 0 : -0.08,
4263
+ drag: type === "dust" ? 2.5 : isFire ? 1.5 : 0.9
4264
+ }
4265
+ };
4266
+ }
4267
+
4122
4268
  // src/particles.ts
4123
4269
  function installParticles(kit) {
4124
4270
  const THREE = kit.THREE;
@@ -4210,7 +4356,7 @@ function installParticles(kit) {
4210
4356
  });
4211
4357
  }
4212
4358
  const list = fire ? FIRE_TEXTURES : SMOKE_TEXTURES;
4213
- const mat = new THREE.SpriteMaterial({
4359
+ const build = () => new THREE.SpriteMaterial({
4214
4360
  map: list[Math.floor(Math.random() * list.length)],
4215
4361
  color: new THREE.Color(col),
4216
4362
  transparent: true,
@@ -4218,6 +4364,7 @@ function installParticles(kit) {
4218
4364
  opacity: op,
4219
4365
  blending: fire ? THREE.AdditiveBlending : THREE.NormalBlending
4220
4366
  });
4367
+ const mat = kit.particleMaterials ? kit.particleMaterials.own(build) : build();
4221
4368
  const dis = { value: 0 };
4222
4369
  mat.userData.dis = dis;
4223
4370
  mat.onBeforeCompile = function(sh) {
@@ -4245,11 +4392,37 @@ function installParticles(kit) {
4245
4392
  function killParticle(c, i) {
4246
4393
  const P = c.particles[i];
4247
4394
  c.effects.remove(P.m);
4248
- if (P.own) P.m.material.dispose();
4395
+ if (P.own) {
4396
+ if (kit.particleMaterials) kit.particleMaterials.release(P.m.material);
4397
+ else P.m.material.dispose();
4398
+ }
4249
4399
  c.particles.splice(i, 1);
4250
4400
  }
4401
+ function routeParticle(c, effects, type, pos, vel, o) {
4402
+ c.effects.updateWorldMatrix(true, false);
4403
+ const at = pos.clone().applyMatrix4(c.effects.matrixWorld);
4404
+ const to = vel.clone().transformDirection(c.effects.matrixWorld).multiplyScalar(vel.length());
4405
+ const colour = o.col == null ? void 0 : new THREE.Color(o.col).getHex();
4406
+ effects.play(
4407
+ routedParticleRequest({
4408
+ type,
4409
+ position: [at.x, at.y, at.z],
4410
+ velocity: [to.x, to.y, to.z],
4411
+ ...o.variant ? { variant: o.variant } : {},
4412
+ ...colour !== void 0 ? { color: colour } : {},
4413
+ ...o.op != null ? { alpha: o.op } : {},
4414
+ ...o.s ? { size: o.s } : {},
4415
+ ...o.life ? { life: o.life } : {},
4416
+ ...o.grow ? { grow: o.grow } : {}
4417
+ })
4418
+ );
4419
+ return null;
4420
+ }
4251
4421
  kit.spawn = function(c, type, pos, vel, o) {
4252
4422
  o = o || {};
4423
+ const effects = ROUTED_PARTICLE_TYPES[type] ? kit.effects?.() : void 0;
4424
+ if (effects) return routeParticle(c, effects, type, pos, vel, o);
4425
+ 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");
4253
4426
  if (c.particles.length >= PARTICLE_LIMIT) {
4254
4427
  const idx = c.particles.findIndex(function(q) {
4255
4428
  return TRANSIENT_TYPES[q.type];
@@ -4504,6 +4677,14 @@ function installParticles(kit) {
4504
4677
  }
4505
4678
 
4506
4679
  // src/fluids.ts
4680
+ var PUDDLE_COLORS = {
4681
+ coolant: 3123279,
4682
+ brake_fluid: 12093738,
4683
+ transmission_fluid: 10099746,
4684
+ power_steering_fluid: 9054768,
4685
+ washer_fluid: 2977744,
4686
+ battery_acid: 14211272
4687
+ };
4507
4688
  function installFluids(kit) {
4508
4689
  const THREE = kit.THREE;
4509
4690
  const V3 = THREE.Vector3;
@@ -4521,6 +4702,7 @@ function installFluids(kit) {
4521
4702
  };
4522
4703
  kit.defineFluid("stain", { fresh: "#4a4a44", dry: "#26241f", wipeable: false });
4523
4704
  const _fr = new THREE.Raycaster();
4705
+ _fr.layers.enableAll();
4524
4706
  kit.splat = function(c, p, n, r, o) {
4525
4707
  o = o || {};
4526
4708
  n = n.clone().normalize();
@@ -4722,6 +4904,13 @@ function installFluids(kit) {
4722
4904
  transparent: true,
4723
4905
  opacity: 0.9,
4724
4906
  depthWrite: false
4907
+ }) : PUDDLE_COLORS[kind] ? new THREE.MeshStandardMaterial({
4908
+ color: PUDDLE_COLORS[kind],
4909
+ roughness: 0.05,
4910
+ metalness: 0.1,
4911
+ transparent: true,
4912
+ opacity: 0.7,
4913
+ depthWrite: false
4725
4914
  }) : new THREE.MeshStandardMaterial({
4726
4915
  color: 0,
4727
4916
  roughness: 0.03,
@@ -4962,31 +5151,55 @@ function installFluids(kit) {
4962
5151
  }
4963
5152
 
4964
5153
  // src/glass-damage.ts
5154
+ function crackedGlassMaterial(THREE, texture) {
5155
+ return new THREE.MeshStandardMaterial({
5156
+ color: 16777215,
5157
+ map: texture,
5158
+ emissive: 6976118,
5159
+ emissiveMap: texture,
5160
+ transparent: true,
5161
+ roughness: 0.25,
5162
+ metalness: 0.1,
5163
+ side: THREE.DoubleSide,
5164
+ depthWrite: false
5165
+ });
5166
+ }
4965
5167
  function installGlassDamage(kit) {
4966
5168
  const THREE = kit.THREE;
4967
5169
  const V3 = THREE.Vector3;
4968
5170
  const PI = Math.PI;
4969
5171
  const I = kit._;
4970
5172
  const document2 = kit.document;
4971
- const { DOUBLE_SIDE, MATERIALS, crease, cutGeometry, mergeGeometries, subdivide, tumble } = I;
4972
- function glassUV(msh, pLocal) {
5173
+ const { MATERIALS, crease, cutGeometry, mergeGeometries, subdivide, tumble } = I;
5174
+ function glassUV(c, msh, pLocal) {
4973
5175
  const pos = msh.geometry.attributes.position;
4974
5176
  const uv = msh.geometry.attributes.uv;
4975
5177
  if (!uv) return [0.5, 0.5];
4976
- const inv = new THREE.Matrix4().copy(msh.matrix).invert();
4977
- const q = pLocal.clone().applyMatrix4(inv);
4978
- let best = 1e9;
4979
- let bi = 0;
4980
- const v = new V3();
4981
- for (let i = 0; i < pos.count; i++) {
4982
- v.fromBufferAttribute(pos, i);
4983
- const d = v.distanceToSquared(q);
4984
- if (d < best) {
4985
- best = d;
4986
- bi = i;
4987
- }
5178
+ c.root.updateMatrixWorld(true);
5179
+ const q = msh.worldToLocal(c.vehicle.localToWorld(pLocal.clone()));
5180
+ const idx = msh.geometry.index;
5181
+ const tri = new THREE.Triangle();
5182
+ const near = new V3();
5183
+ const bary = new V3();
5184
+ let best = Infinity;
5185
+ const out = [0.5, 0.5];
5186
+ const count = idx ? idx.count : pos.count;
5187
+ for (let f = 0; f + 2 < count; f += 3) {
5188
+ const ia = idx ? idx.getX(f) : f;
5189
+ const ib = idx ? idx.getX(f + 1) : f + 1;
5190
+ const ic = idx ? idx.getX(f + 2) : f + 2;
5191
+ tri.a.fromBufferAttribute(pos, ia);
5192
+ tri.b.fromBufferAttribute(pos, ib);
5193
+ tri.c.fromBufferAttribute(pos, ic);
5194
+ tri.closestPointToPoint(q, near);
5195
+ const d = near.distanceToSquared(q);
5196
+ if (d >= best) continue;
5197
+ best = d;
5198
+ tri.getBarycoord(near, bary);
5199
+ out[0] = uv.getX(ia) * bary.x + uv.getX(ib) * bary.y + uv.getX(ic) * bary.z;
5200
+ out[1] = uv.getY(ia) * bary.x + uv.getY(ib) * bary.y + uv.getY(ic) * bary.z;
4988
5201
  }
4989
- return [uv.getX(bi), uv.getY(bi)];
5202
+ return out;
4990
5203
  }
4991
5204
  function drawWeb(x, cx, cy, s, ax, rnd) {
4992
5205
  const n = 16 + Math.floor(rnd() * 8);
@@ -5063,19 +5276,9 @@ function installGlassDamage(kit) {
5063
5276
  x0.fillStyle = "rgba(48,66,72,0.5)";
5064
5277
  x0.fillRect(0, 0, 256, 256);
5065
5278
  G.tex = new THREE.CanvasTexture(G.cv);
5066
- msh.material = new THREE.MeshStandardMaterial({
5067
- color: 16777215,
5068
- map: G.tex,
5069
- emissive: 6976118,
5070
- emissiveMap: G.tex,
5071
- transparent: true,
5072
- roughness: 0.25,
5073
- metalness: 0.1,
5074
- side: DOUBLE_SIDE,
5075
- depthWrite: false
5076
- });
5279
+ msh.material = crackedGlassMaterial(THREE, G.tex);
5077
5280
  }
5078
- const uv = glassUV(msh, p);
5281
+ const uv = glassUV(c, msh, p);
5079
5282
  const x = G.cv.getContext("2d");
5080
5283
  drawWeb(x, uv[0] * 256, (1 - uv[1]) * 256, sc != null ? sc : 0.6 + force * 0.5, Math.max(0.4, G.aspect || 1), c.rand);
5081
5284
  G.tex.needsUpdate = true;
@@ -5220,6 +5423,13 @@ function installGlassDamage(kit) {
5220
5423
  }
5221
5424
  const seatGeo = [];
5222
5425
  const d = dir.clone().normalize();
5426
+ if (msh.parent && msh.parent !== c.vehicle) {
5427
+ c.root.updateMatrixWorld(true);
5428
+ const toVehicle = new THREE.Matrix4().copy(c.vehicle.matrixWorld).invert().multiply(msh.parent.matrixWorld);
5429
+ cubesAt.forEach(function(q) {
5430
+ q.applyMatrix4(toVehicle);
5431
+ });
5432
+ }
5223
5433
  cubesAt.forEach(function(p, i) {
5224
5434
  if (i % 3 === 0 && c.seatTops.length && !c.rollState && !c.flipped) {
5225
5435
  let best = null;
@@ -5422,6 +5632,7 @@ function installImpact(kit) {
5422
5632
  polygonOffsetUnits: -2
5423
5633
  });
5424
5634
  const _ray = new THREE.Raycaster();
5635
+ _ray.layers.enableAll();
5425
5636
  function findSurface(c, p, dir) {
5426
5637
  c.root.updateMatrixWorld(true);
5427
5638
  const Mw = c.vehicle.matrixWorld;
@@ -5503,6 +5714,7 @@ function installImpact(kit) {
5503
5714
  const v0 = new V3();
5504
5715
  const w = new V3();
5505
5716
  const sc = new V3();
5717
+ const local = new V3();
5506
5718
  const cab = m.cabin && !c.flipped && !c.rollState ? m.cabin : null;
5507
5719
  c.deform.forEach(function(msh) {
5508
5720
  if (!msh.parent || !msh.visible) return;
@@ -5517,7 +5729,15 @@ function installImpact(kit) {
5517
5729
  irel.copy(rel).invert();
5518
5730
  if (!g.userData.p0) g.userData.p0 = new Float32Array(pos.array);
5519
5731
  const p0 = g.userData.p0;
5732
+ local.copy(p).applyMatrix4(irel);
5733
+ const reach = H.R / Math.max(1e-6, Math.min(sc.x, sc.y, sc.z));
5734
+ const reach2 = reach * reach;
5735
+ const arr = pos.array;
5520
5736
  for (let i = 0; i < pos.count; i++) {
5737
+ const dx = arr[3 * i] - local.x;
5738
+ const dy = arr[3 * i + 1] - local.y;
5739
+ const dz = arr[3 * i + 2] - local.z;
5740
+ if (dx * dx + dy * dy + dz * dz >= reach2) continue;
5521
5741
  v.fromBufferAttribute(pos, i).applyMatrix4(rel);
5522
5742
  const a = dentField(v, H);
5523
5743
  if (a <= 0) continue;
@@ -5533,10 +5753,39 @@ function installImpact(kit) {
5533
5753
  }
5534
5754
  if (changed) {
5535
5755
  pos.needsUpdate = true;
5536
- if (g.userData.crease) crease(g, 42);
5756
+ if (g.userData.crease && c.smoothLater) c.smoothLater(msh);
5757
+ else if (g.userData.crease) crease(g, 42);
5537
5758
  else g.computeVertexNormals();
5538
- g.computeBoundingSphere();
5539
- g.computeBoundingBox();
5759
+ const sphere = g.boundingSphere ?? (g.boundingSphere = new THREE.Sphere());
5760
+ if (pos.isInterleavedBufferAttribute || pos.normalized || Object.keys(g.morphAttributes).length > 0) {
5761
+ g.computeBoundingBox();
5762
+ g.boundingBox.getCenter(sphere.center);
5763
+ let far2 = 0;
5764
+ for (let i = 0; i < pos.count; i++) far2 = Math.max(far2, sphere.center.distanceToSquared(v.fromBufferAttribute(pos, i)));
5765
+ sphere.radius = Math.sqrt(far2);
5766
+ } else {
5767
+ const box2 = g.boundingBox ?? (g.boundingBox = new THREE.Box3());
5768
+ let x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
5769
+ for (let i = 0; i < pos.count * 3; i += 3) {
5770
+ const x = arr[i], y = arr[i + 1], z = arr[i + 2];
5771
+ if (x < x0) x0 = x;
5772
+ if (y < y0) y0 = y;
5773
+ if (z < z0) z0 = z;
5774
+ if (x > x1) x1 = x;
5775
+ if (y > y1) y1 = y;
5776
+ if (z > z1) z1 = z;
5777
+ }
5778
+ box2.min.set(x0, y0, z0);
5779
+ box2.max.set(x1, y1, z1);
5780
+ box2.getCenter(sphere.center);
5781
+ const cx = sphere.center.x, cy = sphere.center.y, cz = sphere.center.z;
5782
+ let far2 = 0;
5783
+ for (let i = 0; i < pos.count * 3; i += 3) {
5784
+ const dx = cx - arr[i], dy = cy - arr[i + 1], dz = cz - arr[i + 2];
5785
+ far2 = Math.max(far2, dx * dx + dy * dy + dz * dz);
5786
+ }
5787
+ sphere.radius = Math.sqrt(far2);
5788
+ }
5540
5789
  }
5541
5790
  });
5542
5791
  const dset = new Set(c.deform);
@@ -5620,13 +5869,14 @@ function installImpact(kit) {
5620
5869
  kit.impact = impact;
5621
5870
  kit.partEffects = {};
5622
5871
  const _pr = new THREE.Raycaster();
5872
+ _pr.layers.enableAll();
5623
5873
  function probe(c, o, d, far) {
5624
5874
  c.root.updateMatrixWorld(true);
5625
5875
  const Mw = c.vehicle.matrixWorld;
5626
5876
  const inv = new THREE.Matrix4().copy(Mw).invert();
5627
5877
  _pr.set(o.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
5628
5878
  _pr.far = far || 10;
5629
- const hits = _pr.intersectObject(c.vehicle, true);
5879
+ const hits = raycastIndexed(THREE, _pr, [c.vehicle], true);
5630
5880
  for (let i = 0; i < hits.length; i++) {
5631
5881
  const h = hits[i];
5632
5882
  const ob = h.object;
@@ -5939,7 +6189,8 @@ function installExplosion(kit) {
5939
6189
  const G = gm2.userData.glass;
5940
6190
  if (!G || G.broken || !gm2.visible || !gm2.parent) return;
5941
6191
  if (G.type === "laminated") {
5942
- G.type = "tempered";
6192
+ for (let k2 = 0; k2 < 3; k2++) I.crackGlass(c, gm2, I.glassPt ? I.glassPt(c, gm2, c.rand) : gm2.position.clone(), 1.2);
6193
+ return;
5943
6194
  }
5944
6195
  I.shatterGlass(c, gm2, 1.4, new V3(0, 1, 0));
5945
6196
  });
@@ -5950,19 +6201,6 @@ function installExplosion(kit) {
5950
6201
  fling(c, c.hood.piv, new V3(rv(1, 3), rv(6, 8.5), rv(-1.5, 1.5)), new V3(rv(-4, 4), rv(-2, 2), rv(3, 7)));
5951
6202
  c.hood = null;
5952
6203
  }
5953
- const W = m.width || 1.6;
5954
- const gh = m.glasshouse;
5955
- if (m.kind === "car" && gh) {
5956
- ;
5957
- [1, -1].forEach(function(sd) {
5958
- const dh = Math.max(0.35, (m.belt || 1) - (m.clearance || 0.3) - 0.08);
5959
- const dx = gh.fb - 0.55;
5960
- const door = c.roundBox(0.95, dh, 0.045, 0.03, c.paint(m.color || "#777"), dx, (m.clearance || 0.3) + dh / 2 + 0.04, sd * (W / 2 + 0.01));
5961
- const hole = c.roundBox(0.9, dh * 0.95, 0.02, 0.02, MATERIALS.dark, dx, (m.clearance || 0.3) + dh / 2 + 0.04, sd * (W / 2 - 0.01));
5962
- hole.userData.noFollow = true;
5963
- fling(c, door, c.dirToEffects(new V3(rv(-1, 1), rv(3.5, 6), sd * rv(4, 6.5))), new V3(rv(-5, 5), rv(-4, 4), rv(-5, 5)));
5964
- });
5965
- }
5966
6204
  if (m.rearBumper && m.rearBumper.parent)
5967
6205
  fling(
5968
6206
  c,
@@ -5970,20 +6208,6 @@ function installExplosion(kit) {
5970
6208
  c.dirToEffects(new V3(rv(-5, -3), rv(4, 6), rv(-1, 1))),
5971
6209
  new V3(rv(-3, 3), rv(-6, 6), rv(-3, 3))
5972
6210
  );
5973
- if (c.wheels.length) {
5974
- const wi = Math.floor(c.rand() * c.wheels.length);
5975
- const wh = c.wheels[wi];
5976
- c.wheels.splice(wi, 1);
5977
- c.parts.forEach(function(p) {
5978
- if (p.obj === wh) p.done = true;
5979
- });
5980
- fling(
5981
- c,
5982
- wh,
5983
- c.dirToEffects(new V3(rv(-2, 2), rv(5, 7.5), (wh.position.z >= 0 ? 1 : -1) * rv(3, 5))),
5984
- new V3(rv(-8, 8), rv(-3, 3), rv(-8, 8))
5985
- );
5986
- }
5987
6211
  burnAll(c);
5988
6212
  c.emitters = c.emitters.filter(function(e) {
5989
6213
  return e.kind === "exhaust" ? false : true;
@@ -6992,6 +7216,7 @@ function installZoneDecals(kit) {
6992
7216
  return hole;
6993
7217
  }
6994
7218
  const _zr = new THREE.Raycaster();
7219
+ _zr.layers.enableAll();
6995
7220
  const PROXY_MIN_TRIANGLES = 500;
6996
7221
  const STROKE_REACH = 0.6;
6997
7222
  const _pv = new V3();
@@ -7226,6 +7451,7 @@ function installZoneDecals(kit) {
7226
7451
  return out;
7227
7452
  }
7228
7453
  const _rt = new THREE.Raycaster();
7454
+ _rt.layers.enableAll();
7229
7455
  function scratchPath(c, s, len, ang, cands, B) {
7230
7456
  const n = s.n.clone().normalize();
7231
7457
  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);
@@ -10507,6 +10733,8 @@ function createVehicleKit(options) {
10507
10733
  V3: THREE.Vector3,
10508
10734
  PI: Math.PI,
10509
10735
  document: doc,
10736
+ particleMaterials: opts.particleMaterials,
10737
+ effects: opts.effects,
10510
10738
  strings,
10511
10739
  t,
10512
10740
  matchers: createMatchers(strings),
@@ -10762,7 +10990,9 @@ function createVehicleKit(options) {
10762
10990
  assignDeep(strings, resolveStrings(opts.locale));
10763
10991
  return kit;
10764
10992
  };
10765
- const { THREE: _three, lod: _lod, locale: _locale, document: _document, plugins, ...config } = opts;
10993
+ const { THREE: _three, lod: _lod, locale: _locale, document: _document, particleMaterials: _particleMaterials, effects: _effects, plugins, ...config } = opts;
10994
+ void _particleMaterials;
10995
+ void _effects;
10766
10996
  void _three;
10767
10997
  void _lod;
10768
10998
  void _locale;
@@ -10772,6 +11002,17 @@ function createVehicleKit(options) {
10772
11002
  return kit;
10773
11003
  }
10774
11004
 
11005
+ // src/destructible/armour.ts
11006
+ var ARMOUR_LEVELS = {
11007
+ IIIA: { glassStops: 1700, glassHp: 6e3, glassTakes: 0.25, steel: 8 },
11008
+ III: { glassStops: 3600, glassHp: 9e3, glassTakes: 0.25, steel: 30 }
11009
+ };
11010
+ function armourLevelOf(meta, option) {
11011
+ if (option !== void 0) return option;
11012
+ if (meta.armor === "IIIA" || meta.armor === "III") return meta.armor;
11013
+ return meta.armored || meta.bullet ? "III" : null;
11014
+ }
11015
+
10775
11016
  // src/destructible/impl.ts
10776
11017
  import { ignitionChance, resolveIgnitionRules, sparkTargetFactor } from "@adatechnology/game-contracts";
10777
11018
 
@@ -10818,6 +11059,12 @@ var boxOf = (z) => ({
10818
11059
  center: [(z.box.x0 + z.box.x1) / 2, (z.box.y0 + z.box.y1) / 2, (z.box.z0 + z.box.z1) / 2],
10819
11060
  size: [Math.max(0.02, z.box.x1 - z.box.x0), Math.max(0.02, z.box.y1 - z.box.y0), Math.max(0.02, z.box.z1 - z.box.z0)]
10820
11061
  });
11062
+ var PART_FLUIDS = {
11063
+ gearbox: { fluid: "transmission_fluid", liters: 6 },
11064
+ brakes: { fluid: "brake_fluid", liters: 0.6 },
11065
+ steering: { fluid: "power_steering_fluid", liters: 1 },
11066
+ battery: { fluid: "battery_acid", liters: 3 }
11067
+ };
10821
11068
  var skinOf = (kind) => kind === "plane" || kind === "heli" || kind === "boat" ? "aluminium" : "steel";
10822
11069
  var isFender = (id) => /^fender/.test(id);
10823
11070
  var roleOfZone = (z) => {
@@ -10861,13 +11108,22 @@ var floorPan = (b) => ({
10861
11108
  center: [b.center[0], b.center[1] - b.size[1] / 2 + Math.min(0.14, b.size[1]) / 2 + 0.04, b.center[2]],
10862
11109
  size: [b.size[0] * 0.5, Math.min(0.14, b.size[1]), b.size[2] * 0.95]
10863
11110
  });
11111
+ function paneBox(c, pane, inverse) {
11112
+ pane.geometry.computeBoundingBox();
11113
+ const box = pane.geometry.boundingBox.clone().applyMatrix4(pane.matrixWorld);
11114
+ box.applyMatrix4(inverse);
11115
+ const center = [(box.min.x + box.max.x) / 2, (box.min.y + box.max.y) / 2, (box.min.z + box.max.z) / 2];
11116
+ const size = [box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z];
11117
+ return { center, size };
11118
+ }
10864
11119
  function buildVehicleModel(kit, c, options = {}) {
10865
11120
  const kind = kindOf(c);
10866
11121
  const m = c.meta;
10867
11122
  const spec = c.spec;
10868
11123
  const t = kit.t;
10869
11124
  const skin = skinOf(kind);
10870
- const armour = m.armored || m.bullet ? 12 : 1;
11125
+ const armoured = options.armour ?? null;
11126
+ const armour = armoured ? 12 : 1;
10871
11127
  const mass = vehicleMass(c);
10872
11128
  const W = m.width ?? 1.7;
10873
11129
  const F = m.front ?? 2;
@@ -10933,8 +11189,8 @@ function buildVehicleModel(kit, c, options = {}) {
10933
11189
  }
10934
11190
  if (role === "glass") {
10935
11191
  const glass = (z.glass ?? [])[0];
10936
- const laminated = glass?.userData?.glass?.type === "laminated" || m.bullet;
10937
- add({ id, role, name: label, material: "glass", box, mass: 10 * area, hp: laminated ? 4e3 : 450, zoneId: id, visual: false, tags: ["glass", ...tags] });
11192
+ const laminated = glass?.userData?.glass?.type === "laminated" || !!armoured;
11193
+ add({ id, role, name: label, material: "glass", box, mass: 10 * area, hp: armoured ? armoured.glassHp : laminated ? 4e3 : 450, zoneId: id, visual: false, tags: ["glass", ...tags] });
10938
11194
  continue;
10939
11195
  }
10940
11196
  if (role === "light") {
@@ -10972,7 +11228,7 @@ function buildVehicleModel(kit, c, options = {}) {
10972
11228
  zoneId: id,
10973
11229
  visual: false,
10974
11230
  contains: { fluid: diesel ? "diesel" : "fuel", liters },
10975
- explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320 },
11231
+ explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320, effect: "car_fuel_tank" },
10976
11232
  ignitionC: diesel ? 330 : 260,
10977
11233
  tags: ["fuel_tank", ...tags]
10978
11234
  });
@@ -11001,6 +11257,16 @@ function buildVehicleModel(kit, c, options = {}) {
11001
11257
  tags: [role, ...tags]
11002
11258
  });
11003
11259
  }
11260
+ const claimed = new Set(zones.filter((z) => roleOfZone(z) === "glass").flatMap((z) => z.glass ?? []));
11261
+ c.root.updateMatrixWorld(true);
11262
+ const inverse = c.vehicle.matrixWorld.clone().invert();
11263
+ for (const pane of c.glass ?? []) {
11264
+ if (claimed.has(pane)) continue;
11265
+ const G = pane.userData.glass;
11266
+ const bb = paneBox(c, pane, inverse);
11267
+ const laminated = G?.type === "laminated" || !!armoured;
11268
+ add({ id: `pane_${G?.key ?? "glass"}`, role: "glass", name: String(G?.label ?? G?.key ?? "glass"), material: "glass", box: bb, mass: 10 * areaOf(bb.size), hp: armoured ? armoured.glassHp : laminated ? 4e3 : 450, panes: [pane], visual: false, tags: ["glass"] });
11269
+ }
11004
11270
  if (!hubZone) {
11005
11271
  const h = Math.max(0.15, belt - clearance);
11006
11272
  add({
@@ -11055,7 +11321,7 @@ function buildVehicleModel(kit, c, options = {}) {
11055
11321
  visual: false,
11056
11322
  mechanical: true,
11057
11323
  contains: { fluid: diesel ? "diesel" : "fuel", liters },
11058
- explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320 },
11324
+ explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320, effect: "car_fuel_tank" },
11059
11325
  ignitionC: diesel ? 330 : 260,
11060
11326
  tags: ["fuel_tank"]
11061
11327
  });
@@ -11332,10 +11598,163 @@ function buildVehicleModel(kit, c, options = {}) {
11332
11598
  }
11333
11599
  }
11334
11600
  void has;
11601
+ for (const part of parts) {
11602
+ const fluid = PART_FLUIDS[part.role];
11603
+ if (fluid && !part.contains && !(kind === "bike" && part.role === "steering")) part.contains = { ...fluid };
11604
+ }
11335
11605
  return { parts, joints, hub: hubId, hitch };
11336
11606
  }
11337
11607
 
11608
+ // src/destructible/subdivide-near.ts
11609
+ var SUBDIVIDE_DEPTH = 12;
11610
+ function subdivideNear(THREE, params) {
11611
+ const { mesh, maxLen, near } = params;
11612
+ const src = mesh.geometry.index ? mesh.geometry.toNonIndexed() : mesh.geometry;
11613
+ const a = src.attributes.position.array;
11614
+ const toWorld = mesh.matrixWorld;
11615
+ const out = [];
11616
+ const mid = (p, q) => [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2, (p[2] + q[2]) / 2];
11617
+ const split = (A, B, C, depth) => {
11618
+ const ab = Math.hypot(A[0] - B[0], A[1] - B[1], A[2] - B[2]);
11619
+ const bc = Math.hypot(B[0] - C[0], B[1] - C[1], B[2] - C[2]);
11620
+ const ca = Math.hypot(C[0] - A[0], C[1] - A[1], C[2] - A[2]);
11621
+ const longest = Math.max(ab, bc, ca);
11622
+ if (longest <= maxLen || depth > SUBDIVIDE_DEPTH) {
11623
+ out.push(...A, ...B, ...C);
11624
+ return;
11625
+ }
11626
+ if (longest === ab) {
11627
+ const D = mid(A, B);
11628
+ split(A, D, C, depth + 1);
11629
+ split(D, B, C, depth + 1);
11630
+ } else if (longest === bc) {
11631
+ const E = mid(B, C);
11632
+ split(A, B, E, depth + 1);
11633
+ split(A, E, C, depth + 1);
11634
+ } else {
11635
+ const F = mid(C, A);
11636
+ split(A, B, F, depth + 1);
11637
+ split(F, B, C, depth + 1);
11638
+ }
11639
+ };
11640
+ for (let i = 0; i < a.length; i += 9) {
11641
+ const A = [a[i], a[i + 1], a[i + 2]];
11642
+ const B = [a[i + 3], a[i + 4], a[i + 5]];
11643
+ const C = [a[i + 6], a[i + 7], a[i + 8]];
11644
+ const corners = [A, B, C].map((p) => new THREE.Vector3(p[0], p[1], p[2]).applyMatrix4(toWorld));
11645
+ if (near(corners)) split(A, B, C, 0);
11646
+ else out.push(...A, ...B, ...C);
11647
+ }
11648
+ const g = new THREE.BufferGeometry();
11649
+ g.setAttribute("position", new THREE.Float32BufferAttribute(out, 3));
11650
+ mesh.geometry.dispose();
11651
+ mesh.geometry = g;
11652
+ }
11653
+
11338
11654
  // src/destructible/visual.ts
11655
+ var HOLE_DECAL_SCALE = 2.4;
11656
+ var DOOR_DEPTH = 0.07;
11657
+ var DOOR_EDGE_PIECES = 8;
11658
+ var DOOR_EDGE_REACH = 0.08;
11659
+ var DOOR_CARD_COLOR = "#3b3e42";
11660
+ var DOOR_LINER_COLOR = "#5a5e64";
11661
+ var DOOR_LINER_DARK = 0.15;
11662
+ var DOOR_STRIP_THIN = 0.08;
11663
+ var DOOR_STRIP_NEAR = 0.04;
11664
+ var DOOR_STRIP_TALL = 0.22;
11665
+ var STRIP_SMOOTH_DEG = 180;
11666
+ var DOOR_STRIP_VERTICES = 2e4;
11667
+ var DOOR_SILL = 0.25;
11668
+ var DOOR_STRIP_OVER = 0.3;
11669
+ var DOOR_TOP_OVER_PANE = 0.03;
11670
+ var HOLE_TEXTURE_SIZE = 64;
11671
+ var LAMINATED_HP = 1e3;
11672
+ var HOOD_SINK = 2e-3;
11673
+ var HOOD_GAP_CLOSED = 0.016;
11674
+ var DOOR_HANDLE_INSET = 0.12;
11675
+ var DOOR_HANDLE_MIN = 0.07;
11676
+ var DOOR_HANDLE_MAX = 0.24;
11677
+ var DOOR_HANDLE_THIN = 0.07;
11678
+ var DOOR_HANDLE_OUTSIDE = 0.15;
11679
+ var PANE_SEAT_OUT = 0.05;
11680
+ var PANE_SEAT_IN = -0.05;
11681
+ var PANE_SEAT_REACH = 0.14;
11682
+ var PANE_SEAT_SAMPLES = 24;
11683
+ var PANE_SEAT_LIFT = 4e-3;
11684
+ var PANE_FRAME_NEAR = 0.07;
11685
+ var PANE_MIN_SLOPE = 0.01;
11686
+ var PANE_EDGE_BAND = 0.03;
11687
+ var PANE_REACH_STEP = 0.01;
11688
+ var PANE_REACH_MAX = 0.25;
11689
+ var COWL_UNDER_GLASS = 0;
11690
+ var COWL_NEAR_PANE = 0.08;
11691
+ var COWL_PLATE = 0.06;
11692
+ var COWL_DASH = 0.5;
11693
+ var COWL_DEEP = 0.3;
11694
+ var WINDOW_FRAME_NEAR = 0.06;
11695
+ var WINDOW_FRAME_SPAN = 0.6;
11696
+ var DASHBOARD_COLOR = "#121314";
11697
+ var COWL_CUT_SUB = 0.1;
11698
+ var COWL_CUT_MARGIN = 0.08;
11699
+ var SKIN_UNDER_PANE = 0.025;
11700
+ var SKIN_EDGE_KEPT = 0.06;
11701
+ var SIDE_PANE_FACING = 0.6;
11702
+ var COWL_LONG = 0.8;
11703
+ var PANE_BAR_VERTICES = 48;
11704
+ var PANE_BAR_LENGTH = 0.3;
11705
+ var PANE_BORDER = 0.03;
11706
+ var PANE_BORDER_LIFT = 3e-3;
11707
+ var DRAWN_MIRROR_SIZE = 0.3;
11708
+ var DRAWN_MIRROR_REACH = 0.4;
11709
+ var DRAWN_MIRROR_MOUNT = 0.2;
11710
+ var MIRROR_GLASS_FIT = 0.76;
11711
+ var MIRROR_GLASS_PROUD = 1e-3;
11712
+ var MIRROR_FACE_DEPTH = 0.012;
11713
+ var MIRROR_GLASS_THIN = 0.04;
11714
+ var DEFAULT_WHEEL_RADIUS = 0.3;
11715
+ var SIDEWALL_SHARE = 0.3;
11716
+ var MIRROR_GLASS_MIN = 0.05;
11717
+ var MIRROR_GLASS_COLOR = "#59636a";
11718
+ var HOLE_RIM = { steel: "#b7bcc0", aluminium: "#d3d6d8", plastic: "#d9d6cc", rubber: "#141414", glass: "#eef3f5" };
11719
+ var holeMaterials = /* @__PURE__ */ new WeakMap();
11720
+ var holeDisc = null;
11721
+ function holeDiscGeometry(THREE) {
11722
+ holeDisc = holeDisc ?? new THREE.CircleGeometry(1, 12);
11723
+ return holeDisc;
11724
+ }
11725
+ function holeMaterial(kit, material) {
11726
+ const key = kit;
11727
+ const cache2 = holeMaterials.get(key) ?? /* @__PURE__ */ new Map();
11728
+ holeMaterials.set(key, cache2);
11729
+ const known = cache2.get(material);
11730
+ if (known) return known;
11731
+ const I = kit._;
11732
+ const THREE = kit.THREE;
11733
+ const canvas = kit.document?.createElement?.("canvas");
11734
+ const x = canvas?.getContext?.("2d");
11735
+ let made = I.MATERIALS.dark;
11736
+ if (x) {
11737
+ canvas.width = canvas.height = HOLE_TEXTURE_SIZE;
11738
+ const mid = HOLE_TEXTURE_SIZE / 2;
11739
+ x.fillStyle = HOLE_RIM[material] ?? HOLE_RIM.steel;
11740
+ x.beginPath();
11741
+ for (let i = 0; i < 18; i++) {
11742
+ const a = i / 18 * Math.PI * 2;
11743
+ const r = mid * (i % 2 ? 0.98 : 0.76);
11744
+ x.lineTo(mid + Math.cos(a) * r, mid + Math.sin(a) * r);
11745
+ }
11746
+ x.closePath();
11747
+ x.fill();
11748
+ x.fillStyle = "#050505";
11749
+ x.beginPath();
11750
+ x.arc(mid, mid, mid * 0.42, 0, Math.PI * 2);
11751
+ x.fill();
11752
+ const map = new THREE.CanvasTexture(canvas);
11753
+ made = new THREE.MeshStandardMaterial({ map, transparent: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -2, roughness: 0.55, metalness: material === "steel" || material === "aluminium" ? 0.6 : 0 });
11754
+ }
11755
+ cache2.set(material, made);
11756
+ return made;
11757
+ }
11339
11758
  var VehicleVisuals = class {
11340
11759
  constructor(kit, c, model) {
11341
11760
  __publicField(this, "kit");
@@ -11353,14 +11772,23 @@ var VehicleVisuals = class {
11353
11772
  __publicField(this, "syncPlates", /* @__PURE__ */ new Map());
11354
11773
  /** Doors and lids whose own skin was cut from the body and hung on the hinge (true), or that had none to take and keep the plate (false). */
11355
11774
  __publicField(this, "skins", /* @__PURE__ */ new Map());
11775
+ /** How high each door's cut reaches. */
11776
+ __publicField(this, "doorTops", /* @__PURE__ */ new Map());
11356
11777
  __publicField(this, "lightsOn", true);
11357
11778
  /** Heavy body meshes whose triangle index is still to build (null once done). */
11358
11779
  __publicField(this, "warming");
11359
11780
  /** A panel was torn off since the last `pump`: that frame already paid for the cut, the smoothing starts on the next one. */
11360
11781
  __publicField(this, "tornThisFrame", false);
11782
+ /** A shared crease queue owns the smoothing: `pump` only closes the frame of the cut. */
11783
+ __publicField(this, "queued", false);
11784
+ /** A panel was torn and the queue has not asked yet: the first `pendingUnits(frame)` after it holds the work for that frame. */
11785
+ __publicField(this, "holdArmed", false);
11786
+ __publicField(this, "holdFrame", -1);
11361
11787
  /** Normal smoothing still running, by mesh: a mesh cut again gets a new task and the old one is dropped. */
11362
11788
  __publicField(this, "smoothing", /* @__PURE__ */ new Map());
11363
11789
  __publicField(this, "hood");
11790
+ ;
11791
+ c.smoothLater = (mesh) => this.smoothLater(mesh);
11364
11792
  this.kit = kit;
11365
11793
  this.c = c;
11366
11794
  this.model = model;
@@ -11369,6 +11797,9 @@ var VehicleVisuals = class {
11369
11797
  for (const p of model.parts) this.byId.set(p.id, p);
11370
11798
  c.root.updateMatrixWorld(true);
11371
11799
  this.hood = this.nameHood();
11800
+ this.seatPanes();
11801
+ this.seatDoorHandles();
11802
+ this.seatHood();
11372
11803
  this.warming = c.deform.filter((msh) => msh.geometry && msh.geometry.attributes.position && msh.geometry.attributes.position.count >= 1500);
11373
11804
  if (!this.warming.length) this.warming = null;
11374
11805
  for (const p of model.parts) {
@@ -11389,6 +11820,7 @@ var VehicleVisuals = class {
11389
11820
  const id = z ? z.id : this.model.parts.find((p) => p.role === "hood")?.id;
11390
11821
  if (!id) return null;
11391
11822
  c.hood.piv.name = id;
11823
+ c.hood.piv.userData.plateOf = id;
11392
11824
  return { id, open: c.hood.open, sign: c.hood.sign || 1 };
11393
11825
  }
11394
11826
  mirrorObjects(p) {
@@ -11476,6 +11908,7 @@ var VehicleVisuals = class {
11476
11908
  recess.userData.noFollow = true;
11477
11909
  const piv = new THREE.Group();
11478
11910
  piv.name = p.id;
11911
+ piv.userData.plateOf = p.id;
11479
11912
  piv.position.set(hx, cy, sk + s * 0.016);
11480
11913
  piv.userData.rigid = true;
11481
11914
  this.c.vehicle.add(piv);
@@ -11492,6 +11925,7 @@ var VehicleVisuals = class {
11492
11925
  recess.userData.noFollow = true;
11493
11926
  const piv = new THREE.Group();
11494
11927
  piv.name = p.id;
11928
+ piv.userData.plateOf = p.id;
11495
11929
  piv.position.set(sk - 0.016, B.y1 - 0.05, 0);
11496
11930
  piv.userData.rigid = true;
11497
11931
  this.c.vehicle.add(piv);
@@ -11555,10 +11989,20 @@ var VehicleVisuals = class {
11555
11989
  try {
11556
11990
  if (p.role === "door") {
11557
11991
  const dirty = /* @__PURE__ */ new Set();
11558
- 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];
11559
- const pcs = I.cutPanel(this.c, z, rect, true, dirty).map((x) => x.mesh).filter((m) => !!m);
11992
+ const glassBox = new this.THREE.Box3();
11993
+ for (const g of I.windowsOver(this.c, z, inv)) glassBox.union(I.localBox(this.c, g, inv));
11994
+ const top = glassBox.isEmpty() ? B.y1 - 0.02 : Math.max(B.y1 - 0.02, glassBox.min.y + DOOR_TOP_OVER_PANE);
11995
+ this.doorTops.set(id, top);
11996
+ const rect = [B.x0 + 0.03, B.x1 - 0.03, B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1), top];
11997
+ const pcs = I.cutPanel(this.c, z, rect, true, dirty, { anyN: true }).map((x) => x.mesh).filter((m) => !!m);
11560
11998
  this.mend(dirty);
11561
- if (pcs.length) objs = [...pcs, ...I.followersIn(this.c, z, inv), ...I.windowsOver(this.c, z, inv)];
11999
+ if (pcs.length) {
12000
+ const panes = I.windowsOver(this.c, z, inv);
12001
+ const seals = panes.map((g) => I.paneSeals.get(g)).filter((s) => !!s);
12002
+ const mirrors = this.mirrorsOn(z);
12003
+ const frames = this.framesAround(panes, inv);
12004
+ objs = [...pcs, ...I.followersIn(this.c, z, inv), ...panes, ...seals, ...mirrors, ...frames];
12005
+ }
11562
12006
  } else {
11563
12007
  const pcs = this.cutZone(z);
11564
12008
  if (pcs.length) objs = [...pcs, ...I.followersIn(this.c, z, inv)];
@@ -11576,10 +12020,152 @@ var VehicleVisuals = class {
11576
12020
  const di = deform.indexOf(o);
11577
12021
  if (di >= 0) deform.splice(di, 1);
11578
12022
  }
12023
+ if (p.role === "door" && objs.length > 0) {
12024
+ this.doorEdges(piv, z, this.doorTops.get(id) ?? B.y1 - 0.02);
12025
+ this.liftDoorTrim(objs, z);
12026
+ this.cutStripsAcross(piv, z, inv);
12027
+ }
11579
12028
  piv.quaternion.copy(q);
11580
12029
  this.c.root.updateMatrixWorld(true);
11581
12030
  return objs.length > 0;
11582
12031
  }
12032
+ /** 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. */
12033
+ doorEdges(piv, z, y1) {
12034
+ const THREE = this.THREE;
12035
+ const B = z.box;
12036
+ const side2 = z.side < 0 ? -1 : 1;
12037
+ const flat = piv.position.z - side2 * 0.016;
12038
+ const x0 = B.x0 + 0.03;
12039
+ const x1 = B.x1 - 0.03;
12040
+ const y0 = B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1);
12041
+ const loop = [];
12042
+ const run = (ax, ay, bx, by) => {
12043
+ 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]);
12044
+ };
12045
+ run(x0, y0, x1, y0);
12046
+ run(x1, y0, x1, y1);
12047
+ run(x1, y1, x0, y1);
12048
+ run(x0, y1, x0, y0);
12049
+ const heights = loop.map(([x, y]) => {
12050
+ const at = this.surface("z", side2, B, [[x, y]]);
12051
+ return at != null && Math.abs(at - flat) < DOOR_EDGE_REACH ? at : flat;
12052
+ });
12053
+ const band = (lift) => {
12054
+ const position = [];
12055
+ const index = [];
12056
+ loop.forEach(([x, y], i) => {
12057
+ position.push(x, y, heights[i] + side2 * lift, x, y, heights[i] + side2 * (lift - DOOR_DEPTH));
12058
+ const a = i * 2;
12059
+ const b = (i + 1) % loop.length * 2;
12060
+ index.push(a, a + 1, b, b, a + 1, b + 1);
12061
+ });
12062
+ const g = new THREE.BufferGeometry();
12063
+ g.setAttribute("position", new THREE.Float32BufferAttribute(position, 3));
12064
+ g.setIndex(index);
12065
+ g.computeVertexNormals();
12066
+ return g;
12067
+ };
12068
+ const paint = this.paint().clone();
12069
+ paint.side = THREE.DoubleSide;
12070
+ const cardPosition = [];
12071
+ const mid = loop.reduce((acc, [x, y], i) => [acc[0] + x, acc[1] + y, acc[2] + heights[i]], [0, 0, 0]).map((v) => v / loop.length);
12072
+ const cardZ = (z2) => z2 - side2 * DOOR_DEPTH;
12073
+ loop.forEach(([x, y], i) => {
12074
+ const j = (i + 1) % loop.length;
12075
+ cardPosition.push(mid[0], mid[1], cardZ(mid[2]), x, y, cardZ(heights[i]), loop[j][0], loop[j][1], cardZ(heights[j]));
12076
+ });
12077
+ const cardGeometry = new THREE.BufferGeometry();
12078
+ cardGeometry.setAttribute("position", new THREE.Float32BufferAttribute(cardPosition, 3));
12079
+ cardGeometry.computeVertexNormals();
12080
+ const cardMaterial = this.I.makeMaterial(DOOR_CARD_COLOR, { roughness: 0.85 }).clone();
12081
+ cardMaterial.side = THREE.DoubleSide;
12082
+ const card = new THREE.Mesh(cardGeometry, cardMaterial);
12083
+ card.userData.noFollow = true;
12084
+ card.castShadow = false;
12085
+ this.c.vehicle.add(card);
12086
+ piv.attach(card);
12087
+ const edges = new THREE.Mesh(band(2e-3), paint);
12088
+ const lining = new THREE.Mesh(band(2e-3), paint);
12089
+ for (const m of [edges, lining]) {
12090
+ m.userData.noFollow = true;
12091
+ m.castShadow = false;
12092
+ this.c.vehicle.add(m);
12093
+ }
12094
+ piv.attach(edges);
12095
+ }
12096
+ /** The frame the model draws round a door's window (a dark strip over the glass) goes with the door: left behind it hangs in the air over the opening. */
12097
+ framesAround(panes, inv) {
12098
+ const THREE = this.THREE;
12099
+ const boxes = panes.map((g) => this.I.localBox(this.c, g, inv).expandByScalar(WINDOW_FRAME_NEAR));
12100
+ return this.c.vehicle.children.filter((o) => {
12101
+ if (!o.isMesh || !o.visible || o.userData.glass || o.userData.decal || this.c.deform?.includes(o)) return false;
12102
+ const box = this.I.localBox(this.c, o, inv);
12103
+ if (box.isEmpty()) return false;
12104
+ return boxes.some((b) => b.containsBox(box) && box.getSize(new THREE.Vector3()).x > b.getSize(new THREE.Vector3()).x * WINDOW_FRAME_SPAN);
12105
+ });
12106
+ }
12107
+ /** A front door carries the mirror drawn at its corner. */
12108
+ mirrorsOn(z) {
12109
+ if (!z.mirror) return [];
12110
+ const own = (z.mirrors ?? []).filter((o) => o.parent);
12111
+ if (own.length) return own;
12112
+ const part = this.model.parts.find((q) => q.role === "mirror" && Math.sign(q.box.center[2]) === Math.sign(z.side));
12113
+ return part ? (this.extra.get(part.id) ?? []).filter((o) => o.parent) : [];
12114
+ }
12115
+ /** The panel drawn inside the door is lit like the rest of a cabin, not a black slab from end to end. */
12116
+ liftDoorTrim(objs, z) {
12117
+ const card = this.I.makeMaterial(DOOR_LINER_COLOR, { roughness: 0.85 });
12118
+ const inv = this.I.worldInverseOf(this.c);
12119
+ for (const o of objs) {
12120
+ if (!o.isMesh || o.userData.glass) continue;
12121
+ const box = this.I.localBox(this.c, o, inv);
12122
+ const size = box.getSize(new this.THREE.Vector3());
12123
+ 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;
12124
+ }
12125
+ }
12126
+ /** 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. */
12127
+ cutStripsAcross(piv, z, inv) {
12128
+ const THREE = this.THREE;
12129
+ const B = z.box;
12130
+ const x0 = B.x0 + 0.03;
12131
+ const x1 = B.x1 - 0.03;
12132
+ const y0 = B.y0 + DOOR_SILL;
12133
+ const glassTop = (I) => {
12134
+ let top = -Infinity;
12135
+ for (const g of I.windowsOver(this.c, z, inv)) top = Math.max(top, I.localBox(this.c, g, inv).max.y);
12136
+ return top;
12137
+ };
12138
+ const y1 = Math.max(B.y1, glassTop(this.I)) + DOOR_STRIP_NEAR;
12139
+ const deform = this.c.deform ?? [];
12140
+ for (const o of [...this.c.vehicle.children]) {
12141
+ if (!o.isMesh || o === piv || o.userData.glass || o.userData.decal || !o.visible || deform.includes(o) && o.geometry.attributes.position.count > DOOR_STRIP_VERTICES) continue;
12142
+ const box = this.I.localBox(this.c, o, inv);
12143
+ const size = box.getSize(new THREE.Vector3());
12144
+ const mid = box.getCenter(new THREE.Vector3());
12145
+ if (Math.sign(mid.z) !== Math.sign(z.side) || size.z > DOOR_STRIP_THIN || size.y > DOOR_STRIP_TALL || Math.min(box.max.x, x1) - Math.max(box.min.x, x0) < (x1 - x0) * DOOR_STRIP_OVER || mid.y < y0 || mid.y > y1) continue;
12146
+ const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld);
12147
+ const v = new THREE.Vector3();
12148
+ const sdf = (x, y, zz) => {
12149
+ v.set(x, y, zz).applyMatrix4(toVehicle);
12150
+ return Math.max(x0 - v.x, v.x - x1);
12151
+ };
12152
+ const over = new THREE.Mesh(o.geometry.clone(), o.material);
12153
+ over.position.copy(o.position);
12154
+ over.quaternion.copy(o.quaternion);
12155
+ over.scale.copy(o.scale);
12156
+ this.c.vehicle.add(over);
12157
+ over.updateMatrixWorld(true);
12158
+ const overToVehicle = new THREE.Matrix4().multiplyMatrices(inv, over.matrixWorld);
12159
+ const w = new THREE.Vector3();
12160
+ this.kit.cutMesh(over, [{ sdf: (x, y, zz) => {
12161
+ w.set(x, y, zz).applyMatrix4(overToVehicle);
12162
+ return -Math.max(x0 - w.x, w.x - x1);
12163
+ } }], STRIP_SMOOTH_DEG, {});
12164
+ this.kit.cutMesh(o, [{ sdf }], STRIP_SMOOTH_DEG, {});
12165
+ over.userData.noFollow = true;
12166
+ piv.attach(over);
12167
+ }
12168
+ }
11583
12169
  mend(dirty) {
11584
12170
  dirty.forEach((msh) => {
11585
12171
  const g = msh.geometry;
@@ -11589,7 +12175,470 @@ var VehicleVisuals = class {
11589
12175
  g.computeBoundingBox();
11590
12176
  });
11591
12177
  }
12178
+ /**
12179
+ * 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
12180
+ * comes to the shell along its own normal, by how far the shell is just outside each of its vertices.
12181
+ */
12182
+ seatPanes() {
12183
+ const THREE = this.THREE;
12184
+ const c = this.c;
12185
+ c.root.updateMatrixWorld(true);
12186
+ const shell = c.deform.filter((o) => o.isMesh && !o.userData.glass && c.paints?.has(o.material));
12187
+ if (!shell.length) return;
12188
+ const ray = new THREE.Raycaster();
12189
+ ray.layers.enableAll();
12190
+ for (const pane of c.glass) {
12191
+ if (pane.userData.glass?.outline) continue;
12192
+ const local = this.shellAround(pane, shell);
12193
+ if (!local) continue;
12194
+ const seat = this.paneSeat(pane, [local], ray);
12195
+ if (!seat) continue;
12196
+ const near = c.vehicle.children.filter((o) => o.isMesh && o !== pane && !o.userData.glass && !shell.includes(o) && this.onPane(o, pane, seat));
12197
+ for (const m of [pane, ...near]) this.movePane(m, seat);
12198
+ const past = this.shellPast(pane, [local], ray, seat.normal);
12199
+ if (past > 0) for (const m of [pane, ...near]) this.movePane(m, { normal: seat.normal, samples: [{ at: seat.samples[0].at, by: past }] });
12200
+ this.reachShellBelow(pane, local, ray, seat.normal);
12201
+ this.paneBorder(pane, near, seat);
12202
+ this.keepUnderPane(pane, seat.normal);
12203
+ const across = new this.THREE.Vector3(0, 0, 1).transformDirection(this.c.vehicle.matrixWorld);
12204
+ if (Math.abs(seat.normal.dot(across)) < SIDE_PANE_FACING) {
12205
+ for (const body of shell) if (body.parent === this.c.vehicle) this.cutUnderPane(body, pane, seat.normal, SKIN_UNDER_PANE, SKIN_EDGE_KEPT);
12206
+ }
12207
+ }
12208
+ }
12209
+ /** Only the triangles of the shell around a pane, in world space: seating casts many rays and the whole hull has too many triangles for each. */
12210
+ shellAround(pane, shell) {
12211
+ const THREE = this.THREE;
12212
+ const box = new THREE.Box3().setFromObject(pane).expandByScalar(PANE_SEAT_REACH + PANE_SEAT_OUT);
12213
+ const out = [];
12214
+ const a = new THREE.Vector3();
12215
+ const b = new THREE.Vector3();
12216
+ const d = new THREE.Vector3();
12217
+ const tri = new THREE.Box3();
12218
+ for (const mesh of shell) {
12219
+ const pos = mesh.geometry.attributes.position;
12220
+ const idx = mesh.geometry.index;
12221
+ const count = idx ? idx.count : pos.count;
12222
+ const e = mesh.matrixWorld.elements;
12223
+ const world = (v, i) => {
12224
+ const x = pos.getX(i);
12225
+ const y = pos.getY(i);
12226
+ const z = pos.getZ(i);
12227
+ v.set(e[0] * x + e[4] * y + e[8] * z + e[12], e[1] * x + e[5] * y + e[9] * z + e[13], e[2] * x + e[6] * y + e[10] * z + e[14]);
12228
+ };
12229
+ for (let f = 0; f + 2 < count; f += 3) {
12230
+ world(a, idx ? idx.getX(f) : f);
12231
+ world(b, idx ? idx.getX(f + 1) : f + 1);
12232
+ world(d, idx ? idx.getX(f + 2) : f + 2);
12233
+ tri.makeEmpty().expandByPoint(a).expandByPoint(b).expandByPoint(d);
12234
+ if (tri.intersectsBox(box)) out.push(a.x, a.y, a.z, b.x, b.y, b.z, d.x, d.y, d.z);
12235
+ }
12236
+ }
12237
+ if (!out.length) return null;
12238
+ const g = new THREE.BufferGeometry();
12239
+ g.setAttribute("position", new THREE.Float32BufferAttribute(out, 3));
12240
+ const proxy = new THREE.Mesh(g, new THREE.MeshBasicMaterial({ side: THREE.DoubleSide }));
12241
+ proxy.updateMatrixWorld(true);
12242
+ return proxy;
12243
+ }
12244
+ /** The outward normal of a pane and how far the shell is at each sampled point (world). */
12245
+ paneSeat(pane, shell, ray) {
12246
+ const THREE = this.THREE;
12247
+ const pos = pane.geometry.attributes.position;
12248
+ const box = new THREE.Box3().setFromObject(pane);
12249
+ const centre = box.getCenter(new THREE.Vector3());
12250
+ const vehicleCentre = new THREE.Box3().setFromObject(this.c.root).getCenter(new THREE.Vector3());
12251
+ const tri = new THREE.Triangle();
12252
+ const normal = new THREE.Vector3();
12253
+ let largest = 0;
12254
+ const idx = pane.geometry.index;
12255
+ const count = idx ? idx.count : pos.count;
12256
+ for (let f = 0; f + 2 < count; f += 3) {
12257
+ tri.a.fromBufferAttribute(pos, idx ? idx.getX(f) : f);
12258
+ tri.b.fromBufferAttribute(pos, idx ? idx.getX(f + 1) : f + 1);
12259
+ tri.c.fromBufferAttribute(pos, idx ? idx.getX(f + 2) : f + 2);
12260
+ if (tri.getArea() > largest) {
12261
+ largest = tri.getArea();
12262
+ tri.getNormal(normal);
12263
+ }
12264
+ }
12265
+ normal.transformDirection(pane.matrixWorld);
12266
+ if (normal.dot(centre.clone().sub(vehicleCentre)) < 0) normal.negate();
12267
+ const samples = [];
12268
+ const seen = /* @__PURE__ */ new Set();
12269
+ const v = new THREE.Vector3();
12270
+ const step = Math.max(1, Math.floor(pos.count / PANE_SEAT_SAMPLES));
12271
+ for (let i = 0; i < pos.count; i += step) {
12272
+ v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
12273
+ const key = `${Math.round(v.x * 100)},${Math.round(v.y * 100)},${Math.round(v.z * 100)}`;
12274
+ if (seen.has(key)) continue;
12275
+ seen.add(key);
12276
+ const away = v.clone().sub(centre);
12277
+ away.addScaledVector(normal, -away.dot(normal)).normalize();
12278
+ let by = -Infinity;
12279
+ for (const off of [PANE_SEAT_OUT, PANE_SEAT_IN, PANE_SEAT_IN * 3]) {
12280
+ const at = v.clone().addScaledVector(away, off);
12281
+ ray.set(at.addScaledVector(normal, PANE_SEAT_REACH), normal.clone().negate());
12282
+ ray.far = PANE_SEAT_REACH * 2;
12283
+ const hit = raycastIndexed(THREE, ray, shell)[0];
12284
+ if (hit) by = Math.max(by, PANE_SEAT_REACH - hit.distance);
12285
+ }
12286
+ if (by > -Infinity) samples.push({ at: v.clone(), by });
12287
+ }
12288
+ return samples.length >= 3 ? { normal, samples } : null;
12289
+ }
12290
+ /** The lower edge of a pane comes down to the shell: left short, the dashboard shows through the slit as a black bar under the glass. */
12291
+ reachShellBelow(pane, shell, ray, normal) {
12292
+ const THREE = this.THREE;
12293
+ const down = new THREE.Vector3(0, -1, 0).applyQuaternion(this.c.root.getWorldQuaternion(new THREE.Quaternion()));
12294
+ down.addScaledVector(normal, -down.dot(normal));
12295
+ if (down.lengthSq() < PANE_MIN_SLOPE) return;
12296
+ down.normalize();
12297
+ const g = pane.geometry;
12298
+ const pos = g.attributes.position;
12299
+ const v = new THREE.Vector3();
12300
+ let lowest = -Infinity;
12301
+ for (let i = 0; i < pos.count; i++) lowest = Math.max(lowest, v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld).dot(down));
12302
+ const edge = [];
12303
+ const mid = new THREE.Vector3();
12304
+ for (let i = 0; i < pos.count; i++) {
12305
+ v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
12306
+ if (v.dot(down) > lowest - PANE_EDGE_BAND) {
12307
+ edge.push(i);
12308
+ mid.add(v);
12309
+ }
12310
+ }
12311
+ if (!edge.length) return;
12312
+ mid.divideScalar(edge.length);
12313
+ let reach = 0;
12314
+ for (let d = PANE_REACH_STEP; d <= PANE_REACH_MAX; d += PANE_REACH_STEP) {
12315
+ const at = mid.clone().addScaledVector(down, d).addScaledVector(normal, PANE_SEAT_REACH);
12316
+ ray.set(at, normal.clone().negate());
12317
+ ray.far = PANE_SEAT_REACH * 2;
12318
+ if (ray.intersectObject(shell, false)[0]) {
12319
+ reach = d + PANE_REACH_STEP;
12320
+ break;
12321
+ }
12322
+ }
12323
+ if (!reach) return;
12324
+ const inverse = pane.matrixWorld.clone().invert();
12325
+ for (const i of edge) {
12326
+ v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld).addScaledVector(down, reach).applyMatrix4(inverse);
12327
+ pos.setXYZ(i, v.x, v.y, v.z);
12328
+ }
12329
+ pos.needsUpdate = true;
12330
+ g.computeBoundingBox();
12331
+ g.computeBoundingSphere();
12332
+ }
12333
+ /** The plate drawn right under a windshield stays only outside the glass, in the body's paint: under the glass it would show inside the cabin. */
12334
+ keepUnderPane(pane, normal) {
12335
+ const THREE = this.THREE;
12336
+ const paneBox2 = new THREE.Box3().setFromObject(pane).expandByScalar(COWL_NEAR_PANE);
12337
+ const pos = pane.geometry.attributes.position;
12338
+ const centre = new THREE.Vector3();
12339
+ const v = new THREE.Vector3();
12340
+ for (let i = 0; i < pos.count; i++) centre.add(v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld));
12341
+ centre.divideScalar(pos.count);
12342
+ const plane = centre.dot(normal);
12343
+ for (const o of this.c.vehicle.children) {
12344
+ if (!o.isMesh || o === pane || o.userData.glass || this.c.deform?.includes(o)) continue;
12345
+ const box = new THREE.Box3().setFromObject(o);
12346
+ const size = box.getSize(new THREE.Vector3());
12347
+ if (o.geometry.attributes.position.count <= PANE_BAR_VERTICES || size.y > COWL_DASH || Math.max(size.x, size.z) < COWL_LONG || Math.min(size.x, size.z) < COWL_DEEP || !paneBox2.intersectsBox(box)) continue;
12348
+ if (box.getCenter(new THREE.Vector3()).dot(normal) - plane >= COWL_UNDER_GLASS) continue;
12349
+ if (size.y >= COWL_PLATE) {
12350
+ o.material = this.I.makeMaterial(DASHBOARD_COLOR, { roughness: 1, metalness: 0 });
12351
+ continue;
12352
+ }
12353
+ o.material = this.paint();
12354
+ this.cutUnderPane(o, pane, normal);
12355
+ }
12356
+ }
12357
+ /** Cuts away the part of a mesh that lies under a pane (behind its plane, inside its outline seen along its normal). */
12358
+ cutUnderPane(mesh, pane, normal, behind = -Infinity, inset = 0) {
12359
+ const THREE = this.THREE;
12360
+ const outline = this.paneOutline(pane, normal);
12361
+ if (outline.length < 3) return;
12362
+ const u = new THREE.Vector3(0, 1, 0).cross(normal);
12363
+ if (u.lengthSq() < 1e-4) u.set(1, 0, 0);
12364
+ u.normalize();
12365
+ const w = normal.clone().cross(u);
12366
+ const cx = outline.reduce((sum, p) => sum + p.dot(u), 0) / outline.length;
12367
+ const cy = outline.reduce((sum, p) => sum + p.dot(w), 0) / outline.length;
12368
+ const poly = outline.map((p) => {
12369
+ const x = p.dot(u);
12370
+ const y = p.dot(w);
12371
+ const d = Math.hypot(x - cx, y - cy) || 1;
12372
+ const k = Math.max(0, d - inset) / d;
12373
+ return [cx + (x - cx) * k, cy + (y - cy) * k];
12374
+ });
12375
+ const plane = outline.reduce((sum, p) => sum + p.dot(normal), 0) / outline.length;
12376
+ const inside = (x, y) => {
12377
+ let hit = false;
12378
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
12379
+ const [xi, yi] = poly[i];
12380
+ const [xj, yj] = poly[j];
12381
+ if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) hit = !hit;
12382
+ }
12383
+ return hit;
12384
+ };
12385
+ const v = new THREE.Vector3();
12386
+ const toWorld = mesh.matrixWorld;
12387
+ mesh.updateMatrixWorld(true);
12388
+ const xs = poly.map((p) => p[0]);
12389
+ const ys = poly.map((p) => p[1]);
12390
+ const [x0, x1, y0, y1] = [Math.min(...xs) - COWL_CUT_MARGIN, Math.max(...xs) + COWL_CUT_MARGIN, Math.min(...ys) - COWL_CUT_MARGIN, Math.max(...ys) + COWL_CUT_MARGIN];
12391
+ subdivideNear(THREE, { mesh, maxLen: COWL_CUT_SUB, near: (corners) => {
12392
+ const us = corners.map((c) => c.dot(u));
12393
+ const ws = corners.map((c) => c.dot(w));
12394
+ return Math.max(...us) >= x0 && Math.min(...us) <= x1 && Math.max(...ws) >= y0 && Math.min(...ws) <= y1;
12395
+ } });
12396
+ this.kit.cutMesh(
12397
+ mesh,
12398
+ [
12399
+ {
12400
+ sdf: (x, y, z) => {
12401
+ v.set(x, y, z).applyMatrix4(toWorld);
12402
+ return inside(v.dot(u), v.dot(w)) && plane - v.dot(normal) > behind ? -1 : 1;
12403
+ }
12404
+ }
12405
+ ],
12406
+ 42,
12407
+ {}
12408
+ );
12409
+ }
12410
+ /** The straight bars drawn round a quad pane stand off its edges: they give way to a thin dark band that lies on the glass, as on a bonded windshield. */
12411
+ paneBorder(pane, near, seat) {
12412
+ const THREE = this.THREE;
12413
+ const bars = near.filter((o) => {
12414
+ if (o.geometry.attributes.position.count > PANE_BAR_VERTICES) return false;
12415
+ const size = new THREE.Box3().setFromObject(o).getSize(new THREE.Vector3());
12416
+ return Math.max(size.x, size.y, size.z) > PANE_BAR_LENGTH;
12417
+ });
12418
+ if (!bars.length) return;
12419
+ const outline = this.paneOutline(pane, seat.normal);
12420
+ if (outline.length < 3) return;
12421
+ const centre = new THREE.Vector3();
12422
+ for (const p of outline) centre.add(p);
12423
+ centre.divideScalar(outline.length);
12424
+ const position = [];
12425
+ const toVehicle = this.I.worldInverseOf(this.c);
12426
+ const put = (p) => {
12427
+ const q = p.clone().addScaledVector(seat.normal, PANE_BORDER_LIFT).applyMatrix4(toVehicle);
12428
+ position.push(q.x, q.y, q.z);
12429
+ };
12430
+ const inward = (p) => p.clone().add(centre.clone().sub(p).normalize().multiplyScalar(PANE_BORDER));
12431
+ outline.forEach((a, i) => {
12432
+ const b = outline[(i + 1) % outline.length];
12433
+ const c = inward(b);
12434
+ const d = inward(a);
12435
+ for (const p of [a, b, c, a, c, d]) put(p);
12436
+ });
12437
+ if (!position.length) return;
12438
+ for (const o of bars) o.visible = false;
12439
+ const band = new THREE.BufferGeometry();
12440
+ band.setAttribute("position", new THREE.Float32BufferAttribute(position, 3));
12441
+ band.computeVertexNormals();
12442
+ const material = this.I.MATERIALS.dark.clone();
12443
+ material.side = THREE.DoubleSide;
12444
+ const mesh = new THREE.Mesh(band, material);
12445
+ mesh.userData.noFollow = true;
12446
+ mesh.castShadow = false;
12447
+ mesh.raycast = () => {
12448
+ };
12449
+ this.c.vehicle.add(mesh);
12450
+ }
12451
+ /** The outline of a pane, the convex hull of its vertices seen along its normal, in world space. */
12452
+ paneOutline(pane, normal) {
12453
+ const THREE = this.THREE;
12454
+ const pos = pane.geometry.attributes.position;
12455
+ const u = new THREE.Vector3(0, 1, 0).cross(normal);
12456
+ if (u.lengthSq() < 1e-4) u.set(1, 0, 0);
12457
+ u.normalize();
12458
+ const w = normal.clone().cross(u);
12459
+ const points = [];
12460
+ for (let i = 0; i < pos.count; i++) {
12461
+ const p = new THREE.Vector3().fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
12462
+ points.push({ x: p.dot(u), y: p.dot(w), p });
12463
+ }
12464
+ points.sort((a, b) => a.x - b.x || a.y - b.y);
12465
+ const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
12466
+ const half = (list) => {
12467
+ const out = [];
12468
+ for (const q of list) {
12469
+ while (out.length >= 2 && cross(out[out.length - 2], out[out.length - 1], q) <= 0) out.pop();
12470
+ out.push(q);
12471
+ }
12472
+ out.pop();
12473
+ return out;
12474
+ };
12475
+ return [...half(points), ...half([...points].reverse())].map((q) => q.p);
12476
+ }
12477
+ /** How far the shell still stands past a seated pane, over its triangles. */
12478
+ shellPast(pane, shell, ray, normal) {
12479
+ const THREE = this.THREE;
12480
+ const pos = pane.geometry.attributes.position;
12481
+ const idx = pane.geometry.index;
12482
+ const count = idx ? idx.count : pos.count;
12483
+ const tri = new THREE.Triangle();
12484
+ const step = Math.max(1, Math.floor(count / 3 / PANE_SEAT_SAMPLES)) * 3;
12485
+ let past = 0;
12486
+ for (let f = 0; f + 2 < count; f += step) {
12487
+ tri.a.fromBufferAttribute(pos, idx ? idx.getX(f) : f);
12488
+ tri.b.fromBufferAttribute(pos, idx ? idx.getX(f + 1) : f + 1);
12489
+ tri.c.fromBufferAttribute(pos, idx ? idx.getX(f + 2) : f + 2);
12490
+ const mid = tri.getMidpoint(new THREE.Vector3()).applyMatrix4(pane.matrixWorld);
12491
+ ray.set(mid.addScaledVector(normal, PANE_SEAT_REACH), normal.clone().negate());
12492
+ ray.far = PANE_SEAT_REACH;
12493
+ const hit = raycastIndexed(THREE, ray, shell)[0];
12494
+ if (hit) past = Math.max(past, PANE_SEAT_REACH - hit.distance + PANE_SEAT_LIFT);
12495
+ }
12496
+ return past;
12497
+ }
12498
+ /** A frame or a seal drawn on the pane: small, and close to its plane. */
12499
+ onPane(o, pane, seat) {
12500
+ const THREE = this.THREE;
12501
+ const box = new THREE.Box3().setFromObject(o);
12502
+ if (box.isEmpty() || !new THREE.Box3().setFromObject(pane).expandByScalar(PANE_FRAME_NEAR).containsBox(box)) return false;
12503
+ const at = box.getCenter(new THREE.Vector3());
12504
+ return Math.abs(at.clone().sub(seat.samples[0].at).dot(seat.normal)) < PANE_SEAT_REACH;
12505
+ }
12506
+ movePane(mesh, seat) {
12507
+ const THREE = this.THREE;
12508
+ const g = mesh.geometry.clone();
12509
+ const pos = g.attributes.position;
12510
+ const inverse = mesh.matrixWorld.clone().invert();
12511
+ const v = new THREE.Vector3();
12512
+ for (let i = 0; i < pos.count; i++) {
12513
+ v.fromBufferAttribute(pos, i).applyMatrix4(mesh.matrixWorld);
12514
+ let weight = 0;
12515
+ let by = 0;
12516
+ for (const s of seat.samples) {
12517
+ const w = 1 / (s.at.distanceToSquared(v) + 1e-4);
12518
+ weight += w;
12519
+ by += s.by * w;
12520
+ }
12521
+ v.addScaledVector(seat.normal, by / weight + PANE_SEAT_LIFT).applyMatrix4(inverse);
12522
+ pos.setXYZ(i, v.x, v.y, v.z);
12523
+ }
12524
+ pos.needsUpdate = true;
12525
+ g.computeBoundingBox();
12526
+ g.computeBoundingSphere();
12527
+ mesh.geometry = g;
12528
+ }
12529
+ /** A closed hood is flush with the fenders beside it: the height it was drawn with to keep clear of the hull goes, the hull has its hole. */
12530
+ seatHood() {
12531
+ const hood = this.c.hood;
12532
+ if (!hood?.lift) return;
12533
+ const THREE = this.THREE;
12534
+ const box = new THREE.Box3().setFromObject(hood.piv).getSize(new THREE.Vector3());
12535
+ for (const part of hood.piv.children) {
12536
+ part.position.y -= hood.lift + HOOD_SINK;
12537
+ part.scale.multiply(new THREE.Vector3(1 + HOOD_GAP_CLOSED / Math.max(0.3, box.x), 1, 1 + HOOD_GAP_CLOSED / Math.max(0.3, box.z)));
12538
+ }
12539
+ }
12540
+ /** A handle is at the free edge of its door, the end away from the hinge. */
12541
+ seatDoorHandles() {
12542
+ const THREE = this.THREE;
12543
+ const c = this.c;
12544
+ const inv = this.I.worldInverseOf(c);
12545
+ const bodyBox = new THREE.Box3();
12546
+ for (const o of c.deform) if (o.isMesh && !o.userData.glass && c.paints?.has(o.material)) bodyBox.union(this.I.localBox(c, o, inv));
12547
+ const outerHalf = Math.max(Math.abs(bodyBox.min.z), Math.abs(bodyBox.max.z));
12548
+ for (const p of this.model.parts) {
12549
+ if (p.role !== "door") continue;
12550
+ const z = this.zone(p);
12551
+ if (!z) continue;
12552
+ const B = z.box;
12553
+ const free = z.hinge === "rear" ? B.x1 - DOOR_HANDLE_INSET : B.x0 + DOOR_HANDLE_INSET;
12554
+ for (const o of c.vehicle.children) {
12555
+ if (!o.isMesh || o.material !== this.I.MATERIALS.chrome || o.userData.glass) continue;
12556
+ const box = this.I.localBox(c, o, inv);
12557
+ const size = box.getSize(new THREE.Vector3());
12558
+ const at = box.getCenter(new THREE.Vector3());
12559
+ const isHandle = size.x > DOOR_HANDLE_MIN && size.x < DOOR_HANDLE_MAX && size.y < DOOR_HANDLE_THIN && size.z < DOOR_HANDLE_THIN;
12560
+ 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;
12561
+ if (Math.abs(at.z) < outerHalf - DOOR_HANDLE_OUTSIDE) continue;
12562
+ const skin = this.surface("z", z.side < 0 ? -1 : 1, B, [[free, at.y]]);
12563
+ o.position.x += free - at.x;
12564
+ if (skin != null) o.position.z += skin + (z.side < 0 ? -1 : 1) * (size.z / 2) - at.z;
12565
+ }
12566
+ }
12567
+ c.root.updateMatrixWorld(true);
12568
+ }
12569
+ /** The glass of a drawn mirror sits inside its rounded housing, without corners standing past it. */
12570
+ seatMirrorGlass(glass, drawn) {
12571
+ const g = glass.geometry.clone();
12572
+ g.computeBoundingBox();
12573
+ const mid = g.boundingBox.getCenter(new this.THREE.Vector3());
12574
+ g.translate(-mid.x, -mid.y, -mid.z);
12575
+ g.scale(MIRROR_GLASS_FIT, MIRROR_GLASS_FIT, MIRROR_GLASS_FIT);
12576
+ g.translate(mid.x, mid.y, mid.z);
12577
+ glass.geometry = g;
12578
+ const THREE = this.THREE;
12579
+ const volumeOf = (o) => {
12580
+ const size = new THREE.Box3().setFromObject(o).getSize(new THREE.Vector3());
12581
+ return size.x * size.y * size.z;
12582
+ };
12583
+ const housing = drawn.filter((o) => o !== glass && o.isMesh && o.geometry).sort((a, b) => volumeOf(b) - volumeOf(a))[0];
12584
+ if (housing) {
12585
+ this.c.root.updateMatrixWorld(true);
12586
+ const inv = this.I.worldInverseOf(this.c);
12587
+ glass.updateMatrixWorld(true);
12588
+ const gb = this.I.localBox(this.c, glass, inv);
12589
+ const hb = this.I.localBox(this.c, housing, inv);
12590
+ const side2 = hb.getCenter(new THREE.Vector3()).x > gb.getCenter(new THREE.Vector3()).x ? -1 : 1;
12591
+ const face = side2 < 0 ? hb.min.x + MIRROR_FACE_DEPTH : hb.max.x - MIRROR_FACE_DEPTH;
12592
+ const shape = housing.geometry.clone();
12593
+ const pos = shape.attributes.position;
12594
+ const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, housing.matrixWorld);
12595
+ const back = toVehicle.clone().invert();
12596
+ const v = new THREE.Vector3();
12597
+ for (let i = 0; i < pos.count; i++) {
12598
+ v.fromBufferAttribute(pos, i).applyMatrix4(toVehicle);
12599
+ if ((v.x - face) * side2 <= 0) continue;
12600
+ v.x = face;
12601
+ v.applyMatrix4(back);
12602
+ pos.setXYZ(i, v.x, v.y, v.z);
12603
+ }
12604
+ shape.computeVertexNormals();
12605
+ housing.geometry = shape;
12606
+ const outer = side2 < 0 ? gb.min.x : gb.max.x;
12607
+ glass.position.x += face + side2 * MIRROR_GLASS_PROUD - outer;
12608
+ }
12609
+ glass.material = this.I.makeMaterial(MIRROR_GLASS_COLOR, { metalness: 0.95, roughness: 0.12 });
12610
+ }
12611
+ /** The mirror the model already draws around where the part says, if it draws one. */
12612
+ drawnMirror(p) {
12613
+ const THREE = this.THREE;
12614
+ const [cx, cy, cz] = p.box.center;
12615
+ this.c.root.updateMatrixWorld(true);
12616
+ const inv = this.I.worldInverseOf(this.c);
12617
+ const small = this.c.vehicle.children.flatMap((o) => {
12618
+ if (o.userData.glass || o.userData.decal || !(o.isMesh || o.isGroup) || !o.visible || this.c.deform?.includes(o)) return [];
12619
+ const box = this.I.localBox(this.c, o, inv);
12620
+ if (box.isEmpty()) return [];
12621
+ const size = box.getSize(new THREE.Vector3());
12622
+ return Math.max(size.x, size.y, size.z) < DRAWN_MIRROR_SIZE ? [{ o, at: box.getCenter(new THREE.Vector3()) }] : [];
12623
+ });
12624
+ 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);
12625
+ return small.filter(({ at }) => housing.some((h) => h.at.distanceTo(at) < DRAWN_MIRROR_MOUNT)).map(({ o }) => o);
12626
+ }
11592
12627
  makeMirror(p) {
12628
+ const drawn = this.drawnMirror(p);
12629
+ if (drawn.length) {
12630
+ for (const o of drawn) {
12631
+ if (!o.isMesh || o.material !== this.I.MATERIALS.chrome) continue;
12632
+ const size = new this.THREE.Box3().setFromObject(o).getSize(new this.THREE.Vector3());
12633
+ const [thin, mid] = size.toArray().sort((a, b) => a - b);
12634
+ if (thin < MIRROR_GLASS_THIN && mid > MIRROR_GLASS_MIN) {
12635
+ this.seatMirrorGlass(o, drawn);
12636
+ o.userData.mirrorGlass = true;
12637
+ }
12638
+ }
12639
+ this.extra.set(p.id, drawn);
12640
+ return;
12641
+ }
11593
12642
  const [cx, cy, cz] = p.box.center;
11594
12643
  const s = cz < 0 ? -1 : 1;
11595
12644
  const mats = this.I.MATERIALS;
@@ -11597,14 +12646,26 @@ var VehicleVisuals = class {
11597
12646
  g.name = `obj_${p.id}`;
11598
12647
  g.position.set(cx, cy, cz);
11599
12648
  this.c.vehicle.add(g);
11600
- this.box(0.05, 0.04, 0.1, mats.dark, 0, -0.04, -s * 0.07, g);
11601
- const housing = this.box(0.1, 0.12, 0.05, mats.plastic ?? mats.dark, 0, 0, s * 0.02, g);
12649
+ this.box(0.05, 0.035, 0.05, mats.dark, 0, -0.04, -s * 0.06, g);
12650
+ const housing = this.box(0.08, 0.11, 0.125, mats.plastic ?? mats.dark, 0, 0, s * 0.0225, g);
11602
12651
  housing.userData.mirrorHousing = true;
11603
- const glass = this.box(0.095, 0.1, 6e-3, mats.chrome ?? mats.dark, 0, 0, s * 0.048, g);
12652
+ const glass = this.box(6e-3, 0.095, 0.115, mats.chrome ?? mats.dark, -0.043, 0, s * 0.0225, g);
11604
12653
  glass.userData.mirrorGlass = true;
11605
12654
  g.userData.rigid = true;
11606
12655
  this.extra.set(p.id, [g]);
11607
12656
  }
12657
+ /** A round through the mirror shatters its glass: the shards drop out of the housing, which stays on with the hole. Returns whether there was glass to break. */
12658
+ shatterMirror(id) {
12659
+ let broke = false;
12660
+ for (const o of this.extra.get(id) ?? []) {
12661
+ o.traverse((m) => {
12662
+ if (!m.userData.mirrorGlass || !m.visible) return;
12663
+ m.visible = false;
12664
+ broke = true;
12665
+ });
12666
+ }
12667
+ return broke;
12668
+ }
11608
12669
  makeBattery(p) {
11609
12670
  const mats = this.I.MATERIALS;
11610
12671
  const g = new this.THREE.Group();
@@ -11630,17 +12691,33 @@ var VehicleVisuals = class {
11630
12691
  }
11631
12692
  // ---- marks -------------------------------------------------------------------------------------------------------------------------------
11632
12693
  /** A bullet hole: a dark disc lying on the surface at `point` (`c.vehicle` coordinates). */
11633
- holeDecal(point, normal, diameter) {
12694
+ holeDecal(point, normal, diameter, part) {
11634
12695
  const THREE = this.THREE;
11635
- const m = new THREE.Mesh(new THREE.CircleGeometry(Math.max(6e-3, diameter / 2), 10), this.I.MATERIALS.dark);
12696
+ const pane = part?.role === "glass" ? this.panesOf(part).find((g) => g.visible) : void 0;
12697
+ if (part?.role === "glass" && (!pane || pane.userData.glass?.broken)) return;
12698
+ const radius = Math.max(6e-3, diameter / 2) * HOLE_DECAL_SCALE;
12699
+ const m = new THREE.Mesh(holeDiscGeometry(THREE), holeMaterial(this.kit, part?.material ?? "steel"));
12700
+ m.scale.set(radius, radius, 1);
11636
12701
  const n = new THREE.Vector3(...normal);
11637
- m.position.set(...point).addScaledVector(n, 4e-3);
11638
- m.lookAt(m.position.clone().add(n));
12702
+ m.position.set(...point).addScaledVector(n, 3e-3);
12703
+ this.c.vehicle.add(m);
12704
+ m.lookAt(this.c.vehicle.localToWorld(m.position.clone().add(n)));
11639
12705
  m.userData.decal = true;
11640
12706
  m.userData.noFollow = true;
12707
+ m.renderOrder = 3;
11641
12708
  m.raycast = () => {
11642
12709
  };
11643
- this.c.vehicle.add(m);
12710
+ 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);
12711
+ if (carrier) carrier.attach(m);
12712
+ }
12713
+ /** The door or lid a hinge group stands for. */
12714
+ /** The part a separately drawn piece stands for (a mirror on the door, a battery under the hood), so a shot at it lands on that part. */
12715
+ extraIdOf(o) {
12716
+ for (const [id, objs] of this.extra) if (objs.includes(o)) return id;
12717
+ return void 0;
12718
+ }
12719
+ plateIdOf(o) {
12720
+ return o.userData.plateOf;
11644
12721
  }
11645
12722
  spark(point, dir, count = 8) {
11646
12723
  const V3 = this.THREE.Vector3;
@@ -11680,10 +12757,8 @@ var VehicleVisuals = class {
11680
12757
  }
11681
12758
  /** Glass: cracks (laminated) or shatters (tempered); returns whether it broke. */
11682
12759
  hitGlass(p, point, dir, force) {
11683
- const z = this.zone(p);
11684
- if (!z) return false;
11685
12760
  const V3 = this.THREE.Vector3;
11686
- const list = z.glass.filter((g) => g.parent && g.visible);
12761
+ const list = this.panesOf(p).filter((g) => g.parent && g.visible);
11687
12762
  if (!list.length) return false;
11688
12763
  const pt = new V3(...point);
11689
12764
  let best = list[0];
@@ -11697,12 +12772,20 @@ var VehicleVisuals = class {
11697
12772
  best = g;
11698
12773
  }
11699
12774
  }
12775
+ if (p.hp > LAMINATED_HP && best.userData.glass) best.userData.glass.type = "laminated";
11700
12776
  this.kit.hitGlass(this.c, best, pt, new V3(...dir), force);
11701
12777
  return !!best.userData.glass?.broken;
11702
12778
  }
12779
+ panesOf(p) {
12780
+ return p.panes ?? this.zone(p)?.glass ?? [];
12781
+ }
11703
12782
  glassBreak(p) {
11704
12783
  const z = this.zone(p);
11705
- if (!z) return;
12784
+ if (!z) {
12785
+ 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);
12786
+ this.gone.add(p.id);
12787
+ return;
12788
+ }
11706
12789
  for (const g of z.glass) if (g.parent && g.visible) this.I.glassBand(this.c, z, g, 4);
11707
12790
  this.gone.add(p.id);
11708
12791
  }
@@ -11711,6 +12794,17 @@ var VehicleVisuals = class {
11711
12794
  if (!z) return;
11712
12795
  for (const w of z.wheels) if (w.parent) this.I.flattenTire(w, k);
11713
12796
  }
12797
+ /** A tyre driven flat until it tore apart: the rubber is gone and the bare rim shows. */
12798
+ tyreTornOff(p) {
12799
+ const z = this.zone(p);
12800
+ if (!z) return;
12801
+ for (const w of z.wheels) {
12802
+ if (!w.parent || w.userData.tornOff) continue;
12803
+ w.userData.tornOff = true;
12804
+ for (const m of this.I.tireMeshes(w)) m.visible = false;
12805
+ w.position.y -= (w.userData.r ?? DEFAULT_WHEEL_RADIUS) * SIDEWALL_SHARE;
12806
+ }
12807
+ }
11714
12808
  /** Burning rubber: the tyre sags and goes black as it melts. */
11715
12809
  tyreMelt(p, melt) {
11716
12810
  const z = this.zone(p);
@@ -11730,6 +12824,7 @@ var VehicleVisuals = class {
11730
12824
  smoothLater(msh) {
11731
12825
  this.smoothing.set(msh, this.I.creaseTask(msh.geometry, 42));
11732
12826
  this.tornThisFrame = true;
12827
+ this.holdArmed = true;
11733
12828
  }
11734
12829
  /** Spends at most `budgetMs` finishing the pending normal smoothing. Call once per frame. */
11735
12830
  pump(budgetMs = 2.5) {
@@ -11737,6 +12832,7 @@ var VehicleVisuals = class {
11737
12832
  this.tornThisFrame = false;
11738
12833
  return;
11739
12834
  }
12835
+ if (this.queued) return;
11740
12836
  if (!this.smoothing.size && !this.warming) return;
11741
12837
  const deadline = performance.now() + budgetMs;
11742
12838
  for (const [msh, task] of this.smoothing) {
@@ -11745,6 +12841,34 @@ var VehicleVisuals = class {
11745
12841
  }
11746
12842
  this.warmIndexes(deadline);
11747
12843
  }
12844
+ setQueued(isQueued) {
12845
+ this.queued = isQueued;
12846
+ }
12847
+ /** 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). */
12848
+ pendingUnits(frame) {
12849
+ if (this.holdArmed && frame !== void 0) {
12850
+ this.holdArmed = false;
12851
+ this.holdFrame = frame;
12852
+ }
12853
+ if (frame !== void 0 && frame === this.holdFrame) return 0;
12854
+ if (frame === void 0 && this.tornThisFrame) return 0;
12855
+ return this.smoothing.size + (this.warming ? 1 : 0);
12856
+ }
12857
+ /** One slice of the pending work, for the shared queue. */
12858
+ runStep(deadline) {
12859
+ const next = this.smoothing.entries().next();
12860
+ if (!next.done) {
12861
+ const [msh, task] = next.value;
12862
+ if (msh.geometry !== task.geo || task.step(deadline)) this.smoothing.delete(msh);
12863
+ return true;
12864
+ }
12865
+ if (!this.warming) return false;
12866
+ this.warmIndexes(deadline);
12867
+ return true;
12868
+ }
12869
+ finishAll() {
12870
+ while (this.runStep(Infinity)) ;
12871
+ }
11748
12872
  /** Measures the triangles of the heavy body meshes once, a slice per frame, so the first panel torn off finds them ready (see `triIndexOf`). */
11749
12873
  warmIndexes(deadline) {
11750
12874
  if (!this.warming) return;
@@ -11916,6 +13040,7 @@ var VehicleVisuals = class {
11916
13040
  }
11917
13041
  pieceTasks.forEach((task, mesh) => this.smoothing.set(mesh, task));
11918
13042
  this.tornThisFrame = true;
13043
+ this.holdArmed = true;
11919
13044
  dirty.forEach((msh) => {
11920
13045
  const g = msh.geometry;
11921
13046
  if (g.userData.crease || !g.index) this.smoothLater(msh);
@@ -12003,6 +13128,41 @@ var BLUNT = { steel: 0.8, aluminium: 1, glass: 1.5, plastic: 1.2, rubber: 0.3 };
12003
13128
  var SHIELDED = /* @__PURE__ */ new Set(["engine", "gearbox", "frame", "radiator", "fuel_tank", "oil_pan", "battery", "alternator", "brakes", "steering", "shocks"]);
12004
13129
  var AMBIENT = 20;
12005
13130
  var FLAME = 700;
13131
+ var STEEL_MM = { panel: 0.8, door: 1.6, hood: 0.8, lid: 0.8, roof: 0.8, fender: 0.8, pillar: 2, frame: 4, rim: 3, engine: 40, gearbox: 25, fuel_tank: 1, radiator: 1, battery: 2, brakes: 10, steering: 8, shocks: 6, oil_pan: 2, alternator: 4, hitch: 8, other: 2 };
13132
+ var ARMOUR_STEEL_MM = { IIIA: 4, III: 7 };
13133
+ var SOFT_LEAD = 0.8;
13134
+ var TYRE_FLAT_HEALTH = 0.1;
13135
+ var TYRE_BURST_JOULES = 1500;
13136
+ var TYRE_BURST_CALIBER = 12;
13137
+ var TYRE_LEAK_PER_MM2 = 26e-5;
13138
+ var FLAT_TYRE_LIFE_M = 1500;
13139
+ var FLAT_TYRE_TOWN_SPEED = 8;
13140
+ var RUN_FLAT_LIFE_M = 8e4;
13141
+ var RUN_FLAT_SPEED = 22;
13142
+ var TYRE_RUNFLAT_HEALTH = 0.6;
13143
+ var CRUMPLE_BASE_M = 0.04;
13144
+ var CRUMPLE_PER_FORCE_M = 0.15;
13145
+ var CRUMPLE_MIN_FORCE = 0.3;
13146
+ var CRUMPLE_MAX_FORCE = 1.4;
13147
+ var RIM_HEALTH = 0.05;
13148
+ var RIM_GRIND_PER_M = 4e-4;
13149
+ var TYRE_FLAT_SAG = 0.24;
13150
+ var TYRE_FLAT_AIR = 0.2;
13151
+ var BRAKE_FLUID_NEEDED = 0.6;
13152
+ var BRAKE_NO_FLUID = 0.15;
13153
+ var ENGINE_WORKING_C = 90;
13154
+ var ENGINE_SETTLE_S = 30;
13155
+ var ENGINE_HEATING = 0.5;
13156
+ var ENGINE_STEAM_C = 120;
13157
+ var ENGINE_SEIZE_C = 150;
13158
+ var COOLANT_NEEDED = 0.4;
13159
+ var OIL_NEEDED = 0.3;
13160
+ var OIL_WEAR = 4e-3;
13161
+ var ENGINE_SEIZE_HP = 0.2;
13162
+ var EXPANDED_HOLE = 1.6;
13163
+ var MIN_SPENT = 0.08;
13164
+ var WELDED = /* @__PURE__ */ new Set(["roof", "pillar", "frame", "panel"]);
13165
+ var CRUMPLES = /* @__PURE__ */ new Set(["door", "hood", "lid", "fender", "bumper"]);
12006
13166
  var FRACTURE = { glass: "shatter", plastic: "snap", rubber: "tear", steel: "none", aluminium: "none" };
12007
13167
  var add3 = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
12008
13168
  var r3 = (v) => [Math.round(v[0] * 1e5) / 1e5, Math.round(v[1] * 1e5) / 1e5, Math.round(v[2] * 1e5) / 1e5];
@@ -12014,6 +13174,8 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12014
13174
  let c = vehicleCtx;
12015
13175
  const id = options.id ?? c.key;
12016
13176
  const headless = !!options.headless;
13177
+ const armourId = armourLevelOf(c.meta, options.armour);
13178
+ const armourLevel = armourId ? { ...ARMOUR_LEVELS[armourId], ...options.armourLevels?.[armourId] } : null;
12017
13179
  const seedBase = strHash(`${c.key}|${id}|${options.seed ?? 0}`);
12018
13180
  let rand = rng(seedBase);
12019
13181
  const ignitionRules = resolveIgnitionRules(options.ignition);
@@ -12022,6 +13184,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12022
13184
  let clock = 0;
12023
13185
  let model;
12024
13186
  let vis = null;
13187
+ let releaseCrease;
12025
13188
  const ps = /* @__PURE__ */ new Map();
12026
13189
  let js = [];
12027
13190
  let leaks = /* @__PURE__ */ new Map();
@@ -12070,7 +13233,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12070
13233
  return d.ignitionC ? 1 : 0;
12071
13234
  };
12072
13235
  const setup = () => {
12073
- model = buildVehicleModel(kit, c, { hitch: options.hitch });
13236
+ model = buildVehicleModel(kit, c, { hitch: options.hitch, armour: armourLevel });
12074
13237
  ps.clear();
12075
13238
  for (const d of model.parts) {
12076
13239
  const kg = burnKg(d);
@@ -12090,6 +13253,8 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12090
13253
  lost: false,
12091
13254
  status: "intact",
12092
13255
  flat: false,
13256
+ air: 1,
13257
+ airLoss: 0,
12093
13258
  fireLoss: 0
12094
13259
  });
12095
13260
  }
@@ -12106,7 +13271,9 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12106
13271
  rand = rng(seedBase);
12107
13272
  igniteRand = rng(strHash(`${c.key}|${id}|ignition|${options.seed ?? 0}`));
12108
13273
  steerSign = rand() < 0.5 ? -1 : 1;
13274
+ releaseCrease?.();
12109
13275
  vis = headless ? null : new VehicleVisuals(kit, c, model);
13276
+ releaseCrease = vis && options.creaseQueue ? options.creaseQueue.track(vis) : void 0;
12110
13277
  recomputeGroups();
12111
13278
  };
12112
13279
  const recomputeGroups = () => {
@@ -12188,7 +13355,8 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12188
13355
  vis.breakLight(d, true);
12189
13356
  break;
12190
13357
  case "tyre":
12191
- vis.tyreFlat(d, 0.34);
13358
+ if (status === "broken") vis.tyreTornOff(d);
13359
+ else vis.tyreFlat(d, 0.34);
12192
13360
  break;
12193
13361
  case "mirror":
12194
13362
  case "bumper":
@@ -12218,7 +13386,8 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12218
13386
  const damage = (p, amount, dtype, source, out, cause) => {
12219
13387
  if (p.lost || !(amount > 0)) return;
12220
13388
  const before = p.hp;
12221
- p.hp = Math.max(0, p.hp - amount);
13389
+ const crumples = dtype === "blunt" && CRUMPLES.has(p.def.role) && (p.def.material === "steel" || p.def.material === "aluminium");
13390
+ p.hp = Math.max(WELDED.has(p.def.role) || crumples ? 1 : 0, p.hp - amount);
12222
13391
  out.push(ev({ type: "damage", part: p.def.id, amount: r2(before - p.hp), hp: r2(p.hp), damageType: dtype, ...source ? { source } : {} }));
12223
13392
  wear(p, amount, out);
12224
13393
  syncZone(p);
@@ -12261,11 +13430,22 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12261
13430
  const p = v.clone().applyMatrix4(M);
12262
13431
  return [p.x, p.y, p.z];
12263
13432
  };
12264
- const armour = () => c.meta.armored || c.meta.bullet ? 30 : 1;
13433
+ const armour = () => armourLevel?.steel ?? 1;
13434
+ const steelMm = (d) => {
13435
+ const base = STEEL_MM[d.role] ?? STEEL_MM.panel;
13436
+ return d.material === "steel" || d.material === "aluminium" ? base * (armourLevel ? ARMOUR_STEEL_MM[armourId === "III" ? "III" : "IIIA"] / STEEL_MM.panel : 1) : base;
13437
+ };
13438
+ const goesThrough = (h, d, p, energy, res) => {
13439
+ const piercing = h.piercing ?? 1;
13440
+ const metal = d.material === "steel" || d.material === "aluminium";
13441
+ const mm = h.penetration?.steel;
13442
+ if (metal && mm != null) return mm >= steelMm(d) * (p.dents.length > 3 ? 0.9 : 1);
13443
+ return energy * (metal || d.material === "glass" ? piercing : 1) >= res;
13444
+ };
12265
13445
  const resist = (d, p) => {
12266
13446
  let base = RESIST[d.material] ?? 100;
12267
13447
  if (d.material === "glass" && d.hp > 1e3) base = 120;
12268
- if (d.material === "glass" && c.meta.bullet) base *= 30;
13448
+ if (d.material === "glass" && armourLevel) base = armourLevel.glassStops;
12269
13449
  const k = ROLE_RESIST[d.role] ?? 1;
12270
13450
  const arm = d.material === "steel" || d.material === "aluminium" ? armour() : 1;
12271
13451
  return base * k * arm * (p.dents.length > 3 ? 0.9 : 1);
@@ -12288,7 +13468,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12288
13468
  if (vis) {
12289
13469
  const pos = new V3(d.box.center[0], Math.max(0.2, (c.meta.clearance ?? 0.3) * 0.8), d.box.center[2]);
12290
13470
  if (fluid === "coolant") kit.addEmitter(c, new V3(d.box.center[0], d.box.center[1] + 0.2, d.box.center[2]), "white", { rate: 5, s: 0.8 });
12291
- else if ((c.meta.kind ?? "car") !== "boat") l.pud = I.puddle(c, fluidKind(fluid), pos, fluid === "oil" ? 0.8 : 1.1);
13471
+ if ((c.meta.kind ?? "car") !== "boat") l.pud = I.puddle(c, fluidKind(fluid), pos, fluid === "oil" ? 0.8 : fluid === "brake_fluid" ? 0.4 : 1.1);
12292
13472
  }
12293
13473
  }
12294
13474
  const out3 = dirToWorld(new V3(dirLocal.x * 0.5, dirLocal.y * 0.5 - 0.6, dirLocal.z * 0.5));
@@ -12325,7 +13505,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12325
13505
  exploded = true;
12326
13506
  const d = tank.def;
12327
13507
  const spec = d.explodes ?? { energy: 8e5, radius: 5, atC: 320 };
12328
- out.push(ev({ type: "explode", part: d.id, point: toWorld(d.box.center), energy: spec.energy, radius: spec.radius }));
13508
+ out.push(ev({ type: "explode", part: d.id, point: toWorld(d.box.center), energy: spec.energy, radius: spec.radius, ..."effect" in spec && spec.effect !== void 0 ? { effect: spec.effect } : {} }));
12329
13509
  tank.liters = 0;
12330
13510
  leaks.delete(d.id);
12331
13511
  if (vis) kit.explode(c, { delay: 0.6 });
@@ -12417,15 +13597,15 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12417
13597
  sync();
12418
13598
  const M = toFrameMatrix();
12419
13599
  const v = toLocal(h.point);
12420
- const p = locate(v, 0.2);
13600
+ const p = (h.part ? ps.get(h.part) : void 0) ?? locate(v, 0.2);
12421
13601
  if (!p) return;
12422
13602
  const d = p.def;
12423
13603
  const E = h.energy ?? 0;
12424
13604
  const cal = h.caliber ?? 9;
12425
13605
  const dir = dirToLocal(h.direction ?? [0, 0, -1]).normalize();
12426
- const n = normalOf(d, v);
13606
+ const n = h.normal ? dirToLocal(h.normal).normalize() : normalOf(d, v);
12427
13607
  const res = resist(d, p);
12428
- const through = E >= res;
13608
+ const through = goesThrough(h, d, p, E, res);
12429
13609
  const cosInc = Math.abs(dir.dot(n));
12430
13610
  const metal = d.material === "steel" || d.material === "aluminium";
12431
13611
  const worldPoint = toWorld(v);
@@ -12436,13 +13616,21 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12436
13616
  vis?.spark([v.x, v.y, v.z], [refl.x, refl.y, refl.z], 6);
12437
13617
  return;
12438
13618
  }
12439
- const holeDiam = Math.max(6e-3, cal / 1e3 * (d.material === "glass" ? 1.4 : 1));
13619
+ const opens = (h.piercing ?? 1) < SOFT_LEAD ? EXPANDED_HOLE : 1;
13620
+ const holeDiam = Math.max(6e-3, cal / 1e3 * (d.material === "glass" ? 1.4 : 1) * opens);
13621
+ const energyLeft = () => {
13622
+ const metal2 = d.material === "steel" || d.material === "aluminium";
13623
+ const mm = h.penetration?.steel;
13624
+ const spent = metal2 && mm ? clamp(steelMm(d) / mm, MIN_SPENT, 1) : clamp(res / Math.max(E, 1), MIN_SPENT, 1);
13625
+ return E * (1 - spent);
13626
+ };
12440
13627
  const push = (hole) => {
12441
13628
  if (!hole) return;
12442
13629
  const hp = { point: r3(fromVehicle(v, M)), normal: r3(fromVehicle(n, new THREE.Matrix4().extractRotation(M))), diameter: r2(holeDiam), through };
12443
13630
  p.holes.push(hp);
12444
- 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])))))) } : {} }));
12445
- vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam);
13631
+ out.push(ev({ type: "hole", part: d.id, hole: hp, worldPoint, ...through ? { energyLeft: r2(energyLeft()) } : {}, ...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])))))) } : {} }));
13632
+ vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam, d);
13633
+ if (d.role === "mirror") vis?.shatterMirror(d.id);
12446
13634
  startLeak(p, v, cal, out, dir);
12447
13635
  };
12448
13636
  switch (d.material) {
@@ -12455,7 +13643,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12455
13643
  const crack = { point: r3(fromVehicle(v, M)), normal: r3(fromVehicle(n, new THREE.Matrix4().extractRotation(M))), length: r2(0.15 + force * 0.35), rays: 6 + Math.round(force * 6) };
12456
13644
  p.cracks.push(crack);
12457
13645
  out.push(ev({ type: "crack", part: d.id, crack, worldPoint }));
12458
- damage(p, E * 0.5, "ballistic", h.source, out, worldPoint);
13646
+ damage(p, E * (armourLevel && !through ? armourLevel.glassTakes : 0.5), "ballistic", h.source, out, worldPoint);
12459
13647
  } else damage(p, laminated ? E * 0.5 : p.hp, "ballistic", h.source, out, worldPoint);
12460
13648
  break;
12461
13649
  }
@@ -12463,8 +13651,8 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12463
13651
  push(through);
12464
13652
  damage(p, E * 0.4, "ballistic", h.source, out, worldPoint);
12465
13653
  if (through && !p.lost && !p.flat) {
12466
- p.flat = true;
12467
- vis?.tyreFlat(d, 0.24);
13654
+ if (E >= TYRE_BURST_JOULES || cal >= TYRE_BURST_CALIBER) deflate(p, 1);
13655
+ else p.airLoss += TYRE_LEAK_PER_MM2 * Math.PI * (cal / 2) ** 2;
12468
13656
  }
12469
13657
  break;
12470
13658
  }
@@ -12580,7 +13768,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12580
13768
  const electric = c.spec.fuel === "electric";
12581
13769
  const tank = tankPart();
12582
13770
  const fuel = tank ? tank.liters ?? 0 : electric ? 1 : 1;
12583
- const engineOk = sound("engine") && !exploded && fuel > 0.05;
13771
+ const engineOk = sound("engine") && !exploded && !seized && fuel > 0.05;
12584
13772
  const powered = sound("battery") && sound("alternator");
12585
13773
  const gearOk = sound("gearbox");
12586
13774
  const brakes = [...ps.values()].find((p) => p.def.role === "brakes");
@@ -12740,9 +13928,62 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12740
13928
  const state = () => stateCache ?? (stateCache = buildState());
12741
13929
  const isSimulating = () => {
12742
13930
  if (leaks.size > 0) return true;
12743
- for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT)) return true;
13931
+ for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT || p.airLoss > 0 && p.air > 0)) return true;
12744
13932
  return false;
12745
13933
  };
13934
+ let engineHeat = ENGINE_WORKING_C;
13935
+ let seized = false;
13936
+ let steaming = false;
13937
+ const surfaceRays = new THREE.Raycaster();
13938
+ surfaceRays.layers.enableAll();
13939
+ surfaceRays.camera = new THREE.PerspectiveCamera();
13940
+ let meshParts = null;
13941
+ const paneParts = () => {
13942
+ if (meshParts) return meshParts;
13943
+ meshParts = /* @__PURE__ */ new Map();
13944
+ for (const d of model.parts) {
13945
+ if (d.role !== "glass") continue;
13946
+ const z = kit.zonesOf(c).find((q) => q.id === d.zoneId);
13947
+ for (const g of z?.glass ?? d.panes ?? []) meshParts.set(g, d.id);
13948
+ }
13949
+ return meshParts;
13950
+ };
13951
+ const isDrawnSurface = (o) => {
13952
+ for (let q = o; q && q !== c.vehicle; q = q.parent) if (!q.visible || q.userData.decal || q.userData.ghost) return false;
13953
+ return true;
13954
+ };
13955
+ const partOfSurface = (o, v) => {
13956
+ const pane = paneParts().get(o);
13957
+ if (pane) return ps.get(pane) ?? null;
13958
+ for (let q = o; q && q !== c.vehicle; q = q.parent) {
13959
+ if (vis?.plateIdOf(q)) return ps.get(vis.plateIdOf(q)) ?? null;
13960
+ const piece = vis?.extraIdOf(q);
13961
+ if (piece) return ps.get(piece) ?? null;
13962
+ }
13963
+ return locate(v, 0.08, (p) => p.def.role === "glass");
13964
+ };
13965
+ const surfaceMeshes = [];
13966
+ const collectSurfaces = (o) => {
13967
+ if (!o.visible || o.userData.decal || o.userData.ghost || o.userData.noSurfaceRay) return;
13968
+ if (o.isMesh) surfaceMeshes.push(o);
13969
+ for (const child of o.children) collectSurfaces(child);
13970
+ };
13971
+ const surfaceRay = (ray) => {
13972
+ sync();
13973
+ surfaceRays.set(new V3(ray.origin[0], ray.origin[1], ray.origin[2]), new V3(ray.direction[0], ray.direction[1], ray.direction[2]));
13974
+ surfaceRays.far = ray.maxDistance;
13975
+ surfaceMeshes.length = 0;
13976
+ for (const child of c.vehicle.children) collectSurfaces(child);
13977
+ for (const h of surfaceRays.intersectObjects(surfaceMeshes, false)) {
13978
+ if (!h.face || !isDrawnSurface(h.object)) continue;
13979
+ const p = partOfSurface(h.object, c.vehicle.worldToLocal(h.point.clone()));
13980
+ if (!p || p.lost || ray.ignored?.(p.def.id)) continue;
13981
+ const n = h.face.normal.clone().transformDirection(h.object.matrixWorld);
13982
+ if (n.dot(surfaceRays.ray.direction) > 0) n.negate();
13983
+ 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 };
13984
+ }
13985
+ return null;
13986
+ };
12746
13987
  const hit = (h) => {
12747
13988
  stateCache = void 0;
12748
13989
  const out = [];
@@ -12767,7 +14008,29 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12767
14008
  applyEffects();
12768
14009
  return out;
12769
14010
  };
14011
+ const runFlat = armourId != null;
14012
+ const tyreHealth = (p) => {
14013
+ if (p.lost) return RIM_HEALTH;
14014
+ if (p.flat) return runFlat ? TYRE_RUNFLAT_HEALTH : TYRE_FLAT_HEALTH;
14015
+ return Math.min(p.hp / p.def.hp, 0.2 + 0.8 * p.air);
14016
+ };
14017
+ const tyreOf = (rim) => {
14018
+ let best;
14019
+ let bestD = Infinity;
14020
+ for (const p of ps.values()) {
14021
+ if (p.def.role !== "tyre") continue;
14022
+ const d = Math.hypot(p.def.box.center[0] - rim.def.box.center[0], p.def.box.center[2] - rim.def.box.center[2]);
14023
+ if (d < bestD) [best, bestD] = [p, d];
14024
+ }
14025
+ return best;
14026
+ };
14027
+ const deflate = (p, share) => {
14028
+ p.air = Math.max(0, p.air - share);
14029
+ vis?.tyreFlat(p.def, TYRE_FLAT_SAG * (1 - p.air));
14030
+ if (p.air <= TYRE_FLAT_AIR) p.flat = true;
14031
+ };
12770
14032
  const update = (dt) => {
14033
+ for (const p of ps.values()) if (p.airLoss > 0 && p.air > 0 && !p.lost) deflate(p, p.airLoss * dt);
12771
14034
  clock += dt;
12772
14035
  const out = queue;
12773
14036
  queue = [];
@@ -12865,9 +14128,14 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12865
14128
  },
12866
14129
  describe,
12867
14130
  hit,
14131
+ surfaceRay,
12868
14132
  update,
12869
14133
  state,
12870
14134
  reset,
14135
+ dispose: () => {
14136
+ releaseCrease?.();
14137
+ releaseCrease = void 0;
14138
+ },
12871
14139
  disassemble,
12872
14140
  extinguish,
12873
14141
  serialize: () => ({
@@ -12878,6 +14146,70 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12878
14146
  partName: (pid) => ps.get(pid)?.def.name ?? pid,
12879
14147
  status: computeStatus,
12880
14148
  animations: () => createVehicleClips(model, vis, kit, c),
14149
+ crumple: (point, direction, depth) => {
14150
+ if (!vis || depth <= 0) return;
14151
+ sync();
14152
+ const force = clamp((depth - CRUMPLE_BASE_M) / CRUMPLE_PER_FORCE_M, CRUMPLE_MIN_FORCE, CRUMPLE_MAX_FORCE);
14153
+ kit.impact(c, toLocal(point), dirToLocal(direction).normalize(), force, { maxDepth: depth });
14154
+ stateCache = void 0;
14155
+ },
14156
+ rollFlat: (meters, speed) => {
14157
+ for (const p of ps.values()) {
14158
+ if (p.def.role !== "tyre" || !p.flat || p.lost) continue;
14159
+ const life = runFlat ? RUN_FLAT_LIFE_M * Math.min(1, RUN_FLAT_SPEED / Math.max(speed, 1)) : FLAT_TYRE_LIFE_M * clamp(FLAT_TYRE_TOWN_SPEED / Math.max(speed, 1), 0.2, 1.5);
14160
+ p.hp = Math.max(0, p.hp - p.def.hp * meters / life);
14161
+ if (p.hp > 0) continue;
14162
+ const at = toWorld(p.def.box.center);
14163
+ lose(p, "broken", queue, at);
14164
+ queue.push(ev({ type: "debris", part: p.def.id, kind: "fibers", point: at, normal: [0, 1, 0], count: 12, speed: Math.max(2, speed * 0.5) }));
14165
+ }
14166
+ let grinding = 0;
14167
+ for (const rim of ps.values()) {
14168
+ if (rim.def.role !== "rim" || rim.lost || !tyreOf(rim)?.lost) continue;
14169
+ grinding += 1;
14170
+ rim.hp = Math.max(1, rim.hp - rim.def.hp * RIM_GRIND_PER_M * meters);
14171
+ const [x, y, z] = rim.def.box.center;
14172
+ queue.push(ev({ type: "debris", part: rim.def.id, kind: "sparks", point: toWorld([x, y - rim.def.box.size[1] / 2, z]), normal: [0, 1, 0], count: Math.min(30, Math.ceil(4 + speed)), speed: Math.max(2, speed * 0.4) }));
14173
+ }
14174
+ stateCache = void 0;
14175
+ return grinding;
14176
+ },
14177
+ runEngine: (seconds, load) => {
14178
+ const engine = [...ps.values()].find((p) => p.def.role === "engine");
14179
+ if (!engine || engine.lost || seized) return engineHeat;
14180
+ const share = (fluid) => {
14181
+ const list = [...ps.values()].filter((p) => p.def.contains?.fluid === fluid);
14182
+ return list.length ? Math.min(...list.map((p) => (p.liters ?? 0) / p.def.contains.liters)) : 1;
14183
+ };
14184
+ const coolant = share("coolant");
14185
+ const oil = share("oil");
14186
+ const work = 0.4 + 0.6 * clamp(load, 0, 1);
14187
+ if (coolant < COOLANT_NEEDED) engineHeat += (COOLANT_NEEDED - coolant) / COOLANT_NEEDED * ENGINE_HEATING * work * seconds;
14188
+ else engineHeat += (ENGINE_WORKING_C - engineHeat) * Math.min(1, seconds / ENGINE_SETTLE_S);
14189
+ if (oil < OIL_NEEDED) engine.hp = Math.max(0, engine.hp - engine.def.hp * OIL_WEAR * work * seconds * ((OIL_NEEDED - oil) / OIL_NEEDED));
14190
+ if (engineHeat >= ENGINE_STEAM_C && vis && !steaming) {
14191
+ steaming = true;
14192
+ kit.addEmitter(c, new V3(engine.def.box.center[0], engine.def.box.center[1] + 0.4, 0), "white", { rate: 8, s: 1 });
14193
+ }
14194
+ if (engineHeat >= ENGINE_SEIZE_C || engine.hp <= engine.def.hp * ENGINE_SEIZE_HP) {
14195
+ seized = true;
14196
+ queue.push(ev({ type: "damage", part: engine.def.id, amount: 0, hp: r2(engine.hp), damageType: "heat" }));
14197
+ }
14198
+ stateCache = void 0;
14199
+ return engineHeat;
14200
+ },
14201
+ driveReport: () => {
14202
+ const health = (role) => {
14203
+ const list = [...ps.values()].filter((p) => p.def.role === role);
14204
+ if (!list.length) return 1;
14205
+ return Math.min(...list.map((p) => p.lost || !attached(p.def.id) ? 0 : p.hp / p.def.hp));
14206
+ };
14207
+ const tyres = [...ps.values()].filter((p) => p.def.role === "tyre").map((p) => ({ x: p.def.box.center[0], z: p.def.box.center[2], health: tyreHealth(p) }));
14208
+ const status = computeStatus();
14209
+ const brakeSystem = [...ps.values()].find((p) => p.def.role === "brakes" && p.def.contains);
14210
+ const fluidLeft = brakeSystem ? clamp((brakeSystem.liters ?? 0) / brakeSystem.def.contains.liters / BRAKE_FLUID_NEEDED, 0, 1) : 1;
14211
+ return { tyres, brakes: Math.min(1, 1 / status.brakeFactor) * (BRAKE_NO_FLUID + (1 - BRAKE_NO_FLUID) * fluidLeft), engine: status.canStart ? health("engine") : 0, suspension: health("shocks") };
14212
+ },
12881
14213
  partWorldCenter: (pid) => {
12882
14214
  sync();
12883
14215
  const d = ps.get(pid)?.def;
@@ -12906,6 +14238,90 @@ function buildVehicleItem(kit, key, options = {}) {
12906
14238
  return { root, vehicle };
12907
14239
  }
12908
14240
 
14241
+ // src/destructible/crease-queue.ts
14242
+ import { DEFERRED_QUEUE_ID } from "@adatechnology/game-contracts";
14243
+ function createCreaseQueue() {
14244
+ const tracked = [];
14245
+ let cursor = 0;
14246
+ let lastFrame;
14247
+ let attached = false;
14248
+ const deferred = {
14249
+ id: DEFERRED_QUEUE_ID.VEHICLE_CREASE,
14250
+ pending(frame) {
14251
+ lastFrame = frame;
14252
+ let units = 0;
14253
+ for (const work of tracked) units += work.pendingUnits(frame);
14254
+ return units;
14255
+ },
14256
+ run(deadline, clock) {
14257
+ let ran = 0;
14258
+ let skipped = 0;
14259
+ while (tracked.length > 0 && skipped < tracked.length) {
14260
+ if (ran > 0 && clock() >= deadline) break;
14261
+ const work = tracked[cursor % tracked.length];
14262
+ cursor = (cursor + 1) % tracked.length;
14263
+ if (work.pendingUnits(lastFrame) > 0 && work.runStep(deadline)) {
14264
+ ran++;
14265
+ skipped = 0;
14266
+ } else skipped++;
14267
+ }
14268
+ return ran;
14269
+ },
14270
+ flush() {
14271
+ for (const work of tracked) work.finishAll();
14272
+ },
14273
+ attach() {
14274
+ attached = true;
14275
+ for (const work of tracked) work.setQueued(true);
14276
+ },
14277
+ detach() {
14278
+ attached = false;
14279
+ for (const work of tracked) work.setQueued(false);
14280
+ }
14281
+ };
14282
+ return {
14283
+ deferredQueue: () => deferred,
14284
+ track(work) {
14285
+ tracked.push(work);
14286
+ work.setQueued(attached);
14287
+ return () => {
14288
+ const at = tracked.indexOf(work);
14289
+ if (at < 0) return;
14290
+ tracked.splice(at, 1);
14291
+ work.setQueued(false);
14292
+ };
14293
+ }
14294
+ };
14295
+ }
14296
+
14297
+ // src/destructible/crash.ts
14298
+ var CRASH_RULES = { energyToDamage: 0.3, hitSpacing: 0.6, minSpeed: 1.5, rows: [0, 0.3], crushPerMs: 0.035, maxCrush: 0.6 };
14299
+ function crashDeltaV(speed, massKg, otherMassKg) {
14300
+ if (!Number.isFinite(otherMassKg)) return speed;
14301
+ return speed * otherMassKg / (massKg + otherMassKg);
14302
+ }
14303
+ function crashVehicle(d, crash) {
14304
+ const rules = { ...CRASH_RULES, ...crash.rules };
14305
+ const deltaV = crashDeltaV(crash.speed, crash.massKg, crash.otherMassKg);
14306
+ const energy = 0.5 * crash.massKg * deltaV * deltaV;
14307
+ if (crash.speed < rules.minSpeed) return { deltaV, energy, events: [] };
14308
+ const [dx, , dz] = crash.direction;
14309
+ const length = Math.hypot(dx, dz) || 1;
14310
+ const across = [-dz / length, 0, dx / length];
14311
+ const hits = Math.max(1, Math.round(crash.contactWidth / rules.hitSpacing));
14312
+ const each = energy * rules.energyToDamage / (hits * rules.rows.length);
14313
+ const events = [];
14314
+ for (const up of rules.rows)
14315
+ for (let i = 0; i < hits; i++) {
14316
+ const offset = hits === 1 ? 0 : (i / (hits - 1) - 0.5) * crash.contactWidth;
14317
+ const point = [crash.point[0] + across[0] * offset, crash.point[1] + up, crash.point[2] + across[2] * offset];
14318
+ events.push(...d.hit({ type: "blunt", point, direction: [dx / length, 0, dz / length], energy: each, radius: rules.hitSpacing / 2, ...crash.source ? { source: crash.source } : {} }));
14319
+ }
14320
+ const crush = Math.min(rules.maxCrush, deltaV * rules.crushPerMs);
14321
+ d.crumple(crash.point, [dx / length, 0, dz / length], crush);
14322
+ return { deltaV, energy, events };
14323
+ }
14324
+
12909
14325
  // src/destructible/enclosure.ts
12910
14326
  var CABIN_LEAK = 0.02;
12911
14327
  var HOLE_RATE_PER_AREA = 3e3;
@@ -12990,16 +14406,23 @@ function cabinEnclosure(source, options = {}) {
12990
14406
  };
12991
14407
  }
12992
14408
  export {
14409
+ ARMOUR_LEVELS,
12993
14410
  CABIN_LEAK,
14411
+ CRASH_RULES,
12994
14412
  JOINT_RULES,
12995
14413
  LEGACY_STATES,
12996
14414
  LEGACY_ZONE_IDS,
12997
14415
  SEED_SALTS,
14416
+ armourLevelOf,
12998
14417
  buildVehicleItem,
12999
14418
  buildVehicleModel,
13000
14419
  cabinBoxOf,
13001
14420
  cabinEnclosure,
13002
14421
  clamp,
14422
+ crackedGlassMaterial,
14423
+ crashDeltaV,
14424
+ crashVehicle,
14425
+ createCreaseQueue,
13003
14426
  createLod,
13004
14427
  createTranslate,
13005
14428
  createVehicleDestructible,