@adatechnology/game-destruction-three 0.10.0 → 0.11.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.
@@ -27,11 +27,14 @@ var GameDestructionThree = (() => {
27
27
  DEFAULT_FIRE: () => DEFAULT_FIRE,
28
28
  DESTRUCTION_SOUND_BANK: () => DESTRUCTION_SOUND_BANK,
29
29
  DESTRUCTION_SOUND_KEYS: () => DESTRUCTION_SOUND_KEYS,
30
+ DRAG_SOUNDS: () => DRAG_SOUNDS,
30
31
  EXPLOSION_BOOM: () => EXPLOSION_BOOM,
31
32
  SCENE_SOUNDS: () => SCENE_SOUNDS,
32
33
  createDestructionRenderer: () => createDestructionRenderer,
33
34
  createDestructionSound: () => createDestructionSound,
35
+ dragSoundKey: () => dragSoundKey,
34
36
  heatColor: () => heatColor,
37
+ impactKey: () => impactKey,
35
38
  specFromObject3D: () => specFromObject3D
36
39
  });
37
40
 
@@ -6387,6 +6390,24 @@ var GameDestructionThree = (() => {
6387
6390
  for (const decal2 of this.decals) decal2.on = false;
6388
6391
  this.count = 0;
6389
6392
  }
6393
+ /**
6394
+ * Takes away the marks inside a box: world `center`, `half` extents along the axes of the world `rotation` (a quaternion, x y z w; none means the
6395
+ * world axes). The surface they were on is gone (a brick came out of a wall with its bullet holes). Returns how many went.
6396
+ */
6397
+ clearInside(center, half, rotation = [0, 0, 0, 1]) {
6398
+ const qx = -rotation[0], qy = -rotation[1], qz = -rotation[2], qw = rotation[3];
6399
+ let gone = 0;
6400
+ for (const decal2 of this.decals) {
6401
+ if (!decal2.on) continue;
6402
+ const vx2 = decal2.x - center[0], vy2 = decal2.y - center[1], vz2 = decal2.z - center[2];
6403
+ const tx = 2 * (qy * vz2 - qz * vy2), ty = 2 * (qz * vx2 - qx * vz2), tz = 2 * (qx * vy2 - qy * vx2);
6404
+ const lx = vx2 + qw * tx + (qy * tz - qz * ty), ly = vy2 + qw * ty + (qz * tx - qx * tz), lz = vz2 + qw * tz + (qx * ty - qy * tx);
6405
+ if (Math.abs(lx) > half[0] || Math.abs(ly) > half[1] || Math.abs(lz) > half[2]) continue;
6406
+ decal2.on = false;
6407
+ gone++;
6408
+ }
6409
+ return gone;
6410
+ }
6390
6411
  update(dt) {
6391
6412
  this.count = 0;
6392
6413
  for (const decal2 of this.decals) {
@@ -8589,6 +8610,44 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8589
8610
  }
8590
8611
  var LIGHT_BUDGET = EVENT_BUDGET.blastLights + EVENT_BUDGET.fireLights;
8591
8612
 
8613
+ // src/fitted-boxes.ts
8614
+ var SLICE_LENGTH = 0.12;
8615
+ var MAX_SLICES = 12;
8616
+ var SNUG_RATIO = 1.1;
8617
+ function pushFittedBoxes(local2, matrix, out) {
8618
+ const e = matrix.elements;
8619
+ const sizes = [0, 1, 2].map((k) => k === 0 ? local2.max.x - local2.min.x : k === 1 ? local2.max.y - local2.min.y : local2.max.z - local2.min.z);
8620
+ const lengths = [0, 1, 2].map((k) => sizes[k] * Math.hypot(e[k * 4], e[k * 4 + 1], e[k * 4 + 2]));
8621
+ const longest = lengths.indexOf(Math.max(...lengths));
8622
+ const whole = worldBoxOf(local2, matrix, longest, 0, 1);
8623
+ const span = (whole[3] - whole[0]) * (whole[4] - whole[1]) * (whole[5] - whole[2]);
8624
+ const volume = lengths[0] * lengths[1] * lengths[2];
8625
+ const slices = span <= volume * SNUG_RATIO ? 1 : Math.min(MAX_SLICES, Math.ceil(lengths[longest] / SLICE_LENGTH));
8626
+ if (slices <= 1) {
8627
+ out.push(...whole);
8628
+ return;
8629
+ }
8630
+ for (let i = 0; i < slices; i++) out.push(...worldBoxOf(local2, matrix, longest, i / slices, (i + 1) / slices));
8631
+ }
8632
+ function worldBoxOf(local2, matrix, axis2, from, to) {
8633
+ const e = matrix.elements;
8634
+ const low = [local2.min.x, local2.min.y, local2.min.z];
8635
+ const high = [local2.max.x, local2.max.y, local2.max.z];
8636
+ const start = low[axis2];
8637
+ const size = high[axis2] - start;
8638
+ low[axis2] = start + size * from;
8639
+ high[axis2] = start + size * to;
8640
+ const box = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];
8641
+ for (const x of [low[0], high[0]]) for (const y of [low[1], high[1]]) for (const z of [low[2], high[2]]) {
8642
+ for (let k = 0; k < 3; k++) {
8643
+ const v = e[k] * x + e[k + 4] * y + e[k + 8] * z + e[k + 12];
8644
+ box[k] = Math.min(box[k], v);
8645
+ box[k + 3] = Math.max(box[k + 3], v);
8646
+ }
8647
+ }
8648
+ return box;
8649
+ }
8650
+
8592
8651
  // src/math.ts
8593
8652
  function createRng(seed) {
8594
8653
  let a = seed >>> 0;
@@ -8630,12 +8689,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8630
8689
  var SHEET_MAX = 4e-3;
8631
8690
  var SHEET_AIR = 2.5;
8632
8691
  var SHEET_SWAY = 3;
8692
+ var BIG_LENGTH = 0.2;
8693
+ var BIG_WIDTH = 0.03;
8694
+ var isBig = (size) => {
8695
+ const [, middle, longest] = [...size].sort((a, b) => a - b);
8696
+ return longest >= BIG_LENGTH && middle >= BIG_WIDTH;
8697
+ };
8633
8698
  var FragmentSystem = class {
8634
8699
  constructor(THREE, o) {
8635
8700
  __publicField(this, "THREE", THREE);
8636
8701
  __publicField(this, "o", o);
8637
8702
  __publicField(this, "list", []);
8638
8703
  __publicField(this, "box");
8704
+ /** The mesh box of a fragment before its scale. */
8705
+ __publicField(this, "unitBox", { min: { x: -0.5, y: -0.5, z: -0.5 }, max: { x: 0.5, y: 0.5, z: 0.5 } });
8639
8706
  __publicField(this, "shardGeos", []);
8640
8707
  __publicField(this, "materials", /* @__PURE__ */ new Map());
8641
8708
  __publicField(this, "q");
@@ -8644,6 +8711,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8644
8711
  __publicField(this, "v");
8645
8712
  __publicField(this, "w");
8646
8713
  __publicField(this, "rng", createRng(1374496523));
8714
+ /** Count of `update` calls: what makes the fragments of one burst tell apart from older ones. */
8715
+ __publicField(this, "tick", 0);
8647
8716
  __publicField(this, "markGeometry", null);
8648
8717
  __publicField(this, "markMaterial", null);
8649
8718
  this.box = new THREE.BoxGeometry(1, 1, 1);
@@ -8711,8 +8780,28 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8711
8780
  mesh2.add(disc);
8712
8781
  }
8713
8782
  }
8714
- /** Create the mesh of a fragment and start its motion. `maxAllowed` is the live budget. */
8783
+ /**
8784
+ * Create the mesh of a fragment and start its motion. `maxAllowed` is the live budget: over it the oldest fragment still showing fades out to make room, and a burst
8785
+ * bigger than the budget (every live fragment was made in this same frame) does not make the rest at all, so a blast never builds a thousand meshes only to fade them
8786
+ * out. Returns null when the fragment was not made.
8787
+ */
8715
8788
  spawn(f, material, home, maxAllowed) {
8789
+ if (this.list.length >= maxAllowed) {
8790
+ let alive = 0;
8791
+ let oldest;
8792
+ let oldestSmall;
8793
+ for (const l of this.list) {
8794
+ if (l.dying) continue;
8795
+ alive++;
8796
+ oldest ?? (oldest = l);
8797
+ if (!l.body) oldestSmall ?? (oldestSmall = l);
8798
+ }
8799
+ oldest = oldestSmall ?? oldest;
8800
+ if (alive >= maxAllowed) {
8801
+ if (!oldest || oldest.born === this.tick) return null;
8802
+ oldest.dying = true;
8803
+ }
8804
+ }
8716
8805
  const geo = f.shape === "shard" ? this.shardGeometry(hashString(f.id)) : this.box;
8717
8806
  const mesh2 = new this.THREE.Mesh(geo, material);
8718
8807
  mesh2.position.set(f.center[0], f.center[1], f.center[2]);
@@ -8736,19 +8825,42 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8736
8825
  rest: 0,
8737
8826
  fade: 1,
8738
8827
  dying: false,
8739
- air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0
8828
+ air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0,
8829
+ body: null,
8830
+ born: this.tick
8740
8831
  };
8741
- this.list.push(live);
8742
- let alive = this.list.reduce((n, l) => n + (l.dying ? 0 : 1), 0);
8743
- for (let i = 0; i < this.list.length && alive > maxAllowed; i++) {
8744
- if (!this.list[i].dying) {
8745
- this.list[i].dying = true;
8746
- alive--;
8747
- }
8832
+ if (f.shape !== "shard" && isBig(f.size)) {
8833
+ live.body = {
8834
+ mass: f.mass,
8835
+ push: (x, z) => {
8836
+ live.vel.x = x;
8837
+ live.vel.z = z;
8838
+ live.vel.y = Math.max(live.vel.y, 0.3);
8839
+ live.asleep = false;
8840
+ live.rest = 0;
8841
+ }
8842
+ };
8748
8843
  }
8844
+ this.list.push(live);
8749
8845
  this.addMarks(mesh2, f);
8750
8846
  return mesh2;
8751
8847
  }
8848
+ /** The boxes of the big fragments around (x, z). */
8849
+ solidsNear(x, z, radius, found) {
8850
+ const boxes = [];
8851
+ for (const f of this.list) {
8852
+ if (!f.body || f.dying) continue;
8853
+ const reach = f.half.length() + radius;
8854
+ if (Math.abs(f.mesh.position.x - x) > reach || Math.abs(f.mesh.position.z - z) > reach) continue;
8855
+ f.mesh.updateMatrixWorld();
8856
+ boxes.length = 0;
8857
+ pushFittedBoxes(this.unitBox, f.mesh.matrixWorld, boxes);
8858
+ for (let i = 0; i < boxes.length; i += 6) {
8859
+ if (x < boxes[i] - radius || x > boxes[i + 3] + radius || z < boxes[i + 2] - radius || z > boxes[i + 5] + radius) continue;
8860
+ found(f.home, [boxes[i], boxes[i + 1], boxes[i + 2]], [boxes[i + 3], boxes[i + 4], boxes[i + 5]], f.body);
8861
+ }
8862
+ }
8863
+ }
8752
8864
  /** Add velocity to every fragment within `radius` of a point (blasts). */
8753
8865
  blast(point, radius, speed) {
8754
8866
  for (const f of this.list) {
@@ -8764,7 +8876,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8764
8876
  f.rest = 0;
8765
8877
  }
8766
8878
  }
8879
+ /** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
8880
+ wakeRegion(x, z, radius) {
8881
+ for (const f of this.list) {
8882
+ if (!f.asleep || Math.hypot(f.mesh.position.x - x, f.mesh.position.z - z) > radius) continue;
8883
+ f.asleep = false;
8884
+ f.rest = 0;
8885
+ }
8886
+ }
8767
8887
  update(dt) {
8888
+ this.tick++;
8768
8889
  const g = this.o.gravity;
8769
8890
  for (let i = this.list.length - 1; i >= 0; i--) {
8770
8891
  const f = this.list[i];
@@ -8779,7 +8900,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8779
8900
  }
8780
8901
  if (f.asleep) {
8781
8902
  f.rest += dt;
8782
- if (this.o.life > 0 && f.rest > this.o.life) f.dying = true;
8903
+ if (this.o.life > 0 && f.rest > this.o.life && !f.body) f.dying = true;
8783
8904
  continue;
8784
8905
  }
8785
8906
  this.step(f, dt, g);
@@ -8853,7 +8974,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8853
8974
  let small = 0;
8854
8975
  for (let i = 1; i < 3; i++) if (half[i] < half[small]) small = i;
8855
8976
  let axis2 = small;
8856
- if (Math.abs(up[small]) < 0.35) {
8977
+ if (Math.abs(up[small]) < 0.35 && !f.body) {
8857
8978
  axis2 = 0;
8858
8979
  for (let i = 1; i < 3; i++) if (Math.abs(up[i]) > Math.abs(up[axis2])) axis2 = i;
8859
8980
  }
@@ -8979,6 +9100,30 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8979
9100
  var SLOP = 2e-3;
8980
9101
  var NARROW = 0.3;
8981
9102
  var TOPPLE = 3;
9103
+ var TIP_ACCELERATION = 30;
9104
+ var TIP_LYING = 0.25;
9105
+ var TIP_SECONDS = 3;
9106
+ var STEP = 0.03;
9107
+ var boxTopAt = (s, x, z) => {
9108
+ const e = s.e;
9109
+ const dx = x - s.c[0], dz = z - s.c[2];
9110
+ let t0 = -Infinity, t1 = Infinity;
9111
+ for (let k = 0; k < 3; k++) {
9112
+ const p = e[k * 4] * dx + e[k * 4 + 2] * dz;
9113
+ const d = e[k * 4 + 1];
9114
+ const h = s.half[k];
9115
+ if (Math.abs(d) < 1e-9) {
9116
+ if (Math.abs(p) > h) return -Infinity;
9117
+ continue;
9118
+ }
9119
+ let a = (-h - p) / d, b = (h - p) / d;
9120
+ if (a > b) [a, b] = [b, a];
9121
+ t0 = Math.max(t0, a);
9122
+ t1 = Math.min(t1, b);
9123
+ if (t0 > t1) return -Infinity;
9124
+ }
9125
+ return s.c[1] + t1;
9126
+ };
8982
9127
  var PieceSystem = class {
8983
9128
  constructor(THREE, o) {
8984
9129
  __publicField(this, "THREE", THREE);
@@ -8990,6 +9135,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8990
9135
  __publicField(this, "dq");
8991
9136
  __publicField(this, "m");
8992
9137
  __publicField(this, "ax");
9138
+ __publicField(this, "solids", []);
8993
9139
  this.p = new THREE.Vector3();
8994
9140
  this.q = new THREE.Quaternion();
8995
9141
  this.dq = new THREE.Quaternion();
@@ -8999,6 +9145,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8999
9145
  get(objectId) {
9000
9146
  return this.list.find((b) => b.objectId === objectId) ?? null;
9001
9147
  }
9148
+ /** Sets a piece falling over, about the world axes x and z (a unit axis): its feet are thinner than the box that stands for it, so the box alone would hold it up. */
9149
+ tip(body, x, z) {
9150
+ body.tipping = { x, z, seconds: TIP_SECONDS };
9151
+ body.sleeping = false;
9152
+ }
9002
9153
  create(objectId, root, halfExtents, localCenter, mass, velocity, angular) {
9003
9154
  const existing = this.get(objectId);
9004
9155
  if (existing) {
@@ -9012,6 +9163,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9012
9163
  }
9013
9164
  const body = {
9014
9165
  objectId,
9166
+ tipping: null,
9015
9167
  root,
9016
9168
  velocity: velocity ? [...velocity] : [0, 0, 0],
9017
9169
  angularVelocity: angular ? [...angular] : [0, 0, 0],
@@ -9081,8 +9233,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9081
9233
  const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
9082
9234
  return [e[0] * lx + e[4] * ly + e[8] * lz, e[1] * lx + e[5] * ly + e[9] * lz, e[2] * lx + e[6] * ly + e[10] * lz];
9083
9235
  }
9236
+ /** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
9237
+ wakeRegion(x, z, radius) {
9238
+ for (const b of this.list) {
9239
+ if (!b.sleeping || b.held || Math.hypot(b.root.position.x - x, b.root.position.z - z) > radius) continue;
9240
+ b.sleeping = false;
9241
+ b.still = 0;
9242
+ }
9243
+ }
9084
9244
  update(dt) {
9085
9245
  this.time += dt;
9246
+ this.solids = this.list.map((body) => {
9247
+ const c = this.centerOf(body, new this.THREE.Vector3());
9248
+ const h = body.halfExtents;
9249
+ return { body, c: [c.x, c.y, c.z], e: [...new this.THREE.Matrix4().makeRotationFromQuaternion(body.root.quaternion).elements], half: h, r: Math.hypot(h[0], h[1], h[2]) };
9250
+ });
9086
9251
  for (const b of this.list) {
9087
9252
  if (b.held) {
9088
9253
  b.velocity = [0, 0, 0];
@@ -9096,6 +9261,17 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9096
9261
  }
9097
9262
  this.separate(dt);
9098
9263
  }
9264
+ /** Where a corner of `self` at `(x, y, z)` lands: the ground, the top of another piece or of something fixed under it. */
9265
+ supportAt(self, x, y, z, step) {
9266
+ let top = this.o.groundAt(x, z);
9267
+ for (const s of this.solids) {
9268
+ if (s.body === self || s.body.held || Math.abs(x - s.c[0]) > s.r || Math.abs(z - s.c[2]) > s.r) continue;
9269
+ const t = boxTopAt(s, x, z);
9270
+ if (t > top && t <= y + step) top = t;
9271
+ }
9272
+ const fixed = this.o.staticTopAt?.(x, z, y, step) ?? -Infinity;
9273
+ return Math.max(top, fixed);
9274
+ }
9099
9275
  integrate(b, dt) {
9100
9276
  const c = this.centerOf(b, this.p);
9101
9277
  const pos = [c.x, c.y, c.z];
@@ -9134,8 +9310,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9134
9310
  const sy = i & 2 ? hy : -hy;
9135
9311
  const sz = i & 4 ? hz : -hz;
9136
9312
  const r = [e[0] * sx + e[4] * sy + e[8] * sz, e[1] * sx + e[5] * sy + e[9] * sz, e[2] * sx + e[6] * sy + e[10] * sz];
9137
- const gy = this.o.groundAt(pos[0] + r[0], pos[2] + r[2]);
9138
- const pen = gy - (pos[1] + r[1]);
9313
+ const cy = pos[1] + r[1];
9314
+ const gy = this.supportAt(b, pos[0] + r[0], cy, pos[2] + r[2], STEP + Math.max(0, -v[1]) * h * 2);
9315
+ const pen = gy - cy;
9139
9316
  if (pen > -SLOP) {
9140
9317
  rs.push(r);
9141
9318
  if (pen > maxPen) maxPen = pen;
@@ -9158,7 +9335,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9158
9335
  }
9159
9336
  const lx = pos[0] + sx / rs.length;
9160
9337
  const lz = pos[2] + sz / rs.length;
9161
- landPoint = [lx, this.o.groundAt(lx, lz), lz];
9338
+ landPoint = [lx, pos[1] - Math.max(...rs.map((r) => -r[1])), lz];
9162
9339
  }
9163
9340
  for (let it = 0; it < 4; it++) {
9164
9341
  for (const r of rs) {
@@ -9211,6 +9388,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9211
9388
  const wx = x1 - x0;
9212
9389
  const wz = z1 - z0;
9213
9390
  unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
9391
+ if (b.tipping) {
9392
+ b.tipping.seconds -= h;
9393
+ if (height < TIP_LYING || b.tipping.seconds <= 0) b.tipping = null;
9394
+ else {
9395
+ w[0] += b.tipping.x * TIP_ACCELERATION * h;
9396
+ w[2] += b.tipping.z * TIP_ACCELERATION * h;
9397
+ unstable = true;
9398
+ }
9399
+ }
9214
9400
  if (unstable) {
9215
9401
  const dirX = x0 + x1 > 0 ? -1 : 1;
9216
9402
  const dirZ = z0 + z1 > 0 ? -1 : 1;
@@ -9247,6 +9433,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9247
9433
  b.still = 0;
9248
9434
  }
9249
9435
  }
9436
+ /** Half the height of the box of a piece as it is turned now. */
9437
+ halfHeight(b) {
9438
+ const e = this.m.makeRotationFromQuaternion(b.root.quaternion).elements;
9439
+ return Math.abs(e[1]) * b.halfExtents[0] + Math.abs(e[5]) * b.halfExtents[1] + Math.abs(e[9]) * b.halfExtents[2];
9440
+ }
9250
9441
  /** Push overlapping pieces apart (horizontal bounding circles), gently, so they do not interpenetrate forever. */
9251
9442
  separate(dt) {
9252
9443
  const n = this.list.length;
@@ -9265,6 +9456,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9265
9456
  this.centerOf(a, ca);
9266
9457
  this.centerOf(b, cb);
9267
9458
  if (Math.abs(ca.y - cb.y) > ra + rb) continue;
9459
+ if (Math.abs(ca.y - cb.y) >= (this.halfHeight(a) + this.halfHeight(b)) * 0.9) continue;
9268
9460
  const rh = (Math.max(a.halfExtents[0], a.halfExtents[2]) + Math.max(b.halfExtents[0], b.halfExtents[2])) * 0.8;
9269
9461
  let dx = cb.x - ca.x;
9270
9462
  let dz = cb.z - ca.z;
@@ -9660,14 +9852,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9660
9852
  smoke: "grey"
9661
9853
  },
9662
9854
  visual: {
9663
- charColor: 1709072
9855
+ charColor: 1709072,
9856
+ innerColor: 14730131
9664
9857
  },
9665
9858
  tags: [
9666
9859
  "wood",
9667
9860
  "organic",
9668
9861
  "flammable"
9669
9862
  ],
9670
- compressive: 1e5
9863
+ compressive: 1e5,
9864
+ hits: {
9865
+ blunt: {
9866
+ tear: "splinters",
9867
+ tearJoules: 60
9868
+ }
9869
+ }
9671
9870
  },
9672
9871
  {
9673
9872
  id: "wood_hard",
@@ -9697,14 +9896,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9697
9896
  smoke: "grey"
9698
9897
  },
9699
9898
  visual: {
9700
- charColor: 1709072
9899
+ charColor: 1709072,
9900
+ innerColor: 13213802
9701
9901
  },
9702
9902
  tags: [
9703
9903
  "wood",
9704
9904
  "organic",
9705
9905
  "flammable"
9706
9906
  ],
9707
- compressive: 15e4
9907
+ compressive: 15e4,
9908
+ hits: {
9909
+ blunt: {
9910
+ tear: "splinters",
9911
+ tearJoules: 150
9912
+ }
9913
+ }
9708
9914
  },
9709
9915
  {
9710
9916
  id: "plywood",
@@ -9734,14 +9940,24 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9734
9940
  smoke: "grey"
9735
9941
  },
9736
9942
  visual: {
9737
- charColor: 1709072
9943
+ charColor: 1709072,
9944
+ innerColor: 14862234
9738
9945
  },
9739
9946
  tags: [
9740
9947
  "wood",
9741
9948
  "organic",
9742
9949
  "flammable"
9743
9950
  ],
9744
- compressive: 6e4
9951
+ compressive: 6e4,
9952
+ hits: {
9953
+ ballistic: {
9954
+ tear: "splinters"
9955
+ },
9956
+ blunt: {
9957
+ tear: "splinters",
9958
+ tearJoules: 80
9959
+ }
9960
+ }
9745
9961
  },
9746
9962
  {
9747
9963
  id: "mdf",
@@ -9768,14 +9984,25 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9768
9984
  smoke: "grey"
9769
9985
  },
9770
9986
  visual: {
9771
- charColor: 1709072
9987
+ charColor: 1709072,
9988
+ innerColor: 12096362
9772
9989
  },
9773
9990
  tags: [
9774
9991
  "wood",
9775
9992
  "organic",
9776
9993
  "flammable"
9777
9994
  ],
9778
- compressive: 3e4
9995
+ compressive: 3e4,
9996
+ hits: {
9997
+ ballistic: {
9998
+ tear: "lumps",
9999
+ tearJoules: 120
10000
+ },
10001
+ blunt: {
10002
+ tear: "lumps",
10003
+ tearJoules: 20
10004
+ }
10005
+ }
9779
10006
  },
