@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.
package/dist/index.js CHANGED
@@ -3188,6 +3188,7 @@ function installInterior(kit) {
3188
3188
  }
3189
3189
 
3190
3190
  // src/hood.ts
3191
+ var HOOD_LIFT = 8e-3;
3191
3192
  function installHood(kit) {
3192
3193
  const THREE = kit.THREE;
3193
3194
  const V3 = THREE.Vector3;
@@ -3210,7 +3211,7 @@ function installHood(kit) {
3210
3211
  for (let i = 0; i < p.count; i++) {
3211
3212
  p.setX(i, cx + (p.getX(i) - cx) * sx);
3212
3213
  p.setZ(i, p.getZ(i) * sz);
3213
- p.setY(i, p.getY(i) + 8e-3);
3214
+ p.setY(i, p.getY(i) + HOOD_LIFT);
3214
3215
  }
3215
3216
  geo.translate(-hx, -hy, 0);
3216
3217
  const under = geo.clone();
@@ -3239,7 +3240,8 @@ function installHood(kit) {
3239
3240
  yMin: bb.min.y,
3240
3241
  zw: bb.max.z,
3241
3242
  label: o.label,
3242
- noLatch: !!o.noLatch
3243
+ noLatch: !!o.noLatch,
3244
+ lift: HOOD_LIFT
3243
3245
  };
3244
3246
  c.part("hood", { key: "hood" }, piv, new V3((hx + bb.max.x) / 2, hy, 0), 0.9);
3245
3247
  return c.hood;
@@ -4675,6 +4677,14 @@ function installParticles(kit) {
4675
4677
  }
4676
4678
 
4677
4679
  // src/fluids.ts
4680
+ var PUDDLE_COLORS = {
4681
+ coolant: 3123279,
4682
+ brake_fluid: 12093738,
4683
+ transmission_fluid: 10099746,
4684
+ power_steering_fluid: 9054768,
4685
+ washer_fluid: 2977744,
4686
+ battery_acid: 14211272
4687
+ };
4678
4688
  function installFluids(kit) {
4679
4689
  const THREE = kit.THREE;
4680
4690
  const V3 = THREE.Vector3;
@@ -4894,6 +4904,13 @@ function installFluids(kit) {
4894
4904
  transparent: true,
4895
4905
  opacity: 0.9,
4896
4906
  depthWrite: false
4907
+ }) : PUDDLE_COLORS[kind] ? new THREE.MeshStandardMaterial({
4908
+ color: PUDDLE_COLORS[kind],
4909
+ roughness: 0.05,
4910
+ metalness: 0.1,
4911
+ transparent: true,
4912
+ opacity: 0.7,
4913
+ depthWrite: false
4897
4914
  }) : new THREE.MeshStandardMaterial({
4898
4915
  color: 0,
4899
4916
  roughness: 0.03,
@@ -5739,12 +5756,36 @@ function installImpact(kit) {
5739
5756
  if (g.userData.crease && c.smoothLater) c.smoothLater(msh);
5740
5757
  else if (g.userData.crease) crease(g, 42);
5741
5758
  else g.computeVertexNormals();
5742
- g.computeBoundingBox();
5743
5759
  const sphere = g.boundingSphere ?? (g.boundingSphere = new THREE.Sphere());
5744
- g.boundingBox.getCenter(sphere.center);
5745
- let far2 = 0;
5746
- for (let i = 0; i < pos.count; i++) far2 = Math.max(far2, sphere.center.distanceToSquared(v.fromBufferAttribute(pos, i)));
5747
- sphere.radius = Math.sqrt(far2);
5760
+ if (pos.isInterleavedBufferAttribute || pos.normalized || Object.keys(g.morphAttributes).length > 0) {
5761
+ g.computeBoundingBox();
5762
+ g.boundingBox.getCenter(sphere.center);
5763
+ let far2 = 0;
5764
+ for (let i = 0; i < pos.count; i++) far2 = Math.max(far2, sphere.center.distanceToSquared(v.fromBufferAttribute(pos, i)));
5765
+ sphere.radius = Math.sqrt(far2);
5766
+ } else {
5767
+ const box2 = g.boundingBox ?? (g.boundingBox = new THREE.Box3());
5768
+ let x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
5769
+ for (let i = 0; i < pos.count * 3; i += 3) {
5770
+ const x = arr[i], y = arr[i + 1], z = arr[i + 2];
5771
+ if (x < x0) x0 = x;
5772
+ if (y < y0) y0 = y;
5773
+ if (z < z0) z0 = z;
5774
+ if (x > x1) x1 = x;
5775
+ if (y > y1) y1 = y;
5776
+ if (z > z1) z1 = z;
5777
+ }
5778
+ box2.min.set(x0, y0, z0);
5779
+ box2.max.set(x1, y1, z1);
5780
+ box2.getCenter(sphere.center);
5781
+ const cx = sphere.center.x, cy = sphere.center.y, cz = sphere.center.z;
5782
+ let far2 = 0;
5783
+ for (let i = 0; i < pos.count * 3; i += 3) {
5784
+ const dx = cx - arr[i], dy = cy - arr[i + 1], dz = cz - arr[i + 2];
5785
+ far2 = Math.max(far2, dx * dx + dy * dy + dz * dz);
5786
+ }
5787
+ sphere.radius = Math.sqrt(far2);
5788
+ }
5748
5789
  }
5749
5790
  });
5750
5791
  const dset = new Set(c.deform);
@@ -6148,7 +6189,8 @@ function installExplosion(kit) {
6148
6189
  const G = gm2.userData.glass;
6149
6190
  if (!G || G.broken || !gm2.visible || !gm2.parent) return;
6150
6191
  if (G.type === "laminated") {
6151
- G.type = "tempered";
6192
+ for (let k2 = 0; k2 < 3; k2++) I.crackGlass(c, gm2, I.glassPt ? I.glassPt(c, gm2, c.rand) : gm2.position.clone(), 1.2);
6193
+ return;
6152
6194
  }
6153
6195
  I.shatterGlass(c, gm2, 1.4, new V3(0, 1, 0));
6154
6196
  });
@@ -6159,19 +6201,6 @@ function installExplosion(kit) {
6159
6201
  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)));
6160
6202
  c.hood = null;
6161
6203
  }
6162
- const W = m.width || 1.6;
6163
- const gh = m.glasshouse;
6164
- if (m.kind === "car" && gh) {
6165
- ;
6166
- [1, -1].forEach(function(sd) {
6167
- const dh = Math.max(0.35, (m.belt || 1) - (m.clearance || 0.3) - 0.08);
6168
- const dx = gh.fb - 0.55;
6169
- 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));
6170
- 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));
6171
- hole.userData.noFollow = true;
6172
- 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)));
6173
- });
6174
- }
6175
6204
  if (m.rearBumper && m.rearBumper.parent)
6176
6205
  fling(
6177
6206
  c,
@@ -6179,20 +6208,6 @@ function installExplosion(kit) {
6179
6208
  c.dirToEffects(new V3(rv(-5, -3), rv(4, 6), rv(-1, 1))),
6180
6209
  new V3(rv(-3, 3), rv(-6, 6), rv(-3, 3))
6181
6210
  );
6182
- if (c.wheels.length) {
6183
- const wi = Math.floor(c.rand() * c.wheels.length);
6184
- const wh = c.wheels[wi];
6185
- c.wheels.splice(wi, 1);
6186
- c.parts.forEach(function(p) {
6187
- if (p.obj === wh) p.done = true;
6188
- });
6189
- fling(
6190
- c,
6191
- wh,
6192
- c.dirToEffects(new V3(rv(-2, 2), rv(5, 7.5), (wh.position.z >= 0 ? 1 : -1) * rv(3, 5))),
6193
- new V3(rv(-8, 8), rv(-3, 3), rv(-8, 8))
6194
- );
6195
- }
6196
6211
  burnAll(c);
6197
6212
  c.emitters = c.emitters.filter(function(e) {
6198
6213
  return e.kind === "exhaust" ? false : true;
@@ -10987,6 +11002,17 @@ function createVehicleKit(options) {
10987
11002
  return kit;
10988
11003
  }
10989
11004
 
11005
+ // src/destructible/armour.ts
11006
+ var ARMOUR_LEVELS = {
11007
+ IIIA: { glassStops: 1700, glassHp: 6e3, glassTakes: 0.25, steel: 8 },
11008
+ III: { glassStops: 3600, glassHp: 9e3, glassTakes: 0.25, steel: 30 }
11009
+ };
11010
+ function armourLevelOf(meta, option) {
11011
+ if (option !== void 0) return option;
11012
+ if (meta.armor === "IIIA" || meta.armor === "III") return meta.armor;
11013
+ return meta.armored || meta.bullet ? "III" : null;
11014
+ }
11015
+
10990
11016
  // src/destructible/impl.ts
10991
11017
  import { ignitionChance, resolveIgnitionRules, sparkTargetFactor } from "@adatechnology/game-contracts";
10992
11018
 
@@ -11033,6 +11059,12 @@ var boxOf = (z) => ({
11033
11059
  center: [(z.box.x0 + z.box.x1) / 2, (z.box.y0 + z.box.y1) / 2, (z.box.z0 + z.box.z1) / 2],
11034
11060
  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)]
11035
11061
  });
11062
+ var PART_FLUIDS = {
11063
+ gearbox: { fluid: "transmission_fluid", liters: 6 },
11064
+ brakes: { fluid: "brake_fluid", liters: 0.6 },
11065
+ steering: { fluid: "power_steering_fluid", liters: 1 },
11066
+ battery: { fluid: "battery_acid", liters: 3 }
11067
+ };
11036
11068
  var skinOf = (kind) => kind === "plane" || kind === "heli" || kind === "boat" ? "aluminium" : "steel";
11037
11069
  var isFender = (id) => /^fender/.test(id);
11038
11070
  var roleOfZone = (z) => {
@@ -11090,7 +11122,8 @@ function buildVehicleModel(kit, c, options = {}) {
11090
11122
  const spec = c.spec;
11091
11123
  const t = kit.t;
11092
11124
  const skin = skinOf(kind);
11093
- const armour = m.armored || m.bullet ? 12 : 1;
11125
+ const armoured = options.armour ?? null;
11126
+ const armour = armoured ? 12 : 1;
11094
11127
  const mass = vehicleMass(c);
11095
11128
  const W = m.width ?? 1.7;
11096
11129
  const F = m.front ?? 2;
@@ -11156,8 +11189,8 @@ function buildVehicleModel(kit, c, options = {}) {
11156
11189
  }
11157
11190
  if (role === "glass") {
11158
11191
  const glass = (z.glass ?? [])[0];
11159
- const laminated = glass?.userData?.glass?.type === "laminated" || m.bullet;
11160
- add({ id, role, name: label, material: "glass", box, mass: 10 * area, hp: laminated ? 4e3 : 450, zoneId: id, visual: false, tags: ["glass", ...tags] });
11192
+ const laminated = glass?.userData?.glass?.type === "laminated" || !!armoured;
11193
+ add({ id, role, name: label, material: "glass", box, mass: 10 * area, hp: armoured ? armoured.glassHp : laminated ? 4e3 : 450, zoneId: id, visual: false, tags: ["glass", ...tags] });
11161
11194
  continue;
11162
11195
  }
11163
11196
  if (role === "light") {
@@ -11195,7 +11228,7 @@ function buildVehicleModel(kit, c, options = {}) {
11195
11228
  zoneId: id,
11196
11229
  visual: false,
11197
11230
  contains: { fluid: diesel ? "diesel" : "fuel", liters },
11198
- explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320 },
11231
+ explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320, effect: "car_fuel_tank" },
11199
11232
  ignitionC: diesel ? 330 : 260,
11200
11233
  tags: ["fuel_tank", ...tags]
11201
11234
  });
@@ -11231,8 +11264,8 @@ function buildVehicleModel(kit, c, options = {}) {
11231
11264
  if (claimed.has(pane)) continue;
11232
11265
  const G = pane.userData.glass;
11233
11266
  const bb = paneBox(c, pane, inverse);
11234
- const laminated = G?.type === "laminated" || m.bullet;
11235
- 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"] });
11267
+ const laminated = G?.type === "laminated" || !!armoured;
11268
+ 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"] });
11236
11269
  }
11237
11270
  if (!hubZone) {
11238
11271
  const h = Math.max(0.15, belt - clearance);
@@ -11288,7 +11321,7 @@ function buildVehicleModel(kit, c, options = {}) {
11288
11321
  visual: false,
11289
11322
  mechanical: true,
11290
11323
  contains: { fluid: diesel ? "diesel" : "fuel", liters },
11291
- explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320 },
11324
+ explodes: diesel ? void 0 : { energy: 12e5 * (liters / 55), radius: 5 + liters / 30, atC: 320, effect: "car_fuel_tank" },
11292
11325
  ignitionC: diesel ? 330 : 260,
11293
11326
  tags: ["fuel_tank"]
11294
11327
  });
