using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using TMPro; using System; namespace Ashwild.Inventory { /// /// Which container a slot cell maps to, so a drop can be routed to the right operation: a plain /// inventory swap, or a deposit/withdraw/move when a chest is involved. /// public enum SlotContainer { Inventory, Chest } /// /// One draggable slot cell, used everywhere the same way: the inventory grid, the hotbar and the /// chest module. It is a dumb view — it renders whatever it is handed and reports its drags, clicks /// and hovers back to a manager as a (container, index) pair; it never mutates any model itself. /// The manager turns a drop into the matching operation (swap, deposit, withdraw, move). /// public class SlotUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IDropHandler, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler { #region Serialized Fields [Header("References")] [SerializeField] private Image iconImage; [SerializeField] private TextMeshProUGUI quantityText; [SerializeField] private Image highlightImage; [Header("Uses Bar")] [Tooltip("Root object of the uses/durability bar — shown only for items that track uses.")] [SerializeField] private GameObject usageBarRoot; [Tooltip("Filled image whose fillAmount maps to the remaining uses (Image Type = Filled).")] [SerializeField] private Image usageBarFill; [Tooltip("Icon tint applied when the item still has uses left.")] [SerializeField] private Color normalTint = Color.white; [Tooltip("Icon (and bar) tint applied when the item is depleted — a broken repairable tool / empty container.")] [SerializeField] private Color depletedTint = Color.red; #endregion #region State private SlotContainer container; private int slotIndex; private bool hasItem; private Action onDrop; private Action onClicked; private Action onHoverEnter; private Action onHoverExit; public int SlotIndex => slotIndex; public SlotContainer Container => container; // Static drag state shared across every cell (inventory, hotbar and chest grids). private static SlotUI draggedSlot; private static GameObject ghostObject; private static Image ghostIcon; private static TextMeshProUGUI ghostQuantity; #endregion #region Setup /// /// Wires the single shared drag ghost used by every slot cell in the scene. /// public static void SetupGhost(GameObject ghost, Image icon, TextMeshProUGUI qty) { ghostObject = ghost; ghostIcon = icon; ghostQuantity = qty; if (ghostObject != null) ghostObject.SetActive(false); } /// /// Binds this cell to a container and index and the manager callbacks it reports interactions to. /// Drops carry both the dragged and the target (container, index) so the manager can route them. /// public void Initialize(SlotContainer slotContainer, int index, Action dropCallback, Action clickCallback, Action hoverEnterCallback = null, Action hoverExitCallback = null) { container = slotContainer; slotIndex = index; onDrop = dropCallback; onClicked = clickCallback; onHoverEnter = hoverEnterCallback; onHoverExit = hoverExitCallback; } #endregion #region Rendering /// /// Redraws the cell from an inventory slot (the local, client-authoritative container). /// public void UpdateVisual(InventorySlot slot) { if (slot == null || slot.IsEmpty) { RenderEmpty(); return; } ItemData item = slot.ItemData; bool hasUses = item.HasUses; float fill = hasUses && item.MaxUses > 0 ? (float)slot.CurrentUses / item.MaxUses : 0f; RenderItem(item.Icon, slot.Quantity, hasUses, hasUses && slot.IsDepleted, fill); } /// /// Redraws the cell from a resolved chest view (item + quantity + remaining uses). A null item /// renders the empty look. Uses -1 for items that do not track uses. /// public void UpdateVisual(ItemData item, int quantity, int uses) { if (item == null) { RenderEmpty(); return; } bool hasUses = item.HasUses; bool depleted = hasUses && uses <= 0; float fill = hasUses && item.MaxUses > 0 ? (float)Mathf.Max(0, uses) / item.MaxUses : 0f; RenderItem(item.Icon, quantity, hasUses, depleted, fill); } /// /// Draws an item: icon, quantity (hidden for single stacks) and the uses bar. Preserves the /// current icon alpha so a mid-drag fade survives a refresh, and tints the icon red when /// depleted. Shared by both the inventory and the chest render paths. /// private void RenderItem(Sprite icon, int quantity, bool hasUses, bool depleted, float fill) { hasItem = true; iconImage.gameObject.SetActive(true); iconImage.sprite = icon; bool showQty = quantity > 1; quantityText.gameObject.SetActive(showQty); if (showQty) quantityText.text = quantity.ToString(); if (usageBarRoot != null) usageBarRoot.SetActive(hasUses); Color tint = depleted ? depletedTint : normalTint; tint.a = iconImage.color.a; iconImage.color = tint; if (hasUses && usageBarFill != null) usageBarFill.fillAmount = fill; } /// /// Clears the cell to its empty look. /// private void RenderEmpty() { hasItem = false; iconImage.gameObject.SetActive(false); quantityText.gameObject.SetActive(false); if (usageBarRoot != null) usageBarRoot.SetActive(false); } /// /// Toggles the selection highlight (used by the hotbar for the active slot). /// public void SetSelected(bool selected) { if (highlightImage != null) highlightImage.gameObject.SetActive(selected); } #endregion #region Drag & Drop /// /// Starts dragging this cell's stack (left button, non-empty only), building the shared ghost /// from what the cell currently shows — container-agnostic, so inventory and chest cells drag /// identically. /// public void OnBeginDrag(PointerEventData eventData) { if (eventData.button != PointerEventData.InputButton.Left) return; if (!hasItem) return; draggedSlot = this; if (ghostObject != null) { ghostObject.SetActive(true); ghostIcon.sprite = iconImage.sprite; ghostIcon.gameObject.SetActive(true); bool showQty = quantityText.gameObject.activeSelf; ghostQuantity.gameObject.SetActive(showQty); if (showQty) ghostQuantity.text = quantityText.text; ghostObject.transform.position = eventData.position; } Color c = iconImage.color; c.a = 0.4f; iconImage.color = c; } /// /// Moves the ghost with the pointer. /// public void OnDrag(PointerEventData eventData) { if (draggedSlot != this) return; if (ghostObject != null) ghostObject.transform.position = eventData.position; } /// /// Ends the drag: hides the ghost and restores the source cell's opacity. /// public void OnEndDrag(PointerEventData eventData) { if (draggedSlot != this) return; if (ghostObject != null) ghostObject.SetActive(false); Color c = iconImage.color; c.a = 1f; iconImage.color = c; draggedSlot = null; } /// /// Drop target: reports the dragged and target (container, index) so the manager routes the /// transfer (swap, deposit, withdraw or move). /// public void OnDrop(PointerEventData eventData) { if (draggedSlot == null || draggedSlot == this) return; onDrop?.Invoke(draggedSlot.container, draggedSlot.slotIndex, container, slotIndex); } /// /// Reports a right-click so the manager can act (context menu in normal mode, quick transfer /// while a chest is open). /// public void OnPointerClick(PointerEventData eventData) { if (eventData.dragging) return; if (eventData.button == PointerEventData.InputButton.Right) onClicked?.Invoke(container, slotIndex, true); } /// /// Reports the hovered cell so the manager can show its description panel. Suppressed while a /// drag is in progress, where the panel would only get in the way. /// public void OnPointerEnter(PointerEventData eventData) { if (draggedSlot != null) return; onHoverEnter?.Invoke(container, slotIndex); } /// /// Reports that the pointer left the cell so the manager can hide the description panel. /// public void OnPointerExit(PointerEventData eventData) { onHoverExit?.Invoke(); } #endregion } }