@adatechnology/game-weapons-three 0.10.4 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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. */
@@ -39847,6 +40300,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39847
40300
  liquid: "honey",
39848
40301
  fill: 0.9
39849
40302
  }
40303
+ },
40304
+ shapes: {
40305
+ box: {
40306
+ hollow: true,
40307
+ welded: true
40308
+ }
39850
40309
  }
39851
40310
  };
39852
40311
  var DESTRUCTION_MATERIAL_BY_TAG = {
@@ -39860,6 +40319,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39860
40319
  };
39861
40320
  var FAMILIES = destruction_default.families;
39862
40321
  var CONTENTS = destruction_default.contents;
40322
+ var SHAPES2 = destruction_default.shapes;
40323
+ var shapeDestruction = (shapeId) => SHAPES2[shapeId];
39863
40324
  var FAMILY_OF_MODEL = new Map(Object.entries(FAMILIES).flatMap(([family, entry2]) => entry2.models.map((model) => [model, family])));
39864
40325
  var itemDestruction = (modelId) => {
39865
40326
  const family = FAMILY_OF_MODEL.get(modelId);
@@ -39973,9 +40434,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39973
40434
  root.userData.tessellationTriangles = added;
39974
40435
  }
39975
40436
  function tagForDestruction(root, options = {}) {
39976
- const { modelId, weight, size, three } = options;
40437
+ const { modelId, shapeId, weight, size, powder, three } = options;
39977
40438
  if (weight !== void 0 && weight > 0) root.userData.destructionMass = weight;
39978
- const rule = modelId === void 0 ? void 0 : itemDestruction(modelId);
40439
+ const rule = modelId !== void 0 ? itemDestruction(modelId) : shapeId !== void 0 ? shapeDestruction(shapeId) : void 0;
39979
40440
  if (rule?.tessellate) tessellateShapes(root, rule.tessellate, three);
39980
40441
  const solid2 = [];
39981
40442
  root.traverse((object2) => {
@@ -39990,6 +40451,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39990
40451
  return;
39991
40452
  }
39992
40453
  if (rule?.hollow) mesh3.userData.hollow = true;
40454
+ if (powder) mesh3.userData.destructionPowder = { color: powder.color };
39993
40455
  solid2.push(mesh3);
39994
40456
  });
39995
40457
  if (!rule?.welded || solid2.length < 2) return;
@@ -42467,10 +42929,41 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42467
42929
  const m = metal2(9277590);
42468
42930
  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
42931
  };
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]));
42932
+ var opening_default = {
42933
+ families: {
42934
+ drink_can: { style: "pull_tab" },
42935
+ food_tin: { style: "cut_lid" }
42936
+ },
42937
+ models: {
42938
+ empty_can: { style: "none" },
42939
+ canned_beans: { food: "#7a2e1c" },
42940
+ canned_corn_and_peas: { food: "#c7b03c" },
42941
+ canned_sausages: { food: "#c98d5c" },
42942
+ canned_sardines: { style: "peel_lid", food: "#8e979c" },
42943
+ condensed_milk: { food: "#e3cf9c" },
42944
+ energy_drink: { style: "pull_tab" },
42945
+ honey_jar: { style: "screw_cap", food: "#d99a1c" },
42946
+ home_preserves: { style: "screw_cap", food: "#8e1f3a" },
42947
+ cooking_oil: { style: "screw_cap", food: "#e3b830" },
42948
+ water_bottle: { style: "screw_cap" },
42949
+ water_jug_5l: { style: "screw_cap" },
42950
+ dirty_water: { style: "screw_cap" },
42951
+ sports_drink: { style: "screw_cap" },
42952
+ coconut_water: { style: "screw_cap" },
42953
+ cachaca: { style: "screw_cap" },
42954
+ communion_wine: { style: "screw_cap" }
42955
+ }
42956
+ };
42957
+ var FAMILIES2 = opening_default.families;
42958
+ var MODELS = opening_default.models;
42959
+ var asLid = (mesh3) => {
42960
+ mesh3.userData.itemLid = true;
42961
+ return mesh3;
42962
+ };
42963
+ 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
42964
  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
42965
  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]));
42966
+ 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
42967
  var firstAidKit = ({ mesh: mesh3, bx, torus: torus2, G, plastic }) => {
42475
42968
  const r = plastic(11680300);
42476
42969
  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 +43123,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42630
43123
  mesh3(cyl(0.41, 0.41, 0.86, 12), food(9314106), [0, -0.2, 0]),
42631
43124
  mesh3(cyl(0.34, 0.445, 0.12, 12), glass(13625070, 0.5), [0, 0.39, 0]),
42632
43125
  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]),
43126
+ asLid(mesh3(cyl(0.38, 0.38, 0.15, 12), metal2(12163644), [0, 0.575, 0])),
42634
43127
  mesh3(cyl(0.45, 0.45, 0.4, 12), paper2(15327172), [0, -0.2, 0]),
42635
43128
  mesh3(cyl(0.452, 0.452, 0.08, 12), cloth2(10172986), [0, -0.2, 0])
42636
43129
  );
@@ -42805,7 +43298,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42805
43298
  mesh3(cyl(0.395, 0.395, 0.68, 12), glass(14925936, 0.55), [0, -0.16, 0]),
42806
43299
  mesh3(cyl(0.37, 0.37, 0.6, 12), food(14260764), [0, -0.2, 0]),
42807
43300
  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]),
43301
+ asLid(mesh3(cyl(0.3, 0.3, 0.18, 12), metal2(7031324), [0, 0.41, 0])),
42809
43302
  mesh3(cyl(0.4, 0.4, 0.34, 12), paper2(15853512), [0, -0.2, 0]),
42810
43303
  mesh3(cyl(0.402, 0.402, 0.05, 12), paper2(9067028), [0, -0.1, 0]),
42811
43304
  mesh3(cyl(0.402, 0.402, 0.05, 12), paper2(9067028), [0, -0.3, 0])
