@adatechnology/game-weapons-three 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19681,6 +19681,62 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19681
19681
  return angle === 0 ? q : quat.mul(q, quat.fromAxisAngle([0, 0, 1], angle));
19682
19682
  };
19683
19683
  var clamp = (v, a, b) => Math.max(a, Math.min(b, v));
19684
+ var splinterBox = (inp, box5, limit = inp.maxFragments) => {
19685
+ const { rnd: rnd2 } = inp;
19686
+ const dims = box5.s;
19687
+ const [fmin, fmax] = inp.fragmentSize;
19688
+ const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
19689
+ const [a, b] = [0, 1, 2].filter((i) => i !== g);
19690
+ const w = Math.max(fmin * 1.5, 0.01);
19691
+ const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(limit / 2)));
19692
+ const maxLen = fmax * 4;
19693
+ const cells = bsp(box5, c, [a, b], rnd2);
19694
+ const pieces = [];
19695
+ for (const cell of cells) {
19696
+ const extra = rnd2() < 0.4 ? 1 : 0;
19697
+ const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
19698
+ for (const p of bsp(cell, k, [g], rnd2)) pieces.push(p);
19699
+ }
19700
+ return pieces.slice(0, limit).map((p) => ({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] }));
19701
+ };
19702
+ var severAt = (inp, root, at3, gap) => {
19703
+ const dims = root.s;
19704
+ const L = [0, 1, 2].sort((x, y) => dims[y] - dims[x])[0];
19705
+ const half = dims[L] / 2;
19706
+ const lo = clamp(at3 - gap / 2, -half, half), hi = clamp(at3 + gap / 2, -half, half);
19707
+ const out = [];
19708
+ const stub = (from, to) => {
19709
+ if (to - from < 0.02) return;
19710
+ const c = [root.c[0], root.c[1], root.c[2]], sz = [dims[0], dims[1], dims[2]];
19711
+ c[L] = (from + to) / 2;
19712
+ sz[L] = to - from;
19713
+ out.push({ center: c, size: sz, q: quat.identity(), shape: "slab", weight: sz[0] * sz[1] * sz[2] });
19714
+ };
19715
+ stub(-half, lo);
19716
+ stub(hi, half);
19717
+ const band = { c: [root.c[0], root.c[1], root.c[2]], s: [dims[0], dims[1], dims[2]] };
19718
+ band.c[L] = (lo + hi) / 2;
19719
+ band.s[L] = Math.max(hi - lo, 0.01);
19720
+ for (const p of splinterBox(inp, band, Math.max(3, inp.maxFragments - out.length))) out.push(p);
19721
+ return out;
19722
+ };
19723
+ var tearStack = (inp, root, t, u, v, n, sheet) => {
19724
+ const { dims, rnd: rnd2, maxFragments } = inp;
19725
+ const layers = clamp(Math.ceil(dims[t] / STACK_LAYER), 2, 4);
19726
+ const per = clamp(n, 3, Math.max(3, Math.floor(maxFragments / layers)));
19727
+ const out = [];
19728
+ for (let k = 0; k < layers; k++) {
19729
+ const off = -dims[t] / 2 + (k + 0.5) * dims[t] / layers;
19730
+ for (const b of bsp(root, per, [u, v], rnd2)) {
19731
+ if (out.length >= maxFragments) return out;
19732
+ const c = [b.c[0], b.c[1], b.c[2]];
19733
+ c[t] = off;
19734
+ out.push({ center: c, size: [b.s[u], b.s[v], sheet], q: frame(u, v, t, (rnd2() - 0.5) * 0.8), shape: "scrap", weight: b.s[u] * b.s[v], inner: out.length % 4 !== 0 });
19735
+ }
19736
+ }
19737
+ return out;
19738
+ };
19739
+ var STACK_LAYER = 0.012;
19684
19740
  var planFragments = (inp) => {
19685
19741
  const { style, dims, rnd: rnd2, maxFragments } = inp;
19686
19742
  const [fmin, fmax] = inp.fragmentSize;
@@ -19702,27 +19758,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19702
19758
  return out;
19703
19759
  }
19704
19760
  if (style === "splinter") {
19705
- const g = inp.grain ?? [0, 1, 2].sort((a2, b2) => dims[b2] - dims[a2])[0];
19706
- const [a, b] = [0, 1, 2].filter((i) => i !== g);
19707
- const w = Math.max(fmin * 1.5, 0.01);
19708
- const c = clamp(Math.round(dims[a] * dims[b] / (w * w)), 3, Math.max(3, Math.floor(maxFragments / 2)));
19709
- const maxLen = fmax * 4;
19710
- const cells = bsp(root, c, [a, b], rnd2);
19711
- const pieces = [];
19712
- for (const cell of cells) {
19713
- let list2 = [cell];
19714
- const extra = rnd2() < 0.4 ? 1 : 0;
19715
- const k = Math.max(1, Math.ceil(dims[g] / maxLen)) + extra;
19716
- list2 = bsp(cell, k, [g], rnd2);
19717
- for (const p of list2) pieces.push(p);
19718
- }
19719
- for (const p of pieces.slice(0, maxFragments)) {
19720
- out.push({ center: p.c, size: [p.s[g], p.s[a], p.s[b]], q: frame(g, a, b), shape: "splinter", weight: p.s[0] * p.s[1] * p.s[2] });
19721
- }
19761
+ if (inp.cut) return severAt(inp, root, inp.cut.at, inp.cut.gap);
19762
+ for (const p of splinterBox(inp, root)) out.push(p);
19722
19763
  return out;
19723
19764
  }