@@ -11565,38 +11598,121 @@ function buildVehicleModel(kit, c, options = {}) {
11565
11598
  }
11566
11599
  }
11567
11600
  void has;
11601
+ for (const part of parts) {
11602
+ const fluid = PART_FLUIDS[part.role];
11603
+ if (fluid && !part.contains && !(kind === "bike" && part.role === "steering")) part.contains = { ...fluid };
11604
+ }
11568
11605
  return { parts, joints, hub: hubId, hitch };
11569
11606
  }
11570
11607
 
11608
+ // src/destructible/subdivide-near.ts
11609
+ var SUBDIVIDE_DEPTH = 12;
11610
+ function subdivideNear(THREE, params) {
11611
+ const { mesh, maxLen, near } = params;
11612
+ const src = mesh.geometry.index ? mesh.geometry.toNonIndexed() : mesh.geometry;
11613
+ const a = src.attributes.position.array;
11614
+ const toWorld = mesh.matrixWorld;
11615
+ const out = [];
11616
+ const mid = (p, q) => [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2, (p[2] + q[2]) / 2];
11617
+ const split = (A, B, C, depth) => {
11618
+ const ab = Math.hypot(A[0] - B[0], A[1] - B[1], A[2] - B[2]);
11619
+ const bc = Math.hypot(B[0] - C[0], B[1] - C[1], B[2] - C[2]);
11620
+ const ca = Math.hypot(C[0] - A[0], C[1] - A[1], C[2] - A[2]);
11621
+ const longest = Math.max(ab, bc, ca);
11622
+ if (longest <= maxLen || depth > SUBDIVIDE_DEPTH) {
11623
+ out.push(...A, ...B, ...C);
11624
+ return;
11625
+ }
11626
+ if (longest === ab) {
11627
+ const D = mid(A, B);
11628
+ split(A, D, C, depth + 1);
11629
+ split(D, B, C, depth + 1);
11630
+ } else if (longest === bc) {
11631
+ const E = mid(B, C);
11632
+ split(A, B, E, depth + 1);
11633
+ split(A, E, C, depth + 1);
11634
+ } else {
11635
+ const F = mid(C, A);
11636
+ split(A, B, F, depth + 1);
11637
+ split(F, B, C, depth + 1);
11638
+ }
11639
+ };
11640
+ for (let i = 0; i < a.length; i += 9) {
11641
+ const A = [a[i], a[i + 1], a[i + 2]];
11642
+ const B = [a[i + 3], a[i + 4], a[i + 5]];
11643
+ const C = [a[i + 6], a[i + 7], a[i + 8]];
11644
+ const corners = [A, B, C].map((p) => new THREE.Vector3(p[0], p[1], p[2]).applyMatrix4(toWorld));
11645
+ if (near(corners)) split(A, B, C, 0);
11646
+ else out.push(...A, ...B, ...C);
11647
+ }
11648
+ const g = new THREE.BufferGeometry();
11649
+ g.setAttribute("position", new THREE.Float32BufferAttribute(out, 3));
11650
+ mesh.geometry.dispose();
11651
+ mesh.geometry = g;
11652
+ }
11653
+
11571
11654
  // src/destructible/visual.ts
11572
11655
  var HOLE_DECAL_SCALE = 2.4;
11573
11656
  var DOOR_DEPTH = 0.07;
11574
- var DOOR_EDGE_PIECES = 16;
11657
+ var DOOR_EDGE_PIECES = 8;
11658
+ var DOOR_EDGE_REACH = 0.08;
11575
11659
  var DOOR_CARD_COLOR = "#3b3e42";
11576
11660
  var DOOR_LINER_COLOR = "#5a5e64";
11577
11661
  var DOOR_LINER_DARK = 0.15;
11578
- var DOOR_STRIP_THIN = 0.05;
11662
+ var DOOR_STRIP_THIN = 0.08;
11579
11663
  var DOOR_STRIP_NEAR = 0.04;
11664
+ var DOOR_STRIP_TALL = 0.22;
11665
+ var STRIP_SMOOTH_DEG = 180;
11666
+ var DOOR_STRIP_VERTICES = 2e4;
11667
+ var DOOR_SILL = 0.25;
11580
11668
  var DOOR_STRIP_OVER = 0.3;
11581
11669
  var DOOR_TOP_OVER_PANE = 0.03;
11582
- var DOOR_STRIP_SUB = 0.02;
11583
11670
  var HOLE_TEXTURE_SIZE = 64;
11671
+ var LAMINATED_HP = 1e3;
11672
+ var HOOD_SINK = 2e-3;
11673
+ var HOOD_GAP_CLOSED = 0.016;
11584
11674
  var DOOR_HANDLE_INSET = 0.12;
11585
11675
  var DOOR_HANDLE_MIN = 0.07;
11586
11676
  var DOOR_HANDLE_MAX = 0.24;
11587
11677
  var DOOR_HANDLE_THIN = 0.07;
11588
- var DOOR_HANDLE_OUTSIDE = 0.1;
11678
+ var DOOR_HANDLE_OUTSIDE = 0.15;
11589
11679
  var PANE_SEAT_OUT = 0.05;
11590
11680
  var PANE_SEAT_IN = -0.05;
11591
11681
  var PANE_SEAT_REACH = 0.14;
11592
11682
  var PANE_SEAT_SAMPLES = 24;
11593
11683
  var PANE_SEAT_LIFT = 4e-3;
11594
11684
  var PANE_FRAME_NEAR = 0.07;
11685
+ var PANE_MIN_SLOPE = 0.01;
11686
+ var PANE_EDGE_BAND = 0.03;
11687
+ var PANE_REACH_STEP = 0.01;
11688
+ var PANE_REACH_MAX = 0.25;
11689
+ var COWL_UNDER_GLASS = 0;
11690
+ var COWL_NEAR_PANE = 0.08;
11691
+ var COWL_PLATE = 0.06;
11692
+ var COWL_DASH = 0.5;
11693
+ var COWL_DEEP = 0.3;
11694
+ var WINDOW_FRAME_NEAR = 0.06;
11695
+ var WINDOW_FRAME_SPAN = 0.6;
11696
+ var DASHBOARD_COLOR = "#121314";
11697
+ var COWL_CUT_SUB = 0.1;
11698
+ var COWL_CUT_MARGIN = 0.08;
11699
+ var SKIN_UNDER_PANE = 0.025;
11700
+ var SKIN_EDGE_KEPT = 0.06;
11701
+ var SIDE_PANE_FACING = 0.6;
11702
+ var COWL_LONG = 0.8;
11703
+ var PANE_BAR_VERTICES = 48;
11704
+ var PANE_BAR_LENGTH = 0.3;
11705
+ var PANE_BORDER = 0.03;
11706
+ var PANE_BORDER_LIFT = 3e-3;
11595
11707
  var DRAWN_MIRROR_SIZE = 0.3;
11596
11708
  var DRAWN_MIRROR_REACH = 0.4;
11597
11709
  var DRAWN_MIRROR_MOUNT = 0.2;
11598
11710
  var MIRROR_GLASS_FIT = 0.76;
