62 KiB
《听仙》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 浏览器和局域网手机上实时预览。
前置依赖 无
实现方式
- 在
/home/xsl/xianxia/下初始化 Vite 项目:npm create vite@latest . -- --template vanilla npm install - 修改
vite.config.js开启局域网访问:export default { server: { host: '0.0.0.0', port: 5173 } } - 按上方目录结构创建空文件占位。
index.html设置<meta name="viewport" content="width=device-width">和黑色背景。
交付物
package.json+vite.config.jssrc/index.html(空白黑屏页,含"开始听仙"按钮占位)npm run dev成功启动,手机浏览器可访问
测试用例
| # | 操作 | 预期结果 |
|---|---|---|
| 1 | npm run dev |
控制台无报错,显示本地和网络地址 |
| 2 | PC 浏览器访问 localhost:5173 |
页面加载,黑色背景,有"开始听仙"按钮 |
| 3 | 手机浏览器访问局域网 IP | 同上,无布局错误 |
P0-02:移动端测试环境搭建
目标 确保每个阶段的成果都能在真实手机上测试(iOS Safari + Android Chrome)。
前置依赖 P0-01
实现方式
- 确认开发机与手机在同一局域网。
- 在手机浏览器上将游戏页面添加到主屏幕(模拟 PWA 全屏)。
- 准备 Chrome DevTools 远程调试(Android via USB)或 Safari Web Inspector(iOS via USB)。
- 写
scripts/open_qr.sh:用qrencode生成局域网 URL 的二维码,方便快速打开。#!/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:
# 读取 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 格式:
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 后处理验证:
# 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
实现方式
- 用 VoxCPM2 准备 3 组候选参考音频(建议:沉稳男声/清冽女声/中性叙事声)。
- 对每组音色,用相同的测试文本生成 3 句语音:
"你站在清风镇的中心广场。四周传来市集的喧嚣声。" "战斗胜利。你获得经验值 120 点,灵石 30 枚。" "菜单。角色信息。" - 在手机耳机下播放,评估清晰度、疲劳感(长时间听是否舒适)、仙侠感。
- 记录选定音色的参考音频路径到
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 结构:
<!-- 启动层(初始可见) -->
<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 音频解锁逻辑:
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:
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' */ }
}
关键参数(可配置常量):
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:
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:
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:
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:
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.jssrc/state/ExploreState.js(移动、焦点、单击、进菜单)src/state/MenuState.js(基础菜单框架,菜单项为空占位)
测试用例
| # | 场景 | 预期 |
|---|---|---|
| 1 | 初始状态 | 进入探索态,触发 onEnter |
| 2 | 探索态下滑 | 切换到菜单态,播放菜单打开音效 |
| 3 | 菜单态左滑 | 切换回探索态,播放菜单关闭音效 |
| 4 | 状态切换时旧状态 onExit 被调用 |
控制台可见日志 |
P1-07:房间数据结构与清风镇地图
目标 定义 5 个清风镇房间的数据,可在运行时被导航系统读取。
前置依赖 P0-04(设计已补全)
实现方式
src/data/rooms.json(5 个房间):
{
"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:
// 进入房间时的播报顺序(严格按策划案 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 运行脚本并后处理:
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.oggassets/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:
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:
{
"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:
{
"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:
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:
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() 中:
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 中):
_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 中内嵌,此任务确保:
- 每次状态变化(回合切换、敌方出招结算)都递增
stateVersion - 玩家单击查看状态时,
playerViewedVersion = stateVersion - 玩家上/下滑切换技能/目标时,也更新
playerViewedVersion - 双击出招时校验两个版本是否一致
测试用例
| # | 场景 | 预期 |
|---|---|---|
| 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 条 mp3assets/sfx/:6 个 wavassets/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 条目:
// 输入: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:
{
"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 条教学任务):
{
"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:
// 战斗结算时调用 Quest.onKill(monsterId)
// 播报进度:"灰狼 1/3。"
// 目标达成时播报:"任务目标完成!请回去找张道长。"
交付物
src/data/quests.jsonsrc/game/Quest.js(接取、进度、完成全流程)- 任务进度持久化到
localStorage
测试用例
| # | 场景 | 预期 |
|---|---|---|
| 1 | 接取任务后击杀第一只灰狼 | 播报"灰狼 1/3" |
| 2 | 击杀第 3 只 | 播报"灰狼 3/3,任务目标完成,请回去找张道长" |
| 3 | 回到 NPC 对话选择"交任务" | 播报结算语音,获得奖励 |
| 4 | 刷新页面 | 任务进度保留 |
P3-05:物品与背包数据
目标 定义 5 种 MVP 物品,实现背包数据结构(增删改查)。
前置依赖 无
实现方式
src/data/items.json(5 种):
{
"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:
// 最多 30 格
// add(itemId, qty),remove(itemId, qty),use(itemId),equip(itemId)
// 满背包时返回特殊结果,由调用方触发语音提示
交付物
src/data/items.jsonsrc/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 中添加装备槽:
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:
// 经验阈值:练气一层→二层=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-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 | — |
| 初始装备 | 无 | — |