9780
10007
  {
9781
10008
  id: "glass",
@@ -9889,8 +10116,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9889
10116
  damageScale: {
9890
10117
  explosive: 1.2
9891
10118
  },
10119
+ hits: {
10120
+ ballistic: {
10121
+ unitJoules: 500,
10122
+ floor: 900,
10123
+ crater: true
10124
+ },
10125
+ blunt: {
10126
+ unitJoules: 1500,
10127
+ floor: 60
10128
+ }
10129
+ },
9892
10130
  hitDebris: "chips",
9893
10131
  breakDebris: "dust",
10132
+ visual: { innerColor: 10197652 },
9894
10133
  thermal: {
9895
10134
  conductivity: 0.15,
9896
10135
  heatCapacity: 2.5
@@ -9921,6 +10160,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9921
10160
  ],
9922
10161
  hitDebris: "chips",
9923
10162
  breakDebris: "dust",
10163
+ visual: { innerColor: 12085567 },
9924
10164
  thermal: {
9925
10165
  conductivity: 0.15,
9926
10166
  heatCapacity: 2.5
@@ -9951,6 +10191,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9951
10191
  ],
9952
10192
  hitDebris: "dust",
9953
10193
  breakDebris: "dust",
10194
+ visual: { innerColor: 14999507 },
9954
10195
  thermal: {
9955
10196
  conductivity: 0.1,
9956
10197
  heatCapacity: 1.5
@@ -9976,6 +10217,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9976
10217
  ],
9977
10218
  hitDebris: "chips",
9978
10219
  breakDebris: "dust",
10220
+ visual: { innerColor: 9407364 },
9979
10221
  thermal: {
9980
10222
  conductivity: 0.2,
9981
10223
  heatCapacity: 2.5
@@ -10257,7 +10499,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10257
10499
  smoke: "white"
10258
10500
  },
10259
10501
  visual: {
10260
- charColor: 1709072
10502
+ charColor: 1709072,
10503
+ innerColor: 14207126
10261
10504
  },
10262
10505
  tags: [
10263
10506
  "plant",
@@ -10265,7 +10508,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10265
10508
  "organic",
10266
10509
  "flammable"
10267
10510
  ],
10268
- compressive: 6e4
10511
+ compressive: 6e4,
10512
+ hits: {
10513
+ blunt: {
10514
+ tear: "none"
10515
+ }
10516
+ }
10269
10517
  },
