246 lines
8.3 KiB
C#
246 lines
8.3 KiB
C#
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using UnityEngine.EventSystems;
|
|
using TMPro;
|
|
using Ashwild.Inventory;
|
|
|
|
namespace Ashwild.Storage
|
|
{
|
|
/// <summary>
|
|
/// Which container a chest-window cell belongs to, so a drop can be routed to the right transfer.
|
|
/// </summary>
|
|
public enum ChestSlotContainer
|
|
{
|
|
Inventory,
|
|
Chest
|
|
}
|
|
|
|
/// <summary>
|
|
/// One draggable cell in the chest window, used for both sides — the player's inventory (left) and
|
|
/// the chest (right). Unlike the inventory's SlotUI it carries which container and index it maps to,
|
|
/// so a drop hands the pair to the owning <see cref="ChestUI"/>, which turns it into a deposit,
|
|
/// withdraw, move-within or inventory swap. It only renders and reports drags; every real mutation
|
|
/// goes through the chest's server-authoritative RPCs.
|
|
/// </summary>
|
|
public class ChestSlotUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IDropHandler, IPointerClickHandler
|
|
{
|
|
#region Serialized Fields
|
|
|
|
[Header("References")]
|
|
[SerializeField] private Image iconImage;
|
|
[SerializeField] private TextMeshProUGUI quantityText;
|
|
|
|
[Header("Uses Bar")]
|
|
[Tooltip("Root 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;
|
|
[SerializeField] private Color normalTint = Color.white;
|
|
[SerializeField] private Color depletedTint = Color.red;
|
|
|
|
#endregion
|
|
|
|
#region State
|
|
|
|
private ChestUI owner;
|
|
private ChestSlotContainer container;
|
|
private int index;
|
|
|
|
public ChestSlotContainer Container => container;
|
|
public int Index => index;
|
|
|
|
// Static drag state shared across every chest-window cell (both grids).
|
|
private static ChestSlotUI draggedSlot;
|
|
private static GameObject ghostObject;
|
|
private static Image ghostIcon;
|
|
private static TextMeshProUGUI ghostQuantity;
|
|
|
|
#endregion
|
|
|
|
#region Setup
|
|
|
|
/// <summary>
|
|
/// Wires the single shared drag ghost used by every cell in the window.
|
|
/// </summary>
|
|
public static void SetupGhost(GameObject ghost, Image icon, TextMeshProUGUI qty)
|
|
{
|
|
ghostObject = ghost;
|
|
ghostIcon = icon;
|
|
ghostQuantity = qty;
|
|
if (ghostObject != null) ghostObject.SetActive(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Binds this cell to a container and index within the owning window.
|
|
/// </summary>
|
|
public void Initialize(ChestUI ownerUI, ChestSlotContainer slotContainer, int slotIndex)
|
|
{
|
|
owner = ownerUI;
|
|
container = slotContainer;
|
|
index = slotIndex;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Rendering
|
|
|
|
/// <summary>
|
|
/// Redraws the cell from whichever container it belongs to (the local inventory or the chest's
|
|
/// replicated view).
|
|
/// </summary>
|
|
public void Refresh()
|
|
{
|
|
if (container == ChestSlotContainer.Inventory)
|
|
{
|
|
PlayerInventory inv = PlayerInventory.Instance;
|
|
InventorySlot slot = inv != null ? inv.GetSlot(index) : null;
|
|
if (slot == null || slot.IsEmpty)
|
|
{
|
|
RenderEmpty();
|
|
return;
|
|
}
|
|
Render(slot.ItemData, slot.Quantity, slot.ItemData.HasUses ? slot.CurrentUses : -1);
|
|
}
|
|
else
|
|
{
|
|
if (owner != null && owner.Chest != null
|
|
&& owner.Chest.TryGetSlot(index, out ItemData item, out int qty, out int uses))
|
|
Render(item, qty, uses);
|
|
else
|
|
RenderEmpty();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the cell to its empty look.
|
|
/// </summary>
|
|
private void RenderEmpty()
|
|
{
|
|
iconImage.gameObject.SetActive(false);
|
|
quantityText.gameObject.SetActive(false);
|
|
if (usageBarRoot != null) usageBarRoot.SetActive(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws an item's icon, quantity and uses bar. Keeps the mid-drag fade on the cell currently
|
|
/// being dragged, but otherwise forces full opacity.
|
|
/// </summary>
|
|
private void Render(ItemData item, int quantity, int uses)
|
|
{
|
|
iconImage.gameObject.SetActive(true);
|
|
iconImage.sprite = item.Icon;
|
|
|
|
bool showQty = quantity > 1;
|
|
quantityText.gameObject.SetActive(showQty);
|
|
if (showQty) quantityText.text = quantity.ToString();
|
|
|
|
bool hasUses = item.HasUses;
|
|
if (usageBarRoot != null) usageBarRoot.SetActive(hasUses);
|
|
|
|
bool depleted = hasUses && uses <= 0;
|
|
Color tint = depleted ? depletedTint : normalTint;
|
|
tint.a = draggedSlot == this ? iconImage.color.a : 1f;
|
|
iconImage.color = tint;
|
|
|
|
if (hasUses && usageBarFill != null && item.MaxUses > 0)
|
|
usageBarFill.fillAmount = (float)Mathf.Max(0, uses) / item.MaxUses;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Drag & Drop
|
|
|
|
/// <summary>
|
|
/// Starts dragging this cell's stack (left button, non-empty only), showing the shared ghost.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the ghost with the pointer.
|
|
/// </summary>
|
|
public void OnDrag(PointerEventData eventData)
|
|
{
|
|
if (draggedSlot != this) return;
|
|
if (ghostObject != null) ghostObject.transform.position = eventData.position;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ends the drag: hides the ghost and restores the source cell's opacity.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Drop target: hands the dragged cell and this cell to the window to route the transfer.
|
|
/// </summary>
|
|
public void OnDrop(PointerEventData eventData)
|
|
{
|
|
if (draggedSlot == null || draggedSlot == this) return;
|
|
if (owner != null) owner.HandleTransfer(draggedSlot, this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Right-click: quick-transfers this cell to the other container (deposit or withdraw).
|
|
/// </summary>
|
|
public void OnPointerClick(PointerEventData eventData)
|
|
{
|
|
if (eventData.dragging) return;
|
|
if (eventData.button == PointerEventData.InputButton.Right && owner != null)
|
|
owner.HandleQuickTransfer(this);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Internal Helpers
|
|
|
|
/// <summary>
|
|
/// Whether this cell currently holds an item in its container.
|
|
/// </summary>
|
|
private bool HasItem()
|
|
{
|
|
if (container == ChestSlotContainer.Inventory)
|
|
{
|
|
PlayerInventory inv = PlayerInventory.Instance;
|
|
InventorySlot s = inv != null ? inv.GetSlot(index) : null;
|
|
return s != null && !s.IsEmpty;
|
|
}
|
|
return owner != null && owner.Chest != null && owner.Chest.TryGetSlot(index, out _, out _, out _);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|