@adatechnology/game-weapons-three 0.10.4 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19264,26 +19264,26 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19264
19264
  for (const e of planned.items) if (e.type === "wall_switch") e.lit = (e.connections.controls || []).some((id) => (planned.items.find((x) => x.id === id) || {}).lit === true);
19265
19265
  }
19266
19266
  for (const e of planned.items) if (e.type === "lantern_lamp") e.item.on = !!e.lit;
19267
- let scratch = null;
19267
+ let scratch2 = null;
19268
19268
  function probe(e, hinge, setback) {
19269
- if (!scratch) scratch = GI.createWorld({ locale, strings: hh.strings, items: hh.items, loot: hh.loot, grid: false });
19269
+ if (!scratch2) scratch2 = GI.createWorld({ locale, strings: hh.strings, items: hh.items, loot: hh.loot, grid: false });
19270
19270
  const it = e.item;
19271
19271
  const x = it.x + Math.sin(it.ry) * setback;
19272
19272
  const z = it.z + Math.cos(it.ry) * setback;
19273
19273
  const spec = hh.build(e.type, Object.assign({}, it, { hinge }), HH.createRecorder().api);
19274
- scratch.add("__probe", spec, { x, y: e.floorY, z, ry: it.ry });
19274
+ scratch2.add("__probe", spec, { x, y: e.floorY, z, ry: it.ry });
19275
19275
  const blocks = (px, pz) => isBlocked2(px, pz, e.floor);
19276
19276
  let sum2 = 0;
19277
19277
  let min2 = 1;
19278
19278
  let n = 0;
19279
19279
  for (const p of spec.parts || []) {
19280
19280
  if (p.motion.kind !== "hinge" || !(p.bounds || p.sweep)) continue;
19281
- const t = scratch.fitSweep("__probe", p.id, blocks);
19281
+ const t = scratch2.fitSweep("__probe", p.id, blocks);
19282
19282
  sum2 += t;
19283
19283
  min2 = Math.min(min2, t);
19284
19284
  n++;
19285
19285
  }
19286
- scratch.remove("__probe");
19286
+ scratch2.remove("__probe");
19287
19287
  return { sum: sum2, min: min2, n };
19288
19288
  }
19289
19289
  const better = (a, b) => a.sum > b.sum + 1e-9 || Math.abs(a.sum - b.sum) <= 1e-9 && a.min > b.min + 1e-9;
@@ -21972,6 +21972,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21972
21972
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
21973
21973
  var VENT_DIAMETER = 8e-3;
21974
21974
  var FLAME_LIGHTS_LEAK_C = 470;
21975
+ var RESTING_HOLD_COS = Math.cos(Math.PI / 6);
21975
21976
  var BREACH_RADIUS = 0.55;
21976
21977
  var BREACH_BLAST_FACTOR = 6;
21977
21978
  var BREACH_AREA = 0.25;
@@ -21987,6 +21988,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21987
21988
  var BREACH_GRID_CAP = 4096;
21988
21989
  var CHIP_ENERGY = 200;
21989
21990
  var CHIP_SPLASH = 0.5;
21991
+ var CRATER_SPENT = 0.6;
21990
21992
  var BAR_BOW = 0.08;
21991
21993
  var BULLET_JOINT_SPAN = 0.3;
21992
21994
  var CUT_SLICE = 1;
@@ -22007,12 +22009,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22007
22009
  const cells = p.st.chipped;
22008
22010
  if (!cells?.length) return 0;
22009
22011
  const [u, v] = planeAxes(p.thin);
22010
- const along = p.dims[u] >= p.dims[v] ? u : v;
22012
+ const role = p.spec.structure?.role;
22013
+ const up = rotateVec(p.q, [0, 0, 0].map((_, k) => k === v ? 1 : 0));
22014
+ 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;
22015
+ const along = (role === "wall" || role === "partition" || role === "column") && upright >= 0 ? upright : p.dims[u] >= p.dims[v] ? u : v;
22011
22016
  const across2 = along === u ? v : u;
22012
22017
  let worst = 0;
22013
22018
  for (const c of cells) {
22014
- let gone = 0;
22015
- for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across2];
22019
+ const spans = [];
22020
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) spans.push([d.center[across2] - d.size[across2] / 2, d.center[across2] + d.size[across2] / 2]);
22021
+ spans.sort((a, b) => a[0] - b[0]);
22022
+ let gone = 0, end3 = -Infinity;
22023
+ for (const [a, b] of spans) {
22024
+ if (b <= end3) continue;
22025
+ gone += b - Math.max(a, end3);
22026
+ end3 = b;
22027
+ }
22016
22028
  worst = Math.max(worst, gone / Math.max(p.dims[across2], 1e-6));
22017
22029
  }
22018
22030
  return Math.min(1, worst);
@@ -22308,7 +22320,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22308
22320
  if (k < 0 || j.spec.kind !== "tie") return k;
22309
22321
  return partXf(o, o.parts[k]).c[1] > partXf(o, o.parts[r]).c[1] + 1e-6 ? k : -1;
22310
22322
  };
22323
+ let poseVersion = 0;
22324
+ const contactMemo = /* @__PURE__ */ new WeakMap();
22311
22325
  const contactBox = (p, support) => {
22326
+ let byTouching = contactMemo.get(p);
22327
+ if (!byTouching) contactMemo.set(p, byTouching = /* @__PURE__ */ new Map());
22328
+ const known = byTouching.get(support);
22329
+ if (known && known.version === poseVersion) return known.box;
22330
+ const box5 = measureContact(p, support);
22331
+ byTouching.set(support, { version: poseVersion, box: box5 });
22332
+ return box5;
22333
+ };
22334
+ const measureContact = (p, support) => {
22312
22335
  const A = aabbOf(p.spec.shape, p.pos, p.q), B = aabbOf(support.spec.shape, support.pos, support.q);
22313
22336
  const lo = [0, 0, 0], hi = [0, 0, 0];
22314
22337
  for (let k = 0; k < 3; k++) {
@@ -23103,7 +23126,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23103
23126
  p.st.holes.push(hole);
23104
23127
  if (p.st.holes.length > maxHoles) p.st.holes.shift();
23105
23128
  markDamaged(p);
23106
- emit2({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {} });
23129
+ emit2({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {}, ...p.spec.powder ? { powder: { ...p.spec.powder } } : {} });
23107
23130
  setStage(o, p, 1, {}, src);
23108
23131
  return hole;
23109
23132
  };
@@ -23120,6 +23143,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23120
23143
  const detachEvents = (o, opened, by, src) => {
23121
23144
  for (const j of opened) emit2({ ...base(o, src), type: "detach", joint: j.spec.id, parts: [j.spec.a, j.spec.b], by, groups: o.st.groups.map((g) => [...g]) });
23122
23145
  };
23146
+ const slideOffResting = (o, rotation) => {
23147
+ const upY = 1 - 2 * (rotation[0] * rotation[0] + rotation[2] * rotation[2]);
23148
+ if (upY >= RESTING_HOLD_COS) return;
23149
+ const opened = o.joints.filter((j) => j.spec.kind === "resting" && !j.st.open);
23150
+ if (opened.length === 0) return;
23151
+ run(() => {
23152
+ for (const j of opened) {
23153
+ j.st.open = true;
23154
+ j.st.openedBy = "break";
23155
+ }
23156
+ solveStructure(o);
23157
+ recomputeGroups(o);
23158
+ detachEvents(o, opened, "break");
23159
+ collapseCheck(o);
23160
+ });
23161
+ };
23123
23162
  const checkDestroyed = (o, src) => {
23124
23163
  if (!o.st.destroyed && o.parts.length > 0 && o.parts.every(isLost)) {
23125
23164
  o.st.destroyed = true;
@@ -23220,7 +23259,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23220
23259
  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 }));
23221
23260
  const unitsOf2 = (p) => p.spec.units ?? { size: [p.mat.fragmentSize?.[1] ?? 0.2, p.mat.fragmentSize?.[1] ?? 0.2, p.thickness], bond: "stack" };
23222
23261
  const FRAGMENT_SHAPE = { shatter: "shard", splinter: "splinter", tear: "scrap" };
23223
- const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag) => {
23262
+ const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag, inner = false) => {
23224
23263
  const { c, q } = partXf(o, p);
23225
23264
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
23226
23265
  const [u, v] = planeAxes(p.thin);
@@ -23244,7 +23283,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23244
23283
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd2(), dv[2] * k],
23245
23284
  angularVelocity: [(rnd2() - 0.5) * 8, (rnd2() - 0.5) * 8, (rnd2() - 0.5) * 8],
23246
23285
  mass: mass * faceArea(cell) / total,
23247
- ...marks.length > 0 ? { marks } : {}
23286
+ ...marks.length > 0 ? { marks } : {},
23287
+ ...inner ? { inner } : {}
23248
23288
  };
23249
23289
  });
23250
23290
  };
@@ -23285,7 +23325,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23285
23325
  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);
23286
23326
  };
23287
23327
  const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
23288
- var _a, _b;
23328
+ var _a, _b, _c;
23289
23329
  if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
23290
23330
  const rule = p.mat.hits?.[kind];
23291
23331
  const perKg = rule?.unitJoules ?? chipEnergy;
@@ -23295,7 +23335,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23295
23335
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
23296
23336
  const grid = gridOf(p);
23297
23337
  const worn = (_a = p.rt).chips ?? (_a.chips = {});
23338
+ const cut = (_b = p.rt).craters ?? (_b.craters = {});
23298
23339
  const out = [];
23340
+ const shot = kind === "ballistic";
23299
23341
  for (let i = 0; i < grid.length; i++) {
23300
23342
  const cell = grid[i];
23301
23343
  if (worn[i] === -1) continue;
@@ -23308,18 +23350,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23308
23350
  worn[i] = total;
23309
23351
  continue;
23310
23352
  }
23311
- worn[i] = -1;
23312
- out.push(cell);
23353
+ if (!shot) {
23354
+ worn[i] = -1;
23355
+ out.push(cell);
23356
+ continue;
23357
+ }
23358
+ const size = [cell.size[0], cell.size[1], cell.size[2]];
23359
+ const center = [cell.center[0], cell.center[1], cell.center[2]];
23360
+ for (const a of [u, v]) {
23361
+ const w = Math.min(cell.size[a], craterAcross(hitEnergy) * (0.85 + 0.3 * rnd2()));
23362
+ const lo = cell.center[a] - cell.size[a] / 2 + w / 2, hi = cell.center[a] + cell.size[a] / 2 - w / 2;
23363
+ size[a] = w;
23364
+ center[a] = clamp2(lp[a], lo, hi);
23365
+ }
23366
+ const taken2 = (cut[i] ?? 0) + size[u] * size[v];
23367
+ if (taken2 >= cell.size[u] * cell.size[v] * CRATER_SPENT) {
23368
+ worn[i] = -1;
23369
+ out.push({ ...cell, crater: true });
23370
+ continue;
23371
+ }
23372
+ worn[i] = 0;
23373
+ cut[i] = taken2;
23374
+ out.push({ center, size, crater: true });
23313
23375
  }
23314
23376
  if (out.length === 0) return;
23315
23377
  const { c, q } = partXf(o, p);
23316
- const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
23378
+ const chipped = (_c = p.st).chipped ?? (_c.chipped = []);
23317
23379
  const holes = localHolesOf(p);
23318
23380
  for (const cell of out) {
23319
23381
  const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
23320
23382
  const center = toWorld(c, q, cell.center);
23321
23383
  const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
23322
- const fragments = cellFragments(o, p, [cell], wp, dir, speed, mass, `c${chipped.length}_`);
23384
+ const tag = `c${chipped.length}_`;
23385
+ const fragments = cell.crater ? craterFragments(o, p, cell, dir, speed, mass, tag) : cellFragments(o, p, [cell], wp, dir, speed, mass, tag, true);
23323
23386
  p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
23324
23387
  chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
23325
23388
  p.st.mass = Math.max(0, p.st.mass - mass);
@@ -23336,6 +23399,44 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23336
23399
  collapseCheck(o, src);
23337
23400
  }
23338
23401
  };
23402
+ const craterAcross = (energy) => clamp2(0.03 + 12e-4 * Math.sqrt(Math.max(0, energy)), 0.03, 0.14);
23403
+ const craterFragments = (o, p, cell, dir, speed, mass, tag) => {
23404
+ const { c, q } = partXf(o, p);
23405
+ const [u, v] = planeAxes(p.thin);
23406
+ const d = dir ?? rotateVec(q, [p.thin === 0 ? 1 : 0, p.thin === 1 ? 1 : 0, p.thin === 2 ? 1 : 0]);
23407
+ const exitSign = Math.sign(rotateVec(quat.conjugate(q), d)[p.thin]) || 1;
23408
+ const bit = Math.max(0.01, Math.min(cell.size[u], cell.size[v]) / 3);
23409
+ const n = clamp2(Math.round(cell.size[u] * cell.size[v] / (bit * bit * 3)), 4, 10) + Math.floor(rnd2() * 3);
23410
+ const out = [];
23411
+ for (let i = 0; i < n; i++) {
23412
+ const back = i % 3 === 2;
23413
+ const lc = [cell.center[0], cell.center[1], cell.center[2]];
23414
+ lc[u] += (rnd2() - 0.5) * cell.size[u] * 0.7;
23415
+ lc[v] += (rnd2() - 0.5) * cell.size[v] * 0.7;
23416
+ lc[p.thin] += (back ? -exitSign : exitSign) * (p.thickness / 2 + 0.01);
23417
+ const size = [0, 0, 0];
23418
+ size[u] = bit * (0.6 + 0.8 * rnd2());
23419
+ size[v] = bit * (0.5 + 0.7 * rnd2());
23420
+ size[p.thin] = bit * (0.4 + 0.6 * rnd2());
23421
+ const spread3 = [(rnd2() - 0.5) * 0.9, (rnd2() - 0.3) * 0.9, (rnd2() - 0.5) * 0.9];
23422
+ const k = speed * (back ? 0.4 + 0.4 * rnd2() : 0.7 + 0.8 * rnd2());
23423
+ const vdir = vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3));
23424
+ out.push({
23425
+ id: `${p.spec.id}#${tag}${i}`,
23426
+ part: p.spec.id,
23427
+ material: p.spec.material,
23428
+ shape: "chunk",
23429
+ center: toWorld(c, q, lc),
23430
+ size,
23431
+ rotation: q,
23432
+ velocity: [vdir[0] * k, vdir[1] * k + 0.5 * rnd2(), vdir[2] * k],
23433
+ angularVelocity: [(rnd2() - 0.5) * 20, (rnd2() - 0.5) * 20, (rnd2() - 0.5) * 20],
23434
+ mass: mass / n,
23435
+ inner: true
23436
+ });
23437
+ }
23438
+ return out;
23439
+ };
23339
23440
  const exposeRebar = (o, p, src) => {
23340
23441
  const r = p.spec.reinforcement;
23341
23442
  if (!r) return;
@@ -23623,7 +23724,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23623
23724
  });
