@adatechnology/game-floorplan-three 0.4.1 → 0.5.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.
package/dist/index.d.ts CHANGED
@@ -599,7 +599,8 @@ interface FurnitureSpecResult {
599
599
  * Materials: `wood` is `wood_hard`, and `wood_soft` for thin boards and panels (under 3.5 cm); `metal` is `steel`; `fabric`, `glass` and `plant` keep
600
600
  * their name; `plain` is `plastic`, or `paper` for shelves and racks (their books and boxes). Parts that touch the floor are anchored. Joints come from
601
601
  * `inferJoints` (screws between wood, welds or bolts between metal...), except that paper rests on what it lies on with a `resting` joint (weak: it falls when
602
- * the board breaks) and is not joined to its neighbours.
602
+ * the board breaks) and is not joined to its neighbours. Shelves and racks stand as a carcass: the sides carry, each shelf needs both of its ends
603
+ * held, and the back panel carries nothing, so a broken side drops the shelves it held, with their books.
603
604
  *
604
605
  * @example
605
606
  * const { spec, build, top } = furnitureSpec(THREE, 'bookcase') // 1.2 x 0.35 x 2 m, boards and books
@@ -648,6 +649,15 @@ interface WallPiece {
648
649
  readonly spec: any;
649
650
  /** Draws the wall with the house renderer: meshes carry `userData.destructionPart` of their part, in the space of the spec. */
650
651
  build(THREE: any): any;
652
+ /**
653
+ * Once a drawing from `build` is in a destruction world as `objectId` (with this `spec`), the house renderer follows its events and draws on it what
654
+ * a generic destruction renderer cannot: the bricks that shots and blows knock out (`chip`) and the opening of a breach, as gaps in the wall's
655
+ * courses. `restore` puts the wall back (after `world.reset`), `dispose` stops following.
656
+ */
657
+ follow(root: any, world: any, objectId: string): {
658
+ restore(): void;
659
+ dispose(): void;
660
+ } | null;
651
661
  }
652
662
  /**
653
663
  * The plan of a free-standing wall along x, centred on the origin: a one-storey plan whose only wall is exterior on both sides, with the masonry construction of
@@ -3551,8 +3551,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
3551
3551
  var OPEN_ROOM_TYPES = /* @__PURE__ */ new Set(["balcony", "terrace", "veranda", "balcony_gourmet"]);
3552
3552
  var rectsOf = (c) => c.parts && c.parts.length ? c.parts : [[c.u0, c.v0, c.u1, c.v1]];
3553
3553
  var isOpenArea = (c) => OPEN_ROOM_TYPES.has(c.type) || c.outdoor === true;
