using UnityEngine; using TMPro; using Ashwild.Inventory; namespace Ashwild.Storage { /// /// The chest module of the inventory window: the right-side grid that shows the opened chest's /// contents, mirror of the hover-description module it replaces while a chest is open. It is a pure /// view driven by — it builds its grid of cells from /// the bound chest's replicated view and refreshes them live, but it never routes transfers itself: /// every cell reports its drop to , which turns it into the /// matching server-authoritative deposit/withdraw/move on the . /// [DisallowMultipleComponent] public class ChestPanelUI : MonoBehaviour { #region Serialized Fields [Header("Window")] [Tooltip("Root holder toggled on/off as the module is shown or hidden.")] [SerializeField] private GameObject root; [SerializeField] private TextMeshProUGUI titleText; [Header("Grid")] [Tooltip("Parent the chest cells are laid out under.")] [SerializeField] private Transform slotContainer; [Tooltip("Prefab carrying a SlotUI — the same cell used by the inventory grid and the hotbar.")] [SerializeField] private GameObject slotPrefab; #endregion #region State private SlotUI[] slots; private Chest boundChest; /// /// The chest currently shown (null while the module is hidden). Read by InventoryUI to route /// transfers. /// public Chest Chest => boundChest; #endregion #region Unity Lifecycle /// /// Parks the module hidden so it only appears once a chest is opened. /// private void Awake() { if (root != null) root.SetActive(false); } /// /// Unbinds the chest event if the module is torn down while open. /// private void OnDestroy() { if (boundChest != null) boundChest.SlotViewChanged -= HandleChestSlotChanged; } #endregion #region Show / Hide /// /// Binds a chest and shows the module: (re)builds the grid to the chest's slot count, subscribes /// to its replicated view so cells refresh live, sets the title and draws every cell. /// public void Bind(Chest chest) { if (boundChest != null) boundChest.SlotViewChanged -= HandleChestSlotChanged; boundChest = chest; if (root != null) root.SetActive(true); BuildGrid(); if (boundChest != null) { boundChest.SlotViewChanged += HandleChestSlotChanged; if (titleText != null) titleText.text = boundChest.DisplayName; } RefreshAll(); } /// /// Hides the module, unbinds the chest and releases it. /// public void Hide() { if (boundChest != null) boundChest.SlotViewChanged -= HandleChestSlotChanged; boundChest = null; if (root != null) root.SetActive(false); } #endregion #region Grid Building /// /// Builds (or rebuilds when the size differs) the 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 BuildGrid() { int count = boundChest != null ? boundChest.SlotCount : 0; if (slots != null && slots.Length == count) return; if (slots != null) foreach (SlotUI s in slots) if (s != null) Destroy(s.gameObject); slots = new SlotUI[count]; for (int i = 0; i < count; i++) { SlotUI slot = Instantiate(slotPrefab, slotContainer).GetComponent(); slot.Initialize(SlotContainer.Chest, i, InventoryUI.HandleSlotDrop, HandleChestSlotClicked); slots[i] = slot; } } #endregion #region Interaction /// /// Right-click on a chest cell quick-transfers its whole stack into the inventory (auto-placed). /// private void HandleChestSlotClicked(SlotContainer container, int index, bool rightClick) { if (!rightClick || boundChest == null) return; PredictEmptied(index); boundChest.RequestQuickTransfer(SlotContainer.Chest, index); } /// /// Draws a cell as emptied straight away, while its move is still in flight to the server. /// /// Emptying the chest travels on the SyncList (flushed at end of tick) but the matching grant is /// a TargetRpc (sent immediately), so the item would otherwise appear in the inventory a moment /// *before* leaving the chest — a visible double. This only repaints the cell: the authoritative /// model is untouched, so if the server disagrees (a co-op partner grabbed the stack first) the /// next replicated update simply paints the item back. /// public void PredictEmptied(int index) { if (slots == null || index < 0 || index >= slots.Length) return; slots[index].UpdateVisual((ItemData)null, 0, -1); } #endregion #region Refresh /// /// Refreshes a single cell, or the whole grid when the collection was (re)seeded (index -1). /// private void HandleChestSlotChanged(int index) { if (slots == null) return; if (index < 0) { RefreshAll(); return; } if (index < slots.Length) RefreshSlot(index); } /// /// Redraws every chest cell. /// private void RefreshAll() { if (slots == null) return; for (int i = 0; i < slots.Length; i++) RefreshSlot(i); } /// /// Draws one cell from the chest's replicated view, or empty when the slot holds nothing. /// private void RefreshSlot(int index) { if (boundChest != null && boundChest.TryGetSlot(index, out ItemData item, out int qty, out int uses)) slots[index].UpdateVisual(item, qty, uses); else slots[index].UpdateVisual((ItemData)null, 0, -1); } #endregion } }