Files
ichni_Official/Assets/Scripts/NewStorySystem/Data/StoryBlockDefinition.cs

224 lines
9.2 KiB
C#
Raw Normal View History

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,
2026-07-20 16:56:04 -04:00
Completed,
Forbidden
2026-07-05 16:08:23 -04:00
}
/// <summary>
/// Block 的类型,决定点击后触发的行为。
/// </summary>
public enum StoryBlockType
{
Text,
Song,
Tutorial
}
2026-07-20 16:56:04 -04:00
/// <summary>
/// TextBlock 的叙事层级。层级只决定剧情树中的视觉样式和 checkpoint 候选资格,
/// 不会自行改变 Block 的解锁条件或 Yarn 执行逻辑。
/// </summary>
public enum TextBlockImportance
{
Important,
Secondary
}
2026-07-05 16:08:23 -04:00
/// <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")]
2026-07-20 16:56:04 -04:00
[InfoBox("留空表示无条件解锁(章节起始即可用)。")]
public StoryCondition unlockCondition = new StoryCondition();
/// <summary>
/// 用于禁用当前 Block 的可选条件。它与解锁条件共用同一种通用条件树,
/// 因而可直接使用剧情变量、Block 完成状态及 AND / OR / NOT 组合。
/// 条件满足时,该 Block 的状态固定为 Forbidden且优先于 Completed / Current / Locked。
/// </summary>
[FoldoutGroup("$blockId")]
[LabelText("Forbidden Condition")]
[InfoBox("可选。留空表示此 Block 不会被条件禁用;满足时显示为 Forbidden 且不可点击。")]
public StoryCondition forbiddenCondition = new StoryCondition();
2026-07-05 16:08:23 -04:00
// ── 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;
2026-07-20 16:56:04 -04:00
/// <summary>
/// TextBlock 的视觉和叙事层级。Important 用于主线转折与后续 checkpoint
/// Secondary 用于支线或补充剧情。默认 Important以保持现有 TextBlock 的呈现语义。
/// </summary>
[FoldoutGroup("$blockId")]
[ShowIf("blockType", StoryBlockType.Text)]
[LabelText("Text Importance")]
public TextBlockImportance textImportance = TextBlockImportance.Important;
2026-07-05 16:08:23 -04:00
// ── Song Block ──────────────────────────────────────────────────────────
[FoldoutGroup("$blockId")]
[ShowIf("blockType", StoryBlockType.Song)]
2026-07-20 16:56:04 -04:00
[LabelText("Song Name (Stable ID)")]
[InfoBox("对应 ChapterSelectionUnit 中 SongItemData.songName 的稳定歌曲标识。它用于查找歌曲资料与进入选曲,不能填写 displaySongName、翻译文本或作曲者名称。")]
2026-07-05 16:08:23 -04:00
public string songName;
2026-07-20 16:56:04 -04:00
/// <summary>
/// 插画在 SongBlock 的 Mask 中使用的实际本地 Y 坐标。SongBlock 的 Illustration 固定为 400 x 225
/// Mask 可视区域固定为当前 Prefab 的尺寸;在不露出上下空白的前提下,安全范围为 -74.25 到 74.25。
/// 该值是最终坐标而非偏移量0 表示居中,负值向下移动,正值向上移动。
/// </summary>
[FoldoutGroup("$blockId")]
[ShowIf("blockType", StoryBlockType.Song)]
[LabelText("Illustration Local Y")]
[Range(-74.25f, 74.25f)]
[InfoBox("用于微调 16:9 插画在 SongBlock Mask 中的纵向裁切。请保持 SongBlockUI 的 Illustration 尺寸为 400 x 225。")]
public float illustrationLocalY;
2026-07-05 16:08:23 -04:00
// ── Tutorial Block ──────────────────────────────────────────────────────
[FoldoutGroup("$blockId")]
[ShowIf("blockType", StoryBlockType.Tutorial)]
[LabelText("Tutorial Name")]
public string tutorialName;
/// <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>
2026-07-20 16:56:04 -04:00
/// 通用剧情条件容器。以一棵可组合的条件树(<see cref="StoryConditionNode"/>)表达,
/// 支持“与 / 或 / 非 + 叶子条件”的任意嵌套。
/// <para>该类型不限定用途,可用于 Block 的解锁、禁用、Helper 对话筛选等所有剧情判断。</para>
/// <para>根节点为空表示“未配置条件”,其是否视为通过由调用方根据业务语义决定;
/// 例如 unlockCondition 留空即允许,而 forbiddenCondition 留空即不禁用。</para>
2026-07-05 16:08:23 -04:00
/// </summary>
[InlineProperty]
[Serializable]
2026-07-20 16:56:04 -04:00
public class StoryCondition
2026-07-05 16:08:23 -04:00
{
[HideLabel]
[SerializeReference]
2026-07-20 16:56:04 -04:00
[InfoBox("留空表示未配置条件。可选择 All Of(AND) / Any Of(OR) / Not 复合节点进行任意嵌套。")]
2026-07-05 16:08:23 -04:00
public StoryConditionNode root;
/// <summary>
2026-07-20 16:56:04 -04:00
/// 当前容器是否已配置实际条件。
/// </summary>
public bool IsConfigured => root != null;
/// <summary>
/// 检查已配置条件是否满足。根节点为空时返回 false调用方应结合
/// <see cref="IsConfigured"/> 决定“未配置条件”的业务含义。
2026-07-05 16:08:23 -04:00
/// </summary>
/// <param name="getVariable">按变量名返回其整型值的委托。</param>
/// <param name="isBlockCompleted">按 blockId 判断该 block 是否已完成的委托。</param>
public bool IsSatisfied(Func<string, int> getVariable, Func<string, bool> isBlockCompleted)
{
2026-07-20 16:56:04 -04:00
return root != null && root.Evaluate(getVariable, isBlockCompleted);
2026-07-05 16:08:23 -04:00
}
}
}