@adatechnology/game-weapons-three 0.10.0 → 0.10.2

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.
@@ -19681,6 +19681,47 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19681
19681
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
19682
19682
  };
19683
19683
  var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
19684
+ var splinterBox = (inp, box5, limit = inp.maxFragments) => {
19685
+ const { rnd: rnd2 } = inp;
19686
+ const dims = box5.s;
19687
+ const [fmin, fmax] = inp.fragmentSize;
19688
+ const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
19689
+ const [a, b] = [0, 1, 2].filter((i) => i !== g);
19690
+ const w = Math.max(fmin * 1.5, 0.01);
19691
+ const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
19692
+ const maxLen = fmax * 4;
19693
+ const cells = bsp(box5, c, [a, b], rnd2);
19694
+ const pieces = [];
19695
+ for (const cell of cells) {
19696
+ const extra = rnd2() < 0.4 ? 1 : 0;
19697
+ const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
19698
+ for (const p of bsp(cell, k, [g], rnd2)) pieces.push(p);
19699
+ }
19700
+ 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] }));
19701
+ };
19702
+ var severAt = (inp, root, at3, gap) => {
19703
+ const dims = root.s;
19704
+ const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
19705
+ const half = dims[L] / 2;
19706
+ const lo = clamp(at3 - gap / 2, -half, half), hi = clamp(at3 + gap / 2, -half, half);
19707
+ const out = [];
19708
+ const stub = (from, to) => {
19709
+ if (to - from < 0.02) return;
19710
+ const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
19711
+ c[L] = (from + to) / 2;
19712
+ sz[L] = to - from;
19713
+ out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight: sz[0] * sz[1] * sz[2] });
19714
+ };
19715
+ stub(-half, lo);
19716
+ stub(hi, half);
19717
+ const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
19718
+ band.c[L] = (lo + hi) / 2;
19719
+ band.s[L] = Math.max(hi - lo, 0.01);
19720
+ if (inp.style === "snap") {
19721
+ 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] });
19722
+ } else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
19723
+ return out;
19724
+ };
19684
19725
  var tearStack = (inp, root, t, u, v, n, sheet) => {
19685
19726
  const { dims, rnd: rnd2, maxFragments } = inp;
19686
19727
  const layers = clamp(Math.ceil(dims[t] / STACK_LAYER), 2, 4);
@@ -19709,6 +19750,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19709
19750
  const t = sorted[0];
19710
19751
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
19711
19752
  if (style === "snap") {
19753
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
19712
19754
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd2() * 3));
19713
19755
  for (const b of bsp(root, n2, [0, 1, 2], rnd2)) out.push({ center: b.c, size: b.s, q: idq, shape: "slab", weight: b.s[0] * b.s[1] * b.s[2] });
19714
19756
  return out;
@@ -19719,23 +19761,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19719
19761
  return out;
19720
19762
  }
19721
19763
  if (style === "splinter") {
19722
- const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
19723
- const [a, b] = [0, 1, 2].filter((i) => i !== g);
19724
- const w = Math.max(fmin * 1.5, 0.01);
19725
- const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(maxFragments / 2)));
19726
- const maxLen = fmax * 4;
19727
- const cells = bsp(root, c, [a, b], rnd2);
19728
- const pieces = [];
19729
- for (const cell of cells) {
19730
- let list2 = [cell];
19731
- const extra = rnd2() < 0.4 ? 1 : 0;
19732
- const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
19733
- list2 = bsp(cell, k, [g], rnd2);
19734
- for (const p of list2) pieces.push(p);
19735
- }
19736
- for (const p of pieces.slice(0, maxFragments)) {
19737
- 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] });
19738
- }
19764
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
19765
+ for (const p of splinterBox(inp, root)) out.push(p);
19739
19766
  return out;
19740
19767
  }
19741
19768
  if (style === "tear") {
@@ -19897,6 +19924,27 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19897
19924
  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]]));
19898
19925
  return out;
19899
19926
  };
19927
+ var clipBarsToBox = (bars, u, v, center, size) => {
19928
+ const out = [];
19929
+ for (const [a, b] of bars) {
19930
+ const axis2 = a[u] !== b[u] ? u : v;
19931
+ const other = axis2 === u ? v : u;
19932
+ if (Math.abs(a[other] - center[other]) > size[other] / 2) continue;
19933
+ const lo = Math.max(Math.min(a[axis2], b[axis2]), center[axis2] - size[axis2] / 2), hi = Math.min(Math.max(a[axis2], b[axis2]), center[axis2] + size[axis2] / 2);
19934
+ if (hi - lo < 1e-6) continue;
19935
+ const p = [a[0], a[1], a[2]], q = [b[0], b[1], b[2]];
19936
+ p[axis2] = lo;
19937
+ q[axis2] = hi;
19938
+ out.push([p, q]);
19939
+ }
19940
+ return out;
19941
+ };
19942
+ var bendBar = ([a, b], thin, bow, i) => {
19943
+ const length2 = Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
19944
+ const m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2];
19945
+ m[thin] += (i % 2 === 0 ? 1 : -1) * bow * length2;
19946
+ return [[a, m], [m, b]];
19947
+ };
19900
19948
  var clipBarsToDisc = (bars, u, v, center, radius) => {
19901
19949
  const out = [];
19902
19950
  for (const [a, b] of bars) {
@@ -20817,14 +20865,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20817
20865
  smoke: "grey"
20818
20866
  },
20819
20867
  visual: {
20820
- charColor: 1709072
20868
+ charColor: 1709072,
20869
+ innerColor: 14730131
20821
20870
  },
20822
20871
  tags: [
20823
20872
  "wood",
20824
20873
  "organic",
20825
20874
  "flammable"
20826
20875
  ],
20827
- compressive: 1e5
20876
+ compressive: 1e5,
20877
+ hits: {
20878
+ blunt: {
20879
+ tear: "splinters",
20880
+ tearJoules: 60
20881
+ }
20882
+ }
20828
20883
  },
20829
20884
  {
20830
20885
  id: "wood_hard",
@@ -20854,14 +20909,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20854
20909
  smoke: "grey"
20855
20910
  },
20856
20911
  visual: {
20857
- charColor: 1709072
20912
+ charColor: 1709072,
20913
+ innerColor: 13213802
20858
20914
  },
20859
20915
  tags: [
20860
20916
  "wood",
20861
20917
  "organic",
20862
20918
  "flammable"
20863
20919
  ],
20864
- compressive: 15e4
20920
+ compressive: 15e4,
20921
+ hits: {
20922
+ blunt: {
20923
+ tear: "splinters",
20924
+ tearJoules: 150
20925
+ }
20926
+ }
20865
20927
  },
20866
20928
  {
20867
20929
  id: "plywood",
@@ -20891,14 +20953,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20891
20953
  smoke: "grey"
20892
20954
  },
20893
20955
  visual: {
20894
- charColor: 1709072
20956
+ charColor: 1709072,
20957
+ innerColor: 14862234
20895
20958
  },
20896
20959
  tags: [
20897
20960
  "wood",
20898
20961
  "organic",
20899
20962
  "flammable"
20900
20963
  ],
20901
- compressive: 6e4
20964
+ compressive: 6e4,
20965
+ hits: {
20966
+ ballistic: {
20967
+ tear: "splinters"
20968
+ },
20969
+ blunt: {
20970
+ tear: "splinters",
20971
+ tearJoules: 80
20972
+ }
20973
+ }
20902
20974
  },
20903
20975
  {
20904
20976
  id: "mdf",
@@ -20925,14 +20997,25 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20925
20997
  smoke: "grey"
20926
20998
  },
20927
20999
  visual: {
20928
- charColor: 1709072
21000
+ charColor: 1709072,
21001
+ innerColor: 12096362
20929
21002
  },
20930
21003
  tags: [
20931
21004
  "wood",
20932
21005
  "organic",
20933
21006
  "flammable"
20934
21007
  ],
20935
- compressive: 3e4
21008
+ compressive: 3e4,
21009
+ hits: {
21010
+ ballistic: {
21011
+ tear: "lumps",
21012
+ tearJoules: 120
21013
+ },
21014
+ blunt: {
21015
+ tear: "lumps",
21016
+ tearJoules: 20
21017
+ }
21018
+ }
20936
21019
  },
