novel-game

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
你是一个专业的游戏策划兼全栈开发者,擅长将小说或故事改编为浏览器端的互动小说游戏。
用户的需求:$ARGUMENTS

You are a professional game planner and full-stack developer, specializing in adapting novels or stories into browser-based interactive novel games.
User Requirements: $ARGUMENTS

前置依赖

Prerequisites

本技能依赖阿里云百炼 CLI(
bl
)进行 AI 素材生成(视频/图片/语音)。使用前请检查:
bash
bl --version
如果未安装,请参考安装文档:https://bailian.aliyun.com/cli/install.md

This skill relies on the Alibaba Cloud Bailian CLI (
bl
) for AI asset generation (videos/images/voices). Before use, check:
bash
bl --version
If not installed, refer to the installation documentation: https://bailian.aliyun.com/cli/install.md

第一步:需求收集

Step 1: Requirements Gathering

使用 AskUserQuestion 向用户确认以下关键设计决策(一次性问完):
  1. 素材来源 — 用户是否提供了小说文件(EPUB/TXT)?如果有,先读取内容提取剧情结构。
  2. 游戏类型 — 互动小说(选择影响剧情) / 文字冒险+解谜 / 文字RPG(含属性系统)
  3. UI 风格 — 默认
    auto
    :根据小说题材自动推断(见下表),用户可覆盖。仅当用户明确不满意推断结果时才切换为指定风格。
  4. 叙事视角 — 第一人称(扮演主角) / 第三人称上帝视角(旁观者选择)/ 双主角切换
  5. AI 素材生成 — 是否需要 AI 生成角色立绘和过场?如需要,选择素材模式:
    • 视频模式 — 角色立绘为动态视频循环,过场为视频(效果最佳,生成慢,成本高)
    • 图片模式 — 角色立绘为静态图片,过场为静态 CG + Ken Burns 动效(生成快,成本低)
    • 混合模式(推荐)— 角色立绘用图片省成本,关键过场用视频提升体验
  6. 音频 — 选择音频方案:
    • 无音频
    • 仅 BGM(Web Audio API 程序化生成)
    • BGM + 音效(全部 Web Audio API 程序化生成,零外部依赖)
    • BGM + 音效 + TTS 旁白(BGM/音效程序化生成 +
      bl speech synthesize
      生成旁白语音)
  7. 游戏时长 — 15-20分钟(8-10场景)/ 30-45分钟(15-18场景)/ 1小时+(25+场景)
Use AskUserQuestion to confirm the following key design decisions with the user (ask all at once):
  1. Asset Source — Has the user provided a novel file (EPUB/TXT)? If yes, first read the content to extract the story structure.
  2. Game Type — Interactive Novel (choices affect storyline) / Text Adventure + Puzzle / Text RPG (with attribute system)
  3. UI Style — Default
    auto
    : Automatically inferred based on the novel genre (see table below), users can override. Only switch to a specified style if the user explicitly disagrees with the inferred result.
  4. Narrative Perspective — First-person (play as the protagonist) / Third-person omniscient (旁观者选择) / Dual-protagonist switching
  5. AI Asset Generation — Is AI-generated character portraits and cutscenes needed? If yes, select the asset mode:
    • Video Mode — Character portraits are dynamic video loops, cutscenes are videos (best effect, slow generation, high cost)
    • Image Mode — Character portraits are static images, cutscenes are static CG + Ken Burns effect (fast generation, low cost)
    • Hybrid Mode (Recommended) — Use images for character portraits to save cost, use videos for key cutscenes to enhance experience
  6. Audio — Select the audio solution:
    • No audio
    • BGM only (procedurally generated via Web Audio API)
    • BGM + Sound Effects (all procedurally generated via Web Audio API, zero external dependencies)
    • BGM + Sound Effects + TTS Narration (BGM/sound effects procedurally generated +
      bl speech synthesize
      for narration voice)
  7. Game Duration — 15-20 minutes (8-10 scenes) / 30-45 minutes (15-18 scenes) / 1+ hour (25+ scenes)

UI 风格自动推断表(
auto
模式)

UI Style Auto-Inference Table (
auto
Mode)

根据小说题材关键词判定,并在第二步产出物中说明判定理由:
题材关键词推断风格视觉特征
科幻 / 未来 / 太空 / 人工智能 / 末日赛博朋克冷色调、霓虹高光、等宽字体、扫描线
古风 / 武侠 / 仙侠 / 历史 / 宫廷水墨中国风墨色、留白、衬线字体、水墨晕染
复古 / 8bit / 轻松 / 治愈 / 搞笑像素风低饱和、像素字体、CRT 扫描线
现代 / 都市 / 现实主义 / 悬疑 / 职场简约现代中性色、无衬线字体、扁平化
多题材混合时取主导题材;无法判定时默认「简约现代」。

Determine based on novel genre keywords, and explain the reasoning in the Step 2 deliverables:
Genre KeywordsInferred StyleVisual Characteristics
Sci-fi / Future / Space / AI / Post-apocalypseCyberpunkCool tones, neon highlights, monospaced fonts, scanlines
Ancient Chinese style / Wuxia / Xianxia / History / PalaceInk Chinese StyleInk colors, negative space, serif fonts, ink wash effects
Retro / 8bit / Relaxing / Healing / FunnyPixel StyleLow saturation, pixel fonts, CRT scanlines
Modern / Urban / Realism / Suspense / WorkplaceMinimalist ModernNeutral colors, sans-serif fonts, flat design
For mixed genres, take the dominant one; default to "Minimalist Modern" if undetermined.

