docs: reorganize docs into guide/ and electron/, add 6 new guide docs, update README

This commit is contained in:
2026-06-10 17:01:48 +08:00
parent 99f80147c7
commit 686b1b45ea
11 changed files with 950 additions and 75 deletions

138
docs/guide/BRANCHING.md Normal file
View File

@@ -0,0 +1,138 @@
# 分支叙事指南
## 基本分支
最简单的分支:一个场景 → 多个选项 → 不同目标场景。
```json
"scene_1": {
"id": "scene_1",
"videoUrl": "scene_1/video.mp4",
"choices": [
{ "text": "帮助他", "targetScene": "help" },
{ "text": "离开", "targetScene": "leave" }
]
}
```
## 条件分支
选项可以根据变量条件显示/隐藏:
```json
"choices": [
{
"text": "高信任路线",
"targetScene": "trust_path",
"conditions": [
{ "variable": "trust", "op": ">=", "value": 80 }
]
},
{ "text": "普通路线", "targetScene": "normal_path" }
]
```
`op` 支持:`>`, `<`, `>=`, `<=`, `==`, `!=`
## 变量与效果
全局变量在 JSON 顶层定义初始值:
```json
{
"variables": { "trust": 50, "courage": 0, "investigation": 0 }
}
```
选项选择后应用效果:
```json
{
"text": "与陌生人握手",
"targetScene": "trust_ending",
"effects": [
{ "type": "add", "target": "trust", "value": 30 }
]
}
```
效果类型:
- `"set"` — 设置变量为指定值
- `"add"` — 增加(负数=减少)
## 场景进入效果
进入场景时自动触发:
```json
"ending": {
"id": "ending",
"videoUrl": "ending/video.mp4",
"onEnter": [
{ "type": "set", "target": "completed_game", "value": 1 }
]
}
```
## 限时选择
```json
{
"text": "快速决定!",
"targetScene": "timeout_scene",
"timeLimit": 10
}
```
10 秒内不选,自动选这个选项。`timeLimit: 0` 或省略 = 不限时。
## 默认跳转
无选项或有选项但都不满足条件时,自动跳转:
```json
"scene": {
"id": "scene",
"videoUrl": "scene/video.mp4",
"choices": [], // 无选项时自动跳
"nextScene": "auto_next"
}
```
优先级choices > nextScene > 什么都没配(游戏结束)。
## 关键选择提示Prompt
重要选项可以配置前置金色标识 + 选后浮现提示:
```json
{
"text": "与陌生人握手",
"textKey": "left_door.choice.handshake",
"prompt": "陌生人会记住你的善意",
"promptKey": "left_door.prompt.handshake",
"targetScene": "trust_ending"
}
```
- 前置:选项按钮左侧显示金色竖线 + 淡金边框
- 后置:选择确认后,画面中央浮现 prompt 文字2 秒淡出
`promptKey` 支持 i18n配置方法见 [国际化指南](I18N.md)。
## 多国语言选项
选项文案支持中/英/日等多语言,使用 `textKey` 机制:
```json
{
"text": "继续前进",
"textKey": "qte_success.choice.continue",
"targetScene": "continue_ending"
}
```
- `text` 是回退值(翻译找不到时使用)
- `textKey` 指向 `public/locales/zh.json` 中的翻译
详细配置见 [国际化指南](I18N.md)。

159
docs/guide/I18N.md Normal file
View File

