using System.Collections.Generic; using System.Reflection; using Ichni.Story.UI; using UnityEngine; using UnityEngine.Events; using Yarn.Unity; using Yarn.Unity.UnityLocalization; namespace Ichni.Story.Dialogue { /// /// 故事树 block 与 Yarn 之间的桥接控制器。 /// 点击文本块 → 启动对应 Yarn 节点;对话结束 → 持久化剧情变量、标记 block 完成、 /// 重算解锁并存档、依次执行结束回调(如歌曲解锁提示,阶段 4 使用)。 /// public class StoryDialogueController : MonoBehaviour { public static StoryDialogueController instance; [Header("References")] [Tooltip("驱动 Yarn 对话的 DialogueRunner。")] public DialogueRunner dialogueRunner; [Tooltip("故事树页面:对话进行时淡出、结束后淡入(可选)。")] public StoryUIPage storyUIPage; /// /// 对话结束后依次执行并清空的回调队列。供 unlock_song 等 Yarn 命令(阶段 4) /// 把"解锁提示"排队到对话结束后弹出。 /// [System.NonSerialized] public List dialogueEndActions = new List(); // 当前正在播放对话的 block id;对话结束时据此标记完成 private string _activeBlockId; private void Awake() { instance = this; EnsureUnityLocalisedLineProvider(); } private void OnEnable() { if (dialogueRunner != null && dialogueRunner.onDialogueComplete != null) dialogueRunner.onDialogueComplete.AddListener(HandleDialogueComplete); } private void OnDisable() { if (dialogueRunner != null && dialogueRunner.onDialogueComplete != null) dialogueRunner.onDialogueComplete.RemoveListener(HandleDialogueComplete); } /// /// 播放某文本块对应的 Yarn 节点。若对话已在进行、引用缺失或节点名为空则忽略。 /// public void PlayBlock(TextBlockView block) { if (block == null) return; if (dialogueRunner == null) { Debug.LogError("[StoryDialogueController] 未配置 DialogueRunner,无法开始对话。"); return; } if (dialogueRunner.IsDialogueRunning) { Debug.LogWarning("[StoryDialogueController] 已有对话在进行中,忽略本次点击。"); return; } if (string.IsNullOrEmpty(block.YarnNodeName)) { Debug.LogWarning($"[StoryDialogueController] block '{block.blockId}' 未配置 yarnNodeName,无法开始对话。"); return; } // 将 DialogueRunner 的 YarnProject 对齐到当前章节(支持多章节共用一个 Runner) EnsureProjectForCurrentChapter(); _activeBlockId = block.blockId; if (storyUIPage != null) storyUIPage.FadeOut(); // StartDialogue 返回 YarnTask;此处即发即忘,完成由 onDialogueComplete 处理 dialogueRunner.StartDialogue(block.YarnNodeName).Forget(); } // 将 DialogueRunner 的 YarnProject 切换为当前已构建章节的 StoryData.yarnProject(若不同且未在运行) private void EnsureProjectForCurrentChapter() { StoryTreeController tree = StoryManager.instance != null ? StoryManager.instance.treeController : null; StoryData data = tree != null ? tree.ActiveStoryData : null; if (data == null || data.yarnProject == null) return; if (dialogueRunner.YarnProject != data.yarnProject && !dialogueRunner.IsDialogueRunning) dialogueRunner.SetProject(data.yarnProject); } /// /// DialogueRunner.lineProvider 为 [SerializeReference] internal 字段。 /// Inspector 的 "Use Unity Localised Line Provider" 按钮有时无法将 MonoBehaviour /// 引用正确持久化到该字段;此方法在运行时用反射补救,确保使用正确的 Provider。 /// private void EnsureUnityLocalisedLineProvider() { if (dialogueRunner == null) return; FieldInfo field = typeof(DialogueRunner).GetField( "lineProvider", BindingFlags.NonPublic | BindingFlags.Instance); if (field == null) return; // 已经是 UnityLocalisedLineProvider 则无需处理 if (field.GetValue(dialogueRunner) is UnityLocalisedLineProvider) return; UnityLocalisedLineProvider provider = dialogueRunner.GetComponent(); if (provider != null) { field.SetValue(dialogueRunner, provider); Debug.Log("[StoryDialogueController] Assigned UnityLocalisedLineProvider to DialogueRunner.lineProvider."); } else { Debug.LogWarning("[StoryDialogueController] UnityLocalisedLineProvider not found on DialogueRunner's GameObject. Localized line text will be unavailable."); } } private void HandleDialogueComplete() { // 持久化对话过程中写入的全局剧情变量 if (GameSaveManager.instance != null && GameSaveManager.instance.StorySaveModule != null) GameSaveManager.instance.StorySaveModule.SaveVariables(); // 标记 block 完成 → 重算解锁 → 存档(OnBlockCompleted 内部已去重并保存章节进度) if (!string.IsNullOrEmpty(_activeBlockId) && StoryManager.instance != null && StoryManager.instance.treeController != null) { StoryManager.instance.treeController.OnBlockCompleted(_activeBlockId); } _activeBlockId = null; // 依次执行并清空结束回调(如歌曲解锁提示) if (dialogueEndActions.Count > 0) { List pending = new List(dialogueEndActions); dialogueEndActions.Clear(); foreach (UnityAction action in pending) action?.Invoke(); } if (storyUIPage != null) storyUIPage.FadeIn(); } } }