@adatechnology/game-destruction 0.9.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
@@ -340,6 +340,47 @@ var frame = (ax, ay, az, angle = 0) => {
340
340
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
341
341
  };
342
342
  var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
343
+ var splinterBox = (inp, box, limit = inp.maxFragments) => {
344
+ const { rnd } = inp;
345
+ const dims = box.s;
346
+ const [fmin, fmax] = inp.fragmentSize;
347
+ const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
348
+ const [a, b] = [0, 1, 2].filter((i) => i !== g);
349
+ const w = Math.max(fmin * 1.5, 0.01);
350
+ const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
351
+ const maxLen = fmax * 4;
352
+ const cells = bsp(box, c, [a, b], rnd);
353
+ const pieces = [];
354
+ for (const cell of cells) {
355
+ const extra = rnd() < 0.4 ? 1 : 0;
356
+ const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
357
+ for (const p of bsp(cell, k, [g], rnd)) pieces.push(p);
358
+ }
359
+ return pieces.slice(0, limit).map((p) => ({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] }));
360
+ };
361
+ var severAt = (inp, root, at, gap) => {
362
+ const dims = root.s;
363
+ const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
364
+ const half = dims[L] / 2;
365
+ const lo = clamp(at - gap / 2, -half, half), hi = clamp(at + gap / 2, -half, half);
366
+ const out = [];
367
+ const stub = (from, to) => {
368
+ if (to - from < 0.02) return;
369
+ const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
370
+ c[L] = (from + to) / 2;
371
+ sz[L] = to - from;
372
+ out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight: sz[0] * sz[1] * sz[2] });
373
+ };
374
+ stub(-half, lo);
375
+ stub(hi, half);
376
+ const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
377
+ band.c[L] = (lo + hi) / 2;
378
+ band.s[L] = Math.max(hi - lo, 0.01);
379
+ if (inp.style === "snap") {
380
+ for (const b of bsp(band, Math.max(3, Math.min(6, inp.maxFragments - out.length)), [0, 1, 2], inp.rnd)) out.push({ center: b.c, size: b.s, q: quat.identity(), shape: "chunk", weight: b.s[0] * b.s[1] * b.s[2] });
381
+ } else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
382
+ return out;
383
+ };
343
384
  var tearStack = (inp, root, t, u, v, n, sheet) => {
344
385
  const { dims, rnd, maxFragments } = inp;
345
386
  const layers = clamp(Math.ceil(dims[t] / STACK_LAYER), 2, 4);
@@ -368,6 +409,7 @@ var planFragments = (inp) => {
368
409
  const t = sorted[0];
369
410
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
370
411
  if (style === "snap") {
412
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
371
413
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
372
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] });
373
415
  return out;
@@ -378,23 +420,8 @@ var planFragments = (inp) => {
378
420
  return out;
379
421
  }
380
422
  if (style === "splinter") {
381
- const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
382
- const [a, b] = [0, 1, 2].filter((i) => i !== g);
383
- const w = Math.max(fmin * 1.5, 0.01);
384
- const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(maxFragments / 2)));
385
- const maxLen = fmax * 4;
386
- const cells = bsp(root, c, [a, b], rnd);
387
- const pieces = [];
388
- for (const cell of cells) {
389
- let list = [cell];
390
- const extra = rnd() < 0.4 ? 1 : 0;
391
- const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
392
- list = bsp(cell, k, [g], rnd);
393
- for (const p of list) pieces.push(p);
394
- }
395
- for (const p of pieces.slice(0, maxFragments)) {
396
- out.push({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] });
397
- }
423
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
424
+ for (const p of splinterBox(inp, root)) out.push(p);
398
425
  return out;
399
426
  }
400
427
  if (style === "tear") {
@@ -562,6 +589,27 @@ var rebarBars = (dims, thin, spacing) => {
562
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]]));
563
590
  return out;
