@adatechnology/game-vehicles 0.6.0 → 0.6.2

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,17 +23,22 @@ 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,
36
39
  crackedGlassMaterial: () => crackedGlassMaterial,
40
+ crashDeltaV: () => crashDeltaV,
41
+ crashVehicle: () => crashVehicle,
37
42
  createCreaseQueue: () => createCreaseQueue,
38
43
  createLod: () => createLod,
39
44
  createTranslate: () => createTranslate,
@@ -57,7 +62,7 @@ var GameVehicles = (() => {
57
62
  wrapThreeForLod: () => wrapThreeForLod
58
63
  });
59
64
 
60
- // ../game-shapes/dist/chunk-IFXQIDSA.js
65
+ // ../game-shapes/dist/chunk-M5N2SHNL.js
61
66
  function canonicalJson(value) {
62
67
  if (value === null) return "null";
63
68
  if (typeof value === "number") return Object.is(value, -0) ? "-0" : Number.isFinite(value) ? String(value) : JSON.stringify(String(value));
@@ -1304,8 +1309,8 @@ var GameVehicles = (() => {
1304
1309
  groups: []
1305
1310
  };
1306
1311
  }
1307
- function make(kind, p) {
1308
- switch (kind) {
1312
+ function make(p) {
1313
+ switch (p.kind) {
1309
1314
  case "box": {
1310
1315
  const size = p.size ?? [1, 1, 1];
1311
1316
  const segments = p.segments ?? [1, 1, 1];
@@ -1334,7 +1339,7 @@ var GameVehicles = (() => {
1334
1339
  case "mesh":
1335
1340
  return mesh(p);
1336
1341
  default:
1337
- throw new Error(`game-shapes: unknown primitive kind "${String(kind)}"`);
1342
+ throw new Error(`game-shapes: unknown primitive kind "${String(p.kind)}"`);
1338
1343
  }
1339
1344
  }
1340
1345
  var templates = /* @__PURE__ */ new Map();
@@ -1342,7 +1347,7 @@ var GameVehicles = (() => {
1342
1347
  var stats = { hits: 0, misses: 0 };
1343
1348
  function buildPrimitive(node, options = {}) {
1344
1349
  const parameters = primitiveParameters(node, options);
1345
- if (node.kind === "mesh" || templateLimit === 0) return make(node.kind, parameters);
1350
+ if (node.kind === "mesh" || templateLimit === 0) return make({ ...node, ...parameters });
1346
1351
  const key = `${node.kind}:${canonicalJson(parameters)}`;
1347
1352
  const cached = templates.get(key);
1348
1353
  if (cached) {
@@ -1352,7 +1357,7 @@ var GameVehicles = (() => {
1352
1357
  return cached;
1353
1358
  }
1354
1359
  stats.misses++;
1355
- const built = make(node.kind, parameters);
1360
+ const built = make({ ...node, ...parameters });
1356
1361
  templates.set(key, built);
1357
1362
  if (templates.size > templateLimit) templates.delete(templates.keys().next().value);
1358
1363
  return built;
@@ -1446,6 +1451,7 @@ var GameVehicles = (() => {
1446
1451
  };
1447
1452
 
1448
1453
  // ../game-contracts/dist/index.js
1454
+ var DEFAULT_BLAST_IMPULSE = { bodyArea: 0.7, lift: 0.15, minSpeed: 0.3, tntPerJoule: 0.24 / 2e5 };
1449
1455
  var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
1450
1456
  var DEFAULT_IGNITION_RULES = {
1451
1457
  fillCurve: [
@@ -4877,6 +4883,7 @@ var GameVehicles = (() => {
4877
4883
  }
4878
4884
 
4879
4885
  // src/hood.ts
4886
+ var HOOD_LIFT = 8e-3;
4880
4887
  function installHood(kit) {
4881
4888
  const THREE = kit.THREE;
4882
4889
  const V3 = THREE.Vector3;
@@ -4899,7 +4906,7 @@ var GameVehicles = (() => {
4899
4906
  for (let i = 0; i < p.count; i++) {
4900
4907
  p.setX(i, cx + (p.getX(i) - cx) * sx);
4901
4908
  p.setZ(i, p.getZ(i) * sz);
4902
- p.setY(i, p.getY(i) + 8e-3);
4909
+ p.setY(i, p.getY(i) + HOOD_LIFT);
4903
4910
  }
4904
4911
  geo.translate(-hx, -hy, 0);
4905
4912
  const under = geo.clone();
@@ -4928,7 +4935,8 @@ var GameVehicles = (() => {
4928
4935
  yMin: bb.min.y,
4929
4936
  zw: bb.max.z,
4930
4937
  label: o.label,
4931
- noLatch: !!o.noLatch
4938
+ noLatch: !!o.noLatch,
4939
+ lift: HOOD_LIFT
4932
4940
  };
4933
4941
  c.part("hood", { key: "hood" }, piv, new V3((hx + bb.max.x) / 2, hy, 0), 0.9);
4934
4942
  return c.hood;
@@ -6361,6 +6369,14 @@ var GameVehicles = (() => {
6361
6369
  }
6362
6370
 
6363
6371
  // src/fluids.ts
6372
+ var PUDDLE_COLORS = {
6373
+ coolant: 3123279,
6374
+ brake_fluid: 12093738,
6375
+ transmission_fluid: 10099746,
6376
+ power_steering_fluid: 9054768,
6377
+ washer_fluid: 2977744,
6378
+ battery_acid: 14211272
6379
+ };
6364
6380
  function installFluids(kit) {
6365
6381
  const THREE = kit.THREE;
6366
6382
  const V3 = THREE.Vector3;
@@ -6580,6 +6596,13 @@ var GameVehicles = (() => {
6580
6596
  transparent: true,
6581
6597
  opacity: 0.9,
6582
6598
  depthWrite: false
6599
+ }) : PUDDLE_COLORS[kind] ? new THREE.MeshStandardMaterial({
6600
+ color: PUDDLE_COLORS[kind],
6601
+ roughness: 0.05,
6602
+ metalness: 0.1,
6603
+ transparent: true,
6604
+ opacity: 0.7,
6605
+ depthWrite: false
6583
6606
  }) : new THREE.MeshStandardMaterial({
6584
6607
  color: 0,
6585
6608
  roughness: 0.03,
@@ -7425,12 +7448,36 @@ var GameVehicles = (() => {
7425
7448
  if (g.userData.crease && c.smoothLater) c.smoothLater(msh);
7426
7449
  else if (g.userData.crease) crease(g, 42);
7427
7450
  else g.computeVertexNormals();
7428
- g.computeBoundingBox();
7429
7451
  const sphere2 = g.boundingSphere ?? (g.boundingSphere = new THREE.Sphere());
7430
- g.boundingBox.getCenter(sphere2.center);
7431
- let far2 = 0;
7432
- for (let i = 0; i < pos.count; i++) far2 = Math.max(far2, sphere2.center.distanceToSquared(v.fromBufferAttribute(pos, i)));
7433
- sphere2.radius = Math.sqrt(far2);
7452
+ if (pos.isInterleavedBufferAttribute || pos.normalized || Object.keys(g.morphAttributes).length > 0) {
7453
+ g.computeBoundingBox();
7454
+ g.boundingBox.getCenter(sphere2.center);
7455
+ let far2 = 0;
7456
+ for (let i = 0; i < pos.count; i++) far2 = Math.max(far2, sphere2.center.distanceToSquared(v.fromBufferAttribute(pos, i)));
7457
+ sphere2.radius = Math.sqrt(far2);
7458
+ } else {
7459
+ const box3 = g.boundingBox ?? (g.boundingBox = new THREE.Box3());
7460
+ let x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
7461
+ for (let i = 0; i < pos.count * 3; i += 3) {
7462
+ const x = arr[i], y = arr[i + 1], z = arr[i + 2];
7463
+ if (x < x0) x0 = x;
7464
+ if (y < y0) y0 = y;
7465
+ if (z < z0) z0 = z;
7466
+ if (x > x1) x1 = x;
7467
+ if (y > y1) y1 = y;
7468
+ if (z > z1) z1 = z;
7469
+ }
7470
+ box3.min.set(x0, y0, z0);
7471
+ box3.max.set(x1, y1, z1);
7472
+ box3.getCenter(sphere2.center);
7473
+ const cx = sphere2.center.x, cy = sphere2.center.y, cz = sphere2.center.z;
7474
+ let far2 = 0;
7475
+ for (let i = 0; i < pos.count * 3; i += 3) {
7476
+ const dx = cx - arr[i], dy = cy - arr[i + 1], dz = cz - arr[i + 2];
7477
+ far2 = Math.max(far2, dx * dx + dy * dy + dz * dz);
7478
+ }
7479
+ sphere2.radius = Math.sqrt(far2);
7480
+ }
7434
7481
  }
7435
7482
  });
7436
7483
  const dset = new Set(c.deform);
@@ -7834,7 +7881,8 @@ var GameVehicles = (() => {
7834
7881
  const G = gm2.userData.glass;
7835
7882
  if (!G || G.broken || !gm2.visible || !gm2.parent) return;
7836
7883
  if (G.type === "laminated") {
7837
- G.type = "tempered";
7884
+ for (let k2 = 0; k2 < 3; k2++) I.crackGlass(c, gm2, I.glassPt ? I.glassPt(c, gm2, c.rand) : gm2.position.clone(), 1.2);
7885
+ return;
7838
7886
  }
7839
7887
  I.shatterGlass(c, gm2, 1.4, new V3(0, 1, 0));
7840
7888
  });
@@ -7845,19 +7893,6 @@ var GameVehicles = (() => {
7845
7893
  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)));
7846
7894
  c.hood = null;
7847
7895
  }
7848
- const W = m.width || 1.6;
7849
- const gh = m.glasshouse;
7850
- if (m.kind === "car" && gh) {
7851
- ;
7852
- [1, -1].forEach(function(sd) {
7853
- const dh = Math.max(0.35, (m.belt || 1) - (m.clearance || 0.3) - 0.08);
7854
- const dx = gh.fb - 0.55;
7855
- 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));
7856
- 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));
7857
- hole.userData.noFollow = true;
7858
- 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)));
7859
- });
7860
- }
7861
7896
  if (m.rearBumper && m.rearBumper.parent)
7862
7897
  fling(
7863
7898
  c,
@@ -7865,20 +7900,6 @@ var GameVehicles = (() => {
7865
7900
  c.dirToEffects(new V3(rv(-5, -3), rv(4, 6), rv(-1, 1))),
7866
7901
  new V3(rv(-3, 3), rv(-6, 6), rv(-3, 3))
7867
7902
  );
7868
- if (c.wheels.length) {
7869
- const wi = Math.floor(c.rand() * c.wheels.length);
7870
- const wh = c.wheels[wi];
7871
- c.wheels.splice(wi, 1);
7872
- c.parts.forEach(function(p) {
7873
- if (p.obj === wh) p.done = true;
7874
- });
7875
- fling(
7876
- c,
7877
- wh,
7878
- c.dirToEffects(new V3(rv(-2, 2), rv(5, 7.5), (wh.position.z >= 0 ? 1 : -1) * rv(3, 5))),
7879
- new V3(rv(-8, 8), rv(-3, 3), rv(-8, 8))
7880
- );
7881
- }
7882
7903
  burnAll(c);
7883
7904
  c.emitters = c.emitters.filter(function(e) {
7884
7905
  return e.kind === "exhaust" ? false : true;
@@ -12673,6 +12694,17 @@ var GameVehicles = (() => {
12673
12694
  return kit;
12674
12695
  }
12675
12696
 
12697
+ // src/destructible/armour.ts
12698
+ var ARMOUR_LEVELS = {
12699
+ IIIA: { glassStops: 1700, glassHp: 6e3, glassTakes: 0.25, steel: 8 },
12700
+ III: { glassStops: 3600, glassHp: 9e3, glassTakes: 0.25, steel: 30 }
12701
+ };
12702
+ function armourLevelOf(meta, option) {
12703
+ if (option !== void 0) return option;
12704
+ if (meta.armor === "IIIA" || meta.armor === "III") return meta.armor;
12705
+ return meta.armored || meta.bullet ? "III" : null;
12706
+ }
12707
+
12676
12708
  // src/destructible/model.ts
12677
12709
  var JOINT_RULES = {
12678
12710
  hinge: { strength: 5e3, tools: ["wrench", "screwdriver"], seconds: 3 },
@@ -12716,6 +12748,12 @@ var GameVehicles = (() => {
12716
12748
  center: [(z.box.x0 + z.box.x1) / 2, (z.box.y0 + z.box.y1) / 2, (z.box.z0 + z.box.z1) / 2],
12717
12749
  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)]
12718
12750
  });
12751
+ var PART_FLUIDS = {
12752
+ gearbox: { fluid: "transmission_fluid", liters: 6 },
12753
+ brakes: { fluid: "brake_fluid", liters: 0.6 },
12754
+ steering: { fluid: "power_steering_fluid", liters: 1 },
12755
+ battery: { fluid: "battery_acid", liters: 3 }
12756
+ };
12719
12757
  var skinOf = (kind) => kind === "plane" || kind === "heli" || kind === "boat" ? "aluminium" : "steel";
12720
12758
  var isFender = (id) => /^fender/.test(id);
12721
12759
  var roleOfZone = (z) => {
@@ -12773,7 +12811,8 @@ var GameVehicles = (() => {
12773
12811
  const spec = c.spec;
12774
12812
  const t = kit.t;
12775
12813
  const skin = skinOf(kind);
12776
- const armour = m.armored || m.bullet ? 12 : 1;
12814
+ const armoured = options.armour ?? null;
12815
+ const armour = armoured ? 12 : 1;
12777
12816
  const mass = vehicleMass(c);
12778
12817
  const W = m.width ?? 1.7;
12779
12818
  const F = m.front ?? 2;
@@ -12839,8 +12878,8 @@ var GameVehicles = (() => {
12839
12878
  }
12840
12879
  if (role === "glass") {
12841
12880
  const glass = (z.glass ?? [])[0];
12842
- const laminated = glass?.userData?.glass?.type === "laminated" || m.bullet;
12843
- add({ id, role, name: label, material: "glass", box: box2, mass: 10 * area3, hp: laminated ? 4e3 : 450, zoneId: id, visual: false, tags: ["glass", ...tags] });
12881
+ const laminated = glass?.userData?.glass?.type === "laminated" || !!armoured;
12882
+ 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] });
12844
12883
  continue;
12845
12884
  }
12846
12885
  if (role === "light") {
@@ -12878,7 +12917,7 @@ var GameVehicles = (() => {
12878
12917
  zoneId: id,
12879
12918
  visual: false,
12880
12919
  contains: { fluid: diesel ? "diesel" : "fuel", liters },
12881
- explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320 },
12920
+ explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320, effect: "car_fuel_tank" },
12882
12921
  ignitionC: diesel ? 330 : 260,
12883
12922
  tags: ["fuel_tank", ...tags]
12884
12923
  });
@@ -12914,8 +12953,8 @@ var GameVehicles = (() => {
12914
12953
  if (claimed.has(pane)) continue;
12915
12954
  const G = pane.userData.glass;
12916
12955
  const bb = paneBox(c, pane, inverse);
12917
- const laminated = G?.type === "laminated" || m.bullet;
12918
- add({ id: `pane_${G?.key ?? "glass"}`, role: "glass", name: String(G?.label ?? G?.key ?? "glass"), material: "glass", box: bb, mass: 10 * areaOf(bb.size), hp: laminated ? 4e3 : 450, panes: [pane], visual: false, tags: ["glass"] });
12956
+ const laminated = G?.type === "laminated" || !!armoured;
12957
+ 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"] });
12919
12958
  }
12920
12959
  if (!hubZone) {
12921
12960
  const h = Math.max(0.15, belt - clearance);
@@ -12971,7 +13010,7 @@ var GameVehicles = (() => {
12971
13010
  visual: false,
12972
13011
  mechanical: true,
12973
13012
  contains: { fluid: diesel ? "diesel" : "fuel", liters },
12974
- explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320 },
13013
+ explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320, effect: "car_fuel_tank" },
12975
13014
  ignitionC: diesel ? 330 : 260,
12976
13015
  tags: ["fuel_tank"]
12977
13016
  });
@@ -13248,38 +13287,121 @@ var GameVehicles = (() => {
13248
13287
  }
13249
13288
  }
13250
13289
  void has;
13290
+ for (const part of parts) {
13291
+ const fluid = PART_FLUIDS[part.role];
13292
+ if (fluid && !part.contains && !(kind === "bike" && part.role === "steering")) part.contains = { ...fluid };
13293
+ }
13251
13294
  return { parts, joints, hub: hubId, hitch };
13252
13295
  }
13253
13296
 
13297
+ // src/destructible/subdivide-near.ts
13298
+ var SUBDIVIDE_DEPTH = 12;
13299
+ function subdivideNear(THREE, params) {
13300
+ const { mesh: mesh2, maxLen, near } = params;
13301
+ const src = mesh2.geometry.index ? mesh2.geometry.toNonIndexed() : mesh2.geometry;
13302
+ const a = src.attributes.position.array;
13303
+ const toWorld = mesh2.matrixWorld;
13304
+ const out = [];
13305
+ const mid = (p, q) => [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2, (p[2] + q[2]) / 2];
13306
+ const split = (A, B, C, depth) => {
13307
+ const ab = Math.hypot(A[0] - B[0], A[1] - B[1], A[2] - B[2]);
13308
+ const bc = Math.hypot(B[0] - C[0], B[1] - C[1], B[2] - C[2]);
13309
+ const ca = Math.hypot(C[0] - A[0], C[1] - A[1], C[2] - A[2]);
13310
+ const longest = Math.max(ab, bc, ca);
13311
+ if (longest <= maxLen || depth > SUBDIVIDE_DEPTH) {
13312
+ out.push(...A, ...B, ...C);
13313
+ return;
13314
+ }
13315
+ if (longest === ab) {
13316
+ const D = mid(A, B);
13317
+ split(A, D, C, depth + 1);
13318
+ split(D, B, C, depth + 1);
13319
+ } else if (longest === bc) {
13320
+ const E = mid(B, C);
13321
+ split(A, B, E, depth + 1);
13322
+ split(A, E, C, depth + 1);
13323
+ } else {
13324
+ const F = mid(C, A);
13325
+ split(A, B, F, depth + 1);
13326
+ split(F, B, C, depth + 1);
13327
+ }
13328
+ };
13329
+ for (let i = 0; i < a.length; i += 9) {
13330
+ const A = [a[i], a[i + 1], a[i + 2]];
13331
+ const B = [a[i + 3], a[i + 4], a[i + 5]];
13332
+ const C = [a[i + 6], a[i + 7], a[i + 8]];
13333
+ const corners = [A, B, C].map((p) => new THREE.Vector3(p[0], p[1], p[2]).applyMatrix4(toWorld));
13334
+ if (near(corners)) split(A, B, C, 0);
13335
+ else out.push(...A, ...B, ...C);
13336
+ }
13337
+ const g = new THREE.BufferGeometry();
13338
+ g.setAttribute("position", new THREE.Float32BufferAttribute(out, 3));
13339
+ mesh2.geometry.dispose();
13340
+ mesh2.geometry = g;
13341
+ }
13342
+
13254
13343
  // src/destructible/visual.ts
13255
13344
  var HOLE_DECAL_SCALE = 2.4;
13256
13345
  var DOOR_DEPTH = 0.07;
13257
- var DOOR_EDGE_PIECES = 16;
13346
+ var DOOR_EDGE_PIECES = 8;
13347
+ var DOOR_EDGE_REACH = 0.08;
13258
13348
  var DOOR_CARD_COLOR = "#3b3e42";
13259
13349
  var DOOR_LINER_COLOR = "#5a5e64";
13260
13350
  var DOOR_LINER_DARK = 0.15;
13261
- var DOOR_STRIP_THIN = 0.05;
13351
+ var DOOR_STRIP_THIN = 0.08;
13262
13352
  var DOOR_STRIP_NEAR = 0.04;
13353
+ var DOOR_STRIP_TALL = 0.22;
13354
+ var STRIP_SMOOTH_DEG = 180;
13355
+ var DOOR_STRIP_VERTICES = 2e4;
13356
+ var DOOR_SILL = 0.25;
13263
13357
  var DOOR_STRIP_OVER = 0.3;
13264
13358
  var DOOR_TOP_OVER_PANE = 0.03;
13265
- var DOOR_STRIP_SUB = 0.02;
13266
13359
  var HOLE_TEXTURE_SIZE = 64;
13360
+ var LAMINATED_HP = 1e3;
13361
+ var HOOD_SINK = 2e-3;
13362
+ var HOOD_GAP_CLOSED = 0.016;
13267
13363
  var DOOR_HANDLE_INSET = 0.12;
13268
13364
  var DOOR_HANDLE_MIN = 0.07;
13269
13365
  var DOOR_HANDLE_MAX = 0.24;
13270
13366
  var DOOR_HANDLE_THIN = 0.07;
13271
- var DOOR_HANDLE_OUTSIDE = 0.1;
13367
+ var DOOR_HANDLE_OUTSIDE = 0.15;
13272
13368
  var PANE_SEAT_OUT = 0.05;
13273
13369
  var PANE_SEAT_IN = -0.05;
13274
13370
  var PANE_SEAT_REACH = 0.14;
13275
13371
  var PANE_SEAT_SAMPLES = 24;
13276
13372
  var PANE_SEAT_LIFT = 4e-3;
13277
13373
  var PANE_FRAME_NEAR = 0.07;
13374
+ var PANE_MIN_SLOPE = 0.01;
13375
+ var PANE_EDGE_BAND = 0.03;
13376
+ var PANE_REACH_STEP = 0.01;
13377
+ var PANE_REACH_MAX = 0.25;
13378
+ var COWL_UNDER_GLASS = 0;
13379
+ var COWL_NEAR_PANE = 0.08;
13380
+ var COWL_PLATE = 0.06;
13381
+ var COWL_DASH = 0.5;
13382
+ var COWL_DEEP = 0.3;
13383
+ var WINDOW_FRAME_NEAR = 0.06;
13384
+ var WINDOW_FRAME_SPAN = 0.6;
13385
+ var DASHBOARD_COLOR = "#121314";
13386
+ var COWL_CUT_SUB = 0.1;
13387
+ var COWL_CUT_MARGIN = 0.08;
13388
+ var SKIN_UNDER_PANE = 0.025;
13389
+ var SKIN_EDGE_KEPT = 0.06;
13390
+ var SIDE_PANE_FACING = 0.6;
13391
+ var COWL_LONG = 0.8;
13392
+ var PANE_BAR_VERTICES = 48;
13393
+ var PANE_BAR_LENGTH = 0.3;
13394
+ var PANE_BORDER = 0.03;
13395
+ var PANE_BORDER_LIFT = 3e-3;
13278
13396
  var DRAWN_MIRROR_SIZE = 0.3;
13279
13397
  var DRAWN_MIRROR_REACH = 0.4;
13280
13398
  var DRAWN_MIRROR_MOUNT = 0.2;
13281
13399
  var MIRROR_GLASS_FIT = 0.76;
13400
+ var MIRROR_GLASS_PROUD = 1e-3;
13401
+ var MIRROR_FACE_DEPTH = 0.012;
13282
13402
  var MIRROR_GLASS_THIN = 0.04;
13403
+ var DEFAULT_WHEEL_RADIUS = 0.3;
13404
+ var SIDEWALL_SHARE = 0.3;
13283
13405
  var MIRROR_GLASS_MIN = 0.05;
13284
13406
  var MIRROR_GLASS_COLOR = "#59636a";
13285
13407
  var HOLE_RIM = { steel: "#b7bcc0", aluminium: "#d3d6d8", plastic: "#d9d6cc", rubber: "#141414", glass: "#eef3f5" };
@@ -13366,6 +13488,7 @@ var GameVehicles = (() => {
13366
13488
  this.hood = this.nameHood();
13367
13489
  this.seatPanes();
13368
13490
  this.seatDoorHandles();
13491
+ this.seatHood();
13369
13492
  this.warming = c.deform.filter((msh) => msh.geometry && msh.geometry.attributes.position && msh.geometry.attributes.position.count >= 1500);
13370
13493
  if (!this.warming.length) this.warming = null;
13371
13494
  for (const p of model.parts) {
@@ -13566,7 +13689,8 @@ var GameVehicles = (() => {
13566
13689
  const panes = I.windowsOver(this.c, z, inv);
13567
13690
  const seals = panes.map((g) => I.paneSeals.get(g)).filter((s) => !!s);
13568
13691
  const mirrors = this.mirrorsOn(z);
13569
- objs = [...pcs, ...this.doorCards(pcs, z), ...I.followersIn(this.c, z, inv), ...panes, ...seals, ...mirrors];
13692
+ const frames = this.framesAround(panes, inv);
13693
+ objs = [...pcs, ...I.followersIn(this.c, z, inv), ...panes, ...seals, ...mirrors, ...frames];
13570
13694
  }
13571
13695
  } else {
13572
13696
  const pcs = this.cutZone(z);
@@ -13611,7 +13735,10 @@ var GameVehicles = (() => {
13611
13735
  run(x1, y0, x1, y1);
13612
13736
  run(x1, y1, x0, y1);
13613
13737
  run(x0, y1, x0, y0);
13614
- const heights = loop.map(([x, y]) => this.surface("z", side2, B, [[x, y]]) ?? flat);
13738
+ const heights = loop.map(([x, y]) => {
13739
+ const at = this.surface("z", side2, B, [[x, y]]);
13740
+ return at != null && Math.abs(at - flat) < DOOR_EDGE_REACH ? at : flat;
13741
+ });
13615
13742
  const band = (lift) => {
13616
13743
  const position = [];
13617
13744
  const index = [];
@@ -13629,6 +13756,23 @@ var GameVehicles = (() => {
13629
13756
  };
13630
13757
  const paint = this.paint().clone();
13631
13758
  paint.side = THREE.DoubleSide;
13759
+ const cardPosition = [];
13760
+ 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);
13761
+ const cardZ = (z2) => z2 - side2 * DOOR_DEPTH;
13762
+ loop.forEach(([x, y], i) => {
13763
+ const j = (i + 1) % loop.length;
13764
+ cardPosition.push(mid[0], mid[1], cardZ(mid[2]), x, y, cardZ(heights[i]), loop[j][0], loop[j][1], cardZ(heights[j]));
13765
+ });
13766
+ const cardGeometry = new THREE.BufferGeometry();
13767
+ cardGeometry.setAttribute("position", new THREE.Float32BufferAttribute(cardPosition, 3));
13768
+ cardGeometry.computeVertexNormals();
13769
+ const cardMaterial = this.I.makeMaterial(DOOR_CARD_COLOR, { roughness: 0.85 }).clone();
13770
+ cardMaterial.side = THREE.DoubleSide;
13771
+ const card = new THREE.Mesh(cardGeometry, cardMaterial);
13772
+ card.userData.noFollow = true;
13773
+ card.castShadow = false;
13774
+ this.c.vehicle.add(card);
13775
+ piv.attach(card);
13632
13776
  const edges = new THREE.Mesh(band(2e-3), paint);
13633
13777
  const lining = new THREE.Mesh(band(2e-3), paint);
13634
13778
  for (const m of [edges, lining]) {
@@ -13638,6 +13782,17 @@ var GameVehicles = (() => {
13638
13782
  }
13639
13783
  piv.attach(edges);
13640
13784
  }
13785
+ /** 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. */
13786
+ framesAround(panes, inv) {
13787
+ const THREE = this.THREE;
13788
+ const boxes = panes.map((g) => this.I.localBox(this.c, g, inv).expandByScalar(WINDOW_FRAME_NEAR));
13789
+ return this.c.vehicle.children.filter((o) => {
13790
+ if (!o.isMesh || !o.visible || o.userData.glass || o.userData.decal || this.c.deform?.includes(o)) return false;
13791
+ const box2 = this.I.localBox(this.c, o, inv);
13792
+ if (box2.isEmpty()) return false;
13793
+ return boxes.some((b) => b.containsBox(box2) && box2.getSize(new THREE.Vector3()).x > b.getSize(new THREE.Vector3()).x * WINDOW_FRAME_SPAN);
13794
+ });
13795
+ }
13641
13796
  /** A front door carries the mirror drawn at its corner. */
13642
13797
  mirrorsOn(z) {
13643
13798
  if (!z.mirror) return [];
@@ -13663,45 +13818,42 @@ var GameVehicles = (() => {
13663
13818
  const B = z.box;
13664
13819
  const x0 = B.x0 + 0.03;
13665
13820
  const x1 = B.x1 - 0.03;
13666
- const y0 = B.y0 - DOOR_STRIP_NEAR;
13667
- const y1 = B.y1 + DOOR_STRIP_NEAR;
13821
+ const y0 = B.y0 + DOOR_SILL;
13822
+ const glassTop = (I) => {
13823
+ let top = -Infinity;
13824
+ for (const g of I.windowsOver(this.c, z, inv)) top = Math.max(top, I.localBox(this.c, g, inv).max.y);
13825
+ return top;
13826
+ };
13827
+ const y1 = Math.max(B.y1, glassTop(this.I)) + DOOR_STRIP_NEAR;
13668
13828
  const deform = this.c.deform ?? [];
13669
13829
  for (const o of [...this.c.vehicle.children]) {
13670
- if (!o.isMesh || o === piv || deform.includes(o) || o.userData.glass || o.userData.decal || !o.visible) continue;
13830
+ 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;
13671
13831
  const box2 = this.I.localBox(this.c, o, inv);
13672
13832
  const size = box2.getSize(new THREE.Vector3());
13673
13833
  const mid = box2.getCenter(new THREE.Vector3());
13674
- if (Math.sign(mid.z) !== Math.sign(z.side) || size.z > DOOR_STRIP_THIN || size.y > DOOR_STRIP_THIN || Math.min(box2.max.x, x1) - Math.max(box2.min.x, x0) < (x1 - x0) * DOOR_STRIP_OVER || mid.y < y0 || mid.y > y1) continue;
13834
+ 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;
13675
13835
  const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld);
13676
13836
  const v = new THREE.Vector3();
13677
13837
  const sdf = (x, y, zz) => {
13678
13838
  v.set(x, y, zz).applyMatrix4(toVehicle);
13679
13839
  return Math.max(x0 - v.x, v.x - x1);
13680
13840
  };
13681
- this.kit.cutMesh(o, [{ sdf }], 42, { sub: DOOR_STRIP_SUB });
13682
- const from = Math.max(box2.min.x, x0);
13683
- const to = Math.min(box2.max.x, x1);
13684
- const piece = this.box(to - from, size.y, size.z, o.material, (from + to) / 2, mid.y, mid.z);
13685
- piece.userData.noFollow = true;
13686
- piv.attach(piece);
13687
- }
13688
- }
13689
- /** The inside of a door: its skin brought in by the depth of the door, in the trim's dark plastic, so the open door is a door and not one sheet of paint. */
13690
- doorCards(skins, z) {
13691
- const cardMaterial = this.I.makeMaterial(DOOR_CARD_COLOR, { roughness: 0.8 });
13692
- return skins.map((skin) => {
13693
- const card = new this.THREE.Mesh(skin.geometry, cardMaterial);
13694
- card.position.copy(skin.position);
13695
- card.quaternion.copy(skin.quaternion);
13696
- card.scale.copy(skin.scale);
13697
- const side2 = z.side < 0 ? -1 : 1;
13698
- const inward = -side2 * DOOR_DEPTH;
13699
- card.position.z += inward;
13700
- card.userData.noFollow = true;
13701
- card.castShadow = false;
13702
- this.c.vehicle.add(card);
13703
- return card;
13704
- });
13841
+ const over = new THREE.Mesh(o.geometry.clone(), o.material);
13842
+ over.position.copy(o.position);
13843
+ over.quaternion.copy(o.quaternion);
13844
+ over.scale.copy(o.scale);
13845
+ this.c.vehicle.add(over);
13846
+ over.updateMatrixWorld(true);
13847
+ const overToVehicle = new THREE.Matrix4().multiplyMatrices(inv, over.matrixWorld);
13848
+ const w = new THREE.Vector3();
13849
+ this.kit.cutMesh(over, [{ sdf: (x, y, zz) => {
13850
+ w.set(x, y, zz).applyMatrix4(overToVehicle);
13851
+ return -Math.max(x0 - w.x, w.x - x1);
13852
+ } }], STRIP_SMOOTH_DEG, {});
13853
+ this.kit.cutMesh(o, [{ sdf }], STRIP_SMOOTH_DEG, {});
13854
+ over.userData.noFollow = true;
13855
+ piv.attach(over);
13856
+ }
13705
13857
  }
13706
13858
  mend(dirty) {
13707
13859
  dirty.forEach((msh) => {
@@ -13726,14 +13878,58 @@ var GameVehicles = (() => {
13726
13878
  ray.layers.enableAll();
13727
13879
  for (const pane of c.glass) {
13728
13880
  if (pane.userData.glass?.outline) continue;
13729
- const seat = this.paneSeat(pane, shell, ray);
13881
+ const local = this.shellAround(pane, shell);
13882
+ if (!local) continue;
13883
+ const seat = this.paneSeat(pane, [local], ray);
13730
13884
  if (!seat) continue;
13731
13885
  const near = c.vehicle.children.filter((o) => o.isMesh && o !== pane && !o.userData.glass && !shell.includes(o) && this.onPane(o, pane, seat));
13732
13886
  for (const m of [pane, ...near]) this.movePane(m, seat);
13733
- const past = this.shellPast(pane, shell, ray, seat.normal);
13887
+ const past = this.shellPast(pane, [local], ray, seat.normal);
13734
13888
  if (past > 0) for (const m of [pane, ...near]) this.movePane(m, { normal: seat.normal, samples: [{ at: seat.samples[0].at, by: past }] });
13889
+ this.reachShellBelow(pane, local, ray, seat.normal);
13890
+ this.paneBorder(pane, near, seat);
13891
+ this.keepUnderPane(pane, seat.normal);
13892
+ const across = new this.THREE.Vector3(0, 0, 1).transformDirection(this.c.vehicle.matrixWorld);
13893
+ if (Math.abs(seat.normal.dot(across)) < SIDE_PANE_FACING) {
13894
+ for (const body of shell) if (body.parent === this.c.vehicle) this.cutUnderPane(body, pane, seat.normal, SKIN_UNDER_PANE, SKIN_EDGE_KEPT);
13895
+ }
13735
13896
  }
13736
13897
  }
13898
+ /** 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. */
13899
+ shellAround(pane, shell) {
13900
+ const THREE = this.THREE;
13901
+ const box2 = new THREE.Box3().setFromObject(pane).expandByScalar(PANE_SEAT_REACH + PANE_SEAT_OUT);
13902
+ const out = [];
13903
+ const a = new THREE.Vector3();
13904
+ const b = new THREE.Vector3();
13905
+ const d = new THREE.Vector3();
13906
+ const tri = new THREE.Box3();
13907
+ for (const mesh2 of shell) {
13908
+ const pos = mesh2.geometry.attributes.position;
13909
+ const idx = mesh2.geometry.index;
13910
+ const count = idx ? idx.count : pos.count;
13911
+ const e = mesh2.matrixWorld.elements;
13912
+ const world = (v, i) => {
13913
+ const x = pos.getX(i);
13914
+ const y = pos.getY(i);
13915
+ const z = pos.getZ(i);
13916
+ 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]);
13917
+ };
13918
+ for (let f = 0; f + 2 < count; f += 3) {
13919
+ world(a, idx ? idx.getX(f) : f);
13920
+ world(b, idx ? idx.getX(f + 1) : f + 1);
13921
+ world(d, idx ? idx.getX(f + 2) : f + 2);
13922
+ tri.makeEmpty().expandByPoint(a).expandByPoint(b).expandByPoint(d);
13923
+ if (tri.intersectsBox(box2)) out.push(a.x, a.y, a.z, b.x, b.y, b.z, d.x, d.y, d.z);
13924
+ }
13925
+ }
13926
+ if (!out.length) return null;
13927
+ const g = new THREE.BufferGeometry();
13928
+ g.setAttribute("position", new THREE.Float32BufferAttribute(out, 3));
13929
+ const proxy = new THREE.Mesh(g, new THREE.MeshBasicMaterial({ side: THREE.DoubleSide }));
13930
+ proxy.updateMatrixWorld(true);
13931
+ return proxy;
13932
+ }
13737
13933
  /** The outward normal of a pane and how far the shell is at each sampled point (world). */
13738
13934
  paneSeat(pane, shell, ray) {
13739
13935
  const THREE = this.THREE;
@@ -13780,6 +13976,193 @@ var GameVehicles = (() => {
13780
13976
  }
13781
13977
  return samples.length >= 3 ? { normal, samples } : null;
13782
13978
  }
13979
+ /** 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. */
13980
+ reachShellBelow(pane, shell, ray, normal) {
13981
+ const THREE = this.THREE;
13982
+ const down = new THREE.Vector3(0, -1, 0).applyQuaternion(this.c.root.getWorldQuaternion(new THREE.Quaternion()));
13983
+ down.addScaledVector(normal, -down.dot(normal));
13984
+ if (down.lengthSq() < PANE_MIN_SLOPE) return;
13985
+ down.normalize();
13986
+ const g = pane.geometry;
13987
+ const pos = g.attributes.position;
13988
+ const v = new THREE.Vector3();
13989
+ let lowest = -Infinity;
13990
+ for (let i = 0; i < pos.count; i++) lowest = Math.max(lowest, v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld).dot(down));
13991
+ const edge = [];
13992
+ const mid = new THREE.Vector3();
13993
+ for (let i = 0; i < pos.count; i++) {
13994
+ v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
13995
+ if (v.dot(down) > lowest - PANE_EDGE_BAND) {
13996
+ edge.push(i);
13997
+ mid.add(v);
13998
+ }
13999
+ }
14000
+ if (!edge.length) return;
14001
+ mid.divideScalar(edge.length);
14002
+ let reach = 0;
14003
+ for (let d = PANE_REACH_STEP; d <= PANE_REACH_MAX; d += PANE_REACH_STEP) {
14004
+ const at = mid.clone().addScaledVector(down, d).addScaledVector(normal, PANE_SEAT_REACH);
14005
+ ray.set(at, normal.clone().negate());
14006
+ ray.far = PANE_SEAT_REACH * 2;
14007
+ if (ray.intersectObject(shell, false)[0]) {
14008
+ reach = d + PANE_REACH_STEP;
14009
+ break;
14010
+ }
14011
+ }
14012
+ if (!reach) return;
14013
+ const inverse = pane.matrixWorld.clone().invert();
14014
+ for (const i of edge) {
14015
+ v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld).addScaledVector(down, reach).applyMatrix4(inverse);
14016
+ pos.setXYZ(i, v.x, v.y, v.z);
14017
+ }
14018
+ pos.needsUpdate = true;
14019
+ g.computeBoundingBox();
14020
+ g.computeBoundingSphere();
14021
+ }
14022
+ /** 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. */
14023
+ keepUnderPane(pane, normal) {
14024
+ const THREE = this.THREE;
14025
+ const paneBox2 = new THREE.Box3().setFromObject(pane).expandByScalar(COWL_NEAR_PANE);
14026
+ const pos = pane.geometry.attributes.position;
14027
+ const centre = new THREE.Vector3();
14028
+ const v = new THREE.Vector3();
14029
+ for (let i = 0; i < pos.count; i++) centre.add(v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld));
14030
+ centre.divideScalar(pos.count);
14031
+ const plane2 = centre.dot(normal);
14032
+ for (const o of this.c.vehicle.children) {
14033
+ if (!o.isMesh || o === pane || o.userData.glass || this.c.deform?.includes(o)) continue;
14034
+ const box2 = new THREE.Box3().setFromObject(o);
14035
+ const size = box2.getSize(new THREE.Vector3());
14036
+ 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;
14037
+ if (box2.getCenter(new THREE.Vector3()).dot(normal) - plane2 >= COWL_UNDER_GLASS) continue;
14038
+ if (size.y >= COWL_PLATE) {
14039
+ o.material = this.I.makeMaterial(DASHBOARD_COLOR, { roughness: 1, metalness: 0 });
14040
+ continue;
14041
+ }
14042
+ o.material = this.paint();
14043
+ this.cutUnderPane(o, pane, normal);
14044
+ }
14045
+ }
14046
+ /** Cuts away the part of a mesh that lies under a pane (behind its plane, inside its outline seen along its normal). */
14047
+ cutUnderPane(mesh2, pane, normal, behind = -Infinity, inset = 0) {
14048
+ const THREE = this.THREE;
14049
+ const outline = this.paneOutline(pane, normal);
14050
+ if (outline.length < 3) return;
14051
+ const u = new THREE.Vector3(0, 1, 0).cross(normal);
14052
+ if (u.lengthSq() < 1e-4) u.set(1, 0, 0);
14053
+ u.normalize();
14054
+ const w = normal.clone().cross(u);
14055
+ const cx = outline.reduce((sum, p) => sum + p.dot(u), 0) / outline.length;
14056
+ const cy = outline.reduce((sum, p) => sum + p.dot(w), 0) / outline.length;
14057
+ const poly = outline.map((p) => {
14058
+ const x = p.dot(u);
14059
+ const y = p.dot(w);
14060
+ const d = Math.hypot(x - cx, y - cy) || 1;
14061
+ const k = Math.max(0, d - inset) / d;
14062
+ return [cx + (x - cx) * k, cy + (y - cy) * k];
14063
+ });
14064
+ const plane2 = outline.reduce((sum, p) => sum + p.dot(normal), 0) / outline.length;
14065
+ const inside = (x, y) => {
14066
+ let hit = false;
14067
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
14068
+ const [xi, yi] = poly[i];
14069
+ const [xj, yj] = poly[j];
14070
+ if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) hit = !hit;
14071
+ }
14072
+ return hit;
14073
+ };
14074
+ const v = new THREE.Vector3();
14075
+ const toWorld = mesh2.matrixWorld;
14076
+ mesh2.updateMatrixWorld(true);
14077
+ const xs = poly.map((p) => p[0]);
14078
+ const ys = poly.map((p) => p[1]);
14079
+ 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];
14080
+ subdivideNear(THREE, { mesh: mesh2, maxLen: COWL_CUT_SUB, near: (corners) => {
14081
+ const us = corners.map((c) => c.dot(u));
14082
+ const ws = corners.map((c) => c.dot(w));
14083
+ return Math.max(...us) >= x0 && Math.min(...us) <= x1 && Math.max(...ws) >= y0 && Math.min(...ws) <= y1;
14084
+ } });
14085
+ this.kit.cutMesh(
14086
+ mesh2,
14087
+ [
14088
+ {
14089
+ sdf: (x, y, z) => {
14090
+ v.set(x, y, z).applyMatrix4(toWorld);
14091
+ return inside(v.dot(u), v.dot(w)) && plane2 - v.dot(normal) > behind ? -1 : 1;
14092
+ }
14093
+ }
14094
+ ],
14095
+ 42,
14096
+ {}
14097
+ );
14098
+ }
14099
+ /** 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. */
14100
+ paneBorder(pane, near, seat) {
14101
+ const THREE = this.THREE;
14102
+ const bars = near.filter((o) => {
14103
+ if (o.geometry.attributes.position.count > PANE_BAR_VERTICES) return false;
14104
+ const size = new THREE.Box3().setFromObject(o).getSize(new THREE.Vector3());
14105
+ return Math.max(size.x, size.y, size.z) > PANE_BAR_LENGTH;
14106
+ });
14107
+ if (!bars.length) return;
14108
+ const outline = this.paneOutline(pane, seat.normal);
14109
+ if (outline.length < 3) return;
14110
+ const centre = new THREE.Vector3();
14111
+ for (const p of outline) centre.add(p);
14112
+ centre.divideScalar(outline.length);
14113
+ const position = [];
14114
+ const toVehicle = this.I.worldInverseOf(this.c);
14115
+ const put = (p) => {
14116
+ const q = p.clone().addScaledVector(seat.normal, PANE_BORDER_LIFT).applyMatrix4(toVehicle);
14117
+ position.push(q.x, q.y, q.z);
14118
+ };
14119
+ const inward = (p) => p.clone().add(centre.clone().sub(p).normalize().multiplyScalar(PANE_BORDER));
14120
+ outline.forEach((a, i) => {
14121
+ const b = outline[(i + 1) % outline.length];
14122
+ const c = inward(b);
14123
+ const d = inward(a);
14124
+ for (const p of [a, b, c, a, c, d]) put(p);
14125
+ });
14126
+ if (!position.length) return;
14127
+ for (const o of bars) o.visible = false;
14128
+ const band = new THREE.BufferGeometry();
14129
+ band.setAttribute("position", new THREE.Float32BufferAttribute(position, 3));
14130
+ band.computeVertexNormals();
14131
+ const material = this.I.MATERIALS.dark.clone();
14132
+ material.side = THREE.DoubleSide;
14133
+ const mesh2 = new THREE.Mesh(band, material);
14134
+ mesh2.userData.noFollow = true;
14135
+ mesh2.castShadow = false;
14136
+ mesh2.raycast = () => {
14137
+ };
14138
+ this.c.vehicle.add(mesh2);
14139
+ }
14140
+ /** The outline of a pane, the convex hull of its vertices seen along its normal, in world space. */
14141
+ paneOutline(pane, normal) {
14142
+ const THREE = this.THREE;
14143
+ const pos = pane.geometry.attributes.position;
14144
+ const u = new THREE.Vector3(0, 1, 0).cross(normal);
14145
+ if (u.lengthSq() < 1e-4) u.set(1, 0, 0);
14146
+ u.normalize();
14147
+ const w = normal.clone().cross(u);
14148
+ const points = [];
14149
+ for (let i = 0; i < pos.count; i++) {
14150
+ const p = new THREE.Vector3().fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
14151
+ points.push({ x: p.dot(u), y: p.dot(w), p });
14152
+ }
14153
+ points.sort((a, b) => a.x - b.x || a.y - b.y);
14154
+ const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
14155
+ const half = (list2) => {
14156
+ const out = [];
14157
+ for (const q of list2) {
14158
+ while (out.length >= 2 && cross(out[out.length - 2], out[out.length - 1], q) <= 0) out.pop();
14159
+ out.push(q);
14160
+ }
14161
+ out.pop();
14162
+ return out;
14163
+ };
14164
+ return [...half(points), ...half([...points].reverse())].map((q) => q.p);
14165
+ }
13783
14166
  /** How far the shell still stands past a seated pane, over its triangles. */
13784
14167
  shellPast(pane, shell, ray, normal) {
13785
14168
  const THREE = this.THREE;
@@ -13832,11 +14215,25 @@ var GameVehicles = (() => {
13832
14215
  g.computeBoundingSphere();
13833
14216
  mesh2.geometry = g;
13834
14217
  }
14218
+ /** 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. */
14219
+ seatHood() {
14220
+ const hood = this.c.hood;
14221
+ if (!hood?.lift) return;
14222
+ const THREE = this.THREE;
14223
+ const box2 = new THREE.Box3().setFromObject(hood.piv).getSize(new THREE.Vector3());
14224
+ for (const part of hood.piv.children) {
14225
+ part.position.y -= hood.lift + HOOD_SINK;
14226
+ 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)));
14227
+ }
14228
+ }
13835
14229
  /** A handle is at the free edge of its door, the end away from the hinge. */
13836
14230
  seatDoorHandles() {
13837
14231
  const THREE = this.THREE;
13838
14232
  const c = this.c;
13839
14233
  const inv = this.I.worldInverseOf(c);
14234
+ const bodyBox = new THREE.Box3();
14235
+ 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));
14236
+ const outerHalf = Math.max(Math.abs(bodyBox.min.z), Math.abs(bodyBox.max.z));
13840
14237
  for (const p of this.model.parts) {
13841
14238
  if (p.role !== "door") continue;
13842
14239
  const z = this.zone(p);
@@ -13850,8 +14247,7 @@ var GameVehicles = (() => {
13850
14247
  const at = box2.getCenter(new THREE.Vector3());
13851
14248
  const isHandle = size.x > DOOR_HANDLE_MIN && size.x < DOOR_HANDLE_MAX && size.y < DOOR_HANDLE_THIN && size.z < DOOR_HANDLE_THIN;
13852
14249
  if (!isHandle || Math.sign(at.z) !== Math.sign(z.side) || at.x < B.x0 || at.x > B.x1 || at.y < B.y0 || at.y > B.y1) continue;
13853
- const here = this.surface("z", z.side < 0 ? -1 : 1, B, [[at.x, at.y]]);
13854
- if (here != null && Math.abs(here - at.z) > DOOR_HANDLE_OUTSIDE) continue;
14250
+ if (Math.abs(at.z) < outerHalf - DOOR_HANDLE_OUTSIDE) continue;
13855
14251
  const skin = this.surface("z", z.side < 0 ? -1 : 1, B, [[free, at.y]]);
13856
14252
  o.position.x += free - at.x;
13857
14253
  if (skin != null) o.position.z += skin + (z.side < 0 ? -1 : 1) * (size.z / 2) - at.z;
@@ -13860,7 +14256,7 @@ var GameVehicles = (() => {
13860
14256
  c.root.updateMatrixWorld(true);
13861
14257
  }
13862
14258
  /** The glass of a drawn mirror sits inside its rounded housing, without corners standing past it. */
13863
- seatMirrorGlass(glass) {
14259
+ seatMirrorGlass(glass, drawn) {
13864
14260
  const g = glass.geometry.clone();
13865
14261
  g.computeBoundingBox();
13866
14262
  const mid = g.boundingBox.getCenter(new this.THREE.Vector3());
@@ -13868,6 +14264,37 @@ var GameVehicles = (() => {
13868
14264
  g.scale(MIRROR_GLASS_FIT, MIRROR_GLASS_FIT, MIRROR_GLASS_FIT);
13869
14265
  g.translate(mid.x, mid.y, mid.z);
13870
14266
  glass.geometry = g;
14267
+ const THREE = this.THREE;
14268
+ const volumeOf = (o) => {
14269
+ const size = new THREE.Box3().setFromObject(o).getSize(new THREE.Vector3());
14270
+ return size.x * size.y * size.z;
14271
+ };
14272
+ const housing = drawn.filter((o) => o !== glass && o.isMesh && o.geometry).sort((a, b) => volumeOf(b) - volumeOf(a))[0];
14273
+ if (housing) {
14274
+ this.c.root.updateMatrixWorld(true);
14275
+ const inv = this.I.worldInverseOf(this.c);
14276
+ glass.updateMatrixWorld(true);
14277
+ const gb = this.I.localBox(this.c, glass, inv);
14278
+ const hb = this.I.localBox(this.c, housing, inv);
14279
+ const side2 = hb.getCenter(new THREE.Vector3()).x > gb.getCenter(new THREE.Vector3()).x ? -1 : 1;
14280
+ const face = side2 < 0 ? hb.min.x + MIRROR_FACE_DEPTH : hb.max.x - MIRROR_FACE_DEPTH;
14281
+ const shape = housing.geometry.clone();
14282
+ const pos = shape.attributes.position;
14283
+ const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, housing.matrixWorld);
14284
+ const back = toVehicle.clone().invert();
14285
+ const v = new THREE.Vector3();
14286
+ for (let i = 0; i < pos.count; i++) {
14287
+ v.fromBufferAttribute(pos, i).applyMatrix4(toVehicle);
14288
+ if ((v.x - face) * side2 <= 0) continue;
14289
+ v.x = face;
14290
+ v.applyMatrix4(back);
14291
+ pos.setXYZ(i, v.x, v.y, v.z);
14292
+ }
14293
+ shape.computeVertexNormals();
14294
+ housing.geometry = shape;
14295
+ const outer = side2 < 0 ? gb.min.x : gb.max.x;
14296
+ glass.position.x += face + side2 * MIRROR_GLASS_PROUD - outer;
14297
+ }
13871
14298
  glass.material = this.I.makeMaterial(MIRROR_GLASS_COLOR, { metalness: 0.95, roughness: 0.12 });
13872
14299
  }
13873
14300
  /** The mirror the model already draws around where the part says, if it draws one. */
@@ -13892,7 +14319,11 @@ var GameVehicles = (() => {
13892
14319
  for (const o of drawn) {
13893
14320
  if (!o.isMesh || o.material !== this.I.MATERIALS.chrome) continue;
13894
14321
  const size = new this.THREE.Box3().setFromObject(o).getSize(new this.THREE.Vector3());
13895
- if (size.x < MIRROR_GLASS_THIN && size.y > MIRROR_GLASS_MIN && size.z > MIRROR_GLASS_MIN) this.seatMirrorGlass(o);
14322
+ const [thin, mid] = size.toArray().sort((a, b) => a - b);
14323
+ if (thin < MIRROR_GLASS_THIN && mid > MIRROR_GLASS_MIN) {
14324
+ this.seatMirrorGlass(o, drawn);
14325
+ o.userData.mirrorGlass = true;
14326
+ }
13896
14327
  }
13897
14328
  this.extra.set(p.id, drawn);
13898
14329
  return;
@@ -13904,14 +14335,26 @@ var GameVehicles = (() => {
13904
14335
  g.name = `obj_${p.id}`;
13905
14336
  g.position.set(cx, cy, cz);
13906
14337
  this.c.vehicle.add(g);
13907
- this.box(0.05, 0.04, 0.1, mats.dark, 0, -0.04, -s * 0.07, g);
13908
- const housing = this.box(0.1, 0.12, 0.05, mats.plastic ?? mats.dark, 0, 0, s * 0.02, g);
14338
+ this.box(0.05, 0.035, 0.05, mats.dark, 0, -0.04, -s * 0.06, g);
14339
+ const housing = this.box(0.08, 0.11, 0.125, mats.plastic ?? mats.dark, 0, 0, s * 0.0225, g);
13909
14340
  housing.userData.mirrorHousing = true;
13910
- const glass = this.box(0.095, 0.1, 6e-3, mats.chrome ?? mats.dark, 0, 0, s * 0.048, g);
14341
+ const glass = this.box(6e-3, 0.095, 0.115, mats.chrome ?? mats.dark, -0.043, 0, s * 0.0225, g);
13911
14342
  glass.userData.mirrorGlass = true;
13912
14343
  g.userData.rigid = true;
13913
14344
  this.extra.set(p.id, [g]);
13914
14345
  }
14346
+ /** 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. */
14347
+ shatterMirror(id) {
14348
+ let broke = false;
14349
+ for (const o of this.extra.get(id) ?? []) {
14350
+ o.traverse((m) => {
14351
+ if (!m.userData.mirrorGlass || !m.visible) return;
14352
+ m.visible = false;
14353
+ broke = true;
14354
+ });
14355
+ }
14356
+ return broke;
14357
+ }
13915
14358
  makeBattery(p) {
13916
14359
  const mats = this.I.MATERIALS;
13917
14360
  const g = new this.THREE.Group();
@@ -13957,6 +14400,11 @@ var GameVehicles = (() => {
13957
14400
  if (carrier) carrier.attach(m);
13958
14401
  }
13959
14402
  /** The door or lid a hinge group stands for. */
14403
+ /** 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. */
14404
+ extraIdOf(o) {
14405
+ for (const [id, objs] of this.extra) if (objs.includes(o)) return id;
14406
+ return void 0;
14407
+ }
13960
14408
  plateIdOf(o) {
13961
14409
  return o.userData.plateOf;
13962
14410
  }
@@ -14013,6 +14461,7 @@ var GameVehicles = (() => {
14013
14461
  best = g;
14014
14462
  }
14015
14463
  }
14464
+ if (p.hp > LAMINATED_HP && best.userData.glass) best.userData.glass.type = "laminated";
14016
14465
  this.kit.hitGlass(this.c, best, pt, new V3(...dir), force);
14017
14466
  return !!best.userData.glass?.broken;
14018
14467
  }
@@ -14034,6 +14483,17 @@ var GameVehicles = (() => {
14034
14483
  if (!z) return;
14035
14484
  for (const w of z.wheels) if (w.parent) this.I.flattenTire(w, k);
14036
14485
  }
14486
+ /** A tyre driven flat until it tore apart: the rubber is gone and the bare rim shows. */
14487
+ tyreTornOff(p) {
14488
+ const z = this.zone(p);
14489
+ if (!z) return;
14490
+ for (const w of z.wheels) {
14491
+ if (!w.parent || w.userData.tornOff) continue;
14492
+ w.userData.tornOff = true;
14493
+ for (const m of this.I.tireMeshes(w)) m.visible = false;
14494
+ w.position.y -= (w.userData.r ?? DEFAULT_WHEEL_RADIUS) * SIDEWALL_SHARE;
14495
+ }
14496
+ }
14037
14497
  /** Burning rubber: the tyre sags and goes black as it melts. */
14038
14498
  tyreMelt(p, melt) {
14039
14499
  const z = this.zone(p);
@@ -14357,6 +14817,41 @@ var GameVehicles = (() => {
14357
14817
  var SHIELDED = /* @__PURE__ */ new Set(["engine", "gearbox", "frame", "radiator", "fuel_tank", "oil_pan", "battery", "alternator", "brakes", "steering", "shocks"]);
14358
14818
  var AMBIENT = 20;
14359
14819
  var FLAME = 700;
14820
+ 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 };
14821
+ var ARMOUR_STEEL_MM = { IIIA: 4, III: 7 };
14822
+ var SOFT_LEAD = 0.8;
14823
+ var TYRE_FLAT_HEALTH = 0.1;
14824
+ var TYRE_BURST_JOULES = 1500;
14825
+ var TYRE_BURST_CALIBER = 12;
14826
+ var TYRE_LEAK_PER_MM2 = 26e-5;
14827
+ var FLAT_TYRE_LIFE_M = 1500;
14828
+ var FLAT_TYRE_TOWN_SPEED = 8;
14829
+ var RUN_FLAT_LIFE_M = 8e4;
14830
+ var RUN_FLAT_SPEED = 22;
14831
+ var TYRE_RUNFLAT_HEALTH = 0.6;
14832
+ var CRUMPLE_BASE_M = 0.04;
14833
+ var CRUMPLE_PER_FORCE_M = 0.15;
14834
+ var CRUMPLE_MIN_FORCE = 0.3;
14835
+ var CRUMPLE_MAX_FORCE = 1.4;
14836
+ var RIM_HEALTH = 0.05;
14837
+ var RIM_GRIND_PER_M = 4e-4;
14838
+ var TYRE_FLAT_SAG = 0.24;
14839
+ var TYRE_FLAT_AIR = 0.2;
14840
+ var BRAKE_FLUID_NEEDED = 0.6;
14841
+ var BRAKE_NO_FLUID = 0.15;
14842
+ var ENGINE_WORKING_C = 90;
14843
+ var ENGINE_SETTLE_S = 30;
14844
+ var ENGINE_HEATING = 0.5;
14845
+ var ENGINE_STEAM_C = 120;
14846
+ var ENGINE_SEIZE_C = 150;
14847
+ var COOLANT_NEEDED = 0.4;
14848
+ var OIL_NEEDED = 0.3;
14849
+ var OIL_WEAR = 4e-3;
14850
+ var ENGINE_SEIZE_HP = 0.2;
14851
+ var EXPANDED_HOLE = 1.6;
14852
+ var MIN_SPENT = 0.08;
14853
+ var WELDED = /* @__PURE__ */ new Set(["roof", "pillar", "frame", "panel"]);
14854
+ var CRUMPLES = /* @__PURE__ */ new Set(["door", "hood", "lid", "fender", "bumper"]);
14360
14855
  var FRACTURE = { glass: "shatter", plastic: "snap", rubber: "tear", steel: "none", aluminium: "none" };
14361
14856
  var add3 = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
14362
14857
  var r3 = (v) => [Math.round(v[0] * 1e5) / 1e5, Math.round(v[1] * 1e5) / 1e5, Math.round(v[2] * 1e5) / 1e5];
@@ -14368,6 +14863,8 @@ var GameVehicles = (() => {
14368
14863
  let c = vehicleCtx;
14369
14864
  const id = options.id ?? c.key;
14370
14865
  const headless = !!options.headless;
14866
+ const armourId = armourLevelOf(c.meta, options.armour);
14867
+ const armourLevel = armourId ? { ...ARMOUR_LEVELS[armourId], ...options.armourLevels?.[armourId] } : null;
14371
14868
  const seedBase = strHash(`${c.key}|${id}|${options.seed ?? 0}`);
14372
14869
  let rand = rng(seedBase);
14373
14870
  const ignitionRules = resolveIgnitionRules(options.ignition);
@@ -14425,7 +14922,7 @@ var GameVehicles = (() => {
14425
14922
  return d.ignitionC ? 1 : 0;
14426
14923
  };
14427
14924
  const setup = () => {
14428
- model = buildVehicleModel(kit, c, { hitch: options.hitch });
14925
+ model = buildVehicleModel(kit, c, { hitch: options.hitch, armour: armourLevel });
14429
14926
  ps.clear();
14430
14927
  for (const d of model.parts) {
14431
14928
  const kg = burnKg(d);
@@ -14445,6 +14942,8 @@ var GameVehicles = (() => {
14445
14942
  lost: false,
14446
14943
  status: "intact",
14447
14944
  flat: false,
14945
+ air: 1,
14946
+ airLoss: 0,
14448
14947
  fireLoss: 0
14449
14948
  });
14450
14949
  }
@@ -14545,7 +15044,8 @@ var GameVehicles = (() => {
14545
15044
  vis.breakLight(d, true);
14546
15045
  break;
14547
15046
  case "tyre":
14548
- vis.tyreFlat(d, 0.34);
15047
+ if (status === "broken") vis.tyreTornOff(d);
15048
+ else vis.tyreFlat(d, 0.34);
14549
15049
  break;
14550
15050
  case "mirror":
14551
15051
  case "bumper":
@@ -14575,7 +15075,8 @@ var GameVehicles = (() => {
14575
15075
  const damage = (p, amount, dtype, source, out, cause) => {
14576
15076
  if (p.lost || !(amount > 0)) return;
14577
15077
  const before = p.hp;
14578
- p.hp = Math.max(0, p.hp - amount);
15078
+ const crumples = dtype === "blunt" && CRUMPLES.has(p.def.role) && (p.def.material === "steel" || p.def.material === "aluminium");
15079
+ p.hp = Math.max(WELDED.has(p.def.role) || crumples ? 1 : 0, p.hp - amount);
14579
15080
  out.push(ev({ type: "damage", part: p.def.id, amount: r2(before - p.hp), hp: r2(p.hp), damageType: dtype, ...source ? { source } : {} }));
14580
15081
  wear(p, amount, out);
14581
15082
  syncZone(p);
@@ -14618,11 +15119,22 @@ var GameVehicles = (() => {
14618
15119
  const p = v.clone().applyMatrix4(M);
14619
15120
  return [p.x, p.y, p.z];
14620
15121
  };
14621
- const armour = () => c.meta.armored || c.meta.bullet ? 30 : 1;
15122
+ const armour = () => armourLevel?.steel ?? 1;
15123
+ const steelMm = (d) => {
15124
+ const base = STEEL_MM[d.role] ?? STEEL_MM.panel;
15125
+ return d.material === "steel" || d.material === "aluminium" ? base * (armourLevel ? ARMOUR_STEEL_MM[armourId === "III" ? "III" : "IIIA"] / STEEL_MM.panel : 1) : base;
15126
+ };
15127
+ const goesThrough = (h, d, p, energy, res) => {
15128
+ const piercing = h.piercing ?? 1;
15129
+ const metal = d.material === "steel" || d.material === "aluminium";
15130
+ const mm = h.penetration?.steel;
15131
+ if (metal && mm != null) return mm >= steelMm(d) * (p.dents.length > 3 ? 0.9 : 1);
15132
+ return energy * (metal || d.material === "glass" ? piercing : 1) >= res;
15133
+ };
14622
15134
  const resist = (d, p) => {
14623
15135
  let base = RESIST[d.material] ?? 100;
14624
15136
  if (d.material === "glass" && d.hp > 1e3) base = 120;
14625
- if (d.material === "glass" && c.meta.bullet) base *= 30;
15137
+ if (d.material === "glass" && armourLevel) base = armourLevel.glassStops;
14626
15138
  const k = ROLE_RESIST[d.role] ?? 1;
14627
15139
  const arm = d.material === "steel" || d.material === "aluminium" ? armour() : 1;
14628
15140
  return base * k * arm * (p.dents.length > 3 ? 0.9 : 1);
@@ -14645,7 +15157,7 @@ var GameVehicles = (() => {
14645
15157
  if (vis) {
14646
15158
  const pos = new V3(d.box.center[0], Math.max(0.2, (c.meta.clearance ?? 0.3) * 0.8), d.box.center[2]);
14647
15159
  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 });
14648
- else if ((c.meta.kind ?? "car") !== "boat") l.pud = I.puddle(c, fluidKind(fluid), pos, fluid === "oil" ? 0.8 : 1.1);
15160
+ 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);
14649
15161
  }
14650
15162
  }
14651
15163
  const out3 = dirToWorld(new V3(dirLocal.x * 0.5, dirLocal.y * 0.5 - 0.6, dirLocal.z * 0.5));
@@ -14682,7 +15194,7 @@ var GameVehicles = (() => {
14682
15194
  exploded = true;
14683
15195
  const d = tank.def;
14684
15196
  const spec = d.explodes ?? { energy: 8e5, radius: 5, atC: 320 };
14685
- out.push(ev({ type: "explode", part: d.id, point: toWorld(d.box.center), energy: spec.energy, radius: spec.radius }));
15197
+ 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 } : {} }));
14686
15198
  tank.liters = 0;
14687
15199
  leaks.delete(d.id);
14688
15200
  if (vis) kit.explode(c, { delay: 0.6 });
@@ -14782,7 +15294,7 @@ var GameVehicles = (() => {
14782
15294
  const dir = dirToLocal(h.direction ?? [0, 0, -1]).normalize();
14783
15295
  const n = h.normal ? dirToLocal(h.normal).normalize() : normalOf(d, v);
14784
15296
  const res = resist(d, p);
14785
- const through = E >= res;
15297
+ const through = goesThrough(h, d, p, E, res);
14786
15298
  const cosInc = Math.abs(dir.dot(n));
14787
15299
  const metal = d.material === "steel" || d.material === "aluminium";
14788
15300
  const worldPoint = toWorld(v);
@@ -14793,13 +15305,21 @@ var GameVehicles = (() => {
14793
15305
  vis?.spark([v.x, v.y, v.z], [refl.x, refl.y, refl.z], 6);
14794
15306
  return;
14795
15307
  }
14796
- const holeDiam = Math.max(6e-3, cal / 1e3 * (d.material === "glass" ? 1.4 : 1));
15308
+ const opens = (h.piercing ?? 1) < SOFT_LEAD ? EXPANDED_HOLE : 1;
15309
+ const holeDiam = Math.max(6e-3, cal / 1e3 * (d.material === "glass" ? 1.4 : 1) * opens);
15310
+ const energyLeft = () => {
15311
+ const metal2 = d.material === "steel" || d.material === "aluminium";
15312
+ const mm = h.penetration?.steel;
15313
+ const spent = metal2 && mm ? clamp(steelMm(d) / mm, MIN_SPENT, 1) : clamp(res / Math.max(E, 1), MIN_SPENT, 1);
15314
+ return E * (1 - spent);
15315
+ };
14797
15316
  const push = (hole) => {
14798
15317
  if (!hole) return;
14799
15318
  const hp = { point: r3(fromVehicle(v, M)), normal: r3(fromVehicle(n, new THREE.Matrix4().extractRotation(M))), diameter: r2(holeDiam), through };
14800
15319
  p.holes.push(hp);
14801
- out.push(ev({ type: "hole", part: d.id, hole: hp, worldPoint, ...through ? { exitPoint: add3(worldPoint, r3(dirToWorld(dir.clone().multiplyScalar(Math.max(0.02, Math.min(d.box.size[0], d.box.size[1], d.box.size[2])))))) } : {} }));
15320
+ 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])))))) } : {} }));
14802
15321
  vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam, d);
15322
+ if (d.role === "mirror") vis?.shatterMirror(d.id);
14803
15323
  startLeak(p, v, cal, out, dir);
14804
15324
  };
14805
15325
  switch (d.material) {
@@ -14812,7 +15332,7 @@ var GameVehicles = (() => {
14812
15332
  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) };
14813
15333
  p.cracks.push(crack);
14814
15334
  out.push(ev({ type: "crack", part: d.id, crack, worldPoint }));
14815
- damage(p, E * 0.5, "ballistic", h.source, out, worldPoint);
15335
+ damage(p, E * (armourLevel && !through ? armourLevel.glassTakes : 0.5), "ballistic", h.source, out, worldPoint);
14816
15336
  } else damage(p, laminated ? E * 0.5 : p.hp, "ballistic", h.source, out, worldPoint);
14817
15337
  break;
14818
15338
  }
@@ -14820,8 +15340,8 @@ var GameVehicles = (() => {
14820
15340
  push(through);
14821
15341
  damage(p, E * 0.4, "ballistic", h.source, out, worldPoint);
14822
15342
  if (through && !p.lost && !p.flat) {
14823
- p.flat = true;
14824
- vis?.tyreFlat(d, 0.24);
15343
+ if (E >= TYRE_BURST_JOULES || cal >= TYRE_BURST_CALIBER) deflate(p, 1);
15344
+ else p.airLoss += TYRE_LEAK_PER_MM2 * Math.PI * (cal / 2) ** 2;
14825
15345
  }
14826
15346
  break;
14827
15347
  }
@@ -14937,7 +15457,7 @@ var GameVehicles = (() => {
14937
15457
  const electric = c.spec.fuel === "electric";
14938
15458
  const tank = tankPart();
14939
15459
  const fuel = tank ? tank.liters ?? 0 : electric ? 1 : 1;
14940
- const engineOk = sound("engine") && !exploded && fuel > 0.05;
15460
+ const engineOk = sound("engine") && !exploded && !seized && fuel > 0.05;
14941
15461
  const powered = sound("battery") && sound("alternator");
14942
15462
  const gearOk = sound("gearbox");
14943
15463
  const brakes = [...ps.values()].find((p) => p.def.role === "brakes");
@@ -15097,9 +15617,12 @@ var GameVehicles = (() => {
15097
15617
  const state = () => stateCache ?? (stateCache = buildState());
15098
15618
  const isSimulating = () => {
15099
15619
  if (leaks.size > 0) return true;
15100
- for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT)) return true;
15620
+ for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT || p.airLoss > 0 && p.air > 0)) return true;
15101
15621
  return false;
15102
15622
  };
15623
+ let engineHeat = ENGINE_WORKING_C;
15624
+ let seized = false;
15625
+ let steaming = false;
15103
15626
  const surfaceRays = new THREE.Raycaster();
15104
15627
  surfaceRays.layers.enableAll();
15105
15628
  surfaceRays.camera = new THREE.PerspectiveCamera();
@@ -15123,14 +15646,24 @@ var GameVehicles = (() => {
15123
15646
  if (pane) return ps.get(pane) ?? null;
15124
15647
  for (let q = o; q && q !== c.vehicle; q = q.parent) {
15125
15648
  if (vis?.plateIdOf(q)) return ps.get(vis.plateIdOf(q)) ?? null;
15649
+ const piece = vis?.extraIdOf(q);
15650
+ if (piece) return ps.get(piece) ?? null;
15126
15651
  }
15127
15652
  return locate(v, 0.08, (p) => p.def.role === "glass");
15128
15653
  };
15654
+ const surfaceMeshes = [];
15655
+ const collectSurfaces = (o) => {
15656
+ if (!o.visible || o.userData.decal || o.userData.ghost || o.userData.noSurfaceRay) return;
15657
+ if (o.isMesh) surfaceMeshes.push(o);
15658
+ for (const child of o.children) collectSurfaces(child);
15659
+ };
15129
15660
  const surfaceRay = (ray) => {
15130
15661
  sync();
15131
15662
  surfaceRays.set(new V3(ray.origin[0], ray.origin[1], ray.origin[2]), new V3(ray.direction[0], ray.direction[1], ray.direction[2]));
15132
15663
  surfaceRays.far = ray.maxDistance;
15133
- for (const h of surfaceRays.intersectObject(c.vehicle, true)) {
15664
+ surfaceMeshes.length = 0;
15665
+ for (const child of c.vehicle.children) collectSurfaces(child);
15666
+ for (const h of surfaceRays.intersectObjects(surfaceMeshes, false)) {
15134
15667
  if (!h.face || !isDrawnSurface(h.object)) continue;
15135
15668
  const p = partOfSurface(h.object, c.vehicle.worldToLocal(h.point.clone()));
15136
15669
  if (!p || p.lost || ray.ignored?.(p.def.id)) continue;
@@ -15164,7 +15697,29 @@ var GameVehicles = (() => {
15164
15697
  applyEffects();
15165
15698
  return out;
15166
15699
  };
15700
+ const runFlat = armourId != null;
15701
+ const tyreHealth = (p) => {
15702
+ if (p.lost) return RIM_HEALTH;
15703
+ if (p.flat) return runFlat ? TYRE_RUNFLAT_HEALTH : TYRE_FLAT_HEALTH;
15704
+ return Math.min(p.hp / p.def.hp, 0.2 + 0.8 * p.air);
15705
+ };
15706
+ const tyreOf = (rim) => {
15707
+ let best;
15708
+ let bestD = Infinity;
15709
+ for (const p of ps.values()) {
15710
+ if (p.def.role !== "tyre") continue;
15711
+ const d = Math.hypot(p.def.box.center[0] - rim.def.box.center[0], p.def.box.center[2] - rim.def.box.center[2]);
15712
+ if (d < bestD) [best, bestD] = [p, d];
15713
+ }
15714
+ return best;
15715
+ };
15716
+ const deflate = (p, share) => {
15717
+ p.air = Math.max(0, p.air - share);
15718
+ vis?.tyreFlat(p.def, TYRE_FLAT_SAG * (1 - p.air));
15719
+ if (p.air <= TYRE_FLAT_AIR) p.flat = true;
15720
+ };
15167
15721
  const update = (dt) => {
15722
+ for (const p of ps.values()) if (p.airLoss > 0 && p.air > 0 && !p.lost) deflate(p, p.airLoss * dt);
15168
15723
  clock += dt;
15169
15724
  const out = queue;
15170
15725
  queue = [];
@@ -15280,6 +15835,70 @@ var GameVehicles = (() => {
15280
15835
  partName: (pid) => ps.get(pid)?.def.name ?? pid,
15281
15836
  status: computeStatus,
15282
15837
  animations: () => createVehicleClips(model, vis, kit, c),
15838
+ crumple: (point, direction, depth) => {
15839
+ if (!vis || depth <= 0) return;
15840
+ sync();
15841
+ const force = clamp((depth - CRUMPLE_BASE_M) / CRUMPLE_PER_FORCE_M, CRUMPLE_MIN_FORCE, CRUMPLE_MAX_FORCE);
15842
+ kit.impact(c, toLocal(point), dirToLocal(direction).normalize(), force, { maxDepth: depth });
15843
+ stateCache = void 0;
15844
+ },
15845
+ rollFlat: (meters, speed) => {
15846
+ for (const p of ps.values()) {
15847
+ if (p.def.role !== "tyre" || !p.flat || p.lost) continue;
15848
+ 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);
15849
+ p.hp = Math.max(0, p.hp - p.def.hp * meters / life);
15850
+ if (p.hp > 0) continue;
15851
+ const at = toWorld(p.def.box.center);
15852
+ lose(p, "broken", queue, at);
15853
+ 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) }));
15854
+ }
15855
+ let grinding = 0;
15856
+ for (const rim of ps.values()) {
15857
+ if (rim.def.role !== "rim" || rim.lost || !tyreOf(rim)?.lost) continue;
15858
+ grinding += 1;
15859
+ rim.hp = Math.max(1, rim.hp - rim.def.hp * RIM_GRIND_PER_M * meters);
15860
+ const [x, y, z] = rim.def.box.center;
15861
+ 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) }));
15862
+ }
15863
+ stateCache = void 0;
15864
+ return grinding;
15865
+ },
15866
+ runEngine: (seconds, load) => {
15867
+ const engine = [...ps.values()].find((p) => p.def.role === "engine");
15868
+ if (!engine || engine.lost || seized) return engineHeat;
15869
+ const share = (fluid) => {
15870
+ const list2 = [...ps.values()].filter((p) => p.def.contains?.fluid === fluid);
15871
+ return list2.length ? Math.min(...list2.map((p) => (p.liters ?? 0) / p.def.contains.liters)) : 1;
15872
+ };
15873
+ const coolant = share("coolant");
15874
+ const oil = share("oil");
15875
+ const work = 0.4 + 0.6 * clamp(load, 0, 1);
15876
+ if (coolant < COOLANT_NEEDED) engineHeat += (COOLANT_NEEDED - coolant) / COOLANT_NEEDED * ENGINE_HEATING * work * seconds;
15877
+ else engineHeat += (ENGINE_WORKING_C - engineHeat) * Math.min(1, seconds / ENGINE_SETTLE_S);
15878
+ if (oil < OIL_NEEDED) engine.hp = Math.max(0, engine.hp - engine.def.hp * OIL_WEAR * work * seconds * ((OIL_NEEDED - oil) / OIL_NEEDED));
15879
+ if (engineHeat >= ENGINE_STEAM_C && vis && !steaming) {
15880
+ steaming = true;
15881
+ kit.addEmitter(c, new V3(engine.def.box.center[0], engine.def.box.center[1] + 0.4, 0), "white", { rate: 8, s: 1 });
15882
+ }
15883
+ if (engineHeat >= ENGINE_SEIZE_C || engine.hp <= engine.def.hp * ENGINE_SEIZE_HP) {
15884
+ seized = true;
15885
+ queue.push(ev({ type: "damage", part: engine.def.id, amount: 0, hp: r2(engine.hp), damageType: "heat" }));
15886
+ }
15887
+ stateCache = void 0;
15888
+ return engineHeat;
15889
+ },
15890
+ driveReport: () => {
15891
+ const health = (role) => {
15892
+ const list2 = [...ps.values()].filter((p) => p.def.role === role);
15893
+ if (!list2.length) return 1;
15894
+ return Math.min(...list2.map((p) => p.lost || !attached(p.def.id) ? 0 : p.hp / p.def.hp));
15895
+ };
15896
+ 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) }));
15897
+ const status = computeStatus();
15898
+ const brakeSystem = [...ps.values()].find((p) => p.def.role === "brakes" && p.def.contains);
15899
+ const fluidLeft = brakeSystem ? clamp((brakeSystem.liters ?? 0) / brakeSystem.def.contains.liters / BRAKE_FLUID_NEEDED, 0, 1) : 1;
15900
+ 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") };
15901
+ },
15283
15902
  partWorldCenter: (pid) => {
15284
15903
  sync();
15285
15904
  const d = ps.get(pid)?.def;
@@ -15363,6 +15982,34 @@ var GameVehicles = (() => {
15363
15982
  };
15364
15983
  }
15365
15984
 
15985
+ // src/destructible/crash.ts
15986
+ var CRASH_RULES = { energyToDamage: 0.3, hitSpacing: 0.6, minSpeed: 1.5, rows: [0, 0.3], crushPerMs: 0.035, maxCrush: 0.6 };
15987
+ function crashDeltaV(speed, massKg, otherMassKg) {
15988
+ if (!Number.isFinite(otherMassKg)) return speed;
15989
+ return speed * otherMassKg / (massKg + otherMassKg);
15990
+ }
15991
+ function crashVehicle(d, crash) {
15992
+ const rules = { ...CRASH_RULES, ...crash.rules };
15993
+ const deltaV = crashDeltaV(crash.speed, crash.massKg, crash.otherMassKg);
15994
+ const energy = 0.5 * crash.massKg * deltaV * deltaV;
15995
+ if (crash.speed < rules.minSpeed) return { deltaV, energy, events: [] };
15996
+ const [dx, , dz] = crash.direction;
15997
+ const length = Math.hypot(dx, dz) || 1;
15998
+ const across = [-dz / length, 0, dx / length];
15999
+ const hits = Math.max(1, Math.round(crash.contactWidth / rules.hitSpacing));
16000
+ const each = energy * rules.energyToDamage / (hits * rules.rows.length);
16001
+ const events = [];
16002
+ for (const up of rules.rows)
16003
+ for (let i = 0; i < hits; i++) {
16004
+ const offset = hits === 1 ? 0 : (i / (hits - 1) - 0.5) * crash.contactWidth;
16005
+ const point = [crash.point[0] + across[0] * offset, crash.point[1] + up, crash.point[2] + across[2] * offset];
16006
+ events.push(...d.hit({ type: "blunt", point, direction: [dx / length, 0, dz / length], energy: each, radius: rules.hitSpacing / 2, ...crash.source ? { source: crash.source } : {} }));
16007
+ }
16008
+ const crush = Math.min(rules.maxCrush, deltaV * rules.crushPerMs);
16009
+ d.crumple(crash.point, [dx / length, 0, dz / length], crush);
16010
+ return { deltaV, energy, events };
16011
+ }
16012
+
15366
16013
  // src/destructible/enclosure.ts
15367
16014
  var CABIN_LEAK = 0.02;
15368
16015
  var HOLE_RATE_PER_AREA = 3e3;