@adatechnology/game-destruction 0.16.0 → 0.18.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.
@@ -452,6 +452,22 @@ var GameDestruction = (() => {
452
452
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
453
453
  };
454
454
  var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
455
+ var splitAlong = (box, count, axis, rnd) => {
456
+ const shares = Array.from({ length: count }, () => 0.25 + 1.5 * rnd());
457
+ const total = shares.reduce((sum, share) => sum + share, 0);
458
+ const out = [];
459
+ let from = box.c[axis] - box.s[axis] / 2;
460
+ for (const share of shares) {
461
+ const length = box.s[axis] * share / total;
462
+ const c = [box.c[0], box.c[1], box.c[2]];
463
+ const size = [box.s[0], box.s[1], box.s[2]];
464
+ c[axis] = from + length / 2;
465
+ size[axis] = length;
466
+ out.push({ c, s: size });
467
+ from += length;
468
+ }
469
+ return out;
470
+ };
455
471
  var splinterBox = (inp, box, limit = inp.maxFragments) => {
456
472
  const { rnd } = inp;
457
473
  const dims = box.s;
@@ -461,35 +477,81 @@ var GameDestruction = (() => {
461
477
  const w = Math.max(fmin * 1.5, 0.01);
462
478
  const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
463
479
  const maxLen = fmax * 4;
464
- const cells = bsp(box, c, [a, b], rnd);
480
+ const cells = [];
481
+ for (const cell of bsp(box, c, [a, b], rnd)) {
482
+ const wide = cell.s[a] >= cell.s[b] ? a : b;
483
+ if (rnd() < 0.4 && cell.s[wide] > fmin * 0.5) for (const part of splitAlong(cell, 2, wide, rnd)) cells.push(part);
484
+ else cells.push(cell);
485
+ }
465
486
  const pieces = [];
466
487
  for (const cell of cells) {
467
- const extra = rnd() < 0.4 ? 1 : 0;
488
+ const extra = Math.floor(rnd() * 3);
468
489
  const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
469
- for (const p of bsp(cell, k, [g], rnd)) pieces.push(p);
490
+ for (const p of splitAlong(cell, k, g, rnd)) pieces.push(p);
470
491
  }
471
492
  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] }));
472
493
  };
494
+ var PLY = 3e-3;
495
+ var PLY_GAP = 0.6;
496
+ var PLY_SHEET = 0.012;
497
+ var plyBreak = (inp, root, limit) => {
498
+ const { rnd } = inp;
499
+ const dims = root.s;
500
+ const sorted = [0, 1, 2].sort((x, y) => dims[x] - dims[y]);
501
+ const t = sorted[0];
502
+ const [u, v] = [sorted[1], sorted[2]].sort((x, y) => x - y);
503
+ const along = dims[u] >= dims[v] ? u : v;
504
+ const across = along === u ? v : u;
505
+ const layers = clamp(Math.round(dims[t] / PLY / 2), 2, 3);
506
+ const pitch = dims[t] / layers;
507
+ const th = Math.min(pitch, PLY_SHEET);
508
+ const per = Math.max(2, Math.floor(limit / layers));
509
+ const out = [];
510
+ for (let k = 0; k < layers; k++) {
511
+ const layer = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
512
+ layer.c[t] = root.c[t] + (-dims[t] / 2 + (k + 0.5) * pitch) * (pitch > PLY_SHEET ? 1 : 1 + PLY_GAP);
513
+ layer.s[t] = th;
514
+ for (const cell of bsp(layer, per, [u, v], rnd)) {
515
+ if (out.length >= limit) return out;
516
+ if (rnd() < 0.35) {
517
+ const strand = cell.s[across] * (0.15 + 0.2 * rnd());
518
+ out.push({ center: cell.c, size: [cell.s[along], strand, th * 0.7], q: frame(along, across, t), shape: "splinter", weight: cell.s[along] * strand * th });
519
+ } else {
520
+ out.push({ center: cell.c, size: [cell.s[u], cell.s[v], th], q: frame(u, v, t, (rnd() - 0.5) * 0.5), shape: "scrap", weight: cell.s[u] * cell.s[v] * th });
521
+ }
522
+ }
523
+ }
524
+ return out;
525
+ };
526
+ var granules = (inp, box, count) => bsp(box, count, [0, 1, 2], inp.rnd).map((b) => ({ center: b.c, size: b.s, q: quat.identity(), shape: "chunk", weight: b.s[0] * b.s[1] * b.s[2] }));
527
+ var CRUMBS = 6;
473
528
  var severAt = (inp, root, at, gap) => {
474
529
  const dims = root.s;
475
530
  const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
476
531
  const half = dims[L] / 2;
477
532
  const lo = clamp(at - gap / 2, -half, half), hi = clamp(at + gap / 2, -half, half);
478
533
  const out = [];
479
- const stub = (from, to) => {
534
+ const fibrous = inp.form === "slivers" || inp.form === "plies";
535
+ const [a, b] = [0, 1, 2].filter((i) => i !== L);
536
+ const stub = (from, to, breakAtEnd) => {
480
537
  if (to - from < 0.02) return;
481
538
  const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
482
539
  c[L] = (from + to) / 2;
483
540
  sz[L] = to - from;
484
- out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight: sz[0] * sz[1] * sz[2] });
541
+ const weight = sz[0] * sz[1] * sz[2];
542
+ if (fibrous) out.push({ center: c, size: [sz[L], sz[a], sz[b]], q: frame(L, a, b, breakAtEnd ? 0 : Math.PI), shape: "slab", weight });
543
+ else out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight });
485
544
  };
