@adatechnology/game-items-three 0.3.1 → 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
  }
@@ -12694,6 +12746,47 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12694
12746
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
12695
12747
  };
12696
12748
  var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
12749
+ var splinterBox = (inp, box4, limit = inp.maxFragments) => {
12750
+ const { rnd } = inp;
12751
+ const dims = box4.s;
12752
+ const [fmin, fmax] = inp.fragmentSize;
12753
+ const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
12754
+ const [a, b] = [0, 1, 2].filter((i) => i !== g);
12755
+ const w = Math.max(fmin * 1.5, 0.01);
12756
+ const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
12757
+ const maxLen = fmax * 4;
12758
+ const cells = bsp(box4, c, [a, b], rnd);
12759
+ const pieces = [];
12760
+ for (const cell of cells) {
12761
+ const extra = rnd() < 0.4 ? 1 : 0;
12762
+ const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
12763
+ for (const p of bsp(cell, k, [g], rnd)) pieces.push(p);
12764
+ }
12765
+ return pieces.slice(0, limit).map((p) => ({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] }));
12766
+ };
12767
+ var severAt = (inp, root, at, gap) => {
12768
+ const dims = root.s;
12769
+ const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
12770
+ const half = dims[L] / 2;
12771
+ const lo = clamp(at - gap / 2, -half, half), hi = clamp(at + gap / 2, -half, half);
12772
+ const out = [];
12773
+ const stub = (from, to) => {
12774
+ if (to - from < 0.02) return;
12775
+ const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
12776
+ c[L] = (from + to) / 2;
12777
+ sz[L] = to - from;
12778
+ out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight: sz[0] * sz[1] * sz[2] });
12779
+ };
12780
+ stub(-half, lo);
12781
+ stub(hi, half);
12782
+ const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
12783
+ band.c[L] = (lo + hi) / 2;
12784
+ band.s[L] = Math.max(hi - lo, 0.01);
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);
12788
+ return out;
12789
+ };
12697
12790
  var tearStack = (inp, root, t, u, v, n, sheet) => {
12698
12791
  const { dims, rnd, maxFragments } = inp;
12699
12792
  const layers = clamp(Math.ceil(dims[t] / STACK_LAYER), 2, 4);
@@ -12722,6 +12815,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12722
12815
  const t = sorted[0];
12723
12816
  const [u, v] = [sorted[1], sorted[2]].sort((a, b) => a - b);
12724
12817
  if (style === "snap") {
12818
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
12725
12819
  const n2 = Math.min(maxFragments, 2 + Math.floor(rnd() * 3));
12726
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] });
12727
12821
  return out;
@@ -12732,23 +12826,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12732
12826
  return out;
12733
12827
  }
12734
12828
  if (style === "splinter") {
12735
- const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
12736
- const [a, b] = [0, 1, 2].filter((i) => i !== g);
12737
- const w = Math.max(fmin * 1.5, 0.01);
12738
- const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(maxFragments / 2)));
12739
- const maxLen = fmax * 4;
12740
- const cells = bsp(root, c, [a, b], rnd);
12741
- const pieces = [];
12742
- for (const cell of cells) {
12743
- let list2 = [cell];
12744
- const extra = rnd() < 0.4 ? 1 : 0;
12745
- const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
12746
- list2 = bsp(cell, k, [g], rnd);
12747
- for (const p of list2) pieces.push(p);
12748
- }
12749
- for (const p of pieces.slice(0, maxFragments)) {
12750
- out.push({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] });
12751
- }
12829
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
12830
+ for (const p of splinterBox(inp, root)) out.push(p);
12752
12831
  return out;
12753
12832
  }
12754
12833
  if (style === "tear") {
@@ -12910,6 +12989,27 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12910
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]]));
12911
12990
  return out;
12912
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
+ };
12913
13013
  var clipBarsToDisc = (bars, u, v, center, radius) => {
12914
13014
  const out = [];
12915
13015
  for (const [a, b] of bars) {
@@ -13830,14 +13930,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13830
13930
  smoke: "grey"
13831
13931
  },
13832
13932
  visual: {
13833
- charColor: 1709072
13933
+ charColor: 1709072,
13934
+ innerColor: 14730131
13834
13935
  },
13835
13936
  tags: [
13836
13937
  "wood",
13837
13938
  "organic",
13838
13939
  "flammable"
13839
13940
  ],
13840
- compressive: 1e5
13941
+ compressive: 1e5,
13942
+ hits: {
13943
+ blunt: {
13944
+ tear: "splinters",
13945
+ tearJoules: 60
13946
+ }
13947
+ }
13841
13948
  },
13842
13949
  {
13843
13950
  id: "wood_hard",
@@ -13867,14 +13974,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13867
13974
  smoke: "grey"
13868
13975
  },
13869
13976
  visual: {
13870
- charColor: 1709072
13977
+ charColor: 1709072,
13978
+ innerColor: 13213802
13871
13979
  },