19724
19765
  if (style === "tear") {
19725
19766
  const n2 = clamp(Math.round(dims[u] * dims[v] / (mean * mean)), 3, maxFragments);
19767
+ const sheet = inp.sheet ?? 0;
19768
+ if (sheet > 0 && dims[t] > sheet * 4) return tearStack(inp, root, t, u, v, n2, sheet);
19726
19769
  const th2 = Math.min(inp.thickness, dims[t]);
19727
19770
  for (const b of bsp(root, n2, [u, v], rnd2)) {
19728
19771
  out.push({ center: [b.c[0], b.c[1], b.c[2]], size: [b.s[u], b.s[v], th2], q: frame(u, v, t), shape: "scrap", weight: b.s[u] * b.s[v] });
@@ -21305,6 +21348,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21305
21348
  0.03,
21306
21349
  0.15
21307
21350
  ],
21351
+ sheet: 1e-3,
21308
21352
  damageScale: {
21309
21353
  slash: 3
21310
21354
  },
@@ -21320,7 +21364,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21320
21364
  smoke: "grey"
21321
21365
  },
21322
21366
  visual: {
21323
- charColor: 1709072
21367
+ charColor: 1709072,
21368
+ innerColor: 15524048
21324
21369
  },
21325
21370
  tags: [
21326
21371
  "fabric",
@@ -21702,9 +21747,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21702
21747
  var BREACH_AREA = 0.25;
21703
21748
  var BREACH_BLAST_REACH = 0.12;
21704
21749
  var RESIDUAL_CAPACITY = 0.15;
21750
+ var SEVER_BAND = 0.1;
21751
+ var SEVER_ASPECT = 3;
21752
+ var SEVER_BALLISTIC = 2;
21705
21753
  var SUPPORT_TOLERANCE = 0.05;
21706
21754
  var UNIT_FRAGMENT_FACTOR = 4;
21707
21755
  var BREACH_GRID_CAP = 4096;
21756
+ var CHIP_ENERGY = 200;
21757
+ var CHIP_SPLASH = 0.5;
21708
21758
  var CUT_SLICE = 1;
21709
21759
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
21710
21760
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -21748,6 +21798,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
21748
21798
  let windCfg = windOpts ? resolveWind(windOpts) : null;
21749
21799
  const ignitionRules = resolveIgnitionRules(options.ignition);
21750
21800
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
21801
+ const chipEnergy = options.breach?.chipEnergy ?? CHIP_ENERGY;
21751
21802
  const rng2 = createRng2(options.seed ?? 1);
21752
21803
  const rnd2 = () => rng2.next();
21753
21804
  let time = 0;
@@ -22121,41 +22172,68 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22121
22172
  markDamaged(p);
22122
22173
  if (p.rt.hinge >= flexure.failRotationDeg * Math.PI / 180) hingeDrop(o, p, src);
22123
22174
  };
