@adatechnology/game-items-three 0.3.1 → 0.3.2

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.
@@ -12694,6 +12694,45 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12694
12694
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
12695
12695
  };
12696
12696
  var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
12697
+ var splinterBox = (inp, box4, limit = inp.maxFragments) => {
12698
+ const { rnd } = inp;
12699
+ const dims = box4.s;
12700
+ const [fmin, fmax] = inp.fragmentSize;
12701
+ const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
12702
+ const [a, b] = [0, 1, 2].filter((i) => i !== g);
12703
+ const w = Math.max(fmin * 1.5, 0.01);
12704
+ const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
12705
+ const maxLen = fmax * 4;
12706
+ const cells = bsp(box4, c, [a, b], rnd);
12707
+ const pieces = [];
12708
+ for (const cell of cells) {
12709
+ const extra = rnd() < 0.4 ? 1 : 0;
12710
+ const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
12711
+ for (const p of bsp(cell, k, [g], rnd)) pieces.push(p);
12712
+ }
12713
+ 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] }));
12714
+ };
12715
+ var severAt = (inp, root, at, gap) => {
12716
+ const dims = root.s;
12717
+ const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
12718
+ const half = dims[L] / 2;
12719
+ const lo = clamp(at - gap / 2, -half, half), hi = clamp(at + gap / 2, -half, half);
12720
+ const out = [];
12721
+ const stub = (from, to) => {
12722
+ if (to - from < 0.02) return;
12723
+ const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
12724
+ c[L] = (from + to) / 2;
12725
+ sz[L] = to - from;
12726
+ out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight: sz[0] * sz[1] * sz[2] });
12727
+ };
12728
+ stub(-half, lo);
12729
+ stub(hi, half);
12730
+ const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
12731
+ band.c[L] = (lo + hi) / 2;
12732
+ band.s[L] = Math.max(hi - lo, 0.01);
12733
+ for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
12734
+ return out;
12735
+ };
12697
12736
  var tearStack = (inp, root, t, u, v, n, sheet) => {
12698
12737
  const { dims, rnd, maxFragments } = inp;
12699
12738
  const layers = clamp(Math.ceil(dims[t] / STACK_LAYER), 2, 4);
@@ -12732,23 +12771,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12732
12771
  return out;
12733
12772
  }
12734
12773
  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
- }
12774
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
12775
+ for (const p of splinterBox(inp, root)) out.push(p);
12752
12776
  return out;
12753
12777
  }
12754
12778
  if (style === "tear") {
@@ -14736,9 +14760,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14736
14760
  var BREACH_AREA = 0.25;
14737
14761
  var BREACH_BLAST_REACH = 0.12;
14738
14762
  var RESIDUAL_CAPACITY = 0.15;
14763
+ var SEVER_BAND = 0.1;
14764
+ var SEVER_ASPECT = 3;
14765
+ var SEVER_BALLISTIC = 2;
14739
14766
  var SUPPORT_TOLERANCE = 0.05;
14740
14767
  var UNIT_FRAGMENT_FACTOR = 4;
14741
14768
  var BREACH_GRID_CAP = 4096;
14769
+ var CHIP_ENERGY = 200;
14770
+ var CHIP_SPLASH = 0.5;
14742
14771
  var CUT_SLICE = 1;
14743
14772
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
14744
14773
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -14782,6 +14811,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14782
14811
  let windCfg = windOpts ? resolveWind(windOpts) : null;
14783
14812
  const ignitionRules = resolveIgnitionRules(options.ignition);
14784
14813
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
14814
+ const chipEnergy = options.breach?.chipEnergy ?? CHIP_ENERGY;
14785
14815
  const rng3 = createRng(options.seed ?? 1);
14786
14816
  const rnd = () => rng3.next();
14787
14817
  let time = 0;
@@ -15155,41 +15185,68 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15155
15185
  markDamaged(p);
15156
15186
  if (p.rt.hinge >= flexure.failRotationDeg * Math.PI / 180) hingeDrop(o, p, src);
15157
15187
  };
