@adatechnology/game-items-three 0.3.2 → 0.4.1

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.
@@ -2640,8 +2640,8 @@ var GameItemsThree = (() => {
2640
2640
  bedHeight = roadY * 1e3 + z * 14 - (x - params.roadSlopeAnchorX) * 5;
2641
2641
  roughness = 0.55;
2642
2642
  }
2643
- const noise2 = valueNoise(column * 0.35, row * 0.35) * 0.6 + valueNoise(column * 1.3, row * 1.3) * 0.4;
2644
- bed[index] = bedHeight + (noise2 - 0.5) * roughness * 2;
2643
+ const noise3 = valueNoise(column * 0.35, row * 0.35) * 0.6 + valueNoise(column * 1.3, row * 1.3) * 0.4;
2644
+ bed[index] = bedHeight + (noise3 - 0.5) * roughness * 2;
2645
2645
  vertical[index] = region === 0 || region === 2 ? 0.95 : 1;
2646
2646
  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);
2647
2647
  }
@@ -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,8 +14194,20 @@ 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",
14210
+ visual: { innerColor: 10197652 },
14088
14211
  thermal: {
14089
14212
  conductivity: 0.15,
14090
14213
  heatCapacity: 2.5
@@ -14115,6 +14238,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14115
14238
  ],
14116
14239
  hitDebris: "chips",
14117
14240
  breakDebris: "dust",
14241
+ visual: { innerColor: 12085567 },
14118
14242
  thermal: {
14119
14243
  conductivity: 0.15,
14120
14244
  heatCapacity: 2.5
@@ -14145,6 +14269,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14145
14269
  ],
14146
14270
  hitDebris: "dust",
14147
14271
  breakDebris: "dust",
14272
+ visual: { innerColor: 14999507 },
14148
14273
  thermal: {
14149
14274
  conductivity: 0.1,
14150
14275
  heatCapacity: 1.5
@@ -14170,6 +14295,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14170
14295
  ],
14171
14296
  hitDebris: "chips",
14172
14297
  breakDebris: "dust",
14298
+ visual: { innerColor: 9407364 },
14173
14299
  thermal: {
14174
14300
  conductivity: 0.2,
14175
14301
  heatCapacity: 2.5
@@ -14451,7 +14577,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14451
14577
  smoke: "white"
14452
14578
  },
14453
14579
  visual: {
14454
- charColor: 1709072
14580
+ charColor: 1709072,
14581
+ innerColor: 14207126
14455
14582
  },
14456
14583
  tags: [
14457
14584
  "plant",
@@ -14459,7 +14586,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14459
14586
  "organic",
14460
14587
  "flammable"
14461
14588
  ],
14462
- compressive: 6e4
14589
+ compressive: 6e4,
14590
+ hits: {
14591
+ blunt: {
14592
+ tear: "none"
14593
+ }
14594
+ }
14463
14595
  },
14464
14596
  {
14465
14597
  id: "cactus",
@@ -14574,6 +14706,130 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14574
14706
  "masonry"
14575
14707
  ],
14576
14708
  compressive: 2e4
14709
+ },
14710
+ {
14711
+ id: "asphalt",
14712
+ density: 2350,
14713
+ hpPerKg: 10,
14714
+ penetration: 14,
14715
+ penetrationClass: "concrete",
14716
+ penetrationScale: 0.8,
14717
+ ductility: 0.1,
14718
+ fracture: "crumble",
14719
+ fragmentSize: [
14720
+ 0.03,
14721
+ 0.15
14722
+ ],
14723
+ hitDebris: "chips",
14724
+ breakDebris: "dust",
14725
+ thermal: {
14726
+ conductivity: 0.1,
14727
+ heatCapacity: 2,
14728
+ ignitionC: 400,
14729
+ burnRate: 2e-3,
14730
+ smoke: "black"
14731
+ },
14732
+ tags: [
14733
+ "masonry",
14734
+ "brittle"
14735
+ ],
14736
+ compressive: 3e4
14737
+ },
14738
+ {
14739
+ id: "sand",
14740
+ density: 1600,
14741
+ hpPerKg: 2,
14742
+ penetration: 6,
14743
+ ductility: 0.4,
14744
+ fracture: "crumble",
14745
+ fragmentSize: [
14746
+ 0.01,
14747
+ 0.04
14748
+ ],
14749
+ hitDebris: "dust",
14750
+ breakDebris: "dust",
14751
+ thermal: {
14752
+ conductivity: 0.1,
14753
+ heatCapacity: 2
14754
+ },
14755
+ tags: [
14756
+ "soil"
14757
+ ],
14758
+ compressive: 1e4
14759
+ },
14760
+ {
14761
+ id: "gravel",
14762
+ density: 1800,
14763
+ hpPerKg: 3,
14764
+ penetration: 8,
14765
+ ductility: 0.3,
14766
+ fracture: "crumble",
14767
+ fragmentSize: [
14768
+ 0.02,
14769
+ 0.06
14770
+ ],
14771
+ hitDebris: "chips",
14772
+ breakDebris: "dust",
14773
+ thermal: {
14774
+ conductivity: 0.1,
14775
+ heatCapacity: 2
14776
+ },
14777
+ tags: [
14778
+ "soil",
14779
+ "masonry"
14780
+ ],
14781
+ compressive: 2e4
14782
+ },
14783
+ {
14784
+ id: "turf",
14785
+ density: 900,
14786
+ hpPerKg: 3,
14787
+ penetration: 4,
14788
+ ductility: 0.5,
14789
+ fracture: "crumble",
14790
+ fragmentSize: [
14791
+ 0.03,
14792
+ 0.1
14793
+ ],
14794
+ hitDebris: "dust",
14795
+ breakDebris: "dust",
14796
+ thermal: {
14797
+ conductivity: 0.1,
14798
+ heatCapacity: 1.5,
14799
+ ignitionC: 250,
14800
+ burnRate: 0.03,
14801
+ smoke: "white"
14802
+ },
14803
+ tags: [
14804
+ "soil",
14805
+ "organic"
14806
+ ],
14807
+ compressive: 15e3
14808
+ },
14809
+ {
14810
+ id: "mortar",
14811
+ density: 1900,
14812
+ hpPerKg: 10,
14813
+ penetration: 12,
14814
+ penetrationClass: "concrete",
14815
+ penetrationScale: 1.2,
14816
+ ductility: 0.03,
14817
+ fracture: "crumble",
14818
+ fragmentSize: [
14819
+ 0.02,
14820
+ 0.08
14821
+ ],
14822
+ hitDebris: "chips",
14823
+ breakDebris: "dust",
14824
+ thermal: {
14825
+ conductivity: 0.15,
14826
+ heatCapacity: 2.5
14827
+ },
14828
+ tags: [
14829
+ "masonry",
14830
+ "brittle"
14831
+ ],
14832
+ compressive: 1e5
14577
14833
  }
