@adatechnology/game-destruction 0.4.2 → 0.5.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;
@@ -1607,6 +1771,13 @@ var GameDestruction = (() => {
1607
1771
  const maxHp = Math.max(1, ps.hp ?? mass0 * mat.hpPerKg);
1608
1772
  const fuel0 = mat.thermal.ignitionC !== void 0 ? mass0 * (mat.thermal.fuelFraction ?? 0) : 0;
1609
1773
  const c = carry?.parts.get(ps.id);
1774
+ const staged = !!(ps.layers?.length || ps.reinforcement || ps.units || ps.breach);
1775
+ const layers = (ps.layers ?? []).map((l, i) => {
1776
+ const lm = materials.get(l.material);
1777
+ if (!lm) throw new Error(`unknown material "${l.material}" on layer ${i} of part "${ps.id}"`);
1778
+ return resolveLayer(l, i, lm);
1779
+ });
1780
+ if (ps.reinforcement && !materials.has(ps.reinforcement.material)) throw new Error(`unknown material "${ps.reinforcement.material}" on the reinforcement of part "${ps.id}"`);
1610
1781
  const pst = c?.st ?? {
1611
1782
  id: ps.id,
1612
1783
  material: ps.material,
@@ -1622,7 +1793,8 @@ var GameDestruction = (() => {
1622
1793
  dents: [],
1623
1794
  holes: [],
1624
1795
  cracks: [],
1625
- group: 0
1796
+ group: 0,
1797
+ ...staged ? { stage: 0 } : {}
1626
1798
  };
1627
1799
  if (!c && ps.contains) pst.liters = ps.contains.liters;
1628
1800
  const p = {
@@ -1641,7 +1813,10 @@ var GameDestruction = (() => {
1641
1813
  rt: c?.rt ?? { ...freshRuntime(), prevTemp: pst.temperature },
1642
1814
  structural: !!ps.structure && ps.structure.role !== "none",
1643
1815
  anchored: !!ps.anchored || ps.structure?.role === "foundation",
1644
- cap: ps.structure ? partCapacity(ps, mat) : 0
1816
+ cap: ps.structure ? partCapacity(ps, mat) : 0,
1817
+ staged,
1818
+ thin: thinAxisOf(shapeDims(ps.shape)),
1819
+ layers
1645
1820
  };
1646
1821
  if (p.structural) o.hasStructure = true;
1647
1822
  if (p.rt.chainDue !== void 0 && !isLost(p)) primed.set(p, o);
@@ -1781,7 +1956,7 @@ var GameDestruction = (() => {
1781
1956
  o.unsup[i] = o.anyAnchor && !isLost(p);
1782
1957
  continue;
1783
1958
  }
1784
- p.st.stress = p.st.load / p.cap;
1959
+ p.st.stress = p.st.load / (p.cap * capacityScale(p));
1785
1960
  if (p.st.stress > o.maxStress) o.maxStress = p.st.stress;
1786
1961
  }
1787
1962
  if (o.maxStress > 0.8) o.active = true;
@@ -1800,6 +1975,7 @@ var GameDestruction = (() => {
1800
1975
  for (const p of ps) {
1801
1976
  o.collapsed.add(p.spec.id);
1802
1977
  p.st.status = "collapsed";
1978
+ setStage(o, p, 3, {}, src);
1803
1979
  }
1804
1980
  emit({ ...base(o, src), type: "collapse", parts: ps.map((p) => p.spec.id), mass: ps.reduce((sum, p) => sum + p.st.mass, 0) });
1805
1981
  }
@@ -1853,6 +2029,7 @@ var GameDestruction = (() => {
1853
2029
  if (isLost(p) || ignore?.has(`${o.id}\0${p.spec.id}`)) continue;
1854
2030
  const r = rayPart(o, p, origin, d);
1855
2031
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
2032
+ if (p.st.opening && insideOpening(o, p, vec.madd(origin, d, r.t0))) continue;
1856
2033
  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
2034
  }
1858
2035
  }
@@ -1961,6 +2138,7 @@ var GameDestruction = (() => {
1961
2138
  if (p.st.holes.length > maxHoles) p.st.holes.shift();
1962
2139
  markDamaged(p);
1963
2140
  emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {} });
2141
+ setStage(o, p, 1, {}, src);
1964
2142
  return hole;
1965
2143
  };
1966
2144
  const addCrack = (o, p, wp, wn, length, src) => {
@@ -1969,6 +2147,7 @@ var GameDestruction = (() => {
1969
2147
  if (p.st.cracks.length > 16) p.st.cracks.shift();
1970
2148
  markDamaged(p);
1971
2149
  emit({ ...base(o, src), type: "crack", part: p.spec.id, crack, worldPoint: wp });
2150
+ setStage(o, p, 1, {}, src);
1972
2151
  };
1973
2152
  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
2153
  const holeRate = (hole, pressurized) => (pressurized ? 0.5 : 0.1) * (hole.diameter / 9e-3);
@@ -2017,6 +2196,7 @@ var GameDestruction = (() => {
2017
2196
  if (split && mine.length) amount /= mine.length;
2018
2197
  for (const j of mine) {
2019
2198
  if (j.st.open) continue;
2199
+ if (j.spec.kind === "rebar" && (o.parts[j.ia].spec.reinforcement || o.parts[j.ib].spec.reinforcement)) continue;
2020
2200
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
2021
2201
  j.st.hp -= amount;
2022
2202
  if (j.st.hp <= 0) {
@@ -2030,23 +2210,172 @@ var GameDestruction = (() => {
2030
2210
  }
2031
2211
  }
2032
2212
  };
2213
+ const setStage = (o, p, stage, extra = {}, src) => {
2214
+ if (!p.staged || (p.st.stage ?? 0) >= stage) return;
2215
+ p.st.stage = stage;
2216
+ emit({ ...base(o, src), type: "stage", part: p.spec.id, stage, ...extra });
2217
+ };
2218
+ const faceOf = (o, p, worldNormal) => rotateVec(quat.conjugate(partXf(o, p).q), worldNormal)[p.thin] >= 0 ? 1 : -1;
2219
+ const spallOuter = (o, p, wp, face, normal, radius, src) => {
2220
+ var _a;
2221
+ if (p.layers.length === 0) return;
2222
+ const lp = objLocal(o, wp);
2223
+ const layer = outerLayer(p.layers, p.rt.spalls, face, lp);
2224
+ if (!layer) return;
2225
+ const spalls = (_a = p.rt).spalls ?? (_a.spalls = []);
2226
+ spalls.push([layer.index, face, lp[0], lp[1], lp[2], radius]);
2227
+ if (spalls.length > MAX_SPALLS) spalls.shift();
2228
+ markDamaged(p);
2229
+ emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: layer.index, point: wp, area: Math.PI * radius * radius });
2230
+ debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
2231
+ setStage(o, p, 1, {}, src);
2232
+ };
2233
+ const insideOpening = (o, p, objectPoint) => {
2234
+ const op = p.st.opening;
2235
+ if (!op) return false;
2236
+ const [u, v] = planeAxes(p.thin);
2237
+ const lp = toLocal(p.pos, p.q, objectPoint);
2238
+ const lc = toLocal(p.pos, p.q, op.center);
2239
+ return Math.hypot(lp[u] - lc[u], lp[v] - lc[v]) <= op.radius;
2240
+ };
2241
+ const breachAreaOf = (p, damage, blast) => damage / (p.mat.hpPerKg * p.mat.density) / Math.max(p.thickness, 5e-3) * (blast ? breachBlastFactor : 1);
2242
+ 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 }));
2243
+ const unitsOf = (p) => p.spec.units ?? { size: [p.mat.fragmentSize?.[1] ?? 0.2, p.mat.fragmentSize?.[1] ?? 0.2, p.thickness], bond: "stack" };
2244
+ const FRAGMENT_SHAPE = { shatter: "shard", splinter: "splinter", tear: "scrap" };
2245
+ const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag) => {
2246
+ const { c, q } = partXf(o, p);
2247
+ const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
2248
+ const [u, v] = planeAxes(p.thin);
2249
+ const faceArea = (cell) => cell.size[u] * cell.size[v];
2250
+ const total = cells.reduce((sum, cell) => sum + faceArea(cell), 0) || 1;
2251
+ const shape = p.spec.units ? "chunk" : FRAGMENT_SHAPE[p.mat.fracture] ?? "chunk";
2252
+ return cells.map((cell, i) => {
2253
+ const cw = toWorld(c, q, cell.center);
2254
+ const out = vec.normalize(vec.sub(cw, hitW), [0, 1, 0]);
2255
+ const dv = dir ? vec.normalize(vec.madd(out, dir, 0.6)) : out;
2256
+ const k = speed * (0.6 + 0.8 * rnd());
2257
+ const marks = marksInBox(cell.center, cell.size, void 0, holes);
2258
+ return {
2259
+ id: `${p.spec.id}#${tag}${i}`,
2260
+ part: p.spec.id,
2261
+ material: p.spec.material,
2262
+ shape,
2263
+ center: cw,
2264
+ size: [cell.size[0], cell.size[1], cell.size[2]],
2265
+ rotation: q,
2266
+ velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
2267
+ angularVelocity: [(rnd() - 0.5) * 8, (rnd() - 0.5) * 8, (rnd() - 0.5) * 8],
2268
+ mass: mass * faceArea(cell) / total,
2269
+ ...marks.length > 0 ? { marks } : {}
2270
+ };
2271
+ });
2272
+ };
2273
+ const worldBars = (o, p, bars) => {
2274
+ const { c, q } = partXf(o, p);
2275
+ return bars.map(([a, b]) => [toWorld(c, q, a), toWorld(c, q, b)]);
2276
+ };
2277
+ const addBreach = (o, p, wp, area, energy, dir, src) => {
2278
+ var _a;
2279
+ if (!p.staged || area <= 0 || (p.st.stage ?? 0) >= 2 || p.st.stripped || isLost(p)) return;
2280
+ const lp = objLocal(o, wp);
2281
+ const list = (_a = p.rt).breach ?? (_a.breach = []);
2282
+ list.push([lp[0], lp[1], lp[2], area]);
2283
+ if (list.length > MAX_SPALLS) list.shift();
2284
+ let total = 0;
2285
+ const sum = [0, 0, 0];
2286
+ for (const [x, y, z, a] of list) {
2287
+ if (vec.distance([x, y, z], lp) > breachRadius) continue;
2288
+ total += a;
2289
+ sum[0] += x * a;
2290
+ sum[1] += y * a;
2291
+ sum[2] += z * a;
2292
+ }
2293
+ if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
2294
+ openBreach(o, p, [sum[0] / total, sum[1] / total, sum[2] / total], total, energy, dir, src);
2295
+ };
2296
+ const openBreach = (o, p, center, total, energy, dir, src) => {
2297
+ const [u, v] = planeAxes(p.thin);
2298
+ const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
2299
+ const lc = toLocal(p.pos, p.q, center);
2300
+ lc[p.thin] = 0;
2301
+ const { c, q } = partXf(o, p);
2302
+ const worldCenter = toWorld(c, q, lc);
2303
+ if (2 * radius >= Math.min(p.dims[u], p.dims[v])) {
2304
+ failPart(o, p, worldCenter, dir, energy, 12, src);
2305
+ return;
2306
+ }
2307
+ const cells = cellsWithin(unitGrid(p.dims, p.thin, unitsOf(p), BREACH_GRID_CAP), p.thin, lc, radius);
2308
+ const removed = p.st.mass * cells.reduce((sum, cell) => sum + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
2309
+ const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
2310
+ const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
2311
+ const holes = localHolesOf(p);
2312
+ p.st.holes = p.st.holes.filter((_, i) => !cells.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[i]]).length > 0));
2313
+ p.st.mass = Math.max(0, p.st.mass - removed);
2314
+ p.rt.removed = (p.rt.removed ?? 0) + removed;
2315
+ p.st.opening = { center: objLocal(o, worldCenter), radius };
2316
+ markDamaged(p);
2317
+ setStage(o, p, 1, {}, src);
2318
+ setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
2319
+ debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
2320
+ const reinforcement = p.spec.reinforcement;
2321
+ if (reinforcement) {
2322
+ const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
2323
+ if (bars.length > 0) emit({ ...base(o, src), type: "expose", part: p.spec.id, bars });
2324
+ }
2325
+ if (p.structural) solveStructure(o);
2326
+ };
2327
+ const stripPart = (o, p, reinforcement, src) => {
2328
+ const bars = rebarBars(p.dims, p.thin, reinforcement.spacing);
2329
+ p.st.stripped = true;
2330
+ p.st.hp = p.st.maxHp;
2331
+ p.st.mass = rebarMass(bars, reinforcement.diameter, materials.get(reinforcement.material)?.density);
2332
+ p.st.holes = [];
2333
+ p.st.burning = false;
2334
+ p.rt.jet = false;
2335
+ markDamaged(p);
2336
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
2337
+ solveStructure(o);
2338
+ collapseCheck(o, src);
2339
+ };
2340
+ const unitFragmentPlan = (p, cap) => {
2341
+ const grid = unitGrid(p.dims, p.thin, unitsOf(p), cap);
2342
+ const op = p.st.opening;
2343
+ const cells = op ? cellsOutside(grid, p.thin, toLocal(p.pos, p.q, op.center), op.radius) : grid;
2344
+ const [u, v] = planeAxes(p.thin);
2345
+ return cells.map((cell) => ({ center: cell.center, size: cell.size, q: quat.identity(), shape: "chunk", weight: cell.size[u] * cell.size[v] }));
2346
+ };
2347
+ const loseCage = (o, p, src) => {
2348
+ p.st.status = "broken";
2349
+ p.st.hp = 0;
2350
+ p.st.mass = 0;
2351
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: [] });
2352
+ debris(o, p, "sparks", partXf(o, p).c, [0, 1, 0], 6, 3, src);
2353
+ partLost(o, p, src);
2354
+ };
2033
2355
  const failPart = (o, p, origin, dir, fragEnergy, cap, src) => {
2034
2356
  if (isLost(p)) return;
2357
+ if (p.st.stripped) {
2358
+ loseCage(o, p, src);
2359
+ return;
2360
+ }
2035
2361
  const ex = p.spec.explodes;
2036
2362
  const style = p.mat.fracture === "none" ? "snap" : p.mat.fracture;
2037
2363
  const { c, q } = partXf(o, p);
2038
2364
  const hitW = origin ?? c;
2039
2365
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
2040
- const plan = planFragments({
2366
+ const hitLocal = toLocal(c, q, hitW);
2367
+ const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
2368
+ const plan = unitPlan.length > 0 ? unitPlan : planFragments({
2041
2369
  style,
2042
2370
  dims: p.dims,
2043
2371
  thickness: p.thickness,
2044
2372
  grain,
2045
- hit: toLocal(c, q, hitW),
2373
+ hit: hitLocal,
2046
2374
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
2047
2375
  maxFragments,
2048
2376
  rnd
2049
2377
  });
2378
+ const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
2050
2379
  const mass = p.st.mass;
2051
2380
  const totW = plan.reduce((s, f) => s + f.weight, 0) || 1;
2052
2381
  const energy = ex ? ex.energy * 0.2 : fragEnergy;
@@ -2056,6 +2385,7 @@ var GameDestruction = (() => {
2056
2385
  const out = vec.normalize(vec.sub(cw, hitW), [0, 1, 0]);
2057
2386
  const dv = dir ? vec.normalize(vec.madd(out, dir, 0.6)) : out;
2058
2387
  const k = speed * (0.6 + 0.8 * rnd());
2388
+ const marks = marksInBox(f.center, f.size, f.q, holes);
2059
2389
  return {
2060
2390
  id: `${p.spec.id}#${i}`,
2061
2391
  part: p.spec.id,
@@ -2066,16 +2396,25 @@ var GameDestruction = (() => {
2066
2396
  rotation: quat.mul(q, f.q),
2067
2397
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
2068
2398
  angularVelocity: [(rnd() - 0.5) * 12, (rnd() - 0.5) * 12, (rnd() - 0.5) * 12],
2069
- mass: mass * f.weight / totW
2399
+ mass: mass * f.weight / totW,
2400
+ ...marks.length > 0 ? { marks } : {}
2070
2401
  };
2071
2402
  });
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;
2403
+ const reinforcement = p.spec.reinforcement;
2404
+ if (!reinforcement) {
2405
+ p.st.status = "broken";
2406
+ p.st.hp = 0;
2407
+ p.st.mass = 0;
2408
+ p.st.burning = false;
2409
+ p.rt.jet = false;
2410
+ }
2077
2411
  emit({ ...base(o, src), type: "fracture", part: p.spec.id, style, fragments, point: hitW });
2078
2412
  debris(o, p, p.mat.breakDebris, c, [0, 1, 0], clamp2(Math.round(Math.cbrt(partVolume(p.spec.shape)) * 30), 3, 20), 2, src);
2413
+ setStage(o, p, 3, {}, src);
2414
+ if (reinforcement) {
2415
+ stripPart(o, p, reinforcement, src);
2416
+ return;
2417
+ }
2079
2418
  if (ex) {
2080
2419
  emit({ ...base(o, src), type: "explode", part: p.spec.id, point: c, energy: ex.energy, radius: ex.radius });
2081
2420
  blasts.push({ point: c, energy: ex.energy, radius: ex.radius, source: src });
@@ -2109,10 +2448,17 @@ var GameDestruction = (() => {
2109
2448
  };
2110
2449
  const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src) => {
2111
2450
  const mat = p.mat;
2451
+ if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry))) {
2452
+ return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry, d, Math.min(pathLen, p.thickness)), damage: 0 };
2453
+ }
2112
2454
  const sin = Math.min(1, Math.abs(vec.dot(d, n)));
2113
2455
  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;
2456
+ const slant = Math.max(sin, 0.3);
2457
+ const effM = p.thickness / slant;
2458
+ const face = p.layers.length > 0 ? faceOf(o, p, n) : 1;
2459
+ const exitLocal = objLocal(o, vec.madd(entry, d, Math.min(pathLen, effM)));
2460
+ 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;
2461
+ const thr = coats > 0 ? (mat.penetration * p.thickness * 1e3 + coats) / slant : mat.penetration * effM * 1e3;
2116
2462
  const penetrated = E > thr;
2117
2463
  const scale = (mat.damageScale?.ballistic ?? 1) * (mat.fracture === "shatter" ? fragility(mat.ductility, p.thickness) : 1);
2118
2464
  const id = p.spec.id;
@@ -2123,13 +2469,16 @@ var GameDestruction = (() => {
2123
2469
  const dmg2 = applyDamage(o, p, E * DMG_PER_J * scale * 0.2, "ballistic", entry, d, E * 0.1, 12, src);
2124
2470
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
2125
2471
  }
2472
+ if (p.layers.length > 0) spallOuter(o, p, entry, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
2126
2473
  const brittle = mat.ductility < 0.3;
2127
2474
  const ratio = E / Math.max(thr, 1e-6);
2128
2475
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
2129
2476
  let exitPoint;
2130
2477
  if (penetrated) {
2131
- exitPoint = vec.madd(entry, d, Math.min(pathLen, effM));
2478
+ exitPoint = vec.madd(entry, d, Math.min(pathLen, effM + layersThickness(p.layers, face) / slant + layersThickness(p.layers, -face) / slant));
2132
2479
  const hole = addHole(o, p, entry, n, caliber / 1e3 * (mat.ductility >= 0.3 ? 1 : 1.5), true, exitPoint, src);
2480
+ if (p.layers.length > 0) spallOuter(o, p, exitPoint, -face, vec.scale(n, -1), Math.max(0.03, caliber / 1e3 * 4), src);
2481
+ addBreach(o, p, entry, Math.PI * (hole.diameter / 2) * (hole.diameter / 2), E, d, src);
2133
2482
  if (brittle && mat.fracture === "shatter") addCrack(o, p, entry, n, crackLen, src);
2134
2483
  if (p.spec.contains && (p.st.liters ?? 0) > 0) {
2135
2484
  emit({
@@ -2174,8 +2523,11 @@ var GameDestruction = (() => {
2174
2523
  }
2175
2524
  if (kind === "blunt") wearJoints(o, p, E * 0.3, void 0, 0, src);
2176
2525
  else wearJoints(o, p, E * 0.3, point, 0.06, src);
2526
+ 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
2527
  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);
2528
+ const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
2529
+ addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
2530
+ return done;
2179
2531
  };
2180
2532
  const blastObject = (o, point, energy, radius, src) => {
2181
2533
  const start = cur.length;
@@ -2211,7 +2563,10 @@ var GameDestruction = (() => {
2211
2563
  }
2212
2564
  }
2213
2565
  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);
2566
+ 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);
2567
+ const dealt = applyDamage(o, p, Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1), "explosive", point, null, Ein * 0.3, 40, src);
2568
+ res.damage += dealt;
2569
+ addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
2215
2570
  }
2216
2571
  res.events = cur.slice(start);
2217
2572
  return res;
@@ -2482,7 +2837,7 @@ var GameDestruction = (() => {
2482
2837
  }
2483
2838
  o.active = hot;
2484
2839
  };
2485
- const spread = (sources, dt) => {
2840
+ const spread2 = (sources, dt) => {
2486
2841
  for (const src of sources) {
2487
2842
  if (!src.p.st.burning && !src.p.rt.jet) continue;
2488
2843
  const sc = toWorld(src.o.st.position, src.o.st.rotation, src.p.pos);
@@ -2783,7 +3138,7 @@ var GameDestruction = (() => {
2783
3138
  time += dt;
2784
3139
  const sources = [];
2785
3140
  for (const o of [...objects.values()]) if (o.active) stepObject(o, dt, sources);
2786
- if (sources.length) spread(sources, dt);
3141
+ if (sources.length) spread2(sources, dt);
2787
3142
  detonatePrimed(time);
2788
3143
  for (const im of [...impls.values()]) {
2789
3144
  if (!im.impl.update) continue;
@@ -2822,7 +3177,7 @@ var GameDestruction = (() => {
2822
3177
  counter,
2823
3178
  objects: [...objects.values()].map((o) => {
2824
3179
  const runtime = { parts: {}, joints: {}, collapsed: [...o.collapsed] };
2825
- for (const p of o.parts) runtime.parts[p.spec.id] = { ...p.rt };
3180
+ for (const p of o.parts) runtime.parts[p.spec.id] = clone(p.rt);
2826
3181
  for (const j of o.joints) runtime.joints[j.spec.id] = j.step;
2827
3182
  return { id: o.id, spec: clone(o.spec), state: clone(o.st), runtime };
2828
3183
  }),
@@ -2839,7 +3194,7 @@ var GameDestruction = (() => {
2839
3194
  const state = clone(e.state);
2840
3195
  const carry = {
2841
3196
  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 } }])
3197
+ state.parts.map((st) => [st.id, { st, rt: e.runtime?.parts[st.id] ? clone(e.runtime.parts[st.id]) : { ...freshRuntime(), prevTemp: st.temperature } }])
2843
3198
  ),
2844
3199
  joints: new Map(state.joints.map((st) => [st.id, { st, step: e.runtime?.joints[st.id] ?? 0 }])),
2845
3200
  collapsed: e.runtime?.collapsed
@@ -2858,6 +3213,109 @@ var GameDestruction = (() => {
2858
3213
  return world;
2859
3214
  };
2860
3215
 
3216
+ // src/data/assemblies.json
3217
+ var assemblies_default = [
3218
+ {
3219
+ id: "brick_wall_plastered",
3220
+ name: "Plastered brick wall",
3221
+ core: { material: "brick", thickness: 0.09 },
3222
+ layers: [{ material: "plaster", thickness: 0.02, faces: "both" }],
3223
+ units: { size: [0.23, 0.075, 0.11], bond: "running", mortar: "plaster" },
3224
+ breach: { area: 0.25 }
3225
+ },
3226
+ {
3227
+ id: "concrete_block_wall",
3228
+ name: "Concrete block wall",
3229
+ core: { material: "concrete", thickness: 0.14 },
3230
+ fill: 0.55,
3231
+ units: { size: [0.39, 0.19, 0.14], bond: "running", mortar: "plaster" },
3232
+ breach: { area: 0.25 }
3233
+ },
3234
+ {
3235
+ id: "reinforced_concrete",
3236
+ name: "Reinforced concrete wall",
3237
+ core: { material: "concrete", thickness: 0.2 },
3238
+ reinforcement: { material: "steel", spacing: 0.18, diameter: 8e-3, residualCapacity: 0.15 },
3239
+ units: { size: [0.6, 0.42, 0.2], bond: "stack", mortar: "concrete" },
3240
+ breach: { area: 0.25 }
3241
+ },
3242
+ {
3243
+ id: "drywall",
3244
+ name: "Drywall partition on steel studs",
3245
+ core: { material: "plaster", thickness: 0.0125 },
3246
+ layers: [{ material: "plaster", thickness: 0.0125, faces: "+" }],
3247
+ depth: 0.095,
3248
+ breach: { area: 0.25 }
3249
+ },
3250
+ {
3251
+ id: "wood_frame_wall",
3252
+ name: "Wood frame wall",
3253
+ core: { material: "plywood", thickness: 0.012 },
3254
+ layers: [{ material: "plaster", thickness: 0.0125, faces: "-" }],
3255
+ depth: 0.1135,
3256
+ breach: { area: 0.25 }
3257
+ },
3258
+ {
3259
+ id: "log_wall",
3260
+ name: "Log wall",
3261
+ core: { material: "wood_soft", thickness: 0.2 },
3262
+ fill: 0.785,
3263
+ breach: { area: 0.25 }
3264
+ },
3265
+ {
3266
+ id: "sheet_metal_shack",
3267
+ name: "Corrugated sheet metal wall",
3268
+ core: { material: "steel", thickness: 6e-4 },
3269
+ depth: 0.02,
3270
+ breach: { area: 0.25 }
3271
+ },
3272
+ {
3273
+ id: "glass_pane",
3274
+ name: "Glass pane",
3275
+ core: { material: "glass", thickness: 6e-3 },
3276
+ breach: { area: 0.1 }
3277
+ }
3278
+ ];
3279
+
3280
+ // src/assemblies.ts
3281
+ var ASSEMBLIES = assemblies_default;
3282
+ var registerAssembly = (def) => {
3283
+ const at = ASSEMBLIES.findIndex((a) => a.id === def.id);
3284
+ if (at >= 0) ASSEMBLIES[at] = def;
3285
+ else ASSEMBLIES.push(def);
3286
+ };
3287
+ var getAssembly = (id) => ASSEMBLIES.find((a) => a.id === id);
3288
+ var faceCount = (layer) => layer.faces === "both" ? 2 : 1;
3289
+ var assemblyDepth = (def) => def.depth ?? def.core.thickness + (def.layers ?? []).reduce((sum, l) => sum + l.thickness * faceCount(l), 0);
3290
+ var assemblyPart = (assemblyId, options) => {
3291
+ const def = getAssembly(assemblyId);
3292
+ if (!def) throw new Error(`unknown assembly "${assemblyId}"`);
3293
+ const densityOf = (id) => {
3294
+ const m = options.materials?.get(id) ?? MATERIALS.find((x) => x.id === id);
3295
+ if (!m) throw new Error(`unknown material "${id}" in assembly "${assemblyId}"`);
3296
+ return m.density;
3297
+ };
3298
+ const fill = def.fill ?? 1;
3299
+ const area = options.width * options.height;
3300
+ const coreMass = area * def.core.thickness * fill * densityOf(def.core.material);
3301
+ const layerMass = (def.layers ?? []).reduce((sum, l) => sum + area * l.thickness * faceCount(l) * densityOf(l.material), 0);
3302
+ return {
3303
+ id: options.id,
3304
+ material: def.core.material,
3305
+ shape: { kind: "box", size: [options.width, options.height, assemblyDepth(def)] },
3306
+ position: options.position,
3307
+ ...options.rotation ? { rotation: options.rotation } : {},
3308
+ mass: coreMass + layerMass,
3309
+ thickness: def.core.thickness * fill,
3310
+ ...def.layers ? { layers: def.layers.map((l) => ({ ...l })) } : {},
3311
+ ...def.units ? { units: { ...def.units, size: [...def.units.size] } } : {},
3312
+ ...def.reinforcement ? { reinforcement: { ...def.reinforcement } } : {},
3313
+ ...def.breach ? { breach: { ...def.breach } } : {},
3314
+ ...options.structure ? { structure: options.structure } : {},
3315
+ ...options.extra
3316
+ };
3317
+ };
3318
+
2861
3319
  // src/frame.ts
2862
3320
  var buildFrame = (o = {}) => {
2863
3321
  const spanX = o.spanX ?? 8, spanZ = o.spanZ ?? 8;
@@ -2965,6 +3423,8 @@ var GameDestruction = (() => {
2965
3423
  var sizeStructure = (spec, options = {}) => {
2966
3424
  const safety = options.safety ?? 0.65;
2967
3425
  const redundancy = options.redundancy;
3426
+ const redundancyRoles = options.redundancyRoles ?? ["column"];
3427
+ const safetyOf = (role) => options.safetyByRole?.[role] ?? safety;
2968
3428
  const loads = (s) => {
2969
3429
  const w = createDestruction(options.materials ? { materials: options.materials } : {});
2970
3430
  const id = w.add(s);
@@ -2977,11 +3437,11 @@ var GameDestruction = (() => {
2977
3437
  if (!p.structure || p.structure.role === "none") continue;
2978
3438
  const b = base.get(p.id);
2979
3439
  if (!b || b.stress <= 0) continue;
2980
- need.set(p.id, b.load / safety);
3440
+ need.set(p.id, b.load / safetyOf(p.structure.role));
2981
3441
  }
2982
3442
  if (redundancy !== void 0) {
2983
3443
  for (const col of out.parts) {
2984
- if (col.structure?.role !== "column") continue;
3444
+ if (!col.structure || !redundancyRoles.includes(col.structure.role)) continue;
2985
3445
  const reduced = {
2986
3446
  ...out,
2987
3447
  parts: out.parts.filter((p) => p.id !== col.id),
@@ -3081,6 +3541,10 @@ var GameDestruction = (() => {
3081
3541
  primed: "about to explode",
3082
3542
  stress: "overload",
3083
3543
  collapse: "collapsed",
3544
+ spall: "plaster came off",
3545
+ stage: "damage stage",
3546
+ expose: "rebar bare",
3547
+ body: "body",
3084
3548
  destroyed: "destroyed"
3085
3549
  },
3086
3550
  fractureStyles: { shatter: "shatter", crumble: "crumble", splinter: "splinter", tear: "tear", snap: "snap", none: "none" },
@@ -3163,6 +3627,10 @@ var GameDestruction = (() => {
3163
3627
  primed: "prestes a explodir",
3164
3628
  stress: "sobrecarga",
3165
3629
  collapse: "desabou",
3630
+ spall: "reboco soltou",
3631
+ stage: "est\xE1gio de dano",
3632
+ expose: "vergalh\xE3o \xE0 mostra",
3633
+ body: "corpo",
3166
3634
  destroyed: "destru\xEDdo"
3167
3635
  },
3168
3636
  fractureStyles: { shatter: "estilha\xE7ar", crumble: "esfarelar", splinter: "lascar", tear: "rasgar", snap: "partir", none: "nenhuma" },