3554
- function planLod(houses, focus, budget) {
3555
- const l = houses.map((c) => ({ c, d: c.distance(focus), n0: c.stats.drawCalls, n1: c.stats.drawCallsLOD1, level: 0 })).sort((a, b) => a.d - b.d);
3554
+ function planLod(houses2, focus, budget) {
3555
+ const l = houses2.map((c) => ({ c, d: c.distance(focus), n0: c.stats.drawCalls, n1: c.stats.drawCallsLOD1, level: 0 })).sort((a, b) => a.d - b.d);
3556
3556
  let total = l.reduce((s, x) => s + x.n0, 0);
3557
3557
  for (let i = l.length - 1; i >= 0 && total > budget; i--) {
3558
3558
  total -= l[i].n0 - l[i].n1;
@@ -10695,12 +10695,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
10695
10695
  A.invalidate = () => {
10696
10696
  for (let i = 0; i < NS; i++) PL[i].shadow.needsUpdate = true;
10697
10697
  };
10698
- A.update = function(houses, focus, op) {
10698
+ A.update = function(houses2, focus, op) {
10699
10699
  op = op || {};
10700
- houses = (Array.isArray(houses) ? houses : [houses]).filter(Boolean);
10700
+ houses2 = (Array.isArray(houses2) ? houses2 : [houses2]).filter(Boolean);
10701
10701
  _f.set(focus.x, focus.y, focus.z);
10702
10702
  const rooms = [], exterior = [];
10703
- for (const house of houses) {
10703
+ for (const house of houses2) {
10704
10704
  if (house.group && !house.group.visible) continue;
10705
10705
  house.house.updateMatrixWorld(true);
10706
10706
  const M = house.house.matrixWorld, by = /* @__PURE__ */ new Map();
@@ -11232,6 +11232,62 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11232
11232
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
11233
11233
  };
11234
11234
  var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
11235
+ var splinterBox = (inp, box3, limit = inp.maxFragments) => {
11236
+ const { rnd } = inp;
11237
+ const dims = box3.s;
11238
+ const [fmin, fmax] = inp.fragmentSize;
11239
+ const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
11240
+ const [a, b] = [0, 1, 2].filter((i) => i !== g);
11241
+ const w = Math.max(fmin * 1.5, 0.01);
11242
+ const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
11243
+ const maxLen = fmax * 4;
11244
+ const cells = bsp(box3, c, [a, b], rnd);
11245
+ const pieces = [];
11246
+ for (const cell of cells) {
11247
+ const extra = rnd() < 0.4 ? 1 : 0;
11248
+ const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
11249
+ for (const p of bsp(cell, k, [g], rnd)) pieces.push(p);
11250
+ }
11251
+ 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] }));
11252
+ };
11253
+ var severAt = (inp, root, at, gap) => {
11254
+ const dims = root.s;
11255
+ const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
11256
+ const half = dims[L] / 2;
11257
+ const lo = clamp(at - gap / 2, -half, half), hi = clamp(at + gap / 2, -half, half);
11258
+ const out = [];
11259
+ const stub = (from, to) => {
11260
+ if (to - from < 0.02) return;
11261
+ const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
11262
+ c[L] = (from + to) / 2;
11263
+ sz[L] = to - from;
11264
+ out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight: sz[0] * sz[1] * sz[2] });
11265
+ };
11266
+ stub(-half, lo);
11267
+ stub(hi, half);
11268
+ const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
11269
+ band.c[L] = (lo + hi) / 2;
11270
+ band.s[L] = Math.max(hi - lo, 0.01);
11271
+ for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
11272
+ return out;
11273
+ };
11274
+ var tearStack = (inp, root, t, u, v, n, sheet) => {
11275
+ const { dims, rnd, maxFragments } = inp;
11276
+ const layers = clamp(Math.ceil(dims[t] / STACK_LAYER), 2, 4);
11277
+ const per = clamp(n, 3, Math.max(3, Math.floor(maxFragments / layers)));
11278
+ const out = [];
11279
+ for (let k = 0; k < layers; k++) {
11280
+ const off = -dims[t] / 2 + (k + 0.5) * dims[t] / layers;
11281
+ for (const b of bsp(root, per, [u, v], rnd)) {
11282
+ if (out.length >= maxFragments) return out;
11283
+ const c = [b.c[0], b.c[1], b.c[2]];
11284
+ c[t] = off;
11285
+ out.push({ center: c, size: [b.s[u], b.s[v], sheet], q: frame(u, v, t, (rnd() - 0.5) * 0.8), shape: "scrap", weight: b.s[u] * b.s[v], inner: out.length % 4 !== 0 });
11286
+ }
11287
+ }
11288
+ return out;
11289
+ };
11290
+ var STACK_LAYER = 0.012;
11235
11291
  var planFragments = (inp) => {
11236
11292
  const { style, dims, rnd, maxFragments } = inp;
11237
11293
  const [fmin, fmax] = inp.fragmentSize;
@@ -11253,27 +11309,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11253
11309
  return out;
11254
11310
  }
11255
11311
  if (style === "splinter") {
11256
- const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
11257
- const [a, b] = [0, 1, 2].filter((i) => i !== g);
11258
- const w = Math.max(fmin * 1.5, 0.01);
11259
- const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(maxFragments / 2)));
11260
- const maxLen = fmax * 4;
11261
- const cells = bsp(root, c, [a, b], rnd);
11262
- const pieces = [];
11263
- for (const cell of cells) {
11264
- let list2 = [cell];
11265
- const extra = rnd() < 0.4 ? 1 : 0;
11266
- const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
11267
- list2 = bsp(cell, k, [g], rnd);
11268
- for (const p of list2) pieces.push(p);
11269
- }
11270
- for (const p of pieces.slice(0, maxFragments)) {
11271
- 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] });
11272
- }
11312
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
11313
+ for (const p of splinterBox(inp, root)) out.push(p);
11273
11314
  return out;
11274
11315
  }
