@adatechnology/game-destruction 0.4.2 → 0.6.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.
@@ -21,17 +21,21 @@ var GameDestruction = (() => {
21
21
  // src/index.ts
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
+ ASSEMBLIES: () => ASSEMBLIES,
24
25
  FIRE_PRIME_C: () => FIRE_PRIME_C,
25
26
  IMPACT_CONTRACT: () => IMPACT_CONTRACT,
26
27
  JOINT_BY_MATERIALS: () => JOINT_BY_MATERIALS,
27
28
  JOINT_DEFAULTS: () => JOINT_DEFAULTS,
28
29
  MATERIALS: () => MATERIALS,
30
+ assemblyDepth: () => assemblyDepth,
31
+ assemblyPart: () => assemblyPart,
29
32
  buildFrame: () => buildFrame,
30
33
  checkDestructible: () => checkDestructible2,
31
34
  checkHit: () => checkHit,
32
35
  createDestruction: () => createDestruction,
33
36
  explosivePart: () => explosivePart,
34
37
  fragility: () => fragility,
38
+ getAssembly: () => getAssembly,
35
39
  guessMaterial: () => guessMaterial,
36
40
  inferJoints: () => inferJoints,
37
41
  locales: () => locales,
@@ -41,6 +45,7 @@ var GameDestruction = (() => {
41
45
  partVolume: () => partVolume,
42
46
  quat: () => quat,
43
47
  quatFromEuler: () => quatFromEuler,
48
+ registerAssembly: () => registerAssembly,
44
49
  resolveStrings: () => resolveStrings,
45
50
  rotateVec: () => rotateVec,
46
51
  sizeStructure: () => sizeStructure,
@@ -470,6 +475,153 @@ var GameDestruction = (() => {
470
475
  return out;
471
476
  };
472
477
 
478
+ // src/breach.ts
479
+ var planeAxes = (thin) => thin === 1 ? [0, 2] : [thin === 0 ? 2 : 0, 1];
480
+ var unitGrid = (dims, thin, units, cap) => {
481
+ const [u, v] = planeAxes(thin);
482
+ let unitU = Math.min(units.size[0], dims[u]), unitV = Math.min(units.size[1], dims[v]);
483
+ const rowsFor = (h) => Math.ceil(dims[v] / h - 1e-9);
484
+ const colsFor = (w) => Math.ceil(dims[u] / w - 1e-9) + (units.bond === "running" ? 1 : 0);
485
+ const merge = Math.ceil(Math.sqrt(rowsFor(unitV) * colsFor(unitU) / cap));
486
+ if (merge > 1) {
487
+ unitU = Math.min(unitU * merge, dims[u]);
488
+ unitV = Math.min(unitV * merge, dims[v]);
489
+ }
490
+ const cells = [];
491
+ for (let row = 0; row < rowsFor(unitV); row++) {
492
+ const v0 = -dims[v] / 2 + row * unitV, v1 = Math.min(v0 + unitV, dims[v] / 2);
493
+ const shift = units.bond === "running" && row % 2 === 1 ? unitU / 2 : 0;
494
+ for (let col = 0; col < colsFor(unitU); col++) {
495
+ const u0 = Math.max(-dims[u] / 2 + col * unitU - shift, -dims[u] / 2), u1 = Math.min(-dims[u] / 2 + (col + 1) * unitU - shift, dims[u] / 2);
496
+ if (u1 - u0 < 0.01) continue;
497
+ const center = [0, 0, 0], size = [0, 0, 0];
498
+ center[u] = (u0 + u1) / 2;
499
+ center[v] = (v0 + v1) / 2;
500
+ size[u] = u1 - u0;
501
+ size[v] = v1 - v0;
502
+ size[thin] = dims[thin];
503
+ cells.push({ center, size });
504
+ }
505
+ }
506
+ return cells;
507
+ };
508
+ var planeDistance = (cell, thin, point) => {
509
+ const [u, v] = planeAxes(thin);
510
+ const du = cell.center[u] - point[u], dv = cell.center[v] - point[v];
511
+ return Math.sqrt(du * du + dv * dv);
512
+ };
513
+ var cellsWithin = (cells, thin, point, radius) => {
514
+ const inside = cells.filter((c) => planeDistance(c, thin, point) <= radius);
515
+ if (inside.length > 0 || cells.length === 0) return inside;
516
+ return [cells.reduce((best, c) => planeDistance(c, thin, point) < planeDistance(best, thin, point) ? c : best)];
517
+ };
518
+ var cellsOutside = (cells, thin, point, radius) => cells.filter((c) => planeDistance(c, thin, point) > radius);
519
+ var MAX_MARKS_PER_FRAGMENT = 12;
520
+ var marksInBox = (center, size, rotation, holes) => {
521
+ const inverse = rotation ? quat.conjugate(rotation) : void 0;
522
+ const marks = [];
523
+ for (const hole of holes) {
524
+ const offset = [hole.point[0] - center[0], hole.point[1] - center[1], hole.point[2] - center[2]];
525
+ const point = inverse ? rotateVec(inverse, offset) : offset;
526
+ const inside = point.every((value, k) => Math.abs(value) <= size[k] / 2 + hole.diameter / 2);
527
+ if (!inside) continue;
528
+ marks.push({ point, normal: inverse ? rotateVec(inverse, hole.normal) : hole.normal, diameter: hole.diameter });
529
+ if (marks.length >= MAX_MARKS_PER_FRAGMENT) break;
530
+ }
531
+ return marks;
532
+ };
533
+
534
+ // src/layers.ts
535
+ var MAX_SPALLS = 64;
536
+ var thinAxisOf = (dims) => dims[0] <= dims[1] && dims[0] <= dims[2] ? 0 : dims[1] <= dims[2] ? 1 : 2;
537
+ var resolveLayer = (def, index, mat) => ({ index, def, mat, energy: mat.penetration * def.thickness * 1e3 });
538
+ var onFace = (layer, face) => layer.def.faces === "both" || layer.def.faces === (face > 0 ? "+" : "-");
539
+ var isSpalled = (spalls, layer, face, point) => {
540
+ for (const s of spalls ?? []) {
541
+ if (s[0] !== layer || s[1] !== face) continue;
542
+ const dx = s[2] - point[0], dy = s[3] - point[1], dz = s[4] - point[2];
543
+ if (dx * dx + dy * dy + dz * dz <= s[5] * s[5]) return true;
544
+ }
545
+ return false;
546
+ };
547
+ var outerLayer = (layers, spalls, face, point) => layers.find((l) => onFace(l, face) && !isSpalled(spalls, l.index, face, point));
548
+ var layersEnergy = (layers, spalls, face, point) => {
549
+ let sum = 0;
550
+ for (const l of layers) if (onFace(l, face) && !isSpalled(spalls, l.index, face, point)) sum += l.energy;
551
+ return sum;
552
+ };
553
+ var layersThickness = (layers, face) => {
554
+ let sum = 0;
555
+ for (const l of layers) if (onFace(l, face)) sum += l.def.thickness;
556
+ return sum;
557
+ };
558
+
559
+ // src/rebar.ts
560
+ var COVER = 0.03;
561
+ var MAX_BARS_PER_DIRECTION = 24;
562
+ var MAX_MEMBER_BARS_PER_SIDE = 6;
563
+ var STEEL_DENSITY = 7850;
564
+ var spread = (length, cover, spacing, cap) => {
565
+ const room = Math.max(0, length - 2 * cover);
566
+ const count = Math.min(cap, Math.max(2, Math.floor(room / spacing) + 1));
567
+ const step = count > 1 && room / (count - 1) < spacing ? room / (count - 1) : spacing;
568
+ return Array.from({ length: count }, (_, i) => (i - (count - 1) / 2) * step);
569
+ };
570
+ var bar = (axis, along, fixed) => {
571
+ const a = [0, 0, 0], b = [0, 0, 0];
572
+ for (const [index, value] of fixed) {
573
+ a[index] = value;
574
+ b[index] = value;
575
+ }
576
+ a[axis] = -along;
577
+ b[axis] = along;
578
+ return [a, b];
579
+ };
580
+ var isMember = (dims) => {
581
+ const [a = 0, b = 0, c = 0] = [...dims].sort((x, y) => x - y);
582
+ return b / a < 3 && c / b >= 3;
583
+ };
584
+ var rebarBars = (dims, thin, spacing) => {
585
+ const out = [];
586
+ if (isMember(dims)) {
587
+ const long = dims.indexOf(Math.max(...dims));
588
+ const [u2 = 0, v2 = 0] = [0, 1, 2].filter((i) => i !== long);
589
+ const us = spread(dims[u2], COVER, spacing, MAX_MEMBER_BARS_PER_SIDE);
590
+ const vs = spread(dims[v2], COVER, spacing, MAX_MEMBER_BARS_PER_SIDE);
591
+ us.forEach((x, i) => vs.forEach((y, j) => {
592
+ if (i === 0 || j === 0 || i === us.length - 1 || j === vs.length - 1) out.push(bar(long, dims[long] / 2 - 0.02, [[u2, x], [v2, y]]));
593
+ }));
594
+ return out;
595
+ }
596
+ const [u = 0, v = 0] = [0, 1, 2].filter((i) => i !== thin);
597
+ for (const y of spread(dims[v], COVER, spacing, MAX_BARS_PER_DIRECTION)) out.push(bar(u, dims[u] / 2 - 0.02, [[v, y], [thin, 0]]));
598
+ for (const x of spread(dims[u], COVER, spacing, MAX_BARS_PER_DIRECTION)) out.push(bar(v, dims[v] / 2 - 0.02, [[u, x], [thin, 0]]));
599
+ return out;
600
+ };
601
+ var clipBarsToDisc = (bars, u, v, center, radius) => {
602
+ const out = [];
603
+ for (const [a, b] of bars) {
604
+ const axis = a[u] !== b[u] ? u : v;
605
+ const other = axis === u ? v : u;
606
+ const off = a[other] - center[other === u ? 0 : 1];
607
+ if (Math.abs(off) >= radius) continue;
608
+ const half = Math.sqrt(radius * radius - off * off);
609
+ const mid = center[axis === u ? 0 : 1];
610
+ const lo = Math.max(Math.min(a[axis], b[axis]), mid - half), hi = Math.min(Math.max(a[axis], b[axis]), mid + half);
611
+ if (hi - lo < 1e-6) continue;
612
+ const p = [a[0], a[1], a[2]], q = [b[0], b[1], b[2]];
613
+ p[axis] = lo;
614
+ q[axis] = hi;
615
+ out.push([p, q]);
616
+ }
617
+ return out;
618
+ };
619
+ var rebarMass = (bars, diameter, density = STEEL_DENSITY) => {
620
+ let length = 0;
621
+ for (const [a, b] of bars) length += Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) + Math.abs(a[2] - b[2]);
622
+ return Math.PI * (diameter / 2) * (diameter / 2) * length * density;
623
+ };
624
+
473
625
  // src/data/materials.json
474
626
  var materials_default = [
475
627
  {
@@ -1478,9 +1630,19 @@ var GameDestruction = (() => {
1478
1630
  var DMG_PER_J = 0.02;
1479
1631
  var BLAST_COUPLING = 0.1;
1480
1632
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
1633
+ var BREACH_RADIUS = 0.55;
1634
+ var BREACH_BLAST_FACTOR = 6;
1635
+ var BREACH_AREA = 0.25;
1636
+ var RESIDUAL_CAPACITY = 0.15;
1637
+ var UNIT_FRAGMENT_FACTOR = 4;
1638
+ var BREACH_GRID_CAP = 4096;
1481
1639
  var CUT_SLICE = 1;
1482
1640
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
1483
1641
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
1642
+ var capacityScale = (p) => {
1643
+ if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
1644
+ return p.rt.removed ? Math.max(0.05, 1 - p.rt.removed / p.mass0) : 1;
1645
+ };
1484
1646
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
1485
1647
  var clone = (v) => structuredClone(v);
1486
1648
  var overloadRate = (stress) => Math.min(0.6, 0.12 + 0.8 * (stress - 1));
@@ -1503,6 +1665,8 @@ var GameDestruction = (() => {
1503
1665
  const maxDents = options.maxDents ?? 24;
1504
1666
  const maxHoles = options.maxHoles ?? 32;
1505
1667
  const fireSpread = options.fireSpread ?? 0.6;
1668
+ const breachRadius = options.breach?.radius ?? BREACH_RADIUS;
1669
+ const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
1506
1670
  const rng = createRng(options.seed ?? 1);
1507
1671
  const rnd = () => rng.next();
1508
1672
  let time = 0;
@@ -1511,6 +1675,8 @@ var GameDestruction = (() => {
1511
1675
  const listeners = /* @__PURE__ */ new Map();
1512
1676
  let cur = [];
1513
1677
  const blasts = [];
1678
+ const frontQueue = [];
1679
+ const blastFrontSpeed = options.blastFrontSpeed ?? 0;
1514
1680
  const primed = /* @__PURE__ */ new Map();
1515
1681
  let orderCounter = 0;
1516
1682
  const impls = /* @__PURE__ */ new Map();
@@ -1607,6 +1773,13 @@ var GameDestruction = (() => {
1607
1773
  const maxHp = Math.max(1, ps.hp ?? mass0 * mat.hpPerKg);
1608
1774
  const fuel0 = mat.thermal.ignitionC !== void 0 ? mass0 * (mat.thermal.fuelFraction ?? 0) : 0;
1609
1775
  const c = carry?.parts.get(ps.id);
1776
+ const staged = !!(ps.layers?.length || ps.reinforcement || ps.units || ps.breach);
1777
+ const layers = (ps.layers ?? []).map((l, i) => {
1778
+ const lm = materials.get(l.material);
1779
+ if (!lm) throw new Error(`unknown material "${l.material}" on layer ${i} of part "${ps.id}"`);
1780
+ return resolveLayer(l, i, lm);
1781
+ });
1782
+ if (ps.reinforcement && !materials.has(ps.reinforcement.material)) throw new Error(`unknown material "${ps.reinforcement.material}" on the reinforcement of part "${ps.id}"`);
1610
1783
  const pst = c?.st ?? {
1611
1784
  id: ps.id,
1612
1785
  material: ps.material,
@@ -1622,7 +1795,8 @@ var GameDestruction = (() => {
1622
1795
  dents: [],
1623
1796
  holes: [],
1624
1797
  cracks: [],
1625
- group: 0
1798
+ group: 0,
1799
+ ...staged ? { stage: 0 } : {}
1626
1800
  };
1627
1801
  if (!c && ps.contains) pst.liters = ps.contains.liters;
1628
1802
  const p = {
@@ -1641,7 +1815,10 @@ var GameDestruction = (() => {
1641
1815
  rt: c?.rt ?? { ...freshRuntime(), prevTemp: pst.temperature },
1642
1816
  structural: !!ps.structure && ps.structure.role !== "none",
1643
1817
  anchored: !!ps.anchored || ps.structure?.role === "foundation",
1644
- cap: ps.structure ? partCapacity(ps, mat) : 0
1818
+ cap: ps.structure ? partCapacity(ps, mat) : 0,
1819
+ staged,
1820
+ thin: thinAxisOf(shapeDims(ps.shape)),
1821
+ layers
1645
1822
  };
1646
1823
  if (p.structural) o.hasStructure = true;
1647
1824
  if (p.rt.chainDue !== void 0 && !isLost(p)) primed.set(p, o);
@@ -1781,7 +1958,7 @@ var GameDestruction = (() => {
1781
1958
  o.unsup[i] = o.anyAnchor && !isLost(p);
1782
1959
  continue;
1783
1960
  }
1784
- p.st.stress = p.st.load / p.cap;
1961
+ p.st.stress = p.st.load / (p.cap * capacityScale(p));
1785
1962
  if (p.st.stress > o.maxStress) o.maxStress = p.st.stress;
1786
1963
  }
1787
1964
  if (o.maxStress > 0.8) o.active = true;
@@ -1800,6 +1977,7 @@ var GameDestruction = (() => {
1800
1977
  for (const p of ps) {
1801
1978
  o.collapsed.add(p.spec.id);
1802
1979
  p.st.status = "collapsed";
1980
+ setStage(o, p, 3, {}, src);
1803
1981
  }
1804
1982
  emit({ ...base(o, src), type: "collapse", parts: ps.map((p) => p.spec.id), mass: ps.reduce((sum, p) => sum + p.st.mass, 0) });
1805
1983
  }
@@ -1853,6 +2031,7 @@ var GameDestruction = (() => {
1853
2031
  if (isLost(p) || ignore?.has(`${o.id}\0${p.spec.id}`)) continue;
1854
2032
  const r = rayPart(o, p, origin, d);
1855
2033
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
2034
+ if (p.st.opening && insideOpening(o, p, vec.madd(origin, d, r.t0))) continue;
1856
2035
  if (!best || r.t0 < best.distance) best = { object: o.id, part: p.spec.id, point: vec.madd(origin, d, r.t0), normal: r.n, distance: r.t0, pathLen: r.t1 - r.t0 };
1857
2036
  }
1858
2037
  }
@@ -1961,6 +2140,7 @@ var GameDestruction = (() => {
1961
2140
  if (p.st.holes.length > maxHoles) p.st.holes.shift();
1962
2141
  markDamaged(p);
1963
2142
  emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {} });
2143
+ setStage(o, p, 1, {}, src);
1964
2144
  return hole;
1965
2145
  };
1966
2146
  const addCrack = (o, p, wp, wn, length, src) => {
@@ -1969,6 +2149,7 @@ var GameDestruction = (() => {
1969
2149
  if (p.st.cracks.length > 16) p.st.cracks.shift();
1970
2150
  markDamaged(p);
1971
2151
  emit({ ...base(o, src), type: "crack", part: p.spec.id, crack, worldPoint: wp });
2152
+ setStage(o, p, 1, {}, src);
1972
2153
  };
1973
2154
  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 });
1974
2155
  const holeRate = (hole, pressurized) => (pressurized ? 0.5 : 0.1) * (hole.diameter / 9e-3);
@@ -2017,6 +2198,7 @@ var GameDestruction = (() => {
2017
2198
  if (split && mine.length) amount /= mine.length;
2018
2199
  for (const j of mine) {
2019
2200
  if (j.st.open) continue;
2201
+ if (j.spec.kind === "rebar" && (o.parts[j.ia].spec.reinforcement || o.parts[j.ib].spec.reinforcement)) continue;
2020
2202
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
2021
2203
  j.st.hp -= amount;
2022
2204
  if (j.st.hp <= 0) {
@@ -2030,23 +2212,172 @@ var GameDestruction = (() => {
2030
2212
  }
2031
2213
  }
2032
2214
  };
2215
+ const setStage = (o, p, stage, extra = {}, src) => {
2216
+ if (!p.staged || (p.st.stage ?? 0) >= stage) return;
2217
+ p.st.stage = stage;
2218
+ emit({ ...base(o, src), type: "stage", part: p.spec.id, stage, ...extra });
2219
+ };
2220
+ const faceOf = (o, p, worldNormal) => rotateVec(quat.conjugate(partXf(o, p).q), worldNormal)[p.thin] >= 0 ? 1 : -1;
2221
+ const spallOuter = (o, p, wp, face, normal, radius, src) => {
2222
+ var _a;
2223
+ if (p.layers.length === 0) return;
2224
+ const lp = objLocal(o, wp);
2225
+ const layer = outerLayer(p.layers, p.rt.spalls, face, lp);
2226
+ if (!layer) return;
2227
+ const spalls = (_a = p.rt).spalls ?? (_a.spalls = []);
2228
+ spalls.push([layer.index, face, lp[0], lp[1], lp[2], radius]);
2229
+ if (spalls.length > MAX_SPALLS) spalls.shift();
2230
+ markDamaged(p);
2231
+ emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: layer.index, point: wp, area: Math.PI * radius * radius });
2232
+ debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
2233
+ setStage(o, p, 1, {}, src);
2234
+ };
2235
+ const insideOpening = (o, p, objectPoint) => {
2236
+ const op = p.st.opening;
2237
+ if (!op) return false;
2238
+ const [u, v] = planeAxes(p.thin);
2239
+ const lp = toLocal(p.pos, p.q, objectPoint);
2240
+ const lc = toLocal(p.pos, p.q, op.center);
2241
+ return Math.hypot(lp[u] - lc[u], lp[v] - lc[v]) <= op.radius;
2242
+ };
2243
+ const breachAreaOf = (p, damage, blast) => damage / (p.mat.hpPerKg * p.mat.density) / Math.max(p.thickness, 5e-3) * (blast ? breachBlastFactor : 1);
2244
+ 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 }));
2245
+ const unitsOf = (p) => p.spec.units ?? { size: [p.mat.fragmentSize?.[1] ?? 0.2, p.mat.fragmentSize?.[1] ?? 0.2, p.thickness], bond: "stack" };
2246
+ const FRAGMENT_SHAPE = { shatter: "shard", splinter: "splinter", tear: "scrap" };
2247
+ const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag) => {
2248
+ const { c, q } = partXf(o, p);
2249
+ const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
2250
+ const [u, v] = planeAxes(p.thin);
2251
+ const faceArea = (cell) => cell.size[u] * cell.size[v];
2252
+ const total = cells.reduce((sum, cell) => sum + faceArea(cell), 0) || 1;
2253
+ const shape = p.spec.units ? "chunk" : FRAGMENT_SHAPE[p.mat.fracture] ?? "chunk";
2254
+ return cells.map((cell, i) => {
2255
+ const cw = toWorld(c, q, cell.center);
2256
+ const out = vec.normalize(vec.sub(cw, hitW), [0, 1, 0]);
2257
+ const dv = dir ? vec.normalize(vec.madd(out, dir, 0.6)) : out;
2258
+ const k = speed * (0.6 + 0.8 * rnd());
2259
+ const marks = marksInBox(cell.center, cell.size, void 0, holes);
2260
+ return {
2261
+ id: `${p.spec.id}#${tag}${i}`,
2262
+ part: p.spec.id,
2263
+ material: p.spec.material,
2264
+ shape,
2265
+ center: cw,
2266
+ size: [cell.size[0], cell.size[1], cell.size[2]],
2267
+ rotation: q,
2268
+ velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
2269
+ angularVelocity: [(rnd() - 0.5) * 8, (rnd() - 0.5) * 8, (rnd() - 0.5) * 8],
2270
+ mass: mass * faceArea(cell) / total,
2271
+ ...marks.length > 0 ? { marks } : {}
2272
+ };
2273
+ });
2274
+ };
2275
+ const worldBars = (o, p, bars) => {
2276
+ const { c, q } = partXf(o, p);
2277
+ return bars.map(([a, b]) => [toWorld(c, q, a), toWorld(c, q, b)]);
2278
+ };
2279
+ const addBreach = (o, p, wp, area, energy, dir, src) => {
2280
+ var _a;
2281
+ if (!p.staged || area <= 0 || (p.st.stage ?? 0) >= 2 || p.st.stripped || isLost(p)) return;
2282
+ const lp = objLocal(o, wp);
2283
+ const list = (_a = p.rt).breach ?? (_a.breach = []);
2284
+ list.push([lp[0], lp[1], lp[2], area]);
2285
+ if (list.length > MAX_SPALLS) list.shift();
2286
+ let total = 0;
2287
+ const sum = [0, 0, 0];
2288
+ for (const [x, y, z, a] of list) {
2289
+ if (vec.distance([x, y, z], lp) > breachRadius) continue;
2290
+ total += a;
2291
+ sum[0] += x * a;
2292
+ sum[1] += y * a;
2293
+ sum[2] += z * a;
2294
+ }
2295
+ if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
2296
+ openBreach(o, p, [sum[0] / total, sum[1] / total, sum[2] / total], total, energy, dir, src);
2297
+ };
2298
+ const openBreach = (o, p, center, total, energy, dir, src) => {
2299
+ const [u, v] = planeAxes(p.thin);
2300
+ const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
2301
+ const lc = toLocal(p.pos, p.q, center);
2302
+ lc[p.thin] = 0;
2303
+ const { c, q } = partXf(o, p);
2304
+ const worldCenter = toWorld(c, q, lc);
2305
+ if (2 * radius >= Math.min(p.dims[u], p.dims[v])) {
2306
+ failPart(o, p, worldCenter, dir, energy, 12, src);
2307
+ return;
2308
+ }
2309
+ const cells = cellsWithin(unitGrid(p.dims, p.thin, unitsOf(p), BREACH_GRID_CAP), p.thin, lc, radius);
2310
+ const removed = p.st.mass * cells.reduce((sum, cell) => sum + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
2311
+ const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
2312
+ const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
2313
+ const holes = localHolesOf(p);
2314
+ p.st.holes = p.st.holes.filter((_, i) => !cells.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[i]]).length > 0));
2315
+ p.st.mass = Math.max(0, p.st.mass - removed);
2316
+ p.rt.removed = (p.rt.removed ?? 0) + removed;
2317
+ p.st.opening = { center: objLocal(o, worldCenter), radius };
2318
+ markDamaged(p);
2319
+ setStage(o, p, 1, {}, src);
2320
+ setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
2321
+ debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
2322
+ const reinforcement = p.spec.reinforcement;
2323
+ if (reinforcement) {
2324
+ const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
2325
+ if (bars.length > 0) emit({ ...base(o, src), type: "expose", part: p.spec.id, bars });
2326
+ }
2327
+ if (p.structural) solveStructure(o);
2328
+ };
2329
+ const stripPart = (o, p, reinforcement, src) => {
2330
+ const bars = rebarBars(p.dims, p.thin, reinforcement.spacing);
2331
+ p.st.stripped = true;
2332
+ p.st.hp = p.st.maxHp;
2333
+ p.st.mass = rebarMass(bars, reinforcement.diameter, materials.get(reinforcement.material)?.density);
2334
+ p.st.holes = [];
2335
+ p.st.burning = false;
2336
+ p.rt.jet = false;
2337
+ markDamaged(p);
2338
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
2339
+ solveStructure(o);
2340
+ collapseCheck(o, src);
2341
+ };
2342
+ const unitFragmentPlan = (p, cap) => {
2343
+ const grid = unitGrid(p.dims, p.thin, unitsOf(p), cap);
2344
+ const op = p.st.opening;
2345
+ const cells = op ? cellsOutside(grid, p.thin, toLocal(p.pos, p.q, op.center), op.radius) : grid;
2346
+ const [u, v] = planeAxes(p.thin);
2347
+ return cells.map((cell) => ({ center: cell.center, size: cell.size, q: quat.identity(), shape: "chunk", weight: cell.size[u] * cell.size[v] }));
2348
+ };
2349
+ const loseCage = (o, p, src) => {
2350
+ p.st.status = "broken";
2351
+ p.st.hp = 0;
2352
+ p.st.mass = 0;
2353
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: [] });
2354
+ debris(o, p, "sparks", partXf(o, p).c, [0, 1, 0], 6, 3, src);
2355
+ partLost(o, p, src);
2356
+ };
2033
2357
  const failPart = (o, p, origin, dir, fragEnergy, cap, src) => {
2034
2358
  if (isLost(p)) return;
2359
+ if (p.st.stripped) {
2360
+ loseCage(o, p, src);
2361
+ return;
2362
+ }
2035
2363
  const ex = p.spec.explodes;
2036
2364
  const style = p.mat.fracture === "none" ? "snap" : p.mat.fracture;
2037
2365
  const { c, q } = partXf(o, p);
2038
2366
  const hitW = origin ?? c;
2039
2367
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
2040
- const plan = planFragments({
2368
+ const hitLocal = toLocal(c, q, hitW);
2369
+ const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
2370
+ const plan = unitPlan.length > 0 ? unitPlan : planFragments({
2041
2371
  style,
2042
2372
  dims: p.dims,
2043
2373
  thickness: p.thickness,
2044
2374
  grain,
2045
- hit: toLocal(c, q, hitW),
2375
+ hit: hitLocal,
2046
2376
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
2047
2377
  maxFragments,
2048
2378
  rnd
2049
2379
  });
2380
+ const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
2050
2381
  const mass = p.st.mass;
2051
2382
  const totW = plan.reduce((s, f) => s + f.weight, 0) || 1;
2052
2383
  const energy = ex ? ex.energy * 0.2 : fragEnergy;
@@ -2056,6 +2387,7 @@ var GameDestruction = (() => {
2056
2387
  const out = vec.normalize(vec.sub(cw, hitW), [0, 1, 0]);
2057
2388
  const dv = dir ? vec.normalize(vec.madd(out, dir, 0.6)) : out;
2058
2389
  const k = speed * (0.6 + 0.8 * rnd());
2390
+ const marks = marksInBox(f.center, f.size, f.q, holes);
2059
2391
  return {
2060
2392
  id: `${p.spec.id}#${i}`,
2061
2393
  part: p.spec.id,
@@ -2066,18 +2398,27 @@ var GameDestruction = (() => {
2066
2398
  rotation: quat.mul(q, f.q),
2067
2399
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
2068
2400
  angularVelocity: [(rnd() - 0.5) * 12, (rnd() - 0.5) * 12, (rnd() - 0.5) * 12],
2069
- mass: mass * f.weight / totW
2401
+ mass: mass * f.weight / totW,
2402
+ ...marks.length > 0 ? { marks } : {}
2070
2403
  };
2071
2404
  });
2072
- p.st.status = "broken";
2073
- p.st.hp = 0;
2074
- p.st.mass = 0;
2075
- p.st.burning = false;
2076
- p.rt.jet = false;
2405
+ const reinforcement = p.spec.reinforcement;
2406
+ if (!reinforcement) {
2407
+ p.st.status = "broken";
2408
+ p.st.hp = 0;
2409
+ p.st.mass = 0;
2410
+ p.st.burning = false;
2411
+ p.rt.jet = false;
2412
+ }
2077
2413
  emit({ ...base(o, src), type: "fracture", part: p.spec.id, style, fragments, point: hitW });
2078
2414
  debris(o, p, p.mat.breakDebris, c, [0, 1, 0], clamp2(Math.round(Math.cbrt(partVolume(p.spec.shape)) * 30), 3, 20), 2, src);
2415
+ setStage(o, p, 3, {}, src);
2416
+ if (reinforcement) {
2417
+ stripPart(o, p, reinforcement, src);
2418
+ return;
2419
+ }
2079
2420
  if (ex) {
2080
- emit({ ...base(o, src), type: "explode", part: p.spec.id, point: c, energy: ex.energy, radius: ex.radius });
2421
+ emit({ ...base(o, src), type: "explode", part: p.spec.id, point: c, energy: ex.energy, radius: ex.radius, ...ex.effect !== void 0 ? { effect: ex.effect } : {} });
2081
2422
  blasts.push({ point: c, energy: ex.energy, radius: ex.radius, source: src });
2082
2423
  }
2083
2424
  partLost(o, p, src);
@@ -2109,10 +2450,17 @@ var GameDestruction = (() => {
2109
2450
  };
2110
2451
  const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src) => {
2111
2452
  const mat = p.mat;
2453
+ if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry))) {
2454
+ return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry, d, Math.min(pathLen, p.thickness)), damage: 0 };
2455
+ }
2112
2456
  const sin = Math.min(1, Math.abs(vec.dot(d, n)));
2113
2457
  const angle = Math.asin(sin) * 180 / Math.PI;
2114
- const effM = p.thickness / Math.max(sin, 0.3);
2115
- const thr = mat.penetration * effM * 1e3;
2458
+ const slant = Math.max(sin, 0.3);
2459
+ const effM = p.thickness / slant;
2460
+ const face = p.layers.length > 0 ? faceOf(o, p, n) : 1;
2461
+ const exitLocal = objLocal(o, vec.madd(entry, d, Math.min(pathLen, effM)));
2462
+ const coats = p.layers.length > 0 ? layersEnergy(p.layers, p.rt.spalls, face, objLocal(o, entry)) + layersEnergy(p.layers, p.rt.spalls, -face, exitLocal) : 0;
2463
+ const thr = coats > 0 ? (mat.penetration * p.thickness * 1e3 + coats) / slant : mat.penetration * effM * 1e3;
2116
2464
  const penetrated = E > thr;
2117
2465
  const scale = (mat.damageScale?.ballistic ?? 1) * (mat.fracture === "shatter" ? fragility(mat.ductility, p.thickness) : 1);
2118
2466
  const id = p.spec.id;
@@ -2123,13 +2471,16 @@ var GameDestruction = (() => {
2123
2471
  const dmg2 = applyDamage(o, p, E * DMG_PER_J * scale * 0.2, "ballistic", entry, d, E * 0.1, 12, src);
2124
2472
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
2125
2473
  }
2474
+ if (p.layers.length > 0) spallOuter(o, p, entry, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
2126
2475
  const brittle = mat.ductility < 0.3;
2127
2476
  const ratio = E / Math.max(thr, 1e-6);
2128
2477
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
2129
2478
  let exitPoint;
2130
2479
  if (penetrated) {
2131
- exitPoint = vec.madd(entry, d, Math.min(pathLen, effM));
2480
+ exitPoint = vec.madd(entry, d, Math.min(pathLen, effM + layersThickness(p.layers, face) / slant + layersThickness(p.layers, -face) / slant));
2132
2481
  const hole = addHole(o, p, entry, n, caliber / 1e3 * (mat.ductility >= 0.3 ? 1 : 1.5), true, exitPoint, src);
2482
+ if (p.layers.length > 0) spallOuter(o, p, exitPoint, -face, vec.scale(n, -1), Math.max(0.03, caliber / 1e3 * 4), src);
2483
+ addBreach(o, p, entry, Math.PI * (hole.diameter / 2) * (hole.diameter / 2), E, d, src);
2133
2484
  if (brittle && mat.fracture === "shatter") addCrack(o, p, entry, n, crackLen, src);
2134
2485
  if (p.spec.contains && (p.st.liters ?? 0) > 0) {
2135
2486
  emit({
@@ -2174,8 +2525,11 @@ var GameDestruction = (() => {
2174
2525
  }
2175
2526
  if (kind === "blunt") wearJoints(o, p, E * 0.3, void 0, 0, src);
2176
2527
  else wearJoints(o, p, E * 0.3, point, 0.06, src);
2528
+ if (kind === "blunt" && E > 0 && p.layers.length > 0) spallOuter(o, p, point, faceOf(o, p, n), n, Math.max(contactR, 0.05), src);
2177
2529
  const scale = mat.damageScale?.[kind] ?? 1;
2178
- return applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
2530
+ const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
2531
+ addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
2532
+ return done;
2179
2533
  };
2180
2534
  const blastObject = (o, point, energy, radius, src) => {
2181
2535
  const start = cur.length;
@@ -2211,7 +2565,15 @@ var GameDestruction = (() => {
2211
2565
  }
2212
2566
  }
2213
2567
  wearJoints(o, p, Ein * 0.5, void 0, 0, src);
2214
- res.damage += applyDamage(o, p, Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1), "explosive", point, null, Ein * 0.3, 40, src);
2568
+ const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
2569
+ if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
2570
+ frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
2571
+ continue;
2572
+ }
2573
+ if (Ein > 20 && p.layers.length > 0) spallOuter(o, p, wp, faceOf(o, p, wn), wn, clamp2(0.05 + Math.sqrt(Ein) * 2e-3, 0.05, 0.4), src);
2574
+ const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
2575
+ res.damage += dealt;
2576
+ addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
2215
2577
  }
2216
2578
  res.events = cur.slice(start);
2217
2579
  return res;
@@ -2482,7 +2844,7 @@ var GameDestruction = (() => {
2482
2844
  }
2483
2845
  o.active = hot;
2484
2846
  };
2485
- const spread = (sources, dt) => {
2847
+ const spread2 = (sources, dt) => {
2486
2848
  for (const src of sources) {
2487
2849
  if (!src.p.st.burning && !src.p.rt.jet) continue;
2488
2850
  const sc = toWorld(src.o.st.position, src.o.st.rotation, src.p.pos);
@@ -2534,6 +2896,15 @@ var GameDestruction = (() => {
2534
2896
  }
2535
2897
  time = end;
2536
2898
  };
2899
+ const releaseFront = () => {
2900
+ if (frontQueue.length === 0) return;
2901
+ frontQueue.sort((a, b) => a.due - b.due);
2902
+ while (frontQueue.length > 0 && frontQueue[0].due <= time) {
2903
+ const hit = frontQueue.shift();
2904
+ if (objects.get(hit.o.id) !== hit.o) continue;
2905
+ applyDamage(hit.o, hit.p, hit.amount, "explosive", hit.point, null, hit.fragEnergy, 40, hit.src);
2906
+ }
2907
+ };
2537
2908
  const withCollapses = (r) => {
2538
2909
  const last = r.value[r.value.length - 1];
2539
2910
  if (!last) return r.value;
@@ -2783,8 +3154,9 @@ var GameDestruction = (() => {
2783
3154
  time += dt;
2784
3155
  const sources = [];
2785
3156
  for (const o of [...objects.values()]) if (o.active) stepObject(o, dt, sources);
2786
- if (sources.length) spread(sources, dt);
3157
+ if (sources.length) spread2(sources, dt);
2787
3158
  detonatePrimed(time);
3159
+ releaseFront();
2788
3160
  for (const im of [...impls.values()]) {
2789
3161
  if (!im.impl.update) continue;
2790
3162
  for (const e of im.impl.update(dt) ?? []) emit(e.time === void 0 ? { ...e, time } : e);
@@ -2822,7 +3194,7 @@ var GameDestruction = (() => {
2822
3194
  counter,
2823
3195
  objects: [...objects.values()].map((o) => {
2824
3196
  const runtime = { parts: {}, joints: {}, collapsed: [...o.collapsed] };
2825
- for (const p of o.parts) runtime.parts[p.spec.id] = { ...p.rt };
3197
+ for (const p of o.parts) runtime.parts[p.spec.id] = clone(p.rt);
2826
3198
  for (const j of o.joints) runtime.joints[j.spec.id] = j.step;
2827
3199
  return { id: o.id, spec: clone(o.spec), state: clone(o.st), runtime };
2828
3200
  }),
@@ -2839,7 +3211,7 @@ var GameDestruction = (() => {
2839
3211
  const state = clone(e.state);
2840
3212
  const carry = {
2841
3213
  parts: new Map(
2842
- state.parts.map((st) => [st.id, { st, rt: e.runtime?.parts[st.id] ? { ...e.runtime.parts[st.id] } : { ...freshRuntime(), prevTemp: st.temperature } }])
3214
+ state.parts.map((st) => [st.id, { st, rt: e.runtime?.parts[st.id] ? clone(e.runtime.parts[st.id]) : { ...freshRuntime(), prevTemp: st.temperature } }])
2843
3215
  ),
2844
3216
  joints: new Map(state.joints.map((st) => [st.id, { st, step: e.runtime?.joints[st.id] ?? 0 }])),
2845
3217
  collapsed: e.runtime?.collapsed
@@ -2858,6 +3230,109 @@ var GameDestruction = (() => {
2858
3230
  return world;
2859
3231
  };
2860
3232
 
3233
+ // src/data/assemblies.json
3234
+ var assemblies_default = [
3235
+ {
3236
+ id: "brick_wall_plastered",
3237
+ name: "Plastered brick wall",
3238
+ core: { material: "brick", thickness: 0.09 },
3239
+ layers: [{ material: "plaster", thickness: 0.02, faces: "both" }],
3240
+ units: { size: [0.23, 0.075, 0.11], bond: "running", mortar: "plaster" },
3241
+ breach: { area: 0.25 }
3242
+ },
3243
+ {
3244
+ id: "concrete_block_wall",
3245
+ name: "Concrete block wall",
3246
+ core: { material: "concrete", thickness: 0.14 },
3247
+ fill: 0.55,
3248
+ units: { size: [0.39, 0.19, 0.14], bond: "running", mortar: "plaster" },
3249
+ breach: { area: 0.25 }
3250
+ },
3251
+ {
3252
+ id: "reinforced_concrete",
3253
+ name: "Reinforced concrete wall",
3254
+ core: { material: "concrete", thickness: 0.2 },
3255
+ reinforcement: { material: "steel", spacing: 0.18, diameter: 8e-3, residualCapacity: 0.15 },
3256
+ units: { size: [0.6, 0.42, 0.2], bond: "stack", mortar: "concrete" },
3257
+ breach: { area: 0.25 }
3258
+ },
3259
+ {
3260
+ id: "drywall",
3261
+ name: "Drywall partition on steel studs",
3262
+ core: { material: "plaster", thickness: 0.0125 },
3263
+ layers: [{ material: "plaster", thickness: 0.0125, faces: "+" }],
3264
+ depth: 0.095,
3265
+ breach: { area: 0.25 }
3266
+ },
3267
+ {
3268
+ id: "wood_frame_wall",
3269
+ name: "Wood frame wall",
3270
+ core: { material: "plywood", thickness: 0.012 },
3271
+ layers: [{ material: "plaster", thickness: 0.0125, faces: "-" }],
3272
+ depth: 0.1135,
3273
+ breach: { area: 0.25 }
3274
+ },
3275
+ {
3276
+ id: "log_wall",
3277
+ name: "Log wall",
3278
+ core: { material: "wood_soft", thickness: 0.2 },
3279
+ fill: 0.785,
3280
+ breach: { area: 0.25 }
3281
+ },
3282
+ {
3283
+ id: "sheet_metal_shack",
3284
+ name: "Corrugated sheet metal wall",
3285
+ core: { material: "steel", thickness: 6e-4 },
3286
+ depth: 0.02,
3287
+ breach: { area: 0.25 }
3288
+ },
3289
+ {
3290
+ id: "glass_pane",
3291
+ name: "Glass pane",
3292
+ core: { material: "glass", thickness: 6e-3 },
3293
+ breach: { area: 0.1 }
3294
+ }
3295
+ ];
3296
+
3297
+ // src/assemblies.ts
3298
+ var ASSEMBLIES = assemblies_default;
3299
+ var registerAssembly = (def) => {
3300
+ const at = ASSEMBLIES.findIndex((a) => a.id === def.id);
3301
+ if (at >= 0) ASSEMBLIES[at] = def;
3302
+ else ASSEMBLIES.push(def);
3303
+ };
3304
+ var getAssembly = (id) => ASSEMBLIES.find((a) => a.id === id);
3305
+ var faceCount = (layer) => layer.faces === "both" ? 2 : 1;
3306
+ var assemblyDepth = (def) => def.depth ?? def.core.thickness + (def.layers ?? []).reduce((sum, l) => sum + l.thickness * faceCount(l), 0);
3307
+ var assemblyPart = (assemblyId, options) => {
3308
+ const def = getAssembly(assemblyId);
3309
+ if (!def) throw new Error(`unknown assembly "${assemblyId}"`);
3310
+ const densityOf = (id) => {
3311
+ const m = options.materials?.get(id) ?? MATERIALS.find((x) => x.id === id);
3312
+ if (!m) throw new Error(`unknown material "${id}" in assembly "${assemblyId}"`);
3313
+ return m.density;
3314
+ };
3315
+ const fill = def.fill ?? 1;
3316
+ const area = options.width * options.height;
3317
+ const coreMass = area * def.core.thickness * fill * densityOf(def.core.material);
3318
+ const layerMass = (def.layers ?? []).reduce((sum, l) => sum + area * l.thickness * faceCount(l) * densityOf(l.material), 0);
3319
+ return {
3320
+ id: options.id,
3321
+ material: def.core.material,
3322
+ shape: { kind: "box", size: [options.width, options.height, assemblyDepth(def)] },
3323
+ position: options.position,
3324
+ ...options.rotation ? { rotation: options.rotation } : {},
3325
+ mass: coreMass + layerMass,
3326
+ thickness: def.core.thickness * fill,
3327
+ ...def.layers ? { layers: def.layers.map((l) => ({ ...l })) } : {},
3328
+ ...def.units ? { units: { ...def.units, size: [...def.units.size] } } : {},
3329
+ ...def.reinforcement ? { reinforcement: { ...def.reinforcement } } : {},
3330
+ ...def.breach ? { breach: { ...def.breach } } : {},
3331
+ ...options.structure ? { structure: options.structure } : {},
3332
+ ...options.extra
3333
+ };
3334
+ };
3335
+
2861
3336
  // src/frame.ts
2862
3337
  var buildFrame = (o = {}) => {
2863
3338
  const spanX = o.spanX ?? 8, spanZ = o.spanZ ?? 8;
@@ -2965,6 +3440,8 @@ var GameDestruction = (() => {
2965
3440
  var sizeStructure = (spec, options = {}) => {
2966
3441
  const safety = options.safety ?? 0.65;
2967
3442
  const redundancy = options.redundancy;
3443
+ const redundancyRoles = options.redundancyRoles ?? ["column"];
3444
+ const safetyOf = (role) => options.safetyByRole?.[role] ?? safety;
2968
3445
  const loads = (s) => {
2969
3446
  const w = createDestruction(options.materials ? { materials: options.materials } : {});
2970
3447
  const id = w.add(s);
@@ -2977,11 +3454,11 @@ var GameDestruction = (() => {
2977
3454
  if (!p.structure || p.structure.role === "none") continue;
2978
3455
  const b = base.get(p.id);
2979
3456
  if (!b || b.stress <= 0) continue;
2980
- need.set(p.id, b.load / safety);
3457
+ need.set(p.id, b.load / safetyOf(p.structure.role));
2981
3458
  }
2982
3459
  if (redundancy !== void 0) {
2983
3460
  for (const col of out.parts) {
2984
- if (col.structure?.role !== "column") continue;
3461
+ if (!col.structure || !redundancyRoles.includes(col.structure.role)) continue;
2985
3462
  const reduced = {
2986
3463
  ...out,
2987
3464
  parts: out.parts.filter((p) => p.id !== col.id),
@@ -3081,6 +3558,10 @@ var GameDestruction = (() => {
3081
3558
  primed: "about to explode",
3082
3559
  stress: "overload",
3083
3560
  collapse: "collapsed",
3561
+ spall: "plaster came off",
3562
+ stage: "damage stage",
3563
+ expose: "rebar bare",
3564
+ body: "body",
3084
3565
  destroyed: "destroyed"
3085
3566
  },
3086
3567
  fractureStyles: { shatter: "shatter", crumble: "crumble", splinter: "splinter", tear: "tear", snap: "snap", none: "none" },
@@ -3163,6 +3644,10 @@ var GameDestruction = (() => {
3163
3644
  primed: "prestes a explodir",
3164
3645
  stress: "sobrecarga",
3165
3646
  collapse: "desabou",
3647
+ spall: "reboco soltou",
3648
+ stage: "est\xE1gio de dano",
3649
+ expose: "vergalh\xE3o \xE0 mostra",
3650
+ body: "corpo",
3166
3651
  destroyed: "destru\xEDdo"
3167
3652
  },
3168
3653
  fractureStyles: { shatter: "estilha\xE7ar", crumble: "esfarelar", splinter: "lascar", tear: "rasgar", snap: "partir", none: "nenhuma" },