Files
ichni_Official/Assets/Scripts/NewStorySystem/Data/StoryData.cs
2026-07-05 16:08:23 -04:00

239 lines
10 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.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using Yarn.Unity;
#if UNITY_EDITOR
using UnityEditor;
using Sirenix.Utilities.Editor;
#endif
namespace Ichni.Story
{
/// <summary>
/// 单个章节的故事树数据,以 ScriptableObject 形式存储所有 block 定义与关联资产引用。
/// 布局完全由每个 block 的 (gridColumn, gridRow) 静态决定,运行时全量展示。
/// </summary>
[CreateAssetMenu(fileName = "StoryData", menuName = "Ichni/Story/New/StoryData")]
public class StoryData : SerializedScriptableObject
{
[LabelText("Chapter Index")]
public string chapterIndex;
[LabelText("Yarn Project")]
[InfoBox("该章节对应的 YarnProject 编译资产,阶段 2 接线。")]
public YarnProject yarnProject;
[LabelText("Blocks")]
[ListDrawerSettings(ShowFoldout = true, DefaultExpandedState = false, DraggableItems = true, ShowItemCount = true)]
public List<StoryBlockDefinition> blocks = new List<StoryBlockDefinition>();
// ── Queries ─────────────────────────────────────────────────────────────
/// <summary>
/// 根据 blockId 获取 block 定义,未找到时返回 null。
/// </summary>
public StoryBlockDefinition GetBlock(string blockId)
{
foreach (StoryBlockDefinition block in blocks)
{
if (block.blockId == blockId)
return block;
}
return null;
}
/// <summary>
/// 获取指定 blockId 的所有直接后继 block 定义。
/// </summary>
public IEnumerable<StoryBlockDefinition> GetNextBlocks(string blockId)
{
StoryBlockDefinition source = GetBlock(blockId);
if (source == null) yield break;
foreach (string nextId in source.nextBlockIds)
{
StoryBlockDefinition next = GetBlock(nextId);
if (next != null)
yield return next;
}
}
// ── Editor Preview ────────────────────────────────────────────────────────
#if UNITY_EDITOR
private enum StoryPreviewMode { Detailed, Simple }
// 编辑器 UI 临时状态(不参与序列化)
[System.NonSerialized] private StoryPreviewMode _previewMode = StoryPreviewMode.Detailed;
[System.NonSerialized] private Vector2 _previewPan;
[System.NonSerialized] private bool _isPanningPreview;
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 ID
private const float SimpleCellWidth = 62f;
private const float SimpleCellHeight = 26f;
private const float SimpleStepX = 92f;
private const float SimpleStepY = 42f;
/// <summary>
/// 在 Inspector 顶部绘制只读的故事树结构预览(流程图样式,连线左右相连)。
/// 提供"详细 / 简略"两种可拖拽平移的视图;按 (gridColumn, gridRow) 摆放 block
/// 支持负数与小数坐标,(0,0) 对应左侧中部。
/// </summary>
[PropertyOrder(-10)]
[OnInspectorGUI]
private void DrawStoryPreview()
{
SirenixEditorGUI.Title("Story Tree Preview", null, TextAlignment.Left, true);
if (blocks == null || blocks.Count == 0)
{
EditorGUILayout.HelpBox("尚未定义任何 block。", MessageType.Info);
return;
}
// 顶部工具条:模式切换 + 复位视图
EditorGUILayout.BeginHorizontal();
_previewMode = (StoryPreviewMode)GUILayout.Toolbar(
(int)_previewMode, new[] { "Detailed", "Simple" }, GUILayout.Height(20f));
if (GUILayout.Button("Reset View", GUILayout.Width(90f), GUILayout.Height(20f)))
_previewPan = Vector2.zero;
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;
Rect area = GUILayoutUtility.GetRect(0f, PreviewViewportHeight, GUILayout.ExpandWidth(true));
EditorGUI.DrawRect(area, new Color(0.12f, 0.12f, 0.14f, 1f));
HandlePreviewPanning(area);
// 裁剪到预览区,超出部分(平移后)不外溢到其它 Inspector 元素
GUI.BeginClip(area);
{
// (0,0) 网格点 = 视口左侧中线(叠加平移量)
float originX = PreviewPaddingLeft + _previewPan.x;
float originY = area.height * 0.5f + _previewPan.y;
// 计算各 block 矩形(左中对齐网格点,与运行时 pivot(0,0.5) 一致),按 blockId 索引
Dictionary<string, Rect> cellRects = new Dictionary<string, Rect>();
foreach (StoryBlockDefinition block in blocks)
{
if (block == null || string.IsNullOrEmpty(block.blockId)) 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);
}
// 先画连线:左右相连的流程图(起点右中 → 终点左中,水平切线)
Handles.BeginGUI();
foreach (StoryBlockDefinition block in blocks)
{
if (block == null || !cellRects.TryGetValue(block.blockId, out Rect fromRect)) continue;
foreach (string nextId in block.nextBlockIds)
{
if (string.IsNullOrEmpty(nextId) || !cellRects.TryGetValue(nextId, out Rect toRect)) continue;
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;
Handles.DrawBezier(start, end, startTan, endTan,
new Color(0.82f, 0.82f, 0.88f, 1f), null, simple ? 2f : 3f);
}
}
Handles.EndGUI();
// 再画 block 方块
GUIStyle labelStyle = new GUIStyle(EditorStyles.boldLabel)
{
alignment = TextAnchor.MiddleCenter,
wordWrap = true,
richText = false,
fontSize = simple ? 9 : 11,
normal = { textColor = Color.white }
};
foreach (StoryBlockDefinition block in blocks)
{
if (block == null || !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()}";
GUI.Label(rect, label, labelStyle);
}
}
GUI.EndClip();
EditorGUILayout.HelpBox(
"在预览区内拖拽可平移视图。列 = 水平(右为正),行 = 垂直(下为正、上为负),(0,0) 位于左侧中部,支持负数与小数。",
MessageType.None);
}
/// <summary>
/// 处理预览区内的鼠标拖拽平移。
/// </summary>
private void HandlePreviewPanning(Rect area)
{
Event e = Event.current;
switch (e.type)
{
case EventType.MouseDown:
if (area.Contains(e.mousePosition))
{
_isPanningPreview = true;
e.Use();
}
break;
case EventType.MouseDrag:
if (_isPanningPreview)
{
_previewPan += e.delta;
GUI.changed = true;
e.Use();
}
break;
case EventType.MouseUp:
if (_isPanningPreview)
{
_isPanningPreview = false;
e.Use();
}
break;
}
}
/// <summary>
/// 预览中不同 block 类型的填充色。
/// </summary>
private static Color GetTypeColor(StoryBlockType type)
{
return type switch
{
StoryBlockType.Text => new Color(0.20f, 0.42f, 0.72f, 1f), // 蓝
StoryBlockType.Song => new Color(0.62f, 0.30f, 0.66f, 1f), // 紫
StoryBlockType.Tutorial => new Color(0.28f, 0.56f, 0.36f, 1f), // 绿
_ => new Color(0.4f, 0.4f, 0.4f, 1f)
};
}
#endif
}
}