Files
zeling_v2/Assets/_Game/Scripts/UI/HUD/QuestTrackerWidget.cs
2026-05-25 11:54:37 +08:00

224 lines
9.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 任务追踪 HUD 控件(架构 10_UIModule §HUD
/// 自动追踪最近开始的活跃任务,显示任务名称与各目标的完成进度。
/// 订阅 QuestEventChannelRegistry 中的广播频道,实现响应式更新。
///
/// Inspector 必填:
/// _questDatabase — 注册到游戏中的所有 QuestSO 列表(与 QuestManager._allQuests 保持一致)
/// _questTitleText — 显示任务标题的 TMP_Text
/// _objectiveRowTemplate — 目标行模板(保持非激活状态,用于实例化)
/// _objectiveContainer — 目标行父节点
/// 事件频道字段 — 从 QuestEventChannelRegistry 对应字段拖入
/// </summary>
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_QuestStartedpayload = questId")]
[SerializeField] private StringEventChannelSO _onQuestStarted;
[Tooltip("EVT_QuestCompletedpayload = questId")]
[SerializeField] private StringEventChannelSO _onQuestCompleted;
[Tooltip("EVT_QuestFailedpayload = questId")]
[SerializeField] private StringEventChannelSO _onQuestFailed;
[Tooltip("EVT_QuestAbandonedpayload = questId")]
[SerializeField] private StringEventChannelSO _onQuestAbandoned;
[Tooltip("EVT_QuestReadyToCompletepayload = questId")]
[SerializeField] private StringEventChannelSO _onQuestReadyToComplete;
[Tooltip("EVT_QuestObjectiveBatchUpdated同帧内多目标聚合更新")]
[SerializeField] private QuestObjectiveBatchEventChannelSO _onObjectiveBatchUpdated;
// ── 私有状态 ──────────────────────────────────────────────────────────
private string _trackedQuestId;
private QuestSO _trackedQuest;
private readonly List<GameObject> _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<string, (int Progress, int Required)> _progressCache =
new(System.StringComparer.Ordinal);
// ── 生命周期 ──────────────────────────────────────────────────────────
private void Awake()
{
gameObject.SetActive(false);
}
private void OnEnable()
{
// 延迟解析而非缓存到字段:支持 ServiceLocator 中途替换实现(测试/多场景)
_questManager = ServiceLocator.GetOrDefault<IQuestManager>();
_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<TMP_Text>(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;
}
}
}