2026-07-18 16:51:18 -04:00
|
|
|
|
using Ichni.Menu;
|
|
|
|
|
|
using Ichni.Menu.UI;
|
2026-07-19 16:44:58 -04:00
|
|
|
|
using Ichni.Story.UI;
|
2026-07-18 16:51:18 -04:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Ichni.Story
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// TutorialBlock 的菜单层流程协调器。
|
|
|
|
|
|
/// 负责验证配置、生成通用 SelectionBox 选项、写入剧情进度,并在“游玩”时发起 GameScene 运行。
|
|
|
|
|
|
/// 不保存“教程成绩”或“教程是否通关”;本阶段规则是玩家确认游玩或跳过即视为已处理。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static class TutorialFlowController
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
2026-07-19 16:44:58 -04:00
|
|
|
|
/// 由 <see cref="UI.TutorialBlockView"/> 点击时调用。配置无效或 MessageUIPage 未配置时只报错,不修改剧情进度。
|
2026-07-18 16:51:18 -04:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static void RequestTutorial(StoryBlockDefinition tutorialBlock)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!TryResolveTutorialRunData(tutorialBlock, out ChapterSelectionUnit chapter,
|
|
|
|
|
|
out SongItemData song, out DifficultyData difficulty))
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 16:44:58 -04:00
|
|
|
|
MessageUIPage messageUIPage = MessageUIPage.instance;
|
|
|
|
|
|
if (messageUIPage == null)
|
2026-07-18 16:51:18 -04:00
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
Debug.LogWarning("[TutorialFlowController] MessageUIPage 未就绪,教程不会被解锁。");
|
2026-07-18 16:51:18 -04:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TutorialFlowController 只描述“有哪些选择、各自触发什么效果”;
|
|
|
|
|
|
// 具体按钮由通用 SelectionBox 在运行时生成,后续其它系统可复用同一 UI。
|
2026-07-19 16:44:58 -04:00
|
|
|
|
messageUIPage.ShowSelectionBox(
|
2026-07-18 16:51:18 -04:00
|
|
|
|
tutorialBlock.tutorialName,
|
|
|
|
|
|
string.Empty,
|
|
|
|
|
|
new List<SelectionBoxOption>
|
|
|
|
|
|
{
|
|
|
|
|
|
new("游玩教程", () => PlayTutorial(tutorialBlock, chapter, song, difficulty)),
|
|
|
|
|
|
new("跳过教程", () => SkipTutorial(tutorialBlock))
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void PlayTutorial(StoryBlockDefinition tutorialBlock, ChapterSelectionUnit chapter,
|
|
|
|
|
|
SongItemData song, DifficultyData difficulty)
|
|
|
|
|
|
{
|
|
|
|
|
|
MenuManager menuManager = MenuManager.instance;
|
|
|
|
|
|
if (menuManager == null || menuManager.isEnteringGame)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning("[TutorialFlowController] 菜单正在切换,忽略教程游玩请求。");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 先验证能否启动 GameScene,再写入“已处理”进度;避免无效配置导致错误解锁。
|
|
|
|
|
|
if (!CanBeginTutorialGame(chapter, song, difficulty))
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
if (!StoryProgress.ResolveTutorial(tutorialBlock))
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
// 规则:玩家确认“游玩教程”的瞬间即解锁后续剧情,不等待教程结算。
|
|
|
|
|
|
menuManager.TryBeginGame(chapter, song, difficulty, MenuReturnDestination.Story,
|
|
|
|
|
|
menuManager.storyUIPage);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void SkipTutorial(StoryBlockDefinition tutorialBlock)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 规则:玩家确认“跳过教程”的瞬间即解锁后续剧情,并保留在故事树。
|
|
|
|
|
|
StoryProgress.ResolveTutorial(tutorialBlock);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool CanBeginTutorialGame(ChapterSelectionUnit chapter, SongItemData song, DifficultyData difficulty)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (MenuManager.instance == null || InformationTransistor.instance == null ||
|
|
|
|
|
|
!MenuManager.instance.CanBeginGame)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning("[TutorialFlowController] MenuManager、InformationTransistor 或 GameScene 预加载未就绪,无法进入教程。");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (chapter == null || song == null || difficulty == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning("[TutorialFlowController] 教程缺少章节、歌曲或难度配置,无法进入教程。");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 解析 TutorialBlock 的 Key、教程曲目与稳定难度 ID。
|
|
|
|
|
|
/// 任何一项缺失都会失败且不写入剧情变量,防止错误配置意外解锁后续内容。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static bool TryResolveTutorialRunData(StoryBlockDefinition tutorialBlock,
|
|
|
|
|
|
out ChapterSelectionUnit chapter, out SongItemData song, out DifficultyData difficulty)
|
|
|
|
|
|
{
|
|
|
|
|
|
chapter = ChapterSelectionManager.instance?.currentChapter;
|
|
|
|
|
|
song = null;
|
|
|
|
|
|
difficulty = null;
|
|
|
|
|
|
|
|
|
|
|
|
if (tutorialBlock == null || tutorialBlock.blockType != StoryBlockType.Tutorial ||
|
|
|
|
|
|
string.IsNullOrWhiteSpace(tutorialBlock.tutorialKey) ||
|
|
|
|
|
|
string.IsNullOrWhiteSpace(tutorialBlock.tutorialProgressVariable) ||
|
|
|
|
|
|
tutorialBlock.tutorialDifficultySaveId < 0)
|
|
|
|
|
|
{
|
2026-07-19 16:44:58 -04:00
|
|
|
|
Debug.LogWarning($"[TutorialFlowController] TutorialBlock '{tutorialBlock?.blockId}' 的 Tutorial Key、Progress Variable 或 Difficulty Save ID 未配置完整。\n" +
|
|
|
|
|
|
$"Tutorial Key:{tutorialBlock.tutorialKey}, Progress Variable:{tutorialBlock.tutorialProgressVariable}, Difficulty Save ID:{tutorialBlock.tutorialDifficultySaveId}");
|
2026-07-18 16:51:18 -04:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TutorialCollection collection = ChapterSelectionManager.instance?.tutorialCollection;
|
|
|
|
|
|
if (chapter == null || collection == null || !collection.TryGetTutorialSong(tutorialBlock.tutorialKey, out song))
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning($"[TutorialFlowController] TutorialBlock '{tutorialBlock.blockId}' 无法解析章节或教程曲目。");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (song.difficultyDataList == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning($"[TutorialFlowController] 教程 '{tutorialBlock.tutorialKey}' 没有难度列表。");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
foreach (DifficultyData candidate in song.difficultyDataList)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (candidate != null && candidate.isAvailable &&
|
|
|
|
|
|
candidate.saveDifficultyId == tutorialBlock.tutorialDifficultySaveId)
|
|
|
|
|
|
{
|
|
|
|
|
|
difficulty = candidate;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Debug.LogWarning($"[TutorialFlowController] 教程 '{tutorialBlock.tutorialKey}' 中不存在可用的 Difficulty Save ID '{tutorialBlock.tutorialDifficultySaveId}'。");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|