14578
14834
  ];
14579
14835
  var material_hints_default = {
@@ -14747,6 +15003,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14747
15003
  return (1 + 4 * (0.3 - ductility) / 0.3) * Math.min(25, Math.max(1, (0.02 / Math.max(thickness, 1e-4)) ** 2));
14748
15004
  };
14749
15005
  var THIN_SHEET_DISHING = 5e-3;
15006
+ var SWEEP_BACK = 0.01;
15007
+ function sweepOrigins(box4, d, spacing) {
15008
+ const origins = [];
15009
+ const line = (from, to) => {
15010
+ const n = Math.max(1, Math.ceil((to - from) / spacing));
15011
+ return Array.from({ length: n + 1 }, (_, i) => from + (to - from) * i / n);
15012
+ };
15013
+ for (const axis of [0, 1, 2]) {
15014
+ if (Math.abs(d[axis]) < 1e-3) continue;
15015
+ const lead = d[axis] > 0 ? box4.max[axis] - SWEEP_BACK : box4.min[axis] + SWEEP_BACK;
15016
+ const u = (axis + 1) % 3;
15017
+ const v = (axis + 2) % 3;
15018
+ for (const a of line(box4.min[u], box4.max[u])) {
15019
+ for (const b of line(box4.min[v], box4.max[v])) {
15020
+ const point = [0, 0, 0];
15021
+ point[axis] = lead;
15022
+ point[u] = a;
15023
+ point[v] = b;
15024
+ origins.push(point);
15025
+ }
15026
+ }
15027
+ }
15028
+ return origins;
15029
+ }
15030
+ var THIN_SHEET_CRUSH = 5e-4;
15031
+ var THIN_SHEET_CONTACT = 0.025;
14750
15032
  var DMG_PER_J = 0.02;
14751
15033
  var BLAST_COUPLING = 0.1;
14752
15034
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
@@ -14763,11 +15045,15 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14763
15045
  var SEVER_BAND = 0.1;
14764
15046
  var SEVER_ASPECT = 3;
14765
15047
  var SEVER_BALLISTIC = 2;
15048
+ var TEAR_JOULES = 150;
15049
+ var tears = 0;
14766
15050
  var SUPPORT_TOLERANCE = 0.05;
14767
15051
  var UNIT_FRAGMENT_FACTOR = 4;
14768
15052
  var BREACH_GRID_CAP = 4096;
14769
15053
  var CHIP_ENERGY = 200;
14770
15054
  var CHIP_SPLASH = 0.5;
15055
+ var BAR_BOW = 0.08;
15056
+ var BULLET_JOINT_SPAN = 0.3;
14771
15057
  var CUT_SLICE = 1;
14772
15058
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
14773
15059
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -14777,8 +15063,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14777
15063
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
14778
15064
  };
14779
15065
  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;
15066
+ const residual = p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
15067
+ if (p.st.stripped) return residual;
15068
+ const kept = Math.min(p.rt.removed ? 1 - p.rt.removed / p.mass0 : 1, 1 - sectionLoss(p));
15069
+ return Math.max(p.spec.reinforcement ? residual : 0.05, kept);
15070
+ };
15071
+ var sectionLoss = (p) => {
15072
+ const cells = p.st.chipped;
15073
+ if (!cells?.length) return 0;
15074
+ const [u, v] = planeAxes(p.thin);
15075
+ const along = p.dims[u] >= p.dims[v] ? u : v;
15076
+ const across2 = along === u ? v : u;
15077
+ let worst = 0;
15078
+ for (const c of cells) {
15079
+ let gone = 0;
15080
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across2];
15081
+ worst = Math.max(worst, gone / Math.max(p.dims[across2], 1e-6));
15082
+ }
15083
+ return Math.min(1, worst);
14782
15084
  };
14783
15085
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
14784
15086
  var clone = (v) => structuredClone(v);
@@ -15878,7 +16180,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15878
16180
  emit({ ...base(o, src), type: "crack", part: p.spec.id, crack, worldPoint: wp });
15879
16181
  setStage(o, p, 1, {}, src);
15880
16182
  };
15881
- const debris = (o, p, kind, point, normal, count, speed, src) => emit({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed });
16183
+ const debris = (o, p, kind, point, normal, count, speed, src, material) => emit({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed, ...material && material !== p.spec.material ? { material } : {} });
15882
16184
  const holeRate = (hole, pressurized) => (pressurized ? 0.5 : 0.1) * (hole.diameter / 9e-3);
15883
16185
  const detachEvents = (o, opened, by, src) => {
15884
16186
  for (const j of opened) emit({ ...base(o, src), type: "detach", joint: j.spec.id, parts: [j.spec.a, j.spec.b], by, groups: o.st.groups.map((g) => [...g]) });
@@ -15919,7 +16221,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15919
16221
  const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
15920
16222
  return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
15921
16223
  };
15922
- const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false) => {
16224
+ const jointLength = (o, j) => {
16225
+ const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
16226
+ let len = 0;
16227
+ 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]));
16228
+ return len;
16229
+ };
16230
+ const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false, span) => {
15923
16231
  if (amount <= 0) return;
15924
16232
  const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
15925
16233
  if (split && mine.length) amount /= mine.length;
@@ -15927,7 +16235,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15927
16235
  if (j.st.open) continue;
15928
16236
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
15929
16237
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
15930
- j.st.hp -= amount;
16238
+ j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
15931
16239
  if (j.st.hp <= 0) {
15932
16240
  j.st.hp = 0;
15933
16241
  j.st.open = true;
@@ -15956,7 +16264,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15956
16264
  if (spalls.length > MAX_SPALLS) spalls.shift();
15957
16265
  markDamaged(p);
15958
16266
  emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: layer.index, point: wp, area: Math.PI * radius * radius });
