MissionStartTrigger.cs
using Sandbox;
using SWB.Shared;

// Put this on the object you already have in the map, alongside a
// trigger Collider sized to the area the player needs to stand in to
// interact with it. Walking into range and holding the Use button (E
// by default) for HoldDuration seconds kicks off the mission: cops
// start spawning and the survive timer starts counting.
public sealed class MissionStartTrigger : Component, Component.ITriggerListener, IInteractable
{
	[Property]
	public MissionSpawner Spawner { get; set; }

	[Property]
	public MissionTimer Timer { get; set; }

	[Property]
	public float HoldDuration { get; set; } = 3f;

	bool _playerInRange = false;
	float _holdProgress = 0f;
	bool _missionStarted = false;

	// Drives the on-screen prompt - see IInteractable. This one matters
	// most of the three: it's a 3 second hold, so without a progress bar
	// it just feels like nothing is happening.
	public bool PlayerInRange => _playerInRange;
	public float HoldFraction => HoldDuration <= 0f ? 0f : _holdProgress / HoldDuration;
	public string PromptText => "Hold E to start the job";
	public bool SuppressPrompt => _missionStarted;

	public void OnTriggerEnter( Collider other )
	{
		if ( other.GameObject.Root.Tags.Has( "player" ) )
			_playerInRange = true;
	}

	public void OnTriggerExit( Collider other )
	{
		if ( other.GameObject.Root.Tags.Has( "player" ) )
		{
			_playerInRange = false;
			_holdProgress = 0f; // walking away mid-hold cancels progress
		}
	}

	protected override void OnUpdate()
	{
		if ( _missionStarted || !_playerInRange )
			return;

		if ( Input.Down( InputButtonHelper.Use ) )
		{
			_holdProgress += Time.Delta;

			if ( _holdProgress >= HoldDuration )
				BeginMission();
		}
		else
		{
			_holdProgress = 0f;
		}
	}

	void BeginMission()
	{
		// Local guard so a held key can't fire this twice before the
		// broadcast lands back on us.
		_missionStarted = true;
		BeginMissionBroadcast();
	}

	// Input is read locally, so without broadcasting, whoever held E
	// starts the job only on their own machine - everyone else sees
	// nothing and has to start it themselves. This puts every client into
	// the mission together off one person's interaction.
	//
	// Safe to run on all clients: MissionSpawner ignores StartSpawning on
	// non-hosts, so only the host actually produces cops. The rest just
	// need their timer and HUD running.
	[Rpc.Broadcast]
	void BeginMissionBroadcast()
	{
		_missionStarted = true;
		Spawner?.StartSpawning();
		Timer?.StartMission();

		// The "go here to start the job" waypoint has done its job. Found
		// on this same GameObject rather than wired in the editor, so
		// adding the marker is the only step.
		var marker = Components.Get<ObjectiveMarker>();

		if ( marker is not null )
			marker.Enabled = false;
	}
}