@adatechnology/game-items-three 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2761,14 +2761,19 @@ var GameItemsThree = (() => {
2761
2761
  },
2762
2762
  tessellate: {
2763
2763
  cylinder: {
2764
- heightSegments: 6,
2765
- radialSegments: 20
2764
+ heightSegments: 24,
2765
+ radialSegments: 40,
2766
+ capRings: 6
2766
2767
  },
2767
2768
  box: {
2768
2769
  widthSegments: 6,
2769
2770
  heightSegments: 1,
2770
2771
  depthSegments: 4
2771
2772
  }
2773
+ },
2774
+ wall: {
2775
+ body: 1e-4,
2776
+ ends: 25e-5
2772
2777
  }
2773
2778
  }
2774
2779
  },
@@ -2792,14 +2797,19 @@ var GameItemsThree = (() => {
2792
2797
  },
2793
2798
  tessellate: {
2794
2799
  cylinder: {
2795
- heightSegments: 6,
2796
- radialSegments: 20
2800
+ heightSegments: 24,
2801
+ radialSegments: 40,
2802
+ capRings: 6
2797
2803
  },
2798
2804
  box: {
2799
2805
  widthSegments: 6,
2800
2806
  heightSegments: 1,
2801
2807
  depthSegments: 4
2802
2808
  }
2809
+ },
2810
+ wall: {
2811
+ body: 18e-5,
2812
+ ends: 22e-5
2803
2813
  }
2804
2814
  }
2805
2815
  }
