NPCs/Agent.cs
namespace Opium.AI;

public abstract partial class Agent : Actor, Actor.IReceptor
{
	/// <summary>
	/// An action called when the NPC is damaged by something.
	/// </summary>
	[Property, Category( "Actions" )] public Action< Sandbox.DamageInfo> OnDamageAction { get; set; }

	/// <summary>
	/// An action called when the NPC is killed by something.
	/// </summary>
	[Property, Category( "Actions" )] public Action<Sandbox.DamageInfo> OnKilledAction { get; set; }

	/// <summary>
	/// The state machine this NPC is using.
	/// </summary>
	[Property] public StateMachine StateMachine { get; set; }

	/// <summary>
	/// Ragdoll
	/// </summary>
	[Property] public ModelPhysics Physics { get; set; }
	[Property] public ModelCollider Collider { get; set; }

	/// <summary>
	/// Weapon
	/// </summary>
	private BaseWeapon weapon;
	[Property] public BaseWeapon Weapon
	{
		get => weapon;
		set
		{
			weapon = value;

			if ( weapon.IsValid() )
			{
				weapon.Actor = this;
			}
		}
	}

	public override BaseWeapon ActiveWeapon => Weapon;

	/// <summary>
	/// Model
	/// </summary>
	[Property] public SkinnedModelRenderer Model { get; set; }

	[Property] public GameObject CameraGameObject { get; set; }
	public override GameObject CameraObject => CameraGameObject;

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

	public override void OnDamage( in Sandbox.DamageInfo damage )
	{
		base.OnDamage( damage );

		if ( OnDamageAction != null ) OnDamageAction?.Invoke( damage );

		if ( Model is not null )
		{
			var health = Health.Remap( 0, 100, 0.6f, 0f );
			Model?.SceneObject.Attributes.Set( "bloodamount", health );
		}

		Scene.BroadcastStimulus( new FriendGotHurtStimulus( Transform.Position ) );
	}

	async void RagdollAsync()
	{

		Tags.Add( "ragdoll" );
		await GameTask.DelaySeconds( 0.2f );

		Physics.GameObject.SetParent( null );
		Physics.GameObject.BreakFromPrefab();
		Physics.GameObject.Tags.Add( "ragdoll" );

		Physics.GameObject.DestroyAsync( 10f );
		
		// Destroy the agent immediately
		GameObject.Destroy();

		// Creates a nav blocker for this ragdoll
		var blocker = TemporaryNavBlocker.Create( Physics.GameObject.Transform.Position, new( 32, 32, 32 ), 10f );

		Physics.Enabled = true;
	}

	public override void OnKilled( in Sandbox.DamageInfo damage )
	{
		base.OnKilled( damage );

		if ( OnKilledAction != null ) OnKilledAction?.Invoke( damage );

		var pickup = DropWeapon( Weapon );
		if ( pickup is not null )
		{
			pickup.Durability = Game.Random.Int( 25, 75 );
		}

		if ( Collider is not null )
		{
			Collider.Enabled = false;
		}

		if ( Physics is not null )
		{
			RagdollAsync();
		}
		else
		{
			GameObject.Destroy();
		}
	}

	public override WeaponPickup DropWeapon( BaseWeapon weapon )
	{
		return weapon?.Drop( CameraObject.Transform.Position, CameraObject.Transform.Position + CameraObject.Transform.Rotation.Forward * 5, CameraObject.Transform.Rotation.Forward );
	}

	public override void OnEvent( string eventName, params object[] obj )
	{
		if ( eventName == "damage" )
		{
			var damageInfo = (DamageInfo)obj[0];
			LastStimulus = new HurtStimulus( damageInfo.Position );
		}

		StateMachine?.OnEvent( eventName, obj );
	}

	void UpdateStateMachine()
	{
		StateMachine?.UpdateStateMachine();

		if ( StateMachine?.CurrentState.GetWishSpeed() is { } wishSpeed )
		{
			WishMove *= wishSpeed;
		}
	}

	TimeSince TimeSinceFootstepEvent = 0;
	private const float FootstepEventDelay = 0.3f;

	protected override void UpdateMovement()
	{
		BuildWishInput();
		DoMechanicsUpdate();
		UpdateStateMachine();
		BuildWishVelocity();
		Accelerate();

		if ( CharacterController.Velocity.Length > 0f && TimeSinceFootstepEvent > FootstepEventDelay )
		{
			Scene.BroadcastStimulus( FootstepStimulus.From( this, WishMove.Length ) );
			TimeSinceFootstepEvent = 0;
		}
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();

		UpdateWalk();
		UpdateMovement();
		FindStimuli();
		
		// Kill off stimuli if it's based on old information
		if ( LastStimulus is not null && !LastStimulus.ShouldReact( this ) )
		{		
			LastStimulus = null;
		}
	}

	/// <summary>
	/// The last stimulus info this actor took.
	/// </summary>
	public Stimulus LastStimulus { get; set; }

	public void OnStimulusReceived( Stimulus stimulusInfo )
	{
		if ( stimulusInfo.HasExpired )
			return;


		if ( stimulusInfo.ShouldReact( this ) )
		{
			LastStimulus = stimulusInfo;
		}
	}

	public virtual bool Hates( Actor other )
	{
		return true;
	}
}