@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.
package/dist/index.js CHANGED
@@ -419,6 +419,153 @@ var planFragments = (inp) => {
419
419
  return out;
420
420
  };
421
421
 
422
+ // src/breach.ts
423
+ var planeAxes = (thin) => thin === 1 ? [0, 2] : [thin === 0 ? 2 : 0, 1];
424
+ var unitGrid = (dims, thin, units, cap) => {
425
+ const [u, v] = planeAxes(thin);
426
+ let unitU = Math.min(units.size[0], dims[u]), unitV = Math.min(units.size[1], dims[v]);
427
+ const rowsFor = (h) => Math.ceil(dims[v] / h - 1e-9);
428
+ const colsFor = (w) => Math.ceil(dims[u] / w - 1e-9) + (units.bond === "running" ? 1 : 0);
429
+ const merge = Math.ceil(Math.sqrt(rowsFor(unitV) * colsFor(unitU) / cap));
430
+ if (merge > 1) {
431
+ unitU = Math.min(unitU * merge, dims[u]);
432
+ unitV = Math.min(unitV * merge, dims[v]);
433
+ }
434
+ const cells = [];
435
+ for (let row = 0; row < rowsFor(unitV); row++) {
436
+ const v0 = -dims[v] / 2 + row * unitV, v1 = Math.min(v0 + unitV, dims[v] / 2);
437
+ const shift = units.bond === "running" && row % 2 === 1 ? unitU / 2 : 0;
438
+ for (let col = 0; col < colsFor(unitU); col++) {
439
+ 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);
440
+ if (u1 - u0 < 0.01) continue;
441
+ const center = [0, 0, 0], size = [0, 0, 0];
442
+ center[u] = (u0 + u1) / 2;
443
+ center[v] = (v0 + v1) / 2;
444
+ size[u] = u1 - u0;
445
+ size[v] = v1 - v0;
446
+ size[thin] = dims[thin];
447
+ cells.push({ center, size });
448
+ }
449
+ }
450
+ return cells;
451
+ };
452
+ var planeDistance = (cell, thin, point) => {
453
+ const [u, v] = planeAxes(thin);
454
+ const du = cell.center[u] - point[u], dv = cell.center[v] - point[v];
455
+ return Math.sqrt(du * du + dv * dv);
456
+ };
457
+ var cellsWithin = (cells, thin, point, radius) => {
458
+ const inside = cells.filter((c) => planeDistance(c, thin, point) <= radius);
459
+ if (inside.length > 0 || cells.length === 0) return inside;
460
+ return [cells.reduce((best, c) => planeDistance(c, thin, point) < planeDistance(best, thin, point) ? c : best)];
461
+ };
462
+ var cellsOutside = (cells, thin, point, radius) => cells.filter((c) => planeDistance(c, thin, point) > radius);
463
+ var MAX_MARKS_PER_FRAGMENT = 12;
464
+ var marksInBox = (center, size, rotation, holes) => {
465
+ const inverse = rotation ? quat.conjugate(rotation) : void 0;
466
+ const marks = [];
467
+ for (const hole of holes) {
468
+ const offset = [hole.point[0] - center[0], hole.point[1] - center[1], hole.point[2] - center[2]];
469
+ const point = inverse ? rotateVec(inverse, offset) : offset;
470
+ const inside = point.every((value, k) => Math.abs(value) <= size[k] / 2 + hole.diameter / 2);
471
+ if (!inside) continue;
472
+ marks.push({ point, normal: inverse ? rotateVec(inverse, hole.normal) : hole.normal, diameter: hole.diameter });
473
+ if (marks.length >= MAX_MARKS_PER_FRAGMENT) break;
474
+ }
475
+ return marks;
476
+ };
477
+
478
+ // src/layers.ts
479
+ var MAX_SPALLS = 64;
480
+ var thinAxisOf = (dims) => dims[0] <= dims[1] && dims[0] <= dims[2] ? 0 : dims[1] <= dims[2] ? 1 : 2;
481
+ var resolveLayer = (def, index, mat) => ({ index, def, mat, energy: mat.penetration * def.thickness * 1e3 });
482
+ var onFace = (layer, face) => layer.def.faces === "both" || layer.def.faces === (face > 0 ? "+" : "-");
483
+ var isSpalled = (spalls, layer, face, point) => {
484
+ for (const s of spalls ?? []) {
485
+ if (s[0] !== layer || s[1] !== face) continue;
486
+ const dx = s[2] - point[0], dy = s[3] - point[1], dz = s[4] - point[2];
487
+ if (dx * dx + dy * dy + dz * dz <= s[5] * s[5]) return true;
488
+ }
489
+ return false;
490
+ };
491
+ var outerLayer = (layers, spalls, face, point) => layers.find((l) => onFace(l, face) && !isSpalled(spalls, l.index, face, point));
492
+ var layersEnergy = (layers, spalls, face, point) => {
493
+ let sum = 0;
494
+ for (const l of layers) if (onFace(l, face) && !isSpalled(spalls, l.index, face, point)) sum += l.energy;
495
+ return sum;
496
+ };
497
+ var layersThickness = (layers, face) => {
498
+ let sum = 0;
499
+ for (const l of layers) if (onFace(l, face)) sum += l.def.thickness;
500
+ return sum;
501
+ };
502
+
503
+ // src/rebar.ts
504
+ var COVER = 0.03;
505
+ var MAX_BARS_PER_DIRECTION = 24;
506
+ var MAX_MEMBER_BARS_PER_SIDE = 6;
507
+ var STEEL_DENSITY = 7850;
508
+ var spread = (length, cover, spacing, cap) => {
509
+ const room = Math.max(0, length - 2 * cover);
510
+ const count = Math.min(cap, Math.max(2, Math.floor(room / spacing) + 1));
511
+ const step = count > 1 && room / (count - 1) < spacing ? room / (count - 1) : spacing;
512
+ return Array.from({ length: count }, (_, i) => (i - (count - 1) / 2) * step);
513
+ };
514
+ var bar = (axis, along, fixed) => {
515
+ const a = [0, 0, 0], b = [0, 0, 0];
516
+ for (const [index, value] of fixed) {
517
+ a[index] = value;
518
+ b[index] = value;
519
+ }
520
+ a[axis] = -along;
521
+ b[axis] = along;
522
+ return [a, b];
523
+ };
524
+ var isMember = (dims) => {
525
+ const [a = 0, b = 0, c = 0] = [...dims].sort((x, y) => x - y);
526
+ return b / a < 3 && c / b >= 3;
527
+ };
528
+ var rebarBars = (dims, thin, spacing) => {
529
+ const out = [];
530
+ if (isMember(dims)) {
531
+ const long = dims.indexOf(Math.max(...dims));
532
+ const [u2 = 0, v2 = 0] = [0, 1, 2].filter((i) => i !== long);
533
+ const us = spread(dims[u2], COVER, spacing, MAX_MEMBER_BARS_PER_SIDE);
534
+ const vs = spread(dims[v2], COVER, spacing, MAX_MEMBER_BARS_PER_SIDE);
535
+ us.forEach((x, i) => vs.forEach((y, j) => {
536
+ 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]]));
537
+ }));
538
+ return out;
539
+ }
540
+ const [u = 0, v = 0] = [0, 1, 2].filter((i) => i !== thin);
541
+ 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]]));
542
+ 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]]));
543
+ return out;
544
+ };
545
+ var clipBarsToDisc = (bars, u, v, center, radius) => {
546
+ const out = [];
547
+ for (const [a, b] of bars) {
548
+ const axis = a[u] !== b[u] ? u : v;
549
+ const other = axis === u ? v : u;
550
+ const off = a[other] - center[other === u ? 0 : 1];
551
+ if (Math.abs(off) >= radius) continue;
552
+ const half = Math.sqrt(radius * radius - off * off);
553
+ const mid = center[axis === u ? 0 : 1];
554
+ const lo = Math.max(Math.min(a[axis], b[axis]), mid - half), hi = Math.min(Math.max(a[axis], b[axis]), mid + half);
555
+ if (hi - lo < 1e-6) continue;
556
+ const p = [a[0], a[1], a[2]], q = [b[0], b[1], b[2]];
557
+ p[axis] = lo;
558
+ q[axis] = hi;
559
+ out.push([p, q]);
560
+ }
561
+ return out;
562
+ };
563
+ var rebarMass = (bars, diameter, density = STEEL_DENSITY) => {
564
+ let length = 0;
565
+ 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]);
566
+ return Math.PI * (diameter / 2) * (diameter / 2) * length * density;
567
+ };
568
+
422
569
  // src/data/materials.json