15959
- debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
16267
+ debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src, layer.mat.id);
16268
+ setStage(o, p, 1, {}, src);
16269
+ };
16270
+ const crater = (o, p, wp, normal, radius, src) => {
16271
+ markDamaged(p);
16272
+ emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: -1, point: wp, area: Math.PI * radius * radius });
16273
+ debris(o, p, p.mat.breakDebris, wp, normal, 6, 1.5, src);
15960
16274
  setStage(o, p, 1, {}, src);
15961
16275
  };
15962
16276
  const insideOpening = (o, p, objectPoint) => {
@@ -16035,9 +16349,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16035
16349
  const lp = toLocal(p.pos, p.q, objectPoint);
16036
16350
  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
16351
  };
16038
- const chipUnits = (o, p, wp, energy, dir, radius, src) => {
16352
+ const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
16039
16353
  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;
16354
+ if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
16355
+ const rule = p.mat.hits?.[kind];
16356
+ const perKg = rule?.unitJoules ?? chipEnergy;
16357
+ const energy = hitEnergy - (rule?.floor ?? 0);
16358
+ if (perKg <= 0 || energy <= 0) return;
16041
16359
  const [u, v] = planeAxes(p.thin);
16042
16360
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
16043
16361
  const grid = gridOf(p);
@@ -16051,7 +16369,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16051
16369
  const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
16052
16370
  const total = (worn[i] ?? 0) + energy * share;
16053
16371
  const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
16054
- if (total < chipEnergy * mass) {
16372
+ if (total < perKg * mass) {
16055
16373
  worn[i] = total;
16056
16374
  continue;
16057
16375
  }
@@ -16076,7 +16394,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16076
16394
  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
16395
  debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
16078
16396
  }
16397
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
16079
16398
  for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
16399
+ if (p.structural && !isLost(p)) {
16400
+ solveStructure(o);
16401
+ collapseCheck(o, src);
16402
+ }
16403
+ };
16404
+ const exposeRebar = (o, p, src) => {
16405
+ const r = p.spec.reinforcement;
16406
+ if (!r) return;
16407
+ const [u, v] = planeAxes(p.thin);
16408
+ const all = rebarBars(p.dims, p.thin, r.spacing);
16409
+ let bars;
16410
+ if (p.st.stripped) bars = all;
16411
+ else {
16412
+ bars = [];
16413
+ for (const cell of p.st.chipped ?? []) bars.push(...clipBarsToBox(all, u, v, cell.center, cell.size));
16414
+ const op = p.st.opening;
16415
+ if (op) {
16416
+ const lc = toLocal(p.pos, p.q, op.center);
16417
+ bars.push(...clipBarsToDisc(all, u, v, [lc[u], lc[v]], op.radius + 0.05));
16418
+ }
16419
+ }
16420
+ const bow = clamp2(((p.st.stress ?? 0) - 1) / 0.5, 0, 1);
16421
+ if (bow > 0) bars = bars.flatMap((bar2, i) => bendBar(bar2, p.thin, bow * BAR_BOW, i));
16422
+ emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
16080
16423
  };
16081
16424
  const openBreach = (o, p, center, total, energy, dir, src) => {
16082
16425
  const [u, v] = planeAxes(p.thin);
@@ -16104,10 +16447,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16104
16447
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
16105
16448
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
16106
16449
  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
- }
16450
+ if (reinforcement) exposeRebar(o, p, src);
16111
16451
  if (p.structural) solveStructure(o);
16112
16452
  };
16113
16453
  const stripPart = (o, p, reinforcement, src) => {
@@ -16119,8 +16459,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16119
16459
  p.st.burning = false;
16120
16460
  p.rt.jet = false;
16121
16461
  markDamaged(p);
16122
- emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
16123
16462
  solveStructure(o);
16463
+ exposeRebar(o, p, src);
16124
16464
  collapseCheck(o, src);
16125
16465
  };
16126
16466
  const unitFragmentPlan = (p, cap) => {
@@ -16174,7 +16514,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16174
16514
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
16175
16515
  const hitLocal = toLocal(c, q, hitW);
16176
16516
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
16177
- const axis = style === "splinter" ? severAxis(p) : -1;
16517
+ const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
16178
16518
  const cutAt2 = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
16179
16519
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
16180
16520
  style,
@@ -16246,7 +16586,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16246
16586
  };
16247
16587
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
16248
16588
  const severAxis = (p) => {
16249
- if (p.mat.fracture !== "splinter" || p.spec.shape.kind !== "box") return -1;
16589
+ if (p.mat.fracture !== "splinter" && p.mat.fracture !== "snap" || p.spec.shape.kind !== "box") return -1;
16250
16590
  const d = p.dims;
16251
16591
  const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
16252
16592
  return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
@@ -16307,6 +16647,49 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16307
16647
  }
16308
16648
  }
16309
16649
  };
16650
+ const tearOf = (mat, kind) => {
16651
+ const h = mat.hits?.[kind];
16652
+ const tear = h?.tear ?? (kind === "ballistic" && mat.penetrationClass === "wood" ? "splinters" : "none");
16653
+ return { tear, joules: h?.tearJoules ?? TEAR_JOULES };
16654
+ };
16655
+ const tearAt = (o, p, kind, entry2, exit, d, E, scale, src) => {
16656
+ const { tear, joules } = tearOf(p.mat, kind);
16657
+ if (tear === "none") return;
16658
+ const shot = kind === "ballistic";
16659
+ const n = shot ? exit ? clamp2(Math.round(E / joules), 1, 7) : 1 : clamp2(Math.floor(E / joules), 0, 5);
16660
+ if (n === 0) return;
16661
+ const fibre = tear === "lumps";
16662
+ const { q } = partXf(o, p);
16663
+ const along = (p.spec.grain ? AXIS[p.spec.grain] : void 0) ?? [0, 1, 2].sort((a, b) => p.dims[b] - p.dims[a])[0];
16664
+ const fragments = [];
16665
+ for (let i = 0; i < n; i++) {
16666
+ const back = !exit || i % 3 === 2;
16667
+ 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());
16668
+ const size = [w, w, w];
16669
+ size[along] = len;
16670
+ size[(along + 1) % 3] = w;
16671
+ size[(along + 2) % 3] = t;
16672
+ const spread3 = [(rnd() - 0.5) * 0.8, (rnd() - 0.2) * 0.8, (rnd() - 0.5) * 0.8];
16673
+ const speed = shot ? back ? 1.5 + 2 * rnd() : 2 + 4 * rnd() : 0.8 + 1.2 * rnd();
16674
+ const v = vec.scale(vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3)), speed);
16675
+ const mass = size[0] * size[1] * size[2] * p.mat.density;
16676
+ fragments.push({
16677
+ id: `${p.spec.id}#t${tears++}-${i}`,
16678
+ part: p.spec.id,
16679
+ material: p.spec.material,
16680
+ shape: fibre ? "chunk" : "splinter",
16681
+ center: back || !exit ? vec.madd(entry2, d, -0.01) : vec.madd(exit, d, 0.01),
16682
+ size,
16683
+ rotation: q,
16684
+ velocity: v,
16685
+ inner: true,
16686
+ angularVelocity: [(rnd() - 0.5) * 30, (rnd() - 0.5) * 30, (rnd() - 0.5) * 30],
16687
+ mass
16688
+ });
16689
+ }
16690
+ const patch = [scale * 4, scale * 4, scale * 4];
16691
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments });
16692
+ };
16310
16693
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
16311
16694
  const mat = p.mat;