23624
23725
  }
23625
23726
  const patch = [scale * 4, scale * 4, scale * 4];
23626
- emit2({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments });
23727
+ emit2({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments, torn: true });
23627
23728
  };
23628
23729
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round5 = {}) => {
23629
23730
  const mat = p.mat;
@@ -23682,12 +23783,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23682
23783
  } else if (mat.ductility >= 0.5) {
23683
23784
  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);
23684
23785
  } else if (brittle) {
23685
- addCrack(o, p, entry2, n, crackLen, src);
23786
+ if (!p.spec.units) addCrack(o, p, entry2, n, crackLen, src);
23686
23787
  if (mat.fracture !== "shatter") addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
23687
23788
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
23688
23789
  debris2(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
23689
23790
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
23690
- const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
23791
+ const hollowTear = mat.fracture === "tear" && p.spec.fill !== void 0 && p.spec.fill < 0.5;
23792
+ const absorbed = penetrated && (mat.ductility >= 0.5 && mat.fracture !== "tear" || hollowTear) ? Math.min(E, thr) : E;
23691
23793
  const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
23692
23794
  if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
23693
23795
  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);
@@ -23772,10 +23874,25 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23772
23874
  if (dist2 <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
23773
23875
  return reach;
23774
23876
  };
23877
+ const blastProbe = { partsTested: 0, partsNear: 0 };
23878
+ const blastCandidates = (o, point, radius) => {
23879
+ const local2 = toLocal(o.st.position, o.st.rotation, point);
23880
+ const near = [];
23881
+ for (const p of o.parts) {
23882
+ const reach = radius + p.bound;
23883
+ const dx = local2[0] - p.pos[0];
23884
+ const dy = local2[1] - p.pos[1];
23885
+ const dz = local2[2] - p.pos[2];
23886
+ if (dx * dx + dy * dy + dz * dz < reach * reach) near.push(p);
23887
+ }
23888
+ blastProbe.partsTested += o.parts.length;
23889
+ blastProbe.partsNear += near.length;
23890
+ return near;
23891
+ };
23775
23892
  const blastObject = (o, point, energy, radius, src) => {
23776
23893
  const start = cur.length;
23777
23894
  const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
23778
- for (const p of o.parts) blastPart(o, p, point, energy, radius, src, res);
23895
+ for (const p of blastCandidates(o, point, radius)) blastPart(o, p, point, energy, radius, src, res);
23779
23896
  res.events = cur.slice(start);
23780
23897
  return res;
23781
23898
  };
@@ -23899,7 +24016,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23899
24016
  let order = 0;
23900
24017
  for (const o of inRange) {
23901
24018
  if (only && only !== o) continue;
23902
- for (const p of o.parts) {
24019
+ for (const p of blastCandidates(o, point, radius)) {
23903
24020
  if (isLost(p)) continue;
23904
24021
  const dist2 = distToPart(o, p, point);
23905
24022
  if (dist2 < radius) items.push({ dist: dist2, order: order++, o, p });
@@ -24446,6 +24563,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
24446
24563
  const p = getPart(o, partId);
24447
24564
  p.pos = [position[0], position[1], position[2]];
24448
24565
  p.q = q;
24566
+ poseVersion++;
24449
24567
  updateBound(o);
24450
24568
  },
24451
24569
  remove(id) {
@@ -24464,6 +24582,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
24464
24582
  const o = getObj(id);
24465
24583
  o.base = { pos: [position[0], position[1], position[2]], rot: [rotation[0], rotation[1], rotation[2], rotation[3]] };
24466
24584
  applySoilPose(o);
24585
+ slideOffResting(o, rotation);
24467
24586
  },
24468
24587
  splitGroup(id, group, newId) {
24469
24588
  if (impls.has(id)) throw new Error(`object "${id}" is a registered DestructibleImpl and cannot be split; its module owns its pieces`);
@@ -24487,7 +24606,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
24487
24606
  if (o.spec.name !== void 0) s.name = o.spec.name;
24488
24607
  if (o.spec.tags) s.tags = [...o.spec.tags];
24489
24608
  if (o.spec.soil !== void 0) s.soil = o.spec.soil;
24490
- return clone(s);
24609
+ return s;
24491
24610
  };
24492
24611
  const inNew = (pid) => moved.has(pid);
24493
24612
  const inOld = (pid) => !moved.has(pid);
@@ -24514,7 +24633,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
24514
24633
  const p = getPart(o, partId);
24515
24634
  return whole(() => doHit(o, h, p));
24516
24635
  },
24636
+ blastProbe() {
24637
+ return { ...blastProbe };
24638
+ },
24517
24639
  explode(point, energy, radius, source, blast) {
24640
+ blastProbe.partsTested = 0;
24641
+ blastProbe.partsNear = 0;
24518
24642
  return withCollapses(run(() => blastWorld(point, energy, radius, source, blast)));
24519
24643
  },
24520
24644
  raycast(origin, direction, maxDistance = Infinity) {
@@ -24524,7 +24648,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
24524
24648
  sweepBox(box5, direction, distance3, options2 = {}) {
24525
24649
  const d = vec.normalize(direction);
24526
24650
  const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
24527
- const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
24651
+ const hosted = options2.ignore === void 0 || ignored ? void 0 : impls.get(options2.ignore);
24652
+ 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;
24528
24653
  let best = null;
24529
24654
  for (const origin of sweepOrigins(box5, d, options2.spacing ?? 0.2)) {
24530
24655
  const r = raycastInternal(origin, d, distance3 + SWEEP_BACK, ignore);
@@ -34127,6 +34252,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34127
34252
  function collapseRange(mesh3, v0, v1) {
34128
34253
  const pos = mesh3.geometry.attributes.position;
34129
34254
  const a = pos.array;
34255
+ const u = mesh3.userData;
34256
+ if (!u._p0) u._p0 = a.slice();
34257
+ const list2 = u.collapsedRanges ?? (u.collapsedRanges = []);
34258
+ if (!list2.some((r) => r[0] === v0 && r[1] === v1)) list2.push([v0, v1]);
34130
34259
  const saved = a.slice(v0 * 3, v1 * 3);
34131
34260
  const x = a[v0 * 3], y = a[v0 * 3 + 1], z = a[v0 * 3 + 2];
34132
34261
  for (let k = v0; k < v1; k++) {
@@ -34141,6 +34270,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34141
34270
  const pos = mesh3.geometry.attributes.position;
34142
34271
  pos.array.set(saved, v0 * 3);
34143
34272
  pos.needsUpdate = true;
34273
+ const list2 = mesh3.userData.collapsedRanges;
34274
+ if (list2) mesh3.userData.collapsedRanges = list2.filter((r) => r[0] !== v0);
34144
34275
  }
34145
34276
  function noise2(x, y, cell) {
34146
34277
  const h = (i2, j2) => {
@@ -34155,13 +34286,16 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34155
34286
  var SPALL_MIN = 0.025;
34156
34287
  var SPALL_RAGGED = 0.045;
34157
34288
  var COVER2 = 0.015;
34289
+ var MAX_FLAKES = 64;
34158
34290
  function installDestructionBreach(K, _shared) {
34159
34291
  const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
34160
34292
  function createBreachView(opts) {
34161
34293
  const { house, structure } = opts;
34162
34294
  const views = /* @__PURE__ */ new Map();
34295
+ const dirty = /* @__PURE__ */ new Set();
34296
+ const paintBudgetMs = opts.paintBudgetMs ?? Infinity;
34297
+ const dropped = [];
34163
34298
  let lod = 0;
34164
- const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
34165
34299
  const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
34166
34300
  const tmp = { m: new THREE.Matrix4(), q: new THREE.Quaternion(), p: new THREE.Vector3(), s: new THREE.Vector3(), c: new THREE.Color() };
34167
34301
  const local2 = (p) => {
@@ -34230,6 +34364,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34230
34364
  const mesh3 = new THREE.Mesh(g, material);
34231
34365
  mesh3.name = "breach-shell";
34232
34366
  mesh3.userData.breachMask = mask;
34367
+ mesh3.userData.cutAt = (p) => {
34368
+ const v = local2([p.x, p.y, p.z]);
34369
+ 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);
34370
+ return i >= 0 && i < mask.w && j >= 0 && j < mask.h && mask.data[j * mask.w + i] > 127;
34371
+ };
34233
34372
  mesh3.position.copy(it.mesh.position);
34234
34373
  mesh3.quaternion.copy(it.mesh.quaternion);
34235
34374
  mesh3.scale.copy(it.mesh.scale);
@@ -34245,6 +34384,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34245
34384
  toHouse.value.copy(house.matrixWorld).invert().multiply(mesh3.matrixWorld);
34246
34385
  return mesh3;
34247
34386
  }
34387
+ function unitsMaterial(core) {
34388
+ const material = new THREE.MeshLambertMaterial({ color: 16777215, side: THREE.DoubleSide });
34389
+ material.onBeforeCompile = (sh) => {
34390
+ sh.uniforms.uCore = { value: core.texture };
34391
+ sh.uniforms.uCoreRect = { value: core.rect };
34392
+ sh.uniforms.uCoreAlongX = { value: core.alongU ? 1 : 0 };
34393
+ 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");
34394
+ 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;");
34395
+ };
34396
+ material.customProgramCacheKey = () => "breachCore";
34397
+ material.userData.breachCore = core;
34398
+ return material;
34399
+ }
34248
34400
  function maskOf(hostId, parts) {
34249
34401
  const host = structure.byId.get(hostId);
34250
34402
  const [u0, v0, u1, v1] = host.aabb;
@@ -34260,41 +34412,73 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34260
34412
  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) };
34261
34413
  }
34262
34414
  const boxDistance = (s, y, a, b, c, d) => Math.hypot(Math.max(a - s, 0, s - b), Math.max(c - y, 0, y - d));
34415
+ 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);
34416
+ function covered(c, boxes) {
34417
+ const n = 8;
34418
+ let hit = 0;
34419
+ for (let j = 0; j < n; j++) {
34420
+ const y = c.y0 + (j + 0.5) / n * (c.y1 - c.y0);
34421
+ for (let i = 0; i < n; i++) {
34422
+ const s = c.a + (i + 0.5) / n * (c.b - c.a);
34423
+ if (boxes.some((k) => s >= k.s0 && s <= k.s1 && y >= k.y0 && y <= k.y1)) hit++;
34424
+ }
34425
+ }
34426
+ return hit / (n * n);
34427
+ }
34263
34428
  function paint2(view) {
34264
- const { mask } = view;
34429
+ const { mask, core } = view;
34265
34430
  const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
34266
34431
  const holes = [];
34432
+ const pits = [];
34267
34433
  const discs = [];
34434
+ const flakes = [];
34268
34435
  const shown = [];
34269
34436
  for (const id of view.parts) {
34270
- const part = structure.byId.get(id), units2 = specOf(id).units, chips2 = view.chips.get(id), opening = view.openings.get(id);
34437
+ const part = structure.byId.get(id), units2 = specOf(id).units, chips2 = view.chips.get(id), opening = view.openings.get(id), spalls = view.spalls.get(id);
34271
34438
  const all = cells(part, units2);
34272
34439
  const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
34273
34440
  const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
34274
34441
  if (opening) discs.push({ s: along(opening.center), y: opening.center[1], r: opening.radius });
34275
- const gone = (c) => chips2.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;
34442
+ for (const k of spalls) flakes.push({ s: along(k.center), y: k.center[1], r: k.radius });
34443
+ const boxes = chips2.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 }));
34444
+ 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;
34276
34445
  const kept = [];
34277
34446
  for (const c of all) (gone(c) ? holes : kept).push(c);
34278
34447
  for (const c of kept) shown.push({ c, part, units: units2, thick, mid });
34448
+ for (const k of boxes) {
34449
+ 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;
34450
+ 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 });
34451
+ }
34279
34452
  }
34280
34453
  const reach = SPALL_MIN + SPALL_RAGGED;
34454
+ const ragged = (s, y) => 0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011);
34281
34455
  mask.data.fill(0);
34282
- const stamp = (s0, s1, y0, y1, inside2) => {
34283
- 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));
34284
- 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));
34456
+ core.data.fill(0);
34457
+ const stamp = (m, s0, s1, y0, y1, inside2) => {
34458
+ 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));
34459
+ 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));
34285
34460
  for (let j = j0; j <= j1; j++) {
34286
- const y = mask.y0 + (j + 0.5) / mask.h * mask.height;
34461
+ const y = m.y0 + (j + 0.5) / m.h * m.height;
34287
34462
  for (let i = i0; i <= i1; i++) {
34288
- const s = (i + 0.5) / mask.w * mask.len;
34289
- const edge = SPALL_MIN + SPALL_RAGGED * (0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011));
34290
- if (inside2(s, y) <= edge) mask.data[j * mask.w + i] = 255;
34463
+ const s = (i + 0.5) / m.w * m.len;
34464
+ if (inside2(s, y)) m.data[j * m.w + i] = 255;
34291
34465
  }
34292
34466
  }
34293
34467
  };
34294
- 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));
34295
- 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));
34468
+ const plaster = (d, s, y) => d <= SPALL_MIN + SPALL_RAGGED * ragged(s, y);
34469
+ 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));
34470
+ 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));
34471
+ for (const f of flakes) {
34472
+ 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));
34473
+ 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);
34474
+ }
34475
+ for (const p of pits) {
34476
+ 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));
34477
+ 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));
34478
+ }
34296
34479
  mask.texture.needsUpdate = true;
34297
- 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);
34480
+ core.texture.needsUpdate = true;
34481
+ 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);
34298
34482
  const list2 = [];
34299
34483
  const tone = (hex2, k) => tmp.c.setHex(hex2).multiplyScalar(k).getHex();