@@ -42891,7 +43384,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42891
43384
  mesh3(cyl(0.345, 0.345, 1.7, 12), glass(14923824, 0.65), [0, -0.45, 0]),
42892
43385
  mesh3(cyl(0.12, 0.345, 0.5, 12), glass(14923824, 0.65), [0, 0.65, 0]),
42893
43386
  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]),
43387
+ asLid(mesh3(cyl(0.14, 0.14, 0.15, 12), plastic(4160042), [0, 1.225, 0])),
42895
43388
  mesh3(cyl(0.35, 0.35, 0.8, 12), paper2(15853512), [0, -0.5, 0]),
42896
43389
  mesh3(cyl(0.352, 0.352, 0.2, 12), paper2(4160042), [0, -0.5, 0])
42897
43390
  );
@@ -42923,7 +43416,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42923
43416
  const body = mesh3(bx(1.5, 2.3, 1.5), glass(10473710, 0.75), [0, -0.5, 0]);
42924
43417
  const shoulder = mesh3(bx(1.2, 0.45, 1.2), glass(10473710, 0.75), [0, 0.875, 0]);
42925
43418
  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]);
43419
+ const cap = asLid(mesh3(cyl(0.4, 0.4, 0.25, 12), dark, [0, 1.525, 0]));
42927
43420
  const grip = mesh3(bx(0.16, 0.9, 0.3), dark, [1.07, 0.6, 0]);
42928
43421
  const upperBar = mesh3(bx(0.45, 0.16, 0.3), dark, [0.925, 0.97, 0]);
42929
43422
  const lowerBar = mesh3(bx(0.45, 0.16, 0.3), dark, [0.925, 0.23, 0]);
@@ -42936,7 +43429,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42936
43429
  const label = mesh3(cyl(0.332, 0.332, 0.7, 12), paper2(1448738), [0, -0.12, 0]);
42937
43430
  const stripe = mesh3(cyl(0.332, 0.332, 0.1, 12), paper2(15753232), [0, 0.28, 0]);
42938
43431
  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]);
43432
+ const cap = asLid(mesh3(cyl(0.19, 0.19, 0.2, 12), plastic(15753232), [0, 0.815, 0]));
42940
43433
  return G(bottle2, label, stripe, collar, cap);
42941
43434
  };
42942
43435
  var milkCarton = ({ mesh: mesh3, bx, cyl, extrude: extrude2, G, paper: paper2, plastic }) => {
@@ -42975,7 +43468,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42975
43468
  const goldBand = mesh3(cyl(0.382, 0.382, 0.05, 12), metal2(11896880), [0, 0.075, 0]);
42976
43469
  const crossUpright = mesh3(bx(0.06, 0.26, 0.02), paper2(8003371), [0, -0.35, 0.375]);
42977
43470
  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]);
43471
+ const foil = asLid(mesh3(cyl(0.15, 0.15, 0.46, 12), metal2(11896880), [0, 1.27, 0]));
42979
43472
  return G(bottle2, label, goldBand, crossUpright, crossArm, foil);
42980
43473
  };
42981
43474
  var coconutWater = ({ mesh: mesh3, bx, cyl, lathe: lathe2, G, plastic, paper: paper2 }) => {
@@ -42984,7 +43477,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42984
43477
  const nut = mesh3(cyl(0.11, 0.11, 0.03, 10), paper2(7031339), [0, -0.12, 0.285], [Math.PI / 2, 0, 0]);
42985
43478
  const leafLeft = mesh3(bx(0.2, 0.05, 0.01), paper2(15135448), [-0.12, 0.08, 0.292], [0, 0, 0.5]);
42986
43479
  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]);
43480
+ const cap = asLid(mesh3(cyl(0.14, 0.14, 0.2, 12), plastic(4165450), [0, 0.75, 0]));
42988
43481
  return G(bottle2, label, nut, leafLeft, leafRight, cap);
42989
43482
  };
42990
43483
  var dirtyWater = ({ mesh: mesh3, cyl, lathe: lathe2, G, plastic, paper: paper2, glass, food }) => {
@@ -42992,7 +43485,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42992
43485
  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
43486
  const sediment = mesh3(cyl(0.31, 0.27, 0.12, 8), food(4865314), [0, -0.82, 0]);
42994
43487
  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]);
43488
+ const cap = asLid(mesh3(cyl(0.19, 0.19, 0.2, 8), plastic(5922920), [0, 0.815, 0]));
42996
43489
  return G(bottle2, murk, sediment, label, cap);
42997
43490
  };
42998
43491
  var painkillers = ({ mesh: mesh3, bx, cyl, G, paper: paper2, metal: metal2, glass }) => {
@@ -45608,7 +46101,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
45608
46101
  const materials = kit.materials.slice(first);
45609
46102
  if (options.wear) setWear(materials, options.wear);
45610
46103
  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 });
46104
+ 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
46105
  markKitOrigin(root, { owner: "game-items-three", kind: "item", id: item.id, ...kind === "shape" ? { standIn: true } : {} });
45613
46106
  return { root, kind, ...shape ? { shape } : {}, size: item.size, materials };
45614
46107
  }
@@ -48895,7 +49388,6 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
48895
49388
  var MAX_CASINGS = 120;
48896
49389
  var MAX_MAGAZINES = 12;
48897
49390
  var LIFETIME_S = 25;
48898
- var HAND_OFFSET = [0.2, -0.2, -0.42];
48899
49391
  var GRAVITY2 = 9.81;
48900
49392
  function createBoxFx(options) {
48901
49393
  const { THREE, scene, camera: camera2, arsenal, groundAt } = options;
@@ -48917,6 +49409,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
48917
49409
  let dip = 0;
48918
49410
  let reloading = false;
48919
49411
  const v = new THREE.Vector3();
49412
+ const v2 = new THREE.Vector3();
49413
+ const eyeFrame = createEyeFrame(THREE, camera2);
49414
+ let rear = 0;
49415
+ let lengthMetres = 0;
48920
49416
  const q = new THREE.Quaternion();
48921
49417
  const q2 = new THREE.Quaternion();
48922
49418
  const pointForward = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
@@ -48928,13 +49424,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
48928
49424
  if (!def || BODY_ACTIONS.has(def.id)) return;
48929
49425
  model = buildWeaponModel(THREE, def);
48930
49426
  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);
