@adatechnology/game-floorplan-three 0.6.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.
@@ -12693,6 +12693,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12693
12693
  },
12694
12694
  hitDebris: "chips",
12695
12695
  breakDebris: "dust",
12696
+ visual: { innerColor: 10197652 },
12696
12697
  thermal: {
12697
12698
  conductivity: 0.15,
12698
12699
  heatCapacity: 2.5
@@ -12723,6 +12724,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12723
12724
  ],
12724
12725
  hitDebris: "chips",
12725
12726
  breakDebris: "dust",
12727
+ visual: { innerColor: 12085567 },
12726
12728
  thermal: {
12727
12729
  conductivity: 0.15,
12728
12730
  heatCapacity: 2.5
@@ -12753,6 +12755,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12753
12755
  ],
12754
12756
  hitDebris: "dust",
12755
12757
  breakDebris: "dust",
12758
+ visual: { innerColor: 14999507 },
12756
12759
  thermal: {
12757
12760
  conductivity: 0.1,
12758
12761
  heatCapacity: 1.5
@@ -12778,6 +12781,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12778
12781
  ],
12779
12782
  hitDebris: "chips",
12780
12783
  breakDebris: "dust",
12784
+ visual: { innerColor: 9407364 },
12781
12785
  thermal: {
12782
12786
  conductivity: 0.2,
12783
12787
  heatCapacity: 2.5
@@ -13519,6 +13523,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13519
13523
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
13520
13524
  var VENT_DIAMETER = 8e-3;
13521
13525
  var FLAME_LIGHTS_LEAK_C = 470;
13526
+ var RESTING_HOLD_COS = Math.cos(Math.PI / 6);
13522
13527
  var BREACH_RADIUS = 0.55;
13523
13528
  var BREACH_BLAST_FACTOR = 6;
13524
13529
  var BREACH_AREA = 0.25;
@@ -13534,7 +13539,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13534
13539
  var BREACH_GRID_CAP = 4096;
13535
13540
  var CHIP_ENERGY = 200;
13536
13541
  var CHIP_SPLASH = 0.5;
13542
+ var CRATER_SPENT = 0.6;
13537
13543
  var BAR_BOW = 0.08;
13544
+ var BULLET_JOINT_SPAN = 0.3;
13538
13545
  var CUT_SLICE = 1;
13539
13546
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
13540
13547
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -13553,12 +13560,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13553
13560
  const cells = p.st.chipped;
13554
13561
  if (!cells?.length) return 0;
13555
13562
  const [u, v] = planeAxes(p.thin);
13556
- 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;
13557
13567
  const across = along === u ? v : u;
13558
13568
  let worst = 0;
13559
13569
  for (const c of cells) {
13560
- let gone = 0;
13561
- 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
+ }
13562
13579
  worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
13563
13580
  }
13564
13581
  return Math.min(1, worst);
@@ -13854,7 +13871,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13854
13871
  if (k < 0 || j.spec.kind !== "tie") return k;
13855
13872
  return partXf(o, o.parts[k]).c[1] > partXf(o, o.parts[r]).c[1] + 1e-6 ? k : -1;
13856
13873
  };
13874
+ let poseVersion = 0;
13875
+ const contactMemo = /* @__PURE__ */ new WeakMap();
13857
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) => {
13858
13886
  const A = aabbOf(p.spec.shape, p.pos, p.q), B = aabbOf(support.spec.shape, support.pos, support.q);
13859
13887
  const lo = [0, 0, 0], hi = [0, 0, 0];
13860
13888
  for (let k = 0; k < 3; k++) {
@@ -14649,7 +14677,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14649
14677
  p.st.holes.push(hole);
14650
14678
  if (p.st.holes.length > maxHoles) p.st.holes.shift();
14651
14679
  markDamaged(p);
14652
- 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 } } : {} });
14653
14681
  setStage(o, p, 1, {}, src);
14654
14682
  return hole;
14655
14683
  };
@@ -14661,11 +14689,27 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14661
14689
  emit({ ...base(o, src), type: "crack", part: p.spec.id, crack, worldPoint: wp });
14662
14690
  setStage(o, p, 1, {}, src);
14663
14691
  };
14664
- const debris = (o, p, kind, point, normal, count, speed, src) => emit({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed });
14692
+ const debris = (o, p, kind, point, normal, count, speed, src, material) => emit({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed, ...material && material !== p.spec.material ? { material } : {} });
14665
14693
  const holeRate = (hole, pressurized) => (pressurized ? 0.5 : 0.1) * (hole.diameter / 9e-3);
