@adatechnology/game-destruction-three 0.17.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -764,7 +764,9 @@ var SLAM_MIN_SPEED = 2;
764
764
  var SLAM_BOUNCE = 0.2;
765
765
  var SLAM_FLOOR_NORMAL = 0.7;
766
766
  var SUB_DT = 1 / 120;
767
- var MU = 0.7;
767
+ var MU_STATIC = 0.7;
768
+ var MU_DYNAMIC = 0.6;
769
+ var SLIP_SPEED = 0.02;
768
770
  var SLEEP_AFTER = 0.5;
769
771
  var CALM_DRIFT = 0.01;
770
772
  var CALM_TURN = 0.05;
@@ -783,6 +785,9 @@ var SIDE_BOUNCE = 0.1;
783
785
  var EDGE_POINTS = [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75];
784
786
  var SUPPORT_CHECKS2 = 16;
785
787
  var SUPPORT_GAP2 = 0.02;
788
+ var WAKE_ABOVE_SPEED = 0.05;
789
+ var WAKE_ABOVE_SPIN = 0.1;
790
+ var topFace = 1;
786
791
  var boxTopAt = (s, x, z) => {
787
792
  const e = s.e;
788
793
  const dx = x - s.c[0], dz = z - s.c[2];
@@ -798,11 +803,24 @@ var boxTopAt = (s, x, z) => {
798
803
  let a = (-h - p) / d, b = (h - p) / d;
799
804
  if (a > b) [a, b] = [b, a];
800
805
  t0 = Math.max(t0, a);
801
- t1 = Math.min(t1, b);
806
+ if (b < t1) {
807
+ t1 = b;
808
+ topFace = k;
809
+ }
802
810
  if (t0 > t1) return -Infinity;
803
811
  }
804
812
  return s.c[1] + t1;
805
813
  };