22175
+ const freeSpan = (p, supports) => {
22176
+ if (supports.length === 0) return Infinity;
22177
+ const d = p.dims;
22178
+ const L = [0, 1, 2].sort((a, b) => d[b] - d[a])[0];
22179
+ const spans = supports.map((k) => contactBox(p, k)).map(([lo, hi]) => [lo[L], hi[L]]);
22180
+ let far = 0;
22181
+ for (let i = 0; i <= 16; i++) {
22182
+ const x = (i / 16 - 0.5) * d[L];
22183
+ let near = Infinity;
22184
+ for (const [lo, hi] of spans) near = Math.min(near, Math.max(lo - x, x - hi, 0));
22185
+ far = Math.max(far, near);
22186
+ }
22187
+ return 2 * far;
22188
+ };
22124
22189
  const solveStructure = (o) => {
22125
22190
  o.maxStress = 0;
22126
22191
  if (!o.hasStructure) return;
22127
22192
  const parts = o.parts;
22128
22193
  const n = parts.length;
22129
22194
  const dist2 = new Array(n).fill(-1);
22130
- const queue = [];
22131
22195
  const ok = (i) => parts[i].structural && !isLost(parts[i]);
22132
22196
  const isPartition = (i) => parts[i].spec.structure?.role === "partition";
22133
- for (let i = 0; i < n; i++) if (ok(i) && parts[i].anchored) {
22134
- dist2[i] = 0;
22135
- queue.push(i);
22136
- }
22137
- for (let h = 0; h < queue.length; h++) {
22138
- const s = queue[h];
22139
- if (isPartition(s)) continue;
22140
- for (const j of o.byPart.get(parts[s].spec.id)) {
22141
- if (j.st.open || !j.bears) continue;
22142
- const r = j.ia === s ? j.ib : j.ia;
22143
- if (dist2[r] >= 0 || !ok(r) || restsOn(o, j, r) !== s) continue;
22144
- dist2[r] = dist2[s] + 1;
22145
- queue.push(r);
22197
+ const out = /* @__PURE__ */ new Set();
22198
+ let queue = [];
22199
+ let lowerOf = /* @__PURE__ */ new Map();
22200
+ for (; ; ) {
22201
+ dist2.fill(-1);
22202
+ queue = [];
22203
+ for (let i = 0; i < n; i++) if (ok(i) && !out.has(i) && parts[i].anchored) {
22204
+ dist2[i] = 0;
22205
+ queue.push(i);
22206
+ }
22207
+ for (let h = 0; h < queue.length; h++) {
22208
+ const s = queue[h];
22209
+ if (isPartition(s)) continue;
22210
+ for (const j of o.byPart.get(parts[s].spec.id)) {
22211
+ if (j.st.open || !j.bears) continue;
22212
+ const r = j.ia === s ? j.ib : j.ia;
22213
+ if (dist2[r] >= 0 || !ok(r) || out.has(r) || restsOn(o, j, r) !== s) continue;
22214
+ dist2[r] = dist2[s] + 1;
22215
+ queue.push(r);
22216
+ }
22146
22217
  }
22147
- }
22148
- const incoming = new Array(n).fill(0);
22149
- const lowerOf = /* @__PURE__ */ new Map();
22150
- for (const i of queue) {
22151
- const lower = [];
22152
- for (const j of o.byPart.get(parts[i].spec.id)) {
22153
- if (j.st.open || !j.bears) continue;
22154
- const k = restsOn(o, j, i);
22155
- if (k >= 0 && dist2[k] >= 0 && dist2[k] < dist2[i] && !isPartition(k)) lower.push(k);
22218
+ lowerOf = /* @__PURE__ */ new Map();
22219
+ let dropped = false;
22220
+ for (const i of queue) {
22221
+ const lower = [];
22222
+ for (const j of o.byPart.get(parts[i].spec.id)) {
22223
+ if (j.st.open || !j.bears) continue;
22224
+ const k = restsOn(o, j, i);
22225
+ if (k >= 0 && dist2[k] >= 0 && dist2[k] < dist2[i] && !isPartition(k)) lower.push(k);
22226
+ }
22227
+ lowerOf.set(i, lower);
22228
+ const max2 = parts[i].spec.structure?.maxSpan;
22229
+ if (max2 !== void 0 && !parts[i].anchored && freeSpan(parts[i], lower.map((k) => parts[k])) > max2) {
22230
+ out.add(i);
22231
+ dropped = true;
22232
+ }
22156
22233
  }
22157
- lowerOf.set(i, lower);
22234
+ if (!dropped) break;
22158
22235
  }
22236
+ const incoming = new Array(n).fill(0);
22159
22237
  const bending = new Array(n).fill(0);
22160
22238
  const through = parts.map(() => []);
22161
22239
  const pass = (k, at3, mass) => {
@@ -22667,6 +22745,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22667
22745
  const r = rayPart(o, p, origin, d);
22668
22746
  if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
22669
22747
  if (p.st.opening && insideOpening(o, p, vec.madd(origin, d, r.t0))) continue;
22748
+ if (p.st.chipped && insideChip(p, objLocal(o, vec.madd(origin, d, r.t0)))) continue;
22670
22749
  if (!best || r.t0 < best.distance) best = { object: o.id, part: p.spec.id, point: vec.madd(origin, d, r.t0), normal: r.n, distance: r.t0, pathLen: r.t1 - r.t0 };
22671
22750
  }
22672
22751
  }
@@ -22930,6 +23009,62 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22930
23009
  if (total < (p.spec.breach?.area ?? BREACH_AREA)) return;
22931
23010
  openBreach(o, p, [sum2[0] / total, sum2[1] / total, sum2[2] / total], total, energy, dir, src);
22932
23011
  };
23012
+ const unitGrids = /* @__PURE__ */ new WeakMap();
23013
+ const gridOf = (p) => {
23014
+ let grid = unitGrids.get(p);
23015
+ if (!grid) unitGrids.set(p, grid = unitGrid(p.dims, p.thin, unitsOf2(p), BREACH_GRID_CAP));
23016
+ return grid;
23017
+ };
23018
+ const insideChip = (p, objectPoint) => {
23019
+ const cells = p.st.chipped;
23020
+ if (!cells?.length) return false;
23021
+ const [u, v] = planeAxes(p.thin);
23022
+ const lp = toLocal(p.pos, p.q, objectPoint);
23023
+ 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);
23024
+ };
23025
+ const chipUnits = (o, p, wp, energy, dir, radius, src) => {
23026
+ var _a, _b;
23027
+ if (!p.spec.units || p.spec.reinforcement || !p.staged || chipEnergy <= 0 || energy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
23028
+ const [u, v] = planeAxes(p.thin);
23029
+ const lp = toLocal(p.pos, p.q, objLocal(o, wp));
23030
+ const grid = gridOf(p);
23031
+ const worn = (_a = p.rt).chips ?? (_a.chips = {});
23032
+ const out = [];
23033
+ for (let i = 0; i < grid.length; i++) {
23034
+ const cell = grid[i];
23035
+ if (worn[i] === -1) continue;
23036
+ const gap = Math.hypot(Math.max(0, Math.abs(lp[u] - cell.center[u]) - cell.size[u] / 2), Math.max(0, Math.abs(lp[v] - cell.center[v]) - cell.size[v] / 2));
23037
+ if (gap > radius) continue;
23038
+ const share = gap === 0 ? 1 : CHIP_SPLASH * (1 - gap / radius);
23039
+ const total = (worn[i] ?? 0) + energy * share;
23040
+ const mass = p.mat.density * cell.size[0] * cell.size[1] * cell.size[2];
23041
+ if (total < chipEnergy * mass) {
23042
+ worn[i] = total;
23043
+ continue;
23044
+ }
23045
+ worn[i] = -1;
23046
+ out.push(cell);
23047
+ }
23048
+ if (out.length === 0) return;
23049
+ const { c, q } = partXf(o, p);
23050
+ const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
23051
+ const holes = localHolesOf(p);
23052
+ for (const cell of out) {
23053
+ const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
23054
+ const center = toWorld(c, q, cell.center);
23055
+ const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
23056
+ const fragments = cellFragments(o, p, [cell], wp, dir, speed, mass, `c${chipped.length}_`);
23057
+ p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
23058
+ chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
23059
+ p.st.mass = Math.max(0, p.st.mass - mass);
23060
+ p.rt.removed = (p.rt.removed ?? 0) + mass;
23061
+ markDamaged(p);
23062
+ setStage(o, p, 1, {}, src);
23063
+ emit2({ ...base(o, src), type: "chip", part: p.spec.id, center, size: [cell.size[0], cell.size[1], cell.size[2]], rotation: q, fragments });
23064
+ debris2(o, p, p.mat.breakDebris, center, vec.scale(dir ?? [0, 1, 0], -1), 6, 2, src);
23065
+ }
23066
+ for (const cell of out) addBreach(o, p, toWorld(c, q, cell.center), cell.size[u] * cell.size[v], energy, dir, src);
23067
+ };
22933
23068
  const openBreach = (o, p, center, total, energy, dir, src) => {
22934
23069
  const [u, v] = planeAxes(p.thin);
22935
23070
  const radius = clamp2(Math.sqrt(total / Math.PI), Math.sqrt((p.spec.breach?.area ?? BREACH_AREA) / Math.PI), breachRadius);
@@ -22941,7 +23076,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
22941
23076
  failPart(o, p, worldCenter, dir, energy, 12, src);
22942
23077
  return;
22943
23078
  }
22944
- const cells = cellsWithin(unitGrid(p.dims, p.thin, unitsOf2(p), BREACH_GRID_CAP), p.thin, lc, radius);
23079
+ const worn = p.rt.chips;
23080
+ const cells = cellsWithin(gridOf(p).filter((_, i) => worn?.[i] !== -1), p.thin, lc, radius);
22945
23081
  const removed = p.st.mass * cells.reduce((sum2, cell) => sum2 + cell.size[u] * cell.size[v], 0) / (p.dims[u] * p.dims[v]);
22946
23082
  const speed = clamp2(Math.sqrt(2 * energy * 0.1 / Math.max(removed, 0.05)), 0.5, 8);
22947
23083
  const fragments = cellFragments(o, p, cells, worldCenter, dir, speed, removed, "b");
@@ -23025,6 +23161,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23025
23161
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
23026
23162
  const hitLocal = toLocal(c, q, hitW);
23027
23163
  const unitPlan = p.spec.units ? unitFragmentPlan(p, maxFragments * UNIT_FRAGMENT_FACTOR) : [];
23164
+ const axis2 = style === "splinter" ? severAxis(p) : -1;
23165
+ const cutAt2 = axis2 < 0 ? void 0 : p.rt.cutAt ?? (origin ? hitLocal[axis2] : void 0);
23028
23166
  const plan = unitPlan.length > 0 ? unitPlan : planFragments({
23029
23167
  style,
23030
23168
  dims: p.dims,
@@ -23033,7 +23171,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23033
23171
  hit: hitLocal,
23034
23172
  fragmentSize: p.mat.fragmentSize ?? [0.04, 0.2],
23035
23173
  maxFragments,
23036
- rnd: rnd2
23174
+ rnd: rnd2,
23175
+ sheet: p.mat.sheet,
23176
+ ...cutAt2 !== void 0 ? { cut: { at: cutAt2, gap: SEVER_BAND } } : {}
23037
23177
  });
23038
23178
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
23039
23179
  const mass = p.st.mass;
@@ -23057,7 +23197,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23057
23197
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd2(), dv[2] * k],
23058
23198
  angularVelocity: [(rnd2() - 0.5) * 12, (rnd2() - 0.5) * 12, (rnd2() - 0.5) * 12],
23059
23199
  mass: mass * f.weight / totW,
23060
- ...marks.length > 0 ? { marks } : {}
23200
+ ...marks.length > 0 ? { marks } : {},
23201
+ ...f.inner ? { inner: true } : {}
23061
23202
  };