15188
+ const freeSpan = (p, supports) => {
15189
+ if (supports.length === 0) return Infinity;
15190
+ const d = p.dims;
15191
+ const L = [0, 1, 2].sort((a, b) => d[b] - d[a])[0];
15192
+ const spans = supports.map((k) => contactBox(p, k)).map(([lo, hi]) => [lo[L], hi[L]]);
15193
+ let far = 0;
15194
+ for (let i = 0; i <= 16; i++) {
15195
+ const x = (i / 16 - 0.5) * d[L];
15196
+ let near = Infinity;
15197
+ for (const [lo, hi] of spans) near = Math.min(near, Math.max(lo - x, x - hi, 0));
15198
+ far = Math.max(far, near);
15199
+ }
15200
+ return 2 * far;
15201
+ };
15158
15202
  const solveStructure = (o) => {
15159
15203
  o.maxStress = 0;
15160
15204
  if (!o.hasStructure) return;
15161
15205
  const parts = o.parts;
15162
15206
  const n = parts.length;
15163
15207
  const dist = new Array(n).fill(-1);
15164
- const queue = [];
15165
15208
  const ok = (i) => parts[i].structural && !isLost(parts[i]);
15166
15209
  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);
15210
+ const out = /* @__PURE__ */ new Set();
15211
+ let queue = [];
15212
+ let lowerOf = /* @__PURE__ */ new Map();
15213
+ for (; ; ) {
15214
+ dist.fill(-1);
15215
+ queue = [];
15216
+ for (let i = 0; i < n; i++) if (ok(i) && !out.has(i) && parts[i].anchored) {
15217
+ dist[i] = 0;
15218
+ queue.push(i);
15219
+ }
15220
+ for (let h = 0; h < queue.length; h++) {
15221
+ const s = queue[h];
15222
+ if (isPartition(s)) continue;
15223
+ for (const j of o.byPart.get(parts[s].spec.id)) {
15224
+ if (j.st.open || !j.bears) continue;
15225
+ const r = j.ia === s ? j.ib : j.ia;
15226
+ if (dist[r] >= 0 || !ok(r) || out.has(r) || restsOn(o, j, r) !== s) continue;
15227
+ dist[r] = dist[s] + 1;
15228
+ queue.push(r);
15229
+ }
15180
15230
  }
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);
15231
+ lowerOf = /* @__PURE__ */ new Map();
15232
+ let dropped = false;
15233
+ for (const i of queue) {
15234
+ const lower = [];
15235
+ for (const j of o.byPart.get(parts[i].spec.id)) {
15236
+ if (j.st.open || !j.bears) continue;
15237
+ const k = restsOn(o, j, i);
15238
+ if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
15239
+ }
15240
+ lowerOf.set(i, lower);
15241
+ const max = parts[i].spec.structure?.maxSpan;
15242
+ if (max !== void 0 && !parts[i].anchored && freeSpan(parts[i], lower.map((k) => parts[k])) > max) {
15243
+ out.add(i);
15244
+ dropped = true;
15245
+ }
15190
15246
  }
15191
- lowerOf.set(i, lower);
15247
+ if (!dropped) break;
15192
15248
  }
15249
+ const incoming = new Array(n).fill(0);
15193
15250
  const bending = new Array(n).fill(0);
15194
15251
  const through = parts.map(() => []);
15195
15252
  const pass = (k, at, mass) => {
@@ -15701,6 +15758,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15701
15758
  const r = rayPart(o, p, origin, d);
15702
15759
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
15703
15760
  if (p.st.opening && insideOpening(o, p, vec.madd(origin, d, r.t0))) continue;
15761
+ if (p.st.chipped && insideChip(p, objLocal(o, vec.madd(origin, d, r.t0)))) continue;
15704
15762
  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
15763
  }
15706
15764
  }
@@ -15964,6 +16022,62 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15964
16022
  if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
15965
16023
  openBreach(o, p, [sum[0] / total, sum[1] / total, sum[2] / total], total, energy, dir, src);
15966
16024
  };
