@adatechnology/game-floorplan-three 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13523,6 +13523,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13523
13523
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
13524
13524
  var VENT_DIAMETER = 8e-3;
13525
13525
  var FLAME_LIGHTS_LEAK_C = 470;
13526
+ var RESTING_HOLD_COS = Math.cos(Math.PI / 6);
13526
13527
  var BREACH_RADIUS = 0.55;
13527
13528
  var BREACH_BLAST_FACTOR = 6;
13528
13529
  var BREACH_AREA = 0.25;
@@ -13538,6 +13539,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13538
13539
  var BREACH_GRID_CAP = 4096;
13539
13540
  var CHIP_ENERGY = 200;
13540
13541
  var CHIP_SPLASH = 0.5;
13542
+ var CRATER_SPENT = 0.6;
13543
+ var EDGE_REACH = 1.5;
13541
13544
  var BAR_BOW = 0.08;
13542
13545
  var BULLET_JOINT_SPAN = 0.3;
13543
13546
  var CUT_SLICE = 1;
@@ -13558,12 +13561,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13558
13561
  const cells = p.st.chipped;
13559
13562
  if (!cells?.length) return 0;
13560
13563
  const [u, v] = planeAxes(p.thin);
13561
- const along = p.dims[u] >= p.dims[v] ? u : v;
13564
+ const role = p.spec.structure?.role;
13565
+ const up = rotateVec(p.q, [0, 0, 0].map((_, k) => k === v ? 1 : 0));
13566
+ const upright = Math.abs(up[1]) > 0.7 ? v : Math.abs(rotateVec(p.q, [0, 0, 0].map((_, k) => k === u ? 1 : 0))[1]) > 0.7 ? u : -1;
13567
+ const along = (role === "wall" || role === "partition" || role === "column") && upright >= 0 ? upright : p.dims[u] >= p.dims[v] ? u : v;
13562
13568
  const across = along === u ? v : u;
13563
13569
  let worst = 0;
13564
13570
  for (const c of cells) {
13565
- let gone = 0;
13566
- for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across];
13571
+ const spans = [];
13572
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) spans.push([d.center[across] - d.size[across] / 2, d.center[across] + d.size[across] / 2]);
13573
+ spans.sort((a, b) => a[0] - b[0]);
13574
+ let gone = 0, end = -Infinity;
13575
+ for (const [a, b] of spans) {
13576
+ if (b <= end) continue;
13577
+ gone += b - Math.max(a, end);
13578
+ end = b;
13579
+ }
13567
13580
  worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
13568
13581
  }
13569
13582
  return Math.min(1, worst);
@@ -13859,7 +13872,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13859
13872
  if (k < 0 || j.spec.kind !== "tie") return k;
13860
13873
  return partXf(o, o.parts[k]).c[1] > partXf(o, o.parts[r]).c[1] + 1e-6 ? k : -1;
13861
13874
  };
13875
+ let poseVersion = 0;
13876
+ const contactMemo = /* @__PURE__ */ new WeakMap();
13862
13877
  const contactBox = (p, support) => {
13878
+ let byTouching = contactMemo.get(p);
13879
+ if (!byTouching) contactMemo.set(p, byTouching = /* @__PURE__ */ new Map());
13880
+ const known = byTouching.get(support);
13881
+ if (known && known.version === poseVersion) return known.box;
13882
+ const box3 = measureContact(p, support);
13883
+ byTouching.set(support, { version: poseVersion, box: box3 });
13884
+ return box3;
13885
+ };
13886
+ const measureContact = (p, support) => {
13863
13887
  const A = aabbOf(p.spec.shape, p.pos, p.q), B = aabbOf(support.spec.shape, support.pos, support.q);
13864
13888
  const lo = [0, 0, 0], hi = [0, 0, 0];
13865
13889
  for (let k = 0; k < 3; k++) {
@@ -14654,7 +14678,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14654
14678
  p.st.holes.push(hole);
14655
14679
  if (p.st.holes.length > maxHoles) p.st.holes.shift();
14656
14680
  markDamaged(p);
14657
- emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {} });
14681
+ emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {}, ...p.spec.powder ? { powder: { ...p.spec.powder } } : {} });
14658
14682
  setStage(o, p, 1, {}, src);
14659
14683
  return hole;
14660
14684
  };
@@ -14671,6 +14695,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14671
14695
  const detachEvents = (o, opened, by, src) => {
14672
14696
  for (const j of opened) emit({ ...base(o, src), type: "detach", joint: j.spec.id, parts: [j.spec.a, j.spec.b], by, groups: o.st.groups.map((g) => [...g]) });
14673
14697
  };
14698
+ const slideOffResting = (o, rotation) => {
14699
+ const upY = 1 - 2 * (rotation[0] * rotation[0] + rotation[2] * rotation[2]);
14700
+ if (upY >= RESTING_HOLD_COS) return;
14701
+ const opened = o.joints.filter((j) => j.spec.kind === "resting" && !j.st.open);
14702
+ if (opened.length === 0) return;
14703
+ run(() => {
14704
+ for (const j of opened) {
14705
+ j.st.open = true;
14706
+ j.st.openedBy = "break";
14707
+ }
14708
+ solveStructure(o);
14709
+ recomputeGroups(o);
14710
+ detachEvents(o, opened, "break");
14711
+ collapseCheck(o);
14712
+ });
14713
+ };
14674
14714
  const checkDestroyed = (o, src) => {
14675
14715
  if (!o.st.destroyed && o.parts.length > 0 && o.parts.every(isLost)) {
14676
14716
  o.st.destroyed = true;
@@ -14771,7 +14811,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14771
14811
  const localHolesOf = (p) => p.st.holes.map((h) => ({ point: toLocal(p.pos, p.q, h.point), normal: rotateVec(quat.conjugate(p.q), h.normal), diameter: h.diameter }));
14772
14812
  const unitsOf2 = (p) => p.spec.units ?? { size: [p.mat.fragmentSize?.[1] ?? 0.2, p.mat.fragmentSize?.[1] ?? 0.2, p.thickness], bond: "stack" };
14773
14813
  const FRAGMENT_SHAPE = { shatter: "shard", splinter: "splinter", tear: "scrap" };
14774
- const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag) => {
14814
+ const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag, inner = false) => {
14775
14815
  const { c, q } = partXf(o, p);
14776
14816
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
14777
14817
  const [u, v] = planeAxes(p.thin);
@@ -14795,7 +14835,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14795
14835
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
14796
14836
  angularVelocity: [(rnd() - 0.5) * 8, (rnd() - 0.5) * 8, (rnd() - 0.5) * 8],
14797
14837
  mass: mass * faceArea(cell) / total,
14798
- ...marks.length > 0 ? { marks } : {}
14838
+ ...marks.length > 0 ? { marks } : {},
14839
+ ...inner ? { inner } : {}
14799
14840
  };
14800
14841
  });
