@adatechnology/game-weapons-three 0.10.1 → 0.10.3

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.
@@ -19717,7 +19717,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19717
19717
  const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
19718
19718
  band.c[L] = (lo + hi) / 2;
19719
19719
  band.s[L] = Math.max(hi - lo, 0.01);
19720
- for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
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);
19721
19723
  return out;
19722
19724
  };
19723
19725
  var tearStack = (inp, root, t, u, v, n, sheet) => {
@@ -19748,6 +19750,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19748
19750
  const t = sorted[0];
19749
19751
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
19750
19752
  if (style === "snap") {
19753
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
19751
19754
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd2() * 3));
19752
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] });
19753
19756
  return out;
@@ -19921,6 +19924,27 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19921
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]]));
19922
19925
  return out;
19923
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
+ };
19924
19948
  var clipBarsToDisc = (bars, u, v, center, radius) => {
19925
19949
  const out = [];
19926
19950
  for (const [a, b] of bars) {
@@ -20841,14 +20865,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20841
20865
  smoke: "grey"
20842
20866
  },
20843
20867
  visual: {
20844
- charColor: 1709072
20868
+ charColor: 1709072,
20869
+ innerColor: 14730131
20845
20870
  },
20846
20871
  tags: [
20847
20872
  "wood",
20848
20873
  "organic",
20849
20874
  "flammable"
20850
20875
  ],
20851
- compressive: 1e5
20876
+ compressive: 1e5,
20877
+ hits: {
20878
+ blunt: {
20879
+ tear: "splinters",
20880
+ tearJoules: 60
20881
+ }
20882
+ }
20852
20883
  },
20853
20884
  {
20854
20885
  id: "wood_hard",
@@ -20878,14 +20909,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20878
20909
  smoke: "grey"
20879
20910
  },
20880
20911
  visual: {
20881
- charColor: 1709072
20912
+ charColor: 1709072,
20913
+ innerColor: 13213802
20882
20914
  },
20883
20915
  tags: [
20884
20916
  "wood",
20885
20917
  "organic",
20886
20918
  "flammable"
20887
20919
  ],
20888
- compressive: 15e4
20920
+ compressive: 15e4,
20921
+ hits: {
20922
+ blunt: {
20923
+ tear: "splinters",
20924
+ tearJoules: 150
20925
+ }
20926
+ }
20889
20927
  },
20890
20928
  {
20891
20929
  id: "plywood",
@@ -20915,14 +20953,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20915
20953
  smoke: "grey"
20916
20954
  },
20917
20955
  visual: {
20918
- charColor: 1709072
20956
+ charColor: 1709072,
20957
+ innerColor: 14862234
20919
20958
  },
20920
20959
  tags: [
20921
20960
  "wood",
20922
20961
  "organic",
20923
20962
  "flammable"
20924
20963
  ],
20925
- compressive: 6e4
20964
+ compressive: 6e4,
20965
+ hits: {
20966
+ ballistic: {
20967
+ tear: "splinters"
20968
+ },
20969
+ blunt: {
20970
+ tear: "splinters",
20971
+ tearJoules: 80
20972
+ }
20973
+ }
20926
20974
  },
20927
20975
  {
20928
20976
  id: "mdf",
@@ -20949,14 +20997,25 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20949
20997
  smoke: "grey"
20950
20998
  },
20951
20999
  visual: {
20952
- charColor: 1709072
21000
+ charColor: 1709072,
21001
+ innerColor: 12096362
20953
21002
  },
20954
21003
  tags: [
20955
21004
  "wood",
20956
21005
  "organic",
20957
21006
  "flammable"
20958
21007
  ],
20959
- 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
+ }
20960
21019
  },
20961
21020
  {
20962
21021
  id: "glass",
@@ -21070,8 +21129,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21070
21129
  damageScale: {
21071
21130
  explosive: 1.2
21072
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
+ },
21073
21143
  hitDebris: "chips",
21074
21144
  breakDebris: "dust",
21145
+ visual: { innerColor: 10197652 },
21075
21146
  thermal: {
21076
21147
  conductivity: 0.15,
21077
21148
  heatCapacity: 2.5
@@ -21102,6 +21173,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21102
21173
  ],
21103
21174
  hitDebris: "chips",
21104
21175
  breakDebris: "dust",
21176
+ visual: { innerColor: 12085567 },
21105
21177
  thermal: {
21106
21178
  conductivity: 0.15,
21107
21179
  heatCapacity: 2.5
@@ -21132,6 +21204,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21132
21204
  ],
21133
21205
  hitDebris: "dust",
21134
21206
  breakDebris: "dust",
21207
+ visual: { innerColor: 14999507 },
21135
21208
  thermal: {
21136
21209
  conductivity: 0.1,
21137
21210
  heatCapacity: 1.5
@@ -21157,6 +21230,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21157
21230
  ],
21158
21231
  hitDebris: "chips",
21159
21232
  breakDebris: "dust",
21233
+ visual: { innerColor: 9407364 },
21160
21234
  thermal: {
21161
21235
  conductivity: 0.2,
21162
21236
  heatCapacity: 2.5
@@ -21438,7 +21512,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21438
21512
  smoke: "white"
21439
21513
  },
21440
21514
  visual: {
21441
- charColor: 1709072
21515
+ charColor: 1709072,
21516
+ innerColor: 14207126
21442
21517
  },
21443
21518
  tags: [
21444
21519
  "plant",
@@ -21446,7 +21521,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21446
21521
  "organic",
21447
21522
  "flammable"
21448
21523
  ],
21449
- compressive: 6e4
21524
+ compressive: 6e4,
21525
+ hits: {
21526
+ blunt: {
21527
+ tear: "none"
21528
+ }
21529
+ }
21450
21530
  },
21451
21531
  {
21452
21532
  id: "cactus",
@@ -21561,6 +21641,130 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21561
21641
  "masonry"
21562
21642
  ],
21563
21643
  compressive: 2e4
21644
+ },
21645
+ {
21646
+ id: "asphalt",
21647
+ density: 2350,
21648
+ hpPerKg: 10,
21649
+ penetration: 14,
21650
+ penetrationClass: "concrete",
21651
+ penetrationScale: 0.8,
21652
+ ductility: 0.1,
21653
+ fracture: "crumble",
21654
+ fragmentSize: [
21655
+ 0.03,
21656
+ 0.15
21657
+ ],
21658
+ hitDebris: "chips",
21659
+ breakDebris: "dust",
21660
+ thermal: {
21661
+ conductivity: 0.1,
21662
+ heatCapacity: 2,
21663
+ ignitionC: 400,
21664
+ burnRate: 2e-3,
21665
+ smoke: "black"
21666
+ },
21667
+ tags: [
21668
+ "masonry",
21669
+ "brittle"
21670
+ ],
21671
+ compressive: 3e4
21672
+ },
21673
+ {
21674
+ id: "sand",
21675
+ density: 1600,
21676
+ hpPerKg: 2,
21677
+ penetration: 6,
21678
+ ductility: 0.4,
21679
+ fracture: "crumble",
21680
+ fragmentSize: [
21681
+ 0.01,
21682
+ 0.04
21683
+ ],
21684
+ hitDebris: "dust",
21685
+ breakDebris: "dust",
21686
+ thermal: {
21687
+ conductivity: 0.1,
21688
+ heatCapacity: 2
21689
+ },
21690
+ tags: [
21691
+ "soil"
21692
+ ],
21693
+ compressive: 1e4
21694
+ },
21695
+ {
21696
+ id: "gravel",
21697
+ density: 1800,
21698
+ hpPerKg: 3,
21699
+ penetration: 8,
21700
+ ductility: 0.3,
21701
+ fracture: "crumble",
21702
+ fragmentSize: [
21703
+ 0.02,
21704
+ 0.06
21705
+ ],
21706
+ hitDebris: "chips",
21707
+ breakDebris: "dust",
21708
+ thermal: {
21709
+ conductivity: 0.1,
21710
+ heatCapacity: 2
21711
+ },
21712
+ tags: [
21713
+ "soil",
21714
+ "masonry"
21715
+ ],
21716
+ compressive: 2e4
21717
+ },
21718
+ {
21719
+ id: "turf",
21720
+ density: 900,
21721
+ hpPerKg: 3,
21722
+ penetration: 4,
21723
+ ductility: 0.5,
21724
+ fracture: "crumble",
21725
+ fragmentSize: [
21726
+ 0.03,
21727
+ 0.1
21728
+ ],
21729
+ hitDebris: "dust",
21730
+ breakDebris: "dust",
21731
+ thermal: {
21732
+ conductivity: 0.1,
21733
+ heatCapacity: 1.5,
21734
+ ignitionC: 250,
21735
+ burnRate: 0.03,
21736
+ smoke: "white"
21737
+ },
21738
+ tags: [
21739
+ "soil",
21740
+ "organic"
21741
+ ],
21742
+ compressive: 15e3
21743
+ },
21744
+ {
21745
+ id: "mortar",
21746
+ density: 1900,
21747
+ hpPerKg: 10,
21748
+ penetration: 12,
21749
+ penetrationClass: "concrete",
21750
+ penetrationScale: 1.2,
21751
+ ductility: 0.03,
21752
+ fracture: "crumble",
21753
+ fragmentSize: [
21754
+ 0.02,
21755
+ 0.08
21756
+ ],
21757
+ hitDebris: "chips",
21758
+ breakDebris: "dust",
21759
+ thermal: {
21760
+ conductivity: 0.15,
21761
+ heatCapacity: 2.5
21762
+ },
21763
+ tags: [
21764
+ "masonry",
21765
+ "brittle"
21766
+ ],
21767
+ compressive: 1e5
21564
21768
  }