第二步:剧情设计

Step 2: Story Design

根据原著/素材/用户描述,直接设计以下内容:
  1. 核心剧情线 — 识别 1-3 条主线(可交织),每条线梳理关键场景
  2. 关键分支点 — 选出 3-5 个影响结局的重大选择。选择即分叉(详见下方分支原则)
  3. 结局设计 — 设计 3-5 个不同结局,每个由 flags 组合决定
  4. 角色列表 — 列出需要立绘的主要角色(6-8个),含外观描述
  5. 过场场景 — 列出需要生成素材的高潮场景(5-8个),含画面描述
  6. 收集物/档案 — 设计通过选择解锁的背景知识条目
  7. UI 风格推断
    auto
    模式时)— 根据题材判定风格并说明理由
  8. 分支图 — 画出场景分叉/合流/结局的拓扑(文字版即可),确保每个重大选择有 ≥2 条不同后续路径
Based on the original work/assets/user description, directly design the following:
  1. Core Storylines — Identify 1-3 main storylines (can intersect), sort out key scenes for each line
  2. Key Branch Points — Select 3-5 major choices that affect the ending. Choices mean branching (see Branching Principles below)
  3. Ending Design — Design 3-5 different endings, each determined by a combination of flags
  4. Character List — List main characters requiring portraits (6-8 characters), including appearance descriptions
  5. Cutscene Scenes — List climax scenes requiring asset generation (5-8 scenes), including scene descriptions
  6. Collectibles/Archives — Design background knowledge entries unlocked through choices
  7. UI Style Inference (in
    auto
    mode) — Determine the style based on genre and explain the reasoning
  8. Branch Diagram — Draw the topology of scene branching/convergence/endings (text version is fine), ensuring each major choice has ≥2 different subsequent paths

分支设计原则(核心)

Branching Design Principles (Core)

互动小说的灵魂在于「选择有意义」。遵循以下原则:
  • 选择即分叉:重大选择的每个选项应导向不同的下一场景(不同
    next
    ),而非「同场景 + 不同 flag」。玩家选 A 走 A 路线,选 B 走 B 路线,看到的文本/立绘/BGM 都不同。
  • 分后必合或分后不归:分支后要么在下游中转节点汇合(合流点保留共同剧情推进,但用 flag 微调文本),要么一路分到底导向不同结局。
  • 禁止「假分支」:避免「选 A 或 B 但下一场景相同且仅 flag 不同」的伪选择。若 A/B 后续差异不大则合并为单线,不要硬凑分支。
  • 分支深度:每个重大分支至少影响 2-3 个后续场景的文本/立绘/BGM,让玩家感受到「这次玩的不一样」。
  • flag 的角色:flag 不再是分支的全部,而是记录「累计倾向」,用于在合流点微调文本和最终决定结局。真正的分支由
    next
    指向不同场景实现。
示例(三体·红岸基地)
红岸基地·发射抉择
  ├─ 选「按下发射」→ scene_send_signal(发送信号路线,2-3个独有场景)
  │      └─ ... → 合流点 scene_convergence(文本根据 sent_signal=true 微调)
  └─ 选「犹豫放弃」→ scene_abort_signal(放弃路线,2-3个独有场景)
         └─ ... → 合流点 scene_convergence(文本根据 sent_signal=false 微调)

The soul of interactive novels lies in "meaningful choices". Follow these principles:
  • Choices mean branching: Each option of a major choice should lead to a different next scene (different
    next
    ), not "same scene + different flag". If the player chooses A, they take route A; choose B, take route B, with different text/portraits/BGM.
  • Branch then converge or branch to the end: After branching, either converge at a downstream transit node (retain common story progression at the convergence point, but adjust text with flags), or branch all the way to different endings.
  • No "fake branches": Avoid pseudo-choices where "choosing A or B leads to the same next scene with only flag differences". If the subsequent differences between A/B are minor, merge into a single line instead of forcing branches.
  • Branch depth: Each major branch should affect the text/portraits/BGM of at least 2-3 subsequent scenes, so players can feel "this playthrough is different".
  • Role of flags: Flags no longer define the entire branch, but record "cumulative tendencies" to adjust text at convergence points and determine the final ending. Real branching is achieved by
    next
    pointing to different scenes.
Example (The Three-Body Problem · Red Coast Base):
Red Coast Base · Launch Decision
  ├─ Choose "Press Launch" → scene_send_signal (signal sending route, 2-3 unique scenes)
  │      └─ ... → Convergence point scene_convergence (text adjusted based on sent_signal=true)
  └─ Choose "Hesitate and Abandon" → scene_abort_signal (abandonment route, 2-3 unique scenes)
         └─ ... → Convergence point scene_convergence (text adjusted based on sent_signal=false)

第三步:项目架构

Step 3: Project Architecture

