using UnityEngine;
namespace BaseGames.Dialogue
{
///
/// 对话行结构(架构 14_NarrativeModule §3)。
/// 每行包含说话人、文本(本地化 Key)和可选的语音片段。
///
/// 推荐通过 actor 引用 DialogueActorSO 统一管理头像/名称;
/// speakerNameKey / portraitSprite 作为无 SO 时的直接覆盖(保持向后兼容)。
///
[System.Serializable]
public struct DialogueLine
{
[Tooltip("说话人角色(推荐)。actor 优先;留空时回退到 speakerNameKey / portraitSprite")]
public DialogueActorSO actor;
[Tooltip("说话人本地化 key,留空时使用 actor.nameKey")]
public string speakerNameKey;
[TextArea(2, 5)]
public string textKey; // 本地化文本 key(如 "DLG_Elder_001")
[Tooltip("说话人头像,留空时使用 actor.portrait")]
public Sprite portraitSprite;
public AudioClip voiceClip; // 可选语音
[Min(0.01f)]
public float typewriterDelay; // 每字符延迟(秒,0 = 使用默认 0.03f)
///
/// 获取最终使用的说话人名称 Key:actor 优先,回退到直接字段。
///
public string ResolvedNameKey => actor != null && !string.IsNullOrEmpty(actor.nameKey)
? actor.nameKey : speakerNameKey;
///
/// 获取最终使用的头像:actor 优先,回退到直接字段。
///
public Sprite ResolvedPortrait => actor != null && actor.portrait != null
? actor.portrait : portraitSprite;
}
///
/// 对话序列 SO(架构 14_NarrativeModule §3)。
/// 一个 NPC 对话场合对应一个序列,由若干 DialogueLine 组成。
/// 资产路径: Assets/ScriptableObjects/Dialogue/DLG_{NpcId}_{Context}.asset
///
[CreateAssetMenu(menuName = "BaseGames/Dialogue/DialogueSequence")]
public class DialogueSequenceSO : ScriptableObject
{
public string sequenceId; // 全局唯一,如 "DLG_Elder_Quest_Available"
public DialogueLine[] lines;
///
/// 条件变体:所有 requiredFlags 均满足时替换整个序列(AND 关系)。
/// 与 NarrativeNPC.DialogueVersion 的多条件语义保持一致。
///
[System.Serializable]
public struct ConditionalVariant
{
[Tooltip("全部满足时激活此变体(AND 关系)。留空表示无条件。")]
[WorldStateFlag]
public string[] requiredFlags;
public DialogueSequenceSO sequence;
}
public ConditionalVariant[] variants;
#if UNITY_EDITOR
// sequenceId → 资产路径,5 秒 TTL,跨所有 DialogueSequenceSO.OnValidate 共用,
// 避免每次 Save 都重扫所有同类 SO(O(1) 路径比对代替 O(n) 全量扫描)。
private static System.Collections.Generic.Dictionary s_seqIdToPath;
private static double s_seqIdsCacheTime = -10.0;
private static System.Collections.Generic.Dictionary 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(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(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
}
}