RobberyZone.cs
using Sandbox;
using System.Collections.Generic;
using System.Linq;

// The area the crew has to hold while the robbery runs - the gas station
// interior. Put this on a GameObject with a trigger Collider covering it.
//
// MissionTimer only advances the robbery while somebody is inside, so
// wandering off stalls the job instead of letting you kite cops around the
// map for the whole timer. Assign it to MissionTimer.RobberyZone; leave
// that unset and the timer behaves as it did before, running regardless of
// where anyone is.
public sealed class RobberyZone : Component, Component.ITriggerListener
{
	// Tracked as a set rather than a count because OnTriggerEnter can fire
	// more than once for the same body (multiple colliders, re-entry), and
	// a bare counter would drift out of step and never return to zero.
	readonly HashSet<GameObject> _inside = new();

	/// <summary>
	/// Is anyone still in the fight standing in here?
	/// </summary>
	public bool AnyPlayerInside => LivePlayersInside().Any();

	public int PlayerCountInside => LivePlayersInside().Count();

	public void OnTriggerEnter( Collider other )
	{
		var player = other.GameObject.Root;

		if ( player.Tags.Has( "player" ) )
			_inside.Add( player );
	}

	public void OnTriggerExit( Collider other )
	{
		_inside.Remove( other.GameObject.Root );
	}

	// Filtered on read rather than on the way in, because a player's state
	// changes while they're standing inside: someone who goes down in the
	// store is physically still in the trigger but shouldn't be holding
	// the robbery open for the rest of the crew.
	IEnumerable<GameObject> LivePlayersInside()
	{
		return _inside.Where( go => go.IsValid()
			&& !go.Tags.Has( "escaped" )
			&& !go.Tags.Has( "eliminated" ) );
	}
}