11711
+ var MIRROR_GLASS_PROUD = 1e-3;
11712
+ var MIRROR_FACE_DEPTH = 0.012;
11599
11713
  var MIRROR_GLASS_THIN = 0.04;
11714
+ var DEFAULT_WHEEL_RADIUS = 0.3;
11715
+ var SIDEWALL_SHARE = 0.3;
11600
11716
  var MIRROR_GLASS_MIN = 0.05;
11601
11717
  var MIRROR_GLASS_COLOR = "#59636a";
11602
11718
  var HOLE_RIM = { steel: "#b7bcc0", aluminium: "#d3d6d8", plastic: "#d9d6cc", rubber: "#141414", glass: "#eef3f5" };
@@ -11683,6 +11799,7 @@ var VehicleVisuals = class {
11683
11799
  this.hood = this.nameHood();
11684
11800
  this.seatPanes();
11685
11801
  this.seatDoorHandles();
11802
+ this.seatHood();
11686
11803
  this.warming = c.deform.filter((msh) => msh.geometry && msh.geometry.attributes.position && msh.geometry.attributes.position.count >= 1500);
11687
11804
  if (!this.warming.length) this.warming = null;
11688
11805
  for (const p of model.parts) {
@@ -11883,7 +12000,8 @@ var VehicleVisuals = class {
11883
12000
  const panes = I.windowsOver(this.c, z, inv);
11884
12001
  const seals = panes.map((g) => I.paneSeals.get(g)).filter((s) => !!s);
11885
12002
  const mirrors = this.mirrorsOn(z);
11886
- objs = [...pcs, ...this.doorCards(pcs, z), ...I.followersIn(this.c, z, inv), ...panes, ...seals, ...mirrors];
12003
+ const frames = this.framesAround(panes, inv);
12004
+ objs = [...pcs, ...I.followersIn(this.c, z, inv), ...panes, ...seals, ...mirrors, ...frames];
11887
12005
  }
11888
12006
  } else {
11889
12007
  const pcs = this.cutZone(z);
@@ -11928,7 +12046,10 @@ var VehicleVisuals = class {
11928
12046
  run(x1, y0, x1, y1);
11929
12047
  run(x1, y1, x0, y1);
11930
12048
  run(x0, y1, x0, y0);
11931
- const heights = loop.map(([x, y]) => this.surface("z", side2, B, [[x, y]]) ?? flat);
12049
+ const heights = loop.map(([x, y]) => {
12050
+ const at = this.surface("z", side2, B, [[x, y]]);
12051
+ return at != null && Math.abs(at - flat) < DOOR_EDGE_REACH ? at : flat;
12052
+ });
11932
12053
  const band = (lift) => {
11933
12054
  const position = [];
11934
12055
  const index = [];
@@ -11946,6 +12067,23 @@ var VehicleVisuals = class {
11946
12067
  };
11947
12068
  const paint = this.paint().clone();
11948
12069
  paint.side = THREE.DoubleSide;
12070
+ const cardPosition = [];
12071
+ 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);
12072
+ const cardZ = (z2) => z2 - side2 * DOOR_DEPTH;
12073
+ loop.forEach(([x, y], i) => {
12074
+ const j = (i + 1) % loop.length;
12075
+ cardPosition.push(mid[0], mid[1], cardZ(mid[2]), x, y, cardZ(heights[i]), loop[j][0], loop[j][1], cardZ(heights[j]));
12076
+ });
12077
+ const cardGeometry = new THREE.BufferGeometry();
12078
+ cardGeometry.setAttribute("position", new THREE.Float32BufferAttribute(cardPosition, 3));
12079
+ cardGeometry.computeVertexNormals();
12080
+ const cardMaterial = this.I.makeMaterial(DOOR_CARD_COLOR, { roughness: 0.85 }).clone();
12081
+ cardMaterial.side = THREE.DoubleSide;
12082
+ const card = new THREE.Mesh(cardGeometry, cardMaterial);
12083
+ card.userData.noFollow = true;
12084
+ card.castShadow = false;
12085
+ this.c.vehicle.add(card);
12086
+ piv.attach(card);
11949
12087
  const edges = new THREE.Mesh(band(2e-3), paint);
11950
12088
  const lining = new THREE.Mesh(band(2e-3), paint);
11951
12089
  for (const m of [edges, lining]) {
@@ -11955,6 +12093,17 @@ var VehicleVisuals = class {
11955
12093
  }
11956
12094
  piv.attach(edges);
11957
12095
  }
12096
+ /** 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. */
12097
+ framesAround(panes, inv) {
12098
+ const THREE = this.THREE;
12099
+ const boxes = panes.map((g) => this.I.localBox(this.c, g, inv).expandByScalar(WINDOW_FRAME_NEAR));
12100
+ return this.c.vehicle.children.filter((o) => {
12101
+ if (!o.isMesh || !o.visible || o.userData.glass || o.userData.decal || this.c.deform?.includes(o)) return false;
12102
+ const box = this.I.localBox(this.c, o, inv);
12103
+ if (box.isEmpty()) return false;
12104
+ return boxes.some((b) => b.containsBox(box) && box.getSize(new THREE.Vector3()).x > b.getSize(new THREE.Vector3()).x * WINDOW_FRAME_SPAN);
12105
+ });
12106
+ }
11958
12107
  /** A front door carries the mirror drawn at its corner. */
11959
12108
  mirrorsOn(z) {
11960
12109
  if (!z.mirror) return [];
@@ -11980,45 +12129,42 @@ var VehicleVisuals = class {
11980
12129
  const B = z.box;
11981
12130
  const x0 = B.x0 + 0.03;
11982
12131
  const x1 = B.x1 - 0.03;
11983
- const y0 = B.y0 - DOOR_STRIP_NEAR;
11984
- const y1 = B.y1 + DOOR_STRIP_NEAR;
12132
+ const y0 = B.y0 + DOOR_SILL;
12133
+ const glassTop = (I) => {
12134
+ let top = -Infinity;
12135
+ for (const g of I.windowsOver(this.c, z, inv)) top = Math.max(top, I.localBox(this.c, g, inv).max.y);
12136
+ return top;
12137
+ };
12138
+ const y1 = Math.max(B.y1, glassTop(this.I)) + DOOR_STRIP_NEAR;
11985
12139
  const deform = this.c.deform ?? [];
11986
12140
  for (const o of [...this.c.vehicle.children]) {
11987
- if (!o.isMesh || o === piv || deform.includes(o) || o.userData.glass || o.userData.decal || !o.visible) continue;
12141
+ 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;
11988
12142
  const box = this.I.localBox(this.c, o, inv);
11989
12143
  const size = box.getSize(new THREE.Vector3());
11990
12144
  const mid = box.getCenter(new THREE.Vector3());
11991
- if (Math.sign(mid.z) !== Math.sign(z.side) || size.z > DOOR_STRIP_THIN || size.y > DOOR_STRIP_THIN || Math.min(box.max.x, x1) - Math.max(box.min.x, x0) < (x1 - x0) * DOOR_STRIP_OVER || mid.y < y0 || mid.y > y1) continue;
12145
+ if (Math.sign(mid.z) !== Math.sign(z.side) || size.z > DOOR_STRIP_THIN || size.y > DOOR_STRIP_TALL || Math.min(box.max.x, x1) - Math.max(box.min.x, x0) < (x1 - x0) * DOOR_STRIP_OVER || mid.y < y0 || mid.y > y1) continue;
11992
12146
  const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld);
11993
12147
  const v = new THREE.Vector3();
11994
12148
  const sdf = (x, y, zz) => {
11995
12149
  v.set(x, y, zz).applyMatrix4(toVehicle);
11996
12150
  return Math.max(x0 - v.x, v.x - x1);
11997
12151
  };
11998
- this.kit.cutMesh(o, [{ sdf }], 42, { sub: DOOR_STRIP_SUB });
11999
- const from = Math.max(box.min.x, x0);
12000
- const to = Math.min(box.max.x, x1);
12001
- const piece = this.box(to - from, size.y, size.z, o.material, (from + to) / 2, mid.y, mid.z);
12002
- piece.userData.noFollow = true;
12003
- piv.attach(piece);
12004
- }
12005
- }
12006
- /** 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. */
12007
- doorCards(skins, z) {
12008
- const cardMaterial = this.I.makeMaterial(DOOR_CARD_COLOR, { roughness: 0.8 });
12009
- return skins.map((skin) => {
12010
- const card = new this.THREE.Mesh(skin.geometry, cardMaterial);
12011
- card.position.copy(skin.position);
12012
- card.quaternion.copy(skin.quaternion);
12013
- card.scale.copy(skin.scale);
12014
- const side2 = z.side < 0 ? -1 : 1;
12015
- const inward = -side2 * DOOR_DEPTH;
12016
- card.position.z += inward;
12017
- card.userData.noFollow = true;
12018
- card.castShadow = false;
12019
- this.c.vehicle.add(card);
12020
- return card;
12021
- });
12152
+ const over = new THREE.Mesh(o.geometry.clone(), o.material);
12153
+ over.position.copy(o.position);
12154
+ over.quaternion.copy(o.quaternion);
12155
+ over.scale.copy(o.scale);
12156
+ this.c.vehicle.add(over);
12157
+ over.updateMatrixWorld(true);
12158
+ const overToVehicle = new THREE.Matrix4().multiplyMatrices(inv, over.matrixWorld);
12159
+ const w = new THREE.Vector3();
12160
+ this.kit.cutMesh(over, [{ sdf: (x, y, zz) => {
12161
+ w.set(x, y, zz).applyMatrix4(overToVehicle);
12162
+ return -Math.max(x0 - w.x, w.x - x1);
12163
+ } }], STRIP_SMOOTH_DEG, {});
12164
+ this.kit.cutMesh(o, [{ sdf }], STRIP_SMOOTH_DEG, {});
12165
+ over.userData.noFollow = true;
12166
+ piv.attach(over);
12167
+ }
12022
12168
  }
12023
12169
  mend(dirty) {
12024
12170
  dirty.forEach((msh) => {
@@ -12043,14 +12189,58 @@ var VehicleVisuals = class {
12043
12189
  ray.layers.enableAll();
12044
12190
  for (const pane of c.glass) {
12045
12191
  if (pane.userData.glass?.outline) continue;
12046
- const seat = this.paneSeat(pane, shell, ray);
12192
+ const local = this.shellAround(pane, shell);
12193
+ if (!local) continue;
12194
+ const seat = this.paneSeat(pane, [local], ray);
12047
12195
  if (!seat) continue;
12048
12196
  const near = c.vehicle.children.filter((o) => o.isMesh && o !== pane && !o.userData.glass && !shell.includes(o) && this.onPane(o, pane, seat));
12049
12197
  for (const m of [pane, ...near]) this.movePane(m, seat);
12050
- const past = this.shellPast(pane, shell, ray, seat.normal);
12198
+ const past = this.shellPast(pane, [local], ray, seat.normal);
12051
12199
  if (past > 0) for (const m of [pane, ...near]) this.movePane(m, { normal: seat.normal, samples: [{ at: seat.samples[0].at, by: past }] });
12200
+ this.reachShellBelow(pane, local, ray, seat.normal);
12201
+ this.paneBorder(pane, near, seat);
12202
+ this.keepUnderPane(pane, seat.normal);
12203
+ const across = new this.THREE.Vector3(0, 0, 1).transformDirection(this.c.vehicle.matrixWorld);
12204
+ if (Math.abs(seat.normal.dot(across)) < SIDE_PANE_FACING) {
12205
+ for (const body of shell) if (body.parent === this.c.vehicle) this.cutUnderPane(body, pane, seat.normal, SKIN_UNDER_PANE, SKIN_EDGE_KEPT);
12206
+ }
12052
12207
  }
12053
12208
  }
12209
+ /** 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. */
12210
+ shellAround(pane, shell) {
12211
+ const THREE = this.THREE;
12212
+ const box = new THREE.Box3().setFromObject(pane).expandByScalar(PANE_SEAT_REACH + PANE_SEAT_OUT);
12213
+ const out = [];
12214
+ const a = new THREE.Vector3();
12215
+ const b = new THREE.Vector3();
12216
+ const d = new THREE.Vector3();
12217
+ const tri = new THREE.Box3();
12218
+ for (const mesh of shell) {
12219
+ const pos = mesh.geometry.attributes.position;
12220
+ const idx = mesh.geometry.index;
12221
+ const count = idx ? idx.count : pos.count;
12222
+ const e = mesh.matrixWorld.elements;
12223
+ const world = (v, i) => {
12224
+ const x = pos.getX(i);
12225
+ const y = pos.getY(i);
12226
+ const z = pos.getZ(i);
12227
+ 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]);
12228
+ };
12229
+ for (let f = 0; f + 2 < count; f += 3) {
12230
+ world(a, idx ? idx.getX(f) : f);
12231
+ world(b, idx ? idx.getX(f + 1) : f + 1);
12232
+ world(d, idx ? idx.getX(f + 2) : f + 2);
12233
+ tri.makeEmpty().expandByPoint(a).expandByPoint(b).expandByPoint(d);
12234
+ if (tri.intersectsBox(box)) out.push(a.x, a.y, a.z, b.x, b.y, b.z, d.x, d.y, d.z);
12235
+ }
12236
+ }
12237
+ if (!out.length) return null;
12238
+ const g = new THREE.BufferGeometry();
12239
+ g.setAttribute("position", new THREE.Float32BufferAttribute(out, 3));
12240
+ const proxy = new THREE.Mesh(g, new THREE.MeshBasicMaterial({ side: THREE.DoubleSide }));
12241
+ proxy.updateMatrixWorld(true);
12242
+ return proxy;
12243
+ }
12054
12244
  /** The outward normal of a pane and how far the shell is at each sampled point (world). */
12055
12245
  paneSeat(pane, shell, ray) {
12056
12246
  const THREE = this.THREE;
@@ -12097,6 +12287,193 @@ var VehicleVisuals = class {
12097
12287
  }
12098
12288
  return samples.length >= 3 ? { normal, samples } : null;
12099
12289
  }
12290
+ /** 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. */
12291
+ reachShellBelow(pane, shell, ray, normal) {
12292
+ const THREE = this.THREE;
12293
+ const down = new THREE.Vector3(0, -1, 0).applyQuaternion(this.c.root.getWorldQuaternion(new THREE.Quaternion()));
12294
+ down.addScaledVector(normal, -down.dot(normal));
12295
+ if (down.lengthSq() < PANE_MIN_SLOPE) return;
12296
+ down.normalize();
12297
+ const g = pane.geometry;
12298
+ const pos = g.attributes.position;
12299
+ const v = new THREE.Vector3();
12300
+ let lowest = -Infinity;
12301
+ for (let i = 0; i < pos.count; i++) lowest = Math.max(lowest, v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld).dot(down));
12302
+ const edge = [];
12303
+ const mid = new THREE.Vector3();
12304
+ for (let i = 0; i < pos.count; i++) {
12305
+ v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
12306
+ if (v.dot(down) > lowest - PANE_EDGE_BAND) {
12307
+ edge.push(i);
12308
+ mid.add(v);
12309
+ }
12310
+ }
12311
+ if (!edge.length) return;
12312
+ mid.divideScalar(edge.length);
12313
+ let reach = 0;
12314
+ for (let d = PANE_REACH_STEP; d <= PANE_REACH_MAX; d += PANE_REACH_STEP) {
12315
+ const at = mid.clone().addScaledVector(down, d).addScaledVector(normal, PANE_SEAT_REACH);
12316
+ ray.set(at, normal.clone().negate());
12317
+ ray.far = PANE_SEAT_REACH * 2;
12318
+ if (ray.intersectObject(shell, false)[0]) {
12319
+ reach = d + PANE_REACH_STEP;
12320
+ break;
12321
+ }
12322
+ }
12323
+ if (!reach) return;
12324
+ const inverse = pane.matrixWorld.clone().invert();
12325
+ for (const i of edge) {
12326
+ v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld).addScaledVector(down, reach).applyMatrix4(inverse);
12327
+ pos.setXYZ(i, v.x, v.y, v.z);
12328
+ }
12329
+ pos.needsUpdate = true;
12330
+ g.computeBoundingBox();
12331
+ g.computeBoundingSphere();
12332
+ }
12333
+ /** 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. */
12334
+ keepUnderPane(pane, normal) {
12335
+ const THREE = this.THREE;
12336
+ const paneBox2 = new THREE.Box3().setFromObject(pane).expandByScalar(COWL_NEAR_PANE);
12337
+ const pos = pane.geometry.attributes.position;
12338
+ const centre = new THREE.Vector3();
12339
+ const v = new THREE.Vector3();
12340
+ for (let i = 0; i < pos.count; i++) centre.add(v.fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld));
12341
+ centre.divideScalar(pos.count);
12342
+ const plane = centre.dot(normal);
12343
+ for (const o of this.c.vehicle.children) {
12344
+ if (!o.isMesh || o === pane || o.userData.glass || this.c.deform?.includes(o)) continue;
12345
+ const box = new THREE.Box3().setFromObject(o);
12346
+ const size = box.getSize(new THREE.Vector3());
12347
+ 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(box)) continue;
12348
+ if (box.getCenter(new THREE.Vector3()).dot(normal) - plane >= COWL_UNDER_GLASS) continue;
12349
+ if (size.y >= COWL_PLATE) {
12350
+ o.material = this.I.makeMaterial(DASHBOARD_COLOR, { roughness: 1, metalness: 0 });
12351
+ continue;
12352
+ }
12353
+ o.material = this.paint();
12354
+ this.cutUnderPane(o, pane, normal);
12355
+ }
12356
+ }
12357
+ /** Cuts away the part of a mesh that lies under a pane (behind its plane, inside its outline seen along its normal). */
12358
+ cutUnderPane(mesh, pane, normal, behind = -Infinity, inset = 0) {
12359
+ const THREE = this.THREE;
12360
+ const outline = this.paneOutline(pane, normal);
12361
+ if (outline.length < 3) return;
12362
+ const u = new THREE.Vector3(0, 1, 0).cross(normal);
12363
+ if (u.lengthSq() < 1e-4) u.set(1, 0, 0);
12364
+ u.normalize();
12365
+ const w = normal.clone().cross(u);
12366
+ const cx = outline.reduce((sum, p) => sum + p.dot(u), 0) / outline.length;
12367
+ const cy = outline.reduce((sum, p) => sum + p.dot(w), 0) / outline.length;
12368
+ const poly = outline.map((p) => {
12369
+ const x = p.dot(u);
12370
+ const y = p.dot(w);
12371
+ const d = Math.hypot(x - cx, y - cy) || 1;
12372
+ const k = Math.max(0, d - inset) / d;
12373
+ return [cx + (x - cx) * k, cy + (y - cy) * k];
12374
+ });
12375
+ const plane = outline.reduce((sum, p) => sum + p.dot(normal), 0) / outline.length;
12376
+ const inside = (x, y) => {
12377
+ let hit = false;
12378
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
12379
+ const [xi, yi] = poly[i];
12380
+ const [xj, yj] = poly[j];
12381
+ if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) hit = !hit;
12382
+ }
12383
+ return hit;
12384
+ };
12385
+ const v = new THREE.Vector3();
12386
+ const toWorld = mesh.matrixWorld;
12387
+ mesh.updateMatrixWorld(true);
12388
+ const xs = poly.map((p) => p[0]);
12389
+ const ys = poly.map((p) => p[1]);
12390
+ 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];
12391
+ subdivideNear(THREE, { mesh, maxLen: COWL_CUT_SUB, near: (corners) => {
12392
+ const us = corners.map((c) => c.dot(u));
12393
+ const ws = corners.map((c) => c.dot(w));
12394
+ return Math.max(...us) >= x0 && Math.min(...us) <= x1 && Math.max(...ws) >= y0 && Math.min(...ws) <= y1;
12395
+ } });
12396
+ this.kit.cutMesh(
12397
+ mesh,
12398
+ [
12399
+ {
12400
+ sdf: (x, y, z) => {
12401
+ v.set(x, y, z).applyMatrix4(toWorld);
12402
+ return inside(v.dot(u), v.dot(w)) && plane - v.dot(normal) > behind ? -1 : 1;
12403
+ }
12404
+ }
12405
+ ],
12406
+ 42,
12407
+ {}
12408
+ );
12409
+ }
12410
+ /** 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. */
12411
+ paneBorder(pane, near, seat) {
12412
+ const THREE = this.THREE;
12413
+ const bars = near.filter((o) => {
12414
+ if (o.geometry.attributes.position.count > PANE_BAR_VERTICES) return false;
12415
+ const size = new THREE.Box3().setFromObject(o).getSize(new THREE.Vector3());
12416
+ return Math.max(size.x, size.y, size.z) > PANE_BAR_LENGTH;
12417
+ });
12418
+ if (!bars.length) return;
12419
+ const outline = this.paneOutline(pane, seat.normal);
12420
+ if (outline.length < 3) return;
12421
+ const centre = new THREE.Vector3();
12422
+ for (const p of outline) centre.add(p);
12423
+ centre.divideScalar(outline.length);
12424
+ const position = [];
12425
+ const toVehicle = this.I.worldInverseOf(this.c);
12426
+ const put = (p) => {
12427
+ const q = p.clone().addScaledVector(seat.normal, PANE_BORDER_LIFT).applyMatrix4(toVehicle);
12428
+ position.push(q.x, q.y, q.z);
12429
+ };
12430
+ const inward = (p) => p.clone().add(centre.clone().sub(p).normalize().multiplyScalar(PANE_BORDER));
12431
+ outline.forEach((a, i) => {
12432
+ const b = outline[(i + 1) % outline.length];
12433
+ const c = inward(b);
12434
+ const d = inward(a);
12435
+ for (const p of [a, b, c, a, c, d]) put(p);
12436
+ });
12437
+ if (!position.length) return;
12438
+ for (const o of bars) o.visible = false;
12439
+ const band = new THREE.BufferGeometry();
12440
+ band.setAttribute("position", new THREE.Float32BufferAttribute(position, 3));
12441
+ band.computeVertexNormals();
12442
+ const material = this.I.MATERIALS.dark.clone();
12443
+ material.side = THREE.DoubleSide;
12444
+ const mesh = new THREE.Mesh(band, material);
12445
+ mesh.userData.noFollow = true;
12446
+ mesh.castShadow = false;
12447
+ mesh.raycast = () => {
12448
+ };
12449
+ this.c.vehicle.add(mesh);
12450
+ }
12451
+ /** The outline of a pane, the convex hull of its vertices seen along its normal, in world space. */
12452
+ paneOutline(pane, normal) {
12453
+ const THREE = this.THREE;
12454
+ const pos = pane.geometry.attributes.position;
12455
+ const u = new THREE.Vector3(0, 1, 0).cross(normal);
12456
+ if (u.lengthSq() < 1e-4) u.set(1, 0, 0);
12457
+ u.normalize();
12458
+ const w = normal.clone().cross(u);
12459
+ const points = [];
12460
+ for (let i = 0; i < pos.count; i++) {
12461
+ const p = new THREE.Vector3().fromBufferAttribute(pos, i).applyMatrix4(pane.matrixWorld);
12462
+ points.push({ x: p.dot(u), y: p.dot(w), p });
12463
+ }
12464
+ points.sort((a, b) => a.x - b.x || a.y - b.y);
12465
+ const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
12466
+ const half = (list) => {
12467
+ const out = [];
12468
+ for (const q of list) {
12469
+ while (out.length >= 2 && cross(out[out.length - 2], out[out.length - 1], q) <= 0) out.pop();
12470
+ out.push(q);
12471
+ }
12472
+ out.pop();
12473
+ return out;
12474
+ };
12475
+ return [...half(points), ...half([...points].reverse())].map((q) => q.p);
12476
+ }
12100
12477
  /** How far the shell still stands past a seated pane, over its triangles. */
