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("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); 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 (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); } } 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)); } } } }