23062
23203
  });
23063
23204
  const reinforcement = p.spec.reinforcement;
@@ -23091,10 +23232,33 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23091
23232
  emit2({ ...base(o, src), type: "primed", part: p.spec.id, point: partXf(o, p).c, trigger, delay });
23092
23233
  };
23093
23234
  const CHAIN_TRIGGER_OF = { ballistic: "shot", explosive: "explosion" };
23235
+ const severAxis = (p) => {
23236
+ if (p.mat.fracture !== "splinter" || p.spec.shape.kind !== "box") return -1;
23237
+ const d = p.dims;
23238
+ const order = [0, 1, 2].sort((a, b) => d[b] - d[a]);
23239
+ return d[order[0]] >= SEVER_ASPECT * d[order[1]] && d[order[0]] >= 3 * SEVER_BAND ? order[0] : -1;
23240
+ };
23241
+ const bandOf = (o, p, axis2, origin) => {
23242
+ const { c, q } = partXf(o, p);
23243
+ const along = toLocal(c, q, origin)[axis2];
23244
+ const L = p.dims[axis2];
23245
+ return clamp2(Math.floor((along + L / 2) / SEVER_BAND), 0, Math.ceil(L / SEVER_BAND) - 1);
23246
+ };
23094
23247
  const applyDamage = (o, p, amount, type, origin, dir, fragEnergy, cap, src) => {
23248
+ var _a;
23095
23249
  if (isLost(p) || amount <= 0) return 0;
23096
23250
  const prev = p.st.hp;
23097
23251
  p.st.hp -= amount;
23252
+ const axis2 = origin && type !== "fire" && type !== "heat" ? severAxis(p) : -1;
23253
+ if (axis2 >= 0 && p.st.hp > 0) {
23254
+ const band = bandOf(o, p, axis2, origin);
23255
+ const cuts = (_a = p.rt).cuts ?? (_a.cuts = {});
23256
+ cuts[band] = (cuts[band] ?? 0) + amount * (type === "ballistic" ? SEVER_BALLISTIC : 1);
23257
+ if (cuts[band] >= p.st.maxHp * SEVER_BAND / p.dims[axis2]) {
23258
+ p.rt.cutAt = -p.dims[axis2] / 2 + (band + 0.5) * SEVER_BAND;
23259
+ p.st.hp = 0;
23260
+ }
23261
+ }
23098
23262
  markDamaged(p);
23099
23263
  const trigger = CHAIN_TRIGGER_OF[type];
23100
23264
  const chained = trigger !== void 0 && chainDelayOf(p.spec, trigger) !== void 0;
@@ -23133,7 +23297,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23133
23297
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round5 = {}) => {
23134
23298
  const mat = p.mat;
23135
23299
  const piercing = round5.piercing ?? 1;
23136
- if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry2))) {
23300
+ if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry2)) || insideChip(p, objLocal(o, entry2))) {
23137
23301
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry2, d, Math.min(pathLen, p.thickness)), damage: 0 };
23138
23302
  }
