剧情+对话完善

This commit is contained in:
SoulliesOfficial
2026-07-21 15:24:42 -04:00
parent 8f230831e9
commit 810d019619
161 changed files with 7271 additions and 1893 deletions

View File

@@ -26,6 +26,25 @@ namespace Ichni.Story
Tutorial
}
/// <summary>
/// 一个 Block 如何自动处理指向它的直接连接线。
/// <para>连接线表达基础流程关系,因此默认要求所有直接前置 Block 都已完成;该结果会与
/// <see cref="StoryBlockDefinition.unlockCondition"/> 按 AND 合并。</para>
/// <para>路线汇合时应使用 <see cref="RequireAnyDirectPredecessor"/>;仅用于视觉连线、
/// 不应影响可用性的连接则使用 <see cref="IgnoreConnections"/>。</para>
/// </summary>
public enum ConnectedBlockUnlockRule
{
/// <summary>默认值:全部直接前置 Block 都必须完成。</summary>
RequireAllDirectPredecessors = 0,
/// <summary>至少一个直接前置 Block 完成即可,适用于互斥分支的汇合节点。</summary>
RequireAnyDirectPredecessor = 1,
/// <summary>忽略连接线,只按手写 Unlock Condition 判断。</summary>
IgnoreConnections = 2
}
/// <summary>
/// TextBlock 的叙事层级。层级只决定剧情树中的视觉样式和 checkpoint 候选资格,
/// 不会自行改变 Block 的解锁条件或 Yarn 执行逻辑。
@@ -53,34 +72,59 @@ namespace Ichni.Story
/// 故事树中单个 block 的完整数据定义,由 <see cref="StoryData"/> 持有。
/// </summary>
[InlineProperty]
[HideReferenceObjectPicker]
[Serializable]
public class StoryBlockDefinition
{
[FoldoutGroup("$blockId", false)]
[LabelText("Block ID")]
// 仅用于 Odin Inspector 的动态折叠标题,不参与序列化或运行时状态推导。
// 在列表中即可识别 Block 的身份、类型与布局位置,减少频繁展开确认的需要。
private string EditorHeader => string.IsNullOrWhiteSpace(blockId)
? "New Block"
: $"{blockId} · {blockType} · C{gridColumn:0.##} / R{gridRow:0.##}";
[FoldoutGroup("$EditorHeader", false)]
[HorizontalGroup("$EditorHeader/Identity", 0.58f)]
[LabelText("ID")]
[Tooltip("Block 的稳定唯一 ID。连接、条件、Timeline Marker 与存档均依赖该值;建议使用英文、数字和下划线。")]
public string blockId;
[FoldoutGroup("$blockId")]
[FoldoutGroup("$EditorHeader")]
[HorizontalGroup("$EditorHeader/Identity")]
[LabelText("Type")]
[Tooltip("决定该 Block 的运行时入口与下方显示的专属配置。")]
public StoryBlockType blockType;
[FoldoutGroup("$blockId")]
[LabelText("Grid Column")]
[FoldoutGroup("$EditorHeader")]
[HorizontalGroup("$EditorHeader/Layout")]
[LabelText("Column")]
[Tooltip("故事树网格中的列:向右为正,(0,0) 位于容器左侧中部。可为负数与小数(用于微调)。")]
public float gridColumn;
[FoldoutGroup("$blockId")]
[LabelText("Grid Row")]
[FoldoutGroup("$EditorHeader")]
[HorizontalGroup("$EditorHeader/Layout")]
[LabelText("Row")]
[Tooltip("故事树网格中的行向下为正、向上为负0 对应垂直中线。可为小数(用于微调)。")]
public float gridRow;
[FoldoutGroup("$blockId")]
[LabelText("Next Block IDs")]
[FoldoutGroup("$EditorHeader/Flow", false)]
[LabelText("Next Blocks")]
[Tooltip("从本 Block 指向的后续 Block ID。该列表既决定连接线也会成为后续 Block 的自动前置条件来源。")]
[ListDrawerSettings(ShowFoldout = true, DefaultExpandedState = false, ShowItemCount = true)]
public List<string> nextBlockIds = new List<string>();
[FoldoutGroup("$blockId")]
[LabelText("Unlock Condition")]
[InfoBox("留空表示无条件解锁(章节起始即可用)。")]
/// <summary>
/// 自动把所有指向本 Block 的直接连接线转化为基础解锁条件。
/// 没有前置连接线的章节起始 Block 不受此字段限制;默认“全部前置完成”再与手写条件按 AND 合并。
/// </summary>
[FoldoutGroup("$EditorHeader/Flow")]
[LabelText("Connected Rule")]
[Tooltip("默认 Require All所有直接前置 Block 完成后才可用。互斥路线汇合使用 Require Any纯视觉连接使用 Ignore。")]
public ConnectedBlockUnlockRule connectedBlockUnlockRule =
ConnectedBlockUnlockRule.RequireAllDirectPredecessors;
[FoldoutGroup("$EditorHeader/Rules", false)]
[LabelText("Unlock")]
[Tooltip("可选的额外可用条件;它与 Connected Rule 按 AND 合并。没有前置连接的起始 Block 通常留空即可。")]
public StoryCondition unlockCondition = new StoryCondition();
/// <summary>
@@ -88,38 +132,41 @@ namespace Ichni.Story
/// 因而可直接使用剧情变量、Block 完成状态及 AND / OR / NOT 组合。
/// 条件满足时,该 Block 的状态固定为 Forbidden且优先于 Completed / Current / Locked。
/// </summary>
[FoldoutGroup("$blockId")]
[LabelText("Forbidden Condition")]
[InfoBox("可选。留空表示此 Block 不会被条件禁用;满足时显示为 Forbidden 且不可点击。")]
[FoldoutGroup("$EditorHeader/Rules")]
[LabelText("Forbidden")]
[Tooltip("可选。满足时该 Block 固定显示为 Forbidden 且不可点击;留空表示不会被条件禁用。")]
public StoryCondition forbiddenCondition = new StoryCondition();
// ── Text Block ──────────────────────────────────────────────────────────
[FoldoutGroup("$blockId")]
[FoldoutGroup("$EditorHeader/Text Content", false)]
[ShowIf("blockType", StoryBlockType.Text)]
[LabelText("Yarn Node Name")]
[LabelText("Yarn Node")]
[Tooltip("点击 TextBlock 后由 Yarn Spinner 开始执行的节点名。")]
public string yarnNodeName;
[FoldoutGroup("$blockId")]
[FoldoutGroup("$EditorHeader/Text Content")]
[ShowIf("blockType", StoryBlockType.Text)]
[LabelText("Title Key (Localization)")]
[LabelText("Title Key")]
[Tooltip("该 TextBlock 标题使用的 Unity Localization Entry Key留空时视觉层可使用 Block ID 作为开发阶段回退。")]
public string titleKey;
/// <summary>
/// TextBlock 的视觉和叙事层级。Important 用于主线转折与后续 checkpoint
/// Secondary 用于支线或补充剧情。默认 Important以保持现有 TextBlock 的呈现语义。
/// </summary>
[FoldoutGroup("$blockId")]
[FoldoutGroup("$EditorHeader/Text Content")]
[ShowIf("blockType", StoryBlockType.Text)]
[LabelText("Text Importance")]
[LabelText("Importance")]
[Tooltip("Important 用于主线转折与后续 CheckpointSecondary 用于支线或补充剧情,并决定对应的 Block Prefab。")]
public TextBlockImportance textImportance = TextBlockImportance.Important;
// ── Song Block ──────────────────────────────────────────────────────────
[FoldoutGroup("$blockId")]
[FoldoutGroup("$EditorHeader/Song Content", false)]
[ShowIf("blockType", StoryBlockType.Song)]
[LabelText("Song Name (Stable ID)")]
[InfoBox("对应 ChapterSelectionUnit 中 SongItemData.songName 的稳定歌曲标识。它用于查找歌曲资料与进入选曲,不能填写 displaySongName、翻译文本或作曲者名称。")]
[LabelText("Song ID")]
[Tooltip("对应 ChapterSelectionUnit 中 SongItemData.songName 的稳定歌曲标识。不能填写 displaySongName、翻译文本或作曲者名称。")]
public string songName;
/// <summary>
@@ -127,18 +174,19 @@ namespace Ichni.Story
/// Mask 可视区域固定为当前 Prefab 的尺寸;在不露出上下空白的前提下,安全范围为 -74.25 到 74.25。
/// 该值是最终坐标而非偏移量0 表示居中,负值向下移动,正值向上移动。
/// </summary>
[FoldoutGroup("$blockId")]
[FoldoutGroup("$EditorHeader/Song Content")]
[ShowIf("blockType", StoryBlockType.Song)]
[LabelText("Illustration Local Y")]
[LabelText("Illustration Y")]
[Range(-74.25f, 74.25f)]
[InfoBox("用于微调 16:9 插画在 SongBlock Mask 中的纵向裁切。请保持 SongBlockUI 的 Illustration 尺寸为 400 x 225。")]
[Tooltip("微调 16:9 插画在 SongBlock Mask 中的纵向裁切。SongBlockUI 的 Illustration 尺寸应保持为 400 × 225。")]
public float illustrationLocalY;
// ── Tutorial Block ──────────────────────────────────────────────────────
[FoldoutGroup("$blockId")]
[FoldoutGroup("$EditorHeader/Tutorial Content", false)]
[ShowIf("blockType", StoryBlockType.Tutorial)]
[LabelText("Tutorial Name")]
[LabelText("Display Name")]
[Tooltip("仅用于 StoryBlock 的显示与编辑识别,不参与教程查找。")]
public string tutorialName;
/// <summary>
@@ -146,29 +194,32 @@ namespace Ichni.Story
/// 只用于运行时查找,不使用展示名称或章节名称作为逻辑匹配条件。
/// 命名统一使用小写英文、数字和下划线,例如 <c>chapter0_intro</c>。
/// </summary>
[FoldoutGroup("$blockId")]
[FoldoutGroup("$EditorHeader/Tutorial Content")]
[ShowIf("blockType", StoryBlockType.Tutorial)]
[LabelText("Tutorial Key")]
[Tooltip("TutorialCollection 中的稳定 Key。使用小写英文、数字和下划线例如 chapter0_intro。")]
public string tutorialKey;
/// <summary>
/// 教程被玩家选择“游玩”或“跳过”后写入的全局剧情变量。
/// 教程被玩家选择“游玩”或“跳过”后写入的当前章节剧情变量。
/// 后续 Block 应通过 <see cref="VariableCondition"/> 检查该变量是否等于 1 来解锁。
/// 命名统一使用小写英文、数字和下划线,例如
/// <c>story_tutorial_chapter0_intro_resolved</c>。
/// </summary>
[FoldoutGroup("$blockId")]
[FoldoutGroup("$EditorHeader/Tutorial Content")]
[ShowIf("blockType", StoryBlockType.Tutorial)]
[LabelText("Progress Variable")]
[Tooltip("玩家选择游玩或跳过教程后写入 true 的章节剧情变量;后续 Block 可通过条件检查它。")]
public string tutorialProgressVariable;
/// <summary>
/// 要启动的教程难度的稳定存档 ID对应 <see cref="Ichni.Menu.DifficultyData.saveDifficultyId"/>。
/// 不使用难度列表下标,避免调整难度排列后教程进入错误谱面。
/// </summary>
[FoldoutGroup("$blockId")]
[FoldoutGroup("$EditorHeader/Tutorial Content")]
[ShowIf("blockType", StoryBlockType.Tutorial)]
[LabelText("Tutorial Difficulty Save ID")]
[LabelText("Difficulty Save ID")]
[Tooltip("要启动教程谱面的 DifficultyData.saveDifficultyId。不要使用难度列表下标以免排序变化后指向错误谱面。")]
public int tutorialDifficultySaveId = -1;
/// <summary>
@@ -196,12 +247,13 @@ namespace Ichni.Story
/// 例如 unlockCondition 留空即允许,而 forbiddenCondition 留空即不禁用。</para>
/// </summary>
[InlineProperty]
[HideReferenceObjectPicker]
[Serializable]
public class StoryCondition
{
[HideLabel]
[SerializeReference]
[InfoBox("留空表示未配置条件。可选择 All Of(AND) / Any Of(OR) / Not 复合节点进行任意嵌套。")]
[Tooltip("留空表示未配置条件。可选择 All Of(AND) / Any Of(OR) / Not 复合节点进行任意嵌套。")]
public StoryConditionNode root;
/// <summary>
@@ -219,5 +271,15 @@ namespace Ichni.Story
{
return root != null && root.Evaluate(getVariable, isBlockCompleted);
}
#if UNITY_EDITOR
/// <summary>
/// Odin 条件 Drawer 使用的只读摘要;不参与运行时求值和序列化。
/// </summary>
public string GetEditorSummary()
{
return root == null ? "None" : root.GetEditorSummary();
}
#endif
}
}