@adatechnology/game-destruction 0.9.0 → 0.11.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.
@@ -450,6 +450,47 @@ var GameDestruction = (() => {
450
450
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
451
451
  };
452
452
  var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
453
+ var splinterBox = (inp, box, limit = inp.maxFragments) => {
454
+ const { rnd } = inp;
455
+ const dims = box.s;
456
+ const [fmin, fmax] = inp.fragmentSize;
457
+ const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
458
+ const [a, b] = [0, 1, 2].filter((i) => i !== g);
459
+ const w = Math.max(fmin * 1.5, 0.01);
460
+ const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
461
+ const maxLen = fmax * 4;
462
+ const cells = bsp(box, c, [a, b], rnd);
463
+ const pieces = [];
464
+ for (const cell of cells) {
465
+ const extra = rnd() < 0.4 ? 1 : 0;
466
+ const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
467
+ for (const p of bsp(cell, k, [g], rnd)) pieces.push(p);
468
+ }
469
+ return pieces.slice(0, limit).map((p) => ({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] }));
470
+ };
471
+ var severAt = (inp, root, at, gap) => {
472
+ const dims = root.s;
473
+ const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
474
+ const half = dims[L] / 2;
475
+ const lo = clamp(at - gap / 2, -half, half), hi = clamp(at + gap / 2, -half, half);
476
+ const out = [];
477
+ const stub = (from, to) => {
478
+ if (to - from < 0.02) return;
479
+ const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
480
+ c[L] = (from + to) / 2;
481
+ sz[L] = to - from;
482
+ out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight: sz[0] * sz[1] * sz[2] });
483
+ };
484
+ stub(-half, lo);
485
+ stub(hi, half);
486
+ const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
487
+ band.c[L] = (lo + hi) / 2;
488
+ band.s[L] = Math.max(hi - lo, 0.01);
489
+ if (inp.style === "snap") {
490
+ for (const b of bsp(band, Math.max(3, Math.min(6, inp.maxFragments - out.length)), [0, 1, 2], inp.rnd)) out.push({ center: b.c, size: b.s, q: quat.identity(), shape: "chunk", weight: b.s[0] * b.s[1] * b.s[2] });
491
+ } else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
492
+ return out;
493
+ };
453
494
  var tearStack = (inp, root, t, u, v, n, sheet) => {
454
495
  const { dims, rnd, maxFragments } = inp;
455
496
  const layers = clamp(Math.ceil(dims[t] / STACK_LAYER), 2, 4);
@@ -478,6 +519,7 @@ var GameDestruction = (() => {
478
519
  const t = sorted[0];
479
520
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
480
521
  if (style === "snap") {
522
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
481
523
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
482
524
  for (const b of bsp(root, n2, [0, 1, 2], rnd)) out.push({ center: b.c, size: b.s, q: idq, shape: "slab", weight: b.s[0] * b.s[1] * b.s[2] });
483
525
  return out;
@@ -488,23 +530,8 @@ var GameDestruction = (() => {
488
530
  return out;
489
531
  }
490
532
  if (style === "splinter") {
491
- const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
492
- const [a, b] = [0, 1, 2].filter((i) => i !== g);
493
- const w = Math.max(fmin * 1.5, 0.01);
494
- const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(maxFragments / 2)));
495
- const maxLen = fmax * 4;
496
- const cells = bsp(root, c, [a, b], rnd);
497
- const pieces = [];
498
- for (const cell of cells) {
499
- let list = [cell];
500
- const extra = rnd() < 0.4 ? 1 : 0;
501
- const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
502
- list = bsp(cell, k, [g], rnd);
503
- for (const p of list) pieces.push(p);
504
- }
505
- for (const p of pieces.slice(0, maxFragments)) {
506
- out.push({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] });
507
- }
533
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
534
+ for (const p of splinterBox(inp, root)) out.push(p);
508
535
  return out;
509
536
  }
510
537
  if (style === "tear") {
@@ -672,6 +699,27 @@ var GameDestruction = (() => {
672
699
  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]]));
673
700
  return out;
674
701
  };