34300
34484
  for (const { c, part, units: units2, thick, mid } of shown) {
@@ -34304,7 +34488,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34304
34488
  const base = fragmentMaterial(part.material).color.getHex();
34305
34489
  const n = noise2(centre * 3.1, y * 2.7, 0.07);
34306
34490
  if (units2.mortar === "concrete") {
34307
- at3(len, h, thick - 2 * COVER2 - 0.03 * n, tone(base, 0.86 + 0.2 * n));
34491
+ at3(len, h, thick - 2 * COVER2, tone(base, 0.9 + 0.04 * n));
34308
34492
  } else {
34309
34493
  at3(len, h, thick - 2 * COVER2 - 0.012, 10327948);
34310
34494
  at3(len - 0.012, h - 0.012, thick - 2 * COVER2, tone(base, 0.8 + 0.35 * n));
@@ -34314,7 +34498,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34314
34498
  view.units.removeFromParent();
34315
34499
  view.units.dispose();
34316
34500
  }
34317
- const units = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
34501
+ const units = new THREE.InstancedMesh(unitGeometry, view.unitMaterial, Math.max(1, list2.length));
34318
34502
  units.name = "breach:" + view.host;
34319
34503
  list2.forEach((u, i) => {
34320
34504
  tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
@@ -34330,6 +34514,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34330
34514
  house.add(units);
34331
34515
  view.units = units;
34332
34516
  view.gone = holes.length;
34517
+ view.pits = pits.length;
34518
+ view.flakes = flakes.length;
34519
+ }
34520
+ function chipBox(chip, alongU) {
34521
+ house.updateMatrixWorld(true);
34522
+ const q = tmp.q.set(chip.rotation[0], chip.rotation[1], chip.rotation[2], chip.rotation[3]).premultiply(house.getWorldQuaternion(new THREE.Quaternion()).invert());
34523
+ let a = 0, u = 0;
34524
+ for (let k = 0; k < 3; k++) {
34525
+ const axis2 = tmp.p.set(k === 0 ? 1 : 0, k === 1 ? 1 : 0, k === 2 ? 1 : 0).applyQuaternion(q);
34526
+ a += Math.abs(alongU ? axis2.x : axis2.z) * chip.size[k] / 2;
34527
+ u += Math.abs(axis2.y) * chip.size[k] / 2;
34528
+ }
34529
+ return { center: local2(chip.center), along: a, up: u };
34333
34530
  }
34334
34531
  function draw2(partId, change) {
34335
34532
  if (lod !== 0) return false;
@@ -34341,23 +34538,49 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34341
34538
  if (!items.length) return false;
34342
34539
  const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
34343
34540
  if (!parts.includes(partId)) return false;
34344
- const mask = maskOf(hostId, parts);
34345
- view = { host: hostId, parts, items, mask, shells: [], saved: null, units: null, gone: 0, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
34541
+ const mask = maskOf(hostId, parts), core = maskOf(hostId, parts);
34542
+ view = {
34543
+ host: hostId,
34544
+ parts,
34545
+ items,
34546
+ mask,
34547
+ core,
34548
+ unitMaterial: unitsMaterial(core),
34549
+ shells: [],
34550
+ saved: null,
34551
+ units: null,
34552
+ gone: 0,
34553
+ pits: 0,
34554
+ chips: new Map(parts.map((id) => [id, []])),
34555
+ spalls: new Map(parts.map((id) => [id, []])),
34556
+ openings: /* @__PURE__ */ new Map(),
34557
+ flakes: 0
34558
+ };
34346
34559
  }
34347
34560
  if (!view.chips.has(partId)) return false;
34348
34561
  if (change.opening) {
34349
34562
  if (view.openings.has(partId)) return false;
34350
34563
  view.openings.set(partId, { center: local2(change.opening.center), radius: change.opening.radius });
34351
34564
  }
34352
- if (change.chip) view.chips.get(partId).push(local2(change.chip));
34565
+ if (change.chip) view.chips.get(partId).push(chipBox(change.chip, view.mask.alongU));
34566
+ if (change.spall) {
34567
+ const list2 = view.spalls.get(partId);
34568
+ list2.push({ center: local2(change.spall.center), radius: change.spall.radius });
34569
+ if (list2.length > MAX_FLAKES) list2.shift();
34570
+ }
34353
34571
  if (!view.saved) {
34354
34572
  view.shells = view.items.map((it) => shell(it, view.mask));
34573
+ for (const m of view.shells) m.userData.behind = () => view.units;
34355
34574
  view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
34356
34575
  }
34357
- paint2(view);
34358
34576
  views.set(hostId, view);
34359
- invalidate();
34360
- invalidateShadow();
34577
+ if (opts.deferPaint && change.spall) dirty.add(view);
34578
+ else {
34579
+ dirty.delete(view);
34580
+ paint2(view);
34581
+ invalidate();
34582
+ invalidateShadow();
34583
+ }
34361
34584
  return true;
34362
34585
  }
34363
34586
  function plain2(view, shown) {
@@ -34366,24 +34589,70 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34366
34589
  else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
34367
34590
  }
34368
34591
  }
34592
+ function flush(budgetMs = paintBudgetMs) {
34593
+ if (dirty.size === 0) return 0;
34594
+ const deadline = performance.now() + budgetMs;
34595
+ for (const view of dirty) {
34596
+ dirty.delete(view);
34597
+ paint2(view);
34598
+ if (performance.now() >= deadline) break;
34599
+ }
34600
+ invalidate();
34601
+ invalidateShadow();
34602
+ return dirty.size;
34603
+ }
34369
34604
  function dispose(view) {
34370
- view.units.removeFromParent();
34371
- view.units.dispose();
34605
+ dirty.delete(view);
34606
+ if (view.units) {
34607
+ view.units.removeFromParent();
34608
+ view.units.dispose();
34609
+ }
34610
+ view.unitMaterial.dispose();
34372
34611
  for (const m of view.shells) {
34373
34612
  m.removeFromParent();
34374
34613
  m.geometry.dispose();
34375
34614
  m.material.dispose();
34376
34615
  }
34377
34616
  view.mask.texture.dispose();
34617
+ view.core.texture.dispose();
34378
34618
  }
34379
34619
  return {
34380
34620
  /** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
34381
34621
  stage(event) {
34382
34622
  return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
34383
34623
  },
34384
- /** 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. */
34624
+ /**
34625
+ * 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
34626
+ * renderer leaves its flat patch out on the meshes `claim` marked). Returns true when the wall was cut.
34627
+ */
34628
+ spall(event) {
34629
+ const r = Math.sqrt(Math.max(0, event.area) / Math.PI);
34630
+ return r > 0 ? draw2(event.part, { spall: { center: event.point, radius: r } }) : false;
34631
+ },
34632
+ /**
34633
+ * Marks the meshes that draw a wall of units as drawing its spalls itself (`userData.drawnSpalls`, the part ids): the destruction renderer then
34634
+ * throws the bits and leaves its patch out. Call it once the spec is known.
34635
+ */
34636
+ claim() {
34637
+ var _a;
34638
+ for (const q of opts.spec()?.parts ?? []) {
34639
+ if (!q.units) continue;
34640
+ const host = hostOf(q.id);
34641
+ if (!host) continue;
34642
+ for (const it of structure.meshesOf(host)) {
34643
+ if (!it.mesh) continue;
34644
+ const list2 = (_a = it.mesh.userData).drawnSpalls ?? (_a.drawnSpalls = []);
34645
+ if (!list2.includes(q.id)) list2.push(q.id);
34646
+ }
34647
+ }
34648
+ },
34649
+ /**
34650
+ * 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
34651
+ * tore off a face) leaves the marks the renderer drew. Returns true when the wall was cut.
34652
+ */
34385
34653
  chip(event) {
34386
- return draw2(event.part, { chip: event.center });
34654
+ if (event.torn) return false;
34655
+ return draw2(event.part, { chip: { center: event.center, size: event.size ?? [0, 0, 0], rotation: event.rotation ?? [0, 0, 0, 1] } });
34387
34656
  },
34388
34657
  /** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
34389
34658
  drop(partId) {
@@ -34391,9 +34660,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34391
34660
  if (!v) return;
34392
34661
  dispose(v);
34393
34662
  views.delete(partId);
34663
+ if (v.saved) dropped.push(v);
34394
34664
  },
34395
34665
  /** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
34396
34666
  setLod(level) {
34667
+ flush(Infinity);
34397
34668
  const was = lod;
34398
34669
  lod = level;
34399
34670
  if (was === 0 === (level === 0)) return;
@@ -34412,21 +34683,27 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34412
34683
  if (lod === 0) plain2(v, true);
34413
34684
  }
34414
34685
  views.clear();
34686
+ for (const v of dropped) plain2(v, true);
34687
+ dropped.length = 0;
34415
34688
  },
34689
+ flush,
34416
34690
  /** The wall has damage drawn. */
34417
34691
  has(partId) {
34418
34692
  return views.has(partId);
34419
34693
  },
34420
- /** Walls with damage drawn, units out of them, and units laid bare at the edge of the cuts. */
34421
- statistics: () => ({
34694
+ /** 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. */
34695
+ statistics: () => (flush(Infinity), {
34422
34696
  walls: views.size,
34423
34697
  gone: [...views.values()].reduce((n, v) => n + v.gone, 0),
34698
+ craters: [...views.values()].reduce((n, v) => n + v.pits, 0),
34699
+ flakes: [...views.values()].reduce((n, v) => n + v.flakes, 0),
34424
34700
  units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
34425
34701
  })
34426
34702
  };
34427
34703
  }
34428
34704
  Object.assign(K, { createBreachView });
34429
34705
  }
34706
+ var FOLLOW_FLUSH_FALLBACK_MS = 100;
34430
34707
  function installDestructionWorld(K, shared) {
34431
34708
  const { THREE, cfg, invalidateShadow, invalidate, rngString, FRAGMENT_GEO, fragmentMaterial, createBreachView } = K;
34432
34709
  function createHouseDestruction(root, house, structure, parts, colliders, water) {
@@ -34435,7 +34712,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34435
34712
  const R = cfg.destructionRenderer || null, listeners = /* @__PURE__ */ new Set();
34436
34713
  D.renderer = R;
34437
34714
  D.spec = null;
34438
- const breach = createBreachView({ house, structure, spec: () => D.spec });
34715
+ const breach = createBreachView({ house, structure, spec: () => D.spec, deferPaint: true, ...cfg.breachPaintBudgetMs !== void 0 ? { paintBudgetMs: cfg.breachPaintBudgetMs } : {} });
34439
34716
  D.breach = breach;
34440
34717
  D.onEvent = (fn) => {
34441
34718
  listeners.add(fn);
@@ -34620,6 +34897,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34620
34897
  case "chip":
34621
34898
  breach.chip(e);
34622
34899
  break;
34900
+ case "spall":
34901
+ breach.spall(e);
34902
+ break;
34623
34903
  case "collapse":
34624
34904
  for (const id of e.parts) {
34625
34905
  breach.drop(id);
@@ -34661,7 +34941,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34661
34941
  D.specOptions = specOptions || null;
34662
34942
  D.spec = structure.spec(specOptions);
34663
34943
  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] });
34664
- if (R) R.bind(D.id, house, { parts: rangesOf() });
34944
+ breach.claim();
34945
+ if (R) R.bind(D.id, house, { parts: rangesOf(), onFrame: () => breach.flush() });
34665
34946
  off = D.world.on("*", handleEvent);
34666
34947
  D.active = true;
34667
34948
  return D;
@@ -34676,6 +34957,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34676
34957
  };
34677
34958
  D.follow = (world, id, spec) => {
34678
34959
  D.spec = spec;
34960
+ breach.claim();
34961
+ let fallback2 = null;
34679
34962
  const offFollow = world.on("*", (e) => {
34680
34963
  if (e.object !== id) return;
34681
34964
  switch (e.type) {
@@ -34685,6 +34968,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34685
34968
  case "chip":
34686
34969
  breach.chip(e);
34687
34970
  break;
34971
+ case "spall":
34972
+ breach.spall(e);
34973
+ break;
34688
34974
  case "fracture":
34689
34975
  breach.drop(e.part);
34690
34976
  break;
@@ -34694,10 +34980,16 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34694
34980
  default:
34695
34981
  break;
34696
34982
  }
34983
+ if (!fallback2 && typeof setTimeout === "function") fallback2 = setTimeout(() => {
34984
+ fallback2 = null;
34985
+ breach.flush(Infinity);
34986
+ }, FOLLOW_FLUSH_FALLBACK_MS);
34697
34987
  });
34698
34988
  return { restore: () => breach.restore(), dispose: () => {
34699
34989
  offFollow();
34700
34990
  breach.restore();
34991
+ }, update: () => {
34992
+ breach.flush();
34701
34993
  } };
34702
34994
  };
34703
34995
  D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
@@ -34730,6 +35022,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34730
35022
  return r;
34731
35023
  };
34732
35024
  D.animate = (dt) => {
35025
+ breach.flush();
34733
35026
  if (R) return;
34734
35027
  let moved = false;
34735
35028
  for (const [id, list2] of bodies) {
@@ -36752,6 +37045,54 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
36752
37045
  store.buffer ?? (store.buffer = bufferOf(context, [samples], context.sampleRate * seconds));
36753
37046
  return store.buffer;
36754
37047
  }
37048
+ var nowMs = () => typeof performance === "undefined" ? Date.now() : performance.now();
37049
+ function scheduleTask(task) {
37050
+ if (typeof MessageChannel === "undefined") {
37051
+ setTimeout(task, 0);
37052
+ return;
37053
+ }
37054
+ const channel = new MessageChannel();
37055
+ channel.port1.onmessage = () => {
37056
+ channel.port1.close();
37057
+ task();
37058
+ };
37059
+ channel.port2.postMessage(0);
37060
+ }
37061
+ function createStartQueue(options = {}) {
37062
+ const budgetMs = options.budgetMs ?? 2;
37063
+ const maxLatencyMs = options.maxLatencyMs ?? 250;
37064
+ let jobs = [];
37065
+ let scheduled = false;
37066
+ const drain = () => {
37067
+ scheduled = false;
37068
+ const began = nowMs();
37069
+ while (jobs.length > 0) {
37070
+ const job = jobs.shift();
37071
+ if (nowMs() - job.queuedAt > maxLatencyMs) job.drop();
37072
+ else job.run();
37073
+ if (nowMs() - began >= budgetMs) break;
37074
+ }
37075
+ if (jobs.length > 0) schedule();
37076
+ };
37077
+ const schedule = () => {
37078
+ if (scheduled) return;
37079
+ scheduled = true;
37080
+ scheduleTask(drain);
37081
+ };
37082
+ return {
37083
+ push(job) {
37084
+ jobs.push({ ...job, queuedAt: nowMs() });
37085
+ schedule();
37086
+ },
37087
+ clear() {
37088
+ for (const job of jobs) job.drop();
37089
+ jobs = [];
37090
+ },
37091
+ get size() {
37092
+ return jobs.length;
37093
+ }
37094
+ };
37095
+ }
36755
37096
  var end = (layer2, start) => start + layer2.duration + layer2.tail;
36756
37097
  var firstFilter = (layer2) => layer2.filters?.bank[0];
36757
37098
  var offsetOf = (layer2) => layer2.source.kind === "noise" ? layer2.source.offset : 0;
@@ -37192,15 +37533,43 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37192
37533
  ...playOptions.params ? { params: playOptions.params } : {},
37193
37534
  ...options.occlusion && playOptions.position && recipe.falloff?.model !== "none" ? { occlusion: options.occlusion(playOptions.position) } : {}
37194
37535
  });
