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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2551 @@
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 NARROW = 0.3;
694
+ var TOPPLE = 3;
695
+ var PieceSystem = class {
696
+ constructor(THREE, o) {
697
+ __publicField(this, "THREE", THREE);
698
+ __publicField(this, "o", o);
699
+ __publicField(this, "list", []);
700
+ __publicField(this, "time", 0);
701
+ __publicField(this, "p");
702
+ __publicField(this, "q");
703
+ __publicField(this, "dq");
704
+ __publicField(this, "m");
705
+ __publicField(this, "ax");
706
+ this.p = new THREE.Vector3();
707
+ this.q = new THREE.Quaternion();
708
+ this.dq = new THREE.Quaternion();
709
+ this.m = new THREE.Matrix4();
710
+ this.ax = new THREE.Vector3();
711
+ }
712
+ get(objectId) {
713
+ return this.list.find((b) => b.objectId === objectId) ?? null;
714
+ }
715
+ create(objectId, root, halfExtents, localCenter, mass, velocity, angular) {
716
+ const existing = this.get(objectId);
717
+ if (existing) {
718
+ existing.root = root;
719
+ existing.halfExtents = halfExtents;
720
+ existing.mass = mass;
721
+ this.setCenter(existing, localCenter);
722
+ if (velocity) existing.velocity = [...velocity];
723
+ existing.sleeping = false;
724
+ return existing;
725
+ }
726
+ const body = {
727
+ objectId,
728
+ root,
729
+ velocity: velocity ? [...velocity] : [0, 0, 0],
730
+ angularVelocity: angular ? [...angular] : [0, 0, 0],
731
+ halfExtents: [Math.max(0.01, halfExtents[0]), Math.max(0.01, halfExtents[1]), Math.max(0.01, halfExtents[2])],
732
+ mass: Math.max(0.05, mass),
733
+ sleeping: false,
734
+ held: false,
735
+ localCenter: [localCenter.x, localCenter.y, localCenter.z],
736
+ centerV: localCenter.clone(),
737
+ still: 0,
738
+ lastHit: -1,
739
+ onGround: false
740
+ };
741
+ this.list.push(body);
742
+ return body;
743
+ }
744
+ /** Move the centre of mass (after a part broke off). */
745
+ setCenter(b, c) {
746
+ b.centerV.copy(c);
747
+ b.localCenter = [c.x, c.y, c.z];
748
+ }
749
+ remove(objectId) {
750
+ const i = this.list.findIndex((b) => b.objectId === objectId);
751
+ if (i >= 0) this.list.splice(i, 1);
752
+ }
753
+ clear() {
754
+ this.list.length = 0;
755
+ }
756
+ /** World position of the centre of mass. */
757
+ centerOf(b, out) {
758
+ return out.copy(b.centerV).applyQuaternion(b.root.quaternion).add(b.root.position);
759
+ }
760
+ impulse(b, impulse, point) {
761
+ const inv = 1 / b.mass;
762
+ b.velocity[0] += impulse[0] * inv;
763
+ b.velocity[1] += impulse[1] * inv;
764
+ b.velocity[2] += impulse[2] * inv;
765
+ if (point) {
766
+ const c = this.centerOf(b, this.p);
767
+ const rx = point[0] - c.x;
768
+ const ry = point[1] - c.y;
769
+ const rz = point[2] - c.z;
770
+ const tx = ry * impulse[2] - rz * impulse[1];
771
+ const ty = rz * impulse[0] - rx * impulse[2];
772
+ const tz = rx * impulse[1] - ry * impulse[0];
773
+ const w = this.mulInvInertia(b, tx, ty, tz);
774
+ b.angularVelocity[0] += w[0];
775
+ b.angularVelocity[1] += w[1];
776
+ b.angularVelocity[2] += w[2];
777
+ }
778
+ b.sleeping = false;
779
+ b.still = 0;
780
+ }
781
+ /** 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). */
782
+ mulInvInertia(b, x, y, z) {
783
+ this.m.makeRotationFromQuaternion(b.root.quaternion);
784
+ return this.mulInvInertiaWith(b, this.m.elements, x, y, z);
785
+ }
786
+ mulInvInertiaWith(b, e, x, y, z) {
787
+ const [hx, hy, hz] = b.halfExtents;
788
+ const k = 3 / b.mass;
789
+ const ix = k / (hy * hy + hz * hz);
790
+ const iy = k / (hx * hx + hz * hz);
791
+ const iz = k / (hx * hx + hy * hy);
792
+ const lx = (e[0] * x + e[1] * y + e[2] * z) * ix;
793
+ const ly = (e[4] * x + e[5] * y + e[6] * z) * iy;
794
+ const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
795
+ 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];
796
+ }
797
+ update(dt) {
798
+ this.time += dt;
799
+ for (const b of this.list) {
800
+ if (b.held) {
801
+ b.velocity = [0, 0, 0];
802
+ b.angularVelocity = [0, 0, 0];
803
+ b.sleeping = false;
804
+ b.still = 0;
805
+ continue;
806
+ }
807
+ if (b.sleeping) continue;
808
+ this.integrate(b, dt);
809
+ }
810
+ this.separate(dt);
811
+ }
812
+ integrate(b, dt) {
813
+ const c = this.centerOf(b, this.p);
814
+ const pos = [c.x, c.y, c.z];
815
+ const q = this.q.copy(b.root.quaternion);
816
+ const v = b.velocity;
817
+ const w = b.angularVelocity;
818
+ const [hx, hy, hz] = b.halfExtents;
819
+ const invM = 1 / b.mass;
820
+ const steps = clamp(Math.ceil(dt / SUB_DT), 1, 8);
821
+ const h = dt / steps;
822
+ let landing = 0;
823
+ let landPoint = [pos[0], pos[1], pos[2]];
824
+ let contactedLast = false;
825
+ let touched = false;
826
+ let unstable = false;
827
+ for (let s = 0; s < steps; s++) {
828
+ v[1] -= this.o.gravity * h;
829
+ pos[0] += v[0] * h;
830
+ pos[1] += v[1] * h;
831
+ pos[2] += v[2] * h;
832
+ const wl = Math.hypot(w[0], w[1], w[2]);
833
+ if (wl > 1e-6) {
834
+ this.dq.setFromAxisAngle(this.ax.set(w[0] / wl, w[1] / wl, w[2] / wl), wl * h);
835
+ q.premultiply(this.dq).normalize();
836
+ }
837
+ const damp = Math.exp(-0.15 * h);
838
+ w[0] *= damp;
839
+ w[1] *= damp;
840
+ w[2] *= damp;
841
+ this.m.makeRotationFromQuaternion(q);
842
+ const e = this.m.elements;
843
+ const rs = [];
844
+ let maxPen = 0;
845
+ for (let i = 0; i < 8; i++) {
846
+ const sx = i & 1 ? hx : -hx;
847
+ const sy = i & 2 ? hy : -hy;
848
+ const sz = i & 4 ? hz : -hz;
849
+ 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];
850
+ const gy = this.o.groundAt(pos[0] + r[0], pos[2] + r[2]);
851
+ const pen = gy - (pos[1] + r[1]);
852
+ if (pen > -SLOP) {
853
+ rs.push(r);
854
+ if (pen > maxPen) maxPen = pen;
855
+ }
856
+ }
857
+ contactedLast = rs.length > 0;
858
+ if (contactedLast) touched = true;
859
+ if (!contactedLast) continue;
860
+ pos[1] += Math.max(0, maxPen - SLOP * 0.5);
861
+ let impact = 0;
862
+ for (const r of rs) impact = Math.max(impact, -(v[1] + w[2] * r[0] - w[0] * r[2]));
863
+ const restitution = impact > 2 ? 0.25 : 0;
864
+ if (impact > landing) {
865
+ landing = impact;
866
+ let sx = 0;
867
+ let sz = 0;
868
+ for (const r of rs) {
869
+ sx += r[0];
870
+ sz += r[2];
871
+ }
872
+ const lx = pos[0] + sx / rs.length;
873
+ const lz = pos[2] + sz / rs.length;
874
+ landPoint = [lx, this.o.groundAt(lx, lz), lz];
875
+ }
876
+ for (let it = 0; it < 4; it++) {
877
+ for (const r of rs) {
878
+ let vx = v[0] + (w[1] * r[2] - w[2] * r[1]);
879
+ let vy = v[1] + (w[2] * r[0] - w[0] * r[2]);
880
+ let vz = v[2] + (w[0] * r[1] - w[1] * r[0]);
881
+ const vn = vy;
882
+ if (vn >= 0) continue;
883
+ const a = this.mulInvInertiaWith(b, e, -r[2], 0, r[0]);
884
+ const denom = invM + (a[2] * r[0] - a[0] * r[2]);
885
+ const j = -(1 + (it === 0 ? restitution : 0)) * vn / denom;
886
+ v[1] += j * invM;
887
+ const dw = this.mulInvInertiaWith(b, e, -r[2] * j, 0, r[0] * j);
888
+ w[0] += dw[0];
889
+ w[1] += dw[1];
890
+ w[2] += dw[2];
891
+ vx = v[0] + (w[1] * r[2] - w[2] * r[1]);
892
+ vy = v[1] + (w[2] * r[0] - w[0] * r[2]);
893
+ vz = v[2] + (w[0] * r[1] - w[1] * r[0]);
894
+ const vt = Math.hypot(vx, vz);
895
+ if (vt > 1e-5) {
896
+ const tx = vx / vt;
897
+ const tz = vz / vt;
898
+ const rx = r[1] * tz;
899
+ const ry = r[2] * tx - r[0] * tz;
900
+ const rz = -r[1] * tx;
901
+ const at = this.mulInvInertiaWith(b, e, rx, ry, rz);
902
+ const cx = at[1] * r[2] - at[2] * r[1];
903
+ const cz = at[0] * r[1] - at[1] * r[0];
904
+ const denomT = invM + cx * tx + cz * tz;
905
+ const jt = Math.min(vt / denomT, MU * j);
906
+ v[0] -= tx * jt * invM;
907
+ v[2] -= tz * jt * invM;
908
+ const dwt = this.mulInvInertiaWith(b, e, rx * jt, ry * jt, rz * jt);
909
+ w[0] -= dwt[0];
910
+ w[1] -= dwt[1];
911
+ w[2] -= dwt[2];
912
+ }
913
+ }
914
+ }
915
+ let x0 = Infinity, x1 = -Infinity, z0 = Infinity, z1 = -Infinity, low = 0;
916
+ for (const r of rs) {
917
+ x0 = Math.min(x0, r[0]);
918
+ x1 = Math.max(x1, r[0]);
919
+ z0 = Math.min(z0, r[2]);
920
+ z1 = Math.max(z1, r[2]);
921
+ low = Math.min(low, r[1]);
922
+ }
923
+ const height = -low;
924
+ const wx = x1 - x0;
925
+ const wz = z1 - z0;
926
+ unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
927
+ if (unstable) {
928
+ const dirX = x0 + x1 > 0 ? -1 : 1;
929
+ const dirZ = z0 + z1 > 0 ? -1 : 1;
930
+ const k = TOPPLE * Math.min(height, 1) * h;
931
+ if (wx <= wz) w[2] -= dirX * k;
932
+ else w[0] += dirZ * k;
933
+ }
934
+ const rd = Math.exp((rs.length >= 3 ? -3 : -1.5) * h);
935
+ w[0] *= rd;
936
+ w[1] *= rd;
937
+ w[2] *= rd;
938
+ const fd = Math.exp(-0.3 * h);
939
+ v[0] *= fd;
940
+ v[2] *= fd;
941
+ }
942
+ b.root.quaternion.copy(q);
943
+ const off = this.ax.copy(b.centerV).applyQuaternion(q);
944
+ b.root.position.set(pos[0] - off.x, pos[1] - off.y, pos[2] - off.z);
945
+ b.root.updateMatrixWorld(true);
946
+ b.onGround = contactedLast;
947
+ if (landing > HARD_LANDING && this.time - b.lastHit > 0.15) {
948
+ b.lastHit = this.time;
949
+ this.o.onHardLanding(b, landPoint, landing);
950
+ }
951
+ const still = touched && !unstable && Math.hypot(v[0], v[1], v[2]) < 0.12 && Math.hypot(w[0], w[1], w[2]) < 0.45;
952
+ if (still) {
953
+ b.still += dt;
954
+ if (b.still >= SLEEP_AFTER) {
955
+ b.sleeping = true;
956
+ v[0] = v[1] = v[2] = 0;
957
+ w[0] = w[1] = w[2] = 0;
958
+ }
959
+ } else {
960
+ b.still = 0;
961
+ }
962
+ }
963
+ /** Push overlapping pieces apart (horizontal bounding circles), gently, so they do not interpenetrate forever. */
964
+ separate(dt) {
965
+ const n = this.list.length;
966
+ if (n < 2) return;
967
+ const maxMove = 1.5 * dt;
968
+ const ca = new this.THREE.Vector3();
969
+ const cb = new this.THREE.Vector3();
970
+ for (let i = 0; i < n; i++) {
971
+ const a = this.list[i];
972
+ for (let j = i + 1; j < n; j++) {
973
+ const b = this.list[j];
974
+ if (a.held && b.held) continue;
975
+ if (a.sleeping && b.sleeping) continue;
976
+ const ra = Math.hypot(a.halfExtents[0], a.halfExtents[1], a.halfExtents[2]);
977
+ const rb = Math.hypot(b.halfExtents[0], b.halfExtents[1], b.halfExtents[2]);
978
+ this.centerOf(a, ca);
979
+ this.centerOf(b, cb);
980
+ if (Math.abs(ca.y - cb.y) > ra + rb) continue;
981
+ const rh = (Math.max(a.halfExtents[0], a.halfExtents[2]) + Math.max(b.halfExtents[0], b.halfExtents[2])) * 0.8;
982
+ let dx = cb.x - ca.x;
983
+ let dz = cb.z - ca.z;
984
+ let d = Math.hypot(dx, dz);
985
+ if (d >= rh) continue;
986
+ if (d < 1e-5) {
987
+ dx = 1;
988
+ dz = 0;
989
+ d = 1;
990
+ }
991
+ const push = Math.min(maxMove, (rh - d) * 0.5);
992
+ const wa = a.held ? 0 : b.held ? 1 : b.mass / (a.mass + b.mass);
993
+ const wb = b.held ? 0 : a.held ? 1 : a.mass / (a.mass + b.mass);
994
+ const ux = dx / (d || 1);
995
+ const uz = dz / (d || 1);
996
+ if (!a.held) {
997
+ a.root.position.x -= ux * push * 2 * wa;
998
+ a.root.position.z -= uz * push * 2 * wa;
999
+ a.sleeping = false;
1000
+ }
1001
+ if (!b.held) {
1002
+ b.root.position.x += ux * push * 2 * wb;
1003
+ b.root.position.z += uz * push * 2 * wb;
1004
+ b.sleeping = false;
1005
+ }
1006
+ }
1007
+ }
1008
+ }
1009
+ };
1010
+
1011
+ // src/spec.ts
1012
+ import { guessMaterial, inferJoints } from "@adatechnology/game-destruction";
1013
+ function partHintOf(value) {
1014
+ if (typeof value === "string") return value ? { id: value } : null;
1015
+ if (!value || typeof value !== "object") return null;
1016
+ const v = value;
1017
+ if (typeof v.id !== "string" || !v.id) return null;
1018
+ const str = (x) => typeof x === "string" && x ? x : void 0;
1019
+ return { id: v.id, material: str(v.material), parent: str(v.parent), kind: str(v.kind) };
1020
+ }
1021
+ var METAL = /steel|iron|metal|alumin|copper|brass|bronze|tin|titan|chrome|zinc|lead/i;
1022
+ function sanitizeId(name) {
1023
+ let id = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
1024
+ if (id && /^[0-9]/.test(id)) id = `p_${id}`;
1025
+ return id;
1026
+ }
1027
+ function isMesh(o) {
1028
+ return o.isMesh === true;
1029
+ }
1030
+ function sizeOf(box) {
1031
+ return [box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z];
1032
+ }
1033
+ function specFromObject3D(THREE, root, options = {}) {
1034
+ const fallback = options.fallback ?? "plastic";
1035
+ const minSize = options.minSize ?? 0.015;
1036
+ root.updateMatrixWorld(true);
1037
+ const inv = root.matrixWorld.clone().invert();
1038
+ const entries = [];
1039
+ const byKey = /* @__PURE__ */ new Map();
1040
+ const visit = (o) => {
1041
+ if (!o.visible || o.userData?.destructionIgnore) return;
1042
+ if (isMesh(o) && o.geometry) {
1043
+ const geom = o.geometry;
1044
+ if (!geom.boundingBox) geom.computeBoundingBox();
1045
+ if (geom.boundingBox && !geom.boundingBox.isEmpty()) {
1046
+ const box = geom.boundingBox.clone().applyMatrix4(new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld));
1047
+ const mats = Array.isArray(o.material) ? o.material : [o.material];
1048
+ const matName = mats[0]?.name ?? "";
1049
+ const ud = o.userData ?? {};
1050
+ const hint = partHintOf(ud.destructionPart);
1051
+ const material = options.materialOf?.(o) ?? (typeof ud.destructionMaterial === "string" ? ud.destructionMaterial : void 0) ?? hint?.material ?? guessMaterial(`${o.name} ${matName} ${typeof ud.material === "string" ? ud.material : ""}`) ?? fallback;
1052
+ const explicit = hint?.id ?? null;
1053
+ const hollow = ud.hollow === true || /hollow/i.test(o.name);
1054
+ const fill = options.fillOf?.(o, material);
1055
+ const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
1056
+ const existing = explicit ? byKey.get(explicit) : void 0;
1057
+ if (existing) {
1058
+ const before = sizeOf(existing.box);
1059
+ const bv = before[0] * before[1] * before[2];
1060
+ const ns = sizeOf(box);
1061
+ existing.box.union(box);
1062
+ existing.meshes.push(o);
1063
+ if (ns[0] * ns[1] * ns[2] > bv) existing.material = material;
1064
+ existing.hollow = existing.hollow || hollow;
1065
+ if (fill !== void 0) existing.fill = fill;
1066
+ } else {
1067
+ const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, tags, fixedId: !!explicit, name: o.name, parent: hint?.parent, jointKind: hint?.kind };
1068
+ entries.push(e);
1069
+ if (explicit) byKey.set(explicit, e);
1070
+ }
1071
+ }
1072
+ }
1073
+ for (const c of o.children) visit(c);
1074
+ };
1075
+ visit(root);
1076
+ const eps = 5e-3;
1077
+ const overlapVolume = (a, b) => {
1078
+ const x = Math.min(a.max.x, b.max.x) - Math.max(a.min.x, b.min.x) + eps;
1079
+ const y = Math.min(a.max.y, b.max.y) - Math.max(a.min.y, b.min.y) + eps;
1080
+ const z = Math.min(a.max.z, b.max.z) - Math.max(a.min.z, b.min.z) + eps;
1081
+ return x > 0 && y > 0 && z > 0 ? x * y * z : 0;
1082
+ };
1083
+ const largest = (e) => Math.max(...sizeOf(e.box));
1084
+ const kept = [];
1085
+ const tiny = [];
1086
+ for (const e of entries) (!e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
1087
+ for (const t of tiny) {
1088
+ let best = null;
1089
+ let bestV = 0;
1090
+ for (const k of kept) {
1091
+ const v = overlapVolume(t.box, k.box);
1092
+ if (v > bestV) {
1093
+ bestV = v;
1094
+ best = k;
1095
+ }
1096
+ }
1097
+ if (!best) {
1098
+ let bd = 0.05;
1099
+ for (const k of kept) {
1100
+ const d = k.box.distanceToPoint(t.box.getCenter(new THREE.Vector3()));
1101
+ if (d < bd) {
1102
+ bd = d;
1103
+ best = k;
1104
+ }
1105
+ }
1106
+ }
1107
+ if (best) {
1108
+ best.meshes.push(...t.meshes);
1109
+ } else {
1110
+ kept.push(t);
1111
+ }
1112
+ }
1113
+ const used = /* @__PURE__ */ new Set();
1114
+ const parts = [];
1115
+ const meshesOf = {};
1116
+ let counter = 0;
1117
+ for (const e of kept) {
1118
+ counter++;
1119
+ let base = (e.explicitId ? sanitizeId(e.explicitId) : "") || sanitizeId(e.name) || `part_${counter}`;
1120
+ let id = base;
1121
+ for (let n = 2; used.has(id); n++) id = `${base}_${n}`;
1122
+ used.add(id);
1123
+ base = id;
1124
+ const size = sizeOf(e.box);
1125
+ const center = e.box.getCenter(new THREE.Vector3());
1126
+ const part = {
1127
+ id,
1128
+ material: e.material,
1129
+ shape: { kind: "box", size: [Math.max(size[0], 2e-3), Math.max(size[1], 2e-3), Math.max(size[2], 2e-3)] },
1130
+ position: [center.x, center.y, center.z]
1131
+ };
1132
+ const minSide = Math.min(...size);
1133
+ if (e.fill !== void 0) part.fill = e.fill;
1134
+ else if (e.hollow) part.fill = 0.05;
1135
+ if (minSide < 0.01 && METAL.test(e.material)) {
1136
+ part.fill = e.fill ?? 1;
1137
+ part.thickness = Math.max(minSide, 5e-4);
1138
+ }
1139
+ if (e.tags) part.tags = [...e.tags];
1140
+ const ao = options.anchorBelow ?? -1;
1141
+ if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
1142
+ parts.push(part);
1143
+ meshesOf[id] = e.meshes;
1144
+ }
1145
+ const spec = { parts };
1146
+ if (options.name !== void 0) spec.name = options.name;
1147
+ else if (root.name) spec.name = root.name;
1148
+ if (options.tags) spec.tags = [...options.tags];
1149
+ spec.joints = options.joints === false ? [] : inferJoints(parts);
1150
+ if (options.joints !== false) {
1151
+ const finalId = /* @__PURE__ */ new Map();
1152
+ kept.forEach((e, i) => finalId.set(e, parts[i].id));
1153
+ const idOf = /* @__PURE__ */ new Map();
1154
+ for (const e of kept) if (e.explicitId) idOf.set(e.explicitId, finalId.get(e));
1155
+ const joints = spec.joints;
1156
+ for (const e of kept) {
1157
+ if (!e.parent) continue;
1158
+ const id = finalId.get(e);
1159
+ const parent = idOf.get(e.parent) ?? (used.has(sanitizeId(e.parent)) ? sanitizeId(e.parent) : null);
1160
+ if (!parent || parent === id) continue;
1161
+ const at = joints.findIndex((j) => j.a === parent && j.b === id || j.a === id && j.b === parent);
1162
+ const kind = e.jointKind ?? (at >= 0 ? joints[at].kind : "resting");
1163
+ const joint = { id: `${parent}~${id}`, a: parent, b: id, kind };
1164
+ if (at >= 0) joints[at] = joint;
1165
+ else joints.push(joint);
1166
+ }
1167
+ }
1168
+ return { spec, parts: meshesOf };
1169
+ }
1170
+
1171
+ // src/heat.ts
1172
+ var STOPS = [
1173
+ [450, 0],
1174
+ [500, 2753536],
1175
+ [650, 9049344],
1176
+ [800, 14174728],
1177
+ [950, 16747540],
1178
+ [1100, 16761402],
1179
+ [1250, 16771232],
1180
+ [1400, 16777215]
1181
+ ];
1182
+ function heatColor(temperatureC) {
1183
+ if (!(temperatureC > STOPS[0][0])) return 0;
1184
+ const last = STOPS[STOPS.length - 1];
1185
+ if (temperatureC >= last[0]) return last[1];
1186
+ for (let i = 1; i < STOPS.length; i++) {
1187
+ const [t1, c1] = STOPS[i];
1188
+ if (temperatureC <= t1) {
1189
+ const [t0, c0] = STOPS[i - 1];
1190
+ return mixHex(c0, c1, (temperatureC - t0) / (t1 - t0));
1191
+ }
1192
+ }
1193
+ return last[1];
1194
+ }
1195
+
1196
+ // src/visual.ts
1197
+ var POS_ATTR = "position";
1198
+ function positionAttr(geo) {
1199
+ const a = geo.getAttribute(POS_ATTR);
1200
+ return a && !a.isInterleavedBufferAttribute ? a : null;
1201
+ }
1202
+ function materialsOf(m) {
1203
+ return Array.isArray(m) ? m : [m];
1204
+ }
1205
+ function colorOf(m) {
1206
+ const c = m.color;
1207
+ return c && c.isColor ? c : null;
1208
+ }
1209
+ var DecalKit = class {
1210
+ constructor(THREE) {
1211
+ __publicField(this, "THREE", THREE);
1212
+ __publicField(this, "disc", null);
1213
+ __publicField(this, "discMat", null);
1214
+ __publicField(this, "lineMats", /* @__PURE__ */ new Map());
1215
+ }
1216
+ discGeometry() {
1217
+ return this.disc ?? (this.disc = new this.THREE.CircleGeometry(1, 14));
1218
+ }
1219
+ discMaterial() {
1220
+ return this.discMat ?? (this.discMat = new this.THREE.MeshBasicMaterial({
1221
+ color: 328965,
1222
+ transparent: true,
1223
+ opacity: 0.92,
1224
+ depthWrite: false,
1225
+ polygonOffset: true,
1226
+ polygonOffsetFactor: -4,
1227
+ polygonOffsetUnits: -4
1228
+ }));
1229
+ }
1230
+ lineMaterial(color) {
1231
+ let m = this.lineMats.get(color);
1232
+ if (!m) {
1233
+ m = new this.THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.9, depthWrite: false });
1234
+ this.lineMats.set(color, m);
1235
+ }
1236
+ return m;
1237
+ }
1238
+ dispose() {
1239
+ this.disc?.dispose();
1240
+ this.discMat?.dispose();
1241
+ for (const m of this.lineMats.values()) m.dispose();
1242
+ this.disc = null;
1243
+ this.discMat = null;
1244
+ this.lineMats.clear();
1245
+ }
1246
+ };
1247
+ var tmpV = (THREE) => new THREE.Vector3();
1248
+ var Visuals = class {
1249
+ constructor(THREE, lowQuality, decalCap) {
1250
+ __publicField(this, "THREE", THREE);
1251
+ __publicField(this, "lowQuality", lowQuality);
1252
+ __publicField(this, "decalCap", decalCap);
1253
+ __publicField(this, "kit");
1254
+ __publicField(this, "a");
1255
+ __publicField(this, "b");
1256
+ __publicField(this, "m4");
1257
+ __publicField(this, "box");
1258
+ /** Total live decals across meshes (kept in sync by add/remove). */
1259
+ __publicField(this, "decalCount", 0);
1260
+ __publicField(this, "tints", /* @__PURE__ */ new WeakMap());
1261
+ this.kit = new DecalKit(THREE);
1262
+ this.a = tmpV(THREE);
1263
+ this.b = tmpV(THREE);
1264
+ this.m4 = new THREE.Matrix4();
1265
+ this.box = new THREE.Box3();
1266
+ }
1267
+ makeMeshVis(mesh, home) {
1268
+ return {
1269
+ mesh,
1270
+ home,
1271
+ origGeometry: mesh.geometry,
1272
+ origMaterial: mesh.material,
1273
+ origVisible: mesh.visible,
1274
+ origParent: mesh.parent,
1275
+ origPosition: mesh.position.clone(),
1276
+ origQuaternion: mesh.quaternion.clone(),
1277
+ origScale: mesh.scale.clone(),
1278
+ ownMat: false,
1279
+ ownGeo: false,
1280
+ work: null,
1281
+ baseColors: null,
1282
+ decals: [],
1283
+ meltApplied: 0,
1284
+ bottomY: 0,
1285
+ moved: false
1286
+ };
1287
+ }
1288
+ vertexCount(mv) {
1289
+ return mv.mesh.geometry.getAttribute(POS_ATTR)?.count ?? 0;
1290
+ }
1291
+ /**
1292
+ * Average vertex colour of a mesh whose material uses `vertexColors` (computed once per mesh, from its original geometry), else null. Fragments
1293
+ * and debris of such a mesh multiply it by `material.color`; without it they come out white.
1294
+ */
1295
+ vertexTint(mv) {
1296
+ if (this.tints.has(mv.mesh)) return this.tints.get(mv.mesh) ?? null;
1297
+ let tint = null;
1298
+ const used = materialsOf(mv.origMaterial).some((m) => m.vertexColors === true);
1299
+ const attr = used ? mv.origGeometry.getAttribute("color") : void 0;
1300
+ if (attr && attr.count > 0 && attr.itemSize >= 3) {
1301
+ let r = 0, g = 0, b = 0;
1302
+ for (let i = 0; i < attr.count; i++) {
1303
+ r += attr.getX(i);
1304
+ g += attr.getY(i);
1305
+ b += attr.getZ(i);
1306
+ }
1307
+ tint = new this.THREE.Color(r / attr.count, g / attr.count, b / attr.count);
1308
+ }
1309
+ this.tints.set(mv.mesh, tint);
1310
+ return tint;
1311
+ }
1312
+ /** Colour of the original material (before any char) times the vertex tint, `0xRRGGBB`, or -1. */
1313
+ baseColorHex(mv) {
1314
+ const c = mv.baseColors?.[0] ?? colorOf(materialsOf(mv.origMaterial)[0]);
1315
+ if (!c) return -1;
1316
+ const tint = this.vertexTint(mv);
1317
+ return tint ? c.clone().multiply(tint).getHex() : c.getHex();
1318
+ }
1319
+ /** Is the original material transparent? */
1320
+ isTransparent(mv) {
1321
+ return materialsOf(mv.origMaterial).some((m) => m.transparent && m.opacity < 1);
1322
+ }
1323
+ ownMaterial(mv) {
1324
+ if (mv.ownMat) return;
1325
+ mv.ownMat = true;
1326
+ const src = materialsOf(mv.origMaterial);
1327
+ const cloned = src.map((m) => m.clone());
1328
+ mv.baseColors = src.map((m) => colorOf(m)?.clone() ?? new this.THREE.Color(8947848));
1329
+ mv.mesh.material = Array.isArray(mv.origMaterial) ? cloned : cloned[0];
1330
+ }
1331
+ /** Clone the geometry (vertex edits); false when its positions cannot be edited. */
1332
+ ownGeometry(mv) {
1333
+ if (mv.ownGeo) return true;
1334
+ const pos = positionAttr(mv.mesh.geometry);
1335
+ if (!pos) return false;
1336
+ mv.ownGeo = true;
1337
+ const g = mv.mesh.geometry.clone();
1338
+ mv.mesh.geometry = g;
1339
+ mv.work = new Float32Array(positionAttr(g).array);
1340
+ return true;
1341
+ }
1342
+ // ------------------------------------------------------------------------------------------------------------------------------------
1343
+ // Dents
1344
+ // ------------------------------------------------------------------------------------------------------------------------------------
1345
+ /**
1346
+ * Push the vertices of the mesh within `dent.radius` of `worldPoint` inwards along `worldNormal` by `depth * (1 - (d/r)^2)^2`.
1347
+ * Returns false when nothing was changed (too far, low quality on a heavy mesh, or no editable positions).
1348
+ */
1349
+ applyDent(mv, worldPoint, worldNormal, dent) {
1350
+ const mesh = mv.mesh;
1351
+ if (this.lowQuality && this.vertexCount(mv) > 2e4) return false;
1352
+ mesh.updateWorldMatrix(true, false);
1353
+ const s = mesh.matrixWorld.getMaxScaleOnAxis() || 1;
1354
+ const lp = mesh.worldToLocal(this.a.copy(worldPoint));
1355
+ const ln = this.b.copy(worldNormal).transformDirection(this.m4.copy(mesh.matrixWorld).invert()).normalize();
1356
+ const r = dent.radius / s;
1357
+ const depth = dent.depth / s;
1358
+ const geo = mesh.geometry;
1359
+ if (!geo.boundingBox) geo.computeBoundingBox();
1360
+ if (geo.boundingBox && geo.boundingBox.distanceToPoint(lp) > r) return false;
1361
+ if (!this.ownGeometry(mv)) return false;
1362
+ const work = mv.work;
1363
+ const pos = positionAttr(mesh.geometry);
1364
+ const r2 = r * r;
1365
+ let touched = false;
1366
+ const px = lp.x;
1367
+ const py = lp.y;
1368
+ const pz = lp.z;
1369
+ const nx = ln.x;
1370
+ const ny = ln.y;
1371
+ const nz = ln.z;
1372
+ for (let i = 0; i < work.length; i += 3) {
1373
+ const dx = work[i] - px;
1374
+ const dy = work[i + 1] - py;
1375
+ const dz = work[i + 2] - pz;
1376
+ const d2 = dx * dx + dy * dy + dz * dz;
1377
+ if (d2 >= r2) continue;
1378
+ const f = 1 - d2 / r2;
1379
+ const k = depth * f * f;
1380
+ work[i] -= nx * k;
1381
+ work[i + 1] -= ny * k;
1382
+ work[i + 2] -= nz * k;
1383
+ touched = true;
1384
+ }
1385
+ if (!touched) return false;
1386
+ this.writePositions(mv, pos);
1387
+ return true;
1388
+ }
1389
+ /** Copy `work` (dents) into the geometry, adding the melt sag when the mesh is melting. */
1390
+ writePositions(mv, pos) {
1391
+ const work = mv.work;
1392
+ const arr = pos.array;
1393
+ const melt = mv.meltApplied;
1394
+ if (melt <= 0 || this.lowQuality && work.length / 3 > 2e4) {
1395
+ arr.set(work);
1396
+ } else {
1397
+ const geo2 = mv.mesh.geometry;
1398
+ if (!geo2.boundingBox) geo2.computeBoundingBox();
1399
+ const bb = geo2.boundingBox;
1400
+ const h = Math.max(1e-4, bb.max.y - bb.min.y);
1401
+ const minY = bb.min.y;
1402
+ for (let i = 0, v = 0; i < work.length; i += 3, v++) {
1403
+ const n = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
1404
+ const n2 = (Math.imul(v + 7, 40503) >>> 0) % 1e3 / 1e3;
1405
+ const lowness = 1 - (work[i + 1] - minY) / h;
1406
+ const spread = 1 + 0.12 * melt * (0.4 + n2) * (0.3 + lowness);
1407
+ arr[i] = work[i] * spread;
1408
+ arr[i + 2] = work[i + 2] * spread;
1409
+ arr[i + 1] = Math.max(minY, work[i + 1] - n * melt * h * 0.08);
1410
+ }
1411
+ }
1412
+ pos.needsUpdate = true;
1413
+ const geo = mv.mesh.geometry;
1414
+ geo.computeVertexNormals();
1415
+ geo.boundingBox = null;
1416
+ geo.boundingSphere = null;
1417
+ }
1418
+ // ------------------------------------------------------------------------------------------------------------------------------------
1419
+ // Decals
1420
+ // ------------------------------------------------------------------------------------------------------------------------------------
1421
+ /** Decal pose in mesh space: position and a rotation taking +Z to the surface normal. */
1422
+ decalPose(mesh, worldPoint, worldNormal, lift) {
1423
+ mesh.updateWorldMatrix(true, false);
1424
+ const s = mesh.matrixWorld.getMaxScaleOnAxis() || 1;
1425
+ const ln = worldNormal.clone().transformDirection(this.m4.copy(mesh.matrixWorld).invert()).normalize();
1426
+ const pos = mesh.worldToLocal(worldPoint.clone()).addScaledVector(ln, lift / s);
1427
+ const quat = new this.THREE.Quaternion().setFromUnitVectors(new this.THREE.Vector3(0, 0, 1), ln);
1428
+ return { s, pos, quat };
1429
+ }
1430
+ addDecal(mv, decal) {
1431
+ mv.mesh.add(decal.obj);
1432
+ mv.decals.push(decal);
1433
+ this.decalCount++;
1434
+ while (mv.decals.length > 48 || this.decalCount > this.decalCap) {
1435
+ const old = mv.decals.shift();
1436
+ if (!old) break;
1437
+ this.disposeDecal(old);
1438
+ }
1439
+ }
1440
+ disposeDecal(d) {
1441
+ d.obj.parent?.remove(d.obj);
1442
+ d.geometry?.dispose();
1443
+ this.decalCount = Math.max(0, this.decalCount - 1);
1444
+ }
1445
+ addHole(mv, worldPoint, worldNormal, diameter) {
1446
+ const { s, pos, quat } = this.decalPose(mv.mesh, worldPoint, worldNormal, 25e-4);
1447
+ const disc = new this.THREE.Mesh(this.kit.discGeometry(), this.kit.discMaterial());
1448
+ disc.position.copy(pos);
1449
+ disc.quaternion.copy(quat);
1450
+ disc.scale.setScalar(Math.max(4e-3, diameter) * 0.5 * 1.25 / s);
1451
+ disc.renderOrder = 2;
1452
+ this.addDecal(mv, { obj: disc });
1453
+ }
1454
+ /** A star of jagged lines: `rays` rays up to `length` metres, seeded so the same event always draws the same star. */
1455
+ addCrack(mv, worldPoint, worldNormal, rays, length, color, seed) {
1456
+ const { s, pos, quat } = this.decalPose(mv.mesh, worldPoint, worldNormal, 2e-3);
1457
+ const rnd = createRng(seed);
1458
+ const verts = [];
1459
+ const n = clamp(Math.round(rays), 3, 16);
1460
+ const L = Math.max(0.02, length) / s;
1461
+ for (let r = 0; r < n; r++) {
1462
+ let ang = r / n * Math.PI * 2 + (rnd() - 0.5) * 0.5;
1463
+ let x = 0;
1464
+ let y = 0;
1465
+ const segs = 3;
1466
+ const total = L * (0.6 + 0.4 * rnd());
1467
+ for (let k = 0; k < segs; k++) {
1468
+ const step = total / segs * (0.7 + 0.6 * rnd());
1469
+ ang += (rnd() - 0.5) * 0.45;
1470
+ const nx = x + Math.cos(ang) * step;
1471
+ const ny = y + Math.sin(ang) * step;
1472
+ verts.push(x, y, 0, nx, ny, 0);
1473
+ if (k > 0 && rnd() < 0.5) {
1474
+ const ba = ang + (rnd() < 0.5 ? -1 : 1) * (0.5 + rnd() * 0.5);
1475
+ const bl = step * (0.6 + 0.6 * rnd());
1476
+ verts.push(nx, ny, 0, nx + Math.cos(ba) * bl, ny + Math.sin(ba) * bl, 0);
1477
+ }
1478
+ x = nx;
1479
+ y = ny;
1480
+ }
1481
+ }
1482
+ const ringR = L * 0.12;
1483
+ for (let k = 0; k < 8; k++) {
1484
+ const a0 = k / 8 * Math.PI * 2;
1485
+ const a1 = (k + 1) / 8 * Math.PI * 2;
1486
+ if (rnd() < 0.75) verts.push(Math.cos(a0) * ringR, Math.sin(a0) * ringR, 0, Math.cos(a1) * ringR, Math.sin(a1) * ringR, 0);
1487
+ }
1488
+ const geo = new this.THREE.BufferGeometry();
1489
+ geo.setAttribute("position", new this.THREE.Float32BufferAttribute(verts, 3));
1490
+ const lines = new this.THREE.LineSegments(geo, this.kit.lineMaterial(color));
1491
+ lines.position.copy(pos);
1492
+ lines.quaternion.copy(quat);
1493
+ lines.renderOrder = 2;
1494
+ this.addDecal(mv, { obj: lines, geometry: geo });
1495
+ }
1496
+ // ------------------------------------------------------------------------------------------------------------------------------------
1497
+ // Char and heat glow
1498
+ // ------------------------------------------------------------------------------------------------------------------------------------
1499
+ setChar(pv, char, charColor) {
1500
+ if (Math.abs(char - pv.charApplied) < 4e-3) return;
1501
+ pv.charApplied = char;
1502
+ const target = new this.THREE.Color(charColor);
1503
+ for (const mv of pv.meshes) {
1504
+ this.ownMaterial(mv);
1505
+ const mats = materialsOf(mv.mesh.material);
1506
+ mats.forEach((m, i) => {
1507
+ const c = colorOf(m);
1508
+ const base = mv.baseColors?.[i];
1509
+ if (c && base) c.copy(base).lerp(target, clamp(char, 0, 1));
1510
+ });
1511
+ }
1512
+ }
1513
+ /** Emissive glow of a hot part. `temperature` below the glow threshold switches it off. */
1514
+ setGlow(pv, temperatureC) {
1515
+ const hex = heatColor(temperatureC);
1516
+ const key = hex === 0 ? 0 : Math.round(temperatureC / 5) * 5;
1517
+ if (key === pv.glowApplied) return;
1518
+ pv.glowApplied = key;
1519
+ const intensity = hex === 0 ? 0 : clamp(0.35 + (temperatureC - 450) / 900, 0, 1.4);
1520
+ for (const mv of pv.meshes) {
1521
+ this.ownMaterial(mv);
1522
+ for (const m of materialsOf(mv.mesh.material)) {
1523
+ const e = m.emissive;
1524
+ if (e && e.isColor) {
1525
+ e.setHex(hex);
1526
+ m.emissiveIntensity = intensity;
1527
+ }
1528
+ }
1529
+ }
1530
+ }
1531
+ // ------------------------------------------------------------------------------------------------------------------------------------
1532
+ // Melt
1533
+ // ------------------------------------------------------------------------------------------------------------------------------------
1534
+ /** Sag the part: shorter in Y (down to 25 %), a bit wider, vertices pushed down with noise, the bottom stays put. */
1535
+ setMelt(pv, melt) {
1536
+ const m = clamp(melt, 0, 1);
1537
+ if (Math.abs(m - pv.meltApplied) < 0.01) return;
1538
+ pv.meltApplied = m;
1539
+ for (const mv of pv.meshes) {
1540
+ const mesh = mv.mesh;
1541
+ if (mv.meltApplied === 0 && m > 0) {
1542
+ mesh.updateWorldMatrix(true, false);
1543
+ mv.bottomY = this.box.setFromObject(mesh).min.y;
1544
+ }
1545
+ mv.meltApplied = m;
1546
+ mv.moved = true;
1547
+ const sy = lerp(1, 0.25, m);
1548
+ const sxz = 1 + 0.18 * m;
1549
+ mesh.scale.set(mv.origScale.x * sxz, mv.origScale.y * sy, mv.origScale.z * sxz);
1550
+ mesh.position.copy(mv.origPosition);
1551
+ mesh.updateWorldMatrix(true, false);
1552
+ const bottom = this.box.setFromObject(mesh).min.y;
1553
+ const dy = mv.bottomY - bottom;
1554
+ if (Math.abs(dy) > 1e-6 && mesh.parent) {
1555
+ mesh.parent.updateWorldMatrix(true, false);
1556
+ const inv = this.m4.copy(mesh.parent.matrixWorld).invert();
1557
+ this.a.set(0, dy, 0).applyMatrix4(inv);
1558
+ this.b.set(0, 0, 0).applyMatrix4(inv);
1559
+ mesh.position.copy(mv.origPosition).add(this.a.sub(this.b));
1560
+ }
1561
+ if (!(this.lowQuality && this.vertexCount(mv) > 2e4) && this.ownGeometry(mv)) {
1562
+ this.writePositions(mv, positionAttr(mesh.geometry));
1563
+ }
1564
+ }
1565
+ }
1566
+ // ------------------------------------------------------------------------------------------------------------------------------------
1567
+ // Visibility and restore
1568
+ // ------------------------------------------------------------------------------------------------------------------------------------
1569
+ hide(pv, reason) {
1570
+ pv.gone = reason;
1571
+ for (const mv of pv.meshes) mv.mesh.visible = false;
1572
+ }
1573
+ /** Bring one mesh back to how it was when it was bound. */
1574
+ restoreMesh(mv) {
1575
+ const mesh = mv.mesh;
1576
+ for (const d of mv.decals) this.disposeDecal(d);
1577
+ mv.decals.length = 0;
1578
+ if (mv.ownGeo) {
1579
+ mesh.geometry.dispose();
1580
+ mesh.geometry = mv.origGeometry;
1581
+ mv.ownGeo = false;
1582
+ mv.work = null;
1583
+ }
1584
+ if (mv.ownMat) {
1585
+ for (const m of materialsOf(mesh.material)) m.dispose();
1586
+ mesh.material = mv.origMaterial;
1587
+ mv.ownMat = false;
1588
+ mv.baseColors = null;
1589
+ }
1590
+ mesh.visible = mv.origVisible;
1591
+ if (mv.origParent && mesh.parent !== mv.origParent) {
1592
+ mv.origParent.add(mesh);
1593
+ mv.moved = true;
1594
+ }
1595
+ if (mv.moved) {
1596
+ mesh.position.copy(mv.origPosition);
1597
+ mesh.quaternion.copy(mv.origQuaternion);
1598
+ mesh.scale.copy(mv.origScale);
1599
+ mv.moved = false;
1600
+ }
1601
+ mv.meltApplied = 0;
1602
+ }
1603
+ restorePart(pv) {
1604
+ for (const mv of pv.meshes) this.restoreMesh(mv);
1605
+ pv.charApplied = 0;
1606
+ pv.glowApplied = 0;
1607
+ pv.meltApplied = 0;
1608
+ pv.gone = "none";
1609
+ }
1610
+ dispose() {
1611
+ this.kit.dispose();
1612
+ }
1613
+ };
1614
+
1615
+ // src/renderer.ts
1616
+ var GLOW_FROM_C = 430;
1617
+ var QUEUE_CAP = 4e3;
1618
+ var FLUID_COLORS = {
1619
+ water: 9226495,
1620
+ fuel: 14262322,
1621
+ oil: 2760728,
1622
+ gas: 15921906,
1623
+ sap: 12098362,
1624
+ acid: 10211900,
1625
+ blood: 9046538
1626
+ };
1627
+ var SMOKE_TONES = { white: 15132390, grey: 9079434, black: 2763306 };
1628
+ var DEFAULT_SPEED = {
1629
+ sparks: 3,
1630
+ splinters: 2.5,
1631
+ dust: 1,
1632
+ shards: 2.5,
1633
+ fibers: 1.2,
1634
+ leaves: 1.2,
1635
+ drips: 0.5,
1636
+ ash: 0.8,
1637
+ chips: 2.5,
1638
+ smoke: 0.8,
1639
+ steam: 1,
1640
+ fire: 1.2,
1641
+ flash: 0,
1642
+ foam: 2.5
1643
+ };
1644
+ function impactKey(def) {
1645
+ if (!def) return "impact_wood";
1646
+ const tags = def.tags ?? [];
1647
+ if (tags.includes("metal")) return "impact_metal";
1648
+ if (def.visual?.transparent || def.fracture === "shatter") return "impact_glass";
1649
+ if (def.fracture === "crumble") return "impact_masonry";
1650
+ if (def.fracture === "tear") return "impact_soft";
1651
+ return def.fracture === "none" ? "impact_metal" : "impact_wood";
1652
+ }
1653
+ function fractureKey(def, style) {
1654
+ switch (style) {
1655
+ case "shatter":
1656
+ return "shatter_glass";
1657
+ case "crumble":
1658
+ return "crumble";
1659
+ case "splinter":
1660
+ return "splinter";
1661
+ default:
1662
+ return impactKey(def);
1663
+ }
1664
+ }
1665
+ function fallbackColor(id, def) {
1666
+ if (def?.tags?.includes("metal")) return 9080726;
1667
+ if (def?.visual?.transparent) return 10473696;
1668
+ const h = hashString(id);
1669
+ return mixHex(7035464, 10130568, h % 100 / 100);
1670
+ }
1671
+ function materialsOf2(m) {
1672
+ return Array.isArray(m) ? m : [m];
1673
+ }
1674
+ function createDestructionRenderer(THREE, options) {
1675
+ return new Renderer(THREE, options);
1676
+ }
1677
+ var Renderer = class {
1678
+ constructor(THREE, o) {
1679
+ __publicField(this, "THREE", THREE);
1680
+ __publicField(this, "o", o);
1681
+ __publicField(this, "world");
1682
+ __publicField(this, "scene");
1683
+ __publicField(this, "quality");
1684
+ __publicField(this, "low");
1685
+ __publicField(this, "groundAt");
1686
+ __publicField(this, "gravity");
1687
+ __publicField(this, "autoSplit");
1688
+ __publicField(this, "maxFragments");
1689
+ __publicField(this, "bounds", /* @__PURE__ */ new Map());
1690
+ __publicField(this, "queue", []);
1691
+ __publicField(this, "unsubscribe");
1692
+ __publicField(this, "visuals");
1693
+ __publicField(this, "particles");
1694
+ __publicField(this, "fragments");
1695
+ __publicField(this, "ground");
1696
+ __publicField(this, "pieceSys");
1697
+ __publicField(this, "active", /* @__PURE__ */ new Map());
1698
+ __publicField(this, "fires", /* @__PURE__ */ new Map());
1699
+ __publicField(this, "leaks", []);
1700
+ __publicField(this, "homeTags", []);
1701
+ __publicField(this, "throttles", /* @__PURE__ */ new Map());
1702
+ __publicField(this, "rng", createRng(12648430));
1703
+ __publicField(this, "light", null);
1704
+ __publicField(this, "lightT", 0);
1705
+ __publicField(this, "lightDur", 0.3);
1706
+ __publicField(this, "lightPeak", 0);
1707
+ __publicField(this, "time", 0);
1708
+ __publicField(this, "scanTimer", 0);
1709
+ __publicField(this, "disposed", false);
1710
+ __publicField(this, "box");
1711
+ __publicField(this, "tmpQ");
1712
+ __publicField(this, "v1");
1713
+ __publicField(this, "v2");
1714
+ this.world = o.world;
1715
+ this.scene = o.scene;
1716
+ this.quality = o.quality ?? "medium";
1717
+ this.low = this.quality === "low";
1718
+ this.groundAt = o.groundAt ?? (() => 0);
1719
+ this.gravity = o.gravity ?? 9.81;
1720
+ this.autoSplit = o.autoSplit !== false;
1721
+ this.maxFragments = o.maxFragments ?? 160;
1722
+ this.box = new THREE.Box3();
1723
+ this.tmpQ = new THREE.Quaternion();
1724
+ this.v1 = new THREE.Vector3();
1725
+ this.v2 = new THREE.Vector3();
1726
+ const maxParticles = Math.max(8, Math.floor((o.maxParticles ?? 2500) * (this.low ? 0.5 : 1)));
1727
+ this.visuals = new Visuals(THREE, this.low, 600);
1728
+ this.ground = new GroundSpots(THREE, o.scene, this.groundAt, 48);
1729
+ this.particles = new ParticleSystem(THREE, {
1730
+ capacity: maxParticles,
1731
+ scene: o.scene,
1732
+ density: this.low ? 0.6 : 1,
1733
+ pointScale: o.pointScale ?? 700,
1734
+ groundAt: this.groundAt,
1735
+ gravity: this.gravity,
1736
+ onLand: (x, _y, z, color, tag) => {
1737
+ const home = this.homeTags[tag - 1] ?? "";
1738
+ this.ground.add("puddle", x, z, 0.05 + this.rng() * 0.04, color, home);
1739
+ }
1740
+ });
1741
+ this.fragments = new FragmentSystem(THREE, {
1742
+ scene: o.scene,
1743
+ groundAt: this.groundAt,
1744
+ gravity: this.gravity,
1745
+ max: this.maxFragments,
1746
+ life: o.fragmentLife ?? 12
1747
+ });
1748
+ this.pieceSys = new PieceSystem(THREE, {
1749
+ groundAt: this.groundAt,
1750
+ gravity: this.gravity,
1751
+ scene: o.scene,
1752
+ onHardLanding: (p, point, speed) => this.hardLanding(p, point, speed)
1753
+ });
1754
+ this.unsubscribe = this.world.on("*", (ev) => {
1755
+ if (this.queue.length < QUEUE_CAP) this.queue.push(ev);
1756
+ });
1757
+ }
1758
+ // ----------------------------------------------------------------------------------------------------------------------------------
1759
+ // Binding
1760
+ // ----------------------------------------------------------------------------------------------------------------------------------
1761
+ bind(objectId, root, options = {}) {
1762
+ if (this.bounds.has(objectId)) this.unbind(objectId);
1763
+ const state = this.world.state(objectId);
1764
+ const partIds = new Set((state?.parts ?? []).map((p) => p.id));
1765
+ const byPart = /* @__PURE__ */ new Map();
1766
+ const taken = /* @__PURE__ */ new Set();
1767
+ const add = (pid, m) => {
1768
+ if (taken.has(m)) return;
1769
+ taken.add(m);
1770
+ const l = byPart.get(pid);
1771
+ if (l) l.push(m);
1772
+ else byPart.set(pid, [m]);
1773
+ };
1774
+ const meshesIn = (o) => {
1775
+ const out = [];
1776
+ o.traverse((c) => {
1777
+ if (c.isMesh) out.push(c);
1778
+ });
1779
+ return out;
1780
+ };
1781
+ if (options.parts) {
1782
+ for (const [pid, v] of Object.entries(options.parts)) {
1783
+ for (const o of Array.isArray(v) ? v : [v]) for (const m of meshesIn(o)) add(pid, m);
1784
+ }
1785
+ } else {
1786
+ const leftovers = [];
1787
+ for (const m of meshesIn(root)) {
1788
+ const ud = partHintOf(m.userData?.destructionPart)?.id;
1789
+ if (ud !== void 0 && partIds.has(ud)) add(ud, m);
1790
+ else if (partIds.has(m.name)) add(m.name, m);
1791
+ else leftovers.push(m);
1792
+ }
1793
+ if (leftovers.length) this.matchByBox(objectId, root, leftovers, add);
1794
+ }
1795
+ const bound = { id: objectId, root, sync: options.syncTransform !== false, parts: /* @__PURE__ */ new Map(), lastPos: null, lastQuat: null, piece: null, isSplit: false };
1796
+ for (const pid of partIds) bound.parts.set(pid, this.makePartVis(objectId, pid, state?.parts.find((p) => p.id === pid)?.material ?? "", byPart.get(pid) ?? []));
1797
+ for (const [pid, meshes] of byPart) {
1798
+ if (!bound.parts.has(pid)) bound.parts.set(pid, this.makePartVis(objectId, pid, this.world.part(objectId, pid)?.material ?? "", meshes));
1799
+ }
1800
+ this.bounds.set(objectId, bound);
1801
+ }
1802
+ makePartVis(home, id, material, meshes) {
1803
+ return { id, home, material, meshes: meshes.map((m) => this.visuals.makeMeshVis(m, home)), charApplied: 0, glowApplied: 0, meltApplied: 0, gone: "none" };
1804
+ }
1805
+ /** Match unlabelled meshes to the part whose shape contains the centre of the mesh's bounding box. */
1806
+ matchByBox(objectId, root, meshes, add) {
1807
+ let specParts;
1808
+ try {
1809
+ specParts = this.world.serialize?.().objects.find((o) => o.id === objectId)?.spec.parts;
1810
+ } catch {
1811
+ specParts = void 0;
1812
+ }
1813
+ if (!specParts) return;
1814
+ root.updateMatrixWorld(true);
1815
+ const e = new this.THREE.Euler();
1816
+ const q = new this.THREE.Quaternion();
1817
+ for (const m of meshes) {
1818
+ this.box.setFromObject(m);
1819
+ if (this.box.isEmpty()) continue;
1820
+ const c = root.worldToLocal(this.box.getCenter(this.v1));
1821
+ let best = null;
1822
+ let bestVol = Infinity;
1823
+ for (const p of specParts) {
1824
+ const l = this.v2.set(c.x - p.position[0], c.y - p.position[1], c.z - p.position[2]);
1825
+ if (p.rotation) l.applyQuaternion(q.setFromEuler(e.set(p.rotation[0], p.rotation[1], p.rotation[2], "XYZ")).invert());
1826
+ let inside = false;
1827
+ let vol = 0;
1828
+ const s = p.shape;
1829
+ if (s.kind === "box") {
1830
+ 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;
1831
+ vol = s.size[0] * s.size[1] * s.size[2];
1832
+ } else if (s.kind === "cylinder") {
1833
+ inside = Math.hypot(l.x, l.z) <= s.radius && Math.abs(l.y) <= s.height / 2;
1834
+ vol = Math.PI * s.radius * s.radius * s.height;
1835
+ } else {
1836
+ inside = l.length() <= s.radius;
1837
+ vol = 4 / 3 * Math.PI * s.radius ** 3;
1838
+ }
1839
+ if (inside && vol < bestVol) {
1840
+ best = p;
1841
+ bestVol = vol;
1842
+ }
1843
+ }
1844
+ if (best) add(best.id, m);
1845
+ }
1846
+ }
1847
+ unbind(objectId) {
1848
+ const b = this.bounds.get(objectId);
1849
+ if (!b) return;
1850
+ this.restore(objectId);
1851
+ this.pieceSys.remove(objectId);
1852
+ this.bounds.delete(objectId);
1853
+ }
1854
+ // ----------------------------------------------------------------------------------------------------------------------------------
1855
+ // Pieces
1856
+ // ----------------------------------------------------------------------------------------------------------------------------------
1857
+ /** Half extents and centre of the box around some meshes, in the local space of `root`. */
1858
+ measure(root, meshes) {
1859
+ root.updateMatrixWorld(true);
1860
+ const inv = root.matrixWorld.clone().invert();
1861
+ const total = new this.THREE.Box3();
1862
+ const m4 = new this.THREE.Matrix4();
1863
+ for (const m of meshes) {
1864
+ const g = m.geometry;
1865
+ if (!g.boundingBox) g.computeBoundingBox();
1866
+ if (!g.boundingBox || g.boundingBox.isEmpty()) continue;
1867
+ total.union(g.boundingBox.clone().applyMatrix4(m4.multiplyMatrices(inv, m.matrixWorld)));
1868
+ }
1869
+ if (total.isEmpty()) return { half: [0.25, 0.25, 0.25], center: new this.THREE.Vector3() };
1870
+ const size = total.getSize(new this.THREE.Vector3());
1871
+ 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()) };
1872
+ }
1873
+ meshesOf(b) {
1874
+ const out = [];
1875
+ for (const pv of b.parts.values()) if (pv.gone === "none") for (const mv of pv.meshes) out.push(mv.mesh);
1876
+ return out;
1877
+ }
1878
+ massOf(objectId) {
1879
+ const st = this.world.state(objectId);
1880
+ if (!st) return 5;
1881
+ let m = 0;
1882
+ for (const p of st.parts) if (p.status !== "broken" && p.status !== "melted" && p.status !== "ashes") m += p.mass;
1883
+ return m > 0 ? m : 5;
1884
+ }
1885
+ makePiece(objectId, options = {}) {
1886
+ const b = this.bounds.get(objectId);
1887
+ if (!b) throw new Error(`game-destruction-three: object "${objectId}" is not bound`);
1888
+ if (b.root.parent && b.root.parent !== this.scene) this.scene.attach(b.root);
1889
+ const { half, center } = this.measure(b.root, this.meshesOf(b));
1890
+ const piece = this.pieceSys.create(objectId, b.root, half, center, options.mass ?? this.massOf(objectId), options.velocity);
1891
+ b.piece = piece;
1892
+ return piece;
1893
+ }
1894
+ pieces() {
1895
+ return [...this.pieceSys.list];
1896
+ }
1897
+ piece(objectId) {
1898
+ return this.pieceSys.get(objectId);
1899
+ }
1900
+ impulse(objectId, impulse, point) {
1901
+ const p = this.pieceSys.get(objectId);
1902
+ if (p) this.pieceSys.impulse(p, impulse, point);
1903
+ }
1904
+ hardLanding(p, point, speed) {
1905
+ const key = this.impactSoundOf(p.objectId);
1906
+ this.sound(key, point, clamp(speed / 10, 0.2, 1));
1907
+ this.syncBound(p.objectId);
1908
+ try {
1909
+ this.world.hit(p.objectId, { type: "blunt", point, energy: 0.5 * p.mass * speed * speed, radius: 0.08, direction: [0, -1, 0], source: "fall" });
1910
+ } catch {
1911
+ }
1912
+ }
1913
+ impactSoundOf(objectId) {
1914
+ const st = this.world.state(objectId);
1915
+ let best;
1916
+ let mass = -1;
1917
+ for (const p of st?.parts ?? []) {
1918
+ if (p.mass > mass) {
1919
+ mass = p.mass;
1920
+ best = this.world.materials.get(p.material);
1921
+ }
1922
+ }
1923
+ return impactKey(best);
1924
+ }
1925
+ // ----------------------------------------------------------------------------------------------------------------------------------
1926
+ // Update
1927
+ // ----------------------------------------------------------------------------------------------------------------------------------
1928
+ update(dt) {
1929
+ if (this.disposed) return;
1930
+ dt = clamp(dt, 0, 0.1);
1931
+ this.time += dt;
1932
+ this.drainQueue();
1933
+ this.updateActive(dt);
1934
+ this.updateLeaks(dt);
1935
+ this.pieceSys.update(dt);
1936
+ this.fragments.update(dt);
1937
+ this.ground.update(dt);
1938
+ this.updateLight(dt);
1939
+ this.particles.update(dt);
1940
+ this.syncTransforms();
1941
+ }
1942
+ drainQueue() {
1943
+ const batch = this.queue.splice(0, this.queue.length);
1944
+ for (const ev of batch) {
1945
+ try {
1946
+ this.apply(ev);
1947
+ } catch (err) {
1948
+ if (typeof console !== "undefined") console.error("game-destruction-three: failed to apply", ev.type, err);
1949
+ }
1950
+ }
1951
+ }
1952
+ syncTransforms() {
1953
+ for (const b of this.bounds.values()) this.syncOne(b);
1954
+ }
1955
+ /** Push the current transform of one bound object to the world right now (used before a landing hit). */
1956
+ syncBound(objectId) {
1957
+ const b = this.bounds.get(objectId);
1958
+ if (b) this.syncOne(b);
1959
+ }
1960
+ syncOne(b) {
1961
+ if (!b.sync) return;
1962
+ const p = this.v1;
1963
+ const q = this.tmpQ;
1964
+ b.root.updateWorldMatrix(true, false);
1965
+ b.root.matrixWorld.decompose(p, q, this.v2);
1966
+ const last = b.lastPos;
1967
+ const lq = b.lastQuat;
1968
+ 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;
1969
+ if (!moved) return;
1970
+ b.lastPos = [p.x, p.y, p.z];
1971
+ b.lastQuat = [q.x, q.y, q.z, q.w];
1972
+ 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]);
1973
+ }
1974
+ // ----------------------------------------------------------------------------------------------------------------------------------
1975
+ // Events
1976
+ // ----------------------------------------------------------------------------------------------------------------------------------
1977
+ apply(ev) {
1978
+ switch (ev.type) {
1979
+ case "damage":
1980
+ if (ev.damageType === "heat" || ev.damageType === "fire" || ev.damageType === "explosive") this.activate(ev.object, ev.part);
1981
+ break;
1982
+ case "dent":
1983
+ this.onDent(ev);
1984
+ break;
1985
+ case "hole":
1986
+ this.onHole(ev);
1987
+ break;
1988
+ case "crack":
1989
+ this.onCrack(ev);
1990
+ break;
1991
+ case "ricochet": {
1992
+ const d = ev.direction;
1993
+ this.particles.burst("sparks", ev.point, d, 4 + Math.min(10, ev.energy / 150), 4);
1994
+ this.sound("ricochet", ev.point, 0.7);
1995
+ break;
1996
+ }
1997
+ case "debris":
1998
+ 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));
1999
+ break;
2000
+ case "fracture":
2001
+ this.onFracture(ev);
2002
+ break;
2003
+ case "detach":
2004
+ this.onDetach(ev);
2005
+ break;
2006
+ case "disassemble_progress": {
2007
+ const k = `unscrew:${ev.joint}`;
2008
+ if (this.time - (this.throttles.get(k) ?? -1) > 0.25) {
2009
+ this.throttles.set(k, this.time);
2010
+ this.sound("unscrew", this.rootPoint(ev.object), clamp(0.3 + ev.progress * 0.5, 0, 1));
2011
+ }
2012
+ break;
2013
+ }
2014
+ case "ignite":
2015
+ this.fires.set(`${ev.object}|${ev.part}`, { object: ev.object, part: ev.part, point: ev.point, accFire: 0, accSmoke: 0 });
2016
+ this.activate(ev.object, ev.part);
2017
+ this.particles.burst("fire", ev.point, [0, 1, 0], 6, 1);
2018
+ this.sound("ignite", ev.point, 0.7);
2019
+ this.sound("burn_loop", ev.point, 0.5);
2020
+ break;
2021
+ case "extinguish": {
2022
+ const f = this.fires.get(`${ev.object}|${ev.part}`);
2023
+ if (f) {
2024
+ this.fires.delete(`${ev.object}|${ev.part}`);
2025
+ this.particles.burst("steam", f.point, [0, 1, 0], 14, 1.2);
2026
+ }
2027
+ this.activate(ev.object, ev.part);
2028
+ this.sound("extinguish", f?.point ?? this.rootPoint(ev.object), 0.6);
2029
+ break;
2030
+ }
2031
+ case "burnout": {
2032
+ this.fires.delete(`${ev.object}|${ev.part}`);
2033
+ this.particles.burst("ash", ev.point, [0, 1, 0], 16, 1);
2034
+ this.particles.burst("smoke", ev.point, [0, 1, 0], 6, 0.8, { color: SMOKE_TONES.grey });
2035
+ const pv = this.partVis(ev.object, ev.part);
2036
+ if (pv) {
2037
+ const def = this.world.materials.get(pv.material);
2038
+ this.visuals.setChar(pv, 1, def?.visual?.charColor ?? 1709072);
2039
+ }
2040
+ this.activate(ev.object, ev.part);
2041
+ break;
2042
+ }
2043
+ case "melt":
2044
+ this.activate(ev.object, ev.part);
2045
+ if (this.time - (this.throttles.get(`melt:${ev.object}|${ev.part}`) ?? -9) > 1.5) {
2046
+ this.throttles.set(`melt:${ev.object}|${ev.part}`, this.time);
2047
+ this.sound("melt_sizzle", this.rootPoint(ev.object), 0.4);
2048
+ }
2049
+ break;
2050
+ case "drip": {
2051
+ const b = this.bounds.get(ev.object);
2052
+ const pv = b?.parts.get(ev.part);
2053
+ const def = this.world.materials.get(ev.material);
2054
+ const color = (pv && this.partColor(pv, def)) ?? fallbackColor(ev.material, def);
2055
+ let tag = this.homeTags.indexOf(ev.object) + 1;
2056
+ if (tag === 0) tag = this.homeTags.push(ev.object);
2057
+ this.particles.spawn("drips", ev.point, [(this.rng() - 0.5) * 0.1, -0.1, (this.rng() - 0.5) * 0.1], { color, tag });
2058
+ break;
2059
+ }
2060
+ case "leak": {
2061
+ const key = `${ev.object}|${ev.part}`;
2062
+ 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);
2063
+ if (same) {
2064
+ same.rate = ev.rate;
2065
+ same.direction = ev.direction;
2066
+ } else {
2067
+ if (this.leaks.length >= 24) this.leaks.shift();
2068
+ 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 });
2069
+ }
2070
+ void key;
2071
+ break;
2072
+ }
2073
+ case "explode":
2074
+ this.onExplode(ev);
2075
+ break;
2076
+ case "destroyed":
2077
+ break;
2078
+ }
2079
+ }
2080
+ rootPoint(objectId) {
2081
+ const b = this.bounds.get(objectId);
2082
+ return b ? [b.root.position.x, b.root.position.y, b.root.position.z] : [0, 0, 0];
2083
+ }
2084
+ sound(key, point, volume) {
2085
+ this.o.onSound?.(key, point, volume);
2086
+ }
2087
+ partVis(objectId, partId) {
2088
+ return this.bounds.get(objectId)?.parts.get(partId) ?? null;
2089
+ }
2090
+ activate(objectId, partId) {
2091
+ this.active.set(`${objectId}|${partId}`, { object: objectId, part: partId });
2092
+ }
2093
+ /** Current colour of the part's first mesh (char included), or null. */
2094
+ partColor(pv, def) {
2095
+ const mv = pv.meshes[0];
2096
+ if (!mv) return null;
2097
+ const mat = materialsOf2(mv.mesh.material)[0];
2098
+ const c = mat?.color;
2099
+ if (c && c.isColor) {
2100
+ const tint = this.visuals.vertexTint(mv);
2101
+ return tint ? c.clone().multiply(tint).getHex() : c.getHex();
2102
+ }
2103
+ return def ? fallbackColor(def.id, def) : null;
2104
+ }
2105
+ debrisOptions(kind, objectId, partId) {
2106
+ const pv = this.partVis(objectId, partId);
2107
+ const def = pv ? this.world.materials.get(pv.material) : void 0;
2108
+ if (kind === "smoke") return { color: SMOKE_TONES[def?.thermal.smoke ?? "grey"] };
2109
+ if (kind === "fibers" || kind === "leaves") {
2110
+ const c = pv ? this.visuals.baseColorHex(pv.meshes[0] ?? {}) : -1;
2111
+ return c >= 0 ? { color: c } : void 0;
2112
+ }
2113
+ if (kind === "dust" || kind === "chips") {
2114
+ const c = pv && pv.meshes[0] ? this.visuals.baseColorHex(pv.meshes[0]) : -1;
2115
+ return c >= 0 && kind === "chips" ? { color: c } : void 0;
2116
+ }
2117
+ return void 0;
2118
+ }
2119
+ /** Mesh of the part closest to a world point. */
2120
+ nearestMesh(pv, p) {
2121
+ let best = null;
2122
+ let bd = Infinity;
2123
+ for (const mv of pv.meshes) {
2124
+ if (!mv.mesh.visible) continue;
2125
+ const d = this.box.setFromObject(mv.mesh).distanceToPoint(p);
2126
+ if (d < bd) {
2127
+ bd = d;
2128
+ best = mv;
2129
+ }
2130
+ }
2131
+ return best;
2132
+ }
2133
+ worldDir(b, v) {
2134
+ b.root.updateWorldMatrix(true, false);
2135
+ return new this.THREE.Vector3(v[0], v[1], v[2]).applyQuaternion(b.root.getWorldQuaternion(this.tmpQ)).normalize();
2136
+ }
2137
+ onDent(ev) {
2138
+ const b = this.bounds.get(ev.object);
2139
+ const pv = b?.parts.get(ev.part);
2140
+ if (!b || !pv) return;
2141
+ const def = this.world.materials.get(pv.material);
2142
+ const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
2143
+ const wn = this.worldDir(b, ev.dent.normal);
2144
+ for (const mv of pv.meshes) {
2145
+ if (!mv.mesh.visible) continue;
2146
+ this.visuals.applyDent(mv, wp, wn, ev.dent);
2147
+ }
2148
+ 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);
2149
+ this.sound(impactKey(def), ev.worldPoint, clamp(0.3 + ev.dent.depth * 12, 0.3, 1));
2150
+ }
2151
+ onHole(ev) {
2152
+ const b = this.bounds.get(ev.object);
2153
+ const pv = b?.parts.get(ev.part);
2154
+ if (!b || !pv) return;
2155
+ const def = this.world.materials.get(pv.material);
2156
+ const n = this.worldDir(b, ev.hole.normal);
2157
+ const draw = (point, normal) => {
2158
+ const wp = new this.THREE.Vector3(point[0], point[1], point[2]);
2159
+ const mv = this.nearestMesh(pv, wp);
2160
+ if (!mv) return;
2161
+ if (def?.visual?.transparent) {
2162
+ const seed = hashNumbers(ev.time, point[0], point[1], point[2]);
2163
+ this.visuals.addCrack(mv, wp, normal, 7, Math.max(0.05, ev.hole.diameter * 5), 15267071, seed);
2164
+ } else {
2165
+ this.visuals.addHole(mv, wp, normal, ev.hole.diameter);
2166
+ }
2167
+ };
2168
+ draw(ev.worldPoint, n);
2169
+ if (ev.hole.through && ev.exitPoint) draw(ev.exitPoint, n.clone().negate());
2170
+ if (def?.tags?.includes("metal")) this.particles.burst("sparks", ev.worldPoint, [n.x, n.y, n.z], 6, 3.5);
2171
+ this.sound(impactKey(def), ev.worldPoint, 0.6);
2172
+ }
2173
+ onCrack(ev) {
2174
+ const b = this.bounds.get(ev.object);
2175
+ const pv = b?.parts.get(ev.part);
2176
+ if (!b || !pv) return;
2177
+ const def = this.world.materials.get(pv.material);
2178
+ const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
2179
+ const mv = this.nearestMesh(pv, wp);
2180
+ if (mv) {
2181
+ const color = def?.visual?.transparent ? 15267071 : 2762532;
2182
+ 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]));
2183
+ }
2184
+ this.sound(impactKey(def), ev.worldPoint, 0.5);
2185
+ }
2186
+ onFracture(ev) {
2187
+ const b = this.bounds.get(ev.object);
2188
+ const pv = b?.parts.get(ev.part);
2189
+ const matId = pv?.material || (ev.fragments[0]?.material ?? "");
2190
+ const def = this.world.materials.get(matId);
2191
+ let color = fallbackColor(matId, def);
2192
+ let opacity = 0.5;
2193
+ const transparent = def?.visual?.transparent === true;
2194
+ if (pv) {
2195
+ const c = this.partColor(pv, def);
2196
+ if (c !== null) color = c;
2197
+ const m = pv.meshes[0] ? materialsOf2(pv.meshes[0].origMaterial)[0] : void 0;
2198
+ if (m && m.transparent) opacity = clamp(m.opacity, 0.2, 0.8);
2199
+ this.visuals.hide(pv, "broken");
2200
+ }
2201
+ const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
2202
+ const step = this.low ? 2 : 1;
2203
+ for (let i = 0; i < ev.fragments.length; i += step) {
2204
+ const fr = ev.fragments[i];
2205
+ const fdef = this.world.materials.get(fr.material);
2206
+ const mat = this.fragments.materialFor(fr.material, fdef === def || !fdef ? color : fallbackColor(fr.material, fdef), transparent || fdef?.visual?.transparent === true, opacity);
2207
+ this.fragments.spawn(fr, mat, ev.object, maxAlive);
2208
+ }
2209
+ const up = [0, 1, 0];
2210
+ const sizeK = clamp(ev.fragments.length / 10, 0.5, 2);
2211
+ switch (ev.style) {
2212
+ case "shatter":
2213
+ this.particles.burst("shards", ev.point, up, 18 * sizeK, 3);
2214
+ break;
2215
+ case "crumble":
2216
+ this.particles.burst("dust", ev.point, up, 16 * sizeK, 1.2, { color: pv ? this.partColor(pv, def) ?? void 0 : void 0 });
2217
+ this.particles.burst("chips", ev.point, up, 6, 3);
2218
+ break;
2219
+ case "splinter":
2220
+ this.particles.burst("splinters", ev.point, up, 14 * sizeK, 3);
2221
+ this.particles.burst("dust", ev.point, up, 5, 0.8, { color: 11901556 });
2222
+ break;
2223
+ case "tear":
2224
+ this.particles.burst("fibers", ev.point, up, 12 * sizeK, 1.6, { color });
2225
+ break;
2226
+ case "snap":
2227
+ this.particles.burst("chips", ev.point, up, 8 * sizeK, 3);
2228
+ break;
2229
+ default:
2230
+ this.particles.burst("sparks", ev.point, up, 10, 4);
2231
+ }
2232
+ this.sound(fractureKey(def, ev.style), ev.point, clamp(0.4 + ev.fragments.length * 0.03, 0.4, 1));
2233
+ }
2234
+ onDetach(ev) {
2235
+ this.sound("detach", this.rootPoint(ev.object), 0.5);
2236
+ if (!this.autoSplit) return;
2237
+ const b = this.bounds.get(ev.object);
2238
+ if (!b) return;
2239
+ const st = this.world.state(ev.object);
2240
+ if (!st || st.groups.length < 2) return;
2241
+ const loose = st.groups.length - 1;
2242
+ for (let i = 0; i < loose; i++) {
2243
+ let newId;
2244
+ try {
2245
+ newId = this.world.splitGroup(ev.object, 1);
2246
+ } catch {
2247
+ break;
2248
+ }
2249
+ this.adoptSplit(b, newId);
2250
+ }
2251
+ const old = b.piece;
2252
+ if (old) {
2253
+ const { half, center } = this.measure(b.root, this.meshesOf(b));
2254
+ old.halfExtents = half;
2255
+ this.pieceSys.setCenter(old, center);
2256
+ old.mass = Math.max(0.05, this.massOf(ev.object));
2257
+ old.sleeping = false;
2258
+ }
2259
+ }
2260
+ /** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
2261
+ adoptSplit(from, newId) {
2262
+ const state = this.world.state(newId);
2263
+ const root = from.root;
2264
+ root.updateWorldMatrix(true, false);
2265
+ const group = new this.THREE.Group();
2266
+ group.name = `destruction-piece-${newId}`;
2267
+ root.getWorldPosition(group.position);
2268
+ root.getWorldQuaternion(group.quaternion);
2269
+ this.scene.add(group);
2270
+ group.updateMatrixWorld(true);
2271
+ const nb = { id: newId, root: group, sync: true, parts: /* @__PURE__ */ new Map(), lastPos: null, lastQuat: null, piece: null, isSplit: true };
2272
+ for (const p of state?.parts ?? []) {
2273
+ const pv = from.parts.get(p.id);
2274
+ if (!pv) continue;
2275
+ from.parts.delete(p.id);
2276
+ nb.parts.set(p.id, pv);
2277
+ for (const mv of pv.meshes) {
2278
+ group.attach(mv.mesh);
2279
+ mv.moved = true;
2280
+ }
2281
+ }
2282
+ this.bounds.set(newId, nb);
2283
+ const meshes = this.meshesOf(nb);
2284
+ if (meshes.length === 0) return;
2285
+ const { half, center } = this.measure(group, meshes);
2286
+ let vel = [0, 0, 0];
2287
+ let ang = [0, 0, 0];
2288
+ const op = from.piece;
2289
+ if (op) {
2290
+ vel = [...op.velocity];
2291
+ ang = [...op.angularVelocity];
2292
+ const c = this.pieceSys.centerOf(op, new this.THREE.Vector3());
2293
+ const nc = center.clone().applyQuaternion(group.quaternion).add(group.position);
2294
+ vel[0] += ang[1] * (nc.z - c.z) - ang[2] * (nc.y - c.y);
2295
+ vel[1] += ang[2] * (nc.x - c.x) - ang[0] * (nc.z - c.z);
2296
+ vel[2] += ang[0] * (nc.y - c.y) - ang[1] * (nc.x - c.x);
2297
+ }
2298
+ const piece = this.pieceSys.create(newId, group, half, center, this.massOf(newId), vel, ang);
2299
+ nb.piece = piece;
2300
+ this.o.onPiece?.(piece);
2301
+ }
2302
+ onExplode(ev) {
2303
+ const p = ev.point;
2304
+ const k = clamp(ev.radius / 3, 0.4, 2.5);
2305
+ this.particles.burst("flash", p, [0, 1, 0], 1, 0, { size: k });
2306
+ this.particles.burst("fire", p, [0, 1, 0], 34 * k, 4 * k, { size: 1 + k * 0.5 });
2307
+ this.particles.burst("smoke", p, [0, 1, 0], 24 * k, 2.2, { color: SMOKE_TONES.black, size: 1 + k * 0.6 });
2308
+ this.particles.burst("dust", p, [0, 1, 0], 22 * k, 3.5 * k);
2309
+ this.particles.burst("sparks", p, [0, 1, 0], 16 * k, 8);
2310
+ this.particles.push(p, ev.radius, 4);
2311
+ if (this.o.explosionLight !== false) {
2312
+ if (!this.light) {
2313
+ this.light = new this.THREE.PointLight(16754784, 0, 10, 2);
2314
+ this.light.name = "destruction-explosion-light";
2315
+ }
2316
+ if (!this.light.parent) this.scene.add(this.light);
2317
+ this.light.position.set(p[0], p[1] + 0.5, p[2]);
2318
+ this.light.distance = Math.max(6, ev.radius * 3);
2319
+ this.lightPeak = clamp(Math.sqrt(Math.max(1, ev.energy)) * 0.6, 10, 400);
2320
+ this.lightDur = 0.35;
2321
+ this.lightT = this.lightDur;
2322
+ }
2323
+ const c = this.v1;
2324
+ for (const pc of this.pieceSys.list) {
2325
+ this.pieceSys.centerOf(pc, c);
2326
+ const dx = c.x - p[0];
2327
+ const dy = c.y - p[1];
2328
+ const dz = c.z - p[2];
2329
+ const d = Math.hypot(dx, dy, dz);
2330
+ if (d > ev.radius) continue;
2331
+ const fall = (1 - d / ev.radius) ** 2;
2332
+ const speed = Math.min(30, Math.sqrt(2 * ev.energy * 0.02 / pc.mass)) * fall;
2333
+ const inv = d > 1e-4 ? 1 / d : 0;
2334
+ const dir = d > 1e-4 ? [dx * inv, dy * inv + 0.3, dz * inv] : [0, 1, 0];
2335
+ const spread = pc.halfExtents[0];
2336
+ 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]);
2337
+ }
2338
+ this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(ev.energy) * 0.08));
2339
+ this.sound("explode", p, clamp(0.5 + ev.radius / 10, 0.5, 1));
2340
+ }
2341
+ updateLight(dt) {
2342
+ if (!this.light || this.lightT <= 0) return;
2343
+ this.lightT = Math.max(0, this.lightT - dt);
2344
+ const t = this.lightT / this.lightDur;
2345
+ this.light.intensity = this.lightPeak * t * t;
2346
+ }
2347
+ // ----------------------------------------------------------------------------------------------------------------------------------
2348
+ // Fire, heat, char, melt, leaks
2349
+ // ----------------------------------------------------------------------------------------------------------------------------------
2350
+ updateActive(dt) {
2351
+ this.scanTimer += dt;
2352
+ if (this.scanTimer >= 0.4) {
2353
+ this.scanTimer = 0;
2354
+ for (const b of this.bounds.values()) {
2355
+ for (const pv of b.parts.values()) {
2356
+ if (pv.gone !== "none" && pv.gone !== "broken") continue;
2357
+ const ps = this.world.part(b.id, pv.id);
2358
+ if (!ps) continue;
2359
+ const def = this.world.materials.get(pv.material);
2360
+ 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")) {
2361
+ this.activate(b.id, pv.id);
2362
+ }
2363
+ }
2364
+ }
2365
+ }
2366
+ for (const [key, a] of this.active) {
2367
+ const pv = this.partVis(a.object, a.part);
2368
+ const ps = pv ? this.world.part(a.object, a.part) : null;
2369
+ if (!pv || !ps) {
2370
+ this.active.delete(key);
2371
+ continue;
2372
+ }
2373
+ const def = this.world.materials.get(pv.material);
2374
+ if (pv.gone === "none") {
2375
+ this.visuals.setChar(pv, ps.char, def?.visual?.charColor ?? 1709072);
2376
+ if (def?.visual?.glows) this.visuals.setGlow(pv, ps.temperature);
2377
+ this.visuals.setMelt(pv, ps.melt);
2378
+ if (ps.status === "melted") this.onGone(a.object, pv, "melted", def);
2379
+ else if (ps.status === "ashes") this.onGone(a.object, pv, "ashes", def);
2380
+ }
2381
+ const fire = this.fires.get(key);
2382
+ if (fire) {
2383
+ if (ps.burning && ps.status !== "broken" && ps.status !== "ashes" && ps.status !== "melted") this.emitFire(fire, pv, def, dt);
2384
+ else this.fires.delete(key);
2385
+ }
2386
+ const hot = def?.visual?.glows === true && ps.temperature > GLOW_FROM_C;
2387
+ if (!ps.burning && !hot && Math.abs(ps.char - pv.charApplied) < 0.01 && Math.abs(ps.melt - pv.meltApplied) < 0.01) {
2388
+ if (def?.visual?.glows && pv.gone === "none") this.visuals.setGlow(pv, 0);
2389
+ this.active.delete(key);
2390
+ }
2391
+ }
2392
+ }
2393
+ onGone(objectId, pv, reason, def) {
2394
+ const bb = new this.THREE.Box3();
2395
+ for (const mv of pv.meshes) bb.union(this.box.setFromObject(mv.mesh));
2396
+ this.visuals.hide(pv, reason);
2397
+ if (bb.isEmpty()) return;
2398
+ const c = bb.getCenter(new this.THREE.Vector3());
2399
+ const s = bb.getSize(new this.THREE.Vector3());
2400
+ const footprint = Math.sqrt(Math.max(s.x * s.z, 1e-4));
2401
+ if (reason === "melted") {
2402
+ this.ground.add("puddle", c.x, c.z, clamp(footprint * 0.55, 0.05, 1), this.partColor(pv, def) ?? fallbackColor(pv.material, def), objectId);
2403
+ } else {
2404
+ this.ground.add("ash", c.x, c.z, clamp(footprint * 0.35, 0.04, 0.5), 1907224, objectId);
2405
+ this.particles.burst("ash", [c.x, bb.max.y, c.z], [0, 1, 0], 10, 0.8);
2406
+ }
2407
+ }
2408
+ emitFire(f, pv, def, dt) {
2409
+ const bb = this.box;
2410
+ bb.makeEmpty();
2411
+ const tmp = new this.THREE.Box3();
2412
+ for (const mv of pv.meshes) if (mv.mesh.visible) bb.union(tmp.setFromObject(mv.mesh));
2413
+ let cx = f.point[0];
2414
+ let cy = f.point[1];
2415
+ let cz = f.point[2];
2416
+ let sx = 0.2;
2417
+ let sy = 0.2;
2418
+ let sz = 0.2;
2419
+ if (!bb.isEmpty()) {
2420
+ const c = bb.getCenter(this.v1);
2421
+ const s = bb.getSize(this.v2);
2422
+ cx = c.x;
2423
+ cy = bb.min.y;
2424
+ cz = c.z;
2425
+ sx = s.x;
2426
+ sy = s.y;
2427
+ sz = s.z;
2428
+ }
2429
+ const vol = clamp(Math.cbrt(Math.max(sx * sy * sz, 1e-5)), 0.05, 1.5);
2430
+ f.accFire += (18 + 70 * vol) * dt;
2431
+ f.accSmoke += (6 + 24 * vol) * dt;
2432
+ const tone = SMOKE_TONES[def?.thermal.smoke ?? "grey"];
2433
+ const rnd = this.rng;
2434
+ while (f.accFire >= 1) {
2435
+ f.accFire -= 1;
2436
+ 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 });
2437
+ }
2438
+ while (f.accSmoke >= 1) {
2439
+ f.accSmoke -= 1;
2440
+ 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 });
2441
+ }
2442
+ }
2443
+ updateLeaks(dt) {
2444
+ for (let i = this.leaks.length - 1; i >= 0; i--) {
2445
+ const l = this.leaks[i];
2446
+ const ps = this.world.part(l.object, l.part);
2447
+ if (!ps || ps.liters === void 0 || ps.liters <= 0) {
2448
+ this.leaks.splice(i, 1);
2449
+ continue;
2450
+ }
2451
+ l.acc += clamp(20 + l.rate * 400, 15, 120) * dt;
2452
+ const color = FLUID_COLORS[l.fluid] ?? 11184810;
2453
+ const d = l.direction;
2454
+ const dl = Math.hypot(d[0], d[1], d[2]) || 1;
2455
+ const sp = l.pressurized ? 2.5 + this.rng() * 1.5 : 0.3 + this.rng() * 0.3;
2456
+ while (l.acc >= 1) {
2457
+ l.acc -= 1;
2458
+ 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];
2459
+ if (l.fluid === "gas") this.particles.spawn("steam", l.point, vel, { color, size: 0.5, alpha: 0.5 });
2460
+ else this.particles.spawn("drips", l.point, vel, { color, tag: this.rng() < 0.03 ? this.tagOf(l.object) : 0 });
2461
+ }
2462
+ }
2463
+ }
2464
+ tagOf(object) {
2465
+ let tag = this.homeTags.indexOf(object) + 1;
2466
+ if (tag === 0) tag = this.homeTags.push(object);
2467
+ return tag;
2468
+ }
2469
+ // ----------------------------------------------------------------------------------------------------------------------------------
2470
+ // Emit, clear, restore, stats, dispose
2471
+ // ----------------------------------------------------------------------------------------------------------------------------------
2472
+ emit(kind, point, normal, count, speed) {
2473
+ this.particles.burst(kind, point, normal, count, speed ?? DEFAULT_SPEED[kind] ?? 1, kind === "smoke" ? { color: SMOKE_TONES.grey } : void 0);
2474
+ }
2475
+ restore(objectId) {
2476
+ const homes = /* @__PURE__ */ new Set([objectId]);
2477
+ const self = this.bounds.get(objectId);
2478
+ if (self?.isSplit) for (const pv of self.parts.values()) homes.add(pv.home);
2479
+ for (const home of homes) this.restoreHome(home);
2480
+ for (const [key, a] of [...this.active]) if (homes.has(a.object)) this.active.delete(key);
2481
+ for (const [key, f] of [...this.fires]) if (homes.has(f.object)) this.fires.delete(key);
2482
+ for (let i = this.leaks.length - 1; i >= 0; i--) if (homes.has(this.leaks[i].object)) this.leaks.splice(i, 1);
2483
+ for (const home of homes) {
2484
+ this.fragments.removeHome(home);
2485
+ this.ground.removeHome(home);
2486
+ }
2487
+ }
2488
+ restoreHome(home) {
2489
+ const homeBound = this.bounds.get(home);
2490
+ for (const b of [...this.bounds.values()]) {
2491
+ for (const [pid, pv] of [...b.parts]) {
2492
+ if (pv.home !== home) continue;
2493
+ this.visuals.restorePart(pv);
2494
+ if (b !== homeBound && homeBound) {
2495
+ b.parts.delete(pid);
2496
+ homeBound.parts.set(pid, pv);
2497
+ }
2498
+ }
2499
+ if (b.isSplit && b.parts.size === 0) {
2500
+ this.scene.remove(b.root);
2501
+ this.pieceSys.remove(b.id);
2502
+ this.bounds.delete(b.id);
2503
+ }
2504
+ }
2505
+ }
2506
+ clear() {
2507
+ this.queue.length = 0;
2508
+ this.active.clear();
2509
+ this.fires.clear();
2510
+ this.leaks.length = 0;
2511
+ for (const id of [...this.bounds.keys()]) if (this.bounds.has(id)) this.restore(id);
2512
+ for (const b of [...this.bounds.values()]) {
2513
+ if (b.isSplit) {
2514
+ for (const pv of b.parts.values()) this.visuals.restorePart(pv);
2515
+ this.scene.remove(b.root);
2516
+ this.pieceSys.remove(b.id);
2517
+ this.bounds.delete(b.id);
2518
+ }
2519
+ }
2520
+ this.fragments.clear();
2521
+ this.ground.clear();
2522
+ this.particles.clear();
2523
+ if (this.light) {
2524
+ this.light.intensity = 0;
2525
+ this.lightT = 0;
2526
+ this.scene.remove(this.light);
2527
+ }
2528
+ }
2529
+ stats() {
2530
+ return { fragments: this.fragments.count, particles: this.particles.live, pieces: this.pieceSys.list.length, decals: this.visuals.decalCount + this.ground.count };
2531
+ }
2532
+ dispose() {
2533
+ if (this.disposed) return;
2534
+ this.unsubscribe();
2535
+ this.clear();
2536
+ for (const id of [...this.bounds.keys()]) this.unbind(id);
2537
+ this.fragments.dispose();
2538
+ this.ground.dispose();
2539
+ this.particles.dispose();
2540
+ this.visuals.dispose();
2541
+ this.light?.dispose?.();
2542
+ this.light = null;
2543
+ this.disposed = true;
2544
+ }
2545
+ };
2546
+ export {
2547
+ createDestructionRenderer,
2548
+ heatColor,
2549
+ specFromObject3D
2550
+ };
2551
+ //# sourceMappingURL=index.js.map