Files
ichni_Official/Assets/Scripts/NewStorySystem/Data/StoryBlockDefinition.cs
SoulliesOfficial c30bb258b1 Story排版
2026-07-20 16:56:04 -04:00

224 lines
9.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
namespace Ichni.Story
{
/// <summary>
/// Block 在故事树中的状态:已锁定、当前可交互、已完成。
/// </summary>
public enum StoryBlockState
{
Locked,
Current,
Completed,
Forbidden
}
/// <summary>
/// Block 的类型,决定点击后触发的行为。
/// </summary>
public enum StoryBlockType
{
Text,
Song,
Tutorial
}
/// <summary>
/// TextBlock 的叙事层级。层级只决定剧情树中的视觉样式和 checkpoint 候选资格,
/// 不会自行改变 Block 的解锁条件或 Yarn 执行逻辑。
/// </summary>
public enum TextBlockImportance
{
Important,
Secondary
}
/// <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")]
[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();
// ── 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;
/// <summary>
/// TextBlock 的视觉和叙事层级。Important 用于主线转折与后续 checkpoint
/// Secondary 用于支线或补充剧情。默认 Important以保持现有 TextBlock 的呈现语义。
/// </summary>
[FoldoutGroup("$blockId")]
[ShowIf("blockType", StoryBlockType.Text)]
[LabelText("Text Importance")]
public TextBlockImportance textImportance = TextBlockImportance.Important;
// ── Song Block ──────────────────────────────────────────────────────────
[FoldoutGroup("$blockId")]
[ShowIf("blockType", StoryBlockType.Song)]
[LabelText("Song Name (Stable ID)")]
[InfoBox("对应 ChapterSelectionUnit 中 SongItemData.songName 的稳定歌曲标识。它用于查找歌曲资料与进入选曲,不能填写 displaySongName、翻译文本或作曲者名称。")]
public string songName;
/// <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;
// ── 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;
/// <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>
/// 通用剧情条件容器。以一棵可组合的条件树(<see cref="StoryConditionNode"/>)表达,
/// 支持“与 / 或 / 非 + 叶子条件”的任意嵌套。
/// <para>该类型不限定用途,可用于 Block 的解锁、禁用、Helper 对话筛选等所有剧情判断。</para>
/// <para>根节点为空表示“未配置条件”,其是否视为通过由调用方根据业务语义决定;
/// 例如 unlockCondition 留空即允许,而 forbiddenCondition 留空即不禁用。</para>
/// </summary>
[InlineProperty]
[Serializable]
public class StoryCondition
{
[HideLabel]
[SerializeReference]
[InfoBox("留空表示未配置条件。可选择 All Of(AND) / Any Of(OR) / Not 复合节点进行任意嵌套。")]
public StoryConditionNode root;
/// <summary>
/// 当前容器是否已配置实际条件。
/// </summary>
public bool IsConfigured => root != null;
/// <summary>
/// 检查已配置条件是否满足。根节点为空时返回 false调用方应结合
/// <see cref="IsConfigured"/> 决定“未配置条件”的业务含义。
/// </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);
}
}
}