PoliceSpawns.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
// Shared spawn-point picking for cops. Used by both MissionSpawner (new
// cops) and CopBotAI (cops respawning after being killed) - without both
// going through here, dead cops would keep popping back into existence
// next to the player even though fresh ones respect the perimeter.
public static class PoliceSpawns
{
// Spawn points are placed in the editor and never move, so finding
// them once per scene is enough. Worth caching because Pick() runs on
// every cop death, and with a dozen cops fighting that's constant.
static readonly List<GameObject> _points = new();
static Scene _cachedScene;
/// <summary>
/// Every point tagged police_spawn in the scene. Shared list - read
/// it, don't hold onto it or modify it.
/// </summary>
public static IReadOnlyList<GameObject> All( Scene scene )
{
if ( scene is null )
return Array.Empty<GameObject>();
// Rebuild on a scene change, and also if anything in the cache has
// been destroyed - that's the only way the set can change during a
// job, and it's cheap to check compared to rebuilding every call.
var stale = _cachedScene != scene || _points.Any( go => !go.IsValid() );
if ( !stale )
return _points;
_cachedScene = scene;
_points.Clear();
_points.AddRange( scene.FindAllWithTag( "police_spawn" ) );
return _points;
}
/// <summary>
/// A random spawn point at least minDistance away from every player.
/// Null if there are no spawn points at all.
/// </summary>
public static GameObject Pick( Scene scene, float minDistance )
{
var points = All( scene );
if ( points.Count == 0 )
return null;
var players = Crew.ActivePlayers( scene );
if ( players.Count == 0 )
return points[Random.Shared.Next( points.Count )];
// Squared distances - avoids a square root per point per player,
// and comparisons come out the same as long as both sides are
// squared. Matters because this runs every time a cop dies.
var minDistanceSqr = minDistance * minDistance;
float NearestPlayerSqr( GameObject point )
{
var nearest = float.MaxValue;
foreach ( var player in players )
nearest = MathF.Min( nearest, point.WorldPosition.DistanceSquared( player.WorldPosition ) );
return nearest;
}
var outsidePerimeter = points
.Where( p => NearestPlayerSqr( p ) >= minDistanceSqr )
.ToList();
if ( outsidePerimeter.Count > 0 )
return outsidePerimeter[Random.Shared.Next( outsidePerimeter.Count )];
// Every point is inside the perimeter - a small map, or the crew
// spread out to cover the exits. Fall back to the furthest one
// rather than refusing to spawn, because silently stopping the
// waves is a worse failure than one cop appearing too close.
return points.OrderByDescending( NearestPlayerSqr ).First();
}
}