@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.
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,6 +1851,17 @@ 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",
1797
1867
  thermal: {
@@ -2160,7 +2230,8 @@ var materials_default = [
2160
2230
  smoke: "white"
2161
2231
  },
2162
2232
  visual: {
2163
- charColor: 1709072
2233
+ charColor: 1709072,
2234
+ innerColor: 14207126
2164
2235
  },
2165
2236
  tags: [
2166
2237
  "plant",
@@ -2168,7 +2239,12 @@ var materials_default = [
2168
2239
  "organic",
2169
2240
  "flammable"
2170
2241
  ],
2171
- compressive: 6e4
2242
+ compressive: 6e4,
2243
+ hits: {
2244
+ blunt: {
2245
+ tear: "none"
2246
+ }
2247
+ }
2172
2248
  },
2173
2249
  {
2174
2250
  id: "cactus",
@@ -2283,6 +2359,130 @@ var materials_default = [
2283
2359
  "masonry"
2284
2360
  ],
2285
2361
  compressive: 2e4
2362
+ },
2363
+ {
2364
+ id: "asphalt",
2365
+ density: 2350,
2366
+ hpPerKg: 10,
2367
+ penetration: 14,
2368
+ penetrationClass: "concrete",
2369
+ penetrationScale: 0.8,
2370
+ ductility: 0.1,
2371
+ fracture: "crumble",
2372
+ fragmentSize: [
2373
+ 0.03,
2374
+ 0.15
2375
+ ],
2376
+ hitDebris: "chips",
2377
+ breakDebris: "dust",
2378
+ thermal: {
2379
+ conductivity: 0.1,
2380
+ heatCapacity: 2,
2381
+ ignitionC: 400,
2382
+ burnRate: 2e-3,
2383
+ smoke: "black"
2384
+ },
2385
+ tags: [
2386
+ "masonry",
2387
+ "brittle"
2388
+ ],
2389
+ compressive: 3e4
2390
+ },
2391
+ {
2392
+ id: "sand",
2393
+ density: 1600,
2394
+ hpPerKg: 2,
2395
+ penetration: 6,
2396
+ ductility: 0.4,
2397
+ fracture: "crumble",
2398
+ fragmentSize: [
2399
+ 0.01,
2400
+ 0.04
2401
+ ],
2402
+ hitDebris: "dust",
2403
+ breakDebris: "dust",
2404
+ thermal: {
2405
+ conductivity: 0.1,
2406
+ heatCapacity: 2
2407
+ },
2408
+ tags: [
2409
+ "soil"
2410
+ ],
2411
+ compressive: 1e4
2412
+ },
2413
+ {
2414
+ id: "gravel",
2415
+ density: 1800,
2416
+ hpPerKg: 3,
2417
+ penetration: 8,
2418
+ ductility: 0.3,
2419
+ fracture: "crumble",
2420
+ fragmentSize: [
2421
+ 0.02,
2422
+ 0.06
2423
+ ],
2424
+ hitDebris: "chips",
2425
+ breakDebris: "dust",
2426
+ thermal: {
2427
+ conductivity: 0.1,
2428
+ heatCapacity: 2
2429
+ },
2430
+ tags: [
2431
+ "soil",
2432
+ "masonry"
2433
+ ],
2434
+ compressive: 2e4
2435
+ },
2436
+ {
2437
+ id: "turf",
2438
+ density: 900,
2439
+ hpPerKg: 3,
2440
+ penetration: 4,
2441
+ ductility: 0.5,
2442
+ fracture: "crumble",
2443
+ fragmentSize: [
2444
+ 0.03,
2445
+ 0.1
2446
+ ],
2447
+ hitDebris: "dust",
2448
+ breakDebris: "dust",
2449
+ thermal: {
2450
+ conductivity: 0.1,
2451
+ heatCapacity: 1.5,
2452
+ ignitionC: 250,
2453
+ burnRate: 0.03,
2454
+ smoke: "white"
2455
+ },
2456
+ tags: [
2457
+ "soil",
2458
+ "organic"
2459
+ ],
2460
+ compressive: 15e3
2461
+ },
2462
+ {
2463
+ id: "mortar",
2464
+ density: 1900,
2465
+ hpPerKg: 10,
2466
+ penetration: 12,
2467
+ penetrationClass: "concrete",
2468
+ penetrationScale: 1.2,
2469
+ ductility: 0.03,
2470
+ fracture: "crumble",
2471
+ fragmentSize: [
2472
+ 0.02,
2473
+ 0.08
2474
+ ],
2475
+ hitDebris: "chips",
2476
+ breakDebris: "dust",
2477
+ thermal: {
2478
+ conductivity: 0.15,
2479
+ heatCapacity: 2.5
2480
+ },
2481
+ tags: [
2482
+ "masonry",
2483
+ "brittle"
2484
+ ],
2485
+ compressive: 1e5
2286
2486
  }
2287
2487
  ];
