@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.
package/dist/index.js CHANGED
@@ -376,7 +376,9 @@ var severAt = (inp, root, at, gap) => {
376
376
  const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
377
377
  band.c[L] = (lo + hi) / 2;
378
378
  band.s[L] = Math.max(hi - lo, 0.01);
379
- for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
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] });
381
+ } else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
380
382
  return out;
381
383
  };
382
384
  var tearStack = (inp, root, t, u, v, n, sheet) => {
@@ -407,6 +409,7 @@ var planFragments = (inp) => {
407
409
  const t = sorted[0];
408
410
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
409
411
  if (style === "snap") {
412
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
410
413
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
411
414
  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] });
412
415
  return out;
@@ -586,6 +589,27 @@ var rebarBars = (dims, thin, spacing) => {
586
589
  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]]));
587
590
  return out;
588
591
  };
592
+ var clipBarsToBox = (bars, u, v, center, size) => {
593
+ const out = [];
594
+ for (const [a, b] of bars) {
595
+ const axis = a[u] !== b[u] ? u : v;
596
+ const other = axis === u ? v : u;
597
+ if (Math.abs(a[other] - center[other]) > size[other] / 2) continue;
598
+ 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);
599
+ if (hi - lo < 1e-6) continue;
600
+ const p = [a[0], a[1], a[2]], q = [b[0], b[1], b[2]];
601
+ p[axis] = lo;
602
+ q[axis] = hi;
603
+ out.push([p, q]);
604
+ }
605
+ return out;
606
+ };
607
+ var bendBar = ([a, b], thin, bow, i) => {
608
+ const length = Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
609
+ const m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2];
610
+ m[thin] += (i % 2 === 0 ? 1 : -1) * bow * length;
611
+ return [[a, m], [m, b]];
612
+ };
589
613
  var clipBarsToDisc = (bars, u, v, center, radius) => {
590
614
  const out = [];
591
615
  for (const [a, b] of bars) {
@@ -1563,14 +1587,21 @@ var materials_default = [
1563
1587
  smoke: "grey"
1564
1588
  },
1565
1589
  visual: {
1566
- charColor: 1709072
1590
+ charColor: 1709072,
1591
+ innerColor: 14730131
1567
1592
  },
1568
1593
  tags: [
1569
1594
  "wood",
1570
1595
  "organic",
1571
1596
  "flammable"
1572
1597
  ],
1573
- compressive: 1e5
1598
+ compressive: 1e5,
1599
+ hits: {
1600
+ blunt: {
1601
+ tear: "splinters",
1602
+ tearJoules: 60
1603
+ }
1604
+ }
1574
1605
  },
1575
1606
  {
1576
1607
  id: "wood_hard",
@@ -1600,14 +1631,21 @@ var materials_default = [
1600
1631
  smoke: "grey"
1601
1632
  },
1602
1633
  visual: {
1603
- charColor: 1709072
1634
+ charColor: 1709072,
1635
+ innerColor: 13213802
1604
1636
  },
1605
1637
  tags: [
1606
1638
  "wood",
1607
1639
  "organic",
1608
1640
  "flammable"
1609
1641
  ],
1610
- compressive: 15e4
1642
+ compressive: 15e4,
1643
+ hits: {
1644
+ blunt: {
1645
+ tear: "splinters",
1646
+ tearJoules: 150
1647
+ }
1648
+ }
1611
1649
  },
1612
1650
  {
1613
1651
  id: "plywood",
@@ -1637,14 +1675,24 @@ var materials_default = [
1637
1675
  smoke: "grey"
1638
1676
  },
1639
1677
  visual: {
1640
- charColor: 1709072
1678
+ charColor: 1709072,
1679
+ innerColor: 14862234
1641
1680
  },
1642
1681
  tags: [
1643
1682
  "wood",
1644
1683
  "organic",
1645
1684
  "flammable"
1646
1685
  ],
1647
- compressive: 6e4
1686
+ compressive: 6e4,
1687
+ hits: {
1688
+ ballistic: {
1689
+ tear: "splinters"
1690
+ },
1691
+ blunt: {
1692
+ tear: "splinters",
1693
+ tearJoules: 80
1694
+ }
1695
+ }
1648
1696
  },
1649
1697
  {
1650
1698
  id: "mdf",
@@ -1671,14 +1719,25 @@ var materials_default = [
1671
1719
  smoke: "grey"
1672
1720
  },
1673
1721
  visual: {
1674
- charColor: 1709072
1722
+ charColor: 1709072,
1723
+ innerColor: 12096362
1675
1724
  },
1676
1725
  tags: [
1677
1726
  "wood",
1678
1727
  "organic",
1679
1728
  "flammable"
1680
1729
  ],
1681
- compressive: 3e4
1730
+ compressive: 3e4,
1731
+ hits: {
1732
+ ballistic: {
1733
+ tear: "lumps",
1734
+ tearJoules: 120
1735
+ },
1736
+ blunt: {
1737
+ tear: "lumps",
1738
+ tearJoules: 20
1739
+ }
1740
+ }
1682
1741
  },
1683
1742
  {
1684
1743
  id: "glass",
@@ -1792,8 +1851,20 @@ var materials_default = [
1792
1851
  damageScale: {
1793
1852
  explosive: 1.2
1794
1853
  },
1854
+ hits: {
1855
+ ballistic: {
1856
+ unitJoules: 500,
1857
+ floor: 900,
1858
+ crater: true
1859
+ },
1860
+ blunt: {
1861
+ unitJoules: 1500,
1862
+ floor: 60
1863
+ }
1864
+ },
1795
1865
  hitDebris: "chips",
1796
1866
  breakDebris: "dust",
1867
+ visual: { innerColor: 10197652 },
1797
1868
  thermal: {
1798
1869
  conductivity: 0.15,
1799
1870
  heatCapacity: 2.5
@@ -1824,6 +1895,7 @@ var materials_default = [
1824
1895
  ],
1825
1896
  hitDebris: "chips",
1826
1897
  breakDebris: "dust",
1898
+ visual: { innerColor: 12085567 },
1827
1899
  thermal: {
1828
1900
  conductivity: 0.15,
1829
1901
  heatCapacity: 2.5
@@ -1854,6 +1926,7 @@ var materials_default = [
1854
1926
  ],
1855
1927
  hitDebris: "dust",
1856
1928
  breakDebris: "dust",
1929
+ visual: { innerColor: 14999507 },
1857
1930
  thermal: {
1858
1931
  conductivity: 0.1,
1859
1932
  heatCapacity: 1.5
@@ -1879,6 +1952,7 @@ var materials_default = [
1879
1952
  ],
1880
1953
  hitDebris: "chips",
1881
1954
  breakDebris: "dust",
1955
+ visual: { innerColor: 9407364 },
1882
1956
  thermal: {
1883
1957
  conductivity: 0.2,
1884
1958
  heatCapacity: 2.5
@@ -2160,7 +2234,8 @@ var materials_default = [
2160
2234
  smoke: "white"
2161
2235
  },
2162
2236
  visual: {
2163
- charColor: 1709072
2237
+ charColor: 1709072,
2238
+ innerColor: 14207126
2164
2239
  },
2165
2240
  tags: [
2166
2241
  "plant",
@@ -2168,7 +2243,12 @@ var materials_default = [
2168
2243
  "organic",
2169
2244
  "flammable"
2170
2245
  ],
2171
- compressive: 6e4
2246
+ compressive: 6e4,
2247
+ hits: {
2248
+ blunt: {
2249
+ tear: "none"
2250
+ }
2251
+ }
2172
2252
  },
2173
2253
  {
2174
2254
  id: "cactus",
@@ -2283,6 +2363,130 @@ var materials_default = [
2283
2363
  "masonry"
2284
2364
  ],
2285
2365
  compressive: 2e4
2366
+ },
2367
+ {
2368
+ id: "asphalt",
2369
+ density: 2350,
2370
+ hpPerKg: 10,
2371
+ penetration: 14,
2372
+ penetrationClass: "concrete",
2373
+ penetrationScale: 0.8,
2374
+ ductility: 0.1,
2375
+ fracture: "crumble",
2376
+ fragmentSize: [
2377
+ 0.03,
2378
+ 0.15
2379
+ ],
2380
+ hitDebris: "chips",
2381
+ breakDebris: "dust",
2382
+ thermal: {
2383
+ conductivity: 0.1,
2384
+ heatCapacity: 2,
2385
+ ignitionC: 400,
2386
+ burnRate: 2e-3,
2387
+ smoke: "black"
2388
+ },
2389
+ tags: [
2390
+ "masonry",
2391
+ "brittle"
2392
+ ],
2393
+ compressive: 3e4
2394
+ },
2395
+ {
2396
+ id: "sand",
2397
+ density: 1600,
2398
+ hpPerKg: 2,
2399
+ penetration: 6,
2400
+ ductility: 0.4,
2401
+ fracture: "crumble",
2402
+ fragmentSize: [
2403
+ 0.01,
2404
+ 0.04
2405
+ ],
2406
+ hitDebris: "dust",
2407
+ breakDebris: "dust",
2408
+ thermal: {
2409
+ conductivity: 0.1,
2410
+ heatCapacity: 2
2411
+ },
2412
+ tags: [
2413
+ "soil"
2414
+ ],
2415
+ compressive: 1e4
2416
+ },
2417
+ {
2418
+ id: "gravel",
2419
+ density: 1800,
2420
+ hpPerKg: 3,
2421
+ penetration: 8,
2422
+ ductility: 0.3,
2423
+ fracture: "crumble",
2424
+ fragmentSize: [
2425
+ 0.02,
2426
+ 0.06
2427
+ ],
2428
+ hitDebris: "chips",
2429
+ breakDebris: "dust",
2430
+ thermal: {
2431
+ conductivity: 0.1,
2432
+ heatCapacity: 2
2433
+ },
2434
+ tags: [
2435
+ "soil",
2436
+ "masonry"
2437
+ ],
2438
+ compressive: 2e4
2439
+ },
2440
+ {
2441
+ id: "turf",
2442
+ density: 900,
2443
+ hpPerKg: 3,
2444
+ penetration: 4,
2445
+ ductility: 0.5,
2446
+ fracture: "crumble",
2447
+ fragmentSize: [
2448
+ 0.03,
2449
+ 0.1
2450
+ ],
2451
+ hitDebris: "dust",
2452
+ breakDebris: "dust",
2453
+ thermal: {
2454
+ conductivity: 0.1,
2455
+ heatCapacity: 1.5,
2456
+ ignitionC: 250,
2457
+ burnRate: 0.03,
2458
+ smoke: "white"
2459
+ },
2460
+ tags: [
2461
+ "soil",
2462
+ "organic"
2463
+ ],
2464
+ compressive: 15e3
2465
+ },
2466
+ {
2467
+ id: "mortar",
2468
+ density: 1900,
2469
+ hpPerKg: 10,
2470
+ penetration: 12,
2471
+ penetrationClass: "concrete",
2472
+ penetrationScale: 1.2,
2473
+ ductility: 0.03,
2474
+ fracture: "crumble",
2475
+ fragmentSize: [
2476
+ 0.02,
2477
+ 0.08
2478
+ ],
2479
+ hitDebris: "chips",
2480
+ breakDebris: "dust",
2481
+ thermal: {
2482
+ conductivity: 0.15,
2483
+ heatCapacity: 2.5
2484
+ },
2485
+ tags: [
2486
+ "masonry",
2487
+ "brittle"
2488
+ ],
2489
+ compressive: 1e5
2286
2490
  }
2287
2491
  ];
2288
2492
 
@@ -2468,6 +2672,32 @@ var fragility = (ductility, thickness) => {
2468
2672
  // src/world.ts
2469
2673
  import { ignitionChance, resolveIgnitionRules, sparkTargetFactor } from "@adatechnology/game-contracts";
2470
2674
  var THIN_SHEET_DISHING = 5e-3;
2675
+ var SWEEP_BACK = 0.01;
2676
+ function sweepOrigins(box, d, spacing) {
2677
+ const origins = [];
2678
+ const line = (from, to) => {
2679
+ const n = Math.max(1, Math.ceil((to - from) / spacing));
2680
+ return Array.from({ length: n + 1 }, (_, i) => from + (to - from) * i / n);
2681
+ };
2682
+ for (const axis of [0, 1, 2]) {
2683
+ if (Math.abs(d[axis]) < 1e-3) continue;
2684
+ const lead = d[axis] > 0 ? box.max[axis] - SWEEP_BACK : box.min[axis] + SWEEP_BACK;
2685
+ const u = (axis + 1) % 3;
2686
+ const v = (axis + 2) % 3;
2687
+ for (const a of line(box.min[u], box.max[u])) {
2688
+ for (const b of line(box.min[v], box.max[v])) {
2689
+ const point = [0, 0, 0];
2690
+ point[axis] = lead;
2691
+ point[u] = a;
2692
+ point[v] = b;
2693
+ origins.push(point);
2694
+ }
2695
+ }
2696
+ }
2697
+ return origins;
2698
+ }
2699
+ var THIN_SHEET_CRUSH = 5e-4;
2700
+ var THIN_SHEET_CONTACT = 0.025;
2471
2701
  var DMG_PER_J = 0.02;
2472
2702
  var BLAST_COUPLING = 0.1;
2473
2703
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
@@ -2484,11 +2714,15 @@ var RESIDUAL_CAPACITY = 0.15;
2484
2714
  var SEVER_BAND = 0.1;
2485
2715
  var SEVER_ASPECT = 3;
2486
2716
  var SEVER_BALLISTIC = 2;
2717
+ var TEAR_JOULES = 150;
2718
+ var tears = 0;
2487
2719
  var SUPPORT_TOLERANCE = 0.05;
2488
2720
  var UNIT_FRAGMENT_FACTOR = 4;
2489
2721
  var BREACH_GRID_CAP = 4096;
2490
2722
  var CHIP_ENERGY = 200;
2491
2723
  var CHIP_SPLASH = 0.5;
2724
+ var BAR_BOW = 0.08;
2725
+ var BULLET_JOINT_SPAN = 0.3;
2492
2726
  var CUT_SLICE = 1;
2493
2727
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
2494
2728
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -2498,8 +2732,24 @@ var penetrationOf = (mat, table) => {
2498
2732
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
2499
2733
  };
2500
2734
  var capacityScale = (p) => {
2501
- if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
2502
- return p.rt.removed ? Math.max(0.05, 1 - p.rt.removed / p.mass0) : 1;
2735
+ const residual = p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
2736
+ if (p.st.stripped) return residual;
2737
+ const kept = Math.min(p.rt.removed ? 1 - p.rt.removed / p.mass0 : 1, 1 - sectionLoss(p));
2738
+ return Math.max(p.spec.reinforcement ? residual : 0.05, kept);
2739
+ };
2740
+ var sectionLoss = (p) => {
2741
+ const cells = p.st.chipped;
2742
+ if (!cells?.length) return 0;
2743
+ const [u, v] = planeAxes(p.thin);
2744
+ const along = p.dims[u] >= p.dims[v] ? u : v;
2745
+ const across = along === u ? v : u;
2746
+ let worst = 0;
2747
+ for (const c of cells) {
2748
+ let gone = 0;
2749
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across];
2750
+ worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
2751
+ }
2752
+ return Math.min(1, worst);
2503
2753
  };
2504
2754
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
2505
2755
  var clone = (v) => structuredClone(v);
@@ -3599,7 +3849,7 @@ var createDestruction = (options = {}) => {
3599
3849
  emit({ ...base(o, src), type: "crack", part: p.spec.id, crack, worldPoint: wp });
3600
3850
  setStage(o, p, 1, {}, src);
3601
3851
  };
3602
- 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 });
3852
+ 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 } : {} });
3603
3853
  const holeRate = (hole, pressurized) => (pressurized ? 0.5 : 0.1) * (hole.diameter / 9e-3);
3604
3854
  const detachEvents = (o, opened, by, src) => {
3605
3855
  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]) });