564
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
+ };
565
613
  var clipBarsToDisc = (bars, u, v, center, radius) => {
566
614
  const out = [];
567
615
  for (const [a, b] of bars) {
@@ -1539,14 +1587,21 @@ var materials_default = [
1539
1587
  smoke: "grey"
1540
1588
  },
1541
1589
  visual: {
1542
- charColor: 1709072
1590
+ charColor: 1709072,
1591
+ innerColor: 14730131
1543
1592
  },
1544
1593
  tags: [
1545
1594
  "wood",
1546
1595
  "organic",
1547
1596
  "flammable"
1548
1597
  ],
1549
- compressive: 1e5
1598
+ compressive: 1e5,
1599
+ hits: {
1600
+ blunt: {
1601
+ tear: "splinters",
1602
+ tearJoules: 60
1603
+ }
1604
+ }
1550
1605
  },
1551
1606
  {
1552
1607
  id: "wood_hard",
@@ -1576,14 +1631,21 @@ var materials_default = [
1576
1631
  smoke: "grey"
1577
1632
  },
1578
1633
  visual: {
1579
- charColor: 1709072
1634
+ charColor: 1709072,
1635
+ innerColor: 13213802
1580
1636
  },
1581
1637
  tags: [
1582
1638
  "wood",
1583
1639
  "organic",
1584
1640
  "flammable"
1585
1641
  ],
1586
- compressive: 15e4
1642
+ compressive: 15e4,
1643
+ hits: {
1644
+ blunt: {
1645
+ tear: "splinters",
1646
+ tearJoules: 150
1647
+ }
1648
+ }
1587
1649
  },
1588
1650
  {
1589
1651
  id: "plywood",
@@ -1613,14 +1675,24 @@ var materials_default = [
1613
1675
  smoke: "grey"
1614
1676
  },
1615
1677
  visual: {
1616
- charColor: 1709072
1678
+ charColor: 1709072,
1679
+ innerColor: 14862234
1617
1680
  },
1618
1681
  tags: [
1619
1682
  "wood",
1620
1683
  "organic",
1621
1684
  "flammable"
1622
1685
  ],
1623
- compressive: 6e4
1686
+ compressive: 6e4,
1687
+ hits: {
1688
+ ballistic: {
1689
+ tear: "splinters"
1690
+ },
1691
+ blunt: {
1692
+ tear: "splinters",
1693
+ tearJoules: 80
1694
+ }
1695
+ }
1624
1696
  },
1625
1697
  {
1626
1698
  id: "mdf",
@@ -1647,14 +1719,25 @@ var materials_default = [
1647
1719
  smoke: "grey"
1648
1720
  },
1649
1721
  visual: {
1650
- charColor: 1709072
1722
+ charColor: 1709072,
1723
+ innerColor: 12096362
1651
1724
  },
1652
1725
  tags: [
1653
1726
  "wood",
1654
1727
  "organic",
1655
1728
  "flammable"
1656
1729
  ],
1657
- 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
+ }
1658
1741
  },