23139
23303
  const sin2 = Math.min(1, Math.abs(vec.dot(d, n)));
@@ -23192,6 +23356,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23192
23356
  debris2(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
23193
23357
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
23194
23358
  const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry2, d, E * 0.1, 12, src);
23359
+ chipUnits(o, p, entry2, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
23195
23360
  return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
23196
23361
  };
23197
23362
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
@@ -23219,6 +23384,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
23219
23384
  const scale = mat.damageScale?.[kind] ?? 1;
23220
23385
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
23221
23386
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
23387
+ if (kind === "blunt") chipUnits(o, p, point, E, d, Math.max(contactR, 0.03), src);
23222
23388
  return done;
23223
23389
  };
23224
23390
  const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
@@ -33237,12 +33403,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33237
33403
  A.invalidate = () => {
33238
33404
  for (let i = 0; i < NS; i++) PL[i].shadow.needsUpdate = true;
33239
33405
  };
33240
- A.update = function(houses, focus, op) {
33406
+ A.update = function(houses2, focus, op) {
33241
33407
  op = op || {};
33242
- houses = (Array.isArray(houses) ? houses : [houses]).filter(Boolean);
33408
+ houses2 = (Array.isArray(houses2) ? houses2 : [houses2]).filter(Boolean);
33243
33409
  _f.set(focus.x, focus.y, focus.z);
33244
33410
  const rooms = [], exterior = [];
33245
- for (const house of houses) {
33411
+ for (const house of houses2) {
33246
33412
  if (house.group && !house.group.visible) continue;
33247
33413
  house.house.updateMatrixWorld(true);
33248
33414
  const M = house.house.matrixWorld, by = /* @__PURE__ */ new Map();
@@ -33621,7 +33787,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33621
33787
  }
33622
33788
  return null;
33623
33789
  };
33624
- function courses(part, units, opening, paint2) {
33790
+ const local2 = (p) => {
33791
+ house.updateMatrixWorld(true);
33792
+ const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
33793
+ return [v.x, v.y, v.z];
33794
+ };
33795
+ function courses(part, units, opening, chips2, paint2) {
33625
33796
  const [u0, v0, u1, v1] = part.aabb;
33626
33797
  const alongU = u1 - u0 >= v1 - v0;
33627
33798
  const len = alongU ? u1 - u0 : v1 - v0;
@@ -33645,23 +33816,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33645
33816
  const x = alongU ? u0 + mid : (u0 + u1) / 2;
33646
33817
  const z = alongU ? (v0 + v1) / 2 : v0 + mid;
33647
33818
  const y = part.y0 + (r + 0.5) * rowH;
33648
- const d = Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]);
33649
- if (d < opening.radius) continue;
33650
- list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: d < opening.radius + 0.25 || paint2 === null ? edge : paint2 });
33819
+ const d = opening ? Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]) : Infinity;
33820
+ if (opening && d < opening.radius) continue;
33821
+ const gone = (c) => Math.abs(c[1] - y) <= rowH / 2 && (alongU ? c[0] >= u0 + a && c[0] <= u0 + b : c[2] >= v0 + a && c[2] <= v0 + b);
33822
+ if (chips2.some(gone)) continue;
33823
+ const near = opening && d < opening.radius + 0.25 || chips2.some((c) => Math.hypot(c[0] - x, c[1] - y, c[2] - z) < 0.15);
33824
+ list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: near || paint2 === null ? edge : paint2 });
33651
33825
  }
33652
33826
  }
33653
33827
  return list2;
33654
33828
  }