10270
10518
  {
10271
10519
  id: "cactus",
@@ -10380,6 +10628,130 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10380
10628
  "masonry"
10381
10629
  ],
10382
10630
  compressive: 2e4
10631
+ },
10632
+ {
10633
+ id: "asphalt",
10634
+ density: 2350,
10635
+ hpPerKg: 10,
10636
+ penetration: 14,
10637
+ penetrationClass: "concrete",
10638
+ penetrationScale: 0.8,
10639
+ ductility: 0.1,
10640
+ fracture: "crumble",
10641
+ fragmentSize: [
10642
+ 0.03,
10643
+ 0.15
10644
+ ],
10645
+ hitDebris: "chips",
10646
+ breakDebris: "dust",
10647
+ thermal: {
10648
+ conductivity: 0.1,
10649
+ heatCapacity: 2,
10650
+ ignitionC: 400,
10651
+ burnRate: 2e-3,
10652
+ smoke: "black"
10653
+ },
10654
+ tags: [
10655
+ "masonry",
10656
+ "brittle"
10657
+ ],
10658
+ compressive: 3e4
10659
+ },
10660
+ {
10661
+ id: "sand",
10662
+ density: 1600,
10663
+ hpPerKg: 2,
10664
+ penetration: 6,
10665
+ ductility: 0.4,
10666
+ fracture: "crumble",
10667
+ fragmentSize: [
10668
+ 0.01,
10669
+ 0.04
10670
+ ],
10671
+ hitDebris: "dust",
10672
+ breakDebris: "dust",
10673
+ thermal: {
10674
+ conductivity: 0.1,
10675
+ heatCapacity: 2
10676
+ },
10677
+ tags: [
10678
+ "soil"
10679
+ ],
10680
+ compressive: 1e4
10681
+ },
10682
+ {
10683
+ id: "gravel",
10684
+ density: 1800,
10685
+ hpPerKg: 3,
10686
+ penetration: 8,
10687
+ ductility: 0.3,
10688
+ fracture: "crumble",
10689
+ fragmentSize: [
10690
+ 0.02,
10691
+ 0.06
10692
+ ],
10693
+ hitDebris: "chips",
10694
+ breakDebris: "dust",
10695
+ thermal: {
10696
+ conductivity: 0.1,
10697
+ heatCapacity: 2
10698
+ },
10699
+ tags: [
10700
+ "soil",
10701
+ "masonry"
10702
+ ],
10703
+ compressive: 2e4
10704
+ },
10705
+ {
10706
+ id: "turf",
10707
+ density: 900,
10708
+ hpPerKg: 3,
10709
+ penetration: 4,
10710
+ ductility: 0.5,
10711
+ fracture: "crumble",
10712
+ fragmentSize: [
10713
+ 0.03,
10714
+ 0.1
10715
+ ],
10716
+ hitDebris: "dust",
10717
+ breakDebris: "dust",
10718
+ thermal: {
10719
+ conductivity: 0.1,
10720
+ heatCapacity: 1.5,
10721
+ ignitionC: 250,
10722
+ burnRate: 0.03,
10723
+ smoke: "white"
10724
+ },
10725
+ tags: [
10726
+ "soil",
10727
+ "organic"
10728
+ ],
10729
+ compressive: 15e3
10730
+ },
10731
+ {
10732
+ id: "mortar",
10733
+ density: 1900,
10734
+ hpPerKg: 10,
10735
+ penetration: 12,
10736
+ penetrationClass: "concrete",
10737
+ penetrationScale: 1.2,
10738
+ ductility: 0.03,
10739
+ fracture: "crumble",
10740
+ fragmentSize: [
10741
+ 0.02,
10742
+ 0.08
10743
+ ],
10744
+ hitDebris: "chips",
10745
+ breakDebris: "dust",
10746
+ thermal: {
10747
+ conductivity: 0.15,
10748
+ heatCapacity: 2.5
10749
+ },
10750
+ tags: [
10751
+ "masonry",
10752
+ "brittle"
10753
+ ],
10754
+ compressive: 1e5
10383
10755
  }
