AI/ActionSystem/Actions/FleeAction.cs
namespace HC3;

using Sandbox;
using System;
using System.Linq;

public sealed class FleeAction : AgentAction
{
	private Agent _threat;
	private Vector3 _fleeTarget;
	private bool _hasFleeTarget;

	public override float Score()
	{
		if ( !Agent.IsValid() )
			return 0f;

		_threat = FindClosestThreat();
		if ( !_threat.IsValid() )
			return 0f;

		return 1000f;
	}

	public override void StartAction()
	{
		base.StartAction();

		if ( TryFindFleeTarget( Agent, _threat, out var target ) )
		{
			_fleeTarget = target;
			_hasFleeTarget = true;
			Agent.Controller.IsRunning = true;
			Agent.Controller.Navigate( _fleeTarget );
		}
		else
		{
			_hasFleeTarget = false;
		}
	}

	public override Status TickAction()
	{
		if ( !_hasFleeTarget || !_threat.IsValid() )
			return Status.Failure;

		if ( Agent.Controller.IsNavigating )
			return Status.Running;

		return Status.Success;
	}

	public override void StopAction()
	{
		base.StopAction();
		Agent.Controller.IsRunning = false;
	}

	public override ActionDisplayInfo? GetDisplay()
	{
		return new ActionDisplayInfo( "running_with_errors", "Fleeing", Agent.Controller.GetPathProgress() );
	}

	private Agent FindClosestThreat()
	{
		Agent closest = null;
		float closestDist = float.MaxValue;
		foreach ( var x in Scene.GetAll<Agent>() )
		{
			if ( x == Agent || !x.Tags.Has( "escaped" ) )
				continue;
			float dist = x.WorldPosition.DistanceSquared( Agent.WorldPosition );
			if ( dist < closestDist )
			{
				closestDist = dist;
				closest = x;
			}
		}
		return closest;
	}

	public static bool TryFindFleeTarget( Agent agent, Agent predator, out Vector3 fleeTarget, NavFlags flags = NavFlags.Default )
	{
		fleeTarget = default;

		var myPos = agent.WorldPosition;
		var predatorPos = predator.WorldPosition;
		var awayFromPredator = (myPos - predatorPos).Normal;

		var gridPos = GridManager.WorldToGridPosition3D( myPos );
		int regionId = GridManager.GetRegion( gridPos );

		var candidates = GridNavigation.Instance.GetNavablePaths( regionId, flags );
		if ( !candidates.Any() ) return false;

		// Find best flee target without LINQ allocation
		Vector3Int? best = null;
		float bestScore = float.MinValue;

		foreach ( var cell in candidates )
		{
			var world = GridManager.GridToWorldPosition( cell );
			if ( world.DistanceSquared( predatorPos ) <= myPos.DistanceSquared( predatorPos ) )
				continue;

			var toCell = (world - myPos).Normal;
			float alignment = Vector3.Dot( toCell, awayFromPredator );
			float distance = world.DistanceSquared( predatorPos );
			float score = alignment * 0.6f + MathF.Sqrt( distance ) * 0.4f;

			if ( score > bestScore )
			{
				bestScore = score;
				best = cell;
			}
		}

		if ( best is null )
			return false;

		fleeTarget = GridManager.GridToWorldPosition( best.Value ) + GridManager.CentreOffset;
		return true;
	}
}


file sealed class RunAwayNode : Node
{
	private readonly Agent Agent;
	private readonly Vector3 TargetPosition;
	private readonly float Threshold;

	public RunAwayNode( Agent agent, Vector3 target, float threshold )
	{
		Agent = agent;
		TargetPosition = target;
		Threshold = threshold;
	}

	public override Status Tick()
	{
		if ( Agent.WorldPosition.DistanceSquared( TargetPosition ) <= Threshold * Threshold )
			return Status.Success;

		Agent.Controller.Navigate( TargetPosition );
		return Status.Running;
	}

	public override ActionDisplayInfo? GetDisplay()
	{
		float dist = Agent.WorldPosition.Distance( TargetPosition );
		float progress = 1f - (dist / 1000f).Clamp( 0f, 1f );
		return new ActionDisplayInfo( "running", "Fleeing from danger...", progress );
	}
}