2026-07-05 16:08:23 -04:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using Sirenix.OdinInspector;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using Yarn.Unity;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Ichni.Story
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 单个章节的故事树数据,以 ScriptableObject 形式存储所有 block 定义与关联资产引用。
|
|
|
|
|
|
/// 布局完全由每个 block 的 (gridColumn, gridRow) 静态决定,运行时全量展示。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[CreateAssetMenu(fileName = "StoryData", menuName = "Ichni/Story/New/StoryData")]
|
2026-07-21 15:24:42 -04:00
|
|
|
|
public partial class StoryData : SerializedScriptableObject
|
2026-07-05 16:08:23 -04:00
|
|
|
|
{
|
2026-07-21 15:24:42 -04:00
|
|
|
|
[TitleGroup("Chapter Settings", Alignment = TitleAlignments.Centered)]
|
2026-07-05 16:08:23 -04:00
|
|
|
|
[LabelText("Chapter Index")]
|
2026-07-21 15:24:42 -04:00
|
|
|
|
[Tooltip("章节的稳定索引。StorySave、StoryManager 与 ChapterSelection 都以该值定位当前章节。")]
|
2026-07-05 16:08:23 -04:00
|
|
|
|
public string chapterIndex;
|
|
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
[TitleGroup("Chapter Settings")]
|
2026-07-05 16:08:23 -04:00
|
|
|
|
[LabelText("Yarn Project")]
|
2026-07-21 15:24:42 -04:00
|
|
|
|
[Tooltip("该章节对应的 YarnProject 编译资产。TextBlock 会在运行时从此项目中执行配置的 Yarn Node。")]
|
2026-07-05 16:08:23 -04:00
|
|
|
|
public YarnProject yarnProject;
|
|
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 本章节永久剧情变量的默认值。它们只属于本章节,且不会直接写入存档;
|
|
|
|
|
|
/// 玩家第一次通过 Yarn 或其它剧情入口写入同名变量后,存档值才会覆盖这里的默认值。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[LabelText("Initial Variables")]
|
|
|
|
|
|
[ListDrawerSettings(ShowFoldout = true, DefaultExpandedState = false, DraggableItems = true, ShowItemCount = true)]
|
|
|
|
|
|
[Tooltip("变量 Key 使用小写英文、数字和下划线。Yarn 请通过 <<set_*>> 与 get_*() 访问,不使用 <<declare>>、<<set>> 或 $variable。")]
|
|
|
|
|
|
public List<StoryVariableDefinition> initialVariables = new List<StoryVariableDefinition>();
|
|
|
|
|
|
|
2026-07-05 16:08:23 -04:00
|
|
|
|
[LabelText("Blocks")]
|
|
|
|
|
|
[ListDrawerSettings(ShowFoldout = true, DefaultExpandedState = false, DraggableItems = true, ShowItemCount = true)]
|
2026-07-21 15:24:42 -04:00
|
|
|
|
[Tooltip("章节的完整剧情树定义。列表排序不决定运行时流程;连接关系、条件与网格坐标才是唯一依据。")]
|
2026-07-05 16:08:23 -04:00
|
|
|
|
public List<StoryBlockDefinition> blocks = new List<StoryBlockDefinition>();
|
|
|
|
|
|
|
2026-07-20 16:56:04 -04:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 章节顶部 Timeline 的静态标记定义。
|
|
|
|
|
|
/// 标记的横向位置始终由 <see cref="StoryTimelineMarkerDefinition.anchorBlockId"/> 指向的
|
|
|
|
|
|
/// Block 的 <c>gridColumn</c> 推导,不在这里重复保存坐标,避免剧情树与 Timeline 脱节。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[LabelText("Timeline Markers")]
|
|
|
|
|
|
[ListDrawerSettings(ShowFoldout = true, DefaultExpandedState = false, DraggableItems = true, ShowItemCount = true)]
|
2026-07-21 15:24:42 -04:00
|
|
|
|
[Tooltip("每个标记锚定到一个 TextBlock;运行时使用该 Block 的 Grid Column 对齐 Timeline。TextBlock 为 Current / Completed 时显示,Locked / Forbidden 时隐藏。")]
|
2026-07-20 16:56:04 -04:00
|
|
|
|
public List<StoryTimelineMarkerDefinition> timelineMarkers = new List<StoryTimelineMarkerDefinition>();
|
|
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 仅供编辑器自检使用的互斥结局分组,不参与运行时的解锁、禁用或存档逻辑。
|
|
|
|
|
|
/// 实际路线禁用仍完全通过各 Block 的 Forbidden Condition 表达,以保留剧情变量组合的灵活性。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[LabelText("Exclusive Route Groups")]
|
|
|
|
|
|
[ListDrawerSettings(ShowFoldout = true, DefaultExpandedState = false, DraggableItems = true, ShowItemCount = true)]
|
|
|
|
|
|
[Tooltip("把同一章节中只能同时达成一个的结局 Block 放入同一组。Validate Story Data 会检查引用、终点形态与 Forbidden Condition 配置;不会改变运行时逻辑。")]
|
|
|
|
|
|
public List<StoryRouteValidationGroup> exclusiveRouteGroups = new List<StoryRouteValidationGroup>();
|
|
|
|
|
|
|
2026-07-05 16:08:23 -04:00
|
|
|
|
// ── Queries ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 根据 blockId 获取 block 定义,未找到时返回 null。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public StoryBlockDefinition GetBlock(string blockId)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (StoryBlockDefinition block in blocks)
|
|
|
|
|
|
{
|
2026-07-21 15:24:42 -04:00
|
|
|
|
if (block != null && block.blockId == blockId)
|
2026-07-05 16:08:23 -04:00
|
|
|
|
return block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-07-21 15:24:42 -04:00
|
|
|
|
/// 尝试获取一个章节变量的静态默认定义。重复 Key 取列表中第一项,
|
|
|
|
|
|
/// 因此配置时应保持 Key 唯一,以免在 Inspector 中产生歧义。
|
2026-07-05 16:08:23 -04:00
|
|
|
|
/// </summary>
|
2026-07-21 15:24:42 -04:00
|
|
|
|
public bool TryGetInitialVariable(string key, out StoryVariableDefinition definition)
|
2026-07-05 16:08:23 -04:00
|
|
|
|
{
|
2026-07-21 15:24:42 -04:00
|
|
|
|
if (!string.IsNullOrWhiteSpace(key) && initialVariables != null)
|
2026-07-05 16:08:23 -04:00
|
|
|
|
{
|
2026-07-21 15:24:42 -04:00
|
|
|
|
foreach (StoryVariableDefinition item in initialVariables)
|
2026-07-05 16:08:23 -04:00
|
|
|
|
{
|
2026-07-21 15:24:42 -04:00
|
|
|
|
if (item != null && item.key == key)
|
2026-07-05 16:08:23 -04:00
|
|
|
|
{
|
2026-07-21 15:24:42 -04:00
|
|
|
|
definition = item;
|
|
|
|
|
|
return true;
|
2026-07-05 16:08:23 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-20 16:56:04 -04:00
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
definition = null;
|
|
|
|
|
|
return false;
|
2026-07-20 16:56:04 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-07-21 15:24:42 -04:00
|
|
|
|
/// 获取指定 blockId 的所有直接后继 block 定义。
|
2026-07-20 16:56:04 -04:00
|
|
|
|
/// </summary>
|
2026-07-21 15:24:42 -04:00
|
|
|
|
public IEnumerable<StoryBlockDefinition> GetNextBlocks(string blockId)
|
2026-07-20 16:56:04 -04:00
|
|
|
|
{
|
2026-07-21 15:24:42 -04:00
|
|
|
|
StoryBlockDefinition source = GetBlock(blockId);
|
|
|
|
|
|
if (source == null) yield break;
|
2026-07-20 16:56:04 -04:00
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
if (source.nextBlockIds == null)
|
|
|
|
|
|
yield break;
|
2026-07-20 16:56:04 -04:00
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
foreach (string nextId in source.nextBlockIds)
|
2026-07-05 16:08:23 -04:00
|
|
|
|
{
|
2026-07-21 15:24:42 -04:00
|
|
|
|
StoryBlockDefinition next = GetBlock(nextId);
|
|
|
|
|
|
if (next != null)
|
|
|
|
|
|
yield return next;
|
2026-07-05 16:08:23 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|