Weapons/WeaponModel/ViewModel.cs
using static BaseWeapon;

public sealed partial class ViewModel : WeaponModel, IWeaponEvent, ICameraSetup, PlayerController.IEvents
{
	[ConVar( "sbdm.hideviewmodel", ConVarFlags.Cheat )]
	private static bool HideViewModel { get; set; } = false;

	/// <summary>
	/// Turns on incremental reloading parameters.
	/// </summary>
	[Property, Group( "Animation" )]
	public bool IsIncremental { get; set; } = false;

	/// <summary>
	/// Animation speed in general.
	/// </summary>
	[Property, Group( "Animation" )]
	public float AnimationSpeed { get; set; } = 1.0f;

	/// <summary>
	/// Animation speed for incremental reload sections.
	/// </summary>
	[Property, Group( "Animation" )]
	public float IncrementalAnimationSpeed { get; set; } = 3.0f;

	[Property] public bool UseFastAnimations { get; set; } = false;

	/// <summary>
	/// How much inertia should this weapon have?
	/// </summary>
	[Property, Group( "Inertia" )]
	Vector2 InertiaScale { get; set; } = new Vector2( 2, 2 );

	bool IsAttacking;
	TimeSince AttackDuration;

	Vector2 lastInertia;
	Vector2 currentInertia;
	bool isFirstUpdate = true;

	protected override void OnStart()
	{
		foreach ( var renderer in GetComponentsInChildren<ModelRenderer>() )
		{
			// Don't render shadows for viewmodels
			renderer.RenderType = ModelRenderer.ShadowRenderType.Off;
		}
	}

	protected override void OnUpdate()
	{
		UpdateAnimation();
	}

	void ApplyInertia()
	{
		// Need to fetch data from the camera for the first frame
		if ( isFirstUpdate )
		{
			var rot = Scene.Camera.WorldRotation;

			lastInertia = new Vector2( rot.Pitch(), rot.Yaw() );
			currentInertia = Vector2.Zero;
			isFirstUpdate = false;
		}

		var newPitch = Scene.Camera.WorldRotation.Pitch();
		var newYaw = Scene.Camera.WorldRotation.Yaw();

		currentInertia = new Vector2( Angles.NormalizeAngle( newPitch - lastInertia.x ), Angles.NormalizeAngle( lastInertia.y - newYaw ) );
		lastInertia = new( newPitch, newYaw );
	}

	void ICameraSetup.Setup( CameraComponent cc )
	{
		Renderer.Enabled = !HideViewModel;

		WorldPosition = cc.WorldPosition;
		WorldRotation = cc.WorldRotation;

		ApplyInertia();
		ApplyAnimationTransform( cc );
	}

	void ApplyAnimationTransform( CameraComponent cc )
	{
		if ( !Renderer.IsValid() ) return;

		var bone = Renderer.GetBoneObject( "camera" );
		if ( !bone.IsValid() )
		{
			return;
		}

		var scale = 1;

		cc.LocalRotation *= bone.LocalRotation * scale;
		cc.LocalPosition += cc.WorldRotation * bone.LocalPosition * scale;
	}

	void UpdateAnimation()
	{
		var playerController = GetComponentInParent<PlayerController>();
		if ( !playerController.IsValid() ) return;

		Renderer.Set( "b_twohanded", true );
		Renderer.Set( "b_grounded", playerController.IsOnGround );
		Renderer.Set( "move_bob", GamePreferences.ViewBobbing ? playerController.Velocity.Length.Remap( 0, playerController.RunSpeed * 2f ) : 0 );

		Renderer.Set( "aim_pitch_inertia", currentInertia.x * InertiaScale.x );
		Renderer.Set( "aim_yaw_inertia", currentInertia.y * InertiaScale.y );

		Renderer.Set( "attack_hold", IsAttacking ? AttackDuration.Relative.Clamp( 0f, 1f ) : 0f );
		Renderer.Set( "deploy_type", UseFastAnimations ? 1 : 0 );
		Renderer.Set( "reload_type", UseFastAnimations ? 1 : 0 );

		var hovered = playerController.Hovered;
		var isHovering = hovered.IsValid() && hovered.WorldPosition.DistanceSquared( WorldPosition ) < 16384;

		Renderer.Set( "b_grab", isHovering );
	}

	void IWeaponEvent.OnAttack( IWeaponEvent.AttackEvent e )
	{
		Renderer?.Set( "b_attack", true );

		DoMuzzleEffect();
		DoEjectBrass();

		if ( IsThrowable )
		{
			Renderer?.Set( "b_throw", true );

			Invoke( 0.5f, () =>
			{
				Renderer?.Set( "b_deploy_new", true );
				Renderer?.Set( "b_pull", false );
			} );
		}
	}

	void IWeaponEvent.OnAttackHit( IWeaponEvent.AttackEvent e )
	{
		Renderer?.Set( "b_attack_has_hit", true );
	}

	void IWeaponEvent.CreateRangedEffects( BaseWeapon weapon, Vector3 hitPoint, Vector3? origin )
	{
		DoTracerEffect( hitPoint, origin );
	}

	void PlayerController.IEvents.StartPressing( Component target )
	{
		var grabAction = target.GetComponent<GrabAction>();
		var actionStyle = grabAction.IsValid() ? (int)grabAction.Style : 1;
		Renderer.Set( "grab_action", actionStyle );
	}

	/// <summary>
	/// Called when starting to reload a weapon.
	/// </summary>
	void IWeaponEvent.OnReloadStart()
	{
		Renderer?.Set( "speed_reload", AnimationSpeed );
		Renderer?.Set( IsIncremental ? "b_reloading" : "b_reload", true );
	}

	/// <summary>
	/// Called when incrementally reloading a weapon.
	/// </summary>
	void IWeaponEvent.OnIncrementalReload()
	{
		Renderer?.Set( "speed_reload", IncrementalAnimationSpeed );
		Renderer?.Set( "b_reloading_shell", true );
	}

	const float IncrementalReloadDelay = 0.5f;

	void IWeaponEvent.OnReloadFinish()
	{
		if ( IsIncremental )
		{
			//
			// Stops the reload after a little delay so it's not immediately cancelling the animation.
			//
			Invoke( IncrementalReloadDelay, () =>
			{
				Renderer?.Set( "speed_reload", AnimationSpeed );
				Renderer?.Set( "b_reloading", false );
			} );
		}
		else
		{
			Renderer?.Set( "b_reload", false );
		}
	}
}