Loading...
Loading...
Adapt novels/stories into interactive novel web games (React SPA), featuring AI-generated assets (optional videos/images), optional TTS narration, procedural audio, branching story engine, and save system. Use the `bl` CLI for asset generation (`bl video generate` / `bl image generate` / `bl speech synthesize`). Activate when users mention interactive novels, text adventure games, novel-adapted games, H5 interactive games, branching storylines, or visual novels.
npx skill4agent add modelstudioai/skills novel-gameblbl --versionautobl speech synthesizeauto| 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 |
autonextnextRed 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)npx create-react-appsrc/
├── 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)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
}{
"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" }
}
}{
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,
}#scene_idlocalStorage.setItem('novel_autosave', JSON.stringify(state))novel_save_1novel_save_2novel_save_3{ state, savedAt, sceneTitle, playTime }generated-assets.jsontypetype: "video"<video src={path} autoPlay loop muted playsInline />type: "image"<img src={path} />animation: breathe 3s ease-in-out infinitetype: "video"<video>type: "image"<img>animation: kenburns 5s ease-in-outscale(1.1) translate(-2%, -2%)scale(1.0) translate(0, 0)<video preload="none">video.load()requestIdleCallbackvideo.pause(); video.removeAttribute('src'); video.load()loading="lazy"<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">clicktouchendenv(safe-area-inset-bottom)touch-action: manipulation-webkit-touch-callout: none; user-select: noneblscripts/generate-assets.shblbl video generatebl video ref# 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.mp4bl image generate# 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_basebl speech synthesize# 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--async#!/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--imagebl[ -f path ]/assets/portraits/ye_wenjie.mp4--asyncscripts/generate-assets.shbl video generate--download--asyncbl video download--sizewidth*height1920*108016:9--imageblpublic/assets/video.pause(); video.removeAttribute('src'); video.load()nextnext