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