14666
14694
  const detachEvents = (o, opened, by, src) => {
14667
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]) });
14668
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
+ };
14669
14713
  const checkDestroyed = (o, src) => {
14670
14714
  if (!o.st.destroyed && o.parts.length > 0 && o.parts.every(isLost)) {
14671
14715
  o.st.destroyed = true;
@@ -14702,7 +14746,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14702
14746
  const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
14703
14747
  return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
14704
14748
  };
14705
- const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false) => {
14749
+ const jointLength = (o, j) => {
14750
+ const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
14751
+ let len = 0;
14752
+ for (let k = 0; k < 3; k++) len = Math.max(len, Math.min(A.max[k], B.max[k]) - Math.max(A.min[k], B.min[k]));
14753
+ return len;
14754
+ };
14755
+ const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false, span) => {
14706
14756
  if (amount <= 0) return;
14707
14757
  const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
14708
14758
  if (split && mine.length) amount /= mine.length;
@@ -14710,7 +14760,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14710
14760
  if (j.st.open) continue;
14711
14761
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
14712
14762
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
14713
- j.st.hp -= amount;
14763
+ j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
14714
14764
  if (j.st.hp <= 0) {
14715
14765
  j.st.hp = 0;
14716
14766
  j.st.open = true;
@@ -14739,7 +14789,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14739
14789
  if (spalls.length > MAX_SPALLS) spalls.shift();
14740
14790
  markDamaged(p);
14741
14791
  emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: layer.index, point: wp, area: Math.PI * radius * radius });
14742
- debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
14792
+ debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src, layer.mat.id);
14743
14793
  setStage(o, p, 1, {}, src);
14744
14794
  };
14745
14795
  const crater = (o, p, wp, normal, radius, src) => {
@@ -14760,7 +14810,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14760
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 }));
14761
14811
  const unitsOf2 = (p) => p.spec.units ?? { size: [p.mat.fragmentSize?.[1] ?? 0.2, p.mat.fragmentSize?.[1] ?? 0.2, p.thickness], bond: "stack" };
14762
14812
  const FRAGMENT_SHAPE = { shatter: "shard", splinter: "splinter", tear: "scrap" };
14763
- const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag) => {
14813
+ const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag, inner = false) => {
14764
14814
  const { c, q } = partXf(o, p);
14765
14815
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
14766
14816
  const [u, v] = planeAxes(p.thin);
@@ -14784,7 +14834,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14784
14834
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
14785
14835
  angularVelocity: [(rnd() - 0.5) * 8, (rnd() - 0.5) * 8, (rnd() - 0.5) * 8],
14786
14836
  mass: mass * faceArea(cell) / total,
14787
- ...marks.length > 0 ? { marks } : {}
14837
+ ...marks.length > 0 ? { marks } : {},
14838
+ ...inner ? { inner } : {}
14788
14839
  };
14789
14840
  });
14790
14841
  };
@@ -14825,7 +14876,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14825
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);
14826
14877
  };
14827
14878
  const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
14828
- var _a, _b;
14879
+ var _a, _b, _c;
14829
14880
  if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
14830
14881
  const rule = p.mat.hits?.[kind];
14831
14882
  const perKg = rule?.unitJoules ?? chipEnergy;
@@ -14835,7 +14886,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14835
14886
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
14836
14887
  const grid = gridOf(p);
14837
14888
  const worn = (_a = p.rt).chips ?? (_a.chips = {});
14889
+ const cut = (_b = p.rt).craters ?? (_b.craters = {});
14838
14890
  const out = [];
14891
+ const shot = kind === "ballistic";
14839
14892
  for (let i = 0; i < grid.length; i++) {
14840
14893
  const cell = grid[i];
14841
14894
  if (worn[i] === -1) continue;
@@ -14848,18 +14901,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14848
14901
  worn[i] = total;
14849
14902
  continue;
14850
14903
  }
14851
- worn[i] = -1;
14852
- 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 });
14853
14926
  }
14854
14927
  if (out.length === 0) return;
14855
14928
  const { c, q } = partXf(o, p);
14856
- const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
14929
+ const chipped = (_c = p.st).chipped ?? (_c.chipped = []);
14857
14930
  const holes = localHolesOf(p);
14858
14931
  for (const cell of out) {
14859
14932
  const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
14860
14933
  const center = toWorld(c, q, cell.center);
14861
14934
  const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
14862
- 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);
14863
14937
  p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
14864
14938
  chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
14865
14939
  p.st.mass = Math.max(0, p.st.mass - mass);
@@ -14876,6 +14950,44 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14876
14950
  collapseCheck(o, src);
14877
14951
  }
