using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; using BaseGames.Core; using BaseGames.Core.Events; using BaseGames.Quest; using BaseGames.Localization; namespace BaseGames.UI.HUD { /// /// 任务追踪 HUD 控件(架构 10_UIModule §HUD)。 /// 自动追踪最近开始的活跃任务,显示任务名称与各目标的完成进度。 /// 订阅 QuestEventChannelRegistry 中的广播频道,实现响应式更新。 /// /// Inspector 必填: /// _questDatabase — 注册到游戏中的所有 QuestSO 列表(与 QuestManager._allQuests 保持一致) /// _questTitleText — 显示任务标题的 TMP_Text /// _objectiveRowTemplate — 目标行模板(保持非激活状态,用于实例化) /// _objectiveContainer — 目标行父节点 /// 事件频道字段 — 从 QuestEventChannelRegistry 对应字段拖入 /// public class QuestTrackerWidget : MonoBehaviour { // ── Inspector 字段 ──────────────────────────────────────────────────── [Header("UI 根节点")] [SerializeField] private TMP_Text _questTitleText; [SerializeField] private GameObject _objectiveRowTemplate; // kept inactive [SerializeField] private Transform _objectiveContainer; [Header("Event Channels")] [Tooltip("EVT_QuestStarted:payload = questId")] [SerializeField] private StringEventChannelSO _onQuestStarted; [Tooltip("EVT_QuestCompleted:payload = questId")] [SerializeField] private StringEventChannelSO _onQuestCompleted; [Tooltip("EVT_QuestFailed:payload = questId")] [SerializeField] private StringEventChannelSO _onQuestFailed; [Tooltip("EVT_QuestAbandoned:payload = questId")] [SerializeField] private StringEventChannelSO _onQuestAbandoned; [Tooltip("EVT_QuestReadyToComplete:payload = questId")] [SerializeField] private StringEventChannelSO _onQuestReadyToComplete; [Tooltip("EVT_QuestObjectiveBatchUpdated:同帧内多目标聚合更新")] [SerializeField] private QuestObjectiveBatchEventChannelSO _onObjectiveBatchUpdated; // ── 私有状态 ────────────────────────────────────────────────────────── private string _trackedQuestId; private QuestSO _trackedQuest; private readonly List _rowPool = new(); private readonly List<(TMP_Text label, TMP_Text progress)> _activeRows = new(); private readonly CompositeDisposable _subs = new(); // IQuestManager 在 OnEnable 时从 ServiceLocator 解析,消除对 _questDatabase 数组的重复注入需求 private IQuestManager _questManager; // ── 进度字典:objectiveId → (Progress, Required) ───────────────────── private readonly Dictionary _progressCache = new(System.StringComparer.Ordinal); // ── 生命周期 ────────────────────────────────────────────────────────── private void Awake() { gameObject.SetActive(false); } private void OnEnable() { // 延迟解析而非缓存到字段:支持 ServiceLocator 中途替换实现(测试/多场景) _questManager = ServiceLocator.GetOrDefault(); _onQuestStarted?.Subscribe(OnQuestStarted).AddTo(_subs); _onQuestCompleted?.Subscribe(OnQuestEnded).AddTo(_subs); _onQuestFailed?.Subscribe(OnQuestEnded).AddTo(_subs); _onQuestAbandoned?.Subscribe(OnQuestEnded).AddTo(_subs); _onQuestReadyToComplete?.Subscribe(OnQuestReadyToComplete).AddTo(_subs); _onObjectiveBatchUpdated?.Subscribe(OnObjectiveBatchUpdated).AddTo(_subs); } private void OnDisable() { _subs.Clear(); } // ── 事件处理 ────────────────────────────────────────────────────────── private void OnQuestStarted(string questId) { // 自动追踪最新开始的任务 _trackedQuestId = questId; // 通过 IQuestManager 接口查找 QuestSO,无需在 HUD 中重复注入数据库 _questManager?.TryGetQuest(questId, out _trackedQuest); _progressCache.Clear(); Rebuild(); } private void OnQuestEnded(string questId) { if (questId != _trackedQuestId) return; _trackedQuestId = null; _trackedQuest = null; _progressCache.Clear(); gameObject.SetActive(false); } private void OnQuestReadyToComplete(string questId) { if (questId == _trackedQuestId) { // 可选:更改标题颜色以提示玩家交任务 if (_questTitleText != null) _questTitleText.color = Color.yellow; } } private void OnObjectiveBatchUpdated(QuestObjectiveBatchEvent batch) { if (batch.QuestId != _trackedQuestId) return; if (batch.Updates == null) return; foreach (var update in batch.Updates) _progressCache[update.ObjectiveId] = (update.Progress, update.Required); RefreshObjectiveRows(); } // ── UI 重建 ─────────────────────────────────────────────────────────── private void Rebuild() { if (string.IsNullOrEmpty(_trackedQuestId)) { gameObject.SetActive(false); return; } gameObject.SetActive(true); // 标题 if (_questTitleText != null) { _questTitleText.color = Color.white; if (_trackedQuest != null && !string.IsNullOrEmpty(_trackedQuest.displayNameKey)) { string title = LocalizationManager.Get(_trackedQuest.displayNameKey, LocalizationTable.Quest); _questTitleText.text = string.IsNullOrEmpty(title) || title == _trackedQuest.displayNameKey ? _trackedQuestId : title; } else { _questTitleText.text = _trackedQuestId; } } // 返还所有行到对象池 foreach (var row in _activeRows) if (row.label != null) row.label.transform.parent.gameObject.SetActive(false); _activeRows.Clear(); // 生成目标行 if (_trackedQuest?.objectives == null) return; foreach (var obj in _trackedQuest.objectives) { if (obj == null) continue; var rowGo = GetOrCreateRow(); var texts = rowGo.GetComponentsInChildren(includeInactive: true); TMP_Text labelText = texts.Length > 0 ? texts[0] : null; TMP_Text progressText = texts.Length > 1 ? texts[1] : null; if (labelText != null) { string objText = LocalizationManager.Get(obj.displayTextKey, LocalizationTable.Quest); labelText.text = string.IsNullOrEmpty(objText) || objText == obj.displayTextKey ? obj.objectiveId : objText; } _progressCache.TryGetValue(obj.objectiveId, out var cached); int prog = cached.Progress; int req = cached.Required > 0 ? cached.Required : obj.GetRequiredCount(); if (progressText != null) progressText.text = req > 1 ? $"{prog}/{req}" : string.Empty; rowGo.SetActive(true); _activeRows.Add((labelText, progressText)); } } private void RefreshObjectiveRows() { if (_trackedQuest?.objectives == null) return; for (int i = 0; i < _activeRows.Count && i < _trackedQuest.objectives.Length; i++) { var obj = _trackedQuest.objectives[i]; if (obj == null) continue; _progressCache.TryGetValue(obj.objectiveId, out var cached); int prog = cached.Progress; int req = cached.Required > 0 ? cached.Required : obj.GetRequiredCount(); var (_, progressText) = _activeRows[i]; if (progressText != null) progressText.text = req > 1 ? $"{prog}/{req}" : string.Empty; } } // ── 对象池 ──────────────────────────────────────────────────────────── private GameObject GetOrCreateRow() { foreach (var r in _rowPool) if (r != null && !r.activeSelf) return r; var go = Instantiate(_objectiveRowTemplate, _objectiveContainer); _rowPool.Add(go); return go; } } }