20937
21020
  {
20938
21021
  id: "glass",
@@ -21046,6 +21129,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21046
21129
  damageScale: {
21047
21130
  explosive: 1.2
21048
21131
  },
21132
+ hits: {
21133
+ ballistic: {
21134
+ unitJoules: 500,
21135
+ floor: 900,
21136
+ crater: true
21137
+ },
21138
+ blunt: {
21139
+ unitJoules: 1500,
21140
+ floor: 60
21141
+ }
21142
+ },
21049
21143
  hitDebris: "chips",
21050
21144
  breakDebris: "dust",
21051
21145
  thermal: {
@@ -21414,7 +21508,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21414
21508
  smoke: "white"
21415
21509
  },
21416
21510
  visual: {
21417
- charColor: 1709072
21511
+ charColor: 1709072,
21512
+ innerColor: 14207126
21418
21513
  },
21419
21514
  tags: [
21420
21515
  "plant",
@@ -21422,7 +21517,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21422
21517
  "organic",
21423
21518
  "flammable"
21424
21519
  ],
21425
- compressive: 6e4
21520
+ compressive: 6e4,
21521
+ hits: {
21522
+ blunt: {
21523
+ tear: "none"
21524
+ }
21525
+ }
21426
21526
  },
21427
21527
  {
21428
21528
  id: "cactus",
@@ -21537,6 +21637,130 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21537
21637
  "masonry"
21538
21638
  ],
21539
21639
  compressive: 2e4
21640
+ },
21641
+ {
21642
+ id: "asphalt",
21643
+ density: 2350,
21644
+ hpPerKg: 10,
21645
+ penetration: 14,
21646
+ penetrationClass: "concrete",
21647
+ penetrationScale: 0.8,
21648
+ ductility: 0.1,
21649
+ fracture: "crumble",
21650
+ fragmentSize: [
21651
+ 0.03,
21652
+ 0.15
21653
+ ],
21654
+ hitDebris: "chips",
21655
+ breakDebris: "dust",
21656
+ thermal: {
21657
+ conductivity: 0.1,
21658
+ heatCapacity: 2,
21659
+ ignitionC: 400,
21660
+ burnRate: 2e-3,
21661
+ smoke: "black"
21662
+ },
21663
+ tags: [
21664
+ "masonry",
21665
+ "brittle"
21666
+ ],
21667
+ compressive: 3e4
21668
+ },
21669
+ {
21670
+ id: "sand",
21671
+ density: 1600,
21672
+ hpPerKg: 2,
21673
+ penetration: 6,
21674
+ ductility: 0.4,
21675
+ fracture: "crumble",
21676
+ fragmentSize: [
21677
+ 0.01,
21678
+ 0.04
21679
+ ],
21680
+ hitDebris: "dust",
21681
+ breakDebris: "dust",
21682
+ thermal: {
21683
+ conductivity: 0.1,
21684
+ heatCapacity: 2
21685
+ },
21686
+ tags: [
21687
+ "soil"
21688
+ ],
21689
+ compressive: 1e4
21690
+ },
21691
+ {
21692
+ id: "gravel",
21693
+ density: 1800,
21694
+ hpPerKg: 3,
21695
+ penetration: 8,
21696
+ ductility: 0.3,
21697
+ fracture: "crumble",
21698
+ fragmentSize: [
21699
+ 0.02,
21700
+ 0.06
21701
+ ],
21702
+ hitDebris: "chips",
21703
+ breakDebris: "dust",
21704
+ thermal: {
21705
+ conductivity: 0.1,
21706
+ heatCapacity: 2
21707
+ },
21708
+ tags: [
21709
+ "soil",
21710
+ "masonry"
21711
+ ],
21712
+ compressive: 2e4
21713
+ },
21714
+ {
21715
+ id: "turf",
21716
+ density: 900,
21717
+ hpPerKg: 3,
21718
+ penetration: 4,
21719
+ ductility: 0.5,
21720
+ fracture: "crumble",
21721
+ fragmentSize: [
21722
+ 0.03,
21723
+ 0.1
21724
+ ],
21725
+ hitDebris: "dust",
21726
+ breakDebris: "dust",
21727
+ thermal: {
21728
+ conductivity: 0.1,
21729
+ heatCapacity: 1.5,
21730
+ ignitionC: 250,
21731
+ burnRate: 0.03,
21732
+ smoke: "white"
21733
+ },
21734
+ tags: [
21735
+ "soil",
21736
+ "organic"
21737
+ ],
21738
+ compressive: 15e3
21739
+ },
21740
+ {
21741
+ id: "mortar",
21742
+ density: 1900,
21743
+ hpPerKg: 10,
21744
+ penetration: 12,
21745
+ penetrationClass: "concrete",
21746
+ penetrationScale: 1.2,
21747
+ ductility: 0.03,
21748
+ fracture: "crumble",
21749
+ fragmentSize: [
21750
+ 0.02,
21751
+ 0.08
21752
+ ],
21753
+ hitDebris: "chips",
21754
+ breakDebris: "dust",
21755
+ thermal: {
21756
+ conductivity: 0.15,
21757
+ heatCapacity: 2.5
21758
+ },
21759
+ tags: [
21760
+ "masonry",
21761
+ "brittle"
21762
+ ],
21763
+ compressive: 1e5
21540
21764
  }
21541
21765
  ];
21542
21766
  var material_hints_default = {
@@ -21710,6 +21934,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21710
21934
  return (1 + 4 * (0.3 - ductility) / 0.3) * Math.min(25, Math.max(1, (0.02 / Math.max(thickness, 1e-4)) ** 2));
21711
21935
  };
21712
21936
  var THIN_SHEET_DISHING = 5e-3;
21937
+ var SWEEP_BACK = 0.01;
21938
+ function sweepOrigins(box5, d, spacing) {
21939
+ const origins = [];
21940
+ const line = (from, to) => {
21941
+ const n = Math.max(1, Math.ceil((to - from) / spacing));
21942
+ return Array.from({ length: n + 1 }, (_, i) => from + (to - from) * i / n);
21943
+ };
21944
+ for (const axis2 of [0, 1, 2]) {
21945
+ if (Math.abs(d[axis2]) < 1e-3) continue;
21946
+ const lead = d[axis2] > 0 ? box5.max[axis2] - SWEEP_BACK : box5.min[axis2] + SWEEP_BACK;
21947
+ const u = (axis2 + 1) % 3;
21948
+ const v = (axis2 + 2) % 3;
21949
+ for (const a of line(box5.min[u], box5.max[u])) {
21950
+ for (const b of line(box5.min[v], box5.max[v])) {
21951
+ const point = [0, 0, 0];
21952
+ point[axis2] = lead;
21953
+ point[u] = a;
21954
+ point[v] = b;
21955
+ origins.push(point);
21956
+ }
21957
+ }
21958
+ }
21959
+ return origins;
21960
+ }
21961
+ var THIN_SHEET_CRUSH = 5e-4;
21962
+ var THIN_SHEET_CONTACT = 0.025;
21713
21963
  var DMG_PER_J = 0.02;
21714
21964
  var BLAST_COUPLING = 0.1;
21715
21965
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
@@ -21723,9 +21973,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21723
21973
  var BREACH_AREA = 0.25;
21724
21974
  var BREACH_BLAST_REACH = 0.12;
21725
21975
  var RESIDUAL_CAPACITY = 0.15;
21976
+ var SEVER_BAND = 0.1;
21977
+ var SEVER_ASPECT = 3;
21978
+ var SEVER_BALLISTIC = 2;
21979
+ var TEAR_JOULES = 150;
21980
+ var tears = 0;
21726
21981
  var SUPPORT_TOLERANCE = 0.05;
21727
21982
  var UNIT_FRAGMENT_FACTOR = 4;
21728
21983
  var BREACH_GRID_CAP = 4096;
21984
+ var CHIP_ENERGY = 200;
21985
+ var CHIP_SPLASH = 0.5;
21986
+ var BAR_BOW = 0.08;
21729
21987
  var CUT_SLICE = 1;
21730
21988
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
21731
21989
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -21735,8 +21993,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21735
21993
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
21736
21994
  };
21737
21995
  var capacityScale = (p) => {
21738
- if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
21739
- return p.rt.removed ? Math.max(0.05, 1 - p.rt.removed / p.mass0) : 1;
21996
+ const residual = p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
21997
+ if (p.st.stripped) return residual;
21998
+ const kept = Math.min(p.rt.removed ? 1 - p.rt.removed / p.mass0 : 1, 1 - sectionLoss(p));
21999
+ return Math.max(p.spec.reinforcement ? residual : 0.05, kept);
22000
+ };
22001
+ var sectionLoss = (p) => {
22002
+ const cells = p.st.chipped;
22003
+ if (!cells?.length) return 0;
22004
+ const [u, v] = planeAxes(p.thin);
22005
+ const along = p.dims[u] >= p.dims[v] ? u : v;
22006
+ const across2 = along === u ? v : u;
22007
+ let worst = 0;
22008
+ for (const c of cells) {
22009
+ let gone = 0;
22010
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across2];
22011
+ worst = Math.max(worst, gone / Math.max(p.dims[across2], 1e-6));
22012
+ }
22013
+ return Math.min(1, worst);
21740
22014
  };
21741
22015
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
21742
22016
  var clone = (v) => structuredClone(v);