33655
- function open(event) {
33656
- if (lod !== 0 || views.has(event.part) || !event.opening) return false;
33657
- const part = structure.byId.get(event.part);
33658
- const spec = opts.spec()?.parts?.find((q) => q.id === event.part);
33829
+ function draw2(partId, change) {
33830
+ if (lod !== 0) return false;
33831
+ const part = structure.byId.get(partId);
33832
+ const spec = opts.spec()?.parts?.find((q) => q.id === partId);
33659
33833
  if (!part || !spec || !spec.units) return false;
33660
- const items = structure.meshesOf(event.part).filter((it) => it.mesh);
33661
- if (!items.length) return false;
33662
- const list2 = courses(part, spec.units, event.opening, wallColour(items));
33834
+ let view = views.get(partId);
33835
+ if (!view) {
33836
+ const items = structure.meshesOf(partId).filter((it) => it.mesh);
33837
+ if (!items.length) return false;
33838
+ view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: [] };
33839
+ }
33840
+ if (change.opening) {
33841
+ if (view.opening) return false;
33842
+ view.opening = { center: local2(change.opening.center), radius: change.opening.radius };
33843
+ }
33844
+ if (change.chip) view.chips.push(local2(change.chip));
33845
+ const list2 = courses(part, spec.units, view.opening, view.chips, view.paint);
33846
+ if (view.mesh) {
33847
+ view.mesh.removeFromParent();
33848
+ view.mesh.dispose();
33849
+ }
33663
33850
  const mesh3 = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
33664
- mesh3.name = "breach:" + event.part;
33851
+ mesh3.name = "breach:" + partId;
33665
33852
  list2.forEach((u, i) => {
33666
33853
  tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
33667
33854
  mesh3.setMatrixAt(i, tmp.m);
@@ -33673,8 +33860,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33673
33860
  mesh3.castShadow = true;
33674
33861
  mesh3.receiveShadow = true;
33675
33862
  house.add(mesh3);
33676
- const saved = items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
33677
- views.set(event.part, { mesh: mesh3, saved, units: list2.length, opening: event.opening });
33863
+ if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
33864
+ view.mesh = mesh3;
33865
+ view.units = list2.length;
33866
+ views.set(partId, view);
33678
33867
  invalidate();
33679
33868
  invalidateShadow();
33680
33869
  return true;
@@ -33688,7 +33877,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33688
33877
  return {
33689
33878
  /** Handles a `stage` event of the house's object. Returns true when the wall was redrawn as courses. */
33690
33879
  stage(event) {
33691
- return event.stage === 2 ? open(event) : false;
33880
+ return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
33881
+ },
33882
+ /** Handles a `chip` event: the unit that came out leaves a gap in the courses. Returns true when the wall was redrawn. */
33883
+ chip(event) {
33884
+ return draw2(event.part, { chip: event.center });
33692
33885
  },
33693
33886
  /** The part broke or fell: the courses go (the plain range stays collapsed, the renderer hides the part). */
33694
33887
  drop(partId) {
@@ -33918,6 +34111,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33918
34111
  case "stage":
33919
34112
  breach.stage(e);
33920
34113
  break;
34114
+ case "chip":
34115
+ breach.chip(e);
34116
+ break;
33921
34117
  case "collapse":
33922
34118
  for (const id of e.parts) {
33923
34119
  breach.drop(id);
@@ -33972,6 +34168,32 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
33972
34168
  if (D.active && D.world.has(D.id)) D.world.remove(D.id);
33973
34169
  D.active = false;
33974
34170
  };
34171
+ D.follow = (world, id, spec) => {
34172
+ D.spec = spec;
34173
+ const offFollow = world.on("*", (e) => {
34174
+ if (e.object !== id) return;
34175
+ switch (e.type) {
34176
+ case "stage":
34177
+ breach.stage(e);
34178
+ break;
34179
+ case "chip":
34180
+ breach.chip(e);
34181
+ break;
34182
+ case "fracture":
34183
+ breach.drop(e.part);
34184
+ break;
34185
+ case "collapse":
34186
+ for (const p of e.parts) breach.drop(p);
34187
+ break;
34188
+ default:
34189
+ break;
34190
+ }
34191
+ });
34192
+ return { restore: () => breach.restore(), dispose: () => {
34193
+ offFollow();
34194
+ breach.restore();
34195
+ } };
34196
+ };
33975
34197
  D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
33976
34198
  D.state = () => D.active ? D.world.state(D.id) : null;
33977
34199
  D.reset = () => {
@@ -34694,6 +34916,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34694
34916
  return buildApi(K, shared);
34695
34917
  }
34696
34918
  var WOOD_THIN = 0.035;
34919
+ var SHELF_SPAN = 1.5;
34920
+ var SHELVING = /shelf|bookcase|rack/;
34921
+ function carcass(parts, drawn) {
34922
+ const all = [...drawn.values()];
34923
+ const width = Math.max(...all.map((p) => p.max[0])) - Math.min(...all.map((p) => p.min[0]));
34924
+ for (const part of parts) {
34925
+ if (LOOSE.has(part.material) || part.shape.kind !== "box" || part.rotation) continue;
34926
+ const [x, y, z] = part.shape.size;
34927
+ if (y >= x && y >= z) {
34928
+ if (z < x && x >= 0.6 * width) continue;
34929
+ part.structure = { role: "column" };
34930
+ } else if (y <= x && y <= z) part.structure = { role: "beam", maxSpan: SHELF_SPAN * Math.max(x, z) };
34931
+ }
34932
+ }
34697
34933
  var FLOOR_TOUCH = 5e-3;
34698
34934
  var LOOSE = /* @__PURE__ */ new Set(["paper"]);
34699
34935
  var BASE_MATERIAL = {
@@ -34809,6 +35045,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34809
35045
  if (area3 < 0.6 * footprint || p.max[1] < 0.2) continue;
34810
35046
  if (!top || p.max[1] > top.y + 1e-6) top = { part: p.id, y: p.max[1] };
34811
35047
  }
35048
+ if (opts.carcass) carcass(parts, byId);
34812
35049
  const spec = { name, parts, joints, tags: ["furniture"] };
34813
35050
  const colors = new Map(drawn.map((p) => [p.id, p.color]));
34814
35051
  const build = (T3) => {
@@ -34852,7 +35089,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34852
35089
  if (w === void 0 || d === void 0 || h === void 0) throw new Error(`game-floorplan-three: furniture type "${type}" is not in the catalogue, so give w, d and h`);
34853
35090
  const o = opts.options ?? {};
34854
35091
  const seed = Math.round(opts.seed ?? 1);
34855
- const plain2 = /shelf|bookcase|rack/.test(type) ? "paper" : "plastic";
35092
+ const plain2 = SHELVING.test(type) ? "paper" : "plastic";
34856
35093
  const root = { x: seed, z: 0, ry: 0, w, d, h, y: 0, base: 0, palette: o.palette, seed: `furniture_${type}_${seed}` };
34857
35094
  const { K, shared } = renderer.internals;
34858
35095
  const before = K.PARTS.length;
@@ -34871,7 +35108,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
34871
35108
  shared.ctx = saved.ctx;
34872
35109
  }
34873
35110
  },
34874
- { materials: { plain: plain2, ...o.materials }, chairs: o.chairs }
35111
+ { materials: { plain: plain2, ...o.materials }, chairs: o.chairs, carcass: SHELVING.test(type) }
34875
35112
  );
34876
35113
  const spouts = K.PARTS.slice(before).filter((p) => p.type === "water_tap").map((p) => [p.pos[0] - root.x, p.pos[1], p.pos[2] - root.z]);
34877
35114
  K.PARTS.length = before;
@@ -37811,6 +38048,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
37811
38048
  /** Closure of the fingers on a grip and on a forend (0 open, 1 closed). */
37812
38049
  closed: 0.9,
37813
38050
  forend: 0.6,
38051
+ /** How far the shooting hand slides on its grip so the thumb reaches the safety lever. */
38052
+ thumbReach: 0.02,
37814
38053
  /** How fast the supporting hand goes to the magazine and back, per second. */
37815
38054
  magRate: 7,
37816
38055
  /** The hands shrink with the tool: a tool drawn at scale `s` takes hands at `s ** handScaleExponent`. */
@@ -48497,9 +48736,14 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
48497
48736
  );
48498
48737
  return [misfire, ...stopped];
48499
48738
  }
48739
+ var SAFETY_MOTION = { cant: 0.22, lift: 0.012, in: 0.14, flickAt: 0.16, flick: 0.1, outAt: 0.32, out: 0.16, lever: 0.6 };
48500
48740
  function safetyClips(context) {
48501
- if (!context.parts.safety) return [];
48502
- return [composeClip("safety_on", [turn("safety", "z", 0, 0.12, 0, -0.6)]), composeClip("safety_off", [turn("safety", "z", 0, 0.12, -0.6, 0)]), composeClip("mode", [turn("safety", "z", 0, 0.07, 0, 0.4), turn("safety", "z", 0.07, 0.07, 0.4, 0)])];
48741
+ const { cant, lift, in: rise, flickAt, flick, outAt, out, lever } = SAFETY_MOTION;
48742
+ const carry = () => [turn(ROOT, "x", 0, rise, 0, cant), move(ROOT, "y", 0, rise, 0, lift), turn(ROOT, "x", outAt, out, cant, 0), move(ROOT, "y", outAt, out, lift, 0)];
48743
+ const flickTo = (from, to) => context.parts.safety ? [turn("safety", "z", flickAt, flick, from, to)] : [];
48744
+ const clips = [composeClip("safety_on", [...carry(), ...flickTo(0, -lever)]), composeClip("safety_off", [...carry(), ...flickTo(-lever, 0)])];
48745
+ if (context.parts.safety) clips.push(composeClip("mode", [turn("safety", "z", 0, 0.07, 0, 0.4), turn("safety", "z", 0.07, 0.07, 0.4, 0)]));
48746
+ return clips;
48503
48747
  }
48504
48748
  var drawClip = () => composeClip("draw", [move(ROOT, "y", 0, 0.35, -0.1, 0), turn(ROOT, "z", 0, 0.35, -0.6, 0)]);
48505
48749
 
@@ -58270,8 +58514,169 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58270
58514
  hand: "L",
58271
58515
  anchor: "cylinder_side",
58272
58516
  merge: 1
58517
+ },
58518
+ {
58519
+ family: "pistol",
58520
+ clips: [
58521
+ "safety_on",
58522
+ "safety_off"
58523
+ ],
58524
+ hand: "R",
58525
+ anchor: "thumb_safety",
58526
+ whole: true,
58527
+ lead: 0.3,
58528
+ tail: 0.3
58529
+ },
58530
+ {
58531
+ family: "revolver",
58532
+ clips: [
58533
+ "safety_on",
58534
+ "safety_off"
58535
+ ],
58536
+ hand: "R",
58537
+ anchor: "thumb_safety",
58538
+ whole: true,
58539
+ lead: 0.3,
58540
+ tail: 0.3
58541
+ },
58542
+ {
58543
+ family: "rifle",
58544
+ clips: [
58545
+ "safety_on",
58546
+ "safety_off"
58547
+ ],
58548
+ hand: "R",
58549
+ anchor: "thumb_safety",
58550
+ whole: true,
58551
+ lead: 0.3,
58552
+ tail: 0.3,
58553
+ style: "ar"
58554
+ },
58555
+ {
58556
+ family: "p90",
58557
+ clips: [
58558
+ "safety_on",
58559
+ "safety_off"
58560
+ ],
58561
+ hand: "R",
58562
+ anchor: "thumb_safety",
58563
+ whole: true,
58564
+ lead: 0.3,
58565
+ tail: 0.3
58566
+ },
58567
+ {
58568
+ family: "pump",
58569
+ clips: [
58570
+ "safety_on",
58571
+ "safety_off"
58572
+ ],
58573
+ hand: "R",
58574
+ anchor: "thumb_safety",
58575
+ whole: true,
58576
+ lead: 0.3,
58577
+ tail: 0.3
58578
+ },
58579
+ {
58580
+ family: "semi12",
58581
+ clips: [
58582
+ "safety_on",
58583
+ "safety_off"
58584
+ ],
58585
+ hand: "R",
58586
+ anchor: "thumb_safety",
58587
+ whole: true,
58588
+ lead: 0.3,
58589
+ tail: 0.3
58590
+ },
58591
+ {
58592
+ family: "semiint",
58593
+ clips: [
58594
+ "safety_on",
58595
+ "safety_off"
58596
+ ],
58597
+ hand: "R",
58598
+ anchor: "thumb_safety",
58599
+ whole: true,
58600
+ lead: 0.3,
58601
+ tail: 0.3
58602
+ },
58603
+ {
58604
+ family: "break",
58605
+ clips: [
58606
+ "safety_on",
58607
+ "safety_off"
58608
+ ],
58609
+ hand: "R",
58610
+ anchor: "thumb_safety",
58611
+ whole: true,
58612
+ lead: 0.3,
58613
+ tail: 0.3
58614
+ },
58615
+ {
58616
+ family: "bolt",
58617
+ clips: [
58618
+ "safety_on",
58619
+ "safety_off"
58620
+ ],
58621
+ hand: "R",
58622
+ anchor: "thumb_safety",
58623
+ whole: true,
58624
+ lead: 0.3,
58625
+ tail: 0.3
58626
+ },
58627
+ {
58628
+ family: "boltmag",
58629
+ clips: [
58630
+ "safety_on",
58631
+ "safety_off"
58632
+ ],
58633
+ hand: "R",
58634
+ anchor: "thumb_safety",
58635
+ whole: true,
58636
+ lead: 0.3,
58637
+ tail: 0.3
58638
+ },
58639
+ {
58640
+ family: "lever",
58641
+ clips: [
58642
+ "safety_on",
58643
+ "safety_off"
58644
+ ],
58645
+ hand: "R",
58646
+ anchor: "thumb_safety",
58647
+ whole: true,
58648
+ lead: 0.3,
58649
+ tail: 0.3
58650
+ },
58651
+ {
58652
+ family: "belt",
58653
+ clips: [
58654
+ "safety_on",
58655
+ "safety_off"
58656
+ ],
58657
+ hand: "R",
58658
+ anchor: "thumb_safety",
58659
+ whole: true,
58660
+ lead: 0.3,
58661
+ tail: 0.3
58662
+ },
58663
+ {
58664
+ family: "rifle",
58665
+ clips: [
58666
+ "safety_on",
58667
+ "safety_off"
58668
+ ],
58669
+ hand: "L",
58670
+ anchor: "lever_right",
58671
+ style: "ak",
58672
+ whole: true,
58673
+ lead: 0.3,
58674
+ tail: 0.3
58273
58675
  }