21565
21769
  ];
21566
21770
  var material_hints_default = {
@@ -21734,6 +21938,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21734
21938
  return (1 + 4 * (0.3 - ductility) / 0.3) * Math.min(25, Math.max(1, (0.02 / Math.max(thickness, 1e-4)) ** 2));
21735
21939
  };
21736
21940
  var THIN_SHEET_DISHING = 5e-3;
21941
+ var SWEEP_BACK = 0.01;
21942
+ function sweepOrigins(box5, d, spacing) {
21943
+ const origins = [];
21944
+ const line = (from, to) => {
21945
+ const n = Math.max(1, Math.ceil((to - from) / spacing));
21946
+ return Array.from({ length: n + 1 }, (_, i) => from + (to - from) * i / n);
21947
+ };
21948
+ for (const axis2 of [0, 1, 2]) {
21949
+ if (Math.abs(d[axis2]) < 1e-3) continue;
21950
+ const lead = d[axis2] > 0 ? box5.max[axis2] - SWEEP_BACK : box5.min[axis2] + SWEEP_BACK;
21951
+ const u = (axis2 + 1) % 3;
21952
+ const v = (axis2 + 2) % 3;
21953
+ for (const a of line(box5.min[u], box5.max[u])) {
21954
+ for (const b of line(box5.min[v], box5.max[v])) {
21955
+ const point = [0, 0, 0];
21956
+ point[axis2] = lead;
21957
+ point[u] = a;
21958
+ point[v] = b;
21959
+ origins.push(point);
21960
+ }
21961
+ }
21962
+ }
21963
+ return origins;
21964
+ }
21965
+ var THIN_SHEET_CRUSH = 5e-4;
21966
+ var THIN_SHEET_CONTACT = 0.025;
21737
21967
  var DMG_PER_J = 0.02;
21738
21968
  var BLAST_COUPLING = 0.1;
21739
21969
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
@@ -21750,11 +21980,15 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21750
21980
  var SEVER_BAND = 0.1;
21751
21981
  var SEVER_ASPECT = 3;
21752
21982
  var SEVER_BALLISTIC = 2;
21983
+ var TEAR_JOULES = 150;
21984
+ var tears = 0;
21753
21985
  var SUPPORT_TOLERANCE = 0.05;
21754
21986
  var UNIT_FRAGMENT_FACTOR = 4;
21755
21987
  var BREACH_GRID_CAP = 4096;
21756
21988
  var CHIP_ENERGY = 200;
21757
21989
  var CHIP_SPLASH = 0.5;
21990
+ var BAR_BOW = 0.08;
21991
+ var BULLET_JOINT_SPAN = 0.3;
21758
21992
  var CUT_SLICE = 1;
21759
21993
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
21760
21994
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -21764,8 +21998,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21764
21998
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
21765
21999
  };
21766
22000
  var capacityScale = (p) => {
21767
- if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
21768
- return p.rt.removed ? Math.max(0.05, 1 - p.rt.removed / p.mass0) : 1;
22001
+ const residual = p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
22002
+ if (p.st.stripped) return residual;
22003
+ const kept = Math.min(p.rt.removed ? 1 - p.rt.removed / p.mass0 : 1, 1 - sectionLoss(p));
22004
+ return Math.max(p.spec.reinforcement ? residual : 0.05, kept);
22005
+ };
22006
+ var sectionLoss = (p) => {
22007
+ const cells = p.st.chipped;
22008
+ if (!cells?.length) return 0;
22009
+ const [u, v] = planeAxes(p.thin);
22010
+ const along = p.dims[u] >= p.dims[v] ? u : v;
22011
+ const across2 = along === u ? v : u;
22012
+ let worst = 0;
22013
+ for (const c of cells) {
22014
+ let gone = 0;
22015
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across2];
22016
+ worst = Math.max(worst, gone / Math.max(p.dims[across2], 1e-6));
22017
+ }
22018
+ return Math.min(1, worst);
21769
22019
  };
21770
22020
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
21771
22021
  var clone = (v) => structuredClone(v);
@@ -22865,7 +23115,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22865
23115
  emit2({ ...base(o, src), type: "crack", part: p.spec.id, crack, worldPoint: wp });
22866
23116
  setStage(o, p, 1, {}, src);
22867
23117
  };
22868
- const debris2 = (o, p, kind, point, normal, count, speed, src) => emit2({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed });
23118
+ const debris2 = (o, p, kind, point, normal, count, speed, src, material) => emit2({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed, ...material && material !== p.spec.material ? { material } : {} });
22869
23119
  const holeRate = (hole, pressurized) => (pressurized ? 0.5 : 0.1) * (hole.diameter / 9e-3);
22870
23120
  const detachEvents = (o, opened, by, src) => {
22871
23121
  for (const j of opened) emit2({ ...base(o, src), type: "detach", joint: j.spec.id, parts: [j.spec.a, j.spec.b], by, groups: o.st.groups.map((g) => [...g]) });
@@ -22906,7 +23156,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22906
23156
  const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
22907
23157
  return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
22908
23158
  };
22909
- const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false) => {
23159
+ const jointLength = (o, j) => {
23160
+ const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
23161
+ let len = 0;
23162
+ for (let k = 0; k < 3; k++) len = Math.max(len, Math.min(A.max[k], B.max[k]) - Math.max(A.min[k], B.min[k]));
23163
+ return len;
23164
+ };
23165
+ const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false, span) => {
22910
23166
  if (amount <= 0) return;
22911
23167
  const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
22912
23168
  if (split && mine.length) amount /= mine.length;
@@ -22914,7 +23170,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22914
23170
  if (j.st.open) continue;
22915
23171
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
22916
23172
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
22917
- j.st.hp -= amount;
23173
+ j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
22918
23174
  if (j.st.hp <= 0) {
22919
23175
  j.st.hp = 0;
22920
23176
  j.st.open = true;
@@ -22943,7 +23199,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22943
23199
  if (spalls.length > MAX_SPALLS) spalls.shift();
22944
23200
  markDamaged(p);
22945
23201
  emit2({ ...base(o, src), type: "spall", part: p.spec.id, layer: layer2.index, point: wp, area: Math.PI * radius * radius });
22946
- debris2(o, p, layer2.mat.hitDebris, wp, normal, 5, 2, src);
23202
+ debris2(o, p, layer2.mat.hitDebris, wp, normal, 5, 2, src, layer2.mat.id);
23203
+ setStage(o, p, 1, {}, src);
23204
+ };
23205
+ const crater = (o, p, wp, normal, radius, src) => {
23206
+ markDamaged(p);
23207
+ emit2({ ...base(o, src), type: "spall", part: p.spec.id, layer: -1, point: wp, area: Math.PI * radius * radius });
23208
+ debris2(o, p, p.mat.breakDebris, wp, normal, 6, 1.5, src);
22947
23209
  setStage(o, p, 1, {}, src);
22948
23210
  };
22949
23211
  const insideOpening = (o, p, objectPoint) => {
@@ -23022,9 +23284,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23022
23284
  const lp = toLocal(p.pos, p.q, objectPoint);
23023
23285
  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);
23024
23286
  };
