@adatechnology/game-destruction-three 0.9.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
 
@@ -59,6 +62,13 @@ var GameDestructionThree = (() => {
59
62
  node.userData[KIT_ORIGIN_KEY] = { ...origin };
60
63
  return object;
61
64
  }
65
+ function kitOriginOf(object) {
66
+ for (let node = object; node; node = node.parent) {
67
+ const origin = node.userData?.[KIT_ORIGIN_KEY];
68
+ if (origin && typeof origin.owner === "string") return origin;
69
+ }
70
+ return null;
71
+ }
62
72
 
63
73
  // ../game-vfx/dist/index.js
64
74
  var __defProp2 = Object.defineProperty;
@@ -8582,6 +8592,44 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8582
8592
  }
8583
8593
  var LIGHT_BUDGET = EVENT_BUDGET.blastLights + EVENT_BUDGET.fireLights;
8584
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
+
8585
8633
  // src/math.ts
8586
8634
  function createRng(seed) {
8587
8635
  let a = seed >>> 0;
@@ -8623,12 +8671,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8623
8671
  var SHEET_MAX = 4e-3;
8624
8672
  var SHEET_AIR = 2.5;
8625
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
+ };
8626
8680
  var FragmentSystem = class {
8627
8681
  constructor(THREE, o) {
8628
8682
  __publicField(this, "THREE", THREE);
8629
8683
  __publicField(this, "o", o);
8630
8684
  __publicField(this, "list", []);
8631
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 } });
8632
8688
  __publicField(this, "shardGeos", []);
8633
8689
  __publicField(this, "materials", /* @__PURE__ */ new Map());
8634
8690
  __publicField(this, "q");
@@ -8637,6 +8693,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8637
8693
  __publicField(this, "v");
8638
8694
  __publicField(this, "w");
8639
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);
8640
8698
  __publicField(this, "markGeometry", null);
8641
8699
  __publicField(this, "markMaterial", null);
8642
8700
  this.box = new THREE.BoxGeometry(1, 1, 1);
@@ -8704,8 +8762,28 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8704
8762
  mesh2.add(disc);
8705
8763
  }
8706
8764
  }
8707
- /** 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
+ */
8708
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
+ }
8709
8787
  const geo = f.shape === "shard" ? this.shardGeometry(hashString(f.id)) : this.box;
8710
8788
  const mesh2 = new this.THREE.Mesh(geo, material);
8711
8789
  mesh2.position.set(f.center[0], f.center[1], f.center[2]);
@@ -8729,19 +8807,42 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8729
8807
  rest: 0,
8730
8808
  fade: 1,
8731
8809
  dying: false,
8732
- 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
8733
8813
  };
8734
- this.list.push(live);
8735
- let alive = this.list.reduce((n, l) => n + (l.dying ? 0 : 1), 0);
8736
- for (let i = 0; i < this.list.length && alive > maxAllowed; i++) {
8737
- if (!this.list[i].dying) {
8738
- this.list[i].dying = true;
8739
- alive--;
8740
- }
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
+ };
8741
8825
  }
8826
+ this.list.push(live);
8742
8827
  this.addMarks(mesh2, f);
8743
8828
  return mesh2;
8744
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
+ }
8745
8846
  /** Add velocity to every fragment within `radius` of a point (blasts). */
