Files
zeling_v2/Assets/_Game/Scripts/Quest/QuestGiver.cs
Joywayer 8e88fc42e9 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>
2026-05-25 07:37:03 +08:00

188 lines
9.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
using BaseGames.Core;
using BaseGames.Dialogue;
using BaseGames.Player;
using QuestStateEnum = BaseGames.Core.Events.QuestState;
using SL = BaseGames.Core.ServiceLocator;
using L10n = BaseGames.Localization.LocalizationManager;
namespace BaseGames.Quest
{
/// <summary>
/// 可发布/完成任务的 NPC架构 22_QuestChallengeModule §6
/// 继承 InteractableNPC根据任务状态切换对话版本在交互时处理任务接收/完成。
/// </summary>
public class QuestGiver : InteractableNPC
{
[Header("任务")]
[Tooltip("该 NPC 可提供的所有任务,按优先级从高到低排列。\n" +
"交互时从列表头部找到第一个 Available 或 Active 状态的任务作为当前任务;\n" +
"若全部已完成,显示最后一个已完成任务的 completedDialogue。")]
[SerializeField] private QuestSO[] _offeredQuests;
[Header("对话版本(根据任务状态切换)")]
[Tooltip("任务尚未接取QuestState.Available时播放。通常是 NPC 发布任务、介绍背景的对话。")]
[SerializeField] private DialogueSequenceSO _availableDialogue;
[Tooltip("任务已接取、目标尚未全部完成QuestState.Active时播放。通常是 NPC 催促或加油打气的对话。")]
[SerializeField] private DialogueSequenceSO _activeDialogue;
[Tooltip("全部非可选目标已达成、任务可以交付时播放IsReadyToComplete = true。\n" +
"通常是 NPC 感谢、确认收取物品的对话,播放后执行 CompleteQuest 逻辑。")]
[SerializeField] private DialogueSequenceSO _readyDialogue;
[Tooltip("任务已完成QuestState.Completed后再次交互时播放。通常是 NPC 闲聊或后续剧情的对话。")]
[SerializeField] private DialogueSequenceSO _completedDialogue;
// ── InteractableNPC 覆盖 ──────────────────────────────────────────────
// 缓存上次查找结果,避免 InteractPrompt get每帧调用重复遍历 _offeredQuests。
// 当状态可能变更时OnEnable、Interact_Internal 后)标记为脏。
private QuestSO _cachedQuest;
private QuestStateEnum _cachedState;
private bool _cacheDirty = true;
// 本地化 Key 常量 — 对应 UI 本地化表中的条目;
// 如本地化表未配置该 KeyGetPrompt 会降级为内联的中文默认文本。
private const string K_Accept = "QUEST_PROMPT_ACCEPT";
private const string K_Submit = "QUEST_PROMPT_SUBMIT";
private const string K_InProgress = "QUEST_PROMPT_IN_PROGRESS";
private const string K_Paused = "QUEST_PROMPT_PAUSED";
private const string K_Talk = "QUEST_PROMPT_TALK";
// 缓存 IQuestManager + IQuestEventSource 引用,避免每次访问 InteractPrompt 调用 SL.GetOrDefault
private IQuestManager _questManager;
private IQuestEventSource _questEvents;
protected override void OnEnable()
{
base.OnEnable();
_cacheDirty = true;
_questManager = SL.GetOrDefault<IQuestManager>();
_questEvents = _questManager as IQuestEventSource;
if (_questEvents != null)
{
_questEvents.OnQuestStateChanged += HandleQuestStateChanged;
_questEvents.OnAfterSaveLoaded += HandleAfterSaveLoaded;
}
}
protected override void OnDisable()
{
base.OnDisable();
if (_questEvents != null)
{
_questEvents.OnQuestStateChanged -= HandleQuestStateChanged;
_questEvents.OnAfterSaveLoaded -= HandleAfterSaveLoaded;
_questEvents = null;
}
_questManager = null;
}
// 任务状态变化时自动标记缓存失效(无需再手动设 _cacheDirty
private void HandleQuestStateChanged(string questId, QuestStateEnum from, QuestStateEnum to)
{
if (_offeredQuests == null) return;
foreach (var q in _offeredQuests)
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)
{
var v = L10n.Get(key, "UI");
return v != key ? v : fallback;
}
public override string InteractPrompt
{
get
{
var quest = GetCachedQuest();
if (quest == null || _questManager == null) return base.InteractPrompt;
return _cachedState switch
{
QuestStateEnum.Available => GetPrompt(K_Accept, "接受任务"),
QuestStateEnum.Active => _questManager.IsReadyToComplete(quest.questId)
? GetPrompt(K_Submit, "提交任务")
: GetPrompt(K_InProgress, "进行中…"),
QuestStateEnum.Paused => GetPrompt(K_Paused, "暂停中…"),
QuestStateEnum.Completed => GetPrompt(K_Talk, "对话"),
_ => base.InteractPrompt,
};
}
}
protected override void Interact_Internal(Transform player)
{
var quest = GetCachedQuest();
if (quest == null || _questManager == null) return;
if (_cachedState == QuestStateEnum.Available)
{
_questManager.AcceptQuest(quest.questId);
// OnQuestStateChanged 事件会自动触发 HandleQuestStateChanged → _cacheDirty = true
}
else if (_cachedState == QuestStateEnum.Active && _questManager.IsReadyToComplete(quest.questId))
{
// 直接从 player 获取 PlayerStats避免对 PlayerController 的程序集依赖
var stats = player.GetComponentInParent<PlayerStats>();
_questManager.CompleteQuest(quest.questId, stats);
// OnQuestStateChanged 事件会自动触发 HandleQuestStateChanged → _cacheDirty = true
}
}
protected override DialogueSequenceSO GetCurrentDialogue()
{
var quest = GetCachedQuest();
if (quest == null || _questManager == null) return base.GetCurrentDialogue();
return _cachedState switch
{
QuestStateEnum.Available => _availableDialogue,
QuestStateEnum.Active => _questManager.IsReadyToComplete(quest.questId)
? _readyDialogue : _activeDialogue,
QuestStateEnum.Paused => _activeDialogue, // 暂停中显示"催促"对话,不触发任何状态推进
QuestStateEnum.Completed => _completedDialogue,
_ => base.GetCurrentDialogue(),
};
}
// ── 私有辅助 ─────────────────────────────────────────────────────────
/// <summary>
/// 返回缓存的当前任务(处于 Available/Active/Paused 的第一个,或最后一个已完成任务)。
/// 若缓存不脏,直接返回上次结果,避免每帧遍历 _offeredQuests。
/// 任务状态事件HandleQuestStateChanged或 OnEnable 会自动将 _cacheDirty 置 true
/// 确保下次访问状态是最新的。
/// </summary>
private QuestSO GetCachedQuest()
{
if (!_cacheDirty && _cachedQuest != null) return _cachedQuest;
var qm = _questManager;
if (_offeredQuests == null || qm == null) { _cacheDirty = false; return null; }
QuestSO lastCompleted = null;
foreach (var q in _offeredQuests)
{
if (q == null) continue;
var s = qm.GetState(q.questId);
if (s == QuestStateEnum.Available || s == QuestStateEnum.Active || s == QuestStateEnum.Paused)
{
_cachedQuest = q;
_cachedState = s;
_cacheDirty = false;
return _cachedQuest;
}
if (s == QuestStateEnum.Completed) lastCompleted = q;
}
_cachedQuest = lastCompleted;
_cachedState = lastCompleted != null ? QuestStateEnum.Completed : QuestStateEnum.Unavailable;
_cacheDirty = false;
return _cachedQuest;
}
}
}