Levels/MansionGame.Level.cs

Host-side level manager for the MansionGame. It stores the active Level, handles starting/ending/advancing levels, respawning players at level start, crediting stats/achievements when levels are escaped, and exposes methods to set/next/restart levels.

Networking
using System;
using System.Linq;
using System.Threading.Tasks;
using Sandbox;

namespace BrickJam;

public sealed partial class MansionGame
{
	/// <summary>The active level orchestrator (host-side). Replaces the legacy <c>[Net] CurrentLevel</c>.</summary>
	public Level CurrentLevel { get; private set; }

	protected override void OnStart()
	{
		if ( Networking.IsHost )
			RestartGame();
	}

	/// <summary>
	/// Start-of-level reset: reclaim any spectator pawns and make sure every connected client has a living
	/// player (late joiners who only had a spectator get a fresh player here). Replaces the legacy
	/// <c>Level.RespawnAll</c>.
	/// </summary>
	public void RespawnAll()
	{
		foreach ( var channel in Connection.All )
		{
			RemoveSpectator( channel );

			if ( !spawnedPlayers.TryGetValue( channel.Id, out var go ) || !go.IsValid() )
			{
				SpawnPlayer( channel );
				spawnedPlayers.TryGetValue( channel.Id, out go );
			}

			if ( go.IsValid() && go.Components.TryGet<Player>( out var player ) )
			{
				player.Spectating = false;

				// Compute the spawn HOST-side (where CurrentLevelType is already correct) and hand it to the
				// owner, so late level-type replication can't strand remote players in the previous level.
				var spawn = GetSpawnPoint();
				spawn.Position += Vector3.Up * 50f;
				player.Respawn( spawn );
			}
		}
	}

	// Implements the partial declared in MansionGame.Timer.cs.
	partial void OnTimerExpired() => RestartGame();

	public static async void SetLevel<T>() where T : Level, new()
	{
		if ( !Networking.IsHost || !Instance.IsValid() )
			return;

		if ( Instance.CurrentLevel is not null )
		{
			await Instance.CurrentLevel.End();
			Instance.CurrentLevel = null;
		}

		var level = new T();
		Instance.CurrentLevel = level;
		Instance.CurrentLevelType = level.Type;

		await level.Start();
	}

	public static void NextLevel()
	{
		if ( !Instance.IsValid() )
			return;

		// Survived a gameplay level -> credit each living player (Services stat). The shop isn't a "level".
		if ( Instance.CurrentLevel is MansionLevel or DungeonLevel or BathroomsLevel )
		{
			foreach ( var p in Instance.Scene.GetAllComponents<Player>().Where( x => x.IsAlive ) )
				p.TrackStat( GameStats.LevelsCompleted, 1 );

			// Achievement crediting for leaving this level via the trapdoor (Grand Tour + Left For Dead).
			// The Bathrooms exit is the FinalDoor, which credits itself.
			CreditLevelEscape( Instance.CurrentLevel.Type );
		}

		switch ( Instance.CurrentLevel )
		{
			case ShopLevel: SetLevel<MansionLevel>(); break;
			case MansionLevel: SetLevel<DungeonLevel>(); break;
			case DungeonLevel: SetLevel<BathroomsLevel>(); break;
			case BathroomsLevel: SetLevel<ShopLevel>(); break;
		}
	}

	public static async void RestartLevel()
	{
		if ( !Instance.IsValid() || Instance.CurrentLevel is null )
			return;

		await Instance.CurrentLevel.End();
		await Instance.CurrentLevel.Start();
	}

	public static void RestartGame()
	{
		ResetRandomSeed();
		SetLevel<ShopLevel>();
	}

	/// <summary>
	/// Host-side achievement crediting when players escape <paramref name="escapedType"/> (trapdoor or final
	/// door): Grand Tour (record the escaped level for everyone who got out) and Left For Dead (the lone
	/// survivor leaving dead co-op partners behind). Routed per-player to their own client.
	/// </summary>
	public static void CreditLevelEscape( LevelType escapedType )
	{
		if ( !Instance.IsValid() )
			return;

		var players = Instance.Scene.GetAllComponents<Player>().ToList();
		var survivors = players.Where( x => x.IsAlive ).ToList();

		foreach ( var p in survivors )
			p.NotifyLevelEscaped( (int)escapedType );

		// Left For Dead: exactly one survivor escaping past at least one dead co-op partner.
		if ( players.Count >= 2 && survivors.Count == 1 && players.Any( x => !x.IsAlive ) )
			survivors[0].TrackAchievement( GameStats.AchLeftForDead );
	}
}