Storyline+Dialog初步

This commit is contained in:
SoulliesOfficial
2026-07-05 16:08:23 -04:00
parent afa8a56e1d
commit d031afd075
464 changed files with 25716 additions and 4209 deletions

View File

@@ -0,0 +1,390 @@
using System.Collections.Generic;
using System.Linq;
using Ichni.Story.UI;
using Sirenix.OdinInspector;
using UniRx;
using UnityEngine;
namespace Ichni.Story
{
/// <summary>
/// 故事树控制器。从 <see cref="StoryData"/> 一次性全量构建所有 block 视图与连接线,
/// 布局由每个 block 的 (gridColumn, gridRow) 静态决定;每个 block 的
/// <see cref="StoryBlockState"/> 由"已完成集合 + 解锁条件"实时推导。
/// 存档只保存已完成 block 的 id 集合。
/// </summary>
public class StoryTreeController : MonoBehaviour
{
[Header("UI References")]
[Tooltip("blocks 与 connectors 共用的单一容器(即 ScrollRect 的 contentlocalScale 必须为 1")]
public RectTransform content;
public GameObject textBlockPrefab;
public GameObject songBlockPrefab;
public GameObject tutorialBlockPrefab;
public GameObject connectorPrefab;
[Header("Layout Settings")]
[Tooltip("网格单元格尺寸(决定相邻列/行的步距,全局统一)")]
public Vector2 cellSize = new Vector2(400f, 200f);
[Tooltip("相邻单元格之间的间隔(水平 x、垂直 y")]
public Vector2 cellSpacing = new Vector2(120f, 150f);
public float marginLeft = 50f;
public float marginRight = 50f;
public float marginTop = 50f;
public float marginBottom = 50f;
public float minContentWidth = 2560f;
public float minContentHeight = 1440f;
[Header("Runtime")]
public StoryBlockView currentBlock;
public List<StoryBlockView> blocks = new List<StoryBlockView>();
public List<BlockConnectorView> connectors = new List<BlockConnectorView>();
private string _chapterIndex;
private StoryData _storyData;
/// <summary>当前已构建章节的 StoryData供对话控制器切换 YarnProject 等使用)。</summary>
public StoryData ActiveStoryData => _storyData;
/// <summary>当前已构建章节的索引。</summary>
public string ActiveChapterIndex => _chapterIndex;
// 当前章节已完成的 block id 集合(状态推导的唯一进度来源)
private readonly HashSet<string> _completed = new HashSet<string>();
private void Start()
{
// 存档系统未就绪时跳过;正式流程由 StoryManager.OpenChapter 在进入剧情页时触发构建。
if (GameSaveManager.instance == null || GameSaveManager.instance.StorySaveModule == null)
return;
if (ChapterSelectionManager.instance != null &&
ChapterSelectionManager.instance.currentChapter != null)
{
BuildChapter(ChapterSelectionManager.instance.currentChapter.chapterIndex);
}
}
// ── Build ─────────────────────────────────────────────────────────────────
/// <summary>
/// 构建指定章节的故事树:全量生成所有 block 与连接线,状态由存档中的已完成集合推导。
/// </summary>
public void BuildChapter(string chapterIndex)
{
_chapterIndex = chapterIndex;
_storyData = StoryManager.instance != null ? StoryManager.instance.GetStoryData(chapterIndex) : null;
if (_storyData == null)
{
Debug.LogError($"[StoryTreeController] 找不到章节 '{chapterIndex}' 的 StoryData请在 StoryManager.storyDatas 中登记。");
return;
}
if (GameSaveManager.instance == null || GameSaveManager.instance.StorySaveModule == null)
{
Debug.LogWarning("[StoryTreeController] 存档系统尚未就绪GameSaveManager.StorySaveModule 为空),跳过构建。");
return;
}
ClearTree();
// 载入已完成集合(新章节为空)
ChapterStorySave save = GameSaveManager.instance.StorySaveModule.GetChapter(chapterIndex);
_completed.Clear();
foreach (string id in save.completedBlockIds)
_completed.Add(id);
// 全量生成所有 block位置按网格换算状态按已完成集合 + 解锁条件推导)
foreach (StoryBlockDefinition def in _storyData.blocks)
GenerateBlock(def);
// 全量生成所有连接线
foreach (StoryBlockDefinition def in _storyData.blocks)
foreach (string nextId in def.nextBlockIds)
GenerateConnector(def.blockId, nextId);
SetUpBackground();
// 连线在下一帧刷新(见 RefreshConnectors等待 Unity 完成 Canvas 布局,
// 使 block 端口的世界坐标稳定,避免端口坐标尚未生效时塌缩到同一点。
RefreshConnectors();
}
// ── Generation ──────────────────────────────────────────────────────────
/// <summary>
/// 按定义生成一个 block 视图:位置由网格换算,状态由推导得出。
/// </summary>
public StoryBlockView GenerateBlock(StoryBlockDefinition def)
{
GameObject prefab = def.blockType switch
{
StoryBlockType.Text => textBlockPrefab,
StoryBlockType.Song => songBlockPrefab,
StoryBlockType.Tutorial => tutorialBlockPrefab,
_ => null
};
if (prefab == null)
{
Debug.LogError($"[StoryTreeController] block 类型 {def.blockType} 未配置预制体。");
return null;
}
StoryBlockView view = Instantiate(prefab, content).GetComponent<StoryBlockView>();
if (view == null)
{
Debug.LogError($"[StoryTreeController] 预制体 '{prefab.name}' 缺少 StoryBlockView 组件。");
return null;
}
view.Initialize(def, GetGridPosition(def), DeriveState(def));
blocks.Add(view);
return view;
}
/// <summary>
/// 在两个已存在的 block 之间生成连接线。
/// </summary>
public void GenerateConnector(string fromBlockId, string toBlockId)
{
StoryBlockView from = GetBlockView(fromBlockId);
StoryBlockView to = GetBlockView(toBlockId);
if (from == null || to == null)
{
Debug.LogWarning($"[StoryTreeController] 无法连线:'{fromBlockId}' -> '{toBlockId}'block 不存在)。");
return;
}
// 连接线与 block 同处一个容器content置为第一个子物体渲染在所有 block 之后方。
GameObject connectorObject = Instantiate(connectorPrefab, content);
connectorObject.transform.SetAsFirstSibling();
BlockConnectorView connector = connectorObject.GetComponent<BlockConnectorView>();
if (connector == null)
{
Debug.LogError($"[StoryTreeController] 连接线预制体 '{connectorPrefab.name}' 缺少 BlockConnectorView 组件。");
Destroy(connectorObject);
return;
}
// 仅记录起止 block 引用;曲线在布局刷新后由 RefreshConnectors 统一计算。
connector.startBlock = from;
connector.endBlock = to;
connectors.Add(connector);
}
// ── State / Unlocking ─────────────────────────────────────────────────────
/// <summary>
/// 依据"已完成集合 + 解锁条件"推导单个 block 的状态。
/// 已完成 → Completed否则满足解锁条件 → Current否则 → Locked。
/// 解锁条件为空视为无条件满足(章节起始即为 Current
/// </summary>
private StoryBlockState DeriveState(StoryBlockDefinition def)
{
if (_completed.Contains(def.blockId))
return StoryBlockState.Completed;
if (def.unlockCondition.IsSatisfied(GetVariable, IsBlockCompleted))
return StoryBlockState.Current;
return StoryBlockState.Locked;
}
/// <summary>
/// 重新推导并应用所有 block 的状态(在完成某 block 或变量变化后调用)。
/// </summary>
public void RefreshAllStates()
{
foreach (StoryBlockView view in blocks)
{
StoryBlockDefinition def = _storyData.GetBlock(view.blockId);
if (def == null) continue;
view.ApplyState(DeriveState(def));
}
}
/// <summary>
/// 标记某 block 完成,推导重算并保存。对话系统(阶段 2在对话结束时调用。
/// </summary>
public void OnBlockCompleted(string blockId)
{
if (!_completed.Add(blockId))
return; // 已完成,无需处理
RefreshAllStates();
SaveChapter();
}
private bool IsBlockCompleted(string blockId) => _completed.Contains(blockId);
// 阶段 2 将改由 StoryVariableStorage 提供;此处从全局变量存档读取作占位
private int GetVariable(string variableName)
{
StoryVariablesSave vars = GameSaveManager.instance.StorySaveModule.variables;
if (vars.floatVariables.TryGetValue(variableName, out float f))
return Mathf.RoundToInt(f);
if (vars.boolVariables.TryGetValue(variableName, out bool b))
return b ? 1 : 0;
return 0;
}
// ── Persistence ─────────────────────────────────────────────────────────
/// <summary>
/// 保存当前章节进度(已完成 block 集合)。选项由对话系统维护、不在此覆盖。
/// </summary>
[Button]
public void SaveChapter()
{
if (string.IsNullOrEmpty(_chapterIndex)) return;
ChapterStorySave save = GameSaveManager.instance.StorySaveModule.GetChapter(_chapterIndex);
save.chapterIndex = _chapterIndex;
save.completedBlockIds = _completed.ToList();
GameSaveManager.instance.StorySaveModule.SaveChapter(save);
}
// ── Layout ────────────────────────────────────────────────────────────────
/// <summary>
/// 将 block 的网格坐标 (gridColumn, gridRow) 换算为 BlockContainer 中的 anchoredPosition。
/// 约定左侧中线为原点列向右为正行向下为正、向上为负anchoredPosition.y 取负)。
/// gridColumn / gridRow 支持负数与小数,可用于微调 block 位置。
/// </summary>
private Vector2 GetGridPosition(StoryBlockDefinition def)
{
float x = marginLeft + def.gridColumn * (cellSize.x + cellSpacing.x);
float y = -def.gridRow * (cellSize.y + cellSpacing.y);
return new Vector2(x, y);
}
/// <summary>
/// 根据 block 分布计算并设置 content(BlockContainer) 尺寸。
/// block 采用左中 pivotanchoredPosition.x 为左边缘y 相对垂直中线(上正下负)。
/// </summary>
public void SetUpBackground()
{
if (blocks.Count == 0)
{
content.sizeDelta = new Vector2(minContentWidth, minContentHeight);
return;
}
float maxRight = 0f;
float maxVertical = 0f; // 距垂直中线的最大延伸(上下取较大者,用于对称扩展 content 高度)
foreach (StoryBlockView block in blocks)
{
RectTransform rect = block.blockRect;
Vector2 pos = rect.anchoredPosition;
Vector2 size = rect.sizeDelta;
float rightEdge = pos.x + size.x;
float topExtent = pos.y + size.y * 0.5f;
float bottomExtent = -(pos.y - size.y * 0.5f);
if (rightEdge > maxRight) maxRight = rightEdge;
if (topExtent > maxVertical) maxVertical = topExtent;
if (bottomExtent > maxVertical) maxVertical = bottomExtent;
}
float width = Mathf.Max(maxRight + marginRight, minContentWidth);
float height = Mathf.Max(maxVertical * 2f + marginTop + marginBottom, minContentHeight);
content.sizeDelta = new Vector2(width, height);
}
/// <summary>
/// 重算所有连接线曲线(在 block 位置确定后调用)。
/// </summary>
public void RefreshConnectors()
{
Observable.NextFrame().Subscribe(_ =>
{
foreach (BlockConnectorView connector in connectors)
{
connector.SetCurve();
}
}).AddTo(this);
}
// ── Helpers / Debug ─────────────────────────────────────────────────────
public StoryBlockView GetBlockView(string blockId) =>
blocks.FirstOrDefault(b => b.blockId == blockId);
private void ClearTree()
{
foreach (StoryBlockView block in blocks)
if (block != null) Destroy(block.gameObject);
blocks.Clear();
foreach (BlockConnectorView connector in connectors)
if (connector != null) Destroy(connector.gameObject);
connectors.Clear();
if (content != null)
content.sizeDelta = Vector2.zero;
}
/// <summary>
/// 编辑器测试用:以当前章节重新构建故事树。
/// </summary>
[Button]
public void DebugBuildCurrentChapter()
{
if (ChapterSelectionManager.instance != null &&
ChapterSelectionManager.instance.currentChapter != null)
{
BuildChapter(ChapterSelectionManager.instance.currentChapter.chapterIndex);
}
else
{
Debug.LogWarning("[StoryTreeController] 当前没有选中的章节。");
}
}
/// <summary>
/// 编辑器测试用:模拟完成当前选中的 block验证解锁重算与存档阶段 2 前用于替代对话。
/// </summary>
[Button]
public void DebugCompleteCurrentBlock()
{
if (currentBlock == null)
{
Debug.LogWarning("[StoryTreeController] currentBlock 为空,请先点击一个 block。");
return;
}
OnBlockCompleted(currentBlock.blockId);
}
/// <summary>
/// 编辑器测试用:清空当前章节进度并重建(全部回到未完成的初始推导状态)。
/// </summary>
[Button]
public void DebugResetChapterProgress()
{
if (string.IsNullOrEmpty(_chapterIndex))
{
Debug.LogWarning("[StoryTreeController] 尚未构建任何章节。");
return;
}
_completed.Clear();
SaveChapter();
BuildChapter(_chapterIndex);
}
}
}