@adatechnology/game-destruction-three 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2475 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/math.ts
6
+ function createRng(seed) {
7
+ let a = seed >>> 0;
8
+ return () => {
9
+ a = a + 1831565813 >>> 0;
10
+ let t = a;
11
+ t = Math.imul(t ^ t >>> 15, t | 1);
12
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
13
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
14
+ };
15
+ }
16
+ function hashNumbers(...values) {
17
+ let h = 2166136261;
18
+ for (const v of values) {
19
+ const n = Math.round(v * 1e3) | 0;
20
+ h = Math.imul(h ^ n, 16777619);
21
+ h ^= h >>> 13;
22
+ }
23
+ return h >>> 0;
24
+ }
25
+ function hashString(s) {
26
+ let h = 2166136261;
27
+ for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619);
28
+ return h >>> 0;
29
+ }
30
+ var clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
31
+ var lerp = (a, b, t) => a + (b - a) * t;
32
+ function hexToRgb(hex) {
33
+ return [(hex >> 16 & 255) / 255, (hex >> 8 & 255) / 255, (hex & 255) / 255];
34
+ }
35
+ function mixHex(a, b, t) {
36
+ const ar = a >> 16 & 255;
37
+ const ag = a >> 8 & 255;
38
+ const ab = a & 255;
39
+ const r = Math.round(ar + ((b >> 16 & 255) - ar) * t);
40
+ const g = Math.round(ag + ((b >> 8 & 255) - ag) * t);
41
+ const bl = Math.round(ab + ((b & 255) - ab) * t);
42
+ return r << 16 | g << 8 | bl;
43
+ }
44
+
45
+ // src/fragments.ts
46
+ var FragmentSystem = class {
47
+ constructor(THREE, o) {
48
+ __publicField(this, "THREE", THREE);
49
+ __publicField(this, "o", o);
50
+ __publicField(this, "list", []);
51
+ __publicField(this, "box");
52
+ __publicField(this, "shardGeos", []);
53
+ __publicField(this, "materials", /* @__PURE__ */ new Map());
54
+ __publicField(this, "q");
55
+ __publicField(this, "dq");
56
+ __publicField(this, "m");
57
+ __publicField(this, "v");
58
+ __publicField(this, "w");
59
+ __publicField(this, "rng", createRng(1374496523));
60
+ this.box = new THREE.BoxGeometry(1, 1, 1);
61
+ this.q = new THREE.Quaternion();
62
+ this.dq = new THREE.Quaternion();
63
+ this.m = new THREE.Matrix4();
64
+ this.v = new THREE.Vector3();
65
+ this.w = new THREE.Vector3();
66
+ }
67
+ get count() {
68
+ return this.list.length;
69
+ }
70
+ /** Meshes of every live fragment (tests and picking). */
71
+ meshes() {
72
+ return this.list.map((f) => f.mesh);
73
+ }
74
+ /** One shared material per source material id + colour (+ transparency). */
75
+ materialFor(materialId, color, transparent, opacity = 0.5) {
76
+ const key = `${materialId}:${color}:${transparent ? 1 : 0}`;
77
+ let m = this.materials.get(key);
78
+ if (!m) {
79
+ m = transparent ? new this.THREE.MeshStandardMaterial({ color, roughness: 0.15, metalness: 0.1, transparent: true, opacity, side: this.THREE.DoubleSide, depthWrite: false }) : new this.THREE.MeshLambertMaterial({ color });
80
+ this.materials.set(key, m);
81
+ }
82
+ return m;
83
+ }
84
+ shardGeometry(seed) {
85
+ const idx = seed % 6;
86
+ let g = this.shardGeos[idx];
87
+ if (!g) {
88
+ const r = createRng(1e3 + idx);
89
+ const n = 3 + Math.floor(r() * 3);
90
+ const shape = new this.THREE.Shape();
91
+ const pts = [];
92
+ for (let i = 0; i < n; i++) {
93
+ const a = i / n * Math.PI * 2 + (r() - 0.5) * 0.7;
94
+ const rad = 0.35 + r() * 0.15;
95
+ pts.push([Math.cos(a) * rad, Math.sin(a) * rad]);
96
+ }
97
+ shape.moveTo(pts[0][0], pts[0][1]);
98
+ for (let i = 1; i < n; i++) shape.lineTo(pts[i][0], pts[i][1]);
99
+ shape.closePath();
100
+ g = new this.THREE.ExtrudeGeometry(shape, { depth: 1, bevelEnabled: false });
101
+ g.translate(0, 0, -0.5);
102
+ this.shardGeos[idx] = g;
103
+ }
104
+ return g;
105
+ }
106
+ /** Create the mesh of a fragment and start its motion. `maxAllowed` is the live budget. */
107
+ spawn(f, material, home, maxAllowed) {
108
+ const geo = f.shape === "shard" ? this.shardGeometry(hashString(f.id)) : this.box;
109
+ const mesh = new this.THREE.Mesh(geo, material);
110
+ mesh.position.set(f.center[0], f.center[1], f.center[2]);
111
+ mesh.quaternion.set(f.rotation[0], f.rotation[1], f.rotation[2], f.rotation[3]).normalize();
112
+ const sz = f.size;
113
+ const sc = new this.THREE.Vector3(Math.max(5e-3, sz[0]), Math.max(5e-3, sz[1]), Math.max(3e-3, sz[2]));
114
+ mesh.scale.copy(sc);
115
+ mesh.castShadow = true;
116
+ this.o.scene.add(mesh);
117
+ const live = {
118
+ mesh,
119
+ home,
120
+ vel: new this.THREE.Vector3(f.velocity[0], f.velocity[1], f.velocity[2]),
121
+ ang: new this.THREE.Vector3(f.angularVelocity[0], f.angularVelocity[1], f.angularVelocity[2]),
122
+ // shards are thin: the contact extent uses the real (small) thickness, the chunk uses half of each side
123
+ half: sc.clone().multiplyScalar(0.5),
124
+ baseScale: sc,
125
+ still: 0,
126
+ asleep: false,
127
+ rest: 0,
128
+ fade: 1,
129
+ dying: false
130
+ };
131
+ this.list.push(live);
132
+ let alive = this.list.reduce((n, l) => n + (l.dying ? 0 : 1), 0);
133
+ for (let i = 0; i < this.list.length && alive > maxAllowed; i++) {
134
+ if (!this.list[i].dying) {
135
+ this.list[i].dying = true;
136
+ alive--;
137
+ }
138
+ }
139
+ }
140
+ /** Add velocity to every fragment within `radius` of a point (blasts). */
141
+ blast(point, radius, speed) {
142
+ for (const f of this.list) {
143
+ const d = this.v.copy(f.mesh.position).sub(this.w.set(point[0], point[1], point[2]));
144
+ const dist = d.length();
145
+ if (dist > radius || f.dying) continue;
146
+ const k = (1 - dist / radius) ** 2 * speed;
147
+ d.normalize().multiplyScalar(k);
148
+ d.y += k * 0.4;
149
+ f.vel.add(d);
150
+ f.ang.add(this.w.set(this.rng() - 0.5, this.rng() - 0.5, this.rng() - 0.5).multiplyScalar(k));
151
+ f.asleep = false;
152
+ f.rest = 0;
153
+ }
154
+ }
155
+ update(dt) {
156
+ const g = this.o.gravity;
157
+ for (let i = this.list.length - 1; i >= 0; i--) {
158
+ const f = this.list[i];
159
+ if (f.dying) {
160
+ f.fade -= dt / 0.6;
161
+ if (f.fade <= 0) {
162
+ this.o.scene.remove(f.mesh);
163
+ this.list.splice(i, 1);
164
+ continue;
165
+ }
166
+ f.mesh.scale.copy(f.baseScale).multiplyScalar(f.fade);
167
+ }
168
+ if (f.asleep) {
169
+ f.rest += dt;
170
+ if (this.o.life > 0 && f.rest > this.o.life) f.dying = true;
171
+ continue;
172
+ }
173
+ this.step(f, dt, g);
174
+ }
175
+ }
176
+ step(f, dt, g) {
177
+ const mesh = f.mesh;
178
+ f.vel.y -= g * dt;
179
+ mesh.position.addScaledVector(f.vel, dt);
180
+ const w = f.ang.length();
181
+ if (w > 1e-5) {
182
+ this.dq.setFromAxisAngle(this.v.copy(f.ang).multiplyScalar(1 / w), w * dt);
183
+ mesh.quaternion.premultiply(this.dq).normalize();
184
+ }
185
+ this.m.makeRotationFromQuaternion(mesh.quaternion);
186
+ const e = this.m.elements;
187
+ const h = f.half;
188
+ const sc = f.dying ? f.fade : 1;
189
+ const ext = (Math.abs(e[1]) * h.x + Math.abs(e[5]) * h.y + Math.abs(e[9]) * h.z) * sc;
190
+ const gy = this.o.groundAt(mesh.position.x, mesh.position.z);
191
+ let contact = false;
192
+ if (mesh.position.y - ext < gy) {
193
+ contact = true;
194
+ mesh.position.y = gy + ext;
195
+ if (f.vel.y < -0.6) {
196
+ f.vel.y = -f.vel.y * 0.25;
197
+ f.vel.x *= 0.7;
198
+ f.vel.z *= 0.7;
199
+ f.ang.multiplyScalar(0.55);
200
+ f.ang.x += (this.rng() - 0.5) * 2;
201
+ f.ang.z += (this.rng() - 0.5) * 2;
202
+ } else {
203
+ f.vel.y = 0;
204
+ const fr = Math.exp(-7 * dt);
205
+ f.vel.x *= fr;
206
+ f.vel.z *= fr;
207
+ f.ang.multiplyScalar(Math.exp(-6 * dt));
208
+ }
209
+ } else {
210
+ f.ang.multiplyScalar(Math.exp(-0.15 * dt));
211
+ }
212
+ if (contact && f.vel.lengthSq() < 4) this.settle(f, dt);
213
+ const slow = f.vel.lengthSq() < 0.012 && f.ang.lengthSq() < 0.2;
214
+ if (contact && slow) {
215
+ f.still += dt;
216
+ if (f.still > 0.4) {
217
+ f.asleep = true;
218
+ f.vel.set(0, 0, 0);
219
+ f.ang.set(0, 0, 0);
220
+ }
221
+ } else {
222
+ f.still = 0;
223
+ }
224
+ }
225
+ /** Tip the fragment towards lying on its flattest face. */
226
+ settle(f, dt) {
227
+ const mesh = f.mesh;
228
+ this.m.makeRotationFromQuaternion(mesh.quaternion);
229
+ const e = this.m.elements;
230
+ const up = [e[1], e[5], e[9]];
231
+ const half = [f.half.x, f.half.y, f.half.z];
232
+ let small = 0;
233
+ for (let i = 1; i < 3; i++) if (half[i] < half[small]) small = i;
234
+ let axis = small;
235
+ if (Math.abs(up[small]) < 0.35) {
236
+ axis = 0;
237
+ for (let i = 1; i < 3; i++) if (Math.abs(up[i]) > Math.abs(up[axis])) axis = i;
238
+ }
239
+ const sign = up[axis] >= 0 ? 1 : -1;
240
+ this.v.set(e[axis * 4], e[axis * 4 + 1], e[axis * 4 + 2]).multiplyScalar(sign);
241
+ this.w.set(0, 1, 0);
242
+ this.dq.setFromUnitVectors(this.v, this.w);
243
+ this.q.copy(this.dq).multiply(mesh.quaternion);
244
+ mesh.quaternion.slerp(this.q, clamp(dt * 7, 0, 1));
245
+ }
246
+ /** Remove the fragments of one object (after `restore`). */
247
+ removeHome(home) {
248
+ for (let i = this.list.length - 1; i >= 0; i--) {
249
+ if (this.list[i].home === home) {
250
+ this.o.scene.remove(this.list[i].mesh);
251
+ this.list.splice(i, 1);
252
+ }
253
+ }
254
+ }
255
+ clear() {
256
+ for (const f of this.list) this.o.scene.remove(f.mesh);
257
+ this.list.length = 0;
258
+ }
259
+ dispose() {
260
+ this.clear();
261
+ this.box.dispose();
262
+ for (const g of this.shardGeos) g?.dispose();
263
+ this.shardGeos = [];
264
+ for (const m of this.materials.values()) m.dispose();
265
+ this.materials.clear();
266
+ }
267
+ };
268
+
269
+ // src/ground.ts
270
+ var MAX_RADIUS = 1.2;
271
+ var GroundSpots = class {
272
+ constructor(THREE, scene, groundAt, cap) {
273
+ __publicField(this, "THREE", THREE);
274
+ __publicField(this, "scene", scene);
275
+ __publicField(this, "groundAt", groundAt);
276
+ __publicField(this, "cap", cap);
277
+ __publicField(this, "spots", []);
278
+ __publicField(this, "geometry");
279
+ __publicField(this, "materials", /* @__PURE__ */ new Map());
280
+ this.geometry = new THREE.CircleGeometry(1, 20);
281
+ this.geometry.rotateX(-Math.PI / 2);
282
+ }
283
+ get count() {
284
+ return this.spots.length;
285
+ }
286
+ material(kind, color) {
287
+ const key = `${kind}:${color}`;
288
+ let m = this.materials.get(key);
289
+ if (!m) {
290
+ m = new this.THREE.MeshBasicMaterial({
291
+ color,
292
+ transparent: true,
293
+ opacity: kind === "puddle" ? 0.88 : 0.95,
294
+ depthWrite: false,
295
+ polygonOffset: true,
296
+ polygonOffsetFactor: -2,
297
+ polygonOffsetUnits: -2
298
+ });
299
+ this.materials.set(key, m);
300
+ }
301
+ return m;
302
+ }
303
+ /** Add (or grow) a spot at (x, z). `radius` is the radius this addition contributes. */
304
+ add(kind, x, z, radius, color, home) {
305
+ for (const s of this.spots) {
306
+ if (s.kind !== kind) continue;
307
+ const d = Math.hypot(s.x - x, s.z - z);
308
+ if (d < Math.max(s.target, radius) * 0.9) {
309
+ s.target = Math.min(MAX_RADIUS, Math.sqrt(s.target * s.target + radius * radius * 0.5));
310
+ return;
311
+ }
312
+ }
313
+ while (this.spots.length >= this.cap) this.remove(this.spots[0]);
314
+ const mesh = new this.THREE.Mesh(this.geometry, this.material(kind, color));
315
+ const y = this.groundAt(x, z) + 4e-3 + this.spots.length % 8 * 4e-4;
316
+ mesh.position.set(x, y, z);
317
+ mesh.scale.setScalar(1e-3);
318
+ mesh.renderOrder = 1;
319
+ this.scene.add(mesh);
320
+ this.spots.push({ mesh, kind, home, x, z, radius: 1e-3, target: Math.min(MAX_RADIUS, radius) });
321
+ }
322
+ remove(s) {
323
+ const i = this.spots.indexOf(s);
324
+ if (i >= 0) this.spots.splice(i, 1);
325
+ this.scene.remove(s.mesh);
326
+ }
327
+ update(dt) {
328
+ const k = Math.min(1, dt * 3);
329
+ for (const s of this.spots) {
330
+ if (Math.abs(s.radius - s.target) < 1e-4) continue;
331
+ s.radius = lerp(s.radius, s.target, k);
332
+ s.mesh.scale.setScalar(s.radius);
333
+ }
334
+ }
335
+ /** Remove the spots made for one object (after its `restore`). */
336
+ removeHome(home) {
337
+ for (const s of [...this.spots]) if (s.home === home) this.remove(s);
338
+ }
339
+ clear() {
340
+ for (const s of [...this.spots]) this.remove(s);
341
+ }
342
+ dispose() {
343
+ this.clear();
344
+ this.geometry.dispose();
345
+ for (const m of this.materials.values()) m.dispose();
346
+ this.materials.clear();
347
+ }
348
+ };
349
+
350
+ // src/particles.ts
351
+ var KINDS = {
352
+ fire: { additive: true, life: [0.45, 0.9], size: [0.1, 0.2], grow: 0.3, gravity: 0, rise: 2.4, drag: 1.2, c0: 16760896, c1: 12591104, alpha: 0.9, fadeIn: 0.12, cone: 0.7, ground: "none", flicker: 0.35 },
353
+ smoke: { additive: false, life: [1.8, 3.2], size: [0.14, 0.24], grow: 3.2, gravity: 0, rise: 0.7, drag: 0.8, c0: 6710886, c1: 10132122, alpha: 0.35, fadeIn: 0.15, cone: 0.6, ground: "none" },
354
+ sparks: { additive: true, life: [0.25, 0.6], size: [0.018, 0.03], grow: 0.5, gravity: 1, rise: 0, drag: 0.3, c0: 16773792, c1: 16738832, alpha: 1, fadeIn: 0.02, cone: 0.55, ground: "bounce" },
355
+ dust: { additive: false, life: [1, 2], size: [0.08, 0.14], grow: 2.5, gravity: 0.05, rise: 0, drag: 2.5, c0: 9274745, c1: 9274745, alpha: 0.4, fadeIn: 0.1, cone: 0.4, ground: "none" },
356
+ shards: { additive: true, life: [0.8, 1.4], size: [0.02, 0.035], grow: 0.8, gravity: 1, rise: 0, drag: 0.2, c0: 14677247, c1: 10473727, alpha: 0.9, fadeIn: 0.03, cone: 0.5, ground: "bounce", flicker: 0.5 },
357
+ splinters: { additive: false, life: [0.8, 1.5], size: [0.02, 0.035], grow: 1, gravity: 1, rise: 0, drag: 0.3, c0: 10516042, c1: 8017200, alpha: 1, fadeIn: 0.02, cone: 0.5, ground: "bounce" },
358
+ chips: { additive: false, life: [0.8, 1.5], size: [0.025, 0.04], grow: 1, gravity: 1, rise: 0, drag: 0.3, c0: 9077624, c1: 7235679, alpha: 1, fadeIn: 0.02, cone: 0.5, ground: "bounce" },
359
+ fibers: { additive: false, life: [1.5, 2.5], size: [0.02, 0.03], grow: 1, gravity: 0.15, rise: 0, drag: 2, c0: 12101768, c1: 12101768, alpha: 0.9, fadeIn: 0.05, cone: 0.4, ground: "stop", flutter: 2.5 },
360
+ leaves: { additive: false, life: [1.5, 2.8], size: [0.03, 0.05], grow: 1, gravity: 0.15, rise: 0, drag: 2, c0: 6003260, c1: 4882224, alpha: 0.95, fadeIn: 0.05, cone: 0.4, ground: "stop", flutter: 2.8 },
361
+ drips: { additive: false, life: [2, 3], size: [0.025, 0.035], grow: 1, gravity: 1, rise: 0, drag: 0, c0: 6974058, c1: 6974058, alpha: 1, fadeIn: 0.01, cone: 0.2, ground: "land" },
362
+ ash: { additive: false, life: [2, 4], size: [0.02, 0.04], grow: 1.2, gravity: 0.08, rise: 0, drag: 1.5, c0: 2762275, c1: 1710102, alpha: 0.8, fadeIn: 0.1, cone: 0.5, ground: "stop", flutter: 0.6 },
363
+ steam: { additive: false, life: [1, 2], size: [0.12, 0.2], grow: 3, gravity: 0, rise: 0.9, drag: 1, c0: 16777215, c1: 14739694, alpha: 0.35, fadeIn: 0.15, cone: 0.6, ground: "none" },
364
+ flash: { additive: true, life: [0.12, 0.2], size: [1.2, 1.8], grow: 1.6, gravity: 0, rise: 0, drag: 0, c0: 16774080, c1: 16748592, alpha: 1, fadeIn: 0.1, cone: 0, ground: "none" },
365
+ foam: { additive: false, life: [1, 1.8], size: [0.06, 0.1], grow: 1.5, gravity: 0.6, rise: 0, drag: 1.5, c0: 16054527, c1: 14542576, alpha: 0.8, fadeIn: 0.05, cone: 0.5, ground: "stop" }
366
+ };
367
+ var KIND_IDS = Object.keys(KINDS);
368
+ var KIND_INDEX = {};
369
+ KIND_IDS.forEach((k, i) => KIND_INDEX[k] = i);
370
+ var KIND_LIST = KIND_IDS.map((k) => KINDS[k]);
371
+ var VERT = `
372
+ uniform float uScale;
373
+ attribute float size;
374
+ attribute float alpha;
375
+ attribute vec3 pcolor;
376
+ varying float vAlpha;
377
+ varying vec3 vColor;
378
+ void main() {
379
+ vAlpha = alpha;
380
+ vColor = pcolor;
381
+ vec4 mv = modelViewMatrix * vec4(position, 1.0);
382
+ gl_PointSize = min(size * uScale / max(0.1, -mv.z), 256.0);
383
+ gl_Position = projectionMatrix * mv;
384
+ }`;
385
+ var FRAG = `
386
+ varying float vAlpha;
387
+ varying vec3 vColor;
388
+ void main() {
389
+ float d = length(gl_PointCoord - vec2(0.5)) * 2.0;
390
+ float a = (1.0 - smoothstep(0.35, 1.0, d)) * vAlpha;
391
+ if (a < 0.004) discard;
392
+ gl_FragColor = vec4(vColor, a);
393
+ }`;
394
+ var Pool = class {
395
+ constructor(THREE, capacity, additive, pointScale) {
396
+ __publicField(this, "capacity", capacity);
397
+ __publicField(this, "points");
398
+ __publicField(this, "geometry");
399
+ __publicField(this, "material");
400
+ __publicField(this, "live", 0);
401
+ __publicField(this, "next", 0);
402
+ __publicField(this, "high", 0);
403
+ __publicField(this, "pos");
404
+ __publicField(this, "col");
405
+ __publicField(this, "alp");
406
+ __publicField(this, "siz");
407
+ __publicField(this, "vel");
408
+ __publicField(this, "age");
409
+ __publicField(this, "life");
410
+ __publicField(this, "size0");
411
+ __publicField(this, "alive");
412
+ __publicField(this, "kind");
413
+ __publicField(this, "seed");
414
+ __publicField(this, "alphaMul");
415
+ __publicField(this, "tag");
416
+ __publicField(this, "colorHex");
417
+ __publicField(this, "bounced");
418
+ const n = Math.max(1, capacity);
419
+ this.pos = new Float32Array(n * 3);
420
+ this.col = new Float32Array(n * 3);
421
+ this.alp = new Float32Array(n);
422
+ this.siz = new Float32Array(n);
423
+ this.vel = new Float32Array(n * 3);
424
+ this.age = new Float32Array(n);
425
+ this.life = new Float32Array(n);
426
+ this.size0 = new Float32Array(n);
427
+ this.alive = new Uint8Array(n);
428
+ this.kind = new Uint8Array(n);
429
+ this.seed = new Float32Array(n);
430
+ this.alphaMul = new Float32Array(n);
431
+ this.tag = new Int32Array(n);
432
+ this.colorHex = new Int32Array(n);
433
+ this.bounced = new Uint8Array(n);
434
+ this.geometry = new THREE.BufferGeometry();
435
+ const attr = (a, size) => {
436
+ const at = new THREE.BufferAttribute(a, size);
437
+ at.setUsage(THREE.DynamicDrawUsage);
438
+ return at;
439
+ };
440
+ this.geometry.setAttribute("position", attr(this.pos, 3));
441
+ this.geometry.setAttribute("pcolor", attr(this.col, 3));
442
+ this.geometry.setAttribute("alpha", attr(this.alp, 1));
443
+ this.geometry.setAttribute("size", attr(this.siz, 1));
444
+ this.geometry.setDrawRange(0, 0);
445
+ this.material = new THREE.ShaderMaterial({
446
+ uniforms: { uScale: { value: pointScale } },
447
+ vertexShader: VERT,
448
+ fragmentShader: FRAG,
449
+ transparent: true,
450
+ depthWrite: false,
451
+ blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending
452
+ });
453
+ this.points = new THREE.Points(this.geometry, this.material);
454
+ this.points.frustumCulled = false;
455
+ this.points.name = additive ? "destruction-particles-additive" : "destruction-particles";
456
+ this.points.renderOrder = additive ? 11 : 10;
457
+ }
458
+ spawn(kind, x, y, z, vx, vy, vz, def, rnd, o) {
459
+ if (this.capacity <= 0) return;
460
+ const i = this.next;
461
+ this.next = (this.next + 1) % this.capacity;
462
+ if (this.alive[i]) this.live--;
463
+ this.alive[i] = 1;
464
+ this.live++;
465
+ if (i + 1 > this.high) this.high = i + 1;
466
+ this.kind[i] = kind;
467
+ this.pos[i * 3] = x;
468
+ this.pos[i * 3 + 1] = y;
469
+ this.pos[i * 3 + 2] = z;
470
+ this.vel[i * 3] = vx;
471
+ this.vel[i * 3 + 1] = vy;
472
+ this.vel[i * 3 + 2] = vz;
473
+ this.age[i] = 0;
474
+ this.life[i] = lerp(def.life[0], def.life[1], rnd()) * (o?.life ?? 1);
475
+ this.size0[i] = lerp(def.size[0], def.size[1], rnd()) * (o?.size ?? 1);
476
+ this.seed[i] = rnd();
477
+ this.alphaMul[i] = o?.alpha ?? 1;
478
+ this.tag[i] = o?.tag ?? 0;
479
+ this.colorHex[i] = o?.color ?? -1;
480
+ this.bounced[i] = 0;
481
+ this.siz[i] = this.size0[i];
482
+ this.alp[i] = 0;
483
+ }
484
+ kill(i) {
485
+ if (!this.alive[i]) return;
486
+ this.alive[i] = 0;
487
+ this.live--;
488
+ this.alp[i] = 0;
489
+ this.siz[i] = 0;
490
+ }
491
+ clear() {
492
+ this.alive.fill(0);
493
+ this.alp.fill(0);
494
+ this.siz.fill(0);
495
+ this.live = 0;
496
+ this.high = 0;
497
+ this.next = 0;
498
+ this.geometry.setDrawRange(0, 0);
499
+ }
500
+ /** Move a live particle (used by external pushes such as blasts). */
501
+ push(cx, cy, cz, radius, speed) {
502
+ for (let i = 0; i < this.high; i++) {
503
+ if (!this.alive[i]) continue;
504
+ const dx = this.pos[i * 3] - cx;
505
+ const dy = this.pos[i * 3 + 1] - cy;
506
+ const dz = this.pos[i * 3 + 2] - cz;
507
+ const d = Math.hypot(dx, dy, dz);
508
+ if (d > radius || d < 1e-4) continue;
509
+ const f = (1 - d / radius) * speed;
510
+ this.vel[i * 3] += dx / d * f;
511
+ this.vel[i * 3 + 1] += dy / d * f;
512
+ this.vel[i * 3 + 2] += dz / d * f;
513
+ }
514
+ }
515
+ update(dt, host) {
516
+ let top = 0;
517
+ for (let i = 0; i < this.high; i++) {
518
+ if (!this.alive[i]) continue;
519
+ const def = KIND_LIST[this.kind[i]];
520
+ const age = this.age[i] += dt;
521
+ const life = this.life[i];
522
+ if (age >= life) {
523
+ this.kill(i);
524
+ continue;
525
+ }
526
+ const t = age / life;
527
+ const i3 = i * 3;
528
+ let vx = this.vel[i3];
529
+ let vy = this.vel[i3 + 1];
530
+ let vz = this.vel[i3 + 2];
531
+ vy += (def.rise - def.gravity * host.gravity) * dt;
532
+ if (def.flutter) {
533
+ const ph = age * 7 + this.seed[i] * 40;
534
+ vx += Math.sin(ph) * def.flutter * dt;
535
+ vz += Math.cos(ph * 1.3) * def.flutter * dt;
536
+ }
537
+ if (def.drag > 0) {
538
+ const k = Math.exp(-def.drag * dt);
539
+ vx *= k;
540
+ vy *= k;
541
+ vz *= k;
542
+ }
543
+ const x = this.pos[i3] + vx * dt;
544
+ let y = this.pos[i3 + 1] + vy * dt;
545
+ const z = this.pos[i3 + 2] + vz * dt;
546
+ if (def.ground !== "none") {
547
+ const gy = host.groundAt(x, z) + 5e-3;
548
+ if (y < gy) {
549
+ if (def.ground === "land") {
550
+ const tag = this.tag[i];
551
+ if (tag !== 0) host.onLand(x, gy, z, this.colorHex[i] >= 0 ? this.colorHex[i] : def.c0, tag);
552
+ this.kill(i);
553
+ continue;
554
+ }
555
+ y = gy;
556
+ if (def.ground === "bounce" && !this.bounced[i]) {
557
+ this.bounced[i] = 1;
558
+ vy = Math.abs(vy) * 0.35;
559
+ vx *= 0.6;
560
+ vz *= 0.6;
561
+ } else {
562
+ vx = vy = vz = 0;
563
+ }
564
+ }
565
+ }
566
+ this.vel[i3] = vx;
567
+ this.vel[i3 + 1] = vy;
568
+ this.vel[i3 + 2] = vz;
569
+ this.pos[i3] = x;
570
+ this.pos[i3 + 1] = y;
571
+ this.pos[i3 + 2] = z;
572
+ this.siz[i] = this.size0[i] * lerp(1, def.grow, t);
573
+ const c0 = this.colorHex[i] >= 0 ? this.colorHex[i] : def.c0;
574
+ const c1 = this.colorHex[i] >= 0 ? this.colorHex[i] : def.c1;
575
+ const a = hexToRgb(c0);
576
+ const b = hexToRgb(c1);
577
+ this.col[i3] = lerp(a[0], b[0], t);
578
+ this.col[i3 + 1] = lerp(a[1], b[1], t);
579
+ this.col[i3 + 2] = lerp(a[2], b[2], t);
580
+ let env = t < def.fadeIn ? t / def.fadeIn : 1 - (t - def.fadeIn) / (1 - def.fadeIn);
581
+ if (def.flicker) env *= 1 - def.flicker * (0.5 + 0.5 * Math.sin(age * 38 + this.seed[i] * 50));
582
+ this.alp[i] = clamp(env, 0, 1) * def.alpha * this.alphaMul[i];
583
+ top = i + 1;
584
+ }
585
+ this.high = top;
586
+ this.geometry.setDrawRange(0, top);
587
+ for (const name of ["position", "pcolor", "alpha", "size"]) {
588
+ ;
589
+ this.geometry.getAttribute(name).needsUpdate = true;
590
+ }
591
+ }
592
+ dispose() {
593
+ this.geometry.dispose();
594
+ this.material.dispose();
595
+ }
596
+ };
597
+ var ParticleSystem = class {
598
+ constructor(THREE, o) {
599
+ __publicField(this, "o", o);
600
+ __publicField(this, "add");
601
+ __publicField(this, "normal");
602
+ __publicField(this, "rng", createRng(2654435761));
603
+ __publicField(this, "attached", false);
604
+ __publicField(this, "host");
605
+ const addCap = Math.floor(o.capacity * 0.4);
606
+ this.add = new Pool(THREE, addCap, true, o.pointScale);
607
+ this.normal = new Pool(THREE, o.capacity - addCap, false, o.pointScale);
608
+ this.host = { groundAt: o.groundAt, gravity: o.gravity, onLand: o.onLand };
609
+ }
610
+ get capacity() {
611
+ return this.add.capacity + this.normal.capacity;
612
+ }
613
+ get live() {
614
+ return this.add.live + this.normal.live;
615
+ }
616
+ ensureAttached() {
617
+ if (this.attached) return;
618
+ this.attached = true;
619
+ this.o.scene.add(this.add.points, this.normal.points);
620
+ }
621
+ /** Remove the Points from the scene (they come back on the next emit). */
622
+ detach() {
623
+ if (!this.attached) return;
624
+ this.attached = false;
625
+ this.o.scene.remove(this.add.points, this.normal.points);
626
+ }
627
+ /** Spawn one particle with an explicit velocity. */
628
+ spawn(kind, p, v, opts) {
629
+ const def = KINDS[kind];
630
+ this.ensureAttached();
631
+ (def.additive ? this.add : this.normal).spawn(KIND_INDEX[kind], p[0], p[1], p[2], v[0], v[1], v[2], def, this.rng, opts);
632
+ }
633
+ /** Spawn `count` particles leaving `point` around `normal` at about `speed` m/s. */
634
+ burst(kind, point, normal, count, speed = 1, opts) {
635
+ const def = KINDS[kind];
636
+ const n = Math.max(0, Math.round(count * this.o.density));
637
+ const rnd = this.rng;
638
+ const nl = Math.hypot(normal[0], normal[1], normal[2]) || 1;
639
+ const nx = normal[0] / nl;
640
+ const ny = normal[1] / nl;
641
+ const nz = normal[2] / nl;
642
+ for (let k = 0; k < n; k++) {
643
+ const u = rnd() * 2 - 1;
644
+ const ph = rnd() * Math.PI * 2;
645
+ const s = Math.sqrt(1 - u * u);
646
+ const rx = s * Math.cos(ph);
647
+ const ry = u;
648
+ const rz = s * Math.sin(ph);
649
+ let dx = nx * def.cone + rx * (1 - def.cone);
650
+ let dy = ny * def.cone + ry * (1 - def.cone);
651
+ let dz = nz * def.cone + rz * (1 - def.cone);
652
+ dx += rx * 0.35;
653
+ dy += ry * 0.35;
654
+ dz += rz * 0.35;
655
+ const l = Math.hypot(dx, dy, dz) || 1;
656
+ const sp = speed * (0.35 + 0.65 * rnd());
657
+ const jitter = 0.01 + (def.size[1] > 0.5 ? 0.15 : 0.02);
658
+ this.spawn(
659
+ kind,
660
+ [point[0] + (rnd() - 0.5) * jitter, point[1] + (rnd() - 0.5) * jitter, point[2] + (rnd() - 0.5) * jitter],
661
+ [dx / l * sp, dy / l * sp, dz / l * sp],
662
+ opts
663
+ );
664
+ }
665
+ }
666
+ /** Blast wave: shove live particles away from a point. */
667
+ push(point, radius, speed) {
668
+ this.add.push(point[0], point[1], point[2], radius, speed);
669
+ this.normal.push(point[0], point[1], point[2], radius, speed);
670
+ }
671
+ update(dt) {
672
+ if (this.add.live > 0 || this.add.points.geometry.drawRange.count > 0) this.add.update(dt, this.host);
673
+ if (this.normal.live > 0 || this.normal.points.geometry.drawRange.count > 0) this.normal.update(dt, this.host);
674
+ }
675
+ clear() {
676
+ this.add.clear();
677
+ this.normal.clear();
678
+ this.detach();
679
+ }
680
+ dispose() {
681
+ this.detach();
682
+ this.add.dispose();
683
+ this.normal.dispose();
684
+ }
685
+ };
686
+
687
+ // src/pieces.ts
688
+ var SUB_DT = 1 / 120;
689
+ var MU = 0.7;
690
+ var SLEEP_AFTER = 0.5;
691
+ var HARD_LANDING = 3;
692
+ var SLOP = 2e-3;
693
+ var PieceSystem = class {
694
+ constructor(THREE, o) {
695
+ __publicField(this, "THREE", THREE);
696
+ __publicField(this, "o", o);
697
+ __publicField(this, "list", []);
698
+ __publicField(this, "time", 0);
699
+ __publicField(this, "p");
700
+ __publicField(this, "q");
701
+ __publicField(this, "dq");
702
+ __publicField(this, "m");
703
+ __publicField(this, "ax");
704
+ this.p = new THREE.Vector3();
705
+ this.q = new THREE.Quaternion();
706
+ this.dq = new THREE.Quaternion();
707
+ this.m = new THREE.Matrix4();
708
+ this.ax = new THREE.Vector3();
709
+ }
710
+ get(objectId) {
711
+ return this.list.find((b) => b.objectId === objectId) ?? null;
712
+ }
713
+ create(objectId, root, halfExtents, localCenter, mass, velocity, angular) {
714
+ const existing = this.get(objectId);
715
+ if (existing) {
716
+ existing.root = root;
717
+ existing.halfExtents = halfExtents;
718
+ existing.mass = mass;
719
+ this.setCenter(existing, localCenter);
720
+ if (velocity) existing.velocity = [...velocity];
721
+ existing.sleeping = false;
722
+ return existing;
723
+ }
724
+ const body = {
725
+ objectId,
726
+ root,
727
+ velocity: velocity ? [...velocity] : [0, 0, 0],
728
+ angularVelocity: angular ? [...angular] : [0, 0, 0],
729
+ halfExtents: [Math.max(0.01, halfExtents[0]), Math.max(0.01, halfExtents[1]), Math.max(0.01, halfExtents[2])],
730
+ mass: Math.max(0.05, mass),
731
+ sleeping: false,
732
+ held: false,
733
+ localCenter: [localCenter.x, localCenter.y, localCenter.z],
734
+ centerV: localCenter.clone(),
735
+ still: 0,
736
+ lastHit: -1,
737
+ onGround: false
738
+ };
739
+ this.list.push(body);
740
+ return body;
741
+ }
742
+ /** Move the centre of mass (after a part broke off). */
743
+ setCenter(b, c) {
744
+ b.centerV.copy(c);
745
+ b.localCenter = [c.x, c.y, c.z];
746
+ }
747
+ remove(objectId) {
748
+ const i = this.list.findIndex((b) => b.objectId === objectId);
749
+ if (i >= 0) this.list.splice(i, 1);
750
+ }
751
+ clear() {
752
+ this.list.length = 0;
753
+ }
754
+ /** World position of the centre of mass. */
755
+ centerOf(b, out) {
756
+ return out.copy(b.centerV).applyQuaternion(b.root.quaternion).add(b.root.position);
757
+ }
758
+ impulse(b, impulse, point) {
759
+ const inv = 1 / b.mass;
760
+ b.velocity[0] += impulse[0] * inv;
761
+ b.velocity[1] += impulse[1] * inv;
762
+ b.velocity[2] += impulse[2] * inv;
763
+ if (point) {
764
+ const c = this.centerOf(b, this.p);
765
+ const rx = point[0] - c.x;
766
+ const ry = point[1] - c.y;
767
+ const rz = point[2] - c.z;
768
+ const tx = ry * impulse[2] - rz * impulse[1];
769
+ const ty = rz * impulse[0] - rx * impulse[2];
770
+ const tz = rx * impulse[1] - ry * impulse[0];
771
+ const w = this.mulInvInertia(b, tx, ty, tz);
772
+ b.angularVelocity[0] += w[0];
773
+ b.angularVelocity[1] += w[1];
774
+ b.angularVelocity[2] += w[2];
775
+ }
776
+ b.sleeping = false;
777
+ b.still = 0;
778
+ }
779
+ /** 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). */
780
+ mulInvInertia(b, x, y, z) {
781
+ this.m.makeRotationFromQuaternion(b.root.quaternion);
782
+ return this.mulInvInertiaWith(b, this.m.elements, x, y, z);
783
+ }
784
+ mulInvInertiaWith(b, e, x, y, z) {
785
+ const [hx, hy, hz] = b.halfExtents;
786
+ const k = 3 / b.mass;
787
+ const ix = k / (hy * hy + hz * hz);
788
+ const iy = k / (hx * hx + hz * hz);
789
+ const iz = k / (hx * hx + hy * hy);
790
+ const lx = (e[0] * x + e[1] * y + e[2] * z) * ix;
791
+ const ly = (e[4] * x + e[5] * y + e[6] * z) * iy;
792
+ const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
793
+ 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];
794
+ }
795
+ update(dt) {
796
+ this.time += dt;
797
+ for (const b of this.list) {
798
+ if (b.held) {
799
+ b.velocity = [0, 0, 0];
800
+ b.angularVelocity = [0, 0, 0];
801
+ b.sleeping = false;
802
+ b.still = 0;
803
+ continue;
804
+ }
805
+ if (b.sleeping) continue;
806
+ this.integrate(b, dt);
807
+ }
808
+ this.separate(dt);
809
+ }
810
+ integrate(b, dt) {
811
+ const c = this.centerOf(b, this.p);
812
+ const pos = [c.x, c.y, c.z];
813
+ const q = this.q.copy(b.root.quaternion);
814
+ const v = b.velocity;
815
+ const w = b.angularVelocity;
816
+ const [hx, hy, hz] = b.halfExtents;
817
+ const invM = 1 / b.mass;
818
+ const steps = clamp(Math.ceil(dt / SUB_DT), 1, 8);
819
+ const h = dt / steps;
820
+ let landing = 0;
821
+ let landPoint = [pos[0], pos[1], pos[2]];
822
+ let contactedLast = false;
823
+ let touched = false;
824
+ for (let s = 0; s < steps; s++) {
825
+ v[1] -= this.o.gravity * h;
826
+ pos[0] += v[0] * h;
827
+ pos[1] += v[1] * h;
828
+ pos[2] += v[2] * h;
829
+ const wl = Math.hypot(w[0], w[1], w[2]);
830
+ if (wl > 1e-6) {
831
+ this.dq.setFromAxisAngle(this.ax.set(w[0] / wl, w[1] / wl, w[2] / wl), wl * h);
832
+ q.premultiply(this.dq).normalize();
833
+ }
834
+ const damp = Math.exp(-0.15 * h);
835
+ w[0] *= damp;
836
+ w[1] *= damp;
837
+ w[2] *= damp;
838
+ this.m.makeRotationFromQuaternion(q);
839
+ const e = this.m.elements;
840
+ const rs = [];
841
+ let maxPen = 0;
842
+ for (let i = 0; i < 8; i++) {
843
+ const sx = i & 1 ? hx : -hx;
844
+ const sy = i & 2 ? hy : -hy;
845
+ const sz = i & 4 ? hz : -hz;
846
+ 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];
847
+ const gy = this.o.groundAt(pos[0] + r[0], pos[2] + r[2]);
848
+ const pen = gy - (pos[1] + r[1]);
849
+ if (pen > -SLOP) {
850
+ rs.push(r);
851
+ if (pen > maxPen) maxPen = pen;
852
+ }
853
+ }
854
+ contactedLast = rs.length > 0;
855
+ if (contactedLast) touched = true;
856
+ if (!contactedLast) continue;
857
+ pos[1] += Math.max(0, maxPen - SLOP * 0.5);
858
+ let impact = 0;
859
+ for (const r of rs) impact = Math.max(impact, -(v[1] + w[2] * r[0] - w[0] * r[2]));
860
+ const restitution = impact > 2 ? 0.25 : 0;
861
+ if (impact > landing) {
862
+ landing = impact;
863
+ let sx = 0;
864
+ let sz = 0;
865
+ for (const r of rs) {
866
+ sx += r[0];
867
+ sz += r[2];
868
+ }
869
+ const lx = pos[0] + sx / rs.length;
870
+ const lz = pos[2] + sz / rs.length;
871
+ landPoint = [lx, this.o.groundAt(lx, lz), lz];
872
+ }
873
+ for (let it = 0; it < 4; it++) {
874
+ for (const r of rs) {
875
+ let vx = v[0] + (w[1] * r[2] - w[2] * r[1]);
876
+ let vy = v[1] + (w[2] * r[0] - w[0] * r[2]);
877
+ let vz = v[2] + (w[0] * r[1] - w[1] * r[0]);
878
+ const vn = vy;
879
+ if (vn >= 0) continue;
880
+ const a = this.mulInvInertiaWith(b, e, -r[2], 0, r[0]);
881
+ const denom = invM + (a[2] * r[0] - a[0] * r[2]);
882
+ const j = -(1 + (it === 0 ? restitution : 0)) * vn / denom;
883
+ v[1] += j * invM;
884
+ const dw = this.mulInvInertiaWith(b, e, -r[2] * j, 0, r[0] * j);
885
+ w[0] += dw[0];
886
+ w[1] += dw[1];
887
+ w[2] += dw[2];
888
+ vx = v[0] + (w[1] * r[2] - w[2] * r[1]);
889
+ vy = v[1] + (w[2] * r[0] - w[0] * r[2]);
890
+ vz = v[2] + (w[0] * r[1] - w[1] * r[0]);
891
+ const vt = Math.hypot(vx, vz);
892
+ if (vt > 1e-5) {
893
+ const tx = vx / vt;
894
+ const tz = vz / vt;
895
+ const rx = r[1] * tz;
896
+ const ry = r[2] * tx - r[0] * tz;
897
+ const rz = -r[1] * tx;
898
+ const at = this.mulInvInertiaWith(b, e, rx, ry, rz);
899
+ const cx = at[1] * r[2] - at[2] * r[1];
900
+ const cz = at[0] * r[1] - at[1] * r[0];
901
+ const denomT = invM + cx * tx + cz * tz;
902
+ const jt = Math.min(vt / denomT, MU * j);
903
+ v[0] -= tx * jt * invM;
904
+ v[2] -= tz * jt * invM;
905
+ const dwt = this.mulInvInertiaWith(b, e, rx * jt, ry * jt, rz * jt);
906
+ w[0] -= dwt[0];
907
+ w[1] -= dwt[1];
908
+ w[2] -= dwt[2];
909
+ }
910
+ }
911
+ }
912
+ const rd = Math.exp((rs.length >= 3 ? -3 : -1.5) * h);
913
+ w[0] *= rd;
914
+ w[1] *= rd;
915
+ w[2] *= rd;
916
+ const fd = Math.exp(-0.3 * h);
917
+ v[0] *= fd;
918
+ v[2] *= fd;
919
+ }
920
+ b.root.quaternion.copy(q);
921
+ const off = this.ax.copy(b.centerV).applyQuaternion(q);
922
+ b.root.position.set(pos[0] - off.x, pos[1] - off.y, pos[2] - off.z);
923
+ b.root.updateMatrixWorld(true);
924
+ b.onGround = contactedLast;
925
+ if (landing > HARD_LANDING && this.time - b.lastHit > 0.15) {
926
+ b.lastHit = this.time;
927
+ this.o.onHardLanding(b, landPoint, landing);
928
+ }
929
+ const still = touched && Math.hypot(v[0], v[1], v[2]) < 0.12 && Math.hypot(w[0], w[1], w[2]) < 0.45;
930
+ if (still) {
931
+ b.still += dt;
932
+ if (b.still >= SLEEP_AFTER) {
933
+ b.sleeping = true;
934
+ v[0] = v[1] = v[2] = 0;
935
+ w[0] = w[1] = w[2] = 0;
936
+ }
937
+ } else {
938
+ b.still = 0;
939
+ }
940
+ }
941
+ /** Push overlapping pieces apart (horizontal bounding circles), gently, so they do not interpenetrate forever. */
942
+ separate(dt) {
943
+ const n = this.list.length;
944
+ if (n < 2) return;
945
+ const maxMove = 1.5 * dt;
946
+ const ca = new this.THREE.Vector3();
947
+ const cb = new this.THREE.Vector3();
948
+ for (let i = 0; i < n; i++) {
949
+ const a = this.list[i];
950
+ for (let j = i + 1; j < n; j++) {
951
+ const b = this.list[j];
952
+ if (a.held && b.held) continue;
953
+ if (a.sleeping && b.sleeping) continue;
954
+ const ra = Math.hypot(a.halfExtents[0], a.halfExtents[1], a.halfExtents[2]);
955
+ const rb = Math.hypot(b.halfExtents[0], b.halfExtents[1], b.halfExtents[2]);
956
+ this.centerOf(a, ca);
957
+ this.centerOf(b, cb);
958
+ if (Math.abs(ca.y - cb.y) > ra + rb) continue;
959
+ const rh = (Math.max(a.halfExtents[0], a.halfExtents[2]) + Math.max(b.halfExtents[0], b.halfExtents[2])) * 0.8;
960
+ let dx = cb.x - ca.x;
961
+ let dz = cb.z - ca.z;
962
+ let d = Math.hypot(dx, dz);
963
+ if (d >= rh) continue;
964
+ if (d < 1e-5) {
965
+ dx = 1;
966
+ dz = 0;
967
+ d = 1;
968
+ }
969
+ const push = Math.min(maxMove, (rh - d) * 0.5);
970
+ const wa = a.held ? 0 : b.held ? 1 : b.mass / (a.mass + b.mass);
971
+ const wb = b.held ? 0 : a.held ? 1 : a.mass / (a.mass + b.mass);
972
+ const ux = dx / (d || 1);
973
+ const uz = dz / (d || 1);
974
+ if (!a.held) {
975
+ a.root.position.x -= ux * push * 2 * wa;
976
+ a.root.position.z -= uz * push * 2 * wa;
977
+ a.sleeping = false;
978
+ }
979
+ if (!b.held) {
980
+ b.root.position.x += ux * push * 2 * wb;
981
+ b.root.position.z += uz * push * 2 * wb;
982
+ b.sleeping = false;
983
+ }
984
+ }
985
+ }
986
+ }
987
+ };
988
+
989
+ // src/heat.ts
990
+ var STOPS = [
991
+ [450, 0],
992
+ [500, 2753536],
993
+ [650, 9049344],
994
+ [800, 14174728],
995
+ [950, 16747540],
996
+ [1100, 16761402],
997
+ [1250, 16771232],
998
+ [1400, 16777215]
999
+ ];
1000
+ function heatColor(temperatureC) {
1001
+ if (!(temperatureC > STOPS[0][0])) return 0;
1002
+ const last = STOPS[STOPS.length - 1];
1003
+ if (temperatureC >= last[0]) return last[1];
1004
+ for (let i = 1; i < STOPS.length; i++) {
1005
+ const [t1, c1] = STOPS[i];
1006
+ if (temperatureC <= t1) {
1007
+ const [t0, c0] = STOPS[i - 1];
1008
+ return mixHex(c0, c1, (temperatureC - t0) / (t1 - t0));
1009
+ }
1010
+ }
1011
+ return last[1];
1012
+ }
1013
+
1014
+ // src/visual.ts
1015
+ var POS_ATTR = "position";
1016
+ function positionAttr(geo) {
1017
+ const a = geo.getAttribute(POS_ATTR);
1018
+ return a && !a.isInterleavedBufferAttribute ? a : null;
1019
+ }
1020
+ function materialsOf(m) {
1021
+ return Array.isArray(m) ? m : [m];
1022
+ }
1023
+ function colorOf(m) {
1024
+ const c = m.color;
1025
+ return c && c.isColor ? c : null;
1026
+ }
1027
+ var DecalKit = class {
1028
+ constructor(THREE) {
1029
+ __publicField(this, "THREE", THREE);
1030
+ __publicField(this, "disc", null);
1031
+ __publicField(this, "discMat", null);
1032
+ __publicField(this, "lineMats", /* @__PURE__ */ new Map());
1033
+ }
1034
+ discGeometry() {
1035
+ return this.disc ?? (this.disc = new this.THREE.CircleGeometry(1, 14));
1036
+ }
1037
+ discMaterial() {
1038
+ return this.discMat ?? (this.discMat = new this.THREE.MeshBasicMaterial({
1039
+ color: 328965,
1040
+ transparent: true,
1041
+ opacity: 0.92,
1042
+ depthWrite: false,
1043
+ polygonOffset: true,
1044
+ polygonOffsetFactor: -4,
1045
+ polygonOffsetUnits: -4
1046
+ }));
1047
+ }
1048
+ lineMaterial(color) {
1049
+ let m = this.lineMats.get(color);
1050
+ if (!m) {
1051
+ m = new this.THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.9, depthWrite: false });
1052
+ this.lineMats.set(color, m);
1053
+ }
1054
+ return m;
1055
+ }
1056
+ dispose() {
1057
+ this.disc?.dispose();
1058
+ this.discMat?.dispose();
1059
+ for (const m of this.lineMats.values()) m.dispose();
1060
+ this.disc = null;
1061
+ this.discMat = null;
1062
+ this.lineMats.clear();
1063
+ }
1064
+ };
1065
+ var tmpV = (THREE) => new THREE.Vector3();
1066
+ var Visuals = class {
1067
+ constructor(THREE, lowQuality, decalCap) {
1068
+ __publicField(this, "THREE", THREE);
1069
+ __publicField(this, "lowQuality", lowQuality);
1070
+ __publicField(this, "decalCap", decalCap);
1071
+ __publicField(this, "kit");
1072
+ __publicField(this, "a");
1073
+ __publicField(this, "b");
1074
+ __publicField(this, "m4");
1075
+ __publicField(this, "box");
1076
+ /** Total live decals across meshes (kept in sync by add/remove). */
1077
+ __publicField(this, "decalCount", 0);
1078
+ this.kit = new DecalKit(THREE);
1079
+ this.a = tmpV(THREE);
1080
+ this.b = tmpV(THREE);
1081
+ this.m4 = new THREE.Matrix4();
1082
+ this.box = new THREE.Box3();
1083
+ }
1084
+ makeMeshVis(mesh, home) {
1085
+ return {
1086
+ mesh,
1087
+ home,
1088
+ origGeometry: mesh.geometry,
1089
+ origMaterial: mesh.material,
1090
+ origVisible: mesh.visible,
1091
+ origParent: mesh.parent,
1092
+ origPosition: mesh.position.clone(),
1093
+ origQuaternion: mesh.quaternion.clone(),
1094
+ origScale: mesh.scale.clone(),
1095
+ ownMat: false,
1096
+ ownGeo: false,
1097
+ work: null,
1098
+ baseColors: null,
1099
+ decals: [],
1100
+ meltApplied: 0,
1101
+ bottomY: 0,
1102
+ moved: false
1103
+ };
1104
+ }
1105
+ vertexCount(mv) {
1106
+ return mv.mesh.geometry.getAttribute(POS_ATTR)?.count ?? 0;
1107
+ }
1108
+ /** Colour of the original material (before any char), `0xRRGGBB`, or -1. */
1109
+ baseColorHex(mv) {
1110
+ const c = mv.baseColors?.[0] ?? colorOf(materialsOf(mv.origMaterial)[0]);
1111
+ return c ? c.getHex() : -1;
1112
+ }
1113
+ /** Is the original material transparent? */
1114
+ isTransparent(mv) {
1115
+ return materialsOf(mv.origMaterial).some((m) => m.transparent && m.opacity < 1);
1116
+ }
1117
+ ownMaterial(mv) {
1118
+ if (mv.ownMat) return;
1119
+ mv.ownMat = true;
1120
+ const src = materialsOf(mv.origMaterial);
1121
+ const cloned = src.map((m) => m.clone());
1122
+ mv.baseColors = src.map((m) => colorOf(m)?.clone() ?? new this.THREE.Color(8947848));
1123
+ mv.mesh.material = Array.isArray(mv.origMaterial) ? cloned : cloned[0];
1124
+ }
1125
+ /** Clone the geometry (vertex edits); false when its positions cannot be edited. */
1126
+ ownGeometry(mv) {
1127
+ if (mv.ownGeo) return true;
1128
+ const pos = positionAttr(mv.mesh.geometry);
1129
+ if (!pos) return false;
1130
+ mv.ownGeo = true;
1131
+ const g = mv.mesh.geometry.clone();
1132
+ mv.mesh.geometry = g;
1133
+ mv.work = new Float32Array(positionAttr(g).array);
1134
+ return true;
1135
+ }
1136
+ // ------------------------------------------------------------------------------------------------------------------------------------
1137
+ // Dents
1138
+ // ------------------------------------------------------------------------------------------------------------------------------------
1139
+ /**
1140
+ * Push the vertices of the mesh within `dent.radius` of `worldPoint` inwards along `worldNormal` by `depth * (1 - (d/r)^2)^2`.
1141
+ * Returns false when nothing was changed (too far, low quality on a heavy mesh, or no editable positions).
1142
+ */
1143
+ applyDent(mv, worldPoint, worldNormal, dent) {
1144
+ const mesh = mv.mesh;
1145
+ if (this.lowQuality && this.vertexCount(mv) > 2e4) return false;
1146
+ mesh.updateWorldMatrix(true, false);
1147
+ const s = mesh.matrixWorld.getMaxScaleOnAxis() || 1;
1148
+ const lp = mesh.worldToLocal(this.a.copy(worldPoint));
1149
+ const ln = this.b.copy(worldNormal).transformDirection(this.m4.copy(mesh.matrixWorld).invert()).normalize();
1150
+ const r = dent.radius / s;
1151
+ const depth = dent.depth / s;
1152
+ const geo = mesh.geometry;
1153
+ if (!geo.boundingBox) geo.computeBoundingBox();
1154
+ if (geo.boundingBox && geo.boundingBox.distanceToPoint(lp) > r) return false;
1155
+ if (!this.ownGeometry(mv)) return false;
1156
+ const work = mv.work;
1157
+ const pos = positionAttr(mesh.geometry);
1158
+ const r2 = r * r;
1159
+ let touched = false;
1160
+ const px = lp.x;
1161
+ const py = lp.y;
1162
+ const pz = lp.z;
1163
+ const nx = ln.x;
1164
+ const ny = ln.y;
1165
+ const nz = ln.z;
1166
+ for (let i = 0; i < work.length; i += 3) {
1167
+ const dx = work[i] - px;
1168
+ const dy = work[i + 1] - py;
1169
+ const dz = work[i + 2] - pz;
1170
+ const d2 = dx * dx + dy * dy + dz * dz;
1171
+ if (d2 >= r2) continue;
1172
+ const f = 1 - d2 / r2;
1173
+ const k = depth * f * f;
1174
+ work[i] -= nx * k;
1175
+ work[i + 1] -= ny * k;
1176
+ work[i + 2] -= nz * k;
1177
+ touched = true;
1178
+ }
1179
+ if (!touched) return false;
1180
+ this.writePositions(mv, pos);
1181
+ return true;
1182
+ }
1183
+ /** Copy `work` (dents) into the geometry, adding the melt sag when the mesh is melting. */
1184
+ writePositions(mv, pos) {
1185
+ const work = mv.work;
1186
+ const arr = pos.array;
1187
+ const melt = mv.meltApplied;
1188
+ if (melt <= 0 || this.lowQuality && work.length / 3 > 2e4) {
1189
+ arr.set(work);
1190
+ } else {
1191
+ const geo2 = mv.mesh.geometry;
1192
+ if (!geo2.boundingBox) geo2.computeBoundingBox();
1193
+ const bb = geo2.boundingBox;
1194
+ const h = Math.max(1e-4, bb.max.y - bb.min.y);
1195
+ const minY = bb.min.y;
1196
+ for (let i = 0, v = 0; i < work.length; i += 3, v++) {
1197
+ const n = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
1198
+ const n2 = (Math.imul(v + 7, 40503) >>> 0) % 1e3 / 1e3;
1199
+ const lowness = 1 - (work[i + 1] - minY) / h;
1200
+ const spread = 1 + 0.12 * melt * (0.4 + n2) * (0.3 + lowness);
1201
+ arr[i] = work[i] * spread;
1202
+ arr[i + 2] = work[i + 2] * spread;
1203
+ arr[i + 1] = Math.max(minY, work[i + 1] - n * melt * h * 0.08);
1204
+ }
1205
+ }
1206
+ pos.needsUpdate = true;
1207
+ const geo = mv.mesh.geometry;
1208
+ geo.computeVertexNormals();
1209
+ geo.boundingBox = null;
1210
+ geo.boundingSphere = null;
1211
+ }
1212
+ // ------------------------------------------------------------------------------------------------------------------------------------
1213
+ // Decals
1214
+ // ------------------------------------------------------------------------------------------------------------------------------------
1215
+ /** Decal pose in mesh space: position and a rotation taking +Z to the surface normal. */
1216
+ decalPose(mesh, worldPoint, worldNormal, lift) {
1217
+ mesh.updateWorldMatrix(true, false);
1218
+ const s = mesh.matrixWorld.getMaxScaleOnAxis() || 1;
1219
+ const ln = worldNormal.clone().transformDirection(this.m4.copy(mesh.matrixWorld).invert()).normalize();
1220
+ const pos = mesh.worldToLocal(worldPoint.clone()).addScaledVector(ln, lift / s);
1221
+ const quat = new this.THREE.Quaternion().setFromUnitVectors(new this.THREE.Vector3(0, 0, 1), ln);
1222
+ return { s, pos, quat };
1223
+ }
1224
+ addDecal(mv, decal) {
1225
+ mv.mesh.add(decal.obj);
1226
+ mv.decals.push(decal);
1227
+ this.decalCount++;
1228
+ while (mv.decals.length > 48 || this.decalCount > this.decalCap) {
1229
+ const old = mv.decals.shift();
1230
+ if (!old) break;
1231
+ this.disposeDecal(old);
1232
+ }
1233
+ }
1234
+ disposeDecal(d) {
1235
+ d.obj.parent?.remove(d.obj);
1236
+ d.geometry?.dispose();
1237
+ this.decalCount = Math.max(0, this.decalCount - 1);
1238
+ }
1239
+ addHole(mv, worldPoint, worldNormal, diameter) {
1240
+ const { s, pos, quat } = this.decalPose(mv.mesh, worldPoint, worldNormal, 25e-4);
1241
+ const disc = new this.THREE.Mesh(this.kit.discGeometry(), this.kit.discMaterial());
1242
+ disc.position.copy(pos);
1243
+ disc.quaternion.copy(quat);
1244
+ disc.scale.setScalar(Math.max(4e-3, diameter) * 0.5 * 1.25 / s);
1245
+ disc.renderOrder = 2;
1246
+ this.addDecal(mv, { obj: disc });
1247
+ }
1248
+ /** A star of jagged lines: `rays` rays up to `length` metres, seeded so the same event always draws the same star. */
1249
+ addCrack(mv, worldPoint, worldNormal, rays, length, color, seed) {
1250
+ const { s, pos, quat } = this.decalPose(mv.mesh, worldPoint, worldNormal, 2e-3);
1251
+ const rnd = createRng(seed);
1252
+ const verts = [];
1253
+ const n = clamp(Math.round(rays), 3, 16);
1254
+ const L = Math.max(0.02, length) / s;
1255
+ for (let r = 0; r < n; r++) {
1256
+ let ang = r / n * Math.PI * 2 + (rnd() - 0.5) * 0.5;
1257
+ let x = 0;
1258
+ let y = 0;
1259
+ const segs = 3;
1260
+ const total = L * (0.6 + 0.4 * rnd());
1261
+ for (let k = 0; k < segs; k++) {
1262
+ const step = total / segs * (0.7 + 0.6 * rnd());
1263
+ ang += (rnd() - 0.5) * 0.45;
1264
+ const nx = x + Math.cos(ang) * step;
1265
+ const ny = y + Math.sin(ang) * step;
1266
+ verts.push(x, y, 0, nx, ny, 0);
1267
+ if (k > 0 && rnd() < 0.5) {
1268
+ const ba = ang + (rnd() < 0.5 ? -1 : 1) * (0.5 + rnd() * 0.5);
1269
+ const bl = step * (0.6 + 0.6 * rnd());
1270
+ verts.push(nx, ny, 0, nx + Math.cos(ba) * bl, ny + Math.sin(ba) * bl, 0);
1271
+ }
1272
+ x = nx;
1273
+ y = ny;
1274
+ }
1275
+ }
1276
+ const ringR = L * 0.12;
1277
+ for (let k = 0; k < 8; k++) {
1278
+ const a0 = k / 8 * Math.PI * 2;
1279
+ const a1 = (k + 1) / 8 * Math.PI * 2;
1280
+ if (rnd() < 0.75) verts.push(Math.cos(a0) * ringR, Math.sin(a0) * ringR, 0, Math.cos(a1) * ringR, Math.sin(a1) * ringR, 0);
1281
+ }
1282
+ const geo = new this.THREE.BufferGeometry();
1283
+ geo.setAttribute("position", new this.THREE.Float32BufferAttribute(verts, 3));
1284
+ const lines = new this.THREE.LineSegments(geo, this.kit.lineMaterial(color));
1285
+ lines.position.copy(pos);
1286
+ lines.quaternion.copy(quat);
1287
+ lines.renderOrder = 2;
1288
+ this.addDecal(mv, { obj: lines, geometry: geo });
1289
+ }
1290
+ // ------------------------------------------------------------------------------------------------------------------------------------
1291
+ // Char and heat glow
1292
+ // ------------------------------------------------------------------------------------------------------------------------------------
1293
+ setChar(pv, char, charColor) {
1294
+ if (Math.abs(char - pv.charApplied) < 4e-3) return;
1295
+ pv.charApplied = char;
1296
+ const target = new this.THREE.Color(charColor);
1297
+ for (const mv of pv.meshes) {
1298
+ this.ownMaterial(mv);
1299
+ const mats = materialsOf(mv.mesh.material);
1300
+ mats.forEach((m, i) => {
1301
+ const c = colorOf(m);
1302
+ const base = mv.baseColors?.[i];
1303
+ if (c && base) c.copy(base).lerp(target, clamp(char, 0, 1));
1304
+ });
1305
+ }
1306
+ }
1307
+ /** Emissive glow of a hot part. `temperature` below the glow threshold switches it off. */
1308
+ setGlow(pv, temperatureC) {
1309
+ const hex = heatColor(temperatureC);
1310
+ const key = hex === 0 ? 0 : Math.round(temperatureC / 5) * 5;
1311
+ if (key === pv.glowApplied) return;
1312
+ pv.glowApplied = key;
1313
+ const intensity = hex === 0 ? 0 : clamp(0.35 + (temperatureC - 450) / 900, 0, 1.4);
1314
+ for (const mv of pv.meshes) {
1315
+ this.ownMaterial(mv);
1316
+ for (const m of materialsOf(mv.mesh.material)) {
1317
+ const e = m.emissive;
1318
+ if (e && e.isColor) {
1319
+ e.setHex(hex);
1320
+ m.emissiveIntensity = intensity;
1321
+ }
1322
+ }
1323
+ }
1324
+ }
1325
+ // ------------------------------------------------------------------------------------------------------------------------------------
1326
+ // Melt
1327
+ // ------------------------------------------------------------------------------------------------------------------------------------
1328
+ /** Sag the part: shorter in Y (down to 25 %), a bit wider, vertices pushed down with noise, the bottom stays put. */
1329
+ setMelt(pv, melt) {
1330
+ const m = clamp(melt, 0, 1);
1331
+ if (Math.abs(m - pv.meltApplied) < 0.01) return;
1332
+ pv.meltApplied = m;
1333
+ for (const mv of pv.meshes) {
1334
+ const mesh = mv.mesh;
1335
+ if (mv.meltApplied === 0 && m > 0) {
1336
+ mesh.updateWorldMatrix(true, false);
1337
+ mv.bottomY = this.box.setFromObject(mesh).min.y;
1338
+ }
1339
+ mv.meltApplied = m;
1340
+ mv.moved = true;
1341
+ const sy = lerp(1, 0.25, m);
1342
+ const sxz = 1 + 0.18 * m;
1343
+ mesh.scale.set(mv.origScale.x * sxz, mv.origScale.y * sy, mv.origScale.z * sxz);
1344
+ mesh.position.copy(mv.origPosition);
1345
+ mesh.updateWorldMatrix(true, false);
1346
+ const bottom = this.box.setFromObject(mesh).min.y;
1347
+ const dy = mv.bottomY - bottom;
1348
+ if (Math.abs(dy) > 1e-6 && mesh.parent) {
1349
+ mesh.parent.updateWorldMatrix(true, false);
1350
+ const inv = this.m4.copy(mesh.parent.matrixWorld).invert();
1351
+ this.a.set(0, dy, 0).applyMatrix4(inv);
1352
+ this.b.set(0, 0, 0).applyMatrix4(inv);
1353
+ mesh.position.copy(mv.origPosition).add(this.a.sub(this.b));
1354
+ }
1355
+ if (!(this.lowQuality && this.vertexCount(mv) > 2e4) && this.ownGeometry(mv)) {
1356
+ this.writePositions(mv, positionAttr(mesh.geometry));
1357
+ }
1358
+ }
1359
+ }
1360
+ // ------------------------------------------------------------------------------------------------------------------------------------
1361
+ // Visibility and restore
1362
+ // ------------------------------------------------------------------------------------------------------------------------------------
1363
+ hide(pv, reason) {
1364
+ pv.gone = reason;
1365
+ for (const mv of pv.meshes) mv.mesh.visible = false;
1366
+ }
1367
+ /** Bring one mesh back to how it was when it was bound. */
1368
+ restoreMesh(mv) {
1369
+ const mesh = mv.mesh;
1370
+ for (const d of mv.decals) this.disposeDecal(d);
1371
+ mv.decals.length = 0;
1372
+ if (mv.ownGeo) {
1373
+ mesh.geometry.dispose();
1374
+ mesh.geometry = mv.origGeometry;
1375
+ mv.ownGeo = false;
1376
+ mv.work = null;
1377
+ }
1378
+ if (mv.ownMat) {
1379
+ for (const m of materialsOf(mesh.material)) m.dispose();
1380
+ mesh.material = mv.origMaterial;
1381
+ mv.ownMat = false;
1382
+ mv.baseColors = null;
1383
+ }
1384
+ mesh.visible = mv.origVisible;
1385
+ if (mv.origParent && mesh.parent !== mv.origParent) {
1386
+ mv.origParent.add(mesh);
1387
+ mv.moved = true;
1388
+ }
1389
+ if (mv.moved) {
1390
+ mesh.position.copy(mv.origPosition);
1391
+ mesh.quaternion.copy(mv.origQuaternion);
1392
+ mesh.scale.copy(mv.origScale);
1393
+ mv.moved = false;
1394
+ }
1395
+ mv.meltApplied = 0;
1396
+ }
1397
+ restorePart(pv) {
1398
+ for (const mv of pv.meshes) this.restoreMesh(mv);
1399
+ pv.charApplied = 0;
1400
+ pv.glowApplied = 0;
1401
+ pv.meltApplied = 0;
1402
+ pv.gone = "none";
1403
+ }
1404
+ dispose() {
1405
+ this.kit.dispose();
1406
+ }
1407
+ };
1408
+
1409
+ // src/renderer.ts
1410
+ var GLOW_FROM_C = 430;
1411
+ var QUEUE_CAP = 4e3;
1412
+ var FLUID_COLORS = {
1413
+ water: 9226495,
1414
+ fuel: 14262322,
1415
+ oil: 2760728,
1416
+ gas: 15921906,
1417
+ sap: 12098362,
1418
+ acid: 10211900,
1419
+ blood: 9046538
1420
+ };
1421
+ var SMOKE_TONES = { white: 15132390, grey: 9079434, black: 2763306 };
1422
+ var DEFAULT_SPEED = {
1423
+ sparks: 3,
1424
+ splinters: 2.5,
1425
+ dust: 1,
1426
+ shards: 2.5,
1427
+ fibers: 1.2,
1428
+ leaves: 1.2,
1429
+ drips: 0.5,
1430
+ ash: 0.8,
1431
+ chips: 2.5,
1432
+ smoke: 0.8,
1433
+ steam: 1,
1434
+ fire: 1.2,
1435
+ flash: 0,
1436
+ foam: 2.5
1437
+ };
1438
+ function impactKey(def) {
1439
+ if (!def) return "impact_wood";
1440
+ const tags = def.tags ?? [];
1441
+ if (tags.includes("metal")) return "impact_metal";
1442
+ if (def.visual?.transparent || def.fracture === "shatter") return "impact_glass";
1443
+ if (def.fracture === "crumble") return "impact_masonry";
1444
+ if (def.fracture === "tear") return "impact_soft";
1445
+ return def.fracture === "none" ? "impact_metal" : "impact_wood";
1446
+ }
1447
+ function fractureKey(def, style) {
1448
+ switch (style) {
1449
+ case "shatter":
1450
+ return "shatter_glass";
1451
+ case "crumble":
1452
+ return "crumble";
1453
+ case "splinter":
1454
+ return "splinter";
1455
+ default:
1456
+ return impactKey(def);
1457
+ }
1458
+ }
1459
+ function fallbackColor(id, def) {
1460
+ if (def?.tags?.includes("metal")) return 9080726;
1461
+ if (def?.visual?.transparent) return 10473696;
1462
+ const h = hashString(id);
1463
+ return mixHex(7035464, 10130568, h % 100 / 100);
1464
+ }
1465
+ function materialsOf2(m) {
1466
+ return Array.isArray(m) ? m : [m];
1467
+ }
1468
+ function createDestructionRenderer(THREE, options) {
1469
+ return new Renderer(THREE, options);
1470
+ }
1471
+ var Renderer = class {
1472
+ constructor(THREE, o) {
1473
+ __publicField(this, "THREE", THREE);
1474
+ __publicField(this, "o", o);
1475
+ __publicField(this, "world");
1476
+ __publicField(this, "scene");
1477
+ __publicField(this, "quality");
1478
+ __publicField(this, "low");
1479
+ __publicField(this, "groundAt");
1480
+ __publicField(this, "gravity");
1481
+ __publicField(this, "autoSplit");
1482
+ __publicField(this, "maxFragments");
1483
+ __publicField(this, "bounds", /* @__PURE__ */ new Map());
1484
+ __publicField(this, "queue", []);
1485
+ __publicField(this, "unsubscribe");
1486
+ __publicField(this, "visuals");
1487
+ __publicField(this, "particles");
1488
+ __publicField(this, "fragments");
1489
+ __publicField(this, "ground");
1490
+ __publicField(this, "pieceSys");
1491
+ __publicField(this, "active", /* @__PURE__ */ new Map());
1492
+ __publicField(this, "fires", /* @__PURE__ */ new Map());
1493
+ __publicField(this, "leaks", []);
1494
+ __publicField(this, "homeTags", []);
1495
+ __publicField(this, "throttles", /* @__PURE__ */ new Map());
1496
+ __publicField(this, "rng", createRng(12648430));
1497
+ __publicField(this, "light", null);
1498
+ __publicField(this, "lightT", 0);
1499
+ __publicField(this, "lightDur", 0.3);
1500
+ __publicField(this, "lightPeak", 0);
1501
+ __publicField(this, "time", 0);
1502
+ __publicField(this, "scanTimer", 0);
1503
+ __publicField(this, "disposed", false);
1504
+ __publicField(this, "box");
1505
+ __publicField(this, "tmpQ");
1506
+ __publicField(this, "v1");
1507
+ __publicField(this, "v2");
1508
+ this.world = o.world;
1509
+ this.scene = o.scene;
1510
+ this.quality = o.quality ?? "medium";
1511
+ this.low = this.quality === "low";
1512
+ this.groundAt = o.groundAt ?? (() => 0);
1513
+ this.gravity = o.gravity ?? 9.81;
1514
+ this.autoSplit = o.autoSplit !== false;
1515
+ this.maxFragments = o.maxFragments ?? 160;
1516
+ this.box = new THREE.Box3();
1517
+ this.tmpQ = new THREE.Quaternion();
1518
+ this.v1 = new THREE.Vector3();
1519
+ this.v2 = new THREE.Vector3();
1520
+ const maxParticles = Math.max(8, Math.floor((o.maxParticles ?? 2500) * (this.low ? 0.5 : 1)));
1521
+ this.visuals = new Visuals(THREE, this.low, 600);
1522
+ this.ground = new GroundSpots(THREE, o.scene, this.groundAt, 48);
1523
+ this.particles = new ParticleSystem(THREE, {
1524
+ capacity: maxParticles,
1525
+ scene: o.scene,
1526
+ density: this.low ? 0.6 : 1,
1527
+ pointScale: o.pointScale ?? 700,
1528
+ groundAt: this.groundAt,
1529
+ gravity: this.gravity,
1530
+ onLand: (x, _y, z, color, tag) => {
1531
+ const home = this.homeTags[tag - 1] ?? "";
1532
+ this.ground.add("puddle", x, z, 0.05 + this.rng() * 0.04, color, home);
1533
+ }
1534
+ });
1535
+ this.fragments = new FragmentSystem(THREE, {
1536
+ scene: o.scene,
1537
+ groundAt: this.groundAt,
1538
+ gravity: this.gravity,
1539
+ max: this.maxFragments,
1540
+ life: o.fragmentLife ?? 12
1541
+ });
1542
+ this.pieceSys = new PieceSystem(THREE, {
1543
+ groundAt: this.groundAt,
1544
+ gravity: this.gravity,
1545
+ scene: o.scene,
1546
+ onHardLanding: (p, point, speed) => this.hardLanding(p, point, speed)
1547
+ });
1548
+ this.unsubscribe = this.world.on("*", (ev) => {
1549
+ if (this.queue.length < QUEUE_CAP) this.queue.push(ev);
1550
+ });
1551
+ }
1552
+ // ----------------------------------------------------------------------------------------------------------------------------------
1553
+ // Binding
1554
+ // ----------------------------------------------------------------------------------------------------------------------------------
1555
+ bind(objectId, root, options = {}) {
1556
+ if (this.bounds.has(objectId)) this.unbind(objectId);
1557
+ const state = this.world.state(objectId);
1558
+ const partIds = new Set((state?.parts ?? []).map((p) => p.id));
1559
+ const byPart = /* @__PURE__ */ new Map();
1560
+ const taken = /* @__PURE__ */ new Set();
1561
+ const add = (pid, m) => {
1562
+ if (taken.has(m)) return;
1563
+ taken.add(m);
1564
+ const l = byPart.get(pid);
1565
+ if (l) l.push(m);
1566
+ else byPart.set(pid, [m]);
1567
+ };
1568
+ const meshesIn = (o) => {
1569
+ const out = [];
1570
+ o.traverse((c) => {
1571
+ if (c.isMesh) out.push(c);
1572
+ });
1573
+ return out;
1574
+ };
1575
+ if (options.parts) {
1576
+ for (const [pid, v] of Object.entries(options.parts)) {
1577
+ for (const o of Array.isArray(v) ? v : [v]) for (const m of meshesIn(o)) add(pid, m);
1578
+ }
1579
+ } else {
1580
+ const leftovers = [];
1581
+ for (const m of meshesIn(root)) {
1582
+ const ud = m.userData?.destructionPart;
1583
+ if (typeof ud === "string" && partIds.has(ud)) add(ud, m);
1584
+ else if (partIds.has(m.name)) add(m.name, m);
1585
+ else leftovers.push(m);
1586
+ }
1587
+ if (leftovers.length) this.matchByBox(objectId, root, leftovers, add);
1588
+ }
1589
+ const bound = { id: objectId, root, sync: options.syncTransform !== false, parts: /* @__PURE__ */ new Map(), lastPos: null, lastQuat: null, piece: null, isSplit: false };
1590
+ for (const pid of partIds) bound.parts.set(pid, this.makePartVis(objectId, pid, state?.parts.find((p) => p.id === pid)?.material ?? "", byPart.get(pid) ?? []));
1591
+ for (const [pid, meshes] of byPart) {
1592
+ if (!bound.parts.has(pid)) bound.parts.set(pid, this.makePartVis(objectId, pid, this.world.part(objectId, pid)?.material ?? "", meshes));
1593
+ }
1594
+ this.bounds.set(objectId, bound);
1595
+ }
1596
+ makePartVis(home, id, material, meshes) {
1597
+ return { id, home, material, meshes: meshes.map((m) => this.visuals.makeMeshVis(m, home)), charApplied: 0, glowApplied: 0, meltApplied: 0, gone: "none" };
1598
+ }
1599
+ /** Match unlabelled meshes to the part whose shape contains the centre of the mesh's bounding box. */
1600
+ matchByBox(objectId, root, meshes, add) {
1601
+ let specParts;
1602
+ try {
1603
+ specParts = this.world.serialize?.().objects.find((o) => o.id === objectId)?.spec.parts;
1604
+ } catch {
1605
+ specParts = void 0;
1606
+ }
1607
+ if (!specParts) return;
1608
+ root.updateMatrixWorld(true);
1609
+ const e = new this.THREE.Euler();
1610
+ const q = new this.THREE.Quaternion();
1611
+ for (const m of meshes) {
1612
+ this.box.setFromObject(m);
1613
+ if (this.box.isEmpty()) continue;
1614
+ const c = root.worldToLocal(this.box.getCenter(this.v1));
1615
+ let best = null;
1616
+ let bestVol = Infinity;
1617
+ for (const p of specParts) {
1618
+ const l = this.v2.set(c.x - p.position[0], c.y - p.position[1], c.z - p.position[2]);
1619
+ if (p.rotation) l.applyQuaternion(q.setFromEuler(e.set(p.rotation[0], p.rotation[1], p.rotation[2], "XYZ")).invert());
1620
+ let inside = false;
1621
+ let vol = 0;
1622
+ const s = p.shape;
1623
+ if (s.kind === "box") {
1624
+ inside = Math.abs(l.x) <= s.size[0] / 2 && Math.abs(l.y) <= s.size[1] / 2 && Math.abs(l.z) <= s.size[2] / 2;
1625
+ vol = s.size[0] * s.size[1] * s.size[2];
1626
+ } else if (s.kind === "cylinder") {
1627
+ inside = Math.hypot(l.x, l.z) <= s.radius && Math.abs(l.y) <= s.height / 2;
1628
+ vol = Math.PI * s.radius * s.radius * s.height;
1629
+ } else {
1630
+ inside = l.length() <= s.radius;
1631
+ vol = 4 / 3 * Math.PI * s.radius ** 3;
1632
+ }
1633
+ if (inside && vol < bestVol) {
1634
+ best = p;
1635
+ bestVol = vol;
1636
+ }
1637
+ }
1638
+ if (best) add(best.id, m);
1639
+ }
1640
+ }
1641
+ unbind(objectId) {
1642
+ const b = this.bounds.get(objectId);
1643
+ if (!b) return;
1644
+ this.restore(objectId);
1645
+ this.pieceSys.remove(objectId);
1646
+ this.bounds.delete(objectId);
1647
+ }
1648
+ // ----------------------------------------------------------------------------------------------------------------------------------
1649
+ // Pieces
1650
+ // ----------------------------------------------------------------------------------------------------------------------------------
1651
+ /** Half extents and centre of the box around some meshes, in the local space of `root`. */
1652
+ measure(root, meshes) {
1653
+ root.updateMatrixWorld(true);
1654
+ const inv = root.matrixWorld.clone().invert();
1655
+ const total = new this.THREE.Box3();
1656
+ const m4 = new this.THREE.Matrix4();
1657
+ for (const m of meshes) {
1658
+ const g = m.geometry;
1659
+ if (!g.boundingBox) g.computeBoundingBox();
1660
+ if (!g.boundingBox || g.boundingBox.isEmpty()) continue;
1661
+ total.union(g.boundingBox.clone().applyMatrix4(m4.multiplyMatrices(inv, m.matrixWorld)));
1662
+ }
1663
+ if (total.isEmpty()) return { half: [0.25, 0.25, 0.25], center: new this.THREE.Vector3() };
1664
+ const size = total.getSize(new this.THREE.Vector3());
1665
+ return { half: [Math.max(0.02, size.x / 2), Math.max(0.02, size.y / 2), Math.max(0.02, size.z / 2)], center: total.getCenter(new this.THREE.Vector3()) };
1666
+ }
1667
+ meshesOf(b) {
1668
+ const out = [];
1669
+ for (const pv of b.parts.values()) if (pv.gone === "none") for (const mv of pv.meshes) out.push(mv.mesh);
1670
+ return out;
1671
+ }
1672
+ massOf(objectId) {
1673
+ const st = this.world.state(objectId);
1674
+ if (!st) return 5;
1675
+ let m = 0;
1676
+ for (const p of st.parts) if (p.status !== "broken" && p.status !== "melted" && p.status !== "ashes") m += p.mass;
1677
+ return m > 0 ? m : 5;
1678
+ }
1679
+ makePiece(objectId, options = {}) {
1680
+ const b = this.bounds.get(objectId);
1681
+ if (!b) throw new Error(`game-destruction-three: object "${objectId}" is not bound`);
1682
+ if (b.root.parent && b.root.parent !== this.scene) this.scene.attach(b.root);
1683
+ const { half, center } = this.measure(b.root, this.meshesOf(b));
1684
+ const piece = this.pieceSys.create(objectId, b.root, half, center, options.mass ?? this.massOf(objectId), options.velocity);
1685
+ b.piece = piece;
1686
+ return piece;
1687
+ }
1688
+ pieces() {
1689
+ return [...this.pieceSys.list];
1690
+ }
1691
+ piece(objectId) {
1692
+ return this.pieceSys.get(objectId);
1693
+ }
1694
+ impulse(objectId, impulse, point) {
1695
+ const p = this.pieceSys.get(objectId);
1696
+ if (p) this.pieceSys.impulse(p, impulse, point);
1697
+ }
1698
+ hardLanding(p, point, speed) {
1699
+ const key = this.impactSoundOf(p.objectId);
1700
+ this.sound(key, point, clamp(speed / 10, 0.2, 1));
1701
+ this.syncBound(p.objectId);
1702
+ try {
1703
+ this.world.hit(p.objectId, { type: "blunt", point, energy: 0.5 * p.mass * speed * speed, radius: 0.08, direction: [0, -1, 0], source: "fall" });
1704
+ } catch {
1705
+ }
1706
+ }
1707
+ impactSoundOf(objectId) {
1708
+ const st = this.world.state(objectId);
1709
+ let best;
1710
+ let mass = -1;
1711
+ for (const p of st?.parts ?? []) {
1712
+ if (p.mass > mass) {
1713
+ mass = p.mass;
1714
+ best = this.world.materials.get(p.material);
1715
+ }
1716
+ }
1717
+ return impactKey(best);
1718
+ }
1719
+ // ----------------------------------------------------------------------------------------------------------------------------------
1720
+ // Update
1721
+ // ----------------------------------------------------------------------------------------------------------------------------------
1722
+ update(dt) {
1723
+ if (this.disposed) return;
1724
+ dt = clamp(dt, 0, 0.1);
1725
+ this.time += dt;
1726
+ this.drainQueue();
1727
+ this.updateActive(dt);
1728
+ this.updateLeaks(dt);
1729
+ this.pieceSys.update(dt);
1730
+ this.fragments.update(dt);
1731
+ this.ground.update(dt);
1732
+ this.updateLight(dt);
1733
+ this.particles.update(dt);
1734
+ this.syncTransforms();
1735
+ }
1736
+ drainQueue() {
1737
+ const batch = this.queue.splice(0, this.queue.length);
1738
+ for (const ev of batch) {
1739
+ try {
1740
+ this.apply(ev);
1741
+ } catch (err) {
1742
+ if (typeof console !== "undefined") console.error("game-destruction-three: failed to apply", ev.type, err);
1743
+ }
1744
+ }
1745
+ }
1746
+ syncTransforms() {
1747
+ for (const b of this.bounds.values()) this.syncOne(b);
1748
+ }
1749
+ /** Push the current transform of one bound object to the world right now (used before a landing hit). */
1750
+ syncBound(objectId) {
1751
+ const b = this.bounds.get(objectId);
1752
+ if (b) this.syncOne(b);
1753
+ }
1754
+ syncOne(b) {
1755
+ if (!b.sync) return;
1756
+ const p = this.v1;
1757
+ const q = this.tmpQ;
1758
+ b.root.updateWorldMatrix(true, false);
1759
+ b.root.matrixWorld.decompose(p, q, this.v2);
1760
+ const last = b.lastPos;
1761
+ const lq = b.lastQuat;
1762
+ const moved = !last || !lq || Math.abs(p.x - last[0]) + Math.abs(p.y - last[1]) + Math.abs(p.z - last[2]) > 1e-5 || Math.abs(q.x - lq[0]) + Math.abs(q.y - lq[1]) + Math.abs(q.z - lq[2]) + Math.abs(q.w - lq[3]) > 1e-6;
1763
+ if (!moved) return;
1764
+ b.lastPos = [p.x, p.y, p.z];
1765
+ b.lastQuat = [q.x, q.y, q.z, q.w];
1766
+ if (this.world.has(b.id)) this.world.setTransform(b.id, [p.x, p.y, p.z], [q.x, q.y, q.z, q.w]);
1767
+ }
1768
+ // ----------------------------------------------------------------------------------------------------------------------------------
1769
+ // Events
1770
+ // ----------------------------------------------------------------------------------------------------------------------------------
1771
+ apply(ev) {
1772
+ switch (ev.type) {
1773
+ case "damage":
1774
+ if (ev.damageType === "heat" || ev.damageType === "fire" || ev.damageType === "explosive") this.activate(ev.object, ev.part);
1775
+ break;
1776
+ case "dent":
1777
+ this.onDent(ev);
1778
+ break;
1779
+ case "hole":
1780
+ this.onHole(ev);
1781
+ break;
1782
+ case "crack":
1783
+ this.onCrack(ev);
1784
+ break;
1785
+ case "ricochet": {
1786
+ const d = ev.direction;
1787
+ this.particles.burst("sparks", ev.point, d, 4 + Math.min(10, ev.energy / 150), 4);
1788
+ this.sound("ricochet", ev.point, 0.7);
1789
+ break;
1790
+ }
1791
+ case "debris":
1792
+ this.particles.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));
1793
+ break;
1794
+ case "fracture":
1795
+ this.onFracture(ev);
1796
+ break;
1797
+ case "detach":
1798
+ this.onDetach(ev);
1799
+ break;
1800
+ case "disassemble_progress": {
1801
+ const k = `unscrew:${ev.joint}`;
1802
+ if (this.time - (this.throttles.get(k) ?? -1) > 0.25) {
1803
+ this.throttles.set(k, this.time);
1804
+ this.sound("unscrew", this.rootPoint(ev.object), clamp(0.3 + ev.progress * 0.5, 0, 1));
1805
+ }
1806
+ break;
1807
+ }
1808
+ case "ignite":
1809
+ this.fires.set(`${ev.object}|${ev.part}`, { object: ev.object, part: ev.part, point: ev.point, accFire: 0, accSmoke: 0 });
1810
+ this.activate(ev.object, ev.part);
1811
+ this.particles.burst("fire", ev.point, [0, 1, 0], 6, 1);
1812
+ this.sound("ignite", ev.point, 0.7);
1813
+ this.sound("burn_loop", ev.point, 0.5);
1814
+ break;
1815
+ case "extinguish": {
1816
+ const f = this.fires.get(`${ev.object}|${ev.part}`);
1817
+ if (f) {
1818
+ this.fires.delete(`${ev.object}|${ev.part}`);
1819
+ this.particles.burst("steam", f.point, [0, 1, 0], 14, 1.2);
1820
+ }
1821
+ this.activate(ev.object, ev.part);
1822
+ this.sound("extinguish", f?.point ?? this.rootPoint(ev.object), 0.6);
1823
+ break;
1824
+ }
1825
+ case "burnout": {
1826
+ this.fires.delete(`${ev.object}|${ev.part}`);
1827
+ this.particles.burst("ash", ev.point, [0, 1, 0], 16, 1);
1828
+ this.particles.burst("smoke", ev.point, [0, 1, 0], 6, 0.8, { color: SMOKE_TONES.grey });
1829
+ const pv = this.partVis(ev.object, ev.part);
1830
+ if (pv) {
1831
+ const def = this.world.materials.get(pv.material);
1832
+ this.visuals.setChar(pv, 1, def?.visual?.charColor ?? 1709072);
1833
+ }
1834
+ this.activate(ev.object, ev.part);
1835
+ break;
1836
+ }
1837
+ case "melt":
1838
+ this.activate(ev.object, ev.part);
1839
+ if (this.time - (this.throttles.get(`melt:${ev.object}|${ev.part}`) ?? -9) > 1.5) {
1840
+ this.throttles.set(`melt:${ev.object}|${ev.part}`, this.time);
1841
+ this.sound("melt_sizzle", this.rootPoint(ev.object), 0.4);
1842
+ }
1843
+ break;
1844
+ case "drip": {
1845
+ const b = this.bounds.get(ev.object);
1846
+ const pv = b?.parts.get(ev.part);
1847
+ const def = this.world.materials.get(ev.material);
1848
+ const color = (pv && this.partColor(pv, def)) ?? fallbackColor(ev.material, def);
1849
+ let tag = this.homeTags.indexOf(ev.object) + 1;
1850
+ if (tag === 0) tag = this.homeTags.push(ev.object);
1851
+ this.particles.spawn("drips", ev.point, [(this.rng() - 0.5) * 0.1, -0.1, (this.rng() - 0.5) * 0.1], { color, tag });
1852
+ break;
1853
+ }
1854
+ case "leak": {
1855
+ const key = `${ev.object}|${ev.part}`;
1856
+ const same = this.leaks.find((l) => l.object === ev.object && l.part === ev.part && Math.hypot(l.point[0] - ev.point[0], l.point[1] - ev.point[1], l.point[2] - ev.point[2]) < 0.03);
1857
+ if (same) {
1858
+ same.rate = ev.rate;
1859
+ same.direction = ev.direction;
1860
+ } else {
1861
+ if (this.leaks.length >= 24) this.leaks.shift();
1862
+ this.leaks.push({ object: ev.object, part: ev.part, fluid: ev.fluid, point: ev.point, direction: ev.direction, rate: ev.rate, pressurized: ev.pressurized, acc: 0 });
1863
+ }
1864
+ void key;
1865
+ break;
1866
+ }
1867
+ case "explode":
1868
+ this.onExplode(ev);
1869
+ break;
1870
+ case "destroyed":
1871
+ break;
1872
+ }
1873
+ }
1874
+ rootPoint(objectId) {
1875
+ const b = this.bounds.get(objectId);
1876
+ return b ? [b.root.position.x, b.root.position.y, b.root.position.z] : [0, 0, 0];
1877
+ }
1878
+ sound(key, point, volume) {
1879
+ this.o.onSound?.(key, point, volume);
1880
+ }
1881
+ partVis(objectId, partId) {
1882
+ return this.bounds.get(objectId)?.parts.get(partId) ?? null;
1883
+ }
1884
+ activate(objectId, partId) {
1885
+ this.active.set(`${objectId}|${partId}`, { object: objectId, part: partId });
1886
+ }
1887
+ /** Current colour of the part's first mesh (char included), or null. */
1888
+ partColor(pv, def) {
1889
+ const mv = pv.meshes[0];
1890
+ if (!mv) return null;
1891
+ const mat = materialsOf2(mv.mesh.material)[0];
1892
+ const c = mat?.color;
1893
+ if (c && c.isColor) return c.getHex();
1894
+ return def ? fallbackColor(def.id, def) : null;
1895
+ }
1896
+ debrisOptions(kind, objectId, partId) {
1897
+ const pv = this.partVis(objectId, partId);
1898
+ const def = pv ? this.world.materials.get(pv.material) : void 0;
1899
+ if (kind === "smoke") return { color: SMOKE_TONES[def?.thermal.smoke ?? "grey"] };
1900
+ if (kind === "fibers" || kind === "leaves") {
1901
+ const c = pv ? this.visuals.baseColorHex(pv.meshes[0] ?? {}) : -1;
1902
+ return c >= 0 ? { color: c } : void 0;
1903
+ }
1904
+ if (kind === "dust" || kind === "chips") {
1905
+ const c = pv && pv.meshes[0] ? this.visuals.baseColorHex(pv.meshes[0]) : -1;
1906
+ return c >= 0 && kind === "chips" ? { color: c } : void 0;
1907
+ }
1908
+ return void 0;
1909
+ }
1910
+ /** Mesh of the part closest to a world point. */
1911
+ nearestMesh(pv, p) {
1912
+ let best = null;
1913
+ let bd = Infinity;
1914
+ for (const mv of pv.meshes) {
1915
+ if (!mv.mesh.visible) continue;
1916
+ const d = this.box.setFromObject(mv.mesh).distanceToPoint(p);
1917
+ if (d < bd) {
1918
+ bd = d;
1919
+ best = mv;
1920
+ }
1921
+ }
1922
+ return best;
1923
+ }
1924
+ worldDir(b, v) {
1925
+ b.root.updateWorldMatrix(true, false);
1926
+ return new this.THREE.Vector3(v[0], v[1], v[2]).applyQuaternion(b.root.getWorldQuaternion(this.tmpQ)).normalize();
1927
+ }
1928
+ onDent(ev) {
1929
+ const b = this.bounds.get(ev.object);
1930
+ const pv = b?.parts.get(ev.part);
1931
+ if (!b || !pv) return;
1932
+ const def = this.world.materials.get(pv.material);
1933
+ const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
1934
+ const wn = this.worldDir(b, ev.dent.normal);
1935
+ for (const mv of pv.meshes) {
1936
+ if (!mv.mesh.visible) continue;
1937
+ this.visuals.applyDent(mv, wp, wn, ev.dent);
1938
+ }
1939
+ if (def?.tags?.includes("metal")) this.particles.burst("sparks", ev.worldPoint, [wn.x, wn.y, wn.z], 4 + Math.min(8, ev.dent.depth * 150), 3.2);
1940
+ this.sound(impactKey(def), ev.worldPoint, clamp(0.3 + ev.dent.depth * 12, 0.3, 1));
1941
+ }
1942
+ onHole(ev) {
1943
+ const b = this.bounds.get(ev.object);
1944
+ const pv = b?.parts.get(ev.part);
1945
+ if (!b || !pv) return;
1946
+ const def = this.world.materials.get(pv.material);
1947
+ const n = this.worldDir(b, ev.hole.normal);
1948
+ const draw = (point, normal) => {
1949
+ const wp = new this.THREE.Vector3(point[0], point[1], point[2]);
1950
+ const mv = this.nearestMesh(pv, wp);
1951
+ if (!mv) return;
1952
+ if (def?.visual?.transparent) {
1953
+ const seed = hashNumbers(ev.time, point[0], point[1], point[2]);
1954
+ this.visuals.addCrack(mv, wp, normal, 7, Math.max(0.05, ev.hole.diameter * 5), 15267071, seed);
1955
+ } else {
1956
+ this.visuals.addHole(mv, wp, normal, ev.hole.diameter);
1957
+ }
1958
+ };
1959
+ draw(ev.worldPoint, n);
1960
+ if (ev.hole.through && ev.exitPoint) draw(ev.exitPoint, n.clone().negate());
1961
+ if (def?.tags?.includes("metal")) this.particles.burst("sparks", ev.worldPoint, [n.x, n.y, n.z], 6, 3.5);
1962
+ this.sound(impactKey(def), ev.worldPoint, 0.6);
1963
+ }
1964
+ onCrack(ev) {
1965
+ const b = this.bounds.get(ev.object);
1966
+ const pv = b?.parts.get(ev.part);
1967
+ if (!b || !pv) return;
1968
+ const def = this.world.materials.get(pv.material);
1969
+ const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
1970
+ const mv = this.nearestMesh(pv, wp);
1971
+ if (mv) {
1972
+ const color = def?.visual?.transparent ? 15267071 : 2762532;
1973
+ this.visuals.addCrack(mv, wp, this.worldDir(b, ev.crack.normal), ev.crack.rays, ev.crack.length, color, hashNumbers(ev.time, ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]));
1974
+ }
1975
+ this.sound(impactKey(def), ev.worldPoint, 0.5);
1976
+ }
1977
+ onFracture(ev) {
1978
+ const b = this.bounds.get(ev.object);
1979
+ const pv = b?.parts.get(ev.part);
1980
+ const matId = pv?.material || (ev.fragments[0]?.material ?? "");
1981
+ const def = this.world.materials.get(matId);
1982
+ let color = fallbackColor(matId, def);
1983
+ let opacity = 0.5;
1984
+ const transparent = def?.visual?.transparent === true;
1985
+ if (pv) {
1986
+ const c = this.partColor(pv, def);
1987
+ if (c !== null) color = c;
1988
+ const m = pv.meshes[0] ? materialsOf2(pv.meshes[0].origMaterial)[0] : void 0;
1989
+ if (m && m.transparent) opacity = clamp(m.opacity, 0.2, 0.8);
1990
+ this.visuals.hide(pv, "broken");
1991
+ }
1992
+ const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
1993
+ const step = this.low ? 2 : 1;
1994
+ for (let i = 0; i < ev.fragments.length; i += step) {
1995
+ const fr = ev.fragments[i];
1996
+ const fdef = this.world.materials.get(fr.material);
1997
+ const mat = this.fragments.materialFor(fr.material, fdef === def || !fdef ? color : fallbackColor(fr.material, fdef), transparent || fdef?.visual?.transparent === true, opacity);
1998
+ this.fragments.spawn(fr, mat, ev.object, maxAlive);
1999
+ }
2000
+ const up = [0, 1, 0];
2001
+ const sizeK = clamp(ev.fragments.length / 10, 0.5, 2);
2002
+ switch (ev.style) {
2003
+ case "shatter":
2004
+ this.particles.burst("shards", ev.point, up, 18 * sizeK, 3);
2005
+ break;
2006
+ case "crumble":
2007
+ this.particles.burst("dust", ev.point, up, 16 * sizeK, 1.2, { color: pv ? this.partColor(pv, def) ?? void 0 : void 0 });
2008
+ this.particles.burst("chips", ev.point, up, 6, 3);
2009
+ break;
2010
+ case "splinter":
2011
+ this.particles.burst("splinters", ev.point, up, 14 * sizeK, 3);
2012
+ this.particles.burst("dust", ev.point, up, 5, 0.8, { color: 11901556 });
2013
+ break;
2014
+ case "tear":
2015
+ this.particles.burst("fibers", ev.point, up, 12 * sizeK, 1.6, { color });
2016
+ break;
2017
+ case "snap":
2018
+ this.particles.burst("chips", ev.point, up, 8 * sizeK, 3);
2019
+ break;
2020
+ default:
2021
+ this.particles.burst("sparks", ev.point, up, 10, 4);
2022
+ }
2023
+ this.sound(fractureKey(def, ev.style), ev.point, clamp(0.4 + ev.fragments.length * 0.03, 0.4, 1));
2024
+ }
2025
+ onDetach(ev) {
2026
+ this.sound("detach", this.rootPoint(ev.object), 0.5);
2027
+ if (!this.autoSplit) return;
2028
+ const b = this.bounds.get(ev.object);
2029
+ if (!b) return;
2030
+ const st = this.world.state(ev.object);
2031
+ if (!st || st.groups.length < 2) return;
2032
+ const loose = st.groups.length - 1;
2033
+ for (let i = 0; i < loose; i++) {
2034
+ let newId;
2035
+ try {
2036
+ newId = this.world.splitGroup(ev.object, 1);
2037
+ } catch {
2038
+ break;
2039
+ }
2040
+ this.adoptSplit(b, newId);
2041
+ }
2042
+ const old = b.piece;
2043
+ if (old) {
2044
+ const { half, center } = this.measure(b.root, this.meshesOf(b));
2045
+ old.halfExtents = half;
2046
+ this.pieceSys.setCenter(old, center);
2047
+ old.mass = Math.max(0.05, this.massOf(ev.object));
2048
+ old.sleeping = false;
2049
+ }
2050
+ }
2051
+ /** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
2052
+ adoptSplit(from, newId) {
2053
+ const state = this.world.state(newId);
2054
+ const root = from.root;
2055
+ root.updateWorldMatrix(true, false);
2056
+ const group = new this.THREE.Group();
2057
+ group.name = `destruction-piece-${newId}`;
2058
+ root.getWorldPosition(group.position);
2059
+ root.getWorldQuaternion(group.quaternion);
2060
+ this.scene.add(group);
2061
+ group.updateMatrixWorld(true);
2062
+ const nb = { id: newId, root: group, sync: true, parts: /* @__PURE__ */ new Map(), lastPos: null, lastQuat: null, piece: null, isSplit: true };
2063
+ for (const p of state?.parts ?? []) {
2064
+ const pv = from.parts.get(p.id);
2065
+ if (!pv) continue;
2066
+ from.parts.delete(p.id);
2067
+ nb.parts.set(p.id, pv);
2068
+ for (const mv of pv.meshes) {
2069
+ group.attach(mv.mesh);
2070
+ mv.moved = true;
2071
+ }
2072
+ }
2073
+ this.bounds.set(newId, nb);
2074
+ const meshes = this.meshesOf(nb);
2075
+ if (meshes.length === 0) return;
2076
+ const { half, center } = this.measure(group, meshes);
2077
+ let vel = [0, 0, 0];
2078
+ let ang = [0, 0, 0];
2079
+ const op = from.piece;
2080
+ if (op) {
2081
+ vel = [...op.velocity];
2082
+ ang = [...op.angularVelocity];
2083
+ const c = this.pieceSys.centerOf(op, new this.THREE.Vector3());
2084
+ const nc = center.clone().applyQuaternion(group.quaternion).add(group.position);
2085
+ vel[0] += ang[1] * (nc.z - c.z) - ang[2] * (nc.y - c.y);
2086
+ vel[1] += ang[2] * (nc.x - c.x) - ang[0] * (nc.z - c.z);
2087
+ vel[2] += ang[0] * (nc.y - c.y) - ang[1] * (nc.x - c.x);
2088
+ }
2089
+ const piece = this.pieceSys.create(newId, group, half, center, this.massOf(newId), vel, ang);
2090
+ nb.piece = piece;
2091
+ this.o.onPiece?.(piece);
2092
+ }
2093
+ onExplode(ev) {
2094
+ const p = ev.point;
2095
+ const k = clamp(ev.radius / 3, 0.4, 2.5);
2096
+ this.particles.burst("flash", p, [0, 1, 0], 1, 0, { size: k });
2097
+ this.particles.burst("fire", p, [0, 1, 0], 34 * k, 4 * k, { size: 1 + k * 0.5 });
2098
+ this.particles.burst("smoke", p, [0, 1, 0], 24 * k, 2.2, { color: SMOKE_TONES.black, size: 1 + k * 0.6 });
2099
+ this.particles.burst("dust", p, [0, 1, 0], 22 * k, 3.5 * k);
2100
+ this.particles.burst("sparks", p, [0, 1, 0], 16 * k, 8);
2101
+ this.particles.push(p, ev.radius, 4);
2102
+ if (this.o.explosionLight !== false) {
2103
+ if (!this.light) {
2104
+ this.light = new this.THREE.PointLight(16754784, 0, 10, 2);
2105
+ this.light.name = "destruction-explosion-light";
2106
+ }
2107
+ if (!this.light.parent) this.scene.add(this.light);
2108
+ this.light.position.set(p[0], p[1] + 0.5, p[2]);
2109
+ this.light.distance = Math.max(6, ev.radius * 3);
2110
+ this.lightPeak = clamp(Math.sqrt(Math.max(1, ev.energy)) * 0.6, 10, 400);
2111
+ this.lightDur = 0.35;
2112
+ this.lightT = this.lightDur;
2113
+ }
2114
+ const c = this.v1;
2115
+ for (const pc of this.pieceSys.list) {
2116
+ this.pieceSys.centerOf(pc, c);
2117
+ const dx = c.x - p[0];
2118
+ const dy = c.y - p[1];
2119
+ const dz = c.z - p[2];
2120
+ const d = Math.hypot(dx, dy, dz);
2121
+ if (d > ev.radius) continue;
2122
+ const fall = (1 - d / ev.radius) ** 2;
2123
+ const speed = Math.min(30, Math.sqrt(2 * ev.energy * 0.02 / pc.mass)) * fall;
2124
+ const inv = d > 1e-4 ? 1 / d : 0;
2125
+ const dir = d > 1e-4 ? [dx * inv, dy * inv + 0.3, dz * inv] : [0, 1, 0];
2126
+ const spread = pc.halfExtents[0];
2127
+ this.pieceSys.impulse(pc, [dir[0] * speed * pc.mass, dir[1] * speed * pc.mass, dir[2] * speed * pc.mass], [c.x + (this.rng() - 0.5) * spread, c.y + spread * 0.5, c.z + (this.rng() - 0.5) * spread]);
2128
+ }
2129
+ this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(ev.energy) * 0.08));
2130
+ this.sound("explode", p, clamp(0.5 + ev.radius / 10, 0.5, 1));
2131
+ }
2132
+ updateLight(dt) {
2133
+ if (!this.light || this.lightT <= 0) return;
2134
+ this.lightT = Math.max(0, this.lightT - dt);
2135
+ const t = this.lightT / this.lightDur;
2136
+ this.light.intensity = this.lightPeak * t * t;
2137
+ }
2138
+ // ----------------------------------------------------------------------------------------------------------------------------------
2139
+ // Fire, heat, char, melt, leaks
2140
+ // ----------------------------------------------------------------------------------------------------------------------------------
2141
+ updateActive(dt) {
2142
+ this.scanTimer += dt;
2143
+ if (this.scanTimer >= 0.4) {
2144
+ this.scanTimer = 0;
2145
+ for (const b of this.bounds.values()) {
2146
+ for (const pv of b.parts.values()) {
2147
+ if (pv.gone !== "none" && pv.gone !== "broken") continue;
2148
+ const ps = this.world.part(b.id, pv.id);
2149
+ if (!ps) continue;
2150
+ const def = this.world.materials.get(pv.material);
2151
+ if (ps.burning || def?.visual?.glows && ps.temperature > GLOW_FROM_C || Math.abs(ps.char - pv.charApplied) > 0.01 || Math.abs(ps.melt - pv.meltApplied) > 0.01 || pv.gone === "none" && (ps.status === "melted" || ps.status === "ashes")) {
2152
+ this.activate(b.id, pv.id);
2153
+ }
2154
+ }
2155
+ }
2156
+ }
2157
+ for (const [key, a] of this.active) {
2158
+ const pv = this.partVis(a.object, a.part);
2159
+ const ps = pv ? this.world.part(a.object, a.part) : null;
2160
+ if (!pv || !ps) {
2161
+ this.active.delete(key);
2162
+ continue;
2163
+ }
2164
+ const def = this.world.materials.get(pv.material);
2165
+ if (pv.gone === "none") {
2166
+ this.visuals.setChar(pv, ps.char, def?.visual?.charColor ?? 1709072);
2167
+ if (def?.visual?.glows) this.visuals.setGlow(pv, ps.temperature);
2168
+ this.visuals.setMelt(pv, ps.melt);
2169
+ if (ps.status === "melted") this.onGone(a.object, pv, "melted", def);
2170
+ else if (ps.status === "ashes") this.onGone(a.object, pv, "ashes", def);
2171
+ }
2172
+ const fire = this.fires.get(key);
2173
+ if (fire) {
2174
+ if (ps.burning && ps.status !== "broken" && ps.status !== "ashes" && ps.status !== "melted") this.emitFire(fire, pv, def, dt);
2175
+ else this.fires.delete(key);
2176
+ }
2177
+ const hot = def?.visual?.glows === true && ps.temperature > GLOW_FROM_C;
2178
+ if (!ps.burning && !hot && Math.abs(ps.char - pv.charApplied) < 0.01 && Math.abs(ps.melt - pv.meltApplied) < 0.01) {
2179
+ if (def?.visual?.glows && pv.gone === "none") this.visuals.setGlow(pv, 0);
2180
+ this.active.delete(key);
2181
+ }
2182
+ }
2183
+ }
2184
+ onGone(objectId, pv, reason, def) {
2185
+ const bb = new this.THREE.Box3();
2186
+ for (const mv of pv.meshes) bb.union(this.box.setFromObject(mv.mesh));
2187
+ this.visuals.hide(pv, reason);
2188
+ if (bb.isEmpty()) return;
2189
+ const c = bb.getCenter(new this.THREE.Vector3());
2190
+ const s = bb.getSize(new this.THREE.Vector3());
2191
+ const footprint = Math.sqrt(Math.max(s.x * s.z, 1e-4));
2192
+ if (reason === "melted") {
2193
+ this.ground.add("puddle", c.x, c.z, clamp(footprint * 0.55, 0.05, 1), this.partColor(pv, def) ?? fallbackColor(pv.material, def), objectId);
2194
+ } else {
2195
+ this.ground.add("ash", c.x, c.z, clamp(footprint * 0.35, 0.04, 0.5), 1907224, objectId);
2196
+ this.particles.burst("ash", [c.x, bb.max.y, c.z], [0, 1, 0], 10, 0.8);
2197
+ }
2198
+ }
2199
+ emitFire(f, pv, def, dt) {
2200
+ const bb = this.box;
2201
+ bb.makeEmpty();
2202
+ const tmp = new this.THREE.Box3();
2203
+ for (const mv of pv.meshes) if (mv.mesh.visible) bb.union(tmp.setFromObject(mv.mesh));
2204
+ let cx = f.point[0];
2205
+ let cy = f.point[1];
2206
+ let cz = f.point[2];
2207
+ let sx = 0.2;
2208
+ let sy = 0.2;
2209
+ let sz = 0.2;
2210
+ if (!bb.isEmpty()) {
2211
+ const c = bb.getCenter(this.v1);
2212
+ const s = bb.getSize(this.v2);
2213
+ cx = c.x;
2214
+ cy = bb.min.y;
2215
+ cz = c.z;
2216
+ sx = s.x;
2217
+ sy = s.y;
2218
+ sz = s.z;
2219
+ }
2220
+ const vol = clamp(Math.cbrt(Math.max(sx * sy * sz, 1e-5)), 0.05, 1.5);
2221
+ f.accFire += (18 + 70 * vol) * dt;
2222
+ f.accSmoke += (6 + 24 * vol) * dt;
2223
+ const tone = SMOKE_TONES[def?.thermal.smoke ?? "grey"];
2224
+ const rnd = this.rng;
2225
+ while (f.accFire >= 1) {
2226
+ f.accFire -= 1;
2227
+ this.particles.spawn("fire", [cx + (rnd() - 0.5) * sx, cy + sy * (0.3 + 0.7 * rnd()), cz + (rnd() - 0.5) * sz], [(rnd() - 0.5) * 0.4, 0.5 + rnd() * 0.9, (rnd() - 0.5) * 0.4], { size: 0.6 + vol * 0.8 });
2228
+ }
2229
+ while (f.accSmoke >= 1) {
2230
+ f.accSmoke -= 1;
2231
+ this.particles.spawn("smoke", [cx + (rnd() - 0.5) * sx, cy + sy * (0.8 + 0.4 * rnd()), cz + (rnd() - 0.5) * sz], [(rnd() - 0.5) * 0.3 + 0.1, 0.4 + rnd() * 0.5, (rnd() - 0.5) * 0.3], { color: tone, size: 0.6 + vol });
2232
+ }
2233
+ }
2234
+ updateLeaks(dt) {
2235
+ for (let i = this.leaks.length - 1; i >= 0; i--) {
2236
+ const l = this.leaks[i];
2237
+ const ps = this.world.part(l.object, l.part);
2238
+ if (!ps || ps.liters === void 0 || ps.liters <= 0) {
2239
+ this.leaks.splice(i, 1);
2240
+ continue;
2241
+ }
2242
+ l.acc += clamp(20 + l.rate * 400, 15, 120) * dt;
2243
+ const color = FLUID_COLORS[l.fluid] ?? 11184810;
2244
+ const d = l.direction;
2245
+ const dl = Math.hypot(d[0], d[1], d[2]) || 1;
2246
+ const sp = l.pressurized ? 2.5 + this.rng() * 1.5 : 0.3 + this.rng() * 0.3;
2247
+ while (l.acc >= 1) {
2248
+ l.acc -= 1;
2249
+ const vel = [d[0] / dl * sp + (this.rng() - 0.5) * 0.25, d[1] / dl * sp + (this.rng() - 0.5) * 0.25, d[2] / dl * sp + (this.rng() - 0.5) * 0.25];
2250
+ if (l.fluid === "gas") this.particles.spawn("steam", l.point, vel, { color, size: 0.5, alpha: 0.5 });
2251
+ else this.particles.spawn("drips", l.point, vel, { color, tag: this.rng() < 0.03 ? this.tagOf(l.object) : 0 });
2252
+ }
2253
+ }
2254
+ }
2255
+ tagOf(object) {
2256
+ let tag = this.homeTags.indexOf(object) + 1;
2257
+ if (tag === 0) tag = this.homeTags.push(object);
2258
+ return tag;
2259
+ }
2260
+ // ----------------------------------------------------------------------------------------------------------------------------------
2261
+ // Emit, clear, restore, stats, dispose
2262
+ // ----------------------------------------------------------------------------------------------------------------------------------
2263
+ emit(kind, point, normal, count, speed) {
2264
+ this.particles.burst(kind, point, normal, count, speed ?? DEFAULT_SPEED[kind] ?? 1, kind === "smoke" ? { color: SMOKE_TONES.grey } : void 0);
2265
+ }
2266
+ restore(objectId) {
2267
+ const homes = /* @__PURE__ */ new Set([objectId]);
2268
+ const self = this.bounds.get(objectId);
2269
+ if (self?.isSplit) for (const pv of self.parts.values()) homes.add(pv.home);
2270
+ for (const home of homes) this.restoreHome(home);
2271
+ for (const [key, a] of [...this.active]) if (homes.has(a.object)) this.active.delete(key);
2272
+ for (const [key, f] of [...this.fires]) if (homes.has(f.object)) this.fires.delete(key);
2273
+ for (let i = this.leaks.length - 1; i >= 0; i--) if (homes.has(this.leaks[i].object)) this.leaks.splice(i, 1);
2274
+ for (const home of homes) {
2275
+ this.fragments.removeHome(home);
2276
+ this.ground.removeHome(home);
2277
+ }
2278
+ }
2279
+ restoreHome(home) {
2280
+ const homeBound = this.bounds.get(home);
2281
+ for (const b of [...this.bounds.values()]) {
2282
+ for (const [pid, pv] of [...b.parts]) {
2283
+ if (pv.home !== home) continue;
2284
+ this.visuals.restorePart(pv);
2285
+ if (b !== homeBound && homeBound) {
2286
+ b.parts.delete(pid);
2287
+ homeBound.parts.set(pid, pv);
2288
+ }
2289
+ }
2290
+ if (b.isSplit && b.parts.size === 0) {
2291
+ this.scene.remove(b.root);
2292
+ this.pieceSys.remove(b.id);
2293
+ this.bounds.delete(b.id);
2294
+ }
2295
+ }
2296
+ }
2297
+ clear() {
2298
+ this.queue.length = 0;
2299
+ this.active.clear();
2300
+ this.fires.clear();
2301
+ this.leaks.length = 0;
2302
+ for (const id of [...this.bounds.keys()]) if (this.bounds.has(id)) this.restore(id);
2303
+ for (const b of [...this.bounds.values()]) {
2304
+ if (b.isSplit) {
2305
+ for (const pv of b.parts.values()) this.visuals.restorePart(pv);
2306
+ this.scene.remove(b.root);
2307
+ this.pieceSys.remove(b.id);
2308
+ this.bounds.delete(b.id);
2309
+ }
2310
+ }
2311
+ this.fragments.clear();
2312
+ this.ground.clear();
2313
+ this.particles.clear();
2314
+ if (this.light) {
2315
+ this.light.intensity = 0;
2316
+ this.lightT = 0;
2317
+ this.scene.remove(this.light);
2318
+ }
2319
+ }
2320
+ stats() {
2321
+ return { fragments: this.fragments.count, particles: this.particles.live, pieces: this.pieceSys.list.length, decals: this.visuals.decalCount + this.ground.count };
2322
+ }
2323
+ dispose() {
2324
+ if (this.disposed) return;
2325
+ this.unsubscribe();
2326
+ this.clear();
2327
+ for (const id of [...this.bounds.keys()]) this.unbind(id);
2328
+ this.fragments.dispose();
2329
+ this.ground.dispose();
2330
+ this.particles.dispose();
2331
+ this.visuals.dispose();
2332
+ this.light?.dispose?.();
2333
+ this.light = null;
2334
+ this.disposed = true;
2335
+ }
2336
+ };
2337
+
2338
+ // src/spec.ts
2339
+ import { guessMaterial, inferJoints } from "@adatechnology/game-destruction";
2340
+ var METAL = /steel|iron|metal|alumin|copper|brass|bronze|tin|titan|chrome|zinc|lead/i;
2341
+ function sanitizeId(name) {
2342
+ let id = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
2343
+ if (id && /^[0-9]/.test(id)) id = `p_${id}`;
2344
+ return id;
2345
+ }
2346
+ function isMesh(o) {
2347
+ return o.isMesh === true;
2348
+ }
2349
+ function sizeOf(box) {
2350
+ return [box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z];
2351
+ }
2352
+ function specFromObject3D(THREE, root, options = {}) {
2353
+ const fallback = options.fallback ?? "plastic";
2354
+ const minSize = options.minSize ?? 0.015;
2355
+ root.updateMatrixWorld(true);
2356
+ const inv = root.matrixWorld.clone().invert();
2357
+ const entries = [];
2358
+ const byKey = /* @__PURE__ */ new Map();
2359
+ const visit = (o) => {
2360
+ if (!o.visible || o.userData?.destructionIgnore) return;
2361
+ if (isMesh(o) && o.geometry) {
2362
+ const geom = o.geometry;
2363
+ if (!geom.boundingBox) geom.computeBoundingBox();
2364
+ if (geom.boundingBox && !geom.boundingBox.isEmpty()) {
2365
+ const box = geom.boundingBox.clone().applyMatrix4(new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld));
2366
+ const mats = Array.isArray(o.material) ? o.material : [o.material];
2367
+ const matName = mats[0]?.name ?? "";
2368
+ const ud = o.userData ?? {};
2369
+ const material = options.materialOf?.(o) ?? (typeof ud.destructionMaterial === "string" ? ud.destructionMaterial : void 0) ?? guessMaterial(`${o.name} ${matName} ${typeof ud.material === "string" ? ud.material : ""}`) ?? fallback;
2370
+ const explicit = typeof ud.destructionPart === "string" && ud.destructionPart ? ud.destructionPart : null;
2371
+ const hollow = ud.hollow === true || /hollow/i.test(o.name);
2372
+ const fill = options.fillOf?.(o, material);
2373
+ const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
2374
+ const existing = explicit ? byKey.get(explicit) : void 0;
2375
+ if (existing) {
2376
+ const before = sizeOf(existing.box);
2377
+ const bv = before[0] * before[1] * before[2];
2378
+ const ns = sizeOf(box);
2379
+ existing.box.union(box);
2380
+ existing.meshes.push(o);
2381
+ if (ns[0] * ns[1] * ns[2] > bv) existing.material = material;
2382
+ existing.hollow = existing.hollow || hollow;
2383
+ if (fill !== void 0) existing.fill = fill;
2384
+ } else {
2385
+ const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, tags, fixedId: !!explicit, name: o.name };
2386
+ entries.push(e);
2387
+ if (explicit) byKey.set(explicit, e);
2388
+ }
2389
+ }
2390
+ }
2391
+ for (const c of o.children) visit(c);
2392
+ };
2393
+ visit(root);
2394
+ const eps = 5e-3;
2395
+ const overlapVolume = (a, b) => {
2396
+ const x = Math.min(a.max.x, b.max.x) - Math.max(a.min.x, b.min.x) + eps;
2397
+ const y = Math.min(a.max.y, b.max.y) - Math.max(a.min.y, b.min.y) + eps;
2398
+ const z = Math.min(a.max.z, b.max.z) - Math.max(a.min.z, b.min.z) + eps;
2399
+ return x > 0 && y > 0 && z > 0 ? x * y * z : 0;
2400
+ };
2401
+ const largest = (e) => Math.max(...sizeOf(e.box));
2402
+ const kept = [];
2403
+ const tiny = [];
2404
+ for (const e of entries) (!e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
2405
+ for (const t of tiny) {
2406
+ let best = null;
2407
+ let bestV = 0;
2408
+ for (const k of kept) {
2409
+ const v = overlapVolume(t.box, k.box);
2410
+ if (v > bestV) {
2411
+ bestV = v;
2412
+ best = k;
2413
+ }
2414
+ }
2415
+ if (!best) {
2416
+ let bd = 0.05;
2417
+ for (const k of kept) {
2418
+ const d = k.box.distanceToPoint(t.box.getCenter(new THREE.Vector3()));
2419
+ if (d < bd) {
2420
+ bd = d;
2421
+ best = k;
2422
+ }
2423
+ }
2424
+ }
2425
+ if (best) {
2426
+ best.meshes.push(...t.meshes);
2427
+ } else {
2428
+ kept.push(t);
2429
+ }
2430
+ }
2431
+ const used = /* @__PURE__ */ new Set();
2432
+ const parts = [];
2433
+ const meshesOf = {};
2434
+ let counter = 0;
2435
+ for (const e of kept) {
2436
+ counter++;
2437
+ let base = (e.explicitId ? sanitizeId(e.explicitId) : "") || sanitizeId(e.name) || `part_${counter}`;
2438
+ let id = base;
2439
+ for (let n = 2; used.has(id); n++) id = `${base}_${n}`;
2440
+ used.add(id);
2441
+ base = id;
2442
+ const size = sizeOf(e.box);
2443
+ const center = e.box.getCenter(new THREE.Vector3());
2444
+ const part = {
2445
+ id,
2446
+ material: e.material,
2447
+ shape: { kind: "box", size: [Math.max(size[0], 2e-3), Math.max(size[1], 2e-3), Math.max(size[2], 2e-3)] },
2448
+ position: [center.x, center.y, center.z]
2449
+ };
2450
+ const minSide = Math.min(...size);
2451
+ if (e.fill !== void 0) part.fill = e.fill;
2452
+ else if (e.hollow) part.fill = 0.05;
2453
+ if (minSide < 0.01 && METAL.test(e.material)) {
2454
+ part.fill = e.fill ?? 1;
2455
+ part.thickness = Math.max(minSide, 5e-4);
2456
+ }
2457
+ if (e.tags) part.tags = [...e.tags];
2458
+ const ao = options.anchorBelow ?? -1;
2459
+ if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
2460
+ parts.push(part);
2461
+ meshesOf[id] = e.meshes;
2462
+ }
2463
+ const spec = { parts };
2464
+ if (options.name !== void 0) spec.name = options.name;
2465
+ else if (root.name) spec.name = root.name;
2466
+ if (options.tags) spec.tags = [...options.tags];
2467
+ spec.joints = options.joints === false ? [] : inferJoints(parts);
2468
+ return { spec, parts: meshesOf };
2469
+ }
2470
+ export {
2471
+ createDestructionRenderer,
2472
+ heatColor,
2473
+ specFromObject3D
2474
+ };
2475
+ //# sourceMappingURL=index.js.map