423
570
  var materials_default = [
424
571
  {
@@ -1427,9 +1574,19 @@ var fragility = (ductility, thickness) => {
1427
1574
  var DMG_PER_J = 0.02;
1428
1575
  var BLAST_COUPLING = 0.1;
1429
1576
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
1577
+ var BREACH_RADIUS = 0.55;
1578
+ var BREACH_BLAST_FACTOR = 6;
1579
+ var BREACH_AREA = 0.25;
1580
+ var RESIDUAL_CAPACITY = 0.15;
1581
+ var UNIT_FRAGMENT_FACTOR = 4;
1582
+ var BREACH_GRID_CAP = 4096;
1430
1583
  var CUT_SLICE = 1;
1431
1584
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
1432
1585
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
1586
+ var capacityScale = (p) => {
1587
+ if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
1588
+ return p.rt.removed ? Math.max(0.05, 1 - p.rt.removed / p.mass0) : 1;
1589
+ };
1433
1590
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
1434
1591
  var clone = (v) => structuredClone(v);
1435
1592
  var overloadRate = (stress) => Math.min(0.6, 0.12 + 0.8 * (stress - 1));
@@ -1452,6 +1609,8 @@ var createDestruction = (options = {}) => {
1452
1609
  const maxDents = options.maxDents ?? 24;
1453
1610
  const maxHoles = options.maxHoles ?? 32;
1454
1611
  const fireSpread = options.fireSpread ?? 0.6;
1612
+ const breachRadius = options.breach?.radius ?? BREACH_RADIUS;
1613
+ const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
1455
1614
  const rng = createRng(options.seed ?? 1);
1456
1615
  const rnd = () => rng.next();
1457
1616
  let time = 0;
@@ -1556,6 +1715,13 @@ var createDestruction = (options = {}) => {
1556
1715
  const maxHp = Math.max(1, ps.hp ?? mass0 * mat.hpPerKg);
1557
1716
  const fuel0 = mat.thermal.ignitionC !== void 0 ? mass0 * (mat.thermal.fuelFraction ?? 0) : 0;
1558
1717
  const c = carry?.parts.get(ps.id);
1718
+ const staged = !!(ps.layers?.length || ps.reinforcement || ps.units || ps.breach);
1719
+ const layers = (ps.layers ?? []).map((l, i) => {
1720
+ const lm = materials.get(l.material);
1721
+ if (!lm) throw new Error(`unknown material "${l.material}" on layer ${i} of part "${ps.id}"`);
1722
+ return resolveLayer(l, i, lm);
1723
+ });
1724
+ if (ps.reinforcement && !materials.has(ps.reinforcement.material)) throw new Error(`unknown material "${ps.reinforcement.material}" on the reinforcement of part "${ps.id}"`);
1559
1725
  const pst = c?.st ?? {
1560
1726
  id: ps.id,
1561
1727
  material: ps.material,
@@ -1571,7 +1737,8 @@ var createDestruction = (options = {}) => {
1571
1737
  dents: [],
1572
1738
  holes: [],
1573
1739
  cracks: [],
1574
- group: 0
1740
+ group: 0,
1741
+ ...staged ? { stage: 0 } : {}
1575
1742
  };
1576
1743
  if (!c && ps.contains) pst.liters = ps.contains.liters;
1577
1744
  const p = {
@@ -1590,7 +1757,10 @@ var createDestruction = (options = {}) => {
1590
1757
  rt: c?.rt ?? { ...freshRuntime(), prevTemp: pst.temperature },
1591
1758
  structural: !!ps.structure && ps.structure.role !== "none",
1592
1759
  anchored: !!ps.anchored || ps.structure?.role === "foundation",
1593
- cap: ps.structure ? partCapacity(ps, mat) : 0
1760
+ cap: ps.structure ? partCapacity(ps, mat) : 0,
1761
+ staged,
1762
+ thin: thinAxisOf(shapeDims(ps.shape)),
1763
+ layers
1594
1764
  };
1595
1765
  if (p.structural) o.hasStructure = true;
1596
1766
  if (p.rt.chainDue !== void 0 && !isLost(p)) primed.set(p, o);
@@ -1730,7 +1900,7 @@ var createDestruction = (options = {}) => {
1730
1900
  o.unsup[i] = o.anyAnchor && !isLost(p);
1731
1901
  continue;
1732
1902
  }
1733
- p.st.stress = p.st.load / p.cap;
1903
+ p.st.stress = p.st.load / (p.cap * capacityScale(p));
1734
1904
  if (p.st.stress > o.maxStress) o.maxStress = p.st.stress;
1735
1905
  }
1736
1906
  if (o.maxStress > 0.8) o.active = true;
@@ -1749,6 +1919,7 @@ var createDestruction = (options = {}) => {
1749
1919
  for (const p of ps) {
1750
1920
  o.collapsed.add(p.spec.id);
1751
1921
  p.st.status = "collapsed";
1922
+ setStage(o, p, 3, {}, src);
1752
1923
  }
1753
1924
  emit({ ...base(o, src), type: "collapse", parts: ps.map((p) => p.spec.id), mass: ps.reduce((sum, p) => sum + p.st.mass, 0) });
1754
1925
  }
@@ -1802,6 +1973,7 @@ var createDestruction = (options = {}) => {
1802
1973
  if (isLost(p) || ignore?.has(`${o.id}\0${p.spec.id}`)) continue;
1803
1974
  const r = rayPart(o, p, origin, d);
1804
1975
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
1976
+ if (p.st.opening && insideOpening(o, p, vec.madd(origin, d, r.t0))) continue;
1805
1977
  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 };
1806
1978
  }
1807
1979
  }
@@ -1910,6 +2082,7 @@ var createDestruction = (options = {}) => {
1910
2082
  if (p.st.holes.length > maxHoles) p.st.holes.shift();
1911
2083
  markDamaged(p);
1912
2084
  emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {} });
2085
+ setStage(o, p, 1, {}, src);
1913
2086
  return hole;
1914
2087
  };
1915
2088
  const addCrack = (o, p, wp, wn, length, src) => {
@@ -1918,6 +2091,7 @@ var createDestruction = (options = {}) => {
1918
2091
  if (p.st.cracks.length > 16) p.st.cracks.shift();
1919
2092
  markDamaged(p);
1920
2093
  emit({ ...base(o, src), type: "crack", part: p.spec.id, crack, worldPoint: wp });
2094
+ setStage(o, p, 1, {}, src);
1921
2095
  };
1922
2096
  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 });
1923
2097
  const holeRate = (hole, pressurized) => (pressurized ? 0.5 : 0.1) * (hole.diameter / 9e-3);
@@ -1966,6 +2140,7 @@ var createDestruction = (options = {}) => {
1966
2140
  if (split && mine.length) amount /= mine.length;
1967
2141
  for (const j of mine) {
1968
2142
  if (j.st.open) continue;
2143
+ if (j.spec.kind === "rebar" && (o.parts[j.ia].spec.reinforcement || o.parts[j.ib].spec.reinforcement)) continue;
1969
2144
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
1970
2145
  j.st.hp -= amount;
1971
2146
  if (j.st.hp <= 0) {
@@ -1979,23 +2154,172 @@ var createDestruction = (options = {}) => {
1979
2154
  }
1980
2155
  }
1981
2156
  };
2157
+ const setStage = (o, p, stage, extra = {}, src) => {
2158
+ if (!p.staged || (p.st.stage ?? 0) >= stage) return;
2159
+ p.st.stage = stage;
2160
+ emit({ ...base(o, src), type: "stage", part: p.spec.id, stage, ...extra });
2161
+ };
2162
+ const faceOf = (o, p, worldNormal) => rotateVec(quat.conjugate(partXf(o, p).q), worldNormal)[p.thin] >= 0 ? 1 : -1;
2163
+ const spallOuter = (o, p, wp, face, normal, radius, src) => {
2164
+ var _a;
2165
+ if (p.layers.length === 0) return;
2166
+ const lp = objLocal(o, wp);
2167
+ const layer = outerLayer(p.layers, p.rt.spalls, face, lp);
2168
+ if (!layer) return;
2169
+ const spalls = (_a = p.rt).spalls ?? (_a.spalls = []);
2170
+ spalls.push([layer.index, face, lp[0], lp[1], lp[2], radius]);
2171
+ if (spalls.length > MAX_SPALLS) spalls.shift();
2172
+ markDamaged(p);
2173
+ emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: layer.index, point: wp, area: Math.PI * radius * radius });
2174
+ debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
2175
+ setStage(o, p, 1, {}, src);
2176
+ };
2177
+ const insideOpening = (o, p, objectPoint) => {
2178
+ const op = p.st.opening;
2179
+ if (!op) return false;
2180
+ const [u, v] = planeAxes(p.thin);
2181
+ const lp = toLocal(p.pos, p.q, objectPoint);
2182
+ const lc = toLocal(p.pos, p.q, op.center);
2183
+ return Math.hypot(lp[u] - lc[u], lp[v] - lc[v]) <= op.radius;
2184
+ };
2185
+ const breachAreaOf = (p, damage, blast) => damage / (p.mat.hpPerKg * p.mat.density) / Math.max(p.thickness, 5e-3) * (blast ? breachBlastFactor : 1);
2186
+ 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 }));
2187
+ const unitsOf = (p) => p.spec.units ?? { size: [p.mat.fragmentSize?.[1] ?? 0.2, p.mat.fragmentSize?.[1] ?? 0.2, p.thickness], bond: "stack" };
2188
+ const FRAGMENT_SHAPE = { shatter: "shard", splinter: "splinter", tear: "scrap" };
2189
+ const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag) => {
2190
+ const { c, q } = partXf(o, p);
2191
+ const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
2192
+ const [u, v] = planeAxes(p.thin);
2193
+ const faceArea = (cell) => cell.size[u] * cell.size[v];
2194
+ const total = cells.reduce((sum, cell) => sum + faceArea(cell), 0) || 1;
2195
+ const shape = p.spec.units ? "chunk" : FRAGMENT_SHAPE[p.mat.fracture] ?? "chunk";
2196
+ return cells.map((cell, i) => {
2197
+ const cw = toWorld(c, q, cell.center);
2198
+ const out = vec.normalize(vec.sub(cw, hitW), [0, 1, 0]);
2199
+ const dv = dir ? vec.normalize(vec.madd(out, dir, 0.6)) : out;
2200
+ const k = speed * (0.6 + 0.8 * rnd());
2201
+ const marks = marksInBox(cell.center, cell.size, void 0, holes);
2202
+ return {
2203
+ id: `${p.spec.id}#${tag}${i}`,
2204
+ part: p.spec.id,
2205
+ material: p.spec.material,
2206
+ shape,
2207
+ center: cw,
2208
+ size: [cell.size[0], cell.size[1], cell.size[2]],
2209
+ rotation: q,
2210
+ velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
2211
+ angularVelocity: [(rnd() - 0.5) * 8, (rnd() - 0.5) * 8, (rnd() - 0.5) * 8],
2212
+ mass: mass * faceArea(cell) / total,
2213
+ ...marks.length > 0 ? { marks } : {}
2214
+ };
2215
+ });
2216
+ };
2217
+ const worldBars = (o, p, bars) => {
2218
+ const { c, q } = partXf(o, p);
2219
+ return bars.map(([a, b]) => [toWorld(c, q, a), toWorld(c, q, b)]);
2220
+ };
2221
+ const addBreach = (o, p, wp, area, energy, dir, src) => {
2222
+ var _a;
2223
+ if (!p.staged || area <= 0 || (p.st.stage ?? 0) >= 2 || p.st.stripped || isLost(p)) return;
2224
+ const lp = objLocal(o, wp);
2225
+ const list = (_a = p.rt).breach ?? (_a.breach = []);
2226
+ list.push([lp[0], lp[1], lp[2], area]);
2227
+ if (list.length > MAX_SPALLS) list.shift();
2228
+ let total = 0;
2229
+ const sum = [0, 0, 0];
2230
+ for (const [x, y, z, a] of list) {
2231
+ if (vec.distance([x, y, z], lp) > breachRadius) continue;
2232
+ total += a;
2233
+ sum[0] += x * a;
2234
+ sum[1] += y * a;
2235
+ sum[2] += z * a;
2236
+ }
2237
+ if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
2238
+ openBreach(o, p, [sum[0] / total, sum[1] / total, sum[2] / total], total, energy, dir, src);
2239
+ };
2240
+ const openBreach = (o, p, center, total, energy, dir, src) => {
2241
+ const [u, v] = planeAxes(p.thin);
2242
+ const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
2243
+ const lc = toLocal(p.pos, p.q, center);
2244
+ lc[p.thin] = 0;
2245
+ const { c, q } = partXf(o, p);
2246
+ const worldCenter = toWorld(c, q, lc);
2247
+ if (2 * radius >= Math.min(p.dims[u], p.dims[v])) {
2248
+ failPart(o, p, worldCenter, dir, energy, 12, src);
2249
+ return;
2250
+ }
2251
+ const cells = cellsWithin(unitGrid(p.dims, p.thin, unitsOf(p), BREACH_GRID_CAP), p.thin, lc, radius);
2252
+ const removed = p.st.mass * cells.reduce((sum, cell) => sum + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
2253
+ const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
2254
+ const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
2255
+ const holes = localHolesOf(p);
2256
+ p.st.holes = p.st.holes.filter((_, i) => !cells.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[i]]).length > 0));
2257
+ p.st.mass = Math.max(0, p.st.mass - removed);
2258
+ p.rt.removed = (p.rt.removed ?? 0) + removed;
2259
+ p.st.opening = { center: objLocal(o, worldCenter), radius };
2260
+ markDamaged(p);
2261
+ setStage(o, p, 1, {}, src);
2262
+ setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
2263
+ debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
2264
+ const reinforcement = p.spec.reinforcement;
2265
+ if (reinforcement) {
2266
+ const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
2267
+ if (bars.length > 0) emit({ ...base(o, src), type: "expose", part: p.spec.id, bars });
2268
+ }
2269
+ if (p.structural) solveStructure(o);
2270
+ };
2271
+ const stripPart = (o, p, reinforcement, src) => {
2272
+ const bars = rebarBars(p.dims, p.thin, reinforcement.spacing);
2273
+ p.st.stripped = true;
2274
+ p.st.hp = p.st.maxHp;
2275
+ p.st.mass = rebarMass(bars, reinforcement.diameter, materials.get(reinforcement.material)?.density);
2276
+ p.st.holes = [];
2277
+ p.st.burning = false;
2278
+ p.rt.jet = false;
2279
+ markDamaged(p);
2280
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
2281
+ solveStructure(o);
2282
+ collapseCheck(o, src);
2283
+ };
2284
+ const unitFragmentPlan = (p, cap) => {
2285
+ const grid = unitGrid(p.dims, p.thin, unitsOf(p), cap);
2286
+ const op = p.st.opening;
2287
+ const cells = op ? cellsOutside(grid, p.thin, toLocal(p.pos, p.q, op.center), op.radius) : grid;
2288
+ const [u, v] = planeAxes(p.thin);
2289
+ return cells.map((cell) => ({ center: cell.center, size: cell.size, q: quat.identity(), shape: "chunk", weight: cell.size[u] * cell.size[v] }));
2290
+ };
2291
+ const loseCage = (o, p, src) => {
2292
+ p.st.status = "broken";
2293
+ p.st.hp = 0;
2294
+ p.st.mass = 0;
2295
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: [] });
2296
+ debris(o, p, "sparks", partXf(o, p).c, [0, 1, 0], 6, 3, src);
2297
+ partLost(o, p, src);
2298
+ };
1982
2299
  const failPart = (o, p, origin, dir, fragEnergy, cap, src) => {
1983
2300
  if (isLost(p)) return;
2301
+ if (p.st.stripped) {
2302
+ loseCage(o, p, src);
2303
+ return;
2304
+ }
1984
2305
  const ex = p.spec.explodes;
1985
2306
  const style = p.mat.fracture === "none" ? "snap" : p.mat.fracture;
1986
2307
  const { c, q } = partXf(o, p);
1987
2308
  const hitW = origin ?? c;
1988
2309
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
1989
- const plan = planFragments({
2310
+ const hitLocal = toLocal(c, q, hitW);
2311
+ const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
2312
+ const plan = unitPlan.length > 0 ? unitPlan : planFragments({
1990
2313
  style,
1991
2314
  dims: p.dims,
1992
2315
  thickness: p.thickness,
1993
2316
  grain,
1994
- hit: toLocal(c, q, hitW),
2317
+ hit: hitLocal,
1995
2318
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
1996
2319
  maxFragments,
1997
2320
  rnd
1998
2321
  });
2322
+ const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
1999
2323
  const mass = p.st.mass;
2000
2324
  const totW = plan.reduce((s, f) => s + f.weight, 0) || 1;
2001
2325
  const energy = ex ? ex.energy * 0.2 : fragEnergy;
@@ -2005,6 +2329,7 @@ var createDestruction = (options = {}) => {
2005
2329
  const out = vec.normalize(vec.sub(cw, hitW), [0, 1, 0]);
2006
2330
  const dv = dir ? vec.normalize(vec.madd(out, dir, 0.6)) : out;
2007
2331
  const k = speed * (0.6 + 0.8 * rnd());
2332
+ const marks = marksInBox(f.center, f.size, f.q, holes);
2008
2333
  return {
2009
2334
  id: `${p.spec.id}#${i}`,
2010
2335
  part: p.spec.id,
@@ -2015,16 +2340,25 @@ var createDestruction = (options = {}) => {
2015
2340
  rotation: quat.mul(q, f.q),
2016
2341
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
2017
2342
  angularVelocity: [(rnd() - 0.5) * 12, (rnd() - 0.5) * 12, (rnd() - 0.5) * 12],
2018
- mass: mass * f.weight / totW
2343
+ mass: mass * f.weight / totW,
2344
+ ...marks.length > 0 ? { marks } : {}
2019
2345
  };
2020
2346
  });
2021
- p.st.status = "broken";
2022
- p.st.hp = 0;
2023
- p.st.mass = 0;
2024
- p.st.burning = false;
2025
- p.rt.jet = false;
2347
+ const reinforcement = p.spec.reinforcement;
2348
+ if (!reinforcement) {
2349
+ p.st.status = "broken";
2350
+ p.st.hp = 0;
2351
+ p.st.mass = 0;
2352
+ p.st.burning = false;
2353
+ p.rt.jet = false;
2354
+ }
2026
2355
  emit({ ...base(o, src), type: "fracture", part: p.spec.id, style, fragments, point: hitW });
2027
2356
  debris(o, p, p.mat.breakDebris, c, [0, 1, 0], clamp2(Math.round(Math.cbrt(partVolume(p.spec.shape)) * 30), 3, 20), 2, src);
2357
+ setStage(o, p, 3, {}, src);
2358
+ if (reinforcement) {
2359
+ stripPart(o, p, reinforcement, src);
2360
+ return;
2361
+ }
2028
2362
  if (ex) {
2029
2363
  emit({ ...base(o, src), type: "explode", part: p.spec.id, point: c, energy: ex.energy, radius: ex.radius });
2030
2364
  blasts.push({ point: c, energy: ex.energy, radius: ex.radius, source: src });
@@ -2058,10 +2392,17 @@ var createDestruction = (options = {}) => {
2058
2392
  };
2059
2393
  const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src) => {
2060
2394
  const mat = p.mat;
2395
+ if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry))) {
2396
+ return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry, d, Math.min(pathLen, p.thickness)), damage: 0 };
2397
+ }
2061
2398
  const sin = Math.min(1, Math.abs(vec.dot(d, n)));