16025
+ const unitGrids = /* @__PURE__ */ new WeakMap();
16026
+ const gridOf = (p) => {
16027
+ let grid = unitGrids.get(p);
16028
+ if (!grid) unitGrids.set(p, grid = unitGrid(p.dims, p.thin, unitsOf2(p), BREACH_GRID_CAP));
16029
+ return grid;
16030
+ };
16031
+ const insideChip = (p, objectPoint) => {
16032
+ const cells = p.st.chipped;
16033
+ if (!cells?.length) return false;
16034
+ const [u, v] = planeAxes(p.thin);
16035
+ const lp = toLocal(p.pos, p.q, objectPoint);
16036
+ return cells.some((c) => Math.abs(lp[u] - c.center[u]) <= c.size[u] / 2 && Math.abs(lp[v] - c.center[v]) <= c.size[v] / 2);
16037
+ };
16038
+ const chipUnits = (o, p, wp, energy, dir, radius, src) => {
16039
+ var _a, _b;
16040
+ if (!p.spec.units || p.spec.reinforcement || !p.staged || chipEnergy <= 0 || energy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
16041
+ const [u, v] = planeAxes(p.thin);
16042
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
16043
+ const grid = gridOf(p);
16044
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
16045
+ const out = [];
16046
+ for (let i = 0; i < grid.length; i++) {
16047
+ const cell = grid[i];
16048
+ if (worn[i] === -1) continue;
16049
+ 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));
16050
+ if (gap > radius) continue;
16051
+ const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
16052
+ const total = (worn[i] ?? 0) + energy * share;
16053
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
16054
+ if (total < chipEnergy * mass) {
16055
+ worn[i] = total;
16056
+ continue;
16057
+ }
16058
+ worn[i] = -1;
16059
+ out.push(cell);
16060
+ }
16061
+ if (out.length === 0) return;
16062
+ const { c, q } = partXf(o, p);
16063
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
16064
+ const holes = localHolesOf(p);
16065
+ for (const cell of out) {
16066
+ const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
16067
+ const center = toWorld(c, q, cell.center);
16068
+ const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
16069
+ const fragments = cellFragments(o, p, [cell], wp, dir, speed, mass, `c${chipped.length}_`);
16070
+ p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
16071
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
16072
+ p.st.mass = Math.max(0, p.st.mass - mass);
16073
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
16074
+ markDamaged(p);
16075
+ setStage(o, p, 1, {}, src);
16076
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
16077
+ debris(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
16078
+ }
16079
+ for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
16080
+ };
15967
16081
  const openBreach = (o, p, center, total, energy, dir, src) => {
15968
16082
  const [u, v] = planeAxes(p.thin);
15969
16083
  const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
@@ -15975,7 +16089,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15975
16089
  failPart(o, p, worldCenter, dir, energy, 12, src);
15976
16090
  return;
15977
16091
  }
15978
- const cells = cellsWithin(unitGrid(p.dims, p.thin, unitsOf2(p), BREACH_GRID_CAP), p.thin, lc, radius);
16092
+ const worn = p.rt.chips;
16093
+ const cells = cellsWithin(gridOf(p).filter((_, i) => worn?.[i] !== -1), p.thin, lc, radius);
15979
16094
  const removed = p.st.mass * cells.reduce((sum, cell) => sum + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
15980
16095
  const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
15981
16096
  const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
@@ -16059,6 +16174,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16059
16174
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
16060
16175
  const hitLocal = toLocal(c, q, hitW);
16061
16176
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
16177
+ const axis = style === "splinter" ? severAxis(p) : -1;
16178
+ const cutAt2 = axis < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis] : void 0);
16062
16179
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
16063
16180
  style,
16064
16181
  dims: p.dims,
@@ -16068,7 +16185,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16068
16185
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
16069
16186
  maxFragments,
16070
16187
  rnd,
16071
- sheet: p.mat.sheet
16188
+ sheet: p.mat.sheet,
16189
+ ...cutAt2 !== void 0 ? { cut: { at: cutAt2, gap: SEVER_BAND } } : {}
16072
16190
  });
16073
16191
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
16074
16192
  const mass = p.st.mass;
@@ -16127,10 +16245,33 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16127
16245
  emit({ ...base(o, src), type: "primed", part: p.spec.id, point: partXf(o, p).c, trigger, delay });