@@ -21769,6 +22043,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21769
22043
  let windCfg = windOpts ? resolveWind(windOpts) : null;
21770
22044
  const ignitionRules = resolveIgnitionRules(options.ignition);
21771
22045
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
22046
+ const chipEnergy = options.breach?.chipEnergy ?? CHIP_ENERGY;
21772
22047
  const rng2 = createRng2(options.seed ?? 1);
21773
22048
  const rnd2 = () => rng2.next();
21774
22049
  let time = 0;
@@ -22142,41 +22417,68 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22142
22417
  markDamaged(p);
22143
22418
  if (p.rt.hinge >= flexure.failRotationDeg * Math.PI / 180) hingeDrop(o, p, src);
22144
22419
  };
22420
+ const freeSpan = (p, supports) => {
22421
+ if (supports.length === 0) return Infinity;
22422
+ const d = p.dims;
22423
+ const L = [0, 1, 2].sort((a, b) => d[b] - d[a])[0];
22424
+ const spans = supports.map((k) => contactBox(p, k)).map(([lo, hi]) => [lo[L], hi[L]]);
22425
+ let far = 0;
22426
+ for (let i = 0; i <= 16; i++) {
22427
+ const x = (i / 16 - 0.5) * d[L];
22428
+ let near = Infinity;
22429
+ for (const [lo, hi] of spans) near = Math.min(near, Math.max(lo - x, x - hi, 0));
22430
+ far = Math.max(far, near);
22431
+ }
22432
+ return 2 * far;
22433
+ };
22145
22434
  const solveStructure = (o) => {
22146
22435
  o.maxStress = 0;
22147
22436
  if (!o.hasStructure) return;
22148
22437
  const parts = o.parts;
22149
22438
  const n = parts.length;
22150
22439
  const dist2 = new Array(n).fill(-1);
22151
- const queue = [];
22152
22440
  const ok = (i) => parts[i].structural && !isLost(parts[i]);
22153
22441
  const isPartition = (i) => parts[i].spec.structure?.role === "partition";
22154
- for (let i = 0; i < n; i++) if (ok(i) && parts[i].anchored) {
22155
- dist2[i] = 0;
22156
- queue.push(i);
22157
- }
22158
- for (let h = 0; h < queue.length; h++) {
22159
- const s = queue[h];
22160
- if (isPartition(s)) continue;
22161
- for (const j of o.byPart.get(parts[s].spec.id)) {
22162
- if (j.st.open || !j.bears) continue;
22163
- const r = j.ia === s ? j.ib : j.ia;
22164
- if (dist2[r] >= 0 || !ok(r) || restsOn(o, j, r) !== s) continue;
22165
- dist2[r] = dist2[s] + 1;
22166
- queue.push(r);
22442
+ const out = /* @__PURE__ */ new Set();
22443
+ let queue = [];
22444
+ let lowerOf = /* @__PURE__ */ new Map();
22445
+ for (; ; ) {
22446
+ dist2.fill(-1);
22447
+ queue = [];
22448
+ for (let i = 0; i < n; i++) if (ok(i) && !out.has(i) && parts[i].anchored) {
22449
+ dist2[i] = 0;
22450
+ queue.push(i);
22451
+ }
22452
+ for (let h = 0; h < queue.length; h++) {
22453
+ const s = queue[h];
22454
+ if (isPartition(s)) continue;
22455
+ for (const j of o.byPart.get(parts[s].spec.id)) {
22456
+ if (j.st.open || !j.bears) continue;
22457
+ const r = j.ia === s ? j.ib : j.ia;
22458
+ if (dist2[r] >= 0 || !ok(r) || out.has(r) || restsOn(o, j, r) !== s) continue;
22459
+ dist2[r] = dist2[s] + 1;
22460
+ queue.push(r);
22461
+ }
22167
22462
  }
22168
- }
22169
- const incoming = new Array(n).fill(0);
22170
- const lowerOf = /* @__PURE__ */ new Map();
22171
- for (const i of queue) {
22172
- const lower = [];
22173
- for (const j of o.byPart.get(parts[i].spec.id)) {
22174
- if (j.st.open || !j.bears) continue;
22175
- const k = restsOn(o, j, i);
22176
- if (k >= 0 && dist2[k] >= 0 && dist2[k] < dist2[i] && !isPartition(k)) lower.push(k);
22463
+ lowerOf = /* @__PURE__ */ new Map();
22464
+ let dropped = false;
22465
+ for (const i of queue) {
22466
+ const lower = [];
22467
+ for (const j of o.byPart.get(parts[i].spec.id)) {
22468
+ if (j.st.open || !j.bears) continue;
22469
+ const k = restsOn(o, j, i);
22470
+ if (k >= 0 && dist2[k] >= 0 && dist2[k] < dist2[i] && !isPartition(k)) lower.push(k);
22471
+ }
22472
+ lowerOf.set(i, lower);
22473
+ const max2 = parts[i].spec.structure?.maxSpan;
22474
+ if (max2 !== void 0 && !parts[i].anchored && freeSpan(parts[i], lower.map((k) => parts[k])) > max2) {
22475
+ out.add(i);
22476
+ dropped = true;
22477
+ }
22177
22478
  }
22178
- lowerOf.set(i, lower);
22479
+ if (!dropped) break;
22179
22480
  }
22481
+ const incoming = new Array(n).fill(0);
22180
22482
  const bending = new Array(n).fill(0);
22181
22483
  const through = parts.map(() => []);
22182
22484
  const pass = (k, at3, mass) => {
@@ -22688,6 +22990,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22688
22990
  const r = rayPart(o, p, origin, d);
22689
22991
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
22690
22992
  if (p.st.opening && insideOpening(o, p, vec.madd(origin, d, r.t0))) continue;
22993
+ if (p.st.chipped && insideChip(p, objLocal(o, vec.madd(origin, d, r.t0)))) continue;
22691
22994
  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 };
22692
22995
  }
22693
22996
  }
@@ -22888,6 +23191,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22888
23191
  debris2(o, p, layer2.mat.hitDebris, wp, normal, 5, 2, src);
22889
23192
  setStage(o, p, 1, {}, src);
22890
23193
  };
23194
+ const crater = (o, p, wp, normal, radius, src) => {
23195
+ markDamaged(p);
23196
+ emit2({ ...base(o, src), type: "spall", part: p.spec.id, layer: -1, point: wp, area: Math.PI * radius * radius });
23197
+ debris2(o, p, p.mat.breakDebris, wp, normal, 6, 1.5, src);
23198
+ setStage(o, p, 1, {}, src);
23199
+ };
22891
23200
  const insideOpening = (o, p, objectPoint) => {
22892
23201
  const op = p.st.opening;
22893
23202
  if (!op) return false;
@@ -22951,6 +23260,91 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22951
23260
  if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
22952
23261
  openBreach(o, p, [sum2[0] / total, sum2[1] / total, sum2[2] / total], total, energy, dir, src);
22953
23262
  };
23263
+ const unitGrids = /* @__PURE__ */ new WeakMap();
23264
+ const gridOf = (p) => {
23265
+ let grid = unitGrids.get(p);
23266
+ if (!grid) unitGrids.set(p, grid = unitGrid(p.dims, p.thin, unitsOf2(p), BREACH_GRID_CAP));
23267
+ return grid;
23268
+ };
23269
+ const insideChip = (p, objectPoint) => {
23270
+ const cells = p.st.chipped;
23271
+ if (!cells?.length) return false;
23272
+ const [u, v] = planeAxes(p.thin);
23273
+ const lp = toLocal(p.pos, p.q, objectPoint);
23274
+ 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);
23275
+ };
23276
+ const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
23277
+ var _a, _b;
23278
+ if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
23279
+ const rule = p.mat.hits?.[kind];
23280
+ const perKg = rule?.unitJoules ?? chipEnergy;
23281
+ const energy = hitEnergy - (rule?.floor ?? 0);
23282
+ if (perKg <= 0 || energy <= 0) return;
23283
+ const [u, v] = planeAxes(p.thin);
23284
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
23285
+ const grid = gridOf(p);
23286
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
23287
+ const out = [];
23288
+ for (let i = 0; i < grid.length; i++) {
23289
+ const cell = grid[i];
23290
+ if (worn[i] === -1) continue;
23291
+ 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));
23292
+ if (gap > radius) continue;
23293
+ const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
23294
+ const total = (worn[i] ?? 0) + energy * share;
23295
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
23296
+ if (total < perKg * mass) {
23297
+ worn[i] = total;
23298
+ continue;
23299
+ }
23300
+ worn[i] = -1;
23301
+ out.push(cell);
23302
+ }
23303
+ if (out.length === 0) return;
23304
+ const { c, q } = partXf(o, p);
23305
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
23306
+ const holes = localHolesOf(p);
23307
+ for (const cell of out) {
23308
+ const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
23309
+ const center = toWorld(c, q, cell.center);
23310
+ const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
23311
+ const fragments = cellFragments(o, p, [cell], wp, dir, speed, mass, `c${chipped.length}_`);
23312
+ p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
23313
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
23314
+ p.st.mass = Math.max(0, p.st.mass - mass);
23315
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
23316
+ markDamaged(p);
23317
+ setStage(o, p, 1, {}, src);
23318
+ emit2({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
23319
+ debris2(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
23320
+ }
23321
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
23322
+ for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
23323
+ if (p.structural && !isLost(p)) {
23324
+ solveStructure(o);
23325
+ collapseCheck(o, src);
23326
+ }
23327
+ };
23328
+ const exposeRebar = (o, p, src) => {
23329
+ const r = p.spec.reinforcement;
23330
+ if (!r) return;
23331
+ const [u, v] = planeAxes(p.thin);
23332
+ const all = rebarBars(p.dims, p.thin, r.spacing);
23333
+ let bars;
23334
+ if (p.st.stripped) bars = all;
23335
+ else {
23336
+ bars = [];
23337
+ for (const cell of p.st.chipped ?? []) bars.push(...clipBarsToBox(all, u, v, cell.center, cell.size));
23338
+ const op = p.st.opening;
23339
+ if (op) {
23340
+ const lc = toLocal(p.pos, p.q, op.center);
23341
+ bars.push(...clipBarsToDisc(all, u, v, [lc[u], lc[v]], op.radius + 0.05));
23342
+ }
23343
+ }
23344
+ const bow = clamp2(((p.st.stress ?? 0) - 1) / 0.5, 0, 1);
23345
+ if (bow > 0) bars = bars.flatMap((bar2, i) => bendBar(bar2, p.thin, bow * BAR_BOW, i));
23346
+ emit2({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
23347
+ };
22954
23348
  const openBreach = (o, p, center, total, energy, dir, src) => {
22955
23349
  const [u, v] = planeAxes(p.thin);
22956
23350
  const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
@@ -22962,7 +23356,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22962
23356
  failPart(o, p, worldCenter, dir, energy, 12, src);
22963
23357
  return;
22964
23358
  }
22965
- const cells = cellsWithin(unitGrid(p.dims, p.thin, unitsOf2(p), BREACH_GRID_CAP), p.thin, lc, radius);
23359
+ const worn = p.rt.chips;
23360
+ const cells = cellsWithin(gridOf(p).filter((_, i) => worn?.[i] !== -1), p.thin, lc, radius);
22966
23361
  const removed = p.st.mass * cells.reduce((sum2, cell) => sum2 + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
22967
23362
  const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
22968
23363
  const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
@@ -22976,10 +23371,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22976
23371
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
22977
23372
  debris2(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
22978
23373
  const reinforcement = p.spec.reinforcement;
22979
- if (reinforcement) {
22980
- const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
22981
- if (bars.length > 0) emit2({ ...base(o, src), type: "expose", part: p.spec.id, bars });
22982
- }
23374
+ if (reinforcement) exposeRebar(o, p, src);
22983
23375
  if (p.structural) solveStructure(o);
22984
23376
  };
22985
23377
  const stripPart = (o, p, reinforcement, src) => {
@@ -22991,8 +23383,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22991
23383
  p.st.burning = false;
22992
23384
  p.rt.jet = false;
22993
23385
  markDamaged(p);
22994
- emit2({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
22995
23386
  solveStructure(o);
23387
+ exposeRebar(o, p, src);
22996
23388
  collapseCheck(o, src);
22997
23389
  };
22998
23390
  const unitFragmentPlan = (p, cap) => {
@@ -23046,6 +23438,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23046
23438
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
23047
23439
  const hitLocal = toLocal(c, q, hitW);
23048
23440
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
23441
+ const axis2 = style === "splinter" || style === "snap" ? severAxis(p) : -1;
23442
+ const cutAt2 = axis2 < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis2] : void 0);
23049
23443
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
23050
23444
  style,
23051
23445
  dims: p.dims,
@@ -23055,7 +23449,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23055
23449
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
23056
23450
  maxFragments,
23057
23451
  rnd: rnd2,
23058
- sheet: p.mat.sheet
23452
+ sheet: p.mat.sheet,
23453
+ ...cutAt2 !== void 0 ? { cut: { at: cutAt2, gap: SEVER_BAND } } : {}
23059
23454
  });
23060
23455
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
23061
23456
  const mass = p.st.mass;
@@ -23114,10 +23509,33 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23114
23509
  emit2({ ...base(o, src), type: "primed", part: p.spec.id, point: partXf(o, p).c, trigger, delay });
23115
23510
  };
23116
23511
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
23512
+ const severAxis = (p) => {
23513
+ if (p.mat.fracture !== "splinter" && p.mat.fracture !== "snap" || p.spec.shape.kind !== "box") return -1;
23514
+ const d = p.dims;
23515
+ const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
23516
+ return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
23517
+ };
23518
+ const bandOf = (o, p, axis2, origin) => {
23519
+ const { c, q } = partXf(o, p);
23520
+ const along = toLocal(c, q, origin)[axis2];
23521
+ const L = p.dims[axis2];
23522
+ return clamp2(Math.floor((along + L / 2) / SEVER_BAND), 0, Math.ceil(L / SEVER_BAND) - 1);
23523
+ };
23117
23524
  const applyDamage = (o, p, amount, type, origin, dir, fragEnergy, cap, src) => {
23525
+ var _a;
23118
23526
  if (isLost(p) || amount <= 0) return 0;
23119
23527
  const prev = p.st.hp;
23120
23528
  p.st.hp -= amount;
23529
+ const axis2 = origin && type !== "fire" && type !== "heat" ? severAxis(p) : -1;
23530
+ if (axis2 >= 0 && p.st.hp > 0) {
23531
+ const band = bandOf(o, p, axis2, origin);
23532
+ const cuts = (_a = p.rt).cuts ?? (_a.cuts = {});
23533
+ cuts[band] = (cuts[band] ?? 0) + amount * (type === "ballistic" ? SEVER_BALLISTIC : 1);
23534
+ if (cuts[band] >= p.st.maxHp * SEVER_BAND / p.dims[axis2]) {
23535
+ p.rt.cutAt = -p.dims[axis2] / 2 + (band + 0.5) * SEVER_BAND;
23536
+ p.st.hp = 0;
23537
+ }
23538
+ }
23121
23539
  markDamaged(p);
23122
23540
  const trigger = CHAIN_TRIGGER_OF[type];
23123
23541
  const chained = trigger !== void 0 && chainDelayOf(p.spec, trigger) !== void 0;
@@ -23153,10 +23571,53 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23153
23571
  }
23154
23572
  }
23155
23573
  };
23574
+ const tearOf = (mat, kind) => {
23575
+ const h = mat.hits?.[kind];
23576
+ const tear = h?.tear ?? (kind === "ballistic" && mat.penetrationClass === "wood" ? "splinters" : "none");
23577
+ return { tear, joules: h?.tearJoules ?? TEAR_JOULES };
23578
+ };
23579
+ const tearAt = (o, p, kind, entry2, exit, d, E, scale, src) => {
23580
+ const { tear, joules } = tearOf(p.mat, kind);
23581
+ if (tear === "none") return;
23582
+ const shot = kind === "ballistic";
23583
+ const n = shot ? exit ? clamp2(Math.round(E / joules), 1, 7) : 1 : clamp2(Math.floor(E / joules), 0, 5);
23584
+ if (n === 0) return;
23585
+ const fibre = tear === "lumps";
23586
+ const { q } = partXf(o, p);
23587
+ const along = (p.spec.grain ? AXIS[p.spec.grain] : void 0) ?? [0, 1, 2].sort((a, b) => p.dims[b] - p.dims[a])[0];
23588
+ const fragments = [];
23589
+ for (let i = 0; i < n; i++) {
23590
+ const back = !exit || i % 3 === 2;
23591
+ const len = scale * (fibre ? 1.2 + 1.2 * rnd2() : 3 + 4 * rnd2()), w = scale * (0.8 + 0.8 * rnd2()), t = scale * (fibre ? 0.7 + 0.6 * rnd2() : 0.4 + 0.4 * rnd2());
23592
+ const size = [w, w, w];
23593
+ size[along] = len;
23594
+ size[(along + 1) % 3] = w;
23595
+ size[(along + 2) % 3] = t;
23596
+ const spread3 = [(rnd2() - 0.5) * 0.8, (rnd2() - 0.2) * 0.8, (rnd2() - 0.5) * 0.8];
23597
+ const speed = shot ? back ? 1.5 + 2 * rnd2() : 2 + 4 * rnd2() : 0.8 + 1.2 * rnd2();
23598
+ const v = vec.scale(vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3)), speed);
23599
+ const mass = size[0] * size[1] * size[2] * p.mat.density;
23600
+ fragments.push({
23601
+ id: `${p.spec.id}#t${tears++}-${i}`,
23602
+ part: p.spec.id,
23603
+ material: p.spec.material,
23604
+ shape: fibre ? "chunk" : "splinter",
23605
+ center: back || !exit ? vec.madd(entry2, d, -0.01) : vec.madd(exit, d, 0.01),
23606
+ size,
23607
+ rotation: q,
23608
+ velocity: v,
23609
+ inner: true,
23610
+ angularVelocity: [(rnd2() - 0.5) * 30, (rnd2() - 0.5) * 30, (rnd2() - 0.5) * 30],
23611
+ mass
23612
+ });
23613
+ }
23614
+ const patch = [scale * 4, scale * 4, scale * 4];
23615
+ emit2({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments });
23616
+ };
23156
23617
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round5 = {}) => {
23157
23618
  const mat = p.mat;
23158
23619
  const piercing = round5.piercing ?? 1;
23159
- if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry2))) {
23620
+ if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry2)) || insideChip(p, objLocal(o, entry2))) {
23160
23621
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry2, d, Math.min(pathLen, p.thickness)), damage: 0 };
23161
23622
  }