23025
- const chipUnits = (o, p, wp, energy, dir, radius, src) => {
23287
+ const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
23026
23288
  var _a, _b;
23027
- if (!p.spec.units || p.spec.reinforcement || !p.staged || chipEnergy <= 0 || energy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
23289
+ if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
23290
+ const rule = p.mat.hits?.[kind];
23291
+ const perKg = rule?.unitJoules ?? chipEnergy;
23292
+ const energy = hitEnergy - (rule?.floor ?? 0);
23293
+ if (perKg <= 0 || energy <= 0) return;
23028
23294
  const [u, v] = planeAxes(p.thin);
23029
23295
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
23030
23296
  const grid = gridOf(p);
@@ -23038,7 +23304,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23038
23304
  const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
23039
23305
  const total = (worn[i] ?? 0) + energy * share;
23040
23306
  const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
23041
- if (total < chipEnergy * mass) {
23307
+ if (total < perKg * mass) {
23042
23308
  worn[i] = total;
23043
23309
  continue;
23044
23310
  }
@@ -23063,7 +23329,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23063
23329
  emit2({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
23064
23330
  debris2(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
23065
23331
  }
23332
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
23066
23333
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
23334
+ if (p.structural && !isLost(p)) {
23335
+ solveStructure(o);
23336
+ collapseCheck(o, src);
23337
+ }
23338
+ };
23339
+ const exposeRebar = (o, p, src) => {
23340
+ const r = p.spec.reinforcement;
23341
+ if (!r) return;
23342
+ const [u, v] = planeAxes(p.thin);
23343
+ const all = rebarBars(p.dims, p.thin, r.spacing);
23344
+ let bars;
23345
+ if (p.st.stripped) bars = all;
23346
+ else {
23347
+ bars = [];
23348
+ for (const cell of p.st.chipped ?? []) bars.push(...clipBarsToBox(all, u, v, cell.center, cell.size));
23349
+ const op = p.st.opening;
23350
+ if (op) {
23351
+ const lc = toLocal(p.pos, p.q, op.center);
23352
+ bars.push(...clipBarsToDisc(all, u, v, [lc[u], lc[v]], op.radius + 0.05));
23353
+ }
23354
+ }
23355
+ const bow = clamp2(((p.st.stress ?? 0) - 1) / 0.5, 0, 1);
23356
+ if (bow > 0) bars = bars.flatMap((bar2, i) => bendBar(bar2, p.thin, bow * BAR_BOW, i));
23357
+ emit2({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
23067
23358
  };
23068
23359
  const openBreach = (o, p, center, total, energy, dir, src) => {
23069
23360
  const [u, v] = planeAxes(p.thin);
@@ -23091,10 +23382,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23091
23382
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
23092
23383
  debris2(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
23093
23384
  const reinforcement = p.spec.reinforcement;
23094
- if (reinforcement) {
23095
- const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
23096
- if (bars.length > 0) emit2({ ...base(o, src), type: "expose", part: p.spec.id, bars });
23097
- }
23385
+ if (reinforcement) exposeRebar(o, p, src);
23098
23386
  if (p.structural) solveStructure(o);
23099
23387
  };
23100
23388
  const stripPart = (o, p, reinforcement, src) => {
@@ -23106,8 +23394,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23106
23394
  p.st.burning = false;
23107
23395
  p.rt.jet = false;
23108
23396
  markDamaged(p);
23109
- emit2({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
23110
23397
  solveStructure(o);
23398
+ exposeRebar(o, p, src);
23111
23399
  collapseCheck(o, src);
23112
23400
  };
23113
23401
  const unitFragmentPlan = (p, cap) => {
@@ -23161,7 +23449,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23161
23449
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
23162
23450
  const hitLocal = toLocal(c, q, hitW);
23163
23451
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
23164
- const axis2 = style === "splinter" ? severAxis(p) : -1;
23452
+ const axis2 = style === "splinter" || style === "snap" ? severAxis(p) : -1;
23165
23453
  const cutAt2 = axis2 < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis2] : void 0);
23166
23454
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
23167
23455
  style,
@@ -23233,7 +23521,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23233
23521
  };
23234
23522
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
23235
23523
  const severAxis = (p) => {
23236
- if (p.mat.fracture !== "splinter" || p.spec.shape.kind !== "box") return -1;
23524
+ if (p.mat.fracture !== "splinter" && p.mat.fracture !== "snap" || p.spec.shape.kind !== "box") return -1;
23237
23525
  const d = p.dims;
23238
23526
  const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
23239
23527
  return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
@@ -23294,6 +23582,49 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23294
23582
  }
23295
23583
  }
23296
23584
  };
23585
+ const tearOf = (mat, kind) => {
23586
+ const h = mat.hits?.[kind];
23587
+ const tear = h?.tear ?? (kind === "ballistic" && mat.penetrationClass === "wood" ? "splinters" : "none");
23588
+ return { tear, joules: h?.tearJoules ?? TEAR_JOULES };
23589
+ };
23590
+ const tearAt = (o, p, kind, entry2, exit, d, E, scale, src) => {
23591
+ const { tear, joules } = tearOf(p.mat, kind);
23592
+ if (tear === "none") return;
23593
+ const shot = kind === "ballistic";
23594
+ const n = shot ? exit ? clamp2(Math.round(E / joules), 1, 7) : 1 : clamp2(Math.floor(E / joules), 0, 5);
23595
+ if (n === 0) return;
23596
+ const fibre = tear === "lumps";
23597
+ const { q } = partXf(o, p);
23598
+ const along = (p.spec.grain ? AXIS[p.spec.grain] : void 0) ?? [0, 1, 2].sort((a, b) => p.dims[b] - p.dims[a])[0];
23599
+ const fragments = [];
23600
+ for (let i = 0; i < n; i++) {
23601
+ const back = !exit || i % 3 === 2;
23602
+ 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());
23603
+ const size = [w, w, w];
23604
+ size[along] = len;
23605
+ size[(along + 1) % 3] = w;
23606
+ size[(along + 2) % 3] = t;
23607
+ const spread3 = [(rnd2() - 0.5) * 0.8, (rnd2() - 0.2) * 0.8, (rnd2() - 0.5) * 0.8];
23608
+ const speed = shot ? back ? 1.5 + 2 * rnd2() : 2 + 4 * rnd2() : 0.8 + 1.2 * rnd2();
23609
+ const v = vec.scale(vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3)), speed);
23610
+ const mass = size[0] * size[1] * size[2] * p.mat.density;
23611
+ fragments.push({
23612
+ id: `${p.spec.id}#t${tears++}-${i}`,
23613
+ part: p.spec.id,
23614
+ material: p.spec.material,
23615
+ shape: fibre ? "chunk" : "splinter",
23616
+ center: back || !exit ? vec.madd(entry2, d, -0.01) : vec.madd(exit, d, 0.01),
23617
+ size,
23618
+ rotation: q,
23619
+ velocity: v,
23620
+ inner: true,
23621
+ angularVelocity: [(rnd2() - 0.5) * 30, (rnd2() - 0.5) * 30, (rnd2() - 0.5) * 30],
23622
+ mass
23623
+ });
23624
+ }
23625
+ const patch = [scale * 4, scale * 4, scale * 4];
23626
+ emit2({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments });
23627
+ };
23297
23628
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round5 = {}) => {
23298
23629
  const mat = p.mat;
23299
23630
  const piercing = round5.piercing ?? 1;
@@ -23322,6 +23653,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23322
23653
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
23323
23654
  }
23324
23655
  if (p.layers.length > 0) spallOuter(o, p, entry2, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
23656
+ else if (mat.hits?.ballistic?.crater) crater(o, p, entry2, n, clamp2(0.02 + Math.sqrt(E) * 15e-4, 0.02, 0.15), src);
23325
23657
  const brittle = mat.ductility < 0.3;
23326
23658
  const ratio = Ep / Math.max(thr, 1e-6);
23327
23659
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
@@ -23354,9 +23686,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23354
23686
  if (mat.fracture !== "shatter") addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
23355
23687
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
23356
23688
  debris2(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
23357
- wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
23358
- const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry2, d, E * 0.1, 12, src);
23359
- chipUnits(o, p, entry2, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
23689
+ wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
23690
+ const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
23691
+ const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
23692
+ if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
23693
+ 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);
23360
23694
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
23361
23695
  };
23362
23696
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -23364,14 +23698,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23364
23698
  const thrMm = mat.penetration * p.thickness * 1e3;
23365
23699
  const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
23366
23700
  const Ec = E * fragile;
23701
+ const sheet = mat.ductility >= 0.5 && p.thickness <= THIN_SHEET_CRUSH;
23702
+ const fold = sheet ? Math.min(4, Math.sqrt(1e-3 / p.thickness)) : 1;
23703
+ const patch = sheet ? Math.min(contactR, THIN_SHEET_CONTACT) : contactR;
23367
23704
  if (E > 0) {
23368
23705
  if (kind === "blunt") {
23369
- if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp2(E * 1e-4, 1e-3, 0.06), src);
23706
+ 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);
23370
23707
  else if (mat.ductility < 0.3) {
23371
23708
  if (Ec >= 5) addCrack(o, p, point, n, clamp2(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
23372
23709
  } 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);
23373
23710
  } else if (mat.fracture === "tear") {
23374
23711
  addHole(o, p, point, n, clamp2(0.02 + E * 5e-4, 0.02, 0.3), true, void 0, src);
23712
+ } else if (mat.ductility >= 0.3 && E >= thrMm) {
23713
+ addHole(o, p, point, n, clamp2(contactR, 0.01, 0.08), true, void 0, src);
23714
+ if (sheet) addDent(o, p, point, n, Math.max(patch * 0.6, 0.01), clamp2(E * 4e-5 * fold, 5e-4, 0.02), src);
23375
23715
  } else if (mat.ductility >= 0.5) {
23376
23716
  addDent(o, p, point, n, Math.max(contactR, 0.01), clamp2(E * 4e-5, 5e-4, 0.02), src);
23377
23717
  }
@@ -23384,7 +23724,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23384
23724
  const scale = mat.damageScale?.[kind] ?? 1;
23385
23725
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
23386
23726
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
23387
- if (kind === "blunt") chipUnits(o, p, point, E, d, Math.max(contactR, 0.03), src);
23727
+ if (kind === "blunt") chipUnits(o, p, "blunt", point, E, d, Math.max(contactR, 0.03), src);
23728
+ if (E > 0 && !isLost(p)) tearAt(o, p, kind, point, void 0, d, E, clamp2(contactR * 0.2, 6e-3, 0.02), src);
23388
23729
  return done;
23389
23730
  };
