using System.Collections.Generic; using UnityEngine; using BaseGames.Core.Events; using BaseGames.Core.Save; using QuestStateEnum = BaseGames.Core.Events.QuestState; namespace BaseGames.Quest { /// /// 运行时任务管理器(架构 22_QuestChallengeModule §5)。 /// 挂在 Persistent 场景 [GameManagers] 下。 /// 事件驱动追踪目标进度,不主动轮询。 /// 实现 ISaveable,通过 SaveManager 持久化任务状态。 /// /// _allQuests 由编辑器 OnValidate / "刷新任务列表" 右键菜单自动填充, /// 无需策划人员手动拖入 ScriptableObject。 /// public class QuestManager : MonoBehaviour, ISaveable, IQuestManager { // ── Inspector ──────────────────────────────────────────────────────── [Tooltip("所有 QuestSO 资产。编辑器会自动同步,无需手动维护。")] [SerializeField] private QuestSO[] _allQuests; [Header("Event Channels(监听)")] [SerializeField] private StringEventChannelSO _onEnemyDied; // EVT_EnemyDied(enemyId) [SerializeField] private StringEventChannelSO _onCollectiblePickup; // EVT_CollectiblePickup(itemId) [SerializeField] private StringEventChannelSO _onSceneLoaded; // EVT_SceneLoaded(sceneName) [SerializeField] private StringEventChannelSO _onNpcDialogueCompleted; // EVT_NpcDialogueCompleted(npcId) [SerializeField] private StringEventChannelSO _onSkillUsed; // EVT_SkillUsed(abilityType.ToString()) [Header("Event Channels(广播)")] [SerializeField] private StringEventChannelSO _onQuestStarted; // questId [SerializeField] private StringEventChannelSO _onQuestCompleted; // questId [SerializeField] private StringEventChannelSO _onQuestFailed; // questId [SerializeField] private QuestObjectiveEventChannelSO _onObjectiveUpdated; // ── Runtime State ──────────────────────────────────────────────────── private readonly Dictionary _questStates = new(); private readonly Dictionary _objectiveStates = new(); /// questId → QuestSO 快速查找表(由 Awake 构建,将 GetQuestSO O(n) 降为 O(1))。 private Dictionary _questIndex; private readonly CompositeDisposable _subs = new(); public StringEventChannelSO OnQuestStarted => _onQuestStarted; public StringEventChannelSO OnQuestCompleted => _onQuestCompleted; /// 供 SaveManager 迭代的任务状态字典(只读视图)。 public IReadOnlyDictionary QuestStates => _questStates; private void Awake() { if (BaseGames.Core.ServiceLocator.GetOrDefault() != null) { Destroy(gameObject); return; } BaseGames.Core.ServiceLocator.Register(this); // 构建任务字典索引,将 GetQuestSO 变为 O(1) _questIndex = new Dictionary(_allQuests?.Length ?? 0); if (_allQuests != null) foreach (var q in _allQuests) if (q != null && !string.IsNullOrEmpty(q.questId)) _questIndex[q.questId] = q; #if UNITY_EDITOR || DEVELOPMENT_BUILD ValidateQuestIds(); #endif } #if UNITY_EDITOR || DEVELOPMENT_BUILD private void ValidateQuestIds() { if (_allQuests == null) return; var seen = new HashSet(System.StringComparer.Ordinal); foreach (var q in _allQuests) { if (q == null) continue; if (string.IsNullOrEmpty(q.questId)) Debug.LogError($"[QuestManager] QuestSO '{q.name}' 缺少 questId,此任务将无法被引用。", q); else if (!seen.Add(q.questId)) Debug.LogError($"[QuestManager] 重复的 questId '{q.questId}'(资产:{q.name}),将导致任务系统异常。", q); } } #endif private void OnEnable() { _onEnemyDied?.Subscribe(HandleEnemyDefeated).AddTo(_subs); _onCollectiblePickup?.Subscribe(HandleItemCollected).AddTo(_subs); _onSceneLoaded?.Subscribe(HandleSceneLoaded).AddTo(_subs); _onNpcDialogueCompleted?.Subscribe(HandleNpcDialogue).AddTo(_subs); _onSkillUsed?.Subscribe(HandleSkillUsed).AddTo(_subs); BaseGames.Core.ServiceLocator.GetOrDefault()?.Register(this); } private void OnDisable() { _subs.Clear(); BaseGames.Core.ServiceLocator.GetOrDefault()?.Unregister(this); } private void OnDestroy() { BaseGames.Core.ServiceLocator.Unregister(this); } // ── 公共 API ────────────────────────────────────────────────────────── /// NPC 接受任务时调用。 public void AcceptQuest(string questId) { if (!CanAccept(questId)) return; _questStates[questId] = QuestStateEnum.Active; _onQuestStarted?.Raise(questId); } /// NPC 完成任务时调用。 public void CompleteQuest(string questId, IRewardTarget rewardTarget) { if (!IsReadyToComplete(questId)) return; var quest = GetQuestSO(questId); quest.reward?.Apply(rewardTarget); _questStates[questId] = QuestStateEnum.Completed; _onQuestCompleted?.Raise(questId); // 解锁后续任务(分支) // conditionQuest == null 表示默认分支,conditionQuest != null 则要求该任务已完成。 // 不 break —— 允许同时解锁多个后续任务(如完成任务后同时开放多条支线)。 if (quest.branches != null) { foreach (var branch in quest.branches) { if (branch.conditionQuest == null || GetState(branch.conditionQuest.questId) == QuestStateEnum.Completed) { if (branch.nextQuest != null) _questStates[branch.nextQuest.questId] = QuestStateEnum.Available; } } } } public QuestStateEnum GetState(string questId) => _questStates.TryGetValue(questId, out var s) ? s : QuestStateEnum.Unavailable; public bool IsReadyToComplete(string questId) { var quest = GetQuestSO(questId); if (quest == null || GetState(questId) != QuestStateEnum.Active) return false; if (quest.objectives == null) return true; foreach (var obj in quest.objectives) { if (!obj.IsOptional && !IsObjectiveComplete(obj)) return false; } return true; } // ── ISaveable ───────────────────────────────────────────────────────── public void OnSave(SaveData data) { data.Quests.QuestStates.Clear(); foreach (var (id, state) in _questStates) { data.Quests.QuestStates[id] = new BaseGames.Core.Save.QuestState { Status = state.ToString(), ProgressCounts = BuildProgressList(id), }; } } public void OnLoad(SaveData data) { _questStates.Clear(); _objectiveStates.Clear(); foreach (var (id, saved) in data.Quests.QuestStates) { if (System.Enum.TryParse(saved.Status, out var parsedState)) _questStates[id] = parsedState; // 恢复各目标进度 var quest = GetQuestSO(id); if (quest?.objectives != null && saved.ProgressCounts != null) { for (int i = 0; i < quest.objectives.Length && i < saved.ProgressCounts.Count; i++) { var obj = quest.objectives[i]; if (obj == null) continue; if (!_objectiveStates.TryGetValue(obj.objectiveId, out var os)) os = _objectiveStates[obj.objectiveId] = new QuestObjectiveState(); os.progressCount = saved.ProgressCounts[i]; } } } } // ── 私有辅助 ───────────────────────────────────────────────────────── private bool CanAccept(string questId) { if (GetState(questId) != QuestStateEnum.Available) return false; var quest = GetQuestSO(questId); if (quest?.prerequisiteQuests == null) return true; foreach (var pre in quest.prerequisiteQuests) { if (pre == null) continue; if (GetState(pre.questId) != QuestStateEnum.Completed) return false; } return true; } private bool IsObjectiveComplete(QuestObjectiveSO obj) { if (!_objectiveStates.TryGetValue(obj.objectiveId, out var s)) s = new QuestObjectiveState(); bool result = obj.EvaluateCompletion(s); // 首次达成时写回 completed 标志,避免 s 是本地临时对象时标志丢失 if (result && !s.completed) { s.completed = true; _objectiveStates[obj.objectiveId] = s; } return result; } private List BuildProgressList(string questId) { var quest = GetQuestSO(questId); if (quest?.objectives == null) return new List(0); var list = new List(quest.objectives.Length); foreach (var obj in quest.objectives) { _objectiveStates.TryGetValue(obj?.objectiveId ?? string.Empty, out var os); list.Add(os?.progressCount ?? 0); } return list; } // ── 事件处理 ───────────────────────────────────────────────────────── private void HandleEnemyDefeated(string enemyId) { ForEachActiveObjective(obj => { if (obj.targetEnemyId == enemyId) IncrementProgress(obj.objectiveId); }); } private void HandleItemCollected(string itemId) { ForEachActiveObjective(obj => { if (obj.itemId == itemId) IncrementProgress(obj.objectiveId); }); } private void HandleNpcDialogue(string npcId) { ForEachActiveObjective(obj => { if (obj.targetNpcId == npcId) IncrementProgress(obj.objectiveId); }); } private void HandleSceneLoaded(string sceneName) { ForEachActiveObjective(obj => { if (obj.sceneName == sceneName) IncrementProgress(obj.objectiveId); }); } private void HandleSkillUsed(string abilityTypeName) { // 用 Enum.TryParse 避免大小写/ToString 格式差异导致静默失败 if (!System.Enum.TryParse(abilityTypeName, ignoreCase: true, out var parsed)) return; ForEachActiveObjective(obj => { if (obj.requiredAbility == parsed) IncrementProgress(obj.objectiveId); }); } private void ForEachActiveObjective(System.Action action) where T : QuestObjectiveSO { foreach (var (qid, state) in _questStates) { if (state != QuestStateEnum.Active) continue; var quest = GetQuestSO(qid); if (quest?.objectives == null) continue; foreach (var obj in quest.objectives) { if (obj is T typed) action(typed); } } } private void IncrementProgress(string objectiveId) { if (!_objectiveStates.TryGetValue(objectiveId, out var s)) s = _objectiveStates[objectiveId] = new QuestObjectiveState(); s.progressCount++; _onObjectiveUpdated?.Raise(new QuestObjectiveEvent { ObjectiveId = objectiveId, Progress = s.progressCount, }); } private QuestSO GetQuestSO(string id) => _questIndex != null && _questIndex.TryGetValue(id, out var q) ? q : null; // ── 编辑器自动维护 ──────────────────────────────────────────────────── #if UNITY_EDITOR /// /// 编辑器中每次属性变更时自动同步项目内所有 QuestSO,并校验前置任务是否存在循环引用。 /// private void OnValidate() { EditorRefreshQuestList(); ValidatePrerequisites(); } [ContextMenu("刷新任务列表")] private void EditorRefreshQuestList() { string[] guids = UnityEditor.AssetDatabase.FindAssets("t:QuestSO"); var list = new List(guids.Length); foreach (var guid in guids) { string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid); var q = UnityEditor.AssetDatabase.LoadAssetAtPath(path); if (q != null) list.Add(q); } _allQuests = list.ToArray(); UnityEditor.EditorUtility.SetDirty(this); } #endif #if UNITY_EDITOR || DEVELOPMENT_BUILD /// /// 通过 DFS 后序遍历检测 prerequisiteQuests 中是否存在循环引用。 /// 在编辑器 OnValidate 及开发构建 Awake 时调用,发现问题立即打 LogError。 /// [System.Diagnostics.Conditional("UNITY_EDITOR")] // ContextMenu 只在编辑器生效 [UnityEngine.ContextMenu("校验前置任务循环引用")] private void ValidatePrerequisites() { if (_allQuests == null) return; // 先建立 id → SO 快速查找表 var index = new Dictionary(_allQuests.Length); foreach (var q in _allQuests) { if (q == null || string.IsNullOrEmpty(q.questId)) continue; index[q.questId] = q; } // 三色 DFS:0=未访问 1=灰(栈中)2=黑(已完成) var color = new Dictionary(index.Count); bool HasCycle(string startId, List path) { if (!index.ContainsKey(startId)) return false; if (color.TryGetValue(startId, out int c)) { if (c == 1) { path.Add(startId); Debug.LogError( $"[QuestManager] 前置任务循环引用: {string.Join(" → ", path)}", this); return true; } return false; // 已完成 } color[startId] = 1; path.Add(startId); var prereqs = index[startId].prerequisiteQuests; if (prereqs != null) { foreach (var pre in prereqs) { if (pre == null || string.IsNullOrEmpty(pre.questId)) continue; if (HasCycle(pre.questId, path)) return true; } } path.RemoveAt(path.Count - 1); color[startId] = 2; return false; } foreach (var questId in index.Keys) HasCycle(questId, new List()); } #endif } }