49427
+ rear = new THREE.Box3().setFromObject(model).min.x;
49428
+ lengthMetres = length2;
48932
49429
  model.quaternion.copy(pointForward);
48933
49430
  hand.add(model);
48934
49431
  inHand = def.id;
48935
- hold = holdOfProp(THREE, model, def.category, model.scale.x);
49432
+ hold = holdOfProp(THREE, model, def.category, 1);
49433
+ };
49434
+ const handPoint = (x, y, z, out) => {
49435
+ if (!model) return out.set(x, y, z).applyQuaternion(camera2.quaternion).add(camera2.position);
49436
+ return out.set(x, y, z).applyQuaternion(camera2.quaternion).add(model.getWorldPosition(v2));
48936
49437
  };
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
49438
  const throwBody = (kind, mesh3, from, velocity, radius) => {
48939
49439
  if (!(kind === "casing" ? layers.casings : layers.magazines)) {
48940
49440
  mesh3.geometry.dispose();
@@ -49045,10 +49545,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
49045
49545
  update(dt) {
49046
49546
  kick *= Math.exp(-dt * 14);
49047
49547
  dip += ((reloading ? 1 : 0) - dip) * Math.min(1, dt * 8);
49048
- if (inHand) {
49049
- handPoint(0, -0.12 * dip, 0.05 * kick, hand.position);
49548
+ if (inHand && model) {
49549
+ const handgun = def?.category === "pistol" || def?.category === "revolver";
49550
+ placeOnBody({ THREE, hand, holder: model, frame: options.bodyHold?.() ?? eyeFrame(), rear, lengthMetres, handgun, right: hold?.right ?? null, left: hold?.left ?? null });
49551
+ hand.position.add(v.set(0, -0.12 * dip, 0.05 * kick).applyQuaternion(camera2.quaternion));
49050
49552
  q.setFromEuler(new THREE.Euler(0.14 * kick - 0.25 * dip, 0, 0.6 * dip));
49051
- hand.quaternion.copy(camera2.quaternion).multiply(q);
49553
+ hand.quaternion.multiply(q);
49052
49554
  }
49053
49555
  for (const body of [...bodies]) {
49054
49556
  stepBody(body, dt);
@@ -57769,6 +58271,11 @@ void main(){
57769
58271
  var MAX_CONFORM_STEPS = 48;
57770
58272
  var MIN_CONFORM_STEPS = 16;
57771
58273
  var BLOOD_GROW_RATE = 1.2;
58274
+ var BLOOD_START_GROWTH = 0.05;
58275
+ var DEFAULT_MAX_GROUND_MARKS = 64;
58276
+ var DEFAULT_MAX_ROCKS = 512;
58277
+ var DEFAULT_MAX_BLOOD = 256;
58278
+ var MAX_SPARE_GROUND_GEOMETRIES = 16;
57772
58279
  function createMarkLayer(THREE, options) {
57773
58280
  const group = new THREE.Group();
57774
58281
  const textures = createMarkTextures(THREE, options.createCanvas);
@@ -57792,110 +58299,168 @@ void main(){
57792
58299
  const bloodMaterial = new THREE.MeshStandardMaterial({ color: 6031882, roughness: 0.15, metalness: 0.1, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
57793
58300
  const bloodGeometry = new THREE.CircleGeometry(0.5, 9).rotateX(-Math.PI / 2);
57794
58301
  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);
58302
+ const maxGroundMarks = options.maxGroundMarks ?? DEFAULT_MAX_GROUND_MARKS;
58303
+ const instanced = {};
58304
+ function rockMaterial() {
58305
+ if (!instanced.rockMaterial) {
58306
+ instanced.rockMaterial = new THREE.MeshStandardMaterial({ color: 16777215, roughness: 0.9, metalness: 0, flatShading: true });
58307
+ owned.push(instanced.rockMaterial);
57804
58308
  }
57805
- return material;
58309
+ return instanced.rockMaterial;
58310
+ }
58311
+ function bloodMesh() {
58312
+ return instanced.blood ?? (instanced.blood = attach(createInstanced(bloodGeometry, bloodMaterial, options.maxBlood ?? DEFAULT_MAX_BLOOD)));
58313
+ }
58314
+ function rockMesh() {
58315
+ return instanced.rock ?? (instanced.rock = attach(createInstanced(options.rockGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
58316
+ }
58317
+ function plateMesh() {
58318
+ return instanced.plate ?? (instanced.plate = attach(createInstanced(options.plateGeometry, rockMaterial(), options.maxRocks ?? DEFAULT_MAX_ROCKS), true));
58319
+ }
58320
+ function attach(mesh3, castShadow = false) {
58321
+ mesh3.castShadow = castShadow;
58322
+ group.add(mesh3);
58323
+ return mesh3;
58324
+ }
58325
+ const groundMarks = [];
58326
+ const spareGeometries = [];
58327
+ const blood = [];
58328
+ const placed = { blood: 0, rock: 0, plate: 0 };
58329
+ const matrix = new THREE.Matrix4();
58330
+ const position = new THREE.Vector3();
58331
+ const quaternion2 = new THREE.Quaternion();
58332
+ const euler = new THREE.Euler();
58333
+ const scale = new THREE.Vector3();
58334
+ const color2 = new THREE.Color();
58335
+ const up = new THREE.Vector3(0, 1, 0);
58336
+ function createInstanced(geometry, material, capacity) {
58337
+ const mesh3 = new THREE.InstancedMesh(geometry, material, capacity);
58338
+ mesh3.count = 0;
58339
+ mesh3.frustumCulled = false;
58340
+ mesh3.receiveShadow = true;
58341
+ return mesh3;
58342
+ }
58343
+ function takeSpare(vertexCount) {
58344
+ const found = spareGeometries.findIndex((geometry) => geometry.getAttribute("position").count === vertexCount);
58345
+ return found < 0 ? void 0 : spareGeometries.splice(found, 1)[0];
58346
+ }
58347
+ function evictOldestGroundMark() {
58348
+ const oldest = groundMarks.shift();
58349
+ group.remove(oldest.mesh);
58350
+ spareGeometries.push(oldest.mesh.geometry);
58351
+ if (spareGeometries.length > MAX_SPARE_GROUND_GEOMETRIES) spareGeometries.shift().dispose();
57806
58352
  }
57807
58353
  function conform(mark, material) {
57808
58354
  const radius = mark.radius;
57809
58355
  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;
58356
+ if (groundMarks.length >= maxGroundMarks) evictOldestGroundMark();
58357
+ const geometry = takeSpare((steps + 1) * (steps + 1)) ?? new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
58358
+ const grid = geometry.attributes.position;
58359
+ const cell = 2 / steps;
57812
58360
  const cos2 = Math.cos(mark.rotation);
57813
58361
  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;
58362
+ for (let index = 0; index < grid.count; index++) {
58363
+ const localX = Math.fround(index % (steps + 1) * cell - 1) * radius;
58364
+ const localZ = Math.fround(Math.floor(index / (steps + 1)) * cell - 1) * radius;
57817
58365
  const worldX = mark.x + localX * cos2 - localZ * sin2;
57818
58366
  const worldZ = mark.z + localX * sin2 + localZ * cos2;
57819
- position.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
58367
+ grid.setXYZ(index, worldX, (options.ground?.heightAt(worldX, worldZ) ?? 0) + mark.lift, worldZ);
57820
58368
  }
58369
+ grid.needsUpdate = true;
57821
58370
  geometry.computeVertexNormals();
58371
+ geometry.attributes.normal.needsUpdate = true;
58372
+ geometry.boundingSphere = null;
58373
+ geometry.boundingBox = null;
57822
58374
  const mesh3 = new THREE.Mesh(geometry, material);
57823
58375
  mesh3.receiveShadow = true;
57824
- return mesh3;
58376
+ group.add(mesh3);
58377
+ groundMarks.push({ mesh: mesh3, steps });
57825
58378
  }
57826
58379
  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;
58380
+ const mesh3 = mark.plate ? plateMesh() : rockMesh();
58381
+ const slot = mark.plate ? "plate" : "rock";
58382
+ const index = placed[slot]++ % mesh3.instanceMatrix.count;
58383
+ quaternion2.setFromEuler(euler.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]));
58384
+ 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])));
58385
+ mesh3.setColorAt(index, color2.set(mark.color));
58386
+ mesh3.count = Math.min(placed[slot], mesh3.instanceMatrix.count);
58387
+ mesh3.instanceMatrix.needsUpdate = true;
58388
+ mesh3.instanceColor.needsUpdate = true;
58389
+ }
58390
+ function writeBlood(index, drawn, growth) {
58391
+ quaternion2.setFromAxisAngle(up, drawn.rotation);
58392
+ const mesh3 = bloodMesh();
58393
+ 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)));
58394
+ mesh3.instanceMatrix.needsUpdate = true;
57834
58395
  }
57835
58396
  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;
58397
+ const index = placed.blood++ % bloodMesh().instanceMatrix.count;
58398
+ const drawn = { position: [mark.x, mark.y, mark.z], rotation: mark.rotation, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 };
58399
+ blood[index] = drawn;
58400
+ writeBlood(index, drawn, BLOOD_START_GROWTH);
58401
+ bloodMesh().count = Math.min(placed.blood, bloodMesh().instanceMatrix.count);
57844
58402
  }
57845
58403
  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
- }
58404
+ if (mark.kind === "scorch") conform(mark, scorchMaterials[mark.variant]);
58405
+ else if (mark.kind === "dirt") conform(mark, dirtMaterial);
58406
+ else if (mark.kind === "rock") addRock(mark);
58407
+ else addBlood(mark);
58408
+ }
58409
+ function clearMarks() {
58410
+ for (const drawn of groundMarks) group.remove(drawn.mesh);
58411
+ for (const drawn of groundMarks) drawn.mesh.geometry.dispose();
58412
+ groundMarks.length = 0;
58413
+ for (const geometry of spareGeometries) geometry.dispose();
58414
+ spareGeometries.length = 0;
57857
58415
  blood.length = 0;
57858
- meshCount = 0;
58416
+ placed.blood = placed.rock = placed.plate = 0;
58417
+ for (const mesh3 of [instanced.blood, instanced.rock, instanced.plate]) if (mesh3) mesh3.count = 0;
57859
58418
  }
