@adatechnology/game-weapons-three 0.10.3 → 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. */
@@ -39701,6 +40154,55 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39701
40154
  ends: 22e-5
39702
40155
  }
39703
40156
  }
40157
+ },
40158
+ plastic_bottle: {
40159
+ models: [
40160
+ "water_bottle",
40161
+ "water_jug_5l",
40162
+ "dirty_water",
40163
+ "sports_drink",
40164
+ "coconut_water",
40165
+ "cooking_oil",
40166
+ "alcohol_70",
40167
+ "fuel_jerrycan"
40168
+ ],
40169
+ rule: {
40170
+ hollow: true,
40171
+ welded: true,
40172
+ skinTags: [
40173
+ "paper",
40174
+ "food"
40175
+ ],
40176
+ materials: {
40177
+ glass: "plastic"
40178
+ }
40179
+ }
40180
+ },
40181
+ glass_bottle: {
40182
+ models: [
40183
+ "cachaca",
40184
+ "communion_wine",
40185
+ "honey_jar",
40186
+ "home_preserves"
40187
+ ],
40188
+ rule: {
40189
+ hollow: true,
40190
+ welded: true,
40191
+ skinTags: [
40192
+ "paper",
40193
+ "food"
40194
+ ]
40195
+ }
40196
+ },
40197
+ drink_carton: {
40198
+ models: [
40199
+ "juice_box",
40200
+ "milk_carton"
40201
+ ],
40202
+ rule: {
40203
+ hollow: true,
40204
+ welded: true
40205
+ }
39704
40206
  }
39705
40207
  },
39706
40208
  contents: {
@@ -39716,6 +40218,67 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39716
40218
  liquid: "soda",
39717
40219
  fill: 0.95
39718
40220
  },
40221
+ water_bottle: {
40222
+ liquid: "water",
40223
+ fill: 0.95
40224
+ },
40225
+ water_jug_5l: {
40226
+ liquid: "water",
40227
+ fill: 0.95,
40228
+ box: true
40229
+ },
40230
+ dirty_water: {
40231
+ liquid: "water",
40232
+ fill: 0.9
40233
+ },
40234
+ sports_drink: {
40235
+ liquid: "juice",
40236
+ fill: 0.95
40237
+ },
40238
+ coconut_water: {
40239
+ liquid: "water",
40240
+ fill: 0.9,
40241
+ box: true
40242
+ },
40243
+ juice_box: {
40244
+ liquid: "juice",
40245
+ fill: 0.95,
40246
+ box: true
40247
+ },
40248
+ milk_carton: {
40249
+ liquid: "milk",
40250
+ fill: 0.95,
40251
+ box: true
40252
+ },
40253
+ cachaca: {
40254
+ liquid: "spirits",
40255
+ fill: 0.9
40256
+ },
40257
+ communion_wine: {
40258
+ liquid: "wine",
40259
+ fill: 0.9
40260
+ },
40261
+ cooking_oil: {
40262
+ liquid: "cooking_oil",
40263
+ fill: 0.9
40264
+ },
40265
+ alcohol_70: {
40266
+ liquid: "spirits",
40267
+ fill: 0.9
40268
+ },
40269
+ honey_jar: {
40270
+ liquid: "honey",
40271
+ fill: 0.85
40272
+ },
40273
+ home_preserves: {
40274
+ liquid: "water",
40275
+ fill: 0.5
40276
+ },
40277
+ fuel_jerrycan: {
40278
+ liquid: "fuel",
40279
+ fill: 0.85,
40280
+ box: true
40281
+ },
39719
40282
  canned_beans: {
39720
40283
  liquid: "juice",
39721
40284
  fill: 0.4
@@ -39737,6 +40300,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39737
40300
  liquid: "honey",
39738
40301
  fill: 0.9
39739
40302
  }
40303
+ },
40304
+ shapes: {
40305
+ box: {
40306
+ hollow: true,
40307
+ welded: true
40308
+ }
39740
40309
  }
39741
40310
  };
39742
40311
  var DESTRUCTION_MATERIAL_BY_TAG = {
@@ -39750,6 +40319,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39750
40319
  };
39751
40320
  var FAMILIES = destruction_default.families;
39752
40321
  var CONTENTS = destruction_default.contents;
40322
+ var SHAPES2 = destruction_default.shapes;
40323
+ var shapeDestruction = (shapeId) => SHAPES2[shapeId];
39753
40324
  var FAMILY_OF_MODEL = new Map(Object.entries(FAMILIES).flatMap(([family, entry2]) => entry2.models.map((model) => [model, family])));
39754
40325
  var itemDestruction = (modelId) => {
39755
40326
  const family = FAMILY_OF_MODEL.get(modelId);
@@ -39863,9 +40434,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39863
40434
  root.userData.tessellationTriangles = added;
39864
40435
  }
39865
40436
  function tagForDestruction(root, options = {}) {
39866
- const { modelId, weight, size, three } = options;
40437
+ const { modelId, shapeId, weight, size, powder, three } = options;
39867
40438
  if (weight !== void 0 && weight > 0) root.userData.destructionMass = weight;
39868
- const rule = modelId === void 0 ? void 0 : itemDestruction(modelId);
40439
+ const rule = modelId !== void 0 ? itemDestruction(modelId) : shapeId !== void 0 ? shapeDestruction(shapeId) : void 0;
39869
40440
  if (rule?.tessellate) tessellateShapes(root, rule.tessellate, three);
39870
40441
  const solid2 = [];
39871
40442
  root.traverse((object2) => {
@@ -39880,6 +40451,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
39880
40451
  return;
39881
40452
  }
39882
40453
  if (rule?.hollow) mesh3.userData.hollow = true;
40454
+ if (powder) mesh3.userData.destructionPowder = { color: powder.color };
39883
40455
  solid2.push(mesh3);
39884
40456
  });
