A scene-system Door component that implements swinging door behavior and state transitions. It tracks initial rotation, opens/closes on use (host-only), plays open/close sounds, animates rotation in OnFixedUpdate, and marks nearby GridAStar grid cells with a "door" tag so NPC pathfinding avoids doorways; it also prompts monsters to recalculate paths after changes.
using System;
using System.Linq;
using Sandbox;
namespace BrickJam;
/// <summary>
/// Swinging-door behaviour for the <see cref="Door"/> map component (stub/data in
/// <c>LegacyMapEntities.cs</c>). Scene-System port of the legacy <c>Door</c> open/close state machine.
///
/// Simplified: the door pivots around its GameObject origin (legacy computed a hinge from the
/// collision bounds). DEFERRED: the swing-into-player push trace.
/// </summary>
public sealed partial class Door
{
private Rotation initialRotation;
private int side = 1;
private bool captured;
private void EnsureCaptured()
{
if ( captured )
return;
initialRotation = WorldRotation;
captured = true;
}
protected override void OnStart()
{
base.OnStart();
EnsureCaptured();
// NOTE: do NOT reset Locked here - MansionMapInstance.CreateDoor() sets it from the
// Hammer keyvalue before OnStart runs, and clobbering it unlocks every map-locked door.
}
public override void Use( Player user )
{
if ( !Networking.IsHost )
return;
if ( Locked )
return; // TODO (interactions): lockpicking UI.
if ( State is DoorState.Open or DoorState.Opening )
Close();
else
Open( user );
}
public void Open( Component user )
{
EnsureCaptured();
State = DoorState.Opening;
if ( user.IsValid() )
{
var local = WorldTransform.PointToLocal( user.WorldPosition );
side = local.y > 0 ? -1 : 1;
}
// Swing runs host-only (OnFixedUpdate), so broadcast the SFX or only the host hears it.
SoundExtensions.BroadcastPlay( "sounds/doors/dooropen.sound", WorldPosition );
}
public void Close()
{
EnsureCaptured();
State = DoorState.Closing;
}
protected override void OnFixedUpdate()
{
if ( !Networking.IsHost )
return;
if ( State is DoorState.Open or DoorState.Closed )
return;
const float time = 0.25f;
const float angle = 90f;
var state = (int)State;
var yaw = initialRotation.Yaw() + Math.Max( state, 0 ) * angle * side;
var target = initialRotation.Angles().WithYaw( yaw ).ToRotation();
if ( WorldRotation.Distance( target ).AlmostEqual( 0, 1f ) )
{
State = State == DoorState.Opening ? DoorState.Open : DoorState.Closed;
WorldRotation = target;
if ( State == DoorState.Closed )
SoundExtensions.BroadcastPlay( "sounds/doors/doorclose.sound", WorldPosition );
OccupyCells();
return;
}
WorldRotation = Rotation.Lerp( WorldRotation, target, 1f / time * Time.Delta );
}
/// <summary>
/// Re-tag the grid cells in the doorway as "door" so NPC A* heavily avoids routing through doors (a cost
/// malus, not impassable). Re-runs whenever the door settles since its rotation - and thus which cells the
/// doorway covers - changes. Scene-System port of legacy <c>Door.OccupyCells</c>.
/// </summary>
public void OccupyCells()
{
foreach ( var grid in GridAStar.Grid.Grids.Values )
{
if ( grid is null || !grid.IsInsideBounds( WorldPosition ) )
continue;
// Clear our previous door tags + occupancy in a wide area (the door may have rotated/opened).
for ( var x = -16; x < 16; x++ )
for ( var y = -16; y < 16; y++ )
{
var checkPos = WorldPosition + WorldRotation.Forward * 5f * x + WorldRotation.Right * 5f * y + Vector3.Up * 5f;
var prev = grid.GetCell( checkPos );
if ( prev is null )
continue;
prev.Tags.Remove( "door" );
if ( prev.OccupyingEntity == this )
prev.RemoveOccupant();
}
// Tag the doorway strip as "door" - a HIGH A* cost (NPCs avoid routing through doors) but NOT
// impassable. We deliberately do NOT SetOccupant the cells: occupancy blocks LineOfSight / path
// simplification through the doorway and (since OccupyCells re-runs as the door swings without
// clearing it) accumulated stale occupancy that kept NPCs/Doob from pathing through doorways.
for ( var x = 0; x < 15; x++ )
for ( var y = -2; y < 3; y++ )
{
var checkPos = WorldPosition + WorldRotation.Forward * 5f * x + WorldRotation.Right * 5f * y + Vector3.Up * 5f;
grid.GetCell( checkPos )?.Tags.Add( "door" );
}
}
if ( MansionGame.Instance?.CurrentLevel is not null )
foreach ( var monster in MansionGame.Instance.CurrentLevel.Monsters.ToList() )
monster.RecalculatePath();
}
}