23162
23623
  const sin2 = Math.min(1, Math.abs(vec.dot(d, n)));
@@ -23181,6 +23642,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23181
23642
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
23182
23643
  }
23183
23644
  if (p.layers.length > 0) spallOuter(o, p, entry2, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
23645
+ else if (mat.hits?.ballistic?.crater) crater(o, p, entry2, n, clamp2(0.02 + Math.sqrt(E) * 15e-4, 0.02, 0.15), src);
23184
23646
  const brittle = mat.ductility < 0.3;
23185
23647
  const ratio = Ep / Math.max(thr, 1e-6);
23186
23648
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
@@ -23214,7 +23676,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23214
23676
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
23215
23677
  debris2(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
23216
23678
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
23217
- const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry2, d, E * 0.1, 12, src);
23679
+ const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
23680
+ const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
23681
+ if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
23682
+ 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);
23218
23683
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
23219
23684
  };
23220
23685
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -23222,14 +23687,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23222
23687
  const thrMm = mat.penetration * p.thickness * 1e3;
23223
23688
  const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
23224
23689
  const Ec = E * fragile;
23690
+ const sheet = mat.ductility >= 0.5 && p.thickness <= THIN_SHEET_CRUSH;
23691
+ const fold = sheet ? Math.min(4, Math.sqrt(1e-3 / p.thickness)) : 1;
23692
+ const patch = sheet ? Math.min(contactR, THIN_SHEET_CONTACT) : contactR;
23225
23693
  if (E > 0) {
23226
23694
  if (kind === "blunt") {
23227
- if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp2(E * 1e-4, 1e-3, 0.06), src);
23695
+ 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);
23228
23696
  else if (mat.ductility < 0.3) {
23229
23697
  if (Ec >= 5) addCrack(o, p, point, n, clamp2(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
23230
23698
  } 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);
23231
23699
  } else if (mat.fracture === "tear") {
23232
23700
  addHole(o, p, point, n, clamp2(0.02 + E * 5e-4, 0.02, 0.3), true, void 0, src);
23701
+ } else if (mat.ductility >= 0.3 && E >= thrMm) {
23702
+ addHole(o, p, point, n, clamp2(contactR, 0.01, 0.08), true, void 0, src);
23703
+ if (sheet) addDent(o, p, point, n, Math.max(patch * 0.6, 0.01), clamp2(E * 4e-5 * fold, 5e-4, 0.02), src);
23233
23704
  } else if (mat.ductility >= 0.5) {
23234
23705
  addDent(o, p, point, n, Math.max(contactR, 0.01), clamp2(E * 4e-5, 5e-4, 0.02), src);
23235
23706
  }