39885
40457
  if (!rule?.welded || solid2.length < 2) return;
@@ -42357,10 +42929,41 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42357
42929
  const m = metal2(9277590);
42358
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]));
42359
42931
  };
42360
- 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]));
42361
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]));
42362
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]));
42363
- 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]));
42364
42967
  var firstAidKit = ({ mesh: mesh3, bx, torus: torus2, G, plastic }) => {
42365
42968
  const r = plastic(11680300);
42366
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]));
@@ -42520,7 +43123,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42520
43123
  mesh3(cyl(0.41, 0.41, 0.86, 12), food(9314106), [0, -0.2, 0]),
42521
43124
  mesh3(cyl(0.34, 0.445, 0.12, 12), glass(13625070, 0.5), [0, 0.39, 0]),
42522
43125
  mesh3(cyl(0.34, 0.34, 0.05, 12), glass(13625070, 0.5), [0, 0.475, 0]),
42523
- 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])),
42524
43127
  mesh3(cyl(0.45, 0.45, 0.4, 12), paper2(15327172), [0, -0.2, 0]),
42525
43128
  mesh3(cyl(0.452, 0.452, 0.08, 12), cloth2(10172986), [0, -0.2, 0])
42526
43129
  );
@@ -42695,7 +43298,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42695
43298
  mesh3(cyl(0.395, 0.395, 0.68, 12), glass(14925936, 0.55), [0, -0.16, 0]),
42696
43299
  mesh3(cyl(0.37, 0.37, 0.6, 12), food(14260764), [0, -0.2, 0]),
42697
43300
  mesh3(cyl(0.3, 0.395, 0.14, 12), glass(14925936, 0.55), [0, 0.25, 0]),
42698
- 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])),
42699
43302
  mesh3(cyl(0.4, 0.4, 0.34, 12), paper2(15853512), [0, -0.2, 0]),
42700
43303
  mesh3(cyl(0.402, 0.402, 0.05, 12), paper2(9067028), [0, -0.1, 0]),
42701
43304
  mesh3(cyl(0.402, 0.402, 0.05, 12), paper2(9067028), [0, -0.3, 0])
@@ -42781,7 +43384,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42781
43384
  mesh3(cyl(0.345, 0.345, 1.7, 12), glass(14923824, 0.65), [0, -0.45, 0]),
42782
43385
  mesh3(cyl(0.12, 0.345, 0.5, 12), glass(14923824, 0.65), [0, 0.65, 0]),
42783
43386
  mesh3(cyl(0.12, 0.12, 0.25, 12), glass(14923824, 0.65), [0, 1.025, 0]),
42784
- 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])),
42785
43388
  mesh3(cyl(0.35, 0.35, 0.8, 12), paper2(15853512), [0, -0.5, 0]),
42786
43389
  mesh3(cyl(0.352, 0.352, 0.2, 12), paper2(4160042), [0, -0.5, 0])
42787
43390
  );
@@ -42813,7 +43416,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42813
43416
  const body = mesh3(bx(1.5, 2.3, 1.5), glass(10473710, 0.75), [0, -0.5, 0]);
42814
43417
  const shoulder = mesh3(bx(1.2, 0.45, 1.2), glass(10473710, 0.75), [0, 0.875, 0]);
42815
43418
  const neck = mesh3(cyl(0.34, 0.34, 0.3, 12), dark, [0, 1.25, 0]);
42816
- 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]));
42817
43420
  const grip = mesh3(bx(0.16, 0.9, 0.3), dark, [1.07, 0.6, 0]);
42818
43421
  const upperBar = mesh3(bx(0.45, 0.16, 0.3), dark, [0.925, 0.97, 0]);
42819
43422
  const lowerBar = mesh3(bx(0.45, 0.16, 0.3), dark, [0.925, 0.23, 0]);
@@ -42826,7 +43429,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42826
43429
  const label = mesh3(cyl(0.332, 0.332, 0.7, 12), paper2(1448738), [0, -0.12, 0]);
42827
43430
  const stripe = mesh3(cyl(0.332, 0.332, 0.1, 12), paper2(15753232), [0, 0.28, 0]);
42828
43431
  const collar = mesh3(cyl(0.2, 0.2, 0.04, 12), plastic(15263976), [0, 0.705, 0]);
42829
- 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]));
42830
43433
  return G(bottle2, label, stripe, collar, cap);
42831
43434
  };
42832
43435
  var milkCarton = ({ mesh: mesh3, bx, cyl, extrude: extrude2, G, paper: paper2, plastic }) => {
@@ -42865,7 +43468,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42865
43468
  const goldBand = mesh3(cyl(0.382, 0.382, 0.05, 12), metal2(11896880), [0, 0.075, 0]);
42866
43469
  const crossUpright = mesh3(bx(0.06, 0.26, 0.02), paper2(8003371), [0, -0.35, 0.375]);
42867
43470
  const crossArm = mesh3(bx(0.18, 0.06, 0.02), paper2(8003371), [0, -0.3, 0.375]);
42868
- 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]));
42869
43472
  return G(bottle2, label, goldBand, crossUpright, crossArm, foil);
