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

62 KiB
Raw Permalink Blame History

《听仙》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 项目:
    npm create vite@latest . -- --template vanilla
    npm install
    
  2. 修改 vite.config.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 的二维码,方便快速打开。
    #!/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 64kbps22050Hz
# 输出到 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-StepBGM/长音效):

  • 手动启动 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

实现方式

  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 结构:

<!-- 启动层(初始可见) -->
<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 时位移 < 10px300ms 内无第二次 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-03UI 音效合成模块

目标 用 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.js13 种音效全部实现)
  • 调试页面:按钮逐一触发每种音效(开发用,不进游戏)

测试用例

# 场景 预期
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.js
  • src/state/ExploreState.js(移动、焦点、单击、进菜单)
  • src/state/MenuState.js(基础菜单框架,菜单项为空占位)

测试用例

# 场景 预期
1 初始状态 进入探索态,触发 onEnter
2 探索态下滑 切换到菜单态,播放菜单打开音效
3 菜单态左滑 切换回探索态,播放菜单关闭音效
4 状态切换时旧状态 onExit 被调用 控制台可见日志

P1-07:房间数据结构与清风镇地图

目标 定义 5 个清风镇房间的数据,可在运行时被导航系统读取。

前置依赖 P0-04(设计已补全)

实现方式

src/data/rooms.json5 个房间):

{
  "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.json5 个房间完整定义)
  • src/game/World.jsgetRoom(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.js8 个菜单项,含超时逻辑)

测试用例

# 场景 预期
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 运行脚本并后处理:

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.oggambient_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-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 2v0.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 = 150physAtk = 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.json3 个技能完整定义)
  • src/game/SkillSystem.jsapplySkill(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:每回合使用攻击技能(随机选)
cautiousHP > 50% 时攻击,HP ≤ 50% 时有 30% 概率防御

交付物

  • src/data/monsters.json(2 种怪物 + 2 种怪物技能:scratch、bite、howl
  • src/game/MonsterAI.jschooseAction(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.jsonEnterRoom() 中:

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.jsonqingfeng_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 条目:

// 输入: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 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.json100+ 个词组元素)
  • 角色创建引导语音(TTS,约 20 段,纳入 P3-11

测试用例

# 场景 预期
1 上/下滑切换道号 每次听到新道号语音
2 右滑自定义道号 键盘弹出,输入"青云散人"后双击确认 → 语音"道号——青云散人,可对否?"
3 选择出身后 播放专属出生叙事,然后出现在对应初始房间

P3-02NPC 数据与对话树

目标 定义教程 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-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.json1 条教学任务):

{
  "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.json
  • src/game/Quest.js(接取、进度、完成全流程)
  • 任务进度持久化到 localStorage

测试用例

# 场景 预期
1 接取任务后击杀第一只灰狼 播报"灰狼 1/3"
2 击杀第 3 只 播报"灰狼 3/3,任务目标完成,请回去找张道长"
3 回到 NPC 对话选择"交任务" 播报结算语音,获得奖励
4 刷新页面 任务进度保留

P3-05:物品与背包数据

目标 定义 5 种 MVP 物品,实现背包数据结构(增删改查)。

前置依赖

实现方式

src/data/items.json5 种):

{
  "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.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 中添加装备槽:

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-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
初始装备