using UnityEngine; using UnityEngine.UI; using TMPro; using Ashwild.Player; using Ashwild.Storage; using Ashwild.UI; namespace Ashwild.Inventory { /// /// The inventory window and the manager of its right-side modules. It is a local UI panel /// (opening/closing is purely local — the item data lives in the networked ), /// driven by the GameUIManager panel stack. The left grid + hotbar are always the player's inventory; /// the right area swaps between two modules: the hover description (normal browsing) and the chest /// module () when a chest is opened. Opening a chest reuses this same window /// — the inventory and hotbar cells double as the deposit source, so nothing is rebuilt — and every /// drop is routed here into the right operation, chest transfers going through the chest's /// server-authoritative RPCs so two players sharing a chest stay in sync. /// 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). A chest reuses this same window/kind. /// public override PanelKind Kind => PanelKind.Inventory; /// /// The single inventory window in the scene, reached by a chest's Interact() and by slot cells /// routing their drops here. /// public static InventoryUI Instance { get; private set; } #region Serialized Fields [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("Right Modules")] [Tooltip("Holder of the hover-description module — shown while browsing, hidden while a chest is open.")] [SerializeField] private GameObject descriptionModule; [SerializeField] private HoverDescriptionUI hoverDescription; [Tooltip("The chest module shown in place of the description while a chest is open.")] [SerializeField] private ChestPanelUI chestPanel; [Header("Categories")] [SerializeField] private InventoryCategoryManager categoryManager; [Header("Hotbar")] [SerializeField] private HotbarUI hotbarUI; #endregion #region State private SlotUI[] slotUIs; private PlayerInventory inventory; private bool bound; /// /// The chest bound while the window is in chest mode (null during normal inventory browsing). /// private Chest boundChest; #endregion #region Unity Lifecycle /// /// Registers the singleton (in addition to the base panel setup). /// protected override void Awake() { base.Awake(); Instance = this; } /// /// 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 + chest) — 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(); } /// /// Clears the listener and the singleton on teardown. /// private void OnDestroy() { if (inventory != null) inventory.onSlotChanged.RemoveListener(RefreshSlot); if (Instance == this) Instance = null; } #endregion #region Build /// /// 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(SlotContainer.Inventory, slotIndex, HandleSlotDrop, OnSlotClicked, OnSlotHoverEnter, OnSlotHoverExit); slotUIs[i] = slotUI; } inventory.onSlotChanged.AddListener(RefreshSlot); RefreshAll(); } #endregion #region Panel Lifecycle /// /// Binds a chest and opens this window in chest mode through the panel stack. Called from a /// chest's Interact(); the chest is picked up in Show(). /// public void OpenChest(Chest chest) { if (chest == null) return; boundChest = chest; if (UIManager.Instance != null) UIManager.Instance.OpenPanel(this); } /// /// Opens the inventory window. Enters chest mode when a chest was bound (right side shows the /// chest module, category is forced to and locked on the inventory), otherwise normal browsing /// (right side shows the description). Always lands on the inventory category, never craft. /// public override void Show() { inventoryPanel.SetActive(true); bool chestMode = boundChest != null; if (categoryManager != null) { categoryManager.Open(); categoryManager.SetLocked(chestMode); } if (chestMode) { if (descriptionModule != null) descriptionModule.SetActive(false); if (chestPanel != null) chestPanel.Bind(boundChest); PlayerEvents.RaiseChestOpenChanged(true); } else { if (chestPanel != null) chestPanel.Hide(); if (descriptionModule != null) descriptionModule.SetActive(true); } RefreshAll(); if (hotbarUI != null) hotbarUI.OnInventoryOpen(); } /// /// Closes the window and tears down its transient UI (ghost, context menu, chest binding). /// public override void Hide() { if (ghostObject != null) ghostObject.SetActive(false); if (contextMenu != null) contextMenu.Hide(); if (hoverDescription != null) hoverDescription.Hide(); if (categoryManager != null) { categoryManager.SetLocked(false); categoryManager.Close(); } if (hotbarUI != null) hotbarUI.OnInventoryClose(); CloseChestMode(); 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); CloseChestMode(); inventoryPanel.SetActive(false); } /// /// Leaves chest mode: hides the chest module, releases the chest and puts the description module /// back. Shared by both close paths so they can never drift apart — HideInstant used to forget /// the description and left the right side blank on the next open. /// private void CloseChestMode() { if (boundChest == null) return; if (chestPanel != null) chestPanel.Hide(); boundChest = null; if (descriptionModule != null) descriptionModule.SetActive(true); PlayerEvents.RaiseChestOpenChanged(false); } #endregion #region Transfer Routing /// /// Routes a slot drop to whoever owns the slots: the inventory itself when both ends are local, /// otherwise the chest (which reconciles it server-side). Both paths end up in the same /// rules, so a chest drag behaves exactly like an inventory drag. /// Static so every cell (inventory, hotbar, chest) reports here without a per-cell reference. /// public static void HandleSlotDrop(SlotContainer fromContainer, int fromIndex, SlotContainer toContainer, int toIndex) { if (fromContainer == SlotContainer.Inventory && toContainer == SlotContainer.Inventory) { PlayerInventory inv = PlayerInventory.Instance; if (inv != null) inv.MoveSlot(fromIndex, toIndex); return; } Chest chest = Instance != null ? Instance.boundChest : null; if (chest == null) return; // Paint the chest cell as emptied now: its clear replicates on the SyncList (end of tick) // while the grant is an immediate TargetRpc, so without this the stack visibly lands in the // inventory before it leaves the chest. if (fromContainer == SlotContainer.Chest && Instance.chestPanel != null) Instance.chestPanel.PredictEmptied(fromIndex); chest.RequestMove(fromContainer, fromIndex, toContainer, toIndex); } /// /// Quick-transfers a stack to the other container (auto-placed), for right-clicks. Exposed so the /// hotbar — which cannot reach the private chest binding — can route its own cells here. /// public void RequestQuickTransfer(SlotContainer container, int index) { if (boundChest != null) boundChest.RequestQuickTransfer(container, index); } #endregion #region Event Handlers /// /// Right-click on an inventory cell: a quick deposit while a chest is open, otherwise the slot /// context menu. /// private void OnSlotClicked(SlotContainer container, int index, bool rightClick) { if (!rightClick) return; if (boundChest != null) { boundChest.RequestQuickTransfer(SlotContainer.Inventory, index); return; } if (contextMenu != null) contextMenu.Show(index); else if (inventory != null) inventory.UseItem(index); } /// /// Builds the description payload for the hovered slot and shows the side panel. Suppressed in /// chest mode (the description module is hidden) and for empty slots. The duration bar reflects /// the item's remaining uses/durability. /// private void OnSlotHoverEnter(SlotContainer container, int index) { if (boundChest != null) return; if (hoverDescription == null || inventory == null) return; InventorySlot slot = inventory.GetSlot(index); if (slot == null || slot.IsEmpty) { hoverDescription.Hide(); return; } hoverDescription.Show(ItemDescriptionView.From(slot)); } /// /// Hides the description panel when the pointer leaves a slot. /// private void OnSlotHoverExit() { if (hoverDescription != null) hoverDescription.Hide(); } #endregion #region Refresh 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)); } } #endregion } }