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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3545 @@
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 PieceSystem = class {
720
+ constructor(THREE, o) {
721
+ __publicField(this, "THREE", THREE);
722
+ __publicField(this, "o", o);
723
+ __publicField(this, "list", []);
724
+ __publicField(this, "time", 0);
725
+ __publicField(this, "p");
726
+ __publicField(this, "q");
727
+ __publicField(this, "dq");
728
+ __publicField(this, "m");
729
+ __publicField(this, "ax");
730
+ this.p = new THREE.Vector3();
731
+ this.q = new THREE.Quaternion();
732
+ this.dq = new THREE.Quaternion();
733
+ this.m = new THREE.Matrix4();
734
+ this.ax = new THREE.Vector3();
735
+ }
736
+ get(objectId) {
737
+ return this.list.find((b) => b.objectId === objectId) ?? null;
738
+ }
739
+ create(objectId, root, halfExtents, localCenter, mass, velocity, angular) {
740
+ const existing = this.get(objectId);
741
+ if (existing) {
742
+ existing.root = root;
743
+ existing.halfExtents = halfExtents;
744
+ existing.mass = mass;
745
+ this.setCenter(existing, localCenter);
746
+ if (velocity) existing.velocity = [...velocity];
747
+ existing.sleeping = false;
748
+ return existing;
749
+ }
750
+ const body = {
751
+ objectId,
752
+ root,
753
+ velocity: velocity ? [...velocity] : [0, 0, 0],
754
+ angularVelocity: angular ? [...angular] : [0, 0, 0],
755
+ halfExtents: [Math.max(0.01, halfExtents[0]), Math.max(0.01, halfExtents[1]), Math.max(0.01, halfExtents[2])],
756
+ mass: Math.max(0.05, mass),
757
+ sleeping: false,
758
+ held: false,
759
+ localCenter: [localCenter.x, localCenter.y, localCenter.z],
760
+ centerV: localCenter.clone(),
761
+ still: 0,
762
+ lastHit: -1,
763
+ onGround: false
764
+ };
765
+ this.list.push(body);
766
+ return body;
767
+ }
768
+ /** Move the centre of mass (after a part broke off). */
769
+ setCenter(b, c) {
770
+ b.centerV.copy(c);
771
+ b.localCenter = [c.x, c.y, c.z];
772
+ }
773
+ remove(objectId) {
774
+ const i = this.list.findIndex((b) => b.objectId === objectId);
775
+ if (i >= 0) this.list.splice(i, 1);
776
+ }
777
+ clear() {
778
+ this.list.length = 0;
779
+ }
780
+ /** World position of the centre of mass. */
781
+ centerOf(b, out) {
782
+ return out.copy(b.centerV).applyQuaternion(b.root.quaternion).add(b.root.position);
783
+ }
784
+ impulse(b, impulse, point) {
785
+ const inv = 1 / b.mass;
786
+ b.velocity[0] += impulse[0] * inv;
787
+ b.velocity[1] += impulse[1] * inv;
788
+ b.velocity[2] += impulse[2] * inv;
789
+ if (point) {
790
+ const c = this.centerOf(b, this.p);
791
+ const rx = point[0] - c.x;
792
+ const ry = point[1] - c.y;
793
+ const rz = point[2] - c.z;
794
+ const tx = ry * impulse[2] - rz * impulse[1];
795
+ const ty = rz * impulse[0] - rx * impulse[2];
796
+ const tz = rx * impulse[1] - ry * impulse[0];
797
+ const w = this.mulInvInertia(b, tx, ty, tz);
798
+ b.angularVelocity[0] += w[0];
799
+ b.angularVelocity[1] += w[1];
800
+ b.angularVelocity[2] += w[2];
801
+ }
802
+ b.sleeping = false;
803
+ b.still = 0;
804
+ }
805
+ /** 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). */
806
+ mulInvInertia(b, x, y, z) {
807
+ this.m.makeRotationFromQuaternion(b.root.quaternion);
808
+ return this.mulInvInertiaWith(b, this.m.elements, x, y, z);
809
+ }
810
+ mulInvInertiaWith(b, e, x, y, z) {
811
+ const [hx, hy, hz] = b.halfExtents;
812
+ const k = 3 / b.mass;
813
+ const ix = k / (hy * hy + hz * hz);
814
+ const iy = k / (hx * hx + hz * hz);
815
+ const iz = k / (hx * hx + hy * hy);
816
+ const lx = (e[0] * x + e[1] * y + e[2] * z) * ix;
817
+ const ly = (e[4] * x + e[5] * y + e[6] * z) * iy;
818
+ const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
819
+ 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];
820
+ }
821
+ update(dt) {
822
+ this.time += dt;
823
+ for (const b of this.list) {
824
+ if (b.held) {
825
+ b.velocity = [0, 0, 0];
826
+ b.angularVelocity = [0, 0, 0];
827
+ b.sleeping = false;
828
+ b.still = 0;
829
+ continue;
830
+ }
831
+ if (b.sleeping) continue;
832
+ this.integrate(b, dt);
833
+ }
834
+ this.separate(dt);
835
+ }
836
+ integrate(b, dt) {
837
+ const c = this.centerOf(b, this.p);
838
+ const pos = [c.x, c.y, c.z];
839
+ const q = this.q.copy(b.root.quaternion);
840
+ const v = b.velocity;
841
+ const w = b.angularVelocity;
842
+ const [hx, hy, hz] = b.halfExtents;
843
+ const invM = 1 / b.mass;
844
+ const steps = clamp(Math.ceil(dt / SUB_DT), 1, 8);
845
+ const h = dt / steps;
846
+ let landing = 0;
847
+ let landPoint = [pos[0], pos[1], pos[2]];
848
+ let contactedLast = false;
849
+ let touched = false;
850
+ for (let s = 0; s < steps; s++) {
851
+ v[1] -= this.o.gravity * h;
852
+ pos[0] += v[0] * h;
853
+ pos[1] += v[1] * h;
854
+ pos[2] += v[2] * h;
855
+ const wl = Math.hypot(w[0], w[1], w[2]);
856
+ if (wl > 1e-6) {
857
+ this.dq.setFromAxisAngle(this.ax.set(w[0] / wl, w[1] / wl, w[2] / wl), wl * h);
858
+ q.premultiply(this.dq).normalize();
859
+ }
860
+ const damp = Math.exp(-0.15 * h);
861
+ w[0] *= damp;
862
+ w[1] *= damp;
863
+ w[2] *= damp;
864
+ this.m.makeRotationFromQuaternion(q);
865
+ const e = this.m.elements;
866
+ const rs = [];
867
+ let maxPen = 0;
868
+ for (let i = 0; i < 8; i++) {
869
+ const sx = i & 1 ? hx : -hx;
870
+ const sy = i & 2 ? hy : -hy;
871
+ const sz = i & 4 ? hz : -hz;
872
+ 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];
873
+ const gy = this.o.groundAt(pos[0] + r[0], pos[2] + r[2]);
874
+ const pen = gy - (pos[1] + r[1]);
875
+ if (pen > -SLOP) {
876
+ rs.push(r);
877
+ if (pen > maxPen) maxPen = pen;
878
+ }
879
+ }
880
+ contactedLast = rs.length > 0;
881
+ if (contactedLast) touched = true;
882
+ if (!contactedLast) continue;
883
+ pos[1] += Math.max(0, maxPen - SLOP * 0.5);
884
+ let impact = 0;
885
+ for (const r of rs) impact = Math.max(impact, -(v[1] + w[2] * r[0] - w[0] * r[2]));
886
+ const restitution = impact > 2 ? 0.25 : 0;
887
+ if (impact > landing) {
888
+ landing = impact;
889
+ let sx = 0;
890
+ let sz = 0;
891
+ for (const r of rs) {
892
+ sx += r[0];
893
+ sz += r[2];
894
+ }
895
+ const lx = pos[0] + sx / rs.length;
896
+ const lz = pos[2] + sz / rs.length;
897
+ landPoint = [lx, this.o.groundAt(lx, lz), lz];
898
+ }
899
+ for (let it = 0; it < 4; it++) {
900
+ for (const r of rs) {
901
+ let vx = v[0] + (w[1] * r[2] - w[2] * r[1]);
902
+ let vy = v[1] + (w[2] * r[0] - w[0] * r[2]);
903
+ let vz = v[2] + (w[0] * r[1] - w[1] * r[0]);
904
+ const vn = vy;
905
+ if (vn >= 0) continue;
906
+ const a = this.mulInvInertiaWith(b, e, -r[2], 0, r[0]);
907
+ const denom = invM + (a[2] * r[0] - a[0] * r[2]);
908
+ const j = -(1 + (it === 0 ? restitution : 0)) * vn / denom;
909
+ v[1] += j * invM;
910
+ const dw = this.mulInvInertiaWith(b, e, -r[2] * j, 0, r[0] * j);
911
+ w[0] += dw[0];
912
+ w[1] += dw[1];
913
+ w[2] += dw[2];
914
+ vx = v[0] + (w[1] * r[2] - w[2] * r[1]);
915
+ vy = v[1] + (w[2] * r[0] - w[0] * r[2]);
916
+ vz = v[2] + (w[0] * r[1] - w[1] * r[0]);
917
+ const vt = Math.hypot(vx, vz);
918
+ if (vt > 1e-5) {
919
+ const tx = vx / vt;
920
+ const tz = vz / vt;
921
+ const rx = r[1] * tz;
922
+ const ry = r[2] * tx - r[0] * tz;
923
+ const rz = -r[1] * tx;
924
+ const at = this.mulInvInertiaWith(b, e, rx, ry, rz);
925
+ const cx = at[1] * r[2] - at[2] * r[1];
926
+ const cz = at[0] * r[1] - at[1] * r[0];
927
+ const denomT = invM + cx * tx + cz * tz;
928
+ const jt = Math.min(vt / denomT, MU * j);
929
+ v[0] -= tx * jt * invM;
930
+ v[2] -= tz * jt * invM;
931
+ const dwt = this.mulInvInertiaWith(b, e, rx * jt, ry * jt, rz * jt);
932
+ w[0] -= dwt[0];
933
+ w[1] -= dwt[1];
934
+ w[2] -= dwt[2];
935
+ }
936
+ }
937
+ }
938
+ const rd = Math.exp((rs.length >= 3 ? -3 : -1.5) * h);
939
+ w[0] *= rd;
940
+ w[1] *= rd;
941
+ w[2] *= rd;
942
+ const fd = Math.exp(-0.3 * h);
943
+ v[0] *= fd;
944
+ v[2] *= fd;
945
+ }
946
+ b.root.quaternion.copy(q);
947
+ const off = this.ax.copy(b.centerV).applyQuaternion(q);
948
+ b.root.position.set(pos[0] - off.x, pos[1] - off.y, pos[2] - off.z);
949
+ b.root.updateMatrixWorld(true);
950
+ b.onGround = contactedLast;
951
+ if (landing > HARD_LANDING && this.time - b.lastHit > 0.15) {
952
+ b.lastHit = this.time;
953
+ this.o.onHardLanding(b, landPoint, landing);
954
+ }
955
+ const still = touched && Math.hypot(v[0], v[1], v[2]) < 0.12 && Math.hypot(w[0], w[1], w[2]) < 0.45;
956
+ if (still) {
957
+ b.still += dt;
958
+ if (b.still >= SLEEP_AFTER) {
959
+ b.sleeping = true;
960
+ v[0] = v[1] = v[2] = 0;
961
+ w[0] = w[1] = w[2] = 0;
962
+ }
963
+ } else {
964
+ b.still = 0;
965
+ }
966
+ }
967
+ /** Push overlapping pieces apart (horizontal bounding circles), gently, so they do not interpenetrate forever. */
968
+ separate(dt) {
969
+ const n = this.list.length;
970
+ if (n < 2) return;
971
+ const maxMove = 1.5 * dt;
972
+ const ca = new this.THREE.Vector3();
973
+ const cb = new this.THREE.Vector3();
974
+ for (let i = 0; i < n; i++) {
975
+ const a = this.list[i];
976
+ for (let j = i + 1; j < n; j++) {
977
+ const b = this.list[j];
978
+ if (a.held && b.held) continue;
979
+ if (a.sleeping && b.sleeping) continue;
980
+ const ra = Math.hypot(a.halfExtents[0], a.halfExtents[1], a.halfExtents[2]);
981
+ const rb = Math.hypot(b.halfExtents[0], b.halfExtents[1], b.halfExtents[2]);
982
+ this.centerOf(a, ca);
983
+ this.centerOf(b, cb);
984
+ if (Math.abs(ca.y - cb.y) > ra + rb) continue;
985
+ const rh = (Math.max(a.halfExtents[0], a.halfExtents[2]) + Math.max(b.halfExtents[0], b.halfExtents[2])) * 0.8;
986
+ let dx = cb.x - ca.x;
987
+ let dz = cb.z - ca.z;
988
+ let d = Math.hypot(dx, dz);
989
+ if (d >= rh) continue;
990
+ if (d < 1e-5) {
991
+ dx = 1;
992
+ dz = 0;
993
+ d = 1;
994
+ }
995
+ const push = Math.min(maxMove, (rh - d) * 0.5);
996
+ const wa = a.held ? 0 : b.held ? 1 : b.mass / (a.mass + b.mass);
997
+ const wb = b.held ? 0 : a.held ? 1 : a.mass / (a.mass + b.mass);
998
+ const ux = dx / (d || 1);
999
+ const uz = dz / (d || 1);
1000
+ if (!a.held) {
1001
+ a.root.position.x -= ux * push * 2 * wa;
1002
+ a.root.position.z -= uz * push * 2 * wa;
1003
+ a.sleeping = false;
1004
+ }
1005
+ if (!b.held) {
1006
+ b.root.position.x += ux * push * 2 * wb;
1007
+ b.root.position.z += uz * push * 2 * wb;
1008
+ b.sleeping = false;
1009
+ }
1010
+ }
1011
+ }
1012
+ }
1013
+ };
1014
+
1015
+ // src/heat.ts
1016
+ var STOPS = [
1017
+ [450, 0],
1018
+ [500, 2753536],
1019
+ [650, 9049344],
1020
+ [800, 14174728],
1021
+ [950, 16747540],
1022
+ [1100, 16761402],
1023
+ [1250, 16771232],
1024
+ [1400, 16777215]
1025
+ ];
1026
+ function heatColor(temperatureC) {
1027
+ if (!(temperatureC > STOPS[0][0])) return 0;
1028
+ const last = STOPS[STOPS.length - 1];
1029
+ if (temperatureC >= last[0]) return last[1];
1030
+ for (let i = 1; i < STOPS.length; i++) {
1031
+ const [t1, c1] = STOPS[i];
1032
+ if (temperatureC <= t1) {
1033
+ const [t0, c0] = STOPS[i - 1];
1034
+ return mixHex(c0, c1, (temperatureC - t0) / (t1 - t0));
1035
+ }
1036
+ }
1037
+ return last[1];
1038
+ }
1039
+
1040
+ // src/visual.ts
1041
+ var POS_ATTR = "position";
1042
+ function positionAttr(geo) {
1043
+ const a = geo.getAttribute(POS_ATTR);
1044
+ return a && !a.isInterleavedBufferAttribute ? a : null;
1045
+ }
1046
+ function materialsOf(m) {
1047
+ return Array.isArray(m) ? m : [m];
1048
+ }
1049
+ function colorOf(m) {
1050
+ const c = m.color;
1051
+ return c && c.isColor ? c : null;
1052
+ }
1053
+ var DecalKit = class {
1054
+ constructor(THREE) {
1055
+ __publicField(this, "THREE", THREE);
1056
+ __publicField(this, "disc", null);
1057
+ __publicField(this, "discMat", null);
1058
+ __publicField(this, "lineMats", /* @__PURE__ */ new Map());
1059
+ }
1060
+ discGeometry() {
1061
+ return this.disc ?? (this.disc = new this.THREE.CircleGeometry(1, 14));
1062
+ }
1063
+ discMaterial() {
1064
+ return this.discMat ?? (this.discMat = new this.THREE.MeshBasicMaterial({
1065
+ color: 328965,
1066
+ transparent: true,
1067
+ opacity: 0.92,
1068
+ depthWrite: false,
1069
+ polygonOffset: true,
1070
+ polygonOffsetFactor: -4,
1071
+ polygonOffsetUnits: -4
1072
+ }));
1073
+ }
1074
+ lineMaterial(color) {
1075
+ let m = this.lineMats.get(color);
1076
+ if (!m) {
1077
+ m = new this.THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.9, depthWrite: false });
1078
+ this.lineMats.set(color, m);
1079
+ }
1080
+ return m;
1081
+ }
1082
+ dispose() {
1083
+ this.disc?.dispose();
1084
+ this.discMat?.dispose();
1085
+ for (const m of this.lineMats.values()) m.dispose();
1086
+ this.disc = null;
1087
+ this.discMat = null;
1088
+ this.lineMats.clear();
1089
+ }
1090
+ };
1091
+ var tmpV = (THREE) => new THREE.Vector3();
1092
+ var Visuals = class {
1093
+ constructor(THREE, lowQuality, decalCap) {
1094
+ __publicField(this, "THREE", THREE);
1095
+ __publicField(this, "lowQuality", lowQuality);
1096
+ __publicField(this, "decalCap", decalCap);
1097
+ __publicField(this, "kit");
1098
+ __publicField(this, "a");
1099
+ __publicField(this, "b");
1100
+ __publicField(this, "m4");
1101
+ __publicField(this, "box");
1102
+ /** Total live decals across meshes (kept in sync by add/remove). */
1103
+ __publicField(this, "decalCount", 0);
1104
+ this.kit = new DecalKit(THREE);
1105
+ this.a = tmpV(THREE);
1106
+ this.b = tmpV(THREE);
1107
+ this.m4 = new THREE.Matrix4();
1108
+ this.box = new THREE.Box3();
1109
+ }
1110
+ makeMeshVis(mesh, home) {
1111
+ return {
1112
+ mesh,
1113
+ home,
1114
+ origGeometry: mesh.geometry,
1115
+ origMaterial: mesh.material,
1116
+ origVisible: mesh.visible,
1117
+ origParent: mesh.parent,
1118
+ origPosition: mesh.position.clone(),
1119
+ origQuaternion: mesh.quaternion.clone(),
1120
+ origScale: mesh.scale.clone(),
1121
+ ownMat: false,
1122
+ ownGeo: false,
1123
+ work: null,
1124
+ baseColors: null,
1125
+ decals: [],
1126
+ meltApplied: 0,
1127
+ bottomY: 0,
1128
+ moved: false
1129
+ };
1130
+ }
1131
+ vertexCount(mv) {
1132
+ return mv.mesh.geometry.getAttribute(POS_ATTR)?.count ?? 0;
1133
+ }
1134
+ /** Colour of the original material (before any char), `0xRRGGBB`, or -1. */
1135
+ baseColorHex(mv) {
1136
+ const c = mv.baseColors?.[0] ?? colorOf(materialsOf(mv.origMaterial)[0]);
1137
+ return c ? c.getHex() : -1;
1138
+ }
1139
+ /** Is the original material transparent? */
1140
+ isTransparent(mv) {
1141
+ return materialsOf(mv.origMaterial).some((m) => m.transparent && m.opacity < 1);
1142
+ }
1143
+ ownMaterial(mv) {
1144
+ if (mv.ownMat) return;
1145
+ mv.ownMat = true;
1146
+ const src = materialsOf(mv.origMaterial);
1147
+ const cloned = src.map((m) => m.clone());
1148
+ mv.baseColors = src.map((m) => colorOf(m)?.clone() ?? new this.THREE.Color(8947848));
1149
+ mv.mesh.material = Array.isArray(mv.origMaterial) ? cloned : cloned[0];
1150
+ }
1151
+ /** Clone the geometry (vertex edits); false when its positions cannot be edited. */
1152
+ ownGeometry(mv) {
1153
+ if (mv.ownGeo) return true;
1154
+ const pos = positionAttr(mv.mesh.geometry);
1155
+ if (!pos) return false;
1156
+ mv.ownGeo = true;
1157
+ const g = mv.mesh.geometry.clone();
1158
+ mv.mesh.geometry = g;
1159
+ mv.work = new Float32Array(positionAttr(g).array);
1160
+ return true;
1161
+ }
1162
+ // ------------------------------------------------------------------------------------------------------------------------------------
1163
+ // Dents
1164
+ // ------------------------------------------------------------------------------------------------------------------------------------
1165
+ /**
1166
+ * Push the vertices of the mesh within `dent.radius` of `worldPoint` inwards along `worldNormal` by `depth * (1 - (d/r)^2)^2`.
1167
+ * Returns false when nothing was changed (too far, low quality on a heavy mesh, or no editable positions).
1168
+ */
1169
+ applyDent(mv, worldPoint, worldNormal, dent) {
1170
+ const mesh = mv.mesh;
1171
+ if (this.lowQuality && this.vertexCount(mv) > 2e4) return false;
1172
+ mesh.updateWorldMatrix(true, false);
1173
+ const s = mesh.matrixWorld.getMaxScaleOnAxis() || 1;
1174
+ const lp = mesh.worldToLocal(this.a.copy(worldPoint));
1175
+ const ln = this.b.copy(worldNormal).transformDirection(this.m4.copy(mesh.matrixWorld).invert()).normalize();
1176
+ const r = dent.radius / s;
1177
+ const depth = dent.depth / s;
1178
+ const geo = mesh.geometry;
1179
+ if (!geo.boundingBox) geo.computeBoundingBox();
1180
+ if (geo.boundingBox && geo.boundingBox.distanceToPoint(lp) > r) return false;
1181
+ if (!this.ownGeometry(mv)) return false;
1182
+ const work = mv.work;
1183
+ const pos = positionAttr(mesh.geometry);
1184
+ const r2 = r * r;
1185
+ let touched = false;
1186
+ const px = lp.x;
1187
+ const py = lp.y;
1188
+ const pz = lp.z;
1189
+ const nx = ln.x;
1190
+ const ny = ln.y;
1191
+ const nz = ln.z;
1192
+ for (let i = 0; i < work.length; i += 3) {
1193
+ const dx = work[i] - px;
1194
+ const dy = work[i + 1] - py;
1195
+ const dz = work[i + 2] - pz;
1196
+ const d2 = dx * dx + dy * dy + dz * dz;
1197
+ if (d2 >= r2) continue;
1198
+ const f = 1 - d2 / r2;
1199
+ const k = depth * f * f;
1200
+ work[i] -= nx * k;
1201
+ work[i + 1] -= ny * k;
1202
+ work[i + 2] -= nz * k;
1203
+ touched = true;
1204
+ }
1205
+ if (!touched) return false;
1206
+ this.writePositions(mv, pos);
1207
+ return true;
1208
+ }
1209
+ /** Copy `work` (dents) into the geometry, adding the melt sag when the mesh is melting. */
1210
+ writePositions(mv, pos) {
1211
+ const work = mv.work;
1212
+ const arr = pos.array;
1213
+ const melt = mv.meltApplied;
1214
+ if (melt <= 0 || this.lowQuality && work.length / 3 > 2e4) {
1215
+ arr.set(work);
1216
+ } else {
1217
+ const geo2 = mv.mesh.geometry;
1218
+ if (!geo2.boundingBox) geo2.computeBoundingBox();
1219
+ const bb = geo2.boundingBox;
1220
+ const h = Math.max(1e-4, bb.max.y - bb.min.y);
1221
+ const minY = bb.min.y;
1222
+ for (let i = 0, v = 0; i < work.length; i += 3, v++) {
1223
+ const n = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
1224
+ const n2 = (Math.imul(v + 7, 40503) >>> 0) % 1e3 / 1e3;
1225
+ const lowness = 1 - (work[i + 1] - minY) / h;
1226
+ const spread = 1 + 0.12 * melt * (0.4 + n2) * (0.3 + lowness);
1227
+ arr[i] = work[i] * spread;
1228
+ arr[i + 2] = work[i + 2] * spread;
1229
+ arr[i + 1] = Math.max(minY, work[i + 1] - n * melt * h * 0.08);
1230
+ }
1231
+ }
1232
+ pos.needsUpdate = true;
1233
+ const geo = mv.mesh.geometry;
1234
+ geo.computeVertexNormals();
1235
+ geo.boundingBox = null;
1236
+ geo.boundingSphere = null;
1237
+ }
1238
+ // ------------------------------------------------------------------------------------------------------------------------------------
1239
+ // Decals
1240
+ // ------------------------------------------------------------------------------------------------------------------------------------
1241
+ /** Decal pose in mesh space: position and a rotation taking +Z to the surface normal. */
1242
+ decalPose(mesh, worldPoint, worldNormal, lift) {
1243
+ mesh.updateWorldMatrix(true, false);
1244
+ const s = mesh.matrixWorld.getMaxScaleOnAxis() || 1;
1245
+ const ln = worldNormal.clone().transformDirection(this.m4.copy(mesh.matrixWorld).invert()).normalize();
1246
+ const pos = mesh.worldToLocal(worldPoint.clone()).addScaledVector(ln, lift / s);
1247
+ const quat2 = new this.THREE.Quaternion().setFromUnitVectors(new this.THREE.Vector3(0, 0, 1), ln);
1248
+ return { s, pos, quat: quat2 };
1249
+ }
1250
+ addDecal(mv, decal) {
1251
+ mv.mesh.add(decal.obj);
1252
+ mv.decals.push(decal);
1253
+ this.decalCount++;
1254
+ while (mv.decals.length > 48 || this.decalCount > this.decalCap) {
1255
+ const old = mv.decals.shift();
1256
+ if (!old) break;
1257
+ this.disposeDecal(old);
1258
+ }
1259
+ }
1260
+ disposeDecal(d) {
1261
+ d.obj.parent?.remove(d.obj);
1262
+ d.geometry?.dispose();
1263
+ this.decalCount = Math.max(0, this.decalCount - 1);
1264
+ }
1265
+ addHole(mv, worldPoint, worldNormal, diameter) {
1266
+ const { s, pos, quat: quat2 } = this.decalPose(mv.mesh, worldPoint, worldNormal, 25e-4);
1267
+ const disc = new this.THREE.Mesh(this.kit.discGeometry(), this.kit.discMaterial());
1268
+ disc.position.copy(pos);
1269
+ disc.quaternion.copy(quat2);
1270
+ disc.scale.setScalar(Math.max(4e-3, diameter) * 0.5 * 1.25 / s);
1271
+ disc.renderOrder = 2;
1272
+ this.addDecal(mv, { obj: disc });
1273
+ }
1274
+ /** A star of jagged lines: `rays` rays up to `length` metres, seeded so the same event always draws the same star. */
1275
+ addCrack(mv, worldPoint, worldNormal, rays, length, color, seed) {
1276
+ const { s, pos, quat: quat2 } = this.decalPose(mv.mesh, worldPoint, worldNormal, 2e-3);
1277
+ const rnd = createRng(seed);
1278
+ const verts = [];
1279
+ const n = clamp(Math.round(rays), 3, 16);
1280
+ const L = Math.max(0.02, length) / s;
1281
+ for (let r = 0; r < n; r++) {
1282
+ let ang = r / n * Math.PI * 2 + (rnd() - 0.5) * 0.5;
1283
+ let x = 0;
1284
+ let y = 0;
1285
+ const segs = 3;
1286
+ const total = L * (0.6 + 0.4 * rnd());
1287
+ for (let k = 0; k < segs; k++) {
1288
+ const step = total / segs * (0.7 + 0.6 * rnd());
1289
+ ang += (rnd() - 0.5) * 0.45;
1290
+ const nx = x + Math.cos(ang) * step;
1291
+ const ny = y + Math.sin(ang) * step;
1292
+ verts.push(x, y, 0, nx, ny, 0);
1293
+ if (k > 0 && rnd() < 0.5) {
1294
+ const ba = ang + (rnd() < 0.5 ? -1 : 1) * (0.5 + rnd() * 0.5);
1295
+ const bl = step * (0.6 + 0.6 * rnd());
1296
+ verts.push(nx, ny, 0, nx + Math.cos(ba) * bl, ny + Math.sin(ba) * bl, 0);
1297
+ }
1298
+ x = nx;
1299
+ y = ny;
1300
+ }
1301
+ }
1302
+ const ringR = L * 0.12;
1303
+ for (let k = 0; k < 8; k++) {
1304
+ const a0 = k / 8 * Math.PI * 2;
1305
+ const a1 = (k + 1) / 8 * Math.PI * 2;
1306
+ if (rnd() < 0.75) verts.push(Math.cos(a0) * ringR, Math.sin(a0) * ringR, 0, Math.cos(a1) * ringR, Math.sin(a1) * ringR, 0);
1307
+ }
1308
+ const geo = new this.THREE.BufferGeometry();
1309
+ geo.setAttribute("position", new this.THREE.Float32BufferAttribute(verts, 3));
1310
+ const lines = new this.THREE.LineSegments(geo, this.kit.lineMaterial(color));
1311
+ lines.position.copy(pos);
1312
+ lines.quaternion.copy(quat2);
1313
+ lines.renderOrder = 2;
1314
+ this.addDecal(mv, { obj: lines, geometry: geo });
1315
+ }
1316
+ // ------------------------------------------------------------------------------------------------------------------------------------
1317
+ // Char and heat glow
1318
+ // ------------------------------------------------------------------------------------------------------------------------------------
1319
+ setChar(pv, char, charColor) {
1320
+ if (Math.abs(char - pv.charApplied) < 4e-3) return;
1321
+ pv.charApplied = char;
1322
+ const target = new this.THREE.Color(charColor);
1323
+ for (const mv of pv.meshes) {
1324
+ this.ownMaterial(mv);
1325
+ const mats = materialsOf(mv.mesh.material);
1326
+ mats.forEach((m, i) => {
1327
+ const c = colorOf(m);
1328
+ const base = mv.baseColors?.[i];
1329
+ if (c && base) c.copy(base).lerp(target, clamp(char, 0, 1));
1330
+ });
1331
+ }
1332
+ }
1333
+ /** Emissive glow of a hot part. `temperature` below the glow threshold switches it off. */
1334
+ setGlow(pv, temperatureC) {
1335
+ const hex = heatColor(temperatureC);
1336
+ const key = hex === 0 ? 0 : Math.round(temperatureC / 5) * 5;
1337
+ if (key === pv.glowApplied) return;
1338
+ pv.glowApplied = key;
1339
+ const intensity = hex === 0 ? 0 : clamp(0.35 + (temperatureC - 450) / 900, 0, 1.4);
1340
+ for (const mv of pv.meshes) {
1341
+ this.ownMaterial(mv);
1342
+ for (const m of materialsOf(mv.mesh.material)) {
1343
+ const e = m.emissive;
1344
+ if (e && e.isColor) {
1345
+ e.setHex(hex);
1346
+ m.emissiveIntensity = intensity;
1347
+ }
1348
+ }
1349
+ }
1350
+ }
1351
+ // ------------------------------------------------------------------------------------------------------------------------------------
1352
+ // Melt
1353
+ // ------------------------------------------------------------------------------------------------------------------------------------
1354
+ /** Sag the part: shorter in Y (down to 25 %), a bit wider, vertices pushed down with noise, the bottom stays put. */
1355
+ setMelt(pv, melt) {
1356
+ const m = clamp(melt, 0, 1);
1357
+ if (Math.abs(m - pv.meltApplied) < 0.01) return;
1358
+ pv.meltApplied = m;
1359
+ for (const mv of pv.meshes) {
1360
+ const mesh = mv.mesh;
1361
+ if (mv.meltApplied === 0 && m > 0) {
1362
+ mesh.updateWorldMatrix(true, false);
1363
+ mv.bottomY = this.box.setFromObject(mesh).min.y;
1364
+ }
1365
+ mv.meltApplied = m;
1366
+ mv.moved = true;
1367
+ const sy = lerp(1, 0.25, m);
1368
+ const sxz = 1 + 0.18 * m;
1369
+ mesh.scale.set(mv.origScale.x * sxz, mv.origScale.y * sy, mv.origScale.z * sxz);
1370
+ mesh.position.copy(mv.origPosition);
1371
+ mesh.updateWorldMatrix(true, false);
1372
+ const bottom = this.box.setFromObject(mesh).min.y;
1373
+ const dy = mv.bottomY - bottom;
1374
+ if (Math.abs(dy) > 1e-6 && mesh.parent) {
1375
+ mesh.parent.updateWorldMatrix(true, false);
1376
+ const inv = this.m4.copy(mesh.parent.matrixWorld).invert();
1377
+ this.a.set(0, dy, 0).applyMatrix4(inv);
1378
+ this.b.set(0, 0, 0).applyMatrix4(inv);
1379
+ mesh.position.copy(mv.origPosition).add(this.a.sub(this.b));
1380
+ }
1381
+ if (!(this.lowQuality && this.vertexCount(mv) > 2e4) && this.ownGeometry(mv)) {
1382
+ this.writePositions(mv, positionAttr(mesh.geometry));
1383
+ }
1384
+ }
1385
+ }
1386
+ // ------------------------------------------------------------------------------------------------------------------------------------
1387
+ // Visibility and restore
1388
+ // ------------------------------------------------------------------------------------------------------------------------------------
1389
+ hide(pv, reason) {
1390
+ pv.gone = reason;
1391
+ for (const mv of pv.meshes) mv.mesh.visible = false;
1392
+ }
1393
+ /** Bring one mesh back to how it was when it was bound. */
1394
+ restoreMesh(mv) {
1395
+ const mesh = mv.mesh;
1396
+ for (const d of mv.decals) this.disposeDecal(d);
1397
+ mv.decals.length = 0;
1398
+ if (mv.ownGeo) {
1399
+ mesh.geometry.dispose();
1400
+ mesh.geometry = mv.origGeometry;
1401
+ mv.ownGeo = false;
1402
+ mv.work = null;
1403
+ }
1404
+ if (mv.ownMat) {
1405
+ for (const m of materialsOf(mesh.material)) m.dispose();
1406
+ mesh.material = mv.origMaterial;
1407
+ mv.ownMat = false;
1408
+ mv.baseColors = null;
1409
+ }
1410
+ mesh.visible = mv.origVisible;
1411
+ if (mv.origParent && mesh.parent !== mv.origParent) {
1412
+ mv.origParent.add(mesh);
1413
+ mv.moved = true;
1414
+ }
1415
+ if (mv.moved) {
1416
+ mesh.position.copy(mv.origPosition);
1417
+ mesh.quaternion.copy(mv.origQuaternion);
1418
+ mesh.scale.copy(mv.origScale);
1419
+ mv.moved = false;
1420
+ }
1421
+ mv.meltApplied = 0;
1422
+ }
1423
+ restorePart(pv) {
1424
+ for (const mv of pv.meshes) this.restoreMesh(mv);
1425
+ pv.charApplied = 0;
1426
+ pv.glowApplied = 0;
1427
+ pv.meltApplied = 0;
1428
+ pv.gone = "none";
1429
+ }
1430
+ dispose() {
1431
+ this.kit.dispose();
1432
+ }
1433
+ };
1434
+
1435
+ // src/renderer.ts
1436
+ var GLOW_FROM_C = 430;
1437
+ var QUEUE_CAP = 4e3;
1438
+ var FLUID_COLORS = {
1439
+ water: 9226495,
1440
+ fuel: 14262322,
1441
+ oil: 2760728,
1442
+ gas: 15921906,
1443
+ sap: 12098362,
1444
+ acid: 10211900,
1445
+ blood: 9046538
1446
+ };
1447
+ var SMOKE_TONES = { white: 15132390, grey: 9079434, black: 2763306 };
1448
+ var DEFAULT_SPEED = {
1449
+ sparks: 3,
1450
+ splinters: 2.5,
1451
+ dust: 1,
1452
+ shards: 2.5,
1453
+ fibers: 1.2,
1454
+ leaves: 1.2,
1455
+ drips: 0.5,
1456
+ ash: 0.8,
1457
+ chips: 2.5,
1458
+ smoke: 0.8,
1459
+ steam: 1,
1460
+ fire: 1.2,
1461
+ flash: 0,
1462
+ foam: 2.5
1463
+ };
1464
+ function impactKey(def) {
1465
+ if (!def) return "impact_wood";
1466
+ const tags = def.tags ?? [];
1467
+ if (tags.includes("metal")) return "impact_metal";
1468
+ if (def.visual?.transparent || def.fracture === "shatter") return "impact_glass";
1469
+ if (def.fracture === "crumble") return "impact_masonry";
1470
+ if (def.fracture === "tear") return "impact_soft";
1471
+ return def.fracture === "none" ? "impact_metal" : "impact_wood";
1472
+ }
1473
+ function fractureKey(def, style) {
1474
+ switch (style) {
1475
+ case "shatter":
1476
+ return "shatter_glass";
1477
+ case "crumble":
1478
+ return "crumble";
1479
+ case "splinter":
1480
+ return "splinter";
1481
+ default:
1482
+ return impactKey(def);
1483
+ }
1484
+ }
1485
+ function fallbackColor(id, def) {
1486
+ if (def?.tags?.includes("metal")) return 9080726;
1487
+ if (def?.visual?.transparent) return 10473696;
1488
+ const h = hashString(id);
1489
+ return mixHex(7035464, 10130568, h % 100 / 100);
1490
+ }
1491
+ function materialsOf2(m) {
1492
+ return Array.isArray(m) ? m : [m];
1493
+ }
1494
+ function createDestructionRenderer(THREE, options) {
1495
+ return new Renderer(THREE, options);
1496
+ }
1497
+ var Renderer = class {
1498
+ constructor(THREE, o) {
1499
+ __publicField(this, "THREE", THREE);
1500
+ __publicField(this, "o", o);
1501
+ __publicField(this, "world");
1502
+ __publicField(this, "scene");
1503
+ __publicField(this, "quality");
1504
+ __publicField(this, "low");
1505
+ __publicField(this, "groundAt");
1506
+ __publicField(this, "gravity");
1507
+ __publicField(this, "autoSplit");
1508
+ __publicField(this, "maxFragments");
1509
+ __publicField(this, "bounds", /* @__PURE__ */ new Map());
1510
+ __publicField(this, "queue", []);
1511
+ __publicField(this, "unsubscribe");
1512
+ __publicField(this, "visuals");
1513
+ __publicField(this, "particles");
1514
+ __publicField(this, "fragments");
1515
+ __publicField(this, "ground");
1516
+ __publicField(this, "pieceSys");
1517
+ __publicField(this, "active", /* @__PURE__ */ new Map());
1518
+ __publicField(this, "fires", /* @__PURE__ */ new Map());
1519
+ __publicField(this, "leaks", []);
1520
+ __publicField(this, "homeTags", []);
1521
+ __publicField(this, "throttles", /* @__PURE__ */ new Map());
1522
+ __publicField(this, "rng", createRng(12648430));
1523
+ __publicField(this, "light", null);
1524
+ __publicField(this, "lightT", 0);
1525
+ __publicField(this, "lightDur", 0.3);
1526
+ __publicField(this, "lightPeak", 0);
1527
+ __publicField(this, "time", 0);
1528
+ __publicField(this, "scanTimer", 0);
1529
+ __publicField(this, "disposed", false);
1530
+ __publicField(this, "box");
1531
+ __publicField(this, "tmpQ");
1532
+ __publicField(this, "v1");
1533
+ __publicField(this, "v2");
1534
+ this.world = o.world;
1535
+ this.scene = o.scene;
1536
+ this.quality = o.quality ?? "medium";
1537
+ this.low = this.quality === "low";
1538
+ this.groundAt = o.groundAt ?? (() => 0);
1539
+ this.gravity = o.gravity ?? 9.81;
1540
+ this.autoSplit = o.autoSplit !== false;
1541
+ this.maxFragments = o.maxFragments ?? 160;
1542
+ this.box = new THREE.Box3();
1543
+ this.tmpQ = new THREE.Quaternion();
1544
+ this.v1 = new THREE.Vector3();
1545
+ this.v2 = new THREE.Vector3();
1546
+ const maxParticles = Math.max(8, Math.floor((o.maxParticles ?? 2500) * (this.low ? 0.5 : 1)));
1547
+ this.visuals = new Visuals(THREE, this.low, 600);
1548
+ this.ground = new GroundSpots(THREE, o.scene, this.groundAt, 48);
1549
+ this.particles = new ParticleSystem(THREE, {
1550
+ capacity: maxParticles,
1551
+ scene: o.scene,
1552
+ density: this.low ? 0.6 : 1,
1553
+ pointScale: o.pointScale ?? 700,
1554
+ groundAt: this.groundAt,
1555
+ gravity: this.gravity,
1556
+ onLand: (x, _y, z, color, tag) => {
1557
+ const home = this.homeTags[tag - 1] ?? "";
1558
+ this.ground.add("puddle", x, z, 0.05 + this.rng() * 0.04, color, home);
1559
+ }
1560
+ });
1561
+ this.fragments = new FragmentSystem(THREE, {
1562
+ scene: o.scene,
1563
+ groundAt: this.groundAt,
1564
+ gravity: this.gravity,
1565
+ max: this.maxFragments,
1566
+ life: o.fragmentLife ?? 12
1567
+ });
1568
+ this.pieceSys = new PieceSystem(THREE, {
1569
+ groundAt: this.groundAt,
1570
+ gravity: this.gravity,
1571
+ scene: o.scene,
1572
+ onHardLanding: (p, point, speed) => this.hardLanding(p, point, speed)
1573
+ });
1574
+ this.unsubscribe = this.world.on("*", (ev) => {
1575
+ if (this.queue.length < QUEUE_CAP) this.queue.push(ev);
1576
+ });
1577
+ }
1578
+ // ----------------------------------------------------------------------------------------------------------------------------------
1579
+ // Binding
1580
+ // ----------------------------------------------------------------------------------------------------------------------------------
1581
+ bind(objectId, root, options = {}) {
1582
+ if (this.bounds.has(objectId)) this.unbind(objectId);
1583
+ const state = this.world.state(objectId);
1584
+ const partIds = new Set((state?.parts ?? []).map((p) => p.id));
1585
+ const byPart = /* @__PURE__ */ new Map();
1586
+ const taken = /* @__PURE__ */ new Set();
1587
+ const add = (pid, m) => {
1588
+ if (taken.has(m)) return;
1589
+ taken.add(m);
1590
+ const l = byPart.get(pid);
1591
+ if (l) l.push(m);
1592
+ else byPart.set(pid, [m]);
1593
+ };
1594
+ const meshesIn = (o) => {
1595
+ const out = [];
1596
+ o.traverse((c) => {
1597
+ if (c.isMesh) out.push(c);
1598
+ });
1599
+ return out;
1600
+ };
1601
+ if (options.parts) {
1602
+ for (const [pid, v] of Object.entries(options.parts)) {
1603
+ for (const o of Array.isArray(v) ? v : [v]) for (const m of meshesIn(o)) add(pid, m);
1604
+ }
1605
+ } else {
1606
+ const leftovers = [];
1607
+ for (const m of meshesIn(root)) {
1608
+ const ud = m.userData?.destructionPart;
1609
+ if (typeof ud === "string" && partIds.has(ud)) add(ud, m);
1610
+ else if (partIds.has(m.name)) add(m.name, m);
1611
+ else leftovers.push(m);
1612
+ }
1613
+ if (leftovers.length) this.matchByBox(objectId, root, leftovers, add);
1614
+ }
1615
+ const bound = { id: objectId, root, sync: options.syncTransform !== false, parts: /* @__PURE__ */ new Map(), lastPos: null, lastQuat: null, piece: null, isSplit: false };
1616
+ for (const pid of partIds) bound.parts.set(pid, this.makePartVis(objectId, pid, state?.parts.find((p) => p.id === pid)?.material ?? "", byPart.get(pid) ?? []));
1617
+ for (const [pid, meshes] of byPart) {
1618
+ if (!bound.parts.has(pid)) bound.parts.set(pid, this.makePartVis(objectId, pid, this.world.part(objectId, pid)?.material ?? "", meshes));
1619
+ }
1620
+ this.bounds.set(objectId, bound);
1621
+ }
1622
+ makePartVis(home, id, material, meshes) {
1623
+ return { id, home, material, meshes: meshes.map((m) => this.visuals.makeMeshVis(m, home)), charApplied: 0, glowApplied: 0, meltApplied: 0, gone: "none" };
1624
+ }
1625
+ /** Match unlabelled meshes to the part whose shape contains the centre of the mesh's bounding box. */
1626
+ matchByBox(objectId, root, meshes, add) {
1627
+ let specParts;
1628
+ try {
1629
+ specParts = this.world.serialize?.().objects.find((o) => o.id === objectId)?.spec.parts;
1630
+ } catch {
1631
+ specParts = void 0;
1632
+ }
1633
+ if (!specParts) return;
1634
+ root.updateMatrixWorld(true);
1635
+ const e = new this.THREE.Euler();
1636
+ const q = new this.THREE.Quaternion();
1637
+ for (const m of meshes) {
1638
+ this.box.setFromObject(m);
1639
+ if (this.box.isEmpty()) continue;
1640
+ const c = root.worldToLocal(this.box.getCenter(this.v1));
1641
+ let best = null;
1642
+ let bestVol = Infinity;
1643
+ for (const p of specParts) {
1644
+ const l = this.v2.set(c.x - p.position[0], c.y - p.position[1], c.z - p.position[2]);
1645
+ if (p.rotation) l.applyQuaternion(q.setFromEuler(e.set(p.rotation[0], p.rotation[1], p.rotation[2], "XYZ")).invert());
1646
+ let inside = false;
1647
+ let vol = 0;
1648
+ const s = p.shape;
1649
+ if (s.kind === "box") {
1650
+ 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;
1651
+ vol = s.size[0] * s.size[1] * s.size[2];
1652
+ } else if (s.kind === "cylinder") {
1653
+ inside = Math.hypot(l.x, l.z) <= s.radius && Math.abs(l.y) <= s.height / 2;
1654
+ vol = Math.PI * s.radius * s.radius * s.height;
1655
+ } else {
1656
+ inside = l.length() <= s.radius;
1657
+ vol = 4 / 3 * Math.PI * s.radius ** 3;
1658
+ }
1659
+ if (inside && vol < bestVol) {
1660
+ best = p;
1661
+ bestVol = vol;
1662
+ }
1663
+ }
1664
+ if (best) add(best.id, m);
1665
+ }
1666
+ }
1667
+ unbind(objectId) {
1668
+ const b = this.bounds.get(objectId);
1669
+ if (!b) return;
1670
+ this.restore(objectId);
1671
+ this.pieceSys.remove(objectId);
1672
+ this.bounds.delete(objectId);
1673
+ }
1674
+ // ----------------------------------------------------------------------------------------------------------------------------------
1675
+ // Pieces
1676
+ // ----------------------------------------------------------------------------------------------------------------------------------
1677
+ /** Half extents and centre of the box around some meshes, in the local space of `root`. */
1678
+ measure(root, meshes) {
1679
+ root.updateMatrixWorld(true);
1680
+ const inv = root.matrixWorld.clone().invert();
1681
+ const total = new this.THREE.Box3();
1682
+ const m4 = new this.THREE.Matrix4();
1683
+ for (const m of meshes) {
1684
+ const g = m.geometry;
1685
+ if (!g.boundingBox) g.computeBoundingBox();
1686
+ if (!g.boundingBox || g.boundingBox.isEmpty()) continue;
1687
+ total.union(g.boundingBox.clone().applyMatrix4(m4.multiplyMatrices(inv, m.matrixWorld)));
1688
+ }
1689
+ if (total.isEmpty()) return { half: [0.25, 0.25, 0.25], center: new this.THREE.Vector3() };
1690
+ const size = total.getSize(new this.THREE.Vector3());
1691
+ 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()) };
1692
+ }
1693
+ meshesOf(b) {
1694
+ const out = [];
1695
+ for (const pv of b.parts.values()) if (pv.gone === "none") for (const mv of pv.meshes) out.push(mv.mesh);
1696
+ return out;
1697
+ }
1698
+ massOf(objectId) {
1699
+ const st = this.world.state(objectId);
1700
+ if (!st) return 5;
1701
+ let m = 0;
1702
+ for (const p of st.parts) if (p.status !== "broken" && p.status !== "melted" && p.status !== "ashes") m += p.mass;
1703
+ return m > 0 ? m : 5;
1704
+ }
1705
+ makePiece(objectId, options = {}) {
1706
+ const b = this.bounds.get(objectId);
1707
+ if (!b) throw new Error(`game-destruction-three: object "${objectId}" is not bound`);
1708
+ if (b.root.parent && b.root.parent !== this.scene) this.scene.attach(b.root);
1709
+ const { half, center } = this.measure(b.root, this.meshesOf(b));
1710
+ const piece = this.pieceSys.create(objectId, b.root, half, center, options.mass ?? this.massOf(objectId), options.velocity);
1711
+ b.piece = piece;
1712
+ return piece;
1713
+ }
1714
+ pieces() {
1715
+ return [...this.pieceSys.list];
1716
+ }
1717
+ piece(objectId) {
1718
+ return this.pieceSys.get(objectId);
1719
+ }
1720
+ impulse(objectId, impulse, point) {
1721
+ const p = this.pieceSys.get(objectId);
1722
+ if (p) this.pieceSys.impulse(p, impulse, point);
1723
+ }
1724
+ hardLanding(p, point, speed) {
1725
+ const key = this.impactSoundOf(p.objectId);
1726
+ this.sound(key, point, clamp(speed / 10, 0.2, 1));
1727
+ this.syncBound(p.objectId);
1728
+ try {
1729
+ this.world.hit(p.objectId, { type: "blunt", point, energy: 0.5 * p.mass * speed * speed, radius: 0.08, direction: [0, -1, 0], source: "fall" });
1730
+ } catch {
1731
+ }
1732
+ }
1733
+ impactSoundOf(objectId) {
1734
+ const st = this.world.state(objectId);
1735
+ let best;
1736
+ let mass = -1;
1737
+ for (const p of st?.parts ?? []) {
1738
+ if (p.mass > mass) {
1739
+ mass = p.mass;
1740
+ best = this.world.materials.get(p.material);
1741
+ }
1742
+ }
1743
+ return impactKey(best);
1744
+ }
1745
+ // ----------------------------------------------------------------------------------------------------------------------------------
1746
+ // Update
1747
+ // ----------------------------------------------------------------------------------------------------------------------------------
1748
+ update(dt) {
1749
+ if (this.disposed) return;
1750
+ dt = clamp(dt, 0, 0.1);
1751
+ this.time += dt;
1752
+ this.drainQueue();
1753
+ this.updateActive(dt);
1754
+ this.updateLeaks(dt);
1755
+ this.pieceSys.update(dt);
1756
+ this.fragments.update(dt);
1757
+ this.ground.update(dt);
1758
+ this.updateLight(dt);
1759
+ this.particles.update(dt);
1760
+ this.syncTransforms();
1761
+ }
1762
+ drainQueue() {
1763
+ const batch = this.queue.splice(0, this.queue.length);
1764
+ for (const ev of batch) {
1765
+ try {
1766
+ this.apply(ev);
1767
+ } catch (err) {
1768
+ if (typeof console !== "undefined") console.error("game-destruction-three: failed to apply", ev.type, err);
1769
+ }
1770
+ }
1771
+ }
1772
+ syncTransforms() {
1773
+ for (const b of this.bounds.values()) this.syncOne(b);
1774
+ }
1775
+ /** Push the current transform of one bound object to the world right now (used before a landing hit). */
1776
+ syncBound(objectId) {
1777
+ const b = this.bounds.get(objectId);
1778
+ if (b) this.syncOne(b);
1779
+ }
1780
+ syncOne(b) {
1781
+ if (!b.sync) return;
1782
+ const p = this.v1;
1783
+ const q = this.tmpQ;
1784
+ b.root.updateWorldMatrix(true, false);
1785
+ b.root.matrixWorld.decompose(p, q, this.v2);
1786
+ const last = b.lastPos;
1787
+ const lq = b.lastQuat;
1788
+ 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;
1789
+ if (!moved) return;
1790
+ b.lastPos = [p.x, p.y, p.z];
1791
+ b.lastQuat = [q.x, q.y, q.z, q.w];
1792
+ 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]);
1793
+ }
1794
+ // ----------------------------------------------------------------------------------------------------------------------------------
1795
+ // Events
1796
+ // ----------------------------------------------------------------------------------------------------------------------------------
1797
+ apply(ev) {
1798
+ switch (ev.type) {
1799
+ case "damage":
1800
+ if (ev.damageType === "heat" || ev.damageType === "fire" || ev.damageType === "explosive") this.activate(ev.object, ev.part);
1801
+ break;
1802
+ case "dent":
1803
+ this.onDent(ev);
1804
+ break;
1805
+ case "hole":
1806
+ this.onHole(ev);
1807
+ break;
1808
+ case "crack":
1809
+ this.onCrack(ev);
1810
+ break;
1811
+ case "ricochet": {
1812
+ const d = ev.direction;
1813
+ this.particles.burst("sparks", ev.point, d, 4 + Math.min(10, ev.energy / 150), 4);
1814
+ this.sound("ricochet", ev.point, 0.7);
1815
+ break;
1816
+ }
1817
+ case "debris":
1818
+ 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));
1819
+ break;
1820
+ case "fracture":
1821
+ this.onFracture(ev);
1822
+ break;
1823
+ case "detach":
1824
+ this.onDetach(ev);
1825
+ break;
1826
+ case "disassemble_progress": {
1827
+ const k = `unscrew:${ev.joint}`;
1828
+ if (this.time - (this.throttles.get(k) ?? -1) > 0.25) {
1829
+ this.throttles.set(k, this.time);
1830
+ this.sound("unscrew", this.rootPoint(ev.object), clamp(0.3 + ev.progress * 0.5, 0, 1));
1831
+ }
1832
+ break;
1833
+ }
1834
+ case "ignite":
1835
+ this.fires.set(`${ev.object}|${ev.part}`, { object: ev.object, part: ev.part, point: ev.point, accFire: 0, accSmoke: 0 });
1836
+ this.activate(ev.object, ev.part);
1837
+ this.particles.burst("fire", ev.point, [0, 1, 0], 6, 1);
1838
+ this.sound("ignite", ev.point, 0.7);
1839
+ this.sound("burn_loop", ev.point, 0.5);
1840
+ break;
1841
+ case "extinguish": {
1842
+ const f = this.fires.get(`${ev.object}|${ev.part}`);
1843
+ if (f) {
1844
+ this.fires.delete(`${ev.object}|${ev.part}`);
1845
+ this.particles.burst("steam", f.point, [0, 1, 0], 14, 1.2);
1846
+ }
1847
+ this.activate(ev.object, ev.part);
1848
+ this.sound("extinguish", f?.point ?? this.rootPoint(ev.object), 0.6);
1849
+ break;
1850
+ }
1851
+ case "burnout": {
1852
+ this.fires.delete(`${ev.object}|${ev.part}`);
1853
+ this.particles.burst("ash", ev.point, [0, 1, 0], 16, 1);
1854
+ this.particles.burst("smoke", ev.point, [0, 1, 0], 6, 0.8, { color: SMOKE_TONES.grey });
1855
+ const pv = this.partVis(ev.object, ev.part);
1856
+ if (pv) {
1857
+ const def = this.world.materials.get(pv.material);
1858
+ this.visuals.setChar(pv, 1, def?.visual?.charColor ?? 1709072);
1859
+ }
1860
+ this.activate(ev.object, ev.part);
1861
+ break;
1862
+ }
1863
+ case "melt":
1864
+ this.activate(ev.object, ev.part);
1865
+ if (this.time - (this.throttles.get(`melt:${ev.object}|${ev.part}`) ?? -9) > 1.5) {
1866
+ this.throttles.set(`melt:${ev.object}|${ev.part}`, this.time);
1867
+ this.sound("melt_sizzle", this.rootPoint(ev.object), 0.4);
1868
+ }
1869
+ break;
1870
+ case "drip": {
1871
+ const b = this.bounds.get(ev.object);
1872
+ const pv = b?.parts.get(ev.part);
1873
+ const def = this.world.materials.get(ev.material);
1874
+ const color = (pv && this.partColor(pv, def)) ?? fallbackColor(ev.material, def);
1875
+ let tag = this.homeTags.indexOf(ev.object) + 1;
1876
+ if (tag === 0) tag = this.homeTags.push(ev.object);
1877
+ this.particles.spawn("drips", ev.point, [(this.rng() - 0.5) * 0.1, -0.1, (this.rng() - 0.5) * 0.1], { color, tag });
1878
+ break;
1879
+ }
1880
+ case "leak": {
1881
+ const key = `${ev.object}|${ev.part}`;
1882
+ 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);
1883
+ if (same) {
1884
+ same.rate = ev.rate;
1885
+ same.direction = ev.direction;
1886
+ } else {
1887
+ if (this.leaks.length >= 24) this.leaks.shift();
1888
+ 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 });
1889
+ }
1890
+ void key;
1891
+ break;
1892
+ }
1893
+ case "explode":
1894
+ this.onExplode(ev);
1895
+ break;
1896
+ case "destroyed":
1897
+ break;
1898
+ }
1899
+ }
1900
+ rootPoint(objectId) {
1901
+ const b = this.bounds.get(objectId);
1902
+ return b ? [b.root.position.x, b.root.position.y, b.root.position.z] : [0, 0, 0];
1903
+ }
1904
+ sound(key, point, volume) {
1905
+ this.o.onSound?.(key, point, volume);
1906
+ }
1907
+ partVis(objectId, partId) {
1908
+ return this.bounds.get(objectId)?.parts.get(partId) ?? null;
1909
+ }
1910
+ activate(objectId, partId) {
1911
+ this.active.set(`${objectId}|${partId}`, { object: objectId, part: partId });
1912
+ }
1913
+ /** Current colour of the part's first mesh (char included), or null. */
1914
+ partColor(pv, def) {
1915
+ const mv = pv.meshes[0];
1916
+ if (!mv) return null;
1917
+ const mat = materialsOf2(mv.mesh.material)[0];
1918
+ const c = mat?.color;
1919
+ if (c && c.isColor) return c.getHex();
1920
+ return def ? fallbackColor(def.id, def) : null;
1921
+ }
1922
+ debrisOptions(kind, objectId, partId) {
1923
+ const pv = this.partVis(objectId, partId);
1924
+ const def = pv ? this.world.materials.get(pv.material) : void 0;
1925
+ if (kind === "smoke") return { color: SMOKE_TONES[def?.thermal.smoke ?? "grey"] };
1926
+ if (kind === "fibers" || kind === "leaves") {
1927
+ const c = pv ? this.visuals.baseColorHex(pv.meshes[0] ?? {}) : -1;
1928
+ return c >= 0 ? { color: c } : void 0;
1929
+ }
1930
+ if (kind === "dust" || kind === "chips") {
1931
+ const c = pv && pv.meshes[0] ? this.visuals.baseColorHex(pv.meshes[0]) : -1;
1932
+ return c >= 0 && kind === "chips" ? { color: c } : void 0;
1933
+ }
1934
+ return void 0;
1935
+ }
1936
+ /** Mesh of the part closest to a world point. */
1937
+ nearestMesh(pv, p) {
1938
+ let best = null;
1939
+ let bd = Infinity;
1940
+ for (const mv of pv.meshes) {
1941
+ if (!mv.mesh.visible) continue;
1942
+ const d = this.box.setFromObject(mv.mesh).distanceToPoint(p);
1943
+ if (d < bd) {
1944
+ bd = d;
1945
+ best = mv;
1946
+ }
1947
+ }
1948
+ return best;
1949
+ }
1950
+ worldDir(b, v) {
1951
+ b.root.updateWorldMatrix(true, false);
1952
+ return new this.THREE.Vector3(v[0], v[1], v[2]).applyQuaternion(b.root.getWorldQuaternion(this.tmpQ)).normalize();
1953
+ }
1954
+ onDent(ev) {
1955
+ const b = this.bounds.get(ev.object);
1956
+ const pv = b?.parts.get(ev.part);
1957
+ if (!b || !pv) return;
1958
+ const def = this.world.materials.get(pv.material);
1959
+ const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
1960
+ const wn = this.worldDir(b, ev.dent.normal);
1961
+ for (const mv of pv.meshes) {
1962
+ if (!mv.mesh.visible) continue;
1963
+ this.visuals.applyDent(mv, wp, wn, ev.dent);
1964
+ }
1965
+ 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);
1966
+ this.sound(impactKey(def), ev.worldPoint, clamp(0.3 + ev.dent.depth * 12, 0.3, 1));
1967
+ }
1968
+ onHole(ev) {
1969
+ const b = this.bounds.get(ev.object);
1970
+ const pv = b?.parts.get(ev.part);
1971
+ if (!b || !pv) return;
1972
+ const def = this.world.materials.get(pv.material);
1973
+ const n = this.worldDir(b, ev.hole.normal);
1974
+ const draw = (point, normal) => {
1975
+ const wp = new this.THREE.Vector3(point[0], point[1], point[2]);
1976
+ const mv = this.nearestMesh(pv, wp);
1977
+ if (!mv) return;
1978
+ if (def?.visual?.transparent) {
1979
+ const seed = hashNumbers(ev.time, point[0], point[1], point[2]);
1980
+ this.visuals.addCrack(mv, wp, normal, 7, Math.max(0.05, ev.hole.diameter * 5), 15267071, seed);
1981
+ } else {
1982
+ this.visuals.addHole(mv, wp, normal, ev.hole.diameter);
1983
+ }
1984
+ };
1985
+ draw(ev.worldPoint, n);
1986
+ if (ev.hole.through && ev.exitPoint) draw(ev.exitPoint, n.clone().negate());
1987
+ if (def?.tags?.includes("metal")) this.particles.burst("sparks", ev.worldPoint, [n.x, n.y, n.z], 6, 3.5);
1988
+ this.sound(impactKey(def), ev.worldPoint, 0.6);
1989
+ }
1990
+ onCrack(ev) {
1991
+ const b = this.bounds.get(ev.object);
1992
+ const pv = b?.parts.get(ev.part);
1993
+ if (!b || !pv) return;
1994
+ const def = this.world.materials.get(pv.material);
1995
+ const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
1996
+ const mv = this.nearestMesh(pv, wp);
1997
+ if (mv) {
1998
+ const color = def?.visual?.transparent ? 15267071 : 2762532;
1999
+ 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]));
2000
+ }
2001
+ this.sound(impactKey(def), ev.worldPoint, 0.5);
2002
+ }
2003
+ onFracture(ev) {
2004
+ const b = this.bounds.get(ev.object);
2005
+ const pv = b?.parts.get(ev.part);
2006
+ const matId = pv?.material || (ev.fragments[0]?.material ?? "");
2007
+ const def = this.world.materials.get(matId);
2008
+ let color = fallbackColor(matId, def);
2009
+ let opacity = 0.5;
2010
+ const transparent = def?.visual?.transparent === true;
2011
+ if (pv) {
2012
+ const c = this.partColor(pv, def);
2013
+ if (c !== null) color = c;
2014
+ const m = pv.meshes[0] ? materialsOf2(pv.meshes[0].origMaterial)[0] : void 0;
2015
+ if (m && m.transparent) opacity = clamp(m.opacity, 0.2, 0.8);
2016
+ this.visuals.hide(pv, "broken");
2017
+ }
2018
+ const maxAlive = this.low ? Math.floor(this.maxFragments / 2) : this.maxFragments;
2019
+ const step = this.low ? 2 : 1;
2020
+ for (let i = 0; i < ev.fragments.length; i += step) {
2021
+ const fr = ev.fragments[i];
2022
+ const fdef = this.world.materials.get(fr.material);
2023
+ const mat = this.fragments.materialFor(fr.material, fdef === def || !fdef ? color : fallbackColor(fr.material, fdef), transparent || fdef?.visual?.transparent === true, opacity);
2024
+ this.fragments.spawn(fr, mat, ev.object, maxAlive);
2025
+ }
2026
+ const up = [0, 1, 0];
2027
+ const sizeK = clamp(ev.fragments.length / 10, 0.5, 2);
2028
+ switch (ev.style) {
2029
+ case "shatter":
2030
+ this.particles.burst("shards", ev.point, up, 18 * sizeK, 3);
2031
+ break;
2032
+ case "crumble":
2033
+ this.particles.burst("dust", ev.point, up, 16 * sizeK, 1.2, { color: pv ? this.partColor(pv, def) ?? void 0 : void 0 });
2034
+ this.particles.burst("chips", ev.point, up, 6, 3);
2035
+ break;
2036
+ case "splinter":
2037
+ this.particles.burst("splinters", ev.point, up, 14 * sizeK, 3);
2038
+ this.particles.burst("dust", ev.point, up, 5, 0.8, { color: 11901556 });
2039
+ break;
2040
+ case "tear":
2041
+ this.particles.burst("fibers", ev.point, up, 12 * sizeK, 1.6, { color });
2042
+ break;
2043
+ case "snap":
2044
+ this.particles.burst("chips", ev.point, up, 8 * sizeK, 3);
2045
+ break;
2046
+ default:
2047
+ this.particles.burst("sparks", ev.point, up, 10, 4);
2048
+ }
2049
+ this.sound(fractureKey(def, ev.style), ev.point, clamp(0.4 + ev.fragments.length * 0.03, 0.4, 1));
2050
+ }
2051
+ onDetach(ev) {
2052
+ this.sound("detach", this.rootPoint(ev.object), 0.5);
2053
+ if (!this.autoSplit) return;
2054
+ const b = this.bounds.get(ev.object);
2055
+ if (!b) return;
2056
+ const st = this.world.state(ev.object);
2057
+ if (!st || st.groups.length < 2) return;
2058
+ const loose = st.groups.length - 1;
2059
+ for (let i = 0; i < loose; i++) {
2060
+ let newId;
2061
+ try {
2062
+ newId = this.world.splitGroup(ev.object, 1);
2063
+ } catch {
2064
+ break;
2065
+ }
2066
+ this.adoptSplit(b, newId);
2067
+ }
2068
+ const old = b.piece;
2069
+ if (old) {
2070
+ const { half, center } = this.measure(b.root, this.meshesOf(b));
2071
+ old.halfExtents = half;
2072
+ this.pieceSys.setCenter(old, center);
2073
+ old.mass = Math.max(0.05, this.massOf(ev.object));
2074
+ old.sleeping = false;
2075
+ }
2076
+ }
2077
+ /** The renderer side of `splitGroup`: a new group holding the meshes of the new object, made a piece. */
2078
+ adoptSplit(from, newId) {
2079
+ const state = this.world.state(newId);
2080
+ const root = from.root;
2081
+ root.updateWorldMatrix(true, false);
2082
+ const group = new this.THREE.Group();
2083
+ group.name = `destruction-piece-${newId}`;
2084
+ root.getWorldPosition(group.position);
2085
+ root.getWorldQuaternion(group.quaternion);
2086
+ this.scene.add(group);
2087
+ group.updateMatrixWorld(true);
2088
+ const nb = { id: newId, root: group, sync: true, parts: /* @__PURE__ */ new Map(), lastPos: null, lastQuat: null, piece: null, isSplit: true };
2089
+ for (const p of state?.parts ?? []) {
2090
+ const pv = from.parts.get(p.id);
2091
+ if (!pv) continue;
2092
+ from.parts.delete(p.id);
2093
+ nb.parts.set(p.id, pv);
2094
+ for (const mv of pv.meshes) {
2095
+ group.attach(mv.mesh);
2096
+ mv.moved = true;
2097
+ }
2098
+ }
2099
+ this.bounds.set(newId, nb);
2100
+ const meshes = this.meshesOf(nb);
2101
+ if (meshes.length === 0) return;
2102
+ const { half, center } = this.measure(group, meshes);
2103
+ let vel = [0, 0, 0];
2104
+ let ang = [0, 0, 0];
2105
+ const op = from.piece;
2106
+ if (op) {
2107
+ vel = [...op.velocity];
2108
+ ang = [...op.angularVelocity];
2109
+ const c = this.pieceSys.centerOf(op, new this.THREE.Vector3());
2110
+ const nc = center.clone().applyQuaternion(group.quaternion).add(group.position);
2111
+ vel[0] += ang[1] * (nc.z - c.z) - ang[2] * (nc.y - c.y);
2112
+ vel[1] += ang[2] * (nc.x - c.x) - ang[0] * (nc.z - c.z);
2113
+ vel[2] += ang[0] * (nc.y - c.y) - ang[1] * (nc.x - c.x);
2114
+ }
2115
+ const piece = this.pieceSys.create(newId, group, half, center, this.massOf(newId), vel, ang);
2116
+ nb.piece = piece;
2117
+ this.o.onPiece?.(piece);
2118
+ }
2119
+ onExplode(ev) {
2120
+ const p = ev.point;
2121
+ const k = clamp(ev.radius / 3, 0.4, 2.5);
2122
+ this.particles.burst("flash", p, [0, 1, 0], 1, 0, { size: k });
2123
+ this.particles.burst("fire", p, [0, 1, 0], 34 * k, 4 * k, { size: 1 + k * 0.5 });
2124
+ this.particles.burst("smoke", p, [0, 1, 0], 24 * k, 2.2, { color: SMOKE_TONES.black, size: 1 + k * 0.6 });
2125
+ this.particles.burst("dust", p, [0, 1, 0], 22 * k, 3.5 * k);
2126
+ this.particles.burst("sparks", p, [0, 1, 0], 16 * k, 8);
2127
+ this.particles.push(p, ev.radius, 4);
2128
+ if (this.o.explosionLight !== false) {
2129
+ if (!this.light) {
2130
+ this.light = new this.THREE.PointLight(16754784, 0, 10, 2);
2131
+ this.light.name = "destruction-explosion-light";
2132
+ }
2133
+ if (!this.light.parent) this.scene.add(this.light);
2134
+ this.light.position.set(p[0], p[1] + 0.5, p[2]);
2135
+ this.light.distance = Math.max(6, ev.radius * 3);
2136
+ this.lightPeak = clamp(Math.sqrt(Math.max(1, ev.energy)) * 0.6, 10, 400);
2137
+ this.lightDur = 0.35;
2138
+ this.lightT = this.lightDur;
2139
+ }
2140
+ const c = this.v1;
2141
+ for (const pc of this.pieceSys.list) {
2142
+ this.pieceSys.centerOf(pc, c);
2143
+ const dx = c.x - p[0];
2144
+ const dy = c.y - p[1];
2145
+ const dz = c.z - p[2];
2146
+ const d = Math.hypot(dx, dy, dz);
2147
+ if (d > ev.radius) continue;
2148
+ const fall = (1 - d / ev.radius) ** 2;
2149
+ const speed = Math.min(30, Math.sqrt(2 * ev.energy * 0.02 / pc.mass)) * fall;
2150
+ const inv = d > 1e-4 ? 1 / d : 0;
2151
+ const dir = d > 1e-4 ? [dx * inv, dy * inv + 0.3, dz * inv] : [0, 1, 0];
2152
+ const spread = pc.halfExtents[0];
2153
+ 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]);
2154
+ }
2155
+ this.fragments.blast(p, ev.radius, Math.min(20, Math.sqrt(ev.energy) * 0.08));
2156
+ this.sound("explode", p, clamp(0.5 + ev.radius / 10, 0.5, 1));
2157
+ }
2158
+ updateLight(dt) {
2159
+ if (!this.light || this.lightT <= 0) return;
2160
+ this.lightT = Math.max(0, this.lightT - dt);
2161
+ const t = this.lightT / this.lightDur;
2162
+ this.light.intensity = this.lightPeak * t * t;
2163
+ }
2164
+ // ----------------------------------------------------------------------------------------------------------------------------------
2165
+ // Fire, heat, char, melt, leaks
2166
+ // ----------------------------------------------------------------------------------------------------------------------------------
2167
+ updateActive(dt) {
2168
+ this.scanTimer += dt;
2169
+ if (this.scanTimer >= 0.4) {
2170
+ this.scanTimer = 0;
2171
+ for (const b of this.bounds.values()) {
2172
+ for (const pv of b.parts.values()) {
2173
+ if (pv.gone !== "none" && pv.gone !== "broken") continue;
2174
+ const ps = this.world.part(b.id, pv.id);
2175
+ if (!ps) continue;
2176
+ const def = this.world.materials.get(pv.material);
2177
+ 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")) {
2178
+ this.activate(b.id, pv.id);
2179
+ }
2180
+ }
2181
+ }
2182
+ }
2183
+ for (const [key, a] of this.active) {
2184
+ const pv = this.partVis(a.object, a.part);
2185
+ const ps = pv ? this.world.part(a.object, a.part) : null;
2186
+ if (!pv || !ps) {
2187
+ this.active.delete(key);
2188
+ continue;
2189
+ }
2190
+ const def = this.world.materials.get(pv.material);
2191
+ if (pv.gone === "none") {
2192
+ this.visuals.setChar(pv, ps.char, def?.visual?.charColor ?? 1709072);
2193
+ if (def?.visual?.glows) this.visuals.setGlow(pv, ps.temperature);
2194
+ this.visuals.setMelt(pv, ps.melt);
2195
+ if (ps.status === "melted") this.onGone(a.object, pv, "melted", def);
2196
+ else if (ps.status === "ashes") this.onGone(a.object, pv, "ashes", def);
2197
+ }
2198
+ const fire = this.fires.get(key);
2199
+ if (fire) {
2200
+ if (ps.burning && ps.status !== "broken" && ps.status !== "ashes" && ps.status !== "melted") this.emitFire(fire, pv, def, dt);
2201
+ else this.fires.delete(key);
2202
+ }
2203
+ const hot = def?.visual?.glows === true && ps.temperature > GLOW_FROM_C;
2204
+ if (!ps.burning && !hot && Math.abs(ps.char - pv.charApplied) < 0.01 && Math.abs(ps.melt - pv.meltApplied) < 0.01) {
2205
+ if (def?.visual?.glows && pv.gone === "none") this.visuals.setGlow(pv, 0);
2206
+ this.active.delete(key);
2207
+ }
2208
+ }
2209
+ }
2210
+ onGone(objectId, pv, reason, def) {
2211
+ const bb = new this.THREE.Box3();
2212
+ for (const mv of pv.meshes) bb.union(this.box.setFromObject(mv.mesh));
2213
+ this.visuals.hide(pv, reason);
2214
+ if (bb.isEmpty()) return;
2215
+ const c = bb.getCenter(new this.THREE.Vector3());
2216
+ const s = bb.getSize(new this.THREE.Vector3());
2217
+ const footprint = Math.sqrt(Math.max(s.x * s.z, 1e-4));
2218
+ if (reason === "melted") {
2219
+ this.ground.add("puddle", c.x, c.z, clamp(footprint * 0.55, 0.05, 1), this.partColor(pv, def) ?? fallbackColor(pv.material, def), objectId);
2220
+ } else {
2221
+ this.ground.add("ash", c.x, c.z, clamp(footprint * 0.35, 0.04, 0.5), 1907224, objectId);
2222
+ this.particles.burst("ash", [c.x, bb.max.y, c.z], [0, 1, 0], 10, 0.8);
2223
+ }
2224
+ }
2225
+ emitFire(f, pv, def, dt) {
2226
+ const bb = this.box;
2227
+ bb.makeEmpty();
2228
+ const tmp = new this.THREE.Box3();
2229
+ for (const mv of pv.meshes) if (mv.mesh.visible) bb.union(tmp.setFromObject(mv.mesh));
2230
+ let cx = f.point[0];
2231
+ let cy = f.point[1];
2232
+ let cz = f.point[2];
2233
+ let sx = 0.2;
2234
+ let sy = 0.2;
2235
+ let sz = 0.2;
2236
+ if (!bb.isEmpty()) {
2237
+ const c = bb.getCenter(this.v1);
2238
+ const s = bb.getSize(this.v2);
2239
+ cx = c.x;
2240
+ cy = bb.min.y;
2241
+ cz = c.z;
2242
+ sx = s.x;
2243
+ sy = s.y;
2244
+ sz = s.z;
2245
+ }
2246
+ const vol = clamp(Math.cbrt(Math.max(sx * sy * sz, 1e-5)), 0.05, 1.5);
2247
+ f.accFire += (18 + 70 * vol) * dt;
2248
+ f.accSmoke += (6 + 24 * vol) * dt;
2249
+ const tone = SMOKE_TONES[def?.thermal.smoke ?? "grey"];
2250
+ const rnd = this.rng;
2251
+ while (f.accFire >= 1) {
2252
+ f.accFire -= 1;
2253
+ 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 });
2254
+ }
2255
+ while (f.accSmoke >= 1) {
2256
+ f.accSmoke -= 1;
2257
+ 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 });
2258
+ }
2259
+ }
2260
+ updateLeaks(dt) {
2261
+ for (let i = this.leaks.length - 1; i >= 0; i--) {
2262
+ const l = this.leaks[i];
2263
+ const ps = this.world.part(l.object, l.part);
2264
+ if (!ps || ps.liters === void 0 || ps.liters <= 0) {
2265
+ this.leaks.splice(i, 1);
2266
+ continue;
2267
+ }
2268
+ l.acc += clamp(20 + l.rate * 400, 15, 120) * dt;
2269
+ const color = FLUID_COLORS[l.fluid] ?? 11184810;
2270
+ const d = l.direction;
2271
+ const dl = Math.hypot(d[0], d[1], d[2]) || 1;
2272
+ const sp = l.pressurized ? 2.5 + this.rng() * 1.5 : 0.3 + this.rng() * 0.3;
2273
+ while (l.acc >= 1) {
2274
+ l.acc -= 1;
2275
+ 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];
2276
+ if (l.fluid === "gas") this.particles.spawn("steam", l.point, vel, { color, size: 0.5, alpha: 0.5 });
2277
+ else this.particles.spawn("drips", l.point, vel, { color, tag: this.rng() < 0.03 ? this.tagOf(l.object) : 0 });
2278
+ }
2279
+ }
2280
+ }
2281
+ tagOf(object) {
2282
+ let tag = this.homeTags.indexOf(object) + 1;
2283
+ if (tag === 0) tag = this.homeTags.push(object);
2284
+ return tag;
2285
+ }
2286
+ // ----------------------------------------------------------------------------------------------------------------------------------
2287
+ // Emit, clear, restore, stats, dispose
2288
+ // ----------------------------------------------------------------------------------------------------------------------------------
2289
+ emit(kind, point, normal, count, speed) {
2290
+ this.particles.burst(kind, point, normal, count, speed ?? DEFAULT_SPEED[kind] ?? 1, kind === "smoke" ? { color: SMOKE_TONES.grey } : void 0);
2291
+ }
2292
+ restore(objectId) {
2293
+ const homes = /* @__PURE__ */ new Set([objectId]);
2294
+ const self = this.bounds.get(objectId);
2295
+ if (self?.isSplit) for (const pv of self.parts.values()) homes.add(pv.home);
2296
+ for (const home of homes) this.restoreHome(home);
2297
+ for (const [key, a] of [...this.active]) if (homes.has(a.object)) this.active.delete(key);
2298
+ for (const [key, f] of [...this.fires]) if (homes.has(f.object)) this.fires.delete(key);
2299
+ for (let i = this.leaks.length - 1; i >= 0; i--) if (homes.has(this.leaks[i].object)) this.leaks.splice(i, 1);
2300
+ for (const home of homes) {
2301
+ this.fragments.removeHome(home);
2302
+ this.ground.removeHome(home);
2303
+ }
2304
+ }
2305
+ restoreHome(home) {
2306
+ const homeBound = this.bounds.get(home);
2307
+ for (const b of [...this.bounds.values()]) {
2308
+ for (const [pid, pv] of [...b.parts]) {
2309
+ if (pv.home !== home) continue;
2310
+ this.visuals.restorePart(pv);
2311
+ if (b !== homeBound && homeBound) {
2312
+ b.parts.delete(pid);
2313
+ homeBound.parts.set(pid, pv);
2314
+ }
2315
+ }
2316
+ if (b.isSplit && b.parts.size === 0) {
2317
+ this.scene.remove(b.root);
2318
+ this.pieceSys.remove(b.id);
2319
+ this.bounds.delete(b.id);
2320
+ }
2321
+ }
2322
+ }
2323
+ clear() {
2324
+ this.queue.length = 0;
2325
+ this.active.clear();
2326
+ this.fires.clear();
2327
+ this.leaks.length = 0;
2328
+ for (const id of [...this.bounds.keys()]) if (this.bounds.has(id)) this.restore(id);
2329
+ for (const b of [...this.bounds.values()]) {
2330
+ if (b.isSplit) {
2331
+ for (const pv of b.parts.values()) this.visuals.restorePart(pv);
2332
+ this.scene.remove(b.root);
2333
+ this.pieceSys.remove(b.id);
2334
+ this.bounds.delete(b.id);
2335
+ }
2336
+ }
2337
+ this.fragments.clear();
2338
+ this.ground.clear();
2339
+ this.particles.clear();
2340
+ if (this.light) {
2341
+ this.light.intensity = 0;
2342
+ this.lightT = 0;
2343
+ this.scene.remove(this.light);
2344
+ }
2345
+ }
2346
+ stats() {
2347
+ return { fragments: this.fragments.count, particles: this.particles.live, pieces: this.pieceSys.list.length, decals: this.visuals.decalCount + this.ground.count };
2348
+ }
2349
+ dispose() {
2350
+ if (this.disposed) return;
2351
+ this.unsubscribe();
2352
+ this.clear();
2353
+ for (const id of [...this.bounds.keys()]) this.unbind(id);
2354
+ this.fragments.dispose();
2355
+ this.ground.dispose();
2356
+ this.particles.dispose();
2357
+ this.visuals.dispose();
2358
+ this.light?.dispose?.();
2359
+ this.light = null;
2360
+ this.disposed = true;
2361
+ }
2362
+ };
2363
+
2364
+ // ../game-destruction/dist/index.js
2365
+ var EPS = 1e-9;
2366
+ var IDENTITY = [0, 0, 0, 1];
2367
+ var quat = {
2368
+ identity: () => [IDENTITY[0], IDENTITY[1], IDENTITY[2], IDENTITY[3]],
2369
+ /** Hamilton product `a * b` (apply `b` first, then `a`). */
2370
+ mul: (a, b) => [
2371
+ a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
2372
+ a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
2373
+ a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
2374
+ a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]
2375
+ ],
2376
+ conjugate: (q) => [-q[0], -q[1], -q[2], q[3]],
2377
+ normalize: (q) => {
2378
+ const l = Math.hypot(q[0], q[1], q[2], q[3]);
2379
+ return l < EPS ? [0, 0, 0, 1] : [q[0] / l, q[1] / l, q[2] / l, q[3] / l];
2380
+ },
2381
+ /** Rotation of `angle` radians around a unit `axis`. */
2382
+ fromAxisAngle: (axis, angle) => {
2383
+ const s = Math.sin(angle / 2);
2384
+ return [axis[0] * s, axis[1] * s, axis[2] * s, Math.cos(angle / 2)];
2385
+ },
2386
+ /** Rotation matrix, row-major 3x3. */
2387
+ toMatrix: (q) => {
2388
+ const [x, y, z, w] = q;
2389
+ 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;
2390
+ 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)];
2391
+ },
2392
+ /** Quaternion from three orthonormal columns (the images of the x, y and z axes). */
2393
+ fromColumns: (cx, cy, cz) => {
2394
+ const m00 = cx[0], m10 = cx[1], m20 = cx[2];
2395
+ const m01 = cy[0], m11 = cy[1], m21 = cy[2];
2396
+ const m02 = cz[0], m12 = cz[1], m22 = cz[2];
2397
+ const tr = m00 + m11 + m22;
2398
+ let q;
2399
+ if (tr > 0) {
2400
+ const s = Math.sqrt(tr + 1) * 2;
2401
+ q = [(m21 - m12) / s, (m02 - m20) / s, (m10 - m01) / s, 0.25 * s];
2402
+ } else if (m00 > m11 && m00 > m22) {
2403
+ const s = Math.sqrt(1 + m00 - m11 - m22) * 2;
2404
+ q = [0.25 * s, (m01 + m10) / s, (m02 + m20) / s, (m21 - m12) / s];
2405
+ } else if (m11 > m22) {
2406
+ const s = Math.sqrt(1 + m11 - m00 - m22) * 2;
2407
+ q = [(m01 + m10) / s, 0.25 * s, (m12 + m21) / s, (m02 - m20) / s];
2408
+ } else {
2409
+ const s = Math.sqrt(1 + m22 - m00 - m11) * 2;
2410
+ q = [(m02 + m20) / s, (m12 + m21) / s, 0.25 * s, (m10 - m01) / s];
2411
+ }
2412
+ return quat.normalize(q);
2413
+ }
2414
+ };
2415
+ var quatFromEuler = (x, y, z) => {
2416
+ const c1 = Math.cos(x / 2), c2 = Math.cos(y / 2), c3 = Math.cos(z / 2);
2417
+ const s1 = Math.sin(x / 2), s2 = Math.sin(y / 2), s3 = Math.sin(z / 2);
2418
+ 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];
2419
+ };
2420
+ var shapeDims = (s) => {
2421
+ if (s.kind === "box") return [s.size[0], s.size[1], s.size[2]];
2422
+ if (s.kind === "cylinder") return [s.radius * 2, s.height, s.radius * 2];
2423
+ return [s.radius * 2, s.radius * 2, s.radius * 2];
2424
+ };
2425
+ var aabbOf = (s, center, q) => {
2426
+ let hx, hy, hz;
2427
+ if (s.kind === "sphere") {
2428
+ hx = hy = hz = s.radius;
2429
+ } else {
2430
+ const m = quat.toMatrix(q);
2431
+ if (s.kind === "box") {
2432
+ const a = s.size[0] / 2, b = s.size[1] / 2, c = s.size[2] / 2;
2433
+ hx = Math.abs(m[0]) * a + Math.abs(m[1]) * b + Math.abs(m[2]) * c;
2434
+ hy = Math.abs(m[3]) * a + Math.abs(m[4]) * b + Math.abs(m[5]) * c;
2435
+ hz = Math.abs(m[6]) * a + Math.abs(m[7]) * b + Math.abs(m[8]) * c;
2436
+ } else {
2437
+ const ax = [m[1], m[4], m[7]];
2438
+ const hh = s.height / 2;
2439
+ const e = (i) => s.radius * Math.sqrt(Math.max(0, 1 - ax[i] * ax[i])) + hh * Math.abs(ax[i]);
2440
+ hx = e(0);
2441
+ hy = e(1);
2442
+ hz = e(2);
2443
+ }
2444
+ }
2445
+ return { min: [center[0] - hx, center[1] - hy, center[2] - hz], max: [center[0] + hx, center[1] + hy, center[2] + hz] };
2446
+ };
2447
+ var materials_default = [
2448
+ {
2449
+ id: "steel",
2450
+ density: 7850,
2451
+ hpPerKg: 400,
2452
+ penetration: 110,
2453
+ ductility: 0.9,
2454
+ fracture: "none",
2455
+ damageScale: {
2456
+ slash: 0.1,
2457
+ blunt: 0.8
2458
+ },
2459
+ hitDebris: "sparks",
2460
+ breakDebris: "sparks",
2461
+ ricochetDeg: 20,
2462
+ thermal: {
2463
+ meltC: 1450,
2464
+ meltRate: 0.05,
2465
+ conductivity: 0.5,
2466
+ heatCapacity: 3
2467
+ },
2468
+ visual: {
2469
+ glows: true
2470
+ },
2471
+ tags: [
2472
+ "metal",
2473
+ "conductive"
2474
+ ],
2475
+ compressive: 2e6
2476
+ },
2477
+ {
2478
+ id: "cast_iron",
2479
+ density: 7200,
2480
+ hpPerKg: 150,
2481
+ penetration: 160,
2482
+ ductility: 0.2,
2483
+ fracture: "snap",
2484
+ fragmentSize: [
2485
+ 0.08,
2486
+ 0.3
2487
+ ],
2488
+ damageScale: {
2489
+ slash: 0.1,
2490
+ blunt: 1.2
2491
+ },
2492
+ hitDebris: "sparks",
2493
+ breakDebris: "chips",
2494
+ ricochetDeg: 15,
2495
+ thermal: {
2496
+ meltC: 1200,
2497
+ meltRate: 0.05,
2498
+ conductivity: 0.45,
2499
+ heatCapacity: 3
2500
+ },
2501
+ visual: {
2502
+ glows: true
2503
+ },
2504
+ tags: [
2505
+ "metal",
2506
+ "conductive"
2507
+ ],
2508
+ compressive: 15e5
2509
+ },
2510
+ {
2511
+ id: "aluminium",
2512
+ density: 2700,
2513
+ hpPerKg: 250,
2514
+ penetration: 60,
2515
+ ductility: 0.85,
2516
+ fracture: "none",
2517
+ damageScale: {
2518
+ slash: 0.2
2519
+ },
2520
+ hitDebris: "sparks",
2521
+ breakDebris: "sparks",
2522
+ ricochetDeg: 15,
2523
+ thermal: {
2524
+ meltC: 660,
2525
+ meltRate: 0.06,
2526
+ conductivity: 0.8,
2527
+ heatCapacity: 1.5
2528
+ },
2529
+ visual: {
2530
+ glows: true
2531
+ },
2532
+ tags: [
2533
+ "metal",
2534
+ "conductive"
2535
+ ],
2536
+ compressive: 8e5
2537
+ },
2538
+ {
2539
+ id: "copper",
2540
+ density: 8960,
2541
+ hpPerKg: 300,
2542
+ penetration: 70,
2543
+ ductility: 0.95,
2544
+ fracture: "none",
2545
+ damageScale: {
2546
+ slash: 0.2
2547
+ },
2548
+ hitDebris: "sparks",
2549
+ breakDebris: "sparks",
2550
+ ricochetDeg: 15,
2551
+ thermal: {
2552
+ meltC: 1085,
2553
+ meltRate: 0.05,
2554
+ conductivity: 1,
2555
+ heatCapacity: 2.5
2556
+ },
2557
+ visual: {
2558
+ glows: true
2559
+ },
2560
+ tags: [
2561
+ "metal",
2562
+ "conductive"
2563
+ ],
2564
+ compressive: 1e6
2565
+ },
2566
+ {
2567
+ id: "wood_soft",
2568
+ density: 500,
2569
+ hpPerKg: 90,
2570
+ penetration: 9,
2571
+ ductility: 0.35,
2572
+ fracture: "splinter",
2573
+ fragmentSize: [
2574
+ 0.03,
2575
+ 0.15
2576
+ ],
2577
+ damageScale: {
2578
+ blunt: 1.2,
2579
+ slash: 1.5
2580
+ },
2581
+ hitDebris: "splinters",
2582
+ breakDebris: "splinters",
2583
+ thermal: {
2584
+ ignitionC: 300,
2585
+ flameC: 800,
2586
+ fuelFraction: 0.9,
2587
+ burnRate: 0.012,
2588
+ conductivity: 0.05,
2589
+ heatCapacity: 1.2,
2590
+ smoke: "grey"
2591
+ },
2592
+ visual: {
2593
+ charColor: 1709072
2594
+ },
2595
+ tags: [
2596
+ "wood",
2597
+ "organic",
2598
+ "flammable"
2599
+ ],
2600
+ compressive: 1e5
2601
+ },
2602
+ {
2603
+ id: "wood_hard",
2604
+ density: 750,
2605
+ hpPerKg: 140,
2606
+ penetration: 14,
2607
+ ductility: 0.3,
2608
+ fracture: "splinter",
2609
+ fragmentSize: [
2610
+ 0.03,
2611
+ 0.2
2612
+ ],
2613
+ damageScale: {
2614
+ slash: 1.2
2615
+ },
2616
+ hitDebris: "splinters",
2617
+ breakDebris: "splinters",
2618
+ thermal: {
2619
+ ignitionC: 330,
2620
+ flameC: 800,
2621
+ fuelFraction: 0.9,
2622
+ burnRate: 0.01,
2623
+ conductivity: 0.06,
2624
+ heatCapacity: 1.4,
2625
+ smoke: "grey"
2626
+ },
2627
+ visual: {
2628
+ charColor: 1709072
2629
+ },
2630
+ tags: [
2631
+ "wood",
2632
+ "organic",
2633
+ "flammable"
2634
+ ],
2635
+ compressive: 15e4
2636
+ },
2637
+ {
2638
+ id: "plywood",
2639
+ density: 600,
2640
+ hpPerKg: 70,
2641
+ penetration: 7,
2642
+ ductility: 0.3,
2643
+ fracture: "snap",
2644
+ fragmentSize: [
2645
+ 0.1,
2646
+ 0.4
2647
+ ],
2648
+ damageScale: {
2649
+ slash: 1.3
2650
+ },
2651
+ hitDebris: "splinters",
2652
+ breakDebris: "splinters",
2653
+ thermal: {
2654
+ ignitionC: 300,
2655
+ flameC: 800,
2656
+ fuelFraction: 0.85,
2657
+ burnRate: 0.013,
2658
+ conductivity: 0.05,
2659
+ heatCapacity: 1.2,
2660
+ smoke: "grey"
2661
+ },
2662
+ visual: {
2663
+ charColor: 1709072
2664
+ },
2665
+ tags: [
2666
+ "wood",
2667
+ "organic",
2668
+ "flammable"
2669
+ ],
2670
+ compressive: 6e4
2671
+ },
2672
+ {
2673
+ id: "mdf",
2674
+ density: 750,
2675
+ hpPerKg: 45,
2676
+ penetration: 6,
2677
+ ductility: 0.15,
2678
+ fracture: "snap",
2679
+ fragmentSize: [
2680
+ 0.08,
2681
+ 0.35
2682
+ ],
2683
+ hitDebris: "chips",
2684
+ breakDebris: "dust",
2685
+ thermal: {
2686
+ ignitionC: 290,
2687
+ flameC: 750,
2688
+ fuelFraction: 0.85,
2689
+ burnRate: 0.014,
2690
+ conductivity: 0.05,
2691
+ heatCapacity: 1.2,
2692
+ smoke: "grey"
2693
+ },
2694
+ visual: {
2695
+ charColor: 1709072
2696
+ },
2697
+ tags: [
2698
+ "wood",
2699
+ "organic",
2700
+ "flammable"
2701
+ ],
2702
+ compressive: 3e4
2703
+ },
2704
+ {
2705
+ id: "glass",
2706
+ density: 2500,
2707
+ hpPerKg: 12,
2708
+ penetration: 6,
2709
+ ductility: 0.02,
2710
+ fracture: "shatter",
2711
+ fragmentSize: [
2712
+ 0.05,
2713
+ 0.25
2714
+ ],
2715
+ damageScale: {
2716
+ blunt: 1.5
2717
+ },
2718
+ hitDebris: "shards",
2719
+ breakDebris: "shards",
2720
+ ricochetDeg: 8,
2721
+ thermal: {
2722
+ meltC: 1400,
2723
+ meltRate: 0.03,
2724
+ conductivity: 0.1,
2725
+ heatCapacity: 1.5,
2726
+ thermalShockC: 150
2727
+ },
2728
+ visual: {
2729
+ glows: true,
2730
+ transparent: true
2731
+ },
2732
+ tags: [
2733
+ "glass",
2734
+ "brittle"
2735
+ ],
2736
+ compressive: 1e5
2737
+ },
2738
+ {
2739
+ id: "tempered_glass",
2740
+ density: 2500,
2741
+ hpPerKg: 30,
2742
+ penetration: 9,
2743
+ ductility: 0.02,
2744
+ fracture: "crumble",
2745
+ fragmentSize: [
2746
+ 6e-3,
2747
+ 0.02
2748
+ ],
2749
+ hitDebris: "shards",
2750
+ breakDebris: "shards",
2751
+ ricochetDeg: 8,
2752
+ thermal: {
2753
+ meltC: 1400,
2754
+ meltRate: 0.03,
2755
+ conductivity: 0.1,
2756
+ heatCapacity: 1.5,
2757
+ thermalShockC: 250
2758
+ },
2759
+ visual: {
2760
+ glows: true,
2761
+ transparent: true
2762
+ },
2763
+ tags: [
2764
+ "glass",
2765
+ "brittle"
2766
+ ],
2767
+ compressive: 2e5
2768
+ },
2769
+ {
2770
+ id: "ceramic",
2771
+ density: 2400,
2772
+ hpPerKg: 25,
2773
+ penetration: 12,
2774
+ ductility: 0.02,
2775
+ fracture: "shatter",
2776
+ fragmentSize: [
2777
+ 0.03,
2778
+ 0.12
2779
+ ],
2780
+ damageScale: {
2781
+ blunt: 1.3
2782
+ },
2783
+ hitDebris: "chips",
2784
+ breakDebris: "shards",
2785
+ thermal: {
2786
+ conductivity: 0.1,
2787
+ heatCapacity: 1.5,
2788
+ thermalShockC: 200
2789
+ },
2790
+ tags: [
2791
+ "ceramic",
2792
+ "brittle"
2793
+ ],
2794
+ compressive: 15e4
2795
+ },
2796
+ {
2797
+ id: "concrete",
2798
+ density: 2300,
2799
+ hpPerKg: 14,
2800
+ penetration: 18,
2801
+ ductility: 0.03,
2802
+ fracture: "crumble",
2803
+ fragmentSize: [
2804
+ 0.05,
2805
+ 0.25
2806
+ ],
2807
+ damageScale: {
2808
+ explosive: 1.2
2809
+ },
2810
+ hitDebris: "chips",
2811
+ breakDebris: "dust",
2812
+ thermal: {
2813
+ conductivity: 0.15,
2814
+ heatCapacity: 2.5
2815
+ },
2816
+ tags: [
2817
+ "masonry",
2818
+ "brittle"
2819
+ ],
2820
+ compressive: 444e3
2821
+ },
2822
+ {
2823
+ id: "brick",
2824
+ density: 1900,
2825
+ hpPerKg: 12,
2826
+ penetration: 14,
2827
+ ductility: 0.05,
2828
+ fracture: "crumble",
2829
+ fragmentSize: [
2830
+ 0.04,
2831
+ 0.12
2832
+ ],
2833
+ hitDebris: "chips",
2834
+ breakDebris: "dust",
2835
+ thermal: {
2836
+ conductivity: 0.15,
2837
+ heatCapacity: 2.5
2838
+ },
2839
+ tags: [
2840
+ "masonry",
2841
+ "brittle"
2842
+ ],
2843
+ compressive: 4e4
2844
+ },
2845
+ {
2846
+ id: "plaster",
2847
+ density: 850,
2848
+ hpPerKg: 18,
2849
+ penetration: 3,
2850
+ ductility: 0.05,
2851
+ fracture: "crumble",
2852
+ fragmentSize: [
2853
+ 0.05,
2854
+ 0.3
2855
+ ],
2856
+ hitDebris: "dust",
2857
+ breakDebris: "dust",
2858
+ thermal: {
2859
+ conductivity: 0.1,
2860
+ heatCapacity: 1.5
2861
+ },
2862
+ tags: [
2863
+ "masonry",
2864
+ "brittle"
2865
+ ],
2866
+ compressive: 8e3
2867
+ },
2868
+ {
2869
+ id: "stone",
2870
+ density: 2700,
2871
+ hpPerKg: 25,
2872
+ penetration: 25,
2873
+ ductility: 0.03,
2874
+ fracture: "crumble",
2875
+ fragmentSize: [
2876
+ 0.06,
2877
+ 0.3
2878
+ ],
2879
+ hitDebris: "chips",
2880
+ breakDebris: "dust",
2881
+ thermal: {
2882
+ conductivity: 0.2,
2883
+ heatCapacity: 2.5
2884
+ },
2885
+ tags: [
2886
+ "masonry",
2887
+ "brittle"
2888
+ ],
2889
+ compressive: 3e5
2890
+ },
2891
+ {
2892
+ id: "plastic",
2893
+ density: 1100,
2894
+ hpPerKg: 60,
2895
+ penetration: 8,
2896
+ ductility: 0.5,
2897
+ fracture: "snap",
2898
+ fragmentSize: [
2899
+ 0.05,
2900
+ 0.3
2901
+ ],
2902
+ hitDebris: "chips",
2903
+ breakDebris: "chips",
2904
+ thermal: {
2905
+ ignitionC: 400,
2906
+ flameC: 700,
2907
+ fuelFraction: 0.8,
2908
+ burnRate: 0.02,
2909
+ meltC: 200,
2910
+ meltRate: 0.06,
2911
+ conductivity: 0.05,
2912
+ heatCapacity: 1,
2913
+ smoke: "black"
2914
+ },
2915
+ visual: {
2916
+ charColor: 855309
2917
+ },
2918
+ tags: [
2919
+ "plastic",
2920
+ "flammable"
2921
+ ],
2922
+ compressive: 4e4
2923
+ },
2924
+ {
2925
+ id: "rubber",
2926
+ density: 1100,
2927
+ hpPerKg: 80,
2928
+ penetration: 12,
2929
+ ductility: 0.9,
2930
+ fracture: "tear",
2931
+ fragmentSize: [
2932
+ 0.05,
2933
+ 0.2
2934
+ ],
2935
+ hitDebris: "chips",
2936
+ breakDebris: "fibers",
2937
+ thermal: {
2938
+ ignitionC: 350,
2939
+ flameC: 750,
2940
+ fuelFraction: 0.6,
2941
+ burnRate: 0.015,
2942
+ conductivity: 0.05,
2943
+ heatCapacity: 1.2,
2944
+ smoke: "black"
2945
+ },
2946
+ visual: {
2947
+ charColor: 855309
2948
+ },
2949
+ tags: [
2950
+ "rubber",
2951
+ "flammable"
2952
+ ],
2953
+ compressive: 2e4
2954
+ },
2955
+ {
2956
+ id: "fabric",
2957
+ density: 400,
2958
+ hpPerKg: 20,
2959
+ penetration: 0.8,
2960
+ ductility: 0.8,
2961
+ fracture: "tear",
2962
+ fragmentSize: [
2963
+ 0.05,
2964
+ 0.25
2965
+ ],
2966
+ damageScale: {
2967
+ slash: 4,
2968
+ ballistic: 0.5
2969
+ },
2970
+ hitDebris: "fibers",
2971
+ breakDebris: "fibers",
2972
+ thermal: {
2973
+ ignitionC: 230,
2974
+ flameC: 600,
2975
+ fuelFraction: 1,
2976
+ burnRate: 0.05,
2977
+ conductivity: 0.03,
2978
+ heatCapacity: 0.6,
2979
+ smoke: "grey"
2980
+ },
2981
+ visual: {
2982
+ charColor: 1709072
2983
+ },
2984
+ tags: [
2985
+ "fabric",
2986
+ "organic",
2987
+ "flammable"
2988
+ ],
2989
+ compressive: 500
2990
+ },
2991
+ {
2992
+ id: "foam",
2993
+ density: 30,
2994
+ hpPerKg: 15,
2995
+ penetration: 0.5,
2996
+ ductility: 0.7,
2997
+ fracture: "tear",
2998
+ fragmentSize: [
2999
+ 0.03,
3000
+ 0.15
3001
+ ],
3002
+ damageScale: {
3003
+ slash: 2,
3004
+ ballistic: 0.4
3005
+ },
3006
+ hitDebris: "fibers",
3007
+ breakDebris: "fibers",
3008
+ thermal: {
3009
+ ignitionC: 250,
3010
+ flameC: 900,
3011
+ fuelFraction: 0.95,
3012
+ burnRate: 0.09,
3013
+ conductivity: 0.02,
3014
+ heatCapacity: 0.5,
3015
+ smoke: "black"
3016
+ },
3017
+ visual: {
3018
+ charColor: 855309
3019
+ },
3020
+ tags: [
3021
+ "fabric",
3022
+ "flammable"
3023
+ ],
3024
+ compressive: 100
3025
+ },
3026
+ {
3027
+ id: "leather",
3028
+ density: 900,
3029
+ hpPerKg: 50,
3030
+ penetration: 4,
3031
+ ductility: 0.8,
3032
+ fracture: "tear",
3033
+ fragmentSize: [
3034
+ 0.05,
3035
+ 0.25
3036
+ ],
3037
+ damageScale: {
3038
+ slash: 2
3039
+ },
3040
+ hitDebris: "fibers",
3041
+ breakDebris: "fibers",
3042
+ thermal: {
3043
+ ignitionC: 320,
3044
+ flameC: 650,
3045
+ fuelFraction: 0.7,
3046
+ burnRate: 0.01,
3047
+ conductivity: 0.05,
3048
+ heatCapacity: 1,
3049
+ smoke: "grey"
3050
+ },
3051
+ visual: {
3052
+ charColor: 1709072
3053
+ },
3054
+ tags: [
3055
+ "fabric",
3056
+ "organic",
3057
+ "flammable"
3058
+ ],
3059
+ compressive: 2e3
3060
+ },
3061
+ {
3062
+ id: "paper",
3063
+ density: 700,
3064
+ hpPerKg: 8,
3065
+ penetration: 0.3,
3066
+ ductility: 0.5,
3067
+ fracture: "tear",
3068
+ fragmentSize: [
3069
+ 0.03,
3070
+ 0.15
3071
+ ],
3072
+ damageScale: {
3073
+ slash: 3
3074
+ },
3075
+ hitDebris: "fibers",
3076
+ breakDebris: "fibers",
3077
+ thermal: {
3078
+ ignitionC: 230,
3079
+ flameC: 600,
3080
+ fuelFraction: 1,
3081
+ burnRate: 0.06,
3082
+ conductivity: 0.03,
3083
+ heatCapacity: 0.5,
3084
+ smoke: "grey"
3085
+ },
3086
+ visual: {
3087
+ charColor: 1709072
3088
+ },
3089
+ tags: [
3090
+ "fabric",
3091
+ "organic",
3092
+ "flammable"
3093
+ ],
3094
+ compressive: 500
3095
+ },
3096
+ {
3097
+ id: "foliage",
3098
+ density: 150,
3099
+ hpPerKg: 20,
3100
+ penetration: 0.5,
3101
+ ductility: 0.6,
3102
+ fracture: "tear",
3103
+ fragmentSize: [
3104
+ 0.03,
3105
+ 0.1
3106
+ ],
3107
+ damageScale: {
3108
+ slash: 4
3109
+ },
3110
+ hitDebris: "leaves",
3111
+ breakDebris: "leaves",
3112
+ thermal: {
3113
+ ignitionC: 300,
3114
+ flameC: 700,
3115
+ fuelFraction: 0.8,
3116
+ burnRate: 0.08,
3117
+ conductivity: 0.03,
3118
+ heatCapacity: 0.8,
3119
+ smoke: "white"
3120
+ },
3121
+ visual: {
3122
+ charColor: 1709072
3123
+ },
3124
+ tags: [
3125
+ "plant",
3126
+ "organic",
3127
+ "flammable"
3128
+ ],
3129
+ compressive: 500
3130
+ },
3131
+ {
3132
+ id: "green_wood",
3133
+ density: 650,
3134
+ hpPerKg: 120,
3135
+ penetration: 10,
3136
+ ductility: 0.5,
3137
+ fracture: "splinter",
3138
+ fragmentSize: [
3139
+ 0.03,
3140
+ 0.2
3141
+ ],
3142
+ damageScale: {
3143
+ slash: 1.3
3144
+ },
3145
+ hitDebris: "splinters",
3146
+ breakDebris: "leaves",
3147
+ thermal: {
3148
+ ignitionC: 400,
3149
+ flameC: 700,
3150
+ fuelFraction: 0.8,
3151
+ burnRate: 6e-3,
3152
+ conductivity: 0.06,
3153
+ heatCapacity: 2,
3154
+ smoke: "white"
3155
+ },
3156
+ visual: {
3157
+ charColor: 1709072
3158
+ },
3159
+ tags: [
3160
+ "plant",
3161
+ "wood",
3162
+ "organic",
3163
+ "flammable"
3164
+ ],
3165
+ compressive: 6e4
3166
+ },
3167
+ {
3168
+ id: "cactus",
3169
+ density: 900,
3170
+ hpPerKg: 30,
3171
+ penetration: 2,
3172
+ ductility: 0.6,
3173
+ fracture: "tear",
3174
+ fragmentSize: [
3175
+ 0.04,
3176
+ 0.15
3177
+ ],
3178
+ damageScale: {
3179
+ slash: 3
3180
+ },
3181
+ hitDebris: "fibers",
3182
+ breakDebris: "fibers",
3183
+ thermal: {
3184
+ ignitionC: 450,
3185
+ flameC: 650,
3186
+ fuelFraction: 0.3,
3187
+ burnRate: 0.01,
3188
+ conductivity: 0.05,
3189
+ heatCapacity: 3,
3190
+ smoke: "white"
3191
+ },
3192
+ visual: {
3193
+ charColor: 1709072
3194
+ },
3195
+ tags: [
3196
+ "plant",
3197
+ "organic",
3198
+ "flammable"
3199
+ ],
3200
+ compressive: 5e3
3201
+ },
3202
+ {
3203
+ id: "flesh_fruit",
3204
+ density: 950,
3205
+ hpPerKg: 15,
3206
+ penetration: 1.5,
3207
+ ductility: 0.7,
3208
+ fracture: "tear",
3209
+ fragmentSize: [
3210
+ 0.02,
3211
+ 0.08
3212
+ ],
3213
+ damageScale: {
3214
+ slash: 2
3215
+ },
3216
+ hitDebris: "drips",
3217
+ breakDebris: "drips",
3218
+ thermal: {
3219
+ conductivity: 0.05,
3220
+ heatCapacity: 3
3221
+ },
3222
+ tags: [
3223
+ "plant",
3224
+ "organic"
3225
+ ],
3226
+ compressive: 1e3
3227
+ },
3228
+ {
3229
+ id: "wax",
3230
+ density: 900,
3231
+ hpPerKg: 30,
3232
+ penetration: 3,
3233
+ ductility: 0.5,
3234
+ fracture: "snap",
3235
+ fragmentSize: [
3236
+ 0.05,
3237
+ 0.2
3238
+ ],
3239
+ hitDebris: "chips",
3240
+ breakDebris: "chips",
3241
+ thermal: {
3242
+ ignitionC: 230,
3243
+ flameC: 700,
3244
+ fuelFraction: 0.9,
3245
+ burnRate: 0.02,
3246
+ meltC: 60,
3247
+ meltRate: 0.2,
3248
+ conductivity: 0.05,
3249
+ heatCapacity: 0.8,
3250
+ smoke: "grey"
3251
+ },
3252
+ tags: [
3253
+ "wax",
3254
+ "flammable"
3255
+ ],
3256
+ compressive: 5e3
3257
+ },
3258
+ {
3259
+ id: "soil",
3260
+ density: 1300,
3261
+ hpPerKg: 3,
3262
+ penetration: 5,
3263
+ ductility: 0.3,
3264
+ fracture: "crumble",
3265
+ fragmentSize: [
3266
+ 0.03,
3267
+ 0.12
3268
+ ],
3269
+ hitDebris: "dust",
3270
+ breakDebris: "dust",
3271
+ thermal: {
3272
+ conductivity: 0.1,
3273
+ heatCapacity: 2.5
3274
+ },
3275
+ tags: [
3276
+ "soil",
3277
+ "masonry"
3278
+ ],
3279
+ compressive: 2e4
3280
+ }
3281
+ ];
3282
+ var material_hints_default = {
3283
+ "cast iron": "cast_iron",
3284
+ cast_iron: "cast_iron",
3285
+ castiron: "cast_iron",
3286
+ metal: "steel",
3287
+ steel: "steel",
3288
+ iron: "steel",
3289
+ chrome: "steel",
3290
+ alumin: "aluminium",
3291
+ alu_: "aluminium",
3292
+ "alu-": "aluminium",
3293
+ "alu ": "aluminium",
3294
+ copper: "copper",
3295
+ brass: "copper",
3296
+ bronze: "copper",
3297
+ wood: "wood_soft",
3298
+ timber: "wood_soft",
3299
+ plank: "wood_soft",
3300
+ lumber: "wood_soft",
3301
+ pine: "wood_soft",
3302
+ hardwood: "wood_hard",
3303
+ oak: "wood_hard",
3304
+ plywood: "plywood",
3305
+ mdf: "mdf",
3306
+ chipboard: "mdf",
3307
+ glass: "glass",
3308
+ window: "glass",
3309
+ mirror: "glass",
3310
+ tempered: "tempered_glass",
3311
+ windshield: "tempered_glass",
3312
+ fabric: "fabric",
3313
+ cloth: "fabric",
3314
+ cushion: "fabric",
3315
+ sofa: "fabric",
3316
+ curtain: "fabric",
3317
+ carpet: "fabric",
3318
+ textile: "fabric",
3319
+ canvas: "fabric",
3320
+ foam: "foam",
3321
+ sponge: "foam",
3322
+ mattress: "foam",
3323
+ leather: "leather",
3324
+ paper: "paper",
3325
+ cardboard: "paper",
3326
+ leaf: "foliage",
3327
+ leaves: "foliage",
3328
+ foliage: "foliage",
3329
+ bush: "foliage",
3330
+ grass: "foliage",
3331
+ trunk: "green_wood",
3332
+ branch: "green_wood",
3333
+ twig: "green_wood",
3334
+ cactus: "cactus",
3335
+ fruit: "flesh_fruit",
3336
+ apple: "flesh_fruit",
3337
+ melon: "flesh_fruit",
3338
+ wax: "wax",
3339
+ candle: "wax",
3340
+ concrete: "concrete",
3341
+ cement: "concrete",
3342
+ brick: "brick",
3343
+ tile: "ceramic",
3344
+ ceramic: "ceramic",
3345
+ porcelain: "ceramic",
3346
+ stone: "stone",
3347
+ rock: "stone",
3348
+ marble: "stone",
3349
+ plaster: "plaster",
3350
+ drywall: "plaster",
3351
+ gypsum: "plaster",
3352
+ plastic: "plastic",
3353
+ pvc: "plastic",
3354
+ acrylic: "plastic",
3355
+ rubber: "rubber",
3356
+ tire: "rubber",
3357
+ tyre: "rubber",
3358
+ soil: "soil",
3359
+ dirt: "soil",
3360
+ potting: "soil",
3361
+ mud: "soil"
3362
+ };
3363
+ var MATERIALS = materials_default;
3364
+ var has = (m, tag) => !!m.tags && m.tags.includes(tag);
3365
+ var JOINT_BY_MATERIALS = (a, b) => {
3366
+ const any = (tag) => has(a, tag) || has(b, tag);
3367
+ const both = (tag) => has(a, tag) && has(b, tag);
3368
+ if (any("glass")) return any("plastic") ? "clip" : "glue";
3369
+ if (any("fabric")) return "tie";
3370
+ if (both("plant")) return "stem";
3371
+ if (any("plant")) return "tie";
3372
+ if (both("masonry")) return "mortar";
3373
+ if (any("masonry") && any("metal")) return "bolt";
3374
+ if (both("metal")) return "weld";
3375
+ if (any("plastic")) return "clip";
3376
+ if (any("rubber") || any("wax")) return "glue";
3377
+ return "screw";
3378
+ };
3379
+ var MATERIAL_MAP = new Map(MATERIALS.map((m) => [m.id, m]));
3380
+ var HINTS = Object.entries(material_hints_default).sort((x, y) => y[0].length - x[0].length);
3381
+ var guessMaterial = (name) => {
3382
+ const n = name.toLowerCase();
3383
+ for (const [key, id] of HINTS) if (n.includes(key)) return id;
3384
+ return void 0;
3385
+ };
3386
+ var partThickness = (part) => {
3387
+ const dims = shapeDims(part.shape);
3388
+ const min = Math.min(dims[0], dims[1], dims[2]);
3389
+ return Math.max(1e-4, Math.min(part.thickness ?? min * (part.fill ?? 1), min));
3390
+ };
3391
+ var inferJoints = (parts, tolerance = 0.01, materials = MATERIAL_MAP) => {
3392
+ const boxes = parts.map((p) => aabbOf(p.shape, p.position, quatFromEuler(...p.rotation ?? [0, 0, 0])));
3393
+ const out = [];
3394
+ for (let i = 0; i < parts.length; i++) {
3395
+ for (let j = i + 1; j < parts.length; j++) {
3396
+ const a = boxes[i], b = boxes[j];
3397
+ let touch = true;
3398
+ 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;
3399
+ if (!touch) continue;
3400
+ const ma = materials.get(parts[i].material);
3401
+ const mb = materials.get(parts[j].material);
3402
+ let kind = ma && mb ? JOINT_BY_MATERIALS(ma, mb) : "screw";
3403
+ const thin = Math.min(partThickness(parts[i]), partThickness(parts[j]));
3404
+ if (kind === "weld" && thin >= 0.02) kind = "bolt";
3405
+ if (kind === "screw" && ma && mb && has(ma, "wood") && has(mb, "wood") && thin < 0.02) kind = "nail";
3406
+ out.push({ id: `${parts[i].id}~${parts[j].id}`, a: parts[i].id, b: parts[j].id, kind });
3407
+ }
3408
+ }
3409
+ return out;
3410
+ };
3411
+
3412
+ // src/spec.ts
3413
+ var METAL = /steel|iron|metal|alumin|copper|brass|bronze|tin|titan|chrome|zinc|lead/i;
3414
+ function sanitizeId(name) {
3415
+ let id = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
3416
+ if (id && /^[0-9]/.test(id)) id = `p_${id}`;
3417
+ return id;
3418
+ }
3419
+ function isMesh(o) {
3420
+ return o.isMesh === true;
3421
+ }
3422
+ function sizeOf(box) {
3423
+ return [box.max.x - box.min.x, box.max.y - box.min.y, box.max.z - box.min.z];
3424
+ }
3425
+ function specFromObject3D(THREE, root, options = {}) {
3426
+ const fallback = options.fallback ?? "plastic";
3427
+ const minSize = options.minSize ?? 0.015;
3428
+ root.updateMatrixWorld(true);
3429
+ const inv = root.matrixWorld.clone().invert();
3430
+ const entries = [];
3431
+ const byKey = /* @__PURE__ */ new Map();
3432
+ const visit = (o) => {
3433
+ if (!o.visible || o.userData?.destructionIgnore) return;
3434
+ if (isMesh(o) && o.geometry) {
3435
+ const geom = o.geometry;
3436
+ if (!geom.boundingBox) geom.computeBoundingBox();
3437
+ if (geom.boundingBox && !geom.boundingBox.isEmpty()) {
3438
+ const box = geom.boundingBox.clone().applyMatrix4(new THREE.Matrix4().multiplyMatrices(inv, o.matrixWorld));
3439
+ const mats = Array.isArray(o.material) ? o.material : [o.material];
3440
+ const matName = mats[0]?.name ?? "";
3441
+ const ud = o.userData ?? {};
3442
+ const material = options.materialOf?.(o) ?? (typeof ud.destructionMaterial === "string" ? ud.destructionMaterial : void 0) ?? guessMaterial(`${o.name} ${matName} ${typeof ud.material === "string" ? ud.material : ""}`) ?? fallback;
3443
+ const explicit = typeof ud.destructionPart === "string" && ud.destructionPart ? ud.destructionPart : null;
3444
+ const hollow = ud.hollow === true || /hollow/i.test(o.name);
3445
+ const fill = options.fillOf?.(o, material);
3446
+ const tags = Array.isArray(ud.destructionTags) ? ud.destructionTags : void 0;
3447
+ const existing = explicit ? byKey.get(explicit) : void 0;
3448
+ if (existing) {
3449
+ const before = sizeOf(existing.box);
3450
+ const bv = before[0] * before[1] * before[2];
3451
+ const ns = sizeOf(box);
3452
+ existing.box.union(box);
3453
+ existing.meshes.push(o);
3454
+ if (ns[0] * ns[1] * ns[2] > bv) existing.material = material;
3455
+ existing.hollow = existing.hollow || hollow;
3456
+ if (fill !== void 0) existing.fill = fill;
3457
+ } else {
3458
+ const e = { key: explicit ?? `m${entries.length}`, meshes: [o], box, material, explicitId: explicit, fill, hollow, tags, fixedId: !!explicit, name: o.name };
3459
+ entries.push(e);
3460
+ if (explicit) byKey.set(explicit, e);
3461
+ }
3462
+ }
3463
+ }
3464
+ for (const c of o.children) visit(c);
3465
+ };
3466
+ visit(root);
3467
+ const eps = 5e-3;
3468
+ const overlapVolume = (a, b) => {
3469
+ const x = Math.min(a.max.x, b.max.x) - Math.max(a.min.x, b.min.x) + eps;
3470
+ const y = Math.min(a.max.y, b.max.y) - Math.max(a.min.y, b.min.y) + eps;
3471
+ const z = Math.min(a.max.z, b.max.z) - Math.max(a.min.z, b.min.z) + eps;
3472
+ return x > 0 && y > 0 && z > 0 ? x * y * z : 0;
3473
+ };
3474
+ const largest = (e) => Math.max(...sizeOf(e.box));
3475
+ const kept = [];
3476
+ const tiny = [];
3477
+ for (const e of entries) (!e.fixedId && largest(e) < minSize ? tiny : kept).push(e);
3478
+ for (const t of tiny) {
3479
+ let best = null;
3480
+ let bestV = 0;
3481
+ for (const k of kept) {
3482
+ const v = overlapVolume(t.box, k.box);
3483
+ if (v > bestV) {
3484
+ bestV = v;
3485
+ best = k;
3486
+ }
3487
+ }
3488
+ if (!best) {
3489
+ let bd = 0.05;
3490
+ for (const k of kept) {
3491
+ const d = k.box.distanceToPoint(t.box.getCenter(new THREE.Vector3()));
3492
+ if (d < bd) {
3493
+ bd = d;
3494
+ best = k;
3495
+ }
3496
+ }
3497
+ }
3498
+ if (best) {
3499
+ best.meshes.push(...t.meshes);
3500
+ } else {
3501
+ kept.push(t);
3502
+ }
3503
+ }
3504
+ const used = /* @__PURE__ */ new Set();
3505
+ const parts = [];
3506
+ const meshesOf = {};
3507
+ let counter = 0;
3508
+ for (const e of kept) {
3509
+ counter++;
3510
+ let base = (e.explicitId ? sanitizeId(e.explicitId) : "") || sanitizeId(e.name) || `part_${counter}`;
3511
+ let id = base;
3512
+ for (let n = 2; used.has(id); n++) id = `${base}_${n}`;
3513
+ used.add(id);
3514
+ base = id;
3515
+ const size = sizeOf(e.box);
3516
+ const center = e.box.getCenter(new THREE.Vector3());
3517
+ const part = {
3518
+ id,
3519
+ material: e.material,
3520
+ shape: { kind: "box", size: [Math.max(size[0], 2e-3), Math.max(size[1], 2e-3), Math.max(size[2], 2e-3)] },
3521
+ position: [center.x, center.y, center.z]
3522
+ };
3523
+ const minSide = Math.min(...size);
3524
+ if (e.fill !== void 0) part.fill = e.fill;
3525
+ else if (e.hollow) part.fill = 0.05;
3526
+ if (minSide < 0.01 && METAL.test(e.material)) {
3527
+ part.fill = e.fill ?? 1;
3528
+ part.thickness = Math.max(minSide, 5e-4);
3529
+ }
3530
+ if (e.tags) part.tags = [...e.tags];
3531
+ const ao = options.anchorBelow ?? -1;
3532
+ if (ao >= 0 && e.box.min.y <= ao) part.anchored = true;
3533
+ parts.push(part);
3534
+ meshesOf[id] = e.meshes;
3535
+ }
3536
+ const spec = { parts };
3537
+ if (options.name !== void 0) spec.name = options.name;
3538
+ else if (root.name) spec.name = root.name;
3539
+ if (options.tags) spec.tags = [...options.tags];
3540
+ spec.joints = options.joints === false ? [] : inferJoints(parts);
3541
+ return { spec, parts: meshesOf };
3542
+ }
3543
+ return __toCommonJS(index_exports);
3544
+ })();
3545
+ //# sourceMappingURL=index.global.js.map