2026-07-05 16:08:23 -04:00
|
|
|
|
using Sirenix.OdinInspector;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.UI;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Ichni.Story.UI
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 故事树中单个 block 的视图基类。负责通用的定位、状态视觉与点击分发。
|
|
|
|
|
|
/// 具体类型(文本 / 音乐 / 教程)由子类实现 <see cref="OnClicked"/>。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
// 运行时实际承载点击事件的 Button。TextBlock 会在切换状态视觉时替换其背景,
|
|
|
|
|
|
// 因此不能只依赖 Prefab 初始序列化的 button 引用。
|
|
|
|
|
|
private Button _boundInteractionButton;
|
|
|
|
|
|
|
2026-07-05 16:08:23 -04:00
|
|
|
|
// 关联的定义,供子类读取类型专属字段
|
|
|
|
|
|
protected StoryBlockDefinition definition;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 初始化 block 视图。逻辑坐标已由控制器按网格换算为 <paramref name="position"/>
|
|
|
|
|
|
/// (以 BlockContainer 左侧中线为原点、x 向右为正、y 向下为正的 anchoredPosition)。
|
|
|
|
|
|
/// </summary>
|
2026-07-20 16:56:04 -04:00
|
|
|
|
/// <param name="def">Block 定义(提供类型专属数据与剧情配置)。</param>
|
2026-07-05 16:08:23 -04:00
|
|
|
|
/// <param name="position">已换算好的 anchoredPosition。</param>
|
|
|
|
|
|
/// <param name="blockState">推导得到的 block 状态。</param>
|
|
|
|
|
|
public virtual void Initialize(StoryBlockDefinition def, Vector2 position, StoryBlockState blockState)
|
|
|
|
|
|
{
|
|
|
|
|
|
definition = def;
|
|
|
|
|
|
blockId = def.blockId;
|
|
|
|
|
|
state = blockState;
|
|
|
|
|
|
|
2026-07-20 16:56:04 -04:00
|
|
|
|
// 统一坐标约定:以 BlockContainer 的左侧中线为原点,Block 的中心向右为 +x、向下为 +y。
|
|
|
|
|
|
// 所有类型使用中心 Pivot,使相同 gridColumn 始终代表同一条视觉中心列,
|
|
|
|
|
|
// 不会再因 Important / Secondary / Song / Tutorial 的宽度不同而产生左右间距不对称。
|
2026-07-05 16:08:23 -04:00
|
|
|
|
blockRect.anchorMin = new Vector2(0f, 0.5f);
|
|
|
|
|
|
blockRect.anchorMax = new Vector2(0f, 0.5f);
|
2026-07-20 16:56:04 -04:00
|
|
|
|
blockRect.pivot = new Vector2(0.5f, 0.5f);
|
|
|
|
|
|
|
|
|
|
|
|
// 不在基类覆盖 sizeDelta:Block 的尺寸是 Prefab 的视觉契约。
|
|
|
|
|
|
// Important / Secondary TextBlock,以及未来不同尺寸的 Song / Tutorial Block,
|
|
|
|
|
|
// 都可保留各自的宽高;StoryTreeController 会据此计算 Content 边界和连接线端口。
|
2026-07-05 16:08:23 -04:00
|
|
|
|
blockRect.anchoredPosition = position;
|
|
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
BindInteractionButton(button);
|
2026-07-05 16:08:23 -04:00
|
|
|
|
|
|
|
|
|
|
ApplyState(state);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 应用状态并更新视觉。基类默认按状态切换按钮可交互性;
|
|
|
|
|
|
/// 子类可 override 以更新锁定图标、颜色等具体表现。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public virtual void ApplyState(StoryBlockState newState)
|
|
|
|
|
|
{
|
|
|
|
|
|
state = newState;
|
|
|
|
|
|
|
2026-07-21 15:24:42 -04:00
|
|
|
|
RefreshButtonInteractable();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 绑定当前 Block 的交互 Button。
|
|
|
|
|
|
/// TextBlock 更换状态背景时会销毁旧背景并生成新的 Button;必须先解绑旧对象、
|
|
|
|
|
|
/// 再绑定新对象,才能避免点击监听丢失或因重复绑定而触发多次。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
protected void BindInteractionButton(Button newButton)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_boundInteractionButton == newButton)
|
|
|
|
|
|
{
|
|
|
|
|
|
RefreshButtonInteractable();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (_boundInteractionButton != null)
|
|
|
|
|
|
_boundInteractionButton.onClick.RemoveListener(HandleClick);
|
|
|
|
|
|
|
|
|
|
|
|
_boundInteractionButton = newButton;
|
|
|
|
|
|
button = newButton;
|
|
|
|
|
|
|
|
|
|
|
|
if (_boundInteractionButton != null)
|
|
|
|
|
|
_boundInteractionButton.onClick.AddListener(HandleClick);
|
|
|
|
|
|
|
|
|
|
|
|
RefreshButtonInteractable();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 依据剧情状态刷新当前交互 Button。Completed 仍允许回顾,只有 Locked 与
|
|
|
|
|
|
/// Forbidden 状态禁止点击。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
protected void RefreshButtonInteractable()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_boundInteractionButton != null)
|
|
|
|
|
|
_boundInteractionButton.interactable =
|
|
|
|
|
|
state != StoryBlockState.Locked && state != StoryBlockState.Forbidden;
|
2026-07-05 16:08:23 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void HandleClick()
|
|
|
|
|
|
{
|
2026-07-20 16:56:04 -04:00
|
|
|
|
if (state == StoryBlockState.Locked || state == StoryBlockState.Forbidden)
|
2026-07-05 16:08:23 -04:00
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
if (StoryManager.instance != null && StoryManager.instance.treeController != null)
|
|
|
|
|
|
StoryManager.instance.treeController.currentBlock = this;
|
|
|
|
|
|
|
|
|
|
|
|
OnClicked();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 点击(非锁定状态)时的具体行为,由子类实现。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
protected abstract void OnClicked();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|