1659
1742
  {
1660
1743
  id: "glass",
@@ -1768,6 +1851,17 @@ var materials_default = [
1768
1851
  damageScale: {
1769
1852
  explosive: 1.2
1770
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
+ },
1771
1865
  hitDebris: "chips",
1772
1866
  breakDebris: "dust",
1773
1867
  thermal: {
@@ -2136,7 +2230,8 @@ var materials_default = [
2136
2230
  smoke: "white"
2137
2231
  },
2138
2232
  visual: {
2139
- charColor: 1709072
2233
+ charColor: 1709072,
2234
+ innerColor: 14207126
2140
2235
  },
2141
2236
  tags: [
2142
2237
  "plant",
@@ -2144,7 +2239,12 @@ var materials_default = [
2144
2239
  "organic",
2145
2240
  "flammable"
2146
2241
  ],
2147
- compressive: 6e4
2242
+ compressive: 6e4,
2243
+ hits: {
2244
+ blunt: {
2245
+ tear: "none"
2246
+ }
2247
+ }
2148
2248
  },
2149
2249
  {
2150
2250
  id: "cactus",
@@ -2259,6 +2359,130 @@ var materials_default = [
2259
2359
  "masonry"
2260
2360
  ],
2261
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
2262
2486
  }
2263
2487
  ];
2264
2488
 
@@ -2444,6 +2668,32 @@ var fragility = (ductility, thickness) => {
2444
2668
  // src/world.ts
2445
2669
  import { ignitionChance, resolveIgnitionRules, sparkTargetFactor } from "@adatechnology/game-contracts";
2446
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;
2447
2697
  var DMG_PER_J = 0.02;
2448
2698
  var BLAST_COUPLING = 0.1;
2449
2699
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
@@ -2457,9 +2707,17 @@ var BREACH_BLAST_FACTOR = 6;
2457
2707
  var BREACH_AREA = 0.25;
2458
2708
  var BREACH_BLAST_REACH = 0.12;
2459
2709
  var RESIDUAL_CAPACITY = 0.15;
2710
+ var SEVER_BAND = 0.1;
2711
+ var SEVER_ASPECT = 3;
2712
+ var SEVER_BALLISTIC = 2;
2713
+ var TEAR_JOULES = 150;
2714
+ var tears = 0;
2460
2715
  var SUPPORT_TOLERANCE = 0.05;
2461
2716
  var UNIT_FRAGMENT_FACTOR = 4;
2462
2717
  var BREACH_GRID_CAP = 4096;
2718
+ var CHIP_ENERGY = 200;
2719
+ var CHIP_SPLASH = 0.5;
2720
+ var BAR_BOW = 0.08;
2463
2721
  var CUT_SLICE = 1;
2464
2722
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
2465
2723
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -2469,8 +2727,24 @@ var penetrationOf = (mat, table) => {
2469
2727
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
2470
2728
  };
2471
2729
  var capacityScale = (p) => {
2472
- if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
2473
- 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);
2474
2748
  };
2475
2749
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
2476
2750
  var clone = (v) => structuredClone(v);
@@ -2503,6 +2777,7 @@ var createDestruction = (options = {}) => {
2503
2777
  let windCfg = windOpts ? resolveWind(windOpts) : null;
2504
2778
  const ignitionRules = resolveIgnitionRules(options.ignition);
2505
2779
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
2780
+ const chipEnergy = options.breach?.chipEnergy ?? CHIP_ENERGY;
2506
2781
  const rng = createRng(options.seed ?? 1);
2507
2782
  const rnd = () => rng.next();
2508
2783
  let time = 0;
@@ -2876,41 +3151,68 @@ var createDestruction = (options = {}) => {
2876
3151
  markDamaged(p);
2877
3152
  if (p.rt.hinge >= flexure.failRotationDeg * Math.PI / 180) hingeDrop(o, p, src);
2878
3153
  };
3154
+ const freeSpan = (p, supports) => {
3155
+ if (supports.length === 0) return Infinity;
3156
+ const d = p.dims;
3157
+ const L = [0, 1, 2].sort((a, b) => d[b] - d[a])[0];
3158
+ const spans = supports.map((k) => contactBox(p, k)).map(([lo, hi]) => [lo[L], hi[L]]);
3159
+ let far = 0;
3160
+ for (let i = 0; i <= 16; i++) {
3161
+ const x = (i / 16 - 0.5) * d[L];
3162
+ let near = Infinity;
3163
+ for (const [lo, hi] of spans) near = Math.min(near, Math.max(lo - x, x - hi, 0));
3164
+ far = Math.max(far, near);
3165
+ }
3166
+ return 2 * far;
3167
+ };
2879
3168
  const solveStructure = (o) => {
2880
3169
  o.maxStress = 0;
2881
3170
  if (!o.hasStructure) return;
2882
3171
  const parts = o.parts;
2883
3172
  const n = parts.length;
2884
3173
  const dist = new Array(n).fill(-1);
2885
- const queue = [];
2886
3174
  const ok = (i) => parts[i].structural && !isLost(parts[i]);
2887
3175
  const isPartition = (i) => parts[i].spec.structure?.role === "partition";
2888
- for (let i = 0; i < n; i++) if (ok(i) && parts[i].anchored) {
2889
- dist[i] = 0;
2890
- queue.push(i);
2891
- }
2892
- for (let h = 0; h < queue.length; h++) {
2893
- const s = queue[h];
2894
- if (isPartition(s)) continue;
2895
- for (const j of o.byPart.get(parts[s].spec.id)) {
2896
- if (j.st.open || !j.bears) continue;
2897
- const r = j.ia === s ? j.ib : j.ia;
2898
- if (dist[r] >= 0 || !ok(r) || restsOn(o, j, r) !== s) continue;
2899
- dist[r] = dist[s] + 1;
2900
- queue.push(r);
3176
+ const out = /* @__PURE__ */ new Set();
3177
+ let queue = [];
3178
+ let lowerOf = /* @__PURE__ */ new Map();
3179
+ for (; ; ) {
3180
+ dist.fill(-1);
3181
+ queue = [];
3182
+ for (let i = 0; i < n; i++) if (ok(i) && !out.has(i) && parts[i].anchored) {
3183
+ dist[i] = 0;
3184
+ queue.push(i);
2901
3185
  }
2902
- }
2903
- const incoming = new Array(n).fill(0);
2904
- const lowerOf = /* @__PURE__ */ new Map();
2905
- for (const i of queue) {
2906
- const lower = [];
2907
- for (const j of o.byPart.get(parts[i].spec.id)) {
2908
- if (j.st.open || !j.bears) continue;
2909
- const k = restsOn(o, j, i);
2910
- if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
3186
+ for (let h = 0; h < queue.length; h++) {
3187
+ const s = queue[h];
3188
+ if (isPartition(s)) continue;
3189
+ for (const j of o.byPart.get(parts[s].spec.id)) {
3190
+ if (j.st.open || !j.bears) continue;
3191
+ const r = j.ia === s ? j.ib : j.ia;
3192
+ if (dist[r] >= 0 || !ok(r) || out.has(r) || restsOn(o, j, r) !== s) continue;
3193
+ dist[r] = dist[s] + 1;
3194
+ queue.push(r);
3195
+ }
2911
3196
  }
2912
- lowerOf.set(i, lower);
3197
+ lowerOf = /* @__PURE__ */ new Map();
3198
+ let dropped = false;
3199
+ for (const i of queue) {
3200
+ const lower = [];
3201
+ for (const j of o.byPart.get(parts[i].spec.id)) {
3202
+ if (j.st.open || !j.bears) continue;
3203
+ const k = restsOn(o, j, i);
3204
+ if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
3205
+ }
3206
+ lowerOf.set(i, lower);
3207
+ const max = parts[i].spec.structure?.maxSpan;
3208
+ if (max !== void 0 && !parts[i].anchored && freeSpan(parts[i], lower.map((k) => parts[k])) > max) {
3209
+ out.add(i);
3210
+ dropped = true;
3211
+ }
3212
+ }
3213
+ if (!dropped) break;
2913
3214
  }
3215
+ const incoming = new Array(n).fill(0);
2914
3216
  const bending = new Array(n).fill(0);
2915
3217
  const through = parts.map(() => []);
2916
3218
  const pass = (k, at, mass) => {
@@ -3422,6 +3724,7 @@ var createDestruction = (options = {}) => {
3422
3724
  const r = rayPart(o, p, origin, d);
3423
3725
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
3424
3726
  if (p.st.opening && insideOpening(o, p, vec.madd(origin, d, r.t0))) continue;
3727
+ if (p.st.chipped && insideChip(p, objLocal(o, vec.madd(origin, d, r.t0)))) continue;
3425
3728
  if (!best || r.t0 < best.distance) best = { object: o.id, part: p.spec.id, point: vec.madd(origin, d, r.t0), normal: r.n, distance: r.t0, pathLen: r.t1 - r.t0 };
3426
3729
  }
3427
3730
  }
@@ -3622,6 +3925,12 @@ var createDestruction = (options = {}) => {
3622
3925
  debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
3623
3926
  setStage(o, p, 1, {}, src);
3624
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
+ };
3625
3934
  const insideOpening = (o, p, objectPoint) => {
3626
3935
  const op = p.st.opening;
3627
3936
  if (!op) return false;
@@ -3685,6 +3994,91 @@ var createDestruction = (options = {}) => {
3685
3994
  if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
3686
3995
  openBreach(o, p, [sum[0] / total, sum[1] / total, sum[2] / total], total, energy, dir, src);
3687
3996
  };
3997
+ const unitGrids = /* @__PURE__ */ new WeakMap();
3998
+ const gridOf = (p) => {
3999
+ let grid = unitGrids.get(p);
4000
+ if (!grid) unitGrids.set(p, grid = unitGrid(p.dims, p.thin, unitsOf(p), BREACH_GRID_CAP));
4001
+ return grid;
4002
+ };
4003
+ const insideChip = (p, objectPoint) => {
4004
+ const cells = p.st.chipped;
4005
+ if (!cells?.length) return false;
4006
+ const [u, v] = planeAxes(p.thin);
4007
+ const lp = toLocal(p.pos, p.q, objectPoint);
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);
4009
+ };
4010
+ const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
4011
+ var _a, _b;
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;
4017
+ const [u, v] = planeAxes(p.thin);
4018
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
4019
+ const grid = gridOf(p);
4020
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
4021
+ const out = [];
4022
+ for (let i = 0; i < grid.length; i++) {
4023
+ const cell = grid[i];
4024
+ if (worn[i] === -1) continue;
4025
+ const gap = Math.hypot(Math.max(0, Math.abs(lp[u] - cell.center[u]) - cell.size[u] / 2), Math.max(0, Math.abs(lp[v] - cell.center[v]) - cell.size[v] / 2));
4026
+ if (gap > radius) continue;
4027
+ const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
4028
+ const total = (worn[i] ?? 0) + energy * share;
4029
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
4030
+ if (total < perKg * mass) {
4031
+ worn[i] = total;
4032
+ continue;
4033
+ }
4034
+ worn[i] = -1;
4035
+ out.push(cell);
4036
+ }
4037
+ if (out.length === 0) return;
4038
+ const { c, q } = partXf(o, p);
4039
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
4040
+ const holes = localHolesOf(p);
4041
+ for (const cell of out) {
4042
+ const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
4043
+ const center = toWorld(c, q, cell.center);
4044
+ const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
4045
+ const fragments = cellFragments(o, p, [cell], wp, dir, speed, mass, `c${chipped.length}_`);
4046
+ p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
4047
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
4048
+ p.st.mass = Math.max(0, p.st.mass - mass);
4049
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
4050
+ markDamaged(p);
4051
+ setStage(o, p, 1, {}, src);
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 });
4053
+ debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
4054
+ }
4055
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
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) });
4081
+ };
3688
4082
  const openBreach = (o, p, center, total, energy, dir, src) => {
3689
4083
  const [u, v] = planeAxes(p.thin);
3690
4084
  const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
@@ -3696,7 +4090,8 @@ var createDestruction = (options = {}) => {
3696
4090
  failPart(o, p, worldCenter, dir, energy, 12, src);
3697
4091
  return;
3698
4092
  }
3699
- const cells = cellsWithin(unitGrid(p.dims, p.thin, unitsOf(p), BREACH_GRID_CAP), p.thin, lc, radius);
4093
+ const worn = p.rt.chips;
4094
+ const cells = cellsWithin(gridOf(p).filter((_, i) => worn?.[i] !== -1), p.thin, lc, radius);
3700
4095
  const removed = p.st.mass * cells.reduce((sum, cell) => sum + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
3701
4096
  const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
3702
4097
  const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
@@ -3710,10 +4105,7 @@ var createDestruction = (options = {}) => {
3710
4105
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
3711
4106
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
3712
4107
  const reinforcement = p.spec.reinforcement;
3713
- if (reinforcement) {
3714
- const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
3715
- if (bars.length > 0) emit({ ...base(o, src), type: "expose", part: p.spec.id, bars });
3716
- }
4108
+ if (reinforcement) exposeRebar(o, p, src);
3717
4109
  if (p.structural) solveStructure(o);
3718
4110
  };
3719
4111
  const stripPart = (o, p, reinforcement, src) => {
@@ -3725,8 +4117,8 @@ var createDestruction = (options = {}) => {
3725
4117
  p.st.burning = false;
3726
4118
  p.rt.jet = false;
3727
4119
  markDamaged(p);
3728
- emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
3729
4120
  solveStructure(o);
4121
+ exposeRebar(o, p, src);
3730
4122
  collapseCheck(o, src);
3731
4123
  };
3732
4124
  const unitFragmentPlan = (p, cap) => {
@@ -3780,6 +4172,8 @@ var createDestruction = (options = {}) => {
3780
4172
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
3781
4173
  const hitLocal = toLocal(c, q, hitW);
3782
4174
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
4175
+ const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
4176
+ const cutAt = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
3783
4177
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
3784
4178
  style,
3785
4179
  dims: p.dims,
@@ -3789,7 +4183,8 @@ var createDestruction = (options = {}) => {
3789
4183
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
3790
4184
  maxFragments,
3791
4185
  rnd,
3792
- sheet: p.mat.sheet
4186
+ sheet: p.mat.sheet,
4187
+ ...cutAt !== void 0 ? { cut: { at: cutAt, gap: SEVER_BAND } } : {}
3793
4188
  });
3794
4189
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
3795
4190
  const mass = p.st.mass;
@@ -3848,10 +4243,33 @@ var createDestruction = (options = {}) => {
3848
4243
  emit({ ...base(o, src), type: "primed", part: p.spec.id, point: partXf(o, p).c, trigger, delay });
3849
4244
  };
3850
4245
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
4246
+ const severAxis = (p) => {
4247
+ if (p.mat.fracture !== "splinter" && p.mat.fracture !== "snap" || p.spec.shape.kind !== "box") return -1;
4248
+ const d = p.dims;
4249
+ const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
4250
+ return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
4251
+ };
4252
+ const bandOf = (o, p, axis, origin) => {
4253
+ const { c, q } = partXf(o, p);
4254
+ const along = toLocal(c, q, origin)[axis];
4255
+ const L = p.dims[axis];
4256
+ return clamp2(Math.floor((along + L / 2) / SEVER_BAND), 0, Math.ceil(L / SEVER_BAND) - 1);
4257
+ };
3851
4258
  const applyDamage = (o, p, amount, type, origin, dir, fragEnergy, cap, src) => {
4259
+ var _a;
3852
4260
  if (isLost(p) || amount <= 0) return 0;
3853
4261
  const prev = p.st.hp;
3854
4262
  p.st.hp -= amount;
4263
+ const axis = origin && type !== "fire" && type !== "heat" ? severAxis(p) : -1;
4264
+ if (axis >= 0 && p.st.hp > 0) {
4265
+ const band = bandOf(o, p, axis, origin);
4266
+ const cuts = (_a = p.rt).cuts ?? (_a.cuts = {});
4267
+ cuts[band] = (cuts[band] ?? 0) + amount * (type === "ballistic" ? SEVER_BALLISTIC : 1);
4268
+ if (cuts[band] >= p.st.maxHp * SEVER_BAND / p.dims[axis]) {
4269
+ p.rt.cutAt = -p.dims[axis] / 2 + (band + 0.5) * SEVER_BAND;
4270
+ p.st.hp = 0;
4271
+ }
4272
+ }
3855
4273
  markDamaged(p);
3856
4274
  const trigger = CHAIN_TRIGGER_OF[type];
3857
4275
  const chained = trigger !== void 0 && chainDelayOf(p.spec, trigger) !== void 0;
@@ -3887,10 +4305,53 @@ var createDestruction = (options = {}) => {
3887
4305
  }
3888
4306
  }
3889
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
+ };
3890
4351
  const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src, round = {}) => {
3891
4352
  const mat = p.mat;
3892
4353
  const piercing = round.piercing ?? 1;
3893
- if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry))) {
4354
+ if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry)) || insideChip(p, objLocal(o, entry))) {
3894
4355
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry, d, Math.min(pathLen, p.thickness)), damage: 0 };
3895
4356
  }