42870
43473
  };
42871
43474
  var coconutWater = ({ mesh: mesh3, bx, cyl, lathe: lathe2, G, plastic, paper: paper2 }) => {
@@ -42874,7 +43477,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42874
43477
  const nut = mesh3(cyl(0.11, 0.11, 0.03, 10), paper2(7031339), [0, -0.12, 0.285], [Math.PI / 2, 0, 0]);
42875
43478
  const leafLeft = mesh3(bx(0.2, 0.05, 0.01), paper2(15135448), [-0.12, 0.08, 0.292], [0, 0, 0.5]);
42876
43479
  const leafRight = mesh3(bx(0.2, 0.05, 0.01), paper2(15135448), [0.12, 0.08, 0.292], [0, 0, -0.5]);
42877
- 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]));
42878
43481
  return G(bottle2, label, nut, leafLeft, leafRight, cap);
42879
43482
  };
42880
43483
  var dirtyWater = ({ mesh: mesh3, cyl, lathe: lathe2, G, plastic, paper: paper2, glass, food }) => {
@@ -42882,7 +43485,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
42882
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));
42883
43486
  const sediment = mesh3(cyl(0.31, 0.27, 0.12, 8), food(4865314), [0, -0.82, 0]);
42884
43487
  const label = mesh3(cyl(0.332, 0.332, 0.22, 12), paper2(13617588), [0, 0.38, 0]);
42885
- 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]));
42886
43489
  return G(bottle2, murk, sediment, label, cap);
42887
43490
  };
42888
43491
  var painkillers = ({ mesh: mesh3, bx, cyl, G, paper: paper2, metal: metal2, glass }) => {
@@ -45498,7 +46101,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
45498
46101
  const materials = kit.materials.slice(first);
45499
46102
  if (options.wear) setWear(materials, options.wear);
45500
46103
  root.userData.wear = options.wear ?? 0;
45501
- 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 });
45502
46105
  markKitOrigin(root, { owner: "game-items-three", kind: "item", id: item.id, ...kind === "shape" ? { standIn: true } : {} });
45503
46106
  return { root, kind, ...shape ? { shape } : {}, size: item.size, materials };
45504
46107
  }
@@ -48785,7 +49388,6 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
48785
49388
  var MAX_CASINGS = 120;
48786
49389
  var MAX_MAGAZINES = 12;
48787
49390
  var LIFETIME_S = 25;
48788
- var HAND_OFFSET = [0.2, -0.2, -0.42];
48789
49391
  var GRAVITY2 = 9.81;
48790
49392
  function createBoxFx(options) {
48791
49393
  const { THREE, scene, camera: camera2, arsenal, groundAt } = options;
@@ -48807,6 +49409,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
48807
49409
  let dip = 0;
48808
49410
  let reloading = false;
48809
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;
48810
49416
  const q = new THREE.Quaternion();
48811
49417
  const q2 = new THREE.Quaternion();
48812
49418
  const pointForward = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
@@ -48818,13 +49424,17 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
48818
49424
  if (!def || BODY_ACTIONS.has(def.id)) return;
48819
49425
  model = buildWeaponModel(THREE, def);
48820
49426
  const length2 = new THREE.Box3().setFromObject(model).getSize(v).x;
48821
- 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;
48822
49429
  model.quaternion.copy(pointForward);
48823
49430
  hand.add(model);
48824
49431
  inHand = def.id;
48825
- 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));
48826
49437
  };
