AI/AgentTickSystem.cs
using System.Diagnostics;

namespace HC3;

/// <summary>
/// Batch-processes all agent updates
/// </summary>
public sealed class AgentTickSystem : GameObjectSystem<AgentTickSystem>
{
	[ConVar( "hc3.debug.agenttick" )]
	public static bool DebugTiming { get; set; }

	/// <summary>
	/// Last measured update time in milliseconds (movement, animation, wearables).
	/// </summary>
	public static float LastUpdateMs { get; private set; }

	/// <summary>
	/// Last measured fixed update time in milliseconds (AI scoring, needs).
	/// </summary>
	public static float LastFixedUpdateMs { get; private set; }

	private readonly Stopwatch _sw = new();
	private RealTimeSince _lastLog;

	public AgentTickSystem( Scene scene ) : base( scene )
	{
		Listen( Stage.StartUpdate, 0, OnUpdate, "AgentTickSystem.Update" );
		Listen( Stage.StartFixedUpdate, 0, OnFixedUpdate, "AgentTickSystem.FixedUpdate" );
	}

	private void OnUpdate()
	{
		if ( DebugTiming ) _sw.Restart();

		// Single pass: movement + animation + agent-specific updates
		foreach ( var agent in Scene.GetAll<Agent>() )
		{
			if ( !agent.Active ) continue;
			if ( agent is IPlacementObject { IsPlaced: false } ) continue;
			agent.Controller?.Tick();
			agent.Tick();
		}

		if ( DebugTiming )
		{
			_sw.Stop();
			LastUpdateMs = (float)_sw.Elapsed.TotalMilliseconds;

			if ( _lastLog > 1f )
			{
				_lastLog = 0;
			}
		}
	}

	private void OnFixedUpdate()
	{
		if ( DebugTiming ) _sw.Restart();

		// Single pass: AI scoring + needs decay
		foreach ( var agent in Scene.GetAll<Agent>() )
		{
			if ( !agent.Active ) continue;
			if ( agent is IPlacementObject { IsPlaced: false } ) continue;
			agent.ActionController?.Tick();
			agent.FixedTick();
		}

		if ( DebugTiming )
		{
			_sw.Stop();
			LastFixedUpdateMs = (float)_sw.Elapsed.TotalMilliseconds;
		}
	}
}