@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.
package/dist/index.js CHANGED
@@ -340,6 +340,22 @@ var frame = (ax, ay, az, angle = 0) => {
340
340
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
341
341
  };
342
342
  var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
343
+ var splitAlong = (box, count, axis, rnd) => {
344
+ const shares = Array.from({ length: count }, () => 0.25 + 1.5 * rnd());
345
+ const total = shares.reduce((sum, share) => sum + share, 0);
346
+ const out = [];
347
+ let from = box.c[axis] - box.s[axis] / 2;
348
+ for (const share of shares) {
349
+ const length = box.s[axis] * share / total;
350
+ const c = [box.c[0], box.c[1], box.c[2]];
351
+ const size = [box.s[0], box.s[1], box.s[2]];
352
+ c[axis] = from + length / 2;
353
+ size[axis] = length;
354
+ out.push({ c, s: size });
355
+ from += length;
356
+ }
357
+ return out;
358
+ };
343
359
  var splinterBox = (inp, box, limit = inp.maxFragments) => {
344
360
  const { rnd } = inp;
345
361
  const dims = box.s;
@@ -349,35 +365,81 @@ var splinterBox = (inp, box, limit = inp.maxFragments) => {
349
365
  const w = Math.max(fmin * 1.5, 0.01);
350
366
  const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
351
367
  const maxLen = fmax * 4;
352
- const cells = bsp(box, c, [a, b], rnd);
368
+ const cells = [];
369
+ for (const cell of bsp(box, c, [a, b], rnd)) {
370
+ const wide = cell.s[a] >= cell.s[b] ? a : b;
371
+ if (rnd() < 0.4 && cell.s[wide] > fmin * 0.5) for (const part of splitAlong(cell, 2, wide, rnd)) cells.push(part);
372
+ else cells.push(cell);
373
+ }
353
374
  const pieces = [];
354
375
  for (const cell of cells) {
355
- const extra = rnd() < 0.4 ? 1 : 0;
376
+ const extra = Math.floor(rnd() * 3);
356
377
  const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
357
- for (const p of bsp(cell, k, [g], rnd)) pieces.push(p);
378
+ for (const p of splitAlong(cell, k, g, rnd)) pieces.push(p);
358
379
  }
359
380
  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] }));
360
381
  };
382
+ var PLY = 3e-3;
383
+ var PLY_GAP = 0.6;
384
+ var PLY_SHEET = 0.012;
385
+ var plyBreak = (inp, root, limit) => {
386
+ const { rnd } = inp;
387
+ const dims = root.s;
388
+ const sorted = [0, 1, 2].sort((x, y) => dims[x] - dims[y]);
389
+ const t = sorted[0];
390
+ const [u, v] = [sorted[1], sorted[2]].sort((x, y) => x - y);
391
+ const along = dims[u] >= dims[v] ? u : v;
392
+ const across = along === u ? v : u;
393
+ const layers = clamp(Math.round(dims[t] / PLY / 2), 2, 3);
394
+ const pitch = dims[t] / layers;
395
+ const th = Math.min(pitch, PLY_SHEET);
396
+ const per = Math.max(2, Math.floor(limit / layers));
397
+ const out = [];
398
+ for (let k = 0; k < layers; k++) {
399
+ const layer = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
400
+ layer.c[t] = root.c[t] + (-dims[t] / 2 + (k + 0.5) * pitch) * (pitch > PLY_SHEET ? 1 : 1 + PLY_GAP);
401
+ layer.s[t] = th;
402
+ for (const cell of bsp(layer, per, [u, v], rnd)) {
403
+ if (out.length >= limit) return out;
404
+ if (rnd() < 0.35) {
405
+ const strand = cell.s[across] * (0.15 + 0.2 * rnd());
406
+ 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 });
407
+ } else {
408
+ 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 });
409
+ }
410
+ }
411
+ }
412
+ return out;
413
+ };
414
+ 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] }));
415
+ var CRUMBS = 6;
361
416
  var severAt = (inp, root, at, gap) => {
362
417
  const dims = root.s;
363
418
  const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
364
419
  const half = dims[L] / 2;
365
420
  const lo = clamp(at - gap / 2, -half, half), hi = clamp(at + gap / 2, -half, half);
366
421
  const out = [];
367
- const stub = (from, to) => {
422
+ const fibrous = inp.form === "slivers" || inp.form === "plies";
423
+ const [a, b] = [0, 1, 2].filter((i) => i !== L);
424
+ const stub = (from, to, breakAtEnd) => {
368
425
  if (to - from < 0.02) return;
369
426
  const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
370
427
  c[L] = (from + to) / 2;
371
428
  sz[L] = to - from;
372
- out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight: sz[0] * sz[1] * sz[2] });
429
+ const weight = sz[0] * sz[1] * sz[2];
430
+ 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 });
431
+ else out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight });
373
432
  };