48827
- 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);
48828
49438
  const throwBody = (kind, mesh3, from, velocity, radius) => {
48829
49439
  if (!(kind === "casing" ? layers.casings : layers.magazines)) {
48830
49440
  mesh3.geometry.dispose();
@@ -48935,10 +49545,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
48935
49545
  update(dt) {
48936
49546
  kick *= Math.exp(-dt * 14);
48937
49547
  dip += ((reloading ? 1 : 0) - dip) * Math.min(1, dt * 8);
48938
- if (inHand) {
48939
- 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));
48940
49552
  q.setFromEuler(new THREE.Euler(0.14 * kick - 0.25 * dip, 0, 0.6 * dip));
48941
- hand.quaternion.copy(camera2.quaternion).multiply(q);
49553
+ hand.quaternion.multiply(q);
48942
49554
  }
48943
49555
  for (const body of [...bodies]) {
48944
49556
  stepBody(body, dt);
@@ -57659,6 +58271,11 @@ void main(){
57659
58271
  var MAX_CONFORM_STEPS = 48;
57660
58272
  var MIN_CONFORM_STEPS = 16;
57661
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;
57662
58279
  function createMarkLayer(THREE, options) {
57663
58280
  const group = new THREE.Group();
57664
58281
  const textures = createMarkTextures(THREE, options.createCanvas);
@@ -57682,110 +58299,168 @@ void main(){
57682
58299
  const bloodMaterial = new THREE.MeshStandardMaterial({ color: 6031882, roughness: 0.15, metalness: 0.1, polygonOffset: true, polygonOffsetFactor: -3, polygonOffsetUnits: -3 });
57683
58300
  const bloodGeometry = new THREE.CircleGeometry(0.5, 9).rotateX(-Math.PI / 2);
57684
58301
  owned.push(bloodMaterial, bloodGeometry);
57685
- const rockMaterials = /* @__PURE__ */ new Map();
57686
- const blood = [];
57687
- let meshCount = 0;
57688
- function rockMaterial(color2) {
57689
- let material = rockMaterials.get(color2);
57690
- if (!material) {
57691
- material = new THREE.MeshStandardMaterial({ color: color2, roughness: 0.9, metalness: 0, flatShading: true });
57692
- rockMaterials.set(color2, material);
57693
- 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);
57694
58308
  }
57695
- 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();
57696
58352
  }
57697
58353
  function conform(mark, material) {
57698
58354
  const radius = mark.radius;
57699
58355
  const steps = Math.max(MIN_CONFORM_STEPS, Math.min(MAX_CONFORM_STEPS, Math.round(radius * 8)));
57700
- const geometry = new THREE.PlaneGeometry(2, 2, steps, steps).rotateX(-Math.PI / 2);
57701
- 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;
57702
58360
  const cos2 = Math.cos(mark.rotation);
57703
58361
  const sin2 = Math.sin(mark.rotation);
57704
- for (let index = 0; index < position.count; index++) {
57705
- const localX = position.getX(index) * radius;
57706
- 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;
57707
58365
  const worldX = mark.x + localX * cos2 - localZ * sin2;
57708
58366
  const worldZ = mark.z + localX * sin2 + localZ * cos2;
57709
- 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);
57710
58368
  }
58369
+ grid.needsUpdate = true;
57711
58370
  geometry.computeVertexNormals();
58371
+ geometry.attributes.normal.needsUpdate = true;
58372
+ geometry.boundingSphere = null;
58373
+ geometry.boundingBox = null;
57712
58374
  const mesh3 = new THREE.Mesh(geometry, material);
57713
58375
  mesh3.receiveShadow = true;
57714
- return mesh3;
58376
+ group.add(mesh3);
58377
+ groundMarks.push({ mesh: mesh3, steps });
57715
58378
  }
57716
58379
  function addRock(mark) {
57717
- const mesh3 = new THREE.Mesh(mark.plate ? options.plateGeometry : options.rockGeometry, rockMaterial(mark.color));
57718
- mesh3.position.set(mark.x, mark.y, mark.z);
57719
- mesh3.scale.set(mark.scale[0], mark.scale[1], mark.scale[2]);
57720
- mesh3.rotation.set(mark.rotation[0], mark.rotation[1], mark.rotation[2]);
57721
- mesh3.castShadow = true;
57722
- mesh3.receiveShadow = true;
57723
- 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;
57724
58395
  }
57725
58396
  function addBlood(mark) {
57726
- const mesh3 = new THREE.Mesh(bloodGeometry, bloodMaterial);
57727
- mesh3.position.set(mark.x, mark.y, mark.z);
57728
- mesh3.scale.set(mark.scaleX, 1, mark.scaleZ);
57729
- mesh3.rotation.y = mark.rotation;
57730
- mesh3.receiveShadow = true;
57731
- mesh3.scale.multiplyScalar(0.05);
57732
- blood.push({ mesh: mesh3, scaleX: mark.scaleX, scaleZ: mark.scaleZ, age: 0 });
57733
- 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);
57734
58402
  }
57735
58403
  function build(mark) {
57736
- if (mark.kind === "scorch") return conform(mark, scorchMaterials[mark.variant]);
57737
- if (mark.kind === "dirt") return conform(mark, dirtMaterial);
57738
- if (mark.kind === "rock") return addRock(mark);
57739
- return addBlood(mark);
57740
- }
57741
- function clearMeshes() {
57742
- for (const child of [...group.children]) {
57743
- group.remove(child);
57744
- const geometry = child.geometry;
57745
- if (geometry !== options.rockGeometry && geometry !== options.plateGeometry && geometry !== bloodGeometry) geometry.dispose();
57746
- }
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;
57747
58415
  blood.length = 0;
57748
- 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;
57749
58418
  }
57750
58419
  return {
57751
58420
  group,
57752
58421
  handle(event) {
57753
- if (event.type === "clear") {
57754
- clearMeshes();
57755
- return;
57756
- }
57757
- group.add(build(event.mark));
57758
- meshCount++;
58422
+ if (event.type === "clear") clearMarks();
58423
+ else build(event.mark);
58424
+ },
58425
+ prepare() {
58426
+ bloodMesh();
58427
+ rockMesh();
58428
+ plateMesh();
57759
58429
  },
57760
58430
  update(dt) {
57761
- for (const drawn of blood) {
57762
- 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;
57763
58434
  drawn.age += dt * BLOOD_GROW_RATE;
57764
- const ease3 = 1 - Math.pow(1 - Math.min(1, drawn.age), 2.5);
57765
- 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)));
57766
58436
  }
57767
58437
  },
57768
- count: () => meshCount,
58438
+ count: () => groundMarks.length + [instanced.blood, instanced.rock, instanced.plate].filter((mesh3) => mesh3 && mesh3.count > 0).length,
57769
58439
  dispose() {
57770
- clearMeshes();
58440
+ clearMarks();
58441
+ for (const mesh3 of [instanced.blood, instanced.rock, instanced.plate]) mesh3?.dispose();
57771
58442
  for (const item of owned) item.dispose();
57772
58443
  }
