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:
@@ -0,0 +1,24 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Core.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// NPC 对话切换事件负载(强类型,替代 "{npcId}:{sequenceId}" 字符串拼接)。
|
||||
/// </summary>
|
||||
public struct NpcDialogueChangeEvent
|
||||
{
|
||||
/// <summary>NPC 的唯一 ID(对应 NpcSO.npcId)。</summary>
|
||||
public string npcId;
|
||||
|
||||
/// <summary>要切换到的对话序列 ID(对应 DialogueSequenceSO.sequenceId)。</summary>
|
||||
public string newSequenceId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NPC 对话切换事件频道。
|
||||
/// 由 ChangeNPCDialogueAction 在事件链中触发;NPC 组件订阅后根据自身 npcId 过滤处理。
|
||||
/// 资产路径建议:Assets/ScriptableObjects/Events/EVT_NpcDialogueChange.asset
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Events/NpcDialogueChange")]
|
||||
public class NpcDialogueChangeEventChannelSO : BaseEventChannelSO<NpcDialogueChangeEvent> { }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 111b5e123d3c3bc4ab5114666d8d2641
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -9,7 +9,9 @@ namespace BaseGames.Core.Events
|
||||
Available = 1,
|
||||
Active = 2,
|
||||
Completed = 3,
|
||||
Failed = 4
|
||||
Failed = 4,
|
||||
/// <summary>任务已接取但被暂停(如剧情锁定),不推进目标,不触发失败判定。</summary>
|
||||
Paused = 5,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -23,7 +25,7 @@ namespace BaseGames.Core.Events
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 任务目标进度事件负载。
|
||||
/// 任务目标进度事件负载(单目标)。
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct QuestObjectiveEvent
|
||||
@@ -33,4 +35,17 @@ namespace BaseGames.Core.Events
|
||||
public int Progress;
|
||||
public int Required;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同帧内某任务多个目标同时更新时的批量事件负载。
|
||||
/// 订阅此事件可在一帧内一次性处理同任务的所有目标变更,避免 UI 多次重绘。
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct QuestObjectiveBatchEvent
|
||||
{
|
||||
/// <summary>发生目标进度变更的任务 ID。</summary>
|
||||
public string QuestId;
|
||||
/// <summary>本帧内该任务所有更新过的单目标事件列表(至少 1 个)。</summary>
|
||||
public System.Collections.Generic.List<QuestObjectiveEvent> Updates;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,13 @@ namespace BaseGames.Core.Events
|
||||
{
|
||||
[CreateAssetMenu(menuName = "BaseGames/Events/QuestObjective")]
|
||||
public class QuestObjectiveEventChannelSO : BaseEventChannelSO<QuestObjectiveEvent> { }
|
||||
|
||||
/// <summary>
|
||||
/// 批量任务目标进度事件频道。
|
||||
/// 同帧内同一任务多个目标同时更新时,聚合为一次广播,
|
||||
/// 供 UI 侧监听以避免同帧多次重绘任务追踪 HUD。
|
||||
/// 资产路径建议:Assets/ScriptableObjects/Events/EVT_QuestObjectiveBatchUpdated.asset
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Events/QuestObjectiveBatch")]
|
||||
public class QuestObjectiveBatchEventChannelSO : BaseEventChannelSO<QuestObjectiveBatchEvent> { }
|
||||
}
|
||||
|
||||
12
Assets/_Game/Scripts/Core/IWorldStateReader.cs
Normal file
12
Assets/_Game/Scripts/Core/IWorldStateReader.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace BaseGames.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 只读世界状态查询接口(用于对话版本条件判断)。
|
||||
/// 解耦 NarrativeNPC / DialogueVersion 对 WorldStateRegistry 具体类型的直接依赖。
|
||||
/// </summary>
|
||||
public interface IWorldStateReader
|
||||
{
|
||||
/// <summary>检查指定 Flag 是否已设置。</summary>
|
||||
bool HasFlag(string key);
|
||||
}
|
||||
}
|
||||
@@ -134,8 +134,19 @@ namespace BaseGames.Core.Save
|
||||
[Serializable]
|
||||
public class QuestState
|
||||
{
|
||||
/// <summary>
|
||||
/// 此 QuestState 数据格式版本号。
|
||||
/// 1 = 原始格式(ProgressCounts 按索引,已弃用)
|
||||
/// 2 = Round 24+ 格式(ObjectiveProgress 按 objectiveId 键值对)
|
||||
/// OnLoad 按此字段显式选择解析路径,杜绝依赖 Count > 0 的隐式推断。
|
||||
/// </summary>
|
||||
public int DataVersion = 2;
|
||||
public string Status; // "NotStarted"|"Active"|"Completed"|"Failed"
|
||||
public int ObjectiveIndex;
|
||||
/// <summary>新格式(Round 24+,DataVersion=2):objectiveId → progressCount,重排目标顺序后存档不会错位。</summary>
|
||||
public Dictionary<string, int> ObjectiveProgress = new();
|
||||
/// <summary>旧格式(按数组索引,DataVersion=1):仅用于迁移旧版存档,新存档不再写入。已弃用,将在后续版本移除。</summary>
|
||||
[System.Obsolete("旧格式存档兼容字段,仅供 OnLoad DataVersion=1 迁移使用。新存档改用 ObjectiveProgress(objectiveId 键值对)。")]
|
||||
public List<int> ProgressCounts = new();
|
||||
public string GiverNpcId;
|
||||
}
|
||||
|
||||
74
Assets/_Game/Scripts/Core/WorldFlagRegistrySO.cs
Normal file
74
Assets/_Game/Scripts/Core/WorldFlagRegistrySO.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace BaseGames.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个世界状态标志的定义条目。
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class FlagEntry
|
||||
{
|
||||
[Tooltip("标志唯一 ID,与 SetFlagAction / FlagSetCondition 中填写的字符串完全一致。")]
|
||||
public string id;
|
||||
|
||||
[Tooltip("描述该标志代表的游戏事件或状态(仅供编辑器参考,运行时不使用)。")]
|
||||
public string description;
|
||||
|
||||
[Tooltip("下拉菜单中的分组路径,使用 '/' 分隔层级,例如 '剧情/Boss'。留空则不分组。")]
|
||||
public string group;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 世界状态标志注册表 —— 统一维护项目中所有合法的世界标志 ID、描述和分组。
|
||||
/// 在 Inspector 中为 [WorldStateFlag] 属性提供下拉补全,减少手输错误。
|
||||
/// 创建方式:Project 右键 → Create / BaseGames / Core / WorldFlagRegistry
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Core/WorldFlagRegistry", fileName = "WorldFlagRegistry")]
|
||||
public class WorldFlagRegistrySO : ScriptableObject
|
||||
{
|
||||
[Tooltip("所有合法的世界状态标志定义。由策划/程序在此集中维护。")]
|
||||
public FlagEntry[] flags;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private static WorldFlagRegistrySO _editorInstance;
|
||||
private static double _editorInstanceExpiry;
|
||||
|
||||
/// <summary>
|
||||
/// 编辑器下 30 秒缓存的单例引用(扫描 AssetDatabase 得到)。
|
||||
/// 运行时不可用,请在 UNITY_EDITOR 条件块中调用。
|
||||
/// </summary>
|
||||
public static WorldFlagRegistrySO EditorInstance
|
||||
{
|
||||
get
|
||||
{
|
||||
double now = EditorApplication.timeSinceStartup;
|
||||
if (_editorInstance != null && now < _editorInstanceExpiry)
|
||||
return _editorInstance;
|
||||
|
||||
var guids = AssetDatabase.FindAssets("t:WorldFlagRegistrySO");
|
||||
if (guids.Length == 0)
|
||||
{
|
||||
_editorInstance = null;
|
||||
_editorInstanceExpiry = now + 30.0;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (guids.Length > 1)
|
||||
Debug.LogWarning($"[WorldFlagRegistrySO] 发现 {guids.Length} 个 WorldFlagRegistry.asset," +
|
||||
"将使用第一个。建议项目中只保留一个。");
|
||||
|
||||
string path = AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
_editorInstance = AssetDatabase.LoadAssetAtPath<WorldFlagRegistrySO>(path);
|
||||
_editorInstanceExpiry = now + 30.0;
|
||||
return _editorInstance;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>强制下次访问 EditorInstance 时重新扫描 AssetDatabase。</summary>
|
||||
public static void InvalidateEditorCache() => _editorInstance = null;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
22
Assets/_Game/Scripts/Core/WorldStateFlagAttribute.cs
Normal file
22
Assets/_Game/Scripts/Core/WorldStateFlagAttribute.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 标记一个 string 或 string[] 字段为世界状态标志 Key。
|
||||
/// 在 Inspector 中会显示已知标志下拉菜单,支持直接输入新标志。
|
||||
/// 定义于 BaseGames.Core,可被 Dialogue / Quest / EventChain 等模块无耦合使用。
|
||||
/// </summary>
|
||||
public sealed class WorldStateFlagAttribute : PropertyAttribute { }
|
||||
|
||||
/// <summary>
|
||||
/// 世界状态标志的逻辑组合模式,供 Dialogue 条件变体和 Quest 分支条件共用。
|
||||
/// </summary>
|
||||
public enum WorldStateFlagLogic
|
||||
{
|
||||
/// <summary>全部 requiredFlags 均满足时条件成立(默认,向后兼容)。</summary>
|
||||
And,
|
||||
/// <summary>任意一个 requiredFlag 满足即可使条件成立。</summary>
|
||||
Or,
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Core/WorldStateFlagAttribute.cs.meta
Normal file
11
Assets/_Game/Scripts/Core/WorldStateFlagAttribute.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e10b4c60cc9052f4e83381ceb09424a3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user