@@ -2893,12 +2903,53 @@ var GameItemsThree = (() => {
2893
2903
  if (!box4) return 0;
2894
2904
  return (box4.max.x - box4.min.x) * (box4.max.y - box4.min.y) * (box4.max.z - box4.min.z);
2895
2905
  }
2896
- function tessellated(geometry, rule) {
2906
+ function ringedCylinder(T2, p, radial, maxRows, rings) {
2907
+ const half = p.height / 2;
2908
+ const column = 2 * Math.PI * Math.max(p.radiusTop, p.radiusBottom, 1e-6) / radial;
2909
+ const rows = Math.max(1, Math.min(maxRows, Math.round(p.height / column)));
2910
+ const points = [];
2911
+ if (!p.openEnded) for (let i = 0; i <= rings; i++) points.push(new T2.Vector2(p.radiusBottom * i / rings, -half));
2912
+ 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));
2913
+ if (!p.openEnded) for (let i = rings; i >= 0; i--) points.push(new T2.Vector2(p.radiusTop * i / rings, half));
2914
+ return lathed(T2, points, radial, p.thetaStart, p.thetaLength);
2915
+ }
2916
+ function lathed(T2, points, radial, phiStart, phiLength) {
2917
+ const geometry = new T2.LatheGeometry([...points], radial, phiStart, phiLength);
2918
+ geometry.computeVertexNormals();
2919
+ const position = geometry.getAttribute("position");
2920
+ const normal = geometry.getAttribute("normal");
2921
+ const middle = (Math.min(...points.map((point) => point.y)) + Math.max(...points.map((point) => point.y))) / 2;
2922
+ for (let i = 0; i < normal.count; i++) {
2923
+ if (Math.hypot(normal.getX(i), normal.getY(i), normal.getZ(i)) > 0.5) continue;
2924
+ normal.setXYZ(i, 0, position.getY(i) >= middle ? 1 : -1, 0);
2925
+ }
2926
+ return geometry;
2927
+ }
2928
+ function finerProfile(T2, points, step) {
2929
+ const out = [];
2930
+ points.forEach((point, index) => {
2931
+ const next = points[index + 1];
2932
+ out.push(point.clone());
2933
+ if (!next) return;
2934
+ const pieces = Math.max(1, Math.round(point.distanceTo(next) / step));
2935
+ for (let k = 1; k < pieces; k++) out.push(point.clone().lerp(next, k / pieces));
2936
+ });
2937
+ return out;
2938
+ }
2939
+ function tessellated(geometry, rule, T2) {
2897
2940
  const Shape = geometry.constructor;
2898
2941
  if (geometry.type === "CylinderGeometry" && rule.cylinder) {
2899
- const { radiusTop, radiusBottom, height, openEnded, thetaStart, thetaLength } = geometry.parameters;
2942
+ const parameters = geometry.parameters;
2943
+ const { radiusTop, radiusBottom, height, openEnded, thetaStart, thetaLength } = parameters;
2944
+ if (T2 && rule.cylinder.capRings !== void 0) return ringedCylinder(T2, parameters, rule.cylinder.radialSegments, rule.cylinder.heightSegments, rule.cylinder.capRings);
2900
2945
  return new Shape(radiusTop, radiusBottom, height, rule.cylinder.radialSegments, rule.cylinder.heightSegments, openEnded, thetaStart, thetaLength);
2901
2946
  }
2947
+ if (geometry.type === "LatheGeometry" && rule.cylinder && T2 && rule.cylinder.capRings !== void 0) {
2948
+ const { points, segments, phiStart, phiLength } = geometry.parameters;
2949
+ if (segments >= rule.cylinder.radialSegments) return void 0;
2950
+ const widest = Math.max(...points.map((point) => point.x), 1e-6);
2951
+ return lathed(T2, finerProfile(T2, points, 2 * Math.PI * widest / rule.cylinder.radialSegments), rule.cylinder.radialSegments, phiStart, phiLength);
2952
+ }
2902
2953
  if (geometry.type === "BoxGeometry" && rule.box) {
2903
2954
  const { width, height, depth } = geometry.parameters;
2904
2955
  return new Shape(width, height, depth, rule.box.widthSegments, rule.box.heightSegments, rule.box.depthSegments);
@@ -2906,14 +2957,14 @@ var GameItemsThree = (() => {
2906
2957
  return void 0;
2907
2958
  }
2908
2959
  var triangleCount = (geometry) => (geometry.index ? geometry.index.count : geometry.getAttribute("position").count) / 3;
2909
- function tessellateShapes(root, rule) {
2960
+ function tessellateShapes(root, rule, T2) {
2910
2961
  const rebuilt = /* @__PURE__ */ new Map();
2911
2962
  root.traverse((object2) => {
2912
2963
  const mesh2 = object2;
2913
2964
  if (!mesh2.isMesh) return;
2914
2965
  let fresh = rebuilt.get(mesh2.geometry);
2915
2966
  if (!fresh) {
2916
- fresh = tessellated(mesh2.geometry, rule);
2967
+ fresh = tessellated(mesh2.geometry, rule, T2);
2917
2968
  if (!fresh) return;
2918
2969
  rebuilt.set(mesh2.geometry, fresh);
2919
2970
  }
@@ -2927,10 +2978,10 @@ var GameItemsThree = (() => {
2927
2978
  root.userData.tessellationTriangles = added;
2928
2979
  }
2929
2980
  function tagForDestruction(root, options = {}) {
2930
- const { modelId, weight, size } = options;
2981
+ const { modelId, weight, size, three } = options;
2931
2982
  if (weight !== void 0 && weight > 0) root.userData.destructionMass = weight;
2932
2983
  const rule = modelId === void 0 ? void 0 : itemDestruction(modelId);
2933
- if (rule?.tessellate) tessellateShapes(root, rule.tessellate);
2984
+ if (rule?.tessellate) tessellateShapes(root, rule.tessellate, three);
2934
2985
  const solid = [];
2935
2986
  root.traverse((object2) => {
2936
2987
  const mesh2 = object2;
@@ -2950,6 +3001,7 @@ var GameItemsThree = (() => {
2950
3001
  const anchor = solid.reduce((largest, mesh2) => volumeOf(mesh2) > volumeOf(largest) ? mesh2 : largest);
2951
3002
  solid.forEach((mesh2, index) => {
2952
3003
  mesh2.userData.destructionPart = mesh2 === anchor ? { id: SHELL_PART_ID } : { id: `shell_${index}`, parent: SHELL_PART_ID, kind: "weld" };
3004
+ if (rule.wall) mesh2.userData.destructionThickness = mesh2 === anchor ? rule.wall.body : rule.wall.ends;
2953
3005
  });
2954
3006
  const contents = size === void 0 ? void 0 : itemContents({ ...modelId === void 0 ? {} : { model: modelId }, size, ...weight === void 0 ? {} : { weight } });
2955
3007
  if (contents) anchor.userData.destructionContains = { fluid: contents.liquid, liters: contents.liters };
@@ -9070,7 +9122,7 @@ var GameItemsThree = (() => {
9070
9122
  const materials = kit.materials.slice(first);
9071
9123
  if (options.wear) setWear(materials, options.wear);
9072
9124
  root.userData.wear = options.wear ?? 0;
9073
- if (options.destructible) tagForDestruction(root, { ...kind === "model" && item.model !== void 0 ? { modelId: item.model } : {}, ...item.weight === void 0 ? {} : { weight: item.weight }, size: item.size });
9125
+ 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 });
9074
9126
  markKitOrigin(root, { owner: "game-items-three", kind: "item", id: item.id, ...kind === "shape" ? { standIn: true } : {} });
9075
9127
  return { root, kind, ...shape ? { shape } : {}, size: item.size, materials };
9076
9128
  }
@@ -12730,7 +12782,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12730
12782
  const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
12731
12783
  band.c[L] = (lo + hi) / 2;
12732
12784
  band.s[L] = Math.max(hi - lo, 0.01);
12733
- for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
12785
+ if (inp.style === "snap") {
12786
+ 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] });
12787
+ } else for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
12734
12788
  return out;
12735
12789
  };
12736
12790
  var tearStack = (inp, root, t, u, v, n, sheet) => {
@@ -12761,6 +12815,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12761
12815
  const t = sorted[0];
12762
12816
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
12763
12817
  if (style === "snap") {
12818
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
12764
12819
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
12765
12820
  for (const b of bsp(root, n2, [0, 1, 2], rnd)) out.push({ center: b.c, size: b.s, q: idq, shape: "slab", weight: b.s[0] * b.s[1] * b.s[2] });
12766
12821
  return out;
@@ -12934,6 +12989,27 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12934
12989
  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]]));
12935
12990
  return out;
12936
12991
  };
12992
+ var clipBarsToBox = (bars, u, v, center, size) => {
12993
+ const out = [];
12994
+ for (const [a, b] of bars) {
12995
+ const axis = a[u] !== b[u] ? u : v;
12996
+ const other = axis === u ? v : u;
12997
+ if (Math.abs(a[other] - center[other]) > size[other] / 2) continue;
12998
+ const lo = Math.max(Math.min(a[axis], b[axis]), center[axis] - size[axis] / 2), hi = Math.min(Math.max(a[axis], b[axis]), center[axis] + size[axis] / 2);
12999
+ if (hi - lo < 1e-6) continue;
13000
+ const p = [a[0], a[1], a[2]], q = [b[0], b[1], b[2]];
13001
+ p[axis] = lo;
13002
+ q[axis] = hi;
13003
+ out.push([p, q]);
13004
+ }
13005
+ return out;
13006
+ };
13007
+ var bendBar = ([a, b], thin, bow, i) => {
13008
+ const length = Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
13009
+ const m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2];
13010
+ m[thin] += (i % 2 === 0 ? 1 : -1) * bow * length;
13011
+ return [[a, m], [m, b]];
13012
+ };
12937
13013
  var clipBarsToDisc = (bars, u, v, center, radius) => {
12938
13014
  const out = [];
12939
13015
  for (const [a, b] of bars) {
@@ -13854,14 +13930,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13854
13930
  smoke: "grey"
13855
13931
  },
13856
13932
  visual: {
13857
- charColor: 1709072
13933
+ charColor: 1709072,
13934
+ innerColor: 14730131
13858
13935
  },
13859
13936
  tags: [
13860
13937
  "wood",
13861
13938
  "organic",
13862
13939
  "flammable"
13863
13940
  ],
13864
- compressive: 1e5
13941
+ compressive: 1e5,
13942
+ hits: {
13943
+ blunt: {
13944
+ tear: "splinters",
13945
+ tearJoules: 60
13946
+ }
13947
+ }
13865
13948
  },
13866
13949
  {
13867
13950
  id: "wood_hard",
@@ -13891,14 +13974,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13891
13974
  smoke: "grey"
13892
13975
  },
13893
13976
  visual: {
13894
- charColor: 1709072
13977
+ charColor: 1709072,
13978
+ innerColor: 13213802
13895
13979
  },
13896
13980
  tags: [
13897
13981
  "wood",
13898
13982
  "organic",
13899
13983
  "flammable"
13900
13984
  ],
13901
- compressive: 15e4
13985
+ compressive: 15e4,
13986
+ hits: {
13987
+ blunt: {
13988
+ tear: "splinters",
13989
+ tearJoules: 150
13990
+ }
13991
+ }
13902
13992
  },
13903
13993
  {
13904
13994
  id: "plywood",
@@ -13928,14 +14018,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13928
14018
  smoke: "grey"
13929
14019
  },
13930
14020
  visual: {
13931
- charColor: 1709072
14021
+ charColor: 1709072,
14022
+ innerColor: 14862234
13932
14023
  },
13933
14024
  tags: [
13934
14025
  "wood",
13935
14026
  "organic",
13936
14027
  "flammable"
13937
14028
  ],
13938
- compressive: 6e4
14029
+ compressive: 6e4,
14030
+ hits: {
14031
+ ballistic: {
14032
+ tear: "splinters"
14033
+ },
14034
+ blunt: {
14035
+ tear: "splinters",
14036
+ tearJoules: 80
14037
+ }
14038
+ }
13939
14039
  },
13940
14040
  {
13941
14041
  id: "mdf",
@@ -13962,14 +14062,25 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13962
14062
  smoke: "grey"
13963
14063
  },
13964
14064
  visual: {
13965
- charColor: 1709072
14065
+ charColor: 1709072,
14066
+ innerColor: 12096362
13966
14067
  },
13967
14068
  tags: [
13968
14069
  "wood",
13969
14070
  "organic",
13970
14071
  "flammable"
13971
14072
  ],
13972
- compressive: 3e4
14073
+ compressive: 3e4,
14074
+ hits: {
14075
+ ballistic: {
14076
+ tear: "lumps",
14077
+ tearJoules: 120
14078
+ },
14079
+ blunt: {
14080
+ tear: "lumps",
14081
+ tearJoules: 20
14082
+ }
14083
+ }
13973
14084
  },
13974
14085
  {
13975
14086
  id: "glass",
@@ -14083,6 +14194,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14083
14194
  damageScale: {
14084
14195
  explosive: 1.2
14085
14196
  },
14197
+ hits: {
14198
+ ballistic: {
14199
+ unitJoules: 500,
14200
+ floor: 900,
14201
+ crater: true
14202
+ },
14203
+ blunt: {
14204
+ unitJoules: 1500,
14205
+ floor: 60
14206
+ }
14207
+ },
14086
14208
  hitDebris: "chips",
14087
14209
  breakDebris: "dust",
14088
14210
  thermal: {
@@ -14451,7 +14573,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14451
14573
  smoke: "white"
14452
14574
  },
14453
14575
  visual: {
14454
- charColor: 1709072
14576
+ charColor: 1709072,
14577
+ innerColor: 14207126
14455
14578
  },
14456
14579
  tags: [
14457
14580
  "plant",
@@ -14459,7 +14582,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14459
14582
  "organic",
14460
14583
  "flammable"
14461
14584
  ],
14462
- compressive: 6e4
14585
+ compressive: 6e4,
14586
+ hits: {
14587
+ blunt: {
14588
+ tear: "none"
14589
+ }
14590
+ }
14463
14591
  },
14464
14592
  {
14465
14593
  id: "cactus",
@@ -14574,6 +14702,130 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14574
14702
  "masonry"
14575
14703
  ],
14576
14704
  compressive: 2e4
14705
+ },
14706
+ {
14707
+ id: "asphalt",
14708
+ density: 2350,
14709
+ hpPerKg: 10,
14710
+ penetration: 14,
14711
+ penetrationClass: "concrete",
14712
+ penetrationScale: 0.8,
14713
+ ductility: 0.1,
14714
+ fracture: "crumble",
14715
+ fragmentSize: [
14716
+ 0.03,
14717
+ 0.15
14718
+ ],
14719
+ hitDebris: "chips",
14720
+ breakDebris: "dust",
14721
+ thermal: {
14722
+ conductivity: 0.1,
14723
+ heatCapacity: 2,
14724
+ ignitionC: 400,
14725
+ burnRate: 2e-3,
14726
+ smoke: "black"
14727
+ },
14728
+ tags: [
14729
+ "masonry",
14730
+ "brittle"
14731
+ ],
14732
+ compressive: 3e4
14733
+ },
14734
+ {
14735
+ id: "sand",
14736
+ density: 1600,
14737
+ hpPerKg: 2,
14738
+ penetration: 6,
14739
+ ductility: 0.4,
14740
+ fracture: "crumble",
14741
+ fragmentSize: [
14742
+ 0.01,
14743
+ 0.04
14744
+ ],
14745
+ hitDebris: "dust",
14746
+ breakDebris: "dust",
14747
+ thermal: {
14748
+ conductivity: 0.1,
14749
+ heatCapacity: 2
14750
+ },
14751
+ tags: [
14752
+ "soil"
14753
+ ],
14754
+ compressive: 1e4
14755
+ },
14756
+ {
14757
+ id: "gravel",
14758
+ density: 1800,
14759
+ hpPerKg: 3,
14760
+ penetration: 8,
14761
+ ductility: 0.3,
14762
+ fracture: "crumble",
14763
+ fragmentSize: [
14764
+ 0.02,
14765
+ 0.06
14766
+ ],
14767
+ hitDebris: "chips",
14768
+ breakDebris: "dust",
14769
+ thermal: {
14770
+ conductivity: 0.1,
14771
+ heatCapacity: 2
14772
+ },
14773
+ tags: [
14774
+ "soil",
14775
+ "masonry"
14776
+ ],
14777
+ compressive: 2e4
14778
+ },
14779
+ {
14780
+ id: "turf",
14781
+ density: 900,
14782
+ hpPerKg: 3,
14783
+ penetration: 4,
14784
+ ductility: 0.5,
14785
+ fracture: "crumble",
14786
+ fragmentSize: [
14787
+ 0.03,
14788
+ 0.1
14789
+ ],
14790
+ hitDebris: "dust",
14791
+ breakDebris: "dust",
14792
+ thermal: {
14793
+ conductivity: 0.1,
14794
+ heatCapacity: 1.5,
14795
+ ignitionC: 250,
14796
+ burnRate: 0.03,
14797
+ smoke: "white"
14798
+ },
14799
+ tags: [
14800
+ "soil",
14801
+ "organic"
14802
+ ],
14803
+ compressive: 15e3
14804
+ },
14805
+ {
14806
+ id: "mortar",
14807
+ density: 1900,
14808
+ hpPerKg: 10,
14809
+ penetration: 12,
14810
+ penetrationClass: "concrete",
14811
+ penetrationScale: 1.2,
14812
+ ductility: 0.03,
14813
+ fracture: "crumble",
14814
+ fragmentSize: [
14815
+ 0.02,
14816
+ 0.08
14817
+ ],
14818
+ hitDebris: "chips",
14819
+ breakDebris: "dust",
14820
+ thermal: {
14821
+ conductivity: 0.15,
14822
+ heatCapacity: 2.5
14823
+ },
14824
+ tags: [
14825
+ "masonry",
14826
+ "brittle"
14827
+ ],
14828
+ compressive: 1e5
14577
14829
  }
