Player/Player.Use.cs

Player interaction component. Updates what usable the player is looking at, manages local hold-to-use timing, sends a host RPC to validate and perform uses, handles locked usables and upgrade effects.

NetworkingNative Interop
using Sandbox;

namespace BrickJam;

/// <summary>
/// Player interaction flow. Scene-System port of the legacy <c>Player.Use</c>.
///
/// The legacy <c>InteractionRequest</c>/<c>[Net]</c> bookkeeping is replaced with: the owning client
/// traces for a <see cref="LegacyUsableComponent"/>, runs the hold timer locally, then fires an
/// <c>[Rpc.Host]</c> that re-traces from the player's synced eye on the host and calls
/// <see cref="LegacyUsableComponent.Use"/> authoritatively (so no non-networked component reference
/// has to cross the wire).
///
/// Locked usables route to the lockpicking minigame (see <see cref="OpenLockpicker"/> /
/// <see cref="FinishLockpick"/>); "Lock Breaker" smashes them instantly. DEFERRED: the radial-progress
/// HUD (the <see cref="InteractionProgress"/> value is exposed for it).
/// </summary>
public sealed partial class Player
{
	[Property, InputAction] public string UseButton { get; set; } = "use";

	public float UseRange => 100f;

	/// <summary>The usable the player is currently looking at (owner-side).</summary>
	public LegacyUsableComponent HoveredUsable { get; private set; }
	public Vector3 HoveredPoint { get; private set; }

	public bool IsInteracting => interacting.IsValid();
	public float InteractionProgress => (IsInteracting && interactionDuration > 0f)
		? 1f - ((float)interactionLeft / interactionDuration).Clamp( 0f, 1f )
		: 0f;

	private LegacyUsableComponent interacting;
	private TimeUntil interactionLeft;
	private float interactionDuration;

	private void UpdateUse()
	{
		UpdateHovered();

		var holding = !CommandsLocked && Input.Down( UseButton );

		if ( !holding )
		{
			CancelInteraction();
			return;
		}

		// Currently mid-interaction: cancel if we looked away or it became unusable.
		if ( interacting.IsValid() )
		{
			if ( interacting != HoveredUsable || !interacting.CanUse )
			{
				CancelInteraction();
				return;
			}

			if ( interactionLeft )
			{
				CancelInteraction();
				RequestUse();
			}

			return;
		}

		// Not interacting yet: start on a fresh press over a usable target.
		if ( Input.Pressed( UseButton ) && HoveredUsable.IsValid() && HoveredUsable.CanUse )
			BeginInteraction( HoveredUsable );
	}

	private void UpdateHovered()
	{
		var trace = Scene.Trace
			.Ray( EyePosition, EyePosition + InputRotation.Forward * UseRange )
			.WithTag( "usable" )
			.IgnoreGameObjectHierarchy( GameObject )
			.Radius( 2f )
			.Run();

		HoveredUsable = trace.GameObject?.Components.Get<LegacyUsableComponent>();
		HoveredPoint = trace.Hit ? trace.HitPosition : Vector3.Zero;
	}

	private void BeginInteraction( LegacyUsableComponent usable )
	{
		interacting = usable;
		interactionDuration = usable.InteractionDuration / (HasUpgrade( "Faster Use" ) ? 3f : 1f);
		interactionLeft = interactionDuration;
	}

	private void CancelInteraction()
	{
		interacting = null;
		interactionDuration = 0f;
	}

	/// <summary>
	/// Runs on the host: re-trace from this player's (synced) eye and apply the interaction
	/// authoritatively.
	/// </summary>
	[Rpc.Host]
	private void RequestUse()
	{
		var trace = Scene.Trace
			.Ray( EyePosition, EyePosition + InputRotation.Forward * UseRange )
			.WithTag( "usable" )
			.IgnoreGameObjectHierarchy( GameObject )
			.Radius( 2f )
			.Run();

		var usable = trace.GameObject?.Components.Get<LegacyUsableComponent>();
		if ( !usable.IsValid() || !usable.CanUse )
			return;

		usable.User = this;

		if ( usable.Locked )
		{
			// "Lock Breaker" upgrade smashes locks instantly; otherwise open the lockpicking minigame
			// on the owning client.
			if ( HasUpgrade( "Lock Breaker" ) )
			{
				usable.Locked = false;
				if ( usable.Components.TryGet<LockedComponent>( out var locked ) )
					locked.Unlock();

				SoundExtensions.BroadcastPlay( "sounds/lockpicking/lockfall.sound", usable.WorldPosition );
			}
			else
			{
				OpenLockpicker();
			}

			return;
		}

		if ( !usable.CheckUpgrades( this ) )
			return;

		usable.Use( this );
	}
}