11275
11316
  if (style === "tear") {
11276
11317
  const n2 = clamp(Math.round(dims[u] * dims[v] / (mean * mean)), 3, maxFragments);
11318
+ const sheet = inp.sheet ?? 0;
11319
+ if (sheet > 0 && dims[t] > sheet * 4) return tearStack(inp, root, t, u, v, n2, sheet);
11277
11320
  const th2 = Math.min(inp.thickness, dims[t]);
11278
11321
  for (const b of bsp(root, n2, [u, v], rnd)) {
11279
11322
  out.push({ center: [b.c[0], b.c[1], b.c[2]], size: [b.s[u], b.s[v], th2], q: frame(u, v, t), shape: "scrap", weight: b.s[u] * b.s[v] });
@@ -12856,6 +12899,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12856
12899
  0.03,
12857
12900
  0.15
12858
12901
  ],
12902
+ sheet: 1e-3,
12859
12903
  damageScale: {
12860
12904
  slash: 3
12861
12905
  },
@@ -12871,7 +12915,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12871
12915
  smoke: "grey"
12872
12916
  },
12873
12917
  visual: {
12874
- charColor: 1709072
12918
+ charColor: 1709072,
12919
+ innerColor: 15524048
12875
12920
  },
12876
12921
  tags: [
12877
12922
  "fabric",
@@ -13253,9 +13298,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13253
13298
  var BREACH_AREA = 0.25;
13254
13299
  var BREACH_BLAST_REACH = 0.12;
13255
13300
  var RESIDUAL_CAPACITY = 0.15;
13301
+ var SEVER_BAND = 0.1;
13302
+ var SEVER_ASPECT = 3;
13303
+ var SEVER_BALLISTIC = 2;
13256
13304
  var SUPPORT_TOLERANCE = 0.05;
13257
13305
  var UNIT_FRAGMENT_FACTOR = 4;
13258
13306
  var BREACH_GRID_CAP = 4096;
13307
+ var CHIP_ENERGY = 200;
13308
+ var CHIP_SPLASH = 0.5;
13259
13309
  var CUT_SLICE = 1;
13260
13310
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
13261
13311
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -13299,6 +13349,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13299
13349
  let windCfg = windOpts ? resolveWind(windOpts) : null;
13300
13350
  const ignitionRules = resolveIgnitionRules(options.ignition);
13301
13351
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
13352
+ const chipEnergy = options.breach?.chipEnergy ?? CHIP_ENERGY;
13302
13353
  const rng2 = createRng(options.seed ?? 1);
13303
13354
  const rnd = () => rng2.next();
13304
13355
  let time = 0;
@@ -13672,41 +13723,68 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13672
13723
  markDamaged(p);
13673
13724
  if (p.rt.hinge >= flexure.failRotationDeg * Math.PI / 180) hingeDrop(o, p, src);
13674
13725
  };
