AI/ActionSystem/Actions/LoiterAction.cs
namespace HC3;
public sealed class LoiterAction : BehaviorTreeAction
{
private TimeSince _timeSinceLastEnded;
private bool _isLoitering;
private float _duration;
private Building _targetBuilding;
private Vector3 _lookTarget;
[Property] public RangedFloat LoiterDuration { get; set; } = new RangedFloat( 5f, 15f );
[Property] public float LoiterFrequency { get; set; } = 60f;
public override float Score()
{
if ( !ParkManager.Instance?.IsOpen() ?? false )
return 0f;
if ( Controller.IsOnStairs ) return 0;
if ( _isLoitering )
return 200f;
return _timeSinceLastEnded > LoiterFrequency ? 50f : 0f;
}
protected override void OnStart()
{
_timeSinceLastEnded = 0;
}
protected override void OnTreeStart()
{
_duration = Game.Random.Float( LoiterDuration.Min, LoiterDuration.Max );
_isLoitering = true;
PickNearbyBuilding();
}
protected override void OnTreeStop()
{
if ( _isLoitering )
{
_timeSinceLastEnded = 0f;
_isLoitering = false;
}
}
protected override Node BuildTree()
{
if ( _targetBuilding.IsValid() )
{
return new SequenceNode( new List<Node>()
{
new MoveToNode( Agent, _lookTarget, $"Going to look at {_targetBuilding.Title}" ),
new LookAtNode( Agent, _targetBuilding.WorldPosition ),
new DelayNode( _duration, $"Having a look at {_targetBuilding.Title}" )
} );
}
else
{
return new DelayNode( _duration, "Loitering" );
}
}
private void PickNearbyBuilding()
{
_targetBuilding = null;
// Pick a random walkable ride without LINQ allocation
BasicRide pick = null;
int count = 0;
foreach ( var b in Building.ActiveBuildings )
{
if ( b is not BasicRide ride || !ride.IsValid() )
continue;
if ( !GridManager.IsWalkable( Agent.WorldPosition, ride.GetEntrance() ) )
continue;
count++;
if ( Game.Random.Next( count ) == 0 )
pick = ride;
}
if ( pick is null )
return;
var entrance = pick.GetQueueEnd()?.Connections.OfType<Path>().FirstOrDefault( x => x.PathType == PathType.Path );
if ( entrance is not null )
{
_targetBuilding = pick;
_lookTarget = entrance.WorldPosition;
}
}
}