13872
13980
  tags: [
13873
13981
  "wood",
13874
13982
  "organic",
13875
13983
  "flammable"
13876
13984
  ],
13877
- compressive: 15e4
13985
+ compressive: 15e4,
13986
+ hits: {
13987
+ blunt: {
13988
+ tear: "splinters",
13989
+ tearJoules: 150
13990
+ }
13991
+ }
13878
13992
  },
13879
13993
  {
13880
13994
  id: "plywood",
@@ -13904,14 +14018,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13904
14018
  smoke: "grey"
13905
14019
  },
13906
14020
  visual: {
13907
- charColor: 1709072
14021
+ charColor: 1709072,
14022
+ innerColor: 14862234
13908
14023
  },
13909
14024
  tags: [
13910
14025
  "wood",
13911
14026
  "organic",
13912
14027
  "flammable"
13913
14028
  ],
13914
- compressive: 6e4
14029
+ compressive: 6e4,
14030
+ hits: {
14031
+ ballistic: {
14032
+ tear: "splinters"
14033
+ },
14034
+ blunt: {
14035
+ tear: "splinters",
14036
+ tearJoules: 80
14037
+ }
14038
+ }
13915
14039
  },
13916
14040
  {
13917
14041
  id: "mdf",
@@ -13938,14 +14062,25 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13938
14062
  smoke: "grey"
13939
14063
  },
13940
14064
  visual: {
13941
- charColor: 1709072
14065
+ charColor: 1709072,
14066
+ innerColor: 12096362
13942
14067
  },
13943
14068
  tags: [
13944
14069
  "wood",
13945
14070
  "organic",
13946
14071
  "flammable"
13947
14072
  ],
13948
- 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
+ }
13949
14084
  },
13950
14085
  {
13951
14086
  id: "glass",
@@ -14059,6 +14194,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14059
14194
  damageScale: {
14060
14195
  explosive: 1.2
14061
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
+ },
14062
14208
  hitDebris: "chips",
14063
14209
  breakDebris: "dust",
14064
14210
  thermal: {
@@ -14427,7 +14573,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14427
14573
  smoke: "white"
14428
14574
  },
14429
14575
  visual: {
14430
- charColor: 1709072
14576
+ charColor: 1709072,
14577
+ innerColor: 14207126
14431
14578
  },
14432
14579
  tags: [
14433
14580
  "plant",
@@ -14435,7 +14582,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14435
14582
  "organic",
14436
14583
  "flammable"
14437
14584
  ],
14438
- compressive: 6e4
14585
+ compressive: 6e4,
14586
+ hits: {
14587
+ blunt: {
14588
+ tear: "none"
14589
+ }
14590
+ }
14439
14591
  },
14440
14592
  {
14441
14593
  id: "cactus",
@@ -14550,6 +14702,130 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14550
14702
  "masonry"
14551
14703
  ],
14552
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
14553
14829
  }
14554
14830
  ];
14555
14831
  var material_hints_default = {
@@ -14723,6 +14999,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14723
14999
  return (1 + 4 * (0.3 - ductility) / 0.3) * Math.min(25, Math.max(1, (0.02 / Math.max(thickness, 1e-4)) ** 2));
14724
15000
  };
14725
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;
14726
15028
  var DMG_PER_J = 0.02;
14727
15029
  var BLAST_COUPLING = 0.1;
14728
15030
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
@@ -14736,9 +15038,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14736
15038
  var BREACH_AREA = 0.25;
14737
15039
  var BREACH_BLAST_REACH = 0.12;
14738
15040
  var RESIDUAL_CAPACITY = 0.15;
15041
+ var SEVER_BAND = 0.1;
15042
+ var SEVER_ASPECT = 3;
15043
+ var SEVER_BALLISTIC = 2;
15044
+ var TEAR_JOULES = 150;
15045
+ var tears = 0;
14739
15046
  var SUPPORT_TOLERANCE = 0.05;
14740
15047
  var UNIT_FRAGMENT_FACTOR = 4;
14741
15048
  var BREACH_GRID_CAP = 4096;
15049
+ var CHIP_ENERGY = 200;
15050
+ var CHIP_SPLASH = 0.5;
15051
+ var BAR_BOW = 0.08;
14742
15052
  var CUT_SLICE = 1;
14743
15053
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
14744
15054
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -14748,8 +15058,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14748
15058
  return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
14749
15059
  };
14750
15060
  var capacityScale = (p) => {
14751
- if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
14752
- 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);
14753
15079
  };
14754
15080
  var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
14755
15081
  var clone = (v) => structuredClone(v);
@@ -14782,6 +15108,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14782
15108
  let windCfg = windOpts ? resolveWind(windOpts) : null;
14783
15109
  const ignitionRules = resolveIgnitionRules(options.ignition);