37195
- const start = (plan, store, voiceId, delay = 0) => {
37536
+ const startQueue = options.deferredStart ? createStartQueue(options.deferredStart) : null;
37537
+ const build = (plan, store, delay) => {
37196
37538
  const audio = context;
37197
- const realized = realize(plan, {
37539
+ return realize(plan, {
37198
37540
  context: audio,
37199
37541
  output: outputFor(plan.bus),
37200
37542
  ...chain?.wet ? { wet: chain.wet } : {},
37201
37543
  noise: () => noiseBuffer(store, audio, mix3.noise.seconds),
37202
37544
  time: audio.currentTime + delay
37203
37545
  });
37546
+ };
37547
+ const deferVoice = (plan, store, voiceId, delay) => {
37548
+ let built = null;
37549
+ let cancelled = false;
37550
+ const pending = {
37551
+ get sources() {
37552
+ return built?.sources ?? [];
37553
+ },
37554
+ stop() {
37555
+ cancelled = true;
37556
+ built?.stop();
37557
+ }
37558
+ };
37559
+ startQueue.push({
37560
+ run() {
37561
+ if (!cancelled && context) built = build(plan, store, delay);
37562
+ },
37563
+ drop() {
37564
+ if (voiceId === void 0) return;
37565
+ voices.release(voiceId);
37566
+ active.delete(voiceId);
37567
+ }
37568
+ });
37569
+ return pending;
37570
+ };
37571
+ const start = (plan, store, voiceId, delay = 0) => {
37572
+ const realized = startQueue ? deferVoice(plan, store, voiceId, delay) : build(plan, store, delay);
37204
37573
  if (voiceId === void 0) return null;
37205
37574
  voices.setLength(voiceId, plan.length + delay);
37206
37575
  active.set(voiceId, realized);
@@ -37316,6 +37685,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37316
37685
  const audio = ready();
37317
37686
  if (audio) unlockContext(audio);
37318
37687
  },
37688
+ warmUp() {
37689
+ const audio = ready();
37690
+ if (!audio) return false;
37691
+ noiseBuffer(shared, audio, mix3.noise.seconds);
37692
+ return true;
37693
+ },
37319
37694
  level(bus) {
37320
37695
  watched.add(bus);
37321
37696
  ensureBus(bus);
@@ -37339,6 +37714,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37339
37714
  };
37340
37715
  },
37341
37716
  dispose() {
37717
+ startQueue?.clear();
37342
37718
  for (const id of active.keys()) voices.release(id);
37343
37719
  for (const loop of [...loops]) loop.stop();
37344
37720
  active.clear();
@@ -38574,6 +38950,83 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38574
38950
  };
38575
38951
  };
38576
38952
 
38953
+ // src/fx/body-hold.ts
38954
+ var BODY_HOLD = {
38955
+ /** With no body to lend the frame: the pocket of the right shoulder from the eye, in the eye's yaw frame (right, up, back). */
38956
+ eyeToPocket: [0.2, -0.275, 0.17],
38957
+ /** A shouldered gun's bore runs this far above the shoulder pocket (the cheek on the comb). */
38958
+ boreAbove: 0.05,
38959
+ /** A handgun's (or a short two-hand gun's) grip: toward the middle of the chest, a little low, at the reach of bent arms. */
38960
+ handgunGrip: [-0.15, -0.04, -0.42],
38961
+ /** A one-hand tool's grip: low in front of the body, the elbow bent. */
38962
+ toolGrip: [0.02, -0.34, -0.3],
38963
+ /** A tool at least this long, held by two hands, is shouldered (its stock in the pocket). */
38964
+ shoulderedFrom: 0.55,
38965
+ /** The farthest a shouldered gun's grip sits ahead of the pocket; a launcher whose grip is farther slides back, its tube behind the shoulder. */
38966
+ shoulderGripMax: 0.42,
38967
+ /** The nearest a shouldered gun's grip sits ahead of the pocket; a gun with no stock (a pistol grip at its rear) slides forward, its grip not behind the eye. */
38968
+ shoulderGripMin: 0.25,
38969
+ /** Where such a grip is brought to, ahead of the pocket. */
38970
+ shoulderGripTo: 0.34,
38971
+ /** The farthest apart two hands hold a long gun (a support hand past it slides back along the gun, toward the grip). */
38972
+ supportSpan: 0.5,
38973
+ /** How much of the aim's pitch a tool held low in one hand follows. */
38974
+ toolPitch: 0.5
38975
+ };
38976
+ var scratch = { a: null, b: null, euler: null };
38977
+ var placeOnBody = (placement) => {
38978
+ const { THREE, hand, holder, frame: frame2 } = placement;
38979
+ scratch.a ?? (scratch.a = new THREE.Vector3());
38980
+ scratch.b ?? (scratch.b = new THREE.Vector3());
38981
+ holder.scale.setScalar(1);
38982
+ hand.position.set(...frame2.position);
38983
+ hand.quaternion.set(...frame2.quaternion);
38984
+ const shouldered = !placement.handgun && placement.left !== null && placement.lengthMetres >= BODY_HOLD.shoulderedFrom;
38985
+ if (shouldered) {
38986
+ holder.position.set(0, BODY_HOLD.boreAbove, placement.rear);
38987
+ hand.updateMatrixWorld(true);
38988
+ if (!placement.right) return;
38989
+ const grip2 = hand.worldToLocal(placement.right.part.localToWorld(scratch.a.copy(placement.right.offset)));
38990
+ if (-grip2.z <= BODY_HOLD.shoulderGripMax && -grip2.z >= BODY_HOLD.shoulderGripMin) return;
38991
+ holder.position.z += -grip2.z - BODY_HOLD.shoulderGripTo;
38992
+ hand.updateMatrixWorld(true);
38993
+ return;
38994
+ }
38995
+ scratch.euler ?? (scratch.euler = new THREE.Euler());
38996
+ const turn2 = scratch.euler.setFromQuaternion(hand.quaternion, "YXZ");
38997
+ hand.quaternion.setFromEuler(turn2.set(turn2.x * BODY_HOLD.toolPitch, turn2.y, 0, "YXZ"));
38998
+ holder.position.set(0, 0, 0);
38999
+ hand.updateMatrixWorld(true);
39000
+ if (!placement.right) return;
39001
+ const grip = placement.handgun || placement.left ? BODY_HOLD.handgunGrip : BODY_HOLD.toolGrip;
39002
+ const want = scratch.a.set(grip[0], grip[1], grip[2]).applyQuaternion(hand.quaternion).add(hand.position);
39003
+ const now = placement.right.part.localToWorld(scratch.b.copy(placement.right.offset));
39004
+ hand.position.add(want.sub(now));
39005
+ hand.updateMatrixWorld(true);
39006
+ };
39007
+ var limitSupportSpan = (targets) => {
39008
+ const right = targets.R?.position;
39009
+ const left = targets.L?.position;
39010
+ if (!right || !left || !targets.L) return targets;
39011
+ const span = Math.hypot(left[0] - right[0], left[1] - right[1], left[2] - right[2]);
39012
+ if (span <= BODY_HOLD.supportSpan) return targets;
39013
+ const k = BODY_HOLD.supportSpan / span;
39014
+ const position = [right[0] + (left[0] - right[0]) * k, right[1] + (left[1] - right[1]) * k, right[2] + (left[2] - right[2]) * k];
39015
+ return { ...targets, L: { ...targets.L, position } };
39016
+ };
39017
+ var createEyeFrame = (THREE, camera2) => {
39018
+ const frame2 = { position: [0, 0, 0], quaternion: [0, 0, 0, 1] };
39019
+ const turn2 = new THREE.Euler(0, 0, 0, "YXZ");
39020
+ const pocket = new THREE.Vector3();
39021
+ return () => {
39022
+ turn2.setFromQuaternion(camera2.quaternion, "YXZ");
39023
+ pocket.set(...BODY_HOLD.eyeToPocket).applyEuler(turn2.set(0, turn2.y, 0)).add(camera2.position);
39024
+ frame2.position = [pocket.x, pocket.y, pocket.z];
39025
+ frame2.quaternion = [camera2.quaternion.x, camera2.quaternion.y, camera2.quaternion.z, camera2.quaternion.w];
39026
+ return frame2;
39027
+ };
39028
+ };
39029
+
38577
39030
  // src/fx/hand-hold.ts
38578
39031
  var HAND_HOLD = {
38579
39032
  /** Half the width of a pistol grip, the palm sits this far (plus the palm) off its middle. */
@@ -38602,9 +39055,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38602
39055
  /** How far the shooting hand slides on its grip so the thumb reaches the safety lever. */
38603
39056
  thumbReach: 0.02,
38604
39057
  /** How fast the supporting hand goes to the magazine and back, per second. */
38605
- magRate: 7,
38606
- /** The hands shrink with the tool: a tool drawn at scale `s` takes hands at `s ** handScaleExponent`. */
38607
- handScaleExponent: 0.55
39058
+ magRate: 7
38608
39059
  };
38609
39060
  var handFrame = (THREE, x, y) => {
38610
39061
  const ax = x.clone().normalize();
@@ -38694,7 +39145,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38694
39145
  right,
38695
39146
  left,
38696
39147
  leftMagazine,
38697
- scale: holderScale ** HAND_HOLD.handScaleExponent,
39148
+ // the hands at the tool's scale: a tool drawn smaller than the hands looks like a toy in them
39149
+ scale: holderScale,
38698
39150
  grip: HAND_HOLD.closed,
38699
39151
  gripLeft: HAND_HOLD.forend
38700
39152
  };
@@ -38709,7 +39161,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
38709
39161
  const long = head && ["smg", "carbine", "rifle", "sniper", "shotgun", "crossbow", "flame"].includes(category);
38710
39162
  const headSize = head ? size(head) : null;
38711
39163
  const left = long && head && headSize ? { part: group, offset: head.position.clone().add(V(THREE, -headSize.x * 0.15, -headSize.y / 2 - HAND_HOLD.palm, 0)), quaternion: forendFrame(THREE) } : null;
38712
- return { right, left, leftMagazine: null, scale: scale ** HAND_HOLD.handScaleExponent, grip: HAND_HOLD.closed, gripLeft: HAND_HOLD.forend };
39164
+ return { right, left, leftMagazine: null, scale, grip: HAND_HOLD.closed, gripLeft: HAND_HOLD.forend };
38713
39165
  }
38714
39166
 
38715
39167
  // ../game-fluids/dist/index.js
@@ -39847,6 +40299,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39847
40299
  liquid: "honey",
39848
40300
  fill: 0.9
39849
40301
  }
40302
+ },
40303
+ shapes: {
40304
+ box: {
40305
+ hollow: true,
40306
+ welded: true
40307
+ }
39850
40308
  }
39851
40309
  };
39852
40310
  var DESTRUCTION_MATERIAL_BY_TAG = {
@@ -39860,6 +40318,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39860
40318
  };
39861
40319
  var FAMILIES = destruction_default.families;
39862
40320
  var CONTENTS = destruction_default.contents;
40321
+ var SHAPES2 = destruction_default.shapes;
40322
+ var shapeDestruction = (shapeId) => SHAPES2[shapeId];
39863
40323
  var FAMILY_OF_MODEL = new Map(Object.entries(FAMILIES).flatMap(([family, entry2]) => entry2.models.map((model) => [model, family])));
39864
40324
  var itemDestruction = (modelId) => {
39865
40325
  const family = FAMILY_OF_MODEL.get(modelId);
@@ -39973,9 +40433,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39973
40433
  root.userData.tessellationTriangles = added;
39974
40434
  }
39975
40435
  function tagForDestruction(root, options = {}) {
39976
- const { modelId, weight, size, three } = options;
40436
+ const { modelId, shapeId, weight, size, powder, three } = options;
39977
40437
  if (weight !== void 0 && weight > 0) root.userData.destructionMass = weight;
39978
- const rule = modelId === void 0 ? void 0 : itemDestruction(modelId);
40438
+ const rule = modelId !== void 0 ? itemDestruction(modelId) : shapeId !== void 0 ? shapeDestruction(shapeId) : void 0;
39979
40439
  if (rule?.tessellate) tessellateShapes(root, rule.tessellate, three);
39980
40440
  const solid2 = [];
39981
40441
  root.traverse((object2) => {
@@ -39990,6 +40450,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39990
40450
  return;
39991
40451
  }
39992
40452
  if (rule?.hollow) mesh3.userData.hollow = true;
40453
+ if (powder) mesh3.userData.destructionPowder = { color: powder.color };
39993
40454
  solid2.push(mesh3);
39994
40455
  });
39995
40456
  if (!rule?.welded || solid2.length < 2) return;
@@ -42467,10 +42928,41 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42467
42928
  const m = metal2(9277590);
42468
42929
  return G(mesh3(cyl(0.32, 0.32, 0.52), m), mesh3(cyl(0.326, 0.326, 0.34, 14), food(10172964)), mesh3(cyl(0.33, 0.33, 0.06), paper2(14861162), [0, 0.02, 0]), mesh3(torus2(0.31, 0.025), m, [0, 0.26, 0], [Math.PI / 2, 0, 0]), mesh3(torus2(0.31, 0.025), m, [0, -0.26, 0], [Math.PI / 2, 0, 0]));
42469
42930
  };