14878
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
+ };
14879
14991
  const exposeRebar = (o, p, src) => {
14880
14992
  const r = p.spec.reinforcement;
14881
14993
  if (!r) return;
@@ -15163,7 +15275,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15163
15275
  });
15164
15276
  }
15165
15277
  const patch = [scale * 4, scale * 4, scale * 4];
15166
- 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 });
15167
15279
  };
15168
15280
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
15169
15281
  const mat = p.mat;
@@ -15222,12 +15334,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15222
15334
  } else if (mat.ductility >= 0.5) {
15223
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);
15224
15336
  } else if (brittle) {
15225
- addCrack(o, p, entry2, n, crackLen, src);
15337
+ if (!p.spec.units) addCrack(o, p, entry2, n, crackLen, src);
15226
15338
  if (mat.fracture !== "shatter") addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
15227
15339
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
15228
15340
  debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
15229
- wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
15230
- const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
15341
+ wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
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;
15231
15344
  const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
15232
15345
  if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
15233
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);
@@ -15312,10 +15425,25 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15312
15425
  if (dist <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
15313
15426
  return reach;
15314
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
+ };
15315
15443
  const blastObject = (o, point, energy, radius, src) => {
15316
15444
  const start = cur.length;
15317
15445
  const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
15318
- 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);
15319
15447
  res.events = cur.slice(start);
15320
15448
  return res;
15321
15449
  };
@@ -15439,7 +15567,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15439
15567
  let order = 0;
