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