16312
16695
  const piercing = round3.piercing ?? 1;
@@ -16335,6 +16718,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16335
16718
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
16336
16719
  }
16337
16720
  if (p.layers.length > 0) spallOuter(o, p, entry2, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
16721
+ 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
16722
  const brittle = mat.ductility < 0.3;
16339
16723
  const ratio = Ep / Math.max(thr, 1e-6);
16340
16724
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
@@ -16367,9 +16751,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16367
16751
  if (mat.fracture !== "shatter") addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
16368
16752
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
16369
16753
  debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
16370
- 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);
16754
+ wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
16755
+ const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
16756
+ const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
16757
+ if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
16758
+ 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
16759
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
16374
16760
  };
16375
16761
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -16377,14 +16763,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16377
16763
  const thrMm = mat.penetration * p.thickness * 1e3;
16378
16764
  const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
16379
16765
  const Ec = E * fragile;
16766
+ const sheet = mat.ductility >= 0.5 && p.thickness <= THIN_SHEET_CRUSH;
16767
+ const fold = sheet ? Math.min(4, Math.sqrt(1e-3 / p.thickness)) : 1;
16768
+ const patch = sheet ? Math.min(contactR, THIN_SHEET_CONTACT) : contactR;
16380
16769
  if (E > 0) {
16381
16770
  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);
16771
+ 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
16772
  else if (mat.ductility < 0.3) {
16384
16773
  if (Ec >= 5) addCrack(o, p, point, n, clamp2(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
16385
16774
  } 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
16775
  } else if (mat.fracture === "tear") {
16387
16776
  addHole(o, p, point, n, clamp2(0.02 + E * 5e-4, 0.02, 0.3), true, void 0, src);
16777
+ } else if (mat.ductility >= 0.3 && E >= thrMm) {
16778
+ addHole(o, p, point, n, clamp2(contactR, 0.01, 0.08), true, void 0, src);
16779
+ 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
16780
  } else if (mat.ductility >= 0.5) {
16389
16781
  addDent(o, p, point, n, Math.max(contactR, 0.01), clamp2(E * 4e-5, 5e-4, 0.02), src);
16390
16782
  }
@@ -16397,7 +16789,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16397
16789
  const scale = mat.damageScale?.[kind] ?? 1;
16398
16790
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
16399
16791
  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);
16792
+ if (kind === "blunt") chipUnits(o, p, "blunt", point, E, d, Math.max(contactR, 0.03), src);
16793
+ if (E > 0 && !isLost(p)) tearAt(o, p, kind, point, void 0, d, E, clamp2(contactR * 0.2, 6e-3, 0.02), src);
16401
16794
  return done;
16402
16795
  };
16403
16796
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -16976,6 +17369,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16976
17369
  if (p.rt.stressT <= 0) {
16977
17370
  p.rt.stressT = 0.5;
16978
17371
  emit({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
17372
+ if (p.spec.reinforcement && (p.st.stripped || p.st.chipped?.length || p.st.opening)) exposeRebar(o, p);
16979
17373
  }
16980
17374
  applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
16981
17375
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
@@ -17192,6 +17586,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17192
17586
  const r = raycastInternal(origin, direction, maxDistance);
17193
17587
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
17194
17588
  },
17589
+ sweepBox(box4, direction, distance2, options2 = {}) {
17590
+ const d = vec.normalize(direction);
17591
+ const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
17592
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
17593
+ let best = null;
17594
+ for (const origin of sweepOrigins(box4, d, options2.spacing ?? 0.2)) {
17595
+ const r = raycastInternal(origin, d, distance2 + SWEEP_BACK, ignore);
17596
+ if (!r || best && r.distance >= best.distance) continue;
17597
+ best = { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: Math.max(0, r.distance - SWEEP_BACK) };
17598
+ }
17599
+ return best;
17600
+ },
17195
17601
  shoot(origin, direction, energy, caliber, source, round3) {
17196
17602
  const extras = typeof round3 === "number" ? { piercing: round3 } : round3 ?? {};
17197
17603
  return withCollapses(run(() => {
@@ -18763,7 +19169,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18763
19169
  }
18764
19170
  return spec;
18765
19171
  };
18766
- var partSpecOf = (part, capacity, detailed) => detailed && part.assembly ? assemblySpec(part, capacity) : boxSpec(part, capacity);
19172
+ var MEMBER_CAGE = { spacing: 0.1, diameter: 8e-3, unit: 0.1 };
19173
+ var CAGED_ROLES = /* @__PURE__ */ new Set(["beam", "column"]);
19174
+ var caged = (spec, part, rebar2) => {
19175
+ if (!rebar2 || part.material !== "concrete" || !CAGED_ROLES.has(part.role) || spec.reinforcement) return spec;
19176
+ const size = spec.shape.kind === "box" ? spec.shape.size : null;
19177
+ if (!size) return spec;
19178
+ const thin = Math.min(...size);
19179
+ return {
19180
+ ...spec,
19181
+ reinforcement: { material: rebar2, spacing: MEMBER_CAGE.spacing, diameter: MEMBER_CAGE.diameter },
19182
+ units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, thin], bond: "stack", mortar: "concrete" }
19183
+ };
19184
+ };
19185
+ var partSpecOf = (part, capacity, detailed, rebar2) => {
19186
+ if (detailed && part.assembly) return assemblySpec(part, capacity);
19187
+ const spec = boxSpec(part, capacity);
19188
+ return detailed ? caged(spec, part, rebar2) : spec;
19189
+ };
18767
19190
  function toDestructibleSpec(structure, options = {}) {
18768
19191
  const { safety = 0.65, redundancy = 0.97, safetyOthers = 0.45 } = options;
18769
19192
  const kept = structure.parts.filter((part) => !options.filter || options.filter(part));
@@ -18780,7 +19203,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18780
19203
  const sizing = { safety, redundancy, safetyOthers };
18781
19204
  const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts, panels.supports, sizing) };