374
- stub(-half, lo);
375
- stub(hi, half);
433
+ stub(-half, lo, true);
434
+ stub(hi, half, false);
376
435
  const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
377
436
  band.c[L] = (lo + hi) / 2;
378
437
  band.s[L] = Math.max(hi - lo, 0.01);
379
- if (inp.style === "snap") {
380
- 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] });
438
+ const room = inp.maxFragments - out.length;
439
+ if (inp.style === "snap" && inp.form === "plies") for (const p of plyBreak(inp, band, Math.max(3, room))) out.push(p);
440
+ else if (inp.style === "snap" && inp.form === "granules") for (const p of granules(inp, band, clamp(room, 3, 12))) out.push(p);
441
+ else if (inp.style === "snap") {
442
+ 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] });
381
443
  } else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
382
444
  return out;
383
445
  };
@@ -410,8 +472,18 @@ var planFragments = (inp) => {
410
472
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
411
473
  if (style === "snap") {
412
474
  if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
475
+ if (inp.form === "plies") return plyBreak(inp, root, maxFragments);
413
476
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
414
477
  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] });
478
+ if (inp.form !== "granules") return out;
479
+ const crumbs = Math.min(CRUMBS, maxFragments - out.length);
480
+ if (crumbs <= 0) return out;
481
+ const band = { c: [0, 0, 0], s: [0, 0, 0] };
482
+ for (let i = 0; i < 3; i++) {
483
+ band.s[i] = Math.min(dims[i], Math.max(fmin, dims[i] * 0.2));
484
+ band.c[i] = clamp(inp.hit[i], -dims[i] / 2 + band.s[i] / 2, dims[i] / 2 - band.s[i] / 2);
485
+ }
486
+ for (const p of granules(inp, band, crumbs)) out.push({ ...p, weight: p.weight * 0.5 });
415
487
  return out;
416
488
  }
417
489
  if (style === "crumble" || style === "none") {
@@ -520,6 +592,88 @@ var marksInBox = (center, size, rotation, holes) => {
520
592
  }
521
593
  return marks;
522
594
  };