13726
+ const freeSpan = (p, supports) => {
13727
+ if (supports.length === 0) return Infinity;
13728
+ const d = p.dims;
13729
+ const L = [0, 1, 2].sort((a, b) => d[b] - d[a])[0];
13730
+ const spans = supports.map((k) => contactBox(p, k)).map(([lo, hi]) => [lo[L], hi[L]]);
13731
+ let far = 0;
13732
+ for (let i = 0; i <= 16; i++) {
13733
+ const x = (i / 16 - 0.5) * d[L];
13734
+ let near = Infinity;
13735
+ for (const [lo, hi] of spans) near = Math.min(near, Math.max(lo - x, x - hi, 0));
13736
+ far = Math.max(far, near);
13737
+ }
13738
+ return 2 * far;
13739
+ };
13675
13740
  const solveStructure = (o) => {
13676
13741
  o.maxStress = 0;
13677
13742
  if (!o.hasStructure) return;
13678
13743
  const parts = o.parts;
13679
13744
  const n = parts.length;
13680
13745
  const dist = new Array(n).fill(-1);
13681
- const queue = [];
13682
13746
  const ok = (i) => parts[i].structural && !isLost(parts[i]);
13683
13747
  const isPartition = (i) => parts[i].spec.structure?.role === "partition";
13684
- for (let i = 0; i < n; i++) if (ok(i) && parts[i].anchored) {
13685
- dist[i] = 0;
13686
- queue.push(i);
13687
- }
13688
- for (let h = 0; h < queue.length; h++) {
13689
- const s = queue[h];
13690
- if (isPartition(s)) continue;
13691
- for (const j of o.byPart.get(parts[s].spec.id)) {
13692
- if (j.st.open || !j.bears) continue;
13693
- const r = j.ia === s ? j.ib : j.ia;
13694
- if (dist[r] >= 0 || !ok(r) || restsOn(o, j, r) !== s) continue;
13695
- dist[r] = dist[s] + 1;
13696
- queue.push(r);
13748
+ const out = /* @__PURE__ */ new Set();
13749
+ let queue = [];
13750
+ let lowerOf = /* @__PURE__ */ new Map();
13751
+ for (; ; ) {
13752
+ dist.fill(-1);
13753
+ queue = [];
13754
+ for (let i = 0; i < n; i++) if (ok(i) && !out.has(i) && parts[i].anchored) {
13755
+ dist[i] = 0;
13756
+ queue.push(i);
13757
+ }
13758
+ for (let h = 0; h < queue.length; h++) {
13759
+ const s = queue[h];
13760
+ if (isPartition(s)) continue;
13761
+ for (const j of o.byPart.get(parts[s].spec.id)) {
13762
+ if (j.st.open || !j.bears) continue;
13763
+ const r = j.ia === s ? j.ib : j.ia;
13764
+ if (dist[r] >= 0 || !ok(r) || out.has(r) || restsOn(o, j, r) !== s) continue;
13765
+ dist[r] = dist[s] + 1;
13766
+ queue.push(r);
13767
+ }
13697
13768
  }
13698
- }
13699
- const incoming = new Array(n).fill(0);
13700
- const lowerOf = /* @__PURE__ */ new Map();
13701
- for (const i of queue) {
13702
- const lower = [];
13703
- for (const j of o.byPart.get(parts[i].spec.id)) {
13704
- if (j.st.open || !j.bears) continue;
13705
- const k = restsOn(o, j, i);
13706
- if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
13769
+ lowerOf = /* @__PURE__ */ new Map();
13770
+ let dropped = false;
13771
+ for (const i of queue) {
13772
+ const lower = [];
13773
+ for (const j of o.byPart.get(parts[i].spec.id)) {
13774
+ if (j.st.open || !j.bears) continue;
13775
+ const k = restsOn(o, j, i);
13776
+ if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
13777
+ }
13778
+ lowerOf.set(i, lower);
13779
+ const max = parts[i].spec.structure?.maxSpan;
13780
+ if (max !== void 0 && !parts[i].anchored && freeSpan(parts[i], lower.map((k) => parts[k])) > max) {
13781
+ out.add(i);
13782
+ dropped = true;
13783
+ }
13707
13784
  }
13708
- lowerOf.set(i, lower);
13785
+ if (!dropped) break;
13709
13786
  }
13787
+ const incoming = new Array(n).fill(0);
13710
13788
  const bending = new Array(n).fill(0);
13711
13789
  const through = parts.map(() => []);
13712
13790
  const pass = (k, at, mass) => {
@@ -14218,6 +14296,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14218
14296
  const r = rayPart(o, p, origin, d);
14219
14297
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
14220
14298
  if (p.st.opening && insideOpening(o, p, vec.madd(origin, d, r.t0))) continue;
14299
+ if (p.st.chipped && insideChip(p, objLocal(o, vec.madd(origin, d, r.t0)))) continue;
14221
14300
  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 };
14222
14301
  }
14223
14302
  }
@@ -14481,6 +14560,62 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14481
14560
  if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
14482
14561
  openBreach(o, p, [sum[0] / total, sum[1] / total, sum[2] / total], total, energy, dir, src);
14483
14562
  };
14563
+ const unitGrids = /* @__PURE__ */ new WeakMap();
14564
+ const gridOf = (p) => {
14565
+ let grid = unitGrids.get(p);
14566
+ if (!grid) unitGrids.set(p, grid = unitGrid(p.dims, p.thin, unitsOf2(p), BREACH_GRID_CAP));
14567
+ return grid;
14568
+ };
14569
+ const insideChip = (p, objectPoint) => {
14570
+ const cells = p.st.chipped;
14571
+ if (!cells?.length) return false;
14572
+ const [u, v] = planeAxes(p.thin);
14573
+ const lp = toLocal(p.pos, p.q, objectPoint);
14574
+ 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);
14575
+ };
14576
+ const chipUnits = (o, p, wp, energy, dir, radius, src) => {
14577
+ var _a, _b;
14578
+ if (!p.spec.units || p.spec.reinforcement || !p.staged || chipEnergy <= 0 || energy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
14579
+ const [u, v] = planeAxes(p.thin);
14580
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
14581
+ const grid = gridOf(p);
14582
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
14583
+ const out = [];
14584
+ for (let i = 0; i < grid.length; i++) {
14585
+ const cell = grid[i];
14586
+ if (worn[i] === -1) continue;
14587
+ 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));
14588
+ if (gap > radius) continue;
14589
+ const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
14590
+ const total = (worn[i] ?? 0) + energy * share;
14591
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
14592
+ if (total < chipEnergy * mass) {
14593
+ worn[i] = total;
14594
+ continue;
14595
+ }
14596
+ worn[i] = -1;
14597
+ out.push(cell);
14598
+ }
14599
+ if (out.length === 0) return;
14600
+ const { c, q } = partXf(o, p);
14601
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
14602
+ const holes = localHolesOf(p);
14603
+ for (const cell of out) {
14604
+ const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
14605
+ const center = toWorld(c, q, cell.center);
14606
+ const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
14607
+ const fragments = cellFragments(o, p, [cell], wp, dir, speed, mass, `c${chipped.length}_`);
14608
+ p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
14609
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
14610
+ p.st.mass = Math.max(0, p.st.mass - mass);
14611
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
14612
+ markDamaged(p);
14613
+ setStage(o, p, 1, {}, src);
14614
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
14615
+ debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
14616
+ }
14617
+ for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
14618
+ };
14484
14619
  const openBreach = (o, p, center, total, energy, dir, src) => {
14485
14620
  const [u, v] = planeAxes(p.thin);
14486
14621
  const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
@@ -14492,7 +14627,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14492
14627
  failPart(o, p, worldCenter, dir, energy, 12, src);
14493
14628
  return;
14494
14629
  }