14784
15110
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
15111
+ const chipEnergy = options.breach?.chipEnergy ?? CHIP_ENERGY;
14785
15112
  const rng3 = createRng(options.seed ?? 1);
14786
15113
  const rnd = () => rng3.next();
14787
15114
  let time = 0;
@@ -15155,41 +15482,68 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15155
15482
  markDamaged(p);
15156
15483
  if (p.rt.hinge >= flexure.failRotationDeg * Math.PI / 180) hingeDrop(o, p, src);
15157
15484
  };
15485
+ const freeSpan = (p, supports) => {
15486
+ if (supports.length === 0) return Infinity;
15487
+ const d = p.dims;
15488
+ const L = [0, 1, 2].sort((a, b) => d[b] - d[a])[0];
15489
+ const spans = supports.map((k) => contactBox(p, k)).map(([lo, hi]) => [lo[L], hi[L]]);
15490
+ let far = 0;
15491
+ for (let i = 0; i <= 16; i++) {
15492
+ const x = (i / 16 - 0.5) * d[L];
15493
+ let near = Infinity;
15494
+ for (const [lo, hi] of spans) near = Math.min(near, Math.max(lo - x, x - hi, 0));
15495
+ far = Math.max(far, near);
15496
+ }
15497
+ return 2 * far;
15498
+ };
15158
15499
  const solveStructure = (o) => {
15159
15500
  o.maxStress = 0;
15160
15501
  if (!o.hasStructure) return;
15161
15502
  const parts = o.parts;
15162
15503
  const n = parts.length;
15163
15504
  const dist = new Array(n).fill(-1);
15164
- const queue = [];
15165
15505
  const ok = (i) => parts[i].structural && !isLost(parts[i]);
15166
15506
  const isPartition = (i) => parts[i].spec.structure?.role === "partition";
15167
- for (let i = 0; i < n; i++) if (ok(i) && parts[i].anchored) {
15168
- dist[i] = 0;
15169
- queue.push(i);
15170
- }
15171
- for (let h = 0; h < queue.length; h++) {
15172
- const s = queue[h];
15173
- if (isPartition(s)) continue;
15174
- for (const j of o.byPart.get(parts[s].spec.id)) {
15175
- if (j.st.open || !j.bears) continue;
15176
- const r = j.ia === s ? j.ib : j.ia;
15177
- if (dist[r] >= 0 || !ok(r) || restsOn(o, j, r) !== s) continue;
15178
- dist[r] = dist[s] + 1;
15179
- queue.push(r);
15507
+ const out = /* @__PURE__ */ new Set();
15508
+ let queue = [];
15509
+ let lowerOf = /* @__PURE__ */ new Map();
15510
+ for (; ; ) {
15511
+ dist.fill(-1);
15512
+ queue = [];
15513
+ for (let i = 0; i < n; i++) if (ok(i) && !out.has(i) && parts[i].anchored) {
15514
+ dist[i] = 0;
15515
+ queue.push(i);
15516
+ }
15517
+ for (let h = 0; h < queue.length; h++) {
15518
+ const s = queue[h];
15519
+ if (isPartition(s)) continue;
15520
+ for (const j of o.byPart.get(parts[s].spec.id)) {
15521
+ if (j.st.open || !j.bears) continue;
15522
+ const r = j.ia === s ? j.ib : j.ia;
15523
+ if (dist[r] >= 0 || !ok(r) || out.has(r) || restsOn(o, j, r) !== s) continue;
15524
+ dist[r] = dist[s] + 1;
15525
+ queue.push(r);
15526
+ }
15180
15527
  }
15181
- }
15182
- const incoming = new Array(n).fill(0);
15183
- const lowerOf = /* @__PURE__ */ new Map();
15184
- for (const i of queue) {
15185
- const lower = [];
15186
- for (const j of o.byPart.get(parts[i].spec.id)) {
15187
- if (j.st.open || !j.bears) continue;
15188
- const k = restsOn(o, j, i);
15189
- if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
15528
+ lowerOf = /* @__PURE__ */ new Map();
15529
+ let dropped = false;
15530
+ for (const i of queue) {
15531
+ const lower = [];
15532
+ for (const j of o.byPart.get(parts[i].spec.id)) {
15533
+ if (j.st.open || !j.bears) continue;
15534
+ const k = restsOn(o, j, i);
15535
+ if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
15536
+ }
15537
+ lowerOf.set(i, lower);
15538
+ const max = parts[i].spec.structure?.maxSpan;
15539
+ if (max !== void 0 && !parts[i].anchored && freeSpan(parts[i], lower.map((k) => parts[k])) > max) {
15540
+ out.add(i);
15541
+ dropped = true;
15542
+ }
15190
15543
  }
15191
- lowerOf.set(i, lower);
15544
+ if (!dropped) break;
15192
15545
  }
15546
+ const incoming = new Array(n).fill(0);
15193
15547
  const bending = new Array(n).fill(0);
15194
15548
  const through = parts.map(() => []);
15195
15549
  const pass = (k, at, mass) => {
@@ -15701,6 +16055,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15701
16055
  const r = rayPart(o, p, origin, d);
15702
16056
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
15703
16057
  if (p.st.opening && insideOpening(o, p, vec.madd(origin, d, r.t0))) continue;
16058
+ if (p.st.chipped && insideChip(p, objLocal(o, vec.madd(origin, d, r.t0)))) continue;
15704
16059
  if (!best || r.t0 < best.distance) best = { object: o.id, part: p.spec.id, point: vec.madd(origin, d, r.t0), normal: r.n, distance: r.t0, pathLen: r.t1 - r.t0 };
15705
16060
  }
15706
16061
  }
@@ -15901,6 +16256,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15901
16256
  debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
15902
16257
  setStage(o, p, 1, {}, src);
15903
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
+ };
15904
16265
  const insideOpening = (o, p, objectPoint) => {
15905
16266
  const op = p.st.opening;
15906
16267
  if (!op) return false;
@@ -15964,6 +16325,91 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15964
16325
  if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
15965
16326
  openBreach(o, p, [sum[0] / total, sum[1] / total, sum[2] / total], total, energy, dir, src);
15966
16327
  };
16328
+ const unitGrids = /* @__PURE__ */ new WeakMap();
16329
+ const gridOf = (p) => {
16330
+ let grid = unitGrids.get(p);
16331
+ if (!grid) unitGrids.set(p, grid = unitGrid(p.dims, p.thin, unitsOf2(p), BREACH_GRID_CAP));
16332
+ return grid;
16333
+ };
16334
+ const insideChip = (p, objectPoint) => {
16335
+ const cells = p.st.chipped;
16336
+ if (!cells?.length) return false;
16337
+ const [u, v] = planeAxes(p.thin);
16338
+ const lp = toLocal(p.pos, p.q, objectPoint);
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);
16340
+ };
16341
+ const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
16342
+ var _a, _b;
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;
16348
+ const [u, v] = planeAxes(p.thin);
16349
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
16350
+ const grid = gridOf(p);
16351
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
16352
+ const out = [];
16353
+ for (let i = 0; i < grid.length; i++) {
16354
+ const cell = grid[i];
16355
+ if (worn[i] === -1) continue;
16356
+ const gap = Math.hypot(Math.max(0, Math.abs(lp[u] - cell.center[u]) - cell.size[u] / 2), Math.max(0, Math.abs(lp[v] - cell.center[v]) - cell.size[v] / 2));
16357
+ if (gap > radius) continue;
16358
+ const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
16359
+ const total = (worn[i] ?? 0) + energy * share;
16360
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
16361
+ if (total < perKg * mass) {
16362
+ worn[i] = total;
16363
+ continue;
16364
+ }
16365
+ worn[i] = -1;
16366
+ out.push(cell);
16367
+ }
16368
+ if (out.length === 0) return;
16369
+ const { c, q } = partXf(o, p);
16370
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
16371
+ const holes = localHolesOf(p);
16372
+ for (const cell of out) {
16373
+ const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
16374
+ const center = toWorld(c, q, cell.center);
16375
+ const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
16376
+ const fragments = cellFragments(o, p, [cell], wp, dir, speed, mass, `c${chipped.length}_`);
16377
+ p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
16378
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
16379
+ p.st.mass = Math.max(0, p.st.mass - mass);
16380
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
16381
+ markDamaged(p);
16382
+ setStage(o, p, 1, {}, src);
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 });
16384
+ debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
16385
+ }
16386
+ if (p.spec.reinforcement) exposeRebar(o, p, src);
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) });
16412
+ };
15967
16413
  const openBreach = (o, p, center, total, energy, dir, src) => {
15968
16414
  const [u, v] = planeAxes(p.thin);
15969
16415
  const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
@@ -15975,7 +16421,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15975
16421
  failPart(o, p, worldCenter, dir, energy, 12, src);
15976
16422
  return;
15977
16423
  }
