@adatechnology/game-destruction 0.11.0 → 0.13.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.d.ts CHANGED
@@ -452,6 +452,11 @@ interface DestructionWorld {
452
452
  * that target, and a `blast` event sums it up.
453
453
  */
454
454
  explode(point: Vec3, energy: number, radius: number, source?: string, options?: BlastOptions): HitResult[];
455
+ /** Cost of the last `explode`: parts in range of the broad phase (`partsTested`) and those whose bounding sphere the blast reached (`partsNear`). */
456
+ blastProbe(): {
457
+ partsTested: number;
458
+ partsNear: number;
459
+ };
455
460
  /** Raycast against every part shape (world space). */
456
461
  raycast(origin: Vec3, direction: Vec3, maxDistance?: number): RayHit | null;
457
462
  /**
@@ -550,6 +555,8 @@ interface PartRuntime {
550
555
  breach?: Array<[number, number, number, number]>;
551
556
  /** Energy each unit of the part took so far (`breach.chipEnergy`), by its index in the unit grid; -1 when the unit came out. */
552
557
  chips?: Record<number, number>;
558
+ /** Face area (m²) the craters of shots took out of each unit so far, by its index in the unit grid: the rest of a unit comes out past `CRATER_SPENT`. */
559
+ craters?: Record<number, number>;
553
560
  /** Mass in kg that left the part in a breach. */
554
561
  removed?: number;
555
562
  /** Damage taken by each band across a long wooden part (`splinter`), by band index from its negative end: the band that runs out breaks it there. */
@@ -1974,6 +1974,7 @@ var GameDestruction = (() => {
1974
1974
  },
1975
1975
  hitDebris: "chips",
1976
1976
  breakDebris: "dust",
1977
+ visual: { innerColor: 10197652 },
1977
1978
  thermal: {
1978
1979
  conductivity: 0.15,
1979
1980
  heatCapacity: 2.5
@@ -2004,6 +2005,7 @@ var GameDestruction = (() => {
2004
2005
  ],
2005
2006
  hitDebris: "chips",
2006
2007
  breakDebris: "dust",
2008
+ visual: { innerColor: 12085567 },
2007
2009
  thermal: {
2008
2010
  conductivity: 0.15,
2009
2011
  heatCapacity: 2.5
@@ -2034,6 +2036,7 @@ var GameDestruction = (() => {
2034
2036
  ],
2035
2037
  hitDebris: "dust",
2036
2038
  breakDebris: "dust",
2039
+ visual: { innerColor: 14999507 },
2037
2040
  thermal: {
2038
2041
  conductivity: 0.1,
2039
2042
  heatCapacity: 1.5
@@ -2059,6 +2062,7 @@ var GameDestruction = (() => {
2059
2062
  ],
2060
2063
  hitDebris: "chips",
2061
2064
  breakDebris: "dust",
2065
+ visual: { innerColor: 9407364 },
2062
2066
  thermal: {
2063
2067
  conductivity: 0.2,
2064
2068
  heatCapacity: 2.5
@@ -2963,6 +2967,7 @@ var GameDestruction = (() => {
2963
2967
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
2964
2968
  var VENT_DIAMETER = 8e-3;
2965
2969
  var FLAME_LIGHTS_LEAK_C = 470;
2970
+ var RESTING_HOLD_COS = Math.cos(Math.PI / 6);
2966
2971
  var BREACH_RADIUS = 0.55;
2967
2972
  var BREACH_BLAST_FACTOR = 6;
2968
2973
  var BREACH_AREA = 0.25;
@@ -2978,7 +2983,9 @@ var GameDestruction = (() => {
2978
2983
  var BREACH_GRID_CAP = 4096;
2979
2984
  var CHIP_ENERGY = 200;
2980
2985
  var CHIP_SPLASH = 0.5;
2986
+ var CRATER_SPENT = 0.6;
2981
2987
  var BAR_BOW = 0.08;
2988
+ var BULLET_JOINT_SPAN = 0.3;
2982
2989
  var CUT_SLICE = 1;
2983
2990
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
2984
2991
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -2997,12 +3004,22 @@ var GameDestruction = (() => {
2997
3004
  const cells = p.st.chipped;
2998
3005
  if (!cells?.length) return 0;
2999
3006
  const [u, v] = planeAxes(p.thin);
3000
- const along = p.dims[u] >= p.dims[v] ? u : v;
3007
+ const role = p.spec.structure?.role;
3008
+ const up = rotateVec(p.q, [0, 0, 0].map((_, k) => k === v ? 1 : 0));
3009
+ const upright = Math.abs(up[1]) > 0.7 ? v : Math.abs(rotateVec(p.q, [0, 0, 0].map((_, k) => k === u ? 1 : 0))[1]) > 0.7 ? u : -1;
3010
+ const along = (role === "wall" || role === "partition" || role === "column") && upright >= 0 ? upright : p.dims[u] >= p.dims[v] ? u : v;
3001
3011
  const across = along === u ? v : u;
3002
3012
  let worst = 0;
3003
3013
  for (const c of cells) {
3004
- let gone = 0;
3005
- for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across];
3014
+ const spans = [];
3015
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) spans.push([d.center[across] - d.size[across] / 2, d.center[across] + d.size[across] / 2]);
3016
+ spans.sort((a, b) => a[0] - b[0]);
3017
+ let gone = 0, end = -Infinity;
3018
+ for (const [a, b] of spans) {
3019
+ if (b <= end) continue;
3020
+ gone += b - Math.max(a, end);
3021
+ end = b;
3022
+ }
3006
3023
  worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
3007
3024
  }
3008
3025
  return Math.min(1, worst);
@@ -3298,7 +3315,18 @@ var GameDestruction = (() => {
3298
3315
  if (k < 0 || j.spec.kind !== "tie") return k;
3299
3316
  return partXf(o, o.parts[k]).c[1] > partXf(o, o.parts[r]).c[1] + 1e-6 ? k : -1;
3300
3317
  };
3318
+ let poseVersion = 0;
3319
+ const contactMemo = /* @__PURE__ */ new WeakMap();
3301
3320
  const contactBox = (p, support) => {
3321
+ let byTouching = contactMemo.get(p);
3322
+ if (!byTouching) contactMemo.set(p, byTouching = /* @__PURE__ */ new Map());
3323
+ const known = byTouching.get(support);
3324
+ if (known && known.version === poseVersion) return known.box;
3325
+ const box = measureContact(p, support);
3326
+ byTouching.set(support, { version: poseVersion, box });
3327
+ return box;
3328
+ };
3329
+ const measureContact = (p, support) => {
3302
3330
  const A = aabbOf(p.spec.shape, p.pos, p.q), B = aabbOf(support.spec.shape, support.pos, support.q);
3303
3331
  const lo = [0, 0, 0], hi = [0, 0, 0];
3304
3332
  for (let k = 0; k < 3; k++) {
@@ -4093,7 +4121,7 @@ var GameDestruction = (() => {
4093
4121
  p.st.holes.push(hole);
4094
4122
  if (p.st.holes.length > maxHoles) p.st.holes.shift();
4095
4123
  markDamaged(p);
4096
- emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {} });
4124
+ emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {}, ...p.spec.powder ? { powder: { ...p.spec.powder } } : {} });
4097
4125
  setStage(o, p, 1, {}, src);
4098
4126
  return hole;
4099
4127
  };
@@ -4105,11 +4133,27 @@ var GameDestruction = (() => {
4105
4133
  emit({ ...base(o, src), type: "crack", part: p.spec.id, crack, worldPoint: wp });
4106
4134
  setStage(o, p, 1, {}, src);
4107
4135
  };
4108
- const debris = (o, p, kind, point, normal, count, speed, src) => emit({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed });
4136
+ const debris = (o, p, kind, point, normal, count, speed, src, material) => emit({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed, ...material && material !== p.spec.material ? { material } : {} });
4109
4137
  const holeRate = (hole, pressurized) => (pressurized ? 0.5 : 0.1) * (hole.diameter / 9e-3);
4110
4138
  const detachEvents = (o, opened, by, src) => {
4111
4139
  for (const j of opened) emit({ ...base(o, src), type: "detach", joint: j.spec.id, parts: [j.spec.a, j.spec.b], by, groups: o.st.groups.map((g) => [...g]) });
4112
4140
  };
4141
+ const slideOffResting = (o, rotation) => {
4142
+ const upY = 1 - 2 * (rotation[0] * rotation[0] + rotation[2] * rotation[2]);
4143
+ if (upY >= RESTING_HOLD_COS) return;
4144
+ const opened = o.joints.filter((j) => j.spec.kind === "resting" && !j.st.open);
4145
+ if (opened.length === 0) return;
4146
+ run(() => {
4147
+ for (const j of opened) {
4148
+ j.st.open = true;
4149
+ j.st.openedBy = "break";
4150
+ }
4151
+ solveStructure(o);
4152
+ recomputeGroups(o);
4153
+ detachEvents(o, opened, "break");
4154
+ collapseCheck(o);
4155
+ });
4156
+ };
4113
4157
  const checkDestroyed = (o, src) => {
4114
4158
  if (!o.st.destroyed && o.parts.length > 0 && o.parts.every(isLost)) {
4115
4159
  o.st.destroyed = true;
@@ -4146,7 +4190,13 @@ var GameDestruction = (() => {
4146
4190
  const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
4147
4191
  return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
4148
4192
  };
4149
- const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false) => {
4193
+ const jointLength = (o, j) => {
4194
+ const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
4195
+ let len = 0;
4196
+ for (let k = 0; k < 3; k++) len = Math.max(len, Math.min(A.max[k], B.max[k]) - Math.max(A.min[k], B.min[k]));
4197
+ return len;
4198
+ };
4199
+ const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false, span) => {
4150
4200
  if (amount <= 0) return;
4151
4201
  const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
4152
4202
  if (split && mine.length) amount /= mine.length;
@@ -4154,7 +4204,7 @@ var GameDestruction = (() => {
4154
4204
  if (j.st.open) continue;
4155
4205
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
4156
4206
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
4157
- j.st.hp -= amount;
4207
+ j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
4158
4208
  if (j.st.hp <= 0) {
4159
4209
  j.st.hp = 0;
4160
4210
  j.st.open = true;
@@ -4183,7 +4233,7 @@ var GameDestruction = (() => {
4183
4233
  if (spalls.length > MAX_SPALLS) spalls.shift();
4184
4234
  markDamaged(p);
4185
4235
  emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: layer.index, point: wp, area: Math.PI * radius * radius });
4186
- debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
4236
+ debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src, layer.mat.id);
4187
4237
  setStage(o, p, 1, {}, src);
4188
4238
  };
4189
4239
  const crater = (o, p, wp, normal, radius, src) => {
@@ -4204,7 +4254,7 @@ var GameDestruction = (() => {
4204
4254
  const localHolesOf = (p) => p.st.holes.map((h) => ({ point: toLocal(p.pos, p.q, h.point), normal: rotateVec(quat.conjugate(p.q), h.normal), diameter: h.diameter }));
4205
4255
  const unitsOf = (p) => p.spec.units ?? { size: [p.mat.fragmentSize?.[1] ?? 0.2, p.mat.fragmentSize?.[1] ?? 0.2, p.thickness], bond: "stack" };
4206
4256
  const FRAGMENT_SHAPE = { shatter: "shard", splinter: "splinter", tear: "scrap" };
4207
- const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag) => {
4257
+ const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag, inner = false) => {
4208
4258
  const { c, q } = partXf(o, p);
4209
4259
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
4210
4260
  const [u, v] = planeAxes(p.thin);
@@ -4228,7 +4278,8 @@ var GameDestruction = (() => {
4228
4278
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
4229
4279
  angularVelocity: [(rnd() - 0.5) * 8, (rnd() - 0.5) * 8, (rnd() - 0.5) * 8],
4230
4280
  mass: mass * faceArea(cell) / total,
4231
- ...marks.length > 0 ? { marks } : {}
4281
+ ...marks.length > 0 ? { marks } : {},
4282
+ ...inner ? { inner } : {}
4232
4283
  };
4233
4284
  });
4234
4285
  };
@@ -4269,7 +4320,7 @@ var GameDestruction = (() => {
4269
4320
  return cells.some((c) => Math.abs(lp[u] - c.center[u]) <= c.size[u] / 2 && Math.abs(lp[v] - c.center[v]) <= c.size[v] / 2);
4270
4321
  };
4271
4322
  const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
4272
- var _a, _b;
4323
+ var _a, _b, _c;
4273
4324
  if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
4274
4325
  const rule = p.mat.hits?.[kind];
4275
4326
  const perKg = rule?.unitJoules ?? chipEnergy;
@@ -4279,7 +4330,9 @@ var GameDestruction = (() => {
4279
4330
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
4280
4331
  const grid = gridOf(p);
4281
4332
  const worn = (_a = p.rt).chips ?? (_a.chips = {});
4333
+ const cut = (_b = p.rt).craters ?? (_b.craters = {});
4282
4334
  const out = [];
4335
+ const shot = kind === "ballistic";
4283
4336
  for (let i = 0; i < grid.length; i++) {
4284
4337
  const cell = grid[i];
4285
4338
  if (worn[i] === -1) continue;
@@ -4292,18 +4345,39 @@ var GameDestruction = (() => {
4292
4345
  worn[i] = total;
4293
4346
  continue;
4294
4347
  }
4295
- worn[i] = -1;
4296
- out.push(cell);
4348
+ if (!shot) {
4349
+ worn[i] = -1;
4350
+ out.push(cell);
4351
+ continue;
4352
+ }
4353
+ const size = [cell.size[0], cell.size[1], cell.size[2]];
4354
+ const center = [cell.center[0], cell.center[1], cell.center[2]];
4355
+ for (const a of [u, v]) {
4356
+ const w = Math.min(cell.size[a], craterAcross(hitEnergy) * (0.85 + 0.3 * rnd()));
4357
+ const lo = cell.center[a] - cell.size[a] / 2 + w / 2, hi = cell.center[a] + cell.size[a] / 2 - w / 2;
4358
+ size[a] = w;
4359
+ center[a] = clamp2(lp[a], lo, hi);
4360
+ }
4361
+ const taken2 = (cut[i] ?? 0) + size[u] * size[v];
4362
+ if (taken2 >= cell.size[u] * cell.size[v] * CRATER_SPENT) {
4363
+ worn[i] = -1;
4364
+ out.push({ ...cell, crater: true });
4365
+ continue;
4366
+ }
4367
+ worn[i] = 0;
4368
+ cut[i] = taken2;
4369
+ out.push({ center, size, crater: true });
4297
4370
  }
4298
4371
  if (out.length === 0) return;
4299
4372
  const { c, q } = partXf(o, p);
4300
- const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
4373
+ const chipped = (_c = p.st).chipped ?? (_c.chipped = []);
4301
4374
  const holes = localHolesOf(p);
4302
4375
  for (const cell of out) {
4303
4376
  const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
4304
4377
  const center = toWorld(c, q, cell.center);
4305
4378
  const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
4306
- const fragments = cellFragments(o, p, [cell], wp, dir, speed, mass, `c${chipped.length}_`);
4379
+ const tag = `c${chipped.length}_`;
4380
+ const fragments = cell.crater ? craterFragments(o, p, cell, dir, speed, mass, tag) : cellFragments(o, p, [cell], wp, dir, speed, mass, tag, true);
4307
4381
  p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
4308
4382
  chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
4309
4383
  p.st.mass = Math.max(0, p.st.mass - mass);
@@ -4320,6 +4394,44 @@ var GameDestruction = (() => {
4320
4394
  collapseCheck(o, src);
4321
4395
  }
4322
4396
  };
4397
+ const craterAcross = (energy) => clamp2(0.03 + 12e-4 * Math.sqrt(Math.max(0, energy)), 0.03, 0.14);
4398
+ const craterFragments = (o, p, cell, dir, speed, mass, tag) => {
4399
+ const { c, q } = partXf(o, p);
4400
+ const [u, v] = planeAxes(p.thin);
4401
+ const d = dir ?? rotateVec(q, [p.thin === 0 ? 1 : 0, p.thin === 1 ? 1 : 0, p.thin === 2 ? 1 : 0]);
4402
+ const exitSign = Math.sign(rotateVec(quat.conjugate(q), d)[p.thin]) || 1;
4403
+ const bit = Math.max(0.01, Math.min(cell.size[u], cell.size[v]) / 3);
4404
+ const n = clamp2(Math.round(cell.size[u] * cell.size[v] / (bit * bit * 3)), 4, 10) + Math.floor(rnd() * 3);
4405
+ const out = [];
4406
+ for (let i = 0; i < n; i++) {
4407
+ const back = i % 3 === 2;
4408
+ const lc = [cell.center[0], cell.center[1], cell.center[2]];
4409
+ lc[u] += (rnd() - 0.5) * cell.size[u] * 0.7;
4410
+ lc[v] += (rnd() - 0.5) * cell.size[v] * 0.7;
4411
+ lc[p.thin] += (back ? -exitSign : exitSign) * (p.thickness / 2 + 0.01);
4412
+ const size = [0, 0, 0];
4413
+ size[u] = bit * (0.6 + 0.8 * rnd());
4414
+ size[v] = bit * (0.5 + 0.7 * rnd());
4415
+ size[p.thin] = bit * (0.4 + 0.6 * rnd());
4416
+ const spread3 = [(rnd() - 0.5) * 0.9, (rnd() - 0.3) * 0.9, (rnd() - 0.5) * 0.9];
4417
+ const k = speed * (back ? 0.4 + 0.4 * rnd() : 0.7 + 0.8 * rnd());
4418
+ const vdir = vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3));
4419
+ out.push({
4420
+ id: `${p.spec.id}#${tag}${i}`,
4421
+ part: p.spec.id,
4422
+ material: p.spec.material,
4423
+ shape: "chunk",
4424
+ center: toWorld(c, q, lc),
4425
+ size,
4426
+ rotation: q,
4427
+ velocity: [vdir[0] * k, vdir[1] * k + 0.5 * rnd(), vdir[2] * k],
4428
+ angularVelocity: [(rnd() - 0.5) * 20, (rnd() - 0.5) * 20, (rnd() - 0.5) * 20],
4429
+ mass: mass / n,
4430
+ inner: true
4431
+ });
4432
+ }
4433
+ return out;
4434
+ };
4323
4435
  const exposeRebar = (o, p, src) => {
4324
4436
  const r = p.spec.reinforcement;
4325
4437
  if (!r) return;
@@ -4607,7 +4719,7 @@ var GameDestruction = (() => {
4607
4719
  });
4608
4720
  }
4609
4721
  const patch = [scale * 4, scale * 4, scale * 4];
4610
- emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry, size: patch, rotation: q, fragments });
4722
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry, size: patch, rotation: q, fragments, torn: true });
4611
4723
  };
4612
4724
  const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src, round = {}) => {
4613
4725
  const mat = p.mat;
@@ -4666,12 +4778,13 @@ var GameDestruction = (() => {
4666
4778
  } else if (mat.ductility >= 0.5) {
4667
4779
  addDent(o, p, entry, n, Math.max(caliber * 3 / 1e3, 0.012), clamp2(Math.min(Math.max(p.thickness * 3, 3e-3), ratio * 0.01), 1e-3, 0.05), src);
4668
4780
  } else if (brittle) {
4669
- addCrack(o, p, entry, n, crackLen, src);
4781
+ if (!p.spec.units) addCrack(o, p, entry, n, crackLen, src);
4670
4782
  if (mat.fracture !== "shatter") addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
4671
4783
  } else addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
4672
4784
  debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
4673
- wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
4674
- const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
4785
+ wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
4786
+ const hollowTear = mat.fracture === "tear" && p.spec.fill !== void 0 && p.spec.fill < 0.5;
4787
+ const absorbed = penetrated && (mat.ductility >= 0.5 && mat.fracture !== "tear" || hollowTear) ? Math.min(E, thr) : E;
4675
4788
  const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry, d, absorbed * 0.1, 12, src);
4676
4789
  if (!isLost(p)) tearAt(o, p, "ballistic", entry, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
4677
4790
  chipUnits(o, p, "ballistic", entry, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
@@ -4756,10 +4869,25 @@ var GameDestruction = (() => {
4756
4869
  if (dist <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
4757
4870
  return reach;
4758
4871
  };
4872
+ const blastProbe = { partsTested: 0, partsNear: 0 };
4873
+ const blastCandidates = (o, point, radius) => {
4874
+ const local = toLocal(o.st.position, o.st.rotation, point);
4875
+ const near = [];
4876
+ for (const p of o.parts) {
4877
+ const reach = radius + p.bound;
4878
+ const dx = local[0] - p.pos[0];
4879
+ const dy = local[1] - p.pos[1];
4880
+ const dz = local[2] - p.pos[2];
4881
+ if (dx * dx + dy * dy + dz * dz < reach * reach) near.push(p);
4882
+ }
4883
+ blastProbe.partsTested += o.parts.length;
4884
+ blastProbe.partsNear += near.length;
4885
+ return near;
4886
+ };
4759
4887
  const blastObject = (o, point, energy, radius, src) => {
4760
4888
  const start = cur.length;
4761
4889
  const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
4762
- for (const p of o.parts) blastPart(o, p, point, energy, radius, src, res);
4890
+ for (const p of blastCandidates(o, point, radius)) blastPart(o, p, point, energy, radius, src, res);
4763
4891
  res.events = cur.slice(start);
4764
4892
  return res;
4765
4893
  };
@@ -4883,7 +5011,7 @@ var GameDestruction = (() => {
4883
5011
  let order = 0;
4884
5012
  for (const o of inRange) {
4885
5013
  if (only && only !== o) continue;
4886
- for (const p of o.parts) {
5014
+ for (const p of blastCandidates(o, point, radius)) {
4887
5015
  if (isLost(p)) continue;
4888
5016
  const dist = distToPart(o, p, point);
4889
5017
  if (dist < radius) items.push({ dist, order: order++, o, p });
@@ -5430,6 +5558,7 @@ var GameDestruction = (() => {
5430
5558
  const p = getPart(o, partId);
5431
5559
  p.pos = [position[0], position[1], position[2]];
5432
5560
  p.q = q;
5561
+ poseVersion++;
5433
5562
  updateBound(o);
5434
5563
  },
5435
5564
  remove(id) {
@@ -5448,6 +5577,7 @@ var GameDestruction = (() => {
5448
5577
  const o = getObj(id);
5449
5578
  o.base = { pos: [position[0], position[1], position[2]], rot: [rotation[0], rotation[1], rotation[2], rotation[3]] };
5450
5579
  applySoilPose(o);
5580
+ slideOffResting(o, rotation);
5451
5581
  },
5452
5582
  splitGroup(id, group, newId) {
5453
5583
  if (impls.has(id)) throw new Error(`object "${id}" is a registered DestructibleImpl and cannot be split; its module owns its pieces`);
@@ -5471,7 +5601,7 @@ var GameDestruction = (() => {
5471
5601
  if (o.spec.name !== void 0) s.name = o.spec.name;
5472
5602
  if (o.spec.tags) s.tags = [...o.spec.tags];
5473
5603
  if (o.spec.soil !== void 0) s.soil = o.spec.soil;
5474
- return clone(s);
5604
+ return s;
5475
5605
  };
5476
5606
  const inNew = (pid) => moved.has(pid);
5477
5607
  const inOld = (pid) => !moved.has(pid);
@@ -5498,7 +5628,12 @@ var GameDestruction = (() => {
5498
5628
  const p = getPart(o, partId);
5499
5629
  return whole(() => doHit(o, h, p));
5500
5630
  },
5631
+ blastProbe() {
5632
+ return { ...blastProbe };
5633
+ },
5501
5634
  explode(point, energy, radius, source, blast) {
5635
+ blastProbe.partsTested = 0;
5636
+ blastProbe.partsNear = 0;
5502
5637
  return withCollapses(run(() => blastWorld(point, energy, radius, source, blast)));
5503
5638
  },
5504
5639
  raycast(origin, direction, maxDistance = Infinity) {
@@ -5508,7 +5643,8 @@ var GameDestruction = (() => {
5508
5643
  sweepBox(box, direction, distance, options2 = {}) {
5509
5644
  const d = vec.normalize(direction);
5510
5645
  const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
5511
- const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
5646
+ const hosted = options2.ignore === void 0 || ignored ? void 0 : impls.get(options2.ignore);
5647
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : hosted ? new Set(hosted.impl.describe().parts.map((p) => `${hosted.impl.id}\0${p.id}`)) : void 0;
5512
5648
  let best = null;
5513
5649
  for (const origin of sweepOrigins(box, d, options2.spacing ?? 0.2)) {
5514
5650
  const r = raycastInternal(origin, d, distance + SWEEP_BACK, ignore);