swb_base/ViewModelHandler.cs
using SWB.Shared;
using System;

namespace SWB.Base;

public class ViewModelHandler : Component
{
	public ModelRenderer ViewModelRenderer { get; set; }
	public SkinnedModelRenderer ViewModelHandsRenderer { get; set; }
	public Weapon Weapon { get; set; }
	public CameraComponent Camera { get; set; }
	public bool ShouldDraw { get; set; }

	// Editor
	public bool EditorMode { get; set; }
	public AngPos EditorOffset { get; set; }
	public float EditorFOV { get; set; }

	IPlayerBase player => Weapon.Owner;

	float animSpeed = 1;
	float weaponFOVSpeed = 1;

	// Target animation values
	Vector3 targetVectorPos;
	Vector3 targetVectorRot;
	float targetWeaponFOV = -1;

	// Finalized animation values
	Vector3 finalVectorPos;
	Rotation finalRot;
	float finalWeaponFOV;

	// Sway
	Rotation lastEyeRot;

	// Jumping Animation
	float jumpTime;
	float landTime;

	// Aim animation
	float aimTime;

	// Helpful values
	Vector3 localVel;
	bool isAiming;

	protected override void OnDestroy()
	{
		if ( !player.IsFirstPerson ) return;
		player.FieldOfView = Screen.CreateVerticalFieldOfView( Preferences.FieldOfView );
	}

	protected override void OnDisabled()
	{
		// Reinitialize all target values when enabled
		targetWeaponFOV = -1;
	}

	protected override void OnUpdate()
	{
		var renderType = ShouldDraw ? ModelRenderer.ShadowRenderType.Off : ModelRenderer.ShadowRenderType.ShadowsOnly;
		ViewModelRenderer.Enabled = player.IsFirstPerson;
		ViewModelRenderer.RenderType = renderType;

		if ( ViewModelHandsRenderer is not null )
		{
			ViewModelHandsRenderer.Enabled = player.IsFirstPerson;
			ViewModelHandsRenderer.RenderType = renderType;
		}

		if ( !player.IsFirstPerson ) return;

		// For particles & lighting
		Camera.WorldPosition = Scene.Camera.WorldPosition;
		Camera.WorldRotation = Scene.Camera.WorldRotation;

		if ( targetWeaponFOV == -1 )
		{
			targetWeaponFOV = Weapon.ViewModelFOV;
			finalWeaponFOV = Weapon.ViewModelFOV;
		}

		// Skinned modelrenderer has issues with following parent
		WorldPosition = Camera.WorldPosition;
		WorldRotation = Camera.WorldRotation;

		// Pos + FOV
		finalVectorPos = finalVectorPos.LerpTo( targetVectorPos, animSpeed * RealTime.SmoothDelta );
		finalWeaponFOV = MathX.LerpTo( finalWeaponFOV, targetWeaponFOV, weaponFOVSpeed * animSpeed * RealTime.SmoothDelta );

		// Angles
		var targetRot = MathUtil.ToRotation( targetVectorRot );
		finalRot = finalRot.SlerpTo( targetRot, animSpeed * RealTime.SmoothDelta );

		// Reset Speed
		animSpeed = 10 * Weapon.AnimSpeed;

		// Change the angles and positions of the viewmodel with the new vectors
		WorldRotation *= finalRot;
		// Position has to be set after rotation!
		WorldPosition += finalVectorPos.z * WorldRotation.Up + finalVectorPos.y * WorldRotation.Forward + finalVectorPos.x * WorldRotation.Right;
		Camera.FieldOfView = Screen.CreateVerticalFieldOfView( finalWeaponFOV );

		// Initialize the target vectors for this frame
		targetVectorPos = Vector3.Zero;
		targetVectorRot = Vector3.Zero;
		targetWeaponFOV = Weapon.ViewModelFOV;

		// Editor mode
		if ( EditorMode )
		{
			targetVectorRot += MathUtil.ToVector3( EditorOffset.Angle );
			targetVectorPos += EditorOffset.Pos;
			targetWeaponFOV = EditorFOV;
			return;
		}

		// I'm sure there's something already that does this for me, but I spend an hour
		// searching through the wiki and a bunch of other garbage and couldn't find anything...
		// So I'm doing it manually. Problem solved.
		var eyeRot = player.EyeAngles.ToRotation();
		localVel = new Vector3( eyeRot.Right.Dot( player.Velocity ), eyeRot.Forward.Dot( player.Velocity ), player.Velocity.z );

		HandleIdleAnimation();
		HandleWalkAnimation();
		HandleJumpAnimation();

		// Tucking
		isAiming = !Weapon.ShouldTuckVar && Weapon.IsAiming;
		if ( Weapon.RunAnimData != AngPos.Zero && Weapon.ShouldTuckVar )
		{
			var animationCompletion = 1f;

			if ( !player.IsClimbingLadder )
				animationCompletion = Math.Min( 1, ((Weapon.TuckRange - Weapon.TuckDist) / Weapon.TuckRange) + 0.5f );

			targetVectorPos += Weapon.RunAnimData.Pos * animationCompletion;
			targetVectorRot += MathUtil.ToVector3( Weapon.RunAnimData.Angle * animationCompletion );
			return;
		}

		HandleSwayAnimation();
		HandleIronAnimation();
		HandleSprintAnimation();
		HandleCustomizeAnimation();
	}

