Weapons/Base/ViewModel.cs
using Opium;
using Sandbox.Utility;

public sealed class ViewModel : Component, Actor.IEventListener
{
	BaseWeapon weapon;
	[Property] public BaseWeapon Weapon
	{
		get => weapon; 
		set
		{
			if ( weapon == value ) return;
			weapon = value;

			if ( weapon is not null )
			{
				weapon.EventListener += OnWeaponEvent;
			}
		}
	}
	[Property] public SkinnedModelRenderer ModelRenderer { get; set; }
	[Property] public CodeAnimations CodeAnimationFlags { get; set; }

	[Property] public List<GameObject> AmmoStack { get; set; }

	[Property, Group( "Avoidance" )] public float WeaponLength { get; set; } = 50f;
	[Property, Group( "Avoidance" )] public Vector3 WeaponAvoidanceOffset { get; set; } = new( 0.1f, 0.05f, 0.05f );
	[Property, Group( "Avoidance" )] public Angles WeaponAvoidanceAngles { get; set; } = new( 50f, 0, 0 );

	public int CurrentAmmo = 999;

	void TryUpdateAmmoStack()
	{
		if ( Weapon is RangedWeapon range )
		{
			if ( range.CurrentAmmo != CurrentAmmo )
			{
				CurrentAmmo = range.CurrentAmmo;
				UpdateAmmoStack();
			}
			CurrentAmmo = range.CurrentAmmo;
		}
	}

	void UpdateAmmoStack()
	{
		if ( AmmoStack is null ) return;

		for ( int i = 0; i < AmmoStack.Count; i++ )
		{
			var obj = AmmoStack[i];

			if ( CurrentAmmo >= i + 1 )
			{
				obj.Enabled = true;
			}
			else
			{
				obj.Enabled = false;
			}
		}
	}

	[Flags]
	public enum CodeAnimations
	{
		None = 1 << 0,
		Swing = 1 << 1,
		Blocking = 1 << 2,
		EquipState = 1 << 3
	};

	protected override void OnDisabled()
	{
		if ( Weapon is not null )
		{
			Weapon.EventListener -= OnWeaponEvent;
		}
	}

	protected override void OnEnabled()
	{
		DoCodeAnimations();
	}

	void OnWeaponEvent( string eventName )
	{
		if ( eventName == "shoot" )
		{
			DoShootEffects();
		}

		if ( eventName == "attack" )
		{
			ModelRenderer.Set( "b_attack", true );
		}

		if ( eventName == "use" )
		{
			ModelRenderer.Set( "b_grab", true );
		}

		if ( eventName == "ammocheck" )
		{
			ModelRenderer.Set( "b_ammocheck", true );
		}
	}

	void DoShootEffects()
	{
		if ( Weapon is RangedWeapon rangedWeapon && rangedWeapon.MuzzleFlashPrefab is not null )
		{
			var muzzleGameObject = GameObject.GetAllObjects( true ).FirstOrDefault( x => x.Tags.Has( "muzzle" ) );
			if ( muzzleGameObject is not null )
			{
				var instance = rangedWeapon.MuzzleFlashPrefab.Clone();
				instance.Enabled = true;
				instance.SetParent( muzzleGameObject, false );
				instance.Transform.Position = muzzleGameObject.Transform.Position;
				instance.Transform.Rotation = muzzleGameObject.Transform.Rotation;
			}
			var ejectGameObject = GameObject.GetAllObjects( true ).FirstOrDefault( x => x.Tags.Has( "eject" ) );
			if( ejectGameObject is not null )
			{
				var instance = rangedWeapon.EjectPrefab.Clone();
				instance.Enabled = true;
				instance.Transform.Position = ejectGameObject.Transform.Position;
				instance.Transform.Rotation = ejectGameObject.Transform.Rotation;
				var rb = instance.Components.Get<Rigidbody>();
				rb.Velocity = ejectGameObject.Transform.Rotation.Forward * 100f;
			}
		}
	}

	/// <summary>
	/// Accessor for the player controller.
	/// This cast is fine, since viewmodels are only made on players.
	/// </summary>
	public Opium.PlayerController PlayerController => Weapon?.Actor as Opium.PlayerController;

	void SetMeleeWeaponAnimState( string stateName )
	{
		/// this sucks
		var stateToId = stateName switch
		{
			"windup" => 1,
			"attack" => 2,
			"hit" => 3,
			"block" => 4,
			"blockhit" => 5,
			_ => 0
		};

		ModelRenderer.Set( "state", stateToId );
	}

	void SetVariationState( string stateName )
	{
		/// this sucks
		var stateToId = stateName switch
		{
			"left_handed" => 1,
			_ => 0
		};

		ModelRenderer.Set( "variation_state", stateToId );
	}