14578
14830
  ];
14579
14831
  var material_hints_default = {
@@ -14747,6 +14999,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14747
14999
  return (1 + 4 * (0.3 - ductility) / 0.3) * Math.min(25, Math.max(1, (0.02 / Math.max(thickness, 1e-4)) ** 2));
14748
15000
  };
14749
15001
  var THIN_SHEET_DISHING = 5e-3;
15002
+ var SWEEP_BACK = 0.01;
15003
+ function sweepOrigins(box4, d, spacing) {
15004
+ const origins = [];
15005
+ const line = (from, to) => {
15006
+ const n = Math.max(1, Math.ceil((to - from) / spacing));
15007
+ return Array.from({ length: n + 1 }, (_, i) => from + (to - from) * i / n);
15008
+ };
15009
+ for (const axis of [0, 1, 2]) {
15010
+ if (Math.abs(d[axis]) < 1e-3) continue;
15011
+ const lead = d[axis] > 0 ? box4.max[axis] - SWEEP_BACK : box4.min[axis] + SWEEP_BACK;
15012
+ const u = (axis + 1) % 3;
15013
+ const v = (axis + 2) % 3;
15014
+ for (const a of line(box4.min[u], box4.max[u])) {
15015
+ for (const b of line(box4.min[v], box4.max[v])) {
15016
+ const point = [0, 0, 0];
15017
+ point[axis] = lead;
15018
+ point[u] = a;
15019
+ point[v] = b;
15020
+ origins.push(point);
15021
+ }
15022
+ }
15023
+ }
15024
+ return origins;
15025
+ }
15026
+ var THIN_SHEET_CRUSH = 5e-4;
15027
+ var THIN_SHEET_CONTACT = 0.025;
14750
15028
  var DMG_PER_J = 0.02;