8746
8847
  blast(point, radius, speed) {
8747
8848
  for (const f of this.list) {
@@ -8757,7 +8858,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8757
8858
  f.rest = 0;
8758
8859
  }
8759
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
+ }
8760
8869
  update(dt) {
8870
+ this.tick++;
8761
8871
  const g = this.o.gravity;
8762
8872
  for (let i = this.list.length - 1; i >= 0; i--) {
8763
8873
  const f = this.list[i];
@@ -8772,7 +8882,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8772
8882
  }
8773
8883
  if (f.asleep) {
8774
8884
  f.rest += dt;
8775
- 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;
8776
8886
  continue;
8777
8887
  }
8778
8888
  this.step(f, dt, g);
@@ -8846,7 +8956,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8846
8956
  let small = 0;
8847
8957
  for (let i = 1; i < 3; i++) if (half[i] < half[small]) small = i;
8848
8958
  let axis2 = small;
8849
- if (Math.abs(up[small]) < 0.35) {
8959
+ if (Math.abs(up[small]) < 0.35 && !f.body) {
8850
8960
  axis2 = 0;
8851
8961
  for (let i = 1; i < 3; i++) if (Math.abs(up[i]) > Math.abs(up[axis2])) axis2 = i;
8852
8962
  }
@@ -8972,6 +9082,30 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8972
9082
  var SLOP = 2e-3;
8973
9083
  var NARROW = 0.3;
8974
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
+ };
8975
9109
  var PieceSystem = class {
8976
9110
  constructor(THREE, o) {
8977
9111
  __publicField(this, "THREE", THREE);
@@ -8983,6 +9117,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8983
9117
  __publicField(this, "dq");
8984
9118
  __publicField(this, "m");
8985
9119
  __publicField(this, "ax");
9120
+ __publicField(this, "solids", []);
8986
9121
  this.p = new THREE.Vector3();
8987
9122
  this.q = new THREE.Quaternion();
8988
9123
  this.dq = new THREE.Quaternion();
@@ -8992,6 +9127,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
8992
9127
  get(objectId) {
8993
9128
  return this.list.find((b) => b.objectId === objectId) ?? null;
8994
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
+ }
8995
9135
  create(objectId, root, halfExtents, localCenter, mass, velocity, angular) {
8996
9136
  const existing = this.get(objectId);
8997
9137
  if (existing) {
@@ -9005,6 +9145,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9005
9145
  }
9006
9146
  const body = {
9007
9147
  objectId,
9148
+ tipping: null,
9008
9149
  root,
9009
9150
  velocity: velocity ? [...velocity] : [0, 0, 0],
9010
9151
  angularVelocity: angular ? [...angular] : [0, 0, 0],
@@ -9074,8 +9215,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9074
9215
  const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
9075
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];
9076
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
+ }
9077
9226
  update(dt) {
9078
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
+ });
9079
9233
  for (const b of this.list) {
9080
9234
  if (b.held) {
9081
9235
  b.velocity = [0, 0, 0];
@@ -9089,6 +9243,17 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9089
9243
  }
9090
9244
  this.separate(dt);
9091
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
+ }
9092
9257
  integrate(b, dt) {
9093
9258
  const c = this.centerOf(b, this.p);
9094
9259
  const pos = [c.x, c.y, c.z];
@@ -9127,8 +9292,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9127
9292
  const sy = i & 2 ? hy : -hy;
9128
9293
  const sz = i & 4 ? hz : -hz;
9129
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];
9130
- const gy = this.o.groundAt(pos[0] + r[0], pos[2] + r[2]);
9131
- 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;
9132
9298
  if (pen > -SLOP) {
9133
9299
  rs.push(r);
9134
9300
  if (pen > maxPen) maxPen = pen;
@@ -9151,7 +9317,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9151
9317
  }
9152
9318
  const lx = pos[0] + sx / rs.length;
9153
9319
  const lz = pos[2] + sz / rs.length;
9154
- landPoint = [lx, this.o.groundAt(lx, lz), lz];
9320
+ landPoint = [lx, pos[1] - Math.max(...rs.map((r) => -r[1])), lz];
9155
9321
  }
9156
9322
  for (let it = 0; it < 4; it++) {
9157
9323
  for (const r of rs) {
@@ -9204,6 +9370,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9204
9370
  const wx = x1 - x0;
9205
9371
  const wz = z1 - z0;
9206
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
+ }
9207
9382
  if (unstable) {
9208
9383
  const dirX = x0 + x1 > 0 ? -1 : 1;
9209
9384
  const dirZ = z0 + z1 > 0 ? -1 : 1;
@@ -9240,6 +9415,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9240
9415
  b.still = 0;
9241
9416
  }
9242
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
+ }
9243
9423
  /** Push overlapping pieces apart (horizontal bounding circles), gently, so they do not interpenetrate forever. */
9244
9424
  separate(dt) {
9245
9425
  const n = this.list.length;
@@ -9258,6 +9438,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9258
9438
  this.centerOf(a, ca);
9259
9439
  this.centerOf(b, cb);
9260
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;
9261
9442
  const rh = (Math.max(a.halfExtents[0], a.halfExtents[2]) + Math.max(b.halfExtents[0], b.halfExtents[2])) * 0.8;
9262
9443
  let dx = cb.x - ca.x;
9263
9444
  let dz = cb.z - ca.z;
@@ -9653,14 +9834,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9653
9834
  smoke: "grey"
9654
9835
  },
9655
9836
  visual: {
9656
- charColor: 1709072
9837
+ charColor: 1709072,
9838
+ innerColor: 14730131
9657
9839
  },
9658
9840
  tags: [
9659
9841
  "wood",
9660
9842
  "organic",
9661
9843
  "flammable"
9662
9844
  ],
9663
- compressive: 1e5
9845
+ compressive: 1e5,
9846
+ hits: {
9847
+ blunt: {
9848
+ tear: "splinters",
9849
+ tearJoules: 60
9850
+ }
9851
+ }
9664
9852
  },