2062
2399
  const angle = Math.asin(sin) * 180 / Math.PI;
2063
- const effM = p.thickness / Math.max(sin, 0.3);
2064
- const thr = mat.penetration * effM * 1e3;
2400
+ const slant = Math.max(sin, 0.3);
2401
+ const effM = p.thickness / slant;
2402
+ const face = p.layers.length > 0 ? faceOf(o, p, n) : 1;
2403
+ const exitLocal = objLocal(o, vec.madd(entry, d, Math.min(pathLen, effM)));
2404
+ 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;
2405
+ const thr = coats > 0 ? (mat.penetration * p.thickness * 1e3 + coats) / slant : mat.penetration * effM * 1e3;
2065
2406
  const penetrated = E > thr;
2066
2407
  const scale = (mat.damageScale?.ballistic ?? 1) * (mat.fracture === "shatter" ? fragility(mat.ductility, p.thickness) : 1);
2067
2408
  const id = p.spec.id;
@@ -2072,13 +2413,16 @@ var createDestruction = (options = {}) => {
2072
2413
  const dmg2 = applyDamage(o, p, E * DMG_PER_J * scale * 0.2, "ballistic", entry, d, E * 0.1, 12, src);
2073
2414
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
2074
2415
  }
2416
+ if (p.layers.length > 0) spallOuter(o, p, entry, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
2075
2417
  const brittle = mat.ductility < 0.3;
2076
2418
  const ratio = E / Math.max(thr, 1e-6);
2077
2419
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
2078
2420
  let exitPoint;
2079
2421
  if (penetrated) {
2080
- exitPoint = vec.madd(entry, d, Math.min(pathLen, effM));
2422
+ exitPoint = vec.madd(entry, d, Math.min(pathLen, effM + layersThickness(p.layers, face) / slant + layersThickness(p.layers, -face) / slant));
2081
2423
  const hole = addHole(o, p, entry, n, caliber / 1e3 * (mat.ductility >= 0.3 ? 1 : 1.5), true, exitPoint, src);
2424
+ if (p.layers.length > 0) spallOuter(o, p, exitPoint, -face, vec.scale(n, -1), Math.max(0.03, caliber / 1e3 * 4), src);
2425
+ addBreach(o, p, entry, Math.PI * (hole.diameter / 2) * (hole.diameter / 2), E, d, src);
2082
2426
  if (brittle && mat.fracture === "shatter") addCrack(o, p, entry, n, crackLen, src);
2083
2427
  if (p.spec.contains && (p.st.liters ?? 0) > 0) {
2084
2428
  emit({
@@ -2123,8 +2467,11 @@ var createDestruction = (options = {}) => {
2123
2467
  }
2124
2468
  if (kind === "blunt") wearJoints(o, p, E * 0.3, void 0, 0, src);
2125
2469
  else wearJoints(o, p, E * 0.3, point, 0.06, src);
2470
+ if (kind === "blunt" && E > 0 && p.layers.length > 0) spallOuter(o, p, point, faceOf(o, p, n), n, Math.max(contactR, 0.05), src);
2126
2471
  const scale = mat.damageScale?.[kind] ?? 1;
2127
- return applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
2472
+ const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
2473
+ addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
2474
+ return done;
2128
2475
  };
2129
2476
  const blastObject = (o, point, energy, radius, src) => {
2130
2477
  const start = cur.length;
@@ -2160,7 +2507,10 @@ var createDestruction = (options = {}) => {
2160
2507
  }
2161
2508
  }
2162
2509
  wearJoints(o, p, Ein * 0.5, void 0, 0, src);
2163
- res.damage += applyDamage(o, p, Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1), "explosive", point, null, Ein * 0.3, 40, src);
2510
+ 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);
2511
+ const dealt = applyDamage(o, p, Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1), "explosive", point, null, Ein * 0.3, 40, src);
2512
+ res.damage += dealt;
2513
+ addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
2164
2514
  }