	protected virtual void HandleIdleAnimation()
	{
		// No swaying if aiming
		if ( isAiming )
			return;

		// Perform a "breathing" animation
		var breatheTime = RealTime.Now * 2.0f;
		targetVectorPos -= new Vector3( MathF.Cos( breatheTime / 4.0f ) / 8.0f, 0.0f, -MathF.Cos( breatheTime / 4.0f ) / 32.0f );
		targetVectorRot -= new Vector3( MathF.Cos( breatheTime / 5.0f ), MathF.Cos( breatheTime / 4.0f ), MathF.Cos( breatheTime / 7.0f ) );

		// Crouching animation
		if ( player.IsCrouching && player.IsOnGround )
			targetVectorPos += new Vector3( -1.0f, -1.0f, 0.5f );
	}

	protected virtual void HandleWalkAnimation()
	{
		var breatheTime = RealTime.Now * 16.0f;
		var walkSpeed = new Vector3( player.Velocity.x, player.Velocity.y, 0.0f ).Length;
		var maxWalkSpeed = 200.0f;
		var roll = 0.0f;
		var yaw = 0.0f;

		// Check if on the ground
		if ( !player.IsOnGround )
			return;

		// Check if sprinting
		if ( player.IsRunning )
		{
			breatheTime = RealTime.Now * 18.0f;
			maxWalkSpeed = 100.0f;
		}

		// Check for sideways velocity to sway the gun slightly
		if ( isAiming || localVel.x > 0.0f )
			roll = -7.0f * (localVel.x / maxWalkSpeed);
		else if ( localVel.x < 0.0f )
			yaw = 3.0f * (localVel.x / maxWalkSpeed);

		// Check if ADS & firing
		if ( isAiming && Weapon.TimeSincePrimaryShoot < 0.1f )
		{
			targetVectorRot -= new Vector3( 0, 0, roll );
			return;
		}

		// Perform walk cycle
		targetVectorPos -= new Vector3( (-MathF.Cos( breatheTime / 2.0f ) / 5.0f) * walkSpeed / maxWalkSpeed - yaw / 4.0f, 0.0f, 0.0f );
		targetVectorRot -= new Vector3( (Math.Clamp( MathF.Cos( breatheTime ), -0.3f, 0.3f ) * 2.0f) * walkSpeed / maxWalkSpeed, (-MathF.Cos( breatheTime / 2.0f ) * 1.2f) * walkSpeed / maxWalkSpeed - yaw * 1.5f, roll );
	}


	protected virtual void HandleSwayAnimation()
	{
		var swayspeed = 5;

		// Fix the sway faster if we're ironsighting
		if ( isAiming )
			swayspeed = 20;

		// Lerp the eye position
		lastEyeRot = Rotation.Lerp( lastEyeRot, player.Camera.WorldRotation, swayspeed * RealTime.SmoothDelta );

		// Calculate the difference between our current eye angles and old (lerped) eye angles
		var angDif = player.Camera.WorldRotation.Angles() - lastEyeRot.Angles();
		angDif = new Angles( angDif.pitch, MathX.RadianToDegree( MathF.Atan2( MathF.Sin( MathX.DegreeToRadian( angDif.yaw ) ), MathF.Cos( MathX.DegreeToRadian( angDif.yaw ) ) ) ), 0 );

		// Perform sway
		targetVectorPos += new Vector3( Math.Clamp( angDif.yaw * 0.04f, -1.5f, 1.5f ), 0.0f, Math.Clamp( angDif.pitch * 0.04f, -1.5f, 1.5f ) );
		targetVectorRot += new Vector3( Math.Clamp( angDif.pitch * 0.2f, -4.0f, 4.0f ), Math.Clamp( angDif.yaw * 0.2f, -4.0f, 4.0f ), 0.0f );
	}