2288
2488
 
@@ -2468,6 +2668,32 @@ var fragility = (ductility, thickness) => {
2468
2668
  // src/world.ts
2469
2669
  import { ignitionChance, resolveIgnitionRules, sparkTargetFactor } from "@adatechnology/game-contracts";
2470
2670
  var THIN_SHEET_DISHING = 5e-3;
2671
+ var SWEEP_BACK = 0.01;
2672
+ function sweepOrigins(box, d, spacing) {
2673
+ const origins = [];
2674
+ const line = (from, to) => {
2675
+ const n = Math.max(1, Math.ceil((to - from) / spacing));
2676
+ return Array.from({ length: n + 1 }, (_, i) => from + (to - from) * i / n);
2677
+ };
2678
+ for (const axis of [0, 1, 2]) {
2679
+ if (Math.abs(d[axis]) < 1e-3) continue;
2680
+ const lead = d[axis] > 0 ? box.max[axis] - SWEEP_BACK : box.min[axis] + SWEEP_BACK;
2681
+ const u = (axis + 1) % 3;
2682
+ const v = (axis + 2) % 3;
2683
+ for (const a of line(box.min[u], box.max[u])) {
2684
+ for (const b of line(box.min[v], box.max[v])) {
2685
+ const point = [0, 0, 0];
2686
+ point[axis] = lead;
2687
+ point[u] = a;
2688
+ point[v] = b;
2689
+ origins.push(point);
2690
+ }
2691
+ }
2692
+ }
2693
+ return origins;
2694
+ }
2695
+ var THIN_SHEET_CRUSH = 5e-4;
2696
+ var THIN_SHEET_CONTACT = 0.025;
2471
2697
  var DMG_PER_J = 0.02;
2472
2698
  var BLAST_COUPLING = 0.1;
2473
2699
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
@@ -2484,11 +2710,14 @@ var RESIDUAL_CAPACITY = 0.15;
2484
2710
  var SEVER_BAND = 0.1;
2485
2711
  var SEVER_ASPECT = 3;
2486
2712
  var SEVER_BALLISTIC = 2;
2713
+ var TEAR_JOULES = 150;
2714
+ var tears = 0;
2487
2715
  var SUPPORT_TOLERANCE = 0.05;
2488
2716
  var UNIT_FRAGMENT_FACTOR = 4;
2489
2717
  var BREACH_GRID_CAP = 4096;
2490
2718
  var CHIP_ENERGY = 200;
2491
2719
  var CHIP_SPLASH = 0.5;
2720
+ var BAR_BOW = 0.08;
2492
2721
  var CUT_SLICE = 1;
2493
2722
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
2494
2723
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -2498,8 +2727,24 @@ var penetrationOf = (mat, table) => {
2498
2727
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
2499
2728
  };
2500
2729
  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;
2730
+ const residual = p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
2731
+ if (p.st.stripped) return residual;
2732
+ const kept = Math.min(p.rt.removed ? 1 - p.rt.removed / p.mass0 : 1, 1 - sectionLoss(p));
2733
+ return Math.max(p.spec.reinforcement ? residual : 0.05, kept);
2734
+ };
2735
+ var sectionLoss = (p) => {
2736
+ const cells = p.st.chipped;
2737
+ if (!cells?.length) return 0;
2738
+ const [u, v] = planeAxes(p.thin);
2739
+ const along = p.dims[u] >= p.dims[v] ? u : v;
2740
+ const across = along === u ? v : u;
2741
+ let worst = 0;
2742
+ for (const c of cells) {
2743
+ let gone = 0;
2744
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across];
2745
+ worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
2746
+ }
2747
+ return Math.min(1, worst);
2503
2748
  };
2504
2749
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
2505
2750
  var clone = (v) => structuredClone(v);
@@ -3680,6 +3925,12 @@ var createDestruction = (options = {}) => {
3680
3925
  debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
3681
3926
  setStage(o, p, 1, {}, src);
3682
3927
  };
