using Sirenix.OdinInspector; using UnityEngine; using UnityEngine.UI; namespace Ichni.Story.UI { /// /// 故事树中单个 block 的视图基类。负责通用的定位、状态视觉与点击分发。 /// 具体类型(文本 / 音乐 / 教程)由子类实现 。 /// public abstract class StoryBlockView : SerializedMonoBehaviour { [Header("Block State")] public string blockId; public StoryBlockState state; [Header("Common References")] public RectTransform blockRect; public RectTransform inPort; public RectTransform outPort; public Button button; // 关联的定义,供子类读取类型专属字段 protected StoryBlockDefinition definition; /// /// 初始化 block 视图。逻辑坐标已由控制器按网格换算为 /// (以 BlockContainer 左侧中线为原点、x 向右为正、y 向下为正的 anchoredPosition)。 /// /// Block 定义(提供类型专属数据与剧情配置)。 /// 已换算好的 anchoredPosition。 /// 推导得到的 block 状态。 public virtual void Initialize(StoryBlockDefinition def, Vector2 position, StoryBlockState blockState) { definition = def; blockId = def.blockId; state = blockState; // 统一坐标约定:以 BlockContainer 的左侧中线为原点,Block 的中心向右为 +x、向下为 +y。 // 所有类型使用中心 Pivot,使相同 gridColumn 始终代表同一条视觉中心列, // 不会再因 Important / Secondary / Song / Tutorial 的宽度不同而产生左右间距不对称。 blockRect.anchorMin = new Vector2(0f, 0.5f); blockRect.anchorMax = new Vector2(0f, 0.5f); blockRect.pivot = new Vector2(0.5f, 0.5f); // 不在基类覆盖 sizeDelta:Block 的尺寸是 Prefab 的视觉契约。 // Important / Secondary TextBlock,以及未来不同尺寸的 Song / Tutorial Block, // 都可保留各自的宽高;StoryTreeController 会据此计算 Content 边界和连接线端口。 blockRect.anchoredPosition = position; if (button != null) button.onClick.AddListener(HandleClick); ApplyState(state); } /// /// 应用状态并更新视觉。基类默认按状态切换按钮可交互性; /// 子类可 override 以更新锁定图标、颜色等具体表现。 /// public virtual void ApplyState(StoryBlockState newState) { state = newState; if (button != null) button.interactable = state != StoryBlockState.Locked && state != StoryBlockState.Forbidden; } private void HandleClick() { if (state == StoryBlockState.Locked || state == StoryBlockState.Forbidden) return; if (StoryManager.instance != null && StoryManager.instance.treeController != null) StoryManager.instance.treeController.currentBlock = this; OnClicked(); } /// /// 点击(非锁定状态)时的具体行为,由子类实现。 /// protected abstract void OnClicked(); } }