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.
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 );
}
}