9665
9853
  {
9666
9854
  id: "wood_hard",
@@ -9690,14 +9878,21 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9690
9878
  smoke: "grey"
9691
9879
  },
9692
9880
  visual: {
9693
- charColor: 1709072
9881
+ charColor: 1709072,
9882
+ innerColor: 13213802
9694
9883
  },
9695
9884
  tags: [
9696
9885
  "wood",
9697
9886
  "organic",
9698
9887
  "flammable"
9699
9888
  ],
9700
- compressive: 15e4
9889
+ compressive: 15e4,
9890
+ hits: {
9891
+ blunt: {
9892
+ tear: "splinters",
9893
+ tearJoules: 150
9894
+ }
9895
+ }
9701
9896
  },
9702
9897
  {
9703
9898
  id: "plywood",
@@ -9727,14 +9922,24 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9727
9922
  smoke: "grey"
9728
9923
  },
9729
9924
  visual: {
9730
- charColor: 1709072
9925
+ charColor: 1709072,
9926
+ innerColor: 14862234
9731
9927
  },
9732
9928
  tags: [
9733
9929
  "wood",
9734
9930
  "organic",
9735
9931
  "flammable"
9736
9932
  ],
9737
- compressive: 6e4
9933
+ compressive: 6e4,
9934
+ hits: {
9935
+ ballistic: {
9936
+ tear: "splinters"
9937
+ },
9938
+ blunt: {
9939
+ tear: "splinters",
9940
+ tearJoules: 80
9941
+ }
9942
+ }
9738
9943
  },
9739
9944
  {
9740
9945
  id: "mdf",
@@ -9761,14 +9966,25 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9761
9966
  smoke: "grey"
9762
9967
  },
9763
9968
  visual: {
9764
- charColor: 1709072
9969
+ charColor: 1709072,
9970
+ innerColor: 12096362
9765
9971
  },
9766
9972
  tags: [
9767
9973
  "wood",
9768
9974
  "organic",
9769
9975
  "flammable"
9770
9976
  ],
9771
- 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
+ }
9772
9988
  },
