feat: Add WorldStateFlagAttribute and custom property drawer for enhanced dialogue management

- Implemented WorldStateFlagAttribute to mark string fields as world state flags.
- Created NarrativeNPCEditor for custom inspector to visualize dialogue version activation states.
- Developed WorldStateFlagDrawer to provide dropdown menu for known flags in the inspector.
- Introduced ActorModule for managing DialogueActorSO assets, including viewing, creating, and deleting actors.
- Added DialogueModule for managing DialogueSequenceSO assets with detailed previews and action bars.
- Established QuestModule for managing QuestSO assets, including objectives and branches.
- Implemented QuestManagerPostprocessor to automatically refresh QuestManager's quest list on asset changes.
This commit is contained in:
2026-05-24 00:36:11 +08:00
parent 520f84999b
commit 446fd5dcd0
22 changed files with 1908 additions and 101 deletions

View File

@@ -5,17 +5,38 @@ namespace BaseGames.Dialogue
/// <summary>
/// 对话行结构(架构 14_NarrativeModule §3
/// 每行包含说话人、文本(本地化 Key和可选的语音片段。
///
/// 推荐通过 actor 引用 DialogueActorSO 统一管理头像/名称;
/// speakerNameKey / portraitSprite 作为无 SO 时的直接覆盖(保持向后兼容)。
/// </summary>
[System.Serializable]
public struct DialogueLine
{
public string speakerNameKey; // 本地化 key如 "NPC_Elder_Name"
[Tooltip("说话人角色推荐。actor 优先;留空时回退到 speakerNameKey / portraitSprite")]
public DialogueActorSO actor;
[Tooltip("说话人本地化 key留空时使用 actor.nameKey")]
public string speakerNameKey;
[TextArea(2, 5)]
public string textKey; // 本地化文本 key如 "DLG_Elder_001"
public Sprite portraitSprite; // 可选说话人头像
[Tooltip("说话人头像,留空时使用 actor.portrait")]
public Sprite portraitSprite;
public AudioClip voiceClip; // 可选语音
[Min(0.01f)]
public float typewriterDelay; // 每字符延迟0 = 使用默认 0.03f
/// <summary>
/// 获取最终使用的说话人名称 Keyactor 优先,回退到直接字段。
/// </summary>
public string ResolvedNameKey => actor != null && !string.IsNullOrEmpty(actor.nameKey)
? actor.nameKey : speakerNameKey;
/// <summary>
/// 获取最终使用的头像actor 优先,回退到直接字段。
/// </summary>
public Sprite ResolvedPortrait => actor != null && actor.portrait != null
? actor.portrait : portraitSprite;
}
/// <summary>
@@ -29,13 +50,66 @@ namespace BaseGames.Dialogue
public string sequenceId; // 全局唯一,如 "DLG_Elder_Quest_Available"
public DialogueLine[] lines;
/// <summary>条件变体:满足特定世界标志时替换整个序列。</summary>
/// <summary>
/// 条件变体:所有 requiredFlags 均满足时替换整个序列AND 关系)。
/// 与 NarrativeNPC.DialogueVersion 的多条件语义保持一致。
/// </summary>
[System.Serializable]
public struct ConditionalVariant
{
public string conditionFlag; // WorldState flag key
[Tooltip("全部满足时激活此变体AND 关系)。留空表示无条件。")]
[WorldStateFlag]
public string[] requiredFlags;
public DialogueSequenceSO sequence;
}
public ConditionalVariant[] variants;
#if UNITY_EDITOR
// sequenceId → 资产路径5 秒 TTL跨所有 DialogueSequenceSO.OnValidate 共用,
// 避免每次 Save 都重扫所有同类 SOO(1) 路径比对代替 O(n) 全量扫描)。
private static System.Collections.Generic.Dictionary<string, string> s_seqIdToPath;
private static double s_seqIdsCacheTime = -10.0;
private static System.Collections.Generic.Dictionary<string, string> GetSequenceIdCache()
{
double now = UnityEditor.EditorApplication.timeSinceStartup;
if (s_seqIdToPath != null && now - s_seqIdsCacheTime < 5.0)
return s_seqIdToPath;
s_seqIdToPath = new System.Collections.Generic.Dictionary<string, string>(System.StringComparer.Ordinal);
string[] guids = UnityEditor.AssetDatabase.FindAssets("t:DialogueSequenceSO");
foreach (var guid in guids)
{
var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
var seq = UnityEditor.AssetDatabase.LoadAssetAtPath<DialogueSequenceSO>(path);
if (seq != null && !string.IsNullOrEmpty(seq.sequenceId) && !s_seqIdToPath.ContainsKey(seq.sequenceId))
s_seqIdToPath[seq.sequenceId] = path;
}
s_seqIdsCacheTime = now;
return s_seqIdToPath;
}
private void OnValidate()
{
if (string.IsNullOrEmpty(sequenceId))
{
sequenceId = name;
UnityEditor.EditorUtility.SetDirty(this);
}
// 检测重复 sequenceId缓存路径 vs 自身路径比对O(1)5 秒内无需重扫。
var cache = GetSequenceIdCache();
string myPath = UnityEditor.AssetDatabase.GetAssetPath(this);
if (!string.IsNullOrEmpty(myPath) &&
cache.TryGetValue(sequenceId, out var existingPath) &&
existingPath != myPath)
{
Debug.LogError(
$"[DialogueSequenceSO] sequenceId '{sequenceId}' 与 " +
$"'{System.IO.Path.GetFileNameWithoutExtension(existingPath)}' 重复!请修改其中一个。", this);
s_seqIdsCacheTime = -10.0;
}
}
#endif
}
}