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
This skill relies on the Alibaba Cloud Bailian CLI (
) for AI asset generation (videos/images/voices). Before use, check:
If not installed, refer to the installation documentation:
https://bailian.aliyun.com/cli/install.md
Step 1: Requirements Gathering
Use AskUserQuestion to confirm the following key design decisions with the user (ask all at once):
- Asset Source — Has the user provided a novel file (EPUB/TXT)? If yes, first read the content to extract the story structure.
- Game Type — Interactive Novel (choices affect storyline) / Text Adventure + Puzzle / Text RPG (with attribute system)
- UI Style — Default : 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.
- Narrative Perspective — First-person (play as the protagonist) / Third-person omniscient (旁观者选择) / Dual-protagonist switching
- 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
- 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 + for narration voice)
- Game Duration — 15-20 minutes (8-10 scenes) / 30-45 minutes (15-18 scenes) / 1+ hour (25+ scenes)
UI Style Auto-Inference Table ( Mode)
Determine based on novel genre keywords, and explain the reasoning in the Step 2 deliverables:
| Genre Keywords | Inferred Style | Visual Characteristics |
|---|
| Sci-fi / Future / Space / AI / Post-apocalypse | Cyberpunk | Cool tones, neon highlights, monospaced fonts, scanlines |
| Ancient Chinese style / Wuxia / Xianxia / History / Palace | Ink Chinese Style | Ink colors, negative space, serif fonts, ink wash effects |
| Retro / 8bit / Relaxing / Healing / Funny | Pixel Style | Low saturation, pixel fonts, CRT scanlines |
| Modern / Urban / Realism / Suspense / Workplace | Minimalist Modern | Neutral colors, sans-serif fonts, flat design |
For mixed genres, take the dominant one; default to "Minimalist Modern" if undetermined.
Step 2: Story Design
Based on the original work/assets/user description, directly design the following:
- Core Storylines — Identify 1-3 main storylines (can intersect), sort out key scenes for each line
- Key Branch Points — Select 3-5 major choices that affect the ending. Choices mean branching (see Branching Principles below)
- Ending Design — Design 3-5 different endings, each determined by a combination of flags
- Character List — List main characters requiring portraits (6-8 characters), including appearance descriptions
- Cutscene Scenes — List climax scenes requiring asset generation (5-8 scenes), including scene descriptions
- Collectibles/Archives — Design background knowledge entries unlocked through choices
- UI Style Inference (in mode) — Determine the style based on genre and explain the reasoning
- 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)
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 ), 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 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
Initialize with
, 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 Scene Data Structure
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 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" }
}
}
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,
}
Step 5: Key Implementation Patterns
Typewriter Effect (TypeWriter)
- 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
Choice Panel (ChoicePanel)
- 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 Routing
- URL hash syncs with current scene:
- 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
Save System (localStorage)
- Auto-save: Automatically save current state to
localStorage.setItem('novel_autosave', JSON.stringify(state))
after each choice
- Manual save: Support 3 save slots ( / / ), 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 }
Character Portrait (CharacterPortrait) — Auto-Adapt to Video/Image
- Read asset information from , render based on field:
- →
<video src={path} autoPlay loop muted playsInline />
- → + optional breathing effect (CSS
animation: breathe 3s ease-in-out infinite
)
- Display character initial placeholder if no asset exists
Cutscene Playback (CutScene) — Auto-Adapt to Video/Image
- → Full-screen playback, auto-close after completion
- → Full-screen + 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
- All video elements default to , no preloading
- Only preload assets for current scene and next possible scenes
- When switching scenes: to load current → to preload next
- When leaving scene:
video.pause(); video.removeAttribute('src'); video.load()
to release memory
- Images use attribute
Mobile Adaptation
<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 and (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 Asset Generation (Using CLI, ⚠️ Must Download Locally)
Use the Shell script
scripts/generate-assets.sh
to call the
CLI to generate and download assets.
Video Assets — /
bash
# 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
# Character portrait (without reference image → text-to-video)
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
# 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
Image Assets —
bash
# Character portrait (static image)
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
# 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
TTS Narration — (Optional)
bash
# Scene narration
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
Parallel Generation Script Mode (Speed Up Video Assets)
Each video generation takes 2-5 minutes, serial generation is too slow. Use
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 "=== 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 "=== 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
Key Rules
- Assets must be generated offline and downloaded locally: Zero API calls during game runtime
- Local file paths directly passed to : CLI automatically uploads to temporary storage, no manual upload required
- Auto-skip already generated assets: Script checks if local files exist ()
- generated-assets.json only stores local paths: e.g.,
/assets/portraits/ye_wenjie.mp4
, never remote URLs
- Use for parallel video submission: 3-5 concurrent tasks to avoid serial waiting
Web Audio API Procedural Music
- 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
- 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
Execute in the following order, mark tasks as completed after each step:
- Initialize React project + directory structure
- Write story.js (all scene text, choices, branches) — This is the largest workload
- Write characters.js and archives.js
- Implement theme styles (CSS variables, fonts, color schemes, animations, mobile adaptation)
- Implement core components: TypeWriter → GameScene → ChoicePanel → CharacterPortrait (supports video/image)
- Implement CutScene (supports video/image Ken Burns)
- Implement TitleScreen + EndingScreen + ArchivePanel + SaveLoadPanel
- Implement useGameState (reducer + hash routing + localStorage save system)
- Implement useAudio (BGM sheet music + sound effects + optional TTS narration playback)
- If AI assets are needed: Write
scripts/generate-assets.sh
→ execute generation and download
- Implement special scene effects (Canvas dynamic background, click interactions, etc.)
- Integrate App.jsx
- Start dev server, test complete flow in browser (at least walk through two routes to different endings)
- Mobile testing (simulate mobile viewport + touch with Chrome DevTools)
Pitfall Avoidance Guide
The following are lessons learned from actual development, please follow strictly:
- bl video resolution: supports 720P and 1080P, use 720P during testing (faster and cheaper), use 1080P for the final version
- bl video --download auto-polling: The flag automatically waits for task completion and downloads the file, no need to write polling code manually; use + for parallel speedup during batch generation
- bl image generate size: Specify with , format is (e.g., ), also supports ratio format (e.g., )
- Local path auto-upload: The parameter of 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 , 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' point to the same scene with only flag differences, players will feel "choosing is useless". Major choices must 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"