2026-07-05 16:08:23 -04:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using Sirenix.OdinInspector;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Ichni.Story
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Block 在故事树中的状态:已锁定、当前可交互、已完成。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public enum StoryBlockState
|
|
|
|
|
|
{
|
|
|
|
|
|
Locked,
|
|
|
|
|
|
Current,
|
|
|
|
|
|
Completed
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Block 的类型,决定点击后触发的行为。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public enum StoryBlockType
|
|
|
|
|
|
{
|
|
|
|
|
|
Text,
|
|
|
|
|
|
Song,
|
|
|
|
|
|
Tutorial
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 变量条件的比较方式。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public enum VariableComparison
|
|
|
|
|
|
{
|
|
|
|
|
|
Equal,
|
|
|
|
|
|
NotEqual,
|
|
|
|
|
|
Greater,
|
|
|
|
|
|
GreaterOrEqual,
|
|
|
|
|
|
Less,
|
|
|
|
|
|
LessOrEqual
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 故事树中单个 block 的完整数据定义,由 <see cref="StoryData"/> 持有。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[InlineProperty]
|
|
|
|
|
|
[Serializable]
|
|
|
|
|
|
public class StoryBlockDefinition
|
|
|
|
|
|
{
|
|
|
|
|
|
[FoldoutGroup("$blockId", false)]
|
|
|
|
|
|
[LabelText("Block ID")]
|
|
|
|
|
|
public string blockId;
|
|
|
|
|
|
|
|
|
|
|
|
[FoldoutGroup("$blockId")]
|
|
|
|
|
|
[LabelText("Type")]
|
|
|
|
|
|
public StoryBlockType blockType;
|
|
|
|
|
|
|
|
|
|
|
|
[FoldoutGroup("$blockId")]
|
|
|
|
|
|
[LabelText("Grid Column")]
|
|
|
|
|
|
[Tooltip("故事树网格中的列:向右为正,(0,0) 位于容器左侧中部。可为负数与小数(用于微调)。")]
|
|
|
|
|
|
public float gridColumn;
|
|
|
|
|
|
|
|
|
|
|
|
[FoldoutGroup("$blockId")]
|
|
|
|
|
|
[LabelText("Grid Row")]
|
|
|
|
|
|
[Tooltip("故事树网格中的行:向下为正、向上为负,0 对应垂直中线。可为小数(用于微调)。")]
|
|
|
|
|
|
public float gridRow;
|
|
|
|
|
|
|
|
|
|
|
|
[FoldoutGroup("$blockId")]
|
|
|
|
|
|
[LabelText("Next Block IDs")]
|
|
|
|
|
|
public List<string> nextBlockIds = new List<string>();
|
|
|
|
|
|
|
|
|
|
|
|
[FoldoutGroup("$blockId")]
|
|
|
|
|
|
[LabelText("Unlock Condition")]
|
|
|
|
|
|
public UnlockCondition unlockCondition = new UnlockCondition();
|
|
|
|
|
|
|
|
|
|
|
|
// ── Text Block ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
[FoldoutGroup("$blockId")]
|
|
|
|
|
|
[ShowIf("blockType", StoryBlockType.Text)]
|
|
|
|
|
|
[LabelText("Yarn Node Name")]
|
|
|
|
|
|
public string yarnNodeName;
|
|
|
|
|
|
|
|
|
|
|
|
[FoldoutGroup("$blockId")]
|
|
|
|
|
|
[ShowIf("blockType", StoryBlockType.Text)]
|
|
|
|
|
|
[LabelText("Title Key (Localization)")]
|
|
|
|
|
|
public string titleKey;
|
|
|
|
|
|
|
|
|
|
|
|
// ── Song Block ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
[FoldoutGroup("$blockId")]
|
|
|
|
|
|
[ShowIf("blockType", StoryBlockType.Song)]
|
|
|
|
|
|
[LabelText("Song Name")]
|
|
|
|
|
|
public string songName;
|
|
|
|
|
|
|
|
|
|
|
|
// ── Tutorial Block ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
[FoldoutGroup("$blockId")]
|
|
|
|
|
|
[ShowIf("blockType", StoryBlockType.Tutorial)]
|
|
|
|
|
|
[LabelText("Tutorial Name")]
|
|
|
|
|
|
public string tutorialName;
|
|
|
|
|
|
|
2026-07-18 16:51:18 -04:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 教程曲目在 <see cref="Ichni.Menu.TutorialCollection"/> 中的稳定 Key。
|
|
|
|
|
|
/// 只用于运行时查找,不使用展示名称或章节名称作为逻辑匹配条件。
|
|
|
|
|
|
/// 命名统一使用小写英文、数字和下划线,例如 <c>chapter0_intro</c>。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[FoldoutGroup("$blockId")]
|
|
|
|
|
|
[ShowIf("blockType", StoryBlockType.Tutorial)]
|
|
|
|
|
|
[LabelText("Tutorial Key")]
|
|
|
|
|
|
public string tutorialKey;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 教程被玩家选择“游玩”或“跳过”后写入的全局剧情变量。
|
|
|
|
|
|
/// 后续 Block 应通过 <see cref="VariableCondition"/> 检查该变量是否等于 1 来解锁。
|
|
|
|
|
|
/// 命名统一使用小写英文、数字和下划线,例如
|
|
|
|
|
|
/// <c>story_tutorial_chapter0_intro_resolved</c>。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[FoldoutGroup("$blockId")]
|
|
|
|
|
|
[ShowIf("blockType", StoryBlockType.Tutorial)]
|
|
|
|
|
|
[LabelText("Progress Variable")]
|
|
|
|
|
|
public string tutorialProgressVariable;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 要启动的教程难度的稳定存档 ID,对应 <see cref="Ichni.Menu.DifficultyData.saveDifficultyId"/>。
|
|
|
|
|
|
/// 不使用难度列表下标,避免调整难度排列后教程进入错误谱面。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[FoldoutGroup("$blockId")]
|
|
|
|
|
|
[ShowIf("blockType", StoryBlockType.Tutorial)]
|
|
|
|
|
|
[LabelText("Tutorial Difficulty Save ID")]
|
|
|
|
|
|
public int tutorialDifficultySaveId = -1;
|
|
|
|
|
|
|
2026-07-05 16:08:23 -04:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 预览 / 调试用的显示标题:按类型取对应字段,缺省时回退到 blockId。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public string GetDisplayTitle()
|
|
|
|
|
|
{
|
|
|
|
|
|
string title = blockType switch
|
|
|
|
|
|
{
|
|
|
|
|
|
StoryBlockType.Text => titleKey,
|
|
|
|
|
|
StoryBlockType.Song => songName,
|
|
|
|
|
|
StoryBlockType.Tutorial => tutorialName,
|
|
|
|
|
|
_ => null
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return string.IsNullOrEmpty(title) ? blockId : title;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Block 的解锁条件。以一棵可组合的条件树(<see cref="StoryConditionNode"/>)表达,
|
|
|
|
|
|
/// 支持"与 / 或 / 非 + 叶子条件"的任意嵌套。根节点为空时视为无条件解锁(始终满足)。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[InlineProperty]
|
|
|
|
|
|
[Serializable]
|
|
|
|
|
|
public class UnlockCondition
|
|
|
|
|
|
{
|
|
|
|
|
|
[HideLabel]
|
|
|
|
|
|
[SerializeReference]
|
|
|
|
|
|
[InfoBox("留空表示无条件解锁(章节起始即可用)。可选择 All Of(AND) / Any Of(OR) / Not 复合节点进行任意嵌套。")]
|
|
|
|
|
|
public StoryConditionNode root;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 检查解锁条件是否满足。根节点为空视为满足。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="getVariable">按变量名返回其整型值的委托。</param>
|
|
|
|
|
|
/// <param name="isBlockCompleted">按 blockId 判断该 block 是否已完成的委托。</param>
|
|
|
|
|
|
public bool IsSatisfied(Func<string, int> getVariable, Func<string, bool> isBlockCompleted)
|
|
|
|
|
|
{
|
|
|
|
|
|
return root == null || root.Evaluate(getVariable, isBlockCompleted);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|