@adatechnology/game-destruction 0.15.0 → 0.17.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,
@@ -2319,6 +2477,7 @@ var GameDestruction = (() => {
2319
2477
  },
2320
2478
  {
2321
2479
  id: "green_wood",
2480
+ splitForm: "slivers",
2322
2481
  density: 650,
2323
2482
  hpPerKg: 5,
2324
2483
  penetration: 10,
@@ -2599,6 +2758,125 @@ var GameDestruction = (() => {
2599
2758
  "brittle"
2600
2759
  ],
2601
2760
  compressive: 1e5
2761
+ },
2762
+ {
2763
+ id: "mud",
2764
+ density: 1700,
2765
+ hpPerKg: 1,
2766
+ penetration: 5,
2767
+ ductility: 0.8,
2768
+ fracture: "crumble",
2769
+ fragmentSize: [
2770
+ 0.02,
2771
+ 0.08
2772
+ ],
2773
+ hitDebris: "dust",
2774
+ breakDebris: "dust",
2775
+ thermal: {
2776
+ conductivity: 0.5,
2777
+ heatCapacity: 3.2
2778
+ },
2779
+ tags: [
2780
+ "soil",
2781
+ "wet"
2782
+ ],
2783
+ compressive: 5e3
2784
+ },
2785
+ {
2786
+ id: "clay",
2787
+ density: 1800,
2788
+ hpPerKg: 5,
2789
+ penetration: 7,
2790
+ ductility: 0.05,
2791
+ fracture: "crumble",
2792
+ fragmentSize: [
2793
+ 0.02,
2794
+ 0.1
2795
+ ],
2796
+ hitDebris: "chips",
2797
+ breakDebris: "dust",
2798
+ thermal: {
2799
+ conductivity: 0.15,
2800
+ heatCapacity: 2.2
2801
+ },
2802
+ tags: [
2803
+ "soil",
2804
+ "brittle"
2805
+ ],
2806
+ compressive: 6e4
2807
+ },
2808
+ {
2809
+ id: "snow",
2810
+ density: 300,
2811
+ hpPerKg: 1,
2812
+ penetration: 3,
2813
+ ductility: 0.6,
2814
+ fracture: "crumble",
2815
+ fragmentSize: [
2816
+ 0.01,
2817
+ 0.05
2818
+ ],
2819
+ hitDebris: "dust",
2820
+ breakDebris: "dust",
2821
+ thermal: {
2822
+ conductivity: 0.1,
2823
+ heatCapacity: 2.1
2824
+ },
2825
+ tags: [
2826
+ "soil",
2827
+ "frozen"
2828
+ ],
2829
+ compressive: 1e3
2830
+ },
2831
+ {
2832
+ id: "cobble",
2833
+ density: 2650,
2834
+ hpPerKg: 25,
2835
+ penetration: 25,
2836
+ penetrationClass: "concrete",
2837
+ penetrationScale: 0.9,
2838
+ ductility: 0.03,
2839
+ fracture: "crumble",
2840
+ fragmentSize: [
2841
+ 0.05,
2842
+ 0.15
2843
+ ],
2844
+ hitDebris: "chips",
2845
+ breakDebris: "chips",
2846
+ thermal: {
2847
+ conductivity: 0.2,
2848
+ heatCapacity: 2.5
2849
+ },
2850
+ tags: [
2851
+ "masonry",
2852
+ "brittle"
2853
+ ],
2854
+ compressive: 3e5
2855
+ },
2856
+ {
2857
+ id: "paver",
2858
+ density: 2300,
2859
+ hpPerKg: 14,
2860
+ penetration: 18,
2861
+ penetrationClass: "concrete",
2862
+ penetrationScale: 1,
2863
+ ductility: 0.03,
2864
+ fracture: "crumble",
2865
+ fragmentSize: [
2866
+ 0.04,
2867
+ 0.12
2868
+ ],
2869
+ hitDebris: "chips",
2870
+ breakDebris: "chips",
2871
+ thermal: {
2872
+ conductivity: 0.15,
2873
+ heatCapacity: 2.5
2874
+ },
2875
+ tags: [
2876
+ "masonry",
2877
+ "brittle"
2878
+ ],
2879
+ compressive: 35e4
2602
2880
  }
2603
2881
  ];
2604
2882
 
@@ -2782,6 +3060,7 @@ var GameDestruction = (() => {
2782
3060
  };
2783
3061
 
2784
3062
  // ../game-contracts/dist/index.js
3063
+ var DEFAULT_BLAST_IMPULSE = { bodyArea: 0.7, lift: 0.15, minSpeed: 0.3, tntPerJoule: 0.24 / 2e5 };
2785
3064
  var DEFAULT_COST_READS = 1e4;
2786
3065
  var DEFAULT_COST_CEILING_MS = 50;
2787
3066
  var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
@@ -3102,6 +3381,11 @@ var GameDestruction = (() => {
3102
3381
  var BREACH_BLAST_FACTOR = 6;
3103
3382
  var BREACH_AREA = 0.25;
3104
3383
  var BREACH_BLAST_REACH = 0.12;
3384
+ var MEMBER_CUT_REACH = 0.5;
3385
+ var BLAST_REFERENCE_RESISTANCE = 200 * 1900 * 0.13;
3386
+ var BLAST_EDGE_RAGGED = 0.3;
3387
+ var BLAST_EDGE_LOOSE = 1.35;
3388
+ var BLAST_EDGE_WEAR = 0.6;
3105
3389
  var RESIDUAL_CAPACITY = 0.15;
3106
3390
  var SEVER_BAND = 0.1;
3107
3391
  var SEVER_ASPECT = 3;
@@ -4354,7 +4638,7 @@ var GameDestruction = (() => {
4354
4638
  if (j.st.open) continue;
4355
4639
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
4356
4640
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
4357
- j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
4641
+ j.st.hp -= span === void 0 ? amount : Math.min(amount, j.st.maxHp) * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
4358
4642
  if (j.st.hp <= 0) {
4359
4643
  j.st.hp = 0;
4360
4644
  j.st.open = true;
@@ -4482,6 +4766,7 @@ var GameDestruction = (() => {
4482
4766
  const worn = (_a = p.rt).chips ?? (_a.chips = {});
4483
4767
  const cut = (_b = p.rt).craters ?? (_b.craters = {});
4484
4768
  const out = [];
4769
+ const wholeBefore = Object.values(worn).filter((w) => w === -1).length;
4485
4770
  const shot = kind === "ballistic";
4486
4771
  for (let i = 0; i < grid.length; i++) {
4487
4772
  const cell = grid[i];
@@ -4540,6 +4825,7 @@ var GameDestruction = (() => {
4540
4825
  emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
4541
4826
  debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
4542
4827
  }
4828
+ if (Object.values(worn).filter((w) => w === -1).length > wholeBefore) dropUnsupported(o, p, src);
4543
4829
  if (p.spec.reinforcement) exposeRebar(o, p, src);
4544
4830
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
4545
4831
  if (p.structural && !isLost(p)) {
@@ -4643,10 +4929,122 @@ var GameDestruction = (() => {
4643
4929
  setStage(o, p, 1, {}, src);
4644
4930
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
4645
4931
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
4932
+ dropUnsupported(o, p, src);
4646
4933
  const reinforcement = p.spec.reinforcement;
4647
4934
  if (reinforcement) exposeRebar(o, p, src);
4648
4935
  if (p.structural) solveStructure(o);
4649
4936
  };
4937
+ const blastReachOf = (p, energy) => {
4938
+ const perKg = p.mat.hits?.blunt?.unitJoules ?? chipEnergy;
4939
+ const resistance = Math.max(1, perKg * p.mat.density * p.thickness);
4940
+ return BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3) * clamp2(Math.cbrt(BLAST_REFERENCE_RESISTANCE / resistance), 0.3, 2);
4941
+ };
4942
+ const blastUnits = (o, p, wp, energy, dist, src) => {
4943
+ var _a, _b;
4944
+ if (!p.staged || p.st.stripped || isLost(p) || chipEnergy <= 0) return false;
4945
+ const reach = blastReachOf(p, energy);
4946
+ if (dist >= reach) return false;
4947
+ const radius = Math.sqrt(reach * reach - dist * dist);
4948
+ const [u, v] = planeAxes(p.thin);
4949
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
4950
+ const op = p.st.opening ? toLocal(p.pos, p.q, p.st.opening.center) : null;
4951
+ const grid = gridOf(p);
4952
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
4953
+ const perKg = p.mat.hits?.blunt?.unitJoules ?? chipEnergy;
4954
+ const lobes = [rnd() * 2 * Math.PI, rnd() * 2 * Math.PI];
4955
+ const out = [];
4956
+ for (let i = 0; i < grid.length; i++) {
4957
+ const cell = grid[i];
4958
+ if (worn[i] === -1) continue;
4959
+ if (op && p.st.opening && Math.hypot(cell.center[u] - op[u], cell.center[v] - op[v]) <= p.st.opening.radius) continue;
4960
+ const du = cell.center[u] - lp[u], dv = cell.center[v] - lp[v];
4961
+ const angle = Math.atan2(dv, du);
4962
+ 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)));
4963
+ const d = Math.hypot(du, dv);
4964
+ if (d <= edge) {
4965
+ worn[i] = -1;
4966
+ out.push(i);
4967
+ } else if (d <= edge * BLAST_EDGE_LOOSE) {
4968
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
4969
+ worn[i] = Math.max(worn[i] ?? 0, perKg * mass * BLAST_EDGE_WEAR);
4970
+ }
4971
+ }
4972
+ if (out.length === 0) return false;
4973
+ 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));
4974
+ for (const i of loose) worn[i] = -1;
4975
+ const blown = out.map((i) => grid[i]), fallen = loose.map((i) => grid[i]);
4976
+ const volume = (cells2) => cells2.reduce((sum, cell) => sum + cell.size[0] * cell.size[1] * cell.size[2], 0);
4977
+ const blownMass = Math.min(p.st.mass, p.mat.density * volume(blown));
4978
+ const fallenMass = Math.min(p.st.mass - blownMass, p.mat.density * volume(fallen));
4979
+ const speed = clamp2(Math.sqrt(2 * energy * 0.02 / Math.max(blownMass, 0.05)), 1, 12);
4980
+ const { c, q } = partXf(o, p);
4981
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
4982
+ const tag = `x${chipped.length}_`;
4983
+ const fragments = [
4984
+ ...cellFragments(o, p, clumpCells(blown, p.thin, maxFragments), wp, null, speed, blownMass, tag, true),
4985
+ ...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) : []
4986
+ ];
4987
+ const cells = [...blown, ...fallen];
4988
+ const holes = localHolesOf(p);
4989
+ p.st.holes = p.st.holes.filter((_, k) => !cells.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length > 0));
4990
+ const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
4991
+ for (const cell of cells) {
4992
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
4993
+ for (let k = 0; k < 3; k++) {
4994
+ lo[k] = Math.min(lo[k], cell.center[k] - cell.size[k] / 2);
4995
+ hi[k] = Math.max(hi[k], cell.center[k] + cell.size[k] / 2);
4996
+ }
4997
+ }
4998
+ p.st.mass = Math.max(0, p.st.mass - blownMass - fallenMass);
4999
+ p.rt.removed = (p.rt.removed ?? 0) + blownMass + fallenMass;
5000
+ markDamaged(p);
5001
+ setStage(o, p, 1, {}, src);
5002
+ const center = toWorld(c, q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
5003
+ const units = cells.map((cell) => ({ center: toWorld(c, q, cell.center), size: [cell.size[0], cell.size[1], cell.size[2]] }));
5004
+ 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 });
5005
+ debris(o, p, p.mat.breakDebris, center, vec.normalize(vec.sub(center, wp), [0, 1, 0]), 14, 3, src);
5006
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
5007
+ if (p.structural && !isLost(p)) {
5008
+ solveStructure(o);
5009
+ collapseCheck(o, src);
5010
+ }
5011
+ return true;
5012
+ };
5013
+ const dropUnsupported = (o, p, src) => {
5014
+ var _a, _b;
5015
+ if (!p.spec.units || p.spec.units.mortar === p.spec.material || p.st.stripped || isLost(p)) return 0;
5016
+ const [u, v] = planeAxes(p.thin);
5017
+ const grid = gridOf(p);
5018
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
5019
+ const op = p.st.opening ? toLocal(p.pos, p.q, p.st.opening.center) : null;
5020
+ const opened = (cell) => !!op && !!p.st.opening && Math.hypot(cell.center[u] - op[u], cell.center[v] - op[v]) <= p.st.opening.radius;
5021
+ const loose = unsupportedCells(grid, p.thin, p.dims, (i) => worn[i] !== -1 && !opened(grid[i]));
5022
+ if (loose.length === 0) return 0;
5023
+ for (const i of loose) worn[i] = -1;
5024
+ const fallen = loose.map((i) => grid[i]);
5025
+ 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));
5026
+ const { c, q } = partXf(o, p);
5027
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
5028
+ const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
5029
+ for (const cell of fallen) {
5030
+ for (let k = 0; k < 3; k++) {
5031
+ lo[k] = Math.min(lo[k], cell.center[k] - cell.size[k] / 2);
5032
+ hi[k] = Math.max(hi[k], cell.center[k] + cell.size[k] / 2);
5033
+ }
5034
+ }
5035
+ const center = toWorld(c, q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
5036
+ 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);
5037
+ const holes = localHolesOf(p);
5038
+ p.st.holes = p.st.holes.filter((_, k) => !fallen.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length > 0));
5039
+ 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]] });
5040
+ p.st.mass = Math.max(0, p.st.mass - mass);
5041
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
5042
+ markDamaged(p);
5043
+ const units = fallen.map((cell) => ({ center: toWorld(c, q, cell.center), size: [cell.size[0], cell.size[1], cell.size[2]] }));
5044
+ 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 });
5045
+ debris(o, p, p.mat.breakDebris, center, [0, -1, 0], 6, 1, src);
5046
+ return loose.length;
5047
+ };
4650
5048
  const stripPart = (o, p, reinforcement, src) => {
4651
5049
  const bars = rebarBars(p.dims, p.thin, reinforcement.spacing);
4652
5050
  p.st.stripped = true;
@@ -4713,6 +5111,7 @@ var GameDestruction = (() => {
4713
5111
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
4714
5112
  const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
4715
5113
  const cutAt = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
5114
+ const planRnd = p.mat.splitForm ? splitStream(o.id, p.spec.id, hitLocal) : rnd;
4716
5115
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
4717
5116
  style,
4718
5117
  dims: p.dims,
@@ -4721,8 +5120,9 @@ var GameDestruction = (() => {
4721
5120
  hit: hitLocal,
4722
5121
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
4723
5122
  maxFragments,
4724
- rnd,
5123
+ rnd: planRnd,
4725
5124
  sheet: p.mat.sheet,
5125
+ ...p.mat.splitForm ? { form: p.mat.splitForm } : {},
4726
5126
  ...cutAt !== void 0 ? { cut: { at: cutAt, gap: SEVER_BAND } } : {}
4727
5127
  });
4728
5128
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
@@ -4767,7 +5167,7 @@ var GameDestruction = (() => {
4767
5167
  return;
4768
5168
  }
4769
5169
  if (ex) {
4770
- emit({ ...base(o, src), type: "explode", part: p.spec.id, point: c, energy: ex.energy, radius: ex.radius, ...ex.lethalRadius !== void 0 ? { lethalRadius: ex.lethalRadius } : {}, ...ex.effect !== void 0 ? { effect: ex.effect } : {} });
5170
+ emit({ ...base(o, src), type: "explode", part: p.spec.id, point: c, energy: ex.energy, radius: ex.radius, ...ex.lethalRadius !== void 0 ? { lethalRadius: ex.lethalRadius } : {}, ...ex.tnt !== void 0 ? { tnt: ex.tnt } : {}, ...ex.effect !== void 0 ? { effect: ex.effect } : {} });
4771
5171
  blasts.push({ point: c, energy: ex.energy, radius: ex.radius, ...ex.lethalRadius !== void 0 ? { lethalRadius: ex.lethalRadius } : {}, source: src });
4772
5172
  }
4773
5173
  partLost(o, p, src);
@@ -4861,7 +5261,8 @@ var GameDestruction = (() => {
4861
5261
  const fragments = [];
4862
5262
  for (let i = 0; i < n; i++) {
4863
5263
  const back = !exit || i % 3 === 2;
4864
- 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());
5264
+ const plies = p.mat.splitForm === "plies";
5265
+ 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());
4865
5266
  const size = [w, w, w];
4866
5267
  size[along] = len;
4867
5268
  size[(along + 1) % 3] = w;
@@ -5022,7 +5423,7 @@ var GameDestruction = (() => {
5022
5423
  checkIgnite(o, p, src);
5023
5424
  }
5024
5425
  }
5025
- wearJoints(o, p, Ein * 0.5, void 0, 0, src);
5426
+ wearJoints(o, p, Ein * 0.5, void 0, 0, src, false, 2 * BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3));
5026
5427
  const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
5027
5428
  if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
5028
5429
  frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
@@ -5032,7 +5433,10 @@ var GameDestruction = (() => {
5032
5433
  const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
5033
5434
  res.damage += dealt;
5034
5435
  if (p.bend && flexure.enabled) bendByBlast(o, p, point, energy, radius, reach, src);
5035
- if (dist <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
5436
+ const breachReach = BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3);
5437
+ if (p.spec.units && p.spec.units.mortar !== p.spec.material) blastUnits(o, p, wp, energy, dist, src);
5438
+ else if (p.spec.reinforcement && p.spec.units && dist > MEMBER_CUT_REACH * breachReach) blastUnits(o, p, wp, energy, dist, src);
5439
+ else if (dist <= breachReach) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
5036
5440
  return reach;
5037
5441
  };
5038
5442
  const blastProbe = { partsTested: 0, partsNear: 0 };
@@ -5278,6 +5682,11 @@ var GameDestruction = (() => {
5278
5682
  }
5279
5683
  return h | 0;
5280
5684
  };
5685
+ const splitStream = (objectId, partId, hit) => {
5686
+ const codes = Array.from(`${objectId}|${partId}`, (char) => char.charCodeAt(0) / 1e3);
5687
+ const stream = createRng(seedOf(rng.state() / 1e3, ...codes, hit[0], hit[1], hit[2]));
5688
+ return () => stream.next();
5689
+ };
5281
5690
  const throwFragment = (frag, objectId, from, g, centre, index, src, shields, results, reports, mult) => {
5282
5691
  const v = frag.velocity;
5283
5692
  const speed = Math.hypot(v[0], v[1], v[2]);
@@ -6539,7 +6948,12 @@ var GameDestruction = (() => {
6539
6948
  sand: "sand",
6540
6949
  gravel: "gravel",
6541
6950
  turf: "turf",
6542
- mortar: "mortar"
6951
+ mortar: "mortar",
6952
+ mud: "mud",
6953
+ clay: "dry clay",
6954
+ snow: "snow",
6955
+ cobble: "cobblestone",
6956
+ paver: "concrete paver"
6543
6957
  },
6544
6958
  partStatus: { intact: "intact", damaged: "damaged", collapsed: "collapsed", broken: "broken", melted: "melted", ashes: "ashes" },
6545
6959
  jointKinds: {
@@ -6635,7 +7049,12 @@ var GameDestruction = (() => {
6635
7049
  sand: "areia",
6636
7050
  gravel: "brita",
6637
7051
  turf: "grama",
6638
- mortar: "argamassa"
7052
+ mortar: "argamassa",
7053
+ mud: "lama",
7054
+ clay: "barro seco",
7055
+ snow: "neve",
7056
+ cobble: "paralelep\xEDpedo",
7057
+ paver: "bloquete"
6639
7058
  },
6640
7059
  partStatus: { intact: "intacto", damaged: "danificado", collapsed: "desabado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
6641
7060
  jointKinds: {