using UnityEngine; using UnityEngine.UI; using TMPro; using Ashwild.Player; using Ashwild.UI; namespace Ashwild.Inventory { /// /// The inventory window. It is a local UI panel (opening/closing is purely local — the item /// data itself lives in the networked ), so it is driven by the /// GameUIManager panel stack like any other panel. The controller object stays active so it can /// build its grid when the local player spawns; Show/Hide only toggle the visual window. /// public class InventoryUI : UIPanel { /// /// Marks this panel as the inventory: input is locked and the cursor shows, but the world /// keeps running (unlike the pause menu). /// public override PanelKind Kind => PanelKind.Inventory; [Header("References")] [SerializeField] private GameObject inventoryPanel; [SerializeField] private Transform slotContainer; [SerializeField] private GameObject slotPrefab; [Header("Drag Ghost")] [SerializeField] private GameObject ghostObject; [SerializeField] private Image ghostIcon; [SerializeField] private TextMeshProUGUI ghostQuantityText; [Header("Context Menu")] [SerializeField] private SlotContextMenu contextMenu; [Header("Hover Description")] [SerializeField] private HoverDescriptionUI hoverDescription; [Header("Categories")] [SerializeField] private InventoryCategoryManager categoryManager; [Header("Hotbar")] [SerializeField] private HotbarUI hotbarUI; private SlotUI[] slotUIs; private PlayerInventory inventory; private bool bound; /// /// Waits for the networked local player to spawn before building the inventory grid. /// private void OnEnable() { PlayerEvents.LocalPlayerSpawned += HandleLocalPlayerSpawned; } /// /// Unsubscribes — mirrors OnEnable exactly. /// private void OnDisable() { PlayerEvents.LocalPlayerSpawned -= HandleLocalPlayerSpawned; } private void Start() { // Setup shared ghost for all SlotUIs (hotbar + inventory) — does not need the player. SlotUI.SetupGhost(ghostObject, ghostIcon, ghostQuantityText); inventoryPanel.SetActive(false); // The player may already exist (late UI init); otherwise we wait for the spawn event. if (PlayerInventory.Instance != null) BuildInventory(); } /// /// Builds the grid as soon as the local player has spawned on the network. /// private void HandleLocalPlayerSpawned() => BuildInventory(); /// /// Creates the non-hotbar slot UIs from the local player's inventory; runs once. /// private void BuildInventory() { if (bound) return; inventory = PlayerInventory.Instance; if (inventory == null) return; bound = true; // Only create slots for non-hotbar indices (hotbarSize to inventorySize-1) int nonHotbarCount = inventory.InventorySize - inventory.HotbarSize; slotUIs = new SlotUI[nonHotbarCount]; for (int i = 0; i < nonHotbarCount; i++) { int slotIndex = inventory.HotbarSize + i; GameObject slotGO = Instantiate(slotPrefab, slotContainer); SlotUI slotUI = slotGO.GetComponent(); slotUI.Initialize(slotIndex, OnSwapRequested, OnSlotClicked, OnSlotHoverEnter, OnSlotHoverExit); slotUIs[i] = slotUI; } inventory.onSlotChanged.AddListener(RefreshSlot); RefreshAll(); } private void OnDestroy() { if (inventory != null) inventory.onSlotChanged.RemoveListener(RefreshSlot); } /// /// Opens the inventory window (called by the GameUIManager panel stack). /// public override void Show() { inventoryPanel.SetActive(true); RefreshAll(); if (categoryManager != null) categoryManager.Open(); if (hotbarUI != null) hotbarUI.OnInventoryOpen(); } /// /// Closes the inventory window and tears down its transient UI (ghost, context menu). /// public override void Hide() { if (ghostObject != null) ghostObject.SetActive(false); if (contextMenu != null) contextMenu.Hide(); if (hoverDescription != null) hoverDescription.Hide(); if (categoryManager != null) categoryManager.Close(); if (hotbarUI != null) hotbarUI.OnInventoryClose(); inventoryPanel.SetActive(false); } /// /// Instant hide used when the manager initializes panels — just parks the window closed. /// public override void HideInstant() { if (ghostObject != null) ghostObject.SetActive(false); inventoryPanel.SetActive(false); } // Called by any SlotUI (inventory or hotbar) when a drag-drop completes public static void OnSwapRequested(int fromIndex, int toIndex) { PlayerInventory inv = PlayerInventory.Instance; InventorySlot fromSlot = inv.GetSlot(fromIndex); InventorySlot toSlot = inv.GetSlot(toIndex); // Same stackable item: merge if (!fromSlot.IsEmpty && !toSlot.IsEmpty && fromSlot.ItemData == toSlot.ItemData && toSlot.CanAccept(fromSlot.ItemData)) { int leftover = toSlot.AddQuantity(fromSlot.Quantity); if (leftover <= 0) fromSlot.Clear(); else fromSlot.Set(fromSlot.ItemData, leftover); inv.onSlotChanged?.Invoke(fromIndex); inv.onSlotChanged?.Invoke(toIndex); } else { // Swap inv.SwapSlots(fromIndex, toIndex); } } private void OnSlotClicked(int index, bool rightClick) { if (rightClick) { if (contextMenu != null) contextMenu.Show(index); else inventory.UseItem(index); } } /// /// Builds the description payload for the hovered slot and shows the side panel. Empty slots /// keep the panel hidden. The duration bar reflects the item's remaining uses/durability. /// private void OnSlotHoverEnter(int index) { if (hoverDescription == null || inventory == null) return; InventorySlot slot = inventory.GetSlot(index); if (slot == null || slot.IsEmpty) { hoverDescription.Hide(); return; } ItemData item = slot.ItemData; bool hasDuration = item.HasUses; float fill = hasDuration ? (float)slot.CurrentUses / item.MaxUses : 0f; string durationText = hasDuration ? $"{slot.CurrentUses} / {item.MaxUses}" : string.Empty; hoverDescription.Show(new ItemDescriptionView( item.Icon, item.ItemName, item.Description, hasDuration, fill, durationText)); } /// /// Hides the description panel when the pointer leaves a slot. /// private void OnSlotHoverExit() { if (hoverDescription != null) hoverDescription.Hide(); } private void RefreshSlot(int index) { if (slotUIs == null) return; int localIndex = index - inventory.HotbarSize; if (localIndex >= 0 && localIndex < slotUIs.Length) slotUIs[localIndex].UpdateVisual(inventory.GetSlot(index)); } private void RefreshAll() { if (slotUIs == null) return; for (int i = 0; i < slotUIs.Length; i++) { int slotIndex = inventory.HotbarSize + i; slotUIs[i].UpdateVisual(inventory.GetSlot(slotIndex)); } } } }