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 );
}
}