57773
58444
  };
57774
58445
  }
57775
58446
  var SPHERE_DEFAULT_WIDTH = 8;
57776
58447
  var SPHERE_DEFAULT_HEIGHT = 6;
58448
+ var MAX_PARKED_MESHES_PER_KIND = 256;
57777
58449
  function geometryOf(THREE, particle) {
57778
58450
  switch (particle.shape) {
57779
58451
  case "cone":
57780
- 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);
57781
58453
  case "cone_open":
57782
- 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);
57783
58455
  case "sphere":
57784
- 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);
57785
58457
  case "icosahedron":
57786
- return new THREE.IcosahedronGeometry(particle.radius, particle.segments);
58458
+ return new THREE.IcosahedronGeometry(1, particle.segments);
57787
58459
  }
57788
58460
  }
58461
+ function isCone(particle) {
58462
+ return particle.shape === "cone" || particle.shape === "cone_open";
58463
+ }
57789
58464
  function materialOf(THREE, particle) {
57790
58465
  if (particle.lit) return new THREE.MeshStandardMaterial({ color: particle.color, roughness: 1, metalness: 0, transparent: true, opacity: particle.opacity, depthWrite: false });
57791
58466
  return new THREE.MeshBasicMaterial({ color: particle.color, transparent: true, opacity: particle.opacity, depthWrite: false, ...particle.additive ? { blending: THREE.AdditiveBlending } : {} });
@@ -57796,51 +58471,82 @@ void main(){
57796
58471
  (kind) => new THREE.Mesh(geometry, materialOf(THREE, { ...kind, color: 8421504, opacity: 0.5 }))
57797
58472
  );
57798
58473
  }
58474
+ function geometryKeyOf(particle) {
58475
+ return `${particle.shape}:${particle.segments}:${particle.segments2}`;
58476
+ }
57799
58477
  function createMeshLayer(THREE, pool) {
57800
58478
  const group = new THREE.Group();
57801
58479
  group.name = "workshop-meshes";
57802
58480
  const drawn = /* @__PURE__ */ new Map();
58481
+ const geometries = /* @__PURE__ */ new Map();
58482
+ const parked = /* @__PURE__ */ new Map();
57803
58483
  const frame2 = new THREE.Quaternion();
57804
58484
  const euler = new THREE.Euler();
57805
58485
  const turn2 = new THREE.Quaternion();
57806
58486
  const offset = new THREE.Vector3();
57807
- function release(mesh3) {
57808
- mesh3.removeFromParent();
57809
- mesh3.geometry.dispose();
57810
- 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();
57811
58515
  }
57812
58516
  return {
57813
58517
  group,
57814
58518
  sync() {
57815
- const live = /* @__PURE__ */ new Set();
58519
+ stamp++;
57816
58520
  for (const particle of pool.particles) {
57817
- live.add(particle.id);
57818
- let mesh3 = drawn.get(particle.id);
57819
- if (!mesh3) {
57820
- mesh3 = new THREE.Mesh(geometryOf(THREE, particle), materialOf(THREE, particle));
57821
- mesh3.frustumCulled = false;
57822
- mesh3.renderOrder = particle.additive ? 5 : 3;
57823
- drawn.set(particle.id, mesh3);
57824
- group.add(mesh3);
58521
+ let entry2 = drawn.get(particle.id);
58522
+ if (!entry2) {
58523
+ entry2 = acquire(particle);
58524
+ drawn.set(particle.id, entry2);
57825
58525
  }
58526
+ entry2.stamp = stamp;
58527
+ const mesh3 = entry2.mesh;
57826
58528
  frame2.set(particle.frame[0], particle.frame[1], particle.frame[2], particle.frame[3]);
57827
58529
  offset.set(particle.at[0], particle.at[1], particle.at[2]).multiplyScalar(particle.scale).applyQuaternion(frame2);
57828
58530
  mesh3.position.set(particle.x + offset.x, particle.y + offset.y, particle.z + offset.z);
57829
58531
  turn2.setFromEuler(euler.set(particle.rx, particle.ry, particle.rz));
57830
58532
  mesh3.quaternion.copy(frame2).multiply(turn2);
57831
- 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);
57832
58534
  mesh3.material.opacity = particle.opacity;
57833
58535
  }
57834
- for (const [id, mesh3] of drawn) {
57835
- if (live.has(id)) continue;
57836
- release(mesh3);
58536
+ for (const [id, entry2] of drawn) {
58537
+ if (entry2.stamp === stamp) continue;
58538
+ park(entry2);
57837
58539
  drawn.delete(id);
57838
58540
  }
57839
58541
  return pool.count;
57840
58542
  },
57841
58543
  dispose() {
57842
- for (const mesh3 of drawn.values()) release(mesh3);
58544
+ for (const entry2 of drawn.values()) park(entry2);
57843
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();
57844
58550
  group.removeFromParent();
57845
58551
  }
57846
58552
  };
@@ -58408,7 +59114,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58408
59114
  const shocks = createShockLayer(THREE, pools.shocks);
58409
59115
  const sprites = createSpriteLayer(THREE, world.sprites(), options.pointScale ?? 700);
58410
59116
  const lights = createLightLayer(THREE, pools.lights, options.lightScale ?? Math.PI);
58411
- 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 });
58412
59118
  root.add(puffs.mesh, sparks.mesh, glows.mesh, chunks.rocks, chunks.plates, shocks.group, lights.group, sprites.additive, sprites.normal, marks.group);