595
+ var JOINT_GAP = 1e-4;
596
+ var MIN_BEARING_SHARE = 0.25;
597
+ var unsupportedCells = (cells, thin, dims, standing) => {
598
+ if (thin === 1) return [];
599
+ const [u, v] = planeAxes(thin);
600
+ const bottom = -dims[v] / 2;
601
+ const rowOf = (y) => Math.round((y - bottom) * 1e4);
602
+ const rows = /* @__PURE__ */ new Map();
603
+ for (let i = 0; i < cells.length; i++) {
604
+ if (!standing(i)) continue;
605
+ const cell = cells[i];
606
+ const key = rowOf(cell.center[v] - cell.size[v] / 2);
607
+ const row = rows.get(key);
608
+ if (row) row.push(i);
609
+ else rows.set(key, [i]);
610
+ }
611
+ const held = /* @__PURE__ */ new Set();
612
+ const queue = [];
613
+ for (const i of rows.get(0) ?? []) {
614
+ held.add(i);
615
+ queue.push(i);
616
+ }
617
+ 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);
618
+ const climb = () => {
619
+ while (queue.length > 0) {
620
+ const cell = cells[queue.pop()];
621
+ for (const j of rows.get(rowOf(cell.center[v] + cell.size[v] / 2)) ?? []) {
622
+ const other = cells[j];
623
+ if (held.has(j) || overlap(cell, other) <= Math.max(JOINT_GAP, MIN_BEARING_SHARE * Math.min(cell.size[u], other.size[u]))) continue;
624
+ held.add(j);
625
+ queue.push(j);
626
+ }
627
+ }
628
+ };
629
+ for (const row of rows.values()) row.sort((a, b) => cells[a].center[u] - cells[b].center[u]);
630
+ const touching = (a, b) => Math.abs(a.center[u] + a.size[u] / 2 - (b.center[u] - b.size[u] / 2)) <= 1e-3;
631
+ for (let wedged = true; wedged; ) {
632
+ climb();
633
+ wedged = false;
634
+ for (const row of rows.values()) {
635
+ for (let k = 1; k < row.length - 1; k++) {
636
+ const i = row[k];
637
+ if (held.has(i) || !held.has(row[k - 1]) || !held.has(row[k + 1])) continue;
638
+ if (!touching(cells[row[k - 1]], cells[i]) || !touching(cells[i], cells[row[k + 1]])) continue;
639
+ held.add(i);
640
+ queue.push(i);
641
+ wedged = true;
642
+ }
643
+ }
644
+ }
645
+ const loose = [];
646
+ for (const row of rows.values()) for (const i of row) if (!held.has(i)) loose.push(i);
647
+ return loose;
648
+ };
649
+ var clumpCells = (cells, thin, cap) => {
650
+ if (cells.length <= cap) return [...cells];
651
+ const [u, v] = planeAxes(thin);
652
+ let lo0 = Infinity, lo1 = Infinity, hi0 = -Infinity, hi1 = -Infinity;
653
+ for (const cell of cells) {
654
+ lo0 = Math.min(lo0, cell.center[u] - cell.size[u] / 2);
655
+ hi0 = Math.max(hi0, cell.center[u] + cell.size[u] / 2);
656
+ lo1 = Math.min(lo1, cell.center[v] - cell.size[v] / 2);
657
+ hi1 = Math.max(hi1, cell.center[v] + cell.size[v] / 2);
658
+ }
659
+ let side = Math.sqrt((hi0 - lo0) * (hi1 - lo1) / cap);
660
+ for (; ; ) {
661
+ const bins = /* @__PURE__ */ new Map();
662
+ for (const cell of cells) {
663
+ const key = `${Math.floor((cell.center[u] - lo0) / side)}:${Math.floor((cell.center[v] - lo1) / side)}`;
664
+ const bin = bins.get(key) ?? { lo: [Infinity, Infinity, Infinity], hi: [-Infinity, -Infinity, -Infinity] };
665
+ for (let k = 0; k < 3; k++) {
666
+ bin.lo[k] = Math.min(bin.lo[k], cell.center[k] - cell.size[k] / 2);
667
+ bin.hi[k] = Math.max(bin.hi[k], cell.center[k] + cell.size[k] / 2);
668
+ }
669
+ bins.set(key, bin);
670
+ }
671
+ if (bins.size <= cap) {
672
+ 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]] }));
673
+ }
674
+ side *= 1.25;
675
+ }
676
+ };
523
677
 
524
678
  // src/layers.ts
525
679
  var MAX_SPALLS = 64;
@@ -1560,6 +1714,7 @@ var materials_default = [
1560
1714
  },