57860
58419
  return {
57861
58420
  group,
57862
58421
  handle(event) {
57863
- if (event.type === "clear") {
57864
- clearMeshes();
57865
- return;
57866
- }
57867
- group.add(build(event.mark));
57868
- meshCount++;
58422
+ if (event.type === "clear") clearMarks();
58423
+ else build(event.mark);
58424
+ },
58425
+ prepare() {
58426
+ bloodMesh();
58427
+ rockMesh();
58428
+ plateMesh();
57869
58429
  },
57870
58430
  update(dt) {
57871
- for (const drawn of blood) {
57872
- if (drawn.age > 1) continue;
58431
+ for (let index = 0; index < blood.length; index++) {
58432
+ const drawn = blood[index];
58433
+ if (!drawn || drawn.age > 1) continue;
57873
58434
  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));
58435
+ writeBlood(index, drawn, Math.max(BLOOD_START_GROWTH, 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5)));
57876
58436
  }
57877
58437
  },
57878
- count: () => meshCount,
58438
+ count: () => groundMarks.length + [instanced.blood, instanced.rock, instanced.plate].filter((mesh3) => mesh3 && mesh3.count > 0).length,
57879
58439
  dispose() {
57880
- clearMeshes();
58440
+ clearMarks();
58441
+ for (const mesh3 of [instanced.blood, instanced.rock, instanced.plate]) mesh3?.dispose();
57881
58442
  for (const item of owned) item.dispose();
57882
58443
  }