14751
15029
  var BLAST_COUPLING = 0.1;
14752
15030
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
@@ -14763,11 +15041,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14763
15041
  var SEVER_BAND = 0.1;
14764
15042
  var SEVER_ASPECT = 3;
14765
15043
  var SEVER_BALLISTIC = 2;
15044
+ var TEAR_JOULES = 150;
15045
+ var tears = 0;
14766
15046
  var SUPPORT_TOLERANCE = 0.05;
14767
15047
  var UNIT_FRAGMENT_FACTOR = 4;
14768
15048
  var BREACH_GRID_CAP = 4096;
14769
15049
  var CHIP_ENERGY = 200;
14770
15050
  var CHIP_SPLASH = 0.5;
15051
+ var BAR_BOW = 0.08;
14771
15052
  var CUT_SLICE = 1;
14772
15053
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
14773
15054
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -14777,8 +15058,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14777
15058
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
14778
15059
  };
14779
15060
  var capacityScale = (p) => {
14780
- if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
14781
- return p.rt.removed ? Math.max(0.05, 1 - p.rt.removed / p.mass0) : 1;
15061
+ const residual = p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
15062
+ if (p.st.stripped) return residual;
15063
+ const kept = Math.min(p.rt.removed ? 1 - p.rt.removed / p.mass0 : 1, 1 - sectionLoss(p));
15064
+ return Math.max(p.spec.reinforcement ? residual : 0.05, kept);
15065
+ };
15066
+ var sectionLoss = (p) => {
15067
+ const cells = p.st.chipped;
15068
+ if (!cells?.length) return 0;
15069
+ const [u, v] = planeAxes(p.thin);
15070
+ const along = p.dims[u] >= p.dims[v] ? u : v;
15071
+ const across2 = along === u ? v : u;
15072
+ let worst = 0;
15073
+ for (const c of cells) {
15074
+ let gone = 0;
15075
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across2];
15076
+ worst = Math.max(worst, gone / Math.max(p.dims[across2], 1e-6));
15077
+ }
15078
+ return Math.min(1, worst);
14782
15079
  };
14783
15080
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
14784
15081
  var clone = (v) => structuredClone(v);
@@ -15959,6 +16256,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15959
16256
  debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
15960
16257
  setStage(o, p, 1, {}, src);
15961
16258
  };
16259
+ const crater = (o, p, wp, normal, radius, src) => {
16260
+ markDamaged(p);
16261
+ emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: -1, point: wp, area: Math.PI * radius * radius });
16262
+ debris(o, p, p.mat.breakDebris, wp, normal, 6, 1.5, src);
16263
+ setStage(o, p, 1, {}, src);
16264
+ };
15962
16265
  const insideOpening = (o, p, objectPoint) => {
15963
16266
  const op = p.st.opening;
15964
16267
  if (!op) return false;
@@ -16035,9 +16338,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16035
16338
  const lp = toLocal(p.pos, p.q, objectPoint);
16036
16339
  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);
16037
16340
  };
