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