From da2948dff8d6dea81efac6a013ab0e9e807c08d2 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Mon, 25 May 2026 01:00:32 +0800 Subject: [PATCH] refactor: Round 53 remove all legacy backward-compatibility code - QuestSO: remove giverNpcId, prerequisiteQuests/Flags/FlagsLogic, failCondition, conditionFlags, npcDialogueKey fields; simplify GiverNpcId property to giverNpc?.npcId; clean ValidatePrerequisiteCycles/HasPrerequisiteCycle to use prerequisites.questDependencies; remove ValidateBranchDialogueKeys migration warning block; clean QuestPrerequisite doc - QuestManager: remove OnLoad DataVersion 1/2 migration paths (ProgressCounts, hasNewFormat/ useNewFormat); remove CheckQuestDepsAndFlags old-field fallback (prerequisiteQuests/Flags); remove UnlockBranches conditionFlags fallback; remove DispatchEvent failCondition fallback; fix ValidatePrerequisites DFS to scan prerequisites.questDependencies - SaveData: remove ProgressCounts (Obsolete), ObjectiveIndex (unused), GiverNpcId (never written) fields from QuestState; simplify DataVersion doc comment - QuestSOEditor: replace migration-only editor with minimal DrawDefaultInspector - QuestModule: update all prerequisiteQuests/conditionFlags/npcDialogueKey/failCondition references to canonical new fields; update ValidateBranchFlags check 10 - FlagAuditModule: replace conditionFlags/prerequisiteFlags scans with conditionFlagEntries/ prerequisites.flagCondition.flags - NpcSO: remove QuestSO.giverNpcId reference from npcId tooltip - NpcAffinityEvent/RewardSO: update doc comments to reference giverNpc instead of giverNpcId Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Assets/_Game/Scripts/Core/Save/SaveData.cs | 21 +-- Assets/_Game/Scripts/Dialogue/NpcSO.cs | 2 +- .../Scripts/Editor/Modules/FlagAuditModule.cs | 16 +- .../Scripts/Editor/Modules/QuestModule.cs | 66 ++++--- .../Scripts/Editor/Quest/QuestSOEditor.cs | 101 +---------- .../_Game/Scripts/Quest/NpcAffinityEvent.cs | 3 +- Assets/_Game/Scripts/Quest/QuestManager.cs | 164 ++++-------------- Assets/_Game/Scripts/Quest/QuestSO.cs | 87 ++-------- Assets/_Game/Scripts/Quest/RewardSO.cs | 2 +- 9 files changed, 97 insertions(+), 365 deletions(-) diff --git a/Assets/_Game/Scripts/Core/Save/SaveData.cs b/Assets/_Game/Scripts/Core/Save/SaveData.cs index 54060ec..8b0900b 100644 --- a/Assets/_Game/Scripts/Core/Save/SaveData.cs +++ b/Assets/_Game/Scripts/Core/Save/SaveData.cs @@ -134,31 +134,20 @@ namespace BaseGames.Core.Save [Serializable] public class QuestState { - /// - /// 此 QuestState 数据格式版本号。 - /// 1 = 原始格式(ProgressCounts 按索引,已弃用) - /// 2 = Round 24+ 格式(ObjectiveProgress 按 objectiveId 键值对) - /// 3 = Round 52+ 格式(新增 ObjectiveCompleted、StartedAtUtc、CompletedAtUtc) - /// OnLoad 按此字段显式选择解析路径,杜绝依赖 Count > 0 的隐式推断。 - /// + /// 此 QuestState 数据格式版本号,固定为 3。 public int DataVersion = 3; /// 任务运行时状态字符串。有效值:Unavailable|Available|Active|Paused|Completed|Failed。 /// OnLoad 通过 Enum.TryParse 解析;无效值将触发开发模式警告并降级为 Unavailable。 public string Status; - public int ObjectiveIndex; - /// 新格式(DataVersion≥2):objectiveId → progressCount,重排目标顺序后存档不会错位。 + /// objectiveId → progressCount,重排目标顺序后存档不会错位。 public Dictionary ObjectiveProgress = new(); /// 各目标是否已判定完成(objectiveId → completed)。 - /// 防止 GetRequiredCount 在版本迭代中变更后,重新计算结果与存档实际状态不一致。DataVersion≥3 写入。 + /// 防止 GetRequiredCount 在版本迭代中变更后,重新计算结果与存档实际状态不一致。 public Dictionary ObjectiveCompleted = new(); - /// 任务接取时间(Unix 秒时间戳,UTC)。0 = 未记录(旧存档)。DataVersion≥3 写入。 + /// 任务接取时间(Unix 秒时间戳,UTC)。0 = 未记录,跳过。 public long StartedAtUtc; - /// 任务完成时间(Unix 秒时间戳,UTC)。0 = 未完成或未记录。DataVersion≥3 写入。 + /// 任务完成时间(Unix 秒时间戳,UTC)。0 = 未完成或未记录。 public long CompletedAtUtc; - /// 旧格式(按数组索引,DataVersion=1):仅用于迁移旧版存档,新存档不再写入。已弃用,将在后续版本移除。 - [System.Obsolete("旧格式存档兼容字段,仅供 OnLoad DataVersion=1 迁移使用。新存档改用 ObjectiveProgress(objectiveId 键值对)。")] - public List ProgressCounts = new(); - public string GiverNpcId; } // ─── Achievements ───────────────────────────────────────────────────────── diff --git a/Assets/_Game/Scripts/Dialogue/NpcSO.cs b/Assets/_Game/Scripts/Dialogue/NpcSO.cs index 42d7271..3114095 100644 --- a/Assets/_Game/Scripts/Dialogue/NpcSO.cs +++ b/Assets/_Game/Scripts/Dialogue/NpcSO.cs @@ -17,7 +17,7 @@ namespace BaseGames.Dialogue public class NpcSO : ScriptableObject { [Header("标识")] - [Tooltip("NPC 唯一 ID,如 \"NPC_Elder\"。需与 InteractableNPC._npcId、QuestSO.giverNpcId 保持一致。")] + [Tooltip("NPC 唯一 ID,如 \"NPC_Elder\"。需与 InteractableNPC._npcId 保持一致。")] public string npcId; [Header("显示")] diff --git a/Assets/_Game/Scripts/Editor/Modules/FlagAuditModule.cs b/Assets/_Game/Scripts/Editor/Modules/FlagAuditModule.cs index 3a007db..0e95ba6 100644 --- a/Assets/_Game/Scripts/Editor/Modules/FlagAuditModule.cs +++ b/Assets/_Game/Scripts/Editor/Modules/FlagAuditModule.cs @@ -155,17 +155,17 @@ namespace BaseGames.Editor.Modules // 3. 扫描 QuestSO foreach (var quest in AssetOperations.FindAll()) { - // branches[i].conditionFlags → 读取 + // branches[i].conditionFlagEntries → 读取 if (quest.branches != null) foreach (var branch in quest.branches) - if (branch.conditionFlags != null) - foreach (var fid in branch.conditionFlags) - if (!string.IsNullOrEmpty(fid)) - GetOrCreate(fid).readLocations.Add(($"任务分支条件 [{quest.name}]", quest)); + if (branch.conditionFlagEntries != null) + foreach (var entry in branch.conditionFlagEntries) + if (!string.IsNullOrEmpty(entry.flagId)) + GetOrCreate(entry.flagId).readLocations.Add(($"任务分支条件 [{quest.name}]", quest)); - // prerequisiteFlags → 读取 - if (quest.prerequisiteFlags != null) - foreach (var fid in quest.prerequisiteFlags) + // prerequisites.flagCondition.flags → 读取 + if (quest.prerequisites.flagCondition.flags != null) + foreach (var fid in quest.prerequisites.flagCondition.flags) if (!string.IsNullOrEmpty(fid)) GetOrCreate(fid).readLocations.Add(($"任务前置标志 [{quest.name}]", quest)); } diff --git a/Assets/_Game/Scripts/Editor/Modules/QuestModule.cs b/Assets/_Game/Scripts/Editor/Modules/QuestModule.cs index 3da3b87..6f51cbc 100644 --- a/Assets/_Game/Scripts/Editor/Modules/QuestModule.cs +++ b/Assets/_Game/Scripts/Editor/Modules/QuestModule.cs @@ -42,7 +42,7 @@ namespace BaseGames.Editor.Modules Folder, Prefix, s => { - bool hasPre = s.prerequisiteQuests != null && s.prerequisiteQuests.Length > 0; + bool hasPre = s.prerequisites.questDependencies != null && s.prerequisites.questDependencies.Length > 0; // 徽章:分类 + 有前置 string catLabel = s.category switch { @@ -88,7 +88,7 @@ namespace BaseGames.Editor.Modules } _listPane.ExtraFilter = q => { - if (filterPrereq && (q.prerequisiteQuests == null || q.prerequisiteQuests.Length == 0)) return false; + if (filterPrereq && (q.prerequisites.questDependencies == null || q.prerequisites.questDependencies.Length == 0)) return false; if (filterNoObj && (q.objectives != null && q.objectives.Length > 0)) return false; if (filterCanFail && !q.canFail) return false; if (filterCategory.HasValue && q.category != filterCategory.Value) return false; @@ -223,16 +223,16 @@ namespace BaseGames.Editor.Modules }; SkillModule.AddChip(card, "分类", catDisplay); - // 发布 NPC:优先显示 giverNpc.npcId,回退旧 giverNpcId + // 发布 NPC string giverId = s.GiverNpcId; if (!string.IsNullOrEmpty(giverId)) SkillModule.AddChip(card, "发布 NPC", giverId); - if (s.prerequisiteQuests != null && s.prerequisiteQuests.Length > 0) + if (s.prerequisites.questDependencies != null && s.prerequisites.questDependencies.Length > 0) { // 显示每个前置任务的 questId,方便策划一眼看清依赖链 var preIds = new System.Text.StringBuilder(); - foreach (var pre in s.prerequisiteQuests) + foreach (var pre in s.prerequisites.questDependencies) { if (pre == null) continue; if (preIds.Length > 0) preIds.Append(", "); @@ -384,12 +384,8 @@ namespace BaseGames.Editor.Modules SkillModule.AddChip(row, "条件", condition); SkillModule.AddChip(row, "后续任务", next); - // 优先显示新 SO 引用;回退到旧字段(Obsolete) - string seqName = branch.npcDialogueSequence != null - ? branch.npcDialogueSequence.name -#pragma warning disable CS0618 - : branch.npcDialogueKey; -#pragma warning restore CS0618 + // 优先显示 SO 引用名称 + string seqName = branch.npcDialogueSequence != null ? branch.npcDialogueSequence.name : null; if (!string.IsNullOrEmpty(seqName)) SkillModule.AddChip(row, "对话序列", seqName); card.Add(row); @@ -401,7 +397,7 @@ namespace BaseGames.Editor.Modules /// 构建当前任务的依赖关系可视图(折叠面板形式): /// - 上方:前置任务链(此任务需要哪些任务先完成) /// - 下方:后续任务链(此任务完成后可解锁哪些任务) - /// 数据来源:allQuests 中所有 QuestSO 的 prerequisiteQuests 引用,无运行时副作用。 + /// 数据来源:allQuests 中所有 QuestSO 的 prerequisites.questDependencies 引用,无运行时副作用。 /// 节点可点击→选中对应资产(EditorGUIUtility.PingObject)。 /// private static VisualElement BuildDependencyGraph(QuestSO s) @@ -436,10 +432,10 @@ namespace BaseGames.Editor.Modules var allQuests = s_allQuestCache; // ── 前置任务(上游)──────────────────────────────────────────────── - bool hasPrereqs = s.prerequisiteQuests != null && s.prerequisiteQuests.Length > 0; + bool hasPrereqs = s.prerequisites.questDependencies != null && s.prerequisites.questDependencies.Length > 0; AddDepSection(container, "▲ 前置任务(需先完成)", hasPrereqs - ? System.Array.ConvertAll(s.prerequisiteQuests, q => (q, "前置")) + ? System.Array.ConvertAll(s.prerequisites.questDependencies, q => (q, "前置")) : null, hasPrereqs ? null : "(无前置条件,可直接接取)"); @@ -448,8 +444,8 @@ namespace BaseGames.Editor.Modules foreach (var quest in allQuests) { if (quest == null || quest == s) continue; - if (quest.prerequisiteQuests == null) continue; - foreach (var pre in quest.prerequisiteQuests) + if (quest.prerequisites.questDependencies == null) continue; + foreach (var pre in quest.prerequisites.questDependencies) { if (pre == s) { downstream.Add((quest, "解锁")); break; } } @@ -492,8 +488,8 @@ namespace BaseGames.Editor.Modules /// private static bool HasPrerequisiteCycle(QuestSO origin, QuestSO current, System.Collections.Generic.HashSet visited) { - if (current?.prerequisiteQuests == null) return false; - foreach (var pre in current.prerequisiteQuests) + if (current?.prerequisites.questDependencies == null) return false; + foreach (var pre in current.prerequisites.questDependencies) { if (pre == null || string.IsNullOrEmpty(pre.questId)) continue; if (pre == origin) return true; // 回到起点,发现循环 @@ -705,13 +701,13 @@ namespace BaseGames.Editor.Modules /// 1. questId 为空 /// 2. questId 重复 /// 3. objectives 为空(无目标任务) - /// 4. prerequisiteQuests 含空引用 + /// 4. prerequisites.questDependencies 含空引用 /// 5. 前置任务循环依赖(DFS) - /// 6. canFail=true 但 failCondition 为空 - /// 7. reward.affinityBonus != 0 但 giverNpcId 为空(好感度会丢失) + /// 6. canFail=true 但 failConditions 为空 + /// 7. reward.affinityBonus != 0 但 giverNpc 为空(好感度会丢失) /// 8. TriggerZone ↔ ReachAreaObjective markerTag 孤儿交叉检测 /// 9. 同任务内 objectiveId 重复(运行时 compositeKey 碰撞) - /// 10. branches[i].conditionFlags 含空白字符串(策划配置遗漏 flag 名) + /// 10. branches[i].conditionFlagEntries 含空 flagId(策划配置遗漏 flag 名) /// 11. reward.itemIds 含空白字符串或无对应 Collectible 预制件(孤儿奖励 ID) /// 结果在可交互的 QuestValidationResultWindow 中展示,每项问题附"选中"按钮可一键定位资产。 /// @@ -778,18 +774,18 @@ namespace BaseGames.Editor.Modules if (q.objectives == null || q.objectives.Length == 0) addWarn($"{q.questId}: objectives 为空,任务无任何目标。", q); - if (q.prerequisiteQuests != null) - foreach (var pre in q.prerequisiteQuests) - if (pre == null) { addWarn($"{q.questId}: prerequisiteQuests 含空引用,请清理 Inspector 中的空槽。", q); break; } + if (q.prerequisites.questDependencies != null) + foreach (var pre in q.prerequisites.questDependencies) + if (pre == null) { addWarn($"{q.questId}: prerequisites.questDependencies 含空引用,请清理 Inspector 中的空槽。", q); break; } if (HasCircularPrerequisite(q, idMap, new HashSet(StringComparer.Ordinal))) addError($"{q.questId}: 前置任务链存在循环依赖,将导致任务永远无法变为 Available!", q); - if (q.canFail && q.failCondition == null) - addWarn($"{q.questId}: canFail=true 但 failCondition 为空,失败条件永不触发。", q); + if (q.canFail && (q.failConditions == null || q.failConditions.Length == 0)) + addWarn($"{q.questId}: canFail=true 但 failConditions 为空,失败条件永不触发。", q); if (q.reward != null && q.reward.affinityBonus != 0 && string.IsNullOrEmpty(q.GiverNpcId)) - addWarn($"{q.questId}: reward.affinityBonus={q.reward.affinityBonus} 但 GiverNpcId 为空,好感度增量将丢失。", q); + addWarn($"{q.questId}: reward.affinityBonus={q.reward.affinityBonus} 但 giverNpc 未配置,好感度增量将丢失。", q); } } @@ -839,7 +835,7 @@ namespace BaseGames.Editor.Modules } } - // 检查 10:branches[i].conditionFlags 含空白字符串 + // 检查 10:branches[i].conditionFlagEntries 含空 flagId private static void ValidateBranchFlags( List allQuests, System.Action addWarn) @@ -850,10 +846,10 @@ namespace BaseGames.Editor.Modules for (int bi = 0; bi < q.branches.Length; bi++) { var branch = q.branches[bi]; - if (branch.conditionFlags == null || branch.conditionFlags.Length == 0) continue; - for (int fi = 0; fi < branch.conditionFlags.Length; fi++) - if (string.IsNullOrWhiteSpace(branch.conditionFlags[fi])) - addWarn($"任务 '{q.questId}' 分支[{bi}].conditionFlags[{fi}] 为空白字符串,运行时将被跳过,请检查是否遗漏标志名。", q); + if (branch.conditionFlagEntries == null || branch.conditionFlagEntries.Length == 0) continue; + for (int fi = 0; fi < branch.conditionFlagEntries.Length; fi++) + if (string.IsNullOrWhiteSpace(branch.conditionFlagEntries[fi].flagId)) + addWarn($"任务 '{q.questId}' 分支[{bi}].conditionFlagEntries[{fi}].flagId 为空白字符串,运行时将被跳过,请检查是否遗漏标志名。", q); } } } @@ -895,8 +891,8 @@ namespace BaseGames.Editor.Modules HashSet visited) { if (!visited.Add(start.questId)) return true; - if (start.prerequisiteQuests == null) return false; - foreach (var pre in start.prerequisiteQuests) + if (start.prerequisites.questDependencies == null) return false; + foreach (var pre in start.prerequisites.questDependencies) { if (pre == null || string.IsNullOrEmpty(pre.questId)) continue; if (!idMap.TryGetValue(pre.questId, out var preQuest)) continue; diff --git a/Assets/_Game/Scripts/Editor/Quest/QuestSOEditor.cs b/Assets/_Game/Scripts/Editor/Quest/QuestSOEditor.cs index f5ab5d6..ee07ca4 100644 --- a/Assets/_Game/Scripts/Editor/Quest/QuestSOEditor.cs +++ b/Assets/_Game/Scripts/Editor/Quest/QuestSOEditor.cs @@ -1,118 +1,19 @@ using UnityEditor; -using UnityEngine; using BaseGames.Quest; -using BaseGames.Core; namespace BaseGames.Editor.Quest { /// - /// QuestSO 自定义 Inspector。 - /// 在检测到旧版前置字段(prerequisiteQuests / prerequisiteFlags)有数据时, - /// 显示迁移提示框和一键迁移按钮,引导策划将数据迁移到 QuestPrerequisite 统一结构。 + /// QuestSO 自定义 Inspector(默认布局,使用 DrawDefaultInspector)。 /// [CustomEditor(typeof(QuestSO))] public class QuestSOEditor : UnityEditor.Editor { - private bool _showMigrationBox = true; - public override void OnInspectorGUI() { serializedObject.Update(); - - var quest = (QuestSO)target; - - // ── 旧版字段迁移提示 ────────────────────────────────────────────── -#pragma warning disable CS0618 - bool hasLegacyQuests = quest.prerequisiteQuests != null && quest.prerequisiteQuests.Length > 0; - bool hasLegacyFlags = quest.prerequisiteFlags != null && quest.prerequisiteFlags.Length > 0; -#pragma warning restore CS0618 - - if (hasLegacyQuests || hasLegacyFlags) - { - _showMigrationBox = EditorGUILayout.BeginFoldoutHeaderGroup(_showMigrationBox, "⚠ 旧版前置字段迁移"); - if (_showMigrationBox) - { - EditorGUILayout.HelpBox( - "检测到旧版前置字段有数据:\n" + - (hasLegacyQuests ? $" • prerequisiteQuests:{quest.prerequisiteQuests.Length} 项\n" : "") + - (hasLegacyFlags ? $" • prerequisiteFlags:{quest.prerequisiteFlags.Length} 项\n" : "") + - "\n新版 'prerequisites'(QuestPrerequisite)字段已支持更完整的前置配置。\n" + - "点击下方按钮可将旧版数据自动迁移至新字段,迁移后旧字段将被清空。\n" + - "迁移操作可撤销(Ctrl+Z)。", - MessageType.Warning); - - bool hasNewData = quest.prerequisites.HasAny; - if (hasNewData) - EditorGUILayout.HelpBox( - "新版 prerequisites 字段已有数据。点击迁移将与旧版数据合并(去重),不会覆盖现有配置。", - MessageType.Info); - - if (GUILayout.Button("一键迁移旧版前置字段 → prerequisites")) - { - MigrateLegacyPrerequisites(quest); - } - } - EditorGUILayout.EndFoldoutHeaderGroup(); - EditorGUILayout.Space(4); - } - - // ── 默认 Inspector ──────────────────────────────────────────────── DrawDefaultInspector(); - serializedObject.ApplyModifiedProperties(); } - - private static void MigrateLegacyPrerequisites(QuestSO quest) - { - Undo.RecordObject(quest, "迁移 QuestSO 旧版前置字段"); - -#pragma warning disable CS0618 - int legacyQuestCount = quest.prerequisiteQuests?.Length ?? 0; - int legacyFlagCount = quest.prerequisiteFlags?.Length ?? 0; - - // 迁移 prerequisiteQuests → prerequisites.questDependencies(合并去重) - if (quest.prerequisiteQuests != null && quest.prerequisiteQuests.Length > 0) - { - var existing = quest.prerequisites.questDependencies ?? System.Array.Empty(); - var merged = new System.Collections.Generic.HashSet(existing); - foreach (var q in quest.prerequisiteQuests) - if (q != null) merged.Add(q); - - quest.prerequisites.questDependencies = new QuestSO[merged.Count]; - merged.CopyTo(quest.prerequisites.questDependencies); - quest.prerequisiteQuests = System.Array.Empty(); - } - - // 迁移 prerequisiteFlags → prerequisites.flagCondition(合并去重) - if (quest.prerequisiteFlags != null && quest.prerequisiteFlags.Length > 0) - { - var existing = quest.prerequisites.flagCondition.flags ?? System.Array.Empty(); - var merged = new System.Collections.Generic.HashSet( - existing, System.StringComparer.Ordinal); - foreach (var f in quest.prerequisiteFlags) - if (!string.IsNullOrEmpty(f)) merged.Add(f); - - quest.prerequisites.flagCondition.flags = new string[merged.Count]; - merged.CopyTo(quest.prerequisites.flagCondition.flags); - // 迁移逻辑模式(旧字段覆盖新字段,以旧配置为准) - quest.prerequisites.flagCondition.logic = quest.prerequisiteFlagsLogic; - - quest.prerequisiteFlags = System.Array.Empty(); - quest.prerequisiteFlagsLogic = WorldStateFlagLogic.And; - } -#pragma warning restore CS0618 - - EditorUtility.SetDirty(quest); - AssetDatabase.SaveAssets(); - - Debug.Log($"[QuestSOEditor] '{quest.name}' 旧版前置字段迁移完成(任务:{legacyQuestCount} 项,标志:{legacyFlagCount} 项)。", quest); - EditorUtility.DisplayDialog( - "迁移完成", - $"任务 \"{quest.name}\" 旧版前置字段已成功迁移:\n\n" + - $" 前置任务:{legacyQuestCount} 项 → prerequisites.questDependencies\n" + - $" 前置标志:{legacyFlagCount} 项 → prerequisites.flagCondition.flags\n\n" + - "旧版字段已清空。操作可通过 Ctrl+Z 撤销。", - "确定"); - } } } diff --git a/Assets/_Game/Scripts/Quest/NpcAffinityEvent.cs b/Assets/_Game/Scripts/Quest/NpcAffinityEvent.cs index 48a76fd..399afda 100644 --- a/Assets/_Game/Scripts/Quest/NpcAffinityEvent.cs +++ b/Assets/_Game/Scripts/Quest/NpcAffinityEvent.cs @@ -10,7 +10,8 @@ namespace BaseGames.Quest [System.Serializable] public struct NpcAffinityEvent { - /// 发生好感度变化的 NPC ID(与 QuestSO.giverNpcId 保持一致)。 + /// + /// NPC 好感度变化的 NPC ID(与 QuestSO.giverNpc.npcId 保持一致)。 public string npcId; /// 好感度变化量(正值=增加,负值=减少)。 public int delta; diff --git a/Assets/_Game/Scripts/Quest/QuestManager.cs b/Assets/_Game/Scripts/Quest/QuestManager.cs index a971657..0eaf105 100644 --- a/Assets/_Game/Scripts/Quest/QuestManager.cs +++ b/Assets/_Game/Scripts/Quest/QuestManager.cs @@ -438,59 +438,34 @@ namespace BaseGames.Quest 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; + bool hasFlagEntries = branch.conditionFlagEntries != null && branch.conditionFlagEntries.Length > 0; - if (hasFlagConds && saveService != null) + if (hasFlagEntries && saveService != null) { if (branch.conditionFlagsLogic == BaseGames.Core.WorldStateFlagLogic.Or) { conditionMet = false; - if (hasFlagEntries) + foreach (var entry in branch.conditionFlagEntries) { - 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; } - } + if (string.IsNullOrEmpty(entry.flagId)) continue; + bool raw = saveService.GetFlag(entry.flagId); + if (entry.invert ? !raw : raw) { conditionMet = true; break; } } } else { // AND(默认):全部标志均须满足(支持 invert 取反) - if (hasFlagEntries) + foreach (var entry in branch.conditionFlagEntries) { - 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 (string.IsNullOrEmpty(entry.flagId)) continue; + bool raw = saveService.GetFlag(entry.flagId); + if (entry.invert ? raw : !raw) { conditionMet = false; break; } } } } #if UNITY_EDITOR || DEVELOPMENT_BUILD - else if (hasFlagConds && saveService == null) + else if (hasFlagEntries && saveService == null) { Debug.LogWarning( $"[QuestManager] 任务 '{questId}' 分支配置了标志条件,但 ISaveService 未注册," + @@ -700,14 +675,8 @@ namespace BaseGames.Quest 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) + if (saved.ObjectiveProgress != null) { - // 新格式:objectiveId → count,重排顺序后仍可正确恢复 foreach (var obj in quest.objectives) { if (obj == null) continue; @@ -716,29 +685,12 @@ namespace BaseGames.Quest 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; } @@ -775,7 +727,7 @@ namespace BaseGames.Quest /// /// 初始化(或修正)所有任务的 Available/Unavailable 状态。 /// 在 Awake(冷启动)和 OnLoad(存档恢复)后调用。 - /// OnLoad 后 ISaveService 已就绪,会重新评估 prerequisiteFlags, + /// OnLoad 后 ISaveService 已就绪,会重新评估 prerequisites.flagCondition.flags, /// 修正 Awake 期间因服务未就绪而被跳过的标志检查。 /// Active/Completed/Failed 状态来自存档,不重置。 /// @@ -794,7 +746,7 @@ namespace BaseGames.Quest // _affinityInitialized 为 true 说明是 OnLoad 后调用,Awake 期间不打此日志 bool isNewToSave = !_questStates.ContainsKey(q.questId) && _affinityInitialized; #endif - // Available/Unavailable 均重新评估,确保 prerequisiteFlags 变更后状态正确 + // Available/Unavailable 均重新评估,确保 prerequisites.flagCondition.flags 变更后状态正确 _questStates[q.questId] = MeetsPrerequisites(q) ? QuestStateEnum.Available : QuestStateEnum.Unavailable; #if UNITY_EDITOR || DEVELOPMENT_BUILD if (isNewToSave) @@ -824,64 +776,28 @@ namespace BaseGames.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) + if (quest.prerequisites.questDependencies != null) + foreach (var dep in quest.prerequisites.questDependencies) { - 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 (dep == null) continue; + if (string.IsNullOrEmpty(dep.questId)) { - if (pre == null) continue; - if (string.IsNullOrEmpty(pre.questId)) - { #if UNITY_EDITOR || DEVELOPMENT_BUILD - Debug.LogWarning($"[QuestManager] 任务 '{quest.questId}' 的 prerequisiteQuests 含 questId 为空的条目,已跳过该前置条件。"); + Debug.LogWarning($"[QuestManager] 任务 '{quest.questId}' 的 prerequisites.questDependencies 含 questId 为空的条目,已跳过。"); #endif - continue; - } - if (GetState(pre.questId) != QuestStateEnum.Completed) - return new QuestLockInfo { Reason = QuestLockReason.RequiresQuest, Param = pre.questId }; + continue; } + if (GetState(dep.questId) != QuestStateEnum.Completed) + return new QuestLockInfo { Reason = QuestLockReason.RequiresQuest, Param = dep.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 + 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 }; } return new QuestLockInfo { Reason = QuestLockReason.None }; @@ -1065,22 +981,14 @@ namespace BaseGames.Quest // 设计意图:暂停期间目标冻结,失败条件也不判定,恢复后再继续检查。 if (quest.canFail) { - // P3-A:多失败条件支持——failConditions 数组中任意一个达成即失败 bool triggered = false; - if (quest.failConditions != null && quest.failConditions.Length > 0) + if (quest.failConditions != null) { 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(); @@ -1181,7 +1089,7 @@ namespace BaseGames.Quest #if UNITY_EDITOR || DEVELOPMENT_BUILD /// - /// 通过 DFS 后序遍历检测 prerequisiteQuests 中是否存在循环引用。 + /// 通过 DFS 后序遍历检测 prerequisites.questDependencies 中是否存在循环引用。 /// 在编辑器 OnValidate 及开发构建 Awake 时调用,发现问题立即打 LogError。 /// [UnityEngine.ContextMenu("校验前置任务循环引用")] @@ -1219,10 +1127,10 @@ namespace BaseGames.Quest color[startId] = 1; path.Add(startId); - var prereqs = index[startId].prerequisiteQuests; - if (prereqs != null) + var prereqDeps = index[startId].prerequisites.questDependencies; + if (prereqDeps != null) { - foreach (var pre in prereqs) + foreach (var pre in prereqDeps) { if (pre == null || string.IsNullOrEmpty(pre.questId)) continue; if (HasCycle(pre.questId, path)) return true; diff --git a/Assets/_Game/Scripts/Quest/QuestSO.cs b/Assets/_Game/Scripts/Quest/QuestSO.cs index c30aa71..6914d9c 100644 --- a/Assets/_Game/Scripts/Quest/QuestSO.cs +++ b/Assets/_Game/Scripts/Quest/QuestSO.cs @@ -16,21 +16,13 @@ namespace BaseGames.Quest [Tooltip("任务唯一 ID,如 \"Quest_FindMushroom\"。运行时由 QuestManager 以此为键索引,必须全局唯一。")] public string questId; - [Tooltip("发布/完成该任务的 NPC(直接引用 NpcSO 资产,推荐)。\n" + + [Tooltip("发布/完成该任务的 NPC(直接引用 NpcSO 资产)。\n" + "用于完成任务后向该 NPC 应用 affinityBonus,及 CanAccept 好感度门槛检查。\n" + - "留空时跳过好感度相关逻辑;与旧字段 giverNpcId 同时有值时以此 SO 为准。")] + "留空时跳过好感度相关逻辑。")] public NpcSO giverNpc; - [System.Obsolete("已废弃,请改用 giverNpc(NpcSO 直接引用)。保留以兼容现有资产序列化。")] - [HideInInspector] - public string giverNpcId; - - /// 运行时使用的 NPC ID:giverNpc 优先,回退到旧字段 giverNpcId。 - public string GiverNpcId => (giverNpc != null && !string.IsNullOrEmpty(giverNpc.npcId)) - ? giverNpc.npcId -#pragma warning disable CS0618 - : giverNpcId; -#pragma warning restore CS0618 + /// 运行时使用的 NPC ID(来自 giverNpc.npcId)。 + public string GiverNpcId => giverNpc != null ? giverNpc.npcId : string.Empty; [Tooltip("本地化 Key,格式如 \"Quest_FindMushroom_Name\"。通过 LocalizationManager.Get(displayNameKey, \"Quest\") 显示。")] public string displayNameKey; @@ -52,28 +44,9 @@ namespace BaseGames.Quest public QuestObjectiveSO[] objectives; [Header("前置条件")] - [Tooltip("任务前置条件(统一配置版)。将前置任务依赖和世界标志依赖合并为单一结构,便于 Inspector 管理。\n" + - "如旧版字段(prerequisiteQuests / prerequisiteFlags)已有数据,运行时将自动回退使用旧版字段,无需手动迁移。")] + [Tooltip("任务前置条件(前置任务依赖 + 世界标志依赖)。留空表示无前置限制。")] public QuestPrerequisite prerequisites = new QuestPrerequisite(); - // ── 旧版前置字段(向后兼容,新配置请改用 prerequisites)──────────────── - [HideInInspector] - [Tooltip("【已归入 prerequisites.questDependencies,此字段仅用于旧资产兼容】\n" + - "所有前置任务必须处于 Completed 状态,本任务才能被接取。\n" + - "⚠ 此字段计划在 v2.0 移除,请尽快通过 QuestSOEditor 迁移至 prerequisites。")] - public QuestSO[] prerequisiteQuests; - - [HideInInspector] - [Tooltip("【已归入 prerequisites.flagCondition.logic,此字段仅用于旧资产兼容】\n" + - "⚠ 此字段计划在 v2.0 移除,请尽快通过 QuestSOEditor 迁移至 prerequisites。")] - public BaseGames.Core.WorldStateFlagLogic prerequisiteFlagsLogic = BaseGames.Core.WorldStateFlagLogic.And; - - [HideInInspector] - [Tooltip("【已归入 prerequisites.flagCondition.flags,此字段仅用于旧资产兼容】\n" + - "⚠ 此字段计划在 v2.0 移除,请尽快通过 QuestSOEditor 迁移至 prerequisites。")] - [BaseGames.Core.WorldStateFlag] - public string[] prerequisiteFlags; - [Tooltip("接取本任务所需的 NPC 好感度下限(0 = 无限制)。由好感度系统提供实际数值。")] public int minAffinityToAccept; @@ -88,10 +61,6 @@ namespace BaseGames.Quest [Tooltip("失败判定目标列表(任意一个达成即失败)。canFail=true 时有效。\n" + "支持多个失败条件(如「BOSS 在限时内未被击败」OR「关键 NPC 死亡」)。")] public QuestObjectiveSO[] failConditions; - [System.Obsolete("已废弃,请改用 failConditions(数组,支持多个失败条件)。保留以兼容现有资产序列化。")] - [HideInInspector] - [Tooltip("(旧版单一失败条件,已被 failConditions 数组取代。保留以兼容现有资产。)")] - public QuestObjectiveSO failCondition; [Header("接取/完成对话")] [Tooltip("玩家接取任务时自动触发的 NPC 对话序列(如 NPC 委托台词)。\n" + @@ -182,10 +151,7 @@ namespace BaseGames.Quest var visited = new System.Collections.Generic.HashSet(System.StringComparer.Ordinal); visited.Add(questId); -#pragma warning disable CS0618 - QuestSO[] deps = prerequisites.HasAny ? prerequisites.questDependencies : prerequisiteQuests; -#pragma warning restore CS0618 - + QuestSO[] deps = prerequisites.questDependencies; if (deps == null) return; foreach (var dep in deps) @@ -196,7 +162,7 @@ namespace BaseGames.Quest Debug.LogError( $"[QuestSO] '{name}'(questId='{questId}')的前置任务链存在循环依赖!" + $"前置任务 '{dep.name}' 最终指回自身或已访问任务," + - "运行时将导致任务无法被接取。请检查 prerequisites/prerequisiteQuests 配置。", this); + "运行时将导致任务无法被接取。请检查 prerequisites 配置。", this); return; } } @@ -218,9 +184,7 @@ namespace BaseGames.Quest if (string.IsNullOrEmpty(quest.questId)) return false; if (!visited.Add(quest.questId)) return true; // 已在当前路径上 = 环路 -#pragma warning disable CS0618 - QuestSO[] deps = quest.prerequisites.HasAny ? quest.prerequisites.questDependencies : quest.prerequisiteQuests; -#pragma warning restore CS0618 + QuestSO[] deps = quest.prerequisites.questDependencies; if (deps != null) { @@ -288,23 +252,7 @@ namespace BaseGames.Quest private void ValidateBranchDialogueKeys() { - if (branches == null || branches.Length == 0) return; - - foreach (var branch in branches) - { - if (branch == null) continue; - - // npcDialogueSequence 是 SO 直接引用,无需字符串校验。 - // 旧字段 npcDialogueKey(Obsolete)有值时提示迁移。 -#pragma warning disable CS0618 - if (!string.IsNullOrEmpty(branch.npcDialogueKey) && branch.npcDialogueSequence == null) - { - Debug.LogWarning( - $"[QuestSO] '{name}' 分支仍使用旧字段 npcDialogueKey='{branch.npcDialogueKey}'," + - "请迁移至 npcDialogueSequence(直接拖入 DialogueSequenceSO)。", this); - } -#pragma warning restore CS0618 - } + // npcDialogueSequence 是 SO 直接引用;旧字符串字段已移除。 } #endif } @@ -318,20 +266,12 @@ namespace BaseGames.Quest " And(默认)= 全部 conditionFlagEntries 均满足才走本分支\n" + " Or = 任意一个 conditionFlagEntry 满足即可走本分支")] public BaseGames.Core.WorldStateFlagLogic conditionFlagsLogic = BaseGames.Core.WorldStateFlagLogic.And; - [Tooltip("世界状态标志条件(支持 invert 取反)。按 conditionFlagsLogic 逻辑与 conditionQuest 共同决定分支是否激活。\n" + - "优先使用此字段;若为空则自动回退到旧版 conditionFlags 以保证兼容性。")] + [Tooltip("世界状态标志条件(支持 invert 取反)。按 conditionFlagsLogic 逻辑与 conditionQuest 共同决定分支是否激活。")] public BranchFlagEntry[] conditionFlagEntries; - [Tooltip("(旧版兼容字段,已被 conditionFlagEntries 取代。如 conditionFlagEntries 不为空则本字段被忽略。)")] - [HideInInspector] - public string[] conditionFlags; [Tooltip("本分支解锁的后续任务。满足所有条件后,此任务将被设为 Available。")] public QuestSO nextQuest; [Tooltip("完成本任务后触发的 NPC 对话序列(直接引用 DialogueSequenceSO 资产,无需手写 ID)。")] public DialogueSequenceSO npcDialogueSequence; - - [System.Obsolete("已废弃,请改用 npcDialogueSequence(直接 SO 引用)。保留字段以兼容现有资产序列化。")] - [HideInInspector] - public string npcDialogueKey; } /// @@ -365,10 +305,7 @@ namespace BaseGames.Quest // ========================================================================= /// - /// 任务前置条件统一配置结构。 - /// 将旧版三个独立字段(prerequisiteQuests / prerequisiteFlags / prerequisiteFlagsLogic) - /// 合并为单一可序列化类,便于 Inspector 统一管理与代码维护。 - /// 运行时通过 判断是否启用新格式;若未配置则自动回退到旧版字段。 + /// 任务前置条件统一配置结构,将前置任务依赖和世界状态标志依赖合并为单一可序列化类。 /// [Serializable] public class QuestPrerequisite @@ -379,7 +316,7 @@ namespace BaseGames.Quest [Tooltip("世界状态标志前置条件(支持 And / Or 逻辑)。")] public FlagCondition flagCondition; - /// 此前置结构是否配置了任何条件(用于判断是否启用新格式,回退到旧字段)。 + /// 此前置结构是否配置了任何条件。 public bool HasAny => (questDependencies != null && questDependencies.Length > 0) || (flagCondition.flags != null && flagCondition.flags.Length > 0); diff --git a/Assets/_Game/Scripts/Quest/RewardSO.cs b/Assets/_Game/Scripts/Quest/RewardSO.cs index f4e6846..078e79c 100644 --- a/Assets/_Game/Scripts/Quest/RewardSO.cs +++ b/Assets/_Game/Scripts/Quest/RewardSO.cs @@ -25,7 +25,7 @@ namespace BaseGames.Quest public string[] itemIds; [Header("NPC 关系")] - [Tooltip("完成任务后对 giverNpcId(QuestSO 中配置)的好感度增量。\n" + + [Tooltip("完成任务后对发布 NPC(QuestSO.giverNpc)的好感度增量。\n" + "正值=好感增加,负值=好感降低。0 = 不影响好感度。\n" + "增量以强类型 NpcAffinityEvent(npcId + delta + newTotal)广播至 EVT_NpcAffinityChanged,\n" + "并持久化到 SaveData.World.NpcRelations。接收方无需字符串解析。")]