57883
58444
  };
57884
58445
  }
57885
58446
  var SPHERE_DEFAULT_WIDTH = 8;
57886
58447
  var SPHERE_DEFAULT_HEIGHT = 6;
58448
+ var MAX_PARKED_MESHES_PER_KIND = 256;
57887
58449
  function geometryOf(THREE, particle) {
57888
58450
  switch (particle.shape) {
57889
58451
  case "cone":
57890
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, false);
58452
+ return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, false);
57891
58453
  case "cone_open":
57892
- return new THREE.ConeGeometry(particle.radius, particle.height, particle.segments || void 0, particle.segments2 || 1, true);
58454
+ return new THREE.ConeGeometry(1, 1, particle.segments || void 0, particle.segments2 || 1, true);
57893
58455
  case "sphere":
57894
- return new THREE.SphereGeometry(particle.radius, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
58456
+ return new THREE.SphereGeometry(1, particle.segments || SPHERE_DEFAULT_WIDTH, particle.segments2 || SPHERE_DEFAULT_HEIGHT);
57895
58457
  case "icosahedron":
57896
- return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
58458
+ return new THREE.IcosahedronGeometry(1, particle.segments);
57897
58459
  }
57898
58460
  }
58461
+ function isCone(particle) {
58462
+ return particle.shape === "cone" || particle.shape === "cone_open";
58463
+ }
57899
58464
  function materialOf(THREE, particle) {
57900
58465
  if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
57901
58466
  return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
@@ -57906,51 +58471,82 @@ void main(){
57906
58471
  (kind) => new THREE.Mesh(geometry, materialOf(THREE, { ...kind, color: 8421504, opacity: 0.5 }))
57907
58472
  );
57908
58473
  }
58474
+ function geometryKeyOf(particle) {
58475
+ return `${particle.shape}:${particle.segments}:${particle.segments2}`;
58476
+ }
57909
58477
  function createMeshLayer(THREE, pool) {
57910
58478
  const group = new THREE.Group();
57911
58479
  group.name = "workshop-meshes";
57912
58480
  const drawn = /* @__PURE__ */ new Map();
58481
+ const geometries = /* @__PURE__ */ new Map();
58482
+ const parked = /* @__PURE__ */ new Map();
57913
58483
  const frame2 = new THREE.Quaternion();
57914
58484
  const euler = new THREE.Euler();
57915
58485
  const turn2 = new THREE.Quaternion();
57916
58486
  const offset = new THREE.Vector3();
57917
- function release(mesh3) {
57918
- mesh3.removeFromParent();
57919
- mesh3.geometry.dispose();
57920
- mesh3.material.dispose();
58487
+ let stamp = 0;
58488
+ function geometryFor(particle) {
58489
+ const key2 = geometryKeyOf(particle);
58490
+ let geometry = geometries.get(key2);
58491
+ if (!geometry) {
58492
+ geometry = geometryOf(THREE, particle);
58493
+ geometries.set(key2, geometry);
58494
+ }
58495
+ return geometry;
58496
+ }
58497
+ function acquire(particle) {
58498
+ const parkingKey = `${geometryKeyOf(particle)}|${particle.lit}|${particle.additive}`;
58499
+ const mesh3 = parked.get(parkingKey)?.pop() ?? new THREE.Mesh(geometryFor(particle), materialOf(THREE, particle));
58500
+ mesh3.material.color.set(particle.color);
58501
+ mesh3.frustumCulled = false;
58502
+ mesh3.renderOrder = particle.additive ? 5 : 3;
58503
+ group.add(mesh3);
58504
+ return { mesh: mesh3, parkingKey, stamp };
58505
+ }
58506
+ function park(entry2) {
58507
+ entry2.mesh.removeFromParent();
58508
+ let free = parked.get(entry2.parkingKey);
58509
+ if (!free) {
58510
+ free = [];
58511
+ parked.set(entry2.parkingKey, free);
58512
+ }
58513
+ if (free.length < MAX_PARKED_MESHES_PER_KIND) free.push(entry2.mesh);
58514
+ else entry2.mesh.material.dispose();
57921
58515
  }
57922
58516
  return {
57923
58517
  group,
57924
58518
  sync() {
57925
- const live = /* @__PURE__ */ new Set();
58519
+ stamp++;
57926
58520
  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);
58521
+ let entry2 = drawn.get(particle.id);
58522
+ if (!entry2) {
58523
+ entry2 = acquire(particle);
58524
+ drawn.set(particle.id, entry2);
57935
58525
  }
58526
+ entry2.stamp = stamp;
58527
+ const mesh3 = entry2.mesh;
57936
58528
  frame2.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
57937
58529
  offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame2);
57938
58530
  mesh3.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
57939
58531
  turn2.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
57940
58532
  mesh3.quaternion.copy(frame2).multiply(turn2);
57941
- mesh3.scale.setScalar(particle.scale);
58533
+ mesh3.scale.set(particle.radius * particle.scale, (isCone(particle) ? particle.height : particle.radius) * particle.scale, particle.radius * particle.scale);
57942
58534
  mesh3.material.opacity = particle.opacity;
57943
58535
  }
57944
- for (const [id, mesh3] of drawn) {
57945
- if (live.has(id)) continue;
57946
- release(mesh3);
58536
+ for (const [id, entry2] of drawn) {
58537
+ if (entry2.stamp === stamp) continue;
58538
+ park(entry2);
57947
58539
  drawn.delete(id);
57948
58540
  }
57949
58541
  return pool.count;
57950
58542
  },
57951
58543
  dispose() {
57952
- for (const mesh3 of drawn.values()) release(mesh3);
58544
+ for (const entry2 of drawn.values()) park(entry2);
57953
58545
  drawn.clear();
58546
+ for (const free of parked.values()) for (const mesh3 of free) mesh3.material.dispose();
58547
+ parked.clear();
58548
+ for (const geometry of geometries.values()) geometry.dispose();
58549
+ geometries.clear();
57954
58550
  group.removeFromParent();
57955
58551
  }
57956
58552
  };
