@adatechnology/game-floorplan-three 0.4.2 → 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,45 @@ 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
+ };
11235
11274
  var tearStack = (inp, root, t, u, v, n, sheet) => {
11236
11275
  const { dims, rnd, maxFragments } = inp;
11237
11276
  const layers = clamp(Math.ceil(dims[t] / STACK_LAYER), 2, 4);
@@ -11270,23 +11309,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
11270
11309
  return out;
11271
11310
  }
11272
11311
  if (style === "splinter") {
11273
- const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
11274
- const [a, b] = [0, 1, 2].filter((i) => i !== g);
11275
- const w = Math.max(fmin * 1.5, 0.01);
11276
- const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(maxFragments / 2)));
11277
- const maxLen = fmax * 4;
11278
- const cells = bsp(root, c, [a, b], rnd);
11279
- const pieces = [];
11280
- for (const cell of cells) {
11281
- let list2 = [cell];
11282
- const extra = rnd() < 0.4 ? 1 : 0;
11283
- const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
11284
- list2 = bsp(cell, k, [g], rnd);
11285
- for (const p of list2) pieces.push(p);
11286
- }
11287
- for (const p of pieces.slice(0, maxFragments)) {
11288
- 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] });
11289
- }
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);
11290
11314
  return out;
11291
11315
  }
11292
11316
  if (style === "tear") {
@@ -13274,9 +13298,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13274
13298
  var BREACH_AREA = 0.25;
13275
13299
  var BREACH_BLAST_REACH = 0.12;
13276
13300
  var RESIDUAL_CAPACITY = 0.15;
13301
+ var SEVER_BAND = 0.1;
13302
+ var SEVER_ASPECT = 3;
13303
+ var SEVER_BALLISTIC = 2;
13277
13304
  var SUPPORT_TOLERANCE = 0.05;
13278
13305
  var UNIT_FRAGMENT_FACTOR = 4;
13279
13306
  var BREACH_GRID_CAP = 4096;
13307
+ var CHIP_ENERGY = 200;
13308
+ var CHIP_SPLASH = 0.5;
13280
13309
  var CUT_SLICE = 1;
13281
13310
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
13282
13311
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -13320,6 +13349,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13320
13349
  let windCfg = windOpts ? resolveWind(windOpts) : null;
13321
13350
  const ignitionRules = resolveIgnitionRules(options.ignition);
13322
13351
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
13352
+ const chipEnergy = options.breach?.chipEnergy ?? CHIP_ENERGY;
13323
13353
  const rng2 = createRng(options.seed ?? 1);
13324
13354
  const rnd = () => rng2.next();
13325
13355
  let time = 0;
@@ -13693,41 +13723,68 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13693
13723
  markDamaged(p);
13694
13724
  if (p.rt.hinge >= flexure.failRotationDeg * Math.PI / 180) hingeDrop(o, p, src);
13695
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
+ };
13696
13740
  const solveStructure = (o) => {
13697
13741
  o.maxStress = 0;
13698
13742
  if (!o.hasStructure) return;
13699
13743
  const parts = o.parts;
13700
13744
  const n = parts.length;
13701
13745
  const dist = new Array(n).fill(-1);
13702
- const queue = [];
13703
13746
  const ok = (i) => parts[i].structural && !isLost(parts[i]);
13704
13747
  const isPartition = (i) => parts[i].spec.structure?.role === "partition";
13705
- for (let i = 0; i < n; i++) if (ok(i) && parts[i].anchored) {
13706
- dist[i] = 0;
13707
- queue.push(i);
13708
- }
13709
- for (let h = 0; h < queue.length; h++) {
13710
- const s = queue[h];
13711
- if (isPartition(s)) continue;
13712
- for (const j of o.byPart.get(parts[s].spec.id)) {
13713
- if (j.st.open || !j.bears) continue;
13714
- const r = j.ia === s ? j.ib : j.ia;
13715
- if (dist[r] >= 0 || !ok(r) || restsOn(o, j, r) !== s) continue;
13716
- dist[r] = dist[s] + 1;
13717
- 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
+ }
13718
13768
  }
13719
- }
13720
- const incoming = new Array(n).fill(0);
13721
- const lowerOf = /* @__PURE__ */ new Map();
13722
- for (const i of queue) {
13723
- const lower = [];
13724
- for (const j of o.byPart.get(parts[i].spec.id)) {
13725
- if (j.st.open || !j.bears) continue;
13726
- const k = restsOn(o, j, i);
13727
- 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
+ }
13728
13784
  }
13729
- lowerOf.set(i, lower);
13785
+ if (!dropped) break;
13730
13786
  }
13787
+ const incoming = new Array(n).fill(0);
13731
13788
  const bending = new Array(n).fill(0);