3896
4357
  const sin = Math.min(1, Math.abs(vec.dot(d, n)));
@@ -3915,6 +4376,7 @@ var createDestruction = (options = {}) => {
3915
4376
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
3916
4377
  }
3917
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);
3918
4380
  const brittle = mat.ductility < 0.3;
3919
4381
  const ratio = Ep / Math.max(thr, 1e-6);
3920
4382
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
@@ -3948,7 +4410,10 @@ var createDestruction = (options = {}) => {
3948
4410
  } else addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
3949
4411
  debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
3950
4412
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
3951
- const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry, d, E * 0.1, 12, 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);
3952
4417
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
3953
4418
  };
3954
4419
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -3956,14 +4421,20 @@ var createDestruction = (options = {}) => {
3956
4421
  const thrMm = mat.penetration * p.thickness * 1e3;
3957
4422
  const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
3958
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;
3959
4427
  if (E > 0) {
3960
4428
  if (kind === "blunt") {
3961
- 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);
3962
4430
  else if (mat.ductility < 0.3) {
3963
4431
  if (Ec >= 5) addCrack(o, p, point, n, clamp2(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
3964
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);
3965
4433
  } else if (mat.fracture === "tear") {
3966
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);
3967
4438
  } else if (mat.ductility >= 0.5) {
3968
4439
  addDent(o, p, point, n, Math.max(contactR, 0.01), clamp2(E * 4e-5, 5e-4, 0.02), src);
3969
4440
  }
@@ -3976,6 +4447,8 @@ var createDestruction = (options = {}) => {
3976
4447
  const scale = mat.damageScale?.[kind] ?? 1;
3977
4448
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
3978
4449
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, 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);
3979
4452
  return done;
3980
4453
  };
3981
4454
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -4554,6 +5027,7 @@ var createDestruction = (options = {}) => {
4554
5027
  if (p.rt.stressT <= 0) {
4555
5028
  p.rt.stressT = 0.5;
4556
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);
4557
5031
  }
4558
5032
  applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
4559
5033
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
@@ -4770,6 +5244,18 @@ var createDestruction = (options = {}) => {
4770
5244
  const r = raycastInternal(origin, direction, maxDistance);
4771
5245
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
4772
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
+ },
4773
5259
  shoot(origin, direction, energy, caliber, source, round) {
4774
5260
  const extras = typeof round === "number" ? { piercing: round } : round ?? {};
4775
5261
  return withCollapses(run(() => {
@@ -5414,7 +5900,12 @@ var locales = {
5414
5900
  cactus: "cactus",
5415
5901
  flesh_fruit: "fruit",
5416
5902
  wax: "wax",
5417
- soil: "soil"
5903
+ soil: "soil",
5904
+ asphalt: "asphalt",
5905
+ sand: "sand",
5906
+ gravel: "gravel",
5907
+ turf: "turf",
5908
+ mortar: "mortar"
5418
5909
  },
5419
5910
  partStatus: { intact: "intact", damaged: "damaged", collapsed: "collapsed", broken: "broken", melted: "melted", ashes: "ashes" },
5420
5911
  jointKinds: {
@@ -5467,6 +5958,7 @@ var locales = {
5467
5958
  sway: "sway",
5468
5959
  spall: "plaster came off",
5469
5960
  stage: "damage stage",
5961
+ chip: "brick knocked out",
5470
5962
  expose: "rebar bare",
5471
5963
  body: "body",
5472
5964
  blast: "blast",
@@ -5504,7 +5996,12 @@ var locales = {
5504
5996
  cactus: "cacto",
5505
5997
  flesh_fruit: "fruta",
5506
5998
  wax: "cera",
5507
- soil: "terra"
5999
+ soil: "terra",
6000
+ asphalt: "asfalto",
6001
+ sand: "areia",
6002
+ gravel: "brita",
6003
+ turf: "grama",
6004
+ mortar: "argamassa"
5508
6005
  },
5509
6006
  partStatus: { intact: "intacto", damaged: "danificado", collapsed: "desabado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
5510
6007
  jointKinds: {
@@ -5557,6 +6054,7 @@ var locales = {
5557
6054
  sway: "balan\xE7o",
5558
6055
  spall: "reboco soltou",
5559
6056
  stage: "est\xE1gio de dano",
6057
+ chip: "tijolo arrancado",
5560
6058
  expose: "vergalh\xE3o \xE0 mostra",
5561
6059
  body: "corpo",
5562
6060
  blast: "explos\xE3o",