12101
12478
  shellPast(pane, shell, ray, normal) {
12102
12479
  const THREE = this.THREE;
@@ -12149,11 +12526,25 @@ var VehicleVisuals = class {
12149
12526
  g.computeBoundingSphere();
12150
12527
  mesh.geometry = g;
12151
12528
  }
12529
+ /** 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. */
12530
+ seatHood() {
12531
+ const hood = this.c.hood;
12532
+ if (!hood?.lift) return;
12533
+ const THREE = this.THREE;
12534
+ const box = new THREE.Box3().setFromObject(hood.piv).getSize(new THREE.Vector3());
12535
+ for (const part of hood.piv.children) {
12536
+ part.position.y -= hood.lift + HOOD_SINK;
12537
+ part.scale.multiply(new THREE.Vector3(1 + HOOD_GAP_CLOSED / Math.max(0.3, box.x), 1, 1 + HOOD_GAP_CLOSED / Math.max(0.3, box.z)));
12538
+ }
12539
+ }
12152
12540
  /** A handle is at the free edge of its door, the end away from the hinge. */
12153
12541
  seatDoorHandles() {
12154
12542
  const THREE = this.THREE;
12155
12543
  const c = this.c;
12156
12544
  const inv = this.I.worldInverseOf(c);
12545
+ const bodyBox = new THREE.Box3();
12546
+ 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));
12547
+ const outerHalf = Math.max(Math.abs(bodyBox.min.z), Math.abs(bodyBox.max.z));
12157
12548
  for (const p of this.model.parts) {
12158
12549
  if (p.role !== "door") continue;
12159
12550
  const z = this.zone(p);
@@ -12167,8 +12558,7 @@ var VehicleVisuals = class {
12167
12558
  const at = box.getCenter(new THREE.Vector3());
12168
12559
  const isHandle = size.x > DOOR_HANDLE_MIN && size.x < DOOR_HANDLE_MAX && size.y < DOOR_HANDLE_THIN && size.z < DOOR_HANDLE_THIN;
12169
12560
  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;
12170
- const here = this.surface("z", z.side < 0 ? -1 : 1, B, [[at.x, at.y]]);
12171
- if (here != null && Math.abs(here - at.z) > DOOR_HANDLE_OUTSIDE) continue;
12561
+ if (Math.abs(at.z) < outerHalf - DOOR_HANDLE_OUTSIDE) continue;
12172
12562
  const skin = this.surface("z", z.side < 0 ? -1 : 1, B, [[free, at.y]]);
12173
12563
  o.position.x += free - at.x;
12174
12564
  if (skin != null) o.position.z += skin + (z.side < 0 ? -1 : 1) * (size.z / 2) - at.z;
@@ -12177,7 +12567,7 @@ var VehicleVisuals = class {
12177
12567
  c.root.updateMatrixWorld(true);
12178
12568
  }
12179
12569
  /** The glass of a drawn mirror sits inside its rounded housing, without corners standing past it. */
12180
- seatMirrorGlass(glass) {
12570
+ seatMirrorGlass(glass, drawn) {
12181
12571
  const g = glass.geometry.clone();
12182
12572
  g.computeBoundingBox();
12183
12573
  const mid = g.boundingBox.getCenter(new this.THREE.Vector3());
@@ -12185,6 +12575,37 @@ var VehicleVisuals = class {
12185
12575
  g.scale(MIRROR_GLASS_FIT, MIRROR_GLASS_FIT, MIRROR_GLASS_FIT);
12186
12576
  g.translate(mid.x, mid.y, mid.z);
12187
12577
  glass.geometry = g;
12578
+ const THREE = this.THREE;
12579
+ const volumeOf = (o) => {
12580
+ const size = new THREE.Box3().setFromObject(o).getSize(new THREE.Vector3());
12581
+ return size.x * size.y * size.z;
12582
+ };
12583
+ const housing = drawn.filter((o) => o !== glass && o.isMesh && o.geometry).sort((a, b) => volumeOf(b) - volumeOf(a))[0];
12584
+ if (housing) {
12585
+ this.c.root.updateMatrixWorld(true);
12586
+ const inv = this.I.worldInverseOf(this.c);
12587
+ glass.updateMatrixWorld(true);
12588
+ const gb = this.I.localBox(this.c, glass, inv);
12589
+ const hb = this.I.localBox(this.c, housing, inv);
12590
+ const side2 = hb.getCenter(new THREE.Vector3()).x > gb.getCenter(new THREE.Vector3()).x ? -1 : 1;
12591
+ const face = side2 < 0 ? hb.min.x + MIRROR_FACE_DEPTH : hb.max.x - MIRROR_FACE_DEPTH;
12592
+ const shape = housing.geometry.clone();
12593
+ const pos = shape.attributes.position;
12594
+ const toVehicle = new THREE.Matrix4().multiplyMatrices(inv, housing.matrixWorld);
12595
+ const back = toVehicle.clone().invert();
12596
+ const v = new THREE.Vector3();
12597
+ for (let i = 0; i < pos.count; i++) {
12598
+ v.fromBufferAttribute(pos, i).applyMatrix4(toVehicle);
12599
+ if ((v.x - face) * side2 <= 0) continue;
12600
+ v.x = face;
12601
+ v.applyMatrix4(back);
12602
+ pos.setXYZ(i, v.x, v.y, v.z);
12603
+ }
12604
+ shape.computeVertexNormals();
12605
+ housing.geometry = shape;
12606
+ const outer = side2 < 0 ? gb.min.x : gb.max.x;
12607
+ glass.position.x += face + side2 * MIRROR_GLASS_PROUD - outer;
12608
+ }
12188
12609
  glass.material = this.I.makeMaterial(MIRROR_GLASS_COLOR, { metalness: 0.95, roughness: 0.12 });
12189
12610
  }
12190
12611
  /** The mirror the model already draws around where the part says, if it draws one. */
@@ -12209,7 +12630,11 @@ var VehicleVisuals = class {
12209
12630
  for (const o of drawn) {
12210
12631
  if (!o.isMesh || o.material !== this.I.MATERIALS.chrome) continue;
12211
12632
  const size = new this.THREE.Box3().setFromObject(o).getSize(new this.THREE.Vector3());
12212
- if (size.x < MIRROR_GLASS_THIN && size.y > MIRROR_GLASS_MIN && size.z > MIRROR_GLASS_MIN) this.seatMirrorGlass(o);
12633
+ const [thin, mid] = size.toArray().sort((a, b) => a - b);
12634
+ if (thin < MIRROR_GLASS_THIN && mid > MIRROR_GLASS_MIN) {
12635
+ this.seatMirrorGlass(o, drawn);
12636
+ o.userData.mirrorGlass = true;
12637
+ }
12213
12638
  }
12214
12639
  this.extra.set(p.id, drawn);
12215
12640
  return;
@@ -12221,14 +12646,26 @@ var VehicleVisuals = class {
12221
12646
  g.name = `obj_${p.id}`;
12222
12647
  g.position.set(cx, cy, cz);
12223
12648
  this.c.vehicle.add(g);
12224
- this.box(0.05, 0.04, 0.1, mats.dark, 0, -0.04, -s * 0.07, g);
12225
- const housing = this.box(0.1, 0.12, 0.05, mats.plastic ?? mats.dark, 0, 0, s * 0.02, g);
12649
+ this.box(0.05, 0.035, 0.05, mats.dark, 0, -0.04, -s * 0.06, g);
12650
+ const housing = this.box(0.08, 0.11, 0.125, mats.plastic ?? mats.dark, 0, 0, s * 0.0225, g);
12226
12651
  housing.userData.mirrorHousing = true;
12227
- const glass = this.box(0.095, 0.1, 6e-3, mats.chrome ?? mats.dark, 0, 0, s * 0.048, g);
12652
+ const glass = this.box(6e-3, 0.095, 0.115, mats.chrome ?? mats.dark, -0.043, 0, s * 0.0225, g);
12228
12653
  glass.userData.mirrorGlass = true;
12229
12654
  g.userData.rigid = true;
12230
12655
  this.extra.set(p.id, [g]);
12231
12656
  }
12657
+ /** 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. */
12658
+ shatterMirror(id) {
12659
+ let broke = false;
12660
+ for (const o of this.extra.get(id) ?? []) {
12661
+ o.traverse((m) => {
12662
+ if (!m.userData.mirrorGlass || !m.visible) return;
12663
+ m.visible = false;
12664
+ broke = true;
12665
+ });
12666
+ }
12667
+ return broke;
12668
+ }
12232
12669
  makeBattery(p) {
12233
12670
  const mats = this.I.MATERIALS;
12234
12671
  const g = new this.THREE.Group();
@@ -12274,6 +12711,11 @@ var VehicleVisuals = class {
12274
12711
  if (carrier) carrier.attach(m);
12275
12712
  }
12276
12713
  /** The door or lid a hinge group stands for. */
12714
+ /** 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. */
12715
+ extraIdOf(o) {
12716
+ for (const [id, objs] of this.extra) if (objs.includes(o)) return id;
12717
+ return void 0;
12718
+ }
12277
12719
  plateIdOf(o) {
12278
12720
  return o.userData.plateOf;
12279
12721
  }
@@ -12330,6 +12772,7 @@ var VehicleVisuals = class {
12330
12772
  best = g;
12331
12773
  }
12332
12774
  }
12775
+ if (p.hp > LAMINATED_HP && best.userData.glass) best.userData.glass.type = "laminated";
12333
12776
  this.kit.hitGlass(this.c, best, pt, new V3(...dir), force);
12334
12777
  return !!best.userData.glass?.broken;
12335
12778
  }
@@ -12351,6 +12794,17 @@ var VehicleVisuals = class {
12351
12794
  if (!z) return;
12352
12795
  for (const w of z.wheels) if (w.parent) this.I.flattenTire(w, k);
12353
12796
  }
12797
+ /** A tyre driven flat until it tore apart: the rubber is gone and the bare rim shows. */
12798
+ tyreTornOff(p) {
12799
+ const z = this.zone(p);
12800
+ if (!z) return;
12801
+ for (const w of z.wheels) {
12802
+ if (!w.parent || w.userData.tornOff) continue;
12803
+ w.userData.tornOff = true;
12804
+ for (const m of this.I.tireMeshes(w)) m.visible = false;
12805
+ w.position.y -= (w.userData.r ?? DEFAULT_WHEEL_RADIUS) * SIDEWALL_SHARE;
12806
+ }
12807
+ }
12354
12808
  /** Burning rubber: the tyre sags and goes black as it melts. */
12355
12809
  tyreMelt(p, melt) {
12356
12810
  const z = this.zone(p);
@@ -12674,6 +13128,41 @@ var BLUNT = { steel: 0.8, aluminium: 1, glass: 1.5, plastic: 1.2, rubber: 0.3 };
12674
13128
  var SHIELDED = /* @__PURE__ */ new Set(["engine", "gearbox", "frame", "radiator", "fuel_tank", "oil_pan", "battery", "alternator", "brakes", "steering", "shocks"]);
12675
13129
  var AMBIENT = 20;
12676
13130
  var FLAME = 700;
13131
+ 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 };
13132
+ var ARMOUR_STEEL_MM = { IIIA: 4, III: 7 };
13133
+ var SOFT_LEAD = 0.8;
13134
+ var TYRE_FLAT_HEALTH = 0.1;
13135
+ var TYRE_BURST_JOULES = 1500;
13136
+ var TYRE_BURST_CALIBER = 12;
13137
+ var TYRE_LEAK_PER_MM2 = 26e-5;
13138
+ var FLAT_TYRE_LIFE_M = 1500;
13139
+ var FLAT_TYRE_TOWN_SPEED = 8;
13140
+ var RUN_FLAT_LIFE_M = 8e4;
13141
+ var RUN_FLAT_SPEED = 22;
13142
+ var TYRE_RUNFLAT_HEALTH = 0.6;
13143
+ var CRUMPLE_BASE_M = 0.04;
13144
+ var CRUMPLE_PER_FORCE_M = 0.15;
13145
+ var CRUMPLE_MIN_FORCE = 0.3;
13146
+ var CRUMPLE_MAX_FORCE = 1.4;
13147
+ var RIM_HEALTH = 0.05;
13148
+ var RIM_GRIND_PER_M = 4e-4;
13149
+ var TYRE_FLAT_SAG = 0.24;
13150
+ var TYRE_FLAT_AIR = 0.2;
13151
+ var BRAKE_FLUID_NEEDED = 0.6;
13152
+ var BRAKE_NO_FLUID = 0.15;
13153
+ var ENGINE_WORKING_C = 90;
13154
+ var ENGINE_SETTLE_S = 30;
13155
+ var ENGINE_HEATING = 0.5;
13156
+ var ENGINE_STEAM_C = 120;
13157
+ var ENGINE_SEIZE_C = 150;
13158
+ var COOLANT_NEEDED = 0.4;
13159
+ var OIL_NEEDED = 0.3;
13160
+ var OIL_WEAR = 4e-3;
13161
+ var ENGINE_SEIZE_HP = 0.2;
13162
+ var EXPANDED_HOLE = 1.6;
13163
+ var MIN_SPENT = 0.08;
13164
+ var WELDED = /* @__PURE__ */ new Set(["roof", "pillar", "frame", "panel"]);
13165
+ var CRUMPLES = /* @__PURE__ */ new Set(["door", "hood", "lid", "fender", "bumper"]);
12677
13166
  var FRACTURE = { glass: "shatter", plastic: "snap", rubber: "tear", steel: "none", aluminium: "none" };
12678
13167
  var add3 = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
12679
13168
  var r3 = (v) => [Math.round(v[0] * 1e5) / 1e5, Math.round(v[1] * 1e5) / 1e5, Math.round(v[2] * 1e5) / 1e5];
@@ -12685,6 +13174,8 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12685
13174
  let c = vehicleCtx;
12686
13175
  const id = options.id ?? c.key;
12687
13176
  const headless = !!options.headless;
13177
+ const armourId = armourLevelOf(c.meta, options.armour);
13178
+ const armourLevel = armourId ? { ...ARMOUR_LEVELS[armourId], ...options.armourLevels?.[armourId] } : null;
12688
13179
  const seedBase = strHash(`${c.key}|${id}|${options.seed ?? 0}`);
12689
13180
  let rand = rng(seedBase);
12690
13181
  const ignitionRules = resolveIgnitionRules(options.ignition);
@@ -12742,7 +13233,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12742
13233
  return d.ignitionC ? 1 : 0;
12743
13234
  };
12744
13235
  const setup = () => {
12745
- model = buildVehicleModel(kit, c, { hitch: options.hitch });
13236
+ model = buildVehicleModel(kit, c, { hitch: options.hitch, armour: armourLevel });
12746
13237
  ps.clear();
12747
13238
  for (const d of model.parts) {
12748
13239
  const kg = burnKg(d);
@@ -12762,6 +13253,8 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12762
13253
  lost: false,
12763
13254
  status: "intact",
12764
13255
  flat: false,
13256
+ air: 1,
13257
+ airLoss: 0,
12765
13258
  fireLoss: 0
12766
13259
  });
12767
13260
  }
@@ -12862,7 +13355,8 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12862
13355
  vis.breakLight(d, true);
12863
13356
  break;
12864
13357
  case "tyre":
12865
- vis.tyreFlat(d, 0.34);
13358
+ if (status === "broken") vis.tyreTornOff(d);
13359
+ else vis.tyreFlat(d, 0.34);
12866
13360
  break;
12867
13361
  case "mirror":
12868
13362
  case "bumper":
@@ -12892,7 +13386,8 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12892
13386
  const damage = (p, amount, dtype, source, out, cause) => {
12893
13387
  if (p.lost || !(amount > 0)) return;
12894
13388
  const before = p.hp;
12895
- p.hp = Math.max(0, p.hp - amount);
13389
+ const crumples = dtype === "blunt" && CRUMPLES.has(p.def.role) && (p.def.material === "steel" || p.def.material === "aluminium");
13390
+ p.hp = Math.max(WELDED.has(p.def.role) || crumples ? 1 : 0, p.hp - amount);
12896
13391
  out.push(ev({ type: "damage", part: p.def.id, amount: r2(before - p.hp), hp: r2(p.hp), damageType: dtype, ...source ? { source } : {} }));
12897
13392
  wear(p, amount, out);
12898
13393
  syncZone(p);
@@ -12935,11 +13430,22 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12935
13430
  const p = v.clone().applyMatrix4(M);
12936
13431
  return [p.x, p.y, p.z];
12937
13432
  };
12938
- const armour = () => c.meta.armored || c.meta.bullet ? 30 : 1;
13433
+ const armour = () => armourLevel?.steel ?? 1;
13434
+ const steelMm = (d) => {
13435
+ const base = STEEL_MM[d.role] ?? STEEL_MM.panel;
13436
+ return d.material === "steel" || d.material === "aluminium" ? base * (armourLevel ? ARMOUR_STEEL_MM[armourId === "III" ? "III" : "IIIA"] / STEEL_MM.panel : 1) : base;
13437
+ };
13438
+ const goesThrough = (h, d, p, energy, res) => {
13439
+ const piercing = h.piercing ?? 1;
13440
+ const metal = d.material === "steel" || d.material === "aluminium";
13441
+ const mm = h.penetration?.steel;
13442
+ if (metal && mm != null) return mm >= steelMm(d) * (p.dents.length > 3 ? 0.9 : 1);
13443
+ return energy * (metal || d.material === "glass" ? piercing : 1) >= res;
13444
+ };
12939
13445
  const resist = (d, p) => {
12940
13446
  let base = RESIST[d.material] ?? 100;
12941
13447
  if (d.material === "glass" && d.hp > 1e3) base = 120;
12942
- if (d.material === "glass" && c.meta.bullet) base *= 30;
13448
+ if (d.material === "glass" && armourLevel) base = armourLevel.glassStops;
12943
13449
  const k = ROLE_RESIST[d.role] ?? 1;
12944
13450
  const arm = d.material === "steel" || d.material === "aluminium" ? armour() : 1;
12945
13451
  return base * k * arm * (p.dents.length > 3 ? 0.9 : 1);
@@ -12962,7 +13468,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12962
13468
  if (vis) {
12963
13469
  const pos = new V3(d.box.center[0], Math.max(0.2, (c.meta.clearance ?? 0.3) * 0.8), d.box.center[2]);
12964
13470
  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 });
12965
- else if ((c.meta.kind ?? "car") !== "boat") l.pud = I.puddle(c, fluidKind(fluid), pos, fluid === "oil" ? 0.8 : 1.1);
13471
+ 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);
12966
13472
  }