486
- stub(-half, lo);
487
- stub(hi, half);
545
+ stub(-half, lo, true);
546
+ stub(hi, half, false);
488
547
  const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
489
548
  band.c[L] = (lo + hi) / 2;
490
549
  band.s[L] = Math.max(hi - lo, 0.01);
491
- if (inp.style === "snap") {
492
- 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] });
550
+ const room = inp.maxFragments - out.length;
551
+ if (inp.style === "snap" && inp.form === "plies") for (const p of plyBreak(inp, band, Math.max(3, room))) out.push(p);
552
+ else if (inp.style === "snap" && inp.form === "granules") for (const p of granules(inp, band, clamp(room, 3, 12))) out.push(p);
553
+ else if (inp.style === "snap") {
554
+ for (const b2 of bsp(band, Math.max(3, Math.min(6, room)), [0, 1, 2], inp.rnd)) out.push({ center: b2.c, size: b2.s, q: quat.identity(), shape: "chunk", weight: b2.s[0] * b2.s[1] * b2.s[2] });
493
555
  } else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
494
556
  return out;
495
557
  };
@@ -522,8 +584,18 @@ var GameDestruction = (() => {
522
584
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
523
585
  if (style === "snap") {
524
586
  if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
587
+ if (inp.form === "plies") return plyBreak(inp, root, maxFragments);
525
588
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
526
589
  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] });
590
+ if (inp.form !== "granules") return out;
591
+ const crumbs = Math.min(CRUMBS, maxFragments - out.length);
592
+ if (crumbs <= 0) return out;
593
+ const band = { c: [0, 0, 0], s: [0, 0, 0] };
594
+ for (let i = 0; i < 3; i++) {
595
+ band.s[i] = Math.min(dims[i], Math.max(fmin, dims[i] * 0.2));
596
+ band.c[i] = clamp(inp.hit[i], -dims[i] / 2 + band.s[i] / 2, dims[i] / 2 - band.s[i] / 2);
597
+ }
598
+ for (const p of granules(inp, band, crumbs)) out.push({ ...p, weight: p.weight * 0.5 });
527
599
  return out;
528
600
  }