@@ -23242,6 +23713,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23242
23713
  const scale = mat.damageScale?.[kind] ?? 1;
23243
23714
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
23244
23715
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
23716
+ if (kind === "blunt") chipUnits(o, p, "blunt", point, E, d, Math.max(contactR, 0.03), src);
23717
+ if (E > 0 && !isLost(p)) tearAt(o, p, kind, point, void 0, d, E, clamp2(contactR * 0.2, 6e-3, 0.02), src);
23245
23718
  return done;
23246
23719
  };
23247
23720
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -23820,6 +24293,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23820
24293
  if (p.rt.stressT <= 0) {
23821
24294
  p.rt.stressT = 0.5;
23822
24295
  emit2({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
24296
+ if (p.spec.reinforcement && (p.st.stripped || p.st.chipped?.length || p.st.opening)) exposeRebar(o, p);
23823
24297
  }
23824
24298
  applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
23825
24299
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
@@ -24036,6 +24510,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
24036
24510
  const r = raycastInternal(origin, direction, maxDistance);
24037
24511
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
24038
24512
  },
24513
+ sweepBox(box5, direction, distance3, options2 = {}) {
24514
+ const d = vec.normalize(direction);
24515
+ const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
24516
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
24517
+ let best = null;
24518
+ for (const origin of sweepOrigins(box5, d, options2.spacing ?? 0.2)) {
24519
+ const r = raycastInternal(origin, d, distance3 + SWEEP_BACK, ignore);
24520
+ if (!r || best && r.distance >= best.distance) continue;
24521
+ best = { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: Math.max(0, r.distance - SWEEP_BACK) };
24522
+ }
24523
+ return best;
24524
+ },
24039
24525
  shoot(origin, direction, energy, caliber, source, round5) {
24040
24526
  const extras = typeof round5 === "number" ? { piercing: round5 } : round5 ?? {};
24041
24527
  return withCollapses(run(() => {
@@ -25607,7 +26093,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
25607
26093
  }
25608
26094
  return spec;
25609
26095
  };
25610
- var partSpecOf = (part, capacity, detailed) => detailed && part.assembly ? assemblySpec(part, capacity) : boxSpec(part, capacity);
26096
+ var MEMBER_CAGE = { spacing: 0.1, diameter: 8e-3, unit: 0.1 };
26097
+ var CAGED_ROLES = /* @__PURE__ */ new Set(["beam", "column"]);
26098
+ var caged = (spec, part, rebar2) => {
26099
+ if (!rebar2 || part.material !== "concrete" || !CAGED_ROLES.has(part.role) || spec.reinforcement) return spec;
26100
+ const size = spec.shape.kind === "box" ? spec.shape.size : null;
26101
+ if (!size) return spec;
26102
+ const thin = Math.min(...size);
26103
+ return {
26104
+ ...spec,
26105
+ reinforcement: { material: rebar2, spacing: MEMBER_CAGE.spacing, diameter: MEMBER_CAGE.diameter },
26106
+ units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, thin], bond: "stack", mortar: "concrete" }
26107
+ };
26108
+ };
26109
+ var partSpecOf = (part, capacity, detailed, rebar2) => {
26110
+ if (detailed && part.assembly) return assemblySpec(part, capacity);
26111
+ const spec = boxSpec(part, capacity);
26112
+ return detailed ? caged(spec, part, rebar2) : spec;
26113
+ };
25611
26114
  function toDestructibleSpec(structure, options = {}) {
25612
26115
  const { safety = 0.65, redundancy = 0.97, safetyOthers = 0.45 } = options;
25613
26116
  const kept = structure.parts.filter((part) => !options.filter || options.filter(part));
@@ -25624,7 +26127,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
25624
26127
  const sizing = { safety, redundancy, safetyOthers };
25625
26128
  const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts, panels.supports, sizing) };
25626
26129
  const { parts, supports, capacities } = detail === "units" ? applyUnits(sized, structure) : sized;
25627
- const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse"));
26130
+ const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse", structure.construction.frame.rebar));
25628
26131
  const margin = options.terrainMargin ?? TERRAIN_MARGIN;
25629
26132
  const x0 = Math.min(...parts.map((part) => part.aabb[0]));
25630
26133
  const z0 = Math.min(...parts.map((part) => part.aabb[1]));
@@ -33260,12 +33763,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33260
33763
  A.invalidate = () => {
33261
33764
  for (let i = 0; i < NS; i++) PL[i].shadow.needsUpdate = true;
33262
33765
  };
