fix: Round 56 亲密度门槛UI、空npcId警告、任务总览窗口、超时缓存、本地化Key检查、放弃任务交互、CurrentNpcId属性

- QuestManager.ApplyAffinity: giverNpc.npcId 为空时改为 LogWarning+return,不再静默丢弃好感度奖励
- QuestManager.UnlockBranches: 分支对话 npcId 为空时输出 LogWarning,提示开发者可能误推进对话类目标
- QuestGiver.InteractPrompt: Available 状态调用 GetQuestLockInfo,亲密度/前置未满足时显示锁定原因而非'接受任务'
- QuestGiver.Interact_Internal: Available 状态加锁定检查防卫,锁定时提前返回;新增 _allowAbandon 字段(默认 false)
- QuestGiver: Active+未完成+_allowAbandon=true 时显示'放弃任务'并触发 AbandonQuest,接入已有 AbandonQuest 接口
- DialogueManager: 新增 _waitSequenceTimeout 缓存字段,Awake 预创建避免每次 PlayImmediate 分配 WaitForSeconds
- DialogueManager: 新增 _currentNpcId 字段,PlayImmediate 写入、EndDialogue/ForceEnd 清空
- IDialogueService + DialogueManager: 暴露 CurrentNpcId 只读属性,供外部系统主动查询当前对话 NPC
- QuestSO.OnValidate: 对空 displayNameKey/descriptionKey 输出 LogWarning,防止 UI 显示空文本
- 新增 QuestOverviewEditorWindow: BaseGames/Quest/Quest Overview,列出全部 QuestSO,支持搜索/分类过滤;
  Play Mode 下读取 IQuestManager 运行时状态并着色显示;Edit Mode 高亮配置错误行;单击 Ping、双击 Select

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-25 08:06:54 +08:00
parent 8e88fc42e9
commit c7057db27d
6 changed files with 407 additions and 12 deletions

View File

@@ -31,6 +31,11 @@ namespace BaseGames.Quest
[Tooltip("任务已完成QuestState.Completed后再次交互时播放。通常是 NPC 闲聊或后续剧情的对话。")]
[SerializeField] private DialogueSequenceSO _completedDialogue;
[Header("交互选项")]
[Tooltip("勾选后任务进行中Active 且未完成)时交互提示变为"放弃任务",交互即触发 AbandonQuest。\n" +
"适合允许玩家主动放弃的支线任务;主线任务建议保持取消勾选。")]
[SerializeField] private bool _allowAbandon;
// ── InteractableNPC 覆盖 ──────────────────────────────────────────────
// 缓存上次查找结果,避免 InteractPrompt get每帧调用重复遍历 _offeredQuests。
@@ -46,6 +51,8 @@ namespace BaseGames.Quest
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";
private const string K_Locked = "QUEST_PROMPT_LOCKED";
private const string K_Abandon = "QUEST_PROMPT_ABANDON";
// 缓存 IQuestManager + IQuestEventSource 引用,避免每次访问 InteractPrompt 调用 SL.GetOrDefault
private IQuestManager _questManager;
@@ -100,12 +107,32 @@ namespace BaseGames.Quest
{
var quest = GetCachedQuest();
if (quest == null || _questManager == null) return base.InteractPrompt;
if (_cachedState == QuestStateEnum.Available)
{
// 检查亲密度门槛等锁定条件,锁定时显示具体原因而非直接"接受任务"
var lockInfo = _questManager.GetQuestLockInfo(quest.questId);
if (lockInfo.IsLocked)
{
string fallback = lockInfo.Reason == QuestLockReason.InsufficientAffinity
? $"好感度不足({lockInfo.Param}"
: "条件未满足";
return GetPrompt(K_Locked, fallback);
}
return GetPrompt(K_Accept, "接受任务");
}
if (_cachedState == QuestStateEnum.Active)
{
if (_questManager.IsReadyToComplete(quest.questId))
return GetPrompt(K_Submit, "提交任务");
return _allowAbandon
? GetPrompt(K_Abandon, "放弃任务")
: GetPrompt(K_InProgress, "进行中…");
}
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,
@@ -120,15 +147,26 @@ namespace BaseGames.Quest
if (_cachedState == QuestStateEnum.Available)
{
// 亲密度门槛等锁定条件未满足时静默返回InteractPrompt 已显示原因,玩家可见)
var lockInfo = _questManager.GetQuestLockInfo(quest.questId);
if (lockInfo.IsLocked) return;
_questManager.AcceptQuest(quest.questId);
// OnQuestStateChanged 事件会自动触发 HandleQuestStateChanged → _cacheDirty = true
}
else if (_cachedState == QuestStateEnum.Active && _questManager.IsReadyToComplete(quest.questId))
else if (_cachedState == QuestStateEnum.Active)
{
// 直接从 player 获取 PlayerStats避免对 PlayerController 的程序集依赖
var stats = player.GetComponentInParent<PlayerStats>();
_questManager.CompleteQuest(quest.questId, stats);
// OnQuestStateChanged 事件会自动触发 HandleQuestStateChanged → _cacheDirty = true
if (_questManager.IsReadyToComplete(quest.questId))
{
// 直接从 player 获取 PlayerStats避免对 PlayerController 的程序集依赖
var stats = player.GetComponentInParent<PlayerStats>();
_questManager.CompleteQuest(quest.questId, stats);
// OnQuestStateChanged 事件会自动触发 HandleQuestStateChanged → _cacheDirty = true
}
else if (_allowAbandon)
{
_questManager.AbandonQuest(quest.questId);
// OnQuestStateChanged 事件会自动触发 HandleQuestStateChanged → _cacheDirty = true
}
}
}

