swb_base/PlayerCameraHandler.cs

PlayerCameraHandler component that updates the player's camera field of view (FOV) each frame. It smooths FOV changes, applies aim/zoom and scope FOVs, and adjusts transition speeds based on weapon aim info and perk/tech modifiers.

Native Interop
using SWB.Shared;

namespace SWB.Base;

public class PlayerCameraHandler : Component
{
	[Property] public float ThirdPersonAimZoom { get; set; } = 1.4f;

	public Weapon Weapon { get; set; }

	float targetPlayerFOV = -1;
	float finalPlayerFOV;
	float playerFOVSpeed = 1;
	float cachedFOV = -1f;
	float cachedVerticalFOV;

	IPlayerBase player => Weapon.Owner;

	protected override void OnStart()
	{
		if ( IsProxy )
			Enabled = false;
	}

	protected override void OnEnabled()
	{
		targetPlayerFOV = -1;
		playerFOVSpeed = 1;
	}

	protected override void OnDestroy()
	{
		if ( IsProxy || player is null ) return;
		player.FieldOfView = Screen.CreateVerticalFieldOfView( Preferences.FieldOfView );
	}

	protected override void OnUpdate()
	{
		if ( targetPlayerFOV == -1 )
		{
			targetPlayerFOV = Preferences.FieldOfView;
			finalPlayerFOV = Preferences.FieldOfView;
		}

		var decayRate = playerFOVSpeed * 10f;
		if ( decayRate > 0f )
		{
			var halfLife = System.MathF.Log( 2f ) / decayRate;
			finalPlayerFOV = MathX.ExponentialDecay( finalPlayerFOV, targetPlayerFOV, halfLife, RealTime.Delta );
		}

		if ( finalPlayerFOV != cachedFOV )
		{
			cachedFOV = finalPlayerFOV;
			cachedVerticalFOV = Screen.CreateVerticalFieldOfView( finalPlayerFOV );
		}
		player.FieldOfView = cachedVerticalFOV;

		// Initialize the target vectors for this frame
		targetPlayerFOV = Preferences.FieldOfView;

		HandleIronFOV();
	}

	void HandleIronFOV()
	{
		var isAiming = !Weapon.ShouldTuckVar && Weapon.IsAiming;
		if ( isAiming && !Weapon.IsReloading )
		{
			var aimZoom = Weapon.AimInfo.PlayerFOVZoom;

			if ( !player.IsFirstPerson && aimZoom <= 0 )
				aimZoom = ThirdPersonAimZoom;

			if ( aimZoom > 0 )
				targetPlayerFOV = CalculateZoomedFOV( Preferences.FieldOfView, aimZoom );

			if ( Weapon.IsScoping && Weapon.ScopeInfo.Magnification > 0 )
				// ⚠️ Relative to the PLAYER's own FOV, so 2.5x is 2.5x for everyone.
				targetPlayerFOV = Preferences.FieldOfView / Weapon.ScopeInfo.Magnification;
			else if ( Weapon.IsScoping && Weapon.ScopeInfo.FOV > 0 )
				targetPlayerFOV = Weapon.ScopeInfo.FOV;

			// ⚠️ DEADSHOT AND QUICKDRAW BOTH SCALE THE AIM-IN ZOOM. There are two FOVs
			// in an ADS — the viewmodel's and the player camera's — handled in two
			// different files. Speeding up only the one in ViewModelHandler would leave
			// the world zooming at its old rate behind a gun that had already finished
			// moving.
			//
			// ⛔ THAT IS EXACTLY WHAT HAPPENED WITH THE TECH NODE. Quickdraw was wired in
			// ViewModelHandler — covering the pose rate and the VIEWMODEL FOV — and this
			// line was missed, so the camera kept its own zoom rate for one build. The
			// two files are the whole reason this comment exists, and it still did not
			// stop the second one being forgotten. If a third ADS rate ever appears,
			// give all three one shared accessor rather than a third copy of this line.
			//
			// ⚠️ THE SECOND NODE ARRIVED, SO THE ACCESSOR ABOVE ASKED FOR NOW EXISTS.
			// `TechEffects.AdsSpeedFactor` is Quickdraw x Emplacement, read identically
			// here and in ViewModelHandler — a node added to it reaches both FOVs at
			// once. Emplacement is why it could not wait: at x0.25 a one-sided miss
			// desyncs the world zoom from the gun pose by FOUR times, where Quickdraw's
			// 1.20 was barely visible and still shipped wrong for a build.
			playerFOVSpeed = Weapon.AimInfo.AimInFOVSpeed
				* NZombies.PerkEffects.AimSpeedMultiplierFor( Weapon )
				* NZombies.TechEffects.AdsSpeedFactor( Weapon );
		}
		else
		{
			playerFOVSpeed = Weapon.AimInfo.AimOutFOVSpeed;
		}
	}

	/// <summary>
	/// Take the camera’s current FOV, convert it into a projected view size, divide that size by the zoom factor, then convert it back into a new FOV.
	/// </summary>
	static float CalculateZoomedFOV( float baseFOV, float zoom )
	{
		var halfFOV = MathX.DegreeToRadian( baseFOV ) * 0.5f;
		return MathX.RadianToDegree( 2f * System.MathF.Atan( System.MathF.Tan( halfFOV ) / zoom ) );
	}
}