15978
- const cells = cellsWithin(unitGrid(p.dims, p.thin, unitsOf2(p), BREACH_GRID_CAP), p.thin, lc, radius);
16424
+ const worn = p.rt.chips;
16425
+ const cells = cellsWithin(gridOf(p).filter((_, i) => worn?.[i] !== -1), p.thin, lc, radius);
15979
16426
  const removed = p.st.mass * cells.reduce((sum, cell) => sum + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
15980
16427
  const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
15981
16428
  const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
@@ -15989,10 +16436,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15989
16436
  setStage(o, p, 2, { opening: { center: worldCenter, radius }, fragments }, src);
15990
16437
  debris(o, p, p.mat.breakDebris, worldCenter, vec.scale(dir ?? [0, 1, 0], -1), 14, 3, src);
15991
16438
  const reinforcement = p.spec.reinforcement;
15992
- if (reinforcement) {
15993
- const bars = worldBars(o, p, clipBarsToDisc(rebarBars(p.dims, p.thin, reinforcement.spacing), u, v, [lc[u], lc[v]], radius + 0.05));
15994
- if (bars.length > 0) emit({ ...base(o, src), type: "expose", part: p.spec.id, bars });
15995
- }
16439
+ if (reinforcement) exposeRebar(o, p, src);
15996
16440
  if (p.structural) solveStructure(o);
15997
16441
  };