14801
14842
  };
@@ -14836,7 +14877,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14836
14877
  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);
14837
14878
  };
14838
14879
  const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
14839
- var _a, _b;
14880
+ var _a, _b, _c;
14840
14881
  if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
14841
14882
  const rule = p.mat.hits?.[kind];
14842
14883
  const perKg = rule?.unitJoules ?? chipEnergy;
@@ -14846,7 +14887,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14846
14887
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
14847
14888
  const grid = gridOf(p);
14848
14889
  const worn = (_a = p.rt).chips ?? (_a.chips = {});
14890
+ const cut = (_b = p.rt).craters ?? (_b.craters = {});
14849
14891
  const out = [];
14892
+ const shot = kind === "ballistic";
14850
14893
  for (let i = 0; i < grid.length; i++) {
14851
14894
  const cell = grid[i];
14852
14895
  if (worn[i] === -1) continue;
@@ -14859,18 +14902,42 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14859
14902
  worn[i] = total;
14860
14903
  continue;
14861
14904
  }
14862
- worn[i] = -1;
14863
- out.push(cell);
14905
+ if (!shot) {
14906
+ worn[i] = -1;
14907
+ out.push(cell);
14908
+ continue;
14909
+ }
14910
+ const size = [cell.size[0], cell.size[1], cell.size[2]];
14911
+ const center = [cell.center[0], cell.center[1], cell.center[2]];
14912
+ for (const a of [u, v]) {
14913
+ const w = Math.min(cell.size[a], craterAcross(hitEnergy) * (0.85 + 0.3 * rnd()));
14914
+ const lo = cell.center[a] - cell.size[a] / 2 + w / 2, hi = cell.center[a] + cell.size[a] / 2 - w / 2;
14915
+ size[a] = w;
14916
+ center[a] = clamp2(lp[a], lo, hi);
14917
+ const [from, to] = tornToEdge({ p, cell, a, b: a === u ? v : u, at: lp[a], crater: [center[a] - w / 2, center[a] + w / 2], reach: w * EDGE_REACH, gone: (k) => worn[k] === -1 });
14918
+ size[a] = to - from;
14919
+ center[a] = (from + to) / 2;
14920
+ }
14921
+ const taken2 = (cut[i] ?? 0) + size[u] * size[v];
14922
+ if (taken2 >= cell.size[u] * cell.size[v] * CRATER_SPENT) {
14923
+ worn[i] = -1;
14924
+ out.push({ ...cell, crater: true });
14925
+ continue;
14926
+ }
14927
+ worn[i] = 0;
14928
+ cut[i] = taken2;
14929
+ out.push({ center, size, crater: true });
14864
14930
  }
14865
14931
  if (out.length === 0) return;
14866
14932
  const { c, q } = partXf(o, p);
14867
- const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
14933
+ const chipped = (_c = p.st).chipped ?? (_c.chipped = []);
14868
14934
  const holes = localHolesOf(p);
14869
14935
  for (const cell of out) {
14870
14936
  const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
14871
14937
  const center = toWorld(c, q, cell.center);
14872
14938
  const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
14873
- const fragments = cellFragments(o, p, [cell], wp, dir, speed, mass, `c${chipped.length}_`);
14939
+ const tag = `c${chipped.length}_`;
14940
+ const fragments = cell.crater ? craterFragments(o, p, cell, dir, speed, mass, tag) : cellFragments(o, p, [cell], wp, dir, speed, mass, tag, true);
14874
14941
  p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
14875
14942
  chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
14876
14943
  p.st.mass = Math.max(0, p.st.mass - mass);
@@ -14887,6 +14954,57 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14887
14954
  collapseCheck(o, src);
14888
14955
  }
14889
14956
  };
14957
+ const tornToEdge = (o) => {
14958
+ const { p, cell, a, b, at, crater: crater2, reach, gone } = o;
14959
+ const half = p.dims[a] / 2;
14960
+ const lo = cell.center[a] - cell.size[a] / 2, hi = cell.center[a] + cell.size[a] / 2;
14961
+ const free = (side, sign2) => {
14962
+ if (sign2 > 0 ? side >= half - 1e-6 : side <= -half + 1e-6) return sign2 > 0 || a !== planeAxes(p.thin)[1];
14963
+ const probe = side + sign2 * 1e-3;
14964
+ return gridOf(p).some((c, k) => gone(k) && Math.abs(probe - c.center[a]) <= c.size[a] / 2 && Math.abs(cell.center[b] - c.center[b]) <= c.size[b] / 2);
14965
+ };
14966
+ if (at - lo <= reach && free(lo, -1)) return [lo, Math.max(crater2[1], lo)];
14967
+ if (hi - at <= reach && free(hi, 1)) return [Math.min(crater2[0], hi), hi];
14968
+ return crater2;
14969
+ };
14970
+ const craterAcross = (energy) => clamp2(0.03 + 12e-4 * Math.sqrt(Math.max(0, energy)), 0.03, 0.14);
14971
+ const craterFragments = (o, p, cell, dir, speed, mass, tag) => {
14972
+ const { c, q } = partXf(o, p);
14973
+ const [u, v] = planeAxes(p.thin);
14974
+ const d = dir ?? rotateVec(q, [p.thin === 0 ? 1 : 0, p.thin === 1 ? 1 : 0, p.thin === 2 ? 1 : 0]);
14975
+ const exitSign = Math.sign(rotateVec(quat.conjugate(q), d)[p.thin]) || 1;
14976
+ const bit = Math.max(0.01, Math.min(cell.size[u], cell.size[v]) / 3);
14977
+ const n = clamp2(Math.round(cell.size[u] * cell.size[v] / (bit * bit * 3)), 4, 10) + Math.floor(rnd() * 3);
14978
+ const out = [];
14979
+ for (let i = 0; i < n; i++) {
14980
+ const back = i % 3 === 2;
14981
+ const lc = [cell.center[0], cell.center[1], cell.center[2]];
14982
+ lc[u] += (rnd() - 0.5) * cell.size[u] * 0.7;
14983
+ lc[v] += (rnd() - 0.5) * cell.size[v] * 0.7;
14984
+ lc[p.thin] += (back ? -exitSign : exitSign) * (p.thickness / 2 + 0.01);
14985
+ const size = [0, 0, 0];
14986
+ size[u] = bit * (0.6 + 0.8 * rnd());
14987
+ size[v] = bit * (0.5 + 0.7 * rnd());
14988
+ size[p.thin] = bit * (0.4 + 0.6 * rnd());
14989
+ const spread3 = [(rnd() - 0.5) * 0.9, (rnd() - 0.3) * 0.9, (rnd() - 0.5) * 0.9];
14990
+ const k = speed * (back ? 0.4 + 0.4 * rnd() : 0.7 + 0.8 * rnd());
14991
+ const vdir = vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3));
14992
+ out.push({
14993
+ id: `${p.spec.id}#${tag}${i}`,
14994
+ part: p.spec.id,
14995
+ material: p.spec.material,
14996
+ shape: "chunk",
14997
+ center: toWorld(c, q, lc),
14998
+ size,
14999
+ rotation: q,
15000
+ velocity: [vdir[0] * k, vdir[1] * k + 0.5 * rnd(), vdir[2] * k],
15001
+ angularVelocity: [(rnd() - 0.5) * 20, (rnd() - 0.5) * 20, (rnd() - 0.5) * 20],
15002
+ mass: mass / n,
15003
+ inner: true
15004
+ });
15005
+ }
15006
+ return out;
15007
+ };
14890
15008
  const exposeRebar = (o, p, src) => {
14891
15009
  const r = p.spec.reinforcement;
14892
15010
  if (!r) return;
@@ -15174,7 +15292,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15174
15292
  });