814
+ var UP = [0, 1, 0];
815
+ var faceNormal = (s, axis) => {
816
+ const e = s.e;
817
+ const sign = e[axis * 4 + 1] < 0 ? -1 : 1;
818
+ return [sign * e[axis * 4], sign * e[axis * 4 + 1], sign * e[axis * 4 + 2]];
819
+ };
820
+ var pairFriction = (a, b) => ({
821
+ static: Math.sqrt((a?.static ?? MU_STATIC) * (b?.static ?? MU_STATIC)),
822
+ dynamic: Math.sqrt((a?.dynamic ?? MU_DYNAMIC) * (b?.dynamic ?? MU_DYNAMIC))
823
+ });
806
824
  var PieceSystem = class {
807
825
  constructor(THREE, o) {
808
826
  __publicField(this, "THREE", THREE);
@@ -816,6 +834,9 @@ var PieceSystem = class {
816
834
  __publicField(this, "ax");
817
835
  __publicField(this, "solids", []);
818
836
  __publicField(this, "supportTurn", 0);
837
+ /** The surface normal and the friction of what the last `supportAt` found under the point (the ground and fixed tops are level: up, and no piece). */
838
+ __publicField(this, "supportNormal", UP);
839
+ __publicField(this, "supportFriction", null);
819
840
  this.p = new THREE.Vector3();
820
841
  this.q = new THREE.Quaternion();
821
842
  this.dq = new THREE.Quaternion();
@@ -921,8 +942,9 @@ var PieceSystem = class {
921
942
  const lz = (e[8] * x + e[9] * y + e[10] * z) * iz;
922
943
  return [e[0] * lx + e[4] * ly + e[8] * lz, e[1] * lx + e[5] * ly + e[9] * lz, e[2] * lx + e[6] * ly + e[10] * lz];
923
944
  }
924
- /** The points of the box that touch the ground, in its own frame: the 8 corners, then (with `edges`) `EDGE_POINTS` along each of its 12 edges. */
945
+ /** The points of the box that touch the ground, in its own frame: the 8 corners, then (with `edges`) `EDGE_POINTS` along each of its 12 edges; or the piece's own `contacts`. */
925
946
  samples(b, edges) {
947
+ if (b.contacts) return b.contacts;
926
948
  const [hx, hy, hz] = b.halfExtents;
927
949
  const out = [];
928
950
  for (let i = 0; i < 8; i++) out.push([i & 1 ? hx : -hx, i & 2 ? hy : -hy, i & 4 ? hz : -hz]);
@@ -987,6 +1009,10 @@ var PieceSystem = class {
987
1009
  }
988
1010
  if (b.sleeping) continue;
989
1011
  this.integrate(b, dt);
1012
+ if (b.sleeping) continue;
1013
+ const v = b.velocity;
1014
+ const w = b.angularVelocity;
1015
+ if (Math.hypot(v[0], v[1], v[2]) > WAKE_ABOVE_SPEED || Math.hypot(w[0], w[1], w[2]) > WAKE_ABOVE_SPIN) this.wakeAbove(b);
990
1016
  }
991
1017
  this.separate(dt);
992
1018
  }
@@ -1017,15 +1043,48 @@ var PieceSystem = class {
1017
1043
  }
1018
1044
  return false;
1019
1045
  }
1046
+ /** The highest top of a piece (other than `self`) under `(x, z)` that a point at height `y` stands on (at most `step` above it), -Infinity when none; the boxes of the last update. */
1047
+ topAt(x, z, y, step, self) {
1048
+ let top = -Infinity;
1049
+ for (const s of this.solids) {
1050
+ if (s.body.objectId === self || s.body.held || Math.abs(x - s.c[0]) > s.r || Math.abs(z - s.c[2]) > s.r) continue;
1051
+ const t = boxTopAt(s, x, z);
1052
+ if (t > top && t <= y + step) top = t;
1053
+ }
1054
+ return top;
1055
+ }
1056
+ /** What lay asleep on `b` (a box over its top) wakes at once when `b` moves: a can on a table that goes over goes with it. */
1057
+ wakeAbove(b) {
1058
+ const below = this.solids.find((s) => s.body === b);
1059
+ if (!below) return;
1060
+ const top = below.c[1] + below.half[0] * Math.abs(below.e[1]) + below.half[1] * Math.abs(below.e[5]) + below.half[2] * Math.abs(below.e[9]);
1061
+ for (const s of this.solids) {
1062
+ const o = s.body;
1063
+ if (!o.sleeping || o.held || o === b) continue;
1064
+ if (s.c[1] < below.c[1] || s.c[1] - s.r > top + STEP || Math.hypot(s.c[0] - below.c[0], s.c[2] - below.c[2]) > s.r + below.r) continue;
1065
+ o.sleeping = false;
1066
+ o.still = 0;
1067
+ o.calmAt = null;
1068
+ }
1069
+ }
1020
1070
  /** Where a corner of `self` at `(x, y, z)` lands: the ground, the top of another piece or of something fixed under it. */
1021
1071
  supportAt(self, x, y, z, step) {
1022
1072
  let top = this.o.groundAt(x, z);
1073
+ let on = null;
1074
+ let face = 1;
1023
1075
  for (const s of this.solids) {
1024
1076
  if (s.body === self || s.body.held || Math.abs(x - s.c[0]) > s.r || Math.abs(z - s.c[2]) > s.r) continue;
1025
1077
  const t = boxTopAt(s, x, z);
1026
- if (t > top && t <= y + step) top = t;
1078
+ if (t > top && t <= y + step) {
1079
+ top = t;
1080
+ on = s;
1081
+ face = topFace;
1082
+ }
1027
1083
  }
1028
1084
  const fixed = this.o.staticTopAt?.(x, z, y, step) ?? -Infinity;
1085
+ if (fixed >= top) on = null;
1086
+ this.supportNormal = on ? faceNormal(on, face) : UP;
1087
+ this.supportFriction = on?.body.friction ?? null;
1029
1088
  return Math.max(top, fixed);
1030
1089
  }
1031
1090
  /** Stops a piece that ran into something on its way from `start` to `pos` (`sweep`): moves it back to where it met it and takes the speed into it. */
@@ -1108,7 +1167,10 @@ var PieceSystem = class {
1108
1167
  const e = this.m.elements;
1109
1168
  const rs = [];
1110
1169
  const ids = [];
1170
+ const ns = [];
1171
+ const mus = [];
1111
1172
  let maxPen = 0;
1173
+ let penNormal = UP;
1112
1174
  const rise = STEP + Math.max(0, -v[1]) * h * 2;
1113
1175
  let side = null;
1114
1176
  let lowest = Infinity;
@@ -1126,6 +1188,8 @@ var PieceSystem = class {
1126
1188
  if (floor !== void 0 && floor - cy > -SLOP && floor < raw - STEP) {
1127
1189
  rs.push(r);
1128
1190
  ids.push(i);
1191
+ ns.push(UP);
1192
+ mus.push(pairFriction(b.friction, null));
1129
1193
  maxPen = Math.max(maxPen, floor - cy);
1130
1194
  }
1131
1195
  continue;
@@ -1146,12 +1210,19 @@ var PieceSystem = class {
1146
1210
  }
1147
1211
  continue;
1148
1212
  }
1149
- const gy = i < 8 ? this.supportAt(b, px, cy, pz, rise) : Math.max(raw, this.o.staticTopAt?.(px, pz, cy, rise) ?? -Infinity);
1213
+ this.supportNormal = UP;
1214
+ this.supportFriction = null;
1215
+ const gy = i < 8 || b.contacts ? this.supportAt(b, px, cy, pz, rise) : Math.max(raw, this.o.staticTopAt?.(px, pz, cy, rise) ?? -Infinity);
1150
1216
  const pen = gy - cy;
1151
1217
  if (pen > -SLOP) {
1152
1218
  rs.push(r);
1153
1219
  ids.push(i);
1154
- if (pen > maxPen) maxPen = pen;
1220
+ ns.push(this.supportNormal);
1221
+ mus.push(pairFriction(b.friction, this.supportFriction));
1222
+ if (pen > maxPen) {
1223
+ maxPen = pen;
1224
+ penNormal = this.supportNormal;
1225
+ }
1155
1226
  lowest = Math.min(lowest, gy);
1156
1227
  }
1157
1228
  }
@@ -1164,7 +1235,10 @@ var PieceSystem = class {
1164
1235
  contactedLast = rs.length > 0;
1165
1236
  if (contactedLast) touched = true;
1166
1237
  if (!contactedLast) continue;
1167
- pos[1] += Math.max(0, maxPen - SLOP * 0.5);
1238
+ const out = Math.max(0, maxPen - SLOP * 0.5) * penNormal[1];
1239
+ pos[0] += penNormal[0] * out;
1240
+ pos[1] += penNormal[1] * out;
1241
+ pos[2] += penNormal[2] * out;
1168
1242
  let impact2 = 0;
1169
1243
  for (const r of rs) impact2 = Math.max(impact2, -(v[1] + w[2] * r[0] - w[0] * r[2]));
1170
1244
  const restitution = impact2 > 2 ? 0.25 : 0;
@@ -1180,76 +1254,72 @@ var PieceSystem = class {
1180
1254
  const lz = pos[2] + sz / rs.length;
1181
1255
  landPoint = [lx, pos[1] - Math.max(...rs.map((r) => -r[1])), lz];
1182
1256
  }
1183
- const goal = rs.map((r) => {
1184
- const vy = v[1] + (w[2] * r[0] - w[0] * r[2]);
1185
- return vy < 0 ? -restitution * vy : 0;
1257
+ const pointV = (r) => [v[0] + w[1] * r[2] - w[2] * r[1], v[1] + w[2] * r[0] - w[0] * r[2], v[2] + w[0] * r[1] - w[1] * r[0]];
1258
+ const dot = (a, c2) => a[0] * c2[0] + a[1] * c2[1] + a[2] * c2[2];
1259
+ const goal = rs.map((r, k) => {
1260
+ const vn = dot(pointV(r), ns[k]);
1261
+ return vn < 0 ? -restitution * vn : 0;
1262
+ });
1263
+ const slipping = rs.map((r, k) => {
1264
+ const pv = pointV(r);
1265
+ const vn = dot(pv, ns[k]);
1266
+ return Math.hypot(pv[0] - vn * ns[k][0], pv[1] - vn * ns[k][1], pv[2] - vn * ns[k][2]) > SLIP_SPEED;
1186
1267
  });
1187
1268
  const vx0 = v[0];
1188
1269
  const vz0 = v[2];
1189
- if (!b.warm || b.warm.length !== WARM_SLOTS * 3) b.warm = new Float64Array(WARM_SLOTS * 3);
1270
+ if (!b.warm || b.warm.length !== WARM_SLOTS * 4) b.warm = new Float64Array(WARM_SLOTS * 4);
1190
1271
  const warm = b.warm;
1191
- const accN = ids.map((id) => id < WARM_SLOTS ? warm[id * 3] : 0);
1192
- const accT = ids.map((id) => id < WARM_SLOTS ? [warm[id * 3 + 1], warm[id * 3 + 2]] : [0, 0]);
1193
- for (let k = 0; k < rs.length; k++) {
1194
- const r = rs[k];
1195
- const jn = accN[k];
1196
- const [fx, fz] = accT[k];
1197
- if (jn === 0 && fx === 0 && fz === 0) continue;
1198
- v[0] += fx * invM;
1199
- v[1] += jn * invM;
1200
- v[2] += fz * invM;
1201
- const dw = this.mulInvInertiaWith(b, e, r[1] * fz - r[2] * jn, r[2] * fx - r[0] * fz, r[0] * jn - r[1] * fx);
1272
+ const accN = ids.map((id) => id < WARM_SLOTS ? warm[id * 4] : 0);
1273
+ const accT = ids.map((id) => id < WARM_SLOTS ? [warm[id * 4 + 1], warm[id * 4 + 2], warm[id * 4 + 3]] : [0, 0, 0]);
1274
+ const push = (r, j) => {
1275
+ v[0] += j[0] * invM;
1276
+ v[1] += j[1] * invM;
1277
+ v[2] += j[2] * invM;
1278
+ const dw = this.mulInvInertiaWith(b, e, r[1] * j[2] - r[2] * j[1], r[2] * j[0] - r[0] * j[2], r[0] * j[1] - r[1] * j[0]);
1202
1279
  w[0] += dw[0];
1203
1280
  w[1] += dw[1];
1204
1281
  w[2] += dw[2];
1282
+ };
1283
+ const reach = (r, d) => {
1284
+ const a = this.mulInvInertiaWith(b, e, r[1] * d[2] - r[2] * d[1], r[2] * d[0] - r[0] * d[2], r[0] * d[1] - r[1] * d[0]);
1285
+ return invM + (a[1] * r[2] - a[2] * r[1]) * d[0] + (a[2] * r[0] - a[0] * r[2]) * d[1] + (a[0] * r[1] - a[1] * r[0]) * d[2];
1286
+ };
1287
+ for (let k = 0; k < rs.length; k++) {
1288
+ const n = ns[k];
1289
+ const t = accT[k];
1290
+ if (accN[k] !== 0 || t[0] !== 0 || t[1] !== 0 || t[2] !== 0) push(rs[k], [n[0] * accN[k] + t[0], n[1] * accN[k] + t[1], n[2] * accN[k] + t[2]]);
1205
1291
  }
1206
1292
  for (let it = 0; it < SOLVER_PASSES; it++) {
1207
1293
  for (let k = 0; k < rs.length; k++) {
1208
1294
  const r = rs[k];
1209
- const vy = v[1] + (w[2] * r[0] - w[0] * r[2]);
1210
- const a = this.mulInvInertiaWith(b, e, -r[2], 0, r[0]);
1211
- const denom = invM + (a[2] * r[0] - a[0] * r[2]);
1212
- const total = Math.max(0, accN[k] + (goal[k] - vy) / denom);
1295
+ const n = ns[k];
1296
+ const vn = dot(pointV(r), n);
1297
+ const total = Math.max(0, accN[k] + (goal[k] - vn) / reach(r, n));
1213
1298
  const j = total - accN[k];
1214
1299
  accN[k] = total;
1215
- if (j !== 0) {
1216
- v[1] += j * invM;
1217
- w[0] += a[0] * j;
1218
- w[1] += a[1] * j;
1219
- w[2] += a[2] * j;
1220
- }
1221
- const vx = v[0] + (w[1] * r[2] - w[2] * r[1]);
1222
- const vz = v[2] + (w[0] * r[1] - w[1] * r[0]);
1223
- const vt = Math.hypot(vx, vz);
1224
- if (vt < 1e-6 && accT[k][0] === 0 && accT[k][1] === 0) continue;
1225
- let fx = accT[k][0];
1226
- let fz = accT[k][1];
1227
- if (vt > 1e-6) {
1228
- const tx = vx / vt;
1229
- const tz = vz / vt;
1230
- const at = this.mulInvInertiaWith(b, e, r[1] * tz, r[2] * tx - r[0] * tz, -r[1] * tx);
1231
- const cx = at[1] * r[2] - at[2] * r[1];
1232
- const cz = at[0] * r[1] - at[1] * r[0];
1233
- const jt = vt / (invM + cx * tx + cz * tz);
1234
- fx -= tx * jt;
1235
- fz -= tz * jt;
1300
+ if (j !== 0) push(r, [n[0] * j, n[1] * j, n[2] * j]);
1301
+ const pv = pointV(r);
1302
+ const pn = dot(pv, n);
1303
+ const vt = [pv[0] - pn * n[0], pv[1] - pn * n[1], pv[2] - pn * n[2]];
1304
+ const speed = Math.hypot(vt[0], vt[1], vt[2]);
1305
+ const f = [accT[k][0], accT[k][1], accT[k][2]];
1306
+ if (speed > 1e-6) {
1307
+ const d = [vt[0] / speed, vt[1] / speed, vt[2] / speed];
1308
+ const jt = speed / reach(r, d);
1309
+ f[0] -= d[0] * jt;
1310
+ f[1] -= d[1] * jt;
1311
+ f[2] -= d[2] * jt;
1312
+ } else if (f[0] === 0 && f[1] === 0 && f[2] === 0) continue;
1313
+ const most = (slipping[k] ? mus[k].dynamic : mus[k].static) * accN[k];
1314
+ const fl = Math.hypot(f[0], f[1], f[2]);
1315
+ if (fl > most) {
1316
+ f[0] *= most / fl;
1317
+ f[1] *= most / fl;
1318
+ f[2] *= most / fl;
1236
1319
  }
1237
- const most = MU * accN[k];
1238
- const f = Math.hypot(fx, fz);
1239
- if (f > most) {
1240
- fx *= most / f;
1241
- fz *= most / f;
1242
- }
1243
- const dx = fx - accT[k][0];
1244
- const dz = fz - accT[k][1];
1245
- accT[k] = [fx, fz];
1246
- if (dx === 0 && dz === 0) continue;
1247
- v[0] += dx * invM;
1248
- v[2] += dz * invM;
1249
- const dw = this.mulInvInertiaWith(b, e, r[1] * dz, r[2] * dx - r[0] * dz, -r[1] * dx);
1250
- w[0] += dw[0];
1251
- w[1] += dw[1];
1252
- w[2] += dw[2];
1320
+ const dj = [f[0] - accT[k][0], f[1] - accT[k][1], f[2] - accT[k][2]];
1321
+ accT[k] = f;
1322
+ if (dj[0] !== 0 || dj[1] !== 0 || dj[2] !== 0) push(r, dj);
1253
1323
  }
1254
1324
  }
1255
1325
  pos[0] += (v[0] - vx0) * h;
@@ -1258,9 +1328,10 @@ var PieceSystem = class {
1258
1328
  for (let k = 0; k < rs.length; k++) {
1259
1329
  const id = ids[k];
1260
1330
  if (id >= WARM_SLOTS) continue;
1261
- warm[id * 3] = accN[k];
1262
- warm[id * 3 + 1] = accT[k][0];
1263
- warm[id * 3 + 2] = accT[k][1];
1331
+ warm[id * 4] = accN[k];
1332
+ warm[id * 4 + 1] = accT[k][0];
1333
+ warm[id * 4 + 2] = accT[k][1];
1334
+ warm[id * 4 + 3] = accT[k][2];
1264
1335
  }
1265
1336
  let x0 = Infinity, x1 = -Infinity, z0 = Infinity, z1 = -Infinity, low = 0;
1266
1337
  for (const r of rs) {
@@ -1332,6 +1403,13 @@ var PieceSystem = class {
1332
1403
  b.calm += dt;
1333
1404
  return b.calm >= CALM_SLEEP_AFTER;
1334
1405
  }
1406
+ /** Whether the centre `c` of `upper` is over the top of `lower`'s box (as it was at the start of the update). */
1407
+ onTopOf(upper, lower, c) {
1408
+ const s = this.solids.find((one) => one.body === lower);
1409
+ if (!s) return false;
1410
+ const top = boxTopAt(s, c.x, c.z);
1411
+ return top > -Infinity && c.y >= top - 0.01;
1412
+ }
1335
1413
  /** Half the height of the box of a piece as it is turned now. */
1336
1414
  halfHeight(b) {
1337
1415
  const e = this.m.makeRotationFromQuaternion(b.root.quaternion).elements;
@@ -1357,6 +1435,7 @@ var PieceSystem = class {
1357
1435
  this.centerOf(b, cb);
1358
1436
  if (Math.abs(ca.y - cb.y) > ra + rb) continue;
1359
1437
  if (Math.abs(ca.y - cb.y) >= (this.halfHeight(a) + this.halfHeight(b)) * 0.9) continue;
1438
+ if (this.onTopOf(b, a, cb) || this.onTopOf(a, b, ca)) continue;
1360
1439
  const rh = (Math.max(a.halfExtents[0], a.halfExtents[2]) + Math.max(b.halfExtents[0], b.halfExtents[2])) * 0.8;
1361
1440
  let dx = cb.x - ca.x;
1362
1441
  let dz = cb.z - ca.z;
@@ -2764,6 +2843,9 @@ var WOOD_RIM = 1.5;
2764
2843
  var IMPACT_TAGS = ["metal", "glass", "wood"];
2765
2844
  var GLOW_FROM_C = 430;
2766
2845
  var QUEUE_CAP = 4e3;
2846
+ var TRICKLE_SECONDS = [2, 5];
2847
+ var TRICKLE_EVERY = 0.25;
2848
+ var TRICKLE_MAX = 24;
2767
2849
  var STRUCTURAL_EVENTS = /* @__PURE__ */ new Set([
2768
2850
  "fracture",
2769
2851
  "detach",
@@ -2787,6 +2869,10 @@ var STATIC_SPAN = 3;
2787
2869
  var FEET_BAND = 0.03;
2788
2870
  var TIP_MIN_HEIGHT = 0.3;
2789
2871
  var TIP_NARROW = 0.3;
2872
+ var CONTACT_MESHES = 12;
2873
+ var BROKEN_GAP = 0.03;
2874
+ var BROKEN_SAMPLES = [[0.5, 0.5], [0.1, 0.1], [0.9, 0.1], [0.1, 0.9], [0.9, 0.9]];
2875
+ var UNCHECKED_KINDS = /* @__PURE__ */ new Set(["structure", "room", "ground"]);
2790
2876
  var FIRE_CEILING_SECONDS = 3600;
2791
2877
  var GENERIC_REFERENCE_RADIUS = 15;
2792
2878
  var SWEEP_CORE = 0.15;
@@ -2886,10 +2972,14 @@ var Renderer = class {
2886
2972
  __publicField(this, "staticRound", []);
2887
2973
  /** Boxes of the pieces' meshes (as `staticBoxes`), with the time they were measured. */
2888
2974
  __publicField(this, "pieceBoxes", /* @__PURE__ */ new WeakMap());
2975
+ /** Objects broken since the last update: `checkBroken` looks at what is left of them. */
2976
+ __publicField(this, "broken", /* @__PURE__ */ new Set());
2889
2977
  __publicField(this, "staticAt", 0);
2890
2978
  __publicField(this, "queue", []);
2891
2979
  /** The cover of each body around a part of the world that went off, read as the event is emitted: before the blast tore or moved anything. */
2892
2980
  __publicField(this, "coverWhenWentOff", /* @__PURE__ */ new WeakMap());
2981
+ /** Places a blow broke a hard face at: grit and dust keep coming off them for a few seconds, less and less (`TRICKLE_SECONDS`). */
2982
+ __publicField(this, "trickles", []);
2893
2983
  /** Presentation work an event left for later frames (fragments still to spawn, loose groups still to split), run in order within the frame budget. */
2894
2984
  __publicField(this, "slices", []);
2895
2985
  /** When the work of this frame must stop; `Infinity` outside a drain, where a call finishes what it started. */
@@ -3232,10 +3322,12 @@ var Renderer = class {
3232
3322
  dt = clamp(dt, 0, 0.1);
3233
3323
  this.time += dt;
3234
3324
  if (!this.attached) this.drainQueue();
3325
+ this.checkBroken();
3235
3326
  for (const b of this.bounds.values()) b.onFrame?.(dt);
3236
3327
  for (const b of this.bounds.values()) if (b.soilNode && this.world.state(b.id)?.soil?.status === "yielding") this.onSettle(b.id);
3237
3328
  this.updateActive(dt);
3238
3329
  this.updateLeaks(dt);
3330
+ this.updateTrickles(dt);
3239
3331
  this.pieceSys.update(dt);
3240
3332
  this.fragments.update(dt);
3241
3333
  this.ground.update(dt);
@@ -3434,6 +3526,7 @@ var Renderer = class {
3434
3526
  }
3435
3527
  apply(ev) {
3436
3528
  this.staticDirty.add(ev.object);
3529
+ if (ev.type === "fracture" || ev.type === "detach" || ev.type === "collapse" || ev.type === "burnout" || ev.type === "destroyed") this.broken.add(ev.object);
3437
3530
  switch (ev.type) {
3438
3531
  case "damage":
3439
3532
  if (ev.damageType === "heat" || ev.damageType === "fire" || ev.damageType === "explosive") this.activate(ev.object, ev.part);
@@ -3758,9 +3851,34 @@ var Renderer = class {
3758
3851
  const core = def?.visual?.innerColor;
3759
3852
  if (core !== void 0) this.vfx.burst("chips", ev.point, [normal.x, normal.y, normal.z], 5, 2.5, { color: core });
3760
3853
  else this.playEvent({ kind: "spall", material: this.impactMaterial(def) }, ev.point);
3854
+ if (core !== void 0) this.trickleFrom(ev.point, [normal.x, normal.y, normal.z], core);
3761
3855
  if (mv.mesh.userData.drawnSpalls?.includes(ev.part)) return;
3762
3856
  this.visuals.addSpall(mv, wp, normal, Math.sqrt(ev.area / Math.PI), core ?? fallbackColor(pv.material, def));
3763
3857
  }
3858
+ /** Grit and dust go on falling from where a blow broke a hard face (masonry, concrete) for `TRICKLE_SECONDS`, a pinch at a time and less and less. */
3859
+ trickleFrom(point, normal, color) {
3860
+ if (this.trickles.length >= TRICKLE_MAX) this.trickles.shift();
3861
+ const total = TRICKLE_SECONDS[0] + (TRICKLE_SECONDS[1] - TRICKLE_SECONDS[0]) * this.rng();
3862
+ this.trickles.push({ point: [point[0], point[1], point[2]], normal, color, left: total, total, next: TRICKLE_EVERY });
3863
+ }
3864
+ updateTrickles(dt) {
3865
+ for (let i = this.trickles.length - 1; i >= 0; i--) {
3866
+ const t = this.trickles[i];
3867
+ t.left -= dt;
3868
+ t.next -= dt;
3869
+ if (t.left <= 0) {
3870
+ this.trickles.splice(i, 1);
3871
+ continue;
3872
+ }
3873
+ if (t.next > 0) continue;
3874
+ const share = t.left / t.total;
3875
+ t.next = TRICKLE_EVERY / Math.max(share, 0.25);
3876
+ const out = [t.normal[0] * 0.2, -1, t.normal[2] * 0.2];
3877
+ const options = t.color !== void 0 ? { color: t.color } : void 0;
3878
+ this.vfx.burst("chips", t.point, out, this.low ? 1 : 2, 0.3, options);
3879
+ if (this.rng() < 0.5) this.vfx.burst("dust", t.point, out, 1, 0.2, options);
3880
+ }
3881
+ }
3764
3882
  /**
3765
3883
  * Stage 2 of a staged part: the unit-sized fragments come out and the part stays drawn (hiding it is what `fracture` does). The opening itself
3766
3884
  * (cutting the wall's faces) is not drawn yet: it needs the per-vertex binding of the house renderer.
@@ -3796,6 +3914,7 @@ var Renderer = class {
3796
3914
  const tone3 = fr.inner && fdef?.visual?.innerColor !== void 0 ? fdef.visual.innerColor : fdef === def ? color : fallbackColor(fr.material, fdef);
3797
3915
  this.fragments.spawn(fr, this.fragments.materialFor(fr.material, tone3, false), ev.object, maxAlive);
3798
3916
  }
3917
+ if (def?.visual?.innerColor !== void 0 || pv?.material === "concrete" || pv?.material === "brick") this.trickleFrom(ev.center, [0, 0, 0], color);
3799
3918
  if (ev.torn) {
3800
3919
  this.impactSound(pv?.material ?? "", ev.center, 0.5, ev.blow);
3801
3920
  return;
@@ -3985,26 +4104,30 @@ var Renderer = class {
3985
4104
  this.o.onPiece?.(piece);
3986
4105
  }
3987
4106
  /**
3988
- * Furniture that lost what it stood on falls: with every leg of a table broken the top is loose, and with the legs of one side gone what is left topples. Feet are the
3989
- * meshes at the bottom of the object as it was whole; while the ones left still span the same floor (a cupboard with a broken door, a table missing one leg) it stands.
4107
+ * What lost what it stood on falls (furniture, a stand, a crate, a car; not a structure): with every leg of a table broken the top is loose, and with the legs of one side
4108
+ * gone what is left topples. Feet are the meshes at the bottom of the object as it was whole; while the ones left still span the same floor (a cupboard with a broken door,
4109
+ * a table missing one leg) it stands. A piece that lost a part is the box of what is left, touching the ground by the corners of each part (`setContacts`).
3990
4110
  */
3991
4111
  loosenIfFootless(objectId) {
3992
4112
  const b = this.bounds.get(objectId);
3993
4113
  if (!b || !this.autoSplit) return;
3994
4114
  const left = this.meshesOf(b).filter((mv) => this.visuals.shown(mv));
3995
4115
  if (left.length === 0) return;
4116
+ const structural = UNCHECKED_KINDS.has(this.kindOf(b));
3996
4117
  const body = b.piece ?? this.pieceSys.get(objectId);
3997
4118
  if (body) {
3998
4119
  const { half, center } = this.measure(b.root, left);
3999
4120
  body.halfExtents = half;
4000
4121
  this.pieceSys.setCenter(body, center);
4001
4122
  body.mass = Math.max(0.05, this.massOf(objectId));
4123
+ if (!structural) this.setContacts(b, body, left);
4002
4124
  body.sleeping = false;
4003
4125
  return;
4004
4126
  }
4005
- if (kitOriginOf(b.root)?.kind !== "furniture") return;
4127
+ if (structural) return;
4006
4128
  const boxOf = (mv) => this.visuals.worldBox(mv, new this.THREE.Box3());
4007
4129
  const whole = [...b.parts.values()].flatMap((pv) => pv.meshes).map(boxOf).filter((one) => !one.isEmpty());
4130
+ if (!this.standsOn(objectId, whole.map((one) => [one.min.x, one.min.y, one.min.z, one.max.x, one.max.y, one.max.z]))) return;
4008
4131
  const floor = Math.min(...whole.map((one) => one.min.y));
4009
4132
  const feetOf = (boxes) => boxes.filter((one) => one.min.y < floor + FEET_BAND).reduce((feet, one) => feet.union(one), new this.THREE.Box3());
4010
4133
  const was = feetOf(whole);
@@ -4020,9 +4143,116 @@ var Renderer = class {
4020
4143
  if (!narrow && centreX > now.min.x && centreX < now.max.x && centreZ > now.min.z && centreZ < now.max.z) return;
4021
4144
  }
4022
4145
  const piece = this.makePiece(objectId);
4146
+ this.setContacts(b, b.piece ?? piece, left);
4023
4147
  this.tipIfTopHeavy(b);
4024
4148
  this.o.onLoosened?.(objectId, piece, "apart");
4025
4149
  }
4150
+ shownBoxes(objectId, partId) {
4151
+ const b = this.bounds.get(objectId);
4152
+ if (!b) return [];
4153
+ const box = new this.THREE.Box3();
4154
+ const out = [];
4155
+ const pv = partId === void 0 ? null : b.parts.get(partId);
4156
+ if (partId !== void 0 && (!pv || pv.gone !== "none")) return out;
4157
+ for (const mv of pv ? pv.meshes : this.meshesOf(b)) {
4158
+ if (!this.visuals.shown(mv)) continue;
4159
+ this.visuals.worldBox(mv, box);
4160
+ if (!box.isEmpty()) out.push([box.min.x, box.min.y, box.min.z, box.max.x, box.max.y, box.max.z]);
4161
+ }
4162
+ return out;
4163
+ }
4164
+ /**
4165
+ * What is left of each object broken since the last update stands on something, or falls: an object that is not a piece whose lowest drawn box has neither the ground, a
4166
+ * fixed top nor a piece within `BROKEN_GAP` under it is made a piece on the spot (see `onBrokenChecked`).
4167
+ */
4168
+ checkBroken() {
4169
+ if (this.broken.size === 0) return;
4170
+ const ids = [...this.broken];
4171
+ this.broken.clear();
4172
+ for (const id of ids) {
4173
+ const released = this.releaseIfFloating(id);
4174
+ this.o.onBrokenChecked?.(id, released);
4175
+ }
4176
+ }
4177
+ releaseIfFloating(objectId) {
4178
+ const b = this.bounds.get(objectId);
4179
+ if (!b || b.piece || this.pieceSys.get(objectId) || !this.autoSplit || UNCHECKED_KINDS.has(this.kindOf(b))) return false;
4180
+ const boxes = this.shownBoxes(objectId);
4181
+ if (boxes.length === 0) return false;
4182
+ const whole = new this.THREE.Box3();
4183
+ const wholeBoxes = [];
4184
+ for (const pv of b.parts.values()) {
4185
+ for (const mv of pv.meshes) {
4186
+ this.visuals.worldBox(mv, whole);
4187
+ if (!whole.isEmpty()) wholeBoxes.push([whole.min.x, whole.min.y, whole.min.z, whole.max.x, whole.max.y, whole.max.z]);
4188
+ }
4189
+ }
4190
+ if (!this.standsOn(objectId, wholeBoxes) || this.standsOn(objectId, boxes)) return false;
4191
+ const piece = this.makePiece(objectId);
4192
+ this.setContacts(b, b.piece ?? piece, this.meshesOf(b).filter((mv) => this.visuals.shown(mv)));
4193
+ this.tipIfTopHeavy(b);
4194
+ this.o.onLoosened?.(objectId, piece, "apart");
4195
+ return true;
4196
+ }
4197
+ /** Whether the lowest of `boxes` (world boxes of the object `objectId`) has the ground, a fixed top or a piece within `BROKEN_GAP` under it. */
4198
+ standsOn(objectId, boxes) {
4199
+ if (boxes.length === 0) return false;
4200
+ const low = Math.min(...boxes.map((one) => one[1]));
4201
+ for (const [x0, y0, z0, x1, , z1] of boxes) {
4202
+ if (y0 > low + FEET_BAND) continue;
4203
+ for (const [fx, fz] of BROKEN_SAMPLES) {
4204
+ const x = x0 + (x1 - x0) * fx;
4205
+ const z = z0 + (z1 - z0) * fz;
4206
+ const under = Math.max(this.groundAt(x, z), this.staticTopAtBut(objectId, x, z, y0, BROKEN_GAP), this.pieceSys.topAt(x, z, y0, BROKEN_GAP, objectId));
4207
+ if (under >= y0 - BROKEN_GAP) return true;
4208
+ }
4209
+ }
4210
+ return false;
4211
+ }
4212
+ /** The kind of kit piece a bound object is, by the mark on its root or the nearest one above it (a house binds a group inside its marked root); '' when unmarked. */
4213
+ kindOf(b) {
4214
+ for (let o = b.root; o; o = o.parent) {
4215
+ const kind = kitOriginOf(o)?.kind;
4216
+ if (kind) return kind;
4217
+ }
4218
+ return "";
4219
+ }
4220
+ /** `staticTopAt` without the boxes of `objectId` itself. */
4221
+ staticTopAtBut(objectId, x, z, y, step) {
4222
+ let top = -Infinity;
4223
+ for (const [id, a] of this.staticNow()) {
4224
+ if (id === objectId || a.length === 0 || x < a[0] || x > a[3] || z < a[2] || z > a[5]) continue;
4225
+ for (let i = 6; i < a.length; i += 6) {
4226
+ if (x < a[i] || x > a[i + 3] || z < a[i + 2] || z > a[i + 5]) continue;
4227
+ const t = a[i + 4];
4228
+ if (t > top && t <= y + step) top = t;
4229
+ }
4230
+ }
4231
+ return top;
4232
+ }
4233
+ /**
4234
+ * The piece touches the ground by the corners of each part left of it, not by the box around them all: on one leg, or on the legs of one side, its centre of mass is off
4235
+ * what holds it and it goes over until the top meets the floor. Past `CONTACT_MESHES` meshes it keeps the corners of its box.
4236
+ */
4237
+ setContacts(b, body, meshes) {
4238
+ if (meshes.length > CONTACT_MESHES) {
4239
+ body.contacts = void 0;
4240
+ return;
4241
+ }
4242
+ const inv = specFrameInverse(this.THREE, b.root);
4243
+ const m4 = new this.THREE.Matrix4();
4244
+ const lb = new this.THREE.Box3();
4245
+ const [cx, cy, cz] = body.localCenter;
4246
+ const points = [];
4247
+ for (const mv of meshes) {
4248
+ mv.mesh.updateWorldMatrix(true, false);
4249
+ this.visuals.localBox(mv, lb);
4250
+ if (lb.isEmpty()) continue;
4251
+ lb.applyMatrix4(m4.multiplyMatrices(inv, mv.mesh.matrixWorld));
4252
+ for (let i = 0; i < 8; i++) points.push([(i & 1 ? lb.max.x : lb.min.x) - cx, (i & 2 ? lb.max.y : lb.min.y) - cy, (i & 4 ? lb.max.z : lb.min.z) - cz]);
4253
+ }
4254
+ body.contacts = points.length > 0 ? points : void 0;
4255
+ }
4026
4256
  /**
4027
4257
  * A piece of furniture standing on feet too close for its height (a board on two legs in a line, a lone leg) does not stay up: the box of a piece hides that its weight is up
4028
4258
  * high and its feet are thin, so it gets the start of the fall, across the line of its feet.
@@ -4314,6 +4544,7 @@ var Renderer = class {
4314
4544
  this.active.clear();
4315
4545
  for (const key of [...this.fires.keys()]) this.stopFire(key);
4316
4546
  this.leaks.length = 0;
4547
+ this.trickles.length = 0;
4317
4548
  for (const id of [...this.bounds.keys()]) if (this.bounds.has(id)) this.restore(id);
4318
4549
  for (const b of [...this.bounds.values()]) {
4319
4550
  if (b.isSplit) {
@@ -4582,6 +4813,21 @@ var FIRE_LOOP = {
4582
4813
  volume: 0,
4583
4814
  sources: ["sdk: the roar under the crackles of a fire"]
4584
4815
  };
4816
+ var SMOKE_HISS_LOOP = {
4817
+ id: "smoke_grenade_hiss",
4818
+ bus: "world",
4819
+ order: "chain_loop",
4820
+ source: { kind: "noise", loop: true },
4821
+ filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 1400 }, q: 0.7 }, { type: "lowpass", frequency: { set: 7e3 }, q: 0.5 }] },
4822
+ volume: 0,
4823
+ sources: ["sdk: a smoke canister venting (the gas through its holes, steady for its whole burn)"]
4824
+ };
4825
+ var GRENADE_PIN_PULL = blast(
4826
+ "grenade_pin_pull",
4827
+ [noise({ type: "highpass", f: [3200, 4200], d: 0.025, vol: 0.5 }), noise({ type: "bandpass", f: [2200, 2800], q: 2, d: 0.04, vol: 0.4, delay: 0.18 })],
4828
+ ["sdk: the ring pulled from the fuse, then the spoon flying off"],
4829
+ FIRE_FALLOFF
4830
+ );
4585
4831
  var FIRE_FUEL_OF = {
4586
4832
  wood_soft: "wood",
4587
4833
  wood_hard: "wood",
@@ -4771,7 +5017,7 @@ var IMPACT_SOUNDS = [
4771
5017
  impact("plastic_crack", [noise2({ type: "bandpass", f: 1800, q: 2, d: 0.06, vol: 0.75 }), tone2({ f: [450, 550], f2: 250, d: 0.05, vol: 0.22 })]),
4772
5018
  impact("mdf_crumble", [noise2({ type: "lowpass", f: 700, d: 0.25, vol: 0.75, wet: 0.2 }), ...many2(4, () => noise2({ type: "lowpass", f: 500, d: 0.05, vol: 0.32, delay: [0.05, 0.3] }))])
4773
5019
  ];
4774
- var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, ...LEAF_SOUNDS, ...IMPACT_SOUNDS, EXPLOSION_BOOM, ...BLAST_SOUNDS, ...FIRE_SOUNDS], loops: [BURN_LOOP, FIRE_LOOP, ...FIRE_FUEL_LOOPS] };
5020
+ var DESTRUCTION_SOUND_BANK = { id: "destruction_three", sounds: [...SCENE_SOUNDS, ...DRAG_SOUNDS, ...LEAF_SOUNDS, ...IMPACT_SOUNDS, EXPLOSION_BOOM, ...BLAST_SOUNDS, ...FIRE_SOUNDS, GRENADE_PIN_PULL], loops: [BURN_LOOP, FIRE_LOOP, ...FIRE_FUEL_LOOPS, SMOKE_HISS_LOOP] };
4775
5021
 
4776
5022
  // src/audio/sound.ts
4777
5023
  import { FOLEY_BANK, GAME_SFX_BANK } from "@adatechnology/game-audio";
@@ -5106,10 +5352,12 @@ export {
5106
5352
  FIRE_FALLOFF,
5107
5353
  FIRE_LOOP,
5108
5354
  FIRE_SOUNDS,
5355
+ GRENADE_PIN_PULL,
5109
5356
  IMPACT_FAMILY_TAGS,
5110
5357
  IMPACT_SOUNDS,
5111
5358
  IMPACT_SOUND_KEYS,
5112
5359
  SCENE_SOUNDS,
5360
+ SMOKE_HISS_LOOP,
5113
5361
  blastFireSampleBank,
5114
5362
  breakSoundKeys,
5115
5363
  createDestructionRenderer,