@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.
package/dist/index.js CHANGED
@@ -7,6 +7,44 @@ import { blastShove } from "@adatechnology/game-contracts";
7
7
  import { createVfx, effectForEvent, EVENT_EFFECTS_MAP } from "@adatechnology/game-vfx";
8
8
  import { createVfxRenderer } from "@adatechnology/game-vfx-three";
9
9
 
10
+ // src/fitted-boxes.ts
11
+ var SLICE_LENGTH = 0.12;
12
+ var MAX_SLICES = 12;
13
+ var SNUG_RATIO = 1.1;
14
+ function pushFittedBoxes(local, matrix, out) {
15
+ const e = matrix.elements;
16
+ const sizes = [0, 1, 2].map((k) => k === 0 ? local.max.x - local.min.x : k === 1 ? local.max.y - local.min.y : local.max.z - local.min.z);
17
+ const lengths = [0, 1, 2].map((k) => sizes[k] * Math.hypot(e[k * 4], e[k * 4 + 1], e[k * 4 + 2]));
18
+ const longest = lengths.indexOf(Math.max(...lengths));
19
+ const whole = worldBoxOf(local, matrix, longest, 0, 1);
20
+ const span = (whole[3] - whole[0]) * (whole[4] - whole[1]) * (whole[5] - whole[2]);
21
+ const volume = lengths[0] * lengths[1] * lengths[2];
22
+ const slices = span <= volume * SNUG_RATIO ? 1 : Math.min(MAX_SLICES, Math.ceil(lengths[longest] / SLICE_LENGTH));
23
+ if (slices <= 1) {
24
+ out.push(...whole);
25
+ return;
26
+ }
27
+ for (let i = 0; i < slices; i++) out.push(...worldBoxOf(local, matrix, longest, i / slices, (i + 1) / slices));
28
+ }
29
+ function worldBoxOf(local, matrix, axis, from, to) {
30
+ const e = matrix.elements;
31
+ const low = [local.min.x, local.min.y, local.min.z];
32
+ const high = [local.max.x, local.max.y, local.max.z];
33
+ const start = low[axis];
34
+ const size = high[axis] - start;
35
+ low[axis] = start + size * from;
36
+ high[axis] = start + size * to;
37
+ const box = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];
38
+ for (const x of [low[0], high[0]]) for (const y of [low[1], high[1]]) for (const z of [low[2], high[2]]) {
39
+ for (let k = 0; k < 3; k++) {
40
+ const v = e[k] * x + e[k + 4] * y + e[k + 8] * z + e[k + 12];
41
+ box[k] = Math.min(box[k], v);
42
+ box[k + 3] = Math.max(box[k + 3], v);
43
+ }
44
+ }
45
+ return box;
46
+ }
47
+
10
48
  // src/math.ts
