Files
zeling_v2/Assets/_Game/Scripts/Quest/QuestManager.cs

268 lines
12 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 BaseGames.Core.Events;
using BaseGames.Core.Save;
using QuestStateEnum = BaseGames.Core.Events.QuestState;
namespace BaseGames.Quest
{
/// <summary>
/// 运行时任务管理器(架构 22_QuestChallengeModule §5
/// 挂在 Persistent 场景 [GameManagers] 下。
/// 事件驱动追踪目标进度,不主动轮询。
/// 实现 ISaveable通过 SaveManager 持久化任务状态。
/// </summary>
public class QuestManager : MonoBehaviour, ISaveable, IQuestManager
{
// ── Inspector ────────────────────────────────────────────────────────
[SerializeField] private QuestSO[] _allQuests;
[Header("Event Channels监听")]
[SerializeField] private StringEventChannelSO _onEnemyDied; // EVT_EnemyDiedenemyId
[SerializeField] private StringEventChannelSO _onCollectiblePickup; // EVT_CollectiblePickupitemId
[SerializeField] private StringEventChannelSO _onSceneLoaded; // EVT_SceneLoadedsceneName
[SerializeField] private StringEventChannelSO _onNpcDialogueCompleted; // EVT_NpcDialogueCompletednpcId
[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<string, QuestStateEnum> _questStates = new();
private readonly Dictionary<string, QuestObjectiveState> _objectiveStates = new();
/// <summary>questId → QuestSO 快速查找表(由 Awake 构建,将 GetQuestSO O(n) 降为 O(1))。</summary>
private Dictionary<string, QuestSO> _questIndex;
private readonly CompositeDisposable _subs = new();
public StringEventChannelSO OnQuestStarted => _onQuestStarted;
public StringEventChannelSO OnQuestCompleted => _onQuestCompleted;
/// <summary>供 SaveManager 迭代的任务状态字典(只读视图)。</summary>
public IReadOnlyDictionary<string, QuestStateEnum> QuestStates => _questStates;
private void Awake()
{
if (BaseGames.Core.ServiceLocator.GetOrDefault<IQuestManager>() != null) { Destroy(gameObject); return; }
BaseGames.Core.ServiceLocator.Register<IQuestManager>(this);
// 构建任务字典索引,将 GetQuestSO 变为 O(1)
_questIndex = new Dictionary<string, QuestSO>(_allQuests?.Length ?? 0);
if (_allQuests != null)
foreach (var q in _allQuests)
if (q != null && !string.IsNullOrEmpty(q.questId))
_questIndex[q.questId] = q;
}
private void OnEnable()
{
_onEnemyDied?.Subscribe(HandleEnemyDefeated).AddTo(_subs);
_onCollectiblePickup?.Subscribe(HandleItemCollected).AddTo(_subs);
_onSceneLoaded?.Subscribe(HandleSceneLoaded).AddTo(_subs);
_onNpcDialogueCompleted?.Subscribe(HandleNpcDialogue).AddTo(_subs);
BaseGames.Core.ServiceLocator.GetOrDefault<BaseGames.Core.Save.ISaveableRegistry>()?.Register(this);
}
private void OnDisable()
{
_subs.Clear();
BaseGames.Core.ServiceLocator.GetOrDefault<BaseGames.Core.Save.ISaveableRegistry>()?.Unregister(this);
}
private void OnDestroy()
{
BaseGames.Core.ServiceLocator.Unregister<IQuestManager>(this);
}
// ── 公共 API ──────────────────────────────────────────────────────────
/// <summary>NPC 接受任务时调用。</summary>
public void AcceptQuest(string questId)
{
if (!CanAccept(questId)) return;
_questStates[questId] = QuestStateEnum.Active;
_onQuestStarted?.Raise(questId);
}
/// <summary>NPC 完成任务时调用。</summary>
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);
// 解锁后续任务(分支)
if (quest.branches != null)
{
foreach (var branch in quest.branches)
{
if (string.IsNullOrEmpty(branch.conditionQuestId) ||
GetState(branch.conditionQuestId) == QuestStateEnum.Completed)
{
if (branch.nextQuest != null)
_questStates[branch.nextQuest.questId] = QuestStateEnum.Available;
break;
}
}
}
}
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(),
ObjectiveIndex = 0,
ProgressCounts = BuildProgressList(id),
};
}
}
public void OnLoad(SaveData data)
{
_questStates.Clear();
_objectiveStates.Clear();
foreach (var (id, saved) in data.Quests.QuestStates)
{
if (System.Enum.TryParse<QuestStateEnum>(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?.prerequisiteQuestIds == null) return true;
foreach (var pre in quest.prerequisiteQuestIds)
if (GetState(pre) != QuestStateEnum.Completed) return false;
return true;
}
private bool IsObjectiveComplete(QuestObjectiveSO obj)
{
_objectiveStates.TryGetValue(obj.objectiveId, out var s);
s ??= new QuestObjectiveState();
return obj.EvaluateCompletion(s);
}
private List<int> BuildProgressList(string questId)
{
var list = new List<int>();
var quest = GetQuestSO(questId);
if (quest?.objectives == null) return list;
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<DefeatEnemyObjective>(obj =>
{
if (obj.targetEnemyId == enemyId)
IncrementProgress(obj.objectiveId);
});
}
private void HandleItemCollected(string itemId)
{
ForEachActiveObjective<CollectItemObjective>(obj =>
{
if (obj.itemId == itemId)
IncrementProgress(obj.objectiveId);
});
}
private void HandleNpcDialogue(string npcId)
{
ForEachActiveObjective<TalkToNPCObjective>(obj =>
{
if (obj.targetNpcId == npcId)
IncrementProgress(obj.objectiveId);
});
}
private void HandleSceneLoaded(string sceneName)
{
ForEachActiveObjective<ReachAreaObjective>(obj =>
{
if (obj.sceneName == sceneName)
IncrementProgress(obj.objectiveId);
});
}
private void ForEachActiveObjective<T>(System.Action<T> 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;
}
}