16038
- const chipUnits = (o, p, wp, energy, dir, radius, src) => {
16341
+ const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
16039
16342
  var _a, _b;
16040
- if (!p.spec.units || p.spec.reinforcement || !p.staged || chipEnergy <= 0 || energy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
16343
+ if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
16344
+ const rule = p.mat.hits?.[kind];
16345
+ const perKg = rule?.unitJoules ?? chipEnergy;
16346
+ const energy = hitEnergy - (rule?.floor ?? 0);
16347
+ if (perKg <= 0 || energy <= 0) return;
16041
16348
  const [u, v] = planeAxes(p.thin);
16042
16349
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
16043
16350
  const grid = gridOf(p);
@@ -16051,7 +16358,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16051
16358
  const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
16052
16359
  const total = (worn[i] ?? 0) + energy * share;
16053
16360
  const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
16054
- if (total < chipEnergy * mass) {
16361
+ if (total < perKg * mass) {
16055
16362
  worn[i] = total;
16056
16363
  continue;
16057
16364
  }
@@ -16076,7 +16383,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16076
16383
  emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
16077
16384
  debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
16078
16385
  }
16386
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
16079
16387
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
16388
+ if (p.structural && !isLost(p)) {
16389
+ solveStructure(o);
16390
+ collapseCheck(o, src);
16391
+ }
16392
+ };
16393
+ const exposeRebar = (o, p, src) => {
16394
+ const r = p.spec.reinforcement;
16395
+ if (!r) return;
16396
+ const [u, v] = planeAxes(p.thin);
16397
+ const all = rebarBars(p.dims, p.thin, r.spacing);
16398
+ let bars;
16399
+ if (p.st.stripped) bars = all;
16400
+ else {
16401
+ bars = [];
16402
+ for (const cell of p.st.chipped ?? []) bars.push(...clipBarsToBox(all, u, v, cell.center, cell.size));
16403
+ const op = p.st.opening;
16404
+ if (op) {
16405
+ const lc = toLocal(p.pos, p.q, op.center);
16406
+ bars.push(...clipBarsToDisc(all, u, v, [lc[u], lc[v]], op.radius + 0.05));
16407
+ }
16408
+ }
16409
+ const bow = clamp2(((p.st.stress ?? 0) - 1) / 0.5, 0, 1);
16410
+ if (bow > 0) bars = bars.flatMap((bar2, i) => bendBar(bar2, p.thin, bow * BAR_BOW, i));
16411
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
16080
16412
  };
16081
16413
  const openBreach = (o, p, center, total, energy, dir, src) => {
16082
16414
  const [u, v] = planeAxes(p.thin);
@@ -16104,10 +16436,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16104
16436
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
16105
16437
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
16106
16438
  const reinforcement = p.spec.reinforcement;
16107
- if (reinforcement) {
16108
- const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
16109
- if (bars.length > 0) emit({ ...base(o, src), type: "expose", part: p.spec.id, bars });
16110
- }
16439
+ if (reinforcement) exposeRebar(o, p, src);
16111
16440
  if (p.structural) solveStructure(o);
16112
16441
  };
16113
16442
  const stripPart = (o, p, reinforcement, src) => {
@@ -16119,8 +16448,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16119
16448
  p.st.burning = false;
16120
16449
  p.rt.jet = false;
16121
16450
  markDamaged(p);
16122
- emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
16123
16451
  solveStructure(o);
16452
+ exposeRebar(o, p, src);
16124
16453
  collapseCheck(o, src);
16125
16454
  };
16126
16455
  const unitFragmentPlan = (p, cap) => {
@@ -16174,7 +16503,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16174
16503
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
16175
16504
  const hitLocal = toLocal(c, q, hitW);
16176
16505
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
16177
- const axis = style === "splinter" ? severAxis(p) : -1;
16506
+ const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
16178
16507
  const cutAt2 = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
16179
16508
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
16180
16509
  style,
@@ -16246,7 +16575,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16246
16575
  };
16247
16576
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
16248
16577
  const severAxis = (p) => {
16249
- if (p.mat.fracture !== "splinter" || p.spec.shape.kind !== "box") return -1;
16578
+ if (p.mat.fracture !== "splinter" && p.mat.fracture !== "snap" || p.spec.shape.kind !== "box") return -1;
16250
16579
  const d = p.dims;
16251
16580
  const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
16252
16581
  return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
@@ -16307,6 +16636,49 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16307
16636
  }
16308
16637
  }
16309
16638
  };
16639
+ const tearOf = (mat, kind) => {
16640
+ const h = mat.hits?.[kind];
16641
+ const tear = h?.tear ?? (kind === "ballistic" && mat.penetrationClass === "wood" ? "splinters" : "none");
16642
+ return { tear, joules: h?.tearJoules ?? TEAR_JOULES };
16643
+ };
16644
+ const tearAt = (o, p, kind, entry2, exit, d, E, scale, src) => {
16645
+ const { tear, joules } = tearOf(p.mat, kind);
16646
+ if (tear === "none") return;
16647
+ const shot = kind === "ballistic";
16648
+ const n = shot ? exit ? clamp2(Math.round(E / joules), 1, 7) : 1 : clamp2(Math.floor(E / joules), 0, 5);
16649
+ if (n === 0) return;
16650
+ const fibre = tear === "lumps";
16651
+ const { q } = partXf(o, p);
16652
+ const along = (p.spec.grain ? AXIS[p.spec.grain] : void 0) ?? [0, 1, 2].sort((a, b) => p.dims[b] - p.dims[a])[0];
16653
+ const fragments = [];
16654
+ for (let i = 0; i < n; i++) {
16655
+ const back = !exit || i % 3 === 2;
16656
+ const len = scale * (fibre ? 1.2 + 1.2 * rnd() : 3 + 4 * rnd()), w = scale * (0.8 + 0.8 * rnd()), t = scale * (fibre ? 0.7 + 0.6 * rnd() : 0.4 + 0.4 * rnd());
16657
+ const size = [w, w, w];
16658
+ size[along] = len;
16659
+ size[(along + 1) % 3] = w;
16660
+ size[(along + 2) % 3] = t;
16661
+ const spread3 = [(rnd() - 0.5) * 0.8, (rnd() - 0.2) * 0.8, (rnd() - 0.5) * 0.8];
16662
+ const speed = shot ? back ? 1.5 + 2 * rnd() : 2 + 4 * rnd() : 0.8 + 1.2 * rnd();
16663
+ const v = vec.scale(vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3)), speed);
16664
+ const mass = size[0] * size[1] * size[2] * p.mat.density;
16665
+ fragments.push({
16666
+ id: `${p.spec.id}#t${tears++}-${i}`,
16667
+ part: p.spec.id,
16668
+ material: p.spec.material,
16669
+ shape: fibre ? "chunk" : "splinter",
16670
+ center: back || !exit ? vec.madd(entry2, d, -0.01) : vec.madd(exit, d, 0.01),
16671
+ size,
16672
+ rotation: q,
16673
+ velocity: v,
16674
+ inner: true,
16675
+ angularVelocity: [(rnd() - 0.5) * 30, (rnd() - 0.5) * 30, (rnd() - 0.5) * 30],
16676
+ mass
16677
+ });
16678
+ }
16679
+ const patch = [scale * 4, scale * 4, scale * 4];
16680
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments });
16681
+ };
16310
16682
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
16311
16683
  const mat = p.mat;
