Files
Cielonos/Assets/Scripts/MainGame/UI/Common/NotificationUIArea.cs
SoulliesOfficial 39b43680a9 爆更
2026-07-18 03:16:20 -04:00

421 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using Cielonos.MainGame;
using Cielonos.MainGame.Inventory;
using Cielonos.MainGame.Inventory.Collections;
using Sirenix.OdinInspector;
using SLSUtilities.General;
using SLSUtilities.UI;
using UnityEngine;
namespace Cielonos.MainGame.UI
{
public enum NotificationType
{
ItemObtained,
CurrencyChanged,
BattleCompleted,
Warning,
System
}
[Serializable]
public struct NotificationData
{
public NotificationType type;
public string title;
public string message;
public Sprite icon;
public Color accentColor;
public float duration;
public NotificationData(
NotificationType type,
string title,
string message,
Sprite icon,
Color accentColor,
float duration)
{
this.type = type;
this.title = title;
this.message = message;
this.icon = icon;
this.accentColor = accentColor;
this.duration = duration;
}
}
/// <summary>
/// 右上角通用提示队列。该 UI 不阻塞输入,也不参与 UIPageManager 的页面栈。
/// </summary>
public class NotificationUIArea : UIElementBase
{
public static NotificationUIArea Current { get; private set; }
[SerializeField] private NotificationItem itemPrefab;
[SerializeField] private RectTransform itemContainer;
[SerializeField] private BattleCompletionBanner battleCompletionBanner;
[SerializeField, Min(1)] private int maxVisible = 4;
[SerializeField, Min(1f)] private float itemHeight = 100f;
[SerializeField, Min(0f)] private float itemSpacing = 10f;
[SerializeField, Min(0f)] private float defaultDuration = 3f;
[SerializeField, Min(0f)] private float enterDuration = 0.25f;
[SerializeField, Min(0f)] private float moveDuration = 0.2f;
[SerializeField, Min(0f)] private float exitDuration = 0.2f;
[SerializeField, Min(0f)] private float queueWaitTime = 0.12f;
[SerializeField] private Color defaultAccentColor = new Color(0.7f, 0.85f, 1f, 1f);
[SerializeField] private Color currencyAccentColor = new Color(1f, 0.82f, 0.35f, 1f);
[SerializeField] private Color warningAccentColor = new Color(1f, 0.4f, 0.35f, 1f);
[SerializeField] private string battleCompletedMessage = "战斗结束";
private readonly List<NotificationItem> activeItems = new();
private readonly Queue<NotificationData> pendingItems = new();
private bool roomClearedSubscribed;
private float nextSpawnTime;
private float StackStep => itemHeight + itemSpacing;
private void Awake()
{
Current = this;
rectTransform ??= GetComponent<RectTransform>();
canvasGroup ??= GetComponent<CanvasGroup>();
itemContainer ??= rectTransform;
battleCompletionBanner ??= GetComponentInChildren<BattleCompletionBanner>(true);
if (canvasGroup != null)
{
canvasGroup.alpha = 1f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
}
private void Start()
{
gameObject.SetActive(true);
TrySubscribeRoomCleared();
}
private void OnEnable()
{
Current = this;
TrySubscribeRoomCleared();
}
private void OnDestroy()
{
if (Current == this)
{
Current = null;
}
UnsubscribeRoomCleared();
}
private void Update()
{
if (!roomClearedSubscribed)
{
TrySubscribeRoomCleared();
}
ProcessPendingItems();
}
public void Push(NotificationData data)
{
if (itemPrefab == null || itemContainer == null)
{
Debug.LogWarning("[NotificationUIArea] itemPrefab 或 itemContainer 未配置。");
return;
}
EnsureAreaVisible();
pendingItems.Enqueue(PrepareData(data));
ProcessPendingItems();
}
public void PushBattleCompleted()
{
if (battleCompletionBanner != null)
{
battleCompletionBanner.Show(battleCompletedMessage);
return;
}
Push(new NotificationData(
NotificationType.BattleCompleted,
"战斗完成",
battleCompletedMessage,
null,
defaultAccentColor,
defaultDuration));
}
[TitleGroup("Debug")]
[Button("生成测试 Notification")]
private void PushTestNotification()
{
Push(new NotificationData(
NotificationType.System,
"测试通知",
$"Notification Test {DateTime.Now:HH:mm:ss}",
null,
defaultAccentColor,
defaultDuration));
}
public void PushItemObtained(ItemBase item, int amount = 1)
{
if (item == null)
{
return;
}
Push(CreateItemObtainedData(item, amount));
}
public void PushRareMaterial(int amount, string title = "奖励结算")
{
string message = amount > 0
? $"获得 RareMaterial x{amount}"
: "未获得 RareMaterial";
Push(new NotificationData(
NotificationType.CurrencyChanged,
title,
message,
null,
currencyAccentColor,
defaultDuration));
}
public void PushWarning(string title, string message)
{
Push(new NotificationData(
NotificationType.Warning,
title,
message,
null,
warningAccentColor,
defaultDuration));
}
public static NotificationData CreateItemObtainedData(ItemBase item, int amount = 1)
{
ContentData data = item?.contentData;
if (item is PrefabricatedComponent component && component.targetEquipment != null)
{
data = component.targetEquipment.contentData;
}
string itemName = data != null && !string.IsNullOrEmpty(data.displayNameKey)
? data.displayNameKey.Localize("Items")
: item != null ? item.name : "Unknown Item";
bool isUpgrade = item is PrefabricatedComponent;
string title = isUpgrade ? "装备升级" : "获得物品";
string message = isUpgrade
? $"升级:{itemName} x{Mathf.Max(1, amount)}"
: $"{itemName} x{Mathf.Max(1, amount)}";
return new NotificationData(
NotificationType.ItemObtained,
title,
message,
data?.itemIcon,
GetItemAccentColor(data),
3f);
}
private static Color GetItemAccentColor(ContentData data)
{
return data != null
? RewardUIFormatter.GetRarityColor(data.itemRarity)
: new Color(0.7f, 0.85f, 1f, 1f);
}
private NotificationData PrepareData(NotificationData data)
{
if (data.accentColor.a <= 0f)
{
data.accentColor = defaultAccentColor;
}
if (data.duration <= 0f)
{
data.duration = defaultDuration;
}
return data;
}
private void ProcessPendingItems()
{
if (pendingItems.Count == 0 || itemPrefab == null || itemContainer == null || maxVisible < 1)
{
return;
}
if (Time.unscaledTime < nextSpawnTime)
{
return;
}
SpawnItem(pendingItems.Dequeue());
nextSpawnTime = Time.unscaledTime + queueWaitTime;
}
private void SpawnItem(NotificationData data)
{
CleanupDestroyedItems();
while (activeItems.Count >= maxVisible)
{
RemoveOldest();
}
NotificationItem item = Instantiate(itemPrefab, itemContainer);
if (item == null)
{
return;
}
activeItems.Insert(0, item);
item.Setup(data, defaultDuration);
ApplyLayout(true);
item.PlayIn(GetTargetPosition(0), enterDuration, () => Remove(item));
}
private void ApplyLayout(bool animate)
{
CleanupDestroyedItems();
for (int i = 0; i < activeItems.Count; i++)
{
NotificationItem item = activeItems[i];
if (item == null)
{
continue;
}
Vector2 target = GetTargetPosition(i);
if (animate && i > 0)
{
item.MoveTo(target, moveDuration);
}
else
{
item.SetPosition(target);
}
}
}
private Vector2 GetTargetPosition(int index)
{
// itemContainer 应锚定在右上角。新提示位于堆栈底部,旧提示向上移动。
Rect rect = itemContainer != null
? itemContainer.rect
: new Rect(0f, 0f, 0f, maxVisible * StackStep);
float pivotY = itemContainer != null ? itemContainer.pivot.y : 0.5f;
float bottom = -rect.height * pivotY;
float baseline = bottom + itemHeight * 0.5f;
return new Vector2(0f, baseline + index * StackStep);
}
private void RemoveOldest()
{
int lastIndex = activeItems.Count - 1;
if (lastIndex < 0)
{
return;
}
NotificationItem oldest = activeItems[lastIndex];
activeItems.RemoveAt(lastIndex);
if (oldest != null)
{
oldest.DismissImmediate(null);
}
}
private void Remove(NotificationItem item)
{
if (item == null)
{
return;
}
if (!activeItems.Remove(item))
{
return;
}
item.PlayOut(null, exitDuration);
ApplyLayout(true);
}
private void CleanupDestroyedItems()
{
activeItems.RemoveAll(item => item == null);
}
private void EnsureAreaVisible()
{
if (!gameObject.activeSelf)
{
gameObject.SetActive(true);
}
if (canvasGroup == null)
{
return;
}
canvasGroup.alpha = 1f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
private void TrySubscribeRoomCleared()
{
if (roomClearedSubscribed || RunManager.Instance == null)
{
return;
}
RunManager.Instance.OnCombatRoomCleared += HandleCombatRoomCleared;
roomClearedSubscribed = true;
}
private void UnsubscribeRoomCleared()
{
if (!roomClearedSubscribed || RunManager.Instance == null)
{
roomClearedSubscribed = false;
return;
}
RunManager.Instance.OnCombatRoomCleared -= HandleCombatRoomCleared;
roomClearedSubscribed = false;
}
private void HandleCombatRoomCleared(RunMapNode node)
{
PushBattleCompleted();
}
public static NotificationUIArea Resolve()
{
if (Current != null)
{
return Current;
}
Current = FindFirstObjectByType<NotificationUIArea>(FindObjectsInactive.Include);
return Current;
}
}
}