GameMode/GameMode.cs

Singleton GameMode component for a King of the Hill game. It manages state machine access, spawn zone setup for local players, notifications on start and level up, tracks spawn zone clones to block opposing teams, and exposes utility like time-left formatting and starter team selection.

NetworkingFile Access
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 = 5,
			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 = 5,
			Zone = ENotificationZone.Center
		});
	}
}