@@ -58518,7 +59114,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58518
59114
  const shocks = createShockLayer(THREE, pools.shocks);
58519
59115
  const sprites = createSpriteLayer(THREE, world.sprites(), options.pointScale ?? 700);
58520
59116
  const lights = createLightLayer(THREE, pools.lights, options.lightScale ?? Math.PI);
58521
- const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry });
59117
+ const marks = createMarkLayer(THREE, { ground: pools.ground, createCanvas: options.createCanvas, rockGeometry, plateGeometry, ...options.markLimits });
58522
59118
  root.add(puffs.mesh, sparks.mesh, glows.mesh, chunks.rocks, chunks.plates, shocks.group, lights.group, sprites.additive, sprites.normal, marks.group);
58523
59119
  const stopMarks = world.onMarks((event) => marks.handle(event));
58524
59120
  const overlay = options.overlay ? createScreenOverlay(options.overlay) : void 0;
@@ -58544,6 +59140,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58544
59140
  const position = options.camera.position;
58545
59141
  world.setCamera([position.x, position.y, position.z]);
58546
59142
  }
59143
+ function attachWorkshopLayers() {
59144
+ const engine = world.peekWorkshop();
59145
+ if (!engine || engine === workshopEngine) return;
59146
+ workshopLayers?.dispose();
59147
+ if (workshopLayers) root.remove(workshopLayers.group);
59148
+ workshopEngine = engine;
59149
+ workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
59150
+ root.add(workshopLayers.group);
59151
+ applyLayers();
59152
+ }
58547
59153
  function syncAll() {
58548
59154
  lastDrawn.puffs = puffs.sync();
58549
59155
  lastDrawn.sparks = sparks.sync();
@@ -58552,15 +59158,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58552
59158
  lastDrawn.shocks = shocks.sync();
58553
59159
  lastDrawn.sprites = sprites.sync();
58554
59160
  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
- }
59161
+ attachWorkshopLayers();
58564
59162
  const workshopCalls = workshopLayers?.sync() ?? 0;
58565
59163
  const glow = pools.lights.glow;
58566
59164
  uniforms.uGlowPos.value.set(glow.x, glow.y, glow.z);
@@ -58613,6 +59211,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58613
59211
  },
58614
59212
  warmUpProbes() {
58615
59213
  releaseAnchors();
59214
+ marks.prepare();
59215
+ world.workshop();
59216
+ attachWorkshopLayers();
58616
59217
  const meshes = createMeshLayerProbes(THREE);
58617
59218
  for (const mesh3 of meshes) mesh3.frustumCulled = false;
58618
59219
  const group = new THREE.Group();
@@ -58679,6 +59280,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58679
59280
 
58680
59281
  // src/fx/heavy.ts
58681
59282
  var PI14 = Math.PI;
59283
+ var ROCKET_LIGHT_NAME = "rocket-light";
59284
+ var rocketsInFlight = /* @__PURE__ */ new WeakMap();
59285
+ function createRocketLight(THREE) {
59286
+ const light = new THREE.PointLight(16751168, 0, 2.5);
59287
+ light.name = ROCKET_LIGHT_NAME;
59288
+ light.position.set(0, -50, 0);
59289
+ return light;
59290
+ }
58682
59291
  var flameMaterials = (kit) => {
58683
59292
  const { THREE } = kit;
58684
59293
  return [
@@ -58708,8 +59317,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58708
59317
  exhaust.rotation.z = PI14 / 2;
58709
59318
  exhaust.position.x = -0.08;
58710
59319
  rocket.add(exhaust);
58711
- const light = new THREE.PointLight(16751168, 3, 2.5);
58712
- rocket.add(light);
59320
+ const light = context.rocketLight;
59321
+ light.intensity = 3;
59322
+ rocketsInFlight.set(light, (rocketsInFlight.get(light) ?? 0) + 1);
58713
59323
  context.every(
58714
59324
  1.6,
58715
59325
  (progress, dt) => {
@@ -58717,6 +59327,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58717
59327
  const distance3 = 1.2 * flight + (flight > 0.25 ? 9 * (flight - 0.25) ** 2 : 0);
58718
59328
  rocket.position.copy(muzzle2).addScaledVector(forward, distance3);
58719
59329
  rocket.rotation.x += 0.25 * dt * 60;
59330
+ light.position.copy(rocket.position);
58720
59331
  exhaust.scale.set(1, 0.6 + random() * 0.8, 1);
58721
59332
  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
59333
  },
@@ -58724,7 +59335,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58724
59335
  rocket.removeFromParent();
58725
59336
  exhaust.geometry.dispose();
58726
59337
  exhaust.material.dispose();
58727
- light.dispose();
59338
+ const remaining = (rocketsInFlight.get(light) ?? 1) - 1;
59339
+ rocketsInFlight.set(light, remaining);
59340
+ if (remaining > 0) return;
59341
+ light.intensity = 0;
59342
+ light.position.set(0, -50, 0);
58728
59343
  }
58729
59344
  );
58730
59345
  }
@@ -59539,11 +60154,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59539
60154
 
59540
60155
  // src/fx/viewmodel.ts
59541
60156
  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
60157
  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 };
60158
+ var WALL_PULL = { margin: 0.03, max: 1.2, rate: 14, probe: 3 };
60159
+ var HAND_IN_VIEW = { floor: -0.88, max: 0.3, rate: 10 };
59547
60160
  var LAUNCH_VOLUME = 0.6;