15998
16442
  const stripPart = (o, p, reinforcement, src) => {
@@ -16004,8 +16448,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16004
16448
  p.st.burning = false;
16005
16449
  p.rt.jet = false;
16006
16450
  markDamaged(p);
16007
- emit({ ...base(o, src), type: "expose", part: p.spec.id, bars: worldBars(o, p, bars) });
16008
16451
  solveStructure(o);
16452
+ exposeRebar(o, p, src);
16009
16453
  collapseCheck(o, src);
16010
16454
  };
16011
16455
  const unitFragmentPlan = (p, cap) => {
@@ -16059,6 +16503,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16059
16503
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
16060
16504
  const hitLocal = toLocal(c, q, hitW);
16061
16505
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
16506
+ const axis = style === "splinter" || style === "snap" ? severAxis(p) : -1;
16507
+ const cutAt2 = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
16062
16508
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
16063
16509
  style,
16064
16510
  dims: p.dims,
@@ -16068,7 +16514,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16068
16514
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
16069
16515
  maxFragments,
16070
16516
  rnd,
16071
- sheet: p.mat.sheet
16517
+ sheet: p.mat.sheet,
16518
+ ...cutAt2 !== void 0 ? { cut: { at: cutAt2, gap: SEVER_BAND } } : {}
16072
16519
  });
16073
16520
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
16074
16521
  const mass = p.st.mass;
@@ -16127,10 +16574,33 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16127
16574
  emit({ ...base(o, src), type: "primed", part: p.spec.id, point: partXf(o, p).c, trigger, delay });
16128
16575
  };
16129
16576
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
16577
+ const severAxis = (p) => {
16578
+ if (p.mat.fracture !== "splinter" && p.mat.fracture !== "snap" || p.spec.shape.kind !== "box") return -1;
16579
+ const d = p.dims;
16580
+ const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
16581
+ return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
16582
+ };
16583
+ const bandOf = (o, p, axis, origin) => {
16584
+ const { c, q } = partXf(o, p);
16585
+ const along = toLocal(c, q, origin)[axis];
16586
+ const L = p.dims[axis];
16587
+ return clamp2(Math.floor((along + L / 2) / SEVER_BAND), 0, Math.ceil(L / SEVER_BAND) - 1);
16588
+ };
16130
16589
  const applyDamage = (o, p, amount, type, origin, dir, fragEnergy, cap, src) => {
16590
+ var _a;
16131
16591
  if (isLost(p) || amount <= 0) return 0;
16132
16592
  const prev = p.st.hp;
16133
16593
  p.st.hp -= amount;
16594
+ const axis = origin && type !== "fire" && type !== "heat" ? severAxis(p) : -1;
16595
+ if (axis >= 0 && p.st.hp > 0) {
16596
+ const band = bandOf(o, p, axis, origin);
16597
+ const cuts = (_a = p.rt).cuts ?? (_a.cuts = {});
16598
+ cuts[band] = (cuts[band] ?? 0) + amount * (type === "ballistic" ? SEVER_BALLISTIC : 1);
16599
+ if (cuts[band] >= p.st.maxHp * SEVER_BAND / p.dims[axis]) {
16600
+ p.rt.cutAt = -p.dims[axis] / 2 + (band + 0.5) * SEVER_BAND;
16601
+ p.st.hp = 0;
16602
+ }
16603
+ }
16134
16604
  markDamaged(p);
16135
16605
  const trigger = CHAIN_TRIGGER_OF[type];
16136
16606
  const chained = trigger !== void 0 && chainDelayOf(p.spec, trigger) !== void 0;
@@ -16166,10 +16636,53 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16166
16636
  }
16167
16637
  }
16168
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
+ };
16169
16682
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
16170
16683
  const mat = p.mat;
16171
16684
  const piercing = round3.piercing ?? 1;
16172
- if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry2))) {
16685
+ if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry2)) || insideChip(p, objLocal(o, entry2))) {
16173
16686
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry2, d, Math.min(pathLen, p.thickness)), damage: 0 };
16174
16687
  }
16175
16688
  const sin = Math.min(1, Math.abs(vec.dot(d, n)));
@@ -16194,6 +16707,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16194
16707
  return { penetrated: false, exitEnergy: 0, damage: dmg2 };
16195
16708
  }
16196
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);
16197
16711
  const brittle = mat.ductility < 0.3;
16198
16712
  const ratio = Ep / Math.max(thr, 1e-6);
