Map/Door.Runtime.cs

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.

NetworkingFile Access
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();
	}
}