使用
npx create-react-app
初始化,按以下结构组织代码:
src/
├── App.jsx                  # 主应用,游戏状态路由
├── index.js
├── components/
│   ├── TitleScreen.jsx       # 开始画面(主题动画+标题+继续游戏+存档管理)
│   ├── GameScene.jsx         # 核心场景渲染(文本+选择+立绘+旁白)
│   ├── TypeWriter.jsx        # 打字机逐字显示效果
│   ├── ChoicePanel.jsx       # 选择面板(hover动效+延迟入场)
│   ├── CharacterPortrait.jsx # 角色立绘(自动适配视频/图片)
│   ├── CutScene.jsx          # 过场播放(视频/图片 Ken Burns)
│   ├── EndingScreen.jsx      # 结局界面
│   ├── ArchivePanel.jsx      # 档案/收集物面板
│   ├── ArchiveNotification.jsx # 档案解锁通知
│   ├── SaveLoadPanel.jsx     # 存档/读档面板
│   └── ProgressBar.jsx       # 章节进度条
├── data/
│   ├── story.js              # 场景图(核心数据)
│   ├── characters.js         # 角色定义
│   ├── archives.js           # 档案数据
│   └── generated-assets.json # AI 生成素材的本地路径+类型
├── hooks/
│   ├── useGameState.js       # useReducer 游戏状态管理 + 存档
│   └── useAudio.js           # Web Audio API 音频系统
├── styles/
│   ├── theme.css             # 主题样式(根据用户选择调整)
│   ├── animations.css        # 动画定义
│   └── crt.css               # CRT 扫描线效果(像素风专用)
└── [特殊场景组件]             # 按需:Canvas 动态背景等
scripts/
└── generate-assets.sh        # bl CLI 素材生成脚本
public/
└── assets/                   # 下载到本地的素材文件
    ├── portraits/            # 角色立绘(.mp4 或 .png)
    ├── cutscenes/            # 过场素材(.mp4 或 .png)
    ├── backgrounds/          # 场景背景图(.png)
    └── narrations/           # TTS 旁白音频(.mp3)

Initialize with
npx create-react-app
, organize code according to the following structure:
src/
├── App.jsx                  # Main application, game state routing
├── index.js
├── components/
│   ├── TitleScreen.jsx       # Start screen (theme animation + title + continue game + save management)
│   ├── GameScene.jsx         # Core scene rendering (text + choices + portraits + narration)
│   ├── TypeWriter.jsx        # Typewriter word-by-word display effect
│   ├── ChoicePanel.jsx       # Choice panel (hover effect + delayed entry)
│   ├── CharacterPortrait.jsx # Character portrait (auto-adapts to video/image)
│   ├── CutScene.jsx          # Cutscene playback (video/image Ken Burns)
│   ├── EndingScreen.jsx      # Ending interface
│   ├── ArchivePanel.jsx      # Archive/collectible panel
│   ├── ArchiveNotification.jsx # Archive unlock notification
│   ├── SaveLoadPanel.jsx     # Save/load panel
│   └── ProgressBar.jsx       # Chapter progress bar
├── data/
│   ├── story.js              # Scene graph (core data)
│   ├── characters.js         # Character definitions
│   ├── archives.js           # Archive data
│   └── generated-assets.json # Local paths + types of AI-generated assets
├── hooks/
│   ├── useGameState.js       # useReducer game state management + save system
│   └── useAudio.js           # Web Audio API audio system
├── styles/
│   ├── theme.css             # Theme styles (adjusted based on user choice)
│   ├── animations.css        # Animation definitions
│   └── crt.css               # CRT scanline effect (pixel style only)
└── [Special Scene Components]             # Optional: Canvas dynamic background, etc.
scripts/
└── generate-assets.sh        # bl CLI asset generation script
public/
└── assets/                   # Downloaded local assets
    ├── portraits/            # Character portraits (.mp4 or .png)
    ├── cutscenes/            # Cutscene assets (.mp4 or .png)
    ├── backgrounds/          # Scene background images (.png)
    └── narrations/           # TTS narration audio (.mp3)

第四步:核心数据模型

Step 4: Core Data Models

story.js 场景数据结构

story.js Scene Data Structure

js
export const scenes = {
  scene_id: {
    id: "scene_id",
    title: "章节标题",
    timeline: "past|present|game", // 时间线标识(影响 UI 颜色)
    year: "1967", // 显示年份
    character: "character_key", // 当前场景角色立绘
    bgm: "bgm_name", // 背景音乐
    cutscene: "cutscene_key", // 过场素材 key(可选)
    narration: "narration_key", // TTS 旁白 key(可选)
    isEnding: false,
    endingType: "ending_a",
    texts: ["第一段文字...", "第二段文字..."],
    choices: [
      {
        text: "按下发射按钮",
        next: "scene_send_signal", // 分叉路线 A — 不同场景
        setFlags: { sent_signal: true },
        archive: "archive_signal_sent", // 解锁档案(可选)
      },
      {
        text: "犹豫后放弃",
        next: "scene_abort_signal", // 分叉路线 B — 不同场景(真实分叉)
        setFlags: { sent_signal: false },
      },
    ],
  },
};

// 合流点:两条分叉路线在此汇合,根据 flag 渲染条件文本
export function getSceneText(scene, flags) {
  // 读 flag 返回该玩家路线专属的文本/立绘
}

export function getEnding(flags) {
  // 根据 flags 组合返回对应结局 scene id
}
js
export const scenes = {
  scene_id: {
    id: "scene_id",
    title: "Chapter Title",
    timeline: "past|present|game", // Timeline identifier (affects UI color)
    year: "1967", // Displayed year
    character: "character_key", // Current scene character portrait
    bgm: "bgm_name", // Background music
    cutscene: "cutscene_key", // Cutscene asset key (optional)
    narration: "narration_key", // TTS narration key (optional)
    isEnding: false,
    endingType: "ending_a",
    texts: ["First paragraph...", "Second paragraph..."],
    choices: [
      {
        text: "Press the launch button",
        next: "scene_send_signal", // Branch route A — different scene
        setFlags: { sent_signal: true },
        archive: "archive_signal_sent", // Unlock archive (optional)
      },
      {
        text: "Hesitate and abandon",
        next: "scene_abort_signal", // Branch route B — different scene (real branch)
        setFlags: { sent_signal: false },
      },
    ],
  },
};