3928
+ const crater = (o, p, wp, normal, radius, src) => {
3929
+ markDamaged(p);
3930
+ emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: -1, point: wp, area: Math.PI * radius * radius });
3931
+ debris(o, p, p.mat.breakDebris, wp, normal, 6, 1.5, src);
3932
+ setStage(o, p, 1, {}, src);
3933
+ };
3683
3934
  const insideOpening = (o, p, objectPoint) => {
3684
3935
  const op = p.st.opening;
3685
3936
  if (!op) return false;
@@ -3756,9 +4007,13 @@ var createDestruction = (options = {}) => {
3756
4007
  const lp = toLocal(p.pos, p.q, objectPoint);
3757
4008
  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
4009
  };
3759
- const chipUnits = (o, p, wp, energy, dir, radius, src) => {
4010
+ const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
3760
4011
  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;
4012
+ if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
4013
+ const rule = p.mat.hits?.[kind];
4014
+ const perKg = rule?.unitJoules ?? chipEnergy;
4015
+ const energy = hitEnergy - (rule?.floor ?? 0);
4016
+ if (perKg <= 0 || energy <= 0) return;
3762
4017
  const [u, v] = planeAxes(p.thin);
3763
4018
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
3764
4019
  const grid = gridOf(p);
@@ -3772,7 +4027,7 @@ var createDestruction = (options = {}) => {
3772
4027
  const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
3773
4028
  const total = (worn[i] ?? 0) + energy * share;
3774
4029
  const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
3775
- if (total < chipEnergy * mass) {
4030
+ if (total < perKg * mass) {
3776
4031
  worn[i] = total;
3777
4032
  continue;
3778
4033
  }
@@ -3797,7 +4052,32 @@ var createDestruction = (options = {}) => {
3797
4052
  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
4053
  debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
3799
4054
  }
4055
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
3800
4056
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
4057
+ if (p.structural && !isLost(p)) {
4058
+ solveStructure(o);
4059
+ collapseCheck(o, src);
4060
+ }
4061
+ };
4062
+ const exposeRebar = (o, p, src) => {
4063
+ const r = p.spec.reinforcement;
4064
+ if (!r) return;
4065
+ const [u, v] = planeAxes(p.thin);
4066
+ const all = rebarBars(p.dims, p.thin, r.spacing);
4067
+ let bars;
4068
+ if (p.st.stripped) bars = all;
4069
+ else {
4070
+ bars = [];
4071
+ for (const cell of p.st.chipped ?? []) bars.push(...clipBarsToBox(all, u, v, cell.center, cell.size));
4072
+ const op = p.st.opening;
4073
+ if (op) {
4074
+ const lc = toLocal(p.pos, p.q, op.center);
4075
+ bars.push(...clipBarsToDisc(all, u, v, [lc[u], lc[v]], op.radius + 0.05));
4076
+ }
4077
+ }
4078
+ const bow = clamp2(((p.st.stress ?? 0) - 1) / 0.5, 0, 1);
4079
+ if (bow > 0) bars = bars.flatMap((bar2, i) => bendBar(bar2, p.thin, bow * BAR_BOW, i));
4080
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
3801
4081
  };
3802
4082
  const openBreach = (o, p, center, total, energy, dir, src) => {
3803
4083
  const [u, v] = planeAxes(p.thin);
@@ -3825,10 +4105,7 @@ var createDestruction = (options = {}) => {
3825
4105
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
3826
4106
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
3827
4107
  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
- }
4108
+ if (reinforcement) exposeRebar(o, p, src);
3832
4109
  if (p.structural) solveStructure(o);
3833
4110
  };
3834
4111
  const stripPart = (o, p, reinforcement, src) => {
@@ -3840,8 +4117,8 @@ var createDestruction = (options = {}) => {
3840
4117
  p.st.burning = false;
3841
4118
  p.rt.jet = false;
3842
4119
  markDamaged(p);
3843
- emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
3844
4120
  solveStructure(o);
4121
+ exposeRebar(o, p, src);
3845
4122
  collapseCheck(o, src);
3846
4123
  };
3847
4124
  const unitFragmentPlan = (p, cap) => {
@@ -3895,7 +4172,7 @@ var createDestruction = (options = {}) => {
3895
4172
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
3896
4173
  const hitLocal = toLocal(c, q, hitW);
3897
4174
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
3898
- const axis = style === "splinter" ? severAxis(p) : -1;
4175
+ const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
3899
4176
  const cutAt = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
3900
4177
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
3901
4178
  style,
@@ -3967,7 +4244,7 @@ var createDestruction = (options = {}) => {
3967
4244
  };
3968
4245
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
3969
4246
  const severAxis = (p) => {
3970
- if (p.mat.fracture !== "splinter" || p.spec.shape.kind !== "box") return -1;
4247
+ if (p.mat.fracture !== "splinter" && p.mat.fracture !== "snap" || p.spec.shape.kind !== "box") return -1;
3971
4248
  const d = p.dims;
3972
4249
  const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
3973
4250
  return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
@@ -4028,6 +4305,49 @@ var createDestruction = (options = {}) => {
4028
4305
  }
4029
4306
  }
4030
4307
  };
4308
+ const tearOf = (mat, kind) => {
4309
+ const h = mat.hits?.[kind];
4310
+ const tear = h?.tear ?? (kind === "ballistic" && mat.penetrationClass === "wood" ? "splinters" : "none");
4311
+ return { tear, joules: h?.tearJoules ?? TEAR_JOULES };
4312
+ };
4313
+ const tearAt = (o, p, kind, entry, exit, d, E, scale, src) => {
4314
+ const { tear, joules } = tearOf(p.mat, kind);
4315
+ if (tear === "none") return;
4316
+ const shot = kind === "ballistic";
4317
+ const n = shot ? exit ? clamp2(Math.round(E / joules), 1, 7) : 1 : clamp2(Math.floor(E / joules), 0, 5);
4318
+ if (n === 0) return;
4319
+ const fibre = tear === "lumps";
4320
+ const { q } = partXf(o, p);
4321
+ const along = (p.spec.grain ? AXIS[p.spec.grain] : void 0) ?? [0, 1, 2].sort((a, b) => p.dims[b] - p.dims[a])[0];
4322
+ const fragments = [];
4323
+ for (let i = 0; i < n; i++) {
4324
+ const back = !exit || i % 3 === 2;
4325
+ 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());
4326
+ const size = [w, w, w];
4327
+ size[along] = len;
4328
+ size[(along + 1) % 3] = w;
4329
+ size[(along + 2) % 3] = t;
4330
+ const spread3 = [(rnd() - 0.5) * 0.8, (rnd() - 0.2) * 0.8, (rnd() - 0.5) * 0.8];
4331
+ const speed = shot ? back ? 1.5 + 2 * rnd() : 2 + 4 * rnd() : 0.8 + 1.2 * rnd();
4332
+ const v = vec.scale(vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3)), speed);
4333
+ const mass = size[0] * size[1] * size[2] * p.mat.density;
4334
+ fragments.push({
4335
+ id: `${p.spec.id}#t${tears++}-${i}`,
4336
+ part: p.spec.id,
4337
+ material: p.spec.material,
4338
+ shape: fibre ? "chunk" : "splinter",
4339
+ center: back || !exit ? vec.madd(entry, d, -0.01) : vec.madd(exit, d, 0.01),
4340
+ size,
4341
+ rotation: q,
4342
+ velocity: v,
4343
+ inner: true,
4344
+ angularVelocity: [(rnd() - 0.5) * 30, (rnd() - 0.5) * 30, (rnd() - 0.5) * 30],
4345
+ mass
4346
+ });
4347
+ }
4348
+ const patch = [scale * 4, scale * 4, scale * 4];
4349
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry, size: patch, rotation: q, fragments });
4350
+ };
4031
4351
  const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src, round = {}) => {
4032
4352
  const mat = p.mat;
4033
4353
  const piercing = round.piercing ?? 1;
@@ -4056,6 +4376,7 @@ var createDestruction = (options = {}) => {
4056
4376
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
4057
4377
  }
4058
4378
  if (p.layers.length > 0) spallOuter(o, p, entry, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
4379
+ 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
4380
  const brittle = mat.ductility < 0.3;
4060
4381
  const ratio = Ep / Math.max(thr, 1e-6);
4061
4382
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
@@ -4089,8 +4410,10 @@ var createDestruction = (options = {}) => {
4089
4410
  } else addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
4090
4411
  debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
4091
4412
  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);
4413
+ const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
4414
+ const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry, d, absorbed * 0.1, 12, src);
4415
+ if (!isLost(p)) tearAt(o, p, "ballistic", entry, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
4416
+ 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
4417
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
4095
4418
  };
