@adatechnology/game-floorplan-three 0.7.0 → 0.8.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.
@@ -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,7 @@ 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;
13541
13543
  var BAR_BOW = 0.08;
13542
13544
  var BULLET_JOINT_SPAN = 0.3;
13543
13545
  var CUT_SLICE = 1;
@@ -13558,12 +13560,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13558
13560
  const cells = p.st.chipped;
13559
13561
  if (!cells?.length) return 0;
13560
13562
  const [u, v] = planeAxes(p.thin);
13561
- const along = p.dims[u] >= p.dims[v] ? u : v;
13563
+ const role = p.spec.structure?.role;
13564
+ const up = rotateVec(p.q, [0, 0, 0].map((_, k) => k === v ? 1 : 0));
13565
+ 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;
13566
+ const along = (role === "wall" || role === "partition" || role === "column") && upright >= 0 ? upright : p.dims[u] >= p.dims[v] ? u : v;
13562
13567
  const across = along === u ? v : u;
13563
13568
  let worst = 0;
13564
13569
  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];
13570
+ const spans = [];
13571
+ 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]);
13572
+ spans.sort((a, b) => a[0] - b[0]);
13573
+ let gone = 0, end = -Infinity;
13574
+ for (const [a, b] of spans) {
13575
+ if (b <= end) continue;
13576
+ gone += b - Math.max(a, end);
13577
+ end = b;
13578
+ }
13567
13579
  worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
13568
13580
  }
13569
13581
  return Math.min(1, worst);
@@ -13859,7 +13871,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13859
13871
  if (k < 0 || j.spec.kind !== "tie") return k;
13860
13872
  return partXf(o, o.parts[k]).c[1] > partXf(o, o.parts[r]).c[1] + 1e-6 ? k : -1;
13861
13873
  };
13874
+ let poseVersion = 0;
13875
+ const contactMemo = /* @__PURE__ */ new WeakMap();
13862
13876
  const contactBox = (p, support) => {
13877
+ let byTouching = contactMemo.get(p);
13878
+ if (!byTouching) contactMemo.set(p, byTouching = /* @__PURE__ */ new Map());
13879
+ const known = byTouching.get(support);
13880
+ if (known && known.version === poseVersion) return known.box;
13881
+ const box3 = measureContact(p, support);
13882
+ byTouching.set(support, { version: poseVersion, box: box3 });
13883
+ return box3;
13884
+ };
13885
+ const measureContact = (p, support) => {
13863
13886
  const A = aabbOf(p.spec.shape, p.pos, p.q), B = aabbOf(support.spec.shape, support.pos, support.q);
13864
13887
  const lo = [0, 0, 0], hi = [0, 0, 0];
13865
13888
  for (let k = 0; k < 3; k++) {
@@ -14654,7 +14677,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14654
14677
  p.st.holes.push(hole);
14655
14678
  if (p.st.holes.length > maxHoles) p.st.holes.shift();
14656
14679
  markDamaged(p);
14657
- emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {} });
14680
+ emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {}, ...p.spec.powder ? { powder: { ...p.spec.powder } } : {} });
14658
14681
  setStage(o, p, 1, {}, src);
14659
14682
  return hole;
14660
14683
  };
@@ -14671,6 +14694,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14671
14694
  const detachEvents = (o, opened, by, src) => {
14672
14695
  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
14696
  };
14697
+ const slideOffResting = (o, rotation) => {
14698
+ const upY = 1 - 2 * (rotation[0] * rotation[0] + rotation[2] * rotation[2]);
14699
+ if (upY >= RESTING_HOLD_COS) return;
14700
+ const opened = o.joints.filter((j) => j.spec.kind === "resting" && !j.st.open);
14701
+ if (opened.length === 0) return;
14702
+ run(() => {
14703
+ for (const j of opened) {
14704
+ j.st.open = true;
14705
+ j.st.openedBy = "break";
14706
+ }
14707
+ solveStructure(o);
14708
+ recomputeGroups(o);
14709
+ detachEvents(o, opened, "break");
14710
+ collapseCheck(o);
14711
+ });
14712
+ };
14674
14713
  const checkDestroyed = (o, src) => {
14675
14714
  if (!o.st.destroyed && o.parts.length > 0 && o.parts.every(isLost)) {
14676
14715
  o.st.destroyed = true;
@@ -14771,7 +14810,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14771
14810
  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
14811
  const unitsOf2 = (p) => p.spec.units ?? { size: [p.mat.fragmentSize?.[1] ?? 0.2, p.mat.fragmentSize?.[1] ?? 0.2, p.thickness], bond: "stack" };
14773
14812
  const FRAGMENT_SHAPE = { shatter: "shard", splinter: "splinter", tear: "scrap" };
14774
- const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag) => {
14813
+ const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag, inner = false) => {
14775
14814
  const { c, q } = partXf(o, p);
14776
14815
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
14777
14816
  const [u, v] = planeAxes(p.thin);
@@ -14795,7 +14834,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14795
14834
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
14796
14835
  angularVelocity: [(rnd() - 0.5) * 8, (rnd() - 0.5) * 8, (rnd() - 0.5) * 8],
14797
14836
  mass: mass * faceArea(cell) / total,
14798
- ...marks.length > 0 ? { marks } : {}
14837
+ ...marks.length > 0 ? { marks } : {},
14838
+ ...inner ? { inner } : {}
14799
14839
  };
14800
14840
  });