16312
16684
  const piercing = round3.piercing ?? 1;
@@ -16335,6 +16707,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16335
16707
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
16336
16708
  }
16337
16709
  if (p.layers.length > 0) spallOuter(o, p, entry2, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
16710
+ else if (mat.hits?.ballistic?.crater) crater(o, p, entry2, n, clamp2(0.02 + Math.sqrt(E) * 15e-4, 0.02, 0.15), src);
16338
16711
  const brittle = mat.ductility < 0.3;
16339
16712
  const ratio = Ep / Math.max(thr, 1e-6);
16340
16713
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
@@ -16368,8 +16741,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16368
16741
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
16369
16742
  debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
16370
16743
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
16371
- const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry2, d, E * 0.1, 12, src);
16372
- chipUnits(o, p, entry2, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
16744
+ const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
16745
+ const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
16746
+ if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
16747
+ 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);
16373
16748
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
16374
16749
  };
16375
16750
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -16377,14 +16752,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16377
16752
  const thrMm = mat.penetration * p.thickness * 1e3;
16378
16753
  const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
16379
16754
  const Ec = E * fragile;
16755
+ const sheet = mat.ductility >= 0.5 && p.thickness <= THIN_SHEET_CRUSH;
16756
+ const fold = sheet ? Math.min(4, Math.sqrt(1e-3 / p.thickness)) : 1;
16757
+ const patch = sheet ? Math.min(contactR, THIN_SHEET_CONTACT) : contactR;
16380
16758
  if (E > 0) {
16381
16759
  if (kind === "blunt") {
16382
- if (mat.ductility >= 0.5) addDent(o, p, point, n, Math.max(contactR, 0.015), clamp2(E * 1e-4, 1e-3, 0.06), src);
16760
+ 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);
16383
16761
  else if (mat.ductility < 0.3) {
16384
16762
  if (Ec >= 5) addCrack(o, p, point, n, clamp2(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
16385
16763
  } 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);
16386
16764
  } else if (mat.fracture === "tear") {
16387
16765
  addHole(o, p, point, n, clamp2(0.02 + E * 5e-4, 0.02, 0.3), true, void 0, src);
16766
+ } else if (mat.ductility >= 0.3 && E >= thrMm) {
16767
+ addHole(o, p, point, n, clamp2(contactR, 0.01, 0.08), true, void 0, src);
16768
+ if (sheet) addDent(o, p, point, n, Math.max(patch * 0.6, 0.01), clamp2(E * 4e-5 * fold, 5e-4, 0.02), src);
16388
16769
  } else if (mat.ductility >= 0.5) {
16389
16770
  addDent(o, p, point, n, Math.max(contactR, 0.01), clamp2(E * 4e-5, 5e-4, 0.02), src);
16390
16771
  }
@@ -16397,7 +16778,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16397
16778
  const scale = mat.damageScale?.[kind] ?? 1;
16398
16779
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
16399
16780
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
16400
- if (kind === "blunt") chipUnits(o, p, point, E, d, Math.max(contactR, 0.03), src);
16781
+ if (kind === "blunt") chipUnits(o, p, "blunt", point, E, d, Math.max(contactR, 0.03), src);
16782
+ if (E > 0 && !isLost(p)) tearAt(o, p, kind, point, void 0, d, E, clamp2(contactR * 0.2, 6e-3, 0.02), src);
16401
16783
  return done;
16402
16784
  };