58413
59119
  const stopMarks = world.onMarks((event) => marks.handle(event));
58414
59120
  const overlay = options.overlay ? createScreenOverlay(options.overlay) : void 0;
@@ -58434,6 +59140,16 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58434
59140
  const position = options.camera.position;
58435
59141
  world.setCamera([position.x, position.y, position.z]);
58436
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
+ }
58437
59153
  function syncAll() {
58438
59154
  lastDrawn.puffs = puffs.sync();
58439
59155
  lastDrawn.sparks = sparks.sync();
@@ -58442,15 +59158,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58442
59158
  lastDrawn.shocks = shocks.sync();
58443
59159
  lastDrawn.sprites = sprites.sync();
58444
59160
  lights.sync();
58445
- const engine = world.peekWorkshop();
58446
- if (engine && engine !== workshopEngine) {
58447
- workshopLayers?.dispose();
58448
- if (workshopLayers) root.remove(workshopLayers.group);
58449
- workshopEngine = engine;
58450
- workshopLayers = createWorkshopLayers(THREE, engine, options.pointScale ?? 700, 1);
58451
- root.add(workshopLayers.group);
58452
- applyLayers();
58453
- }
59161
+ attachWorkshopLayers();
58454
59162
  const workshopCalls = workshopLayers?.sync() ?? 0;
58455
59163
  const glow = pools.lights.glow;
58456
59164
  uniforms.uGlowPos.value.set(glow.x, glow.y, glow.z);
@@ -58503,6 +59211,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58503
59211
  },
58504
59212
  warmUpProbes() {
58505
59213
  releaseAnchors();
59214
+ marks.prepare();
59215
+ world.workshop();
59216
+ attachWorkshopLayers();
58506
59217
  const meshes = createMeshLayerProbes(THREE);
58507
59218
  for (const mesh3 of meshes) mesh3.frustumCulled = false;
58508
59219
  const group = new THREE.Group();
@@ -58569,6 +59280,14 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58569
59280
 
58570
59281
  // src/fx/heavy.ts
58571
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
+ }
58572
59291
  var flameMaterials = (kit) => {
58573
59292
  const { THREE } = kit;
58574
59293
  return [
@@ -58598,8 +59317,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58598
59317
  exhaust.rotation.z = PI14 / 2;
58599
59318
  exhaust.position.x = -0.08;
58600
59319
  rocket.add(exhaust);
58601
- const light = new THREE.PointLight(16751168, 3, 2.5);
58602
- rocket.add(light);
59320
+ const light = context.rocketLight;
59321
+ light.intensity = 3;
59322
+ rocketsInFlight.set(light, (rocketsInFlight.get(light) ?? 0) + 1);
58603
59323
  context.every(
58604
59324
  1.6,
58605
59325
  (progress, dt) => {
@@ -58607,6 +59327,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58607
59327
  const distance3 = 1.2 * flight + (flight > 0.25 ? 9 * (flight - 0.25) ** 2 : 0);
58608
59328
  rocket.position.copy(muzzle2).addScaledVector(forward, distance3);
58609
59329
  rocket.rotation.x += 0.25 * dt * 60;
59330
+ light.position.copy(rocket.position);
58610
59331
  exhaust.scale.set(1, 0.6 + random() * 0.8, 1);
58611
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);
58612
59333
  },
@@ -58614,7 +59335,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58614
59335
  rocket.removeFromParent();
58615
59336
  exhaust.geometry.dispose();
58616
59337
  exhaust.material.dispose();
58617
- 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);
58618
59343
  }
58619
59344
  );
58620
59345
  }
@@ -59429,11 +60154,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59429
60154
 
59430
60155
  // src/fx/viewmodel.ts
59431
60156
  var PI16 = Math.PI;
59432
- var HAND_OFFSET2 = [0.2, -0.2, -0.42];
59433
- var HANDGUN_OFFSET = [0.1, -0.12, -0.44];
59434
60157
  var HEAT_DISPLAY_MAX_FALLBACK = 120;
59435
- var WALL_PULL = { margin: 0.03, max: 0.7, rate: 14, probe: 3 };
59436
- 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 };
59437
60160
  var LAUNCH_VOLUME = 0.6;
59438
60161
  var fittedOf2 = (parts) => parts.map((part) => ({ id: part.id, mount: part.mount, ...part.integral ? { integral: true } : {}, ...part.on === void 0 ? {} : { on: part.on } }));
59439
60162
  var collapsedStockPose = (move2) => {
@@ -59463,6 +60186,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59463
60186
  const hand = new THREE.Group();
59464
60187
  hand.name = VIEWMODEL_NODE_NAME;
59465
60188
  scene.add(hand);
60189
+ const rocketLight = createRocketLight(THREE);
60190
+ scene.add(rocketLight);
59466
60191
  const holder = new THREE.Group();
59467
60192
  holder.quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), PI16 / 2);
59468
60193
  hand.add(holder);
@@ -59519,7 +60244,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59519
60244
  positionOf: (part) => worldPositionOf(current, part, new THREE.Vector3()),
59520
60245
  orientation: () => current.model.root.getWorldQuaternion(new THREE.Quaternion()),