14801
14841
  };
@@ -14836,7 +14876,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14836
14876
  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
14877
  };
14838
14878
  const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
14839
- var _a, _b;
14879
+ var _a, _b, _c;
14840
14880
  if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
14841
14881
  const rule = p.mat.hits?.[kind];
14842
14882
  const perKg = rule?.unitJoules ?? chipEnergy;
@@ -14846,7 +14886,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14846
14886
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
14847
14887
  const grid = gridOf(p);
14848
14888
  const worn = (_a = p.rt).chips ?? (_a.chips = {});
14889
+ const cut = (_b = p.rt).craters ?? (_b.craters = {});
14849
14890
  const out = [];
14891
+ const shot = kind === "ballistic";
14850
14892
  for (let i = 0; i < grid.length; i++) {
14851
14893
  const cell = grid[i];
14852
14894
  if (worn[i] === -1) continue;
@@ -14859,18 +14901,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14859
14901
  worn[i] = total;
14860
14902
  continue;
14861
14903
  }
14862
- worn[i] = -1;
14863
- out.push(cell);
14904
+ if (!shot) {
14905
+ worn[i] = -1;
14906
+ out.push(cell);
14907
+ continue;
14908
+ }
14909
+ const size = [cell.size[0], cell.size[1], cell.size[2]];
14910
+ const center = [cell.center[0], cell.center[1], cell.center[2]];
14911
+ for (const a of [u, v]) {
14912
+ const w = Math.min(cell.size[a], craterAcross(hitEnergy) * (0.85 + 0.3 * rnd()));
14913
+ const lo = cell.center[a] - cell.size[a] / 2 + w / 2, hi = cell.center[a] + cell.size[a] / 2 - w / 2;
14914
+ size[a] = w;
14915
+ center[a] = clamp2(lp[a], lo, hi);
14916
+ }
14917
+ const taken2 = (cut[i] ?? 0) + size[u] * size[v];
14918
+ if (taken2 >= cell.size[u] * cell.size[v] * CRATER_SPENT) {
14919
+ worn[i] = -1;
14920
+ out.push({ ...cell, crater: true });
14921
+ continue;
14922
+ }
14923
+ worn[i] = 0;
14924
+ cut[i] = taken2;
14925
+ out.push({ center, size, crater: true });
14864
14926
  }
14865
14927
  if (out.length === 0) return;
14866
14928
  const { c, q } = partXf(o, p);
14867
- const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
14929
+ const chipped = (_c = p.st).chipped ?? (_c.chipped = []);
14868
14930
  const holes = localHolesOf(p);
14869
14931
  for (const cell of out) {
14870
14932
  const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
14871
14933
  const center = toWorld(c, q, cell.center);
14872
14934
  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}_`);
14935
+ const tag = `c${chipped.length}_`;
14936
+ const fragments = cell.crater ? craterFragments(o, p, cell, dir, speed, mass, tag) : cellFragments(o, p, [cell], wp, dir, speed, mass, tag, true);
14874
14937
  p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
14875
14938
  chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
14876
14939
  p.st.mass = Math.max(0, p.st.mass - mass);
@@ -14887,6 +14950,44 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14887
14950
  collapseCheck(o, src);
14888
14951
  }
14889
14952
  };
14953
+ const craterAcross = (energy) => clamp2(0.03 + 12e-4 * Math.sqrt(Math.max(0, energy)), 0.03, 0.14);
14954
+ const craterFragments = (o, p, cell, dir, speed, mass, tag) => {
14955
+ const { c, q } = partXf(o, p);
14956
+ const [u, v] = planeAxes(p.thin);
14957
+ const d = dir ?? rotateVec(q, [p.thin === 0 ? 1 : 0, p.thin === 1 ? 1 : 0, p.thin === 2 ? 1 : 0]);
14958
+ const exitSign = Math.sign(rotateVec(quat.conjugate(q), d)[p.thin]) || 1;
14959
+ const bit = Math.max(0.01, Math.min(cell.size[u], cell.size[v]) / 3);
14960
+ const n = clamp2(Math.round(cell.size[u] * cell.size[v] / (bit * bit * 3)), 4, 10) + Math.floor(rnd() * 3);
14961
+ const out = [];
14962
+ for (let i = 0; i < n; i++) {
14963
+ const back = i % 3 === 2;
14964
+ const lc = [cell.center[0], cell.center[1], cell.center[2]];
14965
+ lc[u] += (rnd() - 0.5) * cell.size[u] * 0.7;
14966
+ lc[v] += (rnd() - 0.5) * cell.size[v] * 0.7;
14967
+ lc[p.thin] += (back ? -exitSign : exitSign) * (p.thickness / 2 + 0.01);
14968
+ const size = [0, 0, 0];
14969
+ size[u] = bit * (0.6 + 0.8 * rnd());
14970
+ size[v] = bit * (0.5 + 0.7 * rnd());
14971
+ size[p.thin] = bit * (0.4 + 0.6 * rnd());
14972
+ const spread3 = [(rnd() - 0.5) * 0.9, (rnd() - 0.3) * 0.9, (rnd() - 0.5) * 0.9];
14973
+ const k = speed * (back ? 0.4 + 0.4 * rnd() : 0.7 + 0.8 * rnd());
14974
+ const vdir = vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3));
14975
+ out.push({
14976
+ id: `${p.spec.id}#${tag}${i}`,
14977
+ part: p.spec.id,
14978
+ material: p.spec.material,
14979
+ shape: "chunk",
14980
+ center: toWorld(c, q, lc),
14981
+ size,
14982
+ rotation: q,
14983
+ velocity: [vdir[0] * k, vdir[1] * k + 0.5 * rnd(), vdir[2] * k],
14984
+ angularVelocity: [(rnd() - 0.5) * 20, (rnd() - 0.5) * 20, (rnd() - 0.5) * 20],
14985
+ mass: mass / n,
14986
+ inner: true
14987
+ });
14988
+ }
14989
+ return out;
14990
+ };
14890
14991
  const exposeRebar = (o, p, src) => {
14891
14992
  const r = p.spec.reinforcement;
14892
14993
  if (!r) return;
@@ -15174,7 +15275,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15174
15275
  });
