@adatechnology/game-vehicles 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,6 +33,8 @@ var GameVehicles = (() => {
33
33
  cabinBoxOf: () => cabinBoxOf,
34
34
  cabinEnclosure: () => cabinEnclosure,
35
35
  clamp: () => clamp,
36
+ crackedGlassMaterial: () => crackedGlassMaterial,
37
+ createCreaseQueue: () => createCreaseQueue,
36
38
  createLod: () => createLod,
37
39
  createTranslate: () => createTranslate,
38
40
  createVehicleDestructible: () => createVehicleDestructible,
@@ -55,7 +57,7 @@ var GameVehicles = (() => {
55
57
  wrapThreeForLod: () => wrapThreeForLod
56
58
  });
57
59
 
58
- // ../game-shapes/dist/chunk-R4RY23RH.js
60
+ // ../game-shapes/dist/chunk-IFXQIDSA.js
59
61
  function canonicalJson(value) {
60
62
  if (value === null) return "null";
61
63
  if (typeof value === "number") return Object.is(value, -0) ? "-0" : Number.isFinite(value) ? String(value) : JSON.stringify(String(value));
@@ -1443,6 +1445,127 @@ var GameVehicles = (() => {
1443
1445
  custom: { fields: { generator: str, params: () => null }, required: ["generator"] }
1444
1446
  };
1445
1447
 
1448
+ // ../game-contracts/dist/index.js
1449
+ var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
1450
+ var DEFAULT_IGNITION_RULES = {
1451
+ fillCurve: [
1452
+ [0, 0.6],
1453
+ [0.1, 1],
1454
+ [0.35, 0.6],
1455
+ [0.65, 0.3],
1456
+ [0.9, 0.15],
1457
+ [1, 0.12]
1458
+ ],
1459
+ flameFillInfluence: 0.5,
1460
+ flameDefaults: { incendiary: 0.7, tracer: 0.2 },
1461
+ sparkTarget: { steel: 1, iron: 1, aluminium: 0.25 },
1462
+ sparkMetalDefault: 0.5,
1463
+ sparkFluid: { diesel: 0.15 },
1464
+ fireNear: { radius: 1.5, chancePerSecond: 0.5 }
1465
+ };
1466
+ var resolveIgnitionRules = (rules) => ({
1467
+ ...DEFAULT_IGNITION_RULES,
1468
+ ...rules,
1469
+ flameDefaults: { ...DEFAULT_IGNITION_RULES.flameDefaults, ...rules?.flameDefaults },
1470
+ sparkTarget: { ...DEFAULT_IGNITION_RULES.sparkTarget, ...rules?.sparkTarget },
1471
+ sparkFluid: { ...DEFAULT_IGNITION_RULES.sparkFluid, ...rules?.sparkFluid },
1472
+ fireNear: { ...DEFAULT_IGNITION_RULES.fireNear, ...rules?.fireNear }
1473
+ });
1474
+ var fillFactor = (fill, curve = DEFAULT_IGNITION_RULES.fillCurve) => {
1475
+ if (curve.length === 0) return 1;
1476
+ const f = Math.min(1, Math.max(0, fill));
1477
+ if (f <= curve[0][0]) return curve[0][1];
1478
+ for (let i = 1; i < curve.length; i++) {
1479
+ const [x1, y1] = curve[i];
1480
+ const [x0, y0] = curve[i - 1];
1481
+ if (f <= x1) return x1 === x0 ? y1 : y0 + (y1 - y0) * (f - x0) / (x1 - x0);
1482
+ }
1483
+ return curve[curve.length - 1][1];
1484
+ };
1485
+ var sparkTargetFactor = (materialId, metal, rules = DEFAULT_IGNITION_RULES) => !metal ? 0 : rules.sparkTarget[materialId] ?? rules.sparkMetalDefault;
1486
+ var ignitionChance = (hit, target, rules = DEFAULT_IGNITION_RULES) => {
1487
+ const k = fillFactor(target.fill, rules.fillCurve);
1488
+ const spark = (hit.ignition?.spark ?? 0) * target.sparkTarget * (target.fluid === void 0 ? 1 : rules.sparkFluid[target.fluid] ?? 1) * k;
1489
+ const flameBase = hit.ignition?.flame ?? Math.max(hit.incendiary ? rules.flameDefaults.incendiary : 0, hit.tracer ? rules.flameDefaults.tracer : 0);
1490
+ const flame = flameBase * (1 - rules.flameFillInfluence * (1 - k));
1491
+ return 1 - (1 - Math.min(1, Math.max(0, spark))) * (1 - Math.min(1, Math.max(0, flame)));
1492
+ };
1493
+ var DEFERRED_QUEUE_ID = {
1494
+ DESTRUCTION_EVENTS: "destruction.events",
1495
+ BREACH_PAINT: "floorplan.breach-paint",
1496
+ AUDIO_VOICES: "audio.voices",
1497
+ VEHICLE_CREASE: "vehicles.crease"
1498
+ };
1499
+ var DEFERRED_PRIORITY = {
1500
+ [DEFERRED_QUEUE_ID.DESTRUCTION_EVENTS]: { priority: 300, phase: "present" },
1501
+ [DEFERRED_QUEUE_ID.BREACH_PAINT]: { priority: 250, phase: "present" },
1502
+ [DEFERRED_QUEUE_ID.AUDIO_VOICES]: { priority: 200, phase: "idle", reserveMs: 1 },
1503
+ [DEFERRED_QUEUE_ID.VEHICLE_CREASE]: { priority: 100, phase: "idle" }
1504
+ };
1505
+ var WARNED_KEY = /* @__PURE__ */ Symbol.for("@adatechnology/game-contracts.warned");
1506
+ var warnedKeys = () => {
1507
+ const holder = globalThis;
1508
+ return holder[WARNED_KEY] ?? (holder[WARNED_KEY] = /* @__PURE__ */ new Set());
1509
+ };
1510
+ var isProduction = () => {
1511
+ try {
1512
+ return process.env.NODE_ENV === "production";
1513
+ } catch {
1514
+ return false;
1515
+ }
1516
+ };
1517
+ var isQuiet = () => globalThis.__GAME_KIT_QUIET__ === true;
1518
+ var warnOnce = (key, message) => {
1519
+ if (isQuiet() || isProduction()) return;
1520
+ const seen = warnedKeys();
1521
+ if (seen.has(key)) return;
1522
+ seen.add(key);
1523
+ console.warn(`[game-kit] ${message}`);
1524
+ };
1525
+ var KIT_ORIGIN_KEY = "kitOrigin";
1526
+ function markKitOrigin(object2, origin) {
1527
+ const node = object2;
1528
+ node.userData = node.userData ?? {};
1529
+ node.userData[KIT_ORIGIN_KEY] = { ...origin };
1530
+ return object2;
1531
+ }
1532
+ var SURFACE_IDS = ["concrete", "asphalt", "wood", "tile", "dirt", "grass", "gravel", "sand", "metal", "water", "glass", "carpet"];
1533
+ var GAIT_IDS = ["walk", "run"];
1534
+ var LANDING_IDS = ["light", "heavy"];
1535
+ var BODY_SOUND_IDS = [
1536
+ "jump",
1537
+ "cloth_rustle",
1538
+ "body_fall",
1539
+ "fall_damage",
1540
+ "player_hurt",
1541
+ "player_breath_run",
1542
+ "player_death",
1543
+ "bite",
1544
+ "grab",
1545
+ "punch_swing",
1546
+ "punch_hit_body",
1547
+ "punch_hit_face",
1548
+ "punch_hit_object",
1549
+ "kick_swing",
1550
+ "kick_hit",
1551
+ "kick_hit_object",
1552
+ "shove_two_hands",
1553
+ "knife_swing",
1554
+ "knife_stab",
1555
+ "knife_slash",
1556
+ "knife_hit_object",
1557
+ "blunt_swing",
1558
+ "blunt_hit",
1559
+ "blunt_hit_object",
1560
+ "axe_swing",
1561
+ "axe_chop",
1562
+ "axe_hit_object"
1563
+ ];
1564
+ var footstepSoundId = (surface, gait) => `footstep_${surface}_${gait}`;
1565
+ var FOOTSTEP_SOUND_IDS = SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstepSoundId(surface, gait)));
1566
+ var LAND_SOUND_IDS = SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((landing) => `land_${surface}_${landing}`));
1567
+ var BODY_FOLEY_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
1568
+
1446
1569
  // ../game-shapes-three/dist/index.js
1447
1570
  var UV_BEFORE_NORMAL = /* @__PURE__ */ new Set(["LatheGeometry", "ExtrudeGeometry"]);
1448
1571
  var INDEX_FIRST = /* @__PURE__ */ new Set(["BoxGeometry", "CylinderGeometry", "ConeGeometry", "SphereGeometry", "PlaneGeometry", "CircleGeometry", "RingGeometry", "LatheGeometry", "TorusGeometry"]);
@@ -2459,7 +2582,7 @@ var GameVehicles = (() => {
2459
2582
  }
2460
2583
  return ti.builtBlocks === total;
2461
2584
  }
2462
- function start(geo) {
2585
+ function start(geo, store = cache) {
2463
2586
  const pos = geo.attributes.position;
2464
2587
  const triangles = trianglesOf(geo);
2465
2588
  const ti = {
@@ -2472,7 +2595,7 @@ var GameVehicles = (() => {
2472
2595
  blocks: new Float64Array(blockCount(triangles) * 6),
2473
2596
  builtBlocks: 0
2474
2597
  };
2475
- cache.set(geo, ti);
2598
+ store.set(geo, ti);
2476
2599
  return ti;
2477
2600
  }
2478
2601
  function triIndexOf(geo) {
@@ -2524,6 +2647,106 @@ var GameVehicles = (() => {
2524
2647
  }
2525
2648
  return { box: box2, version: ti.positionVersion };
2526
2649
  }
2650
+ var rayCache = /* @__PURE__ */ new WeakMap();
2651
+ function rayIndexOf(geo) {
2652
+ const shared = cache.get(geo);
2653
+ if (shared && isCurrent(geo, shared) && shared.builtBlocks === blockCount(shared.triangles)) return shared;
2654
+ let ti = rayCache.get(geo);
2655
+ if (!ti || !isCurrent(geo, ti)) ti = start(geo, rayCache);
2656
+ advance(geo, ti, Infinity);
2657
+ return ti;
2658
+ }
2659
+ var BLOCK_SLACK = 1e-6;
2660
+ var scratch = /* @__PURE__ */ new WeakMap();
2661
+ function scratchOf(THREE) {
2662
+ let s = scratch.get(THREE);
2663
+ if (!s) {
2664
+ s = {
2665
+ inverse: new THREE.Matrix4(),
2666
+ local: new THREE.Ray(),
2667
+ box: new THREE.Box3(),
2668
+ a: new THREE.Vector3(),
2669
+ b: new THREE.Vector3(),
2670
+ c: new THREE.Vector3(),
2671
+ point: new THREE.Vector3(),
2672
+ world: new THREE.Vector3(),
2673
+ sphere: new THREE.Sphere()
2674
+ };
2675
+ scratch.set(THREE, s);
2676
+ }
2677
+ return s;
2678
+ }
2679
+ function indexable(THREE, mesh2) {
2680
+ const geo = mesh2.geometry;
2681
+ if (mesh2.raycast !== THREE.Mesh.prototype.raycast) return false;
2682
+ if (!mesh2.isMesh || mesh2.isSkinnedMesh || mesh2.isInstancedMesh || mesh2.isBatchedMesh || !geo || !geo.isBufferGeometry) return false;
2683
+ if (!mesh2.material || Array.isArray(mesh2.material)) return false;
2684
+ if (geo.morphAttributes && geo.morphAttributes.position && geo.morphAttributes.position.length) return false;
2685
+ return supports(geo);
2686
+ }
2687
+ function raycastMesh(THREE, raycaster, mesh2, out) {
2688
+ const geo = mesh2.geometry;
2689
+ const s = scratchOf(THREE);
2690
+ if (geo.boundingSphere === null) geo.computeBoundingSphere();
2691
+ s.sphere.copy(geo.boundingSphere).applyMatrix4(mesh2.matrixWorld);
2692
+ if (!raycaster.ray.intersectsSphere(s.sphere)) return;
2693
+ const ti = rayIndexOf(geo);
2694
+ if (!ti) return;
2695
+ s.inverse.copy(mesh2.matrixWorld).invert();
2696
+ s.local.copy(raycaster.ray).applyMatrix4(s.inverse);
2697
+ const material = mesh2.material;
2698
+ const back = material.side === THREE.BackSide;
2699
+ const cull = material.side === THREE.FrontSide;
2700
+ const P = geo.attributes.position;
2701
+ const ix = geo.index;
2702
+ const count = ix ? ix.count : P.count;
2703
+ const first = Math.max(0, geo.drawRange.start);
2704
+ const last = Math.min(count, geo.drawRange.start + geo.drawRange.count);
2705
+ const t0 = Math.ceil(first / 3);
2706
+ const t1 = Math.floor(last / 3);
2707
+ const B = ti.boxes;
2708
+ const K = ti.blocks;
2709
+ for (let blk = Math.floor(t0 / TRI_BLOCK); blk * TRI_BLOCK < t1; blk++) {
2710
+ const o = blk * 6;
2711
+ s.box.min.set(K[o] - BLOCK_SLACK, K[o + 1] - BLOCK_SLACK, K[o + 2] - BLOCK_SLACK);
2712
+ s.box.max.set(K[o + 3] + BLOCK_SLACK, K[o + 4] + BLOCK_SLACK, K[o + 5] + BLOCK_SLACK);
2713
+ if (!s.local.intersectsBox(s.box)) continue;
2714
+ const end = Math.min(t1, (blk + 1) * TRI_BLOCK);
2715
+ for (let t = Math.max(t0, blk * TRI_BLOCK); t < end; t++) {
2716
+ const j = t * 6;
2717
+ s.box.min.set(B[j] - BLOCK_SLACK, B[j + 1] - BLOCK_SLACK, B[j + 2] - BLOCK_SLACK);
2718
+ s.box.max.set(B[j + 3] + BLOCK_SLACK, B[j + 4] + BLOCK_SLACK, B[j + 5] + BLOCK_SLACK);
2719
+ if (!s.local.intersectsBox(s.box)) continue;
2720
+ const a = ix ? ix.getX(t * 3) : t * 3;
2721
+ const b = ix ? ix.getX(t * 3 + 1) : t * 3 + 1;
2722
+ const c = ix ? ix.getX(t * 3 + 2) : t * 3 + 2;
2723
+ s.a.fromBufferAttribute(P, a);
2724
+ s.b.fromBufferAttribute(P, b);
2725
+ s.c.fromBufferAttribute(P, c);
2726
+ 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);
2727
+ if (hit === null) continue;
2728
+ s.world.copy(s.point).applyMatrix4(mesh2.matrixWorld);
2729
+ const distance = raycaster.ray.origin.distanceTo(s.world);
2730
+ if (distance < raycaster.near || distance > raycaster.far) continue;
2731
+ const normal = new THREE.Vector3();
2732
+ THREE.Triangle.getNormal(s.a, s.b, s.c, normal);
2733
+ out.push({ distance, point: s.world.clone(), object: mesh2, face: { a, b, c, normal, materialIndex: 0 }, faceIndex: Math.floor(t) });
2734
+ }
2735
+ }
2736
+ }
2737
+ function collect(THREE, raycaster, object2, recursive, out) {
2738
+ if (object2.layers.test(raycaster.layers)) {
2739
+ if (indexable(THREE, object2)) raycastMesh(THREE, raycaster, object2, out);
2740
+ else object2.raycast(raycaster, out);
2741
+ }
2742
+ if (recursive) for (const child of object2.children) collect(THREE, raycaster, child, true, out);
2743
+ }
2744
+ function raycastIndexed(THREE, raycaster, objects, recursive = false) {
2745
+ const out = [];
2746
+ for (const object2 of objects) collect(THREE, raycaster, object2, recursive, out);
2747
+ out.sort((x, y) => x.distance - y.distance);
2748
+ return out;
2749
+ }
2527
2750
 
2528
2751
  // src/geometry.ts
2529
2752
  function installGeometry(kit) {
@@ -3263,58 +3486,6 @@ var GameVehicles = (() => {
3263
3486
  Object.assign(I, { roundedBoxGeometry, taper, subdivide, crease, creaseTask, refreshBounds, pathShape, tumble, loftGeometry, hullGeometry, cutGeometry, mergeGeometries, uvProject });
3264
3487
  }
3265
3488
 
3266
- // ../game-contracts/dist/index.js
3267
- var DEFAULT_IGNITION_RULES = {
3268
- fillCurve: [
3269
- [0, 0.6],
3270
- [0.1, 1],
3271
- [0.35, 0.6],
3272
- [0.65, 0.3],
3273
- [0.9, 0.15],
3274
- [1, 0.12]
3275
- ],
3276
- flameFillInfluence: 0.5,
3277
- flameDefaults: { incendiary: 0.7, tracer: 0.2 },
3278
- sparkTarget: { steel: 1, iron: 1, aluminium: 0.25 },
3279
- sparkMetalDefault: 0.5,
3280
- sparkFluid: { diesel: 0.15 },
3281
- fireNear: { radius: 1.5, chancePerSecond: 0.5 }
3282
- };
3283
- var resolveIgnitionRules = (rules) => ({
3284
- ...DEFAULT_IGNITION_RULES,
3285
- ...rules,
3286
- flameDefaults: { ...DEFAULT_IGNITION_RULES.flameDefaults, ...rules?.flameDefaults },
3287
- sparkTarget: { ...DEFAULT_IGNITION_RULES.sparkTarget, ...rules?.sparkTarget },
3288
- sparkFluid: { ...DEFAULT_IGNITION_RULES.sparkFluid, ...rules?.sparkFluid },
3289
- fireNear: { ...DEFAULT_IGNITION_RULES.fireNear, ...rules?.fireNear }
3290
- });
3291
- var fillFactor = (fill, curve = DEFAULT_IGNITION_RULES.fillCurve) => {
3292
- if (curve.length === 0) return 1;
3293
- const f = Math.min(1, Math.max(0, fill));
3294
- if (f <= curve[0][0]) return curve[0][1];
3295
- for (let i = 1; i < curve.length; i++) {
3296
- const [x1, y1] = curve[i];
3297
- const [x0, y0] = curve[i - 1];
3298
- if (f <= x1) return x1 === x0 ? y1 : y0 + (y1 - y0) * (f - x0) / (x1 - x0);
3299
- }
3300
- return curve[curve.length - 1][1];
3301
- };
3302
- var sparkTargetFactor = (materialId, metal, rules = DEFAULT_IGNITION_RULES) => !metal ? 0 : rules.sparkTarget[materialId] ?? rules.sparkMetalDefault;
3303
- var ignitionChance = (hit, target, rules = DEFAULT_IGNITION_RULES) => {
3304
- const k = fillFactor(target.fill, rules.fillCurve);
3305
- const spark = (hit.ignition?.spark ?? 0) * target.sparkTarget * (target.fluid === void 0 ? 1 : rules.sparkFluid[target.fluid] ?? 1) * k;
3306
- const flameBase = hit.ignition?.flame ?? Math.max(hit.incendiary ? rules.flameDefaults.incendiary : 0, hit.tracer ? rules.flameDefaults.tracer : 0);
3307
- const flame = flameBase * (1 - rules.flameFillInfluence * (1 - k));
3308
- return 1 - (1 - Math.min(1, Math.max(0, spark))) * (1 - Math.min(1, Math.max(0, flame)));
3309
- };
3310
- var KIT_ORIGIN_KEY = "kitOrigin";
3311
- function markKitOrigin(object2, origin) {
3312
- const node = object2;
3313
- node.userData = node.userData ?? {};
3314
- node.userData[KIT_ORIGIN_KEY] = { ...origin };
3315
- return object2;
3316
- }
3317
-
3318
3489
  // src/context.ts
3319
3490
  function installContext(kit) {
3320
3491
  const THREE = kit.THREE;
@@ -3908,6 +4079,7 @@ var GameVehicles = (() => {
3908
4079
  }
3909
4080
  kit.profX = profX;
3910
4081
  const _sr = new THREE.Raycaster();
4082
+ _sr.layers.enableAll();
3911
4083
  kit.surfaceAt = function(c, org, dir, o) {
3912
4084
  o = o || {};
3913
4085
  c.root.updateMatrixWorld(true);
@@ -3924,7 +4096,7 @@ var GameVehicles = (() => {
3924
4096
  _sr.set(p0.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
3925
4097
  _sr.near = 0;
3926
4098
  _sr.far = o.max || 20;
3927
- const h = _sr.intersectObjects(cands, false)[0];
4099
+ const h = raycastIndexed(THREE, _sr, cands)[0];
3928
4100
  if (!h) return null;
3929
4101
  const n = h.face ? h.face.normal.clone().transformDirection(h.object.matrixWorld).transformDirection(inv) : d.clone().negate();
3930
4102
  if (n.dot(d) > 0) n.negate();
@@ -3946,6 +4118,7 @@ var GameVehicles = (() => {
3946
4118
  const V3 = THREE.Vector3;
3947
4119
  const I = kit._;
3948
4120
  const { MATERIALS, crease, cutGeometry, pathShape, subdivide, tumble, uvProject } = I;
4121
+ I.paneSeals = /* @__PURE__ */ new WeakMap();
3949
4122
  kit.addGlass = function(c, geo, label, type, o) {
3950
4123
  o = o || {};
3951
4124
  if (!geo.attributes.uv || o.reuv) o.aspect = uvProject(geo, o.uvAxes);
@@ -4012,7 +4185,7 @@ var GameVehicles = (() => {
4012
4185
  cy /= poly.length;
4013
4186
  const base = I.resolveLabel(label ? label(i, polys.length) : { key: "window" });
4014
4187
  const lab = { key: base.key, label: kit.t("parts.glass_side", { label: base.label, s }) };
4015
- kit.addGlass(c, geo, lab, o.type || "tempered", {
4188
+ const pane = kit.addGlass(c, geo, lab, o.type || "tempered", {
4016
4189
  mat: o.mat || (o.see === false ? MATERIALS.glassDark : MATERIALS.glass),
4017
4190
  pos: new V3(cx, cy, s * I.halfAt(Wg, T, cy)),
4018
4191
  outline: shp.getSpacedPoints(56),
@@ -4028,6 +4201,7 @@ var GameVehicles = (() => {
4028
4201
  pts.pop();
4029
4202
  const seal = c.tube(pts, 0.011, MATERIALS.trim, { closed: true, seg: 72, rs: 5 });
4030
4203
  seal.castShadow = false;
4204
+ I.paneSeals.set(pane, seal);
4031
4205
  });
4032
4206
  });
4033
4207
  };
@@ -5737,6 +5911,44 @@ var GameVehicles = (() => {
5737
5911
  Object.assign(I, {});
5738
5912
  }
5739
5913
 
5914
+ // src/routed-particles.ts
5915
+ var ROUTED_PARTICLE_TYPES = { smoke: "particle_puff", dust: "particle_puff", fire: "particle_glow" };
5916
+ var PUFF_LOOKS = {
5917
+ smokewhite: [10133927, 0.42],
5918
+ smokeblack: [1776412, 0.62],
5919
+ smokegray: [5593436, 0.5],
5920
+ smokesteam: [11975873, 0.3],
5921
+ dust: [7235160, 0.34],
5922
+ fire: [16777215, 0.9]
5923
+ };
5924
+ var LIFE = { dust: 1.6, smoke: 2.8, fire: 0.7 };
5925
+ var FIRE_COLOR = 16753968;
5926
+ var SMOKE_START_SCALE = 0.18;
5927
+ function routedParticleRequest(input) {
5928
+ const { type } = input;
5929
+ const look = PUFF_LOOKS[type + (input.variant ?? "")] ?? PUFF_LOOKS[type] ?? PUFF_LOOKS.smokewhite;
5930
+ const life = input.life ?? LIFE[type] ?? 99;
5931
+ const size = (input.size ?? 1) * (input.grow ?? 1);
5932
+ const isFire = type === "fire";
5933
+ const growth = isFire ? 1 : (SMOKE_START_SCALE + 1.5 * (1 - Math.exp(-life * 1.1)) + life * 0.12) / SMOKE_START_SCALE;
5934
+ return {
5935
+ effect: ROUTED_PARTICLE_TYPES[type],
5936
+ point: input.position,
5937
+ values: {
5938
+ vx: input.velocity[0],
5939
+ vy: input.velocity[1],
5940
+ vz: input.velocity[2],
5941
+ life,
5942
+ size: isFire ? size * 0.85 : size * SMOKE_START_SCALE,
5943
+ grow: growth,
5944
+ color: input.color ?? (isFire ? FIRE_COLOR : look[0]),
5945
+ alpha: input.alpha ?? look[1],
5946
+ gravity: isFire ? 0 : type === "dust" ? 0 : -0.08,
5947
+ drag: type === "dust" ? 2.5 : isFire ? 1.5 : 0.9
5948
+ }
5949
+ };
5950
+ }
5951
+
5740
5952
  // src/particles.ts
5741
5953
  function installParticles(kit) {
5742
5954
  const THREE = kit.THREE;
@@ -5828,7 +6040,7 @@ var GameVehicles = (() => {
5828
6040
  });
5829
6041
  }
5830
6042
  const list2 = fire ? FIRE_TEXTURES : SMOKE_TEXTURES;
5831
- const mat = new THREE.SpriteMaterial({
6043
+ const build = () => new THREE.SpriteMaterial({
5832
6044
  map: list2[Math.floor(Math.random() * list2.length)],
5833
6045
  color: new THREE.Color(col),
5834
6046
  transparent: true,
@@ -5836,6 +6048,7 @@ var GameVehicles = (() => {
5836
6048
  opacity: op,
5837
6049
  blending: fire ? THREE.AdditiveBlending : THREE.NormalBlending
5838
6050
  });
6051
+ const mat = kit.particleMaterials ? kit.particleMaterials.own(build) : build();
5839
6052
  const dis = { value: 0 };
5840
6053
  mat.userData.dis = dis;
5841
6054
  mat.onBeforeCompile = function(sh) {
@@ -5863,11 +6076,37 @@ var GameVehicles = (() => {
5863
6076
  function killParticle(c, i) {
5864
6077
  const P = c.particles[i];
5865
6078
  c.effects.remove(P.m);
5866
- if (P.own) P.m.material.dispose();
6079
+ if (P.own) {
6080
+ if (kit.particleMaterials) kit.particleMaterials.release(P.m.material);
6081
+ else P.m.material.dispose();
6082
+ }
5867
6083
  c.particles.splice(i, 1);
5868
6084
  }
6085
+ function routeParticle(c, effects, type, pos, vel, o) {
6086
+ c.effects.updateWorldMatrix(true, false);
6087
+ const at = pos.clone().applyMatrix4(c.effects.matrixWorld);
6088
+ const to = vel.clone().transformDirection(c.effects.matrixWorld).multiplyScalar(vel.length());
6089
+ const colour = o.col == null ? void 0 : new THREE.Color(o.col).getHex();
6090
+ effects.play(
6091
+ routedParticleRequest({
6092
+ type,
6093
+ position: [at.x, at.y, at.z],
6094
+ velocity: [to.x, to.y, to.z],
6095
+ ...o.variant ? { variant: o.variant } : {},
6096
+ ...colour !== void 0 ? { color: colour } : {},
6097
+ ...o.op != null ? { alpha: o.op } : {},
6098
+ ...o.s ? { size: o.s } : {},
6099
+ ...o.life ? { life: o.life } : {},
6100
+ ...o.grow ? { grow: o.grow } : {}
6101
+ })
6102
+ );
6103
+ return null;
6104
+ }
5869
6105
  kit.spawn = function(c, type, pos, vel, o) {
5870
6106
  o = o || {};
6107
+ const effects = ROUTED_PARTICLE_TYPES[type] ? kit.effects?.() : void 0;
6108
+ if (effects) return routeParticle(c, effects, type, pos, vel, o);
6109
+ 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");
5871
6110
  if (c.particles.length >= PARTICLE_LIMIT) {
5872
6111
  const idx = c.particles.findIndex(function(q) {
5873
6112
  return TRANSIENT_TYPES[q.type];
@@ -6139,6 +6378,7 @@ var GameVehicles = (() => {
6139
6378
  };
6140
6379
  kit.defineFluid("stain", { fresh: "#4a4a44", dry: "#26241f", wipeable: false });
6141
6380
  const _fr = new THREE.Raycaster();
6381
+ _fr.layers.enableAll();
6142
6382
  kit.splat = function(c, p, n, r, o) {
6143
6383
  o = o || {};
6144
6384
  n = n.clone().normalize();
@@ -6580,31 +6820,55 @@ var GameVehicles = (() => {
6580
6820
  }
6581
6821
 
6582
6822
  // src/glass-damage.ts
6823
+ function crackedGlassMaterial(THREE, texture) {
6824
+ return new THREE.MeshStandardMaterial({
6825
+ color: 16777215,
6826
+ map: texture,
6827
+ emissive: 6976118,
6828
+ emissiveMap: texture,
6829
+ transparent: true,
6830
+ roughness: 0.25,
6831
+ metalness: 0.1,
6832
+ side: THREE.DoubleSide,
6833
+ depthWrite: false
6834
+ });
6835
+ }
6583
6836
  function installGlassDamage(kit) {
6584
6837
  const THREE = kit.THREE;
6585
6838
  const V3 = THREE.Vector3;
6586
6839
  const PI = Math.PI;
6587
6840
  const I = kit._;
6588
6841
  const document2 = kit.document;
6589
- const { DOUBLE_SIDE, MATERIALS, crease, cutGeometry, mergeGeometries, subdivide, tumble } = I;
6590
- function glassUV(msh, pLocal) {
6842
+ const { MATERIALS, crease, cutGeometry, mergeGeometries, subdivide, tumble } = I;
6843
+ function glassUV(c, msh, pLocal) {
6591
6844
  const pos = msh.geometry.attributes.position;
6592
6845
  const uv = msh.geometry.attributes.uv;
6593
6846
  if (!uv) return [0.5, 0.5];
6594
- const inv = new THREE.Matrix4().copy(msh.matrix).invert();
6595
- const q = pLocal.clone().applyMatrix4(inv);
6596
- let best = 1e9;
6597
- let bi = 0;
6598
- const v = new V3();
6599
- for (let i = 0; i < pos.count; i++) {
6600
- v.fromBufferAttribute(pos, i);
6601
- const d = v.distanceToSquared(q);
6602
- if (d < best) {
6603
- best = d;
6604
- bi = i;
6605
- }
6847
+ c.root.updateMatrixWorld(true);
6848
+ const q = msh.worldToLocal(c.vehicle.localToWorld(pLocal.clone()));
6849
+ const idx = msh.geometry.index;
6850
+ const tri = new THREE.Triangle();
6851
+ const near = new V3();
6852
+ const bary = new V3();
6853
+ let best = Infinity;
6854
+ const out = [0.5, 0.5];
6855
+ const count = idx ? idx.count : pos.count;
6856
+ for (let f = 0; f + 2 < count; f += 3) {
6857
+ const ia = idx ? idx.getX(f) : f;
6858
+ const ib = idx ? idx.getX(f + 1) : f + 1;
6859
+ const ic = idx ? idx.getX(f + 2) : f + 2;
6860
+ tri.a.fromBufferAttribute(pos, ia);
6861
+ tri.b.fromBufferAttribute(pos, ib);
6862
+ tri.c.fromBufferAttribute(pos, ic);
6863
+ tri.closestPointToPoint(q, near);
6864
+ const d = near.distanceToSquared(q);
6865
+ if (d >= best) continue;
6866
+ best = d;
6867
+ tri.getBarycoord(near, bary);
6868
+ out[0] = uv.getX(ia) * bary.x + uv.getX(ib) * bary.y + uv.getX(ic) * bary.z;
6869
+ out[1] = uv.getY(ia) * bary.x + uv.getY(ib) * bary.y + uv.getY(ic) * bary.z;
6606
6870
  }
6607
- return [uv.getX(bi), uv.getY(bi)];
6871
+ return out;
6608
6872
  }
6609
6873
  function drawWeb(x, cx, cy, s, ax, rnd) {
6610
6874
  const n = 16 + Math.floor(rnd() * 8);
@@ -6681,19 +6945,9 @@ var GameVehicles = (() => {
6681
6945
  x0.fillStyle = "rgba(48,66,72,0.5)";
6682
6946
  x0.fillRect(0, 0, 256, 256);
6683
6947
  G.tex = new THREE.CanvasTexture(G.cv);
6684
- msh.material = new THREE.MeshStandardMaterial({
6685
- color: 16777215,
6686
- map: G.tex,
6687
- emissive: 6976118,
6688
- emissiveMap: G.tex,
6689
- transparent: true,
6690
- roughness: 0.25,
6691
- metalness: 0.1,
6692
- side: DOUBLE_SIDE,
6693
- depthWrite: false
6694
- });
6948
+ msh.material = crackedGlassMaterial(THREE, G.tex);
6695
6949
  }
6696
- const uv = glassUV(msh, p);
6950
+ const uv = glassUV(c, msh, p);
6697
6951
  const x = G.cv.getContext("2d");
6698
6952
  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);
6699
6953
  G.tex.needsUpdate = true;
@@ -6838,6 +7092,13 @@ var GameVehicles = (() => {
6838
7092
  }
6839
7093
  const seatGeo = [];
6840
7094
  const d = dir.clone().normalize();
7095
+ if (msh.parent && msh.parent !== c.vehicle) {
7096
+ c.root.updateMatrixWorld(true);
7097
+ const toVehicle = new THREE.Matrix4().copy(c.vehicle.matrixWorld).invert().multiply(msh.parent.matrixWorld);
7098
+ cubesAt.forEach(function(q) {
7099
+ q.applyMatrix4(toVehicle);
7100
+ });
7101
+ }
6841
7102
  cubesAt.forEach(function(p, i) {
6842
7103
  if (i % 3 === 0 && c.seatTops.length && !c.rollState && !c.flipped) {
6843
7104
  let best = null;
@@ -7040,6 +7301,7 @@ var GameVehicles = (() => {
7040
7301
  polygonOffsetUnits: -2
7041
7302
  });
7042
7303
  const _ray = new THREE.Raycaster();
7304
+ _ray.layers.enableAll();
7043
7305
  function findSurface(c, p, dir) {
7044
7306
  c.root.updateMatrixWorld(true);
7045
7307
  const Mw = c.vehicle.matrixWorld;
@@ -7121,6 +7383,7 @@ var GameVehicles = (() => {
7121
7383
  const v0 = new V3();
7122
7384
  const w = new V3();
7123
7385
  const sc = new V3();
7386
+ const local = new V3();
7124
7387
  const cab = m.cabin && !c.flipped && !c.rollState ? m.cabin : null;
7125
7388
  c.deform.forEach(function(msh) {
7126
7389
  if (!msh.parent || !msh.visible) return;
@@ -7135,7 +7398,15 @@ var GameVehicles = (() => {
7135
7398
  irel.copy(rel).invert();
7136
7399
  if (!g.userData.p0) g.userData.p0 = new Float32Array(pos.array);
7137
7400
  const p0 = g.userData.p0;
7401
+ local.copy(p).applyMatrix4(irel);
7402
+ const reach = H.R / Math.max(1e-6, Math.min(sc.x, sc.y, sc.z));
7403
+ const reach2 = reach * reach;
7404
+ const arr = pos.array;
7138
7405
  for (let i = 0; i < pos.count; i++) {
7406
+ const dx = arr[3 * i] - local.x;
7407
+ const dy = arr[3 * i + 1] - local.y;
7408
+ const dz = arr[3 * i + 2] - local.z;
7409
+ if (dx * dx + dy * dy + dz * dz >= reach2) continue;
7139
7410
  v.fromBufferAttribute(pos, i).applyMatrix4(rel);
7140
7411
  const a = dentField(v, H);
7141
7412
  if (a <= 0) continue;
@@ -7151,10 +7422,15 @@ var GameVehicles = (() => {
7151
7422
  }
7152
7423
  if (changed) {
7153
7424
  pos.needsUpdate = true;
7154
- if (g.userData.crease) crease(g, 42);
7425
+ if (g.userData.crease && c.smoothLater) c.smoothLater(msh);
7426
+ else if (g.userData.crease) crease(g, 42);
7155
7427
  else g.computeVertexNormals();
7156
- g.computeBoundingSphere();
7157
7428
  g.computeBoundingBox();
7429
+ const sphere2 = g.boundingSphere ?? (g.boundingSphere = new THREE.Sphere());
7430
+ g.boundingBox.getCenter(sphere2.center);
7431
+ let far2 = 0;
7432
+ for (let i = 0; i < pos.count; i++) far2 = Math.max(far2, sphere2.center.distanceToSquared(v.fromBufferAttribute(pos, i)));
7433
+ sphere2.radius = Math.sqrt(far2);
7158
7434
  }
7159
7435
  });
7160
7436
  const dset = new Set(c.deform);
@@ -7238,13 +7514,14 @@ var GameVehicles = (() => {
7238
7514
  kit.impact = impact;
7239
7515
  kit.partEffects = {};
7240
7516
  const _pr = new THREE.Raycaster();
7517
+ _pr.layers.enableAll();
7241
7518
  function probe(c, o, d, far) {
7242
7519
  c.root.updateMatrixWorld(true);
7243
7520
  const Mw = c.vehicle.matrixWorld;
7244
7521
  const inv = new THREE.Matrix4().copy(Mw).invert();
7245
7522
  _pr.set(o.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
7246
7523
  _pr.far = far || 10;
7247
- const hits = _pr.intersectObject(c.vehicle, true);
7524
+ const hits = raycastIndexed(THREE, _pr, [c.vehicle], true);
7248
7525
  for (let i = 0; i < hits.length; i++) {
7249
7526
  const h = hits[i];
7250
7527
  const ob = h.object;
@@ -8610,6 +8887,7 @@ var GameVehicles = (() => {
8610
8887
  return hole;
8611
8888
  }
8612
8889
  const _zr = new THREE.Raycaster();
8890
+ _zr.layers.enableAll();
8613
8891
  const PROXY_MIN_TRIANGLES = 500;
8614
8892
  const STROKE_REACH = 0.6;
8615
8893
  const _pv = new V3();
@@ -8844,6 +9122,7 @@ var GameVehicles = (() => {
8844
9122
  return out;
8845
9123
  }
8846
9124
  const _rt = new THREE.Raycaster();
9125
+ _rt.layers.enableAll();
8847
9126
  function scratchPath(c, s, len, ang, cands, B) {
8848
9127
  const n = s.n.clone().normalize();
8849
9128
  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);
@@ -12125,6 +12404,8 @@ var GameVehicles = (() => {
12125
12404
  V3: THREE.Vector3,
12126
12405
  PI: Math.PI,
12127
12406
  document: doc,
12407
+ particleMaterials: opts.particleMaterials,
12408
+ effects: opts.effects,
12128
12409
  strings,
12129
12410
  t,
12130
12411
  matchers: createMatchers(strings),
@@ -12380,7 +12661,9 @@ var GameVehicles = (() => {
12380
12661
  assignDeep(strings, resolveStrings(opts.locale));
12381
12662
  return kit;
12382
12663
  };
12383
- const { THREE: _three, lod: _lod, locale: _locale, document: _document, plugins, ...config } = opts;
12664
+ const { THREE: _three, lod: _lod, locale: _locale, document: _document, particleMaterials: _particleMaterials, effects: _effects, plugins, ...config } = opts;
12665
+ void _particleMaterials;
12666
+ void _effects;
12384
12667
  void _three;
12385
12668
  void _lod;
12386
12669
  void _locale;
@@ -12476,6 +12759,14 @@ var GameVehicles = (() => {
12476
12759
  center: [b.center[0], b.center[1] - b.size[1] / 2 + Math.min(0.14, b.size[1]) / 2 + 0.04, b.center[2]],
12477
12760
  size: [b.size[0] * 0.5, Math.min(0.14, b.size[1]), b.size[2] * 0.95]
12478
12761
  });
12762
+ function paneBox(c, pane, inverse) {
12763
+ pane.geometry.computeBoundingBox();
12764
+ const box2 = pane.geometry.boundingBox.clone().applyMatrix4(pane.matrixWorld);
12765
+ box2.applyMatrix4(inverse);
12766
+ const center = [(box2.min.x + box2.max.x) / 2, (box2.min.y + box2.max.y) / 2, (box2.min.z + box2.max.z) / 2];
12767
+ const size = [box2.max.x - box2.min.x, box2.max.y - box2.min.y, box2.max.z - box2.min.z];
12768
+ return { center, size };
12769
+ }
12479
12770
  function buildVehicleModel(kit, c, options = {}) {
12480
12771
  const kind = kindOf(c);
12481
12772
  const m = c.meta;
@@ -12616,6 +12907,16 @@ var GameVehicles = (() => {
12616
12907
  tags: [role, ...tags]
12617
12908
  });
12618
12909
  }
12910
+ const claimed = new Set(zones.filter((z) => roleOfZone(z) === "glass").flatMap((z) => z.glass ?? []));
12911
+ c.root.updateMatrixWorld(true);
12912
+ const inverse = c.vehicle.matrixWorld.clone().invert();
12913
+ for (const pane of c.glass ?? []) {
12914
+ if (claimed.has(pane)) continue;
12915
+ const G = pane.userData.glass;
12916
+ const bb = paneBox(c, pane, inverse);
12917
+ const laminated = G?.type === "laminated" || m.bullet;
12918
+ 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: laminated ? 4e3 : 450, panes: [pane], visual: false, tags: ["glass"] });
12919
+ }
12619
12920
  if (!hubZone) {
12620
12921
  const h = Math.max(0.15, belt - clearance);
12621
12922
  add({
@@ -12951,6 +13252,76 @@ var GameVehicles = (() => {
12951
13252
  }
12952
13253
 
12953
13254
  // src/destructible/visual.ts
13255
+ var HOLE_DECAL_SCALE = 2.4;
13256
+ var DOOR_DEPTH = 0.07;
13257
+ var DOOR_EDGE_PIECES = 16;
13258
+ var DOOR_CARD_COLOR = "#3b3e42";
13259
+ var DOOR_LINER_COLOR = "#5a5e64";
13260
+ var DOOR_LINER_DARK = 0.15;
13261
+ var DOOR_STRIP_THIN = 0.05;
13262
+ var DOOR_STRIP_NEAR = 0.04;
13263
+ var DOOR_STRIP_OVER = 0.3;
13264
+ var DOOR_TOP_OVER_PANE = 0.03;
13265
+ var DOOR_STRIP_SUB = 0.02;
13266
+ var HOLE_TEXTURE_SIZE = 64;
13267
+ var DOOR_HANDLE_INSET = 0.12;
13268
+ var DOOR_HANDLE_MIN = 0.07;
13269
+ var DOOR_HANDLE_MAX = 0.24;
13270
+ var DOOR_HANDLE_THIN = 0.07;
13271
+ var DOOR_HANDLE_OUTSIDE = 0.1;
13272
+ var PANE_SEAT_OUT = 0.05;
13273
+ var PANE_SEAT_IN = -0.05;
13274
+ var PANE_SEAT_REACH = 0.14;
13275
+ var PANE_SEAT_SAMPLES = 24;
13276
+ var PANE_SEAT_LIFT = 4e-3;
13277
+ var PANE_FRAME_NEAR = 0.07;
13278
+ var DRAWN_MIRROR_SIZE = 0.3;
13279
+ var DRAWN_MIRROR_REACH = 0.4;
13280
+ var DRAWN_MIRROR_MOUNT = 0.2;
13281
+ var MIRROR_GLASS_FIT = 0.76;
13282
+ var MIRROR_GLASS_THIN = 0.04;
13283
+ var MIRROR_GLASS_MIN = 0.05;
13284
+ var MIRROR_GLASS_COLOR = "#59636a";
13285
+ var HOLE_RIM = { steel: "#b7bcc0", aluminium: "#d3d6d8", plastic: "#d9d6cc", rubber: "#141414", glass: "#eef3f5" };
13286
+ var holeMaterials = /* @__PURE__ */ new WeakMap();
13287
+ var holeDisc = null;
13288
+ function holeDiscGeometry(THREE) {
13289
+ holeDisc = holeDisc ?? new THREE.CircleGeometry(1, 12);
13290
+ return holeDisc;
13291
+ }
13292
+ function holeMaterial(kit, material) {
13293
+ const key = kit;
13294
+ const cache2 = holeMaterials.get(key) ?? /* @__PURE__ */ new Map();
13295
+ holeMaterials.set(key, cache2);
13296
+ const known = cache2.get(material);
13297
+ if (known) return known;
13298
+ const I = kit._;
13299
+ const THREE = kit.THREE;
13300
+ const canvas = kit.document?.createElement?.("canvas");
13301
+ const x = canvas?.getContext?.("2d");
13302
+ let made = I.MATERIALS.dark;
13303
+ if (x) {
13304
+ canvas.width = canvas.height = HOLE_TEXTURE_SIZE;
13305
+ const mid = HOLE_TEXTURE_SIZE / 2;
13306
+ x.fillStyle = HOLE_RIM[material] ?? HOLE_RIM.steel;
13307
+ x.beginPath();
13308
+ for (let i = 0; i < 18; i++) {
13309
+ const a = i / 18 * Math.PI * 2;
13310
+ const r = mid * (i % 2 ? 0.98 : 0.76);
13311
+ x.lineTo(mid + Math.cos(a) * r, mid + Math.sin(a) * r);
13312
+ }
13313
+ x.closePath();
13314
+ x.fill();
13315
+ x.fillStyle = "#050505";
13316
+ x.beginPath();
13317
+ x.arc(mid, mid, mid * 0.42, 0, Math.PI * 2);
13318
+ x.fill();
13319
+ const map = new THREE.CanvasTexture(canvas);
13320
+ made = new THREE.MeshStandardMaterial({ map, transparent: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -2, roughness: 0.55, metalness: material === "steel" || material === "aluminium" ? 0.6 : 0 });
13321
+ }
13322
+ cache2.set(material, made);
13323
+ return made;
13324
+ }
12954
13325
  var VehicleVisuals = class {
12955
13326
  constructor(kit, c, model) {
12956
13327
  __publicField(this, "kit");
@@ -12968,14 +13339,23 @@ var GameVehicles = (() => {
12968
13339
  __publicField(this, "syncPlates", /* @__PURE__ */ new Map());
12969
13340
  /** 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). */
12970
13341
  __publicField(this, "skins", /* @__PURE__ */ new Map());
13342
+ /** How high each door's cut reaches. */
13343
+ __publicField(this, "doorTops", /* @__PURE__ */ new Map());
12971
13344
  __publicField(this, "lightsOn", true);
12972
13345
  /** Heavy body meshes whose triangle index is still to build (null once done). */
12973
13346
  __publicField(this, "warming");
12974
13347
  /** A panel was torn off since the last `pump`: that frame already paid for the cut, the smoothing starts on the next one. */
12975
13348
  __publicField(this, "tornThisFrame", false);
13349
+ /** A shared crease queue owns the smoothing: `pump` only closes the frame of the cut. */
13350
+ __publicField(this, "queued", false);
13351
+ /** A panel was torn and the queue has not asked yet: the first `pendingUnits(frame)` after it holds the work for that frame. */
13352
+ __publicField(this, "holdArmed", false);
13353
+ __publicField(this, "holdFrame", -1);
12976
13354
  /** Normal smoothing still running, by mesh: a mesh cut again gets a new task and the old one is dropped. */
12977
13355
  __publicField(this, "smoothing", /* @__PURE__ */ new Map());
12978
13356
  __publicField(this, "hood");
13357
+ ;
13358
+ c.smoothLater = (mesh2) => this.smoothLater(mesh2);
12979
13359
  this.kit = kit;
12980
13360
  this.c = c;
12981
13361
  this.model = model;
@@ -12984,6 +13364,8 @@ var GameVehicles = (() => {
12984
13364
  for (const p of model.parts) this.byId.set(p.id, p);
12985
13365
  c.root.updateMatrixWorld(true);
12986
13366
  this.hood = this.nameHood();
13367
+ this.seatPanes();
13368
+ this.seatDoorHandles();
12987
13369
  this.warming = c.deform.filter((msh) => msh.geometry && msh.geometry.attributes.position && msh.geometry.attributes.position.count >= 1500);
12988
13370
  if (!this.warming.length) this.warming = null;
12989
13371
  for (const p of model.parts) {
@@ -13004,6 +13386,7 @@ var GameVehicles = (() => {
13004
13386
  const id = z ? z.id : this.model.parts.find((p) => p.role === "hood")?.id;
13005
13387
  if (!id) return null;
13006
13388
  c.hood.piv.name = id;
13389
+ c.hood.piv.userData.plateOf = id;
13007
13390
  return { id, open: c.hood.open, sign: c.hood.sign || 1 };
13008
13391
  }
13009
13392
  mirrorObjects(p) {
@@ -13091,6 +13474,7 @@ var GameVehicles = (() => {
13091
13474
  recess.userData.noFollow = true;
13092
13475
  const piv = new THREE.Group();
13093
13476
  piv.name = p.id;
13477
+ piv.userData.plateOf = p.id;
13094
13478
  piv.position.set(hx, cy, sk + s * 0.016);
13095
13479
  piv.userData.rigid = true;
13096
13480
  this.c.vehicle.add(piv);
@@ -13107,6 +13491,7 @@ var GameVehicles = (() => {
13107
13491
  recess.userData.noFollow = true;
13108
13492
  const piv = new THREE.Group();
13109
13493
  piv.name = p.id;
13494
+ piv.userData.plateOf = p.id;
13110
13495
  piv.position.set(sk - 0.016, B.y1 - 0.05, 0);
13111
13496
  piv.userData.rigid = true;
13112
13497
  this.c.vehicle.add(piv);
@@ -13170,10 +13555,19 @@ var GameVehicles = (() => {
13170
13555
  try {
13171
13556
  if (p.role === "door") {
13172
13557
  const dirty = /* @__PURE__ */ new Set();
13173
- 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];
13174
- const pcs = I.cutPanel(this.c, z, rect, true, dirty).map((x) => x.mesh).filter((m) => !!m);
13558
+ const glassBox = new this.THREE.Box3();
13559
+ for (const g of I.windowsOver(this.c, z, inv)) glassBox.union(I.localBox(this.c, g, inv));
13560
+ const top = glassBox.isEmpty() ? B.y1 - 0.02 : Math.max(B.y1 - 0.02, glassBox.min.y + DOOR_TOP_OVER_PANE);
13561
+ this.doorTops.set(id, top);
13562
+ const rect = [B.x0 + 0.03, B.x1 - 0.03, B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1), top];
13563
+ const pcs = I.cutPanel(this.c, z, rect, true, dirty, { anyN: true }).map((x) => x.mesh).filter((m) => !!m);
13175
13564
  this.mend(dirty);
13176
- if (pcs.length) objs = [...pcs, ...I.followersIn(this.c, z, inv), ...I.windowsOver(this.c, z, inv)];
13565
+ if (pcs.length) {
13566
+ const panes = I.windowsOver(this.c, z, inv);
13567
+ const seals = panes.map((g) => I.paneSeals.get(g)).filter((s) => !!s);
13568
+ const mirrors = this.mirrorsOn(z);
13569
+ objs = [...pcs, ...this.doorCards(pcs, z), ...I.followersIn(this.c, z, inv), ...panes, ...seals, ...mirrors];
13570
+ }
13177
13571
  } else {
13178
13572
  const pcs = this.cutZone(z);
13179
13573
  if (pcs.length) objs = [...pcs, ...I.followersIn(this.c, z, inv)];
@@ -13191,10 +13585,124 @@ var GameVehicles = (() => {
13191
13585
  const di = deform.indexOf(o);
13192
13586
  if (di >= 0) deform.splice(di, 1);
13193
13587
  }
13588
+ if (p.role === "door" && objs.length > 0) {
13589
+ this.doorEdges(piv, z, this.doorTops.get(id) ?? B.y1 - 0.02);
13590
+ this.liftDoorTrim(objs, z);
13591
+ this.cutStripsAcross(piv, z, inv);
13592
+ }
13194
13593
  piv.quaternion.copy(q);
13195
13594
  this.c.root.updateMatrixWorld(true);
13196
13595
  return objs.length > 0;
13197
13596
  }
13597
+ /** 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. */
13598
+ doorEdges(piv, z, y1) {
13599
+ const THREE = this.THREE;
13600
+ const B = z.box;
13601
+ const side2 = z.side < 0 ? -1 : 1;
13602
+ const flat = piv.position.z - side2 * 0.016;
13603
+ const x0 = B.x0 + 0.03;
13604
+ const x1 = B.x1 - 0.03;
13605
+ const y0 = B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1);
13606
+ const loop = [];
13607
+ const run = (ax, ay, bx, by) => {
13608
+ 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]);
13609
+ };
13610
+ run(x0, y0, x1, y0);
13611
+ run(x1, y0, x1, y1);
13612
+ run(x1, y1, x0, y1);
13613
+ run(x0, y1, x0, y0);
13614
+ const heights = loop.map(([x, y]) => this.surface("z", side2, B, [[x, y]]) ?? flat);
13615
+ const band = (lift) => {
13616
+ const position = [];
13617
+ const index = [];
13618
+ loop.forEach(([x, y], i) => {
13619
+ position.push(x, y, heights[i] + side2 * lift, x, y, heights[i] + side2 * (lift - DOOR_DEPTH));
13620
+ const a = i * 2;
13621
+ const b = (i + 1) % loop.length * 2;
13622
+ index.push(a, a + 1, b, b, a + 1, b + 1);
13623
+ });
13624
+ const g = new THREE.BufferGeometry();
13625
+ g.setAttribute("position", new THREE.Float32BufferAttribute(position, 3));
13626
+ g.setIndex(index);
13627
+ g.computeVertexNormals();
13628
+ return g;
13629
+ };
13630
+ const paint = this.paint().clone();
13631
+ paint.side = THREE.DoubleSide;
13632
+ const edges = new THREE.Mesh(band(2e-3), paint);
13633
+ const lining = new THREE.Mesh(band(2e-3), paint);
13634
+ for (const m of [edges, lining]) {
13635
+ m.userData.noFollow = true;
13636
+ m.castShadow = false;
13637
+ this.c.vehicle.add(m);
13638
+ }
13639
+ piv.attach(edges);
13640
+ }
13641
+ /** A front door carries the mirror drawn at its corner. */
13642
+ mirrorsOn(z) {
13643
+ if (!z.mirror) return [];
13644
+ const own = (z.mirrors ?? []).filter((o) => o.parent);
13645
+ if (own.length) return own;
13646
+ const part = this.model.parts.find((q) => q.role === "mirror" && Math.sign(q.box.center[2]) === Math.sign(z.side));
13647
+ return part ? (this.extra.get(part.id) ?? []).filter((o) => o.parent) : [];
13648
+ }
13649
+ /** The panel drawn inside the door is lit like the rest of a cabin, not a black slab from end to end. */
13650
+ liftDoorTrim(objs, z) {
13651
+ const card = this.I.makeMaterial(DOOR_LINER_COLOR, { roughness: 0.85 });
13652
+ const inv = this.I.worldInverseOf(this.c);
13653
+ for (const o of objs) {
13654
+ if (!o.isMesh || o.userData.glass) continue;
13655
+ const box2 = this.I.localBox(this.c, o, inv);
13656
+ const size = box2.getSize(new this.THREE.Vector3());
13657
+ 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;
13658
+ }
13659
+ }
13660
+ /** 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. */
13661
+ cutStripsAcross(piv, z, inv) {
13662
+ const THREE = this.THREE;
13663
+ const B = z.box;
13664
+ const x0 = B.x0 + 0.03;
13665
+ const x1 = B.x1 - 0.03;
13666
+ const y0 = B.y0 - DOOR_STRIP_NEAR;
13667
+ const y1 = B.y1 + DOOR_STRIP_NEAR;
13668
+ const deform = this.c.deform ?? [];
13669
+ for (const o of [...this.c.vehicle.children]) {
13670
+ if (!o.isMesh || o === piv || deform.includes(o) || o.userData.glass || o.userData.decal || !o.visible) continue;
13671
+ const box2 = this.I.localBox(this.c, o, inv);
13672
+ const size = box2.getSize(new THREE.Vector3());
13673
+ const mid = box2.getCenter(new THREE.Vector3());
13674
+ if (Math.sign(mid.z) !== Math.sign(z.side) || size.z > DOOR_STRIP_THIN || size.y > DOOR_STRIP_THIN || Math.min(box2.max.x, x1) - Math.max(box2.min.x, x0) < (x1 - x0) * DOOR_STRIP_OVER || mid.y < y0 || mid.y > y1) continue;
13675
+ const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld);
13676
+ const v = new THREE.Vector3();
13677
+ const sdf = (x, y, zz) => {
13678
+ v.set(x, y, zz).applyMatrix4(toVehicle);
13679
+ return Math.max(x0 - v.x, v.x - x1);
13680
+ };
13681
+ this.kit.cutMesh(o, [{ sdf }], 42, { sub: DOOR_STRIP_SUB });
13682
+ const from = Math.max(box2.min.x, x0);
13683
+ const to = Math.min(box2.max.x, x1);
13684
+ const piece = this.box(to - from, size.y, size.z, o.material, (from + to) / 2, mid.y, mid.z);
13685
+ piece.userData.noFollow = true;
13686
+ piv.attach(piece);
13687
+ }
13688
+ }
13689
+ /** The inside of a door: its skin brought in by the depth of the door, in the trim's dark plastic, so the open door is a door and not one sheet of paint. */
13690
+ doorCards(skins, z) {
13691
+ const cardMaterial = this.I.makeMaterial(DOOR_CARD_COLOR, { roughness: 0.8 });
13692
+ return skins.map((skin) => {
13693
+ const card = new this.THREE.Mesh(skin.geometry, cardMaterial);
13694
+ card.position.copy(skin.position);
13695
+ card.quaternion.copy(skin.quaternion);
13696
+ card.scale.copy(skin.scale);
13697
+ const side2 = z.side < 0 ? -1 : 1;
13698
+ const inward = -side2 * DOOR_DEPTH;
13699
+ card.position.z += inward;
13700
+ card.userData.noFollow = true;
13701
+ card.castShadow = false;
13702
+ this.c.vehicle.add(card);
13703
+ return card;
13704
+ });
13705
+ }
13198
13706
  mend(dirty) {
13199
13707
  dirty.forEach((msh) => {
13200
13708
  const g = msh.geometry;
@@ -13204,7 +13712,191 @@ var GameVehicles = (() => {
13204
13712
  g.computeBoundingBox();
13205
13713
  });
13206
13714
  }
13715
+ /**
13716
+ * 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
13717
+ * comes to the shell along its own normal, by how far the shell is just outside each of its vertices.
13718
+ */
13719
+ seatPanes() {
13720
+ const THREE = this.THREE;
13721
+ const c = this.c;
13722
+ c.root.updateMatrixWorld(true);
13723
+ const shell = c.deform.filter((o) => o.isMesh && !o.userData.glass && c.paints?.has(o.material));
13724
+ if (!shell.length) return;
13725
+ const ray = new THREE.Raycaster();
13726
+ ray.layers.enableAll();
13727
+ for (const pane of c.glass) {
13728
+ if (pane.userData.glass?.outline) continue;
13729
+ const seat = this.paneSeat(pane, shell, ray);
13730
+ if (!seat) continue;
13731
+ const near = c.vehicle.children.filter((o) => o.isMesh && o !== pane && !o.userData.glass && !shell.includes(o) && this.onPane(o, pane, seat));
13732
+ for (const m of [pane, ...near]) this.movePane(m, seat);
13733
+ const past = this.shellPast(pane, shell, ray, seat.normal);
13734
+ if (past > 0) for (const m of [pane, ...near]) this.movePane(m, { normal: seat.normal, samples: [{ at: seat.samples[0].at, by: past }] });
13735
+ }
13736
+ }
13737
+ /** The outward normal of a pane and how far the shell is at each sampled point (world). */
13738
+ paneSeat(pane, shell, ray) {
13739
+ const THREE = this.THREE;
13740
+ const pos = pane.geometry.attributes.position;
13741
+ const box2 = new THREE.Box3().setFromObject(pane);
13742
+ const centre = box2.getCenter(new THREE.Vector3());
13743
+ const vehicleCentre = new THREE.Box3().setFromObject(this.c.root).getCenter(new THREE.Vector3());
13744
+ const tri = new THREE.Triangle();
13745
+ const normal = new THREE.Vector3();
13746
+ let largest = 0;
13747
+ const idx = pane.geometry.index;
13748
+ const count = idx ? idx.count : pos.count;
13749
+ for (let f = 0; f + 2 < count; f += 3) {
13750
+ tri.a.fromBufferAttribute(pos, idx ? idx.getX(f) : f);
13751
+ tri.b.fromBufferAttribute(pos, idx ? idx.getX(f + 1) : f + 1);
13752
+ tri.c.fromBufferAttribute(pos, idx ? idx.getX(f + 2) : f + 2);
13753
+ if (tri.getArea() > largest) {
13754
+ largest = tri.getArea();
13755
+ tri.getNormal(normal);
13756
+ }
13757
+ }
13758
+ normal.transformDirection(pane.matrixWorld);
13759
+ if (normal.dot(centre.clone().sub(vehicleCentre)) < 0) normal.negate();
13760
+ const samples = [];
13761
+ const seen = /* @__PURE__ */ new Set();
13762
+ const v = new THREE.Vector3();
13763
+ const step = Math.max(1, Math.floor(pos.count / PANE_SEAT_SAMPLES));
13764
+ for (let i = 0; i < pos.count; i += step) {
13765
+ v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
13766
+ const key = `${Math.round(v.x * 100)},${Math.round(v.y * 100)},${Math.round(v.z * 100)}`;
13767
+ if (seen.has(key)) continue;
13768
+ seen.add(key);
13769
+ const away = v.clone().sub(centre);
13770
+ away.addScaledVector(normal, -away.dot(normal)).normalize();
13771
+ let by = -Infinity;
13772
+ for (const off of [PANE_SEAT_OUT, PANE_SEAT_IN, PANE_SEAT_IN * 3]) {
13773
+ const at = v.clone().addScaledVector(away, off);
13774
+ ray.set(at.addScaledVector(normal, PANE_SEAT_REACH), normal.clone().negate());
13775
+ ray.far = PANE_SEAT_REACH * 2;
13776
+ const hit = raycastIndexed(THREE, ray, shell)[0];
13777
+ if (hit) by = Math.max(by, PANE_SEAT_REACH - hit.distance);
13778
+ }
13779
+ if (by > -Infinity) samples.push({ at: v.clone(), by });
13780
+ }
13781
+ return samples.length >= 3 ? { normal, samples } : null;
13782
+ }
13783
+ /** How far the shell still stands past a seated pane, over its triangles. */
13784
+ shellPast(pane, shell, ray, normal) {
13785
+ const THREE = this.THREE;
13786
+ const pos = pane.geometry.attributes.position;
13787
+ const idx = pane.geometry.index;
13788
+ const count = idx ? idx.count : pos.count;
13789
+ const tri = new THREE.Triangle();
13790
+ const step = Math.max(1, Math.floor(count / 3 / PANE_SEAT_SAMPLES)) * 3;
13791
+ let past = 0;
13792
+ for (let f = 0; f + 2 < count; f += step) {
13793
+ tri.a.fromBufferAttribute(pos, idx ? idx.getX(f) : f);
13794
+ tri.b.fromBufferAttribute(pos, idx ? idx.getX(f + 1) : f + 1);
13795
+ tri.c.fromBufferAttribute(pos, idx ? idx.getX(f + 2) : f + 2);
13796
+ const mid = tri.getMidpoint(new THREE.Vector3()).applyMatrix4(pane.matrixWorld);
13797
+ ray.set(mid.addScaledVector(normal, PANE_SEAT_REACH), normal.clone().negate());
13798
+ ray.far = PANE_SEAT_REACH;
13799
+ const hit = raycastIndexed(THREE, ray, shell)[0];
13800
+ if (hit) past = Math.max(past, PANE_SEAT_REACH - hit.distance + PANE_SEAT_LIFT);
13801
+ }
13802
+ return past;
13803
+ }
13804
+ /** A frame or a seal drawn on the pane: small, and close to its plane. */
13805
+ onPane(o, pane, seat) {
13806
+ const THREE = this.THREE;
13807
+ const box2 = new THREE.Box3().setFromObject(o);
13808
+ if (box2.isEmpty() || !new THREE.Box3().setFromObject(pane).expandByScalar(PANE_FRAME_NEAR).containsBox(box2)) return false;
13809
+ const at = box2.getCenter(new THREE.Vector3());
13810
+ return Math.abs(at.clone().sub(seat.samples[0].at).dot(seat.normal)) < PANE_SEAT_REACH;
13811
+ }
13812
+ movePane(mesh2, seat) {
13813
+ const THREE = this.THREE;
13814
+ const g = mesh2.geometry.clone();
13815
+ const pos = g.attributes.position;
13816
+ const inverse = mesh2.matrixWorld.clone().invert();
13817
+ const v = new THREE.Vector3();
13818
+ for (let i = 0; i < pos.count; i++) {
13819
+ v.fromBufferAttribute(pos, i).applyMatrix4(mesh2.matrixWorld);
13820
+ let weight = 0;
13821
+ let by = 0;
13822
+ for (const s of seat.samples) {
13823
+ const w = 1 / (s.at.distanceToSquared(v) + 1e-4);
13824
+ weight += w;
13825
+ by += s.by * w;
13826
+ }
13827
+ v.addScaledVector(seat.normal, by / weight + PANE_SEAT_LIFT).applyMatrix4(inverse);
13828
+ pos.setXYZ(i, v.x, v.y, v.z);
13829
+ }
13830
+ pos.needsUpdate = true;
13831
+ g.computeBoundingBox();
13832
+ g.computeBoundingSphere();
13833
+ mesh2.geometry = g;
13834
+ }
13835
+ /** A handle is at the free edge of its door, the end away from the hinge. */
13836
+ seatDoorHandles() {
13837
+ const THREE = this.THREE;
13838
+ const c = this.c;
13839
+ const inv = this.I.worldInverseOf(c);
13840
+ for (const p of this.model.parts) {
13841
+ if (p.role !== "door") continue;
13842
+ const z = this.zone(p);
13843
+ if (!z) continue;
13844
+ const B = z.box;
13845
+ const free = z.hinge === "rear" ? B.x1 - DOOR_HANDLE_INSET : B.x0 + DOOR_HANDLE_INSET;
13846
+ for (const o of c.vehicle.children) {
13847
+ if (!o.isMesh || o.material !== this.I.MATERIALS.chrome || o.userData.glass) continue;
13848
+ const box2 = this.I.localBox(c, o, inv);
13849
+ const size = box2.getSize(new THREE.Vector3());
13850
+ const at = box2.getCenter(new THREE.Vector3());
13851
+ const isHandle = size.x > DOOR_HANDLE_MIN && size.x < DOOR_HANDLE_MAX && size.y < DOOR_HANDLE_THIN && size.z < DOOR_HANDLE_THIN;
13852
+ 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;
13853
+ const here = this.surface("z", z.side < 0 ? -1 : 1, B, [[at.x, at.y]]);
13854
+ if (here != null && Math.abs(here - at.z) > DOOR_HANDLE_OUTSIDE) continue;
13855
+ const skin = this.surface("z", z.side < 0 ? -1 : 1, B, [[free, at.y]]);
13856
+ o.position.x += free - at.x;
13857
+ if (skin != null) o.position.z += skin + (z.side < 0 ? -1 : 1) * (size.z / 2) - at.z;
13858
+ }
13859
+ }
13860
+ c.root.updateMatrixWorld(true);
13861
+ }
13862
+ /** The glass of a drawn mirror sits inside its rounded housing, without corners standing past it. */
13863
+ seatMirrorGlass(glass) {
13864
+ const g = glass.geometry.clone();
13865
+ g.computeBoundingBox();
13866
+ const mid = g.boundingBox.getCenter(new this.THREE.Vector3());
13867
+ g.translate(-mid.x, -mid.y, -mid.z);
13868
+ g.scale(MIRROR_GLASS_FIT, MIRROR_GLASS_FIT, MIRROR_GLASS_FIT);
13869
+ g.translate(mid.x, mid.y, mid.z);
13870
+ glass.geometry = g;
13871
+ glass.material = this.I.makeMaterial(MIRROR_GLASS_COLOR, { metalness: 0.95, roughness: 0.12 });
13872
+ }
13873
+ /** The mirror the model already draws around where the part says, if it draws one. */
13874
+ drawnMirror(p) {
13875
+ const THREE = this.THREE;
13876
+ const [cx, cy, cz] = p.box.center;
13877
+ this.c.root.updateMatrixWorld(true);
13878
+ const inv = this.I.worldInverseOf(this.c);
13879
+ const small = this.c.vehicle.children.flatMap((o) => {
13880
+ if (o.userData.glass || o.userData.decal || !(o.isMesh || o.isGroup) || !o.visible || this.c.deform?.includes(o)) return [];
13881
+ const box2 = this.I.localBox(this.c, o, inv);
13882
+ if (box2.isEmpty()) return [];
13883
+ const size = box2.getSize(new THREE.Vector3());
13884
+ return Math.max(size.x, size.y, size.z) < DRAWN_MIRROR_SIZE ? [{ o, at: box2.getCenter(new THREE.Vector3()) }] : [];
13885
+ });
13886
+ 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);
13887
+ return small.filter(({ at }) => housing.some((h) => h.at.distanceTo(at) < DRAWN_MIRROR_MOUNT)).map(({ o }) => o);
13888
+ }
13207
13889
  makeMirror(p) {
13890
+ const drawn = this.drawnMirror(p);
13891
+ if (drawn.length) {
13892
+ for (const o of drawn) {
13893
+ if (!o.isMesh || o.material !== this.I.MATERIALS.chrome) continue;
13894
+ const size = new this.THREE.Box3().setFromObject(o).getSize(new this.THREE.Vector3());
13895
+ if (size.x < MIRROR_GLASS_THIN && size.y > MIRROR_GLASS_MIN && size.z > MIRROR_GLASS_MIN) this.seatMirrorGlass(o);
13896
+ }
13897
+ this.extra.set(p.id, drawn);
13898
+ return;
13899
+ }
13208
13900
  const [cx, cy, cz] = p.box.center;
13209
13901
  const s = cz < 0 ? -1 : 1;
13210
13902
  const mats = this.I.MATERIALS;
@@ -13245,17 +13937,28 @@ var GameVehicles = (() => {
13245
13937
  }
13246
13938
  // ---- marks -------------------------------------------------------------------------------------------------------------------------------
13247
13939
  /** A bullet hole: a dark disc lying on the surface at `point` (`c.vehicle` coordinates). */
13248
- holeDecal(point, normal, diameter) {
13940
+ holeDecal(point, normal, diameter, part) {
13249
13941
  const THREE = this.THREE;
13250
- const m = new THREE.Mesh(new THREE.CircleGeometry(Math.max(6e-3, diameter / 2), 10), this.I.MATERIALS.dark);
13942
+ const pane = part?.role === "glass" ? this.panesOf(part).find((g) => g.visible) : void 0;
13943
+ if (part?.role === "glass" && (!pane || pane.userData.glass?.broken)) return;
13944
+ const radius = Math.max(6e-3, diameter / 2) * HOLE_DECAL_SCALE;
13945
+ const m = new THREE.Mesh(holeDiscGeometry(THREE), holeMaterial(this.kit, part?.material ?? "steel"));
13946
+ m.scale.set(radius, radius, 1);
13251
13947
  const n = new THREE.Vector3(...normal);
13252
- m.position.set(...point).addScaledVector(n, 4e-3);
13253
- m.lookAt(m.position.clone().add(n));
13948
+ m.position.set(...point).addScaledVector(n, 3e-3);
13949
+ this.c.vehicle.add(m);
13950
+ m.lookAt(this.c.vehicle.localToWorld(m.position.clone().add(n)));
13254
13951
  m.userData.decal = true;
13255
13952
  m.userData.noFollow = true;
13953
+ m.renderOrder = 3;
13256
13954
  m.raycast = () => {
13257
13955
  };
13258
- this.c.vehicle.add(m);
13956
+ 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);
13957
+ if (carrier) carrier.attach(m);
13958
+ }
13959
+ /** The door or lid a hinge group stands for. */
13960
+ plateIdOf(o) {
13961
+ return o.userData.plateOf;
13259
13962
  }
13260
13963
  spark(point, dir, count = 8) {
13261
13964
  const V3 = this.THREE.Vector3;
@@ -13295,10 +13998,8 @@ var GameVehicles = (() => {
13295
13998
  }
13296
13999
  /** Glass: cracks (laminated) or shatters (tempered); returns whether it broke. */
13297
14000
  hitGlass(p, point, dir, force) {
13298
- const z = this.zone(p);
13299
- if (!z) return false;
13300
14001
  const V3 = this.THREE.Vector3;
13301
- const list2 = z.glass.filter((g) => g.parent && g.visible);
14002
+ const list2 = this.panesOf(p).filter((g) => g.parent && g.visible);
13302
14003
  if (!list2.length) return false;
13303
14004
  const pt = new V3(...point);
13304
14005
  let best = list2[0];
@@ -13315,9 +14016,16 @@ var GameVehicles = (() => {
13315
14016
  this.kit.hitGlass(this.c, best, pt, new V3(...dir), force);
13316
14017
  return !!best.userData.glass?.broken;
13317
14018
  }
14019
+ panesOf(p) {
14020
+ return p.panes ?? this.zone(p)?.glass ?? [];
14021
+ }
13318
14022
  glassBreak(p) {
13319
14023
  const z = this.zone(p);
13320
- if (!z) return;
14024
+ if (!z) {
14025
+ 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);
14026
+ this.gone.add(p.id);
14027
+ return;
14028
+ }
13321
14029
  for (const g of z.glass) if (g.parent && g.visible) this.I.glassBand(this.c, z, g, 4);
13322
14030
  this.gone.add(p.id);
13323
14031
  }
@@ -13345,6 +14053,7 @@ var GameVehicles = (() => {
13345
14053
  smoothLater(msh) {
13346
14054
  this.smoothing.set(msh, this.I.creaseTask(msh.geometry, 42));
13347
14055
  this.tornThisFrame = true;
14056
+ this.holdArmed = true;
13348
14057
  }
13349
14058
  /** Spends at most `budgetMs` finishing the pending normal smoothing. Call once per frame. */
13350
14059
  pump(budgetMs = 2.5) {
@@ -13352,6 +14061,7 @@ var GameVehicles = (() => {
13352
14061
  this.tornThisFrame = false;
13353
14062
  return;
13354
14063
  }
14064
+ if (this.queued) return;
13355
14065
  if (!this.smoothing.size && !this.warming) return;
13356
14066
  const deadline = performance.now() + budgetMs;
13357
14067
  for (const [msh, task] of this.smoothing) {
@@ -13360,6 +14070,34 @@ var GameVehicles = (() => {
13360
14070
  }
13361
14071
  this.warmIndexes(deadline);
13362
14072
  }
14073
+ setQueued(isQueued) {
14074
+ this.queued = isQueued;
14075
+ }
14076
+ /** 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). */
14077
+ pendingUnits(frame) {
14078
+ if (this.holdArmed && frame !== void 0) {
14079
+ this.holdArmed = false;
14080
+ this.holdFrame = frame;
14081
+ }
14082
+ if (frame !== void 0 && frame === this.holdFrame) return 0;
14083
+ if (frame === void 0 && this.tornThisFrame) return 0;
14084
+ return this.smoothing.size + (this.warming ? 1 : 0);
14085
+ }
14086
+ /** One slice of the pending work, for the shared queue. */
14087
+ runStep(deadline) {
14088
+ const next = this.smoothing.entries().next();
14089
+ if (!next.done) {
14090
+ const [msh, task] = next.value;
14091
+ if (msh.geometry !== task.geo || task.step(deadline)) this.smoothing.delete(msh);
14092
+ return true;
14093
+ }
14094
+ if (!this.warming) return false;
14095
+ this.warmIndexes(deadline);
14096
+ return true;
14097
+ }
14098
+ finishAll() {
14099
+ while (this.runStep(Infinity)) ;
14100
+ }
13363
14101
  /** Measures the triangles of the heavy body meshes once, a slice per frame, so the first panel torn off finds them ready (see `triIndexOf`). */
13364
14102
  warmIndexes(deadline) {
13365
14103
  if (!this.warming) return;
@@ -13531,6 +14269,7 @@ var GameVehicles = (() => {
13531
14269
  }
13532
14270
  pieceTasks.forEach((task, mesh2) => this.smoothing.set(mesh2, task));
13533
14271
  this.tornThisFrame = true;
14272
+ this.holdArmed = true;
13534
14273
  dirty.forEach((msh) => {
13535
14274
  const g = msh.geometry;
13536
14275
  if (g.userData.crease || !g.index) this.smoothLater(msh);
@@ -13637,6 +14376,7 @@ var GameVehicles = (() => {
13637
14376
  let clock = 0;
13638
14377
  let model;
13639
14378
  let vis = null;
14379
+ let releaseCrease;
13640
14380
  const ps = /* @__PURE__ */ new Map();
13641
14381
  let js = [];
13642
14382
  let leaks = /* @__PURE__ */ new Map();
@@ -13721,7 +14461,9 @@ var GameVehicles = (() => {
13721
14461
  rand = rng(seedBase);
13722
14462
  igniteRand = rng(strHash(`${c.key}|${id}|ignition|${options.seed ?? 0}`));
13723
14463
  steerSign = rand() < 0.5 ? -1 : 1;
14464
+ releaseCrease?.();
13724
14465
  vis = headless ? null : new VehicleVisuals(kit, c, model);
14466
+ releaseCrease = vis && options.creaseQueue ? options.creaseQueue.track(vis) : void 0;
13725
14467
  recomputeGroups();
13726
14468
  };
13727
14469
  const recomputeGroups = () => {
@@ -14032,13 +14774,13 @@ var GameVehicles = (() => {
14032
14774
  sync();
14033
14775
  const M = toFrameMatrix();
14034
14776
  const v = toLocal(h.point);
14035
- const p = locate(v, 0.2);
14777
+ const p = (h.part ? ps.get(h.part) : void 0) ?? locate(v, 0.2);
14036
14778
  if (!p) return;
14037
14779
  const d = p.def;
14038
14780
  const E = h.energy ?? 0;
14039
14781
  const cal = h.caliber ?? 9;
14040
14782
  const dir = dirToLocal(h.direction ?? [0, 0, -1]).normalize();
14041
- const n = normalOf(d, v);
14783
+ const n = h.normal ? dirToLocal(h.normal).normalize() : normalOf(d, v);
14042
14784
  const res = resist(d, p);
14043
14785
  const through = E >= res;
14044
14786
  const cosInc = Math.abs(dir.dot(n));
@@ -14057,7 +14799,7 @@ var GameVehicles = (() => {
14057
14799
  const hp = { point: r3(fromVehicle(v, M)), normal: r3(fromVehicle(n, new THREE.Matrix4().extractRotation(M))), diameter: r2(holeDiam), through };
14058
14800
  p.holes.push(hp);
14059
14801
  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])))))) } : {} }));
14060
- vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam);
14802
+ vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam, d);
14061
14803
  startLeak(p, v, cal, out, dir);
14062
14804
  };
14063
14805
  switch (d.material) {
@@ -14358,6 +15100,46 @@ var GameVehicles = (() => {
14358
15100
  for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT)) return true;
14359
15101
  return false;
14360
15102
  };
15103
+ const surfaceRays = new THREE.Raycaster();
15104
+ surfaceRays.layers.enableAll();
15105
+ surfaceRays.camera = new THREE.PerspectiveCamera();
15106
+ let meshParts = null;
15107
+ const paneParts = () => {
15108
+ if (meshParts) return meshParts;
15109
+ meshParts = /* @__PURE__ */ new Map();
15110
+ for (const d of model.parts) {
15111
+ if (d.role !== "glass") continue;
15112
+ const z = kit.zonesOf(c).find((q) => q.id === d.zoneId);
15113
+ for (const g of z?.glass ?? d.panes ?? []) meshParts.set(g, d.id);
15114
+ }
15115
+ return meshParts;
15116
+ };
15117
+ const isDrawnSurface = (o) => {
15118
+ for (let q = o; q && q !== c.vehicle; q = q.parent) if (!q.visible || q.userData.decal || q.userData.ghost) return false;
15119
+ return true;
15120
+ };
15121
+ const partOfSurface = (o, v) => {
15122
+ const pane = paneParts().get(o);
15123
+ if (pane) return ps.get(pane) ?? null;
15124
+ for (let q = o; q && q !== c.vehicle; q = q.parent) {
15125
+ if (vis?.plateIdOf(q)) return ps.get(vis.plateIdOf(q)) ?? null;
15126
+ }
15127
+ return locate(v, 0.08, (p) => p.def.role === "glass");
15128
+ };
15129
+ const surfaceRay = (ray) => {
15130
+ sync();
15131
+ surfaceRays.set(new V3(ray.origin[0], ray.origin[1], ray.origin[2]), new V3(ray.direction[0], ray.direction[1], ray.direction[2]));
15132
+ surfaceRays.far = ray.maxDistance;
15133
+ for (const h of surfaceRays.intersectObject(c.vehicle, true)) {
15134
+ if (!h.face || !isDrawnSurface(h.object)) continue;
15135
+ const p = partOfSurface(h.object, c.vehicle.worldToLocal(h.point.clone()));
15136
+ if (!p || p.lost || ray.ignored?.(p.def.id)) continue;
15137
+ const n = h.face.normal.clone().transformDirection(h.object.matrixWorld);
15138
+ if (n.dot(surfaceRays.ray.direction) > 0) n.negate();
15139
+ 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 };
15140
+ }
15141
+ return null;
15142
+ };
14361
15143
  const hit = (h) => {
14362
15144
  stateCache = void 0;
14363
15145
  const out = [];
@@ -14480,9 +15262,14 @@ var GameVehicles = (() => {
14480
15262
  },
14481
15263
  describe,
14482
15264
  hit,
15265
+ surfaceRay,
14483
15266
  update,
14484
15267
  state,
14485
15268
  reset,
15269
+ dispose: () => {
15270
+ releaseCrease?.();
15271
+ releaseCrease = void 0;
15272
+ },
14486
15273
  disassemble,
14487
15274
  extinguish,
14488
15275
  serialize: () => ({
@@ -14521,6 +15308,61 @@ var GameVehicles = (() => {
14521
15308
  return { root, vehicle };
14522
15309
  }
14523
15310
 
15311
+ // src/destructible/crease-queue.ts
15312
+ function createCreaseQueue() {
15313
+ const tracked = [];
15314
+ let cursor = 0;
15315
+ let lastFrame;
15316
+ let attached = false;
15317
+ const deferred = {
15318
+ id: DEFERRED_QUEUE_ID.VEHICLE_CREASE,
15319
+ pending(frame) {
15320
+ lastFrame = frame;
15321
+ let units = 0;
15322
+ for (const work of tracked) units += work.pendingUnits(frame);
15323
+ return units;
15324
+ },
15325
+ run(deadline, clock) {
15326
+ let ran = 0;
15327
+ let skipped = 0;
15328
+ while (tracked.length > 0 && skipped < tracked.length) {
15329
+ if (ran > 0 && clock() >= deadline) break;
15330
+ const work = tracked[cursor % tracked.length];
15331
+ cursor = (cursor + 1) % tracked.length;
15332
+ if (work.pendingUnits(lastFrame) > 0 && work.runStep(deadline)) {
15333
+ ran++;
15334
+ skipped = 0;
15335
+ } else skipped++;
15336
+ }
15337
+ return ran;
15338
+ },
15339
+ flush() {
15340
+ for (const work of tracked) work.finishAll();
15341
+ },
15342
+ attach() {
15343
+ attached = true;
15344
+ for (const work of tracked) work.setQueued(true);
15345
+ },
15346
+ detach() {
15347
+ attached = false;
15348
+ for (const work of tracked) work.setQueued(false);
15349
+ }
15350
+ };
15351
+ return {
15352
+ deferredQueue: () => deferred,
15353
+ track(work) {
15354
+ tracked.push(work);
15355
+ work.setQueued(attached);
15356
+ return () => {
15357
+ const at = tracked.indexOf(work);
15358
+ if (at < 0) return;
15359
+ tracked.splice(at, 1);
15360
+ work.setQueued(false);
15361
+ };
15362
+ }
15363
+ };
15364
+ }
15365
+
14524
15366
  // src/destructible/enclosure.ts
14525
15367
  var CABIN_LEAK = 0.02;
14526
15368
  var HOLE_RATE_PER_AREA = 3e3;