mirror of
https://github.com/modelstudioai/skills.git
synced 2026-09-19 04:50:42 +08:00
feat(novel-game): add image/TTS/save/mobile support, remove bl text chat
- Asset mode: video / image (`bl image generate`) / mixed — user chooses - Optional TTS narration via `bl speech synthesize` - Save system: auto-save + 3 manual slots (localStorage) - Mobile-first: portrait layout, touch events, safe-area, 44px targets - Video lazy loading: preload=none, memory release on scene exit - Parallel asset generation: --async + bl video download batch mode - Ken Burns effect for image-mode cutscenes - Remove bl text chat — agent itself handles story generation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+17
-13
@@ -3,20 +3,22 @@
|
||||
> [中文版 / Chinese →](README.zh.md)
|
||||
|
||||
Turn novels and stories into browser-based interactive fiction games
|
||||
(React SPA) — with AI-generated video portraits, cutscenes,
|
||||
(React SPA) — with AI-generated video/image assets, optional TTS narration,
|
||||
programmatic audio, and branching storylines.
|
||||
|
||||
## Features
|
||||
|
||||
- **Branching narrative engine** — multiple endings, flags system, collectible archives
|
||||
- **AI asset generation** — character portraits and cutscene videos via `bl video generate` / `bl video ref`
|
||||
- **LLM-assisted writing** — plot extraction, scene text generation, video prompt authoring via `bl text chat`
|
||||
- **AI asset generation** — character portraits and cutscenes via video (`bl video generate` / `bl video ref`) or image (`bl image generate`), mixable
|
||||
- **Optional TTS narration** — scene narration and character monologues via `bl speech synthesize`
|
||||
- **Programmatic audio** — BGM and SFX generated entirely with Web Audio API, zero external dependencies
|
||||
- **Save system** — auto-save + 3 manual save slots (localStorage)
|
||||
- **Mobile-first** — portrait layout, touch-optimized, safe-area support
|
||||
- **Multiple UI themes** — pixel art / cyberpunk / ink-wash Chinese / minimal modern
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Alibaba Cloud Model Studio CLI (`bl`)](https://bailian.aliyun.com/cli/install.md) — AI asset generation and LLM calls
|
||||
- [Alibaba Cloud Model Studio CLI (`bl`)](https://bailian.aliyun.com/cli/install.md) — AI asset generation (video/image/speech)
|
||||
- Node.js + npx — React project initialization
|
||||
|
||||
## Usage
|
||||
@@ -28,30 +30,32 @@ In Claude Code (or any skill-aware agent), type:
|
||||
```
|
||||
|
||||
The skill will:
|
||||
1. Ask key design decisions (game type, UI style, length, AI assets)
|
||||
1. Ask key design decisions (game type, UI style, length, asset mode)
|
||||
2. Design the story structure with branching choices
|
||||
3. Scaffold a React project with all components
|
||||
4. Generate AI video assets (portraits + cutscenes) via `bl` CLI
|
||||
4. Generate AI assets (video/image/speech) via `bl` CLI
|
||||
5. Launch a dev server for browser testing
|
||||
|
||||
## Project structure (generated)
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # UI components (TypeWriter, GameScene, ChoicePanel, ...)
|
||||
├── data/ # Story graph, characters, archives
|
||||
├── hooks/ # useGameState (reducer + hash routing), useAudio (Web Audio)
|
||||
├── components/ # UI components (TypeWriter, GameScene, ChoicePanel, SaveLoadPanel, ...)
|
||||
├── data/ # Story graph, characters, archives, asset index
|
||||
├── hooks/ # useGameState (reducer + hash routing + save), useAudio (Web Audio)
|
||||
└── styles/ # Theme CSS (cyberpunk, pixel, ink-wash, ...)
|
||||
scripts/
|
||||
└── generate-assets.sh # bl CLI asset generation script
|
||||
public/assets/ # Downloaded AI-generated video files
|
||||
└── generate-assets.sh # bl CLI asset generation script (supports parallel generation)
|
||||
public/assets/ # Downloaded AI-generated asset files
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
- **Story data** (`story.js`) defines a scene graph — each scene has text segments, choices with flag mutations, and optional cutscene/archive triggers
|
||||
- **Game state** is managed via `useReducer` with hash-based routing for scene navigation
|
||||
- **Character portraits** are 5-second looping videos (720P) displayed in a sidebar; fallback to text initials when videos aren't available
|
||||
- **Cutscenes** are 5-second 1080P videos played as full-screen overlays at key story moments
|
||||
- **Character portraits** support video (5-second looping 720P) or image (with CSS breathing animation), auto-detected
|
||||
- **Cutscenes** support video (full-screen playback) or image (Ken Burns pan/zoom effect), auto-detected
|
||||
- **Save system** — auto-save after each choice + 3 manual save slots in localStorage
|
||||
- **BGM** uses fixed MIDI pitch arrays with multi-voice layering, convolution reverb, and ADSR envelopes
|
||||
- **SFX** includes noise-pulse typing sounds, sweep + chord archive unlocks, and arpeggio scene transitions
|
||||
- **TTS narration** (optional) — pre-generated MP3 files play automatically during scenes
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
# novel-game
|
||||
|
||||
> [English version →](README.md)
|
||||
> [English →](README.md)
|
||||
|
||||
将小说/故事改编为浏览器端的 H5 互动小说游戏(React SPA)——
|
||||
含 AI 生成的动态角色立绘、过场动画、程序化音频和分支剧情引擎。
|
||||
含 AI 生成视频/图片素材、可选 TTS 旁白、程序化音频和分支剧情引擎。
|
||||
|
||||
## 功能
|
||||
|
||||
- **分支剧情引擎** — 多结局、flags 系统、档案收集
|
||||
- **AI 素材生成** — 通过 `bl video generate` / `bl video ref` 生成角色立绘和过场动画
|
||||
- **LLM 辅助创作** — 通过 `bl text chat` 辅助剧情提取、场景文本生成、视频 prompt 编写
|
||||
- **程序化音频** — Web Audio API 生成 BGM 和音效,零外部依赖
|
||||
- **AI 素材生成** — 角色立绘和过场 CG,支持视频 (`bl video generate` / `bl video ref`) 或图片 (`bl image generate`),可混合使用
|
||||
- **可选 TTS 旁白** — 通过 `bl speech synthesize` 生成场景旁白和角色独白
|
||||
- **程序化音频** — BGM 和音效完全用 Web Audio API 生成,零外部依赖
|
||||
- **存档系统** — 自动存档 + 3 个手动存档槽位(localStorage)
|
||||
- **移动端适配** — 竖屏优先、触控优化、安全区适配
|
||||
- **多种 UI 风格** — 像素风 / 赛博朋克 / 水墨中国风 / 简约现代
|
||||
|
||||
## 依赖
|
||||
|
||||
- [阿里云百炼 CLI (`bl`)](https://bailian.aliyun.com/cli/install.md) — AI 素材生成和 LLM 调用
|
||||
- [阿里云百炼 CLI (`bl`)](https://bailian.aliyun.com/cli/install.md) — AI 素材生成(视频/图片/语音)
|
||||
- Node.js + npx — React 项目初始化
|
||||
|
||||
## 使用
|
||||
@@ -23,34 +25,36 @@
|
||||
在 Claude Code(或其他支持 skill 的 agent)中输入:
|
||||
|
||||
```
|
||||
/novel-game 把《三体》改编为互动小说游戏,赛博朋克风格,30分钟时长
|
||||
/novel-game 把《三体》地球往事改编为互动小说,水墨风格,30分钟时长
|
||||
```
|
||||
|
||||
skill 会自动:
|
||||
1. 确认关键设计决策(游戏类型、UI 风格、时长、AI 素材)
|
||||
Skill 将会:
|
||||
1. 询问关键设计决策(游戏类型、UI 风格、时长、素材模式)
|
||||
2. 设计分支剧情结构
|
||||
3. 搭建 React 项目及全部组件
|
||||
4. 通过 `bl` CLI 生成 AI 视频素材(立绘 + 过场动画)
|
||||
3. 初始化 React 项目并实现所有组件
|
||||
4. 通过 `bl` CLI 生成 AI 素材(视频/图片/语音)
|
||||
5. 启动 dev server 在浏览器中测试
|
||||
|
||||
## 生成的项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # UI 组件(TypeWriter、GameScene、ChoicePanel 等)
|
||||
├── data/ # 场景图、角色数据、档案数据
|
||||
├── hooks/ # useGameState(reducer + hash 路由)、useAudio(Web Audio)
|
||||
└── styles/ # 主题样式(赛博朋克、像素风、水墨风等)
|
||||
├── components/ # UI 组件(TypeWriter、GameScene、ChoicePanel、SaveLoadPanel...)
|
||||
├── data/ # 场景图、角色、档案、素材索引
|
||||
├── hooks/ # useGameState(reducer + hash 路由 + 存档)、useAudio(Web Audio)
|
||||
└── styles/ # 主题 CSS(赛博朋克、像素风、水墨、简约...)
|
||||
scripts/
|
||||
└── generate-assets.sh # bl CLI 素材生成脚本
|
||||
public/assets/ # 下载到本地的 AI 生成视频文件
|
||||
└── generate-assets.sh # bl CLI 素材生成脚本(支持并行生成)
|
||||
public/assets/ # 下载到本地的 AI 生成素材文件
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
- **场景数据**(`story.js`)定义场景图——每个场景包含文本段落、带 flag 变更的选择项、可选的过场动画/档案触发
|
||||
- **游戏状态** 通过 `useReducer` 管理,结合 hash 路由实现场景导航
|
||||
- **角色立绘** 为 5 秒循环视频(720P),在侧边栏展示;视频不可用时回退为文字首字占位
|
||||
- **过场动画** 为 5 秒 1080P 视频,在关键剧情节点以全屏覆盖层播放
|
||||
- **BGM** 使用固定 MIDI 音高数组,多声部叠加、卷积混响、ADSR 包络
|
||||
- **音效** 包含噪声脉冲打字音、扫频+和弦档案解锁音、琶音场景转场音
|
||||
- **场景数据** (`story.js`) 定义场景图——每个场景含文本段落、带 flag 变更的分支选择、可选的过场/档案触发
|
||||
- **游戏状态** 通过 `useReducer` 管理,hash 路由实现场景导航
|
||||
- **角色立绘** 支持视频(5秒循环 720P)或图片(带呼吸动效),组件自动适配
|
||||
- **过场** 支持视频(全屏播放)或图片(Ken Burns 缓动效果),自动适配
|
||||
- **存档** 自动存档 + 3 个手动槽位,存入 localStorage
|
||||
- **BGM** 使用固定 MIDI 音高数组 + 多声部叠加 + 卷积混响 + ADSR 包络
|
||||
- **音效** 包括噪声脉冲打字音、扫频档案解锁音、琶音场景切换音
|
||||
- **TTS 旁白**(可选)预生成的 MP3 文件在场景播放时自动播放
|
||||
|
||||
+203
-185
@@ -1,10 +1,10 @@
|
||||
---
|
||||
name: novel-game
|
||||
description: >-
|
||||
将小说/故事改编为互动小说网页游戏(React SPA),含 AI 生成素材、程序化音频、分支剧情引擎。
|
||||
使用 `bl` CLI 完成视频素材生成(`bl video generate` / `bl video ref`)和
|
||||
LLM 辅助剧情设计(`bl text chat`)。当用户提到互动小说、文字冒险游戏、
|
||||
小说改编游戏、H5 互动游戏、分支剧情、视觉小说时激活。
|
||||
将小说/故事改编为互动小说网页游戏(React SPA),含 AI 生成素材(视频/图片可选)、
|
||||
可选 TTS 旁白、程序化音频、分支剧情引擎、存档系统。
|
||||
使用 `bl` CLI 完成素材生成(`bl video generate` / `bl image generate` / `bl speech synthesize`)。
|
||||
当用户提到互动小说、文字冒险游戏、小说改编游戏、H5 互动游戏、分支剧情、视觉小说时激活。
|
||||
---
|
||||
|
||||
你是一个专业的游戏策划兼全栈开发者,擅长将小说或故事改编为浏览器端的互动小说游戏。
|
||||
@@ -15,7 +15,7 @@ description: >-
|
||||
|
||||
## 前置依赖
|
||||
|
||||
本技能依赖阿里云百炼 CLI(`bl`)进行 AI 素材生成和 LLM 辅助创作。使用前请检查:
|
||||
本技能依赖阿里云百炼 CLI(`bl`)进行 AI 素材生成(视频/图片/语音)。使用前请检查:
|
||||
|
||||
```bash
|
||||
bl --version
|
||||
@@ -33,52 +33,28 @@ bl --version
|
||||
2. **游戏类型** — 互动小说(选择影响剧情) / 文字冒险+解谜 / 文字RPG(含属性系统)
|
||||
3. **UI 风格** — 像素风 / 赛博朋克 / 水墨中国风 / 简约现代
|
||||
4. **叙事视角** — 第一人称(扮演主角) / 第三人称上帝视角(旁观者选择)/ 双主角切换
|
||||
5. **AI 素材生成** — 是否需要 AI 生成角色立绘和过场动画?(使用 `bl video generate` / `bl video ref`)
|
||||
6. **音频** — 无音频 / 仅 BGM / BGM + 音效(全部用 Web Audio API 程序化生成,零外部依赖)
|
||||
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+场景)
|
||||
|
||||
---
|
||||
|
||||
## 第二步:剧情设计
|
||||
|
||||
### 使用 `bl text chat` 辅助剧情提取与设计
|
||||
|
||||
如果用户提供了小说原文,使用 `bl text chat` 提取剧情结构:
|
||||
|
||||
```bash
|
||||
# 从小说文件提取核心剧情线、关键分支点、角色列表
|
||||
bl text chat \
|
||||
--system "你是专业的互动小说策划师,擅长从原著中提取适合改编为互动游戏的剧情结构。" \
|
||||
--message "以下是小说原文:$(cat novel.txt)" \
|
||||
--message "请提取:1) 1-3条主线剧情 2) 3-5个适合做分支选择的关键节点 3) 主要角色列表(含外观描述)4) 3-5个可能的不同结局 5) 适合做过场动画的高潮场景" \
|
||||
--max-tokens 8192
|
||||
```
|
||||
|
||||
```bash
|
||||
# 根据大纲生成具体的场景文本和选择
|
||||
bl text chat \
|
||||
--system "你是互动小说编剧,擅长写引人入胜的场景描写和有意义的分支选择。" \
|
||||
--message "根据以下剧情大纲,为每个场景生成详细的叙事文本和 2-3 个分支选择(含对应 flag 设置):$(cat outline.md)" \
|
||||
--max-tokens 8192
|
||||
```
|
||||
|
||||
```bash
|
||||
# 生成 AI 视频素材的 prompt
|
||||
bl text chat \
|
||||
--system "你是 AI 视频生成的 prompt 专家,擅长写出能产生高质量视频的描述。" \
|
||||
--message "为以下互动小说场景生成视频 prompt(中文,每个 50-100 字,描述画面、动作、氛围):$(cat scenes.md)" \
|
||||
--max-tokens 4096
|
||||
```
|
||||
|
||||
### 剧情结构产出物
|
||||
|
||||
根据原著/素材提取:
|
||||
根据原著/素材/用户描述,直接设计以下内容:
|
||||
|
||||
1. **核心剧情线** — 识别 1-3 条主线(可交织),每条线梳理关键场景
|
||||
2. **关键分支点** — 选出 3-5 个影响结局的重大选择
|
||||
3. **结局设计** — 设计 3-5 个不同结局,每个由 flags 组合决定
|
||||
4. **角色列表** — 列出需要立绘的主要角色(6-8个)
|
||||
5. **过场动画** — 列出需要生成视频的高潮场景(5-8个)
|
||||
4. **角色列表** — 列出需要立绘的主要角色(6-8个),含外观描述
|
||||
5. **过场场景** — 列出需要生成素材的高潮场景(5-8个),含画面描述
|
||||
6. **收集物/档案** — 设计通过选择解锁的背景知识条目
|
||||
|
||||
---
|
||||
@@ -92,26 +68,27 @@ src/
|
||||
├── App.jsx # 主应用,游戏状态路由
|
||||
├── index.js
|
||||
├── components/
|
||||
│ ├── TitleScreen.jsx # 开始画面(主题动画+标题)
|
||||
│ ├── GameScene.jsx # 核心场景渲染(文本+选择+立绘)
|
||||
│ ├── TitleScreen.jsx # 开始画面(主题动画+标题+继续游戏+存档管理)
|
||||
│ ├── GameScene.jsx # 核心场景渲染(文本+选择+立绘+旁白)
|
||||
│ ├── TypeWriter.jsx # 打字机逐字显示效果
|
||||
│ ├── ChoicePanel.jsx # 选择面板(hover动效+延迟入场)
|
||||
│ ├── CharacterPortrait.jsx # 角色立绘(视频/图片/首字占位)
|
||||
│ ├── CutScene.jsx # 过场动画播放
|
||||
│ ├── 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 生成素材的本地路径
|
||||
│ └── generated-assets.json # AI 生成素材的本地路径+类型
|
||||
├── hooks/
|
||||
│ ├── useGameState.js # useReducer 游戏状态管理
|
||||
│ ├── useGameState.js # useReducer 游戏状态管理 + 存档
|
||||
│ └── useAudio.js # Web Audio API 音频系统
|
||||
├── styles/
|
||||
│ ├── pixel.css # 主题样式(根据用户选择调整)
|
||||
│ ├── theme.css # 主题样式(根据用户选择调整)
|
||||
│ ├── animations.css # 动画定义
|
||||
│ └── crt.css # CRT 扫描线效果(像素风专用)
|
||||
└── [特殊场景组件] # 按需:Canvas 动态背景等
|
||||
@@ -119,8 +96,10 @@ scripts/
|
||||
└── generate-assets.sh # bl CLI 素材生成脚本
|
||||
public/
|
||||
└── assets/ # 下载到本地的素材文件
|
||||
├── portraits/
|
||||
└── cutscenes/
|
||||
├── portraits/ # 角色立绘(.mp4 或 .png)
|
||||
├── cutscenes/ # 过场素材(.mp4 或 .png)
|
||||
├── backgrounds/ # 场景背景图(.png)
|
||||
└── narrations/ # TTS 旁白音频(.mp3)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -138,18 +117,19 @@ export const scenes = {
|
||||
year: "1967", // 显示年份
|
||||
character: "character_key", // 当前场景角色立绘
|
||||
bgm: "bgm_name", // 背景音乐
|
||||
cutscene: "cutscene_key", // 过场动画(可选)
|
||||
isEnding: false, // 是否为结局场景
|
||||
endingType: "ending_a", // 结局类型标识
|
||||
texts: [ // 逐段显示的文本
|
||||
cutscene: "cutscene_key", // 过场素材 key(可选)
|
||||
narration: "narration_key", // TTS 旁白 key(可选)
|
||||
isEnding: false,
|
||||
endingType: "ending_a",
|
||||
texts: [
|
||||
"第一段文字...",
|
||||
"第二段文字..."
|
||||
],
|
||||
choices: [ // 最后一段文字后出现的选择
|
||||
choices: [
|
||||
{
|
||||
text: "选择A的文字",
|
||||
next: "next_scene_id", // 跳转目标(null = 根据 flags 进入结局)
|
||||
setFlags: { flag_name: true }, // 设置标记
|
||||
setFlags: { flag_name: true },
|
||||
archive: "archive_key" // 解锁档案(可选)
|
||||
}
|
||||
]
|
||||
@@ -161,20 +141,41 @@ export function getEnding(flags) {
|
||||
}
|
||||
```
|
||||
|
||||
### generated-assets.json 数据结构
|
||||
|
||||
```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 状态结构
|
||||
|
||||
```js
|
||||
{
|
||||
phase: 'title' | 'playing' | 'cutscene' | 'ending',
|
||||
currentScene: string, // 当前场景 ID
|
||||
flags: {}, // 玩家选择累积的标记
|
||||
history: string[], // 已访问场景
|
||||
archives: string[], // 已解锁档案
|
||||
textIndex: number, // 当前场景第几段文本
|
||||
typingDone: boolean, // 打字机是否完成当前段
|
||||
currentScene: string,
|
||||
flags: {},
|
||||
history: string[],
|
||||
archives: string[],
|
||||
textIndex: number,
|
||||
typingDone: boolean,
|
||||
showCutscene: boolean,
|
||||
showArchive: boolean,
|
||||
newArchive: null | string, // 新解锁档案通知
|
||||
newArchive: null | string,
|
||||
bgm: string | null,
|
||||
}
|
||||
```
|
||||
@@ -185,7 +186,7 @@ export function getEnding(flags) {
|
||||
|
||||
### 打字机效果(TypeWriter)
|
||||
- 用 setInterval 逐字显示,speed 约 40-50ms
|
||||
- 点击可跳过(立即显示全文)
|
||||
- 点击/触摸可跳过(立即显示全文)
|
||||
- 每个字符触发打字音效回调
|
||||
- 显示完毕调用 onDone 回调
|
||||
|
||||
@@ -193,7 +194,7 @@ export function getEnding(flags) {
|
||||
- 在最后一段文字打字完成后淡入
|
||||
- 每个选项延迟入场动画(nth-child animation-delay)
|
||||
- hover 时边框变色 + 微位移 + 阴影扩大
|
||||
- 点击触发音效 → 设置 flags → 跳转下一场景
|
||||
- 点击触发音效 → 设置 flags → 自动存档 → 跳转下一场景
|
||||
|
||||
### Hash 路由
|
||||
- URL hash 同步当前场景:`#scene_id`
|
||||
@@ -201,145 +202,158 @@ export function getEnding(flags) {
|
||||
- 监听 hashchange 支持浏览器前进/后退
|
||||
- 回到标题时清除 hash
|
||||
|
||||
### 存档系统(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 }`
|
||||
|
||||
### 角色立绘(CharacterPortrait)— 自动适配视频/图片
|
||||
- 从 `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`)
|
||||
- 无素材时显示角色名首字母占位符
|
||||
|
||||
### 过场播放(CutScene)— 自动适配视频/图片
|
||||
- `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)`,模拟镜头缓慢推拉
|
||||
|
||||
### 素材懒加载
|
||||
- 所有视频元素默认 `<video preload="none">`,不预加载
|
||||
- 仅预加载当前场景和下一可能场景的素材
|
||||
- 场景切换时:`video.load()` 加载当前 → `requestIdleCallback` 预加载下一个
|
||||
- 离开场景时:`video.pause(); video.removeAttribute('src'); video.load()` 释放内存
|
||||
- 图片使用 `loading="lazy"` 属性
|
||||
|
||||
### 移动端适配
|
||||
- `<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 元素)
|
||||
|
||||
### AI 素材生成(使用 `bl` CLI,⚠️ 必须下载到本地)
|
||||
|
||||
使用 Shell 脚本 `scripts/generate-assets.sh` 调用 `bl` CLI 生成并下载素材。`bl video generate` 的 `--download` 标志会自动轮询任务状态直到完成并下载文件,无需手写轮询逻辑。
|
||||
使用 Shell 脚本 `scripts/generate-assets.sh` 调用 `bl` CLI 生成并下载素材。
|
||||
|
||||
#### 角色立绘(有参考图 → image-to-video)
|
||||
#### 视频素材 — `bl video generate` / `bl video ref`
|
||||
|
||||
```bash
|
||||
# 从角色参考图生成动态立绘
|
||||
# 角色立绘(有参考图 → image-to-video)
|
||||
bl video generate \
|
||||
--image ./references/character_a.png \
|
||||
--prompt "一个年轻女子微微转头,长发随风飘动,表情温柔,半身特写,电影质感光影" \
|
||||
--resolution 720P \
|
||||
--duration 5 \
|
||||
--watermark false \
|
||||
--download public/assets/portraits/character_a.mp4
|
||||
```
|
||||
--image ./references/ye_wenjie.png \
|
||||
--prompt "一位穿着军绿色工装的年轻女性,目光坚定地注视远方,微风吹动发丝,半身特写" \
|
||||
--resolution 720P --duration 5 --watermark false \
|
||||
--download public/assets/portraits/ye_wenjie.mp4
|
||||
|
||||
#### 角色立绘(无参考图 → text-to-video)
|
||||
|
||||
```bash
|
||||
# 纯文本描述生成角色立绘
|
||||
# 角色立绘(无参考图 → text-to-video)
|
||||
bl video generate \
|
||||
--prompt "水墨画风格,一位身穿白色长袍的青年剑客站在竹林中,风吹竹叶,衣袂飘飘,半身特写" \
|
||||
--resolution 720P \
|
||||
--duration 5 \
|
||||
--watermark false \
|
||||
--download public/assets/portraits/swordsman.mp4
|
||||
```
|
||||
--prompt "一位穿着深色西装的中年物理学家,站在大学讲台前,表情严肃,半身特写" \
|
||||
--resolution 720P --duration 5 --watermark false \
|
||||
--download public/assets/portraits/physicist.mp4
|
||||
|
||||
#### 过场动画
|
||||
|
||||
```bash
|
||||
# 生成高潮场景过场动画
|
||||
# 过场动画
|
||||
bl video generate \
|
||||
--prompt "暴风雨中的悬崖边,闪电划破夜空,两个人影对峙,镜头从远景缓慢推近到中景,电影级画质" \
|
||||
--resolution 1080P \
|
||||
--duration 5 \
|
||||
--watermark false \
|
||||
--download public/assets/cutscenes/cliff_confrontation.mp4
|
||||
```
|
||||
--prompt "红岸基地,巨大的抛物面天线在暴风雨中矗立,闪电照亮天线轮廓,镜头从远景推近,电影级画质" \
|
||||
--resolution 1080P --duration 5 --watermark false \
|
||||
--download public/assets/cutscenes/red_coast_storm.mp4
|
||||
|
||||
#### 多角色场景(使用 `bl video ref` 保持角色一致性)
|
||||
|
||||
```bash
|
||||
# 多角色一致性场景
|
||||
# 多角色一致性场景(bl video ref)
|
||||
bl video ref \
|
||||
--prompt "Image1 和 Image2 面对面站在古城门前,Image1 递出一封信,Image2 犹豫地伸出手,夕阳逆光" \
|
||||
--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/letter_scene.mp4
|
||||
--resolution 1080P --duration 5 --watermark false \
|
||||
--download public/assets/cutscenes/lab_argument.mp4
|
||||
```
|
||||
|
||||
#### 完整生成脚本示例 `scripts/generate-assets.sh`
|
||||
#### 图片素材 — `bl image generate`
|
||||
|
||||
```bash
|
||||
# 角色立绘(静态图片)
|
||||
bl image generate \
|
||||
--prompt "一位穿着军绿色工装的年轻中国女性,目光坚定,黑色短发,70年代风格,半身肖像画,电影光影" \
|
||||
--size 768*1024 --watermark false \
|
||||
--out-dir public/assets/portraits/ --out-prefix ye_wenjie
|
||||
|
||||
# 场景背景图
|
||||
bl image generate \
|
||||
--prompt "1960年代中国大学校园,苏式建筑,红色标语横幅,秋天的梧桐树,灰暗天空,写实油画风格" \
|
||||
--size 1920*1080 --watermark false \
|
||||
--out-dir public/assets/backgrounds/ --out-prefix campus_1967
|
||||
|
||||
# 过场 CG(静态图 + 游戏内 Ken Burns 动效)
|
||||
bl image generate \
|
||||
--prompt "红岸基地全景,巨大的抛物面天线矗立在山顶,黄昏,金色光芒,电影级构图" \
|
||||
--size 1920*1080 --watermark false \
|
||||
--out-dir public/assets/cutscenes/ --out-prefix red_coast_base
|
||||
```
|
||||
|
||||
#### TTS 旁白 — `bl speech synthesize`(可选)
|
||||
|
||||
```bash
|
||||
# 场景旁白
|
||||
bl speech synthesize \
|
||||
--text "1967年,中国大地上,一场浩劫正在改变无数人的命运。" \
|
||||
--voice longxiaochun --language zh \
|
||||
--out public/assets/narrations/scene_opening.mp3
|
||||
|
||||
# 角色独白
|
||||
bl speech synthesize \
|
||||
--text "叶文洁按下了发射按钮,那一刻,她做出了一个改变人类文明命运的决定。" \
|
||||
--voice longxiaochun --language zh \
|
||||
--instruction "用沉重而悲伤的语气" \
|
||||
--out public/assets/narrations/ye_decision.mp3
|
||||
```
|
||||
|
||||
#### 并行生成脚本模式(视频素材提速)
|
||||
|
||||
视频生成每个耗时 2-5 分钟,串行太慢。用 `--async` 并行提交再批量下载:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# 角色立绘生成
|
||||
declare -A PORTRAITS=(
|
||||
["character_a"]="一位身穿红色汉服的少女,微风中秀发飘动,温柔微笑,半身特写"
|
||||
["character_b"]="一位戴着斗笠的中年剑客,目光锐利,轻抚剑柄,半身特写"
|
||||
)
|
||||
# 并行提交所有视频生成任务
|
||||
declare -A TASKS # name → task_id
|
||||
|
||||
echo "=== 生成角色立绘 ==="
|
||||
echo "=== 提交视频生成任务 ==="
|
||||
for name in "${!PORTRAITS[@]}"; do
|
||||
outfile="public/assets/portraits/${name}.mp4"
|
||||
if [ -f "$outfile" ]; then
|
||||
echo " [跳过] $name — 已存在"
|
||||
continue
|
||||
fi
|
||||
[ -f "$outfile" ] && echo " [跳过] $name" && continue
|
||||
|
||||
echo " [生成] $name ..."
|
||||
# 如果有参考图则用 --image
|
||||
if [ -f "references/${name}.png" ]; then
|
||||
bl video generate \
|
||||
--image "references/${name}.png" \
|
||||
--prompt "${PORTRAITS[$name]}" \
|
||||
--resolution 720P --duration 5 --watermark false \
|
||||
--download "$outfile"
|
||||
else
|
||||
bl video generate \
|
||||
--prompt "${PORTRAITS[$name]}" \
|
||||
--resolution 720P --duration 5 --watermark false \
|
||||
--download "$outfile"
|
||||
fi
|
||||
echo " [完成] $name → $outfile"
|
||||
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 CUTSCENES=(
|
||||
["opening"]="古老的卷轴缓缓展开,露出水墨山水画,镜头推进画中世界,色彩从黑白渐变为彩色"
|
||||
["climax"]="暴风雨中两人在悬崖边对峙,闪电照亮面庞,镜头围绕旋转,电影质感"
|
||||
)
|
||||
|
||||
echo "=== 生成过场动画 ==="
|
||||
for name in "${!CUTSCENES[@]}"; do
|
||||
outfile="public/assets/cutscenes/${name}.mp4"
|
||||
if [ -f "$outfile" ]; then
|
||||
echo " [跳过] $name — 已存在"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo " [生成] $name ..."
|
||||
bl video generate \
|
||||
--prompt "${CUTSCENES[$name]}" \
|
||||
--resolution 1080P --duration 5 --watermark false \
|
||||
--download "$outfile"
|
||||
echo " [完成] $name → $outfile"
|
||||
# 等待并下载所有任务
|
||||
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
|
||||
|
||||
# 更新 generated-assets.json
|
||||
echo "=== 更新 generated-assets.json ==="
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const assets = { portraits: {}, cutscenes: {} };
|
||||
for (const f of fs.readdirSync('public/assets/portraits')) {
|
||||
if (f.endsWith('.mp4')) assets.portraits[path.basename(f,'.mp4')] = '/assets/portraits/' + f;
|
||||
}
|
||||
for (const f of fs.readdirSync('public/assets/cutscenes')) {
|
||||
if (f.endsWith('.mp4')) assets.cutscenes[path.basename(f,'.mp4')] = '/assets/cutscenes/' + f;
|
||||
}
|
||||
fs.writeFileSync('src/data/generated-assets.json', JSON.stringify(assets, null, 2));
|
||||
console.log(' 写入完成:', JSON.stringify(assets, null, 2));
|
||||
"
|
||||
|
||||
echo "=== 全部完成 ==="
|
||||
```
|
||||
|
||||
#### 关键规则
|
||||
- **素材必须离线生成并下载到本地**:`bl video generate --download` 一步到位完成生成+等待+下载
|
||||
- **素材必须离线生成并下载到本地**:游戏运行时零 API 调用
|
||||
- **本地文件路径直接传入 `--image`**:`bl` CLI 自动上传到临时存储,无需手动上传
|
||||
- **已生成素材自动跳过**:脚本检查本地文件是否存在(`[ -f path ]`)
|
||||
- **generated-assets.json 只存本地路径**:如 `/assets/portraits/character_a.mp4`,绝不存远程 URL
|
||||
- **游戏运行时零 API 调用**:所有素材都是预生成的本地文件
|
||||
- **generated-assets.json 只存本地路径**:如 `/assets/portraits/ye_wenjie.mp4`,绝不存远程 URL
|
||||
- **视频用 `--async` 并行提交**:3-5 个并发,避免串行等待
|
||||
|
||||
### Web Audio API 程序化音乐
|
||||
- 用 MIDI 音高数组定义旋律乐句,循环播放
|
||||
@@ -362,17 +376,19 @@ echo "=== 全部完成 ==="
|
||||
按以下顺序执行,每步完成后标记 task:
|
||||
|
||||
1. 初始化 React 项目 + 目录结构
|
||||
2. 编写 story.js(所有场景文本、选择、分支)— 这是最大的工作量,可用 `bl text chat` 辅助生成
|
||||
2. 编写 story.js(所有场景文本、选择、分支)— 这是最大的工作量
|
||||
3. 编写 characters.js 和 archives.js
|
||||
4. 实现主题样式(CSS 变量、字体、配色、动画)
|
||||
5. 实现核心组件:TypeWriter → GameScene → ChoicePanel → CharacterPortrait
|
||||
6. 实现 TitleScreen + EndingScreen + ArchivePanel
|
||||
7. 实现 useGameState(reducer + hash 路由)
|
||||
8. 实现 useAudio(BGM 乐谱 + 音效)
|
||||
9. 如需 AI 素材:编写 `scripts/generate-assets.sh` → 执行 `bash scripts/generate-assets.sh` 生成并下载素材
|
||||
10. 实现特殊场景效果(Canvas 动态背景、点击交互等)
|
||||
11. 整合 App.jsx
|
||||
12. 启动 dev server,浏览器测试完整流程(至少走通两条路线到不同结局)
|
||||
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 模拟手机视口 + 触控)
|
||||
|
||||
---
|
||||
|
||||
@@ -381,9 +397,10 @@ echo "=== 全部完成 ==="
|
||||
以下是从实际开发中总结的经验,务必遵循:
|
||||
|
||||
- **bl video 分辨率**:`bl video generate` 支持 720P 和 1080P,测试阶段用 720P(更快更便宜),最终版用 1080P
|
||||
- **bl video --download 自动轮询**:`--download` 标志会自动等待任务完成并下载文件,无需手写轮询代码;如需异步可用 `--async` 获取 task-id 后用 `bl video download --task-id <id> --out <path>` 手动下载
|
||||
- **本地路径自动上传**:`bl` CLI 的 `--image` 接受本地文件路径,会自动上传到临时存储(48小时有效),无需手动上传到 OSS
|
||||
- **素材必须离线生成并下载到本地**:视频生成耗时 2-5 分钟/个,绝不能在游戏运行时调用。`--download` 下载到 `public/assets/`,generated-assets.json 中只存本地路径
|
||||
- **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 乐谱数组循环播放,不要用随机音符漫游(听起来像噪声)
|
||||
@@ -391,4 +408,5 @@ echo "=== 全部完成 ==="
|
||||
- **特殊场景视觉**:Canvas 动态背景必须与叙事内容紧密关联(出现什么元素画什么),不能泛泛画星空了事
|
||||
- **点击交互**:Canvas 场景加粒子爆发 + 冲击波环 + 屏幕震动 + 主题相关额外效果,大幅提升沉浸感
|
||||
- **CRA 清理**:初始化后立即删除 App.css/logo.svg/setupTests.js 等样板文件,避免冲突
|
||||
- **bl text chat 辅助创作**:剧情大纲、场景文本、分支设计、视频 prompt 都可以用 `bl text chat` 辅助生成,大幅减少手写工作量
|
||||
- **移动端字体**:正文最小 16px,否则 iOS Safari 会自动缩放页面
|
||||
- **视频内存泄漏**:离开场景时必须 `video.pause(); video.removeAttribute('src'); video.load()` 释放内存
|
||||
|
||||
Reference in New Issue
Block a user