剧情+对话完善
This commit is contained in:
@@ -15,17 +15,62 @@ namespace Ichni.Story
|
||||
{
|
||||
public string chapterIndex;
|
||||
|
||||
// 已完成 block 的 id 列表(顺序无关,去重由 StoryTreeController 保证)
|
||||
// 已完成 Block 的 ID 列表(顺序无关,去重由 StoryTreeController 保证)。
|
||||
public List<string> completedBlockIds = new();
|
||||
|
||||
// 章节内的对话选项记录(choiceKey -> 选中项索引)。选项不跨章节引用。
|
||||
public Dictionary<string, int> selectedChoices = new();
|
||||
/// <summary>
|
||||
/// 本章节已经确认过的 Yarn 选项。
|
||||
/// Key 由 <see cref="Dialogue.StoryChoiceMemory"/> 根据来源 Block 与可选项文本 ID 生成,
|
||||
/// Value 保存稳定的文本 ID,而非 UI 下标;因此本地化、隐藏不可用选项或调整显示顺序时不会误选。
|
||||
/// </summary>
|
||||
public Dictionary<string, StoryChoiceRecord> selectedChoices = new();
|
||||
|
||||
/// <summary>
|
||||
/// 只属于本章节的永久剧情变量。章节之间完全隔离,重开本章节时也只回滚这里的数据。
|
||||
/// </summary>
|
||||
public StoryVariablesSave storyVariables = new();
|
||||
|
||||
/// <summary>
|
||||
/// 已经到达过的 Timeline 回滚点快照(markerId -> 快照)。
|
||||
/// 快照创建在 Marker 所在列的第一个 Block 写入前,故恢复后该列及其后的内容都会被重置。
|
||||
/// </summary>
|
||||
public Dictionary<string, StoryRollbackSnapshot> markerRollbackSnapshots = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全局剧情变量存档。float / string / bool 三类型与 Yarn Spinner 的
|
||||
/// <c>VariableStorageBehaviour.GetAllVariables/SetAllVariables</c> 契约一致,
|
||||
/// 供阶段 2 的 StoryVariableStorage 直接读写。
|
||||
/// 一次 Yarn 选项的持久化结果。
|
||||
/// 不保存运行时 Option 下标,避免因翻译、选项可用性或显示顺序变化而恢复到错误选项。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class StoryChoiceRecord
|
||||
{
|
||||
/// <summary>触发这组选项的 TextBlock ID,用于诊断和后续内容审查。</summary>
|
||||
public string sourceBlockId;
|
||||
|
||||
/// <summary>由来源 Block 与全部 Option Text ID 生成的稳定选项组 Key。</summary>
|
||||
public string choiceKey;
|
||||
|
||||
/// <summary>玩家第一次选择的 Yarn DialogueOption.TextID。</summary>
|
||||
public string selectedOptionTextId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 一个 TimelineMarker 的章节内回滚快照。
|
||||
/// 它刻意不包含歌曲成绩、解锁 Key、Settings 或其它章节的数据,保证“重开剧情”只影响当前章节故事。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class StoryRollbackSnapshot
|
||||
{
|
||||
public string markerId;
|
||||
public float markerColumn;
|
||||
public List<string> completedBlockIds = new();
|
||||
public Dictionary<string, StoryChoiceRecord> selectedChoices = new();
|
||||
public StoryVariablesSave storyVariables = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 章节剧情变量存档。float / string / bool 三种底层类型由 <see cref="StoryVariables"/>
|
||||
/// 统一读写;Int 以 float 保存,并在读取时恢复为整数语义。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class StoryVariablesSave
|
||||
@@ -34,4 +79,102 @@ namespace Ichni.Story
|
||||
public Dictionary<string, string> stringVariables = new();
|
||||
public Dictionary<string, bool> boolVariables = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 剧情存档对象的深拷贝工具。
|
||||
/// ES3 保存前和 Timeline 回滚前都必须使用副本,避免后续字典写入意外修改已记录的快照。
|
||||
/// </summary>
|
||||
public static class StorySaveCloneUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// 深拷贝完整章节剧情存档。
|
||||
/// <para>对话中途退出与 Timeline 回滚都需要保留“修改前”的独立副本;
|
||||
/// 不能直接引用运行中的 <see cref="ChapterStorySave"/>,否则后续变量或选项写入会污染快照。</para>
|
||||
/// </summary>
|
||||
public static ChapterStorySave CloneChapter(ChapterStorySave source)
|
||||
{
|
||||
source ??= new ChapterStorySave();
|
||||
return new ChapterStorySave
|
||||
{
|
||||
chapterIndex = source.chapterIndex,
|
||||
completedBlockIds = source.completedBlockIds != null
|
||||
? new List<string>(source.completedBlockIds)
|
||||
: new List<string>(),
|
||||
selectedChoices = CloneChoices(source.selectedChoices),
|
||||
storyVariables = CloneVariables(source.storyVariables),
|
||||
markerRollbackSnapshots = CloneRollbackSnapshots(source.markerRollbackSnapshots)
|
||||
};
|
||||
}
|
||||
|
||||
public static StoryVariablesSave CloneVariables(StoryVariablesSave source)
|
||||
{
|
||||
source ??= new StoryVariablesSave();
|
||||
return new StoryVariablesSave
|
||||
{
|
||||
floatVariables = source.floatVariables != null
|
||||
? new Dictionary<string, float>(source.floatVariables)
|
||||
: new Dictionary<string, float>(),
|
||||
stringVariables = source.stringVariables != null
|
||||
? new Dictionary<string, string>(source.stringVariables)
|
||||
: new Dictionary<string, string>(),
|
||||
boolVariables = source.boolVariables != null
|
||||
? new Dictionary<string, bool>(source.boolVariables)
|
||||
: new Dictionary<string, bool>()
|
||||
};
|
||||
}
|
||||
|
||||
public static Dictionary<string, StoryChoiceRecord> CloneChoices(
|
||||
Dictionary<string, StoryChoiceRecord> source)
|
||||
{
|
||||
Dictionary<string, StoryChoiceRecord> copy = new();
|
||||
if (source == null)
|
||||
return copy;
|
||||
|
||||
foreach ((string key, StoryChoiceRecord value) in source)
|
||||
{
|
||||
if (value == null)
|
||||
continue;
|
||||
|
||||
copy[key] = new StoryChoiceRecord
|
||||
{
|
||||
sourceBlockId = value.sourceBlockId,
|
||||
choiceKey = value.choiceKey,
|
||||
selectedOptionTextId = value.selectedOptionTextId
|
||||
};
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 深拷贝 Timeline Marker 快照及其内部的完成列表、选项和变量。
|
||||
/// </summary>
|
||||
public static Dictionary<string, StoryRollbackSnapshot> CloneRollbackSnapshots(
|
||||
Dictionary<string, StoryRollbackSnapshot> source)
|
||||
{
|
||||
Dictionary<string, StoryRollbackSnapshot> copy = new();
|
||||
if (source == null)
|
||||
return copy;
|
||||
|
||||
foreach ((string key, StoryRollbackSnapshot value) in source)
|
||||
{
|
||||
if (value == null)
|
||||
continue;
|
||||
|
||||
copy[key] = new StoryRollbackSnapshot
|
||||
{
|
||||
markerId = value.markerId,
|
||||
markerColumn = value.markerColumn,
|
||||
completedBlockIds = value.completedBlockIds != null
|
||||
? new List<string>(value.completedBlockIds)
|
||||
: new List<string>(),
|
||||
selectedChoices = CloneChoices(value.selectedChoices),
|
||||
storyVariables = CloneVariables(value.storyVariables)
|
||||
};
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user