15175
15276
  }
15176
15277
  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 });
15278
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments, torn: true });
15178
15279
  };
15179
15280
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
15180
15281
  const mat = p.mat;
@@ -15233,12 +15334,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15233
15334
  } else if (mat.ductility >= 0.5) {
15234
15335
  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
15336
  } else if (brittle) {
15236
- addCrack(o, p, entry2, n, crackLen, src);
15337
+ if (!p.spec.units) addCrack(o, p, entry2, n, crackLen, src);
15237
15338
  if (mat.fracture !== "shatter") addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
15238
15339
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
15239
15340
  debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
15240
15341
  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;
15342
+ const hollowTear = mat.fracture === "tear" && p.spec.fill !== void 0 && p.spec.fill < 0.5;
15343
+ const absorbed = penetrated && (mat.ductility >= 0.5 && mat.fracture !== "tear" || hollowTear) ? Math.min(E, thr) : E;
15242
15344
  const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
15243
15345
  if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
15244
15346
  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 +15425,25 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15323
15425
  if (dist <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
15324
15426
  return reach;
15325
15427
  };
15428
+ const blastProbe = { partsTested: 0, partsNear: 0 };
15429
+ const blastCandidates = (o, point, radius) => {
15430
+ const local = toLocal(o.st.position, o.st.rotation, point);
15431
+ const near = [];
15432
+ for (const p of o.parts) {
15433
+ const reach = radius + p.bound;
15434
+ const dx = local[0] - p.pos[0];
15435
+ const dy = local[1] - p.pos[1];
15436
+ const dz = local[2] - p.pos[2];
15437
+ if (dx * dx + dy * dy + dz * dz < reach * reach) near.push(p);
15438
+ }
15439
+ blastProbe.partsTested += o.parts.length;
15440
+ blastProbe.partsNear += near.length;
15441
+ return near;
15442
+ };
15326
15443
  const blastObject = (o, point, energy, radius, src) => {
15327
15444
  const start = cur.length;
15328
15445
  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);
15446
+ for (const p of blastCandidates(o, point, radius)) blastPart(o, p, point, energy, radius, src, res);
15330
15447
  res.events = cur.slice(start);
15331
15448
  return res;
15332
15449
  };
@@ -15450,7 +15567,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15450
15567
  let order = 0;
