168 lines
8.1 KiB
C#
168 lines
8.1 KiB
C#
using Ichni.Story.Dialogue;
|
||
using Ichni.Story.UI;
|
||
using Ichni.Menu;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using Yarn.Unity;
|
||
|
||
namespace Ichni.Story.YarnFunctions
|
||
{
|
||
/// <summary>
|
||
/// 包含与故事树节点控制、全局进程、存档交互相关的 Yarn 自定义命令注册器。
|
||
/// </summary>
|
||
public static class StoryTreeCommands
|
||
{
|
||
// ── Yarn Commands ────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 授予歌曲相关的解锁 Key,并在当前对话结束后弹出提示。
|
||
/// 格式: <<unlock_song unlock_key "song_id">>
|
||
/// <para>Key 必须使用小写字母、数字和下划线,例如 <c>story_ch0_prologue_completed</c>。</para>
|
||
/// <para><c>song_id</c> 必须是 <see cref="SongItemData.songName"/> 的稳定标识,而不是显示名称或本地化文本。
|
||
/// 该参数用于解析玩家可见的歌曲名称,绝不能将技术 Unlock Key 显示给玩家。</para>
|
||
/// 新内容若不需要歌曲提示,优先使用 <see cref="GrantUnlock"/>。
|
||
/// </summary>
|
||
/// <param name="songUnlockKey">要授予的稳定解锁 Key,而不是歌曲显示名称。</param>
|
||
/// <param name="songId">对应 <see cref="SongItemData.songName"/> 的稳定歌曲 ID。</param>
|
||
[YarnCommand("unlock_song")]
|
||
public static void UnlockSong(string songUnlockKey, string songId)
|
||
{
|
||
ExecuteAfterDialogueCommit(() =>
|
||
{
|
||
if (!TryGrantUnlockKey(songUnlockKey, out bool wasNewlyGranted) || !wasNewlyGranted)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (MessageUIPage.instance != null)
|
||
MessageUIPage.instance.ShowUnlockMessage(ResolveSongDisplayName(songId));
|
||
else
|
||
Debug.LogError("[StoryTreeCommands] 弹窗失败:场景中未找到 StoryMessageBoxUIPage 实例!");
|
||
|
||
// 解锁 Key 已写入 UnlockSaveModule,但当前 StoryPage 的 SongBlock 已经实例化,
|
||
// 不会自动重新读取授权状态。立刻刷新可让遮罩、按钮交互和后续 Block 状态与新存档同步。
|
||
StoryManager.instance?.treeController?.RefreshAllStates();
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 通用内容解锁命令,可用于章节、歌曲、教程及未来内容。
|
||
/// 格式: <<grant_unlock unlock_key>>。
|
||
/// 本命令只授予并保存 Key,不绑定任何 UI 提示;需要提示时应由 Yarn 额外调用 <c>show_message</c>。
|
||
/// </summary>
|
||
/// <param name="unlockKey">符合 <see cref="UnlockSaveModule.KeyNamingRule"/> 的稳定解锁 Key。</param>
|
||
[YarnCommand("grant_unlock")]
|
||
public static void GrantUnlock(string unlockKey)
|
||
{
|
||
// 全局解锁不属于 ChapterStorySave,Timeline 回滚永远不会撤销已提交的 Key;
|
||
// 但在当前对话尚未正常结束前,也不能因 Back 而提前写入。
|
||
ExecuteAfterDialogueCommit(() => TryGrantUnlockKey(unlockKey, out _));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在对话结束后,弹出一个自定义 MessageBox 提示。
|
||
/// 文本 Key 会交给 MessageUIPage 的 FIFO 队列,并在该弹窗真正显示前才按当前 Locale 解析。
|
||
/// 格式: <<show_message titleKey contentKey [tableName]>>
|
||
/// </summary>
|
||
/// <param name="titleKey">标题文本或本地化键</param>
|
||
/// <param name="contentKey">正文文本或本地化键</param>
|
||
/// <param name="tableName">Unity Localization 中的 String Table 名称</param>
|
||
[YarnCommand("show_message")]
|
||
public static void ShowMessage(string titleKey, string contentKey, string tableName = "Message")
|
||
{
|
||
Debug.Log($"[StoryTreeCommands] 准备在对话结束后显示本地化弹窗:Table='{tableName}',TitleKey='{titleKey}',ContentKey='{contentKey}'。");
|
||
ExecuteAfterDialogueCommit(() =>
|
||
{
|
||
if (MessageUIPage.instance != null)
|
||
MessageUIPage.instance.ShowLocalizedMessage(
|
||
new LocalizedPopupText(tableName, titleKey),
|
||
new LocalizedPopupText(tableName, contentKey));
|
||
else
|
||
Debug.LogError("[StoryTreeCommands] 无法显示自定义弹窗:未找到 MessageUIPage 实例。");
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在 TextBlock 事务活跃时延后执行全局副作用;无活动事务时立即执行。
|
||
/// 这样 Yarn 命令可以在对话和未来的非对话入口共用,同时保持 Back/失败时不落盘的事务边界。
|
||
/// </summary>
|
||
private static void ExecuteAfterDialogueCommit(System.Action action)
|
||
{
|
||
if (action == null)
|
||
return;
|
||
|
||
StoryDialogueController controller = StoryDialogueController.instance;
|
||
if (controller != null)
|
||
{
|
||
controller.ExecuteAfterDialogueCommit(action.Invoke);
|
||
return;
|
||
}
|
||
|
||
action.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 所有 Yarn 解锁命令共用的授予入口。它负责模块存在性检查、重复授予的幂等处理及立即 ES3 保存。
|
||
/// </summary>
|
||
/// <returns>模块已正常处理该 Key 时返回 true;Key 非法或模块尚未初始化时返回 false。</returns>
|
||
private static bool TryGrantUnlockKey(string unlockKey, out bool wasNewlyGranted)
|
||
{
|
||
wasNewlyGranted = false;
|
||
UnlockSaveModule unlockSaveModule = GameSaveManager.instance?.UnlockSaveModule;
|
||
if (unlockSaveModule == null)
|
||
{
|
||
Debug.LogError("[StoryTreeCommands] 无法授予解锁 Key:UnlockSaveModule 尚未初始化!");
|
||
return false;
|
||
}
|
||
|
||
if (unlockSaveModule.HasKey(unlockKey))
|
||
{
|
||
Debug.Log($"[StoryTreeCommands] 解锁 Key 已存在:{unlockKey}");
|
||
return true;
|
||
}
|
||
|
||
wasNewlyGranted = unlockSaveModule.GrantKey(unlockKey);
|
||
if (wasNewlyGranted)
|
||
{
|
||
Debug.Log($"[StoryTreeCommands] 已成功写入并保存解锁 Key:{unlockKey}");
|
||
}
|
||
|
||
return wasNewlyGranted;
|
||
}
|
||
|
||
// ── 内部辅助 ─────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 以稳定 Song ID 查找当前项目中配置的歌曲显示名。当前阶段使用 <see cref="SongItemData.displaySongName"/>;
|
||
/// 将来歌曲标题接入 Chapter Content 本地化后,只需替换本函数内部的返回逻辑,Yarn 与弹窗 API 无需修改。
|
||
/// </summary>
|
||
private static string ResolveSongDisplayName(string songId)
|
||
{
|
||
List<ChapterSelectionUnit> chapters = ChapterSelectionManager.instance?.chapters;
|
||
if (chapters != null)
|
||
{
|
||
foreach (ChapterSelectionUnit chapter in chapters)
|
||
{
|
||
if (chapter?.songs == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
foreach (SongItemData song in chapter.songs)
|
||
{
|
||
if (song != null && song.songName == songId)
|
||
{
|
||
return string.IsNullOrWhiteSpace(song.displaySongName)
|
||
? song.songName
|
||
: song.displaySongName;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Debug.LogWarning($"[StoryTreeCommands] 无法为歌曲解锁提示解析 Song ID '{songId}',将显示该 ID。", ChapterSelectionManager.instance);
|
||
return songId ?? string.Empty;
|
||
}
|
||
}
|
||
}
|