@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.
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
  }
@@ -970,6 +1144,9 @@ function specFromObject3D(THREE, root, options = {}) {
970
1144
  return { spec, parts: meshesOf };
971
1145
  }
972
1146
 
1147
+ // src/renderer.ts
1148
+ import { kitOriginOf } from "@adatechnology/game-contracts";
1149
+
973
1150
  // src/heat.ts
974
1151
  var STOPS = [
975
1152
  [450, 0],
@@ -1015,9 +1192,10 @@ function shellDisplacementAt(THREE, worldPoint, shells, out) {
1015
1192
  const ids = [0, 1, 2].map((corner) => index ? index.getX(t * 3 + corner) : t * 3 + corner);
1016
1193
  ids.forEach((id, corner) => corners[corner].set(shell.before[id * 3], shell.before[id * 3 + 1], shell.before[id * 3 + 2]));
1017
1194
  triangle.set(corners[0], corners[1], corners[2]);
1195
+ if (triangle.getArea() <= 1e-14) continue;
1018
1196
  triangle.closestPointToPoint(local, closest);
1019
1197
  const distance = closest.distanceToSquared(local);
1020
- if (distance >= nearest) continue;
1198
+ if (!(distance < nearest)) continue;
1021
1199
  nearest = distance;
1022
1200
  nearestScale = shell.mesh.matrixWorld.getMaxScaleOnAxis() || 1;
1023
1201
  triangle.getBarycoord(closest, weights);
@@ -1196,6 +1374,20 @@ var Visuals = class {
1196
1374
  return out;
1197
1375
  }
1198
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
+ }
1199
1391
  worldBox(mv, out) {
1200
1392
  if (!mv.host) return out.setFromObject(mv.mesh);
1201
1393
  this.localBox(mv, out);
@@ -1940,9 +2132,15 @@ var Visuals = class {
1940
2132
 
1941
2133
  // src/renderer.ts
1942
2134
  var rendererCount = 0;
2135
+ var COLLAPSE_CLOUD_KG = 1e3;
1943
2136
  var IMPACT_TAGS = ["metal", "glass", "wood"];
1944
2137
  var GLOW_FROM_C = 430;
1945
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;
1946
2144
  var FIRE_CEILING_SECONDS = 3600;
1947
2145
  var GENERIC_REFERENCE_RADIUS = 15;
1948
2146
  var PRIMED_FIRE_RADIUS = 0.2;
@@ -1987,6 +2185,7 @@ function impactKey(def) {
1987
2185
  if (def.fracture === "tear") return "impact_soft";
1988
2186
  return def.fracture === "none" ? "impact_metal" : "impact_wood";
1989
2187
  }
2188
+ var dragSoundKey = (def) => impactKey(def).replace("impact_", "drag_");
1990
2189
  function fractureKey(def, style) {
1991
2190
  switch (style) {
1992
2191
  case "shatter":
@@ -2028,6 +2227,14 @@ var Renderer = class {
2028
2227
  __publicField(this, "autoSplit");
2029
2228
  __publicField(this, "maxFragments");
2030
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);
2031
2238
  __publicField(this, "queue", []);
2032
2239
  __publicField(this, "unsubscribe");
2033
2240
  __publicField(this, "visuals");
@@ -2089,6 +2296,7 @@ var Renderer = class {
2089
2296
  this.cages = new RebarCages(THREE, o.scene);
2090
2297
  this.pieceSys = new PieceSystem(THREE, {
2091
2298
  groundAt: this.groundAt,
2299
+ staticTopAt: (x, z, y, step) => this.staticTopAt(x, z, y, step),
2092
2300
  gravity: this.gravity,
2093
2301
  scene: o.scene,
2094
2302
  onHardLanding: (p, point, speed) => this.hardLanding(p, point, speed)
@@ -2101,6 +2309,7 @@ var Renderer = class {
2101
2309
  // Binding
2102
2310
  // ----------------------------------------------------------------------------------------------------------------------------------
2103
2311
  bind(objectId, root, options = {}) {
2312
+ this.staticDirty.add(objectId);
2104
2313
  if (this.bounds.has(objectId)) this.unbind(objectId);
2105
2314
  const state = this.world.state(objectId);
2106
2315
  const partIds = new Set((state?.parts ?? []).map((p) => p.id));
@@ -2220,6 +2429,7 @@ var Renderer = class {
2220
2429
  }
2221
2430
  }
2222
2431
  unbind(objectId) {
2432
+ this.staticDirty.add(objectId);
2223
2433
  const b = this.bounds.get(objectId);
2224
2434
  if (!b) return;
2225
2435
  this.restore(objectId);
@@ -2259,6 +2469,7 @@ var Renderer = class {
2259
2469
  return m > 0 ? m : 5;
2260
2470
  }
2261
2471
  makePiece(objectId, options = {}) {
2472
+ this.staticDirty.add(objectId);
2262
2473
  const b = this.bounds.get(objectId);
2263
2474
  if (!b) throw new Error(`game-destruction-three: object "${objectId}" is not bound`);
2264
2475
  if (b.root.parent && b.root.parent !== this.scene) this.scene.attach(b.root);
@@ -2371,7 +2582,100 @@ var Renderer = class {
2371
2582
  // ----------------------------------------------------------------------------------------------------------------------------------
2372
2583
  // Events
2373
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
+ }
2374
2677
  apply(ev) {
2678
+ this.staticDirty.add(ev.object);
2375
2679
  switch (ev.type) {
2376
2680
  case "damage":
2377
2681
  if (ev.damageType === "heat" || ev.damageType === "fire" || ev.damageType === "explosive") this.activate(ev.object, ev.part);
@@ -2403,10 +2707,14 @@ var Renderer = class {
2403
2707
  case "settle":
2404
2708
  this.onSettle(ev.object);
2405
2709
  break;
2406
- case "collapse":
2710
+ case "collapse": {
2711
+ const root = this.bounds.get(ev.object)?.root;
2712
+ const furniture = !!root && kitOriginOf(root)?.kind === "furniture";
2407
2713
  this.splitLoose(ev.object);
2408
- this.playEvent({ kind: "collapse" }, this.rootPoint(ev.object));
2714
+ if (!furniture || ev.mass >= COLLAPSE_CLOUD_KG) this.playEvent({ kind: "collapse" }, this.rootPoint(ev.object));
2715
+ else this.vfx.burst("dust", this.rootPoint(ev.object), [0, 1, 0], clamp(Math.round(ev.mass / 4), 4, 24), 0.8);
2409
2716
  break;
2717
+ }
2410
2718
  case "disassemble_progress": {
2411
2719
  const k = `unscrew:${ev.joint}`;
2412
2720
  if (this.time - (this.throttles.get(k) ?? -1) > 0.25) {
@@ -2436,6 +2744,9 @@ var Renderer = class {
2436
2744
  case "stage":
2437
2745
  this.onStage(ev);
2438
2746
  break;
2747
+ case "chip":
2748
+ this.onChip(ev);
2749
+ break;
2439
2750
  case "expose":
2440
2751
  this.cages.set(`${ev.object}|${ev.part}`, ev.bars);
2441
2752
  if (ev.bars.length > 0) {
@@ -2692,6 +3003,23 @@ var Renderer = class {
2692
3003
  if (!played) this.vfx.burst("dust", ev.opening.center, [0, 1, 0], 14, 1.2, { color });
2693
3004
  this.sound(impactKey(def), ev.opening.center, 0.8);
2694
3005
  }
3006
+ /**
3007
+ * 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
3008
+ * the wall as its courses without that unit); a part drawn as one box keeps its box.
3009
+ */
3010
+ onChip(ev) {
3011
+ const pv = this.partVis(ev.object, ev.part);
3012
+ const def = pv ? this.world.materials.get(pv.material) : void 0;
3013
+ const color = (pv ? this.partColor(pv, def) : null) ?? fallbackColor(pv?.material ?? "", def);
3014
+ const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
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
+ }
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 });
3021
+ this.sound(impactKey(def), ev.center, 0.7);
3022
+ }
2695
3023
  onFracture(ev) {
2696
3024
  const b = this.bounds.get(ev.object);
2697
3025
  const pv = b?.parts.get(ev.part);
@@ -2769,6 +3097,9 @@ var Renderer = class {
2769
3097
  this.pieceSys.setCenter(old, center);
2770
3098
  old.mass = Math.max(0.05, this.massOf(objectId));
2771
3099
  old.sleeping = false;
3100
+ } else if (kitOriginOf(b.root)?.kind === "furniture") {
3101
+ this.makePiece(objectId);
3102
+ this.tipIfTopHeavy(b);
2772
3103
  }
2773
3104
  }
2774
3105
  /** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
@@ -2815,8 +3146,32 @@ var Renderer = class {
2815
3146
  }
2816
3147
  const piece = this.pieceSys.create(newId, group, half, center, this.massOf(newId), vel, ang);
2817
3148
  nb.piece = piece;
3149
+ if (kitOriginOf(from.root)?.kind === "furniture") this.tipIfTopHeavy(nb);
2818
3150
  this.o.onPiece?.(piece);
2819
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
+ }
2820
3175
  onExplode(ev, options = {}) {
2821
3176
  const p = ev.point;
2822
3177
  if (options.visual !== false) this.playExplosion(ev, options);
@@ -3067,6 +3422,10 @@ var Renderer = class {
3067
3422
  this.vfx.sprites().clear();
3068
3423
  this.ownVfx?.world.clear();
3069
3424
  }
3425
+ wakeRegion(x, z, radius) {
3426
+ this.fragments.wakeRegion(x, z, radius);
3427
+ this.pieceSys.wakeRegion(x, z, radius);
3428
+ }
3070
3429
  stats() {
3071
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 };
3072
3431
  }
@@ -3180,7 +3539,15 @@ var BURN_LOOP = {
3180
3539
  volume: 0,
3181
3540
  sources: ["cena:L1316 fire loop (silent in the page: its gain is never raised)", "sdk: the level while burning"]
3182
3541
  };
3183
- 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] };
3184
3551
 
3185
3552
  // src/audio/sound.ts
3186
3553
  import { GAME_SFX_BANK } from "@adatechnology/game-audio";
@@ -3191,6 +3558,11 @@ var DESTRUCTION_SOUND_KEYS = {
3191
3558
  impact_glass: { sound: "ping" },
3192
3559
  impact_masonry: { sound: "chip" },
3193
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" },
3194
3566
  shatter_glass: { sound: "glass_shatter" },
3195
3567
  crumble: { sound: "crumble" },
3196
3568
  splinter: { sound: "crack" },
@@ -3263,11 +3635,14 @@ export {
3263
3635
  DEFAULT_FIRE,
3264
3636
  DESTRUCTION_SOUND_BANK,
3265
3637
  DESTRUCTION_SOUND_KEYS,
3638
+ DRAG_SOUNDS,
3266
3639
  EXPLOSION_BOOM,
3267
3640
  SCENE_SOUNDS,
3268
3641
  createDestructionRenderer,
3269
3642
  createDestructionSound,
3643
+ dragSoundKey,
3270
3644
  heatColor,
3645
+ impactKey,
3271
3646
  specFromObject3D
3272
3647
  };
3273
3648
  //# sourceMappingURL=index.js.map