@adatechnology/game-destruction-three 0.10.0 → 0.11.0

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
 
@@ -8589,6 +8592,44 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8589
8592
  }
8590
8593
  var LIGHT_BUDGET = EVENT_BUDGET.blastLights + EVENT_BUDGET.fireLights;
8591
8594
 
8595
+ // src/fitted-boxes.ts
8596
+ var SLICE_LENGTH = 0.12;
8597
+ var MAX_SLICES = 12;
8598
+ var SNUG_RATIO = 1.1;
8599
+ function pushFittedBoxes(local2, matrix, out) {
8600
+ const e = matrix.elements;
8601
+ 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);
8602
+ const lengths = [0, 1, 2].map((k) => sizes[k] * Math.hypot(e[k * 4], e[k * 4 + 1], e[k * 4 + 2]));
8603
+ const longest = lengths.indexOf(Math.max(...lengths));
8604
+ const whole = worldBoxOf(local2, matrix, longest, 0, 1);
8605
+ const span = (whole[3] - whole[0]) * (whole[4] - whole[1]) * (whole[5] - whole[2]);
8606
+ const volume = lengths[0] * lengths[1] * lengths[2];
8607
+ const slices = span <= volume * SNUG_RATIO ? 1 : Math.min(MAX_SLICES, Math.ceil(lengths[longest] / SLICE_LENGTH));
8608
+ if (slices <= 1) {
8609
+ out.push(...whole);
8610
+ return;
8611
+ }
8612
+ for (let i = 0; i < slices; i++) out.push(...worldBoxOf(local2, matrix, longest, i / slices, (i + 1) / slices));
8613
+ }
8614
+ function worldBoxOf(local2, matrix, axis2, from, to) {
8615
+ const e = matrix.elements;
8616
+ const low = [local2.min.x, local2.min.y, local2.min.z];
8617
+ const high = [local2.max.x, local2.max.y, local2.max.z];
8618
+ const start = low[axis2];
8619
+ const size = high[axis2] - start;
8620
+ low[axis2] = start + size * from;
8621
+ high[axis2] = start + size * to;
8622
+ const box = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];
8623
+ for (const x of [low[0], high[0]]) for (const y of [low[1], high[1]]) for (const z of [low[2], high[2]]) {
8624
+ for (let k = 0; k < 3; k++) {
8625
+ const v = e[k] * x + e[k + 4] * y + e[k + 8] * z + e[k + 12];
8626
+ box[k] = Math.min(box[k], v);
8627
+ box[k + 3] = Math.max(box[k + 3], v);
8628
+ }
8629
+ }
8630
+ return box;
8631
+ }
8632
+
8592
8633
  // src/math.ts
8593
8634
  function createRng(seed) {
8594
8635
  let a = seed >>> 0;
@@ -8630,12 +8671,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8630
8671
  var SHEET_MAX = 4e-3;
8631
8672
  var SHEET_AIR = 2.5;
8632
8673
  var SHEET_SWAY = 3;
8674
+ var BIG_LENGTH = 0.2;
8675
+ var BIG_WIDTH = 0.03;
8676
+ var isBig = (size) => {
8677
+ const [, middle, longest] = [...size].sort((a, b) => a - b);
8678
+ return longest >= BIG_LENGTH && middle >= BIG_WIDTH;
8679
+ };
8633
8680
  var FragmentSystem = class {
8634
8681
  constructor(THREE, o) {
8635
8682
  __publicField(this, "THREE", THREE);
8636
8683
  __publicField(this, "o", o);
8637
8684
  __publicField(this, "list", []);
8638
8685
  __publicField(this, "box");
8686
+ /** The mesh box of a fragment before its scale. */
8687
+ __publicField(this, "unitBox", { min: { x: -0.5, y: -0.5, z: -0.5 }, max: { x: 0.5, y: 0.5, z: 0.5 } });
8639
8688
  __publicField(this, "shardGeos", []);
8640
8689
  __publicField(this, "materials", /* @__PURE__ */ new Map());
8641
8690
  __publicField(this, "q");
@@ -8644,6 +8693,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8644
8693
  __publicField(this, "v");
8645
8694
  __publicField(this, "w");
8646
8695
  __publicField(this, "rng", createRng(1374496523));
8696
+ /** Count of `update` calls: what makes the fragments of one burst tell apart from older ones. */
8697
+ __publicField(this, "tick", 0);
8647
8698
  __publicField(this, "markGeometry", null);
8648
8699
  __publicField(this, "markMaterial", null);
8649
8700
  this.box = new THREE.BoxGeometry(1, 1, 1);
@@ -8711,8 +8762,28 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8711
8762
  mesh2.add(disc);
8712
8763
  }
8713
8764
  }
8714
- /** Create the mesh of a fragment and start its motion. `maxAllowed` is the live budget. */
8765
+ /**
8766
+ * 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
8767
+ * 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
8768
+ * out. Returns null when the fragment was not made.
8769
+ */
8715
8770
  spawn(f, material, home, maxAllowed) {
8771
+ if (this.list.length >= maxAllowed) {
8772
+ let alive = 0;
8773
+ let oldest;
8774
+ let oldestSmall;
8775
+ for (const l of this.list) {
8776
+ if (l.dying) continue;
8777
+ alive++;
8778
+ oldest ?? (oldest = l);
8779
+ if (!l.body) oldestSmall ?? (oldestSmall = l);
8780
+ }
8781
+ oldest = oldestSmall ?? oldest;
8782
+ if (alive >= maxAllowed) {
8783
+ if (!oldest || oldest.born === this.tick) return null;
8784
+ oldest.dying = true;
8785
+ }
8786
+ }
8716
8787
  const geo = f.shape === "shard" ? this.shardGeometry(hashString(f.id)) : this.box;
8717
8788
  const mesh2 = new this.THREE.Mesh(geo, material);
8718
8789
  mesh2.position.set(f.center[0], f.center[1], f.center[2]);
@@ -8736,19 +8807,42 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8736
8807
  rest: 0,
8737
8808
  fade: 1,
8738
8809
  dying: false,
8739
- air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0
8810
+ air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0,
8811
+ body: null,
8812
+ born: this.tick
8740
8813
  };
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
- }
8814
+ if (f.shape !== "shard" && isBig(f.size)) {
8815
+ live.body = {
8816
+ mass: f.mass,
8817
+ push: (x, z) => {
8818
+ live.vel.x = x;
8819
+ live.vel.z = z;
8820
+ live.vel.y = Math.max(live.vel.y, 0.3);
8821
+ live.asleep = false;
8822
+ live.rest = 0;
8823
+ }
8824
+ };
8748
8825
  }
