215 lines
8.9 KiB
C#
215 lines
8.9 KiB
C#
using UnityEngine;
|
|
using System.Collections.Generic;
|
|
using DG.Tweening;
|
|
using Ashwild.Inventory;
|
|
using Ashwild.Player;
|
|
|
|
namespace Ashwild.UI
|
|
{
|
|
/// <summary>
|
|
/// Drives the on-screen item/recipe notification stack. Incoming notifications are queued and
|
|
/// released one at a time on a short delay, so a burst (e.g. one pickup unlocking several recipes)
|
|
/// reveals gradually instead of popping all at once, capped by a max on-screen count.
|
|
/// </summary>
|
|
public class NotificationUI : MonoBehaviour
|
|
{
|
|
[Header("References")]
|
|
[SerializeField] private RectTransform container;
|
|
[SerializeField] private GameObject notificationPrefab;
|
|
|
|
[Header("Layout")]
|
|
[SerializeField] private float notificationHeight = 40f;
|
|
[SerializeField] private float spacing = 6f;
|
|
[SerializeField] private float startOffsetY = 20f;
|
|
|
|
[Header("Timing")]
|
|
[SerializeField] private float displayDuration = 4f;
|
|
|
|
[Header("Animation")]
|
|
[SerializeField] private float slideInDuration = 0.35f;
|
|
[SerializeField] private Ease slideInEase = Ease.OutBack;
|
|
[SerializeField] private float stackSlideDuration = 0.25f;
|
|
[SerializeField] private Ease stackSlideEase = Ease.OutQuad;
|
|
|
|
[Header("Colors")]
|
|
[SerializeField] private Color gainColor = new Color(0.45f, 1f, 0.45f); // picked up / added
|
|
[SerializeField] private Color lossColor = new Color(1f, 0.45f, 0.45f); // dropped / consumed
|
|
|
|
[Header("Discovery")]
|
|
[Tooltip("Badge shown in the number slot when a new item/recipe is discovered.")]
|
|
[SerializeField] private string discoveryLabel = "NEW";
|
|
[SerializeField] private Color discoveryColor = new Color(1f, 0.85f, 0.3f);
|
|
|
|
[Header("Queue")]
|
|
[Tooltip("Minimum delay between two notifications appearing, so a burst reveals one at a time.")]
|
|
[SerializeField] private float spawnDelay = 0.5f;
|
|
[Tooltip("Maximum notifications visible on screen at once; the rest wait in the queue.")]
|
|
[SerializeField] private int maxActiveNotifications = 5;
|
|
[Tooltip("Maximum notifications waiting in the queue; beyond this the oldest pending one is dropped.")]
|
|
[SerializeField] private int maxQueuedNotifications = 20;
|
|
|
|
private readonly List<NotificationItemUI> activeNotifications = new List<NotificationItemUI>();
|
|
private readonly Queue<PendingNotification> pendingQueue = new Queue<PendingNotification>();
|
|
private float spawnCooldown;
|
|
|
|
/// <summary>
|
|
/// A notification waiting to be shown. Holds either an item gain/loss (item + signed quantity,
|
|
/// still mergeable while queued) or a recipe discovery (icon + name).
|
|
/// </summary>
|
|
private class PendingNotification
|
|
{
|
|
public bool isDiscovery;
|
|
public ItemData item;
|
|
public int signedQuantity;
|
|
public Sprite icon;
|
|
public string label;
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
PlayerEvents.ItemAdded += OnItemAdded;
|
|
PlayerEvents.ItemDropped += OnItemDropped;
|
|
PlayerEvents.ItemConsumed += OnItemConsumed;
|
|
PlayerEvents.RecipeDiscovered += OnRecipeDiscovered;
|
|
PlayerEvents.FoodPlacedToCook += OnItemSpent;
|
|
PlayerEvents.FuelAdded += OnItemSpent;
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
PlayerEvents.ItemAdded -= OnItemAdded;
|
|
PlayerEvents.ItemDropped -= OnItemDropped;
|
|
PlayerEvents.ItemConsumed -= OnItemConsumed;
|
|
PlayerEvents.RecipeDiscovered -= OnRecipeDiscovered;
|
|
PlayerEvents.FoodPlacedToCook -= OnItemSpent;
|
|
PlayerEvents.FuelAdded -= OnItemSpent;
|
|
}
|
|
|
|
private void OnItemAdded(ItemData item, int quantity) => Push(item, quantity);
|
|
private void OnItemDropped(ItemData item, int quantity) => Push(item, -quantity);
|
|
private void OnItemConsumed(ItemData item) => Push(item, -1);
|
|
private void OnRecipeDiscovered(Sprite icon, string recipeName) => PushDiscovery(icon, recipeName);
|
|
|
|
// Food placed on a cooking station or fuel fed to it both leave the inventory by one.
|
|
private void OnItemSpent(ItemData item) => Push(item, -1);
|
|
|
|
/// <summary>
|
|
/// Records an item gain/loss. Merges instantly into a matching active notification, then into a
|
|
/// matching one still queued, otherwise enqueues a new one to be released on the spawn delay.
|
|
/// </summary>
|
|
private void Push(ItemData item, int signedQuantity)
|
|
{
|
|
if (item == null || signedQuantity == 0) return;
|
|
|
|
activeNotifications.RemoveAll(n => n == null);
|
|
|
|
bool gain = signedQuantity > 0;
|
|
|
|
for (int i = 0; i < activeNotifications.Count; i++)
|
|
{
|
|
if (activeNotifications[i] != null && !activeNotifications[i].IsDiscovery
|
|
&& activeNotifications[i].ItemData == item
|
|
&& activeNotifications[i].IsGain == gain)
|
|
{
|
|
activeNotifications[i].AddQuantity(signedQuantity, displayDuration);
|
|
return;
|
|
}
|
|
}
|
|
|
|
foreach (PendingNotification pending in pendingQueue)
|
|
{
|
|
if (!pending.isDiscovery && pending.item == item && (pending.signedQuantity > 0) == gain)
|
|
{
|
|
pending.signedQuantity += signedQuantity;
|
|
return;
|
|
}
|
|
}
|
|
|
|
Enqueue(new PendingNotification { isDiscovery = false, item = item, signedQuantity = signedQuantity });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enqueues a one-time "NEW" notification for a newly unlocked recipe (its result icon and name
|
|
/// with the discovery badge). Never merges; released on the spawn delay like the rest.
|
|
/// </summary>
|
|
private void PushDiscovery(Sprite icon, string recipeName)
|
|
{
|
|
Enqueue(new PendingNotification { isDiscovery = true, icon = icon, label = recipeName });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a pending notification to the queue, dropping the oldest waiting one when the queue is
|
|
/// full so the most recent notifications are never lost.
|
|
/// </summary>
|
|
private void Enqueue(PendingNotification pending)
|
|
{
|
|
if (pendingQueue.Count >= maxQueuedNotifications)
|
|
pendingQueue.Dequeue();
|
|
pendingQueue.Enqueue(pending);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ticks the spawn cooldown and releases at most one queued notification per delay, while the
|
|
/// on-screen count is under the cap.
|
|
/// </summary>
|
|
private void Update()
|
|
{
|
|
if (spawnCooldown > 0f)
|
|
spawnCooldown -= Time.deltaTime;
|
|
|
|
if (pendingQueue.Count == 0 || spawnCooldown > 0f) return;
|
|
|
|
activeNotifications.RemoveAll(n => n == null);
|
|
if (activeNotifications.Count >= maxActiveNotifications) return;
|
|
|
|
Spawn(pendingQueue.Dequeue());
|
|
spawnCooldown = spawnDelay;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Instantiates and slides in a notification for a released pending entry, wiring it as either
|
|
/// an item gain/loss or a recipe discovery.
|
|
/// </summary>
|
|
private void Spawn(PendingNotification pending)
|
|
{
|
|
GameObject go = Instantiate(notificationPrefab, container);
|
|
NotificationItemUI notification = go.GetComponent<NotificationItemUI>();
|
|
|
|
float targetY = startOffsetY + activeNotifications.Count * (notificationHeight + spacing);
|
|
|
|
RectTransform rt = notification.RectTransform;
|
|
rt.anchoredPosition = new Vector2(0f, targetY - 30f);
|
|
|
|
if (pending.isDiscovery)
|
|
notification.InitializeDiscovery(pending.icon, pending.label, discoveryLabel, discoveryColor, displayDuration, slideInDuration);
|
|
else
|
|
notification.Initialize(pending.item, pending.signedQuantity, pending.signedQuantity > 0 ? gainColor : lossColor, displayDuration, slideInDuration, slideInEase);
|
|
|
|
rt.DOAnchorPosY(targetY, slideInDuration).SetEase(slideInEase);
|
|
|
|
activeNotifications.Add(notification);
|
|
}
|
|
|
|
private void LateUpdate()
|
|
{
|
|
// Clean up destroyed notifications and reposition remaining ones
|
|
int removed = activeNotifications.RemoveAll(n => n == null);
|
|
if (removed > 0)
|
|
RepositionAll();
|
|
}
|
|
|
|
private void RepositionAll()
|
|
{
|
|
for (int i = 0; i < activeNotifications.Count; i++)
|
|
{
|
|
if (activeNotifications[i] == null) continue;
|
|
|
|
float targetY = startOffsetY + i * (notificationHeight + spacing);
|
|
activeNotifications[i].RectTransform
|
|
.DOAnchorPosY(targetY, stackSlideDuration)
|
|
.SetEase(stackSlideEase);
|
|
}
|
|
}
|
|
}
|
|
}
|