@adatechnology/game-floorplan-structure 1.2.2 → 1.3.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.
@@ -32,6 +32,7 @@ var GameFloorplanStructure = (() => {
32
32
  FLAT_ROOF_DEFAULTS: () => FLAT_ROOF_DEFAULTS,
33
33
  GROUND: () => GROUND,
34
34
  MECHANICAL_DEFAULTS: () => MECHANICAL_DEFAULTS,
35
+ MEMBER_CAGE: () => MEMBER_CAGE,
35
36
  PILE_AREA: () => PILE_AREA,
36
37
  PILE_DEFAULTS: () => PILE_DEFAULTS,
37
38
  PLATE_DEFAULTS: () => PLATE_DEFAULTS,
@@ -2755,6 +2756,47 @@ var GameFloorplanStructure = (() => {
2755
2756
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
2756
2757
  };
2757
2758
  var clamp3 = (v, a, b) => Math.max(a, Math.min(b, v));
2759
+ var splinterBox = (inp, box2, limit = inp.maxFragments) => {
2760
+ const { rnd } = inp;
2761
+ const dims = box2.s;
2762
+ const [fmin, fmax] = inp.fragmentSize;
2763
+ const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
2764
+ const [a, b] = [0, 1, 2].filter((i) => i !== g);
2765
+ const w = Math.max(fmin * 1.5, 0.01);
2766
+ const c = clamp3(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
2767
+ const maxLen = fmax * 4;
2768
+ const cells = bsp(box2, c, [a, b], rnd);
2769
+ const pieces = [];
2770
+ for (const cell of cells) {
2771
+ const extra = rnd() < 0.4 ? 1 : 0;
2772
+ const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
2773
+ for (const p of bsp(cell, k, [g], rnd)) pieces.push(p);
2774
+ }
2775
+ 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] }));
2776
+ };
2777
+ var severAt = (inp, root, at2, gap) => {
2778
+ const dims = root.s;
2779
+ const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
2780
+ const half = dims[L] / 2;
2781
+ const lo = clamp3(at2 - gap / 2, -half, half), hi = clamp3(at2 + gap / 2, -half, half);
2782
+ const out = [];
2783
+ const stub = (from, to) => {
2784
+ if (to - from < 0.02) return;
2785
+ const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
2786
+ c[L] = (from + to) / 2;
2787
+ sz[L] = to - from;
2788
+ out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight: sz[0] * sz[1] * sz[2] });
2789
+ };
2790
+ stub(-half, lo);
2791
+ stub(hi, half);
2792
+ const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
2793
+ band.c[L] = (lo + hi) / 2;
2794
+ band.s[L] = Math.max(hi - lo, 0.01);
2795
+ if (inp.style === "snap") {
2796
+ 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] });
2797
+ } else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
2798
+ return out;
2799
+ };
2758
2800
  var tearStack = (inp, root, t, u, v, n, sheet) => {
2759
2801
  const { dims, rnd, maxFragments } = inp;
2760
2802
  const layers = clamp3(Math.ceil(dims[t] / STACK_LAYER), 2, 4);
@@ -2783,6 +2825,7 @@ var GameFloorplanStructure = (() => {
2783
2825
  const t = sorted[0];
2784
2826
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
2785
2827
  if (style === "snap") {
2828
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
2786
2829
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
2787
2830
  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] });
2788
2831
  return out;
@@ -2793,23 +2836,8 @@ var GameFloorplanStructure = (() => {
2793
2836
  return out;
2794
2837
  }
2795
2838
  if (style === "splinter") {
2796
- const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
2797
- const [a, b] = [0, 1, 2].filter((i) => i !== g);
2798
- const w = Math.max(fmin * 1.5, 0.01);
2799
- const c = clamp3(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(maxFragments / 2)));
2800
- const maxLen = fmax * 4;
2801
- const cells = bsp(root, c, [a, b], rnd);
2802
- const pieces = [];
2803
- for (const cell of cells) {
2804
- let list = [cell];
2805
- const extra = rnd() < 0.4 ? 1 : 0;
2806
- const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
2807
- list = bsp(cell, k, [g], rnd);
2808
- for (const p of list) pieces.push(p);
2809
- }
2810
- for (const p of pieces.slice(0, maxFragments)) {
2811
- 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] });
2812
- }
2839
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
2840
+ for (const p of splinterBox(inp, root)) out.push(p);
2813
2841
  return out;
2814
2842
  }
2815
2843
  if (style === "tear") {
@@ -2971,6 +2999,27 @@ var GameFloorplanStructure = (() => {
2971
2999
  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]]));
2972
3000
  return out;
2973
3001
  };
