AI/Animals/Actions/BreakoutAction.cs
using HC3.Terrain;

namespace HC3;

/// <summary>
/// When an animal's happiness drops too low, it finds the nearest enclosure boundary fence,
/// breaks through it, and escapes. Existing post-escape behaviors (HuntAction, etc.) take over.
/// </summary>
[Icon( "fence" )]
public sealed class BreakoutAction : BehaviorTreeAction
{
	[Property] public float HappinessThreshold { get; set; } = 0.15f;

	private AreaTile _targetFenceTile;
	private TileEdge _breakDirection;

	public BreakoutAction()
	{
		CooldownTime = 30f;
	}

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

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

		if ( Agent is not Animal animal || !animal.IsInEnclosure() )
			return 0f;

		var needSystem = Agent.GetComponent<NeedSystem>();
		if ( needSystem == null || needSystem.Happiness > HappinessThreshold )
			return 0f;

		if ( !FindBoundaryTile( out _targetFenceTile, out _breakDirection ) )
			return 0f;

		return 500f;
	}

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

	protected override void OnTreeStop()
	{
		Agent.Controller.IsRunning = false;
		Agent.Tags.Set( "attack", false );
	}

	protected override Node BuildTree()
	{
		if ( !_targetFenceTile.IsValid() )
			return null;

		var fenceWorldPos = _targetFenceTile.WorldPosition;
		var breakDir = _breakDirection;
		var targetTile = _targetFenceTile;

		return new SequenceNode(
		[
			// 1. Navigate to the boundary fence tile
			new MoveToNode( Agent, fenceWorldPos, "Approaching fence" ),

			// 2. Play attack animation to break the fence
			new TimedSequenceNode(
				Agent,
				"Run_Attack",
				2.0f,
				onStart: () =>
				{
					Agent.Controller.ClearNavigation();
					Agent.Tags.Set( "attack", true );
				},
				onComplete: () =>
				{
					Agent.Tags.Set( "attack", false );
					BreakFenceAndEscape( targetTile, breakDir );
				},
				reason: "Breaking fence",
				icon: "fence"
			),
		] );
	}

	/// <summary>
	/// Finds the nearest enclosure boundary tile in the animal's enclosure region.
	/// </summary>
	private bool FindBoundaryTile( out AreaTile bestTile, out TileEdge bestDirection )
	{
		bestTile = null;
		bestDirection = TileEdge.Up;

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

		if ( regionId < 0 )
			return false;

		var nearest = AreaTile.FindNearestBoundary( regionId, Agent.WorldPosition );
		if ( nearest == null )
			return false;

		bestTile = nearest.Value.Tile;
		bestDirection = nearest.Value.OutwardEdge;
		return bestTile.IsValid();
	}

	/// <summary>
	/// Destroys the fence tile and moves the animal to the nearest walkable path.
	/// </summary>
	private void BreakFenceAndEscape( AreaTile tile, TileEdge direction )
	{
		if ( !tile.IsValid() )
			return;

		var escapeTarget = FindNearestPath( tile.WorldPosition );

		tile.GameObject.Destroy();

		// Move the animal onto a walkable path so it can navigate normally
		Agent.GameObject.WorldPosition = escapeTarget;
		Agent.GameObject.Transform.ClearInterpolation();
	}

	/// <summary>
	/// Finds the nearest walkable path tile to a world position.
	/// Falls back to one cell outside in the break direction if no path found.
	/// </summary>
	private Vector3 FindNearestPath( Vector3 origin )
	{
		float closestDist = float.MaxValue;
		Vector3? closest = null;

		foreach ( var path in Scene.GetAll<Path>() )
		{
			if ( path.IsElevated )
				continue;

			if ( !GridNavigation.Instance.CanEnter( new Vector2Int( path.TilePosition ), NavFlags.Default ) )
				continue;

			float dist = path.WorldPosition.DistanceSquared( origin );
			if ( dist < closestDist )
			{
				closestDist = dist;
				closest = path.WorldPosition;
			}
		}

		if ( closest != null )
			return closest.Value;

		// Fallback: one cell outside the enclosure
		var gridPos = GridManager.WorldToGridPosition( origin );
		var outsideGrid = gridPos + _breakDirection.GetDirection();
		var worldPos = GridManager.GridToWorldPosition( outsideGrid ) + GridManager.CentreOffset;
		return worldPos.WithZ( origin.z );
	}

	public override ActionDisplayInfo? GetDisplay()
	{
		return new ActionDisplayInfo( "fence", "Breaking out!", 0 );
	}
}