@adatechnology/game-destruction 0.10.0 → 0.11.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,6 +1961,17 @@ 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",
1907
1977
  thermal: {
@@ -2270,7 +2340,8 @@ var GameDestruction = (() => {
2270
2340
  smoke: "white"
2271
2341
  },
2272
2342
  visual: {
2273
- charColor: 1709072
2343
+ charColor: 1709072,
2344
+ innerColor: 14207126
2274
2345
  },
2275
2346
  tags: [
2276
2347
  "plant",
@@ -2278,7 +2349,12 @@ var GameDestruction = (() => {
2278
2349
  "organic",
2279
2350
  "flammable"
2280
2351
  ],
2281
- compressive: 6e4
2352
+ compressive: 6e4,
2353
+ hits: {
2354
+ blunt: {
2355
+ tear: "none"
2356
+ }
2357
+ }
2282
2358
  },
2283
2359
  {
2284
2360
  id: "cactus",
@@ -2393,6 +2469,130 @@ var GameDestruction = (() => {
2393
2469
  "masonry"
2394
2470
  ],
2395
2471
  compressive: 2e4
2472
+ },
2473
+ {
2474
+ id: "asphalt",
2475
+ density: 2350,
2476
+ hpPerKg: 10,
2477
+ penetration: 14,
2478
+ penetrationClass: "concrete",
2479
+ penetrationScale: 0.8,
2480
+ ductility: 0.1,
2481
+ fracture: "crumble",
2482
+ fragmentSize: [
2483
+ 0.03,
2484
+ 0.15
2485
+ ],
2486
+ hitDebris: "chips",
2487
+ breakDebris: "dust",
2488
+ thermal: {
2489
+ conductivity: 0.1,
2490
+ heatCapacity: 2,
2491
+ ignitionC: 400,
2492
+ burnRate: 2e-3,
2493
+ smoke: "black"
2494
+ },
2495
+ tags: [
2496
+ "masonry",
2497
+ "brittle"
2498
+ ],
2499
+ compressive: 3e4
2500
+ },
2501
+ {
2502
+ id: "sand",
2503
+ density: 1600,
2504
+ hpPerKg: 2,
2505
+ penetration: 6,
2506
+ ductility: 0.4,
2507
+ fracture: "crumble",
2508
+ fragmentSize: [
2509
+ 0.01,
2510
+ 0.04
2511
+ ],
2512
+ hitDebris: "dust",
2513
+ breakDebris: "dust",
2514
+ thermal: {
2515
+ conductivity: 0.1,
2516
+ heatCapacity: 2
2517
+ },
2518
+ tags: [
2519
+ "soil"
2520
+ ],
2521
+ compressive: 1e4
2522
+ },
2523
+ {
2524
+ id: "gravel",
2525
+ density: 1800,
2526
+ hpPerKg: 3,
2527
+ penetration: 8,
2528
+ ductility: 0.3,
2529
+ fracture: "crumble",
2530
+ fragmentSize: [
2531
+ 0.02,
2532
+ 0.06
2533
+ ],
2534
+ hitDebris: "chips",
2535
+ breakDebris: "dust",
2536
+ thermal: {
2537
+ conductivity: 0.1,
2538
+ heatCapacity: 2
2539
+ },
2540
+ tags: [
2541
+ "soil",
2542
+ "masonry"
2543
+ ],
2544
+ compressive: 2e4
2545
+ },
2546
+ {
2547
+ id: "turf",
2548
+ density: 900,
2549
+ hpPerKg: 3,
2550
+ penetration: 4,
2551
+ ductility: 0.5,
2552
+ fracture: "crumble",
2553
+ fragmentSize: [
2554
+ 0.03,
2555
+ 0.1
2556
+ ],
2557
+ hitDebris: "dust",
2558
+ breakDebris: "dust",
2559
+ thermal: {
2560
+ conductivity: 0.1,
2561
+ heatCapacity: 1.5,
2562
+ ignitionC: 250,
2563
+ burnRate: 0.03,
2564
+ smoke: "white"
2565
+ },
2566
+ tags: [
2567
+ "soil",
2568
+ "organic"
2569
+ ],
2570
+ compressive: 15e3
2571
+ },
2572
+ {
2573
+ id: "mortar",
2574
+ density: 1900,
2575
+ hpPerKg: 10,
2576
+ penetration: 12,
2577
+ penetrationClass: "concrete",
2578
+ penetrationScale: 1.2,
2579
+ ductility: 0.03,
2580
+ fracture: "crumble",
2581
+ fragmentSize: [
2582
+ 0.02,
2583
+ 0.08
2584
+ ],
2585
+ hitDebris: "chips",
2586
+ breakDebris: "dust",
2587
+ thermal: {
2588
+ conductivity: 0.15,
2589
+ heatCapacity: 2.5
2590
+ },
2591
+ tags: [
2592
+ "masonry",
2593
+ "brittle"
2594
+ ],
2595
+ compressive: 1e5
2396
2596
  }
2397
2597
  ];
2398
2598
 
@@ -2729,6 +2929,32 @@ var GameDestruction = (() => {
2729
2929
 
2730
2930
  // src/world.ts
2731
2931
  var THIN_SHEET_DISHING = 5e-3;
2932
+ var SWEEP_BACK = 0.01;
2933
+ function sweepOrigins(box, d, spacing) {
2934
+ const origins = [];
2935
+ const line = (from, to) => {
2936
+ const n = Math.max(1, Math.ceil((to - from) / spacing));
2937
+ return Array.from({ length: n + 1 }, (_, i) => from + (to - from) * i / n);
2938
+ };
2939
+ for (const axis of [0, 1, 2]) {
2940
+ if (Math.abs(d[axis]) < 1e-3) continue;
2941
+ const lead = d[axis] > 0 ? box.max[axis] - SWEEP_BACK : box.min[axis] + SWEEP_BACK;
2942
+ const u = (axis + 1) % 3;
2943
+ const v = (axis + 2) % 3;
2944
+ for (const a of line(box.min[u], box.max[u])) {
2945
+ for (const b of line(box.min[v], box.max[v])) {
2946
+ const point = [0, 0, 0];
2947
+ point[axis] = lead;
2948
+ point[u] = a;
2949
+ point[v] = b;
2950
+ origins.push(point);
2951
+ }
2952
+ }
2953
+ }
2954
+ return origins;
2955
+ }
2956
+ var THIN_SHEET_CRUSH = 5e-4;
2957
+ var THIN_SHEET_CONTACT = 0.025;
2732
2958
  var DMG_PER_J = 0.02;
2733
2959
  var BLAST_COUPLING = 0.1;
2734
2960
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
@@ -2745,11 +2971,14 @@ var GameDestruction = (() => {
2745
2971
  var SEVER_BAND = 0.1;
2746
2972
  var SEVER_ASPECT = 3;
2747
2973
  var SEVER_BALLISTIC = 2;
2974
+ var TEAR_JOULES = 150;
2975
+ var tears = 0;
2748
2976
  var SUPPORT_TOLERANCE = 0.05;
2749
2977
  var UNIT_FRAGMENT_FACTOR = 4;
2750
2978
  var BREACH_GRID_CAP = 4096;
2751
2979
  var CHIP_ENERGY = 200;
2752
2980
  var CHIP_SPLASH = 0.5;
2981
+ var BAR_BOW = 0.08;
2753
2982
  var CUT_SLICE = 1;
2754
2983
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
2755
2984
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -2759,8 +2988,24 @@ var GameDestruction = (() => {
2759
2988
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
2760
2989
  };
2761
2990
  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;
2991
+ const residual = p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
2992
+ if (p.st.stripped) return residual;
2993
+ const kept = Math.min(p.rt.removed ? 1 - p.rt.removed / p.mass0 : 1, 1 - sectionLoss(p));
2994
+ return Math.max(p.spec.reinforcement ? residual : 0.05, kept);
2995
+ };
2996
+ var sectionLoss = (p) => {
2997
+ const cells = p.st.chipped;
2998
+ if (!cells?.length) return 0;
2999
+ const [u, v] = planeAxes(p.thin);
3000
+ const along = p.dims[u] >= p.dims[v] ? u : v;
3001
+ const across = along === u ? v : u;
3002
+ let worst = 0;
3003
+ for (const c of cells) {
3004
+ let gone = 0;
3005
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across];
3006
+ worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
3007
+ }
3008
+ return Math.min(1, worst);
2764
3009
  };
2765
3010
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
2766
3011
  var clone = (v) => structuredClone(v);
@@ -3941,6 +4186,12 @@ var GameDestruction = (() => {
3941
4186
  debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
3942
4187
  setStage(o, p, 1, {}, src);
3943
4188
  };
4189
+ const crater = (o, p, wp, normal, radius, src) => {
4190
+ markDamaged(p);
4191
+ emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: -1, point: wp, area: Math.PI * radius * radius });
4192
+ debris(o, p, p.mat.breakDebris, wp, normal, 6, 1.5, src);
4193
+ setStage(o, p, 1, {}, src);
4194
+ };
3944
4195
  const insideOpening = (o, p, objectPoint) => {
3945
4196
  const op = p.st.opening;
3946
4197
  if (!op) return false;
@@ -4017,9 +4268,13 @@ var GameDestruction = (() => {
4017
4268
  const lp = toLocal(p.pos, p.q, objectPoint);
4018
4269
  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
4270
  };
4020
- const chipUnits = (o, p, wp, energy, dir, radius, src) => {
4271
+ const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
4021
4272
  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;
4273
+ if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
4274
+ const rule = p.mat.hits?.[kind];
4275
+ const perKg = rule?.unitJoules ?? chipEnergy;
4276
+ const energy = hitEnergy - (rule?.floor ?? 0);
4277
+ if (perKg <= 0 || energy <= 0) return;
4023
4278
  const [u, v] = planeAxes(p.thin);
4024
4279
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
4025
4280
  const grid = gridOf(p);
@@ -4033,7 +4288,7 @@ var GameDestruction = (() => {
4033
4288
  const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
4034
4289
  const total = (worn[i] ?? 0) + energy * share;
4035
4290
  const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
4036
- if (total < chipEnergy * mass) {
4291
+ if (total < perKg * mass) {
4037
4292
  worn[i] = total;
4038
4293
  continue;
4039
4294
  }
@@ -4058,7 +4313,32 @@ var GameDestruction = (() => {
4058
4313
  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
4314
  debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
4060
4315
  }
4316
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
4061
4317
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
4318
+ if (p.structural && !isLost(p)) {
4319
+ solveStructure(o);
4320
+ collapseCheck(o, src);
4321
+ }
4322
+ };
4323
+ const exposeRebar = (o, p, src) => {
4324
+ const r = p.spec.reinforcement;
4325
+ if (!r) return;
4326
+ const [u, v] = planeAxes(p.thin);
4327
+ const all = rebarBars(p.dims, p.thin, r.spacing);
4328
+ let bars;
4329
+ if (p.st.stripped) bars = all;
4330
+ else {
4331
+ bars = [];
4332
+ for (const cell of p.st.chipped ?? []) bars.push(...clipBarsToBox(all, u, v, cell.center, cell.size));
4333
+ const op = p.st.opening;
4334
+ if (op) {
4335
+ const lc = toLocal(p.pos, p.q, op.center);
4336
+ bars.push(...clipBarsToDisc(all, u, v, [lc[u], lc[v]], op.radius + 0.05));
4337
+ }
4338
+ }
4339
+ const bow = clamp2(((p.st.stress ?? 0) - 1) / 0.5, 0, 1);
4340
+ if (bow > 0) bars = bars.flatMap((bar2, i) => bendBar(bar2, p.thin, bow * BAR_BOW, i));
4341
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
4062
4342
  };
4063
4343
  const openBreach = (o, p, center, total, energy, dir, src) => {
4064
4344
  const [u, v] = planeAxes(p.thin);
@@ -4086,10 +4366,7 @@ var GameDestruction = (() => {
4086
4366
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
4087
4367
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
4088
4368
  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
- }
4369
+ if (reinforcement) exposeRebar(o, p, src);
4093
4370
  if (p.structural) solveStructure(o);
4094
4371
  };
4095
4372
  const stripPart = (o, p, reinforcement, src) => {
@@ -4101,8 +4378,8 @@ var GameDestruction = (() => {
4101
4378
  p.st.burning = false;
4102
4379
  p.rt.jet = false;
4103
4380
  markDamaged(p);
4104
- emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
4105
4381
  solveStructure(o);
4382
+ exposeRebar(o, p, src);
4106
4383
  collapseCheck(o, src);
4107
4384
  };
4108
4385
  const unitFragmentPlan = (p, cap) => {
@@ -4156,7 +4433,7 @@ var GameDestruction = (() => {
4156
4433
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
4157
4434
  const hitLocal = toLocal(c, q, hitW);
4158
4435
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
4159
- const axis = style === "splinter" ? severAxis(p) : -1;
4436
+ const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
4160
4437
  const cutAt = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
4161
4438
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
4162
4439
  style,
@@ -4228,7 +4505,7 @@ var GameDestruction = (() => {
4228
4505
  };
4229
4506
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
4230
4507
  const severAxis = (p) => {
4231
- if (p.mat.fracture !== "splinter" || p.spec.shape.kind !== "box") return -1;
4508
+ if (p.mat.fracture !== "splinter" && p.mat.fracture !== "snap" || p.spec.shape.kind !== "box") return -1;
4232
4509
  const d = p.dims;
4233
4510
  const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
4234
4511
  return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
@@ -4289,6 +4566,49 @@ var GameDestruction = (() => {
4289
4566
  }
4290
4567
  }
4291
4568
  };
4569
+ const tearOf = (mat, kind) => {
4570
+ const h = mat.hits?.[kind];
4571
+ const tear = h?.tear ?? (kind === "ballistic" && mat.penetrationClass === "wood" ? "splinters" : "none");
4572
+ return { tear, joules: h?.tearJoules ?? TEAR_JOULES };
4573
+ };
4574
+ const tearAt = (o, p, kind, entry, exit, d, E, scale, src) => {
4575
+ const { tear, joules } = tearOf(p.mat, kind);
4576
+ if (tear === "none") return;
4577
+ const shot = kind === "ballistic";
4578
+ const n = shot ? exit ? clamp2(Math.round(E / joules), 1, 7) : 1 : clamp2(Math.floor(E / joules), 0, 5);
4579
+ if (n === 0) return;
4580
+ const fibre = tear === "lumps";
4581
+ const { q } = partXf(o, p);
4582
+ const along = (p.spec.grain ? AXIS[p.spec.grain] : void 0) ?? [0, 1, 2].sort((a, b) => p.dims[b] - p.dims[a])[0];
4583
+ const fragments = [];
4584
+ for (let i = 0; i < n; i++) {
4585
+ const back = !exit || i % 3 === 2;
4586
+ 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());
4587
+ const size = [w, w, w];
4588
+ size[along] = len;
4589
+ size[(along + 1) % 3] = w;
4590
+ size[(along + 2) % 3] = t;
4591
+ const spread3 = [(rnd() - 0.5) * 0.8, (rnd() - 0.2) * 0.8, (rnd() - 0.5) * 0.8];
4592
+ const speed = shot ? back ? 1.5 + 2 * rnd() : 2 + 4 * rnd() : 0.8 + 1.2 * rnd();
4593
+ const v = vec.scale(vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3)), speed);
4594
+ const mass = size[0] * size[1] * size[2] * p.mat.density;
4595
+ fragments.push({
4596
+ id: `${p.spec.id}#t${tears++}-${i}`,
4597
+ part: p.spec.id,
4598
+ material: p.spec.material,
4599
+ shape: fibre ? "chunk" : "splinter",
4600
+ center: back || !exit ? vec.madd(entry, d, -0.01) : vec.madd(exit, d, 0.01),
4601
+ size,
4602
+ rotation: q,
4603
+ velocity: v,
4604
+ inner: true,
4605
+ angularVelocity: [(rnd() - 0.5) * 30, (rnd() - 0.5) * 30, (rnd() - 0.5) * 30],
4606
+ mass
4607
+ });
4608
+ }
4609
+ const patch = [scale * 4, scale * 4, scale * 4];
4610
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry, size: patch, rotation: q, fragments });
4611
+ };
4292
4612
  const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src, round = {}) => {
4293
4613
  const mat = p.mat;
4294
4614
  const piercing = round.piercing ?? 1;
@@ -4317,6 +4637,7 @@ var GameDestruction = (() => {
4317
4637
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
4318
4638
  }
4319
4639
  if (p.layers.length > 0) spallOuter(o, p, entry, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
4640
+ 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
4641
  const brittle = mat.ductility < 0.3;
4321
4642
  const ratio = Ep / Math.max(thr, 1e-6);
4322
4643
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
@@ -4350,8 +4671,10 @@ var GameDestruction = (() => {
4350
4671
  } else addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
4351
4672
  debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
4352
4673
  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);
4674
+ const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
4675
+ const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry, d, absorbed * 0.1, 12, src);
4676
+ if (!isLost(p)) tearAt(o, p, "ballistic", entry, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
4677
+ 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
4678
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
4356
4679
  };
4357
4680
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -4359,14 +4682,20 @@ var GameDestruction = (() => {
4359
4682
  const thrMm = mat.penetration * p.thickness * 1e3;
4360
4683
  const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
4361
4684
  const Ec = E * fragile;
4685
+ const sheet = mat.ductility >= 0.5 && p.thickness <= THIN_SHEET_CRUSH;
4686
+ const fold = sheet ? Math.min(4, Math.sqrt(1e-3 / p.thickness)) : 1;
4687
+ const patch = sheet ? Math.min(contactR, THIN_SHEET_CONTACT) : contactR;
4362
4688
  if (E > 0) {
4363
4689
  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);
4690
+ 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
4691
  else if (mat.ductility < 0.3) {
4366
4692
  if (Ec >= 5) addCrack(o, p, point, n, clamp2(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
4367
4693
  } 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
4694
  } else if (mat.fracture === "tear") {
4369
4695
  addHole(o, p, point, n, clamp2(0.02 + E * 5e-4, 0.02, 0.3), true, void 0, src);
4696
+ } else if (mat.ductility >= 0.3 && E >= thrMm) {
4697
+ addHole(o, p, point, n, clamp2(contactR, 0.01, 0.08), true, void 0, src);
4698
+ 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
4699
  } else if (mat.ductility >= 0.5) {
4371
4700
  addDent(o, p, point, n, Math.max(contactR, 0.01), clamp2(E * 4e-5, 5e-4, 0.02), src);
4372
4701
  }
@@ -4379,7 +4708,8 @@ var GameDestruction = (() => {
4379
4708
  const scale = mat.damageScale?.[kind] ?? 1;
4380
4709
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
4381
4710
  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);
4711
+ if (kind === "blunt") chipUnits(o, p, "blunt", point, E, d, Math.max(contactR, 0.03), src);
4712
+ if (E > 0 && !isLost(p)) tearAt(o, p, kind, point, void 0, d, E, clamp2(contactR * 0.2, 6e-3, 0.02), src);
4383
4713
  return done;
4384
4714
  };
4385
4715
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -4958,6 +5288,7 @@ var GameDestruction = (() => {
4958
5288
  if (p.rt.stressT <= 0) {
4959
5289
  p.rt.stressT = 0.5;
4960
5290
  emit({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
5291
+ if (p.spec.reinforcement && (p.st.stripped || p.st.chipped?.length || p.st.opening)) exposeRebar(o, p);
4961
5292
  }
4962
5293
  applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
4963
5294
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
@@ -5174,6 +5505,18 @@ var GameDestruction = (() => {
5174
5505
  const r = raycastInternal(origin, direction, maxDistance);
5175
5506
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
5176
5507
  },
5508
+ sweepBox(box, direction, distance, options2 = {}) {
5509
+ const d = vec.normalize(direction);
5510
+ const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
5511
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
5512
+ let best = null;
5513
+ for (const origin of sweepOrigins(box, d, options2.spacing ?? 0.2)) {
5514
+ const r = raycastInternal(origin, d, distance + SWEEP_BACK, ignore);
5515
+ if (!r || best && r.distance >= best.distance) continue;
5516
+ best = { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: Math.max(0, r.distance - SWEEP_BACK) };
5517
+ }
5518
+ return best;
5519
+ },
5177
5520
  shoot(origin, direction, energy, caliber, source, round) {
5178
5521
  const extras = typeof round === "number" ? { piercing: round } : round ?? {};
5179
5522
  return withCollapses(run(() => {
@@ -5818,7 +6161,12 @@ var GameDestruction = (() => {
5818
6161
  cactus: "cactus",
5819
6162
  flesh_fruit: "fruit",
5820
6163
  wax: "wax",
5821
- soil: "soil"
6164
+ soil: "soil",
6165
+ asphalt: "asphalt",
6166
+ sand: "sand",
6167
+ gravel: "gravel",
6168
+ turf: "turf",
6169
+ mortar: "mortar"
5822
6170
  },
5823
6171
  partStatus: { intact: "intact", damaged: "damaged", collapsed: "collapsed", broken: "broken", melted: "melted", ashes: "ashes" },
5824
6172
  jointKinds: {
@@ -5909,7 +6257,12 @@ var GameDestruction = (() => {
5909
6257
  cactus: "cacto",
5910
6258
  flesh_fruit: "fruta",
5911
6259
  wax: "cera",
5912
- soil: "terra"
6260
+ soil: "terra",
6261
+ asphalt: "asfalto",
6262
+ sand: "areia",
6263
+ gravel: "brita",
6264
+ turf: "grama",
6265
+ mortar: "argamassa"
5913
6266
  },
5914
6267
  partStatus: { intact: "intacto", damaged: "danificado", collapsed: "desabado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
5915
6268
  jointKinds: {