feat: Round 48 narrative systems improvements
- QuestSO: Add ValidateBranchCycles() DFS detection for branches[].nextQuest loop - QuestSO: Mark three legacy prerequisite fields with v2.0 removal warning in Tooltip - IQuestManager: Add QuestLockReason enum + QuestLockInfo struct (strongly-typed lock info) - IQuestManager: Add GetQuestLockInfo() method to interface; GetQuestLockReason() now delegates to it - IQuestEventSource: Add OnQuestStateChanged(questId, oldState, newState) unified event - QuestManager: Implement GetQuestLockInfo(); fire OnQuestStateChanged on all state transitions - DialogueManager: Add one-frame yield in HandleChoices before ShowChoices (skip-debounce fix) - DialogueManager: Increment _playbackId in ForceEnd() to invalidate residual choice callbacks - DialogueSequenceSO: Add UNITY_EDITOR debug log in TryGetActiveVariant on variant match - WorldStateRegistry: Add OnBatchStateChanged event + BatchMark() batch-write API - DialogueModule: List badge shows warning indicator for unconditional-shadowing variants - DialogueModule: BuildVariantsCard shows logic mode (AND/OR) alongside flag conditions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.World;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -6,23 +7,35 @@ namespace BaseGames.Dialogue
|
||||
{
|
||||
/// <summary>
|
||||
/// 条件对话 NPC(架构 14_NarrativeModule §7)。
|
||||
/// 扩展 InteractableNPC,根据 WorldStateRegistry 标志动态选择对话版本。
|
||||
/// 扩展 InteractableNPC,根据世界状态标志动态选择对话版本。
|
||||
/// 版本列表从高到低优先级排列;第一个满足条件的版本生效。
|
||||
///
|
||||
/// _worldState 可留空:留空时自动从 ServiceLocator 获取全局注册的 IWorldStateReader,
|
||||
/// 便于无需在每个 NPC Prefab 上手动拖入 WorldStateRegistry 资产。
|
||||
/// </summary>
|
||||
public class NarrativeNPC : InteractableNPC
|
||||
{
|
||||
[Header("台词版本集(从高到低优先级排列)")]
|
||||
[Tooltip("条件对话版本列表。运行时从上到下检查,第一个满足条件的版本被播放。\n" +
|
||||
"版本之间的优先级由列表顺序决定——请将最具体的条件放在最上方。")]
|
||||
[SerializeField] private DialogueVersion[] _dialogueVersions;
|
||||
[SerializeField] private DialogueSequenceSO _fallbackDialogue; // 无条件满足时的兜底台词
|
||||
[SerializeField] private WorldStateRegistry _worldState; // SO 注入
|
||||
[Tooltip("所有版本均不满足条件时的兜底对话。务必配置,否则运行时会输出 LogWarning 且 NPC 无对话。")]
|
||||
[SerializeField] private DialogueSequenceSO _fallbackDialogue;
|
||||
[Tooltip("世界状态 SO(可选)。留空时自动从 ServiceLocator 获取全局 IWorldStateReader。\n" +
|
||||
"通常同场景下多个 NPC 共用同一个 WorldStateRegistry;\n" +
|
||||
"若全局已通过 ServiceLocator 注册,可不在此处手动指定。")]
|
||||
[SerializeField] private WorldStateRegistry _worldState;
|
||||
|
||||
protected override DialogueSequenceSO GetCurrentDialogue()
|
||||
{
|
||||
IWorldStateReader reader = _worldState
|
||||
?? ServiceLocator.GetOrDefault<IWorldStateReader>();
|
||||
|
||||
if (_dialogueVersions == null) return _fallbackDialogue;
|
||||
|
||||
foreach (var version in _dialogueVersions)
|
||||
{
|
||||
if (version != null && version.CheckConditions(_worldState))
|
||||
if (version != null && version.CheckConditions(reader))
|
||||
return version.dialogue;
|
||||
}
|
||||
|
||||
@@ -43,6 +56,7 @@ namespace BaseGames.Dialogue
|
||||
{
|
||||
[Tooltip("编辑器显示名,如'森林 Boss 击败后'")]
|
||||
public string versionLabel;
|
||||
[Tooltip("此版本对应的对话序列 SO。条件满足时播放。留空时等同于跳过此版本。")]
|
||||
public DialogueSequenceSO dialogue;
|
||||
|
||||
[Tooltip("全部满足才激活此版本(AND 关系)")]
|
||||
@@ -53,18 +67,21 @@ namespace BaseGames.Dialogue
|
||||
[WorldStateFlag]
|
||||
public string[] blockedByFlags;
|
||||
|
||||
/// <summary>检查此版本的激活条件(AND requiredFlags / NOT blockedByFlags)。</summary>
|
||||
public bool CheckConditions(WorldStateRegistry registry)
|
||||
/// <summary>
|
||||
/// 检查此版本的激活条件(AND requiredFlags / NOT blockedByFlags)。
|
||||
/// reader 为 null 时直接返回 false(无法判断,视为条件不满足)。
|
||||
/// </summary>
|
||||
public bool CheckConditions(IWorldStateReader reader)
|
||||
{
|
||||
if (registry == null) return false;
|
||||
if (reader == null) return false;
|
||||
|
||||
if (requiredFlags != null)
|
||||
foreach (var f in requiredFlags)
|
||||
if (!registry.HasFlag(f)) return false;
|
||||
if (!reader.HasFlag(f)) return false;
|
||||
|
||||
if (blockedByFlags != null)
|
||||
foreach (var f in blockedByFlags)
|
||||
if (registry.HasFlag(f)) return false;
|
||||
if (reader.HasFlag(f)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user