Files
zeling_v2/Assets/_Game/Scripts/Quest/QuestGiver.cs
Joywayer c7057db27d 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>
2026-05-25 08:06:54 +08:00

226 lines
11 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;
[Header("交互选项")]
[Tooltip("勾选后任务进行中Active 且未完成)时交互提示变为"放弃任务",交互即触发 AbandonQuest。\n" +
"适合允许玩家主动放弃的支线任务;主线任务建议保持取消勾选。")]
[SerializeField] private bool _allowAbandon;
// ── 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";
private const string K_Locked = "QUEST_PROMPT_LOCKED";
private const string K_Abandon = "QUEST_PROMPT_ABANDON";
// 缓存 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;
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.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)
{
// 亲密度门槛等锁定条件未满足时静默返回InteractPrompt 已显示原因,玩家可见)
var lockInfo = _questManager.GetQuestLockInfo(quest.questId);
if (lockInfo.IsLocked) return;
_questManager.AcceptQuest(quest.questId);
// OnQuestStateChanged 事件会自动触发 HandleQuestStateChanged → _cacheDirty = true
}
else if (_cachedState == QuestStateEnum.Active)
{
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
}
}
}
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;
}
}
}