1561
1715
  {
1562
1716
  id: "wood_soft",
1717
+ splitForm: "slivers",
1563
1718
  density: 500,
1564
1719
  hpPerKg: 90,
1565
1720
  penetration: 9,
@@ -1605,6 +1760,7 @@ var materials_default = [
1605
1760
  },
1606
1761
  {
1607
1762
  id: "wood_hard",
1763
+ splitForm: "slivers",
1608
1764
  density: 750,
1609
1765
  hpPerKg: 140,
1610
1766
  penetration: 14,
@@ -1649,6 +1805,7 @@ var materials_default = [
1649
1805
  },
1650
1806
  {
1651
1807
  id: "plywood",
1808
+ splitForm: "plies",
1652
1809
  density: 600,
1653
1810
  hpPerKg: 70,
1654
1811
  penetration: 7,
@@ -1696,6 +1853,7 @@ var materials_default = [
1696
1853
  },
1697
1854
  {
1698
1855
  id: "mdf",
1856
+ splitForm: "granules",
1699
1857
  density: 750,
1700
1858
  hpPerKg: 45,
1701
1859
  penetration: 6,
@@ -1862,6 +2020,7 @@ var materials_default = [
1862
2020
  floor: 60
1863
2021
  }
1864
2022
  },
2023
+ ricochetDeg: 12,
1865
2024
  hitDebris: "chips",
1866
2025
  breakDebris: "dust",
1867
2026
  visual: { innerColor: 10197652 },
@@ -1893,6 +2052,7 @@ var materials_default = [
1893
2052
  0.04,
1894
2053
  0.12
1895
2054
  ],
2055
+ ricochetDeg: 10,
1896
2056
  hitDebris: "chips",
1897
2057
  breakDebris: "dust",
1898
2058
  visual: { innerColor: 12085567 },
@@ -1950,6 +2110,7 @@ var materials_default = [
1950
2110
  0.06,
1951
2111
  0.3
1952
2112
  ],
2113
+ ricochetDeg: 15,
1953
2114
  hitDebris: "chips",
1954
2115
  breakDebris: "dust",
1955
2116
  visual: { innerColor: 9407364 },
@@ -2207,6 +2368,7 @@ var materials_default = [
2207
2368
  },
2208
2369
  {
2209
2370
  id: "green_wood",
2371
+ splitForm: "slivers",
2210
2372
  density: 650,
2211
2373
  hpPerKg: 5,
2212
2374
  penetration: 10,
@@ -2377,6 +2539,7 @@ var materials_default = [
2377
2539
  0.03,
2378
2540
  0.15
2379
2541
  ],
2542
+ ricochetDeg: 8,
2380
2543
  hitDebris: "chips",
2381
2544
  breakDebris: "dust",
2382
2545
  thermal: {
@@ -2476,6 +2639,7 @@ var materials_default = [
2476
2639
  0.02,
2477
2640
  0.08
2478
2641
  ],
2642
+ ricochetDeg: 8,
2479
2643
  hitDebris: "chips",
2480
2644
  breakDebris: "dust",
2481
2645
  thermal: {
@@ -2487,6 +2651,127 @@ var materials_default = [
2487
2651
  "brittle"
2488
2652
  ],
2489
2653
  compressive: 1e5
2654
+ },
2655
+ {
2656
+ id: "mud",
2657
+ density: 1700,
2658
+ hpPerKg: 1,
2659
+ penetration: 5,
2660
+ ductility: 0.8,
2661
+ fracture: "crumble",
2662
+ fragmentSize: [
2663
+ 0.02,
2664
+ 0.08
2665
+ ],
2666
+ hitDebris: "dust",
2667
+ breakDebris: "dust",
2668
+ thermal: {
2669
+ conductivity: 0.5,
2670
+ heatCapacity: 3.2
2671
+ },
2672
+ tags: [
2673
+ "soil",
2674
+ "wet"
2675
+ ],
2676
+ compressive: 5e3
2677
+ },
2678
+ {
2679
+ id: "clay",
2680
+ density: 1800,
2681
+ hpPerKg: 5,
2682
+ penetration: 7,
2683
+ ductility: 0.05,
2684
+ fracture: "crumble",
2685
+ fragmentSize: [
2686
+ 0.02,
2687
+ 0.1
2688
+ ],
2689
+ hitDebris: "chips",
2690
+ breakDebris: "dust",
2691
+ thermal: {
2692
+ conductivity: 0.15,
2693
+ heatCapacity: 2.2
2694
+ },
2695
+ tags: [
2696
+ "soil",
2697
+ "brittle"
2698
+ ],
2699
+ compressive: 6e4
2700
+ },
2701
+ {
2702
+ id: "snow",
2703
+ density: 300,
2704
+ hpPerKg: 1,
2705
+ penetration: 3,
2706
+ ductility: 0.6,
2707
+ fracture: "crumble",
2708
+ fragmentSize: [
2709
+ 0.01,
2710
+ 0.05
2711
+ ],
2712
+ hitDebris: "dust",
2713
+ breakDebris: "dust",
2714
+ thermal: {
2715
+ conductivity: 0.1,
2716
+ heatCapacity: 2.1
2717
+ },
2718
+ tags: [
2719
+ "soil",
2720
+ "frozen"
2721
+ ],
2722
+ compressive: 1e3
2723
+ },
2724
+ {
2725
+ id: "cobble",
2726
+ density: 2650,
2727
+ hpPerKg: 25,
2728
+ penetration: 25,
2729
+ penetrationClass: "concrete",
2730
+ penetrationScale: 0.9,
2731
+ ductility: 0.03,
2732
+ fracture: "crumble",
2733
+ fragmentSize: [
2734
+ 0.05,
2735
+ 0.15
2736
+ ],
2737
+ ricochetDeg: 15,
2738
+ hitDebris: "chips",
2739
+ breakDebris: "chips",
2740
+ thermal: {
2741
+ conductivity: 0.2,
2742
+ heatCapacity: 2.5
2743
+ },
2744
+ tags: [
2745
+ "masonry",
2746
+ "brittle"
2747
+ ],
2748
+ compressive: 3e5
2749
+ },
2750
+ {
2751
+ id: "paver",
2752
+ density: 2300,
2753
+ hpPerKg: 14,
2754
+ penetration: 18,
2755
+ penetrationClass: "concrete",
2756
+ penetrationScale: 1,
2757
+ ductility: 0.03,
2758
+ fracture: "crumble",
2759
+ fragmentSize: [
2760
+ 0.04,
2761
+ 0.12
2762
+ ],
2763
+ ricochetDeg: 12,
2764
+ hitDebris: "chips",
2765
+ breakDebris: "chips",
2766
+ thermal: {
2767
+ conductivity: 0.15,
2768
+ heatCapacity: 2.5
2769
+ },
2770
+ tags: [
2771
+ "masonry",
2772
+ "brittle"
2773
+ ],
2774
+ compressive: 35e4
2490
2775
  }
2491
2776
  ];
2492
2777
 
@@ -2711,6 +2996,11 @@ var BREACH_RADIUS = 0.55;
2711
2996
  var BREACH_BLAST_FACTOR = 6;
2712
2997
  var BREACH_AREA = 0.25;
2713
2998
  var BREACH_BLAST_REACH = 0.12;
2999
+ var MEMBER_CUT_REACH = 0.5;
3000
+ var BLAST_REFERENCE_RESISTANCE = 200 * 1900 * 0.13;
3001
+ var BLAST_EDGE_RAGGED = 0.3;
3002
+ var BLAST_EDGE_LOOSE = 1.35;
3003
+ var BLAST_EDGE_WEAR = 0.6;
2714
3004
  var RESIDUAL_CAPACITY = 0.15;
2715
3005
  var SEVER_BAND = 0.1;
2716
3006
  var SEVER_ASPECT = 3;
@@ -3963,7 +4253,7 @@ var createDestruction = (options = {}) => {
3963
4253
  if (j.st.open) continue;
3964
4254
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
3965
4255
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
3966
- j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
4256
+ j.st.hp -= span === void 0 ? amount : Math.min(amount, j.st.maxHp) * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
3967
4257
  if (j.st.hp <= 0) {
3968
4258
  j.st.hp = 0;
3969
4259
  j.st.open = true;
@@ -4091,6 +4381,7 @@ var createDestruction = (options = {}) => {
4091
4381
  const worn = (_a = p.rt).chips ?? (_a.chips = {});
4092
4382
  const cut = (_b = p.rt).craters ?? (_b.craters = {});
4093
4383
  const out = [];
4384
+ const wholeBefore = Object.values(worn).filter((w) => w === -1).length;
4094
4385
  const shot = kind === "ballistic";
4095
4386
  for (let i = 0; i < grid.length; i++) {
4096
4387
  const cell = grid[i];
@@ -4149,6 +4440,7 @@ var createDestruction = (options = {}) => {
4149
4440
  emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
4150
4441
  debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
4151
4442
  }
4443
+ if (Object.values(worn).filter((w) => w === -1).length > wholeBefore) dropUnsupported(o, p, src);
4152
4444
  if (p.spec.reinforcement) exposeRebar(o, p, src);
4153
4445
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
4154
4446
  if (p.structural && !isLost(p)) {
@@ -4252,10 +4544,122 @@ var createDestruction = (options = {}) => {
4252
4544
  setStage(o, p, 1, {}, src);
4253
4545
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
4254
4546
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
4547
+ dropUnsupported(o, p, src);
4255
4548
  const reinforcement = p.spec.reinforcement;
4256
4549
  if (reinforcement) exposeRebar(o, p, src);
4257
4550
  if (p.structural) solveStructure(o);
4258
4551
  };
4552
+ const blastReachOf = (p, energy) => {
4553
+ const perKg = p.mat.hits?.blunt?.unitJoules ?? chipEnergy;
4554
+ const resistance = Math.max(1, perKg * p.mat.density * p.thickness);
4555
+ return BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3) * clamp2(Math.cbrt(BLAST_REFERENCE_RESISTANCE / resistance), 0.3, 2);
4556
+ };
4557
+ const blastUnits = (o, p, wp, energy, dist, src) => {
4558
+ var _a, _b;
4559
+ if (!p.staged || p.st.stripped || isLost(p) || chipEnergy <= 0) return false;
4560
+ const reach = blastReachOf(p, energy);
4561
+ if (dist >= reach) return false;
4562
+ const radius = Math.sqrt(reach * reach - dist * dist);
4563
+ const [u, v] = planeAxes(p.thin);
4564
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
4565
+ const op = p.st.opening ? toLocal(p.pos, p.q, p.st.opening.center) : null;
4566
+ const grid = gridOf(p);
4567
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
4568
+ const perKg = p.mat.hits?.blunt?.unitJoules ?? chipEnergy;
4569
+ const lobes = [rnd() * 2 * Math.PI, rnd() * 2 * Math.PI];
4570
+ const out = [];
4571
+ for (let i = 0; i < grid.length; i++) {
4572
+ const cell = grid[i];
4573
+ if (worn[i] === -1) continue;
4574
+ if (op && p.st.opening && Math.hypot(cell.center[u] - op[u], cell.center[v] - op[v]) <= p.st.opening.radius) continue;
4575
+ const du = cell.center[u] - lp[u], dv = cell.center[v] - lp[v];
4576
+ const angle = Math.atan2(dv, du);
4577
+ 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)));
4578
+ const d = Math.hypot(du, dv);
4579
+ if (d <= edge) {
4580
+ worn[i] = -1;
4581
+ out.push(i);
4582
+ } else if (d <= edge * BLAST_EDGE_LOOSE) {
4583
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
4584
+ worn[i] = Math.max(worn[i] ?? 0, perKg * mass * BLAST_EDGE_WEAR);
4585
+ }
4586
+ }
4587
+ if (out.length === 0) return false;
4588
+ 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));
4589
+ for (const i of loose) worn[i] = -1;
4590
+ const blown = out.map((i) => grid[i]), fallen = loose.map((i) => grid[i]);
4591
+ const volume = (cells2) => cells2.reduce((sum, cell) => sum + cell.size[0] * cell.size[1] * cell.size[2], 0);
4592
+ const blownMass = Math.min(p.st.mass, p.mat.density * volume(blown));
4593
+ const fallenMass = Math.min(p.st.mass - blownMass, p.mat.density * volume(fallen));
4594
+ const speed = clamp2(Math.sqrt(2 * energy * 0.02 / Math.max(blownMass, 0.05)), 1, 12);
4595
+ const { c, q } = partXf(o, p);
4596
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
4597
+ const tag = `x${chipped.length}_`;
4598
+ const fragments = [
4599
+ ...cellFragments(o, p, clumpCells(blown, p.thin, maxFragments), wp, null, speed, blownMass, tag, true),
4600
+ ...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) : []
4601
+ ];
4602
+ const cells = [...blown, ...fallen];
4603
+ const holes = localHolesOf(p);
4604
+ p.st.holes = p.st.holes.filter((_, k) => !cells.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length > 0));
4605
+ const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
4606
+ for (const cell of cells) {
4607
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
4608
+ for (let k = 0; k < 3; k++) {
4609
+ lo[k] = Math.min(lo[k], cell.center[k] - cell.size[k] / 2);
4610
+ hi[k] = Math.max(hi[k], cell.center[k] + cell.size[k] / 2);
4611
+ }
4612
+ }
4613
+ p.st.mass = Math.max(0, p.st.mass - blownMass - fallenMass);
4614
+ p.rt.removed = (p.rt.removed ?? 0) + blownMass + fallenMass;
4615
+ markDamaged(p);
4616
+ setStage(o, p, 1, {}, src);
4617
+ const center = toWorld(c, q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
4618
+ const units = cells.map((cell) => ({ center: toWorld(c, q, cell.center), size: [cell.size[0], cell.size[1], cell.size[2]] }));
4619
+ 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 });
4620
+ debris(o, p, p.mat.breakDebris, center, vec.normalize(vec.sub(center, wp), [0, 1, 0]), 14, 3, src);
4621
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
4622
+ if (p.structural && !isLost(p)) {
4623
+ solveStructure(o);
4624
+ collapseCheck(o, src);
4625
+ }
4626
+ return true;
4627
+ };
4628
+ const dropUnsupported = (o, p, src) => {
4629
+ var _a, _b;
4630
+ if (!p.spec.units || p.spec.units.mortar === p.spec.material || p.st.stripped || isLost(p)) return 0;
4631
+ const [u, v] = planeAxes(p.thin);
4632
+ const grid = gridOf(p);
4633
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
4634
+ const op = p.st.opening ? toLocal(p.pos, p.q, p.st.opening.center) : null;
4635
+ const opened = (cell) => !!op && !!p.st.opening && Math.hypot(cell.center[u] - op[u], cell.center[v] - op[v]) <= p.st.opening.radius;
4636
+ const loose = unsupportedCells(grid, p.thin, p.dims, (i) => worn[i] !== -1 && !opened(grid[i]));
4637
+ if (loose.length === 0) return 0;
4638
+ for (const i of loose) worn[i] = -1;
4639
+ const fallen = loose.map((i) => grid[i]);
4640
+ 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));
4641
+ const { c, q } = partXf(o, p);
4642
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
4643
+ const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
4644
+ for (const cell of fallen) {
4645
+ for (let k = 0; k < 3; k++) {
4646
+ lo[k] = Math.min(lo[k], cell.center[k] - cell.size[k] / 2);
4647
+ hi[k] = Math.max(hi[k], cell.center[k] + cell.size[k] / 2);
4648
+ }
4649
+ }
4650
+ const center = toWorld(c, q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
4651
+ 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);
4652
+ const holes = localHolesOf(p);
4653
+ p.st.holes = p.st.holes.filter((_, k) => !fallen.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length > 0));
4654
+ 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]] });
4655
+ p.st.mass = Math.max(0, p.st.mass - mass);
4656
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
4657
+ markDamaged(p);
4658
+ const units = fallen.map((cell) => ({ center: toWorld(c, q, cell.center), size: [cell.size[0], cell.size[1], cell.size[2]] }));
4659
+ 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 });
4660
+ debris(o, p, p.mat.breakDebris, center, [0, -1, 0], 6, 1, src);
4661
+ return loose.length;
4662
+ };
4259
4663
  const stripPart = (o, p, reinforcement, src) => {
4260
4664
  const bars = rebarBars(p.dims, p.thin, reinforcement.spacing);
4261
4665
  p.st.stripped = true;
@@ -4322,6 +4726,7 @@ var createDestruction = (options = {}) => {
4322
4726
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
4323
4727
  const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
4324
4728
  const cutAt = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
4729
+ const planRnd = p.mat.splitForm ? splitStream(o.id, p.spec.id, hitLocal) : rnd;
4325
4730
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
4326
4731
  style,
4327
4732
  dims: p.dims,
@@ -4330,8 +4735,9 @@ var createDestruction = (options = {}) => {
4330
4735
  hit: hitLocal,
4331
4736
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
4332
4737
  maxFragments,
4333
- rnd,
4738
+ rnd: planRnd,
4334
4739
  sheet: p.mat.sheet,
4740
+ ...p.mat.splitForm ? { form: p.mat.splitForm } : {},
4335
4741
  ...cutAt !== void 0 ? { cut: { at: cutAt, gap: SEVER_BAND } } : {}
4336
4742
  });
4337
4743
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
@@ -4470,7 +4876,8 @@ var createDestruction = (options = {}) => {
4470
4876
  const fragments = [];
4471
4877
  for (let i = 0; i < n; i++) {
4472
4878
  const back = !exit || i % 3 === 2;
4473
- 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());
4879
+ const plies = p.mat.splitForm === "plies";
4880
+ 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());
4474
4881
  const size = [w, w, w];
4475
4882
  size[along] = len;
4476
4883
  size[(along + 1) % 3] = w;
@@ -4631,7 +5038,7 @@ var createDestruction = (options = {}) => {
4631
5038
  checkIgnite(o, p, src);
4632
5039
  }
4633
5040
  }
