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 playerFOVSpeed = 1;

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

	// Finalized animation values
	Vector3 finalVectorPos;
	Vector3 finalVectorRot;
	float finalPlayerFOV;
	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()
	{
		player?.Camera?.FieldOfView = Screen.CreateVerticalFieldOfView( Preferences.FieldOfView );
	}

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

    protected override void OnUpdate()
    {
        // 🔒 ViewModel vain local first-person -tilassa
        if (Scene?.Camera == null || !Scene.Camera.IsMainCamera || player == null || !player.IsFirstPerson)
        {
            if (ViewModelRenderer != null)
                ViewModelRenderer.Enabled = false;

            if (ViewModelHandsRenderer != null)
                ViewModelHandsRenderer.Enabled = false;

            return;
        }

        if (!Enabled || Camera == null || !Camera.Enabled)
            return;

        // ─────────────────────────────────────────
        // ⏱️ Turvallinen dt
        var dt = RealTime.Delta;
        if (dt <= 0f || float.IsNaN(dt) || float.IsInfinity(dt))
            return;

        dt = Math.Min(dt, 0.05f);

        // ⚙️ AnimSpeed ENNEN käyttöä
        animSpeed = 10f * Weapon.AnimSpeed;
        if (float.IsNaN(animSpeed) || float.IsInfinity(animSpeed) || animSpeed <= 0f)
            animSpeed = 10f;

        // ─────────────────────────────────────────
        var renderType = ShouldDraw
            ? ModelRenderer.ShadowRenderType.Off
            : ModelRenderer.ShadowRenderType.ShadowsOnly;

        ViewModelRenderer.Enabled = true;
        ViewModelRenderer.RenderType = renderType;

        if (ViewModelHandsRenderer != null)
        {
            ViewModelHandsRenderer.Enabled = true;
            ViewModelHandsRenderer.RenderType = renderType;
        }

        // Kamera sync
        Camera.WorldPosition = Scene.Camera.WorldPosition;
        Camera.WorldRotation = Scene.Camera.WorldRotation;

        if (targetWeaponFOV == -1)
        {
            targetPlayerFOV = Preferences.FieldOfView;
            finalPlayerFOV = Preferences.FieldOfView;
            targetWeaponFOV = Weapon.FOV;
            finalWeaponFOV = Weapon.FOV;
        }

        WorldPosition = Camera.WorldPosition;
        WorldRotation = Camera.WorldRotation;

        // ─────────────────────────────────────────
        // Smoothit
        finalVectorPos = finalVectorPos.LerpTo(targetVectorPos, animSpeed * dt);
        finalVectorRot = finalVectorRot.LerpTo(targetVectorRot, animSpeed * dt);
        finalPlayerFOV = MathX.LerpTo(finalPlayerFOV, targetPlayerFOV, playerFOVSpeed * animSpeed * dt);
        finalWeaponFOV = MathX.LerpTo(finalWeaponFOV, targetWeaponFOV, playerFOVSpeed * animSpeed * dt);

        // 🔒 Viimeinen NaN-turva
        if (!IsFinite(finalVectorPos) || !IsFinite(finalVectorRot))
        {
            finalVectorPos = Vector3.Zero;
            finalVectorRot = Vector3.Zero;
        }

        // ─────────────────────────────────────────

        // Rotaatiot & positio
        WorldRotation *= Rotation.From(
            finalVectorRot.x,
            finalVectorRot.y,
            finalVectorRot.z
        );

        if (
    !IsFinite(finalVectorPos) ||
    float.IsNaN(WorldRotation.x) || float.IsInfinity(WorldRotation.x) ||
    float.IsNaN(WorldRotation.y) || float.IsInfinity(WorldRotation.y) ||
    float.IsNaN(WorldRotation.z) || float.IsInfinity(WorldRotation.z) ||
    float.IsNaN(WorldRotation.w) || float.IsInfinity(WorldRotation.w)
)
        {
            finalVectorPos = Vector3.Zero;
        }

        WorldPosition +=
            finalVectorPos.z * WorldRotation.Up +
            finalVectorPos.y * WorldRotation.Forward +
            finalVectorPos.x * WorldRotation.Right;

        Camera.FieldOfView = Screen.CreateVerticalFieldOfView(finalWeaponFOV);
        player.Camera.FieldOfView = Screen.CreateVerticalFieldOfView(finalPlayerFOV);

        // ─────────────────────────────────────────
        // Resetoi targetit
        targetVectorPos = Vector3.Zero;
        targetVectorRot = Vector3.Zero;
        targetPlayerFOV = 70f;
        targetWeaponFOV = Weapon.FOV;

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

        // ─────────────────────────────────────────
        // Liikeanimaatiot
        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
        var shouldTuck = Weapon.ShouldTuck(out var tuckDist);
        isAiming = !shouldTuck && Weapon.IsAiming;

        if (Weapon.RunAnimData != AngPos.Zero && shouldTuck)
        {
            var completion = Math.Min(
                1f,
                ((Weapon.TuckRange - tuckDist) / Weapon.TuckRange) + 0.5f
            );

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

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

    static bool IsFinite(Vector3 v)
    {
        return
            !float.IsNaN(v.x) && !float.IsInfinity(v.x) &&
            !float.IsNaN(v.y) && !float.IsInfinity(v.y) &&
            !float.IsNaN(v.z) && !float.IsInfinity(v.z);
    }


    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 ( Input.Down( InputButtonHelper.Duck ) && player.IsOnGround )
			targetVectorPos += new Vector3( -1.0f, -1.0f, 0.5f );
	}


	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 );
	}


	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.Delta );

		// 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 );
	}


	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.AimPlayerFOV > 0 )
				targetPlayerFOV = Weapon.AimPlayerFOV;

			if ( Weapon.IsScoping && Weapon.ScopeInfo.FOV > 0 )
				targetPlayerFOV = Weapon.ScopeInfo.FOV;

			if ( Weapon.AimFOV > 0 )
				targetWeaponFOV = Weapon.AimFOV;

			playerFOVSpeed = Weapon.AimInFOVSpeed;
		}
		else
		{
			aimTime = 0;
			targetWeaponFOV = Weapon.FOV;

			if ( finalPlayerFOV != Weapon.AimPlayerFOV )
			{
				playerFOVSpeed = Weapon.AimOutFOVSpeed;
			}
		}
	}

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


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

	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 ) );
	}

}