@@ -0,0 +1,159 @@
# 国际化指南i18n
## 双源体系
| 类别 | 位置 | 加载 | 维护者 |
|------|------|------|--------|
| **UI 文案**(按钮、菜单、设置) | `src/locales/{zh,en,ja}.json` | `import` 打包进 bundle | 引擎开发者 |
| **故事文案**(选项、提示、章节名) | `public/demo/locales/{zh,en,ja}.json` | `fetch()` 动态加载 | 故事创作者 |
**核心理念:** 游戏制作者只需编辑 `public/` 下的 JSON刷新页面即生效不需要重新打包。
## 数据层 i18n故事文案
### 架构
```json
// public/scenes/demo.json
{
"locales": { "path": "locales/", "languages": ["zh", "en", "ja"] },
"scenes": {
"intro": {
"choices": [
{ "text": "走向左边那扇发光的门", "textKey": "intro.choice.left_door", ... }
]
}
}
}
```
```json
// public/demo/locales/zh.json
{
"intro": {
"choice": {
"left_door": "走向左边那扇发光的门"
}
}
}
```
```json
// public/demo/locales/en.json
{
"intro": {
"choice": {
"left_door": "Walk toward the glowing door on the left"
}
}
}
```
### 所有支持 i18n 的数据字段
| 数据对象 | 字段 | key 字段 | 示例 key |
|---------|------|---------|----------|
| Choice | `text` | `textKey` | `"intro.choice.left_door"` |
| Choice | `prompt` | `promptKey` | `"left_door.prompt.handshake"` |
| QTE | `prompt` | `promptKey` | `"right_door.qte.dodge"` |
| Hotspot | `label` | `labelKey` | `"investigation_site.hotspot.desk"` |
| Chapter | `label` | `labelKey` | `"chapter.ch1"` |
| Ending | `label` | `labelKey` | `"ending.trust_end"` |
| Achievement | `title` | `titleKey` | `"achievement.qte_master.title"` |
| Achievement | `description` | `descKey` | `"achievement.qte_master.desc"` |
**规律:** 每个可国际化的数据字段都有对应的 `xxxKey` 版本。引擎先查 key找不到时回退原始字段值。
### 回退逻辑
```
t('intro.choice.left_door')
→ 查 public/locales/{lang}.json
→ 找到了 → 返回翻译文本
→ 找不到 → 返回 choice.text"走向左边那扇发光的门"
```
### Locale JSON 结构示例
```json
{
"intro": {
"choice": {
"left_door": "...",
"right_door": "...",
"search": "...",
"stay": "..."
}
},
"left_door": {
"choice": {
"handshake": "...",
"reject": "..."
},
"prompt": {
"handshake": "..."
}
},
"right_door": {
"qte": { "dodge": "..." }
},
"chapter": {
"ch1": "第一章:醒来",
"ch2": "第二章:调查"
},
"ending": {
"trust_end": "信任的伙伴",
"alone_end": "独行之路"
},
"achievement": {
"qte_master": {
"title": "反应达人",
"desc": "成功完成一次 QTE"
}
}
}
```
## 新增语言的方法
### 1. 注册到引擎
`src/locales/` 新增语言文件(复制 `en.json` 翻译):
```bash
cp src/locales/en.json src/locales/ko.json
# 编辑 src/locales/ko.json → 翻译 UI 文案
# 编辑 src/composables/useI18n.ts → 新增 `import ko from '@/locales/ko.json'` + 加入 `uiMessages`
```
### 2. 注册到故事数据
`public/demo/locales/` 新增语言文件:
```bash
cp public/demo/locales/en.json public/demo/locales/ko.json
# 编辑 → 翻译故事文案
```
`demo.json``locales.languages` 数组加 `"ko"`
```json
"locales": { "path": "locales/", "languages": ["zh", "en", "ja", "ko"] }
```
### 3. 刷新页面
语言切换按钮自动新增韩语选项,无需重新构建。
## 配置语言目录
所有 locale 文件放在 `assetBase` 指定的基础路径下。JSON 中配置:
```json
{
"assetBase": "my_story/",
"locales": { "path": "lang/", "languages": ["zh", "en"] }
}
```
则语言文件路径为 `my_story/lang/zh.json`

123
docs/guide/INTERACTIONS.md Normal file
View File

