Files
tianshu-engine/ROADMAP.md

24 KiB
Raw Blame History

交互式电影游戏引擎 — 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

场景数据格式

// 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

目标:能播放一段视频 → 弹出选项 → 跳到下一段视频

  • 项目脚手架Vite + Vue3 + TypeScript + Pinia
  • engine/core/Engine.ts — 主循环骨架(加载场景 → 播放 → 等选择 → 切换)
  • engine/core/SceneManager.ts — 加载 JSON按 ID 查找场景节点
  • engine/core/VideoManager.ts — 单 video 元素播放,监听 ended 事件
  • engine/core/StateManager.ts — 变量存取、条件求值、效果执行
  • engine/types.ts — 类型定义
  • src/components/GamePlayer.vue — 挂载 video控制播放
  • src/components/ChoicePanel.vue — 渲染选择按钮,触发引擎切换
  • public/scenes/demo.json — 编写一段简单剧情7 个场景节点)
  • 验证:从 demo.json 加载场景,能走通 开始→选择→分支播放→结束 流程

P1 核心 — 无缝切换 + 条件分支 + 存档1-2 周) 已完成 2026-06-07

  • engine/core/VideoManager.ts 升级 — A/B 双缓冲预加载候选视频CSS 交叉淡化
  • engine/core/SceneManager.ts 升级 — 支持条件分支(根据 variables/flags 过滤选项)
  • engine/systems/SaveSystem.ts — Dexie.js IndexedDB 存取,多槽位
  • engine/systems/ChoiceSystem.ts — 限时选择倒计时,超时默认选择
  • src/components/SaveLoadMenu.vue — 存档/读档 UI
  • src/stores/gameStore.ts — Pinia 全局状态管理(含计时器、存档列表)
  • src/composables/useGameEngine.ts — 桥接层(双 video、存档、计时器
  • src/components/GamePlayer.vue — 双 video 元素 + 交叉淡化 CSS
  • src/components/ChoicePanel.vue — 倒计时进度条 + 计时文字
  • src/App.vue — 整合 SaveLoadMenu、双 video、计时器
  • 验证:条件分支走通,存档读档正常,视频切换交叉淡化

P2 进阶 — QTE + 字幕 + 多存档槽1 周) 已完成 2026-06-07

  • engine/systems/QTESystem.ts — QTE 触发、键盘监听(支持多键匹配)、超时判定
  • src/components/QTEOverlay.vue — SVG 倒计时环 + 按键提示 + 成功/失败动画
  • src/components/Subtitles.vue — WebVTT 解析 + 字幕同步渲染
  • engine/core/Engine.ts — 集成 QTEtimeupdate 检测 + 条件跳转 + 效果应用)
  • 多存档槽位 + 存档缩略图canvas 截图当前视频帧320x180 JPEG
  • engine/core/VideoManager.ts — 新增 getActiveVideoElement() 供截图
  • engine/systems/SaveSystem.ts — DB 版本升级 v2支持 thumbnail 字段)
  • src/components/SaveLoadMenu.vue — 存档缩略图预览
  • 完整事件总线sceneChange, choiceRequest, choiceTimer, choiceTimeout, videoEnd, qteTrigger, qteTimer, qteResult, gameEnd
  • 验证QTE 正常触发与判定ArrowLeft/ArrowRight/A/D 躲石块),字幕同步,存档缩略图正常

P3 编辑器 — 可视化剧情编辑2-3 周) 已完成 2026-06-07

  • 编辑器入口:独立 editor/index.html + editor/main.tsVite 多入口构建)
  • editor/components/SceneGraph.vue — Vue Flow 节点图(场景节点 + 分支/默认/QTE 连线)
  • editor/components/NodeEditor.vue — 右侧面板(视频/字幕路径、nextScene、选项增删改、QTE 参数编辑)
  • editor/components/PreviewPanel.vue — 嵌入播放器实时预览选中场景视频
  • editor/composables/useGraphEditor.ts — 图数据与 JSON 双向同步
  • JSON 导出/导入(文件下载 + 文件选择)
  • 工具栏:新建场景、导入 JSON、导出 JSON、加载示例、起始场景选择
  • vite.config.ts — 多页面构建main + editor
  • 验证:编辑器能产出合法 JSON引擎能正确加载并运行

P4 图片热点 — 点击图片触发分支(待实现)

目标:场景支持静态图片替代视频,图上定义可点击热区,点击不同位置触发不同分支

场景数据设计:

{
  "id": "crime_scene",
  "type": "image",
  "imageUrl": "/images/crime_scene.jpg",
  "subtitleUrl": "/subtitles/crime_scene.vtt",
  "hotspots": [
    {
      "id": "hs_desk",
      "x": 0.15, "y": 0.25, "width": 0.18, "height": 0.12,
      "label": "查看书桌",
      "targetScene": "desk_detail",
      "conditions": [{ "variable": "investigation", "op": ">=", "value": 1 }],
      "effects": [{ "type": "setFlag", "target": "checked_desk" }]
    },
    {
      "id": "hs_window",
      "x": 0.72, "y": 0.08, "width": 0.15, "height": 0.28,
      "label": "查看窗户",
      "targetScene": "window_detail"
    },
    {
      "id": "hs_body",
      "x": 0.40, "y": 0.40, "width": 0.10, "height": 0.22,
      "label": "检查尸体",
      "targetScene": "body_detail",
      "timeLimit": 10
    }
  ],
  "choices": [
    { "text": "离开现场", "targetScene": "leave_room" }
  ]
}