12967
13473
  }
12968
13474
  const out3 = dirToWorld(new V3(dirLocal.x * 0.5, dirLocal.y * 0.5 - 0.6, dirLocal.z * 0.5));
@@ -12999,7 +13505,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
12999
13505
  exploded = true;
13000
13506
  const d = tank.def;
13001
13507
  const spec = d.explodes ?? { energy: 8e5, radius: 5, atC: 320 };
13002
- out.push(ev({ type: "explode", part: d.id, point: toWorld(d.box.center), energy: spec.energy, radius: spec.radius }));
13508
+ 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 } : {} }));
13003
13509
  tank.liters = 0;
13004
13510
  leaks.delete(d.id);
13005
13511
  if (vis) kit.explode(c, { delay: 0.6 });
@@ -13099,7 +13605,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
13099
13605
  const dir = dirToLocal(h.direction ?? [0, 0, -1]).normalize();
13100
13606
  const n = h.normal ? dirToLocal(h.normal).normalize() : normalOf(d, v);
13101
13607
  const res = resist(d, p);
13102
- const through = E >= res;
13608
+ const through = goesThrough(h, d, p, E, res);
13103
13609
  const cosInc = Math.abs(dir.dot(n));
13104
13610
  const metal = d.material === "steel" || d.material === "aluminium";