@@ -0,0 +1,123 @@
# 交互指南 — QTE / Hotspot / Loop
## QTE 快速反应事件
在视频播放到特定时间点时弹出按键提示,玩家在倒计时内按下指定按键。
```json
"qte": {
"triggerTime": 1.0,
"prompt": "躲避飞来的石块!",
"promptKey": "right_door.qte.dodge",
"keys": ["ArrowLeft", "ArrowRight", "a", "d"],
"timeLimit": 3.0,
"successScene": "qte_success",
"failScene": "qte_fail",
"effects": {
"success": [{ "type": "add", "target": "courage", "value": 15 }],
"fail": [{ "type": "add", "target": "trust", "value": -20 }]
}
}
```
| 字段 | 说明 |
|------|------|
| `triggerTime` | 触发时间(秒) |
| `prompt` / `promptKey` | 提示文字 / i18n key |
| `keys` | 有效按键(键盘 key 名,不区分大小写) |
| `timeLimit` | 倒计时(秒) |
| `successScene` / `failScene` | 成功/失败目标场景 |
| `effects` | 成功/失败分别触发效果 |
**注意:** QTE 是模态交互。视频播放到 QTE 触发时暂停场景流程QTE 期间视频结束事件被忽略。
**可访问性:** 玩家可在设置中开启"QTE 按键简化"(所有 QTE 统一为空格键)和"QTE 时限放宽 ×1.5"。
**禁止跳过:** QTE 场景建议设 `"skippable": false`,防止玩家跳过 QTE。
## 图片/视频热点Hotspot
在画面中划定可点击区域,玩家点击后触发分支。
### 图片热点
```json
"investigation_site": {
"type": "image",
"imageUrl": "investigation_site/scene.jpg",
"contentSize": { "w": 1280, "h": 720 },
"hotspots": [
{
"id": "hs_desk",
"label": "查看书桌",
"labelKey": "investigation_site.hotspot.desk",
"targetScene": "desk_detail",
"x": 154, "y": 144, "width": 230, "height": 101
}
]
}
```
### 视频热点(按时间显隐)
```json
"corridor": {
"videoUrl": "corridor/video.mp4",
"contentSize": { "w": 1280, "h": 720 },
"hotspots": [
{
"id": "hs_left",
"label": "走向左边通道",
"targetScene": "left_door",
"x": 26, "y": 216, "width": 384, "height": 324,
"showAt": 1.5
},
{
"id": "hs_right",
"label": "走向右边通道",
"targetScene": "alone_ending",
"x": 870, "y": 216, "width": 384, "height": 324,
"showAt": 5.0
}
]
}
```
| 字段 | 说明 |
|------|------|
| `showAt` | 视频播放到此秒数后才显示热点(可选) |
| `hideAt` | 视频播放到此秒数后隐藏热点(可选) |
| `conditions` | 条件满足时才显示(可选) |
### 坐标系统
Hotspot 坐标使用**绝对像素**,基于 `contentSize` 指定的基准分辨率:
- `x, y` — 左上角像素坐标
- `width, height` — 热点区域像素尺寸
引擎自动处理 `object-fit: contain` 的黑边偏移和屏幕缩放。
**制作建议:** 在 Photoshop 中测量坐标,直接写入 JSON。
## 循环等待Loop
视频播放到指定区间后自动循环,适合"等待玩家做决定"的桥段。
```json
"stay": {
"videoUrl": "stay/loop.mp4",
"loopStart": 3.0,
"loopEnd": 6.0,
"choices": [
{ "text": "站起来离开", "targetScene": "alone_ending" }
]
}
```
- 视频 0-3s 正常播放(正文段)
- 到 3s 时循环开始,选项面板同时弹出
- 视频在 3.0-6.0s 之间反复播放,直到玩家做出选择
- BGM 不受循环影响(独立音频轨道)
**视频制作技巧:** 正文段和循环段合成为一个文件。循环段首尾画面应自然衔接。

116
docs/guide/PUBLISHING.md Normal file
View File