10384
10756
  ];
10385
10757
  var material_hints_default = {
@@ -10575,6 +10947,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10575
10947
  const fill = options.fillOf?.(o, material);
10576
10948
  const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
10577
10949
  const contains = containsOf(ud.destructionContains);
10950
+ const thickness = typeof ud.destructionThickness === "number" && ud.destructionThickness > 0 ? ud.destructionThickness : void 0;
10578
10951
  const existing = explicit ? byKey.get(explicit) : void 0;
10579
10952
  if (existing) {
10580
10953
  const before = sizeOf(existing.box);
@@ -10587,8 +10960,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10587
10960
  existing.skin = existing.skin && skin;
10588
10961
  if (fill !== void 0) existing.fill = fill;
10589
10962
  if (contains) existing.contains = contains;
10963
+ if (thickness !== void 0) existing.thickness = Math.min(existing.thickness ?? thickness, thickness);
10590
10964
  } else {
10591
- const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
10965
+ const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, ...thickness !== void 0 ? { thickness } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
10592
10966
  entries.push(e);
10593
10967
  if (explicit) byKey.set(explicit, e);
10594
10968
  }
@@ -10656,7 +11030,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10656
11030
  const minSide = Math.min(...size);
10657
11031
  if (e.fill !== void 0) part.fill = e.fill;
10658
11032
  else if (e.hollow) part.fill = 0.05;
10659
- if (minSide < 0.01 && METAL.test(e.material)) {
11033
+ if (e.thickness !== void 0) part.thickness = e.thickness;
11034
+ else if (minSide < 0.01 && METAL.test(e.material)) {
10660
11035
  part.fill = e.fill ?? 1;
10661
11036
  part.thickness = Math.max(minSide, 5e-4);
10662
11037
  }
@@ -10747,9 +11122,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10747
11122
  const ids = [0, 1, 2].map((corner) => index ? index.getX(t * 3 + corner) : t * 3 + corner);
10748
11123
  ids.forEach((id, corner) => corners[corner].set(shell.before[id * 3], shell.before[id * 3 + 1], shell.before[id * 3 + 2]));
10749
11124
  triangle.set(corners[0], corners[1], corners[2]);
11125
+ if (triangle.getArea() <= 1e-14) continue;
10750
11126
  triangle.closestPointToPoint(local2, closest);
10751
11127
  const distance = closest.distanceToSquared(local2);
10752
- if (distance >= nearest) continue;
11128
+ if (!(distance < nearest)) continue;
10753
11129
  nearest = distance;
10754
11130
  nearestScale = shell.mesh.matrixWorld.getMaxScaleOnAxis() || 1;
10755
11131
  triangle.getBarycoord(closest, weights);
@@ -10928,6 +11304,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10928
11304
  return out;
10929
11305
  }
10930
11306
  /** Box of the part in world space. */
11307
+ /** Appends the boxes that follow the mesh in the world (`pushFittedBoxes`) to `out`. */
11308
+ fittedBoxes(mv, out) {
11309
+ const box = new this.THREE.Box3();
11310
+ if (mv.host) this.localBox(mv, box);
11311
+ else {
11312
+ const geometry = mv.mesh.geometry;
11313
+ if (!geometry.boundingBox) geometry.computeBoundingBox();
11314
+ if (geometry.boundingBox) box.copy(geometry.boundingBox);
11315
+ else box.makeEmpty();
11316
+ }
11317
+ if (box.isEmpty()) return;
11318
+ mv.mesh.updateWorldMatrix(true, false);
11319
+ pushFittedBoxes(box, mv.mesh.matrixWorld, out);
11320
+ }
10931
11321
  worldBox(mv, out) {
10932
11322
  if (!mv.host) return out.setFromObject(mv.mesh);
10933
11323
  this.localBox(mv, out);
@@ -11191,6 +11581,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11191
11581
  d.geometry?.dispose();
11192
11582
  this.decalCount = Math.max(0, this.decalCount - 1);
11193
11583
  }
11584
+ /**
11585
+ * Takes away the decals of a mesh that sit inside a box (world `center`, `half` extents along the axes of the world `rotation`): the surface they
11586
+ * marked is gone (a unit came out of the wall with its bullet holes and spalls).
11587
+ */
11588
+ clearDecals(mv, center, half, rotation) {
11589
+ const inv = rotation.clone().invert();
11590
+ const p = new this.THREE.Vector3();
11591
+ for (let i = mv.decals.length - 1; i >= 0; i--) {
11592
+ const d = mv.decals[i];
11593
+ d.obj.getWorldPosition(p).sub(center).applyQuaternion(inv);
11594
+ if (Math.abs(p.x) > half.x || Math.abs(p.y) > half.y || Math.abs(p.z) > half.z) continue;
11595
+ this.disposeDecal(d);
11596
+ mv.decals.splice(i, 1);
11597
+ }
11598
+ }
11194
11599
  addHole(mv, worldPoint, worldNormal, diameter) {
11195
11600
  const { s, pos, quat: quat2 } = this.decalPose(mv.mesh, worldPoint, worldNormal, 25e-4);
11196
11601
  const work = mv.host ? mv.host.work : mv.work;
@@ -11673,9 +12078,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11673
12078
  // src/renderer.ts
11674
12079
  var rendererCount = 0;
11675
12080
  var COLLAPSE_CLOUD_KG = 1e3;
12081
+ var CHIP_DECAL_EDGE = 0.05;
11676
12082
  var IMPACT_TAGS = ["metal", "glass", "wood"];
11677
12083
  var GLOW_FROM_C = 430;
11678
12084
  var QUEUE_CAP = 4e3;
12085
+ var STATIC_REFRESH = 0.5;
12086
+ var STATIC_SPAN = 3;
12087
+ var FEET_BAND = 0.03;
12088
+ var TIP_MIN_HEIGHT = 0.3;
12089
+ var TIP_NARROW = 0.3;
11679
12090
  var FIRE_CEILING_SECONDS = 3600;
11680
12091
  var GENERIC_REFERENCE_RADIUS = 15;
11681
12092
  var PRIMED_FIRE_RADIUS = 0.2;
@@ -11720,6 +12131,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11720
12131
  if (def.fracture === "tear") return "impact_soft";
11721
12132
  return def.fracture === "none" ? "impact_metal" : "impact_wood";
11722
12133
  }
12134
+ var dragSoundKey = (def) => impactKey(def).replace("impact_", "drag_");
11723
12135
  function fractureKey(def, style) {
11724
12136
  switch (style) {
11725
12137
  case "shatter":
@@ -11761,6 +12173,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11761
12173
  __publicField(this, "autoSplit");
11762
12174
  __publicField(this, "maxFragments");
11763
12175
  __publicField(this, "bounds", /* @__PURE__ */ new Map());
12176
+ /** World boxes (min xyz, max xyz) of the drawn meshes of the bound objects that are not pieces, for the pieces to land on. */
12177
+ __publicField(this, "staticBoxes", /* @__PURE__ */ new Map());
12178
+ /** Objects to measure again; `staticAll` measures every one. */
12179
+ __publicField(this, "staticDirty", /* @__PURE__ */ new Set());
12180
+ __publicField(this, "staticAll", true);
12181
+ /** Boxes of the pieces' meshes (as `staticBoxes`), with the time they were measured. */
12182
+ __publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
12183
+ __publicField(this, "staticAt", 0);
11764
12184
  __publicField(this, "queue", []);
11765
12185
  __publicField(this, "unsubscribe");
11766
12186
  __publicField(this, "visuals");
@@ -11822,6 +12242,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11822
12242
  this.cages = new RebarCages(THREE, o.scene);
11823
12243
  this.pieceSys = new PieceSystem(THREE, {
11824
12244
  groundAt: this.groundAt,
12245
+ staticTopAt: (x, z, y, step) => this.staticTopAt(x, z, y, step),
11825
12246
  gravity: this.gravity,
11826
12247
  scene: o.scene,
11827
12248
  onHardLanding: (p, point, speed) => this.hardLanding(p, point, speed)
@@ -11834,6 +12255,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11834
12255
  // Binding
11835
12256
  // ----------------------------------------------------------------------------------------------------------------------------------
11836
12257
  bind(objectId, root, options = {}) {
12258
+ this.staticDirty.add(objectId);
11837
12259
  if (this.bounds.has(objectId)) this.unbind(objectId);
11838
12260
  const state = this.world.state(objectId);
11839
12261
  const partIds = new Set((state?.parts ?? []).map((p) => p.id));
@@ -11953,6 +12375,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11953
12375
  }
11954
12376
  }
11955
12377
  unbind(objectId) {
12378
+ this.staticDirty.add(objectId);
11956
12379
  const b = this.bounds.get(objectId);
11957
12380
  if (!b) return;
11958
12381
  this.restore(objectId);
@@ -11992,6 +12415,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11992
12415
  return m > 0 ? m : 5;
11993
12416
  }
11994
12417
  makePiece(objectId, options = {}) {
12418
+ this.staticDirty.add(objectId);
11995
12419
  const b = this.bounds.get(objectId);
11996
12420
  if (!b) throw new Error(`game-destruction-three: object "${objectId}" is not bound`);
11997
12421
  if (b.root.parent && b.root.parent !== this.scene) this.scene.attach(b.root);
@@ -12104,7 +12528,100 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12104
12528
  // ----------------------------------------------------------------------------------------------------------------------------------
12105
12529
  // Events
12106
12530
  // ----------------------------------------------------------------------------------------------------------------------------------
12531
+ /**
12532
+ * The highest top of a bound object that is not a piece under (x, z) a corner at height `y` can land on (at most `step` above it). The world boxes
12533
+ * of their drawn meshes are kept until an event or a moved root changes them; parts drawn inside a merged mesh and meshes wider than `STATIC_SPAN`
12534
+ * are left out (walls, floors, roofs: the terrain under them is the host's `groundAt`).
12535
+ */
12536
+ staticTopAt(x, z, y, step) {
12537
+ let top = -Infinity;
12538
+ for (const a of this.staticNow().values()) {
12539
+ if (a.length === 0 || x < a[0] || x > a[3] || z < a[2] || z > a[5]) continue;
12540
+ for (let i = 6; i < a.length; i += 6) {
12541
+ if (x < a[i] || x > a[i + 3] || z < a[i + 2] || z > a[i + 5]) continue;
12542
+ const t = a[i + 4];
12543
+ if (t > top && t <= y + step) top = t;
12544
+ }
12545
+ }
12546
+ return top;
12547
+ }
12548
+ /** The boxes of the fixed objects (per object: the box around them all, then each one), each object measured again when an event touched it (all of them every `STATIC_REFRESH` s). */
12549
+ staticNow() {
12550
+ if (this.time - this.staticAt > STATIC_REFRESH) {
12551
+ this.staticAt = this.time;
12552
+ this.staticAll = true;
12553
+ }
12554
+ if (!this.staticAll && this.staticDirty.size === 0) return this.staticBoxes;
12555
+ const box = new this.THREE.Box3();
12556
+ const measure = (b) => {
12557
+ if (b.piece || this.pieceSys.get(b.id)) {
12558
+ this.staticBoxes.delete(b.id);
12559
+ return;
12560
+ }
12561
+ const out = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];
12562
+ for (const mv of this.meshesOf(b)) {
12563
+ if (mv.host || mv.loose || !this.visuals.shown(mv)) continue;
12564
+ this.visuals.worldBox(mv, box);
12565
+ if (box.isEmpty() || box.max.x - box.min.x > STATIC_SPAN || box.max.z - box.min.z > STATIC_SPAN) continue;
12566
+ this.visuals.fittedBoxes(mv, out);
12567
+ for (let k = 0; k < 3; k++) {
12568
+ out[k] = Math.min(out[k], box.min.getComponent(k));
12569
+ out[k + 3] = Math.max(out[k + 3], box.max.getComponent(k));
12570
+ }
12571
+ }
12572
+ this.staticBoxes.set(b.id, out.length > 6 ? out : []);
12573
+ };
12574
+ if (this.staticAll) {
12575
+ this.staticBoxes.clear();
12576
+ for (const b of this.bounds.values()) measure(b);
12577
+ } else {
12578
+ for (const id of this.staticDirty) {
12579
+ const b = this.bounds.get(id);
12580
+ if (b) measure(b);
12581
+ else this.staticBoxes.delete(id);
12582
+ }
12583
+ }
12584
+ this.staticAll = false;
12585
+ this.staticDirty.clear();
12586
+ return this.staticBoxes;
12587
+ }
12588
+ /** The boxes of a piece's drawn meshes, measured once per frame. */
12589
+ pieceBoxesOf(pc) {
12590
+ const cached = this.pieceBoxes.get(pc);
12591
+ if (cached && cached.at === this.time) return cached.boxes;
12592
+ const boxes = [];
12593
+ const b = this.bounds.get(pc.objectId);
12594
+ if (b) {
12595
+ for (const mv of this.meshesOf(b)) {
12596
+ if (this.visuals.shown(mv)) this.visuals.fittedBoxes(mv, boxes);
12597
+ }
12598
+ }
12599
+ this.pieceBoxes.set(pc, { at: this.time, boxes });
12600
+ return boxes;
12601
+ }
12602
+ solidsNear(x, z, radius) {
12603
+ const found = [];
12604
+ const scan = (a, id, piece, from) => {
12605
+ if (from > 0 && (a.length === 0 || x < a[0] - radius || x > a[3] + radius || z < a[2] - radius || z > a[5] + radius)) return;
12606
+ for (let i = from; i < a.length; i += 6) {
12607
+ if (x < a[i] - radius || x > a[i + 3] + radius || z < a[i + 2] - radius || z > a[i + 5] + radius) continue;
12608
+ found.push({ objectId: id, min: [a[i], a[i + 1], a[i + 2]], max: [a[i + 3], a[i + 4], a[i + 5]], piece });
12609
+ }
12610
+ };
12611
+ for (const [id, a] of this.staticNow()) scan(a, id, null, 6);
12612
+ const c = this.v1;
12613
+ for (const pc of this.pieceSys.list) {
12614
+ if (pc.held) continue;
12615
+ this.pieceSys.centerOf(pc, c);
12616
+ const reach = Math.hypot(pc.halfExtents[0], pc.halfExtents[1], pc.halfExtents[2]) + radius;
12617
+ if (Math.abs(c.x - x) > reach || Math.abs(c.z - z) > reach) continue;
12618
+ scan(this.pieceBoxesOf(pc), pc.objectId, pc, 0);
12619
+ }
12620
+ this.fragments.solidsNear(x, z, radius, (home, min2, max2, loose) => found.push({ objectId: home, min: min2, max: max2, piece: null, loose }));
12621
+ return found;
12622
+ }
12107
12623
  apply(ev) {
12624
+ this.staticDirty.add(ev.object);
12108
12625
  switch (ev.type) {
12109
12626
  case "damage":
12110
12627
  if (ev.damageType === "heat" || ev.damageType === "fire" || ev.damageType === "explosive") this.activate(ev.object, ev.part);
@@ -12125,7 +12642,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12125
12642
  break;
12126
12643
  }
12127
12644
  case "debris":
12128
- this.vfx.burst(ev.kind, ev.point, ev.normal, ev.count, ev.speed > 0 ? ev.speed : DEFAULT_SPEED[ev.kind] ?? 1, this.debrisOptions(ev.kind, ev.object, ev.part));
12645
+ this.vfx.burst(ev.kind, ev.point, ev.normal, ev.count, ev.speed > 0 ? ev.speed : DEFAULT_SPEED[ev.kind] ?? 1, this.debrisOptions(ev.kind, ev.object, ev.part, ev.material));
12129
12646
  break;
12130
12647
  case "fracture":
12131
12648
  this.onFracture(ev);
@@ -12298,10 +12815,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12298
12815
  }
12299
12816
  return def ? fallbackColor(def.id, def) : null;
12300
12817
  }
12301
- debrisOptions(kind, objectId, partId) {
12818
+ debrisOptions(kind, objectId, partId, material) {
12302
12819
  const pv = this.partVis(objectId, partId);
12303
- const def = pv ? this.world.materials.get(pv.material) : void 0;
12820
+ const def = this.world.materials.get(material ?? pv?.material ?? "");
12304
12821
  if (kind === "smoke") return { color: SMOKE_TONES[def?.thermal.smoke ?? "grey"] };
12822
+ if ((kind === "dust" || kind === "chips") && def?.visual?.innerColor !== void 0) return { color: def.visual.innerColor };
12305
12823
  if (kind === "fibers" || kind === "leaves") {
12306
12824
  if (def?.visual?.innerColor !== void 0) return { color: def.visual.innerColor };
12307
12825
  const c = pv ? this.visuals.baseColorHex(pv.meshes[0] ?? {}) : -1;
@@ -12409,8 +12927,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12409
12927
  const mv = this.nearestMesh(pv, wp);
12410
12928
  if (!mv) return;
12411
12929
  const def = this.world.materials.get(pv.material);
12412
- this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.point);
12413
- this.visuals.addSpall(mv, wp, this.faceNormal(mv, wp), Math.sqrt(ev.area / Math.PI), fallbackColor(pv.material, def));
12930
+ const normal = this.faceNormal(mv, wp);
12931
+ const core = def?.visual?.innerColor;
12932
+ if (core !== void 0) this.vfx.burst("chips", ev.point, [normal.x, normal.y, normal.z], 5, 2.5, { color: core });
12933
+ else this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.point);
12934
+ this.visuals.addSpall(mv, wp, normal, Math.sqrt(ev.area / Math.PI), core ?? fallbackColor(pv.material, def));
12414
12935
  }
12415
12936
  /**
12416
12937
  * Stage 2 of a staged part: the unit-sized fragments come out and the part stays drawn (hiding it is what `fracture` does). The opening itself
@@ -12433,16 +12954,27 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12433
12954
  this.sound(impactKey(def), ev.opening.center, 0.8);
12434
12955
  }
12435
12956
  /**
12436
- * A unit came out of a wall of units: the unit flies as a fragment with a puff of its dust. The gap itself is the drawing's (the house renderer redraws
12437
- * the wall as its courses without that unit); a part drawn as one box keeps its box.
12957
+ * A unit came out of a wall of units: the unit flies as a fragment with a puff of its dust, and the marks drawn on it (bullet holes, spalls, the impact
12958
+ * marks of the effects) go with it. The gap itself is the drawing's (the house renderer cuts the wall where the unit was); a part drawn as one box
12959
+ * keeps its box.
12438
12960
  */
12439
12961
  onChip(ev) {
12440
12962
  const pv = this.partVis(ev.object, ev.part);
12441
12963
  const def = pv ? this.world.materials.get(pv.material) : void 0;
12442
12964
  const color = (pv ? this.partColor(pv, def) : null) ?? fallbackColor(pv?.material ?? "", def);
12443
12965
  const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
12444
- for (const fr of ev.fragments) this.fragments.spawn(fr, this.fragments.materialFor(fr.material, fallbackColor(fr.material, def), false), ev.object, maxAlive);
12445
- if (!this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.center)) this.vfx.burst("dust", ev.center, [0, 1, 0], 6, 0.8, { color });
12966
+ for (const fr of ev.fragments) {
12967
+ const fdef = this.world.materials.get(fr.material);
12968
+ const tone = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
12969
+ this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone, false), ev.object, maxAlive);
12970
+ }
12971
+ const T = this.THREE;
12972
+ const center = new T.Vector3(ev.center[0], ev.center[1], ev.center[2]);
12973
+ const half = new T.Vector3(ev.size[0] / 2 + CHIP_DECAL_EDGE, ev.size[1] / 2 + CHIP_DECAL_EDGE, ev.size[2] / 2 + CHIP_DECAL_EDGE);
12974
+ const rotation = new T.Quaternion(ev.rotation[0], ev.rotation[1], ev.rotation[2], ev.rotation[3]);
12975
+ for (const part of this.bounds.get(ev.object)?.parts.values() ?? []) for (const mv of part.meshes) this.visuals.clearDecals(mv, center, half, rotation);
12976
+ this.vfx.peekWorkshop()?.decals.clearInside(ev.center, [half.x, half.y, half.z], ev.rotation);
12977
+ this.vfx.burst("dust", ev.center, [0, 1, 0], 8, 0.8, { color });
12446
12978
  this.sound(impactKey(def), ev.center, 0.7);
12447
12979
  }