15440
15568
  for (const o of inRange) {
15441
15569
  if (only && only !== o) continue;
15442
- for (const p of o.parts) {
15570
+ for (const p of blastCandidates(o, point, radius)) {
15443
15571
  if (isLost(p)) continue;
15444
15572
  const dist = distToPart(o, p, point);
15445
15573
  if (dist < radius) items.push({ dist, order: order++, o, p });
@@ -15986,6 +16114,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15986
16114
  const p = getPart(o, partId);
15987
16115
  p.pos = [position[0], position[1], position[2]];
15988
16116
  p.q = q;
16117
+ poseVersion++;
15989
16118
  updateBound(o);
15990
16119
  },
15991
16120
  remove(id) {
@@ -16004,6 +16133,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16004
16133
  const o = getObj(id);
16005
16134
  o.base = { pos: [position[0], position[1], position[2]], rot: [rotation[0], rotation[1], rotation[2], rotation[3]] };
16006
16135
  applySoilPose(o);
16136
+ slideOffResting(o, rotation);
16007
16137
  },
16008
16138
  splitGroup(id, group, newId) {
16009
16139
  if (impls.has(id)) throw new Error(`object "${id}" is a registered DestructibleImpl and cannot be split; its module owns its pieces`);
@@ -16027,7 +16157,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16027
16157
  if (o.spec.name !== void 0) s.name = o.spec.name;
16028
16158
  if (o.spec.tags) s.tags = [...o.spec.tags];
16029
16159
  if (o.spec.soil !== void 0) s.soil = o.spec.soil;
16030
- return clone(s);
16160
+ return s;
16031
16161
  };
16032
16162
  const inNew = (pid) => moved.has(pid);
16033
16163
  const inOld = (pid) => !moved.has(pid);
@@ -16054,7 +16184,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16054
16184
  const p = getPart(o, partId);
16055
16185
  return whole(() => doHit(o, h, p));
16056
16186
  },
16187
+ blastProbe() {
16188
+ return { ...blastProbe };
16189
+ },
16057
16190
  explode(point, energy, radius, source, blast) {
16191
+ blastProbe.partsTested = 0;
16192
+ blastProbe.partsNear = 0;
16058
16193
  return withCollapses(run(() => blastWorld(point, energy, radius, source, blast)));
16059
16194
  },
16060
16195
  raycast(origin, direction, maxDistance = Infinity) {
@@ -16064,7 +16199,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16064
16199
  sweepBox(box3, direction, distance2, options2 = {}) {
16065
16200
  const d = vec.normalize(direction);
16066
16201
  const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
16067
- 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;
16068
16204
  let best = null;
16069
16205
  for (const origin of sweepOrigins(box3, d, options2.spacing ?? 0.2)) {
16070
16206
  const r = raycastInternal(origin, d, distance2 + SWEEP_BACK, ignore);
@@ -19578,6 +19714,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19578
19714
  function collapseRange(mesh2, v0, v1) {
19579
19715
  const pos = mesh2.geometry.attributes.position;
19580
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]);
19581
19721
  const saved = a.slice(v0 * 3, v1 * 3);
19582
19722
  const x = a[v0 * 3], y = a[v0 * 3 + 1], z = a[v0 * 3 + 2];
19583
19723
  for (let k = v0; k < v1; k++) {
@@ -19592,65 +19732,57 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19592
19732
  const pos = mesh2.geometry.attributes.position;
19593
19733
  pos.array.set(saved, v0 * 3);
19594
19734
  pos.needsUpdate = true;
19595
- }
19735
+ const list2 = mesh2.userData.collapsedRanges;
19736
+ if (list2) mesh2.userData.collapsedRanges = list2.filter((r) => r[0] !== v0);
19737
+ }
19738
+ function noise2(x, y, cell) {
19739
+ const h = (i2, j2) => {
19740
+ const s = Math.sin(i2 * 127.1 + j2 * 311.7) * 43758.5453;
19741
+ return s - Math.floor(s);
19742
+ };
19743
+ const fx = x / cell, fy = y / cell, i = Math.floor(fx), j = Math.floor(fy), u = fx - i, v = fy - j;
19744
+ const su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v);
19745
+ return (h(i, j) * (1 - su) + h(i + 1, j) * su) * (1 - sv) + (h(i, j + 1) * (1 - su) + h(i + 1, j + 1) * su) * sv;
19746
+ }
19747
+ var MASK_TEXEL = 0.02;
19748
+ var SPALL_MIN = 0.025;
19749
+ var SPALL_RAGGED = 0.045;
19750
+ var COVER2 = 0.015;
19751
+ var MAX_FLAKES = 64;
19596
19752
  function installDestructionBreach(K, _shared) {
19597
19753
  const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
19598
19754
  function createBreachView(opts) {
19599
19755
  const { house, structure } = opts;
19600
19756
  const views = /* @__PURE__ */ new Map();
19757
+ const dirty = /* @__PURE__ */ new Set();
19758
+ const paintBudgetMs = opts.paintBudgetMs ?? Infinity;
19759
+ const dropped = [];
19601
19760
  let lod = 0;
19602
- const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
19603
19761
  const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
19604
19762
  const tmp = { m: new THREE.Matrix4(), q: new THREE.Quaternion(), p: new THREE.Vector3(), s: new THREE.Vector3(), c: new THREE.Color() };
19605
- const wallColour = (items) => {
19606
- for (const it of items) {
19607
- if (!it.mesh) continue;
19608
- const c = it.mesh.geometry.attributes.color;
19609
- if (!c) continue;
19610
- const a = c.array;
19611
- tmp.c.setRGB(a[it.v0 * 3], a[it.v0 * 3 + 1], a[it.v0 * 3 + 2]);
19612
- return tmp.c.getHex();
19613
- }
19614
- return null;
19615
- };
19616
19763
  const local = (p) => {
19617
19764
  house.updateMatrixWorld(true);
19618
19765
  const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
19619
19766
  return [v.x, v.y, v.z];
19620
19767
  };
19621
- function courses(part, units, opening, chips, paint) {
19768
+ function cells(part, units) {
19622
19769
  const [u0, v0, u1, v1] = part.aabb;
19623
19770
  const alongU = u1 - u0 >= v1 - v0;
19624
19771
  const len = alongU ? u1 - u0 : v1 - v0;
19625
- const thick = alongU ? v1 - v0 : u1 - u0;
19626
19772
  const [ul, uh] = units.size;
19627
19773
  const rows = Math.max(1, Math.round((part.y1 - part.y0) / uh));
19628
19774
  const rowH = (part.y1 - part.y0) / rows;
19629
19775
  const per = Math.max(1, Math.round(len / ul));
19630
19776
  const w = len / per;
19631
- const edge = fragmentMaterial(part.material).color.getHex();
19632
- const list2 = [];
19777
+ const out = [];
19633
19778
  for (let r = 0; r < rows; r++) {
19634
19779
  const shift = units.bond === "running" && r & 1 ? w / 2 : 0;
19635
- const cells = per + (shift ? 1 : 0);
19636
- for (let i = 0; i < cells; i++) {
19637
- let a = i * w - shift, b = a + w;
19638
- a = Math.max(0, a);
19639
- b = Math.min(len, b);
19640
- if (b - a < 1e-3) continue;
19641
- const mid = (a + b) / 2;
19642
- const x = alongU ? u0 + mid : (u0 + u1) / 2;
19643
- const z = alongU ? (v0 + v1) / 2 : v0 + mid;
19644
- const y = part.y0 + (r + 0.5) * rowH;
19645
- const d = opening ? Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]) : Infinity;
19646
- if (opening && d < opening.radius) continue;
19647
- const gone = (c) => Math.abs(c[1] - y) <= rowH / 2 && (alongU ? c[0] >= u0 + a && c[0] <= u0 + b : c[2] >= v0 + a && c[2] <= v0 + b);
19648
- if (chips.some(gone)) continue;
19649
- const near = opening && d < opening.radius + 0.25 || chips.some((c) => Math.hypot(c[0] - x, c[1] - y, c[2] - z) < 0.15);
19650
- list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: near || paint === null ? edge : paint });
19780
+ for (let i = 0; i < per + (shift ? 1 : 0); i++) {
19781
+ const a = Math.max(0, i * w - shift), b = Math.min(len, i * w - shift + w);
19782
+ if (b - a > 1e-3) out.push({ a, b, y0: part.y0 + r * rowH, y1: part.y0 + (r + 1) * rowH, r, i });
19651
19783
  }
19652
19784
  }
19653
- return list2;
19785
+ return out;
19654
19786
  }
19655
19787
  const specOf = (id) => opts.spec()?.parts?.find((q) => q.id === id);
19656
19788
  const same = (a, b) => a.every((x, k) => Math.abs(x - b[k]) < 1e-3);
@@ -19664,6 +19796,200 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19664
19796
  return null;
19665
19797
  }