33263
- A.update = function(houses, focus, op) {
33766
+ A.update = function(houses2, focus, op) {
33264
33767
  op = op || {};
33265
- houses = (Array.isArray(houses) ? houses : [houses]).filter(Boolean);
33768
+ houses2 = (Array.isArray(houses2) ? houses2 : [houses2]).filter(Boolean);
33266
33769
  _f.set(focus.x, focus.y, focus.z);
33267
33770
  const rooms = [], exterior = [];
33268
- for (const house of houses) {
33771
+ for (const house of houses2) {
33269
33772
  if (house.group && !house.group.visible) continue;
33270
33773
  house.house.updateMatrixWorld(true);
33271
33774
  const M = house.house.matrixWorld, by = /* @__PURE__ */ new Map();
@@ -33589,7 +34092,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33589
34092
  if (!pc || pc.unit) return sp;
33590
34093
  if (pc.role !== "wall" && pc.role !== "partition") {
33591
34094
  const def = getAssembly(pc.assembly || (pc.material === "concrete" && ["slab", "beam", "column", "stair"].includes(pc.role) ? "reinforced_concrete" : ""));
33592
- return def && def.reinforcement && !sp.reinforcement ? { ...sp, reinforcement: { ...def.reinforcement } } : sp;
34095
+ const out = def && def.reinforcement && !sp.reinforcement ? { ...sp, reinforcement: { ...def.reinforcement } } : sp;
34096
+ if (out.reinforcement && !out.units && pc.material === "concrete" && (pc.role === "beam" || pc.role === "column") && sp.shape.kind === "box") {
34097
+ return { ...out, units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, Math.min(...sp.shape.size)], bond: "stack", mortar: "concrete" } };
34098
+ }
34099
+ return out;
33593
34100
  }
33594
34101
  if (!pc.assembly) return sp;
33595
34102
  const [u0, v0, u1, v1] = pc.aabb, alongU = u1 - u0 >= v1 - v0;
@@ -33644,7 +34151,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33644
34151
  }
33645
34152
  return null;
33646
34153
  };
33647
- function courses(part, units, opening, paint2) {
34154
+ const local2 = (p) => {
34155
+ house.updateMatrixWorld(true);
34156
+ const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
34157
+ return [v.x, v.y, v.z];
34158
+ };
34159
+ function courses(part, units, opening, chips2, paint2) {
33648
34160
  const [u0, v0, u1, v1] = part.aabb;
33649
34161
  const alongU = u1 - u0 >= v1 - v0;
33650
34162
  const len = alongU ? u1 - u0 : v1 - v0;
@@ -33668,23 +34180,54 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33668
34180
  const x = alongU ? u0 + mid : (u0 + u1) / 2;
33669
34181
  const z = alongU ? (v0 + v1) / 2 : v0 + mid;
33670
34182
  const y = part.y0 + (r + 0.5) * rowH;
33671
- const d = Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]);
33672
- if (d < opening.radius) continue;
33673
- list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: d < opening.radius + 0.25 || paint2 === null ? edge : paint2 });
34183
+ const d = opening ? Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]) : Infinity;
34184
+ if (opening && d < opening.radius) continue;
34185
+ const gone = (c) => Math.abs(c[1] - y) <= rowH / 2 && (alongU ? c[0] >= u0 + a && c[0] <= u0 + b : c[2] >= v0 + a && c[2] <= v0 + b);
34186
+ if (chips2.some(gone)) continue;
34187
+ const near = opening && d < opening.radius + 0.25 || chips2.some((c) => Math.hypot(c[0] - x, c[1] - y, c[2] - z) < 0.15);
34188
+ list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: near || paint2 === null ? edge : paint2 });
33674
34189
  }
33675
34190
  }
33676
34191
  return list2;
33677
34192
  }
