# 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
  streamingUrl?: Record<string, string>
  imageUrl?: string
  thumbnail?: string
  contentSize?: { w: number; h: number }
  subtitleUrl?: string
  subtitles?: Record<string, string>
  choices?: Choice[]
  hotspots?: Hotspot[]
  qte?: QTEDefinition
  nextScene?: string | Choice[]
  onEnter?: Effect[]
  loopStart?: number
  loopEnd?: number
  bgmUrl?: string
  bgmVolume?: number
  bgmCrossFade?: number
  bgmDuckLevel?: number
  bgmDuckFade?: number
  videoMuted?: boolean
  skippable?: boolean
  keyMoment?: boolean
  battleHUD?: BattleHUDEntry[]
  battleResult?: BattleResultDef
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 场景唯一标识 |
| `type` | string | `"video"` (默认) 或 `"image"`。image 类型展示图片+热点，不播放视频 |
| `videoUrl` | string | 视频文件路径（本地 MP4，桌面版使用）。image 场景可为空 |
| `streamingUrl` | object | Web 版使用的 CDN 流媒体路径。三档画质：`{ "超清 (1080P)": "...", "高清 (720P)": "...", "标清 (480P)": "..." }`。引擎根据 `getVideoMode()` 自动选择环境 |
| `imageUrl` | string | 图片场景的图片路径 |
| `thumbnail` | 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 \| Choice[] | 无选项时的默认跳转。字符串为单一场景 ID；数组为条件路由，遍历第一个满足 conditions 的跳转。末尾无条件项作为默认目标 |
| `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 等关键场景） |
| `keyMoment` | boolean | StoryGallery 中是否展示为关键节点。`true`=强制展示，`false`=强制隐藏，未设置时自动判断（章节起始/有 choice/是结局） |
| `battleHUD` | BattleHUDEntry[] | 战斗场景中显示的角色属性 HUD。每个 entry 为一个角色（头像 + stats 列表） |
| `battleResult` | BattleResultDef | 胜利结算面板。视频结束后弹出，展示战斗统计数据。战败场景不配置此字段 |

---

## 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"`。优先于 `prompt` |
| `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[]
  timeLimit?: number
}
```

| 字段 | 说明 |
|------|------|
| `id` | 热点唯一标识 |
| `label` | 显示标签（回退值） |
| `labelKey` | i18n key |
| `x, y` | 左上角坐标（像素，基于 contentSize） |
| `width, height` | 尺寸（像素） |
| `showAt` | 视频时间（秒），此时间后显示。不设则始终可见 |
| `hideAt` | 视频时间（秒），此时间后隐藏 |
| `targetScene` | 点击后跳转的场景 |
| `conditions` | 显示条件 |
| `effects` | 点击后触发的效果 |
| `timeLimit` | 限时热点（秒），超时后热点消失 |

---

## 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"`。优先于 `prompt` |
| `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>
}
```

| 字段 | 说明 |
|------|------|
| `id` | 章节 ID |
| `label` | 章节名称（回退值） |
| `labelKey` | i18n key，优先于 `label` |
| `startScene` | 起始场景 ID。玩家到达此场景时章节自动解锁 |
| `thumbnail` | 缩略图路径 |
| `defaultVariables` | 从章节选择界面进入时的默认变量值。未设时 fallback 到全局 `variables` |

### AchievementDef

```typescript
interface AchievementDef {
  id: string
  title: string
  titleKey?: string
  description: string
  descKey?: string
  icon?: string
  hidden?: boolean
  condition: Condition
}
```

| 字段 | 说明 |
|------|------|
| `id` | 成就唯一 ID |
| `title` | 成就标题（回退值） |
| `titleKey` | i18n key，优先于 `title` |
| `description` | 成就描述（回退值） |
| `descKey` | i18n key，优先于 `description` |
| `icon` | 图标路径 |
| `hidden` | `true` 时未解锁不显示标题和描述（显示 ???） |
| `condition` | 解锁条件。变量满足时自动解锁并弹出 toast |

### EndingDef

```typescript
interface EndingDef {
  id: string
  label: string
  labelKey?: string
  sceneId: string
  chapterId?: string
  thumbnail?: string
}
```

| 字段 | 说明 |
|------|------|
| `id` | 结局唯一 ID |
| `label` | 结局名称（回退值） |
| `labelKey` | i18n key，优先于 `label` |
| `sceneId` | 结局场景 ID。玩家到达此场景时结局自动标记已解锁 |
| `chapterId` | 结局归属章节 ID（可选，BFS 自动推导） |
| `thumbnail` | 缩略图路径 |

### LocalesConfig

```typescript
interface LocalesConfig {
  path: string
  languages: string[]
}
```

| 字段 | 说明 |
|------|------|
| `path` | 语言文件目录（相对于 `assetBase`），如 `"locales/"` |
| `languages` | 支持的语言列表，如 `["zh", "en", "ja"]` |

### BattleHUDEntry / BattleHUDStat

战斗场景中显示的角色属性 HUD。配置在 `SceneNode.battleHUD`。

```typescript
interface BattleHUDEntry {
  label: string
  labelKey?: string
  portrait?: string
  stats: BattleHUDStat[]
}

interface BattleHUDStat {
  variable: string
  label: string
  labelKey?: string
  max?: number
  style?: 'bar' | 'number'
}
```

| 字段 | 说明 |
|------|------|
| `label` | 角色名称（回退值） |
| `labelKey` | 角色名称 i18n key |
| `portrait` | 角色头像路径 |
| `stats` | 属性数组。`variable`/`label`/`labelKey`/`max`/`style`。`style` 缺省时根据有无 `max` 自动判断（有则为 bar，无则为 number） |

### BattleResultDef / BattleResultStat

战斗胜利结算面板。配置在 `SceneNode.battleResult`。

```typescript
interface BattleResultDef {
  title: string
  titleKey?: string
  stats: BattleResultStat[]
}

interface BattleResultStat {
  label: string
  labelKey?: string
  variable: string
  max?: number
}
```

| 字段 | 说明 |
|------|------|
| `title` | 结算标题（回退值） |
| `titleKey` | 标题 i18n key |
| `stats` | 统计项数组。`variable`/`label`/`labelKey`/`max` |
