using UnityEngine; using UnityEngine.UI; using TMPro; using Ashwild.Inventory; using Ashwild.UI; namespace Ashwild.Storage { /// /// The chest window: the local player's whole inventory on the left, the opened chest's contents /// on the right. It is a local UI panel — opened by interacting with a , closed /// with Escape — driven by the GameUIManager panel stack like the inventory. The inventory grid is /// client-authoritative data; the chest grid renders the chest's replicated view and every move is /// routed through the chest's server-authoritative RPCs, so two players sharing a chest stay in sync. /// /// Register this panel in GameUIManager.panels so the stack can open/close it. The controller /// object stays active; Show/Hide only toggle the visual window and (un)bind the change events. /// public class ChestUI : UIPanel { /// /// Marks this as a local window: input is locked and the cursor shows, but the world keeps /// running (never pauses), exactly like the inventory. /// public override PanelKind Kind => PanelKind.Chest; /// /// The single chest window in the scene, reached by a chest's Interact(). /// public static ChestUI Instance { get; private set; } #region Serialized Fields [Header("Window")] [Tooltip("Visual window toggled on open/close; the controller object itself stays active.")] [SerializeField] private GameObject windowRoot; [SerializeField] private TextMeshProUGUI titleText; [Header("Grids")] [Tooltip("Parent the player-inventory cells are laid out under (left side).")] [SerializeField] private Transform inventoryContainer; [Tooltip("Parent the chest cells are laid out under (right side).")] [SerializeField] private Transform chestContainer; [Tooltip("Prefab with a ChestSlotUI, instantiated for every cell on both sides.")] [SerializeField] private GameObject slotPrefab; [Header("Drag Ghost")] [SerializeField] private GameObject ghostObject; [SerializeField] private Image ghostIcon; [SerializeField] private TextMeshProUGUI ghostQuantityText; [Header("Transfer Buttons")] [SerializeField] private Button depositAllButton; [SerializeField] private Button withdrawAllButton; #endregion #region State private ChestSlotUI[] inventorySlots; private ChestSlotUI[] chestSlots; private Chest boundChest; /// /// The chest currently displayed on the right side (null while closed). /// public Chest Chest => boundChest; #endregion #region Unity Lifecycle /// /// Registers the singleton (in addition to the base panel setup). /// protected override void Awake() { base.Awake(); Instance = this; } /// /// Sets up the shared ghost, parks the window closed and wires the transfer-all buttons. /// private void Start() { ChestSlotUI.SetupGhost(ghostObject, ghostIcon, ghostQuantityText); if (windowRoot != null) windowRoot.SetActive(false); if (depositAllButton != null) depositAllButton.onClick.AddListener(HandleDepositAll); if (withdrawAllButton != null) withdrawAllButton.onClick.AddListener(HandleWithdrawAll); } /// /// Clears the singleton and drops the button listeners on teardown. /// private void OnDestroy() { if (depositAllButton != null) depositAllButton.onClick.RemoveListener(HandleDepositAll); if (withdrawAllButton != null) withdrawAllButton.onClick.RemoveListener(HandleWithdrawAll); if (Instance == this) Instance = null; } #endregion #region Open / Panel Lifecycle /// /// Binds a chest and opens the window through the panel stack (cursor + input lock handled by /// the GameUIManager). Called from a chest's Interact(). /// public void Open(Chest chest) { if (chest == null) return; boundChest = chest; if (UIManager.Instance != null) UIManager.Instance.OpenPanel(this); } /// /// Opens the window: builds the grids for the bound chest, binds change events and refreshes. /// public override void Show() { if (windowRoot != null) windowRoot.SetActive(true); EnsureInventoryGrid(); BuildChestGrid(); BindChanges(); if (titleText != null && boundChest != null) titleText.text = boundChest.DisplayName; RefreshInventory(); RefreshChest(); } /// /// Closes the window, unbinds change events and releases the chest. /// public override void Hide() { if (ghostObject != null) ghostObject.SetActive(false); UnbindChanges(); boundChest = null; if (windowRoot != null) windowRoot.SetActive(false); } /// /// Instant close used when the manager initializes panels — just parks the window closed. /// public override void HideInstant() { if (ghostObject != null) ghostObject.SetActive(false); UnbindChanges(); boundChest = null; if (windowRoot != null) windowRoot.SetActive(false); } #endregion #region Grid Building /// /// Builds the left grid from the local player's full inventory once; reused across opens since /// the inventory size never changes. /// private void EnsureInventoryGrid() { if (inventorySlots != null) return; PlayerInventory inv = PlayerInventory.Instance; if (inv == null) return; int count = inv.InventorySize; inventorySlots = new ChestSlotUI[count]; for (int i = 0; i < count; i++) { ChestSlotUI slot = Instantiate(slotPrefab, inventoryContainer).GetComponent(); slot.Initialize(this, ChestSlotContainer.Inventory, i); inventorySlots[i] = slot; } } /// /// Builds (or rebuilds when the size differs) the right grid to match the bound chest's slot /// count, so opening a small chest then a large one lays out the right number of cells. /// private void BuildChestGrid() { int count = boundChest != null ? boundChest.SlotCount : 0; if (chestSlots != null && chestSlots.Length == count) return; if (chestSlots != null) foreach (ChestSlotUI s in chestSlots) if (s != null) Destroy(s.gameObject); chestSlots = new ChestSlotUI[count]; for (int i = 0; i < count; i++) { ChestSlotUI slot = Instantiate(slotPrefab, chestContainer).GetComponent(); slot.Initialize(this, ChestSlotContainer.Chest, i); chestSlots[i] = slot; } } #endregion #region Change Binding /// /// Subscribes to the inventory's and the chest's change notifications so cells refresh live. /// private void BindChanges() { PlayerInventory inv = PlayerInventory.Instance; if (inv != null) inv.onSlotChanged.AddListener(HandleInventorySlotChanged); if (boundChest != null) boundChest.SlotViewChanged += HandleChestSlotChanged; } /// /// Unsubscribes — mirrors BindChanges exactly. /// private void UnbindChanges() { PlayerInventory inv = PlayerInventory.Instance; if (inv != null) inv.onSlotChanged.RemoveListener(HandleInventorySlotChanged); if (boundChest != null) boundChest.SlotViewChanged -= HandleChestSlotChanged; } /// /// Refreshes a single inventory cell when its slot changes. /// private void HandleInventorySlotChanged(int index) { if (inventorySlots != null && index >= 0 && index < inventorySlots.Length) inventorySlots[index].Refresh(); } /// /// Refreshes a single chest cell, or the whole grid when the collection was (re)seeded (-1). /// private void HandleChestSlotChanged(int index) { if (chestSlots == null) return; if (index < 0) { RefreshChest(); return; } if (index < chestSlots.Length) chestSlots[index].Refresh(); } /// /// Redraws every inventory cell. /// private void RefreshInventory() { if (inventorySlots == null) return; for (int i = 0; i < inventorySlots.Length; i++) inventorySlots[i].Refresh(); } /// /// Redraws every chest cell. /// private void RefreshChest() { if (chestSlots == null) return; for (int i = 0; i < chestSlots.Length; i++) chestSlots[i].Refresh(); } #endregion #region Transfer Routing /// /// Routes a drag-drop between two cells into the matching operation: inventory swap, deposit, /// withdraw, or move within the chest. /// public void HandleTransfer(ChestSlotUI from, ChestSlotUI to) { if (from == null || to == null || boundChest == null) return; bool fromInv = from.Container == ChestSlotContainer.Inventory; bool toInv = to.Container == ChestSlotContainer.Inventory; if (fromInv && toInv) InventoryUI.OnSwapRequested(from.Index, to.Index); else if (fromInv) boundChest.RequestDepositToSlot(from.Index, to.Index); else if (toInv) boundChest.RequestWithdraw(from.Index); else boundChest.RequestMoveWithin(from.Index, to.Index); } /// /// Right-click quick transfer: an inventory cell deposits (auto-placed), a chest cell withdraws. /// public void HandleQuickTransfer(ChestSlotUI slot) { if (slot == null || boundChest == null) return; if (slot.Container == ChestSlotContainer.Inventory) boundChest.RequestDepositToSlot(slot.Index, -1); else boundChest.RequestWithdraw(slot.Index); } /// /// "Deposit all" button: stores every inventory item in the chest. /// private void HandleDepositAll() { if (boundChest != null) boundChest.RequestDepositAll(); } /// /// "Take all" button: withdraws every chest item into the inventory. /// private void HandleWithdrawAll() { if (boundChest != null) boundChest.RequestWithdrawAll(); } #endregion } }