59521
60246
  every: (seconds, step, done) => void tasks.push({ delay: 0, seconds, elapsed: 0, step, done }),
59522
- groundAt
60247
+ groundAt,
60248
+ rocketLight
59523
60249
  });
59524
60250
  const equip = (weapon) => {
59525
60251
  const def = arsenal.weapons.get(weapon);
@@ -59532,14 +60258,11 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59532
60258
  reach = 0;
59533
60259
  pull = 0;
59534
60260
  lift = 0;
59535
- handHeight = 0;
59536
- handDepth = 0;
59537
60261
  rigStock(THREE, model, def);
59538
60262
  rigUnderbarrel(THREE, model, def);
59539
- const length2 = new THREE.Box3().setFromObject(model.root).getSize(new THREE.Vector3()).x;
59540
- const scale = length2 > 0.8 ? 0.55 : length2 > 0.4 ? 0.75 : 1;
59541
- holder.scale.setScalar(scale);
59542
- 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;
59543
60266
  holder.add(model.root);
59544
60267
  const caliber = caliberOf(arsenal, def);
59545
60268
  const dims = caliber?.cartridge;
@@ -59551,7 +60274,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59551
60274
  def,
59552
60275
  caliber,
59553
60276
  model,
59554
- holderScale: scale,
60277
+ holderScale: 1,
59555
60278
  lengthMetres: length2,
59556
60279
  caseType: caliber?.caseType ?? "rifle",
59557
60280
  soundProfile: soundProfileOf(weapon, model.parts.family),
@@ -59581,7 +60304,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59581
60304
  applyCondition(kit, model, condition);
59582
60305
  lastAmmo = options.tool?.().ammo ?? 0;
59583
60306
  hand.updateMatrixWorld(true);
59584
- hold = holdOfModel(THREE, model, scale);
60307
+ hold = holdOfModel(THREE, model, 1);
59585
60308
  hold = { ...hold, works: handWorkOf(THREE, model, weapon, hold.right) };
59586
60309
  triggerRest = model.parts.trigger?.rotation.z ?? 0;
59587
60310
  lastShot = -Infinity;
@@ -59601,8 +60324,6 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59601
60324
  let reach = 0;
59602
60325
  let pull = 0;
59603
60326
  let lift = 0;
59604
- let handHeight = 0;
59605
- let handDepth = 0;
59606
60327
  const ahead = new THREE.Vector3();
59607
60328
  const up = new THREE.Vector3();
59608
60329
  const seen = new THREE.Vector3();
@@ -59615,19 +60336,31 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59615
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));
59616
60337
  return farthest;
59617
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
+ };
59618
60354
  const place = (dt = 0) => {
59619
- const family = session?.model.parts.family;
59620
- const [x, y, z] = family === "pistol" || family === "revolver" ? HANDGUN_OFFSET : HAND_OFFSET2;
59621
- hand.position.set(x, y, z).applyQuaternion(camera2.quaternion).add(camera2.position);
59622
- hand.quaternion.copy(camera2.quaternion);
59623
- if (session && handDepth > 0.05 && "fov" in camera2) {
59624
- const tanHalf = Math.tan((camera2.fov ?? 70) * PI16 / 360);
59625
- const want2 = Math.max(0, Math.min(HAND_IN_VIEW.max, lift + (HAND_IN_VIEW.floor - handHeight) * tanHalf * handDepth));
59626
- lift += (want2 - lift) * Math.min(1, dt * HAND_IN_VIEW.rate);
59627
- if (lift > 1e-4) hand.position.addScaledVector(up.set(0, 1, 0).applyQuaternion(camera2.quaternion), lift);
59628
- }
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 });
60359
+ hand.updateMatrixWorld(true);
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);
59629
60362
  hand.updateMatrixWorld(true);
59630
- if (!options.contact || !session) return;
60363
+ if (!options.contact) return;
59631
60364
  ahead.set(0, 0, -1).applyQuaternion(camera2.quaternion);
59632
60365
  if (reach === 0) reach = Math.max(0.01, measureReach(session));
59633
60366
  const free = options.contact(camera2.position, ahead, WALL_PULL.probe);
@@ -59741,12 +60474,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59741
60474
  const targets = targetsOf(THREE, hold, session.model.root.getWorldQuaternion(rootQuaternion), magazineHand, hold.works ? workWeights(hold.works, player?.running ?? []) : void 0);
59742
60475
  const trigger = session.model.parts.trigger;
59743
60476
  if (trigger) targets.fingers = { index: indexCurl(trigger.rotation.z - triggerRest, clock - lastShot) };
59744
- if (targets.R && "fov" in camera2) {
59745
- seen.set(...targets.R.position).sub(camera2.position).applyQuaternion(eyeTurn.copy(camera2.quaternion).invert());
59746
- handDepth = -seen.z;
59747
- handHeight = handDepth > 0.05 ? seen.y / handDepth / Math.tan((camera2.fov ?? 70) * PI16 / 360) : 0;
59748
- }
59749
- return targets;
60477
+ return limitSupportSpan(targets);
59750
60478
  },
59751
60479
  get inHand() {
59752
60480
  return session?.id ?? null;
@@ -59765,6 +60493,8 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59765
60493
  particles.dispose();
59766
60494
  ownVfx?.dispose();
59767
60495
  hand.removeFromParent();
60496
+ rocketLight.removeFromParent();
60497
+ rocketLight.dispose();
59768
60498
  }