529
601
  if (style === "crumble" || style === "none") {
@@ -632,6 +704,88 @@ var GameDestruction = (() => {
632
704
  }
633
705
  return marks;
634
706
  };
707
+ var JOINT_GAP = 1e-4;
708
+ var MIN_BEARING_SHARE = 0.25;
709
+ var unsupportedCells = (cells, thin, dims, standing) => {
710
+ if (thin === 1) return [];
711
+ const [u, v] = planeAxes(thin);
712
+ const bottom = -dims[v] / 2;
713
+ const rowOf = (y) => Math.round((y - bottom) * 1e4);
714
+ const rows = /* @__PURE__ */ new Map();
715
+ for (let i = 0; i < cells.length; i++) {
716
+ if (!standing(i)) continue;
717
+ const cell = cells[i];
718
+ const key = rowOf(cell.center[v] - cell.size[v] / 2);
719
+ const row = rows.get(key);
720
+ if (row) row.push(i);
721
+ else rows.set(key, [i]);
722
+ }
723
+ const held = /* @__PURE__ */ new Set();
724
+ const queue = [];
725
+ for (const i of rows.get(0) ?? []) {
726
+ held.add(i);
727
+ queue.push(i);
728
+ }
729
+ const overlap = (a, b) => Math.min(a.center[u] + a.size[u] / 2, b.center[u] + b.size[u] / 2) - Math.max(a.center[u] - a.size[u] / 2, b.center[u] - b.size[u] / 2);
730
+ const climb = () => {
731
+ while (queue.length > 0) {
732
+ const cell = cells[queue.pop()];
733
+ for (const j of rows.get(rowOf(cell.center[v] + cell.size[v] / 2)) ?? []) {
734
+ const other = cells[j];
735
+ if (held.has(j) || overlap(cell, other) <= Math.max(JOINT_GAP, MIN_BEARING_SHARE * Math.min(cell.size[u], other.size[u]))) continue;
736
+ held.add(j);
737
+ queue.push(j);
738
+ }
739
+ }
740
+ };
741
+ for (const row of rows.values()) row.sort((a, b) => cells[a].center[u] - cells[b].center[u]);
742
+ const touching = (a, b) => Math.abs(a.center[u] + a.size[u] / 2 - (b.center[u] - b.size[u] / 2)) <= 1e-3;
743
+ for (let wedged = true; wedged; ) {
744
+ climb();
745
+ wedged = false;
746
+ for (const row of rows.values()) {
747
+ for (let k = 1; k < row.length - 1; k++) {
748
+ const i = row[k];
749
+ if (held.has(i) || !held.has(row[k - 1]) || !held.has(row[k + 1])) continue;
750
+ if (!touching(cells[row[k - 1]], cells[i]) || !touching(cells[i], cells[row[k + 1]])) continue;
751
+ held.add(i);
752
+ queue.push(i);
753
+ wedged = true;
754
+ }
755
+ }
756
+ }
757
+ const loose = [];
758
+ for (const row of rows.values()) for (const i of row) if (!held.has(i)) loose.push(i);
759
+ return loose;
760
+ };
761
+ var clumpCells = (cells, thin, cap) => {
762
+ if (cells.length <= cap) return [...cells];
763
+ const [u, v] = planeAxes(thin);
764
+ let lo0 = Infinity, lo1 = Infinity, hi0 = -Infinity, hi1 = -Infinity;
765
+ for (const cell of cells) {
766
+ lo0 = Math.min(lo0, cell.center[u] - cell.size[u] / 2);
767
+ hi0 = Math.max(hi0, cell.center[u] + cell.size[u] / 2);
768
+ lo1 = Math.min(lo1, cell.center[v] - cell.size[v] / 2);
769
+ hi1 = Math.max(hi1, cell.center[v] + cell.size[v] / 2);
770
+ }
771
+ let side = Math.sqrt((hi0 - lo0) * (hi1 - lo1) / cap);
772
+ for (; ; ) {
773
+ const bins = /* @__PURE__ */ new Map();
774
+ for (const cell of cells) {
775
+ const key = `${Math.floor((cell.center[u] - lo0) / side)}:${Math.floor((cell.center[v] - lo1) / side)}`;
776
+ const bin = bins.get(key) ?? { lo: [Infinity, Infinity, Infinity], hi: [-Infinity, -Infinity, -Infinity] };
777
+ for (let k = 0; k < 3; k++) {
778
+ bin.lo[k] = Math.min(bin.lo[k], cell.center[k] - cell.size[k] / 2);
779
+ bin.hi[k] = Math.max(bin.hi[k], cell.center[k] + cell.size[k] / 2);
780
+ }
781
+ bins.set(key, bin);
782
+ }
783
+ if (bins.size <= cap) {
784
+ return [...bins.values()].map(({ lo, hi }) => ({ center: [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2], size: [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]] }));
785
+ }
786
+ side *= 1.25;
787
+ }
788
+ };
635
789
 
636
790
  // src/layers.ts
637
791
  var MAX_SPALLS = 64;
