Files
Emberwild/Assets/GAME/Script/Inventory/PlayerInventory.cs
T
2026-06-22 16:18:34 +02:00

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
}
}