13105
13611
  const worldPoint = toWorld(v);
@@ -13110,13 +13616,21 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
13110
13616
  vis?.spark([v.x, v.y, v.z], [refl.x, refl.y, refl.z], 6);
13111
13617
  return;
13112
13618
  }
13113
- const holeDiam = Math.max(6e-3, cal / 1e3 * (d.material === "glass" ? 1.4 : 1));
13619
+ const opens = (h.piercing ?? 1) < SOFT_LEAD ? EXPANDED_HOLE : 1;
13620
+ const holeDiam = Math.max(6e-3, cal / 1e3 * (d.material === "glass" ? 1.4 : 1) * opens);
13621
+ const energyLeft = () => {
13622
+ const metal2 = d.material === "steel" || d.material === "aluminium";
13623
+ const mm = h.penetration?.steel;
13624
+ const spent = metal2 && mm ? clamp(steelMm(d) / mm, MIN_SPENT, 1) : clamp(res / Math.max(E, 1), MIN_SPENT, 1);
13625
+ return E * (1 - spent);
13626
+ };
13114
13627
  const push = (hole) => {
13115
13628
  if (!hole) return;
13116
13629
  const hp = { point: r3(fromVehicle(v, M)), normal: r3(fromVehicle(n, new THREE.Matrix4().extractRotation(M))), diameter: r2(holeDiam), through };
13117
13630
  p.holes.push(hp);
13118
- 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])))))) } : {} }));
13631
+ 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])))))) } : {} }));
13119
13632
  vis?.holeDecal([v.x, v.y, v.z], [n.x, n.y, n.z], holeDiam, d);