19666
19798
  const riders = (hostId) => structure.parts.filter((q) => q.id !== hostId && specOf(q.id)?.units && hostOf(q.id) === hostId && !structure.meshesOf(q.id).some((it) => it.mesh)).map((q) => q.id);
19799
+ function shell(it, mask) {
19800
+ const g0 = it.mesh.geometry, g = new THREE.BufferGeometry();
19801
+ const src = it.mesh.userData._p0 || g0.attributes.position.array;
19802
+ for (const [name, attr] of Object.entries(g0.attributes)) {
19803
+ const k = attr.itemSize;
19804
+ g.setAttribute(name, new THREE.BufferAttribute((name === "position" ? src : attr.array).slice(it.v0 * k, it.v1 * k), k));
19805
+ }
19806
+ if (g0.index) {
19807
+ const ix = g0.index.array.slice(it.i0, it.i1);
19808
+ for (let k = 0; k < ix.length; k++) ix[k] -= it.v0;
19809
+ g.setIndex(new THREE.BufferAttribute(ix, 1));
19810
+ }
19811
+ g.computeBoundingBox();
19812
+ g.computeBoundingSphere();
19813
+ const base = it.mesh.material;
19814
+ const material = base.clone();
19815
+ const toHouse = { value: new THREE.Matrix4() };
19816
+ material.onBeforeCompile = (sh) => {
19817
+ base.onBeforeCompile?.(sh);
19818
+ sh.uniforms.uToHouse = toHouse;
19819
+ sh.uniforms.uMask = { value: mask.texture };
19820
+ sh.uniforms.uMaskRect = { value: mask.rect };
19821
+ sh.uniforms.uMaskAlongX = { value: mask.alongU ? 1 : 0 };
19822
+ sh.vertexShader = sh.vertexShader.replace("void main() {", "uniform mat4 uToHouse;\nvarying vec3 vHousePos;\nvoid main() {").replace("#include <begin_vertex>", "#include <begin_vertex>\nvHousePos = (uToHouse * vec4(transformed, 1.0)).xyz;");
19823
+ sh.fragmentShader = sh.fragmentShader.replace("void main() {", "uniform sampler2D uMask;\nuniform vec4 uMaskRect;\nuniform float uMaskAlongX;\nvarying vec3 vHousePos;\nvoid main() {\n vec2 mUv = vec2(((uMaskAlongX > 0.5 ? vHousePos.x : vHousePos.z) - uMaskRect.x) / uMaskRect.z, (vHousePos.y - uMaskRect.y) / uMaskRect.w);\n if (mUv.x >= 0.0 && mUv.x <= 1.0 && mUv.y >= 0.0 && mUv.y <= 1.0 && texture2D(uMask, mUv).r > 0.5) discard;");
19824
+ };
19825
+ material.customProgramCacheKey = () => "breachMask" + (base.customProgramCacheKey?.() ?? "");
19826
+ const mesh2 = new THREE.Mesh(g, material);
19827
+ mesh2.name = "breach-shell";
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
+ };
19834
+ mesh2.position.copy(it.mesh.position);
19835
+ mesh2.quaternion.copy(it.mesh.quaternion);
19836
+ mesh2.scale.copy(it.mesh.scale);
19837
+ mesh2.castShadow = it.mesh.castShadow;
19838
+ mesh2.receiveShadow = it.mesh.receiveShadow;
19839
+ mesh2.renderOrder = it.mesh.renderOrder;
19840
+ mesh2.onBeforeRender = () => {
19841
+ house.updateMatrixWorld();
19842
+ toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
19843
+ };
19844
+ it.mesh.parent.add(mesh2);
19845
+ mesh2.updateMatrixWorld(true);
19846
+ toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
19847
+ return mesh2;
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
+ }
19862
+ function maskOf(hostId, parts) {
19863
+ const host = structure.byId.get(hostId);
19864
+ const [u0, v0, u1, v1] = host.aabb;
19865
+ const alongU = u1 - u0 >= v1 - v0;
19866
+ const y0 = Math.min(...parts.map((id) => structure.byId.get(id).y0)), y1 = Math.max(...parts.map((id) => structure.byId.get(id).y1));
19867
+ const len = alongU ? u1 - u0 : v1 - v0;
19868
+ const w = Math.max(2, Math.min(2048, Math.ceil(len / MASK_TEXEL))), h = Math.max(2, Math.min(1024, Math.ceil((y1 - y0) / MASK_TEXEL)));
19869
+ const data = new Uint8Array(w * h);
19870
+ const texture = new THREE.DataTexture(data, w, h, THREE.RedFormat);
19871
+ texture.magFilter = THREE.LinearFilter;
19872
+ texture.minFilter = THREE.LinearFilter;
19873
+ texture.needsUpdate = true;
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) };
19875
+ }
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
+ }
19890
+ function paint(view) {
19891
+ const { mask, core } = view;
19892
+ const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
19893
+ const holes = [];
19894
+ const pits = [];
19895
+ const discs = [];
19896
+ const flakes = [];
19897
+ const shown = [];
19898
+ for (const id of view.parts) {
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);
19900
+ const all = cells(part, units2);
19901
+ const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
19902
+ const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
19903
+ if (opening) discs.push({ s: along(opening.center), y: opening.center[1], r: 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;
19907
+ const kept = [];
19908
+ for (const c of all) (gone(c) ? holes : kept).push(c);
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
+ }
19914
+ }
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);
19917
+ mask.data.fill(0);
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));
19922
+ for (let j = j0; j <= j1; j++) {
19923
+ const y = m.y0 + (j + 0.5) / m.h * m.height;
19924
+ for (let i = i0; i <= i1; i++) {
19925
+ const s = (i + 0.5) / m.w * m.len;
19926
+ if (inside2(s, y)) m.data[j * m.w + i] = 255;
19927
+ }
19928
+ }
19929
+ };
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
+ }
19941
+ mask.texture.needsUpdate = true;
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);
19944
+ const list2 = [];
19945
+ const tone = (hex2, k) => tmp.c.setHex(hex2).multiplyScalar(k).getHex();
19946
+ for (const { c, part, units: units2, thick, mid } of shown) {
19947
+ if (!near(c)) continue;
19948
+ 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;
19949
+ 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 });
19950
+ const base = fragmentMaterial(part.material).color.getHex();
19951
+ const n = noise2(centre * 3.1, y * 2.7, 0.07);
19952
+ if (units2.mortar === "concrete") {
19953
+ at(len, h, thick - 2 * COVER2, tone(base, 0.9 + 0.04 * n));
19954
+ } else {
19955
+ at(len, h, thick - 2 * COVER2 - 0.012, 10327948);
19956
+ at(len - 0.012, h - 0.012, thick - 2 * COVER2, tone(base, 0.8 + 0.35 * n));
19957
+ }
19958
+ }
19959
+ if (view.units) {
19960
+ view.units.removeFromParent();
19961
+ view.units.dispose();
19962
+ }
19963
+ const units = new THREE.InstancedMesh(unitGeometry, view.unitMaterial, Math.max(1, list2.length));
19964
+ units.name = "breach:" + view.host;
19965
+ list2.forEach((u, i) => {
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));
19967
+ units.setMatrixAt(i, tmp.m);
19968
+ units.setColorAt(i, tmp.c.setHex(u.colour));
19969
+ });
19970
+ units.count = list2.length;
19971
+ units.instanceMatrix.needsUpdate = true;
19972
+ if (units.instanceColor) units.instanceColor.needsUpdate = true;
19973
+ units.castShadow = true;
19974
+ units.receiveShadow = true;
19975
+ units.visible = lod === 0;
19976
+ house.add(units);
19977
+ view.units = units;
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 };
19992
+ }
19667
19993
  function draw2(partId, change) {
19668
19994
  if (lod !== 0) return false;
19669
19995
  const hostId = hostOf(partId);
@@ -19673,40 +19999,50 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19673
19999
  const items = structure.meshesOf(hostId).filter((it) => it.mesh);
19674
20000
  if (!items.length) return false;
19675
20001
  const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
19676
- view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
20002
+ if (!parts.includes(partId)) return false;
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
+ };
19677
20021
  }