59769
60499
  };
59770
60500
  }
@@ -59789,6 +60519,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
59789
60519
  ...options.attachmentModels ? { attachmentModels: options.attachmentModels } : {},
59790
60520
  ...options.raycast ? { raycast: options.raycast } : {},
59791
60521
  ...options.contact ? { contact: options.contact } : {},
60522
+ ...options.bodyHold ? { bodyHold: options.bodyHold } : {},
59792
60523
  ...options.effects ? { effects: options.effects } : {},
59793
60524
  ...options.flyRocket === void 0 ? {} : { flyRocket: options.flyRocket }
59794
60525
  });
@@ -60564,7 +61295,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
60564
61295
  var P13_VALVE_MESH = "valve";
60565
61296
  var P13_VALVE_COLOR = 12095802;
60566
61297
  var P13_WALL = 25e-4;
60567
- var SHAPES2 = {
61298
+ var SHAPES3 = {
60568
61299
  fuel_barrel: { shape: { kind: "cylinder", radius: 0.3, height: 0.9 }, position: [0, 0.45, 0], color: 3104666 },
60569
61300
  propane_p13: {
60570
61301
  shape: { kind: "cylinder", radius: 0.176, height: 0.49 },
@@ -60575,7 +61306,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
60575
61306
  },
60576
61307
  car_fuel_tank: { shape: { kind: "box", size: [0.9, 0.25, 0.5] }, position: [0, 0.4, 0], color: 4935509 }
60577
61308
  };
60578
- var WORLD_EXPLOSIVE_IDS = Object.keys(SHAPES2);
61309
+ var WORLD_EXPLOSIVE_IDS = Object.keys(SHAPES3);
60579
61310
  var SHELL = { thickness: 1e-3, fill: 0.03 };
60580
61311
  var explosivePart = (def, shape, scale) => ({
60581
61312
  id: "tank",
@@ -60621,7 +61352,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
60621
61352
  return [centre.x, centre.y, centre.z];
60622
61353
  };
60623
61354
  var worldExplosiveShell = (THREE, defId) => {
60624
- const shape = SHAPES2[defId];
61355
+ const shape = SHAPES3[defId];
60625
61356
  const model = shape ? modelOf(THREE, shape, 2) : void 0;
60626
61357
  if (!model) return void 0;
60627
61358
  model.traverse((node) => {
@@ -60634,7 +61365,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
60634
61365
  return model;
60635
61366
  };
60636
61367
  var worldExplosiveItem = (def, name, options = {}) => {
60637
- const shape = SHAPES2[def.id];
61368
+ const shape = SHAPES3[def.id];
60638
61369
  if (!shape) return void 0;
60639
61370
  const scale = () => options.scale?.() ?? 1;
60640
61371
  const spec = () => ({ name, parts: [explosivePart(def, shape, scale())], joints: [], tags: ["explosive"] });
@@ -61069,6 +61800,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
61069
61800
  raycast: createLaserRaycast(lent.THREE, lent.scene, lent.camera),
61070
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)
61071
61802
  contact: createLaserRaycast(lent.THREE, lent.scene, lent.camera, { now: () => simulatedMs }),
61803
+ ...lent.holdFrame ? { bodyHold: () => lent.holdFrame?.() ?? null } : {},
61072
61804
  ...lent.effects ? { effects: lent.effects } : {},
61073
61805
  ...options.attachmentModels ? { attachmentModels: options.attachmentModels } : {},
61074
61806
  ...options.condition ? { condition: options.condition } : {},
@@ -61881,7 +62613,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
61881
62613
  };
61882
62614
  for (const { kind, item } of PUSH_KINDS) PUSH_LAYOUT[kind].forEach(([distance3, degrees]) => lay(kind, item, bearing(centre, distance3, degrees)));
61883
62615
  if (mode === "grenade") {
61884
- lay("zombie", "zombie_walker", bearing(centre, 3.4, -34));
62616
+ lay("zombie", "zombie_walker", bearing(centre, 3.4, -130));
61885
62617
  lay("survivor", "person_police_officer", bearing(centre, 7.5, 78));
61886
62618
  lay("camp_table", "camp_table", bearing(centre, 3, 90));
61887
62619
  }
@@ -61924,7 +62656,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
61924
62656
  const outward = ((now[0] - from[0]) * (from[0] - blast.point[0]) + (now[2] - from[2]) * (from[2] - blast.point[2])) / away;
61925
62657
  return { ...body, distance: distance3, outward, moved: flat(now, from) };
61926
62658
  });
61927
- 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);
61928
62660
  const reached = rows.filter((row) => row.distance < blast.radius * 0.85 && !torn(row));
61929
62661
  const movedAway = reached.filter((row) => row.outward >= MOVED);
61930
62662
  const least = reached.length === 0 ? 0 : Math.min(...reached.map((row) => row.outward));
@@ -61937,9 +62669,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
61937
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 });
61938
62670
  }
61939
62671
  for (const row of reached.filter((entry2) => entry2.kind === "zombie" || entry2.kind === "survivor")) {
61940
- 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 } });
61941
62673
  }
61942
- 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);
61943
62675
  if (table) expect(table.moved >= MOVED, fillText2(text.push_heavy, { what: things[table.itemId] ?? table.itemId, moved: table.moved.toFixed(2) }), { table });
61944
62676
  }
61945
62677
  };