View File

@@ -395,7 +395,13 @@ namespace BaseGames.Quest
"但 giverNpc 未配置,好感度无法发放。请在 QuestSO 中指定 giverNpc。", quest);
return;
}
if (string.IsNullOrEmpty(quest.GiverNpcId)) return;
if (string.IsNullOrEmpty(quest.GiverNpcId))
{
Debug.LogWarning(
$"[QuestManager] 任务 '{quest.questId}' 的 giverNpc.npcId 为空字符串,好感度无法写入。" +
"请在对应 NpcSO 中填写有效的 npcId 后重新保存 QuestSO。", quest.giverNpc);
return;
}
_npcRelations.TryGetValue(quest.GiverNpcId, out int current);
int newTotal = current + quest.reward.affinityBonus;
@@ -504,7 +510,13 @@ namespace BaseGames.Quest
{
if (dialogueService != null)
{
dialogueService.StartDialogue(branch.npcDialogueSequence, quest.GiverNpcId ?? "");
string npcId = quest.GiverNpcId;
if (string.IsNullOrEmpty(npcId))
Debug.LogWarning(
$"[QuestManager] 任务 '{questId}' 完成分支对话的 giverNpc.npcId 为空," +
"EVT_NpcDialogueCompleted 将广播空 npcId可能错误推进对话类目标进度。" +
"请在 NpcSO 中填写有效的 npcId。");
dialogueService.StartDialogue(branch.npcDialogueSequence, npcId ?? "");
dialoguePlayed = true;
}
else

View File

@@ -118,6 +118,16 @@ namespace BaseGames.Quest
s_questIdsCacheTime = -10.0;
}
// 本地化 Key 完整性检查:空 Key 会导致 UI 显示空文本(未本地化内容)
if (string.IsNullOrWhiteSpace(displayNameKey))
Debug.LogWarning(
$"[QuestSO] '{name}'questId='{questId}')的 displayNameKey 为空," +
"任务日志 UI 将显示空白名称。请填写本地化 Key如 \"Quest_{questId}_Name\"。", this);
if (string.IsNullOrWhiteSpace(descriptionKey))
Debug.LogWarning(
$"[QuestSO] '{name}'questId='{questId}')的 descriptionKey 为空," +
"任务详情 UI 将显示空白描述。请填写本地化 Key如 \"Quest_{questId}_Desc\"。", this);
ValidateObjectiveIds();
ValidatePrerequisiteCycles();
ValidateBranchCycles();