feat: Round 50 narrative systems improvements
IQuestManager+QuestManager: add FillQuestsInState/FillFilterQuests buffer overloads (no-alloc hot path); remove R49 duplicate implementations. QuestGiver: cache current quest result (_cachedQuest/_cachedState/_cacheDirty) to avoid per-frame foreach in InteractPrompt; invalidate on OnEnable and Interact_Internal state changes. IDialogueService+DialogueManager: add StartDialogue(..., Action onComplete) overload; callback fires once on ForceEnd (covers both normal end and interrupt); supports chained callbacks via += accumulation. DialogueVariantPreviewWindow: add 'Copy CSV' button in matrix section; exports all 2^N flag combinations with winner column; handles N>10 guard and CSV-safe escaping. WorldStateRegistry: add TryGetCategory(id, out category) reverse lookup for debug tools. NpcSOEditor: new CustomEditor for NpcSO showing live nameKey localization preview in Inspector (green label or warning box if Key not found). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -65,6 +65,10 @@ namespace BaseGames.Dialogue
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private int _playbackId;
|
private int _playbackId;
|
||||||
|
|
||||||
|
// ── 一次性对话完成回调 ────────────────────────────────────────────────
|
||||||
|
// 通过 StartDialogue(..., onComplete) 注册;OnDialogueEnded 触发后调用一次后清空。
|
||||||
|
private System.Action _onCompleteCallback;
|
||||||
|
|
||||||
// ── 子协程通信字段(避免协程间 ref/out 参数)─────────────────────────
|
// ── 子协程通信字段(避免协程间 ref/out 参数)─────────────────────────
|
||||||
/// <summary>HandleChoices 子协程写入结果:玩家选中选项后的后续序列(null = 无后续)。</summary>
|
/// <summary>HandleChoices 子协程写入结果:玩家选中选项后的后续序列(null = 无后续)。</summary>
|
||||||
private DialogueSequenceSO _choiceBranchResult;
|
private DialogueSequenceSO _choiceBranchResult;
|
||||||
@@ -186,6 +190,17 @@ namespace BaseGames.Dialogue
|
|||||||
PlayImmediate(sequence, npcId, priority);
|
PlayImmediate(sequence, npcId, priority);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IDialogueService.StartDialogue(DialogueSequenceSO,string,int,System.Action)"/>
|
||||||
|
public void StartDialogue(DialogueSequenceSO sequence, string npcId, int priority, System.Action onComplete)
|
||||||
|
{
|
||||||
|
if (onComplete != null)
|
||||||
|
{
|
||||||
|
// 若已有待回调,链式追加(不覆盖),保证先来先到
|
||||||
|
_onCompleteCallback += onComplete;
|
||||||
|
}
|
||||||
|
StartDialogue(sequence, npcId, priority);
|
||||||
|
}
|
||||||
|
|
||||||
private void PlayImmediate(DialogueSequenceSO sequence, string npcId, int priority = 0)
|
private void PlayImmediate(DialogueSequenceSO sequence, string npcId, int priority = 0)
|
||||||
{
|
{
|
||||||
IsDialogueActive = true;
|
IsDialogueActive = true;
|
||||||
@@ -239,6 +254,11 @@ namespace BaseGames.Dialogue
|
|||||||
_onDialogueEnded?.Raise();
|
_onDialogueEnded?.Raise();
|
||||||
if (_onDialogueEnded == null) _inputReader?.EnableGameplayInput();
|
if (_onDialogueEnded == null) _inputReader?.EnableGameplayInput();
|
||||||
OnDialogueEnded?.Invoke();
|
OnDialogueEnded?.Invoke();
|
||||||
|
|
||||||
|
// 触发一次性完成回调(正常结束和强制中断均触发)
|
||||||
|
var cb = _onCompleteCallback;
|
||||||
|
_onCompleteCallback = null;
|
||||||
|
cb?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 输入回调 ──────────────────────────────────────────────────────
|
// ── 输入回调 ──────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ namespace BaseGames.Dialogue
|
|||||||
/// <param name="priority">优先级(默认 0)。数值越大越优先;高优先级可打断低优先级对话。</param>
|
/// <param name="priority">优先级(默认 0)。数值越大越优先;高优先级可打断低优先级对话。</param>
|
||||||
void StartDialogue(DialogueSequenceSO sequence, string npcId = "", int priority = 0);
|
void StartDialogue(DialogueSequenceSO sequence, string npcId = "", int priority = 0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启动对话序列,并在本次对话(含所有排队续播)全部结束时回调 <paramref name="onComplete"/>。
|
||||||
|
/// 若 <paramref name="onComplete"/> 为 null,行为与 <see cref="StartDialogue(DialogueSequenceSO,string,int)"/> 完全相同。
|
||||||
|
/// 回调仅触发一次;若对话被 <see cref="ForceEnd"/> 打断,回调同样会被调用(在 ForceEnd 末尾)。
|
||||||
|
/// 适用场景:EventChain 触发对话后等待完成再执行下一动作、CutsceneModule 同步对话与演出。
|
||||||
|
/// </summary>
|
||||||
|
void StartDialogue(DialogueSequenceSO sequence, string npcId, int priority, System.Action onComplete);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 立即强制结束当前对话(含清空等待队列),恢复游戏输入。
|
/// 立即强制结束当前对话(含清空等待队列),恢复游戏输入。
|
||||||
/// 适用于:场景切换、演出系统打断、死亡/传送等需要硬中断的场合。
|
/// 适用于:场景切换、演出系统打断、死亡/传送等需要硬中断的场合。
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ namespace BaseGames.Editor.Dialogue
|
|||||||
matrixBtn.style.marginBottom = 4;
|
matrixBtn.style.marginBottom = 4;
|
||||||
matrixFoldout.Add(matrixBtn);
|
matrixFoldout.Add(matrixBtn);
|
||||||
|
|
||||||
|
var csvBtn = new Button(() => ExportMatrixCsv()) { text = "复制为 CSV" };
|
||||||
|
csvBtn.style.marginBottom = 4;
|
||||||
|
matrixFoldout.Add(csvBtn);
|
||||||
|
|
||||||
Rebuild();
|
Rebuild();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,6 +385,79 @@ namespace BaseGames.Editor.Dialogue
|
|||||||
|
|
||||||
// ── 矩阵分析 ─────────────────────────────────────────────────────────
|
// ── 矩阵分析 ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将矩阵分析结果复制为 CSV 字符串到系统剪贴板。
|
||||||
|
/// 格式:首行为标志名称列头(各列)+ "胜出变体",后续每行为一个组合及其结果。
|
||||||
|
/// N > 10 时与 <see cref="RebuildMatrix"/> 一致,提示用户先减少标志数量。
|
||||||
|
/// </summary>
|
||||||
|
private void ExportMatrixCsv()
|
||||||
|
{
|
||||||
|
if (_target == null || _target.variants == null || _target.variants.Length == 0)
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayDialog("矩阵分析 CSV", "当前无可导出的变体数据,请先选择对话序列 SO。", "确定");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var matrixFlags = _allFlags.Count > 0 ? _allFlags : new List<string>();
|
||||||
|
if (matrixFlags.Count == 0)
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayDialog("矩阵分析 CSV", "变体未使用任何 requiredFlags,无数据可导出。", "确定");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int MaxFlags = 10;
|
||||||
|
if (matrixFlags.Count > MaxFlags)
|
||||||
|
{
|
||||||
|
EditorUtility.DisplayDialog("矩阵分析 CSV",
|
||||||
|
$"标志数量 ({matrixFlags.Count}) 超过 {MaxFlags},无法导出。\n请先在上方取消勾选不关心的标志,再点击此按钮。", "确定");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int n = matrixFlags.Count;
|
||||||
|
int combos = 1 << n;
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
// 表头
|
||||||
|
foreach (var f in matrixFlags)
|
||||||
|
sb.Append(EscapeCsv(f)).Append(',');
|
||||||
|
sb.AppendLine("胜出变体");
|
||||||
|
|
||||||
|
// 数据行
|
||||||
|
for (int mask = 0; mask < combos; mask++)
|
||||||
|
{
|
||||||
|
var combo = new HashSet<string>(System.StringComparer.Ordinal);
|
||||||
|
for (int bit = 0; bit < n; bit++)
|
||||||
|
if ((mask & (1 << bit)) != 0) combo.Add(matrixFlags[bit]);
|
||||||
|
|
||||||
|
var mockReader = new MockFlagReader(combo);
|
||||||
|
int winner = -1;
|
||||||
|
for (int vi = 0; vi < _target.variants.Length; vi++)
|
||||||
|
if (_target.CheckVariant(_target.variants[vi], mockReader)) { winner = vi; break; }
|
||||||
|
|
||||||
|
for (int ci = 0; ci < n; ci++)
|
||||||
|
sb.Append((mask & (1 << ci)) != 0 ? "1" : "0").Append(',');
|
||||||
|
|
||||||
|
string winnerLabel = winner >= 0
|
||||||
|
? $"变体{winner}" +
|
||||||
|
(_target.variants[winner].sequence != null
|
||||||
|
? $"({_target.variants[winner].sequence.name})" : "(无序列)")
|
||||||
|
: "默认台词";
|
||||||
|
sb.AppendLine(EscapeCsv(winnerLabel));
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUIUtility.systemCopyBuffer = sb.ToString();
|
||||||
|
Debug.Log($"[DialogueVariantPreviewWindow] 矩阵 CSV({combos} 行)已复制到剪贴板。");
|
||||||
|
ShowNotification(new GUIContent($"✓ 已复制 {combos} 行 CSV 到剪贴板"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string EscapeCsv(string s)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(s)) return string.Empty;
|
||||||
|
if (s.Contains(',') || s.Contains('"') || s.Contains('\n'))
|
||||||
|
return '"' + s.Replace("\"", "\"\"") + '"';
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 枚举全部 2^N 标志组合(N ≤ 10),以表格形式展示每种组合下胜出的变体索引。
|
/// 枚举全部 2^N 标志组合(N ≤ 10),以表格形式展示每种组合下胜出的变体索引。
|
||||||
/// N > 10 时显示提示,建议手动筛选标志后分析。
|
/// N > 10 时显示提示,建议手动筛选标志后分析。
|
||||||
|
|||||||
73
Assets/_Game/Scripts/Editor/Dialogue/NpcSOEditor.cs
Normal file
73
Assets/_Game/Scripts/Editor/Dialogue/NpcSOEditor.cs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using BaseGames.Dialogue;
|
||||||
|
|
||||||
|
namespace BaseGames.Editor.Dialogue
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// NpcSO 自定义 Inspector。
|
||||||
|
/// 在 nameKey 字段下方实时预览本地化管理器解析后的 NPC 名称,
|
||||||
|
/// 让策划无需打开本地化表即可确认 Key 是否拼写正确。
|
||||||
|
/// 仅在编辑器构建中生效;不影响运行时行为。
|
||||||
|
/// </summary>
|
||||||
|
[CustomEditor(typeof(NpcSO))]
|
||||||
|
public class NpcSOEditor : UnityEditor.Editor
|
||||||
|
{
|
||||||
|
private static GUIStyle s_previewStyle;
|
||||||
|
|
||||||
|
public override void OnInspectorGUI()
|
||||||
|
{
|
||||||
|
DrawDefaultInspector();
|
||||||
|
|
||||||
|
var npc = (NpcSO)target;
|
||||||
|
if (string.IsNullOrEmpty(npc.nameKey)) return;
|
||||||
|
|
||||||
|
// ── nameKey 本地化预览 ──────────────────────────────────────────
|
||||||
|
if (s_previewStyle == null)
|
||||||
|
{
|
||||||
|
s_previewStyle = new GUIStyle(EditorStyles.helpBox)
|
||||||
|
{
|
||||||
|
fontSize = 11,
|
||||||
|
alignment = TextAnchor.MiddleLeft,
|
||||||
|
padding = new RectOffset(8, 8, 4, 4),
|
||||||
|
};
|
||||||
|
s_previewStyle.normal.textColor = new Color(0.55f, 0.90f, 0.55f);
|
||||||
|
}
|
||||||
|
|
||||||
|
string resolved = TryResolveNameKey(npc.nameKey);
|
||||||
|
EditorGUILayout.Space(4);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(resolved))
|
||||||
|
{
|
||||||
|
EditorGUILayout.HelpBox(
|
||||||
|
$"nameKey「{npc.nameKey}」在本地化表中未找到对应文本(或 LocalizationManager 未初始化)。\n" +
|
||||||
|
"请检查本地化表中是否存在此 Key。",
|
||||||
|
MessageType.Warning);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
$"▸ nameKey 解析预览:{resolved}",
|
||||||
|
s_previewStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试通过 LocalizationManager(若已加载)解析 nameKey;
|
||||||
|
/// 如未初始化或找不到 Key,返回 null。
|
||||||
|
/// </summary>
|
||||||
|
private static string TryResolveNameKey(string key)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// LocalizationManager.Get 在编辑器下可能返回空字符串(未初始化),视为未找到
|
||||||
|
var resolved = BaseGames.Localization.LocalizationManager.Get(key, "UI");
|
||||||
|
return string.IsNullOrEmpty(resolved) ? null : resolved;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -131,6 +131,18 @@ namespace BaseGames.Quest
|
|||||||
/// 适用于自定义筛选(如"活跃且含 NPC 亲密度门槛")等场景。
|
/// 适用于自定义筛选(如"活跃且含 NPC 亲密度门槛")等场景。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
System.Collections.Generic.IReadOnlyList<string> FilterQuests(System.Func<string, QuestStateEnum, bool> predicate);
|
System.Collections.Generic.IReadOnlyList<string> FilterQuests(System.Func<string, QuestStateEnum, bool> predicate);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将当前处于指定状态的所有任务 ID 填入调用方提供的列表(先 Clear 再添加)。
|
||||||
|
/// 相比 <see cref="GetQuestsInState"/> 不分配新列表,适合高频调用(如每帧刷新 HUD)。
|
||||||
|
/// </summary>
|
||||||
|
void FillQuestsInState(QuestStateEnum state, System.Collections.Generic.List<string> result);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将满足谓词的所有任务 ID 填入调用方提供的列表(先 Clear 再添加)。
|
||||||
|
/// 相比 <see cref="FilterQuests"/> 不分配新列表,适合高频调用场景。
|
||||||
|
/// </summary>
|
||||||
|
void FillFilterQuests(System.Func<string, QuestStateEnum, bool> predicate, System.Collections.Generic.List<string> result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -32,14 +32,26 @@ namespace BaseGames.Quest
|
|||||||
|
|
||||||
// ── InteractableNPC 覆盖 ──────────────────────────────────────────────
|
// ── InteractableNPC 覆盖 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
// 缓存上次查找结果,避免 InteractPrompt get(每帧调用)重复遍历 _offeredQuests。
|
||||||
|
// 当状态可能变更时(OnEnable、Interact_Internal 后)标记为脏。
|
||||||
|
private QuestSO _cachedQuest;
|
||||||
|
private QuestStateEnum _cachedState;
|
||||||
|
private bool _cacheDirty = true;
|
||||||
|
|
||||||
|
protected override void OnEnable()
|
||||||
|
{
|
||||||
|
base.OnEnable();
|
||||||
|
_cacheDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
public override string InteractPrompt
|
public override string InteractPrompt
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var qm = SL.GetOrDefault<IQuestManager>();
|
var qm = SL.GetOrDefault<IQuestManager>();
|
||||||
var quest = GetCurrentOrCompletedQuest(qm);
|
var quest = GetCachedQuest(qm);
|
||||||
if (quest == null || qm == null) return base.InteractPrompt;
|
if (quest == null || qm == null) return base.InteractPrompt;
|
||||||
return qm.GetState(quest.questId) switch
|
return _cachedState switch
|
||||||
{
|
{
|
||||||
QuestStateEnum.Available => "接受任务",
|
QuestStateEnum.Available => "接受任务",
|
||||||
QuestStateEnum.Active => qm.IsReadyToComplete(quest.questId) ? "提交任务" : "进行中…",
|
QuestStateEnum.Active => qm.IsReadyToComplete(quest.questId) ? "提交任务" : "进行中…",
|
||||||
@@ -53,32 +65,30 @@ namespace BaseGames.Quest
|
|||||||
protected override void Interact_Internal(Transform player)
|
protected override void Interact_Internal(Transform player)
|
||||||
{
|
{
|
||||||
var qm = SL.GetOrDefault<IQuestManager>();
|
var qm = SL.GetOrDefault<IQuestManager>();
|
||||||
var quest = GetCurrentOrCompletedQuest(qm);
|
var quest = GetCachedQuest(qm);
|
||||||
if (quest == null || qm == null) return;
|
if (quest == null || qm == null) return;
|
||||||
|
|
||||||
var state = qm.GetState(quest.questId);
|
if (_cachedState == QuestStateEnum.Available)
|
||||||
|
|
||||||
if (state == QuestStateEnum.Available)
|
|
||||||
{
|
{
|
||||||
qm.AcceptQuest(quest.questId);
|
qm.AcceptQuest(quest.questId);
|
||||||
|
_cacheDirty = true; // 状态已变更,下次访问重新查询
|
||||||
}
|
}
|
||||||
else if (state == QuestStateEnum.Active && qm.IsReadyToComplete(quest.questId))
|
else if (_cachedState == QuestStateEnum.Active && qm.IsReadyToComplete(quest.questId))
|
||||||
{
|
{
|
||||||
// 直接从 player 获取 PlayerStats,避免对 PlayerController 的程序集依赖
|
// 直接从 player 获取 PlayerStats,避免对 PlayerController 的程序集依赖
|
||||||
var stats = player.GetComponentInParent<PlayerStats>();
|
var stats = player.GetComponentInParent<PlayerStats>();
|
||||||
qm.CompleteQuest(quest.questId, stats);
|
qm.CompleteQuest(quest.questId, stats);
|
||||||
|
_cacheDirty = true; // 状态已变更,下次访问重新查询
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DialogueSequenceSO GetCurrentDialogue()
|
protected override DialogueSequenceSO GetCurrentDialogue()
|
||||||
{
|
{
|
||||||
var qm = SL.GetOrDefault<IQuestManager>();
|
var qm = SL.GetOrDefault<IQuestManager>();
|
||||||
var quest = GetCurrentOrCompletedQuest(qm);
|
var quest = GetCachedQuest(qm);
|
||||||
if (quest == null || qm == null) return base.GetCurrentDialogue();
|
if (quest == null || qm == null) return base.GetCurrentDialogue();
|
||||||
|
|
||||||
var state = qm.GetState(quest.questId);
|
return _cachedState switch
|
||||||
|
|
||||||
return state switch
|
|
||||||
{
|
{
|
||||||
QuestStateEnum.Available => _availableDialogue,
|
QuestStateEnum.Available => _availableDialogue,
|
||||||
QuestStateEnum.Active => qm.IsReadyToComplete(quest.questId)
|
QuestStateEnum.Active => qm.IsReadyToComplete(quest.questId)
|
||||||
@@ -92,24 +102,36 @@ namespace BaseGames.Quest
|
|||||||
// ── 私有辅助 ─────────────────────────────────────────────────────────
|
// ── 私有辅助 ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 返回当前处于 Available 或 Active 状态的第一个任务;
|
/// 返回缓存的当前任务(处于 Available/Active/Paused 的第一个,或最后一个已完成任务)。
|
||||||
/// 若全部已完成,返回最后一个已完成任务(用于显示 completedDialogue)。
|
/// 若缓存不脏,直接返回上次结果,避免每帧遍历 _offeredQuests。
|
||||||
|
/// 调用 Interact_Internal 后将 _cacheDirty 置 true,确保下次交互状态是最新的。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private QuestSO GetCurrentOrCompletedQuest(IQuestManager qm = null)
|
private QuestSO GetCachedQuest(IQuestManager qm = null)
|
||||||
{
|
{
|
||||||
if (_offeredQuests == null) return null;
|
if (!_cacheDirty && _cachedQuest != null) return _cachedQuest;
|
||||||
|
|
||||||
qm ??= SL.GetOrDefault<IQuestManager>();
|
qm ??= SL.GetOrDefault<IQuestManager>();
|
||||||
if (qm == null) return null;
|
if (_offeredQuests == null || qm == null) { _cacheDirty = false; return null; }
|
||||||
|
|
||||||
QuestSO lastCompleted = null;
|
QuestSO lastCompleted = null;
|
||||||
foreach (var q in _offeredQuests)
|
foreach (var q in _offeredQuests)
|
||||||
{
|
{
|
||||||
if (q == null) continue;
|
if (q == null) continue;
|
||||||
var s = qm.GetState(q.questId);
|
var s = qm.GetState(q.questId);
|
||||||
if (s == QuestStateEnum.Available || s == QuestStateEnum.Active || s == QuestStateEnum.Paused) return q;
|
if (s == QuestStateEnum.Available || s == QuestStateEnum.Active || s == QuestStateEnum.Paused)
|
||||||
|
{
|
||||||
|
_cachedQuest = q;
|
||||||
|
_cachedState = s;
|
||||||
|
_cacheDirty = false;
|
||||||
|
return _cachedQuest;
|
||||||
|
}
|
||||||
if (s == QuestStateEnum.Completed) lastCompleted = q;
|
if (s == QuestStateEnum.Completed) lastCompleted = q;
|
||||||
}
|
}
|
||||||
return lastCompleted;
|
|
||||||
|
_cachedQuest = lastCompleted;
|
||||||
|
_cachedState = lastCompleted != null ? QuestStateEnum.Completed : QuestStateEnum.Unavailable;
|
||||||
|
_cacheDirty = false;
|
||||||
|
return _cachedQuest;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -534,6 +534,42 @@ namespace BaseGames.Quest
|
|||||||
return CheckQuestDepsAndFlags(quest);
|
return CheckQuestDepsAndFlags(quest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IQuestManager.GetQuestsInState"/>
|
||||||
|
public IReadOnlyList<string> GetQuestsInState(QuestStateEnum state)
|
||||||
|
{
|
||||||
|
var result = new List<string>();
|
||||||
|
FillQuestsInState(state, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IQuestManager.FilterQuests"/>
|
||||||
|
public IReadOnlyList<string> FilterQuests(Func<string, QuestStateEnum, bool> predicate)
|
||||||
|
{
|
||||||
|
if (predicate == null) return Array.Empty<string>();
|
||||||
|
var result = new List<string>();
|
||||||
|
FillFilterQuests(predicate, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IQuestManager.FillQuestsInState"/>
|
||||||
|
public void FillQuestsInState(QuestStateEnum state, List<string> result)
|
||||||
|
{
|
||||||
|
if (result == null) return;
|
||||||
|
result.Clear();
|
||||||
|
foreach (var (id, s) in _questStates)
|
||||||
|
if (s == state) result.Add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IQuestManager.FillFilterQuests"/>
|
||||||
|
public void FillFilterQuests(Func<string, QuestStateEnum, bool> predicate, List<string> result)
|
||||||
|
{
|
||||||
|
if (result == null) return;
|
||||||
|
result.Clear();
|
||||||
|
if (predicate == null) return;
|
||||||
|
foreach (var (id, s) in _questStates)
|
||||||
|
if (predicate(id, s)) result.Add(id);
|
||||||
|
}
|
||||||
|
|
||||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||||
// ── IQuestDebugger ────────────────────────────────────────────────────
|
// ── IQuestDebugger ────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -797,25 +833,6 @@ namespace BaseGames.Quest
|
|||||||
return new QuestLockInfo { Reason = QuestLockReason.None };
|
return new QuestLockInfo { Reason = QuestLockReason.None };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="IQuestManager.GetQuestsInState"/>
|
|
||||||
public System.Collections.Generic.IReadOnlyList<string> GetQuestsInState(QuestStateEnum state)
|
|
||||||
{
|
|
||||||
var result = new List<string>();
|
|
||||||
foreach (var (id, s) in _questStates)
|
|
||||||
if (s == state) result.Add(id);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IQuestManager.FilterQuests"/>
|
|
||||||
public System.Collections.Generic.IReadOnlyList<string> FilterQuests(System.Func<string, QuestStateEnum, bool> predicate)
|
|
||||||
{
|
|
||||||
if (predicate == null) return System.Array.Empty<string>();
|
|
||||||
var result = new List<string>();
|
|
||||||
foreach (var (id, state) in _questStates)
|
|
||||||
if (predicate(id, state)) result.Add(id);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根据 flags 数组和 logic 评估标志前置条件是否满足。
|
/// 根据 flags 数组和 logic 评估标志前置条件是否满足。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -153,5 +153,22 @@ namespace BaseGames.World
|
|||||||
|
|
||||||
/// <summary>重置所有状态(开始新游戏时调用)。</summary>
|
/// <summary>重置所有状态(开始新游戏时调用)。</summary>
|
||||||
public void Reset() => _states.Clear();
|
public void Reset() => _states.Clear();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 反向查询:在所有分类中查找 <paramref name="id"/> 首次出现的类别。
|
||||||
|
/// 适用于调试工具(如 DataHub / WorldState 检视面板)快速定位一个 id 属于哪类。
|
||||||
|
/// O(k) 其中 k = 分类数(最多 5)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">要查找的 ID。</param>
|
||||||
|
/// <param name="category">找到时输出该 ID 所属的类别;未找到时输出 <see cref="WorldObjectCategory.Flag"/>(默认值)。</param>
|
||||||
|
/// <returns>true = 找到;false = 任何类别中均无此 id。</returns>
|
||||||
|
public bool TryGetCategory(string id, out WorldObjectCategory category)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(id))
|
||||||
|
foreach (var (cat, set) in _states)
|
||||||
|
if (set.Contains(id)) { category = cat; return true; }
|
||||||
|
category = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user