23390
23731
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -23963,6 +24304,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23963
24304
  if (p.rt.stressT <= 0) {
23964
24305
  p.rt.stressT = 0.5;
23965
24306
  emit2({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
24307
+ if (p.spec.reinforcement && (p.st.stripped || p.st.chipped?.length || p.st.opening)) exposeRebar(o, p);
23966
24308
  }
23967
24309
  applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
23968
24310
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
@@ -24179,6 +24521,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
24179
24521
  const r = raycastInternal(origin, direction, maxDistance);
24180
24522
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
24181
24523
  },
24524
+ sweepBox(box5, direction, distance3, options2 = {}) {
24525
+ const d = vec.normalize(direction);
24526
+ const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
24527
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
24528
+ let best = null;
24529
+ for (const origin of sweepOrigins(box5, d, options2.spacing ?? 0.2)) {
24530
+ const r = raycastInternal(origin, d, distance3 + SWEEP_BACK, ignore);
24531
+ if (!r || best && r.distance >= best.distance) continue;
24532
+ best = { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: Math.max(0, r.distance - SWEEP_BACK) };
24533
+ }
24534
+ return best;
24535
+ },
24182
24536
  shoot(origin, direction, energy, caliber, source, round5) {
24183
24537
  const extras = typeof round5 === "number" ? { piercing: round5 } : round5 ?? {};
24184
24538
  return withCollapses(run(() => {
@@ -25750,7 +26104,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
25750
26104
  }
25751
26105
  return spec;
25752
26106
  };
25753
- var partSpecOf = (part, capacity, detailed) => detailed && part.assembly ? assemblySpec(part, capacity) : boxSpec(part, capacity);
26107
+ var MEMBER_CAGE = { spacing: 0.1, diameter: 8e-3, unit: 0.1 };
26108
+ var CAGED_ROLES = /* @__PURE__ */ new Set(["beam", "column"]);
26109
+ var caged = (spec, part, rebar2) => {
26110
+ if (!rebar2 || part.material !== "concrete" || !CAGED_ROLES.has(part.role) || spec.reinforcement) return spec;
26111
+ const size = spec.shape.kind === "box" ? spec.shape.size : null;
26112
+ if (!size) return spec;
26113
+ const thin = Math.min(...size);
26114
+ return {
26115
+ ...spec,
26116
+ reinforcement: { material: rebar2, spacing: MEMBER_CAGE.spacing, diameter: MEMBER_CAGE.diameter },
26117
+ units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, thin], bond: "stack", mortar: "concrete" }
26118
+ };
26119
+ };
26120
+ var partSpecOf = (part, capacity, detailed, rebar2) => {
26121
+ if (detailed && part.assembly) return assemblySpec(part, capacity);
26122
+ const spec = boxSpec(part, capacity);
26123
+ return detailed ? caged(spec, part, rebar2) : spec;
26124
+ };
25754
26125
  function toDestructibleSpec(structure, options = {}) {
25755
26126
  const { safety = 0.65, redundancy = 0.97, safetyOthers = 0.45 } = options;
25756
26127
  const kept = structure.parts.filter((part) => !options.filter || options.filter(part));
@@ -25767,7 +26138,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
25767
26138
  const sizing = { safety, redundancy, safetyOthers };
25768
26139
  const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts, panels.supports, sizing) };
25769
26140
  const { parts, supports, capacities } = detail === "units" ? applyUnits(sized, structure) : sized;
25770
- const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse"));
26141
+ const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse", structure.construction.frame.rebar));
25771
26142
  const margin = options.terrainMargin ?? TERRAIN_MARGIN;
25772
26143
  const x0 = Math.min(...parts.map((part) => part.aabb[0]));
25773
26144
  const z0 = Math.min(...parts.map((part) => part.aabb[1]));
@@ -33732,7 +34103,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33732
34103
  if (!pc || pc.unit) return sp;
33733
34104
  if (pc.role !== "wall" && pc.role !== "partition") {
33734
34105
  const def = getAssembly(pc.assembly || (pc.material === "concrete" && ["slab", "beam", "column", "stair"].includes(pc.role) ? "reinforced_concrete" : ""));
33735
- return def && def.reinforcement && !sp.reinforcement ? { ...sp, reinforcement: { ...def.reinforcement } } : sp;
34106
+ const out = def && def.reinforcement && !sp.reinforcement ? { ...sp, reinforcement: { ...def.reinforcement } } : sp;
34107
+ if (out.reinforcement && !out.units && pc.material === "concrete" && (pc.role === "beam" || pc.role === "column") && sp.shape.kind === "box") {
34108
+ return { ...out, units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, Math.min(...sp.shape.size)], bond: "stack", mortar: "concrete" } };
34109
+ }
34110
+ return out;
33736
34111
  }
33737
34112
  if (!pc.assembly) return sp;
33738
34113
  const [u0, v0, u1, v1] = pc.aabb, alongU = u1 - u0 >= v1 - v0;
@@ -33767,6 +34142,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33767
34142
  pos.array.set(saved, v0 * 3);
33768
34143
  pos.needsUpdate = true;
33769
34144
  }
34145
+ function noise2(x, y, cell) {
34146
+ const h = (i2, j2) => {
34147
+ const s = Math.sin(i2 * 127.1 + j2 * 311.7) * 43758.5453;
34148
+ return s - Math.floor(s);
34149
+ };
34150
+ const fx = x / cell, fy = y / cell, i = Math.floor(fx), j = Math.floor(fy), u = fx - i, v = fy - j;
34151
+ const su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v);
34152
+ return (h(i, j) * (1 - su) + h(i + 1, j) * su) * (1 - sv) + (h(i, j + 1) * (1 - su) + h(i + 1, j + 1) * su) * sv;
34153
+ }
34154
+ var MASK_TEXEL = 0.02;
34155
+ var SPALL_MIN = 0.025;
34156
+ var SPALL_RAGGED = 0.045;
34157
+ var COVER2 = 0.015;
33770
34158
  function installDestructionBreach(K, _shared) {
33771
34159
  const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
33772
34160
  function createBreachView(opts) {
@@ -33776,94 +34164,198 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33776
34164
  const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
33777
34165
  const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
33778
34166
  const tmp = { m: new THREE.Matrix4(), q: new THREE.Quaternion(), p: new THREE.Vector3(), s: new THREE.Vector3(), c: new THREE.Color() };
33779
- const wallColour = (items) => {
33780
- for (const it of items) {
33781
- if (!it.mesh) continue;
33782
- const c = it.mesh.geometry.attributes.color;
33783
- if (!c) continue;
33784
- const a = c.array;
33785
- tmp.c.setRGB(a[it.v0 * 3], a[it.v0 * 3 + 1], a[it.v0 * 3 + 2]);
33786
- return tmp.c.getHex();
33787
- }
33788
- return null;
33789
- };
33790
34167
  const local2 = (p) => {
33791
34168
  house.updateMatrixWorld(true);
33792
34169
  const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
33793
34170
  return [v.x, v.y, v.z];
33794
34171
  };
33795
- function courses(part, units, opening, chips2, paint2) {
34172
+ function cells(part, units) {
33796
34173
  const [u0, v0, u1, v1] = part.aabb;
33797
34174
  const alongU = u1 - u0 >= v1 - v0;
33798
34175
  const len = alongU ? u1 - u0 : v1 - v0;
33799
- const thick = alongU ? v1 - v0 : u1 - u0;
33800
34176
  const [ul, uh] = units.size;
33801
34177
  const rows = Math.max(1, Math.round((part.y1 - part.y0) / uh));
33802
34178
  const rowH = (part.y1 - part.y0) / rows;
33803
34179
  const per = Math.max(1, Math.round(len / ul));
33804
34180
  const w = len / per;
33805
- const edge = fragmentMaterial(part.material).color.getHex();
33806
- const list2 = [];
34181
+ const out = [];
33807
34182
  for (let r = 0; r < rows; r++) {
33808
34183
  const shift = units.bond === "running" && r & 1 ? w / 2 : 0;
33809
- const cells = per + (shift ? 1 : 0);
33810
- for (let i = 0; i < cells; i++) {
33811
- let a = i * w - shift, b = a + w;
33812
- a = Math.max(0, a);
33813
- b = Math.min(len, b);
33814
- if (b - a < 1e-3) continue;
33815
- const mid = (a + b) / 2;
33816
- const x = alongU ? u0 + mid : (u0 + u1) / 2;
33817
- const z = alongU ? (v0 + v1) / 2 : v0 + mid;
33818
- const y = part.y0 + (r + 0.5) * rowH;
33819
- const d = opening ? Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]) : Infinity;
33820
- if (opening && d < opening.radius) continue;
33821
- 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);
33822
- if (chips2.some(gone)) continue;
33823
- const near = opening && d < opening.radius + 0.25 || chips2.some((c) => Math.hypot(c[0] - x, c[1] - y, c[2] - z) < 0.15);
33824
- list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: near || paint2 === null ? edge : paint2 });
34184
+ for (let i = 0; i < per + (shift ? 1 : 0); i++) {
34185
+ const a = Math.max(0, i * w - shift), b = Math.min(len, i * w - shift + w);
34186
+ if (b - a > 1e-3) out.push({ a, b, y0: part.y0 + r * rowH, y1: part.y0 + (r + 1) * rowH, r, i });
33825
34187
  }
33826
34188
  }
