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
}
}