diff --git a/Assets/_Game/Scripts/Editor/Dialogue/DialogueVariantPreviewWindow.cs b/Assets/_Game/Scripts/Editor/Dialogue/DialogueVariantPreviewWindow.cs index 824805c..e08daff 100644 --- a/Assets/_Game/Scripts/Editor/Dialogue/DialogueVariantPreviewWindow.cs +++ b/Assets/_Game/Scripts/Editor/Dialogue/DialogueVariantPreviewWindow.cs @@ -23,6 +23,7 @@ namespace BaseGames.Editor.Dialogue private ObjectField _targetField; private VisualElement _flagContainer; private VisualElement _resultContainer; + private VisualElement _matrixContainer; private static readonly Color ColWin = new(0.20f, 0.75f, 0.35f, 1f); private static readonly Color ColFail = new(0.55f, 0.55f, 0.55f, 1f); @@ -106,6 +107,20 @@ namespace BaseGames.Editor.Dialogue _resultContainer = new VisualElement(); scrollView.Add(_resultContainer); + rootVisualElement.Add(MakeDivider()); + + // ── 矩阵分析区 ── + var matrixFoldout = new Foldout { text = "矩阵分析(所有标志组合 → 胜出变体)", value = false }; + matrixFoldout.style.marginTop = 4; + rootVisualElement.Add(matrixFoldout); + + _matrixContainer = new VisualElement(); + matrixFoldout.Add(_matrixContainer); + + var matrixBtn = new Button(() => RebuildMatrix()) { text = "矩阵分析" }; + matrixBtn.style.marginBottom = 4; + matrixFoldout.Add(matrixBtn); + Rebuild(); } @@ -364,6 +379,119 @@ namespace BaseGames.Editor.Dialogue public bool HasFlag(string key) => _flags.Contains(key); } + // ── 矩阵分析 ───────────────────────────────────────────────────────── + + /// + /// 枚举全部 2^N 标志组合(N ≤ 10),以表格形式展示每种组合下胜出的变体索引。 + /// N > 10 时显示提示,建议手动筛选标志后分析。 + /// + private void RebuildMatrix() + { + if (_matrixContainer == null) return; + _matrixContainer.Clear(); + + if (_target == null || _target.variants == null || _target.variants.Length == 0) + { + _matrixContainer.Add(new Label("(无可分析的变体)") { style = { opacity = 0.5f, fontSize = 11 } }); + return; + } + + var matrixFlags = _allFlags.Count > 0 ? _allFlags : new List(); + if (matrixFlags.Count == 0) + { + _matrixContainer.Add(new Label("(变体不使用任何 requiredFlags,无需矩阵分析)") { style = { opacity = 0.5f, fontSize = 11 } }); + return; + } + + const int MaxFlags = 10; + if (matrixFlags.Count > MaxFlags) + { + var warn = new Label($"⚠ 标志数量 ({matrixFlags.Count}) 超过 {MaxFlags},枚举 2^N 组合代价过高。请在上方取消勾选不关心的标志后重新点击「矩阵分析」。"); + warn.style.fontSize = 11; + warn.style.color = new StyleColor(new Color(0.9f, 0.7f, 0.2f)); + warn.style.whiteSpace = WhiteSpace.Normal; + _matrixContainer.Add(warn); + return; + } + + int n = matrixFlags.Count; + int combos = 1 << n; // 2^n + + // ── 表头 ── + var headerRow = MakeMatrixRow(isHeader: true); + for (int ci = 0; ci < n; ci++) + { + var cell = MakeMatrixCell(matrixFlags[ci], isHeader: true); + cell.style.minWidth = 90; + headerRow.Add(cell); + } + headerRow.Add(MakeMatrixCell("胜出变体", isHeader: true)); + _matrixContainer.Add(headerRow); + + // ── 数据行 ── + for (int mask = 0; mask < combos; mask++) + { + var combo = new HashSet(System.StringComparer.Ordinal); + for (int bit = 0; bit < n; bit++) + if ((mask & (1 << bit)) != 0) combo.Add(matrixFlags[bit]); + + // 求胜出变体 + var mockReader = new MockFlagReader(combo); + int winner = -1; + for (int vi = 0; vi < _target.variants.Length; vi++) + if (_target.CheckVariant(_target.variants[vi], mockReader)) { winner = vi; break; } + + string winnerText = winner >= 0 + ? $"变体 {winner}" + + (_target.variants[winner].sequence != null + ? $"\n({_target.variants[winner].sequence.name})" + : "(无序列)") + : "默认台词"; + + var dataRow = MakeMatrixRow(isHeader: false); + // 标志列 + for (int ci = 0; ci < n; ci++) + { + bool on = (mask & (1 << ci)) != 0; + var cell = MakeMatrixCell(on ? "✓" : "–", isHeader: false); + cell.style.color = new StyleColor(on ? ColWin : ColFail); + cell.style.minWidth = 90; + dataRow.Add(cell); + } + // 胜出列 + var winCell = MakeMatrixCell(winnerText, isHeader: false); + winCell.style.color = new StyleColor(winner >= 0 ? ColWin : new Color(0.5f, 0.5f, 0.5f)); + dataRow.Add(winCell); + + _matrixContainer.Add(dataRow); + } + } + + private static VisualElement MakeMatrixRow(bool isHeader) + { + var row = new VisualElement(); + row.style.flexDirection = FlexDirection.Row; + row.style.borderBottomWidth = 1; + row.style.borderBottomColor = new StyleColor(new Color(0.3f, 0.3f, 0.3f, 0.5f)); + if (isHeader) + row.style.backgroundColor = new StyleColor(new Color(0.22f, 0.22f, 0.28f, 1f)); + return row; + } + + private static Label MakeMatrixCell(string text, bool isHeader) + { + var lbl = new Label(text); + lbl.style.fontSize = isHeader ? 10 : 10; + lbl.style.unityFontStyleAndWeight = isHeader ? FontStyle.Bold : FontStyle.Normal; + lbl.style.paddingLeft = 4; + lbl.style.paddingRight = 4; + lbl.style.paddingTop = 3; + lbl.style.paddingBottom = 3; + lbl.style.whiteSpace = WhiteSpace.Normal; + lbl.style.width = 80; + return lbl; + } + // ── 辅助 ───────────────────────────────────────────────────────────── private static VisualElement MakeDivider() diff --git a/Assets/_Game/Scripts/Editor/Modules/EventChainModule.cs b/Assets/_Game/Scripts/Editor/Modules/EventChainModule.cs index b1aeb8a..a4a15e8 100644 --- a/Assets/_Game/Scripts/Editor/Modules/EventChainModule.cs +++ b/Assets/_Game/Scripts/Editor/Modules/EventChainModule.cs @@ -431,10 +431,10 @@ namespace BaseGames.Editor.Modules var capturedC = c; var forceBtn = new Button(() => { - var mgr = UnityEngine.Object.FindObjectOfType(); + var mgr = BaseGames.Core.ServiceLocator.GetOrDefault(); if (mgr == null) { - Debug.LogWarning("[EventChainModule] 场景中未找到 EventChainManager,无法强制触发。"); + Debug.LogWarning("[EventChainModule] ServiceLocator 中未找到 EventChainManager,无法强制触发。请确认场景中已挂载并注册。"); return; } Debug.Log($"[EventChainModule] 强制触发链:{capturedC.chainId}"); @@ -519,6 +519,48 @@ namespace BaseGames.Editor.Modules AddError($"{c.chainId}: actions[{i}] 为 null,运行时将触发 NullReferenceException。", c); } + // ⑥ 自触发检测:某条件检查的标志由同一链的 Action 写入(可能造成链被自身条件阻断或无限反复触发) + var flagFieldFlags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance; + foreach (var c in allChains) + { + if (string.IsNullOrWhiteSpace(c.chainId)) continue; + var writtenFlags = new System.Collections.Generic.HashSet(StringComparer.Ordinal); + var readFlags = new System.Collections.Generic.HashSet(StringComparer.Ordinal); + + if (c.actions != null) + foreach (var action in c.actions) + { + if (action == null) continue; + foreach (var field in action.GetType().GetFields(flagFieldFlags)) + if ((field.Name.Contains("flag", System.StringComparison.OrdinalIgnoreCase) || + field.Name.Contains("Flag", System.StringComparison.OrdinalIgnoreCase)) + && field.FieldType == typeof(string)) + { + var val = field.GetValue(action) as string; + if (!string.IsNullOrEmpty(val)) writtenFlags.Add(val); + } + } + + if (c.conditions != null) + foreach (var cond in c.conditions) + { + if (cond == null) continue; + foreach (var field in cond.GetType().GetFields(flagFieldFlags)) + if ((field.Name.Contains("flag", System.StringComparison.OrdinalIgnoreCase) || + field.Name.Contains("Flag", System.StringComparison.OrdinalIgnoreCase)) + && field.FieldType == typeof(string)) + { + var val = field.GetValue(cond) as string; + if (!string.IsNullOrEmpty(val)) readFlags.Add(val); + } + } + + foreach (var flagId in readFlags) + if (writtenFlags.Contains(flagId)) + AddWarn($"{c.chainId}: 条件读取的标志 '{flagId}' 同时被本链的 Action 写入。" + + "若该标志在触发前已被设置,链将永远无法执行或会产生意外的循环行为。", c); + } + Debug.Log($"[EventChainModule] 验证完成:{allChains.Count} 条事件链,{errorCount} 个错误,{warnCount} 个警告。"); QuestValidationResultWindow.Show(issues, errorCount, warnCount, allChains.Count, "事件链批量验证结果", "事件链"); } diff --git a/Assets/_Game/Scripts/Quest/IQuestManager.cs b/Assets/_Game/Scripts/Quest/IQuestManager.cs index 2758429..55701ed 100644 --- a/Assets/_Game/Scripts/Quest/IQuestManager.cs +++ b/Assets/_Game/Scripts/Quest/IQuestManager.cs @@ -119,6 +119,18 @@ namespace BaseGames.Quest /// 若任务当前可以接取,返回 的实例。 /// QuestLockInfo GetQuestLockInfo(string questId); + + /// + /// 返回当前处于指定状态的所有任务 ID 快照列表。 + /// 适用于任务日志 UI 分组显示、成就系统批量统计等场景。 + /// + System.Collections.Generic.IReadOnlyList GetQuestsInState(QuestStateEnum state); + + /// + /// 对所有已注册任务执行谓词过滤,返回满足条件的任务 ID 快照列表。 + /// 适用于自定义筛选(如"活跃且含 NPC 亲密度门槛")等场景。 + /// + System.Collections.Generic.IReadOnlyList FilterQuests(System.Func predicate); } /// diff --git a/Assets/_Game/Scripts/Quest/QuestManager.cs b/Assets/_Game/Scripts/Quest/QuestManager.cs index 459f2ea..01fb1e1 100644 --- a/Assets/_Game/Scripts/Quest/QuestManager.cs +++ b/Assets/_Game/Scripts/Quest/QuestManager.cs @@ -122,6 +122,10 @@ namespace BaseGames.Quest /// 已广播过 EVT_QuestReadyToComplete 的任务 ID 集合(防重复通知)。 /// 任务完成/失败时从集合移除,再次激活后可重新通知。 private readonly HashSet _notifiedReadyQuests = new(); +#if UNITY_EDITOR || DEVELOPMENT_BUILD + /// 任务暂停时记录的 realtimeSinceStartup(供 ResumeQuest 计算暂停持续时长并日志输出)。 + private readonly Dictionary _pauseTimestamps = new(); +#endif /// 供需要直接订阅 SO 频道的系统使用(推荐使用 IQuestEventSource C# 事件)。 public StringEventChannelSO QuestStartedChannel => _onQuestStarted; /// 供需要直接订阅 SO 频道的系统使用(推荐使用 IQuestEventSource C# 事件)。 @@ -313,6 +317,10 @@ namespace BaseGames.Quest 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); } @@ -327,6 +335,14 @@ namespace BaseGames.Quest 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); } @@ -367,16 +383,18 @@ namespace BaseGames.Quest #if UNITY_EDITOR || DEVELOPMENT_BUILD Debug.LogWarning( $"[QuestManager] 任务 '{quest.questId}' 好感度奖励 +{quest.reward.affinityBonus} " + - $"将超出 NPC '{quest.GiverNpcId}' 的上限 {maxAffinity}(当前 {current}),已截断至 {maxAffinity}。"); + $"将超出 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 = quest.reward.affinityBonus, + delta = actualDelta, newTotal = newTotal }); } @@ -503,7 +521,7 @@ namespace BaseGames.Quest 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 }; @@ -512,36 +530,8 @@ namespace BaseGames.Quest return new QuestLockInfo { Reason = QuestLockReason.InsufficientAffinity, Param = $"{affinity}/{quest.minAffinityToAccept}" }; } - // 前置任务依赖检查(新版优先,回退旧版) -#pragma warning disable CS0618 - var deps = quest.prerequisites.HasAny ? quest.prerequisites.questDependencies : quest.prerequisiteQuests; -#pragma warning restore CS0618 - if (deps != null) - { - foreach (var dep in deps) - { - if (dep == null || string.IsNullOrEmpty(dep.questId)) continue; - if (GetState(dep.questId) != QuestStateEnum.Completed) - return new QuestLockInfo { Reason = QuestLockReason.RequiresQuest, Param = dep.questId }; - } - } - - // 世界标志条件检查 - var fc = quest.prerequisites.HasAny ? quest.prerequisites.flagCondition : default; -#pragma warning disable CS0618 - if (!quest.prerequisites.HasAny && quest.prerequisiteFlags != null && quest.prerequisiteFlags.Length > 0) - fc = new QuestPrerequisite.FlagCondition - { flags = quest.prerequisiteFlags, logic = quest.prerequisiteFlagsLogic }; -#pragma warning restore CS0618 - - if (fc.flags != null && fc.flags.Length > 0) - { - var svc = BaseGames.Core.ServiceLocator.GetOrDefault(); - if (svc != null && !EvaluateFlagPrerequisites(fc.flags, fc.logic, svc)) - return new QuestLockInfo { Reason = QuestLockReason.FlagConditionNotMet }; - } - - return new QuestLockInfo { Reason = QuestLockReason.None }; // 无锁定,任务可接取 + // 前置依赖 + 标志检查委托给 CheckQuestDepsAndFlags(单一权威实现) + return CheckQuestDepsAndFlags(quest); } #if UNITY_EDITOR || DEVELOPMENT_BUILD @@ -687,67 +677,9 @@ namespace BaseGames.Quest private bool CanAccept(string questId) { + // 状态必须为 Available;其余门槛检查委托给 GetQuestLockInfo(单一权威实现)。 if (GetState(questId) != QuestStateEnum.Available) return false; - var quest = GetQuestSO(questId); - if (quest == null) return false; - - // 好感度门槛检查:_npcRelations 仅在 OnLoad 后有效 - if (quest.minAffinityToAccept > 0 && !string.IsNullOrEmpty(quest.GiverNpcId)) - { - if (!_affinityInitialized) - { -#if UNITY_EDITOR || DEVELOPMENT_BUILD - Debug.LogWarning( - $"[QuestManager] CanAccept: 好感度数据尚未从存档加载(OnLoad 未完成)," + - $"任务 '{questId}' 的好感度门槛检查暂时拒绝接取。"); -#endif - return false; - } - _npcRelations.TryGetValue(quest.GiverNpcId, out int affinity); - if (affinity < quest.minAffinityToAccept) return false; - } - - // 前置条件检查:优先使用新版 prerequisites 结构,回退到旧版字段 - if (quest.prerequisites.HasAny) - { - if (quest.prerequisites.questDependencies != null) - foreach (var dep in quest.prerequisites.questDependencies) - { - if (dep == null) continue; - if (GetState(dep.questId) != QuestStateEnum.Completed) return false; - } - var fc = quest.prerequisites.flagCondition; - if (fc.flags != null && fc.flags.Length > 0) - { - var svc = BaseGames.Core.ServiceLocator.GetOrDefault(); - if (svc != null && !EvaluateFlagPrerequisites(fc.flags, fc.logic, svc)) return false; - } - } - else - { - // 旧版字段回退(兼容现有资产) - if (quest.prerequisiteQuests != null) - foreach (var pre in quest.prerequisiteQuests) - { - if (pre == null) continue; - if (GetState(pre.questId) != QuestStateEnum.Completed) return false; - } - - 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 false; - } -#if UNITY_EDITOR || DEVELOPMENT_BUILD - else Debug.LogWarning( - $"[QuestManager] CanAccept: 任务 '{questId}' 的 prerequisiteFlags 需要 ISaveService,但服务未注册,标志检查已跳过。"); -#endif - } - } - - return true; + return !GetQuestLockInfo(questId).IsLocked; } /// @@ -784,17 +716,26 @@ namespace BaseGames.Quest } /// - /// 检查任务是否满足全部前置条件(不含状态检查),用于 InitializeAvailableQuests 初始化。 - /// 与 CanAccept 的区别:CanAccept 需要任务已经是 Available;此方法仅判断前置依赖是否达成。 - /// 优先读取新版 结构;若未配置则回退到旧版字段。 + /// 检查任务是否满足全部前置条件(不含状态和亲密度检查),用于 InitializeAvailableQuests 初始化。 + /// 与 CanAccept 的区别:CanAccept 需要任务已经是 Available 且包含亲密度检查;此方法仅判断前置依赖是否达成。 + /// 委托给 实现,不再重复前置逻辑。 /// private bool MeetsPrerequisites(QuestSO quest) { - if (quest == null) return false; + 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) { - // 新版前置结构:questDependencies + flagCondition if (quest.prerequisites.questDependencies != null) foreach (var dep in quest.prerequisites.questDependencies) { @@ -802,25 +743,27 @@ namespace BaseGames.Quest if (string.IsNullOrEmpty(dep.questId)) { #if UNITY_EDITOR || DEVELOPMENT_BUILD - Debug.LogWarning( - $"[QuestManager] 任务 '{quest.questId}' 的 prerequisites.questDependencies 含 questId 为空的条目,已跳过。"); + Debug.LogWarning($"[QuestManager] 任务 '{quest.questId}' 的 prerequisites.questDependencies 含 questId 为空的条目,已跳过。"); #endif continue; } - if (GetState(dep.questId) != QuestStateEnum.Completed) return false; + 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(); - if (svc != null && !EvaluateFlagPrerequisites(fc.flags, fc.logic, svc)) return false; // 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) { @@ -828,12 +771,12 @@ namespace BaseGames.Quest if (string.IsNullOrEmpty(pre.questId)) { #if UNITY_EDITOR || DEVELOPMENT_BUILD - Debug.LogWarning( - $"[QuestManager] 任务 '{quest.questId}' 的 prerequisiteQuests 含 questId 为空的条目,已跳过该前置条件。"); + Debug.LogWarning($"[QuestManager] 任务 '{quest.questId}' 的 prerequisiteQuests 含 questId 为空的条目,已跳过该前置条件。"); #endif continue; } - if (GetState(pre.questId) != QuestStateEnum.Completed) return false; + if (GetState(pre.questId) != QuestStateEnum.Completed) + return new QuestLockInfo { Reason = QuestLockReason.RequiresQuest, Param = pre.questId }; } if (quest.prerequisiteFlags != null && quest.prerequisiteFlags.Length > 0) @@ -841,13 +784,36 @@ namespace BaseGames.Quest var svc = BaseGames.Core.ServiceLocator.GetOrDefault(); if (svc != null) { - if (!EvaluateFlagPrerequisites(quest.prerequisiteFlags, quest.prerequisiteFlagsLogic, svc)) return false; + if (!EvaluateFlagPrerequisites(quest.prerequisiteFlags, quest.prerequisiteFlagsLogic, svc)) + return new QuestLockInfo { Reason = QuestLockReason.FlagConditionNotMet }; } - // ISaveService 未就绪(Awake 阶段)→ 保守跳过;OnLoad 后 InitializeAvailableQuests 重新评估 +#if UNITY_EDITOR || DEVELOPMENT_BUILD + else Debug.LogWarning($"[QuestManager] 任务 '{quest.questId}' 的 prerequisiteFlags 需要 ISaveService,但服务未注册,标志检查已跳过。"); +#endif } +#pragma warning restore CS0618 } - return true; + return new QuestLockInfo { Reason = QuestLockReason.None }; + } + + /// + public System.Collections.Generic.IReadOnlyList GetQuestsInState(QuestStateEnum state) + { + var result = new List(); + foreach (var (id, s) in _questStates) + if (s == state) result.Add(id); + return result; + } + + /// + public System.Collections.Generic.IReadOnlyList FilterQuests(System.Func predicate) + { + if (predicate == null) return System.Array.Empty(); + var result = new List(); + foreach (var (id, state) in _questStates) + if (predicate(id, state)) result.Add(id); + return result; } /// diff --git a/Assets/_Game/Scripts/Quest/QuestSO.cs b/Assets/_Game/Scripts/Quest/QuestSO.cs index a4c1a8e..280d05f 100644 --- a/Assets/_Game/Scripts/Quest/QuestSO.cs +++ b/Assets/_Game/Scripts/Quest/QuestSO.cs @@ -200,10 +200,16 @@ namespace BaseGames.Quest /// /// 深度优先遍历前置链,检测是否存在环路。 /// 已访问节点集 在回溯时移除,保证同一链条中不误报平行分支。 + /// 超过 32 层时停止递归并输出警告,防止编辑器因超深嵌套卡顿。 /// private static bool HasPrerequisiteCycle(QuestSO quest, - System.Collections.Generic.HashSet visited) + System.Collections.Generic.HashSet visited, int depth = 0) { + if (depth > 32) + { + Debug.LogWarning($"[QuestSO] 前置链深度超过 32 层(路径末端:'{quest.name}'),已停止检测。请减少任务链深度。"); + return false; + } if (string.IsNullOrEmpty(quest.questId)) return false; if (!visited.Add(quest.questId)) return true; // 已在当前路径上 = 环路 @@ -215,7 +221,7 @@ namespace BaseGames.Quest { foreach (var dep in deps) { - if (dep != null && HasPrerequisiteCycle(dep, visited)) + if (dep != null && HasPrerequisiteCycle(dep, visited, depth + 1)) return true; } } @@ -250,10 +256,16 @@ namespace BaseGames.Quest /// /// 深度优先遍历 branches[].nextQuest 链,检测是否存在环路(DFS 回溯)。 + /// 超过 32 层时停止递归并输出警告,防止编辑器因超深嵌套卡顿。 /// private static bool HasBranchCycle(QuestSO quest, - System.Collections.Generic.HashSet visited) + System.Collections.Generic.HashSet visited, int depth = 0) { + if (depth > 32) + { + Debug.LogWarning($"[QuestSO] 分支链深度超过 32 层(路径末端:'{quest.name}'),已停止检测。请减少分支链深度。"); + return false; + } if (string.IsNullOrEmpty(quest.questId)) return false; if (!visited.Add(quest.questId)) return true; // 已在路径上 = 环路 @@ -261,7 +273,7 @@ namespace BaseGames.Quest { foreach (var branch in quest.branches) { - if (branch?.nextQuest != null && HasBranchCycle(branch.nextQuest, visited)) + if (branch?.nextQuest != null && HasBranchCycle(branch.nextQuest, visited, depth + 1)) return true; } } diff --git a/Assets/_Game/Scripts/World/WorldStateRegistry.cs b/Assets/_Game/Scripts/World/WorldStateRegistry.cs index bf1efc7..33be38a 100644 --- a/Assets/_Game/Scripts/World/WorldStateRegistry.cs +++ b/Assets/_Game/Scripts/World/WorldStateRegistry.cs @@ -124,10 +124,11 @@ namespace BaseGames.World if (data == null) return; - foreach (var id in data.World.CollectedIds) Mark(WorldObjectCategory.Collectible, id); - foreach (var id in data.World.ActivatedSavePoints) Mark(WorldObjectCategory.SavePoint, id); - foreach (var id in data.World.OpenedDoors) Mark(WorldObjectCategory.Door, id); - foreach (var id in data.World.DestroyedObjectIds) Mark(WorldObjectCategory.Destroyed, id); + var world = data.World; + if (world?.CollectedIds != null) foreach (var id in world.CollectedIds) Mark(WorldObjectCategory.Collectible, id); + if (world?.ActivatedSavePoints != null) foreach (var id in world.ActivatedSavePoints) Mark(WorldObjectCategory.SavePoint, id); + if (world?.OpenedDoors != null) foreach (var id in world.OpenedDoors) Mark(WorldObjectCategory.Door, id); + if (world?.DestroyedObjectIds != null) foreach (var id in world.DestroyedObjectIds) Mark(WorldObjectCategory.Destroyed, id); if (data.EventChains?.WorldFlags != null) foreach (var kv in data.EventChains.WorldFlags)