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, IQuestEventSource
#if UNITY_EDITOR || DEVELOPMENT_BUILD
, IQuestDebugger
#endif
{
// ── Inspector ────────────────────────────────────────────────────────
[Tooltip("所有 QuestSO 资产。编辑器会自动同步,无需手动维护。")]
[SerializeField] private QuestSO[] _allQuests;
[Header("事件频道注册表(推荐)")]
[Tooltip("将全部事件频道集中到一个 SO 中,方便多场景复用。\n" +
"若设置此注册表,下方独立频道字段将被自动忽略(注册表优先)。\n" +
"创建方式:右键菜单 → BaseGames/Quest/EventChannelRegistry。")]
[SerializeField] private QuestEventChannelRegistry _eventChannelRegistry;
[Header("Event Channels(监听)— 未设置注册表时生效")]
[Tooltip("EVT_EnemyDied:payload = enemyId(string)。敌人死亡时由战斗系统广播,驱动击败类目标进度。")]
[SerializeField] private StringEventChannelSO _onEnemyDied;
[Tooltip("EVT_CollectiblePickup:payload = itemId(string)。拾取物品时广播,驱动收集类目标进度,同时也作为 RewardSO 物品发放频道。")]
[SerializeField] private StringEventChannelSO _onCollectiblePickup;
[Tooltip("EVT_SceneLoaded:payload = sceneName(string)。场景切换完成时广播,驱动到达类目标进度。")]
[SerializeField] private StringEventChannelSO _onSceneLoaded;
[Tooltip("EVT_NpcDialogueCompleted:payload = npcId(string)。DialogueManager 播完一段对话后广播,驱动对话类目标进度。")]
[SerializeField] private StringEventChannelSO _onNpcDialogueCompleted;
[Tooltip("EVT_SkillUsed:payload = AbilityType.ToString()(string)。玩家使用技能时广播,驱动技能使用类目标进度。")]
[SerializeField] private StringEventChannelSO _onSkillUsed;
[Tooltip("EVT_AreaReached:payload = markerTag(string)。TriggerZone 组件在玩家进入碰撞体时广播,驱动精确区域到达类目标进度。")]
[SerializeField] private StringEventChannelSO _onAreaReached;
[Header("Event Channels(广播)— 未设置注册表时生效")]
[Tooltip("EVT_QuestStarted:payload = questId。AcceptQuest 成功后广播,供任务日志 UI 新增条目、任务追踪 HUD 激活等监听。")]
[SerializeField] private StringEventChannelSO _onQuestStarted;
[Tooltip("EVT_QuestCompleted:payload = questId。CompleteQuest 成功后广播,供成就系统、任务日志、剧情触发器等监听。")]
[SerializeField] private StringEventChannelSO _onQuestCompleted;
[Tooltip("EVT_QuestFailed:payload = questId。失败条件触发后广播,供失败提示 UI、任务日志、剧情触发器等监听。")]
[SerializeField] private StringEventChannelSO _onQuestFailed;
[Tooltip("EVT_QuestObjectiveUpdated:payload = QuestObjectiveEvent(questId + progress)。目标进度变化时广播,供任务追踪 HUD 更新进度条等监听。")]
[SerializeField] private QuestObjectiveEventChannelSO _onObjectiveUpdated;
[Tooltip("EVT_QuestObjectiveBatchUpdated:同帧内同一任务多目标聚合后广播一次(payload = QuestObjectiveBatchEvent)。\n" +
"供追踪 HUD 订阅以避免同帧多次重绘;留空则仅使用逐条 EVT_QuestObjectiveUpdated。")]
[SerializeField] private QuestObjectiveBatchEventChannelSO _onObjectiveBatchUpdated;
[Tooltip("EVT_NpcAffinityChanged:payload = NpcAffinityEvent(npcId + delta + newTotal 强类型,零字符串解析),供 UI/好感度系统监听。")]
[SerializeField] private NpcAffinityEventChannelSO _onNpcAffinityChanged;
[Tooltip("EVT_DialogueKeyUnlocked:payload = unlockDialogueKey,供 NPC 台词系统监听。")]
[SerializeField] private StringEventChannelSO _onDialogueKeyUnlocked;
[Tooltip("EVT_QuestReadyToComplete:payload = questId。目标全部达成、可回去交任务时广播一次(去重)。\n" +
"供任务日志 UI 高亮、地图标记、提示 HUD 等监听。")]
[SerializeField] private StringEventChannelSO _onQuestReadyToComplete;
[Tooltip("EVT_QuestAbandoned:payload = questId。玩家主动放弃任务(Active → Available)时广播。\n" +
"供任务日志 UI 移除追踪条目、提示 HUD 清空等监听。")]
[SerializeField] private StringEventChannelSO _onQuestAbandoned;
[Tooltip("EVT_QuestPaused:payload = questId。PauseQuest 成功(Active → Paused)后广播。\n" +
"供任务日志 UI 更新状态标记、UI/成就系统监听。")]
[SerializeField] private StringEventChannelSO _onQuestPaused;
[Tooltip("EVT_QuestResumed:payload = questId。ResumeQuest 成功(Paused → Active)后广播。\n" +
"供任务日志 UI 恢复追踪条目、UI/成就系统监听。")]
[SerializeField] private StringEventChannelSO _onQuestResumed;
[Header("扩展事件频道(自定义目标类型)")]
[Tooltip("标准六类事件(敌人/物品/场景/对话/技能/区域)已在上方独立字段配置。\n" +
"若新增自定义 QuestEventType 和 StringEventChannelSO,在此数组添加绑定即可,无需修改代码。")]
[SerializeField] private QuestEventChannelBinding[] _extraEventChannels;
// ── 事件频道访问器(注册表优先,回退到独立字段)────────────────────────
// 支持从 QuestEventChannelRegistry 或独立字段访问,无需修改下游广播/订阅代码。
private StringEventChannelSO Chan_EnemyDied => _eventChannelRegistry?.onEnemyDied ?? _onEnemyDied;
private StringEventChannelSO Chan_CollectiblePickup => _eventChannelRegistry?.onCollectiblePickup ?? _onCollectiblePickup;
private StringEventChannelSO Chan_SceneLoaded => _eventChannelRegistry?.onSceneLoaded ?? _onSceneLoaded;
private StringEventChannelSO Chan_NpcDialogueCompleted => _eventChannelRegistry?.onNpcDialogueCompleted ?? _onNpcDialogueCompleted;
private StringEventChannelSO Chan_SkillUsed => _eventChannelRegistry?.onSkillUsed ?? _onSkillUsed;
private StringEventChannelSO Chan_AreaReached => _eventChannelRegistry?.onAreaReached ?? _onAreaReached;
private StringEventChannelSO Chan_QuestStarted => _eventChannelRegistry?.onQuestStarted ?? _onQuestStarted;
private StringEventChannelSO Chan_QuestCompleted => _eventChannelRegistry?.onQuestCompleted ?? _onQuestCompleted;
private StringEventChannelSO Chan_QuestFailed => _eventChannelRegistry?.onQuestFailed ?? _onQuestFailed;
private StringEventChannelSO Chan_QuestAbandoned => _eventChannelRegistry?.onQuestAbandoned ?? _onQuestAbandoned;
private StringEventChannelSO Chan_QuestPaused => _eventChannelRegistry?.onQuestPaused ?? _onQuestPaused;
private StringEventChannelSO Chan_QuestResumed => _eventChannelRegistry?.onQuestResumed ?? _onQuestResumed;
private StringEventChannelSO Chan_QuestReadyToComplete => _eventChannelRegistry?.onQuestReadyToComplete ?? _onQuestReadyToComplete;
private QuestObjectiveEventChannelSO Chan_ObjectiveUpdated => _eventChannelRegistry?.onObjectiveUpdated ?? _onObjectiveUpdated;
private QuestObjectiveBatchEventChannelSO Chan_ObjectiveBatch => _eventChannelRegistry?.onObjectiveBatchUpdated ?? _onObjectiveBatchUpdated;
private NpcAffinityEventChannelSO Chan_NpcAffinityChanged => _eventChannelRegistry?.onNpcAffinityChanged ?? _onNpcAffinityChanged;
private StringEventChannelSO Chan_DialogueKeyUnlocked => _eventChannelRegistry?.onDialogueKeyUnlocked ?? _onDialogueKeyUnlocked;
// ── Runtime State ────────────────────────────────────────────────────
private readonly Dictionary _questStates = new();
private readonly Dictionary _objectiveStates = new();
/// questId → QuestSO 快速查找表(由 Awake 构建,将 GetQuestSO O(n) 降为 O(1))。
private Dictionary _questIndex;
///
/// (questId, objectiveId) → compositeKey 预缓存表。
/// 由 Awake 与 _questIndex 同步构建,消除 DispatchEvent 高频内循环的字符串拼接分配。
/// key = (questId, objectiveId),value = CompositeKey(questId, objectiveId)。
///
private Dictionary<(string, string), string> _compositeKeyCache;
private readonly CompositeDisposable _subs = new();
/// npcId → 好感度数值(从 SaveData.World.NpcRelations 同步,由 CompleteQuest 更新)。
private Dictionary _npcRelations = new();
///
/// OnLoad 完成后置为 true,标记好感度字典已从存档初始化。
/// 防止 CanAccept 在 OnLoad 前被调用时,对空字典产生错误的通过判定。
///
private bool _affinityInitialized;
/// 已广播过 EVT_QuestReadyToComplete 的任务 ID 集合(防重复通知)。
/// 任务完成/失败时从集合移除,再次激活后可重新通知。
private readonly HashSet _notifiedReadyQuests = new();
#if UNITY_EDITOR || DEVELOPMENT_BUILD
/// 任务暂停时记录的 realtimeSinceStartup(供 ResumeQuest 计算暂停持续时长并日志输出)。
private readonly Dictionary _pauseTimestamps = new();
#endif
/// questId → 接取时间(Unix 秒,UTC)。用于存档和统计分析。
private readonly Dictionary _startedAtUtc = new();
/// questId → 完成时间(Unix 秒,UTC)。0 表示未完成或旧存档未记录。
private readonly Dictionary _completedAtUtc = new();
/// 供需要直接订阅 SO 频道的系统使用(推荐使用 IQuestEventSource C# 事件)。
public StringEventChannelSO QuestStartedChannel => _onQuestStarted;
/// 供需要直接订阅 SO 频道的系统使用(推荐使用 IQuestEventSource C# 事件)。
public StringEventChannelSO QuestCompletedChannel => _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;
// 预构建 compositeKey 缓存,消除 DispatchEvent 高频内循环的字符串拼接分配
int cacheCapacity = 0;
if (_allQuests != null)
foreach (var q in _allQuests)
if (q?.objectives != null) cacheCapacity += q.objectives.Length;
_compositeKeyCache = new Dictionary<(string, string), string>(cacheCapacity);
foreach (var q in _questIndex.Values)
{
if (q.objectives == null) continue;
foreach (var obj in q.objectives)
{
if (obj == null || string.IsNullOrEmpty(obj.objectiveId)) continue;
var cacheKey = (q.questId, obj.objectiveId);
if (!_compositeKeyCache.ContainsKey(cacheKey))
_compositeKeyCache[cacheKey] = CompositeKey(q.questId, obj.objectiveId);
}
}
#if UNITY_EDITOR || DEVELOPMENT_BUILD
ValidateQuestIds();
ValidatePrerequisites();
#endif
// 将无前置条件的任务初始化为 Available,确保冷启动时可接取
InitializeAvailableQuests();
}
#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()
{
Chan_EnemyDied?.Subscribe(p => DispatchEvent(QuestEventType.EnemyDefeated, p)).AddTo(_subs);
Chan_CollectiblePickup?.Subscribe(p => DispatchEvent(QuestEventType.ItemCollected, p)).AddTo(_subs);
Chan_SceneLoaded?.Subscribe(p => DispatchEvent(QuestEventType.SceneLoaded, p)).AddTo(_subs);
Chan_NpcDialogueCompleted?.Subscribe(p => DispatchEvent(QuestEventType.NpcDialogueCompleted, p)).AddTo(_subs);
Chan_SkillUsed?.Subscribe(p => DispatchEvent(QuestEventType.SkillUsed, p)).AddTo(_subs);
Chan_AreaReached?.Subscribe(p => DispatchEvent(QuestEventType.AreaReached, p)).AddTo(_subs);
// 扩展绑定:Inspector 中配置的自定义频道,无需修改代码即可支持新 QuestEventType
if (_extraEventChannels != null)
{
foreach (var binding in _extraEventChannels)
{
var capturedType = binding.eventType;
binding.channel?.Subscribe(p => DispatchEvent(capturedType, p)).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);
}
// ── IQuestEventSource(代码订阅入口,无需直接持有 SO 频道)──────────────
///
public event System.Action OnQuestStarted;
///
public event System.Action OnQuestCompleted;
///
public event System.Action OnQuestFailed;
///
public event System.Action OnQuestAbandoned;
///
public event System.Action OnQuestPaused;
///
public event System.Action OnQuestResumed;
///
public event System.Action OnQuestReadyToComplete;
///
public event System.Action OnQuestStateChanged;
// ── 公共 API ──────────────────────────────────────────────────────────
/// NPC 接受任务时调用。
public void AcceptQuest(string questId)
{
if (string.IsNullOrEmpty(questId)) return;
// CanAccept 内部已通过 GetState() != Available 检查,防止重复接取 Active/Completed 任务产生重复事件
if (!CanAccept(questId)) return;
var oldState = GetState(questId);
_questStates[questId] = QuestStateEnum.Active;
_startedAtUtc[questId] = System.DateTimeOffset.UtcNow.ToUnixTimeSeconds();
OnQuestStateChanged?.Invoke(questId, oldState, QuestStateEnum.Active);
Chan_QuestStarted?.Raise(questId);
OnQuestStarted?.Invoke(questId);
// 触发接取任务对话(NPC 委托台词)
var quest = GetQuestSO(questId);
if (quest?.acceptDialogueSequence != null)
{
var dialogueService = BaseGames.Core.ServiceLocator.GetOrDefault();
if (dialogueService != null)
dialogueService.StartDialogue(quest.acceptDialogueSequence, quest.GiverNpcId ?? "");
#if UNITY_EDITOR || DEVELOPMENT_BUILD
else
Debug.LogWarning(
$"[QuestManager] 任务 '{questId}' 接取时需播放对话 '{quest.acceptDialogueSequence.name}'," +
"但 IDialogueService 未注册,对话被跳过。");
#endif
}
}
///
/// 玩家主动放弃进行中的任务(Active → Available/Unavailable)。
/// 清除已积累的目标进度,广播 EVT_QuestAbandoned,
/// 任务回到可重新接取状态(前置满足 → Available,否则 → Unavailable)。
///
public void AbandonQuest(string questId)
{
if (string.IsNullOrEmpty(questId)) return;
var curState = GetState(questId);
// Paused 状态:自动恢复为 Active 再放弃,调用方无需手动二步操作
if (curState == QuestStateEnum.Paused)
_questStates[questId] = QuestStateEnum.Active;
else if (curState != QuestStateEnum.Active)
return;
// 清除该任务的所有目标进度
var quest = GetQuestSO(questId);
if (quest?.objectives != null)
{
foreach (var obj in quest.objectives)
{
if (obj == null || string.IsNullOrEmpty(obj.objectiveId)) continue;
_objectiveStates.Remove(GetCompositeKey(questId, obj.objectiveId));
}
}
_notifiedReadyQuests.Remove(questId);
// 回到可接取状态(前置满足则 Available,否则 Unavailable)
var questSo = GetQuestSO(questId);
var newState = MeetsPrerequisites(questSo)
? QuestStateEnum.Available
: QuestStateEnum.Unavailable;
_questStates[questId] = newState;
OnQuestStateChanged?.Invoke(questId, QuestStateEnum.Active, newState);
Chan_QuestAbandoned?.Raise(questId);
OnQuestAbandoned?.Invoke(questId);
}
///
/// 暂停进行中的任务(Active → Paused)。
/// 暂停期间:目标进度事件不推进,失败条件不判定。
/// 通过 ResumeQuest 恢复。非 Active 状态调用无效。
///
public void PauseQuest(string questId)
{
if (string.IsNullOrEmpty(questId)) return;
if (GetState(questId) != QuestStateEnum.Active) return;
_questStates[questId] = QuestStateEnum.Paused;
OnQuestStateChanged?.Invoke(questId, QuestStateEnum.Active, QuestStateEnum.Paused);
#if UNITY_EDITOR || DEVELOPMENT_BUILD
_pauseTimestamps[questId] = UnityEngine.Time.realtimeSinceStartup;
Debug.Log($"[QuestManager] 任务 '{questId}' 已暂停(realtimeSinceStartup={UnityEngine.Time.realtimeSinceStartup:F2}s)。");
#endif
Chan_QuestPaused?.Raise(questId);
OnQuestPaused?.Invoke(questId);
}
///
/// 恢复已暂停的任务(Paused → Active)。
/// 非 Paused 状态调用无效。
///
public void ResumeQuest(string questId)
{
if (string.IsNullOrEmpty(questId)) return;
if (GetState(questId) != QuestStateEnum.Paused) return;
_questStates[questId] = QuestStateEnum.Active;
OnQuestStateChanged?.Invoke(questId, QuestStateEnum.Paused, QuestStateEnum.Active);
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (_pauseTimestamps.TryGetValue(questId, out float pausedAt))
{
float duration = UnityEngine.Time.realtimeSinceStartup - pausedAt;
_pauseTimestamps.Remove(questId);
Debug.Log($"[QuestManager] 任务 '{questId}' 已恢复(暂停持续 {duration:F2}s)。");
}
#endif
Chan_QuestResumed?.Raise(questId);
OnQuestResumed?.Invoke(questId);
}
/// NPC 完成任务时调用。
public void CompleteQuest(string questId, IRewardTarget rewardTarget)
{
if (!IsReadyToComplete(questId)) return;
var quest = GetQuestSO(questId);
if (quest == null) return; // IsReadyToComplete 已通过,此处防御冗余调用竞态
// 先更新状态再发放奖励:确保 Apply 即使抛出异常,任务状态也已正确写入
_questStates[questId] = QuestStateEnum.Completed;
_completedAtUtc[questId] = System.DateTimeOffset.UtcNow.ToUnixTimeSeconds();
_notifiedReadyQuests.Remove(questId);
OnQuestStateChanged?.Invoke(questId, QuestStateEnum.Active, QuestStateEnum.Completed);
Chan_QuestCompleted?.Raise(questId);
OnQuestCompleted?.Invoke(questId);
// 奖励发放:用 try-catch 包裹,防止 Apply 异常导致好感度/对话解锁等后续逻辑中断
try { quest.reward?.Apply(rewardTarget); }
catch (System.Exception ex)
{
Debug.LogError(
$"[QuestManager] 任务 '{questId}' 奖励发放时抛出异常(任务状态已提交为 Completed):{ex.Message}\n{ex.StackTrace}");
}
ApplyAffinity(quest);
UnlockDialogueKey(quest);
UnlockBranches(questId, quest);
}
/// 将奖励好感度更新到本地缓存并广播事件。上限由 NpcSO.maxAffinity 控制(>0 时生效)。
private void ApplyAffinity(QuestSO quest)
{
if (quest.reward == null || quest.reward.affinityBonus == 0) return;
if (string.IsNullOrEmpty(quest.GiverNpcId)) return;
_npcRelations.TryGetValue(quest.GiverNpcId, out int current);
int newTotal = current + quest.reward.affinityBonus;
// 上限截断:npcSO.maxAffinity > 0 时好感度不得超过上限
int maxAffinity = quest.giverNpc?.maxAffinity ?? 0;
if (maxAffinity > 0 && newTotal > maxAffinity)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.LogWarning(
$"[QuestManager] 任务 '{quest.questId}' 好感度奖励 +{quest.reward.affinityBonus} " +
$"将超出 NPC '{quest.GiverNpcId}' 的上限 {maxAffinity}(当前 {current}),已截断至 {maxAffinity},实际增量为 {maxAffinity - current}。");
#endif
newTotal = maxAffinity;
}
// 广播实际写入的 delta(截断后),而非请求值;UI 层显示 "+5" 而非因截断产生误导的 "+20"
int actualDelta = newTotal - current;
_npcRelations[quest.GiverNpcId] = newTotal;
Chan_NpcAffinityChanged?.Raise(new NpcAffinityEvent
{
npcId = quest.GiverNpcId,
delta = actualDelta,
newTotal = newTotal
});
}
/// 广播对话解锁事件,供 NPC 台词管理系统监听并切换新对话集。
private void UnlockDialogueKey(QuestSO quest)
{
if (quest.reward == null || string.IsNullOrEmpty(quest.reward.unlockDialogueKey)) return;
Chan_DialogueKeyUnlocked?.Raise(quest.reward.unlockDialogueKey);
}
///
/// 解锁满足条件的后续任务分支,并触发相应 NPC 完成反应对话。
/// 允许同时满足多个分支(并行支线解锁)。
///
private void UnlockBranches(string questId, QuestSO quest)
{
if (quest.branches == null) return;
var dialogueService = BaseGames.Core.ServiceLocator.GetOrDefault();
var saveService = BaseGames.Core.ServiceLocator.GetOrDefault();
foreach (var branch in quest.branches)
{
// 任务条件
bool conditionMet = branch.conditionQuest == null ||
GetState(branch.conditionQuest.questId) == QuestStateEnum.Completed;
if (!conditionMet) continue;
// 世界状态标志条件(And/Or 由 conditionFlagsLogic 决定)
// 优先用新版 conditionFlagEntries(支持 invert/NOT 取反),若为空则回退到旧版 conditionFlags
// saveService 未注入时降级:跳过标志检查,仅由 conditionQuest 决定分支
bool hasFlagEntries = branch.conditionFlagEntries != null && branch.conditionFlagEntries.Length > 0;
bool hasLegacyFlags = branch.conditionFlags != null && branch.conditionFlags.Length > 0;
bool hasFlagConds = hasFlagEntries || hasLegacyFlags;
if (hasFlagConds && saveService != null)
{
if (branch.conditionFlagsLogic == BaseGames.Core.WorldStateFlagLogic.Or)
{
conditionMet = false;
if (hasFlagEntries)
{
foreach (var entry in branch.conditionFlagEntries)
{
if (string.IsNullOrEmpty(entry.flagId)) continue;
bool raw = saveService.GetFlag(entry.flagId);
if (entry.invert ? !raw : raw) { conditionMet = true; break; }
}
}
else
{
foreach (var flag in branch.conditionFlags)
{
if (!string.IsNullOrEmpty(flag) && saveService.GetFlag(flag))
{ conditionMet = true; break; }
}
}
}
else
{
// AND(默认):全部标志均须满足(支持 invert 取反)
if (hasFlagEntries)
{
foreach (var entry in branch.conditionFlagEntries)
{
if (string.IsNullOrEmpty(entry.flagId)) continue;
bool raw = saveService.GetFlag(entry.flagId);
if (entry.invert ? raw : !raw) { conditionMet = false; break; }
}
}
else
{
foreach (var flag in branch.conditionFlags)
{
if (string.IsNullOrEmpty(flag)) continue;
if (!saveService.GetFlag(flag)) { conditionMet = false; break; }
}
}
}
}
#if UNITY_EDITOR || DEVELOPMENT_BUILD
else if (hasFlagConds && saveService == null)
{
Debug.LogWarning(
$"[QuestManager] 任务 '{questId}' 分支配置了标志条件,但 ISaveService 未注册," +
"标志条件已跳过(降级为仅 conditionQuest 判断)。");
}
#endif
if (!conditionMet) continue;
if (branch.nextQuest != null)
_questStates[branch.nextQuest.questId] = QuestStateEnum.Available;
// 触发 NPC 完成反应对话(如 NPC 说"太好了,谢谢你!")
if (branch.npcDialogueSequence != null)
{
if (dialogueService != null)
dialogueService.StartDialogue(branch.npcDialogueSequence, quest.GiverNpcId ?? "");
else
Debug.LogWarning(
$"[QuestManager] 任务 '{questId}' 完成后需播放 NPC 对话 " +
$"'{branch.npcDialogueSequence.name}',但 DialogueService 未注册到 ServiceLocator,对话被跳过。");
}
}
}
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 == null) continue;
if (!obj.IsOptional && !IsObjectiveComplete(questId, obj)) return false;
}
return true;
}
public int GetNpcAffinity(string npcId)
=> (!string.IsNullOrEmpty(npcId) && _npcRelations.TryGetValue(npcId, out int v)) ? v : 0;
///
/// 返回任务无法被接取的原因(本地化 Key 格式)。
/// 内部委托给 实现,保持向后兼容。
///
public string GetQuestLockReason(string questId) => GetQuestLockInfo(questId).ToLocalizationKey();
///
public QuestLockInfo GetQuestLockInfo(string questId)
{
if (string.IsNullOrEmpty(questId)) return new QuestLockInfo { Reason = QuestLockReason.NotFound };
var state = GetState(questId);
switch (state)
{
case QuestStateEnum.Active: return new QuestLockInfo { Reason = QuestLockReason.AlreadyActive };
case QuestStateEnum.Completed: return new QuestLockInfo { Reason = QuestLockReason.AlreadyCompleted };
case QuestStateEnum.Failed: return new QuestLockInfo { Reason = QuestLockReason.Failed };
case QuestStateEnum.Paused: return new QuestLockInfo { Reason = QuestLockReason.Paused };
}
// Unavailable / Available 都需要进一步细化
var quest = GetQuestSO(questId);
if (quest == null) return new QuestLockInfo { Reason = QuestLockReason.NotFound };
// 好感度门槛检查(仅 GetQuestLockInfo 关心,不影响 MeetsPrerequisites)
if (quest.minAffinityToAccept > 0 && !string.IsNullOrEmpty(quest.GiverNpcId))
{
if (!_affinityInitialized) return new QuestLockInfo { Reason = QuestLockReason.DataNotLoaded };
_npcRelations.TryGetValue(quest.GiverNpcId, out int affinity);
if (affinity < quest.minAffinityToAccept)
return new QuestLockInfo { Reason = QuestLockReason.InsufficientAffinity, Param = $"{affinity}/{quest.minAffinityToAccept}" };
}
// 前置依赖 + 标志检查委托给 CheckQuestDepsAndFlags(单一权威实现)
return CheckQuestDepsAndFlags(quest);
}
///
public IReadOnlyList GetQuestsInState(QuestStateEnum state)
{
var result = new List();
FillQuestsInState(state, result);
return result;
}
///
public IReadOnlyList FilterQuests(Func predicate)
{
if (predicate == null) return Array.Empty();
var result = new List();
FillFilterQuests(predicate, result);
return result;
}
///
public void FillQuestsInState(QuestStateEnum state, List result)
{
if (result == null) return;
result.Clear();
foreach (var (id, s) in _questStates)
if (s == state) result.Add(id);
}
///
public void FillFilterQuests(Func predicate, List result)
{
if (result == null) return;
result.Clear();
if (predicate == null) return;
foreach (var (id, s) in _questStates)
if (predicate(id, s)) result.Add(id);
}
#if UNITY_EDITOR || DEVELOPMENT_BUILD
// ── IQuestDebugger ────────────────────────────────────────────────────
///
/// 将任务重置为 Available(前置满足)或 Unavailable(前置未满足),并清除目标进度。
/// 仅供开发/调试使用(IQuestDebugger),不广播 QuestStarted / QuestCompleted 等运行时事件。
/// 正式发布构建中此方法不存在;调用方通过 (qm as IQuestDebugger)?.ResetQuest(id) 使用。
///
/// 要重置的任务 ID。
/// true = 同步扣回本任务发放的好感度增量,防止反复完成累积。
public void ResetQuest(string questId, bool rollbackAffinity = true)
{
if (string.IsNullOrEmpty(questId)) return;
var quest = GetQuestSO(questId);
if (quest == null) return;
// 好感度回滚(仅当任务已处于 Completed 状态且配置了好感度奖励时)
if (rollbackAffinity
&& _questStates.TryGetValue(questId, out var curState)
&& curState == QuestStateEnum.Completed
&& quest.reward != null
&& quest.reward.affinityBonus != 0
&& !string.IsNullOrEmpty(quest.GiverNpcId))
{
_npcRelations.TryGetValue(quest.GiverNpcId, out int current);
int rolled = current - quest.reward.affinityBonus;
_npcRelations[quest.GiverNpcId] = rolled;
Debug.Log($"[QuestManager] 回滚任务 '{questId}' 的好感度增量 " +
$"{quest.reward.affinityBonus:+#;-#},'{quest.GiverNpcId}' 好感度:{current} → {rolled}。");
}
// 清除目标进度
if (quest.objectives != null)
{
foreach (var obj in quest.objectives)
{
if (obj == null || string.IsNullOrEmpty(obj.objectiveId)) continue;
_objectiveStates.Remove(GetCompositeKey(questId, obj.objectiveId));
}
}
_notifiedReadyQuests.Remove(questId);
// 重置状态:前置满足 → Available,否则 Unavailable
_questStates[questId] = MeetsPrerequisites(quest)
? QuestStateEnum.Available
: QuestStateEnum.Unavailable;
Debug.Log($"[QuestManager] 任务 '{questId}' 已重置为 [{_questStates[questId]}](好感度回滚:{rollbackAffinity})。");
}
#endif
// ── 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
{
DataVersion = 3,
Status = state.ToString(),
ObjectiveProgress = BuildObjectiveProgress(id),
ObjectiveCompleted = BuildObjectiveCompleted(id),
StartedAtUtc = _startedAtUtc.TryGetValue(id, out var sta) ? sta : 0L,
CompletedAtUtc = _completedAtUtc.TryGetValue(id, out var cta) ? cta : 0L,
};
}
// 将本地 NPC 好感度缓存回写到存档
data.World.NpcRelations.Clear();
foreach (var (npcId, val) in _npcRelations)
data.World.NpcRelations[npcId] = val;
}
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;
#if UNITY_EDITOR || DEVELOPMENT_BUILD
else
Debug.LogWarning(
$"[QuestManager] OnLoad:任务 '{id}' 的 Status 值 '{saved.Status}' 无法解析为 QuestStateEnum," +
"已忽略(该任务将不出现在运行时 _questStates,等同于 Unavailable)。" +
"请检查存档文件是否损坏,或任务状态枚举定义是否发生变更。");
#endif
var quest = GetQuestSO(id);
if (quest?.objectives == null) continue;
bool hasNewFormat = saved.ObjectiveProgress != null && saved.ObjectiveProgress.Count > 0;
// DataVersion >= 2:新格式(objectiveId 键值对);DataVersion <= 1 或遗留存档:旧格式(按索引)
// Count > 0 作为无 DataVersion 字段时的兼容兜底
bool useNewFormat = saved.DataVersion >= 2 || hasNewFormat;
if (useNewFormat && saved.ObjectiveProgress != null)
{
// 新格式:objectiveId → count,重排顺序后仍可正确恢复
foreach (var obj in quest.objectives)
{
if (obj == null) continue;
if (!saved.ObjectiveProgress.TryGetValue(obj.objectiveId, out int count)) continue;
string compositeKey = GetCompositeKey(id, obj.objectiveId);
if (!_objectiveStates.TryGetValue(compositeKey, out var os))
os = _objectiveStates[compositeKey] = new QuestObjectiveState();
os.progressCount = count;
// DataVersion >= 3:从存档恢复 completed 标志(防止 GetRequiredCount 变更后判定漂移)
if (saved.ObjectiveCompleted != null &&
saved.ObjectiveCompleted.TryGetValue(obj.objectiveId, out bool done))
os.completed = done;
}
}
else if (saved.ProgressCounts != null
#pragma warning disable CS0618 // ProgressCounts 弃用字段:仅在此处读取用于旧存档迁移,不再写入
&& saved.ProgressCounts.Count > 0)
{
// 旧格式兼容(按数组索引):迁移旧存档用,不再写入新存档
for (int i = 0; i < quest.objectives.Length && i < saved.ProgressCounts.Count; i++)
{
var obj = quest.objectives[i];
if (obj == null) continue;
string compositeKey = GetCompositeKey(id, obj.objectiveId);
if (!_objectiveStates.TryGetValue(compositeKey, out var os))
os = _objectiveStates[compositeKey] = new QuestObjectiveState();
os.progressCount = saved.ProgressCounts[i];
}
}
#pragma warning restore CS0618
// DataVersion >= 3:恢复任务开始 / 完成时间戳(0 = 旧存档未记录,跳过)
if (saved.StartedAtUtc != 0) _startedAtUtc[id] = saved.StartedAtUtc;
if (saved.CompletedAtUtc != 0) _completedAtUtc[id] = saved.CompletedAtUtc;
}
// 从存档恢复 NPC 好感度缓存(供 CanAccept 门槛检查使用)
_npcRelations = new Dictionary(data.World.NpcRelations);
_affinityInitialized = true;
#if UNITY_EDITOR || DEVELOPMENT_BUILD
// 检测存档中存在但当前版本中已不存在的任务(已删除或重命名的旧任务)
if (_questIndex != null)
{
foreach (var savedQuestId in data.Quests.QuestStates.Keys)
{
if (!_questIndex.ContainsKey(savedQuestId))
Debug.LogWarning(
$"[QuestManager] 存档中任务 '{savedQuestId}' 在当前版本不存在,已自动忽略" +
"(可能为已删除或重命名的任务)。如属正常版本迭代,可忽略此警告。");
}
}
#endif
// 存档中未记录的无前置任务,在新周目/首次加载后也保证可接取
InitializeAvailableQuests();
}
// ── 私有辅助 ─────────────────────────────────────────────────────────
private bool CanAccept(string questId)
{
// 状态必须为 Available;其余门槛检查委托给 GetQuestLockInfo(单一权威实现)。
if (GetState(questId) != QuestStateEnum.Available) return false;
return !GetQuestLockInfo(questId).IsLocked;
}
///
/// 初始化(或修正)所有任务的 Available/Unavailable 状态。
/// 在 Awake(冷启动)和 OnLoad(存档恢复)后调用。
/// OnLoad 后 ISaveService 已就绪,会重新评估 prerequisiteFlags,
/// 修正 Awake 期间因服务未就绪而被跳过的标志检查。
/// Active/Completed/Failed 状态来自存档,不重置。
///
private void InitializeAvailableQuests()
{
if (_questIndex == null) return;
foreach (var q in _questIndex.Values)
{
var cur = GetState(q.questId);
// 运行时终态来自存档,不重新评估
if (cur == QuestStateEnum.Active || cur == QuestStateEnum.Paused ||
cur == QuestStateEnum.Completed || cur == QuestStateEnum.Failed)
continue;
#if UNITY_EDITOR || DEVELOPMENT_BUILD
// _affinityInitialized 为 true 说明是 OnLoad 后调用,Awake 期间不打此日志
bool isNewToSave = !_questStates.ContainsKey(q.questId) && _affinityInitialized;
#endif
// Available/Unavailable 均重新评估,确保 prerequisiteFlags 变更后状态正确
_questStates[q.questId] = MeetsPrerequisites(q) ? QuestStateEnum.Available : QuestStateEnum.Unavailable;
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (isNewToSave)
Debug.Log(
$"[QuestManager] 新增任务 '{q.questId}' 在存档中无记录(DLC/补丁新增)," +
$"初始化状态 → {_questStates[q.questId]}。");
#endif
}
}
///
/// 检查任务是否满足全部前置条件(不含状态和亲密度检查),用于 InitializeAvailableQuests 初始化。
/// 与 CanAccept 的区别:CanAccept 需要任务已经是 Available 且包含亲密度检查;此方法仅判断前置依赖是否达成。
/// 委托给 实现,不再重复前置逻辑。
///
private bool MeetsPrerequisites(QuestSO quest)
{
return CheckQuestDepsAndFlags(quest).Reason == QuestLockReason.None;
}
///
/// 检查任务的前置依赖(任务完成 + 世界标志),不含亲密度和状态检查。
/// 是 (经 GetQuestLockInfo 间接调用)、、
/// 共享的单一权威实现,消除三处重复逻辑。
///
private QuestLockInfo CheckQuestDepsAndFlags(QuestSO quest)
{
if (quest == null) return new QuestLockInfo { Reason = QuestLockReason.NotFound };
if (quest.prerequisites.HasAny)
{
if (quest.prerequisites.questDependencies != null)
foreach (var dep in quest.prerequisites.questDependencies)
{
if (dep == null) continue;
if (string.IsNullOrEmpty(dep.questId))
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.LogWarning($"[QuestManager] 任务 '{quest.questId}' 的 prerequisites.questDependencies 含 questId 为空的条目,已跳过。");
#endif
continue;
}
if (GetState(dep.questId) != QuestStateEnum.Completed)
return new QuestLockInfo { Reason = QuestLockReason.RequiresQuest, Param = dep.questId };
}
var fc = quest.prerequisites.flagCondition;
if (fc.flags != null && fc.flags.Length > 0)
{
var svc = BaseGames.Core.ServiceLocator.GetOrDefault();
// ISaveService 未就绪(Awake 阶段)→ 保守跳过;OnLoad 后重新评估
if (svc != null && !EvaluateFlagPrerequisites(fc.flags, fc.logic, svc))
return new QuestLockInfo { Reason = QuestLockReason.FlagConditionNotMet };
}
}
else
{
// 旧版字段回退(兼容现有资产)
#pragma warning disable CS0618
if (quest.prerequisiteQuests != null)
foreach (var pre in quest.prerequisiteQuests)
{
if (pre == null) continue;
if (string.IsNullOrEmpty(pre.questId))
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.LogWarning($"[QuestManager] 任务 '{quest.questId}' 的 prerequisiteQuests 含 questId 为空的条目,已跳过该前置条件。");
#endif
continue;
}
if (GetState(pre.questId) != QuestStateEnum.Completed)
return new QuestLockInfo { Reason = QuestLockReason.RequiresQuest, Param = pre.questId };
}
if (quest.prerequisiteFlags != null && quest.prerequisiteFlags.Length > 0)
{
var svc = BaseGames.Core.ServiceLocator.GetOrDefault();
if (svc != null)
{
if (!EvaluateFlagPrerequisites(quest.prerequisiteFlags, quest.prerequisiteFlagsLogic, svc))
return new QuestLockInfo { Reason = QuestLockReason.FlagConditionNotMet };
}
#if UNITY_EDITOR || DEVELOPMENT_BUILD
else Debug.LogWarning($"[QuestManager] 任务 '{quest.questId}' 的 prerequisiteFlags 需要 ISaveService,但服务未注册,标志检查已跳过。");
#endif
}
#pragma warning restore CS0618
}
return new QuestLockInfo { Reason = QuestLockReason.None };
}
///
/// 根据 flags 数组和 logic 评估标志前置条件是否满足。
///
private static bool EvaluateFlagPrerequisites(string[] flags, BaseGames.Core.WorldStateFlagLogic logic, ISaveService svc)
{
if (logic == BaseGames.Core.WorldStateFlagLogic.Or)
{
foreach (var flag in flags)
if (!string.IsNullOrEmpty(flag) && svc.GetFlag(flag)) return true;
return false;
}
// And 逻辑(默认)
foreach (var flag in flags)
if (!string.IsNullOrEmpty(flag) && !svc.GetFlag(flag)) return false;
return true;
}
///
/// 只读检查目标是否已完成(不修改任何状态)。
/// 供 DispatchEvent 失败条件评估使用,避免副作用。
///
private bool CheckObjective(string questId, QuestObjectiveSO obj)
{
if (obj == null || string.IsNullOrEmpty(obj.objectiveId))
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.LogWarning($"[QuestManager] 任务 '{questId}' 含 objectiveId 为空的目标,无法评估完成状态。");
#endif
return false;
}
string compositeKey = GetCompositeKey(questId, obj.objectiveId);
_objectiveStates.TryGetValue(compositeKey, out var s);
// EvaluateCompletion 读取 s(可为 null/default),不写回
return obj.EvaluateCompletion(s ?? new QuestObjectiveState());
}
///
/// 检查目标是否完成,并在首次达成时写回 completed 标志。
/// 仅由 IsReadyToComplete 调用,防止重复计为完成。
///
private bool IsObjectiveComplete(string questId, QuestObjectiveSO obj)
{
if (obj == null || string.IsNullOrEmpty(obj.objectiveId))
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.LogWarning($"[QuestManager] 任务 '{questId}' 含 objectiveId 为空的目标,无法评估完成状态。");
#endif
return false;
}
string compositeKey = GetCompositeKey(questId, obj.objectiveId);
if (!_objectiveStates.TryGetValue(compositeKey, out var s))
s = new QuestObjectiveState();
bool result = obj.EvaluateCompletion(s);
// 首次达成时写回 completed 标志,避免 s 是本地临时对象时标志丢失
if (result && !s.completed)
{
s.completed = true;
_objectiveStates[compositeKey] = s;
}
return result;
}
///
/// 将当前任务的各目标进度序列化为 objectiveId → count 字典。
/// 按 objectiveId 键存储,策划重排目标顺序后存档数据不会错位。
///
private Dictionary BuildObjectiveProgress(string questId)
{
var quest = GetQuestSO(questId);
if (quest?.objectives == null) return new Dictionary(0);
var dict = new Dictionary(quest.objectives.Length, System.StringComparer.Ordinal);
foreach (var obj in quest.objectives)
{
if (obj == null || string.IsNullOrEmpty(obj.objectiveId)) continue;
_objectiveStates.TryGetValue(GetCompositeKey(questId, obj.objectiveId), out var os);
dict[obj.objectiveId] = os?.progressCount ?? 0;
}
return dict;
}
///
/// 构建当前任务各目标的 completed 标志字典(objectiveId → completed)。
/// 存入 ,
/// 防止版本迭代中 GetRequiredCount 变更后,进度数值与实际完成状态脱钩。
///
private Dictionary BuildObjectiveCompleted(string questId)
{
var quest = GetQuestSO(questId);
if (quest?.objectives == null) return new Dictionary(0);
var dict = new Dictionary(quest.objectives.Length, System.StringComparer.Ordinal);
foreach (var obj in quest.objectives)
{
if (obj == null || string.IsNullOrEmpty(obj.objectiveId)) continue;
_objectiveStates.TryGetValue(GetCompositeKey(questId, obj.objectiveId), out var os);
dict[obj.objectiveId] = os?.completed ?? false;
}
return dict;
}
// ── 事件路由 ─────────────────────────────────────────────────────────
// 统一分派入口:所有事件频道均路由到此方法,由各目标 SO 自行判断是否匹配。
// 新增目标类型只需在 QuestObjectiveSO 子类中 override TryHandleEvent,
// 此处无需任何修改。
///
/// 字符串载荷重载(向后兼容)。内部将 string 包装为 后委托给强类型重载。
/// 所有外部调用方(订阅者、频道处理器)保持原有 string 签名,无需修改。
///
private void DispatchEvent(QuestEventType eventType, string payload)
=> DispatchEvent(eventType, new StringQuestPayload(payload));
///
/// 强类型载荷主实现。子类目标 SO 可 override TryHandleEvent(QuestEventType, IQuestEventPayload, QuestObjectiveState)
/// 以直接获取结构化载荷,避免字符串解析开销。
///
private void DispatchEvent(QuestEventType eventType, IQuestEventPayload payload)
{
// ─ 第1次遍历:更新目标进度 + 同步收集失败候选 ─────────────────────
// 将 CheckQuestFailConditions 内联到此处,避免对 _questStates 的独立第2次迭代。
List toFail = null;
// 批量事件暂存:questId → 本帧内该任务所有更新过的目标事件(惰性分配,仅在有目标变更时创建)
Dictionary> pendingBatchUpdates = null;
foreach (var (qid, state) in _questStates)
{
// Paused 任务跳过所有事件处理(目标进度和失败条件均冻结),
// 直到 ResumeQuest() 恢复后才继续推进。
if (state != QuestStateEnum.Active) continue;
var quest = GetQuestSO(qid);
if (quest == null) continue;
// 目标进度处理
if (quest.objectives != null)
{
foreach (var obj in quest.objectives)
{
if (obj == null || string.IsNullOrEmpty(obj.objectiveId)) continue;
// P3-B:前置目标顺序依赖检查——前置目标未完成时跳过本目标的事件路由
if (!string.IsNullOrEmpty(obj.prerequisiteObjectiveId))
{
string prereqKey = GetCompositeKey(qid, obj.prerequisiteObjectiveId);
if (!_objectiveStates.TryGetValue(prereqKey, out var prereqOs) || !prereqOs.completed)
continue;
}
string compositeKey = GetCompositeKey(qid, obj.objectiveId);
if (!_objectiveStates.TryGetValue(compositeKey, out var os))
os = _objectiveStates[compositeKey] = new QuestObjectiveState();
if (obj.TryHandleEvent(eventType, payload, os))
{
var evt = new QuestObjectiveEvent
{
QuestId = qid,
ObjectiveId = obj.objectiveId,
Progress = os.progressCount,
Required = obj.GetRequiredCount(),
};
// 逐条事件:供向后兼容的逐目标监听者使用
Chan_ObjectiveUpdated?.Raise(evt);
// 批量事件积累(同任务多目标聚合为一次广播,减少 UI 重绘)
if (Chan_ObjectiveBatch != null)
{
pendingBatchUpdates ??= new Dictionary>(System.StringComparer.Ordinal);
if (!pendingBatchUpdates.TryGetValue(qid, out var list))
pendingBatchUpdates[qid] = list = new List(capacity: 4);
list.Add(evt);
}
}
}
}
// 失败条件检查(同次遍历内完成,惰性分配 toFail 列表)
// Paused 任务在此处已被跳过(见上方 state != Active continue),
// 设计意图:暂停期间目标冻结,失败条件也不判定,恢复后再继续检查。
if (quest.canFail)
{
// P3-A:多失败条件支持——failConditions 数组中任意一个达成即失败
bool triggered = false;
if (quest.failConditions != null && quest.failConditions.Length > 0)
{
foreach (var fc in quest.failConditions)
{
if (fc != null && CheckObjective(qid, fc)) { triggered = true; break; }
}
}
else
{
// 向后兼容旧版单一 failCondition 字段(Obsolete,将在后续版本移除)
#pragma warning disable CS0618
triggered = quest.failCondition != null && CheckObjective(qid, quest.failCondition);
#pragma warning restore CS0618
}
if (triggered)
{
toFail ??= new List();
toFail.Add(qid);
}
}
}
// 批量目标事件:每个有变更的任务广播一次聚合事件
if (pendingBatchUpdates != null)
{
foreach (var kv in pendingBatchUpdates)
Chan_ObjectiveBatch.Raise(new QuestObjectiveBatchEvent
{
QuestId = kv.Key,
Updates = kv.Value,
});
}
// 在遍历结束后统一应用失败状态,避免迭代中修改字典
if (toFail != null)
{
foreach (var qid in toFail)
{
_questStates[qid] = QuestStateEnum.Failed;
_notifiedReadyQuests.Remove(qid);
OnQuestStateChanged?.Invoke(qid, QuestStateEnum.Active, QuestStateEnum.Failed);
Chan_QuestFailed?.Raise(qid);
OnQuestFailed?.Invoke(qid);
}
}
// ─ 第2次遍历:检查就绪通知(必须在失败状态写入后,避免刚失败的任务误报)─
foreach (var (qid, state) in _questStates)
{
if (state != QuestStateEnum.Active) continue;
if (_notifiedReadyQuests.Contains(qid)) continue;
if (IsReadyToComplete(qid))
{
_notifiedReadyQuests.Add(qid);
Chan_QuestReadyToComplete?.Raise(qid);
OnQuestReadyToComplete?.Invoke(qid);
}
}
}
private QuestSO GetQuestSO(string id)
=> _questIndex != null && _questIndex.TryGetValue(id, out var q) ? q : null;
///
/// 优先从预缓存表查找 compositeKey(O(1),零字符串分配);
/// 缓存未命中时 fallback 到 CompositeKey() 动态构建(运行时新增的目标)。
///
private string GetCompositeKey(string questId, string objectiveId)
{
if (_compositeKeyCache != null &&
_compositeKeyCache.TryGetValue((questId, objectiveId), out var cached))
return cached;
return CompositeKey(questId, objectiveId);
}
///
/// 组合任务目标的复合键(格式 "{questId}.{objectiveId}")。
/// 全文统一通过此方法构建,objectiveId 为空时用 "__empty__" 占位保证唯一性。
///
private static string CompositeKey(string questId, string objectiveId)
=> string.IsNullOrEmpty(objectiveId)
? $"{questId}.__empty__"
: $"{questId}.{objectiveId}";
// ── 编辑器自动维护 ────────────────────────────────────────────────────
#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。
///
[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
}
}