319 lines
12 KiB
Markdown
319 lines
12 KiB
Markdown
# 交互式电影游戏引擎 — Roadmap
|
||
|
||
## 技术栈
|
||
|
||
- **框架**: Vue 3 (Composition API + `<script setup>`)
|
||
- **构建**: Vite
|
||
- **状态管理**: Pinia
|
||
- **可视化编辑器**: Vue Flow
|
||
- **存储**: IndexedDB (Dexie.js)
|
||
- **BGM**: Web Audio API
|
||
- **语言**: TypeScript
|
||
- **视频**: 原生 `<video>` + A/B 双缓冲
|
||
|
||
## 实现路线
|
||
|
||
### P18 视频加载失败恢复(待实现)
|
||
|
||
目标:视频加载失败时显示错误画面 + 重试/跳过按钮,不再静默黑屏。
|
||
|
||
- [ ] `engine/core/VideoManager.ts` — `play`/`switchTo` 增加错误回调 + 超时检测(5 秒)+ 重试(最多 3 次,指数退避 1s/2s/4s)
|
||
- [ ] `src/components/VideoErrorOverlay.vue` — 错误画面:图标 + 提示 + [重试] [跳过] 按钮
|
||
- [ ] `src/stores/gameStore.ts` — `videoError` 状态
|
||
- [ ] `src/App.vue` — 整合 VideoErrorOverlay
|
||
- [ ] 验证:断网播放 → 错误画面 → 重试恢复 → 跳过下一场景
|
||
|
||
### P24 多画质视频 — 本地 + CDN 流双模式 ✅ 已完成 2026-06-10
|
||
|
||
目标:桌面版用本地 `videoUrl`,Web 版用 CDN `streamingUrl`(HLS 流)。
|
||
Web 版不打包视频文件,用户手动选择超清/高清/标清,系统提示各画质所需网速。
|
||
|
||
**设计决策:**
|
||
|
||
| 决策 | 做法 |
|
||
|------|------|
|
||
| **环境检测** | Electron `preload.js` 注入 `__ELECTRON__` → `VideoManager` 判断走本地还是 CDN |
|
||
| **Web 画质** | 用户从设置面板手动选择(超清/高清/标清),非带宽自适应。localStorage 持久化 |
|
||
| **Web 打包** | `pack:html` 跳过 `videos/` 目录,音频/图片/字幕保留 |
|
||
| **HLS 兼容** | Safari 原生播放 `.m3u8`;Chrome/Edge 按需动态 `import('hls.js')`(~100KB) |
|
||
|
||
**场景数据设计:**
|
||
|
||
```json
|
||
{
|
||
"id": "intro",
|
||
"videoUrl": "/videos/intro.mp4",
|
||
"streamingUrl": {
|
||
"超清 (1080P)": "https://cdn.example.com/hls/intro/1080p.m3u8",
|
||
"高清 (720P)": "https://cdn.example.com/hls/intro/720p.m3u8",
|
||
"标清 (480P)": "https://cdn.example.com/hls/intro/480p.m3u8"
|
||
}
|
||
}
|
||
```
|
||
|
||
**设置面板画质选项:**
|
||
|
||
| 选项 | 网速提示 |
|
||
|------|---------|
|
||
| 超清 (1080P) | 需要 2.5 Mbps |
|
||
| 高清 (720P) | 需要 2 Mbps |
|
||
| 标清 (480P) | 需要 0.8 Mbps |
|
||
|
||
**实现清单:**
|
||
|
||
- [x] `engine/types.ts` — `SceneNode.streamingUrl?: Record<string, string>`
|
||
- [x] `engine/core/VideoManager.ts` — `resolveVideoUrl(scene, quality)` + `streamingQuality` 属性
|
||
- [x] `engine/core/Engine.ts` — `goToScene` 用 `resolveVideoUrl` 替代直接 `scene.videoUrl`
|
||
- [x] `electron/preload.js` — `contextBridge.exposeInMainWorld('__ELECTRON__', true)`
|
||
- [x] `electron/main.js` — `webPreferences.preload` 加载 preload.js
|
||
- [x] `src/stores/gameStore.ts` — `preferredQuality` + localStorage 持久化
|
||
- [x] `src/components/AccessibilitySettings.vue` — Web 模式新增画质下拉(附网速提示)
|
||
- [x] `src/App.vue` — watch `preferredQuality` → sync 到 `engine.videoManager.streamingQuality`
|
||
- [x] `scripts/pack-html.cjs` — 跳过 `videos/` 目录
|
||
- [x] 验证:TypeScript + Vite build 通过
|
||
- [ ] 验证:Electron `window.__ELECTRON__` = true,使用本地 `videoUrl`
|
||
- [ ] 验证:浏览器 `window.__ELECTRON__` = undefined,设置面板显示画质下拉
|
||
- [ ] 验证:`pack:html` 产物不包含 `videos/` 目录
|
||
|
||
### P25 条件路由 — nextScene 支持条件数组 ✅ 已完成 2026-06-12
|
||
|
||
目标:`nextScene` 从单一场景 ID 扩展为条件路由数组。第一个满足条件的场景自动跳转,
|
||
否则 fallback 到末尾无条件的默认场景。不局限于 QTE,所有场景均可使用。
|
||
|
||
**场景数据设计:**
|
||
|
||
```json
|
||
{
|
||
"id": "combat_router",
|
||
"nextScene": [
|
||
{ "conditions": [{ "variable": "enemy_hp", "op": "<=", "value": 0 }], "targetScene": "victory" },
|
||
{ "conditions": [{ "variable": "player_hp", "op": "<=", "value": 0 }], "targetScene": "defeat" },
|
||
{ "targetScene": "combat" }
|
||
]
|
||
}
|
||
```
|
||
|
||
**引擎行为:**
|
||
|
||
```
|
||
onVideoEnd(scene)
|
||
├── nextScene 是 string?→ 现存逻辑不变
|
||
├── nextScene 是 Choice[]?
|
||
│ → 遍历数组,第一个满足 conditions 的 → 跳转到它的 targetScene
|
||
│ → 都不满足 → endGame()
|
||
└── 无 nextScene → 现有逻辑不变
|
||
```
|
||
|
||
**使用场景:**
|
||
|
||
```
|
||
QTE 成功 → effects: enemy_hp -= 25
|
||
→ successScene = "combat_router"
|
||
├── enemy_hp <= 0 → victory 场景
|
||
├── player_hp <= 0 → defeat 场景
|
||
└── 否则 → 回到 QTE 场景(循环)
|
||
```
|
||
|
||
**实现清单:**
|
||
|
||
- [x] `engine/types.ts` — `SceneNode.nextScene` 类型改为 `string | Choice[]`
|
||
- [x] `engine/core/Engine.ts` — `onVideoEnd` 中加数组判断,遍历 conditions 跳转
|
||
- [x] `engine/core/SceneManager.ts` — `getCandidateTargetIds` 支持数组 nextScene
|
||
- [x] `src/components/StoryGallery.vue` — BFS 遍历 + `buildPlayerTree` 支持数组 nextScene
|
||
- [x] `public/scenes/demo.json` — 新增 `combat_router` 条件路由示例
|
||
- [x] 验证:TypeScript + Vite build 通过
|
||
|
||
### P26 关键节点过滤 — StoryGallery 只展示剧情分叉点 ✅ 已完成 2026-06-12
|
||
|
||
目标:StoryGallery 不再展示所有场景,只展示剧情关键节点(章节起始、选择分支点、结局点)。
|
||
QTE 场景和过渡/路由场景被过滤,子节点上浮一级。
|
||
|
||
**三层判断:**
|
||
|
||
| 优先级 | 来源 | 说明 |
|
||
|:--:|------|------|
|
||
| 1 | `SceneNode.keyMoment` | 手动覆盖。`true`=强制展示,`false`=强制隐藏 |
|
||
| 2 | `endings[].sceneId` | 结局节点 |
|
||
| 3 | 自动判断 | 章节起点 / 有 `choices`(分支点)→ 关键节点。QTE 不算 |
|
||
|
||
**扁平化:** 非关键节点不渲染,子节点上浮到父节点层级,路径语义不变。
|
||
|
||
**实现清单:**
|
||
|
||
- [x] `engine/types.ts` — `SceneNode.keyMoment?: boolean`
|
||
- [x] `src/components/StoryGallery.vue` — `isKeyMoment()` 三层逻辑 + `collectKeyTargets()` 扁平化非关键节点
|
||
- [x] 验证:TypeScript + Vite build 通过
|
||
|
||
### P27 全局计时器 — 跨场景时间压力(待实现)
|
||
|
||
目标:跨场景倒计时,时间用尽强制跳转。独立的 `TimerSystem` 类 + 三个新 Effect 类型,
|
||
支持启动/停止/重置/加减时间。
|
||
|
||
**Effect 类型:**
|
||
|
||
| Effect type | 参数 | 说明 |
|
||
|-------------|------|------|
|
||
| `startTimer` | `duration`(秒), `expireScene` | 启动倒计时。如已存在则重置 |
|
||
| `stopTimer` | — | 暂停计时器 |
|
||
| `addTime` | `value`(秒) | 增加剩余时间(正数)或扣减(负数) |
|
||
|
||
**使用场景:**
|
||
|
||
```json
|
||
{
|
||
"id": "chapter_start",
|
||
"onEnter": [
|
||
{ "type": "startTimer", "duration": 3600, "value": 3600, "target": "timeout_ending" }
|
||
]
|
||
}
|
||
```
|
||
|
||
**TimerSystem 核心逻辑:**
|
||
|
||
```typescript
|
||
class TimerSystem {
|
||
private remaining: number = 0
|
||
private expireScene: string = ''
|
||
private intervalId: ReturnType<typeof setInterval> | null = null
|
||
private onExpire: ((sceneId: string) => void) | null = null
|
||
|
||
start(duration: number, expireScene: string) { ... }
|
||
stop() { ... }
|
||
addTime(seconds: number) { ... }
|
||
getRemaining(): number { ... }
|
||
}
|
||
```
|
||
|
||
setInterval 每秒递减,剩余 ≤0 时调用 `onExpire(expireScene)`。
|
||
|
||
**UI 显示:** PlaybackBar 右下角 `MM:SS` 格式,最后一分钟变红。
|
||
|
||
**实现清单:**
|
||
|
||
- [ ] `engine/systems/TimerSystem.ts` — **新建** — 计时器核心逻辑
|
||
- [ ] `engine/types.ts` — Effect 新增 `startTimer`/`stopTimer`/`addTime` 类型
|
||
- [ ] `engine/core/StateManager.ts` — `apply` 中处理新 Effect
|
||
- [ ] `engine/core/Engine.ts` — 集成 `TimerSystem`;`startChapter` 停止旧 Timer
|
||
- [ ] `engine/systems/SaveSystem.ts` — 存档/读档包含 Timer 状态
|
||
- [ ] `src/components/PlaybackBar.vue` — HUD 显示倒计时
|
||
|
||
### P28 随机路由 — 变量初始值/场景随机选择(待实现)
|
||
|
||
目标:`nextScene` / 变量初始值支持随机选择,每次玩法不同。
|
||
|
||
### P29 背包/装备系统 — 物品持有影响叙事(待实现)
|
||
|
||
目标:玩家可持有物品,物品影响 `conditions` 判断、选择可见性、场景解锁。
|
||
|
||
### P30 通关评分/反馈 — 结算面板展示统计(待实现)
|
||
|
||
目标:通关后展示玩家行为统计(线索数、成就数、结局数)。DeathPanel 升级为通用的 `ResultPanel`,死亡和通关统一走这里。
|
||
|
||
**数据设计(GameData 顶层):**
|
||
|
||
```json
|
||
{
|
||
"stats": [
|
||
{ "label": "线索发现", "variable": "investigation", "max": 5 },
|
||
{ "label": "QTE 成功次数", "variable": "qte_succeeded" },
|
||
{ "label": "达成结局数", "type": "endingsCount" }
|
||
]
|
||
}
|
||
```
|
||
|
||
| 字段 | 说明 |
|
||
|------|------|
|
||
| `label` | 统计项名称 |
|
||
| `variable` | 从 `variables` 读值 |
|
||
| `max` | 满分(可选),用于进度条 |
|
||
| `type: "endingsCount"` | 特殊统计 — `visitedSceneIds ∩ endings[].sceneId` 计数 |
|
||
|
||
**实现清单:**
|
||
|
||
- [ ] `engine/types.ts` — `GameData.stats?: StatDef[]`
|
||
- [ ] `src/components/DeathPanel.vue` → 升级为 `ResultPanel.vue`
|
||
- [ ] `src/App.vue` — `gameEnd` 触发后展示 ResultPanel
|
||
|
||
### P31 战斗 HUD + 结算面板 — RPG HUD 流派 ✅ 已完成 2026-06-12
|
||
|
||
目标:战斗场景中展示角色属性 HUD(头像 + HP/MP 条 + 数值),胜利后弹出结算面板。
|
||
走 RPG HUD 流派,非极简派。战败不做结算面板,直接走战败叙事。
|
||
|
||
**SceneNode 新增字段:**
|
||
|
||
```json
|
||
{
|
||
"id": "combat",
|
||
"videoUrl": "combat/combat.mp4",
|
||
"qte": { ... },
|
||
"battleHUD": [
|
||
{
|
||
"label": "你",
|
||
"portrait": "images/player.jpg",
|
||
"stats": [
|
||
{ "variable": "player_hp", "label": "HP", "max": 100 },
|
||
{ "variable": "player_mp", "label": "MP", "max": 50 },
|
||
{ "variable": "combo_score", "label": "连击" }
|
||
]
|
||
},
|
||
{
|
||
"label": "敌人",
|
||
"portrait": "images/enemy.jpg",
|
||
"stats": [
|
||
{ "variable": "enemy_hp", "label": "HP", "max": 100 }
|
||
]
|
||
}
|
||
],
|
||
"battleResult": {
|
||
"title": "战斗胜利!",
|
||
"stats": [
|
||
{ "label": "剩余生命", "variable": "player_hp" },
|
||
{ "label": "QTE 成功次数", "variable": "qte_succeeded" }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
**BattleHUD 字段说明:**
|
||
|
||
| 字段 | 说明 |
|
||
|------|------|
|
||
| `label` | 角色名称 |
|
||
| `portrait` | 角色头像路径 |
|
||
| `stats` | 属性数组。`variable`/`label`/`max`/`style`(`"bar"` 或 `"number"`,缺省时根据有无 `max` 自动判断) |
|
||
|
||
**布局:** 角色头像左侧,stats 竖排叠在头像右侧。多角色水平排列在屏幕一侧。
|
||
|
||
**组件:**
|
||
|
||
| 组件 | 说明 |
|
||
|------|------|
|
||
| `BattleHUD.vue` | 战斗场景中显示角色属性条,`variables` 实时响应 |
|
||
| `BattleResult.vue` | 胜利结算面板 — 标题 + stats + "继续"按钮 → 下一场景 |
|
||
|
||
**实现清单:**
|
||
|
||
- [x] `engine/types.ts` — `BattleHUDStat` / `BattleHUDEntry` / `BattleResultStat` / `BattleResultDef` 接口
|
||
- [x] `src/components/BattleHUD.vue` — **新建** — 角色头像 + stats 进度条/数值,i18n labelKey
|
||
- [x] `src/components/BattleResult.vue` — **新建** — 胜利结算面板 + titleKey + "继续"按钮
|
||
- [x] `src/stores/gameStore.ts` — `variable()` 读值 + `showBattleResult` 状态
|
||
- [x] `src/composables/useGameEngine.ts` — `sceneChange` 中检测 `scene.battleResult` 自动弹出
|
||
- [x] `src/App.vue` — 整合 BattleHUD + BattleResult
|
||
- [x] `src/locales/zh.json` + `en.json` — `continue` / `toMenu` i18n
|
||
- [x] `public/scenes/demo.json` — `right_door` 场景添加 `battleHUD` 示例
|
||
- [x] 验证:TypeScript + Vite build 通过
|
||
|
||
## 已完成
|
||
|
||
P0~P23 全部实现(除 P18)。详见 [CHANGELOG.md](CHANGELOG.md)。
|
||
|
||
## 相关文档
|
||
|
||
| 文档 | 说明 |
|
||
|------|------|
|
||
| [docs/SCENE_JSON_SPEC.md](docs/SCENE_JSON_SPEC.md) | 场景 JSON 完整字段参考手册 |
|
||
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | 关键架构决策记录 |
|
||
| [PRODUCTION.md](PRODUCTION.md) | 生产级交付检查清单 |
|
||
| [FUTURE.md](FUTURE.md) | 远期功能扩展笔记 |
|
||
| [CHANGELOG.md](CHANGELOG.md) | 功能更新日志 |
|