8826
+ this.list.push(live);
8749
8827
  this.addMarks(mesh2, f);
8750
8828
  return mesh2;
8751
8829
  }
8830
+ /** The boxes of the big fragments around (x, z). */
8831
+ solidsNear(x, z, radius, found) {
8832
+ const boxes = [];
8833
+ for (const f of this.list) {
8834
+ if (!f.body || f.dying) continue;
8835
+ const reach = f.half.length() + radius;
8836
+ if (Math.abs(f.mesh.position.x - x) > reach || Math.abs(f.mesh.position.z - z) > reach) continue;
8837
+ f.mesh.updateMatrixWorld();
8838
+ boxes.length = 0;
8839
+ pushFittedBoxes(this.unitBox, f.mesh.matrixWorld, boxes);
8840
+ for (let i = 0; i < boxes.length; i += 6) {
8841
+ if (x < boxes[i] - radius || x > boxes[i + 3] + radius || z < boxes[i + 2] - radius || z > boxes[i + 5] + radius) continue;
8842
+ found(f.home, [boxes[i], boxes[i + 1], boxes[i + 2]], [boxes[i + 3], boxes[i + 4], boxes[i + 5]], f.body);
8843
+ }
8844
+ }
8845
+ }
8752
8846
  /** Add velocity to every fragment within `radius` of a point (blasts). */
8753
8847
  blast(point, radius, speed) {
8754
8848
  for (const f of this.list) {
@@ -8764,7 +8858,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8764
8858
  f.rest = 0;
8765
8859
  }
8766
8860
  }
