# 《听仙》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 | — | | 初始装备 | 无 | — |