Server-side Level base class that orchestrates level lifecycle and content. It defines level type, music lookup, world bounds, start/end flow, monster tracking, respawn logic, and factory mapping from LevelType to CLR level classes.
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Sandbox;
namespace BrickJam;
/// <summary>
/// Level orchestration. The legacy <c>Level</c> was an <c>Entity</c> (for replication); in the Scene
/// System it is a plain server-side object owned by <see cref="MansionGame"/>. The single replicated
/// fact clients need — the current level type — lives on the manager as
/// <see cref="MansionGame.CurrentLevelType"/>.
///
/// DEFERRED: grid generation (<c>GenerateGrid</c>, needs the grid package), music
/// (<c>ProcessMusic</c>), and the black-screen transition + event-log messages (UI system).
/// </summary>
public abstract partial class Level
{
public abstract LevelType Type { get; }
public string Music => GetMusic( Type );
public virtual BBox WorldBox => new( new Vector3( -100000f ), new Vector3( 100000f ) );
/// <summary>
/// Music track per level. Single source of truth so the client-side music player
/// (<see cref="MansionGame"/>) can resolve a track from the replicated <see cref="LevelType"/>
/// without a host-side <see cref="Level"/> instance.
/// </summary>
public static string GetMusic( LevelType type ) => type switch
{
LevelType.Shop => "sounds/music/scary_quest_at_midnight.sound",
LevelType.Mansion => "sounds/music/looming_trees_in_eerie_woods.sound",
LevelType.Dungeon => "sounds/music/malevolent_sightings_in_the_room.sound",
LevelType.Bathrooms => "sounds/music/depths_and_terror.sound",
_ => null,
};
public LegacyUsableComponent Exit { get; set; }
public List<NPC> Monsters { get; } = new();
public TimeSince SinceStarted { get; set; }
public static bool GameIsEnding { get; set; }
protected Scene Scene => MansionGame.Instance.Scene;
public virtual void Compute()
{
var players = Scene.GetAllComponents<Player>().ToList();
if ( players.Count > 0 && players.All( x => !x.IsAlive ) && !GameIsEnding )
{
MansionGame.RestartGame();
GameIsEnding = true;
// TODO (UI): Eventlog "Looks like everyone died, better luck next time!"
}
}
protected void RespawnAll() => MansionGame.Instance.RespawnAll();
public virtual async Task Start()
{
await GameTask.Yield();
RespawnAll();
// Companion spawning for players who bought the upgrade.
foreach ( var player in Scene.GetAllComponents<Player>().ToList() )
{
if ( player.HasUpgrade( "Cartoony Sidekick" ) )
{
var doob = NPC.Create<Doob>( player.WorldPosition, player.WorldRotation );
doob.Owner = player;
player.Doob = doob;
// Who's a Good Boy?: Doob summoned to protect this player.
player.TrackAchievement( GameStats.AchGoodBoy );
}
}
Exit?.GameObject.Destroy();
if ( Type == LevelType.Bathrooms )
{
var finalDoors = Scene.GetAllComponents<ValidFinalDoorPosition>().ToList();
var spot = MansionGame.Random.FromList( finalDoors, null );
if ( spot is not null )
Exit = FinalDoor.Create( spot.WorldPosition, spot.WorldRotation );
}
else
{
var trapdoors = Scene.GetAllComponents<ValidTrapdoorPosition>().Where( x => x.LevelType == Type ).ToList();
var spot = MansionGame.Random.FromList( trapdoors, null );
if ( spot is not null )
Exit = Trapdoor.Create( spot.WorldPosition, spot.WorldRotation );
}
foreach ( var spawner in Scene.GetAllComponents<LootSpawner>().Where( x => x.LevelType == Type ).ToList() )
spawner.SpawnLoot();
foreach ( var door in Scene.GetAllComponents<Door>().Where( x => x.LevelType == Type ).ToList() )
door.Close();
await GameTask.DelayRealtimeSeconds( 1f );
await GenerateGrid();
GameIsEnding = false;
SinceStarted = 0f; // reset the level clock (used by the Slipped on a Soap achievement)
MansionGame.Instance.TimerStart();
}
public virtual async Task End()
{
await GameTask.Yield();
MansionGame.Instance?.ShowBlackScreen( 2f, 1f, 1f );
Exit?.GameObject.Destroy();
Exit = null;
foreach ( var monster in Monsters.ToList() )
RemoveMonster( monster );
foreach ( var spawner in Scene.GetAllComponents<LootSpawner>().ToList() )
spawner.DeleteLoot();
foreach ( var door in Scene.GetAllComponents<Door>().Where( x => WorldBox.Contains( x.WorldPosition ) ).ToList() )
door.Close();
foreach ( var loot in Scene.GetAllComponents<Loot>().ToList() )
loot.GameObject.Destroy();
MansionGame.Instance.TimerStop();
foreach ( var doob in Scene.GetAllComponents<Doob>().ToList() )
{
if ( doob.Owner.IsValid() )
doob.Owner.Doob = null;
doob.GameObject.Destroy();
}
}
public virtual void RegisterMonster( NPC monster ) => Monsters.Add( monster );
public virtual void RemoveMonster( NPC monster )
{
Monsters.Remove( monster );
if ( monster.IsValid() )
monster.GameObject.Destroy();
}
public static Type GetClrType( LevelType type ) => type switch
{
LevelType.Shop => typeof( ShopLevel ),
LevelType.Mansion => typeof( MansionLevel ),
LevelType.Dungeon => typeof( DungeonLevel ),
LevelType.Bathrooms => typeof( BathroomsLevel ),
_ => null,
};
}