Map instance class for the Mansion level. It handles creating and wiring map-placed entities (player spawns, doors, loot spawners, shop, signs, etc.) when map objects are instantiated, adding components, models, tags and network-spawning host-owned objects.
using Sandbox;
namespace BrickJam;
public sealed partial class MansionMapInstance : MapInstance
{
// NPC navigation now uses the GridAStar grid (built per-level in Level.GenerateGrid), not the scene
// NavMesh, so the old OnMapLoaded navmesh-rebuild hook has been removed.
protected override void OnCreateObject( GameObject go, MapLoader.ObjectEntry kv )
{
if ( !Game.IsPlaying ) return;
if ( !Networking.IsHost ) return;
switch ( kv.TypeName?.ToLowerInvariant() )
{
case "playerspawn":
case "player_spawn":
CreatePlayerSpawn( go, kv );
break;
case "validtrapdoorposition":
case "valid_trapdoor_position":
CreateValidTrapdoorPosition( go, kv );
break;
case "validfinaldoorposition":
case "valid_final_door_position":
go.Components.Create<ValidFinalDoorPosition>();
break;
case "lootspawner":
case "loot_spawner":
CreateLootSpawner( go, kv );
break;
case "pissingguyspawner":
case "pissing_guy_spawner":
CreatePissingGuySpawner( go, kv );
break;
case "door":
CreateDoor( go, kv );
break;
case "shopdoor":
case "shop_door":
CreateShopDoor( go );
break;
case "shop":
CreateShop( go );
break;
case "slipperysign":
case "slippery_sign":
CreateSlipperySign( go );
break;
case "sellarea":
case "sell_area":
CreateSellArea( go );
break;
default:
// Surface anything we didn't handle so unmapped legacy Hammer types are obvious.
if ( !string.IsNullOrWhiteSpace( kv.TypeName ) )
Log.Info( $"[MansionMapInstance] Unhandled map entity type: '{kv.TypeName}'" );
break;
}
}
private static void CreatePlayerSpawn( GameObject go, MapLoader.ObjectEntry kv )
{
var component = go.Components.Create<PlayerSpawn>();
component.LevelType = kv.GetValue( "LevelType", LevelType.None );
}
private static void CreateValidTrapdoorPosition( GameObject go, MapLoader.ObjectEntry kv )
{
var component = go.Components.Create<ValidTrapdoorPosition>();
component.LevelType = kv.GetValue( "LevelType", LevelType.None );
}
private static void CreateLootSpawner( GameObject go, MapLoader.ObjectEntry kv )
{
var component = go.Components.Create<LootSpawner>();
component.LootToSpawn = ResolveLootPrefab( kv );
component.IsContainer = kv.GetValue( "IsContainer", false );
component.ChanceToSpawn = kv.GetValue( "ChanceToSpawn", 0.5f );
component.LevelType = kv.GetValue( "LevelType", LevelType.None );
Log.Info( $"[MansionMapInstance] LootSpawner: level={component.LevelType} container={component.IsContainer} " +
$"chance={component.ChanceToSpawn} loot={component.LootToSpawn?.ResourceName ?? "<none>"}" );
}
/// <summary>
/// Resolve the loot resource referenced by a spawner. Tries the direct resource path first,
/// then falls back to looking it up by file name via the <see cref="LootPrefab"/> registry
/// (handles path-format differences between the legacy map data and the current project layout).
/// </summary>
private static LootPrefab ResolveLootPrefab( MapLoader.ObjectEntry kv )
{
var loot = kv.GetResource<LootPrefab>( "LootToSpawn" );
if ( loot is not null )
return loot;
var raw = kv.GetString( "LootToSpawn" );
if ( string.IsNullOrWhiteSpace( raw ) )
return null;
return LootPrefab.Get( System.IO.Path.GetFileNameWithoutExtension( raw ) );
}
private static void CreatePissingGuySpawner( GameObject go, MapLoader.ObjectEntry kv )
{
var component = go.Components.Create<PissingGuySpawner>();
component.ChanceToSpawn = kv.GetValue( "ChanceToSpawn", 0.35f );
AddModel( go, "models/pissing_guy/pissing_guy.vmdl", false );
}
private static void CreateDoor( GameObject go, MapLoader.ObjectEntry kv )
{
// OnCreateObject is host-only (guard at the top of the override), so the door is built once on
// the host and replicated to every client via NetworkSpawn. This is essential: a client's
// RequestUse ([Rpc.Host]) only ever swings the HOST's door, so it MUST be one shared networked
// object - otherwise a client's own local copy would stay shut ("clients can't open doors").
var component = go.Components.Create<Door>();
component.LevelType = kv.GetValue( "LevelType", LevelType.None );
component.AlternativeModel = kv.GetValue( "AlternativeModel", false );
component.Locked = kv.GetValue( "Locked", false );
AddModel( go, GetDoorModel( component.LevelType, component.AlternativeModel ), true );
go.Tags.Add( "door" );
go.NetworkSpawn();
}
private static void CreateShopDoor( GameObject go )
{
go.Components.Create<ShopDoor>();
AddModel( go, "models/furniture/mansion_furniture/mansion_door.vmdl", true );
go.Tags.Add( "door" );
// Host-only creation (see OnCreateObject), so network it or clients can't see/use the entrance.
go.NetworkSpawn();
}
private static void CreateShop( GameObject go )
{
go.Components.Create<Shop>();
AddModel( go, "models/furniture/shop/shop_counter.vmdl", true );
go.Tags.Add( "solid", "shop" );
// Network it: clients need the counter's "solid" collision and a usable target to aim at.
go.NetworkSpawn();
}
private static void CreateSlipperySign( GameObject go )
{
go.Components.Create<SlipperySign>();
AddModel( go, "models/furniture/bathrooms_furniture/wet_floor_sign.vmdl", true );
go.Tags.Add( "slip", "nocollide" );
// Network it: each client's owner-side CalculateSlip traces the "slip" tag locally.
go.NetworkSpawn();
}
private static void CreateSellArea( GameObject go )
{
go.Components.Create<SellArea>();
go.Tags.Add( "sell_area" );
}
private static string GetDoorModel( LevelType levelType, bool alternativeModel )
{
return levelType switch
{
LevelType.Dungeon => alternativeModel
? "models/furniture/dungeon_props/dungeon_jail_door.vmdl"
: "models/furniture/dungeon_props/dungeon_wood_door.vmdl",
LevelType.Bathrooms => "models/furniture/dungeon_props/bathroom_stall_door.vmdl",
_ => "models/furniture/mansion_furniture/mansion_door.vmdl",
};
}
private static void AddModel( GameObject go, string modelPath, bool withCollider )
{
var model = Model.Load( modelPath );
if ( model is null )
return;
var renderer = go.Components.Create<SkinnedModelRenderer>();
renderer.Model = model;
if ( !withCollider )
return;
var collider = go.Components.Create<ModelCollider>();
collider.Model = model;
}
}