Inventory/PlayerInventory.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
public sealed class PlayerInventory : Component
{
private List<InventorySlot> _slots;
public IReadOnlyList<InventorySlot> Slots => _slots;
[Property] private InventoryPanel InventoryPanel { get; set; }
/// <summary>Weapon prefabs to pick from at random for the primary slot on spawn.</summary>
[Property, Group( "Loadout" )] public List<GameObject> InitialPrimaryWeapon;
/// <summary>Weapon prefabs to pick from at random for the secondary slot on spawn.</summary>
[Property, Group( "Loadout" )] public List<GameObject> InitialSecondaryWeapon;
/// <summary>Fixed items to spawn into inventory. Primary and secondary entries are skipped when a random weapon of that type already spawned.</summary>
[Property, Group( "Loadout" )] public List<GameObject> InitialLoadout;
protected override void OnStart()
{
_slots = [..GetComponentsInChildren<InventorySlot>().OrderBy(slot => slot.SlotIndex)];
foreach ( var group in _slots.GroupBy( s => s.SlotIndex ).Where( g => g.Count() > 1 ) )
{
var names = string.Join( ", ", group.Select( s => s.GameObject.Name ) );
throw new InvalidOperationException( $"Duplicate inventory slot index {group.Key} on: {names}" );
}
SetupInventoryPanel();
SetupInitialLoadout();
}
private void SetupInventoryPanel()
{
if ( IsProxy )
{
InventoryPanel.Enabled = false;
return;
}
InventoryPanel.Slots = _slots;
InventoryPanel.Enabled = true;
}
private void SetupInitialLoadout()
{
if ( IsProxy )
return;
var hasPrimary = TryStowRandomFrom( InitialPrimaryWeapon );
var hasSecondary = TryStowRandomFrom( InitialSecondaryWeapon );
if ( InitialLoadout is null )
return;
foreach ( var itemPrefab in InitialLoadout )
{
var type = itemPrefab.Components.Get<InventoryItem>()?.Type;
if ( hasPrimary && type == ItemType.Primary )
continue;
if ( hasSecondary && type == ItemType.Secondary )
continue;
TryStowLoadoutItem( itemPrefab );
}
}
private bool TryStowRandomFrom( List<GameObject> prefabs )
{
if ( prefabs is not { Count: > 0 } )
return false;
var prefab = prefabs[Game.Random.Int( 0, prefabs.Count - 1 )];
return TryStowLoadoutItem( prefab );
}
private bool TryStowLoadoutItem( GameObject itemPrefab )
{
if ( !itemPrefab.IsValid() )
return false;
var gameObject = itemPrefab.Clone();
var item = gameObject.GetComponent<InventoryItem>();
if ( !item.IsValid() )
{
Log.Error( $"Initial loadout item {itemPrefab.Name} does not have an InventoryItem component" );
return false;
}
if ( !TryStow( item ) )
{
Log.Error( $"No free inventory slot for initial loadout item {itemPrefab.Name} of type {item.Type}" );
return false;
}
gameObject.NetworkSpawn();
return true;
}
/// <summary>
/// Stows an item on the body, used by every platform. Prefers the slot the item already
/// reserved (desktop pickups), otherwise the nearest free slot to <paramref name="nearPosition"/>
/// (a VR hand), otherwise any free slot of the right type.
/// </summary>
public bool TryStow( InventoryItem item, Vector3? nearPosition = null )
{
if ( !item.IsValid() )
return false;
var slot = ResolveStowSlot( item, nearPosition );
return slot is not null && slot.TryStow( item );
}
private InventorySlot ResolveStowSlot( InventoryItem item, Vector3? nearPosition )
{
var reservedSlot = item.Slot;
if ( reservedSlot is not null && reservedSlot.CanAccept( item ) )
return reservedSlot;
if ( nearPosition.HasValue )
return FindNearestFreeSlot( item.Type, nearPosition.Value, float.MaxValue );
return TryGetFreeSlot( item.Type );
}
public InventorySlot TryGetFreeSlot( ItemType type )
{
return _slots.FirstOrDefault( s => s.SlotType == type && s.IsEmpty );
}
public InventorySlot GetSlot( InventorySlotIndex index )
{
return _slots.FirstOrDefault( s => s.SlotIndex == index );
}
public InventorySlot FindNearestFreeSlot( ItemType type, Vector3 position, float maxDistance )
{
InventorySlot nearest = null;
float nearestDistance = maxDistance;
foreach ( var slot in _slots )
{
if ( slot.SlotType != type || !slot.IsEmpty )
continue;
float distance = slot.WorldPosition.Distance( position );
if ( distance > slot.Radius || distance > nearestDistance )
continue;
nearestDistance = distance;
nearest = slot;
}
return nearest;
}
public InventoryItem GetHeldItemOfType( ItemType type )
{
foreach ( var slot in _slots )
{
var item = slot.StoredItem;
if ( item is null || item.Type != type || item.IsStowed )
continue;
if ( item.IsHeld )
return item;
}
foreach ( var grabPoint in Scene.GetAllComponents<GrabPoint>() )
{
if ( !grabPoint.IsGrabbed )
continue;
var item = grabPoint.Components.GetInAncestorsOrSelf<InventoryItem>();
if ( item is not null && item.Type == type && !item.IsStowed )
return item;
}
return null;
}
public void DropAllItems()
{
// Both grabbers no-op when they aren't holding anything, so the inactive
// platform's grabber doesn't need to be filtered out here.
foreach ( var handGrab in GameObject.Components.GetAll<HandGrab>( FindMode.InDescendants ) )
handGrab.ForceDropHeld();
GameObject.Components.Get<DesktopGrab>( FindMode.InDescendants )?.ForceDropHeld();
foreach ( var slot in _slots )
{
var item = slot.StoredItem;
if ( item is null || !item.IsStowed )
continue;
item.DropToWorld();
}
}
}