8861
+ /** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
8862
+ wakeRegion(x, z, radius) {
8863
+ for (const f of this.list) {
8864
+ if (!f.asleep || Math.hypot(f.mesh.position.x - x, f.mesh.position.z - z) > radius) continue;
8865
+ f.asleep = false;
8866
+ f.rest = 0;
8867
+ }
8868
+ }
8767
8869
  update(dt) {
8870
+ this.tick++;
8768
8871
  const g = this.o.gravity;
8769
8872
  for (let i = this.list.length - 1; i >= 0; i--) {
8770
8873
  const f = this.list[i];
@@ -8779,7 +8882,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8779
8882
  }
8780
8883
  if (f.asleep) {
8781
8884
  f.rest += dt;
8782
- if (this.o.life > 0 && f.rest > this.o.life) f.dying = true;
8885
+ if (this.o.life > 0 && f.rest > this.o.life && !f.body) f.dying = true;
8783
8886
  continue;
8784
8887
  }
8785
8888
  this.step(f, dt, g);
@@ -8853,7 +8956,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8853
8956
  let small = 0;
8854
8957
  for (let i = 1; i < 3; i++) if (half[i] < half[small]) small = i;
8855
8958
  let axis2 = small;
8856
- if (Math.abs(up[small]) < 0.35) {
8959
+ if (Math.abs(up[small]) < 0.35 && !f.body) {
8857
8960
  axis2 = 0;
8858
8961
  for (let i = 1; i < 3; i++) if (Math.abs(up[i]) > Math.abs(up[axis2])) axis2 = i;
8859
8962
  }
@@ -8979,6 +9082,30 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8979
9082
  var SLOP = 2e-3;
8980
9083
  var NARROW = 0.3;
8981
9084
  var TOPPLE = 3;
9085
+ var TIP_ACCELERATION = 30;
9086
+ var TIP_LYING = 0.25;
9087
+ var TIP_SECONDS = 3;
9088
+ var STEP = 0.03;
9089
+ var boxTopAt = (s, x, z) => {
9090
+ const e = s.e;
9091
+ const dx = x - s.c[0], dz = z - s.c[2];
9092
+ let t0 = -Infinity, t1 = Infinity;
9093
+ for (let k = 0; k < 3; k++) {
9094
+ const p = e[k * 4] * dx + e[k * 4 + 2] * dz;
9095
+ const d = e[k * 4 + 1];
9096
+ const h = s.half[k];
9097
+ if (Math.abs(d) < 1e-9) {
9098
+ if (Math.abs(p) > h) return -Infinity;
9099
+ continue;
9100
+ }
9101
+ let a = (-h - p) / d, b = (h - p) / d;
9102
+ if (a > b) [a, b] = [b, a];
9103
+ t0 = Math.max(t0, a);
9104
+ t1 = Math.min(t1, b);
9105
+ if (t0 > t1) return -Infinity;
9106
+ }
9107
+ return s.c[1] + t1;
9108
+ };
8982
9109
  var PieceSystem = class {
8983
9110
  constructor(THREE, o) {
8984
9111
  __publicField(this, "THREE", THREE);
@@ -8990,6 +9117,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8990
9117
  __publicField(this, "dq");
8991
9118
  __publicField(this, "m");
8992
9119
  __publicField(this, "ax");
9120
+ __publicField(this, "solids", []);
8993
9121
  this.p = new THREE.Vector3();
8994
9122
  this.q = new THREE.Quaternion();
8995
9123
  this.dq = new THREE.Quaternion();
@@ -8999,6 +9127,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8999
9127
  get(objectId) {
9000
9128
  return this.list.find((b) => b.objectId === objectId) ?? null;
9001
9129
  }
9130
+ /** 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. */
9131
+ tip(body, x, z) {
9132
+ body.tipping = { x, z, seconds: TIP_SECONDS };
9133
+ body.sleeping = false;
9134
+ }
9002
9135
  create(objectId, root, halfExtents, localCenter, mass, velocity, angular) {
9003
9136
  const existing = this.get(objectId);
9004
9137
  if (existing) {
@@ -9012,6 +9145,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9012
9145
  }
9013
9146
  const body = {
9014
9147
  objectId,
9148
+ tipping: null,
9015
9149
  root,
9016
9150
  velocity: velocity ? [...velocity] : [0, 0, 0],
9017
9151
  angularVelocity: angular ? [...angular] : [0, 0, 0],
@@ -9081,8 +9215,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9081
9215
  const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
9082
9216
  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
9217
  }
9218
+ /** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
9219
+ wakeRegion(x, z, radius) {
9220
+ for (const b of this.list) {
9221
+ if (!b.sleeping || b.held || Math.hypot(b.root.position.x - x, b.root.position.z - z) > radius) continue;
9222
+ b.sleeping = false;
9223
+ b.still = 0;
9224
+ }
9225
+ }
9084
9226
  update(dt) {
9085
9227
  this.time += dt;
9228
+ this.solids = this.list.map((body) => {
9229
+ const c = this.centerOf(body, new this.THREE.Vector3());
9230
+ const h = body.halfExtents;
9231
+ 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]) };
9232
+ });
9086
9233
  for (const b of this.list) {
9087
9234
  if (b.held) {
9088
9235
  b.velocity = [0, 0, 0];
@@ -9096,6 +9243,17 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9096
9243
  }
9097
9244
  this.separate(dt);
9098
9245
  }
9246
+ /** Where a corner of `self` at `(x, y, z)` lands: the ground, the top of another piece or of something fixed under it. */
9247
+ supportAt(self, x, y, z, step) {
9248
+ let top = this.o.groundAt(x, z);
9249
+ for (const s of this.solids) {
9250
+ if (s.body === self || s.body.held || Math.abs(x - s.c[0]) > s.r || Math.abs(z - s.c[2]) > s.r) continue;
9251
+ const t = boxTopAt(s, x, z);
9252
+ if (t > top && t <= y + step) top = t;
9253
+ }
9254
+ const fixed = this.o.staticTopAt?.(x, z, y, step) ?? -Infinity;
9255
+ return Math.max(top, fixed);
9256
+ }
9099
9257
  integrate(b, dt) {
9100
9258
  const c = this.centerOf(b, this.p);
9101
9259
  const pos = [c.x, c.y, c.z];
@@ -9134,8 +9292,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9134
9292
  const sy = i & 2 ? hy : -hy;
9135
9293
  const sz = i & 4 ? hz : -hz;
9136
9294
  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]);
9295
+ const cy = pos[1] + r[1];
9296
+ const gy = this.supportAt(b, pos[0] + r[0], cy, pos[2] + r[2], STEP + Math.max(0, -v[1]) * h * 2);
9297
+ const pen = gy - cy;
9139
9298
  if (pen > -SLOP) {
9140
9299
  rs.push(r);
9141
9300
  if (pen > maxPen) maxPen = pen;
@@ -9158,7 +9317,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9158
9317
  }
9159
9318
  const lx = pos[0] + sx / rs.length;
9160
9319
  const lz = pos[2] + sz / rs.length;
9161
- landPoint = [lx, this.o.groundAt(lx, lz), lz];
9320
+ landPoint = [lx, pos[1] - Math.max(...rs.map((r) => -r[1])), lz];
9162
9321
  }
