@adatechnology/game-weapons-three 0.10.4 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1511,6 +1511,83 @@ var createLaserRaycast = (THREE, scene, camera, options = {}) => {
1511
1511
  };
1512
1512
  };
1513
1513
 
1514
+ // src/fx/body-hold.ts
1515
+ var BODY_HOLD = {
1516
+ /** 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). */
1517
+ eyeToPocket: [0.2, -0.275, 0.17],
1518
+ /** A shouldered gun's bore runs this far above the shoulder pocket (the cheek on the comb). */
1519
+ boreAbove: 0.05,
1520
+ /** 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. */
1521
+ handgunGrip: [-0.15, -0.04, -0.42],
1522
+ /** A one-hand tool's grip: low in front of the body, the elbow bent. */
1523
+ toolGrip: [0.02, -0.34, -0.3],
1524
+ /** A tool at least this long, held by two hands, is shouldered (its stock in the pocket). */
1525
+ shoulderedFrom: 0.55,
1526
+ /** 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. */
1527
+ shoulderGripMax: 0.42,
1528
+ /** 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. */
1529
+ shoulderGripMin: 0.25,
1530
+ /** Where such a grip is brought to, ahead of the pocket. */
1531
+ shoulderGripTo: 0.34,
1532
+ /** The farthest apart two hands hold a long gun (a support hand past it slides back along the gun, toward the grip). */
1533
+ supportSpan: 0.5,
1534
+ /** How much of the aim's pitch a tool held low in one hand follows. */
1535
+ toolPitch: 0.5
1536
+ };
1537
+ var scratch = { a: null, b: null, euler: null };
1538
+ var placeOnBody = (placement) => {
1539
+ const { THREE, hand, holder, frame } = placement;
1540
+ scratch.a ?? (scratch.a = new THREE.Vector3());
1541
+ scratch.b ?? (scratch.b = new THREE.Vector3());
1542
+ holder.scale.setScalar(1);
1543
+ hand.position.set(...frame.position);
1544
+ hand.quaternion.set(...frame.quaternion);
1545
+ const shouldered = !placement.handgun && placement.left !== null && placement.lengthMetres >= BODY_HOLD.shoulderedFrom;
1546
+ if (shouldered) {
1547
+ holder.position.set(0, BODY_HOLD.boreAbove, placement.rear);
1548
+ hand.updateMatrixWorld(true);
1549
+ if (!placement.right) return;
1550
+ const grip2 = hand.worldToLocal(placement.right.part.localToWorld(scratch.a.copy(placement.right.offset)));
1551
+ if (-grip2.z <= BODY_HOLD.shoulderGripMax && -grip2.z >= BODY_HOLD.shoulderGripMin) return;
1552
+ holder.position.z += -grip2.z - BODY_HOLD.shoulderGripTo;
1553
+ hand.updateMatrixWorld(true);
1554
+ return;
1555
+ }
1556
+ scratch.euler ?? (scratch.euler = new THREE.Euler());
1557
+ const turn2 = scratch.euler.setFromQuaternion(hand.quaternion, "YXZ");
1558
+ hand.quaternion.setFromEuler(turn2.set(turn2.x * BODY_HOLD.toolPitch, turn2.y, 0, "YXZ"));
1559
+ holder.position.set(0, 0, 0);
1560
+ hand.updateMatrixWorld(true);
1561
+ if (!placement.right) return;
1562
+ const grip = placement.handgun || placement.left ? BODY_HOLD.handgunGrip : BODY_HOLD.toolGrip;
1563
+ const want = scratch.a.set(grip[0], grip[1], grip[2]).applyQuaternion(hand.quaternion).add(hand.position);
1564
+ const now = placement.right.part.localToWorld(scratch.b.copy(placement.right.offset));
1565
+ hand.position.add(want.sub(now));
1566
+ hand.updateMatrixWorld(true);
1567
+ };
1568
+ var limitSupportSpan = (targets) => {
1569
+ const right = targets.R?.position;
1570
+ const left = targets.L?.position;
1571
+ if (!right || !left || !targets.L) return targets;
1572
+ const span = Math.hypot(left[0] - right[0], left[1] - right[1], left[2] - right[2]);
1573
+ if (span <= BODY_HOLD.supportSpan) return targets;
1574
+ const k = BODY_HOLD.supportSpan / span;
1575
+ const position = [right[0] + (left[0] - right[0]) * k, right[1] + (left[1] - right[1]) * k, right[2] + (left[2] - right[2]) * k];
1576
+ return { ...targets, L: { ...targets.L, position } };
1577
+ };
1578
+ var createEyeFrame = (THREE, camera) => {
1579
+ const frame = { position: [0, 0, 0], quaternion: [0, 0, 0, 1] };
1580
+ const turn2 = new THREE.Euler(0, 0, 0, "YXZ");
1581
+ const pocket = new THREE.Vector3();
1582
+ return () => {
1583
+ turn2.setFromQuaternion(camera.quaternion, "YXZ");
1584
+ pocket.set(...BODY_HOLD.eyeToPocket).applyEuler(turn2.set(0, turn2.y, 0)).add(camera.position);
1585
+ frame.position = [pocket.x, pocket.y, pocket.z];
1586
+ frame.quaternion = [camera.quaternion.x, camera.quaternion.y, camera.quaternion.z, camera.quaternion.w];
1587
+ return frame;
1588
+ };
1589
+ };
1590
+
1514
1591
  // src/fx/hand-hold.ts
