@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.
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,
@@ -2207,6 +2365,7 @@ var materials_default = [
2207
2365
  },
2208
2366
  {
2209
2367
  id: "green_wood",
2368
+ splitForm: "slivers",
2210
2369
  density: 650,
2211
2370
  hpPerKg: 5,
2212
2371
  penetration: 10,
@@ -2487,6 +2646,125 @@ var materials_default = [
2487
2646
  "brittle"
2488
2647
  ],
2489
2648
  compressive: 1e5
2649
+ },
2650
+ {
2651
+ id: "mud",
2652
+ density: 1700,
2653
+ hpPerKg: 1,
2654
+ penetration: 5,
2655
+ ductility: 0.8,
2656
+ fracture: "crumble",
2657
+ fragmentSize: [
2658
+ 0.02,
2659
+ 0.08
2660
+ ],
2661
+ hitDebris: "dust",
2662
+ breakDebris: "dust",
2663
+ thermal: {
2664
+ conductivity: 0.5,
2665
+ heatCapacity: 3.2
2666
+ },
2667
+ tags: [
2668
+ "soil",
2669
+ "wet"
2670
+ ],
2671
+ compressive: 5e3
2672
+ },
2673
+ {
2674
+ id: "clay",
2675
+ density: 1800,
2676
+ hpPerKg: 5,
2677
+ penetration: 7,
2678
+ ductility: 0.05,
2679
+ fracture: "crumble",
2680
+ fragmentSize: [
2681
+ 0.02,
2682
+ 0.1
2683
+ ],
2684
+ hitDebris: "chips",
2685
+ breakDebris: "dust",
2686
+ thermal: {
2687
+ conductivity: 0.15,
2688
+ heatCapacity: 2.2
2689
+ },
2690
+ tags: [
2691
+ "soil",
2692
+ "brittle"
2693
+ ],
2694
+ compressive: 6e4
2695
+ },
2696
+ {
2697
+ id: "snow",
2698
+ density: 300,
2699
+ hpPerKg: 1,
2700
+ penetration: 3,
2701
+ ductility: 0.6,
2702
+ fracture: "crumble",
2703
+ fragmentSize: [
2704
+ 0.01,
2705
+ 0.05
2706
+ ],
2707
+ hitDebris: "dust",
2708
+ breakDebris: "dust",
2709
+ thermal: {
2710
+ conductivity: 0.1,
2711
+ heatCapacity: 2.1
2712
+ },
2713
+ tags: [
2714
+ "soil",
2715
+ "frozen"
2716
+ ],
2717
+ compressive: 1e3
2718
+ },
2719
+ {
2720
+ id: "cobble",
2721
+ density: 2650,
2722
+ hpPerKg: 25,
2723
+ penetration: 25,
2724
+ penetrationClass: "concrete",
2725
+ penetrationScale: 0.9,
2726
+ ductility: 0.03,
2727
+ fracture: "crumble",
2728
+ fragmentSize: [
2729
+ 0.05,
2730
+ 0.15
2731
+ ],
2732
+ hitDebris: "chips",
2733
+ breakDebris: "chips",
2734
+ thermal: {
2735
+ conductivity: 0.2,
2736
+ heatCapacity: 2.5
2737
+ },
2738
+ tags: [
2739
+ "masonry",
2740
+ "brittle"
2741
+ ],
2742
+ compressive: 3e5
2743
+ },
2744
+ {
2745
+ id: "paver",
2746
+ density: 2300,
2747
+ hpPerKg: 14,
2748
+ penetration: 18,
2749
+ penetrationClass: "concrete",
2750
+ penetrationScale: 1,
2751
+ ductility: 0.03,
2752
+ fracture: "crumble",
2753
+ fragmentSize: [
2754
+ 0.04,
2755
+ 0.12
2756
+ ],
2757
+ hitDebris: "chips",
2758
+ breakDebris: "chips",
2759
+ thermal: {
2760
+ conductivity: 0.15,
2761
+ heatCapacity: 2.5
2762
+ },
2763
+ tags: [
2764
+ "masonry",
2765
+ "brittle"
2766
+ ],
2767
+ compressive: 35e4
2490
2768
  }
2491
2769
  ];
2492
2770
 
@@ -2711,6 +2989,11 @@ var BREACH_RADIUS = 0.55;
2711
2989
  var BREACH_BLAST_FACTOR = 6;
2712
2990
  var BREACH_AREA = 0.25;
2713
2991
  var BREACH_BLAST_REACH = 0.12;
2992
+ var MEMBER_CUT_REACH = 0.5;
2993
+ var BLAST_REFERENCE_RESISTANCE = 200 * 1900 * 0.13;
2994
+ var BLAST_EDGE_RAGGED = 0.3;
2995
+ var BLAST_EDGE_LOOSE = 1.35;
2996
+ var BLAST_EDGE_WEAR = 0.6;
2714
2997
  var RESIDUAL_CAPACITY = 0.15;