@@ -3640,7 +3890,13 @@ var createDestruction = (options = {}) => {
3640
3890
  const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
3641
3891
  return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
3642
3892
  };
3643
- const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false) => {
3893
+ const jointLength = (o, j) => {
3894
+ const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
3895
+ let len = 0;
3896
+ 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]));
3897
+ return len;
3898
+ };
3899
+ const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false, span) => {
3644
3900
  if (amount <= 0) return;
3645
3901
  const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
3646
3902
  if (split && mine.length) amount /= mine.length;
@@ -3648,7 +3904,7 @@ var createDestruction = (options = {}) => {
3648
3904
  if (j.st.open) continue;
3649
3905
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
3650
3906
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
3651
- j.st.hp -= amount;
3907
+ j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
3652
3908
  if (j.st.hp <= 0) {
3653
3909
  j.st.hp = 0;
3654
3910
  j.st.open = true;
@@ -3677,7 +3933,13 @@ var createDestruction = (options = {}) => {
3677
3933
  if (spalls.length > MAX_SPALLS) spalls.shift();
3678
3934
  markDamaged(p);
3679
3935
  emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: layer.index, point: wp, area: Math.PI * radius * radius });
3680
- debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
3936
+ debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src, layer.mat.id);
3937
+ setStage(o, p, 1, {}, src);
3938
+ };
3939
+ const crater = (o, p, wp, normal, radius, src) => {
3940
+ markDamaged(p);
3941
+ emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: -1, point: wp, area: Math.PI * radius * radius });
3942
+ debris(o, p, p.mat.breakDebris, wp, normal, 6, 1.5, src);
3681
3943
  setStage(o, p, 1, {}, src);