@@ -0,0 +1,116 @@
# 打包发布指南
## 准备工作
```bash
npm install
npm run build # 先构建,自动校验 JSON 合法性
```
构建产物在 `dist/`,是纯静态文件,可直接部署到任意 HTTP 服务器。
## Web 版发布
### 一键打包
```bash
npm run pack:html
```
生成 `release/mygame.zip`,上传到任意平台:
| 平台 | 上传方式 |
|------|---------|
| **itch.io** | 直接上传 zip |
| **Netlify** | 拖拽 `dist/` 文件夹到 Drop |
| **GitHub Pages** | 推送 `dist/``gh-pages` 分支 |
| **自有服务器** | 上传 `dist/` 到任意静态文件服务Nginx, Apache, Caddy |
### 域名/CDN
如果把素材放到 CDN只需改一行
```json
{
"assetBase": "https://cdn.example.com/mygame/"
}
```
所有 `videoUrl: "scene_1/video.mp4"` 自动拼为 `https://cdn.example.com/mygame/scene_1/video.mp4`
## 桌面版发布
### macOS
```bash
npm run pack:mac
```
生成 `release/MyGame-darwin-arm64/`。将整个文件夹打包为 `.dmg` 或直接分发文件夹。用户双击 `MyGame.app` 即可运行。
### Windows
```bash
npm run pack:win
```
生成 `release/MyGame-win32-x64/`。运行 `MyGame.exe`
## 桌面版命令行参数
打包后的应用支持 `--scene` 参数指定剧情文件:
```bash
# macOS
./MyGame.app/Contents/MacOS/MyGame --scene=./scenes/my_story.json
# Windows
MyGame.exe --scene=./scenes/my_story.json
```
## 素材管理
### 本地开发
所有素材放在 `public/`Vite 自动 serve。
### 生产发布
- 视频/音频通常较大(几百 MB建议单独分发或放 CDN
- `.gitignore` 中已排除 `public/videos/`,视频不提交到 Git
- 打包脚本自动复制 `public/``dist/`,无需手动处理
### 目录规范
```
public/demo/ ← 示例数据
scenes/demo.json
locales/{zh,en,ja}.json
intro/video.mp4
shared/bgm.mp3
public/your_story/ ← 你的游戏(复制此结构)
scenes/main.json
locales/{zh,en}.json
scene_1/video.mp4
```
## 常见问题
### Q: 构建时提示 "JSON 不合法"
检查 JSON 文件是否有多余逗号(最后一项不能有逗号)。
### Q: 打包后视频不加载
检查 `assetBase` 配置,确保路径拼接正确。开发模式 `assetBase: ""`,发布到 CDN 时改为完整 URL。
### Q: 打包体积太大
- 视频是最大的文件建议单独制作低码率预览版2Mbps用于小体积分发
- 音频用 MP3 128kbps 即可
- 缩略图 JPG 质量 60% 足够
### Q: 如何制作安装包(.dmg / .exe 安装程序)
参考 `docs/electron/packaging-guide.md`

87
docs/guide/QUICK_START.md Normal file
View File

@@ -0,0 +1,87 @@
# 快速开始 — 5 分钟制作你的第一个交互式电影游戏
## 1. 准备视频
用任何方式制作三个短视频MP4 H.264
```
my_story/
scene_1/video.mp4 ← 开场视频
scene_2/video.mp4 ← 分支 A 视频
ending/video.mp4 ← 结局视频
```
推荐参数1280×72030fps2-5Mbps。
## 2. 写剧情 JSON
创建 `public/scenes/my_story.json`
```json
{
"assetBase": "my_story/",
"startScene": "scene_1",
"variables": {},
"scenes": {
"scene_1": {
"id": "scene_1",
"videoUrl": "scene_1/video.mp4",
"choices": [
{ "text": "选择 A", "targetScene": "scene_2" },
{ "text": "选择 B", "targetScene": "ending" }
]
},
"scene_2": {
"id": "scene_2",
"videoUrl": "scene_2/video.mp4",
"nextScene": "ending"
},
"ending": {
"id": "ending",
"videoUrl": "ending/video.mp4",
"choices": []
}
}
}
```
## 3. 启动
```bash
npm install
npm run dev
```
打开 `http://localhost:5173/?scene=my_story.json`
## 4. 发生了什么
- 场景 1 播放 → 视频结束后弹出两个选项
- 选 A → 场景 2 → 自动跳转到 ending
- 选 B → 直接到 ending
- ending 无选项 → 游戏结束,返回主菜单
## 下一步
- 想加限定条件的选择?→ [分支叙事指南](BRANCHING.md)
- 想加 QTE 或热点?→ [交互指南](INTERACTIONS.md)
- 想加背景音乐或字幕?→ [场景 JSON 参考](SCENE_JSON_SPEC.md)
- 想支持多语言?→ [国际化指南](I18N.md)
- 想打包发布?→ [发布指南](PUBLISHING.md)
## 素材组织建议
```
my_story/
scene_1/video.mp4 ← 每个场景一个文件夹
scene_2/video.mp4
ending/video.mp4
shared/ ← 跨场景共享素材
bgm.mp3
thumb.jpg
locales/ ← 多语言翻译文件(可选)
zh.json
en.json
```
JSON 中所有路径都是相对于素材根目录的相对路径,配合 `assetBase` 前缀使用。

View File

@@ -0,0 +1,255 @@
# Scene JSON 完整字段参考
## 顶层结构
```json
{
"assetBase": "",
"locales": { "path": "locales/", "languages": ["zh", "en"] },
"startScene": "intro",
"variables": { "trust": 50, "courage": 0 },
"introVideo": "__intro__/logo.mp4",
"menuVideo": "__intro__/menu_bg.mp4",
"scenes": { ... },
"chapters": [ ... ],
"achievements": [ ... ],
"endings": [ ... ]
}
```
| 字段 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `assetBase` | string | 否 | 所有资源路径前缀,默认 `""`。设 `"demo/"``videoUrl: "intro/video.mp4"` 自动拼成 `demo/intro/video.mp4`。改 CDN 只需改这一行 |
| `locales` | object | 否 | 多语言配置。`path` 为 locale 文件目录(相对于 `assetBase``languages` 为支持的语言列表 |
| `startScene` | string | 是 | 开始场景的 ID |
| `variables` | object | 否 | 全局变量初始值 |
| `introVideo` | string | 否 | 开场视频路径 |
| `menuVideo` | string | 否 | 主菜单背景视频路径(自动循环播放) |
| `scenes` | object | 是 | 所有场景的集合key 为场景 ID |
| `chapters` | array | 否 | 章节列表 |
| `achievements` | array | 否 | 成就列表 |
| `endings` | array | 否 | 结局列表 |
---
## SceneNode
```typescript
interface SceneNode {
id: string
type?: 'video' | 'image'
videoUrl: string
imageUrl?: string
contentSize?: { w: number; h: number }
subtitleUrl?: string
subtitles?: Record<string, string>
choices?: Choice[]
hotspots?: Hotspot[]
qte?: QTEDefinition
nextScene?: string
onEnter?: Effect[]
loopStart?: number
loopEnd?: number
bgmUrl?: string
bgmVolume?: number
bgmCrossFade?: number
bgmDuckLevel?: number
bgmDuckFade?: number
videoMuted?: boolean
skippable?: boolean
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 场景唯一标识 |
| `type` | string | `"video"` (默认) 或 `"image"`。image 类型展示图片+热点,不播放视频 |
| `videoUrl` | string | 视频文件路径。image 场景可为空 |
| `imageUrl` | string | 图片场景的图片路径 |
| `contentSize` | object | 内容基准分辨率 `{ w: 1280, h: 720 }`Hotspot 坐标基于此计算。用于 object-fit: contain 的偏移补偿 |
| `subtitleUrl` | string | 回退字幕路径。优先使用 `subtitles` |
| `subtitles` | object | 多语言字幕 `{ "zh": "...", "en": "..." }`。语言切换时自动选择 |
| `choices` | Choice[] | 选项列表 |
| `hotspots` | Hotspot[] | 可点击热点区域 |
| `qte` | QTEDefinition | QTE 定义 |
| `nextScene` | string | 无选项时的默认跳转场景 |
| `onEnter` | Effect[] | 进入场景时触发的效果 |
| `loopStart` | number | 循环起始时间(秒) |
| `loopEnd` | number | 循环结束时间(秒)。视频播放到 loopEnd 时跳回 loopStart |
| `bgmUrl` | string | 背景音乐路径 |
| `bgmVolume` | number | 背景音乐音量0-1默认 0.8 |
| `bgmCrossFade` | number | 背景音乐交叉淡化时长(秒),默认 2.0 |
| `bgmDuckLevel` | number | BGM Duck 压低比例0-1默认 0.35 |
| `bgmDuckFade` | number | BGM Duck 过渡时长(秒),默认 0.5 |
| `videoMuted` | boolean | 视频静音(配合独立 BGM 使用) |
| `skippable` | boolean | `false` = 禁止跳过(用于 QTE 等关键场景) |
---
## Choice
```typescript
interface Choice {
text: string
textKey?: string
prompt?: string
promptKey?: string
targetScene: string
conditions?: Condition[]
effects?: Effect[]
timeLimit?: number
}
```
| 字段 | 说明 |
|------|------|
| `text` | 选项文本(回退值) |
| `textKey` | i18n key优先于 `text`。如 `"intro.choice.left_door"` |
| `prompt` | 选择后浮现的提示文字(回退值) |
| `promptKey` | prompt 的 i18n key。如 `"left_door.prompt.handshake"` |
| `targetScene` | 目标场景 ID |
| `conditions` | 显示条件,不满足的选项隐藏 |
| `effects` | 选择后触发的效果 |
| `timeLimit` | 限时秒数0=不限时 |
---
## Hotspot
```typescript
interface Hotspot {
id: string
label: string
labelKey?: string
targetScene: string
x: number
y: number
width: number
height: number
showAt?: number
hideAt?: number
conditions?: Condition[]
effects?: Effect[]
}
```
| 字段 | 说明 |
|------|------|
| `id` | 热点唯一标识 |
| `label` | 显示标签(回退值) |
| `labelKey` | i18n key |
| `x, y` | 左上角坐标(像素,基于 contentSize |
| `width, height` | 尺寸(像素) |
| `showAt` | 视频时间(秒),此时间后显示。不设则始终可见 |
| `hideAt` | 视频时间(秒),此时间后隐藏 |
| `targetScene` | 点击后跳转的场景 |
| `conditions` | 显示条件 |
| `effects` | 点击后触发的效果 |
---
## QTEDefinition
```typescript
interface QTEDefinition {
triggerTime: number
prompt: string
promptKey?: string
keys: string[]
timeLimit: number
successScene: string
failScene: string
effects?: {
success: Effect[]
fail: Effect[]
}
}
```
| 字段 | 说明 |
|------|------|
| `triggerTime` | 触发时间(秒),视频播放到此时间弹出 QTE |
| `prompt` | 提示文字(回退值) |
| `promptKey` | i18n key。如 `"right_door.qte.dodge"` |
| `keys` | 有效按键列表,如 `["ArrowLeft", "ArrowRight", "a", "d"]` |
| `timeLimit` | 限时秒数 |
| `successScene` | 成功跳转场景 |
| `failScene` | 失败/超时跳转场景 |
| `effects` | 成功/失败分别触发效果 |
---
## 其他类型
### Condition
```typescript
interface Condition {
variable: string
op: '>' | '<' | '>=' | '<=' | '==' | '!='
value: number | string | boolean
}
```
### Effect
```typescript
interface Effect {
type: 'set' | 'add'
target: string
value?: number | string | boolean
}
```
- `set` — 设置变量值
- `add` — 变量增加/减少
### ChapterInfo
```typescript
interface ChapterInfo {
id: string
label: string
labelKey?: string
startScene: string
thumbnail?: string
defaultVariables?: Record<string, number>
}
```
### AchievementDef
```typescript
interface AchievementDef {
id: string
title: string
titleKey?: string
description: string
descKey?: string
icon?: string
hidden?: boolean
condition: Condition
}
```
### EndingDef
```typescript
interface EndingDef {
id: string
label: string
labelKey?: string
sceneId: string
chapterId?: string
thumbnail?: string
}
```
### LocalesConfig
```typescript
interface LocalesConfig {
path: string
languages: string[]
}
```