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

@@ -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>();
}
}
}