2165
2515
  res.events = cur.slice(start);
2166
2516
  return res;
@@ -2431,7 +2781,7 @@ var createDestruction = (options = {}) => {
2431
2781
  }
2432
2782
  o.active = hot;
2433
2783
  };
2434
- const spread = (sources, dt) => {
2784
+ const spread2 = (sources, dt) => {
2435
2785
  for (const src of sources) {
2436
2786
  if (!src.p.st.burning && !src.p.rt.jet) continue;
2437
2787
  const sc = toWorld(src.o.st.position, src.o.st.rotation, src.p.pos);
@@ -2732,7 +3082,7 @@ var createDestruction = (options = {}) => {
2732
3082
  time += dt;
2733
3083
  const sources = [];
2734
3084
  for (const o of [...objects.values()]) if (o.active) stepObject(o, dt, sources);
2735
- if (sources.length) spread(sources, dt);
3085
+ if (sources.length) spread2(sources, dt);
2736
3086
  detonatePrimed(time);
2737
3087
  for (const im of [...impls.values()]) {
2738
3088
  if (!im.impl.update) continue;
@@ -2771,7 +3121,7 @@ var createDestruction = (options = {}) => {
2771
3121
  counter,
2772
3122
  objects: [...objects.values()].map((o) => {
2773
3123
  const runtime = { parts: {}, joints: {}, collapsed: [...o.collapsed] };
2774
- for (const p of o.parts) runtime.parts[p.spec.id] = { ...p.rt };
3124
+ for (const p of o.parts) runtime.parts[p.spec.id] = clone(p.rt);
2775
3125
  for (const j of o.joints) runtime.joints[j.spec.id] = j.step;
2776
3126
  return { id: o.id, spec: clone(o.spec), state: clone(o.st), runtime };
2777
3127
  }),
@@ -2788,7 +3138,7 @@ var createDestruction = (options = {}) => {
2788
3138
  const state = clone(e.state);
2789
3139
  const carry = {
2790
3140
  parts: new Map(
2791
- state.parts.map((st) => [st.id, { st, rt: e.runtime?.parts[st.id] ? { ...e.runtime.parts[st.id] } : { ...freshRuntime(), prevTemp: st.temperature } }])
3141
+ state.parts.map((st) => [st.id, { st, rt: e.runtime?.parts[st.id] ? clone(e.runtime.parts[st.id]) : { ...freshRuntime(), prevTemp: st.temperature } }])
2792
3142
  ),
2793
3143
  joints: new Map(state.joints.map((st) => [st.id, { st, step: e.runtime?.joints[st.id] ?? 0 }])),
2794
3144
  collapsed: e.runtime?.collapsed
@@ -2807,6 +3157,109 @@ var createDestruction = (options = {}) => {
2807
3157
  return world;
2808
3158
  };
2809
3159
 
3160
+ // src/data/assemblies.json
3161
+ var assemblies_default = [
3162
+ {
3163
+ id: "brick_wall_plastered",
3164
+ name: "Plastered brick wall",
3165
+ core: { material: "brick", thickness: 0.09 },
3166
+ layers: [{ material: "plaster", thickness: 0.02, faces: "both" }],
3167
+ units: { size: [0.23, 0.075, 0.11], bond: "running", mortar: "plaster" },
3168
+ breach: { area: 0.25 }
3169
+ },
3170
+ {
3171
+ id: "concrete_block_wall",
3172
+ name: "Concrete block wall",
3173
+ core: { material: "concrete", thickness: 0.14 },
3174
+ fill: 0.55,
3175
+ units: { size: [0.39, 0.19, 0.14], bond: "running", mortar: "plaster" },
3176
+ breach: { area: 0.25 }
3177
+ },
3178
+ {
3179
+ id: "reinforced_concrete",
3180
+ name: "Reinforced concrete wall",
3181
+ core: { material: "concrete", thickness: 0.2 },
3182
+ reinforcement: { material: "steel", spacing: 0.18, diameter: 8e-3, residualCapacity: 0.15 },
3183
+ units: { size: [0.6, 0.42, 0.2], bond: "stack", mortar: "concrete" },
3184
+ breach: { area: 0.25 }
3185
+ },
3186
+ {
3187
+ id: "drywall",
3188
+ name: "Drywall partition on steel studs",
3189
+ core: { material: "plaster", thickness: 0.0125 },
3190
+ layers: [{ material: "plaster", thickness: 0.0125, faces: "+" }],
3191
+ depth: 0.095,
3192
+ breach: { area: 0.25 }
3193
+ },
3194
+ {
3195
+ id: "wood_frame_wall",
3196
+ name: "Wood frame wall",
3197
+ core: { material: "plywood", thickness: 0.012 },
3198
+ layers: [{ material: "plaster", thickness: 0.0125, faces: "-" }],
3199
+ depth: 0.1135,
3200
+ breach: { area: 0.25 }
3201
+ },
3202
+ {
3203
+ id: "log_wall",
3204
+ name: "Log wall",
3205
+ core: { material: "wood_soft", thickness: 0.2 },
3206
+ fill: 0.785,
3207
+ breach: { area: 0.25 }
3208
+ },
3209
+ {
3210
+ id: "sheet_metal_shack",
3211
+ name: "Corrugated sheet metal wall",
3212
+ core: { material: "steel", thickness: 6e-4 },
3213
+ depth: 0.02,
3214
+ breach: { area: 0.25 }
3215
+ },
3216
+ {
3217
+ id: "glass_pane",
3218
+ name: "Glass pane",
3219
+ core: { material: "glass", thickness: 6e-3 },
3220
+ breach: { area: 0.1 }
3221
+ }
3222
+ ];
3223
+
3224
+ // src/assemblies.ts
3225
+ var ASSEMBLIES = assemblies_default;
3226
+ var registerAssembly = (def) => {
3227
+ const at = ASSEMBLIES.findIndex((a) => a.id === def.id);
3228
+ if (at >= 0) ASSEMBLIES[at] = def;
3229
+ else ASSEMBLIES.push(def);
3230
+ };
3231
+ var getAssembly = (id) => ASSEMBLIES.find((a) => a.id === id);
3232
+ var faceCount = (layer) => layer.faces === "both" ? 2 : 1;
3233
+ var assemblyDepth = (def) => def.depth ?? def.core.thickness + (def.layers ?? []).reduce((sum, l) => sum + l.thickness * faceCount(l), 0);
3234
+ var assemblyPart = (assemblyId, options) => {
3235
+ const def = getAssembly(assemblyId);
3236
+ if (!def) throw new Error(`unknown assembly "${assemblyId}"`);
3237
+ const densityOf = (id) => {
3238
+ const m = options.materials?.get(id) ?? MATERIALS.find((x) => x.id === id);
3239
+ if (!m) throw new Error(`unknown material "${id}" in assembly "${assemblyId}"`);
3240
+ return m.density;
3241
+ };
3242
+ const fill = def.fill ?? 1;
3243
+ const area = options.width * options.height;
3244
+ const coreMass = area * def.core.thickness * fill * densityOf(def.core.material);
3245
+ const layerMass = (def.layers ?? []).reduce((sum, l) => sum + area * l.thickness * faceCount(l) * densityOf(l.material), 0);
3246
+ return {
3247
+ id: options.id,
3248
+ material: def.core.material,
3249
+ shape: { kind: "box", size: [options.width, options.height, assemblyDepth(def)] },
3250
+ position: options.position,
3251
+ ...options.rotation ? { rotation: options.rotation } : {},
3252
+ mass: coreMass + layerMass,
3253
+ thickness: def.core.thickness * fill,
3254
+ ...def.layers ? { layers: def.layers.map((l) => ({ ...l })) } : {},
3255
+ ...def.units ? { units: { ...def.units, size: [...def.units.size] } } : {},
3256
+ ...def.reinforcement ? { reinforcement: { ...def.reinforcement } } : {},
3257
+ ...def.breach ? { breach: { ...def.breach } } : {},
3258
+ ...options.structure ? { structure: options.structure } : {},
3259
+ ...options.extra
3260
+ };
3261
+ };
3262
+
2810
3263
  // src/frame.ts
2811
3264
  var buildFrame = (o = {}) => {
2812
3265
  const spanX = o.spanX ?? 8, spanZ = o.spanZ ?? 8;
@@ -2914,6 +3367,8 @@ var buildFrame = (o = {}) => {
2914
3367
  var sizeStructure = (spec, options = {}) => {
2915
3368
  const safety = options.safety ?? 0.65;
2916
3369
  const redundancy = options.redundancy;
3370
+ const redundancyRoles = options.redundancyRoles ?? ["column"];
3371
+ const safetyOf = (role) => options.safetyByRole?.[role] ?? safety;
2917
3372
  const loads = (s) => {
2918
3373
  const w = createDestruction(options.materials ? { materials: options.materials } : {});
2919
3374
  const id = w.add(s);
@@ -2926,11 +3381,11 @@ var sizeStructure = (spec, options = {}) => {
2926
3381
  if (!p.structure || p.structure.role === "none") continue;
2927
3382
  const b = base.get(p.id);
2928
3383
  if (!b || b.stress <= 0) continue;
2929
- need.set(p.id, b.load / safety);
3384
+ need.set(p.id, b.load / safetyOf(p.structure.role));
2930
3385
  }
2931
3386
  if (redundancy !== void 0) {
2932
3387
  for (const col of out.parts) {
2933
- if (col.structure?.role !== "column") continue;
3388
+ if (!col.structure || !redundancyRoles.includes(col.structure.role)) continue;
2934
3389
  const reduced = {
2935
3390
  ...out,
2936
3391
  parts: out.parts.filter((p) => p.id !== col.id),
@@ -3030,6 +3485,10 @@ var locales = {
3030
3485
  primed: "about to explode",
3031
3486
  stress: "overload",
3032
3487
  collapse: "collapsed",
3488
+ spall: "plaster came off",
3489
+ stage: "damage stage",
3490
+ expose: "rebar bare",
3491
+ body: "body",
3033
3492
  destroyed: "destroyed"
3034
3493
  },
3035
3494
  fractureStyles: { shatter: "shatter", crumble: "crumble", splinter: "splinter", tear: "tear", snap: "snap", none: "none" },
@@ -3112,6 +3571,10 @@ var locales = {
3112
3571
  primed: "prestes a explodir",
3113
3572
  stress: "sobrecarga",
3114
3573
  collapse: "desabou",
3574
+ spall: "reboco soltou",
3575
+ stage: "est\xE1gio de dano",
3576
+ expose: "vergalh\xE3o \xE0 mostra",
3577
+ body: "corpo",
3115
3578
  destroyed: "destru\xEDdo"
3116
3579
  },
3117
3580
  fractureStyles: { shatter: "estilha\xE7ar", crumble: "esfarelar", splinter: "lascar", tear: "rasgar", snap: "partir", none: "nenhuma" },
@@ -3128,17 +3591,21 @@ var checkDestructible = (impl, options = {}) => {
3128
3591
  return check(impl, contractOptions);
3129
3592
  };
3130
3593
  export {
3594
+ ASSEMBLIES,
3131
3595
  FIRE_PRIME_C,
3132
3596
  IMPACT_CONTRACT,
3133
3597
  JOINT_BY_MATERIALS,
3134
3598
  JOINT_DEFAULTS,
3135
3599
  MATERIALS,
3600
+ assemblyDepth,
3601
+ assemblyPart,
3136
3602
  buildFrame,
3137
3603
  checkDestructible,
3138
3604
  checkHit,
3139
3605
  createDestruction,
3140
3606
  explosivePart,
3141
3607
  fragility,
3608
+ getAssembly,
3142
3609
  guessMaterial,
3143
3610
  inferJoints,
3144
3611
  locales,
@@ -3148,6 +3615,7 @@ export {
3148
3615
  partVolume,
3149
3616
  quat,
3150
3617
  quatFromEuler,
3618
+ registerAssembly,
3151
3619
  resolveStrings,
3152
3620
  rotateVec,
3153
3621
  sizeStructure,