13633
+ if (d.role === "mirror") vis?.shatterMirror(d.id);
13120
13634
  startLeak(p, v, cal, out, dir);
13121
13635
  };
13122
13636
  switch (d.material) {
@@ -13129,7 +13643,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
13129
13643
  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) };
13130
13644
  p.cracks.push(crack);
13131
13645
  out.push(ev({ type: "crack", part: d.id, crack, worldPoint }));
13132
- damage(p, E * 0.5, "ballistic", h.source, out, worldPoint);
13646
+ damage(p, E * (armourLevel && !through ? armourLevel.glassTakes : 0.5), "ballistic", h.source, out, worldPoint);
13133
13647
  } else damage(p, laminated ? E * 0.5 : p.hp, "ballistic", h.source, out, worldPoint);
13134
13648
  break;
13135
13649
  }
@@ -13137,8 +13651,8 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
13137
13651
  push(through);
13138
13652
  damage(p, E * 0.4, "ballistic", h.source, out, worldPoint);
13139
13653
  if (through && !p.lost && !p.flat) {
13140
- p.flat = true;
13141
- vis?.tyreFlat(d, 0.24);
13654
+ if (E >= TYRE_BURST_JOULES || cal >= TYRE_BURST_CALIBER) deflate(p, 1);
13655
+ else p.airLoss += TYRE_LEAK_PER_MM2 * Math.PI * (cal / 2) ** 2;
13142
13656
  }
13143
13657
  break;
13144
13658
  }
@@ -13254,7 +13768,7 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
13254
13768
  const electric = c.spec.fuel === "electric";
13255
13769
  const tank = tankPart();
13256
13770
  const fuel = tank ? tank.liters ?? 0 : electric ? 1 : 1;
13257
- const engineOk = sound("engine") && !exploded && fuel > 0.05;
13771
+ const engineOk = sound("engine") && !exploded && !seized && fuel > 0.05;
13258
13772
  const powered = sound("battery") && sound("alternator");
13259
13773
  const gearOk = sound("gearbox");
13260
13774
  const brakes = [...ps.values()].find((p) => p.def.role === "brakes");
@@ -13414,9 +13928,12 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
13414
13928
  const state = () => stateCache ?? (stateCache = buildState());
13415
13929
  const isSimulating = () => {
13416
13930
  if (leaks.size > 0) return true;
13417
- for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT)) return true;
13931
+ for (const p of ps.values()) if (!p.lost && (p.burning || p.temp > AMBIENT || p.airLoss > 0 && p.air > 0)) return true;
13418
13932
  return false;
13419
13933
  };
13934
+ let engineHeat = ENGINE_WORKING_C;
13935
+ let seized = false;
13936
+ let steaming = false;
13420
13937
  const surfaceRays = new THREE.Raycaster();
13421
13938
  surfaceRays.layers.enableAll();
13422
13939
  surfaceRays.camera = new THREE.PerspectiveCamera();
@@ -13440,14 +13957,24 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
13440
13957
  if (pane) return ps.get(pane) ?? null;
13441
13958
  for (let q = o; q && q !== c.vehicle; q = q.parent) {
13442
13959
  if (vis?.plateIdOf(q)) return ps.get(vis.plateIdOf(q)) ?? null;
13960
+ const piece = vis?.extraIdOf(q);
13961
+ if (piece) return ps.get(piece) ?? null;
13443
13962
  }
13444
13963
  return locate(v, 0.08, (p) => p.def.role === "glass");
13445
13964
  };
