@adatechnology/game-destruction-three 0.11.0 → 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 +687 -182
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +480 -94
- 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
|
-
v[2] -= tz * jt * invM;
|
|
769
|
-
const dwt = this.mulInvInertiaWith(b, e, rx * jt, ry * jt, rz * jt);
|
|
770
|
-
w[0] -= dwt[0];
|
|
771
|
-
w[1] -= dwt[1];
|
|
772
|
-
w[2] -= dwt[2];
|
|
926
|
+
const jt = vt / (invM + cx * tx + cz * tz);
|
|
927
|
+
fx -= tx * jt;
|
|
928
|
+
fz -= tz * jt;
|
|
773
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;
|
|
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
|
}
|
|
@@ -1651,6 +1891,21 @@ var Visuals = class {
|
|
|
1651
1891
|
d.geometry?.dispose();
|
|
1652
1892
|
this.decalCount = Math.max(0, this.decalCount - 1);
|
|
1653
1893
|
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Takes away the decals of a mesh that sit inside a box (world `center`, `half` extents along the axes of the world `rotation`): the surface they
|
|
1896
|
+
* marked is gone (a unit came out of the wall with its bullet holes and spalls).
|
|
1897
|
+
*/
|
|
1898
|
+
clearDecals(mv, center, half, rotation) {
|
|
1899
|
+
const inv = rotation.clone().invert();
|
|
1900
|
+
const p = new this.THREE.Vector3();
|
|
1901
|
+
for (let i = mv.decals.length - 1; i >= 0; i--) {
|
|
1902
|
+
const d = mv.decals[i];
|
|
1903
|
+
d.obj.getWorldPosition(p).sub(center).applyQuaternion(inv);
|
|
1904
|
+
if (Math.abs(p.x) > half.x || Math.abs(p.y) > half.y || Math.abs(p.z) > half.z) continue;
|
|
1905
|
+
this.disposeDecal(d);
|
|
1906
|
+
mv.decals.splice(i, 1);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1654
1909
|
addHole(mv, worldPoint, worldNormal, diameter) {
|
|
1655
1910
|
const { s, pos, quat } = this.decalPose(mv.mesh, worldPoint, worldNormal, 25e-4);
|
|
1656
1911
|
const work = mv.host ? mv.host.work : mv.work;
|
|
@@ -1855,6 +2110,7 @@ var Visuals = class {
|
|
|
1855
2110
|
if (!pos || !work) return;
|
|
1856
2111
|
const arr = pos.array;
|
|
1857
2112
|
arr.set(work);
|
|
2113
|
+
collapseForeign(host.mesh, arr);
|
|
1858
2114
|
for (const s of host.sharers) {
|
|
1859
2115
|
if (s.merged && s.merged.host === host) {
|
|
1860
2116
|
if (s.collapsed) this.collapseRange(arr, s.merged.range);
|
|
@@ -2059,9 +2315,9 @@ var Visuals = class {
|
|
|
2059
2315
|
mv.touched = false;
|
|
2060
2316
|
if (!host.ownGeo) return;
|
|
2061
2317
|
const work = host.work;
|
|
2062
|
-
const op =
|
|
2318
|
+
const op = basePositions(host);
|
|
2063
2319
|
const [v0, v1] = merged.range;
|
|
2064
|
-
if (op) work.set(op.
|
|
2320
|
+
if (op) work.set(op.subarray(v0 * 3, v1 * 3), v0 * 3);
|
|
2065
2321
|
const attr = host.mesh.geometry.getAttribute("color");
|
|
2066
2322
|
if (attr) {
|
|
2067
2323
|
const orig = host.origGeometry.getAttribute("color");
|
|
@@ -2075,6 +2331,13 @@ var Visuals = class {
|
|
|
2075
2331
|
if (host.sharers.every((s) => !s.touched)) {
|
|
2076
2332
|
host.mesh.geometry.dispose();
|
|
2077
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
|
+
}
|
|
2078
2341
|
host.work = null;
|
|
2079
2342
|
host.ownGeo = false;
|
|
2080
2343
|
if (host.ownMat) {
|
|
@@ -2133,9 +2396,13 @@ var Visuals = class {
|
|
|
2133
2396
|
// src/renderer.ts
|
|
2134
2397
|
var rendererCount = 0;
|
|
2135
2398
|
var COLLAPSE_CLOUD_KG = 1e3;
|
|
2399
|
+
var CHIP_DECAL_EDGE = 0.05;
|
|
2400
|
+
var OPAQUE_CRACK = 0.1;
|
|
2401
|
+
var WOOD_RIM = 1.5;
|
|
2136
2402
|
var IMPACT_TAGS = ["metal", "glass", "wood"];
|
|
2137
2403
|
var GLOW_FROM_C = 430;
|
|
2138
2404
|
var QUEUE_CAP = 4e3;
|
|
2405
|
+
var MIN_FRAGMENTS_PER_SLICE = 8;
|
|
2139
2406
|
var STATIC_REFRESH = 0.5;
|
|
2140
2407
|
var STATIC_SPAN = 3;
|
|
2141
2408
|
var FEET_BAND = 0.03;
|
|
@@ -2211,6 +2478,7 @@ function isMeshRange(v) {
|
|
|
2211
2478
|
const r = v;
|
|
2212
2479
|
return !!r.mesh && Array.isArray(r.range);
|
|
2213
2480
|
}
|
|
2481
|
+
var RNG_SEED = 12648430;
|
|
2214
2482
|
function createDestructionRenderer(THREE, options) {
|
|
2215
2483
|
return new Renderer(THREE, options);
|
|
2216
2484
|
}
|
|
@@ -2226,6 +2494,7 @@ var Renderer = class {
|
|
|
2226
2494
|
__publicField(this, "gravity");
|
|
2227
2495
|
__publicField(this, "autoSplit");
|
|
2228
2496
|
__publicField(this, "maxFragments");
|
|
2497
|
+
__publicField(this, "eventBudgetMs");
|
|
2229
2498
|
__publicField(this, "bounds", /* @__PURE__ */ new Map());
|
|
2230
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. */
|
|
2231
2500
|
__publicField(this, "staticBoxes", /* @__PURE__ */ new Map());
|
|
@@ -2236,6 +2505,10 @@ var Renderer = class {
|
|
|
2236
2505
|
__publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
|
|
2237
2506
|
__publicField(this, "staticAt", 0);
|
|
2238
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);
|
|
2239
2512
|
__publicField(this, "unsubscribe");
|
|
2240
2513
|
__publicField(this, "visuals");
|
|
2241
2514
|
__publicField(this, "fragments");
|
|
@@ -2249,7 +2522,7 @@ var Renderer = class {
|
|
|
2249
2522
|
__publicField(this, "leaks", []);
|
|
2250
2523
|
__publicField(this, "homeTags", []);
|
|
2251
2524
|
__publicField(this, "throttles", /* @__PURE__ */ new Map());
|
|
2252
|
-
__publicField(this, "rng", createRng(
|
|
2525
|
+
__publicField(this, "rng", createRng(RNG_SEED));
|
|
2253
2526
|
__publicField(this, "vfx");
|
|
2254
2527
|
/** Names this renderer to the world's drips, so a shared world tells each renderer where its own landed. */
|
|
2255
2528
|
__publicField(this, "owner", `destruction-${++rendererCount}`);
|
|
@@ -2273,6 +2546,7 @@ var Renderer = class {
|
|
|
2273
2546
|
this.gravity = o.gravity ?? 9.81;
|
|
2274
2547
|
this.autoSplit = o.autoSplit !== false;
|
|
2275
2548
|
this.maxFragments = o.maxFragments ?? 160;
|
|
2549
|
+
this.eventBudgetMs = o.eventBudgetMs ?? Infinity;
|
|
2276
2550
|
this.box = new THREE.Box3();
|
|
2277
2551
|
this.tmpQ = new THREE.Quaternion();
|
|
2278
2552
|
this.v1 = new THREE.Vector3();
|
|
@@ -2358,6 +2632,7 @@ var Renderer = class {
|
|
|
2358
2632
|
if (leftovers.length) this.matchByBox(objectId, root, leftovers, add);
|
|
2359
2633
|
}
|
|
2360
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;
|
|
2361
2636
|
const hosts = /* @__PURE__ */ new Map();
|
|
2362
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));
|
|
2363
2638
|
for (const [pid, meshes] of byPart) {
|
|
@@ -2517,6 +2792,7 @@ var Renderer = class {
|
|
|
2517
2792
|
dt = clamp(dt, 0, 0.1);
|
|
2518
2793
|
this.time += dt;
|
|
2519
2794
|
this.drainQueue();
|
|
2795
|
+
for (const b of this.bounds.values()) b.onFrame?.(dt);
|
|
2520
2796
|
for (const b of this.bounds.values()) if (b.soilNode && this.world.state(b.id)?.soil?.status === "yielding") this.onSettle(b.id);
|
|
2521
2797
|
this.updateActive(dt);
|
|
2522
2798
|
this.updateLeaks(dt);
|
|
@@ -2528,11 +2804,32 @@ var Renderer = class {
|
|
|
2528
2804
|
}
|
|
2529
2805
|
drainQueue() {
|
|
2530
2806
|
const batch = this.queue.splice(0, this.queue.length);
|
|
2531
|
-
|
|
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++) {
|
|
2532
2829
|
try {
|
|
2533
|
-
this.
|
|
2830
|
+
this.slices.shift()();
|
|
2534
2831
|
} catch (err) {
|
|
2535
|
-
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);
|
|
2536
2833
|
}
|
|
2537
2834
|
}
|
|
2538
2835
|
}
|
|
@@ -2696,7 +2993,7 @@ var Renderer = class {
|
|
|
2696
2993
|
break;
|
|
2697
2994
|
}
|
|
2698
2995
|
case "debris":
|
|
2699
|
-
this.vfx.burst(ev.kind, ev.point, ev.normal, ev.count, ev.speed > 0 ? ev.speed : DEFAULT_SPEED[ev.kind] ?? 1, this.debrisOptions(ev.kind, ev.object, ev.part));
|
|
2996
|
+
this.vfx.burst(ev.kind, ev.point, ev.normal, ev.count, ev.speed > 0 ? ev.speed : DEFAULT_SPEED[ev.kind] ?? 1, this.debrisOptions(ev.kind, ev.object, ev.part, ev.material));
|
|
2700
2997
|
break;
|
|
2701
2998
|
case "fracture":
|
|
2702
2999
|
this.onFracture(ev);
|
|
@@ -2869,10 +3166,11 @@ var Renderer = class {
|
|
|
2869
3166
|
}
|
|
2870
3167
|
return def ? fallbackColor(def.id, def) : null;
|
|
2871
3168
|
}
|
|
2872
|
-
debrisOptions(kind, objectId, partId) {
|
|
3169
|
+
debrisOptions(kind, objectId, partId, material) {
|
|
2873
3170
|
const pv = this.partVis(objectId, partId);
|
|
2874
|
-
const def =
|
|
3171
|
+
const def = this.world.materials.get(material ?? pv?.material ?? "");
|
|
2875
3172
|
if (kind === "smoke") return { color: SMOKE_TONES[def?.thermal.smoke ?? "grey"] };
|
|
3173
|
+
if ((kind === "dust" || kind === "chips") && def?.visual?.innerColor !== void 0) return { color: def.visual.innerColor };
|
|
2876
3174
|
if (kind === "fibers" || kind === "leaves") {
|
|
2877
3175
|
if (def?.visual?.innerColor !== void 0) return { color: def.visual.innerColor };
|
|
2878
3176
|
const c = pv ? this.visuals.baseColorHex(pv.meshes[0] ?? {}) : -1;
|
|
@@ -2929,12 +3227,19 @@ var Renderer = class {
|
|
|
2929
3227
|
const seed = hashNumbers(ev.time, point[0], point[1], point[2]);
|
|
2930
3228
|
this.visuals.addCrack(mv, wp, normal, 7, Math.max(0.05, ev.hole.diameter * 5), 15267071, seed);
|
|
2931
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);
|
|
2932
3232
|
this.visuals.addHole(mv, wp, normal, ev.hole.diameter);
|
|
2933
3233
|
}
|
|
2934
3234
|
};
|
|
2935
3235
|
draw(ev.worldPoint, n);
|
|
2936
3236
|
if (ev.hole.through && ev.exitPoint) draw(ev.exitPoint, n.clone().negate());
|
|
2937
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
|
+
}
|
|
2938
3243
|
this.sound(impactKey(def), ev.worldPoint, 0.6);
|
|
2939
3244
|
}
|
|
2940
3245
|
onCrack(ev) {
|
|
@@ -2945,8 +3250,11 @@ var Renderer = class {
|
|
|
2945
3250
|
const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
|
|
2946
3251
|
const mv = this.nearestMesh(pv, wp);
|
|
2947
3252
|
if (mv) {
|
|
2948
|
-
const
|
|
2949
|
-
|
|
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]));
|
|
2950
3258
|
}
|
|
2951
3259
|
this.sound(impactKey(def), ev.worldPoint, 0.5);
|
|
2952
3260
|
}
|
|
@@ -2980,8 +3288,12 @@ var Renderer = class {
|
|
|
2980
3288
|
const mv = this.nearestMesh(pv, wp);
|
|
2981
3289
|
if (!mv) return;
|
|
2982
3290
|
const def = this.world.materials.get(pv.material);
|
|
2983
|
-
|
|
2984
|
-
|
|
3291
|
+
const normal = this.faceNormal(mv, wp);
|
|
3292
|
+
const core = def?.visual?.innerColor;
|
|
3293
|
+
if (core !== void 0) this.vfx.burst("chips", ev.point, [normal.x, normal.y, normal.z], 5, 2.5, { color: core });
|
|
3294
|
+
else this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.point);
|
|
3295
|
+
if (mv.mesh.userData.drawnSpalls?.includes(ev.part)) return;
|
|
3296
|
+
this.visuals.addSpall(mv, wp, normal, Math.sqrt(ev.area / Math.PI), core ?? fallbackColor(pv.material, def));
|
|
2985
3297
|
}
|
|
2986
3298
|
/**
|
|
2987
3299
|
* Stage 2 of a staged part: the unit-sized fragments come out and the part stays drawn (hiding it is what `fracture` does). The opening itself
|
|
@@ -3004,8 +3316,9 @@ var Renderer = class {
|
|
|
3004
3316
|
this.sound(impactKey(def), ev.opening.center, 0.8);
|
|
3005
3317
|
}
|
|
3006
3318
|
/**
|
|
3007
|
-
* A unit came out of a wall of units: the unit flies as a fragment with a puff of its dust
|
|
3008
|
-
* the
|
|
3319
|
+
* A unit came out of a wall of units: the unit flies as a fragment with a puff of its dust, and the marks drawn on it (bullet holes, spalls, the impact
|
|
3320
|
+
* marks of the effects) go with it. The gap itself is the drawing's (the house renderer cuts the wall where the unit was); a part drawn as one box
|
|
3321
|
+
* keeps its box.
|
|
3009
3322
|
*/
|
|
3010
3323
|
onChip(ev) {
|
|
3011
3324
|
const pv = this.partVis(ev.object, ev.part);
|
|
@@ -3017,7 +3330,17 @@ var Renderer = class {
|
|
|
3017
3330
|
const tone2 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
|
|
3018
3331
|
this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone2, false), ev.object, maxAlive);
|
|
3019
3332
|
}
|
|
3020
|
-
if (
|
|
3333
|
+
if (ev.torn) {
|
|
3334
|
+
this.sound(impactKey(def), ev.center, 0.5);
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3337
|
+
const T = this.THREE;
|
|
3338
|
+
const center = new T.Vector3(ev.center[0], ev.center[1], ev.center[2]);
|
|
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);
|
|
3340
|
+
const rotation = new T.Quaternion(ev.rotation[0], ev.rotation[1], ev.rotation[2], ev.rotation[3]);
|
|
3341
|
+
for (const part of this.bounds.get(ev.object)?.parts.values() ?? []) for (const mv of part.meshes) this.visuals.clearDecals(mv, center, half, rotation);
|
|
3342
|
+
this.vfx.peekWorkshop()?.decals.clearInside(ev.center, [half.x, half.y, half.z], ev.rotation);
|
|
3343
|
+
this.vfx.burst("dust", ev.center, [0, 1, 0], 8, 0.8, { color });
|
|
3021
3344
|
this.sound(impactKey(def), ev.center, 0.7);
|
|
3022
3345
|
}
|
|
3023
3346
|
onFracture(ev) {
|
|
@@ -3035,15 +3358,7 @@ var Renderer = class {
|
|
|
3035
3358
|
if (m && m.transparent) opacity = clamp(m.opacity, 0.2, 0.8);
|
|
3036
3359
|
this.visuals.hide(pv, "broken");
|
|
3037
3360
|
}
|
|
3038
|
-
|
|
3039
|
-
const step = this.low ? 2 : 1;
|
|
3040
|
-
for (let i = 0; i < ev.fragments.length; i += step) {
|
|
3041
|
-
const fr = ev.fragments[i];
|
|
3042
|
-
const fdef = this.world.materials.get(fr.material);
|
|
3043
|
-
const tone2 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def || !fdef ? color : fallbackColor(fr.material, fdef);
|
|
3044
|
-
const mat = this.fragments.materialFor(fr.material, tone2, transparent || fdef?.visual?.transparent === true, opacity);
|
|
3045
|
-
this.fragments.spawn(fr, mat, ev.object, maxAlive);
|
|
3046
|
-
}
|
|
3361
|
+
this.spawnFragments(ev, 0, { def, color, opacity, transparent });
|
|
3047
3362
|
const up = [0, 1, 0];
|
|
3048
3363
|
const sizeK = clamp(ev.fragments.length / 10, 0.5, 2);
|
|
3049
3364
|
switch (ev.style) {
|
|
@@ -3069,6 +3384,22 @@ var Renderer = class {
|
|
|
3069
3384
|
}
|
|
3070
3385
|
this.sound(fractureKey(def, ev.style), ev.point, clamp(0.4 + ev.fragments.length * 0.03, 0.4, 1));
|
|
3071
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
|
+
}
|
|
3072
3403
|
onDetach(ev) {
|
|
3073
3404
|
this.sound("detach", this.rootPoint(ev.object), 0.5);
|
|
3074
3405
|
this.splitLoose(ev.object);
|
|
@@ -3082,6 +3413,13 @@ var Renderer = class {
|
|
|
3082
3413
|
if (!st || st.groups.length < 2) return;
|
|
3083
3414
|
const loose = st.groups.length - 1;
|
|
3084
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
|
+
}
|
|
3085
3423
|
let newId;
|
|
3086
3424
|
try {
|
|
3087
3425
|
newId = this.world.splitGroup(objectId, 1);
|
|
@@ -3098,8 +3436,9 @@ var Renderer = class {
|
|
|
3098
3436
|
old.mass = Math.max(0.05, this.massOf(objectId));
|
|
3099
3437
|
old.sleeping = false;
|
|
3100
3438
|
} else if (kitOriginOf(b.root)?.kind === "furniture") {
|
|
3101
|
-
this.makePiece(objectId);
|
|
3439
|
+
const piece = this.makePiece(objectId);
|
|
3102
3440
|
this.tipIfTopHeavy(b);
|
|
3441
|
+
this.o.onLoosened?.(objectId, piece, "apart");
|
|
3103
3442
|
}
|
|
3104
3443
|
}
|
|
3105
3444
|
/** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
|
|
@@ -3208,7 +3547,7 @@ var Renderer = class {
|
|
|
3208
3547
|
const piece = this.makePiece(b.id, { mass });
|
|
3209
3548
|
const body = this.pieceSys.get(b.id);
|
|
3210
3549
|
if (!body) continue;
|
|
3211
|
-
this.o.onLoosened?.(b.id, piece);
|
|
3550
|
+
this.o.onLoosened?.(b.id, piece, "blast");
|
|
3212
3551
|
const spread = half[0];
|
|
3213
3552
|
const momentum = hit.speed * mass;
|
|
3214
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]);
|
|
@@ -3403,6 +3742,7 @@ var Renderer = class {
|
|
|
3403
3742
|
}
|
|
3404
3743
|
clear() {
|
|
3405
3744
|
this.queue.length = 0;
|
|
3745
|
+
this.slices.length = 0;
|
|
3406
3746
|
this.active.clear();
|
|
3407
3747
|
for (const key of [...this.fires.keys()]) this.stopFire(key);
|
|
3408
3748
|
this.leaks.length = 0;
|
|
@@ -3422,6 +3762,15 @@ var Renderer = class {
|
|
|
3422
3762
|
this.vfx.sprites().clear();
|
|
3423
3763
|
this.ownVfx?.world.clear();
|
|
3424
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
|
+
}
|
|
3425
3774
|
wakeRegion(x, z, radius) {
|
|
3426
3775
|
this.fragments.wakeRegion(x, z, radius);
|
|
3427
3776
|
this.pieceSys.wakeRegion(x, z, radius);
|
|
@@ -3547,7 +3896,18 @@ var DRAG_SOUNDS = [
|
|
|
3547
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] }))]),
|
|
3548
3897
|
drag("drag_soft", [noise({ type: "lowpass", f: 420, d: 0.28, vol: 0.3 })])
|
|
3549
3898
|
];
|
|
3550
|
-
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] };
|
|
3551
3911
|
|
|
3552
3912
|
// src/audio/sound.ts
|
|
3553
3913
|
import { GAME_SFX_BANK } from "@adatechnology/game-audio";
|
|
@@ -3563,6 +3923,32 @@ var DESTRUCTION_SOUND_KEYS = {
|
|
|
3563
3923
|
drag_glass: { sound: "drag_glass" },
|
|
3564
3924
|
drag_masonry: { sound: "drag_masonry" },
|
|
3565
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" },
|
|
3566
3952
|
shatter_glass: { sound: "glass_shatter" },
|
|
3567
3953
|
crumble: { sound: "crumble" },
|
|
3568
3954
|
splinter: { sound: "crack" },
|