16128
16246
  };
16129
16247
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
16248
+ const severAxis = (p) => {
16249
+ if (p.mat.fracture !== "splinter" || p.spec.shape.kind !== "box") return -1;
16250
+ const d = p.dims;
16251
+ const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
16252
+ return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
16253
+ };
16254
+ const bandOf = (o, p, axis, origin) => {
16255
+ const { c, q } = partXf(o, p);
16256
+ const along = toLocal(c, q, origin)[axis];
16257
+ const L = p.dims[axis];
16258
+ return clamp2(Math.floor((along + L / 2) / SEVER_BAND), 0, Math.ceil(L / SEVER_BAND) - 1);
16259
+ };
16130
16260
  const applyDamage = (o, p, amount, type, origin, dir, fragEnergy, cap, src) => {
16261
+ var _a;
16131
16262
  if (isLost(p) || amount <= 0) return 0;
16132
16263
  const prev = p.st.hp;
16133
16264
  p.st.hp -= amount;
16265
+ const axis = origin && type !== "fire" && type !== "heat" ? severAxis(p) : -1;
16266
+ if (axis >= 0 && p.st.hp > 0) {
16267
+ const band = bandOf(o, p, axis, origin);
16268
+ const cuts = (_a = p.rt).cuts ?? (_a.cuts = {});
16269
+ cuts[band] = (cuts[band] ?? 0) + amount * (type === "ballistic" ? SEVER_BALLISTIC : 1);
16270
+ if (cuts[band] >= p.st.maxHp * SEVER_BAND / p.dims[axis]) {
16271
+ p.rt.cutAt = -p.dims[axis] / 2 + (band + 0.5) * SEVER_BAND;
16272
+ p.st.hp = 0;
16273
+ }
16274
+ }
16134
16275
  markDamaged(p);
16135
16276
  const trigger = CHAIN_TRIGGER_OF[type];
16136
16277
  const chained = trigger !== void 0 && chainDelayOf(p.spec, trigger) !== void 0;
@@ -16169,7 +16310,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16169
16310
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
16170
16311
  const mat = p.mat;
16171
16312
  const piercing = round3.piercing ?? 1;
16172
- if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry2))) {
16313
+ if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry2)) || insideChip(p, objLocal(o, entry2))) {
16173
16314
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry2, d, Math.min(pathLen, p.thickness)), damage: 0 };
16174
16315
  }
16175
16316
  const sin = Math.min(1, Math.abs(vec.dot(d, n)));
@@ -16228,6 +16369,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16228
16369
  debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
16229
16370
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
16230
16371
  const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry2, d, E * 0.1, 12, src);
16372
+ chipUnits(o, p, entry2, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
16231
16373
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
16232
16374
  };
16233
16375
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -16255,6 +16397,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16255
16397
  const scale = mat.damageScale?.[kind] ?? 1;
16256
16398
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
16257
16399
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
16400
+ if (kind === "blunt") chipUnits(o, p, point, E, d, Math.max(contactR, 0.03), src);
16258
16401
  return done;
16259
16402
  };
16260
16403
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -26273,12 +26416,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26273
26416
  A.invalidate = () => {
26274
26417
  for (let i = 0; i < NS; i++) PL[i].shadow.needsUpdate = true;
26275
26418
  };
