fix: Round 55 递归硬中止、存档加载缓存刷新、好感度空值防护、选项穿透延迟、分支对话去重

- QuestSO.HasPrerequisiteCycle/HasBranchCycle: depth>32 改为 LogError+return true 硬中止,防止栈溢出
- DialogueSequenceSO.HasChoiceCycle: 新增 depth 参数及 >32 硬中止,同时更新递归调用传 depth+1
- IQuestEventSource: 新增 OnAfterSaveLoaded 事件接口,供存档加载后统一刷新缓存
- QuestManager.OnLoad: 末尾触发 OnAfterSaveLoaded,确保所有缓存组件收到通知
- QuestGiver: 订阅 OnAfterSaveLoaded 设 _cacheDirty,存档恢复后 NPC 交互提示始终最新
- QuestManager.ApplyAffinity: 新增 giverNpc null 显式 LogWarning、maxAffinity<0 LogError 防护
- DialogueManager: 选项穿透防护改为预创建 WaitForSeconds(0.15f),替代 yield return null
- QuestManager.UnlockBranches: 多分支同时满足时只播首个有对话的分支,防止重复播放

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-25 07:37:03 +08:00
parent 943178cbc1
commit 8e88fc42e9
6 changed files with 67 additions and 17 deletions

View File

@@ -241,6 +241,8 @@ namespace BaseGames.Quest
public event System.Action<string> OnQuestReadyToComplete;
/// <inheritdoc/>
public event System.Action<string, QuestStateEnum, QuestStateEnum> OnQuestStateChanged;
/// <inheritdoc/>
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
/// <summary>
/// 解锁满足条件的后续任务分支,并触发相应 NPC 完成反应对话。
/// 允许同时满足多个分支(并行支线解锁)
/// 允许同时满足多个分支(并行支线解锁),但 NPC 完成对话只播放首个有对话的分支,
/// 避免多分支同时满足时连续打断播放导致对话混乱。
/// </summary>
private void UnlockBranches(string questId, QuestSO quest)
{
@@ -430,6 +448,9 @@ namespace BaseGames.Quest
var dialogueService = BaseGames.Core.ServiceLocator.GetOrDefault<BaseGames.Dialogue.IDialogueService>();
var saveService = BaseGames.Core.ServiceLocator.GetOrDefault<BaseGames.Core.ISaveService>();
// 同一次完成事件只播放首个有对话配置的满足分支,避免多分支同时满足时重复播放对话
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();
}
// ── 私有辅助 ─────────────────────────────────────────────────────────