feat: electron desktop packaging, CDN asset migration, production docs, scene JSON spec

This commit is contained in:
2026-06-09 23:20:27 +08:00
parent 48fb89449a
commit 3a7dd2f405
35 changed files with 900 additions and 135 deletions

419
docs/SCENE_JSON_SPEC.md Normal file
View File

@@ -0,0 +1,419 @@
# 场景 JSON 规范文档
电影游戏引擎的场景数据全部定义在 JSON 文件中。本文档是完整字段参考手册。
---
## 目录
1. [顶层结构 (GameData)](#1-顶层结构-gamedata)
2. [场景节点 (SceneNode)](#2-场景节点-scenenode)
3. [选项 (Choice)](#3-选项-choice)
4. [图片/视频热点 (Hotspot)](#4-图片视频热点-hotspot)
5. [快速反应事件 (QTEDefinition)](#5-快速反应事件-qtedefinition)
6. [条件和效果 (Condition & Effect)](#6-条件和效果-condition--effect)
7. [背景音乐 (BGM 字段)](#7-背景音乐-bgm-字段)
8. [章节 (ChapterInfo)](#8-章节-chapterinfo)
9. [成就 (AchievementDef)](#9-成就-achievementdef)
10. [结局 (EndingDef)](#10-结局-endingdef)
11. [完整示例](#11-完整示例)
12. [验证规则](#12-验证规则)
---
## 1. 顶层结构 (GameData)
```json
{
"startScene": "intro",
"variables": { "trust": 50, "courage": 0 },
"scenes": { ... },
"chapters": [ ... ],
"achievements": [ ... ],
"endings": [ ... ]
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `startScene` | string | ✅ | 游戏开始的场景 ID |
| `variables` | `Record<string, number>` | ✅ | 全局变量初始值(如好感度、勇气值等,制作者自定义) |
| `scenes` | `Record<string, SceneNode>` | ✅ | 所有场景节点key 为场景唯一 ID |
| `chapters` | ChapterInfo[] | 否 | 章节定义,用于章节选择功能 |
| `achievements` | AchievementDef[] | 否 | 成就定义,满足条件时自动解锁 |
| `endings` | EndingDef[] | 否 | 结局定义,用于结局画廊 |
---
## 2. 场景节点 (SceneNode)
```json
{
"id": "intro",
"type": "video",
"videoUrl": "/videos/intro.mp4",
"imageUrl": "/images/room.jpg",
"subtitleUrl": "/subtitles/intro.vtt",
"subtitles": { "zh": "/subtitles/intro_zh.vtt", "en": "/subtitles/intro_en.vtt" },
"choices": [ ... ],
"hotspots": [ ... ],
"qte": { ... },
"nextScene": "auto_next",
"onEnter": [ { "type": "set", "target": "visited_room", "value": 1 } ],
"loopStart": 8.0,
"loopEnd": 10.0,
"bgmUrl": "/audio/tense.mp3",
"bgmVolume": 0.8,
"bgmCrossFade": 2.0,
"bgmDuckLevel": 0.35,
"bgmDuckFade": 0.5,
"videoMuted": false,
"skippable": true
}
```
| 字段 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `id` | string | — | **唯一标识**。任意字符串,建议用有意义的英文名 |
| `type` | `"video"` \| `"image"` | `"video"` | `"image"` 时显示静态图片和热点,不播放视频 |
| `videoUrl` | string | `""` | 视频文件路径,推荐 `/videos/xxx.mp4` |
| `imageUrl` | string | — | 图片场景的图片路径 |
| `subtitleUrl` | string | — | 字幕 VTT 文件路径(单语言,向后兼容) |
| `subtitles` | `Record<string, string>` | — | 多语言字幕映射,如 `{"zh": "...", "en": "..."}`。优先级高于 `subtitleUrl` |
| `choices` | Choice[] | — | 选项列表。为空或不存在时场景播放完毕后自动结束(或走 `nextScene` |
| `hotspots` | Hotspot[] | — | 可点击热区。视频热点按 `showAt`/`hideAt` 时间轴显隐 |
| `qte` | QTEDefinition | — | QTE 事件定义 |
| `nextScene` | string | — | 无选项时的默认下一场景 ID自动跳转 |
| `onEnter` | Effect[] | — | 进入场景时执行的效果(如设置变量) |
| `loopStart` | number | — | 循环起点(秒)。到达后弹出选项并开始循环。需配合 `loopEnd` |
| `loopEnd` | number | — | 循环终点(秒)。播到时 seek 回 `loopStart` |
| `bgmUrl` | string | — | 背景音乐 MP3 路径。null/省略时静默 |
| `bgmVolume` | number | `0.8` | BGM 音量0~1 |
| `bgmCrossFade` | number | `2.0` | BGM 切换时的交叉淡化时长(秒) |
| `bgmDuckLevel` | number | `0.35` | QTE/选项出现时 BGM 降低到的百分比 |
| `bgmDuckFade` | number | `0.5` | Ducking 的渐变时长(秒) |
| `videoMuted` | boolean | `false` | 是否静音视频自带音轨 |
| `skippable` | boolean | `true` | `false` 时禁止跳过此场景(即使已看过) |
---
## 3. 选项 (Choice)
```json
{
"text": "帮助陌生人",
"textKey": "scene.intro.choice.help",
"prompt": "你的善意将被记住",
"targetScene": "help_ending",
"conditions": [
{ "variable": "trust", "op": ">=", "value": 50 }
],
"effects": [
{ "type": "add", "target": "trust", "value": 20 }
],
"timeLimit": 10
}
```
| 字段 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `text` | string | — | 选项显示文字(默认语言) |
| `textKey` | string | — | 多语言 key。有值时用翻译文件查文字否则用 `text` |
| `prompt` | string | — | 选后浮现提示文字Telltale 式 "某人会记住这件事")。有值时按钮前置金色标识 |
| `targetScene` | string | — | 点击后跳转的目标场景 ID |
| `conditions` | Condition[] | — | 显示条件。所有条件都满足时选项才显示 |
| `effects` | Effect[] | — | 选择后的效果(修改变量值等) |
| `timeLimit` | number | — | 限时选择。0 或不设表示不限时 |
---
## 4. 图片/视频热点 (Hotspot)
```json
{
"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" } ],
"timeLimit": 10
}
```
| 字段 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `id` | string | — | 热区唯一标识 |
| `label` | string | — | 鼠标悬停时显示的提示文字 |
| `targetScene` | string | — | 点击后跳转的目标场景 ID |
| `x` | number | — | 热区左上角 X 坐标,**相对比例0~1**,自适应屏幕 |
| `y` | number | — | 热区左上角 Y 坐标,**相对比例0~1** |
| `width` | number | — | 热区宽度,**相对比例0~1** |
| `height` | number | — | 热区高度,**相对比例0~1** |
| `showAt` | number | — | 视频模式下的出现时间(秒)。未设时始终显示 |
| `hideAt` | number | — | 视频模式下的消失时间(秒)。未设时始终显示 |
| `conditions` | Condition[] | — | 显示条件 |
| `effects` | Effect[] | — | 点击后的效果 |
| `timeLimit` | number | — | 限时热区(秒)。超时后热区消失,不触发跳转 |
---
## 5. 快速反应事件 (QTEDefinition)
```json
{
"triggerTime": 2.5,
"prompt": "躲避飞来的石块!",
"keys": ["ArrowLeft", "ArrowRight", "a", "d"],
"timeLimit": 3.0,
"successScene": "qte_win",
"failScene": "qte_lose",
"effects": {
"success": [ { "type": "add", "target": "courage", "value": 10 } ],
"fail": [ { "type": "add", "target": "health", "value": -20 } ]
}
}
```
| 字段 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `triggerTime` | number | — | QTE 触发时间(视频播到第几秒时触发) |
| `prompt` | string | — | QTE 提示文字 |
| `keys` | string[] | — | 需按下的键,大小写不敏感。如 `["Space"]` |
| `timeLimit` | number | — | 限时(秒) |
| `successScene` | string | — | 成功时跳转的场景 ID |
| `failScene` | string | — | 失败/超时时跳转的场景 ID |
| `effects` | object | — | 成功/失败各自的效果(`effects.success``effects.fail` |
---
## 6. 条件和效果 (Condition & Effect)
### Condition条件
```json
{ "variable": "trust", "op": ">=", "value": 80 }
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `variable` | string | 变量名 |
| `op` | string | 比较操作符:`>` / `<` / `>=` / `<=` / `==` / `!=` |
| `value` | number | 比较值 |
> **注意:** 条件中变量的值都是 number 类型。布尔值用 `== 1` 表示 true`== 0` 表示 false。
### Effect效果
```json
{ "type": "set", "target": "trust", "value": 80 }
{ "type": "add", "target": "courage", "value": 10 }
```
| `type` | 说明 |
|--------|------|
| `set` | 设置变量为目标值 |
| `add` | 变量增加指定值(负数为减) |
| 字段 | 类型 | 说明 |
|------|------|------|
| `target` | string | 变量名(与 Condition 的 `variable` 对应) |
| `value` | number | 值 |
---
## 7. 背景音乐 (BGM 字段)
BGM 字段定义在 SceneNode 中,由独立 AudioSystem 驱动,不受视频循环/切换影响。
| 字段 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `bgmUrl` | string | — | BGM MP3 路径。**两个相邻场景的 `bgmUrl` 相同时 → BGM 不中断继续播放。** 不同时 → 交叉淡化切换。null/省略时 → fade out |
| `bgmVolume` | number | `0.8` | BGM 音量0~1 |
| `bgmCrossFade` | number | `2.0` | 交叉淡化时长(秒) |
| `bgmDuckLevel` | number | `0.35` | QTE 触发 / 选项出现 / 热点出现时 BGM 自动降到 `bgmVolume × bgmDuckLevel` |
| `bgmDuckFade` | number | `0.5` | Ducking 渐变时长(秒) |
| `videoMuted` | boolean | `false` | 是否静音视频自带音轨。引擎不自动设置,需制作者手动指定 |
**制作建议:** BGM 使用独立的 .mp3 文件,视频导出时不要嵌入背景音乐。这样 BGM 在场景切换和画面循环时保持连贯。
---
## 8. 章节 (ChapterInfo)
```json
{
"id": "ch1",
"label": "第一章:相遇",
"startScene": "intro",
"thumbnail": "/images/ch1.jpg",
"defaultVariables": { "trust": 50, "courage": 0 }
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `id` | string | ✅ | 章节唯一标识 |
| `label` | string | ✅ | 章节显示名称(支持多语言) |
| `startScene` | string | ✅ | 章节起始场景 ID。**玩家到达此场景时 → 章节自动解锁** |
| `thumbnail` | string | 否 | 章节缩略图路径320×180 推荐) |
| `defaultVariables` | `Record<string, number>` | 否 | 从章节入口跳转时套用的变量初始值。未设时 fallback 到全局 `variables` |
---
## 9. 成就 (AchievementDef)
```json
{
"id": "helper",
"title": "善良的人",
"description": "选择帮助陌生人",
"icon": "/images/ach_helper.png",
"hidden": false,
"condition": { "variable": "trust", "op": ">=", "value": 70 }
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `id` | string | ✅ | 成就唯一标识 |
| `title` | string | ✅ | 成就标题 |
| `description` | string | ✅ | 成就描述 |
| `icon` | string | 否 | 成就图标路径(可选) |
| `hidden` | boolean | 否 | `true` 时未解锁不显示标题和描述(显示 ??? |
| `condition` | Condition | ✅ | 解锁条件。变量满足时自动解锁并弹出 toast |
---
## 10. 结局 (EndingDef)
```json
{
"id": "help_end",
"label": "伸出援手",
"sceneId": "help_ending",
"thumbnail": "/images/end_help.jpg"
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|:--:|------|
| `id` | string | ✅ | 结局唯一标识 |
| `label` | string | ✅ | 结局显示名称 |
| `sceneId` | string | ✅ | 结局场景 ID。**玩家到达此场景时 → 结局自动标记已解锁** |
| `thumbnail` | string | 否 | 结局缩略图路径320×180 推荐) |
---
## 11. 完整示例
以下示例覆盖所有功能:
```json
{
"startScene": "intro",
"variables": { "trust": 50, "courage": 0, "investigation": 0 },
"scenes": {
"intro": {
"id": "intro",
"videoUrl": "/videos/intro.mp4",
"subtitles": { "zh": "/subtitles/intro_zh.vtt", "en": "/subtitles/intro_en.vtt" },
"bgmUrl": "/audio/calm.mp3",
"bgmVolume": 0.6,
"loopStart": 5.0,
"loopEnd": 8.0,
"choices": [
{
"text": "帮助陌生人",
"textKey": "scene.intro.choice.help",
"prompt": "你的善意将被记住",
"targetScene": "help_ending",
"effects": [{ "type": "add", "target": "trust", "value": 20 }]
},
{
"text": "调查房间",
"textKey": "scene.intro.choice.investigate",
"targetScene": "crime_scene"
}
]
},
"crime_scene": {
"id": "crime_scene",
"type": "image",
"imageUrl": "/images/crime_scene.jpg",
"hotspots": [
{
"id": "hs_desk",
"label": "查看书桌",
"targetScene": "desk_detail",
"x": 0.15, "y": 0.30, "width": 0.25, "height": 0.35,
"effects": [{ "type": "add", "target": "investigation", "value": 1 }]
}
],
"choices": [
{ "text": "离开房间", "targetScene": "corridor" }
]
},
"corridor": {
"id": "corridor",
"videoUrl": "/videos/corridor.mp4",
"bgmUrl": "/audio/tense.mp3",
"bgmVolume": 0.7,
"bgmCrossFade": 1.5,
"qte": {
"triggerTime": 2.0,
"prompt": "躲避飞来的石块!",
"keys": ["Space"],
"timeLimit": 3.0,
"successScene": "qte_win",
"failScene": "qte_lose",
"effects": {
"success": [{ "type": "add", "target": "courage", "value": 10 }],
"fail": [{ "type": "add", "target": "trust", "value": -10 }]
}
}
},
"help_ending": {
"id": "help_ending",
"videoUrl": "/videos/help.mp4",
"bgmUrl": "/audio/calm.mp3",
"bgmVolume": 0.6,
"onEnter": [{ "type": "set", "target": "completed_game", "value": 1 }],
"choices": []
}
},
"chapters": [
{
"id": "ch1", "label": "第一章", "startScene": "intro",
"thumbnail": "/images/ch1.jpg",
"defaultVariables": { "trust": 50, "courage": 0, "investigation": 0 }
}
],
"achievements": [
{
"id": "helper", "title": "善良的人", "description": "选择帮助陌生人",
"hidden": false,
"condition": { "variable": "trust", "op": ">=", "value": 70 }
}
],
"endings": [
{ "id": "help_end", "label": "伸出援手", "sceneId": "help_ending", "thumbnail": "/images/end_help.jpg" }
]
}
```
---
## 12. 验证规则
| 规则 | 说明 |
|------|------|
| **ID 唯一性** | `scenes` 的 key、`Choice.targetScene``Hotspot.targetScene``QTEDefinition.successScene`/`failScene``ChapterInfo.startScene``EndingDef.sceneId` 必须引用存在的场景 ID |
| **循环引用** | 场景 A 的 targetScene 指向 A 自身 → 允许(重玩同一场景) |
| **死路检测** | 有 `conditions` 的 choice 如果条件永远无法满足 → 该分支永久不可达(建议制作者验证) |
| **videoUrl 与 imageUrl** | `type: "image"` 时可省略 `videoUrl``type: "video"`(默认)时必须提供 `videoUrl` |
| **loopStart / loopEnd** | 两者必须同时出现或同时不出现。`loopStart < loopEnd` |
| **JSON 格式** | 严格 JSON不支持注释、尾逗号。用编辑器导出可保证格式正确 |