DesktopGrab.cs
using Sandbox;
using Sandbox.Citizen;
using Sandbox.Mapping;
using System.Linq;
public sealed class DesktopGrab : Component
{
[RequireComponent] private PlayerViewModel PlayerViewModel { get; set; }
[Property] private CameraComponent Camera { get; set; }
[Property] public float InteractionDistance { get; set; } = 150f;
private Player Player { get; set; }
private PlayerInventory Inventory { get; set; }
private PlayerAnimationBroadcast AnimBroadcaster { get; set; }
private GrabPoint _activeGrabPoint;
private GrabPoint _secondaryGrabPoint;
private bool _throwWindupActive;
private bool _throwRequested;
private float _throwReadyAt;
private bool _throwAnimActive;
private float _throwReleaseAt;
private static readonly string[] SlotActions = ["slot1", "slot2", "slot3", "slot4", "slot5"];
protected override void OnStart()
{
Player = GetComponentInParent<Player>();
Inventory = Player?.Inventory;
AnimBroadcaster = Player?.Avatar?.GetComponent<PlayerAnimationBroadcast>();
var initialInventorySlot = Inventory?.Slots.FirstOrDefault( slot => slot.StoredItem.IsValid() );
if ( initialInventorySlot.IsValid() )
{
EquipFromSlot( initialInventorySlot );
}
}
protected override void OnUpdate()
{
if ( IsProxy ) return;
// The held item may have been destroyed out from under us (e.g. a grenade
// exploding in hand) — clean up the stale state before doing anything else.
if ( _activeGrabPoint is not null && !_activeGrabPoint.IsValid() )
ClearHeldState();
if ( _throwAnimActive && Time.Now >= _throwReleaseAt )
{
ReleaseToWorld( GetThrowVelocity() );
ClearThrowAnimState();
return;
}
HandleSlotInput();
if ( Input.Pressed( "use" ) )
HandleUseInput();
if ( Input.Pressed( "drop" ) )
ForceDropHeld();
if ( _activeGrabPoint is not null && !_throwAnimActive )
{
var throwable = _activeGrabPoint.ItemRoot.GetComponent<Throwable>();
var isThrowable = throwable is not null;
if ( Input.Pressed( "attack1" ) && isThrowable )
{
PlayerViewModel.Instance.PlayCharge();
StartThrowWindup( throwable.ThrowDelay );
}
if ( Input.Released( "attack1" ) && _throwWindupActive )
_throwRequested = true;
if ( _throwWindupActive && _throwRequested && Time.Now >= _throwReadyAt )
BeginThrowAnimation( throwable );
_activeGrabPoint.SetTriggerState( Input.Down( "attack1" ) );
if ( Input.Pressed( "reload" ) )
_activeGrabPoint.Reload();
}
}
private void HandleUseInput()
{
var gameObject = GetLookTrace();
if ( !gameObject.IsValid() )
return;
var door = gameObject.GetComponent<Door>();
if ( door.IsValid() )
door.Toggle( gameObject );
else
TryGrabObject( gameObject );
}
private void HandleSlotInput()
{
for ( int i = 0; i < SlotActions.Length; i++ )
{
if ( !Input.Pressed( SlotActions[i] ) )
continue;
HandleSlotKey( (InventorySlotIndex)i );
}
}
private void HandleSlotKey( InventorySlotIndex slotIndex )
{
if ( Inventory is null )
return;
var slot = Inventory.GetSlot( slotIndex );
if ( slot is null )
return;
var slotItem = slot.StoredItem;
if ( slotItem is null )
return;
var currentItem = _activeGrabPoint?.Components.GetInAncestorsOrSelf<InventoryItem>();
bool pressedHeldItemsSlot = currentItem is not null && currentItem.SlotObject == slot.GameObject;
if ( !HolsterCurrentItem() )
return;
// Tapping the held item's own slot key just holsters it
if ( pressedHeldItemsSlot )
return;
EquipFromSlot( slot );
}
private GameObject GetLookTrace()
{
var ray = Camera.ScreenNormalToRay( new Vector2( 0.5f, 0.5f ) );
var trace = Scene.Trace.Ray( ray, InteractionDistance )
.UsePhysicsWorld()
.Run();
if ( !trace.Hit || trace.GameObject is null )
return null;
return trace.GameObject;
}
private void TryGrabObject( GameObject gameObject )
{
var itemRoot = gameObject.Root;
var inventoryItem = itemRoot.Components.Get<InventoryItem>();
if ( inventoryItem is null || Inventory is null || inventoryItem.IsStowed )
return;
var freeSlot = Inventory.TryGetFreeSlot( inventoryItem.Type );
if ( freeSlot is null )
return;
if ( !CanEquip( inventoryItem ) || !HolsterCurrentItem() )
return;
// Equip first: Grab() takes network ownership of the item, and only the
// owner may write the item's [Sync] slot properties in Reserve().
EquipToHand( inventoryItem );
freeSlot.Reserve( inventoryItem );
}
private void EquipFromSlot( InventorySlot slot )
{
var inventoryItem = slot.StoredItem;
if ( inventoryItem is null || !CanEquip( inventoryItem ) )
return;
EquipToHand( inventoryItem );
}
/// <summary>True when the item has a primary grip that no one is holding yet.</summary>
private static bool CanEquip( InventoryItem item )
{
var primaryGrip = FindGrip( item, secondary: false );
return primaryGrip is not null && !primaryGrip.IsGrabbed;
}
private static GrabPoint FindGrip( InventoryItem item, bool secondary )
{
return item.GameObject.Components
.GetAll<GrabPoint>( FindMode.InDescendants )
.FirstOrDefault( g => g.IsSecondaryGrip == secondary );
}
private void EquipToHand( InventoryItem item )
{
_activeGrabPoint = FindGrip( item, secondary: false );
_secondaryGrabPoint = FindGrip( item, secondary: true );
// Unstows the item and takes network ownership of it
_activeGrabPoint.Grab( false );
var weaponRoot = item.GameObject;
var handRAttachment = Player.Avatar.GetAttachmentObject( "hold_r" );
weaponRoot.SetParent( handRAttachment, false );
weaponRoot.LocalRotation = Rotation.FromRoll( 0 );
weaponRoot.LocalPosition = item.RightHandPosition;
AnimBroadcaster.HoldType = item.HoldType;
AnimBroadcaster.Handedness = item.Handedness;
AnimBroadcaster.SecondaryGrip = _secondaryGrabPoint?.GameObject;
// Desktop holds show the viewmodel instead of the world model
item.SetWorldModelVisible( false );
var viewModel = _activeGrabPoint.GetComponentInParent<ViewModel>();
if ( viewModel is not null )
PlayerViewModel.Show( viewModel );
}
/// <summary>
/// Puts the held item back in its inventory slot, keeping it on the body.
/// Returns true if the hand ended up empty.
/// </summary>
private bool HolsterCurrentItem()
{
if ( _activeGrabPoint is null )
return true;
var inventoryItem = _activeGrabPoint.Components.GetInAncestorsOrSelf<InventoryItem>();
if ( inventoryItem is null || Inventory is null || !Inventory.TryStow( inventoryItem ) )
return false;
_activeGrabPoint.Drop();
ClearHeldState();
return true;
}
/// <summary>Drops the held item into the world, ignoring its inventory slot.</summary>
public void ForceDropHeld()
{
ReleaseToWorld( null );
}
/// <summary>
/// Throws the held item along the camera direction using its Throwable settings.
/// </summary>
public void ThrowObject()
{
if ( _activeGrabPoint is null )
return;
var throwable = _activeGrabPoint.ItemRoot.GetComponent<Throwable>();
if ( throwable is null )
return;
BeginThrowAnimation( throwable );
}
/// <summary>
/// Plays the throw viewmodel animation, then releases the item after ThrowAnimDuration.
/// </summary>
private void BeginThrowAnimation( Throwable throwable )
{
if ( _activeGrabPoint is null )
return;
ClearThrowWindup();
PlayerViewModel.Instance?.PlayAttack();
_throwAnimActive = true;
_throwReleaseAt = Time.Now + throwable.ThrowAnimDuration;
}
private Vector3? GetThrowVelocity()
{
if ( _activeGrabPoint is null )
return null;
var throwable = _activeGrabPoint.ItemRoot.GetComponent<Throwable>();
if ( throwable is null )
return null;
var ray = Camera.ScreenNormalToRay( new Vector2( 0.5f, 0.5f ) );
var direction = (ray.Forward + Vector3.Up * throwable.UpwardBoost).Normal;
return direction * throwable.ThrowForce;
}
private void ReleaseToWorld( Vector3? throwVelocity )
{
if ( _activeGrabPoint is null )
return;
_activeGrabPoint.ItemRoot.Components.Get<InventoryItem>()?.DropToWorld();
_activeGrabPoint.Drop( throwVelocity );
ClearHeldState();
}
/// <summary>
/// Forgets the currently held item and resets viewmodel/animation state.
/// Safe to call when the item has already been destroyed.
/// </summary>
private void ClearHeldState()
{
PlayerViewModel.Hide();
_activeGrabPoint = null;
_secondaryGrabPoint = null;
ClearThrowWindup();
ClearThrowAnimState();
AnimBroadcaster.HoldType = CitizenAnimationHelper.HoldTypes.None;
AnimBroadcaster.SecondaryGrip = null;
}
private void StartThrowWindup( float delay )
{
_throwWindupActive = true;
_throwRequested = false;
_throwReadyAt = Time.Now + delay;
}
private void ClearThrowWindup()
{
_throwWindupActive = false;
_throwRequested = false;
_throwReadyAt = 0f;
}
private void ClearThrowAnimState()
{
_throwAnimActive = false;
_throwReleaseAt = 0f;
}
}