Files
tianshu-engine/ROADMAP.md

598 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 交互式电影游戏引擎 — Roadmap
## 技术栈
- **框架**: Vue 3 (Composition API + `<script setup>`)
- **构建**: Vite
- **状态管理**: Pinia
- **可视化编辑器**: Vue Flow
- **存储**: IndexedDB (Dexie.js)
- **语言**: TypeScript
- **视频**: 原生 `<video>` + A/B 双缓冲
## 项目结构
```
moviegame/
├── engine/ # 框架无关的核心引擎(纯 TS
│ ├── core/
│ │ ├── Engine.ts # 主循环,驱动各子系统
│ │ ├── SceneManager.ts # 剧情节点图遍历
│ │ ├── VideoManager.ts # A/B 双缓冲视频播放
│ │ └── StateManager.ts # 全局状态、条件求值
│ ├── systems/
│ │ ├── ChoiceSystem.ts # 选择 UI + 倒计时
│ │ ├── QTESystem.ts # QTE 触发、判定、超时
│ │ └── SaveSystem.ts # IndexedDB 存取
│ └── types.ts # 场景数据类型定义
├── src/ # Vue 应用(播放器端)
│ ├── components/
│ │ ├── GamePlayer.vue # 主播放器(挂载 video 元素)
│ │ ├── ChoicePanel.vue # 选项面板
│ │ ├── QTEOverlay.vue # QTE 遮罩
│ │ ├── SaveLoadMenu.vue # 存档界面
│ │ └── Subtitles.vue # 字幕显示
│ ├── composables/
│ │ ├── useGameEngine.ts # 引擎实例 + 生命周期
│ │ ├── useVideoPlayer.ts # 视频状态响应式封装
│ │ └── useGameState.ts # 状态响应式封装
│ ├── stores/
│ │ └── gameStore.ts # 游戏全局状态Pinia
│ ├── App.vue
│ └── main.ts
├── editor/ # Vue Flow 可视化编辑器(独立入口)
│ ├── components/
│ │ ├── SceneGraph.vue
│ │ ├── NodeEditor.vue
│ │ └── PreviewPanel.vue
│ ├── composables/
│ │ └── useGraphEditor.ts
│ └── App.vue
├── public/
│ ├── videos/ # 视频资源
│ └── scenes/
│ └── demo.json # 示例剧情数据
├── vite.config.ts
├── tsconfig.json
├── package.json
└── index.html
```
## 场景数据格式
```typescript
// engine/types.ts
interface SceneNode {
id: string;
videoUrl: string;
subtitleUrl?: string;
choices?: Choice[];
qte?: QTEDefinition;
nextScene?: string;
onEnter?: Effect[];
}
interface Choice {
text: string;
targetScene: string;
conditions?: Condition[];
effects?: Effect[];
timeLimit?: number; // 限时选择0=不限时
}
interface Condition {
variable: string;
op: '>' | '<' | '>=' | '<=' | '==' | '!=' | 'hasFlag';
value: number | string | boolean;
}
interface Effect {
type: 'set' | 'add' | 'toggleFlag' | 'triggerEvent';
target: string;
value?: number | string | boolean;
}
interface QTEDefinition {
triggerTime: number;
prompt: string;
keys: string[];
timeLimit: number;
successScene: string;
failScene: string;
effects?: {
success: Effect[];
fail: Effect[];
};
}
interface GameData {
scenes: Record<string, SceneNode>;
startScene: string;
variables: Record<string, number>;
}
interface SaveData {
slot: number;
timestamp: number;
currentScene: string;
variables: Record<string, number>;
flags: string[];
history: ChoiceRecord[];
thumbnail?: string;
}
```
## 实现路线
### P0 MVP — 最小可玩原型3-5 天)✅ 已完成 2026-06-07
目标:能播放一段视频 → 弹出选项 → 跳到下一段视频
- [x] 项目脚手架Vite + Vue3 + TypeScript + Pinia
- [x] `engine/core/Engine.ts` — 主循环骨架(加载场景 → 播放 → 等选择 → 切换)
- [x] `engine/core/SceneManager.ts` — 加载 JSON按 ID 查找场景节点
- [x] `engine/core/VideoManager.ts` — 单 video 元素播放,监听 ended 事件
- [x] `engine/core/StateManager.ts` — 变量存取、条件求值、效果执行
- [x] `engine/types.ts` — 类型定义
- [x] `src/components/GamePlayer.vue` — 挂载 video控制播放
- [x] `src/components/ChoicePanel.vue` — 渲染选择按钮,触发引擎切换
- [x] `public/scenes/demo.json` — 编写一段简单剧情7 个场景节点)
- [x] 验证:从 demo.json 加载场景,能走通 开始→选择→分支播放→结束 流程
### P1 核心 — 无缝切换 + 条件分支 + 存档1-2 周)✅ 已完成 2026-06-07
- [x] `engine/core/VideoManager.ts` 升级 — A/B 双缓冲预加载候选视频CSS 交叉淡化
- [x] `engine/core/SceneManager.ts` 升级 — 支持条件分支(根据 variables/flags 过滤选项)
- [x] `engine/systems/SaveSystem.ts` — Dexie.js IndexedDB 存取,多槽位
- [x] `engine/systems/ChoiceSystem.ts` — 限时选择倒计时,超时默认选择
- [x] `src/components/SaveLoadMenu.vue` — 存档/读档 UI
- [x] `src/stores/gameStore.ts` — Pinia 全局状态管理(含计时器、存档列表)
- [x] `src/composables/useGameEngine.ts` — 桥接层(双 video、存档、计时器
- [x] `src/components/GamePlayer.vue` — 双 video 元素 + 交叉淡化 CSS
- [x] `src/components/ChoicePanel.vue` — 倒计时进度条 + 计时文字
- [x] `src/App.vue` — 整合 SaveLoadMenu、双 video、计时器
- [x] 验证:条件分支走通,存档读档正常,视频切换交叉淡化
### P2 进阶 — QTE + 字幕 + 多存档槽1 周)✅ 已完成 2026-06-07
- [x] `engine/systems/QTESystem.ts` — QTE 触发、键盘监听(支持多键匹配)、超时判定
- [x] `src/components/QTEOverlay.vue` — SVG 倒计时环 + 按键提示 + 成功/失败动画
- [x] `src/components/Subtitles.vue` — WebVTT 解析 + 字幕同步渲染
- [x] `engine/core/Engine.ts` — 集成 QTEtimeupdate 检测 + 条件跳转 + 效果应用)
- [x] 多存档槽位 + 存档缩略图canvas 截图当前视频帧320x180 JPEG
- [x] `engine/core/VideoManager.ts` — 新增 `getActiveVideoElement()` 供截图
- [x] `engine/systems/SaveSystem.ts` — DB 版本升级 v2支持 thumbnail 字段)
- [x] `src/components/SaveLoadMenu.vue` — 存档缩略图预览
- [x] 完整事件总线sceneChange, choiceRequest, choiceTimer, choiceTimeout, videoEnd, qteTrigger, qteTimer, qteResult, gameEnd
- [x] 验证QTE 正常触发与判定ArrowLeft/ArrowRight/A/D 躲石块),字幕同步,存档缩略图正常
### P3 编辑器 — 可视化剧情编辑2-3 周)✅ 已完成 2026-06-07
- [x] 编辑器入口:独立 `editor/index.html` + `editor/main.ts`Vite 多入口构建)
- [x] `editor/components/SceneGraph.vue` — Vue Flow 节点图(场景节点 + 分支/默认/QTE 连线)
- [x] `editor/components/NodeEditor.vue` — 右侧面板(视频/字幕路径、nextScene、选项增删改、QTE 参数编辑)
- [x] `editor/components/PreviewPanel.vue` — 嵌入播放器实时预览选中场景视频
- [x] `editor/composables/useGraphEditor.ts` — 图数据与 JSON 双向同步
- [x] JSON 导出/导入(文件下载 + 文件选择)
- [x] 工具栏:新建场景、导入 JSON、导出 JSON、加载示例、起始场景选择
- [x] `vite.config.ts` — 多页面构建main + editor
- [x] 验证:编辑器能产出合法 JSON引擎能正确加载并运行
### P4 视频/图片热点 — 点击画面区域触发分支 ✅ 已完成 2026-06-08
目标在视频或图片上定义可点击热区Hotspot玩家点击画面不同位置触发不同分支。
热区既可覆盖在静态图片上(调查/解谜场景),也可覆盖在播放中的视频上(根据时间轴淡入淡出)。
**视频热点 vs 图片热点(架构统一,差异仅两点):**
| | 图片热点 | 视频热点 |
|------|----------|----------|
| 底层内容 | `<img>` 元素 | `<video>` 元素(已经在播) |
| 热点出现时机 | 始终可见 | 按时间轴出现/消失(`showAt`/`hideAt` |
**场景数据设计:**
```json
{
"id": "investigation",
"type": "video",
"videoUrl": "/videos/investigation.mp4",
"subtitleUrl": "/subtitles/investigation.vtt",
"hotspots": [
{
"id": "hs_desk",
"label": "查看书桌",
"targetScene": "desk_detail",
"x": 0.15, "y": 0.30, "width": 0.25, "height": 0.35,
"showAt": 2.0,
"hideAt": 8.0,
"conditions": [{ "variable": "investigation", "op": ">=", "value": 1 }],
"effects": [{ "type": "setFlag", "target": "checked_desk" }]
},
{
"id": "hs_window",
"label": "靠近窗户",
"targetScene": "window_look",
"x": 0.70, "y": 0.10, "width": 0.20, "height": 0.40,
"showAt": 5.0,
"hideAt": 10.0
},
{
"id": "hs_door",
"label": "离开房间",
"targetScene": "leave_room",
"x": 0.30, "y": 0.60, "width": 0.40, "height": 0.30,
"timeLimit": 15
}
],
"choices": [
{ "text": "放弃调查", "targetScene": "give_up" }
]
}
```
**字段约定:**
- `x/y/width/height` — 热区坐标,使用**相对比例**0~1自适应屏幕尺寸
- `showAt`/`hideAt` — 视频热点的时间轴(秒),未设置时热区始终可见(兼容图片场景和始终可见的视频热点)
- `hotspots` 支持 `conditions`(条件显隐)、`effects`(点击后效果)、`timeLimit`(限时热区)
- 热点场景仍可同时附带底部 `choices`(如"放弃调查"按钮)
- `type` 字段区分 `"video"`(默认)和 `"image"`(静态图,此时 `imageUrl` 替代 `videoUrl`
**实现清单:**
- [x] `engine/types.ts``SceneNode.type` 字段、`Hotspot` 接口(含 `showAt`/`hideAt`
- [x] `src/components/HotspotLayer.vue` — 通用热区覆盖层叠加在视频或图片之上render 热区矩形 + hover 高亮 + label 浮动提示
- [x] `engine/core/Engine.ts` — 视频模式下监听 timeupdate按时显隐热区点击热区触发分支跳转
- [ ] `editor/components/NodeEditor.vue` — 场景类型切换(视频/图片)+ 热区列表编辑 + 时间轴参数showAt/hideAt
- [x] `public/images/` — 示例图片目录
- [x] `public/scenes/demo.json` — 新增图片热点场景 `investigation_site` + 视频热点场景 `corridor`
- [x] 验证:图片热区点击触发、视频热区按时出现/消失、条件过滤、hover 高亮
### P5 选择等待循环 — 单文件内时间锚点无缝循环 ✅ 已完成 2026-06-08
目标:视频结束后画面不暂停,而是在同一文件内通过 `loopStart`/`loopEnd` 时间锚点实现无切换循环,
选项浮在循环画面之上。和《底特律变人》《The Dark Pictures Anthology》等商业游戏的做法一致。
**为什么不用单独 loop 文件做 cross-fade**
- 任何文件切换(硬切或淡入)都会产生可感知的割裂感
- 商业游戏的循环效果本质上就是同一帧内 `video.currentTime = loopStart`,完全透明
- 同一文件内 seek 只在下一个 timeupdate 触发(~250ms但对 ≥2 秒的循环区间来说误差 <5%肉眼无感
**做法对比:**
| 方案 | 体验 |
|------|------|
| ~~主视频 → cross-fade → loopVideo 文件~~ | 两画面重叠 300ms**割裂** |
| ~~主视频 → 硬切 → loopVideo 文件~~ | 一帧黑/依赖浏览器 |
| **同一文件内 `loopStart/loopEnd` seek** | **完全无缝AAA 游戏标准** |
**场景数据设计:**
```json
{
"id": "tense_moment",
"videoUrl": "/videos/tense_full.mp4",
"loopStart": 8.0,
"loopEnd": 10.0,
"choices": [
{ "text": "冒险救人", "targetScene": "rescue" },
{ "text": "悄悄离开", "targetScene": "flee" }
]
}
```
素材制作流程导演剪辑时将主剧情段 + 循环段合成为一个 MP4 文件比如
```
0:00 ~ 8:00 正常剧情演绎
8:00 ~ 10:00 循环片段(角色呼吸、张望)── 循环起点 loopStart=8, loopEnd=10
```
**工作流程:**
```
┌─ 主视频正常播放0s → loopStart
├─ time >= loopStart → 标记"已到达循环区间"
│ └─ timeupdate 持续检测time >= loopEnd → video.currentTime = loopStart无任何过渡
│ └─ 无限循环中...
│ │
│ ├─ 用户选择 ──→ break loop → switchTo(nextScene)
│ │
│ ├─ 视频 ended → 自动触发循环区间
│ │
│ └─ 选项面板在循环开始时浮出
└─ 无 loopStart 的场景 → 保持现有行为(结束后暂停,等待选择)
```
**关键设计细节:**
- 检测循环完全依赖 `timeupdate` 事件无需 `requestAnimationFrame` 或额外定时器——浏览器 ~250ms timeupdate 间隔对 2s 的循环段误差可忽略
- 循环中 A/B 预加载仍然工作inactive slot 加载第一个候选目标场景用户选择后 cross-fade 过去
- `loopStart` 既是触发选项显示的时机视频到达此处时 emit `choiceRequest`也是循环起点
- `loopEnd` 为循环终点到达后 seek `loopStart`
- 若只设 `loopStart` 不设 `loopEnd`则循环区间为 `loopStart → 视频结尾`
**实现清单:**
- [x] `engine/types.ts` `SceneNode.loopStart?: number`, `loopEnd?: number`
- [x] `engine/core/VideoManager.ts` 新增 `seekTo(time)` 方法
- [x] `engine/core/Engine.ts` `checkLoop(time)` timeupdate 中检测循环区间`onVideoEnd` 循环活跃时跳过`goToScene` 重置 `loopActive`
- [x] `public/scenes/demo.json` `stay` 场景添加 loopStart=3, loopEnd=6, 循环中显示选项
- [x] `public/videos/stay_loop.mp4` 6s 测试视频0-3s 蓝色正文 + 3-6s 绿色循环段
- [x] 验证正文播放完毕 进入循环 选项浮现 画面无缝来回 选择后跳转
### P6 独立背景音乐 — 画面循环不打断 BGM待实现
目标将背景音乐从视频中剥离由独立 AudioManager 驱动视频循环/切换时 BGM 保持连贯播放
不同场景之间用交叉淡化衔接这也是底特律变人》《The Dark Pictures Anthology等商业游戏的标配
**架构变更:**
```
Engine
├── VideoManagerA/B 双缓冲,只管画面和视频内音轨)
│ └── loopVideo 循环时只管画面 → BGM 不受影响
└── AudioManager独立 AudioContext/HTMLAudioElement只管 BGM
└── 按场景播放/交叉淡化/循环,独立于视频生命周期
```
**场景数据设计:**
```json
{
"id": "tense_moment",
"videoUrl": "/videos/tense_no_bgm.mp4",
"loopVideoUrl": "/videos/tense_loop_no_bgm.mp4",
"bgmUrl": "/audio/tense_bgm.mp3",
"bgmVolume": 0.8,
"bgmCrossFade": 2.0,
"bgmContinue": true,
"videoMuted": true,
"choices": [...]
}
```
**工作流对比:**
| 阶段 | 改进前 | 改进后 |
|------|--------|--------|
| 主视频播放 | 视频自带 BGM | AudioManager 独立播 BGM视频 muted |
| 进入选择等待 | 循环视频 BGM 断掉重来 | 视频循环 BGM 继续连贯 |
| 选择后切场景 | 下一段视频 BGM 硬切 | BGM 交叉淡化过渡bgmCrossFade |
| 关音乐/调音量 | 无法控制 | AudioManager 提供音量/静音接口 |
| 同一 BGM 多场景 | 每个 mp4 嵌一份浪费 | 共用同一文件节省带宽 |
**实现清单:**
- [ ] `engine/systems/AudioSystem.ts` 新增BGM 播放交叉淡化GainNode ramp)、循环音量/静音
- [ ] `engine/core/Engine.ts` 集成 AudioSystem场景切换时对比 bgmUrl同源继续不同crossFade
- [ ] `engine/types.ts` `SceneNode` `bgmUrl``bgmVolume``bgmCrossFade``bgmContinue``videoMuted`
- [ ] `engine/core/VideoManager.ts` 支持 `muted` 参数视频音轨静音但在 BGM 模式下禁用
- [ ] `public/scenes/demo.json` 示例场景拆分视频+音频
- [ ] `editor/components/NodeEditor.vue` BGM 字段编辑面板
- [ ] 验证BGM 跨越视频循环连续播放场景切换交叉淡化音量控制生效
### P7 全屏模式 — 沉浸式浏览器体验(待实现)
目标一键进入全屏播放模式播放中自动隐藏 UI选项/菜单等浮层除外提供 F11 级别的沉浸感
**实现清单:**
- [ ] `src/composables/useFullscreen.ts` Fullscreen API 封装`element.requestFullscreen()`ESC 退出监听
- [ ] `src/App.vue` 全屏按钮工具栏或浮动按钮播放中隐藏非关键 UI 元素
- [ ] 指针空闲隐藏鼠标 3 秒不动自动隐藏光标`pointer-events: none` 过渡
- [ ] 全屏下选项面板仍可见z-index 高于视频层
- [ ] 验证F11 等效全屏ESC 退出播放中 UI 自动隐藏/鼠标移动恢复
### P8 章节选择 — 通关后可跳转(待实现)
目标玩家通关后可从中途任意章节起始点重新开始每个章节记录一个入口场景 ID展示章节缩略图和标题
**数据结构设计:**
```json
{
"chapters": [
{ "id": "ch1", "label": "第一章:醒来", "startScene": "intro", "thumbnail": "/images/ch1.jpg" },
{ "id": "ch2", "label": "第二章:抉择", "startScene": "left_door", "thumbnail": "/images/ch2.jpg" }
]
}
```
**实现清单:**
- [ ] `engine/types.ts` `GameData.chapters` 字段`ChapterInfo` 接口
- [ ] `engine/systems/SaveSystem.ts` 章节解锁状态持久化已通关章节记录
- [ ] `src/components/ChapterSelect.vue` 章节选择界面缩略图网格 + 标题 + 锁定/解锁状态
- [ ] `engine/core/Engine.ts` `startChapter(chapterId)` 方法跳转到指定章节起始场景
- [ ] `src/App.vue` 主菜单新游戏 / 章节选择 / 继续
- [ ] 验证通关后章节解锁从章节入口跳转正确未解锁章节灰显
### P9 跳过已看 + 倍速播放(待实现)
目标玩家重玩时可以跳过已看过的场景或加速播放2x/4x避免重复等待
**实现清单:**
- [ ] `engine/systems/SkipSystem.ts` 记录已观看的场景 ID 列表持久化到 IndexedDB
- [ ] `src/components/SkipIndicator.vue` "按住 Space 跳过" 进度环指示器
- [ ] `engine/core/VideoManager.ts` `setPlaybackRate(rate)` 方法原生 `video.playbackRate`
- [ ] `src/App.vue` 跳过按钮/快捷键Space 长按 进度环走满 触发 skip倍速切换按钮1x/2x/4x
- [ ] `engine/core/Engine.ts` 跳过逻辑跳过 直接触发 `onVideoEnd` 流程倍速不触发跳过
- [ ] `public/scenes/demo.json` 添加 `skippable: true/false` 字段关键场景禁止跳过
- [ ] 验证已看场景可跳过未看不可跳过倍速切换正常关键场景不可跳过
### P10 键盘/手柄导航(待实现)
目标支持纯键盘或手柄操作整个游戏流程选择选项确认QTE菜单适配"躺沙发"体验
**实现清单:**
- [ ] `engine/systems/InputSystem.ts` 统一输入抽象层键盘方向键/WASD+ 手柄Gamepad API+ 鼠标
- [ ] 选项高亮导航:↑↓ 移动焦点Enter/Space 确认有视觉高亮指示器
- [ ] QTE 键位整合到 InputSystem目前 QTE 直接监听 `keydown`
- [ ] `src/components/ChoicePanel.vue` 键盘焦点环样式`focus-visible`
- [ ] `src/App.vue` 菜单键Esc 打开/关闭菜单
- [ ] 验证纯键盘完成一次完整流程开始选择QTE存档读档结束)、手柄连接时自动切换
### P11 多语言字幕(待实现)
目标支持多语言字幕切换UI 文本国际化同一个场景可有中//日等多个字幕文件
**数据结构设计:**
```json
{
"id": "intro",
"videoUrl": "/videos/intro.mp4",
"subtitles": {
"zh": "/subtitles/intro_zh.vtt",
"en": "/subtitles/intro_en.vtt",
"ja": "/subtitles/intro_ja.vtt"
},
"choices": [...]
}
```
**实现清单:**
- [ ] `engine/types.ts` `SceneNode.subtitles` 改为 `Record<lang, url>``Choice.text` 支持多语言 key
- [ ] `engine/systems/I18nSystem.ts` 语言切换当前语言持久化到 localStorage
- [ ] `src/components/Subtitles.vue` 监听语言切换动态加载对应 VTT
- [ ] `src/components/ChoicePanel.vue` 选项文字支持 i18n 映射
- [ ] `src/components/LanguageSwitch.vue` 语言选择下拉菜单顶部或菜单中
- [ ] `public/scenes/demo.json` 中英双语字幕示例
- [ ] 验证语言切换后字幕/UI 即时更新刷新保持语言偏好
### P12 场景过渡特效(待实现)
目标场景切换不再只有 opacity 交叉淡化支持更多电影转场语言淡黑fade to black)、白闪fade to white)、模糊blur)、滑动等
**实现清单:**
- [ ] `src/components/TransitionOverlay.vue` 全屏遮罩层CSS transition 控制颜色和透明度
- [ ] `engine/core/Engine.ts` `SceneNode.transition?: TransitionDef`在执行视频切换前先播放过渡
- [ ] `engine/types.ts` `TransitionDef { type: 'fadeBlack' | 'fadeWhite' | 'dissolve' | 'cut' | 'blur'; duration: number }`
- [ ] `engine/core/VideoManager.ts` 过渡时序过渡遮罩覆盖 切换视频 过渡遮罩消失
- [ ] 验证淡黑转场白闪转场不同 duration 参数生效
### P13 重玩驱动系统 — 成就 + 统计 + 结局画廊 + 关键选择提示(待实现)
目标增加重玩驱动力告知玩家"还有未发现的内容"激发探索欲望涵盖 Telltale "[某人]会记住"、
Quantic Dream 的结局流程图Steam 式成就系统通关后的全局统计
**子功能清单:**
**13a. 关键选择提示:**
- [ ] `Choice.prompt?: string` 选择前弹出的提示文字 "[某人] 会记住你的选择"
- [ ] `src/components/ChoicePanel.vue` 选项确认前展示提示动画短暂放大/光效
**13b. 成就系统:**
- [ ] `engine/systems/AchievementSystem.ts` 成就定义`{ id, title, description, icon, condition(scene, state) }`
- [ ] 成就触发检测场景切换时匹配条件解锁持久化到 IndexedDB
- [ ] `src/components/AchievementToast.vue` 解锁时弹出提示底部 toast + 音效
- [ ] `src/components/AchievementPanel.vue` 成就列表页面全部/已解锁/未解锁
**13c. 结局画廊:**
- [ ] `GameData.endings` 结局定义`{ id, label, thumbnail, unlockCondition }`
- [ ] `src/components/EndingGallery.vue` 结局缩略图网格已解锁显示画面未解锁显示?剪影 + 提示
- [ ] 通关后自动跳转到结局画廊或主菜单入口
**13d. 全局统计:**
- [ ] `engine/systems/StatsSystem.ts` 计数线索收集数总死亡/失败次数各结局达成率
- [ ] `src/components/StatsPanel.vue` 通关后展示统计面板"73% 的玩家选择了救人"、"你收集了 3/5 个线索"
- [ ] 全局百分比需要后端聚合数据`/api/stats`纯本地可展示个人计数
**13e. 章节剧情回顾:**
- [ ] `src/components/ChapterRecap.vue` 章节结束后显示本分支的简化流程图已走过的路径高亮
- [ ] 基于 SceneGraph 现有的 Vue Flow 节点图实现只读模式
**验证:** 成就解锁弹出 toast结局画廊正确显示解锁状态章节结束后显示回顾统计面板数据正确
### P14 沉浸感提升 — SFX 音效 + 对话轮 UI + 氛围特效 + 动态字幕(待实现)
目标提升视听表现力"视频播放器"进化到"电影级游戏"。
**子功能清单:**
**14a. 独立音效层 (SFX)**
- [ ] `engine/systems/AudioSystem.ts` 升级 支持多轨道BGM 轨道 + SFX 轨道可叠加各自独立音量
- [ ] `SceneNode.sfx` 场景音效定义`[{ url, trigger: 'enter' | 'choice_a' | 'qte_success', volume }]`
- [ ] `engine/core/Engine.ts` 事件触发对应 SFX 播放进入场景做选择QTE 结果
**14b. 对话轮 UI**
- [ ] `src/components/DialogueWheel.vue` 替代/补充底部选项列表圆环布局选项按弧度分布中心显示倒计时
- [ ] 键盘/手柄方向键对应轮盘位置(↑=上方选项,↓=下方选项
- [ ] `Choice.wheelPosition?: number` 手动指定在轮盘中的角度0~360
**14c. 氛围特效:**
- [ ] `src/components/AmbientEffects.vue` 全屏 CSS filter 叠加`brightness`/`contrast`/`saturate` 渐变
- [ ] 画面震动`CSS transform: translate(random)` 关键帧动画
- [ ] `SceneNode.effects?: AmbientEffect[]` 场景级特效声明震动暗角色彩偏移闪光
**14d. 动态字幕:**
- [ ] `engine/types.ts` `SubtitleCue.speaker?: string`, `SubtitleCue.color?: string`, `SubtitleCue.position?: 'left' | 'center' | 'right'`
- [ ] `src/components/Subtitles.vue` 升级说话人标签前缀 + 颜色区分 + 根据 position 调整水平偏移
**验证:** SFX 不打断 BGM对话轮方向键选择震动特效不卡顿说话人颜色区分清晰
### P15 平台化 — 云存档 + 可访问性 + 自适应码率(待实现)
目标面向分发和用户多样性的补全功能
**子功能清单:**
**15a. 云存档(需后端):**
- [ ] 存档上传/下载 API 接口设计REST: `PUT /saves/:slot`, `GET /saves`
- [ ] `engine/systems/SaveSystem.ts` 升级 save/load 支持 `remote: boolean` 参数
- [ ] 登录态管理可选不强制登录本地存档为主云存档为可选项
**15b. 可访问性设置:**
- [ ] 字幕样式自定义字体大小背景透明度颜色//绿
- [ ] 高对比度模式`filter: contrast(1.3)` 全局应用
- [ ] QTE 辅助模式放宽时间限制简化按键如单键替代组合键
- [ ] 色盲模式选项颜色不依赖红/绿区分
- [ ] `src/components/AccessibilitySettings.vue` 可访问性设置面板
**15c. 自适应码率:**
- [ ] `engine/core/VideoManager.ts` 支持 HLS`.m3u8` DASH`.mpd`流媒体源
- [ ] `SceneNode.videoUrl` 支持多码率`{ auto: '/videos/hls/scene.m3u8', hd: '/videos/scene_1080p.mp4' }`
- [ ] 网络质量检测`navigator.connection` API自动降级
**15d. 其余补充:**
- [ ] 主菜单界面新游戏 / 继续 / 章节选择 / 成就 / 结局画廊 / 设置
- [ ] 设置持久化到 localStorage语言音量可访问性偏好
- [ ] 导演评论音轨开关`SceneNode.commentaryUrl?: string`作为可选第二音轨
**验证:** 云存档跨设备同步字幕大小调整生效码率自适应切换无卡顿
## 依赖清单
```json
{
"dependencies": {
"vue": "^3.4",
"pinia": "^2.1",
"@vue-flow/core": "^1.x",
"@vue-flow/background": "^1.x",
"@vue-flow/controls": "^1.x",
"dexie": "^4.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0",
"typescript": "^5.3",
"vite": "^5.0",
"vue-tsc": "^2.0"
}
}
```
## 关键架构决策记录
1. **引擎与 UI 分离**: `engine/` 下纯 TS import VueUI 层通过 composables 桥接
2. **A/B 双缓冲**: 两个 `<video>` 元素轮换一个播放时另一个预加载候选视频
3. **JSON 驱动**: 所有剧情数据放在 JSON 编辑器本质是 JSON 的可视化读写工具
4. **IndexedDB 存档**: localStorage 容量大可存储截屏缩略图