33827
- return list2;
34189
+ return out;
34190
+ }
34191
+ const specOf = (id) => opts.spec()?.parts?.find((q) => q.id === id);
34192
+ const same = (a, b) => a.every((x, k) => Math.abs(x - b[k]) < 1e-3);
34193
+ function hostOf(partId) {
34194
+ if (structure.meshesOf(partId).some((it) => it.mesh)) return partId;
34195
+ const part = structure.byId.get(partId);
34196
+ if (!part) return null;
34197
+ for (const q of structure.parts) {
34198
+ 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;
34199
+ }
34200
+ return null;
34201
+ }
34202
+ 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);
34203
+ function shell(it, mask) {
34204
+ const g0 = it.mesh.geometry, g = new THREE.BufferGeometry();
34205
+ const src = it.mesh.userData._p0 || g0.attributes.position.array;
34206
+ for (const [name, attr] of Object.entries(g0.attributes)) {
34207
+ const k = attr.itemSize;
34208
+ g.setAttribute(name, new THREE.BufferAttribute((name === "position" ? src : attr.array).slice(it.v0 * k, it.v1 * k), k));
34209
+ }
34210
+ if (g0.index) {
34211
+ const ix = g0.index.array.slice(it.i0, it.i1);
34212
+ for (let k = 0; k < ix.length; k++) ix[k] -= it.v0;
34213
+ g.setIndex(new THREE.BufferAttribute(ix, 1));
34214
+ }
34215
+ g.computeBoundingBox();
34216
+ g.computeBoundingSphere();
34217
+ const base = it.mesh.material;
34218
+ const material = base.clone();
34219
+ const toHouse = { value: new THREE.Matrix4() };
34220
+ material.onBeforeCompile = (sh) => {
34221
+ base.onBeforeCompile?.(sh);
34222
+ sh.uniforms.uToHouse = toHouse;
34223
+ sh.uniforms.uMask = { value: mask.texture };
34224
+ sh.uniforms.uMaskRect = { value: mask.rect };
34225
+ sh.uniforms.uMaskAlongX = { value: mask.alongU ? 1 : 0 };
34226
+ sh.vertexShader = sh.vertexShader.replace("void main() {", "uniform mat4 uToHouse;\nvarying vec3 vHousePos;\nvoid main() {").replace("#include <begin_vertex>", "#include <begin_vertex>\nvHousePos = (uToHouse * vec4(transformed, 1.0)).xyz;");
34227
+ sh.fragmentShader = sh.fragmentShader.replace("void main() {", "uniform sampler2D uMask;\nuniform vec4 uMaskRect;\nuniform float uMaskAlongX;\nvarying vec3 vHousePos;\nvoid main() {\n vec2 mUv = vec2(((uMaskAlongX > 0.5 ? vHousePos.x : vHousePos.z) - uMaskRect.x) / uMaskRect.z, (vHousePos.y - uMaskRect.y) / uMaskRect.w);\n if (mUv.x >= 0.0 && mUv.x <= 1.0 && mUv.y >= 0.0 && mUv.y <= 1.0 && texture2D(uMask, mUv).r > 0.5) discard;");
34228
+ };
34229
+ material.customProgramCacheKey = () => "breachMask" + (base.customProgramCacheKey?.() ?? "");
34230
+ const mesh3 = new THREE.Mesh(g, material);
34231
+ mesh3.name = "breach-shell";
34232
+ mesh3.userData.breachMask = mask;
34233
+ mesh3.position.copy(it.mesh.position);
34234
+ mesh3.quaternion.copy(it.mesh.quaternion);
34235
+ mesh3.scale.copy(it.mesh.scale);
34236
+ mesh3.castShadow = it.mesh.castShadow;
34237
+ mesh3.receiveShadow = it.mesh.receiveShadow;
34238
+ mesh3.renderOrder = it.mesh.renderOrder;
34239
+ mesh3.onBeforeRender = () => {
34240
+ house.updateMatrixWorld();
34241
+ toHouse.value.copy(house.matrixWorld).invert().multiply(mesh3.matrixWorld);
34242
+ };
34243
+ it.mesh.parent.add(mesh3);
34244
+ mesh3.updateMatrixWorld(true);
34245
+ toHouse.value.copy(house.matrixWorld).invert().multiply(mesh3.matrixWorld);
34246
+ return mesh3;
34247
+ }
34248
+ function maskOf(hostId, parts) {
34249
+ const host = structure.byId.get(hostId);
34250
+ const [u0, v0, u1, v1] = host.aabb;
34251
+ const alongU = u1 - u0 >= v1 - v0;
34252
+ const y0 = Math.min(...parts.map((id) => structure.byId.get(id).y0)), y1 = Math.max(...parts.map((id) => structure.byId.get(id).y1));
34253
+ const len = alongU ? u1 - u0 : v1 - v0;
34254
+ const w = Math.max(2, Math.min(2048, Math.ceil(len / MASK_TEXEL))), h = Math.max(2, Math.min(1024, Math.ceil((y1 - y0) / MASK_TEXEL)));
34255
+ const data = new Uint8Array(w * h);
34256
+ const texture = new THREE.DataTexture(data, w, h, THREE.RedFormat);
34257
+ texture.magFilter = THREE.LinearFilter;
34258
+ texture.minFilter = THREE.LinearFilter;
34259
+ texture.needsUpdate = true;
34260
+ return { data, texture, w, h, alongU, start: alongU ? u0 : v0, len, y0, height: y1 - y0, rect: new THREE.Vector4(alongU ? u0 : v0, y0, len, y1 - y0) };
34261
+ }
34262
+ const boxDistance = (s, y, a, b, c, d) => Math.hypot(Math.max(a - s, 0, s - b), Math.max(c - y, 0, y - d));
34263
+ function paint2(view) {
34264
+ const { mask } = view;
34265
+ const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
34266
+ const holes = [];
34267
+ const discs = [];
34268
+ const shown = [];
34269
+ for (const id of view.parts) {
34270
+ const part = structure.byId.get(id), units2 = specOf(id).units, chips2 = view.chips.get(id), opening = view.openings.get(id);
34271
+ const all = cells(part, units2);
34272
+ const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
34273
+ const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
34274
+ if (opening) discs.push({ s: along(opening.center), y: opening.center[1], r: opening.radius });
34275
+ const gone = (c) => chips2.some((p) => p[1] >= c.y0 && p[1] <= c.y1 && along(p) >= c.a && along(p) <= c.b) || opening && Math.hypot((c.a + c.b) / 2 - along(opening.center), (c.y0 + c.y1) / 2 - opening.center[1]) < opening.radius;
34276
+ const kept = [];
34277
+ for (const c of all) (gone(c) ? holes : kept).push(c);
34278
+ for (const c of kept) shown.push({ c, part, units: units2, thick, mid });
34279
+ }
34280
+ const reach = SPALL_MIN + SPALL_RAGGED;
34281
+ mask.data.fill(0);
34282
+ const stamp = (s0, s1, y0, y1, inside2) => {
34283
+ const i0 = Math.max(0, Math.floor((s0 - reach) / mask.len * mask.w)), i1 = Math.min(mask.w - 1, Math.ceil((s1 + reach) / mask.len * mask.w));
34284
+ const j0 = Math.max(0, Math.floor((y0 - reach - mask.y0) / mask.height * mask.h)), j1 = Math.min(mask.h - 1, Math.ceil((y1 + reach - mask.y0) / mask.height * mask.h));
34285
+ for (let j = j0; j <= j1; j++) {
34286
+ const y = mask.y0 + (j + 0.5) / mask.h * mask.height;
34287
+ for (let i = i0; i <= i1; i++) {
34288
+ const s = (i + 0.5) / mask.w * mask.len;
34289
+ const edge = SPALL_MIN + SPALL_RAGGED * (0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011));
34290
+ if (inside2(s, y) <= edge) mask.data[j * mask.w + i] = 255;
34291
+ }
34292
+ }
34293
+ };
34294
+ for (const c of holes) stamp(c.a, c.b, c.y0, c.y1, (s, y) => boxDistance(s, y, c.a, c.b, c.y0, c.y1));
34295
+ for (const d of discs) stamp(d.s - d.r, d.s + d.r, d.y - d.r, d.y + d.r, (s, y) => Math.max(0, Math.hypot(s - d.s, y - d.y) - d.r));
34296
+ mask.texture.needsUpdate = true;
34297
+ const near = (c) => holes.some((h) => boxDistance((c.a + c.b) / 2, (c.y0 + c.y1) / 2, h.a - (c.b - c.a) / 2, h.b + (c.b - c.a) / 2, h.y0 - (c.y1 - c.y0) / 2, h.y1 + (c.y1 - c.y0) / 2) <= reach) || discs.some((d) => boxDistance(d.s, d.y, c.a, c.b, c.y0, c.y1) <= d.r + reach);
34298
+ const list2 = [];
34299
+ const tone = (hex2, k) => tmp.c.setHex(hex2).multiplyScalar(k).getHex();
34300
+ for (const { c, part, units: units2, thick, mid } of shown) {
34301
+ if (!near(c)) continue;
34302
+ const centre = mask.start + (c.a + c.b) / 2, y = (c.y0 + c.y1) / 2, len = c.b - c.a, h = c.y1 - c.y0;
34303
+ const at3 = (s, dy, d, colour) => list2.push({ x: mask.alongU ? centre : mid, y, z: mask.alongU ? mid : centre, sx: mask.alongU ? s : d, sy: dy, sz: mask.alongU ? d : s, colour });
34304
+ const base = fragmentMaterial(part.material).color.getHex();
34305
+ const n = noise2(centre * 3.1, y * 2.7, 0.07);
34306
+ if (units2.mortar === "concrete") {
34307
+ at3(len, h, thick - 2 * COVER2 - 0.03 * n, tone(base, 0.86 + 0.2 * n));
34308
+ } else {
34309
+ at3(len, h, thick - 2 * COVER2 - 0.012, 10327948);
34310
+ at3(len - 0.012, h - 0.012, thick - 2 * COVER2, tone(base, 0.8 + 0.35 * n));
34311
+ }
34312
+ }
34313
+ if (view.units) {
34314
+ view.units.removeFromParent();
34315
+ view.units.dispose();
34316
+ }
34317
+ const units = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
34318
+ units.name = "breach:" + view.host;
34319
+ list2.forEach((u, i) => {
34320
+ tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
34321
+ units.setMatrixAt(i, tmp.m);
34322
+ units.setColorAt(i, tmp.c.setHex(u.colour));
34323
+ });
34324
+ units.count = list2.length;
34325
+ units.instanceMatrix.needsUpdate = true;
34326
+ if (units.instanceColor) units.instanceColor.needsUpdate = true;
34327
+ units.castShadow = true;
34328
+ units.receiveShadow = true;
34329
+ units.visible = lod === 0;
34330
+ house.add(units);
34331
+ view.units = units;
34332
+ view.gone = holes.length;
33828
34333
  }