3682
3944
  };
3683
3945
  const insideOpening = (o, p, objectPoint) => {
@@ -3756,9 +4018,13 @@ var createDestruction = (options = {}) => {
3756
4018
  const lp = toLocal(p.pos, p.q, objectPoint);
3757
4019
  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);
3758
4020
  };
3759
- const chipUnits = (o, p, wp, energy, dir, radius, src) => {
4021
+ const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
3760
4022
  var _a, _b;
3761
- if (!p.spec.units || p.spec.reinforcement || !p.staged || chipEnergy <= 0 || energy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
4023
+ if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
4024
+ const rule = p.mat.hits?.[kind];
4025
+ const perKg = rule?.unitJoules ?? chipEnergy;
4026
+ const energy = hitEnergy - (rule?.floor ?? 0);
4027
+ if (perKg <= 0 || energy <= 0) return;
3762
4028
  const [u, v] = planeAxes(p.thin);
3763
4029
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
3764
4030
  const grid = gridOf(p);
@@ -3772,7 +4038,7 @@ var createDestruction = (options = {}) => {
3772
4038
  const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
3773
4039
  const total = (worn[i] ?? 0) + energy * share;
3774
4040
  const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
3775
- if (total < chipEnergy * mass) {
4041
+ if (total < perKg * mass) {
3776
4042
  worn[i] = total;
3777
4043
  continue;
3778
4044
  }
@@ -3797,7 +4063,32 @@ var createDestruction = (options = {}) => {
3797
4063
  emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
3798
4064
  debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
3799
4065
  }
4066
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
3800
4067
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
4068
+ if (p.structural && !isLost(p)) {
4069
+ solveStructure(o);
4070
+ collapseCheck(o, src);
4071
+ }
4072
+ };
4073
+ const exposeRebar = (o, p, src) => {
4074
+ const r = p.spec.reinforcement;
4075
+ if (!r) return;
4076
+ const [u, v] = planeAxes(p.thin);
4077
+ const all = rebarBars(p.dims, p.thin, r.spacing);
4078
+ let bars;
4079
+ if (p.st.stripped) bars = all;
4080
+ else {
4081
+ bars = [];
4082
+ for (const cell of p.st.chipped ?? []) bars.push(...clipBarsToBox(all, u, v, cell.center, cell.size));
4083
+ const op = p.st.opening;
4084
+ if (op) {
4085
+ const lc = toLocal(p.pos, p.q, op.center);
4086
+ bars.push(...clipBarsToDisc(all, u, v, [lc[u], lc[v]], op.radius + 0.05));
4087
+ }
4088
+ }
4089
+ const bow = clamp2(((p.st.stress ?? 0) - 1) / 0.5, 0, 1);
4090
+ if (bow > 0) bars = bars.flatMap((bar2, i) => bendBar(bar2, p.thin, bow * BAR_BOW, i));
4091
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
3801
4092
  };
3802
4093
  const openBreach = (o, p, center, total, energy, dir, src) => {
3803
4094
  const [u, v] = planeAxes(p.thin);
@@ -3825,10 +4116,7 @@ var createDestruction = (options = {}) => {
3825
4116
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
3826
4117
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
3827
4118
  const reinforcement = p.spec.reinforcement;
3828
- if (reinforcement) {
3829
- const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
3830
- if (bars.length > 0) emit({ ...base(o, src), type: "expose", part: p.spec.id, bars });
3831
- }
4119
+ if (reinforcement) exposeRebar(o, p, src);
3832
4120
  if (p.structural) solveStructure(o);
3833
4121
  };
3834
4122
  const stripPart = (o, p, reinforcement, src) => {
@@ -3840,8 +4128,8 @@ var createDestruction = (options = {}) => {
3840
4128
  p.st.burning = false;
3841
4129
  p.rt.jet = false;
3842
4130
  markDamaged(p);
3843
- emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
3844
4131
  solveStructure(o);
4132
+ exposeRebar(o, p, src);
3845
4133
  collapseCheck(o, src);
3846
4134
  };
3847
4135
  const unitFragmentPlan = (p, cap) => {
@@ -3895,7 +4183,7 @@ var createDestruction = (options = {}) => {
3895
4183
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
3896
4184
  const hitLocal = toLocal(c, q, hitW);
3897
4185
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
3898
- const axis = style === "splinter" ? severAxis(p) : -1;
4186
+ const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
3899
4187
  const cutAt = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
3900
4188
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
3901
4189
  style,
@@ -3967,7 +4255,7 @@ var createDestruction = (options = {}) => {
3967
4255
  };
3968
4256
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
3969
4257
  const severAxis = (p) => {
3970
- if (p.mat.fracture !== "splinter" || p.spec.shape.kind !== "box") return -1;
4258
+ if (p.mat.fracture !== "splinter" && p.mat.fracture !== "snap" || p.spec.shape.kind !== "box") return -1;
3971
4259
  const d = p.dims;
3972
4260
  const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
3973
4261
  return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
@@ -4028,6 +4316,49 @@ var createDestruction = (options = {}) => {
4028
4316
  }
4029
4317
  }
4030
4318
  };
4319
+ const tearOf = (mat, kind) => {
4320
+ const h = mat.hits?.[kind];
4321
+ const tear = h?.tear ?? (kind === "ballistic" && mat.penetrationClass === "wood" ? "splinters" : "none");
4322
+ return { tear, joules: h?.tearJoules ?? TEAR_JOULES };
4323
+ };
4324
+ const tearAt = (o, p, kind, entry, exit, d, E, scale, src) => {
4325
+ const { tear, joules } = tearOf(p.mat, kind);
4326
+ if (tear === "none") return;
4327
+ const shot = kind === "ballistic";
4328
+ const n = shot ? exit ? clamp2(Math.round(E / joules), 1, 7) : 1 : clamp2(Math.floor(E / joules), 0, 5);
4329
+ if (n === 0) return;
4330
+ const fibre = tear === "lumps";
4331
+ const { q } = partXf(o, p);
4332
+ const along = (p.spec.grain ? AXIS[p.spec.grain] : void 0) ?? [0, 1, 2].sort((a, b) => p.dims[b] - p.dims[a])[0];
4333
+ const fragments = [];
4334
+ for (let i = 0; i < n; i++) {
4335
+ const back = !exit || i % 3 === 2;
4336
+ 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());
4337
+ const size = [w, w, w];
4338
+ size[along] = len;
4339
+ size[(along + 1) % 3] = w;
4340
+ size[(along + 2) % 3] = t;
4341
+ const spread3 = [(rnd() - 0.5) * 0.8, (rnd() - 0.2) * 0.8, (rnd() - 0.5) * 0.8];
4342
+ const speed = shot ? back ? 1.5 + 2 * rnd() : 2 + 4 * rnd() : 0.8 + 1.2 * rnd();
4343
+ const v = vec.scale(vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3)), speed);
4344
+ const mass = size[0] * size[1] * size[2] * p.mat.density;
4345
+ fragments.push({
4346
+ id: `${p.spec.id}#t${tears++}-${i}`,
4347
+ part: p.spec.id,
4348
+ material: p.spec.material,
4349
+ shape: fibre ? "chunk" : "splinter",
4350
+ center: back || !exit ? vec.madd(entry, d, -0.01) : vec.madd(exit, d, 0.01),
4351
+ size,
4352
+ rotation: q,
4353
+ velocity: v,
4354
+ inner: true,
4355
+ angularVelocity: [(rnd() - 0.5) * 30, (rnd() - 0.5) * 30, (rnd() - 0.5) * 30],
4356
+ mass
4357
+ });
4358
+ }
4359
+ const patch = [scale * 4, scale * 4, scale * 4];
4360
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry, size: patch, rotation: q, fragments });
4361
+ };
4031
4362
  const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src, round = {}) => {
4032
4363
  const mat = p.mat;
4033
4364
  const piercing = round.piercing ?? 1;
@@ -4056,6 +4387,7 @@ var createDestruction = (options = {}) => {
4056
4387
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
4057
4388
  }
4058
4389
  if (p.layers.length > 0) spallOuter(o, p, entry, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
4390
+ else if (mat.hits?.ballistic?.crater) crater(o, p, entry, n, clamp2(0.02 + Math.sqrt(E) * 15e-4, 0.02, 0.15), src);
4059
4391
  const brittle = mat.ductility < 0.3;
4060
4392
  const ratio = Ep / Math.max(thr, 1e-6);
4061
4393
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
@@ -4088,9 +4420,11 @@ var createDestruction = (options = {}) => {
4088
4420
  if (mat.fracture !== "shatter") addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
4089
4421
  } else addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
4090
4422
  debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
4091
- wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
4092
- const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry, d, E * 0.1, 12, src);
4093
- chipUnits(o, p, entry, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
4423
+ wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
4424
+ const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
4425
+ const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry, d, absorbed * 0.1, 12, src);
4426
+ if (!isLost(p)) tearAt(o, p, "ballistic", entry, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
4427
+ 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);
4094
4428
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
4095
4429
  };
