using System; using UnityEngine; using BaseGames.Dialogue; namespace BaseGames.Quest { /// /// 任务定义 SO(架构 22_QuestChallengeModule §2)。 /// 资产路径: Assets/ScriptableObjects/Quest/Quest_{questId}.asset /// [CreateAssetMenu(menuName = "BaseGames/Quest/Quest")] public class QuestSO : ScriptableObject { [Header("标识")] public string questId; // 唯一 ID,如 "Quest_FindMushroom" [Tooltip("本地化 Key,格式如 \"Quest_FindMushroom_Name\"。通过 LocalizationManager.Get(displayNameKey, \"Quest\") 显示。")] public string displayNameKey; [Tooltip("本地化 Key,格式如 \"Quest_FindMushroom_Desc\"。通过 LocalizationManager.Get(descriptionKey, \"Quest\") 显示。")] public string descriptionKey; public Sprite icon; [Header("目标链")] public QuestObjectiveSO[] objectives; // 按顺序完成,全部完成 = 可交完 [Header("前置条件")] public QuestSO[] prerequisiteQuests; // 所有前置任务 Completed 后才可接 public int minAffinityToAccept; // NPC 好感度门槛(0 = 无限制) [Header("奖励")] public RewardSO reward; [Header("失败条件(可选)")] public bool canFail; public QuestObjectiveSO failCondition; [Header("完成后续任务(分支)")] public QuestBranch[] branches; // ── 编辑器校验 ──────────────────────────────────────────────────────── #if UNITY_EDITOR // questId → 资产路径,5 秒 TTL,跨所有 QuestSO.OnValidate 共用。 // 重复检测时只需将缓存路径与自身路径比对(O(1)),无需全量扫描。 private static System.Collections.Generic.Dictionary s_questIdToPath; private static double s_questIdsCacheTime = -10.0; private static System.Collections.Generic.Dictionary GetQuestIdCache() { double now = UnityEditor.EditorApplication.timeSinceStartup; if (s_questIdToPath != null && now - s_questIdsCacheTime < 5.0) return s_questIdToPath; s_questIdToPath = new System.Collections.Generic.Dictionary(System.StringComparer.Ordinal); string[] guids = UnityEditor.AssetDatabase.FindAssets("t:QuestSO"); foreach (var guid in guids) { var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid); var q = UnityEditor.AssetDatabase.LoadAssetAtPath(path); if (q != null && !string.IsNullOrEmpty(q.questId) && !s_questIdToPath.ContainsKey(q.questId)) s_questIdToPath[q.questId] = path; } s_questIdsCacheTime = now; return s_questIdToPath; } private void OnValidate() { if (string.IsNullOrWhiteSpace(questId)) { Debug.LogWarning($"[QuestSO] '{name}' 缺少 questId,保存前请填写。", this); return; } // 检测重复 questId:缓存路径 vs 自身路径比对(O(1)),5 秒内无需重扫。 var cache = GetQuestIdCache(); string myPath = UnityEditor.AssetDatabase.GetAssetPath(this); if (!string.IsNullOrEmpty(myPath) && cache.TryGetValue(questId, out var existingPath) && existingPath != myPath) { Debug.LogError( $"[QuestSO] questId '{questId}' 与 " + $"'{System.IO.Path.GetFileNameWithoutExtension(existingPath)}' 重复!请修改其中一个。", this); s_questIdsCacheTime = -10.0; } ValidateBranchDialogueKeys(); } 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 } } #endif } [Serializable] public class QuestBranch { /// 若此前置任务已完成 → 走本分支(null = 默认分支)。 public QuestSO conditionQuest; public QuestSO nextQuest; /// 完成后触发的 NPC 对话序列(直接引用,避免手写 sequenceId 字符串出错)。 public DialogueSequenceSO npcDialogueSequence; [System.Obsolete("已废弃,请改用 npcDialogueSequence(直接 SO 引用)。保留字段以兼容现有资产序列化。")] [HideInInspector] public string npcDialogueKey; } }