(Feat) add crafting discovery + notification + inventory tool tips
This commit is contained in:
@@ -24,6 +24,10 @@ namespace Ashwild.Inventory
|
||||
[Header("Context Menu")]
|
||||
[SerializeField] private SlotContextMenu contextMenu;
|
||||
|
||||
[Header("Hover Description")]
|
||||
[Tooltip("Same panel used by the inventory — assign the shared HoverDescriptionUI here too.")]
|
||||
[SerializeField] private HoverDescriptionUI hoverDescription;
|
||||
|
||||
private PlayerInventory inventory;
|
||||
private Tweener scaleTween;
|
||||
private Tweener alphaTween;
|
||||
@@ -51,7 +55,7 @@ namespace Ashwild.Inventory
|
||||
{
|
||||
// Visual setup that does not need the player.
|
||||
for (int i = 0; i < hotbarSlot.Length; i++)
|
||||
hotbarSlot[i].Initialize(i, InventoryUI.OnSwapRequested, OnSlotClicked);
|
||||
hotbarSlot[i].Initialize(i, InventoryUI.OnSwapRequested, OnSlotClicked, OnSlotHoverEnter, OnSlotHoverExit);
|
||||
|
||||
transform.localScale = Vector3.one * idleScale;
|
||||
if (canvasGroup != null)
|
||||
@@ -167,6 +171,40 @@ namespace Ashwild.Inventory
|
||||
hotbarSlot[index].UpdateVisual(inventory.GetSlot(index));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the shared description panel for a hovered hotbar slot, but only while the inventory
|
||||
/// is open — during normal play the hotbar must not pop a description. The duration bar
|
||||
/// reflects the item's remaining uses/durability.
|
||||
/// </summary>
|
||||
private void OnSlotHoverEnter(int index)
|
||||
{
|
||||
if (hoverDescription == null || inventory == null || !PlayerEvents.IsInventoryOpen) return;
|
||||
|
||||
InventorySlot slot = inventory.GetSlot(index);
|
||||
if (slot == null || slot.IsEmpty)
|
||||
{
|
||||
hoverDescription.Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
ItemData item = slot.ItemData;
|
||||
bool hasDuration = item.HasUses;
|
||||
float fill = hasDuration ? (float)slot.CurrentUses / item.MaxUses : 0f;
|
||||
string durationText = hasDuration ? $"{slot.CurrentUses} / {item.MaxUses}" : string.Empty;
|
||||
|
||||
hoverDescription.Show(new ItemDescriptionView(
|
||||
item.Icon, item.ItemName, item.Description, hasDuration, fill, durationText));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides the description panel when the pointer leaves a hotbar slot.
|
||||
/// </summary>
|
||||
private void OnSlotHoverExit()
|
||||
{
|
||||
if (hoverDescription != null)
|
||||
hoverDescription.Hide();
|
||||
}
|
||||
|
||||
private void UpdateSelection(int selectedIndex)
|
||||
{
|
||||
for (int i = 0; i < hotbarSlot.Length; i++)
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using DG.Tweening;
|
||||
|
||||
namespace Ashwild.Inventory
|
||||
{
|
||||
/// <summary>
|
||||
/// The fixed description panel shown beside the inventory — a pure view. When the pointer
|
||||
/// enters a filled slot the inventory manager hands it an <see cref="ItemDescriptionView"/> and
|
||||
/// it fades its CanvasGroup in, drawing the icon, name, description and (for uses-tracked items)
|
||||
/// the duration bar. It never reads ItemData or the inventory itself; it renders what it is given.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class HoverDescriptionUI : MonoBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private CanvasGroup canvasGroup;
|
||||
[SerializeField] private Image iconImage;
|
||||
[SerializeField] private TextMeshProUGUI nameText;
|
||||
[SerializeField] private TextMeshProUGUI descriptionText;
|
||||
|
||||
[Header("Duration Bar")]
|
||||
[Tooltip("Root object of the duration/uses bar — shown only for items that track uses.")]
|
||||
[SerializeField] private GameObject durationBarRoot;
|
||||
[Tooltip("Filled image whose fillAmount maps to the remaining uses (Image Type = Filled).")]
|
||||
[SerializeField] private Image durationBarFill;
|
||||
[Tooltip("Label drawn over the bar, e.g. \"3 / 5\".")]
|
||||
[SerializeField] private TextMeshProUGUI durationText;
|
||||
|
||||
[Header("Animation")]
|
||||
[SerializeField] private float fadeDuration = 0.15f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private Tweener fadeTween;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Starts hidden so the panel only appears once a slot is actually hovered.
|
||||
/// </summary>
|
||||
private void Awake() => SetHidden();
|
||||
|
||||
/// <summary>
|
||||
/// Kills the fade tween so it never targets the CanvasGroup after teardown.
|
||||
/// </summary>
|
||||
private void OnDestroy() => fadeTween?.Kill();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Draws the payload and fades the panel in. Called by the inventory manager on hover enter.
|
||||
/// </summary>
|
||||
public void Show(ItemDescriptionView view)
|
||||
{
|
||||
if (iconImage != null)
|
||||
{
|
||||
iconImage.sprite = view.Icon;
|
||||
iconImage.enabled = view.Icon != null;
|
||||
}
|
||||
if (nameText != null)
|
||||
nameText.text = view.Name;
|
||||
if (descriptionText != null)
|
||||
descriptionText.text = view.Description;
|
||||
|
||||
if (durationBarRoot != null)
|
||||
durationBarRoot.SetActive(view.HasDuration);
|
||||
if (view.HasDuration)
|
||||
{
|
||||
if (durationBarFill != null)
|
||||
durationBarFill.fillAmount = view.DurationFill;
|
||||
if (durationText != null)
|
||||
durationText.text = view.DurationText;
|
||||
}
|
||||
|
||||
fadeTween?.Kill();
|
||||
fadeTween = canvasGroup.DOFade(1f, fadeDuration).SetUpdate(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fades the panel out via its CanvasGroup only — the GameObject stays active so it can be
|
||||
/// shown again instantly. Called on hover exit / inventory close.
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
fadeTween?.Kill();
|
||||
fadeTween = canvasGroup.DOFade(0f, fadeDuration).SetUpdate(true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Instantly parks the panel invisible via the CanvasGroup, without deactivating it.
|
||||
/// </summary>
|
||||
private void SetHidden() => canvasGroup.alpha = 0f;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 40050a905c9108244901dadab24daa28
|
||||
@@ -33,6 +33,9 @@ namespace Ashwild.Inventory
|
||||
[Header("Context Menu")]
|
||||
[SerializeField] private SlotContextMenu contextMenu;
|
||||
|
||||
[Header("Hover Description")]
|
||||
[SerializeField] private HoverDescriptionUI hoverDescription;
|
||||
|
||||
[Header("Categories")]
|
||||
[SerializeField] private InventoryCategoryManager categoryManager;
|
||||
|
||||
@@ -93,7 +96,7 @@ namespace Ashwild.Inventory
|
||||
int slotIndex = inventory.HotbarSize + i;
|
||||
GameObject slotGO = Instantiate(slotPrefab, slotContainer);
|
||||
SlotUI slotUI = slotGO.GetComponent<SlotUI>();
|
||||
slotUI.Initialize(slotIndex, OnSwapRequested, OnSlotClicked);
|
||||
slotUI.Initialize(slotIndex, OnSwapRequested, OnSlotClicked, OnSlotHoverEnter, OnSlotHoverExit);
|
||||
slotUIs[i] = slotUI;
|
||||
}
|
||||
|
||||
@@ -133,6 +136,9 @@ namespace Ashwild.Inventory
|
||||
if (contextMenu != null)
|
||||
contextMenu.Hide();
|
||||
|
||||
if (hoverDescription != null)
|
||||
hoverDescription.Hide();
|
||||
|
||||
if (categoryManager != null)
|
||||
categoryManager.Close();
|
||||
|
||||
@@ -192,6 +198,39 @@ namespace Ashwild.Inventory
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the description payload for the hovered slot and shows the side panel. Empty slots
|
||||
/// keep the panel hidden. The duration bar reflects the item's remaining uses/durability.
|
||||
/// </summary>
|
||||
private void OnSlotHoverEnter(int index)
|
||||
{
|
||||
if (hoverDescription == null || inventory == null) return;
|
||||
|
||||
InventorySlot slot = inventory.GetSlot(index);
|
||||
if (slot == null || slot.IsEmpty)
|
||||
{
|
||||
hoverDescription.Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
ItemData item = slot.ItemData;
|
||||
bool hasDuration = item.HasUses;
|
||||
float fill = hasDuration ? (float)slot.CurrentUses / item.MaxUses : 0f;
|
||||
string durationText = hasDuration ? $"{slot.CurrentUses} / {item.MaxUses}" : string.Empty;
|
||||
|
||||
hoverDescription.Show(new ItemDescriptionView(
|
||||
item.Icon, item.ItemName, item.Description, hasDuration, fill, durationText));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides the description panel when the pointer leaves a slot.
|
||||
/// </summary>
|
||||
private void OnSlotHoverExit()
|
||||
{
|
||||
if (hoverDescription != null)
|
||||
hoverDescription.Hide();
|
||||
}
|
||||
|
||||
private void RefreshSlot(int index)
|
||||
{
|
||||
if (slotUIs == null) return;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ashwild.Inventory
|
||||
{
|
||||
/// <summary>
|
||||
/// The view payload for the hover description panel: exactly what the panel renders (icon,
|
||||
/// name, description, and an optional duration/uses bar), nothing more. The inventory manager
|
||||
/// builds one of these from a hovered slot and pushes it into the view, so the panel never
|
||||
/// touches ItemData or the inventory model — it just draws what it is handed.
|
||||
/// </summary>
|
||||
public readonly struct ItemDescriptionView
|
||||
{
|
||||
public readonly Sprite Icon;
|
||||
public readonly string Name;
|
||||
public readonly string Description;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the hovered item tracks uses/durability and should show its duration bar.
|
||||
/// </summary>
|
||||
public readonly bool HasDuration;
|
||||
|
||||
/// <summary>
|
||||
/// Remaining uses as a 0..1 fill for the bar's Filled image.
|
||||
/// </summary>
|
||||
public readonly float DurationFill;
|
||||
|
||||
/// <summary>
|
||||
/// The remaining/max label drawn over the bar, e.g. "3 / 5".
|
||||
/// </summary>
|
||||
public readonly string DurationText;
|
||||
|
||||
public ItemDescriptionView(Sprite icon, string name, string description,
|
||||
bool hasDuration, float durationFill, string durationText)
|
||||
{
|
||||
Icon = icon;
|
||||
Name = name;
|
||||
Description = description;
|
||||
HasDuration = hasDuration;
|
||||
DurationFill = durationFill;
|
||||
DurationText = durationText;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b901ece22e48ee344837ad720d6f2e18
|
||||
@@ -6,7 +6,7 @@ using System;
|
||||
|
||||
namespace Ashwild.Inventory
|
||||
{
|
||||
public class SlotUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IDropHandler, IPointerClickHandler
|
||||
public class SlotUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IDropHandler, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private Image iconImage;
|
||||
@@ -26,6 +26,8 @@ namespace Ashwild.Inventory
|
||||
private int slotIndex;
|
||||
private Action<int, int> onSwapRequested;
|
||||
private Action<int, bool> onClicked;
|
||||
private Action<int> onHoverEnter;
|
||||
private Action onHoverExit;
|
||||
|
||||
public int SlotIndex => slotIndex;
|
||||
|
||||
@@ -43,11 +45,14 @@ namespace Ashwild.Inventory
|
||||
ghostObject.SetActive(false);
|
||||
}
|
||||
|
||||
public void Initialize(int index, Action<int, int> swapCallback, Action<int, bool> clickCallback)
|
||||
public void Initialize(int index, Action<int, int> swapCallback, Action<int, bool> clickCallback,
|
||||
Action<int> hoverEnterCallback = null, Action hoverExitCallback = null)
|
||||
{
|
||||
slotIndex = index;
|
||||
onSwapRequested = swapCallback;
|
||||
onClicked = clickCallback;
|
||||
onHoverEnter = hoverEnterCallback;
|
||||
onHoverExit = hoverExitCallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -170,5 +175,23 @@ namespace Ashwild.Inventory
|
||||
if (eventData.button == PointerEventData.InputButton.Right)
|
||||
onClicked?.Invoke(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.
|
||||
/// </summary>
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
if (draggedSlot != null) return;
|
||||
onHoverEnter?.Invoke(slotIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports that the pointer left the slot so the manager can hide the description panel.
|
||||
/// </summary>
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
onHoverExit?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user