26276
- A.update = function(houses, focus, op) {
26419
+ A.update = function(houses2, focus, op) {
26277
26420
  op = op || {};
26278
- houses = (Array.isArray(houses) ? houses : [houses]).filter(Boolean);
26421
+ houses2 = (Array.isArray(houses2) ? houses2 : [houses2]).filter(Boolean);
26279
26422
  _f.set(focus.x, focus.y, focus.z);
26280
26423
  const rooms = [], exterior = [];
26281
- for (const house of houses) {
26424
+ for (const house of houses2) {
26282
26425
  if (house.group && !house.group.visible) continue;
26283
26426
  house.house.updateMatrixWorld(true);
26284
26427
  const M = house.house.matrixWorld, by = /* @__PURE__ */ new Map();
@@ -26657,7 +26800,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26657
26800
  }
26658
26801
  return null;
26659
26802
  };
26660
- function courses(part, units, opening, paint) {
26803
+ const local = (p) => {
26804
+ house.updateMatrixWorld(true);
26805
+ const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
26806
+ return [v.x, v.y, v.z];
26807
+ };
26808
+ function courses(part, units, opening, chips, paint) {
26661
26809
  const [u0, v0, u1, v1] = part.aabb;
26662
26810
  const alongU = u1 - u0 >= v1 - v0;
26663
26811
  const len = alongU ? u1 - u0 : v1 - v0;
@@ -26681,23 +26829,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26681
26829
  const x = alongU ? u0 + mid : (u0 + u1) / 2;
26682
26830
  const z = alongU ? (v0 + v1) / 2 : v0 + mid;
26683
26831
  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 });
26832
+ const d = opening ? Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]) : Infinity;
26833
+ if (opening && d < opening.radius) continue;
26834
+ const gone = (c) => Math.abs(c[1] - y) <= rowH / 2 && (alongU ? c[0] >= u0 + a && c[0] <= u0 + b : c[2] >= v0 + a && c[2] <= v0 + b);
26835
+ if (chips.some(gone)) continue;
26836
+ const near = opening && d < opening.radius + 0.25 || chips.some((c) => Math.hypot(c[0] - x, c[1] - y, c[2] - z) < 0.15);
26837
+ list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: near || paint === null ? edge : paint });
26687
26838
  }
26688
26839
  }
26689
26840
  return list2;
26690
26841
  }
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);
26842
+ function draw2(partId, change) {
26843
+ if (lod !== 0) return false;
26844
+ const part = structure.byId.get(partId);
26845
+ const spec = opts.spec()?.parts?.find((q) => q.id === partId);
26695
26846
  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));
26847
+ let view = views.get(partId);
26848
+ if (!view) {
26849
+ const items = structure.meshesOf(partId).filter((it) => it.mesh);
26850
+ if (!items.length) return false;
26851
+ view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: [] };
26852
+ }
26853
+ if (change.opening) {
26854
+ if (view.opening) return false;
26855
+ view.opening = { center: local(change.opening.center), radius: change.opening.radius };
26856
+ }
26857
+ if (change.chip) view.chips.push(local(change.chip));
26858
+ const list2 = courses(part, spec.units, view.opening, view.chips, view.paint);
26859
+ if (view.mesh) {
26860
+ view.mesh.removeFromParent();
26861
+ view.mesh.dispose();
26862
+ }
26699
26863
  const mesh2 = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
26700
- mesh2.name = "breach:" + event.part;
26864
+ mesh2.name = "breach:" + partId;
26701
26865
  list2.forEach((u, i) => {
26702
26866
  tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
26703
26867
  mesh2.setMatrixAt(i, tmp.m);
@@ -26709,8 +26873,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26709
26873
  mesh2.castShadow = true;
26710
26874
  mesh2.receiveShadow = true;
26711
26875
  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 });
26876
+ if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
26877
+ view.mesh = mesh2;
26878
+ view.units = list2.length;
26879
+ views.set(partId, view);
26714
26880
  invalidate();
26715
26881
  invalidateShadow();
26716
26882
  return true;
