Files
xianxia/docs/MVP任务清单.md
2026-05-22 01:49:24 +08:00

2037 lines
62 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 《听仙》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(本地开发服务+手机局域网测试)
语言:JavaScriptES2022 模块化,不强制 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` 设置 `<meta name="viewport" content="width=device-width">` 和黑色背景。
**交付物**
- `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 InspectoriOS 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 64kbps22050Hz
# 输出到 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-StepBGM/长音效):**
- 手动启动 `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 1v0.1 纯音频交互原型
**阶段目标**:玩家能打开游戏,在 3-5 个房间之间移动,通过语音了解所处位置,通过菜单查看基本状态。无战斗,无NPC,无物品。
**验证问题**:手势操作是否直觉?音频队列是否清晰?玩家能否理解所处环境?
---
### P1-01:启动页与音频解锁
**目标**
实现极简启动页,用户点击"开始听仙"后解锁浏览器音频权限,进入全黑游戏模式。
**前置依赖**
P0-01
**实现方式**
`src/index.html` 结构:
```html
<!-- 启动层(初始可见) -->
<div id="launch-screen">
<button id="start-btn" aria-label="开始听仙">开始听仙</button>
</div>
<!-- 游戏层(初始隐藏,黑色背景) -->
<div id="game-screen" hidden aria-live="polite" aria-atomic="false"></div>
```
`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 时位移 < 10px300ms 内无第二次 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-03UI 音效合成模块
**目标**
用 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-10v0.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 生成城镇 BGMACE-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 文件大小 | 单条语音 < 100KBBGM < 1.5MB |
---
### P1-11v0.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-12v0.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 2v0.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:每回合使用攻击技能(随机选)
cautiousHP > 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-08v0.2 音频资源批量生成
**目标**
生成 v0.2 战斗原型所需的全部新增音频资源(约 30 个文件)。
**前置依赖**
P0-03P0-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,"状态已更新,请先查看。"
```
数字语音(TTS10 段):
```
num_0 到 num_9,文本"零一二三四五六七八九"各一段
```
通用战斗词汇(TTS10 段):
```
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, "获得"
```
技能名称语音(TTS3 段):
```
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 Audio6 个):
```
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
```
战斗 BGMACE-Step1 首):
```
bgm_combat.ogg 60s 循环,紧张鼓点驱动,中国风
```
**交付物**
- `assets/voice/`:约 40 条 mp3
- `assets/sfx/`6 个 wav
- `assets/bgm/bgm_combat.ogg`
---
### P2-09v0.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-11v0.2 集成测试与移动端验证
**测试场景**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 探索触发遭遇 | 听到警告语音 + 战斗 BGM 切入 |
| 2 | 完整回合流程 | 回合开始→出招→敌方行动→回合结算,语音流完整 |
| 3 | 切换技能再出招 | 只使用最后选择的技能 |
| 4 | 出招版本检查 | 新回合开始后未查看,双击无效 |
| 5 | 战斗胜利 | BGM 切换,播报经验/金币,返回探索态 |
| 6 | 战斗失败 | 播报失败,HP 恢复 30%,返回探索态 |
| 7 | 10 场连续战斗 | 无崩溃,无音频混乱 |
**v0.2 验收标准(阶段门禁)**
- 能完成从遭遇→战斗→胜利/失败→返回探索的完整流程
- 找 1 位用户不看屏幕,只听音频,能说出当前是第几回合、双方大致血量
---
## Phase 3v0.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-02NPC 数据与对话树
**目标**
定义教程 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-02NPC数据)
**实现方式**
`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-02P2-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-06P3-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-11v0.3 音频资源批量生成
**目标**
生成 v0.3 阶段所需的全部新增音频(约 50 个文件)。
**前置依赖**
P0-03P0-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-12v0.3 音频质检与验收
与 P1-11、P2-09 相同流程。重点额外检查:
- 旁白音色与张道长音色**可被明显区分**
- 对话语音的语气与角色性格一致(老者:沉稳慈祥,不急促)
- 出生叙事:语速、停顿、情绪契合开场氛围
**交付物**
- `docs/音频质检_v03.md`
---
### P3-13:教学任务完整流程串联
**目标**
确保所有模块正确连接,玩家能完整走通 MVP 核心路径。
**前置依赖**
P3-01 P3-12 全部完成
**核心路径验证:**
```
启动游戏
→ 角色创建(选道号、灵根、出身)
→ 出现在清风镇广场
→ 找到张道长(切换焦点→双击进对话)
→ 接取"清剿灰狼"任务
→ 前往镇北郊外(移动)
→ 触发遭遇战(野兔精或灰狼)
→ 战斗胜利(3只灰狼)
→ 任务目标完成播报
→ 返回广场找张道长
→ 交任务,领奖励
→ 菜单查看角色信息(级别/经验更新)
```
**交付物**
- 无新代码,此任务为集成验证
- `docs/MVP核心路径测试记录.md`:记录走查结果
---
### P3-14MVP 完整体验测试与评估
**目标**
在真实移动设备上进行完整的 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 | — |
| 初始装备 | 无 | — |