9163
9322
  for (let it = 0; it < 4; it++) {
9164
9323
  for (const r of rs) {
@@ -9211,6 +9370,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9211
9370
  const wx = x1 - x0;
9212
9371
  const wz = z1 - z0;
9213
9372
  unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
9373
+ if (b.tipping) {
9374
+ b.tipping.seconds -= h;
9375
+ if (height < TIP_LYING || b.tipping.seconds <= 0) b.tipping = null;
9376
+ else {
9377
+ w[0] += b.tipping.x * TIP_ACCELERATION * h;
9378
+ w[2] += b.tipping.z * TIP_ACCELERATION * h;
9379
+ unstable = true;
9380
+ }
9381
+ }
9214
9382
  if (unstable) {
9215
9383
  const dirX = x0 + x1 > 0 ? -1 : 1;
9216
9384
  const dirZ = z0 + z1 > 0 ? -1 : 1;
@@ -9247,6 +9415,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9247
9415
  b.still = 0;
9248
9416
  }
9249
9417
  }
9418
+ /** Half the height of the box of a piece as it is turned now. */
9419
+ halfHeight(b) {
9420
+ const e = this.m.makeRotationFromQuaternion(b.root.quaternion).elements;
9421
+ return Math.abs(e[1]) * b.halfExtents[0] + Math.abs(e[5]) * b.halfExtents[1] + Math.abs(e[9]) * b.halfExtents[2];
9422
+ }
9250
9423
  /** Push overlapping pieces apart (horizontal bounding circles), gently, so they do not interpenetrate forever. */
9251
9424
  separate(dt) {
9252
9425
  const n = this.list.length;
@@ -9265,6 +9438,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9265
9438
  this.centerOf(a, ca);
9266
9439
  this.centerOf(b, cb);
9267
9440
  if (Math.abs(ca.y - cb.y) > ra + rb) continue;
9441
+ if (Math.abs(ca.y - cb.y) >= (this.halfHeight(a) + this.halfHeight(b)) * 0.9) continue;
9268
9442
  const rh = (Math.max(a.halfExtents[0], a.halfExtents[2]) + Math.max(b.halfExtents[0], b.halfExtents[2])) * 0.8;
9269
9443
  let dx = cb.x - ca.x;
9270
9444
  let dz = cb.z - ca.z;
@@ -9660,14 +9834,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9660
9834
  smoke: "grey"
9661
9835
  },
9662
9836
  visual: {
9663
- charColor: 1709072
9837
+ charColor: 1709072,
9838
+ innerColor: 14730131
9664
9839
  },
9665
9840
  tags: [
9666
9841
  "wood",
9667
9842
  "organic",
9668
9843
  "flammable"
9669
9844
  ],
9670
- compressive: 1e5
9845
+ compressive: 1e5,
9846
+ hits: {
9847
+ blunt: {
9848
+ tear: "splinters",
9849
+ tearJoules: 60
9850
+ }
9851
+ }
9671
9852
  },
9672
9853
  {
9673
9854
  id: "wood_hard",
@@ -9697,14 +9878,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9697
9878
  smoke: "grey"
9698
9879
  },
9699
9880
  visual: {
9700
- charColor: 1709072
9881
+ charColor: 1709072,
9882
+ innerColor: 13213802
9701
9883
  },
9702
9884
  tags: [
9703
9885
  "wood",
9704
9886
  "organic",
9705
9887
  "flammable"
9706
9888
  ],
9707
- compressive: 15e4
9889
+ compressive: 15e4,
9890
+ hits: {
9891
+ blunt: {
9892
+ tear: "splinters",
9893
+ tearJoules: 150
9894
+ }
9895
+ }
9708
9896
  },
9709
9897
  {
9710
9898
  id: "plywood",
@@ -9734,14 +9922,24 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9734
9922
  smoke: "grey"
9735
9923
  },
9736
9924
  visual: {
9737
- charColor: 1709072
9925
+ charColor: 1709072,
9926
+ innerColor: 14862234
9738
9927
  },
9739
9928
  tags: [
9740
9929
  "wood",
9741
9930
  "organic",
9742
9931
  "flammable"
9743
9932
  ],
9744
- compressive: 6e4
9933
+ compressive: 6e4,
9934
+ hits: {
9935
+ ballistic: {
9936
+ tear: "splinters"
9937
+ },
9938
+ blunt: {
9939
+ tear: "splinters",
9940
+ tearJoules: 80
9941
+ }
9942
+ }
9745
9943
  },
9746
9944
  {
9747
9945
  id: "mdf",
@@ -9768,14 +9966,25 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9768
9966
  smoke: "grey"
9769
9967
  },
9770
9968
  visual: {
9771
- charColor: 1709072
9969
+ charColor: 1709072,
9970
+ innerColor: 12096362
9772
9971
  },
9773
9972
  tags: [
9774
9973
  "wood",
9775
9974
  "organic",
9776
9975
  "flammable"
9777
9976
  ],
9778
- compressive: 3e4
9977
+ compressive: 3e4,
9978
+ hits: {
9979
+ ballistic: {
9980
+ tear: "lumps",
9981
+ tearJoules: 120
9982
+ },
9983
+ blunt: {
9984
+ tear: "lumps",
9985
+ tearJoules: 20
9986
+ }
9987
+ }
9779
9988
  },
9780
9989
  {
9781
9990
  id: "glass",
@@ -9889,6 +10098,17 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9889
10098
  damageScale: {
9890
10099
  explosive: 1.2
9891
10100
  },
10101
+ hits: {
10102
+ ballistic: {
10103
+ unitJoules: 500,
10104
+ floor: 900,
10105
+ crater: true
10106
+ },
10107
+ blunt: {
10108
+ unitJoules: 1500,
10109
+ floor: 60
10110
+ }
10111
+ },
9892
10112
  hitDebris: "chips",
9893
10113
  breakDebris: "dust",
9894
10114
  thermal: {
@@ -10257,7 +10477,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10257
10477
  smoke: "white"
10258
10478
  },
10259
10479
  visual: {
10260
- charColor: 1709072
10480
+ charColor: 1709072,
10481
+ innerColor: 14207126
10261
10482
  },
10262
10483
  tags: [
10263
10484
  "plant",
@@ -10265,7 +10486,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10265
10486
  "organic",
10266
10487
  "flammable"
10267
10488
  ],
10268
- compressive: 6e4
10489
+ compressive: 6e4,
10490
+ hits: {
10491
+ blunt: {
10492
+ tear: "none"
10493
+ }
10494
+ }
10269
10495
  },
10270
10496
  {
10271
10497
  id: "cactus",
@@ -10380,6 +10606,130 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10380
10606
  "masonry"
10381
10607
  ],
10382
10608
  compressive: 2e4
