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:
@@ -74,6 +74,8 @@ namespace BaseGames.Dialogue
|
|||||||
private DialogueSequenceSO _choiceBranchResult;
|
private DialogueSequenceSO _choiceBranchResult;
|
||||||
/// <summary>HandleChoices 子协程写入结果:true = 分支深度超限,优雅降级(继续播放后续行)。</summary>
|
/// <summary>HandleChoices 子协程写入结果:true = 分支深度超限,优雅降级(继续播放后续行)。</summary>
|
||||||
private bool _branchDepthExceeded;
|
private bool _branchDepthExceeded;
|
||||||
|
/// <summary>当前正在播放对话的 NPC ID(无对话时为 null)。供外部系统主动查询"谁在说话"。</summary>
|
||||||
|
private string _currentNpcId;
|
||||||
|
|
||||||
// ── 复用 Yield 指令,避免协程每行 new WaitUntil 闭包 ───────────────
|
// ── 复用 Yield 指令,避免协程每行 new WaitUntil 闭包 ───────────────
|
||||||
private sealed class WaitTypingOrSkip : CustomYieldInstruction
|
private sealed class WaitTypingOrSkip : CustomYieldInstruction
|
||||||
@@ -101,6 +103,8 @@ namespace BaseGames.Dialogue
|
|||||||
private WaitForChoice _waitForChoice;
|
private WaitForChoice _waitForChoice;
|
||||||
// 延迟 0.15s 防止玩家快速连击穿透:跳过打字机后立即触发选项0
|
// 延迟 0.15s 防止玩家快速连击穿透:跳过打字机后立即触发选项0
|
||||||
private WaitForSeconds _waitChoiceInputGuard;
|
private WaitForSeconds _waitChoiceInputGuard;
|
||||||
|
// 超时守卫等待指令(与 _sequenceTimeoutSeconds 同步,在 Awake 初始化,避免每次 PlayImmediate 分配)
|
||||||
|
private WaitForSeconds _waitSequenceTimeout;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 当 IsDialogueActive 时排队等待的对话请求。
|
/// 当 IsDialogueActive 时排队等待的对话请求。
|
||||||
@@ -113,6 +117,12 @@ namespace BaseGames.Dialogue
|
|||||||
/// <summary>当前是否有对话正在播放。</summary>
|
/// <summary>当前是否有对话正在播放。</summary>
|
||||||
public bool IsDialogueActive { get; private set; }
|
public bool IsDialogueActive { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前正在播放对话的 NPC ID。无对话活跃时为 <see langword="null"/>。
|
||||||
|
/// 供地图标记、HUD、分析埋点等外部系统主动查询"当前谁在说话",无需订阅事件。
|
||||||
|
/// </summary>
|
||||||
|
public string CurrentNpcId => _currentNpcId;
|
||||||
|
|
||||||
/// <summary>当前正在播放的对话优先级(0 = 默认)。高优先级请求可打断低优先级。</summary>
|
/// <summary>当前正在播放的对话优先级(0 = 默认)。高优先级请求可打断低优先级。</summary>
|
||||||
private int _currentPriority;
|
private int _currentPriority;
|
||||||
|
|
||||||
@@ -129,6 +139,8 @@ namespace BaseGames.Dialogue
|
|||||||
_waitSkip = new WaitSkip(this);
|
_waitSkip = new WaitSkip(this);
|
||||||
_waitForChoice = new WaitForChoice(this);
|
_waitForChoice = new WaitForChoice(this);
|
||||||
_waitChoiceInputGuard = new WaitForSeconds(0.15f);
|
_waitChoiceInputGuard = new WaitForSeconds(0.15f);
|
||||||
|
if (_sequenceTimeoutSeconds > 0f)
|
||||||
|
_waitSequenceTimeout = new WaitForSeconds(_sequenceTimeoutSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDestroy()
|
private void OnDestroy()
|
||||||
@@ -230,6 +242,7 @@ namespace BaseGames.Dialogue
|
|||||||
private void PlayImmediate(DialogueSequenceSO sequence, string npcId, int priority = 0)
|
private void PlayImmediate(DialogueSequenceSO sequence, string npcId, int priority = 0)
|
||||||
{
|
{
|
||||||
IsDialogueActive = true;
|
IsDialogueActive = true;
|
||||||
|
_currentNpcId = npcId;
|
||||||
_currentPriority = priority;
|
_currentPriority = priority;
|
||||||
_skipRequested = false;
|
_skipRequested = false;
|
||||||
_selectedChoiceIndex = -1;
|
_selectedChoiceIndex = -1;
|
||||||
@@ -249,7 +262,7 @@ namespace BaseGames.Dialogue
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private IEnumerator SequenceTimeoutGuard(string npcId)
|
private IEnumerator SequenceTimeoutGuard(string npcId)
|
||||||
{
|
{
|
||||||
yield return new WaitForSeconds(_sequenceTimeoutSeconds);
|
yield return _waitSequenceTimeout ?? new WaitForSeconds(_sequenceTimeoutSeconds);
|
||||||
if (!IsDialogueActive) yield break;
|
if (!IsDialogueActive) yield break;
|
||||||
Debug.LogError(
|
Debug.LogError(
|
||||||
$"[DialogueManager] 对话序列 (npcId='{npcId}') 超时 {_sequenceTimeoutSeconds}s 未结束," +
|
$"[DialogueManager] 对话序列 (npcId='{npcId}') 超时 {_sequenceTimeoutSeconds}s 未结束," +
|
||||||
@@ -271,6 +284,7 @@ namespace BaseGames.Dialogue
|
|||||||
_dialogueBox?.HideChoices();
|
_dialogueBox?.HideChoices();
|
||||||
_dialogueBox?.Hide();
|
_dialogueBox?.Hide();
|
||||||
IsDialogueActive = false;
|
IsDialogueActive = false;
|
||||||
|
_currentNpcId = null;
|
||||||
_currentPriority = 0;
|
_currentPriority = 0;
|
||||||
_skipRequested = false;
|
_skipRequested = false;
|
||||||
_selectedChoiceIndex = -1;
|
_selectedChoiceIndex = -1;
|
||||||
@@ -424,6 +438,7 @@ namespace BaseGames.Dialogue
|
|||||||
{
|
{
|
||||||
_dialogueBox?.Hide();
|
_dialogueBox?.Hide();
|
||||||
IsDialogueActive = false;
|
IsDialogueActive = false;
|
||||||
|
_currentNpcId = null;
|
||||||
_currentPriority = 0;
|
_currentPriority = 0;
|
||||||
|
|
||||||
// 优先通过 _onDialogueEnded 事件让 InputManager 决定如何恢复输入;
|
// 优先通过 _onDialogueEnded 事件让 InputManager 决定如何恢复输入;
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ namespace BaseGames.Dialogue
|
|||||||
/// <summary>当前是否有对话正在播放。</summary>
|
/// <summary>当前是否有对话正在播放。</summary>
|
||||||
bool IsDialogueActive { get; }
|
bool IsDialogueActive { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前正在播放对话的 NPC ID。无对话活跃时为 <see langword="null"/>。
|
||||||
|
/// 供地图标记、HUD、分析埋点等外部系统主动查询,无需订阅事件。
|
||||||
|
/// </summary>
|
||||||
|
string CurrentNpcId { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 每次对话序列(含分支链)全部播完后触发。
|
/// 每次对话序列(含分支链)全部播完后触发。
|
||||||
/// 测试代码可订阅此事件等待对话结束,无需依赖 VoidEventChannelSO 资产。
|
/// 测试代码可订阅此事件等待对话结束,无需依赖 VoidEventChannelSO 资产。
|
||||||
|
|||||||
314
Assets/_Game/Scripts/Editor/Quest/QuestOverviewEditorWindow.cs
Normal file
314
Assets/_Game/Scripts/Editor/Quest/QuestOverviewEditorWindow.cs
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using BaseGames.Quest;
|
||||||
|
using QuestStateEnum = BaseGames.Core.Events.QuestState;
|
||||||
|
|
||||||
|
namespace BaseGames.Editor.Quest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 任务总览编辑器窗口(架构 22_QuestChallengeModule §Editor)。
|
||||||
|
/// 菜单:BaseGames/Quest/Quest Overview
|
||||||
|
///
|
||||||
|
/// 功能:
|
||||||
|
/// - 列出项目中所有 QuestSO 资产,支持按名称/ID/分类过滤
|
||||||
|
/// - Play Mode:读取 IQuestManager 运行时状态,以颜色区分 Active / Completed / Failed 等
|
||||||
|
/// - 单击行 → 在 Project 窗口中高亮(Ping)对应资产
|
||||||
|
/// - 双击行 → 在 Inspector 中选中对应资产
|
||||||
|
/// - 顶部统计栏(编辑器):总数 / 缺少 questId / 缺少本地化 Key
|
||||||
|
/// - Play Mode 统计栏:Active / Completed / Failed 数量
|
||||||
|
/// </summary>
|
||||||
|
public class QuestOverviewEditorWindow : EditorWindow
|
||||||
|
{
|
||||||
|
// ── State ──────────────────────────────────────────────────────────
|
||||||
|
private QuestSO[] _allQuests = System.Array.Empty<QuestSO>();
|
||||||
|
private QuestSO[] _filtered = System.Array.Empty<QuestSO>();
|
||||||
|
private Vector2 _scroll;
|
||||||
|
private string _searchText = string.Empty;
|
||||||
|
private QuestCategory _categoryFilter = (QuestCategory)(-1); // -1 = 全部
|
||||||
|
private double _lastRefreshTime = -30.0;
|
||||||
|
private const double RefreshIntervalSeconds = 5.0;
|
||||||
|
|
||||||
|
// ── Play Mode runtime 状态缓存 ─────────────────────────────────────
|
||||||
|
private IQuestManager _runtimeManager;
|
||||||
|
private bool _isPlayMode;
|
||||||
|
|
||||||
|
// ── Colors ─────────────────────────────────────────────────────────
|
||||||
|
private static readonly Color ColActive = new Color(0.25f, 0.70f, 0.95f, 0.85f);
|
||||||
|
private static readonly Color ColCompleted = new Color(0.20f, 0.80f, 0.30f, 0.85f);
|
||||||
|
private static readonly Color ColFailed = new Color(0.90f, 0.25f, 0.25f, 0.85f);
|
||||||
|
private static readonly Color ColPaused = new Color(0.90f, 0.75f, 0.20f, 0.85f);
|
||||||
|
private static readonly Color ColAvail = new Color(0.80f, 0.80f, 0.80f, 0.70f);
|
||||||
|
private static readonly Color ColUnavail = new Color(0.45f, 0.45f, 0.45f, 0.60f);
|
||||||
|
private static readonly Color ColError = new Color(1.00f, 0.60f, 0.10f, 0.85f);
|
||||||
|
|
||||||
|
// ── Menu ───────────────────────────────────────────────────────────
|
||||||
|
[MenuItem("BaseGames/Quest/Quest Overview")]
|
||||||
|
public static void OpenWindow()
|
||||||
|
{
|
||||||
|
var win = GetWindow<QuestOverviewEditorWindow>("Quest Overview");
|
||||||
|
win.minSize = new Vector2(680, 420);
|
||||||
|
win.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lifecycle ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
EditorApplication.playModeStateChanged += OnPlayModeChanged;
|
||||||
|
RefreshQuestList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable()
|
||||||
|
{
|
||||||
|
EditorApplication.playModeStateChanged -= OnPlayModeChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlayModeChanged(PlayModeStateChange change)
|
||||||
|
{
|
||||||
|
_isPlayMode = EditorApplication.isPlaying;
|
||||||
|
if (_isPlayMode)
|
||||||
|
RefreshRuntimeManager();
|
||||||
|
else
|
||||||
|
_runtimeManager = null;
|
||||||
|
Repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFocus()
|
||||||
|
{
|
||||||
|
RefreshQuestList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GUI ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void OnGUI()
|
||||||
|
{
|
||||||
|
// 定时自动刷新(Edit Mode 下资产可能被增删)
|
||||||
|
if (EditorApplication.timeSinceStartup - _lastRefreshTime > RefreshIntervalSeconds)
|
||||||
|
RefreshQuestList();
|
||||||
|
|
||||||
|
if (_isPlayMode && _runtimeManager == null)
|
||||||
|
RefreshRuntimeManager();
|
||||||
|
|
||||||
|
DrawToolbar();
|
||||||
|
DrawStats();
|
||||||
|
DrawTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawToolbar()
|
||||||
|
{
|
||||||
|
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||||||
|
|
||||||
|
// 搜索框
|
||||||
|
GUI.SetNextControlName("SearchField");
|
||||||
|
var newSearch = EditorGUILayout.TextField(_searchText, EditorStyles.toolbarSearchField,
|
||||||
|
GUILayout.Width(220));
|
||||||
|
if (newSearch != _searchText)
|
||||||
|
{
|
||||||
|
_searchText = newSearch;
|
||||||
|
ApplyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分类过滤
|
||||||
|
GUILayout.Space(8);
|
||||||
|
GUILayout.Label("分类:", GUILayout.Width(36));
|
||||||
|
var cats = new[] { "全部", "Main", "Side", "Daily", "Hidden" };
|
||||||
|
var catVals = new[] { (QuestCategory)(-1), QuestCategory.Main, QuestCategory.Side,
|
||||||
|
QuestCategory.Daily, QuestCategory.Hidden };
|
||||||
|
int curIdx = System.Array.IndexOf(catVals, _categoryFilter);
|
||||||
|
int newIdx = EditorGUILayout.Popup(curIdx < 0 ? 0 : curIdx, cats,
|
||||||
|
EditorStyles.toolbarDropDown, GUILayout.Width(80));
|
||||||
|
if (newIdx != (curIdx < 0 ? 0 : curIdx))
|
||||||
|
{
|
||||||
|
_categoryFilter = catVals[newIdx];
|
||||||
|
ApplyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
|
||||||
|
if (GUILayout.Button("刷新", EditorStyles.toolbarButton, GUILayout.Width(48)))
|
||||||
|
RefreshQuestList();
|
||||||
|
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawStats()
|
||||||
|
{
|
||||||
|
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
|
||||||
|
|
||||||
|
GUILayout.Label($"共 {_allQuests.Length} 个任务 显示 {_filtered.Length}", EditorStyles.miniLabel);
|
||||||
|
|
||||||
|
if (!_isPlayMode)
|
||||||
|
{
|
||||||
|
int missingId = 0, missingKey = 0;
|
||||||
|
foreach (var q in _allQuests)
|
||||||
|
{
|
||||||
|
if (q == null) continue;
|
||||||
|
if (string.IsNullOrWhiteSpace(q.questId)) missingId++;
|
||||||
|
if (string.IsNullOrWhiteSpace(q.displayNameKey)) missingKey++;
|
||||||
|
}
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
if (missingId > 0)
|
||||||
|
{
|
||||||
|
var prev = GUI.color;
|
||||||
|
GUI.color = ColError;
|
||||||
|
GUILayout.Label($"⚠ 缺少 questId: {missingId}", EditorStyles.miniLabel);
|
||||||
|
GUI.color = prev;
|
||||||
|
}
|
||||||
|
if (missingKey > 0)
|
||||||
|
{
|
||||||
|
var prev = GUI.color;
|
||||||
|
GUI.color = ColError;
|
||||||
|
GUILayout.Label($"⚠ 缺少本地化 Key: {missingKey}", EditorStyles.miniLabel);
|
||||||
|
GUI.color = prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_runtimeManager != null)
|
||||||
|
{
|
||||||
|
int active = 0, completed = 0, failed = 0;
|
||||||
|
foreach (var q in _allQuests)
|
||||||
|
{
|
||||||
|
if (q == null || string.IsNullOrEmpty(q.questId)) continue;
|
||||||
|
var s = _runtimeManager.GetState(q.questId);
|
||||||
|
if (s == QuestStateEnum.Active) active++;
|
||||||
|
if (s == QuestStateEnum.Completed) completed++;
|
||||||
|
if (s == QuestStateEnum.Failed) failed++;
|
||||||
|
}
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
GUI.color = ColActive; GUILayout.Label($"进行中 {active}", EditorStyles.miniLabel);
|
||||||
|
GUI.color = ColCompleted; GUILayout.Label($"完成 {completed}", EditorStyles.miniLabel);
|
||||||
|
GUI.color = ColFailed; GUILayout.Label($"失败 {failed}", EditorStyles.miniLabel);
|
||||||
|
GUI.color = Color.white;
|
||||||
|
}
|
||||||
|
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawTable()
|
||||||
|
{
|
||||||
|
// 表头
|
||||||
|
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||||||
|
GUILayout.Label("questId", EditorStyles.miniLabel, GUILayout.Width(200));
|
||||||
|
GUILayout.Label("资产名称", EditorStyles.miniLabel, GUILayout.Width(170));
|
||||||
|
GUILayout.Label("分类", EditorStyles.miniLabel, GUILayout.Width(55));
|
||||||
|
GUILayout.Label("displayNameKey", EditorStyles.miniLabel, GUILayout.Width(160));
|
||||||
|
if (_isPlayMode)
|
||||||
|
GUILayout.Label("运行时状态", EditorStyles.miniLabel, GUILayout.Width(80));
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
_scroll = EditorGUILayout.BeginScrollView(_scroll);
|
||||||
|
|
||||||
|
for (int i = 0; i < _filtered.Length; i++)
|
||||||
|
{
|
||||||
|
var quest = _filtered[i];
|
||||||
|
if (quest == null) continue;
|
||||||
|
|
||||||
|
Color rowColor = Color.clear;
|
||||||
|
string stateLabel = string.Empty;
|
||||||
|
|
||||||
|
if (_isPlayMode && _runtimeManager != null && !string.IsNullOrEmpty(quest.questId))
|
||||||
|
{
|
||||||
|
var s = _runtimeManager.GetState(quest.questId);
|
||||||
|
(rowColor, stateLabel) = s switch
|
||||||
|
{
|
||||||
|
QuestStateEnum.Active => (ColActive, "Active"),
|
||||||
|
QuestStateEnum.Completed => (ColCompleted, "Completed"),
|
||||||
|
QuestStateEnum.Failed => (ColFailed, "Failed"),
|
||||||
|
QuestStateEnum.Paused => (ColPaused, "Paused"),
|
||||||
|
QuestStateEnum.Available => (ColAvail, "Available"),
|
||||||
|
_ => (ColUnavail, "Unavailable"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Edit Mode:对有配置问题的行用警告色
|
||||||
|
bool hasError = string.IsNullOrWhiteSpace(quest.questId)
|
||||||
|
|| string.IsNullOrWhiteSpace(quest.displayNameKey);
|
||||||
|
rowColor = hasError ? new Color(1f, 0.85f, 0.3f, 0.15f) : (i % 2 == 0
|
||||||
|
? new Color(0.2f, 0.2f, 0.2f, 0.1f)
|
||||||
|
: Color.clear);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 行背景
|
||||||
|
Rect rowRect = EditorGUILayout.BeginHorizontal();
|
||||||
|
if (rowColor != Color.clear)
|
||||||
|
EditorGUI.DrawRect(rowRect, rowColor);
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
string.IsNullOrWhiteSpace(quest.questId) ? "⚠ (空)" : quest.questId,
|
||||||
|
GUILayout.Width(200));
|
||||||
|
EditorGUILayout.LabelField(quest.name, GUILayout.Width(170));
|
||||||
|
EditorGUILayout.LabelField(quest.category.ToString(), GUILayout.Width(55));
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
string.IsNullOrWhiteSpace(quest.displayNameKey) ? "⚠ (空)" : quest.displayNameKey,
|
||||||
|
GUILayout.Width(160));
|
||||||
|
if (_isPlayMode)
|
||||||
|
EditorGUILayout.LabelField(stateLabel, GUILayout.Width(80));
|
||||||
|
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
// 单击 Ping,双击 Select
|
||||||
|
if (Event.current.type == EventType.MouseDown && rowRect.Contains(Event.current.mousePosition))
|
||||||
|
{
|
||||||
|
if (Event.current.clickCount == 1)
|
||||||
|
EditorGUIUtility.PingObject(quest);
|
||||||
|
else
|
||||||
|
Selection.activeObject = quest;
|
||||||
|
Event.current.Use();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.EndScrollView();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 数据加载 ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void RefreshQuestList()
|
||||||
|
{
|
||||||
|
var list = new List<QuestSO>();
|
||||||
|
string[] guids = AssetDatabase.FindAssets("t:QuestSO");
|
||||||
|
foreach (var guid in guids)
|
||||||
|
{
|
||||||
|
var q = AssetDatabase.LoadAssetAtPath<QuestSO>(AssetDatabase.GUIDToAssetPath(guid));
|
||||||
|
if (q != null) list.Add(q);
|
||||||
|
}
|
||||||
|
list.Sort((a, b) => string.Compare(a.questId, b.questId, System.StringComparison.Ordinal));
|
||||||
|
_allQuests = list.ToArray();
|
||||||
|
ApplyFilter();
|
||||||
|
_lastRefreshTime = EditorApplication.timeSinceStartup;
|
||||||
|
Repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyFilter()
|
||||||
|
{
|
||||||
|
var result = new List<QuestSO>(_allQuests.Length);
|
||||||
|
foreach (var q in _allQuests)
|
||||||
|
{
|
||||||
|
if (q == null) continue;
|
||||||
|
|
||||||
|
// 分类过滤
|
||||||
|
if ((int)_categoryFilter != -1 && q.category != _categoryFilter) continue;
|
||||||
|
|
||||||
|
// 文字搜索(questId / 资产名 / displayNameKey)
|
||||||
|
if (!string.IsNullOrEmpty(_searchText))
|
||||||
|
{
|
||||||
|
bool match = q.questId.IndexOf(_searchText, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| q.name.IndexOf(_searchText, System.StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
|| (!string.IsNullOrEmpty(q.displayNameKey) &&
|
||||||
|
q.displayNameKey.IndexOf(_searchText, System.StringComparison.OrdinalIgnoreCase) >= 0);
|
||||||
|
if (!match) continue;
|
||||||
|
}
|
||||||
|
result.Add(q);
|
||||||
|
}
|
||||||
|
_filtered = result.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshRuntimeManager()
|
||||||
|
{
|
||||||
|
_runtimeManager = BaseGames.Core.ServiceLocator.GetOrDefault<IQuestManager>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,11 @@ namespace BaseGames.Quest
|
|||||||
[Tooltip("任务已完成(QuestState.Completed)后再次交互时播放。通常是 NPC 闲聊或后续剧情的对话。")]
|
[Tooltip("任务已完成(QuestState.Completed)后再次交互时播放。通常是 NPC 闲聊或后续剧情的对话。")]
|
||||||
[SerializeField] private DialogueSequenceSO _completedDialogue;
|
[SerializeField] private DialogueSequenceSO _completedDialogue;
|
||||||
|
|
||||||
|
[Header("交互选项")]
|
||||||
|
[Tooltip("勾选后,任务进行中(Active 且未完成)时交互提示变为"放弃任务",交互即触发 AbandonQuest。\n" +
|
||||||
|
"适合允许玩家主动放弃的支线任务;主线任务建议保持取消勾选。")]
|
||||||
|
[SerializeField] private bool _allowAbandon;
|
||||||
|
|
||||||
// ── InteractableNPC 覆盖 ──────────────────────────────────────────────
|
// ── InteractableNPC 覆盖 ──────────────────────────────────────────────
|
||||||
|
|
||||||
// 缓存上次查找结果,避免 InteractPrompt get(每帧调用)重复遍历 _offeredQuests。
|
// 缓存上次查找结果,避免 InteractPrompt get(每帧调用)重复遍历 _offeredQuests。
|
||||||
@@ -46,6 +51,8 @@ namespace BaseGames.Quest
|
|||||||
private const string K_InProgress = "QUEST_PROMPT_IN_PROGRESS";
|
private const string K_InProgress = "QUEST_PROMPT_IN_PROGRESS";
|
||||||
private const string K_Paused = "QUEST_PROMPT_PAUSED";
|
private const string K_Paused = "QUEST_PROMPT_PAUSED";
|
||||||
private const string K_Talk = "QUEST_PROMPT_TALK";
|
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
|
// 缓存 IQuestManager + IQuestEventSource 引用,避免每次访问 InteractPrompt 调用 SL.GetOrDefault
|
||||||
private IQuestManager _questManager;
|
private IQuestManager _questManager;
|
||||||
@@ -100,12 +107,32 @@ namespace BaseGames.Quest
|
|||||||
{
|
{
|
||||||
var quest = GetCachedQuest();
|
var quest = GetCachedQuest();
|
||||||
if (quest == null || _questManager == null) return base.InteractPrompt;
|
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
|
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.Paused => GetPrompt(K_Paused, "暂停中…"),
|
||||||
QuestStateEnum.Completed => GetPrompt(K_Talk, "对话"),
|
QuestStateEnum.Completed => GetPrompt(K_Talk, "对话"),
|
||||||
_ => base.InteractPrompt,
|
_ => base.InteractPrompt,
|
||||||
@@ -120,16 +147,27 @@ namespace BaseGames.Quest
|
|||||||
|
|
||||||
if (_cachedState == QuestStateEnum.Available)
|
if (_cachedState == QuestStateEnum.Available)
|
||||||
{
|
{
|
||||||
|
// 亲密度门槛等锁定条件未满足时静默返回(InteractPrompt 已显示原因,玩家可见)
|
||||||
|
var lockInfo = _questManager.GetQuestLockInfo(quest.questId);
|
||||||
|
if (lockInfo.IsLocked) return;
|
||||||
_questManager.AcceptQuest(quest.questId);
|
_questManager.AcceptQuest(quest.questId);
|
||||||
// OnQuestStateChanged 事件会自动触发 HandleQuestStateChanged → _cacheDirty = true
|
// OnQuestStateChanged 事件会自动触发 HandleQuestStateChanged → _cacheDirty = true
|
||||||
}
|
}
|
||||||
else if (_cachedState == QuestStateEnum.Active && _questManager.IsReadyToComplete(quest.questId))
|
else if (_cachedState == QuestStateEnum.Active)
|
||||||
|
{
|
||||||
|
if (_questManager.IsReadyToComplete(quest.questId))
|
||||||
{
|
{
|
||||||
// 直接从 player 获取 PlayerStats,避免对 PlayerController 的程序集依赖
|
// 直接从 player 获取 PlayerStats,避免对 PlayerController 的程序集依赖
|
||||||
var stats = player.GetComponentInParent<PlayerStats>();
|
var stats = player.GetComponentInParent<PlayerStats>();
|
||||||
_questManager.CompleteQuest(quest.questId, stats);
|
_questManager.CompleteQuest(quest.questId, stats);
|
||||||
// OnQuestStateChanged 事件会自动触发 HandleQuestStateChanged → _cacheDirty = true
|
// OnQuestStateChanged 事件会自动触发 HandleQuestStateChanged → _cacheDirty = true
|
||||||
}
|
}
|
||||||
|
else if (_allowAbandon)
|
||||||
|
{
|
||||||
|
_questManager.AbandonQuest(quest.questId);
|
||||||
|
// OnQuestStateChanged 事件会自动触发 HandleQuestStateChanged → _cacheDirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DialogueSequenceSO GetCurrentDialogue()
|
protected override DialogueSequenceSO GetCurrentDialogue()
|
||||||
|
|||||||
@@ -395,7 +395,13 @@ namespace BaseGames.Quest
|
|||||||
"但 giverNpc 未配置,好感度无法发放。请在 QuestSO 中指定 giverNpc。", quest);
|
"但 giverNpc 未配置,好感度无法发放。请在 QuestSO 中指定 giverNpc。", quest);
|
||||||
return;
|
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);
|
_npcRelations.TryGetValue(quest.GiverNpcId, out int current);
|
||||||
int newTotal = current + quest.reward.affinityBonus;
|
int newTotal = current + quest.reward.affinityBonus;
|
||||||
@@ -504,7 +510,13 @@ namespace BaseGames.Quest
|
|||||||
{
|
{
|
||||||
if (dialogueService != null)
|
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;
|
dialoguePlayed = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -118,6 +118,16 @@ namespace BaseGames.Quest
|
|||||||
s_questIdsCacheTime = -10.0;
|
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();
|
ValidateObjectiveIds();
|
||||||
ValidatePrerequisiteCycles();
|
ValidatePrerequisiteCycles();
|
||||||
ValidateBranchCycles();
|
ValidateBranchCycles();
|
||||||
|
|||||||
Reference in New Issue
Block a user