@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.
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. */
@@ -4943,7 +5020,6 @@ var FALLBACK_ENERGY = 300;
4943
5020
  var MAX_CASINGS = 120;
4944
5021
  var MAX_MAGAZINES = 12;
4945
5022
  var LIFETIME_S = 25;
4946
- var HAND_OFFSET = [0.2, -0.2, -0.42];
4947
5023
  var GRAVITY = 9.81;
4948
5024
  function createBoxFx(options) {
4949
5025
  const { THREE, scene, camera, arsenal, groundAt } = options;
@@ -4965,6 +5041,10 @@ function createBoxFx(options) {
4965
5041
  let dip = 0;
4966
5042
  let reloading = false;
4967
5043
  const v = new THREE.Vector3();
5044
+ const v2 = new THREE.Vector3();
5045
+ const eyeFrame = createEyeFrame(THREE, camera);
5046
+ let rear = 0;
5047
+ let lengthMetres = 0;
4968
5048
  const q = new THREE.Quaternion();
4969
5049
  const q2 = new THREE.Quaternion();
4970
5050
  const pointForward = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
@@ -4976,13 +5056,17 @@ function createBoxFx(options) {
4976
5056
  if (!def || BODY_ACTIONS.has(def.id)) return;
4977
5057
  model = buildWeaponModel(THREE, def);
4978
5058
  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);
5059
+ rear = new THREE.Box3().setFromObject(model).min.x;
5060
+ lengthMetres = length;
4980
5061
  model.quaternion.copy(pointForward);
4981
5062
  hand.add(model);
4982
5063
  inHand = def.id;
4983
- hold = holdOfProp(THREE, model, def.category, model.scale.x);
5064
+ hold = holdOfProp(THREE, model, def.category, 1);
5065
+ };
5066
+ const handPoint = (x, y, z, out) => {
5067
+ if (!model) return out.set(x, y, z).applyQuaternion(camera.quaternion).add(camera.position);
5068
+ return out.set(x, y, z).applyQuaternion(camera.quaternion).add(model.getWorldPosition(v2));
4984
5069
  };
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
5070
  const throwBody = (kind, mesh, from, velocity, radius) => {
4987
5071
  if (!(kind === "casing" ? layers.casings : layers.magazines)) {
4988
5072
  mesh.geometry.dispose();
@@ -5093,10 +5177,12 @@ function createBoxFx(options) {
5093
5177
  update(dt) {
5094
5178
  kick *= Math.exp(-dt * 14);
5095
5179
  dip += ((reloading ? 1 : 0) - dip) * Math.min(1, dt * 8);
5096
- if (inHand) {
5097
- handPoint(0, -0.12 * dip, 0.05 * kick, hand.position);
5180
+ if (inHand && model) {
5181
+ const handgun = def?.category === "pistol" || def?.category === "revolver";
5182
+ placeOnBody({ THREE, hand, holder: model, frame: options.bodyHold?.() ?? eyeFrame(), rear, lengthMetres, handgun, right: hold?.right ?? null, left: hold?.left ?? null });
5183
+ hand.position.add(v.set(0, -0.12 * dip, 0.05 * kick).applyQuaternion(camera.quaternion));
5098
5184
  q.setFromEuler(new THREE.Euler(0.14 * kick - 0.25 * dip, 0, 0.6 * dip));
5099
- hand.quaternion.copy(camera.quaternion).multiply(q);
5185
+ hand.quaternion.multiply(q);
5100
5186
  }
5101
5187
  for (const body of [...bodies]) {
5102
5188
  stepBody(body, dt);
@@ -6181,6 +6267,14 @@ function createEffects(THREE, sink, random) {
6181
6267
 
6182
6268
  // src/fx/heavy.ts
6183
6269
  var PI12 = Math.PI;
6270
+ var ROCKET_LIGHT_NAME = "rocket-light";
6271
+ var rocketsInFlight = /* @__PURE__ */ new WeakMap();
6272
+ function createRocketLight(THREE) {
6273
+ const light = new THREE.PointLight(16751168, 0, 2.5);
6274
+ light.name = ROCKET_LIGHT_NAME;
6275
+ light.position.set(0, -50, 0);
6276
+ return light;
6277
+ }
6184
6278
  var flameMaterials = (kit) => {
6185
6279
  const { THREE } = kit;
6186
6280
  return [
@@ -6210,8 +6304,9 @@ function launchRocket(context, flyRound = true) {
6210
6304
  exhaust.rotation.z = PI12 / 2;
6211
6305
  exhaust.position.x = -0.08;
6212
6306
  rocket.add(exhaust);
6213
- const light = new THREE.PointLight(16751168, 3, 2.5);
6214
- rocket.add(light);
6307
+ const light = context.rocketLight;
6308
+ light.intensity = 3;
6309
+ rocketsInFlight.set(light, (rocketsInFlight.get(light) ?? 0) + 1);
6215
6310
  context.every(
6216
6311
  1.6,
6217
6312
  (progress, dt) => {
@@ -6219,6 +6314,7 @@ function launchRocket(context, flyRound = true) {
6219
6314
  const distance = 1.2 * flight + (flight > 0.25 ? 9 * (flight - 0.25) ** 2 : 0);
6220
6315
  rocket.position.copy(muzzle).addScaledVector(forward, distance);
6221
6316
  rocket.rotation.x += 0.25 * dt * 60;
6317
+ light.position.copy(rocket.position);
6222
6318
  exhaust.scale.set(1, 0.6 + random() * 0.8, 1);
6223
6319
  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
6320
  },
@@ -6226,7 +6322,11 @@ function launchRocket(context, flyRound = true) {
6226
6322
  rocket.removeFromParent();
6227
6323
  exhaust.geometry.dispose();
6228
6324
  exhaust.material.dispose();
6229
- light.dispose();
6325
+ const remaining = (rocketsInFlight.get(light) ?? 1) - 1;
6326
+ rocketsInFlight.set(light, remaining);
6327
+ if (remaining > 0) return;
6328
+ light.intensity = 0;
6329
+ light.position.set(0, -50, 0);
6230
6330
  }
6231
6331
  );
6232
6332
  }
@@ -7045,11 +7145,9 @@ function workWeights(works, running) {
7045
7145
 
7046
7146
  // src/fx/viewmodel.ts
7047
7147
  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
7148
  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 };
7149
+ var WALL_PULL = { margin: 0.03, max: 1.2, rate: 14, probe: 3 };
7150
+ var HAND_IN_VIEW = { floor: -0.88, max: 0.3, rate: 10 };
7053
7151
  var LAUNCH_VOLUME = 0.6;
7054
7152
  var fittedOf2 = (parts) => parts.map((part) => ({ id: part.id, mount: part.mount, ...part.integral ? { integral: true } : {}, ...part.on === void 0 ? {} : { on: part.on } }));
7055
7153
  var collapsedStockPose = (move2) => {
@@ -7079,6 +7177,8 @@ function createViewmodel(options) {
7079
7177
  const hand = new THREE.Group();
7080
7178
  hand.name = VIEWMODEL_NODE_NAME;
7081
7179
  scene.add(hand);
7180
+ const rocketLight = createRocketLight(THREE);
7181
+ scene.add(rocketLight);
7082
7182
  const holder = new THREE.Group();
7083
7183
  holder.quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), PI14 / 2);
7084
7184
  hand.add(holder);
@@ -7135,7 +7235,8 @@ function createViewmodel(options) {
7135
7235
  positionOf: (part) => worldPositionOf(current, part, new THREE.Vector3()),
7136
7236
  orientation: () => current.model.root.getWorldQuaternion(new THREE.Quaternion()),
7137
7237
  every: (seconds, step, done) => void tasks.push({ delay: 0, seconds, elapsed: 0, step, done }),
7138
- groundAt
7238
+ groundAt,
7239
+ rocketLight
7139
7240
  });
7140
7241
  const equip = (weapon) => {
7141
7242
  const def = arsenal.weapons.get(weapon);
@@ -7148,14 +7249,11 @@ function createViewmodel(options) {
7148
7249
  reach = 0;
7149
7250
  pull = 0;
7150
7251
  lift = 0;
7151
- handHeight = 0;
7152
- handDepth = 0;
7153
7252
  rigStock(THREE, model, def);
7154
7253
  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));
7254
+ const modelBox = new THREE.Box3().setFromObject(model.root);
7255
+ const length = modelBox.getSize(new THREE.Vector3()).x;
7256
+ modelRear = modelBox.min.x;
7159
7257
  holder.add(model.root);
7160
7258
  const caliber = caliberOf(arsenal, def);
7161
7259
  const dims = caliber?.cartridge;
@@ -7167,7 +7265,7 @@ function createViewmodel(options) {
7167
7265
  def,
7168
7266
  caliber,
7169
7267
  model,
7170
- holderScale: scale,
7268
+ holderScale: 1,
7171
7269
  lengthMetres: length,
7172
7270
  caseType: caliber?.caseType ?? "rifle",
7173
7271
  soundProfile: soundProfileOf(weapon, model.parts.family),
@@ -7197,7 +7295,7 @@ function createViewmodel(options) {
7197
7295
  applyCondition(kit, model, condition);
7198
7296
  lastAmmo = options.tool?.().ammo ?? 0;
7199
7297
  hand.updateMatrixWorld(true);
7200
- hold = holdOfModel(THREE, model, scale);
7298
+ hold = holdOfModel(THREE, model, 1);
7201
7299
  hold = { ...hold, works: handWorkOf(THREE, model, weapon, hold.right) };
7202
7300
  triggerRest = model.parts.trigger?.rotation.z ?? 0;
7203
7301
  lastShot = -Infinity;
@@ -7217,8 +7315,6 @@ function createViewmodel(options) {
7217
7315
  let reach = 0;
7218
7316
  let pull = 0;
7219
7317
  let lift = 0;
7220
- let handHeight = 0;
7221
- let handDepth = 0;
7222
7318
  const ahead = new THREE.Vector3();
7223
7319
  const up = new THREE.Vector3();
7224
7320
  const seen = new THREE.Vector3();
@@ -7231,19 +7327,31 @@ function createViewmodel(options) {
7231
7327
  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
7328
  return farthest;
7233
7329
  };
7330
+ let modelRear = 0;
7331
+ const eyeFrame = createEyeFrame(THREE, camera);
7332
+ const liftToKeepInView = () => {
7333
+ if (!hold || !session || !("fov" in camera)) return 0;
7334
+ const tanHalf = Math.tan((camera.fov ?? 70) * PI14 / 360);
7335
+ eyeTurn.copy(camera.quaternion).invert();
7336
+ const rest = targetsOf(THREE, hold, session.model.root.getWorldQuaternion(rootQuaternion), 0);
7337
+ let needed = 0;
7338
+ for (const [anchor, pose] of [[hold.right, rest.R], [hold.left, rest.L]]) {
7339
+ if (!anchor || !pose || anchor.part !== session.model.root) continue;
7340
+ seen.set(...pose.position).sub(camera.position).applyQuaternion(eyeTurn);
7341
+ if (-seen.z > 0.05) needed = Math.max(needed, HAND_IN_VIEW.floor * -seen.z * tanHalf - seen.y);
7342
+ }
7343
+ return Math.max(0, Math.min(HAND_IN_VIEW.max, needed));
7344
+ };
7234
7345
  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
- }
7346
+ if (!session) return;
7347
+ const family = session.model.parts.family;
7348
+ session.holderScale = 1;
7349
+ 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
7350
  hand.updateMatrixWorld(true);
7246
- if (!options.contact || !session) return;
7351
+ lift += (liftToKeepInView() - lift) * Math.min(1, dt * HAND_IN_VIEW.rate);
7352
+ if (lift > 1e-4) hand.position.addScaledVector(up.set(0, 1, 0).applyQuaternion(camera.quaternion), lift);
7353
+ hand.updateMatrixWorld(true);
7354
+ if (!options.contact) return;
7247
7355
  ahead.set(0, 0, -1).applyQuaternion(camera.quaternion);
7248
7356
  if (reach === 0) reach = Math.max(0.01, measureReach(session));
7249
7357
  const free = options.contact(camera.position, ahead, WALL_PULL.probe);
@@ -7357,12 +7465,7 @@ function createViewmodel(options) {
7357
7465
  const targets = targetsOf(THREE, hold, session.model.root.getWorldQuaternion(rootQuaternion), magazineHand, hold.works ? workWeights(hold.works, player?.running ?? []) : void 0);
7358
7466
  const trigger = session.model.parts.trigger;
7359
7467
  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;
7468
+ return limitSupportSpan(targets);
7366
7469
  },
7367
7470
  get inHand() {
7368
7471
  return session?.id ?? null;
@@ -7381,6 +7484,8 @@ function createViewmodel(options) {
7381
7484
  particles.dispose();
7382
7485
  ownVfx?.dispose();
7383
7486
  hand.removeFromParent();
7487
+ rocketLight.removeFromParent();
7488
+ rocketLight.dispose();
7384
7489
  }
7385
7490
  };
7386
7491
  }
@@ -7405,6 +7510,7 @@ function createWeaponFx(options) {
7405
7510
  ...options.attachmentModels ? { attachmentModels: options.attachmentModels } : {},
7406
7511
  ...options.raycast ? { raycast: options.raycast } : {},
7407
7512
  ...options.contact ? { contact: options.contact } : {},
7513
+ ...options.bodyHold ? { bodyHold: options.bodyHold } : {},
7408
7514
  ...options.effects ? { effects: options.effects } : {},
7409
7515
  ...options.flyRocket === void 0 ? {} : { flyRocket: options.flyRocket }
7410
7516
  });
@@ -8686,6 +8792,7 @@ function createWeaponsTestModule(options = {}) {
8686
8792
  raycast: createLaserRaycast(lent.THREE, lent.scene, lent.camera),
8687
8793
  // 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
8794
  contact: createLaserRaycast(lent.THREE, lent.scene, lent.camera, { now: () => simulatedMs }),
8795
+ ...lent.holdFrame ? { bodyHold: () => lent.holdFrame?.() ?? null } : {},
8689
8796
  ...lent.effects ? { effects: lent.effects } : {},
8690
8797
  ...options.attachmentModels ? { attachmentModels: options.attachmentModels } : {},
8691
8798
  ...options.condition ? { condition: options.condition } : {},
@@ -9505,7 +9612,7 @@ function pushScenario(all, bench, mode) {
9505
9612
  };
9506
9613
  for (const { kind, item } of PUSH_KINDS) PUSH_LAYOUT[kind].forEach(([distance, degrees]) => lay(kind, item, bearing(centre, distance, degrees)));
9507
9614
  if (mode === "grenade") {
9508
- lay("zombie", "zombie_walker", bearing(centre, 3.4, -34));
9615
+ lay("zombie", "zombie_walker", bearing(centre, 3.4, -130));
9509
9616
  lay("survivor", "person_police_officer", bearing(centre, 7.5, 78));
9510
9617
  lay("camp_table", "camp_table", bearing(centre, 3, 90));
9511
9618
  }
@@ -9548,7 +9655,7 @@ function pushScenario(all, bench, mode) {
9548
9655
  const outward = ((now[0] - from[0]) * (from[0] - blast.point[0]) + (now[2] - from[2]) * (from[2] - blast.point[2])) / away;
9549
9656
  return { ...body, distance, outward, moved: flat(now, from) };
9550
9657
  });
9551
- const torn = (row) => PUSH_KINDS.some((entry) => entry.kind === row.kind) && partsOf(row.objectId) < (partsBefore.get(row.objectId) ?? 0);
9658
+ 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
9659
  const reached = rows.filter((row) => row.distance < blast.radius * 0.85 && !torn(row));
9553
9660
  const movedAway = reached.filter((row) => row.outward >= MOVED);
9554
9661
  const least = reached.length === 0 ? 0 : Math.min(...reached.map((row) => row.outward));
@@ -9561,9 +9668,9 @@ function pushScenario(all, bench, mode) {
9561
9668
  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
9669
  }
9563
9670
  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 });
9671
+ 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
9672
  }
9566
- const table = reached.find((row) => HEAVY_ITEMS.includes(row.kind));
9673
+ const table = rows.find((row) => HEAVY_ITEMS.includes(row.kind) && row.distance < blast.radius * 0.85);
9567
9674
  if (table) expect(table.moved >= MOVED, fillText2(text.push_heavy, { what: things[table.itemId] ?? table.itemId, moved: table.moved.toFixed(2) }), { table });
9568
9675
  }
9569
9676
  };