Files
2026-07-07 16:43:51 +02:00

321 lines
12 KiB
C#

using UnityEngine;
using UnityEngine.UI;
using TMPro;
using Ashwild.Inventory;
using Ashwild.UI;
namespace Ashwild.Storage
{
/// <summary>
/// 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 <see cref="Chest"/>, 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 <c>GameUIManager.panels</c> 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.
/// </summary>
public class ChestUI : UIPanel
{
/// <summary>
/// Marks this as a local window: input is locked and the cursor shows, but the world keeps
/// running (never pauses), exactly like the inventory.
/// </summary>
public override PanelKind Kind => PanelKind.Chest;
/// <summary>
/// The single chest window in the scene, reached by a chest's Interact().
/// </summary>
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;
/// <summary>
/// The chest currently displayed on the right side (null while closed).
/// </summary>
public Chest Chest => boundChest;
#endregion
#region Unity Lifecycle
/// <summary>
/// Registers the singleton (in addition to the base panel setup).
/// </summary>
protected override void Awake()
{
base.Awake();
Instance = this;
}
/// <summary>
/// Sets up the shared ghost, parks the window closed and wires the transfer-all buttons.
/// </summary>
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);
}
/// <summary>
/// Clears the singleton and drops the button listeners on teardown.
/// </summary>
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
/// <summary>
/// Binds a chest and opens the window through the panel stack (cursor + input lock handled by
/// the GameUIManager). Called from a chest's Interact().
/// </summary>
public void Open(Chest chest)
{
if (chest == null) return;
boundChest = chest;
if (UIManager.Instance != null)
UIManager.Instance.OpenPanel(this);
}
/// <summary>
/// Opens the window: builds the grids for the bound chest, binds change events and refreshes.
/// </summary>
public override void Show()
{
if (windowRoot != null) windowRoot.SetActive(true);
EnsureInventoryGrid();
BuildChestGrid();
BindChanges();
if (titleText != null && boundChest != null) titleText.text = boundChest.DisplayName;
RefreshInventory();
RefreshChest();
}
/// <summary>
/// Closes the window, unbinds change events and releases the chest.
/// </summary>
public override void Hide()
{
if (ghostObject != null) ghostObject.SetActive(false);
UnbindChanges();
boundChest = null;
if (windowRoot != null) windowRoot.SetActive(false);
}
/// <summary>
/// Instant close used when the manager initializes panels — just parks the window closed.
/// </summary>
public override void HideInstant()
{
if (ghostObject != null) ghostObject.SetActive(false);
UnbindChanges();
boundChest = null;
if (windowRoot != null) windowRoot.SetActive(false);
}
#endregion
#region Grid Building
/// <summary>
/// Builds the left grid from the local player's full inventory once; reused across opens since
/// the inventory size never changes.
/// </summary>
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<ChestSlotUI>();
slot.Initialize(this, ChestSlotContainer.Inventory, i);
inventorySlots[i] = slot;
}
}
/// <summary>
/// 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.
/// </summary>
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<ChestSlotUI>();
slot.Initialize(this, ChestSlotContainer.Chest, i);
chestSlots[i] = slot;
}
}
#endregion
#region Change Binding
/// <summary>
/// Subscribes to the inventory's and the chest's change notifications so cells refresh live.
/// </summary>
private void BindChanges()
{
PlayerInventory inv = PlayerInventory.Instance;
if (inv != null) inv.onSlotChanged.AddListener(HandleInventorySlotChanged);
if (boundChest != null) boundChest.SlotViewChanged += HandleChestSlotChanged;
}
/// <summary>
/// Unsubscribes — mirrors BindChanges exactly.
/// </summary>
private void UnbindChanges()
{
PlayerInventory inv = PlayerInventory.Instance;
if (inv != null) inv.onSlotChanged.RemoveListener(HandleInventorySlotChanged);
if (boundChest != null) boundChest.SlotViewChanged -= HandleChestSlotChanged;
}
/// <summary>
/// Refreshes a single inventory cell when its slot changes.
/// </summary>
private void HandleInventorySlotChanged(int index)
{
if (inventorySlots != null && index >= 0 && index < inventorySlots.Length)
inventorySlots[index].Refresh();
}
/// <summary>
/// Refreshes a single chest cell, or the whole grid when the collection was (re)seeded (-1).
/// </summary>
private void HandleChestSlotChanged(int index)
{
if (chestSlots == null) return;
if (index < 0) { RefreshChest(); return; }
if (index < chestSlots.Length) chestSlots[index].Refresh();
}
/// <summary>
/// Redraws every inventory cell.
/// </summary>
private void RefreshInventory()
{
if (inventorySlots == null) return;
for (int i = 0; i < inventorySlots.Length; i++) inventorySlots[i].Refresh();
}
/// <summary>
/// Redraws every chest cell.
/// </summary>
private void RefreshChest()
{
if (chestSlots == null) return;
for (int i = 0; i < chestSlots.Length; i++) chestSlots[i].Refresh();
}
#endregion
#region Transfer Routing
/// <summary>
/// Routes a drag-drop between two cells into the matching operation: inventory swap, deposit,
/// withdraw, or move within the chest.
/// </summary>
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);
}
/// <summary>
/// Right-click quick transfer: an inventory cell deposits (auto-placed), a chest cell withdraws.
/// </summary>
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);
}
/// <summary>
/// "Deposit all" button: stores every inventory item in the chest.
/// </summary>
private void HandleDepositAll()
{
if (boundChest != null) boundChest.RequestDepositAll();
}
/// <summary>
/// "Take all" button: withdraws every chest item into the inventory.
/// </summary>
private void HandleWithdrawAll()
{
if (boundChest != null) boundChest.RequestWithdrawAll();
}
#endregion
}
}