9773
9989
  {
9774
9990
  id: "glass",
@@ -9882,6 +10098,17 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
9882
10098
  damageScale: {
9883
10099
  explosive: 1.2
9884
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
+ },
9885
10112
  hitDebris: "chips",
9886
10113
  breakDebris: "dust",
9887
10114
  thermal: {
@@ -10250,7 +10477,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10250
10477
  smoke: "white"
10251
10478
  },
10252
10479
  visual: {
10253
- charColor: 1709072
10480
+ charColor: 1709072,
10481
+ innerColor: 14207126
10254
10482
  },
10255
10483
  tags: [
10256
10484
  "plant",
@@ -10258,7 +10486,12 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10258
10486
  "organic",
10259
10487
  "flammable"
10260
10488
  ],
10261
- compressive: 6e4
10489
+ compressive: 6e4,
10490
+ hits: {
10491
+ blunt: {
10492
+ tear: "none"
10493
+ }
10494
+ }
10262
10495
  },
10263
10496
  {
10264
10497
  id: "cactus",
@@ -10373,6 +10606,130 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10373
10606
  "masonry"
10374
10607
  ],
10375
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
10376
10733
  }
10377
10734
  ];
10378
10735
  var material_hints_default = {
@@ -10568,6 +10925,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10568
10925
  const fill = options.fillOf?.(o, material);
10569
10926
  const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
10570
10927
  const contains = containsOf(ud.destructionContains);
10928
+ const thickness = typeof ud.destructionThickness === "number" && ud.destructionThickness > 0 ? ud.destructionThickness : void 0;
10571
10929
  const existing = explicit ? byKey.get(explicit) : void 0;
10572
10930
  if (existing) {
10573
10931
  const before = sizeOf(existing.box);
@@ -10580,8 +10938,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10580
10938
  existing.skin = existing.skin && skin;
10581
10939
  if (fill !== void 0) existing.fill = fill;
10582
10940
  if (contains) existing.contains = contains;
10941
+ if (thickness !== void 0) existing.thickness = Math.min(existing.thickness ?? thickness, thickness);
10583
10942
  } else {
10584
- 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 };
10585
10944
  entries.push(e);
10586
10945
  if (explicit) byKey.set(explicit, e);
10587
10946
  }
@@ -10649,7 +11008,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10649
11008
  const minSide = Math.min(...size);
10650
11009
  if (e.fill !== void 0) part.fill = e.fill;
10651
11010
  else if (e.hollow) part.fill = 0.05;
10652
- 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)) {
10653
11013
  part.fill = e.fill ?? 1;
10654
11014
  part.thickness = Math.max(minSide, 5e-4);
10655
11015
  }
@@ -10740,9 +11100,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10740
11100
  const ids = [0, 1, 2].map((corner) => index ? index.getX(t * 3 + corner) : t * 3 + corner);
10741
11101
  ids.forEach((id, corner) => corners[corner].set(shell.before[id * 3], shell.before[id * 3 + 1], shell.before[id * 3 + 2]));
10742
11102
  triangle.set(corners[0], corners[1], corners[2]);
11103
+ if (triangle.getArea() <= 1e-14) continue;
10743
11104
  triangle.closestPointToPoint(local2, closest);
10744
11105
  const distance = closest.distanceToSquared(local2);
10745
- if (distance >= nearest) continue;
11106
+ if (!(distance < nearest)) continue;
10746
11107
  nearest = distance;
10747
11108
  nearestScale = shell.mesh.matrixWorld.getMaxScaleOnAxis() || 1;
10748
11109
  triangle.getBarycoord(closest, weights);
@@ -10921,6 +11282,20 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
10921
11282
  return out;
10922
11283
  }
10923
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
+ }
10924
11299
  worldBox(mv, out) {
10925
11300
  if (!mv.host) return out.setFromObject(mv.mesh);
10926
11301
  this.localBox(mv, out);
@@ -11665,9 +12040,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11665
12040
 
11666
12041
  // src/renderer.ts
11667
12042
  var rendererCount = 0;
12043
+ var COLLAPSE_CLOUD_KG = 1e3;
11668
12044
  var IMPACT_TAGS = ["metal", "glass", "wood"];
11669
12045
  var GLOW_FROM_C = 430;
11670
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;
11671
12052
  var FIRE_CEILING_SECONDS = 3600;
11672
12053
  var GENERIC_REFERENCE_RADIUS = 15;
11673
12054
  var PRIMED_FIRE_RADIUS = 0.2;
@@ -11712,6 +12093,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11712
12093
  if (def.fracture === "tear") return "impact_soft";
11713
12094
  return def.fracture === "none" ? "impact_metal" : "impact_wood";
11714
12095
  }