4096
4419
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -4098,14 +4421,20 @@ var createDestruction = (options = {}) => {
4098
4421
  const thrMm = mat.penetration * p.thickness * 1e3;
4099
4422
  const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
4100
4423
  const Ec = E * fragile;
4424
+ const sheet = mat.ductility >= 0.5 && p.thickness <= THIN_SHEET_CRUSH;
4425
+ const fold = sheet ? Math.min(4, Math.sqrt(1e-3 / p.thickness)) : 1;
4426
+ const patch = sheet ? Math.min(contactR, THIN_SHEET_CONTACT) : contactR;
4101
4427
  if (E > 0) {
4102
4428
  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);
4429
+ 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
4430
  else if (mat.ductility < 0.3) {
4105
4431
  if (Ec >= 5) addCrack(o, p, point, n, clamp2(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
4106
4432
  } 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
4433
  } else if (mat.fracture === "tear") {
4108
4434
  addHole(o, p, point, n, clamp2(0.02 + E * 5e-4, 0.02, 0.3), true, void 0, src);
4435
+ } else if (mat.ductility >= 0.3 && E >= thrMm) {
4436
+ addHole(o, p, point, n, clamp2(contactR, 0.01, 0.08), true, void 0, src);
4437
+ 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
4438
  } else if (mat.ductility >= 0.5) {
4110
4439
  addDent(o, p, point, n, Math.max(contactR, 0.01), clamp2(E * 4e-5, 5e-4, 0.02), src);
4111
4440
  }
@@ -4118,7 +4447,8 @@ var createDestruction = (options = {}) => {
4118
4447
  const scale = mat.damageScale?.[kind] ?? 1;
4119
4448
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
4120
4449
  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);
4450
+ if (kind === "blunt") chipUnits(o, p, "blunt", point, E, d, Math.max(contactR, 0.03), src);
4451
+ if (E > 0 && !isLost(p)) tearAt(o, p, kind, point, void 0, d, E, clamp2(contactR * 0.2, 6e-3, 0.02), src);
4122
4452
  return done;
4123
4453
  };
4124
4454
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -4697,6 +5027,7 @@ var createDestruction = (options = {}) => {
4697
5027
  if (p.rt.stressT <= 0) {
4698
5028
  p.rt.stressT = 0.5;
4699
5029
  emit({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
5030
+ if (p.spec.reinforcement && (p.st.stripped || p.st.chipped?.length || p.st.opening)) exposeRebar(o, p);
4700
5031
  }
4701
5032
  applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
4702
5033
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
@@ -4913,6 +5244,18 @@ var createDestruction = (options = {}) => {
4913
5244
  const r = raycastInternal(origin, direction, maxDistance);
4914
5245
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
4915
5246
  },
5247
+ sweepBox(box, direction, distance, options2 = {}) {
5248
+ const d = vec.normalize(direction);
5249
+ const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
5250
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
5251
+ let best = null;
5252
+ for (const origin of sweepOrigins(box, d, options2.spacing ?? 0.2)) {
5253
+ const r = raycastInternal(origin, d, distance + SWEEP_BACK, ignore);
5254
+ if (!r || best && r.distance >= best.distance) continue;
5255
+ best = { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: Math.max(0, r.distance - SWEEP_BACK) };
5256
+ }
5257
+ return best;
5258
+ },
4916
5259
  shoot(origin, direction, energy, caliber, source, round) {
4917
5260
  const extras = typeof round === "number" ? { piercing: round } : round ?? {};
4918
5261
  return withCollapses(run(() => {
@@ -5557,7 +5900,12 @@ var locales = {
5557
5900
  cactus: "cactus",
5558
5901
  flesh_fruit: "fruit",
5559
5902
  wax: "wax",
5560
- soil: "soil"
5903
+ soil: "soil",
5904
+ asphalt: "asphalt",
5905
+ sand: "sand",
5906
+ gravel: "gravel",
5907
+ turf: "turf",
5908
+ mortar: "mortar"
5561
5909
  },
5562
5910
  partStatus: { intact: "intact", damaged: "damaged", collapsed: "collapsed", broken: "broken", melted: "melted", ashes: "ashes" },
5563
5911
  jointKinds: {
@@ -5648,7 +5996,12 @@ var locales = {
5648
5996
  cactus: "cacto",
5649
5997
  flesh_fruit: "fruta",
5650
5998
  wax: "cera",
5651
- soil: "terra"
5999
+ soil: "terra",
6000
+ asphalt: "asfalto",
6001
+ sand: "areia",
6002
+ gravel: "brita",
6003
+ turf: "grama",
6004
+ mortar: "argamassa"
5652
6005
  },
5653
6006
  partStatus: { intact: "intacto", damaged: "danificado", collapsed: "desabado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
5654
6007
  jointKinds: {