59548
60161
  var fittedOf2 = (parts) => parts.map((part) => ({ id: part.id, mount: part.mount, ...part.integral ? { integral: true } : {}, ...part.on === void 0 ? {} : { on: part.on } }));
59549
60162
  var collapsedStockPose = (move2) => {
@@ -59573,6 +60186,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59573
60186
  const hand = new THREE.Group();
59574
60187
  hand.name = VIEWMODEL_NODE_NAME;
59575
60188
  scene.add(hand);
60189
+ const rocketLight = createRocketLight(THREE);
60190
+ scene.add(rocketLight);
59576
60191
  const holder = new THREE.Group();
59577
60192
  holder.quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), PI16 / 2);
59578
60193
  hand.add(holder);
@@ -59629,7 +60244,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59629
60244
  positionOf: (part) => worldPositionOf(current, part, new THREE.Vector3()),
59630
60245
  orientation: () => current.model.root.getWorldQuaternion(new THREE.Quaternion()),
59631
60246
  every: (seconds, step, done) => void tasks.push({ delay: 0, seconds, elapsed: 0, step, done }),
59632
- groundAt
60247
+ groundAt,
60248
+ rocketLight
59633
60249
  });
59634
60250
  const equip = (weapon) => {
59635
60251
  const def = arsenal.weapons.get(weapon);
@@ -59642,14 +60258,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59642
60258
  reach = 0;
59643
60259
  pull = 0;
59644
60260
  lift = 0;
59645
- handHeight = 0;
59646
- handDepth = 0;
59647
60261
  rigStock(THREE, model, def);
59648
60262
  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));
60263
+ const modelBox = new THREE.Box3().setFromObject(model.root);
60264
+ const length2 = modelBox.getSize(new THREE.Vector3()).x;
60265
+ modelRear = modelBox.min.x;
59653
60266
  holder.add(model.root);
59654
60267
  const caliber = caliberOf(arsenal, def);
59655
60268
  const dims = caliber?.cartridge;
@@ -59661,7 +60274,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59661
60274
  def,
59662
60275
  caliber,
59663
60276
  model,
59664
- holderScale: scale,
60277
+ holderScale: 1,
59665
60278
  lengthMetres: length2,
59666
60279
  caseType: caliber?.caseType ?? "rifle",
59667
60280
  soundProfile: soundProfileOf(weapon, model.parts.family),
@@ -59691,7 +60304,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59691
60304
  applyCondition(kit, model, condition);
59692
60305
  lastAmmo = options.tool?.().ammo ?? 0;
59693
60306
  hand.updateMatrixWorld(true);
59694
- hold = holdOfModel(THREE, model, scale);
60307
+ hold = holdOfModel(THREE, model, 1);
59695
60308
  hold = { ...hold, works: handWorkOf(THREE, model, weapon, hold.right) };
59696
60309
  triggerRest = model.parts.trigger?.rotation.z ?? 0;
59697
60310
  lastShot = -Infinity;
@@ -59711,8 +60324,6 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59711
60324
  let reach = 0;
59712
60325
  let pull = 0;
59713
60326
  let lift = 0;
59714
- let handHeight = 0;
59715
- let handDepth = 0;
59716
60327
  const ahead = new THREE.Vector3();
59717
60328
  const up = new THREE.Vector3();
59718
60329
  const seen = new THREE.Vector3();
@@ -59725,19 +60336,31 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59725
60336
  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
60337
  return farthest;
59727
60338
  };
60339
+ let modelRear = 0;
60340
+ const eyeFrame = createEyeFrame(THREE, camera2);
60341
+ const liftToKeepInView = () => {
60342
+ if (!hold || !session || !("fov" in camera2)) return 0;
60343
+ const tanHalf = Math.tan((camera2.fov ?? 70) * PI16 / 360);
60344
+ eyeTurn.copy(camera2.quaternion).invert();
60345
+ const rest2 = targetsOf(THREE, hold, session.model.root.getWorldQuaternion(rootQuaternion), 0);
60346
+ let needed = 0;
60347
+ for (const [anchor, pose] of [[hold.right, rest2.R], [hold.left, rest2.L]]) {
60348
+ if (!anchor || !pose || anchor.part !== session.model.root) continue;
60349
+ seen.set(...pose.position).sub(camera2.position).applyQuaternion(eyeTurn);
60350
+ if (-seen.z > 0.05) needed = Math.max(needed, HAND_IN_VIEW.floor * -seen.z * tanHalf - seen.y);
60351
+ }
60352
+ return Math.max(0, Math.min(HAND_IN_VIEW.max, needed));
60353
+ };
59728
60354
  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
- }
60355
+ if (!session) return;
60356
+ const family = session.model.parts.family;
60357
+ session.holderScale = 1;
60358
+ 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
60359
  hand.updateMatrixWorld(true);
59740
- if (!options.contact || !session) return;
60360
+ lift += (liftToKeepInView() - lift) * Math.min(1, dt * HAND_IN_VIEW.rate);
60361
+ if (lift > 1e-4) hand.position.addScaledVector(up.set(0, 1, 0).applyQuaternion(camera2.quaternion), lift);
60362
+ hand.updateMatrixWorld(true);
60363
+ if (!options.contact) return;
59741
60364
  ahead.set(0, 0, -1).applyQuaternion(camera2.quaternion);
59742
60365
  if (reach === 0) reach = Math.max(0.01, measureReach(session));
59743
60366
  const free = options.contact(camera2.position, ahead, WALL_PULL.probe);
@@ -59851,12 +60474,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59851
60474
  const targets = targetsOf(THREE, hold, session.model.root.getWorldQuaternion(rootQuaternion), magazineHand, hold.works ? workWeights(hold.works, player?.running ?? []) : void 0);
59852
60475
  const trigger = session.model.parts.trigger;
59853
60476
  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;
60477
+ return limitSupportSpan(targets);
59860
60478
  },