15451
15568
  for (const o of inRange) {
15452
15569
  if (only && only !== o) continue;
15453
- for (const p of o.parts) {
15570
+ for (const p of blastCandidates(o, point, radius)) {
15454
15571
  if (isLost(p)) continue;
15455
15572
  const dist = distToPart(o, p, point);
15456
15573
  if (dist < radius) items.push({ dist, order: order++, o, p });
@@ -15997,6 +16114,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15997
16114
  const p = getPart(o, partId);
15998
16115
  p.pos = [position[0], position[1], position[2]];
15999
16116
  p.q = q;
16117
+ poseVersion++;
16000
16118
  updateBound(o);
16001
16119
  },
16002
16120
  remove(id) {
@@ -16015,6 +16133,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16015
16133
  const o = getObj(id);
16016
16134
  o.base = { pos: [position[0], position[1], position[2]], rot: [rotation[0], rotation[1], rotation[2], rotation[3]] };
16017
16135
  applySoilPose(o);
16136
+ slideOffResting(o, rotation);
16018
16137
  },
16019
16138
  splitGroup(id, group, newId) {
16020
16139
  if (impls.has(id)) throw new Error(`object "${id}" is a registered DestructibleImpl and cannot be split; its module owns its pieces`);
@@ -16038,7 +16157,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16038
16157
  if (o.spec.name !== void 0) s.name = o.spec.name;
16039
16158
  if (o.spec.tags) s.tags = [...o.spec.tags];
16040
16159
  if (o.spec.soil !== void 0) s.soil = o.spec.soil;
16041
- return clone(s);
16160
+ return s;
16042
16161
  };
16043
16162
  const inNew = (pid) => moved.has(pid);
16044
16163
  const inOld = (pid) => !moved.has(pid);
@@ -16065,7 +16184,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16065
16184
  const p = getPart(o, partId);
16066
16185
  return whole(() => doHit(o, h, p));
16067
16186
  },
16187
+ blastProbe() {
16188
+ return { ...blastProbe };
16189
+ },
16068
16190
  explode(point, energy, radius, source, blast) {
16191
+ blastProbe.partsTested = 0;
16192
+ blastProbe.partsNear = 0;
16069
16193
  return withCollapses(run(() => blastWorld(point, energy, radius, source, blast)));
16070
16194
  },
16071
16195
  raycast(origin, direction, maxDistance = Infinity) {
@@ -16075,7 +16199,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16075
16199
  sweepBox(box3, direction, distance2, options2 = {}) {
16076
16200
  const d = vec.normalize(direction);
16077
16201
  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;
16202
+ const hosted = options2.ignore === void 0 || ignored ? void 0 : impls.get(options2.ignore);
16203
+ 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
16204
  let best = null;
16080
16205
  for (const origin of sweepOrigins(box3, d, options2.spacing ?? 0.2)) {
16081
16206
  const r = raycastInternal(origin, d, distance2 + SWEEP_BACK, ignore);
@@ -19589,6 +19714,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19589
19714
  function collapseRange(mesh2, v0, v1) {
19590
19715
  const pos = mesh2.geometry.attributes.position;
19591
19716
  const a = pos.array;
19717
+ const u = mesh2.userData;
19718
+ if (!u._p0) u._p0 = a.slice();
19719
+ const list2 = u.collapsedRanges ?? (u.collapsedRanges = []);
19720
+ if (!list2.some((r) => r[0] === v0 && r[1] === v1)) list2.push([v0, v1]);
19592
19721
  const saved = a.slice(v0 * 3, v1 * 3);
19593
19722
  const x = a[v0 * 3], y = a[v0 * 3 + 1], z = a[v0 * 3 + 2];
19594
19723
  for (let k = v0; k < v1; k++) {
@@ -19603,6 +19732,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19603
19732
  const pos = mesh2.geometry.attributes.position;
19604
19733
  pos.array.set(saved, v0 * 3);
19605
19734
  pos.needsUpdate = true;
19735
+ const list2 = mesh2.userData.collapsedRanges;
19736
+ if (list2) mesh2.userData.collapsedRanges = list2.filter((r) => r[0] !== v0);
19606
19737
  }
19607
19738
  function noise2(x, y, cell) {
19608
19739
  const h = (i2, j2) => {
@@ -19617,13 +19748,16 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19617
19748
  var SPALL_MIN = 0.025;
19618
19749
  var SPALL_RAGGED = 0.045;
19619
19750
  var COVER2 = 0.015;
19751
+ var MAX_FLAKES = 64;
19620
19752
  function installDestructionBreach(K, _shared) {
19621
19753
  const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
19622
19754
  function createBreachView(opts) {
19623
19755
  const { house, structure } = opts;
19624
19756
  const views = /* @__PURE__ */ new Map();
19757
+ const dirty = /* @__PURE__ */ new Set();
19758
+ const paintBudgetMs = opts.paintBudgetMs ?? Infinity;
19759
+ const dropped = [];
19625
19760
  let lod = 0;
19626
- const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
19627
19761
  const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
19628
19762
  const tmp = { m: new THREE.Matrix4(), q: new THREE.Quaternion(), p: new THREE.Vector3(), s: new THREE.Vector3(), c: new THREE.Color() };
19629
19763
  const local = (p) => {
@@ -19692,6 +19826,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19692
19826
  const mesh2 = new THREE.Mesh(g, material);
19693
19827
  mesh2.name = "breach-shell";
19694
19828
  mesh2.userData.breachMask = mask;
19829
+ mesh2.userData.cutAt = (p) => {
19830
+ const v = local([p.x, p.y, p.z]);
19831
+ 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);
19832
+ return i >= 0 && i < mask.w && j >= 0 && j < mask.h && mask.data[j * mask.w + i] > 127;
19833
+ };
19695
19834
  mesh2.position.copy(it.mesh.position);
19696
19835
  mesh2.quaternion.copy(it.mesh.quaternion);
19697
19836
  mesh2.scale.copy(it.mesh.scale);
@@ -19707,6 +19846,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19707
19846
  toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
19708
19847
  return mesh2;
19709
19848
  }
19849
+ function unitsMaterial(core) {
19850
+ const material = new THREE.MeshLambertMaterial({ color: 16777215, side: THREE.DoubleSide });
19851
+ material.onBeforeCompile = (sh) => {
19852
+ sh.uniforms.uCore = { value: core.texture };
19853
+ sh.uniforms.uCoreRect = { value: core.rect };
19854
+ sh.uniforms.uCoreAlongX = { value: core.alongU ? 1 : 0 };
19855
+ 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");
19856
+ 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;");
19857
+ };
19858
+ material.customProgramCacheKey = () => "breachCore";
19859
+ material.userData.breachCore = core;
19860
+ return material;
19861
+ }
19710
19862
  function maskOf(hostId, parts) {
19711
19863
  const host = structure.byId.get(hostId);
19712
19864
  const [u0, v0, u1, v1] = host.aabb;
@@ -19722,41 +19874,73 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19722
19874
  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
19875
  }
19724
19876
  const boxDistance = (s, y, a, b, c, d) => Math.hypot(Math.max(a - s, 0, s - b), Math.max(c - y, 0, y - d));
19877
+ 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);
19878
+ function covered(c, boxes) {
19879
+ const n = 8;
19880
+ let hit = 0;
19881
+ for (let j = 0; j < n; j++) {
19882
+ const y = c.y0 + (j + 0.5) / n * (c.y1 - c.y0);
19883
+ for (let i = 0; i < n; i++) {
19884
+ const s = c.a + (i + 0.5) / n * (c.b - c.a);
19885
+ if (boxes.some((k) => s >= k.s0 && s <= k.s1 && y >= k.y0 && y <= k.y1)) hit++;
19886
+ }
19887
+ }
19888
+ return hit / (n * n);
19889
+ }
19725
19890
  function paint(view) {
19726
- const { mask } = view;
19891
+ const { mask, core } = view;
19727
19892
  const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
19728
19893
  const holes = [];
19894
+ const pits = [];
19729
19895
  const discs = [];
19896
+ const flakes = [];
19730
19897
  const shown = [];
19731
19898
  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);
19899
+ 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
19900
  const all = cells(part, units2);
19734
19901
  const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
19735
19902
  const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
19736
19903
  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;
19904
+ for (const k of spalls) flakes.push({ s: along(k.center), y: k.center[1], r: k.radius });
19905
+ 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 }));
19906
+ 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
19907
  const kept = [];
19739
19908
  for (const c of all) (gone(c) ? holes : kept).push(c);
19740
19909
  for (const c of kept) shown.push({ c, part, units: units2, thick, mid });
19910
+ for (const k of boxes) {
19911
+ 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;
19912
+ 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 });
19913
+ }
19741
19914
  }
19742
19915
  const reach = SPALL_MIN + SPALL_RAGGED;
19916
+ const ragged = (s, y) => 0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011);
19743
19917
  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));