10609
+ },
10610
+ {
10611
+ id: "asphalt",
10612
+ density: 2350,
10613
+ hpPerKg: 10,
10614
+ penetration: 14,
10615
+ penetrationClass: "concrete",
10616
+ penetrationScale: 0.8,
10617
+ ductility: 0.1,
10618
+ fracture: "crumble",
10619
+ fragmentSize: [
10620
+ 0.03,
10621
+ 0.15
10622
+ ],
10623
+ hitDebris: "chips",
10624
+ breakDebris: "dust",
10625
+ thermal: {
10626
+ conductivity: 0.1,
10627
+ heatCapacity: 2,
10628
+ ignitionC: 400,
10629
+ burnRate: 2e-3,
10630
+ smoke: "black"
10631
+ },
10632
+ tags: [
10633
+ "masonry",
10634
+ "brittle"
10635
+ ],
10636
+ compressive: 3e4
10637
+ },
10638
+ {
10639
+ id: "sand",
10640
+ density: 1600,
10641
+ hpPerKg: 2,
10642
+ penetration: 6,
10643
+ ductility: 0.4,
10644
+ fracture: "crumble",
10645
+ fragmentSize: [
10646
+ 0.01,
10647
+ 0.04
10648
+ ],
10649
+ hitDebris: "dust",
10650
+ breakDebris: "dust",
10651
+ thermal: {
10652
+ conductivity: 0.1,
10653
+ heatCapacity: 2
10654
+ },
10655
+ tags: [
10656
+ "soil"
10657
+ ],
10658
+ compressive: 1e4
10659
+ },
10660
+ {
10661
+ id: "gravel",
10662
+ density: 1800,
10663
+ hpPerKg: 3,
10664
+ penetration: 8,
10665
+ ductility: 0.3,
10666
+ fracture: "crumble",
10667
+ fragmentSize: [
10668
+ 0.02,
10669
+ 0.06
10670
+ ],
10671
+ hitDebris: "chips",
10672
+ breakDebris: "dust",
10673
+ thermal: {
10674
+ conductivity: 0.1,
10675
+ heatCapacity: 2
10676
+ },
10677
+ tags: [
10678
+ "soil",
10679
+ "masonry"
10680
+ ],
10681
+ compressive: 2e4
10682
+ },
10683
+ {
10684
+ id: "turf",
10685
+ density: 900,
10686
+ hpPerKg: 3,
10687
+ penetration: 4,
10688
+ ductility: 0.5,
10689
+ fracture: "crumble",
10690
+ fragmentSize: [
10691
+ 0.03,
10692
+ 0.1
10693
+ ],
10694
+ hitDebris: "dust",
10695
+ breakDebris: "dust",
10696
+ thermal: {
10697
+ conductivity: 0.1,
10698
+ heatCapacity: 1.5,
10699
+ ignitionC: 250,
10700
+ burnRate: 0.03,
10701
+ smoke: "white"
10702
+ },
10703
+ tags: [
10704
+ "soil",
10705
+ "organic"
10706
+ ],
10707
+ compressive: 15e3
10708
+ },
10709
+ {
10710
+ id: "mortar",
10711
+ density: 1900,
10712
+ hpPerKg: 10,
10713
+ penetration: 12,
10714
+ penetrationClass: "concrete",
10715
+ penetrationScale: 1.2,
10716
+ ductility: 0.03,
10717
+ fracture: "crumble",
10718
+ fragmentSize: [
10719
+ 0.02,
10720
+ 0.08
10721
+ ],
10722
+ hitDebris: "chips",
10723
+ breakDebris: "dust",
10724
+ thermal: {
10725
+ conductivity: 0.15,
10726
+ heatCapacity: 2.5
10727
+ },
10728
+ tags: [
10729
+ "masonry",
10730
+ "brittle"
10731
+ ],
10732
+ compressive: 1e5
10383
10733
  }
10384
10734
  ];
10385
10735
  var material_hints_default = {
@@ -10575,6 +10925,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10575
10925
  const fill = options.fillOf?.(o, material);
10576
10926
  const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
10577
10927
  const contains = containsOf(ud.destructionContains);
10928
+ const thickness = typeof ud.destructionThickness === "number" && ud.destructionThickness > 0 ? ud.destructionThickness : void 0;
10578
10929
  const existing = explicit ? byKey.get(explicit) : void 0;
10579
10930
  if (existing) {
10580
10931
  const before = sizeOf(existing.box);
@@ -10587,8 +10938,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10587
10938
  existing.skin = existing.skin && skin;
10588
10939
  if (fill !== void 0) existing.fill = fill;
10589
10940
  if (contains) existing.contains = contains;
10941
+ if (thickness !== void 0) existing.thickness = Math.min(existing.thickness ?? thickness, thickness);
10590
10942
  } 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 };
10943
+ 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
10944
  entries.push(e);
10593
10945
  if (explicit) byKey.set(explicit, e);
10594
10946
  }
@@ -10656,7 +11008,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10656
11008
  const minSide = Math.min(...size);
10657
11009
  if (e.fill !== void 0) part.fill = e.fill;
10658
11010
  else if (e.hollow) part.fill = 0.05;