33829
34334
  function draw2(partId, change) {
33830
34335
  if (lod !== 0) return false;
33831
- const part = structure.byId.get(partId);
33832
- const spec = opts.spec()?.parts?.find((q) => q.id === partId);
33833
- if (!part || !spec || !spec.units) return false;
33834
- let view = views.get(partId);
34336
+ const hostId = hostOf(partId);
34337
+ if (!hostId || !specOf(partId)?.units) return false;
34338
+ let view = views.get(hostId);
33835
34339
  if (!view) {
33836
- const items = structure.meshesOf(partId).filter((it) => it.mesh);
34340
+ const items = structure.meshesOf(hostId).filter((it) => it.mesh);
33837
34341
  if (!items.length) return false;
33838
- view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: [] };
34342
+ const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
34343
+ if (!parts.includes(partId)) return false;
34344
+ const mask = maskOf(hostId, parts);
34345
+ view = { host: hostId, parts, items, mask, shells: [], saved: null, units: null, gone: 0, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
33839
34346
  }
34347
+ if (!view.chips.has(partId)) return false;
33840
34348
  if (change.opening) {
33841
- if (view.opening) return false;
33842
- view.opening = { center: local2(change.opening.center), radius: change.opening.radius };
34349
+ if (view.openings.has(partId)) return false;
34350
+ view.openings.set(partId, { center: local2(change.opening.center), radius: change.opening.radius });
33843
34351
  }
33844
- if (change.chip) view.chips.push(local2(change.chip));
33845
- const list2 = courses(part, spec.units, view.opening, view.chips, view.paint);
33846
- if (view.mesh) {
33847
- view.mesh.removeFromParent();
33848
- view.mesh.dispose();
34352
+ if (change.chip) view.chips.get(partId).push(local2(change.chip));
34353
+ if (!view.saved) {
34354
+ view.shells = view.items.map((it) => shell(it, view.mask));
34355
+ view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
33849
34356
  }
33850
- const mesh3 = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
33851
- mesh3.name = "breach:" + partId;
33852
- list2.forEach((u, i) => {
33853
- tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
33854
- mesh3.setMatrixAt(i, tmp.m);
33855
- mesh3.setColorAt(i, tmp.c.setHex(u.colour));
33856
- });
33857
- mesh3.count = list2.length;
33858
- mesh3.instanceMatrix.needsUpdate = true;
33859
- if (mesh3.instanceColor) mesh3.instanceColor.needsUpdate = true;
33860
- mesh3.castShadow = true;
33861
- mesh3.receiveShadow = true;
33862
- house.add(mesh3);
33863
- if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
33864
- view.mesh = mesh3;
33865
- view.units = list2.length;
33866
- views.set(partId, view);
34357
+ paint2(view);
34358
+ views.set(hostId, view);
33867
34359
  invalidate();
33868
34360
  invalidateShadow();
33869
34361
  return true;
@@ -33874,30 +34366,40 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33874
34366
  else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
33875
34367
  }
33876
34368
  }
34369
+ function dispose(view) {
34370
+ view.units.removeFromParent();
34371
+ view.units.dispose();
34372
+ for (const m of view.shells) {
34373
+ m.removeFromParent();
34374
+ m.geometry.dispose();
34375
+ m.material.dispose();
34376
+ }
34377
+ view.mask.texture.dispose();
34378
+ }
33877
34379
  return {
33878
- /** Handles a `stage` event of the house's object. Returns true when the wall was redrawn as courses. */
34380
+ /** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
33879
34381
  stage(event) {
33880
34382
  return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
33881
34383
  },
33882
- /** Handles a `chip` event: the unit that came out leaves a gap in the courses. Returns true when the wall was redrawn. */
34384
+ /** Handles a `chip` event: the unit that came out leaves a hole in the wall where it was. Returns true when the wall was cut. */
33883
34385
  chip(event) {
33884
34386
  return draw2(event.part, { chip: event.center });
33885
34387
  },
33886
- /** The part broke or fell: the courses go (the plain range stays collapsed, the renderer hides the part). */
34388
+ /** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
33887
34389
  drop(partId) {
33888
34390
  const v = views.get(partId);
33889
34391
  if (!v) return;
33890
- v.mesh.removeFromParent();
33891
- v.mesh.dispose();
34392
+ dispose(v);
33892
34393
  views.delete(partId);
33893
34394
  },
33894
- /** LOD 0 draws the courses; LOD 1 and 2 give the wall its box back. */
34395
+ /** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
33895
34396
  setLod(level) {
33896
34397
  const was = lod;
33897
34398
  lod = level;
33898
34399
  if (was === 0 === (level === 0)) return;
33899
34400
  for (const v of views.values()) {
33900
- v.mesh.visible = level === 0;
34401
+ v.units.visible = level === 0;
34402
+ for (const m of v.shells) m.visible = level === 0;
33901
34403
  plain2(v, level !== 0);
33902
34404
  }
33903
34405
  invalidate();
@@ -33906,17 +34408,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33906
34408
  /** Everything back to what it was (after `world.reset`). */
33907
34409
  restore() {
33908
34410
  for (const v of views.values()) {
33909
- v.mesh.removeFromParent();
33910
- v.mesh.dispose();
34411
+ dispose(v);
33911
34412
  if (lod === 0) plain2(v, true);
33912
34413
  }
33913
34414
  views.clear();
33914
34415
  },
33915
- /** The wall's opening, if it has courses. */
34416
+ /** The wall has damage drawn. */
33916
34417
  has(partId) {
33917
34418
  return views.has(partId);
33918
34419
  },
33919
- statistics: () => ({ walls: views.size, units: [...views.values()].reduce((n, v) => n + v.units, 0) })
34420
+ /** Walls with damage drawn, units out of them, and units laid bare at the edge of the cuts. */
34421
+ statistics: () => ({
34422
+ walls: views.size,
34423
+ gone: [...views.values()].reduce((n, v) => n + v.gone, 0),
34424
+ units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
34425
+ })
33920
34426
  };