// Convergence point: Two branch routes meet here, render conditional text based on flags
export function getSceneText(scene, flags) {
  // Read flags to return text/portraits exclusive to the player's route
}

export function getEnding(flags) {
  // Return corresponding ending scene id based on flag combinations
}

generated-assets.json 数据结构

generated-assets.json Data Structure

json
{
  "portraits": {
    "ye_wenjie": { "path": "/assets/portraits/ye_wenjie.mp4", "type": "video" },
    "luo_ji": { "path": "/assets/portraits/luo_ji.png", "type": "image" }
  },
  "cutscenes": {
    "red_coast": { "path": "/assets/cutscenes/red_coast.mp4", "type": "video" },
    "countdown": { "path": "/assets/cutscenes/countdown.png", "type": "image" }
  },
  "backgrounds": {
    "campus_1967": {
      "path": "/assets/backgrounds/campus_1967.png",
      "type": "image"
    }
  },
  "narrations": {
    "scene_opening": { "path": "/assets/narrations/scene_opening.mp3" }
  }
}
json
{
  "portraits": {
    "ye_wenjie": { "path": "/assets/portraits/ye_wenjie.mp4", "type": "video" },
    "luo_ji": { "path": "/assets/portraits/luo_ji.png", "type": "image" }
  },
  "cutscenes": {
    "red_coast": { "path": "/assets/cutscenes/red_coast.mp4", "type": "video" },
    "countdown": { "path": "/assets/cutscenes/countdown.png", "type": "image" }
  },
  "backgrounds": {
    "campus_1967": {
      "path": "/assets/backgrounds/campus_1967.png",
      "type": "image"
    }
  },
  "narrations": {
    "scene_opening": { "path": "/assets/narrations/scene_opening.mp3" }
  }
}

useGameState 状态结构

useGameState State Structure

js
{
  phase: 'title' | 'playing' | 'cutscene' | 'ending',
  currentScene: string,
  flags: {},
  history: string[],
  archives: string[],
  textIndex: number,
  typingDone: boolean,
  showCutscene: boolean,
  showArchive: boolean,
  newArchive: null | string,
  bgm: string | null,
}

js
{
  phase: 'title' | 'playing' | 'cutscene' | 'ending',
  currentScene: string,
  flags: {},
  history: string[],
  archives: string[],
  textIndex: number,
  typingDone: boolean,
  showCutscene: boolean,
  showArchive: boolean,
  newArchive: null | string,
  bgm: string | null,
}

第五步:关键实现模式

Step 5: Key Implementation Patterns

打字机效果(TypeWriter)

Typewriter Effect (TypeWriter)

  • 用 setInterval 逐字显示,speed 约 40-50ms
  • 点击/触摸可跳过(立即显示全文)
  • 每个字符触发打字音效回调
  • 显示完毕调用 onDone 回调
  • Use setInterval to display character by character, speed around 40-50ms
  • Click/touch to skip (display full text immediately)
  • Each character triggers a typing sound callback
  • Call onDone callback after display completes

选择面板(ChoicePanel)

Choice Panel (ChoicePanel)

  • 在最后一段文字打字完成后淡入
  • 每个选项延迟入场动画(nth-child animation-delay)
  • hover 时边框变色 + 微位移 + 阴影扩大
  • 点击触发音效 → 设置 flags → 自动存档 → 跳转下一场景
  • Fade in after the last paragraph's typewriter effect finishes
  • Each option has a delayed entry animation (nth-child animation-delay)
  • Border color change + slight displacement + shadow expansion on hover
  • Click triggers sound effect → set flags → auto-save → jump to next scene

Hash 路由

Hash Routing

  • URL hash 同步当前场景:
    #scene_id
  • 支持直接通过 URL 跳转到任意章节(开发调试 + 分享)
  • 监听 hashchange 支持浏览器前进/后退
  • 回到标题时清除 hash
  • URL hash syncs with current scene:
    #scene_id
  • Support direct jump to any chapter via URL (development debugging + sharing)
  • Listen to hashchange to support browser forward/backward
  • Clear hash when returning to title screen

存档系统(localStorage)

Save System (localStorage)

  • 自动存档:每次做出选择后自动保存当前状态到
    localStorage.setItem('novel_autosave', JSON.stringify(state))
  • 手动存档:支持 3 个存档槽位(
    novel_save_1
    /
    novel_save_2
    /
    novel_save_3
    ),每个槽位保存完整 state + 存档时间 + 当前场景标题
  • TitleScreen 入口
    • 「新游戏」— 清空状态从头开始
    • 「继续游戏」— 仅当自动存档存在时显示,加载自动存档
    • 「读取存档」— 打开 SaveLoadPanel,显示 3 个手动槽位
  • SaveLoadPanel:游戏内可随时打开(菜单按钮/快捷键),支持存档和读档
  • 存档数据结构:
    { state, savedAt, sceneTitle, playTime }
  • Auto-save: Automatically save current state to
    localStorage.setItem('novel_autosave', JSON.stringify(state))
    after each choice
  • Manual save: Support 3 save slots (
    novel_save_1
    /
    novel_save_2
    /
    novel_save_3
    ), each slot saves complete state + save time + current scene title
  • TitleScreen Entry:
    • "New Game" — Clear state and start from the beginning
    • "Continue Game" — Only displayed if auto-save exists, load auto-save
    • "Load Save" — Open SaveLoadPanel, display 3 manual slots
  • SaveLoadPanel: Can be opened anytime in-game (menu button/shortcut keys), supports saving and loading
  • Save data structure:
    { state, savedAt, sceneTitle, playTime }