10659
- if (minSide < 0.01 && METAL.test(e.material)) {
11011
+ if (e.thickness !== void 0) part.thickness = e.thickness;
11012
+ else if (minSide < 0.01 && METAL.test(e.material)) {
10660
11013
  part.fill = e.fill ?? 1;
10661
11014
  part.thickness = Math.max(minSide, 5e-4);
10662
11015
  }
@@ -10747,9 +11100,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10747
11100
  const ids = [0, 1, 2].map((corner) => index ? index.getX(t * 3 + corner) : t * 3 + corner);
10748
11101
  ids.forEach((id, corner) => corners[corner].set(shell.before[id * 3], shell.before[id * 3 + 1], shell.before[id * 3 + 2]));
10749
11102
  triangle.set(corners[0], corners[1], corners[2]);
11103
+ if (triangle.getArea() <= 1e-14) continue;
10750
11104
  triangle.closestPointToPoint(local2, closest);
10751
11105
  const distance = closest.distanceToSquared(local2);
10752
- if (distance >= nearest) continue;
11106
+ if (!(distance < nearest)) continue;
10753
11107
  nearest = distance;
10754
11108
  nearestScale = shell.mesh.matrixWorld.getMaxScaleOnAxis() || 1;
10755
11109
  triangle.getBarycoord(closest, weights);
@@ -10928,6 +11282,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10928
11282
  return out;
10929
11283
  }
10930
11284
  /** Box of the part in world space. */
11285
+ /** Appends the boxes that follow the mesh in the world (`pushFittedBoxes`) to `out`. */
11286
+ fittedBoxes(mv, out) {
11287
+ const box = new this.THREE.Box3();
11288
+ if (mv.host) this.localBox(mv, box);
11289
+ else {
11290
+ const geometry = mv.mesh.geometry;
11291
+ if (!geometry.boundingBox) geometry.computeBoundingBox();
11292
+ if (geometry.boundingBox) box.copy(geometry.boundingBox);
11293
+ else box.makeEmpty();
11294
+ }
11295
+ if (box.isEmpty()) return;
11296
+ mv.mesh.updateWorldMatrix(true, false);
11297
+ pushFittedBoxes(box, mv.mesh.matrixWorld, out);
11298
+ }
10931
11299
  worldBox(mv, out) {
10932
11300
  if (!mv.host) return out.setFromObject(mv.mesh);
10933
11301
  this.localBox(mv, out);
@@ -11676,6 +12044,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11676
12044
  var IMPACT_TAGS = ["metal", "glass", "wood"];
11677
12045
  var GLOW_FROM_C = 430;
11678
12046
  var QUEUE_CAP = 4e3;
12047
+ var STATIC_REFRESH = 0.5;
12048
+ var STATIC_SPAN = 3;
12049
+ var FEET_BAND = 0.03;
12050
+ var TIP_MIN_HEIGHT = 0.3;
12051
+ var TIP_NARROW = 0.3;
11679
12052
  var FIRE_CEILING_SECONDS = 3600;
11680
12053
  var GENERIC_REFERENCE_RADIUS = 15;
11681
12054
  var PRIMED_FIRE_RADIUS = 0.2;
@@ -11720,6 +12093,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11720
12093
  if (def.fracture === "tear") return "impact_soft";
11721
12094
  return def.fracture === "none" ? "impact_metal" : "impact_wood";
11722
12095
  }