12448
12980
  onFracture(ev) {
@@ -12522,6 +13054,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12522
13054
  this.pieceSys.setCenter(old, center);
12523
13055
  old.mass = Math.max(0.05, this.massOf(objectId));
12524
13056
  old.sleeping = false;
13057
+ } else if (kitOriginOf(b.root)?.kind === "furniture") {
13058
+ this.makePiece(objectId);
13059
+ this.tipIfTopHeavy(b);
12525
13060
  }
12526
13061
  }
12527
13062
  /** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
@@ -12568,8 +13103,32 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12568
13103
  }
12569
13104
  const piece = this.pieceSys.create(newId, group, half, center, this.massOf(newId), vel, ang);
12570
13105
  nb.piece = piece;
13106
+ if (kitOriginOf(from.root)?.kind === "furniture") this.tipIfTopHeavy(nb);
12571
13107
  this.o.onPiece?.(piece);
12572
13108
  }
13109
+ /**
13110
+ * A piece of furniture standing on feet too close for its height (a board on two legs in a line, a lone leg) does not stay up: the box of a piece hides that its weight is up
13111
+ * high and its feet are thin, so it gets the start of the fall, across the line of its feet.
13112
+ */
13113
+ tipIfTopHeavy(b) {
13114
+ const piece = b.piece;
13115
+ if (!piece) return;
13116
+ const box = new this.THREE.Box3();
13117
+ const boxes = this.meshesOf(b).filter((mv) => this.visuals.shown(mv)).map((mv) => this.visuals.worldBox(mv, new this.THREE.Box3()));
13118
+ if (boxes.length === 0) return;
13119
+ for (const one of boxes) box.union(one);
13120
+ const height = box.max.y - box.min.y;
13121
+ const feet = new this.THREE.Box3();
13122
+ for (const one of boxes) if (one.min.y < box.min.y + FEET_BAND) feet.union(one);
13123
+ const spanX = feet.max.x - feet.min.x;
13124
+ const spanZ = feet.max.z - feet.min.z;
13125
+ if (height < TIP_MIN_HEIGHT || Math.min(spanX, spanZ) >= TIP_NARROW * height) return;
13126
+ const across = spanX <= spanZ ? 0 : 2;
13127
+ const lean = across === 0 ? box.min.x + box.max.x - feet.min.x - feet.max.x : box.min.z + box.max.z - feet.min.z - feet.max.z;
13128
+ const away = lean < 0 ? -1 : 1;
13129
+ if (across === 0) this.pieceSys.tip(piece, 0, -away);
13130
+ else this.pieceSys.tip(piece, away, 0);
13131
+ }
12573
13132
  onExplode(ev, options = {}) {
12574
13133
  const p = ev.point;
12575
13134
  if (options.visual !== false) this.playExplosion(ev, options);
@@ -12820,6 +13379,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12820
13379
  this.vfx.sprites().clear();
12821
13380
  this.ownVfx?.world.clear();
12822
13381
  }