4096
4430
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -4098,14 +4432,20 @@ var createDestruction = (options = {}) => {
4098
4432
  const thrMm = mat.penetration * p.thickness * 1e3;
4099
4433
  const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
4100
4434
  const Ec = E * fragile;
4435
+ const sheet = mat.ductility >= 0.5 && p.thickness <= THIN_SHEET_CRUSH;
4436
+ const fold = sheet ? Math.min(4, Math.sqrt(1e-3 / p.thickness)) : 1;
4437
+ const patch = sheet ? Math.min(contactR, THIN_SHEET_CONTACT) : contactR;
4101
4438
  if (E > 0) {
4102
4439
  if (kind === "blunt") {
4103
- if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp2(E * 1e-4, 1e-3, 0.06), src);
4440
+ 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);
4104
4441
  else if (mat.ductility < 0.3) {
4105
4442
  if (Ec >= 5) addCrack(o, p, point, n, clamp2(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
4106
4443
  } 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);
4107
4444
  } else if (mat.fracture === "tear") {
4108
4445
  addHole(o, p, point, n, clamp2(0.02 + E * 5e-4, 0.02, 0.3), true, void 0, src);
4446
+ } else if (mat.ductility >= 0.3 && E >= thrMm) {
4447
+ addHole(o, p, point, n, clamp2(contactR, 0.01, 0.08), true, void 0, src);
4448
+ if (sheet) addDent(o, p, point, n, Math.max(patch * 0.6, 0.01), clamp2(E * 4e-5 * fold, 5e-4, 0.02), src);
4109
4449
  } else if (mat.ductility >= 0.5) {
4110
4450
  addDent(o, p, point, n, Math.max(contactR, 0.01), clamp2(E * 4e-5, 5e-4, 0.02), src);
4111
4451
  }
@@ -4118,7 +4458,8 @@ var createDestruction = (options = {}) => {
4118
4458
  const scale = mat.damageScale?.[kind] ?? 1;
4119
4459
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
4120
4460
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
4121
- if (kind === "blunt") chipUnits(o, p, point, E, d, Math.max(contactR, 0.03), src);
4461
+ if (kind === "blunt") chipUnits(o, p, "blunt", point, E, d, Math.max(contactR, 0.03), src);
4462
+ if (E > 0 && !isLost(p)) tearAt(o, p, kind, point, void 0, d, E, clamp2(contactR * 0.2, 6e-3, 0.02), src);
4122
4463
  return done;
4123
4464
  };
4124
4465
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -4697,6 +5038,7 @@ var createDestruction = (options = {}) => {
4697
5038
  if (p.rt.stressT <= 0) {
4698
5039
  p.rt.stressT = 0.5;
4699
5040
  emit({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
5041
+ if (p.spec.reinforcement && (p.st.stripped || p.st.chipped?.length || p.st.opening)) exposeRebar(o, p);
4700
5042
  }
4701
5043
  applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
4702
5044
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
@@ -4913,6 +5255,18 @@ var createDestruction = (options = {}) => {
4913
5255
  const r = raycastInternal(origin, direction, maxDistance);
4914
5256
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
4915
5257
  },
5258
+ sweepBox(box, direction, distance, options2 = {}) {
5259
+ const d = vec.normalize(direction);
5260
+ const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
5261
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
5262
+ let best = null;
5263
+ for (const origin of sweepOrigins(box, d, options2.spacing ?? 0.2)) {
5264
+ const r = raycastInternal(origin, d, distance + SWEEP_BACK, ignore);
5265
+ if (!r || best && r.distance >= best.distance) continue;
5266
+ best = { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: Math.max(0, r.distance - SWEEP_BACK) };
5267
+ }
5268
+ return best;
5269
+ },
4916
5270
  shoot(origin, direction, energy, caliber, source, round) {
4917
5271
  const extras = typeof round === "number" ? { piercing: round } : round ?? {};
4918
5272
  return withCollapses(run(() => {
@@ -5557,7 +5911,12 @@ var locales = {
5557
5911
  cactus: "cactus",
5558
5912
  flesh_fruit: "fruit",
5559
5913
  wax: "wax",
5560
- soil: "soil"
5914
+ soil: "soil",
5915
+ asphalt: "asphalt",
5916
+ sand: "sand",
5917
+ gravel: "gravel",
5918
+ turf: "turf",
5919
+ mortar: "mortar"
5561
5920
  },
5562
5921
  partStatus: { intact: "intact", damaged: "damaged", collapsed: "collapsed", broken: "broken", melted: "melted", ashes: "ashes" },
5563
5922
  jointKinds: {
@@ -5648,7 +6007,12 @@ var locales = {
5648
6007
  cactus: "cacto",
5649
6008
  flesh_fruit: "fruta",
5650
6009
  wax: "cera",
5651
- soil: "terra"
6010
+ soil: "terra",
6011
+ asphalt: "asfalto",
6012
+ sand: "areia",
6013
+ gravel: "brita",
6014
+ turf: "grama",
6015
+ mortar: "argamassa"
5652
6016
  },
5653
6017
  partStatus: { intact: "intacto", damaged: "danificado", collapsed: "desabado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
5654
6018
  jointKinds: {