Crew.cs
using Sandbox;
using System;
using System.Collections.Generic;

// Who's still in the fight. "Active" means a player who hasn't escaped and
// hasn't been eliminated - the same test several systems need, so it lives
// in one place rather than being re-written per caller.
//
// This gets asked a LOT: the spawner reads it twice a frame, and every cop
// reads it several times a second to pick a target. So it does two things
// to stay cheap:
//
//   1. Uses Scene.FindAllWithTag, which is an index the engine keeps as
//      tags are added and removed, rather than Scene.GetAllObjects, which
//      walks the entire hierarchy. On a real map that's thousands of
//      GameObjects visited per call.
//
//   2. Caches the result for the rest of the frame. Every caller in a
//      frame gets the same answer anyway, so recomputing it per caller is
//      pure waste.
public static class Crew
{
	// The cached answer, plus enough to know when it went stale. Static
	// rather than per-component because the question isn't about any one
	// object - and it has to survive scene loads, which _cachedScene
	// handles by forcing a rebuild when the scene changes underneath us.
	static readonly List<GameObject> _active = new();
	static Scene _cachedScene;
	static float _cachedAt = float.NegativeInfinity;

	/// <summary>
	/// Every player still in the fight. The returned list is shared and
	/// rebuilt each frame - read it, don't hold onto it.
	/// </summary>
	public static IReadOnlyList<GameObject> ActivePlayers( Scene scene )
	{
		if ( scene is null )
			return Array.Empty<GameObject>();

		// Time.Now is constant for the whole of a frame, so comparing
		// against it is "have we already answered this frame?". The scene
		// check catches a mission load, where the old scene's players are
		// gone but no time may have passed yet.
		if ( _cachedScene == scene && _cachedAt == Time.Now )
			return _active;

		_cachedScene = scene;
		_cachedAt = Time.Now;
		_active.Clear();

		foreach ( var go in scene.FindAllWithTag( "player" ) )
		{
			// Out of play, not out of existence: MissionPlayerState leaves
			// eliminated and escaped players enabled so their camera keeps
			// running for spectating, and marks them with a tag instead.
			if ( !go.IsValid() || !go.Enabled )
				continue;

			if ( go.Tags.Has( "escaped" ) || go.Tags.Has( "eliminated" ) )
				continue;

			_active.Add( go );
		}

		return _active;
	}

	/// <summary>
	/// How many players are still in the fight, never below 1.
	/// </summary>
	public static int ActiveCount( Scene scene )
	{
		// Floored at 1 so anything scaling off this can divide or multiply
		// safely. A count of zero only happens the instant the job ends,
		// at which point nothing is reading it anyway.
		return Math.Max( ActivePlayers( scene ).Count, 1 );
	}

	/// <summary>
	/// The number of players difficulty should scale off: how many are
	/// still fighting, but never more than there are people connected.
	/// </summary>
	public static int ScalingSize( Scene scene )
	{
		// The tag count is the honest answer to "who's still fighting",
		// and it's what makes pressure drop as the crew gets whittled
		// down. But it's a count of GameObjects, and anything that leaves
		// a stray player-tagged object behind - a duplicate spawned across
		// a scene change, a corpse that didn't get cleaned up - would read
		// as extra players and multiply the entire police response.
		//
		// The connection count can't drift like that: it's one per human,
		// straight from the network layer. Clamping to it means a bug of
		// that shape costs us accuracy at worst, instead of flooding the
		// map with cops until the game stops responding.
		var connected = Connection.All?.Count ?? 1;

		return Math.Clamp( ActiveCount( scene ), 1, Math.Max( connected, 1 ) );
	}
}