AI/Animals/Actions/RoamEscapedAction.cs
namespace HC3;

/// <summary>
/// When an escaped animal is not hungry, it roams around running away from people.
/// Scores below HuntAction (999) but above normal wander (1).
/// </summary>
public sealed class RoamEscapedAction : BehaviorTreeAction
{
	[Property] public float FleeRadius { get; set; } = 400f;
	[Property] public float HungerThreshold { get; set; } = 0.15f;

	private Vector3 _fleeTarget;

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

		if ( !Agent.Tags.Has( "escaped" ) )
			return 0f;

		// Only roam if not hungry enough to hunt
		var needSystem = Agent.GetComponent<NeedSystem>();
		if ( needSystem != null )
		{
			var hunger = needSystem.GetNeed( "Hunger" );
			if ( hunger.IsValid() && hunger.Level >= HungerThreshold )
				return 0f;
		}

		return 800f;
	}

	protected override void OnTreeStart()
	{
		Agent.Controller.IsRunning = true;
	}

	protected override void OnTreeStop()
	{
		Agent.Controller.IsRunning = false;
	}

	protected override Node BuildTree()
	{
		var nearestPerson = FindClosestPerson();

		if ( nearestPerson.IsValid() )
		{
			// Flee away from the nearest person
			if ( FleeAction.TryFindFleeTarget( Agent, nearestPerson, out _fleeTarget ) )
			{
				return new MoveToNode( Agent, _fleeTarget, "Running away" );
			}
		}

		// No one nearby — just roam to a random far point
		if ( TryFindRoamPoint( out var roamTarget ) )
		{
			return new MoveToNode( Agent, roamTarget, "Roaming" );
		}

		return null;
	}

	private Agent FindClosestPerson()
	{
		Agent closest = null;
		float closestDist = FleeRadius * FleeRadius;

		foreach ( var x in Scene.GetAll<Agent>() )
		{
			if ( x is Animal ) continue;
			if ( x == Agent || !x.IsValid() ) continue;
			if ( x.Building.IsValid() ) continue;
			if ( !x.IsAlive ) continue;

			float dist = x.WorldPosition.DistanceSquared( Agent.WorldPosition );
			if ( dist < closestDist )
			{
				closestDist = dist;
				closest = x;
			}
		}

		return closest;
	}

	private bool TryFindRoamPoint( out Vector3 location )
	{
		location = Vector3.Zero;

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

		var walkableCells = GridNavigation.Instance.GetNavablePaths( regionId, NavFlags.Default );
		if ( walkableCells == null )
			return false;

		// Pick a random far point
		Vector3Int? best = null;
		float bestDist = -1f;
		int sampled = 0;

		foreach ( var pos in walkableCells )
		{
			sampled++;
			if ( sampled > 30 )
				break;

			var world = GridManager.GridToWorldPosition( pos );
			float dist = world.DistanceSquared( Agent.WorldPosition );
			if ( dist > bestDist )
			{
				bestDist = dist;
				best = pos;
			}
		}

		if ( best == null )
			return false;

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

	public override ActionDisplayInfo? GetDisplay()
	{
		return new ActionDisplayInfo( "directions_run", "Roaming free", Agent.Controller.GetPathProgress() );
	}
}