208 lines
7.4 KiB
C#
208 lines
7.4 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using Sirenix.OdinInspector;
|
||
using SLSUtilities.General;
|
||
using SLSUtilities.Narrative;
|
||
using Cielonos.UI;
|
||
using UnityEngine;
|
||
|
||
namespace Cielonos.MainGame.Narrative
|
||
{
|
||
/// <summary>
|
||
/// 全局剧情与对话路由器(单例)。
|
||
/// 所有触发源统一调用 StoryDirector.Instance.PlayStory(storyId) 来启动剧情。
|
||
/// </summary>
|
||
public class StoryDirector : Singleton<StoryDirector>
|
||
{
|
||
[Title("UI Pages")]
|
||
[Required]
|
||
[SerializeField] private DialogUIPage dialogUIPage;
|
||
|
||
private readonly Dictionary<string, NarrativeEntry> _entryLookup = new Dictionary<string, NarrativeEntry>();
|
||
|
||
// ── 事件 ──
|
||
public event Action OnDialogueStarted;
|
||
public event Action OnDialogueEnded;
|
||
|
||
/// <summary>当前是否有对话正在运行。</summary>
|
||
public bool IsDialogueActive => dialogUIPage != null && dialogUIPage.IsOpen;
|
||
|
||
protected override void Awake()
|
||
{
|
||
base.Awake();
|
||
}
|
||
|
||
private void Start()
|
||
{
|
||
InitializeEntries();
|
||
|
||
// 订阅 DialogUIPage 关闭事件以分发 OnDialogueEnded
|
||
if (dialogUIPage != null)
|
||
{
|
||
dialogUIPage.PageClosed += HandleDialogueClosed;
|
||
}
|
||
|
||
// 订阅通用剧情触发器事件,实现全局联动
|
||
NarrativeTrigger.OnNarrativeTriggerFired += PlayStory;
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
if (dialogUIPage != null)
|
||
{
|
||
dialogUIPage.PageClosed -= HandleDialogueClosed;
|
||
}
|
||
|
||
// 注销通用剧情触发器事件,防止内存泄漏
|
||
NarrativeTrigger.OnNarrativeTriggerFired -= PlayStory;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从全局剧情数据库中读取已注册的路由表,构建快速查找字典。
|
||
/// </summary>
|
||
[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} 个剧情路由入口。");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 统一的对话启动入口。NPC、区域触发器、道具等交互源均通过此方法启动剧情。
|
||
/// </summary>
|
||
/// <param name="storyId">对应 NarrativeEntry 的 storyId 标识</param>
|
||
public void PlayStory(string storyId)
|
||
{
|
||
if (string.IsNullOrEmpty(storyId))
|
||
{
|
||
Debug.LogWarning("[StoryDirector] PlayStory 失败:传入的 storyId 为空。");
|
||
return;
|
||
}
|
||
|
||
if (IsDialogueActive)
|
||
{
|
||
Debug.LogWarning("[StoryDirector] 对话正在进行中,忽略新的 PlayStory 请求。");
|
||
return;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绕过条件路由直接播放指定的 Yarn 节点名称(一般用于测试、特异流程或调试)。
|
||
/// </summary>
|
||
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 已关闭,结束当前剧情。");
|
||
OnDialogueEnded?.Invoke();
|
||
}
|
||
|
||
#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
|
||
}
|
||
}
|