@@ -26724,7 +26890,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26724
26890
  return {
26725
26891
  /** Handles a `stage` event of the house's object. Returns true when the wall was redrawn as courses. */
26726
26892
  stage(event) {
26727
- return event.stage === 2 ? open(event) : false;
26893
+ return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
26894
+ },
26895
+ /** Handles a `chip` event: the unit that came out leaves a gap in the courses. Returns true when the wall was redrawn. */
26896
+ chip(event) {
26897
+ return draw2(event.part, { chip: event.center });
26728
26898
  },
26729
26899
  /** The part broke or fell: the courses go (the plain range stays collapsed, the renderer hides the part). */
26730
26900
  drop(partId) {
@@ -26954,6 +27124,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
26954
27124
  case "stage":
26955
27125
  breach.stage(e);
26956
27126
  break;
27127
+ case "chip":
27128
+ breach.chip(e);
27129
+ break;
26957
27130
  case "collapse":
26958
27131
  for (const id of e.parts) {
26959
27132
  breach.drop(id);
@@ -27008,6 +27181,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27008
27181
  if (D.active && D.world.has(D.id)) D.world.remove(D.id);
27009
27182
  D.active = false;
27010
27183
  };
27184
+ D.follow = (world, id, spec) => {
27185
+ D.spec = spec;
27186
+ const offFollow = world.on("*", (e) => {
27187
+ if (e.object !== id) return;
27188
+ switch (e.type) {
27189
+ case "stage":
27190
+ breach.stage(e);
27191
+ break;
27192
+ case "chip":
27193
+ breach.chip(e);
27194
+ break;
27195
+ case "fracture":
27196
+ breach.drop(e.part);
27197
+ break;
27198
+ case "collapse":
27199
+ for (const p of e.parts) breach.drop(p);
27200
+ break;
27201
+ default:
27202
+ break;
27203
+ }
27204
+ });
27205
+ return { restore: () => breach.restore(), dispose: () => {
27206
+ offFollow();
27207
+ breach.restore();
27208
+ } };
27209
+ };
27011
27210
  D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
27012
27211
  D.state = () => D.active ? D.world.state(D.id) : null;
27013
27212
  D.reset = () => {
@@ -27730,6 +27929,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27730
27929
  return buildApi(K, shared);
27731
27930
  }
27732
27931
  var WOOD_THIN = 0.035;
27932
+ var SHELF_SPAN = 1.5;
27933
+ var SHELVING = /shelf|bookcase|rack/;
27934
+ function carcass(parts, drawn) {
27935
+ const all = [...drawn.values()];
27936
+ const width = Math.max(...all.map((p) => p.max[0])) - Math.min(...all.map((p) => p.min[0]));
27937
+ for (const part of parts) {
27938
+ if (LOOSE.has(part.material) || part.shape.kind !== "box" || part.rotation) continue;
27939
+ const [x, y, z] = part.shape.size;
27940
+ if (y >= x && y >= z) {
27941
+ if (z < x && x >= 0.6 * width) continue;
27942
+ part.structure = { role: "column" };
27943
+ } else if (y <= x && y <= z) part.structure = { role: "beam", maxSpan: SHELF_SPAN * Math.max(x, z) };
27944
+ }
27945
+ }
27733
27946
  var FLOOR_TOUCH = 5e-3;
27734
27947
  var LOOSE = /* @__PURE__ */ new Set(["paper"]);
27735
27948
  var BASE_MATERIAL = {
@@ -27845,6 +28058,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27845
28058
  if (area3 < 0.6 * footprint || p.max[1] < 0.2) continue;
27846
28059
  if (!top || p.max[1] > top.y + 1e-6) top = { part: p.id, y: p.max[1] };
27847
28060
  }
28061
+ if (opts.carcass) carcass(parts, byId);
27848
28062
  const spec = { name, parts, joints, tags: ["furniture"] };
27849
28063
  const colors = new Map(drawn.map((p) => [p.id, p.color]));
27850
28064
  const build = (T3) => {
@@ -27888,7 +28102,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27888
28102
  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
28103
  const o = opts.options ?? {};
27890
28104
  const seed = Math.round(opts.seed ?? 1);
27891
- const plain2 = /shelf|bookcase|rack/.test(type) ? "paper" : "plastic";
28105
+ const plain2 = SHELVING.test(type) ? "paper" : "plastic";
27892
28106
  const root = { x: seed, z: 0, ry: 0, w, d, h, y: 0, base: 0, palette: o.palette, seed: `furniture_${type}_${seed}` };
27893
28107
  const { K, shared } = renderer.internals;
27894
28108
  const before = K.PARTS.length;
@@ -27907,7 +28121,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27907
28121
  shared.ctx = saved.ctx;
27908
28122
  }
27909
28123
  },
27910
- { materials: { plain: plain2, ...o.materials }, chairs: o.chairs }
28124
+ { materials: { plain: plain2, ...o.materials }, chairs: o.chairs, carcass: SHELVING.test(type) }
27911
28125
  );
27912
28126
  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
28127
  K.PARTS.length = before;