Levels/Level.cs

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.

NetworkingFile AccessNative Interop
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,
	};
}