feat: Round 51 narrative systems improvements

- SaveData: update QuestState.Status comment to include Paused state
- QuestManager: add inline comment on AcceptQuest duplicate-accept guard
- QuestManager: wrap reward.Apply() in try-catch so exceptions don't
  corrupt already-committed Completed state
- QuestManager.UnlockBranches: support new conditionFlagEntries (invert/
  NOT logic) with graceful fallback to legacy conditionFlags
- QuestGiver: cache IQuestManager field in OnEnable; subscribe to
  OnQuestStateChanged for automatic cache invalidation instead of manual
  _cacheDirty = true after each Interact; remove per-call SL.GetOrDefault
- QuestGiver: replace hardcoded Chinese prompt strings with
  LocalizationManager.Get(key, 'UI') + inline fallback via GetPrompt()
- QuestSO: add BranchFlagEntry struct (flagId + invert) for NOT-logic
  branch conditions; add conditionFlagEntries to QuestBranch with
  HideInInspector on legacy conditionFlags for backward compat
- QuestModule: add static TTL cache (5 s) for FindAll<QuestSO>() in
  PopulateDependencyGraph to avoid re-scanning disk on every foldout open
- NpcSOEditor: add 'jump to localization file' button that pings and
  selects the UI table JSON in the Project window

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-25 00:34:59 +08:00
parent 9c1e70fdeb
commit 48f018f4b8
6 changed files with 181 additions and 43 deletions

View File

@@ -4,6 +4,7 @@ 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
{
@@ -38,25 +39,68 @@ namespace BaseGames.Quest
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;
}
protected override void OnDisable()
{
base.OnDisable();
if (_questEvents != null)
{
_questEvents.OnQuestStateChanged -= HandleQuestStateChanged;
_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; }
}
// 本地化提示词辅助:如 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 qm = SL.GetOrDefault<IQuestManager>();
var quest = GetCachedQuest(qm);
if (quest == null || qm == null) return base.InteractPrompt;
var quest = GetCachedQuest();
if (quest == null || _questManager == null) return base.InteractPrompt;
return _cachedState switch
{
QuestStateEnum.Available => "接受任务",
QuestStateEnum.Active => qm.IsReadyToComplete(quest.questId) ? "提交任务" : "进行中…",
QuestStateEnum.Paused => "暂停中…",
QuestStateEnum.Completed => "对话",
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,
};
}
@@ -64,34 +108,32 @@ namespace BaseGames.Quest
protected override void Interact_Internal(Transform player)
{
var qm = SL.GetOrDefault<IQuestManager>();
var quest = GetCachedQuest(qm);
if (quest == null || qm == null) return;
var quest = GetCachedQuest();
if (quest == null || _questManager == null) return;
if (_cachedState == QuestStateEnum.Available)
{
qm.AcceptQuest(quest.questId);
_cacheDirty = true; // 状态已变更,下次访问重新查询
_questManager.AcceptQuest(quest.questId);
// OnQuestStateChanged 事件会自动触发 HandleQuestStateChanged → _cacheDirty = true
}
else if (_cachedState == QuestStateEnum.Active && qm.IsReadyToComplete(quest.questId))
else if (_cachedState == QuestStateEnum.Active && _questManager.IsReadyToComplete(quest.questId))
{
// 直接从 player 获取 PlayerStats避免对 PlayerController 的程序集依赖
var stats = player.GetComponentInParent<PlayerStats>();
qm.CompleteQuest(quest.questId, stats);
_cacheDirty = true; // 状态已变更,下次访问重新查询
_questManager.CompleteQuest(quest.questId, stats);
// OnQuestStateChanged 事件会自动触发 HandleQuestStateChanged → _cacheDirty = true
}
}
protected override DialogueSequenceSO GetCurrentDialogue()
{
var qm = SL.GetOrDefault<IQuestManager>();
var quest = GetCachedQuest(qm);
if (quest == null || qm == null) return base.GetCurrentDialogue();
var quest = GetCachedQuest();
if (quest == null || _questManager == null) return base.GetCurrentDialogue();
return _cachedState switch
{
QuestStateEnum.Available => _availableDialogue,
QuestStateEnum.Active => qm.IsReadyToComplete(quest.questId)
QuestStateEnum.Active => _questManager.IsReadyToComplete(quest.questId)
? _readyDialogue : _activeDialogue,
QuestStateEnum.Paused => _activeDialogue, // 暂停中显示"催促"对话,不触发任何状态推进
QuestStateEnum.Completed => _completedDialogue,
@@ -104,13 +146,14 @@ namespace BaseGames.Quest
/// <summary>
/// 返回缓存的当前任务(处于 Available/Active/Paused 的第一个,或最后一个已完成任务)。
/// 若缓存不脏,直接返回上次结果,避免每帧遍历 _offeredQuests。
/// 调用 Interact_Internal 后将 _cacheDirty 置 true确保下次交互状态是最新的。
/// 任务状态事件HandleQuestStateChanged或 OnEnable 会自动将 _cacheDirty 置 true
/// 确保下次访问状态是最新的。
/// </summary>
private QuestSO GetCachedQuest(IQuestManager qm = null)
private QuestSO GetCachedQuest()
{
if (!_cacheDirty && _cachedQuest != null) return _cachedQuest;
qm ??= SL.GetOrDefault<IQuestManager>();
var qm = _questManager;
if (_offeredQuests == null || qm == null) { _cacheDirty = false; return null; }
QuestSO lastCompleted = null;