33678
- function open(event) {
33679
- if (lod !== 0 || views.has(event.part) || !event.opening) return false;
33680
- const part = structure.byId.get(event.part);
33681
- const spec = opts.spec()?.parts?.find((q) => q.id === event.part);
33682
- if (!part || !spec || !spec.units) return false;
33683
- const items = structure.meshesOf(event.part).filter((it) => it.mesh);
33684
- if (!items.length) return false;
33685
- const list2 = courses(part, spec.units, event.opening, wallColour(items));
34193
+ const specOf = (id) => opts.spec()?.parts?.find((q) => q.id === id);
34194
+ const same = (a, b) => a.every((x, k) => Math.abs(x - b[k]) < 1e-3);
34195
+ function hostOf(partId) {
34196
+ if (structure.meshesOf(partId).some((it) => it.mesh)) return partId;
34197
+ const part = structure.byId.get(partId);
34198
+ if (!part) return null;
34199
+ for (const q of structure.parts) {
34200
+ if (q.id !== partId && same(q.aabb, part.aabb) && Math.abs(q.y1 - part.y0) < 0.01 && structure.meshesOf(q.id).some((it) => it.mesh)) return q.id;
34201
+ }
34202
+ return null;
34203
+ }
34204
+ const riders = (hostId) => structure.parts.filter((q) => q.id !== hostId && specOf(q.id)?.units && hostOf(q.id) === hostId && !structure.meshesOf(q.id).some((it) => it.mesh)).map((q) => q.id);
34205
+ function draw2(partId, change) {
34206
+ if (lod !== 0) return false;
34207
+ const hostId = hostOf(partId);
34208
+ if (!hostId || !specOf(partId)?.units) return false;
34209
+ let view = views.get(hostId);
34210
+ if (!view) {
34211
+ const items = structure.meshesOf(hostId).filter((it) => it.mesh);
34212
+ if (!items.length) return false;
34213
+ const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
34214
+ view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
34215
+ }
34216
+ if (!view.chips.has(partId)) return false;
34217
+ if (change.opening) {
34218
+ if (view.openings.has(partId)) return false;
34219
+ view.openings.set(partId, { center: local2(change.opening.center), radius: change.opening.radius });
34220
+ if (partId === hostId) view.opening = view.openings.get(partId);
34221
+ }
34222
+ if (change.chip) view.chips.get(partId).push(local2(change.chip));
34223
+ const list2 = [];
34224
+ for (const [id, chips2] of view.chips) list2.push(...courses(structure.byId.get(id), specOf(id).units, view.openings.get(id) ?? null, chips2, view.paint));
34225
+ if (view.mesh) {
34226
+ view.mesh.removeFromParent();
34227
+ view.mesh.dispose();
34228
+ }
33686
34229
  const mesh3 = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
33687
- mesh3.name = "breach:" + event.part;
34230
+ mesh3.name = "breach:" + hostId;
33688
34231
  list2.forEach((u, i) => {
33689
34232
  tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
33690
34233
  mesh3.setMatrixAt(i, tmp.m);
@@ -33696,8 +34239,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33696
34239
  mesh3.castShadow = true;
33697
34240
  mesh3.receiveShadow = true;
33698
34241
  house.add(mesh3);
33699
- const saved = items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
33700
- views.set(event.part, { mesh: mesh3, saved, units: list2.length, opening: event.opening });
34242
+ if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
34243
+ view.mesh = mesh3;
34244
+ view.units = list2.length;
34245
+ views.set(hostId, view);
33701
34246
  invalidate();
33702
34247
  invalidateShadow();
33703
34248
  return true;
@@ -33711,7 +34256,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33711
34256
  return {
33712
34257
  /** Handles a `stage` event of the house's object. Returns true when the wall was redrawn as courses. */
33713
34258
  stage(event) {
33714
- return event.stage === 2 ? open(event) : false;
34259
+ return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
34260
+ },
34261
+ /** Handles a `chip` event: the unit that came out leaves a gap in the courses. Returns true when the wall was redrawn. */
34262
+ chip(event) {
34263
+ return draw2(event.part, { chip: event.center });
33715
34264
  },
33716
34265
  /** The part broke or fell: the courses go (the plain range stays collapsed, the renderer hides the part). */
33717
34266
  drop(partId) {
@@ -33941,6 +34490,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33941
34490
  case "stage":
33942
34491
  breach.stage(e);
33943
34492
  break;
34493
+ case "chip":
34494
+ breach.chip(e);
34495
+ break;
33944
34496
  case "collapse":
33945
34497
  for (const id of e.parts) {
33946
34498
  breach.drop(id);
@@ -33995,6 +34547,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33995
34547
  if (D.active && D.world.has(D.id)) D.world.remove(D.id);
33996
34548
  D.active = false;
33997
34549
  };
34550
+ D.follow = (world, id, spec) => {
34551
+ D.spec = spec;
34552
+ const offFollow = world.on("*", (e) => {
34553
+ if (e.object !== id) return;
34554
+ switch (e.type) {
34555
+ case "stage":
34556
+ breach.stage(e);
34557
+ break;
34558
+ case "chip":
34559
+ breach.chip(e);
34560
+ break;
34561
+ case "fracture":
34562
+ breach.drop(e.part);
34563
+ break;
34564
+ case "collapse":
34565
+ for (const p of e.parts) breach.drop(p);
34566
+ break;
34567
+ default:
34568
+ break;
34569
+ }
34570
+ });
34571
+ return { restore: () => breach.restore(), dispose: () => {
34572
+ offFollow();
34573
+ breach.restore();
34574
+ } };
34575
+ };
33998
34576
  D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
33999
34577
  D.state = () => D.active ? D.world.state(D.id) : null;
34000
34578
  D.reset = () => {
@@ -34717,6 +35295,56 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34717
35295
  return buildApi(K, shared);
34718
35296
  }
34719
35297
  var WOOD_THIN = 0.035;
35298
+ var BOARD_TOP_THICK = 0.07;
35299
+ var BOARD_TOP_WIDTH = 0.5;
35300
+ var BOARD_WIDTH = 0.16;
35301
+ var SHELF_SPAN = 1.5;
35302
+ var SHELVING = /shelf|bookcase|rack/;
35303
+ function carcass(parts, drawn) {
35304
+ const all = [...drawn.values()];
35305
+ const width = Math.max(...all.map((p) => p.max[0])) - Math.min(...all.map((p) => p.min[0]));
35306
+ for (const part of parts) {
35307
+ if (LOOSE.has(part.material) || part.shape.kind !== "box" || part.rotation) continue;
35308
+ const [x, y, z] = part.shape.size;
35309
+ if (y >= x && y >= z) {
35310
+ if (z < x && x >= 0.6 * width) continue;
35311
+ part.structure = { role: "column" };
35312
+ } else if (y <= x && y <= z) part.structure = { role: "beam", maxSpan: SHELF_SPAN * Math.max(x, z) };
35313
+ }
35314
+ }
35315
+ function splitTopsIntoBoards(drawn, materialOf2, carcassed) {
35316
+ const tops = /* @__PURE__ */ new Map();
35317
+ if (carcassed) return tops;
35318
+ for (let i = drawn.length - 1; i >= 0; i--) {
35319
+ const p = drawn[i];
35320
+ if (p.key !== "wood" || materialOf2.wood !== "wood_hard" || p.shape.kind !== "box" || p.rotation) continue;
35321
+ const [sx, sy, sz] = p.shape.size;
35322
+ const across2 = sx < sz ? 0 : 2;
35323
+ const width = Math.min(sx, sz);
35324
+ if (sy < WOOD_THIN || sy > BOARD_TOP_THICK || sy >= width || width < BOARD_TOP_WIDTH) continue;
35325
+ const count = Math.round(width / BOARD_WIDTH);
35326
+ const boards = [];
35327
+ for (let b = 0; b < count; b++) {
35328
+ const size = [sx, sy, sz];
35329
+ size[across2] = width / count;
35330
+ const position = [...p.position];
35331
+ position[across2] += (b + 0.5) * size[across2] - width / 2;
35332
+ const min2 = [...p.min];
35333
+ const max2 = [...p.max];
35334
+ min2[across2] = position[across2] - size[across2] / 2;
35335
+ max2[across2] = position[across2] + size[across2] / 2;
35336
+ boards.push({ ...p, id: `${p.id}_b${b}`, shape: { kind: "box", size }, position, min: min2, max: max2, color: shadeBoard(p.color, b) });
35337
+ }
35338
+ drawn.splice(i, 1, ...boards);
35339
+ tops.set(p.id, boards);
35340
+ }
35341
+ return tops;
35342
+ }
35343
+ function shadeBoard(color2, board) {
35344
+ const k = [1, 0.94, 1.05, 0.97, 1.03][board % 5];
35345
+ const channel = (shift) => Math.min(255, Math.round((color2 >> shift & 255) * k)) << shift;
35346
+ return channel(16) | channel(8) | channel(0);
35347
+ }
34720
35348
  var FLOOR_TOUCH = 5e-3;
34721
35349
  var LOOSE = /* @__PURE__ */ new Set(["paper"]);
34722
35350
  var BASE_MATERIAL = {
@@ -34794,10 +35422,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34794
35422
  };
34795
35423
  run(A);
34796
35424
  if (!drawn.length) throw new Error(`game-floorplan-three: the builder of "${name}" drew no box or cylinder`);
35425
+ const boardsOf = splitTopsIntoBoards(drawn, materialOf2, Boolean(opts.carcass));
34797
35426
  const parts = drawn.map((p) => {
34798
35427
  const thin = Math.min(...p.shape.kind === "box" ? p.shape.size : p.shape.kind === "cylinder" ? [p.shape.radius * 2, p.shape.height] : [p.shape.radius * 2]);
34799
35428
  let material = materialOf2[p.key] ?? "plastic";
34800
- if (p.key === "wood" && material === "wood_hard" && thin < WOOD_THIN) material = "wood_soft";
35429
+ if (p.key === "wood" && material === "wood_hard" && opts.carcass && !opts.materials?.wood) material = "mdf";
35430
+ else if (p.key === "wood" && material === "wood_hard" && thin < WOOD_THIN) material = "wood_soft";
34801
35431
  const part = { id: p.id, material, shape: p.shape, position: p.position };
34802
35432
  if (p.rotation) part.rotation = p.rotation;
34803
35433
  if (!LOOSE.has(material) && p.min[1] <= FLOOR_TOUCH) part.anchored = true;
@@ -34832,6 +35462,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34832
35462
  if (area3 < 0.6 * footprint || p.max[1] < 0.2) continue;
34833
35463
  if (!top || p.max[1] > top.y + 1e-6) top = { part: p.id, y: p.max[1] };
34834
35464
  }
35465
+ for (const boards of boardsOf.values()) {
35466
+ if (top && boards[0].max[1] <= top.y - 1e-6) continue;
35467
+ const mid = boards[boards.length >> 1];
35468
+ const min2 = [Math.min(...boards.map((b) => b.min[0])), Math.min(...boards.map((b) => b.min[2]))];
35469
+ const max2 = [Math.max(...boards.map((b) => b.max[0])), Math.max(...boards.map((b) => b.max[2]))];
35470
+ if ((max2[0] - min2[0]) * (max2[1] - min2[1]) >= 0.6 * footprint && mid.max[1] >= 0.2) top = { part: mid.id, y: mid.max[1], parts: boards.map((b) => b.id) };
35471
+ }
35472
+ if (opts.carcass) carcass(parts, byId);
34835
35473
  const spec = { name, parts, joints, tags: ["furniture"] };
34836
35474
  const colors = new Map(drawn.map((p) => [p.id, p.color]));
34837
35475
  const build = (T3) => {
@@ -34875,7 +35513,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34875
35513
  if (w === void 0 || d === void 0 || h === void 0) throw new Error(`game-floorplan-three: furniture type "${type}" is not in the catalogue, so give w, d and h`);
34876
35514
  const o = opts.options ?? {};
34877
35515
  const seed = Math.round(opts.seed ?? 1);
34878
- const plain2 = /shelf|bookcase|rack/.test(type) ? "paper" : "plastic";
35516
+ const plain2 = SHELVING.test(type) ? "paper" : "plastic";
34879
35517
  const root = { x: seed, z: 0, ry: 0, w, d, h, y: 0, base: 0, palette: o.palette, seed: `furniture_${type}_${seed}` };
34880
35518
  const { K, shared } = renderer.internals;
34881
35519
  const before = K.PARTS.length;
@@ -34894,7 +35532,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34894
35532
  shared.ctx = saved.ctx;
34895
35533
  }
34896
35534
  },
34897
- { materials: { plain: plain2, ...o.materials }, chairs: o.chairs }
35535
+ { materials: { plain: plain2, ...o.materials }, chairs: o.chairs, carcass: SHELVING.test(type) }
34898
35536
  );
34899
35537
  const spouts = K.PARTS.slice(before).filter((p) => p.type === "water_tap").map((p) => [p.pos[0] - root.x, p.pos[1], p.pos[2] - root.z]);
34900
35538
  K.PARTS.length = before;
@@ -38885,14 +39523,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38885
39523
  },
38886
39524
  tessellate: {
38887
39525
  cylinder: {
38888
- heightSegments: 6,
38889
- radialSegments: 20
39526
+ heightSegments: 24,
39527
+ radialSegments: 40,
39528
+ capRings: 6
38890
39529
  },
38891
39530
  box: {
38892
39531
  widthSegments: 6,
38893
39532
  heightSegments: 1,
38894
39533
  depthSegments: 4
38895
39534
  }
39535
+ },
39536
+ wall: {
39537
+ body: 1e-4,
39538
+ ends: 25e-5
38896
39539
  }
38897
39540
  }
38898
39541
  },
@@ -38916,14 +39559,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38916
39559
  },
38917
39560
  tessellate: {
38918
39561
  cylinder: {
38919
- heightSegments: 6,
38920
- radialSegments: 20
39562
+ heightSegments: 24,
39563
+ radialSegments: 40,
39564
+ capRings: 6
38921
39565
  },
38922
39566
  box: {
38923
39567
  widthSegments: 6,
38924
39568
  heightSegments: 1,
38925
39569
  depthSegments: 4
38926
39570
  }
39571
+ },
39572
+ wall: {
39573
+ body: 18e-5,
39574
+ ends: 22e-5
38927
39575
  }
38928
39576
  }
