Inventory/InventoryItem.cs
using Sandbox;
using Sandbox.Citizen;
using System.Linq;

public enum ItemType
{
	Primary,
	Secondary,
	Utility
}

public sealed class InventoryItem : Component
{
	/// <summary>Which inventory slot category this item can be placed in.</summary>
	[Property] public ItemType Type { get; set; }

	/// <summary>Icon material shown for this item in the inventory HUD.</summary>
	[Property] public string ItemIconPath { get; set; } = "/materials/default.webp";

	/// <summary>Citizen hold pose used while this item is in hand (desktop bodies and proxies).</summary>
	[Property] public CitizenAnimationHelper.HoldTypes HoldType { get; private set; }

	/// <summary>Which hand the citizen holds this item in.</summary>
	[Property] public CitizenAnimationHelper.Hand Handedness { get; private set; }
	[Property] public Vector3 RightHandPosition { get; private set; }

	[Sync]
	public GameObject SlotObject { get; private set; }

	[Sync, Change( nameof( OnIsStowedChanged ) )]
	public bool IsStowed { get; set; }

	[Sync, Change( nameof( OnIsPhysicsEnabledChanged ) )]
	public bool IsPhysicsEnabled { get; set; } = true;

	public InventorySlot Slot => SlotObject?.Components.Get<InventorySlot>();

	/// <summary>True while any grip on this item is still held by a hand.</summary>
	public bool IsHeld => GameObject.Components.GetAll<GrabPoint>( FindMode.InDescendants ).Any( g => g.IsGrabbed );

	protected override void OnStart()
	{
		OnIsPhysicsEnabledChanged( IsPhysicsEnabled, IsPhysicsEnabled );

		// An item can spawn in already stowed (initial loadout, or a proxy of someone
		// else's carried item), so apply that state once instead of waiting for a change.
		if ( IsStowed )
			OnIsStowedChanged( false, true );
	}

	public void AssignSlot( GameObject slotObject )
	{
		SlotObject = slotObject;
	}

	public void ClearSlot()
	{
		var slot = Slot;
		if ( slot is not null && slot.StoredItemObject == GameObject )
			slot.ClearStoredItem();

		SlotObject = null;
		IsStowed = false;
	}

	/// <summary>
	/// Detaches this item from the player and restores its world model. Physics is only
	/// re-enabled once nothing holds it; <see cref="GrabPoint.Drop"/> does that for the
	/// last hand to let go, so a two-handed grip keeps driving the item.
	/// </summary>
	public void DropToWorld()
	{
		ClearSlot();

		GameObject.SetParent( null, true );
		SetWorldModelVisible( true );

		if ( !IsHeld )
			IsPhysicsEnabled = true;
	}

	/// <summary>
	/// Shows or hides the item's world model for the local client only; rendering isn't
	/// networked, so each machine decides what its own player gets to see.
	/// </summary>
	public void SetWorldModelVisible( bool visible )
	{
		var modelRenderer = GetComponent<ModelRenderer>();
		if ( modelRenderer?.SceneObject is not null )
			modelRenderer.SceneObject.RenderingEnabled = visible;
	}

	private void OnIsStowedChanged( bool wasStowed, bool isStowed )
	{
		if ( isStowed )
		{
			// A stowed item is only drawn for the VR player carrying it: desktop owners see a
			// viewmodel instead, and nobody else should see items pinned to another body.
			SetWorldModelVisible( !IsProxy && Game.IsRunningInVR );
			return;
		}

		// Out of the slot: other players see it in the carrier's hands again, while a local
		// desktop player stays on their viewmodel until the item is dropped into the world.
		SetWorldModelVisible( IsProxy || Game.IsRunningInVR );
	}

	public void Stow()
	{
		IsStowed = true;
	}

	public void Unstow()
	{
		IsStowed = false;
	}

	private void OnIsPhysicsEnabledChanged( bool oldValue, bool newValue )
	{
		var modelCollider = GetComponent<ModelCollider>( includeDisabled: true );
		modelCollider?.Enabled = newValue;

		var rigidBody = GetComponent<Rigidbody>( includeDisabled: true );
		rigidBody?.Enabled = newValue;
	}
}