16199
16713
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
@@ -16227,7 +16741,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16227
16741
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
16228
16742
  debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
16229
16743
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
16230
- const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry2, d, E * 0.1, 12, 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);
16231
16748
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
16232
16749
  };
16233
16750
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -16235,14 +16752,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16235
16752
  const thrMm = mat.penetration * p.thickness * 1e3;
16236
16753
  const fragile = kind === "blunt" ? fragility(mat.ductility, p.thickness) : 1;
16237
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;
16238
16758
  if (E > 0) {
16239
16759
  if (kind === "blunt") {
16240
- 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);
16241
16761
  else if (mat.ductility < 0.3) {
16242
16762
  if (Ec >= 5) addCrack(o, p, point, n, clamp2(0.04 + Math.min(Ec, E * 6) * 8e-4, 0.04, 0.6), src);
16243
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);
16244
16764
  } else if (mat.fracture === "tear") {
16245
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);
16246
16769
  } else if (mat.ductility >= 0.5) {
16247
16770
  addDent(o, p, point, n, Math.max(contactR, 0.01), clamp2(E * 4e-5, 5e-4, 0.02), src);
16248
16771
  }
@@ -16255,6 +16778,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16255
16778
  const scale = mat.damageScale?.[kind] ?? 1;
16256
16779
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
16257
16780
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, 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);
16258
16783
  return done;
16259
16784
  };
16260
16785
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -16833,6 +17358,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16833
17358
  if (p.rt.stressT <= 0) {
16834
17359
  p.rt.stressT = 0.5;
16835
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);
16836
17362
  }
16837
17363
  applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
16838
17364
  } else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
@@ -17049,6 +17575,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17049
17575
  const r = raycastInternal(origin, direction, maxDistance);
17050
17576
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
17051
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
+ },
17052
17590
  shoot(origin, direction, energy, caliber, source, round3) {
17053
17591
  const extras = typeof round3 === "number" ? { piercing: round3 } : round3 ?? {};
17054
17592
  return withCollapses(run(() => {
@@ -18620,7 +19158,24 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18620
19158
  }
18621
19159
  return spec;
18622
19160
  };
18623
- 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
+ };
18624
19179
  function toDestructibleSpec(structure, options = {}) {
18625
19180
  const { safety = 0.65, redundancy = 0.97, safetyOthers = 0.45 } = options;
18626
19181
  const kept = structure.parts.filter((part) => !options.filter || options.filter(part));
@@ -18637,7 +19192,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
18637
19192
  const sizing = { safety, redundancy, safetyOthers };
18638
19193
  const sized = { ...panels, capacities: options.sized === false ? /* @__PURE__ */ new Map() : sizeCapacities(panels.parts, panels.supports, sizing) };
18639
19194
  const { parts, supports, capacities } = detail === "units" ? applyUnits(sized, structure) : sized;
18640
- 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));
18641
19196
  const margin = options.terrainMargin ?? TERRAIN_MARGIN;
18642
19197
  const x0 = Math.min(...parts.map((part) => part.aabb[0]));
18643
19198
  const z0 = Math.min(...parts.map((part) => part.aabb[1]));
@@ -26273,12 +26828,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26273
26828
  A.invalidate = () => {
26274
26829
  for (let i = 0; i < NS; i++) PL[i].shadow.needsUpdate = true;
26275
26830
  };
26276
- A.update = function(houses, focus, op) {
26831
+ A.update = function(houses2, focus, op) {
26277
26832
  op = op || {};
26278
- houses = (Array.isArray(houses) ? houses : [houses]).filter(Boolean);
26833
+ houses2 = (Array.isArray(houses2) ? houses2 : [houses2]).filter(Boolean);
26279
26834
  _f.set(focus.x, focus.y, focus.z);
26280
26835
  const rooms = [], exterior = [];
26281
- for (const house of houses) {
26836
+ for (const house of houses2) {
26282
26837
  if (house.group && !house.group.visible) continue;
26283
26838
  house.house.updateMatrixWorld(true);
26284
26839
  const M = house.house.matrixWorld, by = /* @__PURE__ */ new Map();
@@ -26602,7 +27157,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26602
27157
  if (!pc || pc.unit) return sp;
26603
27158
  if (pc.role !== "wall" && pc.role !== "partition") {
26604
27159
  const def = getAssembly(pc.assembly || (pc.material === "concrete" && ["slab", "beam", "column", "stair"].includes(pc.role) ? "reinforced_concrete" : ""));
26605
- 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;
26606
27165
  }
26607
27166
  if (!pc.assembly) return sp;
26608
27167
  const [u0, v0, u1, v1] = pc.aabb, alongU = u1 - u0 >= v1 - v0;
@@ -26657,7 +27216,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26657
27216
  }
26658
27217
  return null;
26659
27218
  };
26660
- function courses(part, units, opening, paint) {
27219
+ const local = (p) => {
27220
+ house.updateMatrixWorld(true);
27221
+ const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
27222
+ return [v.x, v.y, v.z];
27223
+ };
27224
+ function courses(part, units, opening, chips, paint) {
26661
27225
  const [u0, v0, u1, v1] = part.aabb;
26662
27226
  const alongU = u1 - u0 >= v1 - v0;
26663
27227
  const len = alongU ? u1 - u0 : v1 - v0;
@@ -26681,23 +27245,54 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26681
27245
  const x = alongU ? u0 + mid : (u0 + u1) / 2;
26682
27246
  const z = alongU ? (v0 + v1) / 2 : v0 + mid;
26683
27247
  const y = part.y0 + (r + 0.5) * rowH;
26684
- const d = Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]);
26685
- if (d < opening.radius) continue;
26686
- list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: d < opening.radius + 0.25 || paint === null ? edge : paint });
27248
+ const d = opening ? Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]) : Infinity;
27249
+ if (opening && d < opening.radius) continue;
27250
+ 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);
27251
+ if (chips.some(gone)) continue;
27252
+ const near = opening && d < opening.radius + 0.25 || chips.some((c) => Math.hypot(c[0] - x, c[1] - y, c[2] - z) < 0.15);
27253
+ list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: near || paint === null ? edge : paint });
26687
27254
  }
