224 lines
9.7 KiB
C#
224 lines
9.7 KiB
C#
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_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<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;
|
||
}
|
||
}
|
||
}
|