18782
19205
  const { parts, supports, capacities } = detail === "units" ? applyUnits(sized, structure) : sized;
18783
- const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse"));
19206
+ const specs = parts.map((part) => partSpecOf(part, capacities.get(part.id), detail !== "coarse", structure.construction.frame.rebar));
18784
19207
  const margin = options.terrainMargin ?? TERRAIN_MARGIN;
18785
19208
  const x0 = Math.min(...parts.map((part) => part.aabb[0]));
18786
19209
  const z0 = Math.min(...parts.map((part) => part.aabb[1]));
@@ -26745,7 +27168,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26745
27168
  if (!pc || pc.unit) return sp;
26746
27169
  if (pc.role !== "wall" && pc.role !== "partition") {
26747
27170
  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;
27171
+ const out = def && def.reinforcement && !sp.reinforcement ? { ...sp, reinforcement: { ...def.reinforcement } } : sp;
27172
+ if (out.reinforcement && !out.units && pc.material === "concrete" && (pc.role === "beam" || pc.role === "column") && sp.shape.kind === "box") {
27173
+ return { ...out, units: { size: [MEMBER_CAGE.unit, MEMBER_CAGE.unit, Math.min(...sp.shape.size)], bond: "stack", mortar: "concrete" } };
27174
+ }
27175
+ return out;
26749
27176
  }
26750
27177
  if (!pc.assembly) return sp;
26751
27178
  const [u0, v0, u1, v1] = pc.aabb, alongU = u1 - u0 >= v1 - v0;
@@ -26780,6 +27207,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26780
27207
  pos.array.set(saved, v0 * 3);
26781
27208
  pos.needsUpdate = true;
26782
27209
  }