26688
27255
  }
26689
27256
  return list2;
26690
27257
  }
26691
- function open(event) {
26692
- if (lod !== 0 || views.has(event.part) || !event.opening) return false;
26693
- const part = structure.byId.get(event.part);
26694
- const spec = opts.spec()?.parts?.find((q) => q.id === event.part);
26695
- if (!part || !spec || !spec.units) return false;
26696
- const items = structure.meshesOf(event.part).filter((it) => it.mesh);
26697
- if (!items.length) return false;
26698
- const list2 = courses(part, spec.units, event.opening, wallColour(items));
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);
27270
+ function draw2(partId, change) {
27271
+ if (lod !== 0) return false;
27272
+ const hostId = hostOf(partId);
27273
+ if (!hostId || !specOf(partId)?.units) return false;
27274
+ let view = views.get(hostId);
27275
+ if (!view) {
27276
+ const items = structure.meshesOf(hostId).filter((it) => it.mesh);
27277
+ if (!items.length) return false;
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() };
27280
+ }
27281
+ if (!view.chips.has(partId)) return false;
27282
+ if (change.opening) {
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);
27286
+ }
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));
27290
+ if (view.mesh) {
27291
+ view.mesh.removeFromParent();
27292
+ view.mesh.dispose();
27293
+ }
26699
27294
  const mesh2 = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
26700
- mesh2.name = "breach:" + event.part;
27295
+ mesh2.name = "breach:" + hostId;
26701
27296
  list2.forEach((u, i) => {
26702
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));
26703
27298
  mesh2.setMatrixAt(i, tmp.m);
@@ -26709,8 +27304,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26709
27304
  mesh2.castShadow = true;
26710
27305
  mesh2.receiveShadow = true;
26711
27306
  house.add(mesh2);
26712
- const saved = items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
26713
- views.set(event.part, { mesh: mesh2, saved, units: list2.length, opening: event.opening });
27307
+ if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
27308
+ view.mesh = mesh2;
27309
+ view.units = list2.length;
27310
+ views.set(hostId, view);
26714
27311
  invalidate();
26715
27312
  invalidateShadow();
26716
27313
  return true;
