@adatechnology/game-test-room 0.27.0 → 0.28.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.d.ts CHANGED
@@ -485,7 +485,9 @@ interface RoomAvatar {
485
485
  */
486
486
  eye?(out?: THREE_NS.Vector3): THREE_NS.Vector3;
487
487
  /** Hides the head and neck from `camera` while the view is `first` (shadows, mirrors and other cameras keep seeing them). */
488
- setView?(mode: CameraMode, camera: THREE_NS.Object3D | null): void;
488
+ setView?(mode: CameraMode, camera: THREE_NS.Object3D | null, options?: {
489
+ readonly arms?: boolean;
490
+ }): void;
489
491
  /**
490
492
  * Plays the strike `clipId` (on the body, in both views) and answers the seconds until its impact, or null when this avatar has no such clip. The room then waits that long
491
493
  * before the blow lands (`strikeClips`); an avatar without `strike`, or one that answers null, gets the blow at the click.
@@ -704,6 +706,19 @@ interface SceneryTap {
704
706
  /** Turn about the vertical axis, radians (default 0). */
705
707
  rotationY?: number;
706
708
  }
709
+ /** What the room needs of first-person arms: a root it puts in the scene, and the pose of each frame (the eye, the hand targets of the tool and the stance, how fast the body moves). */
710
+ interface RoomFirstPersonArms {
711
+ readonly root: THREE_NS.Object3D;
712
+ update(dt: number, state: {
713
+ camera: THREE_NS.Object3D;
714
+ targets?: HandTargets | null;
715
+ stance?: string;
716
+ speed?: number;
717
+ running?: boolean;
718
+ crouch?: boolean;
719
+ }): void;
720
+ dispose(): void;
721
+ }
707
722
  interface TestRoomOptions {
708
723
  THREE: ThreeNamespace;
709
724
  /** Element the bench fills (canvas + HUD). */
@@ -790,6 +805,11 @@ interface TestRoomOptions {
790
805
  ui?: boolean;
791
806
  /** The player's body (see `TestRoom.setAvatar`). */
792
807
  avatar?: RoomAvatar;
808
+ /**
809
+ * Arms drawn for the first-person view (the viewmodel arms, `createFirstPersonArms` of game-characters-three fits it): in first person the room hides the body's arms from the eye, draws
810
+ * these on `FIRST_PERSON_LAYER` with the tool in the hands, and brings their hands to the tool's grips. Absent: the body's arms are the ones seen.
811
+ */
812
+ firstPersonArms?: RoomFirstPersonArms;
793
813
  /** The view the room starts in (default `first`; the V key and `TestRoom.setCameraMode` change it). */
794
814
  cameraMode?: CameraMode;
795
815
  /**
@@ -1299,4 +1319,4 @@ declare function encodeBenchState(controls: readonly BenchControl[], layers: rea
1299
1319
  /** Reads an encoded state as hostile input: unknown ids, NaN, options not offered and malformed pairs are dropped; numbers are clamped. */
1300
1320
  declare function decodeBenchState(encoded: string, controls: readonly ModuleControl[], layers: readonly ModuleLayer[]): Map<string, ControlValue>;
1301
1321
 
1302
- export { type AnimationInfo, type BenchControl, type BenchHandle, type BenchLayer, type BuildFromSpecOptions, type CameraMode, DEFAULT_HAND_STANCES, DEFAULT_STRIKE_CLIPS, DEFAULT_TRIALS, type HudState, type InspectInfo, type KickLeg, LOADING_CLASS, LOADING_CSS, type LoadingOptions, type LoadingState, type LogEntry, MATERIAL_COLORS, type ModuleReport, type NewEntry, type NewLogEntry, type ProjectileModel, type ProjectileQuery, type PunchHand, type RoomAvatar, type RoomAvatarState, type RoomDoorEvent, type RoomDoorState, type RoomFirstPersonState, type RoomGraphicsOptions, type RoomLoading, type RoomMetricId, type RoomPushEvent, type RoomPushState, type RoomReachView, type RoomSeat, type RoomStrings, type ScenarioContext, type ScenarioReport, type SceneryObject, type SceneryTap, type SpawnedObject, type StrikeComboOption, TRIAL_KINDS, type TestRoom, type TestRoomItem, type TestRoomLocale, type TestRoomLog, type TestRoomOptions, type TestRoomScenario, type TestTable, type TestTableContext, type TestTableObject, type TestTableOptions, type TestTablePage, type ThirdPersonOptions, type ThreeNamespace, type ToolMenuGroup, type ToolMenuSection, type ToolMenuTool, type TrialKind, type TrialTool, type UseStatus, type WritableLog, brickWallSpec, buildFromSpec, createLoading, createLog, createTestRoom, createTestTable, decodeBenchState, encodeBenchState, fitRow, loadingMarkup, locales, nextPaint, resolveStrings, rowCentres, trialView };
1322
+ export { type AnimationInfo, type BenchControl, type BenchHandle, type BenchLayer, type BuildFromSpecOptions, type CameraMode, DEFAULT_HAND_STANCES, DEFAULT_STRIKE_CLIPS, DEFAULT_TRIALS, type HudState, type InspectInfo, type KickLeg, LOADING_CLASS, LOADING_CSS, type LoadingOptions, type LoadingState, type LogEntry, MATERIAL_COLORS, type ModuleReport, type NewEntry, type NewLogEntry, type ProjectileModel, type ProjectileQuery, type PunchHand, type RoomAvatar, type RoomAvatarState, type RoomDoorEvent, type RoomDoorState, type RoomFirstPersonArms, type RoomFirstPersonState, type RoomGraphicsOptions, type RoomLoading, type RoomMetricId, type RoomPushEvent, type RoomPushState, type RoomReachView, type RoomSeat, type RoomStrings, type ScenarioContext, type ScenarioReport, type SceneryObject, type SceneryTap, type SpawnedObject, type StrikeComboOption, TRIAL_KINDS, type TestRoom, type TestRoomItem, type TestRoomLocale, type TestRoomLog, type TestRoomOptions, type TestRoomScenario, type TestTable, type TestTableContext, type TestTableObject, type TestTableOptions, type TestTablePage, type ThirdPersonOptions, type ThreeNamespace, type ToolMenuGroup, type ToolMenuSection, type ToolMenuTool, type TrialKind, type TrialTool, type UseStatus, type WritableLog, brickWallSpec, buildFromSpec, createLoading, createLog, createTestRoom, createTestTable, decodeBenchState, encodeBenchState, fitRow, loadingMarkup, locales, nextPaint, resolveStrings, rowCentres, trialView };
@@ -473,6 +473,7 @@ var GameTestRoom = (() => {
473
473
  if (!state.opened) return "sealed";
474
474
  return void 0;
475
475
  };
476
+ var FIRST_PERSON_LAYER = 9;
476
477
  var TWIST_LIFT = 0.012;
477
478
  var TURNING = 0.7;
478
479
  var smooth = (k) => k * k * (3 - 2 * k);
@@ -38174,7 +38175,9 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
38174
38175
  dimensions: {
38175
38176
  length: 0.801,
38176
38177
  barrel: 0.559,
38177
- emptyMass: 39
38178
+ emptyMass: 39,
38179
+ barrels: 6,
38180
+ barrelMass: 2
38178
38181
  },
38179
38182
  archetype: "rifle_7_62x39",
38180
38183
  sources: [
@@ -39154,19 +39157,41 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
39154
39157
  }
39155
39158
  };
39156
39159
  var DEFAULT_HEAT = {
39157
- cap: 140,
39158
- displayMax: 120,
39159
- decayBase: 1.6,
39160
- decayPerPoint: 0.03,
39161
- faultFrom: 60,
39162
- faultSpan: 40,
39163
- perShot: { shell: 3, big: 6, tiny: 0.5, pistol: 1.2, rifle: 2 },
39160
+ cap: 900,
39161
+ displayMax: 750,
39162
+ faultFrom: 350,
39163
+ faultSpan: 200,
39164
+ specificHeat: 490,
39165
+ barrelShare: { shell: 0.48, big: 0.48, tiny: 0.3, pistol: 0.3, rifle: 0.48 },
39166
+ barrelKilogramsPerMetre: {
39167
+ pistol: 1,
39168
+ revolver: 1.4,
39169
+ smg: 1.2,
39170
+ carbine: 1.6,
39171
+ rifle: 1.6,
39172
+ dmr: 2.4,
39173
+ sniper: 2.4,
39174
+ hunting: 2.4,
39175
+ machine_gun: 3,
39176
+ shotgun: 1.1
39177
+ },
39178
+ barrelFallback: { metres: 0.4, kilogramsPerMetre: 1.6 },
39179
+ cooling: { baseSeconds: 30, secondsPerKilogram: 250 },
39164
39180
  perRocket: 35,
39165
- visual: { smokeFrom: 70, smokeSeconds: 2.5, glowFrom: 80, glowSpan: 40 }
39181
+ visual: { smokeFrom: 250, smokeSpan: 300, smokeSeconds: 2.5, glowFrom: 450, glowSpan: 300 }
39166
39182
  };
39167
39183
  var GALLERY_HEAT = {
39168
39184
  ...DEFAULT_HEAT,
39169
- perShot: { shell: 6, big: 9, tiny: 1.2, pistol: 3, rifle: 4.5 }
39185
+ cap: 140,
39186
+ displayMax: 120,
39187
+ faultFrom: 60,
39188
+ faultSpan: 40,
39189
+ visual: { smokeFrom: 70, smokeSpan: 100, smokeSeconds: 2.5, glowFrom: 80, glowSpan: 40 },
39190
+ gallery: {
39191
+ perShot: { shell: 6, big: 9, tiny: 1.2, pistol: 3, rifle: 4.5 },
39192
+ decayBase: 1.6,
39193
+ decayPerPoint: 0.03
39194
+ }
39170
39195
  };
39171
39196
  var defaultFaultChance = (context) => {
39172
39197
  const factor = context.def.action.kind === "firearm" ? context.def.action.faultFactor ?? 1 : 1;
@@ -39449,17 +39474,55 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
39449
39474
  ...explosive.stunSeconds !== void 0 ? { stunSeconds: explosive.stunSeconds } : {}
39450
39475
  };
39451
39476
  };
39477
+ var EPS22 = 1e-9;
39478
+ var MANUAL_MODES = /* @__PURE__ */ new Set(["pump", "bolt", "lever"]);
39479
+ var weaponSpeed = (action, caliber) => action.muzzleSpeed ?? caliber.muzzleSpeed * (action.barrel ?? 1);
39480
+ var weaponNoise = (action, caliber) => action.noise ?? caliber.noise ?? 0;
39481
+ var timingsOf = (action) => "timings" in action ? action.timings : void 0;
39482
+ var drawSeconds = (action, rules) => timingsOf(action)?.draw ?? rules.draw;
39483
+ var clearSeconds = (action, rules) => timingsOf(action)?.clear ?? rules.clear;
39484
+ var manualCycleSeconds = (action, mode) => MANUAL_MODES.has(mode) ? action.timings?.cycle ?? 0 : 0;
39485
+ var planRounds = (action, mode, ammo, firstAt, frameEnd) => {
39486
+ const interval = 60 / action.rpm;
39487
+ const times = [];
39488
+ if (mode === "auto") {
39489
+ for (let t = firstAt; times.length < ammo && (times.length === 0 ? t <= frameEnd + EPS22 : t < frameEnd - EPS22); t += interval) times.push(t);
39490
+ return times;
39491
+ }
39492
+ if (mode === "burst") {
39493
+ const count = Math.min(action.burst ?? 3, ammo);
39494
+ for (let k = 0; k < count; k++) times.push(firstAt + k * interval);
39495
+ return times;
39496
+ }
39497
+ if (mode === "volley") return Array.from({ length: Math.min(action.magazine, ammo) }, () => firstAt);
39498
+ return [firstAt];
39499
+ };
39452
39500
  var SUBSTEP_SECONDS = 1 / 60;
39453
- var decayHeat = (heat, seconds, rules) => {
39501
+ var barrelMassOf = (def, rules) => {
39502
+ const dimensions = def.dimensions;
39503
+ const kilogramsPerMetre = rules.barrelKilogramsPerMetre[def.category] ?? rules.barrelFallback.kilogramsPerMetre;
39504
+ const single = dimensions?.barrelMass ?? (dimensions?.barrel ?? rules.barrelFallback.metres) * kilogramsPerMetre;
39505
+ return single * (dimensions?.barrels ?? 1);
39506
+ };
39507
+ var coolingTimeConstant = (barrelMass, rules) => rules.cooling.baseSeconds + rules.cooling.secondsPerKilogram * barrelMass;
39508
+ var decayHeat = (heat, seconds, barrelMass, rules) => {
39509
+ if (heat <= 0) return 0;
39510
+ if (rules.gallery) return decayGallery(heat, seconds, rules.gallery);
39511
+ return heat * Math.exp(-seconds / coolingTimeConstant(barrelMass, rules));
39512
+ };
39513
+ var decayGallery = (heat, seconds, gallery) => {
39454
39514
  const steps = Math.max(1, Math.ceil(seconds / SUBSTEP_SECONDS - 1e-9));
39455
39515
  const step = seconds / steps;
39456
39516
  let current = heat;
39457
- for (let i = 0; i < steps && current > 0; i++) current = Math.max(0, current - step * (rules.decayBase + current * rules.decayPerPoint));
39517
+ for (let i = 0; i < steps && current > 0; i++) current = Math.max(0, current - step * (gallery.decayBase + current * gallery.decayPerPoint));
39458
39518
  return current;
39459
39519
  };
39460
- var heatPerRound = (action, caliber, rules) => {
39520
+ var heatPerRound = ({ action, caliber, energy, barrelMass, rules }) => {
39461
39521
  if (action.kind === "launcher") return rules.perRocket;
39462
- return caliber?.caseType ? rules.perShot[caliber.caseType] : 0;
39522
+ const caseType = caliber?.caseType;
39523
+ if (!caseType) return 0;
39524
+ if (rules.gallery) return rules.gallery.perShot[caseType];
39525
+ return rules.barrelShare[caseType] * energy / (barrelMass * rules.specificHeat);
39463
39526
  };
39464
39527
  var burstNoise = (base, recentShots, rules) => base * (1 + Math.min(rules.cap, rules.step * recentShots));
39465
39528
  var magazineOut = { stage: "magazine_out", at: 0 };
@@ -39586,29 +39649,6 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
39586
39649
  return roll.ammoQuality.faultChance > 0 && roll.random() < roll.ammoQuality.faultChance ? "misfire" : null;
39587
39650
  };
39588
39651
  var isDudWithoutJam = (action, fault) => fault === "misfire" && action.kind === "firearm" && MISFIRE_ONLY.has(action.family);
39589
- var EPS22 = 1e-9;
39590
- var MANUAL_MODES = /* @__PURE__ */ new Set(["pump", "bolt", "lever"]);
39591
- var weaponSpeed = (action, caliber) => action.muzzleSpeed ?? caliber.muzzleSpeed * (action.barrel ?? 1);
39592
- var weaponNoise = (action, caliber) => action.noise ?? caliber.noise ?? 0;
39593
- var timingsOf = (action) => "timings" in action ? action.timings : void 0;
39594
- var drawSeconds = (action, rules) => timingsOf(action)?.draw ?? rules.draw;
39595
- var clearSeconds = (action, rules) => timingsOf(action)?.clear ?? rules.clear;
39596
- var manualCycleSeconds = (action, mode) => MANUAL_MODES.has(mode) ? action.timings?.cycle ?? 0 : 0;
39597
- var planRounds = (action, mode, ammo, firstAt, frameEnd) => {
39598
- const interval = 60 / action.rpm;
39599
- const times = [];
39600
- if (mode === "auto") {
39601
- for (let t = firstAt; times.length < ammo && (times.length === 0 ? t <= frameEnd + EPS22 : t < frameEnd - EPS22); t += interval) times.push(t);
39602
- return times;
39603
- }
39604
- if (mode === "burst") {
39605
- const count = Math.min(action.burst ?? 3, ammo);
39606
- for (let k = 0; k < count; k++) times.push(firstAt + k * interval);
39607
- return times;
39608
- }
39609
- if (mode === "volley") return Array.from({ length: Math.min(action.magazine, ammo) }, () => firstAt);
39610
- return [firstAt];
39611
- };
39612
39652
  var activeParts = (fitted, catalogue) => fitted.flatMap((part) => {
39613
39653
  const def = catalogue.get(part.id);
39614
39654
  const isOn = def?.kind !== "light" && def?.kind !== "laser" ? true : part.on === true;
@@ -39841,6 +39881,10 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
39841
39881
  get safety() {
39842
39882
  return this._safety;
39843
39883
  }
39884
+ /** Kg of steel that heat up with the shots. */
39885
+ get barrelMass() {
39886
+ return barrelMassOf(this.def, this.rules.heat);
39887
+ }
39844
39888
  get heat() {
39845
39889
  return this._heat;
39846
39890
  }
@@ -39893,7 +39937,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
39893
39937
  }
39894
39938
  advance(seconds) {
39895
39939
  if (seconds <= 0) return;
39896
- this._heat = decayHeat(this._heat, seconds, this.rules.heat);
39940
+ this._heat = decayHeat(this._heat, seconds, this.barrelMass, this.rules.heat);
39897
39941
  this.clock += seconds;
39898
39942
  }
39899
39943
  /** Complete what falls at or before `t`: scheduled events and the reload steps. */
@@ -40445,7 +40489,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
40445
40489
  res.shots.push(shot);
40446
40490
  }
40447
40491
  this._ammo -= 1;
40448
- this._heat = Math.min(this.rules.heat.cap, this._heat + heatPerRound(a, cal, this.rules.heat) * this.load.heatFactor);
40492
+ this._heat = Math.min(this.rules.heat.cap, this._heat + heatPerRound({ action: a, caliber: cal, energy: energy * pellets, barrelMass: this.barrelMass, rules: this.rules.heat }) * this.load.heatFactor);
40449
40493
  this.readyAt = t + 60 / a.rpm;
40450
40494
  this.extra = Math.min(bloomNow + (a.bloomDeg ?? 0), a.spreadDeg * (BLOOM_CAP - 1));
40451
40495
  this.extraClock = t;
@@ -40534,7 +40578,7 @@ void main(){vec2 d=gl_PointCoord-.5;float r=length(d)*2.;if(r>1.)discard;float a
40534
40578
  }
40535
40579
  const t = Math.max(start, this.readyAt);
40536
40580
  this._ammo -= 1;
40537
- this._heat = Math.min(this.rules.heat.cap, this._heat + heatPerRound(a, null, this.rules.heat));
40581
+ this._heat = Math.min(this.rules.heat.cap, this._heat + heatPerRound({ action: a, caliber: null, energy: 0, barrelMass: this.barrelMass, rules: this.rules.heat }));
40538
40582
  this.emit({ type: "shot", shots: 0, rounds: 1, mode: this.mode, at: t });
40539
40583
  return result("ok", { uses: launcherUses(a, this.explosive, origin, dir), recoil: [a.recoil[0], a.recoil[1]], noise: a.noise ?? 0, rounds: 1, roundTimes: [t - start] });
40540
40584
  }
@@ -49409,6 +49453,7 @@ void main(){
49409
49453
  }
49410
49454
 
49411
49455
  // src/player.ts
49456
+ var RECOIL_RETURN = { share: 0.7, seconds: 0.25 };
49412
49457
  var SLAM_MIN_SPEED2 = 1.5;
49413
49458
  var LANDING_MIN_SPEED = 1;
49414
49459
  var EYE = 1.7;
@@ -49438,6 +49483,18 @@ void main(){
49438
49483
  const cam = o.camera;
49439
49484
  let yaw = 0;
49440
49485
  let pitch = 0;
49486
+ const recoilReturn = o.recoilReturn ?? RECOIL_RETURN;
49487
+ const recoilDebt = { pitch: 0, yaw: 0 };
49488
+ const returnRecoil = (dt) => {
49489
+ if (Math.abs(recoilDebt.pitch) + Math.abs(recoilDebt.yaw) < 1e-6) return;
49490
+ const k = 1 - Math.exp(-3 * dt / Math.max(1e-3, recoilReturn.seconds));
49491
+ const back = { pitch: recoilDebt.pitch * k, yaw: recoilDebt.yaw * k };
49492
+ recoilDebt.pitch -= back.pitch;
49493
+ recoilDebt.yaw -= back.yaw;
49494
+ pitch = Math.max(-1.5, Math.min(1.5, pitch - back.pitch));
49495
+ yaw -= back.yaw;
49496
+ apply();
49497
+ };
49441
49498
  let y = 0;
49442
49499
  let eye = EYE;
49443
49500
  let crouch = 0;
@@ -49779,6 +49836,7 @@ void main(){
49779
49836
  return stunLeft;
49780
49837
  },
49781
49838
  update(dt) {
49839
+ returnRecoil(dt);
49782
49840
  if (stunLeft > 0) {
49783
49841
  stunLeft = Math.max(0, stunLeft - dt);
49784
49842
  stunPhase += dt * 2.4;
@@ -49827,6 +49885,8 @@ void main(){
49827
49885
  },
49828
49886
  setPose(position, yw, pt2) {
49829
49887
  knocked = null;
49888
+ recoilDebt.pitch = 0;
49889
+ recoilDebt.yaw = 0;
49830
49890
  px.x = position[0];
49831
49891
  px.z = position[2];
49832
49892
  floorLevel = floorHere();
@@ -49839,6 +49899,8 @@ void main(){
49839
49899
  },
49840
49900
  look(position, target) {
49841
49901
  knocked = null;
49902
+ recoilDebt.pitch = 0;
49903
+ recoilDebt.yaw = 0;
49842
49904
  px.x = position[0];
49843
49905
  px.z = position[2];
49844
49906
  floorLevel = floorHere();
@@ -49853,8 +49915,11 @@ void main(){
49853
49915
  apply();
49854
49916
  },
49855
49917
  kick(pitchDeg, yawDeg) {
49918
+ const before = pitch;
49856
49919
  pitch = Math.max(-1.5, Math.min(1.5, pitch + pitchDeg * Math.PI / 180));
49857
49920
  yaw += yawDeg * Math.PI / 180;
49921
+ recoilDebt.pitch += (pitch - before) * recoilReturn.share;
49922
+ recoilDebt.yaw += yawDeg * Math.PI / 180 * recoilReturn.share;
49858
49923
  apply();
49859
49924
  },
49860
49925
  moveBy(dx, dz) {
@@ -55519,6 +55584,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
55519
55584
  const scene2 = options.scene ?? new THREE.Scene();
55520
55585
  const camera2 = options.camera ?? new THREE.PerspectiveCamera(72, width() / height(), 0.05, 140);
55521
55586
  if (!options.camera) scene2.add(camera2);
55587
+ camera2.layers.disable(FIRST_PERSON_LAYER);
55522
55588
  const resize = () => {
55523
55589
  const w = width();
55524
55590
  const h = height();
@@ -56610,10 +56676,21 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
56610
56676
  return { ...pose, position: pose.at(camDistance), free: camDistance };
56611
56677
  };
56612
56678
  const thirdActive = () => cameraMode === "third" && avatar !== null;
56679
+ const firstPersonArms = options.firstPersonArms ?? null;
56680
+ if (firstPersonArms) {
56681
+ firstPersonArms.root.visible = cameraMode === "first";
56682
+ scene2.add(firstPersonArms.root);
56683
+ }
56613
56684
  const syncAvatarVisible = () => {
56685
+ if (firstPersonArms) firstPersonArms.root.visible = cameraMode === "first";
56614
56686
  if (!avatar) return;
56615
56687
  avatar.root.visible = cameraMode === "third" || !!avatar.eye;
56616
- avatar.setView?.(cameraMode, camera2);
56688
+ avatar.setView?.(cameraMode, camera2, { arms: firstPersonArms !== null });
56689
+ };
56690
+ const updateFirstPersonArms = (dt) => {
56691
+ if (!firstPersonArms || cameraMode !== "first") return;
56692
+ firstPersonArms.update(dt, { camera: camera2, targets: lastHand.targets, stance: lastHand.stance, speed: player.speed, running: avatarState.running, crouch: avatarState.crouch });
56693
+ firstPersonArms.root.traverse((node) => node.layers.set(FIRST_PERSON_LAYER));
56617
56694
  };
56618
56695
  const setCameraMode = (mode) => {
56619
56696
  if (mode !== "first" && mode !== "third") return;
@@ -56697,6 +56774,39 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
56697
56774
  };
56698
56775
  let handSource = null;
56699
56776
  let muzzleSource = null;
56777
+ let viewmodelFov = null;
56778
+ const firstPersonCamera = new THREE.PerspectiveCamera(72, 1, 0.01, 30);
56779
+ firstPersonCamera.layers.set(FIRST_PERSON_LAYER);
56780
+ let firstPersonPasses = 0;
56781
+ const drawFirstPersonLayer = () => {
56782
+ if (!firstPersonArms && viewmodelFov === null) return;
56783
+ const renderer2 = gl;
56784
+ if (typeof renderer2.clearDepth !== "function" || typeof renderer2.render !== "function") return;
56785
+ if (firstPersonPasses++ % 60 === 0) {
56786
+ scene2.traverse((node) => {
56787
+ if (node.isLight) node.layers.enable(FIRST_PERSON_LAYER);
56788
+ });
56789
+ }
56790
+ firstPersonCamera.position.copy(camera2.position);
56791
+ firstPersonCamera.quaternion.copy(camera2.quaternion);
56792
+ const fov = viewmodelFov ?? camera2.fov;
56793
+ if (firstPersonCamera.fov !== fov || firstPersonCamera.aspect !== camera2.aspect) {
56794
+ firstPersonCamera.fov = fov;
56795
+ firstPersonCamera.aspect = camera2.aspect;
56796
+ firstPersonCamera.updateProjectionMatrix();
56797
+ }
56798
+ firstPersonCamera.updateMatrixWorld();
56799
+ const { autoClear } = renderer2;
56800
+ const { background, fog } = scene2;
56801
+ renderer2.autoClear = false;
56802
+ scene2.background = null;
56803
+ scene2.fog = null;
56804
+ renderer2.clearDepth();
56805
+ renderer2.render(scene2, firstPersonCamera);
56806
+ scene2.background = background;
56807
+ scene2.fog = fog;
56808
+ renderer2.autoClear = autoClear ?? true;
56809
+ };
56700
56810
  let debugView = null;
56701
56811
  let lastHand = { stance: "relaxed", triggerFinger: null, targets: null };
56702
56812
  const strikeClips = options.strikeClips ?? DEFAULT_STRIKE_CLIPS;
@@ -56844,7 +56954,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
56844
56954
  const line = new THREE.Line(g, tracerMat);
56845
56955
  line.frustumCulled = false;
56846
56956
  scene2.add(line);
56847
- tr = { line, life: 0 };
56957
+ tr = { line, life: 0, follows: false };
56848
56958
  tracers.push(tr);
56849
56959
  }
56850
56960
  const attr = tr.line.geometry.getAttribute("position");
@@ -56853,6 +56963,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
56853
56963
  attr.needsUpdate = true;
56854
56964
  tr.line.visible = true;
56855
56965
  tr.life = 0.07;
56966
+ tr.follows = muzzleSource !== null;
56856
56967
  };
56857
56968
  const fireZones = [];
56858
56969
  const burningStains = [];
@@ -58168,10 +58279,16 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
58168
58279
  }
58169
58280
  };
58170
58281
  const simulateAfterModules = (dt) => {
58282
+ const drawn = tracers.some((tr) => tr.life > 0 && tr.follows) ? muzzleSource?.() ?? null : null;
58171
58283
  for (const tr of tracers) {
58172
58284
  if (tr.life > 0) {
58173
58285
  tr.life -= dt;
58174
58286
  if (tr.life <= 0) tr.line.visible = false;
58287
+ else if (tr.follows && drawn) {
58288
+ const attr = tr.line.geometry.getAttribute("position");
58289
+ attr.setXYZ(0, drawn.position[0], drawn.position[1], drawn.position[2]);
58290
+ attr.needsUpdate = true;
58291
+ }
58175
58292
  }
58176
58293
  }
58177
58294
  flush();
@@ -58205,6 +58322,7 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
58205
58322
  };
58206
58323
  const updatePresent = (dt) => {
58207
58324
  reachAvatar();
58325
+ updateFirstPersonArms(dt);
58208
58326
  if (progress && simTime - progress.at > 0.25) progress = null;
58209
58327
  ui?.flushLog();
58210
58328
  hudAcc += dt;
@@ -58554,6 +58672,9 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
58554
58672
  if (handSource === source) handSource = null;
58555
58673
  };
58556
58674
  },
58675
+ setViewmodelFov(degrees) {
58676
+ viewmodelFov = degrees;
58677
+ },
58557
58678
  setMuzzleSource(source) {
58558
58679
  muzzleSource = source;
58559
58680
  return () => {
@@ -59285,7 +59406,9 @@ ${SURFACE_GLSL}`).replace("#include <color_fragment>", "#include <color_fragment
59285
59406
  for (const batcher of batchers.values()) batcher.update();
59286
59407
  const restoreCamera = room.applyCamera();
59287
59408
  graphics?.beforeRender(camera2);
59288
- gl.render(scene2, debugView ?? moduleViews.main(camera2));
59409
+ const view = debugView ?? moduleViews.main(camera2);
59410
+ gl.render(scene2, view);
59411
+ if (view === camera2 && cameraMode === "first") drawFirstPersonLayer();
59289
59412
  moduleViews.render(scene2);
59290
59413
  graphics?.afterRender();
59291
59414
  restoreCamera();