Story排版
This commit is contained in:
@@ -12,7 +12,8 @@ namespace Ichni.Story
|
||||
{
|
||||
Locked,
|
||||
Current,
|
||||
Completed
|
||||
Completed,
|
||||
Forbidden
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -25,6 +26,16 @@ namespace Ichni.Story
|
||||
Tutorial
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TextBlock 的叙事层级。层级只决定剧情树中的视觉样式和 checkpoint 候选资格,
|
||||
/// 不会自行改变 Block 的解锁条件或 Yarn 执行逻辑。
|
||||
/// </summary>
|
||||
public enum TextBlockImportance
|
||||
{
|
||||
Important,
|
||||
Secondary
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 变量条件的比较方式。
|
||||
/// </summary>
|
||||
@@ -69,7 +80,18 @@ namespace Ichni.Story
|
||||
|
||||
[FoldoutGroup("$blockId")]
|
||||
[LabelText("Unlock Condition")]
|
||||
public UnlockCondition unlockCondition = new UnlockCondition();
|
||||
[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 ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -83,13 +105,35 @@ namespace Ichni.Story
|
||||
[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")]
|
||||
[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")]
|
||||
@@ -145,26 +189,35 @@ namespace Ichni.Story
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Block 的解锁条件。以一棵可组合的条件树(<see cref="StoryConditionNode"/>)表达,
|
||||
/// 支持"与 / 或 / 非 + 叶子条件"的任意嵌套。根节点为空时视为无条件解锁(始终满足)。
|
||||
/// 通用剧情条件容器。以一棵可组合的条件树(<see cref="StoryConditionNode"/>)表达,
|
||||
/// 支持“与 / 或 / 非 + 叶子条件”的任意嵌套。
|
||||
/// <para>该类型不限定用途,可用于 Block 的解锁、禁用、Helper 对话筛选等所有剧情判断。</para>
|
||||
/// <para>根节点为空表示“未配置条件”,其是否视为通过由调用方根据业务语义决定;
|
||||
/// 例如 unlockCondition 留空即允许,而 forbiddenCondition 留空即不禁用。</para>
|
||||
/// </summary>
|
||||
[InlineProperty]
|
||||
[Serializable]
|
||||
public class UnlockCondition
|
||||
public class StoryCondition
|
||||
{
|
||||
[HideLabel]
|
||||
[SerializeReference]
|
||||
[InfoBox("留空表示无条件解锁(章节起始即可用)。可选择 All Of(AND) / Any Of(OR) / Not 复合节点进行任意嵌套。")]
|
||||
[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);
|
||||
return root != null && root.Evaluate(getVariable, isBlockCompleted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ using UnityEngine;
|
||||
namespace Ichni.Story
|
||||
{
|
||||
/// <summary>
|
||||
/// 解锁条件树的节点基类。通过多态组合出"与 / 或 / 非 + 叶子条件"的复合表达式。
|
||||
/// 由 <see cref="UnlockCondition"/> 借助 <c>[SerializeReference]</c> 序列化持有,
|
||||
/// 通用剧情条件树的节点基类。通过多态组合出“与 / 或 / 非 + 叶子条件”的复合表达式。
|
||||
/// 由 <see cref="StoryCondition"/> 借助 <c>[SerializeReference]</c> 序列化持有,
|
||||
/// Odin 会以类型下拉框展示可选的节点类型。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
@@ -17,7 +17,7 @@ namespace Ichni.Story
|
||||
/// 对该节点求值。
|
||||
/// </summary>
|
||||
/// <param name="getVariable">按变量名返回其整型值的委托。</param>
|
||||
/// <param name="isBlockCompleted">按 blockId 判断该 block 是否已完成的委托。</param>
|
||||
/// <param name="isBlockCompleted">按 blockId 判断该 Block 是否已完成的委托。</param>
|
||||
public abstract bool Evaluate(Func<string, int> getVariable, Func<string, bool> isBlockCompleted);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace Ichni.Story
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复合节点(逻辑非 NOT):对子条件取反。子条件为空视为满足(等价于"非 假")。
|
||||
/// 复合节点(逻辑非 NOT):对子条件取反。子条件为空视为满足(等价于“非 假”)。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[LabelText("Not")]
|
||||
@@ -90,7 +90,7 @@ namespace Ichni.Story
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 叶子节点:指定 block 已完成时满足。
|
||||
/// 叶子节点:指定 Block 已完成时满足。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[LabelText("Block Completed")]
|
||||
@@ -107,6 +107,7 @@ namespace Ichni.Story
|
||||
|
||||
/// <summary>
|
||||
/// 叶子节点:将指定变量的当前值与目标值按比较方式求值。
|
||||
/// 既可用于解锁条件,也可用于 Forbidden Condition;例如当路线变量等于另一条路线的值时禁用 Block。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[LabelText("Variable Compare")]
|
||||
|
||||
@@ -27,6 +27,16 @@ namespace Ichni.Story
|
||||
[ListDrawerSettings(ShowFoldout = true, DefaultExpandedState = false, DraggableItems = true, ShowItemCount = true)]
|
||||
public List<StoryBlockDefinition> blocks = new List<StoryBlockDefinition>();
|
||||
|
||||
/// <summary>
|
||||
/// 章节顶部 Timeline 的静态标记定义。
|
||||
/// 标记的横向位置始终由 <see cref="StoryTimelineMarkerDefinition.anchorBlockId"/> 指向的
|
||||
/// Block 的 <c>gridColumn</c> 推导,不在这里重复保存坐标,避免剧情树与 Timeline 脱节。
|
||||
/// </summary>
|
||||
[LabelText("Timeline Markers")]
|
||||
[ListDrawerSettings(ShowFoldout = true, DefaultExpandedState = false, DraggableItems = true, ShowItemCount = true)]
|
||||
[InfoBox("每个标记锚定到一个 TextBlock;运行时使用该 Block 的 Grid Column 对齐 Timeline。TextBlock 为 Current / Completed 时显示,Locked / Forbidden 时隐藏。未配置标记时,现有章节行为不变。")]
|
||||
public List<StoryTimelineMarkerDefinition> timelineMarkers = new List<StoryTimelineMarkerDefinition>();
|
||||
|
||||
// ── Queries ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
@@ -70,24 +80,30 @@ namespace Ichni.Story
|
||||
|
||||
private const float PreviewViewportHeight = 380f;
|
||||
private const float PreviewPaddingLeft = 24f;
|
||||
private const float PreviewTangent = 44f;
|
||||
|
||||
// 详细版:较大方块,标注 id / 类型 / 标题
|
||||
private const float DetailCellWidth = 150f;
|
||||
private const float DetailCellHeight = 72f;
|
||||
private const float DetailStepX = 210f;
|
||||
private const float DetailStepY = 120f;
|
||||
// 预览与运行时共用同一套“列 / 行代表 Block 中心点”的布局语义。
|
||||
// 这里的尺寸直接对应当前四种 Block Prefab 的设计尺寸;若未来修改 Prefab 尺寸,必须同步更新本组常量。
|
||||
private const float ImportantBlockWidth = 600f;
|
||||
private const float ImportantBlockHeight = 375f;
|
||||
private const float SecondaryBlockWidth = 400f;
|
||||
private const float SecondaryBlockHeight = 200f;
|
||||
private const float SongBlockWidth = 400f;
|
||||
private const float SongBlockHeight = 100f;
|
||||
private const float TutorialBlockWidth = 400f;
|
||||
private const float TutorialBlockHeight = 100f;
|
||||
|
||||
// 简略版:很小的方块,仅标注 Block ID
|
||||
private const float SimpleCellWidth = 62f;
|
||||
private const float SimpleCellHeight = 26f;
|
||||
private const float SimpleStepX = 92f;
|
||||
private const float SimpleStepY = 42f;
|
||||
// 与 StoryTreeController 默认 columnStep / rowStep 保持比例一致,
|
||||
// 因而预览中的相邻列、行间距也能反映运行时的实际布局关系。
|
||||
private const float RuntimeColumnStep = 720f;
|
||||
private const float RuntimeRowStep = 525f;
|
||||
private const float DetailedPreviewScale = 0.25f;
|
||||
private const float SimplePreviewScale = 0.10f;
|
||||
|
||||
/// <summary>
|
||||
/// 在 Inspector 顶部绘制只读的故事树结构预览(流程图样式,连线左右相连)。
|
||||
/// 提供"详细 / 简略"两种可拖拽平移的视图;按 (gridColumn, gridRow) 摆放 block,
|
||||
/// 支持负数与小数坐标,(0,0) 对应左侧中部。
|
||||
/// 提供"详细 / 简略"两种可拖拽平移的视图;按 (gridColumn, gridRow) 摆放 block。
|
||||
/// 每个 Block 使用其运行时 Prefab 尺寸按统一比例缩放后的矩形,且列坐标始终代表 Block 的中心点,
|
||||
/// 从而让 Important / Secondary / Song / Tutorial 的体积与连线端点关系尽量接近游戏内画面。
|
||||
/// </summary>
|
||||
[PropertyOrder(-10)]
|
||||
[OnInspectorGUI]
|
||||
@@ -110,10 +126,9 @@ namespace Ichni.Story
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
bool simple = _previewMode == StoryPreviewMode.Simple;
|
||||
float cellW = simple ? SimpleCellWidth : DetailCellWidth;
|
||||
float cellH = simple ? SimpleCellHeight : DetailCellHeight;
|
||||
float stepX = simple ? SimpleStepX : DetailStepX;
|
||||
float stepY = simple ? SimpleStepY : DetailStepY;
|
||||
float previewScale = simple ? SimplePreviewScale : DetailedPreviewScale;
|
||||
float stepX = RuntimeColumnStep * previewScale;
|
||||
float stepY = RuntimeRowStep * previewScale;
|
||||
|
||||
Rect area = GUILayoutUtility.GetRect(0f, PreviewViewportHeight, GUILayout.ExpandWidth(true));
|
||||
EditorGUI.DrawRect(area, new Color(0.12f, 0.12f, 0.14f, 1f));
|
||||
@@ -123,26 +138,34 @@ namespace Ichni.Story
|
||||
// 裁剪到预览区,超出部分(平移后)不外溢到其它 Inspector 元素
|
||||
GUI.BeginClip(area);
|
||||
{
|
||||
// (0,0) 网格点 = 视口左侧中线(叠加平移量)
|
||||
float originX = PreviewPaddingLeft + _previewPan.x;
|
||||
// (0,0) 是 Block 的逻辑中心点。为避免第一列 Important Block 被左侧裁掉,
|
||||
// 初始视图预留半个最大 Block 宽度;这不改变 StoryData 的坐标语义。
|
||||
float originX = PreviewPaddingLeft + ImportantBlockWidth * previewScale * 0.5f + _previewPan.x;
|
||||
float originY = area.height * 0.5f + _previewPan.y;
|
||||
|
||||
// 计算各 block 矩形(左中对齐网格点,与运行时 pivot(0,0.5) 一致),按 blockId 索引
|
||||
// 计算各 Block 的实际比例矩形。运行时同样以中心 Pivot 放置,因此 gridColumn / gridRow
|
||||
// 在预览中也对应矩形中心,而不是左边缘。
|
||||
Dictionary<string, Rect> cellRects = new Dictionary<string, Rect>();
|
||||
foreach (StoryBlockDefinition block in blocks)
|
||||
{
|
||||
if (block == null || string.IsNullOrEmpty(block.blockId)) continue;
|
||||
if (!HasPreviewableBlockId(block)) continue;
|
||||
|
||||
float px = originX + block.gridColumn * stepX; // 列向右为正(可负、可小数)
|
||||
float py = originY + block.gridRow * stepY; // 行向下为正、向上为负
|
||||
cellRects[block.blockId] = new Rect(px, py - cellH * 0.5f, cellW, cellH);
|
||||
Vector2 blockSize = GetPreviewBlockSize(block, previewScale);
|
||||
float px = originX + block.gridColumn * stepX; // 列向右为正(可负、可小数)
|
||||
float py = originY + block.gridRow * stepY; // 行向下为正、向上为负
|
||||
cellRects[block.blockId] = new Rect(
|
||||
px - blockSize.x * 0.5f,
|
||||
py - blockSize.y * 0.5f,
|
||||
blockSize.x,
|
||||
blockSize.y);
|
||||
}
|
||||
|
||||
// 先画连线:左右相连的流程图(起点右中 → 终点左中,水平切线)
|
||||
// 先画连线:与 BlockConnectorView 相同,起点右中 → 以两个 Block 中心的中点列折线 → 终点左中。
|
||||
// 不使用端口中点作为转折列,避免不同宽度 Block 在同一列时出现不一致的转折位置。
|
||||
Handles.BeginGUI();
|
||||
foreach (StoryBlockDefinition block in blocks)
|
||||
{
|
||||
if (block == null || !cellRects.TryGetValue(block.blockId, out Rect fromRect)) continue;
|
||||
if (!HasPreviewableBlockId(block) || !cellRects.TryGetValue(block.blockId, out Rect fromRect)) continue;
|
||||
|
||||
foreach (string nextId in block.nextBlockIds)
|
||||
{
|
||||
@@ -150,11 +173,12 @@ namespace Ichni.Story
|
||||
|
||||
Vector3 start = new Vector3(fromRect.xMax, fromRect.center.y, 0f);
|
||||
Vector3 end = new Vector3(toRect.xMin, toRect.center.y, 0f);
|
||||
Vector3 startTan = start + Vector3.right * PreviewTangent;
|
||||
Vector3 endTan = end + Vector3.left * PreviewTangent;
|
||||
float midX = (fromRect.center.x + toRect.center.x) * 0.5f;
|
||||
Vector3 mid1 = new Vector3(midX, start.y, 0f);
|
||||
Vector3 mid2 = new Vector3(midX, end.y, 0f);
|
||||
|
||||
Handles.DrawBezier(start, end, startTan, endTan,
|
||||
new Color(0.82f, 0.82f, 0.88f, 1f), null, simple ? 2f : 3f);
|
||||
Handles.color = new Color(0.82f, 0.82f, 0.88f, 1f);
|
||||
Handles.DrawAAPolyLine(simple ? 2f : 3f, start, mid1, mid2, end);
|
||||
}
|
||||
}
|
||||
Handles.EndGUI();
|
||||
@@ -171,22 +195,58 @@ namespace Ichni.Story
|
||||
|
||||
foreach (StoryBlockDefinition block in blocks)
|
||||
{
|
||||
if (block == null || !cellRects.TryGetValue(block.blockId, out Rect rect)) continue;
|
||||
if (!HasPreviewableBlockId(block) || !cellRects.TryGetValue(block.blockId, out Rect rect)) continue;
|
||||
|
||||
EditorGUI.DrawRect(rect, GetTypeColor(block.blockType));
|
||||
string label = simple
|
||||
? block.blockId
|
||||
: $"{block.blockId}\n[{block.blockType}]\n{block.GetDisplayTitle()}";
|
||||
string label = GetPreviewLabel(block, simple, rect.height);
|
||||
GUI.Label(rect, label, labelStyle);
|
||||
}
|
||||
}
|
||||
GUI.EndClip();
|
||||
|
||||
EditorGUILayout.HelpBox(
|
||||
"在预览区内拖拽可平移视图。列 = 水平(右为正),行 = 垂直(下为正、上为负),(0,0) 位于左侧中部,支持负数与小数。",
|
||||
"在预览区内拖拽可平移视图。列 = 水平(右为正),行 = 垂直(下为正、上为负);坐标代表 Block 中心点。预览按当前 Prefab 尺寸比例绘制:Important 600×375、Secondary 400×200、Song / Tutorial 400×100。",
|
||||
MessageType.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回一个 Block 在 Inspector 预览中的尺寸。所有类型使用同一缩放比例,
|
||||
/// 因而宽高比及不同 Block 之间的体积关系与运行时 Prefab 保持一致。
|
||||
/// </summary>
|
||||
private static Vector2 GetPreviewBlockSize(StoryBlockDefinition block, float previewScale)
|
||||
{
|
||||
Vector2 runtimeSize = block.blockType switch
|
||||
{
|
||||
StoryBlockType.Text when block.textImportance == TextBlockImportance.Important =>
|
||||
new Vector2(ImportantBlockWidth, ImportantBlockHeight),
|
||||
StoryBlockType.Text => new Vector2(SecondaryBlockWidth, SecondaryBlockHeight),
|
||||
StoryBlockType.Song => new Vector2(SongBlockWidth, SongBlockHeight),
|
||||
StoryBlockType.Tutorial => new Vector2(TutorialBlockWidth, TutorialBlockHeight),
|
||||
_ => Vector2.zero
|
||||
};
|
||||
|
||||
return runtimeSize * previewScale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据可用高度选择预览标签密度。Song / Tutorial 的真实比例高度较小,
|
||||
/// 因此只显示 Block ID,避免文字撑破矩形而掩盖实际体积关系。
|
||||
/// </summary>
|
||||
private static string GetPreviewLabel(StoryBlockDefinition block, bool simple, float blockHeight)
|
||||
{
|
||||
if (simple || blockHeight < 32f)
|
||||
{
|
||||
return block.blockId;
|
||||
}
|
||||
|
||||
if (blockHeight < 60f)
|
||||
{
|
||||
return $"{block.blockId}\n[{block.blockType}]";
|
||||
}
|
||||
|
||||
return $"{block.blockId}\n[{block.blockType}]\n{block.GetDisplayTitle()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理预览区内的鼠标拖拽平移。
|
||||
/// </summary>
|
||||
@@ -220,6 +280,17 @@ namespace Ichni.Story
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断 Block 能否参与 Inspector 中的故事树预览。
|
||||
/// 新增 Block 时,Odin 会先创建一个尚未填写 blockId 的临时数据项;它还不能作为
|
||||
/// Dictionary 的 Key,也不应在预览中显示或连接。统一在三个预览阶段过滤,避免后续
|
||||
/// Draw / TryGetValue 阶段再次把空引用传入 Dictionary。
|
||||
/// </summary>
|
||||
private static bool HasPreviewableBlockId(StoryBlockDefinition block)
|
||||
{
|
||||
return block != null && !string.IsNullOrWhiteSpace(block.blockId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预览中不同 block 类型的填充色。
|
||||
/// </summary>
|
||||
|
||||
74
Assets/Scripts/NewStorySystem/Data/StoryHelperData.cs
Normal file
74
Assets/Scripts/NewStorySystem/Data/StoryHelperData.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.Story
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局 Helper 的独立数据资产。
|
||||
/// <para>该资产不隶属于任何 StoryData,由 <see cref="StoryManager.helperData"/> 统一引用,
|
||||
/// 因而所有章节都共用同一个 Helper 角色、名称和候选对话池。</para>
|
||||
/// <para>运行时会在满足条件的候选中按权重抽取,并在候选数量允许时避免与上次对话重复;
|
||||
/// 具体抽取和 UI 展示逻辑将在 Helper 功能实现阶段接入。</para>
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "StoryHelperData", menuName = "Ichni/Story/New/Story Helper Data")]
|
||||
public class StoryHelperData : SerializedScriptableObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper 显示名称的 Unity Localization Entry Key。
|
||||
/// </summary>
|
||||
[LabelText("Display Name Key (Localization)")]
|
||||
public string displayNameKey;
|
||||
|
||||
/// <summary>
|
||||
/// 当所有候选对话均不满足条件时显示的兜底对话 Entry Key。
|
||||
/// </summary>
|
||||
[LabelText("Fallback Dialogue Key (Localization)")]
|
||||
public string fallbackDialogueKey;
|
||||
|
||||
/// <summary>
|
||||
/// 全章节共用的候选对话。条件留空表示始终可参与抽取。
|
||||
/// </summary>
|
||||
[LabelText("Dialogue Pool")]
|
||||
[ListDrawerSettings(ShowFoldout = true, DefaultExpandedState = false, DraggableItems = true, ShowItemCount = true)]
|
||||
public List<StoryHelperDialogueDefinition> dialoguePool = new List<StoryHelperDialogueDefinition>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper 对话池中的一条候选记录。
|
||||
/// <para>条件复用故事树的 <see cref="StoryCondition"/>,因此未来可直接根据章节剧情变量、
|
||||
/// 路线变量或指定 Block 的完成状态筛选,而不需要为 Helper 另写一套条件语法。</para>
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[InlineProperty]
|
||||
public class StoryHelperDialogueDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// 候选对话的稳定 ID,仅用于调试、去重和未来数据追踪。
|
||||
/// 命名使用小写英文、数字和下划线,例如 <c>general_greeting_01</c>。
|
||||
/// </summary>
|
||||
[LabelText("Dialogue ID")]
|
||||
public string dialogueId;
|
||||
|
||||
/// <summary>
|
||||
/// 实际显示内容的 Unity Localization Entry Key。
|
||||
/// </summary>
|
||||
[LabelText("Dialogue Key (Localization)")]
|
||||
public string dialogueKey;
|
||||
|
||||
/// <summary>
|
||||
/// 此对话可被抽取的条件;留空表示始终可用。
|
||||
/// </summary>
|
||||
[LabelText("Availability Condition")]
|
||||
public StoryCondition availabilityCondition = new StoryCondition();
|
||||
|
||||
/// <summary>
|
||||
/// 在所有满足条件的候选中的相对抽取权重。值越大,出现概率越高;
|
||||
/// 小于 1 的值在运行时应按 1 处理,避免无效的零或负权重。
|
||||
/// </summary>
|
||||
[LabelText("Weight")]
|
||||
[MinValue(1)]
|
||||
public int weight = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24bdc25737924012ab3c078aa043fcad
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.Story
|
||||
{
|
||||
/// <summary>
|
||||
/// 一个章节 Timeline 标记的静态定义。
|
||||
/// <para>它不保存独立的横坐标:运行时始终跟随锚定 Block 的 <c>gridColumn</c>,
|
||||
/// 使时间轴与剧情树在横向拖动时保持对齐。</para>
|
||||
/// <para>显示文本和 ID 使用 Unity Localization 的 Entry Key;本阶段不绑定具体 String Table,
|
||||
/// 以便后续统一迁移本地化系统时集中配置。</para>
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[InlineProperty]
|
||||
public class StoryTimelineMarkerDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// 标记的稳定唯一 ID,仅用于配置、调试和未来持久化引用。
|
||||
/// 命名使用小写英文、数字和下划线,例如 <c>chapter0_day_01</c>。
|
||||
/// </summary>
|
||||
[LabelText("Marker ID")]
|
||||
public string markerId;
|
||||
|
||||
/// <summary>
|
||||
/// 被此标记锚定的 TextBlock ID。该 Block 的 gridColumn 是标记的唯一位置来源;
|
||||
/// 当它处于 Current 或 Completed 时,标记显示;Locked 或 Forbidden 时,标记隐藏。
|
||||
/// </summary>
|
||||
[LabelText("Anchor Block ID")]
|
||||
public string anchorBlockId;
|
||||
|
||||
/// <summary>
|
||||
/// Timeline 上显示的本地化文本 Key。通常用于时间描述,也可用于特殊叙事文本。
|
||||
/// </summary>
|
||||
[LabelText("Label Key (Localization)")]
|
||||
public string labelKey;
|
||||
|
||||
/// <summary>
|
||||
/// 是否参与章节主线进度百分比计算。支线或纯装饰性时间标记应关闭此项。
|
||||
/// </summary>
|
||||
[LabelText("Contributes To Main Progress")]
|
||||
public bool contributesToMainProgress = true;
|
||||
|
||||
/// <summary>
|
||||
/// 锚定 Block 完成后可贡献的主线进度,范围为 0 到 1。
|
||||
/// 同一章节的不同结局都应配置为 1;运行时取所有有效完成标记中的最大值。
|
||||
/// </summary>
|
||||
[LabelText("Progress Value")]
|
||||
[Range(0f, 1f)]
|
||||
public float progressValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 02587e83d8754ac5828e51066dce570f
|
||||
Reference in New Issue
Block a user