27210
+ function noise2(x, y, cell) {
27211
+ const h = (i2, j2) => {
27212
+ const s = Math.sin(i2 * 127.1 + j2 * 311.7) * 43758.5453;
27213
+ return s - Math.floor(s);
27214
+ };
27215
+ const fx = x / cell, fy = y / cell, i = Math.floor(fx), j = Math.floor(fy), u = fx - i, v = fy - j;
27216
+ const su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v);
27217
+ 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;
27218
+ }
27219
+ var MASK_TEXEL = 0.02;
27220
+ var SPALL_MIN = 0.025;
27221
+ var SPALL_RAGGED = 0.045;
27222
+ var COVER2 = 0.015;
26783
27223
  function installDestructionBreach(K, _shared) {
26784
27224
  const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
26785
27225
  function createBreachView(opts) {
@@ -26789,94 +27229,198 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26789
27229
  const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
26790
27230
  const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
26791
27231
  const tmp = { m: new THREE.Matrix4(), q: new THREE.Quaternion(), p: new THREE.Vector3(), s: new THREE.Vector3(), c: new THREE.Color() };
26792
- const wallColour = (items) => {
26793
- for (const it of items) {
26794
- if (!it.mesh) continue;
26795
- const c = it.mesh.geometry.attributes.color;
26796
- if (!c) continue;
26797
- const a = c.array;
26798
- tmp.c.setRGB(a[it.v0 * 3], a[it.v0 * 3 + 1], a[it.v0 * 3 + 2]);
26799
- return tmp.c.getHex();
26800
- }
26801
- return null;
26802
- };
26803
27232
  const local = (p) => {
26804
27233
  house.updateMatrixWorld(true);
26805
27234
  const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
26806
27235
  return [v.x, v.y, v.z];
26807
27236
  };
26808
- function courses(part, units, opening, chips, paint) {
27237
+ function cells(part, units) {
26809
27238
  const [u0, v0, u1, v1] = part.aabb;
26810
27239
  const alongU = u1 - u0 >= v1 - v0;
26811
27240
  const len = alongU ? u1 - u0 : v1 - v0;
26812
- const thick = alongU ? v1 - v0 : u1 - u0;
26813
27241
  const [ul, uh] = units.size;
26814
27242
  const rows = Math.max(1, Math.round((part.y1 - part.y0) / uh));
26815
27243
  const rowH = (part.y1 - part.y0) / rows;
26816
27244
  const per = Math.max(1, Math.round(len / ul));
26817
27245
  const w = len / per;
26818
- const edge = fragmentMaterial(part.material).color.getHex();
26819
- const list2 = [];
27246
+ const out = [];
26820
27247
  for (let r = 0; r < rows; r++) {
26821
27248
  const shift = units.bond === "running" && r & 1 ? w / 2 : 0;
26822
- const cells = per + (shift ? 1 : 0);
26823
- for (let i = 0; i < cells; i++) {
26824
- let a = i * w - shift, b = a + w;
26825
- a = Math.max(0, a);
26826
- b = Math.min(len, b);
26827
- if (b - a < 1e-3) continue;
26828
- const mid = (a + b) / 2;
26829
- const x = alongU ? u0 + mid : (u0 + u1) / 2;
26830
- const z = alongU ? (v0 + v1) / 2 : v0 + mid;
26831
- const y = part.y0 + (r + 0.5) * rowH;
26832
- const d = opening ? Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]) : Infinity;
26833
- if (opening && d < opening.radius) continue;
26834
- 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);
26835
- if (chips.some(gone)) continue;
26836
- const near = opening && d < opening.radius + 0.25 || chips.some((c) => Math.hypot(c[0] - x, c[1] - y, c[2] - z) < 0.15);
26837
- list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: near || paint === null ? edge : paint });
27249
+ for (let i = 0; i < per + (shift ? 1 : 0); i++) {
27250
+ const a = Math.max(0, i * w - shift), b = Math.min(len, i * w - shift + w);
27251
+ if (b - a > 1e-3) out.push({ a, b, y0: part.y0 + r * rowH, y1: part.y0 + (r + 1) * rowH, r, i });
27252
+ }
27253
+ }
27254
+ return out;
27255
+ }
27256
+ const specOf = (id) => opts.spec()?.parts?.find((q) => q.id === id);
27257
+ const same = (a, b) => a.every((x, k) => Math.abs(x - b[k]) < 1e-3);
27258
+ function hostOf(partId) {
27259
+ if (structure.meshesOf(partId).some((it) => it.mesh)) return partId;
27260
+ const part = structure.byId.get(partId);
27261
+ if (!part) return null;
27262
+ for (const q of structure.parts) {
27263
+ 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;
27264
+ }
27265
+ return null;
27266
+ }
27267
+ 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);
27268
+ function shell(it, mask) {
27269
+ const g0 = it.mesh.geometry, g = new THREE.BufferGeometry();
27270
+ const src = it.mesh.userData._p0 || g0.attributes.position.array;
27271
+ for (const [name, attr] of Object.entries(g0.attributes)) {
27272
+ const k = attr.itemSize;
27273
+ g.setAttribute(name, new THREE.BufferAttribute((name === "position" ? src : attr.array).slice(it.v0 * k, it.v1 * k), k));
27274
+ }
27275
+ if (g0.index) {
27276
+ const ix = g0.index.array.slice(it.i0, it.i1);
27277
+ for (let k = 0; k < ix.length; k++) ix[k] -= it.v0;
27278
+ g.setIndex(new THREE.BufferAttribute(ix, 1));
27279
+ }
27280
+ g.computeBoundingBox();
27281
+ g.computeBoundingSphere();
27282
+ const base = it.mesh.material;
27283
+ const material = base.clone();
27284
+ const toHouse = { value: new THREE.Matrix4() };
27285
+ material.onBeforeCompile = (sh) => {
27286
+ base.onBeforeCompile?.(sh);
27287
+ sh.uniforms.uToHouse = toHouse;
27288
+ sh.uniforms.uMask = { value: mask.texture };
27289
+ sh.uniforms.uMaskRect = { value: mask.rect };
27290
+ sh.uniforms.uMaskAlongX = { value: mask.alongU ? 1 : 0 };
27291
+ 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;");
27292
+ 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;");
27293
+ };
27294
+ material.customProgramCacheKey = () => "breachMask" + (base.customProgramCacheKey?.() ?? "");
27295
+ const mesh2 = new THREE.Mesh(g, material);
27296
+ mesh2.name = "breach-shell";
27297
+ mesh2.userData.breachMask = mask;
27298
+ mesh2.position.copy(it.mesh.position);
27299
+ mesh2.quaternion.copy(it.mesh.quaternion);
27300
+ mesh2.scale.copy(it.mesh.scale);
27301
+ mesh2.castShadow = it.mesh.castShadow;
27302
+ mesh2.receiveShadow = it.mesh.receiveShadow;
27303
+ mesh2.renderOrder = it.mesh.renderOrder;
27304
+ mesh2.onBeforeRender = () => {
27305
+ house.updateMatrixWorld();
27306
+ toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
27307
+ };
27308
+ it.mesh.parent.add(mesh2);
27309
+ mesh2.updateMatrixWorld(true);
27310
+ toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
27311
+ return mesh2;
27312
+ }
27313
+ function maskOf(hostId, parts) {
27314
+ const host = structure.byId.get(hostId);
27315
+ const [u0, v0, u1, v1] = host.aabb;
27316
+ const alongU = u1 - u0 >= v1 - v0;
27317
+ const y0 = Math.min(...parts.map((id) => structure.byId.get(id).y0)), y1 = Math.max(...parts.map((id) => structure.byId.get(id).y1));
27318
+ const len = alongU ? u1 - u0 : v1 - v0;
27319
+ 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)));
27320
+ const data = new Uint8Array(w * h);
27321
+ const texture = new THREE.DataTexture(data, w, h, THREE.RedFormat);
27322
+ texture.magFilter = THREE.LinearFilter;
27323
+ texture.minFilter = THREE.LinearFilter;
27324
+ texture.needsUpdate = true;
27325
+ 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) };
27326
+ }
27327
+ const boxDistance = (s, y, a, b, c, d) => Math.hypot(Math.max(a - s, 0, s - b), Math.max(c - y, 0, y - d));
27328
+ function paint(view) {
27329
+ const { mask } = view;
27330
+ const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
27331
+ const holes = [];
27332
+ const discs = [];
27333
+ const shown = [];
27334
+ for (const id of view.parts) {
27335
+ const part = structure.byId.get(id), units2 = specOf(id).units, chips = view.chips.get(id), opening = view.openings.get(id);
27336
+ const all = cells(part, units2);
27337
+ const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
27338
+ const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
27339
+ if (opening) discs.push({ s: along(opening.center), y: opening.center[1], r: opening.radius });
27340
+ const gone = (c) => chips.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;
27341
+ const kept = [];
27342
+ for (const c of all) (gone(c) ? holes : kept).push(c);
27343
+ for (const c of kept) shown.push({ c, part, units: units2, thick, mid });
27344
+ }
27345
+ const reach = SPALL_MIN + SPALL_RAGGED;
27346
+ mask.data.fill(0);
27347
+ const stamp = (s0, s1, y0, y1, inside2) => {
27348
+ 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));
27349
+ 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));
27350
+ for (let j = j0; j <= j1; j++) {
27351
+ const y = mask.y0 + (j + 0.5) / mask.h * mask.height;
27352
+ for (let i = i0; i <= i1; i++) {
27353
+ const s = (i + 0.5) / mask.w * mask.len;
27354
+ const edge = SPALL_MIN + SPALL_RAGGED * (0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011));
27355
+ if (inside2(s, y) <= edge) mask.data[j * mask.w + i] = 255;
27356
+ }
26838
27357
  }
