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.
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 );
}
}