using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; using BaseGames.Core.Events; using BaseGames.Combat.StatusEffects; using StatusEffectType = BaseGames.Combat.StatusEffects.StatusEffectType; namespace BaseGames.UI.HUD { /// /// 状态效果 HUD —— 在 HUD 上为玩家展示当前激活的状态效果图标、叠层数和倒计时。 /// /// 布局约定:挂在 HUD Canvas 内,持有一个 _slotTemplate(默认 inactive)和一个 _container。 /// 每种 StatusEffectType 最多对应一个槽位实例,通过字典管理。 /// /// 订阅的事件频道: /// _onStatusEffectApplied → OnStatusEffectApplied /// _onStatusEffectExpired → OnStatusEffectExpired /// public class StatusEffectHUD : MonoBehaviour { // ── Inspector 字段 ──────────────────────────────────────────────────── [Header("Slot")] [Tooltip("槽位模板预制(设为 inactive);每种效果类型会从此 Instantiate 一个实例。")] [SerializeField] private GameObject _slotTemplate; [Tooltip("槽位实例的父节点(水平/垂直 LayoutGroup 即可)。")] [SerializeField] private Transform _container; [Header("效果图标配置")] [SerializeField] private SlotConfig[] _slotConfigs; [Header("Event Channels")] [SerializeField] private StatusEffectEventChannelSO _onStatusEffectApplied; [SerializeField] private StatusEffectEventChannelSO _onStatusEffectExpired; // ── 内部数据 ────────────────────────────────────────────────────────── /// 每种 StatusEffectType 对应的 Inspector 配置(图标 Sprite)。 [System.Serializable] public struct SlotConfig { public StatusEffectType effectType; [Tooltip("该状态效果在 HUD 上显示的图标。")] public Sprite icon; } private class SlotInstance { public GameObject root; public Image iconImage; public TMP_Text durationText; // 倒计时文本(可为 null) public TMP_Text stackText; // 叠层数文本(可为 null) public Coroutine countdown; } private readonly Dictionary _activeSlots = new(); private readonly CompositeDisposable _subs = new(); // 倒计时节流:缓存 WaitForSecondsRealtime 以避免每帧堆分配 private readonly WaitForSecondsRealtime _countdownTick = new(0.1f); // 图标快速查找表: Awake 时一次性从 _slotConfigs 构建,避免 OnStatusEffectApplied 高频线性扫描 private Dictionary _iconLookup; // ── 生命周期 ─────────────────────────────────────────────────────── private void Awake() { _iconLookup = new Dictionary(_slotConfigs?.Length ?? 0); if (_slotConfigs != null) foreach (var cfg in _slotConfigs) _iconLookup[cfg.effectType] = cfg.icon; } private void OnEnable() { _onStatusEffectApplied?.Subscribe(OnStatusEffectApplied).AddTo(_subs); _onStatusEffectExpired?.Subscribe(OnStatusEffectExpired).AddTo(_subs); } private void OnDisable() { _subs.Clear(); StopAllCoroutines(); foreach (var slot in _activeSlots.Values) if (slot.root != null) slot.root.SetActive(false); _activeSlots.Clear(); } // ── 事件处理 ────────────────────────────────────────────────────────── private void OnStatusEffectApplied(StatusEffectEvent e) { if (!_activeSlots.TryGetValue(e.EffectType, out var slot)) { slot = CreateSlot(e.EffectType); if (slot == null) return; _activeSlots[e.EffectType] = slot; } slot.root.SetActive(true); RefreshSlotDisplay(slot, e.StackCount, e.RemainingDuration); // 重启倒计时协程(OnStack 刷新持续时间时也会走到这里) if (slot.countdown != null) StopCoroutine(slot.countdown); slot.countdown = StartCoroutine(CountdownRoutine(slot, e.RemainingDuration)); } private void OnStatusEffectExpired(StatusEffectEvent e) { if (!_activeSlots.TryGetValue(e.EffectType, out var slot)) return; if (slot.countdown != null) { StopCoroutine(slot.countdown); slot.countdown = null; } slot.root.SetActive(false); } // ── 协程 ───────────────────────────────────────────────────────────── private IEnumerator CountdownRoutine(SlotInstance slot, float totalDuration) { float endTime = Time.realtimeSinceStartup + totalDuration; while (Time.realtimeSinceStartup < endTime) { float remaining = endTime - Time.realtimeSinceStartup; if (slot.durationText != null) slot.durationText.text = remaining.ToString("F1"); yield return _countdownTick; // 0.1s 节忍,替代每帧 yield return null } slot.countdown = null; slot.root.SetActive(false); } // ── 内部辅助 ────────────────────────────────────────────────────────── private void RefreshSlotDisplay(SlotInstance slot, int stackCount, float duration) { if (slot.durationText != null) slot.durationText.text = duration.ToString("F1"); bool multiStack = stackCount > 1; if (slot.stackText != null) { slot.stackText.gameObject.SetActive(multiStack); if (multiStack) slot.stackText.text = stackCount.ToString(); } } private SlotInstance CreateSlot(StatusEffectType type) { if (_slotTemplate == null || _container == null) return null; _iconLookup.TryGetValue(type, out Sprite icon); // O(1) 查找 var go = Instantiate(_slotTemplate, _container); go.SetActive(false); var slot = new SlotInstance { root = go }; // 约定:第一个 Image 子节点作为图标;第一个 TMP_Text 作为倒计时;第二个作为叠层数 var images = go.GetComponentsInChildren(includeInactive: true); if (images.Length > 0) { slot.iconImage = images[0]; if (icon != null) slot.iconImage.sprite = icon; } var texts = go.GetComponentsInChildren(includeInactive: true); if (texts.Length > 0) slot.durationText = texts[0]; if (texts.Length > 1) slot.stackText = texts[1]; return slot; } } }