19678
20022
  if (!view.chips.has(partId)) return false;
19679
20023
  if (change.opening) {
19680
20024
  if (view.openings.has(partId)) return false;
19681
20025
  view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
19682
- if (partId === hostId) view.opening = view.openings.get(partId);
19683
20026
  }
19684
- if (change.chip) view.chips.get(partId).push(local(change.chip));
19685
- const list2 = [];
19686
- for (const [id, chips] of view.chips) list2.push(...courses(structure.byId.get(id), specOf(id).units, view.openings.get(id) ?? null, chips, view.paint));
19687
- if (view.mesh) {
19688
- view.mesh.removeFromParent();
19689
- view.mesh.dispose();
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
+ }
20033
+ if (!view.saved) {
20034
+ view.shells = view.items.map((it) => shell(it, view.mask));
20035
+ for (const m of view.shells) m.userData.behind = () => view.units;
20036
+ view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
19690
20037
  }
19691
- const mesh2 = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
19692
- mesh2.name = "breach:" + hostId;
19693
- list2.forEach((u, i) => {
19694
- tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
19695
- mesh2.setMatrixAt(i, tmp.m);
19696
- mesh2.setColorAt(i, tmp.c.setHex(u.colour));
19697
- });
19698
- mesh2.count = list2.length;
19699
- mesh2.instanceMatrix.needsUpdate = true;
19700
- if (mesh2.instanceColor) mesh2.instanceColor.needsUpdate = true;
19701
- mesh2.castShadow = true;
19702
- mesh2.receiveShadow = true;
19703
- house.add(mesh2);
19704
- if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
19705
- view.mesh = mesh2;
19706
- view.units = list2.length;
19707
20038
  views.set(hostId, view);
