@adatechnology/game-destruction-three 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +34 -3
- package/dist/index.global.js +613 -37
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +405 -27
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -7,6 +7,44 @@ import { blastShove } from "@adatechnology/game-contracts";
|
|
|
7
7
|
import { createVfx, effectForEvent, EVENT_EFFECTS_MAP } from "@adatechnology/game-vfx";
|
|
8
8
|
import { createVfxRenderer } from "@adatechnology/game-vfx-three";
|
|
9
9
|
|
|
10
|
+
// src/fitted-boxes.ts
|
|
11
|
+
var SLICE_LENGTH = 0.12;
|
|
12
|
+
var MAX_SLICES = 12;
|
|
13
|
+
var SNUG_RATIO = 1.1;
|
|
14
|
+
function pushFittedBoxes(local, matrix, out) {
|
|
15
|
+
const e = matrix.elements;
|
|
16
|
+
const sizes = [0, 1, 2].map((k) => k === 0 ? local.max.x - local.min.x : k === 1 ? local.max.y - local.min.y : local.max.z - local.min.z);
|
|
17
|
+
const lengths = [0, 1, 2].map((k) => sizes[k] * Math.hypot(e[k * 4], e[k * 4 + 1], e[k * 4 + 2]));
|
|
18
|
+
const longest = lengths.indexOf(Math.max(...lengths));
|
|
19
|
+
const whole = worldBoxOf(local, matrix, longest, 0, 1);
|
|
20
|
+
const span = (whole[3] - whole[0]) * (whole[4] - whole[1]) * (whole[5] - whole[2]);
|
|
21
|
+
const volume = lengths[0] * lengths[1] * lengths[2];
|
|
22
|
+
const slices = span <= volume * SNUG_RATIO ? 1 : Math.min(MAX_SLICES, Math.ceil(lengths[longest] / SLICE_LENGTH));
|
|
23
|
+
if (slices <= 1) {
|
|
24
|
+
out.push(...whole);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (let i = 0; i < slices; i++) out.push(...worldBoxOf(local, matrix, longest, i / slices, (i + 1) / slices));
|
|
28
|
+
}
|
|
29
|
+
function worldBoxOf(local, matrix, axis, from, to) {
|
|
30
|
+
const e = matrix.elements;
|
|
31
|
+
const low = [local.min.x, local.min.y, local.min.z];
|
|
32
|
+
const high = [local.max.x, local.max.y, local.max.z];
|
|
33
|
+
const start = low[axis];
|
|
34
|
+
const size = high[axis] - start;
|
|
35
|
+
low[axis] = start + size * from;
|
|
36
|
+
high[axis] = start + size * to;
|
|
37
|
+
const box = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];
|
|
38
|
+
for (const x of [low[0], high[0]]) for (const y of [low[1], high[1]]) for (const z of [low[2], high[2]]) {
|
|
39
|
+
for (let k = 0; k < 3; k++) {
|
|
40
|
+
const v = e[k] * x + e[k + 4] * y + e[k + 8] * z + e[k + 12];
|
|
41
|
+
box[k] = Math.min(box[k], v);
|
|
42
|
+
box[k + 3] = Math.max(box[k + 3], v);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return box;
|
|
46
|
+
}
|
|
47
|
+
|
|
10
48
|
// src/math.ts
|
|
11
49
|
function createRng(seed) {
|
|
12
50
|
let a = seed >>> 0;
|
|
@@ -48,12 +86,20 @@ function mixHex(a, b, t) {
|
|
|
48
86
|
var SHEET_MAX = 4e-3;
|
|
49
87
|
var SHEET_AIR = 2.5;
|
|
50
88
|
var SHEET_SWAY = 3;
|
|
89
|
+
var BIG_LENGTH = 0.2;
|
|
90
|
+
var BIG_WIDTH = 0.03;
|
|
91
|
+
var isBig = (size) => {
|
|
92
|
+
const [, middle, longest] = [...size].sort((a, b) => a - b);
|
|
93
|
+
return longest >= BIG_LENGTH && middle >= BIG_WIDTH;
|
|
94
|
+
};
|
|
51
95
|
var FragmentSystem = class {
|
|
52
96
|
constructor(THREE, o) {
|
|
53
97
|
__publicField(this, "THREE", THREE);
|
|
54
98
|
__publicField(this, "o", o);
|
|
55
99
|
__publicField(this, "list", []);
|
|
56
100
|
__publicField(this, "box");
|
|
101
|
+
/** The mesh box of a fragment before its scale. */
|
|
102
|
+
__publicField(this, "unitBox", { min: { x: -0.5, y: -0.5, z: -0.5 }, max: { x: 0.5, y: 0.5, z: 0.5 } });
|
|
57
103
|
__publicField(this, "shardGeos", []);
|
|
58
104
|
__publicField(this, "materials", /* @__PURE__ */ new Map());
|
|
59
105
|
__publicField(this, "q");
|
|
@@ -62,6 +108,8 @@ var FragmentSystem = class {
|
|
|
62
108
|
__publicField(this, "v");
|
|
63
109
|
__publicField(this, "w");
|
|
64
110
|
__publicField(this, "rng", createRng(1374496523));
|
|
111
|
+
/** Count of `update` calls: what makes the fragments of one burst tell apart from older ones. */
|
|
112
|
+
__publicField(this, "tick", 0);
|
|
65
113
|
__publicField(this, "markGeometry", null);
|
|
66
114
|
__publicField(this, "markMaterial", null);
|
|
67
115
|
this.box = new THREE.BoxGeometry(1, 1, 1);
|
|
@@ -129,8 +177,28 @@ var FragmentSystem = class {
|
|
|
129
177
|
mesh.add(disc);
|
|
130
178
|
}
|
|
131
179
|
}
|
|
132
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* Create the mesh of a fragment and start its motion. `maxAllowed` is the live budget: over it the oldest fragment still showing fades out to make room, and a burst
|
|
182
|
+
* bigger than the budget (every live fragment was made in this same frame) does not make the rest at all, so a blast never builds a thousand meshes only to fade them
|
|
183
|
+
* out. Returns null when the fragment was not made.
|
|
184
|
+
*/
|
|
133
185
|
spawn(f, material, home, maxAllowed) {
|
|
186
|
+
if (this.list.length >= maxAllowed) {
|
|
187
|
+
let alive = 0;
|
|
188
|
+
let oldest;
|
|
189
|
+
let oldestSmall;
|
|
190
|
+
for (const l of this.list) {
|
|
191
|
+
if (l.dying) continue;
|
|
192
|
+
alive++;
|
|
193
|
+
oldest ?? (oldest = l);
|
|
194
|
+
if (!l.body) oldestSmall ?? (oldestSmall = l);
|
|
195
|
+
}
|
|
196
|
+
oldest = oldestSmall ?? oldest;
|
|
197
|
+
if (alive >= maxAllowed) {
|
|
198
|
+
if (!oldest || oldest.born === this.tick) return null;
|
|
199
|
+
oldest.dying = true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
134
202
|
const geo = f.shape === "shard" ? this.shardGeometry(hashString(f.id)) : this.box;
|
|
135
203
|
const mesh = new this.THREE.Mesh(geo, material);
|
|
136
204
|
mesh.position.set(f.center[0], f.center[1], f.center[2]);
|
|
@@ -154,19 +222,42 @@ var FragmentSystem = class {
|
|
|
154
222
|
rest: 0,
|
|
155
223
|
fade: 1,
|
|
156
224
|
dying: false,
|
|
157
|
-
air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0
|
|
225
|
+
air: f.shape === "scrap" && sz[2] <= SHEET_MAX ? SHEET_AIR : 0,
|
|
226
|
+
body: null,
|
|
227
|
+
born: this.tick
|
|
158
228
|
};
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
229
|
+
if (f.shape !== "shard" && isBig(f.size)) {
|
|
230
|
+
live.body = {
|
|
231
|
+
mass: f.mass,
|
|
232
|
+
push: (x, z) => {
|
|
233
|
+
live.vel.x = x;
|
|
234
|
+
live.vel.z = z;
|
|
235
|
+
live.vel.y = Math.max(live.vel.y, 0.3);
|
|
236
|
+
live.asleep = false;
|
|
237
|
+
live.rest = 0;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
166
240
|
}
|
|
241
|
+
this.list.push(live);
|
|
167
242
|
this.addMarks(mesh, f);
|
|
168
243
|
return mesh;
|
|
169
244
|
}
|
|
245
|
+
/** The boxes of the big fragments around (x, z). */
|
|
246
|
+
solidsNear(x, z, radius, found) {
|
|
247
|
+
const boxes = [];
|
|
248
|
+
for (const f of this.list) {
|
|
249
|
+
if (!f.body || f.dying) continue;
|
|
250
|
+
const reach = f.half.length() + radius;
|
|
251
|
+
if (Math.abs(f.mesh.position.x - x) > reach || Math.abs(f.mesh.position.z - z) > reach) continue;
|
|
252
|
+
f.mesh.updateMatrixWorld();
|
|
253
|
+
boxes.length = 0;
|
|
254
|
+
pushFittedBoxes(this.unitBox, f.mesh.matrixWorld, boxes);
|
|
255
|
+
for (let i = 0; i < boxes.length; i += 6) {
|
|
256
|
+
if (x < boxes[i] - radius || x > boxes[i + 3] + radius || z < boxes[i + 2] - radius || z > boxes[i + 5] + radius) continue;
|
|
257
|
+
found(f.home, [boxes[i], boxes[i + 1], boxes[i + 2]], [boxes[i + 3], boxes[i + 4], boxes[i + 5]], f.body);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
170
261
|
/** Add velocity to every fragment within `radius` of a point (blasts). */
|
|
171
262
|
blast(point, radius, speed) {
|
|
172
263
|
for (const f of this.list) {
|
|
@@ -182,7 +273,16 @@ var FragmentSystem = class {
|
|
|
182
273
|
f.rest = 0;
|
|
183
274
|
}
|
|
184
275
|
}
|
|
276
|
+
/** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
|
|
277
|
+
wakeRegion(x, z, radius) {
|
|
278
|
+
for (const f of this.list) {
|
|
279
|
+
if (!f.asleep || Math.hypot(f.mesh.position.x - x, f.mesh.position.z - z) > radius) continue;
|
|
280
|
+
f.asleep = false;
|
|
281
|
+
f.rest = 0;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
185
284
|
update(dt) {
|
|
285
|
+
this.tick++;
|
|
186
286
|
const g = this.o.gravity;
|
|
187
287
|
for (let i = this.list.length - 1; i >= 0; i--) {
|
|
188
288
|
const f = this.list[i];
|
|
@@ -197,7 +297,7 @@ var FragmentSystem = class {
|
|
|
197
297
|
}
|
|
198
298
|
if (f.asleep) {
|
|
199
299
|
f.rest += dt;
|
|
200
|
-
if (this.o.life > 0 && f.rest > this.o.life) f.dying = true;
|
|
300
|
+
if (this.o.life > 0 && f.rest > this.o.life && !f.body) f.dying = true;
|
|
201
301
|
continue;
|
|
202
302
|
}
|
|
203
303
|
this.step(f, dt, g);
|
|
@@ -271,7 +371,7 @@ var FragmentSystem = class {
|
|
|
271
371
|
let small = 0;
|
|
272
372
|
for (let i = 1; i < 3; i++) if (half[i] < half[small]) small = i;
|
|
273
373
|
let axis = small;
|
|
274
|
-
if (Math.abs(up[small]) < 0.35) {
|
|
374
|
+
if (Math.abs(up[small]) < 0.35 && !f.body) {
|
|
275
375
|
axis = 0;
|
|
276
376
|
for (let i = 1; i < 3; i++) if (Math.abs(up[i]) > Math.abs(up[axis])) axis = i;
|
|
277
377
|
}
|
|
@@ -397,6 +497,30 @@ var HARD_LANDING = 3;
|
|
|
397
497
|
var SLOP = 2e-3;
|
|
398
498
|
var NARROW = 0.3;
|
|
399
499
|
var TOPPLE = 3;
|
|
500
|
+
var TIP_ACCELERATION = 30;
|
|
501
|
+
var TIP_LYING = 0.25;
|
|
502
|
+
var TIP_SECONDS = 3;
|
|
503
|
+
var STEP = 0.03;
|
|
504
|
+
var boxTopAt = (s, x, z) => {
|
|
505
|
+
const e = s.e;
|
|
506
|
+
const dx = x - s.c[0], dz = z - s.c[2];
|
|
507
|
+
let t0 = -Infinity, t1 = Infinity;
|
|
508
|
+
for (let k = 0; k < 3; k++) {
|
|
509
|
+
const p = e[k * 4] * dx + e[k * 4 + 2] * dz;
|
|
510
|
+
const d = e[k * 4 + 1];
|
|
511
|
+
const h = s.half[k];
|
|
512
|
+
if (Math.abs(d) < 1e-9) {
|
|
513
|
+
if (Math.abs(p) > h) return -Infinity;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
let a = (-h - p) / d, b = (h - p) / d;
|
|
517
|
+
if (a > b) [a, b] = [b, a];
|
|
518
|
+
t0 = Math.max(t0, a);
|
|
519
|
+
t1 = Math.min(t1, b);
|
|
520
|
+
if (t0 > t1) return -Infinity;
|
|
521
|
+
}
|
|
522
|
+
return s.c[1] + t1;
|
|
523
|
+
};
|
|
400
524
|
var PieceSystem = class {
|
|
401
525
|
constructor(THREE, o) {
|
|
402
526
|
__publicField(this, "THREE", THREE);
|
|
@@ -408,6 +532,7 @@ var PieceSystem = class {
|
|
|
408
532
|
__publicField(this, "dq");
|
|
409
533
|
__publicField(this, "m");
|
|
410
534
|
__publicField(this, "ax");
|
|
535
|
+
__publicField(this, "solids", []);
|
|
411
536
|
this.p = new THREE.Vector3();
|
|
412
537
|
this.q = new THREE.Quaternion();
|
|
413
538
|
this.dq = new THREE.Quaternion();
|
|
@@ -417,6 +542,11 @@ var PieceSystem = class {
|
|
|
417
542
|
get(objectId) {
|
|
418
543
|
return this.list.find((b) => b.objectId === objectId) ?? null;
|
|
419
544
|
}
|
|
545
|
+
/** Sets a piece falling over, about the world axes x and z (a unit axis): its feet are thinner than the box that stands for it, so the box alone would hold it up. */
|
|
546
|
+
tip(body, x, z) {
|
|
547
|
+
body.tipping = { x, z, seconds: TIP_SECONDS };
|
|
548
|
+
body.sleeping = false;
|
|
549
|
+
}
|
|
420
550
|
create(objectId, root, halfExtents, localCenter, mass, velocity, angular) {
|
|
421
551
|
const existing = this.get(objectId);
|
|
422
552
|
if (existing) {
|
|
@@ -430,6 +560,7 @@ var PieceSystem = class {
|
|
|
430
560
|
}
|
|
431
561
|
const body = {
|
|
432
562
|
objectId,
|
|
563
|
+
tipping: null,
|
|
433
564
|
root,
|
|
434
565
|
velocity: velocity ? [...velocity] : [0, 0, 0],
|
|
435
566
|
angularVelocity: angular ? [...angular] : [0, 0, 0],
|
|
@@ -499,8 +630,21 @@ var PieceSystem = class {
|
|
|
499
630
|
const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
|
|
500
631
|
return [e[0] * lx + e[4] * ly + e[8] * lz, e[1] * lx + e[5] * ly + e[9] * lz, e[2] * lx + e[6] * ly + e[10] * lz];
|
|
501
632
|
}
|
|
633
|
+
/** What lies inside the disc looks at the ground again: it was dug or heaped under it. */
|
|
634
|
+
wakeRegion(x, z, radius) {
|
|
635
|
+
for (const b of this.list) {
|
|
636
|
+
if (!b.sleeping || b.held || Math.hypot(b.root.position.x - x, b.root.position.z - z) > radius) continue;
|
|
637
|
+
b.sleeping = false;
|
|
638
|
+
b.still = 0;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
502
641
|
update(dt) {
|
|
503
642
|
this.time += dt;
|
|
643
|
+
this.solids = this.list.map((body) => {
|
|
644
|
+
const c = this.centerOf(body, new this.THREE.Vector3());
|
|
645
|
+
const h = body.halfExtents;
|
|
646
|
+
return { body, c: [c.x, c.y, c.z], e: [...new this.THREE.Matrix4().makeRotationFromQuaternion(body.root.quaternion).elements], half: h, r: Math.hypot(h[0], h[1], h[2]) };
|
|
647
|
+
});
|
|
504
648
|
for (const b of this.list) {
|
|
505
649
|
if (b.held) {
|
|
506
650
|
b.velocity = [0, 0, 0];
|
|
@@ -514,6 +658,17 @@ var PieceSystem = class {
|
|
|
514
658
|
}
|
|
515
659
|
this.separate(dt);
|
|
516
660
|
}
|
|
661
|
+
/** Where a corner of `self` at `(x, y, z)` lands: the ground, the top of another piece or of something fixed under it. */
|
|
662
|
+
supportAt(self, x, y, z, step) {
|
|
663
|
+
let top = this.o.groundAt(x, z);
|
|
664
|
+
for (const s of this.solids) {
|
|
665
|
+
if (s.body === self || s.body.held || Math.abs(x - s.c[0]) > s.r || Math.abs(z - s.c[2]) > s.r) continue;
|
|
666
|
+
const t = boxTopAt(s, x, z);
|
|
667
|
+
if (t > top && t <= y + step) top = t;
|
|
668
|
+
}
|
|
669
|
+
const fixed = this.o.staticTopAt?.(x, z, y, step) ?? -Infinity;
|
|
670
|
+
return Math.max(top, fixed);
|
|
671
|
+
}
|
|
517
672
|
integrate(b, dt) {
|
|
518
673
|
const c = this.centerOf(b, this.p);
|
|
519
674
|
const pos = [c.x, c.y, c.z];
|
|
@@ -552,8 +707,9 @@ var PieceSystem = class {
|
|
|
552
707
|
const sy = i & 2 ? hy : -hy;
|
|
553
708
|
const sz = i & 4 ? hz : -hz;
|
|
554
709
|
const r = [e[0] * sx + e[4] * sy + e[8] * sz, e[1] * sx + e[5] * sy + e[9] * sz, e[2] * sx + e[6] * sy + e[10] * sz];
|
|
555
|
-
const
|
|
556
|
-
const
|
|
710
|
+
const cy = pos[1] + r[1];
|
|
711
|
+
const gy = this.supportAt(b, pos[0] + r[0], cy, pos[2] + r[2], STEP + Math.max(0, -v[1]) * h * 2);
|
|
712
|
+
const pen = gy - cy;
|
|
557
713
|
if (pen > -SLOP) {
|
|
558
714
|
rs.push(r);
|
|
559
715
|
if (pen > maxPen) maxPen = pen;
|
|
@@ -576,7 +732,7 @@ var PieceSystem = class {
|
|
|
576
732
|
}
|
|
577
733
|
const lx = pos[0] + sx / rs.length;
|
|
578
734
|
const lz = pos[2] + sz / rs.length;
|
|
579
|
-
landPoint = [lx,
|
|
735
|
+
landPoint = [lx, pos[1] - Math.max(...rs.map((r) => -r[1])), lz];
|
|
580
736
|
}
|
|
581
737
|
for (let it = 0; it < 4; it++) {
|
|
582
738
|
for (const r of rs) {
|
|
@@ -629,6 +785,15 @@ var PieceSystem = class {
|
|
|
629
785
|
const wx = x1 - x0;
|
|
630
786
|
const wz = z1 - z0;
|
|
631
787
|
unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
|
|
788
|
+
if (b.tipping) {
|
|
789
|
+
b.tipping.seconds -= h;
|
|
790
|
+
if (height < TIP_LYING || b.tipping.seconds <= 0) b.tipping = null;
|
|
791
|
+
else {
|
|
792
|
+
w[0] += b.tipping.x * TIP_ACCELERATION * h;
|
|
793
|
+
w[2] += b.tipping.z * TIP_ACCELERATION * h;
|
|
794
|
+
unstable = true;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
632
797
|
if (unstable) {
|
|
633
798
|
const dirX = x0 + x1 > 0 ? -1 : 1;
|
|
634
799
|
const dirZ = z0 + z1 > 0 ? -1 : 1;
|
|
@@ -665,6 +830,11 @@ var PieceSystem = class {
|
|
|
665
830
|
b.still = 0;
|
|
666
831
|
}
|
|
667
832
|
}
|
|
833
|
+
/** Half the height of the box of a piece as it is turned now. */
|
|
834
|
+
halfHeight(b) {
|
|
835
|
+
const e = this.m.makeRotationFromQuaternion(b.root.quaternion).elements;
|
|
836
|
+
return Math.abs(e[1]) * b.halfExtents[0] + Math.abs(e[5]) * b.halfExtents[1] + Math.abs(e[9]) * b.halfExtents[2];
|
|
837
|
+
}
|
|
668
838
|
/** Push overlapping pieces apart (horizontal bounding circles), gently, so they do not interpenetrate forever. */
|
|
669
839
|
separate(dt) {
|
|
670
840
|
const n = this.list.length;
|
|
@@ -683,6 +853,7 @@ var PieceSystem = class {
|
|
|
683
853
|
this.centerOf(a, ca);
|
|
684
854
|
this.centerOf(b, cb);
|
|
685
855
|
if (Math.abs(ca.y - cb.y) > ra + rb) continue;
|
|
856
|
+
if (Math.abs(ca.y - cb.y) >= (this.halfHeight(a) + this.halfHeight(b)) * 0.9) continue;
|
|
686
857
|
const rh = (Math.max(a.halfExtents[0], a.halfExtents[2]) + Math.max(b.halfExtents[0], b.halfExtents[2])) * 0.8;
|
|
687
858
|
let dx = cb.x - ca.x;
|
|
688
859
|
let dz = cb.z - ca.z;
|
|
@@ -843,6 +1014,7 @@ function specFromObject3D(THREE, root, options = {}) {
|
|
|
843
1014
|
const fill = options.fillOf?.(o, material);
|
|
844
1015
|
const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
|
|
845
1016
|
const contains = containsOf(ud.destructionContains);
|
|
1017
|
+
const thickness = typeof ud.destructionThickness === "number" && ud.destructionThickness > 0 ? ud.destructionThickness : void 0;
|
|
846
1018
|
const existing = explicit ? byKey.get(explicit) : void 0;
|
|
847
1019
|
if (existing) {
|
|
848
1020
|
const before = sizeOf(existing.box);
|
|
@@ -855,8 +1027,9 @@ function specFromObject3D(THREE, root, options = {}) {
|
|
|
855
1027
|
existing.skin = existing.skin && skin;
|
|
856
1028
|
if (fill !== void 0) existing.fill = fill;
|
|
857
1029
|
if (contains) existing.contains = contains;
|
|
1030
|
+
if (thickness !== void 0) existing.thickness = Math.min(existing.thickness ?? thickness, thickness);
|
|
858
1031
|
} else {
|
|
859
|
-
const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
|
|
1032
|
+
const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, skin, tags, fixedId: !!explicit, ...contains ? { contains } : {}, ...thickness !== void 0 ? { thickness } : {}, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
|
|
860
1033
|
entries.push(e);
|
|
861
1034
|
if (explicit) byKey.set(explicit, e);
|
|
862
1035
|
}
|
|
@@ -924,7 +1097,8 @@ function specFromObject3D(THREE, root, options = {}) {
|
|
|
924
1097
|
const minSide = Math.min(...size);
|
|
925
1098
|
if (e.fill !== void 0) part.fill = e.fill;
|
|
926
1099
|
else if (e.hollow) part.fill = 0.05;
|
|
927
|
-
if (
|
|
1100
|
+
if (e.thickness !== void 0) part.thickness = e.thickness;
|
|
1101
|
+
else if (minSide < 0.01 && METAL.test(e.material)) {
|
|
928
1102
|
part.fill = e.fill ?? 1;
|
|
929
1103
|
part.thickness = Math.max(minSide, 5e-4);
|
|
930
1104
|
}
|
|
@@ -1018,9 +1192,10 @@ function shellDisplacementAt(THREE, worldPoint, shells, out) {
|
|
|
1018
1192
|
const ids = [0, 1, 2].map((corner) => index ? index.getX(t * 3 + corner) : t * 3 + corner);
|
|
1019
1193
|
ids.forEach((id, corner) => corners[corner].set(shell.before[id * 3], shell.before[id * 3 + 1], shell.before[id * 3 + 2]));
|
|
1020
1194
|
triangle.set(corners[0], corners[1], corners[2]);
|
|
1195
|
+
if (triangle.getArea() <= 1e-14) continue;
|
|
1021
1196
|
triangle.closestPointToPoint(local, closest);
|
|
1022
1197
|
const distance = closest.distanceToSquared(local);
|
|
1023
|
-
if (distance
|
|
1198
|
+
if (!(distance < nearest)) continue;
|
|
1024
1199
|
nearest = distance;
|
|
1025
1200
|
nearestScale = shell.mesh.matrixWorld.getMaxScaleOnAxis() || 1;
|
|
1026
1201
|
triangle.getBarycoord(closest, weights);
|
|
@@ -1199,6 +1374,20 @@ var Visuals = class {
|
|
|
1199
1374
|
return out;
|
|
1200
1375
|
}
|
|
1201
1376
|
/** Box of the part in world space. */
|
|
1377
|
+
/** Appends the boxes that follow the mesh in the world (`pushFittedBoxes`) to `out`. */
|
|
1378
|
+
fittedBoxes(mv, out) {
|
|
1379
|
+
const box = new this.THREE.Box3();
|
|
1380
|
+
if (mv.host) this.localBox(mv, box);
|
|
1381
|
+
else {
|
|
1382
|
+
const geometry = mv.mesh.geometry;
|
|
1383
|
+
if (!geometry.boundingBox) geometry.computeBoundingBox();
|
|
1384
|
+
if (geometry.boundingBox) box.copy(geometry.boundingBox);
|
|
1385
|
+
else box.makeEmpty();
|
|
1386
|
+
}
|
|
1387
|
+
if (box.isEmpty()) return;
|
|
1388
|
+
mv.mesh.updateWorldMatrix(true, false);
|
|
1389
|
+
pushFittedBoxes(box, mv.mesh.matrixWorld, out);
|
|
1390
|
+
}
|
|
1202
1391
|
worldBox(mv, out) {
|
|
1203
1392
|
if (!mv.host) return out.setFromObject(mv.mesh);
|
|
1204
1393
|
this.localBox(mv, out);
|
|
@@ -1462,6 +1651,21 @@ var Visuals = class {
|
|
|
1462
1651
|
d.geometry?.dispose();
|
|
1463
1652
|
this.decalCount = Math.max(0, this.decalCount - 1);
|
|
1464
1653
|
}
|
|
1654
|
+
/**
|
|
1655
|
+
* 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
|
|
1656
|
+
* marked is gone (a unit came out of the wall with its bullet holes and spalls).
|
|
1657
|
+
*/
|
|
1658
|
+
clearDecals(mv, center, half, rotation) {
|
|
1659
|
+
const inv = rotation.clone().invert();
|
|
1660
|
+
const p = new this.THREE.Vector3();
|
|
1661
|
+
for (let i = mv.decals.length - 1; i >= 0; i--) {
|
|
1662
|
+
const d = mv.decals[i];
|
|
1663
|
+
d.obj.getWorldPosition(p).sub(center).applyQuaternion(inv);
|
|
1664
|
+
if (Math.abs(p.x) > half.x || Math.abs(p.y) > half.y || Math.abs(p.z) > half.z) continue;
|
|
1665
|
+
this.disposeDecal(d);
|
|
1666
|
+
mv.decals.splice(i, 1);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1465
1669
|
addHole(mv, worldPoint, worldNormal, diameter) {
|
|
1466
1670
|
const { s, pos, quat } = this.decalPose(mv.mesh, worldPoint, worldNormal, 25e-4);
|
|
1467
1671
|
const work = mv.host ? mv.host.work : mv.work;
|
|
@@ -1944,9 +2148,15 @@ var Visuals = class {
|
|
|
1944
2148
|
// src/renderer.ts
|
|
1945
2149
|
var rendererCount = 0;
|
|
1946
2150
|
var COLLAPSE_CLOUD_KG = 1e3;
|
|
2151
|
+
var CHIP_DECAL_EDGE = 0.05;
|
|
1947
2152
|
var IMPACT_TAGS = ["metal", "glass", "wood"];
|
|
1948
2153
|
var GLOW_FROM_C = 430;
|
|
1949
2154
|
var QUEUE_CAP = 4e3;
|
|
2155
|
+
var STATIC_REFRESH = 0.5;
|
|
2156
|
+
var STATIC_SPAN = 3;
|
|
2157
|
+
var FEET_BAND = 0.03;
|
|
2158
|
+
var TIP_MIN_HEIGHT = 0.3;
|
|
2159
|
+
var TIP_NARROW = 0.3;
|
|
1950
2160
|
var FIRE_CEILING_SECONDS = 3600;
|
|
1951
2161
|
var GENERIC_REFERENCE_RADIUS = 15;
|
|
1952
2162
|
var PRIMED_FIRE_RADIUS = 0.2;
|
|
@@ -1991,6 +2201,7 @@ function impactKey(def) {
|
|
|
1991
2201
|
if (def.fracture === "tear") return "impact_soft";
|
|
1992
2202
|
return def.fracture === "none" ? "impact_metal" : "impact_wood";
|
|
1993
2203
|
}
|
|
2204
|
+
var dragSoundKey = (def) => impactKey(def).replace("impact_", "drag_");
|
|
1994
2205
|
function fractureKey(def, style) {
|
|
1995
2206
|
switch (style) {
|
|
1996
2207
|
case "shatter":
|
|
@@ -2032,6 +2243,14 @@ var Renderer = class {
|
|
|
2032
2243
|
__publicField(this, "autoSplit");
|
|
2033
2244
|
__publicField(this, "maxFragments");
|
|
2034
2245
|
__publicField(this, "bounds", /* @__PURE__ */ new Map());
|
|
2246
|
+
/** 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
|
+
__publicField(this, "staticBoxes", /* @__PURE__ */ new Map());
|
|
2248
|
+
/** Objects to measure again; `staticAll` measures every one. */
|
|
2249
|
+
__publicField(this, "staticDirty", /* @__PURE__ */ new Set());
|
|
2250
|
+
__publicField(this, "staticAll", true);
|
|
2251
|
+
/** Boxes of the pieces' meshes (as `staticBoxes`), with the time they were measured. */
|
|
2252
|
+
__publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
|
|
2253
|
+
__publicField(this, "staticAt", 0);
|
|
2035
2254
|
__publicField(this, "queue", []);
|
|
2036
2255
|
__publicField(this, "unsubscribe");
|
|
2037
2256
|
__publicField(this, "visuals");
|
|
@@ -2093,6 +2312,7 @@ var Renderer = class {
|
|
|
2093
2312
|
this.cages = new RebarCages(THREE, o.scene);
|
|
2094
2313
|
this.pieceSys = new PieceSystem(THREE, {
|
|
2095
2314
|
groundAt: this.groundAt,
|
|
2315
|
+
staticTopAt: (x, z, y, step) => this.staticTopAt(x, z, y, step),
|
|
2096
2316
|
gravity: this.gravity,
|
|
2097
2317
|
scene: o.scene,
|
|
2098
2318
|
onHardLanding: (p, point, speed) => this.hardLanding(p, point, speed)
|
|
@@ -2105,6 +2325,7 @@ var Renderer = class {
|
|
|
2105
2325
|
// Binding
|
|
2106
2326
|
// ----------------------------------------------------------------------------------------------------------------------------------
|
|
2107
2327
|
bind(objectId, root, options = {}) {
|
|
2328
|
+
this.staticDirty.add(objectId);
|
|
2108
2329
|
if (this.bounds.has(objectId)) this.unbind(objectId);
|
|
2109
2330
|
const state = this.world.state(objectId);
|
|
2110
2331
|
const partIds = new Set((state?.parts ?? []).map((p) => p.id));
|
|
@@ -2224,6 +2445,7 @@ var Renderer = class {
|
|
|
2224
2445
|
}
|
|
2225
2446
|
}
|
|
2226
2447
|
unbind(objectId) {
|
|
2448
|
+
this.staticDirty.add(objectId);
|
|
2227
2449
|
const b = this.bounds.get(objectId);
|
|
2228
2450
|
if (!b) return;
|
|
2229
2451
|
this.restore(objectId);
|
|
@@ -2263,6 +2485,7 @@ var Renderer = class {
|
|
|
2263
2485
|
return m > 0 ? m : 5;
|
|
2264
2486
|
}
|
|
2265
2487
|
makePiece(objectId, options = {}) {
|
|
2488
|
+
this.staticDirty.add(objectId);
|
|
2266
2489
|
const b = this.bounds.get(objectId);
|
|
2267
2490
|
if (!b) throw new Error(`game-destruction-three: object "${objectId}" is not bound`);
|
|
2268
2491
|
if (b.root.parent && b.root.parent !== this.scene) this.scene.attach(b.root);
|
|
@@ -2375,7 +2598,100 @@ var Renderer = class {
|
|
|
2375
2598
|
// ----------------------------------------------------------------------------------------------------------------------------------
|
|
2376
2599
|
// Events
|
|
2377
2600
|
// ----------------------------------------------------------------------------------------------------------------------------------
|
|
2601
|
+
/**
|
|
2602
|
+
* The highest top of a bound object that is not a piece under (x, z) a corner at height `y` can land on (at most `step` above it). The world boxes
|
|
2603
|
+
* of their drawn meshes are kept until an event or a moved root changes them; parts drawn inside a merged mesh and meshes wider than `STATIC_SPAN`
|
|
2604
|
+
* are left out (walls, floors, roofs: the terrain under them is the host's `groundAt`).
|
|
2605
|
+
*/
|
|
2606
|
+
staticTopAt(x, z, y, step) {
|
|
2607
|
+
let top = -Infinity;
|
|
2608
|
+
for (const a of this.staticNow().values()) {
|
|
2609
|
+
if (a.length === 0 || x < a[0] || x > a[3] || z < a[2] || z > a[5]) continue;
|
|
2610
|
+
for (let i = 6; i < a.length; i += 6) {
|
|
2611
|
+
if (x < a[i] || x > a[i + 3] || z < a[i + 2] || z > a[i + 5]) continue;
|
|
2612
|
+
const t = a[i + 4];
|
|
2613
|
+
if (t > top && t <= y + step) top = t;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
return top;
|
|
2617
|
+
}
|
|
2618
|
+
/** The boxes of the fixed objects (per object: the box around them all, then each one), each object measured again when an event touched it (all of them every `STATIC_REFRESH` s). */
|
|
2619
|
+
staticNow() {
|
|
2620
|
+
if (this.time - this.staticAt > STATIC_REFRESH) {
|
|
2621
|
+
this.staticAt = this.time;
|
|
2622
|
+
this.staticAll = true;
|
|
2623
|
+
}
|
|
2624
|
+
if (!this.staticAll && this.staticDirty.size === 0) return this.staticBoxes;
|
|
2625
|
+
const box = new this.THREE.Box3();
|
|
2626
|
+
const measure = (b) => {
|
|
2627
|
+
if (b.piece || this.pieceSys.get(b.id)) {
|
|
2628
|
+
this.staticBoxes.delete(b.id);
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
const out = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];
|
|
2632
|
+
for (const mv of this.meshesOf(b)) {
|
|
2633
|
+
if (mv.host || mv.loose || !this.visuals.shown(mv)) continue;
|
|
2634
|
+
this.visuals.worldBox(mv, box);
|
|
2635
|
+
if (box.isEmpty() || box.max.x - box.min.x > STATIC_SPAN || box.max.z - box.min.z > STATIC_SPAN) continue;
|
|
2636
|
+
this.visuals.fittedBoxes(mv, out);
|
|
2637
|
+
for (let k = 0; k < 3; k++) {
|
|
2638
|
+
out[k] = Math.min(out[k], box.min.getComponent(k));
|
|
2639
|
+
out[k + 3] = Math.max(out[k + 3], box.max.getComponent(k));
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
this.staticBoxes.set(b.id, out.length > 6 ? out : []);
|
|
2643
|
+
};
|
|
2644
|
+
if (this.staticAll) {
|
|
2645
|
+
this.staticBoxes.clear();
|
|
2646
|
+
for (const b of this.bounds.values()) measure(b);
|
|
2647
|
+
} else {
|
|
2648
|
+
for (const id of this.staticDirty) {
|
|
2649
|
+
const b = this.bounds.get(id);
|
|
2650
|
+
if (b) measure(b);
|
|
2651
|
+
else this.staticBoxes.delete(id);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
this.staticAll = false;
|
|
2655
|
+
this.staticDirty.clear();
|
|
2656
|
+
return this.staticBoxes;
|
|
2657
|
+
}
|
|
2658
|
+
/** The boxes of a piece's drawn meshes, measured once per frame. */
|
|
2659
|
+
pieceBoxesOf(pc) {
|
|
2660
|
+
const cached = this.pieceBoxes.get(pc);
|
|
2661
|
+
if (cached && cached.at === this.time) return cached.boxes;
|
|
2662
|
+
const boxes = [];
|
|
2663
|
+
const b = this.bounds.get(pc.objectId);
|
|
2664
|
+
if (b) {
|
|
2665
|
+
for (const mv of this.meshesOf(b)) {
|
|
2666
|
+
if (this.visuals.shown(mv)) this.visuals.fittedBoxes(mv, boxes);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
this.pieceBoxes.set(pc, { at: this.time, boxes });
|
|
2670
|
+
return boxes;
|
|
2671
|
+
}
|
|
2672
|
+
solidsNear(x, z, radius) {
|
|
2673
|
+
const found = [];
|
|
2674
|
+
const scan = (a, id, piece, from) => {
|
|
2675
|
+
if (from > 0 && (a.length === 0 || x < a[0] - radius || x > a[3] + radius || z < a[2] - radius || z > a[5] + radius)) return;
|
|
2676
|
+
for (let i = from; i < a.length; i += 6) {
|
|
2677
|
+
if (x < a[i] - radius || x > a[i + 3] + radius || z < a[i + 2] - radius || z > a[i + 5] + radius) continue;
|
|
2678
|
+
found.push({ objectId: id, min: [a[i], a[i + 1], a[i + 2]], max: [a[i + 3], a[i + 4], a[i + 5]], piece });
|
|
2679
|
+
}
|
|
2680
|
+
};
|
|
2681
|
+
for (const [id, a] of this.staticNow()) scan(a, id, null, 6);
|
|
2682
|
+
const c = this.v1;
|
|
2683
|
+
for (const pc of this.pieceSys.list) {
|
|
2684
|
+
if (pc.held) continue;
|
|
2685
|
+
this.pieceSys.centerOf(pc, c);
|
|
2686
|
+
const reach = Math.hypot(pc.halfExtents[0], pc.halfExtents[1], pc.halfExtents[2]) + radius;
|
|
2687
|
+
if (Math.abs(c.x - x) > reach || Math.abs(c.z - z) > reach) continue;
|
|
2688
|
+
scan(this.pieceBoxesOf(pc), pc.objectId, pc, 0);
|
|
2689
|
+
}
|
|
2690
|
+
this.fragments.solidsNear(x, z, radius, (home, min, max, loose) => found.push({ objectId: home, min, max, piece: null, loose }));
|
|
2691
|
+
return found;
|
|
2692
|
+
}
|
|
2378
2693
|
apply(ev) {
|
|
2694
|
+
this.staticDirty.add(ev.object);
|
|
2379
2695
|
switch (ev.type) {
|
|
2380
2696
|
case "damage":
|
|
2381
2697
|
if (ev.damageType === "heat" || ev.damageType === "fire" || ev.damageType === "explosive") this.activate(ev.object, ev.part);
|
|
@@ -2396,7 +2712,7 @@ var Renderer = class {
|
|
|
2396
2712
|
break;
|
|
2397
2713
|
}
|
|
2398
2714
|
case "debris":
|
|
2399
|
-
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));
|
|
2715
|
+
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));
|
|
2400
2716
|
break;
|
|
2401
2717
|
case "fracture":
|
|
2402
2718
|
this.onFracture(ev);
|
|
@@ -2569,10 +2885,11 @@ var Renderer = class {
|
|
|
2569
2885
|
}
|
|
2570
2886
|
return def ? fallbackColor(def.id, def) : null;
|
|
2571
2887
|
}
|
|
2572
|
-
debrisOptions(kind, objectId, partId) {
|
|
2888
|
+
debrisOptions(kind, objectId, partId, material) {
|
|
2573
2889
|
const pv = this.partVis(objectId, partId);
|
|
2574
|
-
const def =
|
|
2890
|
+
const def = this.world.materials.get(material ?? pv?.material ?? "");
|
|
2575
2891
|
if (kind === "smoke") return { color: SMOKE_TONES[def?.thermal.smoke ?? "grey"] };
|
|
2892
|
+
if ((kind === "dust" || kind === "chips") && def?.visual?.innerColor !== void 0) return { color: def.visual.innerColor };
|
|
2576
2893
|
if (kind === "fibers" || kind === "leaves") {
|
|
2577
2894
|
if (def?.visual?.innerColor !== void 0) return { color: def.visual.innerColor };
|
|
2578
2895
|
const c = pv ? this.visuals.baseColorHex(pv.meshes[0] ?? {}) : -1;
|
|
@@ -2680,8 +2997,11 @@ var Renderer = class {
|
|
|
2680
2997
|
const mv = this.nearestMesh(pv, wp);
|
|
2681
2998
|
if (!mv) return;
|
|
2682
2999
|
const def = this.world.materials.get(pv.material);
|
|
2683
|
-
|
|
2684
|
-
|
|
3000
|
+
const normal = this.faceNormal(mv, wp);
|
|
3001
|
+
const core = def?.visual?.innerColor;
|
|
3002
|
+
if (core !== void 0) this.vfx.burst("chips", ev.point, [normal.x, normal.y, normal.z], 5, 2.5, { color: core });
|
|
3003
|
+
else this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.point);
|
|
3004
|
+
this.visuals.addSpall(mv, wp, normal, Math.sqrt(ev.area / Math.PI), core ?? fallbackColor(pv.material, def));
|
|
2685
3005
|
}
|
|
2686
3006
|
/**
|
|
2687
3007
|
* 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
|
|
@@ -2704,16 +3024,27 @@ var Renderer = class {
|
|
|
2704
3024
|
this.sound(impactKey(def), ev.opening.center, 0.8);
|
|
2705
3025
|
}
|
|
2706
3026
|
/**
|
|
2707
|
-
* A unit came out of a wall of units: the unit flies as a fragment with a puff of its dust
|
|
2708
|
-
* the
|
|
3027
|
+
* 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
|
|
3028
|
+
* 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
|
|
3029
|
+
* keeps its box.
|
|
2709
3030
|
*/
|
|
2710
3031
|
onChip(ev) {
|
|
2711
3032
|
const pv = this.partVis(ev.object, ev.part);
|
|
2712
3033
|
const def = pv ? this.world.materials.get(pv.material) : void 0;
|
|
2713
3034
|
const color = (pv ? this.partColor(pv, def) : null) ?? fallbackColor(pv?.material ?? "", def);
|
|
2714
3035
|
const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
|
|
2715
|
-
for (const fr of ev.fragments)
|
|
2716
|
-
|
|
3036
|
+
for (const fr of ev.fragments) {
|
|
3037
|
+
const fdef = this.world.materials.get(fr.material);
|
|
3038
|
+
const tone2 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
|
|
3039
|
+
this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone2, false), ev.object, maxAlive);
|
|
3040
|
+
}
|
|
3041
|
+
const T = this.THREE;
|
|
3042
|
+
const center = new T.Vector3(ev.center[0], ev.center[1], ev.center[2]);
|
|
3043
|
+
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);
|
|
3044
|
+
const rotation = new T.Quaternion(ev.rotation[0], ev.rotation[1], ev.rotation[2], ev.rotation[3]);
|
|
3045
|
+
for (const part of this.bounds.get(ev.object)?.parts.values() ?? []) for (const mv of part.meshes) this.visuals.clearDecals(mv, center, half, rotation);
|
|
3046
|
+
this.vfx.peekWorkshop()?.decals.clearInside(ev.center, [half.x, half.y, half.z], ev.rotation);
|
|
3047
|
+
this.vfx.burst("dust", ev.center, [0, 1, 0], 8, 0.8, { color });
|
|
2717
3048
|
this.sound(impactKey(def), ev.center, 0.7);
|
|
2718
3049
|
}
|
|
2719
3050
|
onFracture(ev) {
|
|
@@ -2793,6 +3124,9 @@ var Renderer = class {
|
|
|
2793
3124
|
this.pieceSys.setCenter(old, center);
|
|
2794
3125
|
old.mass = Math.max(0.05, this.massOf(objectId));
|
|
2795
3126
|
old.sleeping = false;
|
|
3127
|
+
} else if (kitOriginOf(b.root)?.kind === "furniture") {
|
|
3128
|
+
this.makePiece(objectId);
|
|
3129
|
+
this.tipIfTopHeavy(b);
|
|
2796
3130
|
}
|
|
2797
3131
|
}
|
|
2798
3132
|
/** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
|
|
@@ -2839,8 +3173,32 @@ var Renderer = class {
|
|
|
2839
3173
|
}
|
|
2840
3174
|
const piece = this.pieceSys.create(newId, group, half, center, this.massOf(newId), vel, ang);
|
|
2841
3175
|
nb.piece = piece;
|
|
3176
|
+
if (kitOriginOf(from.root)?.kind === "furniture") this.tipIfTopHeavy(nb);
|
|
2842
3177
|
this.o.onPiece?.(piece);
|
|
2843
3178
|
}
|
|
3179
|
+
/**
|
|
3180
|
+
* A piece of furniture standing on feet too close for its height (a board on two legs in a line, a lone leg) does not stay up: the box of a piece hides that its weight is up
|
|
3181
|
+
* high and its feet are thin, so it gets the start of the fall, across the line of its feet.
|
|
3182
|
+
*/
|
|
3183
|
+
tipIfTopHeavy(b) {
|
|
3184
|
+
const piece = b.piece;
|
|
3185
|
+
if (!piece) return;
|
|
3186
|
+
const box = new this.THREE.Box3();
|
|
3187
|
+
const boxes = this.meshesOf(b).filter((mv) => this.visuals.shown(mv)).map((mv) => this.visuals.worldBox(mv, new this.THREE.Box3()));
|
|
3188
|
+
if (boxes.length === 0) return;
|
|
3189
|
+
for (const one of boxes) box.union(one);
|
|
3190
|
+
const height = box.max.y - box.min.y;
|
|
3191
|
+
const feet = new this.THREE.Box3();
|
|
3192
|
+
for (const one of boxes) if (one.min.y < box.min.y + FEET_BAND) feet.union(one);
|
|
3193
|
+
const spanX = feet.max.x - feet.min.x;
|
|
3194
|
+
const spanZ = feet.max.z - feet.min.z;
|
|
3195
|
+
if (height < TIP_MIN_HEIGHT || Math.min(spanX, spanZ) >= TIP_NARROW * height) return;
|
|
3196
|
+
const across = spanX <= spanZ ? 0 : 2;
|
|
3197
|
+
const lean = across === 0 ? box.min.x + box.max.x - feet.min.x - feet.max.x : box.min.z + box.max.z - feet.min.z - feet.max.z;
|
|
3198
|
+
const away = lean < 0 ? -1 : 1;
|
|
3199
|
+
if (across === 0) this.pieceSys.tip(piece, 0, -away);
|
|
3200
|
+
else this.pieceSys.tip(piece, away, 0);
|
|
3201
|
+
}
|
|
2844
3202
|
onExplode(ev, options = {}) {
|
|
2845
3203
|
const p = ev.point;
|
|
2846
3204
|
if (options.visual !== false) this.playExplosion(ev, options);
|
|
@@ -3091,6 +3449,10 @@ var Renderer = class {
|
|
|
3091
3449
|
this.vfx.sprites().clear();
|
|
3092
3450
|
this.ownVfx?.world.clear();
|
|
3093
3451
|
}
|
|
3452
|
+
wakeRegion(x, z, radius) {
|
|
3453
|
+
this.fragments.wakeRegion(x, z, radius);
|
|
3454
|
+
this.pieceSys.wakeRegion(x, z, radius);
|
|
3455
|
+
}
|
|
3094
3456
|
stats() {
|
|
3095
3457
|
return { fragments: this.fragments.count, particles: this.vfx.sprites().live + this.ownedVfxParticles(), pieces: this.pieceSys.list.length, decals: this.visuals.decalCount + this.ground.count };
|
|
3096
3458
|
}
|
|
@@ -3204,7 +3566,15 @@ var BURN_LOOP = {
|
|
|
3204
3566
|
volume: 0,
|
|
3205
3567
|
sources: ["cena:L1316 fire loop (silent in the page: its gain is never raised)", "sdk: the level while burning"]
|
|
3206
3568
|
};
|
|
3207
|
-
var
|
|
3569
|
+
var drag = (id, layers) => ({ ...sceneRecipe(id, layers, ""), sources: ["sdk: a base dragged on a floor, one grain per stretch"] });
|
|
3570
|
+
var DRAG_SOUNDS = [
|
|
3571
|
+
drag("drag_wood", [noise({ type: "bandpass", f: 380, q: 1.4, d: 0.28, vol: 0.5 }), noise({ type: "bandpass", f: [900, 1300], q: 3, d: 0.08, vol: 0.14 })]),
|
|
3572
|
+
drag("drag_metal", [noise({ type: "bandpass", f: 1500, q: 4, d: 0.26, vol: 0.32 }), tone({ f: [880, 1240], f2: 760, d: 0.18, vol: 0.05 })]),
|
|
3573
|
+
drag("drag_glass", [noise({ type: "bandpass", f: 2600, q: 5, d: 0.2, vol: 0.22 })]),
|
|
3574
|
+
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
|
+
drag("drag_soft", [noise({ type: "lowpass", f: 420, d: 0.28, vol: 0.3 })])
|
|
3576
|
+
];
|
|
3577
|
+
var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, EXPLOSION_BOOM], loops: [BURN_LOOP] };
|
|
3208
3578
|
|
|
3209
3579
|
// src/audio/sound.ts
|
|
3210
3580
|
import { GAME_SFX_BANK } from "@adatechnology/game-audio";
|
|
@@ -3215,6 +3585,11 @@ var DESTRUCTION_SOUND_KEYS = {
|
|
|
3215
3585
|
impact_glass: { sound: "ping" },
|
|
3216
3586
|
impact_masonry: { sound: "chip" },
|
|
3217
3587
|
impact_soft: { sound: "thud" },
|
|
3588
|
+
drag_wood: { sound: "drag_wood" },
|
|
3589
|
+
drag_metal: { sound: "drag_metal" },
|
|
3590
|
+
drag_glass: { sound: "drag_glass" },
|
|
3591
|
+
drag_masonry: { sound: "drag_masonry" },
|
|
3592
|
+
drag_soft: { sound: "drag_soft" },
|
|
3218
3593
|
shatter_glass: { sound: "glass_shatter" },
|
|
3219
3594
|
crumble: { sound: "crumble" },
|
|
3220
3595
|
splinter: { sound: "crack" },
|
|
@@ -3287,11 +3662,14 @@ export {
|
|
|
3287
3662
|
DEFAULT_FIRE,
|
|
3288
3663
|
DESTRUCTION_SOUND_BANK,
|
|
3289
3664
|
DESTRUCTION_SOUND_KEYS,
|
|
3665
|
+
DRAG_SOUNDS,
|
|
3290
3666
|
EXPLOSION_BOOM,
|
|
3291
3667
|
SCENE_SOUNDS,
|
|
3292
3668
|
createDestructionRenderer,
|
|
3293
3669
|
createDestructionSound,
|
|
3670
|
+
dragSoundKey,
|
|
3294
3671
|
heatColor,
|
|
3672
|
+
impactKey,
|
|
3295
3673
|
specFromObject3D
|
|
3296
3674
|
};
|
|
3297
3675
|
//# sourceMappingURL=index.js.map
|