42470
- var waterBottle = ({ mesh: mesh3, cyl, lathe: lathe2, G, plastic, paper: paper2, glass }) => G(mesh3(lathe2([[0, -0.6], [0.2, -0.6], [0.22, -0.5], [0.22, 0.2], [0.19, 0.32], [0.09, 0.45], [0.08, 0.55], [0, 0.55]]), glass(9421788, 0.55)), mesh3(cyl(0.085, 0.085, 0.1, 12), plastic(2907530), [0, 0.6, 0]), mesh3(cyl(0.225, 0.225, 0.22, 16), paper2(15330543), [0, -0.15, 0]), mesh3(cyl(0.226, 0.226, 0.05, 16), plastic(3831976), [0, -0.12, 0]));
42931
+ var opening_default = {
42932
+ families: {
42933
+ drink_can: { style: "pull_tab" },
42934
+ food_tin: { style: "cut_lid" }
42935
+ },
42936
+ models: {
42937
+ empty_can: { style: "none" },
42938
+ canned_beans: { food: "#7a2e1c" },
42939
+ canned_corn_and_peas: { food: "#c7b03c" },
42940
+ canned_sausages: { food: "#c98d5c" },
42941
+ canned_sardines: { style: "peel_lid", food: "#8e979c" },
42942
+ condensed_milk: { food: "#e3cf9c" },
42943
+ energy_drink: { style: "pull_tab" },
42944
+ honey_jar: { style: "screw_cap", food: "#d99a1c" },
42945
+ home_preserves: { style: "screw_cap", food: "#8e1f3a" },
42946
+ cooking_oil: { style: "screw_cap", food: "#e3b830" },
42947
+ water_bottle: { style: "screw_cap" },
42948
+ water_jug_5l: { style: "screw_cap" },
42949
+ dirty_water: { style: "screw_cap" },
42950
+ sports_drink: { style: "screw_cap" },
42951
+ coconut_water: { style: "screw_cap" },
42952
+ cachaca: { style: "screw_cap" },
42953
+ communion_wine: { style: "screw_cap" }
42954
+ }
42955
+ };
42956
+ var FAMILIES2 = opening_default.families;
42957
+ var MODELS = opening_default.models;
42958
+ var asLid = (mesh3) => {
42959
+ mesh3.userData.itemLid = true;
42960
+ return mesh3;
42961
+ };
42962
+ var waterBottle = ({ mesh: mesh3, cyl, lathe: lathe2, G, plastic, paper: paper2, glass }) => G(mesh3(lathe2([[0, -0.6], [0.2, -0.6], [0.22, -0.5], [0.22, 0.2], [0.19, 0.32], [0.09, 0.45], [0.08, 0.55], [0, 0.55]]), glass(9421788, 0.55)), asLid(mesh3(cyl(0.085, 0.085, 0.1, 12), plastic(2907530), [0, 0.6, 0])), mesh3(cyl(0.225, 0.225, 0.22, 16), paper2(15330543), [0, -0.15, 0]), mesh3(cyl(0.226, 0.226, 0.05, 16), plastic(3831976), [0, -0.12, 0]));
42471
42963
  var rice = ({ mesh: mesh3, bx, G, paper: paper2, food }) => G(mesh3(bx(0.7, 0.95, 0.28), paper2(15129792)), mesh3(bx(0.72, 0.12, 0.1), paper2(14010792), [0, 0.52, 0]), mesh3(bx(0.5, 0.3, 0.02), food(11883039), [0, 0.05, 0.15]), mesh3(bx(0.3, 0.1, 0.02), paper2(4160069), [0, -0.25, 0.15]));
42472
42964
  var militaryRation = ({ mesh: mesh3, bx, G, plastic, paper: paper2 }) => G(mesh3(bx(0.75, 1, 0.22), plastic(5923142)), mesh3(bx(0.77, 0.1, 0.23), plastic(4672823), [0, 0.48, 0]), mesh3(bx(0.5, 0.14, 0.01), paper2(2237727), [0, 0.18, 0.115]), mesh3(bx(0.36, 0.06, 0.01), paper2(2237727), [0, -0.02, 0.115]));
42473
- var cachaca = ({ mesh: mesh3, cyl, lathe: lathe2, G, wood, paper: paper2, food, glass }) => G(mesh3(lathe2([[0, -0.6], [0.25, -0.6], [0.27, -0.52], [0.27, 0.1], [0.2, 0.25], [0.09, 0.35], [0.09, 0.58], [0, 0.58]]), glass(12749354, 0.78)), mesh3(cyl(0.08, 0.07, 0.14, 10), wood(9071172), [0, 0.64, 0]), mesh3(cyl(0.275, 0.275, 0.34, 16), paper2(15327692), [0, -0.25, 0]), mesh3(cyl(0.276, 0.276, 0.06, 16), food(8007455), [0, -0.15, 0]));
42965
+ var cachaca = ({ mesh: mesh3, cyl, lathe: lathe2, G, wood, paper: paper2, food, glass }) => G(mesh3(lathe2([[0, -0.6], [0.25, -0.6], [0.27, -0.52], [0.27, 0.1], [0.2, 0.25], [0.09, 0.35], [0.09, 0.58], [0, 0.58]]), glass(12749354, 0.78)), asLid(mesh3(cyl(0.08, 0.07, 0.14, 10), wood(9071172), [0, 0.64, 0])), mesh3(cyl(0.275, 0.275, 0.34, 16), paper2(15327692), [0, -0.25, 0]), mesh3(cyl(0.276, 0.276, 0.06, 16), food(8007455), [0, -0.15, 0]));
42474
42966
  var firstAidKit = ({ mesh: mesh3, bx, torus: torus2, G, plastic }) => {
42475
42967
  const r = plastic(11680300);
42476
42968
  return G(mesh3(bx(1.05, 0.72, 0.38), plastic(15131608)), mesh3(bx(1.07, 0.06, 0.4), plastic(12433578), [0, 0, 0]), mesh3(bx(0.36, 0.1, 0.02), r, [0, 0.1, 0.2]), mesh3(bx(0.1, 0.36, 0.02), r, [0, 0.1, 0.2]), mesh3(torus2(0.16, 0.035, Math.PI), plastic(5591884), [0, 0.36, 0]));
@@ -42630,7 +43122,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42630
43122
  mesh3(cyl(0.41, 0.41, 0.86, 12), food(9314106), [0, -0.2, 0]),
42631
43123
  mesh3(cyl(0.34, 0.445, 0.12, 12), glass(13625070, 0.5), [0, 0.39, 0]),
42632
43124
  mesh3(cyl(0.34, 0.34, 0.05, 12), glass(13625070, 0.5), [0, 0.475, 0]),
42633
- mesh3(cyl(0.38, 0.38, 0.15, 12), metal2(12163644), [0, 0.575, 0]),
43125
+ asLid(mesh3(cyl(0.38, 0.38, 0.15, 12), metal2(12163644), [0, 0.575, 0])),
42634
43126
  mesh3(cyl(0.45, 0.45, 0.4, 12), paper2(15327172), [0, -0.2, 0]),
42635
43127
  mesh3(cyl(0.452, 0.452, 0.08, 12), cloth2(10172986), [0, -0.2, 0])
42636
43128
  );
@@ -42805,7 +43297,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42805
43297
  mesh3(cyl(0.395, 0.395, 0.68, 12), glass(14925936, 0.55), [0, -0.16, 0]),
42806
43298
  mesh3(cyl(0.37, 0.37, 0.6, 12), food(14260764), [0, -0.2, 0]),
42807
43299
  mesh3(cyl(0.3, 0.395, 0.14, 12), glass(14925936, 0.55), [0, 0.25, 0]),
42808
- mesh3(cyl(0.3, 0.3, 0.18, 12), metal2(7031324), [0, 0.41, 0]),
43300
+ asLid(mesh3(cyl(0.3, 0.3, 0.18, 12), metal2(7031324), [0, 0.41, 0])),
42809
43301
  mesh3(cyl(0.4, 0.4, 0.34, 12), paper2(15853512), [0, -0.2, 0]),
42810
43302
  mesh3(cyl(0.402, 0.402, 0.05, 12), paper2(9067028), [0, -0.1, 0]),
42811
43303
  mesh3(cyl(0.402, 0.402, 0.05, 12), paper2(9067028), [0, -0.3, 0])
@@ -42891,7 +43383,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42891
43383
  mesh3(cyl(0.345, 0.345, 1.7, 12), glass(14923824, 0.65), [0, -0.45, 0]),
42892
43384
  mesh3(cyl(0.12, 0.345, 0.5, 12), glass(14923824, 0.65), [0, 0.65, 0]),
42893
43385
  mesh3(cyl(0.12, 0.12, 0.25, 12), glass(14923824, 0.65), [0, 1.025, 0]),
42894
- mesh3(cyl(0.14, 0.14, 0.15, 12), plastic(4160042), [0, 1.225, 0]),
43386
+ asLid(mesh3(cyl(0.14, 0.14, 0.15, 12), plastic(4160042), [0, 1.225, 0])),
42895
43387
  mesh3(cyl(0.35, 0.35, 0.8, 12), paper2(15853512), [0, -0.5, 0]),
42896
43388
  mesh3(cyl(0.352, 0.352, 0.2, 12), paper2(4160042), [0, -0.5, 0])
42897
43389
  );
@@ -42923,7 +43415,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42923
43415
  const body = mesh3(bx(1.5, 2.3, 1.5), glass(10473710, 0.75), [0, -0.5, 0]);
42924
43416
  const shoulder = mesh3(bx(1.2, 0.45, 1.2), glass(10473710, 0.75), [0, 0.875, 0]);
42925
43417
  const neck = mesh3(cyl(0.34, 0.34, 0.3, 12), dark, [0, 1.25, 0]);
42926
- const cap = mesh3(cyl(0.4, 0.4, 0.25, 12), dark, [0, 1.525, 0]);
43418
+ const cap = asLid(mesh3(cyl(0.4, 0.4, 0.25, 12), dark, [0, 1.525, 0]));
42927
43419
  const grip = mesh3(bx(0.16, 0.9, 0.3), dark, [1.07, 0.6, 0]);
42928
43420
  const upperBar = mesh3(bx(0.45, 0.16, 0.3), dark, [0.925, 0.97, 0]);
42929
43421
  const lowerBar = mesh3(bx(0.45, 0.16, 0.3), dark, [0.925, 0.23, 0]);
@@ -42936,7 +43428,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42936
43428
  const label = mesh3(cyl(0.332, 0.332, 0.7, 12), paper2(1448738), [0, -0.12, 0]);
42937
43429
  const stripe = mesh3(cyl(0.332, 0.332, 0.1, 12), paper2(15753232), [0, 0.28, 0]);
42938
43430
  const collar = mesh3(cyl(0.2, 0.2, 0.04, 12), plastic(15263976), [0, 0.705, 0]);
42939
- const cap = mesh3(cyl(0.19, 0.19, 0.2, 12), plastic(15753232), [0, 0.815, 0]);
43431
+ const cap = asLid(mesh3(cyl(0.19, 0.19, 0.2, 12), plastic(15753232), [0, 0.815, 0]));
42940
43432
  return G(bottle2, label, stripe, collar, cap);
42941
43433
  };
42942
43434
  var milkCarton = ({ mesh: mesh3, bx, cyl, extrude: extrude2, G, paper: paper2, plastic }) => {
@@ -42975,7 +43467,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42975
43467
  const goldBand = mesh3(cyl(0.382, 0.382, 0.05, 12), metal2(11896880), [0, 0.075, 0]);
42976
43468
  const crossUpright = mesh3(bx(0.06, 0.26, 0.02), paper2(8003371), [0, -0.35, 0.375]);
42977
43469
  const crossArm = mesh3(bx(0.18, 0.06, 0.02), paper2(8003371), [0, -0.3, 0.375]);
42978
- const foil = mesh3(cyl(0.15, 0.15, 0.46, 12), metal2(11896880), [0, 1.27, 0]);
43470
+ const foil = asLid(mesh3(cyl(0.15, 0.15, 0.46, 12), metal2(11896880), [0, 1.27, 0]));
42979
43471
  return G(bottle2, label, goldBand, crossUpright, crossArm, foil);
42980
43472
  };
42981
43473
  var coconutWater = ({ mesh: mesh3, bx, cyl, lathe: lathe2, G, plastic, paper: paper2 }) => {
@@ -42984,7 +43476,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42984
43476
  const nut = mesh3(cyl(0.11, 0.11, 0.03, 10), paper2(7031339), [0, -0.12, 0.285], [Math.PI / 2, 0, 0]);
42985
43477
  const leafLeft = mesh3(bx(0.2, 0.05, 0.01), paper2(15135448), [-0.12, 0.08, 0.292], [0, 0, 0.5]);
42986
43478
  const leafRight = mesh3(bx(0.2, 0.05, 0.01), paper2(15135448), [0.12, 0.08, 0.292], [0, 0, -0.5]);
42987
- const cap = mesh3(cyl(0.14, 0.14, 0.2, 12), plastic(4165450), [0, 0.75, 0]);
43479
+ const cap = asLid(mesh3(cyl(0.14, 0.14, 0.2, 12), plastic(4165450), [0, 0.75, 0]));
42988
43480
  return G(bottle2, label, nut, leafLeft, leafRight, cap);
42989
43481
  };
42990
43482
  var dirtyWater = ({ mesh: mesh3, cyl, lathe: lathe2, G, plastic, paper: paper2, glass, food }) => {
@@ -42992,7 +43484,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42992
43484
  const murk = mesh3(lathe2([[0, -0.89], [0.27, -0.89], [0.315, -0.84], [0.315, 0.1], [0, 0.1]], 12), food(7035444));
42993
43485
  const sediment = mesh3(cyl(0.31, 0.27, 0.12, 8), food(4865314), [0, -0.82, 0]);
42994
43486
  const label = mesh3(cyl(0.332, 0.332, 0.22, 12), paper2(13617588), [0, 0.38, 0]);
42995
- const cap = mesh3(cyl(0.19, 0.19, 0.2, 8), plastic(5922920), [0, 0.815, 0]);
43487
+ const cap = asLid(mesh3(cyl(0.19, 0.19, 0.2, 8), plastic(5922920), [0, 0.815, 0]));
42996
43488
  return G(bottle2, murk, sediment, label, cap);
42997
43489
  };
42998
43490
  var painkillers = ({ mesh: mesh3, bx, cyl, G, paper: paper2, metal: metal2, glass }) => {
@@ -45608,7 +46100,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
45608
46100
  const materials = kit.materials.slice(first);
45609
46101
  if (options.wear) setWear(materials, options.wear);
45610
46102
  root.userData.wear = options.wear ?? 0;
45611
- if (options.destructible) tagForDestruction(root, { ...kind === "model" && item.model !== void 0 ? { modelId: item.model } : {}, ...item.weight === void 0 ? {} : { weight: item.weight }, size: item.size, three: T2 });
46103
+ if (options.destructible) tagForDestruction(root, { ...kind === "model" && item.model !== void 0 ? { modelId: item.model } : {}, ...shape === void 0 ? {} : { shapeId: shape }, ...item.weight === void 0 ? {} : { weight: item.weight }, size: item.size, ...item.powder ? { powder: item.powder } : {}, three: T2 });
45612
46104
  markKitOrigin(root, { owner: "game-items-three", kind: "item", id: item.id, ...kind === "shape" ? { standIn: true } : {} });
45613
46105
  return { root, kind, ...shape ? { shape } : {}, size: item.size, materials };
45614
46106
  }
@@ -48895,7 +49387,6 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
48895
49387
  var MAX_CASINGS = 120;
48896
49388
  var MAX_MAGAZINES = 12;
48897
49389
  var LIFETIME_S = 25;
48898
- var HAND_OFFSET = [0.2, -0.2, -0.42];
48899
49390
  var GRAVITY2 = 9.81;
