@adatechnology/game-vehicles 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,16 +23,23 @@ var GameVehicles = (() => {
23
23
  // src/index.ts
24
24
  var index_exports = {};
25
25
  __export(index_exports, {
26
+ ARMOUR_LEVELS: () => ARMOUR_LEVELS,
26
27
  CABIN_LEAK: () => CABIN_LEAK,
28
+ CRASH_RULES: () => CRASH_RULES,
27
29
  JOINT_RULES: () => JOINT_RULES,
28
30
  LEGACY_STATES: () => LEGACY_STATES,
29
31
  LEGACY_ZONE_IDS: () => LEGACY_ZONE_IDS,
30
32
  SEED_SALTS: () => SEED_SALTS,
33
+ armourLevelOf: () => armourLevelOf,
31
34
  buildVehicleItem: () => buildVehicleItem,
32
35
  buildVehicleModel: () => buildVehicleModel,
33
36
  cabinBoxOf: () => cabinBoxOf,
34
37
  cabinEnclosure: () => cabinEnclosure,
35
38
  clamp: () => clamp,
39
+ crackedGlassMaterial: () => crackedGlassMaterial,
40
+ crashDeltaV: () => crashDeltaV,
41
+ crashVehicle: () => crashVehicle,
42
+ createCreaseQueue: () => createCreaseQueue,
36
43
  createLod: () => createLod,
37
44
  createTranslate: () => createTranslate,
38
45
  createVehicleDestructible: () => createVehicleDestructible,
@@ -55,7 +62,7 @@ var GameVehicles = (() => {
55
62
  wrapThreeForLod: () => wrapThreeForLod
56
63
  });
57
64
 
58
- // ../game-shapes/dist/chunk-R4RY23RH.js
65
+ // ../game-shapes/dist/chunk-M5N2SHNL.js
59
66
  function canonicalJson(value) {
60
67
  if (value === null) return "null";
61
68
  if (typeof value === "number") return Object.is(value, -0) ? "-0" : Number.isFinite(value) ? String(value) : JSON.stringify(String(value));
@@ -1302,8 +1309,8 @@ var GameVehicles = (() => {
1302
1309
  groups: []
1303
1310
  };
1304
1311
  }
1305
- function make(kind, p) {
1306
- switch (kind) {
1312
+ function make(p) {
1313
+ switch (p.kind) {
1307
1314
  case "box": {
1308
1315
  const size = p.size ?? [1, 1, 1];
1309
1316
  const segments = p.segments ?? [1, 1, 1];
@@ -1332,7 +1339,7 @@ var GameVehicles = (() => {
1332
1339
  case "mesh":
1333
1340
  return mesh(p);
1334
1341
  default:
1335
- throw new Error(`game-shapes: unknown primitive kind "${String(kind)}"`);
1342
+ throw new Error(`game-shapes: unknown primitive kind "${String(p.kind)}"`);
1336
1343
  }
1337
1344
  }
1338
1345
  var templates = /* @__PURE__ */ new Map();
@@ -1340,7 +1347,7 @@ var GameVehicles = (() => {
1340
1347
  var stats = { hits: 0, misses: 0 };
1341
1348
  function buildPrimitive(node, options = {}) {
1342
1349
  const parameters = primitiveParameters(node, options);
1343
- if (node.kind === "mesh" || templateLimit === 0) return make(node.kind, parameters);
1350
+ if (node.kind === "mesh" || templateLimit === 0) return make({ ...node, ...parameters });
1344
1351
  const key = `${node.kind}:${canonicalJson(parameters)}`;
1345
1352
  const cached = templates.get(key);
1346
1353
  if (cached) {
@@ -1350,7 +1357,7 @@ var GameVehicles = (() => {
1350
1357
  return cached;
1351
1358
  }
1352
1359
  stats.misses++;
1353
- const built = make(node.kind, parameters);
1360
+ const built = make({ ...node, ...parameters });
1354
1361
  templates.set(key, built);
1355
1362
  if (templates.size > templateLimit) templates.delete(templates.keys().next().value);
1356
1363
  return built;
@@ -1443,6 +1450,127 @@ var GameVehicles = (() => {
1443
1450
  custom: { fields: { generator: str, params: () => null }, required: ["generator"] }
1444
1451
  };
1445
1452
 
1453
+ // ../game-contracts/dist/index.js
1454
+ var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
1455
+ var DEFAULT_IGNITION_RULES = {
1456
+ fillCurve: [
1457
+ [0, 0.6],
1458
+ [0.1, 1],
1459
+ [0.35, 0.6],
1460
+ [0.65, 0.3],
1461
+ [0.9, 0.15],
1462
+ [1, 0.12]
1463
+ ],
1464
+ flameFillInfluence: 0.5,
1465
+ flameDefaults: { incendiary: 0.7, tracer: 0.2 },
1466
+ sparkTarget: { steel: 1, iron: 1, aluminium: 0.25 },
1467
+ sparkMetalDefault: 0.5,
1468
+ sparkFluid: { diesel: 0.15 },
1469
+ fireNear: { radius: 1.5, chancePerSecond: 0.5 }
1470
+ };
1471
+ var resolveIgnitionRules = (rules) => ({
1472
+ ...DEFAULT_IGNITION_RULES,
1473
+ ...rules,
1474
+ flameDefaults: { ...DEFAULT_IGNITION_RULES.flameDefaults, ...rules?.flameDefaults },
1475
+ sparkTarget: { ...DEFAULT_IGNITION_RULES.sparkTarget, ...rules?.sparkTarget },
1476
+ sparkFluid: { ...DEFAULT_IGNITION_RULES.sparkFluid, ...rules?.sparkFluid },
1477
+ fireNear: { ...DEFAULT_IGNITION_RULES.fireNear, ...rules?.fireNear }
1478
+ });
1479
+ var fillFactor = (fill, curve = DEFAULT_IGNITION_RULES.fillCurve) => {
1480
+ if (curve.length === 0) return 1;
1481
+ const f = Math.min(1, Math.max(0, fill));
1482
+ if (f <= curve[0][0]) return curve[0][1];
1483
+ for (let i = 1; i < curve.length; i++) {
1484
+ const [x1, y1] = curve[i];
1485
+ const [x0, y0] = curve[i - 1];
1486
+ if (f <= x1) return x1 === x0 ? y1 : y0 + (y1 - y0) * (f - x0) / (x1 - x0);
1487
+ }
1488
+ return curve[curve.length - 1][1];
1489
+ };
1490
+ var sparkTargetFactor = (materialId, metal, rules = DEFAULT_IGNITION_RULES) => !metal ? 0 : rules.sparkTarget[materialId] ?? rules.sparkMetalDefault;
1491
+ var ignitionChance = (hit, target, rules = DEFAULT_IGNITION_RULES) => {
1492
+ const k = fillFactor(target.fill, rules.fillCurve);
1493
+ const spark = (hit.ignition?.spark ?? 0) * target.sparkTarget * (target.fluid === void 0 ? 1 : rules.sparkFluid[target.fluid] ?? 1) * k;
1494
+ const flameBase = hit.ignition?.flame ?? Math.max(hit.incendiary ? rules.flameDefaults.incendiary : 0, hit.tracer ? rules.flameDefaults.tracer : 0);
1495
+ const flame = flameBase * (1 - rules.flameFillInfluence * (1 - k));
1496
+ return 1 - (1 - Math.min(1, Math.max(0, spark))) * (1 - Math.min(1, Math.max(0, flame)));
1497
+ };
1498
+ var DEFERRED_QUEUE_ID = {
1499
+ DESTRUCTION_EVENTS: "destruction.events",
1500
+ BREACH_PAINT: "floorplan.breach-paint",
1501
+ AUDIO_VOICES: "audio.voices",
1502
+ VEHICLE_CREASE: "vehicles.crease"
1503
+ };
1504
+ var DEFERRED_PRIORITY = {
1505
+ [DEFERRED_QUEUE_ID.DESTRUCTION_EVENTS]: { priority: 300, phase: "present" },
1506
+ [DEFERRED_QUEUE_ID.BREACH_PAINT]: { priority: 250, phase: "present" },
1507
+ [DEFERRED_QUEUE_ID.AUDIO_VOICES]: { priority: 200, phase: "idle", reserveMs: 1 },
1508
+ [DEFERRED_QUEUE_ID.VEHICLE_CREASE]: { priority: 100, phase: "idle" }
1509
+ };
1510
+ var WARNED_KEY = /* @__PURE__ */ Symbol.for("@adatechnology/game-contracts.warned");
1511
+ var warnedKeys = () => {
1512
+ const holder = globalThis;
1513
+ return holder[WARNED_KEY] ?? (holder[WARNED_KEY] = /* @__PURE__ */ new Set());
1514
+ };
1515
+ var isProduction = () => {
1516
+ try {
1517
+ return process.env.NODE_ENV === "production";
1518
+ } catch {
1519
+ return false;
1520
+ }
1521
+ };
1522
+ var isQuiet = () => globalThis.__GAME_KIT_QUIET__ === true;
1523
+ var warnOnce = (key, message) => {
1524
+ if (isQuiet() || isProduction()) return;
1525
+ const seen = warnedKeys();
1526
+ if (seen.has(key)) return;
1527
+ seen.add(key);
1528
+ console.warn(`[game-kit] ${message}`);
1529
+ };
1530
+ var KIT_ORIGIN_KEY = "kitOrigin";
1531
+ function markKitOrigin(object2, origin) {
1532
+ const node = object2;
1533
+ node.userData = node.userData ?? {};
1534
+ node.userData[KIT_ORIGIN_KEY] = { ...origin };
1535
+ return object2;
1536
+ }
1537
+ var SURFACE_IDS = ["concrete", "asphalt", "wood", "tile", "dirt", "grass", "gravel", "sand", "metal", "water", "glass", "carpet"];
1538
+ var GAIT_IDS = ["walk", "run"];
1539
+ var LANDING_IDS = ["light", "heavy"];
1540
+ var BODY_SOUND_IDS = [
1541
+ "jump",
1542
+ "cloth_rustle",
1543
+ "body_fall",
1544
+ "fall_damage",
1545
+ "player_hurt",
1546
+ "player_breath_run",
1547
+ "player_death",
1548
+ "bite",
1549
+ "grab",
1550
+ "punch_swing",
1551
+ "punch_hit_body",
1552
+ "punch_hit_face",
1553
+ "punch_hit_object",
1554
+ "kick_swing",
1555
+ "kick_hit",
1556
+ "kick_hit_object",
1557
+ "shove_two_hands",
1558
+ "knife_swing",
1559
+ "knife_stab",
1560
+ "knife_slash",
1561
+ "knife_hit_object",
1562
+ "blunt_swing",
1563
+ "blunt_hit",
1564
+ "blunt_hit_object",
1565
+ "axe_swing",
1566
+ "axe_chop",
1567
+ "axe_hit_object"
1568
+ ];
1569
+ var footstepSoundId = (surface, gait) => `footstep_${surface}_${gait}`;
1570
+ var FOOTSTEP_SOUND_IDS = SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstepSoundId(surface, gait)));
1571
+ var LAND_SOUND_IDS = SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((landing) => `land_${surface}_${landing}`));
1572
+ var BODY_FOLEY_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
1573
+
1446
1574
  // ../game-shapes-three/dist/index.js
1447
1575
  var UV_BEFORE_NORMAL = /* @__PURE__ */ new Set(["LatheGeometry", "ExtrudeGeometry"]);
1448
1576
  var INDEX_FIRST = /* @__PURE__ */ new Set(["BoxGeometry", "CylinderGeometry", "ConeGeometry", "SphereGeometry", "PlaneGeometry", "CircleGeometry", "RingGeometry", "LatheGeometry", "TorusGeometry"]);
@@ -2459,7 +2587,7 @@ var GameVehicles = (() => {
2459
2587
  }
2460
2588
  return ti.builtBlocks === total;
2461
2589
  }
2462
- function start(geo) {
2590
+ function start(geo, store = cache) {
2463
2591
  const pos = geo.attributes.position;
2464
2592
  const triangles = trianglesOf(geo);
2465
2593
  const ti = {
@@ -2472,7 +2600,7 @@ var GameVehicles = (() => {
2472
2600
  blocks: new Float64Array(blockCount(triangles) * 6),
2473
2601
  builtBlocks: 0
2474
2602
  };
2475
- cache.set(geo, ti);
2603
+ store.set(geo, ti);
2476
2604
  return ti;
2477
2605
  }
2478
2606
  function triIndexOf(geo) {
@@ -2524,6 +2652,106 @@ var GameVehicles = (() => {
2524
2652
  }
2525
2653
  return { box: box2, version: ti.positionVersion };
2526
2654
  }
2655
+ var rayCache = /* @__PURE__ */ new WeakMap();
2656
+ function rayIndexOf(geo) {
2657
+ const shared = cache.get(geo);
2658
+ if (shared && isCurrent(geo, shared) && shared.builtBlocks === blockCount(shared.triangles)) return shared;
2659
+ let ti = rayCache.get(geo);
2660
+ if (!ti || !isCurrent(geo, ti)) ti = start(geo, rayCache);
2661
+ advance(geo, ti, Infinity);
2662
+ return ti;
2663
+ }
2664
+ var BLOCK_SLACK = 1e-6;
2665
+ var scratch = /* @__PURE__ */ new WeakMap();
2666
+ function scratchOf(THREE) {
2667
+ let s = scratch.get(THREE);
2668
+ if (!s) {
2669
+ s = {
2670
+ inverse: new THREE.Matrix4(),
2671
+ local: new THREE.Ray(),
2672
+ box: new THREE.Box3(),
2673
+ a: new THREE.Vector3(),
2674
+ b: new THREE.Vector3(),
2675
+ c: new THREE.Vector3(),
2676
+ point: new THREE.Vector3(),
2677
+ world: new THREE.Vector3(),
2678
+ sphere: new THREE.Sphere()
2679
+ };
2680
+ scratch.set(THREE, s);
2681
+ }
2682
+ return s;
2683
+ }
2684
+ function indexable(THREE, mesh2) {
2685
+ const geo = mesh2.geometry;
2686
+ if (mesh2.raycast !== THREE.Mesh.prototype.raycast) return false;
2687
+ if (!mesh2.isMesh || mesh2.isSkinnedMesh || mesh2.isInstancedMesh || mesh2.isBatchedMesh || !geo || !geo.isBufferGeometry) return false;
2688
+ if (!mesh2.material || Array.isArray(mesh2.material)) return false;
2689
+ if (geo.morphAttributes && geo.morphAttributes.position && geo.morphAttributes.position.length) return false;
2690
+ return supports(geo);
2691
+ }
2692
+ function raycastMesh(THREE, raycaster, mesh2, out) {
2693
+ const geo = mesh2.geometry;
2694
+ const s = scratchOf(THREE);
2695
+ if (geo.boundingSphere === null) geo.computeBoundingSphere();
2696
+ s.sphere.copy(geo.boundingSphere).applyMatrix4(mesh2.matrixWorld);
2697
+ if (!raycaster.ray.intersectsSphere(s.sphere)) return;
2698
+ const ti = rayIndexOf(geo);
2699
+ if (!ti) return;
2700
+ s.inverse.copy(mesh2.matrixWorld).invert();
2701
+ s.local.copy(raycaster.ray).applyMatrix4(s.inverse);
2702
+ const material = mesh2.material;
2703
+ const back = material.side === THREE.BackSide;
2704
+ const cull = material.side === THREE.FrontSide;
2705
+ const P = geo.attributes.position;
2706
+ const ix = geo.index;
2707
+ const count = ix ? ix.count : P.count;
2708
+ const first = Math.max(0, geo.drawRange.start);
2709
+ const last = Math.min(count, geo.drawRange.start + geo.drawRange.count);
2710
+ const t0 = Math.ceil(first / 3);
2711
+ const t1 = Math.floor(last / 3);
2712
+ const B = ti.boxes;
2713
+ const K = ti.blocks;
2714
+ for (let blk = Math.floor(t0 / TRI_BLOCK); blk * TRI_BLOCK < t1; blk++) {
2715
+ const o = blk * 6;
2716
+ s.box.min.set(K[o] - BLOCK_SLACK, K[o + 1] - BLOCK_SLACK, K[o + 2] - BLOCK_SLACK);
2717
+ s.box.max.set(K[o + 3] + BLOCK_SLACK, K[o + 4] + BLOCK_SLACK, K[o + 5] + BLOCK_SLACK);
2718
+ if (!s.local.intersectsBox(s.box)) continue;
2719
+ const end = Math.min(t1, (blk + 1) * TRI_BLOCK);
2720
+ for (let t = Math.max(t0, blk * TRI_BLOCK); t < end; t++) {
2721
+ const j = t * 6;
2722
+ s.box.min.set(B[j] - BLOCK_SLACK, B[j + 1] - BLOCK_SLACK, B[j + 2] - BLOCK_SLACK);
2723
+ s.box.max.set(B[j + 3] + BLOCK_SLACK, B[j + 4] + BLOCK_SLACK, B[j + 5] + BLOCK_SLACK);
2724
+ if (!s.local.intersectsBox(s.box)) continue;
2725
+ const a = ix ? ix.getX(t * 3) : t * 3;
2726
+ const b = ix ? ix.getX(t * 3 + 1) : t * 3 + 1;
2727
+ const c = ix ? ix.getX(t * 3 + 2) : t * 3 + 2;
2728
+ s.a.fromBufferAttribute(P, a);
2729
+ s.b.fromBufferAttribute(P, b);
2730
+ s.c.fromBufferAttribute(P, c);
2731
+ 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);
2732
+ if (hit === null) continue;
2733
+ s.world.copy(s.point).applyMatrix4(mesh2.matrixWorld);
2734
+ const distance = raycaster.ray.origin.distanceTo(s.world);
2735
+ if (distance < raycaster.near || distance > raycaster.far) continue;
2736
+ const normal = new THREE.Vector3();
2737
+ THREE.Triangle.getNormal(s.a, s.b, s.c, normal);
2738
+ out.push({ distance, point: s.world.clone(), object: mesh2, face: { a, b, c, normal, materialIndex: 0 }, faceIndex: Math.floor(t) });
2739
+ }
2740
+ }
2741
+ }
2742
+ function collect(THREE, raycaster, object2, recursive, out) {
2743
+ if (object2.layers.test(raycaster.layers)) {
2744
+ if (indexable(THREE, object2)) raycastMesh(THREE, raycaster, object2, out);
2745
+ else object2.raycast(raycaster, out);
2746
+ }
2747
+ if (recursive) for (const child of object2.children) collect(THREE, raycaster, child, true, out);
2748
+ }
2749
+ function raycastIndexed(THREE, raycaster, objects, recursive = false) {
2750
+ const out = [];
2751
+ for (const object2 of objects) collect(THREE, raycaster, object2, recursive, out);
2752
+ out.sort((x, y) => x.distance - y.distance);
2753
+ return out;
2754
+ }
2527
2755
 
2528
2756
  // src/geometry.ts
2529
2757
  function installGeometry(kit) {
@@ -3263,58 +3491,6 @@ var GameVehicles = (() => {
3263
3491
  Object.assign(I, { roundedBoxGeometry, taper, subdivide, crease, creaseTask, refreshBounds, pathShape, tumble, loftGeometry, hullGeometry, cutGeometry, mergeGeometries, uvProject });
3264
3492
  }
3265
3493
 
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
3494
  // src/context.ts
3319
3495
  function installContext(kit) {
3320
3496
  const THREE = kit.THREE;
@@ -3908,6 +4084,7 @@ var GameVehicles = (() => {
3908
4084
  }
3909
4085
  kit.profX = profX;
3910
4086
  const _sr = new THREE.Raycaster();
4087
+ _sr.layers.enableAll();
3911
4088
  kit.surfaceAt = function(c, org, dir, o) {
3912
4089
  o = o || {};
3913
4090
  c.root.updateMatrixWorld(true);
@@ -3924,7 +4101,7 @@ var GameVehicles = (() => {
3924
4101
  _sr.set(p0.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
3925
4102
  _sr.near = 0;
3926
4103
  _sr.far = o.max || 20;
3927
- const h = _sr.intersectObjects(cands, false)[0];
4104
+ const h = raycastIndexed(THREE, _sr, cands)[0];
3928
4105
  if (!h) return null;
3929
4106
  const n = h.face ? h.face.normal.clone().transformDirection(h.object.matrixWorld).transformDirection(inv) : d.clone().negate();
3930
4107
  if (n.dot(d) > 0) n.negate();
@@ -3946,6 +4123,7 @@ var GameVehicles = (() => {
3946
4123
  const V3 = THREE.Vector3;
3947
4124
  const I = kit._;
3948
4125
  const { MATERIALS, crease, cutGeometry, pathShape, subdivide, tumble, uvProject } = I;
4126
+ I.paneSeals = /* @__PURE__ */ new WeakMap();
3949
4127
  kit.addGlass = function(c, geo, label, type, o) {
3950
4128
  o = o || {};
3951
4129
  if (!geo.attributes.uv || o.reuv) o.aspect = uvProject(geo, o.uvAxes);
@@ -4012,7 +4190,7 @@ var GameVehicles = (() => {
4012
4190
  cy /= poly.length;
4013
4191
  const base = I.resolveLabel(label ? label(i, polys.length) : { key: "window" });
4014
4192
  const lab = { key: base.key, label: kit.t("parts.glass_side", { label: base.label, s }) };
4015
- kit.addGlass(c, geo, lab, o.type || "tempered", {
4193
+ const pane = kit.addGlass(c, geo, lab, o.type || "tempered", {
4016
4194
  mat: o.mat || (o.see === false ? MATERIALS.glassDark : MATERIALS.glass),
4017
4195
  pos: new V3(cx, cy, s * I.halfAt(Wg, T, cy)),
4018
4196
  outline: shp.getSpacedPoints(56),
@@ -4028,6 +4206,7 @@ var GameVehicles = (() => {
4028
4206
  pts.pop();
4029
4207
  const seal = c.tube(pts, 0.011, MATERIALS.trim, { closed: true, seg: 72, rs: 5 });
4030
4208
  seal.castShadow = false;
4209
+ I.paneSeals.set(pane, seal);
4031
4210
  });
4032
4211
  });
4033
4212
  };
@@ -4703,6 +4882,7 @@ var GameVehicles = (() => {
4703
4882
  }
4704
4883
 
4705
4884
  // src/hood.ts
4885
+ var HOOD_LIFT = 8e-3;
4706
4886
  function installHood(kit) {
4707
4887
  const THREE = kit.THREE;
4708
4888
  const V3 = THREE.Vector3;
@@ -4725,7 +4905,7 @@ var GameVehicles = (() => {
4725
4905
  for (let i = 0; i < p.count; i++) {
4726
4906
  p.setX(i, cx + (p.getX(i) - cx) * sx);
4727
4907
  p.setZ(i, p.getZ(i) * sz);
4728
- p.setY(i, p.getY(i) + 8e-3);
4908
+ p.setY(i, p.getY(i) + HOOD_LIFT);
4729
4909
  }
4730
4910
  geo.translate(-hx, -hy, 0);
4731
4911
  const under = geo.clone();
@@ -4754,7 +4934,8 @@ var GameVehicles = (() => {
4754
4934
  yMin: bb.min.y,
4755
4935
  zw: bb.max.z,
4756
4936
  label: o.label,
4757
- noLatch: !!o.noLatch
4937
+ noLatch: !!o.noLatch,
4938
+ lift: HOOD_LIFT
4758
4939
  };
4759
4940
  c.part("hood", { key: "hood" }, piv, new V3((hx + bb.max.x) / 2, hy, 0), 0.9);
4760
4941
  return c.hood;
@@ -5737,6 +5918,44 @@ var GameVehicles = (() => {
5737
5918
  Object.assign(I, {});
5738
5919
  }
5739
5920
 
5921
+ // src/routed-particles.ts
5922
+ var ROUTED_PARTICLE_TYPES = { smoke: "particle_puff", dust: "particle_puff", fire: "particle_glow" };
5923
+ var PUFF_LOOKS = {
5924
+ smokewhite: [10133927, 0.42],
5925
+ smokeblack: [1776412, 0.62],
5926
+ smokegray: [5593436, 0.5],
5927
+ smokesteam: [11975873, 0.3],
5928
+ dust: [7235160, 0.34],
5929
+ fire: [16777215, 0.9]
5930
+ };
5931
+ var LIFE = { dust: 1.6, smoke: 2.8, fire: 0.7 };
5932
+ var FIRE_COLOR = 16753968;
5933
+ var SMOKE_START_SCALE = 0.18;
5934
+ function routedParticleRequest(input) {
5935
+ const { type } = input;
5936
+ const look = PUFF_LOOKS[type + (input.variant ?? "")] ?? PUFF_LOOKS[type] ?? PUFF_LOOKS.smokewhite;
5937
+ const life = input.life ?? LIFE[type] ?? 99;
5938
+ const size = (input.size ?? 1) * (input.grow ?? 1);
5939
+ const isFire = type === "fire";
5940
+ const growth = isFire ? 1 : (SMOKE_START_SCALE + 1.5 * (1 - Math.exp(-life * 1.1)) + life * 0.12) / SMOKE_START_SCALE;
5941
+ return {
5942
+ effect: ROUTED_PARTICLE_TYPES[type],
5943
+ point: input.position,
5944
+ values: {
5945
+ vx: input.velocity[0],
5946
+ vy: input.velocity[1],
5947
+ vz: input.velocity[2],
5948
+ life,
5949
+ size: isFire ? size * 0.85 : size * SMOKE_START_SCALE,
5950
+ grow: growth,
5951
+ color: input.color ?? (isFire ? FIRE_COLOR : look[0]),
5952
+ alpha: input.alpha ?? look[1],
5953
+ gravity: isFire ? 0 : type === "dust" ? 0 : -0.08,
5954
+ drag: type === "dust" ? 2.5 : isFire ? 1.5 : 0.9
5955
+ }
5956
+ };
5957
+ }
5958
+
5740
5959
  // src/particles.ts
5741
5960
  function installParticles(kit) {
5742
5961
  const THREE = kit.THREE;
@@ -5828,7 +6047,7 @@ var GameVehicles = (() => {
5828
6047
  });
5829
6048
  }
5830
6049
  const list2 = fire ? FIRE_TEXTURES : SMOKE_TEXTURES;
5831
- const mat = new THREE.SpriteMaterial({
6050
+ const build = () => new THREE.SpriteMaterial({
5832
6051
  map: list2[Math.floor(Math.random() * list2.length)],
5833
6052
  color: new THREE.Color(col),
5834
6053
  transparent: true,
@@ -5836,6 +6055,7 @@ var GameVehicles = (() => {
5836
6055
  opacity: op,
5837
6056
  blending: fire ? THREE.AdditiveBlending : THREE.NormalBlending
5838
6057
  });
6058
+ const mat = kit.particleMaterials ? kit.particleMaterials.own(build) : build();
5839
6059
  const dis = { value: 0 };
5840
6060
  mat.userData.dis = dis;
5841
6061
  mat.onBeforeCompile = function(sh) {
@@ -5863,11 +6083,37 @@ var GameVehicles = (() => {
5863
6083
  function killParticle(c, i) {
5864
6084
  const P = c.particles[i];
5865
6085
  c.effects.remove(P.m);
5866
- if (P.own) P.m.material.dispose();
6086
+ if (P.own) {
6087
+ if (kit.particleMaterials) kit.particleMaterials.release(P.m.material);
6088
+ else P.m.material.dispose();
6089
+ }
5867
6090
  c.particles.splice(i, 1);
5868
6091
  }
6092
+ function routeParticle(c, effects, type, pos, vel, o) {
6093
+ c.effects.updateWorldMatrix(true, false);
6094
+ const at = pos.clone().applyMatrix4(c.effects.matrixWorld);
6095
+ const to = vel.clone().transformDirection(c.effects.matrixWorld).multiplyScalar(vel.length());
6096
+ const colour = o.col == null ? void 0 : new THREE.Color(o.col).getHex();
6097
+ effects.play(
6098
+ routedParticleRequest({
6099
+ type,
6100
+ position: [at.x, at.y, at.z],
6101
+ velocity: [to.x, to.y, to.z],
6102
+ ...o.variant ? { variant: o.variant } : {},
6103
+ ...colour !== void 0 ? { color: colour } : {},
6104
+ ...o.op != null ? { alpha: o.op } : {},
6105
+ ...o.s ? { size: o.s } : {},
6106
+ ...o.life ? { life: o.life } : {},
6107
+ ...o.grow ? { grow: o.grow } : {}
6108
+ })
6109
+ );
6110
+ return null;
6111
+ }
5869
6112
  kit.spawn = function(c, type, pos, vel, o) {
5870
6113
  o = o || {};
6114
+ const effects = ROUTED_PARTICLE_TYPES[type] ? kit.effects?.() : void 0;
6115
+ if (effects) return routeParticle(c, effects, type, pos, vel, o);
6116
+ 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
6117
  if (c.particles.length >= PARTICLE_LIMIT) {
5872
6118
  const idx = c.particles.findIndex(function(q) {
5873
6119
  return TRANSIENT_TYPES[q.type];
@@ -6122,6 +6368,14 @@ var GameVehicles = (() => {
6122
6368
  }
6123
6369
 
6124
6370
  // src/fluids.ts
6371
+ var PUDDLE_COLORS = {
6372
+ coolant: 3123279,
6373
+ brake_fluid: 12093738,
6374
+ transmission_fluid: 10099746,
6375
+ power_steering_fluid: 9054768,
6376
+ washer_fluid: 2977744,
6377
+ battery_acid: 14211272
6378
+ };
6125
6379
  function installFluids(kit) {
6126
6380
  const THREE = kit.THREE;
6127
6381
  const V3 = THREE.Vector3;
@@ -6139,6 +6393,7 @@ var GameVehicles = (() => {
6139
6393
  };
6140
6394
  kit.defineFluid("stain", { fresh: "#4a4a44", dry: "#26241f", wipeable: false });
6141
6395
  const _fr = new THREE.Raycaster();
6396
+ _fr.layers.enableAll();
6142
6397
  kit.splat = function(c, p, n, r, o) {
6143
6398
  o = o || {};
6144
6399
  n = n.clone().normalize();
@@ -6340,6 +6595,13 @@ var GameVehicles = (() => {
6340
6595
  transparent: true,
6341
6596
  opacity: 0.9,
6342
6597
  depthWrite: false
6598
+ }) : PUDDLE_COLORS[kind] ? new THREE.MeshStandardMaterial({
6599
+ color: PUDDLE_COLORS[kind],
6600
+ roughness: 0.05,
6601
+ metalness: 0.1,
6602
+ transparent: true,
6603
+ opacity: 0.7,
6604
+ depthWrite: false
6343
6605
  }) : new THREE.MeshStandardMaterial({
6344
6606
  color: 0,
6345
6607
  roughness: 0.03,
@@ -6580,31 +6842,55 @@ var GameVehicles = (() => {
6580
6842
  }
6581
6843
 
6582
6844
  // src/glass-damage.ts
6845
+ function crackedGlassMaterial(THREE, texture) {
6846
+ return new THREE.MeshStandardMaterial({
6847
+ color: 16777215,
6848
+ map: texture,
6849
+ emissive: 6976118,
6850
+ emissiveMap: texture,
6851
+ transparent: true,
6852
+ roughness: 0.25,
6853
+ metalness: 0.1,
6854
+ side: THREE.DoubleSide,
6855
+ depthWrite: false
6856
+ });
6857
+ }
6583
6858
  function installGlassDamage(kit) {
6584
6859
  const THREE = kit.THREE;
6585
6860
  const V3 = THREE.Vector3;
6586
6861
  const PI = Math.PI;
6587
6862
  const I = kit._;
6588
6863
  const document2 = kit.document;
6589
- const { DOUBLE_SIDE, MATERIALS, crease, cutGeometry, mergeGeometries, subdivide, tumble } = I;
6590
- function glassUV(msh, pLocal) {
6864
+ const { MATERIALS, crease, cutGeometry, mergeGeometries, subdivide, tumble } = I;
6865
+ function glassUV(c, msh, pLocal) {
6591
6866
  const pos = msh.geometry.attributes.position;
6592
6867
  const uv = msh.geometry.attributes.uv;
6593
6868
  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
- }
6869
+ c.root.updateMatrixWorld(true);
6870
+ const q = msh.worldToLocal(c.vehicle.localToWorld(pLocal.clone()));
6871
+ const idx = msh.geometry.index;
6872
+ const tri = new THREE.Triangle();
6873
+ const near = new V3();
6874
+ const bary = new V3();
6875
+ let best = Infinity;
6876
+ const out = [0.5, 0.5];
6877
+ const count = idx ? idx.count : pos.count;
6878
+ for (let f = 0; f + 2 < count; f += 3) {
6879
+ const ia = idx ? idx.getX(f) : f;
6880
+ const ib = idx ? idx.getX(f + 1) : f + 1;
6881
+ const ic = idx ? idx.getX(f + 2) : f + 2;
6882
+ tri.a.fromBufferAttribute(pos, ia);
6883
+ tri.b.fromBufferAttribute(pos, ib);
6884
+ tri.c.fromBufferAttribute(pos, ic);
6885
+ tri.closestPointToPoint(q, near);
6886
+ const d = near.distanceToSquared(q);
6887
+ if (d >= best) continue;
6888
+ best = d;
6889
+ tri.getBarycoord(near, bary);
6890
+ out[0] = uv.getX(ia) * bary.x + uv.getX(ib) * bary.y + uv.getX(ic) * bary.z;
6891
+ out[1] = uv.getY(ia) * bary.x + uv.getY(ib) * bary.y + uv.getY(ic) * bary.z;
6606
6892
  }
6607
- return [uv.getX(bi), uv.getY(bi)];
6893
+ return out;
6608
6894
  }
6609
6895
  function drawWeb(x, cx, cy, s, ax, rnd) {
6610
6896
  const n = 16 + Math.floor(rnd() * 8);
@@ -6681,19 +6967,9 @@ var GameVehicles = (() => {
6681
6967
  x0.fillStyle = "rgba(48,66,72,0.5)";
6682
6968
  x0.fillRect(0, 0, 256, 256);
6683
6969
  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
- });
6970
+ msh.material = crackedGlassMaterial(THREE, G.tex);
6695
6971
  }
6696
- const uv = glassUV(msh, p);
6972
+ const uv = glassUV(c, msh, p);
6697
6973
  const x = G.cv.getContext("2d");
6698
6974
  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
6975
  G.tex.needsUpdate = true;
@@ -6838,6 +7114,13 @@ var GameVehicles = (() => {
6838
7114
  }
6839
7115
  const seatGeo = [];
6840
7116
  const d = dir.clone().normalize();
7117
+ if (msh.parent && msh.parent !== c.vehicle) {
7118
+ c.root.updateMatrixWorld(true);
7119
+ const toVehicle = new THREE.Matrix4().copy(c.vehicle.matrixWorld).invert().multiply(msh.parent.matrixWorld);
7120
+ cubesAt.forEach(function(q) {
7121
+ q.applyMatrix4(toVehicle);
7122
+ });
7123
+ }
6841
7124
  cubesAt.forEach(function(p, i) {
6842
7125
  if (i % 3 === 0 && c.seatTops.length && !c.rollState && !c.flipped) {
6843
7126
  let best = null;
@@ -7040,6 +7323,7 @@ var GameVehicles = (() => {
7040
7323
  polygonOffsetUnits: -2
7041
7324
  });
7042
7325
  const _ray = new THREE.Raycaster();
7326
+ _ray.layers.enableAll();
7043
7327
  function findSurface(c, p, dir) {
7044
7328
  c.root.updateMatrixWorld(true);
7045
7329
  const Mw = c.vehicle.matrixWorld;
@@ -7121,6 +7405,7 @@ var GameVehicles = (() => {
7121
7405
  const v0 = new V3();
7122
7406
  const w = new V3();
7123
7407
  const sc = new V3();
7408
+ const local = new V3();
7124
7409
  const cab = m.cabin && !c.flipped && !c.rollState ? m.cabin : null;
7125
7410
  c.deform.forEach(function(msh) {
7126
7411
  if (!msh.parent || !msh.visible) return;
@@ -7135,7 +7420,15 @@ var GameVehicles = (() => {
7135
7420
  irel.copy(rel).invert();
7136
7421
  if (!g.userData.p0) g.userData.p0 = new Float32Array(pos.array);
7137
7422
  const p0 = g.userData.p0;
7423
+ local.copy(p).applyMatrix4(irel);
7424
+ const reach = H.R / Math.max(1e-6, Math.min(sc.x, sc.y, sc.z));
7425
+ const reach2 = reach * reach;
7426
+ const arr = pos.array;
7138
7427
  for (let i = 0; i < pos.count; i++) {
7428
+ const dx = arr[3 * i] - local.x;
7429
+ const dy = arr[3 * i + 1] - local.y;
7430
+ const dz = arr[3 * i + 2] - local.z;
7431
+ if (dx * dx + dy * dy + dz * dz >= reach2) continue;
7139
7432
  v.fromBufferAttribute(pos, i).applyMatrix4(rel);
7140
7433
  const a = dentField(v, H);
7141
7434
  if (a <= 0) continue;
@@ -7151,10 +7444,39 @@ var GameVehicles = (() => {
7151
7444
  }
7152
7445
  if (changed) {
7153
7446
  pos.needsUpdate = true;
7154
- if (g.userData.crease) crease(g, 42);
7447
+ if (g.userData.crease && c.smoothLater) c.smoothLater(msh);
7448
+ else if (g.userData.crease) crease(g, 42);
7155
7449
  else g.computeVertexNormals();
7156
- g.computeBoundingSphere();
7157
- g.computeBoundingBox();
7450
+ const sphere2 = g.boundingSphere ?? (g.boundingSphere = new THREE.Sphere());
7451
+ if (pos.isInterleavedBufferAttribute || pos.normalized || Object.keys(g.morphAttributes).length > 0) {
7452
+ g.computeBoundingBox();
7453
+ g.boundingBox.getCenter(sphere2.center);
7454
+ let far2 = 0;
7455
+ for (let i = 0; i < pos.count; i++) far2 = Math.max(far2, sphere2.center.distanceToSquared(v.fromBufferAttribute(pos, i)));
7456
+ sphere2.radius = Math.sqrt(far2);
7457
+ } else {
7458
+ const box3 = g.boundingBox ?? (g.boundingBox = new THREE.Box3());
7459
+ let x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
7460
+ for (let i = 0; i < pos.count * 3; i += 3) {
7461
+ const x = arr[i], y = arr[i + 1], z = arr[i + 2];
7462
+ if (x < x0) x0 = x;
7463
+ if (y < y0) y0 = y;
7464
+ if (z < z0) z0 = z;
7465
+ if (x > x1) x1 = x;
7466
+ if (y > y1) y1 = y;
7467
+ if (z > z1) z1 = z;
7468
+ }
7469
+ box3.min.set(x0, y0, z0);
7470
+ box3.max.set(x1, y1, z1);
7471
+ box3.getCenter(sphere2.center);
7472
+ const cx = sphere2.center.x, cy = sphere2.center.y, cz = sphere2.center.z;
7473
+ let far2 = 0;
7474
+ for (let i = 0; i < pos.count * 3; i += 3) {
7475
+ const dx = cx - arr[i], dy = cy - arr[i + 1], dz = cz - arr[i + 2];
7476
+ far2 = Math.max(far2, dx * dx + dy * dy + dz * dz);
7477
+ }
7478
+ sphere2.radius = Math.sqrt(far2);
7479
+ }
7158
7480
  }
7159
7481
  });
7160
7482
  const dset = new Set(c.deform);
@@ -7238,13 +7560,14 @@ var GameVehicles = (() => {
7238
7560
  kit.impact = impact;
7239
7561
  kit.partEffects = {};
7240
7562
  const _pr = new THREE.Raycaster();
7563
+ _pr.layers.enableAll();
7241
7564
  function probe(c, o, d, far) {
7242
7565
  c.root.updateMatrixWorld(true);
7243
7566
  const Mw = c.vehicle.matrixWorld;
7244
7567
  const inv = new THREE.Matrix4().copy(Mw).invert();
7245
7568
  _pr.set(o.clone().applyMatrix4(Mw), d.clone().transformDirection(Mw));
7246
7569
  _pr.far = far || 10;
7247
- const hits = _pr.intersectObject(c.vehicle, true);
7570
+ const hits = raycastIndexed(THREE, _pr, [c.vehicle], true);
7248
7571
  for (let i = 0; i < hits.length; i++) {
7249
7572
  const h = hits[i];
7250
7573
  const ob = h.object;
@@ -7557,7 +7880,8 @@ var GameVehicles = (() => {
7557
7880
  const G = gm2.userData.glass;
7558
7881
  if (!G || G.broken || !gm2.visible || !gm2.parent) return;
7559
7882
  if (G.type === "laminated") {
7560
- G.type = "tempered";
7883
+ for (let k2 = 0; k2 < 3; k2++) I.crackGlass(c, gm2, I.glassPt ? I.glassPt(c, gm2, c.rand) : gm2.position.clone(), 1.2);
7884
+ return;
7561
7885
  }
7562
7886
  I.shatterGlass(c, gm2, 1.4, new V3(0, 1, 0));
7563
7887
  });
@@ -7568,19 +7892,6 @@ var GameVehicles = (() => {
7568
7892
  fling(c, c.hood.piv, new V3(rv(1, 3), rv(6, 8.5), rv(-1.5, 1.5)), new V3(rv(-4, 4), rv(-2, 2), rv(3, 7)));
7569
7893
  c.hood = null;
7570
7894
  }
7571
- const W = m.width || 1.6;
7572
- const gh = m.glasshouse;
7573
- if (m.kind === "car" && gh) {
7574
- ;
7575
- [1, -1].forEach(function(sd) {
7576
- const dh = Math.max(0.35, (m.belt || 1) - (m.clearance || 0.3) - 0.08);
7577
- const dx = gh.fb - 0.55;
7578
- const door = c.roundBox(0.95, dh, 0.045, 0.03, c.paint(m.color || "#777"), dx, (m.clearance || 0.3) + dh / 2 + 0.04, sd * (W / 2 + 0.01));
7579
- const hole = c.roundBox(0.9, dh * 0.95, 0.02, 0.02, MATERIALS.dark, dx, (m.clearance || 0.3) + dh / 2 + 0.04, sd * (W / 2 - 0.01));
7580
- hole.userData.noFollow = true;
7581
- fling(c, door, c.dirToEffects(new V3(rv(-1, 1), rv(3.5, 6), sd * rv(4, 6.5))), new V3(rv(-5, 5), rv(-4, 4), rv(-5, 5)));
7582
- });
7583
- }
7584
7895
  if (m.rearBumper && m.rearBumper.parent)
7585
7896
  fling(
7586
7897
  c,
@@ -7588,20 +7899,6 @@ var GameVehicles = (() => {
7588
7899
  c.dirToEffects(new V3(rv(-5, -3), rv(4, 6), rv(-1, 1))),
7589
7900
  new V3(rv(-3, 3), rv(-6, 6), rv(-3, 3))
7590
7901
  );
7591
- if (c.wheels.length) {
7592
- const wi = Math.floor(c.rand() * c.wheels.length);
7593
- const wh = c.wheels[wi];
7594
- c.wheels.splice(wi, 1);
7595
- c.parts.forEach(function(p) {
7596
- if (p.obj === wh) p.done = true;
7597
- });
7598
- fling(
7599
- c,
7600
- wh,
7601
- c.dirToEffects(new V3(rv(-2, 2), rv(5, 7.5), (wh.position.z >= 0 ? 1 : -1) * rv(3, 5))),
7602
- new V3(rv(-8, 8), rv(-3, 3), rv(-8, 8))
7603
- );
7604
- }
7605
7902
  burnAll(c);
7606
7903
  c.emitters = c.emitters.filter(function(e) {
7607
7904
  return e.kind === "exhaust" ? false : true;
@@ -8610,6 +8907,7 @@ var GameVehicles = (() => {
8610
8907
  return hole;
8611
8908
  }
8612
8909
  const _zr = new THREE.Raycaster();
8910
+ _zr.layers.enableAll();
8613
8911
  const PROXY_MIN_TRIANGLES = 500;
8614
8912
  const STROKE_REACH = 0.6;
8615
8913
  const _pv = new V3();
@@ -8844,6 +9142,7 @@ var GameVehicles = (() => {
8844
9142
  return out;
8845
9143
  }
8846
9144
  const _rt = new THREE.Raycaster();
9145
+ _rt.layers.enableAll();
8847
9146
  function scratchPath(c, s, len, ang, cands, B) {
8848
9147
  const n = s.n.clone().normalize();
8849
9148
  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 +12424,8 @@ var GameVehicles = (() => {
12125
12424
  V3: THREE.Vector3,
12126
12425
  PI: Math.PI,
12127
12426
  document: doc,
12427
+ particleMaterials: opts.particleMaterials,
12428
+ effects: opts.effects,
12128
12429
  strings,
12129
12430
  t,
12130
12431
  matchers: createMatchers(strings),
@@ -12380,7 +12681,9 @@ var GameVehicles = (() => {
12380
12681
  assignDeep(strings, resolveStrings(opts.locale));
12381
12682
  return kit;
12382
12683
  };
12383
- const { THREE: _three, lod: _lod, locale: _locale, document: _document, plugins, ...config } = opts;
12684
+ const { THREE: _three, lod: _lod, locale: _locale, document: _document, particleMaterials: _particleMaterials, effects: _effects, plugins, ...config } = opts;
12685
+ void _particleMaterials;
12686
+ void _effects;
12384
12687
  void _three;
12385
12688
  void _lod;
12386
12689
  void _locale;
@@ -12390,6 +12693,17 @@ var GameVehicles = (() => {
12390
12693
  return kit;
12391
12694
  }
12392
12695
 
12696
+ // src/destructible/armour.ts
12697
+ var ARMOUR_LEVELS = {
12698
+ IIIA: { glassStops: 1700, glassHp: 6e3, glassTakes: 0.25, steel: 8 },
12699
+ III: { glassStops: 3600, glassHp: 9e3, glassTakes: 0.25, steel: 30 }
12700
+ };
12701
+ function armourLevelOf(meta, option) {
12702
+ if (option !== void 0) return option;
12703
+ if (meta.armor === "IIIA" || meta.armor === "III") return meta.armor;
12704
+ return meta.armored || meta.bullet ? "III" : null;
12705
+ }
12706
+
12393
12707
  // src/destructible/model.ts
12394
12708
  var JOINT_RULES = {
12395
12709
  hinge: { strength: 5e3, tools: ["wrench", "screwdriver"], seconds: 3 },
@@ -12433,6 +12747,12 @@ var GameVehicles = (() => {
12433
12747
  center: [(z.box.x0 + z.box.x1) / 2, (z.box.y0 + z.box.y1) / 2, (z.box.z0 + z.box.z1) / 2],
12434
12748
  size: [Math.max(0.02, z.box.x1 - z.box.x0), Math.max(0.02, z.box.y1 - z.box.y0), Math.max(0.02, z.box.z1 - z.box.z0)]
12435
12749
  });
12750
+ var PART_FLUIDS = {
12751
+ gearbox: { fluid: "transmission_fluid", liters: 6 },
12752
+ brakes: { fluid: "brake_fluid", liters: 0.6 },
12753
+ steering: { fluid: "power_steering_fluid", liters: 1 },
12754
+ battery: { fluid: "battery_acid", liters: 3 }
12755
+ };
12436
12756
  var skinOf = (kind) => kind === "plane" || kind === "heli" || kind === "boat" ? "aluminium" : "steel";
12437
12757
  var isFender = (id) => /^fender/.test(id);
12438
12758
  var roleOfZone = (z) => {
@@ -12476,13 +12796,22 @@ var GameVehicles = (() => {
12476
12796
  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
12797
  size: [b.size[0] * 0.5, Math.min(0.14, b.size[1]), b.size[2] * 0.95]
12478
12798
  });
12799
+ function paneBox(c, pane, inverse) {
12800
+ pane.geometry.computeBoundingBox();
12801
+ const box2 = pane.geometry.boundingBox.clone().applyMatrix4(pane.matrixWorld);
12802
+ box2.applyMatrix4(inverse);
12803
+ const center = [(box2.min.x + box2.max.x) / 2, (box2.min.y + box2.max.y) / 2, (box2.min.z + box2.max.z) / 2];
12804
+ const size = [box2.max.x - box2.min.x, box2.max.y - box2.min.y, box2.max.z - box2.min.z];
12805
+ return { center, size };
12806
+ }
12479
12807
  function buildVehicleModel(kit, c, options = {}) {
12480
12808
  const kind = kindOf(c);
12481
12809
  const m = c.meta;
12482
12810
  const spec = c.spec;
12483
12811
  const t = kit.t;
12484
12812
  const skin = skinOf(kind);
12485
- const armour = m.armored || m.bullet ? 12 : 1;
12813
+ const armoured = options.armour ?? null;
12814
+ const armour = armoured ? 12 : 1;
12486
12815
  const mass = vehicleMass(c);
12487
12816
  const W = m.width ?? 1.7;
12488
12817
  const F = m.front ?? 2;
@@ -12548,8 +12877,8 @@ var GameVehicles = (() => {
12548
12877
  }
12549
12878
  if (role === "glass") {
12550
12879
  const glass = (z.glass ?? [])[0];
12551
- const laminated = glass?.userData?.glass?.type === "laminated" || m.bullet;
12552
- add({ id, role, name: label, material: "glass", box: box2, mass: 10 * area3, hp: laminated ? 4e3 : 450, zoneId: id, visual: false, tags: ["glass", ...tags] });
12880
+ const laminated = glass?.userData?.glass?.type === "laminated" || !!armoured;
12881
+ add({ id, role, name: label, material: "glass", box: box2, mass: 10 * area3, hp: armoured ? armoured.glassHp : laminated ? 4e3 : 450, zoneId: id, visual: false, tags: ["glass", ...tags] });
12553
12882
  continue;
12554
12883
  }
12555
12884
  if (role === "light") {
@@ -12587,7 +12916,7 @@ var GameVehicles = (() => {
12587
12916
  zoneId: id,
12588
12917
  visual: false,
12589
12918
  contains: { fluid: diesel ? "diesel" : "fuel", liters },
12590
- explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320 },
12919
+ explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320, effect: "car_fuel_tank" },
12591
12920
  ignitionC: diesel ? 330 : 260,
12592
12921
  tags: ["fuel_tank", ...tags]
12593
12922
  });
@@ -12616,6 +12945,16 @@ var GameVehicles = (() => {
12616
12945
  tags: [role, ...tags]
12617
12946
  });
12618
12947
  }
12948
+ const claimed = new Set(zones.filter((z) => roleOfZone(z) === "glass").flatMap((z) => z.glass ?? []));
12949
+ c.root.updateMatrixWorld(true);
12950
+ const inverse = c.vehicle.matrixWorld.clone().invert();
12951
+ for (const pane of c.glass ?? []) {
12952
+ if (claimed.has(pane)) continue;
12953
+ const G = pane.userData.glass;
12954
+ const bb = paneBox(c, pane, inverse);
12955
+ const laminated = G?.type === "laminated" || !!armoured;
12956
+ add({ id: `pane_${G?.key ?? "glass"}`, role: "glass", name: String(G?.label ?? G?.key ?? "glass"), material: "glass", box: bb, mass: 10 * areaOf(bb.size), hp: armoured ? armoured.glassHp : laminated ? 4e3 : 450, panes: [pane], visual: false, tags: ["glass"] });
12957
+ }
12619
12958
  if (!hubZone) {
12620
12959
  const h = Math.max(0.15, belt - clearance);
12621
12960
  add({
@@ -12670,7 +13009,7 @@ var GameVehicles = (() => {
12670
13009
  visual: false,
12671
13010
  mechanical: true,
12672
13011
  contains: { fluid: diesel ? "diesel" : "fuel", liters },
12673
- explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320 },
13012
+ explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320, effect: "car_fuel_tank" },
12674
13013
  ignitionC: diesel ? 330 : 260,
12675
13014
  tags: ["fuel_tank"]
12676
13015
  });
@@ -12947,10 +13286,163 @@ var GameVehicles = (() => {
12947
13286
  }
12948
13287
  }
12949
13288
  void has;
13289
+ for (const part of parts) {
13290
+ const fluid = PART_FLUIDS[part.role];
13291
+ if (fluid && !part.contains && !(kind === "bike" && part.role === "steering")) part.contains = { ...fluid };
13292
+ }
12950
13293
  return { parts, joints, hub: hubId, hitch };
12951
13294
  }
12952
13295
 
13296
+ // src/destructible/subdivide-near.ts
13297
+ var SUBDIVIDE_DEPTH = 12;
13298
+ function subdivideNear(THREE, params) {
13299
+ const { mesh: mesh2, maxLen, near } = params;
13300
+ const src = mesh2.geometry.index ? mesh2.geometry.toNonIndexed() : mesh2.geometry;
13301
+ const a = src.attributes.position.array;
13302
+ const toWorld = mesh2.matrixWorld;
13303
+ const out = [];
13304
+ const mid = (p, q) => [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2, (p[2] + q[2]) / 2];
13305
+ const split = (A, B, C, depth) => {
13306
+ const ab = Math.hypot(A[0] - B[0], A[1] - B[1], A[2] - B[2]);
13307
+ const bc = Math.hypot(B[0] - C[0], B[1] - C[1], B[2] - C[2]);
13308
+ const ca = Math.hypot(C[0] - A[0], C[1] - A[1], C[2] - A[2]);
13309
+ const longest = Math.max(ab, bc, ca);
13310
+ if (longest <= maxLen || depth > SUBDIVIDE_DEPTH) {
13311
+ out.push(...A, ...B, ...C);
13312
+ return;
13313
+ }
13314
+ if (longest === ab) {
13315
+ const D = mid(A, B);
13316
+ split(A, D, C, depth + 1);
13317
+ split(D, B, C, depth + 1);
13318
+ } else if (longest === bc) {
13319
+ const E = mid(B, C);
13320
+ split(A, B, E, depth + 1);
13321
+ split(A, E, C, depth + 1);
13322
+ } else {
13323
+ const F = mid(C, A);
13324
+ split(A, B, F, depth + 1);
13325
+ split(F, B, C, depth + 1);
13326
+ }
13327
+ };
13328
+ for (let i = 0; i < a.length; i += 9) {
13329
+ const A = [a[i], a[i + 1], a[i + 2]];
13330
+ const B = [a[i + 3], a[i + 4], a[i + 5]];
13331
+ const C = [a[i + 6], a[i + 7], a[i + 8]];
13332
+ const corners = [A, B, C].map((p) => new THREE.Vector3(p[0], p[1], p[2]).applyMatrix4(toWorld));
13333
+ if (near(corners)) split(A, B, C, 0);
13334
+ else out.push(...A, ...B, ...C);
13335
+ }
13336
+ const g = new THREE.BufferGeometry();
13337
+ g.setAttribute("position", new THREE.Float32BufferAttribute(out, 3));
13338
+ mesh2.geometry.dispose();
13339
+ mesh2.geometry = g;
13340
+ }
13341
+
12953
13342
  // src/destructible/visual.ts
13343
+ var HOLE_DECAL_SCALE = 2.4;
13344
+ var DOOR_DEPTH = 0.07;
13345
+ var DOOR_EDGE_PIECES = 8;
13346
+ var DOOR_EDGE_REACH = 0.08;
13347
+ var DOOR_CARD_COLOR = "#3b3e42";
13348
+ var DOOR_LINER_COLOR = "#5a5e64";
13349
+ var DOOR_LINER_DARK = 0.15;
13350
+ var DOOR_STRIP_THIN = 0.08;
13351
+ var DOOR_STRIP_NEAR = 0.04;
13352
+ var DOOR_STRIP_TALL = 0.22;
13353
+ var STRIP_SMOOTH_DEG = 180;
13354
+ var DOOR_STRIP_VERTICES = 2e4;
13355
+ var DOOR_SILL = 0.25;
13356
+ var DOOR_STRIP_OVER = 0.3;
13357
+ var DOOR_TOP_OVER_PANE = 0.03;
13358
+ var HOLE_TEXTURE_SIZE = 64;
13359
+ var LAMINATED_HP = 1e3;
13360
+ var HOOD_SINK = 2e-3;
13361
+ var HOOD_GAP_CLOSED = 0.016;
13362
+ var DOOR_HANDLE_INSET = 0.12;
13363
+ var DOOR_HANDLE_MIN = 0.07;
13364
+ var DOOR_HANDLE_MAX = 0.24;
13365
+ var DOOR_HANDLE_THIN = 0.07;
13366
+ var DOOR_HANDLE_OUTSIDE = 0.15;
13367
+ var PANE_SEAT_OUT = 0.05;
13368
+ var PANE_SEAT_IN = -0.05;
13369
+ var PANE_SEAT_REACH = 0.14;
13370
+ var PANE_SEAT_SAMPLES = 24;
13371
+ var PANE_SEAT_LIFT = 4e-3;
13372
+ var PANE_FRAME_NEAR = 0.07;
13373
+ var PANE_MIN_SLOPE = 0.01;
13374
+ var PANE_EDGE_BAND = 0.03;
13375
+ var PANE_REACH_STEP = 0.01;
13376
+ var PANE_REACH_MAX = 0.25;
13377
+ var COWL_UNDER_GLASS = 0;
13378
+ var COWL_NEAR_PANE = 0.08;
13379
+ var COWL_PLATE = 0.06;
13380
+ var COWL_DASH = 0.5;
13381
+ var COWL_DEEP = 0.3;
13382
+ var WINDOW_FRAME_NEAR = 0.06;
13383
+ var WINDOW_FRAME_SPAN = 0.6;
13384
+ var DASHBOARD_COLOR = "#121314";
13385
+ var COWL_CUT_SUB = 0.1;
13386
+ var COWL_CUT_MARGIN = 0.08;
13387
+ var SKIN_UNDER_PANE = 0.025;
13388
+ var SKIN_EDGE_KEPT = 0.06;
13389
+ var SIDE_PANE_FACING = 0.6;
13390
+ var COWL_LONG = 0.8;
13391
+ var PANE_BAR_VERTICES = 48;
13392
+ var PANE_BAR_LENGTH = 0.3;
13393
+ var PANE_BORDER = 0.03;
13394
+ var PANE_BORDER_LIFT = 3e-3;
13395
+ var DRAWN_MIRROR_SIZE = 0.3;
13396
+ var DRAWN_MIRROR_REACH = 0.4;
13397
+ var DRAWN_MIRROR_MOUNT = 0.2;
13398
+ var MIRROR_GLASS_FIT = 0.76;
13399
+ var MIRROR_GLASS_PROUD = 1e-3;
13400
+ var MIRROR_FACE_DEPTH = 0.012;
13401
+ var MIRROR_GLASS_THIN = 0.04;
13402
+ var DEFAULT_WHEEL_RADIUS = 0.3;
13403
+ var SIDEWALL_SHARE = 0.3;
13404
+ var MIRROR_GLASS_MIN = 0.05;
13405
+ var MIRROR_GLASS_COLOR = "#59636a";
13406
+ var HOLE_RIM = { steel: "#b7bcc0", aluminium: "#d3d6d8", plastic: "#d9d6cc", rubber: "#141414", glass: "#eef3f5" };
13407
+ var holeMaterials = /* @__PURE__ */ new WeakMap();
13408
+ var holeDisc = null;
13409
+ function holeDiscGeometry(THREE) {
13410
+ holeDisc = holeDisc ?? new THREE.CircleGeometry(1, 12);
13411
+ return holeDisc;
13412
+ }
13413
+ function holeMaterial(kit, material) {
13414
+ const key = kit;
13415
+ const cache2 = holeMaterials.get(key) ?? /* @__PURE__ */ new Map();
13416
+ holeMaterials.set(key, cache2);
13417
+ const known = cache2.get(material);
13418
+ if (known) return known;
13419
+ const I = kit._;
13420
+ const THREE = kit.THREE;
13421
+ const canvas = kit.document?.createElement?.("canvas");
13422
+ const x = canvas?.getContext?.("2d");
13423
+ let made = I.MATERIALS.dark;
13424
+ if (x) {
13425
+ canvas.width = canvas.height = HOLE_TEXTURE_SIZE;
13426
+ const mid = HOLE_TEXTURE_SIZE / 2;
13427
+ x.fillStyle = HOLE_RIM[material] ?? HOLE_RIM.steel;
13428
+ x.beginPath();
13429
+ for (let i = 0; i < 18; i++) {
13430
+ const a = i / 18 * Math.PI * 2;
13431
+ const r = mid * (i % 2 ? 0.98 : 0.76);
13432
+ x.lineTo(mid + Math.cos(a) * r, mid + Math.sin(a) * r);
13433
+ }
13434
+ x.closePath();
13435
+ x.fill();
13436
+ x.fillStyle = "#050505";
13437
+ x.beginPath();
13438
+ x.arc(mid, mid, mid * 0.42, 0, Math.PI * 2);
13439
+ x.fill();
13440
+ const map = new THREE.CanvasTexture(canvas);
13441
+ made = new THREE.MeshStandardMaterial({ map, transparent: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -2, roughness: 0.55, metalness: material === "steel" || material === "aluminium" ? 0.6 : 0 });
13442
+ }
13443
+ cache2.set(material, made);
13444
+ return made;
13445
+ }
12954
13446
  var VehicleVisuals = class {
12955
13447
  constructor(kit, c, model) {
12956
13448
  __publicField(this, "kit");
@@ -12968,14 +13460,23 @@ var GameVehicles = (() => {
12968
13460
  __publicField(this, "syncPlates", /* @__PURE__ */ new Map());
12969
13461
  /** 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
13462
  __publicField(this, "skins", /* @__PURE__ */ new Map());
13463
+ /** How high each door's cut reaches. */
13464
+ __publicField(this, "doorTops", /* @__PURE__ */ new Map());
12971
13465
  __publicField(this, "lightsOn", true);
12972
13466
  /** Heavy body meshes whose triangle index is still to build (null once done). */
12973
13467
  __publicField(this, "warming");
12974
13468
  /** A panel was torn off since the last `pump`: that frame already paid for the cut, the smoothing starts on the next one. */
12975
13469
  __publicField(this, "tornThisFrame", false);
13470
+ /** A shared crease queue owns the smoothing: `pump` only closes the frame of the cut. */
13471
+ __publicField(this, "queued", false);
13472
+ /** A panel was torn and the queue has not asked yet: the first `pendingUnits(frame)` after it holds the work for that frame. */
13473
+ __publicField(this, "holdArmed", false);
13474
+ __publicField(this, "holdFrame", -1);
12976
13475
  /** Normal smoothing still running, by mesh: a mesh cut again gets a new task and the old one is dropped. */
12977
13476
  __publicField(this, "smoothing", /* @__PURE__ */ new Map());
12978
13477
  __publicField(this, "hood");
13478
+ ;
13479
+ c.smoothLater = (mesh2) => this.smoothLater(mesh2);
12979
13480
  this.kit = kit;
12980
13481
  this.c = c;
12981
13482
  this.model = model;
@@ -12984,6 +13485,9 @@ var GameVehicles = (() => {
12984
13485
  for (const p of model.parts) this.byId.set(p.id, p);
12985
13486
  c.root.updateMatrixWorld(true);
12986
13487
  this.hood = this.nameHood();
13488
+ this.seatPanes();
13489
+ this.seatDoorHandles();
13490
+ this.seatHood();
12987
13491
  this.warming = c.deform.filter((msh) => msh.geometry && msh.geometry.attributes.position && msh.geometry.attributes.position.count >= 1500);
12988
13492
  if (!this.warming.length) this.warming = null;
12989
13493
  for (const p of model.parts) {
@@ -13004,6 +13508,7 @@ var GameVehicles = (() => {
13004
13508
  const id = z ? z.id : this.model.parts.find((p) => p.role === "hood")?.id;
13005
13509
  if (!id) return null;
13006
13510
  c.hood.piv.name = id;
13511
+ c.hood.piv.userData.plateOf = id;
13007
13512
  return { id, open: c.hood.open, sign: c.hood.sign || 1 };
13008
13513
  }
13009
13514
  mirrorObjects(p) {
@@ -13091,6 +13596,7 @@ var GameVehicles = (() => {
13091
13596
  recess.userData.noFollow = true;
13092
13597
  const piv = new THREE.Group();
13093
13598
  piv.name = p.id;
13599
+ piv.userData.plateOf = p.id;
13094
13600
  piv.position.set(hx, cy, sk + s * 0.016);
13095
13601
  piv.userData.rigid = true;
13096
13602
  this.c.vehicle.add(piv);
@@ -13107,6 +13613,7 @@ var GameVehicles = (() => {
13107
13613
  recess.userData.noFollow = true;
13108
13614
  const piv = new THREE.Group();
13109
13615
  piv.name = p.id;
13616
+ piv.userData.plateOf = p.id;
13110
13617
  piv.position.set(sk - 0.016, B.y1 - 0.05, 0);
13111
13618
  piv.userData.rigid = true;
13112
13619
  this.c.vehicle.add(piv);
@@ -13170,10 +13677,20 @@ var GameVehicles = (() => {
13170
13677
  try {
13171
13678
  if (p.role === "door") {
13172
13679
  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);
13680
+ const glassBox = new this.THREE.Box3();
13681
+ for (const g of I.windowsOver(this.c, z, inv)) glassBox.union(I.localBox(this.c, g, inv));
13682
+ const top = glassBox.isEmpty() ? B.y1 - 0.02 : Math.max(B.y1 - 0.02, glassBox.min.y + DOOR_TOP_OVER_PANE);
13683
+ this.doorTops.set(id, top);
13684
+ const rect = [B.x0 + 0.03, B.x1 - 0.03, B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1), top];
13685
+ const pcs = I.cutPanel(this.c, z, rect, true, dirty, { anyN: true }).map((x) => x.mesh).filter((m) => !!m);
13175
13686
  this.mend(dirty);
13176
- if (pcs.length) objs = [...pcs, ...I.followersIn(this.c, z, inv), ...I.windowsOver(this.c, z, inv)];
13687
+ if (pcs.length) {
13688
+ const panes = I.windowsOver(this.c, z, inv);
13689
+ const seals = panes.map((g) => I.paneSeals.get(g)).filter((s) => !!s);
13690
+ const mirrors = this.mirrorsOn(z);
13691
+ const frames = this.framesAround(panes, inv);
13692
+ objs = [...pcs, ...I.followersIn(this.c, z, inv), ...panes, ...seals, ...mirrors, ...frames];
13693
+ }
13177
13694
  } else {
13178
13695
  const pcs = this.cutZone(z);
13179
13696
  if (pcs.length) objs = [...pcs, ...I.followersIn(this.c, z, inv)];
@@ -13191,10 +13708,152 @@ var GameVehicles = (() => {
13191
13708
  const di = deform.indexOf(o);
13192
13709
  if (di >= 0) deform.splice(di, 1);
13193
13710
  }
13711
+ if (p.role === "door" && objs.length > 0) {
13712
+ this.doorEdges(piv, z, this.doorTops.get(id) ?? B.y1 - 0.02);
13713
+ this.liftDoorTrim(objs, z);
13714
+ this.cutStripsAcross(piv, z, inv);
13715
+ }
13194
13716
  piv.quaternion.copy(q);
13195
13717
  this.c.root.updateMatrixWorld(true);
13196
13718
  return objs.length > 0;
13197
13719
  }
13720
+ /** 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. */
13721
+ doorEdges(piv, z, y1) {
13722
+ const THREE = this.THREE;
13723
+ const B = z.box;
13724
+ const side2 = z.side < 0 ? -1 : 1;
13725
+ const flat = piv.position.z - side2 * 0.016;
13726
+ const x0 = B.x0 + 0.03;
13727
+ const x1 = B.x1 - 0.03;
13728
+ const y0 = B.y0 + Math.min(0.08, (B.y1 - B.y0) * 0.1);
13729
+ const loop = [];
13730
+ const run = (ax, ay, bx, by) => {
13731
+ 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]);
13732
+ };
13733
+ run(x0, y0, x1, y0);
13734
+ run(x1, y0, x1, y1);
13735
+ run(x1, y1, x0, y1);
13736
+ run(x0, y1, x0, y0);
13737
+ const heights = loop.map(([x, y]) => {
13738
+ const at = this.surface("z", side2, B, [[x, y]]);
13739
+ return at != null && Math.abs(at - flat) < DOOR_EDGE_REACH ? at : flat;
13740
+ });
13741
+ const band = (lift) => {
13742
+ const position = [];
13743
+ const index = [];
13744
+ loop.forEach(([x, y], i) => {
13745
+ position.push(x, y, heights[i] + side2 * lift, x, y, heights[i] + side2 * (lift - DOOR_DEPTH));
13746
+ const a = i * 2;
13747
+ const b = (i + 1) % loop.length * 2;
13748
+ index.push(a, a + 1, b, b, a + 1, b + 1);
13749
+ });
13750
+ const g = new THREE.BufferGeometry();
13751
+ g.setAttribute("position", new THREE.Float32BufferAttribute(position, 3));
13752
+ g.setIndex(index);
13753
+ g.computeVertexNormals();
13754
+ return g;
13755
+ };
13756
+ const paint = this.paint().clone();
13757
+ paint.side = THREE.DoubleSide;
13758
+ const cardPosition = [];
13759
+ const mid = loop.reduce((acc, [x, y], i) => [acc[0] + x, acc[1] + y, acc[2] + heights[i]], [0, 0, 0]).map((v) => v / loop.length);
13760
+ const cardZ = (z2) => z2 - side2 * DOOR_DEPTH;
13761
+ loop.forEach(([x, y], i) => {
13762
+ const j = (i + 1) % loop.length;
13763
+ cardPosition.push(mid[0], mid[1], cardZ(mid[2]), x, y, cardZ(heights[i]), loop[j][0], loop[j][1], cardZ(heights[j]));
13764
+ });
13765
+ const cardGeometry = new THREE.BufferGeometry();
13766
+ cardGeometry.setAttribute("position", new THREE.Float32BufferAttribute(cardPosition, 3));
13767
+ cardGeometry.computeVertexNormals();
13768
+ const cardMaterial = this.I.makeMaterial(DOOR_CARD_COLOR, { roughness: 0.85 }).clone();
13769
+ cardMaterial.side = THREE.DoubleSide;
13770
+ const card = new THREE.Mesh(cardGeometry, cardMaterial);
13771
+ card.userData.noFollow = true;
13772
+ card.castShadow = false;
13773
+ this.c.vehicle.add(card);
13774
+ piv.attach(card);
13775
+ const edges = new THREE.Mesh(band(2e-3), paint);
13776
+ const lining = new THREE.Mesh(band(2e-3), paint);
13777
+ for (const m of [edges, lining]) {
13778
+ m.userData.noFollow = true;
13779
+ m.castShadow = false;
13780
+ this.c.vehicle.add(m);
13781
+ }
13782
+ piv.attach(edges);
13783
+ }
13784
+ /** The frame the model draws round a door's window (a dark strip over the glass) goes with the door: left behind it hangs in the air over the opening. */
13785
+ framesAround(panes, inv) {
13786
+ const THREE = this.THREE;
13787
+ const boxes = panes.map((g) => this.I.localBox(this.c, g, inv).expandByScalar(WINDOW_FRAME_NEAR));
13788
+ return this.c.vehicle.children.filter((o) => {
13789
+ if (!o.isMesh || !o.visible || o.userData.glass || o.userData.decal || this.c.deform?.includes(o)) return false;
13790
+ const box2 = this.I.localBox(this.c, o, inv);
13791
+ if (box2.isEmpty()) return false;
13792
+ return boxes.some((b) => b.containsBox(box2) && box2.getSize(new THREE.Vector3()).x > b.getSize(new THREE.Vector3()).x * WINDOW_FRAME_SPAN);
13793
+ });
13794
+ }
13795
+ /** A front door carries the mirror drawn at its corner. */
13796
+ mirrorsOn(z) {
13797
+ if (!z.mirror) return [];
13798
+ const own = (z.mirrors ?? []).filter((o) => o.parent);
13799
+ if (own.length) return own;
13800
+ const part = this.model.parts.find((q) => q.role === "mirror" && Math.sign(q.box.center[2]) === Math.sign(z.side));
13801
+ return part ? (this.extra.get(part.id) ?? []).filter((o) => o.parent) : [];
13802
+ }
13803
+ /** The panel drawn inside the door is lit like the rest of a cabin, not a black slab from end to end. */
13804
+ liftDoorTrim(objs, z) {
13805
+ const card = this.I.makeMaterial(DOOR_LINER_COLOR, { roughness: 0.85 });
13806
+ const inv = this.I.worldInverseOf(this.c);
13807
+ for (const o of objs) {
13808
+ if (!o.isMesh || o.userData.glass) continue;
13809
+ const box2 = this.I.localBox(this.c, o, inv);
13810
+ const size = box2.getSize(new this.THREE.Vector3());
13811
+ 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;
13812
+ }
13813
+ }
13814
+ /** 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. */
13815
+ cutStripsAcross(piv, z, inv) {
13816
+ const THREE = this.THREE;
13817
+ const B = z.box;
13818
+ const x0 = B.x0 + 0.03;
13819
+ const x1 = B.x1 - 0.03;
13820
+ const y0 = B.y0 + DOOR_SILL;
13821
+ const glassTop = (I) => {
13822
+ let top = -Infinity;
13823
+ for (const g of I.windowsOver(this.c, z, inv)) top = Math.max(top, I.localBox(this.c, g, inv).max.y);
13824
+ return top;
13825
+ };
13826
+ const y1 = Math.max(B.y1, glassTop(this.I)) + DOOR_STRIP_NEAR;
13827
+ const deform = this.c.deform ?? [];
13828
+ for (const o of [...this.c.vehicle.children]) {
13829
+ if (!o.isMesh || o === piv || o.userData.glass || o.userData.decal || !o.visible || deform.includes(o) && o.geometry.attributes.position.count > DOOR_STRIP_VERTICES) continue;
13830
+ const box2 = this.I.localBox(this.c, o, inv);
13831
+ const size = box2.getSize(new THREE.Vector3());
13832
+ const mid = box2.getCenter(new THREE.Vector3());
13833
+ if (Math.sign(mid.z) !== Math.sign(z.side) || size.z > DOOR_STRIP_THIN || size.y > DOOR_STRIP_TALL || Math.min(box2.max.x, x1) - Math.max(box2.min.x, x0) < (x1 - x0) * DOOR_STRIP_OVER || mid.y < y0 || mid.y > y1) continue;
13834
+ const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld);
13835
+ const v = new THREE.Vector3();
13836
+ const sdf = (x, y, zz) => {
13837
+ v.set(x, y, zz).applyMatrix4(toVehicle);
13838
+ return Math.max(x0 - v.x, v.x - x1);
13839
+ };
13840
+ const over = new THREE.Mesh(o.geometry.clone(), o.material);
13841
+ over.position.copy(o.position);
13842
+ over.quaternion.copy(o.quaternion);
13843
+ over.scale.copy(o.scale);
13844
+ this.c.vehicle.add(over);
13845
+ over.updateMatrixWorld(true);
13846
+ const overToVehicle = new THREE.Matrix4().multiplyMatrices(inv, over.matrixWorld);
13847
+ const w = new THREE.Vector3();
13848
+ this.kit.cutMesh(over, [{ sdf: (x, y, zz) => {
13849
+ w.set(x, y, zz).applyMatrix4(overToVehicle);
13850
+ return -Math.max(x0 - w.x, w.x - x1);
13851
+ } }], STRIP_SMOOTH_DEG, {});
13852
+ this.kit.cutMesh(o, [{ sdf }], STRIP_SMOOTH_DEG, {});
13853
+ over.userData.noFollow = true;
13854
+ piv.attach(over);
13855
+ }
13856
+ }
13198
13857
  mend(dirty) {
13199
13858
  dirty.forEach((msh) => {
13200
13859
  const g = msh.geometry;
@@ -13204,7 +13863,470 @@ var GameVehicles = (() => {
13204
13863
  g.computeBoundingBox();
13205
13864
  });
13206
13865
  }
13866
+ /**
13867
+ * 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
13868
+ * comes to the shell along its own normal, by how far the shell is just outside each of its vertices.
13869
+ */
13870
+ seatPanes() {
13871
+ const THREE = this.THREE;
13872
+ const c = this.c;
13873
+ c.root.updateMatrixWorld(true);
13874
+ const shell = c.deform.filter((o) => o.isMesh && !o.userData.glass && c.paints?.has(o.material));
13875
+ if (!shell.length) return;
13876
+ const ray = new THREE.Raycaster();
13877
+ ray.layers.enableAll();
13878
+ for (const pane of c.glass) {
13879
+ if (pane.userData.glass?.outline) continue;
13880
+ const local = this.shellAround(pane, shell);
13881
+ if (!local) continue;
13882
+ const seat = this.paneSeat(pane, [local], ray);
13883
+ if (!seat) continue;
13884
+ const near = c.vehicle.children.filter((o) => o.isMesh && o !== pane && !o.userData.glass && !shell.includes(o) && this.onPane(o, pane, seat));
13885
+ for (const m of [pane, ...near]) this.movePane(m, seat);
13886
+ const past = this.shellPast(pane, [local], ray, seat.normal);
13887
+ if (past > 0) for (const m of [pane, ...near]) this.movePane(m, { normal: seat.normal, samples: [{ at: seat.samples[0].at, by: past }] });
13888
+ this.reachShellBelow(pane, local, ray, seat.normal);
13889
+ this.paneBorder(pane, near, seat);
13890
+ this.keepUnderPane(pane, seat.normal);
13891
+ const across = new this.THREE.Vector3(0, 0, 1).transformDirection(this.c.vehicle.matrixWorld);
13892
+ if (Math.abs(seat.normal.dot(across)) < SIDE_PANE_FACING) {
13893
+ for (const body of shell) if (body.parent === this.c.vehicle) this.cutUnderPane(body, pane, seat.normal, SKIN_UNDER_PANE, SKIN_EDGE_KEPT);
13894
+ }
13895
+ }
13896
+ }
13897
+ /** Only the triangles of the shell around a pane, in world space: seating casts many rays and the whole hull has too many triangles for each. */
13898
+ shellAround(pane, shell) {
13899
+ const THREE = this.THREE;
13900
+ const box2 = new THREE.Box3().setFromObject(pane).expandByScalar(PANE_SEAT_REACH + PANE_SEAT_OUT);
13901
+ const out = [];
13902
+ const a = new THREE.Vector3();
13903
+ const b = new THREE.Vector3();
13904
+ const d = new THREE.Vector3();
13905
+ const tri = new THREE.Box3();
13906
+ for (const mesh2 of shell) {
13907
+ const pos = mesh2.geometry.attributes.position;
13908
+ const idx = mesh2.geometry.index;
13909
+ const count = idx ? idx.count : pos.count;
13910
+ const e = mesh2.matrixWorld.elements;
13911
+ const world = (v, i) => {
13912
+ const x = pos.getX(i);
13913
+ const y = pos.getY(i);
13914
+ const z = pos.getZ(i);
13915
+ v.set(e[0] * x + e[4] * y + e[8] * z + e[12], e[1] * x + e[5] * y + e[9] * z + e[13], e[2] * x + e[6] * y + e[10] * z + e[14]);
13916
+ };
13917
+ for (let f = 0; f + 2 < count; f += 3) {
13918
+ world(a, idx ? idx.getX(f) : f);
13919
+ world(b, idx ? idx.getX(f + 1) : f + 1);
13920
+ world(d, idx ? idx.getX(f + 2) : f + 2);
13921
+ tri.makeEmpty().expandByPoint(a).expandByPoint(b).expandByPoint(d);
13922
+ if (tri.intersectsBox(box2)) out.push(a.x, a.y, a.z, b.x, b.y, b.z, d.x, d.y, d.z);
13923
+ }
13924
+ }
13925
+ if (!out.length) return null;
13926
+ const g = new THREE.BufferGeometry();
13927
+ g.setAttribute("position", new THREE.Float32BufferAttribute(out, 3));
13928
+ const proxy = new THREE.Mesh(g, new THREE.MeshBasicMaterial({ side: THREE.DoubleSide }));
13929
+ proxy.updateMatrixWorld(true);
13930
+ return proxy;
13931
+ }
13932
+ /** The outward normal of a pane and how far the shell is at each sampled point (world). */
13933
+ paneSeat(pane, shell, ray) {
13934
+ const THREE = this.THREE;
13935
+ const pos = pane.geometry.attributes.position;
13936
+ const box2 = new THREE.Box3().setFromObject(pane);
13937
+ const centre = box2.getCenter(new THREE.Vector3());
13938
+ const vehicleCentre = new THREE.Box3().setFromObject(this.c.root).getCenter(new THREE.Vector3());
13939
+ const tri = new THREE.Triangle();
13940
+ const normal = new THREE.Vector3();
13941
+ let largest = 0;
13942
+ const idx = pane.geometry.index;
13943
+ const count = idx ? idx.count : pos.count;
13944
+ for (let f = 0; f + 2 < count; f += 3) {
13945
+ tri.a.fromBufferAttribute(pos, idx ? idx.getX(f) : f);
13946
+ tri.b.fromBufferAttribute(pos, idx ? idx.getX(f + 1) : f + 1);
13947
+ tri.c.fromBufferAttribute(pos, idx ? idx.getX(f + 2) : f + 2);
13948
+ if (tri.getArea() > largest) {
13949
+ largest = tri.getArea();
13950
+ tri.getNormal(normal);
13951
+ }
13952
+ }
13953
+ normal.transformDirection(pane.matrixWorld);
13954
+ if (normal.dot(centre.clone().sub(vehicleCentre)) < 0) normal.negate();
13955
+ const samples = [];
13956
+ const seen = /* @__PURE__ */ new Set();
13957
+ const v = new THREE.Vector3();
13958
+ const step = Math.max(1, Math.floor(pos.count / PANE_SEAT_SAMPLES));
13959
+ for (let i = 0; i < pos.count; i += step) {
13960
+ v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
13961
+ const key = `${Math.round(v.x * 100)},${Math.round(v.y * 100)},${Math.round(v.z * 100)}`;
13962
+ if (seen.has(key)) continue;
13963
+ seen.add(key);
13964
+ const away = v.clone().sub(centre);
13965
+ away.addScaledVector(normal, -away.dot(normal)).normalize();
13966
+ let by = -Infinity;
13967
+ for (const off of [PANE_SEAT_OUT, PANE_SEAT_IN, PANE_SEAT_IN * 3]) {
13968
+ const at = v.clone().addScaledVector(away, off);
13969
+ ray.set(at.addScaledVector(normal, PANE_SEAT_REACH), normal.clone().negate());
13970
+ ray.far = PANE_SEAT_REACH * 2;
13971
+ const hit = raycastIndexed(THREE, ray, shell)[0];
13972
+ if (hit) by = Math.max(by, PANE_SEAT_REACH - hit.distance);
13973
+ }
13974
+ if (by > -Infinity) samples.push({ at: v.clone(), by });
13975
+ }
13976
+ return samples.length >= 3 ? { normal, samples } : null;
13977
+ }
13978
+ /** The lower edge of a pane comes down to the shell: left short, the dashboard shows through the slit as a black bar under the glass. */
13979
+ reachShellBelow(pane, shell, ray, normal) {
13980
+ const THREE = this.THREE;
13981
+ const down = new THREE.Vector3(0, -1, 0).applyQuaternion(this.c.root.getWorldQuaternion(new THREE.Quaternion()));
13982
+ down.addScaledVector(normal, -down.dot(normal));
13983
+ if (down.lengthSq() < PANE_MIN_SLOPE) return;
13984
+ down.normalize();
13985
+ const g = pane.geometry;
13986
+ const pos = g.attributes.position;
13987
+ const v = new THREE.Vector3();
13988
+ let lowest = -Infinity;
13989
+ for (let i = 0; i < pos.count; i++) lowest = Math.max(lowest, v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld).dot(down));
13990
+ const edge = [];
13991
+ const mid = new THREE.Vector3();
13992
+ for (let i = 0; i < pos.count; i++) {
13993
+ v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
13994
+ if (v.dot(down) > lowest - PANE_EDGE_BAND) {
13995
+ edge.push(i);
13996
+ mid.add(v);
13997
+ }
13998
+ }
13999
+ if (!edge.length) return;
14000
+ mid.divideScalar(edge.length);
14001
+ let reach = 0;
14002
+ for (let d = PANE_REACH_STEP; d <= PANE_REACH_MAX; d += PANE_REACH_STEP) {
14003
+ const at = mid.clone().addScaledVector(down, d).addScaledVector(normal, PANE_SEAT_REACH);
14004
+ ray.set(at, normal.clone().negate());
14005
+ ray.far = PANE_SEAT_REACH * 2;
14006
+ if (ray.intersectObject(shell, false)[0]) {
14007
+ reach = d + PANE_REACH_STEP;
14008
+ break;
14009
+ }
14010
+ }
14011
+ if (!reach) return;
14012
+ const inverse = pane.matrixWorld.clone().invert();
14013
+ for (const i of edge) {
14014
+ v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld).addScaledVector(down, reach).applyMatrix4(inverse);
14015
+ pos.setXYZ(i, v.x, v.y, v.z);
14016
+ }
14017
+ pos.needsUpdate = true;
14018
+ g.computeBoundingBox();
14019
+ g.computeBoundingSphere();
14020
+ }
14021
+ /** The plate drawn right under a windshield stays only outside the glass, in the body's paint: under the glass it would show inside the cabin. */
14022
+ keepUnderPane(pane, normal) {
14023
+ const THREE = this.THREE;
14024
+ const paneBox2 = new THREE.Box3().setFromObject(pane).expandByScalar(COWL_NEAR_PANE);
14025
+ const pos = pane.geometry.attributes.position;
14026
+ const centre = new THREE.Vector3();
14027
+ const v = new THREE.Vector3();
14028
+ for (let i = 0; i < pos.count; i++) centre.add(v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld));
14029
+ centre.divideScalar(pos.count);
14030
+ const plane2 = centre.dot(normal);
14031
+ for (const o of this.c.vehicle.children) {
14032
+ if (!o.isMesh || o === pane || o.userData.glass || this.c.deform?.includes(o)) continue;
14033
+ const box2 = new THREE.Box3().setFromObject(o);
14034
+ const size = box2.getSize(new THREE.Vector3());
14035
+ if (o.geometry.attributes.position.count <= PANE_BAR_VERTICES || size.y > COWL_DASH || Math.max(size.x, size.z) < COWL_LONG || Math.min(size.x, size.z) < COWL_DEEP || !paneBox2.intersectsBox(box2)) continue;
14036
+ if (box2.getCenter(new THREE.Vector3()).dot(normal) - plane2 >= COWL_UNDER_GLASS) continue;
14037
+ if (size.y >= COWL_PLATE) {
14038
+ o.material = this.I.makeMaterial(DASHBOARD_COLOR, { roughness: 1, metalness: 0 });
14039
+ continue;
14040
+ }
14041
+ o.material = this.paint();
14042
+ this.cutUnderPane(o, pane, normal);
14043
+ }
14044
+ }
14045
+ /** Cuts away the part of a mesh that lies under a pane (behind its plane, inside its outline seen along its normal). */
14046
+ cutUnderPane(mesh2, pane, normal, behind = -Infinity, inset = 0) {
14047
+ const THREE = this.THREE;
14048
+ const outline = this.paneOutline(pane, normal);
14049
+ if (outline.length < 3) return;
14050
+ const u = new THREE.Vector3(0, 1, 0).cross(normal);
14051
+ if (u.lengthSq() < 1e-4) u.set(1, 0, 0);
14052
+ u.normalize();
14053
+ const w = normal.clone().cross(u);
14054
+ const cx = outline.reduce((sum, p) => sum + p.dot(u), 0) / outline.length;
14055
+ const cy = outline.reduce((sum, p) => sum + p.dot(w), 0) / outline.length;
14056
+ const poly = outline.map((p) => {
14057
+ const x = p.dot(u);
14058
+ const y = p.dot(w);
14059
+ const d = Math.hypot(x - cx, y - cy) || 1;
14060
+ const k = Math.max(0, d - inset) / d;
14061
+ return [cx + (x - cx) * k, cy + (y - cy) * k];
14062
+ });
14063
+ const plane2 = outline.reduce((sum, p) => sum + p.dot(normal), 0) / outline.length;
14064
+ const inside = (x, y) => {
14065
+ let hit = false;
14066
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
14067
+ const [xi, yi] = poly[i];
14068
+ const [xj, yj] = poly[j];
14069
+ if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) hit = !hit;
14070
+ }
14071
+ return hit;
14072
+ };
14073
+ const v = new THREE.Vector3();
14074
+ const toWorld = mesh2.matrixWorld;
14075
+ mesh2.updateMatrixWorld(true);
14076
+ const xs = poly.map((p) => p[0]);
14077
+ const ys = poly.map((p) => p[1]);
14078
+ const [x0, x1, y0, y1] = [Math.min(...xs) - COWL_CUT_MARGIN, Math.max(...xs) + COWL_CUT_MARGIN, Math.min(...ys) - COWL_CUT_MARGIN, Math.max(...ys) + COWL_CUT_MARGIN];
14079
+ subdivideNear(THREE, { mesh: mesh2, maxLen: COWL_CUT_SUB, near: (corners) => {
14080
+ const us = corners.map((c) => c.dot(u));
14081
+ const ws = corners.map((c) => c.dot(w));
14082
+ return Math.max(...us) >= x0 && Math.min(...us) <= x1 && Math.max(...ws) >= y0 && Math.min(...ws) <= y1;
14083
+ } });
14084
+ this.kit.cutMesh(
14085
+ mesh2,
14086
+ [
14087
+ {
14088
+ sdf: (x, y, z) => {
14089
+ v.set(x, y, z).applyMatrix4(toWorld);
14090
+ return inside(v.dot(u), v.dot(w)) && plane2 - v.dot(normal) > behind ? -1 : 1;
14091
+ }
14092
+ }
14093
+ ],
14094
+ 42,
14095
+ {}
14096
+ );
14097
+ }
14098
+ /** The straight bars drawn round a quad pane stand off its edges: they give way to a thin dark band that lies on the glass, as on a bonded windshield. */
14099
+ paneBorder(pane, near, seat) {
14100
+ const THREE = this.THREE;
14101
+ const bars = near.filter((o) => {
14102
+ if (o.geometry.attributes.position.count > PANE_BAR_VERTICES) return false;
14103
+ const size = new THREE.Box3().setFromObject(o).getSize(new THREE.Vector3());
14104
+ return Math.max(size.x, size.y, size.z) > PANE_BAR_LENGTH;
14105
+ });
14106
+ if (!bars.length) return;
14107
+ const outline = this.paneOutline(pane, seat.normal);
14108
+ if (outline.length < 3) return;
14109
+ const centre = new THREE.Vector3();
14110
+ for (const p of outline) centre.add(p);
14111
+ centre.divideScalar(outline.length);
14112
+ const position = [];
14113
+ const toVehicle = this.I.worldInverseOf(this.c);
14114
+ const put = (p) => {
14115
+ const q = p.clone().addScaledVector(seat.normal, PANE_BORDER_LIFT).applyMatrix4(toVehicle);
14116
+ position.push(q.x, q.y, q.z);
14117
+ };
14118
+ const inward = (p) => p.clone().add(centre.clone().sub(p).normalize().multiplyScalar(PANE_BORDER));
14119
+ outline.forEach((a, i) => {
14120
+ const b = outline[(i + 1) % outline.length];
14121
+ const c = inward(b);
14122
+ const d = inward(a);
14123
+ for (const p of [a, b, c, a, c, d]) put(p);
14124
+ });
14125
+ if (!position.length) return;
14126
+ for (const o of bars) o.visible = false;
14127
+ const band = new THREE.BufferGeometry();
14128
+ band.setAttribute("position", new THREE.Float32BufferAttribute(position, 3));
14129
+ band.computeVertexNormals();
14130
+ const material = this.I.MATERIALS.dark.clone();
14131
+ material.side = THREE.DoubleSide;
14132
+ const mesh2 = new THREE.Mesh(band, material);
14133
+ mesh2.userData.noFollow = true;
14134
+ mesh2.castShadow = false;
14135
+ mesh2.raycast = () => {
14136
+ };
14137
+ this.c.vehicle.add(mesh2);
14138
+ }
14139
+ /** The outline of a pane, the convex hull of its vertices seen along its normal, in world space. */
14140
+ paneOutline(pane, normal) {
14141
+ const THREE = this.THREE;
14142
+ const pos = pane.geometry.attributes.position;
14143
+ const u = new THREE.Vector3(0, 1, 0).cross(normal);
14144
+ if (u.lengthSq() < 1e-4) u.set(1, 0, 0);
14145
+ u.normalize();
14146
+ const w = normal.clone().cross(u);
14147
+ const points = [];
14148
+ for (let i = 0; i < pos.count; i++) {
14149
+ const p = new THREE.Vector3().fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
14150
+ points.push({ x: p.dot(u), y: p.dot(w), p });
14151
+ }
14152
+ points.sort((a, b) => a.x - b.x || a.y - b.y);
14153
+ const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
14154
+ const half = (list2) => {
14155
+ const out = [];
14156
+ for (const q of list2) {
14157
+ while (out.length >= 2 && cross(out[out.length - 2], out[out.length - 1], q) <= 0) out.pop();
14158
+ out.push(q);
14159
+ }
14160
+ out.pop();
14161
+ return out;
14162
+ };
14163
+ return [...half(points), ...half([...points].reverse())].map((q) => q.p);
14164
+ }
14165
+ /** How far the shell still stands past a seated pane, over its triangles. */
14166
+ shellPast(pane, shell, ray, normal) {
14167
+ const THREE = this.THREE;
14168
+ const pos = pane.geometry.attributes.position;
14169
+ const idx = pane.geometry.index;
14170
+ const count = idx ? idx.count : pos.count;
14171
+ const tri = new THREE.Triangle();
14172
+ const step = Math.max(1, Math.floor(count / 3 / PANE_SEAT_SAMPLES)) * 3;
14173
+ let past = 0;
14174
+ for (let f = 0; f + 2 < count; f += step) {
14175
+ tri.a.fromBufferAttribute(pos, idx ? idx.getX(f) : f);
14176
+ tri.b.fromBufferAttribute(pos, idx ? idx.getX(f + 1) : f + 1);
14177
+ tri.c.fromBufferAttribute(pos, idx ? idx.getX(f + 2) : f + 2);
14178
+ const mid = tri.getMidpoint(new THREE.Vector3()).applyMatrix4(pane.matrixWorld);
14179
+ ray.set(mid.addScaledVector(normal, PANE_SEAT_REACH), normal.clone().negate());
14180
+ ray.far = PANE_SEAT_REACH;
14181
+ const hit = raycastIndexed(THREE, ray, shell)[0];
14182
+ if (hit) past = Math.max(past, PANE_SEAT_REACH - hit.distance + PANE_SEAT_LIFT);
14183
+ }
14184
+ return past;
14185
+ }
14186
+ /** A frame or a seal drawn on the pane: small, and close to its plane. */
14187
+ onPane(o, pane, seat) {
14188
+ const THREE = this.THREE;
14189
+ const box2 = new THREE.Box3().setFromObject(o);
14190
+ if (box2.isEmpty() || !new THREE.Box3().setFromObject(pane).expandByScalar(PANE_FRAME_NEAR).containsBox(box2)) return false;
14191
+ const at = box2.getCenter(new THREE.Vector3());
14192
+ return Math.abs(at.clone().sub(seat.samples[0].at).dot(seat.normal)) < PANE_SEAT_REACH;
14193
+ }
14194
+ movePane(mesh2, seat) {
14195
+ const THREE = this.THREE;
14196
+ const g = mesh2.geometry.clone();
14197
+ const pos = g.attributes.position;
14198
+ const inverse = mesh2.matrixWorld.clone().invert();
14199
+ const v = new THREE.Vector3();
14200
+ for (let i = 0; i < pos.count; i++) {
14201
+ v.fromBufferAttribute(pos, i).applyMatrix4(mesh2.matrixWorld);
14202
+ let weight = 0;
14203
+ let by = 0;
14204
+ for (const s of seat.samples) {
14205
+ const w = 1 / (s.at.distanceToSquared(v) + 1e-4);
14206
+ weight += w;
14207
+ by += s.by * w;
14208
+ }
14209
+ v.addScaledVector(seat.normal, by / weight + PANE_SEAT_LIFT).applyMatrix4(inverse);
14210
+ pos.setXYZ(i, v.x, v.y, v.z);
14211
+ }
14212
+ pos.needsUpdate = true;
14213
+ g.computeBoundingBox();
14214
+ g.computeBoundingSphere();
14215
+ mesh2.geometry = g;
14216
+ }
14217
+ /** A closed hood is flush with the fenders beside it: the height it was drawn with to keep clear of the hull goes, the hull has its hole. */
14218
+ seatHood() {
14219
+ const hood = this.c.hood;
14220
+ if (!hood?.lift) return;
14221
+ const THREE = this.THREE;
14222
+ const box2 = new THREE.Box3().setFromObject(hood.piv).getSize(new THREE.Vector3());
14223
+ for (const part of hood.piv.children) {
14224
+ part.position.y -= hood.lift + HOOD_SINK;
14225
+ part.scale.multiply(new THREE.Vector3(1 + HOOD_GAP_CLOSED / Math.max(0.3, box2.x), 1, 1 + HOOD_GAP_CLOSED / Math.max(0.3, box2.z)));
14226
+ }
14227
+ }
14228
+ /** A handle is at the free edge of its door, the end away from the hinge. */
14229
+ seatDoorHandles() {
14230
+ const THREE = this.THREE;
14231
+ const c = this.c;
14232
+ const inv = this.I.worldInverseOf(c);
14233
+ const bodyBox = new THREE.Box3();
14234
+ for (const o of c.deform) if (o.isMesh && !o.userData.glass && c.paints?.has(o.material)) bodyBox.union(this.I.localBox(c, o, inv));
14235
+ const outerHalf = Math.max(Math.abs(bodyBox.min.z), Math.abs(bodyBox.max.z));
14236
+ for (const p of this.model.parts) {
14237
+ if (p.role !== "door") continue;
14238
+ const z = this.zone(p);
14239
+ if (!z) continue;
14240
+ const B = z.box;
14241
+ const free = z.hinge === "rear" ? B.x1 - DOOR_HANDLE_INSET : B.x0 + DOOR_HANDLE_INSET;
14242
+ for (const o of c.vehicle.children) {
14243
+ if (!o.isMesh || o.material !== this.I.MATERIALS.chrome || o.userData.glass) continue;
14244
+ const box2 = this.I.localBox(c, o, inv);
14245
+ const size = box2.getSize(new THREE.Vector3());
14246
+ const at = box2.getCenter(new THREE.Vector3());
14247
+ const isHandle = size.x > DOOR_HANDLE_MIN && size.x < DOOR_HANDLE_MAX && size.y < DOOR_HANDLE_THIN && size.z < DOOR_HANDLE_THIN;
14248
+ 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;
14249
+ if (Math.abs(at.z) < outerHalf - DOOR_HANDLE_OUTSIDE) continue;
14250
+ const skin = this.surface("z", z.side < 0 ? -1 : 1, B, [[free, at.y]]);
14251
+ o.position.x += free - at.x;
14252
+ if (skin != null) o.position.z += skin + (z.side < 0 ? -1 : 1) * (size.z / 2) - at.z;
14253
+ }
14254
+ }
14255
+ c.root.updateMatrixWorld(true);
14256
+ }
14257
+ /** The glass of a drawn mirror sits inside its rounded housing, without corners standing past it. */
14258
+ seatMirrorGlass(glass, drawn) {
14259
+ const g = glass.geometry.clone();
14260
+ g.computeBoundingBox();
14261
+ const mid = g.boundingBox.getCenter(new this.THREE.Vector3());
14262
+ g.translate(-mid.x, -mid.y, -mid.z);
14263
+ g.scale(MIRROR_GLASS_FIT, MIRROR_GLASS_FIT, MIRROR_GLASS_FIT);
14264
+ g.translate(mid.x, mid.y, mid.z);
14265
+ glass.geometry = g;
14266
+ const THREE = this.THREE;
14267
+ const volumeOf = (o) => {
14268
+ const size = new THREE.Box3().setFromObject(o).getSize(new THREE.Vector3());
14269
+ return size.x * size.y * size.z;
14270
+ };
14271
+ const housing = drawn.filter((o) => o !== glass && o.isMesh && o.geometry).sort((a, b) => volumeOf(b) - volumeOf(a))[0];
14272
+ if (housing) {
14273
+ this.c.root.updateMatrixWorld(true);
14274
+ const inv = this.I.worldInverseOf(this.c);
14275
+ glass.updateMatrixWorld(true);
14276
+ const gb = this.I.localBox(this.c, glass, inv);
14277
+ const hb = this.I.localBox(this.c, housing, inv);
14278
+ const side2 = hb.getCenter(new THREE.Vector3()).x > gb.getCenter(new THREE.Vector3()).x ? -1 : 1;
14279
+ const face = side2 < 0 ? hb.min.x + MIRROR_FACE_DEPTH : hb.max.x - MIRROR_FACE_DEPTH;
14280
+ const shape = housing.geometry.clone();
14281
+ const pos = shape.attributes.position;
14282
+ const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, housing.matrixWorld);
14283
+ const back = toVehicle.clone().invert();
14284
+ const v = new THREE.Vector3();
14285
+ for (let i = 0; i < pos.count; i++) {
14286
+ v.fromBufferAttribute(pos, i).applyMatrix4(toVehicle);
14287
+ if ((v.x - face) * side2 <= 0) continue;
14288
+ v.x = face;
14289
+ v.applyMatrix4(back);
14290
+ pos.setXYZ(i, v.x, v.y, v.z);
14291
+ }
14292
+ shape.computeVertexNormals();
14293
+ housing.geometry = shape;
14294
+ const outer = side2 < 0 ? gb.min.x : gb.max.x;
14295
+ glass.position.x += face + side2 * MIRROR_GLASS_PROUD - outer;
14296
+ }
14297
+ glass.material = this.I.makeMaterial(MIRROR_GLASS_COLOR, { metalness: 0.95, roughness: 0.12 });
14298
+ }
14299
+ /** The mirror the model already draws around where the part says, if it draws one. */
14300
+ drawnMirror(p) {
14301
+ const THREE = this.THREE;
14302
+ const [cx, cy, cz] = p.box.center;
14303
+ this.c.root.updateMatrixWorld(true);
14304
+ const inv = this.I.worldInverseOf(this.c);
14305
+ const small = this.c.vehicle.children.flatMap((o) => {
14306
+ if (o.userData.glass || o.userData.decal || !(o.isMesh || o.isGroup) || !o.visible || this.c.deform?.includes(o)) return [];
14307
+ const box2 = this.I.localBox(this.c, o, inv);
14308
+ if (box2.isEmpty()) return [];
14309
+ const size = box2.getSize(new THREE.Vector3());
14310
+ return Math.max(size.x, size.y, size.z) < DRAWN_MIRROR_SIZE ? [{ o, at: box2.getCenter(new THREE.Vector3()) }] : [];
14311
+ });
14312
+ 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);
14313
+ return small.filter(({ at }) => housing.some((h) => h.at.distanceTo(at) < DRAWN_MIRROR_MOUNT)).map(({ o }) => o);
14314
+ }
13207
14315
  makeMirror(p) {
14316
+ const drawn = this.drawnMirror(p);
14317
+ if (drawn.length) {
14318
+ for (const o of drawn) {
14319
+ if (!o.isMesh || o.material !== this.I.MATERIALS.chrome) continue;
14320
+ const size = new this.THREE.Box3().setFromObject(o).getSize(new this.THREE.Vector3());
14321
+ const [thin, mid] = size.toArray().sort((a, b) => a - b);
14322
+ if (thin < MIRROR_GLASS_THIN && mid > MIRROR_GLASS_MIN) {
14323
+ this.seatMirrorGlass(o, drawn);
14324
+ o.userData.mirrorGlass = true;
14325
+ }
14326
+ }
14327
+ this.extra.set(p.id, drawn);
14328
+ return;
14329
+ }
13208
14330
  const [cx, cy, cz] = p.box.center;
13209
14331
  const s = cz < 0 ? -1 : 1;
13210
14332
  const mats = this.I.MATERIALS;
@@ -13212,14 +14334,26 @@ var GameVehicles = (() => {
13212
14334
  g.name = `obj_${p.id}`;
13213
14335
  g.position.set(cx, cy, cz);
13214
14336
  this.c.vehicle.add(g);
13215
- this.box(0.05, 0.04, 0.1, mats.dark, 0, -0.04, -s * 0.07, g);
13216
- const housing = this.box(0.1, 0.12, 0.05, mats.plastic ?? mats.dark, 0, 0, s * 0.02, g);
14337
+ this.box(0.05, 0.035, 0.05, mats.dark, 0, -0.04, -s * 0.06, g);
14338
+ const housing = this.box(0.08, 0.11, 0.125, mats.plastic ?? mats.dark, 0, 0, s * 0.0225, g);
13217
14339
  housing.userData.mirrorHousing = true;
13218
- const glass = this.box(0.095, 0.1, 6e-3, mats.chrome ?? mats.dark, 0, 0, s * 0.048, g);
14340
+ const glass = this.box(6e-3, 0.095, 0.115, mats.chrome ?? mats.dark, -0.043, 0, s * 0.0225, g);
13219
14341
  glass.userData.mirrorGlass = true;
13220
14342
  g.userData.rigid = true;
13221
14343
  this.extra.set(p.id, [g]);
13222
14344
  }
14345
+ /** A round through the mirror shatters its glass: the shards drop out of the housing, which stays on with the hole. Returns whether there was glass to break. */
14346
+ shatterMirror(id) {
14347
+ let broke = false;
14348
+ for (const o of this.extra.get(id) ?? []) {
14349
+ o.traverse((m) => {
14350
+ if (!m.userData.mirrorGlass || !m.visible) return;
14351
+ m.visible = false;
14352
+ broke = true;
14353
+ });
14354
+ }
14355
+ return broke;
14356
+ }
13223
14357
  makeBattery(p) {
13224
14358
  const mats = this.I.MATERIALS;
13225
14359
  const g = new this.THREE.Group();
@@ -13245,17 +14379,33 @@ var GameVehicles = (() => {
13245
14379
  }
13246
14380
  // ---- marks -------------------------------------------------------------------------------------------------------------------------------
13247
14381
  /** A bullet hole: a dark disc lying on the surface at `point` (`c.vehicle` coordinates). */
13248
- holeDecal(point, normal, diameter) {
14382
+ holeDecal(point, normal, diameter, part) {
13249
14383
  const THREE = this.THREE;
13250
- const m = new THREE.Mesh(new THREE.CircleGeometry(Math.max(6e-3, diameter / 2), 10), this.I.MATERIALS.dark);
14384
+ const pane = part?.role === "glass" ? this.panesOf(part).find((g) => g.visible) : void 0;
14385
+ if (part?.role === "glass" && (!pane || pane.userData.glass?.broken)) return;
14386
+ const radius = Math.max(6e-3, diameter / 2) * HOLE_DECAL_SCALE;
14387
+ const m = new THREE.Mesh(holeDiscGeometry(THREE), holeMaterial(this.kit, part?.material ?? "steel"));
14388
+ m.scale.set(radius, radius, 1);
13251
14389
  const n = new THREE.Vector3(...normal);
13252
- m.position.set(...point).addScaledVector(n, 4e-3);
13253
- m.lookAt(m.position.clone().add(n));
14390
+ m.position.set(...point).addScaledVector(n, 3e-3);
14391
+ this.c.vehicle.add(m);
14392
+ m.lookAt(this.c.vehicle.localToWorld(m.position.clone().add(n)));
13254
14393
  m.userData.decal = true;
13255
14394
  m.userData.noFollow = true;
14395
+ m.renderOrder = 3;
13256
14396
  m.raycast = () => {
13257
14397
  };
13258
- this.c.vehicle.add(m);
14398
+ 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);
14399
+ if (carrier) carrier.attach(m);
14400
+ }
14401
+ /** The door or lid a hinge group stands for. */
14402
+ /** The part a separately drawn piece stands for (a mirror on the door, a battery under the hood), so a shot at it lands on that part. */
14403
+ extraIdOf(o) {
14404
+ for (const [id, objs] of this.extra) if (objs.includes(o)) return id;
14405
+ return void 0;
14406
+ }
14407
+ plateIdOf(o) {
14408
+ return o.userData.plateOf;
13259
14409
  }
13260
14410
  spark(point, dir, count = 8) {
13261
14411
  const V3 = this.THREE.Vector3;
@@ -13295,10 +14445,8 @@ var GameVehicles = (() => {
13295
14445
  }
13296
14446
  /** Glass: cracks (laminated) or shatters (tempered); returns whether it broke. */
13297
14447
  hitGlass(p, point, dir, force) {
13298
- const z = this.zone(p);
13299
- if (!z) return false;
13300
14448
  const V3 = this.THREE.Vector3;
13301
- const list2 = z.glass.filter((g) => g.parent && g.visible);
14449
+ const list2 = this.panesOf(p).filter((g) => g.parent && g.visible);
13302
14450
  if (!list2.length) return false;
13303
14451
  const pt = new V3(...point);
13304
14452
  let best = list2[0];
@@ -13312,12 +14460,20 @@ var GameVehicles = (() => {
13312
14460
  best = g;
13313
14461
  }
13314
14462
  }
14463
+ if (p.hp > LAMINATED_HP && best.userData.glass) best.userData.glass.type = "laminated";
13315
14464
  this.kit.hitGlass(this.c, best, pt, new V3(...dir), force);
13316
14465
  return !!best.userData.glass?.broken;
13317
14466
  }
14467
+ panesOf(p) {
14468
+ return p.panes ?? this.zone(p)?.glass ?? [];
14469
+ }
13318
14470
  glassBreak(p) {
13319
14471
  const z = this.zone(p);
13320
- if (!z) return;
14472
+ if (!z) {
14473
+ 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);
14474
+ this.gone.add(p.id);
14475
+ return;
14476
+ }
13321
14477
  for (const g of z.glass) if (g.parent && g.visible) this.I.glassBand(this.c, z, g, 4);
13322
14478
  this.gone.add(p.id);
13323
14479
  }
@@ -13326,6 +14482,17 @@ var GameVehicles = (() => {
13326
14482
  if (!z) return;
13327
14483
  for (const w of z.wheels) if (w.parent) this.I.flattenTire(w, k);
13328
14484
  }
14485
+ /** A tyre driven flat until it tore apart: the rubber is gone and the bare rim shows. */
14486
+ tyreTornOff(p) {
14487
+ const z = this.zone(p);
14488
+ if (!z) return;
14489
+ for (const w of z.wheels) {
14490
+ if (!w.parent || w.userData.tornOff) continue;
14491
+ w.userData.tornOff = true;
14492
+ for (const m of this.I.tireMeshes(w)) m.visible = false;
14493
+ w.position.y -= (w.userData.r ?? DEFAULT_WHEEL_RADIUS) * SIDEWALL_SHARE;
14494
+ }
14495
+ }
13329
14496
  /** Burning rubber: the tyre sags and goes black as it melts. */
13330
14497
  tyreMelt(p, melt) {
13331
14498
  const z = this.zone(p);
@@ -13345,6 +14512,7 @@ var GameVehicles = (() => {
13345
14512
  smoothLater(msh) {
13346
14513
  this.smoothing.set(msh, this.I.creaseTask(msh.geometry, 42));
13347
14514
  this.tornThisFrame = true;
14515
+ this.holdArmed = true;
13348
14516
  }
13349
14517
  /** Spends at most `budgetMs` finishing the pending normal smoothing. Call once per frame. */
13350
14518
  pump(budgetMs = 2.5) {
@@ -13352,6 +14520,7 @@ var GameVehicles = (() => {
13352
14520
  this.tornThisFrame = false;
13353
14521
  return;
13354
14522
  }
14523
+ if (this.queued) return;
13355
14524
  if (!this.smoothing.size && !this.warming) return;
13356
14525
  const deadline = performance.now() + budgetMs;
13357
14526
  for (const [msh, task] of this.smoothing) {
@@ -13360,6 +14529,34 @@ var GameVehicles = (() => {
13360
14529
  }
13361
14530
  this.warmIndexes(deadline);
13362
14531
  }
14532
+ setQueued(isQueued) {
14533
+ this.queued = isQueued;
14534
+ }
14535
+ /** 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). */
14536
+ pendingUnits(frame) {
14537
+ if (this.holdArmed && frame !== void 0) {
14538
+ this.holdArmed = false;
14539
+ this.holdFrame = frame;
14540
+ }
14541
+ if (frame !== void 0 && frame === this.holdFrame) return 0;
14542
+ if (frame === void 0 && this.tornThisFrame) return 0;
14543
+ return this.smoothing.size + (this.warming ? 1 : 0);
14544
+ }
14545
+ /** One slice of the pending work, for the shared queue. */
14546
+ runStep(deadline) {
14547
+ const next = this.smoothing.entries().next();
14548
+ if (!next.done) {
14549
+ const [msh, task] = next.value;
14550
+ if (msh.geometry !== task.geo || task.step(deadline)) this.smoothing.delete(msh);
14551
+ return true;
14552
+ }
14553
+ if (!this.warming) return false;
14554
+ this.warmIndexes(deadline);
14555
+ return true;
14556
+ }
14557
+ finishAll() {
14558
+ while (this.runStep(Infinity)) ;
14559
+ }
13363
14560
  /** 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
14561
  warmIndexes(deadline) {
13365
14562
  if (!this.warming) return;
@@ -13531,6 +14728,7 @@ var GameVehicles = (() => {
13531
14728
  }
13532
14729
  pieceTasks.forEach((task, mesh2) => this.smoothing.set(mesh2, task));
13533
14730
  this.tornThisFrame = true;
14731
+ this.holdArmed = true;
13534
14732
  dirty.forEach((msh) => {
13535
14733
  const g = msh.geometry;
13536
14734
  if (g.userData.crease || !g.index) this.smoothLater(msh);
@@ -13618,6 +14816,41 @@ var GameVehicles = (() => {
13618
14816
  var SHIELDED = /* @__PURE__ */ new Set(["engine", "gearbox", "frame", "radiator", "fuel_tank", "oil_pan", "battery", "alternator", "brakes", "steering", "shocks"]);
13619
14817
  var AMBIENT = 20;
13620
14818
  var FLAME = 700;
14819
+ var STEEL_MM = { panel: 0.8, door: 1.6, hood: 0.8, lid: 0.8, roof: 0.8, fender: 0.8, pillar: 2, frame: 4, rim: 3, engine: 40, gearbox: 25, fuel_tank: 1, radiator: 1, battery: 2, brakes: 10, steering: 8, shocks: 6, oil_pan: 2, alternator: 4, hitch: 8, other: 2 };
14820
+ var ARMOUR_STEEL_MM = { IIIA: 4, III: 7 };
14821
+ var SOFT_LEAD = 0.8;
14822
+ var TYRE_FLAT_HEALTH = 0.1;
14823
+ var TYRE_BURST_JOULES = 1500;
14824
+ var TYRE_BURST_CALIBER = 12;
14825
+ var TYRE_LEAK_PER_MM2 = 26e-5;
14826
+ var FLAT_TYRE_LIFE_M = 1500;
14827
+ var FLAT_TYRE_TOWN_SPEED = 8;
14828
+ var RUN_FLAT_LIFE_M = 8e4;
14829
+ var RUN_FLAT_SPEED = 22;
14830
+ var TYRE_RUNFLAT_HEALTH = 0.6;
14831
+ var CRUMPLE_BASE_M = 0.04;
14832
+ var CRUMPLE_PER_FORCE_M = 0.15;
14833
+ var CRUMPLE_MIN_FORCE = 0.3;
14834
+ var CRUMPLE_MAX_FORCE = 1.4;
14835
+ var RIM_HEALTH = 0.05;
14836
+ var RIM_GRIND_PER_M = 4e-4;
14837
+ var TYRE_FLAT_SAG = 0.24;
14838
+ var TYRE_FLAT_AIR = 0.2;
14839
+ var BRAKE_FLUID_NEEDED = 0.6;
14840
+ var BRAKE_NO_FLUID = 0.15;
14841
+ var ENGINE_WORKING_C = 90;
14842
+ var ENGINE_SETTLE_S = 30;
14843
+ var ENGINE_HEATING = 0.5;
14844
+ var ENGINE_STEAM_C = 120;
14845
+ var ENGINE_SEIZE_C = 150;
14846
+ var COOLANT_NEEDED = 0.4;
14847
+ var OIL_NEEDED = 0.3;
14848
+ var OIL_WEAR = 4e-3;
14849
+ var ENGINE_SEIZE_HP = 0.2;
14850
+ var EXPANDED_HOLE = 1.6;
14851
+ var MIN_SPENT = 0.08;
14852
+ var WELDED = /* @__PURE__ */ new Set(["roof", "pillar", "frame", "panel"]);
14853
+ var CRUMPLES = /* @__PURE__ */ new Set(["door", "hood", "lid", "fender", "bumper"]);
13621
14854
  var FRACTURE = { glass: "shatter", plastic: "snap", rubber: "tear", steel: "none", aluminium: "none" };
13622
14855
  var add3 = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
13623
14856
  var r3 = (v) => [Math.round(v[0] * 1e5) / 1e5, Math.round(v[1] * 1e5) / 1e5, Math.round(v[2] * 1e5) / 1e5];
@@ -13629,6 +14862,8 @@ var GameVehicles = (() => {
13629
14862
  let c = vehicleCtx;
13630
14863
  const id = options.id ?? c.key;
13631
14864
  const headless = !!options.headless;
14865
+ const armourId = armourLevelOf(c.meta, options.armour);
14866
+ const armourLevel = armourId ? { ...ARMOUR_LEVELS[armourId], ...options.armourLevels?.[armourId] } : null;
13632
14867
  const seedBase = strHash(`${c.key}|${id}|${options.seed ?? 0}`);
13633
14868
  let rand = rng(seedBase);
13634
14869
  const ignitionRules = resolveIgnitionRules(options.ignition);
@@ -13637,6 +14872,7 @@ var GameVehicles = (() => {
13637
14872
  let clock = 0;
13638
14873
  let model;
13639
14874
  let vis = null;
14875
+ let releaseCrease;
13640
14876
  const ps = /* @__PURE__ */ new Map();
13641
14877
  let js = [];
13642
14878
  let leaks = /* @__PURE__ */ new Map();
@@ -13685,7 +14921,7 @@ var GameVehicles = (() => {
13685
14921
  return d.ignitionC ? 1 : 0;
13686
14922
  };
13687
14923
  const setup = () => {
13688
- model = buildVehicleModel(kit, c, { hitch: options.hitch });
14924
+ model = buildVehicleModel(kit, c, { hitch: options.hitch, armour: armourLevel });
13689
14925
  ps.clear();
13690
14926
  for (const d of model.parts) {
13691
14927
  const kg = burnKg(d);
@@ -13705,6 +14941,8 @@ var GameVehicles = (() => {
13705
14941
  lost: false,
13706
14942
  status: "intact",
13707
14943
  flat: false,
14944
+ air: 1,
14945
+ airLoss: 0,
13708
14946
  fireLoss: 0
13709
14947
  });
13710
14948
  }
@@ -13721,7 +14959,9 @@ var GameVehicles = (() => {
13721
14959
  rand = rng(seedBase);
13722
14960
  igniteRand = rng(strHash(`${c.key}|${id}|ignition|${options.seed ?? 0}`));
13723
14961
  steerSign = rand() < 0.5 ? -1 : 1;
14962
+ releaseCrease?.();
13724
14963
  vis = headless ? null : new VehicleVisuals(kit, c, model);
14964
+ releaseCrease = vis && options.creaseQueue ? options.creaseQueue.track(vis) : void 0;
13725
14965
  recomputeGroups();
13726
14966
  };
13727
14967
  const recomputeGroups = () => {
@@ -13803,7 +15043,8 @@ var GameVehicles = (() => {
13803
15043
  vis.breakLight(d, true);
13804
15044
  break;
13805
15045
  case "tyre":
13806
- vis.tyreFlat(d, 0.34);
15046
+ if (status === "broken") vis.tyreTornOff(d);
15047
+ else vis.tyreFlat(d, 0.34);
13807
15048
  break;
13808
15049
  case "mirror":
13809
15050
  case "bumper":
@@ -13833,7 +15074,8 @@ var GameVehicles = (() => {
13833
15074
  const damage = (p, amount, dtype, source, out, cause) => {
13834
15075
  if (p.lost || !(amount > 0)) return;
13835
15076
  const before = p.hp;
13836
- p.hp = Math.max(0, p.hp - amount);
15077
+ const crumples = dtype === "blunt" && CRUMPLES.has(p.def.role) && (p.def.material === "steel" || p.def.material === "aluminium");
15078
+ p.hp = Math.max(WELDED.has(p.def.role) || crumples ? 1 : 0, p.hp - amount);
13837
15079
  out.push(ev({ type: "damage", part: p.def.id, amount: r2(before - p.hp), hp: r2(p.hp), damageType: dtype, ...source ? { source } : {} }));
13838
15080
  wear(p, amount, out);
13839
15081
  syncZone(p);
@@ -13876,11 +15118,22 @@ var GameVehicles = (() => {
13876
15118
  const p = v.clone().applyMatrix4(M);
13877
15119
  return [p.x, p.y, p.z];
13878
15120
  };
13879
- const armour = () => c.meta.armored || c.meta.bullet ? 30 : 1;
15121
+ const armour = () => armourLevel?.steel ?? 1;
15122
+ const steelMm = (d) => {
15123
+ const base = STEEL_MM[d.role] ?? STEEL_MM.panel;
15124
+ return d.material === "steel" || d.material === "aluminium" ? base * (armourLevel ? ARMOUR_STEEL_MM[armourId === "III" ? "III" : "IIIA"] / STEEL_MM.panel : 1) : base;
15125
+ };
15126
+ const goesThrough = (h, d, p, energy, res) => {
15127
+ const piercing = h.piercing ?? 1;
15128
+ const metal = d.material === "steel" || d.material === "aluminium";
15129
+ const mm = h.penetration?.steel;
15130
+ if (metal && mm != null) return mm >= steelMm(d) * (p.dents.length > 3 ? 0.9 : 1);
15131
+ return energy * (metal || d.material === "glass" ? piercing : 1) >= res;
15132
+ };
13880
15133
  const resist = (d, p) => {
13881
15134
  let base = RESIST[d.material] ?? 100;
13882
15135
  if (d.material === "glass" && d.hp > 1e3) base = 120;
13883
- if (d.material === "glass" && c.meta.bullet) base *= 30;
15136
+ if (d.material === "glass" && armourLevel) base = armourLevel.glassStops;
13884
15137
  const k = ROLE_RESIST[d.role] ?? 1;
13885
15138
  const arm = d.material === "steel" || d.material === "aluminium" ? armour() : 1;
13886
15139
  return base * k * arm * (p.dents.length > 3 ? 0.9 : 1);
@@ -13903,7 +15156,7 @@ var GameVehicles = (() => {
13903
15156
  if (vis) {
13904
15157
  const pos = new V3(d.box.center[0], Math.max(0.2, (c.meta.clearance ?? 0.3) * 0.8), d.box.center[2]);
13905
15158
  if (fluid === "coolant") kit.addEmitter(c, new V3(d.box.center[0], d.box.center[1] + 0.2, d.box.center[2]), "white", { rate: 5, s: 0.8 });
13906
- else if ((c.meta.kind ?? "car") !== "boat") l.pud = I.puddle(c, fluidKind(fluid), pos, fluid === "oil" ? 0.8 : 1.1);
15159
+ if ((c.meta.kind ?? "car") !== "boat") l.pud = I.puddle(c, fluidKind(fluid), pos, fluid === "oil" ? 0.8 : fluid === "brake_fluid" ? 0.4 : 1.1);
13907
15160
  }
13908
15161
  }
13909
15162
  const out3 = dirToWorld(new V3(dirLocal.x * 0.5, dirLocal.y * 0.5 - 0.6, dirLocal.z * 0.5));
@@ -13940,7 +15193,7 @@ var GameVehicles = (() => {
13940
15193
  exploded = true;
13941
15194
  const d = tank.def;
13942
15195
  const spec = d.explodes ?? { energy: 8e5, radius: 5, atC: 320 };
13943
- out.push(ev({ type: "explode", part: d.id, point: toWorld(d.box.center), energy: spec.energy, radius: spec.radius }));
15196
+ out.push(ev({ type: "explode", part: d.id, point: toWorld(d.box.center), energy: spec.energy, radius: spec.radius, ..."effect" in spec && spec.effect !== void 0 ? { effect: spec.effect } : {} }));
13944
15197
  tank.liters = 0;
13945
15198
  leaks.delete(d.id);
13946
15199
  if (vis) kit.explode(c, { delay: 0.6 });
@@ -14032,15 +15285,15 @@ var GameVehicles = (() => {
14032
15285
  sync();
14033
15286
  const M = toFrameMatrix();
14034
15287
  const v = toLocal(h.point);
14035
- const p = locate(v, 0.2);
15288
+ const p = (h.part ? ps.get(h.part) : void 0) ?? locate(v, 0.2);
14036
15289
  if (!p) return;
14037
15290
  const d = p.def;
14038
15291
  const E = h.energy ?? 0;
14039
15292
  const cal = h.caliber ?? 9;
14040
15293
  const dir = dirToLocal(h.direction ?? [0, 0, -1]).normalize();
14041
- const n = normalOf(d, v);
15294
+ const n = h.normal ? dirToLocal(h.normal).normalize() : normalOf(d, v);
14042
15295
  const res = resist(d, p);
14043
- const through = E >= res;
15296
+ const through = goesThrough(h, d, p, E, res);
14044
15297
  const cosInc = Math.abs(dir.dot(n));
14045
15298
  const metal = d.material === "steel" || d.material === "aluminium";
14046
15299
  const worldPoint = toWorld(v);
@@ -14051,13 +15304,21 @@ var GameVehicles = (() => {
14051
15304
  vis?.spark([v.x, v.y, v.z], [refl.x, refl.y, refl.z], 6);
14052
15305
  return;
14053
15306
  }
14054
- const holeDiam = Math.max(6e-3, cal / 1e3 * (d.material === "glass" ? 1.4 : 1));
15307
+ const opens = (h.piercing ?? 1) < SOFT_LEAD ? EXPANDED_HOLE : 1;
15308
+ const holeDiam = Math.max(6e-3, cal / 1e3 * (d.material === "glass" ? 1.4 : 1) * opens);
15309
+ const energyLeft = () => {
15310
+ const metal2 = d.material === "steel" || d.material === "aluminium";
15311
+ const mm = h.penetration?.steel;
15312
+ const spent = metal2 && mm ? clamp(steelMm(d) / mm, MIN_SPENT, 1) : clamp(res / Math.max(E, 1), MIN_SPENT, 1);
15313
+ return E * (1 - spent);
15314
+ };
14055
15315
  const push = (hole) => {
14056
15316
  if (!hole) return;
14057
15317
  const hp = { point: r3(fromVehicle(v, M)), normal: r3(fromVehicle(n, new THREE.Matrix4().extractRotation(M))), diameter: r2(holeDiam), through };
14058
15318
  p.holes.push(hp);
14059
- 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);
15319
+ out.push(ev({ type: "hole", part: d.id, hole: hp, worldPoint, ...through ? { energyLeft: r2(energyLeft()) } : {}, ...through ? { exitPoint: add3(worldPoint, r3(dirToWorld(dir.clone().multiplyScalar(Math.max(0.02, Math.min(d.box.size[0], d.box.size[1], d.box.size[2])))))) } : {} }));
15320
+ vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam, d);
15321
+ if (d.role === "mirror") vis?.shatterMirror(d.id);
14061
15322
  startLeak(p, v, cal, out, dir);
14062
15323
  };
14063
15324
  switch (d.material) {
@@ -14070,7 +15331,7 @@ var GameVehicles = (() => {
14070
15331
  const crack = { point: r3(fromVehicle(v, M)), normal: r3(fromVehicle(n, new THREE.Matrix4().extractRotation(M))), length: r2(0.15 + force * 0.35), rays: 6 + Math.round(force * 6) };
14071
15332
  p.cracks.push(crack);
14072
15333
  out.push(ev({ type: "crack", part: d.id, crack, worldPoint }));
14073
- damage(p, E * 0.5, "ballistic", h.source, out, worldPoint);
15334
+ damage(p, E * (armourLevel && !through ? armourLevel.glassTakes : 0.5), "ballistic", h.source, out, worldPoint);
14074
15335
  } else damage(p, laminated ? E * 0.5 : p.hp, "ballistic", h.source, out, worldPoint);
14075
15336
  break;
14076
15337
  }
@@ -14078,8 +15339,8 @@ var GameVehicles = (() => {
14078
15339
  push(through);
14079
15340
  damage(p, E * 0.4, "ballistic", h.source, out, worldPoint);
14080
15341
  if (through && !p.lost && !p.flat) {
14081
- p.flat = true;
14082
- vis?.tyreFlat(d, 0.24);
15342
+ if (E >= TYRE_BURST_JOULES || cal >= TYRE_BURST_CALIBER) deflate(p, 1);
15343
+ else p.airLoss += TYRE_LEAK_PER_MM2 * Math.PI * (cal / 2) ** 2;
14083
15344
  }
14084
15345
  break;
14085
15346
  }
@@ -14195,7 +15456,7 @@ var GameVehicles = (() => {
14195
15456
  const electric = c.spec.fuel === "electric";
14196
15457
  const tank = tankPart();
14197
15458
  const fuel = tank ? tank.liters ?? 0 : electric ? 1 : 1;
14198
- const engineOk = sound("engine") && !exploded && fuel > 0.05;
15459
+ const engineOk = sound("engine") && !exploded && !seized && fuel > 0.05;
14199
15460
  const powered = sound("battery") && sound("alternator");
14200
15461
  const gearOk = sound("gearbox");
14201
15462
  const brakes = [...ps.values()].find((p) => p.def.role === "brakes");
@@ -14355,9 +15616,62 @@ var GameVehicles = (() => {
14355
15616
  const state = () => stateCache ?? (stateCache = buildState());
14356
15617
  const isSimulating = () => {
14357
15618
  if (leaks.size > 0) return true;
14358
- for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT)) return true;
15619
+ for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT || p.airLoss > 0 && p.air > 0)) return true;
14359
15620
  return false;
14360
15621
  };
15622
+ let engineHeat = ENGINE_WORKING_C;
15623
+ let seized = false;
15624
+ let steaming = false;
15625
+ const surfaceRays = new THREE.Raycaster();
15626
+ surfaceRays.layers.enableAll();
15627
+ surfaceRays.camera = new THREE.PerspectiveCamera();
15628
+ let meshParts = null;
15629
+ const paneParts = () => {
15630
+ if (meshParts) return meshParts;
15631
+ meshParts = /* @__PURE__ */ new Map();
15632
+ for (const d of model.parts) {
15633
+ if (d.role !== "glass") continue;
15634
+ const z = kit.zonesOf(c).find((q) => q.id === d.zoneId);
15635
+ for (const g of z?.glass ?? d.panes ?? []) meshParts.set(g, d.id);
15636
+ }
15637
+ return meshParts;
15638
+ };
15639
+ const isDrawnSurface = (o) => {
15640
+ for (let q = o; q && q !== c.vehicle; q = q.parent) if (!q.visible || q.userData.decal || q.userData.ghost) return false;
15641
+ return true;
15642
+ };
15643
+ const partOfSurface = (o, v) => {
15644
+ const pane = paneParts().get(o);
15645
+ if (pane) return ps.get(pane) ?? null;
15646
+ for (let q = o; q && q !== c.vehicle; q = q.parent) {
15647
+ if (vis?.plateIdOf(q)) return ps.get(vis.plateIdOf(q)) ?? null;
15648
+ const piece = vis?.extraIdOf(q);
15649
+ if (piece) return ps.get(piece) ?? null;
15650
+ }
15651
+ return locate(v, 0.08, (p) => p.def.role === "glass");
15652
+ };
15653
+ const surfaceMeshes = [];
15654
+ const collectSurfaces = (o) => {
15655
+ if (!o.visible || o.userData.decal || o.userData.ghost || o.userData.noSurfaceRay) return;
15656
+ if (o.isMesh) surfaceMeshes.push(o);
15657
+ for (const child of o.children) collectSurfaces(child);
15658
+ };
15659
+ const surfaceRay = (ray) => {
15660
+ sync();
15661
+ surfaceRays.set(new V3(ray.origin[0], ray.origin[1], ray.origin[2]), new V3(ray.direction[0], ray.direction[1], ray.direction[2]));
15662
+ surfaceRays.far = ray.maxDistance;
15663
+ surfaceMeshes.length = 0;
15664
+ for (const child of c.vehicle.children) collectSurfaces(child);
15665
+ for (const h of surfaceRays.intersectObjects(surfaceMeshes, false)) {
15666
+ if (!h.face || !isDrawnSurface(h.object)) continue;
15667
+ const p = partOfSurface(h.object, c.vehicle.worldToLocal(h.point.clone()));
15668
+ if (!p || p.lost || ray.ignored?.(p.def.id)) continue;
15669
+ const n = h.face.normal.clone().transformDirection(h.object.matrixWorld);
15670
+ if (n.dot(surfaceRays.ray.direction) > 0) n.negate();
15671
+ 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 };
15672
+ }
15673
+ return null;
15674
+ };
14361
15675
  const hit = (h) => {
14362
15676
  stateCache = void 0;
14363
15677
  const out = [];
@@ -14382,7 +15696,29 @@ var GameVehicles = (() => {
14382
15696
  applyEffects();
14383
15697
  return out;
14384
15698
  };
15699
+ const runFlat = armourId != null;
15700
+ const tyreHealth = (p) => {
15701
+ if (p.lost) return RIM_HEALTH;
15702
+ if (p.flat) return runFlat ? TYRE_RUNFLAT_HEALTH : TYRE_FLAT_HEALTH;
15703
+ return Math.min(p.hp / p.def.hp, 0.2 + 0.8 * p.air);
15704
+ };
15705
+ const tyreOf = (rim) => {
15706
+ let best;
15707
+ let bestD = Infinity;
15708
+ for (const p of ps.values()) {
15709
+ if (p.def.role !== "tyre") continue;
15710
+ const d = Math.hypot(p.def.box.center[0] - rim.def.box.center[0], p.def.box.center[2] - rim.def.box.center[2]);
15711
+ if (d < bestD) [best, bestD] = [p, d];
15712
+ }
15713
+ return best;
15714
+ };
15715
+ const deflate = (p, share) => {
15716
+ p.air = Math.max(0, p.air - share);
15717
+ vis?.tyreFlat(p.def, TYRE_FLAT_SAG * (1 - p.air));
15718
+ if (p.air <= TYRE_FLAT_AIR) p.flat = true;
15719
+ };
14385
15720
  const update = (dt) => {
15721
+ for (const p of ps.values()) if (p.airLoss > 0 && p.air > 0 && !p.lost) deflate(p, p.airLoss * dt);
14386
15722
  clock += dt;
14387
15723
  const out = queue;
14388
15724
  queue = [];
@@ -14480,9 +15816,14 @@ var GameVehicles = (() => {
14480
15816
  },
14481
15817
  describe,
14482
15818
  hit,
15819
+ surfaceRay,
14483
15820
  update,
14484
15821
  state,
14485
15822
  reset,
15823
+ dispose: () => {
15824
+ releaseCrease?.();
15825
+ releaseCrease = void 0;
15826
+ },
14486
15827
  disassemble,
14487
15828
  extinguish,
14488
15829
  serialize: () => ({
@@ -14493,6 +15834,70 @@ var GameVehicles = (() => {
14493
15834
  partName: (pid) => ps.get(pid)?.def.name ?? pid,
14494
15835
  status: computeStatus,
14495
15836
  animations: () => createVehicleClips(model, vis, kit, c),
15837
+ crumple: (point, direction, depth) => {
15838
+ if (!vis || depth <= 0) return;
15839
+ sync();
15840
+ const force = clamp((depth - CRUMPLE_BASE_M) / CRUMPLE_PER_FORCE_M, CRUMPLE_MIN_FORCE, CRUMPLE_MAX_FORCE);
15841
+ kit.impact(c, toLocal(point), dirToLocal(direction).normalize(), force, { maxDepth: depth });
15842
+ stateCache = void 0;
15843
+ },
15844
+ rollFlat: (meters, speed) => {
15845
+ for (const p of ps.values()) {
15846
+ if (p.def.role !== "tyre" || !p.flat || p.lost) continue;
15847
+ const life = runFlat ? RUN_FLAT_LIFE_M * Math.min(1, RUN_FLAT_SPEED / Math.max(speed, 1)) : FLAT_TYRE_LIFE_M * clamp(FLAT_TYRE_TOWN_SPEED / Math.max(speed, 1), 0.2, 1.5);
15848
+ p.hp = Math.max(0, p.hp - p.def.hp * meters / life);
15849
+ if (p.hp > 0) continue;
15850
+ const at = toWorld(p.def.box.center);
15851
+ lose(p, "broken", queue, at);
15852
+ queue.push(ev({ type: "debris", part: p.def.id, kind: "fibers", point: at, normal: [0, 1, 0], count: 12, speed: Math.max(2, speed * 0.5) }));
15853
+ }
15854
+ let grinding = 0;
15855
+ for (const rim of ps.values()) {
15856
+ if (rim.def.role !== "rim" || rim.lost || !tyreOf(rim)?.lost) continue;
15857
+ grinding += 1;
15858
+ rim.hp = Math.max(1, rim.hp - rim.def.hp * RIM_GRIND_PER_M * meters);
15859
+ const [x, y, z] = rim.def.box.center;
15860
+ queue.push(ev({ type: "debris", part: rim.def.id, kind: "sparks", point: toWorld([x, y - rim.def.box.size[1] / 2, z]), normal: [0, 1, 0], count: Math.min(30, Math.ceil(4 + speed)), speed: Math.max(2, speed * 0.4) }));
15861
+ }
15862
+ stateCache = void 0;
15863
+ return grinding;
15864
+ },
15865
+ runEngine: (seconds, load) => {
15866
+ const engine = [...ps.values()].find((p) => p.def.role === "engine");
15867
+ if (!engine || engine.lost || seized) return engineHeat;
15868
+ const share = (fluid) => {
15869
+ const list2 = [...ps.values()].filter((p) => p.def.contains?.fluid === fluid);
15870
+ return list2.length ? Math.min(...list2.map((p) => (p.liters ?? 0) / p.def.contains.liters)) : 1;
15871
+ };
15872
+ const coolant = share("coolant");
15873
+ const oil = share("oil");
15874
+ const work = 0.4 + 0.6 * clamp(load, 0, 1);
15875
+ if (coolant < COOLANT_NEEDED) engineHeat += (COOLANT_NEEDED - coolant) / COOLANT_NEEDED * ENGINE_HEATING * work * seconds;
15876
+ else engineHeat += (ENGINE_WORKING_C - engineHeat) * Math.min(1, seconds / ENGINE_SETTLE_S);
15877
+ if (oil < OIL_NEEDED) engine.hp = Math.max(0, engine.hp - engine.def.hp * OIL_WEAR * work * seconds * ((OIL_NEEDED - oil) / OIL_NEEDED));
15878
+ if (engineHeat >= ENGINE_STEAM_C && vis && !steaming) {
15879
+ steaming = true;
15880
+ kit.addEmitter(c, new V3(engine.def.box.center[0], engine.def.box.center[1] + 0.4, 0), "white", { rate: 8, s: 1 });
15881
+ }
15882
+ if (engineHeat >= ENGINE_SEIZE_C || engine.hp <= engine.def.hp * ENGINE_SEIZE_HP) {
15883
+ seized = true;
15884
+ queue.push(ev({ type: "damage", part: engine.def.id, amount: 0, hp: r2(engine.hp), damageType: "heat" }));
15885
+ }
15886
+ stateCache = void 0;
15887
+ return engineHeat;
15888
+ },
15889
+ driveReport: () => {
15890
+ const health = (role) => {
15891
+ const list2 = [...ps.values()].filter((p) => p.def.role === role);
15892
+ if (!list2.length) return 1;
15893
+ return Math.min(...list2.map((p) => p.lost || !attached(p.def.id) ? 0 : p.hp / p.def.hp));
15894
+ };
15895
+ const tyres = [...ps.values()].filter((p) => p.def.role === "tyre").map((p) => ({ x: p.def.box.center[0], z: p.def.box.center[2], health: tyreHealth(p) }));
15896
+ const status = computeStatus();
15897
+ const brakeSystem = [...ps.values()].find((p) => p.def.role === "brakes" && p.def.contains);
15898
+ const fluidLeft = brakeSystem ? clamp((brakeSystem.liters ?? 0) / brakeSystem.def.contains.liters / BRAKE_FLUID_NEEDED, 0, 1) : 1;
15899
+ return { tyres, brakes: Math.min(1, 1 / status.brakeFactor) * (BRAKE_NO_FLUID + (1 - BRAKE_NO_FLUID) * fluidLeft), engine: status.canStart ? health("engine") : 0, suspension: health("shocks") };
15900
+ },
14496
15901
  partWorldCenter: (pid) => {
14497
15902
  sync();
14498
15903
  const d = ps.get(pid)?.def;
@@ -14521,6 +15926,89 @@ var GameVehicles = (() => {
14521
15926
  return { root, vehicle };
14522
15927
  }
14523
15928
 
15929
+ // src/destructible/crease-queue.ts
15930
+ function createCreaseQueue() {
15931
+ const tracked = [];
15932
+ let cursor = 0;
15933
+ let lastFrame;
15934
+ let attached = false;
15935
+ const deferred = {
15936
+ id: DEFERRED_QUEUE_ID.VEHICLE_CREASE,
15937
+ pending(frame) {
15938
+ lastFrame = frame;
15939
+ let units = 0;
15940
+ for (const work of tracked) units += work.pendingUnits(frame);
15941
+ return units;
15942
+ },
15943
+ run(deadline, clock) {
15944
+ let ran = 0;
15945
+ let skipped = 0;
15946
+ while (tracked.length > 0 && skipped < tracked.length) {
15947
+ if (ran > 0 && clock() >= deadline) break;
15948
+ const work = tracked[cursor % tracked.length];
15949
+ cursor = (cursor + 1) % tracked.length;
15950
+ if (work.pendingUnits(lastFrame) > 0 && work.runStep(deadline)) {
15951
+ ran++;
15952
+ skipped = 0;
15953
+ } else skipped++;
15954
+ }
15955
+ return ran;
15956
+ },
15957
+ flush() {
15958
+ for (const work of tracked) work.finishAll();
15959
+ },
15960
+ attach() {
15961
+ attached = true;
15962
+ for (const work of tracked) work.setQueued(true);
15963
+ },
15964
+ detach() {
15965
+ attached = false;
15966
+ for (const work of tracked) work.setQueued(false);
15967
+ }
15968
+ };
15969
+ return {
15970
+ deferredQueue: () => deferred,
15971
+ track(work) {
15972
+ tracked.push(work);
15973
+ work.setQueued(attached);
15974
+ return () => {
15975
+ const at = tracked.indexOf(work);
15976
+ if (at < 0) return;
15977
+ tracked.splice(at, 1);
15978
+ work.setQueued(false);
15979
+ };
15980
+ }
15981
+ };
15982
+ }
15983
+
15984
+ // src/destructible/crash.ts
15985
+ var CRASH_RULES = { energyToDamage: 0.3, hitSpacing: 0.6, minSpeed: 1.5, rows: [0, 0.3], crushPerMs: 0.035, maxCrush: 0.6 };
15986
+ function crashDeltaV(speed, massKg, otherMassKg) {
15987
+ if (!Number.isFinite(otherMassKg)) return speed;
15988
+ return speed * otherMassKg / (massKg + otherMassKg);
15989
+ }
15990
+ function crashVehicle(d, crash) {
15991
+ const rules = { ...CRASH_RULES, ...crash.rules };
15992
+ const deltaV = crashDeltaV(crash.speed, crash.massKg, crash.otherMassKg);
15993
+ const energy = 0.5 * crash.massKg * deltaV * deltaV;
15994
+ if (crash.speed < rules.minSpeed) return { deltaV, energy, events: [] };
15995
+ const [dx, , dz] = crash.direction;
15996
+ const length = Math.hypot(dx, dz) || 1;
15997
+ const across = [-dz / length, 0, dx / length];
15998
+ const hits = Math.max(1, Math.round(crash.contactWidth / rules.hitSpacing));
15999
+ const each = energy * rules.energyToDamage / (hits * rules.rows.length);
16000
+ const events = [];
16001
+ for (const up of rules.rows)
16002
+ for (let i = 0; i < hits; i++) {
16003
+ const offset = hits === 1 ? 0 : (i / (hits - 1) - 0.5) * crash.contactWidth;
16004
+ const point = [crash.point[0] + across[0] * offset, crash.point[1] + up, crash.point[2] + across[2] * offset];
16005
+ events.push(...d.hit({ type: "blunt", point, direction: [dx / length, 0, dz / length], energy: each, radius: rules.hitSpacing / 2, ...crash.source ? { source: crash.source } : {} }));
16006
+ }
16007
+ const crush = Math.min(rules.maxCrush, deltaV * rules.crushPerMs);
16008
+ d.crumple(crash.point, [dx / length, 0, dz / length], crush);
16009
+ return { deltaV, energy, events };
16010
+ }
16011
+
14524
16012
  // src/destructible/enclosure.ts
14525
16013
  var CABIN_LEAK = 0.02;
14526
16014
  var HOLE_RATE_PER_AREA = 3e3;