@adatechnology/game-vehicles 0.6.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,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;
@@ -4877,6 +4882,7 @@ var GameVehicles = (() => {
4877
4882
  }
4878
4883
 
4879
4884
  // src/hood.ts
4885
+ var HOOD_LIFT = 8e-3;
4880
4886
  function installHood(kit) {
4881
4887
  const THREE = kit.THREE;
4882
4888
  const V3 = THREE.Vector3;
@@ -4899,7 +4905,7 @@ var GameVehicles = (() => {
4899
4905
  for (let i = 0; i < p.count; i++) {
4900
4906
  p.setX(i, cx + (p.getX(i) - cx) * sx);
4901
4907
  p.setZ(i, p.getZ(i) * sz);
4902
- p.setY(i, p.getY(i) + 8e-3);
4908
+ p.setY(i, p.getY(i) + HOOD_LIFT);
4903
4909
  }
4904
4910
  geo.translate(-hx, -hy, 0);
4905
4911
  const under = geo.clone();
@@ -4928,7 +4934,8 @@ var GameVehicles = (() => {
4928
4934
  yMin: bb.min.y,
4929
4935
  zw: bb.max.z,
4930
4936
  label: o.label,
4931
- noLatch: !!o.noLatch
4937
+ noLatch: !!o.noLatch,
4938
+ lift: HOOD_LIFT
4932
4939
  };
4933
4940
  c.part("hood", { key: "hood" }, piv, new V3((hx + bb.max.x) / 2, hy, 0), 0.9);
4934
4941
  return c.hood;
@@ -6361,6 +6368,14 @@ var GameVehicles = (() => {
6361
6368
  }
6362
6369
 
6363
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
+ };
6364
6379
  function installFluids(kit) {
6365
6380
  const THREE = kit.THREE;
6366
6381
  const V3 = THREE.Vector3;
@@ -6580,6 +6595,13 @@ var GameVehicles = (() => {
6580
6595
  transparent: true,
6581
6596
  opacity: 0.9,
6582
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
6583
6605
  }) : new THREE.MeshStandardMaterial({
6584
6606
  color: 0,
6585
6607
  roughness: 0.03,
@@ -7425,12 +7447,36 @@ var GameVehicles = (() => {
7425
7447
  if (g.userData.crease && c.smoothLater) c.smoothLater(msh);
7426
7448
  else if (g.userData.crease) crease(g, 42);
7427
7449
  else g.computeVertexNormals();
7428
- g.computeBoundingBox();
7429
7450
  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);
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
+ }
7434
7480
  }
7435
7481
  });
7436
7482
  const dset = new Set(c.deform);
@@ -7834,7 +7880,8 @@ var GameVehicles = (() => {
7834
7880
  const G = gm2.userData.glass;
7835
7881
  if (!G || G.broken || !gm2.visible || !gm2.parent) return;
7836
7882
  if (G.type === "laminated") {
7837
- 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;
7838
7885
  }
7839
7886
  I.shatterGlass(c, gm2, 1.4, new V3(0, 1, 0));
7840
7887
  });
@@ -7845,19 +7892,6 @@ var GameVehicles = (() => {
7845
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)));
7846
7893
  c.hood = null;
7847
7894
  }
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
7895
  if (m.rearBumper && m.rearBumper.parent)
7862
7896
  fling(
7863
7897
  c,
@@ -7865,20 +7899,6 @@ var GameVehicles = (() => {
7865
7899
  c.dirToEffects(new V3(rv(-5, -3), rv(4, 6), rv(-1, 1))),
7866
7900
  new V3(rv(-3, 3), rv(-6, 6), rv(-3, 3))
7867
7901
  );
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
7902
  burnAll(c);
7883
7903
  c.emitters = c.emitters.filter(function(e) {
7884
7904
  return e.kind === "exhaust" ? false : true;
@@ -12673,6 +12693,17 @@ var GameVehicles = (() => {
12673
12693
  return kit;
12674
12694
  }
12675
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
+
12676
12707
  // src/destructible/model.ts
12677
12708
  var JOINT_RULES = {
12678
12709
  hinge: { strength: 5e3, tools: ["wrench", "screwdriver"], seconds: 3 },
@@ -12716,6 +12747,12 @@ var GameVehicles = (() => {
12716
12747
  center: [(z.box.x0 + z.box.x1) / 2, (z.box.y0 + z.box.y1) / 2, (z.box.z0 + z.box.z1) / 2],
12717
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)]
12718
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
+ };
12719
12756
  var skinOf = (kind) => kind === "plane" || kind === "heli" || kind === "boat" ? "aluminium" : "steel";
12720
12757
  var isFender = (id) => /^fender/.test(id);
12721
12758
  var roleOfZone = (z) => {
@@ -12773,7 +12810,8 @@ var GameVehicles = (() => {
12773
12810
  const spec = c.spec;
12774
12811
  const t = kit.t;
12775
12812
  const skin = skinOf(kind);
12776
- const armour = m.armored || m.bullet ? 12 : 1;
12813
+ const armoured = options.armour ?? null;
12814
+ const armour = armoured ? 12 : 1;
12777
12815
  const mass = vehicleMass(c);
12778
12816
  const W = m.width ?? 1.7;
12779
12817
  const F = m.front ?? 2;
@@ -12839,8 +12877,8 @@ var GameVehicles = (() => {
12839
12877
  }
12840
12878
  if (role === "glass") {
12841
12879
  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] });
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] });
12844
12882
  continue;
12845
12883
  }
12846
12884
  if (role === "light") {
@@ -12878,7 +12916,7 @@ var GameVehicles = (() => {
12878
12916
  zoneId: id,
12879
12917
  visual: false,
12880
12918
  contains: { fluid: diesel ? "diesel" : "fuel", liters },
12881
- 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" },
12882
12920
  ignitionC: diesel ? 330 : 260,
12883
12921
  tags: ["fuel_tank", ...tags]
12884
12922
  });
@@ -12914,8 +12952,8 @@ var GameVehicles = (() => {
12914
12952
  if (claimed.has(pane)) continue;
12915
12953
  const G = pane.userData.glass;
12916
12954
  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"] });
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"] });
12919
12957
  }
12920
12958
  if (!hubZone) {
12921
12959
  const h = Math.max(0.15, belt - clearance);
@@ -12971,7 +13009,7 @@ var GameVehicles = (() => {
12971
13009
  visual: false,
12972
13010
  mechanical: true,
12973
13011
  contains: { fluid: diesel ? "diesel" : "fuel", liters },
12974
- 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" },
12975
13013
  ignitionC: diesel ? 330 : 260,
12976
13014
  tags: ["fuel_tank"]
12977
13015
  });
@@ -13248,38 +13286,121 @@ var GameVehicles = (() => {
13248
13286
  }
13249
13287
  }
13250
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
+ }
13251
13293
  return { parts, joints, hub: hubId, hitch };
13252
13294
  }
13253
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
+
13254
13342
  // src/destructible/visual.ts
13255
13343
  var HOLE_DECAL_SCALE = 2.4;
13256
13344
  var DOOR_DEPTH = 0.07;
13257
- var DOOR_EDGE_PIECES = 16;
13345
+ var DOOR_EDGE_PIECES = 8;
13346
+ var DOOR_EDGE_REACH = 0.08;
13258
13347
  var DOOR_CARD_COLOR = "#3b3e42";
13259
13348
  var DOOR_LINER_COLOR = "#5a5e64";
13260
13349
  var DOOR_LINER_DARK = 0.15;
13261
- var DOOR_STRIP_THIN = 0.05;
13350
+ var DOOR_STRIP_THIN = 0.08;
13262
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;
13263
13356
  var DOOR_STRIP_OVER = 0.3;
13264
13357
  var DOOR_TOP_OVER_PANE = 0.03;
13265
- var DOOR_STRIP_SUB = 0.02;
13266
13358
  var HOLE_TEXTURE_SIZE = 64;
13359
+ var LAMINATED_HP = 1e3;
13360
+ var HOOD_SINK = 2e-3;
13361
+ var HOOD_GAP_CLOSED = 0.016;
13267
13362
  var DOOR_HANDLE_INSET = 0.12;
13268
13363
  var DOOR_HANDLE_MIN = 0.07;
13269
13364
  var DOOR_HANDLE_MAX = 0.24;
13270
13365
  var DOOR_HANDLE_THIN = 0.07;
13271
- var DOOR_HANDLE_OUTSIDE = 0.1;
13366
+ var DOOR_HANDLE_OUTSIDE = 0.15;
13272
13367
  var PANE_SEAT_OUT = 0.05;
13273
13368
  var PANE_SEAT_IN = -0.05;
13274
13369
  var PANE_SEAT_REACH = 0.14;
13275
13370
  var PANE_SEAT_SAMPLES = 24;
13276
13371
  var PANE_SEAT_LIFT = 4e-3;
13277
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;
13278
13395
  var DRAWN_MIRROR_SIZE = 0.3;
13279
13396
  var DRAWN_MIRROR_REACH = 0.4;
13280
13397
  var DRAWN_MIRROR_MOUNT = 0.2;
13281
13398
  var MIRROR_GLASS_FIT = 0.76;
13399
+ var MIRROR_GLASS_PROUD = 1e-3;
13400
+ var MIRROR_FACE_DEPTH = 0.012;
13282
13401
  var MIRROR_GLASS_THIN = 0.04;
13402
+ var DEFAULT_WHEEL_RADIUS = 0.3;
13403
+ var SIDEWALL_SHARE = 0.3;
13283
13404
  var MIRROR_GLASS_MIN = 0.05;
13284
13405
  var MIRROR_GLASS_COLOR = "#59636a";
13285
13406
  var HOLE_RIM = { steel: "#b7bcc0", aluminium: "#d3d6d8", plastic: "#d9d6cc", rubber: "#141414", glass: "#eef3f5" };
@@ -13366,6 +13487,7 @@ var GameVehicles = (() => {
13366
13487
  this.hood = this.nameHood();
13367
13488
  this.seatPanes();
13368
13489
  this.seatDoorHandles();
13490
+ this.seatHood();
13369
13491
  this.warming = c.deform.filter((msh) => msh.geometry && msh.geometry.attributes.position && msh.geometry.attributes.position.count >= 1500);
13370
13492
  if (!this.warming.length) this.warming = null;
13371
13493
  for (const p of model.parts) {
@@ -13566,7 +13688,8 @@ var GameVehicles = (() => {
13566
13688
  const panes = I.windowsOver(this.c, z, inv);
13567
13689
  const seals = panes.map((g) => I.paneSeals.get(g)).filter((s) => !!s);
13568
13690
  const mirrors = this.mirrorsOn(z);
13569
- objs = [...pcs, ...this.doorCards(pcs, z), ...I.followersIn(this.c, z, inv), ...panes, ...seals, ...mirrors];
13691
+ const frames = this.framesAround(panes, inv);
13692
+ objs = [...pcs, ...I.followersIn(this.c, z, inv), ...panes, ...seals, ...mirrors, ...frames];
13570
13693
  }
13571
13694
  } else {
13572
13695
  const pcs = this.cutZone(z);
@@ -13611,7 +13734,10 @@ var GameVehicles = (() => {
13611
13734
  run(x1, y0, x1, y1);
13612
13735
  run(x1, y1, x0, y1);
13613
13736
  run(x0, y1, x0, y0);
13614
- const heights = loop.map(([x, y]) => this.surface("z", side2, B, [[x, y]]) ?? flat);
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
+ });
13615
13741
  const band = (lift) => {
13616
13742
  const position = [];
13617
13743
  const index = [];
@@ -13629,6 +13755,23 @@ var GameVehicles = (() => {
13629
13755
  };
13630
13756
  const paint = this.paint().clone();
13631
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);
13632
13775
  const edges = new THREE.Mesh(band(2e-3), paint);
13633
13776
  const lining = new THREE.Mesh(band(2e-3), paint);
13634
13777
  for (const m of [edges, lining]) {
@@ -13638,6 +13781,17 @@ var GameVehicles = (() => {
13638
13781
  }
13639
13782
  piv.attach(edges);
13640
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
+ }
13641
13795
  /** A front door carries the mirror drawn at its corner. */
13642
13796
  mirrorsOn(z) {
13643
13797
  if (!z.mirror) return [];
@@ -13663,45 +13817,42 @@ var GameVehicles = (() => {
13663
13817
  const B = z.box;
13664
13818
  const x0 = B.x0 + 0.03;
13665
13819
  const x1 = B.x1 - 0.03;
13666
- const y0 = B.y0 - DOOR_STRIP_NEAR;
13667
- const y1 = B.y1 + DOOR_STRIP_NEAR;
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;
13668
13827
  const deform = this.c.deform ?? [];
13669
13828
  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;
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;
13671
13830
  const box2 = this.I.localBox(this.c, o, inv);
13672
13831
  const size = box2.getSize(new THREE.Vector3());
13673
13832
  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;
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;
13675
13834
  const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld);
13676
13835
  const v = new THREE.Vector3();
13677
13836
  const sdf = (x, y, zz) => {
13678
13837
  v.set(x, y, zz).applyMatrix4(toVehicle);
13679
13838
  return Math.max(x0 - v.x, v.x - x1);
13680
13839
  };
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
- });
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
+ }
13705
13856
  }
13706
13857
  mend(dirty) {
13707
13858
  dirty.forEach((msh) => {
@@ -13726,14 +13877,58 @@ var GameVehicles = (() => {
13726
13877
  ray.layers.enableAll();
13727
13878
  for (const pane of c.glass) {
13728
13879
  if (pane.userData.glass?.outline) continue;
13729
- const seat = this.paneSeat(pane, shell, ray);
13880
+ const local = this.shellAround(pane, shell);
13881
+ if (!local) continue;
13882
+ const seat = this.paneSeat(pane, [local], ray);
13730
13883
  if (!seat) continue;
13731
13884
  const near = c.vehicle.children.filter((o) => o.isMesh && o !== pane && !o.userData.glass && !shell.includes(o) && this.onPane(o, pane, seat));
13732
13885
  for (const m of [pane, ...near]) this.movePane(m, seat);
13733
- const past = this.shellPast(pane, shell, ray, seat.normal);
13886
+ const past = this.shellPast(pane, [local], ray, seat.normal);
13734
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
+ }
13735
13895
  }
13736
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
+ }
13737
13932
  /** The outward normal of a pane and how far the shell is at each sampled point (world). */
13738
13933
  paneSeat(pane, shell, ray) {
13739
13934
  const THREE = this.THREE;
@@ -13780,6 +13975,193 @@ var GameVehicles = (() => {
13780
13975
  }
13781
13976
  return samples.length >= 3 ? { normal, samples } : null;
13782
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
+ }
13783
14165
  /** How far the shell still stands past a seated pane, over its triangles. */
13784
14166
  shellPast(pane, shell, ray, normal) {
13785
14167
  const THREE = this.THREE;
@@ -13832,11 +14214,25 @@ var GameVehicles = (() => {
13832
14214
  g.computeBoundingSphere();
13833
14215
  mesh2.geometry = g;
13834
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
+ }
13835
14228
  /** A handle is at the free edge of its door, the end away from the hinge. */
13836
14229
  seatDoorHandles() {
13837
14230
  const THREE = this.THREE;
13838
14231
  const c = this.c;
13839
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));
13840
14236
  for (const p of this.model.parts) {
13841
14237
  if (p.role !== "door") continue;
13842
14238
  const z = this.zone(p);
@@ -13850,8 +14246,7 @@ var GameVehicles = (() => {
13850
14246
  const at = box2.getCenter(new THREE.Vector3());
13851
14247
  const isHandle = size.x > DOOR_HANDLE_MIN && size.x < DOOR_HANDLE_MAX && size.y < DOOR_HANDLE_THIN && size.z < DOOR_HANDLE_THIN;
13852
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;
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;
14249
+ if (Math.abs(at.z) < outerHalf - DOOR_HANDLE_OUTSIDE) continue;
13855
14250
  const skin = this.surface("z", z.side < 0 ? -1 : 1, B, [[free, at.y]]);
13856
14251
  o.position.x += free - at.x;
13857
14252
  if (skin != null) o.position.z += skin + (z.side < 0 ? -1 : 1) * (size.z / 2) - at.z;
@@ -13860,7 +14255,7 @@ var GameVehicles = (() => {
13860
14255
  c.root.updateMatrixWorld(true);
13861
14256
  }
13862
14257
  /** The glass of a drawn mirror sits inside its rounded housing, without corners standing past it. */
13863
- seatMirrorGlass(glass) {
14258
+ seatMirrorGlass(glass, drawn) {
13864
14259
  const g = glass.geometry.clone();
13865
14260
  g.computeBoundingBox();
13866
14261
  const mid = g.boundingBox.getCenter(new this.THREE.Vector3());
@@ -13868,6 +14263,37 @@ var GameVehicles = (() => {
13868
14263
  g.scale(MIRROR_GLASS_FIT, MIRROR_GLASS_FIT, MIRROR_GLASS_FIT);
13869
14264
  g.translate(mid.x, mid.y, mid.z);
13870
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
+ }
13871
14297
  glass.material = this.I.makeMaterial(MIRROR_GLASS_COLOR, { metalness: 0.95, roughness: 0.12 });
13872
14298
  }
13873
14299
  /** The mirror the model already draws around where the part says, if it draws one. */
@@ -13892,7 +14318,11 @@ var GameVehicles = (() => {
13892
14318
  for (const o of drawn) {
13893
14319
  if (!o.isMesh || o.material !== this.I.MATERIALS.chrome) continue;
13894
14320
  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);
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
+ }
13896
14326
  }
13897
14327
  this.extra.set(p.id, drawn);
13898
14328
  return;
@@ -13904,14 +14334,26 @@ var GameVehicles = (() => {
13904
14334
  g.name = `obj_${p.id}`;
13905
14335
  g.position.set(cx, cy, cz);
13906
14336
  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);
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);
13909
14339
  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);
14340
+ const glass = this.box(6e-3, 0.095, 0.115, mats.chrome ?? mats.dark, -0.043, 0, s * 0.0225, g);
13911
14341
  glass.userData.mirrorGlass = true;
13912
14342
  g.userData.rigid = true;
13913
14343
  this.extra.set(p.id, [g]);
13914
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
+ }
13915
14357
  makeBattery(p) {
13916
14358
  const mats = this.I.MATERIALS;
13917
14359
  const g = new this.THREE.Group();
@@ -13957,6 +14399,11 @@ var GameVehicles = (() => {
13957
14399
  if (carrier) carrier.attach(m);
13958
14400
  }
13959
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
+ }
13960
14407
  plateIdOf(o) {
13961
14408
  return o.userData.plateOf;
13962
14409
  }
@@ -14013,6 +14460,7 @@ var GameVehicles = (() => {
14013
14460
  best = g;
14014
14461
  }
14015
14462
  }
14463
+ if (p.hp > LAMINATED_HP && best.userData.glass) best.userData.glass.type = "laminated";
14016
14464
  this.kit.hitGlass(this.c, best, pt, new V3(...dir), force);
14017
14465
  return !!best.userData.glass?.broken;
14018
14466
  }
@@ -14034,6 +14482,17 @@ var GameVehicles = (() => {
14034
14482
  if (!z) return;
14035
14483
  for (const w of z.wheels) if (w.parent) this.I.flattenTire(w, k);
14036
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
+ }
14037
14496
  /** Burning rubber: the tyre sags and goes black as it melts. */
14038
14497
  tyreMelt(p, melt) {
14039
14498
  const z = this.zone(p);
@@ -14357,6 +14816,41 @@ var GameVehicles = (() => {
14357
14816
  var SHIELDED = /* @__PURE__ */ new Set(["engine", "gearbox", "frame", "radiator", "fuel_tank", "oil_pan", "battery", "alternator", "brakes", "steering", "shocks"]);
14358
14817
  var AMBIENT = 20;
14359
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"]);
14360
14854
  var FRACTURE = { glass: "shatter", plastic: "snap", rubber: "tear", steel: "none", aluminium: "none" };
14361
14855
  var add3 = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
14362
14856
  var r3 = (v) => [Math.round(v[0] * 1e5) / 1e5, Math.round(v[1] * 1e5) / 1e5, Math.round(v[2] * 1e5) / 1e5];
@@ -14368,6 +14862,8 @@ var GameVehicles = (() => {
14368
14862
  let c = vehicleCtx;
14369
14863
  const id = options.id ?? c.key;
14370
14864
  const headless = !!options.headless;
14865
+ const armourId = armourLevelOf(c.meta, options.armour);
14866
+ const armourLevel = armourId ? { ...ARMOUR_LEVELS[armourId], ...options.armourLevels?.[armourId] } : null;
14371
14867
  const seedBase = strHash(`${c.key}|${id}|${options.seed ?? 0}`);
14372
14868
  let rand = rng(seedBase);
14373
14869
  const ignitionRules = resolveIgnitionRules(options.ignition);
@@ -14425,7 +14921,7 @@ var GameVehicles = (() => {
14425
14921
  return d.ignitionC ? 1 : 0;
14426
14922
  };
14427
14923
  const setup = () => {
14428
- model = buildVehicleModel(kit, c, { hitch: options.hitch });
14924
+ model = buildVehicleModel(kit, c, { hitch: options.hitch, armour: armourLevel });
14429
14925
  ps.clear();
14430
14926
  for (const d of model.parts) {
14431
14927
  const kg = burnKg(d);
@@ -14445,6 +14941,8 @@ var GameVehicles = (() => {
14445
14941
  lost: false,
14446
14942
  status: "intact",
14447
14943
  flat: false,
14944
+ air: 1,
14945
+ airLoss: 0,
14448
14946
  fireLoss: 0
14449
14947
  });
14450
14948
  }
@@ -14545,7 +15043,8 @@ var GameVehicles = (() => {
14545
15043
  vis.breakLight(d, true);
14546
15044
  break;
14547
15045
  case "tyre":
14548
- vis.tyreFlat(d, 0.34);
15046
+ if (status === "broken") vis.tyreTornOff(d);
15047
+ else vis.tyreFlat(d, 0.34);
14549
15048
  break;
14550
15049
  case "mirror":
14551
15050
  case "bumper":
@@ -14575,7 +15074,8 @@ var GameVehicles = (() => {
14575
15074
  const damage = (p, amount, dtype, source, out, cause) => {
14576
15075
  if (p.lost || !(amount > 0)) return;
14577
15076
  const before = p.hp;
14578
- 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);
14579
15079
  out.push(ev({ type: "damage", part: p.def.id, amount: r2(before - p.hp), hp: r2(p.hp), damageType: dtype, ...source ? { source } : {} }));
14580
15080
  wear(p, amount, out);
14581
15081
  syncZone(p);
@@ -14618,11 +15118,22 @@ var GameVehicles = (() => {
14618
15118
  const p = v.clone().applyMatrix4(M);
14619
15119
  return [p.x, p.y, p.z];
14620
15120
  };
14621
- 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
+ };
14622
15133
  const resist = (d, p) => {
14623
15134
  let base = RESIST[d.material] ?? 100;
14624
15135
  if (d.material === "glass" && d.hp > 1e3) base = 120;
14625
- if (d.material === "glass" && c.meta.bullet) base *= 30;
15136
+ if (d.material === "glass" && armourLevel) base = armourLevel.glassStops;
14626
15137
  const k = ROLE_RESIST[d.role] ?? 1;
14627
15138
  const arm = d.material === "steel" || d.material === "aluminium" ? armour() : 1;
14628
15139
  return base * k * arm * (p.dents.length > 3 ? 0.9 : 1);
@@ -14645,7 +15156,7 @@ var GameVehicles = (() => {
14645
15156
  if (vis) {
14646
15157
  const pos = new V3(d.box.center[0], Math.max(0.2, (c.meta.clearance ?? 0.3) * 0.8), d.box.center[2]);
14647
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 });
14648
- 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);
14649
15160
  }
14650
15161
  }
14651
15162
  const out3 = dirToWorld(new V3(dirLocal.x * 0.5, dirLocal.y * 0.5 - 0.6, dirLocal.z * 0.5));
@@ -14682,7 +15193,7 @@ var GameVehicles = (() => {
14682
15193
  exploded = true;
14683
15194
  const d = tank.def;
14684
15195
  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 }));
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 } : {} }));
14686
15197
  tank.liters = 0;
14687
15198
  leaks.delete(d.id);
14688
15199
  if (vis) kit.explode(c, { delay: 0.6 });
@@ -14782,7 +15293,7 @@ var GameVehicles = (() => {
14782
15293
  const dir = dirToLocal(h.direction ?? [0, 0, -1]).normalize();
14783
15294
  const n = h.normal ? dirToLocal(h.normal).normalize() : normalOf(d, v);
14784
15295
  const res = resist(d, p);
14785
- const through = E >= res;
15296
+ const through = goesThrough(h, d, p, E, res);
14786
15297
  const cosInc = Math.abs(dir.dot(n));
14787
15298
  const metal = d.material === "steel" || d.material === "aluminium";
14788
15299
  const worldPoint = toWorld(v);
@@ -14793,13 +15304,21 @@ var GameVehicles = (() => {
14793
15304
  vis?.spark([v.x, v.y, v.z], [refl.x, refl.y, refl.z], 6);
14794
15305
  return;
14795
15306
  }
14796
- 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
+ };
14797
15315
  const push = (hole) => {
14798
15316
  if (!hole) return;
14799
15317
  const hp = { point: r3(fromVehicle(v, M)), normal: r3(fromVehicle(n, new THREE.Matrix4().extractRotation(M))), diameter: r2(holeDiam), through };
14800
15318
  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])))))) } : {} }));
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])))))) } : {} }));
14802
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);
14803
15322
  startLeak(p, v, cal, out, dir);
14804
15323
  };
14805
15324
  switch (d.material) {
@@ -14812,7 +15331,7 @@ var GameVehicles = (() => {
14812
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) };
14813
15332
  p.cracks.push(crack);
14814
15333
  out.push(ev({ type: "crack", part: d.id, crack, worldPoint }));
14815
- 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);
14816
15335
  } else damage(p, laminated ? E * 0.5 : p.hp, "ballistic", h.source, out, worldPoint);
14817
15336
  break;
14818
15337
  }
@@ -14820,8 +15339,8 @@ var GameVehicles = (() => {
14820
15339
  push(through);
14821
15340
  damage(p, E * 0.4, "ballistic", h.source, out, worldPoint);
14822
15341
  if (through && !p.lost && !p.flat) {
14823
- p.flat = true;
14824
- 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;
14825
15344
  }
14826
15345
  break;
14827
15346
  }
@@ -14937,7 +15456,7 @@ var GameVehicles = (() => {
14937
15456
  const electric = c.spec.fuel === "electric";
14938
15457
  const tank = tankPart();
14939
15458
  const fuel = tank ? tank.liters ?? 0 : electric ? 1 : 1;
14940
- const engineOk = sound("engine") && !exploded && fuel > 0.05;
15459
+ const engineOk = sound("engine") && !exploded && !seized && fuel > 0.05;
14941
15460
  const powered = sound("battery") && sound("alternator");
14942
15461
  const gearOk = sound("gearbox");
14943
15462
  const brakes = [...ps.values()].find((p) => p.def.role === "brakes");
@@ -15097,9 +15616,12 @@ var GameVehicles = (() => {
15097
15616
  const state = () => stateCache ?? (stateCache = buildState());
15098
15617
  const isSimulating = () => {
15099
15618
  if (leaks.size > 0) return true;
15100
- 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;
15101
15620
  return false;
15102
15621
  };
15622
+ let engineHeat = ENGINE_WORKING_C;
15623
+ let seized = false;
15624
+ let steaming = false;
15103
15625
  const surfaceRays = new THREE.Raycaster();
15104
15626
  surfaceRays.layers.enableAll();
15105
15627
  surfaceRays.camera = new THREE.PerspectiveCamera();
@@ -15123,14 +15645,24 @@ var GameVehicles = (() => {
15123
15645
  if (pane) return ps.get(pane) ?? null;
15124
15646
  for (let q = o; q && q !== c.vehicle; q = q.parent) {
15125
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;
15126
15650
  }
15127
15651
  return locate(v, 0.08, (p) => p.def.role === "glass");
15128
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
+ };
15129
15659
  const surfaceRay = (ray) => {
15130
15660
  sync();
15131
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]));
15132
15662
  surfaceRays.far = ray.maxDistance;
15133
- for (const h of surfaceRays.intersectObject(c.vehicle, true)) {
15663
+ surfaceMeshes.length = 0;
15664
+ for (const child of c.vehicle.children) collectSurfaces(child);
15665
+ for (const h of surfaceRays.intersectObjects(surfaceMeshes, false)) {
15134
15666
  if (!h.face || !isDrawnSurface(h.object)) continue;
15135
15667
  const p = partOfSurface(h.object, c.vehicle.worldToLocal(h.point.clone()));
15136
15668
  if (!p || p.lost || ray.ignored?.(p.def.id)) continue;
@@ -15164,7 +15696,29 @@ var GameVehicles = (() => {
15164
15696
  applyEffects();
15165
15697
  return out;
15166
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
+ };
15167
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);
15168
15722
  clock += dt;
15169
15723
  const out = queue;
15170
15724
  queue = [];
@@ -15280,6 +15834,70 @@ var GameVehicles = (() => {
15280
15834
  partName: (pid) => ps.get(pid)?.def.name ?? pid,
15281
15835
  status: computeStatus,
15282
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
+ },
15283
15901
  partWorldCenter: (pid) => {
15284
15902
  sync();
15285
15903
  const d = ps.get(pid)?.def;
@@ -15363,6 +15981,34 @@ var GameVehicles = (() => {
15363
15981
  };
15364
15982
  }
15365
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
+
15366
16012
  // src/destructible/enclosure.ts
15367
16013
  var CABIN_LEAK = 0.02;
15368
16014
  var HOLE_RATE_PER_AREA = 3e3;