58274
- ]
58676
+ ],
58677
+ safety: {
58678
+ comment: "Safety on and off (B): the shooting hand slides up its grip so the thumb flicks the lever while the clip cants the weapon; an AK-style rifle (akCharge) has its big lever on the right, pushed by the support hand over the top."
58679
+ }
58275
58680
  };
58276
58681
 
58277
58682
  // src/fx/hand-work.ts
@@ -58299,6 +58704,18 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
58299
58704
  if (!right || !parts.slide) return null;
58300
58705
  return { part: right.part, offset: right.offset.clone().add(V2(THREE, 0, 0.012, 0)), quaternion: right.quaternion };
58301
58706
  }
58707
+ if (kind === "thumb_safety") {
58708
+ if (!right) return null;
58709
+ const toward2 = parts.safety ? right.part.worldToLocal(parts.safety.getWorldPosition(V2(THREE, 0, 0, 0))).sub(right.offset) : V2(THREE, 0, HAND_HOLD.thumbReach, 0);
58710
+ return { part: right.part, offset: right.offset.clone().add(toward2.clampLength(0, HAND_HOLD.thumbReach)), quaternion: right.quaternion };
58711
+ }
58712
+ if (kind === "lever_right") {
58713
+ const receiver = parts.bolt ?? parts.boltHandle;
58714
+ if (!receiver) return null;
58715
+ const b2 = boxIn(THREE, root, receiver);
58716
+ const x = parts.trigger ? root.worldToLocal(parts.trigger.getWorldPosition(V2(THREE, 0, 0, 0))).x : b2.min.x;
58717
+ return anchorAt(receiver, V2(THREE, x, b2.getCenter(V2(THREE, 0, 0, 0)).y, b2.max.z + palm), handFrame(THREE, V2(THREE, 1, 0, 0), V2(THREE, 0, 0, 1)));
58718
+ }
58302
58719
  if (!part) return null;
58303
58720
  const b = boxIn(THREE, root, part);
58304
58721
  const c = b.getCenter(V2(THREE, 0, 0, 0));