13382
+ wakeRegion(x, z, radius) {
13383
+ this.fragments.wakeRegion(x, z, radius);
13384
+ this.pieceSys.wakeRegion(x, z, radius);
13385
+ }
12823
13386
  stats() {
12824
13387
  return { fragments: this.fragments.count, particles: this.vfx.sprites().live + this.ownedVfxParticles(), pieces: this.pieceSys.list.length, decals: this.visuals.decalCount + this.ground.count };
12825
13388
  }
@@ -13112,7 +13675,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13112
13675
  volume: 0,
13113
13676
  sources: ["cena:L1316 fire loop (silent in the page: its gain is never raised)", "sdk: the level while burning"]
13114
13677
  };
13115
- var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
13678
+ var drag = (id, layers) => ({ ...sceneRecipe(id, layers, ""), sources: ["sdk: a base dragged on a floor, one grain per stretch"] });
13679
+ var DRAG_SOUNDS = [
13680
+ drag("drag_wood", [sceneNoise({ type: "bandpass", f: 380, q: 1.4, d: 0.28, vol: 0.5 }), sceneNoise({ type: "bandpass", f: [900, 1300], q: 3, d: 0.08, vol: 0.14 })]),
13681
+ drag("drag_metal", [sceneNoise({ type: "bandpass", f: 1500, q: 4, d: 0.26, vol: 0.32 }), sceneTone({ f: [880, 1240], f2: 760, d: 0.18, vol: 0.05 })]),
13682
+ drag("drag_glass", [sceneNoise({ type: "bandpass", f: 2600, q: 5, d: 0.2, vol: 0.22 })]),
13683
+ drag("drag_masonry", [sceneNoise({ type: "bandpass", f: 260, q: 0.9, d: 0.3, vol: 0.62 }), ...sceneMany(3, () => sceneNoise({ type: "bandpass", f: [600, 1200], q: 2, d: 0.04, vol: 0.14, delay: [0, 0.2] }))]),
13684
+ drag("drag_soft", [sceneNoise({ type: "lowpass", f: 420, d: 0.28, vol: 0.3 })])
13685
+ ];
13686
+ var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
13116
13687
 
13117
13688
  // src/audio/sound.ts
13118
13689
  var DESTRUCTION_SOUND_KEYS = {
@@ -13122,6 +13693,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13122
13693
  impact_glass: { sound: "ping" },
13123
13694
  impact_masonry: { sound: "chip" },
13124
13695
  impact_soft: { sound: "thud" },
13696
+ drag_wood: { sound: "drag_wood" },
13697
+ drag_metal: { sound: "drag_metal" },
13698
+ drag_glass: { sound: "drag_glass" },
13699
+ drag_masonry: { sound: "drag_masonry" },
13700
+ drag_soft: { sound: "drag_soft" },
13125
13701
  shatter_glass: { sound: "glass_shatter" },
13126
13702
  crumble: { sound: "crumble" },
13127
13703
  splinter: { sound: "crack" },