33921
34427
  }
33922
34428
  Object.assign(K, { createBreachView });
@@ -34916,6 +35422,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34916
35422
  return buildApi(K, shared);
34917
35423
  }
34918
35424
  var WOOD_THIN = 0.035;
35425
+ var BOARD_TOP_THICK = 0.07;
35426
+ var BOARD_TOP_WIDTH = 0.5;
35427
+ var BOARD_WIDTH = 0.16;
34919
35428
  var SHELF_SPAN = 1.5;
34920
35429
  var SHELVING = /shelf|bookcase|rack/;
34921
35430
  function carcass(parts, drawn) {
@@ -34930,6 +35439,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34930
35439
  } else if (y <= x && y <= z) part.structure = { role: "beam", maxSpan: SHELF_SPAN * Math.max(x, z) };
34931
35440
  }
34932
35441
  }
35442
+ function splitTopsIntoBoards(drawn, materialOf2, carcassed) {
35443
+ const tops = /* @__PURE__ */ new Map();
35444
+ if (carcassed) return tops;
35445
+ for (let i = drawn.length - 1; i >= 0; i--) {
35446
+ const p = drawn[i];
35447
+ if (p.key !== "wood" || materialOf2.wood !== "wood_hard" || p.shape.kind !== "box" || p.rotation) continue;
35448
+ const [sx, sy, sz] = p.shape.size;
35449
+ const across2 = sx < sz ? 0 : 2;
35450
+ const width = Math.min(sx, sz);
35451
+ if (sy < WOOD_THIN || sy > BOARD_TOP_THICK || sy >= width || width < BOARD_TOP_WIDTH) continue;
35452
+ const count = Math.round(width / BOARD_WIDTH);
35453
+ const boards = [];
35454
+ for (let b = 0; b < count; b++) {
35455
+ const size = [sx, sy, sz];
35456
+ size[across2] = width / count;
35457
+ const position = [...p.position];
35458
+ position[across2] += (b + 0.5) * size[across2] - width / 2;
35459
+ const min2 = [...p.min];
35460
+ const max2 = [...p.max];
35461
+ min2[across2] = position[across2] - size[across2] / 2;
35462
+ max2[across2] = position[across2] + size[across2] / 2;
35463
+ boards.push({ ...p, id: `${p.id}_b${b}`, shape: { kind: "box", size }, position, min: min2, max: max2, color: shadeBoard(p.color, b) });
35464
+ }
35465
+ drawn.splice(i, 1, ...boards);
35466
+ tops.set(p.id, boards);
35467
+ }
35468
+ return tops;
35469
+ }
35470
+ function shadeBoard(color2, board) {
35471
+ const k = [1, 0.94, 1.05, 0.97, 1.03][board % 5];
35472
+ const channel = (shift) => Math.min(255, Math.round((color2 >> shift & 255) * k)) << shift;
35473
+ return channel(16) | channel(8) | channel(0);
35474
+ }
34933
35475
  var FLOOR_TOUCH = 5e-3;
34934
35476
  var LOOSE = /* @__PURE__ */ new Set(["paper"]);
34935
35477
  var BASE_MATERIAL = {
@@ -35007,10 +35549,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
35007
35549
  };
35008
35550
  run(A);
35009
35551
  if (!drawn.length) throw new Error(`game-floorplan-three: the builder of "${name}" drew no box or cylinder`);
35552
+ const boardsOf = splitTopsIntoBoards(drawn, materialOf2, Boolean(opts.carcass));
35010
35553
  const parts = drawn.map((p) => {
35011
35554
  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]);
35012
35555
  let material = materialOf2[p.key] ?? "plastic";
35013
- if (p.key === "wood" && material === "wood_hard" && thin < WOOD_THIN) material = "wood_soft";
35556
+ if (p.key === "wood" && material === "wood_hard" && opts.carcass && !opts.materials?.wood) material = "mdf";
35557
+ else if (p.key === "wood" && material === "wood_hard" && thin < WOOD_THIN) material = "wood_soft";
35014
35558
  const part = { id: p.id, material, shape: p.shape, position: p.position };
35015
35559
  if (p.rotation) part.rotation = p.rotation;
35016
35560
  if (!LOOSE.has(material) && p.min[1] <= FLOOR_TOUCH) part.anchored = true;
@@ -35045,6 +35589,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
35045
35589
  if (area3 < 0.6 * footprint || p.max[1] < 0.2) continue;
35046
35590
  if (!top || p.max[1] > top.y + 1e-6) top = { part: p.id, y: p.max[1] };
35047
35591
  }
35592
+ for (const boards of boardsOf.values()) {
35593
+ if (top && boards[0].max[1] <= top.y - 1e-6) continue;
35594
+ const mid = boards[boards.length >> 1];
35595
+ const min2 = [Math.min(...boards.map((b) => b.min[0])), Math.min(...boards.map((b) => b.min[2]))];
35596
+ const max2 = [Math.max(...boards.map((b) => b.max[0])), Math.max(...boards.map((b) => b.max[2]))];
35597
+ 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) };
35598
+ }
35048
35599
  if (opts.carcass) carcass(parts, byId);
35049
35600
  const spec = { name, parts, joints, tags: ["furniture"] };
35050
35601
  const colors = new Map(drawn.map((p) => [p.id, p.color]));
@@ -38674,8 +39225,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38674
39225
  bedHeight = roadY * 1e3 + z * 14 - (x - params.roadSlopeAnchorX) * 5;
38675
39226
  roughness = 0.55;
38676
39227
  }
38677
- const noise2 = valueNoise(column * 0.35, row * 0.35) * 0.6 + valueNoise(column * 1.3, row * 1.3) * 0.4;
38678
- bed[index] = bedHeight + (noise2 - 0.5) * roughness * 2;
39228
+ const noise3 = valueNoise(column * 0.35, row * 0.35) * 0.6 + valueNoise(column * 1.3, row * 1.3) * 0.4;
39229
+ bed[index] = bedHeight + (noise3 - 0.5) * roughness * 2;
38679
39230
  vertical[index] = region === 0 || region === 2 ? 0.95 : 1;
38680
39231
  pin[index] = 0.62 + 0.76 * (valueNoise(column * 0.21 + 40, row * 0.21) * 0.5 + hashCell(column, row) * 0.5) * (region === 3 ? 1.25 : 1);
38681
39232
  }
@@ -39099,14 +39650,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39099
39650
  },
39100
39651
  tessellate: {
39101
39652
  cylinder: {
39102
- heightSegments: 6,
39103
- radialSegments: 20
39653
+ heightSegments: 24,
39654
+ radialSegments: 40,
39655
+ capRings: 6
39104
39656
  },
39105
39657
  box: {
39106
39658
  widthSegments: 6,
39107
39659
  heightSegments: 1,
39108
39660
  depthSegments: 4
39109
39661
  }
39662
+ },
39663
+ wall: {
39664
+ body: 1e-4,
39665
+ ends: 25e-5
39110
39666
  }
39111
39667
  }
39112
39668
  },
@@ -39130,14 +39686,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39130
39686
  },
39131
39687
  tessellate: {
39132
39688
  cylinder: {
39133
- heightSegments: 6,
39134
- radialSegments: 20
39689
+ heightSegments: 24,
39690
+ radialSegments: 40,
39691
+ capRings: 6
39135
39692
  },
39136
39693
  box: {
39137
39694
  widthSegments: 6,
39138
39695
  heightSegments: 1,
39139
39696
  depthSegments: 4
39140
39697
  }
39698
+ },
39699
+ wall: {
39700
+ body: 18e-5,
39701
+ ends: 22e-5
39141
39702
  }
39142
39703
  }
39143
39704
  }
@@ -39227,12 +39788,53 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39227
39788
  if (!box22) return 0;
39228
39789
  return (box22.max.x - box22.min.x) * (box22.max.y - box22.min.y) * (box22.max.z - box22.min.z);
39229
39790
  }