@@ -26724,7 +27321,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26724
27321
  return {
26725
27322
  /** Handles a `stage` event of the house's object. Returns true when the wall was redrawn as courses. */
26726
27323
  stage(event) {
26727
- return event.stage === 2 ? open(event) : false;
27324
+ return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
27325
+ },
27326
+ /** Handles a `chip` event: the unit that came out leaves a gap in the courses. Returns true when the wall was redrawn. */
27327
+ chip(event) {
27328
+ return draw2(event.part, { chip: event.center });
26728
27329
  },
26729
27330
  /** The part broke or fell: the courses go (the plain range stays collapsed, the renderer hides the part). */
26730
27331
  drop(partId) {
@@ -26954,6 +27555,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26954
27555
  case "stage":
26955
27556
  breach.stage(e);
26956
27557
  break;
27558
+ case "chip":
27559
+ breach.chip(e);
27560
+ break;
26957
27561
  case "collapse":
26958
27562
  for (const id of e.parts) {
26959
27563
  breach.drop(id);
@@ -27008,6 +27612,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27008
27612
  if (D.active && D.world.has(D.id)) D.world.remove(D.id);
27009
27613
  D.active = false;
27010
27614
  };
27615
+ D.follow = (world, id, spec) => {
27616
+ D.spec = spec;
27617
+ const offFollow = world.on("*", (e) => {
27618
+ if (e.object !== id) return;
27619
+ switch (e.type) {
27620
+ case "stage":
27621
+ breach.stage(e);
27622
+ break;
27623
+ case "chip":
27624
+ breach.chip(e);
27625
+ break;
27626
+ case "fracture":
27627
+ breach.drop(e.part);
27628
+ break;
27629
+ case "collapse":
27630
+ for (const p of e.parts) breach.drop(p);
27631
+ break;
27632
+ default:
27633
+ break;
27634
+ }
27635
+ });
27636
+ return { restore: () => breach.restore(), dispose: () => {
27637
+ offFollow();
27638
+ breach.restore();
27639
+ } };
27640
+ };
27011
27641
  D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
27012
27642
  D.state = () => D.active ? D.world.state(D.id) : null;
27013
27643
  D.reset = () => {
@@ -27730,6 +28360,56 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27730
28360
  return buildApi(K, shared);
27731
28361
  }
27732
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;
28366
+ var SHELF_SPAN = 1.5;
28367
+ var SHELVING = /shelf|bookcase|rack/;
28368
+ function carcass(parts, drawn) {
28369
+ const all = [...drawn.values()];
28370
+ const width = Math.max(...all.map((p) => p.max[0])) - Math.min(...all.map((p) => p.min[0]));
28371
+ for (const part of parts) {
28372
+ if (LOOSE.has(part.material) || part.shape.kind !== "box" || part.rotation) continue;
28373
+ const [x, y, z] = part.shape.size;
28374
+ if (y >= x && y >= z) {
28375
+ if (z < x && x >= 0.6 * width) continue;
28376
+ part.structure = { role: "column" };
28377
+ } else if (y <= x && y <= z) part.structure = { role: "beam", maxSpan: SHELF_SPAN * Math.max(x, z) };
28378
+ }
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
+ }
27733
28413
  var FLOOR_TOUCH = 5e-3;
27734
28414
  var LOOSE = /* @__PURE__ */ new Set(["paper"]);
27735
28415
  var BASE_MATERIAL = {
@@ -27807,10 +28487,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27807
28487
  };
27808
28488
  run(A);
27809
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));
27810
28491
  const parts = drawn.map((p) => {
27811
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]);
27812
28493
  let material = materialOf[p.key] ?? "plastic";
27813
- 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";
27814
28496
  const part = { id: p.id, material, shape: p.shape, position: p.position };
27815
28497
  if (p.rotation) part.rotation = p.rotation;
27816
28498
  if (!LOOSE.has(material) && p.min[1] <= FLOOR_TOUCH) part.anchored = true;
@@ -27845,6 +28527,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27845
28527
  if (area3 < 0.6 * footprint || p.max[1] < 0.2) continue;
27846
28528
  if (!top || p.max[1] > top.y + 1e-6) top = { part: p.id, y: p.max[1] };
27847
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
+ }
28537
+ if (opts.carcass) carcass(parts, byId);
27848
28538
  const spec = { name, parts, joints, tags: ["furniture"] };
27849
28539
  const colors = new Map(drawn.map((p) => [p.id, p.color]));
27850
28540
  const build = (T3) => {
@@ -27888,7 +28578,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27888
28578
  if (w === void 0 || d === void 0 || h === void 0) throw new Error(`game-floorplan-three: furniture type "${type}" is not in the catalogue, so give w, d and h`);
27889
28579
  const o = opts.options ?? {};
27890
28580
  const seed = Math.round(opts.seed ?? 1);
27891
- const plain2 = /shelf|bookcase|rack/.test(type) ? "paper" : "plastic";
28581
+ const plain2 = SHELVING.test(type) ? "paper" : "plastic";
27892
28582
  const root = { x: seed, z: 0, ry: 0, w, d, h, y: 0, base: 0, palette: o.palette, seed: `furniture_${type}_${seed}` };
27893
28583
  const { K, shared } = renderer.internals;
27894
28584
  const before = K.PARTS.length;
@@ -27907,7 +28597,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27907
28597
  shared.ctx = saved.ctx;
27908
28598
  }
27909
28599
  },
27910
- { materials: { plain: plain2, ...o.materials }, chairs: o.chairs }
28600
+ { materials: { plain: plain2, ...o.materials }, chairs: o.chairs, carcass: SHELVING.test(type) }
27911
28601
  );
27912
28602
  const spouts = K.PARTS.slice(before).filter((p) => p.type === "water_tap").map((p) => [p.pos[0] - root.x, p.pos[1], p.pos[2] - root.z]);
27913
28603
  K.PARTS.length = before;
@@ -28833,6 +29523,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
28833
29523
  const module = {
28834
29524
  id: "items_area",
28835
29525
  name: text.name,
29526
+ ground: { surface: "tile_floor" },
28836
29527
  group: text.group,
28837
29528
  state,
28838
29529
  placed: () => entries2.map((entry2) => ({ id: entry2.id, itemId: entry2.itemId, group: entry2.group, kind: entry2.kind, size: entry2.size, root: entry2.root })),