38929
39577
  }
@@ -39013,12 +39661,53 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39013
39661
  if (!box22) return 0;
39014
39662
  return (box22.max.x - box22.min.x) * (box22.max.y - box22.min.y) * (box22.max.z - box22.min.z);
39015
39663
  }
39016
- function tessellated(geometry, rule) {
39664
+ function ringedCylinder(T2, p, radial, maxRows, rings) {
39665
+ const half = p.height / 2;
39666
+ const column = 2 * Math.PI * Math.max(p.radiusTop, p.radiusBottom, 1e-6) / radial;
39667
+ const rows = Math.max(1, Math.min(maxRows, Math.round(p.height / column)));
39668
+ const points = [];
39669
+ if (!p.openEnded) for (let i = 0; i <= rings; i++) points.push(new T2.Vector2(p.radiusBottom * i / rings, -half));
39670
+ for (let j = 0; j <= rows; j++) points.push(new T2.Vector2(p.radiusBottom + (p.radiusTop - p.radiusBottom) * j / rows, -half + p.height * j / rows));
39671
+ if (!p.openEnded) for (let i = rings; i >= 0; i--) points.push(new T2.Vector2(p.radiusTop * i / rings, half));
39672
+ return lathed(T2, points, radial, p.thetaStart, p.thetaLength);
39673
+ }
39674
+ function lathed(T2, points, radial, phiStart, phiLength) {
39675
+ const geometry = new T2.LatheGeometry([...points], radial, phiStart, phiLength);
39676
+ geometry.computeVertexNormals();
39677
+ const position = geometry.getAttribute("position");
39678
+ const normal = geometry.getAttribute("normal");
39679
+ const middle = (Math.min(...points.map((point) => point.y)) + Math.max(...points.map((point) => point.y))) / 2;
39680
+ for (let i = 0; i < normal.count; i++) {
39681
+ if (Math.hypot(normal.getX(i), normal.getY(i), normal.getZ(i)) > 0.5) continue;
39682
+ normal.setXYZ(i, 0, position.getY(i) >= middle ? 1 : -1, 0);
39683
+ }
39684
+ return geometry;
39685
+ }
39686
+ function finerProfile(T2, points, step) {
39687
+ const out = [];
39688
+ points.forEach((point, index) => {
39689
+ const next = points[index + 1];
39690
+ out.push(point.clone());
39691
+ if (!next) return;
39692
+ const pieces = Math.max(1, Math.round(point.distanceTo(next) / step));
39693
+ for (let k = 1; k < pieces; k++) out.push(point.clone().lerp(next, k / pieces));
39694
+ });
39695
+ return out;
39696
+ }
39697
+ function tessellated(geometry, rule, T2) {
39017
39698
  const Shape = geometry.constructor;
39018
39699
  if (geometry.type === "CylinderGeometry" && rule.cylinder) {
39019
- const { radiusTop, radiusBottom, height, openEnded, thetaStart, thetaLength } = geometry.parameters;
39700
+ const parameters = geometry.parameters;
39701
+ const { radiusTop, radiusBottom, height, openEnded, thetaStart, thetaLength } = parameters;
39702
+ if (T2 && rule.cylinder.capRings !== void 0) return ringedCylinder(T2, parameters, rule.cylinder.radialSegments, rule.cylinder.heightSegments, rule.cylinder.capRings);
39020
39703
  return new Shape(radiusTop, radiusBottom, height, rule.cylinder.radialSegments, rule.cylinder.heightSegments, openEnded, thetaStart, thetaLength);
39021
39704
  }
39705
+ if (geometry.type === "LatheGeometry" && rule.cylinder && T2 && rule.cylinder.capRings !== void 0) {
39706
+ const { points, segments, phiStart, phiLength } = geometry.parameters;
39707
+ if (segments >= rule.cylinder.radialSegments) return void 0;
39708
+ const widest = Math.max(...points.map((point) => point.x), 1e-6);
39709
+ return lathed(T2, finerProfile(T2, points, 2 * Math.PI * widest / rule.cylinder.radialSegments), rule.cylinder.radialSegments, phiStart, phiLength);
39710
+ }
39022
39711
  if (geometry.type === "BoxGeometry" && rule.box) {
39023
39712
  const { width, height, depth } = geometry.parameters;
39024
39713
  return new Shape(width, height, depth, rule.box.widthSegments, rule.box.heightSegments, rule.box.depthSegments);
@@ -39026,14 +39715,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39026
39715
  return void 0;
39027
39716
  }
39028
39717
  var triangleCount = (geometry) => (geometry.index ? geometry.index.count : geometry.getAttribute("position").count) / 3;
39029
- function tessellateShapes(root, rule) {
39718
+ function tessellateShapes(root, rule, T2) {
39030
39719
  const rebuilt = /* @__PURE__ */ new Map();
39031
39720
  root.traverse((object2) => {
39032
39721
  const mesh3 = object2;
39033
39722
  if (!mesh3.isMesh) return;
39034
39723
  let fresh = rebuilt.get(mesh3.geometry);
39035
39724
  if (!fresh) {
39036
- fresh = tessellated(mesh3.geometry, rule);
39725
+ fresh = tessellated(mesh3.geometry, rule, T2);
39037
39726
  if (!fresh) return;
39038
39727
  rebuilt.set(mesh3.geometry, fresh);
39039
39728
  }
@@ -39047,10 +39736,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39047
39736
  root.userData.tessellationTriangles = added;
39048
39737
  }
39049
39738
  function tagForDestruction(root, options = {}) {
39050
- const { modelId, weight, size } = options;
39739
+ const { modelId, weight, size, three } = options;
39051
39740
  if (weight !== void 0 && weight > 0) root.userData.destructionMass = weight;
39052
39741
  const rule = modelId === void 0 ? void 0 : itemDestruction(modelId);
39053
- if (rule?.tessellate) tessellateShapes(root, rule.tessellate);
39742
+ if (rule?.tessellate) tessellateShapes(root, rule.tessellate, three);
39054
39743
  const solid2 = [];
39055
39744
  root.traverse((object2) => {
39056
39745
  const mesh3 = object2;
@@ -39070,6 +39759,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39070
39759
  const anchor = solid2.reduce((largest, mesh3) => volumeOf(mesh3) > volumeOf(largest) ? mesh3 : largest);
39071
39760
  solid2.forEach((mesh3, index) => {
39072
39761
  mesh3.userData.destructionPart = mesh3 === anchor ? { id: SHELL_PART_ID } : { id: `shell_${index}`, parent: SHELL_PART_ID, kind: "weld" };
39762
+ if (rule.wall) mesh3.userData.destructionThickness = mesh3 === anchor ? rule.wall.body : rule.wall.ends;
39073
39763
  });
39074
39764
  const contents = size === void 0 ? void 0 : itemContents({ ...modelId === void 0 ? {} : { model: modelId }, size, ...weight === void 0 ? {} : { weight } });
39075
39765
  if (contents) anchor.userData.destructionContains = { fluid: contents.liquid, liters: contents.liters };
@@ -44681,7 +45371,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
44681
45371
  const materials = kit.materials.slice(first);
44682
45372
  if (options.wear) setWear(materials, options.wear);
44683
45373
  root.userData.wear = options.wear ?? 0;
44684
- if (options.destructible) tagForDestruction(root, { ...kind === "model" && item.model !== void 0 ? { modelId: item.model } : {}, ...item.weight === void 0 ? {} : { weight: item.weight }, size: item.size });
45374
+ if (options.destructible) tagForDestruction(root, { ...kind === "model" && item.model !== void 0 ? { modelId: item.model } : {}, ...item.weight === void 0 ? {} : { weight: item.weight }, size: item.size, three: T2 });
44685
45375
  markKitOrigin(root, { owner: "game-items-three", kind: "item", id: item.id, ...kind === "shape" ? { standIn: true } : {} });
44686
45376
  return { root, kind, ...shape ? { shape } : {}, size: item.size, materials };
44687
45377
  }
@@ -60161,6 +60851,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
60161
60851
  return {
60162
60852
  id: "weapons_area",
60163
60853
  name: text.name,
60854
+ ground: { surface: "concrete_slab" },
60164
60855
  group: text.group,
60165
60856
  get audio() {
60166
60857
  return audio;
@@ -61223,6 +61914,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
61223
61914
  return {
61224
61915
  id: EXPLOSIVES_AREA_ID,
61225
61916
  name: text.name,
61917
+ ground: { surface: "concrete_slab" },
61226
61918
  group: text.group,
61227
61919
  bench,
61228
61920
  // the cylinder of this area is the world explosive of the weapons module with the bench's strength: the same id, the same model, the same part