19708
- invalidate();
19709
- invalidateShadow();
20039
+ if (opts.deferPaint && change.spall) dirty.add(view);
20040
+ else {
20041
+ dirty.delete(view);
20042
+ paint(view);
20043
+ invalidate();
20044
+ invalidateShadow();
20045
+ }
19710
20046
  return true;
19711
20047
  }
19712
20048
  function plain2(view, shown) {
@@ -19715,30 +20051,88 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19715
20051
  else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
19716
20052
  }
19717
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
+ }
20066
+ function dispose(view) {
20067
+ dirty.delete(view);
20068
+ if (view.units) {
20069
+ view.units.removeFromParent();
20070
+ view.units.dispose();
20071
+ }
20072
+ view.unitMaterial.dispose();
20073
+ for (const m of view.shells) {
20074
+ m.removeFromParent();
20075
+ m.geometry.dispose();
20076
+ m.material.dispose();
20077
+ }
20078
+ view.mask.texture.dispose();
20079
+ view.core.texture.dispose();
20080
+ }
19718
20081
  return {
19719
- /** Handles a `stage` event of the house's object. Returns true when the wall was redrawn as courses. */
20082
+ /** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
19720
20083
  stage(event) {
19721
20084
  return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
19722
20085
  },
19723
- /** Handles a `chip` event: the unit that came out leaves a gap in the courses. Returns true when the wall was redrawn. */
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
+ */
19724
20115
  chip(event) {
19725
- 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] } });
19726
20118
  },
19727
- /** The part broke or fell: the courses go (the plain range stays collapsed, the renderer hides the part). */
20119
+ /** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
19728
20120
  drop(partId) {
19729
20121
  const v = views.get(partId);
19730
20122
  if (!v) return;
19731
- v.mesh.removeFromParent();
19732
- v.mesh.dispose();
20123
+ dispose(v);
19733
20124
  views.delete(partId);
20125
+ if (v.saved) dropped.push(v);
19734
20126
  },
19735
- /** LOD 0 draws the courses; LOD 1 and 2 give the wall its box back. */
20127
+ /** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
19736
20128
  setLod(level) {
20129
+ flush(Infinity);
19737
20130
  const was = lod;
19738
20131
  lod = level;
19739
20132
  if (was === 0 === (level === 0)) return;
19740
20133
  for (const v of views.values()) {
19741
- v.mesh.visible = level === 0;
20134
+ v.units.visible = level === 0;
20135
+ for (const m of v.shells) m.visible = level === 0;
19742
20136
  plain2(v, level !== 0);
19743
20137
  }
19744
20138
  invalidate();
@@ -19747,23 +20141,33 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19747
20141
  /** Everything back to what it was (after `world.reset`). */
19748
20142
  restore() {
19749
20143
  for (const v of views.values()) {
19750
- v.mesh.removeFromParent();
19751
- v.mesh.dispose();
20144
+ dispose(v);
19752
20145
  if (lod === 0) plain2(v, true);
19753
20146
  }
19754
20147
  views.clear();
20148
+ for (const v of dropped) plain2(v, true);
20149
+ dropped.length = 0;
19755
20150
  },
