@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.
@@ -0,0 +1,3622 @@
1
+ "use strict";
2
+ var GameDestructionThree = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
22
+
23
+ // src/index.ts
24
+ var index_exports = {};
25
+ __export(index_exports, {
26
+ createDestructionRenderer: () => createDestructionRenderer,
27
+ heatColor: () => heatColor,
28
+ specFromObject3D: () => specFromObject3D
29
+ });
30
+
31
+ // src/math.ts
32
+ function createRng(seed) {
33
+ let a = seed >>> 0;
34
+ return () => {
35
+ a = a + 1831565813 >>> 0;
36
+ let t = a;
37
+ t = Math.imul(t ^ t >>> 15, t | 1);
38
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
39
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
40
+ };
41
+ }
42
+ function hashNumbers(...values) {
43
+ let h = 2166136261;
44
+ for (const v of values) {
45
+ const n = Math.round(v * 1e3) | 0;
46
+ h = Math.imul(h ^ n, 16777619);
47
+ h ^= h >>> 13;
48
+ }
49
+ return h >>> 0;
50
+ }
51
+ function hashString(s) {
52
+ let h = 2166136261;
53
+ for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619);
54
+ return h >>> 0;
55
+ }
56
+ var clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
57
+ var lerp = (a, b, t) => a + (b - a) * t;
58
+ function hexToRgb(hex) {
59
+ return [(hex >> 16 & 255) / 255, (hex >> 8 & 255) / 255, (hex & 255) / 255];
60
+ }
61
+ function mixHex(a, b, t) {
62
+ const ar = a >> 16 & 255;
63
+ const ag = a >> 8 & 255;
64
+ const ab = a & 255;
65
+ const r = Math.round(ar + ((b >> 16 & 255) - ar) * t);
66
+ const g = Math.round(ag + ((b >> 8 & 255) - ag) * t);
67
+ const bl = Math.round(ab + ((b & 255) - ab) * t);
68
+ return r << 16 | g << 8 | bl;
69
+ }
70
+
71
+ // src/fragments.ts
72
+ var FragmentSystem = class {
73
+ constructor(THREE, o) {
74
+ __publicField(this, "THREE", THREE);
75
+ __publicField(this, "o", o);
76
+ __publicField(this, "list", []);
77
+ __publicField(this, "box");
78
+ __publicField(this, "shardGeos", []);
79
+ __publicField(this, "materials", /* @__PURE__ */ new Map());
80
+ __publicField(this, "q");
81
+ __publicField(this, "dq");
82
+ __publicField(this, "m");
83
+ __publicField(this, "v");
84
+ __publicField(this, "w");
85
+ __publicField(this, "rng", createRng(1374496523));
86
+ this.box = new THREE.BoxGeometry(1, 1, 1);
87
+ this.q = new THREE.Quaternion();
88
+ this.dq = new THREE.Quaternion();
89
+ this.m = new THREE.Matrix4();
90
+ this.v = new THREE.Vector3();
91
+ this.w = new THREE.Vector3();
92
+ }
93
+ get count() {
94
+ return this.list.length;
95
+ }
96
+ /** Meshes of every live fragment (tests and picking). */
97
+ meshes() {
98
+ return this.list.map((f) => f.mesh);
99
+ }
100
+ /** One shared material per source material id + colour (+ transparency). */
101
+ materialFor(materialId, color, transparent, opacity = 0.5) {
102
+ const key = `${materialId}:${color}:${transparent ? 1 : 0}`;
103
+ let m = this.materials.get(key);
104
+ if (!m) {
105
+ 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 });
106
+ this.materials.set(key, m);
107
+ }
108
+ return m;
109
+ }
110
+ shardGeometry(seed) {
111
+ const idx = seed % 6;
112
+ let g = this.shardGeos[idx];
113
+ if (!g) {
114
+ const r = createRng(1e3 + idx);
115
+ const n = 3 + Math.floor(r() * 3);
116
+ const shape = new this.THREE.Shape();
117
+ const pts = [];
118
+ for (let i = 0; i < n; i++) {
119
+ const a = i / n * Math.PI * 2 + (r() - 0.5) * 0.7;
120
+ const rad = 0.35 + r() * 0.15;
121
+ pts.push([Math.cos(a) * rad, Math.sin(a) * rad]);
122
+ }
123
+ shape.moveTo(pts[0][0], pts[0][1]);
124
+ for (let i = 1; i < n; i++) shape.lineTo(pts[i][0], pts[i][1]);
125
+ shape.closePath();
126
+ g = new this.THREE.ExtrudeGeometry(shape, { depth: 1, bevelEnabled: false });
127
+ g.translate(0, 0, -0.5);
128
+ this.shardGeos[idx] = g;
129
+ }
130
+ return g;
131
+ }
132
+ /** Create the mesh of a fragment and start its motion. `maxAllowed` is the live budget. */
133
+ spawn(f, material, home, maxAllowed) {
134
+ const geo = f.shape === "shard" ? this.shardGeometry(hashString(f.id)) : this.box;
135
+ const mesh = new this.THREE.Mesh(geo, material);
136
+ mesh.position.set(f.center[0], f.center[1], f.center[2]);
137
+ mesh.quaternion.set(f.rotation[0], f.rotation[1], f.rotation[2], f.rotation[3]).normalize();
138
+ const sz = f.size;
139
+ const sc = new this.THREE.Vector3(Math.max(5e-3, sz[0]), Math.max(5e-3, sz[1]), Math.max(3e-3, sz[2]));
140
+ mesh.scale.copy(sc);
141
+ mesh.castShadow = true;
142
+ this.o.scene.add(mesh);
143
+ const live = {
144
+ mesh,
145
+ home,
146
+ vel: new this.THREE.Vector3(f.velocity[0], f.velocity[1], f.velocity[2]),
147
+ ang: new this.THREE.Vector3(f.angularVelocity[0], f.angularVelocity[1], f.angularVelocity[2]),
148
+ // shards are thin: the contact extent uses the real (small) thickness, the chunk uses half of each side
149
+ half: sc.clone().multiplyScalar(0.5),
150
+ baseScale: sc,
151
+ still: 0,
152
+ asleep: false,
153
+ rest: 0,
154
+ fade: 1,
155
+ dying: false
156
+ };
157
+ this.list.push(live);
158
+ let alive = this.list.reduce((n, l) => n + (l.dying ? 0 : 1), 0);
159
+ for (let i = 0; i < this.list.length && alive > maxAllowed; i++) {
160
+ if (!this.list[i].dying) {
161
+ this.list[i].dying = true;
162
+ alive--;
163
+ }
164
+ }
165
+ }
166
+ /** Add velocity to every fragment within `radius` of a point (blasts). */
167
+ blast(point, radius, speed) {
168
+ for (const f of this.list) {
169
+ const d = this.v.copy(f.mesh.position).sub(this.w.set(point[0], point[1], point[2]));
170
+ const dist = d.length();
171
+ if (dist > radius || f.dying) continue;
172
+ const k = (1 - dist / radius) ** 2 * speed;
173
+ d.normalize().multiplyScalar(k);
174
+ d.y += k * 0.4;
175
+ f.vel.add(d);
176
+ f.ang.add(this.w.set(this.rng() - 0.5, this.rng() - 0.5, this.rng() - 0.5).multiplyScalar(k));
177
+ f.asleep = false;
178
+ f.rest = 0;
179
+ }
180
+ }
181
+ update(dt) {
182
+ const g = this.o.gravity;
183
+ for (let i = this.list.length - 1; i >= 0; i--) {
184
+ const f = this.list[i];
185
+ if (f.dying) {
186
+ f.fade -= dt / 0.6;
187
+ if (f.fade <= 0) {
188
+ this.o.scene.remove(f.mesh);
189
+ this.list.splice(i, 1);
190
+ continue;
191
+ }
192
+ f.mesh.scale.copy(f.baseScale).multiplyScalar(f.fade);
193
+ }
194
+ if (f.asleep) {
195
+ f.rest += dt;
196
+ if (this.o.life > 0 && f.rest > this.o.life) f.dying = true;
197
+ continue;
198
+ }
199
+ this.step(f, dt, g);
200
+ }
201
+ }
202
+ step(f, dt, g) {
203
+ const mesh = f.mesh;
204
+ f.vel.y -= g * dt;
205
+ mesh.position.addScaledVector(f.vel, dt);
206
+ const w = f.ang.length();
207
+ if (w > 1e-5) {
208
+ this.dq.setFromAxisAngle(this.v.copy(f.ang).multiplyScalar(1 / w), w * dt);
209
+ mesh.quaternion.premultiply(this.dq).normalize();
210
+ }
211
+ this.m.makeRotationFromQuaternion(mesh.quaternion);
212
+ const e = this.m.elements;
213
+ const h = f.half;
214
+ const sc = f.dying ? f.fade : 1;
215
+ const ext = (Math.abs(e[1]) * h.x + Math.abs(e[5]) * h.y + Math.abs(e[9]) * h.z) * sc;
216
+ const gy = this.o.groundAt(mesh.position.x, mesh.position.z);
217
+ let contact = false;
218
+ if (mesh.position.y - ext < gy) {
219
+ contact = true;
220
+ mesh.position.y = gy + ext;
221
+ if (f.vel.y < -0.6) {
222
+ f.vel.y = -f.vel.y * 0.25;
223
+ f.vel.x *= 0.7;
224
+ f.vel.z *= 0.7;
225
+ f.ang.multiplyScalar(0.55);
226
+ f.ang.x += (this.rng() - 0.5) * 2;
227
+ f.ang.z += (this.rng() - 0.5) * 2;
228
+ } else {
229
+ f.vel.y = 0;
230
+ const fr = Math.exp(-7 * dt);
231
+ f.vel.x *= fr;
232
+ f.vel.z *= fr;
233
+ f.ang.multiplyScalar(Math.exp(-6 * dt));
234
+ }
235
+ } else {
236
+ f.ang.multiplyScalar(Math.exp(-0.15 * dt));
237
+ }
238
+ if (contact && f.vel.lengthSq() < 4) this.settle(f, dt);
239
+ const slow = f.vel.lengthSq() < 0.012 && f.ang.lengthSq() < 0.2;
240
+ if (contact && slow) {
241
+ f.still += dt;
242
+ if (f.still > 0.4) {
243
+ f.asleep = true;
244
+ f.vel.set(0, 0, 0);
245
+ f.ang.set(0, 0, 0);
246
+ }
247
+ } else {
248
+ f.still = 0;
249
+ }
250
+ }
251
+ /** Tip the fragment towards lying on its flattest face. */
252
+ settle(f, dt) {
253
+ const mesh = f.mesh;
254
+ this.m.makeRotationFromQuaternion(mesh.quaternion);
255
+ const e = this.m.elements;
256
+ const up = [e[1], e[5], e[9]];
257
+ const half = [f.half.x, f.half.y, f.half.z];
258
+ let small = 0;
259
+ for (let i = 1; i < 3; i++) if (half[i] < half[small]) small = i;
260
+ let axis = small;
261
+ if (Math.abs(up[small]) < 0.35) {
262
+ axis = 0;
263
+ for (let i = 1; i < 3; i++) if (Math.abs(up[i]) > Math.abs(up[axis])) axis = i;
264
+ }
265
+ const sign = up[axis] >= 0 ? 1 : -1;
266
+ this.v.set(e[axis * 4], e[axis * 4 + 1], e[axis * 4 + 2]).multiplyScalar(sign);
267
+ this.w.set(0, 1, 0);
268
+ this.dq.setFromUnitVectors(this.v, this.w);
269
+ this.q.copy(this.dq).multiply(mesh.quaternion);
270
+ mesh.quaternion.slerp(this.q, clamp(dt * 7, 0, 1));
271
+ }
272
+ /** Remove the fragments of one object (after `restore`). */
273
+ removeHome(home) {
274
+ for (let i = this.list.length - 1; i >= 0; i--) {
275
+ if (this.list[i].home === home) {
276
+ this.o.scene.remove(this.list[i].mesh);
277
+ this.list.splice(i, 1);
278
+ }
279
+ }
280
+ }
281
+ clear() {
282
+ for (const f of this.list) this.o.scene.remove(f.mesh);
283
+ this.list.length = 0;
284
+ }
285
+ dispose() {
286
+ this.clear();
287
+ this.box.dispose();
288
+ for (const g of this.shardGeos) g?.dispose();
289
+ this.shardGeos = [];
290
+ for (const m of this.materials.values()) m.dispose();
291
+ this.materials.clear();
292
+ }
293
+ };
294
+
295
+ // src/ground.ts
296
+ var MAX_RADIUS = 1.2;
297
+ var GroundSpots = class {
298
+ constructor(THREE, scene, groundAt, cap) {
299
+ __publicField(this, "THREE", THREE);
300
+ __publicField(this, "scene", scene);
301
+ __publicField(this, "groundAt", groundAt);
302
+ __publicField(this, "cap", cap);
303
+ __publicField(this, "spots", []);
304
+ __publicField(this, "geometry");
305
+ __publicField(this, "materials", /* @__PURE__ */ new Map());
306
+ this.geometry = new THREE.CircleGeometry(1, 20);
307
+ this.geometry.rotateX(-Math.PI / 2);
308
+ }
309
+ get count() {
310
+ return this.spots.length;
311
+ }
312
+ material(kind, color) {
313
+ const key = `${kind}:${color}`;
314
+ let m = this.materials.get(key);
315
+ if (!m) {
316
+ m = new this.THREE.MeshBasicMaterial({
317
+ color,
318
+ transparent: true,
319
+ opacity: kind === "puddle" ? 0.88 : 0.95,
320
+ depthWrite: false,
321
+ polygonOffset: true,
322
+ polygonOffsetFactor: -2,
323
+ polygonOffsetUnits: -2
324
+ });
325
+ this.materials.set(key, m);
326
+ }
327
+ return m;
328
+ }
329
+ /** Add (or grow) a spot at (x, z). `radius` is the radius this addition contributes. */
330
+ add(kind, x, z, radius, color, home) {
331
+ for (const s of this.spots) {
332
+ if (s.kind !== kind) continue;
333
+ const d = Math.hypot(s.x - x, s.z - z);
334
+ if (d < Math.max(s.target, radius) * 0.9) {
335
+ s.target = Math.min(MAX_RADIUS, Math.sqrt(s.target * s.target + radius * radius * 0.5));
336
+ return;
337
+ }
338
+ }
339
+ while (this.spots.length >= this.cap) this.remove(this.spots[0]);
340
+ const mesh = new this.THREE.Mesh(this.geometry, this.material(kind, color));
341
+ const y = this.groundAt(x, z) + 4e-3 + this.spots.length % 8 * 4e-4;
342
+ mesh.position.set(x, y, z);
343
+ mesh.scale.setScalar(1e-3);
344
+ mesh.renderOrder = 1;
345
+ this.scene.add(mesh);
346
+ this.spots.push({ mesh, kind, home, x, z, radius: 1e-3, target: Math.min(MAX_RADIUS, radius) });
347
+ }
348
+ remove(s) {
349
+ const i = this.spots.indexOf(s);
350
+ if (i >= 0) this.spots.splice(i, 1);
351
+ this.scene.remove(s.mesh);
352
+ }
353
+ update(dt) {
354
+ const k = Math.min(1, dt * 3);
355
+ for (const s of this.spots) {
356
+ if (Math.abs(s.radius - s.target) < 1e-4) continue;
357
+ s.radius = lerp(s.radius, s.target, k);
358
+ s.mesh.scale.setScalar(s.radius);
359
+ }
360
+ }
361
+ /** Remove the spots made for one object (after its `restore`). */
362
+ removeHome(home) {
363
+ for (const s of [...this.spots]) if (s.home === home) this.remove(s);
364
+ }
365
+ clear() {
366
+ for (const s of [...this.spots]) this.remove(s);
367
+ }
368
+ dispose() {
369
+ this.clear();
370
+ this.geometry.dispose();
371
+ for (const m of this.materials.values()) m.dispose();
372
+ this.materials.clear();
373
+ }
374
+ };
375
+
376
+ // src/particles.ts
377
+ var KINDS = {
378
+ 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 },
379
+ 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" },
380
+ 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" },
381
+ 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" },
382
+ 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 },
383
+ 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" },
384
+ 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" },
385
+ 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 },
386
+ 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 },
387
+ 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" },
388
+ 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 },
389
+ 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" },
390
+ 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" },
391
+ 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" }
392
+ };
393
+ var KIND_IDS = Object.keys(KINDS);
394
+ var KIND_INDEX = {};
395
+ KIND_IDS.forEach((k, i) => KIND_INDEX[k] = i);
396
+ var KIND_LIST = KIND_IDS.map((k) => KINDS[k]);
397
+ var VERT = `
398
+ uniform float uScale;
399
+ attribute float size;
400
+ attribute float alpha;
401
+ attribute vec3 pcolor;
402
+ varying float vAlpha;
403
+ varying vec3 vColor;
404
+ void main() {
405
+ vAlpha = alpha;
406
+ vColor = pcolor;
407
+ vec4 mv = modelViewMatrix * vec4(position, 1.0);
408
+ gl_PointSize = min(size * uScale / max(0.1, -mv.z), 256.0);
409
+ gl_Position = projectionMatrix * mv;
410
+ }`;
411
+ var FRAG = `
412
+ varying float vAlpha;
413
+ varying vec3 vColor;
414
+ void main() {
415
+ float d = length(gl_PointCoord - vec2(0.5)) * 2.0;
416
+ float a = (1.0 - smoothstep(0.35, 1.0, d)) * vAlpha;
417
+ if (a < 0.004) discard;
418
+ gl_FragColor = vec4(vColor, a);
419
+ }`;
420
+ var Pool = class {
421
+ constructor(THREE, capacity, additive, pointScale) {
422
+ __publicField(this, "capacity", capacity);
423
+ __publicField(this, "points");
424
+ __publicField(this, "geometry");
425
+ __publicField(this, "material");
426
+ __publicField(this, "live", 0);
427
+ __publicField(this, "next", 0);
428
+ __publicField(this, "high", 0);
429
+ __publicField(this, "pos");
430
+ __publicField(this, "col");
431
+ __publicField(this, "alp");
432
+ __publicField(this, "siz");
433
+ __publicField(this, "vel");
434
+ __publicField(this, "age");
435
+ __publicField(this, "life");
436
+ __publicField(this, "size0");
437
+ __publicField(this, "alive");
438
+ __publicField(this, "kind");
439
+ __publicField(this, "seed");
440
+ __publicField(this, "alphaMul");
441
+ __publicField(this, "tag");
442
+ __publicField(this, "colorHex");
443
+ __publicField(this, "bounced");
444
+ const n = Math.max(1, capacity);
445
+ this.pos = new Float32Array(n * 3);
446
+ this.col = new Float32Array(n * 3);
447
+ this.alp = new Float32Array(n);
448
+ this.siz = new Float32Array(n);
449
+ this.vel = new Float32Array(n * 3);
450
+ this.age = new Float32Array(n);
451
+ this.life = new Float32Array(n);
452
+ this.size0 = new Float32Array(n);
453
+ this.alive = new Uint8Array(n);
454
+ this.kind = new Uint8Array(n);
455
+ this.seed = new Float32Array(n);
456
+ this.alphaMul = new Float32Array(n);
457
+ this.tag = new Int32Array(n);
458
+ this.colorHex = new Int32Array(n);
459
+ this.bounced = new Uint8Array(n);
460
+ this.geometry = new THREE.BufferGeometry();
461
+ const attr = (a, size) => {
462
+ const at = new THREE.BufferAttribute(a, size);
463
+ at.setUsage(THREE.DynamicDrawUsage);
464
+ return at;
465
+ };
466
+ this.geometry.setAttribute("position", attr(this.pos, 3));
467
+ this.geometry.setAttribute("pcolor", attr(this.col, 3));
468
+ this.geometry.setAttribute("alpha", attr(this.alp, 1));
469
+ this.geometry.setAttribute("size", attr(this.siz, 1));
470
+ this.geometry.setDrawRange(0, 0);
471
+ this.material = new THREE.ShaderMaterial({
472
+ uniforms: { uScale: { value: pointScale } },
473
+ vertexShader: VERT,
474
+ fragmentShader: FRAG,
475
+ transparent: true,
476
+ depthWrite: false,
477
+ blending: additive ? THREE.AdditiveBlending : THREE.NormalBlending
478
+ });
479
+ this.points = new THREE.Points(this.geometry, this.material);
480
+ this.points.frustumCulled = false;
481
+ this.points.name = additive ? "destruction-particles-additive" : "destruction-particles";
482
+ this.points.renderOrder = additive ? 11 : 10;
483
+ }
484
+ spawn(kind, x, y, z, vx, vy, vz, def, rnd, o) {
485
+ if (this.capacity <= 0) return;
486
+ const i = this.next;
487
+ this.next = (this.next + 1) % this.capacity;
488
+ if (this.alive[i]) this.live--;
489
+ this.alive[i] = 1;
490
+ this.live++;
491
+ if (i + 1 > this.high) this.high = i + 1;
492
+ this.kind[i] = kind;
493
+ this.pos[i * 3] = x;
494
+ this.pos[i * 3 + 1] = y;
495
+ this.pos[i * 3 + 2] = z;
496
+ this.vel[i * 3] = vx;
497
+ this.vel[i * 3 + 1] = vy;
498
+ this.vel[i * 3 + 2] = vz;
499
+ this.age[i] = 0;
500
+ this.life[i] = lerp(def.life[0], def.life[1], rnd()) * (o?.life ?? 1);
501
+ this.size0[i] = lerp(def.size[0], def.size[1], rnd()) * (o?.size ?? 1);
502
+ this.seed[i] = rnd();
503
+ this.alphaMul[i] = o?.alpha ?? 1;
504
+ this.tag[i] = o?.tag ?? 0;
505
+ this.colorHex[i] = o?.color ?? -1;
506
+ this.bounced[i] = 0;
507
+ this.siz[i] = this.size0[i];
508
+ this.alp[i] = 0;
509
+ }
510
+ kill(i) {
511
+ if (!this.alive[i]) return;
512
+ this.alive[i] = 0;
513
+ this.live--;
514
+ this.alp[i] = 0;
515
+ this.siz[i] = 0;
516
+ }
517
+ clear() {
518
+ this.alive.fill(0);
519
+ this.alp.fill(0);
520
+ this.siz.fill(0);
521
+ this.live = 0;
522
+ this.high = 0;
523
+ this.next = 0;
524
+ this.geometry.setDrawRange(0, 0);
525
+ }
526
+ /** Move a live particle (used by external pushes such as blasts). */
527
+ push(cx, cy, cz, radius, speed) {
528
+ for (let i = 0; i < this.high; i++) {
529
+ if (!this.alive[i]) continue;
530
+ const dx = this.pos[i * 3] - cx;
531
+ const dy = this.pos[i * 3 + 1] - cy;
532
+ const dz = this.pos[i * 3 + 2] - cz;
533
+ const d = Math.hypot(dx, dy, dz);
534
+ if (d > radius || d < 1e-4) continue;
535
+ const f = (1 - d / radius) * speed;
536
+ this.vel[i * 3] += dx / d * f;
537
+ this.vel[i * 3 + 1] += dy / d * f;
538
+ this.vel[i * 3 + 2] += dz / d * f;
539
+ }
540
+ }
541
+ update(dt, host) {
542
+ let top = 0;
543
+ for (let i = 0; i < this.high; i++) {
544
+ if (!this.alive[i]) continue;
545
+ const def = KIND_LIST[this.kind[i]];
546
+ const age = this.age[i] += dt;
547
+ const life = this.life[i];
548
+ if (age >= life) {
549
+ this.kill(i);
550
+ continue;
551
+ }
552
+ const t = age / life;
553
+ const i3 = i * 3;
554
+ let vx = this.vel[i3];
555
+ let vy = this.vel[i3 + 1];
556
+ let vz = this.vel[i3 + 2];
557
+ vy += (def.rise - def.gravity * host.gravity) * dt;
558
+ if (def.flutter) {
559
+ const ph = age * 7 + this.seed[i] * 40;
560
+ vx += Math.sin(ph) * def.flutter * dt;
561
+ vz += Math.cos(ph * 1.3) * def.flutter * dt;
562
+ }
563
+ if (def.drag > 0) {
564
+ const k = Math.exp(-def.drag * dt);
565
+ vx *= k;
566
+ vy *= k;
567
+ vz *= k;
568
+ }
569
+ const x = this.pos[i3] + vx * dt;
570
+ let y = this.pos[i3 + 1] + vy * dt;
571
+ const z = this.pos[i3 + 2] + vz * dt;
572
+ if (def.ground !== "none") {
573
+ const gy = host.groundAt(x, z) + 5e-3;
574
+ if (y < gy) {
575
+ if (def.ground === "land") {
576
+ const tag = this.tag[i];
577
+ if (tag !== 0) host.onLand(x, gy, z, this.colorHex[i] >= 0 ? this.colorHex[i] : def.c0, tag);
578
+ this.kill(i);
579
+ continue;
580
+ }
581
+ y = gy;
582
+ if (def.ground === "bounce" && !this.bounced[i]) {
583
+ this.bounced[i] = 1;
584
+ vy = Math.abs(vy) * 0.35;
585
+ vx *= 0.6;
586
+ vz *= 0.6;
587
+ } else {
588
+ vx = vy = vz = 0;
589
+ }
590
+ }
591
+ }
592
+ this.vel[i3] = vx;
593
+ this.vel[i3 + 1] = vy;
594
+ this.vel[i3 + 2] = vz;
595
+ this.pos[i3] = x;
596
+ this.pos[i3 + 1] = y;
597
+ this.pos[i3 + 2] = z;
598
+ this.siz[i] = this.size0[i] * lerp(1, def.grow, t);
599
+ const c0 = this.colorHex[i] >= 0 ? this.colorHex[i] : def.c0;
600
+ const c1 = this.colorHex[i] >= 0 ? this.colorHex[i] : def.c1;
601
+ const a = hexToRgb(c0);
602
+ const b = hexToRgb(c1);
603
+ this.col[i3] = lerp(a[0], b[0], t);
604
+ this.col[i3 + 1] = lerp(a[1], b[1], t);
605
+ this.col[i3 + 2] = lerp(a[2], b[2], t);
606
+ let env = t < def.fadeIn ? t / def.fadeIn : 1 - (t - def.fadeIn) / (1 - def.fadeIn);
607
+ if (def.flicker) env *= 1 - def.flicker * (0.5 + 0.5 * Math.sin(age * 38 + this.seed[i] * 50));
608
+ this.alp[i] = clamp(env, 0, 1) * def.alpha * this.alphaMul[i];
609
+ top = i + 1;
610
+ }
611
+ this.high = top;
612
+ this.geometry.setDrawRange(0, top);
613
+ for (const name of ["position", "pcolor", "alpha", "size"]) {
614
+ ;
615
+ this.geometry.getAttribute(name).needsUpdate = true;
616
+ }
617
+ }
618
+ dispose() {
619
+ this.geometry.dispose();
620
+ this.material.dispose();
621
+ }
622
+ };
623
+ var ParticleSystem = class {
624
+ constructor(THREE, o) {
625
+ __publicField(this, "o", o);
626
+ __publicField(this, "add");
627
+ __publicField(this, "normal");
628
+ __publicField(this, "rng", createRng(2654435761));
629
+ __publicField(this, "attached", false);
630
+ __publicField(this, "host");
631
+ const addCap = Math.floor(o.capacity * 0.4);
632
+ this.add = new Pool(THREE, addCap, true, o.pointScale);
633
+ this.normal = new Pool(THREE, o.capacity - addCap, false, o.pointScale);
634
+ this.host = { groundAt: o.groundAt, gravity: o.gravity, onLand: o.onLand };
635
+ }
636
+ get capacity() {
637
+ return this.add.capacity + this.normal.capacity;
638
+ }
639
+ get live() {
640
+ return this.add.live + this.normal.live;
641
+ }
642
+ ensureAttached() {
643
+ if (this.attached) return;
644
+ this.attached = true;
645
+ this.o.scene.add(this.add.points, this.normal.points);
646
+ }
647
+ /** Remove the Points from the scene (they come back on the next emit). */
648
+ detach() {
649
+ if (!this.attached) return;
650
+ this.attached = false;
651
+ this.o.scene.remove(this.add.points, this.normal.points);
652
+ }
653
+ /** Spawn one particle with an explicit velocity. */
654
+ spawn(kind, p, v, opts) {
655
+ const def = KINDS[kind];
656
+ this.ensureAttached();
657
+ (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);
658
+ }
659
+ /** Spawn `count` particles leaving `point` around `normal` at about `speed` m/s. */
660
+ burst(kind, point, normal, count, speed = 1, opts) {
661
+ const def = KINDS[kind];
662
+ const n = Math.max(0, Math.round(count * this.o.density));
663
+ const rnd = this.rng;
664
+ const nl = Math.hypot(normal[0], normal[1], normal[2]) || 1;
665
+ const nx = normal[0] / nl;
666
+ const ny = normal[1] / nl;
667
+ const nz = normal[2] / nl;
668
+ for (let k = 0; k < n; k++) {
669
+ const u = rnd() * 2 - 1;
670
+ const ph = rnd() * Math.PI * 2;
671
+ const s = Math.sqrt(1 - u * u);
672
+ const rx = s * Math.cos(ph);
673
+ const ry = u;
674
+ const rz = s * Math.sin(ph);
675
+ let dx = nx * def.cone + rx * (1 - def.cone);
676
+ let dy = ny * def.cone + ry * (1 - def.cone);
677
+ let dz = nz * def.cone + rz * (1 - def.cone);
678
+ dx += rx * 0.35;
679
+ dy += ry * 0.35;
680
+ dz += rz * 0.35;
681
+ const l = Math.hypot(dx, dy, dz) || 1;
682
+ const sp = speed * (0.35 + 0.65 * rnd());
683
+ const jitter = 0.01 + (def.size[1] > 0.5 ? 0.15 : 0.02);
684
+ this.spawn(
685
+ kind,
686
+ [point[0] + (rnd() - 0.5) * jitter, point[1] + (rnd() - 0.5) * jitter, point[2] + (rnd() - 0.5) * jitter],
687
+ [dx / l * sp, dy / l * sp, dz / l * sp],
688
+ opts
689
+ );
690
+ }
691
+ }
692
+ /** Blast wave: shove live particles away from a point. */
693
+ push(point, radius, speed) {
694
+ this.add.push(point[0], point[1], point[2], radius, speed);
695
+ this.normal.push(point[0], point[1], point[2], radius, speed);
696
+ }
697
+ update(dt) {
698
+ if (this.add.live > 0 || this.add.points.geometry.drawRange.count > 0) this.add.update(dt, this.host);
699
+ if (this.normal.live > 0 || this.normal.points.geometry.drawRange.count > 0) this.normal.update(dt, this.host);
700
+ }
701
+ clear() {
702
+ this.add.clear();
703
+ this.normal.clear();
704
+ this.detach();
705
+ }
706
+ dispose() {
707
+ this.detach();
708
+ this.add.dispose();
709
+ this.normal.dispose();
710
+ }
711
+ };
712
+
713
+ // src/pieces.ts
714
+ var SUB_DT = 1 / 120;
715
+ var MU = 0.7;
716
+ var SLEEP_AFTER = 0.5;
717
+ var HARD_LANDING = 3;
718
+ var SLOP = 2e-3;
719
+ var NARROW = 0.3;
720
+ var TOPPLE = 3;
721
+ var PieceSystem = class {
722
+ constructor(THREE, o) {
723
+ __publicField(this, "THREE", THREE);
724
+ __publicField(this, "o", o);
725
+ __publicField(this, "list", []);
726
+ __publicField(this, "time", 0);
727
+ __publicField(this, "p");
728
+ __publicField(this, "q");
729
+ __publicField(this, "dq");
730
+ __publicField(this, "m");
731
+ __publicField(this, "ax");
732
+ this.p = new THREE.Vector3();
733
+ this.q = new THREE.Quaternion();
734
+ this.dq = new THREE.Quaternion();
735
+ this.m = new THREE.Matrix4();
736
+ this.ax = new THREE.Vector3();
737
+ }
738
+ get(objectId) {
739
+ return this.list.find((b) => b.objectId === objectId) ?? null;
740
+ }
741
+ create(objectId, root, halfExtents, localCenter, mass, velocity, angular) {
742
+ const existing = this.get(objectId);
743
+ if (existing) {
744
+ existing.root = root;
745
+ existing.halfExtents = halfExtents;
746
+ existing.mass = mass;
747
+ this.setCenter(existing, localCenter);
748
+ if (velocity) existing.velocity = [...velocity];
749
+ existing.sleeping = false;
750
+ return existing;
751
+ }
752
+ const body = {
753
+ objectId,
754
+ root,
755
+ velocity: velocity ? [...velocity] : [0, 0, 0],
756
+ angularVelocity: angular ? [...angular] : [0, 0, 0],
757
+ halfExtents: [Math.max(0.01, halfExtents[0]), Math.max(0.01, halfExtents[1]), Math.max(0.01, halfExtents[2])],
758
+ mass: Math.max(0.05, mass),
759
+ sleeping: false,
760
+ held: false,
761
+ localCenter: [localCenter.x, localCenter.y, localCenter.z],
762
+ centerV: localCenter.clone(),
763
+ still: 0,
764
+ lastHit: -1,
765
+ onGround: false
766
+ };
767
+ this.list.push(body);
768
+ return body;
769
+ }
770
+ /** Move the centre of mass (after a part broke off). */
771
+ setCenter(b, c) {
772
+ b.centerV.copy(c);
773
+ b.localCenter = [c.x, c.y, c.z];
774
+ }
775
+ remove(objectId) {
776
+ const i = this.list.findIndex((b) => b.objectId === objectId);
777
+ if (i >= 0) this.list.splice(i, 1);
778
+ }
779
+ clear() {
780
+ this.list.length = 0;
781
+ }
782
+ /** World position of the centre of mass. */
783
+ centerOf(b, out) {
784
+ return out.copy(b.centerV).applyQuaternion(b.root.quaternion).add(b.root.position);
785
+ }
786
+ impulse(b, impulse, point) {
787
+ const inv = 1 / b.mass;
788
+ b.velocity[0] += impulse[0] * inv;
789
+ b.velocity[1] += impulse[1] * inv;
790
+ b.velocity[2] += impulse[2] * inv;
791
+ if (point) {
792
+ const c = this.centerOf(b, this.p);
793
+ const rx = point[0] - c.x;
794
+ const ry = point[1] - c.y;
795
+ const rz = point[2] - c.z;
796
+ const tx = ry * impulse[2] - rz * impulse[1];
797
+ const ty = rz * impulse[0] - rx * impulse[2];
798
+ const tz = rx * impulse[1] - ry * impulse[0];
799
+ const w = this.mulInvInertia(b, tx, ty, tz);
800
+ b.angularVelocity[0] += w[0];
801
+ b.angularVelocity[1] += w[1];
802
+ b.angularVelocity[2] += w[2];
803
+ }
804
+ b.sleeping = false;
805
+ b.still = 0;
806
+ }
807
+ /** 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). */
808
+ mulInvInertia(b, x, y, z) {
809
+ this.m.makeRotationFromQuaternion(b.root.quaternion);
810
+ return this.mulInvInertiaWith(b, this.m.elements, x, y, z);
811
+ }
812
+ mulInvInertiaWith(b, e, x, y, z) {
813
+ const [hx, hy, hz] = b.halfExtents;
814
+ const k = 3 / b.mass;
815
+ const ix = k / (hy * hy + hz * hz);
816
+ const iy = k / (hx * hx + hz * hz);
817
+ const iz = k / (hx * hx + hy * hy);
818
+ const lx = (e[0] * x + e[1] * y + e[2] * z) * ix;
819
+ const ly = (e[4] * x + e[5] * y + e[6] * z) * iy;
820
+ const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
821
+ 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];
822
+ }
823
+ update(dt) {
824
+ this.time += dt;
825
+ for (const b of this.list) {
826
+ if (b.held) {
827
+ b.velocity = [0, 0, 0];
828
+ b.angularVelocity = [0, 0, 0];
829
+ b.sleeping = false;
830
+ b.still = 0;
831
+ continue;
832
+ }
833
+ if (b.sleeping) continue;
834
+ this.integrate(b, dt);
835
+ }
836
+ this.separate(dt);
837
+ }
838
+ integrate(b, dt) {
839
+ const c = this.centerOf(b, this.p);
840
+ const pos = [c.x, c.y, c.z];
841
+ const q = this.q.copy(b.root.quaternion);
842
+ const v = b.velocity;
843
+ const w = b.angularVelocity;
844
+ const [hx, hy, hz] = b.halfExtents;
845
+ const invM = 1 / b.mass;
846
+ const steps = clamp(Math.ceil(dt / SUB_DT), 1, 8);
847
+ const h = dt / steps;
848
+ let landing = 0;
849
+ let landPoint = [pos[0], pos[1], pos[2]];
850
+ let contactedLast = false;
851
+ let touched = false;
852
+ let unstable = false;
853
+ for (let s = 0; s < steps; s++) {
854
+ v[1] -= this.o.gravity * h;
855
+ pos[0] += v[0] * h;
856
+ pos[1] += v[1] * h;
857
+ pos[2] += v[2] * h;
858
+ const wl = Math.hypot(w[0], w[1], w[2]);
859
+ if (wl > 1e-6) {
860
+ this.dq.setFromAxisAngle(this.ax.set(w[0] / wl, w[1] / wl, w[2] / wl), wl * h);
861
+ q.premultiply(this.dq).normalize();
862
+ }
863
+ const damp = Math.exp(-0.15 * h);
864
+ w[0] *= damp;
865
+ w[1] *= damp;
866
+ w[2] *= damp;
867
+ this.m.makeRotationFromQuaternion(q);
868
+ const e = this.m.elements;
869
+ const rs = [];
870
+ let maxPen = 0;
871
+ for (let i = 0; i < 8; i++) {
872
+ const sx = i & 1 ? hx : -hx;
873
+ const sy = i & 2 ? hy : -hy;
874
+ const sz = i & 4 ? hz : -hz;
875
+ 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];
876
+ const gy = this.o.groundAt(pos[0] + r[0], pos[2] + r[2]);
877
+ const pen = gy - (pos[1] + r[1]);
878
+ if (pen > -SLOP) {
879
+ rs.push(r);
880
+ if (pen > maxPen) maxPen = pen;
881
+ }
882
+ }
883
+ contactedLast = rs.length > 0;
884
+ if (contactedLast) touched = true;
885
+ if (!contactedLast) continue;
886
+ pos[1] += Math.max(0, maxPen - SLOP * 0.5);
887
+ let impact = 0;
888
+ for (const r of rs) impact = Math.max(impact, -(v[1] + w[2] * r[0] - w[0] * r[2]));
889
+ const restitution = impact > 2 ? 0.25 : 0;
890
+ if (impact > landing) {
891
+ landing = impact;
892
+ let sx = 0;
893
+ let sz = 0;
894
+ for (const r of rs) {
895
+ sx += r[0];
896
+ sz += r[2];
897
+ }
898
+ const lx = pos[0] + sx / rs.length;
899
+ const lz = pos[2] + sz / rs.length;
900
+ landPoint = [lx, this.o.groundAt(lx, lz), lz];
901
+ }
902
+ for (let it = 0; it < 4; it++) {
903
+ for (const r of rs) {
904
+ let vx = v[0] + (w[1] * r[2] - w[2] * r[1]);
905
+ let vy = v[1] + (w[2] * r[0] - w[0] * r[2]);
906
+ let vz = v[2] + (w[0] * r[1] - w[1] * r[0]);
907
+ const vn = vy;
908
+ if (vn >= 0) continue;
909
+ const a = this.mulInvInertiaWith(b, e, -r[2], 0, r[0]);
910
+ const denom = invM + (a[2] * r[0] - a[0] * r[2]);
911
+ const j = -(1 + (it === 0 ? restitution : 0)) * vn / denom;
912
+ v[1] += j * invM;
913
+ const dw = this.mulInvInertiaWith(b, e, -r[2] * j, 0, r[0] * j);
914
+ w[0] += dw[0];
915
+ w[1] += dw[1];
916
+ w[2] += dw[2];
917
+ vx = v[0] + (w[1] * r[2] - w[2] * r[1]);
918
+ vy = v[1] + (w[2] * r[0] - w[0] * r[2]);
919
+ vz = v[2] + (w[0] * r[1] - w[1] * r[0]);
920
+ const vt = Math.hypot(vx, vz);
921
+ if (vt > 1e-5) {
922
+ const tx = vx / vt;
923
+ const tz = vz / vt;
924
+ const rx = r[1] * tz;
925
+ const ry = r[2] * tx - r[0] * tz;
926
+ const rz = -r[1] * tx;
927
+ const at = this.mulInvInertiaWith(b, e, rx, ry, rz);
928
+ const cx = at[1] * r[2] - at[2] * r[1];
929
+ const cz = at[0] * r[1] - at[1] * r[0];
930
+ const denomT = invM + cx * tx + cz * tz;
931
+ const jt = Math.min(vt / denomT, MU * j);
932
+ v[0] -= tx * jt * invM;
933
+ v[2] -= tz * jt * invM;
934
+ const dwt = this.mulInvInertiaWith(b, e, rx * jt, ry * jt, rz * jt);
935
+ w[0] -= dwt[0];
936
+ w[1] -= dwt[1];
937
+ w[2] -= dwt[2];
938
+ }
939
+ }
940
+ }
941
+ let x0 = Infinity, x1 = -Infinity, z0 = Infinity, z1 = -Infinity, low = 0;
942
+ for (const r of rs) {
943
+ x0 = Math.min(x0, r[0]);
944
+ x1 = Math.max(x1, r[0]);
945
+ z0 = Math.min(z0, r[2]);
946
+ z1 = Math.max(z1, r[2]);
947
+ low = Math.min(low, r[1]);
948
+ }
949
+ const height = -low;
950
+ const wx = x1 - x0;
951
+ const wz = z1 - z0;
952
+ unstable = Math.min(wx, wz) < NARROW * height || x0 > 0 || x1 < 0 || z0 > 0 || z1 < 0;
953
+ if (unstable) {
954
+ const dirX = x0 + x1 > 0 ? -1 : 1;
955
+ const dirZ = z0 + z1 > 0 ? -1 : 1;
956
+ const k = TOPPLE * Math.min(height, 1) * h;
957
+ if (wx <= wz) w[2] -= dirX * k;
958
+ else w[0] += dirZ * k;
959
+ }
960
+ const rd = Math.exp((rs.length >= 3 ? -3 : -1.5) * h);
961
+ w[0] *= rd;
962
+ w[1] *= rd;
963
+ w[2] *= rd;
964
+ const fd = Math.exp(-0.3 * h);
965
+ v[0] *= fd;
966
+ v[2] *= fd;
967
+ }
968
+ b.root.quaternion.copy(q);
969
+ const off = this.ax.copy(b.centerV).applyQuaternion(q);
970
+ b.root.position.set(pos[0] - off.x, pos[1] - off.y, pos[2] - off.z);
971
+ b.root.updateMatrixWorld(true);
972
+ b.onGround = contactedLast;
973
+ if (landing > HARD_LANDING && this.time - b.lastHit > 0.15) {
974
+ b.lastHit = this.time;
975
+ this.o.onHardLanding(b, landPoint, landing);
976
+ }
977
+ const still = touched && !unstable && Math.hypot(v[0], v[1], v[2]) < 0.12 && Math.hypot(w[0], w[1], w[2]) < 0.45;
978
+ if (still) {
979
+ b.still += dt;
980
+ if (b.still >= SLEEP_AFTER) {
981
+ b.sleeping = true;
982
+ v[0] = v[1] = v[2] = 0;
983
+ w[0] = w[1] = w[2] = 0;
984
+ }
985
+ } else {
986
+ b.still = 0;
987
+ }
988
+ }
989
+ /** Push overlapping pieces apart (horizontal bounding circles), gently, so they do not interpenetrate forever. */
990
+ separate(dt) {
991
+ const n = this.list.length;
992
+ if (n < 2) return;
993
+ const maxMove = 1.5 * dt;
994
+ const ca = new this.THREE.Vector3();
995
+ const cb = new this.THREE.Vector3();
996
+ for (let i = 0; i < n; i++) {
997
+ const a = this.list[i];
998
+ for (let j = i + 1; j < n; j++) {
999
+ const b = this.list[j];
1000
+ if (a.held && b.held) continue;
1001
+ if (a.sleeping && b.sleeping) continue;
1002
+ const ra = Math.hypot(a.halfExtents[0], a.halfExtents[1], a.halfExtents[2]);
1003
+ const rb = Math.hypot(b.halfExtents[0], b.halfExtents[1], b.halfExtents[2]);
1004
+ this.centerOf(a, ca);
1005
+ this.centerOf(b, cb);
1006
+ if (Math.abs(ca.y - cb.y) > ra + rb) continue;
1007
+ const rh = (Math.max(a.halfExtents[0], a.halfExtents[2]) + Math.max(b.halfExtents[0], b.halfExtents[2])) * 0.8;
1008
+ let dx = cb.x - ca.x;
1009
+ let dz = cb.z - ca.z;
1010
+ let d = Math.hypot(dx, dz);
1011
+ if (d >= rh) continue;
1012
+ if (d < 1e-5) {
1013
+ dx = 1;
1014
+ dz = 0;
1015
+ d = 1;
1016
+ }
1017
+ const push = Math.min(maxMove, (rh - d) * 0.5);
1018
+ const wa = a.held ? 0 : b.held ? 1 : b.mass / (a.mass + b.mass);
1019
+ const wb = b.held ? 0 : a.held ? 1 : a.mass / (a.mass + b.mass);
1020
+ const ux = dx / (d || 1);
1021
+ const uz = dz / (d || 1);
1022
+ if (!a.held) {
1023
+ a.root.position.x -= ux * push * 2 * wa;
1024
+ a.root.position.z -= uz * push * 2 * wa;
1025
+ a.sleeping = false;
1026
+ }
1027
+ if (!b.held) {
1028
+ b.root.position.x += ux * push * 2 * wb;
1029
+ b.root.position.z += uz * push * 2 * wb;
1030
+ b.sleeping = false;
1031
+ }
1032
+ }
1033
+ }
1034
+ }
1035
+ };
1036
+
1037
+ // ../game-destruction/dist/index.js
1038
+ var EPS = 1e-9;
1039
+ var IDENTITY = [0, 0, 0, 1];
1040
+ var quat = {
1041
+ identity: () => [IDENTITY[0], IDENTITY[1], IDENTITY[2], IDENTITY[3]],
1042
+ /** Hamilton product `a * b` (apply `b` first, then `a`). */
1043
+ mul: (a, b) => [
1044
+ a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
1045
+ a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
1046
+ a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
1047
+ a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]
1048
+ ],
1049
+ conjugate: (q) => [-q[0], -q[1], -q[2], q[3]],
1050
+ normalize: (q) => {
1051
+ const l = Math.hypot(q[0], q[1], q[2], q[3]);
1052
+ return l < EPS ? [0, 0, 0, 1] : [q[0] / l, q[1] / l, q[2] / l, q[3] / l];
1053
+ },
1054
+ /** Rotation of `angle` radians around a unit `axis`. */
1055
+ fromAxisAngle: (axis, angle) => {
1056
+ const s = Math.sin(angle / 2);
1057
+ return [axis[0] * s, axis[1] * s, axis[2] * s, Math.cos(angle / 2)];
1058
+ },
1059
+ /** Rotation matrix, row-major 3x3. */
1060
+ toMatrix: (q) => {
1061
+ const [x, y, z, w] = q;
1062
+ const xx = x * x, yy = y * y, zz = z * z, xy = x * y, xz = x * z, yz = y * z, wx = w * x, wy = w * y, wz = w * z;
1063
+ return [1 - 2 * (yy + zz), 2 * (xy - wz), 2 * (xz + wy), 2 * (xy + wz), 1 - 2 * (xx + zz), 2 * (yz - wx), 2 * (xz - wy), 2 * (yz + wx), 1 - 2 * (xx + yy)];
1064
+ },
1065
+ /** Quaternion from three orthonormal columns (the images of the x, y and z axes). */
1066
+ fromColumns: (cx, cy, cz) => {
1067
+ const m00 = cx[0], m10 = cx[1], m20 = cx[2];
1068
+ const m01 = cy[0], m11 = cy[1], m21 = cy[2];
1069
+ const m02 = cz[0], m12 = cz[1], m22 = cz[2];
1070
+ const tr = m00 + m11 + m22;
1071
+ let q;
1072
+ if (tr > 0) {
1073
+ const s = Math.sqrt(tr + 1) * 2;
1074
+ q = [(m21 - m12) / s, (m02 - m20) / s, (m10 - m01) / s, 0.25 * s];
1075
+ } else if (m00 > m11 && m00 > m22) {
1076
+ const s = Math.sqrt(1 + m00 - m11 - m22) * 2;
1077
+ q = [0.25 * s, (m01 + m10) / s, (m02 + m20) / s, (m21 - m12) / s];
1078
+ } else if (m11 > m22) {
1079
+ const s = Math.sqrt(1 + m11 - m00 - m22) * 2;
1080
+ q = [(m01 + m10) / s, 0.25 * s, (m12 + m21) / s, (m02 - m20) / s];
1081
+ } else {
1082
+ const s = Math.sqrt(1 + m22 - m00 - m11) * 2;
1083
+ q = [(m02 + m20) / s, (m12 + m21) / s, 0.25 * s, (m10 - m01) / s];
1084
+ }
1085
+ return quat.normalize(q);
1086
+ }
1087
+ };
1088
+ var quatFromEuler = (x, y, z) => {
1089
+ const c1 = Math.cos(x / 2), c2 = Math.cos(y / 2), c3 = Math.cos(z / 2);
1090
+ const s1 = Math.sin(x / 2), s2 = Math.sin(y / 2), s3 = Math.sin(z / 2);
1091
+ return [s1 * c2 * c3 + c1 * s2 * s3, c1 * s2 * c3 - s1 * c2 * s3, c1 * c2 * s3 + s1 * s2 * c3, c1 * c2 * c3 - s1 * s2 * s3];
1092
+ };
1093
+ var shapeDims = (s) => {
1094
+ if (s.kind === "box") return [s.size[0], s.size[1], s.size[2]];
1095
+ if (s.kind === "cylinder") return [s.radius * 2, s.height, s.radius * 2];
1096
+ return [s.radius * 2, s.radius * 2, s.radius * 2];
1097
+ };
1098
+ var aabbOf = (s, center, q) => {
1099
+ let hx, hy, hz;
1100
+ if (s.kind === "sphere") {
1101
+ hx = hy = hz = s.radius;
1102
+ } else {
1103
+ const m = quat.toMatrix(q);
1104
+ if (s.kind === "box") {
1105
+ const a = s.size[0] / 2, b = s.size[1] / 2, c = s.size[2] / 2;
1106
+ hx = Math.abs(m[0]) * a + Math.abs(m[1]) * b + Math.abs(m[2]) * c;
1107
+ hy = Math.abs(m[3]) * a + Math.abs(m[4]) * b + Math.abs(m[5]) * c;
1108
+ hz = Math.abs(m[6]) * a + Math.abs(m[7]) * b + Math.abs(m[8]) * c;
1109
+ } else {
1110
+ const ax = [m[1], m[4], m[7]];
1111
+ const hh = s.height / 2;
1112
+ const e = (i) => s.radius * Math.sqrt(Math.max(0, 1 - ax[i] * ax[i])) + hh * Math.abs(ax[i]);
1113
+ hx = e(0);
1114
+ hy = e(1);
1115
+ hz = e(2);
1116
+ }
1117
+ }
1118
+ return { min: [center[0] - hx, center[1] - hy, center[2] - hz], max: [center[0] + hx, center[1] + hy, center[2] + hz] };
1119
+ };
1120
+ var materials_default = [
1121
+ {
1122
+ id: "steel",
1123
+ density: 7850,
1124
+ hpPerKg: 400,
1125
+ penetration: 110,
1126
+ ductility: 0.9,
1127
+ fracture: "none",
1128
+ damageScale: {
1129
+ slash: 0.1,
1130
+ blunt: 0.8
1131
+ },
1132
+ hitDebris: "sparks",
1133
+ breakDebris: "sparks",
1134
+ ricochetDeg: 20,
1135
+ thermal: {
1136
+ meltC: 1450,
1137
+ meltRate: 0.05,
1138
+ conductivity: 0.5,
1139
+ heatCapacity: 3
1140
+ },
1141
+ visual: {
1142
+ glows: true
1143
+ },
1144
+ tags: [
1145
+ "metal",
1146
+ "conductive"
1147
+ ],
1148
+ compressive: 2e6
1149
+ },
1150
+ {
1151
+ id: "cast_iron",
1152
+ density: 7200,
1153
+ hpPerKg: 150,
1154
+ penetration: 160,
1155
+ ductility: 0.2,
1156
+ fracture: "snap",
1157
+ fragmentSize: [
1158
+ 0.08,
1159
+ 0.3
1160
+ ],
1161
+ damageScale: {
1162
+ slash: 0.1,
1163
+ blunt: 1.2
1164
+ },
1165
+ hitDebris: "sparks",
1166
+ breakDebris: "chips",
1167
+ ricochetDeg: 15,
1168
+ thermal: {
1169
+ meltC: 1200,
1170
+ meltRate: 0.05,
1171
+ conductivity: 0.45,
1172
+ heatCapacity: 3
1173
+ },
1174
+ visual: {
1175
+ glows: true
1176
+ },
1177
+ tags: [
1178
+ "metal",
1179
+ "conductive"
1180
+ ],
1181
+ compressive: 15e5
1182
+ },
1183
+ {
1184
+ id: "aluminium",
1185
+ density: 2700,
1186
+ hpPerKg: 250,
1187
+ penetration: 60,
1188
+ ductility: 0.85,
1189
+ fracture: "none",
1190
+ damageScale: {
1191
+ slash: 0.2
1192
+ },
1193
+ hitDebris: "sparks",
1194
+ breakDebris: "sparks",
1195
+ ricochetDeg: 15,
1196
+ thermal: {
1197
+ meltC: 660,
1198
+ meltRate: 0.06,
1199
+ conductivity: 0.8,
1200
+ heatCapacity: 1.5
1201
+ },
1202
+ visual: {
1203
+ glows: true
1204
+ },
1205
+ tags: [
1206
+ "metal",
1207
+ "conductive"
1208
+ ],
1209
+ compressive: 8e5
1210
+ },
1211
+ {
1212
+ id: "copper",
1213
+ density: 8960,
1214
+ hpPerKg: 300,
1215
+ penetration: 70,
1216
+ ductility: 0.95,
1217
+ fracture: "none",
1218
+ damageScale: {
1219
+ slash: 0.2
1220
+ },
1221
+ hitDebris: "sparks",
1222
+ breakDebris: "sparks",
1223
+ ricochetDeg: 15,
1224
+ thermal: {
1225
+ meltC: 1085,
1226
+ meltRate: 0.05,
1227
+ conductivity: 1,
1228
+ heatCapacity: 2.5
1229
+ },
1230
+ visual: {
1231
+ glows: true
1232
+ },
1233
+ tags: [
1234
+ "metal",
1235
+ "conductive"
1236
+ ],
1237
+ compressive: 1e6
1238
+ },
1239
+ {
1240
+ id: "wood_soft",
1241
+ density: 500,
1242
+ hpPerKg: 90,
1243
+ penetration: 9,
1244
+ ductility: 0.35,
1245
+ fracture: "splinter",
1246
+ fragmentSize: [
1247
+ 0.03,
1248
+ 0.15
1249
+ ],
1250
+ damageScale: {
1251
+ blunt: 1.2,
1252
+ slash: 1.5
1253
+ },
1254
+ hitDebris: "splinters",
1255
+ breakDebris: "splinters",
1256
+ thermal: {
1257
+ ignitionC: 300,
1258
+ flameC: 800,
1259
+ fuelFraction: 0.9,
1260
+ burnRate: 0.012,
1261
+ conductivity: 0.05,
1262
+ heatCapacity: 1.2,
1263
+ smoke: "grey"
1264
+ },
1265
+ visual: {
1266
+ charColor: 1709072
1267
+ },
1268
+ tags: [
1269
+ "wood",
1270
+ "organic",
1271
+ "flammable"
1272
+ ],
1273
+ compressive: 1e5
1274
+ },
1275
+ {
1276
+ id: "wood_hard",
1277
+ density: 750,
1278
+ hpPerKg: 140,
1279
+ penetration: 14,
1280
+ ductility: 0.3,
1281
+ fracture: "splinter",
1282
+ fragmentSize: [
1283
+ 0.03,
1284
+ 0.2
1285
+ ],
1286
+ damageScale: {
1287
+ slash: 1.2
1288
+ },
1289
+ hitDebris: "splinters",
1290
+ breakDebris: "splinters",
1291
+ thermal: {
1292
+ ignitionC: 330,
1293
+ flameC: 800,
1294
+ fuelFraction: 0.9,
1295
+ burnRate: 0.01,
1296
+ conductivity: 0.06,
1297
+ heatCapacity: 1.4,
1298
+ smoke: "grey"
1299
+ },
1300
+ visual: {
1301
+ charColor: 1709072
1302
+ },
1303
+ tags: [
1304
+ "wood",
1305
+ "organic",
1306
+ "flammable"
1307
+ ],
1308
+ compressive: 15e4
1309
+ },
1310
+ {
1311
+ id: "plywood",
1312
+ density: 600,
1313
+ hpPerKg: 70,
1314
+ penetration: 7,
1315
+ ductility: 0.3,
1316
+ fracture: "snap",
1317
+ fragmentSize: [
1318
+ 0.1,
1319
+ 0.4
1320
+ ],
1321
+ damageScale: {
1322
+ slash: 1.3
1323
+ },
1324
+ hitDebris: "splinters",
1325
+ breakDebris: "splinters",
1326
+ thermal: {
1327
+ ignitionC: 300,
1328
+ flameC: 800,
1329
+ fuelFraction: 0.85,
1330
+ burnRate: 0.013,
1331
+ conductivity: 0.05,
1332
+ heatCapacity: 1.2,
1333
+ smoke: "grey"
1334
+ },
1335
+ visual: {
1336
+ charColor: 1709072
1337
+ },
1338
+ tags: [
1339
+ "wood",
1340
+ "organic",
1341
+ "flammable"
1342
+ ],
1343
+ compressive: 6e4
1344
+ },
1345
+ {
1346
+ id: "mdf",
1347
+ density: 750,
1348
+ hpPerKg: 45,
1349
+ penetration: 6,
1350
+ ductility: 0.15,
1351
+ fracture: "snap",
1352
+ fragmentSize: [
1353
+ 0.08,
1354
+ 0.35
1355
+ ],
1356
+ hitDebris: "chips",
1357
+ breakDebris: "dust",
1358
+ thermal: {
1359
+ ignitionC: 290,
1360
+ flameC: 750,
1361
+ fuelFraction: 0.85,
1362
+ burnRate: 0.014,
1363
+ conductivity: 0.05,
1364
+ heatCapacity: 1.2,
1365
+ smoke: "grey"
1366
+ },
1367
+ visual: {
1368
+ charColor: 1709072
1369
+ },
1370
+ tags: [
1371
+ "wood",
1372
+ "organic",
1373
+ "flammable"
1374
+ ],
1375
+ compressive: 3e4
1376
+ },
1377
+ {
1378
+ id: "glass",
1379
+ density: 2500,
1380
+ hpPerKg: 12,
1381
+ penetration: 6,
1382
+ ductility: 0.02,
1383
+ fracture: "shatter",
1384
+ fragmentSize: [
1385
+ 0.05,
1386
+ 0.25
1387
+ ],
1388
+ damageScale: {
1389
+ blunt: 1.5
1390
+ },
1391
+ hitDebris: "shards",
1392
+ breakDebris: "shards",
1393
+ ricochetDeg: 8,
1394
+ thermal: {
1395
+ meltC: 1400,
1396
+ meltRate: 0.03,
1397
+ conductivity: 0.1,
1398
+ heatCapacity: 1.5,
1399
+ thermalShockC: 150
1400
+ },
1401
+ visual: {
1402
+ glows: true,
1403
+ transparent: true
1404
+ },
1405
+ tags: [
1406
+ "glass",
1407
+ "brittle"
1408
+ ],
1409
+ compressive: 1e5
1410
+ },
1411
+ {
1412
+ id: "tempered_glass",
1413
+ density: 2500,
1414
+ hpPerKg: 30,
1415
+ penetration: 9,
1416
+ ductility: 0.02,
1417
+ fracture: "crumble",
1418
+ fragmentSize: [
1419
+ 6e-3,
1420
+ 0.02
1421
+ ],
1422
+ hitDebris: "shards",
1423
+ breakDebris: "shards",
1424
+ ricochetDeg: 8,
1425
+ thermal: {
1426
+ meltC: 1400,
1427
+ meltRate: 0.03,
1428
+ conductivity: 0.1,
1429
+ heatCapacity: 1.5,
1430
+ thermalShockC: 250
1431
+ },
1432
+ visual: {
1433
+ glows: true,
1434
+ transparent: true
1435
+ },
1436
+ tags: [
1437
+ "glass",
1438
+ "brittle"
1439
+ ],
1440
+ compressive: 2e5
1441
+ },
1442
+ {
1443
+ id: "ceramic",
1444
+ density: 2400,
1445
+ hpPerKg: 25,
1446
+ penetration: 12,
1447
+ ductility: 0.02,
1448
+ fracture: "shatter",
1449
+ fragmentSize: [
1450
+ 0.03,
1451
+ 0.12
1452
+ ],
1453
+ damageScale: {
1454
+ blunt: 1.3
1455
+ },
1456
+ hitDebris: "chips",
1457
+ breakDebris: "shards",
1458
+ thermal: {
1459
+ conductivity: 0.1,
1460
+ heatCapacity: 1.5,
1461
+ thermalShockC: 200
1462
+ },
1463
+ tags: [
1464
+ "ceramic",
1465
+ "brittle"
1466
+ ],
1467
+ compressive: 15e4
1468
+ },
1469
+ {
1470
+ id: "concrete",
1471
+ density: 2300,
1472
+ hpPerKg: 14,
1473
+ penetration: 18,
1474
+ ductility: 0.03,
1475
+ fracture: "crumble",
1476
+ fragmentSize: [
1477
+ 0.05,
1478
+ 0.25
1479
+ ],
1480
+ damageScale: {
1481
+ explosive: 1.2
1482
+ },
1483
+ hitDebris: "chips",
1484
+ breakDebris: "dust",
1485
+ thermal: {
1486
+ conductivity: 0.15,
1487
+ heatCapacity: 2.5
1488
+ },
1489
+ tags: [
1490
+ "masonry",
1491
+ "brittle"
1492
+ ],
1493
+ compressive: 444e3
1494
+ },
1495
+ {
1496
+ id: "brick",
1497
+ density: 1900,
1498
+ hpPerKg: 12,
1499
+ penetration: 14,
1500
+ ductility: 0.05,
1501
+ fracture: "crumble",
1502
+ fragmentSize: [
1503
+ 0.04,
1504
+ 0.12
1505
+ ],
1506
+ hitDebris: "chips",
1507
+ breakDebris: "dust",
1508
+ thermal: {
1509
+ conductivity: 0.15,
1510
+ heatCapacity: 2.5
1511
+ },
1512
+ tags: [
1513
+ "masonry",
1514
+ "brittle"
1515
+ ],
1516
+ compressive: 4e4
1517
+ },
1518
+ {
1519
+ id: "plaster",
1520
+ density: 850,
1521
+ hpPerKg: 18,
1522
+ penetration: 3,
1523
+ ductility: 0.05,
1524
+ fracture: "crumble",
1525
+ fragmentSize: [
1526
+ 0.05,
1527
+ 0.3
1528
+ ],
1529
+ hitDebris: "dust",
1530
+ breakDebris: "dust",
1531
+ thermal: {
1532
+ conductivity: 0.1,
1533
+ heatCapacity: 1.5
1534
+ },
1535
+ tags: [
1536
+ "masonry",
1537
+ "brittle"
1538
+ ],
1539
+ compressive: 8e3
1540
+ },
1541
+ {
1542
+ id: "stone",
1543
+ density: 2700,
1544
+ hpPerKg: 25,
1545
+ penetration: 25,
1546
+ ductility: 0.03,
1547
+ fracture: "crumble",
1548
+ fragmentSize: [
1549
+ 0.06,
1550
+ 0.3
1551
+ ],
1552
+ hitDebris: "chips",
1553
+ breakDebris: "dust",
1554
+ thermal: {
1555
+ conductivity: 0.2,
1556
+ heatCapacity: 2.5
1557
+ },
1558
+ tags: [
1559
+ "masonry",
1560
+ "brittle"
1561
+ ],
1562
+ compressive: 3e5
1563
+ },
1564
+ {
1565
+ id: "plastic",
1566
+ density: 1100,
1567
+ hpPerKg: 60,
1568
+ penetration: 8,
1569
+ ductility: 0.5,
1570
+ fracture: "snap",
1571
+ fragmentSize: [
1572
+ 0.05,
1573
+ 0.3
1574
+ ],
1575
+ hitDebris: "chips",
1576
+ breakDebris: "chips",
1577
+ thermal: {
1578
+ ignitionC: 400,
1579
+ flameC: 700,
1580
+ fuelFraction: 0.8,
1581
+ burnRate: 0.02,
1582
+ meltC: 200,
1583
+ meltRate: 0.06,
1584
+ conductivity: 0.05,
1585
+ heatCapacity: 1,
1586
+ smoke: "black"
1587
+ },
1588
+ visual: {
1589
+ charColor: 855309
1590
+ },
1591
+ tags: [
1592
+ "plastic",
1593
+ "flammable"
1594
+ ],
1595
+ compressive: 4e4
1596
+ },
1597
+ {
1598
+ id: "rubber",
1599
+ density: 1100,
1600
+ hpPerKg: 80,
1601
+ penetration: 12,
1602
+ ductility: 0.9,
1603
+ fracture: "tear",
1604
+ fragmentSize: [
1605
+ 0.05,
1606
+ 0.2
1607
+ ],
1608
+ hitDebris: "chips",
1609
+ breakDebris: "fibers",
1610
+ thermal: {
1611
+ ignitionC: 350,
1612
+ flameC: 750,
1613
+ fuelFraction: 0.6,
1614
+ burnRate: 0.015,
1615
+ conductivity: 0.05,
1616
+ heatCapacity: 1.2,
1617
+ smoke: "black"
1618
+ },
1619
+ visual: {
1620
+ charColor: 855309
1621
+ },
1622
+ tags: [
1623
+ "rubber",
1624
+ "flammable"
1625
+ ],
1626
+ compressive: 2e4
1627
+ },
1628
+ {
1629
+ id: "fabric",
1630
+ density: 400,
1631
+ hpPerKg: 20,
1632
+ penetration: 0.8,
1633
+ ductility: 0.8,
1634
+ fracture: "tear",
1635
+ fragmentSize: [
1636
+ 0.05,
1637
+ 0.25
1638
+ ],
1639
+ damageScale: {
1640
+ slash: 4,
1641
+ ballistic: 0.5
1642
+ },
1643
+ hitDebris: "fibers",
1644
+ breakDebris: "fibers",
1645
+ thermal: {
1646
+ ignitionC: 230,
1647
+ flameC: 600,
1648
+ fuelFraction: 1,
1649
+ burnRate: 0.05,
1650
+ conductivity: 0.03,
1651
+ heatCapacity: 0.6,
1652
+ smoke: "grey"
1653
+ },
1654
+ visual: {
1655
+ charColor: 1709072
1656
+ },
1657
+ tags: [
1658
+ "fabric",
1659
+ "organic",
1660
+ "flammable"
1661
+ ],
1662
+ compressive: 500
1663
+ },
1664
+ {
1665
+ id: "foam",
1666
+ density: 30,
1667
+ hpPerKg: 15,
1668
+ penetration: 0.5,
1669
+ ductility: 0.7,
1670
+ fracture: "tear",
1671
+ fragmentSize: [
1672
+ 0.03,
1673
+ 0.15
1674
+ ],
1675
+ damageScale: {
1676
+ slash: 2,
1677
+ ballistic: 0.4
1678
+ },
1679
+ hitDebris: "fibers",
1680
+ breakDebris: "fibers",
1681
+ thermal: {
1682
+ ignitionC: 250,
1683
+ flameC: 900,
1684
+ fuelFraction: 0.95,
1685
+ burnRate: 0.09,
1686
+ conductivity: 0.02,
1687
+ heatCapacity: 0.5,
1688
+ smoke: "black"
1689
+ },
1690
+ visual: {
1691
+ charColor: 855309
1692
+ },
1693
+ tags: [
1694
+ "fabric",
1695
+ "flammable"
1696
+ ],
1697
+ compressive: 100
1698
+ },
1699
+ {
1700
+ id: "leather",
1701
+ density: 900,
1702
+ hpPerKg: 50,
1703
+ penetration: 4,
1704
+ ductility: 0.8,
1705
+ fracture: "tear",
1706
+ fragmentSize: [
1707
+ 0.05,
1708
+ 0.25
1709
+ ],
1710
+ damageScale: {
1711
+ slash: 2
1712
+ },
1713
+ hitDebris: "fibers",
1714
+ breakDebris: "fibers",
1715
+ thermal: {
1716
+ ignitionC: 320,
1717
+ flameC: 650,
1718
+ fuelFraction: 0.7,
1719
+ burnRate: 0.01,
1720
+ conductivity: 0.05,
1721
+ heatCapacity: 1,
1722
+ smoke: "grey"
1723
+ },
1724
+ visual: {
1725
+ charColor: 1709072
1726
+ },
1727
+ tags: [
1728
+ "fabric",
1729
+ "organic",
1730
+ "flammable"
1731
+ ],
1732
+ compressive: 2e3
1733
+ },
1734
+ {
1735
+ id: "paper",
1736
+ density: 700,
1737
+ hpPerKg: 8,
1738
+ penetration: 0.3,
1739
+ ductility: 0.5,
1740
+ fracture: "tear",
1741
+ fragmentSize: [
1742
+ 0.03,
1743
+ 0.15
1744
+ ],
1745
+ damageScale: {
1746
+ slash: 3
1747
+ },
1748
+ hitDebris: "fibers",
1749
+ breakDebris: "fibers",
1750
+ thermal: {
1751
+ ignitionC: 230,
1752
+ flameC: 600,
1753
+ fuelFraction: 1,
1754
+ burnRate: 0.06,
1755
+ conductivity: 0.03,
1756
+ heatCapacity: 0.5,
1757
+ smoke: "grey"
1758
+ },
1759
+ visual: {
1760
+ charColor: 1709072
1761
+ },
1762
+ tags: [
1763
+ "fabric",
1764
+ "organic",
1765
+ "flammable"
1766
+ ],
1767
+ compressive: 500
1768
+ },
1769
+ {
1770
+ id: "foliage",
1771
+ density: 150,
1772
+ hpPerKg: 20,
1773
+ penetration: 0.5,
1774
+ ductility: 0.6,
1775
+ fracture: "tear",
1776
+ fragmentSize: [
1777
+ 0.03,
1778
+ 0.1
1779
+ ],
1780
+ damageScale: {
1781
+ slash: 4
1782
+ },
1783
+ hitDebris: "leaves",
1784
+ breakDebris: "leaves",
1785
+ thermal: {
1786
+ ignitionC: 300,
1787
+ flameC: 700,
1788
+ fuelFraction: 0.8,
1789
+ burnRate: 0.08,
1790
+ conductivity: 0.03,
1791
+ heatCapacity: 0.8,
1792
+ smoke: "white"
1793
+ },
1794
+ visual: {
1795
+ charColor: 1709072
1796
+ },
1797
+ tags: [
1798
+ "plant",
1799
+ "organic",
1800
+ "flammable"
1801
+ ],
1802
+ compressive: 500
1803
+ },
1804
+ {
1805
+ id: "green_wood",
1806
+ density: 650,
1807
+ hpPerKg: 5,
1808
+ penetration: 10,
1809
+ ductility: 0.5,
1810
+ fracture: "splinter",
1811
+ fragmentSize: [
1812
+ 0.03,
1813
+ 0.2
1814
+ ],
1815
+ damageScale: {
1816
+ slash: 1.3,
1817
+ ballistic: 0.1
1818
+ },
1819
+ hitDebris: "splinters",
1820
+ breakDebris: "leaves",
1821
+ thermal: {
1822
+ ignitionC: 400,
1823
+ flameC: 700,
1824
+ fuelFraction: 0.8,
1825
+ burnRate: 6e-3,
1826
+ conductivity: 0.06,
1827
+ heatCapacity: 2,
1828
+ smoke: "white"
1829
+ },
1830
+ visual: {
1831
+ charColor: 1709072
1832
+ },
1833
+ tags: [
1834
+ "plant",
1835
+ "wood",
1836
+ "organic",
1837
+ "flammable"
1838
+ ],
1839
+ compressive: 6e4
1840
+ },
1841
+ {
1842
+ id: "cactus",
1843
+ density: 900,
1844
+ hpPerKg: 30,
1845
+ penetration: 2,
1846
+ ductility: 0.6,
1847
+ fracture: "tear",
1848
+ fragmentSize: [
1849
+ 0.04,
1850
+ 0.15
1851
+ ],
1852
+ damageScale: {
1853
+ slash: 3
1854
+ },
1855
+ hitDebris: "fibers",
1856
+ breakDebris: "fibers",
1857
+ thermal: {
1858
+ ignitionC: 450,
1859
+ flameC: 650,
1860
+ fuelFraction: 0.3,
1861
+ burnRate: 0.01,
1862
+ conductivity: 0.05,
1863
+ heatCapacity: 3,
1864
+ smoke: "white"
1865
+ },
1866
+ visual: {
1867
+ charColor: 1709072
1868
+ },
1869
+ tags: [
1870
+ "plant",
1871
+ "organic",
1872
+ "flammable"
1873
+ ],
1874
+ compressive: 5e3
1875
+ },
1876
+ {
1877
+ id: "flesh_fruit",
1878
+ density: 950,
1879
+ hpPerKg: 15,
1880
+ penetration: 1.5,
1881
+ ductility: 0.7,
1882
+ fracture: "tear",
1883
+ fragmentSize: [
1884
+ 0.02,
1885
+ 0.08
1886
+ ],
1887
+ damageScale: {
1888
+ slash: 2
1889
+ },
1890
+ hitDebris: "drips",
1891
+ breakDebris: "drips",
1892
+ thermal: {
1893
+ conductivity: 0.05,
1894
+ heatCapacity: 3
1895
+ },
1896
+ tags: [
1897
+ "plant",
1898
+ "organic"
1899
+ ],
1900
+ compressive: 1e3
1901
+ },
1902
+ {
1903
+ id: "wax",
1904
+ density: 900,
1905
+ hpPerKg: 30,
1906
+ penetration: 3,
1907
+ ductility: 0.5,
1908
+ fracture: "snap",
1909
+ fragmentSize: [
1910
+ 0.05,
1911
+ 0.2
1912
+ ],
1913
+ hitDebris: "chips",
1914
+ breakDebris: "chips",
1915
+ thermal: {
1916
+ ignitionC: 230,
1917
+ flameC: 700,
1918
+ fuelFraction: 0.9,
1919
+ burnRate: 0.02,
1920
+ meltC: 60,
1921
+ meltRate: 0.2,
1922
+ conductivity: 0.05,
1923
+ heatCapacity: 0.8,
1924
+ smoke: "grey"
1925
+ },
1926
+ tags: [
1927
+ "wax",
1928
+ "flammable"
1929
+ ],
1930
+ compressive: 5e3
1931
+ },
1932
+ {
1933
+ id: "soil",
1934
+ density: 1300,
1935
+ hpPerKg: 3,
1936
+ penetration: 5,
1937
+ ductility: 0.3,
1938
+ fracture: "crumble",
1939
+ fragmentSize: [
1940
+ 0.03,
1941
+ 0.12
1942
+ ],
1943
+ hitDebris: "dust",
1944
+ breakDebris: "dust",
1945
+ thermal: {
1946
+ conductivity: 0.1,
1947
+ heatCapacity: 2.5
1948
+ },
1949
+ tags: [
1950
+ "soil",
1951
+ "masonry"
1952
+ ],
1953
+ compressive: 2e4
1954
+ }
1955
+ ];
1956
+ var material_hints_default = {
1957
+ "cast iron": "cast_iron",
1958
+ cast_iron: "cast_iron",
1959
+ castiron: "cast_iron",
1960
+ metal: "steel",
1961
+ steel: "steel",
1962
+ iron: "steel",
1963
+ chrome: "steel",
1964
+ alumin: "aluminium",
1965
+ alu_: "aluminium",
1966
+ "alu-": "aluminium",
1967
+ "alu ": "aluminium",
1968
+ copper: "copper",
1969
+ brass: "copper",
1970
+ bronze: "copper",
1971
+ wood: "wood_soft",
1972
+ timber: "wood_soft",
1973
+ plank: "wood_soft",
1974
+ lumber: "wood_soft",
1975
+ pine: "wood_soft",
1976
+ hardwood: "wood_hard",
1977
+ oak: "wood_hard",
1978
+ plywood: "plywood",
1979
+ mdf: "mdf",
1980
+ chipboard: "mdf",
1981
+ glass: "glass",
1982
+ window: "glass",
1983
+ mirror: "glass",
1984
+ tempered: "tempered_glass",
1985
+ windshield: "tempered_glass",
1986
+ fabric: "fabric",
1987
+ cloth: "fabric",
1988
+ cushion: "fabric",
1989
+ sofa: "fabric",
1990
+ curtain: "fabric",
1991
+ carpet: "fabric",
1992
+ textile: "fabric",
1993
+ canvas: "fabric",
1994
+ foam: "foam",
1995
+ sponge: "foam",
1996
+ mattress: "foam",
1997
+ leather: "leather",
1998
+ paper: "paper",
1999
+ cardboard: "paper",
2000
+ leaf: "foliage",
2001
+ leaves: "foliage",
2002
+ foliage: "foliage",
2003
+ bush: "foliage",
2004
+ grass: "foliage",
2005
+ trunk: "green_wood",
2006
+ branch: "green_wood",
2007
+ twig: "green_wood",
2008
+ cactus: "cactus",
2009
+ fruit: "flesh_fruit",
2010
+ apple: "flesh_fruit",
2011
+ melon: "flesh_fruit",
2012
+ wax: "wax",
2013
+ candle: "wax",
2014
+ concrete: "concrete",
2015
+ cement: "concrete",
2016
+ brick: "brick",
2017
+ tile: "ceramic",
2018
+ ceramic: "ceramic",
2019
+ porcelain: "ceramic",
2020
+ stone: "stone",
2021
+ rock: "stone",
2022
+ marble: "stone",
2023
+ plaster: "plaster",
2024
+ drywall: "plaster",
2025
+ gypsum: "plaster",
2026
+ plastic: "plastic",
2027
+ pvc: "plastic",
2028
+ acrylic: "plastic",
2029
+ rubber: "rubber",
2030
+ tire: "rubber",
2031
+ tyre: "rubber",
2032
+ soil: "soil",
2033
+ dirt: "soil",
2034
+ potting: "soil",
2035
+ mud: "soil"
2036
+ };
2037
+ var MATERIALS = materials_default;
2038
+ var has = (m, tag) => !!m.tags && m.tags.includes(tag);
2039
+ var JOINT_BY_MATERIALS = (a, b) => {
2040
+ const any = (tag) => has(a, tag) || has(b, tag);
2041
+ const both = (tag) => has(a, tag) && has(b, tag);
2042
+ if (any("glass")) return any("plastic") ? "clip" : "glue";
2043
+ if (any("fabric")) return "tie";
2044
+ if (both("plant")) return "stem";
2045
+ if (any("plant")) return "tie";
2046
+ if (both("masonry")) return "mortar";
2047
+ if (any("masonry") && any("metal")) return "bolt";
2048
+ if (both("metal")) return "weld";
2049
+ if (any("plastic")) return "clip";
2050
+ if (any("rubber") || any("wax")) return "glue";
2051
+ return "screw";
2052
+ };
2053
+ var MATERIAL_MAP = new Map(MATERIALS.map((m) => [m.id, m]));
2054
+ var HINTS = Object.entries(material_hints_default).sort((x, y) => y[0].length - x[0].length);
2055
+ var guessMaterial = (name) => {
2056
+ const n = name.toLowerCase();
2057
+ for (const [key, id] of HINTS) if (n.includes(key)) return id;
2058
+ return void 0;
2059
+ };
2060
+ var partThickness = (part) => {
2061
+ const dims = shapeDims(part.shape);
2062
+ const min = Math.min(dims[0], dims[1], dims[2]);
2063
+ return Math.max(1e-4, Math.min(part.thickness ?? min * (part.fill ?? 1), min));
2064
+ };
2065
+ var inferJoints = (parts, tolerance = 0.01, materials = MATERIAL_MAP) => {
2066
+ const boxes = parts.map((p) => aabbOf(p.shape, p.position, quatFromEuler(...p.rotation ?? [0, 0, 0])));
2067
+ const out = [];
2068
+ for (let i = 0; i < parts.length; i++) {
2069
+ for (let j = i + 1; j < parts.length; j++) {
2070
+ const a = boxes[i], b = boxes[j];
2071
+ let touch = true;
2072
+ for (let k = 0; k < 3 && touch; k++) if (a.min[k] - tolerance > b.max[k] || b.min[k] - tolerance > a.max[k]) touch = false;
2073
+ if (!touch) continue;
2074
+ const ma = materials.get(parts[i].material);
2075
+ const mb = materials.get(parts[j].material);
2076
+ let kind = ma && mb ? JOINT_BY_MATERIALS(ma, mb) : "screw";
2077
+ const thin = Math.min(partThickness(parts[i]), partThickness(parts[j]));
2078
+ if (kind === "weld" && thin >= 0.02) kind = "bolt";
2079
+ if (kind === "screw" && ma && mb && has(ma, "wood") && has(mb, "wood") && thin < 0.02) kind = "nail";
2080
+ out.push({ id: `${parts[i].id}~${parts[j].id}`, a: parts[i].id, b: parts[j].id, kind });
2081
+ }
2082
+ }
2083
+ return out;
2084
+ };
2085
+
2086
+ // src/spec.ts
2087
+ function partHintOf(value) {
2088
+ if (typeof value === "string") return value ? { id: value } : null;
2089
+ if (!value || typeof value !== "object") return null;
2090
+ const v = value;
2091
+ if (typeof v.id !== "string" || !v.id) return null;
2092
+ const str = (x) => typeof x === "string" && x ? x : void 0;
2093
+ return { id: v.id, material: str(v.material), parent: str(v.parent), kind: str(v.kind) };
2094
+ }
2095
+ var METAL = /steel|iron|metal|alumin|copper|brass|bronze|tin|titan|chrome|zinc|lead/i;
2096
+ function sanitizeId(name) {
2097
+ let id = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
2098
+ if (id && /^[0-9]/.test(id)) id = `p_${id}`;
2099
+ return id;
2100
+ }
2101
+ function isMesh(o) {
2102
+ return o.isMesh === true;
2103
+ }
2104
+ function sizeOf(box) {
2105
+ return [box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z];
2106
+ }
2107
+ function specFromObject3D(THREE, root, options = {}) {
2108
+ const fallback = options.fallback ?? "plastic";
2109
+ const minSize = options.minSize ?? 0.015;
2110
+ root.updateMatrixWorld(true);
2111
+ const inv = root.matrixWorld.clone().invert();
2112
+ const entries = [];
2113
+ const byKey = /* @__PURE__ */ new Map();
2114
+ const visit = (o) => {
2115
+ if (!o.visible || o.userData?.destructionIgnore) return;
2116
+ if (isMesh(o) && o.geometry) {
2117
+ const geom = o.geometry;
2118
+ if (!geom.boundingBox) geom.computeBoundingBox();
2119
+ if (geom.boundingBox && !geom.boundingBox.isEmpty()) {
2120
+ const box = geom.boundingBox.clone().applyMatrix4(new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld));
2121
+ const mats = Array.isArray(o.material) ? o.material : [o.material];
2122
+ const matName = mats[0]?.name ?? "";
2123
+ const ud = o.userData ?? {};
2124
+ const hint = partHintOf(ud.destructionPart);
2125
+ 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;
2126
+ const explicit = hint?.id ?? null;
2127
+ const hollow = ud.hollow === true || /hollow/i.test(o.name);
2128
+ const fill = options.fillOf?.(o, material);
2129
+ const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
2130
+ const existing = explicit ? byKey.get(explicit) : void 0;
2131
+ if (existing) {
2132
+ const before = sizeOf(existing.box);
2133
+ const bv = before[0] * before[1] * before[2];
2134
+ const ns = sizeOf(box);
2135
+ existing.box.union(box);
2136
+ existing.meshes.push(o);
2137
+ if (ns[0] * ns[1] * ns[2] > bv) existing.material = material;
2138
+ existing.hollow = existing.hollow || hollow;
2139
+ if (fill !== void 0) existing.fill = fill;
2140
+ } else {
2141
+ 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 };
2142
+ entries.push(e);
2143
+ if (explicit) byKey.set(explicit, e);
2144
+ }
2145
+ }
2146
+ }
2147
+ for (const c of o.children) visit(c);
2148
+ };
2149
+ visit(root);
2150
+ const eps = 5e-3;
2151
+ const overlapVolume = (a, b) => {
2152
+ const x = Math.min(a.max.x, b.max.x) - Math.max(a.min.x, b.min.x) + eps;
2153
+ const y = Math.min(a.max.y, b.max.y) - Math.max(a.min.y, b.min.y) + eps;
2154
+ const z = Math.min(a.max.z, b.max.z) - Math.max(a.min.z, b.min.z) + eps;
2155
+ return x > 0 && y > 0 && z > 0 ? x * y * z : 0;
2156
+ };
2157
+ const largest = (e) => Math.max(...sizeOf(e.box));
2158
+ const kept = [];
2159
+ const tiny = [];
2160
+ for (const e of entries) (!e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
2161
+ for (const t of tiny) {
2162
+ let best = null;
2163
+ let bestV = 0;
2164
+ for (const k of kept) {
2165
+ const v = overlapVolume(t.box, k.box);
2166
+ if (v > bestV) {
2167
+ bestV = v;
2168
+ best = k;
2169
+ }
2170
+ }
2171
+ if (!best) {
2172
+ let bd = 0.05;
2173
+ for (const k of kept) {
2174
+ const d = k.box.distanceToPoint(t.box.getCenter(new THREE.Vector3()));
2175
+ if (d < bd) {
2176
+ bd = d;
2177
+ best = k;
2178
+ }
2179
+ }
2180
+ }
2181
+ if (best) {
2182
+ best.meshes.push(...t.meshes);
2183
+ } else {
2184
+ kept.push(t);
2185
+ }
2186
+ }
2187
+ const used = /* @__PURE__ */ new Set();
2188
+ const parts = [];
2189
+ const meshesOf = {};
2190
+ let counter = 0;
2191
+ for (const e of kept) {
2192
+ counter++;
2193
+ let base = (e.explicitId ? sanitizeId(e.explicitId) : "") || sanitizeId(e.name) || `part_${counter}`;
2194
+ let id = base;
2195
+ for (let n = 2; used.has(id); n++) id = `${base}_${n}`;
2196
+ used.add(id);
2197
+ base = id;
2198
+ const size = sizeOf(e.box);
2199
+ const center = e.box.getCenter(new THREE.Vector3());
2200
+ const part = {
2201
+ id,
2202
+ material: e.material,
2203
+ shape: { kind: "box", size: [Math.max(size[0], 2e-3), Math.max(size[1], 2e-3), Math.max(size[2], 2e-3)] },
2204
+ position: [center.x, center.y, center.z]
2205
+ };
2206
+ const minSide = Math.min(...size);
2207
+ if (e.fill !== void 0) part.fill = e.fill;
2208
+ else if (e.hollow) part.fill = 0.05;
2209
+ if (minSide < 0.01 && METAL.test(e.material)) {
2210
+ part.fill = e.fill ?? 1;
2211
+ part.thickness = Math.max(minSide, 5e-4);
2212
+ }
2213
+ if (e.tags) part.tags = [...e.tags];
2214
+ const ao = options.anchorBelow ?? -1;
2215
+ if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
2216
+ parts.push(part);
2217
+ meshesOf[id] = e.meshes;
2218
+ }
2219
+ const spec = { parts };
2220
+ if (options.name !== void 0) spec.name = options.name;
2221
+ else if (root.name) spec.name = root.name;
2222
+ if (options.tags) spec.tags = [...options.tags];
2223
+ spec.joints = options.joints === false ? [] : inferJoints(parts);
2224
+ if (options.joints !== false) {
2225
+ const finalId = /* @__PURE__ */ new Map();
2226
+ kept.forEach((e, i) => finalId.set(e, parts[i].id));
2227
+ const idOf = /* @__PURE__ */ new Map();
2228
+ for (const e of kept) if (e.explicitId) idOf.set(e.explicitId, finalId.get(e));
2229
+ const joints = spec.joints;
2230
+ for (const e of kept) {
2231
+ if (!e.parent) continue;
2232
+ const id = finalId.get(e);
2233
+ const parent = idOf.get(e.parent) ?? (used.has(sanitizeId(e.parent)) ? sanitizeId(e.parent) : null);
2234
+ if (!parent || parent === id) continue;
2235
+ const at = joints.findIndex((j) => j.a === parent && j.b === id || j.a === id && j.b === parent);
2236
+ const kind = e.jointKind ?? (at >= 0 ? joints[at].kind : "resting");
2237
+ const joint = { id: `${parent}~${id}`, a: parent, b: id, kind };
2238
+ if (at >= 0) joints[at] = joint;
2239
+ else joints.push(joint);
2240
+ }
2241
+ }
2242
+ return { spec, parts: meshesOf };
2243
+ }
2244
+
2245
+ // src/heat.ts
2246
+ var STOPS = [
2247
+ [450, 0],
2248
+ [500, 2753536],
2249
+ [650, 9049344],
2250
+ [800, 14174728],
2251
+ [950, 16747540],
2252
+ [1100, 16761402],
2253
+ [1250, 16771232],
2254
+ [1400, 16777215]
2255
+ ];
2256
+ function heatColor(temperatureC) {
2257
+ if (!(temperatureC > STOPS[0][0])) return 0;
2258
+ const last = STOPS[STOPS.length - 1];
2259
+ if (temperatureC >= last[0]) return last[1];
2260
+ for (let i = 1; i < STOPS.length; i++) {
2261
+ const [t1, c1] = STOPS[i];
2262
+ if (temperatureC <= t1) {
2263
+ const [t0, c0] = STOPS[i - 1];
2264
+ return mixHex(c0, c1, (temperatureC - t0) / (t1 - t0));
2265
+ }
2266
+ }
2267
+ return last[1];
2268
+ }
2269
+
2270
+ // src/visual.ts
2271
+ var POS_ATTR = "position";
2272
+ function positionAttr(geo) {
2273
+ const a = geo.getAttribute(POS_ATTR);
2274
+ return a && !a.isInterleavedBufferAttribute ? a : null;
2275
+ }
2276
+ function materialsOf(m) {
2277
+ return Array.isArray(m) ? m : [m];
2278
+ }
2279
+ function colorOf(m) {
2280
+ const c = m.color;
2281
+ return c && c.isColor ? c : null;
2282
+ }
2283
+ var DecalKit = class {
2284
+ constructor(THREE) {
2285
+ __publicField(this, "THREE", THREE);
2286
+ __publicField(this, "disc", null);
2287
+ __publicField(this, "discMat", null);
2288
+ __publicField(this, "lineMats", /* @__PURE__ */ new Map());
2289
+ }
2290
+ discGeometry() {
2291
+ return this.disc ?? (this.disc = new this.THREE.CircleGeometry(1, 14));
2292
+ }
2293
+ discMaterial() {
2294
+ return this.discMat ?? (this.discMat = new this.THREE.MeshBasicMaterial({
2295
+ color: 328965,
2296
+ transparent: true,
2297
+ opacity: 0.92,
2298
+ depthWrite: false,
2299
+ polygonOffset: true,
2300
+ polygonOffsetFactor: -4,
2301
+ polygonOffsetUnits: -4
2302
+ }));
2303
+ }
2304
+ lineMaterial(color) {
2305
+ let m = this.lineMats.get(color);
2306
+ if (!m) {
2307
+ m = new this.THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.9, depthWrite: false });
2308
+ this.lineMats.set(color, m);
2309
+ }
2310
+ return m;
2311
+ }
2312
+ dispose() {
2313
+ this.disc?.dispose();
2314
+ this.discMat?.dispose();
2315
+ for (const m of this.lineMats.values()) m.dispose();
2316
+ this.disc = null;
2317
+ this.discMat = null;
2318
+ this.lineMats.clear();
2319
+ }
2320
+ };
2321
+ var tmpV = (THREE) => new THREE.Vector3();
2322
+ var Visuals = class {
2323
+ constructor(THREE, lowQuality, decalCap) {
2324
+ __publicField(this, "THREE", THREE);
2325
+ __publicField(this, "lowQuality", lowQuality);
2326
+ __publicField(this, "decalCap", decalCap);
2327
+ __publicField(this, "kit");
2328
+ __publicField(this, "a");
2329
+ __publicField(this, "b");
2330
+ __publicField(this, "m4");
2331
+ __publicField(this, "box");
2332
+ /** Total live decals across meshes (kept in sync by add/remove). */
2333
+ __publicField(this, "decalCount", 0);
2334
+ __publicField(this, "tints", /* @__PURE__ */ new WeakMap());
2335
+ this.kit = new DecalKit(THREE);
2336
+ this.a = tmpV(THREE);
2337
+ this.b = tmpV(THREE);
2338
+ this.m4 = new THREE.Matrix4();
2339
+ this.box = new THREE.Box3();
2340
+ }
2341
+ makeMeshVis(mesh, home) {
2342
+ return {
2343
+ mesh,
2344
+ home,
2345
+ origGeometry: mesh.geometry,
2346
+ origMaterial: mesh.material,
2347
+ origVisible: mesh.visible,
2348
+ origParent: mesh.parent,
2349
+ origPosition: mesh.position.clone(),
2350
+ origQuaternion: mesh.quaternion.clone(),
2351
+ origScale: mesh.scale.clone(),
2352
+ ownMat: false,
2353
+ ownGeo: false,
2354
+ work: null,
2355
+ baseColors: null,
2356
+ decals: [],
2357
+ meltApplied: 0,
2358
+ bottomY: 0,
2359
+ moved: false
2360
+ };
2361
+ }
2362
+ vertexCount(mv) {
2363
+ return mv.mesh.geometry.getAttribute(POS_ATTR)?.count ?? 0;
2364
+ }
2365
+ /**
2366
+ * Average vertex colour of a mesh whose material uses `vertexColors` (computed once per mesh, from its original geometry), else null. Fragments
2367
+ * and debris of such a mesh multiply it by `material.color`; without it they come out white.
2368
+ */
2369
+ vertexTint(mv) {
2370
+ if (this.tints.has(mv.mesh)) return this.tints.get(mv.mesh) ?? null;
2371
+ let tint = null;
2372
+ const used = materialsOf(mv.origMaterial).some((m) => m.vertexColors === true);
2373
+ const attr = used ? mv.origGeometry.getAttribute("color") : void 0;
2374
+ if (attr && attr.count > 0 && attr.itemSize >= 3) {
2375
+ let r = 0, g = 0, b = 0;
2376
+ for (let i = 0; i < attr.count; i++) {
2377
+ r += attr.getX(i);
2378
+ g += attr.getY(i);
2379
+ b += attr.getZ(i);
2380
+ }
2381
+ tint = new this.THREE.Color(r / attr.count, g / attr.count, b / attr.count);
2382
+ }
2383
+ this.tints.set(mv.mesh, tint);
2384
+ return tint;
2385
+ }
2386
+ /** Colour of the original material (before any char) times the vertex tint, `0xRRGGBB`, or -1. */
2387
+ baseColorHex(mv) {
2388
+ const c = mv.baseColors?.[0] ?? colorOf(materialsOf(mv.origMaterial)[0]);
2389
+ if (!c) return -1;
2390
+ const tint = this.vertexTint(mv);
2391
+ return tint ? c.clone().multiply(tint).getHex() : c.getHex();
2392
+ }
2393
+ /** Is the original material transparent? */
2394
+ isTransparent(mv) {
2395
+ return materialsOf(mv.origMaterial).some((m) => m.transparent && m.opacity < 1);
2396
+ }
2397
+ ownMaterial(mv) {
2398
+ if (mv.ownMat) return;
2399
+ mv.ownMat = true;
2400
+ const src = materialsOf(mv.origMaterial);
2401
+ const cloned = src.map((m) => m.clone());
2402
+ mv.baseColors = src.map((m) => colorOf(m)?.clone() ?? new this.THREE.Color(8947848));
2403
+ mv.mesh.material = Array.isArray(mv.origMaterial) ? cloned : cloned[0];
2404
+ }
2405
+ /** Clone the geometry (vertex edits); false when its positions cannot be edited. */
2406
+ ownGeometry(mv) {
2407
+ if (mv.ownGeo) return true;
2408
+ const pos = positionAttr(mv.mesh.geometry);
2409
+ if (!pos) return false;
2410
+ mv.ownGeo = true;
2411
+ const g = mv.mesh.geometry.clone();
2412
+ mv.mesh.geometry = g;
2413
+ mv.work = new Float32Array(positionAttr(g).array);
2414
+ return true;
2415
+ }
2416
+ // ------------------------------------------------------------------------------------------------------------------------------------
2417
+ // Dents
2418
+ // ------------------------------------------------------------------------------------------------------------------------------------
2419
+ /**
2420
+ * Push the vertices of the mesh within `dent.radius` of `worldPoint` inwards along `worldNormal` by `depth * (1 - (d/r)^2)^2`.
2421
+ * Returns false when nothing was changed (too far, low quality on a heavy mesh, or no editable positions).
2422
+ */
2423
+ applyDent(mv, worldPoint, worldNormal, dent) {
2424
+ const mesh = mv.mesh;
2425
+ if (this.lowQuality && this.vertexCount(mv) > 2e4) return false;
2426
+ mesh.updateWorldMatrix(true, false);
2427
+ const s = mesh.matrixWorld.getMaxScaleOnAxis() || 1;
2428
+ const lp = mesh.worldToLocal(this.a.copy(worldPoint));
2429
+ const ln = this.b.copy(worldNormal).transformDirection(this.m4.copy(mesh.matrixWorld).invert()).normalize();
2430
+ const r = dent.radius / s;
2431
+ const depth = dent.depth / s;
2432
+ const geo = mesh.geometry;
2433
+ if (!geo.boundingBox) geo.computeBoundingBox();
2434
+ if (geo.boundingBox && geo.boundingBox.distanceToPoint(lp) > r) return false;
2435
+ if (!this.ownGeometry(mv)) return false;
2436
+ const work = mv.work;
2437
+ const pos = positionAttr(mesh.geometry);
2438
+ const r2 = r * r;
2439
+ let touched = false;
2440
+ const px = lp.x;
2441
+ const py = lp.y;
2442
+ const pz = lp.z;
2443
+ const nx = ln.x;
2444
+ const ny = ln.y;
2445
+ const nz = ln.z;
2446
+ for (let i = 0; i < work.length; i += 3) {
2447
+ const dx = work[i] - px;
2448
+ const dy = work[i + 1] - py;
2449
+ const dz = work[i + 2] - pz;
2450
+ const d2 = dx * dx + dy * dy + dz * dz;
2451
+ if (d2 >= r2) continue;
2452
+ const f = 1 - d2 / r2;
2453
+ const k = depth * f * f;
2454
+ work[i] -= nx * k;
2455
+ work[i + 1] -= ny * k;
2456
+ work[i + 2] -= nz * k;
2457
+ touched = true;
2458
+ }
2459
+ if (!touched) return false;
2460
+ this.writePositions(mv, pos);
2461
+ return true;
2462
+ }
2463
+ /** Copy `work` (dents) into the geometry, adding the melt sag when the mesh is melting. */
2464
+ writePositions(mv, pos) {
2465
+ const work = mv.work;
2466
+ const arr = pos.array;
2467
+ const melt = mv.meltApplied;
2468
+ if (melt <= 0 || this.lowQuality && work.length / 3 > 2e4) {
2469
+ arr.set(work);
2470
+ } else {
2471
+ const geo2 = mv.mesh.geometry;
2472
+ if (!geo2.boundingBox) geo2.computeBoundingBox();
2473
+ const bb = geo2.boundingBox;
2474
+ const h = Math.max(1e-4, bb.max.y - bb.min.y);
2475
+ const minY = bb.min.y;
2476
+ for (let i = 0, v = 0; i < work.length; i += 3, v++) {
2477
+ const n = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
2478
+ const n2 = (Math.imul(v + 7, 40503) >>> 0) % 1e3 / 1e3;
2479
+ const lowness = 1 - (work[i + 1] - minY) / h;
2480
+ const spread = 1 + 0.12 * melt * (0.4 + n2) * (0.3 + lowness);
2481
+ arr[i] = work[i] * spread;
2482
+ arr[i + 2] = work[i + 2] * spread;
2483
+ arr[i + 1] = Math.max(minY, work[i + 1] - n * melt * h * 0.08);
2484
+ }
2485
+ }
2486
+ pos.needsUpdate = true;
2487
+ const geo = mv.mesh.geometry;
2488
+ geo.computeVertexNormals();
2489
+ geo.boundingBox = null;
2490
+ geo.boundingSphere = null;
2491
+ }
2492
+ // ------------------------------------------------------------------------------------------------------------------------------------
2493
+ // Decals
2494
+ // ------------------------------------------------------------------------------------------------------------------------------------
2495
+ /** Decal pose in mesh space: position and a rotation taking +Z to the surface normal. */
2496
+ decalPose(mesh, worldPoint, worldNormal, lift) {
2497
+ mesh.updateWorldMatrix(true, false);
2498
+ const s = mesh.matrixWorld.getMaxScaleOnAxis() || 1;
2499
+ const ln = worldNormal.clone().transformDirection(this.m4.copy(mesh.matrixWorld).invert()).normalize();
2500
+ const pos = mesh.worldToLocal(worldPoint.clone()).addScaledVector(ln, lift / s);
2501
+ const quat2 = new this.THREE.Quaternion().setFromUnitVectors(new this.THREE.Vector3(0, 0, 1), ln);
2502
+ return { s, pos, quat: quat2 };
2503
+ }
2504
+ addDecal(mv, decal) {
2505
+ mv.mesh.add(decal.obj);
2506
+ mv.decals.push(decal);
2507
+ this.decalCount++;
2508
+ while (mv.decals.length > 48 || this.decalCount > this.decalCap) {
2509
+ const old = mv.decals.shift();
2510
+ if (!old) break;
2511
+ this.disposeDecal(old);
2512
+ }
2513
+ }
2514
+ disposeDecal(d) {
2515
+ d.obj.parent?.remove(d.obj);
2516
+ d.geometry?.dispose();
2517
+ this.decalCount = Math.max(0, this.decalCount - 1);
2518
+ }
2519
+ addHole(mv, worldPoint, worldNormal, diameter) {
2520
+ const { s, pos, quat: quat2 } = this.decalPose(mv.mesh, worldPoint, worldNormal, 25e-4);
2521
+ const disc = new this.THREE.Mesh(this.kit.discGeometry(), this.kit.discMaterial());
2522
+ disc.position.copy(pos);
2523
+ disc.quaternion.copy(quat2);
2524
+ disc.scale.setScalar(Math.max(4e-3, diameter) * 0.5 * 1.25 / s);
2525
+ disc.renderOrder = 2;
2526
+ this.addDecal(mv, { obj: disc });
2527
+ }
2528
+ /** A star of jagged lines: `rays` rays up to `length` metres, seeded so the same event always draws the same star. */
2529
+ addCrack(mv, worldPoint, worldNormal, rays, length, color, seed) {
2530
+ const { s, pos, quat: quat2 } = this.decalPose(mv.mesh, worldPoint, worldNormal, 2e-3);
2531
+ const rnd = createRng(seed);
2532
+ const verts = [];
2533
+ const n = clamp(Math.round(rays), 3, 16);
2534
+ const L = Math.max(0.02, length) / s;
2535
+ for (let r = 0; r < n; r++) {
2536
+ let ang = r / n * Math.PI * 2 + (rnd() - 0.5) * 0.5;
2537
+ let x = 0;
2538
+ let y = 0;
2539
+ const segs = 3;
2540
+ const total = L * (0.6 + 0.4 * rnd());
2541
+ for (let k = 0; k < segs; k++) {
2542
+ const step = total / segs * (0.7 + 0.6 * rnd());
2543
+ ang += (rnd() - 0.5) * 0.45;
2544
+ const nx = x + Math.cos(ang) * step;
2545
+ const ny = y + Math.sin(ang) * step;
2546
+ verts.push(x, y, 0, nx, ny, 0);
2547
+ if (k > 0 && rnd() < 0.5) {
2548
+ const ba = ang + (rnd() < 0.5 ? -1 : 1) * (0.5 + rnd() * 0.5);
2549
+ const bl = step * (0.6 + 0.6 * rnd());
2550
+ verts.push(nx, ny, 0, nx + Math.cos(ba) * bl, ny + Math.sin(ba) * bl, 0);
2551
+ }
2552
+ x = nx;
2553
+ y = ny;
2554
+ }
2555
+ }
2556
+ const ringR = L * 0.12;
2557
+ for (let k = 0; k < 8; k++) {
2558
+ const a0 = k / 8 * Math.PI * 2;
2559
+ const a1 = (k + 1) / 8 * Math.PI * 2;
2560
+ if (rnd() < 0.75) verts.push(Math.cos(a0) * ringR, Math.sin(a0) * ringR, 0, Math.cos(a1) * ringR, Math.sin(a1) * ringR, 0);
2561
+ }
2562
+ const geo = new this.THREE.BufferGeometry();
2563
+ geo.setAttribute("position", new this.THREE.Float32BufferAttribute(verts, 3));
2564
+ const lines = new this.THREE.LineSegments(geo, this.kit.lineMaterial(color));
2565
+ lines.position.copy(pos);
2566
+ lines.quaternion.copy(quat2);
2567
+ lines.renderOrder = 2;
2568
+ this.addDecal(mv, { obj: lines, geometry: geo });
2569
+ }
2570
+ // ------------------------------------------------------------------------------------------------------------------------------------
2571
+ // Char and heat glow
2572
+ // ------------------------------------------------------------------------------------------------------------------------------------
2573
+ setChar(pv, char, charColor) {
2574
+ if (Math.abs(char - pv.charApplied) < 4e-3) return;
2575
+ pv.charApplied = char;
2576
+ const target = new this.THREE.Color(charColor);
2577
+ for (const mv of pv.meshes) {
2578
+ this.ownMaterial(mv);
2579
+ const mats = materialsOf(mv.mesh.material);
2580
+ mats.forEach((m, i) => {
2581
+ const c = colorOf(m);
2582
+ const base = mv.baseColors?.[i];
2583
+ if (c && base) c.copy(base).lerp(target, clamp(char, 0, 1));
2584
+ });
2585
+ }
2586
+ }
2587
+ /** Emissive glow of a hot part. `temperature` below the glow threshold switches it off. */
2588
+ setGlow(pv, temperatureC) {
2589
+ const hex = heatColor(temperatureC);
2590
+ const key = hex === 0 ? 0 : Math.round(temperatureC / 5) * 5;
2591
+ if (key === pv.glowApplied) return;
2592
+ pv.glowApplied = key;
2593
+ const intensity = hex === 0 ? 0 : clamp(0.35 + (temperatureC - 450) / 900, 0, 1.4);
2594
+ for (const mv of pv.meshes) {
2595
+ this.ownMaterial(mv);
2596
+ for (const m of materialsOf(mv.mesh.material)) {
2597
+ const e = m.emissive;
2598
+ if (e && e.isColor) {
2599
+ e.setHex(hex);
2600
+ m.emissiveIntensity = intensity;
2601
+ }
2602
+ }
2603
+ }
2604
+ }
2605
+ // ------------------------------------------------------------------------------------------------------------------------------------
2606
+ // Melt
2607
+ // ------------------------------------------------------------------------------------------------------------------------------------
2608
+ /** Sag the part: shorter in Y (down to 25 %), a bit wider, vertices pushed down with noise, the bottom stays put. */
2609
+ setMelt(pv, melt) {
2610
+ const m = clamp(melt, 0, 1);
2611
+ if (Math.abs(m - pv.meltApplied) < 0.01) return;
2612
+ pv.meltApplied = m;
2613
+ for (const mv of pv.meshes) {
2614
+ const mesh = mv.mesh;
2615
+ if (mv.meltApplied === 0 && m > 0) {
2616
+ mesh.updateWorldMatrix(true, false);
2617
+ mv.bottomY = this.box.setFromObject(mesh).min.y;
2618
+ }
2619
+ mv.meltApplied = m;
2620
+ mv.moved = true;
2621
+ const sy = lerp(1, 0.25, m);
2622
+ const sxz = 1 + 0.18 * m;
2623
+ mesh.scale.set(mv.origScale.x * sxz, mv.origScale.y * sy, mv.origScale.z * sxz);
2624
+ mesh.position.copy(mv.origPosition);
2625
+ mesh.updateWorldMatrix(true, false);
2626
+ const bottom = this.box.setFromObject(mesh).min.y;
2627
+ const dy = mv.bottomY - bottom;
2628
+ if (Math.abs(dy) > 1e-6 && mesh.parent) {
2629
+ mesh.parent.updateWorldMatrix(true, false);
2630
+ const inv = this.m4.copy(mesh.parent.matrixWorld).invert();
2631
+ this.a.set(0, dy, 0).applyMatrix4(inv);
2632
+ this.b.set(0, 0, 0).applyMatrix4(inv);
2633
+ mesh.position.copy(mv.origPosition).add(this.a.sub(this.b));
2634
+ }
2635
+ if (!(this.lowQuality && this.vertexCount(mv) > 2e4) && this.ownGeometry(mv)) {
2636
+ this.writePositions(mv, positionAttr(mesh.geometry));
2637
+ }
2638
+ }
2639
+ }
2640
+ // ------------------------------------------------------------------------------------------------------------------------------------
2641
+ // Visibility and restore
2642
+ // ------------------------------------------------------------------------------------------------------------------------------------
2643
+ hide(pv, reason) {
2644
+ pv.gone = reason;
2645
+ for (const mv of pv.meshes) mv.mesh.visible = false;
2646
+ }
2647
+ /** Bring one mesh back to how it was when it was bound. */
2648
+ restoreMesh(mv) {
2649
+ const mesh = mv.mesh;
2650
+ for (const d of mv.decals) this.disposeDecal(d);
2651
+ mv.decals.length = 0;
2652
+ if (mv.ownGeo) {
2653
+ mesh.geometry.dispose();
2654
+ mesh.geometry = mv.origGeometry;
2655
+ mv.ownGeo = false;
2656
+ mv.work = null;
2657
+ }
2658
+ if (mv.ownMat) {
2659
+ for (const m of materialsOf(mesh.material)) m.dispose();
2660
+ mesh.material = mv.origMaterial;
2661
+ mv.ownMat = false;
2662
+ mv.baseColors = null;
2663
+ }
2664
+ mesh.visible = mv.origVisible;
2665
+ if (mv.origParent && mesh.parent !== mv.origParent) {
2666
+ mv.origParent.add(mesh);
2667
+ mv.moved = true;
2668
+ }
2669
+ if (mv.moved) {
2670
+ mesh.position.copy(mv.origPosition);
2671
+ mesh.quaternion.copy(mv.origQuaternion);
2672
+ mesh.scale.copy(mv.origScale);
2673
+ mv.moved = false;
2674
+ }
2675
+ mv.meltApplied = 0;
2676
+ }
2677
+ restorePart(pv) {
2678
+ for (const mv of pv.meshes) this.restoreMesh(mv);
2679
+ pv.charApplied = 0;
2680
+ pv.glowApplied = 0;
2681
+ pv.meltApplied = 0;
2682
+ pv.gone = "none";
2683
+ }
2684
+ dispose() {
2685
+ this.kit.dispose();
2686
+ }
2687
+ };
2688
+
2689
+ // src/renderer.ts
2690
+ var GLOW_FROM_C = 430;
2691
+ var QUEUE_CAP = 4e3;
2692
+ var FLUID_COLORS = {
2693
+ water: 9226495,
2694
+ fuel: 14262322,
2695
+ oil: 2760728,
2696
+ gas: 15921906,
2697
+ sap: 12098362,
2698
+ acid: 10211900,
2699
+ blood: 9046538
2700
+ };
2701
+ var SMOKE_TONES = { white: 15132390, grey: 9079434, black: 2763306 };
2702
+ var DEFAULT_SPEED = {
2703
+ sparks: 3,
2704
+ splinters: 2.5,
2705
+ dust: 1,
2706
+ shards: 2.5,
2707
+ fibers: 1.2,
2708
+ leaves: 1.2,
2709
+ drips: 0.5,
2710
+ ash: 0.8,
2711
+ chips: 2.5,
2712
+ smoke: 0.8,
2713
+ steam: 1,
2714
+ fire: 1.2,
2715
+ flash: 0,
2716
+ foam: 2.5
2717
+ };
2718
+ function impactKey(def) {
2719
+ if (!def) return "impact_wood";
2720
+ const tags = def.tags ?? [];
2721
+ if (tags.includes("metal")) return "impact_metal";
2722
+ if (def.visual?.transparent || def.fracture === "shatter") return "impact_glass";
2723
+ if (def.fracture === "crumble") return "impact_masonry";
2724
+ if (def.fracture === "tear") return "impact_soft";
2725
+ return def.fracture === "none" ? "impact_metal" : "impact_wood";
2726
+ }
2727
+ function fractureKey(def, style) {
2728
+ switch (style) {
2729
+ case "shatter":
2730
+ return "shatter_glass";
2731
+ case "crumble":
2732
+ return "crumble";
2733
+ case "splinter":
2734
+ return "splinter";
2735
+ default:
2736
+ return impactKey(def);
2737
+ }
2738
+ }
2739
+ function fallbackColor(id, def) {
2740
+ if (def?.tags?.includes("metal")) return 9080726;
2741
+ if (def?.visual?.transparent) return 10473696;
2742
+ const h = hashString(id);
2743
+ return mixHex(7035464, 10130568, h % 100 / 100);
2744
+ }
2745
+ function materialsOf2(m) {
2746
+ return Array.isArray(m) ? m : [m];
2747
+ }
2748
+ function createDestructionRenderer(THREE, options) {
2749
+ return new Renderer(THREE, options);
2750
+ }
2751
+ var Renderer = class {
2752
+ constructor(THREE, o) {
2753
+ __publicField(this, "THREE", THREE);
2754
+ __publicField(this, "o", o);
2755
+ __publicField(this, "world");
2756
+ __publicField(this, "scene");
2757
+ __publicField(this, "quality");
2758
+ __publicField(this, "low");
2759
+ __publicField(this, "groundAt");
2760
+ __publicField(this, "gravity");
2761
+ __publicField(this, "autoSplit");
2762
+ __publicField(this, "maxFragments");
2763
+ __publicField(this, "bounds", /* @__PURE__ */ new Map());
2764
+ __publicField(this, "queue", []);
2765
+ __publicField(this, "unsubscribe");
2766
+ __publicField(this, "visuals");
2767
+ __publicField(this, "particles");
2768
+ __publicField(this, "fragments");
2769
+ __publicField(this, "ground");
2770
+ __publicField(this, "pieceSys");
2771
+ __publicField(this, "active", /* @__PURE__ */ new Map());
2772
+ __publicField(this, "fires", /* @__PURE__ */ new Map());
2773
+ __publicField(this, "leaks", []);
2774
+ __publicField(this, "homeTags", []);
2775
+ __publicField(this, "throttles", /* @__PURE__ */ new Map());
2776
+ __publicField(this, "rng", createRng(12648430));
2777
+ __publicField(this, "light", null);
2778
+ __publicField(this, "lightT", 0);
2779
+ __publicField(this, "lightDur", 0.3);
2780
+ __publicField(this, "lightPeak", 0);
2781
+ __publicField(this, "time", 0);
2782
+ __publicField(this, "scanTimer", 0);
2783
+ __publicField(this, "disposed", false);
2784
+ __publicField(this, "box");
2785
+ __publicField(this, "tmpQ");
2786
+ __publicField(this, "v1");
2787
+ __publicField(this, "v2");
2788
+ this.world = o.world;
2789
+ this.scene = o.scene;
2790
+ this.quality = o.quality ?? "medium";
2791
+ this.low = this.quality === "low";
2792
+ this.groundAt = o.groundAt ?? (() => 0);
2793
+ this.gravity = o.gravity ?? 9.81;
2794
+ this.autoSplit = o.autoSplit !== false;
2795
+ this.maxFragments = o.maxFragments ?? 160;
2796
+ this.box = new THREE.Box3();
2797
+ this.tmpQ = new THREE.Quaternion();
2798
+ this.v1 = new THREE.Vector3();
2799
+ this.v2 = new THREE.Vector3();
2800
+ const maxParticles = Math.max(8, Math.floor((o.maxParticles ?? 2500) * (this.low ? 0.5 : 1)));
2801
+ this.visuals = new Visuals(THREE, this.low, 600);
2802
+ this.ground = new GroundSpots(THREE, o.scene, this.groundAt, 48);
2803
+ this.particles = new ParticleSystem(THREE, {
2804
+ capacity: maxParticles,
2805
+ scene: o.scene,
2806
+ density: this.low ? 0.6 : 1,
2807
+ pointScale: o.pointScale ?? 700,
2808
+ groundAt: this.groundAt,
2809
+ gravity: this.gravity,
2810
+ onLand: (x, _y, z, color, tag) => {
2811
+ const home = this.homeTags[tag - 1] ?? "";
2812
+ this.ground.add("puddle", x, z, 0.05 + this.rng() * 0.04, color, home);
2813
+ }
2814
+ });
2815
+ this.fragments = new FragmentSystem(THREE, {
2816
+ scene: o.scene,
2817
+ groundAt: this.groundAt,
2818
+ gravity: this.gravity,
2819
+ max: this.maxFragments,
2820
+ life: o.fragmentLife ?? 12
2821
+ });
2822
+ this.pieceSys = new PieceSystem(THREE, {
2823
+ groundAt: this.groundAt,
2824
+ gravity: this.gravity,
2825
+ scene: o.scene,
2826
+ onHardLanding: (p, point, speed) => this.hardLanding(p, point, speed)
2827
+ });
2828
+ this.unsubscribe = this.world.on("*", (ev) => {
2829
+ if (this.queue.length < QUEUE_CAP) this.queue.push(ev);
2830
+ });
2831
+ }
2832
+ // ----------------------------------------------------------------------------------------------------------------------------------
2833
+ // Binding
2834
+ // ----------------------------------------------------------------------------------------------------------------------------------
2835
+ bind(objectId, root, options = {}) {
2836
+ if (this.bounds.has(objectId)) this.unbind(objectId);
2837
+ const state = this.world.state(objectId);
2838
+ const partIds = new Set((state?.parts ?? []).map((p) => p.id));
2839
+ const byPart = /* @__PURE__ */ new Map();
2840
+ const taken = /* @__PURE__ */ new Set();
2841
+ const add = (pid, m) => {
2842
+ if (taken.has(m)) return;
2843
+ taken.add(m);
2844
+ const l = byPart.get(pid);
2845
+ if (l) l.push(m);
2846
+ else byPart.set(pid, [m]);
2847
+ };
2848
+ const meshesIn = (o) => {
2849
+ const out = [];
2850
+ o.traverse((c) => {
2851
+ if (c.isMesh) out.push(c);
2852
+ });
2853
+ return out;
2854
+ };
2855
+ if (options.parts) {
2856
+ for (const [pid, v] of Object.entries(options.parts)) {
2857
+ for (const o of Array.isArray(v) ? v : [v]) for (const m of meshesIn(o)) add(pid, m);
2858
+ }
2859
+ } else {
2860
+ const leftovers = [];
2861
+ for (const m of meshesIn(root)) {
2862
+ const ud = partHintOf(m.userData?.destructionPart)?.id;
2863
+ if (ud !== void 0 && partIds.has(ud)) add(ud, m);
2864
+ else if (partIds.has(m.name)) add(m.name, m);
2865
+ else leftovers.push(m);
2866
+ }
2867
+ if (leftovers.length) this.matchByBox(objectId, root, leftovers, add);
2868
+ }
2869
+ const bound = { id: objectId, root, sync: options.syncTransform !== false, parts: /* @__PURE__ */ new Map(), lastPos: null, lastQuat: null, piece: null, isSplit: false };
2870
+ for (const pid of partIds) bound.parts.set(pid, this.makePartVis(objectId, pid, state?.parts.find((p) => p.id === pid)?.material ?? "", byPart.get(pid) ?? []));
2871
+ for (const [pid, meshes] of byPart) {
2872
+ if (!bound.parts.has(pid)) bound.parts.set(pid, this.makePartVis(objectId, pid, this.world.part(objectId, pid)?.material ?? "", meshes));
2873
+ }
2874
+ this.bounds.set(objectId, bound);
2875
+ }
2876
+ makePartVis(home, id, material, meshes) {
2877
+ return { id, home, material, meshes: meshes.map((m) => this.visuals.makeMeshVis(m, home)), charApplied: 0, glowApplied: 0, meltApplied: 0, gone: "none" };
2878
+ }
2879
+ /** Match unlabelled meshes to the part whose shape contains the centre of the mesh's bounding box. */
2880
+ matchByBox(objectId, root, meshes, add) {
2881
+ let specParts;
2882
+ try {
2883
+ specParts = this.world.serialize?.().objects.find((o) => o.id === objectId)?.spec.parts;
2884
+ } catch {
2885
+ specParts = void 0;
2886
+ }
2887
+ if (!specParts) return;
2888
+ root.updateMatrixWorld(true);
2889
+ const e = new this.THREE.Euler();
2890
+ const q = new this.THREE.Quaternion();
2891
+ for (const m of meshes) {
2892
+ this.box.setFromObject(m);
2893
+ if (this.box.isEmpty()) continue;
2894
+ const c = root.worldToLocal(this.box.getCenter(this.v1));
2895
+ let best = null;
2896
+ let bestVol = Infinity;
2897
+ for (const p of specParts) {
2898
+ const l = this.v2.set(c.x - p.position[0], c.y - p.position[1], c.z - p.position[2]);
2899
+ if (p.rotation) l.applyQuaternion(q.setFromEuler(e.set(p.rotation[0], p.rotation[1], p.rotation[2], "XYZ")).invert());
2900
+ let inside = false;
2901
+ let vol = 0;
2902
+ const s = p.shape;
2903
+ if (s.kind === "box") {
2904
+ 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;
2905
+ vol = s.size[0] * s.size[1] * s.size[2];
2906
+ } else if (s.kind === "cylinder") {
2907
+ inside = Math.hypot(l.x, l.z) <= s.radius && Math.abs(l.y) <= s.height / 2;
2908
+ vol = Math.PI * s.radius * s.radius * s.height;
2909
+ } else {
2910
+ inside = l.length() <= s.radius;
2911
+ vol = 4 / 3 * Math.PI * s.radius ** 3;
2912
+ }
2913
+ if (inside && vol < bestVol) {
2914
+ best = p;
2915
+ bestVol = vol;
2916
+ }
2917
+ }
2918
+ if (best) add(best.id, m);
2919
+ }
2920
+ }
2921
+ unbind(objectId) {
2922
+ const b = this.bounds.get(objectId);
2923
+ if (!b) return;
2924
+ this.restore(objectId);
2925
+ this.pieceSys.remove(objectId);
2926
+ this.bounds.delete(objectId);
2927
+ }
2928
+ // ----------------------------------------------------------------------------------------------------------------------------------
2929
+ // Pieces
2930
+ // ----------------------------------------------------------------------------------------------------------------------------------
2931
+ /** Half extents and centre of the box around some meshes, in the local space of `root`. */
2932
+ measure(root, meshes) {
2933
+ root.updateMatrixWorld(true);
2934
+ const inv = root.matrixWorld.clone().invert();
2935
+ const total = new this.THREE.Box3();
2936
+ const m4 = new this.THREE.Matrix4();
2937
+ for (const m of meshes) {
2938
+ const g = m.geometry;
2939
+ if (!g.boundingBox) g.computeBoundingBox();
2940
+ if (!g.boundingBox || g.boundingBox.isEmpty()) continue;
2941
+ total.union(g.boundingBox.clone().applyMatrix4(m4.multiplyMatrices(inv, m.matrixWorld)));
2942
+ }
2943
+ if (total.isEmpty()) return { half: [0.25, 0.25, 0.25], center: new this.THREE.Vector3() };
2944
+ const size = total.getSize(new this.THREE.Vector3());
2945
+ 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()) };
2946
+ }
2947
+ meshesOf(b) {
2948
+ const out = [];
2949
+ for (const pv of b.parts.values()) if (pv.gone === "none") for (const mv of pv.meshes) out.push(mv.mesh);
2950
+ return out;
2951
+ }
2952
+ massOf(objectId) {
2953
+ const st = this.world.state(objectId);
2954
+ if (!st) return 5;
2955
+ let m = 0;
2956
+ for (const p of st.parts) if (p.status !== "broken" && p.status !== "melted" && p.status !== "ashes") m += p.mass;
2957
+ return m > 0 ? m : 5;
2958
+ }
2959
+ makePiece(objectId, options = {}) {
2960
+ const b = this.bounds.get(objectId);
2961
+ if (!b) throw new Error(`game-destruction-three: object "${objectId}" is not bound`);
2962
+ if (b.root.parent && b.root.parent !== this.scene) this.scene.attach(b.root);
2963
+ const { half, center } = this.measure(b.root, this.meshesOf(b));
2964
+ const piece = this.pieceSys.create(objectId, b.root, half, center, options.mass ?? this.massOf(objectId), options.velocity);
2965
+ b.piece = piece;
2966
+ return piece;
2967
+ }
2968
+ pieces() {
2969
+ return [...this.pieceSys.list];
2970
+ }
2971
+ piece(objectId) {
2972
+ return this.pieceSys.get(objectId);
2973
+ }
2974
+ impulse(objectId, impulse, point) {
2975
+ const p = this.pieceSys.get(objectId);
2976
+ if (p) this.pieceSys.impulse(p, impulse, point);
2977
+ }
2978
+ hardLanding(p, point, speed) {
2979
+ const key = this.impactSoundOf(p.objectId);
2980
+ this.sound(key, point, clamp(speed / 10, 0.2, 1));
2981
+ this.syncBound(p.objectId);
2982
+ try {
2983
+ this.world.hit(p.objectId, { type: "blunt", point, energy: 0.5 * p.mass * speed * speed, radius: 0.08, direction: [0, -1, 0], source: "fall" });
2984
+ } catch {
2985
+ }
2986
+ }
2987
+ impactSoundOf(objectId) {
2988
+ const st = this.world.state(objectId);
2989
+ let best;
2990
+ let mass = -1;
2991
+ for (const p of st?.parts ?? []) {
2992
+ if (p.mass > mass) {
2993
+ mass = p.mass;
2994
+ best = this.world.materials.get(p.material);
2995
+ }
2996
+ }
2997
+ return impactKey(best);
2998
+ }
2999
+ // ----------------------------------------------------------------------------------------------------------------------------------
3000
+ // Update
3001
+ // ----------------------------------------------------------------------------------------------------------------------------------
3002
+ update(dt) {
3003
+ if (this.disposed) return;
3004
+ dt = clamp(dt, 0, 0.1);
3005
+ this.time += dt;
3006
+ this.drainQueue();
3007
+ this.updateActive(dt);
3008
+ this.updateLeaks(dt);
3009
+ this.pieceSys.update(dt);
3010
+ this.fragments.update(dt);
3011
+ this.ground.update(dt);
3012
+ this.updateLight(dt);
3013
+ this.particles.update(dt);
3014
+ this.syncTransforms();
3015
+ }
3016
+ drainQueue() {
3017
+ const batch = this.queue.splice(0, this.queue.length);
3018
+ for (const ev of batch) {
3019
+ try {
3020
+ this.apply(ev);
3021
+ } catch (err) {
3022
+ if (typeof console !== "undefined") console.error("game-destruction-three: failed to apply", ev.type, err);
3023
+ }
3024
+ }
3025
+ }
3026
+ syncTransforms() {
3027
+ for (const b of this.bounds.values()) this.syncOne(b);
3028
+ }
3029
+ /** Push the current transform of one bound object to the world right now (used before a landing hit). */
3030
+ syncBound(objectId) {
3031
+ const b = this.bounds.get(objectId);
3032
+ if (b) this.syncOne(b);
3033
+ }
3034
+ syncOne(b) {
3035
+ if (!b.sync) return;
3036
+ const p = this.v1;
3037
+ const q = this.tmpQ;
3038
+ b.root.updateWorldMatrix(true, false);
3039
+ b.root.matrixWorld.decompose(p, q, this.v2);
3040
+ const last = b.lastPos;
3041
+ const lq = b.lastQuat;
3042
+ 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;
3043
+ if (!moved) return;
3044
+ b.lastPos = [p.x, p.y, p.z];
3045
+ b.lastQuat = [q.x, q.y, q.z, q.w];
3046
+ 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]);
3047
+ }
3048
+ // ----------------------------------------------------------------------------------------------------------------------------------
3049
+ // Events
3050
+ // ----------------------------------------------------------------------------------------------------------------------------------
3051
+ apply(ev) {
3052
+ switch (ev.type) {
3053
+ case "damage":
3054
+ if (ev.damageType === "heat" || ev.damageType === "fire" || ev.damageType === "explosive") this.activate(ev.object, ev.part);
3055
+ break;
3056
+ case "dent":
3057
+ this.onDent(ev);
3058
+ break;
3059
+ case "hole":
3060
+ this.onHole(ev);
3061
+ break;
3062
+ case "crack":
3063
+ this.onCrack(ev);
3064
+ break;
3065
+ case "ricochet": {
3066
+ const d = ev.direction;
3067
+ this.particles.burst("sparks", ev.point, d, 4 + Math.min(10, ev.energy / 150), 4);
3068
+ this.sound("ricochet", ev.point, 0.7);
3069
+ break;
3070
+ }
3071
+ case "debris":
3072
+ 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));
3073
+ break;
3074
+ case "fracture":
3075
+ this.onFracture(ev);
3076
+ break;
3077
+ case "detach":
3078
+ this.onDetach(ev);
3079
+ break;
3080
+ case "disassemble_progress": {
3081
+ const k = `unscrew:${ev.joint}`;
3082
+ if (this.time - (this.throttles.get(k) ?? -1) > 0.25) {
3083
+ this.throttles.set(k, this.time);
3084
+ this.sound("unscrew", this.rootPoint(ev.object), clamp(0.3 + ev.progress * 0.5, 0, 1));
3085
+ }
3086
+ break;
3087
+ }
3088
+ case "ignite":
3089
+ this.fires.set(`${ev.object}|${ev.part}`, { object: ev.object, part: ev.part, point: ev.point, accFire: 0, accSmoke: 0 });
3090
+ this.activate(ev.object, ev.part);
3091
+ this.particles.burst("fire", ev.point, [0, 1, 0], 6, 1);
3092
+ this.sound("ignite", ev.point, 0.7);
3093
+ this.sound("burn_loop", ev.point, 0.5);
3094
+ break;
3095
+ case "extinguish": {
3096
+ const f = this.fires.get(`${ev.object}|${ev.part}`);
3097
+ if (f) {
3098
+ this.fires.delete(`${ev.object}|${ev.part}`);
3099
+ this.particles.burst("steam", f.point, [0, 1, 0], 14, 1.2);
3100
+ }
3101
+ this.activate(ev.object, ev.part);
3102
+ this.sound("extinguish", f?.point ?? this.rootPoint(ev.object), 0.6);
3103
+ break;
3104
+ }
3105
+ case "burnout": {
3106
+ this.fires.delete(`${ev.object}|${ev.part}`);
3107
+ this.particles.burst("ash", ev.point, [0, 1, 0], 16, 1);
3108
+ this.particles.burst("smoke", ev.point, [0, 1, 0], 6, 0.8, { color: SMOKE_TONES.grey });
3109
+ const pv = this.partVis(ev.object, ev.part);
3110
+ if (pv) {
3111
+ const def = this.world.materials.get(pv.material);
3112
+ this.visuals.setChar(pv, 1, def?.visual?.charColor ?? 1709072);
3113
+ }
3114
+ this.activate(ev.object, ev.part);
3115
+ break;
3116
+ }
3117
+ case "melt":
3118
+ this.activate(ev.object, ev.part);
3119
+ if (this.time - (this.throttles.get(`melt:${ev.object}|${ev.part}`) ?? -9) > 1.5) {
3120
+ this.throttles.set(`melt:${ev.object}|${ev.part}`, this.time);
3121
+ this.sound("melt_sizzle", this.rootPoint(ev.object), 0.4);
3122
+ }
3123
+ break;
3124
+ case "drip": {
3125
+ const b = this.bounds.get(ev.object);
3126
+ const pv = b?.parts.get(ev.part);
3127
+ const def = this.world.materials.get(ev.material);
3128
+ const color = (pv && this.partColor(pv, def)) ?? fallbackColor(ev.material, def);
3129
+ let tag = this.homeTags.indexOf(ev.object) + 1;
3130
+ if (tag === 0) tag = this.homeTags.push(ev.object);
3131
+ this.particles.spawn("drips", ev.point, [(this.rng() - 0.5) * 0.1, -0.1, (this.rng() - 0.5) * 0.1], { color, tag });
3132
+ break;
3133
+ }
3134
+ case "leak": {
3135
+ const key = `${ev.object}|${ev.part}`;
3136
+ 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);
3137
+ if (same) {
3138
+ same.rate = ev.rate;
3139
+ same.direction = ev.direction;
3140
+ } else {
3141
+ if (this.leaks.length >= 24) this.leaks.shift();
3142
+ 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 });
3143
+ }
3144
+ void key;
3145
+ break;
3146
+ }
3147
+ case "explode":
3148
+ this.onExplode(ev);
3149
+ break;
3150
+ case "destroyed":
3151
+ break;
3152
+ }
3153
+ }
3154
+ rootPoint(objectId) {
3155
+ const b = this.bounds.get(objectId);
3156
+ return b ? [b.root.position.x, b.root.position.y, b.root.position.z] : [0, 0, 0];
3157
+ }
3158
+ sound(key, point, volume) {
3159
+ this.o.onSound?.(key, point, volume);
3160
+ }
3161
+ partVis(objectId, partId) {
3162
+ return this.bounds.get(objectId)?.parts.get(partId) ?? null;
3163
+ }
3164
+ activate(objectId, partId) {
3165
+ this.active.set(`${objectId}|${partId}`, { object: objectId, part: partId });
3166
+ }
3167
+ /** Current colour of the part's first mesh (char included), or null. */
3168
+ partColor(pv, def) {
3169
+ const mv = pv.meshes[0];
3170
+ if (!mv) return null;
3171
+ const mat = materialsOf2(mv.mesh.material)[0];
3172
+ const c = mat?.color;
3173
+ if (c && c.isColor) {
3174
+ const tint = this.visuals.vertexTint(mv);
3175
+ return tint ? c.clone().multiply(tint).getHex() : c.getHex();
3176
+ }
3177
+ return def ? fallbackColor(def.id, def) : null;
3178
+ }
3179
+ debrisOptions(kind, objectId, partId) {
3180
+ const pv = this.partVis(objectId, partId);
3181
+ const def = pv ? this.world.materials.get(pv.material) : void 0;
3182
+ if (kind === "smoke") return { color: SMOKE_TONES[def?.thermal.smoke ?? "grey"] };
3183
+ if (kind === "fibers" || kind === "leaves") {
3184
+ const c = pv ? this.visuals.baseColorHex(pv.meshes[0] ?? {}) : -1;
3185
+ return c >= 0 ? { color: c } : void 0;
3186
+ }
3187
+ if (kind === "dust" || kind === "chips") {
3188
+ const c = pv && pv.meshes[0] ? this.visuals.baseColorHex(pv.meshes[0]) : -1;
3189
+ return c >= 0 && kind === "chips" ? { color: c } : void 0;
3190
+ }
3191
+ return void 0;
3192
+ }
3193
+ /** Mesh of the part closest to a world point. */
3194
+ nearestMesh(pv, p) {
3195
+ let best = null;
3196
+ let bd = Infinity;
3197
+ for (const mv of pv.meshes) {
3198
+ if (!mv.mesh.visible) continue;
3199
+ const d = this.box.setFromObject(mv.mesh).distanceToPoint(p);
3200
+ if (d < bd) {
3201
+ bd = d;
3202
+ best = mv;
3203
+ }
3204
+ }
3205
+ return best;
3206
+ }
3207
+ worldDir(b, v) {
3208
+ b.root.updateWorldMatrix(true, false);
3209
+ return new this.THREE.Vector3(v[0], v[1], v[2]).applyQuaternion(b.root.getWorldQuaternion(this.tmpQ)).normalize();
3210
+ }
3211
+ onDent(ev) {
3212
+ const b = this.bounds.get(ev.object);
3213
+ const pv = b?.parts.get(ev.part);
3214
+ if (!b || !pv) return;
3215
+ const def = this.world.materials.get(pv.material);
3216
+ const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
3217
+ const wn = this.worldDir(b, ev.dent.normal);
3218
+ for (const mv of pv.meshes) {
3219
+ if (!mv.mesh.visible) continue;
3220
+ this.visuals.applyDent(mv, wp, wn, ev.dent);
3221
+ }
3222
+ 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);
3223
+ this.sound(impactKey(def), ev.worldPoint, clamp(0.3 + ev.dent.depth * 12, 0.3, 1));
3224
+ }
3225
+ onHole(ev) {
3226
+ const b = this.bounds.get(ev.object);
3227
+ const pv = b?.parts.get(ev.part);
3228
+ if (!b || !pv) return;
3229
+ const def = this.world.materials.get(pv.material);
3230
+ const n = this.worldDir(b, ev.hole.normal);
3231
+ const draw = (point, normal) => {
3232
+ const wp = new this.THREE.Vector3(point[0], point[1], point[2]);
3233
+ const mv = this.nearestMesh(pv, wp);
3234
+ if (!mv) return;
3235
+ if (def?.visual?.transparent) {
3236
+ const seed = hashNumbers(ev.time, point[0], point[1], point[2]);
3237
+ this.visuals.addCrack(mv, wp, normal, 7, Math.max(0.05, ev.hole.diameter * 5), 15267071, seed);
3238
+ } else {
3239
+ this.visuals.addHole(mv, wp, normal, ev.hole.diameter);
3240
+ }
3241
+ };
3242
+ draw(ev.worldPoint, n);
3243
+ if (ev.hole.through && ev.exitPoint) draw(ev.exitPoint, n.clone().negate());
3244
+ if (def?.tags?.includes("metal")) this.particles.burst("sparks", ev.worldPoint, [n.x, n.y, n.z], 6, 3.5);
3245
+ this.sound(impactKey(def), ev.worldPoint, 0.6);
3246
+ }
3247
+ onCrack(ev) {
3248
+ const b = this.bounds.get(ev.object);
3249
+ const pv = b?.parts.get(ev.part);
3250
+ if (!b || !pv) return;
3251
+ const def = this.world.materials.get(pv.material);
3252
+ const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
3253
+ const mv = this.nearestMesh(pv, wp);
3254
+ if (mv) {
3255
+ const color = def?.visual?.transparent ? 15267071 : 2762532;
3256
+ 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]));
3257
+ }
3258
+ this.sound(impactKey(def), ev.worldPoint, 0.5);
3259
+ }
3260
+ onFracture(ev) {
3261
+ const b = this.bounds.get(ev.object);
3262
+ const pv = b?.parts.get(ev.part);
3263
+ const matId = pv?.material || (ev.fragments[0]?.material ?? "");
3264
+ const def = this.world.materials.get(matId);
3265
+ let color = fallbackColor(matId, def);
3266
+ let opacity = 0.5;
3267
+ const transparent = def?.visual?.transparent === true;
3268
+ if (pv) {
3269
+ const c = this.partColor(pv, def);
3270
+ if (c !== null) color = c;
3271
+ const m = pv.meshes[0] ? materialsOf2(pv.meshes[0].origMaterial)[0] : void 0;
3272
+ if (m && m.transparent) opacity = clamp(m.opacity, 0.2, 0.8);
3273
+ this.visuals.hide(pv, "broken");
3274
+ }
3275
+ const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
3276
+ const step = this.low ? 2 : 1;
3277
+ for (let i = 0; i < ev.fragments.length; i += step) {
3278
+ const fr = ev.fragments[i];
3279
+ const fdef = this.world.materials.get(fr.material);
3280
+ const mat = this.fragments.materialFor(fr.material, fdef === def || !fdef ? color : fallbackColor(fr.material, fdef), transparent || fdef?.visual?.transparent === true, opacity);
3281
+ this.fragments.spawn(fr, mat, ev.object, maxAlive);
3282
+ }
3283
+ const up = [0, 1, 0];
3284
+ const sizeK = clamp(ev.fragments.length / 10, 0.5, 2);
3285
+ switch (ev.style) {
3286
+ case "shatter":
3287
+ this.particles.burst("shards", ev.point, up, 18 * sizeK, 3);
3288
+ break;
3289
+ case "crumble":
3290
+ this.particles.burst("dust", ev.point, up, 16 * sizeK, 1.2, { color: pv ? this.partColor(pv, def) ?? void 0 : void 0 });
3291
+ this.particles.burst("chips", ev.point, up, 6, 3);
3292
+ break;
3293
+ case "splinter":
3294
+ this.particles.burst("splinters", ev.point, up, 14 * sizeK, 3);
3295
+ this.particles.burst("dust", ev.point, up, 5, 0.8, { color: 11901556 });
3296
+ break;
3297
+ case "tear":
3298
+ this.particles.burst("fibers", ev.point, up, 12 * sizeK, 1.6, { color });
3299
+ break;
3300
+ case "snap":
3301
+ this.particles.burst("chips", ev.point, up, 8 * sizeK, 3);
3302
+ break;
3303
+ default:
3304
+ this.particles.burst("sparks", ev.point, up, 10, 4);
3305
+ }
3306
+ this.sound(fractureKey(def, ev.style), ev.point, clamp(0.4 + ev.fragments.length * 0.03, 0.4, 1));
3307
+ }
3308
+ onDetach(ev) {
3309
+ this.sound("detach", this.rootPoint(ev.object), 0.5);
3310
+ if (!this.autoSplit) return;
3311
+ const b = this.bounds.get(ev.object);
3312
+ if (!b) return;
3313
+ const st = this.world.state(ev.object);
3314
+ if (!st || st.groups.length < 2) return;
3315
+ const loose = st.groups.length - 1;
3316
+ for (let i = 0; i < loose; i++) {
3317
+ let newId;
3318
+ try {
3319
+ newId = this.world.splitGroup(ev.object, 1);
3320
+ } catch {
3321
+ break;
3322
+ }
3323
+ this.adoptSplit(b, newId);
3324
+ }
3325
+ const old = b.piece;
3326
+ if (old) {
3327
+ const { half, center } = this.measure(b.root, this.meshesOf(b));
3328
+ old.halfExtents = half;
3329
+ this.pieceSys.setCenter(old, center);
3330
+ old.mass = Math.max(0.05, this.massOf(ev.object));
3331
+ old.sleeping = false;
3332
+ }
3333
+ }
3334
+ /** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
3335
+ adoptSplit(from, newId) {
3336
+ const state = this.world.state(newId);
3337
+ const root = from.root;
3338
+ root.updateWorldMatrix(true, false);
3339
+ const group = new this.THREE.Group();
3340
+ group.name = `destruction-piece-${newId}`;
3341
+ root.getWorldPosition(group.position);
3342
+ root.getWorldQuaternion(group.quaternion);
3343
+ this.scene.add(group);
3344
+ group.updateMatrixWorld(true);
3345
+ const nb = { id: newId, root: group, sync: true, parts: /* @__PURE__ */ new Map(), lastPos: null, lastQuat: null, piece: null, isSplit: true };
3346
+ for (const p of state?.parts ?? []) {
3347
+ const pv = from.parts.get(p.id);
3348
+ if (!pv) continue;
3349
+ from.parts.delete(p.id);
3350
+ nb.parts.set(p.id, pv);
3351
+ for (const mv of pv.meshes) {
3352
+ group.attach(mv.mesh);
3353
+ mv.moved = true;
3354
+ }
3355
+ }
3356
+ this.bounds.set(newId, nb);
3357
+ const meshes = this.meshesOf(nb);
3358
+ if (meshes.length === 0) return;
3359
+ const { half, center } = this.measure(group, meshes);
3360
+ let vel = [0, 0, 0];
3361
+ let ang = [0, 0, 0];
3362
+ const op = from.piece;
3363
+ if (op) {
3364
+ vel = [...op.velocity];
3365
+ ang = [...op.angularVelocity];
3366
+ const c = this.pieceSys.centerOf(op, new this.THREE.Vector3());
3367
+ const nc = center.clone().applyQuaternion(group.quaternion).add(group.position);
3368
+ vel[0] += ang[1] * (nc.z - c.z) - ang[2] * (nc.y - c.y);
3369
+ vel[1] += ang[2] * (nc.x - c.x) - ang[0] * (nc.z - c.z);
3370
+ vel[2] += ang[0] * (nc.y - c.y) - ang[1] * (nc.x - c.x);
3371
+ }
3372
+ const piece = this.pieceSys.create(newId, group, half, center, this.massOf(newId), vel, ang);
3373
+ nb.piece = piece;
3374
+ this.o.onPiece?.(piece);
3375
+ }
3376
+ onExplode(ev) {
3377
+ const p = ev.point;
3378
+ const k = clamp(ev.radius / 3, 0.4, 2.5);
3379
+ this.particles.burst("flash", p, [0, 1, 0], 1, 0, { size: k });
3380
+ this.particles.burst("fire", p, [0, 1, 0], 34 * k, 4 * k, { size: 1 + k * 0.5 });
3381
+ this.particles.burst("smoke", p, [0, 1, 0], 24 * k, 2.2, { color: SMOKE_TONES.black, size: 1 + k * 0.6 });
3382
+ this.particles.burst("dust", p, [0, 1, 0], 22 * k, 3.5 * k);
3383
+ this.particles.burst("sparks", p, [0, 1, 0], 16 * k, 8);
3384
+ this.particles.push(p, ev.radius, 4);
3385
+ if (this.o.explosionLight !== false) {
3386
+ if (!this.light) {
3387
+ this.light = new this.THREE.PointLight(16754784, 0, 10, 2);
3388
+ this.light.name = "destruction-explosion-light";
3389
+ }
3390
+ if (!this.light.parent) this.scene.add(this.light);
3391
+ this.light.position.set(p[0], p[1] + 0.5, p[2]);
3392
+ this.light.distance = Math.max(6, ev.radius * 3);
3393
+ this.lightPeak = clamp(Math.sqrt(Math.max(1, ev.energy)) * 0.6, 10, 400);
3394
+ this.lightDur = 0.35;
3395
+ this.lightT = this.lightDur;
3396
+ }
3397
+ const c = this.v1;
3398
+ for (const pc of this.pieceSys.list) {
3399
+ this.pieceSys.centerOf(pc, c);
3400
+ const dx = c.x - p[0];
3401
+ const dy = c.y - p[1];
3402
+ const dz = c.z - p[2];
3403
+ const d = Math.hypot(dx, dy, dz);
3404
+ if (d > ev.radius) continue;
3405
+ const fall = (1 - d / ev.radius) ** 2;
3406
+ const speed = Math.min(30, Math.sqrt(2 * ev.energy * 0.02 / pc.mass)) * fall;
3407
+ const inv = d > 1e-4 ? 1 / d : 0;
3408
+ const dir = d > 1e-4 ? [dx * inv, dy * inv + 0.3, dz * inv] : [0, 1, 0];
3409
+ const spread = pc.halfExtents[0];
3410
+ 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]);
3411
+ }
3412
+ this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(ev.energy) * 0.08));
3413
+ this.sound("explode", p, clamp(0.5 + ev.radius / 10, 0.5, 1));
3414
+ }
3415
+ updateLight(dt) {
3416
+ if (!this.light || this.lightT <= 0) return;
3417
+ this.lightT = Math.max(0, this.lightT - dt);
3418
+ const t = this.lightT / this.lightDur;
3419
+ this.light.intensity = this.lightPeak * t * t;
3420
+ }
3421
+ // ----------------------------------------------------------------------------------------------------------------------------------
3422
+ // Fire, heat, char, melt, leaks
3423
+ // ----------------------------------------------------------------------------------------------------------------------------------
3424
+ updateActive(dt) {
3425
+ this.scanTimer += dt;
3426
+ if (this.scanTimer >= 0.4) {
3427
+ this.scanTimer = 0;
3428
+ for (const b of this.bounds.values()) {
3429
+ for (const pv of b.parts.values()) {
3430
+ if (pv.gone !== "none" && pv.gone !== "broken") continue;
3431
+ const ps = this.world.part(b.id, pv.id);
3432
+ if (!ps) continue;
3433
+ const def = this.world.materials.get(pv.material);
3434
+ 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")) {
3435
+ this.activate(b.id, pv.id);
3436
+ }
3437
+ }
3438
+ }
3439
+ }
3440
+ for (const [key, a] of this.active) {
3441
+ const pv = this.partVis(a.object, a.part);
3442
+ const ps = pv ? this.world.part(a.object, a.part) : null;
3443
+ if (!pv || !ps) {
3444
+ this.active.delete(key);
3445
+ continue;
3446
+ }
3447
+ const def = this.world.materials.get(pv.material);
3448
+ if (pv.gone === "none") {
3449
+ this.visuals.setChar(pv, ps.char, def?.visual?.charColor ?? 1709072);
3450
+ if (def?.visual?.glows) this.visuals.setGlow(pv, ps.temperature);
3451
+ this.visuals.setMelt(pv, ps.melt);
3452
+ if (ps.status === "melted") this.onGone(a.object, pv, "melted", def);
3453
+ else if (ps.status === "ashes") this.onGone(a.object, pv, "ashes", def);
3454
+ }
3455
+ const fire = this.fires.get(key);
3456
+ if (fire) {
3457
+ if (ps.burning && ps.status !== "broken" && ps.status !== "ashes" && ps.status !== "melted") this.emitFire(fire, pv, def, dt);
3458
+ else this.fires.delete(key);
3459
+ }
3460
+ const hot = def?.visual?.glows === true && ps.temperature > GLOW_FROM_C;
3461
+ if (!ps.burning && !hot && Math.abs(ps.char - pv.charApplied) < 0.01 && Math.abs(ps.melt - pv.meltApplied) < 0.01) {
3462
+ if (def?.visual?.glows && pv.gone === "none") this.visuals.setGlow(pv, 0);
3463
+ this.active.delete(key);
3464
+ }
3465
+ }
3466
+ }
3467
+ onGone(objectId, pv, reason, def) {
3468
+ const bb = new this.THREE.Box3();
3469
+ for (const mv of pv.meshes) bb.union(this.box.setFromObject(mv.mesh));
3470
+ this.visuals.hide(pv, reason);
3471
+ if (bb.isEmpty()) return;
3472
+ const c = bb.getCenter(new this.THREE.Vector3());
3473
+ const s = bb.getSize(new this.THREE.Vector3());
3474
+ const footprint = Math.sqrt(Math.max(s.x * s.z, 1e-4));
3475
+ if (reason === "melted") {
3476
+ this.ground.add("puddle", c.x, c.z, clamp(footprint * 0.55, 0.05, 1), this.partColor(pv, def) ?? fallbackColor(pv.material, def), objectId);
3477
+ } else {
3478
+ this.ground.add("ash", c.x, c.z, clamp(footprint * 0.35, 0.04, 0.5), 1907224, objectId);
3479
+ this.particles.burst("ash", [c.x, bb.max.y, c.z], [0, 1, 0], 10, 0.8);
3480
+ }
3481
+ }
3482
+ emitFire(f, pv, def, dt) {
3483
+ const bb = this.box;
3484
+ bb.makeEmpty();
3485
+ const tmp = new this.THREE.Box3();
3486
+ for (const mv of pv.meshes) if (mv.mesh.visible) bb.union(tmp.setFromObject(mv.mesh));
3487
+ let cx = f.point[0];
3488
+ let cy = f.point[1];
3489
+ let cz = f.point[2];
3490
+ let sx = 0.2;
3491
+ let sy = 0.2;
3492
+ let sz = 0.2;
3493
+ if (!bb.isEmpty()) {
3494
+ const c = bb.getCenter(this.v1);
3495
+ const s = bb.getSize(this.v2);
3496
+ cx = c.x;
3497
+ cy = bb.min.y;
3498
+ cz = c.z;
3499
+ sx = s.x;
3500
+ sy = s.y;
3501
+ sz = s.z;
3502
+ }
3503
+ const vol = clamp(Math.cbrt(Math.max(sx * sy * sz, 1e-5)), 0.05, 1.5);
3504
+ f.accFire += (18 + 70 * vol) * dt;
3505
+ f.accSmoke += (6 + 24 * vol) * dt;
3506
+ const tone = SMOKE_TONES[def?.thermal.smoke ?? "grey"];
3507
+ const rnd = this.rng;
3508
+ while (f.accFire >= 1) {
3509
+ f.accFire -= 1;
3510
+ 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 });
3511
+ }
3512
+ while (f.accSmoke >= 1) {
3513
+ f.accSmoke -= 1;
3514
+ 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 });
3515
+ }
3516
+ }
3517
+ updateLeaks(dt) {
3518
+ for (let i = this.leaks.length - 1; i >= 0; i--) {
3519
+ const l = this.leaks[i];
3520
+ const ps = this.world.part(l.object, l.part);
3521
+ if (!ps || ps.liters === void 0 || ps.liters <= 0) {
3522
+ this.leaks.splice(i, 1);
3523
+ continue;
3524
+ }
3525
+ l.acc += clamp(20 + l.rate * 400, 15, 120) * dt;
3526
+ const color = FLUID_COLORS[l.fluid] ?? 11184810;
3527
+ const d = l.direction;
3528
+ const dl = Math.hypot(d[0], d[1], d[2]) || 1;
3529
+ const sp = l.pressurized ? 2.5 + this.rng() * 1.5 : 0.3 + this.rng() * 0.3;
3530
+ while (l.acc >= 1) {
3531
+ l.acc -= 1;
3532
+ 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];
3533
+ if (l.fluid === "gas") this.particles.spawn("steam", l.point, vel, { color, size: 0.5, alpha: 0.5 });
3534
+ else this.particles.spawn("drips", l.point, vel, { color, tag: this.rng() < 0.03 ? this.tagOf(l.object) : 0 });
3535
+ }
3536
+ }
3537
+ }
3538
+ tagOf(object) {
3539
+ let tag = this.homeTags.indexOf(object) + 1;
3540
+ if (tag === 0) tag = this.homeTags.push(object);
3541
+ return tag;
3542
+ }
3543
+ // ----------------------------------------------------------------------------------------------------------------------------------
3544
+ // Emit, clear, restore, stats, dispose
3545
+ // ----------------------------------------------------------------------------------------------------------------------------------
3546
+ emit(kind, point, normal, count, speed) {
3547
+ this.particles.burst(kind, point, normal, count, speed ?? DEFAULT_SPEED[kind] ?? 1, kind === "smoke" ? { color: SMOKE_TONES.grey } : void 0);
3548
+ }
3549
+ restore(objectId) {
3550
+ const homes = /* @__PURE__ */ new Set([objectId]);
3551
+ const self = this.bounds.get(objectId);
3552
+ if (self?.isSplit) for (const pv of self.parts.values()) homes.add(pv.home);
3553
+ for (const home of homes) this.restoreHome(home);
3554
+ for (const [key, a] of [...this.active]) if (homes.has(a.object)) this.active.delete(key);
3555
+ for (const [key, f] of [...this.fires]) if (homes.has(f.object)) this.fires.delete(key);
3556
+ for (let i = this.leaks.length - 1; i >= 0; i--) if (homes.has(this.leaks[i].object)) this.leaks.splice(i, 1);
3557
+ for (const home of homes) {
3558
+ this.fragments.removeHome(home);
3559
+ this.ground.removeHome(home);
3560
+ }
3561
+ }
3562
+ restoreHome(home) {
3563
+ const homeBound = this.bounds.get(home);
3564
+ for (const b of [...this.bounds.values()]) {
3565
+ for (const [pid, pv] of [...b.parts]) {
3566
+ if (pv.home !== home) continue;
3567
+ this.visuals.restorePart(pv);
3568
+ if (b !== homeBound && homeBound) {
3569
+ b.parts.delete(pid);
3570
+ homeBound.parts.set(pid, pv);
3571
+ }
3572
+ }
3573
+ if (b.isSplit && b.parts.size === 0) {
3574
+ this.scene.remove(b.root);
3575
+ this.pieceSys.remove(b.id);
3576
+ this.bounds.delete(b.id);
3577
+ }
3578
+ }
3579
+ }
3580
+ clear() {
3581
+ this.queue.length = 0;
3582
+ this.active.clear();
3583
+ this.fires.clear();
3584
+ this.leaks.length = 0;
3585
+ for (const id of [...this.bounds.keys()]) if (this.bounds.has(id)) this.restore(id);
3586
+ for (const b of [...this.bounds.values()]) {
3587
+ if (b.isSplit) {
3588
+ for (const pv of b.parts.values()) this.visuals.restorePart(pv);
3589
+ this.scene.remove(b.root);
3590
+ this.pieceSys.remove(b.id);
3591
+ this.bounds.delete(b.id);
3592
+ }
3593
+ }
3594
+ this.fragments.clear();
3595
+ this.ground.clear();
3596
+ this.particles.clear();
3597
+ if (this.light) {
3598
+ this.light.intensity = 0;
3599
+ this.lightT = 0;
3600
+ this.scene.remove(this.light);
3601
+ }
3602
+ }
3603
+ stats() {
3604
+ return { fragments: this.fragments.count, particles: this.particles.live, pieces: this.pieceSys.list.length, decals: this.visuals.decalCount + this.ground.count };
3605
+ }
3606
+ dispose() {
3607
+ if (this.disposed) return;
3608
+ this.unsubscribe();
3609
+ this.clear();
3610
+ for (const id of [...this.bounds.keys()]) this.unbind(id);
3611
+ this.fragments.dispose();
3612
+ this.ground.dispose();
3613
+ this.particles.dispose();
3614
+ this.visuals.dispose();
3615
+ this.light?.dispose?.();
3616
+ this.light = null;
3617
+ this.disposed = true;
3618
+ }
3619
+ };
3620
+ return __toCommonJS(index_exports);
3621
+ })();
3622
+ //# sourceMappingURL=index.global.js.map