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
}
}