# 《听仙》MVP 任务清单
> 创建日期:2026-05-22
> 对应策划案:v1.5
> 技术方案:纯前端(HTML5,无服务器)
> MVP 范围:v0.1 → v0.2 → v0.3 三阶段渐进交付
---
## 概览
### 阶段划分
| 阶段 | 目标 | 核心验证 |
|------|------|---------|
| **Phase 0** 基础准备 | 环境、设计补全、音色选定 | 工具链全通,设计无歧义 |
| **Phase 1 / v0.1** 纯音频原型 | 黑屏+手势+音频播报+房间移动 | 手势是否直觉,语音是否可懂 |
| **Phase 2 / v0.2** 战斗原型 | 2种怪+3技能+完整回合制 | 纯音频回合制是否可玩 |
| **Phase 3 / v0.3** 内容原型 | NPC+任务+背包+菜单 | 能否完成"接任务→打怪→交任务"闭环 |
### 技术选型(纯前端)
```
构建工具:Vite(本地开发服务+手机局域网测试)
语言:JavaScript(ES2022 模块化,不强制 TypeScript)
音频:Web Audio API + HTMLAudioElement
手势:Touch Events API
状态:有限状态机(自实现,无框架)
存储:localStorage(进度存档)
数据:JSON 文件(房间/NPC/物品/技能/怪物定义)
```
### 项目目录结构
```
xianxia/
├── docs/ # 策划与任务文档
├── src/
│ ├── index.html # 启动页 + 游戏容器
│ ├── main.js # 入口,组装所有模块
│ ├── audio/
│ │ ├── AudioEngine.js # 四层混音器
│ │ ├── VoiceQueue.js # 语音播报队列
│ │ └── UISynth.js # Web Audio UI音效合成
│ ├── gesture/
│ │ └── GestureRecognizer.js
│ ├── state/
│ │ └── StateMachine.js # 状态机框架
│ ├── game/
│ │ ├── World.js # 房间/地图
│ │ ├── Player.js # 角色属性
│ │ ├── Combat.js # 战斗引擎
│ │ ├── Quest.js # 任务系统
│ │ └── Inventory.js # 背包/物品
│ └── data/
│ ├── rooms.json
│ ├── npcs.json
│ ├── monsters.json
│ ├── items.json
│ ├── skills.json
│ └── quests.json
├── assets/
│ ├── voice/ # TTS 语音片段(mp3)
│ ├── sfx/ # 音效(wav/mp3)
│ ├── ambient/ # 环境氛围音(ogg)
│ └── bgm/ # 背景音乐(ogg)
└── scripts/
├── gen_tts.py # 批量 TTS 生成脚本
├── gen_sfx.py # 批量音效生成脚本
└── tts_manifest.csv # TTS 文本清单
```
### 任务编号规则
`P{阶段}-{序号}` — 例如 P1-03 表示 Phase 1 第 3 个任务。
每个任务包含:目标 / 前置依赖 / 实现方式 / 交付物 / 测试用例。
---
## Phase 0:基础准备
### P0-01:前端开发环境搭建
**目标**
建立可运行的项目骨架,开发者能在 PC 浏览器和局域网手机上实时预览。
**前置依赖**
无
**实现方式**
1. 在 `/home/xsl/xianxia/` 下初始化 Vite 项目:
```bash
npm create vite@latest . -- --template vanilla
npm install
```
2. 修改 `vite.config.js` 开启局域网访问:
```js
export default { server: { host: '0.0.0.0', port: 5173 } }
```
3. 按上方目录结构创建空文件占位。
4. `index.html` 设置 `` 和黑色背景。
**交付物**
- `package.json` + `vite.config.js`
- `src/index.html`(空白黑屏页,含"开始听仙"按钮占位)
- `npm run dev` 成功启动,手机浏览器可访问
**测试用例**
| # | 操作 | 预期结果 |
|---|------|---------|
| 1 | `npm run dev` | 控制台无报错,显示本地和网络地址 |
| 2 | PC 浏览器访问 `localhost:5173` | 页面加载,黑色背景,有"开始听仙"按钮 |
| 3 | 手机浏览器访问局域网 IP | 同上,无布局错误 |
---
### P0-02:移动端测试环境搭建
**目标**
确保每个阶段的成果都能在真实手机上测试(iOS Safari + Android Chrome)。
**前置依赖**
P0-01
**实现方式**
1. 确认开发机与手机在同一局域网。
2. 在手机浏览器上将游戏页面添加到主屏幕(模拟 PWA 全屏)。
3. 准备 Chrome DevTools 远程调试(Android via USB)或 Safari Web Inspector(iOS via USB)。
4. 写 `scripts/open_qr.sh`:用 `qrencode` 生成局域网 URL 的二维码,方便快速打开。
```bash
#!/bin/bash
IP=$(hostname -I | awk '{print $1}')
echo "http://$IP:5173" | qrencode -t ANSIUTF8
```
**交付物**
- `scripts/open_qr.sh`
- 确认 iOS Safari 和 Android Chrome 均能访问开发服务器
**测试用例**
| # | 场景 | 预期结果 |
|---|------|---------|
| 1 | Android Chrome 访问 | 页面正常加载 |
| 2 | iOS Safari 访问 | 页面正常加载 |
| 3 | 添加到主屏幕后打开 | 全屏显示,无浏览器工具栏 |
---
### P0-03:音频生成管线验证与脚本化
**目标**
验证 VoxCPM2 → ffmpeg 的完整 TTS 流程可批量执行,产出可用的 mp3 文件;同时验证 ACE-Step 和 Stable Audio 能产出符合规格的音效/BGM。
**前置依赖**
无(工具已部署)
**实现方式**
**3.1 编写批量 TTS 脚本 `scripts/gen_tts.py`:**
```python
# 读取 tts_manifest.csv (id, text, voice_ref, output_path)
# 调用 VoxCPM2 REST API: POST http://localhost:8000/tts
# 用 ffmpeg 后处理:归一化音量(-16 LUFS),转 mp3 64kbps,22050Hz
# 输出到 assets/voice/{id}.mp3
# 记录失败条目,支持断点续跑
```
`tts_manifest.csv` 格式:
```csv
id,text,voice_ref,priority
sys_welcome,"欢迎来到苍玄大陆。",narrator.wav,P2
sys_menu_open,"菜单。",narrator.wav,P2
```
**3.2 验证 ACE-Step(BGM/长音效):**
- 手动启动 `conda activate acestep`
- 生成一段 30s 测试氛围音,确认输出可用
- 记录启动命令到 `scripts/README_tools.md`
**3.3 验证 Stable Audio(短音效):**
- 启动 Gradio WebUI,生成一段 1s 剑击音效测试
- 确认 44.1kHz WAV 输出正常
**3.4 ffmpeg 后处理验证:**
```bash
# BGM/氛围 → OGG 128kbps
ffmpeg -i input.wav -c:a libvorbis -q:a 4 output.ogg
# 语音 → MP3 64kbps 22050Hz
ffmpeg -i input.wav -af "loudnorm=I=-16:TP=-1.5" -ar 22050 -b:a 64k output.mp3
# 短音效 → WAV 保持原格式(不压缩)
ffmpeg -i input.wav -ar 44100 -ac 1 output.wav
```
**交付物**
- `scripts/gen_tts.py`(可运行,含断点续跑)
- `scripts/tts_manifest.csv`(空模板)
- `scripts/README_tools.md`(各工具启动命令速查)
- 测试输出:3 条 TTS mp3、1 段氛围 ogg、1 个音效 wav,均通过质量验证
**测试用例**
| # | 操作 | 预期结果 |
|---|------|---------|
| 1 | 运行 `gen_tts.py` 生成 3 条测试文本 | 输出 3 个 mp3,无报错 |
| 2 | 手机播放生成的 mp3 | 音量正常,无爆音/底噪 |
| 3 | 手动中断 `gen_tts.py` 再重跑 | 已完成条目跳过,断点续跑正常 |
| 4 | 输入空文本或超长文本(>200字) | 脚本报错并记录,不崩溃 |
---
### P0-04:策划补全——战斗数值体系
**目标**
填补策划案中三处设计空缺,确保 Phase 2 战斗系统有明确的实现依据,不需要在编码时临时拍数字。
**前置依赖**
无
**需补全的三处问题**(来自 review):
**4.1 攻击力推导**
在 `游戏策划案.md` 第四章补充:
```
物理攻击力 = 根骨 × 2 + 装备物理攻击加值
法术攻击力 = 灵力 × 2 + 装备法术攻击加值
物理防御力 = 根骨 × 1.5 + 装备防御加值
法术防御力 = 神识 × 1.5 + 装备法术防御加值
气血上限 = 根骨 × 10 + 境界基础值
灵力上限 = 灵力 × 8 + 境界基础值
```
**4.2 灵根与功法绑定规则**
在 `游戏策划案.md` 第四章 4.4 节补充:
```
规则:灵根不限制可学功法种类,但影响效果。
- 同灵根功法(如火灵根使用火球术):伤害 × 1.2,灵力消耗 × 0.8
- 异灵根功法(如火灵根使用冰锥术):伤害 × 0.9,灵力消耗 × 1.2
- 防御/辅助功法不受灵根影响
设计意图:允许跨灵根尝试,鼓励同灵根专精,不强制锁死功法选择。
```
**4.3 命中率公式计算顺序明确**
在 `游戏策划案.md` 第七章 7.3 节将:
> `最终命中 = 命中率 - 闪避率(下限50%,上限99%)`
改为更明确的表述:
> `最终命中率 = clamp(命中率 - 闪避率, 50%, 99%)`
> 其中命中率本身无上限限制(可超过100%用于抵消高闪避敌人)
**交付物**
- `游戏策划案.md` 中对应三处内容更新
**测试用例(设计评审)**
| # | 场景 | 验证 |
|---|------|------|
| 1 | 练气一层初始角色(根骨10,灵力8)| 物理攻击力=20,气血上限=100+境界基础值 |
| 2 | 火灵根用火球术 vs 冰锥术 | 伤害相差 1.2/0.9 倍,逻辑自洽 |
| 3 | 攻方等级比守方高20级 | 命中率=90%+40%=130%,减去闪避后仍被 cap 在 99% |
---
### P0-05:旁白音色选定与验证
**目标**
确定系统旁白(房间描述、系统提示)的 TTS 参考音频,后续所有 TTS 生成使用统一音色。
**前置依赖**
P0-03
**实现方式**
1. 用 VoxCPM2 准备 3 组候选参考音频(建议:沉稳男声/清冽女声/中性叙事声)。
2. 对每组音色,用相同的测试文本生成 3 句语音:
```
"你站在清风镇的中心广场。四周传来市集的喧嚣声。"
"战斗胜利。你获得经验值 120 点,灵石 30 枚。"
"菜单。角色信息。"
```
3. 在手机耳机下播放,评估清晰度、疲劳感(长时间听是否舒适)、仙侠感。
4. 记录选定音色的参考音频路径到 `scripts/tts_manifest.csv` 头部注释。
**交付物**
- `assets/voice/ref/narrator.wav`(选定的旁白参考音频)
- 3 组测试对比音频(存入 `assets/voice/ref/candidates/`,不进入游戏)
- `docs/音色选定记录.md`:记录选择理由
**测试用例**
| # | 评估维度 | 通过标准 |
|---|---------|---------|
| 1 | 清晰度 | 手机外放能听清每个字 |
| 2 | 疲劳感 | 连续播放 5 分钟不刺耳 |
| 3 | 风格 | 符合仙侠叙事感,不像 GPS 导航 |
| 4 | 一致性 | 同参考音频重复生成两次,结果基本相同 |
---
## Phase 1:v0.1 纯音频交互原型
**阶段目标**:玩家能打开游戏,在 3-5 个房间之间移动,通过语音了解所处位置,通过菜单查看基本状态。无战斗,无NPC,无物品。
**验证问题**:手势操作是否直觉?音频队列是否清晰?玩家能否理解所处环境?
---
### P1-01:启动页与音频解锁
**目标**
实现极简启动页,用户点击"开始听仙"后解锁浏览器音频权限,进入全黑游戏模式。
**前置依赖**
P0-01
**实现方式**
`src/index.html` 结构:
```html
```
`src/main.js` 音频解锁逻辑:
```js
document.getElementById('start-btn').addEventListener('click', async () => {
const ctx = new AudioContext();
await ctx.resume(); // 解锁
// 播放 0.1s 静音确保解锁
const buf = ctx.createBuffer(1, ctx.sampleRate * 0.1, ctx.sampleRate);
const src = ctx.createBufferSource();
src.buffer = buf;
src.connect(ctx.destination);
src.start();
document.getElementById('launch-screen').hidden = true;
document.getElementById('game-screen').hidden = false;
initGame(ctx);
});
```
**交付物**
- `src/index.html`(启动页完整标记)
- 音频解锁逻辑(嵌入 `main.js`)
- 启动页 CSS:黑色背景,居中按钮,无其他视觉元素
**测试用例**
| # | 场景 | 预期结果 |
|---|------|---------|
| 1 | PC Chrome 点击按钮 | 控制台无 AudioContext 报错,进入黑屏 |
| 2 | iOS Safari 点击按钮 | 音频正常解锁,不出现无声黑屏 |
| 3 | 不点按钮直接调用音频 | 音频不播放,保留在启动页 |
| 4 | 解锁失败(模拟)| 保留在启动页,不进入无声黑屏 |
---
### P1-02:手势识别模块
**目标**
识别 6 种手势(上/下/左/右滑动、单击、双击),以事件方式发出,供状态机消费。
**前置依赖**
P1-01
**实现方式**
`src/gesture/GestureRecognizer.js`:
```js
export class GestureRecognizer {
constructor(element) { /* 监听 touchstart/touchend/touchmove */ }
// 识别逻辑
// - 滑动:位移 > 30px,且 X 或 Y 方向占比 > 65%(防斜滑误触)
// - 单击:touchend 时位移 < 10px,300ms 内无第二次 touch
// - 双击:300ms 内两次单击
// - 不识别长按、三击等其他手势
on(gesture, callback) { /* 'swipe-up'|'swipe-down'|'swipe-left'|'swipe-right'|'tap'|'double-tap' */ }
}
```
关键参数(可配置常量):
```js
const SWIPE_MIN_DISTANCE = 30; // px,最小滑动距离
const SWIPE_AXIS_RATIO = 0.65; // 主轴方向占比阈值
const DOUBLE_TAP_DELAY = 300; // ms,双击最大间隔
const TAP_MAX_DISTANCE = 10; // px,点击最大位移
```
**交付物**
- `src/gesture/GestureRecognizer.js`(完整实现)
- 浏览器控制台调试日志(开发模式下打印识别到的手势)
**测试用例**
| # | 操作 | 预期 |
|---|------|------|
| 1 | 手指从下向上快速滑动 | 触发 `swipe-up` |
| 2 | 手指斜向滑动(45°) | 触发主方向的 swipe,不触发两个 |
| 3 | 轻点一次 | 300ms 后触发 `tap` |
| 4 | 快速点击两次(< 300ms)| 触发 `double-tap`,不触发两个 `tap` |
| 5 | 慢点两次(> 300ms 间隔)| 触发两个 `tap` |
| 6 | 滑动 < 30px | 不触发 swipe |
| 7 | iOS Safari 实机测试 | 系统手势(下拉刷新)不干扰游戏手势 |
---
### P1-03:UI 音效合成模块
**目标**
用 Web Audio API 实时合成 13 种 UI 音效,零文件依赖,操作后 < 10ms 内播放。
**前置依赖**
P1-01(需要已解锁的 AudioContext)
**实现方式**
`src/audio/UISynth.js`:
```js
export class UISynth {
constructor(audioContext) { this.ctx = audioContext; }
play(type) {
// type: 'swipe-up'|'swipe-down'|'swipe-left'|'swipe-right'
// 'tap'|'double-tap'|'invalid'
// 'menu-open'|'menu-close'|'list-scroll'
// 'confirm'|'error'|'item-pickup'
const spec = SOUND_SPECS[type];
if (!spec) return;
// 用 OscillatorNode + GainNode 合成短音
// 每种音效定义:频率(Hz)、包络(attack/decay/sustain/release)、波形
}
}
const SOUND_SPECS = {
'swipe-up': { freq: 880, type: 'sine', adsr: [0.01, 0.1, 0, 0.05] },
'swipe-down': { freq: 220, type: 'sine', adsr: [0.01, 0.15, 0, 0.05] },
'swipe-left': { freq: 440, type: 'triangle', adsr: [0.005, 0.08, 0, 0.03] },
'swipe-right': { freq: 550, type: 'triangle', adsr: [0.005, 0.08, 0, 0.03] },
'tap': { freq: 660, type: 'sine', adsr: [0.005, 0.1, 0, 0.04] },
'double-tap': { freq: [660, 880], type: 'sine', adsr: [0.005, 0.08, 0, 0.03] },
'invalid': { freq: 150, type: 'sawtooth', adsr: [0.01, 0.2, 0.1, 0.1] },
// ... 其余6种
};
```
**交付物**
- `src/audio/UISynth.js`(13 种音效全部实现)
- 调试页面:按钮逐一触发每种音效(开发用,不进游戏)
**测试用例**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 调用 `play('swipe-up')` | 立即听到升调短音,无延迟感 |
| 2 | 快速连续调用 10 次 | 每次都响,不互相覆盖/消音 |
| 3 | `play('invalid')` | 音色明显与其他不同,表达"无效" |
| 4 | 耳机下聆听 | 13 种音效能主观区分,不混淆 |
---
### P1-04:音频引擎——四层混音器
**目标**
实现 BGM / 氛围 / 音效 / 语音 四层独立混音,支持各层音量控制和 BGM 交叉淡入淡出。
**前置依赖**
P1-01
**实现方式**
`src/audio/AudioEngine.js`:
```js
export class AudioEngine {
constructor(audioContext) {
this.ctx = audioContext;
// 四个 GainNode → MasterGain → destination
this.layers = {
bgm: { gain: ctx.createGain(), volume: 0.4, loop: true },
ambient: { gain: ctx.createGain(), volume: 0.4, loop: true },
sfx: { gain: ctx.createGain(), volume: 0.8, loop: false },
voice: { gain: ctx.createGain(), volume: 1.0, loop: false },
};
}
// BGM 切换:交叉淡入淡出(2秒)
async setBGM(url) { /* 淡出旧BGM,加载新BGM,淡入 */ }
// 语音播报时 BGM 压低到 20%,播完恢复
onVoiceStart() { this._fadeTo('bgm', 0.2, 0.3); }
onVoiceEnd() { this._fadeTo('bgm', 0.4, 0.5); }
// 氛围音切换(1秒交叉)
setAmbient(url) { /* 同 BGM 逻辑,时间更短 */ }
// 音效:最多同时 3 个
playSFX(url) { /* 超过3个时丢弃最旧的 */ }
_fadeTo(layer, targetVolume, duration) { /* GainNode.gain.linearRampToValueAtTime */ }
}
```
**交付物**
- `src/audio/AudioEngine.js`(四层混音完整实现)
- 验证:两首 BGM 切换时有平滑交叉渐变,无爆音
**测试用例**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 播放 BGM,再调用 `setBGM(url2)` | 旧 BGM 渐出,新 BGM 渐入,无爆音 |
| 2 | 触发 `onVoiceStart()` | BGM 音量在 0.3s 内降到 20% |
| 3 | 触发 `onVoiceEnd()` | BGM 音量恢复到 40% |
| 4 | 同时触发 4 个音效 | 最多 3 个同时播放,第 4 个被丢弃 |
| 5 | 切换氛围音 | 旧氛围渐出,新氛围渐入,无突变 |
---
### P1-05:音频引擎——语音播报队列
**目标**
实现带优先级的语音队列:P0 立即打断,P1 插队,P2/P3 排队;支持音频片段拼接播放;队列上限 5 条。
**前置依赖**
P1-04
**实现方式**
`src/audio/VoiceQueue.js`:
```js
export class VoiceQueue {
constructor(audioEngine) {
this.queue = []; // [{priority, segments, fallbackText}]
this.playing = null;
this.maxSize = 5;
}
enqueue(item) {
// item = { priority: 0-3, segments: [url,...], fallbackText: '' }
if (item.priority === 0) {
this._interrupt();
this.queue.unshift(item);
} else if (item.priority === 1) {
const insertAt = this.queue.findIndex(i => i.priority > 1);
this.queue.splice(insertAt === -1 ? 0 : insertAt, 0, item);
} else {
this.queue.push(item);
}
// 队列溢出:丢弃队尾最低优先级
while (this.queue.length > this.maxSize) {
const dropIdx = this._findLowestPriorityFromTail();
this.queue.splice(dropIdx, 1);
}
if (!this.playing) this._playNext();
}
// 拼接播放:顺序播放 segments 数组中的多个音频片段
async _playSegments(segments) { /* 逐个 HTMLAudioElement 播放 */ }
_interrupt() { /* 停止当前播放,清空 P2/P3 队列 */ }
}
```
**交付物**
- `src/audio/VoiceQueue.js`(完整实现,含片段拼接)
- 单元测试文件 `src/audio/VoiceQueue.test.js`(验证优先级逻辑)
**测试用例**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 队列空时 enqueue P2 | 立即播放 |
| 2 | P2 播放中 enqueue P1 | P2 播完后播 P1 |
| 3 | P2 播放中 enqueue P0 | P2 立即中断,P0 立即开始 |
| 4 | 入队 6 条 P3 | 第 6 条被丢弃,队列保持 5 条 |
| 5 | 入队含 3 段 segments 的消息 | 三段依次播放,中间无明显停顿(≤100ms) |
| 6 | P3 播放中,玩家单击 | P3 被中断,状态播报(P2)正常入队 |
---
### P1-06:游戏状态机框架
**目标**
实现可扩展的有限状态机,管理探索态/战斗态/菜单态等主状态,支持状态进入/退出钩子。
**前置依赖**
P1-02(手势事件),P1-03(UI音效),P1-05(语音队列)
**实现方式**
`src/state/StateMachine.js`:
```js
export class StateMachine {
constructor(states, initialState) {
// states = { 'explore': ExploreState, 'combat': CombatState, ... }
this.current = null;
this.states = states;
this.transition(initialState);
}
transition(stateName, data = {}) {
if (this.current) this.current.onExit();
this.current = new this.states[stateName](this, data);
this.current.onEnter();
}
handleGesture(gesture) {
this.current.handleGesture(gesture);
}
}
// 每个状态实现:
export class BaseState {
onEnter() {}
onExit() {}
handleGesture(gesture) {}
}
```
Phase 1 只实现两个状态:
- `ExploreState`:处理移动、焦点切换、单击查看状态、下滑进菜单
- `MenuState`:处理菜单导航,左滑退出
**交付物**
- `src/state/StateMachine.js`
- `src/state/ExploreState.js`(移动、焦点、单击、进菜单)
- `src/state/MenuState.js`(基础菜单框架,菜单项为空占位)
**测试用例**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 初始状态 | 进入探索态,触发 `onEnter` |
| 2 | 探索态下滑 | 切换到菜单态,播放菜单打开音效 |
| 3 | 菜单态左滑 | 切换回探索态,播放菜单关闭音效 |
| 4 | 状态切换时旧状态 `onExit` 被调用 | 控制台可见日志 |
---
### P1-07:房间数据结构与清风镇地图
**目标**
定义 5 个清风镇房间的数据,可在运行时被导航系统读取。
**前置依赖**
P0-04(设计已补全)
**实现方式**
`src/data/rooms.json`(5 个房间):
```json
{
"qingfeng_square": {
"id": "qingfeng_square",
"name": "清风镇·镇中广场",
"description": "你站在清风镇的中心广场。四周传来商贩的吆喝声和行人的脚步声。",
"area": "清风镇",
"exits": { "north": "qingfeng_north", "east": "qingfeng_inn", "west": "qingfeng_smith" },
"ambient": "market_noise.ogg",
"bgm": "town_bgm.ogg",
"safe_zone": true,
"pvp_allowed": false,
"level_range": [1, 5],
"npcs": [],
"items": []
},
"qingfeng_north": { ... /* 镇北郊外 */ },
"qingfeng_inn": { ... /* 客栈 */ },
"qingfeng_smith": { ... /* 铁匠铺 */ },
"qingfeng_inn2f": { ... /* 客栈二楼,安全区修炼 */ }
}
```
**交付物**
- `src/data/rooms.json`(5 个房间完整定义)
- `src/game/World.js`:`getRoom(id)`、`getRoomExits(id)` 等方法
**测试用例**
| # | 场景 | 预期 |
|---|------|------|
| 1 | `World.getRoom('qingfeng_square')` | 返回完整房间对象 |
| 2 | 检查所有出口的目标房间 ID | 均存在于 rooms.json,无悬空引用 |
| 3 | 安全区标记一致性 | 客栈二楼 `safe_zone: true`,镇北郊外 `false` |
---
### P1-08:探索态——移动与焦点切换
**目标**
实现完整的探索态交互:左/右滑切换焦点,上滑移动,单击查看状态,进房间播报完整语音流。
**前置依赖**
P1-05(语音队列),P1-06(状态机),P1-07(房间数据)
**实现方式**
完善 `src/state/ExploreState.js`:
```js
// 进入房间时的播报顺序(严格按策划案 5.3):
// 1. 移动音效(脚步声,Web Audio 合成,0.5s)
// 2. 氛围音渐变(1s)
// 3. 房间名称 [P2]
// 4. 简短描述 [P2]
// 5. 出口提示 [P2]
// 6. 特殊提示(NPC/物品/玩家)[P3]
// 焦点系统:
// - 焦点列表 = 当前房间的 [出口列表] + [NPC列表] + [物品列表]
// - 进入房间默认焦点为第一个出口
// - 左/右滑循环切换,播报焦点名称
// - 上滑:当前焦点为出口 → 移动;否则播报"当前无法前进"
// 危险移动二次确认:
// - 出口目标房间 level_range[0] > 玩家等级+3 时,第一次上滑只播警告
// - 3秒内再次上滑才真正移动;其他操作取消确认
```
**交付物**
- `src/state/ExploreState.js`(完整探索态逻辑)
- 玩家位置持久化到 `localStorage`(刷新后恢复位置)
**测试用例**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 进入广场 | 依次听到:脚步音、氛围音渐入、"清风镇·镇中广场"、描述、出口提示 |
| 2 | 右滑两次 | 焦点从第一个出口切换到第二个出口,分别播报出口名称 |
| 3 | 焦点在出口时上滑 | 移动到目标房间,触发新房间播报流程 |
| 4 | 焦点在 NPC 时上滑 | 播报"当前无法前进",不移动 |
| 5 | 单击 | 播报"你在 X。当前焦点:Y。" |
| 6 | 向危险区域上滑 | 播报警告,不移动 |
| 7 | 危险区域上滑后 3 秒内再次上滑 | 移动成功 |
| 8 | 刷新页面 | 恢复到上次位置,重新播报房间描述 |
---
### P1-09:菜单态基础框架
**目标**
实现可导航的菜单态,v0.1 阶段只需"角色信息"一个有实际内容的菜单项,其余为占位。
**前置依赖**
P1-06(状态机)
**实现方式**
完善 `src/state/MenuState.js`:
```
菜单项(v0.1 阶段):
1. 角色信息 → 播报"练气初期,气血100,灵力80"(硬编码占位数值)
2. 修炼/突破 → "功能尚未开放"
3-8. 其余项 → "功能尚未开放"
手势:
- 上/下滑:切换焦点菜单项,播报项目名称
- 单击:播报当前菜单项简介
- 双击:执行当前菜单项
- 左滑:关闭菜单,返回探索态
- 60 秒无操作:自动关闭,播报"菜单已关闭"
```
**交付物**
- `src/state/MenuState.js`(8 个菜单项,含超时逻辑)
**测试用例**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 进入菜单 | 播放菜单打开音效 + "菜单。角色信息。" |
| 2 | 下滑 | 焦点移到下一项,播报项目名 |
| 3 | 上滑到第一项再上滑 | 不超出边界,播报无效提示或循环 |
| 4 | 双击"角色信息" | 播报角色信息 |
| 5 | 左滑 | 关闭菜单,返回探索态,播放关闭音效 |
| 6 | 静置 60 秒 | 自动关闭,播报"菜单已关闭" |
---
### P1-10:v0.1 音频资源批量生成
**目标**
生成 v0.1 原型所需的全部预制音频资源(约 23 个文件,不含 Web Audio 合成的 UI 音效)。
**前置依赖**
P0-03(管线验证),P0-05(旁白音色)
**实现方式**
**10.1 填写 TTS 文本清单 `scripts/tts_manifest_v01.csv`:**
系统提示语音(10 段,旁白音色):
```
sys_welcome, "欢迎来到苍玄大陆。"
sys_menu_open, "菜单。"
sys_menu_item_1, "角色信息。"
sys_menu_close, "菜单已关闭。"
sys_invalid_move, "当前无法前进。"
sys_danger_warn, "前方危险,再次上滑确认前进。"
sys_safe_enter, "你进入了安全区。"
sys_safe_leave, "你离开了安全区。"
sys_focus_exit, "出口。"
sys_no_operation, "无法执行。"
```
房间描述语音(5 段,旁白音色):
```
room_square_name, "清风镇·镇中广场。"
room_square_desc, "四周传来商贩的吆喝声和行人的脚步声。"
room_square_exits, "可向北前往镇北郊外,向东进入客栈,向西去铁匠铺。"
room_north_name, "清风镇·镇北郊外。"
room_north_desc, "微风吹过,草虫鸣叫。远处似有妖兽出没的迹象。"
... (其余3个房间类似,共15段)
```
通用词汇片段(5 段):
```
word_you, "你"
word_at, "在"
word_focus, "当前焦点:"
word_health, "气血"
word_mp, "灵力"
```
**10.2 生成环境氛围音(ACE-Step):**
- `ambient_market.ogg`:市集喧嚣,30s 循环,prompt: "busy marketplace, chinese town ambience, crowd noise, vendors calling, footsteps"
- `ambient_wilderness.ogg`:山野,30s 循环,prompt: "outdoor wilderness, insects chirping, wind, peaceful nature"
**10.3 生成城镇 BGM(ACE-Step):**
- `town_bgm.ogg`:90s 循环,中国风轻快,笛子为主,prompt: "chinese fantasy town BGM, flute, light and cheerful, loop"
**10.4 运行脚本并后处理:**
```bash
python scripts/gen_tts.py --manifest scripts/tts_manifest_v01.csv --output assets/voice/
# 后处理:氛围音和BGM转OGG 128kbps
for f in assets/ambient/*.wav; do ffmpeg -i "$f" -c:a libvorbis -q:a 4 "${f%.wav}.ogg"; done
```
**交付物**
- `assets/voice/*.mp3`:约 30 条语音(10 系统 + 15 房间描述 + 5 通用词)
- `assets/ambient/ambient_market.ogg`、`ambient_wilderness.ogg`
- `assets/bgm/town_bgm.ogg`
- 生成日志(记录成功/失败条数)
**测试用例**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 手机播放全部 30 条语音 | 无杂音/爆音,音量一致,吐字清晰 |
| 2 | 循环播放氛围音 5 分钟 | 无明显循环接缝感 |
| 3 | 手机外放 BGM | 音量适中,不刺耳 |
| 4 | 检查 mp3 文件大小 | 单条语音 < 100KB,BGM < 1.5MB |
---
### P1-11:v0.1 音频质检与验收
**目标**
对所有 v0.1 音频资源进行系统性主观评估,不合格的标记重生成。
**前置依赖**
P1-10
**实现方式**
建立评分表 `docs/音频质检_v01.md`:
| 文件 | 清晰度(1-5) | 音量一致(1-5) | 风格适合(1-5) | 结论 | 备注 |
|------|------------|-------------|-------------|------|------|
| sys_welcome.mp3 | | | | 通过/重生成 | |
| ... | | | | | |
评分标准:
- **清晰度 < 3**:重生成
- **音量差异 > 3dB**(主观感知):ffmpeg 重归一化
- **风格 < 3**:考虑更换参考音频或调整生成参数
- BGM/氛围音额外检查:循环接缝是否自然
**交付物**
- `docs/音频质检_v01.md`(全部文件的评分记录)
- 重生成后的替换文件(如有)
**验收标准**
- 所有文件清晰度 ≥ 3,风格 ≥ 3
- BGM 循环接缝无明显断裂感
---
### P1-12:v0.1 集成测试与移动端验证
**目标**
将所有 v0.1 模块串联,在 iOS Safari 和 Android Chrome 实机上完成完整体验走查。
**前置依赖**
P1-01 ~ P1-11 全部完成
**测试场景**
| # | 场景 | 步骤 | 预期 |
|---|------|------|------|
| 1 | 完整启动流程 | 打开页面 → 点击"开始听仙" | 音频解锁,进入黑屏,播放欢迎语 |
| 2 | 基础移动 | 从广场右滑选出口 → 上滑移动 | 听到脚步声→氛围渐变→房间播报 |
| 3 | 焦点循环 | 右滑多次超过出口数量 | 循环回第一个出口 |
| 4 | 单击查看状态 | 在任意房间单击 | 播报"你在 X,当前焦点:Y" |
| 5 | 菜单完整流程 | 下滑→上/下滑导航→双击→左滑退出 | 每步均有对应音效和语音 |
| 6 | 误触容错 | 快速乱滑 | 无崩溃,无无声死锁 |
| 7 | 页面刷新 | 在某房间刷新页面 | 恢复到该房间,重新播报 |
| 8 | 后台切换 | 切到其他应用再返回 | 音频恢复正常播放 |
| 9 | 低电量模式(iOS) | 开启低电量模式 | 音频仍正常 |
| 10 | 连续使用 10 分钟 | 来回移动,多次开关菜单 | 无内存泄漏迹象(页面不卡顿)|
**验收标准(v0.1 阶段门禁)**
- 测试 #1-8 全部通过
- 在 iOS Safari 和 Android Chrome 各完成一次完整走查
- 找 1 位未参与开发的用户试玩 5 分钟,能不借助任何说明完成"移动到3个不同房间"
---
## Phase 2:v0.2 战斗原型
**阶段目标**:玩家在镇北郊外探索时随机遭遇野兔精或灰狼,进入回合制战斗,使用 3 个技能,战斗结束后返回探索态。
**验证问题**:纯音频回合制是否可玩?操作节奏是否舒适?玩家能否在不看屏幕的情况下理解战斗状态?
---
### P2-01:角色属性与数值系统
**目标**
实现角色的基础数值,以 v0.1 中硬编码的占位数值为基础,建立完整的属性对象。
**前置依赖**
P0-04(属性推导公式已确定)
**实现方式**
`src/game/Player.js`:
```js
export class Player {
constructor() {
// 基础属性(v0.2 阶段固定初始值,v0.3 才做角色创建)
this.level = 1;
this.realm = '练气一层';
this.rootBone = 10; // 根骨
this.comprehension = 8; // 悟性
this.spiritPower = 8; // 灵力
this.divineSense = 6; // 神识
this.fortune = 5; // 机缘
this.element = 'fire'; // 灵根(v0.2 固定火灵根)
}
// 推导属性(按 P0-04 公式)
get physAtk() { return this.rootBone * 2 + this.weaponBonus; }
get spellAtk() { return this.spiritPower * 2 + this.spellBonus; }
get physDef() { return Math.floor(this.rootBone * 1.5); }
get maxHP() { return this.rootBone * 10 + 50; } // 50=练气基础值
get maxMP() { return this.spiritPower * 8 + 40; }
}
```
**交付物**
- `src/game/Player.js`(属性推导全部实现)
- v0.2 初始角色数值文档(写入 `docs/MVP任务清单.md` 附录)
**测试用例**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 初始角色 rootBone=10 | `maxHP = 150`,`physAtk = 20` |
| 2 | 装备一把攻击+10的武器 | `physAtk = 30` |
| 3 | 修改根骨后 | 推导属性自动更新(getter) |
---
### P2-02:技能数据定义
**目标**
定义 3 个基础技能的完整数据,包含战斗参数和音频引用。
**前置依赖**
P0-04
**实现方式**
`src/data/skills.json`:
```json
{
"basic_sword": {
"id": "basic_sword",
"name": "御剑术",
"element": "gold",
"type": "attack",
"mpCost": 10,
"coefficient": 1.2,
"hitBonus": 0,
"description": "以灵力驭剑,造成物理伤害。",
"nameAudio": "voice/skill_basic_sword.mp3",
"sfxAudio": "sfx/sword_slash.wav"
},
"iron_shield": {
"id": "iron_shield",
"name": "金钟罩",
"element": "gold",
"type": "defense",
"mpCost": 12,
"coefficient": 0,
"defenseBonus": 0.5,
"duration": 1,
"description": "护体金光,本回合物理伤害减半。",
"nameAudio": "voice/skill_iron_shield.mp3",
"sfxAudio": "sfx/shield_up.wav"
},
"heal_spring": {
"id": "heal_spring",
"name": "回春术",
"element": "wood",
"type": "heal",
"mpCost": 15,
"healCoefficient": 0.8,
"description": "以木灵气疗愈,恢复气血。",
"nameAudio": "voice/skill_heal_spring.mp3",
"sfxAudio": "sfx/heal.wav"
}
}
```
**交付物**
- `src/data/skills.json`(3 个技能完整定义)
- `src/game/SkillSystem.js`:`applySkill(attacker, target, skillId)` 方法
**测试用例**
| # | 技能 | 预期结果 |
|---|------|---------|
| 1 | 御剑术(火灵根使用金系技能)| 伤害 = physAtk × 1.2 × 0.9(异灵根)× 克制系数 |
| 2 | 金钟罩激活后受击 | 伤害减半 |
| 3 | 回春术灵力不足 | 技能无法使用,播报"灵力不足" |
---
### P2-03:怪物数据定义
**目标**
定义野兔精和灰狼两种怪物的完整数据,包含 AI 行为和音频引用。
**前置依赖**
P0-04
**实现方式**
`src/data/monsters.json`:
```json
{
"wild_rabbit": {
"id": "wild_rabbit",
"name": "野兔精",
"level": 2,
"element": "wood",
"hp": 80, "maxHp": 80,
"physAtk": 12, "physDef": 6, "spellDef": 4,
"skills": ["scratch"],
"expReward": 30,
"moneyReward": [5, 15],
"dropTable": [{"itemId": "rabbit_fur", "rate": 0.5}],
"nameAudio": "voice/monster_rabbit.mp3",
"roarAudio": "sfx/rabbit_cry.wav",
"aiType": "aggressive",
"encounterRooms": ["qingfeng_north"]
},
"grey_wolf": {
"id": "grey_wolf",
"name": "灰狼",
"level": 3,
"element": "earth",
"hp": 120, "maxHp": 120,
"physAtk": 18, "physDef": 10, "spellDef": 6,
"skills": ["bite", "howl"],
"expReward": 60,
"moneyReward": [10, 25],
"dropTable": [{"itemId": "wolf_pelt", "rate": 0.4}],
"nameAudio": "voice/monster_wolf.mp3",
"roarAudio": "sfx/wolf_howl.wav",
"aiType": "cautious",
"encounterRooms": ["qingfeng_north"]
}
}
```
怪物 AI 行为(简单规则,非复杂AI):
```
aggressive:每回合使用攻击技能(随机选)
cautious:HP > 50% 时攻击,HP ≤ 50% 时有 30% 概率防御
```
**交付物**
- `src/data/monsters.json`(2 种怪物 + 2 种怪物技能:scratch、bite、howl)
- `src/game/MonsterAI.js`:`chooseAction(monster, combat)` 方法
---
### P2-04:战斗态状态机
**目标**
实现战斗态的手势映射(上/下切技能,左/右切目标,单击查状态,双击出招)。
**前置依赖**
P1-06(状态机框架)
**实现方式**
`src/state/CombatState.js`:
```js
export class CombatState extends BaseState {
onEnter({ monster }) {
this.combat = new Combat(player, monster);
this.currentSkillIdx = 0;
this.stateVersion = 0;
this.playerViewedVersion = 0;
// 播报战斗开始音效 + "战斗开始!你遭遇了[怪物名]。"
this._announceRoundStart();
}
handleGesture(g) {
switch(g) {
case 'swipe-up': this._prevSkill(); break;
case 'swipe-down': this._nextSkill(); break;
case 'swipe-left': this._prevTarget(); break;
case 'swipe-right':this._nextTarget(); break;
case 'tap': this._viewStatus(); break; // 更新 playerViewedVersion
case 'double-tap': this._confirmAttack(); break; // 检查 version 一致性
}
}
_confirmAttack() {
if (this.playerViewedVersion !== this.stateVersion) {
voiceQueue.enqueue({priority:2, text: '状态已更新,请先查看。'});
return;
}
this.combat.playerAction(currentSkill, currentTarget);
}
}
```
**交付物**
- `src/state/CombatState.js`(完整战斗态实现)
---
### P2-05:回合制战斗引擎
**目标**
实现完整的回合制战斗逻辑:伤害计算、命中判定、属性克制、状态效果、胜负判定。
**前置依赖**
P2-01(属性系统),P2-02(技能系统),P2-03(怪物数据)
**实现方式**
`src/game/Combat.js`:
```js
export class Combat {
constructor(player, monster) { ... }
playerAction(skillId, targetId) {
const result = this._resolveAction(player, monster, skillId);
this.stateVersion++;
return result;
// result = { hit: bool, damage: int, effects: [], monsterHp: int }
}
monsterAction() {
const skillId = MonsterAI.chooseAction(this.monster, this);
return this._resolveAction(this.monster, this.player, skillId);
}
_resolveAction(attacker, target, skillId) {
const skill = skills[skillId];
const hit = this._rollHit(attacker, target, skill);
if (!hit) return { hit: false };
let dmg = skill.coefficient * attacker.physAtk - target.physDef * 0.5;
dmg *= this._elementModifier(skill.element, target.element);
dmg *= this._elementAffinity(attacker.element, skill.element); // 灵根亲和
dmg *= this._critModifier(attacker);
dmg *= 0.9 + Math.random() * 0.2; // 浮动
dmg = Math.max(1, Math.round(dmg));
target.hp = Math.max(0, target.hp - dmg);
return { hit: true, damage: dmg, targetHp: target.hp };
}
_rollHit(attacker, target, skill) {
const hitRate = 0.9 + (attacker.level - target.level) * 0.02 + (skill.hitBonus || 0);
const dodgeRate = 0.05;
return Math.random() < Math.min(0.99, Math.max(0.5, hitRate - dodgeRate));
}
}
```
**交付物**
- `src/game/Combat.js`(完整战斗引擎)
- 单元测试 `src/game/Combat.test.js`:验证各公式边界值
**测试用例**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 1级角色打1级野兔精,御剑术 | 伤害在合理范围(约 15-25 点) |
| 2 | 火灵根用御剑术(金系) | 伤害 × 0.9(异灵根惩罚) |
| 3 | 金钟罩激活后挨打 | 受到伤害减半 |
| 4 | 灰狼攻击玩家 | HP 正确减少 |
| 5 | HP 降到 0 | `isAlive()` 返回 false,触发结算 |
| 6 | 命中率公式 | 等级差±20 时,最终命中率不超出 50%-99% |
---
### P2-06:遭遇触发与战斗结算
**目标**
玩家进入镇北郊外时,有概率随机触发遭遇战;战斗结束后结算并返回探索态。
**前置依赖**
P2-04(战斗态),P1-08(探索态)
**实现方式**
在 `src/state/ExploreState.js` 的 `onEnterRoom()` 中:
```js
onEnterRoom(roomId) {
// 先正常播报房间
this._announceRoom(roomId);
// 检查遭遇
const room = World.getRoom(roomId);
if (room.encounterRate && Math.random() < room.encounterRate) {
const monsterId = this._rollMonster(room);
setTimeout(() => {
// 播报遭遇提示,然后切换到战斗态
voiceQueue.enqueue({priority:1, text: `前方灌木丛中传来异响——${monsters[monsterId].name}出现了!`});
stateMachine.transition('combat', { monster: createMonsterInstance(monsterId) });
}, 2000); // 等房间播报完再触发
}
}
```
战斗结算(在 `CombatState` 中):
```js
_onCombatEnd(winner) {
if (winner === 'player') {
const { exp, money, drops } = this.combat.calculateRewards();
// 播报"战斗胜利。获得经验 X 点,灵石 Y 枚。[掉落物品]"
player.gainExp(exp);
player.money += money;
} else {
// 播报"你被击败了。"+ 气血恢复到 30%(不掉级)
player.hp = Math.floor(player.maxHP * 0.3);
}
setTimeout(() => stateMachine.transition('explore'), 3000);
}
```
**交付物**
- 遭遇触发逻辑(整合到 `ExploreState.js`)
- 战斗结算逻辑(整合到 `CombatState.js`)
- `rooms.json` 中 `qingfeng_north` 添加 `encounterRate: 0.4`
---
### P2-07:操作窗口机制(state_version)
**目标**
防止玩家在没有听到最新战斗状态时盲目出招(按策划案 6.2 操作窗口设计)。
**前置依赖**
P2-04(战斗态)
**实现方式**
已在 P2-04 的 `CombatState` 中内嵌,此任务确保:
1. 每次状态变化(回合切换、敌方出招结算)都递增 `stateVersion`
2. 玩家单击查看状态时,`playerViewedVersion = stateVersion`
3. 玩家上/下滑切换技能/目标时,也更新 `playerViewedVersion`
4. 双击出招时校验两个版本是否一致
**测试用例**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 进入新回合,不查看直接双击 | 播报"状态已更新,请先查看",不出招 |
| 2 | 单击查看后双击 | 正常出招 |
| 3 | 切换技能(下滑)后双击 | 视为已"浏览",正常出招 |
---
### P2-08:v0.2 音频资源批量生成
**目标**
生成 v0.2 战斗原型所需的全部新增音频资源(约 30 个文件)。
**前置依赖**
P0-03,P0-05
**内容清单:**
战斗系统提示语音(TTS,旁白音色,10 段):
```
combat_start, "战斗开始!"
combat_round_N, "第[N]回合。"(需 N=1-10,共10段)
combat_your_turn, "你的回合。"
combat_enemy_turn, "敌方行动。"
combat_victory, "战斗胜利!"
combat_defeat, "你被击败了。"
combat_escape_ok, "成功逃脱!"
combat_escape_fail,"逃脱失败!"
combat_low_hp, "气血危急!"
combat_state_update,"状态已更新,请先查看。"
```
数字语音(TTS,10 段):
```
num_0 到 num_9,文本"零一二三四五六七八九"各一段
```
通用战斗词汇(TTS,10 段):
```
word_damage, "点"(伤害后接)
word_hp_restored, "气血恢复"
word_mp_cost, "消耗灵力"
word_miss, "未命中"
word_crit, "暴击"
word_blocked, "被格挡"
word_poisoned, "中毒"
word_exp_gained, "获得经验"
word_money_gained, "灵石"
word_item_dropped, "获得"
```
技能名称语音(TTS,3 段):
```
skill_basic_sword, "御剑术"
skill_iron_shield, "金钟罩"
skill_heal_spring, "回春术"
```
怪物名称+叫声(TTS×2 + 音效×2):
```
monster_rabbit.mp3, TTS "野兔精"
monster_wolf.mp3, TTS "灰狼"
sfx_rabbit_cry.wav, Stable Audio 生成,高亢短叫 < 1s
sfx_wolf_howl.wav, ACE-Step 生成,狼嚎 1-2s
```
战斗音效(Stable Audio,6 个):
```
sfx_sword_slash.wav, 剑击 0.5s
sfx_punch.wav, 拳击 0.3s
sfx_shield_block.wav, 格挡 0.4s
sfx_dodge.wav, 闪避风声 0.3s
sfx_hit.wav, 命中 0.5s
sfx_combat_start.wav, 战斗开始剑鸣+鼓点 2s
```
战斗 BGM(ACE-Step,1 首):
```
bgm_combat.ogg, 60s 循环,紧张鼓点驱动,中国风
```
**交付物**
- `assets/voice/`:约 40 条 mp3
- `assets/sfx/`:6 个 wav
- `assets/bgm/bgm_combat.ogg`
---
### P2-09:v0.2 音频质检与验收
与 P1-11 相同流程,重点额外检查:
- 战斗音效(剑击/命中/格挡)在耳机下是否有空间感
- 战斗 BGM 循环是否流畅,是否与战斗张力匹配
- 数字语音(0-9)音调是否一致,拼接后是否自然
**交付物**
- `docs/音频质检_v02.md`
---
### P2-10:战斗音频流集成
**目标**
将战斗引擎的计算结果转化为正确的音频播报序列,实现策划案 7.2 节描述的完整战斗音频流。
**前置依赖**
P2-05(战斗引擎),P2-09(音频通过质检)
**实现方式**
`src/game/CombatNarrator.js`:将 Combat 的事件结果转为 VoiceQueue 条目:
```js
// 输入:combat.playerAction() 返回的 result 对象
// 输出:VoiceQueue.enqueue() 调用序列
narratePlayerAction(result, skill, target) {
// 音效层:立即播放
audioEngine.playSFX(skill.sfxAudio);
if (!result.hit) {
voiceQueue.enqueue({priority:2, segments:['voice/word_miss.mp3']});
return;
}
// 语音层:技能名 → 伤害数值 → 目标状态
const dmgAudio = this._buildNumberAudio(result.damage); // 拼接数字
voiceQueue.enqueue({priority:2, segments:[
skill.nameAudio,
'voice/word_damage.mp3',
...dmgAudio,
'voice/word_damage_suffix.mp3',
this._buildHpStatusAudio(target),
]});
}
```
**测试用例**
| # | 战斗事件 | 预期音频序列 |
|---|---------|------------|
| 1 | 玩家御剑术命中,造成 25 伤害 | 剑击音→"御剑术"→"造成"→"二十五"→"点伤害"→"野兔精气血剩余XX%" |
| 2 | 玩家攻击未命中 | 空气划过音→"未命中" |
| 3 | 怪物攻击玩家 | 怪物叫声→"[怪物名]发动攻击"→"你受到X点伤害" |
| 4 | 玩家 HP < 20% | 额外播报 P1 优先级"气血危急!" |
---
### P2-11:v0.2 集成测试与移动端验证
**测试场景**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 探索触发遭遇 | 听到警告语音 + 战斗 BGM 切入 |
| 2 | 完整回合流程 | 回合开始→出招→敌方行动→回合结算,语音流完整 |
| 3 | 切换技能再出招 | 只使用最后选择的技能 |
| 4 | 出招版本检查 | 新回合开始后未查看,双击无效 |
| 5 | 战斗胜利 | BGM 切换,播报经验/金币,返回探索态 |
| 6 | 战斗失败 | 播报失败,HP 恢复 30%,返回探索态 |
| 7 | 10 场连续战斗 | 无崩溃,无音频混乱 |
**v0.2 验收标准(阶段门禁)**
- 能完成从遭遇→战斗→胜利/失败→返回探索的完整流程
- 找 1 位用户不看屏幕,只听音频,能说出当前是第几回合、双方大致血量
---
## Phase 3:v0.3 内容原型(MVP)
**阶段目标**:角色创建 → 接教学任务 → 打怪完成任务 → 回城交任务。菜单/背包/装备/商店全部可用。
**验证问题**:能否通过音频完成"接任务→打怪→交任务"完整闭环?
---
### P3-01:角色创建流程
**目标**
实现纯语音引导的角色创建:道号生成/自定义、灵根选择、出身选择。
**前置依赖**
P2-01(属性系统),P1-05(语音队列)
**实现方式**
`src/state/CharCreateState.js`,三步流程:
```
步骤1:道号
→ 系统随机生成 5 个候选道号(从预设词库拼接:云/风/玉/剑/松+子/生/道人等)
→ 语音:"道号——云松子,可对否?双击确认,上滑/下滑换一个,右滑自定义。"
→ 自定义时:弹出键盘输入,确认后语音播报验证
步骤2:灵根
→ 语音:"请选择你的灵根属性。上滑下滑切换,双击确认。"
→ 5 种灵根轮播,每个附带简短属性说明语音
步骤3:出身
→ 3 种出身,选择决定初始房间和开局语音
→ 双击确认后播放 15-20 秒出生叙事音频,进入游戏世界
```
**交付物**
- `src/state/CharCreateState.js`
- 道号词库(`src/data/names.json`,100+ 个词组元素)
- 角色创建引导语音(TTS,约 20 段,纳入 P3-11)
**测试用例**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 上/下滑切换道号 | 每次听到新道号语音 |
| 2 | 右滑自定义道号 | 键盘弹出,输入"青云散人"后双击确认 → 语音"道号——青云散人,可对否?" |
| 3 | 选择出身后 | 播放专属出生叙事,然后出现在对应初始房间 |
---
### P3-02:NPC 数据与对话树
**目标**
定义教程 NPC(张道长)的完整对话树,支持首次见面与日常两种开场。
**前置依赖**
无
**实现方式**
`src/data/npcs.json`:
```json
{
"zhang_daochang": {
"id": "zhang_daochang",
"name": "张道长",
"room": "qingfeng_square",
"voiceRef": "old_man.wav",
"nameAudio": "voice/npc_zhang_name.mp3",
"dialogs": {
"first_meet": {
"audio": "voice/npc_zhang_first.mp3",
"text": "少侠,初来乍到吧?老夫张道长,在此已修行多年。",
"options": [
{ "id": "ask_quest", "text": "请教修炼之法", "audio": "voice/npc_zhang_opt1.mp3",
"next": "give_quest" },
{ "id": "ask_area", "text": "询问附近情况", "audio": "voice/npc_zhang_opt2.mp3",
"next": "area_info" },
{ "id": "farewell", "text": "告辞", "audio": "voice/common_farewell.mp3",
"action": "end_dialog" }
]
},
"give_quest": {
"audio": "voice/npc_zhang_quest.mp3",
"text": "镇北郊外近来妖兽横行,你若有意,可去清剿灰狼10只。",
"action": "offer_quest:quest_wolf_hunt",
"options": [ { "id": "accept", "action": "accept_quest" }, { "id": "decline" } ]
}
}
}
}
```
**交付物**
- `src/data/npcs.json`(张道长完整对话树)
- `src/game/DialogSystem.js`:解析对话树,处理 action
---
### P3-03:对话态交互实现
**目标**
实现对话态的手势交互,与策划案 3.3 节完全一致。
**前置依赖**
P1-06(状态机),P3-02(NPC数据)
**实现方式**
`src/state/DialogState.js`:
```
上/下滑:切换对话选项,播报选项文本
左滑:结束对话,返回探索态
右滑:重听 NPC 最后一句话
单击:播报当前选项内容
双击:选择当前选项
60 秒超时:自动结束对话
```
**交付物**
- `src/state/DialogState.js`
**测试用例**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 双击 NPC | 触发 first_meet 对话,播放对话语音 |
| 2 | 上/下滑 | 在3个选项间切换 |
| 3 | 右滑 | 重播 NPC 最后一句 |
| 4 | 选择"接受任务" | 触发 accept_quest action,播报"接受任务——清剿灰狼" |
| 5 | 左滑 | 结束对话,返回探索态,播报"你向张道长告辞" |
---
### P3-04:任务系统
**目标**
实现任务的接取、进度追踪、播报和完成结算。
**前置依赖**
P3-02,P2-06(战斗结算)
**实现方式**
`src/data/quests.json`(1 条教学任务):
```json
{
"quest_wolf_hunt": {
"id": "quest_wolf_hunt",
"name": "清剿灰狼",
"giver": "zhang_daochang",
"objectives": [
{ "type": "kill", "targetId": "grey_wolf", "required": 3, "current": 0 }
],
"rewards": { "exp": 200, "money": 50, "items": [] },
"progressAudio": "voice/quest_wolf_progress.mp3",
"completeAudio": "voice/quest_wolf_complete.mp3"
}
}
```
`src/game/Quest.js`:
```js
// 战斗结算时调用 Quest.onKill(monsterId)
// 播报进度:"灰狼 1/3。"
// 目标达成时播报:"任务目标完成!请回去找张道长。"
```
**交付物**
- `src/data/quests.json`
- `src/game/Quest.js`(接取、进度、完成全流程)
- 任务进度持久化到 `localStorage`
**测试用例**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 接取任务后击杀第一只灰狼 | 播报"灰狼 1/3" |
| 2 | 击杀第 3 只 | 播报"灰狼 3/3,任务目标完成,请回去找张道长" |
| 3 | 回到 NPC 对话选择"交任务" | 播报结算语音,获得奖励 |
| 4 | 刷新页面 | 任务进度保留 |
---
### P3-05:物品与背包数据
**目标**
定义 5 种 MVP 物品,实现背包数据结构(增删改查)。
**前置依赖**
无
**实现方式**
`src/data/items.json`(5 种):
```json
{
"iron_sword": { "id":"iron_sword", "name":"铁剑", "type":"weapon", "slot":"weapon", "physAtkBonus":10, "quality":"common", "nameAudio":"..." },
"cloth_robe": { "id":"cloth_robe", "name":"布衣", "type":"armor", "slot":"body", "physDefBonus":5, "quality":"common" },
"healing_pill": { "id":"healing_pill", "name":"续命丹", "type":"consumable", "effect":{"hp":50}, "quality":"common" },
"spirit_pill": { "id":"spirit_pill", "name":"回灵丹", "type":"consumable", "effect":{"mp":40}, "quality":"common" },
"wolf_pelt": { "id":"wolf_pelt", "name":"灰狼皮", "type":"material", "quality":"common" }
}
```
`src/game/Inventory.js`:
```js
// 最多 30 格
// add(itemId, qty),remove(itemId, qty),use(itemId),equip(itemId)
// 满背包时返回特殊结果,由调用方触发语音提示
```
**交付物**
- `src/data/items.json`
- `src/game/Inventory.js`
---
### P3-06:背包态交互实现
**目标**
实现完整的背包态手势交互(按策划案 3.3 背包/物品操作态)。
**前置依赖**
P1-06,P3-05
**实现方式**
`src/state/BackpackState.js`:
```
上/下滑:切换物品
左滑:返回菜单态
右滑:切换分类(全部/装备/丹药/材料/其他)
单击:进入物品详情层,播报详细信息
双击:默认操作(使用/装备)
物品详情层:
右滑:打开操作子菜单(使用/装备/丢弃/放入交易)
左滑:返回背包列表
上/下滑:在子菜单中切换
双击:执行操作
```
**交付物**
- `src/state/BackpackState.js`(含物品详情子层)
**测试用例**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 进入背包 | 播报"背包。第一件:[物品名]" |
| 2 | 单击一件装备 | 播报"铁剑,物理攻击+10,凡品。" |
| 3 | 双击续命丹 | 播报"你服下续命丹,气血恢复50点" |
| 4 | 右滑切分类 | 播报"丹药分类,共2件" |
| 5 | 背包满时拾取物品 | 播报"背包已满。获得灰狼皮。是否丢弃一件物品?" |
---
### P3-07:装备系统
**目标**
玩家可穿戴/脱下装备,装备效果即时反映到推导属性。
**前置依赖**
P2-01(属性系统),P3-05(物品数据)
**实现方式**
在 `Player.js` 中添加装备槽:
```js
this.equipment = { weapon: null, body: null, accessory1: null, accessory2: null };
// 穿戴时:player.physAtkBonus += item.physAtkBonus
// 推导属性(getter)自动反映变化
```
在 `BackpackState.js` 穿戴逻辑中:播报"你装备了铁剑。物理攻击力提升至30。"
**交付物**
- `Player.js` 装备槽逻辑
- 装备/脱下操作整合到 `BackpackState.js`
---
### P3-08:商店态实现
**目标**
实现 NPC 商店的完整交互(进入、浏览、购买)。
**前置依赖**
P3-03(对话态),P3-05(物品系统)
**实现方式**
在 `npcs.json` 中为张道长添加商品列表,对话选择"交易"后触发 `stateMachine.transition('shop', {npcId})`。
`src/state/ShopState.js`:
```
上/下滑:浏览商品
左滑:返回对话态
右滑:切换分类
单击:播报商品详情(名称、价格、效果)
双击:购买当前商品
→ 确认"购买续命丹×1,花费灵石20枚。剩余灵石:X枚。"
→ 灵石不足时播报"灵石不足。"
```
**交付物**
- `src/state/ShopState.js`
---
### P3-09:菜单态完善
**目标**
完善菜单态的全部 8 个菜单项,使其与实际游戏数据联动。
**前置依赖**
P1-09(菜单框架),P2-01(角色属性),P3-04(任务),P3-06(背包)
**实现方式**
完善各菜单项的双击行为:
- **角色信息**:播报真实属性("练气X层,气血Y/Z,灵力A/B,根骨C...")
- **修炼/突破**:播报当前经验和下一层需求,v0.3 不实现突破流程
- **背包**:切换到背包态
- **功法**:播报已学技能列表
- **装备**:播报当前穿戴状态
- **任务**:播报进行中任务列表和进度
- **社交**:播报"功能尚未开放"
- **系统**:播报版本号,提供"清除存档"操作
**交付物**
- 更新后的 `MenuState.js`
---
### P3-10:修炼与升级系统
**目标**
实现练气 1-3 层的自动升级(经验满自动提升)和打坐修炼(获取经验)。
**前置依赖**
P2-01(属性系统),P3-09(菜单)
**实现方式**
`src/game/Cultivation.js`:
```js
// 经验阈值:练气一层→二层=200,二层→三层=500
// 击杀怪物、完成任务后调用 player.gainExp(amount)
// 经验满时自动升层(练气1-3层自动,不需要主动突破)
// 升层时播报:"恭喜,你突破至练气X层!气血上限提升!"
// 打坐修炼(在安全区或修炼室双击开始):
// 每 30 秒播报一次"修炼中...",每分钟获得经验 5 点(占位值)
// 单击可查看当前修炼进度
// 移动/遭遇打断修炼
```
**交付物**
- `src/game/Cultivation.js`
- 修炼状态整合到探索态(安全区内检测双击开始修炼)
---
### P3-11:v0.3 音频资源批量生成
**目标**
生成 v0.3 阶段所需的全部新增音频(约 50 个文件)。
**前置依赖**
P0-03,P0-05
**内容清单:**
NPC 对话语音(张道长音色,15 段):
```
npc_zhang_first, "少侠,初来乍到吧?..."
npc_zhang_opt1, "请教修炼之法"
npc_zhang_opt2, "询问附近情况"
npc_zhang_quest, "镇北郊外近来妖兽横行..."
npc_zhang_accept, "好,老夫期待你凯旋。"
npc_zhang_decline, "无妨,有意时再来找老夫。"
npc_zhang_complete, "不错,你果然有几分资质..."
npc_zhang_daily_1, "少侠,今日修行如何?"
npc_zhang_daily_2, "此地灵气浓厚,适合打坐修炼。"
(+6 段其余对话选项和过渡语)
```
> 张道长用区别于旁白的另一参考音色(老者声线)
角色创建引导语音(旁白音色,10 段):
```
char_create_name, "欢迎来到苍玄大陆。请为自己取一个道号。"
char_create_confirm, "道号——{X},可对否?双击确认..."
char_create_element, "请选择你的灵根属性。上滑下滑切换,双击确认。"
char_create_fire, "火灵根。法术伤害见长,克金灵根。"
(+6 段其余灵根+出身介绍)
```
任务相关语音(10 段):
```
quest_accept, "接取任务——清剿灰狼。前往镇北郊外,击败3只灰狼。"
quest_wolf_progress, "灰狼 {N}/3。"(需要 N=1,2 两段)
quest_wolf_done, "任务目标完成!请回去找张道长。"
quest_wolf_complete, "任务完成——清剿灰狼。获得经验200点,灵石50枚。"
(+6 段其余通用任务语音)
```
物品名称语音(5 段):
```
item_iron_sword, "铁剑"
item_cloth_robe, "布衣"
item_healing_pill,"续命丹"
item_spirit_pill, "回灵丹"
item_wolf_pelt, "灰狼皮"
```
出生叙事(旁白音色,1 段 15-20s):
```
birth_narration, "苍玄历某年。灵气异动,上古秘境重现。你,一个普通的修士,就此踏上了漫长的修仙之路……"
```
**10.2 张道长参考音色选定**(与 P0-05 相同流程):
- 生成 3 段老者声线候选,评估后确定 `assets/voice/ref/old_man.wav`
**交付物**
- `assets/voice/*.mp3`:约 50 条新增语音
- `docs/音色选定记录.md` 更新:记录张道长音色选择
---
### P3-12:v0.3 音频质检与验收
与 P1-11、P2-09 相同流程。重点额外检查:
- 旁白音色与张道长音色**可被明显区分**
- 对话语音的语气与角色性格一致(老者:沉稳慈祥,不急促)
- 出生叙事:语速、停顿、情绪契合开场氛围
**交付物**
- `docs/音频质检_v03.md`
---
### P3-13:教学任务完整流程串联
**目标**
确保所有模块正确连接,玩家能完整走通 MVP 核心路径。
**前置依赖**
P3-01 ~ P3-12 全部完成
**核心路径验证:**
```
启动游戏
→ 角色创建(选道号、灵根、出身)
→ 出现在清风镇广场
→ 找到张道长(切换焦点→双击进对话)
→ 接取"清剿灰狼"任务
→ 前往镇北郊外(移动)
→ 触发遭遇战(野兔精或灰狼)
→ 战斗胜利(3只灰狼)
→ 任务目标完成播报
→ 返回广场找张道长
→ 交任务,领奖励
→ 菜单查看角色信息(级别/经验更新)
```
**交付物**
- 无新代码,此任务为集成验证
- `docs/MVP核心路径测试记录.md`:记录走查结果
---
### P3-14:MVP 完整体验测试与评估
**目标**
在真实移动设备上进行完整的 MVP 用户测试,评估核心假设是否成立。
**前置依赖**
P3-13
**测试方案**
**A. 自测核查单(开发者完成)**
| 类别 | 检查项 | 结果 |
|------|-------|------|
| 启动 | iOS Safari + Android Chrome 均能正常启动 | |
| 音频 | 全程无无声卡住现象 | |
| 手势 | 6 种手势识别准确率主观 > 90% | |
| 任务闭环 | 核心路径可从头走到尾 | |
| 存档 | 刷新后进度保留 | |
| 边界 | 背包满、灵力不足、触发危险移动均有正确反馈 | |
**B. 外部用户测试(≥ 2 人,未参与开发)**
观察要点(不提示,只看):
1. 启动后能否在 1 分钟内理解"手势=操作"
2. 能否不看屏幕完成移动和基本战斗
3. 能否找到并接取任务
4. 对音频质量的主观感受
收集反馈:
- 最困惑的操作是什么?
- 最难听清的语音是什么?
- 整体沉浸感(1-5分)
**交付物**
- `docs/MVP用户测试报告.md`:测试结果 + 问题列表 + 优先级排序
**验收标准(MVP 完成门禁)**
- 外部测试者能在无提示下完成"接任务→打怪→交任务"闭环
- 无 P0/P1 级别的体验缺陷(无声死锁、崩溃、任务无法完成)
- 整体沉浸感评分平均 ≥ 3 / 5
---
## 附录 A:任务依赖关系总览
```
P0-01 ──→ P0-02
P0-03 ──→ P1-10, P2-08, P3-11
P0-04 ──→ P1-07, P2-01, P2-02, P2-03
P0-05 ──→ P1-10
P1-01 ──→ P1-02, P1-03, P1-04
P1-04 ──→ P1-05
P1-05 ──→ P1-06
P1-02 ──┐
P1-03 ──┤
P1-05 ──┤─→ P1-06 ──→ P1-07 ──→ P1-08 ──→ P1-09
P1-10 ──→ P1-11 ──→ P1-12
P1-06 ──→ P2-04
P2-01 ──┐
P2-02 ──┤─→ P2-05 ──→ P2-06 ──→ P2-10
P2-03 ──┘
P2-04 ──→ P2-07
P2-08 ──→ P2-09 ──→ P2-10 ──→ P2-11
P2-01 ──→ P3-01
P3-02 ──→ P3-03 ──→ P3-04
P3-05 ──→ P3-06, P3-07, P3-08
P3-03 ──→ P3-08
P1-09 ──→ P3-09
P3-01 到 P3-12 全完成 ──→ P3-13 ──→ P3-14
```
---
## 附录 B:各阶段音频资产汇总
| 阶段 | TTS 语音 | 音效 | 氛围音 | BGM | 合计预生成文件 |
|------|---------|------|-------|-----|-------------|
| v0.1 | 30 | 0 | 2 | 1 | ~33 |
| v0.2 (增量) | 40 | 6 | 0 | 1 | ~47 |
| v0.3 (增量) | 50 | 0 | 0 | 0 | ~50 |
| **v0.3 累计** | **~120** | **6** | **2** | **2** | **~130** |
> UI 音效(13种)通过 Web Audio API 实时合成,不计入预生成文件。
---
## 附录 C:v0.2 初始角色数值(固定值,v0.3 才做角色创建)
| 属性 | 值 | 推导结果 |
|------|----|---------|
| 灵根 | 火 | 同系功法伤害×1.2 |
| 等级 / 境界 | 1 / 练气一层 | — |
| 根骨 | 10 | 气血上限=150,物理攻击=20,物理防御=15 |
| 灵力 | 8 | 灵力上限=104,法术攻击=16 |
| 悟性 | 8 | — |
| 神识 | 6 | 法术防御=9 |
| 机缘 | 5 | — |
| 初始灵石 | 50 | — |
| 初始装备 | 无 | — |