@@ -1672,6 +1826,7 @@ var GameDestruction = (() => {
1672
1826
  },
1673
1827
  {
1674
1828
  id: "wood_soft",
1829
+ splitForm: "slivers",
1675
1830
  density: 500,
1676
1831
  hpPerKg: 90,
1677
1832
  penetration: 9,
@@ -1717,6 +1872,7 @@ var GameDestruction = (() => {
1717
1872
  },
1718
1873
  {
1719
1874
  id: "wood_hard",
1875
+ splitForm: "slivers",
1720
1876
  density: 750,
1721
1877
  hpPerKg: 140,
1722
1878
  penetration: 14,
@@ -1761,6 +1917,7 @@ var GameDestruction = (() => {
1761
1917
  },
1762
1918
  {
1763
1919
  id: "plywood",
1920
+ splitForm: "plies",
1764
1921
  density: 600,
1765
1922
  hpPerKg: 70,
1766
1923
  penetration: 7,
@@ -1808,6 +1965,7 @@ var GameDestruction = (() => {
1808
1965
  },
1809
1966
  {
1810
1967
  id: "mdf",
1968
+ splitForm: "granules",
1811
1969
  density: 750,
1812
1970
  hpPerKg: 45,
1813
1971
  penetration: 6,
@@ -1974,6 +2132,7 @@ var GameDestruction = (() => {
1974
2132
  floor: 60
1975
2133
  }
1976
2134
  },
2135
+ ricochetDeg: 12,
1977
2136
  hitDebris: "chips",
1978
2137
  breakDebris: "dust",
1979
2138
  visual: { innerColor: 10197652 },
@@ -2005,6 +2164,7 @@ var GameDestruction = (() => {
2005
2164
  0.04,
2006
2165
  0.12
2007
2166
  ],
2167
+ ricochetDeg: 10,
2008
2168
  hitDebris: "chips",
2009
2169
  breakDebris: "dust",
2010
2170
  visual: { innerColor: 12085567 },
@@ -2062,6 +2222,7 @@ var GameDestruction = (() => {
2062
2222
  0.06,
2063
2223
  0.3
2064
2224
  ],
2225
+ ricochetDeg: 15,
2065
2226
  hitDebris: "chips",
2066
2227
  breakDebris: "dust",
2067
2228
  visual: { innerColor: 9407364 },
@@ -2319,6 +2480,7 @@ var GameDestruction = (() => {
2319
2480
  },
2320
2481
  {
2321
2482
  id: "green_wood",
2483
+ splitForm: "slivers",
2322
2484
  density: 650,
2323
2485
  hpPerKg: 5,
2324
2486
  penetration: 10,
@@ -2489,6 +2651,7 @@ var GameDestruction = (() => {
2489
2651
  0.03,
2490
2652
  0.15
2491
2653
  ],
2654
+ ricochetDeg: 8,
2492
2655
  hitDebris: "chips",
2493
2656
  breakDebris: "dust",
2494
2657
  thermal: {
@@ -2588,6 +2751,7 @@ var GameDestruction = (() => {
2588
2751
  0.02,
2589
2752
  0.08
2590
2753
  ],
2754
+ ricochetDeg: 8,
2591
2755
  hitDebris: "chips",
2592
2756
  breakDebris: "dust",
2593
2757
  thermal: {
@@ -2599,6 +2763,127 @@ var GameDestruction = (() => {
2599
2763
  "brittle"
2600
2764
  ],
2601
2765
  compressive: 1e5
2766
+ },
2767
+ {
2768
+ id: "mud",
2769
+ density: 1700,
2770
+ hpPerKg: 1,
2771
+ penetration: 5,
2772
+ ductility: 0.8,
2773
+ fracture: "crumble",
2774
+ fragmentSize: [
2775
+ 0.02,
2776
+ 0.08
2777
+ ],
2778
+ hitDebris: "dust",
2779
+ breakDebris: "dust",
2780
+ thermal: {
2781
+ conductivity: 0.5,
2782
+ heatCapacity: 3.2
2783
+ },
2784
+ tags: [
2785
+ "soil",
2786
+ "wet"
2787
+ ],
2788
+ compressive: 5e3
2789
+ },
2790
+ {
2791
+ id: "clay",
2792
+ density: 1800,
2793
+ hpPerKg: 5,
2794
+ penetration: 7,
2795
+ ductility: 0.05,
2796
+ fracture: "crumble",
2797
+ fragmentSize: [
2798
+ 0.02,
2799
+ 0.1
2800
+ ],
2801
+ hitDebris: "chips",
2802
+ breakDebris: "dust",
2803
+ thermal: {
2804
+ conductivity: 0.15,
2805
+ heatCapacity: 2.2
2806
+ },
2807
+ tags: [
2808
+ "soil",
2809
+ "brittle"
2810
+ ],
2811
+ compressive: 6e4
2812
+ },
2813
+ {
2814
+ id: "snow",
2815
+ density: 300,
2816
+ hpPerKg: 1,
2817
+ penetration: 3,
2818
+ ductility: 0.6,
2819
+ fracture: "crumble",
2820
+ fragmentSize: [
2821
+ 0.01,
2822
+ 0.05
2823
+ ],
2824
+ hitDebris: "dust",
2825
+ breakDebris: "dust",
2826
+ thermal: {
2827
+ conductivity: 0.1,
2828
+ heatCapacity: 2.1
2829
+ },
2830
+ tags: [
2831
+ "soil",
2832
+ "frozen"
2833
+ ],
2834
+ compressive: 1e3
2835
+ },
2836
+ {
2837
+ id: "cobble",
2838
+ density: 2650,
2839
+ hpPerKg: 25,
2840
+ penetration: 25,
2841
+ penetrationClass: "concrete",
2842
+ penetrationScale: 0.9,
2843
+ ductility: 0.03,
2844
+ fracture: "crumble",
2845
+ fragmentSize: [
2846
+ 0.05,
2847
+ 0.15
2848
+ ],
2849
+ ricochetDeg: 15,
2850
+ hitDebris: "chips",
2851
+ breakDebris: "chips",
2852
+ thermal: {
2853
+ conductivity: 0.2,
2854
+ heatCapacity: 2.5
2855
+ },
2856
+ tags: [
2857
+ "masonry",
2858
+ "brittle"
2859
+ ],
2860
+ compressive: 3e5
2861
+ },
2862
+ {
2863
+ id: "paver",
2864
+ density: 2300,
2865
+ hpPerKg: 14,
2866
+ penetration: 18,
2867
+ penetrationClass: "concrete",
2868
+ penetrationScale: 1,
2869
+ ductility: 0.03,
2870
+ fracture: "crumble",
2871
+ fragmentSize: [
2872
+ 0.04,
2873
+ 0.12
2874
+ ],
2875
+ ricochetDeg: 12,
2876
+ hitDebris: "chips",
2877
+ breakDebris: "chips",
2878
+ thermal: {
2879
+ conductivity: 0.15,
2880
+ heatCapacity: 2.5
2881
+ },
2882
+ tags: [
2883
+ "masonry",
2884
+ "brittle"
2885
+ ],
2886
+ compressive: 35e4
2602
2887
  }
2603
2888
  ];
2604
2889
 
@@ -3103,6 +3388,11 @@ var GameDestruction = (() => {
3103
3388
  var BREACH_BLAST_FACTOR = 6;
3104
3389
  var BREACH_AREA = 0.25;
3105
3390
  var BREACH_BLAST_REACH = 0.12;
3391
+ var MEMBER_CUT_REACH = 0.5;
3392
+ var BLAST_REFERENCE_RESISTANCE = 200 * 1900 * 0.13;
3393
+ var BLAST_EDGE_RAGGED = 0.3;
3394
+ var BLAST_EDGE_LOOSE = 1.35;
3395
+ var BLAST_EDGE_WEAR = 0.6;
3106
3396
  var RESIDUAL_CAPACITY = 0.15;
3107
3397
  var SEVER_BAND = 0.1;
3108
3398
  var SEVER_ASPECT = 3;
@@ -4355,7 +4645,7 @@ var GameDestruction = (() => {
4355
4645
  if (j.st.open) continue;
4356
4646
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
4357
4647
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
4358
- j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
4648
+ j.st.hp -= span === void 0 ? amount : Math.min(amount, j.st.maxHp) * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
4359
4649
  if (j.st.hp <= 0) {
4360
4650
  j.st.hp = 0;
4361
4651
  j.st.open = true;
@@ -4483,6 +4773,7 @@ var GameDestruction = (() => {
4483
4773
  const worn = (_a = p.rt).chips ?? (_a.chips = {});
4484
4774
  const cut = (_b = p.rt).craters ?? (_b.craters = {});
4485
4775
  const out = [];
4776
+ const wholeBefore = Object.values(worn).filter((w) => w === -1).length;
4486
4777
  const shot = kind === "ballistic";
4487
4778
  for (let i = 0; i < grid.length; i++) {
4488
4779
  const cell = grid[i];
@@ -4541,6 +4832,7 @@ var GameDestruction = (() => {
4541
4832
  emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
4542
4833
  debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
4543
4834
  }
4835
+ if (Object.values(worn).filter((w) => w === -1).length > wholeBefore) dropUnsupported(o, p, src);
4544
4836
  if (p.spec.reinforcement) exposeRebar(o, p, src);
4545
4837
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
4546
4838
  if (p.structural && !isLost(p)) {
@@ -4644,10 +4936,122 @@ var GameDestruction = (() => {
4644
4936
  setStage(o, p, 1, {}, src);
4645
4937
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
4646
4938
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
4939
+ dropUnsupported(o, p, src);
4647
4940
  const reinforcement = p.spec.reinforcement;
4648
4941
  if (reinforcement) exposeRebar(o, p, src);
4649
4942
  if (p.structural) solveStructure(o);
4650
4943
  };
4944
+ const blastReachOf = (p, energy) => {
4945
+ const perKg = p.mat.hits?.blunt?.unitJoules ?? chipEnergy;
4946
+ const resistance = Math.max(1, perKg * p.mat.density * p.thickness);
4947
+ return BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3) * clamp2(Math.cbrt(BLAST_REFERENCE_RESISTANCE / resistance), 0.3, 2);
4948
+ };
4949
+ const blastUnits = (o, p, wp, energy, dist, src) => {
4950
+ var _a, _b;
4951
+ if (!p.staged || p.st.stripped || isLost(p) || chipEnergy <= 0) return false;
4952
+ const reach = blastReachOf(p, energy);
4953
+ if (dist >= reach) return false;
4954
+ const radius = Math.sqrt(reach * reach - dist * dist);
4955
+ const [u, v] = planeAxes(p.thin);
4956
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
4957
+ const op = p.st.opening ? toLocal(p.pos, p.q, p.st.opening.center) : null;
4958
+ const grid = gridOf(p);
4959
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
4960
+ const perKg = p.mat.hits?.blunt?.unitJoules ?? chipEnergy;
4961
+ const lobes = [rnd() * 2 * Math.PI, rnd() * 2 * Math.PI];
4962
+ const out = [];
4963
+ for (let i = 0; i < grid.length; i++) {
4964
+ const cell = grid[i];
4965
+ if (worn[i] === -1) continue;
4966
+ if (op && p.st.opening && Math.hypot(cell.center[u] - op[u], cell.center[v] - op[v]) <= p.st.opening.radius) continue;
4967
+ const du = cell.center[u] - lp[u], dv = cell.center[v] - lp[v];
4968
+ const angle = Math.atan2(dv, du);
4969
+ const edge = radius * (1 + BLAST_EDGE_RAGGED * (0.5 * Math.sin(3 * angle + lobes[0]) + 0.3 * Math.sin(5 * angle + lobes[1]) + 0.4 * (rnd() - 0.5)));
4970
+ const d = Math.hypot(du, dv);
4971
+ if (d <= edge) {
4972
+ worn[i] = -1;
4973
+ out.push(i);
4974
+ } else if (d <= edge * BLAST_EDGE_LOOSE) {
4975
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
4976
+ worn[i] = Math.max(worn[i] ?? 0, perKg * mass * BLAST_EDGE_WEAR);
4977
+ }
4978
+ }
4979
+ if (out.length === 0) return false;
4980
+ const loose = unsupportedCells(grid, p.thin, p.dims, (i) => worn[i] !== -1 && !(op && p.st.opening && Math.hypot(grid[i].center[u] - op[u], grid[i].center[v] - op[v]) <= p.st.opening.radius));
4981
+ for (const i of loose) worn[i] = -1;
4982
+ const blown = out.map((i) => grid[i]), fallen = loose.map((i) => grid[i]);
4983
+ const volume = (cells2) => cells2.reduce((sum, cell) => sum + cell.size[0] * cell.size[1] * cell.size[2], 0);
4984
+ const blownMass = Math.min(p.st.mass, p.mat.density * volume(blown));
4985
+ const fallenMass = Math.min(p.st.mass - blownMass, p.mat.density * volume(fallen));
4986
+ const speed = clamp2(Math.sqrt(2 * energy * 0.02 / Math.max(blownMass, 0.05)), 1, 12);
4987
+ const { c, q } = partXf(o, p);
4988
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
4989
+ const tag = `x${chipped.length}_`;
4990
+ const fragments = [
4991
+ ...cellFragments(o, p, clumpCells(blown, p.thin, maxFragments), wp, null, speed, blownMass, tag, true),
4992
+ ...fallen.length > 0 ? cellFragments(o, p, clumpCells(fallen, p.thin, Math.max(2, Math.round(maxFragments / 4))), wp, null, 0.3, fallenMass, `${tag}f`, true) : []
4993
+ ];
4994
+ const cells = [...blown, ...fallen];
4995
+ const holes = localHolesOf(p);
4996
+ p.st.holes = p.st.holes.filter((_, k) => !cells.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length > 0));
4997
+ const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
4998
+ for (const cell of cells) {
4999
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
5000
+ for (let k = 0; k < 3; k++) {
5001
+ lo[k] = Math.min(lo[k], cell.center[k] - cell.size[k] / 2);
5002
+ hi[k] = Math.max(hi[k], cell.center[k] + cell.size[k] / 2);
5003
+ }
5004
+ }
5005
+ p.st.mass = Math.max(0, p.st.mass - blownMass - fallenMass);
5006
+ p.rt.removed = (p.rt.removed ?? 0) + blownMass + fallenMass;
5007
+ markDamaged(p);
5008
+ setStage(o, p, 1, {}, src);
5009
+ const center = toWorld(c, q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
5010
+ const units = cells.map((cell) => ({ center: toWorld(c, q, cell.center), size: [cell.size[0], cell.size[1], cell.size[2]] }));
5011
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]], rotation: q, fragments, units });
5012
+ debris(o, p, p.mat.breakDebris, center, vec.normalize(vec.sub(center, wp), [0, 1, 0]), 14, 3, src);
5013
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
5014
+ if (p.structural && !isLost(p)) {
5015
+ solveStructure(o);
5016
+ collapseCheck(o, src);
5017
+ }
5018
+ return true;
5019
+ };
5020
+ const dropUnsupported = (o, p, src) => {
5021
+ var _a, _b;
5022
+ if (!p.spec.units || p.spec.units.mortar === p.spec.material || p.st.stripped || isLost(p)) return 0;
5023
+ const [u, v] = planeAxes(p.thin);
5024
+ const grid = gridOf(p);
5025
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
5026
+ const op = p.st.opening ? toLocal(p.pos, p.q, p.st.opening.center) : null;
5027
+ const opened = (cell) => !!op && !!p.st.opening && Math.hypot(cell.center[u] - op[u], cell.center[v] - op[v]) <= p.st.opening.radius;
5028
+ const loose = unsupportedCells(grid, p.thin, p.dims, (i) => worn[i] !== -1 && !opened(grid[i]));
5029
+ if (loose.length === 0) return 0;
5030
+ for (const i of loose) worn[i] = -1;
5031
+ const fallen = loose.map((i) => grid[i]);
5032
+ const mass = Math.min(p.st.mass, p.mat.density * fallen.reduce((sum, cell) => sum + cell.size[0] * cell.size[1] * cell.size[2], 0));
5033
+ const { c, q } = partXf(o, p);
5034
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
5035
+ const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
5036
+ for (const cell of fallen) {
5037
+ for (let k = 0; k < 3; k++) {
5038
+ lo[k] = Math.min(lo[k], cell.center[k] - cell.size[k] / 2);
5039
+ hi[k] = Math.max(hi[k], cell.center[k] + cell.size[k] / 2);
5040
+ }
5041
+ }
5042
+ const center = toWorld(c, q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
5043
+ const fragments = cellFragments(o, p, clumpCells(fallen, p.thin, Math.max(2, Math.round(maxFragments / 4))), vec.add(center, [0, 1, 0]), null, 0.3, mass, `u${chipped.length}_`, true);
5044
+ const holes = localHolesOf(p);
5045
+ p.st.holes = p.st.holes.filter((_, k) => !fallen.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length > 0));
5046
+ for (const cell of fallen) chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
5047
+ p.st.mass = Math.max(0, p.st.mass - mass);
5048
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
5049
+ markDamaged(p);
5050
+ const units = fallen.map((cell) => ({ center: toWorld(c, q, cell.center), size: [cell.size[0], cell.size[1], cell.size[2]] }));
5051
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]], rotation: q, fragments, units });
5052
+ debris(o, p, p.mat.breakDebris, center, [0, -1, 0], 6, 1, src);
5053
+ return loose.length;
5054
+ };
4651
5055
  const stripPart = (o, p, reinforcement, src) => {
4652
5056
  const bars = rebarBars(p.dims, p.thin, reinforcement.spacing);
4653
5057
  p.st.stripped = true;
@@ -4714,6 +5118,7 @@ var GameDestruction = (() => {
4714
5118
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
4715
5119
  const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
4716
5120
  const cutAt = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
5121
+ const planRnd = p.mat.splitForm ? splitStream(o.id, p.spec.id, hitLocal) : rnd;
4717
5122
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
4718
5123
  style,
4719
5124
  dims: p.dims,
@@ -4722,8 +5127,9 @@ var GameDestruction = (() => {
4722
5127
  hit: hitLocal,
4723
5128
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
4724
5129
  maxFragments,
4725
- rnd,
5130
+ rnd: planRnd,
4726
5131
  sheet: p.mat.sheet,
5132
+ ...p.mat.splitForm ? { form: p.mat.splitForm } : {},
4727
5133
  ...cutAt !== void 0 ? { cut: { at: cutAt, gap: SEVER_BAND } } : {}
4728
5134
  });
4729
5135
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
@@ -4862,7 +5268,8 @@ var GameDestruction = (() => {
4862
5268
  const fragments = [];
4863
5269
  for (let i = 0; i < n; i++) {
4864
5270
  const back = !exit || i % 3 === 2;
4865
- 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());
5271
+ const plies = p.mat.splitForm === "plies";
5272
+ 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() : plies ? 0.15 + 0.15 * rnd() : 0.4 + 0.4 * rnd());
4866
5273
  const size = [w, w, w];
4867
5274
  size[along] = len;
4868
5275
  size[(along + 1) % 3] = w;
@@ -5023,7 +5430,7 @@ var GameDestruction = (() => {
5023
5430
  checkIgnite(o, p, src);
5024
5431
  }
5025
5432
  }
5026
- wearJoints(o, p, Ein * 0.5, void 0, 0, src);
5433
+ wearJoints(o, p, Ein * 0.5, void 0, 0, src, false, 2 * BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3));
5027
5434
  const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
5028
5435
  if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
5029
5436
  frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
@@ -5033,7 +5440,10 @@ var GameDestruction = (() => {
5033
5440
  const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
5034
5441
  res.damage += dealt;
5035
5442
  if (p.bend && flexure.enabled) bendByBlast(o, p, point, energy, radius, reach, src);
5036
- if (dist <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
5443
+ const breachReach = BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3);
5444
+ if (p.spec.units && p.spec.units.mortar !== p.spec.material) blastUnits(o, p, wp, energy, dist, src);
5445
+ else if (p.spec.reinforcement && p.spec.units && dist > MEMBER_CUT_REACH * breachReach) blastUnits(o, p, wp, energy, dist, src);
5446
+ else if (dist <= breachReach) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
5037
5447
  return reach;
5038
5448
  };
5039
5449
  const blastProbe = { partsTested: 0, partsNear: 0 };
@@ -5279,6 +5689,11 @@ var GameDestruction = (() => {
5279
5689
  }
5280
5690
  return h | 0;
5281
5691
  };
5692
+ const splitStream = (objectId, partId, hit) => {
5693
+ const codes = Array.from(`${objectId}|${partId}`, (char) => char.charCodeAt(0) / 1e3);
5694
+ const stream = createRng(seedOf(rng.state() / 1e3, ...codes, hit[0], hit[1], hit[2]));
5695
+ return () => stream.next();
5696
+ };
5282
5697
  const throwFragment = (frag, objectId, from, g, centre, index, src, shields, results, reports, mult) => {
5283
5698
  const v = frag.velocity;
5284
5699
  const speed = Math.hypot(v[0], v[1], v[2]);
@@ -6540,7 +6955,12 @@ var GameDestruction = (() => {
6540
6955
  sand: "sand",
6541
6956
  gravel: "gravel",
6542
6957
  turf: "turf",
6543
- mortar: "mortar"
6958
+ mortar: "mortar",
6959
+ mud: "mud",
6960
+ clay: "dry clay",
6961
+ snow: "snow",
6962
+ cobble: "cobblestone",
6963
+ paver: "concrete paver"
6544
6964
  },
6545
6965
  partStatus: { intact: "intact", damaged: "damaged", collapsed: "collapsed", broken: "broken", melted: "melted", ashes: "ashes" },
6546
6966
  jointKinds: {
@@ -6636,7 +7056,12 @@ var GameDestruction = (() => {
6636
7056
  sand: "areia",
6637
7057
  gravel: "brita",
6638
7058
  turf: "grama",
6639
- mortar: "argamassa"
7059
+ mortar: "argamassa",
7060
+ mud: "lama",
7061
+ clay: "barro seco",
7062
+ snow: "neve",
7063
+ cobble: "paralelep\xEDpedo",
7064
+ paver: "bloquete"
6640
7065
  },
6641
7066
  partStatus: { intact: "intacto", damaged: "danificado", collapsed: "desabado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
6642
7067
  jointKinds: {