3002
+ var clipBarsToBox = (bars, u, v, center, size) => {
3003
+ const out = [];
3004
+ for (const [a, b] of bars) {
3005
+ const axis = a[u] !== b[u] ? u : v;
3006
+ const other = axis === u ? v : u;
3007
+ if (Math.abs(a[other] - center[other]) > size[other] / 2) continue;
3008
+ 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);
3009
+ if (hi - lo < 1e-6) continue;
3010
+ const p = [a[0], a[1], a[2]], q = [b[0], b[1], b[2]];
3011
+ p[axis] = lo;
3012
+ q[axis] = hi;
3013
+ out.push([p, q]);
3014
+ }
3015
+ return out;
3016
+ };
3017
+ var bendBar = ([a, b], thin, bow, i) => {
3018
+ const length = Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
3019
+ const m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2];
3020
+ m[thin] += (i % 2 === 0 ? 1 : -1) * bow * length;
3021
+ return [[a, m], [m, b]];
3022
+ };
2974
3023
  var clipBarsToDisc = (bars, u, v, center, radius) => {
2975
3024
  const out = [];
2976
3025
  for (const [a, b] of bars) {
@@ -3891,14 +3940,21 @@ var GameFloorplanStructure = (() => {
3891
3940
  smoke: "grey"
3892
3941
  },
3893
3942
  visual: {
3894
- charColor: 1709072
3943
+ charColor: 1709072,
3944
+ innerColor: 14730131
3895
3945
  },
3896
3946
  tags: [
3897
3947
  "wood",
3898
3948
  "organic",
3899
3949
  "flammable"
3900
3950
  ],
3901
- compressive: 1e5
3951
+ compressive: 1e5,
3952
+ hits: {
3953
+ blunt: {
3954
+ tear: "splinters",
3955
+ tearJoules: 60
3956
+ }
3957
+ }
3902
3958
  },
3903
3959
  {
3904
3960
  id: "wood_hard",
@@ -3928,14 +3984,21 @@ var GameFloorplanStructure = (() => {
3928
3984
  smoke: "grey"
3929
3985
  },
3930
3986
  visual: {
3931
- charColor: 1709072
3987
+ charColor: 1709072,
3988
+ innerColor: 13213802
3932
3989
  },
3933
3990
  tags: [
3934
3991
  "wood",
3935
3992
  "organic",
3936
3993
  "flammable"
3937
3994
  ],
3938
- compressive: 15e4
3995
+ compressive: 15e4,
3996
+ hits: {
3997
+ blunt: {
3998
+ tear: "splinters",
3999
+ tearJoules: 150
4000
+ }
4001
+ }
3939
4002
  },
3940
4003
  {
3941
4004
  id: "plywood",
@@ -3965,14 +4028,24 @@ var GameFloorplanStructure = (() => {
3965
4028
  smoke: "grey"
3966
4029
  },
3967
4030
  visual: {
3968
- charColor: 1709072
4031
+ charColor: 1709072,
4032
+ innerColor: 14862234
3969
4033
  },
3970
4034
  tags: [
3971
4035
  "wood",
3972
4036
  "organic",
3973
4037
  "flammable"
3974
4038
  ],
3975
- compressive: 6e4
4039
+ compressive: 6e4,
4040
+ hits: {
4041
+ ballistic: {
4042
+ tear: "splinters"
4043
+ },
4044
+ blunt: {
4045
+ tear: "splinters",
4046
+ tearJoules: 80
4047
+ }
4048
+ }
3976
4049
  },
3977
4050
  {
3978
4051
  id: "mdf",
@@ -3999,14 +4072,25 @@ var GameFloorplanStructure = (() => {
3999
4072
  smoke: "grey"
4000
4073
  },
4001
4074
  visual: {
4002
- charColor: 1709072
4075
+ charColor: 1709072,
4076
+ innerColor: 12096362
4003
4077
  },
4004
4078
  tags: [
4005
4079
  "wood",
4006
4080
  "organic",
4007
4081
  "flammable"
4008
4082
  ],
4009
- compressive: 3e4
4083
+ compressive: 3e4,
4084
+ hits: {
4085
+ ballistic: {
4086
+ tear: "lumps",
4087
+ tearJoules: 120
4088
+ },
4089
+ blunt: {
4090
+ tear: "lumps",
4091
+ tearJoules: 20
4092
+ }
4093
+ }
4010
4094
  },
4011
4095
  {
4012
4096
  id: "glass",
@@ -4120,6 +4204,17 @@ var GameFloorplanStructure = (() => {
4120
4204
  damageScale: {
4121
4205
  explosive: 1.2
4122
4206
  },
4207
+ hits: {
4208
+ ballistic: {
4209
+ unitJoules: 500,
4210
+ floor: 900,
4211
+ crater: true
4212
+ },
4213
+ blunt: {
4214
+ unitJoules: 1500,
4215
+ floor: 60
4216
+ }
4217
+ },
4123
4218
  hitDebris: "chips",
4124
4219
  breakDebris: "dust",
4125
4220
  thermal: {
@@ -4488,7 +4583,8 @@ var GameFloorplanStructure = (() => {
4488
4583
  smoke: "white"
4489
4584
  },
4490
4585
  visual: {
4491
- charColor: 1709072
4586
+ charColor: 1709072,
4587
+ innerColor: 14207126
4492
4588
  },
4493
4589
  tags: [
4494
4590
  "plant",
@@ -4496,7 +4592,12 @@ var GameFloorplanStructure = (() => {
4496
4592
  "organic",
4497
4593
  "flammable"
4498
4594
  ],
4499
- compressive: 6e4
4595
+ compressive: 6e4,
4596
+ hits: {
4597
+ blunt: {
4598
+ tear: "none"
4599
+ }
4600
+ }
4500
4601
  },
4501
4602
  {
4502
4603
  id: "cactus",
@@ -4611,6 +4712,130 @@ var GameFloorplanStructure = (() => {
4611
4712
  "masonry"
4612
4713
  ],
4613
4714
  compressive: 2e4
4715
+ },
4716
+ {
4717
+ id: "asphalt",
4718
+ density: 2350,
4719
+ hpPerKg: 10,
4720
+ penetration: 14,
4721
+ penetrationClass: "concrete",
4722
+ penetrationScale: 0.8,
4723
+ ductility: 0.1,
4724
+ fracture: "crumble",
4725
+ fragmentSize: [
4726
+ 0.03,
4727
+ 0.15
4728
+ ],
4729
+ hitDebris: "chips",
4730
+ breakDebris: "dust",
4731
+ thermal: {
4732
+ conductivity: 0.1,
4733
+ heatCapacity: 2,
4734
+ ignitionC: 400,
4735
+ burnRate: 2e-3,
4736
+ smoke: "black"
4737
+ },
4738
+ tags: [
4739
+ "masonry",
4740
+ "brittle"
4741
+ ],
4742
+ compressive: 3e4
4743
+ },
4744
+ {
4745
+ id: "sand",
4746
+ density: 1600,
4747
+ hpPerKg: 2,
4748
+ penetration: 6,
4749
+ ductility: 0.4,
4750
+ fracture: "crumble",
4751
+ fragmentSize: [
4752
+ 0.01,
4753
+ 0.04
4754
+ ],
4755
+ hitDebris: "dust",
4756
+ breakDebris: "dust",
4757
+ thermal: {
4758
+ conductivity: 0.1,
4759
+ heatCapacity: 2
4760
+ },
4761
+ tags: [
4762
+ "soil"
4763
+ ],
4764
+ compressive: 1e4
4765
+ },
4766
+ {
4767
+ id: "gravel",
4768
+ density: 1800,
4769
+ hpPerKg: 3,
4770
+ penetration: 8,
4771
+ ductility: 0.3,
4772
+ fracture: "crumble",
4773
+ fragmentSize: [
4774
+ 0.02,
4775
+ 0.06
4776
+ ],
4777
+ hitDebris: "chips",
4778
+ breakDebris: "dust",
4779
+ thermal: {
4780
+ conductivity: 0.1,
4781
+ heatCapacity: 2
4782
+ },
4783
+ tags: [
4784
+ "soil",
4785
+ "masonry"
4786
+ ],
4787
+ compressive: 2e4
4788
+ },
4789
+ {
4790
+ id: "turf",
4791
+ density: 900,
4792
+ hpPerKg: 3,
4793
+ penetration: 4,
4794
+ ductility: 0.5,
4795
+ fracture: "crumble",
4796
+ fragmentSize: [
4797
+ 0.03,
4798
+ 0.1
4799
+ ],
4800
+ hitDebris: "dust",
4801
+ breakDebris: "dust",
4802
+ thermal: {
4803
+ conductivity: 0.1,
4804
+ heatCapacity: 1.5,
4805
+ ignitionC: 250,
4806
+ burnRate: 0.03,
4807
+ smoke: "white"
4808
+ },
4809
+ tags: [
4810
+ "soil",
4811
+ "organic"
4812
+ ],
4813
+ compressive: 15e3
4814
+ },
4815
+ {
4816
+ id: "mortar",
4817
+ density: 1900,
4818
+ hpPerKg: 10,
4819
+ penetration: 12,
4820
+ penetrationClass: "concrete",
4821
+ penetrationScale: 1.2,
4822
+ ductility: 0.03,
4823
+ fracture: "crumble",
4824
+ fragmentSize: [
4825
+ 0.02,
4826
+ 0.08
4827
+ ],
4828
+ hitDebris: "chips",
4829
+ breakDebris: "dust",
4830
+ thermal: {
4831
+ conductivity: 0.15,
4832
+ heatCapacity: 2.5
4833
+ },
4834
+ tags: [
4835
+ "masonry",
4836
+ "brittle"
4837
+ ],
4838
+ compressive: 1e5
4614
4839
  }
4615
4840
  ];
4616
4841
  var material_hints_default = {
@@ -4784,6 +5009,32 @@ var GameFloorplanStructure = (() => {
4784
5009
  return (1 + 4 * (0.3 - ductility) / 0.3) * Math.min(25, Math.max(1, (0.02 / Math.max(thickness, 1e-4)) ** 2));
4785
5010
  };
4786
5011
  var THIN_SHEET_DISHING = 5e-3;
5012
+ var SWEEP_BACK = 0.01;
5013
+ function sweepOrigins(box2, d, spacing) {
5014
+ const origins = [];
5015
+ const line = (from, to) => {
5016
+ const n = Math.max(1, Math.ceil((to - from) / spacing));
5017
+ return Array.from({ length: n + 1 }, (_, i) => from + (to - from) * i / n);
5018
+ };
5019
+ for (const axis of [0, 1, 2]) {
5020
+ if (Math.abs(d[axis]) < 1e-3) continue;
5021
+ const lead = d[axis] > 0 ? box2.max[axis] - SWEEP_BACK : box2.min[axis] + SWEEP_BACK;
5022
+ const u = (axis + 1) % 3;
5023
+ const v = (axis + 2) % 3;
5024
+ for (const a of line(box2.min[u], box2.max[u])) {
5025
+ for (const b of line(box2.min[v], box2.max[v])) {
5026
+ const point = [0, 0, 0];
5027
+ point[axis] = lead;
5028
+ point[u] = a;
5029
+ point[v] = b;
5030
+ origins.push(point);
5031
+ }
5032
+ }
5033
+ }
5034
+ return origins;
5035
+ }
5036
+ var THIN_SHEET_CRUSH = 5e-4;
5037
+ var THIN_SHEET_CONTACT = 0.025;
4787
5038
  var DMG_PER_J = 0.02;
4788
5039
  var BLAST_COUPLING = 0.1;
4789
5040
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
@@ -4797,9 +5048,17 @@ var GameFloorplanStructure = (() => {
4797
5048
  var BREACH_AREA = 0.25;
4798
5049
  var BREACH_BLAST_REACH = 0.12;
4799
5050
  var RESIDUAL_CAPACITY = 0.15;
5051
+ var SEVER_BAND = 0.1;
5052
+ var SEVER_ASPECT = 3;
5053
+ var SEVER_BALLISTIC = 2;
5054
+ var TEAR_JOULES = 150;
5055
+ var tears = 0;
4800
5056
  var SUPPORT_TOLERANCE = 0.05;
4801
5057
  var UNIT_FRAGMENT_FACTOR = 4;
4802
5058
  var BREACH_GRID_CAP = 4096;
5059
+ var CHIP_ENERGY = 200;
5060
+ var CHIP_SPLASH = 0.5;
5061
+ var BAR_BOW = 0.08;
4803
5062
  var CUT_SLICE = 1;
4804
5063
  var cutFactorOf = (shape) => clamp22(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
4805
5064
  var clamp22 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -4809,8 +5068,24 @@ var GameFloorplanStructure = (() => {
4809
5068
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
4810
5069
  };
4811
5070
  var capacityScale = (p) => {
4812
- if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
4813
- return p.rt.removed ? Math.max(0.05, 1 - p.rt.removed / p.mass0) : 1;
5071
+ const residual = p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
5072
+ if (p.st.stripped) return residual;
5073
+ const kept = Math.min(p.rt.removed ? 1 - p.rt.removed / p.mass0 : 1, 1 - sectionLoss(p));
5074
+ return Math.max(p.spec.reinforcement ? residual : 0.05, kept);
5075
+ };
5076
+ var sectionLoss = (p) => {
5077
+ const cells = p.st.chipped;
5078
+ if (!cells?.length) return 0;
5079
+ const [u, v] = planeAxes(p.thin);
5080
+ const along = p.dims[u] >= p.dims[v] ? u : v;
5081
+ const across = along === u ? v : u;
5082
+ let worst = 0;
5083
+ for (const c of cells) {
5084
+ let gone = 0;
5085
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across];
5086
+ worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
5087
+ }
5088
+ return Math.min(1, worst);
4814
5089
  };
4815
5090
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
4816
5091
  var clone = (v) => structuredClone(v);
@@ -4843,6 +5118,7 @@ var GameFloorplanStructure = (() => {
4843
5118
  let windCfg = windOpts ? resolveWind(windOpts) : null;
4844
5119
  const ignitionRules = resolveIgnitionRules(options.ignition);
4845
5120
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
5121
+ const chipEnergy = options.breach?.chipEnergy ?? CHIP_ENERGY;
4846
5122
  const rng = createRng(options.seed ?? 1);
4847
5123
  const rnd = () => rng.next();
4848
5124
  let time = 0;
@@ -5216,41 +5492,68 @@ var GameFloorplanStructure = (() => {
5216
5492
  markDamaged(p);
5217
5493
  if (p.rt.hinge >= flexure.failRotationDeg * Math.PI / 180) hingeDrop(o, p, src);
5218
5494
  };
5495
+ const freeSpan = (p, supports) => {
5496
+ if (supports.length === 0) return Infinity;
5497
+ const d = p.dims;
5498
+ const L = [0, 1, 2].sort((a, b) => d[b] - d[a])[0];
5499
+ const spans = supports.map((k) => contactBox(p, k)).map(([lo, hi]) => [lo[L], hi[L]]);
5500
+ let far = 0;
5501
+ for (let i = 0; i <= 16; i++) {
5502
+ const x = (i / 16 - 0.5) * d[L];
5503
+ let near = Infinity;
5504
+ for (const [lo, hi] of spans) near = Math.min(near, Math.max(lo - x, x - hi, 0));
5505
+ far = Math.max(far, near);
5506
+ }
5507
+ return 2 * far;
5508
+ };
5219
5509
  const solveStructure = (o) => {
5220
5510
  o.maxStress = 0;
5221
5511
  if (!o.hasStructure) return;
5222
5512
  const parts = o.parts;
5223
5513
  const n = parts.length;
5224
5514
  const dist = new Array(n).fill(-1);
5225
- const queue = [];
5226
5515
  const ok = (i) => parts[i].structural && !isLost(parts[i]);
5227
5516
  const isPartition = (i) => parts[i].spec.structure?.role === "partition";
5228
- for (let i = 0; i < n; i++) if (ok(i) && parts[i].anchored) {
5229
- dist[i] = 0;
5230
- queue.push(i);
5231
- }
5232
- for (let h = 0; h < queue.length; h++) {
5233
- const s = queue[h];
5234
- if (isPartition(s)) continue;
5235
- for (const j of o.byPart.get(parts[s].spec.id)) {
5236
- if (j.st.open || !j.bears) continue;
5237
- const r = j.ia === s ? j.ib : j.ia;
5238
- if (dist[r] >= 0 || !ok(r) || restsOn(o, j, r) !== s) continue;
5239
- dist[r] = dist[s] + 1;
5240
- queue.push(r);
5517
+ const out = /* @__PURE__ */ new Set();
5518
+ let queue = [];
5519
+ let lowerOf = /* @__PURE__ */ new Map();
5520
+ for (; ; ) {
5521
+ dist.fill(-1);
5522
+ queue = [];
5523
+ for (let i = 0; i < n; i++) if (ok(i) && !out.has(i) && parts[i].anchored) {
5524
+ dist[i] = 0;
5525
+ queue.push(i);
5241
5526
  }
5242
- }
5243
- const incoming = new Array(n).fill(0);
5244
- const lowerOf = /* @__PURE__ */ new Map();
5245
- for (const i of queue) {
5246
- const lower = [];
5247
- for (const j of o.byPart.get(parts[i].spec.id)) {
5248
- if (j.st.open || !j.bears) continue;
5249
- const k = restsOn(o, j, i);
5250
- if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
5527
+ for (let h = 0; h < queue.length; h++) {
5528
+ const s = queue[h];
5529
+ if (isPartition(s)) continue;
5530
+ for (const j of o.byPart.get(parts[s].spec.id)) {
5531
+ if (j.st.open || !j.bears) continue;
5532
+ const r = j.ia === s ? j.ib : j.ia;
5533
+ if (dist[r] >= 0 || !ok(r) || out.has(r) || restsOn(o, j, r) !== s) continue;
5534
+ dist[r] = dist[s] + 1;
5535
+ queue.push(r);
5536
+ }
5251
5537
  }
5252
- lowerOf.set(i, lower);
5538
+ lowerOf = /* @__PURE__ */ new Map();
5539
+ let dropped = false;
5540
+ for (const i of queue) {
5541
+ const lower = [];
5542
+ for (const j of o.byPart.get(parts[i].spec.id)) {
5543
+ if (j.st.open || !j.bears) continue;
5544
+ const k = restsOn(o, j, i);
5545
+ if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
5546
+ }
5547
+ lowerOf.set(i, lower);
5548
+ const max = parts[i].spec.structure?.maxSpan;
5549
+ if (max !== void 0 && !parts[i].anchored && freeSpan(parts[i], lower.map((k) => parts[k])) > max) {
5550
+ out.add(i);
5551
+ dropped = true;
5552
+ }
5553
+ }
5554
+ if (!dropped) break;
5253
5555
  }
5556
+ const incoming = new Array(n).fill(0);
5254
5557
  const bending = new Array(n).fill(0);
5255
5558
  const through = parts.map(() => []);
5256
5559
  const pass = (k, at2, mass) => {
@@ -5762,6 +6065,7 @@ var GameFloorplanStructure = (() => {
5762
6065
  const r = rayPart(o, p, origin, d);
5763
6066
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
5764
6067
  if (p.st.opening && insideOpening(o, p, vec.madd(origin, d, r.t0))) continue;
6068
+ if (p.st.chipped && insideChip(p, objLocal(o, vec.madd(origin, d, r.t0)))) continue;
5765
6069
  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 };
5766
6070
  }
5767
6071
  }
@@ -5962,6 +6266,12 @@ var GameFloorplanStructure = (() => {
5962
6266
  debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
5963
6267
  setStage(o, p, 1, {}, src);
5964
6268
  };
6269
+ const crater = (o, p, wp, normal, radius, src) => {
6270
+ markDamaged(p);
6271
+ emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: -1, point: wp, area: Math.PI * radius * radius });
6272
+ debris(o, p, p.mat.breakDebris, wp, normal, 6, 1.5, src);
6273
+ setStage(o, p, 1, {}, src);
6274
+ };
5965
6275
  const insideOpening = (o, p, objectPoint) => {
5966
6276
  const op = p.st.opening;
5967
6277
  if (!op) return false;
@@ -6025,6 +6335,91 @@ var GameFloorplanStructure = (() => {
6025
6335
  if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
6026
6336
  openBreach(o, p, [sum[0] / total, sum[1] / total, sum[2] / total], total, energy, dir, src);
6027
6337
  };
6338
+ const unitGrids = /* @__PURE__ */ new WeakMap();
6339
+ const gridOf = (p) => {
6340
+ let grid = unitGrids.get(p);
6341
+ if (!grid) unitGrids.set(p, grid = unitGrid(p.dims, p.thin, unitsOf2(p), BREACH_GRID_CAP));
6342
+ return grid;
6343
+ };
6344
+ const insideChip = (p, objectPoint) => {
6345
+ const cells = p.st.chipped;
6346
+ if (!cells?.length) return false;
6347
+ const [u, v] = planeAxes(p.thin);
6348
+ const lp = toLocal(p.pos, p.q, objectPoint);
6349
+ 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);
6350
+ };
6351
+ const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
6352
+ var _a, _b;
6353
+ if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
6354
+ const rule = p.mat.hits?.[kind];
6355
+ const perKg = rule?.unitJoules ?? chipEnergy;
6356
+ const energy = hitEnergy - (rule?.floor ?? 0);
6357
+ if (perKg <= 0 || energy <= 0) return;
6358
+ const [u, v] = planeAxes(p.thin);
6359
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
6360
+ const grid = gridOf(p);
6361
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
6362
+ const out = [];
6363
+ for (let i = 0; i < grid.length; i++) {
6364
+ const cell = grid[i];
6365
+ if (worn[i] === -1) continue;
6366
+ 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));
6367
+ if (gap > radius) continue;
6368
+ const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
6369
+ const total = (worn[i] ?? 0) + energy * share;
6370
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
6371
+ if (total < perKg * mass) {
6372
+ worn[i] = total;
6373
+ continue;
6374
+ }
6375
+ worn[i] = -1;
6376
+ out.push(cell);
6377
+ }
6378
+ if (out.length === 0) return;
6379
+ const { c, q } = partXf(o, p);
6380
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
6381
+ const holes = localHolesOf(p);
6382
+ for (const cell of out) {
6383
+ const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
6384
+ const center = toWorld(c, q, cell.center);
6385
+ const speed = clamp22(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
6386
+ const fragments = cellFragments(o, p, [cell], wp, dir, speed, mass, `c${chipped.length}_`);
6387
+ p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
6388
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
6389
+ p.st.mass = Math.max(0, p.st.mass - mass);
6390
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
6391
+ markDamaged(p);
6392
+ setStage(o, p, 1, {}, src);
6393
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
6394
+ debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
6395
+ }
6396
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
6397
+ for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
6398
+ if (p.structural && !isLost(p)) {
6399
+ solveStructure(o);
6400
+ collapseCheck(o, src);
6401
+ }
6402
+ };
6403
+ const exposeRebar = (o, p, src) => {
6404
+ const r = p.spec.reinforcement;
6405
+ if (!r) return;
6406
+ const [u, v] = planeAxes(p.thin);
6407
+ const all = rebarBars(p.dims, p.thin, r.spacing);
6408
+ let bars;
6409
+ if (p.st.stripped) bars = all;
6410
+ else {
6411
+ bars = [];
6412
+ for (const cell of p.st.chipped ?? []) bars.push(...clipBarsToBox(all, u, v, cell.center, cell.size));
6413
+ const op = p.st.opening;
6414
+ if (op) {
6415
+ const lc = toLocal(p.pos, p.q, op.center);
6416
+ bars.push(...clipBarsToDisc(all, u, v, [lc[u], lc[v]], op.radius + 0.05));
6417
+ }
6418
+ }
6419
+ const bow = clamp22(((p.st.stress ?? 0) - 1) / 0.5, 0, 1);
6420
+ if (bow > 0) bars = bars.flatMap((bar2, i) => bendBar(bar2, p.thin, bow * BAR_BOW, i));
6421
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
6422
+ };
6028
6423
  const openBreach = (o, p, center, total, energy, dir, src) => {
6029
6424
  const [u, v] = planeAxes(p.thin);
6030
6425
  const radius = clamp22(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
@@ -6036,7 +6431,8 @@ var GameFloorplanStructure = (() => {
6036
6431
  failPart(o, p, worldCenter, dir, energy, 12, src);
6037
6432
  return;
6038
6433
  }
6039
- const cells = cellsWithin(unitGrid(p.dims, p.thin, unitsOf2(p), BREACH_GRID_CAP), p.thin, lc, radius);
6434
+ const worn = p.rt.chips;
6435
+ const cells = cellsWithin(gridOf(p).filter((_, i) => worn?.[i] !== -1), p.thin, lc, radius);
6040
6436
  const removed = p.st.mass * cells.reduce((sum, cell) => sum + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
6041
6437
  const speed = clamp22(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
6042
6438
  const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
@@ -6050,10 +6446,7 @@ var GameFloorplanStructure = (() => {
6050
6446
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
6051
6447
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
6052
6448
  const reinforcement = p.spec.reinforcement;
6053
- if (reinforcement) {
6054
- const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
6055
- if (bars.length > 0) emit({ ...base(o, src), type: "expose", part: p.spec.id, bars });
6056
- }
6449
+ if (reinforcement) exposeRebar(o, p, src);
6057
6450
  if (p.structural) solveStructure(o);
6058
6451
  };
6059
6452
  const stripPart = (o, p, reinforcement, src) => {
@@ -6065,8 +6458,8 @@ var GameFloorplanStructure = (() => {
6065
6458
  p.st.burning = false;
6066
6459
  p.rt.jet = false;
6067
6460
  markDamaged(p);
6068
- emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
6069
6461
  solveStructure(o);
6462
+ exposeRebar(o, p, src);
6070
6463
  collapseCheck(o, src);
6071
6464
  };
6072
6465
  const unitFragmentPlan = (p, cap) => {
@@ -6120,6 +6513,8 @@ var GameFloorplanStructure = (() => {
6120
6513
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
6121
6514
  const hitLocal = toLocal(c, q, hitW);
6122
6515
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
6516
+ const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
6517
+ const cutAt2 = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
6123
6518
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
6124
6519
  style,
6125
6520
  dims: p.dims,
@@ -6129,7 +6524,8 @@ var GameFloorplanStructure = (() => {
6129
6524
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
6130
6525
  maxFragments,
6131
6526
  rnd,
6132
- sheet: p.mat.sheet
6527
+ sheet: p.mat.sheet,
6528
+ ...cutAt2 !== void 0 ? { cut: { at: cutAt2, gap: SEVER_BAND } } : {}
6133
6529
  });
6134
6530
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
6135
6531
  const mass = p.st.mass;
@@ -6188,10 +6584,33 @@ var GameFloorplanStructure = (() => {
6188
6584
  emit({ ...base(o, src), type: "primed", part: p.spec.id, point: partXf(o, p).c, trigger, delay });
6189
6585
  };
6190
6586
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
6587
+ const severAxis = (p) => {
6588
+ if (p.mat.fracture !== "splinter" && p.mat.fracture !== "snap" || p.spec.shape.kind !== "box") return -1;
6589
+ const d = p.dims;
6590
+ const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
6591
+ return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
6592
+ };
6593
+ const bandOf = (o, p, axis, origin) => {
6594
+ const { c, q } = partXf(o, p);
6595
+ const along = toLocal(c, q, origin)[axis];
6596
+ const L = p.dims[axis];
6597
+ return clamp22(Math.floor((along + L / 2) / SEVER_BAND), 0, Math.ceil(L / SEVER_BAND) - 1);
6598
+ };
6191
6599
  const applyDamage = (o, p, amount, type, origin, dir, fragEnergy, cap, src) => {
6600
+ var _a;
6192
6601
  if (isLost(p) || amount <= 0) return 0;
6193
6602
  const prev = p.st.hp;
6194
6603
  p.st.hp -= amount;
6604
+ const axis = origin && type !== "fire" && type !== "heat" ? severAxis(p) : -1;
6605
+ if (axis >= 0 && p.st.hp > 0) {
6606
+ const band = bandOf(o, p, axis, origin);
6607
+ const cuts = (_a = p.rt).cuts ?? (_a.cuts = {});
6608
+ cuts[band] = (cuts[band] ?? 0) + amount * (type === "ballistic" ? SEVER_BALLISTIC : 1);
6609
+ if (cuts[band] >= p.st.maxHp * SEVER_BAND / p.dims[axis]) {
6610
+ p.rt.cutAt = -p.dims[axis] / 2 + (band + 0.5) * SEVER_BAND;
6611
+ p.st.hp = 0;
6612
+ }
6613
+ }
6195
6614
  markDamaged(p);
6196
6615
  const trigger = CHAIN_TRIGGER_OF[type];
6197
6616
  const chained = trigger !== void 0 && chainDelayOf(p.spec, trigger) !== void 0;
@@ -6227,10 +6646,53 @@ var GameFloorplanStructure = (() => {
6227
6646
  }
6228
6647
  }
6229
6648
  };
6649
+ const tearOf = (mat, kind) => {
6650
+ const h = mat.hits?.[kind];
6651
+ const tear = h?.tear ?? (kind === "ballistic" && mat.penetrationClass === "wood" ? "splinters" : "none");
6652
+ return { tear, joules: h?.tearJoules ?? TEAR_JOULES };
6653
+ };
6654
+ const tearAt = (o, p, kind, entry2, exit, d, E, scale, src) => {
6655
+ const { tear, joules } = tearOf(p.mat, kind);
6656
+ if (tear === "none") return;
6657
+ const shot = kind === "ballistic";
6658
+ const n = shot ? exit ? clamp22(Math.round(E / joules), 1, 7) : 1 : clamp22(Math.floor(E / joules), 0, 5);
6659
+ if (n === 0) return;
6660
+ const fibre = tear === "lumps";
6661
+ const { q } = partXf(o, p);
6662
+ const along = (p.spec.grain ? AXIS[p.spec.grain] : void 0) ?? [0, 1, 2].sort((a, b) => p.dims[b] - p.dims[a])[0];
6663
+ const fragments = [];
6664
+ for (let i = 0; i < n; i++) {
6665
+ const back = !exit || i % 3 === 2;
6666
+ 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());
6667
+ const size = [w, w, w];
6668
+ size[along] = len;
6669
+ size[(along + 1) % 3] = w;
6670
+ size[(along + 2) % 3] = t;
6671
+ const spread3 = [(rnd() - 0.5) * 0.8, (rnd() - 0.2) * 0.8, (rnd() - 0.5) * 0.8];
6672
+ const speed = shot ? back ? 1.5 + 2 * rnd() : 2 + 4 * rnd() : 0.8 + 1.2 * rnd();
6673
+ const v = vec.scale(vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3)), speed);
6674
+ const mass = size[0] * size[1] * size[2] * p.mat.density;
6675
+ fragments.push({
6676
+ id: `${p.spec.id}#t${tears++}-${i}`,
6677
+ part: p.spec.id,
6678
+ material: p.spec.material,
6679
+ shape: fibre ? "chunk" : "splinter",
6680
+ center: back || !exit ? vec.madd(entry2, d, -0.01) : vec.madd(exit, d, 0.01),
6681
+ size,
6682
+ rotation: q,
6683
+ velocity: v,
6684
+ inner: true,
6685
+ angularVelocity: [(rnd() - 0.5) * 30, (rnd() - 0.5) * 30, (rnd() - 0.5) * 30],
6686
+ mass
6687
+ });
6688
+ }
6689
+ const patch = [scale * 4, scale * 4, scale * 4];
6690
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments });
6691
+ };
6230
6692
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
6231
6693
  const mat = p.mat;
6232
6694
  const piercing = round3.piercing ?? 1;
6233
- if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry2))) {
6695
+ if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry2)) || insideChip(p, objLocal(o, entry2))) {
6234
6696
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry2, d, Math.min(pathLen, p.thickness)), damage: 0 };
6235
6697
  }
6236
6698
  const sin = Math.min(1, Math.abs(vec.dot(d, n)));
@@ -6255,6 +6717,7 @@ var GameFloorplanStructure = (() => {
6255
6717
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
6256
6718
  }
6257
6719
  if (p.layers.length > 0) spallOuter(o, p, entry2, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
6720
+ else if (mat.hits?.ballistic?.crater) crater(o, p, entry2, n, clamp22(0.02 + Math.sqrt(E) * 15e-4, 0.02, 0.15), src);
6258
6721
  const brittle = mat.ductility < 0.3;
6259
6722
  const ratio = Ep / Math.max(thr, 1e-6);
6260
6723
  const crackLen = clamp22(0.03 + ratio * 0.2, 0.03, 0.8);
@@ -6288,7 +6751,10 @@ var GameFloorplanStructure = (() => {
6288
6751
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
6289
6752
  debris(o, p, mat.hitDebris, entry2, n, clamp22(Math.round(E / 60) + 2, 2, 14), clamp22(Math.sqrt(E) / 6, 1, 8), src);
6290
6753
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
6291
- const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry2, d, E * 0.1, 12, src);
6754
+ const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
6755
+ const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
6756
+ if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
6757
+ chipUnits(o, p, "ballistic", entry2, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
6292
6758
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
6293
6759
  };
6294
6760
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -6296,14 +6762,20 @@ var GameFloorplanStructure = (() => {
6296
6762
  const thrMm = mat.penetration * p.thickness * 1e3;
6297
6763
  const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
6298
6764
  const Ec = E * fragile;
6765
+ const sheet = mat.ductility >= 0.5 && p.thickness <= THIN_SHEET_CRUSH;
6766
+ const fold = sheet ? Math.min(4, Math.sqrt(1e-3 / p.thickness)) : 1;
6767
+ const patch = sheet ? Math.min(contactR, THIN_SHEET_CONTACT) : contactR;
6299
6768
  if (E > 0) {
6300
6769
  if (kind === "blunt") {
6301
- if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp22(E * 1e-4, 1e-3, 0.06), src);
6770
+ if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(patch, 0.015), clamp22(E * 1e-4 * fold, 1e-3, sheet ? Math.min(0.06, Math.min(...p.dims) * 0.45) : 0.06), src);
6302
6771
  else if (mat.ductility < 0.3) {
6303
6772
  if (Ec >= 5) addCrack(o, p, point, n, clamp22(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
6304
6773
  } else if (E >= thrMm * 0.2) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp22(E * 6e-5, 1e-3, 0.04), src);
6305
6774
  } else if (mat.fracture === "tear") {
6306
6775
  addHole(o, p, point, n, clamp22(0.02 + E * 5e-4, 0.02, 0.3), true, void 0, src);
6776
+ } else if (mat.ductility >= 0.3 && E >= thrMm) {
6777
+ addHole(o, p, point, n, clamp22(contactR, 0.01, 0.08), true, void 0, src);
6778
+ if (sheet) addDent(o, p, point, n, Math.max(patch * 0.6, 0.01), clamp22(E * 4e-5 * fold, 5e-4, 0.02), src);
6307
6779
  } else if (mat.ductility >= 0.5) {
6308
6780
  addDent(o, p, point, n, Math.max(contactR, 0.01), clamp22(E * 4e-5, 5e-4, 0.02), src);
6309
6781
  }
@@ -6316,6 +6788,8 @@ var GameFloorplanStructure = (() => {
6316
6788
  const scale = mat.damageScale?.[kind] ?? 1;
6317
6789
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
6318
6790
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
6791
+ if (kind === "blunt") chipUnits(o, p, "blunt", point, E, d, Math.max(contactR, 0.03), src);
6792
+ if (E > 0 && !isLost(p)) tearAt(o, p, kind, point, void 0, d, E, clamp22(contactR * 0.2, 6e-3, 0.02), src);
6319
6793
  return done;
6320
6794
  };
6321
6795
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -6894,6 +7368,7 @@ var GameFloorplanStructure = (() => {
6894
7368
  if (p.rt.stressT <= 0) {
6895
7369
  p.rt.stressT = 0.5;
6896
7370
  emit({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
7371
+ if (p.spec.reinforcement && (p.st.stripped || p.st.chipped?.length || p.st.opening)) exposeRebar(o, p);
6897
7372
  }
6898
7373
  applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
6899
7374
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
@@ -7110,6 +7585,18 @@ var GameFloorplanStructure = (() => {
7110
7585
  const r = raycastInternal(origin, direction, maxDistance);
7111
7586
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
7112
7587
  },
7588
+ sweepBox(box2, direction, distance2, options2 = {}) {
7589
+ const d = vec.normalize(direction);
7590
+ const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
7591
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
7592
+ let best = null;
7593
+ for (const origin of sweepOrigins(box2, d, options2.spacing ?? 0.2)) {
7594
+ const r = raycastInternal(origin, d, distance2 + SWEEP_BACK, ignore);
7595
+ if (!r || best && r.distance >= best.distance) continue;
7596
+ best = { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: Math.max(0, r.distance - SWEEP_BACK) };
7597
+ }
7598
+ return best;
7599
+ },
7113
7600
  shoot(origin, direction, energy, caliber, source, round3) {
7114
7601
  const extras = typeof round3 === "number" ? { piercing: round3 } : round3 ?? {};
7115
7602
  return withCollapses(run(() => {
@@ -8534,7 +9021,24 @@ var GameFloorplanStructure = (() => {
8534
9021
  }
8535
9022
  return spec;
8536
9023
  };
8537
- var partSpecOf = (part, capacity, detailed) => detailed && part.assembly ? assemblySpec(part, capacity) : boxSpec(part, capacity);
9024
+ var MEMBER_CAGE = { spacing: 0.1, diameter: 8e-3, unit: 0.1 };
9025
+ var CAGED_ROLES = /* @__PURE__ */ new Set(["beam", "column"]);
9026
+ var caged = (spec, part, rebar) => {
9027
+ if (!rebar || part.material !== "concrete" || !CAGED_ROLES.has(part.role) || spec.reinforcement) return spec;
9028
+ const size = spec.shape.kind === "box" ? spec.shape.size : null;
9029
+ if (!size) return spec;
9030
+ const thin = Math.min(...size);
9031
+ return {
9032
+ ...spec,
9033
+ reinforcement: { material: rebar, spacing: MEMBER_CAGE.spacing, diameter: MEMBER_CAGE.diameter },
9034
+ units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, thin], bond: "stack", mortar: "concrete" }
9035
+ };
9036
+ };
9037
+ var partSpecOf = (part, capacity, detailed, rebar) => {
9038
+ if (detailed && part.assembly) return assemblySpec(part, capacity);
9039
+ const spec = boxSpec(part, capacity);
9040
+ return detailed ? caged(spec, part, rebar) : spec;
9041
+ };
8538
9042
  function toDestructibleSpec(structure, options = {}) {
8539
9043
  const { safety = 0.65, redundancy = 0.97, safetyOthers = 0.45 } = options;
8540
9044
  const kept = structure.parts.filter((part) => !options.filter || options.filter(part));
@@ -8551,7 +9055,7 @@ var GameFloorplanStructure = (() => {
8551
9055
  const sizing = { safety, redundancy, safetyOthers };
8552
9056
  const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts, panels.supports, sizing) };
8553
9057
  const { parts, supports, capacities } = detail === "units" ? applyUnits(sized, structure) : sized;
8554
- const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse"));
9058
+ const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse", structure.construction.frame.rebar));
8555
9059
  const margin = options.terrainMargin ?? TERRAIN_MARGIN;
8556
9060
  const x0 = Math.min(...parts.map((part) => part.aabb[0]));
8557
9061
  const z0 = Math.min(...parts.map((part) => part.aabb[1]));