19918
+ core.data.fill(0);
19919
+ const stamp = (m, s0, s1, y0, y1, inside2) => {
19920
+ 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));
19921
+ 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
19922
  for (let j = j0; j <= j1; j++) {
19748
- const y = mask.y0 + (j + 0.5) / mask.h * mask.height;
19923
+ const y = m.y0 + (j + 0.5) / m.h * m.height;
19749
19924
  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;
19925
+ const s = (i + 0.5) / m.w * m.len;
19926
+ if (inside2(s, y)) m.data[j * m.w + i] = 255;
19753
19927
  }
19754
19928
  }
19755
19929
  };
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));
19930
+ const plaster = (d, s, y) => d <= SPALL_MIN + SPALL_RAGGED * ragged(s, y);
19931
+ 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));
19932
+ 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));
19933
+ for (const f of flakes) {
19934
+ 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));
19935
+ 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);
19936
+ }
19937
+ for (const p of pits) {
19938
+ 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));
19939
+ 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));
19940
+ }
19758
19941
  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);
19942
+ core.texture.needsUpdate = true;
19943
+ 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
19944
  const list2 = [];
19761
19945
  const tone = (hex2, k) => tmp.c.setHex(hex2).multiplyScalar(k).getHex();
19762
19946
  for (const { c, part, units: units2, thick, mid } of shown) {
@@ -19766,7 +19950,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19766
19950
  const base = fragmentMaterial(part.material).color.getHex();
19767
19951
  const n = noise2(centre * 3.1, y * 2.7, 0.07);
19768
19952
  if (units2.mortar === "concrete") {
19769
- at(len, h, thick - 2 * COVER2 - 0.03 * n, tone(base, 0.86 + 0.2 * n));
19953
+ at(len, h, thick - 2 * COVER2, tone(base, 0.9 + 0.04 * n));
19770
19954
  } else {
19771
19955
  at(len, h, thick - 2 * COVER2 - 0.012, 10327948);
19772
19956
  at(len - 0.012, h - 0.012, thick - 2 * COVER2, tone(base, 0.8 + 0.35 * n));
@@ -19776,7 +19960,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19776
19960
  view.units.removeFromParent();
19777
19961
  view.units.dispose();
19778
19962
  }
19779
- const units = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
19963
+ const units = new THREE.InstancedMesh(unitGeometry, view.unitMaterial, Math.max(1, list2.length));
19780
19964
  units.name = "breach:" + view.host;
19781
19965
  list2.forEach((u, i) => {
19782
19966
  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 +19976,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19792
19976
  house.add(units);
19793
19977
  view.units = units;
19794
19978
  view.gone = holes.length;
19979
+ view.pits = pits.length;
19980
+ view.flakes = flakes.length;
19981
+ }
19982
+ function chipBox(chip, alongU) {
19983
+ house.updateMatrixWorld(true);
19984
+ const q = tmp.q.set(chip.rotation[0], chip.rotation[1], chip.rotation[2], chip.rotation[3]).premultiply(house.getWorldQuaternion(new THREE.Quaternion()).invert());
19985
+ let a = 0, u = 0;
19986
+ for (let k = 0; k < 3; k++) {
19987
+ const axis = tmp.p.set(k === 0 ? 1 : 0, k === 1 ? 1 : 0, k === 2 ? 1 : 0).applyQuaternion(q);
19988
+ a += Math.abs(alongU ? axis.x : axis.z) * chip.size[k] / 2;
19989
+ u += Math.abs(axis.y) * chip.size[k] / 2;
19990
+ }
19991
+ return { center: local(chip.center), along: a, up: u };
19795
19992
  }
19796
19993
  function draw2(partId, change) {
19797
19994
  if (lod !== 0) return false;
@@ -19803,23 +20000,49 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19803
20000
  if (!items.length) return false;
19804
20001
  const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
19805
20002
  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() };
20003
+ const mask = maskOf(hostId, parts), core = maskOf(hostId, parts);
20004
+ view = {
20005
+ host: hostId,
20006
+ parts,
20007
+ items,
20008
+ mask,
20009
+ core,
20010
+ unitMaterial: unitsMaterial(core),
20011
+ shells: [],
20012
+ saved: null,
20013
+ units: null,
20014
+ gone: 0,
20015
+ pits: 0,
20016
+ chips: new Map(parts.map((id) => [id, []])),
20017
+ spalls: new Map(parts.map((id) => [id, []])),
20018
+ openings: /* @__PURE__ */ new Map(),
20019
+ flakes: 0
20020
+ };
19808
20021
  }
19809
20022
  if (!view.chips.has(partId)) return false;
19810
20023
  if (change.opening) {
19811
20024
  if (view.openings.has(partId)) return false;
19812
20025
  view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
19813
20026
  }
19814
- if (change.chip) view.chips.get(partId).push(local(change.chip));
20027
+ if (change.chip) view.chips.get(partId).push(chipBox(change.chip, view.mask.alongU));
20028
+ if (change.spall) {
20029
+ const list2 = view.spalls.get(partId);
20030
+ list2.push({ center: local(change.spall.center), radius: change.spall.radius });
20031
+ if (list2.length > MAX_FLAKES) list2.shift();
20032
+ }
19815
20033
  if (!view.saved) {
19816
20034
  view.shells = view.items.map((it) => shell(it, view.mask));
20035
+ for (const m of view.shells) m.userData.behind = () => view.units;
19817
20036
  view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
19818
20037
  }
19819
- paint(view);
19820
20038
  views.set(hostId, view);
19821
- invalidate();
19822
- invalidateShadow();
20039
+ if (opts.deferPaint && change.spall) dirty.add(view);
20040
+ else {
20041
+ dirty.delete(view);
20042
+ paint(view);
20043
+ invalidate();
20044
+ invalidateShadow();
20045
+ }
19823
20046
  return true;
19824
20047
  }
