canto/Assets/Scripts/Item/Inventory.cs

309 lines
8.7 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using KitsuneCafe.System;
using ObservableCollections;
using UnityEngine;
using Unit = KitsuneCafe.System.Unit;
namespace KitsuneCafe.ItemSystem
{
[Serializable]
public struct InventoryItem : IEquatable<Item>, ICountable
{
public Item Item;
[SerializeField]
private int count;
public readonly int Count => count;
private InventoryItem(Item item, int count)
{
Item = item;
this.count = count;
}
public static int Create(Item item, int count, out InventoryItem? inventoryItem)
{
int quantity = Math.Clamp(count, 0, item.MaxStackCount);
inventoryItem = quantity > 0 ? new InventoryItem(item, quantity) : null;
return quantity;
}
public readonly int IncreaseCount(int count, out InventoryItem? item)
{
return Create(Item, Count + count, out item);
}
public readonly int ReduceCount(int count, out InventoryItem? item)
{
return Create(Item, Count - count, out item);
}
public readonly bool Equals(Item other)
{
return Item == other;
}
public override readonly bool Equals(object obj)
{
return obj is InventoryItem other
&& Item == other.Item
&& Count == other.Count;
}
public override int GetHashCode()
{
return HashCode.Combine(Item, Count);
}
}
public enum InventoryError
{
None,
InventoryFull,
ItemNotFound,
NotEnoughQuantity,
ItemNotStackable,
InvalidQuantity
}
public interface ICountable
{
int Count { get; }
}
public interface IInventory<T> where T : IEquatable<Item>, ICountable
{
int Capacity { get; }
int Count { get; }
IResult<Unit, InventoryError> Add(Item item, int count = 1);
IResult<Unit, InventoryError> Remove(Item item, int count = 1);
IEnumerable<T> Find(Func<T, bool> predicate);
bool Has(Item item);
}
[CreateAssetMenu(menuName = KitsuneCafeMenu.Item + "Inventory")]
public class Inventory : ScriptableObject, IInventory<InventoryItem>, IEnumerable<InventoryItem>, IObservableCollection<InventoryItem>, ISerializationCallbackReceiver
{
[SerializeField]
private List<InventoryItem> serializableItems = new();
private ObservableList<InventoryItem> runtimeItems;
[SerializeField]
private int capacity = 8;
public event NotifyCollectionChangedEventHandler<InventoryItem> CollectionChanged = default;
public int Capacity => capacity;
public int Count => runtimeItems?.Count ?? 0;
public bool IsEmpty => Count == 0;
public bool IsFull => Count == Capacity;
public object SyncRoot => runtimeItems.SyncRoot;
public InventoryItem this[int index]
{
get => runtimeItems[index];
set => runtimeItems[index] = value;
}
private void OnEnable()
{
if (runtimeItems == null)
{
runtimeItems = new ObservableList<InventoryItem>();
}
runtimeItems.CollectionChanged -= OnCollectionChanged;
runtimeItems.CollectionChanged += OnCollectionChanged;
}
private void OnDisable()
{
if (runtimeItems != null)
{
runtimeItems.CollectionChanged -= OnCollectionChanged;
}
}
private void OnCollectionChanged(in NotifyCollectionChangedEventArgs<InventoryItem> e)
{
CollectionChanged?.Invoke(e);
}
public void OnBeforeSerialize()
{
if (runtimeItems != null)
{
serializableItems.Clear();
foreach (var item in runtimeItems)
{
serializableItems.Add(item);
}
}
}
public void OnAfterDeserialize()
{
if (runtimeItems == null)
{
runtimeItems = new ObservableList<InventoryItem>();
}
else
{
runtimeItems.Clear();
}
foreach (var item in serializableItems)
{
runtimeItems.Add(item);
}
}
public IEnumerable<InventoryItem> Find(Func<InventoryItem, bool> predicate)
{
for (int i = 0; i < Count; i++)
{
var item = runtimeItems[i];
if (predicate(item))
{
yield return item;
}
}
}
private IResult<int, InventoryError> AddToExisting(Item item, int count)
{
if (count <= 0)
{
return Result.Err<int, InventoryError>(InventoryError.InvalidQuantity);
}
for (int i = 0; i < Count; i++)
{
var existingItem = runtimeItems[i];
if (existingItem.Equals(item))
{
if (!item.IsStackable)
{
return Result.Err<int, InventoryError>(InventoryError.ItemNotStackable);
}
var consumedAmount = existingItem.IncreaseCount(count, out var updatedItem);
runtimeItems[i] = updatedItem.Value;
count -= consumedAmount;
if (count <= 0)
{
return Result.Ok<int, InventoryError>(0);
}
}
}
return Result.Ok<int, InventoryError>(count);
}
private IResult<Unit, InventoryError> AddNew(Item item, int count)
{
while (count > 0)
{
if (IsFull)
{
return Result.Err<Unit, InventoryError>(InventoryError.InventoryFull);
}
count -= InventoryItem.Create(item, count, out var newItem);
runtimeItems.Add(newItem.Value);
}
return Result.Ok<Unit, InventoryError>(default);
}
public IResult<Unit, InventoryError> Add(Item item, int count = 1)
{
return AddToExisting(item, count)
.Where(count => count > 0)
.SelectMany(count => AddNew(item, count)) switch
{
Ok<Unit, InventoryError> ok => ok,
Err<Unit, InventoryError>(InventoryError.None) => Result.Ok<Unit, InventoryError>(default),
var err => err
};
}
public IResult<Unit, InventoryError> Remove(Item item, int count = 1)
{
if (count <= 0)
{
return Result.Err<Unit, InventoryError>(InventoryError.InvalidQuantity);
}
for (int i = runtimeItems.Count - 1; i >= 0; i--)
{
var existingItem = runtimeItems[i];
if (existingItem.Item == item)
{
if (existingItem.Count > count)
{
existingItem.ReduceCount(count, out var updatedItem);
runtimeItems[i] = updatedItem.Value;
return Result.Ok<Unit, InventoryError>(Unit.Default);
}
else
{
count -= existingItem.Count;
runtimeItems.RemoveAt(i);
if (count == 0)
{
return Result.Ok<Unit, InventoryError>(Unit.Default);
}
}
}
}
if (count > 0)
{
return Result.Err<Unit, InventoryError>(InventoryError.NotEnoughQuantity);
}
return Result.Ok<Unit, InventoryError>(Unit.Default);
}
public bool Has(Item item)
{
for (int i = 0; i < Count; i++)
{
if (runtimeItems[i].Equals(item))
{
return true;
}
}
return false;
}
public IEnumerator<InventoryItem> GetEnumerator()
{
return runtimeItems.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public ISynchronizedView<InventoryItem, TView> CreateView<TView>(Func<InventoryItem, TView> transform)
{
return runtimeItems.CreateView(transform);
}
}
}