13965
+ const surfaceMeshes = [];
13966
+ const collectSurfaces = (o) => {
13967
+ if (!o.visible || o.userData.decal || o.userData.ghost || o.userData.noSurfaceRay) return;
13968
+ if (o.isMesh) surfaceMeshes.push(o);
13969
+ for (const child of o.children) collectSurfaces(child);
13970
+ };
13446
13971
  const surfaceRay = (ray) => {
13447
13972
  sync();
13448
13973
  surfaceRays.set(new V3(ray.origin[0], ray.origin[1], ray.origin[2]), new V3(ray.direction[0], ray.direction[1], ray.direction[2]));
13449
13974
  surfaceRays.far = ray.maxDistance;
13450
- for (const h of surfaceRays.intersectObject(c.vehicle, true)) {
13975
+ surfaceMeshes.length = 0;
13976
+ for (const child of c.vehicle.children) collectSurfaces(child);
13977
+ for (const h of surfaceRays.intersectObjects(surfaceMeshes, false)) {
13451
13978
  if (!h.face || !isDrawnSurface(h.object)) continue;
13452
13979
  const p = partOfSurface(h.object, c.vehicle.worldToLocal(h.point.clone()));
13453
13980
  if (!p || p.lost || ray.ignored?.(p.def.id)) continue;
@@ -13481,7 +14008,29 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
13481
14008
  applyEffects();
13482
14009
  return out;
13483
14010
  };
14011
+ const runFlat = armourId != null;
14012
+ const tyreHealth = (p) => {
14013
+ if (p.lost) return RIM_HEALTH;
14014
+ if (p.flat) return runFlat ? TYRE_RUNFLAT_HEALTH : TYRE_FLAT_HEALTH;
14015
+ return Math.min(p.hp / p.def.hp, 0.2 + 0.8 * p.air);
14016
+ };
14017
+ const tyreOf = (rim) => {
14018
+ let best;
14019
+ let bestD = Infinity;
14020
+ for (const p of ps.values()) {
14021
+ if (p.def.role !== "tyre") continue;
14022
+ const d = Math.hypot(p.def.box.center[0] - rim.def.box.center[0], p.def.box.center[2] - rim.def.box.center[2]);
14023
+ if (d < bestD) [best, bestD] = [p, d];
14024
+ }
14025
+ return best;
14026
+ };
14027
+ const deflate = (p, share) => {
14028
+ p.air = Math.max(0, p.air - share);
14029
+ vis?.tyreFlat(p.def, TYRE_FLAT_SAG * (1 - p.air));
14030
+ if (p.air <= TYRE_FLAT_AIR) p.flat = true;
14031
+ };
13484
14032
  const update = (dt) => {
14033
+ for (const p of ps.values()) if (p.airLoss > 0 && p.air > 0 && !p.lost) deflate(p, p.airLoss * dt);
13485
14034
  clock += dt;
13486
14035
  const out = queue;
13487
14036
  queue = [];
@@ -13597,6 +14146,70 @@ function createVehicleDestructible(kit, vehicleCtx, options = {}) {
13597
14146
  partName: (pid) => ps.get(pid)?.def.name ?? pid,
13598
14147
  status: computeStatus,
13599
14148
  animations: () => createVehicleClips(model, vis, kit, c),
14149
+ crumple: (point, direction, depth) => {
14150
+ if (!vis || depth <= 0) return;
14151
+ sync();
14152
+ const force = clamp((depth - CRUMPLE_BASE_M) / CRUMPLE_PER_FORCE_M, CRUMPLE_MIN_FORCE, CRUMPLE_MAX_FORCE);
14153
+ kit.impact(c, toLocal(point), dirToLocal(direction).normalize(), force, { maxDepth: depth });
14154
+ stateCache = void 0;
14155
+ },
14156
+ rollFlat: (meters, speed) => {
14157
+ for (const p of ps.values()) {
14158
+ if (p.def.role !== "tyre" || !p.flat || p.lost) continue;
14159
+ 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);
14160
+ p.hp = Math.max(0, p.hp - p.def.hp * meters / life);
14161
+ if (p.hp > 0) continue;
14162
+ const at = toWorld(p.def.box.center);
14163
+ lose(p, "broken", queue, at);
14164
+ 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) }));
14165
+ }
14166
+ let grinding = 0;
14167
+ for (const rim of ps.values()) {
14168
+ if (rim.def.role !== "rim" || rim.lost || !tyreOf(rim)?.lost) continue;
14169
+ grinding += 1;
14170
+ rim.hp = Math.max(1, rim.hp - rim.def.hp * RIM_GRIND_PER_M * meters);
14171
+ const [x, y, z] = rim.def.box.center;
14172
+ 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) }));
14173
+ }
14174
+ stateCache = void 0;
14175
+ return grinding;
14176
+ },
14177
+ runEngine: (seconds, load) => {
14178
+ const engine = [...ps.values()].find((p) => p.def.role === "engine");
14179
+ if (!engine || engine.lost || seized) return engineHeat;
14180
+ const share = (fluid) => {
14181
+ const list = [...ps.values()].filter((p) => p.def.contains?.fluid === fluid);
14182
+ return list.length ? Math.min(...list.map((p) => (p.liters ?? 0) / p.def.contains.liters)) : 1;
14183
+ };
14184
+ const coolant = share("coolant");
14185
+ const oil = share("oil");
14186
+ const work = 0.4 + 0.6 * clamp(load, 0, 1);
14187
+ if (coolant < COOLANT_NEEDED) engineHeat += (COOLANT_NEEDED - coolant) / COOLANT_NEEDED * ENGINE_HEATING * work * seconds;
14188
+ else engineHeat += (ENGINE_WORKING_C - engineHeat) * Math.min(1, seconds / ENGINE_SETTLE_S);
14189
+ if (oil < OIL_NEEDED) engine.hp = Math.max(0, engine.hp - engine.def.hp * OIL_WEAR * work * seconds * ((OIL_NEEDED - oil) / OIL_NEEDED));
14190
+ if (engineHeat >= ENGINE_STEAM_C && vis && !steaming) {
14191
+ steaming = true;
14192
+ kit.addEmitter(c, new V3(engine.def.box.center[0], engine.def.box.center[1] + 0.4, 0), "white", { rate: 8, s: 1 });
14193
+ }
14194
+ if (engineHeat >= ENGINE_SEIZE_C || engine.hp <= engine.def.hp * ENGINE_SEIZE_HP) {
14195
+ seized = true;
14196
+ queue.push(ev({ type: "damage", part: engine.def.id, amount: 0, hp: r2(engine.hp), damageType: "heat" }));
14197
+ }
14198
+ stateCache = void 0;
14199
+ return engineHeat;
14200
+ },
14201
+ driveReport: () => {
14202
+ const health = (role) => {
14203
+ const list = [...ps.values()].filter((p) => p.def.role === role);
14204
+ if (!list.length) return 1;
14205
+ return Math.min(...list.map((p) => p.lost || !attached(p.def.id) ? 0 : p.hp / p.def.hp));
14206
+ };
14207
+ 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) }));
14208
+ const status = computeStatus();
14209
+ const brakeSystem = [...ps.values()].find((p) => p.def.role === "brakes" && p.def.contains);
14210
+ const fluidLeft = brakeSystem ? clamp((brakeSystem.liters ?? 0) / brakeSystem.def.contains.liters / BRAKE_FLUID_NEEDED, 0, 1) : 1;
14211
+ 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") };
14212
+ },
13600
14213
  partWorldCenter: (pid) => {
13601
14214
  sync();
13602
14215
  const d = ps.get(pid)?.def;
@@ -13681,6 +14294,34 @@ function createCreaseQueue() {
13681
14294
  };
13682
14295
  }
13683
14296
 
14297
+ // src/destructible/crash.ts
14298
+ var CRASH_RULES = { energyToDamage: 0.3, hitSpacing: 0.6, minSpeed: 1.5, rows: [0, 0.3], crushPerMs: 0.035, maxCrush: 0.6 };
14299
+ function crashDeltaV(speed, massKg, otherMassKg) {
14300
+ if (!Number.isFinite(otherMassKg)) return speed;
14301
+ return speed * otherMassKg / (massKg + otherMassKg);
14302
+ }
14303
+ function crashVehicle(d, crash) {
14304
+ const rules = { ...CRASH_RULES, ...crash.rules };
14305
+ const deltaV = crashDeltaV(crash.speed, crash.massKg, crash.otherMassKg);
14306
+ const energy = 0.5 * crash.massKg * deltaV * deltaV;
14307
+ if (crash.speed < rules.minSpeed) return { deltaV, energy, events: [] };
14308
+ const [dx, , dz] = crash.direction;
14309
+ const length = Math.hypot(dx, dz) || 1;
14310
+ const across = [-dz / length, 0, dx / length];
14311
+ const hits = Math.max(1, Math.round(crash.contactWidth / rules.hitSpacing));
14312
+ const each = energy * rules.energyToDamage / (hits * rules.rows.length);
14313
+ const events = [];
14314
+ for (const up of rules.rows)
14315
+ for (let i = 0; i < hits; i++) {
14316
+ const offset = hits === 1 ? 0 : (i / (hits - 1) - 0.5) * crash.contactWidth;
14317
+ const point = [crash.point[0] + across[0] * offset, crash.point[1] + up, crash.point[2] + across[2] * offset];
14318
+ events.push(...d.hit({ type: "blunt", point, direction: [dx / length, 0, dz / length], energy: each, radius: rules.hitSpacing / 2, ...crash.source ? { source: crash.source } : {} }));
14319
+ }
14320
+ const crush = Math.min(rules.maxCrush, deltaV * rules.crushPerMs);
14321
+ d.crumple(crash.point, [dx / length, 0, dz / length], crush);
14322
+ return { deltaV, energy, events };
14323
+ }
14324
+
13684
14325
  // src/destructible/enclosure.ts
13685
14326
  var CABIN_LEAK = 0.02;
13686
14327
  var HOLE_RATE_PER_AREA = 3e3;
@@ -13765,17 +14406,22 @@ function cabinEnclosure(source, options = {}) {
13765
14406
  };
13766
14407
  }
13767
14408
  export {
14409
+ ARMOUR_LEVELS,
13768
14410
  CABIN_LEAK,
14411
+ CRASH_RULES,
13769
14412
  JOINT_RULES,
13770
14413
  LEGACY_STATES,
13771
14414
  LEGACY_ZONE_IDS,
13772
14415
  SEED_SALTS,
14416
+ armourLevelOf,
13773
14417
  buildVehicleItem,
13774
14418
  buildVehicleModel,
13775
14419
  cabinBoxOf,
13776
14420
  cabinEnclosure,
13777
14421
  clamp,
13778
14422
  crackedGlassMaterial,
14423
+ crashDeltaV,
14424
+ crashVehicle,
13779
14425
  createCreaseQueue,
13780
14426
  createLod,
13781
14427
  createTranslate,