27358
+ };
27359
+ 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));
27360
+ 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));
27361
+ mask.texture.needsUpdate = true;
27362
+ 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);
27363
+ const list2 = [];
27364
+ const tone = (hex3, k) => tmp.c.setHex(hex3).multiplyScalar(k).getHex();
27365
+ for (const { c, part, units: units2, thick, mid } of shown) {
27366
+ if (!near(c)) continue;
27367
+ 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;
27368
+ const at = (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 });
27369
+ const base = fragmentMaterial(part.material).color.getHex();
27370
+ const n = noise2(centre * 3.1, y * 2.7, 0.07);
27371
+ if (units2.mortar === "concrete") {
27372
+ at(len, h, thick - 2 * COVER2 - 0.03 * n, tone(base, 0.86 + 0.2 * n));
27373
+ } else {
27374
+ at(len, h, thick - 2 * COVER2 - 0.012, 10327948);
27375
+ at(len - 0.012, h - 0.012, thick - 2 * COVER2, tone(base, 0.8 + 0.35 * n));
27376
+ }
27377
+ }
27378
+ if (view.units) {
27379
+ view.units.removeFromParent();
27380
+ view.units.dispose();
26839
27381
  }
26840
- return list2;
27382
+ const units = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
27383
+ units.name = "breach:" + view.host;
27384
+ list2.forEach((u, i) => {
27385
+ tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
27386
+ units.setMatrixAt(i, tmp.m);
27387
+ units.setColorAt(i, tmp.c.setHex(u.colour));
27388
+ });
27389
+ units.count = list2.length;
27390
+ units.instanceMatrix.needsUpdate = true;
27391
+ if (units.instanceColor) units.instanceColor.needsUpdate = true;
27392
+ units.castShadow = true;
27393
+ units.receiveShadow = true;
27394
+ units.visible = lod === 0;
27395
+ house.add(units);
27396
+ view.units = units;
27397
+ view.gone = holes.length;
26841
27398
  }
26842
27399
  function draw2(partId, change) {
26843
27400
  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);
27401
+ const hostId = hostOf(partId);
27402
+ if (!hostId || !specOf(partId)?.units) return false;
27403
+ let view = views.get(hostId);
26848
27404
  if (!view) {
26849
- const items = structure.meshesOf(partId).filter((it) => it.mesh);
27405
+ const items = structure.meshesOf(hostId).filter((it) => it.mesh);
26850
27406
  if (!items.length) return false;
26851
- view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: [] };
27407
+ const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
27408
+ if (!parts.includes(partId)) return false;
27409
+ const mask = maskOf(hostId, parts);
27410
+ view = { host: hostId, parts, items, mask, shells: [], saved: null, units: null, gone: 0, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
26852
27411
  }
27412
+ if (!view.chips.has(partId)) return false;
26853
27413
  if (change.opening) {
26854
- if (view.opening) return false;
26855
- view.opening = { center: local(change.opening.center), radius: change.opening.radius };
27414
+ if (view.openings.has(partId)) return false;
27415
+ view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
26856
27416
  }
26857
- if (change.chip) view.chips.push(local(change.chip));
26858
- const list2 = courses(part, spec.units, view.opening, view.chips, view.paint);
26859
- if (view.mesh) {
26860
- view.mesh.removeFromParent();
26861
- view.mesh.dispose();
27417
+ if (change.chip) view.chips.get(partId).push(local(change.chip));
27418
+ if (!view.saved) {
27419
+ view.shells = view.items.map((it) => shell(it, view.mask));
27420
+ view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
26862
27421
  }
26863
- const mesh2 = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
26864
- mesh2.name = "breach:" + partId;
26865
- list2.forEach((u, i) => {
26866
- 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
- mesh2.setMatrixAt(i, tmp.m);
26868
- mesh2.setColorAt(i, tmp.c.setHex(u.colour));
26869
- });
26870
- mesh2.count = list2.length;
26871
- mesh2.instanceMatrix.needsUpdate = true;
26872
- if (mesh2.instanceColor) mesh2.instanceColor.needsUpdate = true;
26873
- mesh2.castShadow = true;
26874
- mesh2.receiveShadow = true;
26875
- house.add(mesh2);
26876
- if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
26877
- view.mesh = mesh2;
26878
- view.units = list2.length;
26879
- views.set(partId, view);
27422
+ paint(view);
27423
+ views.set(hostId, view);
26880
27424
  invalidate();
26881
27425
  invalidateShadow();
26882
27426
  return true;
@@ -26887,30 +27431,40 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26887
27431
  else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
26888
27432
  }
26889
27433
  }
27434
+ function dispose(view) {
27435
+ view.units.removeFromParent();
27436
+ view.units.dispose();
27437
+ for (const m of view.shells) {
27438
+ m.removeFromParent();
27439
+ m.geometry.dispose();
27440
+ m.material.dispose();
27441
+ }
27442
+ view.mask.texture.dispose();
27443
+ }
26890
27444
  return {
26891
- /** Handles a `stage` event of the house's object. Returns true when the wall was redrawn as courses. */
27445
+ /** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
26892
27446
  stage(event) {
26893
27447
  return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
26894
27448
  },
26895
- /** Handles a `chip` event: the unit that came out leaves a gap in the courses. Returns true when the wall was redrawn. */
27449
+ /** 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. */
26896
27450
  chip(event) {
26897
27451
  return draw2(event.part, { chip: event.center });
26898
27452
  },
26899
- /** The part broke or fell: the courses go (the plain range stays collapsed, the renderer hides the part). */
27453
+ /** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
26900
27454
  drop(partId) {
26901
27455
  const v = views.get(partId);
26902
27456
  if (!v) return;
26903
- v.mesh.removeFromParent();
26904
- v.mesh.dispose();
27457
+ dispose(v);
26905
27458
  views.delete(partId);
26906
27459
  },
26907
- /** LOD 0 draws the courses; LOD 1 and 2 give the wall its box back. */
27460
+ /** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
26908
27461
  setLod(level) {
26909
27462
  const was = lod;
26910
27463
  lod = level;
26911
27464
  if (was === 0 === (level === 0)) return;
26912
27465
  for (const v of views.values()) {
26913
- v.mesh.visible = level === 0;
27466
+ v.units.visible = level === 0;
27467
+ for (const m of v.shells) m.visible = level === 0;
26914
27468
  plain2(v, level !== 0);
26915
27469
  }
26916
27470
  invalidate();
@@ -26919,17 +27473,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26919
27473
  /** Everything back to what it was (after `world.reset`). */
26920
27474
  restore() {
26921
27475
  for (const v of views.values()) {
26922
- v.mesh.removeFromParent();
26923
- v.mesh.dispose();
27476
+ dispose(v);
26924
27477
  if (lod === 0) plain2(v, true);
26925
27478
  }
26926
27479
  views.clear();
26927
27480
  },
26928
- /** The wall's opening, if it has courses. */
27481
+ /** The wall has damage drawn. */
26929
27482
  has(partId) {
26930
27483
  return views.has(partId);
26931
27484
  },
26932
- statistics: () => ({ walls: views.size, units: [...views.values()].reduce((n, v) => n + v.units, 0) })
27485
+ /** Walls with damage drawn, units out of them, and units laid bare at the edge of the cuts. */
27486
+ statistics: () => ({
27487
+ walls: views.size,
27488
+ gone: [...views.values()].reduce((n, v) => n + v.gone, 0),
27489
+ units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
27490
+ })
26933
27491
  };