13732
13789
  const through = parts.map(() => []);
13733
13790
  const pass = (k, at, mass) => {
@@ -14239,6 +14296,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14239
14296
  const r = rayPart(o, p, origin, d);
14240
14297
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
14241
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;
14242
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 };
14243
14301
  }
14244
14302
  }
@@ -14502,6 +14560,62 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14502
14560
  if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
14503
14561
  openBreach(o, p, [sum[0] / total, sum[1] / total, sum[2] / total], total, energy, dir, src);
14504
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
+ };
14505
14619
  const openBreach = (o, p, center, total, energy, dir, src) => {
14506
14620
  const [u, v] = planeAxes(p.thin);
14507
14621
  const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
@@ -14513,7 +14627,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14513
14627
  failPart(o, p, worldCenter, dir, energy, 12, src);
14514
14628
  return;
14515
14629
  }
14516
- 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);
14517
14632
  const removed = p.st.mass * cells.reduce((sum, cell) => sum + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
14518
14633
  const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
14519
14634
  const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
@@ -14597,6 +14712,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14597
14712
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
14598
14713
  const hitLocal = toLocal(c, q, hitW);
14599
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);
14600
14717
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
14601
14718
  style,
14602
14719
  dims: p.dims,
@@ -14606,7 +14723,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14606
14723
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
14607
14724
  maxFragments,
14608
14725
  rnd,
14609
- sheet: p.mat.sheet
14726
+ sheet: p.mat.sheet,
14727
+ ...cutAt2 !== void 0 ? { cut: { at: cutAt2, gap: SEVER_BAND } } : {}
14610
14728
  });
14611
14729
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
14612
14730
  const mass = p.st.mass;
@@ -14665,10 +14783,33 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14665
14783
  emit({ ...base(o, src), type: "primed", part: p.spec.id, point: partXf(o, p).c, trigger, delay });
14666
14784
  };
14667
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
+ };
14668
14798
  const applyDamage = (o, p, amount, type, origin, dir, fragEnergy, cap, src) => {
14799
+ var _a;
14669
14800
  if (isLost(p) || amount <= 0) return 0;
14670
14801
  const prev = p.st.hp;
14671
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
+ }
14672
14813
  markDamaged(p);
14673
14814
  const trigger = CHAIN_TRIGGER_OF[type];
14674
14815
  const chained = trigger !== void 0 && chainDelayOf(p.spec, trigger) !== void 0;
@@ -14707,7 +14848,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14707
14848
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
14708
14849
  const mat = p.mat;
14709
14850
  const piercing = round3.piercing ?? 1;
14710
- 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))) {
14711
14852
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry2, d, Math.min(pathLen, p.thickness)), damage: 0 };
14712
14853
  }
14713
14854
  const sin = Math.min(1, Math.abs(vec.dot(d, n)));
@@ -14766,6 +14907,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14766
14907
  debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
14767
14908
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
14768
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);
14769
14911
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
14770
14912
  };
14771
14913
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -14793,6 +14935,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14793
14935
  const scale = mat.damageScale?.[kind] ?? 1;
14794
14936
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
14795
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);
14796
14939
  return done;
14797
14940
  };
14798
14941
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -19106,7 +19249,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19106
19249
  }
19107
19250
  return null;
19108
19251
  };
19109
- 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) {
19110
19258
  const [u0, v0, u1, v1] = part.aabb;
19111
19259
  const alongU = u1 - u0 >= v1 - v0;
19112
19260
  const len = alongU ? u1 - u0 : v1 - v0;
@@ -19130,23 +19278,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19130
19278
  const x = alongU ? u0 + mid : (u0 + u1) / 2;
19131
19279
  const z = alongU ? (v0 + v1) / 2 : v0 + mid;
19132
19280
  const y = part.y0 + (r + 0.5) * rowH;
19133
- const d = Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]);
19134
- if (d < opening.radius) continue;
19135
- 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 });
19136
19287
  }
19137
19288
  }
19138
19289
  return list2;
19139
19290
  }
19140
- function open(event) {
19141
- if (lod !== 0 || views.has(event.part) || !event.opening) return false;
19142
- const part = structure.byId.get(event.part);
19143
- 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);
19144
19295
  if (!part || !spec || !spec.units) return false;
19145
- const items = structure.meshesOf(event.part).filter((it) => it.mesh);
19146
- if (!items.length) return false;
19147
- 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
+ }
19148
19312
  const mesh2 = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
19149
- mesh2.name = "breach:" + event.part;
19313
+ mesh2.name = "breach:" + partId;
19150
19314
  list2.forEach((u, i) => {
19151
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));
19152
19316
  mesh2.setMatrixAt(i, tmp.m);