11
49
  function createRng(seed) {
12
50
  let a = seed >>> 0;
@@ -48,12 +86,20 @@ function mixHex(a, b, t) {
48
86
  var SHEET_MAX = 4e-3;
49
87
  var SHEET_AIR = 2.5;
50
88
  var SHEET_SWAY = 3;
89
+ var BIG_LENGTH = 0.2;
90
+ var BIG_WIDTH = 0.03;
91
+ var isBig = (size) => {
92
+ const [, middle, longest] = [...size].sort((a, b) => a - b);
93
+ return longest >= BIG_LENGTH && middle >= BIG_WIDTH;
94
+ };
51
95
  var FragmentSystem = class {
52
96
  constructor(THREE, o) {
53
97
  __publicField(this, "THREE", THREE);
54
98
  __publicField(this, "o", o);
55
99
  __publicField(this, "list", []);
56
100
  __publicField(this, "box");
101
+ /** The mesh box of a fragment before its scale. */
102
+ __publicField(this, "unitBox", { min: { x: -0.5, y: -0.5, z: -0.5 }, max: { x: 0.5, y: 0.5, z: 0.5 } });
57
103
  __publicField(this, "shardGeos", []);
58
104
  __publicField(this, "materials", /* @__PURE__ */ new Map());
59
105
  __publicField(this, "q");
@@ -62,6 +108,8 @@ var FragmentSystem = class {
62
108
  __publicField(this, "v");
63
109
  __publicField(this, "w");
64
110
  __publicField(this, "rng", createRng(1374496523));
111
+ /** Count of `update` calls: what makes the fragments of one burst tell apart from older ones. */
112
+ __publicField(this, "tick", 0);
65
113
  __publicField(this, "markGeometry", null);
66
114
  __publicField(this, "markMaterial", null);
67
115
  this.box = new THREE.BoxGeometry(1, 1, 1);
@@ -129,8 +177,28 @@ var FragmentSystem = class {
129
177
  mesh.add(disc);
130
178
  }
131
179
  }
132
- /** Create the mesh of a fragment and start its motion. `maxAllowed` is the live budget. */
180
+ /**
181
+ * 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
182
+ * 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
183
+ * out. Returns null when the fragment was not made.
184
+ */
133
185
  spawn(f, material, home, maxAllowed) {
186
+ if (this.list.length >= maxAllowed) {
187
+ let alive = 0;
188
+ let oldest;
189
+ let oldestSmall;
190
+ for (const l of this.list) {
191
+ if (l.dying) continue;
192
+ alive++;
193
+ oldest ?? (oldest = l);
194
+ if (!l.body) oldestSmall ?? (oldestSmall = l);
195
+ }
196
+ oldest = oldestSmall ?? oldest;
197
+ if (alive >= maxAllowed) {
198
+ if (!oldest || oldest.born === this.tick) return null;
199
+ oldest.dying = true;
200
+ }
201
+ }
134
202
  const geo = f.shape === "shard" ? this.shardGeometry(hashString(f.id)) : this.box;
135
203
  const mesh = new this.THREE.Mesh(geo, material);
136
204
  mesh.position.set(f.center[0], f.center[1], f.center[2]);
@@ -154,19 +222,42 @@ var FragmentSystem = class {
154
222
  rest: 0,
155
223
  fade: 1,
156
224
  dying: false,
157
- air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0
225
+ air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0,
226
+ body: null,
227
+ born: this.tick
158
228
  };
159
- this.list.push(live);
160
- let alive = this.list.reduce((n, l) => n + (l.dying ? 0 : 1), 0);
161
- for (let i = 0; i < this.list.length && alive > maxAllowed; i++) {
162
- if (!this.list[i].dying) {
163
- this.list[i].dying = true;
164
- alive--;
165
- }
229
+ if (f.shape !== "shard" && isBig(f.size)) {
230
+ live.body = {
231
+ mass: f.mass,
232
+ push: (x, z) => {
233
+ live.vel.x = x;
234
+ live.vel.z = z;
235
+ live.vel.y = Math.max(live.vel.y, 0.3);
236
+ live.asleep = false;
237
+ live.rest = 0;
238
+ }
239
+ };
166
240
  }
241
+ this.list.push(live);
167
242
  this.addMarks(mesh, f);
168
243
  return mesh;
169
244
  }
245
+ /** The boxes of the big fragments around (x, z). */
246
+ solidsNear(x, z, radius, found) {
247
+ const boxes = [];
248
+ for (const f of this.list) {
249
+ if (!f.body || f.dying) continue;
250
+ const reach = f.half.length() + radius;
251
+ if (Math.abs(f.mesh.position.x - x) > reach || Math.abs(f.mesh.position.z - z) > reach) continue;
252
+ f.mesh.updateMatrixWorld();
253
+ boxes.length = 0;
254
+ pushFittedBoxes(this.unitBox, f.mesh.matrixWorld, boxes);
255
+ for (let i = 0; i < boxes.length; i += 6) {
256
+ if (x < boxes[i] - radius || x > boxes[i + 3] + radius || z < boxes[i + 2] - radius || z > boxes[i + 5] + radius) continue;
257
+ found(f.home, [boxes[i], boxes[i + 1], boxes[i + 2]], [boxes[i + 3], boxes[i + 4], boxes[i + 5]], f.body);
258
+ }
259
+ }
260
+ }
170
261
  /** Add velocity to every fragment within `radius` of a point (blasts). */
171
262
  blast(point, radius, speed) {
172
263
  for (const f of this.list) {
@@ -182,7 +273,16 @@ var FragmentSystem = class {
182
273
  f.rest = 0;
183
274
  }
184
275
  }
276
+ /** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
277
+ wakeRegion(x, z, radius) {
278
+ for (const f of this.list) {
279
+ if (!f.asleep || Math.hypot(f.mesh.position.x - x, f.mesh.position.z - z) > radius) continue;
280
+ f.asleep = false;
281
+ f.rest = 0;
282
+ }
283
+ }
185
284
  update(dt) {
285
+ this.tick++;
186
286
  const g = this.o.gravity;
187
287
  for (let i = this.list.length - 1; i >= 0; i--) {
188
288
  const f = this.list[i];
@@ -197,7 +297,7 @@ var FragmentSystem = class {
197
297
  }
198
298
  if (f.asleep) {
199
299
  f.rest += dt;
200
- if (this.o.life > 0 && f.rest > this.o.life) f.dying = true;
300
+ if (this.o.life > 0 && f.rest > this.o.life && !f.body) f.dying = true;
201
301
  continue;
202
302
  }
203
303
  this.step(f, dt, g);
@@ -271,7 +371,7 @@ var FragmentSystem = class {
271
371
  let small = 0;
272
372
  for (let i = 1; i < 3; i++) if (half[i] < half[small]) small = i;
273
373
  let axis = small;
274
- if (Math.abs(up[small]) < 0.35) {
374
+ if (Math.abs(up[small]) < 0.35 && !f.body) {
275
375
  axis = 0;
276
376
  for (let i = 1; i < 3; i++) if (Math.abs(up[i]) > Math.abs(up[axis])) axis = i;
277
377
  }
@@ -397,6 +497,30 @@ var HARD_LANDING = 3;
397
497
  var SLOP = 2e-3;
398
498
  var NARROW = 0.3;
399
499
  var TOPPLE = 3;
500
+ var TIP_ACCELERATION = 30;
501
+ var TIP_LYING = 0.25;
502
+ var TIP_SECONDS = 3;
503
+ var STEP = 0.03;
504
+ var boxTopAt = (s, x, z) => {
505
+ const e = s.e;
506
+ const dx = x - s.c[0], dz = z - s.c[2];
507
+ let t0 = -Infinity, t1 = Infinity;
508
+ for (let k = 0; k < 3; k++) {
509
+ const p = e[k * 4] * dx + e[k * 4 + 2] * dz;
510
+ const d = e[k * 4 + 1];
511
+ const h = s.half[k];
512
+ if (Math.abs(d) < 1e-9) {
513
+ if (Math.abs(p) > h) return -Infinity;
514
+ continue;
515
+ }
516
+ let a = (-h - p) / d, b = (h - p) / d;
517
+ if (a > b) [a, b] = [b, a];
518
+ t0 = Math.max(t0, a);
519
+ t1 = Math.min(t1, b);
520
+ if (t0 > t1) return -Infinity;
521
+ }
522
+ return s.c[1] + t1;
523
+ };
400
524
  var PieceSystem = class {
401
525
  constructor(THREE, o) {
402
526
  __publicField(this, "THREE", THREE);
@@ -408,6 +532,7 @@ var PieceSystem = class {
408
532
  __publicField(this, "dq");
409
533
  __publicField(this, "m");
410
534
  __publicField(this, "ax");
535
+ __publicField(this, "solids", []);
411
536
  this.p = new THREE.Vector3();
412
537
  this.q = new THREE.Quaternion();
413
538
  this.dq = new THREE.Quaternion();
@@ -417,6 +542,11 @@ var PieceSystem = class {
417
542
  get(objectId) {
418
543
  return this.list.find((b) => b.objectId === objectId) ?? null;
419
544
  }
545
+ /** 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. */
546
+ tip(body, x, z) {
547
+ body.tipping = { x, z, seconds: TIP_SECONDS };
548
+ body.sleeping = false;
549
+ }
420
550
  create(objectId, root, halfExtents, localCenter, mass, velocity, angular) {
421
551
  const existing = this.get(objectId);
422
552
  if (existing) {
@@ -430,6 +560,7 @@ var PieceSystem = class {
430
560
  }
431
561
  const body = {
432
562
  objectId,
563
+ tipping: null,
433
564
  root,
434
565
  velocity: velocity ? [...velocity] : [0, 0, 0],
435
566
  angularVelocity: angular ? [...angular] : [0, 0, 0],
@@ -499,8 +630,21 @@ var PieceSystem = class {
499
630
  const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
500
631
  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];
501
632
  }
633
+ /** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
634
+ wakeRegion(x, z, radius) {
635
+ for (const b of this.list) {
636
+ if (!b.sleeping || b.held || Math.hypot(b.root.position.x - x, b.root.position.z - z) > radius) continue;
637
+ b.sleeping = false;
638
+ b.still = 0;
639
+ }
640
+ }
502
641
  update(dt) {
503
642
  this.time += dt;
643
+ this.solids = this.list.map((body) => {
644
+ const c = this.centerOf(body, new this.THREE.Vector3());
645
+ const h = body.halfExtents;
646
+ 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]) };
647
+ });
504
648
  for (const b of this.list) {
505
649
  if (b.held) {
506
650
  b.velocity = [0, 0, 0];
@@ -514,6 +658,17 @@ var PieceSystem = class {
514
658
  }
515
659
  this.separate(dt);
516
660
  }
661
+ /** Where a corner of `self` at `(x, y, z)` lands: the ground, the top of another piece or of something fixed under it. */
662
+ supportAt(self, x, y, z, step) {
663
+ let top = this.o.groundAt(x, z);
664
+ for (const s of this.solids) {
665
+ if (s.body === self || s.body.held || Math.abs(x - s.c[0]) > s.r || Math.abs(z - s.c[2]) > s.r) continue;
666
+ const t = boxTopAt(s, x, z);
667
+ if (t > top && t <= y + step) top = t;
668
+ }
669
+ const fixed = this.o.staticTopAt?.(x, z, y, step) ?? -Infinity;
670
+ return Math.max(top, fixed);
671
+ }
517
672
  integrate(b, dt) {
518
673
  const c = this.centerOf(b, this.p);
519
674
  const pos = [c.x, c.y, c.z];
@@ -552,8 +707,9 @@ var PieceSystem = class {
552
707
  const sy = i & 2 ? hy : -hy;
553
708
  const sz = i & 4 ? hz : -hz;
554
709
  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];
555
- const gy = this.o.groundAt(pos[0] + r[0], pos[2] + r[2]);
556
- const pen = gy - (pos[1] + r[1]);
710
+ const cy = pos[1] + r[1];
711
+ const gy = this.supportAt(b, pos[0] + r[0], cy, pos[2] + r[2], STEP + Math.max(0, -v[1]) * h * 2);
712
+ const pen = gy - cy;
557
713
  if (pen > -SLOP) {
558
714
  rs.push(r);
559
715
  if (pen > maxPen) maxPen = pen;
@@ -576,7 +732,7 @@ var PieceSystem = class {
576
732
  }
577
733
  const lx = pos[0] + sx / rs.length;
578
734
  const lz = pos[2] + sz / rs.length;
579
- landPoint = [lx, this.o.groundAt(lx, lz), lz];
735
+ landPoint = [lx, pos[1] - Math.max(...rs.map((r) => -r[1])), lz];
580
736
  }
581
737
  for (let it = 0; it < 4; it++) {
582
738
  for (const r of rs) {
@@ -629,6 +785,15 @@ var PieceSystem = class {
629
785
  const wx = x1 - x0;
630
786
  const wz = z1 - z0;
631
787
  unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
788
+ if (b.tipping) {
789
+ b.tipping.seconds -= h;
790
+ if (height < TIP_LYING || b.tipping.seconds <= 0) b.tipping = null;
791
+ else {
792
+ w[0] += b.tipping.x * TIP_ACCELERATION * h;
793
+ w[2] += b.tipping.z * TIP_ACCELERATION * h;
794
+ unstable = true;
795
+ }
796
+ }
632
797
  if (unstable) {
633
798
  const dirX = x0 + x1 > 0 ? -1 : 1;
634
799
  const dirZ = z0 + z1 > 0 ? -1 : 1;
@@ -665,6 +830,11 @@ var PieceSystem = class {
665
830
  b.still = 0;
666
831
  }
667
832
  }
833
+ /** Half the height of the box of a piece as it is turned now. */
834
+ halfHeight(b) {
835
+ const e = this.m.makeRotationFromQuaternion(b.root.quaternion).elements;
836
+ return Math.abs(e[1]) * b.halfExtents[0] + Math.abs(e[5]) * b.halfExtents[1] + Math.abs(e[9]) * b.halfExtents[2];
837
+ }
668
838
  /** Push overlapping pieces apart (horizontal bounding circles), gently, so they do not interpenetrate forever. */
669
839
  separate(dt) {
670
840
  const n = this.list.length;
@@ -683,6 +853,7 @@ var PieceSystem = class {
683
853
  this.centerOf(a, ca);
684
854
  this.centerOf(b, cb);
685
855
  if (Math.abs(ca.y - cb.y) > ra + rb) continue;
856
+ if (Math.abs(ca.y - cb.y) >= (this.halfHeight(a) + this.halfHeight(b)) * 0.9) continue;
686
857
  const rh = (Math.max(a.halfExtents[0], a.halfExtents[2]) + Math.max(b.halfExtents[0], b.halfExtents[2])) * 0.8;
687
858
  let dx = cb.x - ca.x;
688
859
  let dz = cb.z - ca.z;
@@ -843,6 +1014,7 @@ function specFromObject3D(THREE, root, options = {}) {
843
1014
  const fill = options.fillOf?.(o, material);
844
1015
  const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
845
1016
  const contains = containsOf(ud.destructionContains);
1017
+ const thickness = typeof ud.destructionThickness === "number" && ud.destructionThickness > 0 ? ud.destructionThickness : void 0;
846
1018
  const existing = explicit ? byKey.get(explicit) : void 0;
847
1019
  if (existing) {
848
1020
  const before = sizeOf(existing.box);
@@ -855,8 +1027,9 @@ function specFromObject3D(THREE, root, options = {}) {
855
1027
  existing.skin = existing.skin && skin;
856
1028
  if (fill !== void 0) existing.fill = fill;
857
1029
  if (contains) existing.contains = contains;
1030
+ if (thickness !== void 0) existing.thickness = Math.min(existing.thickness ?? thickness, thickness);
858
1031
  } else {
859
- 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 };
1032
+ 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 };
860
1033
  entries.push(e);
861
1034
  if (explicit) byKey.set(explicit, e);
862
1035
  }
@@ -924,7 +1097,8 @@ function specFromObject3D(THREE, root, options = {}) {
924
1097
  const minSide = Math.min(...size);
925
1098
  if (e.fill !== void 0) part.fill = e.fill;
926
1099
  else if (e.hollow) part.fill = 0.05;
927
- if (minSide < 0.01 && METAL.test(e.material)) {
1100
+ if (e.thickness !== void 0) part.thickness = e.thickness;
1101
+ else if (minSide < 0.01 && METAL.test(e.material)) {
928
1102
  part.fill = e.fill ?? 1;
929
1103
  part.thickness = Math.max(minSide, 5e-4);
930
1104
  }
@@ -1018,9 +1192,10 @@ function shellDisplacementAt(THREE, worldPoint, shells, out) {
1018
1192
  const ids = [0, 1, 2].map((corner) => index ? index.getX(t * 3 + corner) : t * 3 + corner);
1019
1193
  ids.forEach((id, corner) => corners[corner].set(shell.before[id * 3], shell.before[id * 3 + 1], shell.before[id * 3 + 2]));
1020
1194
  triangle.set(corners[0], corners[1], corners[2]);
1195
+ if (triangle.getArea() <= 1e-14) continue;
1021
1196
  triangle.closestPointToPoint(local, closest);
1022
1197
  const distance = closest.distanceToSquared(local);
1023
- if (distance >= nearest) continue;
1198
+ if (!(distance < nearest)) continue;
1024
1199
  nearest = distance;
1025
1200
  nearestScale = shell.mesh.matrixWorld.getMaxScaleOnAxis() || 1;
1026
1201
  triangle.getBarycoord(closest, weights);
@@ -1199,6 +1374,20 @@ var Visuals = class {
1199
1374
  return out;
1200
1375
  }
1201
1376
  /** Box of the part in world space. */
1377
+ /** Appends the boxes that follow the mesh in the world (`pushFittedBoxes`) to `out`. */
1378
+ fittedBoxes(mv, out) {
1379
+ const box = new this.THREE.Box3();
1380
+ if (mv.host) this.localBox(mv, box);
1381
+ else {
1382
+ const geometry = mv.mesh.geometry;
1383
+ if (!geometry.boundingBox) geometry.computeBoundingBox();
1384
+ if (geometry.boundingBox) box.copy(geometry.boundingBox);
1385
+ else box.makeEmpty();
1386
+ }
1387
+ if (box.isEmpty()) return;
1388
+ mv.mesh.updateWorldMatrix(true, false);
1389
+ pushFittedBoxes(box, mv.mesh.matrixWorld, out);
1390
+ }
1202
1391
  worldBox(mv, out) {
1203
1392
  if (!mv.host) return out.setFromObject(mv.mesh);
1204
1393
  this.localBox(mv, out);
@@ -1947,6 +2136,11 @@ var COLLAPSE_CLOUD_KG = 1e3;
1947
2136
  var IMPACT_TAGS = ["metal", "glass", "wood"];
1948
2137
  var GLOW_FROM_C = 430;
1949
2138
  var QUEUE_CAP = 4e3;
2139
+ var STATIC_REFRESH = 0.5;
2140
+ var STATIC_SPAN = 3;
2141
+ var FEET_BAND = 0.03;
2142
+ var TIP_MIN_HEIGHT = 0.3;
2143
+ var TIP_NARROW = 0.3;
1950
2144
  var FIRE_CEILING_SECONDS = 3600;
1951
2145
  var GENERIC_REFERENCE_RADIUS = 15;
1952
2146
  var PRIMED_FIRE_RADIUS = 0.2;
@@ -1991,6 +2185,7 @@ function impactKey(def) {
1991
2185
  if (def.fracture === "tear") return "impact_soft";
1992
2186
  return def.fracture === "none" ? "impact_metal" : "impact_wood";
1993
2187
  }
2188
+ var dragSoundKey = (def) => impactKey(def).replace("impact_", "drag_");
1994
2189
  function fractureKey(def, style) {
1995
2190
  switch (style) {
1996
2191
  case "shatter":
@@ -2032,6 +2227,14 @@ var Renderer = class {
2032
2227
  __publicField(this, "autoSplit");
2033
2228
  __publicField(this, "maxFragments");
2034
2229
  __publicField(this, "bounds", /* @__PURE__ */ new Map());
2230
+ /** World boxes (min xyz, max xyz) of the drawn meshes of the bound objects that are not pieces, for the pieces to land on. */
2231
+ __publicField(this, "staticBoxes", /* @__PURE__ */ new Map());
2232
+ /** Objects to measure again; `staticAll` measures every one. */
2233
+ __publicField(this, "staticDirty", /* @__PURE__ */ new Set());
2234
+ __publicField(this, "staticAll", true);
2235
+ /** Boxes of the pieces' meshes (as `staticBoxes`), with the time they were measured. */
2236
+ __publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
2237
+ __publicField(this, "staticAt", 0);
2035
2238
  __publicField(this, "queue", []);
2036
2239
  __publicField(this, "unsubscribe");
2037
2240
  __publicField(this, "visuals");
@@ -2093,6 +2296,7 @@ var Renderer = class {
2093
2296
  this.cages = new RebarCages(THREE, o.scene);
2094
2297
  this.pieceSys = new PieceSystem(THREE, {
2095
2298
  groundAt: this.groundAt,
2299
+ staticTopAt: (x, z, y, step) => this.staticTopAt(x, z, y, step),
2096
2300
  gravity: this.gravity,
2097
2301
  scene: o.scene,
2098
2302
  onHardLanding: (p, point, speed) => this.hardLanding(p, point, speed)
@@ -2105,6 +2309,7 @@ var Renderer = class {
2105
2309
  // Binding
2106
2310
  // ----------------------------------------------------------------------------------------------------------------------------------
2107
2311
  bind(objectId, root, options = {}) {
2312
+ this.staticDirty.add(objectId);
2108
2313
  if (this.bounds.has(objectId)) this.unbind(objectId);
2109
2314
  const state = this.world.state(objectId);
2110
2315
  const partIds = new Set((state?.parts ?? []).map((p) => p.id));
@@ -2224,6 +2429,7 @@ var Renderer = class {
2224
2429
  }
2225
2430
  }
2226
2431
  unbind(objectId) {
2432
+ this.staticDirty.add(objectId);
2227
2433
  const b = this.bounds.get(objectId);
2228
2434
  if (!b) return;
2229
2435
  this.restore(objectId);
@@ -2263,6 +2469,7 @@ var Renderer = class {
2263
2469
  return m > 0 ? m : 5;
2264
2470
  }
2265
2471
  makePiece(objectId, options = {}) {
2472
+ this.staticDirty.add(objectId);
2266
2473
  const b = this.bounds.get(objectId);
2267
2474
  if (!b) throw new Error(`game-destruction-three: object "${objectId}" is not bound`);
2268
2475
  if (b.root.parent && b.root.parent !== this.scene) this.scene.attach(b.root);
@@ -2375,7 +2582,100 @@ var Renderer = class {
2375
2582
  // ----------------------------------------------------------------------------------------------------------------------------------
2376
2583
  // Events
2377
2584
  // ----------------------------------------------------------------------------------------------------------------------------------
2585
+ /**
2586
+ * 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
2587
+ * 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`
2588
+ * are left out (walls, floors, roofs: the terrain under them is the host's `groundAt`).
2589
+ */
2590
+ staticTopAt(x, z, y, step) {
2591
+ let top = -Infinity;
2592
+ for (const a of this.staticNow().values()) {
2593
+ if (a.length === 0 || x < a[0] || x > a[3] || z < a[2] || z > a[5]) continue;
2594
+ for (let i = 6; i < a.length; i += 6) {
2595
+ if (x < a[i] || x > a[i + 3] || z < a[i + 2] || z > a[i + 5]) continue;
2596
+ const t = a[i + 4];
2597
+ if (t > top && t <= y + step) top = t;
2598
+ }
2599
+ }
2600
+ return top;
2601
+ }
2602
+ /** 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). */
2603
+ staticNow() {
2604
+ if (this.time - this.staticAt > STATIC_REFRESH) {
2605
+ this.staticAt = this.time;
2606
+ this.staticAll = true;
2607
+ }
2608
+ if (!this.staticAll && this.staticDirty.size === 0) return this.staticBoxes;
2609
+ const box = new this.THREE.Box3();
2610
+ const measure = (b) => {
2611
+ if (b.piece || this.pieceSys.get(b.id)) {
2612
+ this.staticBoxes.delete(b.id);
2613
+ return;
2614
+ }
2615
+ const out = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];
2616
+ for (const mv of this.meshesOf(b)) {
2617
+ if (mv.host || mv.loose || !this.visuals.shown(mv)) continue;
2618
+ this.visuals.worldBox(mv, box);
2619
+ if (box.isEmpty() || box.max.x - box.min.x > STATIC_SPAN || box.max.z - box.min.z > STATIC_SPAN) continue;
2620
+ this.visuals.fittedBoxes(mv, out);
2621
+ for (let k = 0; k < 3; k++) {
2622
+ out[k] = Math.min(out[k], box.min.getComponent(k));
2623
+ out[k + 3] = Math.max(out[k + 3], box.max.getComponent(k));
2624
+ }
2625
+ }
2626
+ this.staticBoxes.set(b.id, out.length > 6 ? out : []);
2627
+ };
2628
+ if (this.staticAll) {
2629
+ this.staticBoxes.clear();
2630
+ for (const b of this.bounds.values()) measure(b);
2631
+ } else {
2632
+ for (const id of this.staticDirty) {
2633
+ const b = this.bounds.get(id);
2634
+ if (b) measure(b);
2635
+ else this.staticBoxes.delete(id);
2636
+ }
2637
+ }
2638
+ this.staticAll = false;
2639
+ this.staticDirty.clear();
2640
+ return this.staticBoxes;
2641
+ }
2642
+ /** The boxes of a piece's drawn meshes, measured once per frame. */
2643
+ pieceBoxesOf(pc) {
2644
+ const cached = this.pieceBoxes.get(pc);
2645
+ if (cached && cached.at === this.time) return cached.boxes;
2646
+ const boxes = [];
2647
+ const b = this.bounds.get(pc.objectId);
2648
+ if (b) {
2649
+ for (const mv of this.meshesOf(b)) {
2650
+ if (this.visuals.shown(mv)) this.visuals.fittedBoxes(mv, boxes);
2651
+ }
2652
+ }
2653
+ this.pieceBoxes.set(pc, { at: this.time, boxes });
2654
+ return boxes;
2655
+ }
2656
+ solidsNear(x, z, radius) {
2657
+ const found = [];
2658
+ const scan = (a, id, piece, from) => {
2659
+ if (from > 0 && (a.length === 0 || x < a[0] - radius || x > a[3] + radius || z < a[2] - radius || z > a[5] + radius)) return;
2660
+ for (let i = from; i < a.length; i += 6) {
2661
+ if (x < a[i] - radius || x > a[i + 3] + radius || z < a[i + 2] - radius || z > a[i + 5] + radius) continue;
2662
+ found.push({ objectId: id, min: [a[i], a[i + 1], a[i + 2]], max: [a[i + 3], a[i + 4], a[i + 5]], piece });
2663
+ }
2664
+ };
2665
+ for (const [id, a] of this.staticNow()) scan(a, id, null, 6);
2666
+ const c = this.v1;
2667
+ for (const pc of this.pieceSys.list) {
2668
+ if (pc.held) continue;
2669
+ this.pieceSys.centerOf(pc, c);
2670
+ const reach = Math.hypot(pc.halfExtents[0], pc.halfExtents[1], pc.halfExtents[2]) + radius;
2671
+ if (Math.abs(c.x - x) > reach || Math.abs(c.z - z) > reach) continue;
2672
+ scan(this.pieceBoxesOf(pc), pc.objectId, pc, 0);
2673
+ }
2674
+ this.fragments.solidsNear(x, z, radius, (home, min, max, loose) => found.push({ objectId: home, min, max, piece: null, loose }));
2675
+ return found;
2676
+ }
2378
2677
  apply(ev) {
2678
+ this.staticDirty.add(ev.object);
2379
2679
  switch (ev.type) {
2380
2680
  case "damage":
2381
2681
  if (ev.damageType === "heat" || ev.damageType === "fire" || ev.damageType === "explosive") this.activate(ev.object, ev.part);
@@ -2712,7 +3012,11 @@ var Renderer = class {
2712
3012
  const def = pv ? this.world.materials.get(pv.material) : void 0;
2713
3013
  const color = (pv ? this.partColor(pv, def) : null) ?? fallbackColor(pv?.material ?? "", def);
2714
3014
  const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
2715
- for (const fr of ev.fragments) this.fragments.spawn(fr, this.fragments.materialFor(fr.material, fallbackColor(fr.material, def), false), ev.object, maxAlive);
3015
+ for (const fr of ev.fragments) {
3016
+ const fdef = this.world.materials.get(fr.material);
3017
+ const tone2 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
3018
+ this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone2, false), ev.object, maxAlive);
3019
+ }
2716
3020
  if (!this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.center)) this.vfx.burst("dust", ev.center, [0, 1, 0], 6, 0.8, { color });
2717
3021
  this.sound(impactKey(def), ev.center, 0.7);
2718
3022
  }
@@ -2793,6 +3097,9 @@ var Renderer = class {
2793
3097
  this.pieceSys.setCenter(old, center);
2794
3098
  old.mass = Math.max(0.05, this.massOf(objectId));
2795
3099
  old.sleeping = false;
3100
+ } else if (kitOriginOf(b.root)?.kind === "furniture") {
3101
+ this.makePiece(objectId);
3102
+ this.tipIfTopHeavy(b);
2796
3103
  }
2797
3104
  }
2798
3105
  /** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
@@ -2839,8 +3146,32 @@ var Renderer = class {
2839
3146
  }
2840
3147
  const piece = this.pieceSys.create(newId, group, half, center, this.massOf(newId), vel, ang);
2841
3148
  nb.piece = piece;
3149
+ if (kitOriginOf(from.root)?.kind === "furniture") this.tipIfTopHeavy(nb);
2842
3150
  this.o.onPiece?.(piece);
2843
3151
  }
3152
+ /**
3153
+ * 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
3154
+ * high and its feet are thin, so it gets the start of the fall, across the line of its feet.
3155
+ */
3156
+ tipIfTopHeavy(b) {
3157
+ const piece = b.piece;
3158
+ if (!piece) return;
3159
+ const box = new this.THREE.Box3();
3160
+ const boxes = this.meshesOf(b).filter((mv) => this.visuals.shown(mv)).map((mv) => this.visuals.worldBox(mv, new this.THREE.Box3()));
3161
+ if (boxes.length === 0) return;
3162
+ for (const one of boxes) box.union(one);
3163
+ const height = box.max.y - box.min.y;
3164
+ const feet = new this.THREE.Box3();
3165
+ for (const one of boxes) if (one.min.y < box.min.y + FEET_BAND) feet.union(one);
3166
+ const spanX = feet.max.x - feet.min.x;
3167
+ const spanZ = feet.max.z - feet.min.z;
3168
+ if (height < TIP_MIN_HEIGHT || Math.min(spanX, spanZ) >= TIP_NARROW * height) return;
3169
+ const across = spanX <= spanZ ? 0 : 2;
3170
+ 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;
3171
+ const away = lean < 0 ? -1 : 1;
3172
+ if (across === 0) this.pieceSys.tip(piece, 0, -away);
3173
+ else this.pieceSys.tip(piece, away, 0);
3174
+ }
2844
3175
  onExplode(ev, options = {}) {
2845
3176
  const p = ev.point;
2846
3177
  if (options.visual !== false) this.playExplosion(ev, options);
@@ -3091,6 +3422,10 @@ var Renderer = class {
3091
3422
  this.vfx.sprites().clear();
3092
3423
  this.ownVfx?.world.clear();
3093
3424
  }
3425
+ wakeRegion(x, z, radius) {
3426
+ this.fragments.wakeRegion(x, z, radius);
3427
+ this.pieceSys.wakeRegion(x, z, radius);
3428
+ }
3094
3429
  stats() {
3095
3430
  return { fragments: this.fragments.count, particles: this.vfx.sprites().live + this.ownedVfxParticles(), pieces: this.pieceSys.list.length, decals: this.visuals.decalCount + this.ground.count };
3096
3431
  }
@@ -3204,7 +3539,15 @@ var BURN_LOOP = {
3204
3539
  volume: 0,
3205
3540
  sources: ["cena:L1316 fire loop (silent in the page: its gain is never raised)", "sdk: the level while burning"]
3206
3541
  };
3207
- var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
3542
+ var drag = (id, layers) => ({ ...sceneRecipe(id, layers, ""), sources: ["sdk: a base dragged on a floor, one grain per stretch"] });
3543
+ var DRAG_SOUNDS = [
3544
+ drag("drag_wood", [noise({ type: "bandpass", f: 380, q: 1.4, d: 0.28, vol: 0.5 }), noise({ type: "bandpass", f: [900, 1300], q: 3, d: 0.08, vol: 0.14 })]),
3545
+ drag("drag_metal", [noise({ type: "bandpass", f: 1500, q: 4, d: 0.26, vol: 0.32 }), tone({ f: [880, 1240], f2: 760, d: 0.18, vol: 0.05 })]),
3546
+ drag("drag_glass", [noise({ type: "bandpass", f: 2600, q: 5, d: 0.2, vol: 0.22 })]),
3547
+ drag("drag_masonry", [noise({ type: "bandpass", f: 260, q: 0.9, d: 0.3, vol: 0.62 }), ...many(3, () => noise({ type: "bandpass", f: [600, 1200], q: 2, d: 0.04, vol: 0.14, delay: [0, 0.2] }))]),
3548
+ drag("drag_soft", [noise({ type: "lowpass", f: 420, d: 0.28, vol: 0.3 })])
3549
+ ];
3550
+ var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
3208
3551
 
3209
3552
  // src/audio/sound.ts
3210
3553
  import { GAME_SFX_BANK } from "@adatechnology/game-audio";
@@ -3215,6 +3558,11 @@ var DESTRUCTION_SOUND_KEYS = {
3215
3558
  impact_glass: { sound: "ping" },
3216
3559
  impact_masonry: { sound: "chip" },
3217
3560
  impact_soft: { sound: "thud" },
3561
+ drag_wood: { sound: "drag_wood" },
3562
+ drag_metal: { sound: "drag_metal" },
3563
+ drag_glass: { sound: "drag_glass" },
3564
+ drag_masonry: { sound: "drag_masonry" },
3565
+ drag_soft: { sound: "drag_soft" },
3218
3566
  shatter_glass: { sound: "glass_shatter" },
3219
3567
  crumble: { sound: "crumble" },
3220
3568
  splinter: { sound: "crack" },
@@ -3287,11 +3635,14 @@ export {
3287
3635
  DEFAULT_FIRE,
3288
3636
  DESTRUCTION_SOUND_BANK,
3289
3637
  DESTRUCTION_SOUND_KEYS,
3638
+ DRAG_SOUNDS,
3290
3639
  EXPLOSION_BOOM,
3291
3640
  SCENE_SOUNDS,
3292
3641
  createDestructionRenderer,
3293
3642
  createDestructionSound,
3643
+ dragSoundKey,
3294
3644
  heatColor,
3645
+ impactKey,
3295
3646
  specFromObject3D
3296
3647
  };
3297
3648
  //# sourceMappingURL=index.js.map