@adatechnology/game-destruction-three 0.1.0 → 0.1.2

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