16403
16785
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -16976,6 +17358,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16976
17358
  if (p.rt.stressT <= 0) {
16977
17359
  p.rt.stressT = 0.5;
16978
17360
  emit({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
17361
+ if (p.spec.reinforcement && (p.st.stripped || p.st.chipped?.length || p.st.opening)) exposeRebar(o, p);
16979
17362
  }
16980
17363
  applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
16981
17364
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
@@ -17192,6 +17575,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17192
17575
  const r = raycastInternal(origin, direction, maxDistance);
17193
17576
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
17194
17577
  },
17578
+ sweepBox(box4, direction, distance2, options2 = {}) {
17579
+ const d = vec.normalize(direction);
17580
+ const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
17581
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
17582
+ let best = null;
17583
+ for (const origin of sweepOrigins(box4, d, options2.spacing ?? 0.2)) {
17584
+ const r = raycastInternal(origin, d, distance2 + SWEEP_BACK, ignore);
17585
+ if (!r || best && r.distance >= best.distance) continue;
17586
+ best = { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: Math.max(0, r.distance - SWEEP_BACK) };
17587
+ }
17588
+ return best;
17589
+ },
17195
17590
  shoot(origin, direction, energy, caliber, source, round3) {
17196
17591
  const extras = typeof round3 === "number" ? { piercing: round3 } : round3 ?? {};
17197
17592
  return withCollapses(run(() => {
@@ -18763,7 +19158,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18763
19158
  }
18764
19159
  return spec;
18765
19160
  };
18766
- var partSpecOf = (part, capacity, detailed) => detailed && part.assembly ? assemblySpec(part, capacity) : boxSpec(part, capacity);
19161
+ var MEMBER_CAGE = { spacing: 0.1, diameter: 8e-3, unit: 0.1 };
19162
+ var CAGED_ROLES = /* @__PURE__ */ new Set(["beam", "column"]);
19163
+ var caged = (spec, part, rebar2) => {
19164
+ if (!rebar2 || part.material !== "concrete" || !CAGED_ROLES.has(part.role) || spec.reinforcement) return spec;
19165
+ const size = spec.shape.kind === "box" ? spec.shape.size : null;
19166
+ if (!size) return spec;
19167
+ const thin = Math.min(...size);
19168
+ return {
19169
+ ...spec,
19170
+ reinforcement: { material: rebar2, spacing: MEMBER_CAGE.spacing, diameter: MEMBER_CAGE.diameter },
19171
+ units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, thin], bond: "stack", mortar: "concrete" }
19172
+ };
19173
+ };
19174
+ var partSpecOf = (part, capacity, detailed, rebar2) => {
19175
+ if (detailed && part.assembly) return assemblySpec(part, capacity);
19176
+ const spec = boxSpec(part, capacity);
19177
+ return detailed ? caged(spec, part, rebar2) : spec;
19178
+ };
18767
19179
  function toDestructibleSpec(structure, options = {}) {
18768
19180
  const { safety = 0.65, redundancy = 0.97, safetyOthers = 0.45 } = options;
18769
19181
  const kept = structure.parts.filter((part) => !options.filter || options.filter(part));
@@ -18780,7 +19192,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18780
19192
  const sizing = { safety, redundancy, safetyOthers };
18781
19193
  const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts, panels.supports, sizing) };
18782
19194
  const { parts, supports, capacities } = detail === "units" ? applyUnits(sized, structure) : sized;
18783
- const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse"));
19195
+ const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse", structure.construction.frame.rebar));
18784
19196
  const margin = options.terrainMargin ?? TERRAIN_MARGIN;
18785
19197
  const x0 = Math.min(...parts.map((part) => part.aabb[0]));
18786
19198
  const z0 = Math.min(...parts.map((part) => part.aabb[1]));
@@ -26745,7 +27157,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26745
27157
  if (!pc || pc.unit) return sp;
26746
27158
  if (pc.role !== "wall" && pc.role !== "partition") {
26747
27159
  const def = getAssembly(pc.assembly || (pc.material === "concrete" && ["slab", "beam", "column", "stair"].includes(pc.role) ? "reinforced_concrete" : ""));
26748
- return def && def.reinforcement && !sp.reinforcement ? { ...sp, reinforcement: { ...def.reinforcement } } : sp;
27160
+ const out = def && def.reinforcement && !sp.reinforcement ? { ...sp, reinforcement: { ...def.reinforcement } } : sp;
27161
+ if (out.reinforcement && !out.units && pc.material === "concrete" && (pc.role === "beam" || pc.role === "column") && sp.shape.kind === "box") {
27162
+ return { ...out, units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, Math.min(...sp.shape.size)], bond: "stack", mortar: "concrete" } };
27163
+ }
27164
+ return out;
26749
27165
  }
26750
27166
  if (!pc.assembly) return sp;
26751
27167
  const [u0, v0, u1, v1] = pc.aabb, alongU = u1 - u0 >= v1 - v0;
@@ -26839,29 +27255,44 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26839
27255
  }
26840
27256
  return list2;
26841
27257
  }
27258
+ const specOf = (id) => opts.spec()?.parts?.find((q) => q.id === id);
27259
+ const same = (a, b) => a.every((x, k) => Math.abs(x - b[k]) < 1e-3);
27260
+ function hostOf(partId) {
27261
+ if (structure.meshesOf(partId).some((it) => it.mesh)) return partId;
27262
+ const part = structure.byId.get(partId);
27263
+ if (!part) return null;
27264
+ for (const q of structure.parts) {
27265
+ 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;
27266
+ }
27267
+ return null;
27268
+ }
27269
+ 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);
26842
27270
  function draw2(partId, change) {
26843
27271
  if (lod !== 0) return false;
26844
- const part = structure.byId.get(partId);
26845
- const spec = opts.spec()?.parts?.find((q) => q.id === partId);
26846
- if (!part || !spec || !spec.units) return false;
26847
- let view = views.get(partId);
27272
+ const hostId = hostOf(partId);
27273
+ if (!hostId || !specOf(partId)?.units) return false;
27274
+ let view = views.get(hostId);
26848
27275
  if (!view) {
26849
- const items = structure.meshesOf(partId).filter((it) => it.mesh);
27276
+ const items = structure.meshesOf(hostId).filter((it) => it.mesh);
26850
27277
  if (!items.length) return false;
26851
- view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: [] };
27278
+ const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
27279
+ view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
26852
27280
  }
27281
+ if (!view.chips.has(partId)) return false;
26853
27282
  if (change.opening) {
26854
- if (view.opening) return false;
26855
- view.opening = { center: local(change.opening.center), radius: change.opening.radius };
27283
+ if (view.openings.has(partId)) return false;
27284
+ view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
27285
+ if (partId === hostId) view.opening = view.openings.get(partId);
26856
27286
  }
26857
- if (change.chip) view.chips.push(local(change.chip));
26858
- const list2 = courses(part, spec.units, view.opening, view.chips, view.paint);
27287
+ if (change.chip) view.chips.get(partId).push(local(change.chip));
27288
+ const list2 = [];
27289
+ for (const [id, chips] of view.chips) list2.push(...courses(structure.byId.get(id), specOf(id).units, view.openings.get(id) ?? null, chips, view.paint));
26859
27290
  if (view.mesh) {
26860
27291
  view.mesh.removeFromParent();
26861
27292
  view.mesh.dispose();
26862
27293
  }
26863
27294
  const mesh2 = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
