(Feat) Add Build
This commit is contained in:
@@ -6,8 +6,26 @@ using System;
|
||||
|
||||
namespace Ashwild.Inventory
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public enum SlotContainer
|
||||
{
|
||||
Inventory,
|
||||
Chest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public class SlotUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IDropHandler, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private Image iconImage;
|
||||
[SerializeField] private TextMeshProUGUI quantityText;
|
||||
@@ -23,137 +41,197 @@ namespace Ashwild.Inventory
|
||||
[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 Action<int, int> onSwapRequested;
|
||||
private Action<int, bool> onClicked;
|
||||
private Action<int> onHoverEnter;
|
||||
private bool hasItem;
|
||||
|
||||
private Action<SlotContainer, int, SlotContainer, int> onDrop;
|
||||
private Action<SlotContainer, int, bool> onClicked;
|
||||
private Action<SlotContainer, int> onHoverEnter;
|
||||
private Action onHoverExit;
|
||||
|
||||
public int SlotIndex => slotIndex;
|
||||
public SlotContainer Container => container;
|
||||
|
||||
// Static drag state shared across all slots
|
||||
// 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
|
||||
|
||||
/// <summary>
|
||||
/// Wires the single shared drag ghost used by every slot cell in the scene.
|
||||
/// </summary>
|
||||
public static void SetupGhost(GameObject ghost, Image icon, TextMeshProUGUI qty)
|
||||
{
|
||||
ghostObject = ghost;
|
||||
ghostIcon = icon;
|
||||
ghostQuantity = qty;
|
||||
ghostObject.SetActive(false);
|
||||
if (ghostObject != null) ghostObject.SetActive(false);
|
||||
}
|
||||
|
||||
public void Initialize(int index, Action<int, int> swapCallback, Action<int, bool> clickCallback,
|
||||
Action<int> hoverEnterCallback = null, Action hoverExitCallback = null)
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void Initialize(SlotContainer slotContainer, int index,
|
||||
Action<SlotContainer, int, SlotContainer, int> dropCallback,
|
||||
Action<SlotContainer, int, bool> clickCallback,
|
||||
Action<SlotContainer, int> hoverEnterCallback = null,
|
||||
Action hoverExitCallback = null)
|
||||
{
|
||||
container = slotContainer;
|
||||
slotIndex = index;
|
||||
onSwapRequested = swapCallback;
|
||||
onDrop = dropCallback;
|
||||
onClicked = clickCallback;
|
||||
onHoverEnter = hoverEnterCallback;
|
||||
onHoverExit = hoverExitCallback;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rendering
|
||||
|
||||
/// <summary>
|
||||
/// Redraws the slot: icon, quantity, and the uses bar. Uses-tracked items (tools, multi-bite
|
||||
/// food) show a fill bar for their remaining uses and turn red once depleted.
|
||||
/// Redraws the cell from an inventory slot (the local, client-authoritative container).
|
||||
/// </summary>
|
||||
public void UpdateVisual(InventorySlot slot)
|
||||
{
|
||||
if (slot == null || slot.IsEmpty)
|
||||
{
|
||||
iconImage.gameObject.SetActive(false);
|
||||
quantityText.gameObject.SetActive(false);
|
||||
if (usageBarRoot != null) usageBarRoot.SetActive(false);
|
||||
RenderEmpty();
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
iconImage.gameObject.SetActive(true);
|
||||
iconImage.sprite = slot.ItemData.Icon;
|
||||
bool showQty = slot.Quantity > 1;
|
||||
quantityText.gameObject.SetActive(showQty);
|
||||
if (showQty)
|
||||
quantityText.text = slot.Quantity.ToString();
|
||||
|
||||
UpdateUsageBar(slot);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the uses bar and tints the icon red when the item is depleted; hides the bar entirely
|
||||
/// for items that do not track uses. Preserves the current icon alpha so a mid-drag fade stays.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private void UpdateUsageBar(InventorySlot slot)
|
||||
public void UpdateVisual(ItemData item, int quantity, int uses)
|
||||
{
|
||||
bool hasUses = slot.ItemData.HasUses;
|
||||
if (item == null)
|
||||
{
|
||||
RenderEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
if (usageBarRoot != null)
|
||||
usageBarRoot.SetActive(hasUses);
|
||||
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);
|
||||
}
|
||||
|
||||
Color tint = (hasUses && slot.IsDepleted) ? depletedTint : normalTint;
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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 = (float)slot.CurrentUses / slot.ItemData.MaxUses;
|
||||
if (hasUses && usageBarFill != null) usageBarFill.fillAmount = fill;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the cell to its empty look.
|
||||
/// </summary>
|
||||
private void RenderEmpty()
|
||||
{
|
||||
hasItem = false;
|
||||
iconImage.gameObject.SetActive(false);
|
||||
quantityText.gameObject.SetActive(false);
|
||||
if (usageBarRoot != null) usageBarRoot.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the selection highlight (used by the hotbar for the active slot).
|
||||
/// </summary>
|
||||
public void SetSelected(bool selected)
|
||||
{
|
||||
if (highlightImage != null)
|
||||
highlightImage.gameObject.SetActive(selected);
|
||||
}
|
||||
|
||||
// Drag & Drop
|
||||
#endregion
|
||||
|
||||
#region Drag & Drop
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void OnBeginDrag(PointerEventData eventData)
|
||||
{
|
||||
if (eventData.button != PointerEventData.InputButton.Left) return;
|
||||
|
||||
InventorySlot slot = PlayerInventory.Instance.GetSlot(slotIndex);
|
||||
if (slot == null || slot.IsEmpty) return;
|
||||
if (!hasItem) return;
|
||||
|
||||
draggedSlot = this;
|
||||
|
||||
// Show ghost
|
||||
if (ghostObject != null)
|
||||
{
|
||||
ghostObject.SetActive(true);
|
||||
ghostIcon.sprite = slot.ItemData.Icon;
|
||||
ghostIcon.sprite = iconImage.sprite;
|
||||
ghostIcon.gameObject.SetActive(true);
|
||||
|
||||
bool showQty = slot.Quantity > 1;
|
||||
bool showQty = quantityText.gameObject.activeSelf;
|
||||
ghostQuantity.gameObject.SetActive(showQty);
|
||||
if (showQty)
|
||||
ghostQuantity.text = slot.Quantity.ToString();
|
||||
if (showQty) ghostQuantity.text = quantityText.text;
|
||||
|
||||
ghostObject.transform.position = eventData.position;
|
||||
}
|
||||
|
||||
// Make source icon semi-transparent
|
||||
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;
|
||||
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;
|
||||
|
||||
// Hide ghost
|
||||
if (ghostObject != null)
|
||||
ghostObject.SetActive(false);
|
||||
if (ghostObject != null) ghostObject.SetActive(false);
|
||||
|
||||
// Restore icon opacity
|
||||
Color c = iconImage.color;
|
||||
c.a = 1f;
|
||||
iconImage.color = c;
|
||||
@@ -161,37 +239,46 @@ namespace Ashwild.Inventory
|
||||
draggedSlot = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drop target: reports the dragged and target (container, index) so the manager routes the
|
||||
/// transfer (swap, deposit, withdraw or move).
|
||||
/// </summary>
|
||||
public void OnDrop(PointerEventData eventData)
|
||||
{
|
||||
if (draggedSlot == null || draggedSlot == this) return;
|
||||
|
||||
onSwapRequested?.Invoke(draggedSlot.SlotIndex, slotIndex);
|
||||
onDrop?.Invoke(draggedSlot.container, draggedSlot.slotIndex, container, slotIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports a right-click so the manager can act (context menu in normal mode, quick transfer
|
||||
/// while a chest is open).
|
||||
/// </summary>
|
||||
public void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
if (eventData.dragging) return;
|
||||
|
||||
if (eventData.button == PointerEventData.InputButton.Right)
|
||||
onClicked?.Invoke(slotIndex, true);
|
||||
onClicked?.Invoke(container, slotIndex, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports the hovered slot index so the manager can show its description panel. Suppressed
|
||||
/// while a drag is in progress, where the panel would only get in the way.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
if (draggedSlot != null) return;
|
||||
onHoverEnter?.Invoke(slotIndex);
|
||||
onHoverEnter?.Invoke(container, slotIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports that the pointer left the slot so the manager can hide the description panel.
|
||||
/// Reports that the pointer left the cell so the manager can hide the description panel.
|
||||
/// </summary>
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
onHoverExit?.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user