14495
- const cells = cellsWithin(unitGrid(p.dims, p.thin, unitsOf2(p), BREACH_GRID_CAP), p.thin, lc, radius);
14630
+ const worn = p.rt.chips;
14631
+ const cells = cellsWithin(gridOf(p).filter((_, i) => worn?.[i] !== -1), p.thin, lc, radius);
14496
14632
  const removed = p.st.mass * cells.reduce((sum, cell) => sum + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
14497
14633
  const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
14498
14634
  const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
@@ -14576,6 +14712,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14576
14712
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
14577
14713
  const hitLocal = toLocal(c, q, hitW);
14578
14714
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
14715
+ const axis = style === "splinter" ? severAxis(p) : -1;
14716
+ const cutAt2 = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
14579
14717
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
14580
14718
  style,
14581
14719
  dims: p.dims,
@@ -14584,7 +14722,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14584
14722
  hit: hitLocal,
14585
14723
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
14586
14724
  maxFragments,
14587
- rnd
14725
+ rnd,
14726
+ sheet: p.mat.sheet,
14727
+ ...cutAt2 !== void 0 ? { cut: { at: cutAt2, gap: SEVER_BAND } } : {}
14588
14728
  });
14589
14729
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
14590
14730
  const mass = p.st.mass;
@@ -14608,7 +14748,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14608
14748
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
14609
14749
  angularVelocity: [(rnd() - 0.5) * 12, (rnd() - 0.5) * 12, (rnd() - 0.5) * 12],
14610
14750
  mass: mass * f.weight / totW,
14611
- ...marks.length > 0 ? { marks } : {}
14751
+ ...marks.length > 0 ? { marks } : {},
14752
+ ...f.inner ? { inner: true } : {}
14612
14753
  };
14613
14754
  });
14614
14755
  const reinforcement = p.spec.reinforcement;
@@ -14642,10 +14783,33 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14642
14783
  emit({ ...base(o, src), type: "primed", part: p.spec.id, point: partXf(o, p).c, trigger, delay });
14643
14784
  };
