@adatechnology/game-destruction-three 0.11.1 → 0.12.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/README.md +6 -0
- package/dist/index.d.ts +26 -8
- package/dist/index.global.js +630 -174
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +445 -86
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -7,6 +7,9 @@ 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/fragments.ts
|
|
11
|
+
import { markKitOrigin } from "@adatechnology/game-contracts";
|
|
12
|
+
|
|
10
13
|
// src/fitted-boxes.ts
|
|
11
14
|
var SLICE_LENGTH = 0.12;
|
|
12
15
|
var MAX_SLICES = 12;
|
|
@@ -88,6 +91,7 @@ var SHEET_AIR = 2.5;
|
|
|
88
91
|
var SHEET_SWAY = 3;
|
|
89
92
|
var BIG_LENGTH = 0.2;
|
|
90
93
|
var BIG_WIDTH = 0.03;
|
|
94
|
+
var FRAGMENT_SEED = 1374496523;
|
|
91
95
|
var isBig = (size) => {
|
|
92
96
|
const [, middle, longest] = [...size].sort((a, b) => a - b);
|
|
93
97
|
return longest >= BIG_LENGTH && middle >= BIG_WIDTH;
|
|
@@ -107,7 +111,7 @@ var FragmentSystem = class {
|
|
|
107
111
|
__publicField(this, "m");
|
|
108
112
|
__publicField(this, "v");
|
|
109
113
|
__publicField(this, "w");
|
|
110
|
-
__publicField(this, "rng", createRng(
|
|
114
|
+
__publicField(this, "rng", createRng(FRAGMENT_SEED));
|
|
111
115
|
/** Count of `update` calls: what makes the fragments of one burst tell apart from older ones. */
|
|
112
116
|
__publicField(this, "tick", 0);
|
|
113
117
|
__publicField(this, "markGeometry", null);
|
|
@@ -208,6 +212,8 @@ var FragmentSystem = class {
|
|
|
208
212
|
mesh.scale.copy(sc);
|
|
209
213
|
mesh.castShadow = true;
|
|
210
214
|
mesh.userData.destructionDebris = true;
|
|
215
|
+
mesh.name = "fragment";
|
|
216
|
+
markKitOrigin(mesh, { owner: "game-destruction-three", kind: "fragment", id: f.material });
|
|
211
217
|
this.o.scene.add(mesh);
|
|
212
218
|
const live = {
|
|
213
219
|
mesh,
|
|
@@ -224,7 +230,8 @@ var FragmentSystem = class {
|
|
|
224
230
|
dying: false,
|
|
225
231
|
air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0,
|
|
226
232
|
body: null,
|
|
227
|
-
born: this.tick
|
|
233
|
+
born: this.tick,
|
|
234
|
+
life: Math.max(0, typeof this.o.life === "function" ? this.o.life(f, home) : this.o.life)
|
|
228
235
|
};
|
|
229
236
|
if (f.shape !== "shard" && isBig(f.size)) {
|
|
230
237
|
live.body = {
|
|
@@ -233,8 +240,7 @@ var FragmentSystem = class {
|
|
|
233
240
|
live.vel.x = x;
|
|
234
241
|
live.vel.z = z;
|
|
235
242
|
live.vel.y = Math.max(live.vel.y, 0.3);
|
|
236
|
-
live
|
|
237
|
-
live.rest = 0;
|
|
243
|
+
this.rouse(live);
|
|
238
244
|
}
|
|
239
245
|
};
|
|
240
246
|
}
|
|
@@ -269,16 +275,20 @@ var FragmentSystem = class {
|
|
|
269
275
|
d.y += k * 0.4;
|
|
270
276
|
f.vel.add(d);
|
|
271
277
|
f.ang.add(this.w.set(this.rng() - 0.5, this.rng() - 0.5, this.rng() - 0.5).multiplyScalar(k));
|
|
272
|
-
f
|
|
273
|
-
f.rest = 0;
|
|
278
|
+
this.rouse(f);
|
|
274
279
|
}
|
|
275
280
|
}
|
|
281
|
+
/** A fragment that came to rest stops having its matrix recomputed on every frame; whatever moves it again calls this first. */
|
|
282
|
+
rouse(f) {
|
|
283
|
+
f.asleep = false;
|
|
284
|
+
f.rest = 0;
|
|
285
|
+
f.mesh.matrixAutoUpdate = true;
|
|
286
|
+
}
|
|
276
287
|
/** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
|
|
277
288
|
wakeRegion(x, z, radius) {
|
|
278
289
|
for (const f of this.list) {
|
|
279
290
|
if (!f.asleep || Math.hypot(f.mesh.position.x - x, f.mesh.position.z - z) > radius) continue;
|
|
280
|
-
f
|
|
281
|
-
f.rest = 0;
|
|
291
|
+
this.rouse(f);
|
|
282
292
|
}
|
|
283
293
|
}
|
|
284
294
|
update(dt) {
|
|
@@ -287,6 +297,7 @@ var FragmentSystem = class {
|
|
|
287
297
|
for (let i = this.list.length - 1; i >= 0; i--) {
|
|
288
298
|
const f = this.list[i];
|
|
289
299
|
if (f.dying) {
|
|
300
|
+
f.mesh.matrixAutoUpdate = true;
|
|
290
301
|
f.fade -= dt / 0.6;
|
|
291
302
|
if (f.fade <= 0) {
|
|
292
303
|
this.o.scene.remove(f.mesh);
|
|
@@ -297,7 +308,10 @@ var FragmentSystem = class {
|
|
|
297
308
|
}
|
|
298
309
|
if (f.asleep) {
|
|
299
310
|
f.rest += dt;
|
|
300
|
-
if (
|
|
311
|
+
if (f.life > 0 && f.rest > f.life && !f.body) {
|
|
312
|
+
f.dying = true;
|
|
313
|
+
f.mesh.matrixAutoUpdate = true;
|
|
314
|
+
}
|
|
301
315
|
continue;
|
|
302
316
|
}
|
|
303
317
|
this.step(f, dt, g);
|
|
@@ -346,6 +360,8 @@ var FragmentSystem = class {
|
|
|
346
360
|
f.still += dt;
|
|
347
361
|
if (f.still > 0.4) {
|
|
348
362
|
f.asleep = true;
|
|
363
|
+
f.mesh.updateMatrix();
|
|
364
|
+
f.mesh.matrixAutoUpdate = false;
|
|
349
365
|
f.vel.set(0, 0, 0);
|
|
350
366
|
f.ang.set(0, 0, 0);
|
|
351
367
|
}
|
|
@@ -395,6 +411,12 @@ var FragmentSystem = class {
|
|
|
395
411
|
for (const f of this.list) this.o.scene.remove(f.mesh);
|
|
396
412
|
this.list.length = 0;
|
|
397
413
|
}
|
|
414
|
+
/** `clear`, the random stream back to its seed and the tick to 0: what comes next flies as on a new system. */
|
|
415
|
+
reset() {
|
|
416
|
+
this.clear();
|
|
417
|
+
this.rng = createRng(FRAGMENT_SEED);
|
|
418
|
+
this.tick = 0;
|
|
419
|
+
}
|
|
398
420
|
dispose() {
|
|
399
421
|
this.clear();
|
|
400
422
|
this.box.dispose();
|
|
@@ -493,6 +515,9 @@ var GroundSpots = class {
|
|
|
493
515
|
var SUB_DT = 1 / 120;
|
|
494
516
|
var MU = 0.7;
|
|
495
517
|
var SLEEP_AFTER = 0.5;
|
|
518
|
+
var CALM_DRIFT = 0.01;
|
|
519
|
+
var CALM_TURN = 0.05;
|
|
520
|
+
var CALM_SLEEP_AFTER = 1.5;
|
|
496
521
|
var HARD_LANDING = 3;
|
|
497
522
|
var SLOP = 2e-3;
|
|
498
523
|
var NARROW = 0.3;
|
|
@@ -501,6 +526,10 @@ var TIP_ACCELERATION = 30;
|
|
|
501
526
|
var TIP_LYING = 0.25;
|
|
502
527
|
var TIP_SECONDS = 3;
|
|
503
528
|
var STEP = 0.03;
|
|
529
|
+
var WARM_SLOTS = 8 + 12 * 7;
|
|
530
|
+
var SOLVER_PASSES = 10;
|
|
531
|
+
var SIDE_BOUNCE = 0.1;
|
|
532
|
+
var EDGE_POINTS = [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75];
|
|
504
533
|
var boxTopAt = (s, x, z) => {
|
|
505
534
|
const e = s.e;
|
|
506
535
|
const dx = x - s.c[0], dz = z - s.c[2];
|
|
@@ -571,6 +600,8 @@ var PieceSystem = class {
|
|
|
571
600
|
localCenter: [localCenter.x, localCenter.y, localCenter.z],
|
|
572
601
|
centerV: localCenter.clone(),
|
|
573
602
|
still: 0,
|
|
603
|
+
calmAt: null,
|
|
604
|
+
calm: 0,
|
|
574
605
|
lastHit: -1,
|
|
575
606
|
onGround: false
|
|
576
607
|
};
|
|
@@ -589,6 +620,11 @@ var PieceSystem = class {
|
|
|
589
620
|
clear() {
|
|
590
621
|
this.list.length = 0;
|
|
591
622
|
}
|
|
623
|
+
/** `clear` and the clock back to 0. */
|
|
624
|
+
reset() {
|
|
625
|
+
this.clear();
|
|
626
|
+
this.time = 0;
|
|
627
|
+
}
|
|
592
628
|
/** World position of the centre of mass. */
|
|
593
629
|
centerOf(b, out) {
|
|
594
630
|
return out.copy(b.centerV).applyQuaternion(b.root.quaternion).add(b.root.position);
|
|
@@ -613,6 +649,7 @@ var PieceSystem = class {
|
|
|
613
649
|
}
|
|
614
650
|
b.sleeping = false;
|
|
615
651
|
b.still = 0;
|
|
652
|
+
b.calmAt = null;
|
|
616
653
|
}
|
|
617
654
|
/** I^-1 * (x, y, z) in world space for a box with the body's half extents and mass (needs `this.m` to hold the body's rotation). */
|
|
618
655
|
mulInvInertia(b, x, y, z) {
|
|
@@ -630,12 +667,51 @@ var PieceSystem = class {
|
|
|
630
667
|
const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
|
|
631
668
|
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];
|
|
632
669
|
}
|
|
670
|
+
/** The points of the box that touch the ground, in its own frame: the 8 corners, then (with `edges`) `EDGE_POINTS` along each of its 12 edges. */
|
|
671
|
+
samples(b, edges) {
|
|
672
|
+
const [hx, hy, hz] = b.halfExtents;
|
|
673
|
+
const out = [];
|
|
674
|
+
for (let i = 0; i < 8; i++) out.push([i & 1 ? hx : -hx, i & 2 ? hy : -hy, i & 4 ? hz : -hz]);
|
|
675
|
+
if (!edges) return out;
|
|
676
|
+
const h = [hx, hy, hz];
|
|
677
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
678
|
+
const a1 = (axis + 1) % 3;
|
|
679
|
+
const a2 = (axis + 2) % 3;
|
|
680
|
+
for (let k = 0; k < 4; k++) {
|
|
681
|
+
for (const f of EDGE_POINTS) {
|
|
682
|
+
const p = [0, 0, 0];
|
|
683
|
+
p[axis] = f * h[axis];
|
|
684
|
+
p[a1] = k & 1 ? h[a1] : -h[a1];
|
|
685
|
+
p[a2] = k & 2 ? h[a2] : -h[a2];
|
|
686
|
+
out.push(p);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return out;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* An impulse along the unit normal `n` at `r` (from the centre of mass, world) that stops the point closing in along `n` (with `bounce`); `e` holds the body's rotation.
|
|
694
|
+
*/
|
|
695
|
+
contactImpulse(b, e, r, n, bounce) {
|
|
696
|
+
const v = b.velocity;
|
|
697
|
+
const w = b.angularVelocity;
|
|
698
|
+
const vn = (v[0] + w[1] * r[2] - w[2] * r[1]) * n[0] + (v[1] + w[2] * r[0] - w[0] * r[2]) * n[1] + (v[2] + w[0] * r[1] - w[1] * r[0]) * n[2];
|
|
699
|
+
if (vn >= 0) return;
|
|
700
|
+
const a = this.mulInvInertiaWith(b, e, r[1] * n[2] - r[2] * n[1], r[2] * n[0] - r[0] * n[2], r[0] * n[1] - r[1] * n[0]);
|
|
701
|
+
const k = (a[1] * r[2] - a[2] * r[1]) * n[0] + (a[2] * r[0] - a[0] * r[2]) * n[1] + (a[0] * r[1] - a[1] * r[0]) * n[2];
|
|
702
|
+
const j = -(1 + bounce) * vn / (1 / b.mass + k);
|
|
703
|
+
for (let i = 0; i < 3; i++) {
|
|
704
|
+
v[i] += j * n[i] / b.mass;
|
|
705
|
+
w[i] += a[i] * j;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
633
708
|
/** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
|
|
634
709
|
wakeRegion(x, z, radius) {
|
|
635
710
|
for (const b of this.list) {
|
|
636
711
|
if (!b.sleeping || b.held || Math.hypot(b.root.position.x - x, b.root.position.z - z) > radius) continue;
|
|
637
712
|
b.sleeping = false;
|
|
638
713
|
b.still = 0;
|
|
714
|
+
b.calmAt = null;
|
|
639
715
|
}
|
|
640
716
|
}
|
|
641
717
|
update(dt) {
|
|
@@ -651,6 +727,7 @@ var PieceSystem = class {
|
|
|
651
727
|
b.angularVelocity = [0, 0, 0];
|
|
652
728
|
b.sleeping = false;
|
|
653
729
|
b.still = 0;
|
|
730
|
+
b.calmAt = null;
|
|
654
731
|
continue;
|
|
655
732
|
}
|
|
656
733
|
if (b.sleeping) continue;
|
|
@@ -675,7 +752,6 @@ var PieceSystem = class {
|
|
|
675
752
|
const q = this.q.copy(b.root.quaternion);
|
|
676
753
|
const v = b.velocity;
|
|
677
754
|
const w = b.angularVelocity;
|
|
678
|
-
const [hx, hy, hz] = b.halfExtents;
|
|
679
755
|
const invM = 1 / b.mass;
|
|
680
756
|
const steps = clamp(Math.ceil(dt / SUB_DT), 1, 8);
|
|
681
757
|
const h = dt / steps;
|
|
@@ -684,7 +760,30 @@ var PieceSystem = class {
|
|
|
684
760
|
let contactedLast = false;
|
|
685
761
|
let touched = false;
|
|
686
762
|
let unstable = false;
|
|
763
|
+
const corners = this.samples(b, false);
|
|
764
|
+
this.m.makeRotationFromQuaternion(q);
|
|
765
|
+
let gLow = Infinity;
|
|
766
|
+
let gHigh = -Infinity;
|
|
767
|
+
for (const [sx, sy, sz] of corners) {
|
|
768
|
+
const e0 = this.m.elements;
|
|
769
|
+
const y = pos[1] + e0[1] * sx + e0[5] * sy + e0[9] * sz;
|
|
770
|
+
const x = pos[0] + e0[0] * sx + e0[4] * sy + e0[8] * sz;
|
|
771
|
+
const z = pos[2] + e0[2] * sx + e0[6] * sy + e0[10] * sz;
|
|
772
|
+
const g = Math.max(this.o.groundAt(x, z), this.o.staticTopAt?.(x, z, y, STEP) ?? -Infinity);
|
|
773
|
+
gLow = Math.min(gLow, g);
|
|
774
|
+
gHigh = Math.max(gHigh, g);
|
|
775
|
+
}
|
|
776
|
+
const local = gHigh - gLow > STEP ? this.samples(b, true) : corners;
|
|
777
|
+
const prev = local.map(() => [0, 0, 0]);
|
|
687
778
|
for (let s = 0; s < steps; s++) {
|
|
779
|
+
this.m.makeRotationFromQuaternion(q);
|
|
780
|
+
const e0 = this.m.elements;
|
|
781
|
+
for (let i = 0; i < local.length; i++) {
|
|
782
|
+
const [sx, sy, sz] = local[i];
|
|
783
|
+
prev[i][0] = pos[0] + e0[0] * sx + e0[4] * sy + e0[8] * sz;
|
|
784
|
+
prev[i][1] = pos[1] + e0[1] * sx + e0[5] * sy + e0[9] * sz;
|
|
785
|
+
prev[i][2] = pos[2] + e0[2] * sx + e0[6] * sy + e0[10] * sz;
|
|
786
|
+
}
|
|
688
787
|
v[1] -= this.o.gravity * h;
|
|
689
788
|
pos[0] += v[0] * h;
|
|
690
789
|
pos[1] += v[1] * h;
|
|
@@ -701,20 +800,60 @@ var PieceSystem = class {
|
|
|
701
800
|
this.m.makeRotationFromQuaternion(q);
|
|
702
801
|
const e = this.m.elements;
|
|
703
802
|
const rs = [];
|
|
803
|
+
const ids = [];
|
|
704
804
|
let maxPen = 0;
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
805
|
+
const rise = STEP + Math.max(0, -v[1]) * h * 2;
|
|
806
|
+
let side = null;
|
|
807
|
+
let lowest = Infinity;
|
|
808
|
+
for (let i = 0; i < local.length; i++) {
|
|
809
|
+
const [sx, sy, sz] = local[i];
|
|
709
810
|
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];
|
|
811
|
+
const px = pos[0] + r[0];
|
|
812
|
+
const pz = pos[2] + r[2];
|
|
710
813
|
const cy = pos[1] + r[1];
|
|
711
|
-
const
|
|
814
|
+
const was = prev[i];
|
|
815
|
+
const raw = this.o.groundAt(px, pz);
|
|
816
|
+
if (raw > cy - SLOP && was[1] < raw - STEP) {
|
|
817
|
+
if (this.o.groundAt(was[0], was[2]) >= raw - STEP) {
|
|
818
|
+
const floor = b.floor;
|
|
819
|
+
if (floor !== void 0 && floor - cy > -SLOP && floor < raw - STEP) {
|
|
820
|
+
rs.push(r);
|
|
821
|
+
ids.push(i);
|
|
822
|
+
maxPen = Math.max(maxPen, floor - cy);
|
|
823
|
+
}
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
const dx = px - was[0];
|
|
827
|
+
const dz = pz - was[2];
|
|
828
|
+
const len = Math.hypot(dx, dz);
|
|
829
|
+
if (len > 1e-6) {
|
|
830
|
+
let lo = 0;
|
|
831
|
+
let hi = 1;
|
|
832
|
+
for (let it = 0; it < 6; it++) {
|
|
833
|
+
const mid = (lo + hi) / 2;
|
|
834
|
+
if (this.o.groundAt(was[0] + dx * mid, was[2] + dz * mid) >= raw - STEP) hi = mid;
|
|
835
|
+
else lo = mid;
|
|
836
|
+
}
|
|
837
|
+
const back = (1 - lo) * len + 1e-3;
|
|
838
|
+
if (!side || back > side.back) side = { back, dir: [dx / len, dz / len], r };
|
|
839
|
+
}
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
const gy = i < 8 ? this.supportAt(b, px, cy, pz, rise) : Math.max(raw, this.o.staticTopAt?.(px, pz, cy, rise) ?? -Infinity);
|
|
712
843
|
const pen = gy - cy;
|
|
713
844
|
if (pen > -SLOP) {
|
|
714
845
|
rs.push(r);
|
|
846
|
+
ids.push(i);
|
|
715
847
|
if (pen > maxPen) maxPen = pen;
|
|
848
|
+
lowest = Math.min(lowest, gy);
|
|
716
849
|
}
|
|
717
850
|
}
|
|
851
|
+
if (lowest < Infinity) b.floor = lowest;
|
|
852
|
+
if (side) {
|
|
853
|
+
pos[0] -= side.dir[0] * side.back;
|
|
854
|
+
pos[2] -= side.dir[1] * side.back;
|
|
855
|
+
this.contactImpulse(b, e, side.r, [-side.dir[0], 0, -side.dir[1]], SIDE_BOUNCE);
|
|
856
|
+
}
|
|
718
857
|
contactedLast = rs.length > 0;
|
|
719
858
|
if (contactedLast) touched = true;
|
|
720
859
|
if (!contactedLast) continue;
|
|
@@ -734,45 +873,88 @@ var PieceSystem = class {
|
|
|
734
873
|
const lz = pos[2] + sz / rs.length;
|
|
735
874
|
landPoint = [lx, pos[1] - Math.max(...rs.map((r) => -r[1])), lz];
|
|
736
875
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
876
|
+
const goal = rs.map((r) => {
|
|
877
|
+
const vy = v[1] + (w[2] * r[0] - w[0] * r[2]);
|
|
878
|
+
return vy < 0 ? -restitution * vy : 0;
|
|
879
|
+
});
|
|
880
|
+
const vx0 = v[0];
|
|
881
|
+
const vz0 = v[2];
|
|
882
|
+
if (!b.warm || b.warm.length !== WARM_SLOTS * 3) b.warm = new Float64Array(WARM_SLOTS * 3);
|
|
883
|
+
const warm = b.warm;
|
|
884
|
+
const accN = ids.map((id) => id < WARM_SLOTS ? warm[id * 3] : 0);
|
|
885
|
+
const accT = ids.map((id) => id < WARM_SLOTS ? [warm[id * 3 + 1], warm[id * 3 + 2]] : [0, 0]);
|
|
886
|
+
for (let k = 0; k < rs.length; k++) {
|
|
887
|
+
const r = rs[k];
|
|
888
|
+
const jn = accN[k];
|
|
889
|
+
const [fx, fz] = accT[k];
|
|
890
|
+
if (jn === 0 && fx === 0 && fz === 0) continue;
|
|
891
|
+
v[0] += fx * invM;
|
|
892
|
+
v[1] += jn * invM;
|
|
893
|
+
v[2] += fz * invM;
|
|
894
|
+
const dw = this.mulInvInertiaWith(b, e, r[1] * fz - r[2] * jn, r[2] * fx - r[0] * fz, r[0] * jn - r[1] * fx);
|
|
895
|
+
w[0] += dw[0];
|
|
896
|
+
w[1] += dw[1];
|
|
897
|
+
w[2] += dw[2];
|
|
898
|
+
}
|
|
899
|
+
for (let it = 0; it < SOLVER_PASSES; it++) {
|
|
900
|
+
for (let k = 0; k < rs.length; k++) {
|
|
901
|
+
const r = rs[k];
|
|
902
|
+
const vy = v[1] + (w[2] * r[0] - w[0] * r[2]);
|
|
744
903
|
const a = this.mulInvInertiaWith(b, e, -r[2], 0, r[0]);
|
|
745
904
|
const denom = invM + (a[2] * r[0] - a[0] * r[2]);
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
905
|
+
const total = Math.max(0, accN[k] + (goal[k] - vy) / denom);
|
|
906
|
+
const j = total - accN[k];
|
|
907
|
+
accN[k] = total;
|
|
908
|
+
if (j !== 0) {
|
|
909
|
+
v[1] += j * invM;
|
|
910
|
+
w[0] += a[0] * j;
|
|
911
|
+
w[1] += a[1] * j;
|
|
912
|
+
w[2] += a[2] * j;
|
|
913
|
+
}
|
|
914
|
+
const vx = v[0] + (w[1] * r[2] - w[2] * r[1]);
|
|
915
|
+
const vz = v[2] + (w[0] * r[1] - w[1] * r[0]);
|
|
755
916
|
const vt = Math.hypot(vx, vz);
|
|
756
|
-
if (vt
|
|
917
|
+
if (vt < 1e-6 && accT[k][0] === 0 && accT[k][1] === 0) continue;
|
|
918
|
+
let fx = accT[k][0];
|
|
919
|
+
let fz = accT[k][1];
|
|
920
|
+
if (vt > 1e-6) {
|
|
757
921
|
const tx = vx / vt;
|
|
758
922
|
const tz = vz / vt;
|
|
759
|
-
const
|
|
760
|
-
const ry = r[2] * tx - r[0] * tz;
|
|
761
|
-
const rz = -r[1] * tx;
|
|
762
|
-
const at = this.mulInvInertiaWith(b, e, rx, ry, rz);
|
|
923
|
+
const at = this.mulInvInertiaWith(b, e, r[1] * tz, r[2] * tx - r[0] * tz, -r[1] * tx);
|
|
763
924
|
const cx = at[1] * r[2] - at[2] * r[1];
|
|
764
925
|
const cz = at[0] * r[1] - at[1] * r[0];
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
926
|
+
const jt = vt / (invM + cx * tx + cz * tz);
|
|
927
|
+
fx -= tx * jt;
|
|
928
|
+
fz -= tz * jt;
|
|
929
|
+
}
|
|
930
|
+
const most = MU * accN[k];
|
|
931
|
+
const f = Math.hypot(fx, fz);
|
|
932
|
+
if (f > most) {
|
|
933
|
+
fx *= most / f;
|
|
934
|
+
fz *= most / f;
|
|
773
935
|
}
|
|
936
|
+
const dx = fx - accT[k][0];
|
|
937
|
+
const dz = fz - accT[k][1];
|
|
938
|
+
accT[k] = [fx, fz];
|
|
939
|
+
if (dx === 0 && dz === 0) continue;
|
|
940
|
+
v[0] += dx * invM;
|
|
941
|
+
v[2] += dz * invM;
|
|
942
|
+
const dw = this.mulInvInertiaWith(b, e, r[1] * dz, r[2] * dx - r[0] * dz, -r[1] * dx);
|
|
943
|
+
w[0] += dw[0];
|
|
944
|
+
w[1] += dw[1];
|
|
945
|
+
w[2] += dw[2];
|
|
774
946
|
}
|
|
775
947
|
}
|
|
948
|
+
pos[0] += (v[0] - vx0) * h;
|
|
949
|
+
pos[2] += (v[2] - vz0) * h;
|
|
950
|
+
warm.fill(0);
|
|
951
|
+
for (let k = 0; k < rs.length; k++) {
|
|
952
|
+
const id = ids[k];
|
|
953
|
+
if (id >= WARM_SLOTS) continue;
|
|
954
|
+
warm[id * 3] = accN[k];
|
|
955
|
+
warm[id * 3 + 1] = accT[k][0];
|
|
956
|
+
warm[id * 3 + 2] = accT[k][1];
|
|
957
|
+
}
|
|
776
958
|
let x0 = Infinity, x1 = -Infinity, z0 = Infinity, z1 = -Infinity, low = 0;
|
|
777
959
|
for (const r of rs) {
|
|
778
960
|
x0 = Math.min(x0, r[0]);
|
|
@@ -787,7 +969,9 @@ var PieceSystem = class {
|
|
|
787
969
|
unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
|
|
788
970
|
if (b.tipping) {
|
|
789
971
|
b.tipping.seconds -= h;
|
|
790
|
-
|
|
972
|
+
const [hx, hy, hz] = b.halfExtents;
|
|
973
|
+
const upHalf = Math.abs(e[1]) * hx + Math.abs(e[5]) * hy + Math.abs(e[9]) * hz;
|
|
974
|
+
if (upHalf < TIP_LYING || b.tipping.seconds <= 0) b.tipping = null;
|
|
791
975
|
else {
|
|
792
976
|
w[0] += b.tipping.x * TIP_ACCELERATION * h;
|
|
793
977
|
w[2] += b.tipping.z * TIP_ACCELERATION * h;
|
|
@@ -819,16 +1003,26 @@ var PieceSystem = class {
|
|
|
819
1003
|
this.o.onHardLanding(b, landPoint, landing);
|
|
820
1004
|
}
|
|
821
1005
|
const still = touched && !unstable && Math.hypot(v[0], v[1], v[2]) < 0.12 && Math.hypot(w[0], w[1], w[2]) < 0.45;
|
|
822
|
-
if (still)
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
|
|
1006
|
+
if (still) b.still += dt;
|
|
1007
|
+
else b.still = 0;
|
|
1008
|
+
if (b.still >= SLEEP_AFTER || this.staysInPlace(b, touched && !b.tipping, pos, q, dt)) {
|
|
1009
|
+
b.sleeping = true;
|
|
1010
|
+
b.calmAt = null;
|
|
1011
|
+
v[0] = v[1] = v[2] = 0;
|
|
1012
|
+
w[0] = w[1] = w[2] = 0;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
/** Whether the piece has kept to the same spot on the ground for `CALM_SLEEP_AFTER` (see `CALM_DRIFT`). */
|
|
1016
|
+
staysInPlace(b, touched, center, q, dt) {
|
|
1017
|
+
const at = b.calmAt;
|
|
1018
|
+
const turned = at ? 2 * Math.acos(Math.min(1, Math.abs(at.quaternion[0] * q.x + at.quaternion[1] * q.y + at.quaternion[2] * q.z + at.quaternion[3] * q.w))) : Infinity;
|
|
1019
|
+
if (!touched || !at || Math.hypot(center[0] - at.center[0], center[1] - at.center[1], center[2] - at.center[2]) > CALM_DRIFT || turned > CALM_TURN) {
|
|
1020
|
+
b.calmAt = touched ? { center: [center[0], center[1], center[2]], quaternion: [q.x, q.y, q.z, q.w] } : null;
|
|
1021
|
+
b.calm = 0;
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
b.calm += dt;
|
|
1025
|
+
return b.calm >= CALM_SLEEP_AFTER;
|
|
832
1026
|
}
|
|
833
1027
|
/** Half the height of the box of a piece as it is turned now. */
|
|
834
1028
|
halfHeight(b) {
|
|
@@ -846,8 +1040,9 @@ var PieceSystem = class {
|
|
|
846
1040
|
const a = this.list[i];
|
|
847
1041
|
for (let j = i + 1; j < n; j++) {
|
|
848
1042
|
const b = this.list[j];
|
|
849
|
-
|
|
850
|
-
|
|
1043
|
+
const fixedA = a.held || a.sleeping;
|
|
1044
|
+
const fixedB = b.held || b.sleeping;
|
|
1045
|
+
if (fixedA && fixedB) continue;
|
|
851
1046
|
const ra = Math.hypot(a.halfExtents[0], a.halfExtents[1], a.halfExtents[2]);
|
|
852
1047
|
const rb = Math.hypot(b.halfExtents[0], b.halfExtents[1], b.halfExtents[2]);
|
|
853
1048
|
this.centerOf(a, ca);
|
|
@@ -865,19 +1060,17 @@ var PieceSystem = class {
|
|
|
865
1060
|
d = 1;
|
|
866
1061
|
}
|
|
867
1062
|
const push = Math.min(maxMove, (rh - d) * 0.5);
|
|
868
|
-
const wa =
|
|
869
|
-
const wb =
|
|
1063
|
+
const wa = fixedA ? 0 : fixedB ? 1 : b.mass / (a.mass + b.mass);
|
|
1064
|
+
const wb = fixedB ? 0 : fixedA ? 1 : a.mass / (a.mass + b.mass);
|
|
870
1065
|
const ux = dx / (d || 1);
|
|
871
1066
|
const uz = dz / (d || 1);
|
|
872
|
-
if (!
|
|
1067
|
+
if (!fixedA) {
|
|
873
1068
|
a.root.position.x -= ux * push * 2 * wa;
|
|
874
1069
|
a.root.position.z -= uz * push * 2 * wa;
|
|
875
|
-
a.sleeping = false;
|
|
876
1070
|
}
|
|
877
|
-
if (!
|
|
1071
|
+
if (!fixedB) {
|
|
878
1072
|
b.root.position.x += ux * push * 2 * wb;
|
|
879
1073
|
b.root.position.z += uz * push * 2 * wb;
|
|
880
|
-
b.sleeping = false;
|
|
881
1074
|
}
|
|
882
1075
|
}
|
|
883
1076
|
}
|
|
@@ -1014,6 +1207,7 @@ function specFromObject3D(THREE, root, options = {}) {
|
|
|
1014
1207
|
const fill = options.fillOf?.(o, material);
|
|
1015
1208
|
const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
|
|
1016
1209
|
const contains = containsOf(ud.destructionContains);
|
|
1210
|
+
const powder = typeof ud.destructionPowder?.color === "number" ? { color: ud.destructionPowder.color } : void 0;
|
|
1017
1211
|
const thickness = typeof ud.destructionThickness === "number" && ud.destructionThickness > 0 ? ud.destructionThickness : void 0;
|
|
1018
1212
|
const existing = explicit ? byKey.get(explicit) : void 0;
|
|
1019
1213
|
if (existing) {
|
|
@@ -1027,9 +1221,10 @@ function specFromObject3D(THREE, root, options = {}) {
|
|
|
1027
1221
|
existing.skin = existing.skin && skin;
|
|
1028
1222
|
if (fill !== void 0) existing.fill = fill;
|
|
1029
1223
|
if (contains) existing.contains = contains;
|
|
1224
|
+
if (powder) existing.powder = powder;
|
|
1030
1225
|
if (thickness !== void 0) existing.thickness = Math.min(existing.thickness ?? thickness, thickness);
|
|
1031
1226
|
} else {
|
|
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 };
|
|
1227
|
+
const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, ...powder ? { powder } : {}, ...thickness !== void 0 ? { thickness } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
|
|
1033
1228
|
entries.push(e);
|
|
1034
1229
|
if (explicit) byKey.set(explicit, e);
|
|
1035
1230
|
}
|
|
@@ -1104,6 +1299,7 @@ function specFromObject3D(THREE, root, options = {}) {
|
|
|
1104
1299
|
}
|
|
1105
1300
|
if (e.tags) part.tags = [...e.tags];
|
|
1106
1301
|
if (e.contains) part.contains = { ...e.contains };
|
|
1302
|
+
if (e.powder) part.powder = { ...e.powder };
|
|
1107
1303
|
const ao = options.anchorBelow ?? -1;
|
|
1108
1304
|
if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
|
|
1109
1305
|
parts.push(part);
|
|
@@ -1284,6 +1480,22 @@ var SKIN_FOLLOW = 0.85;
|
|
|
1284
1480
|
var SKIN_SLACK = 5e-4;
|
|
1285
1481
|
var SKIN_LAYER_SLACK = 4;
|
|
1286
1482
|
var NEAREST_MARGIN = 1.5;
|
|
1483
|
+
var basePositions = (host) => {
|
|
1484
|
+
const p0 = host.mesh.userData._p0;
|
|
1485
|
+
if (p0) return p0;
|
|
1486
|
+
const op = positionAttr(host.origGeometry);
|
|
1487
|
+
return op ? op.array : null;
|
|
1488
|
+
};
|
|
1489
|
+
var collapseForeign = (mesh, arr) => {
|
|
1490
|
+
for (const [v0, v1] of mesh.userData.collapsedRanges ?? []) {
|
|
1491
|
+
for (let k = v0 + 1; k < v1; k++) {
|
|
1492
|
+
arr[k * 3] = arr[v0 * 3];
|
|
1493
|
+
arr[k * 3 + 1] = arr[v0 * 3 + 1];
|
|
1494
|
+
arr[k * 3 + 2] = arr[v0 * 3 + 2];
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1498
|
+
var DECAL_SNAP = 0.03;
|
|
1287
1499
|
var Visuals = class {
|
|
1288
1500
|
constructor(THREE, lowQuality, decalCap) {
|
|
1289
1501
|
__publicField(this, "THREE", THREE);
|
|
@@ -1293,6 +1505,7 @@ var Visuals = class {
|
|
|
1293
1505
|
__publicField(this, "a");
|
|
1294
1506
|
__publicField(this, "b");
|
|
1295
1507
|
__publicField(this, "m4");
|
|
1508
|
+
__publicField(this, "ray");
|
|
1296
1509
|
__publicField(this, "box");
|
|
1297
1510
|
__publicField(this, "t");
|
|
1298
1511
|
__publicField(this, "lbox");
|
|
@@ -1303,6 +1516,7 @@ var Visuals = class {
|
|
|
1303
1516
|
this.a = tmpV(THREE);
|
|
1304
1517
|
this.b = tmpV(THREE);
|
|
1305
1518
|
this.m4 = new THREE.Matrix4();
|
|
1519
|
+
this.ray = new THREE.Raycaster();
|
|
1306
1520
|
this.box = new THREE.Box3();
|
|
1307
1521
|
this.t = tmpV(THREE);
|
|
1308
1522
|
this.lbox = new THREE.Box3();
|
|
@@ -1456,7 +1670,7 @@ var Visuals = class {
|
|
|
1456
1670
|
host.ownGeo = true;
|
|
1457
1671
|
const g = host.mesh.geometry.clone();
|
|
1458
1672
|
host.mesh.geometry = g;
|
|
1459
|
-
host.work = new Float32Array(positionAttr(g).array);
|
|
1673
|
+
host.work = new Float32Array(basePositions(host) ?? positionAttr(g).array);
|
|
1460
1674
|
return true;
|
|
1461
1675
|
}
|
|
1462
1676
|
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
@@ -1626,12 +1840,38 @@ var Visuals = class {
|
|
|
1626
1840
|
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
1627
1841
|
// Decals
|
|
1628
1842
|
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
1843
|
+
/**
|
|
1844
|
+
* The point on the drawn face of `mesh` under a world point, within `DECAL_SNAP` metres along the normal: the world's box of a part and the face
|
|
1845
|
+
* the renderer drew (a coat of plaster, a merged house mesh) can be a few millimetres apart, and a mark behind the drawn face does not show.
|
|
1846
|
+
*/
|
|
1847
|
+
onFace(mesh, worldPoint, worldNormal) {
|
|
1848
|
+
if (!mesh.isMesh || !mesh.geometry?.attributes?.position) return worldPoint;
|
|
1849
|
+
const n = worldNormal.clone().normalize();
|
|
1850
|
+
this.ray.set(worldPoint.clone().addScaledVector(n, DECAL_SNAP), n.clone().negate());
|
|
1851
|
+
this.ray.near = 0;
|
|
1852
|
+
this.ray.far = 2 * DECAL_SNAP;
|
|
1853
|
+
const hits = [];
|
|
1854
|
+
mesh.raycast(this.ray, hits);
|
|
1855
|
+
for (const c of mesh.parent?.children ?? []) {
|
|
1856
|
+
if (c === mesh || c.name !== "breach-shell") continue;
|
|
1857
|
+
c.raycast(this.ray, hits);
|
|
1858
|
+
const behind = c.userData.behind?.();
|
|
1859
|
+
if (behind) behind.raycast(this.ray, hits);
|
|
1860
|
+
}
|
|
1861
|
+
let best = null;
|
|
1862
|
+
for (const h of hits) {
|
|
1863
|
+
if (h.object.userData.cutAt?.(h.point)) continue;
|
|
1864
|
+
if (h.face && h.face.normal.clone().transformDirection(h.object.matrixWorld).dot(n) <= 0.3) continue;
|
|
1865
|
+
if (!best || Math.abs(h.distance - DECAL_SNAP) < Math.abs(best.distance - DECAL_SNAP)) best = h;
|
|
1866
|
+
}
|
|
1867
|
+
return best ? best.point : worldPoint;
|
|
1868
|
+
}
|
|
1629
1869
|
/** Decal pose in mesh space: position and a rotation taking +Z to the surface normal. */
|
|
1630
1870
|
decalPose(mesh, worldPoint, worldNormal, lift) {
|
|
1631
1871
|
mesh.updateWorldMatrix(true, false);
|
|
1632
1872
|
const s = mesh.matrixWorld.getMaxScaleOnAxis() || 1;
|
|
1633
1873
|
const ln = worldNormal.clone().transformDirection(this.m4.copy(mesh.matrixWorld).invert()).normalize();
|
|
1634
|
-
const pos = mesh.worldToLocal(worldPoint.clone()).addScaledVector(ln, lift / s);
|
|
1874
|
+
const pos = mesh.worldToLocal(this.onFace(mesh, worldPoint, worldNormal).clone()).addScaledVector(ln, lift / s);
|
|
1635
1875
|
const quat = new this.THREE.Quaternion().setFromUnitVectors(new this.THREE.Vector3(0, 0, 1), ln);
|
|
1636
1876
|
return { s, pos, quat };
|
|
1637
1877
|
}
|
|
@@ -1870,6 +2110,7 @@ var Visuals = class {
|
|
|
1870
2110
|
if (!pos || !work) return;
|
|
1871
2111
|
const arr = pos.array;
|
|
1872
2112
|
arr.set(work);
|
|
2113
|
+
collapseForeign(host.mesh, arr);
|
|
1873
2114
|
for (const s of host.sharers) {
|
|
1874
2115
|
if (s.merged && s.merged.host === host) {
|
|
1875
2116
|
if (s.collapsed) this.collapseRange(arr, s.merged.range);
|
|
@@ -2074,9 +2315,9 @@ var Visuals = class {
|
|
|
2074
2315
|
mv.touched = false;
|
|
2075
2316
|
if (!host.ownGeo) return;
|
|
2076
2317
|
const work = host.work;
|
|
2077
|
-
const op =
|
|
2318
|
+
const op = basePositions(host);
|
|
2078
2319
|
const [v0, v1] = merged.range;
|
|
2079
|
-
if (op) work.set(op.
|
|
2320
|
+
if (op) work.set(op.subarray(v0 * 3, v1 * 3), v0 * 3);
|
|
2080
2321
|
const attr = host.mesh.geometry.getAttribute("color");
|
|
2081
2322
|
if (attr) {
|
|
2082
2323
|
const orig = host.origGeometry.getAttribute("color");
|
|
@@ -2090,6 +2331,13 @@ var Visuals = class {
|
|
|
2090
2331
|
if (host.sharers.every((s) => !s.touched)) {
|
|
2091
2332
|
host.mesh.geometry.dispose();
|
|
2092
2333
|
host.mesh.geometry = host.origGeometry;
|
|
2334
|
+
const base = basePositions(host), back = positionAttr(host.origGeometry);
|
|
2335
|
+
if (base && back) {
|
|
2336
|
+
const arr = back.array;
|
|
2337
|
+
arr.set(base);
|
|
2338
|
+
collapseForeign(host.mesh, arr);
|
|
2339
|
+
back.needsUpdate = true;
|
|
2340
|
+
}
|
|
2093
2341
|
host.work = null;
|
|
2094
2342
|
host.ownGeo = false;
|
|
2095
2343
|
if (host.ownMat) {
|
|
@@ -2149,9 +2397,12 @@ var Visuals = class {
|
|
|
2149
2397
|
var rendererCount = 0;
|
|
2150
2398
|
var COLLAPSE_CLOUD_KG = 1e3;
|
|
2151
2399
|
var CHIP_DECAL_EDGE = 0.05;
|
|
2400
|
+
var OPAQUE_CRACK = 0.1;
|
|
2401
|
+
var WOOD_RIM = 1.5;
|
|
2152
2402
|
var IMPACT_TAGS = ["metal", "glass", "wood"];
|
|
2153
2403
|
var GLOW_FROM_C = 430;
|
|
2154
2404
|
var QUEUE_CAP = 4e3;
|
|
2405
|
+
var MIN_FRAGMENTS_PER_SLICE = 8;
|
|
2155
2406
|
var STATIC_REFRESH = 0.5;
|
|
2156
2407
|
var STATIC_SPAN = 3;
|
|
2157
2408
|
var FEET_BAND = 0.03;
|
|
@@ -2227,6 +2478,7 @@ function isMeshRange(v) {
|
|
|
2227
2478
|
const r = v;
|
|
2228
2479
|
return !!r.mesh && Array.isArray(r.range);
|
|
2229
2480
|
}
|
|
2481
|
+
var RNG_SEED = 12648430;
|
|
2230
2482
|
function createDestructionRenderer(THREE, options) {
|
|
2231
2483
|
return new Renderer(THREE, options);
|
|
2232
2484
|
}
|
|
@@ -2242,6 +2494,7 @@ var Renderer = class {
|
|
|
2242
2494
|
__publicField(this, "gravity");
|
|
2243
2495
|
__publicField(this, "autoSplit");
|
|
2244
2496
|
__publicField(this, "maxFragments");
|
|
2497
|
+
__publicField(this, "eventBudgetMs");
|
|
2245
2498
|
__publicField(this, "bounds", /* @__PURE__ */ new Map());
|
|
2246
2499
|
/** World boxes (min xyz, max xyz) of the drawn meshes of the bound objects that are not pieces, for the pieces to land on. */
|
|
2247
2500
|
__publicField(this, "staticBoxes", /* @__PURE__ */ new Map());
|
|
@@ -2252,6 +2505,10 @@ var Renderer = class {
|
|
|
2252
2505
|
__publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
|
|
2253
2506
|
__publicField(this, "staticAt", 0);
|
|
2254
2507
|
__publicField(this, "queue", []);
|
|
2508
|
+
/** Presentation work an event left for later frames (fragments still to spawn, loose groups still to split), run in order within the frame budget. */
|
|
2509
|
+
__publicField(this, "slices", []);
|
|
2510
|
+
/** When the work of this frame must stop; `Infinity` outside `drainQueue`, where a call finishes what it started. */
|
|
2511
|
+
__publicField(this, "sliceDeadline", Infinity);
|
|
2255
2512
|
__publicField(this, "unsubscribe");
|
|
2256
2513
|
__publicField(this, "visuals");
|
|
2257
2514
|
__publicField(this, "fragments");
|
|
@@ -2265,7 +2522,7 @@ var Renderer = class {
|
|
|
2265
2522
|
__publicField(this, "leaks", []);
|
|
2266
2523
|
__publicField(this, "homeTags", []);
|
|
2267
2524
|
__publicField(this, "throttles", /* @__PURE__ */ new Map());
|
|
2268
|
-
__publicField(this, "rng", createRng(
|
|
2525
|
+
__publicField(this, "rng", createRng(RNG_SEED));
|
|
2269
2526
|
__publicField(this, "vfx");
|
|
2270
2527
|
/** Names this renderer to the world's drips, so a shared world tells each renderer where its own landed. */
|
|
2271
2528
|
__publicField(this, "owner", `destruction-${++rendererCount}`);
|
|
@@ -2289,6 +2546,7 @@ var Renderer = class {
|
|
|
2289
2546
|
this.gravity = o.gravity ?? 9.81;
|
|
2290
2547
|
this.autoSplit = o.autoSplit !== false;
|
|
2291
2548
|
this.maxFragments = o.maxFragments ?? 160;
|
|
2549
|
+
this.eventBudgetMs = o.eventBudgetMs ?? Infinity;
|
|
2292
2550
|
this.box = new THREE.Box3();
|
|
2293
2551
|
this.tmpQ = new THREE.Quaternion();
|
|
2294
2552
|
this.v1 = new THREE.Vector3();
|
|
@@ -2374,6 +2632,7 @@ var Renderer = class {
|
|
|
2374
2632
|
if (leftovers.length) this.matchByBox(objectId, root, leftovers, add);
|
|
2375
2633
|
}
|
|
2376
2634
|
const bound = { id: objectId, root, sync: options.syncTransform !== false, parts: /* @__PURE__ */ new Map(), lastPos: null, lastQuat: null, piece: null, isSplit: false };
|
|
2635
|
+
if (options.onFrame) bound.onFrame = options.onFrame;
|
|
2377
2636
|
const hosts = /* @__PURE__ */ new Map();
|
|
2378
2637
|
for (const pid of partIds) bound.parts.set(pid, this.makePartVis(objectId, pid, state?.parts.find((p) => p.id === pid)?.material ?? "", byPart.get(pid) ?? [], hosts));
|
|
2379
2638
|
for (const [pid, meshes] of byPart) {
|
|
@@ -2533,6 +2792,7 @@ var Renderer = class {
|
|
|
2533
2792
|
dt = clamp(dt, 0, 0.1);
|
|
2534
2793
|
this.time += dt;
|
|
2535
2794
|
this.drainQueue();
|
|
2795
|
+
for (const b of this.bounds.values()) b.onFrame?.(dt);
|
|
2536
2796
|
for (const b of this.bounds.values()) if (b.soilNode && this.world.state(b.id)?.soil?.status === "yielding") this.onSettle(b.id);
|
|
2537
2797
|
this.updateActive(dt);
|
|
2538
2798
|
this.updateLeaks(dt);
|
|
@@ -2544,11 +2804,32 @@ var Renderer = class {
|
|
|
2544
2804
|
}
|
|
2545
2805
|
drainQueue() {
|
|
2546
2806
|
const batch = this.queue.splice(0, this.queue.length);
|
|
2547
|
-
|
|
2807
|
+
const deadline = performance.now() + this.eventBudgetMs;
|
|
2808
|
+
this.sliceDeadline = deadline;
|
|
2809
|
+
try {
|
|
2810
|
+
this.runSlices(deadline);
|
|
2811
|
+
for (let i = 0; i < batch.length; i++) {
|
|
2812
|
+
if (i > 0 && performance.now() >= deadline) {
|
|
2813
|
+
this.queue.unshift(...batch.slice(i));
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
try {
|
|
2817
|
+
this.apply(batch[i]);
|
|
2818
|
+
} catch (err) {
|
|
2819
|
+
if (typeof console !== "undefined") console.error("game-destruction-three: failed to apply", batch[i].type, err);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
} finally {
|
|
2823
|
+
this.sliceDeadline = Infinity;
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
/** Runs the work left by earlier frames until the deadline; at least one piece per frame, so it always finishes. */
|
|
2827
|
+
runSlices(deadline) {
|
|
2828
|
+
for (let ran = 0; this.slices.length > 0 && (ran === 0 || performance.now() < deadline); ran++) {
|
|
2548
2829
|
try {
|
|
2549
|
-
this.
|
|
2830
|
+
this.slices.shift()();
|
|
2550
2831
|
} catch (err) {
|
|
2551
|
-
if (typeof console !== "undefined") console.error("game-destruction-three: failed to
|
|
2832
|
+
if (typeof console !== "undefined") console.error("game-destruction-three: failed to run deferred work", err);
|
|
2552
2833
|
}
|
|
2553
2834
|
}
|
|
2554
2835
|
}
|
|
@@ -2946,12 +3227,19 @@ var Renderer = class {
|
|
|
2946
3227
|
const seed = hashNumbers(ev.time, point[0], point[1], point[2]);
|
|
2947
3228
|
this.visuals.addCrack(mv, wp, normal, 7, Math.max(0.05, ev.hole.diameter * 5), 15267071, seed);
|
|
2948
3229
|
} else {
|
|
3230
|
+
const fresh = def?.penetrationClass === "wood" ? def.visual?.innerColor : void 0;
|
|
3231
|
+
if (fresh !== void 0) this.visuals.addSpall(mv, wp, normal, Math.max(6e-3, ev.hole.diameter * WOOD_RIM), fresh);
|
|
2949
3232
|
this.visuals.addHole(mv, wp, normal, ev.hole.diameter);
|
|
2950
3233
|
}
|
|
2951
3234
|
};
|
|
2952
3235
|
draw(ev.worldPoint, n);
|
|
2953
3236
|
if (ev.hole.through && ev.exitPoint) draw(ev.exitPoint, n.clone().negate());
|
|
2954
3237
|
if (def?.tags?.includes("metal")) this.vfx.burst("sparks", ev.worldPoint, [n.x, n.y, n.z], 6, 3.5);
|
|
3238
|
+
if (ev.powder) {
|
|
3239
|
+
const puff = { color: ev.powder.color, size: 0.6, life: 1.6, alpha: 1.3 };
|
|
3240
|
+
this.vfx.burst("dust", ev.worldPoint, [n.x, n.y, n.z], this.low ? 5 : 10, 0.7, puff);
|
|
3241
|
+
if (ev.hole.through && ev.exitPoint) this.vfx.burst("dust", ev.exitPoint, [-n.x, -n.y, -n.z], this.low ? 10 : 20, 1.3, puff);
|
|
3242
|
+
}
|
|
2955
3243
|
this.sound(impactKey(def), ev.worldPoint, 0.6);
|
|
2956
3244
|
}
|
|
2957
3245
|
onCrack(ev) {
|
|
@@ -2962,8 +3250,11 @@ var Renderer = class {
|
|
|
2962
3250
|
const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
|
|
2963
3251
|
const mv = this.nearestMesh(pv, wp);
|
|
2964
3252
|
if (mv) {
|
|
2965
|
-
const
|
|
2966
|
-
|
|
3253
|
+
const glass = !!def?.visual?.transparent;
|
|
3254
|
+
const own = this.partColor(pv, def) ?? fallbackColor(pv.material, def);
|
|
3255
|
+
const color = glass ? 15267071 : new this.THREE.Color(own).multiplyScalar(0.45).getHex();
|
|
3256
|
+
const length = glass ? ev.crack.length : Math.min(ev.crack.length, OPAQUE_CRACK);
|
|
3257
|
+
this.visuals.addCrack(mv, wp, this.worldDir(b, ev.crack.normal), ev.crack.rays, length, color, hashNumbers(ev.time, ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]));
|
|
2967
3258
|
}
|
|
2968
3259
|
this.sound(impactKey(def), ev.worldPoint, 0.5);
|
|
2969
3260
|
}
|
|
@@ -3001,6 +3292,7 @@ var Renderer = class {
|
|
|
3001
3292
|
const core = def?.visual?.innerColor;
|
|
3002
3293
|
if (core !== void 0) this.vfx.burst("chips", ev.point, [normal.x, normal.y, normal.z], 5, 2.5, { color: core });
|
|
3003
3294
|
else this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.point);
|
|
3295
|
+
if (mv.mesh.userData.drawnSpalls?.includes(ev.part)) return;
|
|
3004
3296
|
this.visuals.addSpall(mv, wp, normal, Math.sqrt(ev.area / Math.PI), core ?? fallbackColor(pv.material, def));
|
|
3005
3297
|
}
|
|
3006
3298
|
/**
|
|
@@ -3038,6 +3330,10 @@ var Renderer = class {
|
|
|
3038
3330
|
const tone2 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
|
|
3039
3331
|
this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone2, false), ev.object, maxAlive);
|
|
3040
3332
|
}
|
|
3333
|
+
if (ev.torn) {
|
|
3334
|
+
this.sound(impactKey(def), ev.center, 0.5);
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3041
3337
|
const T = this.THREE;
|
|
3042
3338
|
const center = new T.Vector3(ev.center[0], ev.center[1], ev.center[2]);
|
|
3043
3339
|
const half = new T.Vector3(ev.size[0] / 2 + CHIP_DECAL_EDGE, ev.size[1] / 2 + CHIP_DECAL_EDGE, ev.size[2] / 2 + CHIP_DECAL_EDGE);
|
|
@@ -3062,15 +3358,7 @@ var Renderer = class {
|
|
|
3062
3358
|
if (m && m.transparent) opacity = clamp(m.opacity, 0.2, 0.8);
|
|
3063
3359
|
this.visuals.hide(pv, "broken");
|
|
3064
3360
|
}
|
|
3065
|
-
|
|
3066
|
-
const step = this.low ? 2 : 1;
|
|
3067
|
-
for (let i = 0; i < ev.fragments.length; i += step) {
|
|
3068
|
-
const fr = ev.fragments[i];
|
|
3069
|
-
const fdef = this.world.materials.get(fr.material);
|
|
3070
|
-
const tone2 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def || !fdef ? color : fallbackColor(fr.material, fdef);
|
|
3071
|
-
const mat = this.fragments.materialFor(fr.material, tone2, transparent || fdef?.visual?.transparent === true, opacity);
|
|
3072
|
-
this.fragments.spawn(fr, mat, ev.object, maxAlive);
|
|
3073
|
-
}
|
|
3361
|
+
this.spawnFragments(ev, 0, { def, color, opacity, transparent });
|
|
3074
3362
|
const up = [0, 1, 0];
|
|
3075
3363
|
const sizeK = clamp(ev.fragments.length / 10, 0.5, 2);
|
|
3076
3364
|
switch (ev.style) {
|
|
@@ -3096,6 +3384,22 @@ var Renderer = class {
|
|
|
3096
3384
|
}
|
|
3097
3385
|
this.sound(fractureKey(def, ev.style), ev.point, clamp(0.4 + ev.fragments.length * 0.03, 0.4, 1));
|
|
3098
3386
|
}
|
|
3387
|
+
/** Spawns the fragments of a fracture from index `from`; what does not fit in this frame's budget is spawned by the next frames. */
|
|
3388
|
+
spawnFragments(ev, from, look) {
|
|
3389
|
+
const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
|
|
3390
|
+
const step = this.low ? 2 : 1;
|
|
3391
|
+
for (let i = from; i < ev.fragments.length; i += step) {
|
|
3392
|
+
if (i - from >= MIN_FRAGMENTS_PER_SLICE * step && performance.now() >= this.sliceDeadline) {
|
|
3393
|
+
this.slices.push(() => this.spawnFragments(ev, i, look));
|
|
3394
|
+
return;
|
|
3395
|
+
}
|
|
3396
|
+
const fr = ev.fragments[i];
|
|
3397
|
+
const fdef = this.world.materials.get(fr.material);
|
|
3398
|
+
const tone2 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === look.def || !fdef ? look.color : fallbackColor(fr.material, fdef);
|
|
3399
|
+
const mat = this.fragments.materialFor(fr.material, tone2, look.transparent || fdef?.visual?.transparent === true, look.opacity);
|
|
3400
|
+
this.fragments.spawn(fr, mat, ev.object, maxAlive);
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3099
3403
|
onDetach(ev) {
|
|
3100
3404
|
this.sound("detach", this.rootPoint(ev.object), 0.5);
|
|
3101
3405
|
this.splitLoose(ev.object);
|
|
@@ -3109,6 +3413,13 @@ var Renderer = class {
|
|
|
3109
3413
|
if (!st || st.groups.length < 2) return;
|
|
3110
3414
|
const loose = st.groups.length - 1;
|
|
3111
3415
|
for (let i = 0; i < loose; i++) {
|
|
3416
|
+
if (i > 0 && performance.now() >= this.sliceDeadline) {
|
|
3417
|
+
this.slices.push(() => {
|
|
3418
|
+
this.staticDirty.add(objectId);
|
|
3419
|
+
this.splitLoose(objectId);
|
|
3420
|
+
});
|
|
3421
|
+
return;
|
|
3422
|
+
}
|
|
3112
3423
|
let newId;
|
|
3113
3424
|
try {
|
|
3114
3425
|
newId = this.world.splitGroup(objectId, 1);
|
|
@@ -3125,8 +3436,9 @@ var Renderer = class {
|
|
|
3125
3436
|
old.mass = Math.max(0.05, this.massOf(objectId));
|
|
3126
3437
|
old.sleeping = false;
|
|
3127
3438
|
} else if (kitOriginOf(b.root)?.kind === "furniture") {
|
|
3128
|
-
this.makePiece(objectId);
|
|
3439
|
+
const piece = this.makePiece(objectId);
|
|
3129
3440
|
this.tipIfTopHeavy(b);
|
|
3441
|
+
this.o.onLoosened?.(objectId, piece, "apart");
|
|
3130
3442
|
}
|
|
3131
3443
|
}
|
|
3132
3444
|
/** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
|
|
@@ -3235,7 +3547,7 @@ var Renderer = class {
|
|
|
3235
3547
|
const piece = this.makePiece(b.id, { mass });
|
|
3236
3548
|
const body = this.pieceSys.get(b.id);
|
|
3237
3549
|
if (!body) continue;
|
|
3238
|
-
this.o.onLoosened?.(b.id, piece);
|
|
3550
|
+
this.o.onLoosened?.(b.id, piece, "blast");
|
|
3239
3551
|
const spread = half[0];
|
|
3240
3552
|
const momentum = hit.speed * mass;
|
|
3241
3553
|
this.pieceSys.impulse(body, [hit.direction[0] * momentum, hit.direction[1] * momentum, hit.direction[2] * momentum], [world.x + (this.rng() - 0.5) * spread, world.y + spread * 0.5, world.z + (this.rng() - 0.5) * spread]);
|
|
@@ -3430,6 +3742,7 @@ var Renderer = class {
|
|
|
3430
3742
|
}
|
|
3431
3743
|
clear() {
|
|
3432
3744
|
this.queue.length = 0;
|
|
3745
|
+
this.slices.length = 0;
|
|
3433
3746
|
this.active.clear();
|
|
3434
3747
|
for (const key of [...this.fires.keys()]) this.stopFire(key);
|
|
3435
3748
|
this.leaks.length = 0;
|
|
@@ -3449,6 +3762,15 @@ var Renderer = class {
|
|
|
3449
3762
|
this.vfx.sprites().clear();
|
|
3450
3763
|
this.ownVfx?.world.clear();
|
|
3451
3764
|
}
|
|
3765
|
+
reset() {
|
|
3766
|
+
this.clear();
|
|
3767
|
+
this.rng = createRng(RNG_SEED);
|
|
3768
|
+
this.time = 0;
|
|
3769
|
+
this.staticAt = 0;
|
|
3770
|
+
this.throttles.clear();
|
|
3771
|
+
this.fragments.reset();
|
|
3772
|
+
this.pieceSys.reset();
|
|
3773
|
+
}
|
|
3452
3774
|
wakeRegion(x, z, radius) {
|
|
3453
3775
|
this.fragments.wakeRegion(x, z, radius);
|
|
3454
3776
|
this.pieceSys.wakeRegion(x, z, radius);
|
|
@@ -3574,7 +3896,18 @@ var DRAG_SOUNDS = [
|
|
|
3574
3896
|
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] }))]),
|
|
3575
3897
|
drag("drag_soft", [noise({ type: "lowpass", f: 420, d: 0.28, vol: 0.3 })])
|
|
3576
3898
|
];
|
|
3577
|
-
var
|
|
3899
|
+
var leaf = (id, layers) => ({ ...sceneRecipe(id, layers, ""), sources: ["sdk: a door, lid or drawer worked by hand"] });
|
|
3900
|
+
var LEAF_SOUNDS = [
|
|
3901
|
+
leaf("latch", [noise({ type: "bandpass", f: 2400, q: 4, d: 0.03, vol: 0.35 }), tone({ f: 1900, f2: 1500, d: 0.025, vol: 0.06, delay: 0.035 })]),
|
|
3902
|
+
leaf("creak_wood", [tone({ f: [190, 240], f2: 310, d: 0.5, vol: 0.05 }), noise({ type: "bandpass", f: 520, q: 6, a: 0.05, d: 0.45, vol: 0.12 })]),
|
|
3903
|
+
leaf("creak_metal", [tone({ f: [620, 760], f2: 900, d: 0.35, vol: 0.035 }), noise({ type: "bandpass", f: 1400, q: 8, a: 0.04, d: 0.3, vol: 0.08 })]),
|
|
3904
|
+
leaf("shut_wood", [tone({ f: 120, f2: 60, d: 0.14, vol: 0.5 }), noise({ type: "bandpass", f: 600, q: 1.2, d: 0.1, vol: 0.55, wet: 0.25 }), noise({ type: "bandpass", f: 2400, q: 4, d: 0.03, vol: 0.25, delay: 0.02 })]),
|
|
3905
|
+
leaf("shut_metal", [tone({ f: 160, f2: 85, d: 0.18, vol: 0.55 }), noise({ type: "bandpass", f: 900, q: 1.6, d: 0.12, vol: 0.45, wet: 0.3 }), tone({ f: 1250, f2: 1150, d: 0.12, vol: 0.06 })]),
|
|
3906
|
+
leaf("shut_soft", [tone({ f: 95, f2: 55, d: 0.12, vol: 0.35 }), noise({ type: "lowpass", f: 500, d: 0.1, vol: 0.3 })]),
|
|
3907
|
+
leaf("rattle", many(5, () => noise({ type: "bandpass", f: [1600, 2600], q: 3, d: 0.035, vol: 0.32, delay: [0, 0.45] }))),
|
|
3908
|
+
leaf("slide", [noise({ type: "bandpass", f: 700, q: 2, a: 0.04, d: 0.35, vol: 0.22 }), noise({ type: "bandpass", f: 2200, q: 5, d: 0.3, vol: 0.05 })])
|
|
3909
|
+
];
|
|
3910
|
+
var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, ...LEAF_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
|
|
3578
3911
|
|
|
3579
3912
|
// src/audio/sound.ts
|
|
3580
3913
|
import { GAME_SFX_BANK } from "@adatechnology/game-audio";
|
|
@@ -3590,6 +3923,32 @@ var DESTRUCTION_SOUND_KEYS = {
|
|
|
3590
3923
|
drag_glass: { sound: "drag_glass" },
|
|
3591
3924
|
drag_masonry: { sound: "drag_masonry" },
|
|
3592
3925
|
drag_soft: { sound: "drag_soft" },
|
|
3926
|
+
// a leaf worked by hand (doors, lids, drawers): the latch, the hinge turning, the leaf meeting the frame, a locked one shaken
|
|
3927
|
+
leaf_latch: { sound: "latch" },
|
|
3928
|
+
leaf_creak_wood: { sound: "creak_wood" },
|
|
3929
|
+
leaf_creak_metal: { sound: "creak_metal" },
|
|
3930
|
+
leaf_shut_wood: { sound: "shut_wood" },
|
|
3931
|
+
leaf_shut_metal: { sound: "shut_metal" },
|
|
3932
|
+
leaf_shut_glass: { sound: "shut_metal", volumeScale: 0.6 },
|
|
3933
|
+
leaf_shut_masonry: { sound: "shut_wood" },
|
|
3934
|
+
leaf_shut_soft: { sound: "shut_soft" },
|
|
3935
|
+
leaf_rattle: { sound: "rattle" },
|
|
3936
|
+
leaf_slide: { sound: "slide" },
|
|
3937
|
+
// the keys of the item clips (furniture and vehicle doors, hoods, trunks, drawers)
|
|
3938
|
+
door_latch: { sound: "latch" },
|
|
3939
|
+
hood_latch: { sound: "latch" },
|
|
3940
|
+
trunk_latch: { sound: "latch" },
|
|
3941
|
+
door_open: { sound: "latch" },
|
|
3942
|
+
door_shut: { sound: "shut_metal" },
|
|
3943
|
+
hood_shut: { sound: "shut_metal" },
|
|
3944
|
+
trunk_shut: { sound: "shut_metal" },
|
|
3945
|
+
door_close: { sound: "shut_wood", volumeScale: 0.7 },
|
|
3946
|
+
drawer_open: { sound: "slide" },
|
|
3947
|
+
drawer_close: { sound: "slide" },
|
|
3948
|
+
lid_open: { sound: "latch" },
|
|
3949
|
+
lid_close: { sound: "shut_soft" },
|
|
3950
|
+
flap_open: { sound: "latch" },
|
|
3951
|
+
flap_close: { sound: "shut_soft" },
|
|
3593
3952
|
shatter_glass: { sound: "glass_shatter" },
|
|
3594
3953
|
crumble: { sound: "crumble" },
|
|
3595
3954
|
splinter: { sound: "crack" },
|