@adatechnology/game-destruction 0.16.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
 
@@ -3103,6 +3381,11 @@ var GameDestruction = (() => {
3103
3381
  var BREACH_BLAST_FACTOR = 6;
3104
3382
  var BREACH_AREA = 0.25;
3105
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;
3106
3389
  var RESIDUAL_CAPACITY = 0.15;
3107
3390
  var SEVER_BAND = 0.1;
3108
3391
  var SEVER_ASPECT = 3;
@@ -4355,7 +4638,7 @@ var GameDestruction = (() => {
4355
4638
  if (j.st.open) continue;
4356
4639
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
4357
4640
  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));
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));
4359
4642
  if (j.st.hp <= 0) {
4360
4643
  j.st.hp = 0;
4361
4644
  j.st.open = true;
@@ -4483,6 +4766,7 @@ var GameDestruction = (() => {
4483
4766
  const worn = (_a = p.rt).chips ?? (_a.chips = {});
4484
4767
  const cut = (_b = p.rt).craters ?? (_b.craters = {});
4485
4768
  const out = [];
4769
+ const wholeBefore = Object.values(worn).filter((w) => w === -1).length;
4486
4770
  const shot = kind === "ballistic";
4487
4771
  for (let i = 0; i < grid.length; i++) {
4488
4772
  const cell = grid[i];
@@ -4541,6 +4825,7 @@ var GameDestruction = (() => {
4541
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 });
4542
4826
  debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
4543
4827
  }
4828
+ if (Object.values(worn).filter((w) => w === -1).length > wholeBefore) dropUnsupported(o, p, src);
4544
4829
  if (p.spec.reinforcement) exposeRebar(o, p, src);
4545
4830
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
4546
4831
  if (p.structural && !isLost(p)) {
@@ -4644,10 +4929,122 @@ var GameDestruction = (() => {
4644
4929
  setStage(o, p, 1, {}, src);
4645
4930
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
4646
4931
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
4932
+ dropUnsupported(o, p, src);
4647
4933
  const reinforcement = p.spec.reinforcement;
4648
4934
  if (reinforcement) exposeRebar(o, p, src);
4649
4935
  if (p.structural) solveStructure(o);
4650
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
+ };
4651
5048
  const stripPart = (o, p, reinforcement, src) => {
4652
5049
  const bars = rebarBars(p.dims, p.thin, reinforcement.spacing);
4653
5050
  p.st.stripped = true;
@@ -4714,6 +5111,7 @@ var GameDestruction = (() => {
4714
5111
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
4715
5112
  const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
4716
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;
4717
5115
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
4718
5116
  style,
4719
5117
  dims: p.dims,
@@ -4722,8 +5120,9 @@ var GameDestruction = (() => {
4722
5120
  hit: hitLocal,
4723
5121
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
4724
5122
  maxFragments,
4725
- rnd,
5123
+ rnd: planRnd,
4726
5124
  sheet: p.mat.sheet,
5125
+ ...p.mat.splitForm ? { form: p.mat.splitForm } : {},
4727
5126
  ...cutAt !== void 0 ? { cut: { at: cutAt, gap: SEVER_BAND } } : {}
4728
5127
  });
4729
5128
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
@@ -4862,7 +5261,8 @@ var GameDestruction = (() => {
4862
5261
  const fragments = [];
4863
5262
  for (let i = 0; i < n; i++) {
4864
5263
  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());
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());
4866
5266
  const size = [w, w, w];
4867
5267
  size[along] = len;
4868
5268
  size[(along + 1) % 3] = w;
@@ -5023,7 +5423,7 @@ var GameDestruction = (() => {
5023
5423
  checkIgnite(o, p, src);
5024
5424
  }
5025
5425
  }
5026
- 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));
5027
5427
  const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
5028
5428
  if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
5029
5429
  frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
@@ -5033,7 +5433,10 @@ var GameDestruction = (() => {
5033
5433
  const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
5034
5434
  res.damage += dealt;
5035
5435
  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);
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);
5037
5440
  return reach;
5038
5441
  };
5039
5442
  const blastProbe = { partsTested: 0, partsNear: 0 };
@@ -5279,6 +5682,11 @@ var GameDestruction = (() => {
5279
5682
  }
5280
5683
  return h | 0;
5281
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
+ };
5282
5690
  const throwFragment = (frag, objectId, from, g, centre, index, src, shields, results, reports, mult) => {
5283
5691
  const v = frag.velocity;
5284
5692
  const speed = Math.hypot(v[0], v[1], v[2]);
@@ -6540,7 +6948,12 @@ var GameDestruction = (() => {
6540
6948
  sand: "sand",
6541
6949
  gravel: "gravel",
6542
6950
  turf: "turf",
6543
- mortar: "mortar"
6951
+ mortar: "mortar",
6952
+ mud: "mud",
6953
+ clay: "dry clay",
6954
+ snow: "snow",
6955
+ cobble: "cobblestone",
6956
+ paver: "concrete paver"
6544
6957
  },
6545
6958
  partStatus: { intact: "intact", damaged: "damaged", collapsed: "collapsed", broken: "broken", melted: "melted", ashes: "ashes" },
6546
6959
  jointKinds: {
@@ -6636,7 +7049,12 @@ var GameDestruction = (() => {
6636
7049
  sand: "areia",
6637
7050
  gravel: "brita",
6638
7051
  turf: "grama",
6639
- mortar: "argamassa"
7052
+ mortar: "argamassa",
7053
+ mud: "lama",
7054
+ clay: "barro seco",
7055
+ snow: "neve",
7056
+ cobble: "paralelep\xEDpedo",
7057
+ paver: "bloquete"
6640
7058
  },
6641
7059
  partStatus: { intact: "intacto", damaged: "danificado", collapsed: "desabado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
6642
7060
  jointKinds: {