2715
2998
  var SEVER_BAND = 0.1;
2716
2999
  var SEVER_ASPECT = 3;
@@ -3963,7 +4246,7 @@ var createDestruction = (options = {}) => {
3963
4246
  if (j.st.open) continue;
3964
4247
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
3965
4248
  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));
4249
+ 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
4250
  if (j.st.hp <= 0) {
3968
4251
  j.st.hp = 0;
3969
4252
  j.st.open = true;
@@ -4091,6 +4374,7 @@ var createDestruction = (options = {}) => {
4091
4374
  const worn = (_a = p.rt).chips ?? (_a.chips = {});
4092
4375
  const cut = (_b = p.rt).craters ?? (_b.craters = {});
4093
4376
  const out = [];
4377
+ const wholeBefore = Object.values(worn).filter((w) => w === -1).length;
4094
4378
  const shot = kind === "ballistic";
4095
4379
  for (let i = 0; i < grid.length; i++) {
4096
4380
  const cell = grid[i];
@@ -4149,6 +4433,7 @@ var createDestruction = (options = {}) => {
4149
4433
  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
4434
  debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
4151
4435
  }
4436
+ if (Object.values(worn).filter((w) => w === -1).length > wholeBefore) dropUnsupported(o, p, src);
4152
4437
  if (p.spec.reinforcement) exposeRebar(o, p, src);
4153
4438
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
4154
4439
  if (p.structural && !isLost(p)) {
@@ -4252,10 +4537,122 @@ var createDestruction = (options = {}) => {
4252
4537
  setStage(o, p, 1, {}, src);
4253
4538
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
4254
4539
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
4540
+ dropUnsupported(o, p, src);
4255
4541
  const reinforcement = p.spec.reinforcement;
4256
4542
  if (reinforcement) exposeRebar(o, p, src);
4257
4543
  if (p.structural) solveStructure(o);
4258
4544
  };
4545
+ const blastReachOf = (p, energy) => {
4546
+ const perKg = p.mat.hits?.blunt?.unitJoules ?? chipEnergy;
4547
+ const resistance = Math.max(1, perKg * p.mat.density * p.thickness);
4548
+ return BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3) * clamp2(Math.cbrt(BLAST_REFERENCE_RESISTANCE / resistance), 0.3, 2);
4549
+ };
4550
+ const blastUnits = (o, p, wp, energy, dist, src) => {
4551
+ var _a, _b;
4552
+ if (!p.staged || p.st.stripped || isLost(p) || chipEnergy <= 0) return false;
4553
+ const reach = blastReachOf(p, energy);
4554
+ if (dist >= reach) return false;
4555
+ const radius = Math.sqrt(reach * reach - dist * dist);
4556
+ const [u, v] = planeAxes(p.thin);
4557
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
4558
+ const op = p.st.opening ? toLocal(p.pos, p.q, p.st.opening.center) : null;
4559
+ const grid = gridOf(p);
4560
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
4561
+ const perKg = p.mat.hits?.blunt?.unitJoules ?? chipEnergy;
4562
+ const lobes = [rnd() * 2 * Math.PI, rnd() * 2 * Math.PI];
4563
+ const out = [];
4564
+ for (let i = 0; i < grid.length; i++) {
4565
+ const cell = grid[i];
4566
+ if (worn[i] === -1) continue;
4567
+ if (op && p.st.opening && Math.hypot(cell.center[u] - op[u], cell.center[v] - op[v]) <= p.st.opening.radius) continue;
4568
+ const du = cell.center[u] - lp[u], dv = cell.center[v] - lp[v];
4569
+ const angle = Math.atan2(dv, du);
4570
+ 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)));
4571
+ const d = Math.hypot(du, dv);
4572
+ if (d <= edge) {
4573
+ worn[i] = -1;
4574
+ out.push(i);
4575
+ } else if (d <= edge * BLAST_EDGE_LOOSE) {
4576
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
4577
+ worn[i] = Math.max(worn[i] ?? 0, perKg * mass * BLAST_EDGE_WEAR);
4578
+ }
4579
+ }
4580
+ if (out.length === 0) return false;
4581
+ 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));
4582
+ for (const i of loose) worn[i] = -1;
4583
+ const blown = out.map((i) => grid[i]), fallen = loose.map((i) => grid[i]);
4584
+ const volume = (cells2) => cells2.reduce((sum, cell) => sum + cell.size[0] * cell.size[1] * cell.size[2], 0);
4585
+ const blownMass = Math.min(p.st.mass, p.mat.density * volume(blown));
4586
+ const fallenMass = Math.min(p.st.mass - blownMass, p.mat.density * volume(fallen));
4587
+ const speed = clamp2(Math.sqrt(2 * energy * 0.02 / Math.max(blownMass, 0.05)), 1, 12);
4588
+ const { c, q } = partXf(o, p);
4589
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
4590
+ const tag = `x${chipped.length}_`;
4591
+ const fragments = [
4592
+ ...cellFragments(o, p, clumpCells(blown, p.thin, maxFragments), wp, null, speed, blownMass, tag, true),
4593
+ ...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) : []
4594
+ ];
4595
+ const cells = [...blown, ...fallen];
4596
+ const holes = localHolesOf(p);
4597
+ p.st.holes = p.st.holes.filter((_, k) => !cells.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length > 0));
4598
+ const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
4599
+ for (const cell of cells) {
4600
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
4601
+ for (let k = 0; k < 3; k++) {
4602
+ lo[k] = Math.min(lo[k], cell.center[k] - cell.size[k] / 2);
4603
+ hi[k] = Math.max(hi[k], cell.center[k] + cell.size[k] / 2);
4604
+ }
4605
+ }
4606
+ p.st.mass = Math.max(0, p.st.mass - blownMass - fallenMass);
4607
+ p.rt.removed = (p.rt.removed ?? 0) + blownMass + fallenMass;
4608
+ markDamaged(p);
4609
+ setStage(o, p, 1, {}, src);
4610
+ const center = toWorld(c, q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
4611
+ const units = cells.map((cell) => ({ center: toWorld(c, q, cell.center), size: [cell.size[0], cell.size[1], cell.size[2]] }));
4612
+ 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 });
4613
+ debris(o, p, p.mat.breakDebris, center, vec.normalize(vec.sub(center, wp), [0, 1, 0]), 14, 3, src);
4614
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
4615
+ if (p.structural && !isLost(p)) {
4616
+ solveStructure(o);
4617
+ collapseCheck(o, src);
4618
+ }
4619
+ return true;
4620
+ };
4621
+ const dropUnsupported = (o, p, src) => {
4622
+ var _a, _b;
4623
+ if (!p.spec.units || p.spec.units.mortar === p.spec.material || p.st.stripped || isLost(p)) return 0;
4624
+ const [u, v] = planeAxes(p.thin);
4625
+ const grid = gridOf(p);
4626
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
4627
+ const op = p.st.opening ? toLocal(p.pos, p.q, p.st.opening.center) : null;
4628
+ const opened = (cell) => !!op && !!p.st.opening && Math.hypot(cell.center[u] - op[u], cell.center[v] - op[v]) <= p.st.opening.radius;
4629
+ const loose = unsupportedCells(grid, p.thin, p.dims, (i) => worn[i] !== -1 && !opened(grid[i]));
4630
+ if (loose.length === 0) return 0;
4631
+ for (const i of loose) worn[i] = -1;
4632
+ const fallen = loose.map((i) => grid[i]);
4633
+ 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));
4634
+ const { c, q } = partXf(o, p);
4635
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
4636
+ const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
4637
+ for (const cell of fallen) {
4638
+ for (let k = 0; k < 3; k++) {
4639
+ lo[k] = Math.min(lo[k], cell.center[k] - cell.size[k] / 2);
4640
+ hi[k] = Math.max(hi[k], cell.center[k] + cell.size[k] / 2);
4641
+ }
4642
+ }
4643
+ const center = toWorld(c, q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
4644
+ 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);
4645
+ const holes = localHolesOf(p);
4646
+ p.st.holes = p.st.holes.filter((_, k) => !fallen.some((cell) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length > 0));
4647
+ 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]] });
4648
+ p.st.mass = Math.max(0, p.st.mass - mass);
4649
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
4650
+ markDamaged(p);
4651
+ const units = fallen.map((cell) => ({ center: toWorld(c, q, cell.center), size: [cell.size[0], cell.size[1], cell.size[2]] }));
4652
+ 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 });
4653
+ debris(o, p, p.mat.breakDebris, center, [0, -1, 0], 6, 1, src);
4654
+ return loose.length;
4655
+ };
4259
4656
  const stripPart = (o, p, reinforcement, src) => {
4260
4657
  const bars = rebarBars(p.dims, p.thin, reinforcement.spacing);
4261
4658
  p.st.stripped = true;
@@ -4322,6 +4719,7 @@ var createDestruction = (options = {}) => {
4322
4719
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
4323
4720
  const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
4324
4721
  const cutAt = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
4722
+ const planRnd = p.mat.splitForm ? splitStream(o.id, p.spec.id, hitLocal) : rnd;
4325
4723
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
4326
4724
  style,
4327
4725
  dims: p.dims,
@@ -4330,8 +4728,9 @@ var createDestruction = (options = {}) => {
4330
4728
  hit: hitLocal,
4331
4729
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
4332
4730
  maxFragments,
4333
- rnd,
4731
+ rnd: planRnd,
4334
4732
  sheet: p.mat.sheet,
4733
+ ...p.mat.splitForm ? { form: p.mat.splitForm } : {},
4335
4734
  ...cutAt !== void 0 ? { cut: { at: cutAt, gap: SEVER_BAND } } : {}
4336
4735
  });
4337
4736
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
@@ -4376,7 +4775,7 @@ var createDestruction = (options = {}) => {
4376
4775
  return;
4377
4776
  }
4378
4777
  if (ex) {
4379
- 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 } : {} });
4778
+ 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 } : {} });
4380
4779
  blasts.push({ point: c, energy: ex.energy, radius: ex.radius, ...ex.lethalRadius !== void 0 ? { lethalRadius: ex.lethalRadius } : {}, source: src });