12096
+ var dragSoundKey = (def) => impactKey(def).replace("impact_", "drag_");
11723
12097
  function fractureKey(def, style) {
11724
12098
  switch (style) {
11725
12099
  case "shatter":
@@ -11761,6 +12135,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11761
12135
  __publicField(this, "autoSplit");
11762
12136
  __publicField(this, "maxFragments");
11763
12137
  __publicField(this, "bounds", /* @__PURE__ */ new Map());
12138
+ /** World boxes (min xyz, max xyz) of the drawn meshes of the bound objects that are not pieces, for the pieces to land on. */
12139
+ __publicField(this, "staticBoxes", /* @__PURE__ */ new Map());
12140
+ /** Objects to measure again; `staticAll` measures every one. */
12141
+ __publicField(this, "staticDirty", /* @__PURE__ */ new Set());
12142
+ __publicField(this, "staticAll", true);
12143
+ /** Boxes of the pieces' meshes (as `staticBoxes`), with the time they were measured. */
12144
+ __publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
12145
+ __publicField(this, "staticAt", 0);
11764
12146
  __publicField(this, "queue", []);
11765
12147
  __publicField(this, "unsubscribe");
11766
12148
  __publicField(this, "visuals");
@@ -11822,6 +12204,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11822
12204
  this.cages = new RebarCages(THREE, o.scene);
11823
12205
  this.pieceSys = new PieceSystem(THREE, {
11824
12206
  groundAt: this.groundAt,
12207
+ staticTopAt: (x, z, y, step) => this.staticTopAt(x, z, y, step),
11825
12208
  gravity: this.gravity,
11826
12209
  scene: o.scene,
11827
12210
  onHardLanding: (p, point, speed) => this.hardLanding(p, point, speed)
@@ -11834,6 +12217,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11834
12217
  // Binding
11835
12218
  // ----------------------------------------------------------------------------------------------------------------------------------
11836
12219
  bind(objectId, root, options = {}) {
12220
+ this.staticDirty.add(objectId);
11837
12221
  if (this.bounds.has(objectId)) this.unbind(objectId);
11838
12222
  const state = this.world.state(objectId);
11839
12223
  const partIds = new Set((state?.parts ?? []).map((p) => p.id));
@@ -11953,6 +12337,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11953
12337
  }
11954
12338
  }
11955
12339
  unbind(objectId) {
12340
+ this.staticDirty.add(objectId);
11956
12341
  const b = this.bounds.get(objectId);
11957
12342
  if (!b) return;
11958
12343
  this.restore(objectId);
@@ -11992,6 +12377,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11992
12377
  return m > 0 ? m : 5;
11993
12378
  }
11994
12379
  makePiece(objectId, options = {}) {
12380
+ this.staticDirty.add(objectId);
11995
12381
  const b = this.bounds.get(objectId);
11996
12382
  if (!b) throw new Error(`game-destruction-three: object "${objectId}" is not bound`);
11997
12383
  if (b.root.parent && b.root.parent !== this.scene) this.scene.attach(b.root);
@@ -12104,7 +12490,100 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12104
12490
  // ----------------------------------------------------------------------------------------------------------------------------------
12105
12491
  // Events
12106
12492
  // ----------------------------------------------------------------------------------------------------------------------------------
12493
+ /**
12494
+ * 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
12495
+ * 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`
12496
+ * are left out (walls, floors, roofs: the terrain under them is the host's `groundAt`).
12497
+ */
12498
+ staticTopAt(x, z, y, step) {
12499
+ let top = -Infinity;
12500
+ for (const a of this.staticNow().values()) {
12501
+ if (a.length === 0 || x < a[0] || x > a[3] || z < a[2] || z > a[5]) continue;
12502
+ for (let i = 6; i < a.length; i += 6) {
12503
+ if (x < a[i] || x > a[i + 3] || z < a[i + 2] || z > a[i + 5]) continue;
12504
+ const t = a[i + 4];
12505
+ if (t > top && t <= y + step) top = t;
12506
+ }
12507
+ }
12508
+ return top;
12509
+ }
12510
+ /** 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). */
12511
+ staticNow() {
12512
+ if (this.time - this.staticAt > STATIC_REFRESH) {
12513
+ this.staticAt = this.time;
12514
+ this.staticAll = true;
12515
+ }
12516
+ if (!this.staticAll && this.staticDirty.size === 0) return this.staticBoxes;
12517
+ const box = new this.THREE.Box3();
12518
+ const measure = (b) => {
12519
+ if (b.piece || this.pieceSys.get(b.id)) {
12520
+ this.staticBoxes.delete(b.id);
12521
+ return;
12522
+ }
12523
+ const out = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];
12524
+ for (const mv of this.meshesOf(b)) {
12525
+ if (mv.host || mv.loose || !this.visuals.shown(mv)) continue;
12526
+ this.visuals.worldBox(mv, box);
12527
+ if (box.isEmpty() || box.max.x - box.min.x > STATIC_SPAN || box.max.z - box.min.z > STATIC_SPAN) continue;
12528
+ this.visuals.fittedBoxes(mv, out);
12529
+ for (let k = 0; k < 3; k++) {
12530
+ out[k] = Math.min(out[k], box.min.getComponent(k));
12531
+ out[k + 3] = Math.max(out[k + 3], box.max.getComponent(k));
12532
+ }
12533
+ }
12534
+ this.staticBoxes.set(b.id, out.length > 6 ? out : []);
12535
+ };
12536
+ if (this.staticAll) {
12537
+ this.staticBoxes.clear();
12538
+ for (const b of this.bounds.values()) measure(b);
12539
+ } else {
12540
+ for (const id of this.staticDirty) {
12541
+ const b = this.bounds.get(id);
12542
+ if (b) measure(b);
12543
+ else this.staticBoxes.delete(id);
12544
+ }
12545
+ }
12546
+ this.staticAll = false;
12547
+ this.staticDirty.clear();
12548
+ return this.staticBoxes;
12549
+ }
12550
+ /** The boxes of a piece's drawn meshes, measured once per frame. */
12551
+ pieceBoxesOf(pc) {
12552
+ const cached = this.pieceBoxes.get(pc);
12553
+ if (cached && cached.at === this.time) return cached.boxes;
12554
+ const boxes = [];
12555
+ const b = this.bounds.get(pc.objectId);
12556
+ if (b) {
12557
+ for (const mv of this.meshesOf(b)) {
12558
+ if (this.visuals.shown(mv)) this.visuals.fittedBoxes(mv, boxes);
12559
+ }
12560
+ }
12561
+ this.pieceBoxes.set(pc, { at: this.time, boxes });
12562
+ return boxes;
12563
+ }
12564
+ solidsNear(x, z, radius) {
12565
+ const found = [];
12566
+ const scan = (a, id, piece, from) => {
12567
+ if (from > 0 && (a.length === 0 || x < a[0] - radius || x > a[3] + radius || z < a[2] - radius || z > a[5] + radius)) return;
12568
+ for (let i = from; i < a.length; i += 6) {
12569
+ if (x < a[i] - radius || x > a[i + 3] + radius || z < a[i + 2] - radius || z > a[i + 5] + radius) continue;
12570
+ found.push({ objectId: id, min: [a[i], a[i + 1], a[i + 2]], max: [a[i + 3], a[i + 4], a[i + 5]], piece });
12571
+ }
12572
+ };
12573
+ for (const [id, a] of this.staticNow()) scan(a, id, null, 6);
12574
+ const c = this.v1;
12575
+ for (const pc of this.pieceSys.list) {
12576
+ if (pc.held) continue;
12577
+ this.pieceSys.centerOf(pc, c);
12578
+ const reach = Math.hypot(pc.halfExtents[0], pc.halfExtents[1], pc.halfExtents[2]) + radius;
12579
+ if (Math.abs(c.x - x) > reach || Math.abs(c.z - z) > reach) continue;
12580
+ scan(this.pieceBoxesOf(pc), pc.objectId, pc, 0);
12581
+ }
12582
+ this.fragments.solidsNear(x, z, radius, (home, min2, max2, loose) => found.push({ objectId: home, min: min2, max: max2, piece: null, loose }));
12583
+ return found;
12584
+ }
12107
12585
  apply(ev) {
12586
+ this.staticDirty.add(ev.object);
12108
12587
  switch (ev.type) {
12109
12588
  case "damage":
12110
12589
  if (ev.damageType === "heat" || ev.damageType === "fire" || ev.damageType === "explosive") this.activate(ev.object, ev.part);
@@ -12441,7 +12920,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12441
12920
  const def = pv ? this.world.materials.get(pv.material) : void 0;
12442
12921
  const color = (pv ? this.partColor(pv, def) : null) ?? fallbackColor(pv?.material ?? "", def);
12443
12922
  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);
12923
+ for (const fr of ev.fragments) {
12924
+ const fdef = this.world.materials.get(fr.material);
12925
+ const tone = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
12926
+ this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone, false), ev.object, maxAlive);
12927
+ }
12445
12928
  if (!this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.center)) this.vfx.burst("dust", ev.center, [0, 1, 0], 6, 0.8, { color });
12446
12929
  this.sound(impactKey(def), ev.center, 0.7);
12447
12930
  }
