Inventory/InventoryContainer.cs
using System;
using System.Collections.Immutable;
using System.Text.Json.Serialization;
using Clover.Data;
using Clover.Persistence;
using Clover.Player;
using Sandbox.Diagnostics;
namespace Clover.Inventory;
public sealed partial class InventoryContainer
{
public Guid Id { get; set; } = Guid.NewGuid();
[JsonIgnore] public GameObject Owner { get; set; }
[JsonIgnore] public PlayerCharacter Player => Owner.GetComponent<PlayerCharacter>();
[JsonInclude] public int MaxItems { get; set; } = 20;
[JsonInclude] public List<InventorySlot> Slots = new();
// public delegate void InventoryChangedEventHandler();
// [Property] public event InventoryChangedEventHandler InventoryChanged;
public InventoryContainer()
{
Log.Info( "Creating inventory with default slots" );
}
public InventoryContainer( int slots )
{
Log.Info( $"Creating inventory with {slots} slots" );
MaxItems = slots;
}
public InventorySlot GetSlotByIndex( int index )
{
return Slots.FirstOrDefault( slot => slot.Index == index );
}
public struct InventoryContainerEntry
{
public int Index;
public InventorySlot Slot;
public bool HasSlot => Slot != null;
}
public IEnumerable<InventoryContainerEntry> GetEnumerator()
{
for ( var i = 0; i < MaxItems; i++ )
{
yield return new InventoryContainerEntry { Index = i, Slot = GetSlotByIndex( i ) };
}
}
public List<InventoryContainerEntry> QuerySlots()
{
var entries = new List<InventoryContainerEntry>();
for ( var i = 0; i < MaxItems; i++ )
{
entries.Add( new InventoryContainerEntry { Index = i, Slot = GetSlotByIndex( i ) } );
}
return entries;
}
/// <summary>
/// Returns the index of the first empty slot in the inventory.
/// </summary>
/// <returns>The index of the first empty slot, or -1 if no empty slot is found.</returns>
public int GetFirstFreeEmptyIndex()
{
foreach ( var slot in GetEnumerator() )
{
if ( slot.Slot == null )
{
return slot.Index;
}
}
return -1;
}
public IEnumerable<InventorySlot> GetUsedSlots()
{
return Slots.ToArray();
}
[JsonIgnore] public int FreeSlots => MaxItems - Slots.Count;
[JsonIgnore] public bool HasFreeSlot => FreeSlots > 0;
public void RemoveSlots()
{
Log.Trace( "Removing slots" );
Slots.Clear();
}
public void ImportSlot( InventorySlot slot )
{
if ( Slots.Count >= MaxItems )
{
// throw new System.Exception( "Inventory is full." );
throw new InventoryFullException( "Inventory is full." );
}
slot.InventoryContainer = this;
// if the slot has no index or the index is already taken, assign a new index
if ( slot.Index == -1 || GetSlotByIndex( slot.Index ) != null )
{
Log.Warning( "Imported slot has no index or index is already taken, assigning new index" );
slot.Index = GetFirstFreeEmptyIndex();
}
Slots.Add( slot );
RecalculateIndexes();
}
/// <summary>
/// Add an item to the inventory.
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
/// <exception cref="InventoryFullException"></exception>
public InventorySlot AddItem( PersistentItem item, bool merge = false )
{
InventorySlot slot;
if ( !merge )
{
var index = GetFirstFreeEmptyIndex();
if ( index == -1 )
{
throw new InventoryFullException( "Inventory is full." );
}
slot = new InventorySlot( this );
slot.Index = index;
slot.SetItem( item );
Slots.Add( slot );
}
else
{
slot = GetSlotWithItem( item.ItemData );
if ( slot == null || !item.ItemData.IsStackable )
{
// slot = new InventorySlot<PersistentItem>( this );
// slot.SetItem( item );
// Slots.Add( slot );
return AddItem( item, false );
}
else
{
if ( slot.Amount + 1 > item.ItemData.StackSize )
{
throw new Exception( "Cannot merge item, stack size exceeded" );
}
slot.SetAmount( slot.Amount + 1 );
}
}
RecalculateIndexes();
OnChange();
return slot;
}
/// <summary>
/// Add an item to the inventory at a specific index.
/// </summary>
/// <param name="item"></param>
/// <param name="index"></param>
/// <exception cref="InventoryFullException"></exception>
/// <exception cref="SlotTakenException"></exception>
/// <exception cref="Exception"></exception>
public void AddItemToIndex( PersistentItem item, int index = -1 )
{
if ( index == -1 )
{
index = GetFirstFreeEmptyIndex();
if ( index == -1 )
{
throw new InventoryFullException( "Inventory is full." );
}
}
if ( GetSlotByIndex( index ) != null )
{
// TODO: merge items if possible
throw new SlotTakenException( $"Slot {index} is already taken." );
}
if ( item.ItemData == null ) throw new Exception( "ItemData is null" );
var slot = new InventorySlot( this );
slot.Index = index;
slot.SetItem( item );
Slots.Add( slot );
RecalculateIndexes();
// OnInventoryChanged?.Invoke();
OnChange();
}
/// <summary>
/// Recalculate the indexes of all slots in the inventory, keeping old indexes
/// </summary>
public void RecalculateIndexes()
{
var index = 0;
foreach ( var slot in GetUsedSlots() )
{
// slot.Index = index++;
if ( slot.Index == -1 )
{
Log.Trace( "Slot has no index, assigning new index" );
slot.Index = index++;
}
else
{
Log.Trace( $"Slot has index {slot.Index}, keeping index" );
index = slot.Index + 1;
}
}
// OnInventoryChanged?.Invoke( this );
}
/// <summary>
/// Reset the index for all slots in the inventory based on their location in the list
/// </summary>
public void ResetIndexes()
{
var index = 0;
foreach ( var slot in GetUsedSlots() )
{
slot.Index = index++;
}
// OnInventoryChanged?.Invoke( this );
}
/* public void SortSlots()
{
Slots.Sort( SlotSortingFunc );
} */
/* public void SortByType()
{
// MergeAllSlots();
// XLog.Info( "Inventory", $"Sorting inventory {Id} by type" );
Slots.Sort( ( a, b ) => string.Compare( a.ItemType, b.ItemType, StringComparison.Ordinal ) );
// RecalculateIndexes();
ResetIndexes();
// OnInventoryChanged?.Invoke( this );
// SyncToPlayerList();
} */
public void SortByName()
{
// MergeAllSlots();
// XLog.Info( "Inventory", $"Sorting inventory {Id} by name" );
Slots.Sort( ( a, b ) => string.Compare( a.GetName(), b.GetName(), StringComparison.Ordinal ) );
// RecalculateIndexes();
ResetIndexes();
// OnInventoryChanged?.Invoke( this );
// SyncToPlayerList();
}
public void SortByIndex()
{
// MergeAllSlots();
// XLog.Info( "Inventory", $"Sorting inventory {Id} by index" );
Slots.Sort( ( a, b ) => a.Index.CompareTo( b.Index ) );
// RecalculateIndexes();
ResetIndexes();
// OnInventoryChanged?.Invoke( this );
// SyncToPlayerList();
}
private static int SlotSortingFunc( InventorySlot a, InventorySlot b )
{
var itemA = a.GetItem();
var itemB = b.GetItem();
if ( itemA == null && itemB == null )
{
return 0;
}
if ( itemA == null )
{
return 1;
}
if ( itemB == null )
{
return -1;
}
return string.Compare( itemA.GetName(), itemB.GetName(), System.StringComparison.Ordinal );
}
/* public override void _Process( double delta )
{
if ( Input.IsActionJustPressed( "UseTool" ) )
{
/* if ( CurrentCarriable != null )
{
CurrentCarriable.OnUse( Player );
} *
if ( Player.HasEquippedItem( PlayerController.EquipSlot.Tool ) )
{
Player.GetEquippedItem<Carriable.BaseCarriable>( PlayerController.EquipSlot.Tool ).OnUse( Player );
}
/*var testItem = new InventoryItem( this );
testItem.ItemDataPath = "res://items/furniture/polka_chair/polka_chair.tres";
testItem.DTO = new BaseDTO
{
ItemDataPath = "res://items/furniture/polka_chair/polka_chair.tres",
};
var slot = GetFirstFreeSlot();
if ( slot == null )
{
throw new System.Exception( "No free slots." );
return;
}
slot.SetItem( testItem );*
}
else if ( Input.IsActionJustPressed( "Drop" ) )
{
/*var item = Items.FirstOrDefault();
if ( item != null )
{
DropItem( item );
}*
}
} */
public void OnChange()
{
Game.ActiveScene.RunEvent<IInventoryEvent>( x => x.OnInventoryChanged( this ) );
}
public void RemoveSlot( InventorySlot inventorySlot )
{
Slots.Remove( inventorySlot );
RecalculateIndexes();
OnChange();
}
public void RemoveSlot( int index )
{
var slot = GetSlotByIndex( index );
if ( slot == null )
{
throw new Exception( $"Slot {index} not found." );
}
RemoveSlot( slot );
}
public bool MoveSlot( int slotIndexFrom, int slotIndexTo )
{
if ( slotIndexFrom < 0 || slotIndexFrom >= MaxItems )
{
// Log.Error( $"SlotIndexFrom {slotIndexFrom} is out of range" );
throw new ArgumentOutOfRangeException( $"Move: SlotIndexFrom {slotIndexFrom} is out of range" );
}
if ( slotIndexTo < 0 || slotIndexTo >= MaxItems )
{
// Log.Error( $"SlotIndexTo {slotIndexTo} is out of range" );
// return false;
throw new ArgumentOutOfRangeException( $"Move: SlotIndexTo {slotIndexTo} is out of range" );
}
/* if ( !AllowSlotMoving )
{
throw new Exception( "You cannot move items in this inventory" );
} */
var slotFrom = GetSlotByIndex( slotIndexFrom );
var slotTo = GetSlotByIndex( slotIndexTo );
if ( slotFrom == null )
{
// Log.Error( $"SlotFrom {slotIndexFrom} is null" );
// return false;
throw new Exception( $"Move: SlotFrom {slotIndexFrom} is null" );
}
if ( slotFrom == slotTo )
{
// throw new Exception( $"SlotFrom {slotIndexFrom} is the same as SlotTo {slotIndexTo}" );
return false; // don't throw an exception, just error silently
}
if ( slotTo != null )
{
if ( slotFrom.CanMergeWith( slotTo ) )
{
slotFrom.MergeWith( slotTo );
return true;
}
return SwapSlot( slotIndexFrom, slotIndexTo );
}
slotFrom.Index = slotIndexTo;
// Slots.Sort( ( a, b ) => a.Index.CompareTo( b.Index ) );
// SortByIndex();
RecalculateIndexes();
// FixEventRegistration();
// SyncToPlayerList();
// OnInventoryChanged?.Invoke( this );
OnChange();
return true;
}
public bool SwapSlot( int slotIndexFrom, int slotIndexTo )
{
if ( slotIndexFrom < 0 || slotIndexFrom >= MaxItems )
{
// Log.Error( $"SlotIndexFrom {slotIndexFrom} is out of range" );
// return false;
throw new ArgumentOutOfRangeException( $"Swap: SlotIndexFrom {slotIndexFrom} is out of range" );
}
if ( slotIndexTo < 0 || slotIndexTo >= MaxItems )
{
// Log.Error( $"SlotIndexTo {slotIndexTo} is out of range" );
// return false;
throw new ArgumentOutOfRangeException( $"Swap: SlotIndexTo {slotIndexTo} is out of range" );
}
/* if ( !AllowSlotMoving )
{
throw new Exception( "You cannot move items in this inventory" );
} */
var slotFrom = GetSlotByIndex( slotIndexFrom );
var slotTo = GetSlotByIndex( slotIndexTo );
if ( slotFrom == null )
{
// Log.Error( $"SlotFrom {slotIndexFrom} is null" );
// return false;
throw new Exception( $"Swap: SlotFrom {slotIndexFrom} is null" );
}
if ( slotTo == null )
{
// Log.Error( $"SlotTo {slotIndexTo} is null" );
// return false;
throw new Exception( $"Swap: SlotTo {slotIndexTo} is null" );
}
slotFrom.Index = slotIndexTo;
slotTo.Index = slotIndexFrom;
// Slots.Sort( ( a, b ) => a.Index.CompareTo( b.Index ) );
// SortByIndex();
RecalculateIndexes();
// SyncToPlayerList();
// OnInventoryChanged?.Invoke( this );
OnChange();
return true;
}
public void DeleteAll()
{
Slots.Clear();
// OnInventoryChanged?.Invoke();
OnChange();
}
public bool HasItem( ItemData item )
{
return Slots.Any( slot => slot.HasItem && slot.GetItem().ItemData.IsSameAs( item ) );
}
public bool HasItem( ItemData item, int quantity )
{
return Slots.Any( slot => slot.HasItem && slot.GetItem().ItemData.IsSameAs( item ) && slot.Amount >= quantity );
}
public InventorySlot GetSlotWithItem( ItemData item )
{
return Slots.FirstOrDefault( slot => slot.HasItem && slot.GetItem().ItemData.IsSameAs( item ) );
}
public InventorySlot GetSlotWithItem( ItemData item, int quantity )
{
return Slots.FirstOrDefault( slot =>
slot.HasItem && slot.GetItem().ItemData.IsSameAs( item ) && slot.Amount >= quantity );
}
public IEnumerable<InventorySlot> GetSlotsWithItem( ItemData item )
{
return Slots.Where( slot => slot.HasItem && slot.GetItem().ItemData.IsSameAs( item ) );
}
public void RemoveItem( ItemData item, int quantity )
{
var slot = GetSlotWithItem( item, quantity );
if ( slot == null )
{
throw new Exception( $"Item {item.Name} not found in inventory" );
}
slot.SetAmount( slot.Amount - quantity );
if ( slot.Amount <= 0 )
{
RemoveSlot( slot );
}
OnChange();
}
public bool CanFit( PersistentItem item )
{
if ( FreeSlots > 0 ) return true;
var slots = GetSlotsWithItem( item.ItemData ).ToList();
if ( !slots.Any() ) return false;
var slotWithStackSpaceLeft = slots.Where( s => s.Amount < s.PersistentItem.ItemData.StackSize ).ToList();
if ( slotWithStackSpaceLeft.Any() ) return true;
return false;
}
public bool CanFit( List<PersistentItem> results )
{
var freeSlots = FreeSlots;
var items = results.Count;
if ( freeSlots >= items )
{
return true;
}
foreach ( var result in results )
{
if ( !CanFit( result ) )
{
return false;
}
}
/* var stackableItems = results.Where( r => r.ItemData.StackSize > 1 ).ToList();
var nonStackableItems = results.Where( r => r.ItemData.StackSize == 1 ).ToList();
var stackableItemsCount = stackableItems.Count;
var nonStackableItemsCount = nonStackableItems.Count;
if ( stackableItemsCount == 0 )
{
return false;
}
var stackableItemsTotal = stackableItems.Sum( r => r.ItemData.StackSize );
var stackableItemsFreeSlots = stackableItemsTotal - stackableItemsCount;
if ( stackableItemsFreeSlots >= nonStackableItemsCount )
{
return true;
} */
return true;
}
}
public interface IInventoryEvent
{
void OnInventoryChanged( InventoryContainer container );
}
public class InventoryFullException : System.Exception
{
public InventoryFullException( string message ) : base( message )
{
}
}
public class SlotTakenException : System.Exception
{
public SlotTakenException( string message ) : base( message )
{
}
}