ui/SpectatorListPanel.razor

A Razor UI panel that lists spectator clients. It collects all connections that do not own an in-scene Player component, computes dynamic sizing and opacity based on count and game state, and renders avatar, a videocam icon and a truncated display name for each spectator.

NetworkingFile Access
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel

@{
	var clients = Connection.All.ToList();
	var players = Game.ActiveScene.GetAllComponents<Player>().ToList();

	var spectatorClients = new List<Connection>();
	foreach ( var client in clients )
	{
		var player = players.FirstOrDefault( p => p.Network?.Owner == client );
		if ( player is null )
			spectatorClients.Add( client );
	}

	int count = spectatorClients.Count;
	bool isPlaying = Manager.Instance?.GameState == GameState.Playing;

	// Dynamic sizing: shrink names as more spectators join
	float baseFontSize = isPlaying ? 16f : 22f;
	float fontSize = Math.Max( 10f, baseFontSize - count * 0.5f );
	float baseImgSize = isPlaying ? 20f : 44f;
	float imgSize = Math.Max( 12f, baseImgSize - count * 1.5f );
	float opacity = Math.Max( 0.2f, 0.5f - count * 0.03f );
}

<root class="@(isPlaying ? "playing" : "lobby")" style="opacity: @opacity;">
	@foreach ( var client in spectatorClients )
	{
		int maxNameLen = isPlaying ? 14 : 20;
		string truncatedName = client.DisplayName.Length > maxNameLen
			? client.DisplayName[..maxNameLen] + "…"
			: client.DisplayName;

		<div class="spectator" style="font-size: @(fontSize)px;">
			<i>videocam</i>
			<img class="avatar" src="avatar:@(client.SteamId)" style="width: @(imgSize)px; height: @(imgSize)px;" />
			<label class="name">@truncatedName</label>
		</div>
	}
</root>

@code {
	protected override int BuildHash()
	{
		return System.HashCode.Combine( Connection.All.Count, Manager.Instance?.Players?.Count, Manager.Instance?.GameState );
	}
}