GameMode/GameMode.cs
using KOTH.Notification;
using KOTH.PlayerExp;
using Sandbox.Diagnostics;
using Sandbox.Events;
using Sandbox.Utility;

namespace KOTH;

public record GamemodeInitializedEvent(string Title) : IGameEvent;

public sealed partial class GameMode : SingletonComponent<GameMode>,
	IGameEventHandler<LocalPlayerSpawnedEvent>,
	IGameEventHandler<LocalPlayerDiedEvent>,
	IGameEventHandler<LevelUpEvent>
{
	[Property] public string Title { get; set; } = "KOTH";
	[Property] public string NothingState { get; set; } = "";

	/////////////////////////////////////////////////////////////

	[RequireComponent] public DamageManager DamageManager { get; private set; }
	[RequireComponent] public BuildingManager BuildingManager { get; private set; }
	[RequireComponent] public ClassList ClassList { get; private set; }
	[RequireComponent] public TextChat TextChat { get; private set; }
	[RequireComponent] public NotificationManager NotificationManager { get; private set; }
	[RequireComponent] public ExpManager ExpManager { get; private set; }
	[RequireComponent] public AudioComponent AudioComponent { get; private set; }
	public GameStats GameStats { get => GameObject.GetComponent<GameStats>(); }

	/////////////////////////////////////////////////////////////

	private StateMachineComponent _stateMachine;

	public StateMachineComponent StateMachine =>
		_stateMachine ??= Components.GetInDescendantsOrSelf<StateMachineComponent>();

	/////////////////////////////////////////////////////////////
	
	public string GetTimeLeftString()
	{
		if (float.IsInfinity(StateMachine.NextStateTime) || StateMachine.CurrentState.DefaultDuration == 999)
		{
			return NothingState;
		}
		
		var Remaining = StateMachine.NextStateTime - Time.Now;
		var TimeRemaining = TimeSpan.FromSeconds(Remaining);
		var FormattedTime = string.Format("{0:D2}:{1:D2}", (int)TimeRemaining.TotalMinutes, TimeRemaining.Seconds);

		return FormattedTime;
	}

	/////////////////////////////////////////////////////////////

	protected override void OnAwake()
	{
		if (Networking.IsHost)
		{
			if (Instance is { IsValid: true, Active: true, Scene: { } scene } && scene == Scene)
			{
				GameObject.Enabled = false;
				return;
			}
		}

		base.OnAwake();
	}

	protected override void OnStart()
	{
		base.OnStart();

		NotificationManager.AddNotification(new FNotification()
		{
			Message = $"Welcome to {Title}",
			Duration = 3,
			Zone = ENotificationZone.Center,
			Image = "images/square.png"
		});

		if (Networking.IsHost)
		{
			MapSpawnZones = new();
			foreach (var Object in Scene.GetAllObjects(true))
			{
				if (Object.GetComponent<SpawnZone>() is { } SpawnZone)
				{
					MapSpawnZones.Add(Object);
				}
			}
		}
	}

	/////////////////////////////////////////////////////////////

	[Sync] public NetList<GameObject> MapSpawnZones { get; set; }
	private List<GameObject> SpawnZoneBlockers = new();

	void IGameEventHandler<LocalPlayerSpawnedEvent>.OnGameEvent(LocalPlayerSpawnedEvent PlayerSpawnedEvent)
	{
		var SpawnedPlayer = PlayerSpawnedEvent.Player;
		if (!SpawnedPlayer.IsValid())
		{
			Log.Warning($"{this} got invalid player for PlayerSpawnedEvent");
			return;
		}

		// TODO : we want to accept the scene's version of the spawn as truth but create our own
		// not-networked version that we can act on. This spawns another zone every time u spawn :(

		foreach (var TempZone in SpawnZoneBlockers)
		{
			Log.Warning("?");
			TempZone.Destroy();
		}

		SpawnZoneBlockers.Clear();

		foreach (var SpawnZoneObject in MapSpawnZones)
		{
			var SpawnZone = SpawnZoneObject.GetComponent<SpawnZone>();

			if (!SpawnZone.IsValid())
			{
				Log.Warning($"Invalid spawnzone in MapSpawnZones on {this}");
				continue;
			}

			if (SpawnZone.Team.GetOpponents() == PlayerState.Local.Team)
			{
				var ClonedSpawnObject = SpawnZoneObject.Clone(SpawnZoneObject.WorldPosition,
					SpawnZoneObject.WorldRotation, SpawnZoneObject.WorldScale);
				ClonedSpawnObject.NetworkMode = NetworkMode.Never;

				var ClonedSpawnZone = ClonedSpawnObject.GetComponent<SpawnZone>();
				if (ClonedSpawnZone.IsValid())
				{
					ClonedSpawnZone.CreatePlayerCollisionBox();
					SpawnZoneBlockers.Add(ClonedSpawnObject);
				}
				else
				{
					Log.Warning($"failed to create clone of spawn zone {SpawnZone}");
					ClonedSpawnObject.Destroy();
				}
			}
		}
	}

	public void OnGameEvent(LocalPlayerDiedEvent eventArgs)
	{
		// DestroySpawnZoneBlockers();
	}

	public void DestroySpawnZoneBlockers()
	{
		foreach (var TempZone in SpawnZoneBlockers)
		{
			TempZone.Destroy();
		}

		SpawnZoneBlockers.Clear();
	}

	/////////////////////////////////////////////////////////////

	public static Team GetStarterTeam()
	{
		int Ts = 0;
		int CTs = 0;

		foreach (var PlayerState in GameNetworkManager.PlayerStates)
		{
			if (PlayerState.IsValid())
			{
				if (PlayerState.Team == Team.CounterTerrorist)
				{
					++CTs;
				}

				if (PlayerState.Team == Team.Terrorist)
				{
					++Ts;
				}
			}
		}

		if (Ts > CTs)
		{
			return Team.CounterTerrorist;
		}
		else
		{
			return Team.Terrorist;
		}
	}

	/////////////////////////////////////////////////////////////

	public void OnGameEvent(LevelUpEvent eventArgs)
	{
		NotificationManager.AddNotification(new FNotification()
		{
			Message = $"Level Up: {eventArgs.Level}",
			Duration = 3,
			Zone = ENotificationZone.Center
		});
	}
}