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 { /// /// 单个章节的故事树数据,以 ScriptableObject 形式存储所有 block 定义与关联资产引用。 /// 布局完全由每个 block 的 (gridColumn, gridRow) 静态决定,运行时全量展示。 /// [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 blocks = new List(); // ── Queries ───────────────────────────────────────────────────────────── /// /// 根据 blockId 获取 block 定义,未找到时返回 null。 /// public StoryBlockDefinition GetBlock(string blockId) { foreach (StoryBlockDefinition block in blocks) { if (block.blockId == blockId) return block; } return null; } /// /// 获取指定 blockId 的所有直接后继 block 定义。 /// public IEnumerable 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; /// /// 在 Inspector 顶部绘制只读的故事树结构预览(流程图样式,连线左右相连)。 /// 提供"详细 / 简略"两种可拖拽平移的视图;按 (gridColumn, gridRow) 摆放 block, /// 支持负数与小数坐标,(0,0) 对应左侧中部。 /// [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 cellRects = new Dictionary(); 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); } /// /// 处理预览区内的鼠标拖拽平移。 /// 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; } } /// /// 预览中不同 block 类型的填充色。 /// 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 } }