字段约定:

  • type: "image" — 声明为图片场景(默认/不存在则为视频场景)
  • x/y/width/height — 热区坐标,使用相对比例0~1自适应屏幕尺寸
  • 图片场景仍可同时附带底部 choices(如"离开"选项)
  • hotspots 支持 conditions(条件显隐)、effects(点击后效果)、timeLimit(限时)

实现清单:

  • engine/types.tsSceneNode.type 字段、Hotspot 接口
  • engine/core/Engine.ts — 支持 type: "image" 场景,挂载图片 + 热区
  • src/components/ImageScene.vue — 渲染图片 + 热区矩形 + hover 高亮 + label 浮动提示
  • editor/components/NodeEditor.vue — 场景类型切换(视频/图片)+ 热区可视化编辑(拖放矩形)
  • public/scenes/demo.json — 新增图片场景示例 + 示例图片
  • 验证:图片加载、热区点击触发分支、条件过滤、限时热区

P5 选择等待循环 — 视频结束循环播放(待实现)

目标视频结束后不暂停画面而是无缝切换到一段循环视频Idle Loop选项浮在循环画面之上 消除"画面静止等选择"的割裂感提升电影感。这是《底特律变人》《Late Shift》等商业游戏的标配做法。

场景数据设计:

{
  "id": "tense_moment",
  "videoUrl": "/videos/tense.mp4",
  "loopVideoUrl": "/videos/tense_loop.mp4",
  "choices": [
    { "text": "冒险救人", "targetScene": "rescue" },
    { "text": "悄悄离开", "targetScene": "flee" }
  ]
}

工作流程:

主视频 A槽播放 ──→ ended ──→ B槽切换 loopVideo (loop=true) + 淡入 ──→ 显示选项
                                                                          │
                                                          用户点击选择 ────┘
                                                                          │
                                                     停止循环 → 切换到目标场景

实现清单(基于现有 A/B 双缓冲架构,改动量小):

  • engine/types.tsSceneNode.loopVideoUrl?: string
  • engine/core/VideoManager.tsplayLoop(url) 新方法(复用 inactive 槽,loop=true + 交叉淡化)
  • engine/core/Engine.ts — 视频 ended 时检测 loopVideoUrl,有则调用 playLoop 而非直接触发选项;用户选择后停止循环
  • src/components/ChoicePanel.vue — 无改动(选项已浮在视频层之上)
  • public/scenes/demo.json — 示例场景添加循环视频
  • 验证:主视频结束 → 无缝循环 → 选项中循环播放 → 选择后停止 → 下一场景

P6 独立背景音乐 — 画面循环不打断 BGM待实现

目标:将背景音乐从视频中剥离,由独立 AudioManager 驱动。视频循环/切换时 BGM 保持连贯播放, 不同场景之间用交叉淡化衔接。这也是《底特律变人》《The Dark Pictures Anthology》等商业游戏的标配。

架构变更:

Engine
├── VideoManagerA/B 双缓冲,只管画面和视频内音轨)
│    └── loopVideo 循环时只管画面 → BGM 不受影响
└── AudioManager独立 AudioContext/HTMLAudioElement只管 BGM
     └── 按场景播放/交叉淡化/循环,独立于视频生命周期

场景数据设计:

{
  "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.tsSceneNodebgmUrlbgmVolumebgmCrossFadebgmContinuevideoMuted
  • 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展示章节缩略图和标题。

数据结构设计:

{
  "chapters": [
    { "id": "ch1", "label": "第一章:醒来", "startScene": "intro", "thumbnail": "/images/ch1.jpg" },
    { "id": "ch2", "label": "第二章:抉择", "startScene": "left_door", "thumbnail": "/images/ch2.jpg" }
  ]
}

实现清单:

  • engine/types.tsGameData.chapters 字段、ChapterInfo 接口
  • engine/systems/SaveSystem.ts — 章节解锁状态持久化(已通关章节记录)
  • src/components/ChapterSelect.vue — 章节选择界面:缩略图网格 + 标题 + 锁定/解锁状态
  • engine/core/Engine.tsstartChapter(chapterId) 方法,跳转到指定章节起始场景
  • src/App.vue — 主菜单:新游戏 / 章节选择 / 继续
  • 验证:通关后章节解锁、从章节入口跳转正确、未解锁章节灰显

P9 跳过已看 + 倍速播放(待实现)

目标玩家重玩时可以跳过已看过的场景或加速播放2x/4x避免重复等待。

实现清单:

  • engine/systems/SkipSystem.ts — 记录已观看的场景 ID 列表(持久化到 IndexedDB
  • src/components/SkipIndicator.vue — "按住 Space 跳过" 进度环指示器
  • engine/core/VideoManager.tssetPlaybackRate(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 文本国际化,同一个场景可有中/英/日等多个字幕文件。

数据结构设计:

{
  "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.tsSceneNode.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.tsSceneNode.transition?: TransitionDef,在执行视频切换前先播放过渡
  • engine/types.tsTransitionDef { 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.tsSubtitleCue.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,作为可选第二音轨)

验证: 云存档跨设备同步、字幕大小调整生效、码率自适应切换无卡顿

依赖清单

{
  "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 Vue。UI 层通过 composables 桥接。
  2. A/B 双缓冲: 两个 <video> 元素轮换,一个播放时另一个预加载候选视频。
  3. JSON 驱动: 所有剧情数据放在 JSON 中,编辑器本质是 JSON 的可视化读写工具。
  4. IndexedDB 存档: 比 localStorage 容量大,可存储截屏缩略图。