702
+ var clipBarsToBox = (bars, u, v, center, size) => {
703
+ const out = [];
704
+ for (const [a, b] of bars) {
705
+ const axis = a[u] !== b[u] ? u : v;
706
+ const other = axis === u ? v : u;
707
+ if (Math.abs(a[other] - center[other]) > size[other] / 2) continue;
708
+ const lo = Math.max(Math.min(a[axis], b[axis]), center[axis] - size[axis] / 2), hi = Math.min(Math.max(a[axis], b[axis]), center[axis] + size[axis] / 2);
709
+ if (hi - lo < 1e-6) continue;
710
+ const p = [a[0], a[1], a[2]], q = [b[0], b[1], b[2]];
711
+ p[axis] = lo;
712
+ q[axis] = hi;
713
+ out.push([p, q]);
714
+ }
715
+ return out;
716
+ };
717
+ var bendBar = ([a, b], thin, bow, i) => {
718
+ const length = Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
719
+ const m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2];
720
+ m[thin] += (i % 2 === 0 ? 1 : -1) * bow * length;
721
+ return [[a, m], [m, b]];
722
+ };
675
723
  var clipBarsToDisc = (bars, u, v, center, radius) => {
676
724
  const out = [];
677
725
  for (const [a, b] of bars) {
@@ -1649,14 +1697,21 @@ var GameDestruction = (() => {
1649
1697
  smoke: "grey"
1650
1698
  },
1651
1699
  visual: {
1652
- charColor: 1709072
1700
+ charColor: 1709072,
1701
+ innerColor: 14730131
1653
1702
  },
1654
1703
  tags: [
1655
1704
  "wood",
1656
1705
  "organic",
1657
1706
  "flammable"
1658
1707
  ],
1659
- compressive: 1e5
1708
+ compressive: 1e5,
1709
+ hits: {
1710
+ blunt: {
1711
+ tear: "splinters",
1712
+ tearJoules: 60
1713
+ }
1714
+ }
1660
1715
  },
1661
1716
  {
1662
1717
  id: "wood_hard",
@@ -1686,14 +1741,21 @@ var GameDestruction = (() => {
1686
1741
  smoke: "grey"
1687
1742
  },
1688
1743
  visual: {
1689
- charColor: 1709072
1744
+ charColor: 1709072,
1745
+ innerColor: 13213802
1690
1746
  },
1691
1747
  tags: [
1692
1748
  "wood",
1693
1749
  "organic",
1694
1750
  "flammable"
1695
1751
  ],
1696
- compressive: 15e4
1752
+ compressive: 15e4,
1753
+ hits: {
1754
+ blunt: {
1755
+ tear: "splinters",
1756
+ tearJoules: 150
1757
+ }
1758
+ }
1697
1759
  },
1698
1760
  {
1699
1761
  id: "plywood",
@@ -1723,14 +1785,24 @@ var GameDestruction = (() => {
1723
1785
  smoke: "grey"
1724
1786
  },
1725
1787
  visual: {
1726
- charColor: 1709072
1788
+ charColor: 1709072,
1789
+ innerColor: 14862234
1727
1790
  },
1728
1791
  tags: [
1729
1792
  "wood",
1730
1793
  "organic",
1731
1794
  "flammable"
1732
1795
  ],
1733
- compressive: 6e4
1796
+ compressive: 6e4,
1797
+ hits: {
1798
+ ballistic: {
1799
+ tear: "splinters"
1800
+ },
1801
+ blunt: {
1802
+ tear: "splinters",
1803
+ tearJoules: 80
1804
+ }
1805
+ }
1734
1806
  },
1735
1807
  {
1736
1808
  id: "mdf",
@@ -1757,14 +1829,25 @@ var GameDestruction = (() => {
1757
1829
  smoke: "grey"
1758
1830
  },
1759
1831
  visual: {
1760
- charColor: 1709072
1832
+ charColor: 1709072,
1833
+ innerColor: 12096362
1761
1834
  },
1762
1835
  tags: [
1763
1836
  "wood",
1764
1837
  "organic",
1765
1838
  "flammable"
1766
1839
  ],
1767
- compressive: 3e4
1840
+ compressive: 3e4,
1841
+ hits: {
1842
+ ballistic: {
1843
+ tear: "lumps",
1844
+ tearJoules: 120
1845
+ },
1846
+ blunt: {
1847
+ tear: "lumps",
1848
+ tearJoules: 20
1849
+ }
1850
+ }
1768
1851
  },
1769
1852
  {
1770
1853
  id: "glass",
@@ -1878,6 +1961,17 @@ var GameDestruction = (() => {
1878
1961
  damageScale: {
1879
1962
  explosive: 1.2
1880
1963
  },
1964
+ hits: {
1965
+ ballistic: {
1966
+ unitJoules: 500,
1967
+ floor: 900,
1968
+ crater: true
1969
+ },
1970
+ blunt: {
1971
+ unitJoules: 1500,
1972
+ floor: 60
1973
+ }
1974
+ },
1881
1975
  hitDebris: "chips",
1882
1976
  breakDebris: "dust",
1883
1977
  thermal: {
@@ -2246,7 +2340,8 @@ var GameDestruction = (() => {
2246
2340
  smoke: "white"
2247
2341
  },
2248
2342
  visual: {
2249
- charColor: 1709072
2343
+ charColor: 1709072,
2344
+ innerColor: 14207126
2250
2345
  },
2251
2346
  tags: [
2252
2347
  "plant",
@@ -2254,7 +2349,12 @@ var GameDestruction = (() => {
2254
2349
  "organic",
2255
2350
  "flammable"
2256
2351
  ],
2257
- compressive: 6e4
2352
+ compressive: 6e4,
2353
+ hits: {
2354
+ blunt: {
2355
+ tear: "none"
2356
+ }
2357
+ }
2258
2358
  },
2259
2359
  {
2260
2360
  id: "cactus",
@@ -2369,6 +2469,130 @@ var GameDestruction = (() => {
2369
2469
  "masonry"
2370
2470
  ],
2371
2471
  compressive: 2e4
2472
+ },
2473
+ {
2474
+ id: "asphalt",
2475
+ density: 2350,
2476
+ hpPerKg: 10,
2477
+ penetration: 14,
2478
+ penetrationClass: "concrete",
2479
+ penetrationScale: 0.8,
2480
+ ductility: 0.1,
2481
+ fracture: "crumble",
2482
+ fragmentSize: [
2483
+ 0.03,
2484
+ 0.15
2485
+ ],
2486
+ hitDebris: "chips",
2487
+ breakDebris: "dust",
2488
+ thermal: {
2489
+ conductivity: 0.1,
2490
+ heatCapacity: 2,
2491
+ ignitionC: 400,
2492
+ burnRate: 2e-3,
2493
+ smoke: "black"
2494
+ },
2495
+ tags: [
2496
+ "masonry",
2497
+ "brittle"
2498
+ ],
2499
+ compressive: 3e4
2500
+ },
2501
+ {
2502
+ id: "sand",
2503
+ density: 1600,
2504
+ hpPerKg: 2,
2505
+ penetration: 6,
2506
+ ductility: 0.4,
2507
+ fracture: "crumble",
2508
+ fragmentSize: [
2509
+ 0.01,
2510
+ 0.04
2511
+ ],
2512
+ hitDebris: "dust",
2513
+ breakDebris: "dust",
2514
+ thermal: {
2515
+ conductivity: 0.1,
2516
+ heatCapacity: 2
2517
+ },
2518
+ tags: [
2519
+ "soil"
2520
+ ],
2521
+ compressive: 1e4
2522
+ },
2523
+ {
2524
+ id: "gravel",
2525
+ density: 1800,
2526
+ hpPerKg: 3,
2527
+ penetration: 8,
2528
+ ductility: 0.3,
2529
+ fracture: "crumble",
2530
+ fragmentSize: [
2531
+ 0.02,
2532
+ 0.06
2533
+ ],
2534
+ hitDebris: "chips",
2535
+ breakDebris: "dust",
2536
+ thermal: {
2537
+ conductivity: 0.1,
2538
+ heatCapacity: 2
2539
+ },
2540
+ tags: [
2541
+ "soil",
2542
+ "masonry"
2543
+ ],
2544
+ compressive: 2e4
2545
+ },
2546
+ {
2547
+ id: "turf",
2548
+ density: 900,
2549
+ hpPerKg: 3,
2550
+ penetration: 4,
2551
+ ductility: 0.5,
2552
+ fracture: "crumble",
2553
+ fragmentSize: [
2554
+ 0.03,
2555
+ 0.1
2556
+ ],
2557
+ hitDebris: "dust",
2558
+ breakDebris: "dust",
2559
+ thermal: {
2560
+ conductivity: 0.1,
2561
+ heatCapacity: 1.5,
2562
+ ignitionC: 250,
2563
+ burnRate: 0.03,
2564
+ smoke: "white"
2565
+ },
2566
+ tags: [
2567
+ "soil",
2568
+ "organic"
2569
+ ],
2570
+ compressive: 15e3
2571
+ },
2572
+ {
2573
+ id: "mortar",
2574
+ density: 1900,
2575
+ hpPerKg: 10,
2576
+ penetration: 12,
2577
+ penetrationClass: "concrete",
2578
+ penetrationScale: 1.2,
2579
+ ductility: 0.03,
2580
+ fracture: "crumble",
2581
+ fragmentSize: [
2582
+ 0.02,
2583
+ 0.08
2584
+ ],
2585
+ hitDebris: "chips",
2586
+ breakDebris: "dust",
2587
+ thermal: {
2588
+ conductivity: 0.15,
2589
+ heatCapacity: 2.5
2590
+ },
2591
+ tags: [
2592
+ "masonry",
2593
+ "brittle"
2594
+ ],
2595
+ compressive: 1e5
2372
2596
  }
2373
2597
  ];
2374
2598
 
@@ -2705,6 +2929,32 @@ var GameDestruction = (() => {
2705
2929
 
2706
2930
  // src/world.ts
2707
2931
  var THIN_SHEET_DISHING = 5e-3;
2932
+ var SWEEP_BACK = 0.01;
2933
+ function sweepOrigins(box, d, spacing) {
2934
+ const origins = [];
2935
+ const line = (from, to) => {
2936
+ const n = Math.max(1, Math.ceil((to - from) / spacing));
2937
+ return Array.from({ length: n + 1 }, (_, i) => from + (to - from) * i / n);
2938
+ };
2939
+ for (const axis of [0, 1, 2]) {
2940
+ if (Math.abs(d[axis]) < 1e-3) continue;
2941
+ const lead = d[axis] > 0 ? box.max[axis] - SWEEP_BACK : box.min[axis] + SWEEP_BACK;
2942
+ const u = (axis + 1) % 3;
2943
+ const v = (axis + 2) % 3;
2944
+ for (const a of line(box.min[u], box.max[u])) {
2945
+ for (const b of line(box.min[v], box.max[v])) {
2946
+ const point = [0, 0, 0];
2947
+ point[axis] = lead;
2948
+ point[u] = a;
2949
+ point[v] = b;
2950
+ origins.push(point);
2951
+ }
2952
+ }
2953
+ }
2954
+ return origins;
2955
+ }
2956
+ var THIN_SHEET_CRUSH = 5e-4;
2957
+ var THIN_SHEET_CONTACT = 0.025;
2708
2958
  var DMG_PER_J = 0.02;
2709
2959
  var BLAST_COUPLING = 0.1;
2710
2960
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
@@ -2718,9 +2968,17 @@ var GameDestruction = (() => {
2718
2968
  var BREACH_AREA = 0.25;
2719
2969
  var BREACH_BLAST_REACH = 0.12;
2720
2970
  var RESIDUAL_CAPACITY = 0.15;
2971
+ var SEVER_BAND = 0.1;
2972
+ var SEVER_ASPECT = 3;
2973
+ var SEVER_BALLISTIC = 2;
2974
+ var TEAR_JOULES = 150;
2975
+ var tears = 0;
2721
2976
  var SUPPORT_TOLERANCE = 0.05;
2722
2977
  var UNIT_FRAGMENT_FACTOR = 4;
2723
2978
  var BREACH_GRID_CAP = 4096;
2979
+ var CHIP_ENERGY = 200;
2980
+ var CHIP_SPLASH = 0.5;
2981
+ var BAR_BOW = 0.08;
2724
2982
  var CUT_SLICE = 1;
2725
2983
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
2726
2984
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -2730,8 +2988,24 @@ var GameDestruction = (() => {
2730
2988
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
2731
2989
  };
2732
2990
  var capacityScale = (p) => {
2733
- if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
2734
- return p.rt.removed ? Math.max(0.05, 1 - p.rt.removed / p.mass0) : 1;
2991
+ const residual = p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
2992
+ if (p.st.stripped) return residual;
2993
+ const kept = Math.min(p.rt.removed ? 1 - p.rt.removed / p.mass0 : 1, 1 - sectionLoss(p));
2994
+ return Math.max(p.spec.reinforcement ? residual : 0.05, kept);
2995
+ };
2996
+ var sectionLoss = (p) => {
2997
+ const cells = p.st.chipped;
2998
+ if (!cells?.length) return 0;
2999
+ const [u, v] = planeAxes(p.thin);
3000
+ const along = p.dims[u] >= p.dims[v] ? u : v;
3001
+ const across = along === u ? v : u;
3002
+ let worst = 0;
3003
+ for (const c of cells) {
3004
+ let gone = 0;
3005
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across];
3006
+ worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
3007
+ }
3008
+ return Math.min(1, worst);
2735
3009
  };
2736
3010
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
2737
3011
  var clone = (v) => structuredClone(v);
@@ -2764,6 +3038,7 @@ var GameDestruction = (() => {
2764
3038
  let windCfg = windOpts ? resolveWind(windOpts) : null;
2765
3039
  const ignitionRules = resolveIgnitionRules(options.ignition);
2766
3040
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
3041
+ const chipEnergy = options.breach?.chipEnergy ?? CHIP_ENERGY;
2767
3042
  const rng = createRng(options.seed ?? 1);
2768
3043
  const rnd = () => rng.next();
2769
3044
  let time = 0;
@@ -3137,41 +3412,68 @@ var GameDestruction = (() => {
3137
3412
  markDamaged(p);
3138
3413
  if (p.rt.hinge >= flexure.failRotationDeg * Math.PI / 180) hingeDrop(o, p, src);
3139
3414
  };
3415
+ const freeSpan = (p, supports) => {
3416
+ if (supports.length === 0) return Infinity;
3417
+ const d = p.dims;
3418
+ const L = [0, 1, 2].sort((a, b) => d[b] - d[a])[0];
3419
+ const spans = supports.map((k) => contactBox(p, k)).map(([lo, hi]) => [lo[L], hi[L]]);
3420
+ let far = 0;
3421
+ for (let i = 0; i <= 16; i++) {
3422
+ const x = (i / 16 - 0.5) * d[L];
3423
+ let near = Infinity;
3424
+ for (const [lo, hi] of spans) near = Math.min(near, Math.max(lo - x, x - hi, 0));
3425
+ far = Math.max(far, near);
3426
+ }
3427
+ return 2 * far;
3428
+ };
3140
3429
  const solveStructure = (o) => {
3141
3430
  o.maxStress = 0;
3142
3431
  if (!o.hasStructure) return;
3143
3432
  const parts = o.parts;
3144
3433
  const n = parts.length;
3145
3434
  const dist = new Array(n).fill(-1);
3146
- const queue = [];
3147
3435
  const ok = (i) => parts[i].structural && !isLost(parts[i]);
3148
3436
  const isPartition = (i) => parts[i].spec.structure?.role === "partition";
3149
- for (let i = 0; i < n; i++) if (ok(i) && parts[i].anchored) {
3150
- dist[i] = 0;
3151
- queue.push(i);
3152
- }
3153
- for (let h = 0; h < queue.length; h++) {
3154
- const s = queue[h];
3155
- if (isPartition(s)) continue;
3156
- for (const j of o.byPart.get(parts[s].spec.id)) {
3157
- if (j.st.open || !j.bears) continue;
3158
- const r = j.ia === s ? j.ib : j.ia;
3159
- if (dist[r] >= 0 || !ok(r) || restsOn(o, j, r) !== s) continue;
3160
- dist[r] = dist[s] + 1;
3161
- queue.push(r);
3437
+ const out = /* @__PURE__ */ new Set();
3438
+ let queue = [];
3439
+ let lowerOf = /* @__PURE__ */ new Map();
3440
+ for (; ; ) {
3441
+ dist.fill(-1);
3442
+ queue = [];
3443
+ for (let i = 0; i < n; i++) if (ok(i) && !out.has(i) && parts[i].anchored) {
3444
+ dist[i] = 0;
3445
+ queue.push(i);
3162
3446
  }
3163
- }
3164
- const incoming = new Array(n).fill(0);
3165
- const lowerOf = /* @__PURE__ */ new Map();
3166
- for (const i of queue) {
3167
- const lower = [];
3168
- for (const j of o.byPart.get(parts[i].spec.id)) {
3169
- if (j.st.open || !j.bears) continue;
3170
- const k = restsOn(o, j, i);
3171
- if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
3447
+ for (let h = 0; h < queue.length; h++) {
3448
+ const s = queue[h];
3449
+ if (isPartition(s)) continue;
3450
+ for (const j of o.byPart.get(parts[s].spec.id)) {
3451
+ if (j.st.open || !j.bears) continue;
3452
+ const r = j.ia === s ? j.ib : j.ia;
3453
+ if (dist[r] >= 0 || !ok(r) || out.has(r) || restsOn(o, j, r) !== s) continue;
3454
+ dist[r] = dist[s] + 1;
3455
+ queue.push(r);
3456
+ }
3172
3457
  }
3173
- lowerOf.set(i, lower);
3458
+ lowerOf = /* @__PURE__ */ new Map();
3459
+ let dropped = false;
3460
+ for (const i of queue) {
3461
+ const lower = [];
3462
+ for (const j of o.byPart.get(parts[i].spec.id)) {
3463
+ if (j.st.open || !j.bears) continue;
3464
+ const k = restsOn(o, j, i);
3465
+ if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
3466
+ }
3467
+ lowerOf.set(i, lower);
3468
+ const max = parts[i].spec.structure?.maxSpan;
3469
+ if (max !== void 0 && !parts[i].anchored && freeSpan(parts[i], lower.map((k) => parts[k])) > max) {
3470
+ out.add(i);
3471
+ dropped = true;
3472
+ }
3473
+ }
3474
+ if (!dropped) break;
3174
3475
  }
3476
+ const incoming = new Array(n).fill(0);
3175
3477
  const bending = new Array(n).fill(0);
3176
3478
  const through = parts.map(() => []);
3177
3479
  const pass = (k, at, mass) => {
@@ -3683,6 +3985,7 @@ var GameDestruction = (() => {
3683
3985
  const r = rayPart(o, p, origin, d);
3684
3986
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
3685
3987
  if (p.st.opening && insideOpening(o, p, vec.madd(origin, d, r.t0))) continue;
3988
+ if (p.st.chipped && insideChip(p, objLocal(o, vec.madd(origin, d, r.t0)))) continue;
3686
3989
  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 };
3687
3990
  }
3688
3991
  }
@@ -3883,6 +4186,12 @@ var GameDestruction = (() => {
3883
4186
  debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
3884
4187
  setStage(o, p, 1, {}, src);
3885
4188
  };
4189
+ const crater = (o, p, wp, normal, radius, src) => {
4190
+ markDamaged(p);
4191
+ emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: -1, point: wp, area: Math.PI * radius * radius });
4192
+ debris(o, p, p.mat.breakDebris, wp, normal, 6, 1.5, src);
4193
+ setStage(o, p, 1, {}, src);
4194
+ };
3886
4195
  const insideOpening = (o, p, objectPoint) => {
3887
4196
  const op = p.st.opening;
3888
4197
  if (!op) return false;
@@ -3946,6 +4255,91 @@ var GameDestruction = (() => {
3946
4255
  if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
3947
4256
  openBreach(o, p, [sum[0] / total, sum[1] / total, sum[2] / total], total, energy, dir, src);
3948
4257
  };
4258
+ const unitGrids = /* @__PURE__ */ new WeakMap();
4259
+ const gridOf = (p) => {
4260
+ let grid = unitGrids.get(p);
4261
+ if (!grid) unitGrids.set(p, grid = unitGrid(p.dims, p.thin, unitsOf(p), BREACH_GRID_CAP));
4262
+ return grid;
4263
+ };
4264
+ const insideChip = (p, objectPoint) => {
4265
+ const cells = p.st.chipped;
4266
+ if (!cells?.length) return false;
4267
+ const [u, v] = planeAxes(p.thin);
4268
+ const lp = toLocal(p.pos, p.q, objectPoint);
4269
+ return cells.some((c) => Math.abs(lp[u] - c.center[u]) <= c.size[u] / 2 && Math.abs(lp[v] - c.center[v]) <= c.size[v] / 2);
4270
+ };
4271
+ const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
4272
+ var _a, _b;
4273
+ if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
4274
+ const rule = p.mat.hits?.[kind];
4275
+ const perKg = rule?.unitJoules ?? chipEnergy;
4276
+ const energy = hitEnergy - (rule?.floor ?? 0);
4277
+ if (perKg <= 0 || energy <= 0) return;
4278
+ const [u, v] = planeAxes(p.thin);
4279
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
4280
+ const grid = gridOf(p);
4281
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
4282
+ const out = [];
4283
+ for (let i = 0; i < grid.length; i++) {
4284
+ const cell = grid[i];
4285
+ if (worn[i] === -1) continue;
4286
+ const gap = Math.hypot(Math.max(0, Math.abs(lp[u] - cell.center[u]) - cell.size[u] / 2), Math.max(0, Math.abs(lp[v] - cell.center[v]) - cell.size[v] / 2));
4287
+ if (gap > radius) continue;
4288
+ const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
4289
+ const total = (worn[i] ?? 0) + energy * share;
4290
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
4291
+ if (total < perKg * mass) {
4292
+ worn[i] = total;
4293
+ continue;
4294
+ }
4295
+ worn[i] = -1;
4296
+ out.push(cell);
4297
+ }
4298
+ if (out.length === 0) return;
4299
+ const { c, q } = partXf(o, p);
4300
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
4301
+ const holes = localHolesOf(p);
4302
+ for (const cell of out) {
4303
+ const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
4304
+ const center = toWorld(c, q, cell.center);
4305
+ const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
4306
+ const fragments = cellFragments(o, p, [cell], wp, dir, speed, mass, `c${chipped.length}_`);
4307
+ p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
4308
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
4309
+ p.st.mass = Math.max(0, p.st.mass - mass);
4310
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
4311
+ markDamaged(p);
4312
+ setStage(o, p, 1, {}, src);
4313
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
4314
+ debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
4315
+ }
4316
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
4317
+ for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
4318
+ if (p.structural && !isLost(p)) {
4319
+ solveStructure(o);
4320
+ collapseCheck(o, src);
4321
+ }
4322
+ };
4323
+ const exposeRebar = (o, p, src) => {
4324
+ const r = p.spec.reinforcement;
4325
+ if (!r) return;
4326
+ const [u, v] = planeAxes(p.thin);
4327
+ const all = rebarBars(p.dims, p.thin, r.spacing);
4328
+ let bars;
4329
+ if (p.st.stripped) bars = all;
4330
+ else {
4331
+ bars = [];
4332
+ for (const cell of p.st.chipped ?? []) bars.push(...clipBarsToBox(all, u, v, cell.center, cell.size));
4333
+ const op = p.st.opening;
4334
+ if (op) {
4335
+ const lc = toLocal(p.pos, p.q, op.center);
4336
+ bars.push(...clipBarsToDisc(all, u, v, [lc[u], lc[v]], op.radius + 0.05));
4337
+ }
4338
+ }
4339
+ const bow = clamp2(((p.st.stress ?? 0) - 1) / 0.5, 0, 1);
4340
+ if (bow > 0) bars = bars.flatMap((bar2, i) => bendBar(bar2, p.thin, bow * BAR_BOW, i));
4341
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
4342
+ };
3949
4343
  const openBreach = (o, p, center, total, energy, dir, src) => {
3950
4344
  const [u, v] = planeAxes(p.thin);
3951
4345
  const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
@@ -3957,7 +4351,8 @@ var GameDestruction = (() => {
3957
4351
  failPart(o, p, worldCenter, dir, energy, 12, src);
3958
4352
  return;
3959
4353
  }
3960
- const cells = cellsWithin(unitGrid(p.dims, p.thin, unitsOf(p), BREACH_GRID_CAP), p.thin, lc, radius);
4354
+ const worn = p.rt.chips;
4355
+ const cells = cellsWithin(gridOf(p).filter((_, i) => worn?.[i] !== -1), p.thin, lc, radius);
3961
4356
  const removed = p.st.mass * cells.reduce((sum, cell) => sum + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
3962
4357
  const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
3963
4358
  const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
@@ -3971,10 +4366,7 @@ var GameDestruction = (() => {
3971
4366
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
3972
4367
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
3973
4368
  const reinforcement = p.spec.reinforcement;
3974
- if (reinforcement) {
3975
- const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
3976
- if (bars.length > 0) emit({ ...base(o, src), type: "expose", part: p.spec.id, bars });
3977
- }
4369
+ if (reinforcement) exposeRebar(o, p, src);
3978
4370
  if (p.structural) solveStructure(o);
3979
4371
  };
3980
4372
  const stripPart = (o, p, reinforcement, src) => {
@@ -3986,8 +4378,8 @@ var GameDestruction = (() => {
3986
4378
  p.st.burning = false;
3987
4379
  p.rt.jet = false;
3988
4380
  markDamaged(p);
3989
- emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
3990
4381
  solveStructure(o);
4382
+ exposeRebar(o, p, src);
3991
4383
  collapseCheck(o, src);
3992
4384
  };
3993
4385
  const unitFragmentPlan = (p, cap) => {
@@ -4041,6 +4433,8 @@ var GameDestruction = (() => {
4041
4433
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
4042
4434
  const hitLocal = toLocal(c, q, hitW);
4043
4435
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
4436
+ const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
4437
+ const cutAt = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
4044
4438
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
4045
4439
  style,
4046
4440
  dims: p.dims,
@@ -4050,7 +4444,8 @@ var GameDestruction = (() => {
4050
4444
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
4051
4445
  maxFragments,
4052
4446
  rnd,
4053
- sheet: p.mat.sheet
4447
+ sheet: p.mat.sheet,
4448
+ ...cutAt !== void 0 ? { cut: { at: cutAt, gap: SEVER_BAND } } : {}
4054
4449
  });
4055
4450
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
4056
4451
  const mass = p.st.mass;
@@ -4109,10 +4504,33 @@ var GameDestruction = (() => {
4109
4504
  emit({ ...base(o, src), type: "primed", part: p.spec.id, point: partXf(o, p).c, trigger, delay });
4110
4505
  };
4111
4506
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
4507
+ const severAxis = (p) => {
4508
+ if (p.mat.fracture !== "splinter" && p.mat.fracture !== "snap" || p.spec.shape.kind !== "box") return -1;
4509
+ const d = p.dims;
4510
+ const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
4511
+ return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
4512
+ };
4513
+ const bandOf = (o, p, axis, origin) => {
4514
+ const { c, q } = partXf(o, p);
4515
+ const along = toLocal(c, q, origin)[axis];
4516
+ const L = p.dims[axis];
4517
+ return clamp2(Math.floor((along + L / 2) / SEVER_BAND), 0, Math.ceil(L / SEVER_BAND) - 1);
4518
+ };
4112
4519
  const applyDamage = (o, p, amount, type, origin, dir, fragEnergy, cap, src) => {
4520
+ var _a;
4113
4521
  if (isLost(p) || amount <= 0) return 0;
4114
4522
  const prev = p.st.hp;
4115
4523
  p.st.hp -= amount;
4524
+ const axis = origin && type !== "fire" && type !== "heat" ? severAxis(p) : -1;
4525
+ if (axis >= 0 && p.st.hp > 0) {
4526
+ const band = bandOf(o, p, axis, origin);
4527
+ const cuts = (_a = p.rt).cuts ?? (_a.cuts = {});
4528
+ cuts[band] = (cuts[band] ?? 0) + amount * (type === "ballistic" ? SEVER_BALLISTIC : 1);
4529
+ if (cuts[band] >= p.st.maxHp * SEVER_BAND / p.dims[axis]) {
4530
+ p.rt.cutAt = -p.dims[axis] / 2 + (band + 0.5) * SEVER_BAND;
4531
+ p.st.hp = 0;
4532
+ }
4533
+ }
4116
4534
  markDamaged(p);
4117
4535
  const trigger = CHAIN_TRIGGER_OF[type];
4118
4536
  const chained = trigger !== void 0 && chainDelayOf(p.spec, trigger) !== void 0;
@@ -4148,10 +4566,53 @@ var GameDestruction = (() => {
4148
4566
  }
4149
4567
  }
4150
4568
  };
4569
+ const tearOf = (mat, kind) => {
4570
+ const h = mat.hits?.[kind];
4571
+ const tear = h?.tear ?? (kind === "ballistic" && mat.penetrationClass === "wood" ? "splinters" : "none");
4572
+ return { tear, joules: h?.tearJoules ?? TEAR_JOULES };
4573
+ };
4574
+ const tearAt = (o, p, kind, entry, exit, d, E, scale, src) => {
4575
+ const { tear, joules } = tearOf(p.mat, kind);
4576
+ if (tear === "none") return;
4577
+ const shot = kind === "ballistic";
4578
+ const n = shot ? exit ? clamp2(Math.round(E / joules), 1, 7) : 1 : clamp2(Math.floor(E / joules), 0, 5);
4579
+ if (n === 0) return;
4580
+ const fibre = tear === "lumps";
4581
+ const { q } = partXf(o, p);
4582
+ const along = (p.spec.grain ? AXIS[p.spec.grain] : void 0) ?? [0, 1, 2].sort((a, b) => p.dims[b] - p.dims[a])[0];
4583
+ const fragments = [];
4584
+ for (let i = 0; i < n; i++) {
4585
+ const back = !exit || i % 3 === 2;
4586
+ const len = scale * (fibre ? 1.2 + 1.2 * rnd() : 3 + 4 * rnd()), w = scale * (0.8 + 0.8 * rnd()), t = scale * (fibre ? 0.7 + 0.6 * rnd() : 0.4 + 0.4 * rnd());
4587
+ const size = [w, w, w];
4588
+ size[along] = len;
4589
+ size[(along + 1) % 3] = w;
4590
+ size[(along + 2) % 3] = t;
4591
+ const spread3 = [(rnd() - 0.5) * 0.8, (rnd() - 0.2) * 0.8, (rnd() - 0.5) * 0.8];
4592
+ const speed = shot ? back ? 1.5 + 2 * rnd() : 2 + 4 * rnd() : 0.8 + 1.2 * rnd();
4593
+ const v = vec.scale(vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3)), speed);
4594
+ const mass = size[0] * size[1] * size[2] * p.mat.density;
4595
+ fragments.push({
4596
+ id: `${p.spec.id}#t${tears++}-${i}`,
4597
+ part: p.spec.id,
4598
+ material: p.spec.material,
4599
+ shape: fibre ? "chunk" : "splinter",
4600
+ center: back || !exit ? vec.madd(entry, d, -0.01) : vec.madd(exit, d, 0.01),
4601
+ size,
4602
+ rotation: q,
4603
+ velocity: v,
4604
+ inner: true,
4605
+ angularVelocity: [(rnd() - 0.5) * 30, (rnd() - 0.5) * 30, (rnd() - 0.5) * 30],
4606
+ mass
4607
+ });
4608
+ }
4609
+ const patch = [scale * 4, scale * 4, scale * 4];
4610
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry, size: patch, rotation: q, fragments });
4611
+ };
4151
4612
  const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src, round = {}) => {
4152
4613
  const mat = p.mat;
4153
4614
  const piercing = round.piercing ?? 1;
4154
- if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry))) {
4615
+ if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry)) || insideChip(p, objLocal(o, entry))) {
4155
4616
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry, d, Math.min(pathLen, p.thickness)), damage: 0 };
4156
4617
  }
4157
4618
  const sin = Math.min(1, Math.abs(vec.dot(d, n)));
@@ -4176,6 +4637,7 @@ var GameDestruction = (() => {
4176
4637
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
4177
4638
  }
4178
4639
  if (p.layers.length > 0) spallOuter(o, p, entry, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
4640
+ else if (mat.hits?.ballistic?.crater) crater(o, p, entry, n, clamp2(0.02 + Math.sqrt(E) * 15e-4, 0.02, 0.15), src);
4179
4641
  const brittle = mat.ductility < 0.3;
4180
4642
  const ratio = Ep / Math.max(thr, 1e-6);
4181
4643
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
@@ -4209,7 +4671,10 @@ var GameDestruction = (() => {
4209
4671
  } else addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
4210
4672
  debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
4211
4673
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
4212
- const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry, d, E * 0.1, 12, src);
4674
+ const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
4675
+ const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry, d, absorbed * 0.1, 12, src);
4676
+ if (!isLost(p)) tearAt(o, p, "ballistic", entry, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
4677
+ chipUnits(o, p, "ballistic", entry, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
4213
4678
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
4214
4679
  };
4215
4680
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -4217,14 +4682,20 @@ var GameDestruction = (() => {
4217
4682
  const thrMm = mat.penetration * p.thickness * 1e3;
4218
4683
  const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
4219
4684
  const Ec = E * fragile;
4685
+ const sheet = mat.ductility >= 0.5 && p.thickness <= THIN_SHEET_CRUSH;
4686
+ const fold = sheet ? Math.min(4, Math.sqrt(1e-3 / p.thickness)) : 1;
4687
+ const patch = sheet ? Math.min(contactR, THIN_SHEET_CONTACT) : contactR;
4220
4688
  if (E > 0) {
4221
4689
  if (kind === "blunt") {
4222
- if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp2(E * 1e-4, 1e-3, 0.06), src);
4690
+ if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(patch, 0.015), clamp2(E * 1e-4 * fold, 1e-3, sheet ? Math.min(0.06, Math.min(...p.dims) * 0.45) : 0.06), src);
4223
4691
  else if (mat.ductility < 0.3) {
4224
4692
  if (Ec >= 5) addCrack(o, p, point, n, clamp2(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
4225
4693
  } else if (E >= thrMm * 0.2) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp2(E * 6e-5, 1e-3, 0.04), src);
4226
4694
  } else if (mat.fracture === "tear") {
4227
4695
  addHole(o, p, point, n, clamp2(0.02 + E * 5e-4, 0.02, 0.3), true, void 0, src);
4696
+ } else if (mat.ductility >= 0.3 && E >= thrMm) {
4697
+ addHole(o, p, point, n, clamp2(contactR, 0.01, 0.08), true, void 0, src);
4698
+ if (sheet) addDent(o, p, point, n, Math.max(patch * 0.6, 0.01), clamp2(E * 4e-5 * fold, 5e-4, 0.02), src);
4228
4699
  } else if (mat.ductility >= 0.5) {
4229
4700
  addDent(o, p, point, n, Math.max(contactR, 0.01), clamp2(E * 4e-5, 5e-4, 0.02), src);
4230
4701
  }
@@ -4237,6 +4708,8 @@ var GameDestruction = (() => {
4237
4708
  const scale = mat.damageScale?.[kind] ?? 1;
4238
4709
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
4239
4710
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
4711
+ if (kind === "blunt") chipUnits(o, p, "blunt", point, E, d, Math.max(contactR, 0.03), src);
4712
+ if (E > 0 && !isLost(p)) tearAt(o, p, kind, point, void 0, d, E, clamp2(contactR * 0.2, 6e-3, 0.02), src);
4240
4713
  return done;
4241
4714
  };
4242
4715
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -4815,6 +5288,7 @@ var GameDestruction = (() => {
4815
5288
  if (p.rt.stressT <= 0) {
4816
5289
  p.rt.stressT = 0.5;
4817
5290
  emit({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
5291
+ if (p.spec.reinforcement && (p.st.stripped || p.st.chipped?.length || p.st.opening)) exposeRebar(o, p);
4818
5292
  }
4819
5293
  applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
4820
5294
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
@@ -5031,6 +5505,18 @@ var GameDestruction = (() => {
5031
5505
  const r = raycastInternal(origin, direction, maxDistance);
5032
5506
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
5033
5507
  },
5508
+ sweepBox(box, direction, distance, options2 = {}) {
5509
+ const d = vec.normalize(direction);
5510
+ const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
5511
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
5512
+ let best = null;
5513
+ for (const origin of sweepOrigins(box, d, options2.spacing ?? 0.2)) {
5514
+ const r = raycastInternal(origin, d, distance + SWEEP_BACK, ignore);
5515
+ if (!r || best && r.distance >= best.distance) continue;
5516
+ best = { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: Math.max(0, r.distance - SWEEP_BACK) };
5517
+ }
5518
+ return best;
5519
+ },
5034
5520
  shoot(origin, direction, energy, caliber, source, round) {
5035
5521
  const extras = typeof round === "number" ? { piercing: round } : round ?? {};
5036
5522
  return withCollapses(run(() => {
@@ -5675,7 +6161,12 @@ var GameDestruction = (() => {
5675
6161
  cactus: "cactus",
5676
6162
  flesh_fruit: "fruit",
5677
6163
  wax: "wax",
5678
- soil: "soil"
6164
+ soil: "soil",
6165
+ asphalt: "asphalt",
6166
+ sand: "sand",
6167
+ gravel: "gravel",
6168
+ turf: "turf",
6169
+ mortar: "mortar"
5679
6170
  },
5680
6171
  partStatus: { intact: "intact", damaged: "damaged", collapsed: "collapsed", broken: "broken", melted: "melted", ashes: "ashes" },
5681
6172
  jointKinds: {
@@ -5728,6 +6219,7 @@ var GameDestruction = (() => {
5728
6219
  sway: "sway",
5729
6220
  spall: "plaster came off",
5730
6221
  stage: "damage stage",
6222
+ chip: "brick knocked out",
5731
6223
  expose: "rebar bare",
5732
6224
  body: "body",
5733
6225
  blast: "blast",
@@ -5765,7 +6257,12 @@ var GameDestruction = (() => {
5765
6257
  cactus: "cacto",
5766
6258
  flesh_fruit: "fruta",
5767
6259
  wax: "cera",
5768
- soil: "terra"
6260
+ soil: "terra",
6261
+ asphalt: "asfalto",
6262
+ sand: "areia",
6263
+ gravel: "brita",
6264
+ turf: "grama",
6265
+ mortar: "argamassa"
5769
6266
  },
5770
6267
  partStatus: { intact: "intacto", damaged: "danificado", collapsed: "desabado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
5771
6268
  jointKinds: {
@@ -5818,6 +6315,7 @@ var GameDestruction = (() => {
5818
6315
  sway: "balan\xE7o",
5819
6316
  spall: "reboco soltou",
5820
6317
  stage: "est\xE1gio de dano",
6318
+ chip: "tijolo arrancado",
5821
6319
  expose: "vergalh\xE3o \xE0 mostra",
5822
6320
  body: "corpo",
5823
6321
  blast: "explos\xE3o",