19825
20048
  function plain2(view, shown) {
@@ -19828,24 +20051,70 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19828
20051
  else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
19829
20052
  }
19830
20053
  }
20054
+ function flush(budgetMs = paintBudgetMs) {
20055
+ if (dirty.size === 0) return 0;
20056
+ const deadline = performance.now() + budgetMs;
20057
+ for (const view of dirty) {
20058
+ dirty.delete(view);
20059
+ paint(view);
20060
+ if (performance.now() >= deadline) break;
20061
+ }
20062
+ invalidate();
20063
+ invalidateShadow();
20064
+ return dirty.size;
20065
+ }
19831
20066
  function dispose(view) {
19832
- view.units.removeFromParent();
19833
- view.units.dispose();
20067
+ dirty.delete(view);
20068
+ if (view.units) {
20069
+ view.units.removeFromParent();
20070
+ view.units.dispose();
20071
+ }
20072
+ view.unitMaterial.dispose();
19834
20073
  for (const m of view.shells) {
19835
20074
  m.removeFromParent();
19836
20075
  m.geometry.dispose();
19837
20076
  m.material.dispose();
19838
20077
  }
19839
20078
  view.mask.texture.dispose();
20079
+ view.core.texture.dispose();
19840
20080
  }
19841
20081
  return {
19842
20082
  /** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
19843
20083
  stage(event) {
19844
20084
  return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
19845
20085
  },
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. */
20086
+ /**
20087
+ * 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
20088
+ * renderer leaves its flat patch out on the meshes `claim` marked). Returns true when the wall was cut.
20089
+ */
20090
+ spall(event) {
20091
+ const r = Math.sqrt(Math.max(0, event.area) / Math.PI);
20092
+ return r > 0 ? draw2(event.part, { spall: { center: event.point, radius: r } }) : false;
20093
+ },
20094
+ /**
20095
+ * Marks the meshes that draw a wall of units as drawing its spalls itself (`userData.drawnSpalls`, the part ids): the destruction renderer then
20096
+ * throws the bits and leaves its patch out. Call it once the spec is known.
20097
+ */
20098
+ claim() {
20099
+ var _a;
20100
+ for (const q of opts.spec()?.parts ?? []) {
20101
+ if (!q.units) continue;
20102
+ const host = hostOf(q.id);
20103
+ if (!host) continue;
20104
+ for (const it of structure.meshesOf(host)) {
20105
+ if (!it.mesh) continue;
20106
+ const list2 = (_a = it.mesh.userData).drawnSpalls ?? (_a.drawnSpalls = []);
20107
+ if (!list2.includes(q.id)) list2.push(q.id);
20108
+ }
20109
+ }
20110
+ },
20111
+ /**
20112
+ * 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
20113
+ * tore off a face) leaves the marks the renderer drew. Returns true when the wall was cut.
20114
+ */
19847
20115
  chip(event) {
19848
- return draw2(event.part, { chip: event.center });
20116
+ if (event.torn) return false;
20117
+ return draw2(event.part, { chip: { center: event.center, size: event.size ?? [0, 0, 0], rotation: event.rotation ?? [0, 0, 0, 1] } });
19849
20118
  },
19850
20119
  /** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
19851
20120
  drop(partId) {
@@ -19853,9 +20122,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19853
20122
  if (!v) return;
19854
20123
  dispose(v);
19855
20124
  views.delete(partId);
20125
+ if (v.saved) dropped.push(v);
19856
20126
  },
19857
20127
  /** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
19858
20128
  setLod(level) {
20129
+ flush(Infinity);
19859
20130
  const was = lod;
19860
20131
  lod = level;
19861
20132
  if (was === 0 === (level === 0)) return;
@@ -19874,15 +20145,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19874
20145
  if (lod === 0) plain2(v, true);
19875
20146
  }
19876
20147
  views.clear();
20148
+ for (const v of dropped) plain2(v, true);
20149
+ dropped.length = 0;
19877
20150
  },
20151
+ flush,
19878
20152
  /** The wall has damage drawn. */
19879
20153
  has(partId) {
19880
20154
  return views.has(partId);
19881
20155
  },
19882
- /** Walls with damage drawn, units out of them, and units laid bare at the edge of the cuts. */
19883
- statistics: () => ({
20156
+ /** 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. */
20157
+ statistics: () => (flush(Infinity), {
19884
20158
  walls: views.size,
19885
20159
  gone: [...views.values()].reduce((n, v) => n + v.gone, 0),
20160
+ craters: [...views.values()].reduce((n, v) => n + v.pits, 0),
20161
+ flakes: [...views.values()].reduce((n, v) => n + v.flakes, 0),
19886
20162
  units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
19887
20163
  })
19888
20164
  };
@@ -19891,6 +20167,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19891
20167
  }
19892
20168
 
19893
20169
  // src/destruction/world.ts
20170
+ var FOLLOW_FLUSH_FALLBACK_MS = 100;
19894
20171
  function installDestructionWorld(K, shared) {
19895
20172
  const { THREE, cfg, invalidateShadow, invalidate, rngString, FRAGMENT_GEO, fragmentMaterial, createBreachView } = K;
19896
20173
  function createHouseDestruction(root, house, structure, parts, colliders, water) {
@@ -19899,7 +20176,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19899
20176
  const R = cfg.destructionRenderer || null, listeners = /* @__PURE__ */ new Set();
19900
20177
  D.renderer = R;
19901
20178
  D.spec = null;
19902
- const breach = createBreachView({ house, structure, spec: () => D.spec });
20179
+ const breach = createBreachView({ house, structure, spec: () => D.spec, deferPaint: true, ...cfg.breachPaintBudgetMs !== void 0 ? { paintBudgetMs: cfg.breachPaintBudgetMs } : {} });
19903
20180
  D.breach = breach;
19904
20181
  D.onEvent = (fn) => {
19905
20182
  listeners.add(fn);
@@ -20084,6 +20361,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20084
20361
  case "chip":
20085
20362
  breach.chip(e);
20086
20363
  break;
20364
+ case "spall":
20365
+ breach.spall(e);
20366
+ break;
20087
20367
  case "collapse":
20088
20368
  for (const id of e.parts) {
20089
20369
  breach.drop(id);
@@ -20125,7 +20405,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20125
20405
  D.specOptions = specOptions || null;
20126
20406
  D.spec = structure.spec(specOptions);
20127
20407
  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() });
20408
+ breach.claim();
20409
+ if (R) R.bind(D.id, house, { parts: rangesOf(), onFrame: () => breach.flush() });
20129
20410
  off = D.world.on("*", handleEvent);
20130
20411
  D.active = true;
20131
20412
  return D;
@@ -20140,6 +20421,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20140
20421
  };
20141
20422
  D.follow = (world, id, spec) => {
20142
20423
  D.spec = spec;
20424
+ breach.claim();
20425
+ let fallback = null;
20143
20426
  const offFollow = world.on("*", (e) => {
20144
20427
  if (e.object !== id) return;
20145
20428
  switch (e.type) {
@@ -20149,6 +20432,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20149
20432
  case "chip":
20150
20433
  breach.chip(e);
20151
20434
  break;
20435
+ case "spall":
20436
+ breach.spall(e);
20437
+ break;
20152
20438
  case "fracture":
20153
20439
  breach.drop(e.part);
20154
20440
  break;
@@ -20158,10 +20444,16 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20158
20444
  default:
20159
20445
  break;
20160
20446
  }
20447
+ if (!fallback && typeof setTimeout === "function") fallback = setTimeout(() => {
20448
+ fallback = null;
20449
+ breach.flush(Infinity);
20450
+ }, FOLLOW_FLUSH_FALLBACK_MS);
20161
20451
  });
20162
20452
  return { restore: () => breach.restore(), dispose: () => {
20163
20453
  offFollow();
20164
20454
  breach.restore();
20455
+ }, update: () => {
20456
+ breach.flush();
20165
20457
  } };
20166
20458
  };
20167
20459
  D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
@@ -20194,6 +20486,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20194
20486
  return r;
20195
20487
  };
20196
20488
  D.animate = (dt) => {
20489
+ breach.flush();
20197
20490
  if (R) return;
20198
20491
  let moved = false;
20199
20492
  for (const [id, list2] of bodies) {
@@ -21658,6 +21951,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21658
21951
  houseRenderer = create3(THREE, {
21659
21952
  locale,
21660
21953
  lod: { interior: 10, hide: 40 },
21954
+ breachPaintBudgetMs: 4,
21661
21955
  ...options.house,
21662
21956
  quality: settings.quality,
21663
21957
  ...options.world ? { destruction: options.world } : {},
@@ -21813,9 +22107,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21813
22107
  host?.log(text.steps.no_door);
21814
22108
  return;
21815
22109
  }
21816
- p.house.interact(eyeOf(p));
22110
+ const fo = target.target;
22111
+ const reach = fo && host?.reachFor && fo.type === "swing" && fo.gs?.[0] && fo.handles?.[0] ? doorReach(p, fo) : null;
22112
+ if (reach && host?.reachFor) {
22113
+ host.reachFor({ ...reach, onGrip: () => void p.house.toggleDoor(fo) });
22114
+ } else p.house.interact(eyeOf(p));
21817
22115
  host?.log(fill(text.steps.door, { text: String(target.text) }));
21818
22116
  };
22117
+ const doorReach = (p, fo) => {
22118
+ const g = fo.gs[0];
22119
+ const pivot = fo.handles[0].pivot;
22120
+ const at = (angle) => {
22121
+ const c = Math.cos(angle);
22122
+ const s = Math.sin(angle);
22123
+ const v = new THREE.Vector3(pivot[0] * c + pivot[2] * s, pivot[1], -pivot[0] * s + pivot[2] * c).add(g.position);
22124
+ return g.parent ? g.parent.localToWorld(v) : v;
22125
+ };
22126
+ const camera = host?.camera;
22127
+ const eye = new THREE.Vector3(camera.position.x, 0, camera.position.z);
22128
+ const flat = (v) => Math.hypot(v.x - eye.x, v.z - eye.z);
22129
+ const closed = at(0);
22130
+ const open = at(fo.rot);
22131
+ const opening = !fo.open;
22132
+ return {
22133
+ handle: () => {
22134
+ g.updateMatrixWorld(true);
22135
+ return g.localToWorld(new THREE.Vector3(pivot[0], pivot[1], pivot[2])).toArray();
22136
+ },
22137
+ hinge: () => g.getWorldPosition(new THREE.Vector3()).toArray(),
22138
+ seconds: fo.locked || fo.blockedFlag ? 0.6 : 0.55,
22139
+ pull: opening ? flat(open) <= flat(closed) : flat(closed) <= flat(open),
22140
+ locked: !!(fo.locked || fo.blockedFlag)
22141
+ };
22142
+ };
21819
22143
  const strike = () => {
21820
22144
  const world = options.world;
21821
22145
  if (!world || !host) return;