15175
15293
  }
15176
15294
  const patch = [scale * 4, scale * 4, scale * 4];
15177
- emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments });
15295
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments, torn: true });
15178
15296
  };
15179
15297
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
15180
15298
  const mat = p.mat;
@@ -15233,12 +15351,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15233
15351
  } else if (mat.ductility >= 0.5) {
15234
15352
  addDent(o, p, entry2, n, Math.max(caliber * 3 / 1e3, 0.012), clamp2(Math.min(Math.max(p.thickness * 3, 3e-3), ratio * 0.01), 1e-3, 0.05), src);
15235
15353
  } else if (brittle) {
15236
- addCrack(o, p, entry2, n, crackLen, src);
15354
+ if (!p.spec.units) addCrack(o, p, entry2, n, crackLen, src);
15237
15355
  if (mat.fracture !== "shatter") addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
15238
15356
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
15239
15357
  debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
15240
15358
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
15241
- const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
15359
+ const hollowTear = mat.fracture === "tear" && p.spec.fill !== void 0 && p.spec.fill < 0.5;
15360
+ const absorbed = penetrated && (mat.ductility >= 0.5 && mat.fracture !== "tear" || hollowTear) ? Math.min(E, thr) : E;
15242
15361
  const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
15243
15362
  if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
15244
15363
  chipUnits(o, p, "ballistic", entry2, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
@@ -15323,10 +15442,25 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15323
15442
  if (dist <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
15324
15443
  return reach;
15325
15444
  };
15445
+ const blastProbe = { partsTested: 0, partsNear: 0 };
15446
+ const blastCandidates = (o, point, radius) => {
15447
+ const local = toLocal(o.st.position, o.st.rotation, point);
15448
+ const near = [];
15449
+ for (const p of o.parts) {
15450
+ const reach = radius + p.bound;
15451
+ const dx = local[0] - p.pos[0];
15452
+ const dy = local[1] - p.pos[1];
15453
+ const dz = local[2] - p.pos[2];
15454
+ if (dx * dx + dy * dy + dz * dz < reach * reach) near.push(p);
15455
+ }
15456
+ blastProbe.partsTested += o.parts.length;
15457
+ blastProbe.partsNear += near.length;
15458
+ return near;
15459
+ };
15326
15460
  const blastObject = (o, point, energy, radius, src) => {
15327
15461
  const start = cur.length;
15328
15462
  const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
15329
- for (const p of o.parts) blastPart(o, p, point, energy, radius, src, res);
15463
+ for (const p of blastCandidates(o, point, radius)) blastPart(o, p, point, energy, radius, src, res);
15330
15464
  res.events = cur.slice(start);
15331
15465
  return res;
15332
15466
  };
@@ -15450,7 +15584,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15450
15584
  let order = 0;
15451
15585
  for (const o of inRange) {
15452
15586
  if (only && only !== o) continue;
15453
- for (const p of o.parts) {
15587
+ for (const p of blastCandidates(o, point, radius)) {
15454
15588
  if (isLost(p)) continue;
15455
15589
  const dist = distToPart(o, p, point);
15456
15590
  if (dist < radius) items.push({ dist, order: order++, o, p });
@@ -15997,6 +16131,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15997
16131
  const p = getPart(o, partId);
15998
16132
  p.pos = [position[0], position[1], position[2]];
15999
16133
  p.q = q;
16134
+ poseVersion++;
16000
16135
  updateBound(o);
16001
16136
  },
16002
16137
  remove(id) {
@@ -16015,6 +16150,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16015
16150
  const o = getObj(id);
16016
16151
  o.base = { pos: [position[0], position[1], position[2]], rot: [rotation[0], rotation[1], rotation[2], rotation[3]] };
16017
16152
  applySoilPose(o);
16153
+ slideOffResting(o, rotation);
16018
16154
  },
16019
16155
  splitGroup(id, group, newId) {
16020
16156
  if (impls.has(id)) throw new Error(`object "${id}" is a registered DestructibleImpl and cannot be split; its module owns its pieces`);
@@ -16038,7 +16174,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16038
16174
  if (o.spec.name !== void 0) s.name = o.spec.name;
16039
16175
  if (o.spec.tags) s.tags = [...o.spec.tags];
16040
16176
  if (o.spec.soil !== void 0) s.soil = o.spec.soil;
16041
- return clone(s);
16177
+ return s;
16042
16178
  };
16043
16179
  const inNew = (pid) => moved.has(pid);
16044
16180
  const inOld = (pid) => !moved.has(pid);
@@ -16065,7 +16201,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16065
16201
  const p = getPart(o, partId);
16066
16202
  return whole(() => doHit(o, h, p));
16067
16203
  },
16204
+ blastProbe() {
16205
+ return { ...blastProbe };
16206
+ },
16068
16207
  explode(point, energy, radius, source, blast) {
16208
+ blastProbe.partsTested = 0;
16209
+ blastProbe.partsNear = 0;
16069
16210
  return withCollapses(run(() => blastWorld(point, energy, radius, source, blast)));
16070
16211
  },
16071
16212
  raycast(origin, direction, maxDistance = Infinity) {
@@ -16075,7 +16216,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16075
16216
  sweepBox(box3, direction, distance2, options2 = {}) {
16076
16217
  const d = vec.normalize(direction);
16077
16218
  const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
16078
- const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
16219
+ const hosted = options2.ignore === void 0 || ignored ? void 0 : impls.get(options2.ignore);
16220
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : hosted ? new Set(hosted.impl.describe().parts.map((p) => `${hosted.impl.id}\0${p.id}`)) : void 0;
16079
16221
  let best = null;
16080
16222
  for (const origin of sweepOrigins(box3, d, options2.spacing ?? 0.2)) {
16081
16223
  const r = raycastInternal(origin, d, distance2 + SWEEP_BACK, ignore);
@@ -19589,6 +19731,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19589
19731
  function collapseRange(mesh2, v0, v1) {
19590
19732
  const pos = mesh2.geometry.attributes.position;
19591
19733
  const a = pos.array;
19734
+ const u = mesh2.userData;
19735
+ if (!u._p0) u._p0 = a.slice();
19736
+ const list2 = u.collapsedRanges ?? (u.collapsedRanges = []);
19737
+ if (!list2.some((r) => r[0] === v0 && r[1] === v1)) list2.push([v0, v1]);
19592
19738
  const saved = a.slice(v0 * 3, v1 * 3);
19593
19739
  const x = a[v0 * 3], y = a[v0 * 3 + 1], z = a[v0 * 3 + 2];
19594
19740
  for (let k = v0; k < v1; k++) {
@@ -19603,6 +19749,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19603
19749
  const pos = mesh2.geometry.attributes.position;
19604
19750
  pos.array.set(saved, v0 * 3);
19605
19751
  pos.needsUpdate = true;
19752
+ const list2 = mesh2.userData.collapsedRanges;
19753
+ if (list2) mesh2.userData.collapsedRanges = list2.filter((r) => r[0] !== v0);
19606
19754
  }
19607
19755
  function noise2(x, y, cell) {
19608
19756
  const h = (i2, j2) => {
@@ -19617,13 +19765,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19617
19765
  var SPALL_MIN = 0.025;
19618
19766
  var SPALL_RAGGED = 0.045;
19619
19767
  var COVER2 = 0.015;
19768
+ var MAX_FLAKES = 64;
19769
+ function clipToBox(unit, box3) {
19770
+ const lo = [Math.max(unit.x - unit.sx / 2, box3[0]), Math.max(unit.y - unit.sy / 2, box3[1]), Math.max(unit.z - unit.sz / 2, box3[2])];
19771
+ const hi = [Math.min(unit.x + unit.sx / 2, box3[3]), Math.min(unit.y + unit.sy / 2, box3[4]), Math.min(unit.z + unit.sz / 2, box3[5])];
19772
+ if (hi[0] - lo[0] <= 1e-4 || hi[1] - lo[1] <= 1e-4 || hi[2] - lo[2] <= 1e-4) return null;
19773
+ return { x: (lo[0] + hi[0]) / 2, y: (lo[1] + hi[1]) / 2, z: (lo[2] + hi[2]) / 2, sx: hi[0] - lo[0], sy: hi[1] - lo[1], sz: hi[2] - lo[2], colour: unit.colour };
19774
+ }
19620
19775
  function installDestructionBreach(K, _shared) {
19621
19776
  const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
19622
19777
  function createBreachView(opts) {
19623
19778
  const { house, structure } = opts;
19624
19779
  const views = /* @__PURE__ */ new Map();
19780
+ const dirty = /* @__PURE__ */ new Set();
19781
+ const paintBudgetMs = opts.paintBudgetMs ?? Infinity;
19782
+ const dropped = [];
19625
19783
  let lod = 0;
19626
- const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
19627
19784
  const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
19628
19785
  const tmp = { m: new THREE.Matrix4(), q: new THREE.Quaternion(), p: new THREE.Vector3(), s: new THREE.Vector3(), c: new THREE.Color() };
19629
19786
  const local = (p) => {
@@ -19692,6 +19849,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19692
19849
  const mesh2 = new THREE.Mesh(g, material);
19693
19850
  mesh2.name = "breach-shell";
19694
19851
  mesh2.userData.breachMask = mask;
19852
+ mesh2.userData.cutAt = (p) => {
19853
+ const v = local([p.x, p.y, p.z]);
19854
+ const i = Math.floor(((mask.alongU ? v[0] : v[2]) - mask.start) / mask.len * mask.w), j = Math.floor((v[1] - mask.y0) / mask.height * mask.h);
19855
+ return i >= 0 && i < mask.w && j >= 0 && j < mask.h && mask.data[j * mask.w + i] > 127;
19856
+ };
19695
19857
  mesh2.position.copy(it.mesh.position);
19696
19858
  mesh2.quaternion.copy(it.mesh.quaternion);
19697
19859
  mesh2.scale.copy(it.mesh.scale);
@@ -19707,6 +19869,29 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19707
19869
  toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
19708
19870
  return mesh2;
19709
19871
  }
19872
+ function unitsMaterial(core) {
19873
+ const material = new THREE.MeshLambertMaterial({ color: 16777215, side: THREE.DoubleSide });
19874
+ material.onBeforeCompile = (sh) => {
19875
+ sh.uniforms.uCore = { value: core.texture };
19876
+ sh.uniforms.uCoreRect = { value: core.rect };
19877
+ sh.uniforms.uCoreAlongX = { value: core.alongU ? 1 : 0 };
19878
+ sh.vertexShader = sh.vertexShader.replace("void main() {", "varying vec3 vHousePos;\nvoid main() {").replace("#include <begin_vertex>", "#include <begin_vertex>\n#ifdef USE_INSTANCING\nvHousePos = (instanceMatrix * vec4(transformed, 1.0)).xyz;\n#else\nvHousePos = transformed;\n#endif");
19879
+ sh.fragmentShader = sh.fragmentShader.replace("void main() {", "uniform sampler2D uCore;\nuniform vec4 uCoreRect;\nuniform float uCoreAlongX;\nvarying vec3 vHousePos;\nvoid main() {\n vec2 cUv = vec2(((uCoreAlongX > 0.5 ? vHousePos.x : vHousePos.z) - uCoreRect.x) / uCoreRect.z, (vHousePos.y - uCoreRect.y) / uCoreRect.w);\n if (cUv.x >= 0.0 && cUv.x <= 1.0 && cUv.y >= 0.0 && cUv.y <= 1.0 && texture2D(uCore, cUv).r > 0.5) discard;").replace("#include <color_fragment>", "#include <color_fragment>\nif (!gl_FrontFacing) diffuseColor.rgb *= 0.55;");
19880
+ };
19881
+ material.customProgramCacheKey = () => "breachCore";
19882
+ material.userData.breachCore = core;
19883
+ return material;
19884
+ }
19885
+ function drawnBounds(items) {
19886
+ house.updateMatrixWorld(true);
19887
+ const box3 = new THREE.Box3(), toHouse = new THREE.Matrix4().copy(house.matrixWorld).invert();
19888
+ for (const it of items) {
19889
+ const a = it.mesh.userData._p0 || it.mesh.geometry.attributes.position.array;
19890
+ tmp.m.multiplyMatrices(toHouse, it.mesh.matrixWorld);
19891
+ for (let k = it.v0; k < it.v1; k++) box3.expandByPoint(tmp.p.set(a[k * 3], a[k * 3 + 1], a[k * 3 + 2]).applyMatrix4(tmp.m));
19892
+ }
19893
+ return [box3.min.x, box3.min.y, box3.min.z, box3.max.x, box3.max.y, box3.max.z];
19894
+ }
19710
19895
  function maskOf(hostId, parts) {
19711
19896
  const host = structure.byId.get(hostId);
19712
19897
  const [u0, v0, u1, v1] = host.aabb;
@@ -19722,51 +19907,86 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19722
19907
  return { data, texture, w, h, alongU, start: alongU ? u0 : v0, len, y0, height: y1 - y0, rect: new THREE.Vector4(alongU ? u0 : v0, y0, len, y1 - y0) };
19723
19908
  }
19724
19909
  const boxDistance = (s, y, a, b, c, d) => Math.hypot(Math.max(a - s, 0, s - b), Math.max(c - y, 0, y - d));
19910
+ const ellipseDistance = (s, y, cs, cy, rs, ry) => (Math.hypot((s - cs) / Math.max(rs, 1e-4), (y - cy) / Math.max(ry, 1e-4)) - 1) * Math.min(rs, ry);
19911
+ function covered(c, boxes) {
19912
+ const n = 8;
19913
+ let hit = 0;
19914
+ for (let j = 0; j < n; j++) {
19915
+ const y = c.y0 + (j + 0.5) / n * (c.y1 - c.y0);
19916
+ for (let i = 0; i < n; i++) {
19917
+ const s = c.a + (i + 0.5) / n * (c.b - c.a);
19918
+ if (boxes.some((k) => s >= k.s0 && s <= k.s1 && y >= k.y0 && y <= k.y1)) hit++;
19919
+ }
19920
+ }
19921
+ return hit / (n * n);
19922
+ }
19725
19923
  function paint(view) {
19726
- const { mask } = view;
19924
+ const { mask, core } = view;
19727
19925
  const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
19728
19926
  const holes = [];
19927
+ const pits = [];
19729
19928
  const discs = [];
19929
+ const flakes = [];
19730
19930
  const shown = [];
19731
19931
  for (const id of view.parts) {
19732
- const part = structure.byId.get(id), units2 = specOf(id).units, chips = view.chips.get(id), opening = view.openings.get(id);
19932
+ const part = structure.byId.get(id), units2 = specOf(id).units, chips = view.chips.get(id), opening = view.openings.get(id), spalls = view.spalls.get(id);
19733
19933
  const all = cells(part, units2);
19734
19934
  const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
19735
19935
  const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
19736
19936
  if (opening) discs.push({ s: along(opening.center), y: opening.center[1], r: opening.radius });
19737
- const gone = (c) => chips.some((p) => p[1] >= c.y0 && p[1] <= c.y1 && along(p) >= c.a && along(p) <= c.b) || opening && Math.hypot((c.a + c.b) / 2 - along(opening.center), (c.y0 + c.y1) / 2 - opening.center[1]) < opening.radius;
19937
+ for (const k of spalls) flakes.push({ s: along(k.center), y: k.center[1], r: k.radius });
19938
+ const boxes = chips.map((k) => ({ s0: along(k.center) - k.along, s1: along(k.center) + k.along, y0: k.center[1] - k.up, y1: k.center[1] + k.up }));
19939
+ const gone = (c) => covered(c, boxes.filter((k) => k.s1 > c.a && k.s0 < c.b && k.y1 > c.y0 && k.y0 < c.y1)) >= 0.9 || opening && Math.hypot((c.a + c.b) / 2 - along(opening.center), (c.y0 + c.y1) / 2 - opening.center[1]) < opening.radius;
19738
19940
  const kept = [];
19739
19941
  for (const c of all) (gone(c) ? holes : kept).push(c);
19740
19942
  for (const c of kept) shown.push({ c, part, units: units2, thick, mid });
19943
+ for (const k of boxes) {
19944
+ if (holes.some((h) => k.s0 >= h.a - 1e-4 && k.s1 <= h.b + 1e-4 && k.y0 >= h.y0 - 1e-4 && k.y1 <= h.y1 + 1e-4)) continue;
19945
+ pits.push({ s: (k.s0 + k.s1) / 2, y: (k.y0 + k.y1) / 2, rs: (k.s1 - k.s0) / 2, ry: (k.y1 - k.y0) / 2 });
19946
+ }
19741
19947
  }
19742
19948
  const reach = SPALL_MIN + SPALL_RAGGED;
19949
+ const ragged = (s, y) => 0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011);
19743
19950
  mask.data.fill(0);
19744
- const stamp = (s0, s1, y0, y1, inside2) => {
19745
- const i0 = Math.max(0, Math.floor((s0 - reach) / mask.len * mask.w)), i1 = Math.min(mask.w - 1, Math.ceil((s1 + reach) / mask.len * mask.w));
19746
- const j0 = Math.max(0, Math.floor((y0 - reach - mask.y0) / mask.height * mask.h)), j1 = Math.min(mask.h - 1, Math.ceil((y1 + reach - mask.y0) / mask.height * mask.h));
19951
+ core.data.fill(0);
19952
+ const stamp = (m, s0, s1, y0, y1, inside2) => {
19953
+ const i0 = Math.max(0, Math.floor((s0 - reach) / m.len * m.w)), i1 = Math.min(m.w - 1, Math.ceil((s1 + reach) / m.len * m.w));
19954
+ const j0 = Math.max(0, Math.floor((y0 - reach - m.y0) / m.height * m.h)), j1 = Math.min(m.h - 1, Math.ceil((y1 + reach - m.y0) / m.height * m.h));
19747
19955
  for (let j = j0; j <= j1; j++) {
19748
- const y = mask.y0 + (j + 0.5) / mask.h * mask.height;
19956
+ const y = m.y0 + (j + 0.5) / m.h * m.height;
19749
19957
  for (let i = i0; i <= i1; i++) {
19750
- const s = (i + 0.5) / mask.w * mask.len;
19751
- const edge = SPALL_MIN + SPALL_RAGGED * (0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011));
19752
- if (inside2(s, y) <= edge) mask.data[j * mask.w + i] = 255;
19958
+ const s = (i + 0.5) / m.w * m.len;
19959
+ if (inside2(s, y)) m.data[j * m.w + i] = 255;
19753
19960
  }
19754
19961
  }
19755
19962
  };
19756
- for (const c of holes) stamp(c.a, c.b, c.y0, c.y1, (s, y) => boxDistance(s, y, c.a, c.b, c.y0, c.y1));
19757
- for (const d of discs) stamp(d.s - d.r, d.s + d.r, d.y - d.r, d.y + d.r, (s, y) => Math.max(0, Math.hypot(s - d.s, y - d.y) - d.r));
19963
+ const plaster = (d, s, y) => d <= SPALL_MIN + SPALL_RAGGED * ragged(s, y);
19964
+ for (const c of holes) stamp(mask, c.a, c.b, c.y0, c.y1, (s, y) => plaster(boxDistance(s, y, c.a, c.b, c.y0, c.y1), s, y));
19965
+ for (const d of discs) stamp(mask, d.s - d.r, d.s + d.r, d.y - d.r, d.y + d.r, (s, y) => plaster(Math.max(0, Math.hypot(s - d.s, y - d.y) - d.r), s, y));
19966
+ for (const f of flakes) {
19967
+ const rough = (s, y) => Math.hypot(s - f.s, y - f.y) - f.r * (0.65 + 0.7 * ragged(s * 2.3, y * 2.3));
19968
+ stamp(mask, f.s - 2 * f.r, f.s + 2 * f.r, f.y - 2 * f.r, f.y + 2 * f.r, (s, y) => rough(s, y) <= 0);
19969
+ }
19970
+ for (const p of pits) {
19971
+ stamp(mask, p.s - p.rs, p.s + p.rs, p.y - p.ry, p.y + p.ry, (s, y) => plaster(Math.max(0, ellipseDistance(s, y, p.s, p.y, p.rs, p.ry)), s, y));
19972
+ stamp(core, p.s - p.rs, p.s + p.rs, p.y - p.ry, p.y + p.ry, (s, y) => ellipseDistance(s, y, p.s, p.y, p.rs, p.ry) <= -0.35 * Math.min(p.rs, p.ry) * noise2(s * 1.7, y * 1.3, 0.012));
19973
+ }
19758
19974
  mask.texture.needsUpdate = true;
19759
- const near = (c) => holes.some((h) => boxDistance((c.a + c.b) / 2, (c.y0 + c.y1) / 2, h.a - (c.b - c.a) / 2, h.b + (c.b - c.a) / 2, h.y0 - (c.y1 - c.y0) / 2, h.y1 + (c.y1 - c.y0) / 2) <= reach) || discs.some((d) => boxDistance(d.s, d.y, c.a, c.b, c.y0, c.y1) <= d.r + reach);
19975
+ core.texture.needsUpdate = true;
19976
+ const near = (c) => holes.some((h) => boxDistance((c.a + c.b) / 2, (c.y0 + c.y1) / 2, h.a - (c.b - c.a) / 2, h.b + (c.b - c.a) / 2, h.y0 - (c.y1 - c.y0) / 2, h.y1 + (c.y1 - c.y0) / 2) <= reach) || discs.some((d) => boxDistance(d.s, d.y, c.a, c.b, c.y0, c.y1) <= d.r + reach) || pits.some((p) => boxDistance(p.s, p.y, c.a, c.b, c.y0, c.y1) <= Math.max(p.rs, p.ry) + reach) || flakes.some((f) => boxDistance(f.s, f.y, c.a, c.b, c.y0, c.y1) <= 2 * f.r);
19760
19977
  const list2 = [];
19761
19978
  const tone = (hex2, k) => tmp.c.setHex(hex2).multiplyScalar(k).getHex();
19762
19979
  for (const { c, part, units: units2, thick, mid } of shown) {
19763
19980
  if (!near(c)) continue;
19764
19981
  const centre = mask.start + (c.a + c.b) / 2, y = (c.y0 + c.y1) / 2, len = c.b - c.a, h = c.y1 - c.y0;
19765
- const at = (s, dy, d, colour) => list2.push({ x: mask.alongU ? centre : mid, y, z: mask.alongU ? mid : centre, sx: mask.alongU ? s : d, sy: dy, sz: mask.alongU ? d : s, colour });
19982
+ const at = (s, dy, d, colour) => {
19983
+ const unit = clipToBox({ x: mask.alongU ? centre : mid, y, z: mask.alongU ? mid : centre, sx: mask.alongU ? s : d, sy: dy, sz: mask.alongU ? d : s, colour }, view.bounds);
19984
+ if (unit) list2.push(unit);
19985
+ };
19766
19986
  const base = fragmentMaterial(part.material).color.getHex();
19767
19987
  const n = noise2(centre * 3.1, y * 2.7, 0.07);
19768
19988
  if (units2.mortar === "concrete") {
19769
- at(len, h, thick - 2 * COVER2 - 0.03 * n, tone(base, 0.86 + 0.2 * n));
19989
+ at(len, h, thick - 2 * COVER2, tone(base, 0.9 + 0.04 * n));
19770
19990
  } else {
19771
19991
  at(len, h, thick - 2 * COVER2 - 0.012, 10327948);
19772
19992
  at(len - 0.012, h - 0.012, thick - 2 * COVER2, tone(base, 0.8 + 0.35 * n));
@@ -19776,7 +19996,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19776
19996
  view.units.removeFromParent();
19777
19997
  view.units.dispose();
19778
19998
  }
19779
- const units = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
19999
+ const units = new THREE.InstancedMesh(unitGeometry, view.unitMaterial, Math.max(1, list2.length));
19780
20000
  units.name = "breach:" + view.host;
19781
20001
  list2.forEach((u, i) => {
19782
20002
  tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
@@ -19792,6 +20012,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19792
20012
  house.add(units);
19793
20013
  view.units = units;
19794
20014
  view.gone = holes.length;
20015
+ view.pits = pits.length;
20016
+ view.flakes = flakes.length;
20017
+ }
20018
+ function chipBox(chip, alongU) {
20019
+ house.updateMatrixWorld(true);
20020
+ const q = tmp.q.set(chip.rotation[0], chip.rotation[1], chip.rotation[2], chip.rotation[3]).premultiply(house.getWorldQuaternion(new THREE.Quaternion()).invert());
20021
+ let a = 0, u = 0;
20022
+ for (let k = 0; k < 3; k++) {
20023
+ const axis = tmp.p.set(k === 0 ? 1 : 0, k === 1 ? 1 : 0, k === 2 ? 1 : 0).applyQuaternion(q);
20024
+ a += Math.abs(alongU ? axis.x : axis.z) * chip.size[k] / 2;
20025
+ u += Math.abs(axis.y) * chip.size[k] / 2;
20026
+ }
20027
+ return { center: local(chip.center), along: a, up: u };
19795
20028
  }
19796
20029
  function draw2(partId, change) {
19797
20030
  if (lod !== 0) return false;
@@ -19803,23 +20036,50 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19803
20036
  if (!items.length) return false;
19804
20037
  const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
19805
20038
  if (!parts.includes(partId)) return false;
19806
- const mask = maskOf(hostId, parts);
19807
- view = { host: hostId, parts, items, mask, shells: [], saved: null, units: null, gone: 0, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
20039
+ const mask = maskOf(hostId, parts), core = maskOf(hostId, parts);
20040
+ view = {
20041
+ host: hostId,
20042
+ parts,
20043
+ items,
20044
+ mask,
20045
+ core,
20046
+ unitMaterial: unitsMaterial(core),
20047
+ shells: [],
20048
+ saved: null,
20049
+ units: null,
20050
+ gone: 0,
20051
+ pits: 0,
20052
+ bounds: drawnBounds(items),
20053
+ chips: new Map(parts.map((id) => [id, []])),
20054
+ spalls: new Map(parts.map((id) => [id, []])),
20055
+ openings: /* @__PURE__ */ new Map(),
20056
+ flakes: 0
20057
+ };
19808
20058
  }
19809
20059
  if (!view.chips.has(partId)) return false;
19810
20060
  if (change.opening) {
19811
20061
  if (view.openings.has(partId)) return false;
19812
20062
  view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
19813
20063
  }
19814
- if (change.chip) view.chips.get(partId).push(local(change.chip));
20064
+ if (change.chip) view.chips.get(partId).push(chipBox(change.chip, view.mask.alongU));
20065
+ if (change.spall) {
20066
+ const list2 = view.spalls.get(partId);
20067
+ list2.push({ center: local(change.spall.center), radius: change.spall.radius });
20068
+ if (list2.length > MAX_FLAKES) list2.shift();
20069
+ }
19815
20070
  if (!view.saved) {
19816
20071
  view.shells = view.items.map((it) => shell(it, view.mask));
20072
+ for (const m of view.shells) m.userData.behind = () => view.units;
19817
20073
  view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
19818
20074
  }
19819
- paint(view);
19820
20075
  views.set(hostId, view);
19821
- invalidate();
19822
- invalidateShadow();
20076
+ if (opts.deferPaint && change.spall) dirty.add(view);
20077
+ else {
20078
+ dirty.delete(view);
20079
+ paint(view);
20080
+ invalidate();
20081
+ invalidateShadow();
20082
+ }
19823
20083
  return true;
19824
20084
  }
19825
20085
  function plain2(view, shown) {
@@ -19828,24 +20088,70 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19828
20088
  else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
19829
20089
  }
19830
20090
  }
20091
+ function flush(budgetMs = paintBudgetMs) {
20092
+ if (dirty.size === 0) return 0;
20093
+ const deadline = performance.now() + budgetMs;
20094
+ for (const view of dirty) {
20095
+ dirty.delete(view);
20096
+ paint(view);
20097
+ if (performance.now() >= deadline) break;
20098
+ }
20099
+ invalidate();
20100
+ invalidateShadow();
20101
+ return dirty.size;
20102
+ }
19831
20103
  function dispose(view) {
19832
- view.units.removeFromParent();
19833
- view.units.dispose();
20104
+ dirty.delete(view);
20105
+ if (view.units) {
20106
+ view.units.removeFromParent();
20107
+ view.units.dispose();
20108
+ }
20109
+ view.unitMaterial.dispose();
19834
20110
  for (const m of view.shells) {
19835
20111
  m.removeFromParent();
19836
20112
  m.geometry.dispose();
19837
20113
  m.material.dispose();
19838
20114
  }
19839
20115
  view.mask.texture.dispose();
20116
+ view.core.texture.dispose();
19840
20117
  }
19841
20118
  return {
19842
20119
  /** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
19843
20120
  stage(event) {
19844
20121
  return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
19845
20122
  },
19846
- /** Handles a `chip` event: the unit that came out leaves a hole in the wall where it was. Returns true when the wall was cut. */
20123
+ /**
20124
+ * Handles a `spall` of a wall of units: the coat comes off in a ragged patch and the units show under it, drawn by the house (the destruction
20125
+ * renderer leaves its flat patch out on the meshes `claim` marked). Returns true when the wall was cut.
20126
+ */
20127
+ spall(event) {
20128
+ const r = Math.sqrt(Math.max(0, event.area) / Math.PI);
20129
+ return r > 0 ? draw2(event.part, { spall: { center: event.point, radius: r } }) : false;
20130
+ },
20131
+ /**
20132
+ * Marks the meshes that draw a wall of units as drawing its spalls itself (`userData.drawnSpalls`, the part ids): the destruction renderer then
20133
+ * throws the bits and leaves its patch out. Call it once the spec is known.
20134
+ */
20135
+ claim() {
20136
+ var _a;
20137
+ for (const q of opts.spec()?.parts ?? []) {
20138
+ if (!q.units) continue;
20139
+ const host = hostOf(q.id);
20140
+ if (!host) continue;
20141
+ for (const it of structure.meshesOf(host)) {
20142
+ if (!it.mesh) continue;
20143
+ const list2 = (_a = it.mesh.userData).drawnSpalls ?? (_a.drawnSpalls = []);
20144
+ if (!list2.includes(q.id)) list2.push(q.id);
20145
+ }
20146
+ }
20147
+ },
20148
+ /**
20149
+ * Handles a `chip` event: what came out (a unit, or a crater a shot broke through one) leaves a hole in the wall where it was; a `torn` chip (what a blow
20150
+ * tore off a face) leaves the marks the renderer drew. Returns true when the wall was cut.
20151
+ */
19847
20152
  chip(event) {
19848
- return draw2(event.part, { chip: event.center });
20153
+ if (event.torn) return false;
20154
+ return draw2(event.part, { chip: { center: event.center, size: event.size ?? [0, 0, 0], rotation: event.rotation ?? [0, 0, 0, 1] } });
19849
20155
  },
19850
20156
  /** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
19851
20157
  drop(partId) {
@@ -19853,9 +20159,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19853
20159
  if (!v) return;
19854
20160
  dispose(v);
19855
20161
  views.delete(partId);
20162
+ if (v.saved) dropped.push(v);
19856
20163
  },
19857
20164
  /** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
19858
20165
  setLod(level) {
20166
+ flush(Infinity);
19859
20167
  const was = lod;
19860
20168
  lod = level;
19861
20169
  if (was === 0 === (level === 0)) return;
@@ -19874,15 +20182,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19874
20182
  if (lod === 0) plain2(v, true);
19875
20183
  }
19876
20184
  views.clear();
20185
+ for (const v of dropped) plain2(v, true);
20186
+ dropped.length = 0;
19877
20187
  },
20188
+ flush,
19878
20189
  /** The wall has damage drawn. */
19879
20190
  has(partId) {
19880
20191
  return views.has(partId);
19881
20192
  },
19882
- /** Walls with damage drawn, units out of them, and units laid bare at the edge of the cuts. */
19883
- statistics: () => ({
20193
+ /** Walls with damage drawn, units out of them, craters shots broke through units that stay, and units laid bare at the edge of the cuts. */
20194
+ statistics: () => (flush(Infinity), {
19884
20195
  walls: views.size,
19885
20196
  gone: [...views.values()].reduce((n, v) => n + v.gone, 0),
20197
+ craters: [...views.values()].reduce((n, v) => n + v.pits, 0),
20198
+ flakes: [...views.values()].reduce((n, v) => n + v.flakes, 0),
19886
20199
  units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
19887
20200
  })
19888
20201
  };
@@ -19891,6 +20204,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19891
20204
  }
19892
20205
 
19893
20206
  // src/destruction/world.ts
20207
+ var FOLLOW_FLUSH_FALLBACK_MS = 100;
19894
20208
  function installDestructionWorld(K, shared) {
19895
20209
  const { THREE, cfg, invalidateShadow, invalidate, rngString, FRAGMENT_GEO, fragmentMaterial, createBreachView } = K;
19896
20210
  function createHouseDestruction(root, house, structure, parts, colliders, water) {
@@ -19899,7 +20213,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19899
20213
  const R = cfg.destructionRenderer || null, listeners = /* @__PURE__ */ new Set();
19900
20214
  D.renderer = R;
19901
20215
  D.spec = null;
19902
- const breach = createBreachView({ house, structure, spec: () => D.spec });
20216
+ const breach = createBreachView({ house, structure, spec: () => D.spec, deferPaint: true, ...cfg.breachPaintBudgetMs !== void 0 ? { paintBudgetMs: cfg.breachPaintBudgetMs } : {} });
19903
20217
  D.breach = breach;
19904
20218
  D.onEvent = (fn) => {
19905
20219
  listeners.add(fn);
@@ -20084,6 +20398,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20084
20398
  case "chip":
20085
20399
  breach.chip(e);
20086
20400
  break;
20401
+ case "spall":
20402
+ breach.spall(e);
20403
+ break;
20087
20404
  case "collapse":
20088
20405
  for (const id of e.parts) {
20089
20406
  breach.drop(id);
@@ -20125,7 +20442,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20125
20442
  D.specOptions = specOptions || null;
20126
20443
  D.spec = structure.spec(specOptions);
20127
20444
  D.id = D.world.add(D.spec, { id: "house3d:" + ++shared.destructionSeq, position: [p.x, p.y, p.z], rotation: [q.x, q.y, q.z, q.w] });
20128
- if (R) R.bind(D.id, house, { parts: rangesOf() });
20445
+ breach.claim();
20446
+ if (R) R.bind(D.id, house, { parts: rangesOf(), onFrame: () => breach.flush() });
20129
20447
  off = D.world.on("*", handleEvent);
20130
20448
  D.active = true;
20131
20449
  return D;
@@ -20140,6 +20458,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20140
20458
  };
20141
20459
  D.follow = (world, id, spec) => {
20142
20460
  D.spec = spec;
20461
+ breach.claim();
20462
+ let fallback = null;
20143
20463
  const offFollow = world.on("*", (e) => {
20144
20464
  if (e.object !== id) return;
20145
20465
  switch (e.type) {
@@ -20149,6 +20469,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20149
20469
  case "chip":
20150
20470
  breach.chip(e);
20151
20471
  break;
20472
+ case "spall":
20473
+ breach.spall(e);
20474
+ break;
20152
20475
  case "fracture":
20153
20476
  breach.drop(e.part);
20154
20477
  break;
@@ -20158,10 +20481,16 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20158
20481
  default:
20159
20482
  break;
20160
20483
  }
20484
+ if (!fallback && typeof setTimeout === "function") fallback = setTimeout(() => {
20485
+ fallback = null;
20486
+ breach.flush(Infinity);
20487
+ }, FOLLOW_FLUSH_FALLBACK_MS);
20161
20488
  });
20162
20489
  return { restore: () => breach.restore(), dispose: () => {
20163
20490
  offFollow();
20164
20491
  breach.restore();
20492
+ }, update: () => {
20493
+ breach.flush();
20165
20494
  } };
20166
20495
  };
20167
20496
  D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
@@ -20194,6 +20523,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20194
20523
  return r;
20195
20524
  };
20196
20525
  D.animate = (dt) => {
20526
+ breach.flush();
20197
20527
  if (R) return;
20198
20528
  let moved = false;
20199
20529
  for (const [id, list2] of bodies) {
@@ -21658,6 +21988,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21658
21988
  houseRenderer = create3(THREE, {
21659
21989
  locale,
21660
21990
  lod: { interior: 10, hide: 40 },
21991
+ breachPaintBudgetMs: 4,
21661
21992
  ...options.house,
21662
21993
  quality: settings.quality,
21663
21994
  ...options.world ? { destruction: options.world } : {},
@@ -21813,9 +22144,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21813
22144
  host?.log(text.steps.no_door);
21814
22145
  return;
21815
22146
  }
21816
- p.house.interact(eyeOf(p));
22147
+ const fo = target.target;
22148
+ const reach = fo && host?.reachFor && fo.type === "swing" && fo.gs?.[0] && fo.handles?.[0] ? doorReach(p, fo) : null;
22149
+ if (reach && host?.reachFor) {
22150
+ host.reachFor({ ...reach, onGrip: () => void p.house.toggleDoor(fo) });
22151
+ } else p.house.interact(eyeOf(p));
21817
22152
  host?.log(fill(text.steps.door, { text: String(target.text) }));
21818
22153
  };
22154
+ const doorReach = (p, fo) => {
22155
+ const g = fo.gs[0];
22156
+ const pivot = fo.handles[0].pivot;
22157
+ const at = (angle) => {
22158
+ const c = Math.cos(angle);
22159
+ const s = Math.sin(angle);
22160
+ const v = new THREE.Vector3(pivot[0] * c + pivot[2] * s, pivot[1], -pivot[0] * s + pivot[2] * c).add(g.position);
22161
+ return g.parent ? g.parent.localToWorld(v) : v;
22162
+ };
22163
+ const camera = host?.camera;
22164
+ const eye = new THREE.Vector3(camera.position.x, 0, camera.position.z);
22165
+ const flat = (v) => Math.hypot(v.x - eye.x, v.z - eye.z);
22166
+ const closed = at(0);
22167
+ const open = at(fo.rot);
22168
+ const opening = !fo.open;
22169
+ return {
22170
+ handle: () => {
22171
+ g.updateMatrixWorld(true);
22172
+ return g.localToWorld(new THREE.Vector3(pivot[0], pivot[1], pivot[2])).toArray();
22173
+ },
22174
+ hinge: () => g.getWorldPosition(new THREE.Vector3()).toArray(),
22175
+ seconds: fo.locked || fo.blockedFlag ? 0.6 : 0.55,
22176
+ pull: opening ? flat(open) <= flat(closed) : flat(closed) <= flat(open),
22177
+ locked: !!(fo.locked || fo.blockedFlag)
22178
+ };
22179
+ };
21819
22180
  const strike = () => {
21820
22181
  const world = options.world;
21821
22182
  if (!world || !host) return;