12096
+ var dragSoundKey = (def) => impactKey(def).replace("impact_", "drag_");
11715
12097
  function fractureKey(def, style) {
11716
12098
  switch (style) {
11717
12099
  case "shatter":
@@ -11753,6 +12135,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11753
12135
  __publicField(this, "autoSplit");
11754
12136
  __publicField(this, "maxFragments");
11755
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);
11756
12146
  __publicField(this, "queue", []);
11757
12147
  __publicField(this, "unsubscribe");
11758
12148
  __publicField(this, "visuals");
@@ -11814,6 +12204,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11814
12204
  this.cages = new RebarCages(THREE, o.scene);
11815
12205
  this.pieceSys = new PieceSystem(THREE, {
11816
12206
  groundAt: this.groundAt,
12207
+ staticTopAt: (x, z, y, step) => this.staticTopAt(x, z, y, step),
11817
12208
  gravity: this.gravity,
11818
12209
  scene: o.scene,
11819
12210
  onHardLanding: (p, point, speed) => this.hardLanding(p, point, speed)
@@ -11826,6 +12217,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11826
12217
  // Binding
11827
12218
  // ----------------------------------------------------------------------------------------------------------------------------------
11828
12219
  bind(objectId, root, options = {}) {
12220
+ this.staticDirty.add(objectId);
11829
12221
  if (this.bounds.has(objectId)) this.unbind(objectId);
11830
12222
  const state = this.world.state(objectId);
11831
12223
  const partIds = new Set((state?.parts ?? []).map((p) => p.id));
@@ -11945,6 +12337,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11945
12337
  }
11946
12338
  }
11947
12339
  unbind(objectId) {
12340
+ this.staticDirty.add(objectId);
11948
12341
  const b = this.bounds.get(objectId);
11949
12342
  if (!b) return;
11950
12343
  this.restore(objectId);
@@ -11984,6 +12377,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
11984
12377
  return m > 0 ? m : 5;
11985
12378
  }
11986
12379
  makePiece(objectId, options = {}) {
12380
+ this.staticDirty.add(objectId);
11987
12381
  const b = this.bounds.get(objectId);
11988
12382
  if (!b) throw new Error(`game-destruction-three: object "${objectId}" is not bound`);
11989
12383
  if (b.root.parent && b.root.parent !== this.scene) this.scene.attach(b.root);
@@ -12096,7 +12490,100 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12096
12490
  // ----------------------------------------------------------------------------------------------------------------------------------
12097
12491
  // Events
12098
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
+ }
12099
12585
  apply(ev) {
12586
+ this.staticDirty.add(ev.object);
12100
12587
  switch (ev.type) {
12101
12588
  case "damage":
12102
12589
  if (ev.damageType === "heat" || ev.damageType === "fire" || ev.damageType === "explosive") this.activate(ev.object, ev.part);
@@ -12128,10 +12615,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12128
12615
  case "settle":
12129
12616
  this.onSettle(ev.object);
12130
12617
  break;
12131
- case "collapse":
12618
+ case "collapse": {
12619
+ const root = this.bounds.get(ev.object)?.root;
12620
+ const furniture = !!root && kitOriginOf(root)?.kind === "furniture";
12132
12621
  this.splitLoose(ev.object);
12133
- this.playEvent({ kind: "collapse" }, this.rootPoint(ev.object));
12622
+ if (!furniture || ev.mass >= COLLAPSE_CLOUD_KG) this.playEvent({ kind: "collapse" }, this.rootPoint(ev.object));
12623
+ else this.vfx.burst("dust", this.rootPoint(ev.object), [0, 1, 0], clamp6(Math.round(ev.mass / 4), 4, 24), 0.8);
12134
12624
  break;
12625
+ }
12135
12626
  case "disassemble_progress": {
12136
12627
  const k = `unscrew:${ev.joint}`;
12137
12628
  if (this.time - (this.throttles.get(k) ?? -1) > 0.25) {
@@ -12161,6 +12652,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12161
12652
  case "stage":
12162
12653
  this.onStage(ev);
12163
12654
  break;
12655
+ case "chip":
12656
+ this.onChip(ev);
12657
+ break;
12164
12658
  case "expose":
12165
12659
  this.cages.set(`${ev.object}|${ev.part}`, ev.bars);
12166
12660
  if (ev.bars.length > 0) {
@@ -12417,6 +12911,23 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12417
12911
  if (!played) this.vfx.burst("dust", ev.opening.center, [0, 1, 0], 14, 1.2, { color });
12418
12912
  this.sound(impactKey(def), ev.opening.center, 0.8);
12419
12913
  }
12914
+ /**
12915
+ * 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
12916
+ * the wall as its courses without that unit); a part drawn as one box keeps its box.
12917
+ */
12918
+ onChip(ev) {
12919
+ const pv = this.partVis(ev.object, ev.part);
12920
+ const def = pv ? this.world.materials.get(pv.material) : void 0;
12921
+ const color = (pv ? this.partColor(pv, def) : null) ?? fallbackColor(pv?.material ?? "", def);
12922
+ const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
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
+ }
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 });
12929
+ this.sound(impactKey(def), ev.center, 0.7);
12930
+ }
12420
12931
  onFracture(ev) {
12421
12932
  const b = this.bounds.get(ev.object);
12422
12933
  const pv = b?.parts.get(ev.part);
@@ -12494,6 +13005,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12494
13005
  this.pieceSys.setCenter(old, center);
12495
13006
  old.mass = Math.max(0.05, this.massOf(objectId));
12496
13007
  old.sleeping = false;
13008
+ } else if (kitOriginOf(b.root)?.kind === "furniture") {
13009
+ this.makePiece(objectId);
13010
+ this.tipIfTopHeavy(b);
12497
13011
  }
12498
13012
  }
12499
13013
  /** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
@@ -12540,8 +13054,32 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12540
13054
  }
12541
13055
  const piece = this.pieceSys.create(newId, group, half, center, this.massOf(newId), vel, ang);
12542
13056
  nb.piece = piece;
13057
+ if (kitOriginOf(from.root)?.kind === "furniture") this.tipIfTopHeavy(nb);
12543
13058
  this.o.onPiece?.(piece);
12544
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
+ }
12545
13083
  onExplode(ev, options = {}) {
12546
13084
  const p = ev.point;
12547
13085
  if (options.visual !== false) this.playExplosion(ev, options);
@@ -12792,6 +13330,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
12792
13330
  this.vfx.sprites().clear();
12793
13331
  this.ownVfx?.world.clear();
12794
13332
  }
13333
+ wakeRegion(x, z, radius) {
13334
+ this.fragments.wakeRegion(x, z, radius);
13335
+ this.pieceSys.wakeRegion(x, z, radius);
13336
+ }
12795
13337
  stats() {
12796
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 };
12797
13339
  }
@@ -13084,7 +13626,15 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13084
13626
  volume: 0,
13085
13627
  sources: ["cena:L1316 fire loop (silent in the page: its gain is never raised)", "sdk: the level while burning"]
13086
13628
  };
13087
- 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] };
13088
13638
 
13089
13639
  // src/audio/sound.ts
13090
13640
  var DESTRUCTION_SOUND_KEYS = {
@@ -13094,6 +13644,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
13094
13644
  impact_glass: { sound: "ping" },
13095
13645
  impact_masonry: { sound: "chip" },
13096
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" },
13097
13652
  shatter_glass: { sound: "glass_shatter" },
13098
13653
  crumble: { sound: "crumble" },
13099
13654
  splinter: { sound: "crack" },