14644
14785
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
14786
+ const severAxis = (p) => {
14787
+ if (p.mat.fracture !== "splinter" || p.spec.shape.kind !== "box") return -1;
14788
+ const d = p.dims;
14789
+ const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
14790
+ return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
14791
+ };
14792
+ const bandOf = (o, p, axis, origin) => {
14793
+ const { c, q } = partXf(o, p);
14794
+ const along = toLocal(c, q, origin)[axis];
14795
+ const L = p.dims[axis];
14796
+ return clamp2(Math.floor((along + L / 2) / SEVER_BAND), 0, Math.ceil(L / SEVER_BAND) - 1);
14797
+ };
14645
14798
  const applyDamage = (o, p, amount, type, origin, dir, fragEnergy, cap, src) => {
14799
+ var _a;
14646
14800
  if (isLost(p) || amount <= 0) return 0;
14647
14801
  const prev = p.st.hp;
14648
14802
  p.st.hp -= amount;
14803
+ const axis = origin && type !== "fire" && type !== "heat" ? severAxis(p) : -1;
14804
+ if (axis >= 0 && p.st.hp > 0) {
14805
+ const band = bandOf(o, p, axis, origin);
14806
+ const cuts = (_a = p.rt).cuts ?? (_a.cuts = {});
14807
+ cuts[band] = (cuts[band] ?? 0) + amount * (type === "ballistic" ? SEVER_BALLISTIC : 1);
14808
+ if (cuts[band] >= p.st.maxHp * SEVER_BAND / p.dims[axis]) {
14809
+ p.rt.cutAt = -p.dims[axis] / 2 + (band + 0.5) * SEVER_BAND;
14810
+ p.st.hp = 0;
14811
+ }
14812
+ }
14649
14813
  markDamaged(p);
14650
14814
  const trigger = CHAIN_TRIGGER_OF[type];
14651
14815
  const chained = trigger !== void 0 && chainDelayOf(p.spec, trigger) !== void 0;
@@ -14684,7 +14848,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14684
14848
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
14685
14849
  const mat = p.mat;
14686
14850
  const piercing = round3.piercing ?? 1;
14687
- if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry2))) {
14851
+ if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry2)) || insideChip(p, objLocal(o, entry2))) {
14688
14852
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry2, d, Math.min(pathLen, p.thickness)), damage: 0 };
14689
14853
  }
14690
14854
  const sin = Math.min(1, Math.abs(vec.dot(d, n)));
@@ -14743,6 +14907,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14743
14907
  debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
14744
14908
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
14745
14909
  const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry2, d, E * 0.1, 12, src);
14910
+ chipUnits(o, p, entry2, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
14746
14911
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
14747
14912
  };
14748
14913
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -14770,6 +14935,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14770
14935
  const scale = mat.damageScale?.[kind] ?? 1;
14771
14936
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
14772
14937
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
14938
+ if (kind === "blunt") chipUnits(o, p, point, E, d, Math.max(contactR, 0.03), src);
14773
14939
  return done;
14774
14940
  };
14775
14941
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -19083,7 +19249,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19083
19249
  }
19084
19250
  return null;
19085
19251
  };
19086
- function courses(part, units, opening, paint) {
19252
+ const local = (p) => {
19253
+ house.updateMatrixWorld(true);
19254
+ const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
19255
+ return [v.x, v.y, v.z];
19256
+ };
19257
+ function courses(part, units, opening, chips, paint) {
19087
19258
  const [u0, v0, u1, v1] = part.aabb;
19088
19259
  const alongU = u1 - u0 >= v1 - v0;
19089
19260
  const len = alongU ? u1 - u0 : v1 - v0;
@@ -19107,23 +19278,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19107
19278
  const x = alongU ? u0 + mid : (u0 + u1) / 2;
19108
19279
  const z = alongU ? (v0 + v1) / 2 : v0 + mid;
19109
19280
  const y = part.y0 + (r + 0.5) * rowH;
19110
- const d = Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]);
19111
- if (d < opening.radius) continue;
19112
- 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 });
19281
+ const d = opening ? Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]) : Infinity;
19282
+ if (opening && d < opening.radius) continue;
19283
+ 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);
19284
+ if (chips.some(gone)) continue;
19285
+ const near = opening && d < opening.radius + 0.25 || chips.some((c) => Math.hypot(c[0] - x, c[1] - y, c[2] - z) < 0.15);
19286
+ list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: near || paint === null ? edge : paint });
19113
19287
  }
19114
19288
  }
19115
19289
  return list2;
19116
19290
  }