19756
- /** The wall's opening, if it has courses. */
20151
+ flush,
20152
+ /** The wall has damage drawn. */
19757
20153
  has(partId) {
19758
20154
  return views.has(partId);
19759
20155
  },
19760
- statistics: () => ({ walls: views.size, units: [...views.values()].reduce((n, v) => n + v.units, 0) })
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), {
20158
+ walls: views.size,
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),
20162
+ units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
20163
+ })
19761
20164
  };
19762
20165
  }
19763
20166
  Object.assign(K, { createBreachView });
19764
20167
  }
19765
20168
 
19766
20169
  // src/destruction/world.ts
20170
+ var FOLLOW_FLUSH_FALLBACK_MS = 100;
19767
20171
  function installDestructionWorld(K, shared) {
19768
20172
  const { THREE, cfg, invalidateShadow, invalidate, rngString, FRAGMENT_GEO, fragmentMaterial, createBreachView } = K;
19769
20173
  function createHouseDestruction(root, house, structure, parts, colliders, water) {
@@ -19772,7 +20176,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19772
20176
  const R = cfg.destructionRenderer || null, listeners = /* @__PURE__ */ new Set();
19773
20177
  D.renderer = R;
19774
20178
  D.spec = null;
19775
- 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 } : {} });
19776
20180
  D.breach = breach;
19777
20181
  D.onEvent = (fn) => {
19778
20182
  listeners.add(fn);
@@ -19957,6 +20361,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19957
20361
  case "chip":
19958
20362
  breach.chip(e);
19959
20363
  break;
20364
+ case "spall":
20365
+ breach.spall(e);
20366
+ break;
19960
20367
  case "collapse":
19961
20368
  for (const id of e.parts) {
19962
20369
  breach.drop(id);
@@ -19998,7 +20405,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19998
20405
  D.specOptions = specOptions || null;
19999
20406
  D.spec = structure.spec(specOptions);
20000
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] });
20001
- 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() });
20002
20410
  off = D.world.on("*", handleEvent);
20003
20411
  D.active = true;
20004
20412
  return D;
@@ -20013,6 +20421,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20013
20421
  };
20014
20422
  D.follow = (world, id, spec) => {
20015
20423
  D.spec = spec;
20424
+ breach.claim();
20425
+ let fallback = null;
20016
20426
  const offFollow = world.on("*", (e) => {
20017
20427
  if (e.object !== id) return;
20018
20428
  switch (e.type) {
@@ -20022,6 +20432,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20022
20432
  case "chip":
20023
20433
  breach.chip(e);
20024
20434
  break;
20435
+ case "spall":
20436
+ breach.spall(e);
20437
+ break;
20025
20438
  case "fracture":
20026
20439
  breach.drop(e.part);
20027
20440
  break;
@@ -20031,10 +20444,16 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20031
20444
  default:
20032
20445
  break;
20033
20446
  }
20447
+ if (!fallback && typeof setTimeout === "function") fallback = setTimeout(() => {
20448
+ fallback = null;
20449
+ breach.flush(Infinity);
20450
+ }, FOLLOW_FLUSH_FALLBACK_MS);
20034
20451
  });
20035
20452
  return { restore: () => breach.restore(), dispose: () => {
20036
20453
  offFollow();
20037
20454
  breach.restore();
20455
+ }, update: () => {
20456
+ breach.flush();
20038
20457
  } };
20039
20458
  };
20040
20459
  D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
@@ -20067,6 +20486,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
20067
20486
  return r;
20068
20487
  };
20069
20488
  D.animate = (dt) => {
20489
+ breach.flush();
20070
20490
  if (R) return;
20071
20491
  let moved = false;
20072
20492
  for (const [id, list2] of bodies) {
@@ -21531,6 +21951,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21531
21951
  houseRenderer = create3(THREE, {
21532
21952
  locale,
21533
21953
  lod: { interior: 10, hide: 40 },
21954
+ breachPaintBudgetMs: 4,
21534
21955
  ...options.house,
21535
21956
  quality: settings.quality,
21536
21957
  ...options.world ? { destruction: options.world } : {},
@@ -21686,9 +22107,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21686
22107
  host?.log(text.steps.no_door);
21687
22108
  return;
21688
22109
  }
21689
- 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));
21690
22115
  host?.log(fill(text.steps.door, { text: String(target.text) }));
21691
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
+ };
21692
22143
  const strike = () => {
21693
22144
  const world = options.world;
21694
22145
  if (!world || !host) return;