48900
49391
  function createBoxFx(options) {
48901
49392
  const { THREE, scene, camera: camera2, arsenal, groundAt } = options;
@@ -48917,6 +49408,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
48917
49408
  let dip = 0;
48918
49409
  let reloading = false;
48919
49410
  const v = new THREE.Vector3();
49411
+ const v2 = new THREE.Vector3();
49412
+ const eyeFrame = createEyeFrame(THREE, camera2);
49413
+ let rear = 0;
49414
+ let lengthMetres = 0;
48920
49415
  const q = new THREE.Quaternion();
48921
49416
  const q2 = new THREE.Quaternion();
48922
49417
  const pointForward = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
@@ -48928,13 +49423,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
48928
49423
  if (!def || BODY_ACTIONS.has(def.id)) return;
48929
49424
  model = buildWeaponModel(THREE, def);
48930
49425
  const length2 = new THREE.Box3().setFromObject(model).getSize(v).x;
48931
- model.scale.setScalar(length2 > 0.8 ? 0.55 : length2 > 0.4 ? 0.75 : 1);
49426
+ rear = new THREE.Box3().setFromObject(model).min.x;
49427
+ lengthMetres = length2;
48932
49428
  model.quaternion.copy(pointForward);
48933
49429
  hand.add(model);
48934
49430
  inHand = def.id;
48935
- hold = holdOfProp(THREE, model, def.category, model.scale.x);
49431
+ hold = holdOfProp(THREE, model, def.category, 1);
49432
+ };
49433
+ const handPoint = (x, y, z, out) => {
49434
+ if (!model) return out.set(x, y, z).applyQuaternion(camera2.quaternion).add(camera2.position);
49435
+ return out.set(x, y, z).applyQuaternion(camera2.quaternion).add(model.getWorldPosition(v2));
48936
49436
  };