19117
- function open(event) {
19118
- if (lod !== 0 || views.has(event.part) || !event.opening) return false;
19119
- const part = structure.byId.get(event.part);
19120
- const spec = opts.spec()?.parts?.find((q) => q.id === event.part);
19291
+ function draw2(partId, change) {
19292
+ if (lod !== 0) return false;
19293
+ const part = structure.byId.get(partId);
19294
+ const spec = opts.spec()?.parts?.find((q) => q.id === partId);
19121
19295
  if (!part || !spec || !spec.units) return false;
19122
- const items = structure.meshesOf(event.part).filter((it) => it.mesh);
19123
- if (!items.length) return false;
19124
- const list2 = courses(part, spec.units, event.opening, wallColour(items));
19296
+ let view = views.get(partId);
19297
+ if (!view) {
19298
+ const items = structure.meshesOf(partId).filter((it) => it.mesh);
19299
+ if (!items.length) return false;
19300
+ view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: [] };
19301
+ }
19302
+ if (change.opening) {
19303
+ if (view.opening) return false;
19304
+ view.opening = { center: local(change.opening.center), radius: change.opening.radius };
19305
+ }
19306
+ if (change.chip) view.chips.push(local(change.chip));
19307
+ const list2 = courses(part, spec.units, view.opening, view.chips, view.paint);
19308
+ if (view.mesh) {
19309
+ view.mesh.removeFromParent();
19310
+ view.mesh.dispose();
19311
+ }
19125
19312
  const mesh2 = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
19126
- mesh2.name = "breach:" + event.part;
19313
+ mesh2.name = "breach:" + partId;
19127
19314
  list2.forEach((u, i) => {
19128
19315
  tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
19129
19316
  mesh2.setMatrixAt(i, tmp.m);
@@ -19135,8 +19322,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19135
19322
  mesh2.castShadow = true;
19136
19323
  mesh2.receiveShadow = true;
19137
19324
  house.add(mesh2);
19138
- const saved = items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
19139
- views.set(event.part, { mesh: mesh2, saved, units: list2.length, opening: event.opening });
19325
+ if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
19326
+ view.mesh = mesh2;
19327
+ view.units = list2.length;
19328
+ views.set(partId, view);
19140
19329
  invalidate();
19141
19330
  invalidateShadow();
19142
19331
  return true;
@@ -19150,7 +19339,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19150
19339
  return {
19151
19340
  /** Handles a `stage` event of the house's object. Returns true when the wall was redrawn as courses. */
19152
19341
  stage(event) {
19153
- return event.stage === 2 ? open(event) : false;
19342
+ return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
19343
+ },
19344
+ /** Handles a `chip` event: the unit that came out leaves a gap in the courses. Returns true when the wall was redrawn. */
19345
+ chip(event) {
19346
+ return draw2(event.part, { chip: event.center });
19154
19347
  },
19155
19348
  /** The part broke or fell: the courses go (the plain range stays collapsed, the renderer hides the part). */
19156
19349
  drop(partId) {
@@ -19382,6 +19575,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19382
19575
  case "stage":
19383
19576
  breach.stage(e);
19384
19577
  break;
19578
+ case "chip":
19579
+ breach.chip(e);
19580
+ break;
19385
19581
  case "collapse":
19386
19582
  for (const id of e.parts) {
19387
19583
  breach.drop(id);
@@ -19436,6 +19632,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19436
19632
  if (D.active && D.world.has(D.id)) D.world.remove(D.id);
19437
19633
  D.active = false;
19438
19634
  };
19635
+ D.follow = (world, id, spec) => {
19636
+ D.spec = spec;
19637
+ const offFollow = world.on("*", (e) => {
19638
+ if (e.object !== id) return;
19639
+ switch (e.type) {
19640
+ case "stage":
19641
+ breach.stage(e);
19642
+ break;
19643
+ case "chip":
19644
+ breach.chip(e);
19645
+ break;
19646
+ case "fracture":
19647
+ breach.drop(e.part);
19648
+ break;
19649
+ case "collapse":
19650
+ for (const p of e.parts) breach.drop(p);
19651
+ break;
19652
+ default:
19653
+ break;
19654
+ }
19655
+ });
19656
+ return { restore: () => breach.restore(), dispose: () => {
19657
+ offFollow();
19658
+ breach.restore();
19659
+ } };
19660
+ };
19439
19661
  D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
19440
19662
  D.state = () => D.active ? D.world.state(D.id) : null;
19441
19663
  D.reset = () => {
@@ -22557,6 +22779,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22557
22779
 
22558
22780
  // src/furniture/spec.ts
22559
22781
  var WOOD_THIN = 0.035;
22782
+ var SHELF_SPAN = 1.5;
22783
+ var SHELVING = /shelf|bookcase|rack/;
22784
+ function carcass(parts, drawn) {
22785
+ const all = [...drawn.values()];
22786
+ const width = Math.max(...all.map((p) => p.max[0])) - Math.min(...all.map((p) => p.min[0]));
22787
+ for (const part of parts) {
22788
+ if (LOOSE.has(part.material) || part.shape.kind !== "box" || part.rotation) continue;
22789
+ const [x, y, z] = part.shape.size;
22790
+ if (y >= x && y >= z) {
22791
+ if (z < x && x >= 0.6 * width) continue;
22792
+ part.structure = { role: "column" };
22793
+ } else if (y <= x && y <= z) part.structure = { role: "beam", maxSpan: SHELF_SPAN * Math.max(x, z) };
22794
+ }
22795
+ }
22560
22796
  var FLOOR_TOUCH = 5e-3;
22561
22797
  var LOOSE = /* @__PURE__ */ new Set(["paper"]);
22562
22798
  var BASE_MATERIAL = {
@@ -22672,6 +22908,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22672
22908
  if (area3 < 0.6 * footprint || p.max[1] < 0.2) continue;
22673
22909
  if (!top || p.max[1] > top.y + 1e-6) top = { part: p.id, y: p.max[1] };
22674
22910
  }
22911
+ if (opts.carcass) carcass(parts, byId);
22675
22912
  const spec = { name, parts, joints, tags: ["furniture"] };
22676
22913
  const colors = new Map(drawn.map((p) => [p.id, p.color]));
22677
22914
  const build = (T3) => {
@@ -22715,7 +22952,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22715
22952
  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`);
22716
22953
  const o = opts.options ?? {};
22717
22954
  const seed = Math.round(opts.seed ?? 1);
22718
- const plain2 = /shelf|bookcase|rack/.test(type) ? "paper" : "plastic";
22955
+ const plain2 = SHELVING.test(type) ? "paper" : "plastic";
22719
22956
  const root = { x: seed, z: 0, ry: 0, w, d, h, y: 0, base: 0, palette: o.palette, seed: `furniture_${type}_${seed}` };
22720
22957
  const { K, shared } = renderer.internals;
22721
22958
  const before = K.PARTS.length;
@@ -22734,7 +22971,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22734
22971
  shared.ctx = saved.ctx;
22735
22972
  }
22736
22973
  },
22737
- { materials: { plain: plain2, ...o.materials }, chairs: o.chairs }
22974
+ { materials: { plain: plain2, ...o.materials }, chairs: o.chairs, carcass: SHELVING.test(type) }
22738
22975
  );
22739
22976
  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]);
22740
22977
  K.PARTS.length = before;
@@ -22801,7 +23038,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22801
23038
  function draw(THREE, plan, options) {
22802
23039
  return create3(THREE, { ...options, fleet: null }).build(plan, { quality: "game", terrain: false, noPerimeter: true, pieces: "meshes" });
22803
23040
  }
23041
+ var houses = /* @__PURE__ */ new WeakMap();
22804
23042
  function label(house) {
23043
+ houses.set(house.group, house);
22805
23044
  for (const [id, items] of Object.entries(house.destruction.ranges())) {
22806
23045
  for (const item of items) {
22807
23046
  const object2 = item.isObject3D ? item : item.mesh;
@@ -22814,12 +23053,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22814
23053
  const plan = wallPlan(options);
22815
23054
  const houseOptions = options.house ?? {};
22816
23055
  const probe = draw(THREE, plan, { ...houseOptions, destruction: void 0, destructionRenderer: void 0 });
22817
- const spec = probe.structure.spec({ wallDetail: "coarse" });
23056
+ const spec = probe.structure.spec();
22818
23057
  probe.dispose();
22819
23058
  return {
22820
23059
  plan,
22821
23060
  spec,
22822
- build: (T2) => label(draw(T2, plan, houseOptions))
23061
+ build: (T2) => label(draw(T2, plan, houseOptions)),
23062
+ follow: (root, world, objectId) => houses.get(root)?.destruction?.follow?.(world, objectId, spec) ?? null
22823
23063
  };
22824
23064
  }
22825
23065
  return __toCommonJS(index_exports);