@adatechnology/game-vehicles 0.4.7 → 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.
@@ -23,13 +23,18 @@ var GameVehicles = (() => {
23
23
  // src/index.ts
24
24
  var index_exports = {};
25
25
  __export(index_exports, {
26
+ CABIN_LEAK: () => CABIN_LEAK,
26
27
  JOINT_RULES: () => JOINT_RULES,
27
28
  LEGACY_STATES: () => LEGACY_STATES,
28
29
  LEGACY_ZONE_IDS: () => LEGACY_ZONE_IDS,
29
30
  SEED_SALTS: () => SEED_SALTS,
30
31
  buildVehicleItem: () => buildVehicleItem,
31
32
  buildVehicleModel: () => buildVehicleModel,
33
+ cabinBoxOf: () => cabinBoxOf,
34
+ cabinEnclosure: () => cabinEnclosure,
32
35
  clamp: () => clamp,
36
+ crackedGlassMaterial: () => crackedGlassMaterial,
37
+ createCreaseQueue: () => createCreaseQueue,
33
38
  createLod: () => createLod,
34
39
  createTranslate: () => createTranslate,
35
40
  createVehicleDestructible: () => createVehicleDestructible,
@@ -48,10 +53,11 @@ var GameVehicles = (() => {
48
53
  rng: () => rng,
49
54
  smooth: () => smooth,
50
55
  strHash: () => strHash,
56
+ swingOf: () => swingOf,
51
57
  wrapThreeForLod: () => wrapThreeForLod
52
58
  });
53
59
 
54
- // ../game-shapes/dist/chunk-R4RY23RH.js
60
+ // ../game-shapes/dist/chunk-IFXQIDSA.js
55
61
  function canonicalJson(value) {
56
62
  if (value === null) return "null";
57
63
  if (typeof value === "number") return Object.is(value, -0) ? "-0" : Number.isFinite(value) ? String(value) : JSON.stringify(String(value));
@@ -1439,6 +1445,127 @@ var GameVehicles = (() => {
1439
1445
  custom: { fields: { generator: str, params: () => null }, required: ["generator"] }
1440
1446
  };
1441
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
+
1442
1569
  // ../game-shapes-three/dist/index.js
1443
1570
  var UV_BEFORE_NORMAL = /* @__PURE__ */ new Set(["LatheGeometry", "ExtrudeGeometry"]);
1444
1571
  var INDEX_FIRST = /* @__PURE__ */ new Set(["BoxGeometry", "CylinderGeometry", "ConeGeometry", "SphereGeometry", "PlaneGeometry", "CircleGeometry", "RingGeometry", "LatheGeometry", "TorusGeometry"]);
@@ -2455,7 +2582,7 @@ var GameVehicles = (() => {
2455
2582
  }
2456
2583
  return ti.builtBlocks === total;
2457
2584
  }
2458
- function start(geo) {
2585
+ function start(geo, store = cache) {
2459
2586
  const pos = geo.attributes.position;
2460
2587
  const triangles = trianglesOf(geo);
2461
2588
  const ti = {
@@ -2468,7 +2595,7 @@ var GameVehicles = (() => {
2468
2595
  blocks: new Float64Array(blockCount(triangles) * 6),
2469
2596
  builtBlocks: 0
2470
2597
  };
2471
- cache.set(geo, ti);
2598
+ store.set(geo, ti);
2472
2599
  return ti;
2473
2600
  }
2474
2601
  function triIndexOf(geo) {
@@ -2520,6 +2647,106 @@ var GameVehicles = (() => {
2520
2647
  }
2521
2648
  return { box: box2, version: ti.positionVersion };
2522
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
+ }
2523
2750
 
2524
2751
  // src/geometry.ts
2525
2752
  function installGeometry(kit) {
@@ -3259,58 +3486,6 @@ var GameVehicles = (() => {
3259
3486
  Object.assign(I, { roundedBoxGeometry, taper, subdivide, crease, creaseTask, refreshBounds, pathShape, tumble, loftGeometry, hullGeometry, cutGeometry, mergeGeometries, uvProject });
3260
3487
  }
3261
3488
 
3262
- // ../game-contracts/dist/index.js
3263
- var DEFAULT_IGNITION_RULES = {
3264
- fillCurve: [
3265
- [0, 0.6],
3266
- [0.1, 1],
3267
- [0.35, 0.6],
3268
- [0.65, 0.3],
3269
- [0.9, 0.15],
3270
- [1, 0.12]
3271
- ],
3272
- flameFillInfluence: 0.5,
3273
- flameDefaults: { incendiary: 0.7, tracer: 0.2 },
3274
- sparkTarget: { steel: 1, iron: 1, aluminium: 0.25 },
3275
- sparkMetalDefault: 0.5,
3276
- sparkFluid: { diesel: 0.15 },
3277
- fireNear: { radius: 1.5, chancePerSecond: 0.5 }
3278
- };
3279
- var resolveIgnitionRules = (rules) => ({
3280
- ...DEFAULT_IGNITION_RULES,
3281
- ...rules,
3282
- flameDefaults: { ...DEFAULT_IGNITION_RULES.flameDefaults, ...rules?.flameDefaults },
3283
- sparkTarget: { ...DEFAULT_IGNITION_RULES.sparkTarget, ...rules?.sparkTarget },
3284
- sparkFluid: { ...DEFAULT_IGNITION_RULES.sparkFluid, ...rules?.sparkFluid },
3285
- fireNear: { ...DEFAULT_IGNITION_RULES.fireNear, ...rules?.fireNear }
3286
- });
3287
- var fillFactor = (fill, curve = DEFAULT_IGNITION_RULES.fillCurve) => {
3288
- if (curve.length === 0) return 1;
3289
- const f = Math.min(1, Math.max(0, fill));
3290
- if (f <= curve[0][0]) return curve[0][1];
3291
- for (let i = 1; i < curve.length; i++) {
3292
- const [x1, y1] = curve[i];
3293
- const [x0, y0] = curve[i - 1];
3294
- if (f <= x1) return x1 === x0 ? y1 : y0 + (y1 - y0) * (f - x0) / (x1 - x0);
3295
- }
3296
- return curve[curve.length - 1][1];
3297
- };
3298
- var sparkTargetFactor = (materialId, metal, rules = DEFAULT_IGNITION_RULES) => !metal ? 0 : rules.sparkTarget[materialId] ?? rules.sparkMetalDefault;
3299
- var ignitionChance = (hit, target, rules = DEFAULT_IGNITION_RULES) => {
3300
- const k = fillFactor(target.fill, rules.fillCurve);
3301
- const spark = (hit.ignition?.spark ?? 0) * target.sparkTarget * (target.fluid === void 0 ? 1 : rules.sparkFluid[target.fluid] ?? 1) * k;
3302
- const flameBase = hit.ignition?.flame ?? Math.max(hit.incendiary ? rules.flameDefaults.incendiary : 0, hit.tracer ? rules.flameDefaults.tracer : 0);
3303
- const flame = flameBase * (1 - rules.flameFillInfluence * (1 - k));
3304
- return 1 - (1 - Math.min(1, Math.max(0, spark))) * (1 - Math.min(1, Math.max(0, flame)));
3305
- };
3306
- var KIT_ORIGIN_KEY = "kitOrigin";
3307
- function markKitOrigin(object2, origin) {
3308
- const node = object2;
3309
- node.userData = node.userData ?? {};
3310
- node.userData[KIT_ORIGIN_KEY] = { ...origin };
3311
- return object2;
3312
- }
3313
-
3314
3489
  // src/context.ts
3315
3490
  function installContext(kit) {
3316
3491
  const THREE = kit.THREE;
@@ -3904,6 +4079,7 @@ var GameVehicles = (() => {
3904
4079
  }
3905
4080
  kit.profX = profX;
3906
4081
  const _sr = new THREE.Raycaster();
4082
+ _sr.layers.enableAll();
3907
4083
  kit.surfaceAt = function(c, org, dir, o) {
3908
4084
  o = o || {};
3909
4085
  c.root.updateMatrixWorld(true);
@@ -3920,7 +4096,7 @@ var GameVehicles = (() => {
3920
4096
  _sr.set(p0.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
3921
4097
  _sr.near = 0;
3922
4098
  _sr.far = o.max || 20;
3923
- const h = _sr.intersectObjects(cands, false)[0];
4099
+ const h = raycastIndexed(THREE, _sr, cands)[0];
3924
4100
  if (!h) return null;
3925
4101
  const n = h.face ? h.face.normal.clone().transformDirection(h.object.matrixWorld).transformDirection(inv) : d.clone().negate();
3926
4102
  if (n.dot(d) > 0) n.negate();
@@ -3942,6 +4118,7 @@ var GameVehicles = (() => {
3942
4118
  const V3 = THREE.Vector3;
3943
4119
  const I = kit._;
3944
4120
  const { MATERIALS, crease, cutGeometry, pathShape, subdivide, tumble, uvProject } = I;
4121
+ I.paneSeals = /* @__PURE__ */ new WeakMap();
3945
4122
  kit.addGlass = function(c, geo, label, type, o) {
3946
4123
  o = o || {};
3947
4124
  if (!geo.attributes.uv || o.reuv) o.aspect = uvProject(geo, o.uvAxes);
@@ -4008,7 +4185,7 @@ var GameVehicles = (() => {
4008
4185
  cy /= poly.length;
4009
4186
  const base = I.resolveLabel(label ? label(i, polys.length) : { key: "window" });
4010
4187
  const lab = { key: base.key, label: kit.t("parts.glass_side", { label: base.label, s }) };
4011
- kit.addGlass(c, geo, lab, o.type || "tempered", {
4188
+ const pane = kit.addGlass(c, geo, lab, o.type || "tempered", {
4012
4189
  mat: o.mat || (o.see === false ? MATERIALS.glassDark : MATERIALS.glass),
4013
4190
  pos: new V3(cx, cy, s * I.halfAt(Wg, T, cy)),
4014
4191
  outline: shp.getSpacedPoints(56),
@@ -4024,6 +4201,7 @@ var GameVehicles = (() => {
4024
4201
  pts.pop();
4025
4202
  const seal = c.tube(pts, 0.011, MATERIALS.trim, { closed: true, seg: 72, rs: 5 });
4026
4203
  seal.castShadow = false;
4204
+ I.paneSeals.set(pane, seal);
4027
4205
  });
4028
4206
  });
4029
4207
  };
@@ -5733,6 +5911,44 @@ var GameVehicles = (() => {
5733
5911
  Object.assign(I, {});
5734
5912
  }
5735
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
+
5736
5952
  // src/particles.ts
5737
5953
  function installParticles(kit) {
5738
5954
  const THREE = kit.THREE;
@@ -5824,7 +6040,7 @@ var GameVehicles = (() => {
5824
6040
  });
5825
6041
  }
5826
6042
  const list2 = fire ? FIRE_TEXTURES : SMOKE_TEXTURES;
5827
- const mat = new THREE.SpriteMaterial({
6043
+ const build = () => new THREE.SpriteMaterial({
5828
6044
  map: list2[Math.floor(Math.random() * list2.length)],
5829
6045
  color: new THREE.Color(col),
5830
6046
  transparent: true,
@@ -5832,6 +6048,7 @@ var GameVehicles = (() => {
5832
6048
  opacity: op,
5833
6049
  blending: fire ? THREE.AdditiveBlending : THREE.NormalBlending
5834
6050
  });
6051
+ const mat = kit.particleMaterials ? kit.particleMaterials.own(build) : build();
5835
6052
  const dis = { value: 0 };
5836
6053
  mat.userData.dis = dis;
5837
6054
  mat.onBeforeCompile = function(sh) {
@@ -5859,11 +6076,37 @@ var GameVehicles = (() => {
5859
6076
  function killParticle(c, i) {
5860
6077
  const P = c.particles[i];
5861
6078
  c.effects.remove(P.m);
5862
- 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
+ }
5863
6083
  c.particles.splice(i, 1);
5864
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
+ }
5865
6105
  kit.spawn = function(c, type, pos, vel, o) {
5866
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");
5867
6110
  if (c.particles.length >= PARTICLE_LIMIT) {
5868
6111
  const idx = c.particles.findIndex(function(q) {
5869
6112
  return TRANSIENT_TYPES[q.type];
@@ -6135,6 +6378,7 @@ var GameVehicles = (() => {
6135
6378
  };
6136
6379
  kit.defineFluid("stain", { fresh: "#4a4a44", dry: "#26241f", wipeable: false });
6137
6380
  const _fr = new THREE.Raycaster();
6381
+ _fr.layers.enableAll();
6138
6382
  kit.splat = function(c, p, n, r, o) {
6139
6383
  o = o || {};
6140
6384
  n = n.clone().normalize();
@@ -6576,31 +6820,55 @@ var GameVehicles = (() => {
6576
6820
  }
6577
6821
 
6578
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
+ }
6579
6836
  function installGlassDamage(kit) {
6580
6837
  const THREE = kit.THREE;
6581
6838
  const V3 = THREE.Vector3;
6582
6839
  const PI = Math.PI;
6583
6840
  const I = kit._;
6584
6841
  const document2 = kit.document;
6585
- const { DOUBLE_SIDE, MATERIALS, crease, cutGeometry, mergeGeometries, subdivide, tumble } = I;
6586
- function glassUV(msh, pLocal) {
6842
+ const { MATERIALS, crease, cutGeometry, mergeGeometries, subdivide, tumble } = I;
6843
+ function glassUV(c, msh, pLocal) {
6587
6844
  const pos = msh.geometry.attributes.position;
6588
6845
  const uv = msh.geometry.attributes.uv;
6589
6846
  if (!uv) return [0.5, 0.5];
6590
- const inv = new THREE.Matrix4().copy(msh.matrix).invert();
6591
- const q = pLocal.clone().applyMatrix4(inv);
6592
- let best = 1e9;
6593
- let bi = 0;
6594
- const v = new V3();
6595
- for (let i = 0; i < pos.count; i++) {
6596
- v.fromBufferAttribute(pos, i);
6597
- const d = v.distanceToSquared(q);
6598
- if (d < best) {
6599
- best = d;
6600
- bi = i;
6601
- }
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;
6602
6870
  }
6603
- return [uv.getX(bi), uv.getY(bi)];
6871
+ return out;
6604
6872
  }
6605
6873
  function drawWeb(x, cx, cy, s, ax, rnd) {
6606
6874
  const n = 16 + Math.floor(rnd() * 8);
@@ -6677,19 +6945,9 @@ var GameVehicles = (() => {
6677
6945
  x0.fillStyle = "rgba(48,66,72,0.5)";
6678
6946
  x0.fillRect(0, 0, 256, 256);
6679
6947
  G.tex = new THREE.CanvasTexture(G.cv);
6680
- msh.material = new THREE.MeshStandardMaterial({
6681
- color: 16777215,
6682
- map: G.tex,
6683
- emissive: 6976118,
6684
- emissiveMap: G.tex,
6685
- transparent: true,
6686
- roughness: 0.25,
6687
- metalness: 0.1,
6688
- side: DOUBLE_SIDE,
6689
- depthWrite: false
6690
- });
6948
+ msh.material = crackedGlassMaterial(THREE, G.tex);
6691
6949
  }
6692
- const uv = glassUV(msh, p);
6950
+ const uv = glassUV(c, msh, p);
6693
6951
  const x = G.cv.getContext("2d");
6694
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);
6695
6953
  G.tex.needsUpdate = true;
@@ -6834,6 +7092,13 @@ var GameVehicles = (() => {
6834
7092
  }
6835
7093
  const seatGeo = [];
6836
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
+ }
6837
7102
  cubesAt.forEach(function(p, i) {
6838
7103
  if (i % 3 === 0 && c.seatTops.length && !c.rollState && !c.flipped) {
6839
7104
  let best = null;
@@ -7036,6 +7301,7 @@ var GameVehicles = (() => {
7036
7301
  polygonOffsetUnits: -2
7037
7302
  });
7038
7303
  const _ray = new THREE.Raycaster();
7304
+ _ray.layers.enableAll();
7039
7305
  function findSurface(c, p, dir) {
7040
7306
  c.root.updateMatrixWorld(true);
7041
7307
  const Mw = c.vehicle.matrixWorld;
@@ -7117,6 +7383,7 @@ var GameVehicles = (() => {
7117
7383
  const v0 = new V3();
7118
7384
  const w = new V3();
7119
7385
  const sc = new V3();
7386
+ const local = new V3();
7120
7387
  const cab = m.cabin && !c.flipped && !c.rollState ? m.cabin : null;
7121
7388
  c.deform.forEach(function(msh) {
7122
7389
  if (!msh.parent || !msh.visible) return;
@@ -7131,7 +7398,15 @@ var GameVehicles = (() => {
7131
7398
  irel.copy(rel).invert();
7132
7399
  if (!g.userData.p0) g.userData.p0 = new Float32Array(pos.array);
7133
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;
7134
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;
7135
7410
  v.fromBufferAttribute(pos, i).applyMatrix4(rel);
7136
7411
  const a = dentField(v, H);
7137
7412
  if (a <= 0) continue;
@@ -7147,10 +7422,15 @@ var GameVehicles = (() => {
7147
7422
  }
7148
7423
  if (changed) {
7149
7424
  pos.needsUpdate = true;
7150
- 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);
7151
7427
  else g.computeVertexNormals();
7152
- g.computeBoundingSphere();
7153
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);
7154
7434
  }
7155
7435
  });
7156
7436
  const dset = new Set(c.deform);
@@ -7234,13 +7514,14 @@ var GameVehicles = (() => {
7234
7514
  kit.impact = impact;
7235
7515
  kit.partEffects = {};
7236
7516
  const _pr = new THREE.Raycaster();
7517
+ _pr.layers.enableAll();
7237
7518
  function probe(c, o, d, far) {
7238
7519
  c.root.updateMatrixWorld(true);
7239
7520
  const Mw = c.vehicle.matrixWorld;
7240
7521
  const inv = new THREE.Matrix4().copy(Mw).invert();
7241
7522
  _pr.set(o.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
7242
7523
  _pr.far = far || 10;
7243
- const hits = _pr.intersectObject(c.vehicle, true);
7524
+ const hits = raycastIndexed(THREE, _pr, [c.vehicle], true);
7244
7525
  for (let i = 0; i < hits.length; i++) {
7245
7526
  const h = hits[i];
7246
7527
  const ob = h.object;
@@ -8606,6 +8887,7 @@ var GameVehicles = (() => {
8606
8887
  return hole;
8607
8888
  }
8608
8889
  const _zr = new THREE.Raycaster();
8890
+ _zr.layers.enableAll();
8609
8891
  const PROXY_MIN_TRIANGLES = 500;
8610
8892
  const STROKE_REACH = 0.6;
8611
8893
  const _pv = new V3();
@@ -8840,6 +9122,7 @@ var GameVehicles = (() => {
8840
9122
  return out;
8841
9123
  }
8842
9124
  const _rt = new THREE.Raycaster();
9125
+ _rt.layers.enableAll();
8843
9126
  function scratchPath(c, s, len, ang, cands, B) {
8844
9127
  const n = s.n.clone().normalize();
8845
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);
@@ -12121,6 +12404,8 @@ var GameVehicles = (() => {
12121
12404
  V3: THREE.Vector3,
12122
12405
  PI: Math.PI,
12123
12406
  document: doc,
12407
+ particleMaterials: opts.particleMaterials,
12408
+ effects: opts.effects,
12124
12409
  strings,
12125
12410
  t,
12126
12411
  matchers: createMatchers(strings),
@@ -12376,7 +12661,9 @@ var GameVehicles = (() => {
12376
12661
  assignDeep(strings, resolveStrings(opts.locale));
12377
12662
  return kit;
12378
12663
  };
12379
- 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;
12380
12667
  void _three;
12381
12668
  void _lod;
12382
12669
  void _locale;
@@ -12472,6 +12759,14 @@ var GameVehicles = (() => {
12472
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]],
12473
12760
  size: [b.size[0] * 0.5, Math.min(0.14, b.size[1]), b.size[2] * 0.95]
12474
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
+ }
12475
12770
  function buildVehicleModel(kit, c, options = {}) {
12476
12771
  const kind = kindOf(c);
12477
12772
  const m = c.meta;
@@ -12612,6 +12907,16 @@ var GameVehicles = (() => {
12612
12907
  tags: [role, ...tags]
12613
12908
  });
12614
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
+ }
12615
12920
  if (!hubZone) {
12616
12921
  const h = Math.max(0.15, belt - clearance);
12617
12922
  add({
@@ -12947,6 +13252,76 @@ var GameVehicles = (() => {
12947
13252
  }
12948
13253
 
12949
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
+ }
12950
13325
  var VehicleVisuals = class {
12951
13326
  constructor(kit, c, model) {
12952
13327
  __publicField(this, "kit");
@@ -12964,14 +13339,23 @@ var GameVehicles = (() => {
12964
13339
  __publicField(this, "syncPlates", /* @__PURE__ */ new Map());
12965
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). */
12966
13341
  __publicField(this, "skins", /* @__PURE__ */ new Map());
13342
+ /** How high each door's cut reaches. */
13343
+ __publicField(this, "doorTops", /* @__PURE__ */ new Map());
12967
13344
  __publicField(this, "lightsOn", true);
12968
13345
  /** Heavy body meshes whose triangle index is still to build (null once done). */
12969
13346
  __publicField(this, "warming");
12970
13347
  /** A panel was torn off since the last `pump`: that frame already paid for the cut, the smoothing starts on the next one. */
12971
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);
12972
13354
  /** Normal smoothing still running, by mesh: a mesh cut again gets a new task and the old one is dropped. */
12973
13355
  __publicField(this, "smoothing", /* @__PURE__ */ new Map());
12974
13356
  __publicField(this, "hood");
13357
+ ;
13358
+ c.smoothLater = (mesh2) => this.smoothLater(mesh2);
12975
13359
  this.kit = kit;
12976
13360
  this.c = c;
12977
13361
  this.model = model;
@@ -12980,6 +13364,8 @@ var GameVehicles = (() => {
12980
13364
  for (const p of model.parts) this.byId.set(p.id, p);
12981
13365
  c.root.updateMatrixWorld(true);
12982
13366
  this.hood = this.nameHood();
13367
+ this.seatPanes();
13368
+ this.seatDoorHandles();
12983
13369
  this.warming = c.deform.filter((msh) => msh.geometry && msh.geometry.attributes.position && msh.geometry.attributes.position.count >= 1500);
12984
13370
  if (!this.warming.length) this.warming = null;
12985
13371
  for (const p of model.parts) {
@@ -13000,6 +13386,7 @@ var GameVehicles = (() => {
13000
13386
  const id = z ? z.id : this.model.parts.find((p) => p.role === "hood")?.id;
13001
13387
  if (!id) return null;
13002
13388
  c.hood.piv.name = id;
13389
+ c.hood.piv.userData.plateOf = id;
13003
13390
  return { id, open: c.hood.open, sign: c.hood.sign || 1 };
13004
13391
  }
13005
13392
  mirrorObjects(p) {
@@ -13087,6 +13474,7 @@ var GameVehicles = (() => {
13087
13474
  recess.userData.noFollow = true;
13088
13475
  const piv = new THREE.Group();
13089
13476
  piv.name = p.id;
13477
+ piv.userData.plateOf = p.id;
13090
13478
  piv.position.set(hx, cy, sk + s * 0.016);
13091
13479
  piv.userData.rigid = true;
13092
13480
  this.c.vehicle.add(piv);
@@ -13103,6 +13491,7 @@ var GameVehicles = (() => {
13103
13491
  recess.userData.noFollow = true;
13104
13492
  const piv = new THREE.Group();
13105
13493
  piv.name = p.id;
13494
+ piv.userData.plateOf = p.id;
13106
13495
  piv.position.set(sk - 0.016, B.y1 - 0.05, 0);
13107
13496
  piv.userData.rigid = true;
13108
13497
  this.c.vehicle.add(piv);
@@ -13166,10 +13555,19 @@ var GameVehicles = (() => {
13166
13555
  try {
13167
13556
  if (p.role === "door") {
13168
13557
  const dirty = /* @__PURE__ */ new Set();
13169
- 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];
13170
- 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);
13171
13564
  this.mend(dirty);
13172
- 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
+ }
13173
13571
  } else {
13174
13572
  const pcs = this.cutZone(z);
13175
13573
  if (pcs.length) objs = [...pcs, ...I.followersIn(this.c, z, inv)];
@@ -13187,10 +13585,124 @@ var GameVehicles = (() => {
13187
13585
  const di = deform.indexOf(o);
13188
13586
  if (di >= 0) deform.splice(di, 1);
13189
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
+ }
13190
13593
  piv.quaternion.copy(q);
13191
13594
  this.c.root.updateMatrixWorld(true);
13192
13595
  return objs.length > 0;
13193
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
+ }
13194
13706
  mend(dirty) {
13195
13707
  dirty.forEach((msh) => {
13196
13708
  const g = msh.geometry;
@@ -13200,7 +13712,191 @@ var GameVehicles = (() => {
13200
13712
  g.computeBoundingBox();
13201
13713
  });
13202
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
+ }
13203
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
+ }
13204
13900
  const [cx, cy, cz] = p.box.center;
13205
13901
  const s = cz < 0 ? -1 : 1;
13206
13902
  const mats = this.I.MATERIALS;
@@ -13241,17 +13937,28 @@ var GameVehicles = (() => {
13241
13937
  }
13242
13938
  // ---- marks -------------------------------------------------------------------------------------------------------------------------------
13243
13939
  /** A bullet hole: a dark disc lying on the surface at `point` (`c.vehicle` coordinates). */
13244
- holeDecal(point, normal, diameter) {
13940
+ holeDecal(point, normal, diameter, part) {
13245
13941
  const THREE = this.THREE;
13246
- 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);
13247
13947
  const n = new THREE.Vector3(...normal);
13248
- m.position.set(...point).addScaledVector(n, 4e-3);
13249
- 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)));
13250
13951
  m.userData.decal = true;
13251
13952
  m.userData.noFollow = true;
13953
+ m.renderOrder = 3;
13252
13954
  m.raycast = () => {
13253
13955
  };
13254
- 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;
13255
13962
  }
13256
13963
  spark(point, dir, count = 8) {
13257
13964
  const V3 = this.THREE.Vector3;
@@ -13291,10 +13998,8 @@ var GameVehicles = (() => {
13291
13998
  }
13292
13999
  /** Glass: cracks (laminated) or shatters (tempered); returns whether it broke. */
13293
14000
  hitGlass(p, point, dir, force) {
13294
- const z = this.zone(p);
13295
- if (!z) return false;
13296
14001
  const V3 = this.THREE.Vector3;
13297
- const list2 = z.glass.filter((g) => g.parent && g.visible);
14002
+ const list2 = this.panesOf(p).filter((g) => g.parent && g.visible);
13298
14003
  if (!list2.length) return false;
13299
14004
  const pt = new V3(...point);
13300
14005
  let best = list2[0];
@@ -13311,9 +14016,16 @@ var GameVehicles = (() => {
13311
14016
  this.kit.hitGlass(this.c, best, pt, new V3(...dir), force);
13312
14017
  return !!best.userData.glass?.broken;
13313
14018
  }
14019
+ panesOf(p) {
14020
+ return p.panes ?? this.zone(p)?.glass ?? [];
14021
+ }
13314
14022
  glassBreak(p) {
13315
14023
  const z = this.zone(p);
13316
- 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
+ }
13317
14029
  for (const g of z.glass) if (g.parent && g.visible) this.I.glassBand(this.c, z, g, 4);
13318
14030
  this.gone.add(p.id);
13319
14031
  }
@@ -13341,6 +14053,7 @@ var GameVehicles = (() => {
13341
14053
  smoothLater(msh) {
13342
14054
  this.smoothing.set(msh, this.I.creaseTask(msh.geometry, 42));
13343
14055
  this.tornThisFrame = true;
14056
+ this.holdArmed = true;
13344
14057
  }
13345
14058
  /** Spends at most `budgetMs` finishing the pending normal smoothing. Call once per frame. */
13346
14059
  pump(budgetMs = 2.5) {
@@ -13348,6 +14061,7 @@ var GameVehicles = (() => {
13348
14061
  this.tornThisFrame = false;
13349
14062
  return;
13350
14063
  }
14064
+ if (this.queued) return;
13351
14065
  if (!this.smoothing.size && !this.warming) return;
13352
14066
  const deadline = performance.now() + budgetMs;
13353
14067
  for (const [msh, task] of this.smoothing) {
@@ -13356,6 +14070,34 @@ var GameVehicles = (() => {
13356
14070
  }
13357
14071
  this.warmIndexes(deadline);
13358
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
+ }
13359
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`). */
13360
14102
  warmIndexes(deadline) {
13361
14103
  if (!this.warming) return;
@@ -13527,6 +14269,7 @@ var GameVehicles = (() => {
13527
14269
  }
13528
14270
  pieceTasks.forEach((task, mesh2) => this.smoothing.set(mesh2, task));
13529
14271
  this.tornThisFrame = true;
14272
+ this.holdArmed = true;
13530
14273
  dirty.forEach((msh) => {
13531
14274
  const g = msh.geometry;
13532
14275
  if (g.userData.crease || !g.index) this.smoothLater(msh);
@@ -13633,6 +14376,7 @@ var GameVehicles = (() => {
13633
14376
  let clock = 0;
13634
14377
  let model;
13635
14378
  let vis = null;
14379
+ let releaseCrease;
13636
14380
  const ps = /* @__PURE__ */ new Map();
13637
14381
  let js = [];
13638
14382
  let leaks = /* @__PURE__ */ new Map();
@@ -13717,7 +14461,9 @@ var GameVehicles = (() => {
13717
14461
  rand = rng(seedBase);
13718
14462
  igniteRand = rng(strHash(`${c.key}|${id}|ignition|${options.seed ?? 0}`));
13719
14463
  steerSign = rand() < 0.5 ? -1 : 1;
14464
+ releaseCrease?.();
13720
14465
  vis = headless ? null : new VehicleVisuals(kit, c, model);
14466
+ releaseCrease = vis && options.creaseQueue ? options.creaseQueue.track(vis) : void 0;
13721
14467
  recomputeGroups();
13722
14468
  };
13723
14469
  const recomputeGroups = () => {
@@ -14028,13 +14774,13 @@ var GameVehicles = (() => {
14028
14774
  sync();
14029
14775
  const M = toFrameMatrix();
14030
14776
  const v = toLocal(h.point);
14031
- const p = locate(v, 0.2);
14777
+ const p = (h.part ? ps.get(h.part) : void 0) ?? locate(v, 0.2);
14032
14778
  if (!p) return;
14033
14779
  const d = p.def;
14034
14780
  const E = h.energy ?? 0;
14035
14781
  const cal = h.caliber ?? 9;
14036
14782
  const dir = dirToLocal(h.direction ?? [0, 0, -1]).normalize();
14037
- const n = normalOf(d, v);
14783
+ const n = h.normal ? dirToLocal(h.normal).normalize() : normalOf(d, v);
14038
14784
  const res = resist(d, p);
14039
14785
  const through = E >= res;
14040
14786
  const cosInc = Math.abs(dir.dot(n));
@@ -14053,7 +14799,7 @@ var GameVehicles = (() => {
14053
14799
  const hp = { point: r3(fromVehicle(v, M)), normal: r3(fromVehicle(n, new THREE.Matrix4().extractRotation(M))), diameter: r2(holeDiam), through };
14054
14800
  p.holes.push(hp);
14055
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])))))) } : {} }));
14056
- 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);
14057
14803
  startLeak(p, v, cal, out, dir);
14058
14804
  };
14059
14805
  switch (d.material) {
@@ -14354,6 +15100,46 @@ var GameVehicles = (() => {
14354
15100
  for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT)) return true;
14355
15101
  return false;
14356
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
+ };
14357
15143
  const hit = (h) => {
14358
15144
  stateCache = void 0;
14359
15145
  const out = [];
@@ -14476,9 +15262,14 @@ var GameVehicles = (() => {
14476
15262
  },
14477
15263
  describe,
14478
15264
  hit,
15265
+ surfaceRay,
14479
15266
  update,
14480
15267
  state,
14481
15268
  reset,
15269
+ dispose: () => {
15270
+ releaseCrease?.();
15271
+ releaseCrease = void 0;
15272
+ },
14482
15273
  disassemble,
14483
15274
  extinguish,
14484
15275
  serialize: () => ({
@@ -14516,6 +15307,145 @@ var GameVehicles = (() => {
14516
15307
  root.userData.vehicle = vehicle;
14517
15308
  return { root, vehicle };
14518
15309
  }
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
+
15366
+ // src/destructible/enclosure.ts
15367
+ var CABIN_LEAK = 0.02;
15368
+ var HOLE_RATE_PER_AREA = 3e3;
15369
+ var CRACK_GAP_PER_METRE = 0.5;
15370
+ var THROUGH = 0.18;
15371
+ var HOLE_PATCH = 0.12;
15372
+ var FULL_SWING = 1.15;
15373
+ var SHELL_ROLES = /* @__PURE__ */ new Set(["door", "glass", "roof", "lid", "panel", "pillar"]);
15374
+ var GONE = /* @__PURE__ */ new Set(["broken", "melted", "ashes"]);
15375
+ function cabinBoxOf(vehicle) {
15376
+ const cabin = vehicle.meta.cabin;
15377
+ if (!cabin || !(cabin.x1 > cabin.x0) || !(cabin.y1 > cabin.y0) || !(cabin.z > 0)) return null;
15378
+ return cabin;
15379
+ }
15380
+ function swingOf(vehicle, partId) {
15381
+ const pivot = vehicle.vehicle?.getObjectByName?.(partId);
15382
+ const q = pivot?.quaternion;
15383
+ if (!q) return 0;
15384
+ const angle = 2 * Math.acos(Math.min(1, Math.abs(q.w)));
15385
+ return Math.min(1, angle / FULL_SWING);
15386
+ }
15387
+ var overlaps = (b, c, margin) => Math.abs(b.center[0] - (c.x0 + c.x1) / 2) <= b.size[0] / 2 + (c.x1 - c.x0) / 2 + margin && Math.abs(b.center[1] - (c.y0 + c.y1) / 2) <= b.size[1] / 2 + (c.y1 - c.y0) / 2 + margin && Math.abs(b.center[2]) <= b.size[2] / 2 + c.z + margin;
15388
+ function cabinEnclosure(source, options = {}) {
15389
+ const c = source.vehicle;
15390
+ const cabin = cabinBoxOf(c);
15391
+ const node = c.vehicle;
15392
+ if (!cabin || !node?.localToWorld) return null;
15393
+ const state = source.state();
15394
+ if (state.destroyed) return null;
15395
+ const V3 = node.position.constructor;
15396
+ const Quat = node.quaternion.constructor;
15397
+ const mid = [(cabin.x0 + cabin.x1) / 2, (cabin.y0 + cabin.y1) / 2, 0];
15398
+ const half = [(cabin.x1 - cabin.x0) / 2, (cabin.y1 - cabin.y0) / 2, cabin.z];
15399
+ const ajar = options.ajar ?? 0.06;
15400
+ const byId = new Map(state.parts.map((p) => [p.id, p]));
15401
+ const mainGroup = state.groups[0] ? new Set(state.groups[0]) : null;
15402
+ const openings = [];
15403
+ let crackLength = 0;
15404
+ const toCabin = (b) => ({
15405
+ center: [b.center[0] - mid[0], b.center[1] - mid[1], b.center[2] - mid[2]],
15406
+ halfSize: [b.size[0] / 2 + THROUGH, b.size[1] / 2 + THROUGH, b.size[2] / 2 + THROUGH]
15407
+ });
15408
+ node.updateWorldMatrix?.(true, false);
15409
+ const frame = options.frame ?? c.root;
15410
+ frame?.updateWorldMatrix?.(true, false);
15411
+ const inverse = node.matrixWorld.clone().invert();
15412
+ for (const def of source.model.parts) {
15413
+ if (!SHELL_ROLES.has(def.role) || !overlaps(def.box, cabin, 0.12)) continue;
15414
+ const part = byId.get(def.id);
15415
+ if (!part) continue;
15416
+ const gone = GONE.has(part.status) || mainGroup !== null && !mainGroup.has(def.id);
15417
+ const swung = (def.role === "door" || def.role === "lid") && swingOf(c, def.id) > ajar;
15418
+ if (gone || swung) {
15419
+ openings.push(toCabin(def.box));
15420
+ continue;
15421
+ }
15422
+ for (const hole of part.holes) {
15423
+ if (!hole.through && def.material !== "glass") continue;
15424
+ const area3 = Math.PI * (hole.diameter / 2) ** 2;
15425
+ const p = new V3(hole.point[0], hole.point[1], hole.point[2]);
15426
+ if (frame?.localToWorld) frame.localToWorld(p);
15427
+ p.applyMatrix4(inverse);
15428
+ openings.push({
15429
+ center: [p.x - mid[0], p.y - mid[1], p.z - mid[2]],
15430
+ halfSize: [HOLE_PATCH, HOLE_PATCH, HOLE_PATCH],
15431
+ // the patch is far bigger than the hole: only a share of what crosses it passes, more for a bigger hole
15432
+ rate: Math.min(4, area3 * HOLE_RATE_PER_AREA)
15433
+ });
15434
+ }
15435
+ for (const crack of part.cracks) crackLength += crack.length;
15436
+ }
15437
+ const center = node.localToWorld(new V3(mid[0], mid[1], mid[2]));
15438
+ const q = node.getWorldQuaternion(new Quat());
15439
+ const scale = node.getWorldScale(new V3());
15440
+ const leak = (options.leak ?? CABIN_LEAK) * (1 + crackLength * CRACK_GAP_PER_METRE);
15441
+ return {
15442
+ center: [center.x, center.y, center.z],
15443
+ halfSize: [half[0] * Math.abs(scale.x), half[1] * Math.abs(scale.y), half[2] * Math.abs(scale.z)],
15444
+ orientation: [q.x, q.y, q.z, q.w],
15445
+ openings: openings.map((o) => ({ ...o, center: [o.center[0] * scale.x, o.center[1] * scale.y, o.center[2] * scale.z], halfSize: [o.halfSize[0] * Math.abs(scale.x), o.halfSize[1] * Math.abs(scale.y), o.halfSize[2] * Math.abs(scale.z)] })),
15446
+ leak
15447
+ };
15448
+ }
14519
15449
  return __toCommonJS(index_exports);
14520
15450
  })();
14521
15451
  //# sourceMappingURL=index.global.js.map