48937
- const handPoint = (x, y, z, out) => out.set(HAND_OFFSET[0] + x, HAND_OFFSET[1] + y, HAND_OFFSET[2] + z).applyQuaternion(camera2.quaternion).add(camera2.position);
48938
49437
  const throwBody = (kind, mesh3, from, velocity, radius) => {
48939
49438
  if (!(kind === "casing" ? layers.casings : layers.magazines)) {
48940
49439
  mesh3.geometry.dispose();
@@ -49045,10 +49544,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
49045
49544
  update(dt) {
49046
49545
  kick *= Math.exp(-dt * 14);
49047
49546
  dip += ((reloading ? 1 : 0) - dip) * Math.min(1, dt * 8);
49048
- if (inHand) {
49049
- handPoint(0, -0.12 * dip, 0.05 * kick, hand.position);
49547
+ if (inHand && model) {
49548
+ const handgun = def?.category === "pistol" || def?.category === "revolver";
49549
+ placeOnBody({ THREE, hand, holder: model, frame: options.bodyHold?.() ?? eyeFrame(), rear, lengthMetres, handgun, right: hold?.right ?? null, left: hold?.left ?? null });
49550
+ hand.position.add(v.set(0, -0.12 * dip, 0.05 * kick).applyQuaternion(camera2.quaternion));
49050
49551
  q.setFromEuler(new THREE.Euler(0.14 * kick - 0.25 * dip, 0, 0.6 * dip));
49051
- hand.quaternion.copy(camera2.quaternion).multiply(q);
49552
+ hand.quaternion.multiply(q);
49052
49553
  }
49053
49554
  for (const body of [...bodies]) {
49054
49555
  stepBody(body, dt);
@@ -57769,6 +58270,11 @@ void main(){
57769
58270
  var MAX_CONFORM_STEPS = 48;
57770
58271
  var MIN_CONFORM_STEPS = 16;
57771
58272
  var BLOOD_GROW_RATE = 1.2;
58273
+ var BLOOD_START_GROWTH = 0.05;
58274
+ var DEFAULT_MAX_GROUND_MARKS = 64;
58275
+ var DEFAULT_MAX_ROCKS = 512;
58276
+ var DEFAULT_MAX_BLOOD = 256;
58277
+ var MAX_SPARE_GROUND_GEOMETRIES = 16;
57772
58278
  function createMarkLayer(THREE, options) {
57773
58279
  const group = new THREE.Group();
57774
58280
  const textures = createMarkTextures(THREE, options.createCanvas);
@@ -57792,110 +58298,168 @@ void main(){
57792
58298
  const bloodMaterial = new THREE.MeshStandardMaterial({ color: 6031882, roughness: 0.15, metalness: 0.1, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
57793
58299
  const bloodGeometry = new THREE.CircleGeometry(0.5, 9).rotateX(-Math.PI / 2);
57794
58300
  owned.push(bloodMaterial, bloodGeometry);
57795
- const rockMaterials = /* @__PURE__ */ new Map();
57796
- const blood = [];
57797
- let meshCount = 0;
57798
- function rockMaterial(color2) {
57799
- let material = rockMaterials.get(color2);
57800
- if (!material) {
57801
- material = new THREE.MeshStandardMaterial({ color: color2, roughness: 0.9, metalness: 0, flatShading: true });
57802
- rockMaterials.set(color2, material);
57803
- owned.push(material);
58301
+ const maxGroundMarks = options.maxGroundMarks ?? DEFAULT_MAX_GROUND_MARKS;
58302
+ const instanced = {};
58303
+ function rockMaterial() {
58304
+ if (!instanced.rockMaterial) {
58305
+ instanced.rockMaterial = new THREE.MeshStandardMaterial({ color: 16777215, roughness: 0.9, metalness: 0, flatShading: true });
58306
+ owned.push(instanced.rockMaterial);
57804
58307
  }
57805
- return material;
58308
+ return instanced.rockMaterial;
58309
+ }
58310
+ function bloodMesh() {
58311
+ return instanced.blood ?? (instanced.blood = attach(createInstanced(bloodGeometry, bloodMaterial, options.maxBlood ?? DEFAULT_MAX_BLOOD)));
58312
+ }
58313
+ function rockMesh() {
58314
+ return instanced.rock ?? (instanced.rock = attach(createInstanced(options.rockGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
58315
+ }
58316
+ function plateMesh() {
58317
+ return instanced.plate ?? (instanced.plate = attach(createInstanced(options.plateGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
58318
+ }
58319
+ function attach(mesh3, castShadow = false) {
58320
+ mesh3.castShadow = castShadow;
58321
+ group.add(mesh3);
58322
+ return mesh3;
58323
+ }
58324
+ const groundMarks = [];
58325
+ const spareGeometries = [];
58326
+ const blood = [];
58327
+ const placed = { blood: 0, rock: 0, plate: 0 };
58328
+ const matrix = new THREE.Matrix4();
58329
+ const position = new THREE.Vector3();
58330
+ const quaternion2 = new THREE.Quaternion();
58331
+ const euler = new THREE.Euler();
58332
+ const scale = new THREE.Vector3();
58333
+ const color2 = new THREE.Color();
58334
+ const up = new THREE.Vector3(0, 1, 0);
58335
+ function createInstanced(geometry, material, capacity) {
58336
+ const mesh3 = new THREE.InstancedMesh(geometry, material, capacity);
58337
+ mesh3.count = 0;
58338
+ mesh3.frustumCulled = false;
58339
+ mesh3.receiveShadow = true;
58340
+ return mesh3;
58341
+ }
58342
+ function takeSpare(vertexCount) {
58343
+ const found = spareGeometries.findIndex((geometry) => geometry.getAttribute("position").count === vertexCount);
58344
+ return found < 0 ? void 0 : spareGeometries.splice(found, 1)[0];
58345
+ }
58346
+ function evictOldestGroundMark() {
58347
+ const oldest = groundMarks.shift();
58348
+ group.remove(oldest.mesh);
58349
+ spareGeometries.push(oldest.mesh.geometry);
58350
+ if (spareGeometries.length > MAX_SPARE_GROUND_GEOMETRIES) spareGeometries.shift().dispose();
57806
58351
  }
57807
58352
  function conform(mark, material) {
57808
58353
  const radius = mark.radius;
57809
58354
  const steps = Math.max(MIN_CONFORM_STEPS, Math.min(MAX_CONFORM_STEPS, Math.round(radius * 8)));
57810
- const geometry = new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
57811
- const position = geometry.attributes.position;
58355
+ if (groundMarks.length >= maxGroundMarks) evictOldestGroundMark();
58356
+ const geometry = takeSpare((steps + 1) * (steps + 1)) ?? new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
58357
+ const grid = geometry.attributes.position;
58358
+ const cell = 2 / steps;
57812
58359
  const cos2 = Math.cos(mark.rotation);
57813
58360
  const sin2 = Math.sin(mark.rotation);
57814
- for (let index = 0; index < position.count; index++) {
57815
- const localX = position.getX(index) * radius;
57816
- const localZ = position.getZ(index) * radius;
58361
+ for (let index = 0; index < grid.count; index++) {
58362
+ const localX = Math.fround(index % (steps + 1) * cell - 1) * radius;
58363
+ const localZ = Math.fround(Math.floor(index / (steps + 1)) * cell - 1) * radius;
57817
58364
  const worldX = mark.x + localX * cos2 - localZ * sin2;
57818
58365
  const worldZ = mark.z + localX * sin2 + localZ * cos2;
57819
- position.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
58366
+ grid.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
57820
58367
  }
58368
+ grid.needsUpdate = true;
57821
58369
  geometry.computeVertexNormals();
58370
+ geometry.attributes.normal.needsUpdate = true;
58371
+ geometry.boundingSphere = null;
58372
+ geometry.boundingBox = null;
57822
58373
  const mesh3 = new THREE.Mesh(geometry, material);
57823
58374
  mesh3.receiveShadow = true;
57824
- return mesh3;
58375
+ group.add(mesh3);
58376
+ groundMarks.push({ mesh: mesh3, steps });
57825
58377
  }
57826
58378
  function addRock(mark) {
57827
- const mesh3 = new THREE.Mesh(mark.plate ? options.plateGeometry : options.rockGeometry, rockMaterial(mark.color));
57828
- mesh3.position.set(mark.x, mark.y, mark.z);
57829
- mesh3.scale.set(mark.scale[0], mark.scale[1], mark.scale[2]);
57830
- mesh3.rotation.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]);
57831
- mesh3.castShadow = true;
57832
- mesh3.receiveShadow = true;
57833
- return mesh3;
58379
+ const mesh3 = mark.plate ? plateMesh() : rockMesh();
58380
+ const slot = mark.plate ? "plate" : "rock";
58381
+ const index = placed[slot]++ % mesh3.instanceMatrix.count;
58382
+ quaternion2.setFromEuler(euler.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]));
58383
+ mesh3.setMatrixAt(index, matrix.compose(position.set(mark.x, mark.y, mark.z), quaternion2, scale.set(mark.scale[0], mark.scale[1], mark.scale[2])));
58384
+ mesh3.setColorAt(index, color2.set(mark.color));
58385
+ mesh3.count = Math.min(placed[slot], mesh3.instanceMatrix.count);
58386
+ mesh3.instanceMatrix.needsUpdate = true;
58387
+ mesh3.instanceColor.needsUpdate = true;
58388
+ }
58389
+ function writeBlood(index, drawn, growth) {
58390
+ quaternion2.setFromAxisAngle(up, drawn.rotation);
58391
+ const mesh3 = bloodMesh();
58392
+ mesh3.setMatrixAt(index, matrix.compose(position.set(drawn.position[0], drawn.position[1], drawn.position[2]), quaternion2, scale.set(drawn.scaleX * growth, 1, drawn.scaleZ * growth)));
58393
+ mesh3.instanceMatrix.needsUpdate = true;
57834
58394
  }
57835
58395
  function addBlood(mark) {
57836
- const mesh3 = new THREE.Mesh(bloodGeometry, bloodMaterial);
57837
- mesh3.position.set(mark.x, mark.y, mark.z);
57838
- mesh3.scale.set(mark.scaleX, 1, mark.scaleZ);
57839
- mesh3.rotation.y = mark.rotation;
57840
- mesh3.receiveShadow = true;
57841
- mesh3.scale.multiplyScalar(0.05);
57842
- blood.push({ mesh: mesh3, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 });
57843
- return mesh3;
58396
+ const index = placed.blood++ % bloodMesh().instanceMatrix.count;
58397
+ const drawn = { position: [mark.x, mark.y, mark.z], rotation: mark.rotation, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 };
58398
+ blood[index] = drawn;
58399
+ writeBlood(index, drawn, BLOOD_START_GROWTH);
58400
+ bloodMesh().count = Math.min(placed.blood, bloodMesh().instanceMatrix.count);
57844
58401
  }
57845
58402
  function build(mark) {
57846
- if (mark.kind === "scorch") return conform(mark, scorchMaterials[mark.variant]);
57847
- if (mark.kind === "dirt") return conform(mark, dirtMaterial);
57848
- if (mark.kind === "rock") return addRock(mark);
57849
- return addBlood(mark);
57850
- }
57851
- function clearMeshes() {
57852
- for (const child of [...group.children]) {
57853
- group.remove(child);
57854
- const geometry = child.geometry;
57855
- if (geometry !== options.rockGeometry && geometry !== options.plateGeometry && geometry !== bloodGeometry) geometry.dispose();
57856
- }
58403
+ if (mark.kind === "scorch") conform(mark, scorchMaterials[mark.variant]);
58404
+ else if (mark.kind === "dirt") conform(mark, dirtMaterial);
58405
+ else if (mark.kind === "rock") addRock(mark);
58406
+ else addBlood(mark);
58407
+ }
58408
+ function clearMarks() {
58409
+ for (const drawn of groundMarks) group.remove(drawn.mesh);
58410
+ for (const drawn of groundMarks) drawn.mesh.geometry.dispose();
58411
+ groundMarks.length = 0;
58412
+ for (const geometry of spareGeometries) geometry.dispose();
58413
+ spareGeometries.length = 0;
57857
58414
  blood.length = 0;
57858
- meshCount = 0;
58415
+ placed.blood = placed.rock = placed.plate = 0;
58416
+ for (const mesh3 of [instanced.blood, instanced.rock, instanced.plate]) if (mesh3) mesh3.count = 0;
57859
58417
  }
57860
58418
  return {
57861
58419
  group,
57862
58420
  handle(event) {
57863
- if (event.type === "clear") {
57864
- clearMeshes();
57865
- return;
57866
- }
57867
- group.add(build(event.mark));
57868
- meshCount++;
58421
+ if (event.type === "clear") clearMarks();
58422
+ else build(event.mark);
58423
+ },
58424
+ prepare() {
58425
+ bloodMesh();
58426
+ rockMesh();
58427
+ plateMesh();
57869
58428
  },
57870
58429
  update(dt) {
57871
- for (const drawn of blood) {
57872
- if (drawn.age > 1) continue;
58430
+ for (let index = 0; index < blood.length; index++) {
58431
+ const drawn = blood[index];
58432
+ if (!drawn || drawn.age > 1) continue;
57873
58433
  drawn.age += dt * BLOOD_GROW_RATE;
57874
- const ease3 = 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5);
57875
- drawn.mesh.scale.set(drawn.scaleX * Math.max(0.05, ease3), 1, drawn.scaleZ * Math.max(0.05, ease3));
58434
+ writeBlood(index, drawn, Math.max(BLOOD_START_GROWTH, 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5)));
57876
58435
  }
57877
58436
  },
57878
- count: () => meshCount,
58437
+ count: () => groundMarks.length + [instanced.blood, instanced.rock, instanced.plate].filter((mesh3) => mesh3 && mesh3.count > 0).length,
57879
58438
  dispose() {
57880
- clearMeshes();
58439
+ clearMarks();
58440
+ for (const mesh3 of [instanced.blood, instanced.rock, instanced.plate]) mesh3?.dispose();
57881
58441
  for (const item of owned) item.dispose();
57882
58442
  }
57883
58443
  };
57884
58444
  }
57885
58445
  var SPHERE_DEFAULT_WIDTH = 8;
57886
58446
  var SPHERE_DEFAULT_HEIGHT = 6;
58447
+ var MAX_PARKED_MESHES_PER_KIND = 256;
57887
58448
  function geometryOf(THREE, particle) {
57888
58449
  switch (particle.shape) {
57889
58450
  case "cone":
57890
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, false);
58451
+ return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, false);
57891
58452
  case "cone_open":
57892
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, true);
58453
+ return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, true);
57893
58454
  case "sphere":
57894
- return new THREE.SphereGeometry(particle.radius, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
58455
+ return new THREE.SphereGeometry(1, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
57895
58456
  case "icosahedron":
57896
- return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
58457
+ return new THREE.IcosahedronGeometry(1, particle.segments);
57897
58458
  }
57898
58459
  }
58460
+ function isCone(particle) {
58461
+ return particle.shape === "cone" || particle.shape === "cone_open";
58462
+ }
57899
58463
  function materialOf(THREE, particle) {
57900
58464
  if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
57901
58465
  return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
@@ -57906,51 +58470,82 @@ void main(){
57906
58470
  (kind) => new THREE.Mesh(geometry, materialOf(THREE, { ...kind, color: 8421504, opacity: 0.5 }))
57907
58471
  );
57908
58472
  }
58473
+ function geometryKeyOf(particle) {
58474
+ return `${particle.shape}:${particle.segments}:${particle.segments2}`;
58475
+ }
57909
58476
  function createMeshLayer(THREE, pool) {
57910
58477
  const group = new THREE.Group();
57911
58478
  group.name = "workshop-meshes";
57912
58479
  const drawn = /* @__PURE__ */ new Map();
58480
+ const geometries = /* @__PURE__ */ new Map();
58481
+ const parked = /* @__PURE__ */ new Map();
57913
58482
  const frame2 = new THREE.Quaternion();
57914
58483
  const euler = new THREE.Euler();
57915
58484
  const turn2 = new THREE.Quaternion();
57916
58485
  const offset = new THREE.Vector3();
57917
- function release(mesh3) {
57918
- mesh3.removeFromParent();
57919
- mesh3.geometry.dispose();
57920
- mesh3.material.dispose();
58486
+ let stamp = 0;
58487
+ function geometryFor(particle) {
58488
+ const key2 = geometryKeyOf(particle);
58489
+ let geometry = geometries.get(key2);
58490
+ if (!geometry) {
58491
+ geometry = geometryOf(THREE, particle);
58492
+ geometries.set(key2, geometry);
58493
+ }
58494
+ return geometry;
58495
+ }
58496
+ function acquire(particle) {
58497
+ const parkingKey = `${geometryKeyOf(particle)}|${particle.lit}|${particle.additive}`;
58498
+ const mesh3 = parked.get(parkingKey)?.pop() ?? new THREE.Mesh(geometryFor(particle), materialOf(THREE, particle));
58499
+ mesh3.material.color.set(particle.color);
58500
+ mesh3.frustumCulled = false;
58501
+ mesh3.renderOrder = particle.additive ? 5 : 3;
58502
+ group.add(mesh3);
58503
+ return { mesh: mesh3, parkingKey, stamp };
58504
+ }
58505
+ function park(entry2) {
58506
+ entry2.mesh.removeFromParent();
58507
+ let free = parked.get(entry2.parkingKey);
58508
+ if (!free) {
58509
+ free = [];
58510
+ parked.set(entry2.parkingKey, free);
58511
+ }
58512
+ if (free.length < MAX_PARKED_MESHES_PER_KIND) free.push(entry2.mesh);
58513
+ else entry2.mesh.material.dispose();
57921
58514
  }
57922
58515
  return {
57923
58516
  group,
57924
58517
  sync() {
57925
- const live = /* @__PURE__ */ new Set();
58518
+ stamp++;
57926
58519
  for (const particle of pool.particles) {
57927
- live.add(particle.id);
57928
- let mesh3 = drawn.get(particle.id);
57929
- if (!mesh3) {
57930
- mesh3 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
57931
- mesh3.frustumCulled = false;
57932
- mesh3.renderOrder = particle.additive ? 5 : 3;
57933
- drawn.set(particle.id, mesh3);
57934
- group.add(mesh3);
58520
+ let entry2 = drawn.get(particle.id);
58521
+ if (!entry2) {
58522
+ entry2 = acquire(particle);
58523
+ drawn.set(particle.id, entry2);
57935
58524
  }
58525
+ entry2.stamp = stamp;
58526
+ const mesh3 = entry2.mesh;
57936
58527
  frame2.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
57937
58528
  offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame2);
57938
58529
  mesh3.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
57939
58530
  turn2.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
57940
58531
  mesh3.quaternion.copy(frame2).multiply(turn2);
57941
- mesh3.scale.setScalar(particle.scale);
58532
+ mesh3.scale.set(particle.radius * particle.scale, (isCone(particle) ? particle.height : particle.radius) * particle.scale, particle.radius * particle.scale);
57942
58533
  mesh3.material.opacity = particle.opacity;
57943
58534
  }
57944
- for (const [id, mesh3] of drawn) {
57945
- if (live.has(id)) continue;
57946
- release(mesh3);
58535
+ for (const [id, entry2] of drawn) {
58536
+ if (entry2.stamp === stamp) continue;
58537
+ park(entry2);
57947
58538
  drawn.delete(id);
57948
58539
  }
57949
58540
  return pool.count;
57950
58541
  },
57951
58542
  dispose() {
57952
- for (const mesh3 of drawn.values()) release(mesh3);
58543
+ for (const entry2 of drawn.values()) park(entry2);
57953
58544
  drawn.clear();
58545
+ for (const free of parked.values()) for (const mesh3 of free) mesh3.material.dispose();
58546
+ parked.clear();
58547
+ for (const geometry of geometries.values()) geometry.dispose();
58548
+ geometries.clear();
57954
58549
  group.removeFromParent();
57955
58550
  }
57956
58551
  };
@@ -58518,7 +59113,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58518
59113
  const shocks = createShockLayer(THREE, pools.shocks);
58519
59114
  const sprites = createSpriteLayer(THREE, world.sprites(), options.pointScale ?? 700);
58520
59115
  const lights = createLightLayer(THREE, pools.lights, options.lightScale ?? Math.PI);
58521
- const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry });
59116
+ const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry, ...options.markLimits });
58522
59117
  root.add(puffs.mesh, sparks.mesh, glows.mesh, chunks.rocks, chunks.plates, shocks.group, lights.group, sprites.additive, sprites.normal, marks.group);
58523
59118
  const stopMarks = world.onMarks((event) => marks.handle(event));
58524
59119
  const overlay = options.overlay ? createScreenOverlay(options.overlay) : void 0;
@@ -58544,6 +59139,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58544
59139
  const position = options.camera.position;
58545
59140
  world.setCamera([position.x, position.y, position.z]);
58546
59141
  }
59142
+ function attachWorkshopLayers() {
59143
+ const engine = world.peekWorkshop();
59144
+ if (!engine || engine === workshopEngine) return;
59145
+ workshopLayers?.dispose();
59146
+ if (workshopLayers) root.remove(workshopLayers.group);
59147
+ workshopEngine = engine;
59148
+ workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
59149
+ root.add(workshopLayers.group);
59150
+ applyLayers();
59151
+ }
58547
59152
  function syncAll() {
58548
59153
  lastDrawn.puffs = puffs.sync();
58549
59154
  lastDrawn.sparks = sparks.sync();
@@ -58552,15 +59157,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58552
59157
  lastDrawn.shocks = shocks.sync();
58553
59158
  lastDrawn.sprites = sprites.sync();
58554
59159
  lights.sync();
58555
- const engine = world.peekWorkshop();
58556
- if (engine && engine !== workshopEngine) {
58557
- workshopLayers?.dispose();
58558
- if (workshopLayers) root.remove(workshopLayers.group);
58559
- workshopEngine = engine;
58560
- workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
58561
- root.add(workshopLayers.group);
58562
- applyLayers();
58563
- }
59160
+ attachWorkshopLayers();
58564
59161
  const workshopCalls = workshopLayers?.sync() ?? 0;
58565
59162
  const glow = pools.lights.glow;
58566
59163
  uniforms.uGlowPos.value.set(glow.x, glow.y, glow.z);
@@ -58613,6 +59210,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58613
59210
  },
58614
59211
  warmUpProbes() {
58615
59212
  releaseAnchors();
59213
+ marks.prepare();
59214
+ world.workshop();
59215
+ attachWorkshopLayers();
58616
59216
  const meshes = createMeshLayerProbes(THREE);
58617
59217
  for (const mesh3 of meshes) mesh3.frustumCulled = false;
58618
59218
  const group = new THREE.Group();
@@ -58679,6 +59279,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58679
59279
 
58680
59280
  // src/fx/heavy.ts
58681
59281
  var PI14 = Math.PI;
59282
+ var ROCKET_LIGHT_NAME = "rocket-light";
59283
+ var rocketsInFlight = /* @__PURE__ */ new WeakMap();
59284
+ function createRocketLight(THREE) {
59285
+ const light = new THREE.PointLight(16751168, 0, 2.5);
59286
+ light.name = ROCKET_LIGHT_NAME;
59287
+ light.position.set(0, -50, 0);
59288
+ return light;
59289
+ }
58682
59290
  var flameMaterials = (kit) => {
58683
59291
  const { THREE } = kit;
58684
59292
  return [
@@ -58708,8 +59316,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58708
59316
  exhaust.rotation.z = PI14 / 2;
58709
59317
  exhaust.position.x = -0.08;
58710
59318
  rocket.add(exhaust);
58711
- const light = new THREE.PointLight(16751168, 3, 2.5);
58712
- rocket.add(light);
59319
+ const light = context.rocketLight;
59320
+ light.intensity = 3;
59321
+ rocketsInFlight.set(light, (rocketsInFlight.get(light) ?? 0) + 1);
58713
59322
  context.every(
58714
59323
  1.6,
58715
59324
  (progress, dt) => {
@@ -58717,6 +59326,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58717
59326
  const distance3 = 1.2 * flight + (flight > 0.25 ? 9 * (flight - 0.25) ** 2 : 0);
58718
59327
  rocket.position.copy(muzzle2).addScaledVector(forward, distance3);
58719
59328
  rocket.rotation.x += 0.25 * dt * 60;
59329
+ light.position.copy(rocket.position);
58720
59330
  exhaust.scale.set(1, 0.6 + random() * 0.8, 1);
58721
59331
  if (random() < 0.9) context.effects.smoke(rocket.position.clone().addScaledVector(forward, -0.12), new THREE.Vector3((random() - 0.5) * 0.1, 0.05, (random() - 0.5) * 0.1), 0.02 + random() * 0.015, 2.2, 0.5);
58722
59332
  },
@@ -58724,7 +59334,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58724
59334
  rocket.removeFromParent();
58725
59335
  exhaust.geometry.dispose();
58726
59336
  exhaust.material.dispose();
58727
- light.dispose();
59337
+ const remaining = (rocketsInFlight.get(light) ?? 1) - 1;
59338
+ rocketsInFlight.set(light, remaining);
59339
+ if (remaining > 0) return;
59340
+ light.intensity = 0;
59341
+ light.position.set(0, -50, 0);
58728
59342
  }
58729
59343
  );
58730
59344
  }
@@ -59539,11 +60153,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59539
60153
 
59540
60154
  // src/fx/viewmodel.ts
59541
60155
  var PI16 = Math.PI;
59542
- var HAND_OFFSET2 = [0.2, -0.2, -0.42];
59543
- var HANDGUN_OFFSET = [0.1, -0.12, -0.44];
59544
60156
  var HEAT_DISPLAY_MAX_FALLBACK = 120;
59545
- var WALL_PULL = { margin: 0.03, max: 0.7, rate: 14, probe: 3 };
59546
- var HAND_IN_VIEW = { floor: -0.88, max: 0.18, rate: 10 };
60157
+ var WALL_PULL = { margin: 0.03, max: 1.2, rate: 14, probe: 3 };
60158
+ var HAND_IN_VIEW = { floor: -0.88, max: 0.3, rate: 10 };
59547
60159
  var LAUNCH_VOLUME = 0.6;
59548
60160
  var fittedOf2 = (parts) => parts.map((part) => ({ id: part.id, mount: part.mount, ...part.integral ? { integral: true } : {}, ...part.on === void 0 ? {} : { on: part.on } }));
59549
60161
  var collapsedStockPose = (move2) => {
@@ -59573,6 +60185,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59573
60185
  const hand = new THREE.Group();
59574
60186
  hand.name = VIEWMODEL_NODE_NAME;
59575
60187
  scene.add(hand);
60188
+ const rocketLight = createRocketLight(THREE);
60189
+ scene.add(rocketLight);
59576
60190
  const holder = new THREE.Group();
59577
60191
  holder.quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), PI16 / 2);
59578
60192
  hand.add(holder);
@@ -59629,7 +60243,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59629
60243
  positionOf: (part) => worldPositionOf(current, part, new THREE.Vector3()),
59630
60244
  orientation: () => current.model.root.getWorldQuaternion(new THREE.Quaternion()),
59631
60245
  every: (seconds, step, done) => void tasks.push({ delay: 0, seconds, elapsed: 0, step, done }),
59632
- groundAt
60246
+ groundAt,
60247
+ rocketLight
59633
60248
  });
59634
60249
  const equip = (weapon) => {
59635
60250
  const def = arsenal.weapons.get(weapon);
@@ -59642,14 +60257,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59642
60257
  reach = 0;
59643
60258
  pull = 0;
59644
60259
  lift = 0;
59645
- handHeight = 0;
59646
- handDepth = 0;
59647
60260
  rigStock(THREE, model, def);
59648
60261
  rigUnderbarrel(THREE, model, def);
59649
- const length2 = new THREE.Box3().setFromObject(model.root).getSize(new THREE.Vector3()).x;
59650
- const scale = length2 > 0.8 ? 0.55 : length2 > 0.4 ? 0.75 : 1;
59651
- holder.scale.setScalar(scale);
59652
- holder.position.set(0, 0, Math.min(0.25, 0.25 * length2 * scale));
60262
+ const modelBox = new THREE.Box3().setFromObject(model.root);
60263
+ const length2 = modelBox.getSize(new THREE.Vector3()).x;
60264
+ modelRear = modelBox.min.x;
59653
60265
  holder.add(model.root);
59654
60266
  const caliber = caliberOf(arsenal, def);
59655
60267
  const dims = caliber?.cartridge;
@@ -59661,7 +60273,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59661
60273
  def,
59662
60274
  caliber,
59663
60275
  model,
59664
- holderScale: scale,
60276
+ holderScale: 1,
59665
60277
  lengthMetres: length2,
59666
60278
  caseType: caliber?.caseType ?? "rifle",
59667
60279
  soundProfile: soundProfileOf(weapon, model.parts.family),
@@ -59691,7 +60303,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59691
60303
  applyCondition(kit, model, condition);
59692
60304
  lastAmmo = options.tool?.().ammo ?? 0;
59693
60305
  hand.updateMatrixWorld(true);
59694
- hold = holdOfModel(THREE, model, scale);
60306
+ hold = holdOfModel(THREE, model, 1);
59695
60307
  hold = { ...hold, works: handWorkOf(THREE, model, weapon, hold.right) };
59696
60308
  triggerRest = model.parts.trigger?.rotation.z ?? 0;
59697
60309
  lastShot = -Infinity;
@@ -59711,8 +60323,6 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59711
60323
  let reach = 0;
59712
60324
  let pull = 0;
59713
60325
  let lift = 0;
59714
- let handHeight = 0;
59715
- let handDepth = 0;
59716
60326
  const ahead = new THREE.Vector3();
59717
60327
  const up = new THREE.Vector3();
59718
60328
  const seen = new THREE.Vector3();
@@ -59725,23 +60335,35 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59725
60335
  for (const x of [bounds.min.x, bounds.max.x]) for (const y of [bounds.min.y, bounds.max.y]) for (const z of [bounds.min.z, bounds.max.z]) farthest = Math.max(farthest, corner.set(x, y, z).sub(camera2.position).dot(ahead));
59726
60336
  return farthest;
59727
60337
  };
60338
+ let modelRear = 0;
60339
+ const eyeFrame = createEyeFrame(THREE, camera2);
60340
+ const liftToKeepInView = () => {
60341
+ if (!hold || !session || !("fov" in camera2)) return 0;
60342
+ const tanHalf = Math.tan((camera2.fov ?? 70) * PI16 / 360);
60343
+ eyeTurn.copy(camera2.quaternion).invert();
60344
+ const rest2 = targetsOf(THREE, hold, session.model.root.getWorldQuaternion(rootQuaternion), 0);
60345
+ let needed = 0;
60346
+ for (const [anchor, pose] of [[hold.right, rest2.R], [hold.left, rest2.L]]) {
60347
+ if (!anchor || !pose || anchor.part !== session.model.root) continue;
60348
+ seen.set(...pose.position).sub(camera2.position).applyQuaternion(eyeTurn);
60349
+ if (-seen.z > 0.05) needed = Math.max(needed, HAND_IN_VIEW.floor * -seen.z * tanHalf - seen.y);
60350
+ }
60351
+ return Math.max(0, Math.min(HAND_IN_VIEW.max, needed));
60352
+ };
59728
60353
  const place = (dt = 0) => {
59729
- const family = session?.model.parts.family;
59730
- const [x, y, z] = family === "pistol" || family === "revolver" ? HANDGUN_OFFSET : HAND_OFFSET2;
59731
- hand.position.set(x, y, z).applyQuaternion(camera2.quaternion).add(camera2.position);
59732
- hand.quaternion.copy(camera2.quaternion);
59733
- if (session && handDepth > 0.05 && "fov" in camera2) {
59734
- const tanHalf = Math.tan((camera2.fov ?? 70) * PI16 / 360);
59735
- const want2 = Math.max(0, Math.min(HAND_IN_VIEW.max, lift + (HAND_IN_VIEW.floor - handHeight) * tanHalf * handDepth));
59736
- lift += (want2 - lift) * Math.min(1, dt * HAND_IN_VIEW.rate);
59737
- if (lift > 1e-4) hand.position.addScaledVector(up.set(0, 1, 0).applyQuaternion(camera2.quaternion), lift);
59738
- }
60354
+ if (!session) return;
60355
+ const family = session.model.parts.family;
60356
+ session.holderScale = 1;
60357
+ placeOnBody({ THREE, hand, holder, frame: options.bodyHold?.() ?? eyeFrame(), rear: modelRear, lengthMetres: session.lengthMetres, handgun: family === "pistol" || family === "revolver", right: hold?.right ?? null, left: hold?.left ?? null });
59739
60358
  hand.updateMatrixWorld(true);
59740
- if (!options.contact || !session) return;
60359
+ lift += (liftToKeepInView() - lift) * Math.min(1, dt * HAND_IN_VIEW.rate);
60360
+ if (lift > 1e-4) hand.position.addScaledVector(up.set(0, 1, 0).applyQuaternion(camera2.quaternion), lift);
60361
+ hand.updateMatrixWorld(true);
60362
+ if (!options.contact) return;
59741
60363
  ahead.set(0, 0, -1).applyQuaternion(camera2.quaternion);
59742
60364
  if (reach === 0) reach = Math.max(0.01, measureReach(session));
59743
60365
  const free = options.contact(camera2.position, ahead, WALL_PULL.probe);
59744
- const want = free === null ? 0 : Math.max(0, Math.min(WALL_PULL.max, reach + WALL_PULL.margin - free));
60366
+ const want = free === null ? 0 : Math.max(0, Math.min(Math.max(WALL_PULL.max, session.lengthMetres * session.holderScale), reach + WALL_PULL.margin - free));
59745
60367
  pull += (want - pull) * Math.min(1, dt * WALL_PULL.rate);
59746
60368
  if (pull < 1e-4) return;
59747
60369
  hand.position.addScaledVector(ahead, -pull);
@@ -59851,12 +60473,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59851
60473
  const targets = targetsOf(THREE, hold, session.model.root.getWorldQuaternion(rootQuaternion), magazineHand, hold.works ? workWeights(hold.works, player?.running ?? []) : void 0);
59852
60474
  const trigger = session.model.parts.trigger;
59853
60475
  if (trigger) targets.fingers = { index: indexCurl(trigger.rotation.z - triggerRest, clock - lastShot) };
59854
- if (targets.R && "fov" in camera2) {
59855
- seen.set(...targets.R.position).sub(camera2.position).applyQuaternion(eyeTurn.copy(camera2.quaternion).invert());
59856
- handDepth = -seen.z;
59857
- handHeight = handDepth > 0.05 ? seen.y / handDepth / Math.tan((camera2.fov ?? 70) * PI16 / 360) : 0;
59858
- }
59859
- return targets;
60476
+ return limitSupportSpan(targets);
59860
60477
  },
59861
60478
  get inHand() {
59862
60479
  return session?.id ?? null;
@@ -59875,6 +60492,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59875
60492
  particles.dispose();
59876
60493
  ownVfx?.dispose();
59877
60494
  hand.removeFromParent();
60495
+ rocketLight.removeFromParent();
60496
+ rocketLight.dispose();
59878
60497
  }
59879
60498
  };
59880
60499
  }
@@ -59899,6 +60518,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59899
60518
  ...options.attachmentModels ? { attachmentModels: options.attachmentModels } : {},
59900
60519
  ...options.raycast ? { raycast: options.raycast } : {},
59901
60520
  ...options.contact ? { contact: options.contact } : {},
60521
+ ...options.bodyHold ? { bodyHold: options.bodyHold } : {},
59902
60522
  ...options.effects ? { effects: options.effects } : {},
59903
60523
  ...options.flyRocket === void 0 ? {} : { flyRocket: options.flyRocket }
59904
60524
  });
@@ -60674,7 +61294,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
60674
61294
  var P13_VALVE_MESH = "valve";
60675
61295
  var P13_VALVE_COLOR = 12095802;
60676
61296
  var P13_WALL = 25e-4;
60677
- var SHAPES2 = {
61297
+ var SHAPES3 = {
60678
61298
  fuel_barrel: { shape: { kind: "cylinder", radius: 0.3, height: 0.9 }, position: [0, 0.45, 0], color: 3104666 },
60679
61299
  propane_p13: {
60680
61300
  shape: { kind: "cylinder", radius: 0.176, height: 0.49 },
@@ -60685,7 +61305,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
60685
61305
  },
60686
61306
  car_fuel_tank: { shape: { kind: "box", size: [0.9, 0.25, 0.5] }, position: [0, 0.4, 0], color: 4935509 }
60687
61307
  };
60688
- var WORLD_EXPLOSIVE_IDS = Object.keys(SHAPES2);
61308
+ var WORLD_EXPLOSIVE_IDS = Object.keys(SHAPES3);
60689
61309
  var SHELL = { thickness: 1e-3, fill: 0.03 };
60690
61310
  var explosivePart = (def, shape, scale) => ({
60691
61311
  id: "tank",
@@ -60731,7 +61351,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
60731
61351
  return [centre.x, centre.y, centre.z];
60732
61352
  };
60733
61353
  var worldExplosiveShell = (THREE, defId) => {
60734
- const shape = SHAPES2[defId];
61354
+ const shape = SHAPES3[defId];
60735
61355
  const model = shape ? modelOf(THREE, shape, 2) : void 0;
60736
61356
  if (!model) return void 0;
60737
61357
  model.traverse((node) => {
@@ -60744,7 +61364,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
60744
61364
  return model;
60745
61365
  };
60746
61366
  var worldExplosiveItem = (def, name, options = {}) => {
60747
- const shape = SHAPES2[def.id];
61367
+ const shape = SHAPES3[def.id];
60748
61368
  if (!shape) return void 0;
60749
61369
  const scale = () => options.scale?.() ?? 1;
60750
61370
  const spec = () => ({ name, parts: [explosivePart(def, shape, scale())], joints: [], tags: ["explosive"] });
@@ -61179,6 +61799,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
61179
61799
  raycast: createLaserRaycast(lent.THREE, lent.scene, lent.camera),
61180
61800
  // the weapon against a wall asks as often as the laser, on the clock of the simulation (so a fast-forwarded room measures as often per frame)
61181
61801
  contact: createLaserRaycast(lent.THREE, lent.scene, lent.camera, { now: () => simulatedMs }),
61802
+ ...lent.holdFrame ? { bodyHold: () => lent.holdFrame?.() ?? null } : {},
61182
61803
  ...lent.effects ? { effects: lent.effects } : {},
61183
61804
  ...options.attachmentModels ? { attachmentModels: options.attachmentModels } : {},
61184
61805
  ...options.condition ? { condition: options.condition } : {},
@@ -61991,7 +62612,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
61991
62612
  };
61992
62613
  for (const { kind, item } of PUSH_KINDS) PUSH_LAYOUT[kind].forEach(([distance3, degrees]) => lay(kind, item, bearing(centre, distance3, degrees)));
61993
62614
  if (mode === "grenade") {
61994
- lay("zombie", "zombie_walker", bearing(centre, 3.4, -34));
62615
+ lay("zombie", "zombie_walker", bearing(centre, 3.4, -130));
61995
62616
  lay("survivor", "person_police_officer", bearing(centre, 7.5, 78));
61996
62617
  lay("camp_table", "camp_table", bearing(centre, 3, 90));
61997
62618
  }
@@ -62034,7 +62655,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
62034
62655
  const outward = ((now[0] - from[0]) * (from[0] - blast.point[0]) + (now[2] - from[2]) * (from[2] - blast.point[2])) / away;
62035
62656
  return { ...body, distance: distance3, outward, moved: flat(now, from) };
62036
62657
  });
62037
- const torn = (row) => PUSH_KINDS.some((entry2) => entry2.kind === row.kind) && partsOf(row.objectId) < (partsBefore.get(row.objectId) ?? 0);
62658
+ const torn = (row) => (PUSH_KINDS.some((entry2) => entry2.kind === row.kind) || HEAVY_ITEMS.includes(row.kind)) && partsOf(row.objectId) < (partsBefore.get(row.objectId) ?? 0);
62038
62659
  const reached = rows.filter((row) => row.distance < blast.radius * 0.85 && !torn(row));
62039
62660
  const movedAway = reached.filter((row) => row.outward >= MOVED);
62040
62661
  const least = reached.length === 0 ? 0 : Math.min(...reached.map((row) => row.outward));
@@ -62047,9 +62668,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
62047
62668
  expect(near.outward > far.outward, fillText2(text.push_falloff, { what: things[kind] ?? kind, near: near.outward.toFixed(2), nearDistance: near.distance.toFixed(1), far: far.outward.toFixed(2), farDistance: far.distance.toFixed(1) }), { near: near.outward, far: far.outward });
62048
62669
  }
62049
62670
  for (const row of reached.filter((entry2) => entry2.kind === "zombie" || entry2.kind === "survivor")) {
62050
- expect(row.outward >= MOVED, fillText2(text.push_person, { who: things[row.kind] ?? row.kind, distance: row.distance.toFixed(1), moved: row.outward.toFixed(2) }), { row });
62671
+ expect(row.outward >= MOVED, fillText2(text.push_person, { who: things[row.kind] ?? row.kind, distance: row.distance.toFixed(1), moved: row.outward.toFixed(2) }), { row: { kind: row.kind, distance: row.distance, outward: row.outward } });
62051
62672
  }
62052
- const table = reached.find((row) => HEAVY_ITEMS.includes(row.kind));
62673
+ const table = rows.find((row) => HEAVY_ITEMS.includes(row.kind) && row.distance < blast.radius * 0.85);
62053
62674
  if (table) expect(table.moved >= MOVED, fillText2(text.push_heavy, { what: things[table.itemId] ?? table.itemId, moved: table.moved.toFixed(2) }), { table });
62054
62675
  }
62055
62676
  };