1515
1592
  var HAND_HOLD = {
1516
1593
  /** Half the width of a pistol grip, the palm sits this far (plus the palm) off its middle. */
@@ -1539,9 +1616,7 @@ var HAND_HOLD = {
1539
1616
  /** How far the shooting hand slides on its grip so the thumb reaches the safety lever. */
1540
1617
  thumbReach: 0.02,
1541
1618
  /** How fast the supporting hand goes to the magazine and back, per second. */
1542
- magRate: 7,
1543
- /** The hands shrink with the tool: a tool drawn at scale `s` takes hands at `s ** handScaleExponent`. */
1544
- handScaleExponent: 0.55
1619
+ magRate: 7
1545
1620
  };
1546
1621
  var handFrame = (THREE, x, y) => {
1547
1622
  const ax = x.clone().normalize();
@@ -1631,7 +1706,8 @@ function holdOfModel(THREE, model, holderScale) {
1631
1706
  right,
1632
1707
  left,
1633
1708
  leftMagazine,
1634
- scale: holderScale ** HAND_HOLD.handScaleExponent,
1709
+ // the hands at the tool's scale: a tool drawn smaller than the hands looks like a toy in them
1710
+ scale: holderScale,
1635
1711
  grip: HAND_HOLD.closed,
1636
1712
  gripLeft: HAND_HOLD.forend
1637
1713
  };
@@ -1646,7 +1722,7 @@ function holdOfProp(THREE, group, category, scale) {
1646
1722
  const long = head && ["smg", "carbine", "rifle", "sniper", "shotgun", "crossbow", "flame"].includes(category);
1647
1723
  const headSize = head ? size(head) : null;
1648
1724
  const left = long && head && headSize ? { part: group, offset: head.position.clone().add(V(THREE, -headSize.x * 0.15, -headSize.y / 2 - HAND_HOLD.palm, 0)), quaternion: forendFrame(THREE) } : null;
1649
- return { right, left, leftMagazine: null, scale: scale ** HAND_HOLD.handScaleExponent, grip: HAND_HOLD.closed, gripLeft: HAND_HOLD.forend };
1725
+ return { right, left, leftMagazine: null, scale, grip: HAND_HOLD.closed, gripLeft: HAND_HOLD.forend };
1650
1726
  }
1651
1727
 
1652
1728
  // src/props.ts
@@ -4943,7 +5019,6 @@ var FALLBACK_ENERGY = 300;
4943
5019
  var MAX_CASINGS = 120;
4944
5020
  var MAX_MAGAZINES = 12;
4945
5021
  var LIFETIME_S = 25;
4946
- var HAND_OFFSET = [0.2, -0.2, -0.42];
4947
5022
  var GRAVITY = 9.81;
4948
5023
  function createBoxFx(options) {
4949
5024
  const { THREE, scene, camera, arsenal, groundAt } = options;
@@ -4965,6 +5040,10 @@ function createBoxFx(options) {
4965
5040
  let dip = 0;
4966
5041
  let reloading = false;
4967
5042
  const v = new THREE.Vector3();
5043
+ const v2 = new THREE.Vector3();
5044
+ const eyeFrame = createEyeFrame(THREE, camera);
5045
+ let rear = 0;
5046
+ let lengthMetres = 0;
4968
5047
  const q = new THREE.Quaternion();
4969
5048
  const q2 = new THREE.Quaternion();
4970
5049
  const pointForward = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
@@ -4976,13 +5055,17 @@ function createBoxFx(options) {
4976
5055
  if (!def || BODY_ACTIONS.has(def.id)) return;
4977
5056
  model = buildWeaponModel(THREE, def);
4978
5057
  const length = new THREE.Box3().setFromObject(model).getSize(v).x;
4979
- model.scale.setScalar(length > 0.8 ? 0.55 : length > 0.4 ? 0.75 : 1);
5058
+ rear = new THREE.Box3().setFromObject(model).min.x;
5059
+ lengthMetres = length;
4980
5060
  model.quaternion.copy(pointForward);
4981
5061
  hand.add(model);
4982
5062
  inHand = def.id;
4983
- hold = holdOfProp(THREE, model, def.category, model.scale.x);
5063
+ hold = holdOfProp(THREE, model, def.category, 1);
5064
+ };
5065
+ const handPoint = (x, y, z, out) => {
5066
+ if (!model) return out.set(x, y, z).applyQuaternion(camera.quaternion).add(camera.position);
5067
+ return out.set(x, y, z).applyQuaternion(camera.quaternion).add(model.getWorldPosition(v2));
4984
5068
  };
4985
- const handPoint = (x, y, z, out) => out.set(HAND_OFFSET[0] + x, HAND_OFFSET[1] + y, HAND_OFFSET[2] + z).applyQuaternion(camera.quaternion).add(camera.position);
4986
5069
  const throwBody = (kind, mesh, from, velocity, radius) => {
4987
5070
  if (!(kind === "casing" ? layers.casings : layers.magazines)) {
4988
5071
  mesh.geometry.dispose();
@@ -5093,10 +5176,12 @@ function createBoxFx(options) {
5093
5176
  update(dt) {
5094
5177
  kick *= Math.exp(-dt * 14);
5095
5178
  dip += ((reloading ? 1 : 0) - dip) * Math.min(1, dt * 8);
5096
- if (inHand) {
5097
- handPoint(0, -0.12 * dip, 0.05 * kick, hand.position);
5179
+ if (inHand && model) {
5180
+ const handgun = def?.category === "pistol" || def?.category === "revolver";
5181
+ placeOnBody({ THREE, hand, holder: model, frame: options.bodyHold?.() ?? eyeFrame(), rear, lengthMetres, handgun, right: hold?.right ?? null, left: hold?.left ?? null });
5182
+ hand.position.add(v.set(0, -0.12 * dip, 0.05 * kick).applyQuaternion(camera.quaternion));
5098
5183
  q.setFromEuler(new THREE.Euler(0.14 * kick - 0.25 * dip, 0, 0.6 * dip));
5099
- hand.quaternion.copy(camera.quaternion).multiply(q);
5184
+ hand.quaternion.multiply(q);
5100
5185
  }
5101
5186
  for (const body of [...bodies]) {
5102
5187
  stepBody(body, dt);
@@ -6181,6 +6266,14 @@ function createEffects(THREE, sink, random) {
6181
6266
 
6182
6267
  // src/fx/heavy.ts
6183
6268
  var PI12 = Math.PI;
6269
+ var ROCKET_LIGHT_NAME = "rocket-light";
6270
+ var rocketsInFlight = /* @__PURE__ */ new WeakMap();
6271
+ function createRocketLight(THREE) {
6272
+ const light = new THREE.PointLight(16751168, 0, 2.5);
6273
+ light.name = ROCKET_LIGHT_NAME;
6274
+ light.position.set(0, -50, 0);
6275
+ return light;
6276
+ }
6184
6277
  var flameMaterials = (kit) => {
6185
6278
  const { THREE } = kit;
6186
6279
  return [
@@ -6210,8 +6303,9 @@ function launchRocket(context, flyRound = true) {
6210
6303
  exhaust.rotation.z = PI12 / 2;
6211
6304
  exhaust.position.x = -0.08;
6212
6305
  rocket.add(exhaust);
6213
- const light = new THREE.PointLight(16751168, 3, 2.5);
6214
- rocket.add(light);
6306
+ const light = context.rocketLight;
6307
+ light.intensity = 3;
6308
+ rocketsInFlight.set(light, (rocketsInFlight.get(light) ?? 0) + 1);
6215
6309
  context.every(
6216
6310
  1.6,
6217
6311
  (progress, dt) => {
@@ -6219,6 +6313,7 @@ function launchRocket(context, flyRound = true) {
6219
6313
  const distance = 1.2 * flight + (flight > 0.25 ? 9 * (flight - 0.25) ** 2 : 0);
6220
6314
  rocket.position.copy(muzzle).addScaledVector(forward, distance);
6221
6315
  rocket.rotation.x += 0.25 * dt * 60;
6316
+ light.position.copy(rocket.position);
6222
6317
  exhaust.scale.set(1, 0.6 + random() * 0.8, 1);
6223
6318
  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);
6224
6319
  },
@@ -6226,7 +6321,11 @@ function launchRocket(context, flyRound = true) {
6226
6321
  rocket.removeFromParent();
6227
6322
  exhaust.geometry.dispose();
6228
6323
  exhaust.material.dispose();
6229
- light.dispose();
6324
+ const remaining = (rocketsInFlight.get(light) ?? 1) - 1;
6325
+ rocketsInFlight.set(light, remaining);
6326
+ if (remaining > 0) return;
6327
+ light.intensity = 0;
6328
+ light.position.set(0, -50, 0);
6230
6329
  }
6231
6330
  );
6232
6331
  }
@@ -7045,11 +7144,9 @@ function workWeights(works, running) {
7045
7144
 
7046
7145
  // src/fx/viewmodel.ts
7047
7146
  var PI14 = Math.PI;
7048
- var HAND_OFFSET2 = [0.2, -0.2, -0.42];
7049
- var HANDGUN_OFFSET = [0.1, -0.12, -0.44];
7050
7147
  var HEAT_DISPLAY_MAX_FALLBACK = 120;
7051
- var WALL_PULL = { margin: 0.03, max: 0.7, rate: 14, probe: 3 };
7052
- var HAND_IN_VIEW = { floor: -0.88, max: 0.18, rate: 10 };
7148
+ var WALL_PULL = { margin: 0.03, max: 1.2, rate: 14, probe: 3 };
7149
+ var HAND_IN_VIEW = { floor: -0.88, max: 0.3, rate: 10 };
7053
7150
  var LAUNCH_VOLUME = 0.6;
7054
7151
  var fittedOf2 = (parts) => parts.map((part) => ({ id: part.id, mount: part.mount, ...part.integral ? { integral: true } : {}, ...part.on === void 0 ? {} : { on: part.on } }));
7055
7152
  var collapsedStockPose = (move2) => {
@@ -7079,6 +7176,8 @@ function createViewmodel(options) {
7079
7176
  const hand = new THREE.Group();
7080
7177
  hand.name = VIEWMODEL_NODE_NAME;
7081
7178
  scene.add(hand);
7179
+ const rocketLight = createRocketLight(THREE);
7180
+ scene.add(rocketLight);
7082
7181
  const holder = new THREE.Group();
7083
7182
  holder.quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), PI14 / 2);
7084
7183
  hand.add(holder);
@@ -7135,7 +7234,8 @@ function createViewmodel(options) {
7135
7234
  positionOf: (part) => worldPositionOf(current, part, new THREE.Vector3()),
7136
7235
  orientation: () => current.model.root.getWorldQuaternion(new THREE.Quaternion()),
7137
7236
  every: (seconds, step, done) => void tasks.push({ delay: 0, seconds, elapsed: 0, step, done }),
7138
- groundAt
7237
+ groundAt,
7238
+ rocketLight
7139
7239
  });
7140
7240
  const equip = (weapon) => {
7141
7241
  const def = arsenal.weapons.get(weapon);
@@ -7148,14 +7248,11 @@ function createViewmodel(options) {
7148
7248
  reach = 0;
7149
7249
  pull = 0;
7150
7250
  lift = 0;
7151
- handHeight = 0;
7152
- handDepth = 0;
7153
7251
  rigStock(THREE, model, def);
7154
7252
  rigUnderbarrel(THREE, model, def);
7155
- const length = new THREE.Box3().setFromObject(model.root).getSize(new THREE.Vector3()).x;
7156
- const scale = length > 0.8 ? 0.55 : length > 0.4 ? 0.75 : 1;
7157
- holder.scale.setScalar(scale);
7158
- holder.position.set(0, 0, Math.min(0.25, 0.25 * length * scale));
7253
+ const modelBox = new THREE.Box3().setFromObject(model.root);
7254
+ const length = modelBox.getSize(new THREE.Vector3()).x;
7255
+ modelRear = modelBox.min.x;
7159
7256
  holder.add(model.root);
7160
7257
  const caliber = caliberOf(arsenal, def);
7161
7258
  const dims = caliber?.cartridge;
@@ -7167,7 +7264,7 @@ function createViewmodel(options) {
7167
7264
  def,
7168
7265
  caliber,
7169
7266
  model,
7170
- holderScale: scale,
7267
+ holderScale: 1,
7171
7268
  lengthMetres: length,
7172
7269
  caseType: caliber?.caseType ?? "rifle",
7173
7270
  soundProfile: soundProfileOf(weapon, model.parts.family),
@@ -7197,7 +7294,7 @@ function createViewmodel(options) {
7197
7294
  applyCondition(kit, model, condition);
7198
7295
  lastAmmo = options.tool?.().ammo ?? 0;
7199
7296
  hand.updateMatrixWorld(true);
7200
- hold = holdOfModel(THREE, model, scale);
7297
+ hold = holdOfModel(THREE, model, 1);
7201
7298
  hold = { ...hold, works: handWorkOf(THREE, model, weapon, hold.right) };
7202
7299
  triggerRest = model.parts.trigger?.rotation.z ?? 0;
7203
7300
  lastShot = -Infinity;
@@ -7217,8 +7314,6 @@ function createViewmodel(options) {
7217
7314
  let reach = 0;
7218
7315
  let pull = 0;
7219
7316
  let lift = 0;
7220
- let handHeight = 0;
7221
- let handDepth = 0;
7222
7317
  const ahead = new THREE.Vector3();
7223
7318
  const up = new THREE.Vector3();
7224
7319
  const seen = new THREE.Vector3();
@@ -7231,23 +7326,35 @@ function createViewmodel(options) {
7231
7326
  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(camera.position).dot(ahead));
7232
7327
  return farthest;
7233
7328
  };
7329
+ let modelRear = 0;
7330
+ const eyeFrame = createEyeFrame(THREE, camera);
7331
+ const liftToKeepInView = () => {
7332
+ if (!hold || !session || !("fov" in camera)) return 0;
7333
+ const tanHalf = Math.tan((camera.fov ?? 70) * PI14 / 360);
7334
+ eyeTurn.copy(camera.quaternion).invert();
7335
+ const rest = targetsOf(THREE, hold, session.model.root.getWorldQuaternion(rootQuaternion), 0);
7336
+ let needed = 0;
7337
+ for (const [anchor, pose] of [[hold.right, rest.R], [hold.left, rest.L]]) {
7338
+ if (!anchor || !pose || anchor.part !== session.model.root) continue;
7339
+ seen.set(...pose.position).sub(camera.position).applyQuaternion(eyeTurn);
7340
+ if (-seen.z > 0.05) needed = Math.max(needed, HAND_IN_VIEW.floor * -seen.z * tanHalf - seen.y);
7341
+ }
7342
+ return Math.max(0, Math.min(HAND_IN_VIEW.max, needed));
7343
+ };
7234
7344
  const place = (dt = 0) => {
7235
- const family = session?.model.parts.family;
7236
- const [x, y, z] = family === "pistol" || family === "revolver" ? HANDGUN_OFFSET : HAND_OFFSET2;
7237
- hand.position.set(x, y, z).applyQuaternion(camera.quaternion).add(camera.position);
7238
- hand.quaternion.copy(camera.quaternion);
7239
- if (session && handDepth > 0.05 && "fov" in camera) {
7240
- const tanHalf = Math.tan((camera.fov ?? 70) * PI14 / 360);
7241
- const want2 = Math.max(0, Math.min(HAND_IN_VIEW.max, lift + (HAND_IN_VIEW.floor - handHeight) * tanHalf * handDepth));
7242
- lift += (want2 - lift) * Math.min(1, dt * HAND_IN_VIEW.rate);
7243
- if (lift > 1e-4) hand.position.addScaledVector(up.set(0, 1, 0).applyQuaternion(camera.quaternion), lift);
7244
- }
7345
+ if (!session) return;
7346
+ const family = session.model.parts.family;
7347
+ session.holderScale = 1;
7348
+ 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 });
7245
7349
  hand.updateMatrixWorld(true);
7246
- if (!options.contact || !session) return;
7350
+ lift += (liftToKeepInView() - lift) * Math.min(1, dt * HAND_IN_VIEW.rate);
7351
+ if (lift > 1e-4) hand.position.addScaledVector(up.set(0, 1, 0).applyQuaternion(camera.quaternion), lift);
7352
+ hand.updateMatrixWorld(true);
7353
+ if (!options.contact) return;
7247
7354
  ahead.set(0, 0, -1).applyQuaternion(camera.quaternion);
7248
7355
  if (reach === 0) reach = Math.max(0.01, measureReach(session));
7249
7356
  const free = options.contact(camera.position, ahead, WALL_PULL.probe);
7250
- const want = free === null ? 0 : Math.max(0, Math.min(WALL_PULL.max, reach + WALL_PULL.margin - free));
7357
+ const want = free === null ? 0 : Math.max(0, Math.min(Math.max(WALL_PULL.max, session.lengthMetres * session.holderScale), reach + WALL_PULL.margin - free));
7251
7358
  pull += (want - pull) * Math.min(1, dt * WALL_PULL.rate);
7252
7359
  if (pull < 1e-4) return;
7253
7360
  hand.position.addScaledVector(ahead, -pull);
@@ -7357,12 +7464,7 @@ function createViewmodel(options) {
7357
7464
  const targets = targetsOf(THREE, hold, session.model.root.getWorldQuaternion(rootQuaternion), magazineHand, hold.works ? workWeights(hold.works, player?.running ?? []) : void 0);
7358
7465
  const trigger = session.model.parts.trigger;
7359
7466
  if (trigger) targets.fingers = { index: indexCurl(trigger.rotation.z - triggerRest, clock - lastShot) };
7360
- if (targets.R && "fov" in camera) {
7361
- seen.set(...targets.R.position).sub(camera.position).applyQuaternion(eyeTurn.copy(camera.quaternion).invert());
7362
- handDepth = -seen.z;
7363
- handHeight = handDepth > 0.05 ? seen.y / handDepth / Math.tan((camera.fov ?? 70) * PI14 / 360) : 0;
7364
- }
7365
- return targets;
7467
+ return limitSupportSpan(targets);
7366
7468
  },
7367
7469
  get inHand() {
7368
7470
  return session?.id ?? null;
@@ -7381,6 +7483,8 @@ function createViewmodel(options) {
7381
7483
  particles.dispose();
7382
7484
  ownVfx?.dispose();
7383
7485
  hand.removeFromParent();
7486
+ rocketLight.removeFromParent();
7487
+ rocketLight.dispose();
7384
7488
  }
7385
7489
  };
7386
7490
  }
@@ -7405,6 +7509,7 @@ function createWeaponFx(options) {
7405
7509
  ...options.attachmentModels ? { attachmentModels: options.attachmentModels } : {},
7406
7510
  ...options.raycast ? { raycast: options.raycast } : {},
7407
7511
  ...options.contact ? { contact: options.contact } : {},
7512
+ ...options.bodyHold ? { bodyHold: options.bodyHold } : {},
7408
7513
  ...options.effects ? { effects: options.effects } : {},
7409
7514
  ...options.flyRocket === void 0 ? {} : { flyRocket: options.flyRocket }
7410
7515
  });
@@ -8686,6 +8791,7 @@ function createWeaponsTestModule(options = {}) {
8686
8791
  raycast: createLaserRaycast(lent.THREE, lent.scene, lent.camera),
8687
8792
  // 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)
8688
8793
  contact: createLaserRaycast(lent.THREE, lent.scene, lent.camera, { now: () => simulatedMs }),
8794
+ ...lent.holdFrame ? { bodyHold: () => lent.holdFrame?.() ?? null } : {},
8689
8795
  ...lent.effects ? { effects: lent.effects } : {},
8690
8796
  ...options.attachmentModels ? { attachmentModels: options.attachmentModels } : {},
8691
8797
  ...options.condition ? { condition: options.condition } : {},
@@ -9505,7 +9611,7 @@ function pushScenario(all, bench, mode) {
9505
9611
  };
9506
9612
  for (const { kind, item } of PUSH_KINDS) PUSH_LAYOUT[kind].forEach(([distance, degrees]) => lay(kind, item, bearing(centre, distance, degrees)));
9507
9613
  if (mode === "grenade") {
9508
- lay("zombie", "zombie_walker", bearing(centre, 3.4, -34));
9614
+ lay("zombie", "zombie_walker", bearing(centre, 3.4, -130));
9509
9615
  lay("survivor", "person_police_officer", bearing(centre, 7.5, 78));
9510
9616
  lay("camp_table", "camp_table", bearing(centre, 3, 90));
9511
9617
  }
@@ -9548,7 +9654,7 @@ function pushScenario(all, bench, mode) {
9548
9654
  const outward = ((now[0] - from[0]) * (from[0] - blast.point[0]) + (now[2] - from[2]) * (from[2] - blast.point[2])) / away;
9549
9655
  return { ...body, distance, outward, moved: flat(now, from) };
9550
9656
  });
9551
- const torn = (row) => PUSH_KINDS.some((entry) => entry.kind === row.kind) && partsOf(row.objectId) < (partsBefore.get(row.objectId) ?? 0);
9657
+ const torn = (row) => (PUSH_KINDS.some((entry) => entry.kind === row.kind) || HEAVY_ITEMS.includes(row.kind)) && partsOf(row.objectId) < (partsBefore.get(row.objectId) ?? 0);
9552
9658
  const reached = rows.filter((row) => row.distance < blast.radius * 0.85 && !torn(row));
9553
9659
  const movedAway = reached.filter((row) => row.outward >= MOVED);
9554
9660
  const least = reached.length === 0 ? 0 : Math.min(...reached.map((row) => row.outward));
@@ -9561,9 +9667,9 @@ function pushScenario(all, bench, mode) {
9561
9667
  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 });
9562
9668
  }
9563
9669
  for (const row of reached.filter((entry) => entry.kind === "zombie" || entry.kind === "survivor")) {
9564
- 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 });
9670
+ 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 } });
9565
9671
  }
9566
- const table = reached.find((row) => HEAVY_ITEMS.includes(row.kind));
9672
+ const table = rows.find((row) => HEAVY_ITEMS.includes(row.kind) && row.distance < blast.radius * 0.85);
9567
9673
  if (table) expect(table.moved >= MOVED, fillText2(text.push_heavy, { what: things[table.itemId] ?? table.itemId, moved: table.moved.toFixed(2) }), { table });
9568
9674
  }
9569
9675
  };