@@ -12522,6 +13005,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12522
13005
  this.pieceSys.setCenter(old, center);
12523
13006
  old.mass = Math.max(0.05, this.massOf(objectId));
12524
13007
  old.sleeping = false;
13008
+ } else if (kitOriginOf(b.root)?.kind === "furniture") {
13009
+ this.makePiece(objectId);
13010
+ this.tipIfTopHeavy(b);
12525
13011
  }
12526
13012
  }
12527
13013
  /** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
@@ -12568,8 +13054,32 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12568
13054
  }
12569
13055
  const piece = this.pieceSys.create(newId, group, half, center, this.massOf(newId), vel, ang);
12570
13056
  nb.piece = piece;
13057
+ if (kitOriginOf(from.root)?.kind === "furniture") this.tipIfTopHeavy(nb);
12571
13058
  this.o.onPiece?.(piece);
12572
13059
  }
13060
+ /**
13061
+ * 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
13062
+ * high and its feet are thin, so it gets the start of the fall, across the line of its feet.
13063
+ */
13064
+ tipIfTopHeavy(b) {
13065
+ const piece = b.piece;
13066
+ if (!piece) return;
13067
+ const box = new this.THREE.Box3();
13068
+ const boxes = this.meshesOf(b).filter((mv) => this.visuals.shown(mv)).map((mv) => this.visuals.worldBox(mv, new this.THREE.Box3()));
13069
+ if (boxes.length === 0) return;
13070
+ for (const one of boxes) box.union(one);
13071
+ const height = box.max.y - box.min.y;
13072
+ const feet = new this.THREE.Box3();
13073
+ for (const one of boxes) if (one.min.y < box.min.y + FEET_BAND) feet.union(one);
13074
+ const spanX = feet.max.x - feet.min.x;
13075
+ const spanZ = feet.max.z - feet.min.z;
13076
+ if (height < TIP_MIN_HEIGHT || Math.min(spanX, spanZ) >= TIP_NARROW * height) return;
13077
+ const across = spanX <= spanZ ? 0 : 2;
13078
+ 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;
13079
+ const away = lean < 0 ? -1 : 1;
13080
+ if (across === 0) this.pieceSys.tip(piece, 0, -away);
13081
+ else this.pieceSys.tip(piece, away, 0);
13082
+ }
12573
13083
  onExplode(ev, options = {}) {
12574
13084
  const p = ev.point;
12575
13085
  if (options.visual !== false) this.playExplosion(ev, options);
@@ -12820,6 +13330,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12820
13330
  this.vfx.sprites().clear();
12821
13331
  this.ownVfx?.world.clear();
12822
13332
  }
13333
+ wakeRegion(x, z, radius) {
13334
+ this.fragments.wakeRegion(x, z, radius);
13335
+ this.pieceSys.wakeRegion(x, z, radius);
13336
+ }
12823
13337
  stats() {
12824
13338
  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
13339
  }
@@ -13112,7 +13626,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13112
13626
  volume: 0,
13113
13627
  sources: ["cena:L1316 fire loop (silent in the page: its gain is never raised)", "sdk: the level while burning"]
13114
13628
  };
13115
- var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
13629
+ var drag = (id, layers) => ({ ...sceneRecipe(id, layers, ""), sources: ["sdk: a base dragged on a floor, one grain per stretch"] });
13630
+ var DRAG_SOUNDS = [
13631
+ 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 })]),
13632
+ 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 })]),
13633
+ drag("drag_glass", [sceneNoise({ type: "bandpass", f: 2600, q: 5, d: 0.2, vol: 0.22 })]),
13634
+ 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] }))]),
13635
+ drag("drag_soft", [sceneNoise({ type: "lowpass", f: 420, d: 0.28, vol: 0.3 })])
13636
+ ];
13637
+ var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
13116
13638
 
13117
13639
  // src/audio/sound.ts
13118
13640
  var DESTRUCTION_SOUND_KEYS = {
@@ -13122,6 +13644,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13122
13644
  impact_glass: { sound: "ping" },
13123
13645
  impact_masonry: { sound: "chip" },
13124
13646
  impact_soft: { sound: "thud" },
13647
+ drag_wood: { sound: "drag_wood" },
13648
+ drag_metal: { sound: "drag_metal" },
13649
+ drag_glass: { sound: "drag_glass" },
13650
+ drag_masonry: { sound: "drag_masonry" },
13651
+ drag_soft: { sound: "drag_soft" },
13125
13652
  shatter_glass: { sound: "glass_shatter" },
13126
13653
  crumble: { sound: "crumble" },
13127
13654
  splinter: { sound: "crack" },