	void SetRangedWeaponAnimState( string stateName )
	{
		/// this sucks
		var stateToId = stateName switch
		{
			"attack" => 1,
			"reload" => 2,
			"hit" => 3,
			_ => 0
		};

		ModelRenderer.Set( "state", stateToId );
	}

	void ApplyAnimGraphVariables()
	{
		if ( Weapon is MeleeWeapon melee && melee.IsValid() )
		{
			var attack = melee.CurrentAttack;

			if ( attack.IsValid() && !string.IsNullOrEmpty( attack.CurrentStateInfo.AnimationState ) )
			{
				ModelRenderer.Set( "b_hover", false );
				SetMeleeWeaponAnimState( attack.CurrentStateInfo.AnimationState );
				SetVariationState( attack.CurrentStateInfo.VariationState );
			}
			else
			{
				// Bit shitty, but whatever.
				var interactUse = PlayerController.Components.Get<InteractUse>();
				if ( interactUse is not null && interactUse.InteractingObject is not null )
				{
					ModelRenderer.Set( "b_hover", true );
					return;
				}
				else
				{
					ModelRenderer.Set( "b_hover", false );
				}

				SetMeleeWeaponAnimState( "default" );
			}
		}
		else if ( Weapon is RangedWeapon range && range.IsValid() )
		{
			// Bit dumb
			var isFiring = Weapon.TimeSinceShoot <= 0.5f;
			var interactUse = PlayerController.Components.Get<InteractUse>();
			var isInteracting = interactUse is not null && interactUse.LookingAtObject is not null;
			ModelRenderer.Set( "b_hover", isInteracting && !isFiring && !range.IsAiming );

			SetRangedWeaponAnimState( isFiring ? "attack" : "idle" );
		}
	}

	TimeSince timeSinceLastAction = 5;
	bool shouldDoLowering = false;

	Vector3 lowerOffset = Vector3.Zero;
	Rotation lowerOffsetRot = Rotation.Identity;

	public void DoCodeAnimations()
	{
		if ( !PlayerController.IsValid() )
		{
			Log.Info( $"No player controller. {Weapon}" );
			return;
		}

		// This can happen somehow?
		if ( !weapon.IsValid() )
			return;

		var inventory = PlayerController.Inventory;

		var position = Vector3.Zero;
		position += Transform.Rotation.Forward * 10f;
		position += Transform.Rotation.Down * 20f;
		position += Transform.Rotation.Right * 10f;

		var rotation = Rotation.Identity;
		rotation *= Rotation.FromPitch( 25 );
		rotation *= Rotation.FromYaw( 45 );


		// Wow this is fucking horrible
		shouldDoLowering = weapon.GameObject.Name == "fists";

		if ( inventory.EquipState != PlayerInventory.EquippedState.Ready )
		{
			var timeSince = inventory.EquipState == PlayerInventory.EquippedState.Deploying ? inventory.TimeSinceDeployed : inventory.TimeSinceHolstered;
			var delta = timeSince / inventory.EquipTime;
			bool reverse = inventory.EquipState == PlayerInventory.EquippedState.Deploying ? true : false;

			delta = reverse ? 1 - delta : delta;
			delta = Easing.EaseIn( delta );

			Transform.LocalPosition = position * delta;
			Transform.LocalRotation = rotation * delta;
		}
		else
		{
			Transform.LocalPosition = Vector3.Zero;
			Transform.LocalRotation = Rotation.Identity;
		}

		Vector3 posOffset = Vector3.Down * 25f;
		var interactUse = PlayerController.Components.Get<PlayerInteractor>();

		var shouldLower = shouldDoLowering && timeSinceLastAction > 5f || !PlayerController.IsAlive || interactUse.currentlyCarriedObject.IsValid();

		lowerOffsetRot = Rotation.Lerp( lowerOffsetRot, shouldLower ? Rotation.FromPitch( -25 ) : Rotation.Identity, Time.Delta * 3f );
		lowerOffset = lowerOffset.LerpTo( shouldLower ? posOffset : 0, Time.Delta * 4f );
		Transform.LocalPosition += lowerOffset;
		Transform.LocalRotation *= lowerOffsetRot;
	}

	protected override void OnUpdate()
	{
		ApplyAnimGraphVariables();
		TryUpdateAmmoStack();
		DoCodeAnimations();
	}

	public void OnEvent( Actor actor, string eventName, params object[] obj )
	{
		if ( actor.ActiveWeapon == weapon && ( eventName == "shoot" || eventName == "aim" ) )
		{
			timeSinceLastAction = 0;
		}
	}
}