AI/ActionSystem/AgentActionController.cs
using System;
using System.Text.Json.Serialization;

namespace HC3;

public sealed class AgentActionController : Component
{
	/// <summary>
	/// The agent
	/// </summary>
	[Property] public Agent Agent { get; set; }

	/// <summary>
	/// Random
	/// </summary>
	public Random Random { get; } = new Random( Guid.NewGuid().GetHashCode() );

	/// <summary>
	/// A list of actions for this agent
	/// </summary>
	public IEnumerable<AgentAction> Actions { get; private set; }

	[Sync( SyncFlags.FromHost )]
	private AgentAction _current { get; set; }

	[Property, ReadOnly, JsonIgnore]
	public AgentAction CurrentAction => _current;

	protected override void OnStart()
	{
		Actions = GetComponentsInChildren<AgentAction>();

		// Stagger initial tick so 500 agents don't all score on the same frame
		NextTick = Random.Float( 0f, FixedRate );
	}

	RealTimeUntil NextTick;
	const float FixedRate = 0.25f;

	public void Tick()
	{
		if ( !Networking.IsHost )
			return;

		if ( !NextTick )
			return;

		if ( Actions == null )
			return;

		if ( !Agent.Controller.IsGrounded )
			return;

		NextTick = FixedRate;

		var best = default(AgentAction);
		float bestScore = float.MinValue;
		foreach ( var a in Actions )
		{
			var score = a.ScoreInternal();
			if ( score > bestScore )
			{
				bestScore = score;
				best = a;
			}
		}

		if ( best != _current )
		{
			ClearAction();

			_current = best;
			_current?.StartInternal();
		}

		if ( _current != null )
		{
			var result = _current.TickInternal();
			if ( result != AgentAction.Status.Running )
			{
				ClearAction();
			}
		}
	}

	public void ClearAction()
	{
		if ( !_current.IsValid() )
			return;

		// safety so things can call ClearAction from inside an Action's StopAction without trouble
		var c = _current;
		_current = null;

		c.StopInternal();
	}
}