AI/ActionSystem/Actions/Guest/SatisfyNeedsAction.cs
using System;

namespace HC3;

public sealed class SatisfyNeedsAction : BehaviorTreeAction
{
	private NeedSystem Needs => Guest.Needs;
	private Building _targetBuilding;
	private Guest Guest => Agent as Guest;
	private bool _forced;
	private readonly (Need need, float priority)[] _needScratch = new (Need, float)[16];

	public void Force( Building building )
	{
		_forced = true;
		_targetBuilding = building;
	}

	public override float Score()
	{
		if ( !Guest.IsValid() || Guest.Money <= 0 ) return 0f;

		if ( Guest.SuspicionLevel >= Guest.ArrestSuspicionThreshold )
			return 0f;

		if ( _targetBuilding.IsValid() ) return 300f;

		// Find actionable needs sorted by priority, try buildings for each
		// Uses pre-allocated buffer to avoid LINQ/allocation overhead
		int needCount = 0;

		foreach ( var n in Needs.GetNeeds() )
		{
			if ( !n.IsActionable() )
				continue;

			if ( Guest.Delinquency > n.Template.DelinquencyThreshold )
				continue;

			if ( needCount >= _needScratch.Length )
				break;

			_needScratch[needCount].need = n;
			_needScratch[needCount].priority = n.GetPriority() + Game.Random.Float( -0.2f, 0.2f );
			needCount++;
		}

		// Sort descending by priority (insertion sort, small N)
		for ( int i = 1; i < needCount; i++ )
		{
			var key = _needScratch[i];
			int j = i - 1;
			while ( j >= 0 && _needScratch[j].priority < key.priority )
			{
				_needScratch[j + 1] = _needScratch[j];
				j--;
			}
			_needScratch[j + 1] = key;
		}

		for ( int i = 0; i < needCount; i++ )
		{
			var building = BuildingSelector.FindBestBuildingForNeed( Guest, _needScratch[i].need, Random );
			if ( building.IsValid() )
			{
				_targetBuilding = building;
				return 100f;
			}
		}

		return 0;
	}

	public override void StopAction()
	{
		base.StopAction();

		// make sure we're deregistered and unloaded from anything we might be part of
		// (I suppose maybe nodes should handle this but that seems kinda complex to cover?)
		if ( _targetBuilding.IsValid() )
		{
			_targetBuilding.Unload( Guest );

			if ( _targetBuilding is BasicRide ride )
			{
				ride.RemoveFromQueue( Guest );
			}
		}

		_forced = false;
		_targetBuilding = null;
	}

	protected override Node BuildTree()
	{
		var entrance = _targetBuilding.GetEntrance();
		var ride = _targetBuilding as BasicRide;
		var steps = new List<Node>();

		if ( !_forced )
		{
			steps.Add( new MoveToNode( Agent, entrance, $"Going to {_targetBuilding.Title}" ) );
		}

		// Queue if there's a ride
		if ( ride.IsValid() )
		{
			steps.Add( new QueueNode( ride, Guest ) );
		}

		steps.Add( new UseBuildingNode( _targetBuilding, Guest, Needs ) );

		return new SequenceNode( steps );
	}
}