Game/MansionGame.Spectator.cs

Host-side spectator management for the MansionGame. Tracks per-connection spectator GameObjects, spawns a connection-owned spectator prefab when a player is dead or late-joining, and removes it when the player is alive again.

Networking
using System;
using System.Collections.Generic;
using Sandbox;

namespace BrickJam;

public sealed partial class MansionGame
{
	// Host-side spectator pawn per connection (dead players + late joiners). Not networked itself - the
	// spectator GameObjects are network-spawned and owned by their connection.
	private readonly Dictionary<Guid, GameObject> spectators = new();
	
	[Property] private GameObject SpectatorPrefab { get; set; }

	/// <summary>
	/// True when a client joining right now is too late for this round (timer running and past the grace
	/// window) and should spectate until the next level. Legacy <c>ClientJoined</c> branch.
	/// </summary>
	private bool ShouldSpectateOnJoin => TimerActive && (TimePerLevel - (float)hostTimeOut) > TimeToJoin;

	/// <summary>Spawn a connection-owned spectator fly-cam at <paramref name="at"/>. Host-only.</summary>
	private void SpawnSpectator( Connection channel, Transform at )
	{
		RemoveSpectator( channel );

		var go = SpectatorPrefab?.Clone( new CloneConfig () { Transform = at, Name = $"Spectator - {channel.DisplayName}", StartEnabled = true } );

		go?.NetworkSpawn( channel );
		spectators[channel.Id] = go;
	}

	private void RemoveSpectator( Connection channel )
	{
		if ( spectators.Remove( channel.Id, out var go ) && go.IsValid() )
			go.Destroy();
	}

	/// <summary>
	/// Host reconciliation each tick: a dead player gets a spectator pawn (and yields its camera via the
	/// <see cref="Player.Spectating"/> flag); once alive again the spectator is reclaimed.
	/// </summary>
	private void UpdateSpectators()
	{
		foreach ( var go in spawnedPlayers.Values )
		{
			if ( !go.IsValid() || !go.Components.TryGet<Player>( out var player ) )
				continue;

			var channel = player.Network.Owner;
			if ( channel is null )
				continue;

			var hasSpectator = spectators.ContainsKey( channel.Id );

			if ( !player.IsAlive && !hasSpectator )
			{
				SpawnSpectator( channel, new Transform( player.EyePosition, player.WorldRotation ) );
				player.Spectating = true;
			}
			else if ( player.IsAlive && hasSpectator )
			{
				RemoveSpectator( channel );
				player.Spectating = false;
			}
		}
	}
}