AI/BuildingSelector.cs
using System;

namespace HC3;

#nullable enable

public static class BuildingSelector
{
	private static List<Building> _candidateBuffer;

	public static Building? FindBestBuildingForNeed( Guest guest, Need need, Random rng )
	{
		var guestPos = guest.WorldPosition;

		_candidateBuffer ??= new List<Building>();
		_candidateBuffer.Clear();

		// Collect valid candidates manually instead of LINQ chain
		foreach ( var b in Building.ActiveBuildings )
		{
			if ( !b.SatisfyNeed || b.Need != need.Template )
				continue;
			if ( !b.IsValid() || b.Scene != Game.ActiveScene )
				continue;
			if ( !b.IsAvailable() || guest.Money < b.GuestCost )
				continue;

			_candidateBuffer.Add( b );
		}

		if ( _candidateBuffer.Count == 0 )
			return null;

		// Sort by distance (ascending) in-place
		_candidateBuffer.Sort( ( a, b ) =>
			a.WorldPosition.DistanceSquared( guestPos ).CompareTo( b.WorldPosition.DistanceSquared( guestPos ) )
		);

		// Only check walkability for closest 10
		int walkableCount = 0;
		int limit = Math.Min( _candidateBuffer.Count, 10 );
		for ( int i = 0; i < limit; i++ )
		{
			if ( GridManager.IsWalkable( guestPos, _candidateBuffer[i].GetEntrance() ) )
			{
				// Compact walkable entries to front of buffer
				_candidateBuffer[walkableCount++] = _candidateBuffer[i];
			}
		}

		if ( walkableCount == 0 )
			return null;

		// Fisher-Yates shuffle (in-place, no allocation) on walkable portion
		for ( int i = walkableCount - 1; i > 0; i-- )
		{
			int j = rng.Next( 0, i + 1 );
			(_candidateBuffer[i], _candidateBuffer[j]) = (_candidateBuffer[j], _candidateBuffer[i]);
		}

		// Compute weights and sample weighted random in one pass
		float weightSum = 0f;
		for ( int i = 0; i < walkableCount; i++ )
			weightSum += _candidateBuffer[i].GetInterestWeight( guest );

		if ( weightSum <= 0f )
			return null;

		float roll = rng.Float( 0f, weightSum );
		float accum = 0f;

		for ( int i = 0; i < walkableCount; i++ )
		{
			accum += _candidateBuffer[i].GetInterestWeight( guest );
			if ( accum >= roll )
				return _candidateBuffer[i];
		}

		// Fallback
		return _candidateBuffer[0];
	}
}