using System; using System.Collections.Generic; using Cielonos.MainGame.Interactions; using Sirenix.OdinInspector; using SLSUtilities.General; using SLSUtilities.Narrative; using Cielonos.UI; using UnityEngine; namespace Cielonos.MainGame.Narrative { /// /// 全局剧情与对话路由器(单例)。 /// 所有触发源统一调用 StoryDirector.Instance.PlayStory(storyId) 来启动剧情。 /// public class StoryDirector : Singleton { #region NPC public static readonly Dictionary ActiveNpcs = new Dictionary(); #endregion #region Fields & Properties [Title("UI Pages")] [Required] [SerializeField] private DialogUIPage dialogUIPage; private readonly Dictionary _entryLookup = new Dictionary(); /// 当前是否有对话正在运行。 public bool IsDialogueActive => dialogUIPage != null && dialogUIPage.IsOpen; /// 当前正在播放的对话剧情的启动源 ID(通常是 NPC 的 storyId) public string ActiveStoryId { get; private set; } #endregion #region Events public event Action OnDialogueStarted; public event Action OnDialogueEnded; #endregion #region Unity Lifecycle protected override void Awake() { base.Awake(); } private void Start() { InitializeEntries(); // 订阅 DialogUIPage 关闭事件以分发 OnDialogueEnded if (dialogUIPage != null) { dialogUIPage.PageClosed += HandleDialogueClosed; } // 订阅通用剧情触发器事件,实现全局联动 NarrativeTrigger.OnNarrativeTriggerFired += PlayStory; // 订阅行内动作标记事件,解耦桥接 SLSUtilities.Narrative.UI.AdvancedLinePresenter.OnPlayAnimationRequested += HandlePlayAnimationRequested; SLSUtilities.Narrative.UI.AdvancedLinePresenter.OnStopAnimationRequested += HandleStopAnimationRequested; } private void OnDestroy() { if (dialogUIPage != null) { dialogUIPage.PageClosed -= HandleDialogueClosed; } // 注销通用剧情触发器事件,防止内存泄漏 NarrativeTrigger.OnNarrativeTriggerFired -= PlayStory; // 注销行内动作标记事件 SLSUtilities.Narrative.UI.AdvancedLinePresenter.OnPlayAnimationRequested -= HandlePlayAnimationRequested; SLSUtilities.Narrative.UI.AdvancedLinePresenter.OnStopAnimationRequested -= HandleStopAnimationRequested; } private void HandlePlayAnimationRequested(string animName, string npcName) { Cielonos.Narrative.CustomFunctions.PlayAnimation(animName, npcName); } private void HandleStopAnimationRequested(string npcName) { Cielonos.Narrative.CustomFunctions.StopAnimation(npcName); } #endregion #region Narrative Routing & Playback /// /// 从全局剧情数据库中读取已注册的路由表,构建快速查找字典。 /// [Button("重新加载路由注册表 (Reload Registry)", ButtonSizes.Small)] public void InitializeEntries() { _entryLookup.Clear(); if (StorySystem.Instance == null || StorySystem.Database == null) { Debug.LogWarning("[StoryDirector] StorySystem 实例或 Database 未初始化,跳过路由加载。"); return; } if (StorySystem.Database.narrativeEntries == null) { Debug.LogWarning("[StoryDirector] 剧情路由表 (narrativeEntries) 列表为空。"); return; } foreach (var entry in StorySystem.Database.narrativeEntries) { if (entry == null) continue; if (string.IsNullOrEmpty(entry.storyId)) { Debug.LogWarning($"[StoryDirector] 存在未配置 Story ID 的 NarrativeEntry: {entry.name}"); continue; } if (!_entryLookup.TryAdd(entry.storyId, entry)) { Debug.LogWarning($"[StoryDirector] 重复的 Story ID: '{entry.storyId}',已跳过资产: {entry.name}"); } } Debug.Log($"[StoryDirector] 成功初始化 {_entryLookup.Count} 个剧情路由入口。"); } /// /// 统一的对话启动入口。NPC、区域触发器、道具等交互源均通过此方法启动剧情。 /// /// 对应 NarrativeEntry 的 storyId 标识 public void PlayStory(string storyId) { if (string.IsNullOrEmpty(storyId)) { Debug.LogWarning("[StoryDirector] PlayStory 失败:传入的 storyId 为空。"); return; } if (IsDialogueActive) { Debug.LogWarning("[StoryDirector] 对话正在进行中,忽略新的 PlayStory 请求。"); return; } ActiveStoryId = storyId; // 记录启动源 ID if (!_entryLookup.TryGetValue(storyId, out NarrativeEntry entry)) { Debug.LogWarning($"[StoryDirector] 未找到 ID 为 '{storyId}' 的剧情路由表,尝试直接作为 Yarn 节点播放。"); PlayNode(storyId); return; } // 1. 评估路由匹配(首个满足条件的路由) string targetNode = string.Empty; bool routeMatched = false; foreach (var route in entry.routes) { if (route == null) continue; if (NarrativeConditionEvaluator.Evaluate(route.conditions)) { targetNode = route.targetNode; routeMatched = true; Debug.Log($"[StoryDirector] 剧情路由匹配成功!路由备注: '{route.editorNote}' -> 目标 Yarn 节点: '{targetNode}'"); break; } } // 2. 如果路由均不满足,则使用 Fallback 兜底 if (!routeMatched) { targetNode = entry.fallbackNode; Debug.Log($"[StoryDirector] 未匹配到任何满足的路由条件,使用 Fallback 兜底节点: '{targetNode}'"); } // 3. 校验并播放目标节点 if (string.IsNullOrEmpty(targetNode)) { Debug.LogWarning($"[StoryDirector] 剧情评估完毕,但无可播放的节点(未配置 Fallback且条件皆不满足)。StoryId: {storyId}"); return; } PlayNode(targetNode); } /// /// 绕过条件路由直接播放指定的 Yarn 节点名称(一般用于测试、特异流程或调试)。 /// public void PlayNode(string yarnNode) { if (dialogUIPage == null) { Debug.LogError("[StoryDirector] PlayNode 失败:DialogUIPage 引用未赋值!"); return; } if (IsDialogueActive) { Debug.LogWarning("[StoryDirector] 已经有对话正在进行中,忽略 PlayNode。"); return; } Debug.Log($"[StoryDirector] 开始播放剧情节点: '{yarnNode}'"); OnDialogueStarted?.Invoke(); // 打开 UI 页面并引导其启动特定节点 dialogUIPage.Open(yarnNode); } private void HandleDialogueClosed() { Debug.Log("[StoryDirector] 对话 UI 已关闭,结束当前剧情。"); ActiveStoryId = null; // 置空启动源 OnDialogueEnded?.Invoke(); } #endregion #region Editor Debugging #if UNITY_EDITOR [Title("调试与验证 (Debug & Test)", titleAlignment: TitleAlignments.Centered)] [BoxGroup("调试测试")] [HorizontalGroup("调试测试/Group")] [HideLabel] [Tooltip("输入要测试的 NarrativeEntry 的 storyId")] [InlineButton("TestPlayStory", "测试播放")] [SerializeField] private string testStoryId; [HorizontalGroup("调试测试/Group", Width = 120)] [GUIColor(0.4f, 0.9f, 0.4f)] public void TestPlayStory() { if (string.IsNullOrEmpty(testStoryId)) { Debug.LogWarning("[StoryDirector] 请在文本框中输入要测试的 storyId!"); return; } PlayStory(testStoryId); } #endif #endregion } }