Files
ichni_Official/docs/Plan 剧情系统重构(故事树 + Yarn Spinner 对话,VN 风格).md
SoulliesOfficial 7b7d069b84 StorySystem
2026-07-07 01:28:27 -04:00

439 lines
34 KiB
Markdown
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.
## Plan: 剧情系统重构(故事树 + Yarn Spinner 对话VN 风格)
## Context
**目标项目:** `ichni Official`Unity 6000.3URPWwise。剧情系统为 Official 独有(`IchniCreatorStudio``Story` 模块),因此本次重构**不涉及双项目同步**,也不涉及 BM 类。
**要做什么、为什么:** 现有剧情系统在引入 Yarn Spinner 之前编写,对话处理(`DialogManager` + 自定义文本解析 + DynamicExpresso 解释器 + `DialogUI`)已过时。本计划**从零重构**整个剧情系统:数据、故事树 UI、存档、对话全部重来改用 **Yarn Spinner 3.2.1** 驱动对话,并采用**视觉小说VN风格**呈现(多角色立绘、打字机、表现类 Yarn 命令),对话文本经 **Unity Localization** 统一管理多语言。
**当前系统关键事实(将被替换):**
- `StoryData``SerializedScriptableObject`)按章节定义 `DialogBlockData`/`SongBlockData`/`TutorialBlockData`/`initialBlocks``blockName`/`blockID`/`blockSize`/`blockPosition`/`nextBlocks`)。
- `Storyline``MonoBehaviour`)从 `StoryData` 或 ES3 存档构建节点图,管理 `storyBlocks`/`connectors`,按章节存取;`StoryBlockUIBase``DialogBlockUI`/`SongBlockUI`/`TutorialBlockUI`,点击判断 `StoryBlockState { Locked, Current, Completed }`
- `DialogManager`:自定义解析器(`[段落]{函数}``speaker:句子``$Choice(...)``$Condition{...}`、内联 `{@表达式}`),从 `Resources/Story/[chapter]/Dialogs/` 加载 `TextAsset`
- `StoryInterpreters`DynamicExpresso`SetVariable`/`GetVariable`/`GenerateDialogBlock`/`GenerateSongBlock`/`SetUnlockKey`——不仅分支对话,还动态生成故事树节点、解锁歌曲。
- `DialogUI``DialogUIPage`/`DialogContentFrame`/`DialogTextUI`/`ChoiceGroupUI`/`ChoiceButtonUI`历史记录式scrollback呈现点击推进。
- 存档:`GameSaveManager.StorySaveModule`block 存档、`storyVariables``selectedChoices`ES3 持久化到 `persistentDataPath/StorySaves/[Chapter].json`
- 剧情系统实例位于 `MenuScene.unity``StoryManager``DialogManager` 均在其中)。
**Yarn Spinner 3.2.1 关键 API已核实包源码**
- `DialogueRunner``StartDialogue(nodeName)``UnityEvent onDialogueStart` / `onDialogueComplete``AddCommandHandler(name, Delegate)``dialogueViews``DialoguePresenterBase` 列表)。异步架构(`YarnTask`)。
- `DialoguePresenterBase``Runtime/Views/DialoguePresenterBase.cs`override `YarnTask RunLineAsync(LocalizedLine, LineCancellationToken)``YarnTask<DialogueOption?> RunOptionsAsync(DialogueOption[], LineCancellationToken)``YarnTask OnDialogueStartedAsync()``YarnTask OnDialogueCompleteAsync()`;可选 `OnNodeEnter/OnNodeExit`
- `VariableStorageBehaviour``Runtime/Storage/VariableStorageBehaviour.cs`):自定义变量存储基类。
- `UnityLocalisedLineProvider``Runtime/LineProviders/UnityLocalisedLineProvider.cs``.Installed.cs` 分部类需安装 `com.unity.localization` 才编译):桥接 Unity Localization String Table。
- 表现命令用 `[YarnCommand("name")]` 静态/实例方法或 `AddCommandHandler` 注册。
**设计决策(已与你确认):**
1. **全部从零重构**数据、UI、存档
2. **VN 风格呈现** + 多角色立绘 + 表现类 Yarn 命令(如立绘跳动/移动)。
3. **混合式解锁**:故事树默认在 `StoryData` 静态定义;同时允许 Yarn 命令在特殊情况下插入/解锁 block。
4. **完整表现命令集** + 支持多立绘的 VN Presenter立绘渲染**做抽象**(当前 Sprite未来可换 Live2D/Spine
5. **对话本地化桥接 Unity Localization**(弃用 I2由 Unity Localization 统一管理对话与 UI 文本。
6. **仅优先文本块TextBlock**Song/Tutorial block 作为占位符。
**新目录结构(`/ichni Official/Assets/Scripts/Story/`**
```
Story/
├── Data/ # 数据与 ScriptableObject
│ ├── StoryData.cs # 章节故事树定义(重建)
│ ├── StoryBlockDefinition.cs # block 定义基类 + Text/Song/Tutorial + 解锁条件
│ ├── CharacterData.cs # 单个角色定义id/显示名Key/立绘/情绪)
│ └── CharacterRegistry.cs # 角色注册表id -> CharacterData
├── Tree/ # 故事树运行时 + UI
│ ├── StoryManager.cs # 协调器(重建)
│ ├── StoryTreeController.cs # 构建树/状态/解锁重算/存取(原 Storyline
│ ├── StoryBlockView.cs # block 视图基类(原 StoryBlockUIBase
│ ├── TextBlockView.cs # 文本块(原 DialogBlockUI
│ ├── SongBlockView.cs # 占位
│ ├── TutorialBlockView.cs # 占位
│ └── BlockConnectorView.cs # 连接线(原 BlockConnectorUI
├── Dialogue/ # Yarn 集成
│ ├── StoryDialogueController.cs # block → DialogueRunner 桥接
│ ├── VNDialoguePresenter.cs # DialoguePresenterBase 实现VN
│ ├── VNPortraitStage.cs # 立绘舞台(多槽位)
│ ├── IPortraitRenderer.cs # 立绘渲染抽象接口
│ ├── SpritePortraitRenderer.cs # Sprite 实现
│ ├── StoryVariableStorage.cs # VariableStorageBehaviour 桥接存档
│ ├── StoryPresentationCommands.cs # [YarnCommand] 表现命令
│ ├── StoryTreeCommands.cs # [YarnCommand] 解锁/生成 block/解锁歌曲
│ └── StoryFunctions.cs # [YarnFunction] 条件辅助
├── Save/
│ └── StorySave.cs # 存档 DTO重建
└── UI/
└── StoryUIPage.cs # 故事树页面(适配保留)
```
参考页面:@ichni-Project-Architecture-Report
------
## Modifications
### A. Scripts
#### A1. `Story/Data/StoryBlockDefinition.cs`(新建)
- **描述:** 章节故事树中单个 block 的纯数据定义 + 解锁条件。路径 `/ichni Official/Assets/Scripts/Story/Data/StoryBlockDefinition.cs`
- **遵循模式:** 参考旧 `StoryData.cs``[Serializable]`/`[InlineProperty]`/`[FoldoutGroup("$blockName")]` Odin 风格。
- **内容:**
- `enum StoryBlockType { Text, Song, Tutorial }`
- `[Serializable] class StoryBlockDefinition``string blockId`(唯一,替代旧 `blockName`/`blockID` 二元)、`StoryBlockType type``Vector2 position``Vector2 size``StoryBlockState initialState``List<string> nextBlockIds``UnlockCondition unlockCondition`
- 类型专属数据(用 Odin `[ShowIf]``type` 显示):`string yarnNodeName` + `string titleKey`Text`string songName`Song 占位);`string tutorialName`Tutorial 占位)。
- `[Serializable] class UnlockCondition``List<string> requiredCompletedBlockIds``List<VariableCondition> variableConditions`;方法 `bool IsSatisfied(Func<string,int> getVar, Func<string,bool> isCompleted)`
- `[Serializable] class VariableCondition``string variableName``enum Comparison { Equal, NotEqual, Greater, GreaterOrEqual, Less, LessOrEqual }``int value``bool Evaluate(int actual)`
- **公共 API** `bool UnlockCondition.IsSatisfied(...)``bool VariableCondition.Evaluate(int)`
#### A2. `Story/Data/StoryData.cs`(重建,覆盖现有)
- **描述:** 章节故事树 ScriptableObject。路径 `/ichni Official/Assets/Scripts/Story/Data/StoryData.cs`
- **内容:** `[CreateAssetMenu(menuName="Ichni/Story/StoryData")] class StoryData : SerializedScriptableObject``string chapterIndex``List<StoryBlockDefinition> blocks``YarnProject yarnProject`(该章节编译产物引用)、`AssetReference/string` 本地化 String Table 集合名(供 Presenter/Provider 使用)。
- **公共 API** `StoryBlockDefinition GetBlock(string blockId)``IEnumerable<StoryBlockDefinition> GetInitialBlocks()``IEnumerable<StoryBlockDefinition> GetNextBlocks(string blockId)`
- **依赖:** A1、Yarn `YarnProject`
#### A3. `Story/Data/CharacterData.cs` + `CharacterRegistry.cs`(新建)
- **描述:** 角色立绘/情绪注册表VN Presenter 需要)。路径 `/ichni Official/Assets/Scripts/Story/Data/`
- `**CharacterData``[Serializable]`** `string characterId`(与 Yarn 行的 speaker 名对应)、`string displayNameKey`Unity Localization 键)、`PortraitStyle portraitStyle``enum { Sprite, Live2D, Spine }` 预留)、`SerializedDictionary<string, Sprite> emotionSprites`Sprite 方案)、`GameObject dynamicPortraitPrefab`Live2D/Spine 预留,实现 `IPortraitRenderer`)。
- `**CharacterRegistry``[CreateAssetMenu(menuName="Ichni/Story/CharacterRegistry")] SerializedScriptableObject`** `Dictionary<string, CharacterData> characters``bool TryGet(string id, out CharacterData data)`
- **说明:** 立绘表现不直接耦合 Sprite交由 `IPortraitRenderer`(见 A9读取 `CharacterData` 决定渲染方式,未来接入 Live2D/Spine 只需新增 renderer 实现。
#### A4. `Story/Save/StorySave.cs`(重建,覆盖现有)
- **描述:** 存档 DTO。路径 `/ichni Official/Assets/Scripts/Story/Save/StorySave.cs`
- **内容:** `[Serializable] class StoryBlockSave { string blockId; Vector2 position; StoryBlockState state; List<string> nextBlockIds; }`(合并旧的按类型拆分的多个 Save 类为单一带 `blockId` 的 DTO类型信息从 `StoryData` 反查,存档只存运行态);`[Serializable] class ChapterStorySave { string chapterIndex; List<StoryBlockSave> blocks; }`
- **依赖:** A1`StoryBlockState`、block 类型引用)。
#### A5. `Story/Tree/StoryManager.cs`(重建,覆盖现有)
- **描述:** 剧情系统协调器(单例)。路径 `/ichni Official/Assets/Scripts/Story/Tree/StoryManager.cs`
- **遵循模式:** 参考现有 `StoryManager` 单例 + `[Button] ClearAllStorySave()`;建议继承 `SLSUtilities.General.Singleton<StoryManager>`(项目已有单例基类)。
- **内容:** 持有 `StoryTreeController treeController``StoryDialogueController dialogueController``StoryUIPage storyUIPage``CharacterRegistry characterRegistry``Dictionary<string, StoryData> storyDatas`。保留 `enum StoryBlockState { Locked, Current, Completed }`(移到此文件或独立文件)。保留 `ClearAllStorySave()`(改为调用新 `StorySaveModule` 接口)。
- **公共 API** `void OpenChapter(string chapterIndex)`(构建树)、`StoryData GetStoryData(string chapterIndex)`
#### A6. `Story/Tree/StoryTreeController.cs`(重建自 `Storyline.cs`
- **描述:** 从 `StoryData`/存档构建 block 视图与连接线,管理状态、解锁重算与存取。路径 `/ichni Official/Assets/Scripts/Story/Tree/StoryTreeController.cs`
- **遵循模式:** 参考旧 `Storyline.cs``SetUpStoryline`/`Generate*Block`/`GenerateConnector`/`SaveStoryline`/`ResetStory` 结构与 `ScrollRect content` 布局逻辑。
- **公共 API**
- `void BuildChapter(string chapterIndex)`:优先 `LoadChapter`,无存档则 `ResetToInitial`
- `StoryBlockView GenerateBlock(StoryBlockDefinition def)` / `void GenerateConnector(string fromBlockId, string toBlockId)`
- `void OnBlockCompleted(string blockId)`:设该 block `Completed`,遍历所有 `Locked` block 用 `UnlockCondition.IsSatisfied(...)` 重算,满足者置 `Current`(并按需生成尚未显示的后继 block/连接线),刷新连接线,保存。
- `void UnlockBlock(string blockId)` / `void InsertBlock(StoryBlockDefinition def, string fromBlockId)`:供 Yarn 树命令A11调用实现"混合式"动态解锁/插入。
- `void SaveChapter()` / `void LoadChapter(string chapterIndex)` / `void ResetToInitial(string chapterIndex)`
- `StoryBlockView currentBlock { get; }``IReadOnlyList<StoryBlockView> blocks`
- **解锁判定:** `IsSatisfied``getVar` 委托指向 `StoryVariableStorage`A8`isCompleted` 指向已完成 block 集合。
- **依赖:** A1、A2、A4、A8、`GameSaveManager.StorySaveModule`(重建后,见 A13`ChapterSelectionManager`
#### A7. `Story/Tree/StoryBlockView.cs` + `TextBlockView.cs` + `SongBlockView.cs` + `TutorialBlockView.cs` + `BlockConnectorView.cs`(重建自 Blocks/*
- `**StoryBlockView`(抽象,`MonoBehaviour`** `string blockId``StoryBlockState state``RectTransform inPort/outPort``Button button``virtual void ApplyState(StoryBlockState)`(更新视觉:锁定/当前/已完成外观);`abstract StoryBlockSave GetSave()``abstract void OnClicked()`
- `**TextBlockView`** 覆盖 `OnClicked()``state == Locked` → 忽略;否则 `StoryManager.instance.dialogueController.PlayBlock(this)`(传 `yarnNodeName`)。参考旧 `DialogBlockUI``button.onClick` + 状态判断。
- `**SongBlockView` / `TutorialBlockView`** 占位实现——显示标题/状态,`OnClicked()` 暂仅 `Debug.Log`(保留旧 `SongBlockUI`/`TutorialBlockUI` 视觉字段,交互逻辑注释或占位)。
- `**BlockConnectorView`** 沿用旧 `BlockConnectorUI`,用 `UILineRenderer``outPort``inPort` 画曲线;`SetCurve()`
- **依赖:** A6、A9对话触发
#### A8. `Story/Dialogue/StoryVariableStorage.cs`(新建)
- **描述:** 自定义 Yarn 变量存储,桥接 `GameSaveManager.StorySaveModule.storyVariables`ES3 持久化。路径 `/ichni Official/Assets/Scripts/Story/Dialogue/StoryVariableStorage.cs`
- **遵循模式:** 继承 `Yarn.Unity.VariableStorageBehaviour`,实现其抽象/虚方法(`SetValue`/`TryGetValue`/`Clear`/`Contains` 等——**实现步骤时先 `printAsset` 或 view `VariableStorageBehaviour.cs` 核对当前签名**)。
- **要点:** Yarn 变量以 `$` 前缀命名;本存储把布尔/数值/字符串映射到 `storyVariables`(现为 `Dictionary<string,int>`,需扩展为支持多类型,见 A13。变量读写触发 `StorySaveModule` 持久化。
- **公共 API供 A6 解锁判定复用):** `int GetIntVariable(string name)``bool HasVariable(string name)`
- **依赖:** A13`StorySaveModule`)。
#### A9. `Story/Dialogue/IPortraitRenderer.cs` + `SpritePortraitRenderer.cs`(新建)
- **描述:** 立绘渲染抽象 + Sprite 实现。路径 `/ichni Official/Assets/Scripts/Story/Dialogue/`
- `**IPortraitRenderer`** `void Show(CharacterData character, string emotion)``void Hide()``void SetEmotion(string emotion)``void MoveTo(Vector2 anchoredPos, float duration)``void PlayEffect(string effect, float intensity)`(如 "jump"/"shake"/"nod")、`void SetActiveVisual(bool speaking)`(说话者高亮/变暗)。
- `**SpritePortraitRenderer``MonoBehaviour, IPortraitRenderer`** 持有 `UnityEngine.UI.Image``Show``CharacterData.emotionSprites` 取 Sprite`MoveTo`/`PlayEffect` 用 DOTween项目已含 DOTweenPro实现跳动/移动/晃动。
- **说明:** 未来 Live2D/Spine 只需新增实现 `IPortraitRenderer` 的组件,`CharacterData.portraitStyle`/`dynamicPortraitPrefab` 决定实例化哪个 renderer。
#### A10. `Story/Dialogue/VNPortraitStage.cs`(新建)
- **描述:** 管理多立绘槽位(如 Left/Center/Right按 characterId 分配/复用 renderer。路径 `/ichni Official/Assets/Scripts/Story/Dialogue/VNPortraitStage.cs`
- **公共 API** `void ShowPortrait(string characterId, string slot, string emotion)``void HidePortrait(string characterId)``void HideAll()``void MovePortrait(string characterId, string slot, float duration)``void PlayEffect(string characterId, string effect, float intensity)``void HighlightSpeaker(string characterId)`
- **内部:** `Dictionary<string, IPortraitRenderer> activePortraits`;依据 `CharacterData.portraitStyle` 实例化对应 renderer 到槽位。
- **依赖:** A3、A9。
#### A11. `Story/Dialogue/StoryPresentationCommands.cs`(新建)
- **描述:** 表现类 Yarn 命令集合(`[YarnCommand]`)。路径 `/ichni Official/Assets/Scripts/Story/Dialogue/StoryPresentationCommands.cs`
- **命令集(完整表现集):** 均委托到 `VNPortraitStage`/`VNDialoguePresenter`
- `<<show_portrait characterId slot emotion>>` / `<<hide_portrait characterId>>` / `<<hide_all_portraits>>`
- `<<move_portrait characterId slot duration>>` / `<<jump_portrait characterId intensity>>` / `<<shake_portrait characterId intensity>>` / `<<set_emotion characterId emotion>>`
- `<<set_background bgId duration>>`(背景切换)
- `<<wait seconds>>`Yarn 内置也可,视需要)
- **注册方式:** 实例方法用 `[YarnCommand("show_portrait")]` 挂到与 `DialogueRunner` 同场景的组件上Yarn 自动发现),或在 `StoryDialogueController.Awake``dialogueRunner.AddCommandHandler(...)` 显式注册。**实现步骤时先核对 `DialogueRunner.ActionRegistration.cs``AddCommandHandler` 重载与 `[YarnCommand]` 自动注册要求。**
- **依赖:** A10、A12。
#### A12. `Story/Dialogue/StoryTreeCommands.cs`(新建)
- **描述:** 影响故事树/解锁的 Yarn 命令(替代旧 `StoryInterpreters``GenerateDialogBlock`/`GenerateSongBlock`/`SetUnlockKey`)。路径 `/ichni Official/Assets/Scripts/Story/Dialogue/StoryTreeCommands.cs`
- **命令集:**
- `<<unlock_block blockId>>``StoryTreeController.UnlockBlock`
- `<<insert_block blockId>>` → 从当前 block 生成后继("混合式"动态插入)→ `StoryTreeController.InsertBlock`
- `<<unlock_song songUnlockKey>>` → 复用旧 `SetUnlockKey` 逻辑:写入 `SongSaveModule.storyUnlockKeys`,并把"解锁提示消息"入队到对话结束后执行(见 A14 结束回调)。
- **依赖:** A6、A14、`GameSaveManager.SongSaveModule``ChapterSelectionManager`
#### A13. `Scripts/Saving/GameSaveManager.cs` → `StorySaveModule`(重建剧情存档模块)
- **描述:** 重建 `StorySaveModule`,适配新 DTO 与多类型变量。路径 `/ichni Official/Assets/Scripts/Saving/GameSaveManager.cs``StorySaveModule` 分部/内嵌类)。
- **改动:**
- block 存档改用 `ChapterStorySave`/`StoryBlockSave`A4ES3 存 `persistentDataPath/StorySaves/[Chapter].json`(沿用旧路径约定)。
- `storyVariables` 扩展为支持 Yarn 多类型float/bool/string——建议 `Dictionary<string, string>` 序列化 + 类型标记,或三个分字典;供 `StoryVariableStorage`A8读写。
- 保留 `selectedChoices` 语义Yarn 选项选择记录),如需回放已完成对话可保留(见"实现说明")。
- 保留 `SongSaveModule.storyUnlockKeys` 相关接口A12 依赖)。
- `ClearAllStoryline()` / `ClearStoryKeys()` 接口保留,供 `StoryManager.ClearAllStorySave` 调用。
- **注意:** `GameSaveManager` 含其它模块(`SongSaveModule`),改动**仅限剧情相关字段**,保持其余不变。**实现前先完整 view `GameSaveManager.cs``StorySaveModule` 现状。**
#### A14. `Story/Dialogue/StoryDialogueController.cs`(新建)
- **描述:** block → `DialogueRunner` 的桥接与生命周期协调。路径 `/ichni Official/Assets/Scripts/Story/Dialogue/StoryDialogueController.cs`
- **公共 API**
- `void PlayBlock(TextBlockView block)`:记录当前 block 上下文,`storyUIPage` 淡出/对话页淡入,`dialogueRunner.StartDialogue(block.yarnNodeName)`
- `List<UnityAction> dialogueEndActions`(保留旧 `dialogEndActions` 语义,供 A12 `unlock_song` 入队解锁提示)。
- **事件接线:** 订阅 `dialogueRunner.onDialogueComplete`:若当前 block 为 `Current``treeController.OnBlockCompleted(blockId)`;清空/执行 `dialogueEndActions`(弹解锁提示);`treeController.SaveChapter()`;对话页淡出、故事树页淡入。参考旧 `DialogManager.PlayDialog` 完成分支(旧 L138-152
- **依赖:** Yarn `DialogueRunner`、A6、A7、A12、`StoryUIPage`
#### A15. `Story/Dialogue/VNDialoguePresenter.cs`(新建)
- **描述:** VN 风格对话呈现器,继承 `Yarn.Unity.DialoguePresenterBase`。路径 `/ichni Official/Assets/Scripts/Story/Dialogue/VNDialoguePresenter.cs`
- **override**
- `OnDialogueStartedAsync()`显示对话面板DOTween 淡入)、`portraitStage` 就绪。
- `RunLineAsync(LocalizedLine line, LineCancellationToken token)`:从 `line.CharacterName``CharacterRegistry` 取显示名Unity Localization高亮说话者立绘`portraitStage.HighlightSpeaker`),打字机逐字显示 `line.TextWithoutCharacterName`;等待玩家点击推进(点击→`token` hurry/advance
- `RunOptionsAsync(DialogueOption[], LineCancellationToken)`:实例化选项按钮面板,等待选择,返回选中项;记录到 `selectedChoices`
- `OnDialogueCompleteAsync()`:对话面板淡出。
- **要点:** 全程 `YarnTask` 异步;打字机需响应 `LineCancellationToken`(点击加速/跳过)。**实现步骤时先 view `DialoguePresenterBase.cs``LocalizedLine`/`DialogueOption` 类型核对属性名(`CharacterName`/`TextWithoutCharacterName`/`Text`)。**
- **依赖:** A3、A10、Unity Localization。
#### A16. `Story/Dialogue/StoryFunctions.cs`(新建,可选)
- **描述:** `[YarnFunction]` 辅助,让 `.yarn``<<if>>` 能读取剧情状态。路径 `/ichni Official/Assets/Scripts/Story/Dialogue/StoryFunctions.cs`
- **示例:** `[YarnFunction("is_completed")] static bool IsCompleted(string blockId)``[YarnFunction("story_var")] static int StoryVar(string name)`
- **说明:** 多数条件可直接用 Yarn 变量(`$var`+ `StoryVariableStorage` 完成;此文件仅在需要读取"block 完成状态"等非变量信息时补充。
#### A17. 删除/弃用旧脚本
- **删除:** `Story/Dialog/DialogManager.cs``Story/Dialog/DialogModule.cs``Story/Dialog/StoryInterpreters.cs``Story/StoryUI/DialogUI/*``DialogUIPage`/`DialogContentFrame`/`DialogTextUI`/`ChoiceGroupUI`/`ChoiceButtonUI``Story/StoryUI/Blocks/*` 旧类(迁移到 A7 后删除);旧 `Story/StoryData/StoryData.cs`、旧 `Story/Dialog/StoryManager.cs`、旧 `Story/StoryUI/Storyline.cs`(被 A2/A5/A6 取代)。
- **注意:** DynamicExpresso 若无其它使用者,可保留包(供将来),但剧情不再依赖它。删除前 `grep` 确认无外部引用(尤其 `ChapterSelectionManager`/`Menu``DialogManager` 的调用点,需改为 `StoryDialogueController`)。
------
### B. Text Files`.yarn` 脚本)
#### B1. 示例章节 Yarn 脚本
- **描述:** 每个 TextBlock 对应一个 Yarn 节点。路径示例 `/ichni Official/Assets/Story/Chapters/Chapter1/Chapter1.yarn`(新目录,脱离旧 `Resources/Story/.../Dialogs`)。
- **内容:** 为若干 TextBlock 各写一个 `title:` 节点,含 speaker 行、`<<show_portrait>>`/`<<jump_portrait>>` 等表现命令、`->` 选项、`<<if $var>>` 条件分支、`<<set $var to ...>>`、必要处 `<<unlock_block>>`/`<<unlock_song>>`。用于验证管线。
- **依赖:** A11/A12 命令、A8 变量。
------
### C. Unity AssetsScriptableObject / YarnProject
#### C1. `Chapter1` YarnProject
- **名称:** `/ichni Official/Assets/Story/Chapters/Chapter1/Chapter1.yarnproject`
- **描述:** 编译 B1 的 `.yarn`,配置 **Localisation = Unity**(使用 `UnityLocalisedLineProvider`),关联 Unity Localization String Table Collection。
- **Bezi Actions** 无直接创建 YarnProject 的 Action → **手动创建**(见 E2
#### C2. `StoryData`Chapter1资产
- **名称:** `/ichni Official/Assets/Story/Chapters/Chapter1/Chapter1_StoryData.asset`
- **描述:** 定义 Chapter1 故事树 block、连接、初始状态、`yarnNodeName`、解锁条件,引用 C1 YarnProject。
- **Bezi Actions** `createOrUpdateScriptableObject``scriptableObjectType: "Ichni.Story.StoryData"`)。
- **依赖:** A2、C1。
#### C3. `CharacterRegistry` + 角色资产
- **名称:** `/ichni Official/Assets/Story/Characters/CharacterRegistry.asset`
- **描述:** 登记若干测试角色characterId、displayNameKey、emotion→Sprite
- **Bezi Actions** `createOrUpdateScriptableObject``scriptableObjectType: "Ichni.Story.CharacterRegistry"`)。
- **依赖:** A3。
------
### D. Unity Actions场景 / 预制体 / UI 接线,均在 `MenuScene.unity`
#### D1. Yarn 运行时对象
- **描述:** 在剧情 UI 层新建 `StoryDialogueRoot` GameObject`DialogueRunner` + `UnityLocalisedLineProvider` + `StoryVariableStorage` + `StoryDialogueController` + `StoryPresentationCommands` + `StoryTreeCommands``DialogueRunner.yarnProject` 指向 C1`dialogueViews` 加入 D2 的 `VNDialoguePresenter``variableStorage` 指向 `StoryVariableStorage``lineProvider` 指向 `UnityLocalisedLineProvider`
- **Bezi Actions** `createGameObject``addOrUpdateComponent`(逐个组件;`DialogueRunner` 字段引用用 `UnityObjectReference`)。
- **依赖:** A8/A11/A12/A14/A15、C1。
#### D2. VN 对话 UI
- **描述:** 构建 VN 对话面板:全屏遮罩/背景 `Image`、立绘舞台Left/Center/Right 槽位 `Image`)、对话框(说话者名 `TextMeshProUGUI` + 正文 `TextMeshProUGUI` + 继续指示)、选项面板(竖直布局,运行时实例化选项按钮)。挂 `VNDialoguePresenter``VNPortraitStage`,槽位挂 `SpritePortraitRenderer`
- **Bezi Actions** `createGameObject``addOrUpdateComponent``UnityEngine.UI.Image`/`TMPro.TextMeshProUGUI`/`UnityEngine.UI.Button`)、`setRectTransformLayout`(锚点/拉伸)。
- **依赖:** A9/A10/A15。
#### D3. 故事树 UI 与 block 预制体
- **描述:** 构建/复用故事树滚动视图(`ScrollRect content`);创建 `TextBlockView`/`SongBlockView`/`TutorialBlockView`/`BlockConnectorView` 预制体(含 `Button``inPort`/`outPort`、状态视觉),供 `StoryTreeController` 实例化。将 `StoryManager`/`StoryTreeController`/`StoryUIPage` 组件接线到既有剧情 UI 对象。
- **Bezi Actions** `createGameObject``addOrUpdateComponent``setRectTransformLayout``createPrefab`block 预制体)。
- **依赖:** A5/A6/A7、C2/C3。
------
### E. Manual User Actions
#### E1. 安装 Unity Localization规划弃用 I2
- **描述:** 通过 Package Manager 安装 `com.unity.localization`(使 `UnityLocalisedLineProvider.Installed.cs` 生效);创建 Locale 与 String Table Collection对话文本 + UI 文本。I2 的实际移除可后置,避免一次性破坏现有 UI 文本;本计划仅让**剧情对话**走 Unity Localization。
#### E2. 创建并配置 YarnProject
- **描述:** 在 `Assets/Story/Chapters/Chapter1/` 右键创建 YarnProject纳入 B1 的 `.yarn`Inspector 中设 Localisation 为 Unity 并关联 String Table Collection导入生成基础语言行。Bezi Actions 无 YarnProject 创建能力。)
#### E3. 核对 Yarn 命令/呈现器 API 签名
- **描述:** 实现 A8/A11/A15 前,在 Editor 中确认 `VariableStorageBehaviour``DialoguePresenterBase``LocalizedLine``DialogueOption``DialogueRunner.AddCommandHandler`/`[YarnCommand]` 的确切签名(版本 3.2.1),避免 API 漂移导致编译错误。
------
## Key Patterns to Reuse
- **Source:** `/ichni Official/Assets/Scripts/Story/StoryUI/Storyline.cs`**Pattern:** 故事树构建、连接线生成、按章节 ES3 存取、`ScrollRect` 布局,迁移到 `StoryTreeController`
- **Source:** `/ichni Official/Assets/Scripts/Story/Dialog/DialogManager.cs`L138-152 完成分支、`dialogEndActions`)— **Pattern:** 对话完成 → 置 block `Completed` → 执行结束回调 → 存档,迁移到 `StoryDialogueController.onDialogueComplete`
- **Source:** `/ichni Official/Assets/Scripts/Story/Dialog/StoryInterpreters.cs`**Pattern:** `SetUnlockKey` 的歌曲解锁 + 消息入队逻辑,迁移到 `StoryTreeCommands.unlock_song`
- **Source:** `SLSUtilities/General/Singleton<T>`**Pattern:** `StoryManager` 单例,替换手写 `instance = this`
- **Source:** 旧 `StoryData.cs` 的 Odin `[FoldoutGroup("$blockName")]`/`[InlineProperty]`**Pattern:**`StoryBlockDefinition` 的 Inspector 组织。
- **Source:** `DialogCharacter`(旧 `DialogModule.cs``name`/`title`/`emotions`)— **Pattern:** 演进为 `CharacterData``characterId`/`displayNameKey`/`emotionSprites`)。
------
## Implementation Notes
- **Yarn 3.x 异步:** Presenter 全部 `YarnTask`;打字机与"点击推进"须响应 `LineCancellationToken`hurry-up / cancel否则点击无法加速或跳过。
- **命令注册:** `[YarnCommand]` 自动发现要求承载组件与 `DialogueRunner` 处于可解析范围;若自动发现不稳定,改用 `DialogueRunner.AddCommandHandler` 显式注册(在 `StoryDialogueController.Awake`)。
- **变量类型:** Yarn 变量支持 number/bool/string`storyVariables``int`须扩展多类型存储A13否则 `StoryVariableStorage` 无法承载布尔/字符串。
- **本地化桥接:** speaker 名解析来自 Yarn 行的 `CharacterName`;显示名走 Unity Localization`CharacterData.displayNameKey`),正文走 `UnityLocalisedLineProvider` 关联的 String Table。
- **立绘抽象:** 不要在 Presenter/命令中直接写 `Image`;一律经 `IPortraitRenderer`,为 Live2D/Spine 预留。DOTween 已在项目中,用于跳动/移动/晃动。
- **混合式解锁:** 静态解锁在 `OnBlockCompleted` 重算 `UnlockCondition`;动态解锁由 `<<unlock_block>>`/`<<insert_block>>` 命令在对话中触发——两者都最终调用 `StoryTreeController`,保证存档一致。
- **存档兼容:** 这是破坏性重构,旧 `StorySaves/*.json`/`Choices.json` 结构不兼容;建议在 `StorySaveModule` 加版本号或提供一次性清档(`ClearAllStorySave` 已存在)。
- **场景范围:** 所有场景接线在 `MenuScene.unity`;剧情为 Official 独有,无需同步到 `IchniCreatorStudio`
- **可运行性分阶段:** 每阶段完成后项目应可编译、可进入剧情页。先落数据/树(占位对话),再接 Yarn 管线,再做 VN 呈现与命令,最后清理旧系统。
------
## TODO
**阶段 1 — 数据与故事树骨架(可编译、可显示占位树)**
-
新建 `StoryBlockDefinition.cs`block 定义 + `UnlockCondition`/`VariableCondition` + `StoryBlockType`
-
重建 `StoryData.cs`(章节 block 列表 + `YarnProject` 引用 + 查询 API
-
新建 `CharacterData.cs` + `CharacterRegistry.cs`(含 `portraitStyle` 预留)
-
重建 `StorySave.cs``StoryBlockSave`/`BlockConnectorSave`/`ChapterStorySave`/`StoryVariablesSave`
-
重建 `StoryManager.cs`(单例协调器 + `StoryBlockState`
-
重建 `StoryTreeController.cs`(构建/解锁重算/存取,从 `Storyline` 迁移)
-
重建 `StoryBlockView` + `TextBlockView`/`SongBlockView`/`TutorialBlockView`/`BlockConnectorView`
-
`createOrUpdateScriptableObject` 生成 `Chapter1_StoryData.asset``CharacterRegistry.asset`(用户已手动创建三个 SO 资产)
-
用 Bezi Actions 搭建故事树滚动视图与 block 预制体,接线 `StoryManager`/`StoryTreeController`/`StoryUIPage`(改为用户手动制作,见阶段总结的手动步骤)
-
(提前合并原阶段 5 清理)删除旧剧情/对话脚本网:`DialogManager`/`DialogModule`/`StoryInterpreters`/`Storyline`/旧 `StoryManager`/旧 Block UIs/`DialogUI/*`/旧 `StoryData`/旧 `StorySave``MenuManager` 移除失效 `dialogUIPage` 字段
**阶段 2 — Yarn 对话管线(点击文本块可跑通 Yarn 节点)**
-
安装 `com.unity.localization` 并创建 Locale / String Table Collection手动
-
手动创建 `Chapter0.yarnproject` 并设为 Unity Localisation编写示例 `Chapter0.yarn`
-
新建 `StoryVariableStorage.cs``VariableStorageBehaviour` 桥接存档,多类型变量)
-
重建 `GameSaveManager.StorySaveModule`(新 DTO + 多类型 `storyVariables`,保留 `SongSaveModule` 不变)——已在阶段 1 StorySave 步骤中一并完成:树+选项按章节(单 key 容器变量全局float/string/bool 三类型,匹配 Yarn 契约)
-
新建 `StoryDialogueController.cs``PlayBlock` + `onDialogueComplete` → 置 `Completed`/重算解锁/存档/结束回调)
-
`MenuScene` 搭建 `StoryDialogueRoot``DialogueRunner` + `StoryVariableStorage` + 控制器),接线到 `Chapter0.yarnproject`。注:项目未安装 `com.unity.localization``Chapter0.yarnproject` 使用 Yarn 内置本地化;`DialogueRunner` 在无 lineProvider 时会自动创建 `BuiltinLocalisedLineProvider`,故阶段 2 无需接 Unity Localization。呈现器dialoguePresenters留空待阶段 3 接入 VN 呈现器。
**阶段 3 — VN 呈现与立绘**
-
新建 `VNDialoguePresenter.cs``DialoguePresenterBase`:打字机行、说话者显示名、选项面板、淡入淡出,响应 `LineCancellationToken`
-
新建 `IPortraitRenderer.cs` + `SpritePortraitRenderer.cs`DOTween 跳动/移动/晃动)
-
新建 `VNPortraitStage.cs`(多槽位管理,按 `portraitStyle` 实例化 renderer
-
用 Bezi Actions 搭建 VN 对话 UI背景/立绘槽位/对话框/选项面板),接入 `DialogueRunner.dialogueViews`
**阶段 4 — 表现与树命令、条件**
-
新建 `StoryPresentationCommands.cs``show/hide/move/jump/shake_portrait``set_emotion``set_background`
-
新建 `StoryTreeCommands.cs``unlock_block`/`insert_block`/`unlock_song`,迁移旧 `SetUnlockKey` 歌曲解锁+消息入队)
-
新建 `StoryFunctions.cs``[YarnFunction]` `is_completed`/`story_var`,按需)
-
在示例 `.yarn` 中覆盖表现命令、选项、`<<if>>` 条件、`<<set>>``<<unlock_*>>` 并验证
**阶段 5 — 清理旧系统**
-
`grep` 确认并改接 `ChapterSelectionManager`/`Menu` 等对 `DialogManager` 的调用点到 `StoryDialogueController`
-
删除 `DialogManager.cs`/`DialogModule.cs`/`StoryInterpreters.cs`/`DialogUI/*`/旧 `Blocks/*`/旧 `Storyline.cs`/旧 `StoryData.cs`/旧 `StoryManager.cs`
-
重新编译并清零剧情相关编译错误