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];
}
}