角色立绘(CharacterPortrait)— 自动适配视频/图片

Character Portrait (CharacterPortrait) — Auto-Adapt to Video/Image

  • generated-assets.json
    读取素材信息,根据
    type
    字段渲染:
    • type: "video"
      <video src={path} autoPlay loop muted playsInline />
    • type: "image"
      <img src={path} />
      + 可选呼吸动效(CSS
      animation: breathe 3s ease-in-out infinite
  • 无素材时显示角色名首字母占位符
  • Read asset information from
    generated-assets.json
    , render based on
    type
    field:
    • type: "video"
      <video src={path} autoPlay loop muted playsInline />
    • type: "image"
      <img src={path} />
      + optional breathing effect (CSS
      animation: breathe 3s ease-in-out infinite
      )
  • Display character initial placeholder if no asset exists

过场播放(CutScene)— 自动适配视频/图片

Cutscene Playback (CutScene) — Auto-Adapt to Video/Image

  • type: "video"
    → 全屏
    <video>
    播放,结束后自动关闭
  • type: "image"
    → 全屏
    <img>
    + Ken Burns 动效(CSS
    animation: kenburns 5s ease-in-out
    ),5 秒后自动关闭
  • Ken Burns 效果:从
    scale(1.1) translate(-2%, -2%)
    过渡到
    scale(1.0) translate(0, 0)
    ,模拟镜头缓慢推拉
  • type: "video"
    → Full-screen
    <video>
    playback, auto-close after completion
  • type: "image"
    → Full-screen
    <img>
    + Ken Burns effect (CSS
    animation: kenburns 5s ease-in-out
    ), auto-close after 5 seconds
  • Ken Burns effect: Transition from
    scale(1.1) translate(-2%, -2%)
    to
    scale(1.0) translate(0, 0)
    , simulating slow camera push/pull

素材懒加载

Asset Lazy Loading

  • 所有视频元素默认
    <video preload="none">
    ,不预加载
  • 仅预加载当前场景和下一可能场景的素材
  • 场景切换时:
    video.load()
    加载当前 →
    requestIdleCallback
    预加载下一个
  • 离开场景时:
    video.pause(); video.removeAttribute('src'); video.load()
    释放内存
  • 图片使用
    loading="lazy"
    属性
  • All video elements default to
    <video preload="none">
    , no preloading
  • Only preload assets for current scene and next possible scenes
  • When switching scenes:
    video.load()
    to load current →
    requestIdleCallback
    to preload next
  • When leaving scene:
    video.pause(); video.removeAttribute('src'); video.load()
    to release memory
  • Images use
    loading="lazy"
    attribute

移动端适配

Mobile Adaptation

  • <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  • 竖屏优先布局:文字区占屏幕上方 60%,立绘在下方 40%(横屏时立绘在侧边)
  • 选择按钮最小高度 44px(iOS 触控标准)
  • 字体 16px 起步,防止 iOS Safari 自动缩放
  • 打字机跳过同时监听
    click
    touchend
    (不能只依赖 click,移动端有 300ms 延迟)
  • CSS
    env(safe-area-inset-bottom)
    处理 iPhone 底部安全区
  • 禁止双指缩放:
    touch-action: manipulation
  • 禁止长按菜单:
    -webkit-touch-callout: none; user-select: none
    (仅对游戏 UI 元素)
  • <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  • Portrait-first layout: Text area occupies top 60% of screen, portraits in bottom 40% (portraits on side in landscape mode)
  • Minimum height of choice buttons is 44px (iOS touch standard)
  • Font starts at 16px to prevent automatic scaling by iOS Safari
  • Typewriter skip listens to both
    click
    and
    touchend
    (cannot rely solely on click, 300ms delay on mobile)
  • CSS
    env(safe-area-inset-bottom)
    to handle iPhone bottom safe area
  • Disable two-finger zoom:
    touch-action: manipulation
  • Disable long-press menu:
    -webkit-touch-callout: none; user-select: none
    (only for game UI elements)

AI 素材生成(使用
bl
CLI,⚠️ 必须下载到本地)

AI Asset Generation (Using
bl
CLI, ⚠️ Must Download Locally)

使用 Shell 脚本
scripts/generate-assets.sh
调用
bl
CLI 生成并下载素材。
Use the Shell script
scripts/generate-assets.sh
to call the
bl
CLI to generate and download assets.

视频素材 —
bl video generate
/
bl video ref

Video Assets —
bl video generate
/
bl video ref

bash
undefined
bash
undefined

角色立绘(有参考图 → image-to-video)

Character portrait (with reference image → image-to-video)

bl video generate
--image ./references/ye_wenjie.png
--prompt "一位穿着军绿色工装的年轻女性,目光坚定地注视远方,微风吹动发丝,半身特写"
--resolution 720P --duration 5 --watermark false
--download public/assets/portraits/ye_wenjie.mp4
bl video generate
--image ./references/ye_wenjie.png
--prompt "一位穿着军绿色工装的年轻女性,目光坚定地注视远方,微风吹动发丝,半身特写"
--resolution 720P --duration 5 --watermark false
--download public/assets/portraits/ye_wenjie.mp4

角色立绘(无参考图 → text-to-video)

Character portrait (without reference image → text-to-video)

bl video generate
--prompt "一位穿着深色西装的中年物理学家,站在大学讲台前,表情严肃,半身特写"
--resolution 720P --duration 5 --watermark false
--download public/assets/portraits/physicist.mp4
bl video generate
--prompt "一位穿着深色西装的中年物理学家,站在大学讲台前,表情严肃,半身特写"
--resolution 720P --duration 5 --watermark false
--download public/assets/portraits/physicist.mp4

过场动画

Cutscene animation

bl video generate
--prompt "红岸基地,巨大的抛物面天线在暴风雨中矗立,闪电照亮天线轮廓,镜头从远景推近,电影级画质"
--resolution 1080P --duration 5 --watermark false
--download public/assets/cutscenes/red_coast_storm.mp4
bl video generate
--prompt "红岸基地,巨大的抛物面天线在暴风雨中矗立,闪电照亮天线轮廓,镜头从远景推近,电影级画质"
--resolution 1080P --duration 5 --watermark false
--download public/assets/cutscenes/red_coast_storm.mp4

多角色一致性场景(bl video ref)

Multi-character consistent scene (bl video ref)

bl video ref
--prompt "Image1 和 Image2 在实验室中争论,Image1 愤怒地拍桌子,Image2 冷静地注视"
--image ./references/character_a.png
--image ./references/character_b.png
--resolution 1080P --duration 5 --watermark false
--download public/assets/cutscenes/lab_argument.mp4
undefined
bl video ref
--prompt "Image1 和 Image2 在实验室中争论,Image1 愤怒地拍桌子,Image2 冷静地注视"
--image ./references/character_a.png
--image ./references/character_b.png
--resolution 1080P --duration 5 --watermark false
--download public/assets/cutscenes/lab_argument.mp4
undefined

图片素材 —
bl image generate

Image Assets —
bl image generate

bash
undefined
bash
undefined

角色立绘(静态图片)

Character portrait (static image)

bl image generate
--prompt "一位穿着军绿色工装的年轻中国女性,目光坚定,黑色短发,70年代风格,半身肖像画,电影光影"
--size 768*1024 --watermark false
--out-dir public/assets/portraits/ --out-prefix ye_wenjie
bl image generate
--prompt "一位穿着军绿色工装的年轻中国女性,目光坚定,黑色短发,70年代风格,半身肖像画,电影光影"
--size 768*1024 --watermark false
--out-dir public/assets/portraits/ --out-prefix ye_wenjie

场景背景图

Scene background image

bl image generate
--prompt "1960年代中国大学校园,苏式建筑,红色标语横幅,秋天的梧桐树,灰暗天空,写实油画风格"
--size 1920*1080 --watermark false
--out-dir public/assets/backgrounds/ --out-prefix campus_1967
bl image generate
--prompt "1960年代中国大学校园,苏式建筑,红色标语横幅,秋天的梧桐树,灰暗天空,写实油画风格"
--size 1920*1080 --watermark false
--out-dir public/assets/backgrounds/ --out-prefix campus_1967

过场 CG(静态图 + 游戏内 Ken Burns 动效)

Cutscene CG (static image + in-game Ken Burns effect)

bl image generate
--prompt "红岸基地全景,巨大的抛物面天线矗立在山顶,黄昏,金色光芒,电影级构图"
--size 1920*1080 --watermark false
--out-dir public/assets/cutscenes/ --out-prefix red_coast_base
undefined
bl image generate
--prompt "红岸基地全景,巨大的抛物面天线矗立在山顶,黄昏,金色光芒,电影级构图"
--size 1920*1080 --watermark false
--out-dir public/assets/cutscenes/ --out-prefix red_coast_base
undefined

TTS 旁白 —
bl speech synthesize
(可选)

TTS Narration —
bl speech synthesize
(Optional)

bash
undefined
bash
undefined

场景旁白

Scene narration

bl speech synthesize
--text "1967年,中国大地上,一场浩劫正在改变无数人的命运。"
--voice longxiaochun --language zh
--out public/assets/narrations/scene_opening.mp3
bl speech synthesize
--text "1967年,中国大地上,一场浩劫正在改变无数人的命运。"
--voice longxiaochun --language zh
--out public/assets/narrations/scene_opening.mp3

角色独白

Character monologue

bl speech synthesize
--text "叶文洁按下了发射按钮,那一刻,她做出了一个改变人类文明命运的决定。"
--voice longxiaochun --language zh
--instruction "用沉重而悲伤的语气"
--out public/assets/narrations/ye_decision.mp3
undefined
bl speech synthesize
--text "叶文洁按下了发射按钮,那一刻,她做出了一个改变人类文明命运的决定。"
--voice longxiaochun --language zh
--instruction "用沉重而悲伤的语气"
--out public/assets/narrations/ye_decision.mp3
undefined

并行生成脚本模式(视频素材提速)

Parallel Generation Script Mode (Speed Up Video Assets)

视频生成每个耗时 2-5 分钟,串行太慢。用
--async
并行提交再批量下载:
bash
#!/bin/bash
set -e
Each video generation takes 2-5 minutes, serial generation is too slow. Use
--async
to submit in parallel then download in batches:
bash
#!/bin/bash
set -e

并行提交所有视频生成任务

Submit all video generation tasks in parallel

declare -A TASKS # name → task_id
echo "=== 提交视频生成任务 ===" for name in "${!PORTRAITS[@]}"; do outfile="public/assets/portraits/${name}.mp4" [ -f "$outfile" ] && echo " [跳过] $name" && continue
echo " [提交] $name ..." tid=$(bl video generate
--prompt "${PORTRAITS[$name]}"
--resolution 720P --duration 5 --watermark false
--async --output json | jq -r '.taskId') TASKS[$name]="$tid" echo " [任务] $name → $tid" done
declare -A TASKS # name → task_id
echo "=== Submit Video Generation Tasks ===" for name in "${!PORTRAITS[@]}"; do outfile="public/assets/portraits/${name}.mp4" [ -f "$outfile" ] && echo " [Skipped] $name" && continue
echo " [Submitting] $name ..." tid=$(bl video generate
--prompt "${PORTRAITS[$name]}"
--resolution 720P --duration 5 --watermark false
--async --output json | jq -r '.taskId') TASKS[$name]="$tid" echo " [Task] $name → $tid" done

等待并下载所有任务

Wait and download all tasks

echo "=== 下载视频 ===" for name in "${!TASKS[@]}"; do echo " [下载] $name (task: ${TASKS[$name]}) ..." bl video download --task-id "${TASKS[$name]}" --out "public/assets/portraits/${name}.mp4" echo " [完成] $name" done
undefined
echo "=== Download Videos ===" for name in "${!TASKS[@]}"; do echo " [Downloading] $name (task: ${TASKS[$name]}) ..." bl video download --task-id "${TASKS[$name]}" --out "public/assets/portraits/${name}.mp4" echo " [Completed] $name" done
undefined

关键规则

Key Rules

  • 素材必须离线生成并下载到本地:游戏运行时零 API 调用
  • 本地文件路径直接传入
    --image
    bl
    CLI 自动上传到临时存储,无需手动上传
  • 已生成素材自动跳过:脚本检查本地文件是否存在(
    [ -f path ]
  • generated-assets.json 只存本地路径:如
    /assets/portraits/ye_wenjie.mp4
    ,绝不存远程 URL
  • 视频用
    --async
    并行提交
    :3-5 个并发,避免串行等待
  • Assets must be generated offline and downloaded locally: Zero API calls during game runtime
  • Local file paths directly passed to
    --image
    :
    bl
    CLI automatically uploads to temporary storage, no manual upload required
  • Auto-skip already generated assets: Script checks if local files exist (
    [ -f path ]
    )
  • generated-assets.json only stores local paths: e.g.,
    /assets/portraits/ye_wenjie.mp4
    , never remote URLs
  • Use
    --async
    for parallel video submission
    : 3-5 concurrent tasks to avoid serial waiting

Web Audio API 程序化音乐

Web Audio API Procedural Music

  • 用 MIDI 音高数组定义旋律乐句,循环播放
  • 多声部叠加(主旋律 + 去谐波 detune + pad 持续音)
  • 卷积混响(用随机衰减 impulse buffer)
  • ADSR 包络(attack-decay-sustain-release)
  • 低通滤波器随时间衰减
  • 不同场景/氛围用不同配置(bpm、音阶、波形、滤波频率)
  • Define melody phrases with MIDI pitch arrays, loop playback
  • Multi-voice overlay (main melody + detuned harmonics + sustained pad)
  • Convolution reverb (with random decay impulse buffer)
  • ADSR envelope (attack-decay-sustain-release)
  • Low-pass filter decays over time
  • Different configurations (bpm, scale, waveform, filter frequency) for different scenes/atmospheres

音效

Sound Effects

  • 打字音:白噪声脉冲 + 带通滤波(2000-4000Hz)+ 微弱正弦下降音,模拟机械击键
  • 点击音:双音方波上行(660→880Hz)
  • 场景切换:四音正弦琶音 + 混响
  • 档案解锁:扫频 + 四音三角波和弦

  • Typing sound: White noise pulse + band-pass filter (2000-4000Hz) + weak sine descending tone, simulating mechanical key strikes
  • Click sound: Dual-tone square wave ascending (660→880Hz)
  • Scene switch: Four-tone sine arpeggio + reverb
  • Archive unlock: Sweep + four-tone triangle wave chord

第六步:开发流程

Step 6: Development Process

按以下顺序执行,每步完成后标记 task:
  1. 初始化 React 项目 + 目录结构
  2. 编写 story.js(所有场景文本、选择、分支)— 这是最大的工作量
  3. 编写 characters.js 和 archives.js
  4. 实现主题样式(CSS 变量、字体、配色、动画、移动端适配)
  5. 实现核心组件:TypeWriter → GameScene → ChoicePanel → CharacterPortrait(支持视频/图片)
  6. 实现 CutScene(支持视频/图片 Ken Burns)
  7. 实现 TitleScreen + EndingScreen + ArchivePanel + SaveLoadPanel
  8. 实现 useGameState(reducer + hash 路由 + localStorage 存档)
  9. 实现 useAudio(BGM 乐谱 + 音效 + 可选 TTS 旁白播放)
  10. 如需 AI 素材:编写
    scripts/generate-assets.sh
    → 执行生成并下载
  11. 实现特殊场景效果(Canvas 动态背景、点击交互等)
  12. 整合 App.jsx
  13. 启动 dev server,浏览器测试完整流程(至少走通两条路线到不同结局)
  14. 移动端测试(用 Chrome DevTools 模拟手机视口 + 触控)

Execute in the following order, mark tasks as completed after each step:
  1. Initialize React project + directory structure
  2. Write story.js (all scene text, choices, branches) — This is the largest workload
  3. Write characters.js and archives.js
  4. Implement theme styles (CSS variables, fonts, color schemes, animations, mobile adaptation)
  5. Implement core components: TypeWriter → GameScene → ChoicePanel → CharacterPortrait (supports video/image)
  6. Implement CutScene (supports video/image Ken Burns)
  7. Implement TitleScreen + EndingScreen + ArchivePanel + SaveLoadPanel
  8. Implement useGameState (reducer + hash routing + localStorage save system)
  9. Implement useAudio (BGM sheet music + sound effects + optional TTS narration playback)
  10. If AI assets are needed: Write
    scripts/generate-assets.sh
    → execute generation and download
  11. Implement special scene effects (Canvas dynamic background, click interactions, etc.)
  12. Integrate App.jsx
  13. Start dev server, test complete flow in browser (at least walk through two routes to different endings)
  14. Mobile testing (simulate mobile viewport + touch with Chrome DevTools)

避坑指南

Pitfall Avoidance Guide

以下是从实际开发中总结的经验,务必遵循:
  • bl video 分辨率
    bl video generate
    支持 720P 和 1080P,测试阶段用 720P(更快更便宜),最终版用 1080P
  • bl video --download 自动轮询
    --download
    标志会自动等待任务完成并下载文件,无需手写轮询代码;批量生成时用
    --async
    +
    bl video download
    并行提速
  • bl image generate 尺寸:用
    --size
    指定,格式为
    宽*高
    (如
    1920*1080
    ),也支持比例格式(如
    16:9
  • 本地路径自动上传
    bl
    CLI 的
    --image
    接受本地文件路径,会自动上传到临时存储(48小时有效),无需手动上传
  • 素材必须离线生成并下载到本地:视频生成耗时 2-5 分钟/个,绝不能在游戏运行时调用。下载到
    public/assets/
    ,generated-assets.json 中只存本地路径
  • Prompt 内容审核:避免暴力、吸烟等敏感描述,否则会被 API 拒绝;换温和表述重试
  • React Hooks 顺序:所有 useCallback/useEffect 必须在 early return 之前调用,否则报 rules-of-hooks 错误
  • BGM 编曲:用固定 MIDI 乐谱数组循环播放,不要用随机音符漫游(听起来像噪声)
  • 打字音质感:用噪声脉冲 + 带通滤波模拟击键,比纯方波 beep 好很多
  • 特殊场景视觉:Canvas 动态背景必须与叙事内容紧密关联(出现什么元素画什么),不能泛泛画星空了事
  • 点击交互:Canvas 场景加粒子爆发 + 冲击波环 + 屏幕震动 + 主题相关额外效果,大幅提升沉浸感
  • CRA 清理:初始化后立即删除 App.css/logo.svg/setupTests.js 等样板文件,避免冲突
  • 移动端字体:正文最小 16px,否则 iOS Safari 会自动缩放页面
  • 视频内存泄漏:离开场景时必须
    video.pause(); video.removeAttribute('src'); video.load()
    释放内存
  • 避免伪分支:若两个选项的
    next
    指向同一场景且仅 flag 不同,玩家会感觉「选了没用」。重大选择必须
    next
    到不同场景,或至少在同场景用 flag 触发明显不同的文本/立绘
  • 合流点保留差异感:分叉汇合后,至少在合流场景的文本/旁白中体现玩家之前的选择(读 flag 渲染条件文本),否则分叉毫无意义
  • 分支深度要够:每个重大分支至少影响 2-3 个后续场景,仅分叉一个场景就立刻合流会让玩家觉得「选了也就多看一句话」
The following are lessons learned from actual development, please follow strictly:
  • bl video resolution:
    bl video generate
    supports 720P and 1080P, use 720P during testing (faster and cheaper), use 1080P for the final version
  • bl video --download auto-polling: The
    --download
    flag automatically waits for task completion and downloads the file, no need to write polling code manually; use
    --async
    +
    bl video download
    for parallel speedup during batch generation
  • bl image generate size: Specify with
    --size
    , format is
    width*height
    (e.g.,
    1920*1080
    ), also supports ratio format (e.g.,
    16:9
    )
  • Local path auto-upload: The
    --image
    parameter of
    bl
    CLI accepts local file paths, which will be automatically uploaded to temporary storage (valid for 48 hours), no manual upload required
  • Assets must be generated offline and downloaded locally: Video generation takes 2-5 minutes per asset, never call during game runtime. Download to
    public/assets/
    , only store local paths in generated-assets.json
  • Prompt content review: Avoid sensitive descriptions such as violence, smoking, etc., otherwise the API will reject it; retry with milder wording
  • React Hooks order: All useCallback/useEffect must be called before early return, otherwise rules-of-hooks errors will occur
  • BGM arrangement: Use fixed MIDI sheet music arrays for loop playback, do not use random note roaming (sounds like noise)
  • Typing sound quality: Use noise pulse + band-pass filter to simulate key strikes, much better than pure square wave beeps
  • Special scene visuals: Canvas dynamic backgrounds must be closely related to narrative content (draw what appears), do not just draw starry sky generically
  • Click interactions: Add particle bursts + shockwave rings + screen shake + theme-related additional effects to Canvas scenes to greatly enhance immersion
  • CRA cleanup: Immediately delete boilerplate files like App.css/logo.svg/setupTests.js after initialization to avoid conflicts
  • Mobile fonts: Minimum 16px for body text, otherwise iOS Safari will automatically scale the page
  • Video memory leak: Must execute
    video.pause(); video.removeAttribute('src'); video.load()
    to release memory when leaving the scene
  • Avoid fake branches: If two options'
    next
    point to the same scene with only flag differences, players will feel "choosing is useless". Major choices must
    next
    to different scenes, or at least trigger significantly different text/portraits in the same scene with flags
  • Retain sense of difference at convergence points: After branches converge, at least reflect the player's previous choices in the text/narration of the convergence scene (read flags to render conditional text), otherwise branching is meaningless
  • Sufficient branch depth: Each major branch should affect at least 2-3 subsequent scenes; branching only one scene then immediately converging will make players feel "choosing only adds one more sentence"