NPCs/Agent.Targeting.cs
using System.Numerics;

namespace Opium.AI;

// Should probably all be in HostileNPC
partial class Agent
{
	/// <summary>
	/// Does this agent have line of sight to a specified GameObject?
	/// </summary>
	private bool HasLineOfSight( GameObject gameObject )
	{
		// TODO(alex): line of sight cone here

		if ( gameObject == null )
			return false;

		var trace = Scene.Trace
			.Ray( CameraObject.WorldPosition, gameObject.WorldPosition )
			.IgnoreGameObjectHierarchy( GameObject )
			.WithoutTags( "pickup" )
			.Run();

		if ( !trace.Hit )
			return false;

		return trace.GameObject == gameObject || gameObject.IsDescendant( trace.GameObject );
	}

	/// <summary>
	/// Find ALL players in the scene
	/// </summary>
	public IEnumerable<Opium.PlayerController> GetAllPlayers()
	{
		return Scene.GetAllObjects( true )
			.Where( x => x.Components.Get<Opium.PlayerController>( FindMode.EverythingInSelfAndAncestors ) != null )
			.Select( x => x.Components.Get<Opium.PlayerController>( FindMode.EverythingInSelfAndAncestors ) );
	}

	private IEnumerable<Actor> CachedLOS { get; set; } = new List<Actor>();
	private TimeSince TimeSinceLOSAcquired = 1f;
	private float LOSFrequency => 1f;

	[ConVar( "op_dev_ai_los" )]
	public static bool LOSDebug { get; set; } = false;

	private IEnumerable<Actor> FindActorsInLineOfSight( float range = -1 )
	{
		var forward = CameraObject.WorldRotation.Forward;
		var losSphere = new Sphere( CameraObject.WorldPosition + forward * ( range * 2f ), range * 2f );

		if ( LOSDebug )
		{
			Gizmo.Transform = global::Transform.Zero;
			Gizmo.Draw.Color = Color.White.WithAlpha( 0.25f );
			Gizmo.Draw.LineSphere( losSphere, 32 );
			Gizmo.Draw.Line( CameraObject.WorldPosition, CameraObject.WorldPosition + forward * (range / 2f) );
		}

		if ( TimeSinceLOSAcquired < LOSFrequency )
		{
			return CachedLOS;
		}

		TimeSinceLOSAcquired = 0;

		if ( range < 0 )
		{
			range = DefaultDetectionRange;
		}

		CachedLOS = Scene.FindInPhysics( losSphere )
			.Select( x => x.Components.Get<Actor>( FindMode.EverythingInSelfAndAncestors ) )
			.Where( x => HasLineOfSight( x?.GameObject ) )
			.Where( x => x.IsAlive );

		return CachedLOS;
	}

	public IEnumerable<Actor> FindEnemiesInLineOfSight( float range = -1 )
	{
		if ( range < 0 ) range = DefaultDetectionRange;

		return FindActorsInLineOfSight( range ).Where( Hates );
	}

	public Actor FindClosestEnemyInLineOfSight( float range = -1 )
	{
		if ( range < 0 ) range = DefaultDetectionRange;

		return FindActorsInLineOfSight( range ).FirstOrDefault( Hates );
	}

	/// <summary>
	/// Can this agent see a player?
	/// </summary>
	public bool CanSeeAnyPlayer()
	{
		var target = FindActorsInLineOfSight();
		return (target is not null);
	}

	/// <summary>
	/// Make the agent look at someone.
	/// TODO: Make this use proper Actor movement 
	/// </summary>
	public void LookAt( Vector3 position, float smoothing = 5f )
	{
		var lookRot = Rotation.LookAt( position - CameraObject.WorldPosition );
		var lookRotAngles = new Angles( 0, lookRot.Yaw(), 0 );

		WorldRotation = Rotation.Slerp( WorldRotation, lookRotAngles.ToRotation(), smoothing * Time.Delta );
	}

	/// <summary>
	/// Make the agent look at someone.
	/// TODO: Make this use proper Actor movement 
	/// </summary>
	public void LookAt( Actor target )
	{
		var lookRot = Rotation.LookAt( target.WorldPosition - CameraObject.WorldPosition );
		var lookRotAngles = new Angles( 0, lookRot.Yaw(), 0 );
		WorldRotation = lookRotAngles.ToRotation();	
	}

	/// <summary>
	/// Gets a path (using the navmesh) between two vectors.
	/// </summary>
	public List<Vector3> GetPath( Vector3 pointA, Vector3 pointB )
	{
		var navPath = Scene.NavMesh.GetSimplePath( pointA, pointB );
		return navPath;
	}

	/// <inheritdoc cref="GetPath(Vector3, Vector3)"/>
	public List<Vector3> GetPath( Vector3 target )
	{
		return GetPath( WorldPosition, target );
	}

	TimeUntil nextStimuli = 0f;

	[Property] public float LineOfSightRange { get; set; } = -1;

	public void FindStimuli()
	{
		var enemy = FindClosestEnemyInLineOfSight( LineOfSightRange );

		// Can we actually see anyone?
		if ( enemy == null )
			return;

		LastStimulus = new EnemySpottedStimulus( enemy );

		if ( enemy.ActiveWeapon is MeleeWeapon melee && nextStimuli )
		{
			var swinging = melee.MainAttack.State == AttackState.Windup;
			var rand = Game.Random.Next( 1, 10 );

			if ( swinging && rand >= 3 && nextStimuli )
			{
				nextStimuli = 1;
				Scene.BroadcastStimulus( new AnticipateHitStimulus( enemy.WorldPosition ) );
			}
		}
	}

	public DoorComponent FindNearestDoor( float range = -1f )
	{
		if ( range <= 0 )
			range = 128f;

		var bbox = new BBox( WorldPosition - range, WorldPosition + range );

		var door = Scene.FindInPhysics( bbox )
			.Where( x => x.Components.Get<DoorComponent>( FindMode.EverythingInSelfAndAncestors ) != null )
			.Select( x => x.Components.Get<DoorComponent>( FindMode.EverythingInSelfAndAncestors ) )
			.FirstOrDefault();

		Gizmo.Draw.Color = Color.White;
		Gizmo.Draw.LineBBox( bbox );

		return door;
	}
}