@adatechnology/game-weapons-three 0.16.0 → 0.17.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.
@@ -1,6 +1,6 @@
1
1
  // src/test-area/area.ts
2
2
  import { markKitOrigin as markKitOrigin6 } from "@adatechnology/game-contracts";
3
- import { createArsenal, genericWeapons } from "@adatechnology/game-weapons";
3
+ import { createArsenal, genericWeapons, roundsToWarningOf } from "@adatechnology/game-weapons";
4
4
  import { furnitureSpec } from "@adatechnology/game-floorplan-three";
5
5
  import { isLightBudget, isRenderPools } from "@adatechnology/game-render-three";
6
6
 
@@ -183,6 +183,9 @@ function createAccessoryBench(options) {
183
183
  signature = "";
184
184
  });
185
185
  return {
186
+ get aimProgress() {
187
+ return aimProgress;
188
+ },
186
189
  handleChange(change) {
187
190
  if (change.kind === "action") return act(change.id);
188
191
  if (change.kind === "control" && change.id === SUPPORTED_CONTROL_ID && typeof change.value === "boolean") {
@@ -2460,6 +2463,7 @@ function createWeaponAudio(volume = 0.6, options = {}) {
2460
2463
  }
2461
2464
 
2462
2465
  // src/box-fx.ts
2466
+ import { FIRST_PERSON_LAYER } from "@adatechnology/game-contracts";
2463
2467
  import { createRng, muzzleEnergy as muzzleEnergy3 } from "@adatechnology/game-weapons";
2464
2468
 
2465
2469
  // src/fx/layers.ts
@@ -2521,6 +2525,8 @@ var BODY_HOLD = {
2521
2525
  shoulderGripTo: 0.34,
2522
2526
  /** The farthest apart two hands hold a long gun (a support hand past it slides back along the gun, toward the grip). */
2523
2527
  supportSpan: 0.5,
2528
+ /** A gun carried at the hip (`carry: 'hip'`): its rear grip low at the right hip, a little ahead of it. */
2529
+ hipGrip: [0.25, -0.5, -0.24],
2524
2530
  /** How much of the aim's pitch a tool held low in one hand follows. */
2525
2531
  toolPitch: 0.5
2526
2532
  };
@@ -2532,6 +2538,7 @@ var placeOnBody = (placement) => {
2532
2538
  holder.scale.setScalar(1);
2533
2539
  hand.position.set(...frame.position);
2534
2540
  hand.quaternion.set(...frame.quaternion);
2541
+ if (placement.hip) return placeAtHip(placement);
2535
2542
  const shouldered = !placement.handgun && placement.left !== null && placement.lengthMetres >= BODY_HOLD.shoulderedFrom;
2536
2543
  if (shouldered) {
2537
2544
  holder.position.set(0, BODY_HOLD.boreAbove, placement.rear);
@@ -2555,6 +2562,21 @@ var placeOnBody = (placement) => {
2555
2562
  hand.position.add(want.sub(now));
2556
2563
  hand.updateMatrixWorld(true);
2557
2564
  };
2565
+ var placeAtHip = (placement) => {
2566
+ const { THREE, hand, holder } = placement;
2567
+ scratch.a ?? (scratch.a = new THREE.Vector3());
2568
+ scratch.b ?? (scratch.b = new THREE.Vector3());
2569
+ scratch.euler ?? (scratch.euler = new THREE.Euler());
2570
+ const turn2 = scratch.euler.setFromQuaternion(hand.quaternion, "YXZ");
2571
+ hand.quaternion.setFromEuler(turn2.set(turn2.x * BODY_HOLD.toolPitch, turn2.y, 0, "YXZ"));
2572
+ holder.position.set(0, 0, 0);
2573
+ hand.updateMatrixWorld(true);
2574
+ if (!placement.right) return;
2575
+ const want = scratch.a.set(...BODY_HOLD.hipGrip).applyQuaternion(hand.quaternion).add(hand.position);
2576
+ const now = placement.right.part.localToWorld(scratch.b.copy(placement.right.offset));
2577
+ hand.position.add(want.sub(now));
2578
+ hand.updateMatrixWorld(true);
2579
+ };
2558
2580
  var limitSupportSpan = (targets) => {
2559
2581
  const right = targets.R?.position;
2560
2582
  const left = targets.L?.position;
@@ -2578,6 +2600,57 @@ var createEyeFrame = (THREE, camera) => {
2578
2600
  };
2579
2601
  };
2580
2602
 
2603
+ // src/fx/view-hold.ts
2604
+ var VIEW_HOLD = {
2605
+ long: { grip: [0.2, -0.28, -0.38], fov: 84, sights: { distance: 0.34, fromRear: 0.3 } },
2606
+ handgun: { grip: [0.13, -0.15, -0.33], fov: 76, sights: { distance: 0.42, fromRear: 0.02 } },
2607
+ launcher: { grip: [0.2, -0.14, -0.3], fov: 84, sights: { distance: 0.18, fromRear: 0.25 } },
2608
+ tool: { grip: [0.21, -0.31, -0.36], fov: 84, sights: { distance: 0.3, fromRear: 0.1 }, tilt: 0.3 },
2609
+ // the gun at the hip, its rear grip low and to the right, the barrels ahead of the body; it has no sights to bring to the eye
2610
+ hip: { grip: [0.44, -0.32, -0.42], fov: 84, sights: { distance: 0.6, fromRear: 0.1 } }
2611
+ };
2612
+ var NO_GRIP_REAR = [0.2, -0.24, -0.12];
2613
+ var SIGHT_UNDER_EYE = 0.02;
2614
+ var scratch2 = { grip: null, hip: null, aimed: null, tilt: null };
2615
+ var ease = (k) => k * k * (3 - 2 * k);
2616
+ function placeInView(placement) {
2617
+ const { THREE, hand, holder, camera, right, box: box2, kind } = placement;
2618
+ const hold = VIEW_HOLD[kind];
2619
+ holder.scale.setScalar(1);
2620
+ hand.position.copy(camera.position);
2621
+ hand.quaternion.copy(camera.quaternion);
2622
+ if (hold.tilt) {
2623
+ scratch2.tilt ?? (scratch2.tilt = new THREE.Quaternion());
2624
+ hand.quaternion.multiply(scratch2.tilt.setFromAxisAngle(new THREE.Vector3(1, 0, 0), hold.tilt));
2625
+ }
2626
+ hand.scale.setScalar(1);
2627
+ holder.position.set(0, 0, 0);
2628
+ hand.updateMatrixWorld(true);
2629
+ scratch2.hip ?? (scratch2.hip = new THREE.Vector3());
2630
+ const hip = scratch2.hip;
2631
+ if (right) {
2632
+ scratch2.grip ?? (scratch2.grip = new THREE.Vector3());
2633
+ const grip = hand.worldToLocal(right.part.localToWorld(scratch2.grip.copy(right.offset)));
2634
+ hip.set(hold.grip[0] - grip.x, hold.grip[1] - grip.y, hold.grip[2] - grip.z);
2635
+ } else hip.set(NO_GRIP_REAR[0], NO_GRIP_REAR[1], NO_GRIP_REAR[2] + box2.min.x);
2636
+ const k = ease(Math.max(0, Math.min(1, placement.aim)));
2637
+ if (k > 0) {
2638
+ const along = box2.min.x + hold.sights.fromRear;
2639
+ scratch2.aimed ?? (scratch2.aimed = new THREE.Vector3());
2640
+ hip.lerp(scratch2.aimed.set(0, -SIGHT_UNDER_EYE - box2.max.y, along - hold.sights.distance), k);
2641
+ }
2642
+ holder.position.copy(hip).add(placement.offset);
2643
+ hand.updateMatrixWorld(true);
2644
+ }
2645
+ function toWorldLens(camera, point, worldFov, lensFov) {
2646
+ const k = Math.tan(worldFov * Math.PI / 360) / Math.tan(lensFov * Math.PI / 360);
2647
+ camera.updateMatrixWorld();
2648
+ camera.worldToLocal(point);
2649
+ point.x *= k;
2650
+ point.y *= k;
2651
+ return camera.localToWorld(point);
2652
+ }
2653
+
2581
2654
  // src/fx/hand-hold.ts
2582
2655
  var HAND_HOLD = {
2583
2656
  /** Half the width of a pistol grip, the palm sits this far (plus the palm) off its middle. */
@@ -2656,6 +2729,7 @@ function holdOfModel(THREE, model, holderScale) {
2656
2729
  const toRoot = (x, y, z) => V(THREE, inner.position.x + x * inner.scale.x, y, z);
2657
2730
  const handgun = parts.family === "pistol" || parts.family === "revolver";
2658
2731
  root.updateWorldMatrix(true, true);
2732
+ if (parts.holdRight || parts.holdLeft) return namedHold(THREE, model, holderScale);
2659
2733
  let gripAt;
2660
2734
  let up;
2661
2735
  if (parts.grip) {
@@ -2702,6 +2776,18 @@ function holdOfModel(THREE, model, holderScale) {
2702
2776
  gripLeft: HAND_HOLD.forend
2703
2777
  };
2704
2778
  }
2779
+ function namedHold(THREE, model, holderScale) {
2780
+ const { parts } = model;
2781
+ const on = (mark, fingers) => mark ? { part: mark, offset: V(THREE, 0, HAND_HOLD.palm, 0), quaternion: handFrame(THREE, fingers, V(THREE, 0, 1, 0)) } : null;
2782
+ return {
2783
+ right: on(parts.holdRight, V(THREE, 1, 0, 0)),
2784
+ left: on(parts.holdLeft, V(THREE, 0, 0, 1)),
2785
+ leftMagazine: null,
2786
+ scale: holderScale,
2787
+ grip: HAND_HOLD.closed,
2788
+ gripLeft: HAND_HOLD.closed
2789
+ };
2790
+ }
2705
2791
  function holdOfProp(THREE, group, category, scale) {
2706
2792
  const handle = group.getObjectByName("handle") ?? group.getObjectByName("body") ?? group;
2707
2793
  const head = group.getObjectByName("head");
@@ -3210,6 +3296,15 @@ function createBoxFx(options) {
3210
3296
  const q2 = new THREE.Quaternion();
3211
3297
  const pointForward = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
3212
3298
  const frontOf = new THREE.Vector3();
3299
+ const restBox = new THREE.Box3();
3300
+ const noMotion = new THREE.Vector3();
3301
+ let inLens = false;
3302
+ const setLens = (on) => {
3303
+ if (on !== inLens) options.viewmodelFov?.(on ? VIEW_HOLD.tool.fov : null);
3304
+ inLens = on;
3305
+ const layer = on ? FIRST_PERSON_LAYER : 0;
3306
+ hand.traverse((node) => node.layers.set(layer));
3307
+ };
3213
3308
  const equip = (weapon) => {
3214
3309
  if (model) hand.remove(model);
3215
3310
  model = null;
@@ -3217,6 +3312,8 @@ function createBoxFx(options) {
3217
3312
  def = arsenal.weapons.get(weapon) ?? null;
3218
3313
  if (!def || BODY_ACTIONS.has(def.id)) return;
3219
3314
  model = buildWeaponModel(THREE, def, options.items);
3315
+ restBox.setFromObject(model);
3316
+ inLens = false;
3220
3317
  const length = new THREE.Box3().setFromObject(model).getSize(v).x;
3221
3318
  rear = new THREE.Box3().setFromObject(model).min.x;
3222
3319
  lengthMetres = length;
@@ -3317,6 +3414,7 @@ function createBoxFx(options) {
3317
3414
  return {
3318
3415
  unequip() {
3319
3416
  if (model) hand.remove(model);
3417
+ if (inLens) setLens(false);
3320
3418
  model = null;
3321
3419
  inHand = null;
3322
3420
  hold = null;
@@ -3325,6 +3423,7 @@ function createBoxFx(options) {
3325
3423
  if (!model || !def || !SPOUTS.has(def.action.kind)) return null;
3326
3424
  hand.updateMatrixWorld(true);
3327
3425
  const at = model.localToWorld(v.copy(frontOf));
3426
+ if (inLens) toWorldLens(camera, at, camera.fov ?? 72, VIEW_HOLD.tool.fov);
3328
3427
  const along = v2.set(1, 0, 0).applyQuaternion(model.getWorldQuaternion(q2)).normalize();
3329
3428
  return { position: [at.x, at.y, at.z], direction: [along.x, along.y, along.z], drawsFlash: false };
3330
3429
  },
@@ -3347,7 +3446,13 @@ function createBoxFx(options) {
3347
3446
  update(dt) {
3348
3447
  kick *= Math.exp(-dt * 14);
3349
3448
  dip += ((reloading ? 1 : 0) - dip) * Math.min(1, dt * 8);
3350
- if (inHand && model) {
3449
+ if (inHand && model && options.firstPerson?.()) {
3450
+ setLens(true);
3451
+ placeInView({ THREE, hand, holder: model, camera, right: hold?.right ?? null, box: restBox, kind: "tool", aim: 0, offset: noMotion });
3452
+ hand.position.add(v.set(0, -0.12 * dip, 0.05 * kick).applyQuaternion(camera.quaternion));
3453
+ hand.updateMatrixWorld(true);
3454
+ } else if (inHand && model) {
3455
+ if (inLens) setLens(false);
3351
3456
  const handgun = def?.category === "pistol" || def?.category === "revolver";
3352
3457
  placeOnBody({ THREE, hand, holder: model, frame: options.bodyHold?.() ?? eyeFrame(), rear, lengthMetres, handgun, right: hold?.right ?? null, left: hold?.left ?? null });
3353
3458
  hand.position.add(v.set(0, -0.12 * dip, 0.05 * kick).applyQuaternion(camera.quaternion));
@@ -5892,6 +5997,9 @@ function buildHousing(kit, parts, group) {
5892
5997
  cylinderZ(group, 0.016, 0.07, M.poly, 0.06, -0.01, side * 0.2, 14);
5893
5998
  }
5894
5999
  parts.trigger = box2(group, 0.012, 0.02, 0.016, M.dark, 0.1, 0.085, 0.14, 0.3);
6000
+ parts.holdRight = kit.mark(group, 0.06, 6e-3, 0.2);
6001
+ parts.holdLeft = kit.mark(group, 0.22, 0.164, 0);
6002
+ parts.carry = "hip";
5895
6003
  parts.safety = box2(group, 0.014, 6e-3, 0.01, M.dark, 0.2, 0.073, -0.076, 0.4);
5896
6004
  }
5897
6005
  function buildFeed(kit, parts, group) {
@@ -6319,7 +6427,7 @@ function buildWeaponModelById(THREE, id) {
6319
6427
  }
6320
6428
 
6321
6429
  // src/fx/viewmodel.ts
6322
- import { createToolClipPlayer, toolAnimationStep, warnOnce as warnOnce2 } from "@adatechnology/game-contracts";
6430
+ import { createToolClipPlayer, FIRST_PERSON_LAYER as FIRST_PERSON_LAYER2, toolAnimationStep, warnOnce as warnOnce2 } from "@adatechnology/game-contracts";
6323
6431
  import { discardObject3D as discardObject3D3 } from "@adatechnology/game-shapes-three";
6324
6432
  import { createRng as createRng2, muzzleEnergy as muzzleEnergy4 } from "@adatechnology/game-weapons";
6325
6433
 
@@ -6329,7 +6437,7 @@ var RELOAD_SAMPLE_HITS2 = reload_sample_hits_default.hits;
6329
6437
  var ROOT = "root";
6330
6438
  var AXIS_INDEX = { x: 0, y: 1, z: 2 };
6331
6439
  var EASE_STEPS = 10;
6332
- var ease = (progress) => progress < 0.5 ? 2 * progress * progress : 1 - (-2 * progress + 2) ** 2 / 2;
6440
+ var ease2 = (progress) => progress < 0.5 ? 2 * progress * progress : 1 - (-2 * progress + 2) ** 2 / 2;
6333
6441
  var move = (part, axis, start, duration, from, to) => ({ part, channel: "position", axis, start, duration, from, to });
6334
6442
  var turn = (part, axis, start, duration, from, to) => ({ part, channel: "rotation", axis, start, duration, from, to });
6335
6443
  var cue = (t, type, data) => ({ t, type, ...data ? { data } : {} });
@@ -6343,7 +6451,7 @@ var valueAt = (tweens, t, includeInstants) => {
6343
6451
  for (const tween of tweens) {
6344
6452
  if (tween.start > t || tween.duration === 0 && (tween.start === t ? !includeInstants : false)) continue;
6345
6453
  if (tween.duration === 0 || t >= tween.start + tween.duration) value = tween.to;
6346
- else value = tween.from + (tween.to - tween.from) * ease((t - tween.start) / tween.duration);
6454
+ else value = tween.from + (tween.to - tween.from) * ease2((t - tween.start) / tween.duration);
6347
6455
  }
6348
6456
  return value;
6349
6457
  };
@@ -7425,38 +7533,6 @@ function createEffects(THREE, sink, random) {
7425
7533
  };
7426
7534
  }
7427
7535
 
7428
- // src/fx/view-hold.ts
7429
- var VIEW_HOLD = {
7430
- /** The right hand's grip, from the eye. */
7431
- grip: [0.2, -0.2, -0.3],
7432
- /** A handgun's grip: higher and nearer the middle, the arms out in front. */
7433
- handgunGrip: [0.13, -0.15, -0.33],
7434
- /** A rocket launcher's grip: its tube rides on the shoulder, so the hands are higher. */
7435
- launcherGrip: [0.2, -0.14, -0.3],
7436
- /** A gun with no grip anchor: the rear of its bore, from the eye. */
7437
- rear: [0.2, -0.24, -0.12]
7438
- };
7439
- var scratch2 = { grip: null };
7440
- function placeInView(placement) {
7441
- const { THREE, hand, holder, camera, right } = placement;
7442
- holder.scale.setScalar(1);
7443
- hand.position.copy(camera.position);
7444
- hand.quaternion.copy(camera.quaternion);
7445
- hand.scale.setScalar(1);
7446
- holder.position.set(0, 0, 0);
7447
- hand.updateMatrixWorld(true);
7448
- if (!right) {
7449
- holder.position.set(VIEW_HOLD.rear[0], VIEW_HOLD.rear[1], VIEW_HOLD.rear[2] + placement.rear);
7450
- hand.updateMatrixWorld(true);
7451
- return;
7452
- }
7453
- scratch2.grip ?? (scratch2.grip = new THREE.Vector3());
7454
- const grip = hand.worldToLocal(right.part.localToWorld(scratch2.grip.copy(right.offset)));
7455
- const [x, y, z] = placement.kind === "handgun" ? VIEW_HOLD.handgunGrip : placement.kind === "launcher" ? VIEW_HOLD.launcherGrip : VIEW_HOLD.grip;
7456
- holder.position.set(x - grip.x, y - grip.y, z - grip.z);
7457
- hand.updateMatrixWorld(true);
7458
- }
7459
-
7460
7536
  // src/fx/heavy.ts
7461
7537
  import { LIGHT_PRIORITY as LIGHT_PRIORITY2, geometryKey, materialKey } from "@adatechnology/game-render-three";
7462
7538
  import { discardObject3D } from "@adatechnology/game-shapes-three";
@@ -7845,11 +7921,12 @@ function createClipEventHandler(world, session) {
7845
7921
  switch (event.type) {
7846
7922
  case "muzzle_flash":
7847
7923
  if (data.at === "ugl") {
7848
- if (parts.uglMuzzle && world.layers.muzzleFlash) effects.muzzleFlash(positionOf(parts.uglMuzzle), root.getWorldQuaternion(new THREE.Quaternion()), UGL_FLASH);
7924
+ if (parts.uglMuzzle && world.layers.muzzleFlash) world.afterPose(() => parts.uglMuzzle && effects.muzzleFlash(positionOf(parts.uglMuzzle), root.getWorldQuaternion(new THREE.Quaternion()), UGL_FLASH));
7849
7925
  } else if (parts.muzzle && world.layers.muzzleFlash && (session.def.action.kind === "firearm" || session.def.action.kind === "launcher")) {
7850
7926
  const profile = muzzleSmokeOf({ caliber: session.caliber, weapon: session.def, suppressed: session.suppressed });
7851
7927
  const size = session.def.action.kind === "launcher" ? 2.2 : FLASH_BY_CASE[session.caseType];
7852
- effects.muzzleFlash(positionOf(parts.muzzle), root.getWorldQuaternion(new THREE.Quaternion()), size, profile);
7928
+ const muzzle = parts.muzzle;
7929
+ world.afterPose(() => effects.muzzleFlash(positionOf(muzzle), root.getWorldQuaternion(new THREE.Quaternion()), size, profile));
7853
7930
  }
7854
7931
  break;
7855
7932
  case "eject_casing":
@@ -8390,7 +8467,9 @@ function workWeights(works, running) {
8390
8467
 
8391
8468
  // src/fx/viewmodel.ts
8392
8469
  var PI14 = Math.PI;
8393
- var HEAT_DISPLAY_MAX_FALLBACK = 120;
8470
+ var HEAT_DISPLAY_MAX_FALLBACK = 750;
8471
+ var SMOKE_WISPS_PER_SECOND = 10;
8472
+ var SMOKE_THICKNESS_GAIN = 1.7;
8394
8473
  var WALL_PULL = { margin: 0.03, max: 1.2, rate: 14, probe: 3 };
8395
8474
  var HAND_IN_VIEW = { floor: -0.88, max: 0.3, rate: 10 };
8396
8475
  var LAUNCH_VOLUME = 0.6;
@@ -8409,6 +8488,7 @@ var indexCurl = (turn2, sinceShot) => {
8409
8488
  const held = sinceShot < TRIGGER_FINGER.burstSeconds ? TRIGGER_FINGER.pulled - TRIGGER_FINGER.tremor * (0.5 + 0.5 * Math.sin(sinceShot * 220)) : 0;
8410
8489
  return Math.max(ready + (TRIGGER_FINGER.pulled - ready) * pull, held);
8411
8490
  };
8491
+ var VIEW_MOTION = { sway: 6e-3, swayMax: 0.02, swayRate: 9, bobX: 6e-3, bobY: 8e-3, run: 1.6, fullSpeed: 4, frequency: 3.2 };
8412
8492
  function createViewmodel(options) {
8413
8493
  const { THREE, scene, camera, arsenal, groundAt, audio } = options;
8414
8494
  const kit = modelKit(THREE);
@@ -8416,6 +8496,11 @@ function createViewmodel(options) {
8416
8496
  const layers = options.layers ?? createLayers();
8417
8497
  const distance = () => options.soundDistance?.() ?? 0;
8418
8498
  const particles = createParticles(THREE, scene, groundAt, options.particleLimit);
8499
+ const spawnParticle = particles.spawn.bind(particles);
8500
+ particles.spawn = (object, velocity, particleOptions) => {
8501
+ if (lens) toWorldLens(camera, object.position, worldFovOf(), lens.fov);
8502
+ spawnParticle(object, velocity, particleOptions);
8503
+ };
8419
8504
  particles.onBounce = (kind, hit) => {
8420
8505
  if (!audio || kind !== "casing" && kind !== "magazine") return;
8421
8506
  const surface = hit ? options.surfaceAt?.(hit.object.position.x, hit.object.position.z) : void 0;
@@ -8428,7 +8513,16 @@ function createViewmodel(options) {
8428
8513
  };
8429
8514
  if (!options.effects) warnOnce2("game-weapons-three.own-effects-world", "game-weapons-three made an effects world of its own because the host lent none (options.effects); pass the host effects so the effects share one budget");
8430
8515
  const ownVfx = options.effects ? void 0 : createVfxRenderer(THREE, { scene, camera, vfx: { profile: "medium" }, ...options.lights ? { lights: options.lights } : {} });
8431
- const effects = createEffects(THREE, options.effects ?? ownVfx, random);
8516
+ const worldSink = options.effects ?? ownVfx;
8517
+ const lensSink = worldSink ? {
8518
+ play: (request) => {
8519
+ if (!lens || !request.point) return worldSink.play(request);
8520
+ const at = toWorldLens(camera, lensPoint.set(...request.point), worldFovOf(), lens.fov);
8521
+ return worldSink.play({ ...request, point: [at.x, at.y, at.z] });
8522
+ },
8523
+ stop: (handle) => worldSink.stop(handle)
8524
+ } : void 0;
8525
+ const effects = createEffects(THREE, lensSink, random);
8432
8526
  const hand = new THREE.Group();
8433
8527
  hand.name = VIEWMODEL_NODE_NAME;
8434
8528
  scene.add(hand);
@@ -8454,6 +8548,7 @@ function createViewmodel(options) {
8454
8548
  const rootQuaternion = new THREE.Quaternion();
8455
8549
  const muzzleAt = new THREE.Vector3();
8456
8550
  const muzzleAlong = new THREE.Vector3();
8551
+ const posed = [];
8457
8552
  const world = {
8458
8553
  kit,
8459
8554
  scene,
@@ -8467,13 +8562,17 @@ function createViewmodel(options) {
8467
8562
  const played = audio?.cue(cue2, profile, lead);
8468
8563
  options.onSound?.({ weapon, cue: cue2, profile, ...played ? { played } : {} });
8469
8564
  },
8470
- schedule: (delay, seconds, step, done) => void tasks.push({ delay, seconds, elapsed: 0, step: (progress) => step(ease(progress)), done })
8565
+ afterPose: (emit) => void posed.push(emit),
8566
+ schedule: (delay, seconds, step, done) => void tasks.push({ delay, seconds, elapsed: 0, step: (progress) => step(ease2(progress)), done })
8471
8567
  };
8472
8568
  const heatMax = arsenal.rules.heat.displayMax || HEAT_DISPLAY_MAX_FALLBACK;
8473
8569
  const visual = arsenal.rules.heat.visual;
8474
8570
  const hasSuppressor = (parts) => parts.some((part) => arsenal.attachments.get(part.id)?.kind === "suppressor");
8475
8571
  const release = () => {
8476
8572
  if (!session) return;
8573
+ if (lens) options.viewmodelFov?.(null);
8574
+ lens = null;
8575
+ lensLayer = -1;
8477
8576
  session.attachments?.dispose();
8478
8577
  discardObject3D3(session.model.root);
8479
8578
  player = null;
@@ -8513,6 +8612,9 @@ function createViewmodel(options) {
8513
8612
  const modelBox = new THREE.Box3().setFromObject(model.root);
8514
8613
  const length = modelBox.getSize(new THREE.Vector3()).x;
8515
8614
  modelRear = modelBox.min.x;
8615
+ restBox.copy(modelBox);
8616
+ lens = null;
8617
+ lensLayer = -1;
8516
8618
  holder.add(model.root);
8517
8619
  const caliber = caliberOf(arsenal, def);
8518
8620
  const dims = caliber?.cartridge;
@@ -8575,6 +8677,42 @@ function createViewmodel(options) {
8575
8677
  let reach = 0;
8576
8678
  let pull = 0;
8577
8679
  let lift = 0;
8680
+ const restBox = new THREE.Box3();
8681
+ let lens = null;
8682
+ let lensLayer = -1;
8683
+ const lensPoint = new THREE.Vector3();
8684
+ const worldFovOf = () => camera.fov ?? 72;
8685
+ const setLens = (kind) => {
8686
+ const fov = kind ? VIEW_HOLD[kind].fov : null;
8687
+ if ((lens?.fov ?? null) !== fov) options.viewmodelFov?.(fov);
8688
+ lens = kind && fov !== null ? { kind, fov } : null;
8689
+ const layer = lens ? FIRST_PERSON_LAYER2 : 0;
8690
+ if (layer === lensLayer && !lens) return;
8691
+ lensLayer = layer;
8692
+ hand.traverse((node) => node.layers.set(layer));
8693
+ };
8694
+ const motion = { phase: 0, swayX: 0, swayY: 0, lastYaw: Number.NaN, lastPitch: Number.NaN };
8695
+ const motionEuler = new THREE.Euler(0, 0, 0, "YXZ");
8696
+ const motionOffset = new THREE.Vector3();
8697
+ const viewMotion = (dt, aim) => {
8698
+ const moving = options.motion?.() ?? { speed: 0, running: false };
8699
+ const calm = 1 - 0.8 * Math.max(0, Math.min(1, aim));
8700
+ motionEuler.setFromQuaternion(camera.quaternion, "YXZ");
8701
+ const turnYaw = Number.isNaN(motion.lastYaw) || dt <= 0 ? 0 : Math.atan2(Math.sin(motionEuler.y - motion.lastYaw), Math.cos(motionEuler.y - motion.lastYaw)) / dt;
8702
+ const turnPitch = Number.isNaN(motion.lastPitch) || dt <= 0 ? 0 : (motionEuler.x - motion.lastPitch) / dt;
8703
+ motion.lastYaw = motionEuler.y;
8704
+ motion.lastPitch = motionEuler.x;
8705
+ const follow = Math.min(1, dt * VIEW_MOTION.swayRate);
8706
+ motion.swayX += (Math.max(-VIEW_MOTION.swayMax, Math.min(VIEW_MOTION.swayMax, turnYaw * VIEW_MOTION.sway)) - motion.swayX) * follow;
8707
+ motion.swayY += (Math.max(-VIEW_MOTION.swayMax, Math.min(VIEW_MOTION.swayMax, -turnPitch * VIEW_MOTION.sway)) - motion.swayY) * follow;
8708
+ const stride = Math.min(1, moving.speed / VIEW_MOTION.fullSpeed) * (moving.running ? VIEW_MOTION.run : 1);
8709
+ motion.phase += dt * moving.speed * VIEW_MOTION.frequency;
8710
+ return motionOffset.set(
8711
+ (motion.swayX + Math.sin(motion.phase) * VIEW_MOTION.bobX * stride) * calm,
8712
+ (motion.swayY - Math.abs(Math.cos(motion.phase)) * VIEW_MOTION.bobY * stride) * calm,
8713
+ 0
8714
+ );
8715
+ };
8578
8716
  const ahead = new THREE.Vector3();
8579
8717
  const up = new THREE.Vector3();
8580
8718
  const seen = new THREE.Vector3();
@@ -8607,13 +8745,17 @@ function createViewmodel(options) {
8607
8745
  const family = session.model.parts.family;
8608
8746
  session.holderScale = 1;
8609
8747
  if (options.firstPerson?.()) {
8610
- placeInView({ THREE, hand, holder, camera, right: hold?.right ?? null, rear: modelRear, kind: family === "pistol" || family === "revolver" ? "handgun" : family === "rocket" ? "launcher" : "long" });
8748
+ const kind = session.model.parts.carry === "hip" ? "hip" : family === "pistol" || family === "revolver" ? "handgun" : family === "rocket" ? "launcher" : "long";
8749
+ const aim = kind === "hip" ? 0 : options.aim?.() ?? 0;
8750
+ placeInView({ THREE, hand, holder, camera, right: hold?.right ?? null, box: restBox, kind, aim, offset: viewMotion(dt, aim) });
8751
+ setLens(kind);
8611
8752
  lift = 0;
8612
- pullFromWall(dt);
8753
+ pull = 0;
8613
8754
  return;
8614
8755
  }
8756
+ setLens(null);
8615
8757
  hand.scale.set(1, 1, 1);
8616
- 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 });
8758
+ placeOnBody({ THREE, hand, holder, frame: options.bodyHold?.() ?? eyeFrame(), rear: modelRear, lengthMetres: session.lengthMetres, handgun: family === "pistol" || family === "revolver", hip: session.model.parts.carry === "hip", right: hold?.right ?? null, left: hold?.left ?? null });
8617
8759
  hand.updateMatrixWorld(true);
8618
8760
  lift += (liftToKeepInView() - lift) * Math.min(1, dt * HAND_IN_VIEW.rate);
8619
8761
  if (lift > 1e-4) hand.position.addScaledVector(up.set(0, 1, 0).applyQuaternion(camera.quaternion), lift);
@@ -8689,6 +8831,10 @@ function createViewmodel(options) {
8689
8831
  if (session) {
8690
8832
  place(dt);
8691
8833
  player?.update(dt);
8834
+ if (posed.length) {
8835
+ hand.updateMatrixWorld(true);
8836
+ for (const emit of posed.splice(0)) emit();
8837
+ }
8692
8838
  const wantsMagazine = reloading || options.tool?.().reloading === true;
8693
8839
  magazineHand += ((wantsMagazine ? 1 : 0) - magazineHand) * Math.min(1, dt * HAND_HOLD.magRate);
8694
8840
  const tool = options.tool?.();
@@ -8704,10 +8850,11 @@ function createViewmodel(options) {
8704
8850
  }
8705
8851
  secondsSinceShot += dt;
8706
8852
  if (tool && tool.heat !== void 0 && tool.heat * heatMax > visual.smokeFrom && secondsSinceShot < visual.smokeSeconds && session.model.parts.muzzle) {
8707
- smokeDebt += dt * (tool.heat * heatMax - visual.smokeFrom) * 0.1;
8853
+ const smokeLevel = Math.min(1, (tool.heat * heatMax - visual.smokeFrom) / visual.smokeSpan);
8854
+ smokeDebt += dt * smokeLevel * SMOKE_WISPS_PER_SECOND;
8708
8855
  while (smokeDebt > 1) {
8709
8856
  smokeDebt -= 1;
8710
- const k = 0.5 + (tool.heat * heatMax - visual.smokeFrom) / 60;
8857
+ const k = 0.5 + smokeLevel * SMOKE_THICKNESS_GAIN;
8711
8858
  effects.smoke(worldPositionOf(session, session.model.parts.muzzle, new THREE.Vector3()), new THREE.Vector3((random() - 0.5) * 0.015, 0.07 + random() * 0.08, (random() - 0.5) * 0.015), 4e-3 * k, 1.2 + random() * 0.6, Math.min(0.18, 0.06 + k * 0.04));
8712
8859
  }
8713
8860
  }
@@ -8734,6 +8881,7 @@ function createViewmodel(options) {
8734
8881
  if (!session || !mark) return null;
8735
8882
  hand.updateMatrixWorld(true);
8736
8883
  const at = worldPositionOf(session, mark, muzzleAt);
8884
+ if (lens) toWorldLens(camera, at, worldFovOf(), lens.fov);
8737
8885
  const along = muzzleAlong.set(1, 0, 0).applyQuaternion(session.model.root.getWorldQuaternion(rootQuaternion)).normalize();
8738
8886
  const kind = session.def.action.kind;
8739
8887
  return { position: [at.x, at.y, at.z], direction: [along.x, along.y, along.z], drawsFlash: kind === "firearm" || kind === "launcher" };
@@ -8743,7 +8891,8 @@ function createViewmodel(options) {
8743
8891
  hand.updateMatrixWorld(true);
8744
8892
  const targets = targetsOf(THREE, hold, session.model.root.getWorldQuaternion(rootQuaternion), magazineHand, hold.works ? workWeights(hold.works, player?.running ?? []) : void 0);
8745
8893
  const trigger = session.model.parts.trigger;
8746
- if (trigger) targets.fingers = { index: indexCurl(trigger.rotation.z - triggerRest, clock - lastShot) };
8894
+ if (session.model.parts.carry === "hip") targets.fingers = { index: 1 };
8895
+ else if (trigger) targets.fingers = { index: indexCurl(trigger.rotation.z - triggerRest, clock - lastShot) };
8747
8896
  return limitSupportSpan(targets);
8748
8897
  },
8749
8898
  get inHand() {
@@ -8793,6 +8942,9 @@ function createWeaponFx(options) {
8793
8942
  ...options.contact ? { contact: options.contact } : {},
8794
8943
  ...options.bodyHold ? { bodyHold: options.bodyHold } : {},
8795
8944
  ...options.firstPerson ? { firstPerson: options.firstPerson } : {},
8945
+ ...options.viewmodelFov ? { viewmodelFov: options.viewmodelFov } : {},
8946
+ ...options.aim ? { aim: options.aim } : {},
8947
+ ...options.motion ? { motion: options.motion } : {},
8796
8948
  ...options.effects ? { effects: options.effects } : {},
8797
8949
  ...options.flyRocket === void 0 ? {} : { flyRocket: options.flyRocket },
8798
8950
  ...options.particleLimit ? { particleLimit: options.particleLimit } : {},
@@ -9910,7 +10062,7 @@ var TEXT = {
9910
10062
  fuseLength: "Dynamite fuse length",
9911
10063
  fuseLengthHint: "Metres of safety fuse (131 s per metre): a long one is set down lit, for a delayed trap",
9912
10064
  layers: { muzzle_flash: "Muzzle flash", casings: "Ejected casings", magazines: "Dropped magazines" },
9913
- metrics: { casings_live: "Casings in the scene", magazines_live: "Magazines in the scene", heat: "Barrel heat" }
10065
+ metrics: { casings_live: "Casings in the scene", magazines_live: "Magazines in the scene", heat: "Barrel temperature above ambient", rounds_to_warning: "Rounds to the heat warning (cyclic rate, from cold)" }
9914
10066
  },
9915
10067
  "pt-BR": {
9916
10068
  group: "Armas",
@@ -9946,7 +10098,7 @@ var TEXT = {
9946
10098
  fuseLength: "Pavio da dinamite",
9947
10099
  fuseLengthHint: "Metros de pavio de seguran\xE7a (131 s por metro): um longo \xE9 posto no ch\xE3o aceso, para armadilha com atraso",
9948
10100
  layers: { muzzle_flash: "Clar\xE3o do cano", casings: "C\xE1psulas ejetadas", magazines: "Pentes que caem" },
9949
- metrics: { casings_live: "C\xE1psulas na cena", magazines_live: "Pentes na cena", heat: "Calor do cano" }
10101
+ metrics: { casings_live: "C\xE1psulas na cena", magazines_live: "Pentes na cena", heat: "Temperatura do cano acima do ambiente", rounds_to_warning: "Tiros at\xE9 o aviso de calor (cad\xEAncia, a frio)" }
9950
10102
  }
9951
10103
  };
9952
10104
  var textOf = (locale) => TEXT[locale] ?? TEXT.en;
@@ -10070,7 +10222,12 @@ function createWeaponsTestModule(options = {}) {
10070
10222
  let host = null;
10071
10223
  let soundDistance = 0;
10072
10224
  let soundIndoors = false;
10073
- const heatPercent = () => Math.min(100, (host?.tool().heat ?? 0) * 100);
10225
+ const barrelCelsius = () => (host?.tool().heat ?? 0) * arsenal.rules.heat.displayMax;
10226
+ const roundsToWarning = () => {
10227
+ const weaponId = host?.tool().weapon;
10228
+ const rounds = weaponId ? roundsToWarningOf(arsenal, weaponId) : Infinity;
10229
+ return Number.isFinite(rounds) ? rounds : "-";
10230
+ };
10074
10231
  return {
10075
10232
  id: "weapons_area",
10076
10233
  name: text.name,
@@ -10123,7 +10280,8 @@ function createWeaponsTestModule(options = {}) {
10123
10280
  metrics: () => [
10124
10281
  { id: "casings_live", name: text.metrics.casings_live, read: () => fx?.casings ?? 0 },
10125
10282
  { id: "magazines_live", name: text.metrics.magazines_live, read: () => fx?.magazines ?? 0 },
10126
- { id: "heat", name: text.metrics.heat, unit: "%", max: 100, digits: 0, read: heatPercent },
10283
+ { id: "heat", name: text.metrics.heat, unit: "\xB0C", max: arsenal.rules.heat.displayMax, digits: 0, read: barrelCelsius },
10284
+ { id: "rounds_to_warning", name: text.metrics.rounds_to_warning, read: roundsToWarning },
10127
10285
  ...accessoryMetrics(accessoryText, () => host?.tool() ?? EMPTY_TOOL)
10128
10286
  ],
10129
10287
  attach(lent) {
@@ -10166,6 +10324,10 @@ function createWeaponsTestModule(options = {}) {
10166
10324
  contact: createLaserRaycast(lent.THREE, lent.scene, lent.camera, { now: () => simulatedMs }),
10167
10325
  ...lent.holdFrame ? { bodyHold: () => lent.holdFrame?.() ?? null } : {},
10168
10326
  firstPerson: () => lent.player().cameraMode === "first",
10327
+ // the lens of the gun in first person, how far it is brought to the eye (the right button) and how the body moves (the bob and the sway follow it)
10328
+ ...lent.setViewmodelFov ? { viewmodelFov: (degrees) => lent.setViewmodelFov?.(degrees) } : {},
10329
+ aim: () => accessories.aimProgress,
10330
+ motion: () => ({ speed: lent.player().speed, running: lent.player().running }),
10169
10331
  ...lent.effects ? { effects: lent.effects } : {},
10170
10332
  ...lent.particleLimit ? { particleLimit: lent.particleLimit } : {},
10171
10333
  ...isLightBudget(lent.lights) ? { lights: lent.lights } : {},
@@ -11268,6 +11430,8 @@ export {
11268
11430
  weaponSampleDefaults,
11269
11431
  weaponSampleBank,
11270
11432
  createWeaponAudio,
11433
+ VIEW_HOLD,
11434
+ toWorldLens,
11271
11435
  BODY_ACTIONS,
11272
11436
  weaponProp,
11273
11437
  buildProp,
@@ -11302,6 +11466,7 @@ export {
11302
11466
  buildCartridge,
11303
11467
  buildCasing,
11304
11468
  buildRound,
11469
+ VIEW_MOTION,
11305
11470
  createWeaponFx,
11306
11471
  buildModeledProp,
11307
11472
  buildTripodMountedProp,
@@ -11329,4 +11494,4 @@ export {
11329
11494
  DEFAULT_EXPLOSIVES_SCENE,
11330
11495
  createExplosivesTestModule
11331
11496
  };
11332
- //# sourceMappingURL=chunk-KWJQO6JS.js.map
11497
+ //# sourceMappingURL=chunk-BDYIF7JD.js.map