	protected virtual void HandleIronAnimation()
	{
		if ( isAiming && !Weapon.IsReloading && Weapon.AimAnimData != AngPos.Zero )
		{
			var speedMod = 1f;
			if ( aimTime == 0 )
			{
				aimTime = RealTime.Now;
			}

			var timeDiff = RealTime.Now - aimTime;

			// Mod only while actively scoping
			if ( Weapon.IsScoping || (!Weapon.IsScoping && timeDiff < 0.2f) )
			{
				speedMod = timeDiff * 10;
			}

			animSpeed = 10 * Weapon.AnimSpeed * speedMod;
			targetVectorPos += Weapon.AimAnimData.Pos;
			targetVectorRot += MathUtil.ToVector3( Weapon.AimAnimData.Angle );

			if ( Weapon.AimInfo.ViewModelFOV > 0 )
				targetWeaponFOV = Weapon.AimInfo.ViewModelFOV;

			weaponFOVSpeed = Weapon.AimInfo.AimInFOVSpeed;
		}
		else
		{
			aimTime = 0;
			targetWeaponFOV = Weapon.ViewModelFOV;
		}
	}

	protected virtual void HandleSprintAnimation()
	{
		if ( Weapon.IsRunning && Weapon.RunAnimData != AngPos.Zero && !Weapon.IsCustomizing )
		{
			targetVectorPos += Weapon.RunAnimData.Pos;
			targetVectorRot += MathUtil.ToVector3( Weapon.RunAnimData.Angle );
		}
	}


	protected virtual void HandleCustomizeAnimation()
	{
		if ( Weapon.IsCustomizing && Weapon.CustomizeAnimData != AngPos.Zero )
		{
			targetVectorPos += Weapon.CustomizeAnimData.Pos;
			targetVectorRot += MathUtil.ToVector3( Weapon.CustomizeAnimData.Angle );
		}
	}

	protected virtual void HandleJumpAnimation()
	{
		// If we're not on the ground, reset the landing animation time
		if ( !player.IsOnGround )
			landTime = RealTime.Now + 0.31f;

		// Reset the timers once they elapse
		if ( landTime < RealTime.Now && landTime != 0.0f )
		{
			landTime = 0.0f;
			jumpTime = 0.0f;
		}

		// If we jumped, start the animation
		if ( Input.Down( InputButtonHelper.Jump ) && jumpTime == 0.0f )
		{
			jumpTime = RealTime.Now + 0.31f;
			landTime = 0.0f;
		}

		// If we're not ironsighting, do a fancy jump animation
		if ( !isAiming )
		{
			if ( jumpTime > RealTime.Now )
			{
				// If we jumped, do a curve upwards
				var f = 0.31f - (jumpTime - RealTime.Now);
				var xx = MathUtil.BezierY( f, 0.0f, -4.0f, 0.0f );
				var yy = 0.0f;
				var zz = MathUtil.BezierY( f, 0.0f, -2.0f, -5.0f );
				var pt = MathUtil.BezierY( f, 0.0f, -4.36f, 10.0f );
				var yw = xx;
				var rl = MathUtil.BezierY( f, 0.0f, -10.82f, -5.0f );
				targetVectorPos += new Vector3( xx, yy, zz ) / 4.0f;
				targetVectorRot += new Vector3( pt, yw, rl ) / 4.0f;
				animSpeed = 20.0f;
			}
			else if ( !player.IsOnGround )
			{
				// Shaking while falling
				var breatheTime = RealTime.Now * 30.0f;
				targetVectorPos += new Vector3( MathF.Cos( breatheTime / 2.0f ) / 16.0f, 0.0f, -5.0f + (MathF.Sin( breatheTime / 3.0f ) / 16.0f) ) / 4.0f;
				targetVectorRot += new Vector3( 10.0f - (MathF.Sin( breatheTime / 3.0f ) / 4.0f), MathF.Cos( breatheTime / 2.0f ) / 4.0f, -5.0f ) / 4.0f;
				animSpeed = 20.0f;
			}
			else if ( landTime > RealTime.Now )
			{
				// If we landed, do a fancy curve downwards
				var f = landTime - RealTime.Now;
				var xx = MathUtil.BezierY( f, 0.0f, -4.0f, 0.0f );
				var yy = 0.0f;
				var zz = MathUtil.BezierY( f, 0.0f, -2.0f, -5.0f );
				var pt = MathUtil.BezierY( f, 0.0f, -4.36f, 10.0f );
				var yw = xx;
				var rl = MathUtil.BezierY( f, 0.0f, -10.82f, -5.0f );
				targetVectorPos += new Vector3( xx, yy, zz ) / 2.0f;
				targetVectorRot += new Vector3( pt, yw, rl ) / 2.0f;
				animSpeed = 20.0f;
			}
		}
		else
			targetVectorPos += new Vector3( 0.0f, 0.0f, Math.Clamp( localVel.z / 1000.0f, -1.0f, 1.0f ) );
	}
}