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