Merge branch 'develop' into 记忆检查shader优化

This commit is contained in:
2026-06-29 17:52:47 +08:00
205 changed files with 14335 additions and 14323 deletions
@@ -0,0 +1,140 @@
---
name: "source-command-split-commit"
description: "将当前未提交改动拆分为多个符合规范的原子提交"
---
# source-command-split-commit
Use this skill when the user asks to run the migrated source command `split-commit`.
## Command Template
将当前未提交改动拆分为多个原子提交。执行前会分析变更、生成分组方案,经用户确认后逐组提交。
## 执行流程
### Step 1: 前置检查
1. **清理残留文件**:若存在 `msg.txt`,先 `rm -f msg.txt`
2. **读取规范**:确认 `COMMIT_CONVENTION.md` 存在且可读
### Step 2: 保护分支检查
保护分支列表(按优先级):`AGENTS.md` > `.cursor/rules/` > `.git/config branch.protected` > 默认(`master|main|develop`)
若当前在保护分支,询问用户:
- **A** - 创建 feature 分支后继续拆分(推荐)
- **B** - 转移到新分支后压成单提交
- **C** - 取消
### Step 3: 盘点变更
执行以下命令分析:
- `git status` - 整体状态
- `git diff --stat` - 文件统计
- `git diff` - 详细改动
- `git diff --cached` - 已暂存改动
### Step 4: 规则化分组
按"意图一致"原则分组,**优先按 type 分,再按 scope 分**。
**必须拆分的情况**
- 同一组可归属多个 type
- 同文件包含多个独立目的(如功能+重构)
- 资源替换与代码逻辑混合
**可以合并的情况**(最小拆分原则):
- 同一功能的多文件改动(脚本+Prefab)
- 同一模块的多个相关 Bug 修复
### Step 5: 生成方案表格
以表格形式展示拆分方案:
| 顺序 | 文件 | 变更目的 | 提交头 | 依据 | 方式 |
|------|------|----------|--------|------|------|
| 1 | `路径` | 描述 | `type(scope): subject` | 原因 | `git add`/`git add -p` |
**Type 选择优先级**`fix` > `feat` > `art` > `audio` > `scene` > `yarn` > `perf|refactor|...`
**Scope 规则**:单模块用模块名(如 `blockpuzzle`),跨模块省略。
### Step 6: 用户确认
询问:`[Y 执行 / n 取消 / 提出调整]`
- 若调整:根据反馈修改方案,再次确认
- 若执行:进入 Step 7
- 若取消:清理并退出
### Step 7: 逐组提交
对每组执行:
1. `git add <file>``git add -p <file>`
2. 复查:`git diff --cached --stat``git diff --cached`
3. 写入 `msg.txt` 并提交:`git commit -F msg.txt`
4. 检查状态:`git status`
**失败处理**
- `git add -p` 跳过关键 hunk → 暂停,提示处理方案
- pre-commit hook 失败 → 询问 [R]重试 [S]跳过 [A]中止
### Step 8: 结果校验
输出:
1. `git log --oneline -<N>`
2. 回放清单(验证 type/scope/subject 合规性)
3. 若不合规,询问是否修正
### Step 9: 清理
`rm -f msg.txt`
---
## Unity 改动速查表
| 改动类型 | type | scope 示例 |
|----------|------|-----------|
| 脚本逻辑修复 | `fix` | `blockpuzzle`, `dialog` |
| 脚本新功能 | `feat` | `huoshan`, `clue` |
| 美术资源(贴图/模型/动画) | `art` | 对应模块或省略 |
| 音频资源 | `audio` | `audio-kit` 或省略 |
| 场景配置(灯光/镜头) | `scene` | 对应模块或省略 |
| Yarn 对话 | `yarn` | `dialog` |
| 测试代码 | `test` | 对应模块 |
---
## 规范校验清单(执行前检查)
- [ ] 格式:`<type>(<scope>): <subject>``<type>: <subject>`
- [ ] type ∈ {`feat`,`fix`,`art`,`audio`,`scene`,`yarn`,`perf`,`refactor`,`style`,`docs`,`build`,`chore`,`test`}
- [ ] subject:中文、动词开头、≤50字、无句号
- [ ] 一次提交只做一件事
---
## 中文提交方法
由于 PowerShell 中文乱码问题,统一使用文件方式:
```bash
echo "type(scope): 描述" > msg.txt
git commit -F msg.txt
rm -f msg.txt
```
---
## 关于 Pre-commit Hooks
若配置了 husky/lint-staged
- 每次提交都会触发检查
- 如需跳过(**不推荐**):`git commit -F msg.txt --no-verify`
- 跳过可能导致 CI 失败
---
开始执行请回复 **"开始"**,或告知需要的调整。
+62 -1
View File
@@ -57,5 +57,66 @@
"Temp/": true
},
"dotnet.defaultSolution": "aibis-dream.sln",
"dotnet.preferCSharpExtension": true
"dotnet.preferCSharpExtension": true,
"cSpell.enableFiletypes": [
"yarnspinner"
],
"cSpell.languageSettings": [
{
"languageId": "yarnspinner",
"patterns": [
{
"name": "yarn-commands",
"pattern": "/<<[^>]*>>/g"
},
{
"name": "yarn-expressions",
"pattern": "/\\{[^}]*\\}/g"
},
{
"name": "yarn-variables",
"pattern": "/\\$[a-zA-Z_][a-zA-Z0-9_]*/g"
},
{
"name": "yarn-markup",
"pattern": "/\\[[^\\]]*\\]/g"
},
{
"name": "yarn-hashtags",
"pattern": "/#[a-zA-Z_][a-zA-Z0-9_:-]*/g"
},
{
"name": "yarn-node-markers",
"pattern": "/^(---|===)$/gm"
},
{
"name": "yarn-headers",
"pattern": "/^(title|tags|position|color|colorID|tracking|custom|style):.*/gm"
},
{
"name": "yarn-character-names",
"pattern": "/^\\s*[A-Za-z_][A-Za-z0-9_]*(?=:)/gm"
},
{
"name": "yarn-comments",
"pattern": "/\\/\\/.*$/gm"
}
],
"ignoreRegExpList": [
"yarn-commands",
"yarn-expressions",
"yarn-variables",
"yarn-markup",
"yarn-hashtags",
"yarn-node-markers",
"yarn-headers",
"yarn-character-names",
"yarn-comments"
]
}
],
"cSpell.words": [
"Aibis",
"Newtonsoft"
]
}
+152
View File
@@ -0,0 +1,152 @@
# AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
## Project Overview
AIBIS Dream (also known as "AllOurBrokenParts") is a Unity-based narrative adventure game with investigative elements. The project uses Yarn Spinner for dialogue, features a custom framework with modular systems, and includes multiple mini-games and a modular repair/fix system.
**Unity Version:** 2022.3.7f1c1 (Universal Render Pipeline)
## Architecture
### Core Framework (Assets/Scripts/Framework/)
Modular framework with Kit-pattern utilities:
- **ActionKit**: Action sequencing and command pattern
- **AudioKit**: Audio management (includes FMOD integration)
- **Config / Core**: Configuration and core utilities
- **EventSystemKit**: Decoupled event system
- **LogKit**: Structured logging with categories
- **PoolKit**: Object pooling
- **ResourceKit**: Asset loading via `ResourceSystem` (Addressables-based)
- **Note**: New code must use `ResourceSystem`, never `Resources.Load`. See [Resource Loading Best Practices](Docs/ResourceLoadingBestPractices.md). Asset placement rules in [Resource Management Spec](Docs/ResourceManagementSpec.md).
- **SelectorKit**: Weighted/batch/non-repeat random selection
- **SingletonKit**: Singleton base classes
- **StateMachineKit**: State management
- **OtherKit**: Miscellaneous utilities (`CameraKit`, `AnimatorKit`, `FadeKit`, `LocalizationKit`, etc.)
- **Attribute**: Custom editor attributes
### Key Game Systems
- **Game Loop** (Scripts/Game Loop/): `GameManager` (Singleton entry), `YarnVariableStorage` (Yarn variables), `SaveRestoreOrchestrator` / `SaveSystem` (save snapshots), `SceneLoader`, `ChapterController`, `TalkSceneSO`
- **Dialog System** (Scripts/Dialog System/): Yarn Spinner integration — `DialogController`, `LineRunner`, `LocalisedLineProvider`, `BaseYarnCommand`
- **SceneManagement** (Scripts/SceneManagement/): `ActorKit/`, `AnimatorKit/`, `EnvironmentKit/`, `SpawnerKit/`, `TimelineKit/`, `SceneCenter/`, `DreamDoorSystem`
- **UI** (Scripts/UI/): `DialogUI/`, `Panel/`, `Form/`, `Components/`, `Cursor/`, `UIManager`
- **MiniGame** (Scripts/MiniGame/): `HuoShan/` (language particles, EmotionWave), `BlockPuzzle/`, `Peipei/` (Eye, CutLine, Memory)
- **FixSystem** (Scripts/FixSystem/): Legacy — HeatMap, PunchTape, Memory, screen effects
- **FixSystemNew** (Scripts/FixSystemNew/): Active repair system — `FixSystemCenter`, `BodyModule/`, `Cable/`, `WhackMole/`, `Screen System/`, `Open/`, `Clinic/`
- **AssetRefs** (Scripts/AssetRefs/): Addressable key constants (`ConstRef`, `FixAssetRef`, `TimelineAssetRef`)
### Content & Localization
- **Assets/GameContent/**: Runtime assets by scope (`Common/`, `UI/`, `Scene_{Name}/`, `Feature_{Name}/`)
- **Assets/Language/**: Unity Localization tables (runtime active: `UIText/`, `Locales/`, `Sprite/`; `Dialog/``ActorName/``Params/` currently kept as migration backups)
- **Assets/Resources/Yarn/**: Current Yarn dialogue files (target migration to `Assets/Narrative/` per spec, not yet complete)
- **Assets/StreamingAssets/Config/**: CSV-driven localized gameplay data (`character.csv` for actor names, `params.csv` for `l10n.*` substitution terms)
- **Assets/Prototype/**: HTML/JS prototypes for mechanic testing
## Critical Dependencies
### Unity Packages
- **Yarn Spinner** 3.2.2 (OpenUPM), **Addressables** 1.21.21, **Cinemachine** 2.9.7
- **URP** 14.0.8, **TextMesh Pro** 3.0.6, **Timeline** 1.7.5, **Localization** 1.4.5
- **Visual Effect Graph** 14.0.8, **Visual Scripting** 1.8.0
### Third-Party
- **FMOD** (audio engine), **DOTween** (tweening), **Shapes** (vector graphics)
- **More Mountains Feedbacks** (game feel), **Destructible 2D**
## Development Guidelines
### Code Organization
1. Each major system self-contained in its own folder under `Assets/Scripts/`
2. Kit-pattern utilities in `Framework/` — reusable across systems
3. Singleton pattern for global managers (derived from `SingletonKit`)
4. Favor composition over inheritance
5. All runtime asset loading via `ResourceSystem` — never `Resources.Load`
### Scene Structure
- Scenes in `Assets/Scenes/`; `Persistence` scene hosts global managers and Persistent Loader
- Fix scenes: `{Character}FixScene` (e.g. `HuoShanFixScene`, `PeipeiFixScene`, `ExpressFixScene`)
- Test scenes: `languageTest`, `HuoShanExpressionTest`, `wavetest`, etc.
### Addressables
- Config in `Assets/AddressableAssetsData/`; Groups: `Core`, `Scene_*`, `Feature_*`, `Shared_*`, `Localization-*`
- Key format: `Category/Path` (e.g. `Animation/Peipei`, `Timeline/BlockPuzzle/PanelOpen`)
- Scene keys use bare PascalCase names (e.g. `ClinicOut`)
- Details in [Resource Loading Best Practices](Docs/ResourceLoadingBestPractices.md)
### Yarn Dialogue Files
- Current path: `Assets/Resources/Yarn/` (migration to `Assets/Narrative/Yarn/` planned)
- Pattern: `{Character}_{Stage}.yarn`; UTF-8 encoding required for Chinese
#### Yarn Line Tag Maintenance
- **Line tags** (`#line:xxxxxxx`) must be unique across the entire file. Duplicate tags break Yarn Spinner localization.
- Duplicates typically occur when copying an existing node to create a variant (e.g. `地铁ver1``地铁ver3`).
- **Use the dedup tool** (`Tools/dedup_yarn_tags.py`) for bulk cleanup:
```bash
# Preview duplicates from a specific node onward
python Tools/dedup_yarn_tags.py Assets/Resources/Yarn/Fiction_Day1_begin/Fiction_Day1_begin.yarn --from-node 地铁ver4
# Fix duplicates from a specific line onward
python Tools/dedup_yarn_tags.py Assets/Resources/Yarn/Fiction_Day1_begin/Fiction_Day1_begin.yarn --from-line 535 --fix
# Scan an entire directory (preview only)
python Tools/dedup_yarn_tags.py Assets/Resources/Yarn/ --scan-all
```
#### Yarn Script Generation
- **Do NOT add `#line:` tags when generating or editing Yarn dialogue.** Leave all line-tag generation to the user's localization workflow (Unity Localization + Yarn Spinner import tools).
- When creating new `.yarn` files, write plain dialogue lines without any `#line:` suffixes.
### Asset Naming
- **Scripts**: PascalCase (e.g. `DialogController.cs`)
- **Addressable Keys**: `Category/Path` — no spaces, no extensions, ≤3 levels
- **Addressable Groups**: `{Type}_{PascalCaseName}`
### Git & Commits
- LFS for large binaries; ignored: Library/, Temp/, Build/, *.csproj, *.sln
- **Commit format**: `<type>(<scope>): <subject>` — subject in Chinese, type/scope in English
- Extended types: `feat`, `fix`, `art`, `audio`, `scene`, `yarn`, `perf`, `refactor`, `style`, `docs`, `build`, `chore`, `test`
- See [COMMIT_CONVENTION.md](COMMIT_CONVENTION.md) for full details
## Key Build Commands
```bash
# Windows build
"C:\Program Files\Unity\Hub\Editor\2022.3.7f1c1\Editor\Unity.exe" -batchmode -quit -projectPath . -buildWindows64Player "Build/AIBIS_Dream.exe" -logFile Build/log.txt
```
## Common Tasks
### Adding a New Mini-Game
1. Create `Assets/Scripts/MiniGame/{GameName}/`
2. Add assets to `Assets/GameContent/Feature_{GameName}/`
3. Create Addressable Group `Feature_{GameName}`
4. Create test scene in `Assets/Scenes/`
### Adding Yarn Dialogue
1. Create `.yarn` in `Assets/Resources/Yarn/{Module}/`
2. Ensure YarnProject localization assets/CSV are updated (Unity `Assets/Language/Dialog/` is backup-only during migration)
3. Reference in scene's `DialogueRunner`; test with `DialogController`
## Debugging & Performance
- `LogKit` for structured logging; Addressables Event Viewer for resource debugging
- Monitor `SceneResourceLoader.TotalHandleCount` for leak detection
- Always use PoolKit for frequent instantiation; `LoadUncachedAsync` + `EarlyRelease` for large Timeline assets
- Use `LoadAsync<GameObject>` + `Instantiate` for prefabs, never `InstantiateAsync`
## Important Notes
- Narrative-focused game — dialogue and story take precedence
- `FixSystemNew` is the active repair architecture; `FixSystem` is legacy
- `YarnVariableStorage` holds Yarn runtime variables; save/load flows through `SaveRestoreOrchestrator` and `SaveSystem`
## Reference Documents
- [Resource Loading Best Practices](Docs/ResourceLoadingBestPractices.md) — `ResourceSystem` API, Key/Group/Label naming, lifecycle
- [Resource Management Spec](Docs/ResourceManagementSpec.md) — Asset placement: `GameContent/`, `Narrative/`, `_Archive/`
- [Commit Convention](COMMIT_CONVENTION.md) — Commit message format and branch naming
- [HuoShan Docs](Docs/HuoShan/) — Narrative flow, resources, test flowcharts
---
*This document provides guidance for Codex when working with the AIBIS Dream codebase.*
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: cb2747e27b8dc1e4fac3df4d5edb520c
guid: 8d4c5a2b82ac1ca459e228aaad3255c6
folderAsset: yes
DefaultImporter:
externalObjects: {}
@@ -0,0 +1,667 @@
using System;
using System.Collections.Generic;
using System.IO;
using AibisDream;
using AibisDream.SaveSystem;
using AibisDream.Utility;
using UnityEditor;
using UnityEngine;
namespace AibisDream.SaveSystem.Editor
{
/// <summary>
/// 存读档临时测试工具:触发存档、从槽位/测试归档读档、浏览 testsavs 历史。
/// </summary>
public class SaveSystemTestWindow : EditorWindow
{
private const string MenuPath = "AIBIS/存档测试工具";
private const string WindowTitle = "SaveSystem Test";
private Vector2 _scrollPosition;
private string _lastLog = string.Empty;
private int _restoreSlotIndex;
private int _manualSlotIndex = 1;
private bool _testArchiveEnabled;
private string _selectedArchiveEntryId;
private Texture2D _selectedThumbnail;
private List<TestSaveArchiveEntry> _archiveEntries = new();
private List<TestSaveArchiveEntry> _archiveEntriesSnapshot = new();
private double _lastArchiveRefreshTime;
private bool _repaintQueued;
private List<string> _slotOverviewLabels = new();
private List<string> _restoreLogSnapshot = new();
private string _displaySelectedArchiveEntryId;
private Texture2D _displayThumbnail;
private void RequestRepaint()
{
if (_repaintQueued)
{
return;
}
_repaintQueued = true;
EditorApplication.delayCall += () =>
{
_repaintQueued = false;
if (this != null)
{
Repaint();
}
};
}
[MenuItem(MenuPath)]
public static void Open()
{
var window = GetWindow<SaveSystemTestWindow>(WindowTitle);
window.minSize = new Vector2(480, 560);
}
private void OnEnable()
{
_testArchiveEnabled = EditorPrefs.GetBool(TestSaveArchive.EditorPrefsEnabledKey, true);
RefreshArchiveEntries(requestRepaint: false);
EditorApplication.update += OnEditorUpdate;
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
}
private void OnDisable()
{
EditorApplication.update -= OnEditorUpdate;
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
ReleaseThumbnail();
}
private void OnEditorUpdate()
{
if (!Application.isPlaying)
{
return;
}
if (EditorApplication.timeSinceStartup - _lastArchiveRefreshTime < 1.0)
{
return;
}
RefreshArchiveEntries(requestRepaint: true);
}
private void OnPlayModeStateChanged(PlayModeStateChange state)
{
if (state == PlayModeStateChange.EnteredPlayMode || state == PlayModeStateChange.ExitingPlayMode)
{
RefreshArchiveEntries();
}
}
private void OnGUI()
{
if (Event.current.type == EventType.Layout)
{
SnapshotDynamicGuiData();
}
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
DrawHeader();
GUILayout.Space(8);
DrawTestArchiveSettings();
GUILayout.Space(8);
DrawSaveActions();
GUILayout.Space(8);
DrawSlotRestoreSection();
GUILayout.Space(8);
DrawTestArchiveSection();
GUILayout.Space(8);
DrawRestoreLogSection();
GUILayout.Space(8);
DrawUtilitySection();
GUILayout.Space(8);
DrawLogSection();
EditorGUILayout.EndScrollView();
}
/// <summary>
/// 在 Layout/Repaint 之前快照可变列表,避免同一 GUI 帧内控件数量不一致。
/// </summary>
private void SnapshotDynamicGuiData()
{
_displaySelectedArchiveEntryId = _selectedArchiveEntryId;
_displayThumbnail = _selectedThumbnail;
_archiveEntriesSnapshot.Clear();
for (var i = 0; i < _archiveEntries.Count; i++)
{
_archiveEntriesSnapshot.Add(_archiveEntries[i]);
}
_slotOverviewLabels.Clear();
foreach (var vm in SlotManager.GetSlotViewModels())
{
if (vm.IsEmpty)
{
continue;
}
var meta = SlotManager.LoadMeta(vm.SlotIndex);
_slotOverviewLabels.Add(
$"slot_{vm.SlotIndex}" +
(vm.IsAutoSlot ? " [自动]" : "") +
$" | {vm.SavedAt} | {meta?.nodeName ?? ""} | {vm.SceneName}");
}
_restoreLogSnapshot.Clear();
var restoreLog = SaveRestoreOrchestrator.LastRestoreLog;
for (var i = 0; i < restoreLog.Count; i++)
{
_restoreLogSnapshot.Add(restoreLog[i]);
}
}
private void DrawHeader()
{
EditorGUILayout.LabelField("存读档测试工具", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"用于 Play Mode 下验证自动存档、槽位读档与 testsavs 测试归档。\n" +
"正式存档目录: saves/ | 测试归档: testsavs/(每次自动存档独立子文件夹)",
MessageType.Info);
if (!Application.isPlaying)
{
EditorGUILayout.HelpBox("存档/读档操作需要在 Play Mode 下执行。", MessageType.Warning);
}
EditorGUILayout.LabelField(
$"状态: IsRestoring={SaveRestoreOrchestrator.IsRestoring}, " +
$"SuppressAutoSave={SaveRestoreOrchestrator.IsAutoSaveSuppressed}");
}
private void DrawTestArchiveSettings()
{
EditorGUILayout.LabelField("测试存档模式", EditorStyles.boldLabel);
EditorGUI.BeginChangeCheck();
_testArchiveEnabled = EditorGUILayout.Toggle("启用测试存档模式", _testArchiveEnabled);
if (EditorGUI.EndChangeCheck())
{
EditorPrefs.SetBool(TestSaveArchive.EditorPrefsEnabledKey, _testArchiveEnabled);
}
EditorGUILayout.LabelField($"testsavs 路径: {ConstRef.TestAutoSaveArchivePath}", EditorStyles.miniLabel);
EditorGUILayout.HelpBox(
"开启后:每次 AutoSave 仅在 testsavs 下新建一份独立存档,不写入、不覆盖 saves/slot_0。\n" +
"存多少次就保留多少份;关闭后恢复正式行为(仅覆盖 slot_0)。",
MessageType.None);
}
private void DrawSaveActions()
{
EditorGUILayout.LabelField("存档", EditorStyles.boldLabel);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("触发自动存档 (完整流程)"))
{
TriggerAutoSave();
}
if (GUILayout.Button("仅 Capture (不写盘)"))
{
CaptureInMemory();
}
}
if (GUILayout.Button("手动档: 复制 slot_0 → 上方指定槽位"))
{
CopyAutoToManual();
}
}
private void DrawSlotRestoreSection()
{
EditorGUILayout.LabelField("从正式槽位读档", EditorStyles.boldLabel);
EditorGUILayout.LabelField($"saves 路径: {ConstRef.SaveFilePath}", EditorStyles.miniLabel);
var latest = SlotManager.GetLatestSlotIndex();
EditorGUILayout.LabelField($"最近槽位: {(latest.HasValue ? $"slot_{latest.Value}" : "")}");
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("读档: Latest"))
{
RestoreLatestSlot();
}
if (GUILayout.Button("读档: slot_0"))
{
RestoreSlot(SlotIndex.Auto);
}
}
_restoreSlotIndex = EditorGUILayout.IntSlider("槽位", _restoreSlotIndex, 0, SlotIndex.ManualEnd);
if (GUILayout.Button($"读档: slot_{_restoreSlotIndex}"))
{
RestoreSlot(_restoreSlotIndex);
}
GUILayout.Space(4);
_manualSlotIndex = EditorGUILayout.IntSlider(
"复制到手动槽位",
_manualSlotIndex,
SlotIndex.ManualStart,
SlotIndex.ManualEnd);
DrawSlotBriefOverview();
}
private void DrawSlotBriefOverview()
{
foreach (var label in _slotOverviewLabels)
{
EditorGUILayout.LabelField(label, EditorStyles.miniLabel);
}
}
private void DrawTestArchiveSection()
{
EditorGUILayout.LabelField("testsavs 测试归档", EditorStyles.boldLabel);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("刷新列表"))
{
RefreshArchiveEntries();
}
if (GUILayout.Button("清空 testsavs"))
{
ClearTestArchives();
}
if (GUILayout.Button("打开 testsavs 文件夹"))
{
OpenDirectory(ConstRef.TestAutoSaveArchivePath);
}
}
EditorGUILayout.LabelField($"共 {_archiveEntriesSnapshot.Count} 条", EditorStyles.miniLabel);
if (_archiveEntriesSnapshot.Count == 0)
{
EditorGUILayout.HelpBox(
"暂无测试归档。开启「testsavs 独立归档」后触发自动存档,或从正式 slot_0 手动归档。",
MessageType.None);
}
else
{
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
foreach (var entry in _archiveEntriesSnapshot)
{
DrawArchiveEntry(entry);
}
}
}
if (GUILayout.Button("将当前 slot_0 手动归档到 testsavs"))
{
ArchiveCurrentAutoSlot();
}
}
private void DrawArchiveEntry(TestSaveArchiveEntry entry)
{
var isSelected = _displaySelectedArchiveEntryId == entry.EntryId;
var boxStyle = isSelected ? EditorStyles.helpBox : EditorStyles.textArea;
using (new EditorGUILayout.VerticalScope(boxStyle))
{
EditorGUILayout.LabelField(entry.EntryId, EditorStyles.boldLabel);
EditorGUILayout.LabelField(
$"时间: {entry.Meta?.savedAt ?? ""} | 节点: {entry.Meta?.nodeName ?? ""}",
EditorStyles.miniLabel);
EditorGUILayout.LabelField(
$"场景: {entry.Meta?.sceneName ?? ""} | SO: {entry.Meta?.sceneSoName ?? ""}",
EditorStyles.miniLabel);
if (isSelected)
{
var maxWidth = EditorGUIUtility.currentViewWidth - 48f;
var height = 120f;
if (_displayThumbnail != null)
{
height = Mathf.Min(120f, _displayThumbnail.height * maxWidth / _displayThumbnail.width);
GUILayout.Label(_displayThumbnail, GUILayout.Width(maxWidth), GUILayout.Height(height));
}
else
{
EditorGUILayout.LabelField(
"(无缩略图)",
EditorStyles.miniLabel,
GUILayout.Width(maxWidth),
GUILayout.Height(height));
}
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("选中"))
{
SelectArchiveEntry(entry);
}
if (GUILayout.Button("读档"))
{
RestoreFromArchive(entry);
}
if (GUILayout.Button("删除"))
{
DeleteArchiveEntry(entry);
}
}
}
GUILayout.Space(4);
}
private void DrawRestoreLogSection()
{
EditorGUILayout.LabelField("读档 Pipeline 日志", EditorStyles.boldLabel);
if (_restoreLogSnapshot.Count == 0)
{
EditorGUILayout.LabelField("(暂无)", EditorStyles.miniLabel);
return;
}
foreach (var line in _restoreLogSnapshot)
{
EditorGUILayout.LabelField(line, EditorStyles.wordWrappedMiniLabel);
}
}
private void DrawUtilitySection()
{
EditorGUILayout.LabelField("工具", EditorStyles.boldLabel);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("打开 saves 文件夹"))
{
OpenDirectory(ConstRef.SaveFilePath);
}
if (GUILayout.Button("打开存档验证工具"))
{
SaveSystemValidationWindow.Open();
}
}
}
private void DrawLogSection()
{
EditorGUILayout.LabelField("操作结果", EditorStyles.boldLabel);
EditorGUILayout.SelectableLabel(_lastLog, EditorStyles.textArea, GUILayout.MinHeight(80));
}
private void RefreshArchiveEntries(bool requestRepaint = true)
{
var newEntries = TestSaveArchive.ListEntries();
var entriesChanged = !ArchiveEntriesEqual(newEntries, _archiveEntries);
_archiveEntries = newEntries;
_lastArchiveRefreshTime = EditorApplication.timeSinceStartup;
if (!string.IsNullOrEmpty(_selectedArchiveEntryId) &&
!_archiveEntries.Exists(e => e.EntryId == _selectedArchiveEntryId))
{
_selectedArchiveEntryId = null;
ReleaseThumbnail();
entriesChanged = true;
}
if (requestRepaint && entriesChanged)
{
RequestRepaint();
}
}
private static bool ArchiveEntriesEqual(List<TestSaveArchiveEntry> a, List<TestSaveArchiveEntry> b)
{
if (a.Count != b.Count)
{
return false;
}
for (var i = 0; i < a.Count; i++)
{
if (a[i].EntryId != b[i].EntryId)
{
return false;
}
}
return true;
}
private void SelectArchiveEntry(TestSaveArchiveEntry entry)
{
_selectedArchiveEntryId = entry.EntryId;
ReleaseThumbnail();
if (!string.IsNullOrEmpty(entry.ThumbnailPath) && File.Exists(entry.ThumbnailPath))
{
var bytes = File.ReadAllBytes(entry.ThumbnailPath);
_selectedThumbnail = new Texture2D(2, 2);
_selectedThumbnail.LoadImage(bytes);
}
RequestRepaint();
}
private void ReleaseThumbnail()
{
if (_selectedThumbnail != null)
{
DestroyImmediate(_selectedThumbnail);
_selectedThumbnail = null;
}
}
private void TriggerAutoSave()
{
if (!Application.isPlaying)
{
Log("需要在 Play Mode 下执行。");
return;
}
try
{
if (!SavePointEvaluator.CanAutoSave(out var reason))
{
Log($"CanAutoSave 拒绝: {reason}");
return;
}
var routine = SaveRestoreOrchestrator.AutoSaveRoutine();
while (routine.MoveNext())
{
}
RefreshArchiveEntries();
Log(TestSaveArchive.IsEnabled
? "测试存档模式:已写入 testsavs(未覆盖 slot_0)。"
: "正式模式:已写入 slot_0。");
}
catch (Exception ex)
{
Log($"自动存档失败: {ex}");
}
}
private void CaptureInMemory()
{
try
{
if (YarnVariableStorage.Instance == null)
{
Log("YarnVariableStorage 未初始化。");
return;
}
var snapshot = SnapshotService.Capture();
Log($"Capture 成功: 场景={snapshot.scene?.sceneName}, 节点={snapshot.anchor?.nodeName}");
}
catch (Exception ex)
{
Log($"Capture 失败: {ex.Message}");
}
}
private void CopyAutoToManual()
{
if (!SlotDirectory.Exists(SlotIndex.Auto))
{
Log("slot_0 不存在。");
return;
}
SlotManager.CopyAutoToManual(_manualSlotIndex);
Log($"已复制到 slot_{_manualSlotIndex}。");
}
private void RestoreLatestSlot()
{
var latest = SlotManager.GetLatestSlotIndex();
if (!latest.HasValue)
{
Log("没有 latest_slot。");
return;
}
RestoreSlot(latest.Value);
}
private void RestoreSlot(int slotIndex)
{
if (!Application.isPlaying)
{
Log("读档需要在 Play Mode 下执行。");
return;
}
if (GameManager.Instance == null)
{
Log("GameManager 未初始化。");
return;
}
if (!SlotDirectory.Exists(slotIndex))
{
Log($"slot_{slotIndex} 不存在。");
return;
}
GameManager.Instance.StartWithSlot(slotIndex);
Log($"已启动读档 slot_{slotIndex}。");
}
private void RestoreFromArchive(TestSaveArchiveEntry entry)
{
if (!Application.isPlaying)
{
Log("读档需要在 Play Mode 下执行。");
return;
}
if (GameManager.Instance == null)
{
Log("GameManager 未初始化。");
return;
}
GameManager.Instance.StartWithSaveFile(entry.SnapshotPath);
Log($"已启动读档: {entry.EntryId}");
}
private void ArchiveCurrentAutoSlot()
{
var snapshot = SlotManager.LoadSnapshot(SlotIndex.Auto);
if (snapshot == null)
{
Log("slot_0 无快照。");
return;
}
var thumbnail = SlotManager.LoadThumbnail(SlotIndex.Auto);
var entryId = TestSaveArchive.Archive(snapshot, thumbnail);
RefreshArchiveEntries();
Log($"已手动归档: {entryId}");
}
private void DeleteArchiveEntry(TestSaveArchiveEntry entry)
{
var entryId = entry.EntryId;
EditorApplication.delayCall += () => ConfirmDeleteArchiveEntry(entryId);
}
private void ConfirmDeleteArchiveEntry(string entryId)
{
if (!EditorUtility.DisplayDialog("删除测试归档", $"删除 {entryId}", "删除", "取消"))
{
return;
}
TestSaveArchive.DeleteEntry(entryId);
if (_selectedArchiveEntryId == entryId)
{
_selectedArchiveEntryId = null;
ReleaseThumbnail();
}
RefreshArchiveEntries();
Log($"已删除: {entryId}");
}
private void ClearTestArchives()
{
EditorApplication.delayCall += ConfirmClearTestArchives;
}
private void ConfirmClearTestArchives()
{
if (!EditorUtility.DisplayDialog("清空 testsavs", "删除所有测试归档?不可撤销。", "删除", "取消"))
{
return;
}
TestSaveArchive.ClearAll();
_selectedArchiveEntryId = null;
ReleaseThumbnail();
RefreshArchiveEntries();
Log("已清空 testsavs。");
}
private static void OpenDirectory(string path)
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
EditorUtility.RevealInFinder(path);
}
private void Log(string message)
{
_lastLog = $"[{DateTime.Now:HH:mm:ss}] {message}";
Debug.Log($"[SaveSystemTest] {message}");
RequestRepaint();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 27c49345ec3a4e849af3f9da7045f7a3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,704 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AibisDream;
using AibisDream.SaveSystem;
using AibisDream.Utility;
using Newtonsoft.Json;
using UnityEditor;
using UnityEngine;
namespace AibisDream.SaveSystem.Editor
{
/// <summary>
/// 存档系统 P1/P2/P3 存盘行为验证工具。
/// 不验证读档(P4),不验证复杂 tags 边界(P3 后续)。
/// </summary>
public class SaveSystemValidationWindow : EditorWindow
{
private const string MenuPath = "AIBIS/存档系统验证工具";
private const string WindowTitle = "SaveSystem Validator";
private const float SaveHistoryBoxMinHeight = 400f;
private Vector2 _scrollPosition;
private string _lastLog = string.Empty;
private int _manualSlotIndex = 1;
private int _deleteSlotIndex = 1;
private string _testNodeName = "SomeNode";
private string _testTags = "";
private int _restoreSlotIndex;
private readonly List<SaveHistoryEntry> _saveHistory = new();
private string _lastObservedSavedAt;
/// <summary>运行时自动存档历史条目,仅内存缓存。</summary>
private class SaveHistoryEntry
{
public string SavedAt;
public string NodeName;
public string SceneName;
}
[MenuItem(MenuPath)]
public static void Open()
{
var window = GetWindow<SaveSystemValidationWindow>(WindowTitle);
window.minSize = new Vector2(420, 640);
}
private void OnEnable()
{
EditorApplication.update += OnEditorUpdate;
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
InitializeLastObservedSavedAt();
}
private void OnDisable()
{
EditorApplication.update -= OnEditorUpdate;
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
}
private void OnPlayModeStateChanged(PlayModeStateChange state)
{
if (state == PlayModeStateChange.EnteredPlayMode)
{
_saveHistory.Clear();
InitializeLastObservedSavedAt();
TrySeedHistoryFromCurrentAutoSlot();
Repaint();
return;
}
if (state == PlayModeStateChange.ExitingPlayMode)
{
_saveHistory.Clear();
_lastObservedSavedAt = null;
}
}
private void TrySeedHistoryFromCurrentAutoSlot()
{
var meta = SlotManager.LoadMeta(SlotIndex.Auto);
if (meta == null)
{
return;
}
_saveHistory.Add(CreateHistoryEntry(meta));
_lastObservedSavedAt = meta.savedAt;
}
private static SaveHistoryEntry CreateHistoryEntry(SlotMeta meta)
{
return new SaveHistoryEntry
{
SavedAt = meta.savedAt,
NodeName = string.IsNullOrEmpty(meta.nodeName) ? "(无节点)" : meta.nodeName,
SceneName = meta.sceneName
};
}
private void InitializeLastObservedSavedAt()
{
var meta = SlotManager.LoadMeta(SlotIndex.Auto);
_lastObservedSavedAt = meta?.savedAt;
}
/// <summary>
/// 监听 slot_0 的 meta 变化,运行时每次自动存档都会记录到内存列表中。
/// 纯 Editor 行为,不侵入运行时代码。
/// </summary>
private void OnEditorUpdate()
{
if (!Application.isPlaying)
{
return;
}
var meta = SlotManager.LoadMeta(SlotIndex.Auto);
if (meta == null)
{
return;
}
if (!string.IsNullOrEmpty(_lastObservedSavedAt) && _lastObservedSavedAt == meta.savedAt)
{
return;
}
_lastObservedSavedAt = meta.savedAt;
_saveHistory.Add(CreateHistoryEntry(meta));
Repaint();
}
private void OnGUI()
{
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
DrawHeader();
DrawSlotOverview();
GUILayout.Space(12);
DrawSaveHistorySection();
GUILayout.Space(12);
DrawDirectCaptureSection();
GUILayout.Space(12);
DrawAutoSaveSimulationSection();
GUILayout.Space(12);
DrawManualSlotSection();
GUILayout.Space(12);
DrawRestoreSection();
GUILayout.Space(12);
DrawSavePointEvaluationSection();
GUILayout.Space(12);
DrawUtilitySection();
GUILayout.Space(12);
DrawLogSection();
EditorGUILayout.EndScrollView();
}
#region Drawing
private void DrawHeader()
{
EditorGUILayout.LabelField("存档系统存盘验证", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"本工具仅验证 P1/P2/P3 的存盘行为:快照捕获、槽位落盘、元数据、可存点判定框架。\n" +
"不验证读档(P4)和复杂 tags 边界(P3 后续)。\n" +
"涉及运行时单例(DialogController / GameManager)的操作需要在 Play Mode 下执行。",
MessageType.Info);
if (!Application.isPlaying)
{
EditorGUILayout.HelpBox("当前不在 Play Mode。Capture / TryAutoSave 等依赖运行时单例的操作可能失败。", MessageType.Warning);
}
}
private void DrawSlotOverview()
{
EditorGUILayout.LabelField("槽位概览", EditorStyles.boldLabel);
var latest = SlotManager.GetLatestSlotIndex();
EditorGUILayout.LabelField($"最近槽位: {(latest.HasValue ? latest.Value.ToString() : "")}");
EditorGUILayout.LabelField($"存档根目录: {ConstRef.SaveFilePath}");
EditorGUILayout.LabelField(
$"读档状态: IsRestoring={SaveRestoreOrchestrator.IsRestoring}, " +
$"SuppressAutoSave={SaveRestoreOrchestrator.IsAutoSaveSuppressed}");
var viewModels = SlotManager.GetSlotViewModels();
foreach (var vm in viewModels)
{
DrawSlotViewModel(vm, latest);
}
}
private void DrawSlotViewModel(SlotViewModel vm, int? latest)
{
var isLatest = latest.HasValue && latest.Value == vm.SlotIndex;
var nodeName = SlotManager.LoadMeta(vm.SlotIndex)?.nodeName;
var label = vm.IsEmpty
? $"[{vm.SlotIndex}] (空)"
: $"[{vm.SlotIndex}] {(vm.IsAutoSlot ? "[]" : "")} {(isLatest ? "[]" : "")}\n" +
$" 场景: {vm.SceneName}\n" +
$" SO: {vm.SceneSoName}\n" +
$" 节点: {nodeName}\n" +
$" 时间: {vm.SavedAt}\n" +
$" 缩略图: {(string.IsNullOrEmpty(vm.ThumbnailPath) ? "" : Path.GetFileName(vm.ThumbnailPath))}";
EditorGUILayout.HelpBox(label, vm.IsEmpty ? MessageType.None : MessageType.Info);
}
private void DrawDirectCaptureSection()
{
EditorGUILayout.LabelField("直接捕获快照", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"调用 SnapshotService.Capture() 组装快照,并在内存中检查结构。\n" +
"此操作不写盘,不经过 SavePointEvaluator。",
MessageType.None);
if (GUILayout.Button("Capture Snapshot (内存)"))
{
CaptureSnapshotInMemory();
}
}
private void DrawAutoSaveSimulationSection()
{
EditorGUILayout.LabelField("模拟自动存档", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"调用 SaveRestoreOrchestrator.TryAutoSave(),走完整的判定+捕获+落盘流程。\n" +
"需要在 Play Mode 下且有 DialogController 运行时实例。",
MessageType.None);
if (GUILayout.Button("TryAutoSave (完整流程)"))
{
TryAutoSaveFullFlow();
}
}
private void DrawSaveHistorySection()
{
EditorGUILayout.LabelField("自动存档历史 (运行时)", EditorStyles.boldLabel);
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox, GUILayout.MinHeight(SaveHistoryBoxMinHeight)))
{
if (!Application.isPlaying)
{
EditorGUILayout.HelpBox(
"进入 Play Mode 后,每次 slot_0 自动存档都会在此记录节点名与时间。\n" +
"数据仅保存在当前窗口内存中,退出 Play Mode 后清空。",
MessageType.Info);
}
else if (_saveHistory.Count == 0)
{
EditorGUILayout.LabelField("等待自动存档…", EditorStyles.wordWrappedLabel);
EditorGUILayout.LabelField(
"触发 content / hub / linear 节点,或点击下方 TryAutoSave。",
EditorStyles.miniLabel);
}
else
{
EditorGUILayout.LabelField($"共 {_saveHistory.Count} 条记录", EditorStyles.miniLabel);
for (var i = 0; i < _saveHistory.Count; i++)
{
var entry = _saveHistory[i];
EditorGUILayout.LabelField(
$"{i + 1}. [{entry.SavedAt}] {entry.NodeName}",
EditorStyles.wordWrappedLabel);
EditorGUILayout.LabelField(
$" 场景: {entry.SceneName ?? "N/A"}",
EditorStyles.miniLabel);
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("清空历史记录"))
{
_saveHistory.Clear();
}
}
}
}
private void DrawManualSlotSection()
{
EditorGUILayout.LabelField("手动档操作", EditorStyles.boldLabel);
_manualSlotIndex = EditorGUILayout.IntSlider("复制到槽位", _manualSlotIndex, 1, SlotIndex.ManualEnd);
if (GUILayout.Button("Copy Auto → Manual"))
{
CopyAutoToManual(_manualSlotIndex);
}
GUILayout.Space(8);
_deleteSlotIndex = EditorGUILayout.IntSlider("删除槽位", _deleteSlotIndex, 0, SlotIndex.ManualEnd);
if (GUILayout.Button("Delete Slot"))
{
DeleteSlot(_deleteSlotIndex);
}
GUILayout.Space(8);
if (GUILayout.Button("Clear All Saves"))
{
ClearAllSaves();
}
}
private void DrawSavePointEvaluationSection()
{
EditorGUILayout.LabelField("可存点判定框架检查", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"手动设置节点名和 tags,模拟 OnNodeStart 自动档判定(含 DetourResume)。\n" +
"InProgress 来自运行时 DialogController;全局门控按实际 Play Mode 状态取值。",
MessageType.None);
_testNodeName = EditorGUILayout.TextField("节点名", _testNodeName);
_testTags = EditorGUILayout.TextField("Tags (逗号分隔)", _testTags);
if (GUILayout.Button("Evaluate CanAutoSave"))
{
EvaluateSavePoint();
}
}
private void DrawRestoreSection()
{
EditorGUILayout.LabelField("读档验证 (P4)", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"触发 SaveRestoreOrchestrator.RestoreFromSlot,验证 Phase + Barrier 编排、自动存档抑制与 Provider 还原日志。\n" +
"需要在 Play Mode 下执行;不代表正式玩家 UI。",
MessageType.None);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Restore Latest"))
{
RestoreLatestSlot();
}
if (GUILayout.Button("Restore slot_0"))
{
RestoreSlot(SlotIndex.Auto);
}
}
_restoreSlotIndex = EditorGUILayout.IntSlider("Restore 槽位", _restoreSlotIndex, 0, SlotIndex.ManualEnd);
if (GUILayout.Button($"Restore slot_{_restoreSlotIndex}"))
{
RestoreSlot(_restoreSlotIndex);
}
var restoreLog = SaveRestoreOrchestrator.LastRestoreLog;
if (restoreLog.Count > 0)
{
EditorGUILayout.LabelField("Restore Pipeline Log", EditorStyles.boldLabel);
foreach (var line in restoreLog)
{
EditorGUILayout.LabelField(line, EditorStyles.wordWrappedMiniLabel);
}
}
}
private void DrawUtilitySection()
{
EditorGUILayout.LabelField("工具", EditorStyles.boldLabel);
if (GUILayout.Button("打开存档目录"))
{
OpenSaveDirectory();
}
if (GUILayout.Button("刷新"))
{
Repaint();
Log("已刷新。");
}
}
private void DrawLogSection()
{
GUILayout.Space(12);
EditorGUILayout.LabelField("最后结果", EditorStyles.boldLabel);
EditorGUILayout.SelectableLabel(_lastLog, EditorStyles.textArea, GUILayout.MinHeight(120));
}
#endregion
#region Actions
private void CaptureSnapshotInMemory()
{
try
{
var storage = YarnVariableStorage.Instance;
if (storage == null)
{
Log("YarnVariableStorage 未初始化,尝试直接 new SaveSnapshot 仅做结构演示。");
var demo = new SaveSnapshot
{
gameVersion = Application.version,
savedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
scene = new SceneSnapshotDto { sceneName = "DemoScene" },
anchor = new AnchorSnapshot { nodeName = "DemoNode" },
yarnVariables = new YarnVariablesSnapshot()
};
PrintSnapshotSummary(demo, "演示快照(无运行时)");
return;
}
var snapshot = SnapshotService.Capture();
PrintSnapshotSummary(snapshot, "内存快照");
}
catch (Exception ex)
{
Log($"Capture 失败: {ex}");
}
}
private void TryAutoSaveFullFlow()
{
if (!Application.isPlaying)
{
Log("TryAutoSave 需要在 Play Mode 下执行。");
return;
}
try
{
var beforeLatest = SlotManager.GetLatestSlotIndex();
var beforeSnapshot = SlotManager.LoadSnapshot(0);
if (!SavePointEvaluator.CanAutoSave(out var rejectReason))
{
Log($"CanAutoSave 拒绝: {rejectReason}");
return;
}
var routine = SaveRestoreOrchestrator.AutoSaveRoutine();
while (routine.MoveNext())
{
}
var afterSnapshot = SlotManager.LoadSnapshot(0);
var afterLatest = SlotManager.GetLatestSlotIndex();
var summary = string.Empty;
if (afterSnapshot == null)
{
summary = "无快照写入(可能被 SavePointEvaluator 拒绝)。";
}
else
{
summary = $"落盘成功。\n" +
$"场景: {afterSnapshot.scene?.sceneName}\n" +
$"节点: {afterSnapshot.anchor?.nodeName}\n" +
$"SO: {afterSnapshot.anchor?.sceneSoName}\n" +
$"YarnProject: {afterSnapshot.anchor?.yarnProjectId}\n" +
$"Sections: {string.Join(", ", afterSnapshot.sections?.Keys ?? Enumerable.Empty<string>())}\n" +
$"latest_slot: {(afterLatest.HasValue ? afterLatest.Value.ToString() : "")}";
}
Log(summary);
}
catch (Exception ex)
{
Log($"TryAutoSave 失败: {ex}");
}
}
private void CopyAutoToManual(int slotIndex)
{
try
{
if (!SlotDirectory.Exists(SlotIndex.Auto))
{
Log("自动档不存在,无法复制手动档。");
return;
}
SlotManager.CopyAutoToManual(slotIndex);
Log($"已复制自动档到 slot_{slotIndex}。");
}
catch (Exception ex)
{
Log($"复制失败: {ex.Message}");
}
}
private void DeleteSlot(int slotIndex)
{
try
{
SlotManager.DeleteSlot(slotIndex);
Log($"已删除/清空 slot_{slotIndex}。");
}
catch (Exception ex)
{
Log($"删除失败: {ex.Message}");
}
}
private void ClearAllSaves()
{
if (!EditorUtility.DisplayDialog("确认", "删除所有存档槽位?此操作不可撤销。", "删除", "取消"))
{
return;
}
try
{
for (var i = SlotIndex.Auto; i <= SlotIndex.ManualEnd; i++)
{
SlotManager.DeleteSlot(i);
}
var latestPath = SlotDirectory.GetLatestSlotIndexPath();
if (File.Exists(latestPath))
{
File.Delete(latestPath);
}
Log("已清空所有槽位和 latest_slot 记录。");
}
catch (Exception ex)
{
Log($"清空失败: {ex.Message}");
}
}
private void EvaluateSavePoint()
{
if (!Application.isPlaying)
{
Log("SavePointEvaluator 需要运行时状态(IsRestoring / GameManager pause)。请在 Play Mode 下执行。");
return;
}
try
{
var projectId = DialogController.Instance?.DialogueRunner?.YarnProject?.name;
var tags = ParseTestTags(_testTags);
var inProgress = SavePointEvaluator.InProgressNodeNames;
if (SaveRestoreOrchestrator.IsAutoSaveSuppressed)
{
Log("全局门控: AutoSaveSuppressed");
return;
}
if (SaveRestoreOrchestrator.IsRestoring)
{
Log("全局门控: Restoring(读档重进走 OnRestoreEnterNode,不写盘)");
return;
}
if (GameManager.Instance != null && GameManager.Instance.state.isInPause)
{
Log("全局门控: GamePaused");
return;
}
var detourResume = !string.IsNullOrEmpty(_testNodeName)
&& inProgress.Contains(_testNodeName);
var tagOk = SavePointEvaluator.EvaluateNodeTagsForAutoSave(
_testNodeName, tags, out var tagReason);
var wouldSave = !detourResume && tagOk;
Log(
$"Project = {projectId ?? "(null)"}\n" +
$"InProgress = [{string.Join(", ", inProgress)}]\n" +
$"模拟节点 = {_testNodeName}, tags = [{string.Join(", ", tags)}]\n" +
$"DetourResume = {detourResume}\n" +
$"Tag 判定 = {tagOk}, TagReason = {tagReason}\n" +
$"OnNodeStart 会自动存 = {wouldSave}\n" +
$"(DialogController 当前上下文 CanAutoSave = {SavePointEvaluator.CanAutoSave(out var ctxReason)}, Reason = {ctxReason})");
}
catch (Exception ex)
{
Log($"判定失败: {ex.Message}");
}
}
private static string[] ParseTestTags(string raw)
{
if (string.IsNullOrWhiteSpace(raw))
{
return Array.Empty<string>();
}
return raw.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(tag => tag.Trim())
.Where(tag => tag.Length > 0)
.ToArray();
}
private void RestoreLatestSlot()
{
var latest = SlotManager.GetLatestSlotIndex();
if (!latest.HasValue)
{
Log("没有 latest_slot,无法读档。");
return;
}
RestoreSlot(latest.Value);
}
private void RestoreSlot(int slotIndex)
{
if (!Application.isPlaying)
{
Log("Restore 需要在 Play Mode 下执行。");
return;
}
if (GameManager.Instance == null)
{
Log("GameManager 未初始化,无法启动读档协程。");
return;
}
if (!SlotDirectory.Exists(slotIndex))
{
Log($"slot_{slotIndex} 不存在,无法读档。");
return;
}
GameManager.Instance.StartWithSlot(slotIndex);
Log($"已启动 Restore slot_{slotIndex}。请观察 Restore Pipeline Log 与场景状态。");
}
private void OpenSaveDirectory()
{
var path = ConstRef.SaveFilePath;
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
EditorUtility.RevealInFinder(path);
}
#endregion
#region Helpers
private void PrintSnapshotSummary(SaveSnapshot snapshot, string label)
{
var json = SnapshotPersistence.Serialize(snapshot);
var lines = new List<string>
{
$"=== {label} ===",
$"gameVersion: {snapshot.gameVersion}",
$"savedAt: {snapshot.savedAt}",
$"schemaVersion: {snapshot.schemaVersion}",
$"scene: {snapshot.scene?.sceneName}",
$"anchor.sceneSoName: {snapshot.anchor?.sceneSoName}",
$"anchor.yarnProjectId: {snapshot.anchor?.yarnProjectId}",
$"anchor.nodeName: {snapshot.anchor?.nodeName}",
$"yarnVariables.floats: {snapshot.yarnVariables?.floats?.Count ?? 0} entries",
$"yarnVariables.strings: {snapshot.yarnVariables?.strings?.Count ?? 0} entries",
$"yarnVariables.bools: {snapshot.yarnVariables?.bools?.Count ?? 0} entries",
$"sections: {(snapshot.sections != null ? string.Join(", ", snapshot.sections.Keys) : "null")}",
};
if (snapshot.sections != null)
{
AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.PunchTape);
AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.Fix);
AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.FixPanel);
AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.BodyModule);
AppendFixSectionHint(lines, snapshot, SnapshotProviderIds.Eye);
}
lines.Add("JSON preview (first 1500 chars):");
lines.Add(json.Length > 1500 ? json.Substring(0, 1500) + "..." : json);
Log(string.Join("\n", lines));
}
private static void AppendFixSectionHint(List<string> lines, SaveSnapshot snapshot, string sectionId)
{
if (snapshot.sections.ContainsKey(sectionId))
{
lines.Add($" [fix-related] {sectionId}: captured");
}
}
private void Log(string message)
{
_lastLog = $"[{DateTime.Now:HH:mm:ss}] {message}";
Debug.Log($"[SaveSystemValidator] {message}");
Repaint();
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9719ab6f265683c4484a170f6c4dc843
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+17 -2
View File
@@ -1489,8 +1489,8 @@ MonoBehaviour:
m_ShadowVolumeIntensityEnabled: 0
m_ShadowVolumeIntensity: 0.75
m_LocalBounds:
m_Center: {x: 0.69834995, y: -3.87525, z: 0}
m_Extent: {x: 9.61685, y: 5.77315, z: 0}
m_Center: {x: 0.69945, y: -3.8748999, z: 0}
m_Extent: {x: 9.62075, y: 5.7716, z: 0}
m_PointLightInnerAngle: 360
m_PointLightOuterAngle: 360
m_PointLightInnerRadius: 0
@@ -1889,6 +1889,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 8225446392f64688813d628ffb56cc7c, type: 3}
m_Name:
m_EditorClassIdentifier:
slotKey: 14
data:
dialogViewType: 7
bubbleSlots: []
@@ -2230,6 +2231,7 @@ GameObject:
m_Component:
- component: {fileID: 1964514168895772326}
- component: {fileID: 6318711606202227434}
- component: {fileID: 5359247006764324596}
m_Layer: 0
m_Name: Analysis Camera
m_TagString: Untagged
@@ -2292,6 +2294,19 @@ MonoBehaviour:
m_Calls: []
m_LegacyBlendHint: 0
m_ComponentOwner: {fileID: 7397998753940568990}
--- !u!114 &5359247006764324596
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9005043052954555028}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7432789ba6bd1a340b824676e1e56184, type: 3}
m_Name:
m_EditorClassIdentifier:
cameraType: 27
--- !u!1 &9040823909663253672
GameObject:
m_ObjectHideFlags: 0
@@ -119,13 +119,3 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 79080239c808e074cbbffada2dd2045b, type: 3}
m_Name:
m_EditorClassIdentifier:
actorData:
onSave:
m_PersistentCalls:
m_Calls: []
actorName:
slotName:
actorType: 0
stateName:
alpha: 0
faceParam: 0
@@ -1359,8 +1359,8 @@ MonoBehaviour:
m_ShadowVolumeIntensityEnabled: 0
m_ShadowVolumeIntensity: 0.75
m_LocalBounds:
m_Center: {x: 0.032999992, y: -0.060499907, z: 0}
m_Extent: {x: 14.1184, y: 5.5629997, z: 0}
m_Center: {x: 0.03494978, y: -0.07005, z: 0}
m_Extent: {x: 14.116449, y: 5.57125, z: 0}
m_PointLightInnerAngle: 360
m_PointLightOuterAngle: 360
m_PointLightInnerRadius: 0
@@ -1959,6 +1959,7 @@ GameObject:
m_Component:
- component: {fileID: 8988582123737961170}
- component: {fileID: 5690280413635320205}
- component: {fileID: 3792378371869211096}
m_Layer: 0
m_Name: Clinic Camera
m_TagString: Untagged
@@ -2021,6 +2022,19 @@ MonoBehaviour:
m_Calls: []
m_LegacyBlendHint: 0
m_ComponentOwner: {fileID: 6713959257411942534}
--- !u!114 &3792378371869211096
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6462107428085023161}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7432789ba6bd1a340b824676e1e56184, type: 3}
m_Name:
m_EditorClassIdentifier:
cameraType: 1
--- !u!1 &6543365627926242830
GameObject:
m_ObjectHideFlags: 0
@@ -2352,6 +2366,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 8225446392f64688813d628ffb56cc7c, type: 3}
m_Name:
m_EditorClassIdentifier:
slotKey: 1
data:
dialogViewType: 1
bubbleSlots: []
@@ -2618,6 +2633,7 @@ GameObject:
m_Component:
- component: {fileID: 1289813725535206033}
- component: {fileID: 258174790319769401}
- component: {fileID: 3093863673183708419}
m_Layer: 0
m_Name: Cooling Camera
m_TagString: Untagged
@@ -2680,3 +2696,16 @@ MonoBehaviour:
m_Calls: []
m_LegacyBlendHint: 0
m_ComponentOwner: {fileID: 7830223793077688558}
--- !u!114 &3093863673183708419
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8940458651926956810}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7432789ba6bd1a340b824676e1e56184, type: 3}
m_Name:
m_EditorClassIdentifier:
cameraType: 2
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-99
View File
@@ -1,99 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1001 &1360354125466394617
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 0}
m_Modifications:
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_Pivot.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_Pivot.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_AnchorMax.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_AnchorMax.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_AnchorMin.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_AnchorMin.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_SizeDelta.x
value: 250
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_SizeDelta.y
value: 190
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_LocalPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_LocalPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_LocalPosition.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_LocalRotation.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_LocalRotation.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_LocalRotation.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_AnchoredPosition.x
value: -795
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_AnchoredPosition.y
value: -263
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1347914344471040156, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8732203183804650455, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
propertyPath: m_Name
value: "UF\u7ED8\u56FE"
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 5a170c0b6c933c540a86be691fa6745c, type: 3}
File diff suppressed because it is too large Load Diff
+17 -2
View File
@@ -431,6 +431,7 @@ GameObject:
m_Component:
- component: {fileID: 952162656386370984}
- component: {fileID: 1423545933766743583}
- component: {fileID: 3423442071072641733}
m_Layer: 0
m_Name: Sell Deep Camera
m_TagString: Untagged
@@ -493,6 +494,19 @@ MonoBehaviour:
m_Calls: []
m_LegacyBlendHint: 0
m_ComponentOwner: {fileID: 7541972663295669641}
--- !u!114 &3423442071072641733
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2172269519109149815}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7432789ba6bd1a340b824676e1e56184, type: 3}
m_Name:
m_EditorClassIdentifier:
cameraType: 30
--- !u!1 &2448689962436376099
GameObject:
m_ObjectHideFlags: 0
@@ -1887,8 +1901,8 @@ MonoBehaviour:
m_ShadowVolumeIntensityEnabled: 0
m_ShadowVolumeIntensity: 0.75
m_LocalBounds:
m_Center: {x: 0.69924974, y: -3.88405, z: 0}
m_Extent: {x: 9.612249, y: 5.7785497, z: 0}
m_Center: {x: 0.7031002, y: -3.88395, z: 0}
m_Extent: {x: 9.6236, y: 5.77255, z: 0}
m_PointLightInnerAngle: 360
m_PointLightOuterAngle: 360
m_PointLightInnerRadius: 0
@@ -1950,6 +1964,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 8225446392f64688813d628ffb56cc7c, type: 3}
m_Name:
m_EditorClassIdentifier:
slotKey: 7
data:
dialogViewType: 1
bubbleSlots: []
@@ -1,4 +1,4 @@
title: EmoPlugIn
tags: 待编辑
colorID: 8
@@ -53,7 +53,7 @@ position: 480,-2840
<<jump 检查视觉>>
===
title: IntoEyeView
tags:
tags: event
group:视觉检查
colorID: 6
position: 244,271
@@ -108,26 +108,29 @@ position: 244,271
===
title: MemoryDeepIn
tags:
tags: event
---
<<if visited("OnMemoryViewShow")==false>>
<<jump 第一次进入记忆时>>
<<endif>>
===
title: UFDeepIn
tags:
tags: event
---
<<jump UF深入检查>>
===
title: EyeDeepIn
tags:
tags: event
position: -9,21
---
<<switch_fix_system_to Eye>>
<<ModuleHightLight Eye false>>
===
title: OnMemoryInsert
tags:
tags: event
group:记忆
colorID: 3
position: -1580,-2168
+1
View File
@@ -976,6 +976,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 8225446392f64688813d628ffb56cc7c, type: 3}
m_Name:
m_EditorClassIdentifier:
slotKey: 10
data:
dialogViewType: 1
bubbleSlots: []
+1
View File
@@ -364,6 +364,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 8225446392f64688813d628ffb56cc7c, type: 3}
m_Name:
m_EditorClassIdentifier:
slotKey: 10
data:
dialogViewType: 1
bubbleSlots: []
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+30 -2
View File
@@ -518,6 +518,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 8225446392f64688813d628ffb56cc7c, type: 3}
m_Name:
m_EditorClassIdentifier:
slotKey: 8
data:
dialogViewType: 1
bubbleSlots: []
@@ -568,8 +569,6 @@ MonoBehaviour:
m_EditorClassIdentifier:
subwaySlots: {fileID: 143273129}
tvSlots: {fileID: 494482151}
subwayCam: {fileID: 233946549}
tvCam: {fileID: 1263973096}
newsSpriteRenderer: {fileID: 5352226562362177808}
arriveStation: {fileID: 969974277}
--- !u!1 &226184625
@@ -698,6 +697,7 @@ GameObject:
m_Component:
- component: {fileID: 233946548}
- component: {fileID: 233946549}
- component: {fileID: 233946550}
m_Layer: 0
m_Name: Subway Camera
m_TagString: Untagged
@@ -760,6 +760,19 @@ MonoBehaviour:
m_Calls: []
m_LegacyBlendHint: 0
m_ComponentOwner: {fileID: 511564811}
--- !u!114 &233946550
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 233946547}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7432789ba6bd1a340b824676e1e56184, type: 3}
m_Name:
m_EditorClassIdentifier:
cameraType: 23
--- !u!1 &252705163
GameObject:
m_ObjectHideFlags: 0
@@ -1574,6 +1587,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 8225446392f64688813d628ffb56cc7c, type: 3}
m_Name:
m_EditorClassIdentifier:
slotKey: 9
data:
dialogViewType: 1
bubbleSlots: []
@@ -2685,6 +2699,7 @@ GameObject:
m_Component:
- component: {fileID: 1263973095}
- component: {fileID: 1263973096}
- component: {fileID: 1263973097}
m_Layer: 0
m_Name: TV Camera
m_TagString: Untagged
@@ -2747,6 +2762,19 @@ MonoBehaviour:
m_Calls: []
m_LegacyBlendHint: 0
m_ComponentOwner: {fileID: 11266075}
--- !u!114 &1263973097
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1263973094}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7432789ba6bd1a340b824676e1e56184, type: 3}
m_Name:
m_EditorClassIdentifier:
cameraType: 24
--- !u!1 &1364179052
GameObject:
m_ObjectHideFlags: 0
File diff suppressed because it is too large Load Diff
+1
View File
@@ -735,6 +735,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 8225446392f64688813d628ffb56cc7c, type: 3}
m_Name:
m_EditorClassIdentifier:
slotKey: 10
data:
dialogViewType: 2
bubbleSlots: []
+2
View File
@@ -785,6 +785,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 8225446392f64688813d628ffb56cc7c, type: 3}
m_Name:
m_EditorClassIdentifier:
slotKey: 10
data:
dialogViewType: 1
bubbleSlots: []
@@ -1358,6 +1359,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 8225446392f64688813d628ffb56cc7c, type: 3}
m_Name:
m_EditorClassIdentifier:
slotKey: 11
data:
dialogViewType: 1
bubbleSlots: []
@@ -16,7 +16,7 @@ MonoBehaviour:
- exitName: default
targetScene: {fileID: 11400000, guid: 653fbfbb55fef7740a2360590e29d552, type: 2}
yarnProject: {fileID: 7545569452688597077, guid: 29ebe788cfe2c084e88ffdd8e2f93378, type: 3}
firstSceneName: Scene/ClinicOut
firstSceneName: "Scene/\u9152\u5427\u573A\u666F"
title: "\u9152\u676F\u548C\u724C"
description: "\u9152\u676F\u548C\u724C"
coverPic: {fileID: 21300000, guid: df86302e77350e34ea422cd24043dd93, type: 3}
+20
View File
@@ -23,6 +23,26 @@ namespace AibisDream.Utility
public static readonly string SaveFilePath = Path.Combine(Application.persistentDataPath, "AllOurBrokenParts", "saves");
/// <summary>测试阶段自动存档历史目录;每次自动存档独立子文件夹,不覆盖正式槽位。</summary>
public static readonly string TestAutoSaveArchivePath =
Path.Combine(Application.persistentDataPath, "AllOurBrokenParts", "testsavs");
/// <summary>P2 槽位相关常量。</summary>
public const int SaveSlotCount = 8;
public const int SaveThumbnailWidth = 640;
public const int SaveThumbnailHeight = 360;
public const string SaveSlotDirPrefix = "slot_";
public const string SaveSnapshotFileName = "snapshot";
public const string SaveMetaFileName = "meta";
public const string SaveThumbnailFileName = "thumbnail";
public const string LatestSlotFileName = "latest_slot";
/// <summary>P1 快照层自测落盘路径(P2 槽位层接管后迁移)。</summary>
public static readonly string SnapshotTestPath =
Path.Combine(Application.persistentDataPath, "AllOurBrokenParts", "snapshot_test");
public const string SnapshotTestFileName = "latest_snapshot";
public static readonly string ChapterProgressPath = Path.Combine(Application.persistentDataPath, "AllOurBrokenParts", "Config", "chapter.json");
public static readonly string CharacterConfigPath = Path.Combine(Application.streamingAssetsPath, "Config", "character.csv");
@@ -15,7 +15,7 @@ namespace AibisDream
public static void NextYarn(string exitName = null)
{
EnumEventSystem.Global.Send(EventEnum.NextYarn);
StorageSystem.Instance.ClearLocal();
YarnVariableStorage.Instance.ClearLocal();
DialogController.Instance.StartCoroutine(GameManager.Instance.NextSceneSo(exitName));
}
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using AibisDream.Kit;
using AibisDream.SaveSystem;
using UnityEngine;
using UnityEngine.Events;
using AibisDream.Framework;
@@ -16,20 +17,30 @@ namespace AibisDream
[SerializeField] private LineAdvanceInput lineAdvanceInput;
private string _currentNodeName;
private string[] _currentNodeTags;
private void Start()
{
_dialogueRunner = GetComponentInChildren<DialogueRunner>();
_dialogueRunner.onDialogueStart ??= new UnityEvent();
_dialogueRunner.onDialogueComplete ??= new UnityEvent();
_dialogueRunner.onNodeStart ??= new UnityEventString();
_dialogueRunner.onNodeComplete ??= new UnityEventString();
_dialogueRunner.onDialogueStart.AddListener(() =>
{
EnumEventSystem.Global.Send(InteractionEventEnum.DialogStart);
});
_dialogueRunner.onDialogueComplete.AddListener(() =>
{
EnumEventSystem.Global.Send(InteractionEventEnum.DialogEnd);
ClearCurrentNodeContext();
});
_dialogueRunner.onNodeStart.AddListener(OnNodeStart);
_dialogueRunner.onNodeComplete.AddListener(OnNodeComplete);
}
private void OnEnable()
@@ -76,6 +87,7 @@ namespace AibisDream
/// <param name="yarnProject">Yarn组</param>
public void StartDialog(YarnProject yarnProject)
{
PrepareYarnProjectForDialog(yarnProject);
_dialogueRunner.SetProject(yarnProject);
_ = _dialogueRunner.StartDialogue("Start");
}
@@ -83,14 +95,21 @@ namespace AibisDream
public void StopDialog()
{
_ = _dialogueRunner.Stop();
StorageSystem.Instance.Clear();
SavePointEvaluator.ResetForProject(null);
YarnVariableStorage.Instance.Clear();
DialogUIManager.Instance.HideDialog();
}
public void LoadDialog(YarnProject yarnProject)
{
PrepareYarnProjectForDialog(yarnProject);
_dialogueRunner.SetProject(yarnProject);
_ = _dialogueRunner.StartDialogue("Center");
}
private static void PrepareYarnProjectForDialog(YarnProject yarnProject)
{
var projectId = yarnProject != null ? yarnProject.name : null;
SavePointEvaluator.ResetForProjectIfChanged(projectId);
}
public void StartDialogNode(string nodeName)
@@ -140,30 +159,66 @@ namespace AibisDream
/// </summary>
public (string nodeName, IReadOnlyList<string> tags) GetCurrentNodeContext()
{
if (_dialogueRunner == null)
if (string.IsNullOrEmpty(_currentNodeName))
{
return (null, Array.Empty<string>());
}
var dialogue = _dialogueRunner.Dialogue;
if (dialogue == null)
return (_currentNodeName, _currentNodeTags ?? Array.Empty<string>());
}
private void OnNodeStart(string nodeName)
{
UpdateCurrentNodeContext(nodeName);
var projectId = _dialogueRunner?.YarnProject?.name;
var tags = _currentNodeTags ?? Array.Empty<string>();
if (SaveRestoreOrchestrator.IsRestoring)
{
return (null, Array.Empty<string>());
SavePointEvaluator.OnRestoreEnterNode(projectId, nodeName);
return;
}
var nodeName = dialogue.CurrentNode;
if (string.IsNullOrEmpty(nodeName))
if (!SavePointEvaluator.OnNodeStartForAutoSave(projectId, nodeName, tags, out var reason))
{
return (null, Array.Empty<string>());
if (reason == SavePointRejectReason.DetourResume)
{
Debug.Log($"[DialogController] 跳过 detour 返回续跑的自动存档: {nodeName}");
}
return;
}
var tagsHeader = dialogue.GetHeaderValue(nodeName, "tags");
if (string.IsNullOrWhiteSpace(tagsHeader))
StartCoroutine(SaveRestoreOrchestrator.AutoSaveRoutine(nodeName));
}
private void OnNodeComplete(string nodeName)
{
SavePointEvaluator.OnNodeComplete(_dialogueRunner?.YarnProject?.name, nodeName);
ClearCurrentNodeContext();
}
private void UpdateCurrentNodeContext(string nodeName)
{
if (_dialogueRunner == null || _dialogueRunner.Dialogue == null)
{
return (nodeName, Array.Empty<string>());
ClearCurrentNodeContext();
return;
}
return (nodeName, tagsHeader.Split(Array.Empty<char>(), StringSplitOptions.RemoveEmptyEntries));
_currentNodeName = nodeName;
var tagsHeader = _dialogueRunner.Dialogue.GetHeaderValue(nodeName, "tags");
_currentNodeTags = string.IsNullOrWhiteSpace(tagsHeader)
? Array.Empty<string>()
: tagsHeader.Split(Array.Empty<char>(), StringSplitOptions.RemoveEmptyEntries);
}
private void ClearCurrentNodeContext()
{
_currentNodeName = null;
_currentNodeTags = null;
}
#region
@@ -4,7 +4,7 @@ using UnityEngine;
using DG.Tweening;
using Yarn.Unity;
using AibisDream.Framework;
using Cinemachine;
using AibisDream.Kit;
using UnityEngine.Rendering;
using UnityEngine.UI;
using AibisDream.Kit;
@@ -37,8 +37,6 @@ namespace AibisDream.FixSystem
private static readonly int EnableSaturationId = Shader.PropertyToID("_EnableSaturation");
private static readonly int SaturationId = Shader.PropertyToID("_Saturation");
[SerializeField] private BubbleSlotGroup bubbleSlotGroup;
public BubbleSlotGroup BubbleSlotGroup => bubbleSlotGroup;
[SerializeField] private GameObject memoryView;
[SerializeField] private GameObject projector;
[SerializeField] private SpriteRenderer memoryFinishedSpriteRenderer; // 用于显示记忆处理结束的表现
@@ -55,7 +53,6 @@ namespace AibisDream.FixSystem
[SerializeField] private CanvasGroup playBackground;
[SerializeField] private GameObject memoryGroup;
[SerializeField] private CinemachineVirtualCamera memoryCamera;
private bool isProcessing = false; // 新增变量来追踪是否正在处理记忆
private bool isPlayingFaderSound = false; // 追踪音效是否正在播放
@@ -163,8 +160,6 @@ namespace AibisDream.FixSystem
FixSystemCenter.SystemDic.Register(this);
if (memoryGroup != null)
memoryGroup.SetActive(false);
if (memoryCamera != null)
CameraKit.Instance.RegisterCamera(CameraEnum.Memory, memoryCamera);
}
@@ -363,7 +358,7 @@ namespace AibisDream.FixSystem
/// <summary>
/// 单条记忆播放结束后的复位:不调用 <see cref="MemorySlider.Init"/>(避免再次锁条并触发打开 Timeline),
/// 仅禁用拖动;完整 <see cref="MemorySlider.LockSlider"/> 仅在退出 <see cref="MemoryState"/> 时 <see cref="OnExit"/> 执行。
/// 仅禁用拖动;完整 <see cref="MemorySlider.LockSlider"/> 仅在退出 <see cref="MemoryCue"/> 时 <see cref="OnExit"/> 执行。
/// </summary>
private void ResetAfterMemoryFinished()
{
@@ -1,14 +1,14 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using AibisDream.Kit;
using AibisDream.SaveSystem;
using UnityEngine;
using AibisDream.Framework;
namespace AibisDream.FixSystem
{
/// <summary>
/// 打孔带系统入口:收藏数据、存档注册、Yarn 命令、弹窗与收藏夹面板代理。
/// 打孔带系统入口:收藏数据、Yarn 命令、弹窗与收藏夹面板代理。
/// </summary>
public class PunchTapeManager : Singleton<PunchTapeManager>
{
@@ -19,48 +19,54 @@ namespace AibisDream.FixSystem
[SerializeField] private PunchTapePrinter punchTapePrinter;
public PunchTapePrinter PunchTapePrinter => punchTapePrinter;
private readonly PunchTapeCollectionData _collectionData = new();
private readonly List<PunchTape> _favorites = new();
private GameObject currentPunchTapeGO;
/// <summary>收藏列表变更时通知 <see cref="PunchTapeFavoritesPanel"/> 等订阅方。</summary>
public event Action OnCollectionChanged;
public override void OnSingletonInit()
{
StorageSystem.Instance.RegisterData(_collectionData);
_collectionData.LoadEvent += OnCollectionDataLoaded;
}
public override void OnSingletonDestroy()
{
_collectionData.LoadEvent -= OnCollectionDataLoaded;
}
private void OnCollectionDataLoaded(PunchTapeCollectionData data)
{
OnCollectionChanged?.Invoke();
}
public IReadOnlyList<PunchTape> GetFavorites()
{
return _collectionData.punchTapeFavorites;
return _favorites;
}
public PunchTapeSnapshotDto CapturePunchTapeSnapshot()
{
return new PunchTapeSnapshotDto
{
favorites = _favorites.Select(t => new PunchTape(t.Category, t.ClueName, t.MemoryIndex)
{
used = t.used
}).ToList()
};
}
public void ApplyPunchTapeSnapshot(PunchTapeSnapshotDto dto)
{
_favorites.Clear();
if (dto?.favorites != null)
{
_favorites.AddRange(dto.favorites);
}
OnCollectionChanged?.Invoke();
}
public void AddPunchTape(PunchTape punchTape)
{
if (punchTape == null) return;
if (_collectionData.punchTapeFavorites.Contains(punchTape))
if (_favorites.Contains(punchTape))
return;
_collectionData.punchTapeFavorites.Add(punchTape);
_favorites.Add(punchTape);
OnCollectionChanged?.Invoke();
}
public void RemovePunchTape(PunchTape punchTape)
{
if (punchTape == null) return;
if (!_collectionData.punchTapeFavorites.Remove(punchTape))
if (!_favorites.Remove(punchTape))
return;
OnCollectionChanged?.Invoke();
@@ -129,20 +135,4 @@ namespace AibisDream.FixSystem
currentPunchTapeGO = null;
}
}
/// <summary>
/// 打孔带收藏列表的存档载体;由 <see cref="PunchTapeManager"/> 注册到 <see cref="StorageSystem"/>。
/// </summary>
public class PunchTapeCollectionData : IData
{
public List<PunchTape> punchTapeFavorites = new();
public event Action<PunchTapeCollectionData> LoadEvent;
public IEnumerator Load()
{
LoadEvent?.Invoke(this);
yield break;
}
}
}
}
@@ -1,12 +1,12 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using AibisDream.Framework;
using Cinemachine;
using JetBrains.Annotations;
using UnityEngine;
using AibisDream.FixSystem;
using AibisDream.Kit;
using AibisDream.SaveSystem;
namespace AibisDream.FixSystem
{
@@ -25,10 +25,13 @@ namespace AibisDream.FixSystem
#region
private readonly ModuleSystemData _data = new();
private ModuleState _moduleState = ModuleState.Body;
public bool inHotErr;
private int _heatValue;
/// <summary>获取当前 BodyModule 分组(Body/Head)。</summary>
public ModuleState GetCurrentModuleState() => _moduleState;
private int HeatValue
{
get => _heatValue;
@@ -55,9 +58,6 @@ namespace AibisDream.FixSystem
private void InitComponentRefs()
{
// 注册数据
StorageSystem.Instance.RegisterData(_data);
// 处理内部索引
_bodyModuleGroup = transform.Find("Scalehandler")?.transform.Find("Body Module Group")?.gameObject;
_headModuleGroup = transform.Find("Scalehandler")?.transform.Find("Head Module Group")?.gameObject;
@@ -70,16 +70,73 @@ namespace AibisDream.FixSystem
// 注册到维修系统管理器
RepairSystemManager.Register(this);
// 注册Camera
var virtualCam2 = transform.Find("Body Module Camera Center").GetComponent<ICinemachineCamera>();
CameraKit.Instance.RegisterCamera(CameraEnum.BodyModuleCenter, virtualCam2);
}
private void OnDestroy()
/// <summary>捕获插线模块持久状态(分组 + UF + Chip + 高亮/报警)。</summary>
public BodyModuleSnapshotDto CaptureBodyModuleSnapshot()
{
StorageSystem.Instance.UnregisterData<ModuleSystemData>();
CameraKit.Instance.UnRegisterCamera(CameraEnum.BodyModuleCenter);
var dto = new BodyModuleSnapshotDto
{
moduleState = _moduleState.ToString()
};
var ufModule = FindBodyModuleByName("UF")?.GetComponent<UFModule>();
ufModule?.CaptureState(dto);
var chipModule = FindBodyModuleByName("Color")?.GetComponent<ChipModule>();
chipModule?.CaptureState(dto);
foreach (var module in EnumerateAllBodyModules())
{
var name = module.data.moduleName;
if (module.IsHighlighted) dto.highlightedModules.Add(name);
if (module.IsAlarmed) dto.alarmedModules.Add(name);
}
return dto;
}
/// <summary>写回插线模块持久状态;须在 fix cue 还原之后调用。</summary>
public void ApplyBodyModuleSnapshot(BodyModuleSnapshotDto dto)
{
if (dto == null) return;
if (!string.IsNullOrEmpty(dto.moduleState)
&& Enum.TryParse<ModuleState>(dto.moduleState, out var moduleState))
{
SwitchModuleGroup(moduleState);
}
var ufModule = FindBodyModuleByName("UF")?.GetComponent<UFModule>();
ufModule?.ApplyState(dto);
var chipModule = FindBodyModuleByName("Color")?.GetComponent<ChipModule>();
chipModule?.ApplyState(dto);
var highlighted = new HashSet<string>(dto.highlightedModules ?? Enumerable.Empty<string>());
var alarmed = new HashSet<string>(dto.alarmedModules ?? Enumerable.Empty<string>());
foreach (var module in EnumerateAllBodyModules())
{
var name = module.data.moduleName;
module.Highlight(highlighted.Contains(name));
module.Alarm(alarmed.Contains(name));
}
}
private IEnumerable<BodyModule> EnumerateAllBodyModules()
{
if (_bodyModuleGroup != null)
{
foreach (var module in _bodyModuleGroup.GetComponentsInChildren<BodyModule>(true))
yield return module;
}
if (_headModuleGroup != null)
{
foreach (var module in _headModuleGroup.GetComponentsInChildren<BodyModule>(true))
yield return module;
}
}
/// <summary>
@@ -242,8 +299,7 @@ namespace AibisDream.FixSystem
// 重新获取ModuleGroup
var root = moduleState == ModuleState.Body ? _bodyModuleGroup : _headModuleGroup;
BodyModules = root?.GetComponentsInChildren<BodyModule>().ToList();
// 更改data
_data.moduleState = moduleState;
_moduleState = moduleState;
}
/// <summary>
@@ -383,18 +439,6 @@ namespace AibisDream.FixSystem
}
}
[LoadIndex(2)]
public class ModuleSystemData : IData
{
public ModuleState moduleState;
public IEnumerator Load()
{
FixSystemCenter.SystemDic.Get<BodyModuleSystem>().SwitchModuleGroup(moduleState);
yield break;
}
}
public enum ModuleState
{
Body,
@@ -67,7 +67,7 @@ namespace AibisDream.FixSystem
private void LoadCableState()
{
if (StorageSystem.Instance.TryGetValue(CableRetractedKey, out bool savedRetractedState))
if (YarnVariableStorage.Instance.TryGetValue(CableRetractedKey, out bool savedRetractedState))
{
if (isCableRetracted)
{
@@ -89,7 +89,7 @@ namespace AibisDream.FixSystem
private void SaveCableState()
{
Debug.Log("Save cableRetractedstate" + isCableRetracted);
StorageSystem.Instance.SetValue(CableRetractedKey, isCableRetracted);
YarnVariableStorage.Instance.SetValue(CableRetractedKey, isCableRetracted);
}
private void Update()
@@ -1,7 +1,6 @@
using UnityEngine;
using AibisDream.FixSystem;
using Cinemachine;
using AibisDream.Framework;
using DG.Tweening;
using System.Collections;
using AibisDream.Kit;
@@ -10,7 +9,6 @@ namespace AibisDream
{
public class CuttingManager : MonoBehaviour
{
public BubbleSlotGroup bubbleSlotGroup;
[Header("切割组件")]
public GameObject gearFollowerPrefab;
public GameObject entrancePanelPrefab; // 入场面板预制体
@@ -30,21 +28,7 @@ namespace AibisDream
private void Awake()
{
// 注册
FixSystemCenter.SystemDic.Register(this);
var virtualCam1 = transform.Find("Cut Emo Camera").GetComponent<ICinemachineCamera>();
var virtualCam2 = transform.Find("Cut Memory Camera").GetComponent<ICinemachineCamera>();
var virtualCam3 = transform.Find("Cut Logic Camera").GetComponent<ICinemachineCamera>();
var virtualCam4 = transform.Find("Cut Emo Deep Camera").GetComponent<ICinemachineCamera>();
var virtualCam5 = transform.Find("Cut Memory Deep Camera").GetComponent<ICinemachineCamera>();
var virtualCam6 = transform.Find("Cut Logic Deep Camera").GetComponent<ICinemachineCamera>();
CameraKit.Instance.RegisterCamera(CameraEnum.CutEmo, virtualCam1);
CameraKit.Instance.RegisterCamera(CameraEnum.CutMemory, virtualCam2);
CameraKit.Instance.RegisterCamera(CameraEnum.CutLogic, virtualCam3);
CameraKit.Instance.RegisterCamera(CameraEnum.CutEmoDeep, virtualCam4);
CameraKit.Instance.RegisterCamera(CameraEnum.CutMemoryDeep, virtualCam5);
CameraKit.Instance.RegisterCamera(CameraEnum.CutLogicDeep, virtualCam6);
_mainCamera = Camera.main;
}
@@ -311,17 +295,8 @@ namespace AibisDream
private void OnDestroy()
{
// 从系统字典中注销
FixSystemCenter.SystemDic.Unregister<CuttingManager>();
// 清理相机注册
CameraKit.Instance.UnRegisterCamera(CameraEnum.CutEmo);
CameraKit.Instance.UnRegisterCamera(CameraEnum.CutMemory);
CameraKit.Instance.UnRegisterCamera(CameraEnum.CutLogic);
CameraKit.Instance.UnRegisterCamera(CameraEnum.CutEmoDeep);
CameraKit.Instance.UnRegisterCamera(CameraEnum.CutMemoryDeep);
CameraKit.Instance.UnRegisterCamera(CameraEnum.CutLogicDeep);
// 强制清理所有相关对象
if (gearFollower != null)
{
@@ -28,6 +28,10 @@ namespace AibisDream.FixSystem
/// Yarn 可能在 Start 里异步加载材质完成前就调用 ModuleHightLight,需记下来待加载后再套材质。
/// </summary>
private bool _wantsHighlight;
private bool _wantsAlarm;
public bool IsHighlighted => _wantsHighlight;
public bool IsAlarmed => _wantsAlarm;
[SerializeField]
private SpriteRenderer targetSpriteRenderer; // 目标的 SpriteRenderer
@@ -99,7 +103,7 @@ namespace AibisDream.FixSystem
else
Debug.LogError("[BodyModule] Failed to load AlarmMaterial");
ApplyHighlightVisual();
ApplyModuleVisual();
}
#region
@@ -165,23 +169,25 @@ namespace AibisDream.FixSystem
public void Highlight(bool enable)
{
_wantsHighlight = enable;
ApplyHighlightVisual();
}
private void ApplyHighlightVisual()
{
if (targetSpriteRenderer == null || _highlightMaterial == null) return;
if (_alarmMaterial != null && targetSpriteRenderer.material == _alarmMaterial) return;
targetSpriteRenderer.material = _wantsHighlight ? _highlightMaterial : _originalMaterial;
ApplyModuleVisual();
}
public void Alarm(bool enable)
{
if (targetSpriteRenderer != null && _alarmMaterial != null)
{
targetSpriteRenderer.material = enable ? _alarmMaterial : _originalMaterial;
}
_wantsAlarm = enable;
ApplyModuleVisual();
}
private void ApplyModuleVisual()
{
if (targetSpriteRenderer == null) return;
if (_wantsAlarm && _alarmMaterial != null)
targetSpriteRenderer.material = _alarmMaterial;
else if (_wantsHighlight && _highlightMaterial != null)
targetSpriteRenderer.material = _highlightMaterial;
else
targetSpriteRenderer.material = _originalMaterial;
}
#endregion
@@ -1,27 +1,15 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using AibisDream.Framework;
using AibisDream;
using System;
using AibisDream.SaveSystem;
public class ChipModule : MonoBehaviour
{
private GameObject _chipModule;
private ChipData _chipData=new ();
private bool _isChipRemoved;
private void Awake()
{
// 将数据注册到数据容器内
StorageSystem.Instance.RegisterData(_chipData);
// 添加加载逻辑
_chipData.LoadEvent += LoadData;
_chipModule = transform.Find("Chip").gameObject;
}
// Start is called before the first frame update
public void RemoveChipModule()
{
@@ -30,7 +18,7 @@ public class ChipModule : MonoBehaviour
_chipModule.SetActive(false);
}
_chipData.isChipRemoved = true;
_isChipRemoved = true;
}
public void InstallChipModule()
@@ -40,35 +28,29 @@ public class ChipModule : MonoBehaviour
_chipModule.SetActive(true);
}
_chipData.isChipRemoved = false;
_isChipRemoved = false;
}
public void CaptureState(BodyModuleSnapshotDto dto)
{
if (dto == null) return;
dto.isChipRemoved = _isChipRemoved;
}
public void ApplyState(BodyModuleSnapshotDto dto)
{
if (dto == null) return;
_isChipRemoved = dto.isChipRemoved;
Chipinit();
}
public void Chipinit()
{
_chipModule.SetActive(!_chipData.isChipRemoved);
_chipModule.SetActive(!_isChipRemoved);
}
private void LoadData(ChipData ufData)
{
Chipinit(); // 读取存档文件时触发,将当前系统恢复为目标状态
}
void Start()
private void Start()
{
Chipinit();
}
}
public class ChipData : IData
{
public bool isChipRemoved = false;
public event Action<ChipData> LoadEvent;
public IEnumerator Load()
{
LoadEvent?.Invoke(this);
yield break;
}
}
@@ -1,25 +1,19 @@
using UnityEngine;
using AibisDream.Framework;
using AibisDream;
using System;
using System.Collections;
using AibisDream.SaveSystem;
public class UFModule : MonoBehaviour
{
private GameObject _ufCover;
private GameObject _ufModule;
private GameObject _ufSocket;
private GameObject _ufColor;
private UFData _ufData = new();
private bool _isUFCoverRemoved;
private bool _isUFRemoved;
private bool _isColorRemoved;
private void Awake()
{
// 将数据注册到数据容器内
StorageSystem.Instance.RegisterData(_ufData);
// 添加加载逻辑
_ufData.LoadEvent += LoadData;
_ufModule = transform.Find("Module Pic").gameObject;
_ufSocket = transform.Find("Socket").gameObject;
_ufCover = _ufModule.transform.Find("Cover").gameObject;
@@ -33,7 +27,7 @@ public class UFModule : MonoBehaviour
_ufCover.SetActive(false);
}
_ufData.isUFCoverRemoved = true;
_isUFCoverRemoved = true;
}
public void RemoveUFModule()
@@ -42,10 +36,9 @@ public class UFModule : MonoBehaviour
{
_ufModule.SetActive(false);
_ufSocket.SetActive(false);
}
_ufData.isUFRemoved = true;
_isUFRemoved = true;
}
public void InstallUFCover()
@@ -55,7 +48,7 @@ public class UFModule : MonoBehaviour
_ufCover.SetActive(true);
}
_ufData.isUFCoverRemoved = false;
_isUFCoverRemoved = false;
}
public void InstallUFModule()
@@ -66,8 +59,9 @@ public class UFModule : MonoBehaviour
_ufSocket.SetActive(true);
}
_ufData.isUFRemoved = false;
_isUFRemoved = false;
}
public void InstallUFColor()
{
if (!_ufColor.activeInHierarchy)
@@ -75,8 +69,9 @@ public class UFModule : MonoBehaviour
_ufColor.SetActive(true);
}
_ufData.isColorRemoved = false;
_isColorRemoved = false;
}
public void RemoveUFColor()
{
if (_ufColor.activeInHierarchy)
@@ -84,42 +79,36 @@ public class UFModule : MonoBehaviour
_ufColor.SetActive(false);
}
_ufData.isColorRemoved = true;
_isColorRemoved = true;
}
public void CaptureState(BodyModuleSnapshotDto dto)
{
if (dto == null) return;
dto.isUFCoverRemoved = _isUFCoverRemoved;
dto.isUFRemoved = _isUFRemoved;
dto.isColorRemoved = _isColorRemoved;
}
public void ApplyState(BodyModuleSnapshotDto dto)
{
if (dto == null) return;
_isUFCoverRemoved = dto.isUFCoverRemoved;
_isUFRemoved = dto.isUFRemoved;
_isColorRemoved = dto.isColorRemoved;
UFinit();
}
public void UFinit()
{
_ufModule.SetActive(!_ufData.isUFRemoved);
_ufSocket.SetActive(!_ufData.isUFRemoved);
_ufCover.SetActive(!_ufData.isUFCoverRemoved);
_ufColor.SetActive(!_ufData.isColorRemoved);
_ufModule.SetActive(!_isUFRemoved);
_ufSocket.SetActive(!_isUFRemoved);
_ufCover.SetActive(!_isUFCoverRemoved);
_ufColor.SetActive(!_isColorRemoved);
}
private void LoadData(UFData ufData)
{
UFinit(); // 读取存档文件时触发,将当前系统恢复为目标状态
}
void Start()
private void Start()
{
UFinit();
}
}
public class UFData : IData
{
public bool isUFCoverRemoved = false;
public bool isUFRemoved = false;
public bool isColorRemoved = false;
public event Action<UFData> LoadEvent;
public IEnumerator Load()
{
LoadEvent?.Invoke(this);
yield break;
}
}
+26
View File
@@ -94,6 +94,32 @@ namespace AibisDream.FixSystem
OnPullUpSocket?.Invoke();
}
/// <summary>读档/静默:仅恢复插头精灵可见,不触发 CablePanel 事件。</summary>
public void PullUpSocketSilent()
{
_sprite.enabled = true;
}
/// <summary>读档:无动画回到初始位。</summary>
public void RestoreAtStartPositionImmediate()
{
transform.rotation = _defaultRotation;
transform.position = _startPos;
}
/// <summary>读档:无动画插入指定 socket 的视觉终态。</summary>
public void RestoreInsertedAt(Transform socketPos)
{
if (socketPos == null) return;
transform.position = socketPos.position;
_sprite.enabled = false;
}
public void SetSpriteVisible(bool visible)
{
_sprite.enabled = visible;
}
private void OnDrag(BaseEventData eventData)
{
if (!_isDragging) return;
@@ -1,28 +1,18 @@
using AibisDream.FixSystem;
using AibisDream.Framework;
using Cinemachine;
using UnityEngine;
namespace AibisDream
{
public class ClinicSystem : MonoBehaviour
{
[HideInInspector] public BubbleSlotGroup bubbleSlotGroup;
{
private void Awake()
{
FixSystemCenter.SystemDic.Register(this);
bubbleSlotGroup = transform.Find("Bubble Slot Group").GetComponent<BubbleSlotGroup>();
// 注册Camera
var clinicCam = transform.Find("Clinic Camera").GetComponent<ICinemachineCamera>();
CameraKit.Instance.RegisterCamera(CameraEnum.Clinic, clinicCam);
}
private void OnDestroy()
{
FixSystemCenter.SystemDic.Unregister<ClinicSystem>();
CameraKit.Instance.UnRegisterCamera(CameraEnum.Clinic);
}
}
}
}
@@ -0,0 +1,44 @@
using AibisDream.Framework;
namespace AibisDream.FixSystem
{
/// <summary>
/// Runtime registry for systems available to the Fix scene director and cues.
/// The old SystemDic field remains as a compatibility bridge while call sites migrate.
/// </summary>
public class FixSceneContext
{
public IOCContainer Registry { get; }
public FixSceneContext(IOCContainer registry)
{
Registry = registry;
}
public void Register<T>(T instance)
{
Registry.Register(instance);
}
public T Get<T>() where T : class
{
return Registry.Get<T>();
}
public bool TryGet<T>(out T instance) where T : class
{
instance = Registry.Get<T>();
return instance != null;
}
public T Unregister<T>() where T : class
{
return Registry.Unregister<T>();
}
public void Clear()
{
Registry.Clear();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9f04f3d09ebc4c7499d59fb3ab6c7f4a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,115 @@
using System.Collections;
using AibisDream.Framework;
using UnityEngine;
namespace AibisDream.FixSystem
{
/// <summary>
/// Directs Fix scene mode transitions. This is a scene cue board, not a gameplay state machine:
/// cues orchestrate cameras, timelines, UI masks, bubbles and interaction setup.
/// </summary>
public class FixSceneDirector
{
public FixSceneMode CurrentMode { get; private set; }
public string CurrentArgs { get; private set; } = "";
public bool HasMode { get; private set; }
public bool IsTransitioning { get; private set; }
private IFixSceneCue _currentCue;
public IEnumerator Init(FixSceneMode initMode)
{
yield return TransitionInternal(initMode, "", FixTransitionMode.Animated);
}
public IEnumerator TransitionTo(FixSceneMode nextMode, string args = "")
{
yield return TransitionInternal(nextMode, args, FixTransitionMode.Animated);
}
public IEnumerator TransitionToImmediate(FixSceneMode nextMode, string args = "")
{
yield return TransitionInternal(nextMode, args, FixTransitionMode.Immediate);
}
public IEnumerator QuitCurrentMode()
{
if (_currentCue != null)
{
var transition = new FixTransition(CurrentMode, FixSceneMode.Default, "", FixTransitionMode.Animated);
yield return _currentCue.Exit(transition);
}
CurrentArgs = "";
_currentCue = null;
CurrentMode = FixSceneMode.Default;
HasMode = false;
IsTransitioning = false;
}
private IEnumerator TransitionInternal(FixSceneMode nextMode, string args, FixTransitionMode mode)
{
if (IsTransitioning && mode == FixTransitionMode.Animated)
{
Debug.LogWarning($"[FixSceneDirector] Transition ignored while another transition is running: {CurrentMode} -> {nextMode}");
yield break;
}
if (HasMode && CurrentMode.Equals(nextMode))
{
yield break;
}
IsTransitioning = true;
var transition = new FixTransition(HasMode ? CurrentMode : null, nextMode, args, mode);
if (mode == FixTransitionMode.Animated && _currentCue != null)
{
yield return _currentCue.Exit(transition);
}
_currentCue = CreateCue(nextMode);
CurrentMode = nextMode;
CurrentArgs = args ?? "";
HasMode = true;
EnumEventSystem.Global.Send(EventEnum.FixSceneModeChanged, nextMode);
if (mode == FixTransitionMode.Immediate)
{
yield return _currentCue.EnterImmediate(transition);
}
else
{
yield return _currentCue.Enter(transition);
}
IsTransitioning = false;
}
private static IFixSceneCue CreateCue(FixSceneMode nextMode)
{
return nextMode switch
{
FixSceneMode.Default => new DefaultCue(),
FixSceneMode.Clinic => new ClinicCue(),
FixSceneMode.BodyModule => new BodyModuleCue(),
FixSceneMode.Memory => new MemoryCue(),
FixSceneMode.Eye => new EyeCue(),
FixSceneMode.UF => new UfCue(),
FixSceneMode.Expression => new ExpressionCue(),
FixSceneMode.Emo => new EmoCue(),
FixSceneMode.Op => new OpCue(),
FixSceneMode.EmoCut => new EmoCutCue(),
FixSceneMode.MemoryCut => new MemoryCutCue(),
FixSceneMode.LogicCut => new LogicCutCue(),
FixSceneMode.Dream => new DreamCue(),
FixSceneMode.AnalysisMode => new AnalysisModeCue(),
FixSceneMode.Sales => new SalesCue(),
FixSceneMode.BlockPuzzle => new BlockPuzzleCue(),
_ => new DefaultCue()
};
}
}
}
@@ -1,793 +0,0 @@
using System;
using System.Collections;
using AibisDream.Framework;
using AibisDream.Kit;
using AibisDream.UI;
using DG.Tweening;
using UnityEngine;
namespace AibisDream.FixSystem
{
public class FixStateMachine
{
public FixState CurState => _curState.GetState;
private IState<FixState> _curState;
private StateMachine<FixState> _stateMachine;
private FixSystemData _data;
public FixStateMachine(FixSystemData data)
{
_data = data;
_stateMachine = new StateMachine<FixState>(CreateState);
_stateMachine.OnStateSwitch += state => EnumEventSystem.Global.Send(EventEnum.FixStateSwitch, state);
}
public IEnumerator Init(FixState initState)
{
_data.curState = initState;
_data.args = "";
yield return _stateMachine.Init(initState);
}
public IEnumerator SwitchState(FixState nextState, string args = "")
{
_data.curState = nextState;
_data.args = args;
yield return _stateMachine.SwitchState(nextState, args);
}
public IEnumerator QuitState()
{
_data.curState = FixState.Default;
yield return _stateMachine.QuitState();
}
private static IState<FixState> CreateState(FixState nextState, string args)
{
IState<FixState> res = nextState switch
{
FixState.Default => new DefaultState(),
FixState.Clinic => new ClinicState(),
FixState.BodyModule => new BodyModuleState(),
FixState.Memory => new MemoryState(),
FixState.Eye => new EyeState(),
FixState.UF => new UfState(),
FixState.Expression => new ExpressionState(),
FixState.Emo => new EmoState(),
FixState.Op => new OpState(),
FixState.EmoCut => new EmoCutState(),
FixState.MemoryCut => new MemoryCutState(),
FixState.LogicCut => new LogicCutState(),
FixState.Dream => new DreamState(),
FixState.AnalysisMode => new AnalysisModeState(),
FixState.Sales => new SalesState(),
FixState.BlockPuzzle => new BlockPuzzleState(),
_ => new DefaultState()
};
res.SetArgs(args);
return res;
}
}
public enum FixState
{
Default = 0,
Clinic = 1,
BodyModule = 2,
Memory = 3,
Eye = 4,
UF = 5,
Expression = 6,
Emo = 9,
Op = 10,
EmoCut = 11,
MemoryCut = 12,
LogicCut = 13,
Dream = 14,
AnalysisMode = 15,
Sales = 16,
BlockPuzzle = 17
}
public struct DefaultState : IState<FixState>
{
public FixState GetState => FixState.Default;
public IEnumerator Enter()
{
CameraKit.Instance.SwitchCamera(CameraEnum.Clinic, true);
var clinicSystem = FixSystemCenter.SystemDic.Get<ClinicSystem>();
var data = clinicSystem.bubbleSlotGroup.CollectData();
DialogUIManager.Instance.LoadBubbles(data);
yield break;
}
public IEnumerator Exit()
{
yield break;
}
}
public struct ClinicState : IState<FixState>
{
public FixState GetState => FixState.Clinic;
public void SetArgs(string args)
{
// 无
}
public IEnumerator Enter()
{
// 先加载对话气泡,避免 Yarn 对话(如 测试 节点 me: 释放log)在相机切换完成前执行时找不到 Player 槽位
var clinicSystem = FixSystemCenter.SystemDic.Get<ClinicSystem>();
// 切换相机
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Clinic);
var data = clinicSystem.bubbleSlotGroup.CollectData();
DialogUIManager.Instance.LoadBubbles(data);
}
public IEnumerator Exit()
{
// 无
yield break;
}
}
public struct BodyModuleState : IState<FixState>
{
private string _args;
public FixState GetState => FixState.BodyModule;
public void SetArgs(string args)
{
// 设置参数
_args = args;
}
public IEnumerator Enter()
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
var cablePanel = FixPanelSystem.GetRepairPanel<CablePanel>();
// 拔出插头
// 切换Body或Head
if (Enum.TryParse<ModuleState>(_args, out var moduleState))
{
bodyModuleSystem.SwitchModuleGroup(moduleState);
}
if (cablePanel != null && !FixPanelSystem.Instance.IsSystemOn)
{
// 切换相机
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModuleCenter);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.5f);
yield return new WaitForSeconds(0.5f);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
yield return FixPanelSystem.Instance.StartAllSystems();
}
else
{
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
// 加载气泡
var data = FixPanelSystem.Instance.BubbleSlotGroup.CollectData();
DialogUIManager.Instance.LoadBubbles(data);
// 开启交互
bodyModuleSystem.EnableSystem();
}
public IEnumerator Exit()
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
// 拔出插头
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
// 关闭交互
bodyModuleSystem.DisableSystem();
yield break;
}
}
public struct UfState : IState<FixState>
{
public FixState GetState => FixState.UF;
public void SetArgs(string args)
{
}
public IEnumerator Enter()
{
var ufSystem = FixSystemCenter.SystemDic.Get<UFSystem>();
var eyeSystem = FixSystemCenter.SystemDic.Get<EyeSystem>();
// 先切BodyModule
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
// 插头回到初始插孔
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
// 切换相机
yield return CameraKit.Instance.SwitchCamera(CameraEnum.UF);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.5f);
ufSystem.OpenUFView();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.EyeDeep);
eyeSystem.OnEnter();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.5f);
var data = eyeSystem.bubbleSlotGroup.CollectData();
DialogUIManager.Instance.LoadBubbles(data);
}
public IEnumerator Exit()
{
float duration = 0.5f;
var ufSystem = FixSystemCenter.SystemDic.Get<UFSystem>();
var eyeSystem = FixSystemCenter.SystemDic.Get<EyeSystem>();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(duration);
eyeSystem.OnExit();
ufSystem.CloseUFView();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.UF);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(duration);
}
}
public struct EyeState : IState<FixState>
{
public FixState GetState => FixState.Eye;
public void SetArgs(string args)
{
}
public IEnumerator Enter()
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
bodyModuleSystem.SwitchModuleGroup(ModuleState.Head);
var eyeSystem = FixSystemCenter.SystemDic.Get<EyeSystem>();
// 先切BodyModule
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
//打开盖子
var eyeModule = bodyModuleSystem.FindBodyModuleByType<EyeModule>();
yield return eyeModule.OpenEye();
// 插头回到初始插孔
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
// 切换相机
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Eye);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.5f);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.EyeDeep);
eyeSystem.OnEnter();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.5f);
var data = eyeSystem.bubbleSlotGroup.CollectData();
DialogUIManager.Instance.LoadBubbles(data);
}
public IEnumerator Exit()
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
float duration = 0.5f;
var eyeSystem = FixSystemCenter.SystemDic.Get<EyeSystem>();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(duration);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Eye);
eyeSystem.OnExit();
yield return new WaitForSeconds(0.1f);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(duration);
//关上
var eyeModule = bodyModuleSystem.FindBodyModuleByType<EyeModule>();
yield return eyeModule.CloseEye();
yield return new WaitForSeconds(0.5f);
}
}
public struct MemoryState : IState<FixState>
{
public FixState GetState => FixState.Memory;
public void SetArgs(string args)
{
}
public IEnumerator Enter()
{
float duration = 0.5f;
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
var memorySystem = FixSystemCenter.SystemDic.Get<MemoryProcess>();
// 先切BodyModule
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
yield return memorySystem.DropMemoryProjector();
// 插头回到初始插孔
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
// 相机聚焦到目标位
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Memory);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(duration);
memorySystem.OnEnter();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(duration);
var data = memorySystem.BubbleSlotGroup.CollectData();
DialogUIManager.Instance.LoadBubbles(data);
}
public IEnumerator Exit()
{
float duration = 0.5f;
var memorySystem = FixSystemCenter.SystemDic.Get<MemoryProcess>();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(duration);
memorySystem.OnExit();
memorySystem.CloseMemoryView();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(duration);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
memorySystem.PullMemoryProjector();
}
}
public struct ExpressionState : IState<FixState>
{
public FixState GetState => FixState.Expression;
public void SetArgs(string args)
{
// 无
}
public IEnumerator Enter()
{
// 获取表达系统
var expressionManager = FixSystemCenter.SystemDic.Get<ExpressionManager>();
// 先切 BodyModule(如果需要)
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
var bodyModule = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
bodyModule.SwitchModuleGroup(ModuleState.Head);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
// 开启表达系统视图,并播放火山表达深入
// 切换到表达相机
yield return TimelineCenter.Instance.PlayTimelineAsync("透镜/透镜in");
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Expression);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.5f);
expressionManager.OpenView();
// 切换到深度表达相机
yield return CameraKit.Instance.SwitchCamera(CameraEnum.ExpressionDeep);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.5f);
yield return TimelineCenter.Instance.PlayTimelineAsync("火山表达模块/火山表达深入");
}
public IEnumerator Exit()
{
var expressionManager = FixSystemCenter.SystemDic.Get<ExpressionManager>();
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.5f);
// 重置身体模块
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
// 关闭表达系统视图
expressionManager.CloseView();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Expression);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.5f);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
yield return TimelineCenter.Instance.PlayTimelineAsync("透镜/透镜out");
yield break;
}
}
public struct EmoState : IState<FixState>
{
public FixState GetState => FixState.Emo;
public void SetArgs(string args)
{
}
public IEnumerator Enter()
{
var emoManager = FixSystemCenter.SystemDic.Get<EmoManager>();
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
var bodyModule = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
bodyModule.SwitchModuleGroup(ModuleState.Head);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Emo);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.5f);
//emoManager.OpenView();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.EmoDeep);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.5f);
}
public IEnumerator Exit()
{
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
yield break;
}
}
public struct OpState : IState<FixState>
{
public FixState GetState => FixState.Op;
public void SetArgs(string args)
{
// 无
}
public IEnumerator Enter()
{
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.Op))
{
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Op);
}
}
public IEnumerator Exit()
{
// 无
yield break;
}
}
public struct DreamState : IState<FixState>
{
public FixState GetState => FixState.Dream;
public void SetArgs(string args)
{
// 无
}
public IEnumerator Enter()
{
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.DreamCamera))
{
yield return CameraKit.Instance.SwitchCamera(CameraEnum.DreamCamera);
var openSystem = FixSystemCenter.SystemDic.Get<OpenSystem>();
DialogUIManager.Instance.LoadBubbles(openSystem.bubbleSlotGroup.CollectData());
}
}
public IEnumerator Exit()
{
// 无
yield break;
}
}
public struct EmoCutState : IState<FixState>
{
public FixState GetState => FixState.EmoCut;
public void SetArgs(string args)
{
// 无
}
public IEnumerator Enter()
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
var cuttingManager = FixSystemCenter.SystemDic.Get<CuttingManager>();
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
bodyModuleSystem.SwitchModuleGroup(ModuleState.Head);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
// Cut的气泡统一放在BodyModule下
DialogUIManager.Instance.LoadBubbles(cuttingManager.bubbleSlotGroup.CollectData());
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
var bodyModule = bodyModuleSystem.FindBodyModuleByName("Emo");
if (bodyModule != null)
{
bodyModule.GetCutSpriteRenderer().sortingOrder = 100;
bodyModule.GetCutTextSpriteRenderer().sortingOrder = 101;
bodyModule.GetCutSpriteRenderer().color = new Color(1f, 1f, 1f, 1f);
bodyModule.GetCutSpriteRenderer().GetComponentInChildren<SpriteRenderer>().color =
new Color(1f, 1f, 1f, 1f);
}
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutEmo);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.1f);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutEmoDeep);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.1f);
yield return cuttingManager.StartCutting(bodyModule);
}
public IEnumerator Exit()
{
// 无
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.1f);
var cuttingManager = FixSystemCenter.SystemDic.Get<CuttingManager>();
cuttingManager.ClearCutLine();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutEmo);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.1f);
}
}
public struct MemoryCutState : IState<FixState>
{
public FixState GetState => FixState.MemoryCut;
public void SetArgs(string args)
{
// 无
}
public IEnumerator Enter()
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
var cuttingManager = FixSystemCenter.SystemDic.Get<CuttingManager>();
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
bodyModuleSystem.SwitchModuleGroup(ModuleState.Head);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
// Cut的气泡统一放在BodyModule下
DialogUIManager.Instance.LoadBubbles(cuttingManager.bubbleSlotGroup.CollectData());
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
var bodyModule = bodyModuleSystem.FindBodyModuleByName("Memory");
if (bodyModule != null)
{
bodyModule.GetCutSpriteRenderer().sortingOrder = 100;
bodyModule.GetCutTextSpriteRenderer().sortingOrder = 101;
bodyModule.GetCutSpriteRenderer().color = new Color(1f, 1f, 1f, 1f);
bodyModule.GetCutSpriteRenderer().GetComponentInChildren<SpriteRenderer>().color =
new Color(1f, 1f, 1f, 1f);
}
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutMemory);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.1f);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutMemoryDeep);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.1f);
yield return cuttingManager.StartCutting(bodyModule);
}
public IEnumerator Exit()
{
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.1f);
var cuttingManager = FixSystemCenter.SystemDic.Get<CuttingManager>();
cuttingManager.ClearCutLine();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutMemory);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.1f);
}
}
public struct LogicCutState : IState<FixState>
{
public FixState GetState => FixState.LogicCut;
public void SetArgs(string args)
{
// 无
}
public IEnumerator Enter()
{
var cuttingManager = FixSystemCenter.SystemDic.Get<CuttingManager>();
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
bodyModuleSystem.SwitchModuleGroup(ModuleState.Head);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
// Cut的气泡统一放在BodyModule下
DialogUIManager.Instance.LoadBubbles(cuttingManager.bubbleSlotGroup.CollectData());
var bodyModule = bodyModuleSystem.FindBodyModuleByName("Logic");
if (bodyModule != null)
{
bodyModule.GetCutSpriteRenderer().sortingOrder = 100;
bodyModule.GetCutTextSpriteRenderer().sortingOrder = 101;
bodyModule.GetCutSpriteRenderer().color = new Color(1f, 1f, 1f, 1f);
bodyModule.GetCutSpriteRenderer().GetComponentInChildren<SpriteRenderer>().color =
new Color(1f, 1f, 1f, 1f);
}
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutLogic);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.1f);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutLogicDeep);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.1f);
yield return cuttingManager.StartCutting(bodyModule);
}
public IEnumerator Exit()
{
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.1f);
var cuttingManager = FixSystemCenter.SystemDic.Get<CuttingManager>();
cuttingManager.ClearCutLine();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutLogic);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.1f);
}
}
public struct AnalysisModeState : IState<FixState>
{
public FixState GetState => FixState.AnalysisMode;
public void SetArgs(string args)
{
// 无
}
public IEnumerator Enter()
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
var analysisModeManager = FixSystemCenter.SystemDic.Get<MiniGame.Language.AnalysisModeManager>();
// 先切 BodyModule(如果需要)
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
bodyModuleSystem.SwitchModuleGroup(ModuleState.Head);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
// 插头回到初始插孔
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
// 切换到分析模式相机
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.5f);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.AnalysisMode);
// 进入分析模式
if (analysisModeManager != null)
{
analysisModeManager.OnEnter();
}
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.5f);
}
public IEnumerator Exit()
{
float duration = 0.5f;
var analysisModeManager = FixSystemCenter.SystemDic.Get<MiniGame.Language.AnalysisModeManager>();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(duration);
// 退出分析模式
if (analysisModeManager != null)
{
analysisModeManager.OnExit();
}
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(duration);
}
}
public struct SalesState : IState<FixState>
{
private string _systemView;
public FixState GetState => FixState.Sales;
public void SetArgs(string args)
{
_systemView = args;
}
public IEnumerator Enter()
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
bodyModuleSystem.SwitchModuleGroup(ModuleState.Head);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
// 进入前先播放钳子摘盖子,完成后深入
yield return TimelineCenter.Instance.PlayTimelineAsync("钳子/摘走盖子");
// 获取销售系统管理器
var salesManager = FixSystemCenter.SystemDic.Get<AibisDream.MiniGame.Language.SalesManager>();
if (salesManager != null)
{
// FadeIn
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.5f);
// 打开销售系统视图,传入systemView参数
salesManager.OpenView();
// FadeOut
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.5f);
// 销售模块使用自身的气泡槽位;缺失时再回退到 ScreenSystem,避免读到错误的槽位配置
if (salesManager.bubbleSlotGroup != null)
{
var data = salesManager.bubbleSlotGroup.CollectData();
Debug.Log("[SalesState] Loaded bubble slots from SalesManager.");
DialogUIManager.Instance.LoadBubbles(data);
}
else
{
Debug.LogWarning("[SalesState] SalesManager.bubbleSlotGroup is null, fallback to ScreenSystem bubble slots.");
var data = FixPanelSystem.Instance.BubbleSlotGroup.CollectData();
DialogUIManager.Instance.LoadBubbles(data);
}
}
}
public IEnumerator Exit()
{
float duration = 0.5f;
var salesManager = FixSystemCenter.SystemDic.Get<AibisDream.MiniGame.Language.SalesManager>();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(duration);
// 关闭销售系统视图
if (salesManager != null)
{
salesManager.CloseView();
}
// 关闭前先播放钳子放下盖子,完成后再结
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(duration);
yield return TimelineCenter.Instance.PlayTimelineAsync("钳子/放下盖子");
yield return null;
}
}
public struct BlockPuzzleState : IState<FixState>
{
private const string puzzleName = "Game2";
private const float blockPuzzleScale = 1.6f;
private static BlockPuzzleSystem BlockPuzzleSystem => FixSystemCenter.SystemDic.Get<BlockPuzzleSystem>();
private static BodyModuleSystem BodyModuleSystem => FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
public FixState GetState => FixState.BlockPuzzle;
public void SetArgs(string args)
{
}
public IEnumerator Enter()
{
// 基本操作(其实应该早就处理好了)
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
var bubbleData = FixPanelSystem.Instance.BubbleSlotGroup.CollectData();
DialogUIManager.Instance.LoadBubbles(bubbleData);
yield return TimelineCenter.Instance.PlayTimelineAsync(TimelineAssetRef.EngineModuleEnter);
// 视角移动
var engineModule = BodyModuleSystem.FindBodyModuleByName("Heart");
Tween tween = RepairSystemManager.Instance.ScaleCurrentSystem(blockPuzzleScale, engineModule.transform.position);
if (tween != null)
{
yield return tween.WaitForCompletion();
}
// 初始化拼图
BlockPuzzleSystem.Initialize(puzzleName);
// 开启遮罩
yield return FixPanelSystem.Instance.MaskFadeIn();
RepairSystemManager.Instance.SwitchSystem<BlockPuzzleSystem>();
yield return new WaitForSeconds(0.5f);
yield return FixPanelSystem.Instance.MaskFadeOut();
// 播放进场动画
yield return TimelineCenter.Instance.PlayTimelineAsync(TimelineAssetRef.CablePanelExit);
yield return new WaitForSeconds(0.5f);
yield return TimelineCenter.Instance.PlayTimelineAsync(TimelineAssetRef.BlockPuzzleEnter);
// 解锁
BlockPuzzleSystem.SetLocked(false);
}
public IEnumerator Exit()
{
yield break;
}
}
}
+57 -37
View File
@@ -2,61 +2,81 @@
using System.Collections;
using AibisDream.Framework;
using AibisDream.Kit;
using AibisDream.SaveSystem;
using UnityEngine;
namespace AibisDream.FixSystem
{
public class FixSystemCenter : Singleton<FixSystemCenter>
{
public FixState CurState => StateMachine.CurState;
public FixSceneMode CurrentMode => Director.CurrentMode;
public FixState defaultState = FixState.Default;
private readonly FixSystemData _data = new();
public FixSceneMode defaultMode = FixSceneMode.Default;
public static IOCContainer SystemDic = new();
public static FixSceneContext Context;
public static FixSceneDirector Director;
public override void OnSingletonInit()
{
SystemDic ??= new IOCContainer();
// 初始化状态机
StateMachine = new FixStateMachine(_data);
StartCoroutine(StateMachine.Init(defaultState));
// 注册
StorageSystem.Instance.RegisterData(_data);
Context = new FixSceneContext(SystemDic);
Director = new FixSceneDirector();
}
private void Start()
{
if (SaveRestoreOrchestrator.IsRestoring)
{
return;
}
StartCoroutine(Director.Init(defaultMode));
}
public override void OnSingletonDestroy()
{
// 注销
StorageSystem.Instance.UnregisterData<FixSystemData>();
CameraKit.Instance.RemoveAllCam();
GameManager.Instance.StartCoroutine(StateMachine.QuitState());
SystemDic.Clear();
BubbleSlotKit.Instance.RemoveAll();
GameManager.Instance.StartCoroutine(Director.QuitCurrentMode());
Context?.Clear();
}
#region
// 容纳System
public static IOCContainer SystemDic = new();
#endregion
#region
public static FixStateMachine StateMachine;
#endregion
}
[Serializable]
[LoadIndex(2)]
public class FixSystemData : IData
{
public FixState curState;
public string args;
public IEnumerator Load()
public IEnumerator TransitionTo(FixSceneMode nextMode, string args = "")
{
yield return FixSystemCenter.StateMachine.SwitchState(curState, args);
yield return Director.TransitionTo(nextMode, args);
}
public IEnumerator TransitionToImmediate(FixSceneMode nextMode, string args = "")
{
yield return Director.TransitionToImmediate(nextMode, args);
}
/// <summary>Captures the macro Fix scene cue snapshot.</summary>
public FixSnapshotDto CaptureSnapshot()
{
return new FixSnapshotDto
{
state = Director.CurrentMode.ToString(),
args = Director.CurrentArgs
};
}
/// <summary>Restores the macro Fix scene snapshot by jumping directly to the target cue endpoint.</summary>
public IEnumerator RestoreSnapshot(FixSnapshotDto dto)
{
if (dto == null || string.IsNullOrEmpty(dto.state))
{
yield break;
}
if (!Enum.TryParse<FixSceneMode>(dto.state, out var mode))
{
Debug.LogWarning($"[FixSystemCenter] Could not parse FixSceneMode: {dto.state}");
yield break;
}
yield return Director.TransitionToImmediate(mode, dto.args ?? "");
}
}
}
@@ -1,32 +1,24 @@
using System;
using System;
using System.Collections;
using UnityEngine;
using Yarn.Unity;
namespace AibisDream.FixSystem
{
public static class FixSystemYarnCommand
{
[YarnCommand("switch_fix_system_to")]
public static IEnumerator SwitchFixSystemTo(string targetStateStr, string extraArg = "")
public static IEnumerator SwitchFixSystemTo(string targetModeStr, string extraArg = "")
{
// 字符串转枚举
if (!Enum.TryParse<FixState>(targetStateStr, out var targetState))
if (!Enum.TryParse<FixSceneMode>(targetModeStr, out var targetMode))
{
Debug.Log($"{targetStateStr}不在FixState枚举中");
Debug.Log($"[Yarn] Unknown FixSceneMode: {targetModeStr}");
yield break;
}
yield return FixSystemCenter.StateMachine.SwitchState(targetState, extraArg);
yield return FixSystemCenter.Instance.TransitionTo(targetMode, extraArg);
}
#region
/// <summary>
/// 切换到指定的维修系统(按类型名称)
/// 用法: <<repair_switch_system "BlockPuzzleSystem">>
/// </summary>
[YarnCommand("repair_switch_system")]
public static IEnumerator RepairSwitchSystem(string systemTypeName)
{
@@ -39,18 +31,15 @@ namespace AibisDream.FixSystem
if (systemType == null)
{
Debug.LogError($"[Yarn] 未知的系统类型: {systemTypeName}");
Debug.LogError($"[Yarn] Unknown repair system type: {systemTypeName}");
yield break;
}
RepairSystemManager.Instance.SwitchSystem(systemType);
// 等待切换动画
yield return new WaitForSeconds(RepairSystemManager.Instance.GetComponent<RepairSystemManager>()
?.GetType().GetField("switchDuration", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
?.GetValue(RepairSystemManager.Instance) is float duration ? duration : 0.5f);
}
#endregion
}
}
}
@@ -0,0 +1,47 @@
using System.Collections;
namespace AibisDream.FixSystem
{
public enum FixTransitionMode
{
Animated,
Immediate
}
public readonly struct FixTransition
{
public readonly FixSceneMode? From;
public readonly FixSceneMode To;
public readonly string Args;
public readonly FixTransitionMode Mode;
public FixTransition(FixSceneMode? from, FixSceneMode to, string args, FixTransitionMode mode)
{
From = from;
To = to;
Args = args ?? "";
Mode = mode;
}
}
/// <summary>
/// A cue is a directed scene orchestration unit for a Fix mode.
/// It owns camera, timeline, UI and interaction setup for entering or leaving that mode.
/// </summary>
public interface IFixSceneCue
{
FixSceneMode Mode { get; }
IEnumerator Enter(FixTransition transition);
IEnumerator Exit(FixTransition transition)
{
yield break;
}
IEnumerator EnterImmediate(FixTransition transition)
{
yield break;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7d5df9cd63e8db54da758ad165a5a6b5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,6 +1,4 @@
using AibisDream.FixSystem;
using AibisDream.Framework;
using Cinemachine;
using UnityEngine;
using UnityEngine.Playables;
using Yarn.Unity;
@@ -18,18 +16,9 @@ namespace AibisDream
public SpriteRenderer dreamSprite;
public BubbleSlotGroup bubbleSlotGroup;
private void Awake()
{
// 注册
FixSystemCenter.SystemDic.Register(this);
var virtualCam = transform.Find("Op Camera").GetComponent<ICinemachineCamera>();
CameraKit.Instance.RegisterCamera(CameraEnum.Op, virtualCam);
var breakCam = transform.Find("Break Camera").GetComponent<ICinemachineCamera>();
CameraKit.Instance.RegisterCamera(CameraEnum.BreakCamera, breakCam);
var dreamCam = transform.Find("Dream Camera").GetComponent<ICinemachineCamera>();
CameraKit.Instance.RegisterCamera(CameraEnum.DreamCamera, dreamCam);
}
private void Start()
@@ -1,7 +1,7 @@
using AibisDream.Framework;
using AibisDream.Kit;
using AibisDream.SaveSystem;
using UnityEngine;
using Cinemachine;
using System.Collections;
using DG.Tweening;
@@ -126,14 +126,75 @@ namespace AibisDream.FixSystem
#endregion
#region Capture / Restore
public FixPanelSnapshotDto CapturePanelSnapshot()
{
var cablePanel = GetRepairPanel<CablePanel>();
var dto = new FixPanelSnapshotDto
{
isSystemOn = isSystemOn,
currentRepairSystemType = repairSystemManager?.GetCurrentSystemType()?.Name,
isCableRetracted = cablePanel != null && cablePanel.isCableRetracted
};
if (cablePanel != null && cablePanel.TryGetPluggedModuleName(out var moduleName))
{
dto.pluggedModuleName = moduleName;
}
if (dto.isCableRetracted && !string.IsNullOrEmpty(dto.pluggedModuleName))
{
Debug.LogWarning("[FixPanelSystem] Capture: isCableRetracted 与 pluggedModuleName 冲突,忽略 pluggedModuleName。");
dto.pluggedModuleName = null;
}
if (taskPanel != null)
{
taskPanel.CaptureSnapshot(out dto.isTaskPanelVisible, dto.tasks);
}
return dto;
}
public void ApplyPanelSnapshot(FixPanelSnapshotDto dto)
{
if (dto == null) return;
ApplySystemOnImmediate(dto.isSystemOn);
if (repairSystemManager != null)
{
repairSystemManager.RestoreSystemType(dto.currentRepairSystemType);
}
GetRepairPanel<CablePanel>()?.ApplyCableSnapshot(dto.isCableRetracted, dto.pluggedModuleName);
taskPanel?.ApplySnapshot(dto.isTaskPanelVisible, dto.tasks, immediate: true);
}
/// <summary>读档终态:跳过 StartAllSystems 动画,直接写面板壳层状态。</summary>
public void ApplySystemOnImmediate(bool isOn)
{
isSystemOn = isOn;
if (panelLight != null)
{
panelLight.Initialize(isOn);
}
var cablePanel = GetRepairPanel<CablePanel>();
cablePanel?.InitLight(isOn);
if (screenPanel != null)
{
screenPanel.gameObject.SetActive(isOn);
}
}
#endregion
#region
[Header("默认相机")]
[SerializeField] private CinemachineVirtualCamera fixPanelCamera;
[Header("默认气泡")]
[SerializeField] private BubbleSlotGroup bubbleSlotGroup;
public BubbleSlotGroup BubbleSlotGroup => bubbleSlotGroup;
[Header("默认画布")]
[SerializeField] private Canvas canvas;
@@ -150,17 +211,12 @@ namespace AibisDream.FixSystem
private void RegisterScreenView()
{
// 注册相机
CameraKit.Instance.RegisterCamera(CameraEnum.BodyModule, fixPanelCamera);
DialogUIManager.Instance.RegisterScreenView(DialogViewType.Screen, screenPanel);
DialogUIManager.Instance.RegisterScreenView(DialogViewType.Task, taskPanel);
}
private void UnRegisterScreenView()
{
CameraKit.Instance.UnRegisterCamera(CameraEnum.BodyModule);
DialogUIManager.Instance.UnRegisterScreenView(DialogViewType.Screen);
DialogUIManager.Instance.UnRegisterScreenView(DialogViewType.Task);
}
@@ -212,6 +212,37 @@ namespace AibisDream.FixSystem
: null;
}
/// <summary>读档:按已注册类型名切换中央维修视图(无动画)。</summary>
public void RestoreSystemType(string typeName)
{
Type targetType = typeof(BodyModuleSystem);
if (!string.IsNullOrEmpty(typeName))
{
Type matched = null;
foreach (var kv in _registeredSystems)
{
if (kv.Key.Name == typeName)
{
matched = kv.Key;
break;
}
}
if (matched != null)
{
targetType = matched;
}
else
{
Debug.LogWarning($"[RepairSystemManager] 未找到已注册系统: {typeName},保持当前类型。");
return;
}
}
SwitchSystem(targetType);
}
#endregion
}
}
@@ -14,7 +14,6 @@ namespace AibisDream.FixSystem
public class CablePanel : MonoBehaviour, IOperatorPanel
{
private const string CableRetractedKey = "$global.cableRetracted";
private const int SortingOrderRetracted = 41;
#region
@@ -52,9 +51,8 @@ namespace AibisDream.FixSystem
private void Start()
{
InitComponentRefs();
InitSystem();
LoadCableState();
InitCableVisualFromPrefab();
}
private void OnDestroy()
@@ -82,37 +80,109 @@ namespace AibisDream.FixSystem
plugRef.OnPlugPointerDown += OnPlugPoinerDown;
}
private void LoadCableState()
private void InitCableVisualFromPrefab()
{
// 从Prefab读取
if (isCableRetracted)
{
SetRetractCable();
}
else
{
PlugRef.ReturnToStartPosition();
}
// 从存储系统读取
if (!StorageSystem.Instance.TryGetValue(CableRetractedKey, out bool savedRetractedState))
return;
if (savedRetractedState)
{
SetRetractCable();
}
else
{
if (isCableRetracted)
DropDownCable();
DropDownCableImmediate();
}
}
private void SaveCableState()
public bool TryGetPluggedModuleName(out string moduleName)
{
Debug.Log("Save cableRetractedstate" + isCableRetracted);
StorageSystem.Instance.SetValue(CableRetractedKey, isCableRetracted);
if (curSocket is BodyModule bodyModule)
{
moduleName = bodyModule.data.moduleName;
return !string.IsNullOrEmpty(moduleName);
}
moduleName = null;
return false;
}
/// <summary>读档:同步写回线缆与插头终态,无动画、无 Yarn、无 PlugIn 事件。</summary>
public void ApplyCableSnapshot(bool retracted, string pluggedModuleName)
{
SilentPullUpSocket();
if (retracted)
{
if (ShouldSkipRetractOnRestore())
{
if (isCableRetracted)
{
DropDownCableImmediate();
}
curSocket = null;
return;
}
SetRetractCable();
curSocket = null;
return;
}
DropDownCableImmediate();
if (!string.IsNullOrEmpty(pluggedModuleName))
{
RestorePlugToModule(pluggedModuleName);
}
else
{
curSocket = null;
SetCableMode(CableMode.Physic);
}
}
public void DropDownCableImmediate()
{
PlugRef.PullUpSocketSilent();
CableReelRef.ResetRotation();
PlugRef.RestoreAtStartPositionImmediate();
SetCableMode(CableMode.Physic);
isCableRetracted = false;
}
private void RestorePlugToModule(string moduleName)
{
var module = BodyModuleSystem?.FindBodyModuleByName(moduleName);
if (module == null)
{
Debug.LogWarning($"[CablePanel] RestorePlugToModule: 未找到模块 {moduleName},插头留在初始位。");
curSocket = null;
SetCableMode(CableMode.Physic);
return;
}
curSocket = module;
var socketPos = module.GetSocketPos();
PlugRef.RestoreInsertedAt(socketPos);
CableRef.SetEndPos(socketPos);
SetCableMode(CableMode.Visual);
BodyModuleSystem.CloseModuleSlots(null);
}
private void SilentPullUpSocket()
{
if (curSocket == null) return;
CableRef.SetEndPos(PlugRef.PlugRoot);
curSocket.PlugOut();
curSocket = null;
PlugRef.SetSpriteVisible(true);
}
private static bool ShouldSkipRetractOnRestore()
{
return FixSystemCenter.Instance != null
&& FixSystemCenter.Director.HasMode
&& FixSystemCenter.Director.CurrentMode == FixSceneMode.BodyModule;
}
private void Update()
@@ -126,10 +196,14 @@ namespace AibisDream.FixSystem
public void ResetPlug()
{
// 不在初始插孔里,就从当前插孔拔出来,插回初始插孔
PlugRef.PullUpSocket();
if (IsPlugInSocket())
{
PlugRef.PullUpSocket();
}
BodyModuleSystem.CloseModuleSlots(null);
PlugRef.ReturnToStartPosition();
DropDownCableImmediate();
AudioManager.Instance.PlaySfx("event:/ActionFB/mods_close");
AudioManager.Instance.PlaySfx("event:/FollowInput/plugdrop");
}
@@ -245,7 +319,6 @@ namespace AibisDream.FixSystem
{
ApplyRetractedState();
isCableRetracted = true;
SaveCableState();
});
AudioManager.Instance.PlaySfx("event:/ActionFB/mods_close");
@@ -262,7 +335,6 @@ namespace AibisDream.FixSystem
{
SetCableMode(CableMode.Physic);
isCableRetracted = false;
SaveCableState();
});
}
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using AibisDream.Framework;
using AibisDream.SaveSystem;
using AibisDream.Utility;
using TMPro;
using UnityEngine;
@@ -10,6 +11,9 @@ namespace AibisDream.FixSystem
{
public class TaskPanel : MonoBehaviour, ILineView
{
private const float HiddenLocalX = -10.32f;
private const float ShownLocalX = -7.12f;
#region
[SerializeField] private Transform taskRoot;
@@ -26,8 +30,9 @@ namespace AibisDream.FixSystem
public void ShowPanel()
{
taskRoot.DOLocalMoveX(-7.12f, 1f);
taskRoot.DOLocalMoveX(ShownLocalX, 1f);
}
public void CompleteTask(string lineId)
{
if (_taskDict.Remove(lineId, out var taskItem))
@@ -54,15 +59,70 @@ namespace AibisDream.FixSystem
return;
}
var taskItem = Instantiate(_taskItemPrefab, taskListParent);
var task = taskItem.GetComponent<TMP_Text>();
task.text = $"\u25cf {token.lineInfo.lineText}";
AddTaskItem(token.lineInfo.lineId, $"\u25cf {token.lineInfo.lineText}");
token.TextStart();
_taskDict[token.lineInfo.lineId] = taskItem;
token.TextShown();
token.ForceAdvance();
}
public void CaptureSnapshot(out bool isPanelVisible, List<TaskEntrySnapshotDto> tasks)
{
isPanelVisible = Mathf.Approximately(taskRoot.localPosition.x, ShownLocalX);
tasks.Clear();
foreach (var kv in _taskDict)
{
var text = kv.Value.GetComponent<TMP_Text>()?.text ?? string.Empty;
tasks.Add(new TaskEntrySnapshotDto
{
lineId = kv.Key,
text = text
});
}
}
public void ApplySnapshot(bool isPanelVisible, List<TaskEntrySnapshotDto> tasks, bool immediate)
{
ClearTasks();
if (tasks != null)
{
foreach (var entry in tasks)
{
if (string.IsNullOrEmpty(entry.lineId))
{
continue;
}
AddTaskItem(entry.lineId, entry.text);
}
}
if (immediate)
{
taskRoot.DOKill();
SetPanelVisibleImmediate(isPanelVisible);
}
else if (isPanelVisible)
{
ShowPanel();
}
}
private void AddTaskItem(string lineId, string text)
{
var taskItem = Instantiate(_taskItemPrefab, taskListParent);
var task = taskItem.GetComponent<TMP_Text>();
task.text = text;
_taskDict[lineId] = taskItem;
LayoutRebuilder.ForceRebuildLayoutImmediate(taskListParent.GetComponent<RectTransform>());
}
private void SetPanelVisibleImmediate(bool visible)
{
var pos = taskRoot.localPosition;
pos.x = visible ? ShownLocalX : HiddenLocalX;
taskRoot.localPosition = pos;
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 2c41d75ab4cb4d338c0288b83f61c94e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,52 @@
using System;
using System.Collections;
using AibisDream.Framework;
using DG.Tweening;
using UnityEngine;
namespace AibisDream.FixSystem
{
public struct BlockPuzzleCue : IFixSceneCue
{
private const string puzzleName = "Game2";
private const float blockPuzzleScale = 1.6f;
private static BlockPuzzleSystem BlockPuzzleSystem => FixSystemCenter.SystemDic.Get<BlockPuzzleSystem>();
private static BodyModuleSystem BodyModuleSystem => FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
public FixSceneMode Mode => FixSceneMode.BlockPuzzle;
public IEnumerator Enter(FixTransition transition)
{
// 基本操作(其实应该早就处理好了)
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.BodyModule);
yield return TimelineCenter.Instance.PlayTimelineAsync(TimelineAssetRef.EngineModuleEnter);
// 视角移动
var engineModule = BodyModuleSystem.FindBodyModuleByName("Heart");
Tween tween = RepairSystemManager.Instance.ScaleCurrentSystem(blockPuzzleScale, engineModule.transform.position);
if (tween != null)
{
yield return tween.WaitForCompletion();
}
// 初始化拼图
BlockPuzzleSystem.Initialize(puzzleName);
// 开启遮罩
yield return FixPanelSystem.Instance.MaskFadeIn();
RepairSystemManager.Instance.SwitchSystem<BlockPuzzleSystem>();
yield return new WaitForSeconds(0.5f);
yield return FixPanelSystem.Instance.MaskFadeOut();
// 播放进场动画
yield return TimelineCenter.Instance.PlayTimelineAsync(TimelineAssetRef.CablePanelExit);
yield return new WaitForSeconds(0.5f);
yield return TimelineCenter.Instance.PlayTimelineAsync(TimelineAssetRef.BlockPuzzleEnter);
// 解锁
BlockPuzzleSystem.SetLocked(false);
}
public IEnumerator Exit(FixTransition transition)
{
yield break;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 84691c3d316643b98c99ca16c4560013
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,174 @@
using System;
using System.Collections;
using AibisDream.Framework;
using AibisDream.UI;
using UnityEngine;
namespace AibisDream.FixSystem
{
public struct DefaultCue : IFixSceneCue
{
public FixSceneMode Mode => FixSceneMode.Default;
public IEnumerator Enter(FixTransition transition)
{
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Clinic, true);
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Clinic);
}
public IEnumerator EnterImmediate(FixTransition transition)
{
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Clinic, true);
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Clinic);
}
public IEnumerator Exit(FixTransition transition)
{
yield break;
}
}
public struct ClinicCue : IFixSceneCue
{
public FixSceneMode Mode => FixSceneMode.Clinic;
public IEnumerator Enter(FixTransition transition)
{
// 先加载对话气泡,避免 Yarn 对话(如 测试 节点 me: 释放log)在相机切换完成前执行时找不到 Player 槽位
// 切换相机
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Clinic);
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Clinic);
}
public IEnumerator EnterImmediate(FixTransition transition)
{
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Clinic, true);
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Clinic);
}
public IEnumerator Exit(FixTransition transition)
{
// 无
yield break;
}
}
public struct BodyModuleCue : IFixSceneCue
{
public FixSceneMode Mode => FixSceneMode.BodyModule;
public IEnumerator Enter(FixTransition transition)
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
var cablePanel = FixPanelSystem.GetRepairPanel<CablePanel>();
// 拔出插头
// 切换Body或Head
if (Enum.TryParse<ModuleState>(transition.Args, out var moduleState))
{
bodyModuleSystem.SwitchModuleGroup(moduleState);
}
if (cablePanel != null && !FixPanelSystem.Instance.IsSystemOn)
{
// 切换相机
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModuleCenter);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.5f);
yield return new WaitForSeconds(0.5f);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
yield return FixPanelSystem.Instance.StartAllSystems();
}
else
{
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.BodyModule);
// 开启交互
bodyModuleSystem.EnableSystem();
}
public IEnumerator EnterImmediate(FixTransition transition)
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
// 切换Body或Head
if (Enum.TryParse<ModuleState>(transition.Args, out var moduleState))
{
bodyModuleSystem.SwitchModuleGroup(moduleState);
}
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule, true);
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.BodyModule);
// 开启交互
bodyModuleSystem.EnableSystem();
yield break;
}
public IEnumerator Exit(FixTransition transition)
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
// 拔出插头
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
// 关闭交互
bodyModuleSystem.DisableSystem();
yield break;
}
}
public struct OpCue : IFixSceneCue
{
public FixSceneMode Mode => FixSceneMode.Op;
public IEnumerator Enter(FixTransition transition)
{
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.Op))
{
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Op);
}
}
public IEnumerator EnterImmediate(FixTransition transition)
{
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Op, true);
}
public IEnumerator Exit(FixTransition transition)
{
// 无
yield break;
}
}
public struct DreamCue : IFixSceneCue
{
public FixSceneMode Mode => FixSceneMode.Dream;
public IEnumerator Enter(FixTransition transition)
{
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.DreamCamera))
{
yield return CameraKit.Instance.SwitchCamera(CameraEnum.DreamCamera);
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Open);
}
}
public IEnumerator EnterImmediate(FixTransition transition)
{
yield return CameraKit.Instance.SwitchCamera(CameraEnum.DreamCamera, true);
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Open);
}
public IEnumerator Exit(FixTransition transition)
{
// 无
yield break;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 1e85329d3d1e47c18bec82ccfe25f6ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,142 @@
using System;
using System.Collections;
using AibisDream.Framework;
using AibisDream.UI;
using DG.Tweening;
using UnityEngine;
namespace AibisDream.FixSystem
{
public struct EmoCutCue : IFixSceneCue
{
public FixSceneMode Mode => FixSceneMode.EmoCut;
public IEnumerator Enter(FixTransition transition)
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
var cuttingManager = FixSystemCenter.SystemDic.Get<CuttingManager>();
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
bodyModuleSystem.SwitchModuleGroup(ModuleState.Head);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
// Cut的气泡统一放在BodyModule下
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Cut);
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
var bodyModule = bodyModuleSystem.FindBodyModuleByName("Emo");
if (bodyModule != null)
{
bodyModule.GetCutSpriteRenderer().sortingOrder = 100;
bodyModule.GetCutTextSpriteRenderer().sortingOrder = 101;
bodyModule.GetCutSpriteRenderer().color = new Color(1f, 1f, 1f, 1f);
bodyModule.GetCutSpriteRenderer().GetComponentInChildren<SpriteRenderer>().color =
new Color(1f, 1f, 1f, 1f);
}
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutEmo);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.1f);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutEmoDeep);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.1f);
yield return cuttingManager.StartCutting(bodyModule);
}
public IEnumerator Exit(FixTransition transition)
{
// 无
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.1f);
var cuttingManager = FixSystemCenter.SystemDic.Get<CuttingManager>();
cuttingManager.ClearCutLine();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutEmo);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.1f);
}
}
public struct MemoryCutCue : IFixSceneCue
{
public FixSceneMode Mode => FixSceneMode.MemoryCut;
public IEnumerator Enter(FixTransition transition)
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
var cuttingManager = FixSystemCenter.SystemDic.Get<CuttingManager>();
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
bodyModuleSystem.SwitchModuleGroup(ModuleState.Head);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
// Cut的气泡统一放在BodyModule下
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Cut);
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
var bodyModule = bodyModuleSystem.FindBodyModuleByName("Memory");
if (bodyModule != null)
{
bodyModule.GetCutSpriteRenderer().sortingOrder = 100;
bodyModule.GetCutTextSpriteRenderer().sortingOrder = 101;
bodyModule.GetCutSpriteRenderer().color = new Color(1f, 1f, 1f, 1f);
bodyModule.GetCutSpriteRenderer().GetComponentInChildren<SpriteRenderer>().color =
new Color(1f, 1f, 1f, 1f);
}
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutMemory);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.1f);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutMemoryDeep);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.1f);
yield return cuttingManager.StartCutting(bodyModule);
}
public IEnumerator Exit(FixTransition transition)
{
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.1f);
var cuttingManager = FixSystemCenter.SystemDic.Get<CuttingManager>();
cuttingManager.ClearCutLine();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutMemory);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.1f);
}
}
public struct LogicCutCue : IFixSceneCue
{
public FixSceneMode Mode => FixSceneMode.LogicCut;
public IEnumerator Enter(FixTransition transition)
{
var cuttingManager = FixSystemCenter.SystemDic.Get<CuttingManager>();
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
bodyModuleSystem.SwitchModuleGroup(ModuleState.Head);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
// Cut的气泡统一放在BodyModule下
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Cut);
var bodyModule = bodyModuleSystem.FindBodyModuleByName("Logic");
if (bodyModule != null)
{
bodyModule.GetCutSpriteRenderer().sortingOrder = 100;
bodyModule.GetCutTextSpriteRenderer().sortingOrder = 101;
bodyModule.GetCutSpriteRenderer().color = new Color(1f, 1f, 1f, 1f);
bodyModule.GetCutSpriteRenderer().GetComponentInChildren<SpriteRenderer>().color =
new Color(1f, 1f, 1f, 1f);
}
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutLogic);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.1f);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutLogicDeep);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.1f);
yield return cuttingManager.StartCutting(bodyModule);
}
public IEnumerator Exit(FixTransition transition)
{
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.1f);
var cuttingManager = FixSystemCenter.SystemDic.Get<CuttingManager>();
cuttingManager.ClearCutLine();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.CutLogic);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.1f);
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: f31682a5a8144cfa8ba7f2d8619feaf2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,22 @@
namespace AibisDream.FixSystem
{
public enum FixSceneMode
{
Default = 0,
Clinic = 1,
BodyModule = 2,
Memory = 3,
Eye = 4,
UF = 5,
Expression = 6,
Emo = 9,
Op = 10,
EmoCut = 11,
MemoryCut = 12,
LogicCut = 13,
Dream = 14,
AnalysisMode = 15,
Sales = 16,
BlockPuzzle = 17
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 9dc0800bbef347d5810d82e445a872cb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,193 @@
using System;
using System.Collections;
using AibisDream.Framework;
using AibisDream.UI;
using DG.Tweening;
using UnityEngine;
namespace AibisDream.FixSystem
{
public struct ExpressionCue : IFixSceneCue
{
public FixSceneMode Mode => FixSceneMode.Expression;
public IEnumerator Enter(FixTransition transition)
{
// 获取表达系统
var expressionManager = FixSystemCenter.SystemDic.Get<ExpressionManager>();
// 先切 BodyModule(如果需要)
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
var bodyModule = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
bodyModule.SwitchModuleGroup(ModuleState.Head);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
// 开启表达系统视图,并播放火山表达深入
// 切换到表达相机
yield return TimelineCenter.Instance.PlayTimelineAsync("透镜/透镜in");
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Expression);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.5f);
expressionManager.OpenView();
// 切换到深度表达相机
yield return CameraKit.Instance.SwitchCamera(CameraEnum.ExpressionDeep);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.5f);
yield return TimelineCenter.Instance.PlayTimelineAsync("火山表达模块/火山表达深入");
}
public IEnumerator Exit(FixTransition transition)
{
var expressionManager = FixSystemCenter.SystemDic.Get<ExpressionManager>();
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.5f);
// 重置身体模块
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
// 关闭表达系统视图
expressionManager.CloseView();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Expression);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.5f);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
yield return TimelineCenter.Instance.PlayTimelineAsync("透镜/透镜out");
yield break;
}
}
public struct EmoCue : IFixSceneCue
{
public FixSceneMode Mode => FixSceneMode.Emo;
public IEnumerator Enter(FixTransition transition)
{
var emoManager = FixSystemCenter.SystemDic.Get<EmoManager>();
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
var bodyModule = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
bodyModule.SwitchModuleGroup(ModuleState.Head);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Emo);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.5f);
//emoManager.OpenView();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.EmoDeep);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.5f);
}
public IEnumerator Exit(FixTransition transition)
{
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
yield break;
}
}
public struct AnalysisModeCue : IFixSceneCue
{
public FixSceneMode Mode => FixSceneMode.AnalysisMode;
public IEnumerator Enter(FixTransition transition)
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
var analysisModeManager = FixSystemCenter.SystemDic.Get<MiniGame.Language.AnalysisModeManager>();
// 先切 BodyModule(如果需要)
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
bodyModuleSystem.SwitchModuleGroup(ModuleState.Head);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
// 插头回到初始插孔
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
// 切换到分析模式相机
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.5f);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.AnalysisMode);
// 进入分析模式
if (analysisModeManager != null)
{
analysisModeManager.OnEnter();
}
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.5f);
}
public IEnumerator Exit(FixTransition transition)
{
float duration = 0.5f;
var analysisModeManager = FixSystemCenter.SystemDic.Get<MiniGame.Language.AnalysisModeManager>();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(duration);
// 退出分析模式
if (analysisModeManager != null)
{
analysisModeManager.OnExit();
}
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(duration);
}
}
public struct SalesCue : IFixSceneCue
{
public FixSceneMode Mode => FixSceneMode.Sales;
public IEnumerator Enter(FixTransition transition)
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
bodyModuleSystem.SwitchModuleGroup(ModuleState.Head);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
// 进入前先播放钳子摘盖子,完成后深入
yield return TimelineCenter.Instance.PlayTimelineAsync("钳子/摘走盖子");
// 获取销售系统管理器
var salesManager = FixSystemCenter.SystemDic.Get<AibisDream.MiniGame.Language.SalesManager>();
if (salesManager != null)
{
// FadeIn
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.5f);
// 打开销售系统视图,传入systemView参数
salesManager.OpenView();
// FadeOut
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.5f);
// 销售模块使用自身的气泡槽位;缺失时再回退到 ScreenSystem,避免读到错误的槽位配置
if (BubbleSlotKit.Instance.TryGet(BubbleSlotEnum.Sales, out _))
{
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Sales);
Debug.Log("[SalesCue] Loaded bubble slots from BubbleSlotKit (Sales).");
}
else
{
Debug.LogWarning("[SalesCue] Sales 气泡槽组未注册,回退到 BodyModule。");
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.BodyModule);
}
}
}
public IEnumerator Exit(FixTransition transition)
{
float duration = 0.5f;
var salesManager = FixSystemCenter.SystemDic.Get<AibisDream.MiniGame.Language.SalesManager>();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(duration);
// 关闭销售系统视图
if (salesManager != null)
{
salesManager.CloseView();
}
// 关闭前先播放钳子放下盖子,完成后再结
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(duration);
yield return TimelineCenter.Instance.PlayTimelineAsync("钳子/放下盖子");
yield return null;
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 98f122636e424c2199a237d2850a03b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,153 @@
using System;
using System.Collections;
using AibisDream.Framework;
using AibisDream.UI;
using DG.Tweening;
using UnityEngine;
namespace AibisDream.FixSystem
{
public struct UfCue : IFixSceneCue
{
public FixSceneMode Mode => FixSceneMode.UF;
public IEnumerator Enter(FixTransition transition)
{
var ufSystem = FixSystemCenter.SystemDic.Get<UFSystem>();
var eyeSystem = FixSystemCenter.SystemDic.Get<EyeSystem>();
// 先切BodyModule
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
// 插头回到初始插孔
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
// 切换相机
yield return CameraKit.Instance.SwitchCamera(CameraEnum.UF);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.5f);
ufSystem.OpenUFView();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.EyeDeep);
eyeSystem.OnEnter();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.5f);
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Eye);
}
public IEnumerator Exit(FixTransition transition)
{
float duration = 0.5f;
var ufSystem = FixSystemCenter.SystemDic.Get<UFSystem>();
var eyeSystem = FixSystemCenter.SystemDic.Get<EyeSystem>();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(duration);
eyeSystem.OnExit();
ufSystem.CloseUFView();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.UF);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(duration);
}
}
public struct EyeCue : IFixSceneCue
{
public FixSceneMode Mode => FixSceneMode.Eye;
public IEnumerator Enter(FixTransition transition)
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
bodyModuleSystem.SwitchModuleGroup(ModuleState.Head);
var eyeSystem = FixSystemCenter.SystemDic.Get<EyeSystem>();
// 先切BodyModule
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
//打开盖子
var eyeModule = bodyModuleSystem.FindBodyModuleByType<EyeModule>();
yield return eyeModule.OpenEye();
// 插头回到初始插孔
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
// 切换相机
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Eye);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(0.5f);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.EyeDeep);
eyeSystem.OnEnter();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(0.5f);
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Eye);
}
public IEnumerator EnterImmediate(FixTransition transition)
{
var eyeSystem = FixSystemCenter.SystemDic.Get<EyeSystem>();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.EyeDeep, true);
eyeSystem.OnEnter();
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Eye);
}
public IEnumerator Exit(FixTransition transition)
{
var bodyModuleSystem = FixSystemCenter.SystemDic.Get<BodyModuleSystem>();
float duration = 0.5f;
var eyeSystem = FixSystemCenter.SystemDic.Get<EyeSystem>();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(duration);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Eye);
eyeSystem.OnExit();
yield return new WaitForSeconds(0.1f);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(duration);
//关上
var eyeModule = bodyModuleSystem.FindBodyModuleByType<EyeModule>();
yield return eyeModule.CloseEye();
yield return new WaitForSeconds(0.5f);
}
}
public struct MemoryCue : IFixSceneCue
{
public FixSceneMode Mode => FixSceneMode.Memory;
public IEnumerator Enter(FixTransition transition)
{
float duration = 0.5f;
var memorySystem = FixSystemCenter.SystemDic.Get<MemoryProcess>();
// 先切BodyModule
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
}
yield return memorySystem.DropMemoryProjector();
// 插头回到初始插孔
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
// 相机聚焦到目标位
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Memory);
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(duration);
memorySystem.OnEnter();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(duration);
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Memory);
}
public IEnumerator EnterImmediate(FixTransition transition)
{
var memorySystem = FixSystemCenter.SystemDic.Get<MemoryProcess>();
yield return memorySystem.DropMemoryProjector();
FixPanelSystem.GetRepairPanel<CablePanel>().ResetPlug();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Memory, true);
memorySystem.OnEnter();
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Memory);
}
public IEnumerator Exit(FixTransition transition)
{
float duration = 0.5f;
var memorySystem = FixSystemCenter.SystemDic.Get<MemoryProcess>();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync(duration);
memorySystem.OnExit();
memorySystem.CloseMemoryView();
yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeOutAsync(duration);
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
memorySystem.PullMemoryProjector();
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 90a6a39492ca40edbf1d17ab821c5e01
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -58,17 +58,17 @@ namespace AibisDream.FixSystem
_whackMoleHandler.EndWhackGame();
var whackRes = _whackMoleHandler.res;
Debug.Log($"结果:共计{whackRes.totalMoleNum}个,成功击中{whackRes.hitMoleNum}个");
StorageSystem.Instance.SetValue("$whackRes.totalMoleNum", whackRes.totalMoleNum);
StorageSystem.Instance.SetValue("$whackRes.hitMoleNum", whackRes.hitMoleNum);
YarnVariableStorage.Instance.SetValue("$whackRes.totalMoleNum", whackRes.totalMoleNum);
YarnVariableStorage.Instance.SetValue("$whackRes.hitMoleNum", whackRes.hitMoleNum);
// 流水线结果
if (_data.HasPipeline())
{
var pipelineRes = pipelineSystem.res;
Debug.Log(
$"流水线结果:共计{pipelineRes.totalProductNum}个,成功{pipelineRes.successNum}个,失败{pipelineRes.failNum}个");
StorageSystem.Instance.SetValue("$whackRes.totalProductNum", pipelineRes.totalProductNum);
StorageSystem.Instance.SetValue("$whackRes.successNum", pipelineRes.successNum);
StorageSystem.Instance.SetValue("$whackRes.failNum", pipelineRes.failNum);
YarnVariableStorage.Instance.SetValue("$whackRes.totalProductNum", pipelineRes.totalProductNum);
YarnVariableStorage.Instance.SetValue("$whackRes.successNum", pipelineRes.successNum);
YarnVariableStorage.Instance.SetValue("$whackRes.failNum", pipelineRes.failNum);
}
// 结束
DialogController.Instance?.StartDialogNode("打地鼠结束");
@@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using AibisDream.FixSystem;
using AibisDream.Framework;
using AibisDream.SaveSystem;
using FMOD;
using FMOD.Studio;
using FMODUnity;
@@ -37,18 +38,14 @@ namespace AibisDream.Kit
#region
private List<IUnRegister> _removeTrigger = new();
private readonly AudioData _audioData = new();
private AmbState _currentAmbState = AmbState.Main;
#endregion
private void Start()
{
// 注册事件
RegisterEvent();
// 启动AMB
InitAmb();
// 注册数据
RegisterData();
}
private void RegisterEvent()
@@ -58,16 +55,9 @@ namespace AibisDream.Kit
_removeTrigger.Add(EnumEventSystem.Global.Register(DialogEventEnum.LineShown, OnLineShown));
// 场景切换相关
_removeTrigger.Add(EnumEventSystem.Global.Register<EventEnum, string>(EventEnum.SceneLoad, OnSceneLoad));
_removeTrigger.Add(EnumEventSystem.Global.Register<EventEnum, FixState>(EventEnum.FixStateSwitch, OnFixStateChange));
_removeTrigger.Add(EnumEventSystem.Global.Register<EventEnum, FixSceneMode>(EventEnum.FixSceneModeChanged, OnFixSceneModeChange));
_removeTrigger.Add(EnumEventSystem.Global.Register(GameLoopEnum.GameQuit, OnGameQuit));
}
private void RegisterData()
{
// 注册数据
StorageSystem.Instance.RegisterData(_audioData);
_audioData.LoadEvent += LoadData;
}
public override void OnSingletonDestroy()
{
@@ -83,7 +73,29 @@ namespace AibisDream.Kit
_removeTrigger.Clear();
}
private void LoadData(AudioData data)
public AudioSnapshotDto CaptureSnapshot()
{
var dto = new AudioSnapshotDto
{
ambState = _currentAmbState.ToString()
};
foreach (var pair in _musicDict)
{
pair.Value.GetDescription().getPath(out var path);
dto.musicPaths[pair.Key] = path;
}
foreach (var pair in _sfxDict)
{
pair.Value.GetDescription().getPath(out var path);
dto.sfxPaths[pair.Key] = path;
}
return dto;
}
public void RestoreSnapshot(AudioSnapshotDto data)
{
foreach (var musicEvent in _musicDict.Values)
{
@@ -94,20 +106,36 @@ namespace AibisDream.Kit
{
sfxEvent.release();
}
foreach (var music in data.musicNames)
_musicDict.Clear();
_sfxDict.Clear();
if (data.musicPaths != null)
{
var instance = RuntimeManager.CreateInstance(music.Value);
instance.start();
_musicDict[music.Key] = instance;
foreach (var music in data.musicPaths)
{
var instance = RuntimeManager.CreateInstance(music.Value);
instance.start();
_musicDict[music.Key] = instance;
}
}
foreach (var sfx in data.sfxNames)
if (data.sfxPaths != null)
{
var instance = RuntimeManager.CreateInstance(sfx.Value);
instance.start();
_sfxDict[sfx.Key] = instance;
foreach (var sfx in data.sfxPaths)
{
var instance = RuntimeManager.CreateInstance(sfx.Value);
instance.start();
_sfxDict[sfx.Key] = instance;
}
}
if (!string.IsNullOrEmpty(data.ambState)
&& Enum.TryParse<AmbState>(data.ambState, out var ambState))
{
ChangeAmb(ambState);
}
}
public void PauseAll()
@@ -140,6 +168,7 @@ namespace AibisDream.Kit
public void ChangeAmb(AmbState state)
{
_currentAmbState = state;
RuntimeManager.StudioSystem.setParameterByName(AmbStateName, (float)state);
}
@@ -178,16 +207,14 @@ namespace AibisDream.Kit
_musicDict.Clear();
_sfxDict.Clear();
_audioData.musicNames.Clear();
_audioData.sfxNames.Clear();
_currentAmbState = AmbState.Main;
}
private void OnFixStateChange(FixState nextState)
private void OnFixSceneModeChange(FixSceneMode nextState)
{
// 状态切换时加载
var state = MatchAmbState(nextState);
Debug.Log("OnFixStateChange: " + state);
Debug.Log("OnFixSceneModeChange: " + state);
ChangeAmb(state);
}
@@ -233,14 +260,11 @@ namespace AibisDream.Kit
private void RemoveMusic(string key)
{
_musicDict.Remove(key);
_audioData.musicNames.Remove(key);
}
private void AddMusic(string key, EventInstance eventInstance)
{
_musicDict[key] = eventInstance;
eventInstance.GetDescription().getPath(out var path);
_audioData.musicNames[key] = path;
}
#endregion
@@ -312,14 +336,11 @@ namespace AibisDream.Kit
private void RemoveSfx(string key)
{
_sfxDict.Remove(key);
_audioData.sfxNames.Remove(key);
}
private void AddSfx(string key, EventInstance eventInstance)
{
_sfxDict[key] = eventInstance;
eventInstance.GetDescription().getPath(out var path);
_audioData.sfxNames[key] = path;
}
#region
@@ -361,18 +382,18 @@ namespace AibisDream.Kit
};
}
private static AmbState MatchAmbState(FixState fixState)
private static AmbState MatchAmbState(FixSceneMode fixState)
{
return fixState switch
{
FixState.Clinic => AmbState.Clinic,
FixState.BodyModule => AmbState.Body,
FixState.EmoCut => AmbState.Head,
FixState.MemoryCut => AmbState.Head,
FixState.LogicCut => AmbState.Head,
FixState.UF => AmbState.UF,
FixState.Eye => AmbState.Eye,
FixState.Memory => AmbState.Memory,
FixSceneMode.Clinic => AmbState.Clinic,
FixSceneMode.BodyModule => AmbState.Body,
FixSceneMode.EmoCut => AmbState.Head,
FixSceneMode.MemoryCut => AmbState.Head,
FixSceneMode.LogicCut => AmbState.Head,
FixSceneMode.UF => AmbState.UF,
FixSceneMode.Eye => AmbState.Eye,
FixSceneMode.Memory => AmbState.Memory,
_ => AmbState.Main
};
}
@@ -440,19 +461,5 @@ namespace AibisDream.Kit
public void PauseAll();
public void UnPauseAll();
}
public class AudioData : IData
{
public readonly Dictionary<string, string> sfxNames = new();
public readonly Dictionary<string, string> musicNames = new();
public event Action<AudioData> LoadEvent;
public IEnumerator Load()
{
LoadEvent?.Invoke(this);
yield break;
}
}
}
-189
View File
@@ -1,9 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using AibisDream.Utility;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace AibisDream.Framework
@@ -24,15 +21,6 @@ namespace AibisDream.Framework
public abstract void SaveLevel();
}
public interface IData
{
IEnumerator Load();
void Save()
{
}
}
#region IOC容器
public class IOCContainer
@@ -89,183 +77,6 @@ namespace AibisDream.Framework
}
}
public class DataContainer
{
private Dictionary<string, IData> _instances = new();
public int Count => _instances.Count;
public void Register<T>(T instance) where T : IData
{
var key = typeof(T).AssemblyQualifiedName;
if (key != null) _instances[key] = instance;
}
public T Get<T>() where T : class, IData
{
var key = typeof(T).AssemblyQualifiedName;
if (key != null && _instances.TryGetValue(key, out var retInstance))
{
return retInstance as T;
}
return null;
}
public object Get(Type type)
{
var key = type.AssemblyQualifiedName;
if (key != null && _instances.TryGetValue(key, out var retInstance))
{
return retInstance;
}
return null;
}
public T Unregister<T>() where T : class, IData
{
var res = Get<T>();
var assemblyQualifiedName = typeof(T).AssemblyQualifiedName;
if (res != null && assemblyQualifiedName != null)
{
_instances.Remove(assemblyQualifiedName);
return res;
}
return null;
}
public void Clear()
{
_instances.Clear();
}
public JObject SaveAsJson()
{
foreach (var data in _instances.Values)
{
data.Save();
}
return JObject.FromObject(_instances);
}
public IEnumerator LoadByJson(JObject dataJson)
{
// 从加载序号1到10加载数据
for (var i = 1; i <= 10; i++)
{
// 先筛选对应序号
var curIdx = i;
var idxInstances = _instances.Where(item => LoadIndex.GetLoadIdx(item.Value) == curIdx).ToArray();
// 回填数据并加载
foreach (var instancePair in idxInstances)
{
if (!dataJson.TryGetValue(instancePair.Key, out var data))
{
Debug.Log(instancePair.Key + "未注册");
continue;
}
var fixDataType = Type.GetType(instancePair.Key);
if (fixDataType == null)
{
Debug.Log(instancePair.Key + "类型不存在");
continue;
}
// 把Json中读到的参数填回Data
var tempObj = data.ToObject(fixDataType);
CommonUtil.CopyFields(tempObj, instancePair.Value);
// 回填完成后进行加载
yield return instancePair.Value.Load();
}
}
}
public bool TryGetFieldValue<T>(string variableName, out T fieldValue)
{
fieldValue = default;
var parts = variableName.Split('.');
if (parts.Length <= 3)
{
Debug.LogError($"{variableName}输入格式错误,应为 '$data.类名.字段名'");
return false;
}
var className = parts[1];
var fieldName = parts[2];
var key = _instances.Keys.FirstOrDefault(key => key.Contains(className));
if (string.IsNullOrEmpty(key))
{
Debug.LogError($"未找到类名为 {className} 的实例");
return false;
}
var instance = _instances[key];
var fieldInfo = instance.GetType().GetField(fieldName);
if (fieldInfo == null)
{
Debug.LogError($"未找到字段名为 {fieldName} 的字段");
return false;
}
var value = fieldInfo.GetValue(instance);
if (typeof(T) == typeof(string))
{
// 单独处理字符串类型
fieldValue = (T)(object)(value?.ToString() ?? string.Empty);
return true;
}
if (typeof(T) == typeof(float))
{
// 处理数字类型转换为 float
if (value is IConvertible)
{
try
{
fieldValue = (T)(object)Convert.ToSingle(value);
return true;
}
catch (Exception ex)
{
Debug.LogError($"无法将字段值转换为 float: {ex.Message}");
return false;
}
}
Debug.LogError("字段值不是可转换为 float 的类型");
return false;
}
if (typeof(T) == typeof(bool))
{
// 处理布尔类型转换为 bool
if (value is IConvertible)
{
fieldValue = (T)(object)Convert.ToBoolean(value);
return true;
}
Debug.LogError("字段值不是可转换为 bool 的类型");
return false;
}
if (value is T typedValue)
{
// 处理其他类型
fieldValue = typedValue;
return true;
}
Debug.LogError($"未找到类型为 {typeof(T).Name} 的转换方法");
return false;
}
}
#endregion
#region
@@ -264,6 +264,67 @@ namespace AibisDream.Framework
return null;
}
}
/// <summary>
/// 从 AnimatorController 构建 shortNameHash → 状态短名 映射,供 ActorAnima 快照 Capture 反查。
/// Editor 下遍历完整状态机;运行时 fallback 为 animationClips 名称(与状态名一致时有效)。
/// </summary>
public static Dictionary<int, string> BuildStateHashToNameMap(RuntimeAnimatorController controller)
{
var map = new Dictionary<int, string>();
if (controller == null)
{
return map;
}
#if UNITY_EDITOR
if (controller is AnimatorController animatorController)
{
foreach (var layer in animatorController.layers)
{
if (layer.stateMachine != null)
{
CollectStateNamesFromStateMachine(layer.stateMachine, map);
}
}
return map;
}
#endif
foreach (var clip in controller.animationClips)
{
if (clip == null) continue;
var hash = Animator.StringToHash(clip.name);
map.TryAdd(hash, clip.name);
}
return map;
}
#if UNITY_EDITOR
private static void CollectStateNamesFromStateMachine(
AnimatorStateMachine stateMachine,
Dictionary<int, string> map)
{
if (stateMachine == null) return;
foreach (var childState in stateMachine.states)
{
if (childState.state == null) continue;
var hash = Animator.StringToHash(childState.state.name);
map.TryAdd(hash, childState.state.name);
}
foreach (var subStateMachine in stateMachine.stateMachines)
{
if (subStateMachine.stateMachine != null)
{
CollectStateNamesFromStateMachine(subStateMachine.stateMachine, map);
}
}
}
#endif
}
/// <summary>
@@ -0,0 +1,94 @@
using System.Collections.Generic;
using AibisDream.Kit;
using UnityEngine;
namespace AibisDream.Framework
{
public class BubbleSlotKit : SingletonBase<BubbleSlotKit>
{
private readonly Dictionary<BubbleSlotEnum, BubbleSlotGroup> _slotDict = new();
private BubbleSlotKit()
{
}
public void Register(BubbleSlotEnum slotType, BubbleSlotGroup group)
{
if (slotType == BubbleSlotEnum.Default)
{
Debug.LogWarning("[BubbleSlotKit] 不能注册 Default 类型的气泡槽组。");
return;
}
if (_slotDict.TryGetValue(slotType, out var existing) && existing != null && existing != group)
{
Debug.LogWarning(
$"[BubbleSlotKit] {slotType} 已被 \"{existing.name}\" 注册,将被 \"{group.name}\" 覆盖");
}
_slotDict[slotType] = group;
}
public void Unregister(BubbleSlotEnum slotType)
{
_slotDict.Remove(slotType);
}
public void RemoveAll()
{
_slotDict.Clear();
}
public bool TryGet(BubbleSlotEnum slotType, out BubbleSlotGroup group)
{
if (_slotDict.TryGetValue(slotType, out group) && group != null)
{
return true;
}
group = null;
return false;
}
public BubbleSlotGroupData CollectData(BubbleSlotEnum slotType)
{
if (!TryGet(slotType, out var group))
{
Debug.LogWarning($"[BubbleSlotKit] CollectData: {slotType} 气泡槽组未注册");
return default;
}
return group.CollectData();
}
public void LoadBubbles(BubbleSlotEnum slotType)
{
if (!TryGet(slotType, out var group))
{
Debug.LogWarning($"[BubbleSlotKit] LoadBubbles: {slotType} 气泡槽组未注册");
return;
}
DialogUIManager.Instance.LoadBubbles(group.CollectData());
}
}
public enum BubbleSlotEnum
{
Default,
Clinic,
BodyModule,
Memory,
Eye,
Cut,
Open,
Sales,
Subway,
SubwayTV,
Outside,
Tarot,
Emo,
Expression,
Analysis
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a8f3c2e1b94d4f6a9e7d5c3b2a10876f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+23 -1
View File
@@ -93,6 +93,18 @@ namespace AibisDream.Framework
public void RegisterCamera(CameraEnum camType, ICinemachineCamera virtualCamera)
{
if (_virtualCameraDict.TryGetValue(camType, out var existing) && existing != virtualCamera)
{
var existingName = existing?.VirtualCameraGameObject != null
? existing.VirtualCameraGameObject.name
: "unknown";
var newName = virtualCamera?.VirtualCameraGameObject != null
? virtualCamera.VirtualCameraGameObject.name
: "unknown";
Debug.LogWarning(
$"RegisterCamera: {camType} 已被 \"{existingName}\" 注册,将被 \"{newName}\" 覆盖");
}
_virtualCameraDict[camType] = virtualCamera;
var vcam = virtualCamera.VirtualCameraGameObject.GetComponent<CinemachineVirtualCamera>();
@@ -170,11 +182,15 @@ namespace AibisDream.Framework
var activeBrain = _orthoCameraBrain;
CinemachineBlendDefinition originalBlend = default;
CinemachineBlenderSettings savedCustomBlends = null;
if (immediate)
{
originalBlend = activeBrain.m_DefaultBlend;
activeBrain.m_DefaultBlend = new CinemachineBlendDefinition(
CinemachineBlendDefinition.Style.Cut, 0f);
// Custom Blends 会覆盖 DefaultBlendimmediate 时必须临时禁用,否则仍走 1s 缓动。
savedCustomBlends = activeBrain.m_CustomBlends;
activeBrain.m_CustomBlends = null;
}
_virtualCameraDict.TryGetValue(_curCameraEnum, out var fromVCam);
@@ -193,8 +209,14 @@ namespace AibisDream.Framework
if (immediate)
{
ResetVirCam(lastCamEnum);
activeBrain.ManualUpdate();
var blend = originalBlend;
ActionKit.Delay(0f, () => activeBrain.m_DefaultBlend = blend).StartGlobal();
var customBlends = savedCustomBlends;
ActionKit.DelayFrame(2, () =>
{
activeBrain.m_DefaultBlend = blend;
activeBrain.m_CustomBlends = customBlends;
}).StartGlobal();
return 0f;
}
@@ -0,0 +1,38 @@
using Cinemachine;
using UnityEngine;
namespace AibisDream.Framework
{
[RequireComponent(typeof(CinemachineVirtualCamera))]
public class VirtualCamRegistry : MonoBehaviour
{
[SerializeField] private CameraEnum cameraType;
private void Awake()
{
if (cameraType == CameraEnum.Default)
{
Debug.LogWarning($"[{nameof(VirtualCamRegistry)}] cameraType 不能为 Default,请在 Inspector 配置。", this);
return;
}
if (!TryGetComponent<ICinemachineCamera>(out var virtualCam))
{
Debug.LogWarning($"[{nameof(VirtualCamRegistry)}] 未找到 ICinemachineCamera 组件。", this);
return;
}
CameraKit.Instance.RegisterCamera(cameraType, virtualCam);
}
private void OnDestroy()
{
if (cameraType == CameraEnum.Default)
{
return;
}
CameraKit.Instance?.UnRegisterCamera(cameraType);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7432789ba6bd1a340b824676e1e56184
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -6,11 +6,13 @@ namespace AibisDream.Framework
public class StateMachine<T> where T : Enum
{
public T CurState => _curState.GetState;
public bool HasState => _curState != null;
public IState<T> CurrentStateObj => _curState;
public event Action<T> OnStateSwitch;
private IState<T> _curState;
private readonly Func<T, string, IState<T>> _createState;
public StateMachine(Func<T, string, IState<T>> createState)
{
_createState = createState;
@@ -30,7 +32,28 @@ namespace AibisDream.Framework
OnStateSwitch?.Invoke(nextState);
yield return _curState.Enter();
}
/// <summary>
/// 强制设置内部状态对象,不触发 Exit / Enter / OnStateSwitch。
/// 用于读档等需要绕过正常状态过渡的场景。
/// </summary>
public void SetStateImmediate(T state, string args = "")
{
_curState = _createState.Invoke(state, args);
}
/// <summary>
/// 读档恢复:跳过前一状态的 Exit,直接进入目标状态的 EnterImmediate。
/// </summary>
public IEnumerator SwitchStateImmediate(T nextState, string args = "")
{
if (_curState != null && CurState.Equals(nextState)) yield break;
_curState = _createState.Invoke(nextState, args);
OnStateSwitch?.Invoke(nextState);
yield return _curState.EnterImmediate();
}
public IEnumerator QuitState()
{
yield return _curState?.Exit();
@@ -44,6 +67,12 @@ namespace AibisDream.Framework
public void SetArgs(string args) {}
public IEnumerator Enter();
/// <summary>读档恢复:跳过过渡动画,直接到达终态。</summary>
public IEnumerator EnterImmediate()
{
yield break;
}
public IEnumerator Exit()
{
yield break;
+2 -2
View File
@@ -11,7 +11,7 @@ namespace AibisDream
PlugIn,
PlugOut,
SceneLoad,
FixStateSwitch,
FixSceneModeChanged,
CutEnd
}
@@ -85,4 +85,4 @@ namespace AibisDream
TextSpeed,
LocaleChanged
}
}
}
+26 -29
View File
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using AibisDream.FixSystem;
using AibisDream.Framework;
using AibisDream.Kit;
using AibisDream.SaveSystem;
using AibisDream.UI;
using AibisDream.Utility;
using UnityEngine;
@@ -27,8 +28,6 @@ namespace AibisDream
#endregion
private BindProperty<TalkSceneSO> _currentTalkSceneSo;
private readonly GameLoopData _data = new();
public override void OnSingletonInit()
{
LoadAllSceneSos();
@@ -38,11 +37,9 @@ namespace AibisDream
{
// 管理游戏开始时各部分的初始化
SettingLoader.Init();
// 数据类注册
StorageSystem.Instance.RegisterData(_data);
SnapshotRegistry.EnsureInitialized();
// 场景So事件注册
_currentTalkSceneSo = new BindProperty<TalkSceneSO>();
_currentTalkSceneSo.Register(item => _data.curSceneSoName = item.name);
#if UNITY_EDITOR
if (runMode == RunMode.SingleScene)
@@ -114,7 +111,25 @@ namespace AibisDream
// 游戏开始
EnumEventSystem.Global.Send(GameLoopEnum.GameStart);
StorageSystem.Instance.Load(fileName);
StartCoroutine(LoadFromSaveFile(fileName));
}
public void StartWithSlot(int slotIndex)
{
// 游戏开始
EnumEventSystem.Global.Send(GameLoopEnum.GameStart);
StartCoroutine(LoadFromSlot(slotIndex));
}
private IEnumerator LoadFromSaveFile(string fileName)
{
yield return SaveRestoreOrchestrator.RestoreFromFile(fileName);
}
private IEnumerator LoadFromSlot(int slotIndex)
{
yield return SaveRestoreOrchestrator.RestoreFromSlot(slotIndex);
}
private IEnumerator LoadFirstScene()
@@ -270,6 +285,11 @@ namespace AibisDream
DialogController.Instance.LoadDialog(_currentTalkSceneSo.Value.yarnProject);
}
public TalkSceneSO GetCurrentTalkSceneSo()
{
return _currentTalkSceneSo?.Value;
}
public string GetSceneSoName()
{
return _currentTalkSceneSo.Value.name;
@@ -336,29 +356,6 @@ namespace AibisDream
#endregion
}
[Serializable]
[LoadIndex(10)]
public class GameLoopData : IData
{
public string curSceneSoName;
public IEnumerator Load()
{
GameManager.Instance.SetSceneSoByName(curSceneSoName);
GameManager.Instance.StartDialog();
yield break;
}
}
public static class GameLoopYarnCommand
{
[YarnCommand("auto_save")]
public static void AutoSave()
{
StorageSystem.Instance.Save();
}
}
public struct GameLoopState
{
public bool isInGame;
-22
View File
@@ -1,22 +0,0 @@
using System;
using System.Reflection;
namespace AibisDream
{
public class LoadIndex : Attribute
{
private int Idx { get; }
public LoadIndex(int idx)
{
Idx = idx;
}
public static int GetLoadIdx<T>(T instance)
{
// 先通过反射拿到属性
var attribute = instance.GetType().GetCustomAttribute<LoadIndex>();
return attribute?.Idx ?? 5;
}
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: cbb1d63c733941879217082bda086ade
timeCreated: 1742905792
+7 -19
View File
@@ -1,5 +1,4 @@
using System;
using System.Collections;
using System.Collections;
using AibisDream.Framework;
using AibisDream.Kit;
using AibisDream.FixSystem;
@@ -19,11 +18,12 @@ namespace AibisDream
private string _sceneToLoad;
private bool _isLoading;
private readonly SceneData _data = new();
private string _currentSceneName;
public string CurrentSceneName => _currentSceneName;
public override void OnSingletonInit()
{
StorageSystem.Instance.RegisterData(_data);
}
#endregion
@@ -56,7 +56,7 @@ namespace AibisDream
_sceneToLoad = targetScene;
// 在卸载场景前清理所有系统
if (!string.IsNullOrEmpty(_data.sceneName))
if (!string.IsNullOrEmpty(_currentSceneName))
{
DOTween.KillAll();
@@ -79,7 +79,7 @@ namespace AibisDream
// 场景加载完成,通知各系统
EnumEventSystem.Global.Send(EventEnum.SceneLoad, _sceneToLoad);
_data.sceneName = _sceneToLoad;
_currentSceneName = _sceneToLoad;
UIManager.Instance.GetPanel<InfoPanel>().HideLoading();
_isLoading = false;
}
@@ -100,7 +100,7 @@ namespace AibisDream
ResourceSystem.ReleaseSceneLoader();
yield return Addressables.UnloadSceneAsync(_loadHandle);
_data.sceneName = null;
_currentSceneName = null;
}
_isLoading = false;
@@ -108,16 +108,4 @@ namespace AibisDream
}
}
[Serializable]
[LoadIndex(1)]
public class SceneData : IData
{
public string sceneName;
public IEnumerator Load()
{
// yield return UIManager.Instance.GetPanel<PlayToolPanel>().FadeInAsync();
yield return SceneLoader.Instance.LoadSceneAsync(sceneName);
}
}
}
-403
View File
@@ -1,403 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using AibisDream.Framework;
using AibisDream.Kit;
using AibisDream.Utility;
using AibisDream.UI;
using Newtonsoft.Json.Linq;
using UnityEngine;
using Yarn.Unity;
namespace AibisDream
{
public class StorageSystem : VariableStorageBehaviour
{
private const string VariablePrefix = "$";
private const string GlobalPrefix = "$global_";
private const string DataPrefix = "$data.";
private const int MaxSaveNum = 200;
#region
private readonly Dictionary<string, object> _variableDict = new();
private readonly DataContainer _dataContainer = new();
#endregion
#region
public void RegisterData<T>(T data) where T : IData
{
_dataContainer.Register(data);
}
public void UnregisterData<T>() where T : class, IData
{
_dataContainer.Unregister<T>();
}
#endregion
#region
public override void SetValue(string variableName, bool boolValue)
{
SetValue(variableName, boolValue);
}
public override void SetValue(string variableName, string stringValue)
{
SetValue(variableName, stringValue);
}
public override void SetValue(string variableName, float floatValue)
{
SetValue(variableName, floatValue);
}
private void SetValue<T>(string variableName, T value)
{
var variableType = ValidateVariableName(variableName);
if (variableType == VariableType.Illegal)
{
Debug.LogError($"{variableName}: 缺少$前缀");
return;
}
if (variableType == VariableType.DataField)
{
Debug.LogError($"{variableName}: 暂时不支持直接设置系统数据");
return;
}
// Global和Local直接设置
_variableDict[variableName] = value;
// 数据设置事件广播
var variableItem = new VariableItem
{
variableName = variableName,
value = value
};
EnumEventSystem.Global.Send(StorageEvent.VariableSet, variableItem);
}
public override void Clear()
{
_variableDict.Clear();
}
public void ClearLocal()
{
// 收集所有Local类型的变量
var locals = _variableDict.Keys.Where(item => ValidateVariableName(item) == VariableType.Local).ToArray();
// 清除
foreach (var localKey in locals)
{
_variableDict.Remove(localKey);
}
}
#endregion
#region
public override bool Contains(string variableName)
{
var variableType = ValidateVariableName(variableName);
if (variableType == VariableType.Illegal)
{
return false;
}
return variableType == VariableType.DataField
? _dataContainer.TryGetFieldValue<string>(variableName, out _)
: _variableDict.ContainsKey(variableName);
}
public override bool TryGetValue<T>(string variableName, out T result)
{
var variableType = ValidateVariableName(variableName);
// 非法的直接pass
if (variableType == VariableType.Illegal)
{
result = default;
return false;
}
// 在系统数据中查找
if (variableType == VariableType.DataField)
{
return _dataContainer.TryGetFieldValue(variableName, out result);
}
// 直接查找数据
if (_variableDict.TryGetValue(variableName, out var value))
{
if (value is T target)
{
result = target;
return true;
}
}
if (TryGetInitialValue(variableName, out result))
{
return true;
}
result = default;
return false;
}
private bool TryGetInitialValue<T>(string variableName, out T result)
{
result = default;
if (Program == null || !Program.InitialValues.TryGetValue(variableName, out var initialValue))
{
return false;
}
if (!InitialValueMatchesType<T>(initialValue.ValueCase))
{
return false;
}
return Program.TryGetInitialValue(variableName, out result);
}
private static bool InitialValueMatchesType<T>(Yarn.Operand.ValueOneofCase valueCase)
{
var targetType = typeof(T);
return valueCase switch
{
Yarn.Operand.ValueOneofCase.BoolValue => targetType == typeof(bool),
Yarn.Operand.ValueOneofCase.StringValue => targetType == typeof(string),
Yarn.Operand.ValueOneofCase.FloatValue => targetType == typeof(float),
_ => false
};
}
#endregion
#region
public override void SetAllVariables(Dictionary<string, float> floats, Dictionary<string, string> strings,
Dictionary<string, bool> bools, bool clear = true)
{
if (clear)
{
Clear();
}
foreach (var value in floats)
{
SetValue(value.Key, value.Value);
}
foreach (var value in strings)
{
SetValue(value.Key, value.Value);
}
foreach (var value in bools)
{
SetValue(value.Key, value.Value);
}
}
public override (Dictionary<string, float> FloatVariables, Dictionary<string, string> StringVariables,
Dictionary<string, bool> BoolVariables) GetAllVariables()
{
var floatDict = new Dictionary<string, float>();
var stringDict = new Dictionary<string, string>();
var boolDict = new Dictionary<string, bool>();
foreach (var variablePair in _variableDict)
{
switch (variablePair.Value)
{
case float:
{
var value = Convert.ToSingle(variablePair.Value);
floatDict.Add(variablePair.Key, value);
break;
}
case string:
{
var value = Convert.ToString(variablePair.Value);
stringDict.Add(variablePair.Key, value);
break;
}
case bool:
{
var value = Convert.ToBoolean(variablePair.Value);
boolDict.Add(variablePair.Key, value);
break;
}
default:
Debug.Log($"{variablePair.Key} is not a valid type");
break;
}
}
return (floatDict, stringDict, boolDict);
}
#endregion
#region /
public void Save()
{
ActionKit.Sequence()
.Callback(() => UIManager.Instance.GetPanel<InfoPanel>().ShowSaveLoading())
.Delay(1f)
.Callback(() => UIManager.Instance.GetPanel<InfoPanel>().HideSaveLoading())
.Start(this);
using (new CodeTimer("Save"))
{
// 先序列化Yarn变量
var saveFile = new JObject();
// 保存版本号
saveFile["version"] = $"Ver.{Application.version}";
saveFile["time"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
// 保存系统数据
saveFile["systemData"] = _dataContainer.SaveAsJson();
// 保存对话变量
var (floatDict, stringDict, boolDict) = GetAllVariables();
saveFile["floatDict"] = JObject.FromObject(floatDict);
saveFile["stringDict"] = JObject.FromObject(stringDict);
saveFile["boolDict"] = JObject.FromObject(boolDict);
// 存为Json
JsonUtil.SaveJObject(saveFile, GetSavePath());
#if !UNITY_EDITOR
DeleteOldSave();
#endif
}
}
private void DeleteOldSave()
{
if (Directory.Exists(ConstRef.SaveFilePath))
{
var fileList = Directory.GetFiles(ConstRef.SaveFilePath);
if (fileList.Length > MaxSaveNum)
{
var sorted = fileList.OrderByDescending(f => f).ToArray();
for (var i = MaxSaveNum; i < fileList.Length; i++)
{
File.Delete(sorted[i]);
}
}
}
else
{
Directory.CreateDirectory(ConstRef.SaveFilePath);
}
}
public void Load(string savePath)
{
// 先将存档文件转为JObject
var saveData = JsonUtil.ReadJObject(savePath);
// 加载Yarn数据
var floatDict = saveData["floatDict"]?.ToObject<Dictionary<string, float>>();
var stringDict = saveData["stringDict"]?.ToObject<Dictionary<string, string>>();
var boolDict = saveData["boolDict"]?.ToObject<Dictionary<string, bool>>();
SetAllVariables(floatDict, stringDict, boolDict);
// 加载系统数据
if (saveData["systemData"] is not JObject systemDataJson)
{
Debug.Log("维修数据读取问题");
return;
}
StartCoroutine(_dataContainer.LoadByJson(systemDataJson));
}
private string GetSavePath()
{
Directory.CreateDirectory(ConstRef.SaveFilePath);
string gameStage;
if (TryGetValue("$gameStage", out float gameStageFloat))
{
gameStage = gameStageFloat.ToString(CultureInfo.InvariantCulture);
}
else
{
TryGetValue("$gameStage", out string gameStageStr);
gameStage = gameStageStr;
}
return
$"{ConstRef.SaveFilePath}/{DateTime.Now:yyyyMMdd-HHmmss}_{GameManager.Instance.GetSceneSoName()}_{gameStage}";
}
#endregion
private static VariableType ValidateVariableName(string variableName)
{
if (string.IsNullOrWhiteSpace(variableName) || !variableName.StartsWith(VariablePrefix))
{
return VariableType.Illegal;
}
if (variableName.StartsWith(GlobalPrefix))
{
return VariableType.Global;
}
if (variableName.StartsWith(DataPrefix))
{
return VariableType.DataField;
}
return VariableType.Local;
}
#region
public static StorageSystem Instance { get; private set; }
private void Awake()
{
if (Instance != null)
{
Destroy(gameObject);
}
Instance = this;
}
#endregion
}
public enum VariableType
{
Local,
Global,
DataField,
Illegal
}
public struct VariableItem
{
public string variableName;
public object value;
}
}
@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: bd2f44edfbd942b49bb7a37f80d7a5eb
timeCreated: 1742802488
@@ -0,0 +1,252 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AibisDream.Framework;
using AibisDream.SaveSystem;
using UnityEngine;
using Yarn;
using Yarn.Unity;
namespace AibisDream
{
/// <summary>
/// Yarn 运行时变量存储(VariableStorageBehaviour)。
/// 存档快照、落盘、深度维修容器见 SaveSystem 下各类。
/// </summary>
public class YarnVariableStorage : VariableStorageBehaviour
{
private const string VariablePrefix = "$";
private const string GlobalPrefix = "$global_";
private readonly Dictionary<string, object> _variableDict = new();
#region
public override void SetValue(string variableName, bool boolValue)
{
SetValue(variableName, boolValue);
}
public override void SetValue(string variableName, string stringValue)
{
SetValue(variableName, stringValue);
}
public override void SetValue(string variableName, float floatValue)
{
SetValue(variableName, floatValue);
}
private void SetValue<T>(string variableName, T value)
{
var variableType = ValidateVariableName(variableName);
if (variableType == VariableType.Illegal)
{
Debug.LogError($"{variableName}: 缺少$前缀");
return;
}
_variableDict[variableName] = value;
var variableItem = new VariableItem
{
variableName = variableName,
value = value
};
EnumEventSystem.Global.Send(StorageEvent.VariableSet, variableItem);
}
public override void Clear()
{
_variableDict.Clear();
}
public void ClearLocal()
{
var locals = _variableDict.Keys.Where(item => ValidateVariableName(item) == VariableType.Local).ToArray();
foreach (var localKey in locals)
{
_variableDict.Remove(localKey);
}
}
#endregion
#region
public override bool Contains(string variableName)
{
var variableType = ValidateVariableName(variableName);
if (variableType == VariableType.Illegal)
{
return false;
}
return _variableDict.ContainsKey(variableName);
}
public override bool TryGetValue<T>(string variableName, out T result)
{
var variableType = ValidateVariableName(variableName);
if (variableType == VariableType.Illegal)
{
result = default;
return false;
}
switch (GetVariableKind(variableName))
{
case VariableKind.Stored:
if (_variableDict.TryGetValue(variableName, out var value) && value is T target)
{
result = target;
return true;
}
if (Program == null)
{
throw new InvalidOperationException(
$"Can't get initial value for variable {variableName}, because {nameof(Program)} is not set");
}
return Program.TryGetInitialValue(variableName, out result);
case VariableKind.Smart:
if (SmartVariableEvaluator == null)
{
throw new InvalidOperationException(
$"Can't get value for smart variable {variableName}, because {nameof(SmartVariableEvaluator)} is not set");
}
return SmartVariableEvaluator.TryGetSmartVariable(variableName, out result);
default:
result = default;
return false;
}
}
#endregion
#region SnapshotCapture / SnapshotRestore 使
public override void SetAllVariables(Dictionary<string, float> floats, Dictionary<string, string> strings,
Dictionary<string, bool> bools, bool clear = true)
{
if (clear)
{
Clear();
}
if (floats != null)
{
foreach (var value in floats)
{
SetValue(value.Key, value.Value);
}
}
if (strings != null)
{
foreach (var value in strings)
{
SetValue(value.Key, value.Value);
}
}
if (bools != null)
{
foreach (var value in bools)
{
SetValue(value.Key, value.Value);
}
}
}
public override (Dictionary<string, float> FloatVariables, Dictionary<string, string> StringVariables,
Dictionary<string, bool> BoolVariables) GetAllVariables()
{
var floatDict = new Dictionary<string, float>();
var stringDict = new Dictionary<string, string>();
var boolDict = new Dictionary<string, bool>();
foreach (var variablePair in _variableDict)
{
switch (variablePair.Value)
{
case float:
{
var value = Convert.ToSingle(variablePair.Value);
floatDict.Add(variablePair.Key, value);
break;
}
case string:
{
var value = Convert.ToString(variablePair.Value);
stringDict.Add(variablePair.Key, value);
break;
}
case bool:
{
var value = Convert.ToBoolean(variablePair.Value);
boolDict.Add(variablePair.Key, value);
break;
}
default:
Debug.Log($"{variablePair.Key} is not a valid type");
break;
}
}
return (floatDict, stringDict, boolDict);
}
#endregion
private static VariableType ValidateVariableName(string variableName)
{
if (string.IsNullOrWhiteSpace(variableName) || !variableName.StartsWith(VariablePrefix))
{
return VariableType.Illegal;
}
if (variableName.StartsWith(GlobalPrefix))
{
return VariableType.Global;
}
return VariableType.Local;
}
#region
public static YarnVariableStorage Instance { get; private set; }
private void Awake()
{
if (Instance != null)
{
Destroy(gameObject);
}
Instance = this;
SnapshotRegistry.EnsureInitialized();
}
#endregion
}
public enum VariableType
{
Local,
Global,
Illegal
}
public struct VariableItem
{
public string variableName;
public object value;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bd2f44edfbd942b49bb7a37f80d7a5eb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -123,7 +123,7 @@ namespace AibisDream
var validateResult = validator.Validate(grid.GetShapes());
var shapesInGrid = grid.GetShapes().Select(s => s.BlockShapeData.blockShapeInfo.shapeName).ToArray();
StorageSystem.Instance.SetValue("$shapesInGrid", string.Join(",", shapesInGrid));
YarnVariableStorage.Instance.SetValue("$shapesInGrid", string.Join(",", shapesInGrid));
// 如果验证通过, 则设置变量并弹出对话
if (validateResult.isPass)
@@ -236,7 +236,7 @@ namespace AibisDream
// 点击形状
blockPuzzlePanel.ShowShapeInScreen(blockShape.BlockShapeData.blockShapeInfo);
// 播放事件节点
StorageSystem.Instance.SetValue("$selectedShape", blockShape.BlockShapeData.blockShapeInfo.shapeName);
YarnVariableStorage.Instance.SetValue("$selectedShape", blockShape.BlockShapeData.blockShapeInfo.shapeName);
SingleCastEventSystem.Global.Trigger(DialogEventEnum.StartNode, "ShapeSelected");
}
@@ -6,8 +6,6 @@ using System.Linq;
using DG.Tweening;
using Shapes;
using AibisDream.FixSystem;
using AibisDream.Framework;
using Cinemachine;
namespace AibisDream.MiniGame.Language
{
@@ -137,7 +135,6 @@ namespace AibisDream.MiniGame.Language
/// </summary>
public class AnalysisModeManager : MonoBehaviour
{
public BubbleSlotGroup bubbleSlotGroup;
[Header("Canvas 引用(共享)")]
[SerializeField] private Canvas sharedCanvas; // 共享 LanguageParticleManager 的 canvas
@@ -225,25 +222,10 @@ namespace AibisDream.MiniGame.Language
private void Start()
{
// 注册到系统字典
FixSystemCenter.SystemDic.Register(this);
// 注册相机
var virtualCam = transform.Find("Analysis Camera")?.GetComponent<ICinemachineCamera>();
if (virtualCam != null)
{
CameraKit.Instance.RegisterCamera(CameraEnum.AnalysisMode, virtualCam);
}
SetupUI();
}
private void OnDestroy()
{
CameraKit.Instance?.UnRegisterCamera(CameraEnum.AnalysisMode);
CameraKit.Instance?.UnRegisterCamera(CameraEnum.AnalysisModeDeep);
}
/// <summary>
/// 进入分析模式(由状态机调用)
/// </summary>
@@ -1,13 +1,10 @@
using UnityEngine;
using AibisDream.FixSystem;
using Cinemachine;
using AibisDream.Framework;
namespace AibisDream
{
public class EmoManager : MonoBehaviour
{
public BubbleSlotGroup bubbleSlotGroup;
private GameObject emoView;
void Start()
@@ -15,10 +12,6 @@ namespace AibisDream
FixSystemCenter.SystemDic.Register(this);
emoView = transform.Find("Emo View").gameObject;
emoView.SetActive(false);
var virtualCam = transform.Find("Emo Camera").GetComponent<ICinemachineCamera>();
var virtualCam2 = transform.Find("Emo Deep Camera").GetComponent<ICinemachineCamera>();
CameraKit.Instance.RegisterCamera(CameraEnum.Emo, virtualCam);
CameraKit.Instance.RegisterCamera(CameraEnum.EmoDeep, virtualCam2);
}
public void OpenView()
@@ -2,8 +2,6 @@ using System.Collections;
using DG.Tweening;
using UnityEngine;
using AibisDream.FixSystem;
using Cinemachine;
using AibisDream.Framework;
using AibisDream.MiniGame.Language;
using System.Collections.Generic;
@@ -14,7 +12,6 @@ namespace AibisDream
/// </summary>
public class ExpressionManager : MonoBehaviour
{
public BubbleSlotGroup bubbleSlotGroup;
[Header("系统组件")]
[SerializeField] private LanguageParticleManager particleManager;
[SerializeField] private GameObject expressionView;
@@ -61,19 +58,6 @@ namespace AibisDream
{
glitchController = GetComponentInChildren<SpriteNoiseGlitchController>(true);
}
// 注册相机
var virtualCam = transform.Find("Expression Camera")?.GetComponent<ICinemachineCamera>();
if (virtualCam != null)
{
CameraKit.Instance.RegisterCamera(CameraEnum.Expression, virtualCam);
}
var virtualCam2 = transform.Find("Expression Deep Camera")?.GetComponent<ICinemachineCamera>();
if (virtualCam2 != null)
{
CameraKit.Instance.RegisterCamera(CameraEnum.ExpressionDeep, virtualCam2);
}
}
/// <summary>
@@ -251,10 +235,5 @@ namespace AibisDream
yield return glitchController.StartCoroutine(glitchController.TransitionToAndWait(to, duration));
}
private void OnDestroy()
{
CameraKit.Instance?.UnRegisterCamera(CameraEnum.Expression);
CameraKit.Instance?.UnRegisterCamera(CameraEnum.ExpressionDeep);
}
}
}
@@ -707,7 +707,7 @@ namespace AibisDream.MiniGame.HuoShan
private void TriggerFailure()
{
_failCount++;
StorageSystem.Instance?.SetValue("$knobFailCount", (float)_failCount);
YarnVariableStorage.Instance?.SetValue("$knobFailCount", (float)_failCount);
_hasFailed = true;
Debug.Log($"[KnobController] Failure #{_failCount} at {_currentPower:F1}%, waiting for snapback...");
@@ -1,9 +1,7 @@
using UnityEngine;
using UnityEngine.Events;
using Cinemachine;
using AibisDream;
using AibisDream.FixSystem;
using AibisDream.Framework;
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
@@ -26,15 +24,6 @@ namespace AibisDream.MiniGame.Language
[Tooltip("销售系统主视图对象(会自动查找名为'SaleView'或'SellView'的子对象)")]
[SerializeField] private GameObject saleView;
[HideInInspector] public BubbleSlotGroup bubbleSlotGroup;
[Header("相机配置")]
[Tooltip("主销售相机")]
[SerializeField] private CameraEnum mainCamera = CameraEnum.SaleCamera;
[Tooltip("深度销售相机(可选)")]
[SerializeField] private CameraEnum deepCamera = CameraEnum.SaleDeepCamera;
[Header("销售模块深入 - Crank 与 Slider 协调")]
[Tooltip("发条控制器(不填则从 saleView 下查找)")]
[SerializeField] private CrankController crankController;
@@ -90,7 +79,6 @@ namespace AibisDream.MiniGame.Language
[Tooltip("盖子淡出时长")]
[SerializeField] private float coverFadeDuration = 0.5f;
private readonly List<CameraEnum> registeredCameras = new();
private bool _isProcessorDeepMode;
private Vector3 _saleViewOriginalPos;
private Vector3 _saleViewOriginalScale;
@@ -139,26 +127,9 @@ namespace AibisDream.MiniGame.Language
sliderController?.SetControlActive(true);
}
private void Awake()
{
// 对话气泡槽组(SaleSystem 下名为 "Bubble Slots" 的 GameObject
var bubbleSlots = transform.Find("Bubble Slots");
if (bubbleSlots != null)
{
bubbleSlotGroup = bubbleSlots.GetComponent<BubbleSlotGroup>();
if (bubbleSlotGroup == null)
Debug.LogWarning("[SalesManager] 找到 'Bubble Slots' 但无 BubbleSlotGroup 组件", this);
}
else
{
Debug.LogWarning("[SalesManager] 未找到名为 'Bubble Slots' 的子对象,对话气泡将无法显示", this);
}
}
private void Start()
{
RegisterToSystem();
RegisterCameras();
FindSaleView();
FindSaleViewEnterAnimation();
FindCrankController();
@@ -171,7 +142,6 @@ namespace AibisDream.MiniGame.Language
private void OnDestroy()
{
_deepModeSequence?.Kill();
UnregisterCameras();
UnsubscribeFromCrankSlider();
}
@@ -307,7 +277,7 @@ namespace AibisDream.MiniGame.Language
if (_currentTurnIndex > totalTurns) return;
// 滑片开始转动前,先触发 ChipModule 解释当前步骤(Stage3 等配合)
StorageSystem.Instance?.SetValue("$salesTurnIndex", (float)_currentTurnIndex);
YarnVariableStorage.Instance?.SetValue("$salesTurnIndex", (float)_currentTurnIndex);
if (!string.IsNullOrEmpty(crankSlideStartNode))
DialogController.Instance?.StartDialogNode(crankSlideStartNode);
@@ -327,7 +297,7 @@ namespace AibisDream.MiniGame.Language
{
if (string.IsNullOrEmpty(crankCompleteNode)) return;
StorageSystem.Instance?.SetValue("$salesTurnIndex", (float)_currentTurnIndex);
YarnVariableStorage.Instance?.SetValue("$salesTurnIndex", (float)_currentTurnIndex);
DialogController.Instance?.StartDialogNode(crankCompleteNode);
}
@@ -347,59 +317,6 @@ namespace AibisDream.MiniGame.Language
FixSystemCenter.SystemDic.Register(this);
}
/// <summary>
/// 注册所有相机到CameraKit
/// </summary>
private void RegisterCameras()
{
if (CameraKit.Instance == null)
{
Debug.LogError("[SalesManager] CameraKit.Instance 为 null,无法注册相机!");
return;
}
// 获取所有相机
var virtualCams = GetComponentsInChildren<ICinemachineCamera>();
if (virtualCams.Length == 0)
{
Debug.LogWarning("[SalesManager] 未找到任何 ICinemachineCamera 组件!");
return;
}
// 注册主相机
if (virtualCams.Length > 0)
{
CameraKit.Instance.RegisterCamera(mainCamera, virtualCams[0]);
registeredCameras.Add(mainCamera);
Debug.Log($"[SalesManager] 成功注册主相机: {mainCamera}");
}
// 注册深度相机(如果有)
if (virtualCams.Length > 1)
{
CameraKit.Instance.RegisterCamera(deepCamera, virtualCams[1]);
registeredCameras.Add(deepCamera);
Debug.Log($"[SalesManager] 成功注册深度相机: {deepCamera}");
}
}
/// <summary>
/// 从CameraKit注销所有相机
/// </summary>
private void UnregisterCameras()
{
if (CameraKit.Instance == null) return;
foreach (var cameraType in registeredCameras)
{
CameraKit.Instance.UnRegisterCamera(cameraType);
Debug.Log($"[SalesManager] 已注销相机: {cameraType}");
}
registeredCameras.Clear();
}
/// <summary>
/// 打开销售系统界面
/// </summary>
@@ -90,7 +90,7 @@ public class LineInteraction : MonoBehaviour
Destroy(gameObject);
// 调用对话控制器
DialogController.Instance.StartDialogNode("CutLine");
DialogController.Instance.StartDialogNode("OnCutLineComplete");
}
public void ActivateInteraction()
+42 -48
View File
@@ -7,9 +7,9 @@ using Yarn.Unity;
using System.Linq;
using UnityEngine.UI;
using AibisDream.FixSystem;
using Cinemachine;
using AibisDream.Framework;
using AibisDream.Kit;
using AibisDream.SaveSystem;
using UnityEngine.ResourceManagement.AsyncOperations;
using Sequence = DG.Tweening.Sequence;
@@ -19,7 +19,6 @@ namespace AibisDream
{
private const string MemorySpritePath = "Sprite/Memory/{0}.png";
public BubbleSlotGroup bubbleSlotGroup;
[SerializeField] private List<EyeTarget> targets = new(); // 保存目标列表
public List<EyeTarget> Targets => targets; // 提供目标列表的公共访问
private EyeTarget currentTarget; // 当前选中的目标
@@ -29,7 +28,20 @@ namespace AibisDream
public GameObject eyeView;
public ViewCameraManager cameraManager;
private IEyeStateEffect eyeStateEffect;
private EyeData _eyeData = new();
private EyeColorState _currentState = EyeColorState.CannotImagineColor;
/// <summary>获取当前 Eye 颜色阶段。</summary>
public EyeColorState GetCurrentColorState() => _currentState;
public enum EyeColorState
{
CanImagineColor,
CannotImagineColor,
EyeDisorder,
CanSeeColor,
HaveChip
}
private Tween hsvShiftTween;
private MouseFollowAndZoom mouseFollowAndZoom;
private RenderTexture runtimeEyeRenderTexture;
@@ -168,26 +180,29 @@ namespace AibisDream
public void Init()
{
FixSystemCenter.SystemDic.Register(this);
var virtualCam = transform.Find("Eye Camera").GetComponent<ICinemachineCamera>();
var virtualCamDeep = transform.Find("Eye DeepCamera").GetComponent<ICinemachineCamera>();
CameraKit.Instance.RegisterCamera(CameraEnum.Eye, virtualCam);
CameraKit.Instance.RegisterCamera(CameraEnum.EyeDeep, virtualCamDeep);
StorageSystem.Instance.RegisterData(_eyeData);
// 添加加载逻辑
_eyeData.LoadEvent += LoadData;
// 确保 ViewCamera 默认不激活
if (cameraManager != null)
{
cameraManager.gameObject.SetActive(false);
}
EnsureMouseFollowAndZoom();
}
private void LoadData(EyeData eyedata)
public EyeSnapshotDto CaptureEyeSnapshot()
{
SetEyeColorState(eyedata.currentState);
return new EyeSnapshotDto { eyeColorState = _currentState.ToString() };
}
public void ApplyEyeSnapshot(EyeSnapshotDto dto)
{
if (dto == null || string.IsNullOrEmpty(dto.eyeColorState))
{
return;
}
if (Enum.TryParse(dto.eyeColorState, true, out EyeColorState state))
{
SetEyeColorState(state);
}
}
public void OnEnter()
@@ -267,7 +282,7 @@ namespace AibisDream
private void Start()
{
SetEyeColorState(_eyeData.currentState);
SetEyeColorState(_currentState);
EnsureMouseFollowAndZoom();
}
@@ -304,7 +319,7 @@ namespace AibisDream
yield break;
}
StorageSystem.Instance.SetValue("$currentEyeTarget", target.name);
YarnVariableStorage.Instance.SetValue("$currentEyeTarget", target.name);
StartCoroutine(EyeEffectFadeOut());
cameraManager.MoveCameraTo(target.targetTransform ? target.targetTransform : target.transform, 1f);
@@ -401,7 +416,7 @@ namespace AibisDream
public void SetEyeColorStateForYarn(string state)
{
// Try to parse the string as an EyeColorState
if (!Enum.TryParse(state, true, out EyeData.EyeColorState eyeState))
if (!Enum.TryParse(state, true, out EyeColorState eyeState))
{
throw new ArgumentException("Invalid eye state: " + state, nameof(state));
}
@@ -410,24 +425,24 @@ namespace AibisDream
SetEyeColorState(eyeState);
}
private void SetEyeColorState(EyeData.EyeColorState state)
private void SetEyeColorState(EyeColorState state)
{
_eyeData.currentState = state;
_currentState = state;
switch (state)
{
case EyeData.EyeColorState.CanImagineColor:
case EyeColorState.CanImagineColor:
eyeStateEffect = new CanImagineColorEffect();
break;
case EyeData.EyeColorState.CannotImagineColor:
case EyeColorState.CannotImagineColor:
eyeStateEffect = new CannotImagineColorEffect();
break;
case EyeData.EyeColorState.EyeDisorder:
case EyeColorState.EyeDisorder:
eyeStateEffect = new EyeDisorderEffect();
break;
case EyeData.EyeColorState.CanSeeColor:
case EyeColorState.CanSeeColor:
eyeStateEffect = new CanSeeColorEffect();
break;
case EyeData.EyeColorState.HaveChip:
case EyeColorState.HaveChip:
eyeStateEffect = new HaveChip();
break;
default:
@@ -607,7 +622,7 @@ namespace AibisDream
[YarnCommand("EyeGetColor")]
public void GetColor()
{
SetEyeColorState(EyeData.EyeColorState.CanImagineColor);
SetEyeColorState(EyeColorState.CanImagineColor);
// 对目标列表中的每个目标执行 ColorBack 方法
foreach (EyeTarget target in targets)
{
@@ -617,25 +632,4 @@ namespace AibisDream
// 切换到 CanImagineColorEffect 状态
}
}
public class EyeData : IData
{
public enum EyeColorState
{
CanImagineColor,
CannotImagineColor,
EyeDisorder,
CanSeeColor,
HaveChip
}
public EyeColorState currentState = EyeColorState.CannotImagineColor;
public event Action<EyeData> LoadEvent;
public IEnumerator Load()
{
LoadEvent?.Invoke(this);
yield break;
}
}
}
@@ -3,8 +3,6 @@ using System.Collections.Generic;
using UnityEngine;
using AibisDream;
using AibisDream.FixSystem;
using AibisDream.Framework;
using Cinemachine;
using UnityEngine.Rendering;
using Yarn.Unity;
using DG.Tweening;
@@ -46,8 +44,6 @@ public class UFSystem : MonoBehaviour
private void Awake()
{
FixSystemCenter.SystemDic.Register(this);
var virtualCam = transform.Find("UF Camera").GetComponent<ICinemachineCamera>();
CameraKit.Instance.RegisterCamera(CameraEnum.UF, virtualCam);
eyeSystem = FindObjectOfType<EyeSystem>();
}
+10 -11
View File
@@ -1,6 +1,7 @@
using System.Collections;
using AibisDream;
using AibisDream.FixSystem;
using AibisDream.Framework;
using UnityEngine;
namespace AibisDream.MiniGame.Tarot
@@ -16,9 +17,7 @@ namespace AibisDream.MiniGame.Tarot
[SerializeField] private Sprite backSprite;
[Header("Dialogue Bubbles")]
[Tooltip("塔罗流程中使用的气泡槽位;不指定则进入塔罗时不切换气泡。")]
[SerializeField] private BubbleSlotGroup tarotBubbleSlotGroup;
[Tooltip("本场景普通对话的气泡槽位,用于退出塔罗时恢复;不填则尝试 OutsideCenter / FixPanelSystem。")]
[Tooltip("退出塔罗时恢复的气泡槽位;不填则尝试 Outside / BodyModule。")]
[SerializeField] private BubbleSlotGroup restoreBubbleSlotGroup;
private TarotDeck _deck;
@@ -164,7 +163,7 @@ namespace AibisDream.MiniGame.Tarot
{
_isCardSelected = true;
_selectedIndex = card.Index;
StorageSystem.Instance?.SetValue("$tarotSelectedIndex", (float)card.Index);
YarnVariableStorage.Instance?.SetValue("$tarotSelectedIndex", (float)card.Index);
}
private void OnDestroy()
@@ -176,21 +175,21 @@ namespace AibisDream.MiniGame.Tarot
private void TryEnterTarotBubbleMode()
{
if (tarotBubbleSlotGroup == null)
if (!BubbleSlotKit.Instance.TryGet(BubbleSlotEnum.Tarot, out _))
return;
var snapshot = CollectRestoreSnapshot();
if (snapshot.bubbleSlots == null || snapshot.bubbleSlots.Length == 0)
{
Debug.LogWarning(
"[TarotManager] 已配置塔罗气泡但无法取得有效的恢复快照(请指定 restoreBubbleSlotGroup 或保证 OutsideCenter/FixPanelSystem 气泡可用),跳过气泡切换。",
"[TarotManager] 已配置 Tarot 气泡但无法取得有效的恢复快照(请指定 restoreBubbleSlotGroup 或保证 Outside/BodyModule 气泡已注册),跳过气泡切换。",
this);
return;
}
_savedBubbleData = snapshot;
_bubbleSwapActive = true;
DialogUIManager.Instance.LoadBubbles(tarotBubbleSlotGroup.CollectData());
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Tarot);
}
private void RestoreTarotBubbleMode()
@@ -206,11 +205,11 @@ namespace AibisDream.MiniGame.Tarot
if (restoreBubbleSlotGroup != null)
return restoreBubbleSlotGroup.CollectData();
if (OutsideCenter.Instance != null && OutsideCenter.Instance.bubbleSlotGroup != null)
return OutsideCenter.Instance.bubbleSlotGroup.CollectData();
if (BubbleSlotKit.Instance.TryGet(BubbleSlotEnum.Outside, out _))
return BubbleSlotKit.Instance.CollectData(BubbleSlotEnum.Outside);
if (FixPanelSystem.Instance != null && FixPanelSystem.Instance.BubbleSlotGroup != null)
return FixPanelSystem.Instance.BubbleSlotGroup.CollectData();
if (BubbleSlotKit.Instance.TryGet(BubbleSlotEnum.BodyModule, out _))
return BubbleSlotKit.Instance.CollectData(BubbleSlotEnum.BodyModule);
return default;
}
@@ -1,6 +1,7 @@
fileFormatVersion: 2
guid: 5a170c0b6c933c540a86be691fa6745c
PrefabImporter:
guid: 460a9e992cac6da46b7f32b0bb5c5f4c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 520cc163c88e4c00895fe201004b4384
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using UnityEngine;
namespace AibisDream.SaveSystem.CloudSave
{
/// <summary>
/// 云存档协调器:负责选择当前平台的 backend,并在槽位写入/读取后触发同步钩子。
/// <para>
/// P2 不实现真实云同步逻辑,仅保留扩展点。
/// </para>
/// </summary>
public static class CloudSaveManager
{
private static readonly List<ICloudSaveBackend> Backends = new();
/// <summary>注册一个云存档后端。应在游戏启动时调用一次。</summary>
public static void RegisterBackend(ICloudSaveBackend backend)
{
if (backend == null)
{
return;
}
Backends.Add(backend);
}
/// <summary>清空已注册后端(主要用于测试)。</summary>
public static void ClearBackends()
{
Backends.Clear();
}
/// <summary>槽位写入后调用:触发所有可用后端的异步上传。</summary>
public static void OnSlotWritten(int slotIndex)
{
_ = UploadSlotAsync(slotIndex);
}
/// <summary>槽位读取前调用:可触发下载(P2 默认不自动下载,避免覆盖本地进度)。</summary>
public static void OnSlotRestored(int slotIndex)
{
// P2 占位:未来可在此触发冲突检测与下载。
}
/// <summary>手动触发指定槽位的云同步上传。</summary>
public static async Task UploadSlotAsync(int slotIndex)
{
var filePaths = CollectSlotFilePaths(slotIndex);
if (filePaths.Count == 0)
{
return;
}
foreach (var backend in Backends)
{
if (!backend.IsAvailable)
{
continue;
}
try
{
await backend.UploadSlotAsync(slotIndex, filePaths.ToArray());
Debug.Log($"[CloudSaveManager] {backend.PlatformName} 上传槽位 {slotIndex} 完成。");
}
catch (Exception ex)
{
Debug.LogWarning($"[CloudSaveManager] {backend.PlatformName} 上传槽位 {slotIndex} 失败: {ex.Message}");
}
}
}
/// <summary>收集槽位下需要同步的文件路径。</summary>
private static List<string> CollectSlotFilePaths(int slotIndex)
{
var paths = new List<string>();
var snapshotPath = SlotDirectory.GetSnapshotPath(slotIndex);
if (File.Exists(snapshotPath))
{
paths.Add(snapshotPath);
}
var metaPath = SlotDirectory.GetMetaPath(slotIndex);
if (File.Exists(metaPath))
{
paths.Add(metaPath);
}
var thumbnailPath = SlotDirectory.GetThumbnailPath(slotIndex);
if (File.Exists(thumbnailPath))
{
paths.Add(thumbnailPath);
}
return paths;
}
}
}

Some files were not shown because too many files have changed in this diff Show More