@adatechnology/game-destruction 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3034 @@
1
+ // src/math.ts
2
+ var EPS = 1e-9;
3
+ var vec = {
4
+ add: (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]],
5
+ sub: (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]],
6
+ scale: (a, s) => [a[0] * s, a[1] * s, a[2] * s],
7
+ dot: (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2],
8
+ cross: (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]],
9
+ length: (a) => Math.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]),
10
+ distance: (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]),
11
+ normalize: (a, fallback = [0, 1, 0]) => {
12
+ const l = Math.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]);
13
+ return l < EPS ? [fallback[0], fallback[1], fallback[2]] : [a[0] / l, a[1] / l, a[2] / l];
14
+ },
15
+ lerp: (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t],
16
+ /** `a + b * s` */
17
+ madd: (a, b, s) => [a[0] + b[0] * s, a[1] + b[1] * s, a[2] + b[2] * s]
18
+ };
19
+ var IDENTITY = [0, 0, 0, 1];
20
+ var quat = {
21
+ identity: () => [IDENTITY[0], IDENTITY[1], IDENTITY[2], IDENTITY[3]],
22
+ /** Hamilton product `a * b` (apply `b` first, then `a`). */
23
+ mul: (a, b) => [
24
+ a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
25
+ a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
26
+ a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
27
+ a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]
28
+ ],
29
+ conjugate: (q) => [-q[0], -q[1], -q[2], q[3]],
30
+ normalize: (q) => {
31
+ const l = Math.hypot(q[0], q[1], q[2], q[3]);
32
+ return l < EPS ? [0, 0, 0, 1] : [q[0] / l, q[1] / l, q[2] / l, q[3] / l];
33
+ },
34
+ /** Rotation of `angle` radians around a unit `axis`. */
35
+ fromAxisAngle: (axis, angle) => {
36
+ const s = Math.sin(angle / 2);
37
+ return [axis[0] * s, axis[1] * s, axis[2] * s, Math.cos(angle / 2)];
38
+ },
39
+ /** Rotation matrix, row-major 3x3. */
40
+ toMatrix: (q) => {
41
+ const [x, y, z, w] = q;
42
+ 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;
43
+ 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)];
44
+ },
45
+ /** Quaternion from three orthonormal columns (the images of the x, y and z axes). */
46
+ fromColumns: (cx, cy, cz) => {
47
+ const m00 = cx[0], m10 = cx[1], m20 = cx[2];
48
+ const m01 = cy[0], m11 = cy[1], m21 = cy[2];
49
+ const m02 = cz[0], m12 = cz[1], m22 = cz[2];
50
+ const tr = m00 + m11 + m22;
51
+ let q;
52
+ if (tr > 0) {
53
+ const s = Math.sqrt(tr + 1) * 2;
54
+ q = [(m21 - m12) / s, (m02 - m20) / s, (m10 - m01) / s, 0.25 * s];
55
+ } else if (m00 > m11 && m00 > m22) {
56
+ const s = Math.sqrt(1 + m00 - m11 - m22) * 2;
57
+ q = [0.25 * s, (m01 + m10) / s, (m02 + m20) / s, (m21 - m12) / s];
58
+ } else if (m11 > m22) {
59
+ const s = Math.sqrt(1 + m11 - m00 - m22) * 2;
60
+ q = [(m01 + m10) / s, 0.25 * s, (m12 + m21) / s, (m02 - m20) / s];
61
+ } else {
62
+ const s = Math.sqrt(1 + m22 - m00 - m11) * 2;
63
+ q = [(m02 + m20) / s, (m12 + m21) / s, 0.25 * s, (m10 - m01) / s];
64
+ }
65
+ return quat.normalize(q);
66
+ }
67
+ };
68
+ var quatFromEuler = (x, y, z) => {
69
+ const c1 = Math.cos(x / 2), c2 = Math.cos(y / 2), c3 = Math.cos(z / 2);
70
+ const s1 = Math.sin(x / 2), s2 = Math.sin(y / 2), s3 = Math.sin(z / 2);
71
+ 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];
72
+ };
73
+ var rotateVec = (q, v) => {
74
+ const qx = q[0], qy = q[1], qz = q[2], qw = q[3];
75
+ const tx = 2 * (qy * v[2] - qz * v[1]);
76
+ const ty = 2 * (qz * v[0] - qx * v[2]);
77
+ const tz = 2 * (qx * v[1] - qy * v[0]);
78
+ return [v[0] + qw * tx + (qy * tz - qz * ty), v[1] + qw * ty + (qz * tx - qx * tz), v[2] + qw * tz + (qx * ty - qy * tx)];
79
+ };
80
+ var toLocal = (position, rotation, p) => rotateVec([-rotation[0], -rotation[1], -rotation[2], rotation[3]], [p[0] - position[0], p[1] - position[1], p[2] - position[2]]);
81
+ var toWorld = (position, rotation, p) => {
82
+ const r = rotateVec(rotation, p);
83
+ return [r[0] + position[0], r[1] + position[1], r[2] + position[2]];
84
+ };
85
+ var shapeDims = (s) => {
86
+ if (s.kind === "box") return [s.size[0], s.size[1], s.size[2]];
87
+ if (s.kind === "cylinder") return [s.radius * 2, s.height, s.radius * 2];
88
+ return [s.radius * 2, s.radius * 2, s.radius * 2];
89
+ };
90
+ var shapeBound = (s) => {
91
+ if (s.kind === "box") return 0.5 * Math.hypot(s.size[0], s.size[1], s.size[2]);
92
+ if (s.kind === "cylinder") return Math.hypot(s.radius, s.height / 2);
93
+ return s.radius;
94
+ };
95
+ var shapeArea = (s) => {
96
+ if (s.kind === "box") return 2 * (s.size[0] * s.size[1] + s.size[1] * s.size[2] + s.size[0] * s.size[2]);
97
+ if (s.kind === "cylinder") return 2 * Math.PI * s.radius * s.height + 2 * Math.PI * s.radius * s.radius;
98
+ return 4 * Math.PI * s.radius * s.radius;
99
+ };
100
+ var rayShape = (s, o, d) => {
101
+ if (s.kind === "box") {
102
+ let tmin = -Infinity, tmax = Infinity;
103
+ let a0 = 0, a1 = 0, s0 = 1, s1 = 1;
104
+ for (let i = 0; i < 3; i++) {
105
+ const h = s.size[i] / 2;
106
+ if (Math.abs(d[i]) < EPS) {
107
+ if (Math.abs(o[i]) > h) return null;
108
+ continue;
109
+ }
110
+ let ta = (-h - o[i]) / d[i];
111
+ let tb = (h - o[i]) / d[i];
112
+ let sa = -1, sb = 1;
113
+ if (ta > tb) {
114
+ const t = ta;
115
+ ta = tb;
116
+ tb = t;
117
+ sa = 1;
118
+ sb = -1;
119
+ }
120
+ if (ta > tmin) {
121
+ tmin = ta;
122
+ a0 = i;
123
+ s0 = sa;
124
+ }
125
+ if (tb < tmax) {
126
+ tmax = tb;
127
+ a1 = i;
128
+ s1 = sb;
129
+ }
130
+ if (tmin > tmax) return null;
131
+ }
132
+ if (tmax < 0 || !Number.isFinite(tmin) || !Number.isFinite(tmax)) return null;
133
+ const n02 = [0, 0, 0];
134
+ const n12 = [0, 0, 0];
135
+ n02[a0] = s0;
136
+ n12[a1] = s1;
137
+ return { t0: tmin, n0: n02, t1: tmax, n1: n12 };
138
+ }
139
+ if (s.kind === "sphere") {
140
+ const b = vec.dot(o, d);
141
+ const a2 = vec.dot(d, d);
142
+ const c2 = vec.dot(o, o) - s.radius * s.radius;
143
+ const disc = b * b - a2 * c2;
144
+ if (disc < 0) return null;
145
+ const sq = Math.sqrt(disc);
146
+ const t02 = (-b - sq) / a2;
147
+ const t12 = (-b + sq) / a2;
148
+ if (t12 < 0) return null;
149
+ const p0 = vec.madd(o, d, t02);
150
+ const p1 = vec.madd(o, d, t12);
151
+ return { t0: t02, n0: vec.normalize(p0), t1: t12, n1: vec.normalize(p1) };
152
+ }
153
+ const r = s.radius;
154
+ const hh = s.height / 2;
155
+ const a = d[0] * d[0] + d[2] * d[2];
156
+ const c = o[0] * o[0] + o[2] * o[2] - r * r;
157
+ let tA = -Infinity, tB = Infinity;
158
+ if (a > EPS) {
159
+ const b = o[0] * d[0] + o[2] * d[2];
160
+ const disc = b * b - a * c;
161
+ if (disc < 0) return null;
162
+ const sq = Math.sqrt(disc);
163
+ tA = (-b - sq) / a;
164
+ tB = (-b + sq) / a;
165
+ } else if (c > 0) return null;
166
+ let yA = -Infinity, yB = Infinity;
167
+ if (Math.abs(d[1]) > EPS) {
168
+ yA = (-hh - o[1]) / d[1];
169
+ yB = (hh - o[1]) / d[1];
170
+ if (yA > yB) {
171
+ const t = yA;
172
+ yA = yB;
173
+ yB = t;
174
+ }
175
+ } else if (Math.abs(o[1]) > hh) return null;
176
+ const t0 = Math.max(tA, yA);
177
+ const t1 = Math.min(tB, yB);
178
+ if (t0 > t1 || t1 < 0 || !Number.isFinite(t0) || !Number.isFinite(t1)) return null;
179
+ const normalAt = (t, fromSide, sign) => {
180
+ if (fromSide) {
181
+ const p = vec.madd(o, d, t);
182
+ return vec.normalize([p[0], 0, p[2]], [1, 0, 0]);
183
+ }
184
+ return [0, sign, 0];
185
+ };
186
+ const n0 = normalAt(t0, tA >= yA, d[1] > 0 ? -1 : 1);
187
+ const n1 = normalAt(t1, tB <= yB, d[1] > 0 ? 1 : -1);
188
+ return { t0, n0, t1, n1 };
189
+ };
190
+ var distanceToShape = (s, p) => {
191
+ if (s.kind === "box") {
192
+ const dx = Math.max(Math.abs(p[0]) - s.size[0] / 2, 0);
193
+ const dy2 = Math.max(Math.abs(p[1]) - s.size[1] / 2, 0);
194
+ const dz = Math.max(Math.abs(p[2]) - s.size[2] / 2, 0);
195
+ return Math.hypot(dx, dy2, dz);
196
+ }
197
+ if (s.kind === "sphere") return Math.max(0, vec.length(p) - s.radius);
198
+ const dr = Math.max(0, Math.hypot(p[0], p[2]) - s.radius);
199
+ const dy = Math.max(0, Math.abs(p[1]) - s.height / 2);
200
+ return Math.hypot(dr, dy);
201
+ };
202
+ var surfacePoint = (s, p) => {
203
+ if (s.kind === "box") {
204
+ const h = [s.size[0] / 2, s.size[1] / 2, s.size[2] / 2];
205
+ const inside2 = Math.abs(p[0]) <= h[0] && Math.abs(p[1]) <= h[1] && Math.abs(p[2]) <= h[2];
206
+ if (!inside2) {
207
+ const c = [Math.max(-h[0], Math.min(h[0], p[0])), Math.max(-h[1], Math.min(h[1], p[1])), Math.max(-h[2], Math.min(h[2], p[2]))];
208
+ return { point: c, normal: vec.normalize(vec.sub(p, c)) };
209
+ }
210
+ let axis = 0;
211
+ let best = Infinity;
212
+ for (let i = 0; i < 3; i++) {
213
+ const g = h[i] - Math.abs(p[i]);
214
+ if (g < best) {
215
+ best = g;
216
+ axis = i;
217
+ }
218
+ }
219
+ const sign2 = p[axis] >= 0 ? 1 : -1;
220
+ const point = [p[0], p[1], p[2]];
221
+ point[axis] = sign2 * h[axis];
222
+ const normal = [0, 0, 0];
223
+ normal[axis] = sign2;
224
+ return { point, normal };
225
+ }
226
+ if (s.kind === "sphere") {
227
+ const n = vec.normalize(p);
228
+ return { point: vec.scale(n, s.radius), normal: n };
229
+ }
230
+ const rho = Math.hypot(p[0], p[2]);
231
+ const hh = s.height / 2;
232
+ const inside = rho <= s.radius && Math.abs(p[1]) <= hh;
233
+ if (!inside) {
234
+ const k = rho > s.radius ? s.radius / rho : 1;
235
+ const c = [p[0] * k, Math.max(-hh, Math.min(hh, p[1])), p[2] * k];
236
+ return { point: c, normal: vec.normalize(vec.sub(p, c)) };
237
+ }
238
+ if (s.radius - rho < hh - Math.abs(p[1])) {
239
+ const k = rho < EPS ? 0 : s.radius / rho;
240
+ const point = rho < EPS ? [s.radius, p[1], 0] : [p[0] * k, p[1], p[2] * k];
241
+ return { point, normal: vec.normalize([point[0], 0, point[2]], [1, 0, 0]) };
242
+ }
243
+ const sign = p[1] >= 0 ? 1 : -1;
244
+ return { point: [p[0], sign * hh, p[2]], normal: [0, sign, 0] };
245
+ };
246
+ var aabbOf = (s, center, q) => {
247
+ let hx, hy, hz;
248
+ if (s.kind === "sphere") {
249
+ hx = hy = hz = s.radius;
250
+ } else {
251
+ const m = quat.toMatrix(q);
252
+ if (s.kind === "box") {
253
+ const a = s.size[0] / 2, b = s.size[1] / 2, c = s.size[2] / 2;
254
+ hx = Math.abs(m[0]) * a + Math.abs(m[1]) * b + Math.abs(m[2]) * c;
255
+ hy = Math.abs(m[3]) * a + Math.abs(m[4]) * b + Math.abs(m[5]) * c;
256
+ hz = Math.abs(m[6]) * a + Math.abs(m[7]) * b + Math.abs(m[8]) * c;
257
+ } else {
258
+ const ax = [m[1], m[4], m[7]];
259
+ const hh = s.height / 2;
260
+ const e = (i) => s.radius * Math.sqrt(Math.max(0, 1 - ax[i] * ax[i])) + hh * Math.abs(ax[i]);
261
+ hx = e(0);
262
+ hy = e(1);
263
+ hz = e(2);
264
+ }
265
+ }
266
+ return { min: [center[0] - hx, center[1] - hy, center[2] - hz], max: [center[0] + hx, center[1] + hy, center[2] + hz] };
267
+ };
268
+ var createRng = (seed) => {
269
+ let s = seed | 0;
270
+ return {
271
+ next() {
272
+ s = s + 1831565813 | 0;
273
+ let t = Math.imul(s ^ s >>> 15, 1 | s);
274
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
275
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
276
+ },
277
+ state: () => s,
278
+ set(v) {
279
+ s = v | 0;
280
+ }
281
+ };
282
+ };
283
+
284
+ // src/fracture.ts
285
+ var bsp = (root, count, axes, rnd) => {
286
+ const pieces = [root];
287
+ const vol = (b) => axes.reduce((v, a) => v * b.s[a], 1);
288
+ while (pieces.length < count) {
289
+ let bi = 0;
290
+ for (let i = 1; i < pieces.length; i++) if (vol(pieces[i]) > vol(pieces[bi])) bi = i;
291
+ const b = pieces[bi];
292
+ let ax = axes[0];
293
+ for (const a of axes) if (b.s[a] > b.s[ax]) ax = a;
294
+ if (b.s[ax] < 1e-4) break;
295
+ const r = 0.3 + 0.4 * rnd();
296
+ const l1 = b.s[ax] * r;
297
+ const l2 = b.s[ax] - l1;
298
+ const c1 = [b.c[0], b.c[1], b.c[2]];
299
+ const c2 = [b.c[0], b.c[1], b.c[2]];
300
+ c1[ax] = b.c[ax] - b.s[ax] / 2 + l1 / 2;
301
+ c2[ax] = b.c[ax] + b.s[ax] / 2 - l2 / 2;
302
+ const s1 = [b.s[0], b.s[1], b.s[2]];
303
+ const s2 = [b.s[0], b.s[1], b.s[2]];
304
+ s1[ax] = l1;
305
+ s2[ax] = l2;
306
+ pieces[bi] = { c: c1, s: s1 };
307
+ pieces.push({ c: c2, s: s2 });
308
+ }
309
+ return pieces;
310
+ };
311
+ var parity = (p) => {
312
+ let inv = 0;
313
+ for (let i = 0; i < 3; i++) for (let j = i + 1; j < 3; j++) if (p[i] > p[j]) inv++;
314
+ return inv % 2 === 0 ? 1 : -1;
315
+ };
316
+ var frame = (ax, ay, az, angle = 0) => {
317
+ const e = (i) => [i === 0 ? 1 : 0, i === 1 ? 1 : 0, i === 2 ? 1 : 0];
318
+ const z = e(az);
319
+ const sg = parity([ax, ay, az]);
320
+ const q = quat.fromColumns(e(ax), e(ay), [z[0] * sg, z[1] * sg, z[2] * sg]);
321
+ return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
322
+ };
323
+ var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
324
+ var planFragments = (inp) => {
325
+ const { style, dims, rnd, maxFragments } = inp;
326
+ const [fmin, fmax] = inp.fragmentSize;
327
+ const mean = (fmin + fmax) / 2;
328
+ const root = { c: [0, 0, 0], s: [dims[0], dims[1], dims[2]] };
329
+ const idq = quat.identity();
330
+ const out = [];
331
+ const sorted = [0, 1, 2].sort((a, b) => dims[a] - dims[b]);
332
+ const t = sorted[0];
333
+ const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
334
+ if (style === "snap") {
335
+ const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
336
+ for (const b of bsp(root, n2, [0, 1, 2], rnd)) out.push({ center: b.c, size: b.s, q: idq, shape: "slab", weight: b.s[0] * b.s[1] * b.s[2] });
337
+ return out;
338
+ }
339
+ if (style === "crumble" || style === "none") {
340
+ const n2 = clamp(Math.ceil(dims[0] * dims[1] * dims[2] / (mean * mean * mean)), 4, maxFragments);
341
+ for (const b of bsp(root, n2, [0, 1, 2], rnd)) out.push({ center: b.c, size: b.s, q: idq, shape: "chunk", weight: b.s[0] * b.s[1] * b.s[2] });
342
+ return out;
343
+ }
344
+ if (style === "splinter") {
345
+ const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
346
+ const [a, b] = [0, 1, 2].filter((i) => i !== g);
347
+ const w = Math.max(fmin * 1.5, 0.01);
348
+ const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(maxFragments / 2)));
349
+ const maxLen = fmax * 4;
350
+ const cells = bsp(root, c, [a, b], rnd);
351
+ const pieces = [];
352
+ for (const cell of cells) {
353
+ let list = [cell];
354
+ const extra = rnd() < 0.4 ? 1 : 0;
355
+ const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
356
+ list = bsp(cell, k, [g], rnd);
357
+ for (const p of list) pieces.push(p);
358
+ }
359
+ for (const p of pieces.slice(0, maxFragments)) {
360
+ out.push({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] });
361
+ }
362
+ return out;
363
+ }
364
+ if (style === "tear") {
365
+ const n2 = clamp(Math.round(dims[u] * dims[v] / (mean * mean)), 3, maxFragments);
366
+ const th2 = Math.min(inp.thickness, dims[t]);
367
+ for (const b of bsp(root, n2, [u, v], rnd)) {
368
+ out.push({ center: [b.c[0], b.c[1], b.c[2]], size: [b.s[u], b.s[v], th2], q: frame(u, v, t), shape: "scrap", weight: b.s[u] * b.s[v] });
369
+ }
370
+ return out;
371
+ }
372
+ const th = Math.min(inp.thickness, dims[t]);
373
+ const Hu = dims[u] / 2, Hv = dims[v] / 2;
374
+ const hu = clamp(inp.hit[u], -Hu * 0.95, Hu * 0.95);
375
+ const hv = clamp(inp.hit[v], -Hv * 0.95, Hv * 0.95);
376
+ const R = Math.max(Math.hypot(Hu + Math.abs(hu), Hv + Math.abs(hv)), 0.02);
377
+ const n = clamp(Math.round(dims[u] * dims[v] / (mean * mean)), 6, maxFragments);
378
+ const sectors = clamp(Math.round(Math.sqrt(n * 1.6)), 5, 10);
379
+ const rings = Math.max(1, Math.ceil(n / sectors));
380
+ const radii = [];
381
+ for (let k = 0; k <= rings; k++) radii.push(R * Math.pow(k / rings, 1.3));
382
+ const cap = fmax * 1.5;
383
+ for (let j = 0; j < sectors && out.length < maxFragments; j++) {
384
+ const theta = (j + (rnd() - 0.5) * 0.4) * (Math.PI * 2 / sectors);
385
+ for (let k = 0; k < rings && out.length < maxFragments; k++) {
386
+ const rm = (radii[k] + radii[k + 1]) / 2;
387
+ const cu = hu + rm * Math.cos(theta);
388
+ const cv = hv + rm * Math.sin(theta);
389
+ if (Math.abs(cu) > Hu + 0.01 || Math.abs(cv) > Hv + 0.01) continue;
390
+ const radial = clamp(radii[k + 1] - radii[k], 0.01, cap);
391
+ const tang = clamp(radii[k + 1] * (Math.PI * 2 / sectors) * 0.9, 0.01, cap);
392
+ const c = [0, 0, 0];
393
+ c[u] = clamp(cu, -Hu * 0.98, Hu * 0.98);
394
+ c[v] = clamp(cv, -Hv * 0.98, Hv * 0.98);
395
+ c[t] = 0;
396
+ out.push({ center: c, size: [radial, tang, th], q: frame(u, v, t, theta), shape: "shard", weight: radial * tang });
397
+ }
398
+ }
399
+ if (out.length === 0) out.push({ center: [0, 0, 0], size: [dims[u], dims[v], th], q: frame(u, v, t), shape: "shard", weight: 1 });
400
+ return out;
401
+ };
402
+
403
+ // src/data/materials.json
404
+ var materials_default = [
405
+ {
406
+ id: "steel",
407
+ density: 7850,
408
+ hpPerKg: 400,
409
+ penetration: 110,
410
+ ductility: 0.9,
411
+ fracture: "none",
412
+ damageScale: {
413
+ slash: 0.1,
414
+ blunt: 0.8
415
+ },
416
+ hitDebris: "sparks",
417
+ breakDebris: "sparks",
418
+ ricochetDeg: 20,
419
+ thermal: {
420
+ meltC: 1450,
421
+ meltRate: 0.05,
422
+ conductivity: 0.5,
423
+ heatCapacity: 3
424
+ },
425
+ visual: {
426
+ glows: true
427
+ },
428
+ tags: [
429
+ "metal",
430
+ "conductive"
431
+ ],
432
+ compressive: 2e6
433
+ },
434
+ {
435
+ id: "cast_iron",
436
+ density: 7200,
437
+ hpPerKg: 150,
438
+ penetration: 160,
439
+ ductility: 0.2,
440
+ fracture: "snap",
441
+ fragmentSize: [
442
+ 0.08,
443
+ 0.3
444
+ ],
445
+ damageScale: {
446
+ slash: 0.1,
447
+ blunt: 1.2
448
+ },
449
+ hitDebris: "sparks",
450
+ breakDebris: "chips",
451
+ ricochetDeg: 15,
452
+ thermal: {
453
+ meltC: 1200,
454
+ meltRate: 0.05,
455
+ conductivity: 0.45,
456
+ heatCapacity: 3
457
+ },
458
+ visual: {
459
+ glows: true
460
+ },
461
+ tags: [
462
+ "metal",
463
+ "conductive"
464
+ ],
465
+ compressive: 15e5
466
+ },
467
+ {
468
+ id: "aluminium",
469
+ density: 2700,
470
+ hpPerKg: 250,
471
+ penetration: 60,
472
+ ductility: 0.85,
473
+ fracture: "none",
474
+ damageScale: {
475
+ slash: 0.2
476
+ },
477
+ hitDebris: "sparks",
478
+ breakDebris: "sparks",
479
+ ricochetDeg: 15,
480
+ thermal: {
481
+ meltC: 660,
482
+ meltRate: 0.06,
483
+ conductivity: 0.8,
484
+ heatCapacity: 1.5
485
+ },
486
+ visual: {
487
+ glows: true
488
+ },
489
+ tags: [
490
+ "metal",
491
+ "conductive"
492
+ ],
493
+ compressive: 8e5
494
+ },
495
+ {
496
+ id: "copper",
497
+ density: 8960,
498
+ hpPerKg: 300,
499
+ penetration: 70,
500
+ ductility: 0.95,
501
+ fracture: "none",
502
+ damageScale: {
503
+ slash: 0.2
504
+ },
505
+ hitDebris: "sparks",
506
+ breakDebris: "sparks",
507
+ ricochetDeg: 15,
508
+ thermal: {
509
+ meltC: 1085,
510
+ meltRate: 0.05,
511
+ conductivity: 1,
512
+ heatCapacity: 2.5
513
+ },
514
+ visual: {
515
+ glows: true
516
+ },
517
+ tags: [
518
+ "metal",
519
+ "conductive"
520
+ ],
521
+ compressive: 1e6
522
+ },
523
+ {
524
+ id: "wood_soft",
525
+ density: 500,
526
+ hpPerKg: 90,
527
+ penetration: 9,
528
+ ductility: 0.35,
529
+ fracture: "splinter",
530
+ fragmentSize: [
531
+ 0.03,
532
+ 0.15
533
+ ],
534
+ damageScale: {
535
+ blunt: 1.2,
536
+ slash: 1.5
537
+ },
538
+ hitDebris: "splinters",
539
+ breakDebris: "splinters",
540
+ thermal: {
541
+ ignitionC: 300,
542
+ flameC: 800,
543
+ fuelFraction: 0.9,
544
+ burnRate: 0.012,
545
+ conductivity: 0.05,
546
+ heatCapacity: 1.2,
547
+ smoke: "grey"
548
+ },
549
+ visual: {
550
+ charColor: 1709072
551
+ },
552
+ tags: [
553
+ "wood",
554
+ "organic",
555
+ "flammable"
556
+ ],
557
+ compressive: 1e5
558
+ },
559
+ {
560
+ id: "wood_hard",
561
+ density: 750,
562
+ hpPerKg: 140,
563
+ penetration: 14,
564
+ ductility: 0.3,
565
+ fracture: "splinter",
566
+ fragmentSize: [
567
+ 0.03,
568
+ 0.2
569
+ ],
570
+ damageScale: {
571
+ slash: 1.2
572
+ },
573
+ hitDebris: "splinters",
574
+ breakDebris: "splinters",
575
+ thermal: {
576
+ ignitionC: 330,
577
+ flameC: 800,
578
+ fuelFraction: 0.9,
579
+ burnRate: 0.01,
580
+ conductivity: 0.06,
581
+ heatCapacity: 1.4,
582
+ smoke: "grey"
583
+ },
584
+ visual: {
585
+ charColor: 1709072
586
+ },
587
+ tags: [
588
+ "wood",
589
+ "organic",
590
+ "flammable"
591
+ ],
592
+ compressive: 15e4
593
+ },
594
+ {
595
+ id: "plywood",
596
+ density: 600,
597
+ hpPerKg: 70,
598
+ penetration: 7,
599
+ ductility: 0.3,
600
+ fracture: "snap",
601
+ fragmentSize: [
602
+ 0.1,
603
+ 0.4
604
+ ],
605
+ damageScale: {
606
+ slash: 1.3
607
+ },
608
+ hitDebris: "splinters",
609
+ breakDebris: "splinters",
610
+ thermal: {
611
+ ignitionC: 300,
612
+ flameC: 800,
613
+ fuelFraction: 0.85,
614
+ burnRate: 0.013,
615
+ conductivity: 0.05,
616
+ heatCapacity: 1.2,
617
+ smoke: "grey"
618
+ },
619
+ visual: {
620
+ charColor: 1709072
621
+ },
622
+ tags: [
623
+ "wood",
624
+ "organic",
625
+ "flammable"
626
+ ],
627
+ compressive: 6e4
628
+ },
629
+ {
630
+ id: "mdf",
631
+ density: 750,
632
+ hpPerKg: 45,
633
+ penetration: 6,
634
+ ductility: 0.15,
635
+ fracture: "snap",
636
+ fragmentSize: [
637
+ 0.08,
638
+ 0.35
639
+ ],
640
+ hitDebris: "chips",
641
+ breakDebris: "dust",
642
+ thermal: {
643
+ ignitionC: 290,
644
+ flameC: 750,
645
+ fuelFraction: 0.85,
646
+ burnRate: 0.014,
647
+ conductivity: 0.05,
648
+ heatCapacity: 1.2,
649
+ smoke: "grey"
650
+ },
651
+ visual: {
652
+ charColor: 1709072
653
+ },
654
+ tags: [
655
+ "wood",
656
+ "organic",
657
+ "flammable"
658
+ ],
659
+ compressive: 3e4
660
+ },
661
+ {
662
+ id: "glass",
663
+ density: 2500,
664
+ hpPerKg: 12,
665
+ penetration: 6,
666
+ ductility: 0.02,
667
+ fracture: "shatter",
668
+ fragmentSize: [
669
+ 0.05,
670
+ 0.25
671
+ ],
672
+ damageScale: {
673
+ blunt: 1.5
674
+ },
675
+ hitDebris: "shards",
676
+ breakDebris: "shards",
677
+ ricochetDeg: 8,
678
+ thermal: {
679
+ meltC: 1400,
680
+ meltRate: 0.03,
681
+ conductivity: 0.1,
682
+ heatCapacity: 1.5,
683
+ thermalShockC: 150
684
+ },
685
+ visual: {
686
+ glows: true,
687
+ transparent: true
688
+ },
689
+ tags: [
690
+ "glass",
691
+ "brittle"
692
+ ],
693
+ compressive: 1e5
694
+ },
695
+ {
696
+ id: "tempered_glass",
697
+ density: 2500,
698
+ hpPerKg: 30,
699
+ penetration: 9,
700
+ ductility: 0.02,
701
+ fracture: "crumble",
702
+ fragmentSize: [
703
+ 6e-3,
704
+ 0.02
705
+ ],
706
+ hitDebris: "shards",
707
+ breakDebris: "shards",
708
+ ricochetDeg: 8,
709
+ thermal: {
710
+ meltC: 1400,
711
+ meltRate: 0.03,
712
+ conductivity: 0.1,
713
+ heatCapacity: 1.5,
714
+ thermalShockC: 250
715
+ },
716
+ visual: {
717
+ glows: true,
718
+ transparent: true
719
+ },
720
+ tags: [
721
+ "glass",
722
+ "brittle"
723
+ ],
724
+ compressive: 2e5
725
+ },
726
+ {
727
+ id: "ceramic",
728
+ density: 2400,
729
+ hpPerKg: 25,
730
+ penetration: 12,
731
+ ductility: 0.02,
732
+ fracture: "shatter",
733
+ fragmentSize: [
734
+ 0.03,
735
+ 0.12
736
+ ],
737
+ damageScale: {
738
+ blunt: 1.3
739
+ },
740
+ hitDebris: "chips",
741
+ breakDebris: "shards",
742
+ thermal: {
743
+ conductivity: 0.1,
744
+ heatCapacity: 1.5,
745
+ thermalShockC: 200
746
+ },
747
+ tags: [
748
+ "ceramic",
749
+ "brittle"
750
+ ],
751
+ compressive: 15e4
752
+ },
753
+ {
754
+ id: "concrete",
755
+ density: 2300,
756
+ hpPerKg: 14,
757
+ penetration: 18,
758
+ ductility: 0.03,
759
+ fracture: "crumble",
760
+ fragmentSize: [
761
+ 0.05,
762
+ 0.25
763
+ ],
764
+ damageScale: {
765
+ explosive: 1.2
766
+ },
767
+ hitDebris: "chips",
768
+ breakDebris: "dust",
769
+ thermal: {
770
+ conductivity: 0.15,
771
+ heatCapacity: 2.5
772
+ },
773
+ tags: [
774
+ "masonry",
775
+ "brittle"
776
+ ],
777
+ compressive: 444e3
778
+ },
779
+ {
780
+ id: "brick",
781
+ density: 1900,
782
+ hpPerKg: 12,
783
+ penetration: 14,
784
+ ductility: 0.05,
785
+ fracture: "crumble",
786
+ fragmentSize: [
787
+ 0.04,
788
+ 0.12
789
+ ],
790
+ hitDebris: "chips",
791
+ breakDebris: "dust",
792
+ thermal: {
793
+ conductivity: 0.15,
794
+ heatCapacity: 2.5
795
+ },
796
+ tags: [
797
+ "masonry",
798
+ "brittle"
799
+ ],
800
+ compressive: 4e4
801
+ },
802
+ {
803
+ id: "plaster",
804
+ density: 850,
805
+ hpPerKg: 18,
806
+ penetration: 3,
807
+ ductility: 0.05,
808
+ fracture: "crumble",
809
+ fragmentSize: [
810
+ 0.05,
811
+ 0.3
812
+ ],
813
+ hitDebris: "dust",
814
+ breakDebris: "dust",
815
+ thermal: {
816
+ conductivity: 0.1,
817
+ heatCapacity: 1.5
818
+ },
819
+ tags: [
820
+ "masonry",
821
+ "brittle"
822
+ ],
823
+ compressive: 8e3
824
+ },
825
+ {
826
+ id: "stone",
827
+ density: 2700,
828
+ hpPerKg: 25,
829
+ penetration: 25,
830
+ ductility: 0.03,
831
+ fracture: "crumble",
832
+ fragmentSize: [
833
+ 0.06,
834
+ 0.3
835
+ ],
836
+ hitDebris: "chips",
837
+ breakDebris: "dust",
838
+ thermal: {
839
+ conductivity: 0.2,
840
+ heatCapacity: 2.5
841
+ },
842
+ tags: [
843
+ "masonry",
844
+ "brittle"
845
+ ],
846
+ compressive: 3e5
847
+ },
848
+ {
849
+ id: "plastic",
850
+ density: 1100,
851
+ hpPerKg: 60,
852
+ penetration: 8,
853
+ ductility: 0.5,
854
+ fracture: "snap",
855
+ fragmentSize: [
856
+ 0.05,
857
+ 0.3
858
+ ],
859
+ hitDebris: "chips",
860
+ breakDebris: "chips",
861
+ thermal: {
862
+ ignitionC: 400,
863
+ flameC: 700,
864
+ fuelFraction: 0.8,
865
+ burnRate: 0.02,
866
+ meltC: 200,
867
+ meltRate: 0.06,
868
+ conductivity: 0.05,
869
+ heatCapacity: 1,
870
+ smoke: "black"
871
+ },
872
+ visual: {
873
+ charColor: 855309
874
+ },
875
+ tags: [
876
+ "plastic",
877
+ "flammable"
878
+ ],
879
+ compressive: 4e4
880
+ },
881
+ {
882
+ id: "rubber",
883
+ density: 1100,
884
+ hpPerKg: 80,
885
+ penetration: 12,
886
+ ductility: 0.9,
887
+ fracture: "tear",
888
+ fragmentSize: [
889
+ 0.05,
890
+ 0.2
891
+ ],
892
+ hitDebris: "chips",
893
+ breakDebris: "fibers",
894
+ thermal: {
895
+ ignitionC: 350,
896
+ flameC: 750,
897
+ fuelFraction: 0.6,
898
+ burnRate: 0.015,
899
+ conductivity: 0.05,
900
+ heatCapacity: 1.2,
901
+ smoke: "black"
902
+ },
903
+ visual: {
904
+ charColor: 855309
905
+ },
906
+ tags: [
907
+ "rubber",
908
+ "flammable"
909
+ ],
910
+ compressive: 2e4
911
+ },
912
+ {
913
+ id: "fabric",
914
+ density: 400,
915
+ hpPerKg: 20,
916
+ penetration: 0.8,
917
+ ductility: 0.8,
918
+ fracture: "tear",
919
+ fragmentSize: [
920
+ 0.05,
921
+ 0.25
922
+ ],
923
+ damageScale: {
924
+ slash: 4,
925
+ ballistic: 0.5
926
+ },
927
+ hitDebris: "fibers",
928
+ breakDebris: "fibers",
929
+ thermal: {
930
+ ignitionC: 230,
931
+ flameC: 600,
932
+ fuelFraction: 1,
933
+ burnRate: 0.05,
934
+ conductivity: 0.03,
935
+ heatCapacity: 0.6,
936
+ smoke: "grey"
937
+ },
938
+ visual: {
939
+ charColor: 1709072
940
+ },
941
+ tags: [
942
+ "fabric",
943
+ "organic",
944
+ "flammable"
945
+ ],
946
+ compressive: 500
947
+ },
948
+ {
949
+ id: "foam",
950
+ density: 30,
951
+ hpPerKg: 15,
952
+ penetration: 0.5,
953
+ ductility: 0.7,
954
+ fracture: "tear",
955
+ fragmentSize: [
956
+ 0.03,
957
+ 0.15
958
+ ],
959
+ damageScale: {
960
+ slash: 2,
961
+ ballistic: 0.4
962
+ },
963
+ hitDebris: "fibers",
964
+ breakDebris: "fibers",
965
+ thermal: {
966
+ ignitionC: 250,
967
+ flameC: 900,
968
+ fuelFraction: 0.95,
969
+ burnRate: 0.09,
970
+ conductivity: 0.02,
971
+ heatCapacity: 0.5,
972
+ smoke: "black"
973
+ },
974
+ visual: {
975
+ charColor: 855309
976
+ },
977
+ tags: [
978
+ "fabric",
979
+ "flammable"
980
+ ],
981
+ compressive: 100
982
+ },
983
+ {
984
+ id: "leather",
985
+ density: 900,
986
+ hpPerKg: 50,
987
+ penetration: 4,
988
+ ductility: 0.8,
989
+ fracture: "tear",
990
+ fragmentSize: [
991
+ 0.05,
992
+ 0.25
993
+ ],
994
+ damageScale: {
995
+ slash: 2
996
+ },
997
+ hitDebris: "fibers",
998
+ breakDebris: "fibers",
999
+ thermal: {
1000
+ ignitionC: 320,
1001
+ flameC: 650,
1002
+ fuelFraction: 0.7,
1003
+ burnRate: 0.01,
1004
+ conductivity: 0.05,
1005
+ heatCapacity: 1,
1006
+ smoke: "grey"
1007
+ },
1008
+ visual: {
1009
+ charColor: 1709072
1010
+ },
1011
+ tags: [
1012
+ "fabric",
1013
+ "organic",
1014
+ "flammable"
1015
+ ],
1016
+ compressive: 2e3
1017
+ },
1018
+ {
1019
+ id: "paper",
1020
+ density: 700,
1021
+ hpPerKg: 8,
1022
+ penetration: 0.3,
1023
+ ductility: 0.5,
1024
+ fracture: "tear",
1025
+ fragmentSize: [
1026
+ 0.03,
1027
+ 0.15
1028
+ ],
1029
+ damageScale: {
1030
+ slash: 3
1031
+ },
1032
+ hitDebris: "fibers",
1033
+ breakDebris: "fibers",
1034
+ thermal: {
1035
+ ignitionC: 230,
1036
+ flameC: 600,
1037
+ fuelFraction: 1,
1038
+ burnRate: 0.06,
1039
+ conductivity: 0.03,
1040
+ heatCapacity: 0.5,
1041
+ smoke: "grey"
1042
+ },
1043
+ visual: {
1044
+ charColor: 1709072
1045
+ },
1046
+ tags: [
1047
+ "fabric",
1048
+ "organic",
1049
+ "flammable"
1050
+ ],
1051
+ compressive: 500
1052
+ },
1053
+ {
1054
+ id: "foliage",
1055
+ density: 150,
1056
+ hpPerKg: 20,
1057
+ penetration: 0.5,
1058
+ ductility: 0.6,
1059
+ fracture: "tear",
1060
+ fragmentSize: [
1061
+ 0.03,
1062
+ 0.1
1063
+ ],
1064
+ damageScale: {
1065
+ slash: 4
1066
+ },
1067
+ hitDebris: "leaves",
1068
+ breakDebris: "leaves",
1069
+ thermal: {
1070
+ ignitionC: 300,
1071
+ flameC: 700,
1072
+ fuelFraction: 0.8,
1073
+ burnRate: 0.08,
1074
+ conductivity: 0.03,
1075
+ heatCapacity: 0.8,
1076
+ smoke: "white"
1077
+ },
1078
+ visual: {
1079
+ charColor: 1709072
1080
+ },
1081
+ tags: [
1082
+ "plant",
1083
+ "organic",
1084
+ "flammable"
1085
+ ],
1086
+ compressive: 500
1087
+ },
1088
+ {
1089
+ id: "green_wood",
1090
+ density: 650,
1091
+ hpPerKg: 5,
1092
+ penetration: 10,
1093
+ ductility: 0.5,
1094
+ fracture: "splinter",
1095
+ fragmentSize: [
1096
+ 0.03,
1097
+ 0.2
1098
+ ],
1099
+ damageScale: {
1100
+ slash: 1.3,
1101
+ ballistic: 0.1
1102
+ },
1103
+ hitDebris: "splinters",
1104
+ breakDebris: "leaves",
1105
+ thermal: {
1106
+ ignitionC: 400,
1107
+ flameC: 700,
1108
+ fuelFraction: 0.8,
1109
+ burnRate: 6e-3,
1110
+ conductivity: 0.06,
1111
+ heatCapacity: 2,
1112
+ smoke: "white"
1113
+ },
1114
+ visual: {
1115
+ charColor: 1709072
1116
+ },
1117
+ tags: [
1118
+ "plant",
1119
+ "wood",
1120
+ "organic",
1121
+ "flammable"
1122
+ ],
1123
+ compressive: 6e4
1124
+ },
1125
+ {
1126
+ id: "cactus",
1127
+ density: 900,
1128
+ hpPerKg: 30,
1129
+ penetration: 2,
1130
+ ductility: 0.6,
1131
+ fracture: "tear",
1132
+ fragmentSize: [
1133
+ 0.04,
1134
+ 0.15
1135
+ ],
1136
+ damageScale: {
1137
+ slash: 3
1138
+ },
1139
+ hitDebris: "fibers",
1140
+ breakDebris: "fibers",
1141
+ thermal: {
1142
+ ignitionC: 450,
1143
+ flameC: 650,
1144
+ fuelFraction: 0.3,
1145
+ burnRate: 0.01,
1146
+ conductivity: 0.05,
1147
+ heatCapacity: 3,
1148
+ smoke: "white"
1149
+ },
1150
+ visual: {
1151
+ charColor: 1709072
1152
+ },
1153
+ tags: [
1154
+ "plant",
1155
+ "organic",
1156
+ "flammable"
1157
+ ],
1158
+ compressive: 5e3
1159
+ },
1160
+ {
1161
+ id: "flesh_fruit",
1162
+ density: 950,
1163
+ hpPerKg: 15,
1164
+ penetration: 1.5,
1165
+ ductility: 0.7,
1166
+ fracture: "tear",
1167
+ fragmentSize: [
1168
+ 0.02,
1169
+ 0.08
1170
+ ],
1171
+ damageScale: {
1172
+ slash: 2
1173
+ },
1174
+ hitDebris: "drips",
1175
+ breakDebris: "drips",
1176
+ thermal: {
1177
+ conductivity: 0.05,
1178
+ heatCapacity: 3
1179
+ },
1180
+ tags: [
1181
+ "plant",
1182
+ "organic"
1183
+ ],
1184
+ compressive: 1e3
1185
+ },
1186
+ {
1187
+ id: "wax",
1188
+ density: 900,
1189
+ hpPerKg: 30,
1190
+ penetration: 3,
1191
+ ductility: 0.5,
1192
+ fracture: "snap",
1193
+ fragmentSize: [
1194
+ 0.05,
1195
+ 0.2
1196
+ ],
1197
+ hitDebris: "chips",
1198
+ breakDebris: "chips",
1199
+ thermal: {
1200
+ ignitionC: 230,
1201
+ flameC: 700,
1202
+ fuelFraction: 0.9,
1203
+ burnRate: 0.02,
1204
+ meltC: 60,
1205
+ meltRate: 0.2,
1206
+ conductivity: 0.05,
1207
+ heatCapacity: 0.8,
1208
+ smoke: "grey"
1209
+ },
1210
+ tags: [
1211
+ "wax",
1212
+ "flammable"
1213
+ ],
1214
+ compressive: 5e3
1215
+ },
1216
+ {
1217
+ id: "soil",
1218
+ density: 1300,
1219
+ hpPerKg: 3,
1220
+ penetration: 5,
1221
+ ductility: 0.3,
1222
+ fracture: "crumble",
1223
+ fragmentSize: [
1224
+ 0.03,
1225
+ 0.12
1226
+ ],
1227
+ hitDebris: "dust",
1228
+ breakDebris: "dust",
1229
+ thermal: {
1230
+ conductivity: 0.1,
1231
+ heatCapacity: 2.5
1232
+ },
1233
+ tags: [
1234
+ "soil",
1235
+ "masonry"
1236
+ ],
1237
+ compressive: 2e4
1238
+ }
1239
+ ];
1240
+
1241
+ // src/data/material-hints.json
1242
+ var material_hints_default = {
1243
+ "cast iron": "cast_iron",
1244
+ cast_iron: "cast_iron",
1245
+ castiron: "cast_iron",
1246
+ metal: "steel",
1247
+ steel: "steel",
1248
+ iron: "steel",
1249
+ chrome: "steel",
1250
+ alumin: "aluminium",
1251
+ alu_: "aluminium",
1252
+ "alu-": "aluminium",
1253
+ "alu ": "aluminium",
1254
+ copper: "copper",
1255
+ brass: "copper",
1256
+ bronze: "copper",
1257
+ wood: "wood_soft",
1258
+ timber: "wood_soft",
1259
+ plank: "wood_soft",
1260
+ lumber: "wood_soft",
1261
+ pine: "wood_soft",
1262
+ hardwood: "wood_hard",
1263
+ oak: "wood_hard",
1264
+ plywood: "plywood",
1265
+ mdf: "mdf",
1266
+ chipboard: "mdf",
1267
+ glass: "glass",
1268
+ window: "glass",
1269
+ mirror: "glass",
1270
+ tempered: "tempered_glass",
1271
+ windshield: "tempered_glass",
1272
+ fabric: "fabric",
1273
+ cloth: "fabric",
1274
+ cushion: "fabric",
1275
+ sofa: "fabric",
1276
+ curtain: "fabric",
1277
+ carpet: "fabric",
1278
+ textile: "fabric",
1279
+ canvas: "fabric",
1280
+ foam: "foam",
1281
+ sponge: "foam",
1282
+ mattress: "foam",
1283
+ leather: "leather",
1284
+ paper: "paper",
1285
+ cardboard: "paper",
1286
+ leaf: "foliage",
1287
+ leaves: "foliage",
1288
+ foliage: "foliage",
1289
+ bush: "foliage",
1290
+ grass: "foliage",
1291
+ trunk: "green_wood",
1292
+ branch: "green_wood",
1293
+ twig: "green_wood",
1294
+ cactus: "cactus",
1295
+ fruit: "flesh_fruit",
1296
+ apple: "flesh_fruit",
1297
+ melon: "flesh_fruit",
1298
+ wax: "wax",
1299
+ candle: "wax",
1300
+ concrete: "concrete",
1301
+ cement: "concrete",
1302
+ brick: "brick",
1303
+ tile: "ceramic",
1304
+ ceramic: "ceramic",
1305
+ porcelain: "ceramic",
1306
+ stone: "stone",
1307
+ rock: "stone",
1308
+ marble: "stone",
1309
+ plaster: "plaster",
1310
+ drywall: "plaster",
1311
+ gypsum: "plaster",
1312
+ plastic: "plastic",
1313
+ pvc: "plastic",
1314
+ acrylic: "plastic",
1315
+ rubber: "rubber",
1316
+ tire: "rubber",
1317
+ tyre: "rubber",
1318
+ soil: "soil",
1319
+ dirt: "soil",
1320
+ potting: "soil",
1321
+ mud: "soil"
1322
+ };
1323
+
1324
+ // src/materials.ts
1325
+ var MATERIALS = materials_default;
1326
+ var JOINT_DEFAULTS = {
1327
+ screw: { strength: 80, tools: ["screwdriver"], seconds: 2.5 },
1328
+ bolt: { strength: 200, tools: ["wrench"], seconds: 4 },
1329
+ nail: { strength: 60, tools: ["pry_bar", "hammer"], seconds: 2 },
1330
+ weld: { strength: 600, tools: ["cutting_torch", "saw"], seconds: 8 },
1331
+ glue: { strength: 50, tools: ["pry_bar", "knife"], seconds: 3 },
1332
+ hinge: { strength: 120, tools: ["screwdriver"], seconds: 3 },
1333
+ clip: { strength: 20, tools: ["hand"], seconds: 0.6 },
1334
+ tie: { strength: 15, tools: ["knife", "hand"], seconds: 1 },
1335
+ mortar: { strength: 400, tools: ["hammer"], seconds: 6 },
1336
+ fused: { strength: 1e3, tools: [], seconds: 0 },
1337
+ stem: { strength: 40, tools: ["knife", "saw", "hand"], seconds: 1.5 },
1338
+ rebar: { strength: 1500, tools: ["cutting_torch", "saw"], seconds: 20 },
1339
+ resting: { strength: 30, tools: ["hand"], seconds: 1 }
1340
+ };
1341
+ var has = (m, tag) => !!m.tags && m.tags.includes(tag);
1342
+ var JOINT_BY_MATERIALS = (a, b) => {
1343
+ const any = (tag) => has(a, tag) || has(b, tag);
1344
+ const both = (tag) => has(a, tag) && has(b, tag);
1345
+ if (any("glass")) return any("plastic") ? "clip" : "glue";
1346
+ if (any("fabric")) return "tie";
1347
+ if (both("plant")) return "stem";
1348
+ if (any("plant")) return "tie";
1349
+ if (both("masonry")) return "mortar";
1350
+ if (any("masonry") && any("metal")) return "bolt";
1351
+ if (both("metal")) return "weld";
1352
+ if (any("plastic")) return "clip";
1353
+ if (any("rubber") || any("wax")) return "glue";
1354
+ return "screw";
1355
+ };
1356
+ var MATERIAL_MAP = new Map(MATERIALS.map((m) => [m.id, m]));
1357
+ var HINTS = Object.entries(material_hints_default).sort((x, y) => y[0].length - x[0].length);
1358
+ var guessMaterial = (name) => {
1359
+ const n = name.toLowerCase();
1360
+ for (const [key, id] of HINTS) if (n.includes(key)) return id;
1361
+ return void 0;
1362
+ };
1363
+ var partVolume = (shape) => {
1364
+ if (shape.kind === "box") return shape.size[0] * shape.size[1] * shape.size[2];
1365
+ if (shape.kind === "cylinder") return Math.PI * shape.radius * shape.radius * shape.height;
1366
+ return 4 / 3 * Math.PI * shape.radius ** 3;
1367
+ };
1368
+ var partMass = (part, material) => part.mass ?? material.density * partVolume(part.shape) * (part.fill ?? 1);
1369
+ var partThickness = (part) => {
1370
+ const dims = shapeDims(part.shape);
1371
+ const min = Math.min(dims[0], dims[1], dims[2]);
1372
+ return Math.max(1e-4, Math.min(part.thickness ?? min * (part.fill ?? 1), min));
1373
+ };
1374
+ var inferJoints = (parts, tolerance = 0.01, materials = MATERIAL_MAP) => {
1375
+ const boxes = parts.map((p) => aabbOf(p.shape, p.position, quatFromEuler(...p.rotation ?? [0, 0, 0])));
1376
+ const out = [];
1377
+ for (let i = 0; i < parts.length; i++) {
1378
+ for (let j = i + 1; j < parts.length; j++) {
1379
+ const a = boxes[i], b = boxes[j];
1380
+ let touch = true;
1381
+ 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;
1382
+ if (!touch) continue;
1383
+ const ma = materials.get(parts[i].material);
1384
+ const mb = materials.get(parts[j].material);
1385
+ let kind = ma && mb ? JOINT_BY_MATERIALS(ma, mb) : "screw";
1386
+ const thin = Math.min(partThickness(parts[i]), partThickness(parts[j]));
1387
+ if (kind === "weld" && thin >= 0.02) kind = "bolt";
1388
+ if (kind === "screw" && ma && mb && has(ma, "wood") && has(mb, "wood") && thin < 0.02) kind = "nail";
1389
+ out.push({ id: `${parts[i].id}~${parts[j].id}`, a: parts[i].id, b: parts[j].id, kind });
1390
+ }
1391
+ }
1392
+ return out;
1393
+ };
1394
+ var partCapacity = (part, material) => {
1395
+ if (part.structure?.capacity !== void 0) return part.structure.capacity;
1396
+ const d = shapeDims(part.shape).sort((a, b) => a - b);
1397
+ const role = part.structure?.role;
1398
+ const sh = shapeDims(part.shape);
1399
+ const section = role === "wall" || role === "partition" ? Math.min(sh[0], sh[2]) * Math.max(sh[0], sh[2]) : role === "slab" ? d[0] * d[2] : d[0] * d[1];
1400
+ return Math.max(1, section * (material.compressive ?? 2e4) * (role === "foundation" ? 10 : 1));
1401
+ };
1402
+ var fragility = (ductility, thickness) => {
1403
+ if (ductility >= 0.3) return 1;
1404
+ return (1 + 4 * (0.3 - ductility) / 0.3) * Math.min(25, Math.max(1, (0.02 / Math.max(thickness, 1e-4)) ** 2));
1405
+ };
1406
+
1407
+ // src/world.ts
1408
+ var DMG_PER_J = 0.02;
1409
+ var BLAST_COUPLING = 0.1;
1410
+ var FLAMMABLE_FLUIDS = ["gas", "fuel"];
1411
+ var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
1412
+ var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
1413
+ var clone = (v) => structuredClone(v);
1414
+ var updateBound = (o) => {
1415
+ if (!o.parts.length) return;
1416
+ const c = [0, 0, 0];
1417
+ for (const p of o.parts) for (let k = 0; k < 3; k++) c[k] += p.pos[k] / o.parts.length;
1418
+ let r = 0;
1419
+ for (const p of o.parts) r = Math.max(r, vec.distance(c, p.pos) + p.bound);
1420
+ o.bc = c;
1421
+ o.br = r;
1422
+ };
1423
+ var AXIS = { x: 0, y: 1, z: 2 };
1424
+ var createDestruction = (options = {}) => {
1425
+ const materials = /* @__PURE__ */ new Map();
1426
+ for (const m of MATERIALS) materials.set(m.id, m);
1427
+ for (const m of options.materials ?? []) materials.set(m.id, m);
1428
+ const ambient = options.ambientC ?? 20;
1429
+ const maxFragments = options.maxFragments ?? 24;
1430
+ const maxDents = options.maxDents ?? 24;
1431
+ const maxHoles = options.maxHoles ?? 32;
1432
+ const fireSpread = options.fireSpread ?? 0.6;
1433
+ const rng = createRng(options.seed ?? 1);
1434
+ const rnd = () => rng.next();
1435
+ let time = 0;
1436
+ let counter = 0;
1437
+ const objects = /* @__PURE__ */ new Map();
1438
+ const listeners = /* @__PURE__ */ new Map();
1439
+ let cur = [];
1440
+ const blasts = [];
1441
+ const impls = /* @__PURE__ */ new Map();
1442
+ const taken = (id) => objects.has(id) || impls.has(id);
1443
+ const emit = (e) => {
1444
+ cur.push(e);
1445
+ options.onEvent?.(e);
1446
+ const all = listeners.get("*");
1447
+ if (all) for (const fn of [...all]) fn(e);
1448
+ const typed = listeners.get(e.type);
1449
+ if (typed) for (const fn of [...typed]) fn(e);
1450
+ };
1451
+ const base = (o, source) => source === void 0 ? { object: o.id, time } : { object: o.id, time, source };
1452
+ const pendingCollapse = /* @__PURE__ */ new Map();
1453
+ let runDepth = 0;
1454
+ const run = (fn) => {
1455
+ const prev = cur;
1456
+ const mine = [];
1457
+ cur = mine;
1458
+ runDepth++;
1459
+ try {
1460
+ const value = fn();
1461
+ let guard = 0;
1462
+ while (blasts.length && guard++ < 64) {
1463
+ const b = blasts.shift();
1464
+ blastWorld(b.point, b.energy, b.radius, b.source);
1465
+ }
1466
+ if (runDepth === 1) flushCollapse();
1467
+ return { value, events: mine };
1468
+ } finally {
1469
+ runDepth--;
1470
+ cur = prev;
1471
+ }
1472
+ };
1473
+ const nextId = () => {
1474
+ let id;
1475
+ do
1476
+ id = `obj_${++counter}`;
1477
+ while (taken(id));
1478
+ return id;
1479
+ };
1480
+ const freshRuntime = () => ({ meltEmitted: 0, dripT: 0, prevTemp: ambient, shockT: 0, jet: false });
1481
+ const resolveSpec = (spec) => {
1482
+ const out = clone(spec);
1483
+ const joints = out.joints ?? inferJoints(out.parts, 0.01, materials);
1484
+ const structural = (id) => {
1485
+ const role = out.parts.find((p) => p.id === id)?.structure?.role;
1486
+ return role !== void 0 && role !== "none";
1487
+ };
1488
+ out.joints = joints.map((j) => {
1489
+ const def = JOINT_DEFAULTS[j.kind];
1490
+ return {
1491
+ id: j.id ?? `${j.a}~${j.b}`,
1492
+ a: j.a,
1493
+ b: j.b,
1494
+ kind: j.kind,
1495
+ strength: j.strength ?? def.strength,
1496
+ tools: j.tools ?? [...def.tools],
1497
+ seconds: j.seconds ?? def.seconds,
1498
+ bears: j.bears ?? (structural(j.a) && structural(j.b))
1499
+ };
1500
+ });
1501
+ return out;
1502
+ };
1503
+ const build = (spec, id, pos, rot, carry) => {
1504
+ const st = { id, position: [pos[0], pos[1], pos[2]], rotation: [rot[0], rot[1], rot[2], rot[3]], parts: [], joints: [], groups: [], destroyed: false };
1505
+ if (spec.name !== void 0) st.name = spec.name;
1506
+ const o = {
1507
+ id,
1508
+ spec,
1509
+ st,
1510
+ parts: [],
1511
+ byId: /* @__PURE__ */ new Map(),
1512
+ idx: /* @__PURE__ */ new Map(),
1513
+ joints: [],
1514
+ jointMap: /* @__PURE__ */ new Map(),
1515
+ byPart: /* @__PURE__ */ new Map(),
1516
+ bc: [0, 0, 0],
1517
+ br: 0,
1518
+ active: false,
1519
+ tmp: [],
1520
+ hasStructure: false,
1521
+ dirty: false,
1522
+ maxStress: 0,
1523
+ collapsed: new Set(carry?.collapsed ?? []),
1524
+ unsup: [],
1525
+ anyAnchor: false
1526
+ };
1527
+ for (const ps of spec.parts) {
1528
+ if (o.byId.has(ps.id)) throw new Error(`duplicate part id "${ps.id}"`);
1529
+ const mat = materials.get(ps.material);
1530
+ if (!mat) throw new Error(`unknown material "${ps.material}" on part "${ps.id}"`);
1531
+ const mass0 = partMass(ps, mat);
1532
+ const maxHp = Math.max(1, ps.hp ?? mass0 * mat.hpPerKg);
1533
+ const fuel0 = mat.thermal.ignitionC !== void 0 ? mass0 * (mat.thermal.fuelFraction ?? 0) : 0;
1534
+ const c = carry?.parts.get(ps.id);
1535
+ const pst = c?.st ?? {
1536
+ id: ps.id,
1537
+ material: ps.material,
1538
+ status: "intact",
1539
+ hp: maxHp,
1540
+ maxHp,
1541
+ mass: mass0,
1542
+ temperature: ambient,
1543
+ burning: false,
1544
+ fuel: fuel0,
1545
+ char: 0,
1546
+ melt: 0,
1547
+ dents: [],
1548
+ holes: [],
1549
+ cracks: [],
1550
+ group: 0
1551
+ };
1552
+ if (!c && ps.contains) pst.liters = ps.contains.liters;
1553
+ const p = {
1554
+ spec: ps,
1555
+ mat,
1556
+ st: pst,
1557
+ q: quatFromEuler(...ps.rotation ?? [0, 0, 0]),
1558
+ pos: [ps.position[0], ps.position[1], ps.position[2]],
1559
+ mass0,
1560
+ fuel0,
1561
+ area: shapeArea(ps.shape),
1562
+ thickness: partThickness(ps),
1563
+ dims: shapeDims(ps.shape),
1564
+ bound: shapeBound(ps.shape),
1565
+ rt: c?.rt ?? { ...freshRuntime(), prevTemp: pst.temperature },
1566
+ structural: !!ps.structure && ps.structure.role !== "none",
1567
+ anchored: !!ps.anchored || ps.structure?.role === "foundation",
1568
+ cap: ps.structure ? partCapacity(ps, mat) : 0
1569
+ };
1570
+ if (p.structural) o.hasStructure = true;
1571
+ o.idx.set(ps.id, o.parts.length);
1572
+ o.parts.push(p);
1573
+ o.byId.set(ps.id, p);
1574
+ o.st.parts.push(pst);
1575
+ o.byPart.set(ps.id, []);
1576
+ }
1577
+ for (const js of spec.joints ?? []) {
1578
+ const r = js;
1579
+ const ia = o.idx.get(r.a), ib = o.idx.get(r.b);
1580
+ if (ia === void 0 || ib === void 0) throw new Error(`joint "${r.id}" references an unknown part`);
1581
+ if (o.jointMap.has(r.id)) throw new Error(`duplicate joint id "${r.id}"`);
1582
+ const pa = o.parts[ia], pb = o.parts[ib];
1583
+ const A = aabbOf(pa.spec.shape, pa.spec.position, pa.q);
1584
+ const B = aabbOf(pb.spec.shape, pb.spec.position, pb.q);
1585
+ const pos2 = [0, 0, 0];
1586
+ for (let k = 0; k < 3; k++) pos2[k] = (Math.max(A.min[k], B.min[k]) + Math.min(A.max[k], B.max[k])) / 2;
1587
+ const bears = r.bears ?? (pa.structural && pb.structural);
1588
+ const c = carry?.joints.get(r.id);
1589
+ const jst = c?.st ?? { id: r.id, a: r.a, b: r.b, kind: r.kind, hp: r.strength, maxHp: r.strength, progress: 0, open: false };
1590
+ const j = { spec: r, st: jst, ia, ib, bears, pos: pos2, step: c?.step ?? 0 };
1591
+ o.joints.push(j);
1592
+ o.jointMap.set(r.id, j);
1593
+ o.byPart.get(r.a).push(j);
1594
+ o.byPart.get(r.b).push(j);
1595
+ o.st.joints.push(jst);
1596
+ }
1597
+ for (const [jid, c] of carry?.joints ?? []) if (c.st.open && !o.jointMap.has(jid)) o.st.joints.push(c.st);
1598
+ updateBound(o);
1599
+ o.anyAnchor = o.parts.some((p) => p.anchored);
1600
+ o.unsup = o.parts.map(() => false);
1601
+ solveStructure(o);
1602
+ recomputeGroups(o);
1603
+ o.st.destroyed = o.parts.length > 0 && o.parts.every(isLost);
1604
+ o.active = o.maxStress > 0.8 || o.parts.some((p) => p.st.burning || p.st.temperature > ambient + 0.5 || (p.st.liters ?? 0) > 0);
1605
+ return o;
1606
+ };
1607
+ const recomputeGroups = (o) => {
1608
+ const n = o.parts.length;
1609
+ const parent = Array.from({ length: n }, (_, i) => i);
1610
+ const find = (i) => {
1611
+ while (parent[i] !== i) {
1612
+ parent[i] = parent[parent[i]];
1613
+ i = parent[i];
1614
+ }
1615
+ return i;
1616
+ };
1617
+ const union = (a, b) => {
1618
+ parent[find(a)] = find(b);
1619
+ };
1620
+ const live = (j) => !j.st.open && !isLost(o.parts[j.ia]) && !isLost(o.parts[j.ib]);
1621
+ if (o.unsup.some(Boolean)) {
1622
+ for (const j of o.joints) if (live(j) && !o.unsup[j.ia] && !o.unsup[j.ib]) union(j.ia, j.ib);
1623
+ const supported = /* @__PURE__ */ new Set();
1624
+ for (let i = 0; i < n; i++) if (!isLost(o.parts[i]) && !o.unsup[i] && (o.parts[i].structural || o.parts[i].anchored)) supported.add(find(i));
1625
+ for (const j of o.joints) {
1626
+ if (!live(j) || !(o.unsup[j.ia] || o.unsup[j.ib])) continue;
1627
+ if (o.unsup[j.ia] && o.unsup[j.ib]) union(j.ia, j.ib);
1628
+ else {
1629
+ const u = o.unsup[j.ia] ? j.ia : j.ib;
1630
+ const other = u === j.ia ? j.ib : j.ia;
1631
+ if (!o.parts[other].structural && !supported.has(find(other))) union(u, other);
1632
+ }
1633
+ }
1634
+ } else for (const j of o.joints) if (live(j)) union(j.ia, j.ib);
1635
+ const comps = /* @__PURE__ */ new Map();
1636
+ for (let i = 0; i < n; i++) {
1637
+ if (isLost(o.parts[i])) continue;
1638
+ const r = find(i);
1639
+ const l = comps.get(r);
1640
+ if (l) l.push(i);
1641
+ else comps.set(r, [i]);
1642
+ }
1643
+ const list = [...comps.values()].map((ix) => ({
1644
+ ix,
1645
+ anchored: ix.some((i) => o.parts[i].anchored),
1646
+ mass: ix.reduce((s, i) => s + o.parts[i].st.mass, 0)
1647
+ }));
1648
+ list.sort((a, b) => a.anchored === b.anchored ? b.mass - a.mass || a.ix[0] - b.ix[0] : a.anchored ? -1 : 1);
1649
+ o.st.groups = list.map((g) => g.ix.map((i) => o.parts[i].spec.id));
1650
+ for (const p of o.parts) if (isLost(p)) p.st.group = -1;
1651
+ list.forEach((g, gi) => g.ix.forEach((i) => o.parts[i].st.group = gi));
1652
+ };
1653
+ const restsOn = (j, r) => {
1654
+ const b = j.bears;
1655
+ if (!b) return -1;
1656
+ if (b === true) return j.ia === r ? j.ib : j.ia;
1657
+ if (b === "a_on_b") return j.ia === r ? j.ib : -1;
1658
+ return j.ib === r ? j.ia : -1;
1659
+ };
1660
+ const solveStructure = (o) => {
1661
+ o.maxStress = 0;
1662
+ if (!o.hasStructure) return;
1663
+ const parts = o.parts;
1664
+ const n = parts.length;
1665
+ const dist = new Array(n).fill(-1);
1666
+ const queue = [];
1667
+ const ok = (i) => parts[i].structural && !isLost(parts[i]);
1668
+ const isPartition = (i) => parts[i].spec.structure?.role === "partition";
1669
+ for (let i = 0; i < n; i++) if (ok(i) && parts[i].anchored) {
1670
+ dist[i] = 0;
1671
+ queue.push(i);
1672
+ }
1673
+ for (let h = 0; h < queue.length; h++) {
1674
+ const s = queue[h];
1675
+ if (isPartition(s)) continue;
1676
+ for (const j of o.byPart.get(parts[s].spec.id)) {
1677
+ if (j.st.open || !j.bears) continue;
1678
+ const r = j.ia === s ? j.ib : j.ia;
1679
+ if (dist[r] >= 0 || !ok(r) || restsOn(j, r) !== s) continue;
1680
+ dist[r] = dist[s] + 1;
1681
+ queue.push(r);
1682
+ }
1683
+ }
1684
+ const incoming = new Array(n).fill(0);
1685
+ for (let h = queue.length - 1; h >= 0; h--) {
1686
+ const i = queue[h];
1687
+ const total = parts[i].st.mass + incoming[i];
1688
+ parts[i].st.load = total;
1689
+ const lower = [];
1690
+ for (const j of o.byPart.get(parts[i].spec.id)) {
1691
+ if (j.st.open || !j.bears) continue;
1692
+ const k = restsOn(j, i);
1693
+ if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
1694
+ }
1695
+ for (const k of lower) incoming[k] += total / lower.length;
1696
+ }
1697
+ for (let i = 0; i < n; i++) {
1698
+ const p = parts[i];
1699
+ o.unsup[i] = false;
1700
+ if (!p.structural) continue;
1701
+ if (dist[i] < 0 || isLost(p)) {
1702
+ p.st.load = p.st.mass;
1703
+ p.st.stress = 0;
1704
+ o.unsup[i] = o.anyAnchor && !isLost(p);
1705
+ continue;
1706
+ }
1707
+ p.st.stress = p.st.load / p.cap;
1708
+ if (p.st.stress > o.maxStress) o.maxStress = p.st.stress;
1709
+ }
1710
+ if (o.maxStress > 0.8) o.active = true;
1711
+ };
1712
+ const collapseCheck = (o, src) => {
1713
+ if (!o.hasStructure || !o.parts.some((p) => p.anchored)) return;
1714
+ if (!pendingCollapse.has(o)) pendingCollapse.set(o, src);
1715
+ };
1716
+ const flushCollapse = () => {
1717
+ for (const [o, src] of [...pendingCollapse]) {
1718
+ pendingCollapse.delete(o);
1719
+ if (!objects.has(o.id) || objects.get(o.id) !== o) continue;
1720
+ for (const g of o.st.groups) {
1721
+ const ps = g.map((id) => o.byId.get(id)).filter((p) => !isLost(p) && !o.collapsed.has(p.spec.id));
1722
+ if (ps.length === 0 || g.some((id) => o.byId.get(id).anchored) || !ps.some((p) => p.structural)) continue;
1723
+ for (const p of ps) o.collapsed.add(p.spec.id);
1724
+ emit({ ...base(o, src), type: "collapse", parts: ps.map((p) => p.spec.id), mass: ps.reduce((sum, p) => sum + p.st.mass, 0) });
1725
+ }
1726
+ }
1727
+ };
1728
+ const getObj = (id) => {
1729
+ const o = objects.get(id);
1730
+ if (!o) throw new Error(`unknown object "${id}"`);
1731
+ return o;
1732
+ };
1733
+ const getPart = (o, id) => {
1734
+ const p = o.byId.get(id);
1735
+ if (!p) throw new Error(`unknown part "${id}" in object "${o.id}"`);
1736
+ return p;
1737
+ };
1738
+ const partXf = (o, p) => ({ c: toWorld(o.st.position, o.st.rotation, p.pos), q: quat.mul(o.st.rotation, p.q) });
1739
+ const objLocal = (o, w) => toLocal(o.st.position, o.st.rotation, w);
1740
+ const objLocalDir = (o, w) => rotateVec(quat.conjugate(o.st.rotation), w);
1741
+ const jointWorld = (o, j) => toWorld(o.st.position, o.st.rotation, j.pos);
1742
+ const distToPart = (o, p, w) => {
1743
+ const { c, q } = partXf(o, p);
1744
+ return distanceToShape(p.spec.shape, toLocal(c, q, w));
1745
+ };
1746
+ const partWorldAabb = (o, p) => {
1747
+ const { c, q } = partXf(o, p);
1748
+ return aabbOf(p.spec.shape, c, q);
1749
+ };
1750
+ const rayPartRaw = (pos, rot, spec, pq, origin, d, lp = spec.position) => {
1751
+ const c = toWorld(pos, rot, lp);
1752
+ const q = quat.mul(rot, pq);
1753
+ const qi = quat.conjugate(q);
1754
+ const r = rayShape(spec.shape, rotateVec(qi, vec.sub(origin, c)), rotateVec(qi, d));
1755
+ return r ? { t0: r.t0, t1: r.t1, n: rotateVec(q, r.n0) } : null;
1756
+ };
1757
+ const rayPart = (o, p, origin, d) => rayPartRaw(o.st.position, o.st.rotation, p.spec, p.q, origin, d, p.pos);
1758
+ const raySphereMiss = (origin, d, c, r, maxDist) => {
1759
+ const oc = vec.sub(origin, c);
1760
+ const b = vec.dot(oc, d);
1761
+ const cc = vec.dot(oc, oc) - r * r;
1762
+ if (cc > 0 && b > 0) return true;
1763
+ const disc = b * b - cc;
1764
+ if (disc < 0) return true;
1765
+ return -b - Math.sqrt(disc) > maxDist;
1766
+ };
1767
+ const raycastInternal = (origin, dir, maxDist, ignore) => {
1768
+ const d = vec.normalize(dir);
1769
+ let best = null;
1770
+ for (const o of objects.values()) {
1771
+ if (raySphereMiss(origin, d, toWorld(o.st.position, o.st.rotation, o.bc), o.br, best ? Math.min(maxDist, best.distance) : maxDist)) continue;
1772
+ for (const p of o.parts) {
1773
+ if (isLost(p) || ignore?.has(`${o.id}\0${p.spec.id}`)) continue;
1774
+ const r = rayPart(o, p, origin, d);
1775
+ if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
1776
+ if (!best || r.t0 < best.distance) best = { object: o.id, part: p.spec.id, point: vec.madd(origin, d, r.t0), normal: r.n, distance: r.t0, pathLen: r.t1 - r.t0 };
1777
+ }
1778
+ }
1779
+ for (const im of impls.values()) {
1780
+ for (const ps of im.impl.describe().parts) {
1781
+ if (ignore?.has(`${im.impl.id}\0${ps.id}`)) continue;
1782
+ const ov = im.poses.get(ps.id);
1783
+ const r = rayPartRaw(im.position, im.rotation, ps, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]), origin, d, ov?.pos);
1784
+ if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
1785
+ if (!best || r.t0 < best.distance) best = { object: im.impl.id, part: ps.id, point: vec.madd(origin, d, r.t0), normal: r.n, distance: r.t0, pathLen: r.t1 - r.t0 };
1786
+ }
1787
+ }
1788
+ return best;
1789
+ };
1790
+ const implHit = (im, hit) => {
1791
+ let h = hit;
1792
+ if (hit.space === "local") {
1793
+ h = { ...hit, space: "world", point: toWorld(im.position, im.rotation, hit.point) };
1794
+ if (hit.direction) h.direction = rotateVec(im.rotation, hit.direction);
1795
+ }
1796
+ const start = cur.length;
1797
+ for (const e of im.impl.hit(h) ?? []) emit(e.time === void 0 ? { ...e, time } : e);
1798
+ const mine = cur.slice(start);
1799
+ const res = { object: im.impl.id, damage: 0, penetrated: false, exitEnergy: 0, events: mine, point: hit.point };
1800
+ for (const e of mine) {
1801
+ if (e.type === "damage") res.damage += e.amount;
1802
+ if (e.type === "hole" && e.hole.through) res.penetrated = true;
1803
+ if (res.part === void 0 && "part" in e) res.part = e.part;
1804
+ }
1805
+ return res;
1806
+ };
1807
+ const implJointPos = (im, a, b) => {
1808
+ const parts = im.impl.describe().parts;
1809
+ const pa = parts.find((p) => p.id === a), pb = parts.find((p) => p.id === b);
1810
+ if (!pa || !pb) return null;
1811
+ const A = aabbOf(pa.shape, pa.position, quatFromEuler(...pa.rotation ?? [0, 0, 0]));
1812
+ const B = aabbOf(pb.shape, pb.position, quatFromEuler(...pb.rotation ?? [0, 0, 0]));
1813
+ const pos = [0, 0, 0];
1814
+ for (let k = 0; k < 3; k++) pos[k] = (Math.max(A.min[k], B.min[k]) + Math.min(A.max[k], B.max[k])) / 2;
1815
+ return toWorld(im.position, im.rotation, pos);
1816
+ };
1817
+ const locate = (o, point, dir, tol, only) => {
1818
+ const cands = only ? [only] : o.parts;
1819
+ if (dir && vec.length(dir) > 1e-9) {
1820
+ const d = vec.normalize(dir);
1821
+ const origin = vec.madd(point, d, -0.05);
1822
+ let best = null;
1823
+ let bestT = Infinity;
1824
+ let inside = null;
1825
+ for (const p of cands) {
1826
+ if (isLost(p)) continue;
1827
+ const r = rayPart(o, p, origin, d);
1828
+ if (!r) continue;
1829
+ if (r.t0 >= 0 && r.t0 <= 0.15) {
1830
+ if (r.t0 < bestT) {
1831
+ bestT = r.t0;
1832
+ best = { p, point: vec.madd(origin, d, r.t0), normal: r.n, dir: d, pathLen: Math.max(r.t1 - r.t0, 1e-4) };
1833
+ }
1834
+ } else if (r.t0 < 0 && r.t1 > 0 && !inside) {
1835
+ inside = { p, point, normal: vec.scale(d, -1), dir: d, pathLen: Math.max(r.t1 - 0.05, p.thickness) };
1836
+ }
1837
+ }
1838
+ if (best) return best;
1839
+ if (inside) return inside;
1840
+ }
1841
+ let np = null;
1842
+ let nd = Infinity;
1843
+ for (const p of cands) {
1844
+ if (isLost(p)) continue;
1845
+ const dd = distToPart(o, p, point);
1846
+ if (dd < nd) {
1847
+ nd = dd;
1848
+ np = p;
1849
+ }
1850
+ }
1851
+ if (!np || nd > tol) return null;
1852
+ const { c, q } = partXf(o, np);
1853
+ const sp = surfacePoint(np.spec.shape, toLocal(c, q, point));
1854
+ const normal = rotateVec(q, sp.normal);
1855
+ return { p: np, point: toWorld(c, q, sp.point), normal, dir: dir && vec.length(dir) > 1e-9 ? vec.normalize(dir) : vec.scale(normal, -1), pathLen: np.thickness };
1856
+ };
1857
+ const markDamaged = (p) => {
1858
+ if (p.st.status === "intact") p.st.status = "damaged";
1859
+ };
1860
+ const addDent = (o, p, wp, wn, radius, depth, src) => {
1861
+ const dent = { point: objLocal(o, wp), normal: objLocalDir(o, wn), radius, depth };
1862
+ const arr = p.st.dents;
1863
+ let ni = -1, nd = Infinity;
1864
+ for (let i = 0; i < arr.length; i++) {
1865
+ const dd = vec.distance(arr[i].point, dent.point);
1866
+ if (dd < nd) {
1867
+ nd = dd;
1868
+ ni = i;
1869
+ }
1870
+ }
1871
+ if (ni >= 0 && (arr.length >= maxDents || nd < arr[ni].radius * 0.6)) {
1872
+ arr[ni].depth = Math.min(0.1, Math.max(arr[ni].depth, depth) + depth * 0.3);
1873
+ arr[ni].radius = Math.max(arr[ni].radius, radius);
1874
+ } else arr.push(dent);
1875
+ markDamaged(p);
1876
+ emit({ ...base(o, src), type: "dent", part: p.spec.id, dent, worldPoint: wp });
1877
+ };
1878
+ const addHole = (o, p, wp, wn, diameter, through, exit, src) => {
1879
+ const hole = { point: objLocal(o, wp), normal: objLocalDir(o, wn), diameter, through };
1880
+ p.st.holes.push(hole);
1881
+ if (p.st.holes.length > maxHoles) p.st.holes.shift();
1882
+ markDamaged(p);
1883
+ emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {} });
1884
+ return hole;
1885
+ };
1886
+ const addCrack = (o, p, wp, wn, length, src) => {
1887
+ const crack = { point: objLocal(o, wp), normal: objLocalDir(o, wn), length: Math.min(length, Math.max(p.bound, 0.05)), rays: 5 + Math.floor(rnd() * 5) };
1888
+ p.st.cracks.push(crack);
1889
+ if (p.st.cracks.length > 16) p.st.cracks.shift();
1890
+ markDamaged(p);
1891
+ emit({ ...base(o, src), type: "crack", part: p.spec.id, crack, worldPoint: wp });
1892
+ };
1893
+ const debris = (o, p, kind, point, normal, count, speed, src) => emit({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed });
1894
+ const holeRate = (hole, pressurized) => (pressurized ? 0.5 : 0.1) * (hole.diameter / 9e-3);
1895
+ const detachEvents = (o, opened, by, src) => {
1896
+ for (const j of opened) emit({ ...base(o, src), type: "detach", joint: j.spec.id, parts: [j.spec.a, j.spec.b], by, groups: o.st.groups.map((g) => [...g]) });
1897
+ };
1898
+ const checkDestroyed = (o, src) => {
1899
+ if (!o.st.destroyed && o.parts.length > 0 && o.parts.every(isLost)) {
1900
+ o.st.destroyed = true;
1901
+ emit({ ...base(o, src), type: "destroyed" });
1902
+ }
1903
+ };
1904
+ const partLost = (o, p, src) => {
1905
+ const opened = [];
1906
+ for (const j of o.byPart.get(p.spec.id) ?? []) {
1907
+ if (j.st.open) continue;
1908
+ j.st.open = true;
1909
+ j.st.openedBy = "part_lost";
1910
+ opened.push(j);
1911
+ }
1912
+ solveStructure(o);
1913
+ recomputeGroups(o);
1914
+ detachEvents(o, opened, "part_lost", src);
1915
+ collapseCheck(o, src);
1916
+ checkDestroyed(o, src);
1917
+ };
1918
+ const jointReach = (o, j, point) => {
1919
+ const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
1920
+ let d2 = 0;
1921
+ for (let k = 0; k < 3; k++) {
1922
+ const lo = Math.min(Math.max(A.min[k], B.min[k]), Math.min(A.max[k], B.max[k]));
1923
+ const hi = Math.max(Math.max(A.min[k], B.min[k]), Math.min(A.max[k], B.max[k]));
1924
+ const d = point[k] < lo ? lo - point[k] : point[k] > hi ? point[k] - hi : 0;
1925
+ d2 += d * d;
1926
+ }
1927
+ return Math.sqrt(d2);
1928
+ };
1929
+ const jointReachLimit = (o, j) => {
1930
+ const pa = o.parts[j.ia], pb = o.parts[j.ib];
1931
+ const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
1932
+ return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
1933
+ };
1934
+ const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false) => {
1935
+ if (amount <= 0) return;
1936
+ const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
1937
+ if (split && mine.length) amount /= mine.length;
1938
+ for (const j of mine) {
1939
+ if (j.st.open) continue;
1940
+ if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
1941
+ j.st.hp -= amount;
1942
+ if (j.st.hp <= 0) {
1943
+ j.st.hp = 0;
1944
+ j.st.open = true;
1945
+ j.st.openedBy = "break";
1946
+ solveStructure(o);
1947
+ recomputeGroups(o);
1948
+ detachEvents(o, [j], "break", src);
1949
+ collapseCheck(o, src);
1950
+ }
1951
+ }
1952
+ };
1953
+ const failPart = (o, p, origin, dir, fragEnergy, cap, src) => {
1954
+ if (isLost(p)) return;
1955
+ const ex = p.spec.explodes;
1956
+ const style = p.mat.fracture === "none" ? "snap" : p.mat.fracture;
1957
+ const { c, q } = partXf(o, p);
1958
+ const hitW = origin ?? c;
1959
+ const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
1960
+ const plan = planFragments({
1961
+ style,
1962
+ dims: p.dims,
1963
+ thickness: p.thickness,
1964
+ grain,
1965
+ hit: toLocal(c, q, hitW),
1966
+ fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
1967
+ maxFragments,
1968
+ rnd
1969
+ });
1970
+ const mass = p.st.mass;
1971
+ const totW = plan.reduce((s, f) => s + f.weight, 0) || 1;
1972
+ const energy = ex ? ex.energy * 0.2 : fragEnergy;
1973
+ const speed = clamp2(Math.sqrt(2 * energy / Math.max(mass, 0.05)), 0.3, ex ? 40 : cap);
1974
+ const fragments = plan.map((f, i) => {
1975
+ const cw = toWorld(c, q, f.center);
1976
+ const out = vec.normalize(vec.sub(cw, hitW), [0, 1, 0]);
1977
+ const dv = dir ? vec.normalize(vec.madd(out, dir, 0.6)) : out;
1978
+ const k = speed * (0.6 + 0.8 * rnd());
1979
+ return {
1980
+ id: `${p.spec.id}#${i}`,
1981
+ part: p.spec.id,
1982
+ material: p.spec.material,
1983
+ shape: f.shape,
1984
+ center: cw,
1985
+ size: f.size,
1986
+ rotation: quat.mul(q, f.q),
1987
+ velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
1988
+ angularVelocity: [(rnd() - 0.5) * 12, (rnd() - 0.5) * 12, (rnd() - 0.5) * 12],
1989
+ mass: mass * f.weight / totW
1990
+ };
1991
+ });
1992
+ p.st.status = "broken";
1993
+ p.st.hp = 0;
1994
+ p.st.mass = 0;
1995
+ p.st.burning = false;
1996
+ p.rt.jet = false;
1997
+ emit({ ...base(o, src), type: "fracture", part: p.spec.id, style, fragments, point: hitW });
1998
+ debris(o, p, p.mat.breakDebris, c, [0, 1, 0], clamp2(Math.round(Math.cbrt(partVolume(p.spec.shape)) * 30), 3, 20), 2, src);
1999
+ if (ex) {
2000
+ emit({ ...base(o, src), type: "explode", part: p.spec.id, point: c, energy: ex.energy, radius: ex.radius });
2001
+ blasts.push({ point: c, energy: ex.energy, radius: ex.radius, source: src });
2002
+ }
2003
+ partLost(o, p, src);
2004
+ };
2005
+ const applyDamage = (o, p, amount, type, origin, dir, fragEnergy, cap, src) => {
2006
+ if (isLost(p) || amount <= 0) return 0;
2007
+ const prev = p.st.hp;
2008
+ p.st.hp -= amount;
2009
+ markDamaged(p);
2010
+ if (p.st.hp <= 0 && p.mat.fracture === "none" && !p.spec.explodes) p.st.hp = Math.min(prev, 1);
2011
+ const done = prev - p.st.hp;
2012
+ if (done > 0) emit({ ...base(o, src), type: "damage", part: p.spec.id, amount: done, hp: Math.max(0, p.st.hp), damageType: type });
2013
+ if (p.st.hp <= 0) failPart(o, p, origin, dir, fragEnergy, cap, src);
2014
+ return done;
2015
+ };
2016
+ const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src) => {
2017
+ const mat = p.mat;
2018
+ const sin = Math.min(1, Math.abs(vec.dot(d, n)));
2019
+ const angle = Math.asin(sin) * 180 / Math.PI;
2020
+ const effM = p.thickness / Math.max(sin, 0.3);
2021
+ const thr = mat.penetration * effM * 1e3;
2022
+ const penetrated = E > thr;
2023
+ const scale = mat.damageScale?.ballistic ?? 1;
2024
+ const id = p.spec.id;
2025
+ if (!penetrated && mat.ricochetDeg && angle < mat.ricochetDeg) {
2026
+ const r = vec.sub(d, vec.scale(n, 2 * vec.dot(d, n)));
2027
+ emit({ ...base(o, src), type: "ricochet", part: id, point: entry, direction: vec.normalize(r), energy: E * 0.4 });
2028
+ debris(o, p, mat.hitDebris, entry, n, 3, 3, src);
2029
+ const dmg2 = applyDamage(o, p, E * DMG_PER_J * scale * 0.2, "ballistic", entry, d, E * 0.1, 12, src);
2030
+ return { penetrated: false, exitEnergy: 0, damage: dmg2 };
2031
+ }
2032
+ const brittle = mat.ductility < 0.3;
2033
+ const ratio = E / Math.max(thr, 1e-6);
2034
+ const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
2035
+ let exitPoint;
2036
+ if (penetrated) {
2037
+ exitPoint = vec.madd(entry, d, Math.min(pathLen, effM));
2038
+ const hole = addHole(o, p, entry, n, caliber / 1e3 * (mat.ductility >= 0.3 ? 1 : 1.5), true, exitPoint, src);
2039
+ if (brittle && mat.fracture === "shatter") addCrack(o, p, entry, n, crackLen, src);
2040
+ if (p.spec.contains && (p.st.liters ?? 0) > 0) {
2041
+ emit({
2042
+ ...base(o, src),
2043
+ type: "leak",
2044
+ part: id,
2045
+ fluid: p.spec.contains.fluid,
2046
+ point: entry,
2047
+ direction: n,
2048
+ rate: holeRate(hole, !!p.spec.contains.pressurized),
2049
+ pressurized: !!p.spec.contains.pressurized
2050
+ });
2051
+ }
2052
+ } else if (mat.ductility >= 0.5) {
2053
+ addDent(o, p, entry, n, Math.max(caliber * 3 / 1e3, 0.012), clamp2(Math.min(Math.max(p.thickness * 3, 3e-3), ratio * 0.01), 1e-3, 0.05), src);
2054
+ } else if (brittle) {
2055
+ addCrack(o, p, entry, n, crackLen, src);
2056
+ if (mat.fracture !== "shatter") addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
2057
+ } else addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
2058
+ debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
2059
+ wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
2060
+ const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry, d, E * 0.1, 12, src);
2061
+ return { penetrated, exitEnergy: penetrated ? E - thr : 0, exitPoint, damage: dmg };
2062
+ };
2063
+ const melee = (o, p, kind, E, point, n, d, contactR, src) => {
2064
+ const mat = p.mat;
2065
+ const thrMm = mat.penetration * p.thickness * 1e3;
2066
+ const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
2067
+ const Ec = E * fragile;
2068
+ if (E > 0) {
2069
+ if (kind === "blunt") {
2070
+ if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp2(E * 1e-4, 1e-3, 0.06), src);
2071
+ else if (mat.ductility < 0.3) {
2072
+ if (Ec >= 5) addCrack(o, p, point, n, clamp2(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
2073
+ } else if (E >= thrMm * 0.2) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp2(E * 6e-5, 1e-3, 0.04), src);
2074
+ } else if (mat.fracture === "tear") {
2075
+ addHole(o, p, point, n, clamp2(0.02 + E * 5e-4, 0.02, 0.3), true, void 0, src);
2076
+ } else if (mat.ductility >= 0.5) {
2077
+ addDent(o, p, point, n, Math.max(contactR, 0.01), clamp2(E * 4e-5, 5e-4, 0.02), src);
2078
+ }
2079
+ debris(o, p, mat.hitDebris, point, n, clamp2(Math.round(E / 25) + 1, 1, 10), clamp2(Math.sqrt(E) / 8, 1, 6), src);
2080
+ }
2081
+ if (kind === "blunt") wearJoints(o, p, E * 0.3, void 0, 0, src);
2082
+ else wearJoints(o, p, E * 0.3, point, 0.06, src);
2083
+ const scale = mat.damageScale?.[kind] ?? 1;
2084
+ return applyDamage(o, p, Ec * DMG_PER_J * scale, kind, point, d, E * 0.1, 12, src);
2085
+ };
2086
+ const blastObject = (o, point, energy, radius, src) => {
2087
+ const start = cur.length;
2088
+ const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
2089
+ for (const p of o.parts) {
2090
+ if (isLost(p)) continue;
2091
+ const { c, q } = partXf(o, p);
2092
+ const lp = toLocal(c, q, point);
2093
+ const dist = distanceToShape(p.spec.shape, lp);
2094
+ if (dist >= radius) continue;
2095
+ const f = (1 - dist / radius) ** 2;
2096
+ const Ein = energy * f * BLAST_COUPLING;
2097
+ const sp = surfacePoint(p.spec.shape, lp);
2098
+ const wp = toWorld(c, q, sp.point);
2099
+ const wn = rotateVec(q, sp.normal);
2100
+ if (!res.part) {
2101
+ res.part = p.spec.id;
2102
+ res.point = wp;
2103
+ }
2104
+ const mat = p.mat;
2105
+ if (Ein > 20) {
2106
+ if (mat.ductility >= 0.5) addDent(o, p, wp, wn, clamp2(0.05 + Math.sqrt(Ein) * 2e-3, 0.05, 0.4), clamp2(4e-3 + Ein * 2e-5, 4e-3, 0.08), src);
2107
+ else if (mat.ductility < 0.3 && Ein > 60) addCrack(o, p, wp, wn, clamp2(0.05 + Ein * 5e-4, 0.05, 1), src);
2108
+ debris(o, p, mat.hitDebris, wp, wn, clamp2(Math.round(Ein / 80) + 1, 1, 12), clamp2(Math.sqrt(Ein) / 4, 2, 15), src);
2109
+ }
2110
+ const th = mat.thermal;
2111
+ if (th.ignitionC !== void 0 && p.st.fuel > 0) {
2112
+ p.st.temperature += 300 * f;
2113
+ o.active = true;
2114
+ if (f >= 0.25 && !p.st.burning) {
2115
+ p.st.temperature = Math.max(p.st.temperature, th.ignitionC + 1);
2116
+ checkIgnite(o, p, src);
2117
+ }
2118
+ }
2119
+ wearJoints(o, p, Ein * 0.5, void 0, 0, src);
2120
+ res.damage += applyDamage(o, p, Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1), "explosive", point, null, Ein * 0.3, 40, src);
2121
+ }
2122
+ res.events = cur.slice(start);
2123
+ return res;
2124
+ };
2125
+ const blastWorld = (point, energy, radius, src) => {
2126
+ const out = [];
2127
+ for (const o of [...objects.values()]) {
2128
+ if (vec.distance(toWorld(o.st.position, o.st.rotation, o.bc), point) - o.br >= radius) continue;
2129
+ const r = blastObject(o, point, energy, radius, src);
2130
+ if (r.part) out.push(r);
2131
+ }
2132
+ for (const im of [...impls.values()]) {
2133
+ const inRange = im.impl.describe().parts.some((ps) => {
2134
+ const ov = im.poses.get(ps.id);
2135
+ const c = toWorld(im.position, im.rotation, ov?.pos ?? ps.position);
2136
+ const q = quat.mul(im.rotation, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]));
2137
+ return distanceToShape(ps.shape, toLocal(c, q, point)) < radius;
2138
+ });
2139
+ if (inRange) out.push(implHit(im, { type: "explosive", point, energy, radius, ...src !== void 0 ? { source: src } : {} }));
2140
+ }
2141
+ return out;
2142
+ };
2143
+ const heatObject = (o, point, T, radius, duration, src) => {
2144
+ const start = cur.length;
2145
+ const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
2146
+ for (const p of o.parts) {
2147
+ if (isLost(p)) continue;
2148
+ const dist = distToPart(o, p, point);
2149
+ if (dist >= radius) continue;
2150
+ const f = clamp2(1 - dist / radius, 0.25, 1);
2151
+ if (!res.part) {
2152
+ res.part = p.spec.id;
2153
+ res.point = point;
2154
+ }
2155
+ if (T > p.st.temperature) {
2156
+ const k = 0.8 * f / (p.mat.thermal.heatCapacity ?? 1);
2157
+ p.st.temperature += (T - p.st.temperature) * (1 - Math.exp(-k * duration));
2158
+ }
2159
+ o.active = true;
2160
+ checkIgnite(o, p, src);
2161
+ }
2162
+ res.events = cur.slice(start);
2163
+ return res;
2164
+ };
2165
+ const doHit = (o, hit, only) => {
2166
+ let point = hit.point;
2167
+ let dir = hit.direction;
2168
+ if (hit.space === "local") {
2169
+ point = toWorld(o.st.position, o.st.rotation, point);
2170
+ if (dir) dir = rotateVec(o.st.rotation, dir);
2171
+ }
2172
+ const src = hit.source;
2173
+ const start = cur.length;
2174
+ let res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
2175
+ const E = hit.energy ?? 0;
2176
+ if (hit.type === "ballistic") {
2177
+ const loc = locate(o, point, dir, 0.05, only);
2178
+ if (loc) {
2179
+ const out = ballistic(o, loc.p, loc.point, loc.normal, loc.dir, E, hit.caliber ?? 9, loc.pathLen, src);
2180
+ res = { ...res, part: loc.p.spec.id, point: loc.point, damage: out.damage, penetrated: out.penetrated, exitEnergy: out.exitEnergy };
2181
+ }
2182
+ } else if (hit.type === "blunt" || hit.type === "slash") {
2183
+ const radius = hit.radius ?? 0;
2184
+ const loc = locate(o, point, dir, Math.max(0.05, radius), only);
2185
+ if (loc) {
2186
+ res.part = loc.p.spec.id;
2187
+ res.point = loc.point;
2188
+ res.damage += melee(o, loc.p, hit.type, E, loc.point, loc.normal, loc.dir, radius || 0.03, src);
2189
+ if (radius > 0.1 && !only) {
2190
+ for (const p of o.parts) {
2191
+ if (p === loc.p || isLost(p)) continue;
2192
+ const dd = distToPart(o, p, point);
2193
+ if (dd >= radius) continue;
2194
+ const { c, q } = partXf(o, p);
2195
+ const sp = surfacePoint(p.spec.shape, toLocal(c, q, point));
2196
+ res.damage += melee(o, p, hit.type, E * (1 - dd / radius) ** 2, toWorld(c, q, sp.point), rotateVec(q, sp.normal), loc.dir, radius, src);
2197
+ }
2198
+ }
2199
+ }
2200
+ } else if (hit.type === "explosive") {
2201
+ res = blastObject(o, point, E, hit.radius ?? 2, src);
2202
+ } else {
2203
+ res = heatObject(o, point, hit.temperature ?? 800, hit.radius ?? 0.25, hit.duration ?? 1, src);
2204
+ }
2205
+ res.events = cur.slice(start);
2206
+ return res;
2207
+ };
2208
+ const checkIgnite = (o, p, src) => {
2209
+ const th = p.mat.thermal;
2210
+ if (isLost(p) || p.st.burning || th.ignitionC === void 0 || p.st.fuel <= 0 || p.st.temperature < th.ignitionC) return;
2211
+ p.st.burning = true;
2212
+ p.st.temperature = Math.max(p.st.temperature, th.flameC ?? 800);
2213
+ o.active = true;
2214
+ markDamaged(p);
2215
+ emit({ ...base(o, src), type: "ignite", part: p.spec.id, point: partXf(o, p).c });
2216
+ };
2217
+ const syncMass = (p) => {
2218
+ p.st.mass = Math.max(0, p.mass0 * (1 - p.st.melt) - (p.fuel0 - p.st.fuel));
2219
+ };
2220
+ const burnStep = (o, p, dt) => {
2221
+ const th = p.mat.thermal;
2222
+ const use = Math.min((th.burnRate ?? 0.01) * p.area * dt, p.st.fuel);
2223
+ p.st.fuel -= use;
2224
+ syncMass(p);
2225
+ if (p.structural) o.dirty = true;
2226
+ p.st.temperature = th.flameC ?? 800;
2227
+ markDamaged(p);
2228
+ if (p.fuel0 > 0) {
2229
+ p.st.char = clamp2(1 - p.st.fuel / p.fuel0, 0, 1);
2230
+ p.st.hp = Math.min(p.st.hp, Math.max(1, p.st.maxHp * (p.st.fuel / p.fuel0)));
2231
+ }
2232
+ if (p.st.fuel <= 1e-9) {
2233
+ p.st.fuel = 0;
2234
+ p.st.burning = false;
2235
+ p.st.char = 1;
2236
+ const c = partXf(o, p).c;
2237
+ if ((th.fuelFraction ?? 0) >= 0.8) {
2238
+ p.st.status = "ashes";
2239
+ p.st.hp = 0;
2240
+ p.st.mass = p.mass0 * (1 - (th.fuelFraction ?? 0));
2241
+ emit({ ...base(o), type: "burnout", part: p.spec.id, point: c });
2242
+ debris(o, p, "ash", c, [0, 1, 0], clamp2(Math.round(Math.cbrt(partVolume(p.spec.shape)) * 30), 3, 20), 1);
2243
+ partLost(o, p);
2244
+ } else {
2245
+ emit({ ...base(o), type: "burnout", part: p.spec.id, point: c });
2246
+ }
2247
+ }
2248
+ };
2249
+ const meltStep = (o, p, dt) => {
2250
+ const th = p.mat.thermal;
2251
+ if (th.meltC === void 0 || p.st.temperature <= th.meltC) return;
2252
+ const rate = (th.meltRate ?? 0.05) * ((p.st.temperature - th.meltC) / 100);
2253
+ p.st.melt = Math.min(1, p.st.melt + rate * dt);
2254
+ syncMass(p);
2255
+ if (p.structural) o.dirty = true;
2256
+ markDamaged(p);
2257
+ p.rt.dripT += dt;
2258
+ if (p.rt.dripT >= 0.25) {
2259
+ p.rt.dripT -= 0.25;
2260
+ const b = partWorldAabb(o, p);
2261
+ const pt = [b.min[0] + (b.max[0] - b.min[0]) * (0.2 + 0.6 * rnd()), b.min[1], b.min[2] + (b.max[2] - b.min[2]) * (0.2 + 0.6 * rnd())];
2262
+ emit({ ...base(o), type: "drip", part: p.spec.id, point: pt, material: p.mat.id });
2263
+ }
2264
+ if (p.st.melt - p.rt.meltEmitted >= 0.05 || p.st.melt >= 1) {
2265
+ p.rt.meltEmitted = p.st.melt;
2266
+ emit({ ...base(o), type: "melt", part: p.spec.id, melt: p.st.melt });
2267
+ }
2268
+ if (p.st.melt >= 1) {
2269
+ p.st.status = "melted";
2270
+ p.st.hp = 0;
2271
+ p.st.mass = 0;
2272
+ p.st.burning = false;
2273
+ p.rt.jet = false;
2274
+ partLost(o, p);
2275
+ }
2276
+ };
2277
+ const stepObject = (o, dt, sources) => {
2278
+ const parts = o.parts;
2279
+ const T0 = o.tmp;
2280
+ T0.length = parts.length;
2281
+ for (let i = 0; i < parts.length; i++) T0[i] = parts[i].st.temperature;
2282
+ let hot = false;
2283
+ for (let i = 0; i < parts.length; i++) {
2284
+ const p = parts[i];
2285
+ if (isLost(p)) continue;
2286
+ const th = p.mat.thermal;
2287
+ const hc = th.heatCapacity ?? 1;
2288
+ const s = p.st;
2289
+ if (p.rt.jet) {
2290
+ s.temperature = Math.max(s.temperature, 900);
2291
+ } else if (s.burning) {
2292
+ burnStep(o, p, dt);
2293
+ if (isLost(p)) continue;
2294
+ }
2295
+ if (!s.burning && !p.rt.jet) {
2296
+ let dT = 0;
2297
+ let maxN = s.temperature;
2298
+ for (const j of o.byPart.get(p.spec.id)) {
2299
+ if (j.st.open) continue;
2300
+ const k = j.ia === i ? j.ib : j.ia;
2301
+ if (isLost(parts[k]) || T0[k] <= T0[i]) continue;
2302
+ const c = (th.conductivity + parts[k].mat.thermal.conductivity) / 2;
2303
+ dT += (T0[k] - T0[i]) * (1 - Math.exp(-1.2 * c * dt / hc));
2304
+ if (T0[k] > maxN) maxN = T0[k];
2305
+ }
2306
+ if (dT > 0) s.temperature = Math.min(s.temperature + dT, maxN);
2307
+ s.temperature += (ambient - s.temperature) * (1 - Math.exp(-dt / (20 * hc)));
2308
+ if (Math.abs(s.temperature - ambient) < 0.05) s.temperature = ambient;
2309
+ }
2310
+ meltStep(o, p, dt);
2311
+ if (isLost(p)) continue;
2312
+ checkIgnite(o, p);
2313
+ const ex = p.spec.explodes;
2314
+ if (ex && ex.atC !== void 0 && s.temperature >= ex.atC) {
2315
+ failPart(o, p, null, null, 0, 40);
2316
+ continue;
2317
+ }
2318
+ const cont = p.spec.contains;
2319
+ if (cont && (s.liters ?? 0) > 0) {
2320
+ let rate = 0;
2321
+ for (const h of s.holes) if (h.through) rate += holeRate(h, !!cont.pressurized);
2322
+ if (rate > 0) {
2323
+ s.liters = Math.max(0, (s.liters ?? 0) - rate * dt);
2324
+ hot = true;
2325
+ if (FLAMMABLE_FLUIDS.includes(cont.fluid) && s.temperature >= 350 && !p.rt.jet) {
2326
+ if (ex) {
2327
+ failPart(o, p, null, null, 0, 40);
2328
+ continue;
2329
+ }
2330
+ p.rt.jet = true;
2331
+ s.burning = true;
2332
+ emit({ ...base(o), type: "ignite", part: p.spec.id, point: partXf(o, p).c });
2333
+ }
2334
+ }
2335
+ }
2336
+ if (p.rt.jet && (s.liters ?? 0) <= 0) {
2337
+ p.rt.jet = false;
2338
+ s.burning = false;
2339
+ emit({ ...base(o), type: "extinguish", part: p.spec.id });
2340
+ }
2341
+ const sc = th.thermalShockC;
2342
+ if (sc !== void 0) {
2343
+ p.rt.shockT = Math.max(0, p.rt.shockT - dt);
2344
+ const rate = Math.abs(s.temperature - p.rt.prevTemp) / dt;
2345
+ p.rt.prevTemp = s.temperature;
2346
+ if (rate > sc && p.rt.shockT <= 0) {
2347
+ p.rt.shockT = 1;
2348
+ const { c, q } = partXf(o, p);
2349
+ const lp = [(rnd() - 0.5) * p.dims[0], (rnd() - 0.5) * p.dims[1], (rnd() - 0.5) * p.dims[2]];
2350
+ const sp = surfacePoint(p.spec.shape, lp);
2351
+ addCrack(o, p, toWorld(c, q, sp.point), rotateVec(q, sp.normal), 0.1 + rate / sc * 0.1);
2352
+ applyDamage(o, p, p.st.maxHp * 0.25, "heat", null, null, 5, 3);
2353
+ if (isLost(p)) continue;
2354
+ }
2355
+ }
2356
+ if (s.burning || p.rt.jet) sources.push({ o, p, flame: p.rt.jet ? 900 : th.flameC ?? 800 });
2357
+ if (s.burning || p.rt.jet || s.temperature > ambient + 0.5) hot = true;
2358
+ }
2359
+ if (o.hasStructure) {
2360
+ if (o.dirty) {
2361
+ o.dirty = false;
2362
+ solveStructure(o);
2363
+ }
2364
+ for (const p of parts) {
2365
+ if (!p.structural || isLost(p)) continue;
2366
+ const stress = p.st.stress ?? 0;
2367
+ p.rt.stressT = Math.max(0, (p.rt.stressT ?? 0) - dt);
2368
+ if (stress > 1) {
2369
+ if (p.rt.stressT <= 0) {
2370
+ p.rt.stressT = 0.5;
2371
+ emit({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
2372
+ }
2373
+ applyDamage(o, p, (stress - 1) * p.st.maxHp * 0.5 * dt, "blunt", null, null, 5, 3);
2374
+ } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
2375
+ p.rt.stressT = 0.5;
2376
+ const { c, q } = partXf(o, p);
2377
+ const sp = surfacePoint(p.spec.shape, [(rnd() - 0.5) * p.dims[0], (rnd() - 0.5) * p.dims[1], (rnd() - 0.5) * p.dims[2]]);
2378
+ addCrack(o, p, toWorld(c, q, sp.point), rotateVec(q, sp.normal), 0.1 + (stress - 0.8) * 0.5);
2379
+ }
2380
+ }
2381
+ if (o.maxStress > 0.8) hot = true;
2382
+ }
2383
+ o.active = hot;
2384
+ };
2385
+ const spread = (sources, dt) => {
2386
+ for (const src of sources) {
2387
+ if (!src.p.st.burning && !src.p.rt.jet) continue;
2388
+ const sc = toWorld(src.o.st.position, src.o.st.rotation, src.p.pos);
2389
+ const A = partWorldAabb(src.o, src.p);
2390
+ for (const o of objects.values()) {
2391
+ if (o === src.o) continue;
2392
+ if (vec.distance(toWorld(o.st.position, o.st.rotation, o.bc), sc) > o.br + src.p.bound + fireSpread) continue;
2393
+ for (const q of o.parts) {
2394
+ if (isLost(q) || q.st.burning) continue;
2395
+ const B = partWorldAabb(o, q);
2396
+ const gap = Math.hypot(
2397
+ Math.max(0, A.min[0] - B.max[0], B.min[0] - A.max[0]),
2398
+ Math.max(0, A.min[1] - B.max[1], B.min[1] - A.max[1]),
2399
+ Math.max(0, A.min[2] - B.max[2], B.min[2] - A.max[2])
2400
+ );
2401
+ if (gap >= fireSpread || src.flame <= q.st.temperature) continue;
2402
+ const f = 1 - gap / fireSpread;
2403
+ q.st.temperature += (src.flame - q.st.temperature) * (1 - Math.exp(-0.6 * f * dt / (q.mat.thermal.heatCapacity ?? 1)));
2404
+ o.active = true;
2405
+ }
2406
+ }
2407
+ }
2408
+ };
2409
+ const withCollapses = (r) => {
2410
+ const last = r.value[r.value.length - 1];
2411
+ if (!last) return r.value;
2412
+ for (const e of r.events) if (e.type === "collapse" && !r.value.some((h) => h.events.includes(e))) last.events.push(e);
2413
+ return r.value;
2414
+ };
2415
+ const whole = (fn) => {
2416
+ const r = run(fn);
2417
+ r.value.events = r.events;
2418
+ return r.value;
2419
+ };
2420
+ const world = {
2421
+ materials,
2422
+ get time() {
2423
+ return time;
2424
+ },
2425
+ add(spec, opts = {}) {
2426
+ const id = opts.id ?? nextId();
2427
+ if (taken(id)) throw new Error(`object "${id}" already exists`);
2428
+ objects.set(id, build(resolveSpec(spec), id, opts.position ?? [0, 0, 0], opts.rotation ?? [0, 0, 0, 1]));
2429
+ return id;
2430
+ },
2431
+ register(impl, opts = {}) {
2432
+ const id = opts.id ?? impl.id;
2433
+ if (taken(id)) throw new Error(`object "${id}" already exists`);
2434
+ impls.set(id, { impl, position: opts.position ?? [0, 0, 0], rotation: opts.rotation ?? [0, 0, 0, 1], poses: /* @__PURE__ */ new Map() });
2435
+ return id;
2436
+ },
2437
+ setPartPose(objectId, partId, position, rotation) {
2438
+ const q = quatFromEuler(...rotation ?? [0, 0, 0]);
2439
+ const im = impls.get(objectId);
2440
+ if (im) {
2441
+ im.impl.setPartPose?.(partId, position, rotation);
2442
+ im.poses.set(partId, { pos: [position[0], position[1], position[2]], q });
2443
+ return;
2444
+ }
2445
+ const o = getObj(objectId);
2446
+ const p = getPart(o, partId);
2447
+ p.pos = [position[0], position[1], position[2]];
2448
+ p.q = q;
2449
+ updateBound(o);
2450
+ },
2451
+ remove(id) {
2452
+ objects.delete(id);
2453
+ impls.delete(id);
2454
+ },
2455
+ has: (id) => taken(id),
2456
+ ids: () => [...objects.keys(), ...impls.keys()],
2457
+ setTransform(id, position, rotation) {
2458
+ const im = impls.get(id);
2459
+ if (im) {
2460
+ im.position = [position[0], position[1], position[2]];
2461
+ im.rotation = [rotation[0], rotation[1], rotation[2], rotation[3]];
2462
+ return;
2463
+ }
2464
+ const o = getObj(id);
2465
+ o.st.position = [position[0], position[1], position[2]];
2466
+ o.st.rotation = [rotation[0], rotation[1], rotation[2], rotation[3]];
2467
+ },
2468
+ splitGroup(id, group, newId) {
2469
+ if (impls.has(id)) throw new Error(`object "${id}" is a registered DestructibleImpl and cannot be split; its module owns its pieces`);
2470
+ const o = getObj(id);
2471
+ if (o.st.groups.length < 2) return id;
2472
+ const names = o.st.groups[group];
2473
+ if (!names) throw new Error(`object "${id}" has no group ${group}`);
2474
+ const moved = new Set(names);
2475
+ const nid = newId ?? nextId();
2476
+ if (objects.has(nid)) throw new Error(`object "${nid}" already exists`);
2477
+ const carryFor = (keep, keepOpen = () => false) => ({
2478
+ parts: new Map(o.parts.filter((p) => keep(p.spec.id)).map((p) => [p.spec.id, { st: p.st, rt: p.rt }])),
2479
+ joints: new Map([
2480
+ ...o.joints.filter((j) => keep(j.spec.a) && keep(j.spec.b) || j.st.open && keepOpen(j.spec.a, j.spec.b)).map((j) => [j.spec.id, { st: j.st, step: j.step }]),
2481
+ ...o.st.joints.filter((st) => st.open && !o.jointMap.has(st.id) && keepOpen(st.a, st.b)).map((st) => [st.id, { st, step: 0 }])
2482
+ ]),
2483
+ collapsed: [...o.collapsed].filter(keep)
2484
+ });
2485
+ const sub = (keep) => {
2486
+ const s = { parts: o.spec.parts.filter((p) => keep(p.id)), joints: (o.spec.joints ?? []).filter((j) => keep(j.a) && keep(j.b)) };
2487
+ if (o.spec.name !== void 0) s.name = o.spec.name;
2488
+ if (o.spec.tags) s.tags = [...o.spec.tags];
2489
+ return clone(s);
2490
+ };
2491
+ const inNew = (pid) => moved.has(pid);
2492
+ const inOld = (pid) => !moved.has(pid);
2493
+ const pos = o.st.position, rot = o.st.rotation;
2494
+ const fresh = build(sub(inNew), nid, pos, rot, carryFor(inNew, (a, b) => inNew(a) && inNew(b)));
2495
+ const rest = build(sub(inOld), id, pos, rot, carryFor(inOld, (a, b) => !(inNew(a) && inNew(b))));
2496
+ objects.set(id, rest);
2497
+ objects.set(nid, fresh);
2498
+ return nid;
2499
+ },
2500
+ hit(objectId, h) {
2501
+ const im = impls.get(objectId);
2502
+ if (im) return whole(() => implHit(im, h));
2503
+ const o = getObj(objectId);
2504
+ return whole(() => doHit(o, h));
2505
+ },
2506
+ hitPart(objectId, partId, h) {
2507
+ const im = impls.get(objectId);
2508
+ if (im) return whole(() => implHit(im, h));
2509
+ const o = getObj(objectId);
2510
+ const p = getPart(o, partId);
2511
+ return whole(() => doHit(o, h, p));
2512
+ },
2513
+ explode(point, energy, radius, source) {
2514
+ return withCollapses(run(() => blastWorld(point, energy, radius, source)));
2515
+ },
2516
+ raycast(origin, direction, maxDistance = Infinity) {
2517
+ const r = raycastInternal(origin, direction, maxDistance);
2518
+ return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
2519
+ },
2520
+ shoot(origin, direction, energy, caliber, source) {
2521
+ return withCollapses(run(() => {
2522
+ const results = [];
2523
+ const d = vec.normalize(direction);
2524
+ const ignore = /* @__PURE__ */ new Set();
2525
+ let org = origin;
2526
+ let E = energy;
2527
+ for (let i = 0; i < 4 && E > 0; i++) {
2528
+ const rh = raycastInternal(org, d, Infinity, ignore);
2529
+ if (!rh) break;
2530
+ const im = impls.get(rh.object);
2531
+ if (im) {
2532
+ const r = implHit(im, { type: "ballistic", point: rh.point, direction: d, energy: E, caliber, ...source !== void 0 ? { source } : {} });
2533
+ r.part = rh.part;
2534
+ r.point = rh.point;
2535
+ r.exitEnergy = r.penetrated ? E * 0.5 : 0;
2536
+ results.push(r);
2537
+ ignore.add(`${im.impl.id}\0${rh.part}`);
2538
+ if (!r.penetrated) break;
2539
+ org = vec.madd(rh.point, d, Math.min(rh.pathLen, 0.05) + 1e-3);
2540
+ E = r.exitEnergy;
2541
+ continue;
2542
+ }
2543
+ const o = getObj(rh.object);
2544
+ const p = getPart(o, rh.part);
2545
+ const start = cur.length;
2546
+ const out = ballistic(o, p, rh.point, rh.normal, d, E, caliber, rh.pathLen, source);
2547
+ results.push({ object: o.id, part: p.spec.id, point: rh.point, damage: out.damage, penetrated: out.penetrated, exitEnergy: out.exitEnergy, events: cur.slice(start) });
2548
+ ignore.add(`${o.id}\0${p.spec.id}`);
2549
+ if (!out.penetrated || !out.exitPoint) break;
2550
+ org = vec.madd(out.exitPoint, d, 1e-3);
2551
+ E = out.exitEnergy;
2552
+ }
2553
+ return results;
2554
+ }));
2555
+ },
2556
+ disassemble(objectId, jointId, tool, dt) {
2557
+ const im = impls.get(objectId);
2558
+ if (im) return im.impl.disassemble ? im.impl.disassemble(jointId, tool, dt) : -1;
2559
+ const o = getObj(objectId);
2560
+ const j = o.jointMap.get(jointId);
2561
+ if (!j || !j.spec.tools.includes(tool)) return -1;
2562
+ if (j.st.open) return 1;
2563
+ return run(() => {
2564
+ j.st.progress = Math.min(1, j.st.progress + (j.spec.seconds > 0 ? dt / j.spec.seconds : 1));
2565
+ if (j.st.progress >= 1 - 1e-9) j.st.progress = 1;
2566
+ const step = Math.floor(j.st.progress * 10 + 1e-9);
2567
+ if (step > j.step || j.st.progress >= 1) {
2568
+ j.step = step;
2569
+ emit({ ...base(o), type: "disassemble_progress", joint: j.spec.id, tool, progress: j.st.progress });
2570
+ }
2571
+ if (j.st.progress >= 1) {
2572
+ j.st.open = true;
2573
+ j.st.openedBy = "tool";
2574
+ solveStructure(o);
2575
+ recomputeGroups(o);
2576
+ detachEvents(o, [j], "tool");
2577
+ collapseCheck(o);
2578
+ }
2579
+ return j.st.progress;
2580
+ }).value;
2581
+ },
2582
+ workableJoints(objectId, tool) {
2583
+ const im = impls.get(objectId);
2584
+ if (im) {
2585
+ const spec = im.impl.describe();
2586
+ return im.impl.state().joints.filter((js) => {
2587
+ const j = (spec.joints ?? []).find((x) => (x.id ?? `${x.a}~${x.b}`) === js.id);
2588
+ return !js.open && !!j && (j.tools ?? JOINT_DEFAULTS[j.kind].tools).includes(tool);
2589
+ });
2590
+ }
2591
+ return getObj(objectId).joints.filter((j) => !j.st.open && j.spec.tools.includes(tool)).map((j) => j.st);
2592
+ },
2593
+ nearestJoint(objectId, point, maxDistance = 0.3, tool) {
2594
+ const im = impls.get(objectId);
2595
+ const cands = [];
2596
+ if (im) {
2597
+ const spec = im.impl.describe();
2598
+ for (const js of im.impl.state().joints) {
2599
+ if (js.open) continue;
2600
+ const pos = implJointPos(im, js.a, js.b);
2601
+ const j = (spec.joints ?? []).find((x) => (x.id ?? `${x.a}~${x.b}`) === js.id);
2602
+ cands.push({ st: js, d: pos ? vec.distance(pos, point) : Infinity, ok: !!j && (j.tools ?? JOINT_DEFAULTS[j.kind].tools).includes(tool) });
2603
+ }
2604
+ } else {
2605
+ const o = getObj(objectId);
2606
+ for (const j of o.joints) {
2607
+ if (j.st.open) continue;
2608
+ cands.push({ st: j.st, d: vec.distance(jointWorld(o, j), point), ok: tool ? j.spec.tools.includes(tool) : true });
2609
+ }
2610
+ }
2611
+ const pick = (only) => {
2612
+ let best = null;
2613
+ let bd = maxDistance;
2614
+ for (const c of cands) {
2615
+ if (only && !c.ok) continue;
2616
+ if (c.d <= bd) {
2617
+ bd = c.d;
2618
+ best = c.st;
2619
+ }
2620
+ }
2621
+ return best;
2622
+ };
2623
+ return (tool ? pick(true) : null) ?? pick(false);
2624
+ },
2625
+ extinguish(objectId, partId, coolC = 200) {
2626
+ const im = impls.get(objectId);
2627
+ if (im) {
2628
+ run(() => im.impl.extinguish?.(partId));
2629
+ return;
2630
+ }
2631
+ const o = getObj(objectId);
2632
+ run(() => {
2633
+ for (const p of partId ? [getPart(o, partId)] : o.parts) {
2634
+ if (isLost(p)) continue;
2635
+ if (p.st.burning) {
2636
+ p.st.burning = false;
2637
+ p.rt.jet = false;
2638
+ emit({ ...base(o), type: "extinguish", part: p.spec.id });
2639
+ }
2640
+ p.st.temperature = Math.max(ambient, p.st.temperature - coolC);
2641
+ }
2642
+ o.active = true;
2643
+ });
2644
+ },
2645
+ setLiters(objectId, partId, liters) {
2646
+ if (impls.has(objectId)) return;
2647
+ const p = getPart(getObj(objectId), partId);
2648
+ if (!p.spec.contains) return;
2649
+ p.st.liters = clamp2(liters, 0, p.spec.contains.liters);
2650
+ getObj(objectId).active = true;
2651
+ },
2652
+ update(dt) {
2653
+ if (!(dt > 0)) return [];
2654
+ return run(() => {
2655
+ time += dt;
2656
+ const sources = [];
2657
+ for (const o of [...objects.values()]) if (o.active) stepObject(o, dt, sources);
2658
+ if (sources.length) spread(sources, dt);
2659
+ for (const im of [...impls.values()]) {
2660
+ if (!im.impl.update) continue;
2661
+ for (const e of im.impl.update(dt) ?? []) emit(e.time === void 0 ? { ...e, time } : e);
2662
+ }
2663
+ }).events;
2664
+ },
2665
+ state: (id) => objects.get(id)?.st ?? impls.get(id)?.impl.state() ?? null,
2666
+ snapshot: (id) => {
2667
+ const st = objects.get(id)?.st ?? impls.get(id)?.impl.state();
2668
+ return st ? clone(st) : null;
2669
+ },
2670
+ part: (id, partId) => objects.get(id)?.byId.get(partId)?.st ?? impls.get(id)?.impl.state().parts.find((p) => p.id === partId) ?? null,
2671
+ reset(id) {
2672
+ const im = impls.get(id);
2673
+ if (im) {
2674
+ im.impl.reset?.();
2675
+ return;
2676
+ }
2677
+ const o = getObj(id);
2678
+ objects.set(id, build(o.spec, id, o.st.position, o.st.rotation));
2679
+ },
2680
+ on(type, fn) {
2681
+ let set = listeners.get(type);
2682
+ if (!set) listeners.set(type, set = /* @__PURE__ */ new Set());
2683
+ set.add(fn);
2684
+ return () => {
2685
+ set.delete(fn);
2686
+ };
2687
+ },
2688
+ serialize() {
2689
+ return {
2690
+ version: 1,
2691
+ time,
2692
+ rng: rng.state(),
2693
+ counter,
2694
+ objects: [...objects.values()].map((o) => {
2695
+ const runtime = { parts: {}, joints: {}, collapsed: [...o.collapsed] };
2696
+ for (const p of o.parts) runtime.parts[p.spec.id] = { ...p.rt };
2697
+ for (const j of o.joints) runtime.joints[j.spec.id] = j.step;
2698
+ return { id: o.id, spec: clone(o.spec), state: clone(o.st), runtime };
2699
+ }),
2700
+ ...impls.size ? { impls: [...impls.values()].map((im) => ({ id: im.impl.id, position: [...im.position], rotation: [...im.rotation], data: im.impl.serialize?.() })) } : {}
2701
+ };
2702
+ },
2703
+ load(snap) {
2704
+ if (snap.version !== 1) throw new Error(`unsupported snapshot version ${String(snap.version)}`);
2705
+ objects.clear();
2706
+ time = snap.time;
2707
+ rng.set(snap.rng);
2708
+ counter = snap.counter ?? 0;
2709
+ for (const e of snap.objects) {
2710
+ const state = clone(e.state);
2711
+ const carry = {
2712
+ parts: new Map(
2713
+ state.parts.map((st) => [st.id, { st, rt: e.runtime?.parts[st.id] ? { ...e.runtime.parts[st.id] } : { ...freshRuntime(), prevTemp: st.temperature } }])
2714
+ ),
2715
+ joints: new Map(state.joints.map((st) => [st.id, { st, step: e.runtime?.joints[st.id] ?? 0 }])),
2716
+ collapsed: e.runtime?.collapsed
2717
+ };
2718
+ objects.set(e.id, build(clone(e.spec), e.id, state.position, state.rotation, carry));
2719
+ }
2720
+ for (const e of snap.impls ?? []) {
2721
+ const im = impls.get(e.id);
2722
+ if (!im) continue;
2723
+ im.position = [e.position[0], e.position[1], e.position[2]];
2724
+ im.rotation = [e.rotation[0], e.rotation[1], e.rotation[2], e.rotation[3]];
2725
+ if (e.data !== void 0) im.impl.load?.(e.data);
2726
+ }
2727
+ }
2728
+ };
2729
+ return world;
2730
+ };
2731
+
2732
+ // src/frame.ts
2733
+ var buildFrame = (o = {}) => {
2734
+ const spanX = o.spanX ?? 8, spanZ = o.spanZ ?? 8;
2735
+ const nx = Math.max(2, o.columnsX ?? 3), nz = Math.max(2, o.columnsZ ?? 3);
2736
+ const H = o.height ?? 3, floors = Math.max(1, o.floors ?? 1);
2737
+ const cs = o.columnSize ?? 0.3, bh = o.beamHeight ?? 0.3, bw = 0.2, T = o.slabThickness ?? 0.2, wt = o.wallThickness ?? 0.2;
2738
+ const dw = o.doorWidth ?? 1, dh = o.doorHeight ?? 2.1;
2739
+ const mat = o.material ?? "concrete", wmat = o.wallMaterial ?? "brick";
2740
+ const roof = o.roof ?? true;
2741
+ const xs = Array.from({ length: nx }, (_, i) => -spanX / 2 + i * spanX / (nx - 1));
2742
+ const zs = Array.from({ length: nz }, (_, j) => -spanZ / 2 + j * spanZ / (nz - 1));
2743
+ const parts = [];
2744
+ const joints = [];
2745
+ const on = (a, b, kind) => joints.push({ id: `${a}~${b}`, a, b, kind, bears: "a_on_b" });
2746
+ for (let i = 0; i < nx; i++) {
2747
+ for (let j = 0; j < nz; j++) {
2748
+ parts.push({
2749
+ id: `foundation_${i}_${j}`,
2750
+ material: mat,
2751
+ shape: { kind: "box", size: [cs * 2.2, 0.3, cs * 2.2] },
2752
+ position: [xs[i], -0.15, zs[j]],
2753
+ structure: { role: "foundation" },
2754
+ anchored: true
2755
+ });
2756
+ }
2757
+ }
2758
+ for (let f = 0; f < floors; f++) {
2759
+ const yb = f * (H + T);
2760
+ const top = yb + H;
2761
+ const last = f === floors - 1;
2762
+ const slabId = last && roof ? "roof" : `slab_${f}`;
2763
+ for (let i = 0; i < nx; i++) {
2764
+ for (let j = 0; j < nz; j++) {
2765
+ const id = `column_${f}_${i}_${j}`;
2766
+ parts.push({
2767
+ id,
2768
+ material: mat,
2769
+ shape: { kind: "box", size: [cs, H, cs] },
2770
+ position: [xs[i], yb + H / 2, zs[j]],
2771
+ structure: o.columnCapacity === void 0 ? { role: "column" } : { role: "column", capacity: o.columnCapacity }
2772
+ });
2773
+ on(id, f === 0 ? `foundation_${i}_${j}` : `slab_${f - 1}`, "rebar");
2774
+ }
2775
+ }
2776
+ const beams = [];
2777
+ const beam = (id, size, pos, c1, c2) => {
2778
+ parts.push({ id, material: mat, shape: { kind: "box", size }, position: pos, structure: { role: "beam" } });
2779
+ on(id, c1, "rebar");
2780
+ on(id, c2, "rebar");
2781
+ beams.push(id);
2782
+ };
2783
+ for (let j = 0; j < nz; j++) {
2784
+ for (let i = 0; i < nx - 1; i++) {
2785
+ beam(`beam_x_${f}_${i}_${j}`, [xs[i + 1] - xs[i] - cs, bh, bw], [(xs[i] + xs[i + 1]) / 2, top - bh / 2, zs[j]], `column_${f}_${i}_${j}`, `column_${f}_${i + 1}_${j}`);
2786
+ }
2787
+ }
2788
+ for (let i = 0; i < nx; i++) {
2789
+ for (let j = 0; j < nz - 1; j++) {
2790
+ beam(`beam_z_${f}_${i}_${j}`, [bw, bh, zs[j + 1] - zs[j] - cs], [xs[i], top - bh / 2, (zs[j] + zs[j + 1]) / 2], `column_${f}_${i}_${j}`, `column_${f}_${i}_${j + 1}`);
2791
+ }
2792
+ }
2793
+ const thick = last && roof ? T * 0.75 : T;
2794
+ parts.push({
2795
+ id: slabId,
2796
+ material: mat,
2797
+ shape: { kind: "box", size: [spanX + cs + 0.2, thick, spanZ + cs + 0.2] },
2798
+ position: [0, top + thick / 2, 0],
2799
+ structure: { role: last && roof ? "roof" : "slab" }
2800
+ });
2801
+ for (const b of beams) on(slabId, b, last && roof ? "resting" : "rebar");
2802
+ if (o.walls) {
2803
+ const wh = H - bh;
2804
+ const wy = yb + wh / 2;
2805
+ const wall = (id, size, pos, supports) => {
2806
+ parts.push({ id, material: wmat, shape: { kind: "box", size }, position: pos, structure: { role: "wall" } });
2807
+ for (const s of supports) on(id, s, "mortar");
2808
+ };
2809
+ for (const j of [0, nz - 1]) {
2810
+ for (let i = 0; i < nx - 1; i++) {
2811
+ const c1 = `column_${f}_${i}_${j}`, c2 = `column_${f}_${i + 1}_${j}`;
2812
+ const avail = xs[i + 1] - xs[i] - cs;
2813
+ if (f === 0 && j === 0 && i === 0 && avail > dw + 0.4) {
2814
+ const side = (avail - dw) / 2;
2815
+ const lx = xs[i] + cs / 2 + side / 2, rx = xs[i + 1] - cs / 2 - side / 2;
2816
+ wall(`wall_door_left`, [side, wh, wt], [lx, wy, zs[j]], [c1]);
2817
+ wall(`wall_door_right`, [side, wh, wt], [rx, wy, zs[j]], [c2]);
2818
+ const lh = wh - dh;
2819
+ wall(`wall_door_lintel`, [dw, lh, wt], [xs[i] + cs / 2 + side + dw / 2, yb + dh + lh / 2, zs[j]], ["wall_door_left", "wall_door_right"]);
2820
+ parts.push({ id: "door", material: "wood_hard", shape: { kind: "box", size: [dw - 0.04, dh, 0.05] }, position: [xs[i] + cs / 2 + side + dw / 2, yb + dh / 2, zs[j]], tags: ["door"] });
2821
+ joints.push({ id: "door~wall_door_left", a: "door", b: "wall_door_left", kind: "hinge", bears: false });
2822
+ } else wall(`wall_x_${f}_${i}_${j}`, [avail, wh, wt], [(xs[i] + xs[i + 1]) / 2, wy, zs[j]], [c1, c2]);
2823
+ }
2824
+ }
2825
+ for (const i of [0, nx - 1]) {
2826
+ for (let j = 0; j < nz - 1; j++) {
2827
+ wall(`wall_z_${f}_${i}_${j}`, [wt, wh, zs[j + 1] - zs[j] - cs], [xs[i], wy, (zs[j] + zs[j + 1]) / 2], [`column_${f}_${i}_${j}`, `column_${f}_${i}_${j + 1}`]);
2828
+ }
2829
+ }
2830
+ }
2831
+ }
2832
+ return { name: "frame", parts, joints, tags: ["structure"] };
2833
+ };
2834
+
2835
+ // src/locale.ts
2836
+ var locales = {
2837
+ en: {
2838
+ materialNames: {
2839
+ steel: "steel",
2840
+ cast_iron: "cast iron",
2841
+ aluminium: "aluminium",
2842
+ copper: "copper",
2843
+ wood_soft: "softwood",
2844
+ wood_hard: "hardwood",
2845
+ plywood: "plywood",
2846
+ mdf: "MDF",
2847
+ glass: "glass",
2848
+ tempered_glass: "tempered glass",
2849
+ ceramic: "ceramic",
2850
+ concrete: "concrete",
2851
+ brick: "brick",
2852
+ plaster: "plaster",
2853
+ stone: "stone",
2854
+ plastic: "plastic",
2855
+ rubber: "rubber",
2856
+ fabric: "fabric",
2857
+ foam: "foam",
2858
+ leather: "leather",
2859
+ paper: "paper",
2860
+ foliage: "foliage",
2861
+ green_wood: "green wood",
2862
+ cactus: "cactus",
2863
+ flesh_fruit: "fruit",
2864
+ wax: "wax",
2865
+ soil: "soil"
2866
+ },
2867
+ partStatus: { intact: "intact", damaged: "damaged", broken: "broken", melted: "melted", ashes: "ashes" },
2868
+ jointKinds: {
2869
+ screw: "screw",
2870
+ bolt: "nut and bolt",
2871
+ nail: "nail",
2872
+ weld: "weld",
2873
+ glue: "glue",
2874
+ hinge: "hinge",
2875
+ clip: "clip",
2876
+ tie: "tie",
2877
+ mortar: "mortar",
2878
+ fused: "fused",
2879
+ stem: "stem",
2880
+ rebar: "rebar",
2881
+ resting: "resting"
2882
+ },
2883
+ toolKinds: {
2884
+ hand: "hand",
2885
+ screwdriver: "screwdriver",
2886
+ wrench: "wrench",
2887
+ pry_bar: "pry bar",
2888
+ saw: "saw",
2889
+ cutting_torch: "cutting torch",
2890
+ knife: "knife",
2891
+ hammer: "hammer"
2892
+ },
2893
+ damageTypes: { ballistic: "ballistic", blunt: "blunt", slash: "slash", explosive: "explosive", fire: "fire", heat: "heat" },
2894
+ eventLabels: {
2895
+ damage: "damage",
2896
+ dent: "dent",
2897
+ hole: "hole",
2898
+ crack: "crack",
2899
+ ricochet: "ricochet",
2900
+ debris: "debris",
2901
+ fracture: "broke",
2902
+ detach: "came loose",
2903
+ disassemble_progress: "taking apart",
2904
+ ignite: "caught fire",
2905
+ extinguish: "put out",
2906
+ burnout: "burned to ash",
2907
+ melt: "melting",
2908
+ drip: "dripping",
2909
+ leak: "leak",
2910
+ explode: "exploded",
2911
+ stress: "overload",
2912
+ collapse: "collapsed",
2913
+ destroyed: "destroyed"
2914
+ },
2915
+ fractureStyles: { shatter: "shatter", crumble: "crumble", splinter: "splinter", tear: "tear", snap: "snap", none: "none" },
2916
+ structureRoles: { foundation: "foundation", column: "column", beam: "beam", slab: "slab", wall: "wall", partition: "partition", roof: "roof", stair: "stairs", none: "none" }
2917
+ },
2918
+ "pt-BR": {
2919
+ materialNames: {
2920
+ steel: "a\xE7o",
2921
+ cast_iron: "ferro fundido",
2922
+ aluminium: "alum\xEDnio",
2923
+ copper: "cobre",
2924
+ wood_soft: "madeira macia",
2925
+ wood_hard: "madeira de lei",
2926
+ plywood: "compensado",
2927
+ mdf: "MDF",
2928
+ glass: "vidro",
2929
+ tempered_glass: "vidro temperado",
2930
+ ceramic: "cer\xE2mica",
2931
+ concrete: "concreto",
2932
+ brick: "tijolo",
2933
+ plaster: "gesso",
2934
+ stone: "pedra",
2935
+ plastic: "pl\xE1stico",
2936
+ rubber: "borracha",
2937
+ fabric: "tecido",
2938
+ foam: "espuma",
2939
+ leather: "couro",
2940
+ paper: "papel",
2941
+ foliage: "folhagem",
2942
+ green_wood: "madeira verde",
2943
+ cactus: "cacto",
2944
+ flesh_fruit: "fruta",
2945
+ wax: "cera",
2946
+ soil: "terra"
2947
+ },
2948
+ partStatus: { intact: "intacto", damaged: "danificado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
2949
+ jointKinds: {
2950
+ screw: "parafuso",
2951
+ bolt: "porca e parafuso",
2952
+ nail: "prego",
2953
+ weld: "solda",
2954
+ glue: "cola",
2955
+ hinge: "dobradi\xE7a",
2956
+ clip: "encaixe",
2957
+ tie: "amarra\xE7\xE3o",
2958
+ mortar: "argamassa",
2959
+ fused: "fundido",
2960
+ stem: "caule",
2961
+ rebar: "vergalh\xE3o",
2962
+ resting: "apoiado"
2963
+ },
2964
+ toolKinds: {
2965
+ hand: "m\xE3o",
2966
+ screwdriver: "chave de fenda",
2967
+ wrench: "chave inglesa",
2968
+ pry_bar: "p\xE9 de cabra",
2969
+ saw: "serra",
2970
+ cutting_torch: "ma\xE7arico",
2971
+ knife: "faca",
2972
+ hammer: "martelo"
2973
+ },
2974
+ damageTypes: { ballistic: "tiro", blunt: "impacto", slash: "corte", explosive: "explos\xE3o", fire: "fogo", heat: "calor" },
2975
+ eventLabels: {
2976
+ damage: "dano",
2977
+ dent: "amassado",
2978
+ hole: "furo",
2979
+ crack: "trinca",
2980
+ ricochet: "ricochete",
2981
+ debris: "estilha\xE7os",
2982
+ fracture: "quebrou",
2983
+ detach: "soltou",
2984
+ disassemble_progress: "desmontando",
2985
+ ignite: "pegou fogo",
2986
+ extinguish: "apagou",
2987
+ burnout: "virou cinza",
2988
+ melt: "derretendo",
2989
+ drip: "pingando",
2990
+ leak: "vazamento",
2991
+ explode: "explodiu",
2992
+ stress: "sobrecarga",
2993
+ collapse: "desabou",
2994
+ destroyed: "destru\xEDdo"
2995
+ },
2996
+ fractureStyles: { shatter: "estilha\xE7ar", crumble: "esfarelar", splinter: "lascar", tear: "rasgar", snap: "partir", none: "nenhuma" },
2997
+ structureRoles: { foundation: "funda\xE7\xE3o", column: "pilar", beam: "viga", slab: "laje", wall: "parede", partition: "divis\xF3ria", roof: "telhado", stair: "escada", none: "nenhum" }
2998
+ }
2999
+ };
3000
+ var resolveStrings = (locale) => locales[locale ?? "en"] ?? locales.en;
3001
+
3002
+ // src/contracts.ts
3003
+ import { checkDestructible as check } from "@adatechnology/game-contracts";
3004
+ import { IMPACT_CONTRACT, checkHit } from "@adatechnology/game-contracts";
3005
+ var checkDestructible = (impl, options = {}) => {
3006
+ const contractOptions = { materials: [...MATERIALS.map((m) => m.id), ...options.materials ?? []] };
3007
+ return check(impl, contractOptions);
3008
+ };
3009
+ export {
3010
+ IMPACT_CONTRACT,
3011
+ JOINT_BY_MATERIALS,
3012
+ JOINT_DEFAULTS,
3013
+ MATERIALS,
3014
+ buildFrame,
3015
+ checkDestructible,
3016
+ checkHit,
3017
+ createDestruction,
3018
+ fragility,
3019
+ guessMaterial,
3020
+ inferJoints,
3021
+ locales,
3022
+ partCapacity,
3023
+ partMass,
3024
+ partThickness,
3025
+ partVolume,
3026
+ quat,
3027
+ quatFromEuler,
3028
+ resolveStrings,
3029
+ rotateVec,
3030
+ toLocal,
3031
+ toWorld,
3032
+ vec
3033
+ };
3034
+ //# sourceMappingURL=index.js.map