4381
4780
  }
4382
4781
  partLost(o, p, src);
@@ -4470,7 +4869,8 @@ var createDestruction = (options = {}) => {
4470
4869
  const fragments = [];
4471
4870
  for (let i = 0; i < n; i++) {
4472
4871
  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());
4872
+ const plies = p.mat.splitForm === "plies";
4873
+ 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
4874
  const size = [w, w, w];
4475
4875
  size[along] = len;
4476
4876
  size[(along + 1) % 3] = w;
@@ -4631,7 +5031,7 @@ var createDestruction = (options = {}) => {
4631
5031
  checkIgnite(o, p, src);
4632
5032
  }
4633
5033
  }
4634
- wearJoints(o, p, Ein * 0.5, void 0, 0, src);
5034
+ wearJoints(o, p, Ein * 0.5, void 0, 0, src, false, 2 * BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3));
4635
5035
  const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
4636
5036
  if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
4637
5037
  frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
@@ -4641,7 +5041,10 @@ var createDestruction = (options = {}) => {
4641
5041
  const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
4642
5042
  res.damage += dealt;
4643
5043
  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);
5044
+ const breachReach = BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3);
5045
+ if (p.spec.units && p.spec.units.mortar !== p.spec.material) blastUnits(o, p, wp, energy, dist, src);
5046
+ else if (p.spec.reinforcement && p.spec.units && dist > MEMBER_CUT_REACH * breachReach) blastUnits(o, p, wp, energy, dist, src);
5047
+ else if (dist <= breachReach) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
4645
5048
  return reach;
