diff --git a/Assets/_Game/Scripts/Dialogue/DialogueManager.cs b/Assets/_Game/Scripts/Dialogue/DialogueManager.cs index 6f465c1..64daae0 100644 --- a/Assets/_Game/Scripts/Dialogue/DialogueManager.cs +++ b/Assets/_Game/Scripts/Dialogue/DialogueManager.cs @@ -99,6 +99,8 @@ namespace BaseGames.Dialogue private WaitTypingOrSkip _waitTypingOrSkip; private WaitSkip _waitSkip; private WaitForChoice _waitForChoice; + // 延迟 0.15s 防止玩家快速连击穿透:跳过打字机后立即触发选项0 + private WaitForSeconds _waitChoiceInputGuard; /// /// 当 IsDialogueActive 时排队等待的对话请求。 @@ -123,9 +125,10 @@ namespace BaseGames.Dialogue { if (ServiceLocator.GetOrDefault() != null) { Destroy(gameObject); return; } ServiceLocator.Register(this); - _waitTypingOrSkip = new WaitTypingOrSkip(this); - _waitSkip = new WaitSkip(this); - _waitForChoice = new WaitForChoice(this); + _waitTypingOrSkip = new WaitTypingOrSkip(this); + _waitSkip = new WaitSkip(this); + _waitForChoice = new WaitForChoice(this); + _waitChoiceInputGuard = new WaitForSeconds(0.15f); } private void OnDestroy() @@ -390,9 +393,10 @@ namespace BaseGames.Dialogue // 清除打字机阶段积压的输入,防止选项显示后被立即误触发 _skipRequested = false; _selectedChoiceIndex = -1; - // 延迟一帧:确保此前积压的"确认键"输入在下一帧开始前已被消耗, - // 防止快速连击(先跳过打字机→再误触发选项0)的穿透问题。 - yield return null; + // 延迟 0.15s:确保此前积压的"确认键"输入已被彻底消耗, + // 防止快速连击(跳过打字机→立即误选选项0)的穿透问题。 + // 使用预创建的缓存实例,避免每次分配 WaitForSeconds 对象。 + yield return _waitChoiceInputGuard; // 捕获当前播放标记,防止被打断后遗留回调写入新序列的选择索引 int capturedId = _playbackId; _dialogueBox.ShowChoices(line.choices, idx => diff --git a/Assets/_Game/Scripts/Dialogue/DialogueSequenceSO.cs b/Assets/_Game/Scripts/Dialogue/DialogueSequenceSO.cs index 9fe19c7..b5fa8f8 100644 --- a/Assets/_Game/Scripts/Dialogue/DialogueSequenceSO.cs +++ b/Assets/_Game/Scripts/Dialogue/DialogueSequenceSO.cs @@ -286,8 +286,13 @@ namespace BaseGames.Dialogue } private static bool HasChoiceCycle(DialogueSequenceSO seq, - System.Collections.Generic.HashSet visited) + System.Collections.Generic.HashSet visited, int depth = 0) { + if (depth > 32) + { + Debug.LogError($"[DialogueSequenceSO] 选项链深度超过 32 层(路径末端:'{seq.name}'),已视为存在循环引用并中止检测。请减少 nextSequence 嵌套层数。"); + return true; + } if (string.IsNullOrEmpty(seq.sequenceId)) return false; if (!visited.Add(seq.sequenceId)) return true; if (seq.lines != null) @@ -297,7 +302,7 @@ namespace BaseGames.Dialogue if (line.choices == null) continue; foreach (var choice in line.choices) { - if (choice.nextSequence != null && HasChoiceCycle(choice.nextSequence, visited)) + if (choice.nextSequence != null && HasChoiceCycle(choice.nextSequence, visited, depth + 1)) return true; } } @@ -307,7 +312,7 @@ namespace BaseGames.Dialogue { foreach (var variant in seq.variants) { - if (variant.sequence != null && HasChoiceCycle(variant.sequence, visited)) + if (variant.sequence != null && HasChoiceCycle(variant.sequence, visited, depth + 1)) return true; } } diff --git a/Assets/_Game/Scripts/Quest/IQuestManager.cs b/Assets/_Game/Scripts/Quest/IQuestManager.cs index 58e7b75..a628747 100644 --- a/Assets/_Game/Scripts/Quest/IQuestManager.cs +++ b/Assets/_Game/Scripts/Quest/IQuestManager.cs @@ -173,6 +173,12 @@ namespace BaseGames.Quest /// 参数:(questId, oldState, newState)。 /// event Action OnQuestStateChanged; + /// + /// 存档加载完成后触发(OnLoad 结束时)。 + /// 供 QuestGiver、HUD 等缓存组件订阅以刷新本地缓存, + /// 避免因存档加载跳过 OnQuestStateChanged 事件导致显示陈旧数据。 + /// + event System.Action OnAfterSaveLoaded; } // ========================================================================= diff --git a/Assets/_Game/Scripts/Quest/QuestGiver.cs b/Assets/_Game/Scripts/Quest/QuestGiver.cs index 904bd15..e961be9 100644 --- a/Assets/_Game/Scripts/Quest/QuestGiver.cs +++ b/Assets/_Game/Scripts/Quest/QuestGiver.cs @@ -58,7 +58,10 @@ namespace BaseGames.Quest _questManager = SL.GetOrDefault(); _questEvents = _questManager as IQuestEventSource; if (_questEvents != null) + { _questEvents.OnQuestStateChanged += HandleQuestStateChanged; + _questEvents.OnAfterSaveLoaded += HandleAfterSaveLoaded; + } } protected override void OnDisable() @@ -67,6 +70,7 @@ namespace BaseGames.Quest if (_questEvents != null) { _questEvents.OnQuestStateChanged -= HandleQuestStateChanged; + _questEvents.OnAfterSaveLoaded -= HandleAfterSaveLoaded; _questEvents = null; } _questManager = null; @@ -80,6 +84,9 @@ namespace BaseGames.Quest if (q != null && q.questId == questId) { _cacheDirty = true; return; } } + // 存档加载完成后统一刷新缓存,确保 NPC 交互提示反映最新任务状态 + private void HandleAfterSaveLoaded() => _cacheDirty = true; + // 本地化提示词辅助:如 Key 在表中找不到(返回值等于 Key 自身),回退到内联默认文本 private static string GetPrompt(string key, string fallback) { diff --git a/Assets/_Game/Scripts/Quest/QuestManager.cs b/Assets/_Game/Scripts/Quest/QuestManager.cs index 3d09ad8..660d792 100644 --- a/Assets/_Game/Scripts/Quest/QuestManager.cs +++ b/Assets/_Game/Scripts/Quest/QuestManager.cs @@ -241,6 +241,8 @@ namespace BaseGames.Quest public event System.Action OnQuestReadyToComplete; /// public event System.Action OnQuestStateChanged; + /// + public event System.Action OnAfterSaveLoaded; // ── 公共 API ────────────────────────────────────────────────────────── @@ -384,14 +386,29 @@ namespace BaseGames.Quest private void ApplyAffinity(QuestSO quest) { if (quest.reward == null || quest.reward.affinityBonus == 0) return; + + // 明确检查 giverNpc 引用及 npcId 有效性,提供可操作的错误信息 + if (quest.giverNpc == null) + { + Debug.LogWarning( + $"[QuestManager] 任务 '{quest.questId}' 有好感度奖励 +{quest.reward.affinityBonus}," + + "但 giverNpc 未配置,好感度无法发放。请在 QuestSO 中指定 giverNpc。", quest); + 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) + int maxAffinity = quest.giverNpc.maxAffinity; + if (maxAffinity < 0) + { + Debug.LogError( + $"[QuestManager] NPC '{quest.GiverNpcId}' 的 maxAffinity 配置为负数({maxAffinity})," + + "应为 0(无上限)或正整数。已跳过上限截断,请修正 NpcSO 配置。", quest.giverNpc); + } + else if (maxAffinity > 0 && newTotal > maxAffinity) { #if UNITY_EDITOR || DEVELOPMENT_BUILD Debug.LogWarning( @@ -421,7 +438,8 @@ namespace BaseGames.Quest /// /// 解锁满足条件的后续任务分支,并触发相应 NPC 完成反应对话。 - /// 允许同时满足多个分支(并行支线解锁)。 + /// 允许同时满足多个分支(并行支线解锁),但 NPC 完成对话只播放首个有对话的分支, + /// 避免多分支同时满足时连续打断播放导致对话混乱。 /// private void UnlockBranches(string questId, QuestSO quest) { @@ -430,6 +448,9 @@ namespace BaseGames.Quest var dialogueService = BaseGames.Core.ServiceLocator.GetOrDefault(); var saveService = BaseGames.Core.ServiceLocator.GetOrDefault(); + // 同一次完成事件只播放首个有对话配置的满足分支,避免多分支同时满足时重复播放对话 + bool dialoguePlayed = false; + foreach (var branch in quest.branches) { // 任务条件 @@ -478,10 +499,14 @@ namespace BaseGames.Quest _questStates[branch.nextQuest.questId] = QuestStateEnum.Available; // 触发 NPC 完成反应对话(如 NPC 说"太好了,谢谢你!") - if (branch.npcDialogueSequence != null) + // 仅对首个有对话的满足分支播放,防止多分支同时满足时连续播放导致混乱 + if (!dialoguePlayed && branch.npcDialogueSequence != null) { if (dialogueService != null) + { dialogueService.StartDialogue(branch.npcDialogueSequence, quest.GiverNpcId ?? ""); + dialoguePlayed = true; + } else Debug.LogWarning( $"[QuestManager] 任务 '{questId}' 完成后需播放 NPC 对话 " + @@ -713,6 +738,9 @@ namespace BaseGames.Quest #endif // 存档中未记录的无前置任务,在新周目/首次加载后也保证可接取 InitializeAvailableQuests(); + // 通知所有缓存了任务状态的组件(如 QuestGiver)刷新显示, + // 因存档加载不逐条触发 OnQuestStateChanged,需要统一广播一次刷新信号。 + OnAfterSaveLoaded?.Invoke(); } // ── 私有辅助 ───────────────────────────────────────────────────────── diff --git a/Assets/_Game/Scripts/Quest/QuestSO.cs b/Assets/_Game/Scripts/Quest/QuestSO.cs index 0fc0c8d..691c67e 100644 --- a/Assets/_Game/Scripts/Quest/QuestSO.cs +++ b/Assets/_Game/Scripts/Quest/QuestSO.cs @@ -188,8 +188,8 @@ namespace BaseGames.Quest { if (depth > 32) { - Debug.LogWarning($"[QuestSO] 前置链深度超过 32 层(路径末端:'{quest.name}'),已停止检测。请减少任务链深度。"); - return false; + Debug.LogError($"[QuestSO] 前置链深度超过 32 层(路径末端:'{quest.name}'),已视为存在循环依赖并中止检测。请减少任务链深度。"); + return true; } if (string.IsNullOrEmpty(quest.questId)) return false; if (!visited.Add(quest.questId)) return true; // 已在当前路径上 = 环路 @@ -242,8 +242,8 @@ namespace BaseGames.Quest { if (depth > 32) { - Debug.LogWarning($"[QuestSO] 分支链深度超过 32 层(路径末端:'{quest.name}'),已停止检测。请减少分支链深度。"); - return false; + Debug.LogError($"[QuestSO] 分支链深度超过 32 层(路径末端:'{quest.name}'),已视为存在循环依赖并中止检测。请减少分支链深度。"); + return true; } if (string.IsNullOrEmpty(quest.questId)) return false; if (!visited.Add(quest.questId)) return true; // 已在路径上 = 环路