@adatechnology/game-destruction 0.10.0 → 0.12.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.
@@ -486,7 +486,9 @@ var GameDestruction = (() => {
486
486
  const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
487
487
  band.c[L] = (lo + hi) / 2;
488
488
  band.s[L] = Math.max(hi - lo, 0.01);
489
- for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
489
+ if (inp.style === "snap") {
490
+ 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] });
491
+ } else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
490
492
  return out;
491
493
  };
492
494
  var tearStack = (inp, root, t, u, v, n, sheet) => {
@@ -517,6 +519,7 @@ var GameDestruction = (() => {
517
519
  const t = sorted[0];
518
520
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
519
521
  if (style === "snap") {
522
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
520
523
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
521
524
  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] });
522
525
  return out;
@@ -696,6 +699,27 @@ var GameDestruction = (() => {
696
699
  for (const x of spread(dims[u], COVER, spacing, MAX_BARS_PER_DIRECTION)) out.push(bar(v, dims[v] / 2 - 0.02, [[u, x], [thin, 0]]));
697
700
  return out;
698
701
  };
702
+ var clipBarsToBox = (bars, u, v, center, size) => {
703
+ const out = [];
704
+ for (const [a, b] of bars) {
705
+ const axis = a[u] !== b[u] ? u : v;
706
+ const other = axis === u ? v : u;
707
+ if (Math.abs(a[other] - center[other]) > size[other] / 2) continue;
708
+ const lo = Math.max(Math.min(a[axis], b[axis]), center[axis] - size[axis] / 2), hi = Math.min(Math.max(a[axis], b[axis]), center[axis] + size[axis] / 2);
709
+ if (hi - lo < 1e-6) continue;
710
+ const p = [a[0], a[1], a[2]], q = [b[0], b[1], b[2]];
711
+ p[axis] = lo;
712
+ q[axis] = hi;
713
+ out.push([p, q]);
714
+ }
715
+ return out;
716
+ };
717
+ var bendBar = ([a, b], thin, bow, i) => {
718
+ const length = Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
719
+ const m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2];
720
+ m[thin] += (i % 2 === 0 ? 1 : -1) * bow * length;
721
+ return [[a, m], [m, b]];
722
+ };
699
723
  var clipBarsToDisc = (bars, u, v, center, radius) => {
700
724
  const out = [];
701
725
  for (const [a, b] of bars) {
@@ -1673,14 +1697,21 @@ var GameDestruction = (() => {
1673
1697
  smoke: "grey"
1674
1698
  },
1675
1699
  visual: {
1676
- charColor: 1709072
1700
+ charColor: 1709072,
1701
+ innerColor: 14730131
1677
1702
  },
1678
1703
  tags: [
1679
1704
  "wood",
1680
1705
  "organic",
1681
1706
  "flammable"
1682
1707
  ],
1683
- compressive: 1e5
1708
+ compressive: 1e5,
1709
+ hits: {
1710
+ blunt: {
1711
+ tear: "splinters",
1712
+ tearJoules: 60
1713
+ }
1714
+ }
1684
1715
  },
1685
1716
  {
1686
1717
  id: "wood_hard",
@@ -1710,14 +1741,21 @@ var GameDestruction = (() => {
1710
1741
  smoke: "grey"
1711
1742
  },
1712
1743
  visual: {
1713
- charColor: 1709072
1744
+ charColor: 1709072,
1745
+ innerColor: 13213802
1714
1746
  },
1715
1747
  tags: [
1716
1748
  "wood",
1717
1749
  "organic",
1718
1750
  "flammable"
1719
1751
  ],
1720
- compressive: 15e4
1752
+ compressive: 15e4,
1753
+ hits: {
1754
+ blunt: {
1755
+ tear: "splinters",
1756
+ tearJoules: 150
1757
+ }
1758
+ }
1721
1759
  },
1722
1760
  {
1723
1761
  id: "plywood",
@@ -1747,14 +1785,24 @@ var GameDestruction = (() => {
1747
1785
  smoke: "grey"
1748
1786
  },
1749
1787
  visual: {
1750
- charColor: 1709072
1788
+ charColor: 1709072,
1789
+ innerColor: 14862234
1751
1790
  },
1752
1791
  tags: [
1753
1792
  "wood",
1754
1793
  "organic",
1755
1794
  "flammable"
1756
1795
  ],
1757
- compressive: 6e4
1796
+ compressive: 6e4,
1797
+ hits: {
1798
+ ballistic: {
1799
+ tear: "splinters"
1800
+ },
1801
+ blunt: {
1802
+ tear: "splinters",
1803
+ tearJoules: 80
1804
+ }
1805
+ }
1758
1806
  },
1759
1807
  {
1760
1808
  id: "mdf",
@@ -1781,14 +1829,25 @@ var GameDestruction = (() => {
1781
1829
  smoke: "grey"
1782
1830
  },
1783
1831
  visual: {
1784
- charColor: 1709072
1832
+ charColor: 1709072,
1833
+ innerColor: 12096362
1785
1834
  },
1786
1835
  tags: [
1787
1836
  "wood",
1788
1837
  "organic",
1789
1838
  "flammable"
1790
1839
  ],
1791
- compressive: 3e4
1840
+ compressive: 3e4,
1841
+ hits: {
1842
+ ballistic: {
1843
+ tear: "lumps",
1844
+ tearJoules: 120
1845
+ },
1846
+ blunt: {
1847
+ tear: "lumps",
1848
+ tearJoules: 20
1849
+ }
1850
+ }
1792
1851
  },
1793
1852
  {
1794
1853
  id: "glass",
@@ -1902,8 +1961,20 @@ var GameDestruction = (() => {
1902
1961
  damageScale: {
1903
1962
  explosive: 1.2
1904
1963
  },
1964
+ hits: {
1965
+ ballistic: {
1966
+ unitJoules: 500,
1967
+ floor: 900,
1968
+ crater: true
1969
+ },
1970
+ blunt: {
1971
+ unitJoules: 1500,
1972
+ floor: 60
1973
+ }
1974
+ },
1905
1975
  hitDebris: "chips",
1906
1976
  breakDebris: "dust",
1977
+ visual: { innerColor: 10197652 },
1907
1978
  thermal: {
1908
1979
  conductivity: 0.15,
1909
1980
  heatCapacity: 2.5
@@ -1934,6 +2005,7 @@ var GameDestruction = (() => {
1934
2005
  ],
1935
2006
  hitDebris: "chips",
1936
2007
  breakDebris: "dust",
2008
+ visual: { innerColor: 12085567 },
1937
2009
  thermal: {
1938
2010
  conductivity: 0.15,
1939
2011
  heatCapacity: 2.5
@@ -1964,6 +2036,7 @@ var GameDestruction = (() => {
1964
2036
  ],
1965
2037
  hitDebris: "dust",
1966
2038
  breakDebris: "dust",
2039
+ visual: { innerColor: 14999507 },
1967
2040
  thermal: {
1968
2041
  conductivity: 0.1,
1969
2042
  heatCapacity: 1.5
@@ -1989,6 +2062,7 @@ var GameDestruction = (() => {
1989
2062
  ],
1990
2063
  hitDebris: "chips",
1991
2064
  breakDebris: "dust",
2065
+ visual: { innerColor: 9407364 },
1992
2066
  thermal: {
1993
2067
  conductivity: 0.2,
1994
2068
  heatCapacity: 2.5
@@ -2270,7 +2344,8 @@ var GameDestruction = (() => {
2270
2344
  smoke: "white"
2271
2345
  },
2272
2346
  visual: {
2273
- charColor: 1709072
2347
+ charColor: 1709072,
2348
+ innerColor: 14207126
2274
2349
  },
2275
2350
  tags: [
2276
2351
  "plant",
@@ -2278,7 +2353,12 @@ var GameDestruction = (() => {
2278
2353
  "organic",
2279
2354
  "flammable"
2280
2355
  ],
2281
- compressive: 6e4
2356
+ compressive: 6e4,
2357
+ hits: {
2358
+ blunt: {
2359
+ tear: "none"
2360
+ }
2361
+ }
2282
2362
  },
2283
2363
  {
2284
2364
  id: "cactus",
@@ -2393,6 +2473,130 @@ var GameDestruction = (() => {
2393
2473
  "masonry"
2394
2474
  ],
2395
2475
  compressive: 2e4
2476
+ },
2477
+ {
2478
+ id: "asphalt",
2479
+ density: 2350,
2480
+ hpPerKg: 10,
2481
+ penetration: 14,
2482
+ penetrationClass: "concrete",
2483
+ penetrationScale: 0.8,
2484
+ ductility: 0.1,
2485
+ fracture: "crumble",
2486
+ fragmentSize: [
2487
+ 0.03,
2488
+ 0.15
2489
+ ],
2490
+ hitDebris: "chips",
2491
+ breakDebris: "dust",
2492
+ thermal: {
2493
+ conductivity: 0.1,
2494
+ heatCapacity: 2,
2495
+ ignitionC: 400,
2496
+ burnRate: 2e-3,
2497
+ smoke: "black"
2498
+ },
2499
+ tags: [
2500
+ "masonry",
2501
+ "brittle"
2502
+ ],
2503
+ compressive: 3e4
2504
+ },
2505
+ {
2506
+ id: "sand",
2507
+ density: 1600,
2508
+ hpPerKg: 2,
2509
+ penetration: 6,
2510
+ ductility: 0.4,
2511
+ fracture: "crumble",
2512
+ fragmentSize: [
2513
+ 0.01,
2514
+ 0.04
2515
+ ],
2516
+ hitDebris: "dust",
2517
+ breakDebris: "dust",
2518
+ thermal: {
2519
+ conductivity: 0.1,
2520
+ heatCapacity: 2
2521
+ },
2522
+ tags: [
2523
+ "soil"
2524
+ ],
2525
+ compressive: 1e4
2526
+ },
2527
+ {
2528
+ id: "gravel",
2529
+ density: 1800,
2530
+ hpPerKg: 3,
2531
+ penetration: 8,
2532
+ ductility: 0.3,
2533
+ fracture: "crumble",
2534
+ fragmentSize: [
2535
+ 0.02,
2536
+ 0.06
2537
+ ],
2538
+ hitDebris: "chips",
2539
+ breakDebris: "dust",
2540
+ thermal: {
2541
+ conductivity: 0.1,
2542
+ heatCapacity: 2
2543
+ },
2544
+ tags: [
2545
+ "soil",
2546
+ "masonry"
2547
+ ],
2548
+ compressive: 2e4
2549
+ },
2550
+ {
2551
+ id: "turf",
2552
+ density: 900,
2553
+ hpPerKg: 3,
2554
+ penetration: 4,
2555
+ ductility: 0.5,
2556
+ fracture: "crumble",
2557
+ fragmentSize: [
2558
+ 0.03,
2559
+ 0.1
2560
+ ],
2561
+ hitDebris: "dust",
2562
+ breakDebris: "dust",
2563
+ thermal: {
2564
+ conductivity: 0.1,
2565
+ heatCapacity: 1.5,
2566
+ ignitionC: 250,
2567
+ burnRate: 0.03,
2568
+ smoke: "white"
2569
+ },
2570
+ tags: [
2571
+ "soil",
2572
+ "organic"
2573
+ ],
2574
+ compressive: 15e3
2575
+ },
2576
+ {
2577
+ id: "mortar",
2578
+ density: 1900,
2579
+ hpPerKg: 10,
2580
+ penetration: 12,
2581
+ penetrationClass: "concrete",
2582
+ penetrationScale: 1.2,
2583
+ ductility: 0.03,
2584
+ fracture: "crumble",
2585
+ fragmentSize: [
2586
+ 0.02,
2587
+ 0.08
2588
+ ],
2589
+ hitDebris: "chips",
2590
+ breakDebris: "dust",
2591
+ thermal: {
2592
+ conductivity: 0.15,
2593
+ heatCapacity: 2.5
2594
+ },
2595
+ tags: [
2596
+ "masonry",
2597
+ "brittle"
2598
+ ],
2599
+ compressive: 1e5
2396
2600
  }
2397
2601
  ];
2398
2602
 
@@ -2729,6 +2933,32 @@ var GameDestruction = (() => {
2729
2933
 
2730
2934
  // src/world.ts
2731
2935
  var THIN_SHEET_DISHING = 5e-3;
2936
+ var SWEEP_BACK = 0.01;
2937
+ function sweepOrigins(box, d, spacing) {
2938
+ const origins = [];
2939
+ const line = (from, to) => {
2940
+ const n = Math.max(1, Math.ceil((to - from) / spacing));
2941
+ return Array.from({ length: n + 1 }, (_, i) => from + (to - from) * i / n);
2942
+ };
2943
+ for (const axis of [0, 1, 2]) {
2944
+ if (Math.abs(d[axis]) < 1e-3) continue;
2945
+ const lead = d[axis] > 0 ? box.max[axis] - SWEEP_BACK : box.min[axis] + SWEEP_BACK;
2946
+ const u = (axis + 1) % 3;
2947
+ const v = (axis + 2) % 3;
2948
+ for (const a of line(box.min[u], box.max[u])) {
2949
+ for (const b of line(box.min[v], box.max[v])) {
2950
+ const point = [0, 0, 0];
2951
+ point[axis] = lead;
2952
+ point[u] = a;
2953
+ point[v] = b;
2954
+ origins.push(point);
2955
+ }
2956
+ }
2957
+ }
2958
+ return origins;
2959
+ }
2960
+ var THIN_SHEET_CRUSH = 5e-4;
2961
+ var THIN_SHEET_CONTACT = 0.025;
2732
2962
  var DMG_PER_J = 0.02;
2733
2963
  var BLAST_COUPLING = 0.1;
2734
2964
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
@@ -2745,11 +2975,15 @@ var GameDestruction = (() => {
2745
2975
  var SEVER_BAND = 0.1;
2746
2976
  var SEVER_ASPECT = 3;
2747
2977
  var SEVER_BALLISTIC = 2;
2978
+ var TEAR_JOULES = 150;
2979
+ var tears = 0;
2748
2980
  var SUPPORT_TOLERANCE = 0.05;
2749
2981
  var UNIT_FRAGMENT_FACTOR = 4;
2750
2982
  var BREACH_GRID_CAP = 4096;
2751
2983
  var CHIP_ENERGY = 200;
2752
2984
  var CHIP_SPLASH = 0.5;
2985
+ var BAR_BOW = 0.08;
2986
+ var BULLET_JOINT_SPAN = 0.3;
2753
2987
  var CUT_SLICE = 1;
2754
2988
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
2755
2989
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -2759,8 +2993,24 @@ var GameDestruction = (() => {
2759
2993
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
2760
2994
  };
2761
2995
  var capacityScale = (p) => {
2762
- if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
2763
- return p.rt.removed ? Math.max(0.05, 1 - p.rt.removed / p.mass0) : 1;
2996
+ const residual = p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
2997
+ if (p.st.stripped) return residual;
2998
+ const kept = Math.min(p.rt.removed ? 1 - p.rt.removed / p.mass0 : 1, 1 - sectionLoss(p));
2999
+ return Math.max(p.spec.reinforcement ? residual : 0.05, kept);
3000
+ };
3001
+ var sectionLoss = (p) => {
3002
+ const cells = p.st.chipped;
3003
+ if (!cells?.length) return 0;
3004
+ const [u, v] = planeAxes(p.thin);
3005
+ const along = p.dims[u] >= p.dims[v] ? u : v;
3006
+ const across = along === u ? v : u;
3007
+ let worst = 0;
3008
+ for (const c of cells) {
3009
+ let gone = 0;
3010
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across];
3011
+ worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
3012
+ }
3013
+ return Math.min(1, worst);
2764
3014
  };
2765
3015
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
2766
3016
  var clone = (v) => structuredClone(v);
@@ -3860,7 +4110,7 @@ var GameDestruction = (() => {
3860
4110
  emit({ ...base(o, src), type: "crack", part: p.spec.id, crack, worldPoint: wp });
3861
4111
  setStage(o, p, 1, {}, src);
3862
4112
  };
3863
- const debris = (o, p, kind, point, normal, count, speed, src) => emit({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed });
4113
+ const debris = (o, p, kind, point, normal, count, speed, src, material) => emit({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed, ...material && material !== p.spec.material ? { material } : {} });
3864
4114
  const holeRate = (hole, pressurized) => (pressurized ? 0.5 : 0.1) * (hole.diameter / 9e-3);
3865
4115
  const detachEvents = (o, opened, by, src) => {
3866
4116
  for (const j of opened) emit({ ...base(o, src), type: "detach", joint: j.spec.id, parts: [j.spec.a, j.spec.b], by, groups: o.st.groups.map((g) => [...g]) });
@@ -3901,7 +4151,13 @@ var GameDestruction = (() => {
3901
4151
  const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
3902
4152
  return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
3903
4153
  };
3904
- const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false) => {
4154
+ const jointLength = (o, j) => {
4155
+ const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
4156
+ let len = 0;
4157
+ for (let k = 0; k < 3; k++) len = Math.max(len, Math.min(A.max[k], B.max[k]) - Math.max(A.min[k], B.min[k]));
4158
+ return len;
4159
+ };
4160
+ const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false, span) => {
3905
4161
  if (amount <= 0) return;
3906
4162
  const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
3907
4163
  if (split && mine.length) amount /= mine.length;
@@ -3909,7 +4165,7 @@ var GameDestruction = (() => {
3909
4165
  if (j.st.open) continue;
3910
4166
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
3911
4167
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
3912
- j.st.hp -= amount;
4168
+ j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
3913
4169
  if (j.st.hp <= 0) {
3914
4170
  j.st.hp = 0;
3915
4171
  j.st.open = true;
@@ -3938,7 +4194,13 @@ var GameDestruction = (() => {
3938
4194
  if (spalls.length > MAX_SPALLS) spalls.shift();
3939
4195
  markDamaged(p);
3940
4196
  emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: layer.index, point: wp, area: Math.PI * radius * radius });
3941
- debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
4197
+ debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src, layer.mat.id);
4198
+ setStage(o, p, 1, {}, src);
4199
+ };
4200
+ const crater = (o, p, wp, normal, radius, src) => {
4201
+ markDamaged(p);
4202
+ emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: -1, point: wp, area: Math.PI * radius * radius });
4203
+ debris(o, p, p.mat.breakDebris, wp, normal, 6, 1.5, src);
3942
4204
  setStage(o, p, 1, {}, src);
3943
4205
  };
3944
4206
  const insideOpening = (o, p, objectPoint) => {
@@ -4017,9 +4279,13 @@ var GameDestruction = (() => {
4017
4279
  const lp = toLocal(p.pos, p.q, objectPoint);
4018
4280
  return cells.some((c) => Math.abs(lp[u] - c.center[u]) <= c.size[u] / 2 && Math.abs(lp[v] - c.center[v]) <= c.size[v] / 2);
4019
4281
  };
4020
- const chipUnits = (o, p, wp, energy, dir, radius, src) => {
4282
+ const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
4021
4283
  var _a, _b;
4022
- if (!p.spec.units || p.spec.reinforcement || !p.staged || chipEnergy <= 0 || energy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
4284
+ if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
4285
+ const rule = p.mat.hits?.[kind];
4286
+ const perKg = rule?.unitJoules ?? chipEnergy;
4287
+ const energy = hitEnergy - (rule?.floor ?? 0);
4288
+ if (perKg <= 0 || energy <= 0) return;
4023
4289
  const [u, v] = planeAxes(p.thin);
4024
4290
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
4025
4291
  const grid = gridOf(p);
@@ -4033,7 +4299,7 @@ var GameDestruction = (() => {
4033
4299
  const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
4034
4300
  const total = (worn[i] ?? 0) + energy * share;
4035
4301
  const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
4036
- if (total < chipEnergy * mass) {
4302
+ if (total < perKg * mass) {
4037
4303
  worn[i] = total;
4038
4304
  continue;
4039
4305
  }
@@ -4058,7 +4324,32 @@ var GameDestruction = (() => {
4058
4324
  emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
4059
4325
  debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
4060
4326
  }
4327
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
4061
4328
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
4329
+ if (p.structural && !isLost(p)) {
4330
+ solveStructure(o);
4331
+ collapseCheck(o, src);
4332
+ }
4333
+ };
4334
+ const exposeRebar = (o, p, src) => {
4335
+ const r = p.spec.reinforcement;
4336
+ if (!r) return;
4337
+ const [u, v] = planeAxes(p.thin);
4338
+ const all = rebarBars(p.dims, p.thin, r.spacing);
4339
+ let bars;
4340
+ if (p.st.stripped) bars = all;
4341
+ else {
4342
+ bars = [];
4343
+ for (const cell of p.st.chipped ?? []) bars.push(...clipBarsToBox(all, u, v, cell.center, cell.size));
4344
+ const op = p.st.opening;
4345
+ if (op) {
4346
+ const lc = toLocal(p.pos, p.q, op.center);
4347
+ bars.push(...clipBarsToDisc(all, u, v, [lc[u], lc[v]], op.radius + 0.05));
4348
+ }
4349
+ }
4350
+ const bow = clamp2(((p.st.stress ?? 0) - 1) / 0.5, 0, 1);
4351
+ if (bow > 0) bars = bars.flatMap((bar2, i) => bendBar(bar2, p.thin, bow * BAR_BOW, i));
4352
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
4062
4353
  };
4063
4354
  const openBreach = (o, p, center, total, energy, dir, src) => {
4064
4355
  const [u, v] = planeAxes(p.thin);
@@ -4086,10 +4377,7 @@ var GameDestruction = (() => {
4086
4377
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
4087
4378
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
4088
4379
  const reinforcement = p.spec.reinforcement;
4089
- if (reinforcement) {
4090
- const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
4091
- if (bars.length > 0) emit({ ...base(o, src), type: "expose", part: p.spec.id, bars });
4092
- }
4380
+ if (reinforcement) exposeRebar(o, p, src);
4093
4381
  if (p.structural) solveStructure(o);
4094
4382
  };
4095
4383
  const stripPart = (o, p, reinforcement, src) => {
@@ -4101,8 +4389,8 @@ var GameDestruction = (() => {
4101
4389
  p.st.burning = false;
4102
4390
  p.rt.jet = false;
4103
4391
  markDamaged(p);
4104
- emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
4105
4392
  solveStructure(o);
4393
+ exposeRebar(o, p, src);
4106
4394
  collapseCheck(o, src);
4107
4395
  };
4108
4396
  const unitFragmentPlan = (p, cap) => {
@@ -4156,7 +4444,7 @@ var GameDestruction = (() => {
4156
4444
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
4157
4445
  const hitLocal = toLocal(c, q, hitW);
4158
4446
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
4159
- const axis = style === "splinter" ? severAxis(p) : -1;
4447
+ const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
4160
4448
  const cutAt = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
4161
4449
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
4162
4450
  style,
@@ -4228,7 +4516,7 @@ var GameDestruction = (() => {
4228
4516
  };
4229
4517
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
4230
4518
  const severAxis = (p) => {
4231
- if (p.mat.fracture !== "splinter" || p.spec.shape.kind !== "box") return -1;
4519
+ if (p.mat.fracture !== "splinter" && p.mat.fracture !== "snap" || p.spec.shape.kind !== "box") return -1;
4232
4520
  const d = p.dims;
4233
4521
  const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
4234
4522
  return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
@@ -4289,6 +4577,49 @@ var GameDestruction = (() => {
4289
4577
  }
4290
4578
  }
4291
4579
  };
4580
+ const tearOf = (mat, kind) => {
4581
+ const h = mat.hits?.[kind];
4582
+ const tear = h?.tear ?? (kind === "ballistic" && mat.penetrationClass === "wood" ? "splinters" : "none");
4583
+ return { tear, joules: h?.tearJoules ?? TEAR_JOULES };
4584
+ };
4585
+ const tearAt = (o, p, kind, entry, exit, d, E, scale, src) => {
4586
+ const { tear, joules } = tearOf(p.mat, kind);
4587
+ if (tear === "none") return;
4588
+ const shot = kind === "ballistic";
4589
+ const n = shot ? exit ? clamp2(Math.round(E / joules), 1, 7) : 1 : clamp2(Math.floor(E / joules), 0, 5);
4590
+ if (n === 0) return;
4591
+ const fibre = tear === "lumps";
4592
+ const { q } = partXf(o, p);
4593
+ const along = (p.spec.grain ? AXIS[p.spec.grain] : void 0) ?? [0, 1, 2].sort((a, b) => p.dims[b] - p.dims[a])[0];
4594
+ const fragments = [];
4595
+ for (let i = 0; i < n; i++) {
4596
+ const back = !exit || i % 3 === 2;
4597
+ 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());
4598
+ const size = [w, w, w];
4599
+ size[along] = len;
4600
+ size[(along + 1) % 3] = w;
4601
+ size[(along + 2) % 3] = t;
4602
+ const spread3 = [(rnd() - 0.5) * 0.8, (rnd() - 0.2) * 0.8, (rnd() - 0.5) * 0.8];
4603
+ const speed = shot ? back ? 1.5 + 2 * rnd() : 2 + 4 * rnd() : 0.8 + 1.2 * rnd();
4604
+ const v = vec.scale(vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3)), speed);
4605
+ const mass = size[0] * size[1] * size[2] * p.mat.density;
4606
+ fragments.push({
4607
+ id: `${p.spec.id}#t${tears++}-${i}`,
4608
+ part: p.spec.id,
4609
+ material: p.spec.material,
4610
+ shape: fibre ? "chunk" : "splinter",
4611
+ center: back || !exit ? vec.madd(entry, d, -0.01) : vec.madd(exit, d, 0.01),
4612
+ size,
4613
+ rotation: q,
4614
+ velocity: v,
4615
+ inner: true,
4616
+ angularVelocity: [(rnd() - 0.5) * 30, (rnd() - 0.5) * 30, (rnd() - 0.5) * 30],
4617
+ mass
4618
+ });
4619
+ }
4620
+ const patch = [scale * 4, scale * 4, scale * 4];
4621
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry, size: patch, rotation: q, fragments });
4622
+ };
4292
4623
  const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src, round = {}) => {
4293
4624
  const mat = p.mat;
4294
4625
  const piercing = round.piercing ?? 1;
@@ -4317,6 +4648,7 @@ var GameDestruction = (() => {
4317
4648
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
4318
4649
  }
4319
4650
  if (p.layers.length > 0) spallOuter(o, p, entry, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
4651
+ else if (mat.hits?.ballistic?.crater) crater(o, p, entry, n, clamp2(0.02 + Math.sqrt(E) * 15e-4, 0.02, 0.15), src);
4320
4652
  const brittle = mat.ductility < 0.3;
4321
4653
  const ratio = Ep / Math.max(thr, 1e-6);
4322
4654
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
@@ -4349,9 +4681,11 @@ var GameDestruction = (() => {
4349
4681
  if (mat.fracture !== "shatter") addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
4350
4682
  } else addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
4351
4683
  debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
4352
- wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
4353
- const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry, d, E * 0.1, 12, src);
4354
- chipUnits(o, p, entry, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
4684
+ wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
4685
+ const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
4686
+ const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry, d, absorbed * 0.1, 12, src);
4687
+ if (!isLost(p)) tearAt(o, p, "ballistic", entry, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
4688
+ chipUnits(o, p, "ballistic", entry, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
4355
4689
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
4356
4690
  };
4357
4691
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -4359,14 +4693,20 @@ var GameDestruction = (() => {
4359
4693
  const thrMm = mat.penetration * p.thickness * 1e3;
4360
4694
  const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
4361
4695
  const Ec = E * fragile;
4696
+ const sheet = mat.ductility >= 0.5 && p.thickness <= THIN_SHEET_CRUSH;
4697
+ const fold = sheet ? Math.min(4, Math.sqrt(1e-3 / p.thickness)) : 1;
4698
+ const patch = sheet ? Math.min(contactR, THIN_SHEET_CONTACT) : contactR;
4362
4699
  if (E > 0) {
4363
4700
  if (kind === "blunt") {
4364
- if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp2(E * 1e-4, 1e-3, 0.06), src);
4701
+ if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(patch, 0.015), clamp2(E * 1e-4 * fold, 1e-3, sheet ? Math.min(0.06, Math.min(...p.dims) * 0.45) : 0.06), src);
4365
4702
  else if (mat.ductility < 0.3) {
4366
4703
  if (Ec >= 5) addCrack(o, p, point, n, clamp2(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
4367
4704
  } else if (E >= thrMm * 0.2) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp2(E * 6e-5, 1e-3, 0.04), src);
4368
4705
  } else if (mat.fracture === "tear") {
4369
4706
  addHole(o, p, point, n, clamp2(0.02 + E * 5e-4, 0.02, 0.3), true, void 0, src);
4707
+ } else if (mat.ductility >= 0.3 && E >= thrMm) {
4708
+ addHole(o, p, point, n, clamp2(contactR, 0.01, 0.08), true, void 0, src);
4709
+ if (sheet) addDent(o, p, point, n, Math.max(patch * 0.6, 0.01), clamp2(E * 4e-5 * fold, 5e-4, 0.02), src);
4370
4710
  } else if (mat.ductility >= 0.5) {
4371
4711
  addDent(o, p, point, n, Math.max(contactR, 0.01), clamp2(E * 4e-5, 5e-4, 0.02), src);
4372
4712
  }
@@ -4379,7 +4719,8 @@ var GameDestruction = (() => {
4379
4719
  const scale = mat.damageScale?.[kind] ?? 1;
4380
4720
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
4381
4721
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
4382
- if (kind === "blunt") chipUnits(o, p, point, E, d, Math.max(contactR, 0.03), src);
4722
+ if (kind === "blunt") chipUnits(o, p, "blunt", point, E, d, Math.max(contactR, 0.03), src);
4723
+ if (E > 0 && !isLost(p)) tearAt(o, p, kind, point, void 0, d, E, clamp2(contactR * 0.2, 6e-3, 0.02), src);
4383
4724
  return done;
4384
4725
  };
4385
4726
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -4958,6 +5299,7 @@ var GameDestruction = (() => {
4958
5299
  if (p.rt.stressT <= 0) {
4959
5300
  p.rt.stressT = 0.5;
4960
5301
  emit({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
5302
+ if (p.spec.reinforcement && (p.st.stripped || p.st.chipped?.length || p.st.opening)) exposeRebar(o, p);
4961
5303
  }
4962
5304
  applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
4963
5305
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
@@ -5174,6 +5516,18 @@ var GameDestruction = (() => {
5174
5516
  const r = raycastInternal(origin, direction, maxDistance);
5175
5517
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
5176
5518
  },
5519
+ sweepBox(box, direction, distance, options2 = {}) {
5520
+ const d = vec.normalize(direction);
5521
+ const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
5522
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
5523
+ let best = null;
5524
+ for (const origin of sweepOrigins(box, d, options2.spacing ?? 0.2)) {
5525
+ const r = raycastInternal(origin, d, distance + SWEEP_BACK, ignore);
5526
+ if (!r || best && r.distance >= best.distance) continue;
5527
+ best = { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: Math.max(0, r.distance - SWEEP_BACK) };
5528
+ }
5529
+ return best;
5530
+ },
5177
5531
  shoot(origin, direction, energy, caliber, source, round) {
5178
5532
  const extras = typeof round === "number" ? { piercing: round } : round ?? {};
5179
5533
  return withCollapses(run(() => {
@@ -5818,7 +6172,12 @@ var GameDestruction = (() => {
5818
6172
  cactus: "cactus",
5819
6173
  flesh_fruit: "fruit",
5820
6174
  wax: "wax",
5821
- soil: "soil"
6175
+ soil: "soil",
6176
+ asphalt: "asphalt",
6177
+ sand: "sand",
6178
+ gravel: "gravel",
6179
+ turf: "turf",
6180
+ mortar: "mortar"
5822
6181
  },
5823
6182
  partStatus: { intact: "intact", damaged: "damaged", collapsed: "collapsed", broken: "broken", melted: "melted", ashes: "ashes" },
5824
6183
  jointKinds: {
@@ -5909,7 +6268,12 @@ var GameDestruction = (() => {
5909
6268
  cactus: "cacto",
5910
6269
  flesh_fruit: "fruta",
5911
6270
  wax: "cera",
5912
- soil: "terra"
6271
+ soil: "terra",
6272
+ asphalt: "asfalto",
6273
+ sand: "areia",
6274
+ gravel: "brita",
6275
+ turf: "grama",
6276
+ mortar: "argamassa"
5913
6277
  },
5914
6278
  partStatus: { intact: "intacto", damaged: "danificado", collapsed: "desabado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
5915
6279
  jointKinds: {