349 lines
12 KiB
C#
349 lines
12 KiB
C#
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using FishNet.Connection;
|
|
using FishNet.Object;
|
|
using Ashwild.Network;
|
|
using Ashwild.Player;
|
|
|
|
namespace Ashwild.Inventory
|
|
{
|
|
/// <summary>
|
|
/// The local player's inventory. It is client-authoritative: each player manages its own
|
|
/// slots locally (crafting, use, hotbar all stay local). The network layer is only used to
|
|
/// (a) receive items the server granted from an authoritative pickup, and (b) spawn dropped
|
|
/// world objects on the server. Only the owning client registers the Instance singleton.
|
|
/// </summary>
|
|
[DisallowMultipleComponent]
|
|
public class PlayerInventory : NetworkBehaviour
|
|
{
|
|
#region Singleton
|
|
|
|
/// <summary>
|
|
/// The local player's inventory (set only on the owning client).
|
|
/// </summary>
|
|
public static PlayerInventory Instance { get; private set; }
|
|
|
|
#endregion
|
|
|
|
#region Serialized Fields
|
|
|
|
[Header("Inventory")]
|
|
[SerializeField] private int inventorySize = 30;
|
|
[SerializeField] private int hotbarSize = 10;
|
|
|
|
[Header("Drop")]
|
|
[SerializeField] private Transform dropOrigin;
|
|
[SerializeField] private float dropForwardDistance = 1.5f;
|
|
|
|
[Header("Events (consumed by UI)")]
|
|
public UnityEvent<int> onSlotChanged;
|
|
public UnityEvent<int> onSelectedSlotChanged;
|
|
public UnityEvent<ItemData, int> onItemAdded;
|
|
|
|
#endregion
|
|
|
|
#region State
|
|
|
|
private InventorySlot[] slots;
|
|
private int selectedHotbarIndex;
|
|
|
|
public int InventorySize => inventorySize;
|
|
public int HotbarSize => hotbarSize;
|
|
public int SelectedHotbarIndex => selectedHotbarIndex;
|
|
|
|
#endregion
|
|
|
|
#region Unity Lifecycle
|
|
|
|
/// <summary>
|
|
/// Builds the empty slot array (local data on every copy).
|
|
/// </summary>
|
|
private void Awake()
|
|
{
|
|
slots = new InventorySlot[inventorySize];
|
|
for (int i = 0; i < inventorySize; i++) slots[i] = new InventorySlot();
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
PlayerEvents.HotbarSlotPressed += OnHotbarSlotPressed;
|
|
PlayerEvents.HotbarScroll += HandleHotbarScroll;
|
|
PlayerEvents.DropPressed += OnDropPressed;
|
|
PlayerEvents.ItemPickedUp += OnItemPickedUp;
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
PlayerEvents.HotbarSlotPressed -= OnHotbarSlotPressed;
|
|
PlayerEvents.HotbarScroll -= HandleHotbarScroll;
|
|
PlayerEvents.DropPressed -= OnDropPressed;
|
|
PlayerEvents.ItemPickedUp -= OnItemPickedUp;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Network Lifecycle
|
|
|
|
/// <summary>
|
|
/// Registers the singleton on the owning client only (before LocalPlayerSpawned fires).
|
|
/// </summary>
|
|
public override void OnStartNetwork()
|
|
{
|
|
base.OnStartNetwork();
|
|
if (base.Owner.IsLocalClient) Instance = this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the singleton when this player leaves the network.
|
|
/// </summary>
|
|
public override void OnStopNetwork()
|
|
{
|
|
base.OnStopNetwork();
|
|
if (Instance == this) Instance = null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Bus Handlers
|
|
|
|
private void OnHotbarSlotPressed(int index) => SelectHotbarSlot(index);
|
|
|
|
private void HandleHotbarScroll(float dir)
|
|
{
|
|
if (dir > 0f) SelectHotbarSlot((selectedHotbarIndex - 1 + hotbarSize) % hotbarSize);
|
|
else if (dir < 0f) SelectHotbarSlot((selectedHotbarIndex + 1) % hotbarSize);
|
|
}
|
|
|
|
private void OnDropPressed()
|
|
{
|
|
if (PlayerEvents.IsInventoryOpen) return;
|
|
if (PlayerStats.Instance != null && PlayerStats.Instance.IsDead) return;
|
|
DropItem(selectedHotbarIndex, 1);
|
|
}
|
|
|
|
private void OnItemPickedUp(ItemData item, int qty) => AddItem(item, qty);
|
|
|
|
#endregion
|
|
|
|
#region Network Grant / Drop
|
|
|
|
/// <summary>
|
|
/// Server-side: sends a granted item to this inventory's owning client, where it is added.
|
|
/// Called after the server authorises a pickup.
|
|
/// </summary>
|
|
public void GrantItemFromServer(ItemData item, int quantity)
|
|
{
|
|
if (item == null) return;
|
|
|
|
ushort id = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetId(item) : (ushort)0;
|
|
if (id == 0)
|
|
{
|
|
Debug.LogError($"[PlayerInventory] '{item.ItemName}' is not in the ItemDatabase — cannot grant. Run Rebuild Item Database.", this);
|
|
return;
|
|
}
|
|
|
|
TargetGrantItem(Owner, id, quantity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs on the owning client: resolves the granted item and adds it locally.
|
|
/// </summary>
|
|
[TargetRpc]
|
|
private void TargetGrantItem(NetworkConnection conn, ushort itemId, int quantity)
|
|
{
|
|
ItemData item = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetItem(itemId) : null;
|
|
if (item == null)
|
|
{
|
|
Debug.LogError($"[PlayerInventory] Granted unknown item id {itemId}.", this);
|
|
return;
|
|
}
|
|
|
|
AddItem(item, quantity);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Storage API
|
|
|
|
public InventorySlot GetSlot(int index)
|
|
{
|
|
if (index < 0 || index >= inventorySize) return null;
|
|
return slots[index];
|
|
}
|
|
|
|
public InventorySlot GetSelectedSlot() => slots[selectedHotbarIndex];
|
|
|
|
public bool AddItem(ItemData item, int quantity = 1)
|
|
{
|
|
if (item == null)
|
|
{
|
|
Debug.LogError("[PlayerInventory] AddItem called with no ItemData — pickup ignored.", this);
|
|
return false;
|
|
}
|
|
|
|
int remaining = quantity;
|
|
|
|
for (int i = 0; i < inventorySize && remaining > 0; i++)
|
|
{
|
|
if (!slots[i].IsEmpty && slots[i].ItemData == item && slots[i].CanAccept(item))
|
|
{
|
|
remaining = slots[i].AddQuantity(remaining);
|
|
NotifySlotChanged(i);
|
|
}
|
|
}
|
|
for (int i = 0; i < inventorySize && remaining > 0; i++)
|
|
{
|
|
if (slots[i].IsEmpty)
|
|
{
|
|
int toPlace = (item.IsStackable && remaining > item.MaxStackSize) ? item.MaxStackSize : remaining;
|
|
slots[i].Set(item, toPlace);
|
|
remaining -= toPlace;
|
|
NotifySlotChanged(i);
|
|
}
|
|
}
|
|
|
|
int added = quantity - remaining;
|
|
if (added > 0)
|
|
{
|
|
onItemAdded?.Invoke(item, added);
|
|
PlayerEvents.RaiseItemAdded(item, added);
|
|
}
|
|
return remaining <= 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whether the given quantity of an item would fit without mutating anything.
|
|
/// </summary>
|
|
public bool CanFit(ItemData item, int quantity = 1)
|
|
{
|
|
if (item == null) return false;
|
|
|
|
int remaining = quantity;
|
|
for (int i = 0; i < inventorySize && remaining > 0; i++)
|
|
{
|
|
if (!slots[i].IsEmpty && slots[i].ItemData == item && slots[i].CanAccept(item))
|
|
remaining -= Mathf.Min(remaining, item.MaxStackSize - slots[i].Quantity);
|
|
}
|
|
for (int i = 0; i < inventorySize && remaining > 0; i++)
|
|
{
|
|
if (slots[i].IsEmpty)
|
|
remaining -= Mathf.Min(remaining, item.IsStackable ? item.MaxStackSize : 1);
|
|
}
|
|
return remaining <= 0;
|
|
}
|
|
|
|
public void RemoveItem(int index, int quantity = 1)
|
|
{
|
|
if (index < 0 || index >= inventorySize) return;
|
|
slots[index].RemoveQuantity(quantity);
|
|
NotifySlotChanged(index);
|
|
}
|
|
|
|
public void SwapSlots(int indexA, int indexB)
|
|
{
|
|
if (indexA < 0 || indexA >= inventorySize) return;
|
|
if (indexB < 0 || indexB >= inventorySize) return;
|
|
|
|
ItemData tempData = slots[indexA].ItemData;
|
|
int tempQty = slots[indexA].Quantity;
|
|
|
|
if (slots[indexB].IsEmpty) slots[indexA].Clear();
|
|
else slots[indexA].Set(slots[indexB].ItemData, slots[indexB].Quantity);
|
|
|
|
if (tempData == null) slots[indexB].Clear();
|
|
else slots[indexB].Set(tempData, tempQty);
|
|
|
|
NotifySlotChanged(indexA);
|
|
NotifySlotChanged(indexB);
|
|
}
|
|
|
|
public void UseItem(int index)
|
|
{
|
|
if (index < 0 || index >= inventorySize) return;
|
|
if (slots[index].IsEmpty) return;
|
|
|
|
ItemData item = slots[index].ItemData;
|
|
if (item.ItemType != ItemType.Consumable) return;
|
|
|
|
if (PlayerStats.Instance != null)
|
|
{
|
|
if (item.HealthRestore > 0f) PlayerStats.Instance.Heal(item.HealthRestore);
|
|
if (item.HungerRestore > 0f) PlayerStats.Instance.Feed(item.HungerRestore);
|
|
if (item.ThirstRestore > 0f) PlayerStats.Instance.Drink(item.ThirstRestore);
|
|
}
|
|
PlayerEvents.RaiseItemConsumed(item);
|
|
|
|
RemoveItem(index, 1);
|
|
}
|
|
|
|
public void DropItem(int index, int quantity = 1)
|
|
{
|
|
if (index < 0 || index >= inventorySize) return;
|
|
if (slots[index].IsEmpty) return;
|
|
|
|
ItemData item = slots[index].ItemData;
|
|
Transform origin = dropOrigin != null ? dropOrigin : transform;
|
|
if (item.WorldPrefab != null && origin != null && PickableRegistry.Instance != null)
|
|
{
|
|
Vector3 dropPos = origin.position + origin.forward * dropForwardDistance;
|
|
ushort id = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetId(item) : (ushort)0;
|
|
if (id != 0)
|
|
PickableRegistry.Instance.RequestDropServerRpc(id, quantity, dropPos, origin.rotation);
|
|
else
|
|
Debug.LogWarning($"[PlayerInventory] '{item.ItemName}' is not in the ItemDatabase — drop not spawned. Run Rebuild Item Database.", this);
|
|
}
|
|
|
|
RemoveItem(index, quantity);
|
|
PlayerEvents.RaiseItemDropped(item, quantity);
|
|
}
|
|
|
|
public void SelectHotbarSlot(int index)
|
|
{
|
|
if (index < 0 || index >= hotbarSize) return;
|
|
selectedHotbarIndex = index;
|
|
onSelectedSlotChanged?.Invoke(index);
|
|
PlayerEvents.RaiseSelectedHotbarSlotChanged(index);
|
|
}
|
|
|
|
public bool HasItem(ItemData item, int quantity = 1) => CountItem(item) >= quantity;
|
|
|
|
public int CountItem(ItemData item)
|
|
{
|
|
int count = 0;
|
|
for (int i = 0; i < inventorySize; i++)
|
|
if (!slots[i].IsEmpty && slots[i].ItemData == item) count += slots[i].Quantity;
|
|
return count;
|
|
}
|
|
|
|
public bool RemoveItemByData(ItemData item, int quantity)
|
|
{
|
|
if (!HasItem(item, quantity)) return false;
|
|
|
|
int remaining = quantity;
|
|
for (int i = 0; i < inventorySize && remaining > 0; i++)
|
|
{
|
|
if (!slots[i].IsEmpty && slots[i].ItemData == item)
|
|
{
|
|
int toRemove = Mathf.Min(remaining, slots[i].Quantity);
|
|
slots[i].RemoveQuantity(toRemove);
|
|
remaining -= toRemove;
|
|
NotifySlotChanged(i);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helpers
|
|
|
|
private void NotifySlotChanged(int index)
|
|
{
|
|
onSlotChanged?.Invoke(index);
|
|
PlayerEvents.RaiseInventorySlotChanged(index);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|