59861
60479
  get inHand() {
59862
60480
  return session?.id ?? null;
@@ -59875,6 +60493,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59875
60493
  particles.dispose();
59876
60494
  ownVfx?.dispose();
59877
60495
  hand.removeFromParent();
60496
+ rocketLight.removeFromParent();
60497
+ rocketLight.dispose();
59878
60498
  }
59879
60499
  };
59880
60500
  }
@@ -59899,6 +60519,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59899
60519
  ...options.attachmentModels ? { attachmentModels: options.attachmentModels } : {},
59900
60520
  ...options.raycast ? { raycast: options.raycast } : {},
59901
60521
  ...options.contact ? { contact: options.contact } : {},
60522
+ ...options.bodyHold ? { bodyHold: options.bodyHold } : {},
59902
60523
  ...options.effects ? { effects: options.effects } : {},
59903
60524
  ...options.flyRocket === void 0 ? {} : { flyRocket: options.flyRocket }
59904
60525
  });
@@ -60674,7 +61295,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
60674
61295
  var P13_VALVE_MESH = "valve";
60675
61296
  var P13_VALVE_COLOR = 12095802;
60676
61297
  var P13_WALL = 25e-4;
60677
- var SHAPES2 = {
61298
+ var SHAPES3 = {
60678
61299
  fuel_barrel: { shape: { kind: "cylinder", radius: 0.3, height: 0.9 }, position: [0, 0.45, 0], color: 3104666 },
60679
61300
  propane_p13: {
60680
61301
  shape: { kind: "cylinder", radius: 0.176, height: 0.49 },
@@ -60685,7 +61306,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
60685
61306
  },
60686
61307
  car_fuel_tank: { shape: { kind: "box", size: [0.9, 0.25, 0.5] }, position: [0, 0.4, 0], color: 4935509 }
60687
61308
  };
60688
- var WORLD_EXPLOSIVE_IDS = Object.keys(SHAPES2);
61309
+ var WORLD_EXPLOSIVE_IDS = Object.keys(SHAPES3);
60689
61310
  var SHELL = { thickness: 1e-3, fill: 0.03 };
60690
61311
  var explosivePart = (def, shape, scale) => ({
60691
61312
  id: "tank",
@@ -60731,7 +61352,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
60731
61352
  return [centre.x, centre.y, centre.z];
60732
61353
  };
60733
61354
  var worldExplosiveShell = (THREE, defId) => {
60734
- const shape = SHAPES2[defId];
61355
+ const shape = SHAPES3[defId];
60735
61356
  const model = shape ? modelOf(THREE, shape, 2) : void 0;
60736
61357
  if (!model) return void 0;
60737
61358
  model.traverse((node) => {
@@ -60744,7 +61365,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
60744
61365
  return model;
60745
61366
  };
60746
61367
  var worldExplosiveItem = (def, name, options = {}) => {
60747
- const shape = SHAPES2[def.id];
61368
+ const shape = SHAPES3[def.id];
60748
61369
  if (!shape) return void 0;
60749
61370
  const scale = () => options.scale?.() ?? 1;
60750
61371
  const spec = () => ({ name, parts: [explosivePart(def, shape, scale())], joints: [], tags: ["explosive"] });
@@ -61179,6 +61800,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
61179
61800
  raycast: createLaserRaycast(lent.THREE, lent.scene, lent.camera),
61180
61801
  // 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
61802
  contact: createLaserRaycast(lent.THREE, lent.scene, lent.camera, { now: () => simulatedMs }),
61803
+ ...lent.holdFrame ? { bodyHold: () => lent.holdFrame?.() ?? null } : {},
61182
61804
  ...lent.effects ? { effects: lent.effects } : {},
61183
61805
  ...options.attachmentModels ? { attachmentModels: options.attachmentModels } : {},
61184
61806
  ...options.condition ? { condition: options.condition } : {},
@@ -61991,7 +62613,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
61991
62613
  };
61992
62614
  for (const { kind, item } of PUSH_KINDS) PUSH_LAYOUT[kind].forEach(([distance3, degrees]) => lay(kind, item, bearing(centre, distance3, degrees)));
61993
62615
  if (mode === "grenade") {
61994
- lay("zombie", "zombie_walker", bearing(centre, 3.4, -34));
62616
+ lay("zombie", "zombie_walker", bearing(centre, 3.4, -130));
61995
62617
  lay("survivor", "person_police_officer", bearing(centre, 7.5, 78));
61996
62618
  lay("camp_table", "camp_table", bearing(centre, 3, 90));
61997
62619
  }
@@ -62034,7 +62656,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
62034
62656
  const outward = ((now[0] - from[0]) * (from[0] - blast.point[0]) + (now[2] - from[2]) * (from[2] - blast.point[2])) / away;
62035
62657
  return { ...body, distance: distance3, outward, moved: flat(now, from) };
62036
62658
  });
62037
- const torn = (row) => PUSH_KINDS.some((entry2) => entry2.kind === row.kind) && partsOf(row.objectId) < (partsBefore.get(row.objectId) ?? 0);
62659
+ 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
62660
  const reached = rows.filter((row) => row.distance < blast.radius * 0.85 && !torn(row));
62039
62661
  const movedAway = reached.filter((row) => row.outward >= MOVED);
62040
62662
  const least = reached.length === 0 ? 0 : Math.min(...reached.map((row) => row.outward));
@@ -62047,9 +62669,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
62047
62669
  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
62670
  }
62049
62671
  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 });
62672
+ 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
62673
  }
62052
- const table = reached.find((row) => HEAVY_ITEMS.includes(row.kind));
62674
+ const table = rows.find((row) => HEAVY_ITEMS.includes(row.kind) && row.distance < blast.radius * 0.85);
62053
62675
  if (table) expect(table.moved >= MOVED, fillText2(text.push_heavy, { what: things[table.itemId] ?? table.itemId, moved: table.moved.toFixed(2) }), { table });
62054
62676
  }
62055
62677
  };