26864
- mesh2.name = "breach:" + partId;
27295
+ mesh2.name = "breach:" + hostId;
26865
27296
  list2.forEach((u, i) => {
26866
27297
  tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
26867
27298
  mesh2.setMatrixAt(i, tmp.m);
@@ -26876,7 +27307,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26876
27307
  if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
26877
27308
  view.mesh = mesh2;
26878
27309
  view.units = list2.length;
26879
- views.set(partId, view);
27310
+ views.set(hostId, view);
26880
27311
  invalidate();
26881
27312
  invalidateShadow();
26882
27313
  return true;
@@ -27929,6 +28360,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27929
28360
  return buildApi(K, shared);
27930
28361
  }
27931
28362
  var WOOD_THIN = 0.035;
28363
+ var BOARD_TOP_THICK = 0.07;
28364
+ var BOARD_TOP_WIDTH = 0.5;
28365
+ var BOARD_WIDTH = 0.16;
27932
28366
  var SHELF_SPAN = 1.5;
27933
28367
  var SHELVING = /shelf|bookcase|rack/;
27934
28368
  function carcass(parts, drawn) {
@@ -27943,6 +28377,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27943
28377
  } else if (y <= x && y <= z) part.structure = { role: "beam", maxSpan: SHELF_SPAN * Math.max(x, z) };
27944
28378
  }
27945
28379
  }
28380
+ function splitTopsIntoBoards(drawn, materialOf, carcassed) {
28381
+ const tops = /* @__PURE__ */ new Map();
28382
+ if (carcassed) return tops;
28383
+ for (let i = drawn.length - 1; i >= 0; i--) {
28384
+ const p = drawn[i];
28385
+ if (p.key !== "wood" || materialOf.wood !== "wood_hard" || p.shape.kind !== "box" || p.rotation) continue;
28386
+ const [sx, sy, sz] = p.shape.size;
28387
+ const across2 = sx < sz ? 0 : 2;
28388
+ const width = Math.min(sx, sz);
28389
+ if (sy < WOOD_THIN || sy > BOARD_TOP_THICK || sy >= width || width < BOARD_TOP_WIDTH) continue;
28390
+ const count = Math.round(width / BOARD_WIDTH);
28391
+ const boards = [];
28392
+ for (let b = 0; b < count; b++) {
28393
+ const size = [sx, sy, sz];
28394
+ size[across2] = width / count;
28395
+ const position = [...p.position];
28396
+ position[across2] += (b + 0.5) * size[across2] - width / 2;
28397
+ const min = [...p.min];
28398
+ const max = [...p.max];
28399
+ min[across2] = position[across2] - size[across2] / 2;
28400
+ max[across2] = position[across2] + size[across2] / 2;
28401
+ boards.push({ ...p, id: `${p.id}_b${b}`, shape: { kind: "box", size }, position, min, max, color: shadeBoard(p.color, b) });
28402
+ }
28403
+ drawn.splice(i, 1, ...boards);
28404
+ tops.set(p.id, boards);
28405
+ }
28406
+ return tops;
28407
+ }
28408
+ function shadeBoard(color2, board) {
28409
+ const k = [1, 0.94, 1.05, 0.97, 1.03][board % 5];
28410
+ const channel = (shift) => Math.min(255, Math.round((color2 >> shift & 255) * k)) << shift;
28411
+ return channel(16) | channel(8) | channel(0);
28412
+ }
27946
28413
  var FLOOR_TOUCH = 5e-3;
27947
28414
  var LOOSE = /* @__PURE__ */ new Set(["paper"]);
27948
28415
  var BASE_MATERIAL = {
@@ -28020,10 +28487,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
28020
28487
  };
28021
28488
  run(A);
28022
28489
  if (!drawn.length) throw new Error(`game-floorplan-three: the builder of "${name}" drew no box or cylinder`);
28490
+ const boardsOf = splitTopsIntoBoards(drawn, materialOf, Boolean(opts.carcass));
28023
28491
  const parts = drawn.map((p) => {
28024
28492
  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]);
28025
28493
  let material = materialOf[p.key] ?? "plastic";
28026
- if (p.key === "wood" && material === "wood_hard" && thin < WOOD_THIN) material = "wood_soft";
28494
+ if (p.key === "wood" && material === "wood_hard" && opts.carcass && !opts.materials?.wood) material = "mdf";
28495
+ else if (p.key === "wood" && material === "wood_hard" && thin < WOOD_THIN) material = "wood_soft";
28027
28496
  const part = { id: p.id, material, shape: p.shape, position: p.position };
28028
28497
  if (p.rotation) part.rotation = p.rotation;
28029
28498
  if (!LOOSE.has(material) && p.min[1] <= FLOOR_TOUCH) part.anchored = true;
@@ -28058,6 +28527,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
28058
28527
  if (area3 < 0.6 * footprint || p.max[1] < 0.2) continue;
28059
28528
  if (!top || p.max[1] > top.y + 1e-6) top = { part: p.id, y: p.max[1] };
28060
28529
  }
28530
+ for (const boards of boardsOf.values()) {
28531
+ if (top && boards[0].max[1] <= top.y - 1e-6) continue;
28532
+ const mid = boards[boards.length >> 1];
28533
+ const min = [Math.min(...boards.map((b) => b.min[0])), Math.min(...boards.map((b) => b.min[2]))];
28534
+ const max = [Math.max(...boards.map((b) => b.max[0])), Math.max(...boards.map((b) => b.max[2]))];
28535
+ if ((max[0] - min[0]) * (max[1] - min[1]) >= 0.6 * footprint && mid.max[1] >= 0.2) top = { part: mid.id, y: mid.max[1], parts: boards.map((b) => b.id) };
28536
+ }
28061
28537
  if (opts.carcass) carcass(parts, byId);
28062
28538
  const spec = { name, parts, joints, tags: ["furniture"] };
28063
28539
  const colors = new Map(drawn.map((p) => [p.id, p.color]));
@@ -29047,6 +29523,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
29047
29523
  const module = {
29048
29524
  id: "items_area",
29049
29525
  name: text.name,
29526
+ ground: { surface: "tile_floor" },
29050
29527
  group: text.group,
29051
29528
  state,
29052
29529
  placed: () => entries2.map((entry2) => ({ id: entry2.id, itemId: entry2.itemId, group: entry2.group, kind: entry2.kind, size: entry2.size, root: entry2.root })),