4634
- wearJoints(o, p, Ein * 0.5, void 0, 0, src);
5041
+ wearJoints(o, p, Ein * 0.5, void 0, 0, src, false, 2 * BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3));
4635
5042
  const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
4636
5043
  if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
4637
5044
  frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
@@ -4641,7 +5048,10 @@ var createDestruction = (options = {}) => {
4641
5048
  const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
4642
5049
  res.damage += dealt;
4643
5050
  if (p.bend && flexure.enabled) bendByBlast(o, p, point, energy, radius, reach, src);
4644
- if (dist <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
5051
+ const breachReach = BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3);
5052
+ if (p.spec.units && p.spec.units.mortar !== p.spec.material) blastUnits(o, p, wp, energy, dist, src);
5053
+ else if (p.spec.reinforcement && p.spec.units && dist > MEMBER_CUT_REACH * breachReach) blastUnits(o, p, wp, energy, dist, src);
5054
+ else if (dist <= breachReach) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
4645
5055
  return reach;
4646
5056
  };
4647
5057
  const blastProbe = { partsTested: 0, partsNear: 0 };
@@ -4887,6 +5297,11 @@ var createDestruction = (options = {}) => {
4887
5297
  }
4888
5298
  return h | 0;
4889
5299
  };
5300
+ const splitStream = (objectId, partId, hit) => {
5301
+ const codes = Array.from(`${objectId}|${partId}`, (char) => char.charCodeAt(0) / 1e3);
5302
+ const stream = createRng(seedOf(rng.state() / 1e3, ...codes, hit[0], hit[1], hit[2]));
5303
+ return () => stream.next();
5304
+ };
4890
5305
  const throwFragment = (frag, objectId, from, g, centre, index, src, shields, results, reports, mult) => {
4891
5306
  const v = frag.velocity;
4892
5307
  const speed = Math.hypot(v[0], v[1], v[2]);
@@ -6151,7 +6566,12 @@ var locales = {
6151
6566
  sand: "sand",
6152
6567
  gravel: "gravel",
6153
6568
  turf: "turf",
6154
- mortar: "mortar"
6569
+ mortar: "mortar",
6570
+ mud: "mud",
6571
+ clay: "dry clay",
6572
+ snow: "snow",
6573
+ cobble: "cobblestone",
6574
+ paver: "concrete paver"
6155
6575
  },
6156
6576
  partStatus: { intact: "intact", damaged: "damaged", collapsed: "collapsed", broken: "broken", melted: "melted", ashes: "ashes" },
6157
6577
  jointKinds: {
@@ -6247,7 +6667,12 @@ var locales = {
6247
6667
  sand: "areia",
6248
6668
  gravel: "brita",
6249
6669
  turf: "grama",
6250
- mortar: "argamassa"
6670
+ mortar: "argamassa",
6671
+ mud: "lama",
6672
+ clay: "barro seco",
6673
+ snow: "neve",
6674
+ cobble: "paralelep\xEDpedo",
6675
+ paver: "bloquete"
6251
6676
  },
6252
6677
  partStatus: { intact: "intacto", damaged: "danificado", collapsed: "desabado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
6253
6678
  jointKinds: {