@adatechnology/game-weapons-three 0.15.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") {
@@ -1413,6 +1416,10 @@ function computeCasingSound(input) {
1413
1416
  const surfaceName = surface === "hard" ? "concreto" : surface === "soft" ? "terra" : surface === "wood" ? "madeira" : "metal";
1414
1417
  return { surface, kind, sample: `casing_drop_${surfaceName}_${kind}`, voices };
1415
1418
  }
1419
+ function magazineDropId(material) {
1420
+ const surface = casingSurfaceOf(material);
1421
+ return `mag_drop_${surface === "hard" ? "concreto" : surface === "soft" ? "terra" : surface === "wood" ? "madeira" : "metal"}`;
1422
+ }
1416
1423
 
1417
1424
  // src/audio/flyby.ts
1418
1425
  function computeFlyby(input) {
@@ -2217,7 +2224,7 @@ var CUES_IN_SAMPLE = { mag_release: "mag_out", mag_insert: "mag_seat" };
2217
2224
  var isGunshot = (id) => GUNSHOT_SAMPLE_IDS.includes(id);
2218
2225
  function weaponSampleDefaults(sound3, overrides = {}) {
2219
2226
  if (sound3.category !== "armas") return void 0;
2220
- const base = sound3.id.startsWith("suppressed_") ? SUPPRESSED_SAMPLE : isGunshot(sound3.id) ? GUNSHOT_SAMPLE : sound3.id.startsWith("casing_drop_") ? CASING_SAMPLE : RELOAD_SAMPLE;
2227
+ const base = sound3.id.startsWith("suppressed_") ? SUPPRESSED_SAMPLE : isGunshot(sound3.id) ? GUNSHOT_SAMPLE : sound3.id.startsWith("casing_drop_") || sound3.id.startsWith("mag_drop_") ? CASING_SAMPLE : RELOAD_SAMPLE;
2221
2228
  return { ...base, ...overrides[sound3.id] };
2222
2229
  }
2223
2230
  function weaponSampleBank(options) {
@@ -2268,6 +2275,8 @@ function createWeaponAudio(volume = 0.6, options = {}) {
2268
2275
  void loadSamples();
2269
2276
  return false;
2270
2277
  };
2278
+ const fittedReady = (id) => sampleIds.has(id) && engine.registry.sound(id) === void 0 && engine.registry.set(id) !== void 0;
2279
+ const hostReady = (id) => sampleReady(id) || engine.registry.sound(id) === void 0 && engine.registry.set(id) !== void 0;
2271
2280
  const burst = (config) => {
2272
2281
  const id = config.filter === "highpass" ? BURST_IDS.highpass : BURST_IDS.lowpass;
2273
2282
  const params = { frequency: config.frequency, q: config.q ?? 0.8, gain: Math.max(config.gain, 2e-4), seconds: config.seconds };
@@ -2322,6 +2331,11 @@ function createWeaponAudio(volume = 0.6, options = {}) {
2322
2331
  options.onCasing?.(sound3, "sample");
2323
2332
  return;
2324
2333
  }
2334
+ if (recorded && fittedReady(recorded)) {
2335
+ if ((input.bounce ?? 0) === 0) channel.play(recorded, { volume: Math.min(1, Math.max(0.2, (input.speed ?? 1.5) / 2.5)) });
2336
+ options.onCasing?.(sound3, "fitted");
2337
+ return;
2338
+ }
2325
2339
  for (const voice of sound3.voices) playCasingVoice(voice);
2326
2340
  options.onCasing?.(sound3, "synth");
2327
2341
  };
@@ -2346,7 +2360,9 @@ function createWeaponAudio(volume = 0.6, options = {}) {
2346
2360
  const pitch = 1 + (channel.random() * 2 - 1) * pitchSpread;
2347
2361
  const shot = computeGunshot({ ...base, take, pitch }, options.gunshot);
2348
2362
  const recorded = shot.samples.close;
2349
- if (shot.audible && sampleReady(recorded)) {
2363
+ const sample = shot.audible && sampleReady(recorded);
2364
+ const fitted = shot.audible && !sample && fittedReady(recorded);
2365
+ if (sample || fitted) {
2350
2366
  const occlusion = Math.min(1, Math.max(0, extras.occlusion ?? 0));
2351
2367
  const handle = channel.play(recorded, {
2352
2368
  volume: (extras.volume ?? 1) * (1 - 0.5 * occlusion),
@@ -2359,7 +2375,7 @@ function createWeaponAudio(volume = 0.6, options = {}) {
2359
2375
  for (const voice of shot.voices) if (voice.group === "tail") playGunshotVoice(voice, extras);
2360
2376
  }
2361
2377
  const layers = handle?.kind === "sample" ? handle.plan.layers.map((layer) => ({ role: layer.role, take: layer.take, rate: layer.rate })) : [];
2362
- options.onGunshot?.(shot, { ...base, take, pitch }, { kind: "sample", id: recorded, layers });
2378
+ options.onGunshot?.(shot, { ...base, take, pitch }, fitted ? { kind: "fitted", id: recorded } : { kind: "sample", id: recorded, layers });
2363
2379
  return;
2364
2380
  }
2365
2381
  for (const voice of shot.voices) playGunshotVoice(voice, extras);
@@ -2380,10 +2396,11 @@ function createWeaponAudio(volume = 0.6, options = {}) {
2380
2396
  mechanical,
2381
2397
  cue(id, profile, lead = 0) {
2382
2398
  const carriedBy = CUES_IN_SAMPLE[id];
2383
- if (carriedBy && sampleReady(carriedBy)) return "carried";
2384
- if (sampleReady(id)) {
2399
+ if (carriedBy && (sampleReady(carriedBy) || fittedReady(carriedBy))) return "carried";
2400
+ const recorded = sampleReady(id);
2401
+ if (recorded || fittedReady(id)) {
2385
2402
  channel.play(id, { intensity: WEIGHT_INTENSITY[(profile ?? DEFAULT_SOUND_PROFILE).weight] });
2386
- return "sample";
2403
+ return recorded ? "sample" : "fitted";
2387
2404
  }
2388
2405
  for (const entry of reloadCueSteps(id, profile, cueTable)) step(lead > 0 ? { ...entry, at: entry.at + lead } : entry);
2389
2406
  return "synth";
@@ -2410,7 +2427,12 @@ function createWeaponAudio(volume = 0.6, options = {}) {
2410
2427
  casing,
2411
2428
  flyby(input) {
2412
2429
  const sound3 = computeFlyby({ ...input, pitch: input.pitch ?? 1 + (channel.random() * 2 - 1) * 0.05 });
2413
- for (const voice of sound3?.voices ?? []) playCasingVoice(voice, input.position);
2430
+ if (!sound3) return;
2431
+ if (hostReady(sound3.sample)) {
2432
+ channel.play(sound3.sample, { volume: Math.min(1, 1 / (1 + input.miss / 1.5)), ...input.position ? { position: [input.position[0], input.position[1], input.position[2]] } : {} });
2433
+ return;
2434
+ }
2435
+ for (const voice of sound3.voices) playCasingVoice(voice, input.position);
2414
2436
  },
2415
2437
  clink() {
2416
2438
  if (options.shotModel !== "game") {
@@ -2421,8 +2443,13 @@ function createWeaponAudio(volume = 0.6, options = {}) {
2421
2443
  const at = 0.25 + channel.random() * 0.2;
2422
2444
  sceneTone({ layer: "scene_tone", at, wave: "sine", f, f2: 3800, d: 0.06, vol: 0.06 });
2423
2445
  },
2424
- thud() {
2425
- burst({ seconds: 0.09, gain: 0.35, filter: "lowpass", frequency: 380 });
2446
+ thud(input = {}) {
2447
+ const id = magazineDropId(input.surface);
2448
+ if (hostReady(id)) {
2449
+ if ((input.bounce ?? 0) === 0) channel.play(id, { volume: Math.min(1, Math.max(0.2, (input.speed ?? 1.5) / 2.5)) });
2450
+ return;
2451
+ }
2452
+ burst({ seconds: 0.09, gain: 0.35 * (input.speed === void 0 ? 1 : Math.min(1, Math.max(0.3, input.speed / 2.5))) * ((input.bounce ?? 0) === 0 ? 1 : 0.5), filter: "lowpass", frequency: 380 });
2426
2453
  },
2427
2454
  setVolume(next) {
2428
2455
  const level = Math.max(0, Math.min(1, next));
@@ -2436,6 +2463,7 @@ function createWeaponAudio(volume = 0.6, options = {}) {
2436
2463
  }
2437
2464
 
2438
2465
  // src/box-fx.ts
2466
+ import { FIRST_PERSON_LAYER } from "@adatechnology/game-contracts";
2439
2467
  import { createRng, muzzleEnergy as muzzleEnergy3 } from "@adatechnology/game-weapons";
2440
2468
 
2441
2469
  // src/fx/layers.ts
@@ -2497,6 +2525,8 @@ var BODY_HOLD = {
2497
2525
  shoulderGripTo: 0.34,
2498
2526
  /** The farthest apart two hands hold a long gun (a support hand past it slides back along the gun, toward the grip). */
2499
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],
2500
2530
  /** How much of the aim's pitch a tool held low in one hand follows. */
2501
2531
  toolPitch: 0.5
2502
2532
  };
@@ -2508,6 +2538,7 @@ var placeOnBody = (placement) => {
2508
2538
  holder.scale.setScalar(1);
2509
2539
  hand.position.set(...frame.position);
2510
2540
  hand.quaternion.set(...frame.quaternion);
2541
+ if (placement.hip) return placeAtHip(placement);
2511
2542
  const shouldered = !placement.handgun && placement.left !== null && placement.lengthMetres >= BODY_HOLD.shoulderedFrom;
2512
2543
  if (shouldered) {
2513
2544
  holder.position.set(0, BODY_HOLD.boreAbove, placement.rear);
@@ -2531,6 +2562,21 @@ var placeOnBody = (placement) => {
2531
2562
  hand.position.add(want.sub(now));
2532
2563
  hand.updateMatrixWorld(true);
2533
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
+ };
2534
2580
  var limitSupportSpan = (targets) => {
2535
2581
  const right = targets.R?.position;
2536
2582
  const left = targets.L?.position;
@@ -2554,6 +2600,57 @@ var createEyeFrame = (THREE, camera) => {
2554
2600
  };
2555
2601
  };
2556
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
+
2557
2654
  // src/fx/hand-hold.ts
2558
2655
  var HAND_HOLD = {
2559
2656
  /** Half the width of a pistol grip, the palm sits this far (plus the palm) off its middle. */
@@ -2632,6 +2729,7 @@ function holdOfModel(THREE, model, holderScale) {
2632
2729
  const toRoot = (x, y, z) => V(THREE, inner.position.x + x * inner.scale.x, y, z);
2633
2730
  const handgun = parts.family === "pistol" || parts.family === "revolver";
2634
2731
  root.updateWorldMatrix(true, true);
2732
+ if (parts.holdRight || parts.holdLeft) return namedHold(THREE, model, holderScale);
2635
2733
  let gripAt;
2636
2734
  let up;
2637
2735
  if (parts.grip) {
@@ -2678,6 +2776,18 @@ function holdOfModel(THREE, model, holderScale) {
2678
2776
  gripLeft: HAND_HOLD.forend
2679
2777
  };
2680
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
+ }
2681
2791
  function holdOfProp(THREE, group, category, scale) {
2682
2792
  const handle = group.getObjectByName("handle") ?? group.getObjectByName("body") ?? group;
2683
2793
  const head = group.getObjectByName("head");
@@ -3186,6 +3296,15 @@ function createBoxFx(options) {
3186
3296
  const q2 = new THREE.Quaternion();
3187
3297
  const pointForward = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
3188
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
+ };
3189
3308
  const equip = (weapon) => {
3190
3309
  if (model) hand.remove(model);
3191
3310
  model = null;
@@ -3193,6 +3312,8 @@ function createBoxFx(options) {
3193
3312
  def = arsenal.weapons.get(weapon) ?? null;
3194
3313
  if (!def || BODY_ACTIONS.has(def.id)) return;
3195
3314
  model = buildWeaponModel(THREE, def, options.items);
3315
+ restBox.setFromObject(model);
3316
+ inLens = false;
3196
3317
  const length = new THREE.Box3().setFromObject(model).getSize(v).x;
3197
3318
  rear = new THREE.Box3().setFromObject(model).min.x;
3198
3319
  lengthMetres = length;
@@ -3293,6 +3414,7 @@ function createBoxFx(options) {
3293
3414
  return {
3294
3415
  unequip() {
3295
3416
  if (model) hand.remove(model);
3417
+ if (inLens) setLens(false);
3296
3418
  model = null;
3297
3419
  inHand = null;
3298
3420
  hold = null;
@@ -3301,6 +3423,7 @@ function createBoxFx(options) {
3301
3423
  if (!model || !def || !SPOUTS.has(def.action.kind)) return null;
3302
3424
  hand.updateMatrixWorld(true);
3303
3425
  const at = model.localToWorld(v.copy(frontOf));
3426
+ if (inLens) toWorldLens(camera, at, camera.fov ?? 72, VIEW_HOLD.tool.fov);
3304
3427
  const along = v2.set(1, 0, 0).applyQuaternion(model.getWorldQuaternion(q2)).normalize();
3305
3428
  return { position: [at.x, at.y, at.z], direction: [along.x, along.y, along.z], drawsFlash: false };
3306
3429
  },
@@ -3323,7 +3446,13 @@ function createBoxFx(options) {
3323
3446
  update(dt) {
3324
3447
  kick *= Math.exp(-dt * 14);
3325
3448
  dip += ((reloading ? 1 : 0) - dip) * Math.min(1, dt * 8);
3326
- 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);
3327
3456
  const handgun = def?.category === "pistol" || def?.category === "revolver";
3328
3457
  placeOnBody({ THREE, hand, holder: model, frame: options.bodyHold?.() ?? eyeFrame(), rear, lengthMetres, handgun, right: hold?.right ?? null, left: hold?.left ?? null });
3329
3458
  hand.position.add(v.set(0, -0.12 * dip, 0.05 * kick).applyQuaternion(camera.quaternion));
@@ -5868,6 +5997,9 @@ function buildHousing(kit, parts, group) {
5868
5997
  cylinderZ(group, 0.016, 0.07, M.poly, 0.06, -0.01, side * 0.2, 14);
5869
5998
  }
5870
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";
5871
6003
  parts.safety = box2(group, 0.014, 6e-3, 0.01, M.dark, 0.2, 0.073, -0.076, 0.4);
5872
6004
  }
5873
6005
  function buildFeed(kit, parts, group) {
@@ -6295,7 +6427,7 @@ function buildWeaponModelById(THREE, id) {
6295
6427
  }
6296
6428
 
6297
6429
  // src/fx/viewmodel.ts
6298
- 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";
6299
6431
  import { discardObject3D as discardObject3D3 } from "@adatechnology/game-shapes-three";
6300
6432
  import { createRng as createRng2, muzzleEnergy as muzzleEnergy4 } from "@adatechnology/game-weapons";
6301
6433
 
@@ -6305,7 +6437,7 @@ var RELOAD_SAMPLE_HITS2 = reload_sample_hits_default.hits;
6305
6437
  var ROOT = "root";
6306
6438
  var AXIS_INDEX = { x: 0, y: 1, z: 2 };
6307
6439
  var EASE_STEPS = 10;
6308
- 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;
6309
6441
  var move = (part, axis, start, duration, from, to) => ({ part, channel: "position", axis, start, duration, from, to });
6310
6442
  var turn = (part, axis, start, duration, from, to) => ({ part, channel: "rotation", axis, start, duration, from, to });
6311
6443
  var cue = (t, type, data) => ({ t, type, ...data ? { data } : {} });
@@ -6319,7 +6451,7 @@ var valueAt = (tweens, t, includeInstants) => {
6319
6451
  for (const tween of tweens) {
6320
6452
  if (tween.start > t || tween.duration === 0 && (tween.start === t ? !includeInstants : false)) continue;
6321
6453
  if (tween.duration === 0 || t >= tween.start + tween.duration) value = tween.to;
6322
- 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);
6323
6455
  }
6324
6456
  return value;
6325
6457
  };
@@ -7401,38 +7533,6 @@ function createEffects(THREE, sink, random) {
7401
7533
  };
7402
7534
  }
7403
7535
 
7404
- // src/fx/view-hold.ts
7405
- var VIEW_HOLD = {
7406
- /** The right hand's grip, from the eye. */
7407
- grip: [0.2, -0.2, -0.3],
7408
- /** A handgun's grip: higher and nearer the middle, the arms out in front. */
7409
- handgunGrip: [0.13, -0.15, -0.33],
7410
- /** A rocket launcher's grip: its tube rides on the shoulder, so the hands are higher. */
7411
- launcherGrip: [0.2, -0.14, -0.3],
7412
- /** A gun with no grip anchor: the rear of its bore, from the eye. */
7413
- rear: [0.2, -0.24, -0.12]
7414
- };
7415
- var scratch2 = { grip: null };
7416
- function placeInView(placement) {
7417
- const { THREE, hand, holder, camera, right } = placement;
7418
- holder.scale.setScalar(1);
7419
- hand.position.copy(camera.position);
7420
- hand.quaternion.copy(camera.quaternion);
7421
- hand.scale.setScalar(1);
7422
- holder.position.set(0, 0, 0);
7423
- hand.updateMatrixWorld(true);
7424
- if (!right) {
7425
- holder.position.set(VIEW_HOLD.rear[0], VIEW_HOLD.rear[1], VIEW_HOLD.rear[2] + placement.rear);
7426
- hand.updateMatrixWorld(true);
7427
- return;
7428
- }
7429
- scratch2.grip ?? (scratch2.grip = new THREE.Vector3());
7430
- const grip = hand.worldToLocal(right.part.localToWorld(scratch2.grip.copy(right.offset)));
7431
- const [x, y, z] = placement.kind === "handgun" ? VIEW_HOLD.handgunGrip : placement.kind === "launcher" ? VIEW_HOLD.launcherGrip : VIEW_HOLD.grip;
7432
- holder.position.set(x - grip.x, y - grip.y, z - grip.z);
7433
- hand.updateMatrixWorld(true);
7434
- }
7435
-
7436
7536
  // src/fx/heavy.ts
7437
7537
  import { LIGHT_PRIORITY as LIGHT_PRIORITY2, geometryKey, materialKey } from "@adatechnology/game-render-three";
7438
7538
  import { discardObject3D } from "@adatechnology/game-shapes-three";
@@ -7821,11 +7921,12 @@ function createClipEventHandler(world, session) {
7821
7921
  switch (event.type) {
7822
7922
  case "muzzle_flash":
7823
7923
  if (data.at === "ugl") {
7824
- 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));
7825
7925
  } else if (parts.muzzle && world.layers.muzzleFlash && (session.def.action.kind === "firearm" || session.def.action.kind === "launcher")) {
7826
7926
  const profile = muzzleSmokeOf({ caliber: session.caliber, weapon: session.def, suppressed: session.suppressed });
7827
7927
  const size = session.def.action.kind === "launcher" ? 2.2 : FLASH_BY_CASE[session.caseType];
7828
- 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));
7829
7930
  }
7830
7931
  break;
7831
7932
  case "eject_casing":
@@ -8366,7 +8467,9 @@ function workWeights(works, running) {
8366
8467
 
8367
8468
  // src/fx/viewmodel.ts
8368
8469
  var PI14 = Math.PI;
8369
- 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;
8370
8473
  var WALL_PULL = { margin: 0.03, max: 1.2, rate: 14, probe: 3 };
8371
8474
  var HAND_IN_VIEW = { floor: -0.88, max: 0.3, rate: 10 };
8372
8475
  var LAUNCH_VOLUME = 0.6;
@@ -8385,6 +8488,7 @@ var indexCurl = (turn2, sinceShot) => {
8385
8488
  const held = sinceShot < TRIGGER_FINGER.burstSeconds ? TRIGGER_FINGER.pulled - TRIGGER_FINGER.tremor * (0.5 + 0.5 * Math.sin(sinceShot * 220)) : 0;
8386
8489
  return Math.max(ready + (TRIGGER_FINGER.pulled - ready) * pull, held);
8387
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 };
8388
8492
  function createViewmodel(options) {
8389
8493
  const { THREE, scene, camera, arsenal, groundAt, audio } = options;
8390
8494
  const kit = modelKit(THREE);
@@ -8392,16 +8496,33 @@ function createViewmodel(options) {
8392
8496
  const layers = options.layers ?? createLayers();
8393
8497
  const distance = () => options.soundDistance?.() ?? 0;
8394
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
+ };
8395
8504
  particles.onBounce = (kind, hit) => {
8396
- if (kind === "magazine") audio?.thud();
8397
- if (kind !== "casing" || !audio) return;
8398
- const caseType = hit?.object.userData["caseType"];
8505
+ if (!audio || kind !== "casing" && kind !== "magazine") return;
8399
8506
  const surface = hit ? options.surfaceAt?.(hit.object.position.x, hit.object.position.z) : void 0;
8507
+ if (kind === "magazine") {
8508
+ audio.thud({ ...surface ? { surface } : {}, ...hit ? { speed: hit.speed, bounce: hit.bounce } : {} });
8509
+ return;
8510
+ }
8511
+ const caseType = hit?.object.userData["caseType"];
8400
8512
  audio.casing({ ...caseType ? { caseType } : {}, ...surface ? { surface } : {}, ...hit ? { speed: hit.speed, bounce: hit.bounce } : {} });
8401
8513
  };
8402
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");
8403
8515
  const ownVfx = options.effects ? void 0 : createVfxRenderer(THREE, { scene, camera, vfx: { profile: "medium" }, ...options.lights ? { lights: options.lights } : {} });
8404
- 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);
8405
8526
  const hand = new THREE.Group();
8406
8527
  hand.name = VIEWMODEL_NODE_NAME;
8407
8528
  scene.add(hand);
@@ -8427,6 +8548,7 @@ function createViewmodel(options) {
8427
8548
  const rootQuaternion = new THREE.Quaternion();
8428
8549
  const muzzleAt = new THREE.Vector3();
8429
8550
  const muzzleAlong = new THREE.Vector3();
8551
+ const posed = [];
8430
8552
  const world = {
8431
8553
  kit,
8432
8554
  scene,
@@ -8440,13 +8562,17 @@ function createViewmodel(options) {
8440
8562
  const played = audio?.cue(cue2, profile, lead);
8441
8563
  options.onSound?.({ weapon, cue: cue2, profile, ...played ? { played } : {} });
8442
8564
  },
8443
- 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 })
8444
8567
  };
8445
8568
  const heatMax = arsenal.rules.heat.displayMax || HEAT_DISPLAY_MAX_FALLBACK;
8446
8569
  const visual = arsenal.rules.heat.visual;
8447
8570
  const hasSuppressor = (parts) => parts.some((part) => arsenal.attachments.get(part.id)?.kind === "suppressor");
8448
8571
  const release = () => {
8449
8572
  if (!session) return;
8573
+ if (lens) options.viewmodelFov?.(null);
8574
+ lens = null;
8575
+ lensLayer = -1;
8450
8576
  session.attachments?.dispose();
8451
8577
  discardObject3D3(session.model.root);
8452
8578
  player = null;
@@ -8486,6 +8612,9 @@ function createViewmodel(options) {
8486
8612
  const modelBox = new THREE.Box3().setFromObject(model.root);
8487
8613
  const length = modelBox.getSize(new THREE.Vector3()).x;
8488
8614
  modelRear = modelBox.min.x;
8615
+ restBox.copy(modelBox);
8616
+ lens = null;
8617
+ lensLayer = -1;
8489
8618
  holder.add(model.root);
8490
8619
  const caliber = caliberOf(arsenal, def);
8491
8620
  const dims = caliber?.cartridge;
@@ -8548,6 +8677,42 @@ function createViewmodel(options) {
8548
8677
  let reach = 0;
8549
8678
  let pull = 0;
8550
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
+ };
8551
8716
  const ahead = new THREE.Vector3();
8552
8717
  const up = new THREE.Vector3();
8553
8718
  const seen = new THREE.Vector3();
@@ -8580,13 +8745,17 @@ function createViewmodel(options) {
8580
8745
  const family = session.model.parts.family;
8581
8746
  session.holderScale = 1;
8582
8747
  if (options.firstPerson?.()) {
8583
- 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);
8584
8752
  lift = 0;
8585
- pullFromWall(dt);
8753
+ pull = 0;
8586
8754
  return;
8587
8755
  }
8756
+ setLens(null);
8588
8757
  hand.scale.set(1, 1, 1);
8589
- 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 });
8590
8759
  hand.updateMatrixWorld(true);
8591
8760
  lift += (liftToKeepInView() - lift) * Math.min(1, dt * HAND_IN_VIEW.rate);
8592
8761
  if (lift > 1e-4) hand.position.addScaledVector(up.set(0, 1, 0).applyQuaternion(camera.quaternion), lift);
@@ -8662,6 +8831,10 @@ function createViewmodel(options) {
8662
8831
  if (session) {
8663
8832
  place(dt);
8664
8833
  player?.update(dt);
8834
+ if (posed.length) {
8835
+ hand.updateMatrixWorld(true);
8836
+ for (const emit of posed.splice(0)) emit();
8837
+ }
8665
8838
  const wantsMagazine = reloading || options.tool?.().reloading === true;
8666
8839
  magazineHand += ((wantsMagazine ? 1 : 0) - magazineHand) * Math.min(1, dt * HAND_HOLD.magRate);
8667
8840
  const tool = options.tool?.();
@@ -8677,10 +8850,11 @@ function createViewmodel(options) {
8677
8850
  }
8678
8851
  secondsSinceShot += dt;
8679
8852
  if (tool && tool.heat !== void 0 && tool.heat * heatMax > visual.smokeFrom && secondsSinceShot < visual.smokeSeconds && session.model.parts.muzzle) {
8680
- 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;
8681
8855
  while (smokeDebt > 1) {
8682
8856
  smokeDebt -= 1;
8683
- const k = 0.5 + (tool.heat * heatMax - visual.smokeFrom) / 60;
8857
+ const k = 0.5 + smokeLevel * SMOKE_THICKNESS_GAIN;
8684
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));
8685
8859
  }
8686
8860
  }
@@ -8707,6 +8881,7 @@ function createViewmodel(options) {
8707
8881
  if (!session || !mark) return null;
8708
8882
  hand.updateMatrixWorld(true);
8709
8883
  const at = worldPositionOf(session, mark, muzzleAt);
8884
+ if (lens) toWorldLens(camera, at, worldFovOf(), lens.fov);
8710
8885
  const along = muzzleAlong.set(1, 0, 0).applyQuaternion(session.model.root.getWorldQuaternion(rootQuaternion)).normalize();
8711
8886
  const kind = session.def.action.kind;
8712
8887
  return { position: [at.x, at.y, at.z], direction: [along.x, along.y, along.z], drawsFlash: kind === "firearm" || kind === "launcher" };
@@ -8716,7 +8891,8 @@ function createViewmodel(options) {
8716
8891
  hand.updateMatrixWorld(true);
8717
8892
  const targets = targetsOf(THREE, hold, session.model.root.getWorldQuaternion(rootQuaternion), magazineHand, hold.works ? workWeights(hold.works, player?.running ?? []) : void 0);
8718
8893
  const trigger = session.model.parts.trigger;
8719
- 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) };
8720
8896
  return limitSupportSpan(targets);
8721
8897
  },
8722
8898
  get inHand() {
@@ -8766,6 +8942,9 @@ function createWeaponFx(options) {
8766
8942
  ...options.contact ? { contact: options.contact } : {},
8767
8943
  ...options.bodyHold ? { bodyHold: options.bodyHold } : {},
8768
8944
  ...options.firstPerson ? { firstPerson: options.firstPerson } : {},
8945
+ ...options.viewmodelFov ? { viewmodelFov: options.viewmodelFov } : {},
8946
+ ...options.aim ? { aim: options.aim } : {},
8947
+ ...options.motion ? { motion: options.motion } : {},
8769
8948
  ...options.effects ? { effects: options.effects } : {},
8770
8949
  ...options.flyRocket === void 0 ? {} : { flyRocket: options.flyRocket },
8771
8950
  ...options.particleLimit ? { particleLimit: options.particleLimit } : {},
@@ -9858,6 +10037,7 @@ var TEXT = {
9858
10037
  reloadClip: "Reload of {weapon}: {kind}, clip {clip} ({seconds} s)",
9859
10038
  soundCue: "Sound: {cue} ({weapon})",
9860
10039
  cueRecorded: " \xB7 recorded",
10040
+ cueFitted: " \xB7 fitted copy of the recording",
9861
10041
  cueCarried: " \xB7 inside the next recording",
9862
10042
  gunshotSound: "Shot sound: {profile}, take {take}/4, pitch {pitch}, {space}, {distance} m{far}",
9863
10043
  gunshotRecorded: " \xB7 recorded take {takes}",
@@ -9882,7 +10062,7 @@ var TEXT = {
9882
10062
  fuseLength: "Dynamite fuse length",
9883
10063
  fuseLengthHint: "Metres of safety fuse (131 s per metre): a long one is set down lit, for a delayed trap",
9884
10064
  layers: { muzzle_flash: "Muzzle flash", casings: "Ejected casings", magazines: "Dropped magazines" },
9885
- 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)" }
9886
10066
  },
9887
10067
  "pt-BR": {
9888
10068
  group: "Armas",
@@ -9893,6 +10073,7 @@ var TEXT = {
9893
10073
  reloadClip: "Recarga de {weapon}: {kind}, clipe {clip} ({seconds} s)",
9894
10074
  soundCue: "Som: {cue} ({weapon})",
9895
10075
  cueRecorded: " \xB7 grava\xE7\xE3o",
10076
+ cueFitted: " \xB7 c\xF3pia medida da grava\xE7\xE3o",
9896
10077
  cueCarried: " \xB7 dentro da grava\xE7\xE3o seguinte",
9897
10078
  gunshotSound: "Som do tiro: {profile}, tomada {take}/4, tom {pitch}, {space}, {distance} m{far}",
9898
10079
  gunshotRecorded: " \xB7 grava\xE7\xE3o, tomada {takes}",
@@ -9917,7 +10098,7 @@ var TEXT = {
9917
10098
  fuseLength: "Pavio da dinamite",
9918
10099
  fuseLengthHint: "Metros de pavio de seguran\xE7a (131 s por metro): um longo \xE9 posto no ch\xE3o aceso, para armadilha com atraso",
9919
10100
  layers: { muzzle_flash: "Clar\xE3o do cano", casings: "C\xE1psulas ejetadas", magazines: "Pentes que caem" },
9920
- 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)" }
9921
10102
  }
9922
10103
  };
9923
10104
  var textOf = (locale) => TEXT[locale] ?? TEXT.en;
@@ -10041,7 +10222,12 @@ function createWeaponsTestModule(options = {}) {
10041
10222
  let host = null;
10042
10223
  let soundDistance = 0;
10043
10224
  let soundIndoors = false;
10044
- 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
+ };
10045
10231
  return {
10046
10232
  id: "weapons_area",
10047
10233
  name: text.name,
@@ -10094,7 +10280,8 @@ function createWeaponsTestModule(options = {}) {
10094
10280
  metrics: () => [
10095
10281
  { id: "casings_live", name: text.metrics.casings_live, read: () => fx?.casings ?? 0 },
10096
10282
  { id: "magazines_live", name: text.metrics.magazines_live, read: () => fx?.magazines ?? 0 },
10097
- { 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 },
10098
10285
  ...accessoryMetrics(accessoryText, () => host?.tool() ?? EMPTY_TOOL)
10099
10286
  ],
10100
10287
  attach(lent) {
@@ -10107,20 +10294,20 @@ function createWeaponsTestModule(options = {}) {
10107
10294
  engine: lentEngine,
10108
10295
  ...options.samples ? { samples: options.samples } : {},
10109
10296
  onGunshot: (shot, input, played) => {
10110
- const recorded = played.kind === "sample" && played.layers.length > 0 ? fill(text.gunshotRecorded, { takes: played.layers.map((layer) => `${layer.role} ${layer.take + 1}`).join(", ") }) : "";
10297
+ const recorded = played.kind === "sample" && played.layers.length > 0 ? fill(text.gunshotRecorded, { takes: played.layers.map((layer) => `${layer.role} ${layer.take + 1}`).join(", ") }) : played.kind === "fitted" ? text.cueFitted : "";
10111
10298
  lent.log(
10112
10299
  fill(text.gunshotSound, { profile: shot.profile, take: String(shot.take + 1), pitch: `${shot.pitch >= 1 ? "+" : ""}${((shot.pitch - 1) * 100).toFixed(1)} %`, space: text.spaces[input.space ?? "outdoor"], distance: String(Math.round(input.distance ?? 0)), far: shot.closeMix < 1 ? ` (${Math.round((1 - shot.closeMix) * 100)} % far)` : "" }) + recorded,
10113
10300
  { profile: shot.profile, take: shot.take, pitch: shot.pitch, space: input.space ?? "outdoor", distance: input.distance ?? 0, closeMix: shot.closeMix, delay: shot.delay, samples: shot.samples, played },
10114
10301
  "sound"
10115
10302
  );
10116
10303
  },
10117
- onCasing: (sound3, played) => lent.log(fill(text.casingSound, { case: sound3.kind, surface: text.surfaces[sound3.surface] }) + (played === "sample" ? text.cueRecorded : ""), { case: sound3.kind, surface: sound3.surface, sample: sound3.sample, played }, "sound")
10304
+ onCasing: (sound3, played) => lent.log(fill(text.casingSound, { case: sound3.kind, surface: text.surfaces[sound3.surface] }) + (played === "sample" ? text.cueRecorded : played === "fitted" ? text.cueFitted : ""), { case: sound3.kind, surface: sound3.surface, sample: sound3.sample, played }, "sound")
10118
10305
  });
10119
10306
  }
10120
10307
  let simulatedMs = 0;
10121
10308
  const offClock = lent.onFrame((dt) => simulatedMs += dt * 1e3);
10122
10309
  const created = createWeaponFx({
10123
- onSound: ({ weapon, cue: cue2, profile, played }) => lent.log(fill(text.soundCue, { weapon: arsenal.name(weapon), cue: text.soundCues[cue2] ?? cue2 }) + (played === "sample" ? text.cueRecorded : played === "carried" ? text.cueCarried : ""), { weapon, cue: cue2, weight: profile.weight, material: profile.material, ...played ? { played } : {} }, "sound"),
10310
+ onSound: ({ weapon, cue: cue2, profile, played }) => lent.log(fill(text.soundCue, { weapon: arsenal.name(weapon), cue: text.soundCues[cue2] ?? cue2 }) + (played === "sample" ? text.cueRecorded : played === "fitted" ? text.cueFitted : played === "carried" ? text.cueCarried : ""), { weapon, cue: cue2, weight: profile.weight, material: profile.material, ...played ? { played } : {} }, "sound"),
10124
10311
  THREE: lent.THREE,
10125
10312
  scene: lent.scene,
10126
10313
  camera: lent.camera,
@@ -10137,6 +10324,10 @@ function createWeaponsTestModule(options = {}) {
10137
10324
  contact: createLaserRaycast(lent.THREE, lent.scene, lent.camera, { now: () => simulatedMs }),
10138
10325
  ...lent.holdFrame ? { bodyHold: () => lent.holdFrame?.() ?? null } : {},
10139
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 }),
10140
10331
  ...lent.effects ? { effects: lent.effects } : {},
10141
10332
  ...lent.particleLimit ? { particleLimit: lent.particleLimit } : {},
10142
10333
  ...isLightBudget(lent.lights) ? { lights: lent.lights } : {},
@@ -10170,6 +10361,7 @@ function createWeaponsTestModule(options = {}) {
10170
10361
  const offEvents = lent.on((event) => {
10171
10362
  worldEffects.handle(event, lent.effects);
10172
10363
  if (event.type === "explosion") {
10364
+ if (lentEngine) return;
10173
10365
  const away = Math.hypot(event.point[0] - camera.position.x, event.point[1] - camera.position.y, event.point[2] - camera.position.z);
10174
10366
  ownAudio()?.play("explosion", Math.min(1, EXPLOSION_VOLUME_BASE + event.radius * EXPLOSION_VOLUME_PER_METRE), away);
10175
10367
  return;
@@ -11238,6 +11430,8 @@ export {
11238
11430
  weaponSampleDefaults,
11239
11431
  weaponSampleBank,
11240
11432
  createWeaponAudio,
11433
+ VIEW_HOLD,
11434
+ toWorldLens,
11241
11435
  BODY_ACTIONS,
11242
11436
  weaponProp,
11243
11437
  buildProp,
@@ -11272,6 +11466,7 @@ export {
11272
11466
  buildCartridge,
11273
11467
  buildCasing,
11274
11468
  buildRound,
11469
+ VIEW_MOTION,
11275
11470
  createWeaponFx,
11276
11471
  buildModeledProp,
11277
11472
  buildTripodMountedProp,
@@ -11299,4 +11494,4 @@ export {
11299
11494
  DEFAULT_EXPLOSIVES_SCENE,
11300
11495
  createExplosivesTestModule
11301
11496
  };
11302
- //# sourceMappingURL=chunk-GBO6L7CQ.js.map
11497
+ //# sourceMappingURL=chunk-BDYIF7JD.js.map