39230
- function tessellated(geometry, rule) {
39791
+ function ringedCylinder(T2, p, radial, maxRows, rings) {
39792
+ const half = p.height / 2;
39793
+ const column = 2 * Math.PI * Math.max(p.radiusTop, p.radiusBottom, 1e-6) / radial;
39794
+ const rows = Math.max(1, Math.min(maxRows, Math.round(p.height / column)));
39795
+ const points = [];
39796
+ if (!p.openEnded) for (let i = 0; i <= rings; i++) points.push(new T2.Vector2(p.radiusBottom * i / rings, -half));
39797
+ 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));
39798
+ if (!p.openEnded) for (let i = rings; i >= 0; i--) points.push(new T2.Vector2(p.radiusTop * i / rings, half));
39799
+ return lathed(T2, points, radial, p.thetaStart, p.thetaLength);
39800
+ }
39801
+ function lathed(T2, points, radial, phiStart, phiLength) {
39802
+ const geometry = new T2.LatheGeometry([...points], radial, phiStart, phiLength);
39803
+ geometry.computeVertexNormals();
39804
+ const position = geometry.getAttribute("position");
39805
+ const normal = geometry.getAttribute("normal");
39806
+ const middle = (Math.min(...points.map((point) => point.y)) + Math.max(...points.map((point) => point.y))) / 2;
39807
+ for (let i = 0; i < normal.count; i++) {
39808
+ if (Math.hypot(normal.getX(i), normal.getY(i), normal.getZ(i)) > 0.5) continue;
39809
+ normal.setXYZ(i, 0, position.getY(i) >= middle ? 1 : -1, 0);
39810
+ }
39811
+ return geometry;
39812
+ }
39813
+ function finerProfile(T2, points, step) {
39814
+ const out = [];
39815
+ points.forEach((point, index) => {
39816
+ const next = points[index + 1];
39817
+ out.push(point.clone());
39818
+ if (!next) return;
39819
+ const pieces = Math.max(1, Math.round(point.distanceTo(next) / step));
39820
+ for (let k = 1; k < pieces; k++) out.push(point.clone().lerp(next, k / pieces));
39821
+ });
39822
+ return out;
39823
+ }
39824
+ function tessellated(geometry, rule, T2) {
39231
39825
  const Shape = geometry.constructor;
39232
39826
  if (geometry.type === "CylinderGeometry" && rule.cylinder) {
39233
- const { radiusTop, radiusBottom, height, openEnded, thetaStart, thetaLength } = geometry.parameters;
39827
+ const parameters = geometry.parameters;
39828
+ const { radiusTop, radiusBottom, height, openEnded, thetaStart, thetaLength } = parameters;
39829
+ if (T2 && rule.cylinder.capRings !== void 0) return ringedCylinder(T2, parameters, rule.cylinder.radialSegments, rule.cylinder.heightSegments, rule.cylinder.capRings);
39234
39830
  return new Shape(radiusTop, radiusBottom, height, rule.cylinder.radialSegments, rule.cylinder.heightSegments, openEnded, thetaStart, thetaLength);
39235
39831
  }
39832
+ if (geometry.type === "LatheGeometry" && rule.cylinder && T2 && rule.cylinder.capRings !== void 0) {
39833
+ const { points, segments, phiStart, phiLength } = geometry.parameters;
39834
+ if (segments >= rule.cylinder.radialSegments) return void 0;
39835
+ const widest = Math.max(...points.map((point) => point.x), 1e-6);
39836
+ return lathed(T2, finerProfile(T2, points, 2 * Math.PI * widest / rule.cylinder.radialSegments), rule.cylinder.radialSegments, phiStart, phiLength);
39837
+ }
39236
39838
  if (geometry.type === "BoxGeometry" && rule.box) {
39237
39839
  const { width, height, depth } = geometry.parameters;
39238
39840
  return new Shape(width, height, depth, rule.box.widthSegments, rule.box.heightSegments, rule.box.depthSegments);
@@ -39240,14 +39842,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39240
39842
  return void 0;
39241
39843
  }
39242
39844
  var triangleCount = (geometry) => (geometry.index ? geometry.index.count : geometry.getAttribute("position").count) / 3;
39243
- function tessellateShapes(root, rule) {
39845
+ function tessellateShapes(root, rule, T2) {
39244
39846
  const rebuilt = /* @__PURE__ */ new Map();
39245
39847
  root.traverse((object2) => {
39246
39848
  const mesh3 = object2;
39247
39849
  if (!mesh3.isMesh) return;
39248
39850
  let fresh = rebuilt.get(mesh3.geometry);
39249
39851
  if (!fresh) {
39250
- fresh = tessellated(mesh3.geometry, rule);
39852
+ fresh = tessellated(mesh3.geometry, rule, T2);
39251
39853
  if (!fresh) return;
39252
39854
  rebuilt.set(mesh3.geometry, fresh);
39253
39855
  }
@@ -39261,10 +39863,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39261
39863
  root.userData.tessellationTriangles = added;
39262
39864
  }
39263
39865
  function tagForDestruction(root, options = {}) {
39264
- const { modelId, weight, size } = options;
39866
+ const { modelId, weight, size, three } = options;
39265
39867
  if (weight !== void 0 && weight > 0) root.userData.destructionMass = weight;
39266
39868
  const rule = modelId === void 0 ? void 0 : itemDestruction(modelId);
39267
- if (rule?.tessellate) tessellateShapes(root, rule.tessellate);
39869
+ if (rule?.tessellate) tessellateShapes(root, rule.tessellate, three);
39268
39870
  const solid2 = [];
39269
39871
  root.traverse((object2) => {
39270
39872
  const mesh3 = object2;
@@ -39284,6 +39886,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39284
39886
  const anchor = solid2.reduce((largest, mesh3) => volumeOf(mesh3) > volumeOf(largest) ? mesh3 : largest);
39285
39887
  solid2.forEach((mesh3, index) => {
39286
39888
  mesh3.userData.destructionPart = mesh3 === anchor ? { id: SHELL_PART_ID } : { id: `shell_${index}`, parent: SHELL_PART_ID, kind: "weld" };
39889
+ if (rule.wall) mesh3.userData.destructionThickness = mesh3 === anchor ? rule.wall.body : rule.wall.ends;
39287
39890
  });
39288
39891
  const contents = size === void 0 ? void 0 : itemContents({ ...modelId === void 0 ? {} : { model: modelId }, size, ...weight === void 0 ? {} : { weight } });
39289
39892
  if (contents) anchor.userData.destructionContains = { fluid: contents.liquid, liters: contents.liters };
@@ -44895,7 +45498,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
44895
45498
  const materials = kit.materials.slice(first);
44896
45499
  if (options.wear) setWear(materials, options.wear);
44897
45500
  root.userData.wear = options.wear ?? 0;
44898
- if (options.destructible) tagForDestruction(root, { ...kind === "model" && item.model !== void 0 ? { modelId: item.model } : {}, ...item.weight === void 0 ? {} : { weight: item.weight }, size: item.size });
45501
+ 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 });
44899
45502
  markKitOrigin(root, { owner: "game-items-three", kind: "item", id: item.id, ...kind === "shape" ? { standIn: true } : {} });
44900
45503
  return { root, kind, ...shape ? { shape } : {}, size: item.size, materials };
44901
45504
  }
@@ -55735,6 +56338,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
55735
56338
  for (const decal2 of this.decals) decal2.on = false;
55736
56339
  this.count = 0;
55737
56340
  }
56341
+ /**
56342
+ * Takes away the marks inside a box: world `center`, `half` extents along the axes of the world `rotation` (a quaternion, x y z w; none means the
56343
+ * world axes). The surface they were on is gone (a brick came out of a wall with its bullet holes). Returns how many went.
56344
+ */
56345
+ clearInside(center, half, rotation = [0, 0, 0, 1]) {
56346
+ const qx = -rotation[0], qy = -rotation[1], qz = -rotation[2], qw = rotation[3];
56347
+ let gone = 0;
56348
+ for (const decal2 of this.decals) {
56349
+ if (!decal2.on) continue;
56350
+ const vx2 = decal2.x - center[0], vy2 = decal2.y - center[1], vz2 = decal2.z - center[2];
56351
+ const tx = 2 * (qy * vz2 - qz * vy2), ty = 2 * (qz * vx2 - qx * vz2), tz = 2 * (qx * vy2 - qy * vx2);
56352
+ const lx = vx2 + qw * tx + (qy * tz - qz * ty), ly = vy2 + qw * ty + (qz * tx - qx * tz), lz = vz2 + qw * tz + (qx * ty - qy * tx);
56353
+ if (Math.abs(lx) > half[0] || Math.abs(ly) > half[1] || Math.abs(lz) > half[2]) continue;
56354
+ decal2.on = false;
56355
+ gone++;
56356
+ }
56357
+ return gone;
56358
+ }
55738
56359
  update(dt) {
55739
56360
  this.count = 0;
55740
56361
  for (const decal2 of this.decals) {
@@ -60375,6 +60996,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
60375
60996
  return {
60376
60997
  id: "weapons_area",
60377
60998
  name: text.name,
60999
+ ground: { surface: "concrete_slab" },
60378
61000
  group: text.group,
60379
61001
  get audio() {
60380
61002
  return audio;
@@ -61437,6 +62059,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
61437
62059
  return {
61438
62060
  id: EXPLOSIVES_AREA_ID,
61439
62061
  name: text.name,
62062
+ ground: { surface: "concrete_slab" },
61440
62063
  group: text.group,
61441
62064
  bench,
61442
62065
  // 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