InvestmentTable.cs
using Sandbox;
using SWB.Shared;

// The investment table at the back of the hideout. Put this on a GameObject
// with a trigger Collider covering the table; walk in and hold Use to open
// it, hold Use again (or walk away) to close.
//
// Deliberately the same interaction shape as the Vendor and the job board,
// so there is exactly one thing to learn in the hideout: stand near it,
// hold E.
//
// All the money lives on PlayerLoadout - this is just the door to it.
public sealed class InvestmentTable : Component, Component.ITriggerListener, IInteractable
{
	[Property]
	public float HoldDuration { get; set; } = 0.4f;

	bool _playerInRange = false;
	float _holdProgress = 0f;
	bool _wasHolding = false;

	public bool IsOpen { get; private set; }

	// Drives the on-screen prompt - see IInteractable.
	public bool PlayerInRange => _playerInRange;
	public float HoldFraction => HoldDuration <= 0f ? 0f : _holdProgress / HoldDuration;
	public string PromptText => "Hold E to manage investments";
	public bool SuppressPrompt => IsOpen;

	public void OnTriggerEnter( Collider other )
	{
		if ( other.GameObject.Root.Tags.Has( "player" ) )
			_playerInRange = true;
	}

	public void OnTriggerExit( Collider other )
	{
		if ( !other.GameObject.Root.Tags.Has( "player" ) )
			return;

		_playerInRange = false;
		_holdProgress = 0f;
		Close();
	}

	protected override void OnUpdate()
	{
		if ( !_playerInRange )
			return;

		var holding = Input.Down( InputButtonHelper.Use );

		if ( holding )
		{
			_holdProgress += Time.Delta;

			// Only toggle on the hold completing, then wait for the button
			// to come back up - otherwise one long press would flap the
			// menu open and shut every HoldDuration seconds.
			if ( _holdProgress >= HoldDuration && !_wasHolding )
			{
				_wasHolding = true;
				IsOpen = !IsOpen;
			}
		}
		else
		{
			_holdProgress = 0f;
			_wasHolding = false;
		}
	}

	public void Close()
	{
		IsOpen = false;
	}
}