4646
5049
  };
4647
5050
  const blastProbe = { partsTested: 0, partsNear: 0 };
@@ -4887,6 +5290,11 @@ var createDestruction = (options = {}) => {
4887
5290
  }
4888
5291
  return h | 0;
4889
5292
  };
5293
+ const splitStream = (objectId, partId, hit) => {
5294
+ const codes = Array.from(`${objectId}|${partId}`, (char) => char.charCodeAt(0) / 1e3);
5295
+ const stream = createRng(seedOf(rng.state() / 1e3, ...codes, hit[0], hit[1], hit[2]));
5296
+ return () => stream.next();
5297
+ };
4890
5298
  const throwFragment = (frag, objectId, from, g, centre, index, src, shields, results, reports, mult) => {
4891
5299
  const v = frag.velocity;
4892
5300
  const speed = Math.hypot(v[0], v[1], v[2]);
@@ -6151,7 +6559,12 @@ var locales = {
6151
6559
  sand: "sand",
6152
6560
  gravel: "gravel",
6153
6561
  turf: "turf",
6154
- mortar: "mortar"
6562
+ mortar: "mortar",
6563
+ mud: "mud",
6564
+ clay: "dry clay",
6565
+ snow: "snow",
6566
+ cobble: "cobblestone",
6567
+ paver: "concrete paver"
6155
6568
  },
6156
6569
  partStatus: { intact: "intact", damaged: "damaged", collapsed: "collapsed", broken: "broken", melted: "melted", ashes: "ashes" },
6157
6570
  jointKinds: {
@@ -6247,7 +6660,12 @@ var locales = {
6247
6660
  sand: "areia",
6248
6661
  gravel: "brita",
6249
6662
  turf: "grama",
6250
- mortar: "argamassa"
6663
+ mortar: "argamassa",
6664
+ mud: "lama",
6665
+ clay: "barro seco",
6666
+ snow: "neve",
6667
+ cobble: "paralelep\xEDpedo",
6668
+ paver: "bloquete"
6251
6669
  },
6252
6670
  partStatus: { intact: "intacto", damaged: "danificado", collapsed: "desabado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
6253
6671
  jointKinds: {