@@ -19158,8 +19322,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19158
19322
  mesh2.castShadow = true;
19159
19323
  mesh2.receiveShadow = true;
19160
19324
  house.add(mesh2);
19161
- const saved = items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
19162
- 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);
19163
19329
  invalidate();
19164
19330
  invalidateShadow();
19165
19331
  return true;
@@ -19173,7 +19339,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19173
19339
  return {
19174
19340
  /** Handles a `stage` event of the house's object. Returns true when the wall was redrawn as courses. */
19175
19341
  stage(event) {
19176
- 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 });
19177
19347
  },
19178
19348
  /** The part broke or fell: the courses go (the plain range stays collapsed, the renderer hides the part). */
19179
19349
  drop(partId) {
@@ -19405,6 +19575,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19405
19575
  case "stage":
19406
19576
  breach.stage(e);
19407
19577
  break;
19578
+ case "chip":
19579
+ breach.chip(e);
19580
+ break;
19408
19581
  case "collapse":
19409
19582
  for (const id of e.parts) {
19410
19583
  breach.drop(id);
@@ -19459,6 +19632,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19459
19632
  if (D.active && D.world.has(D.id)) D.world.remove(D.id);
19460
19633
  D.active = false;
19461
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
+ };
19462
19661
  D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
19463
19662
  D.state = () => D.active ? D.world.state(D.id) : null;
19464
19663
  D.reset = () => {
@@ -22580,6 +22779,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22580
22779
 
22581
22780
  // src/furniture/spec.ts
22582
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
+ }
22583
22796
  var FLOOR_TOUCH = 5e-3;
22584
22797
  var LOOSE = /* @__PURE__ */ new Set(["paper"]);
22585
22798
  var BASE_MATERIAL = {
@@ -22695,6 +22908,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22695
22908
  if (area3 < 0.6 * footprint || p.max[1] < 0.2) continue;
22696
22909
  if (!top || p.max[1] > top.y + 1e-6) top = { part: p.id, y: p.max[1] };
22697
22910
  }
22911
+ if (opts.carcass) carcass(parts, byId);
22698
22912
  const spec = { name, parts, joints, tags: ["furniture"] };
22699
22913
  const colors = new Map(drawn.map((p) => [p.id, p.color]));
22700
22914
  const build = (T3) => {
@@ -22738,7 +22952,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22738
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`);
22739
22953
  const o = opts.options ?? {};
22740
22954
  const seed = Math.round(opts.seed ?? 1);
22741
- const plain2 = /shelf|bookcase|rack/.test(type) ? "paper" : "plastic";
22955
+ const plain2 = SHELVING.test(type) ? "paper" : "plastic";
22742
22956
  const root = { x: seed, z: 0, ry: 0, w, d, h, y: 0, base: 0, palette: o.palette, seed: `furniture_${type}_${seed}` };
22743
22957
  const { K, shared } = renderer.internals;
22744
22958
  const before = K.PARTS.length;
@@ -22757,7 +22971,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22757
22971
  shared.ctx = saved.ctx;
22758
22972
  }
22759
22973
  },
22760
- { materials: { plain: plain2, ...o.materials }, chairs: o.chairs }
22974
+ { materials: { plain: plain2, ...o.materials }, chairs: o.chairs, carcass: SHELVING.test(type) }
22761
22975
  );
22762
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]);
22763
22977
  K.PARTS.length = before;
@@ -22824,7 +23038,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22824
23038
  function draw(THREE, plan, options) {
22825
23039
  return create3(THREE, { ...options, fleet: null }).build(plan, { quality: "game", terrain: false, noPerimeter: true, pieces: "meshes" });
22826
23040
  }
23041
+ var houses = /* @__PURE__ */ new WeakMap();
22827
23042
  function label(house) {
23043
+ houses.set(house.group, house);
22828
23044
  for (const [id, items] of Object.entries(house.destruction.ranges())) {
22829
23045
  for (const item of items) {
22830
23046
  const object2 = item.isObject3D ? item : item.mesh;
@@ -22837,12 +23053,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22837
23053
  const plan = wallPlan(options);
22838
23054
  const houseOptions = options.house ?? {};
22839
23055
  const probe = draw(THREE, plan, { ...houseOptions, destruction: void 0, destructionRenderer: void 0 });
22840
- const spec = probe.structure.spec({ wallDetail: "coarse" });
23056
+ const spec = probe.structure.spec();
22841
23057
  probe.dispose();
22842
23058
  return {
22843
23059
  plan,
22844
23060
  spec,
22845
- 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
22846
23063
  };
22847
23064
  }
22848
23065
  return __toCommonJS(index_exports);