26934
27492
  }
26935
27493
  Object.assign(K, { createBreachView });
@@ -27929,6 +28487,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27929
28487
  return buildApi(K, shared);
27930
28488
  }
27931
28489
  var WOOD_THIN = 0.035;
28490
+ var BOARD_TOP_THICK = 0.07;
28491
+ var BOARD_TOP_WIDTH = 0.5;
28492
+ var BOARD_WIDTH = 0.16;
27932
28493
  var SHELF_SPAN = 1.5;
27933
28494
  var SHELVING = /shelf|bookcase|rack/;
27934
28495
  function carcass(parts, drawn) {
@@ -27943,6 +28504,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27943
28504
  } else if (y <= x && y <= z) part.structure = { role: "beam", maxSpan: SHELF_SPAN * Math.max(x, z) };
27944
28505
  }
27945
28506
  }
28507
+ function splitTopsIntoBoards(drawn, materialOf, carcassed) {
28508
+ const tops = /* @__PURE__ */ new Map();
28509
+ if (carcassed) return tops;
28510
+ for (let i = drawn.length - 1; i >= 0; i--) {
28511
+ const p = drawn[i];
28512
+ if (p.key !== "wood" || materialOf.wood !== "wood_hard" || p.shape.kind !== "box" || p.rotation) continue;
28513
+ const [sx, sy, sz] = p.shape.size;
28514
+ const across2 = sx < sz ? 0 : 2;
28515
+ const width = Math.min(sx, sz);
28516
+ if (sy < WOOD_THIN || sy > BOARD_TOP_THICK || sy >= width || width < BOARD_TOP_WIDTH) continue;
28517
+ const count = Math.round(width / BOARD_WIDTH);
28518
+ const boards = [];
28519
+ for (let b = 0; b < count; b++) {
28520
+ const size = [sx, sy, sz];
28521
+ size[across2] = width / count;
28522
+ const position = [...p.position];
28523
+ position[across2] += (b + 0.5) * size[across2] - width / 2;
28524
+ const min = [...p.min];
28525
+ const max = [...p.max];
28526
+ min[across2] = position[across2] - size[across2] / 2;
28527
+ max[across2] = position[across2] + size[across2] / 2;
28528
+ boards.push({ ...p, id: `${p.id}_b${b}`, shape: { kind: "box", size }, position, min, max, color: shadeBoard(p.color, b) });
28529
+ }
28530
+ drawn.splice(i, 1, ...boards);
28531
+ tops.set(p.id, boards);
28532
+ }
28533
+ return tops;
28534
+ }
28535
+ function shadeBoard(color2, board) {
28536
+ const k = [1, 0.94, 1.05, 0.97, 1.03][board % 5];
28537
+ const channel = (shift) => Math.min(255, Math.round((color2 >> shift & 255) * k)) << shift;
28538
+ return channel(16) | channel(8) | channel(0);
28539
+ }
27946
28540
  var FLOOR_TOUCH = 5e-3;
27947
28541
  var LOOSE = /* @__PURE__ */ new Set(["paper"]);
27948
28542
  var BASE_MATERIAL = {
@@ -28020,10 +28614,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
28020
28614
  };
28021
28615
  run(A);
28022
28616
  if (!drawn.length) throw new Error(`game-floorplan-three: the builder of "${name}" drew no box or cylinder`);
28617
+ const boardsOf = splitTopsIntoBoards(drawn, materialOf, Boolean(opts.carcass));
28023
28618
  const parts = drawn.map((p) => {
28024
28619
  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
28620
  let material = materialOf[p.key] ?? "plastic";
28026
- if (p.key === "wood" && material === "wood_hard" && thin < WOOD_THIN) material = "wood_soft";
28621
+ if (p.key === "wood" && material === "wood_hard" && opts.carcass && !opts.materials?.wood) material = "mdf";
28622
+ else if (p.key === "wood" && material === "wood_hard" && thin < WOOD_THIN) material = "wood_soft";
28027
28623
  const part = { id: p.id, material, shape: p.shape, position: p.position };
28028
28624
  if (p.rotation) part.rotation = p.rotation;
28029
28625
  if (!LOOSE.has(material) && p.min[1] <= FLOOR_TOUCH) part.anchored = true;
@@ -28058,6 +28654,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
28058
28654
  if (area3 < 0.6 * footprint || p.max[1] < 0.2) continue;
28059
28655
  if (!top || p.max[1] > top.y + 1e-6) top = { part: p.id, y: p.max[1] };
28060
28656
  }
28657
+ for (const boards of boardsOf.values()) {
28658
+ if (top && boards[0].max[1] <= top.y - 1e-6) continue;
28659
+ const mid = boards[boards.length >> 1];
28660
+ const min = [Math.min(...boards.map((b) => b.min[0])), Math.min(...boards.map((b) => b.min[2]))];
28661
+ const max = [Math.max(...boards.map((b) => b.max[0])), Math.max(...boards.map((b) => b.max[2]))];
28662
+ 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) };
28663
+ }
28061
28664
  if (opts.carcass) carcass(parts, byId);
28062
28665
  const spec = { name, parts, joints, tags: ["furniture"] };
28063
28666
  const colors = new Map(drawn.map((p) => [p.id, p.color]));
@@ -29047,6 +29650,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
29047
29650
  const module = {
29048
29651
  id: "items_area",
29049
29652
  name: text.name,
29653
+ ground: { surface: "tile_floor" },
29050
29654
  group: text.group,
29051
29655
  state,
29052
29656
  placed: () => entries2.map((entry2) => ({ id: entry2.id, itemId: entry2.itemId, group: entry2.group, kind: entry2.kind, size: entry2.size, root: entry2.root })),