HTML5 重写:6 款游戏 + 用户系统 + 全局设置 + 5 轮迭代重构

- 新增 web/ 纯音频 HTML5 平台:共享引擎(手势/WebAudio 三层/设置/震动)、
  Node 零依赖服务器(静态 + 注册登录 + 进度存档 API)
- 游戏:剑与声(RPG 新作)、声命线、暗室(重设计)、音序回响(重设计)、
  节拍行者(跟随领奏重构)、盲航(同向闪避重构);声东击西删除
- 全局功能:男声/女声双套 TTS 资产、三档语速、BGM 音量三档、
  震动反馈语言(10 种震型)、服务器进度同步
- 资产:899 ogg + 899 m4a(Qwen3-TTS 双性别语音、Stable Audio 3 音效、
  Mureka BGM 4+3 首),终检零缺失
- 文档:docs/design/ 13 篇设计审查文档、docs/audio/ 资产管线、
  web/server/README.md API 文档
This commit is contained in:
SoundGame Dev
2026-08-29 21:55:44 +08:00
parent f0f0a9b50e
commit cc0f8c008d
1885 changed files with 11246 additions and 5 deletions
+113
View File
@@ -0,0 +1,113 @@
# 音频资产清单(R1,2026-08-29)
本文档是 5 个 web 游戏的 voice / sfx / bgm 资产总账,以及双性别语音的音色分配。
## 目录约定(R1 新增)
- 双性别旁白:`web/<game>/audio/voice/male/<id>.{ogg,m4a}` 和 `web/<game>/audio/voice/female/<id>.{ogg,m4a}`,两个目录内 id 集合完全一致。
- 原有平铺文件 `web/<game>/audio/voice/<id>.{ogg,m4a}` **暂时保留**,等引擎切换读取 male/female 子目录后由后续轮次清理。
- 所有 web 音频均为双格式:`.ogg`(libvorbis q3,44.1kHz)+ `.m4a`(aac 128k,44.1kHz,iOS 用)。
- 生成管线:`tools/gen_voice_dual.py`(台词表内嵌,从各游戏原 gen 脚本只读提取),staging wav 在 `tools/gen_voice_dual_wav/<game>/<gender>/`。
## 音色分配表(游戏 × 性别 → speaker)
| 游戏 | male | female | 备注 |
|---|---|---|---|
| storyline | Uncle_Fu | Vivian | 仅旁白+系统音双性别;角色音色不动(见下) |
| escape | Uncle_Fu | Serena | male 沿用原旁白音色 |
| simon | Uncle_Fu | Serena | female 沿用原主持人音色 |
| rhythm | Uncle_Fu | Vivian | female 沿用原解说音色 |
| space | Uncle_Fu | Serena | female 沿用原导航音色 |
| rpg | Uncle_Fu | Serena | 旁白;NPC 单音色平铺(见 rpg 章节) |
关于 Ryan:冒烟测试样本在 `tools/samples/ryan_zh_*.wav`(3 条中文句)。Ryan 是英语母语音色(见 `env.d/tts.md`),项目已有中文发音投诉(反馈⑥),男声统一采用中文母语的 Uncle_Fu;如需换成 Ryan,改 `tools/gen_voice_dual.py` 的 `SPEAKERS` 表重跑即可。
## instruct 模板(每性别全游戏内一致)
同一游戏的同一类台词,male/female 使用同一 instruct 模板(仅性别描述词不同),数字/短句(≤2 字)一整批同 instruct 连续生成:
- storyline 旁白:male「低沉醇厚的中年男声旁白,冷静、缓慢,带着末世废墟的苍凉感」/ female「冷静低沉的女声旁白,语速缓慢,带着末世废墟的苍凉感」(另有 WARM/COLD/EPIC 三个结局变体,见脚本)
- escape(R2 起,全游戏同一常量):male「低沉平稳的男声,冷静、克制,带一点神秘感」/ female「沉稳平静的女声,冷静、克制,带一点神秘感」
- simon(R2 起,全游戏同一常量,两性别同文):「平静、清晰,像一个耐心的游戏主持人」
- rhythm(R3 起,全表统一):female(Vivian)「活力清晰的年轻女声,像节奏游戏主持人,情绪饱满但不夸张」/ male(Uncle_Fu)「低沉醇厚的男声,像节奏游戏主持人,平稳有力」
- space(R3 起,全表统一):female(Serena)「平静清晰的女声,像飞船领航员,沉稳可信」/ male(Uncle_Fu)「低沉醇厚的男声,像飞船领航员,沉稳有力」
- rpg(R4 起,旁白全表统一):female(Serena)「平静清晰的年轻女声,像在讲武侠故事」/ male(Uncle_Fu)「低沉醇厚的男声,像说书人,平静有韵味」;NPC 各角色 speaker+instruct 恒定(村长 Uncle_Fu·慈祥和缓的老者/老猎人 Eric·沙哑粗犷豪爽中带着暖意/头目 Dylan·凶狠粗鲁大声喝骂/寨主 Uncle_Fu·低沉阴狠慢条斯理)
## 各游戏资产清单
### storyline《声命线》
- voice 双性别版(male/ 与 female/ 各 36 条):旁白 + 系统音 = `help menu status1 status2 status3 p1–p8 n00_intro n01_wake n04_exit n05_signal n06_whistle n08_vent n09_dead n10_rain n11_child n13_leave n14_ignore n15_bridge n16_culvert n16b_culvert_c n17_cable n17b_cable_c n20_convoy n22_fight n23_tower e1_dawn e2_price e3_alone e4_static`
- voice 角色台词(**不做双性别**,沿用平铺原文件):
| 角色 | speaker | 文件 id |
|---|---|---|
| 林晚(主角) | Serena | n02_linwan n03_radio n12_take n19_betray n21_redeem n24_open n25_secure |
| 猎狗(反派) | Dylan | n07_dog n18_dog |
| 小满(小男孩) | Vivian(稚嫩童声 instruct) | e1_child |
| 林晨(妹妹,收音机录音) | Vivian(收音机 instruct) | n03_msg |
| 广播女声 | Vivian(机械广播 instruct) | n05_bcast |
约定:角色台词在两个性别目录下使用**同一份文件**(由开发复制/链接或引擎回退到平铺目录,见 round1-report)。
- sfx(12):`footsteps gunshot heartbeat radio_static rubble signal_beep struggle ui_confirm ui_menu water whistle wind`
- bgm(1):`dark_drone`
### escape《暗室》(R2 重设计版,最终清单见 `round2-voice-ids.md`)
- voice 双性别版(male/ 与 female/ 各 74 条):引导 `g0_intro g1_move g1_done g2_wall g3_left g4_down g5_inspect g5_key g6_doubletap g6_done g7_goal`、引导纠错 `guide_wrong_* guide_blunt_* guide_offpath guide_skip reopen_prompt`、三级提示 `stuck_ask stuck_{key,cabinet,box,gate}_{1,2,3}`、导航 `nav_{up,down,left,right}`、房间 `room_{storage,cabinet,power,gate}`、用时 `time_band1–4`、保留句 `wall start_room found_key inspect_* unlock_cabinet box_fixed fix_box escape use_fail inv_* replay again empty_1–3`、菜单 `help_ops menu_*`
- voice 已删除(R2):`n0–n9 nshi unit_min unit_sec time_prefix intro tutorial hint_key hint_cabinet hint_box hint_gate`(整句化替代);平铺 `voice/` 旧文件全部移除
- sfx(16):`amb_drip amb_hum amb_wind`(保留)+ R2 新增 `amb_storage amb_cabinet amb_power amb_gate amb_gate_live`(Stable Audio 3,6s,统一 -25dB 均值,storage/power 偏左、cabinet 偏右 pan)+ `door_creak gate_open pickup power_on spark step unlock wall win_chime`;已删除 `ui_deny`(反馈改震动)
- bgm(1):`drone`
### simon《音序回响》(R2 重设计版,最终清单见 `round2-voice-ids.md`)
- voice 双性别版(male/ 与 female/ 各 68 条):交互教学 `t0_welcome t0_goal t1_up–t6_double t_try t_ok t_wrong_* t_blunt_* t7_ready t7_retry t_skip`、轮次 `round_01–round_20 round_more`(整句)、成绩 `score_band1–6`(整句)、流程 `your_turn chances_out retry_use relisten replay rules_prompt rules gameover`、反馈 `correct wrong chance_plus new_record`、菜单 `menu_*`
- voice 已删除(R2):`di lun num_shi num_0–num_9 score_pre best_pre`(拼接件)+ `intro teach_* listen`(旧连播教学);平铺 `voice/` 旧文件全部移除
- sfx(7):`gameover tone_doubletap tone_down tone_left tone_right tone_tap tone_up`;已删除 `correct wrong newrecord`(反馈改震动)
- bgm:**已于 R1 按用户要求移除**(原 `theme`,代码引用由开发删除)
### rhythm《节拍行者》(R3 重构版,台词依据 `docs/design/03-rhythm-voice-ids.md`)
- voice 双性别版(male/ 与 female/ 各 31 条):开场流程 `welcome welcome_back tut_1 tut_2 tut_3 tut_done tut_skip mid_song`、教学纠正 `retry_early retry_late retry_gesture retry_listen retry_skip`、选曲解锁 `select_hint song1_name song2_name song3_name song_locked unlock_song2 unlock_song3`、结算 `grade_s grade_a grade_b grade_c new_best_combo restart`、菜单规则 `rules settings_hint menu_main menu_resume menu_lobby`
- voice 已删除(R3):`tut_start`(平铺旧 id 全部移除,其余旧 id 均在 R2 已替换)
- voice 修订(R5):`menu_main` 改词重录为「菜单。单击切换选项,双击确认。」(对齐 round2-review 裁决②全球菜单约定;选曲界面非模态菜单不受影响)
- sfx(7,R3 补齐):`inst_kick inst_snare inst_hat_l inst_hat_r inst_tom_hi inst_tom_lo inst_accent`(演奏乐器音,详见 round3-report §7;hat_l 闭合镲极左 / hat_r 开放镲极右,accent 为 A minor 合成亮音);已删除 `judge_* tick tick_accent`(R3)与 `cue_*`(R3 收尾,重构不再使用指令音)
- bgm(3,R3 重制):`track90`(90.01 BPM,85.33s)`track105`(104.99 BPM,73.14s)`track120`(120.00 BPM,72.00s),A minor 电子乐,整数小节无缝循环,loudnorm -20 LUFS;旧 `groove` 已删除
### space《盲航》(R3 备产,台词依据 `docs/design/04-space-voice-ids.md`)
- voice 双性别版(male/ 与 female/ 各 52 条):演练 `drill_*`(14)、保护期 `protect_*`(5)、功能引入 `feat_* shield_ready shield_block`(6)、被撞归因 `hit_*`(5)、里程碑 `ms_10 ms_25 ms_50 ms_100`、结算 `time_1–4 dodge_1–4 new_record restart_hint`、菜单系统 `second_run_ask menu_* help_recap settings_hint paused`
- voice 修订(R4):`menu_main` 改词重录为「菜单。单击切换选项,双击确认。」(对齐 round2-review 裁决②全球菜单约定)
- voice 已删除(R3):`num0–9 num_shi num_bai n_time n_sec n_dodge n_ge n_milestone intro1–4 hit1 hit2 gameover restart`(数字拼接与旧教学弃用);平铺旧文件全部移除
- sfx(12):`alarm boost crash dodge hit shield shield_block whoosh_down whoosh_left whoosh_right whoosh_up`
- bgm(1):`engine`
### rpg《剑与声》(R4 新游戏第一章,台词依据 `docs/design/05-rpg-ch1-voice-ids.md` + `05-rpg-addendum.md`)
- voice 旁白双性别版(male/ 与 female/ 各 108 条,male=Uncle_Fu 说书人 / female=Serena 武侠故事):序章 `intro_1–3`、系统状态 `sys_* help_* settings_hint st_*`、场景 `desc_*`(16)、选项 `opt_*`(18)、事件 `evt_*`(6)、战斗 `btl_* cmd_* tut_*`(27,含 R5 补录 `btl_half_bandit`)、商店 `shop_*`(7)、敌方档位 `foe_hp_high/mid/low`(addendum §6.2 补录)
- voice NPC 单音色平铺(14 条,落 `voice/` 根,引擎平铺回退):村长 `npc_headman_1–4`(Uncle_Fu·慈祥和缓)、老猎人 `npc_hunter_1–4`(Eric·沙哑豪爽)、寨丁头目 `npc_elite_1/half/die`(Dylan·凶狠)、黑风寨主 `npc_boss1_1/2/die`(Uncle_Fu·低沉阴狠,与村长同 speaker 不同 instruct)
- sfx(35):前摇 `tel_wolf tel_bandit tel_spider tel_elite tel_boss1 tel_croc tel_shaman tel_puppet tel_boss2`(1.0s 格挡窗口);战斗 `atk_swing atk_hit parry skill_slash enemy_die potion` + SYN `hurt flee`;探索 `chest drink_water rest door_heavy` + SYN `step coin levelup`;环境 `amb_village amb_path amb_fork amb_chest amb_stream amb_hunter amb_deepwood amb_gate amb_hall amb_swamp amb_peak`(5s 无缝循环,均值 -25dB);`ui_deny` 按设计不做
- bgm(4,Mureka):`village battle boss ending`(90s 无缝循环,loudnorm -20);探索期无音乐(addendum §3),`amb_*` 由游戏层手动 loop 接 sfx 总线
- 再生成方法见 `web/rpg/gen/README.md`
## 发音质量
### 本轮已采取的规避措施
- 数字类语音(num_0~9、shi、bai、第、轮、分、秒、颗等)一整批同 instruct 连续生成,减少拼接 prosody 漂移(反馈⑥根因之一)。
- 台词 >40 字按句拆分生成再拼接(句间 0.12s 静音),避免长句吞字。
- 每条 assert 时长 >0.3s、峰值 >0.05,统一峰值归一 0.89;同句两性时长比 >3 判离群并重生成。
### 多音字/发音风险清单(待人工试听确认)
以下条目为文本层面的已知风险点,本轮生成保留原文;如试听发现误读,建议按右栏换写法重生成(改 `tools/gen_voice_dual.py` 台词表):
| 位置 | 原文 | 风险 | 建议换写法 |
|---|---|---|---|
| storyline help | 双击:重听刚才的台词 | 「重」可能读 zhòng | 「双击:重新听刚才的台词」 |
| storyline n11_child / n13_leave / n16b | 数到一千、数数 | 「数」shǔ/shù | 误读时写「数一数,数到一千」 |
| storyline n05_bcast / n19_betray | 三、七点、五 | 坐标需逐字读 | 误读时写「三,七,点,五」 |
| storyline n16b | 数到一百七 | 可能读成「一百七十个」 | 写「数到一百七十」 |
| 多处(escape/simon/space/storyline) | 还(hái)在/还有/还能 | 「还」hái/huán | 误读时改写句子避开 |
| rhythm welcome | S / A / B / C 评级 | 字母发音 | 现行文本为「评级,S!」,试听确认字母读音 |
(本表为文本审查结果;时长离群自动检查已通过的部分见 round1-report,最终发音准确性需人工试听把关。)
+20
View File
@@ -0,0 +1,20 @@
# 全项目音频资产终检(R5,2026-08-29)
| 游戏 | ogg | m4a | 0字节 | 解码失败 | voice male/female id 一致 |
|---|---|---|---|---|---|
| storyline | 133 | 133 | 0 | 0 | ✅ 各 36(平铺 48 个角色/NPC 句,豁免) |
| escape | 166 | 166 | 0 | 0 | ✅ 各 74 |
| simon | 143 | 143 | 0 | 0 | ✅ 各 68 |
| rhythm | 72 | 72 | 0 | 0 | ✅ 各 31 |
| space | 116 | 116 | 0 | 0 | ✅ 各 52 |
| rpg | 269 | 269 | 0 | 0 | ✅ 各 108(平铺 14 个角色/NPC 句,豁免) |
## 结论
全部通过:双格式配对完整、无 0 字节、全部可解码、voice 性别目录 id 集合一致(storyline/rpg 平铺角色句按约定豁免)。
## 附注
- storyline 平铺 48 个 = 12 个角色台词(林晚/猎狗/小满/林晨/广播,单音色永久保留)+ 36 个 R1 旁白旧文件(与 male/ 内容重复的遗产,待引擎切换验证后清理,见 round1-report §2)。
- rpg 平铺 14 个 = NPC 句(设计如此,单音色)。
- 终检脚本:`tools/check_all_assets.py`(可重复执行)。
+72
View File
@@ -0,0 +1,72 @@
# R1 音频轮次报告(2026-08-29,音乐音效总监)
本轮主题:双性别旁白资产管线 + simon BGM 移除。Mureka 花费 $0(无新 BGM,预算余额 $1.85)。
## 1. 新目录约定(给开发)
- 旁白/系统音按性别分目录:`web/<game>/audio/voice/male/<id>.{ogg,m4a}` 与 `web/<game>/audio/voice/female/<id>.{ogg,m4a}`。
- 两个目录的 **id 集合完全一致**(每游戏核对过),引擎按用户选择整目录切换即可,无需按 id 特判。
- 双格式:每个 id 都有 `.ogg`(libvorbis q3 44.1k)和 `.m4a`(aac 128k 44.1k),沿用现有格式协商逻辑。
- **平铺的旧文件 `voice/<id>.*` 本轮保留未动**。引擎切到 male/female 子目录后,后续轮次再删除平铺文件(storyline 角色台词除外,见 §3)。
- staging wav 保留在 `tools/gen_voice_dual_wav/<game>/<gender>/`,供后续轮次增量重生成(管线支持断点续跑,已有且合格的 wav 会跳过)。
## 2. 生成统计
| 游戏 | ids/性别 | male 总时长 | female 总时长 |
|---|---|---|---|
| storyline | 36 | 806.6s | 960.6s |
| escape | 41 | 230.8s | 212.4s |
| simon | 30 | 63.4s | 62.9s |
| rhythm | 9 | 71.5s | 59.0s |
| space | 27 | 78.2s | 65.1s |
| **合计** | **143 × 2 = 286 条** | 1250.5s | 1360.0s |
落盘文件 286 × 2 格式 = 572 个。质量控制:全部通过 时长>0.3s / 峰值>0.05 / 峰值归一 0.89;数字类同 instruct 整批生成;>40 字台词拆句拼接。两性时长离群(>3 倍)初检 3 条,已全部重生成并复检通过:
- `simon/male/num_4`(8.00s → 0.48s)
- `simon/female/teach_end`(29.04s → 14.80s)
- `space/male/n_ge`(2.16s → 1.12s)
## 3. storyline 角色音色映射(角色台词不做双性别)
| 角色 | speaker | 文件 id(沿用平铺原文件) |
|---|---|---|
| 旁白/系统音 | male=Uncle_Fu / female=Vivian | 36 个双性别 id(见 asset-manifest.md) |
| 林晚(主角) | Serena | n02_linwan n03_radio n12_take n19_betray n21_redeem n24_open n25_secure |
| 猎狗(反派) | Dylan | n07_dog n18_dog |
| 小满(小男孩) | Vivian(童声 instruct) | e1_child |
| 林晨(妹妹录音) | Vivian(收音机 instruct) | n03_msg |
| 广播女声 | Vivian(广播 instruct) | n05_bcast |
**需要开发配合**:角色台词在两个性别下使用同一份音频。建议引擎对 storyline 做回退:在 `voice/<gender>/` 找不到的 id 回退读平铺 `voice/<id>`;或由开发/后续轮次把 12 个角色 id 复制进两个子目录(资产侧未复制,避免重复文件进入仓库前等引擎方案定稿)。
注意:female 旁白选用 Vivian 而非 Serena,因为 Serena 是主角林晚的音色,旁白与主角同声会混淆;代价是旁白与小满/林晨/广播同 speaker,已用截然不同的 instruct(冷静低沉 vs 童声/收音机/机械广播)区分,请策划试听确认。
## 4. 音色分配表
| 游戏 | male | female |
|---|---|---|
| storyline | Uncle_Fu(沿用原旁白) | Vivian |
| escape | Uncle_Fu(沿用) | Serena |
| simon | Uncle_Fu | Serena(沿用) |
| rhythm | Uncle_Fu | Vivian(沿用) |
| space | Uncle_Fu | Serena(沿用) |
Ryan(英语母语音色)未采用:项目已有中文发音投诉(反馈⑥),男声统一中文母语 Uncle_Fu。冒烟样本在 `tools/samples/ryan_zh_*.wav`,如策划试听认为可用,改 `tools/gen_voice_dual.py` 的 `SPEAKERS` 重跑即可。
## 5. 需要游戏代码配合的点
1. **引擎支持 voice 性别目录切换**:按用户设置在 `audio/voice/male|female/` 之间切换;storyline 需平铺回退(§3)。
2. **simon 删除 BGM 引用**:资产 `web/simon/audio/bgm/theme.{ogg,m4a}` 已按用户要求删除(bgm/ 目录现为空),请移除 game.js 中的 bgm 加载/播放逻辑及 preload 清单条目。
3. **语音选择 UI 入口**:需要「男声/女声」设置项(全局或每游戏),默认建议 male=storyline/escape、female=simon/rhythm/space(即各游戏原音色性别),具体由策划定。
4. 平铺旧 voice 文件待引擎切换完成后通知音频侧清理(R2+)。
## 6. 质量遗留问题
- 本轮 QC 为自动检查(时长/峰值/两性离群),**未做人工试听**;发音准确性需策划/QA 试听把关。文本层多音字风险清单在 `docs/audio/asset-manifest.md`(如 storyline help 的「重听」建议改「重新听」),确认误读后改台词表重跑单条即可。
- `simon/female/teach_end` 重生成后 14.8s(male 7.8s),比值正常但偏长,建议试听确认无重复啰嗦。
- 管线脚本:`tools/gen_voice_dual.py`(含全部台词表,支持 `--game/--gender/--only/--check/--convert`)。
## 7. Mureka 花费
本轮 $0(无新 BGM 需求;simon BGM 为删除而非重制)。累计 $0.15 / $2.00,未动 `tools/mureka_log.md`(无新条目)。
+59
View File
@@ -0,0 +1,59 @@
# R2 音频轮次报告(2026-08-29,音乐音效总监)
本轮主题:《暗室》《音序回响》重设计语音资产 + escape 环境音扩展。依据 `docs/design/01-escape-redesign.md`、`02-simon-redesign.md` 与 `00-global-ux.md`。Mureka 花费 $0。
## 1. 首要交付物
`docs/audio/round2-voice-ids.md` —— 两款游戏全部语音 id → 台词全文 → instruct 对照表,含新增/保留/删除标记和代码映射区间(time_band/score_band)。开发照此实现即可。
## 2. 新资产统计
| 游戏 | 性别 | id 数 | 总时长 | 文件数(ogg+m4a) |
|---|---|---|---|---|
| escape | male | 74 | 346.6s | 148 |
| escape | female | 74 | 320.6s | 148 |
| simon | male | 68 | 190.2s | 136 |
| simon | female | 68 | 154.3s | 136 |
合计 142 id × 2 性别 = 284 条语音(568 文件)。全部按 R2 新规范重录:每游戏同一 speaker + 同一 instruct 常量(00 文档 §3.2),instruct 不含语速词;一句一文件整句生成,无数字拼接。escape=Uncle_Fu/Serena,simon=Uncle_Fu/Serena(沿用 R1 分配)。
环境音新增 5 条(Stable Audio 3,6s 循环,ogg+m4a):`amb_storage amb_cabinet amb_power amb_gate amb_gate_live`,统一归一到 -25dB 均值(对齐旧 amb_drip/-28.7、amb_wind/-21.3 之间),storage/power 轻微偏左、cabinet 轻微偏右 pan 增强房间辨识;staging wav 在 `tools/gen_amb_wav/`。
## 3. 删除清单(资产侧已执行,代码侧开发跟进)
**escape voice**(20 id × 2 性别 × 2 格式 + 平铺):
- 数字拼接链:`n0–n9 nshi unit_min unit_sec time_prefix` → `time_band1–4` 替代
- 旧开场:`intro tutorial` → GUIDE `g0–g7` 替代
- 旧提示:`hint_key hint_cabinet hint_box hint_gate` → `stuck_*` 三级提示替代
**simon voice**(23 id × 2 性别 × 2 格式 + 平铺):
- 拼接件:`di lun num_shi num_0–num_9 score_pre best_pre` → `round_*`/`score_band*` 整句替代
- 旧教学:`intro teach_up teach_down teach_left teach_right teach_tap teach_doubletap teach_end listen` → 交互教学 `t*_*` 替代
**sfx**:`simon/correct wrong newrecord`、`escape/ui_deny` 删除(反馈改震动,00 文档 §2)。
**平铺目录**:escape/simon 的 `audio/voice/*.{ogg,m4a}` 平铺旧文件已全部移除(引擎已支持 gender 目录)。storyline/rhythm/space 的平铺文件未动。
## 4. QC 结果
- 284 条全部通过:时长 >0.3s、峰值 >0.05、峰值归一 0.89。
- 两性时长离群检测(>3 倍):escape、simon 均 **ALL OK**,本轮零重生成。
- 双格式逐条核对:male/female 各目录 id 数与清单精确一致(74/68),无缺失(convert 已改为按台词表驱动,staging 残留旧 id 不会误落盘)。
- 人工试听仍未做:发音准确性需策划/QA 把关;多音字规避已在新台词写作时执行(如"重听"→"重新听一遍"(reopen_prompt)、避免"还/得/着"歧义写法)。
## 5. 给开发的配合说明
1. 语音 id 以 `round2-voice-ids.md` 为唯一权威清单;删除 id 不得再引用(文件已不存在,引擎容错会静默跳过,但请清理引用)。
2. `time_band1–4` 区间映射:`<120s / 120–300s / 300–600s / ≥600s`;`score_band1–6` 映射:`0 / 1–3 / 4–6 / 7–9 / 10–14 / ≥15`。
3. escape L3 提示 = `stuck_<阶段>_3` + 按当前坐标算路链播 `nav_up/down/left/right`(两句以内);单击检查 = 先重播当前房间 `amb_*` 再播描述。
4. escape 大门通电后环境音切 `amb_gate_live`;空房 `amb_wind`/`amb_hum` 轮换。
5. simon 无 BGM(`bgm/` 已空,勿加载);`sfx/gameover` 保留为结束音。
6. 菜单句 `menu_open/menu_restart/menu_home/menu_back` 两款游戏已各备一份;设置值播报("当前:女声,正常语速")为跨游戏需求,本清单未覆盖,建议 R3 统一处理。
## 6. 管线变更
- `tools/gen_voice_dual.py`:escape/simon 台词表整体替换为 R2 新版(全常量 instruct);`--convert` 改为按台词表驱动;其余游戏(storyline/rhythm/space)台词表与资产未动,脚本仍可全量复跑。
## 7. Mureka 花费
本轮 $0(两款游戏均无新 BGM 需求,simon 为去 BGM)。累计 $0.15 / $2.00。
+242
View File
@@ -0,0 +1,242 @@
# R2 语音 id 清单:escape《暗室》/ simon《音序回响》
> 给开发:本文件是两款游戏重设计的**最终语音资产清单**。`✦` = 新增 id,`●` = 保留 id(文本不变或微调,全部按新 instruct 重录),`✗` = 删除 id(文件已从 voice/male|female/ 移除,代码不得再引用)。
> 所有 id 在 `audio/voice/male/` 与 `audio/voice/female/` 下成对存在(.ogg + .m4a)。
> 生成规范(00 文档 §3):每游戏同一 speaker 常量 + 同一 instruct 常量;instruct 不含语速词(语速由引擎 playbackRate 控制);一句一文件,禁止拼接。
## 音色与 instruct 常量
| 游戏 | 目录 | speaker | instruct(本游戏全部 id 共用) |
|---|---|---|---|
| escape | male | Uncle_Fu | 低沉平稳的男声,冷静、克制,带一点神秘感 |
| escape | female | Serena | 沉稳平静的女声,冷静、克制,带一点神秘感 |
| simon | male | Uncle_Fu | 平静、清晰,像一个耐心的游戏主持人 |
| simon | female | Serena | 平静、清晰,像一个耐心的游戏主持人 |
---
## 一、escape《暗室》(共 74 id)
### 1. 开场引导 GUIDE(G0–G7,✦ 全部新增)
| id | 台词 | 对应设计步骤 |
|---|---|---|
| g0_intro ✦ | 黑暗。你在废弃的地下设施里醒来,什么都想不起来。你必须找到出路。 | G0(单击继续) |
| g1_move ✦ | 先活动一下。朝任意方向滑一下,走一小步。 | G1 |
| g1_done ✦ | 很好,你迈出了第一步。 | G1 反馈 |
| g2_wall ✦ | 这个房间不大,四面都是墙。再滑两下,试试碰碰墙。 | G2 |
| g3_left ✦ | 跟着我指的路走。向左滑。 | G3 |
| g4_down ✦ | 对。再向下滑。 | G3 反馈 + G4 指令(同一句) |
| g5_inspect ✦ | 听到水声了吗?单击一下,仔细检查这个房间。 | G5 |
| g5_key ✦ | 你摸到了一把钥匙! | G5 反馈(谜题链第一环) |
| g6_doubletap ✦ | 钥匙能开锁。双击,是把物品用出去的用法。现在,双击试试。 | G6 |
| g6_done ✦ | 对,就是这样。接下来,靠你自己了。 | G6 反馈 |
| g7_goal ✦ | 找到能用这把钥匙打开的东西。需要帮助时,在原地双击。 | G7 → PLAY |
### 2. 引导纠错与跳过(✦)
| id | 台词 | 用途 |
|---|---|---|
| guide_wrong_up ✦ | 不是这个方向。向上滑。 | 做错方向纠正(4 向各一) |
| guide_wrong_down ✦ | 不是这个方向。向下滑。 | 同上 |
| guide_wrong_left ✦ | 不是这个方向。向左滑。 | 同上 |
| guide_wrong_right ✦ | 不是这个方向。向右滑。 | 同上 |
| guide_wrong_gesture ✦ | 不是这个操作。照我说的做。 | 手势类型错误(该滑动了却点按等) |
| guide_blunt_up ✦ | 手指贴着屏幕,向上推一下。 | 同一步错 3 次的直白重述(4 向各一) |
| guide_blunt_down ✦ | 手指贴着屏幕,向下推一下。 | 同上 |
| guide_blunt_left ✦ | 手指贴着屏幕,向左推一下。 | 同上 |
| guide_blunt_right ✦ | 手指贴着屏幕,向右推一下。 | 同上 |
| guide_offpath ✦ | 先跟我走。 | 引导期间偏离脚本路线的温和拦回 |
| guide_skip ✦ | 跳过引导,直接开始自由探索。 | 引导期间下滑跳过确认 |
| reopen_prompt ✦ | 上滑,重新听一遍引导。单击,直接开始。 | 二周目开场(已完成过引导) |
### 3. 卡关三级提示(✦ 12 句 + 前置句 + 导航句 4 句)
L1/L2 每阶段自动播一次;L3 = `stuck_<阶段>_3` + 按当前位置算路链播 `nav_*`(两句以内)。
| id | 台词 | 级别/阶段 |
|---|---|---|
| stuck_ask ✦ | 需要帮助吗? | L1 前置句(播完接 L1 内容) |
| stuck_key_1 ✦ | 铁锈味最重的地方,值得仔细看看。 | L1·找钥匙 |
| stuck_key_2 ✦ | 去西南角的储物间,单击检查,找到钥匙。 | L2·找钥匙(双击兜底也播这条) |
| stuck_key_3 ✦ | 跟我说的方向走,去西南角的储物间。 | L3·找钥匙(后接 nav_* 链) |
| stuck_cabinet_1 ✦ | 钥匙就在你身上。它能打开某一把锁。 | L1·开铁柜 |
| stuck_cabinet_2 ✦ | 去东边的铁柜房,双击,用钥匙打开铁柜。 | L2·开铁柜 |
| stuck_cabinet_3 ✦ | 跟我说的方向走,去铁柜所在的房间。 | L3·开铁柜 |
| stuck_box_1 ✦ | 保险丝属于电箱。有电流声的地方,就有电箱。 | L1·修电箱 |
| stuck_box_2 ✦ | 去西北角的电箱房,双击,把保险丝装进电箱。 | L2·修电箱 |
| stuck_box_3 ✦ | 跟我说的方向走,去电箱所在的房间。 | L3·修电箱 |
| stuck_gate_1 ✦ | 电已经通了。该去找那扇铁门了。 | L1·开大门 |
| stuck_gate_2 ✦ | 去东南角的大门,双击,打开它,离开这里。 | L2·开大门 |
| stuck_gate_3 ✦ | 跟我说的方向走,去大门所在的房间。 | L3·开大门 |
| nav_up ✦ | 向上滑。 | L3 导航词(算路链播,可两句连读) |
| nav_down ✦ | 向下滑。 | 同上 |
| nav_left ✦ | 向左滑。 | 同上 |
| nav_right ✦ | 向右滑。 | 同上 |
### 4. 房间描述(✦ 首次进入关键房间时播,配合环境音)
| id | 台词 | 房间 |
|---|---|---|
| room_storage ✦ | 一间储物间。货架上堆满杂物,角落里传来水滴声。 | 储物间 (0,2),环境音 amb_storage |
| room_cabinet ✦ | 这个房间里,立着一个铁柜。 | 铁柜房 (2,1),amb_cabinet |
| room_power ✦ | 这个房间的墙上,有一只电箱。 | 电箱房 (0,0),amb_power |
| room_gate ✦ | 面前,是一扇沉重的铁门。 | 大门 (2,2),amb_gate / 通电后 amb_gate_live |
### 5. 用时播报(✦ 4 段整句,替代全部数字拼接)
| id | 台词 | 区间(供代码映射) |
|---|---|---|
| time_band1 ✦ | 你逃出来了,用时不到两分钟。了不起! | t < 120s |
| time_band2 ✦ | 你逃出来了,用时不到五分钟。 | 120s ≤ t < 300s |
| time_band3 ✦ | 你逃出来了,用时不到十分钟。 | 300s ≤ t < 600s |
| time_band4 ✦ | 你逃出来了,用时超过十分钟。 | t ≥ 600s |
### 6. 保留 id(● 文本不变,按新 instruct 双性别重录)
| id | 台词 |
|---|---|
| wall ● | 那边是墙,换个方向。(新文本;引导 G2 与 PLAY 撞墙共用,每次撞墙都播) |
| start_room ● | 你站在设施中央的房间里。滑动移动,单击检查,双击使用物品。(新文本;跳过引导后的 PLAY 开场) |
| found_key ● | 你蹲下身在储物架的角落里摸索,指尖碰到一个冰凉的金属物件——是一把钥匙!钥匙到手了。 |
| inspect_cabinet_locked ● | 这个房间里立着一个铁柜,柜门上挂着一把锁。也许,有什么钥匙能打开它。 |
| unlock_cabinet ● | 钥匙插进锁孔,缓缓转动。咔哒一声,柜门开了。柜子深处,躺着一枚保险丝。保险丝到手了。 |
| inspect_box_dead ● | 墙上是一只电箱,箱盖敞开着,里面的保险丝座空空如也。没有电,这里的大门永远打不开。 |
| box_fixed ● | 电箱运转正常,电流的嗡鸣声平稳而持续。 |
| fix_box ● | 你把保险丝用力按进卡座。火花一闪,嗡鸣声响起——头顶的线路恢复供电了! |
| inspect_gate ● | 面前是一扇沉重的铁门。门边的电子锁一片漆黑,没有任何反应。它需要电力。 |
| inspect_gate_ready ● | 电子锁的指示灯亮了,发出微弱的绿光。大门已经通电。双击,打开它,离开这里。 |
| escape ● | 你扳动开关。电机轰鸣,沉重的铁门缓缓升起,刺眼的光线涌了进来。你眯起眼睛,向前迈出了第一步。自由了。你逃出来了。 |
| use_fail ● | 这里没有用得上东西的地方。换个房间试试,或者先单击检查一下周围。 |
| inv_empty ● | 你摸了摸身上,什么也没有。 |
| inv_key ● | 你身上带着一把钥匙。 |
| inv_fuse ● | 你身上带着一枚保险丝。 |
| inv_both ● | 你身上带着一把钥匙和一枚保险丝。 |
| replay ● | 黑暗再次合拢。你回到了中央的房间。这一次,你会更快。 |
| again ● | 单击屏幕,再来一次。下滑,打开菜单。(新文本;ENDING 提示) |
| empty_1 ● | 房间里空空荡荡,只有你自己的回声。 |
| empty_2 ● | 你伸出手,摸到的只有冰冷粗糙的墙壁。 |
| empty_3 ● | 除了远处的水滴声,这里什么都没有。 |
### 7. 菜单与帮助(✦)
| id | 台词 | 用途 |
|---|---|---|
| help_ops ✦ | 滑动,在房间之间移动。单击,检查房间,或者重听房间的声音。双击,使用物品,或者查看背包。 | 菜单「重听帮助」/ 兜底说明 |
| menu_open ✦ | 菜单。 | 菜单打开播报第一项前 |
| menu_restart ✦ | 重新开始。 | 菜单选项 |
| menu_home ✦ | 回大厅。 | 菜单选项(保存并回大厅) |
| menu_back ✦ | 返回游戏。 | 菜单选项 |
### 8. escape 删除清单(✗ 文件已移除,代码禁止引用)
- 数字拼接链(14):`n0 n1 n2 n3 n4 n5 n6 n7 n8 n9 nshi unit_min unit_sec time_prefix` → 由 `time_band1..4` 替代
- 旧开场(2):`intro tutorial` → 由 GUIDE(g0–g7)替代
- 旧四级提示(4):`hint_key hint_cabinet hint_box hint_gate` → 由 `stuck_*` 三级提示替代
- 平铺目录 `audio/voice/` 下的 escape 旧文件全部移除(引擎已切 gender 目录)
---
## 二、simon《音序回响》(共 68 id)
### 1. 交互式教学(✦ T0–T7;T8 即 round_01)
| id | 台词 | 用途 |
|---|---|---|
| t0_welcome ✦ | 欢迎来到音序回响。先认识六种声音。 | T0(与下句链播) |
| t0_goal ✦ | 我会演奏一串声音,请你记住顺序,原样复现。 | T0 第二句 |
| t1_up ✦ | 向上滑,是这个声音。 | T1(播完接 tone_up,再接 t_try) |
| t2_down ✦ | 向下滑,是这个声音。 | T2 |
| t3_left ✦ | 向左滑,是这个声音。 | T3 |
| t4_right ✦ | 向右滑,是这个声音。 | T4 |
| t5_tap ✦ | 单击,是这个声音。 | T5 |
| t6_double ✦ | 双击,是这个声音。 | T6 |
| t_try ✦ | 你试试。 | T1–T6 共用,等待玩家做该手势 |
| t_ok ✦ | 对了。 | 做对,进下一个 |
| t_wrong_up ✦ | 不是这个,向上滑试试。 | 做错纠正(6 手势各一) |
| t_wrong_down ✦ | 不是这个,向下滑试试。 | 同上 |
| t_wrong_left ✦ | 不是这个,向左滑试试。 | 同上 |
| t_wrong_right ✦ | 不是这个,向右滑试试。 | 同上 |
| t_wrong_tap ✦ | 不是这个,单击试试。 | 同上 |
| t_wrong_double ✦ | 不是这个,双击试试。 | 同上 |
| t_blunt_up ✦ | 手指贴着屏幕,向上推一下。 | 同一手势错 3 次的直白版(6 手势各一) |
| t_blunt_down ✦ | 手指贴着屏幕,向下推一下。 | 同上 |
| t_blunt_left ✦ | 手指贴着屏幕,向左推一下。 | 同上 |
| t_blunt_right ✦ | 手指贴着屏幕,向右推一下。 | 同上 |
| t_blunt_tap ✦ | 用指尖,轻轻点一下屏幕。 | 同上 |
| t_blunt_double ✦ | 用指尖,快速点两下屏幕。 | 同上 |
| t7_ready ✦ | 六种声音都记住了。后面的序列,会越来越长。 | T7(与下句链播) |
| t7_retry ✦ | 答错可以消耗机会重听。双击,可以随时重听。 | T7 第二句 |
| t_skip ✦ | 跳过教学,直接开始。 | 教学期间下滑跳过确认 |
### 2. 轮次播报(✦ 21 句整句)
| id | 台词 |
|---|---|
| round_01 … round_20 ✦ | 第一轮,听好了。/第二轮,听好了。/第三轮,听好了。/第四轮,听好了。/第五轮,听好了。/第六轮,听好了。/第七轮,听好了。/第八轮,听好了。/第九轮,听好了。/第十轮,听好了。/第十一轮,听好了。/第十二轮,听好了。/第十三轮,听好了。/第十四轮,听好了。/第十五轮,听好了。/第十六轮,听好了。/第十七轮,听好了。/第十八轮,听好了。/第十九轮,听好了。/第二十轮,听好了。 |
| round_more ✦ | 新的一轮,听好了。 | round > 20 兜底 |
### 3. 成绩播报(✦ 6 段整句)
| id | 台词 | 区间(供代码映射,score=完成轮数) |
|---|---|---|
| score_band1 ✦ | 一轮都没坚持下来。别灰心。 | score = 0 |
| score_band2 ✦ | 你坚持了三轮以内。 | 1–3 |
| score_band3 ✦ | 你坚持了五轮左右。 | 4–6 |
| score_band4 ✦ | 你坚持了将近十轮。 | 7–9 |
| score_band5 ✦ | 你坚持了十轮以上,很厉害。 | 10–14 |
| score_band6 ✦ | 你坚持了十五轮以上,大师级! | ≥ 15 |
### 4. 流程句(✦/●)
| id | 台词 | 用途 |
|---|---|---|
| your_turn ● | 轮到你了。 | 第 1 轮复现阶段(之后轮次仅震动) |
| chances_out ✦ | 重听机会用完了,小心。 | 机会归 0 时提示 |
| retry_use ✦ | 错了。用一次机会,再听一遍。 | 答错且有机会(自动重播序列) |
| relisten ✦ | 再听一遍。 | 输入阶段双击主动重听(耗机会) |
| replay ● | 单击屏幕,再来一局。 | 结算 |
| rules_prompt ✦ | 上滑,重听规则。下滑,打开菜单。 | 结算/非输入阶段提示 |
| rules ✦ | 上行音,向上滑。下行音,向下滑。左边的音,向左滑。右边的音,向右滑。一声轻响,单击。两声轻响,双击。 | 规则重听句(上滑) |
| gameover ● | 游戏结束。 | 答错且无机会(sfx/gameover 之后链播成绩) |
### 5. 反馈短句(● 文本不变重录)
| id | 台词 | 用途 |
|---|---|---|
| correct ● | 答对了! | 整轮复现成功(每 4 轮播一次,其余仅震动) |
| wrong ● | 错了。 | 答错(retry_use 已含"错了",此句供无机会外的即时反馈,可选) |
| chance_plus ✦ | 机会加一。 | 每过 4 轮机会 +1 |
| new_record ● | 新纪录! | 破纪录 |
### 6. 菜单(✦)
| id | 台词 |
|---|---|
| menu_open ✦ | 菜单。 |
| menu_restart ✦ | 重新开始。 |
| menu_home ✦ | 回大厅。 |
| menu_back ✦ | 返回游戏。 |
### 7. simon 删除清单(✗ 文件已移除,代码禁止引用)
- 数字拼接件(14):`di lun num_shi num_0 num_1 num_2 num_3 num_4 num_5 num_6 num_7 num_8 num_9 score_pre best_pre` → 由 `round_*` / `score_band*` 整句替代
- 旧教学连播(9):`intro teach_up teach_down teach_left teach_right teach_tap teach_doubletap teach_end listen` → 由交互式教学 `t*_*` 替代
- sfx(3):`sfx/correct sfx/wrong sfx/newrecord` 已删除(反馈改震动);`sfx/gameover` 与 6 个 `tone_*` 保留
- 平铺目录 `audio/voice/` 下的 simon 旧文件全部移除
---
## 三、escape 环境音(sfx,非语音)
| id | 状态 | 内容 |
|---|---|---|
| amb_drip | 保留 | 起点 (1,1):单一滴水 |
| amb_wind | 保留 | 空房轮换:风声弱化版 |
| amb_hum | 保留 | 空房轮换:远处嗡鸣弱化版 |
| amb_storage ✦ | 新增 | 储物间:滴水 + 金属杂物轻响 |
| amb_cabinet ✦ | 新增 | 铁柜房:嗡鸣 + 铁皮共振 |
| amb_power ✦ | 新增 | 电箱房:电流嗡鸣 + 偶尔火花 |
| amb_gate ✦ | 新增 | 大门:风声 + 门外空旷感 |
| amb_gate_live ✦ | 新增 | 大门通电后:风声 + 电流声 |
| ui_deny ✗ | 删除 | 反馈提示音退役(改震动) |
+68
View File
@@ -0,0 +1,68 @@
# R3 音频轮次报告(2026-08-29,音乐音效总监)
本轮主题:《节拍行者》重构语音 + 3 首 BPM 精确 BGM,《盲航》重构语音备产。依据 `docs/design/03-rhythm-voice-ids.md`、`03-rhythm-rebuild.md`、`04-space-voice-ids.md`。Mureka 本轮 $0.15 / $0.30,累计 $0.30 / $2.00。
## 1. rhythm 语音(重构版)
- 台词表照录 `docs/design/03-rhythm-voice-ids.md`(31 句),合入 `tools/gen_voice_dual.py` 全量替换 R1 版。female=Vivian / male=Uncle_Fu,全表同一 instruct 常量。
- 生成统计:**male 31 条 / 116.6s,female 31 条 / 105.4s**(ogg+m4a 各 62 文件)。
- QC:时长/峰值/两性离群(>3 倍)全部 **ALL OK,零重生成**。
- 旧 id 清理:平铺 9 个旧 id 全部删除(`welcome rules tut_start tut_done grade_* restart`,其中 `grade_* restart` 按新文案重录覆盖,`welcome` 文案变更重录);`tut_start` 彻底弃用。目录现仅含新表 31 id × 2 性别。
## 2. rhythm BGM(3 首,Mureka)
| 曲目 | 选用候选(task/choice) | 源实测 BPM | 处理 | 成品实测 BPM | 循环时长 |
|---|---|---|---|---|---|
| track90《启程》 | 157963509563393 / 157963627134980(首提候选 1) | 98.75 | atempo=0.911392 拉伸 | **90.01** | 85.33s(32 小节) |
| track105《追风》 | 157963652431875 / 157963780292611(首提候选 0) | **105.00 精确** | 无拉伸 | **104.99** | 73.14s(32 小节) |
| track120《星爆》 | 157964212305921 / 157964317097985(重提候选 0) | **120.00 精确** | 无拉伸 | **120.00** | 72.00s(36 小节) |
- 后处理:首个强 onset 对齐小节头裁剪 → 整数小节 + 末 2 拍与开头 acrossfade 无缝循环 → loudnorm I=-20/TP=-2 → ogg q3 + m4a 128k。成品 mean -21.2/-21.3/-19.9dB。
- **BPM 测量方法学(重要)**:`librosa.beat.beat_track` 在 22050Hz/hop512 下按整帧量化,105 BPM 附近一档=4.3 BPM(读数 103.36/107.67 都是量化伪影)。改用自写梳状扫描 `tools/bpm_precise.py`(0.05 BPM 步进 + 相位扫描 + 抛物线插值,合成 105.00 点击轨校准通过)。`tools/bpm_check.py`(粗筛)+ `bpm_precise.py`(判定)均留在 tools/。
- 重试记录:track90 首提 121.0/98.75、重提 80.0/120.0 四候选均不达标,已达"每首最多重试 1 次"上限,按规则停止重提,选用 98.75 拉伸修正(电子乐 8.9% 减速,瞬态影响可接受,成品实测 90.01)。track120 首提不达标、重提即中 120.00。track105 首提即中。
- 旧 `bgm/groove` 已删除。花费明细已追加 `tools/mureka_log.md`(5 任务 × $0.03 = $0.15)。
## 3. space 语音(备产,已完成)
`docs/design/04-space-voice-ids.md` 在本轮执行期间就绪(52 句),已全量生成:
- **male 52 条 / 188.0s,female 52 条 / 159.4s**(ogg+m4a 各 104 文件)。female=Serena / male=Uncle_Fu,全表同一 instruct。
- QC:**ALL OK,零重生成**。
- 旧 id 清理:`num0–9 num_shi num_bai n_* intro1–4 hit1 hit2 gameover restart` 全部弃用删除(含平铺);`shield_ready shield_block` 沿用 id 重录。
- 注意:重构版闪避改为**同向**(左来左滑),新台词已按此写死,与 R1 旧台词(反向)语义相反——开发务必以新 game.js + 新语音为准,不可混用。
## 4. 删除清单(资产侧已执行)
- rhythm voice 平铺 9 id;rhythm sfx `judge_good judge_miss judge_perfect tick tick_accent`(反馈改震动、数拍融入 BGM);rhythm `bgm/groove`。
- space voice 旧 27 id(平铺 + male/female 全部,新表 52 id 重录替换)。
## 5. 给开发的配合说明
1. **rhythm 乐器音 sfx(已于 R3 收尾补齐,见 §7)**:重构 §4 需要的 `inst_kick inst_snare inst_hat_l inst_hat_r inst_tom_hi inst_tom_lo inst_accent` 已生产落盘;`cue_*` 6 个旧提示音已确认不再使用并删除。
2. BGM 对网格:三首成品 BPM 实测偏差 ≤0.01%,直接按设计 SPB(0.667/0.571/0.5s)排程即可;循环长度均为整数小节,`src.start(t0)` 从 0 播即是小节头。
3. rhythm 结算评级句已含准确率档位文案(S=九成五/A=八成五/B=七成/C=不到七成),与 §2 判定阈值一致,代码直接按 rate 映射 grade_* 即可。
4. space 里程碑触发点:10/25/50/100 颗(`ms_*`);结算 time 档 `<30/30–60/60–120/≥120s`,dodge 档 `<5/5–14/15–49/≥50`。
## 6. Mureka 花费
本轮 $0.15(5 任务,全部 succeeded);累计 **$0.30 / $2.00**。详见 `tools/mureka_log.md` R3 节。
## 7. 补产:rhythm 演奏乐器音 sfx(R3 收尾,2026-08-29)
重构 §4 的 7 个手势乐器音已补齐,落 `web/rhythm/audio/sfx/`(ogg q3 + m4a 128k):
| id | 内容 | 生成方式 | 时长 | 峰值 | 声像/调性 |
|---|---|---|---|---|---|
| inst_kick | 底鼓(tap) | Stable Audio 3("punchy deep EDM kick, dry, single hit") | 0.50s | -0.8dB | 中央 |
| inst_snare | 军鼓(doubletap) | Stable Audio 3 | 0.45s | -1.1dB | 中央 |
| inst_tom_hi | 高通鼓(up) | Stable Audio 3(high pitched synth tom) | 0.50s | -1.2dB | 中央,升调感 |
| inst_tom_lo | 低通鼓(down) | Stable Audio 3(low pitched deep synth tom) | 0.60s | -1.0dB | 中央,降调感 |
| inst_hat_l | 闭合镲(left) | Stable Audio 3(closed hi-hat) | 0.20s | -1.1dB | **极左**(L=-1.1dB / R=-22.7dB) |
| inst_hat_r | 开放镲(right) | Stable Audio 3(open hi-hat) | 0.60s | -2.2dB | **极右**(R=-2.2dB / L=-23.3dB) |
| inst_accent | Perfect 亮音叠加层 | **numpy 程序合成**(音高可控) | 0.38s | -1.8dB(0.8 峰值) | A minor 上行琶音 A5→E6→A6,三音 90ms 间隔快衰减 |
- 调性处理:三首 BGM 均 A minor(Mureka prompt 指定);accent 用 A 小调音级(A/E/A)程序合成,音高精确对齐;鼓件为伪音高打击乐,tom_hi/lo 在 prompt 里分了高低音区。staging wav 在 `tools/gen_inst_wav/`。
- 后处理:去头静音(silenceremove -45dB)→ 定长裁剪 + 末端 40-100ms 淡出防爆音 → 峰值归一 -1dBFS → hat 先 downmix 再 pan 后归一(避免了 pan 叠加削波的一版返工)。
- **id 映射注意**:设计文档 §4 写的叠加层 id 是 `sfx/accent`,本轮按主 agent 指令统一命名 `inst_accent`(与其他 6 件同前缀),开发以 `inst_accent` 为准。
- **cue_* 处置:删除**。通读 03-rhythm-rebuild.md,全文无 cue_* 资产引用(仅 §8.2 有内部变量 `_cues` 指期望音符),§4 手势音全部改走 inst_* 乐器音——旧指令音 `cue_double cue_down cue_left cue_right cue_tap cue_up`(6 id × 2 格式)已删。
- Mureka 花费不变(本轮 $0,累计 $0.30 / $2.00)。
+102
View File
@@ -0,0 +1,102 @@
# R4 音频轮次报告(2026-08-29,音乐音效总监)
本轮主题:新 RPG《剑与声》第一章全套音频资产 + space `menu_main` 改词重录。依据 `docs/design/05-rpg-ch1-voice-ids.md`、`05-rpg-chapter1.md`、`05-rpg-addendum.md`。Mureka 本轮 $0.12 / $0.30,**累计 $0.42 / $2.00**。
## 1. 语音(107 旁白 ×2 + 14 NPC)
| 类别 | 数量 | 总时长 | 落点 |
|---|---|---|---|
| 旁白 male(Uncle_Fu·说书人) | 107 | 282.8s | `web/rpg/audio/voice/male/` |
| 旁白 female(Serena·武侠故事) | 107 | 242.6s | `web/rpg/audio/voice/female/` |
| NPC 单音色(村长/老猎人/头目/寨主) | 14 | 50.9s | `web/rpg/audio/voice/` 平铺 |
- 台词表照录 05-rpg-ch1-voice-ids.md(104 句)+ addendum §6.2 补录 `foe_hp_high/mid/low`(3 句)= 107 句,已合入 `tools/gen_voice_dual.py`(新增 `rpg` 条目与 `NPCS` 平铺机制,管线断点续跑/`--only`/`--check`/`--convert` 全部适用)。
- **计数勘误**:台词表头写"NPC 16 句",实表只有 14 句(headman 4 + hunter 4 + elite 3 + boss1 3);合计 107×2+14 = 228 条(×2 格式 = 456 文件),以此为准。
- QC:时长/峰值/两性离群(>3 倍)**ALL OK,零重生成**。
- 已知风险:村长与黑风寨主同 speaker(Uncle_Fu)不同 instruct(慈祥和缓 vs 低沉阴狠),按设计"从不在同场景出现"处理,**需人工试听确认区分度**。
## 2. BGM(4 首,Mureka)
| id | task/choice | 选用 | 成品 |
|---|---|---|---|
| village | 157968630808577 / 157968750477315 | 候选 1(229.2s) | 90.00s 循环,mean -23.0dB |
| battle | 157968792551426 / 157968893214722 | 候选 0(212.8s) | 90.00s,mean -20.5dB |
| boss | 157968932864001 / 157969029332994 | 候选 1(178.2s) | 90.00s,mean -22.6dB |
| ending | 157969071407105 / 157969188913155 | 候选 1(248.4s) | 90.00s,mean -21.9dB |
- 4 任务一次成功无重试。处理:强 onset 起点 → 90s 裁剪 + 首尾 3s crossfade 无缝循环 → loudnorm -20 LUFS → ogg q3 + m4a。RPG 无节拍网格,BPM 不做硬校验。
- addendum §3 裁定:`bgm/explore` 取消(探索期无音乐),amb 走 sfx 层。花费已追加 `tools/mureka_log.md`。
## 3. SFX(35 件)
- **SA3 29 件**(`tools/gen_rpg_sfx.sh`,staging `tools/gen_rpg_sfx_wav/`):前摇 `tel_*` ×9(含 ch2/3 备用 croc/shaman/puppet/boss2,统一裁 1.0s 格挡窗口)、战斗 `atk_swing atk_hit parry skill_slash enemy_die potion`、探索 `chest drink_water rest door_heavy`、环境 `amb_*` ×11(含备用 swamp/peak,6s→1s 首尾交叉淡化成 5s 循环)。
- **SYN 5 件**(`tools/gen_rpg_syn.py`):`hurt`(60Hz 低频闷击,与 atk_hit 区分)、`flee`(渐远脚步)、`step`(单步)、`coin`(硬币叮当)、`levelup`(A 小调五声 A4-C5-D5-E5-A5 上行钟声琶音)。
- 响度:one-shot 峰值 -1dBFS;amb 均值 -25dB(amb_hunter/amb_hall 因 crackle 峰值封顶初次仅 -31dB,已用 `alimiter level=false` 限幅重做至 -25.9dB——R2 学到的 level=true 陷阱的反向应用)。
- `ui_deny` 按设计不做。`web/rpg/gen/README.md` 有完整清单与再生成方法。
## 4. space `menu_main` 改词重录
按 round2-review 裁决②(全球菜单约定:单击切换/双击确认),文本由「菜单。左右滑选择,单击确认。」改为「**菜单。单击切换选项,双击确认。**」,双性别重录覆盖 `web/space/audio/voice/male|female/menu_main.*`。
**同类遗留**:rhythm `menu_main` 文本同为旧约定("左右滑选择,单击确认"),且 rpg 的 `sys_menu`/`shop_open` 按 RPG 豁免条款保持"左右滑+单击"不变(正确)。rhythm 那句建议 R5 一并处理(rhythm 选曲界面非模态菜单仍用左右滑+单击,仅模态菜单改新约定——需策划确认文案后我重录)。
## 5. 给开发的 id 总表(可直接 import)
语音 id 分组(旁白 male/female 同集,NPC 平铺单集):
```js
const RPG_VOICE = {
intro: ['intro_1','intro_2','intro_3'],
system: ['sys_continue_ask','sys_new_game','sys_saved','sys_rest','sys_drink',
'sys_menu','sys_menu_resume','sys_menu_save','sys_menu_help',
'help_explore','help_battle','settings_hint'],
status: ['st_hp_high','st_hp_mid','st_hp_low','st_gold_poor','st_gold_some','st_gold_rich',
'st_lv_low','st_lv_mid','st_lv_high',
'st_weapon_none','st_weapon_iron','st_weapon_steel','st_weapon_wind',
'st_armor_none','st_armor_leather','st_armor_iron'],
foeHp: ['foe_hp_high','foe_hp_mid','foe_hp_low'], // 敌方血量档位(addendum §6.2)
desc: ['desc_village','desc_village_re','desc_path','desc_path_re','desc_fork','desc_fork_re',
'desc_chest','desc_chest_re','desc_stream','desc_stream_re','desc_hunter','desc_hunter_re',
'desc_deepwood','desc_deepwood_re','desc_gate','desc_hall'],
option: ['opt_go_path','opt_go_fork','opt_go_chest','opt_go_stream','opt_go_hunter',
'opt_go_deepwood','opt_go_gate','opt_back','opt_shop','opt_rest','opt_status',
'opt_investigate','opt_open_chest','opt_drink','opt_talk_hunter','opt_assault',
'opt_sneak','opt_leave'],
event: ['evt_gold15','evt_nothing','evt_chest','evt_hunter_gift','evt_gate_open','evt_ch1_end'],
battle: ['btl_start_wolf','btl_start_bandit','btl_start_spider','btl_start_elite','btl_start_boss1',
'cmd_attack','cmd_skill','cmd_defend','cmd_item','btl_nomp','btl_noitem','btl_flee',
'btl_noflee','btl_enemy_die','btl_win','btl_loot_small','btl_loot_mid','btl_loot_big',
'btl_levelup','btl_lose','btl_lose_hint','tut_b1','tut_b2','tut_b3','tut_b4','tut_b5'],
shop: ['shop_open','shop_price_sword','shop_price_armor','shop_price_potion',
'shop_owned','shop_poor','shop_buy'],
npc: ['npc_headman_1','npc_headman_2','npc_headman_3','npc_headman_4',
'npc_hunter_1','npc_hunter_2','npc_hunter_3','npc_hunter_4',
'npc_elite_1','npc_elite_half','npc_elite_die',
'npc_boss1_1','npc_boss1_2','npc_boss1_die'], // 平铺 voice/,无性别目录
};
const RPG_BGM = ['village','battle','boss','ending'];
const RPG_SFX = {
telegraph: ['tel_wolf','tel_bandit','tel_spider','tel_elite','tel_boss1', // 1.0s 格挡窗口
'tel_croc','tel_shaman','tel_puppet','tel_boss2'], // ch2/3 备用
combat: ['atk_swing','atk_hit','parry','skill_slash','enemy_die','potion','hurt','flee'],
explore: ['step','coin','chest','drink_water','rest','levelup','door_heavy'],
ambient: ['amb_village','amb_path','amb_fork','amb_chest','amb_stream','amb_hunter',
'amb_deepwood','amb_gate','amb_hall','amb_swamp','amb_peak'], // 5s 循环,手动 loop 接 sfx 总线
};
```
配合要点:
1. `amb_*` 需手动 BufferSource loop 接 `_sfxBus`(addendum §3 对实现的唯一新要求;不受 bgm=off 设置影响)。
2. 前摇音 `tel_<enemy>` 起点即格挡窗口起点(1.0s),Boss 二阶段 `tel_boss1` 以 playbackRate 1.2 重播(不加资产)。
3. `foe_hp_*` 为战斗态上滑敌方档位句;降档才播 `st_hp_mid/low`。
4. 场景 `desc_*` 首次/回访成对(`_re` 后缀为回访短句)。
5. **设计缺口(请策划补)**:敌人表中山贼标了半血句 `btl_half_bandit`,但台词表无此 id(头目有 `npc_elite_half`)——按 chapter1 §3"无此语音的敌人跳过"可运行,如需补齐请发文案我重录。`npc_boss3_warn`/`btl_half_shaman`/`btl_half_shadow` 属 ch2/3 最小集(addendum §5),等 ch2 台词表。
## 6. 遗留问题
- 村长/寨主同 speaker 区分度、全部新语音发音准确性:未做人工试听(QC 均为自动检查)。
- rhythm `menu_main` 旧菜单约定文案(§4)。
- rhythm 乐器音 `inst_accent` 与设计文档 §4 的 `accent` 命名差异(R3 已按 inst_accent 落盘,开发以此为准)。
## 7. Mureka 花费
本轮 $0.12(4 任务全成功);累计 **$0.42 / $2.00**,剩 $1.58。明细见 `tools/mureka_log.md` R4 节。
+39
View File
@@ -0,0 +1,39 @@
# R5 音频轮次报告(2026-08-29,音乐音效总监)
最终轮收尾:两处小批量补录 + 全项目资产终检。Mureka 本轮 $0(累计 **$0.42 / $2.00**,未再变动)。
## 1. 补录明细
### rhythm `menu_main` 改词重录(与 space R4 同款问题)
- 旧文案「菜单。左右滑选择,单击确认。」→ 新文案「**菜单。单击切换选项,双击确认。**」(round2-review 裁决②全球菜单约定;rhythm 选曲界面非模态菜单,仍左右滑+单击,不受影响)。
- 双性别重录(male=Uncle_Fu 4.00s / female=Vivian 4.16s,沿用 rhythm 统一 instruct),覆盖 `web/rhythm/audio/voice/male|female/menu_main.{ogg,m4a}`。台词表已同步更新(`tools/gen_voice_dual.py` RHYTHM_LINES)。
### rpg `btl_half_bandit` 补录(设计缺口)
- 敌人表(05-rpg-chapter1 §4.2)给山贼标了半血句但台词表漏发;按 B 组风格补写「**山贼的攻势缓了下来。**」,已合入 `tools/gen_voice_dual.py` RPG_LINES(B 组,27→含此句)。
- 双性别生成(male=Uncle_Fu 2.64s / female=Serena 1.52s),落 `web/rpg/audio/voice/male|female/btl_half_bandit.{ogg,m4a}`。rpg 旁白现为 108 id ×2。
- 触发点:`CHECK_ENEMY_DEAD` 前、山贼 HP 首次 ≤50% 时插播(chapter1 §3 半血叙事钩子)。
## 2. 全项目资产终检(`docs/audio/final-asset-check.md`)
终检脚本 `tools/check_all_assets.py`(可重复执行),检查 6 游戏:双格式配对、0 字节、ffprobe 全量解码、voice 性别目录 id 集合一致性(storyline/rpg 平铺角色句豁免):
| 游戏 | ogg | m4a | voice 性别一致性 |
|---|---|---|---|
| storyline | 133 | 133 | ✅ 各 36(平铺 48 = 12 角色 + 36 R1 旁白遗产) |
| escape | 166 | 166 | ✅ 各 74 |
| simon | 143 | 143 | ✅ 各 68 |
| rhythm | 72 | 72 | ✅ 各 31 |
| space | 116 | 116 | ✅ 各 52 |
| rpg | 269 | 269 | ✅ 各 108(平铺 14 NPC) |
| **合计** | **899** | **899** | 全部通过 |
零 0 字节、零解码失败、零格式缺失。
## 3. 五轮累计遗留(供后续迭代参考)
1. **人工试听从未执行**:全部 QC 为自动检查(时长/峰值/离群/BPM/电平)。多音字风险清单在 asset-manifest.md,村长 vs 寨主同 speaker 区分度(R4)为最高优先试听项。
2. storyline 平铺 36 个 R1 旁白遗产文件待引擎切换验证后清理。
3. ch2/3 台词表最小集(addendum §5:`npc_boss3_warn`、`btl_half_shaman`、`btl_half_shadow`)待策划补发。
4. Ryan 音色未采用样本在 `tools/samples/ryan_zh_*.wav`(如策划想给某游戏换年轻男声,改 SPEAKERS 重跑即可)。
+174
View File
@@ -0,0 +1,174 @@
# 00 · 全局交互规范(Global UX)
> **给开发者的实现要点摘要**
> 1. 设置项(voice / speed / bgm)只在大厅页用 `<select>` 可视化 UI 修改,存 `localStorage["soundgame.settings"]`,由 `engine/settings.js` 管理;游戏启动时只读生效,游戏内不提供修改入口(菜单里只播报当前值)。**R1 已落地,本文 §1/§2/§4 已按实现修订(R2 修订版)。**
> 2. 所有提示音反馈改为震动:`engine/haptics.js`(**已实现**,震型名小写:`confirm/error/success/warning/swipe/heartbeat`);iOS Safari 不支持时**静默降级,绝不回退到提示音**。
> 3. 旁白全部整句预生成:禁止数字/碎片拼接(音色漂移根因),每款游戏旁白固定同一 `speaker` + 同一 `instruct` 常量。
> 4. 手势全局约定:上滑=帮助/状态、下滑=菜单、单击=确认/继续、双击=重听、左右滑=选择/移动;以滑动为核心玩法的游戏可偏离(见 §5 豁免表),但必须在开场交互教学中讲清。
> 5. 用户系统:大厅页登录,进度经 `web/server/server.js` REST API 存服务器(**已实现**),本地 localStorage 做离线缓存。
---
## 1. 设置系统(已实现:engine/settings.js + 大厅设置面板)
### 1.1 三个设置项的精确语义
| 设置项 | 取值(实现值) | 精确语义 |
|---|---|---|
| `voice` | `"female"`(默认)/ `"male"` | 切换 **voice 资产目录**。`play('voice/xxx')` 实际解析为 `audio/voice/<gender>/xxx.{ogg,m4a,mp3,wav}`,找不到时**回退平铺的 `audio/voice/xxx.*`**(已实现;storyline 角色台词靠此回退)。两性别的 id 集合由资产侧保证完全一致(见 docs/audio/round1-report.md),因此不做"回退另一性别"的逻辑。 |
| `speed` | `1.0`(默认)/ `1.35` / `1.7` | **只作用于 voice 层**的 `playbackRate`(创建 voice source 时实时读取,**从下一句开始生效**,正在播的句子不变速)。BGM、sfx 速率不变。TTS 一律按 1.0 语速生成;`instruct` 里禁止出现语速描述词。 |
| `bgm` | `1.0`(默认)/ `0.3` / `0` | BGM 层独立增益总线系数。`0` = 不加载、不播放任何 `bgm/` 资产(`_playBgm` 短路),ducking 逻辑保持原样。改设置即时生效(已实现 subscribe 机制)。 |
存储:localStorage key = `soundgame.settings`,值形如:
```json
{ "voice": "female", "speed": 1.0, "bgm": 1.0 }
```
注意(与设计初稿的差异,**以实现为准**):`speed`/`bgm` 存**数值**(1.0/1.35/1.7、1.0/0.3/0),不是字符串枚举;`engine/settings.js` 对非法键/值逐项丢弃,永不炸。
### 1.2 修改入口:只在大厅页(已实现)
- `web/index.html` 大厅已有"设置"面板:三个 `<select>`(旁白音色:女声/男声;语速:正常/快/特快;背景音乐:正常/小/关闭),变更即 `settings.set()` 写 localStorage。
- 试听句(设计有、R1 未做):变更后播一句样例语音演示 voice+speed,列为 R3 打磨项,非阻塞。
- 游戏内:**只读**。`audio.js` 自动应用三项设置,游戏代码无需改动;下滑呼出的菜单里用语音播报当前设置(如"当前:女声,正常语速"),并提示"请回到大厅修改设置"。
### 1.3 服务器同步(已实现,形式与初稿不同)
- 实现:**没有独立的 `/api/settings`**。设置作为特殊游戏键 `_settings` 存进进度接口:大厅在变更时 `PUT /api/progress { game: '_settings', data: settings.get() }`;登录后 `GET /api/progress?game=_settings` 拉回覆盖本地(服务器为准)。
- 离线/未登录:纯 localStorage,行为不变。
## 2. 震动语言规范(已实现:engine/haptics.js)
### 2.1 动机
用户反馈⑦:提示音(确认音、错误音)与游戏音效/线索音混在一起,盲玩时无法区分"这是反馈还是游戏内容"。震动是独占的触觉通道,不会与任何音频信息混淆。所有"操作反馈"一律用震动;音频只承载游戏内容与叙事。
### 2.2 引擎模块(以实现为准)
`web/engine/haptics.js`(R1 已落地):
```js
import { vibrate, hapticsSupported, HAPTIC_PATTERNS } from '../engine/haptics.js';
vibrate('error'); // 预定义名称(小写)
vibrate([15, 40, 15]); // 自定义震型数组
```
已实现预定义震型(R2 已补全,**以此 10 种为准**):
| 名称 | 震型 (ms) | 语义用途 |
|---|---|---|
| `confirm` | [20] | 确认:点按确认、菜单选中 |
| `error` | [60, 40, 60] | 错误:两连震(做错手势、无效操作) |
| `success` | [100] | 成功:长震(过关、解谜成功、升级) |
| `warning` | [30, 50, 30, 50, 30] | 警告:三连短震(血量低、卡关提示) |
| `swipe` | [10] | 滑动识别成功(最轻,引擎内置) |
| `heartbeat` | [40, 100, 40] | 心跳/倒计时紧张感 |
| `pickup` | [15] | 获得物品/机会(单轻震,不与 error 混淆) |
| `hit` | [80] | 受击/终结性失败:单次重震 |
| `beat` | [8] | 节拍锚点(simon/rhythm 领奏专用,极轻) |
| `danger` | [25, 40, 25, 40, 25] | 危险逼近(可循环,每遍间隔 ≥400ms;RPG 无声前摇用它代替听觉提示) |
### 2.3 语义绑定规则(含引擎内置反馈的分工)
- **gesture.js 已内置基础反馈**(R1 实现):滑动识别成功→自动 `swipe`;tap/doubletap→自动 `confirm`。因此**游戏层不要再对"手势被识别"本身调 `confirm`/`swipe`,否则双重震动**。游戏层只在语义事件处调用:`error`(操作被判定为错)、`success`(过关/成就)、`warning`(警告)、以及自定义 PICKUP/HIT/BEAT/DANGER。
- 震型按**语义**选用,不按游戏选用:任何游戏的"操作错误"都是 `error`,玩家在不同游戏间迁移时触觉语言一致。
- 单次震动总时长 ≤ 500ms;DANGER 循环时每遍间隔 ≥ 400ms,事件结束立即 `navigator.vibrate(0)` 停止。
- 震动的触发源只有两类:a) 玩家的手势被系统接收/拒绝(引擎内置或游戏层语义判定);b) 明确的离散游戏事件(受击、警告、节拍点)。**禁止**持续型/背景型震动。
### 2.4 绝不用震动的场景
1. **长对白播放中**(voice 层有音频在播):旁白叙事期间不震动,避免震动机理掩盖语音细节、也让玩家误以为需要操作。例外:教学流程里"现在请向左滑"这类等待玩家操作的指令句,在语音**播完后**允许一次轻震提示可以操作——震动发生在 voice 结束后,不重叠。注意:教学里玩家出手后引擎已自动震 swipe/confirm,游戏层通常无需再补。
2. 序列/乐句播放中需要玩家纯聆听记音高的阶段(simon/节奏游戏领奏时例外:BEAT 本身就是节拍信息通道,见 02/03 文档)。
3. 同一 200ms 窗口内多个事件只震一次(实现上去抖)。
4. 开场遮罩、大厅页等可视化 UI 不震动。
### 2.5 iOS Safari 降级策略
iOS Safari 不支持 `navigator.vibrate()`。策略:**静默降级**——`haptics.js` 特性检测 `'vibrate' in navigator`,不支持即 no-op(已实现)。**不允许**回退到提示音(反馈⑦的根因就是提示音混淆;iOS 用户没有反馈音,好过错乱的反馈音)。iOS 玩家依赖已有的音频结果反馈(脚步、撞击、机关声),这些属于游戏内容音,保留。各游戏开场教学统一口径:"做对做错,手机会震动告诉你;iPhone 用户听结果声"。
## 3. 旁白质量规范
### 3.1 根因
用户反馈⑥的"音色漂移、发音不准"来自两类做法(旧版 escape/simon/space):
1. **碎片拼接**:`voice/di` + `voice/num_5` + `voice/lun` 拼"第 5 轮"。每个碎片独立生成,音色、气息、响度逐片漂移,接缝明显。
2. **同游戏多 instruct / 长短不一**:有的句带情感指令有的不带,批次间模型状态不一致。
### 3.2 规范(所有游戏强制执行)
1. **一句一文件,整句生成**。凡要播报给玩家的信息,整句一次 TTS 生成,禁止运行时拼接。例外:>40 字的长台词由资产管线在生成期拆句拼接(R1 管线已支持,见 docs/audio/round1-report.md),运行时仍不拼。
2. **数字信息改分段整句**。需要报数字的场景(轮数、成绩、存活时间、用时、准确率),按取值分段预生成整句:
- 取值少且可数(如 simon 轮数):直接为每个值生成整句,"第一轮"…"第二十轮",超出范围用无数字兜底句。
- 取值连续(如存活秒数、准确率):按区间分段整句,如"不到三十秒""超过一分钟""准确率超过八成五"。宁粗勿拼。
- 各游戏的具体分段在各自设计文档中给出。
3. **同游戏同音色**:每款游戏的旁白用固定 speaker 常量 + 固定 INSTRUCT 常量。R1 音色分配表(docs/audio/round1-report.md §4,**以此为准**):male 一律 Uncle_Fu;female:storyline=Vivian、escape=Serena、simon=Serena、rhythm=Vivian、space=Serena。角色音(storyline/RPG 的 NPC)可另有角色 speaker,但**同一角色**所有句子的 speaker+instruct 必须是同一常量,写死在 gen 脚本的台词表表头注释里;NPC 台词不做双性别,平铺存 `voice/` 根目录(引擎平铺回退会命中)。
4. **短句优先**:单句 ≤ 25 个汉字为宜;长信息拆成多句链播(onComplete 串行),每句仍是完整句,不是碎片。
5. **生成管线**:统一用 `tools/gen_voice_dual.py`(R1 已建,台词表驱动,female/male 双跑、断点续跑、ogg+m4a 双格式、峰值归一 0.89)。新台词表由策划以"voice-ids 文档"形式交付(id + 全文 + speaker + instruct),音频总监照录进该脚本。
6. **验收**:自动 QC(时长/峰值/两性时长离群)+ 人工试听;多音字风险句在台词表中标注替代写法(如"重听"→"重新听")。
## 4. 用户系统(服务器进度,已实现:web/server/server.js)
### 4.1 形态(已实现)
- 大厅页"用户"面板:用户名 + 4-6 位数字 PIN 注册/登录;token 存 localStorage key `soundgame.auth`(形如 `{ username, token }`),请求头 `Authorization: Bearer <token>`。
- 服务端 `web/server/server.js`(Node 零依赖,静态托管 + API 一体,`serve.sh` 拉起);数据落盘 `web/server/data/`。局域网玩具级认证,勿暴露公网。
- 未登录/服务器不可达:静默降级为仅本地模式,进度只存 localStorage(key `soundgame.progress.<game>`)。
### 4.2 API 约定(以实现为准)
```
POST /api/register { username, pin } -> 201 { ok, username }
POST /api/login { username, pin } -> 200 { ok, token, username }
GET /api/progress?game=<game> (Bearer) -> 200 { game, data } (无存档则 data=null)
PUT /api/progress { game, data } (Bearer) -> 200 { ok, game }
```
与初稿的差异(**以实现为准**):进度读写用 `?game=` 查询参数 / 请求体字段,不是路径段;`data` 是各游戏**自由格式**的 JSON(服务器不校验结构);没有统一的 `{schema, game, updatedAt, data}` 信封——游戏自行在 `data` 内放 `schema`/`updatedAt` 字段(建议保留,便于迁移)。
### 4.3 进度 data 的两条约定
1. **`summary` 字符串(必须)**:大厅游戏卡片会直接显示 `data.summary`(已实现,index.html `loadProgress()`)。每个游戏的存档 data 里必须维护一句人话摘要,如 `"第一章·溪流 · Lv3 · 金币120"`、`"最高纪录:12 轮"`。
2. 保留键 `_settings`(设置同步用,见 §1.3),游戏不得占用。
3. 写进度时机:局内关键节点(过关、死亡结算、退出菜单选"保存并回大厅")自动 PUT + 写 localStorage;不要求实时。
## 5. 手势一致性约定
### 5.1 全局默认映射
| 手势 | 全局语义 |
|---|---|
| 上滑 | 帮助 / 当前状态播报 |
| 下滑 | 菜单(含:重听帮助、播报当前设置、重新开始、保存并回大厅) |
| 单击 | 确认 / 继续 / 检查 |
| 双击 | 重听(重播当前语音/线索/乐句) |
| 左/右滑 | 选择 / 移动 |
### 5.2 模态菜单约定(R3 修订:采纳 R2 开发实现,round2-review 裁决②)
全球统一的模态菜单交互:**下滑呼出(模态)→ 单击循环切换选项(每切换播选项名)→ 双击确认执行 → 上滑=帮助/规则 → 下滑或"返回游戏"项关闭**。菜单打开时暂停计时类玩法(有计时器的游戏进入暂停态)。
- 标准选项集:重新开始 / 回大厅(存档游戏为"保存并回大厅")/ 返回游戏;打开时先播 `menu_open` + 第一项。
- 为什么单击=切换而非确认:结算页"单击=再来一局"已被占用,菜单内单击若作确认会与重开冲突;单击循环 + 双击确认把"浏览"与"执行"分离,误操作成本为零(escape/simon R2 已一致落地)。
- **RPG《剑与声》豁免**:探索态菜单保持"左右滑切换 + 单击确认"——RPG 探索本身就是"左右滑选项 + 单击确认"范式,菜单与探索同构反而少学一套;战斗态不设菜单(下滑=逃跑)。
- 选曲/存档确认等**非模态**选项界面(如 rhythm 选曲)不属于菜单,仍用"左右滑切换 + 单击确认"。
### 5.3 豁免表(以滑动为核心玩法的游戏)
四向滑动被玩法占用的游戏,"上滑=帮助 / 下滑=菜单"让位,偏离必须在开场交互教学中明确讲出:
| 游戏 | 滑动用途 | 帮助入口 | 菜单入口 |
|---|---|---|---|
| escape 暗室 | 四向滑=移动 | PLAY 中双击无目标时兜底播报目标与提示;卡关计时器自动三级提示 | 仅结算后下滑=模态菜单(PLAY 中无菜单,R3 打磨拟加双击二级入口) |
| simon 音序回响 | 六手势全是答题输入 | 非输入阶段上滑=重听规则 | 结算后下滑=模态菜单 |
| rhythm 节拍行者 | 四向滑=演奏输入 | 乐句间隙上滑=重听规则 | 结算后下滑=模态菜单 |
| space 盲航 | 四向滑=闪避 | 坠毁结算后上滑=重听教学要点 | 结算后下滑=模态菜单 |
| rpg 剑与声 | 左右滑=选择(符合默认) | 上滑=角色状态(符合默认) | 下滑=菜单(豁免:左右滑+单击,见 §5.2) |
| storyline 声命线 | 左右滑=抉择(符合默认) | 上滑=帮助+进度(符合默认) | 下滑=菜单(R3 打磨统一到 §5.2 模态约定) |
### 5.4 通则
- 任何游戏里,**做了一个手势 3 秒内必须有反馈**(引擎内置震动 + 内容音/语音),被判定无效的手势给 `error` 震动;教学阶段除外(教学里错误手势给纠正语音)。
- 单击有 ~300ms 引擎级延迟:即时判定(格挡、节奏打击)一律绑滑动或双击,单击只用于非即时操作(确认、检查、菜单)。新设计继续遵守。
- 结算画面统一:单击=再来一局,下滑=菜单(回大厅)。教学可跳过:任何游戏的引导阶段,下滑=跳过引导(首次强制完整引导的标志位存进度里)。
+105
View File
@@ -0,0 +1,105 @@
# 01 · 《暗室》重设计(escape redesign)
> **给开发者的实现要点摘要**
> 1. 保留 3x3 网格与谜题链骨架(钥匙→铁柜→保险丝→电箱→大门),重写开场:前 ~2 分钟为**引导式脚本**(带玩家走到储物间、教会移动/检查/双击使用),之后进入自由探索。
> 2. 新增**卡关检测 + 三级提示**:以上一次"进展事件"起算,60s 无进展给暗示、120s 给明示、180s 给带路级提示;提示由计时器自动播出,不依赖玩家发现隐藏手势。
> 3. 环境线索音**可重听**:单击检查总是先重播当前房间环境音再播描述;9 个房间环境音去重(关键房间专属线索音)。
> 4. 操作反馈全部改震动(确认/撞墙/拾取/错误),iOS 静默降级;删除数字拼接播报,用时改为分段整句。
> 5. 手势映射(偏离全局默认,教学中讲清):四向滑=移动,单击=检查/重听,双击=使用物品/背包与目标,引导阶段下滑=跳过引导。
---
## 1. 现版诊断:"第一个房间都出不去"断在哪
读 `web/escape/game.js` 现版流程,新手断点有四:
1. **教学是两段可跳过的长旁白,零交互**(`voice/intro` → `voice/tutorial`,game.js:56-87)。玩家听完(或单击跳过)后被直接丢进 `PLAY`,没有任何一次"做对了一件事"的体验确认。移动、检查、使用物品三个核心动作都没有被验证过玩家真的会了。
2. **进入自由探索后没有任何方向感支撑**。起点 (1,1) 播一句 `voice/start_room`,玩家不知道房间有 9 个、不知道自己在哪、不知道目标在西南角。环境音只有 3 种(hum/wind/drip)铺给 9 个房间(`_playAmbient`, game.js:134-145),相邻房间声音雷同,靠环境音导航不成立。
3. **环境线索音只播一次**。进房间播一遍就没了,没听清就永远错过;`voice/wall` 撞墙提示也只播一次(`wallVoicePlayed`)。
4. **提示系统存在但不可达**。`hint_*` 四级提示语音已生成,但唯一入口是"双击且无可用目标"的兜底(game.js:195-198)——玩家根本不知道双击这个手势存在,更不知道要在"错误的地方"双击才能听到提示。卡关玩家恰恰是不会操作的人,提示入口藏在操作里,死循环。
## 2. 新开场:引导式脚本(GUIDE 阶段)
目标:前 2 分钟是"手把手",玩家每做一个动作都得到确认,引导结束时已天然完成谜题链第一环(拿到钥匙)。
### 2.1 流程
阶段 `GUIDE`(脚本化,步骤由玩家手势推进,不是定时旁白):
| 步 | 旁白(整句,功能) | 等待的玩家动作 | 反馈 |
|---|---|---|---|
| G0 | 剧情引入(2 句以内,比现版 intro 短):醒来、漆黑、要逃出去 | 单击继续 | CONFIRM 震动 |
| G1 | "先活动一下。试着朝任意方向滑一下,走一小步。" | 任意滑动 | 脚步声 + SWIPE_OK 震动 + "很好,你迈出了第一步。" |
| G2 | "这个房间不大,边界是墙。再滑两下,试着碰碰墙。" | 滑动 2 次(其中撞墙也计数) | 撞墙:wall 闷响 + ERROR 震动 + "那边是墙,换个方向。"(每次撞墙都播,取消 wallVoicePlayed 一次性限制);未撞墙:脚步 + SWIPE_OK |
| G3 | "跟着我指的路走。向左滑。" | 左滑(做错给纠正语音重等) | 脚步 + "对。再向下滑。" |
| G4 | "再向下滑。" | 下滑 | 脚步 + 到达储物间,播储物间专属环境音 |
| G5 | "听到水声和铁锈味了吗?单击一下,仔细检查这个房间。" | 单击 | pickup 音 + PICKUP 震动 + "你摸到了一把钥匙!"(谜题链第一环完成) |
| G6 | "钥匙应该能开什么。双击是把物品用出去的用法。现在先记住:双击。" | 双击 | 兜底播报背包 + CONFIRM 震动 + "对,就是这样。接下来靠你自己了。" |
| G7 | 目标陈述:"找到能用这把钥匙打开的东西。需要帮助时,在原地双击。" | — | 进入 PLAY |
- 引导期间任何步骤可**下滑跳过**(播"跳过引导"确认后直达 PLAY);跳过标志存 localStorage/进度,第二次进游戏开场直接问"上滑重听引导,单击直接开始"。
- 引导期间做错手势:不惩罚,播一句短纠正("不是这个方向,向左滑")继续等待;同一个步骤错 3 次给 `WARNING` 震动 + 更直白的重述。
- 引导脚本写死路线 (1,1)→(0,1)? 不,(1,1)→左=(0,1),再下滑=(0,2) 储物间。路线合法(现版图 (0,1) 是空房·风)。引导期间超出脚本路线的滑动被温和拦回("先跟我走")。
### 2.2 教学覆盖核对
引导结束时玩家已亲手做过:滑动移动(含撞墙)、单击检查、拾取、双击使用/查看。全部六个手势里只剩"上滑"未用——escape 不用上滑(豁免,见 00 文档 §5.2),无需教。
## 3. 卡关检测与三级提示
### 3.1 进展事件
以下任一发生即重置卡关计时器 `lastProgressAt`:拾取钥匙、打开铁柜、拾取保险丝、修好电箱、首次到达某关键房间(铁柜房/电箱房/大门,各记一次)、成功逃脱。普通移动/检查空房不算进展。
### 3.2 三级提示(计时器自动触发,无需手势)
| 无进展时长 | 级别 | 内容(整句预生成,每级每阶段一句) | 附带 |
|---|---|---|---|
| 60s | L1 暗示 | 面向当前目标的一句意象提示,如"铁锈味最重的地方,值得仔细看看" | WARNING 震动 + 语音提示前播"需要帮助吗?" |
| 120s | L2 明示 | 直接点明目标房间与动作,如"去西南角的储物间,单击检查,找钥匙" | WARNING 震动 |
| 180s | L3 带路 | 以玩家当前坐标算路,逐步导航:"从这里,向左滑,再向下滑"(每次触发按当前位置重算,滑动方向词为预生成的整句组合:方向指令句 4 句 × 顺序连读,非数字拼接,允许两句链播) | WARNING 震动,此后每 60s 重复 L3 直至有进展 |
- 提示级别按当前谜题链阶段(`_hintVoice()` 的 4 阶段:找钥匙/开铁柜/修电箱/开大门)× 3 级 = 12 句新增语音,全部是**整句**。
- 提示播完不重复骚扰:同 level 对同阶段只自动播一次(L3 除外,可周期重复)。
- 主动求提示保留:双击无目标时的兜底语音升级——先播背包,再播 L2 级明示(原来是 L1 级 hint_*)。
## 4. 环境线索音可重听 + 房间声音去重
1. **单击检查 = 重播环境音 + 描述**:`_inspect()` 改为先 `play(当前房间环境音)` 再链播描述语音(现版只播描述)。玩家随时可重听线索。
2. **环境音从 3 种扩到 6 种**,关键房间各有专属签名(Stable Audio 3 生成或合成,双格式):
- 储物间 (0,2):滴水 + 金属杂物轻响(`amb_storage`)
- 铁柜房 (2,1):嗡鸣 + 铁皮共振(`amb_cabinet`)
- 电箱房 (0,0):电流嗡鸣 + 偶尔火花(`amb_power`)
- 大门 (2,2):风声 + 门外空旷感(`amb_gate`)
- 起点 (1,1):单一滴水(`amb_drip`,保留)
- 其余 4 间空房:风声/远处嗡鸣两种弱化版轮换(`amb_wind`/`amb_hum` 保留)
3. 进房间自动播环境音(保留现行为),且**每次进入都播**——不再是"错过就没了",配合单击重听,线索可随时复核。
4. 大门房间通电后环境音切换为带电流声的版本(`amb_gate_live`),听觉上确认"电通了"这条谜题状态。
## 5. 反馈改震动(对齐 00 文档 §2)
| 事件 | 原反馈 | 新反馈 |
|---|---|---|
| 移动成功 | 脚步声 | 脚步声 + SWIPE_OK |
| 撞墙 | wall 闷响 | wall 闷响 + ERROR |
| 拾取/开柜/修电箱 | sfx + 语音 | sfx + 语音 + PICKUP / SUCCESS |
| 双击无目标(无效使用) | ui_deny 提示音 | **删除 ui_deny**,改 ERROR 震动(内容语音保留) |
| 逃脱成功 | 音效+语音 | 音效+语音 + SUCCESS |
| 提示出现 | 无 | WARNING |
`sfx/ui_deny` 这类"反馈提示音"全部退役;内容音(脚步、撞墙、机关)保留。
## 6. 语音重做清单(整句化 + 双音色)
- **删除**:`n0`-`n9`/`nshi`/`unit_min`/`unit_sec`/`time_prefix` 数字拼接链。用时播报改 4 段整句:"不到两分钟""五分钟内""十分钟内""超过十分钟"(`time_band1..4`),前缀"你逃出来了,用时"并入各段整句一次生成。
- **新增**:引导脚本 8 步全部语音(每步 1-2 短句)、12 句三级提示、跳过引导/二周目开场句、储物间等 4 个关键房间的新描述。
- **保留可用**:`empty_*`、`inspect_*`、`found_key`、`unlock_cabinet`、`fix_box`、`escape`、`replay`、`inv_*` 已是整句,重录一版 male 目录即可(文本不变,speaker=Uncle_Fu,instruct 与 female 版同款)。
- 全部语音按 00 文档 §3 生成:同 speaker+instruct 常量、female/male 双目录、ogg+m4a 双格式。
## 7. 流程图(新)
```
start → GUIDE(可下滑跳过)→ PLAY(自由探索 + 三级提示计时器)→ ENDING → 单击重开 / 下滑菜单
```
PLAY 阶段手势:四向滑=移动 / 单击=重播环境音+检查 / 双击=使用物品(无目标→背包+L2 明示)。ENDING:单击=重开,下滑=菜单(回大厅)。
+123
View File
@@ -0,0 +1,123 @@
# 02 · 《音序回响》重设计(simon redesign)
> **给开发者的实现要点摘要**
> 1. **删除 BGM**:`bgm/theme` 不再播放,游戏只有交互音色 + 语音两条线(响应反馈⑥)。
> 2. **旁白全部整句化**:删除 `di/lun/num_*/score_pre/best_pre` 拼接体系,轮次与成绩改预生成整句(轮数 1-20 逐句 + 无数字兜底;成绩分段整句)。
> 3. **交互节奏**:播放序列时每个音前给 `BEAT` 轻震提供节拍感;复现阶段每个手势给 `CONFIRM` 震动确认;对/错用 SUCCESS/ERROR 震动,`sfx/correct`、`sfx/wrong` 提示音删除。
> 4. **教学改交互式**:每教一个音色就让玩家亲手做一次该手势,做对才教下一个。
> 5. **人性化与复活**:每局初始 1 次"重听机会"(输入阶段双击=重听当前序列,消耗机会;答错时若有机会则消耗一次机会重听而非直接结算),每过 4 轮 +1 次(上限 2);速度爬升放缓;前 5 轮避免同手势三连。
---
## 1. 现版诊断
读 `web/simon/game.js`:
1. **BGM 是干扰项**(`bgm/theme` 音量 0.4 常驻,game.js:77)。记忆类游戏的工作记忆负载全在听觉通道,BGM 与六个手势音色同通道竞争,是纯负面。反馈⑥正确,整层去掉。
2. **数字拼接是音色漂移重灾区**:报轮次 `di + num_5 + lun`、报成绩 `score_pre + num_* + lun`(game.js:47-57, 142, 219-222)。这正是反馈⑥"音色漂移发音不准"的来源。
3. **教学是 8 句连续旁白**(`_runTutorial`, game.js:92-117),玩家只能听不能动,听完直接开考。
4. **错一音即死**(`_onInput` → `_gameOver`),无缓冲;轮次越高挫败越强。
5. **反馈音与内容音同质**:`sfx/correct`/`sfx/wrong` 与手势音色同为合成短音,盲听时"我按对了还是错了"要靠音色分辨,混淆。
## 2. 音频层重构
- BGM 层:不播放、不加载。开场静默 0.5s 后直接进教学语音。间隙的"空白感"由环境底噪填充?——**不填**。纯静更利于记忆,这是设计选择不是缺失。
- 手势音色(`tone_*` 6 个):保留现有映射(上=上行琶音 / 下=下行低音 / 左=左声道弹拨 / 右=右声道弹拨 / 单击=底鼓 / 双击=双连响),这是游戏内容,不动。
- 反馈提示音(`sfx/correct`、`sfx/wrong`、`sfx/newrecord`):**删除**,改震动(见 §4)。`sfx/gameover` 保留为内容性结束音(低沉闷响,标识一局终结,不与任何手势音色相似)。
## 3. 旁白整句化清单
### 3.1 删除的拼接件
`voice/di`、`voice/lun`、`voice/num_shi`、`voice/num_0..9`、`voice/score_pre`、`voice/best_pre` 全部退役(资产文件可留,代码不再引用)。
### 3.2 需要重录的句子类型(整句,female/male 双目录)
| 类型 | 句数 | 说明 |
|---|---|---|
| 轮次播报 | 21 | "第一轮,听好了。"…"第二十轮,听好了。"逐轮整句;第 21 句为无数字兜底:"新的一轮,听好了。"(round>20 时用,轮次仍可继续) |
| 成绩播报 | 6 段 | 按完成轮数分段整句:"一轮都没坚持下来,别灰心。"/"你坚持了三轮以内。"/"你坚持了五轮左右。"/"你坚持了将近十轮。"/"你坚持了十轮以上,很厉害。"/"你坚持了十五轮以上,大师级。"(代码按 score 映射分段) |
| 新纪录 | 1 | "新纪录!"(已是整句,保留文本重录) |
| 教学句 | 8 | 见 §5,每句 ≤20 字 |
| 流程句 | 6 | "轮到你了。"/"重听机会用完了,小心。"/"用一次机会,再听一遍。"/"单击屏幕,再来一局。"/"上滑重听规则,下滑打开菜单。"/规则重听句 |
| 反馈短句 | 3 | "答对了!"/"错了。"/"机会加一。"(均已有文本,整句重录) |
生成规范按 00 文档 §3:同一 SPEAKER 常量(female=Serena / male=Uncle_Fu)+ 同一 INSTRUCT 常量(建议"平静清晰的女声/男声,语速平稳"——不写进 instruct 的语速词,instruct 只控情绪:"平静、清晰、像游戏主持人")。
## 4. 交互节奏与震动设计
### 4.1 播放序列阶段(playback)
- 每个手势音播放的**同时**给 `BEAT`(8ms 轻震):节拍锚点帮助玩家分段记忆(把 N 个音切成节拍组),这是 simon 类游戏经典的"chunking"辅助。
- 播放间隔维持"逐个播放",节奏均匀(见 §6 速度曲线),最后一个音播完 400ms 后给一次 `CONFIRM` 震动,表示"轮到你了"(第 1 轮同时播语音,之后轮次只用震动,省时间)。
- 播放阶段手势一律忽略(保留现行为),但给 `ERROR` 震动一次告知"现在不用操作"?——**不给**,播放期误触静默忽略,避免打断聆听(00 文档 §2.4:纯聆听阶段不震动,BEAT 除外因其本身是节拍信息)。
### 4.2 复现阶段(input)
| 事件 | 震动 | 音频 |
|---|---|---|
| 玩家做出手势 | CONFIRM | 立即回声放对应手势音色(保留现行为) |
| 手势正确(序列推进) | (并入上面的 CONFIRM,不额外震) | — |
| 整轮复现成功 | SUCCESS | "答对了!"(每轮都播太密——改为每 4 轮播一次,其余轮次仅震动) |
| 答错且有机会 | ERROR + WARNING(间隔 300ms 先后) | "错了。用一次机会,再听一遍。" → 自动重播序列 |
| 答错且无机会 | ERROR + HIT | `sfx/gameover` + 成绩播报 |
| 双击(消耗机会主动重听) | PICKUP | "再听一遍。"+ 重播序列(inputIdx 重置为 0) |
| 机会 +1(每过 4 轮) | PICKUP | "机会加一。" |
## 5. 交互式教学(tutorial)
现版 8 句连播改为"教一个、练一个":
| 步 | 语音(整句) | 机制 |
|---|---|---|
| T0 | "欢迎来到音序回响。我会演奏一串声音,请你记住顺序,原样复现。先认识六种声音。" | 播完进 T1 |
| T1-T6 | 每个手势一句:"向上滑,是这个声音。"→ 播 tone → "你试试。" | **等待玩家做出该手势**;做对:tone 回响 + CONFIRM 震动 + "对了。"进下一个;做错:ERROR 震动 + "不是这个,向上滑试试。"重等;同一手势错 3 次给更直白描述("手指贴着屏幕,向上推一下") |
| T7 | "六种声音都记住了。序列会越来越长,答错可以消耗机会重听。双击随时重听当前序列。" | 进 T8 |
| T8 | "第一轮,听好了。" | 进正式对局 |
教学时长约 60-80s,全程玩家动手 6 次以上。教学期间下滑=跳过教学(播确认句后直达第一轮)。
## 6. 难度曲线与复活机制
### 6.1 速度曲线(放缓)
现版 `_interval()`: 720ms 起、每轮 -36ms、下限 340ms(game.js:152)——第 11 轮即触底。新版:
```
interval(round) = max(420, 700 - (round - 1) * 25) // 第 12 轮才到 420ms 下限
```
### 6.2 序列生成约束
- 长度 = round + 1(保留,从 2 开始)。
- **前 5 轮**:禁止同一手势连续出现 3 次;第 1 轮两个音不同手势。
- 全部轮次:禁止连续 4 个同手势(防恶意随机)。
- 复活重听的序列**不变**(重听就是重听同一序列,不重新随机)。
### 6.3 机会(retry)规则
- 每局初始 `chances = 1`;每完成 4 轮 `chances = min(2, chances + 1)` 并播报。
- 消耗途径二选一:a) 输入阶段**双击**主动重听当前序列(进度清零重输);b) 答错时自动消耗一次机会,重播序列后再试。
- **双击语义冲突的裁定(R3 修订,round2-review 裁决①)**:输入阶段双击按上下文判定——当前期望输入恰为 doubletap 时双击=**答题**(序列必须永远可答,答题优先),否则双击=**主动重听**(耗机会)。机会为 0 时双击非期望手势:不判答错、不消耗,仅播 `chances_out` 提示。重听播报期间切到非输入阶段,避免手势被吞。
- 无机会时答错 → gameover:成绩分段整句播报 + 新纪录判定 + "单击屏幕,再来一局"。
- `best`(历史最高)从会话内存改为写进度存储(localStorage + 服务器同步,进度 data 带 `summary`,格式"最佳成绩:第 N 轮")。
## 7. 状态机与手势汇总
阶段:`tutorial → announce → playback → input → (replay) → roundend → … → gameover`
| 阶段 | 上滑 | 下滑 | 双击 | 单击 | 四向滑/点按 |
|---|---|---|---|---|---|
| tutorial | — | 跳过教学 | — | — | 教学输入 |
| playback | — | — | — | — | 忽略 |
| input | — | — | 重听(耗机会) | 答题输入 | 答题输入 |
| gameover | 重听规则 | 菜单(重开/回大厅) | — | 重开 | — |
非 input 阶段上滑=重听规则(豁免见 00 文档 §5.2:六手势全是输入,帮助让位到非输入阶段)。
## 8. 资产清单变动
- 删:bgm/theme 引用(文件可保留不播)、sfx/correct、sfx/wrong、拼接数字件。
- 新录:§3.2 全部(约 45 句 × 2 音色 = 90 条 × 2 格式)。
- 保留:6 个 tone、sfx/gameover。
- 更新 `web/simon/README.md` 玩法描述与 `gen/gen_voice.py` 的 `LINES` 表(文本表驱动,female/male 两跑)。
+185
View File
@@ -0,0 +1,185 @@
# 03 · 《节拍行者》重构施工稿(rhythm rebuild,R3 施工依据)
> **给开发者的实现要点摘要**
> 1. 玩法 = **跟随领奏(call & response)**:系统奏 1 小节节奏型(2-4 个手势乐器音),玩家在下一小节的相同拍点用相同手势跟打。3 首曲目(90/105/120 BPM)解锁制。完整谱面见 §3(每首前 8 句全部写出,后续句按 §3.5 规则生成)。
> 2. 判定沿用现版常量:PERFECT ±0.12s / GOOD ±0.25s / TAP_DELAY 0.30s 补偿 / LOOKAHEAD 0.6s / 调度泵 40ms;tap 只允许出现在第 1、3 拍(强拍)。
> 3. 判定反馈全改震动(`engine/haptics.js`):出手震动引擎 gesture.js 已内置(swipe/confirm),游戏层只加 Miss=`error`、16 连击=`success`、乐句全 Perfect=自定义 [20,40,20];领奏音符锚点自定义 BEAT=[8]。`judge_*` 提示音删除。
> 4. 语音台词全表在 **[03-rhythm-voice-ids.md](03-rhythm-voice-ids.md)**(音频总监照录):旁白 female=Vivian / male=Uncle_Fu(R1 音色分配表),准确率分档整句播报。
> 5. 进度 data:`{ summary, unlocked, grades, bestCombo }`(§7),大厅卡片显示 summary。
---
## 1. 现版问题与重构方向(结论,不再展开)
现版"听令行动"(指令音→下一拍做手势)认知负荷四层(认音色→忆映射→换算拍点→精确出手),且 judge 提示音制造"考试感"。重构为跟随领奏:记忆负荷压到 ≤4 音短句,负荷转移到节奏精度;玩家动作触发与 BGM 同调性的**乐器音**,做对时听到自己在演奏。现版的 AudioContext 预排程泵(`_pump`/`_scheduleSfx`/tap 补偿)工程质量好,**保留复用**,只重写谱面与判定逻辑。
## 2. 常量与判定(施工契约,直接抄)
```js
const PERFECT = 0.12; // Perfect 窗口 ±120ms
const GOOD = 0.25; // Good 窗口 ±250ms(蓝牙延迟由它吸收)
const TAP_DELAY = 0.30; // 引擎 tap 回调延迟补偿(沿用现版)
const LOOKAHEAD = 0.6; // 调度前瞻(秒)
const PUMP_MS = 40; // 调度泵周期
```
- **tap 补偿**:判定时 `if (g === 'tap') t -= TAP_DELAY`;tap 音符的 Miss 超时检查放宽 `TAP_DELAY + 0.08`,其他手势放宽 `0.05`(现版 game.js:159-164 原样保留)。
- **tap 位置约束**:谱面中 tap 只允许落在第 1、3 拍(强拍),给补偿留足余量;doubletap 不落在 x.5 反拍(曲目三之前)。
- 判定结果:
- 手势对 + |dt|≤0.12 → Perfect:玩家手势乐器音 + accent 亮音层叠加。
- 手势对 + |dt|≤0.25 → Good:只播乐器音。
- |dt|>0.25 或手势错 → Miss:**不播乐器音**(错过=没出声,本身就是反馈)+ `vibrate('error')`。
- 手势对但窗口内找错音符归属:按现版"最近未判定音符"归属逻辑(game.js:278-293)保留。
- 计分:Perfect=2 / Good=1 / Miss=0;`rate = 得分 / (2 × 总音符数)`;评级 S≥0.95 / A≥0.85 / B≥0.70 / C<0.70。不掉血、不中断,歌照唱完。
- 连击:每 16 连击 `vibrate('success')`(不打断演奏,不播报);乐句全部 Perfect 时乐句末 `vibrate([20,40,20])`。
## 3. 谱面
### 3.1 记号与结构
- 拍点记号:`拍号:手势`,1 小节 = 4 拍(1, 2, 3, 4),`x.5` = 八分音符反拍。
- 手势:`tap`(T) `doubletap`(D) `left`(L) `right`(R) `up`(U) `down`(N)。
- 每个乐句 = **领奏 1 小节 + 应答 1 小节**(应答拍点 = 领奏拍点 + 4 拍)。
- 曲目流程:`数拍 1 小节(BGM 内鼓点,无独立节拍器)→ 逐乐句 → 结算`。
- 乐句间休止:曲目一前 4 句应答后各休 1 小节;曲目二前 2 句各休 1 小节;曲目三不休。
### 3.2 曲目一《启程》90 BPM(SPB=0.667s,10 句,≈70s)
定位:教学曲。全强拍,2→4 音,无反拍。
| 句 | 领奏节奏型 | 音符数 |
|---|---|---|
| 1 | `1:tap, 3:tap` | 2 |
| 2 | `1:left, 3:right` | 2 |
| 3 | `1:tap, 2:doubletap, 3:tap` | 3 |
| 4 | `1:left, 2:doubletap, 3:right` | 3 |
| 5 | `1:tap, 3:left, 4:doubletap` | 3 |
| 6 | `1:right, 3:tap, 4:doubletap` | 3 |
| 7 | `1:up, 3:down, 4:doubletap` | 3 |
| 8 | `1:tap, 2:up, 3:right, 4:doubletap` | 4 |
| 9 | `1:left, 2:right, 3:tap, 4:doubletap` | 4 |
| 10 | `1:tap, 2:doubletap, 3:tap, 4:doubletap` | 4(终句,全鼓组收束) |
### 3.3 曲目二《追风》105 BPM(SPB=0.571s,12 句,≈75s)
定位:引入反拍(第 5 句起)与 4 音密集句。
| 句 | 领奏节奏型 | 音符数 |
|---|---|---|
| 1 | `1:tap, 3:right` | 2 |
| 2 | `1:left, 2:doubletap, 3:tap` | 3 |
| 3 | `1:up, 3:down, 4:doubletap` | 3 |
| 4 | `1:tap, 2:right, 3:left, 4:doubletap` | 4 |
| 5 | `1:tap, 2.5:right, 3:tap` | 3(首个反拍 2.5) |
| 6 | `1:left, 2:up, 3.5:right, 4:doubletap` | 4 |
| 7 | `1:tap, 2:doubletap, 3:down, 3.5:up` | 4 |
| 8 | `1:right, 1.5:left, 3:tap, 4:doubletap` | 4 |
| 9 | `1:tap, 2.5:left, 3:tap, 4:doubletap` | 4 |
| 10 | `1:up, 2:right, 3:down, 3.5:left` | 4 |
| 11 | `1:tap, 2:doubletap, 2.5:right, 3:tap` | 4 |
| 12 | `1:left, 1.5:right, 2:doubletap, 3:tap, 4:doubletap` | 5(终句) |
### 3.4 曲目三《星爆》120 BPM(SPB=0.5s,12 句,≈70s)
定位:4-6 音密集句、切分、doubletap 上反拍。不设十六分音符(盲玩极限)。
| 句 | 领奏节奏型 | 音符数 |
|---|---|---|
| 1 | `1:tap, 2:doubletap, 3:tap, 4:doubletap` | 4 |
| 2 | `1:left, 2:right, 3:left, 4:doubletap` | 4 |
| 3 | `1:up, 2:down, 3:up, 4:doubletap` | 4 |
| 4 | `1:tap, 2.5:left, 3:tap, 4:right` | 4 |
| 5 | `1:left, 1.5:right, 2.5:left, 3:tap` | 4 |
| 6 | `1:tap, 2:up, 2.5:down, 3:tap, 4:doubletap` | 5 |
| 7 | `1:right, 2:doubletap, 3:left, 3.5:right, 4:up` | 5 |
| 8 | `1:tap, 1.5:doubletap, 2.5:left, 3:tap, 3.5:right, 4:doubletap` | 6(高潮句) |
| 9 | `1:left, 2:doubletap, 2.5:up, 3:right, 4:doubletap` | 5 |
| 10 | `1:tap, 2.5:down, 3:tap, 3.5:up, 4:doubletap` | 5 |
| 11 | `1:right, 1.5:left, 2:right, 3:tap, 4:doubletap` | 5 |
| 12 | `1:tap, 2:doubletap, 2.5:left, 3:tap, 3.5:right, 4:doubletap` | 6(终句) |
### 3.5 难度曲线规则(谱面设计约束,续作/改谱遵守)
1. 乐句长度:曲目一 2→4 音,曲目二 2→5 音,曲目三 4→6 音;同曲内单调不减(终句允许 +1 爆发)。
2. 反拍(x.5)引入:曲目一禁用;曲目二第 5 句起、每句至多 1 个反拍音;曲目三不限。
3. tap 只落 1/3 拍(全曲目铁律);doubletap 曲目一/二只落整拍,曲目三可落反拍。
4. 同一手势连续出现 ≤2 次(跨音符);同一乐句内手势种类 ≥2(第 1、10 句的全鼓句除外)。
5. up/down 作为"音高对"尽量成对出现在同一乐句(↑↓ 或 ↓↑),利于听觉记忆。
6. 左右声道手势不放在相邻反拍连续位(如 `1:left, 1.5:right` 只允许曲目三)——声像跳变过快在耳机上易混。
## 4. 手势 → 乐器音映射(sfx 资产)
| 手势 | 乐器音 | sfx id | 声像/音高 |
|---|---|---|---|
| tap | 底鼓 kick | `sfx/inst_kick` | 中央,低频 |
| doubletap | 军鼓 snare | `sfx/inst_snare` | 中央偏上 |
| left | 闭合镲 closed hat | `sfx/inst_hat_l` | 极左 |
| right | 开放镲 open hat | `sfx/inst_hat_r` | 极右 |
| up | 高通 tom | `sfx/inst_tom_hi` | 中央,升调感 |
| down | 低通 tom | `sfx/inst_tom_lo` | 中央,降调感 |
| (叠加层) | Perfect accent 亮音 | `sfx/accent` | 共用 1 个,叠在乐器音上 |
全部与 BGM 同调性(曲目一/二/三 BGM 根音需在 Mureka prompt 里统一,建议 A minor)。乐器音时长 ≤0.4s,起音要硬(盲玩靠起音对拍)。生成方式:Stable Audio 3(打击乐短采样质量足够)或程序合成兜底;accent 可用程序合成(高频短琶音)。
## 5. 流程与教学(30 秒上手)
### 5.1 状态机
```
idle → intro(语音) → tut(3 步教学) → select(选曲,二周目起) → countin → main(逐乐句) → end(结算)
↑_________________|
```
- `select`:左右滑切曲目(播曲目名 + 已解锁状态),单击进入,下滑回大厅。未解锁曲目播报"上一首拿到 B 级以上解锁"。
- 教学只在首次进游戏出现(`progress.data.tutorialDone`);之后 intro 缩减为一句"跟着音乐,我奏一句,你跟一句。"
### 5.2 教学 3 步(台词见 voice-ids 文档 T 组)
| 步 | 内容 | 机制 |
|---|---|---|
| T1 | 领奏 `1:tap`(第 1 拍单底鼓)→ 玩家在第 2 小节同位置跟打 | 做错:error 震动 + 纠正语音(早了/晚了/不是这个手势,按 dt 与手势选句)→ 重播领奏重试;2 次失败跳过进 T2 |
| T2 | 领奏 `1:left, 3:right` → 跟打 | 同上 |
| T3 | 领奏 `1:tap, 2:doubletap, 3:tap` → 跟打 | 通过后进曲目一 |
纠正判定复用正式判定窗口,只是不计分。教学期间 BGM 已在播(从 intro 第 6 秒起),数拍感来自 BGM 本身。
## 6. 震动与语音在流程中的落点
| 时机 | 震动 | 语音 |
|---|---|---|
| 领奏每个音符 | [8](BEAT,自定义) | — |
| 玩家出手 | (引擎内置 swipe/confirm,游戏层不重复) | 乐器音 |
| Miss | error | — |
| 16 连击 | success | — |
| 乐句全 Perfect | [20,40,20] | — |
| 曲目过半 | — | `mid_song`("过半了,保持。") |
| 结算 | success(S/A)/ 无(B/C) | `grade_*` + `restart_hint` |
| 解锁新曲 | success + [20,40,20] | `unlock_song2/3` |
## 7. 进度存储
```json
{
"schema": 1,
"updatedAt": "2026-08-29T12:00:00Z",
"summary": "《追风》B级 · 已解锁2/3首",
"tutorialDone": true,
"unlocked": 2,
"grades": { "song1": "a", "song2": "b" },
"bestCombo": 42
}
```
- 评级提升(如 b→a)、解锁变化、bestCombo 刷新时在结算后 PUT;`summary` 格式:`《曲名》X级 · 已解锁N/3首`(取最高评级的曲目)。
- 解锁规则:曲目 n 拿到 B 级以上 → 解锁 n+1。
## 8. 实现要点(给开发 agent)
1. 保留现版:`AudioContext.currentTime` 预排程、40ms 泵、LOOKAHEAD、`TAP_DELAY` 补偿与 Miss 放宽(现 game.js:26-30, 149-176, 267-293)、BGM 手动 `src.start(t0)` 对网格(game.js:221-239)。
2. 重写:谱面数据从 40 指令流改为 `phrases: [{ notes: [{ beat, gesture }] }]`(本文 §3 表格直接转 JSON);调度时领奏音符排第 k 小节、应答 `_cues` 期望时刻 = 领奏时刻 + 4 拍;间奏休止按曲目参数插入空小节。
3. 玩家乐器音不走预排程:出手即播(sfx 即发即弃),Perfect 时叠播 `sfx/accent`。
4. BGM:3 首重生成(Mureka,90/105/120 BPM 精确、鼓组清晰、A minor、60-90s 无缝循环,沿用现版 groove 归一化工艺);现版 `bgm/groove` 弃用。数拍融入 BGM,删独立 tick 节拍器。
5. 震动接 `engine/haptics.js`;BEAT 用自定义数组 `vibrate([8])`,领奏音符在调度时同步排一个 `setTimeout` 对齐(或用 pump 在音符时刻触发,误差 <40ms 可接受)。
6. 语音全表见 03-rhythm-voice-ids.md;现版 `voice/welcome/rules/tut_start/tut_done` 弃用,`grade_*`/`restart` id 沿用但文案变更(重录)。
7. 结算画面:单击=再来一遍(同曲),右滑=下一首(若已解锁),下滑=回大厅。
8. 更新 `web/rhythm/README.md`(玩法、手势表、进度结构)。
+71
View File
@@ -0,0 +1,71 @@
# 03a · 《节拍行者》语音台词表(voice ids,R3 录音依据)
> **给音频总监的录制说明**
> - 旁白双性别:**female = Vivian,male = Uncle_Fu**(R1 音色分配表,docs/audio/round1-report.md §4)。每句两个性别各录一条,分别落 `web/rhythm/audio/voice/female/<id>.{ogg,m4a}` 与 `.../male/`。
> - instruct 全表统一(同游戏同 instruct,00 文档 §3.2):
> - female(Vivian):`活力清晰的年轻女声,像节奏游戏主持人,情绪饱满但不夸张`
> - male(Uncle_Fu):`低沉醇厚的男声,像节奏游戏主持人,平稳有力`
> - 全表只用这两条 instruct 常量,逐句不换。特例单独标注。
> - 管线:`tools/gen_voice_dual.py` 台词表照录本文件;多音字已规避(无"重听/银行/长短"类风险词)。
> - 准确率按 S/A/B/C 四档直接写进评级句(设计定:档位即评级语义,不另设 90+/80+ 分档句,避免与评级割裂)。
> - id 沿用现版的:`grade_s/a/b/c`、`restart`(文案变更需重录覆盖);其余全部新增。现版 `welcome/rules/tut_start/tut_done` 弃用。
## W 组 · 开场与流程(8 句)
| id | 台词全文 | 备注 |
|---|---|---|
| `welcome` | 欢迎来到节拍行者。跟着音乐,我奏一句,你跟一句。 | 首次进游戏 |
| `welcome_back` | 欢迎回来。我奏一句,你跟一句。 | 二周目起 |
| `tut_1` | 听,底鼓。在下一小节的同一个位置,单击。 | 教学步 1 |
| `tut_2` | 这次是左和右。跟着打,左滑,右滑。 | 教学步 2 |
| `tut_3` | 最后一句,三个音,中间是双击。 | 教学步 3 |
| `tut_done` | 很好,演出开始。 | 教学通过,进曲目一 |
| `tut_skip` | 跳过教学,直接开始。 | 下滑跳过 |
| `mid_song` | 过半了,保持。 | 每首过半播一次 |
## C 组 · 教学纠正(5 句)
| id | 台词全文 | 触发 |
|---|---|---|
| `retry_early` | 早了,等鼓点再出手。 | 教学/练习中 dt < -GOOD |
| `retry_late` | 晚了,跟着鼓点走。 | dt > +GOOD |
| `retry_gesture` | 不是这个手势,再听一遍。 | 手势错 |
| `retry_listen` | 再听一遍,跟着来。 | 重播领奏前 |
| `retry_skip` | 没关系,我们先走下一句。 | 同一步失败 2 次跳过 |
## S 组 · 选曲与解锁(7 句)
| id | 台词全文 | 备注 |
|---|---|---|
| `select_hint` | 左右滑选曲目,单击开始,下滑回大厅。 | 进选曲界面 |
| `song1_name` | 第一首,启程,九十拍。 | 曲目一 |
| `song2_name` | 第二首,追风,一百零五拍。 | 曲目二 |
| `song3_name` | 第三首,星爆,一百二十拍。 | 曲目三 |
| `song_locked` | 这首还没解锁。上一首拿到 B 级以上就能解锁。 | 选中未解锁曲 |
| `unlock_song2` | 新曲目解锁,追风! | 曲目一 ≥B |
| `unlock_song3` | 新曲目解锁,星爆! | 曲目二 ≥B |
## G 组 · 结算(6 句)
| id | 台词全文 | 备注 |
|---|---|---|
| `grade_s` | S 级!准确率超过九成五,完美演出。 | 沿用 id,文案变更重录 |
| `grade_a` | A 级!准确率超过八成五,非常出色。 | 同上 |
| `grade_b` | B 级,准确率超过七成。不错,还能更好。 | 同上 |
| `grade_c` | C 级,准确率不到七成。别灰心,再来一遍。 | 同上 |
| `new_best_combo` | 新连击纪录! | bestCombo 刷新时插在评级句后 |
| `restart_hint` | 单击再来一遍,右滑下一首,下滑回大厅。 | 沿用 id `restart`,文案变更重录 |
## M 组 · 菜单与规则(5 句)
| id | 台词全文 | 备注 |
|---|---|---|
| `rules` | 规则:我奏一句,你在下一小节原样跟打。音要对,拍点也要对。 | 上滑重听规则 |
| `settings_hint` | 音色、语速、背景音乐,请回大厅修改。 | 菜单内播设置 |
| `menu_main` | ~~菜单。左右滑选择,单击确认。~~ → **勘误(R 终审)**:菜单约定已改(00 §5.2),以音频重录版"菜单。单击切换,双击确认。"为准。 | 下滑呼出 |
| `menu_resume` | 继续演奏。 | 菜单项 |
| `menu_lobby` | 回大厅。 | 菜单项 |
---
**合计 31 句 × 2 性别 = 62 条**(其中 grade_*、restart 为重录覆盖)。无数字拼接:拍数(九十/一百零五/一百二十)与准确率档位全部写死在整句里。
+112
View File
@@ -0,0 +1,112 @@
# 04 · 《盲航》重构方案(space rebuild)
> **给开发者的实现要点摘要**
> 1. 47 秒连续旁白教学(intro1-4 链播)改为**交互式演练**:每次只教一件事,玩家立刻实操一颗慢速陨石,做错不惩罚、当场重教。四个方向各练一次,单击/双击在开局后第 20s / 45s 分批引入。
> 2. **闪避方向改为同向**:听到左边的呼啸就向左滑——"把陨石拨开"。反向映射(现版左来右滑)是"没搞懂就死了"的最大认知负担,同向映射让方位声直接映射到同侧手势。
> 3. **前 60 秒保护期**:陨石慢(逼近 4s)、稀(间隔 4s)、撞了不掉血(训练护盾吸收,语音解释)。60s 后语音宣告"保护解除",难度曲线从现版起点再降一档重新爬坡。
> 4. **死亡即教学**:每次被撞,语音说清陨石从哪来、该怎么做;坠毁结算播报分因果。
> 5. 数字拼接全部改整句:里程碑 5 句固定整句,存活时间分 4 段整句。撞击/护盾等反馈改震动。
---
## 1. 现版诊断:"没搞懂就死了"
读 `web/space/game.js`:
1. **教学是 4 句连播旁白**(`voice/intro1..4`, game.js:84-87),约 47 秒。玩家零操作听完,然后第一颗陨石 1.2 秒后就来(`_scheduleSpawn(1200)`, game.js:111)。方位声→反向手势的映射只在旁白里说过一遍,没有任何一次实操验证。
2. **反向闪避违反直觉**(`ESCAPE_SWIPE = {left:'right',...}`, game.js:26)。声音从左边来,手却要往右滑——玩家在前几秒的生存窗口里要完成"定位→取反→出手"三步推理,取反这一步是死亡率主因。
3. **三个机制同时压上来**:闪避、单击加速、双击护盾,47 秒旁白里全部讲完,开局全部可用。玩家还没分清 whoosh 的方位,护盾冷却的播报已经来了。
4. **难度起点过高**:首颗逼近时长 2.8s、刷怪间隔 3.2s(game.js:42-44),对没建立映射的新手等于即死。
5. **撞击无教学价值**:`voice/hit1/hit2` 只报损,不说陨石从哪来、该怎么躲。玩家死了都不知道为什么。
## 2. 核心决策:同向闪避
**听到哪边来,就向哪边滑**(`PUSH_SWIPE = {left:'left', right:'right', up:'up', down:'down'}`),叙事包装为"挥手把陨石拨开/推引擎避开"。
- 方位声(左声道)→ 同侧手势(左滑):零推理,反射直达,这是盲玩能成立的前提。
- 上=高频呼啸→上滑,下=低频轰隆→下滑,保留现有 sfx 方位设计(whoosh_* 四个资产全部复用,只改映射表和文案)。
- 判定窗口沿用现版结构(撞击前 1.5s 内滑动有效 + 250ms 宽限),仅 `need` 从 `ESCAPE_SWIPE[dir]` 改为 `dir` 本身。
- 单击加速、双击护盾机制保留(现版"持续增益规避 tap 延迟"的设计是对的,game.js:146-153 注释写得很清楚),只是引入时机后移(§3.3)。
## 3. 新开场:交互式演练(DRILL 阶段)
### 3.1 演练流程
每步 = 一句短旁白 → 一颗**演练陨石**(逼近 4.5s,撞了不掉血)→ 玩家操作 → 反馈。做错/超时:ERROR 震动 + 一句纠正 + 同一颗重来,**最多重教 2 次后自动演示通过**(不卡流程)。
| 步 | 旁白(整句,≤20字/句) | 演练内容 |
|---|---|---|
| D0 | "欢迎登上盲航。你只有一双耳朵。" + "陨石来了,会有声音告诉你方位。" | 无陨石,建立预期 |
| D1 | "听,左边有陨石。向左滑,把它拨开!" | 左来,需左滑 |
| D2 | "这次在右边。" | 右来,需右滑 |
| D3 | "上面有东西,声音又尖又高。向上滑!" | 上来,需上滑 |
| D4 | "下面是低频的轰隆。向下滑!" | 下来,需下滑 |
| D5 | "四个方向你都会了。引擎点火,出发!" | 进保护期 |
- 每次成功拨开:dodge 音 + SWIPE_OK 震动 + 短肯定("漂亮。"仅 D1/D3 播,避免啰嗦)。
- 每步旁白在**演练陨石的 whoosh 响起之前**播完(语音先、音效后,不叠放),whoosh 响起时玩家已知该做什么。
- 教学期间下滑=跳过演练(二周目直接问"上滑重听演练,单击出发")。
### 3.2 前 60 秒保护期
演练结束进入 `playing` 后启动保护期(`protectionUntil = runStart + 60s`):
| 参数 | 保护期 | 保护期后 |
|---|---|---|
| 逼近时长 | 4.0s(恒定) | 3.2s → 2.0s 爬坡(比现版 2.8→1.8 各抬 0.4s) |
| 刷怪间隔 | 4.0s(恒定) | 3.2s → 1.4s 爬坡 |
| 撞击 | 不掉血:训练护盾吸收,播 shield_block 音 + WARNING 震动 + 语音解释("护盾接住了。那颗在左边,听到左声道的呼啸就向左滑。"按实际方位选句) | 正常掉血 |
| 单击/双击 | 第 20s / 45s 才解锁(见 §3.3) | 可用 |
第 60s 播:"训练护盾关闭,接下来靠你自己了。" + WARNING 震动。之后难度从低档开始 120s 爬满(沿用 RAMP_MS=120s)。
### 3.3 单击/双击分批引入
- **第 20 秒**(保护期内):暂停刷怪 6 秒,旁白:"试试单击,引擎会短促加速,直接冲过空隙。" → 玩家单击 → boost 音 + CONFIRM 震动 + "感觉到了吗?加速期间撞不上你。" → 恢复刷怪。
- **第 45 秒**:同样暂停 6 秒:"双击展开护盾,能挡一次撞击,用完要 30 秒充能。" → 玩家双击 → shield 音 + CONFIRM 震动 → 恢复。
- 若玩家 8 秒内没做,不等待直接继续(机制仍在,之后用上也正常生效)。
- 护盾充能完毕语音(`voice/shield_ready`)保留。
## 4. 死亡即教学 + 结算
### 4.1 撞击反馈(保护期后)
每次被撞:`sfx/hit` + HIT 震动 + 一句归因语音(按方位 4 句预生成:"左边撞上的。听到左声道呼啸,向左滑。"),替换现版 `voice/hit1/hit2` 的纯报损。第二次被撞额外加半句船体状态:"船体只剩最后一次了。"
### 4.2 坠毁结算(整句化)
删除 `numClips` 拼接体系(`voice/num*`/`n_time`/`n_sec`/`n_dodge`/`n_ge`, game.js:47-65):
- **存活时间** 4 段整句:"不到三十秒,再试一次。""超过三十秒,有进步。""超过一分钟,合格的飞行员。""超过两分钟,王牌飞行员。"
- **躲避数** 4 段整句(合并进上句的第二句):"只拨开几颗。""拨开了十来颗。""拨开了几十颗。""拨开了一大群,漂亮。"
- 每局结算 = 时间段整句 + 躲避段整句 + "单击再飞一次,下滑回大厅。"
- **里程碑播报**:`dodges % 10` 的数字拼接改为 5 句固定整句(10/20/30/40/50 颗各一句:"十颗了,稳。"…"五十颗,传奇。"),超出 50 不再播报。
### 4.3 最高分
存活时间最高纪录存进度(`data = { bestSec: n, totalRuns: n }`),结算时若破纪录加一句"新纪录!"(SUCCESS 震动)。
## 5. 震动映射(对齐 00 文档)
| 事件 | 震动 |
|---|---|
| 成功闪避 | SWIPE_OK(dodge 音保留) |
| 警报渐强(撞击前 1s / 0.5s 两段) | DANGER 渐强脉冲(随 alarm 两档,第二档更密) |
| 撞击 | HIT |
| 护盾挡下 | CONFIRM |
| 加速/护盾激活 | CONFIRM |
| 坠毁 | ERROR + HIT(间隔 300ms) |
| 演练做对 | SWIPE_OK;演练做错 ERROR |
现版 `sfx/alarm` 两档警报音**保留**(它是方位+距离的内容信息,不是反馈提示音),震动 DANGER 与其同步叠加。
## 6. 实现要点(给开发 agent)
1. `ESCAPE_SWIPE` 改同向映射(一行改动),`DODGE_WINDOW_MS`/`DODGE_GRACE_MS` 判定结构不动。
2. 新增 `phase='drill'`:复用 `_spawnObstacle` 但参数化(approachMs/spawnInterval/harmless 标志),演练陨石带 `harmless: true` 与指定 `dir`。
3. `_resolve()` 增加保护期分支:`now < protectionUntil` 时撞击走"训练护盾"路径(不掉血、播归因语音)。
4. 暂停刷怪:`_clearTimers()` 后重排 `_scheduleSpawn`(现版已有清晰的 timer 管理,game.js:250-261)。
5. 归因语音按方位选句:`voice/hit_left/hit_right/hit_up/hit_down` + `voice/hit_last`。
6. 语音全部整句重录:演练 8 句、保护期 3 句、单击/双击引入 4 句、归因 5 句、结算 10 句(时间段 4 + 躲避段 4 + 新纪录 + 重开)、里程碑 5 句 ≈ 35 句 × 2 音色。删除全部 num 拼接件。
7. 教学语音与 whoosh 的时序:语音 onComplete 后再 spawn 演练陨石,保证不叠放。
8. 更新 `web/space/README.md` 手势表(同向闪避是玩家可见的最大变化,README 必须同步)。
+105
View File
@@ -0,0 +1,105 @@
# 04a · 《盲航》重构版语音台词表(voice ids,R4 录音依据)
> **给音频总监的录制说明**
> - 设计依据:[04-space-rebuild.md](04-space-rebuild.md)。闪避为**同向**(左来左滑),台词已全部按此写死,不存在反向表述。
> - 旁白双性别:**female = Serena,male = Uncle_Fu**(R1 音色分配表)。落 `web/space/audio/voice/female|male/<id>.{ogg,m4a}`。
> - instruct 全表统一(同游戏同 instruct,00 文档 §3.2):
> - female(Serena):`平静清晰的女声,像飞船领航员,沉稳可信`
> - male(Uncle_Fu):`低沉醇厚的男声,像飞船领航员,沉稳有力`
> - 演练的紧迫感靠台词文案表达,instruct 不变。
> - 全部整句、无数字拼接:方位/次数/存活时间/躲避数全部按句写死或分档。
> - 多音字已规避("重新听"不写"重听";无"撞着/着陆"类风险)。
> - 管线:`tools/gen_voice_dual.py` 照录本表。**本表全量替换 R1 生成的 space 语音**(旧 id 见表末映射)。
## D 组 · 交互式演练(14 句)
| id | 台词全文 | 触发 |
|---|---|---|
| `drill_welcome` | 欢迎登上盲航。你只有一双耳朵。 | D0 |
| `drill_rule` | 陨石来了,会有声音告诉你方位。 | D0 |
| `drill_left` | 听,左边有陨石。向左滑,把它拨开! | D1 |
| `drill_right` | 这次在右边。向右滑! | D2 |
| `drill_up` | 上面有东西,声音又尖又高。向上滑! | D3 |
| `drill_down` | 下面是低沉的轰隆。向下滑! | D4 |
| `drill_good` | 漂亮。 | 演练做对(D1/D3 播) |
| `drill_retry_left` | 那是左边来的,向左滑。 | D1 做错重教 |
| `drill_retry_right` | 那是右边来的,向右滑。 | D2 做错重教 |
| `drill_retry_up` | 声音又高又尖,是上面,向上滑。 | D3 做错重教 |
| `drill_retry_down` | 轰隆声在下面,向下滑。 | D4 做错重教 |
| `drill_moveon` | 没关系,跟着我,我们继续。 | 同一步失败 2 次自动带过 |
| `drill_done` | 四个方向你都会了。引擎点火,出发! | D5 |
| `drill_skip` | 跳过演练,直接出发。 | 下滑跳过 |
## P 组 · 保护期(5 句)
| id | 台词全文 | 触发 |
|---|---|---|
| `protect_left` | 训练护盾接住了。那颗在左边,听到左边的呼啸,向左滑。 | 保护期被撞(左) |
| `protect_right` | 训练护盾接住了。那颗在右边,听到右边的呼啸,向右滑。 | 保护期被撞(右) |
| `protect_up` | 训练护盾接住了。那颗在上面,听到高高的呼啸,向上滑。 | 保护期被撞(上) |
| `protect_down` | 训练护盾接住了。那颗在下面,听到低沉的轰隆,向下滑。 | 保护期被撞(下) |
| `protect_end` | 训练护盾关闭。接下来,靠你自己了。 | 第 60 秒 |
## F 组 · 单击/双击分批引入(6 句)
| id | 台词全文 | 触发 |
|---|---|---|
| `feat_boost` | 试试单击,引擎会短促加速,直接冲过空隙。 | 第 20 秒引入 |
| `feat_boost_done` | 感觉到了吗?加速的时候,陨石撞不上你。 | 单击后确认 |
| `feat_shield` | 双击展开护盾,能挡下一次撞击。 | 第 45 秒引入 |
| `feat_shield_done` | 护盾展开后会自动充能,好了我会告诉你。 | 双击后确认 |
| `shield_ready` | 护盾充能完毕。 | 30 秒冷却结束(沿用 R1 id,重录) |
| `shield_block` | 护盾挡下了撞击。 | 护盾生效(沿用 R1 id,重录) |
## H 组 · 被撞归因(5 句,保护期后)
| id | 台词全文 | 触发 |
|---|---|---|
| `hit_left` | 左边撞上的。听到左边的呼啸,向左滑。 | 第 1/2 次被撞(左) |
| `hit_right` | 右边撞上的。听到右边的呼啸,向右滑。 | 右 |
| `hit_up` | 上面撞上的。听到高高的呼啸,向上滑。 | 上 |
| `hit_down` | 下面撞上的。听到低沉的轰隆,向下滑。 | 下 |
| `hit_last` | 船体只剩最后一次了,小心! | 第 2 次被撞后接在方位句后 |
## M 组 · 里程碑(4 句)
| id | 台词全文 | 触发(躲过数) |
|---|---|---|
| `ms_10` | 十颗了,稳。 | 10 |
| `ms_25` | 二十五颗,老练的飞行员。 | 25 |
| `ms_50` | 五十颗,传奇。 | 50 |
| `ms_100` | 一百颗!天空属于你。 | 100 |
## R 组 · 结算(10 句)
| id | 台词全文 | 触发 |
|---|---|---|
| `time_1` | 不到三十秒,再试一次。 | 存活 <30s |
| `time_2` | 超过三十秒,有进步。 | 30~60s |
| `time_3` | 超过一分钟,合格的飞行员。 | 60~120s |
| `time_4` | 超过两分钟,王牌飞行员。 | ≥120s |
| `dodge_1` | 只拨开几颗。 | 躲过 <5 |
| `dodge_2` | 拨开了十来颗。 | 5~14 |
| `dodge_3` | 拨开了几十颗。 | 15~49 |
| `dodge_4` | 拨开了一大群,漂亮。 | ≥50 |
| `new_record` | 新纪录! | 破纪录(插在 time_* 后) |
| `restart_hint` | 单击再飞一次,下滑回大厅。 | 结算尾 |
## Y 组 · 菜单与系统(8 句)
| id | 台词全文 | 触发 |
|---|---|---|
| `second_run_ask` | 上滑重新听演练,单击直接出发。 | 二周目开场 |
| `menu_main` | ~~菜单。左右滑选择,单击确认。~~ → **勘误(R 终审)**:菜单约定已改(00 §5.2),以音频重录版"菜单。单击切换,双击确认。"为准。 | 下滑呼出 |
| `menu_resume` | 继续飞行。 | 菜单项 |
| `menu_lobby` | 回大厅。 | 菜单项 |
| `menu_help` | 重新听教学要点。 | 菜单项 |
| `help_recap` | 左边来向左滑,右边来向右滑,上面来向上滑,下面来向下滑。单击加速,双击护盾。 | 帮助重听(>25 字,生成期拆两句拼接:句 1 四方位,句 2 单击双击) |
| `settings_hint` | 音色、语速、背景音乐,请回大厅修改。 | 菜单内 |
| `paused` | 已暂停。 | 菜单打开时 |
---
**合计 52 句 × 2 性别 = 104 条**(×2 格式 = 208 文件)。
旧 id 处置:R1 生成的 `num*/n_*/intro1-4/hit1/hit2/gameover/restart` 全部弃用(数字拼接与旧教学);`shield_ready`/`shield_block` 沿用 id 重录。平铺旧文件按 R1 约定在切换验证后统一清理。
+106
View File
@@ -0,0 +1,106 @@
# 05c · 《剑与声》增补规范(rpg addendum,R3 补漏)
> 本文件补 [05-rpg-chapter1.md](05-rpg-chapter1.md) 的施工遗漏:①战斗事件 × 音效 × 震动配合总表;②Boss 无声前摇的震动协议;③音频层叠规则修正(amb 与 BGM 冲突);④若干流程细节。与主文档冲突处以本文件为准。
---
## 1. 战斗事件 × 音效 × 震动配合总表(施工对照表)
震动一律用 `engine/haptics.js` 预定义名;手势识别震动(swipe/confirm)由 gesture.js 内置,下表不再重复列出。
| 战斗事件 | sfx | 震动 | 语音 |
|---|---|---|---|
| 遭遇开始 | `tel_<enemy>`(登场即第一次前摇不播,登场用 `enemy_die` 无——登场无专属 sfx,直接语音) | — | `btl_start_<enemy>` |
| 指令切换(左右滑) | — | (内置 swipe) | 指令名 `cmd_*` |
| 攻击命中 | `atk_swing` + `atk_hit` | — | —(敌方半血/死亡见下) |
| 技能·破空斩 | `skill_slash` | — | mp 不足时 `btl_nomp` |
| 防御指令 | — | — | —(标记生效,无播报,下回合减伤自明) |
| 使用金疮药 | `potion` | `pickup` | 无药时 `btl_noitem` |
| **敌方前摇(有声敌人)** | `tel_<enemy>`(1.0s) | 窗口开启同时 `beat` 一次(提示"现在可以格挡",不干扰听觉辨音) | — |
| **格挡成功** | `parry` | `confirm`(即时,双击无延迟) | — |
| 格挡窗口内做错手势 | — | `error` | — |
| 受击(未格挡) | `hurt` | `hit` | 血量档位降档时 `st_hp_mid` / `st_hp_low`(降档才播,不每击都播) |
| 敌方半血 | — | — | `npc_*_half`(有半血句的敌人)/ Boss 见 §4 |
| 击杀 | `enemy_die` | `success` | `btl_enemy_die` |
| 战斗胜利 | — | — | `btl_win` + `btl_loot_*`(分档) |
| 升级 | `levelup` | `success` | `btl_levelup` |
| 逃跑成功 | `flee` | — | `btl_flee` |
| 逃跑失败(Boss/剧情战) | — | `error` | `btl_noflee` |
| 战败 | `hurt`(最后一击) | `hit` + 300ms 后 `error` | `btl_lose` → `btl_lose_hint` |
要点:**前摇窗口的 `beat` 轻震是"格挡窗口已开"的通道标识**——有声敌人靠前摇音辨招式、靠 beat 确认窗口时机,两者分工。iOS 无震动时靠前摇音本身即可(前摇音起始即窗口起始),无信息损失。
## 2. Boss 无声前摇 · 震动协议(影卫 / 白无瞳三阶段)
无声敌人是第三章核心机制(耳朵最强的剑客遇到没有声音的敌人),触觉成为唯一提示通道。协议:
| 时刻 | 震动 | 说明 |
|---|---|---|
| 窗口开启(t=0) | `danger` [25,40,25,40,25] | 代替前摇音——**无声前摇的"声音"就是这段震型** |
| 窗口中点(t=0.5s) | `heartbeat` [40,100,40] | 催促:窗口过半,不出手就来不及了 |
| 格挡成功 | `confirm` | 同有声敌人 |
| 窗口关闭未格挡 | `hit` | 受击 |
规则:
1. **只有无声敌人**用这套协议;有声敌人前摇窗口只震一次 `beat`(§1 表),绝不用 danger——danger 语义专属于"无声威胁",玩家一听(一感)就知道是影卫/白无瞳。
2. iOS 无震动降级:**静默降级,不回退提示音**(00 §2.5)。无声敌人在 iOS 上不可格挡是已知代价,设计上用数值兜住——影卫 ATK16 对 lv7+ 铁鳞甲玩家 dmg≈10,52+ HP 可吃 5 击,不格挡也能硬打过普通影卫;白无瞳三阶段前由 NPC 台词明示(见 §5),iOS 玩家知情后可用"防御"指令(减伤 50%,不需要时机)作为等效手段。**防御指令因此对 iOS 是必要兜底,不是可选项。**
3. 听风剑(格挡窗口 1.0s→1.3s)对无声敌人同样生效:heartbeat 顺延到 t=0.65s。
4. 无声敌人的 `_resolve` 判定窗口与有声一致(1.0s/1.3s 内双击有效),仅提示通道不同——代码上前摇 = `{ sfx: tel_<enemy> | null, haptic: 'beat' | 'danger+heartbeat' }` 二元组。
## 3. 音频层叠规则修正(amb 与 BGM 冲突)
05-rpg-chapter1.md §7.2 让 `amb_*` 走 bgm 层 loop,与 `bgm/explore` 等同层冲突(引擎 BGM 层同时只有一首)。裁定为:
| 场景/状态 | bgm 层播放 | 说明 |
|---|---|---|
| village | `bgm/village`(音乐) | 安全区给音乐,情绪锚点 |
| 其他探索场景 | `amb_<scene>`(环境音循环,volume 0.6) | **探索期不放音乐**——纯环境音,战斗音乐进来才有对比 |
| 普通战斗 | `bgm/battle` | 进战斗 crossfade 替换 amb;战后 crossfade 回当前场景 amb |
| Boss 战 | `bgm/boss` | 同上 |
| 章结算/剧情(evt_ch1_end 等) | `bgm/ending` | 播完回场景 amb |
资产清单变动:`bgm/explore` **取消**(探索期用 amb,不需要探索音乐),BGM 从 5 首减为 4 首(village/battle/boss/ending);`amb_village` 保留(village 的音乐若停播设置 bgm=0 时用 amb_village 垫底——bgm=0 时 bgm 层整体静默,village 也播 amb_village)。sfx 总数 35 不变。
`bgm=0`(设置关闭音乐)时的行为:village/battle/boss/ending 全部静默,探索场景 amb 照播(amb 属内容环境音不是 BGM——**例外裁定**:amb_* 虽走 bgm 层,但不受 bgm 设置关闭影响;实现上 amb 用 `play(id, {volume})` 时引擎按 bgm 层处理会被设置关停,因此 **amb 改走 sfx 层 loop**——sfx 层即发即弃不支持 loop,需要开发在 game.js 里用 `audio._load` + 手动 BufferSource loop 接 `_sfxBus`(rhythm 现版手动接总线已有先例)。这是本增补对实现的唯一新要求。)
## 4. Boss 阶段转换表现
| Boss | 阶段 | 触发 | 表现 |
|---|---|---|---|
| 黑风寨主 | 二阶段(ATK 10→12) | HP ≤50% | `warning` 震动 + `npc_boss1_2` + 前摇音 `tel_boss1` 从单声双刀变急促(同一 sfx 以 playbackRate 1.2 重播即可,不加资产) |
| 腐沼祭主 | 二阶段 | HP ≤50% | `warning` + 召唤 2 毒蛛(连战队列插入),播专属半血句(R4 补录) |
| 白无瞳 | 三阶段(完全无声) | HP ≤50% / ≤25% | 每阶段 `warning`;三阶段进场播 §5 教学句,此后前摇走 §2 震动协议 |
## 5. 补充台词需求(R4 录不进 ch1 表的,列入 ch2/3 台词表)
| id | 角色 | 台词全文 | 用途 |
|---|---|---|---|
| `npc_boss3_warn` | 旁白 | 他没有声音。闭上眼睛没用,竖起耳朵也没用。这一次,相信你的手。 | 白无瞳三阶段进场(同时向 iOS 玩家暗示:防御指令是兜底) |
| `btl_half_shaman` | 蛊师 | 蛊师半血句(R4 写 ch2 台词表时补全文) | 半血钩子 |
| `btl_half_shadow` | 影卫 | 影卫半血句 | 同上 |
| `menu_hint_escape` | — | (属 escape,见 round2-review §5.1,非本游戏) | — |
规则:ch2/3 台词表沿用 05-rpg-ch1-voice-ids.md 格式,R4 由策划补发;上表是必须包含的最小集。
## 6. 流程细节补遗
1. **上滑状态播报链顺序**(探索态与战斗态共用):`st_lv_*` → `st_hp_*` → `st_gold_*` → `st_weapon_*` → `st_armor_*`,链播可被打断(再次上滑重播,下滑进菜单)。
2. **战斗态上滑**:`st_hp_*` + 敌方血量档位(敌方档位句复用 st_hp_* 文案?——**不复用**,敌人档位用:`foe_hp_high`"敌人气焰正盛。"/`foe_hp_mid`"敌人露出疲态。"/`foe_hp_low`"敌人撑不住了。"——R4 补录 3 句 ×2 性别,列入上表最小集)。
3. **连战转换**(enc_bandits 等):第一只死亡 → `btl_enemy_die` → 不停 BGM → 直接播下一只 `btl_start_<enemy>` + 前摇,玩家 HP/MP 不回、指令重置为攻击。
4. **`stats.playSec` 口径**:累计探索态+战斗态实际时长(菜单/结算暂停不计),存档时累加。
5. **存档 schema 迁移**:R5 内允许丢弃重来(读档 schema≠1 时播 `sys_new_game` 重开,stats 保留)。
6. **菜单豁免确认**:RPG 探索态菜单=左右滑切换+单击确认(00 §5.2 豁免条款,round2-review 裁决②);战斗态下滑=逃跑,不设菜单——与 05-rpg.md §2.2 一致,不变。
## 7. R4/R5 施工就绪核对单
- [x] 场景图 JSON / 遭遇战表 / 战斗状态机(05-rpg-chapter1.md §1-3)
- [x] 全数值表 + 装备节点(05-rpg-chapter1.md §4-5)
- [x] 存档字段 + summary(05-rpg-chapter1.md §6)
- [x] 第一章 120 句台词表(05-rpg-ch1-voice-ids.md)
- [x] 战斗音效/震动配合(本文件 §1)
- [x] 无声前摇协议 + iOS 兜底(本文件 §2)
- [x] 音频层叠规则(本文件 §3,含 amb 走 sfx 层的实现要求)
- [x] BGM 4 首 + sfx 35 个 prompt 草稿(05-rpg-chapter1.md §7 + 本文件 §3 修正)
- [ ] ch2/3 台词表(R4 策划补发,最小集见本文件 §5)
- [ ] ch2/3 场景正文(R5 开发按 ch1 模板扩写)
+176
View File
@@ -0,0 +1,176 @@
# 05b · 《剑与声》第一章语音台词表(voice ids,R4 录音依据)
> **给音频总监的录制说明**
> - **旁白**(A/S/D/O/E/B/SH 组,104 句):双性别。**female = Serena,male = Uncle_Fu**(新游戏,沿用 R1 分配惯例)。落 `web/rpg/audio/voice/female|male/<id>.{ogg,m4a}`。
> - female instruct 常量:`平静清晰的年轻女声,像在讲武侠故事`
> - male instruct 常量:`低沉醇厚的男声,像说书人,平静有韵味`
> - **NPC**(N 组,16 句):单音色、不做双性别,平铺落 `web/rpg/audio/voice/<id>.{ogg,m4a}`(引擎平铺回退命中,同 storyline 角色台词模式)。同一角色 speaker+instruct 恒定:
> - 村长:`Uncle_Fu` + `慈祥和缓的老者`
> - 老猎人:`Eric` + `沙哑粗犷,豪爽中带着暖意`
> - 寨丁头目:`Dylan` + `凶狠粗鲁,大声喝骂`
> - 黑风寨主:`Uncle_Fu` + `低沉阴狠,慢条斯理`(与村长同 speaker 不同 instruct,参照 storyline 先例;两者从不在同场景出现,请试听确认区分度)
> - 主角沈听澜**无台词**:旁白第二人称("你")叙述,省一套主角音色。
> - 数字规避:等级/血量/金币全部分档整句;价格写死在商店句里;无运行时拼接。
> - 管线:`tools/gen_voice_dual.py` 照录;多音字已规避("重听"写作"重新听")。
## A 组 · 序章(旁白 ×3)
| id | 台词全文 |
|---|---|
| `intro_1` | 你的眼睛看不见了。但你的耳朵,比谁都灵。 |
| `intro_2` | 黑风寨掳走了你唯一的徒弟。追进迷雾林,把他们连根拔起。 |
| `intro_3` | 先记住,怎么用耳朵走路。 |
## S 组 · 系统/菜单/状态(旁白 ×28)
| id | 台词全文 | 触发 |
|---|---|---|
| `sys_continue_ask` | 检测到你的旅程记录。单击继续,右滑开始新的旅程。 | 有存档进游戏 |
| `sys_new_game` | 新的旅程,开始了。 | 新游戏 |
| `sys_saved` | 进度已保存。 | 存档写入后 |
| `sys_rest` | 你在篝火边睡了一觉,状态全满。 | village 休息 |
| `sys_drink` | 溪水清冽,你喝了个饱,伤势尽消。 | stream 饮水 |
| `sys_menu` | 菜单。左右滑选择,单击确认。 | 下滑呼出 |
| `sys_menu_resume` | 继续旅程。 | 菜单项 |
| `sys_menu_save` | 保存并回大厅。 | 菜单项 |
| `sys_menu_help` | 重新听帮助。 | 菜单项 |
| `help_explore` | 左右滑切换选项,单击确认。双击重新听场景,上滑查看状态,下滑打开菜单。 | 帮助 |
| `help_battle` | 战斗中,左右滑选招式,单击出手。听到敌人的前摇,双击格挡。 | 帮助 |
| `settings_hint` | 音色、语速、背景音乐,请回大厅修改。 | 菜单内 |
| `st_hp_high` | 你状态很好。 | 血量 >70% |
| `st_hp_mid` | 你受了伤。 | 30~70% |
| `st_hp_low` | 你伤势很重,命悬一线。 | <30%(附带 warning 震动) |
| `st_gold_poor` | 口袋空空。 | 金币 <30 |
| `st_gold_some` | 有些积蓄。 | 30~150 |
| `st_gold_rich` | 腰包鼓鼓。 | >150 |
| `st_lv_low` | 你初出茅庐。 | lv 1-2 |
| `st_lv_mid` | 你小有名气。 | lv 3-5 |
| `st_lv_high` | 你已是高手。 | lv 6+ |
| `st_weapon_none` | 你赤手空拳。 | 无武器 |
| `st_weapon_iron` | 你手持铁剑。 | iron_sword |
| `st_weapon_steel` | 你手持青钢剑。 | steel_sword |
| `st_weapon_wind` | 你手持听风剑。 | wind_sword |
| `st_armor_none` | 你身无片甲。 | 无护甲 |
| `st_armor_leather` | 你身着皮甲。 | leather |
| `st_armor_iron` | 你身着铁鳞甲。 | iron_scale |
## D 组 · 场景描述(旁白 ×16)
| id | 台词全文 |
|---|---|
| `desc_village` | 雾口村。篝火噼啪作响,村民低声议论着林子里的土匪。 |
| `desc_village_re` | 你回到了雾口村。 |
| `desc_path` | 林间小路。风穿过树叶,脚下的落叶沙沙作响。 |
| `desc_path_re` | 你回到林间小路。 |
| `desc_fork` | 岔路口。风声在左右两边,听起来不太一样。 |
| `desc_fork_re` | 你回到岔路口。 |
| `desc_chest` | 一个枯树洞。洞里似乎藏着什么。 |
| `desc_chest_re` | 你回到枯树洞前。 |
| `desc_stream` | 溪流。水声潺潺,是个歇脚的好地方。 |
| `desc_stream_re` | 你回到溪流边。 |
| `desc_hunter` | 猎人小屋。柴火正旺,还有磨刀的声音。 |
| `desc_hunter_re` | 你回到猎人小屋。 |
| `desc_deepwood` | 密林深处。虫鸣急促,远处有狼嚎,杀气很重。 |
| `desc_deepwood_re` | 你回到密林深处。 |
| `desc_gate` | 黑风寨门前。火把熊熊,人声嘈杂。 |
| `desc_hall` | 聚义厅。火盆熊熊,大厅空旷,回声冰冷。 |
## O 组 · 选项名(旁白 ×18)
| id | 台词全文 |
|---|---|
| `opt_go_path` | 前进,去林间小路。 |
| `opt_go_fork` | 前进,去岔路口。 |
| `opt_go_chest` | 往左,看看树洞。 |
| `opt_go_stream` | 往右,去溪流。 |
| `opt_go_hunter` | 前进,去猎人小屋。 |
| `opt_go_deepwood` | 前进,进密林深处。 |
| `opt_go_gate` | 前进,去寨门。 |
| `opt_back` | 返回。 |
| `opt_shop` | 去商店。 |
| `opt_rest` | 休息一晚。 |
| `opt_status` | 查看状态。 |
| `opt_investigate` | 调查四周。 |
| `opt_open_chest` | 打开树洞。 |
| `opt_drink` | 饮水休息。 |
| `opt_talk_hunter` | 和老猎人谈谈。 |
| `opt_assault` | 强攻寨门。 |
| `opt_sneak` | 绕行潜入。 |
| `opt_leave` | 离开。 |
## E 组 · 事件(旁白 ×6)
| id | 台词全文 |
|---|---|
| `evt_gold15` | 你在落叶下摸到十五枚金币。 |
| `evt_nothing` | 这里什么都没有了。 |
| `evt_chest` | 树洞里有两瓶金疮药,你收好了。 |
| `evt_hunter_gift` | 老猎人塞给你一瓶金疮药,还说商店会给你算便宜些。 |
| `evt_gate_open` | 寨门缓缓打开。 |
| `evt_ch1_end` | 寨主倒了,但徒弟不在这里。一张字条,指向南方的腐沼。 |
## B 组 · 战斗(旁白 ×26)
| id | 台词全文 | 触发 |
|---|---|---|
| `btl_start_wolf` | 野狼拦路! | 遭遇狼 |
| `btl_start_bandit` | 山贼围上来了! | 遭遇山贼 |
| `btl_start_spider` | 毒蛛从树上垂了下来! | 遭遇毒蛛 |
| `btl_start_elite` | 寨丁头目挡住了去路! | 遭遇头目 |
| `btl_start_boss1` | 黑风寨主,就在你面前。 | Boss 战 |
| `cmd_attack` | 攻击。 | 指令 |
| `cmd_skill` | 破空斩。 | 指令 |
| `cmd_defend` | 防御。 | 指令 |
| `cmd_item` | 金疮药。 | 指令 |
| `btl_nomp` | 内劲不足。 | mp<3 选技能 |
| `btl_noitem` | 没有金疮药了。 | 无药选道具 |
| `btl_flee` | 你抽身退出了战斗。 | 逃跑成功 |
| `btl_noflee` | 避无可避! | Boss/剧情战逃跑 |
| `btl_enemy_die` | 敌人倒下了。 | 击杀 |
| `btl_win` | 战斗胜利。 | 胜利 |
| `btl_loot_small` | 小有收获。 | 战利 exp+gold <15 |
| `btl_loot_mid` | 收获不少。 | 15~40 |
| `btl_loot_big` | 大获全胜,收获颇丰。 | >40 |
| `btl_levelup` | 你的修为精进了。 | 升级(success 震动) |
| `btl_lose` | 你倒下了,黑暗吞没了一切。 | 战败 |
| `btl_lose_hint` | 记住,听清前摇,双击格挡。 | 战败提示 |
| `tut_b1` | 它要扑了,听前摇! | 教学战 |
| `tut_b2` | 就是现在,双击格挡! | 教学战 |
| `tut_b3` | 挡住了!轮到你了。左右滑选攻击,单击出剑。 | 教学战 |
| `tut_b4` | 没挡住也没关系,再来一次。 | 教学战格挡失败 |
| `tut_b5` | 好剑法!接下来,靠你自己了。 | 教学战结束 |
## SH 组 · 商店(旁白 ×7)
| id | 台词全文 |
|---|---|
| `shop_open` | 商店。左右滑看看货,单击购买。 |
| `shop_price_sword` | 铁剑,一百金。 |
| `shop_price_armor` | 皮甲,八十金。 |
| `shop_price_potion` | 金疮药,三十金。 |
| `shop_owned` | 这件你已经有了。 |
| `shop_poor` | 钱不够。 |
| `shop_buy` | 买下了。 |
## N 组 · NPC(单音色平铺 ×16)
| id | 角色 | 台词全文 |
|---|---|---|
| `npc_headman_1` | 村长 | 外乡人,林子里不太平。这五十金,拿着防身。 |
| `npc_headman_2` | 村长 | 商店就在村里,缺什么就去看看。 |
| `npc_headman_3` | 村长 | 听说你是用耳朵走路的剑客?保重啊。 |
| `npc_headman_4` | 村长 | 救救那些被掳走的孩子吧。 |
| `npc_hunter_1` | 老猎人 | 站住。……哦,是个用耳朵的。 |
| `npc_hunter_2` | 老猎人 | 寨子里有我旧识。带上这个,别死在路上。 |
| `npc_hunter_3` | 老猎人 | 黑风寨主的双刀很快,听清他出刀前的动静。 |
| `npc_hunter_4` | 老猎人 | 去吧。让我听听,你的传说。 |
| `npc_elite_1` | 寨丁头目 | 哪儿来的瞎子,敢闯黑风寨! |
| `npc_elite_half` | 寨丁头目 | 你……你到底是什么人! |
| `npc_elite_die` | 寨丁头目 | 寨主……会替我报仇…… |
| `npc_boss1_1` | 黑风寨主 | 盲剑客?正好,省得挖你的眼睛。 |
| `npc_boss1_2` | 黑风寨主 | 有两下子。可惜,你听不懂死期的声音。 |
| `npc_boss1_die` | 黑风寨主 | 白无瞳……会找到你…… |
---
**合计:旁白 104 句 × 2 性别 + NPC 16 句 = 224 条音频**(×2 格式 = 448 文件)。对话功能与语气大纲对照见 [05-rpg.md](05-rpg.md) §5.3,场景/战斗触发逻辑见 [05-rpg-chapter1.md](05-rpg-chapter1.md)。
+238
View File
@@ -0,0 +1,238 @@
# 05a · 《剑与声》第一章施工稿(rpg chapter 1,R4 资产 / R5 开发依据)
> **给开发者的实现要点摘要**
> 1. 本文件 + [05-rpg-ch1-voice-ids.md](05-rpg-ch1-voice-ids.md)(台词表)+ [05-rpg.md](05-rpg.md)(总设计)三者齐全即可施工。场景图、战斗状态机、数值表、存档结构全部定死,不允许实现时自由发挥。
> 2. 战斗是**定时器驱动的回合状态机**(§3 状态转换表):敌方前摇 1.0s 内双击=格挡(双击即时回调,规避 tap 延迟);指令循环 攻击→技能→防御→道具。
> 3. 第一章 8 场景、5 场固定遭遇战(明雷)、2 件装备 + 道具在商店/宝箱/支线获取(§5)。
> 4. 存档 data 结构 §6(含大厅卡片要显示的 `summary` 字符串);保存时机:场景切换/战斗结束/商店交易后。
> 5. 资产清单 §7:5 首 BGM(Mureka prompt 草稿)+ 35 个 sfx(Stable Audio 3 prompt 草稿 / 程序合成标注)。语音 ~114 句见台词表文件。
---
## 1. 第一章场景图(数据表,直接转 JSON)
```js
const CH1 = {
start: 'village',
scenes: {
village: { name: '雾口村', amb: 'amb_village', options: ['go_path', 'shop', 'rest', 'status'] },
path: { name: '林间小路', amb: 'amb_path', options: ['go_fork', 'investigate', 'back'] },
fork: { name: '岔路口', amb: 'amb_fork', options: ['go_chest', 'go_stream', 'back'] },
chest: { name: '枯树洞', amb: 'amb_chest', options: ['open_chest', 'back'] },
stream: { name: '溪流', amb: 'amb_stream', options: ['drink', 'go_hunter', 'back'],
encounter: 'enc_teach_wolf' }, // 首次进入先触发教学战
hunter: { name: '猎人小屋', amb: 'amb_hunter', options: ['talk_hunter', 'go_deepwood', 'back'] },
deepwood: { name: '密林深处', amb: 'amb_deepwood', options: ['go_gate', 'back'],
encounter: 'enc_bandits' }, // 首次进入触发山贼连战
gate: { name: '黑风寨门前', amb: 'amb_gate', options: ['assault', 'sneak', 'back'] },
boss1: { name: '聚义厅', amb: 'amb_hall', options: [],
encounter: 'enc_boss1' }, // 由 gate 两个选项分别进入
},
};
```
选项语义(选项 id → 行为):
| 选项 id | 播报语音 | 行为 |
|---|---|---|
| `go_*` | `opt_go_<场景>` | 切换场景(播脚步 sfx + 新场景 desc) |
| `back` | `opt_back` | 回上一场景 |
| `shop` | `opt_shop` | 进商店子菜单(§4) |
| `rest` | `opt_rest` | HP/MP 回满,播 `sys_rest`;仅 village |
| `status` | `opt_status` | 角色状态播报(同上滑) |
| `investigate` | `opt_investigate` | 一次性:+15 金币(flag `chest_path`),播 `evt_gold15`;之后播 `evt_nothing` |
| `go_chest` | `opt_go_chest` | 岔路左支线 |
| `open_chest` | `opt_open_chest` | 一次性:金疮药×2(flag `chest_fork`) |
| `drink` | `opt_drink` | HP 回满,播 `sys_drink` |
| `talk_hunter` | `opt_talk_hunter` | 支线对话(flag `hunter_quest`:金疮药×1 + 商店九折 flag `shop_discount`) |
| `assault` | `opt_assault` | 战 `enc_elite` → 胜后进 boss1 |
| `sneak` | `opt_sneak` | 战 `enc_sneak`(较弱)→ 进 boss1 且 HP/MP 回满 |
## 2. 遭遇战表(第一章,全部明雷固定)
| 遭遇 id | 敌人 | 触发 | 特殊 |
|---|---|---|---|
| `enc_teach_wolf` | 教学野狼 ×1 | stream 首次进入 | 教学战:玩家 HP 锁底 1(不败),分步教学(台词表 B 组) |
| `enc_wolf` | 野狼 ×1 | 教学战后 stream 再入 50% 刷新一次 | 可逃 |
| `enc_bandits` | 山贼 ×2(连战两场) | deepwood 首次进入 | 不可逃;两场间不回血 |
| `enc_elite` | 寨丁头目 ×1 | gate 选 assault | 不可逃 |
| `enc_sneak` | 山贼 ×1 + 毒蛛 ×1(连战) | gate 选 sneak | 不可逃;胜后进 boss 前回满 |
| `enc_boss1` | 黑风寨主 ×1 | boss1 进入 | Boss 战,不可逃;半血进二阶段 |
## 3. 战斗状态机(完整状态转换表)
状态:`B_START → (回合循环) → VICTORY | DEFEAT | FLED`
| 状态 | 进入时副作用 | 出口转换 |
|---|---|---|
| `B_START` | 播 `btl_start_<enemy>` + 敌方登场 sfx; init 回合数=1 | 自动 → `ENEMY_TELEGRAPH`(敌方先攻的蛊师类)或 `PLAYER_SELECT`(默认玩家先攻)。第一章全部玩家先攻 |
| `PLAYER_SELECT` | 播当前指令名(`cmd_attack`),左右滑循环切换(攻击→技能→防御→道具),单击执行;上滑播战场状态;下滑请求逃跑 | 单击 → `PLAYER_EXECUTE`;下滑 → `FLEE_CHECK` |
| `PLAYER_EXECUTE` | 按指令结算:攻击=伤害公式;技能=atk×2 耗 mp3(mp 不足播 `btl_nomp` 留在本状态重选);防御=置 halfDamage 标记;道具=金疮药回 40%maxHp(无药播 `btl_noitem` 重选)。播对应 sfx + 敌方受击音 | 结算完 → `CHECK_ENEMY_DEAD` |
| `CHECK_ENEMY_DEAD` | 敌 HP≤0:播 `btl_enemy_die`;连战则拉下一个敌人回 `B_START`(不重置玩家状态) | 敌死 → `VICTORY`(无下一只);敌活 → `ENEMY_TELEGRAPH` |
| `ENEMY_TELEGRAPH` | 播敌方前摇音(`tel_<enemy>`,1.0s);启动格挡窗口 1.0s | 窗口内双击 → 格挡标记=true(`vibrate('confirm')` + `sfx/parry`),→ `ENEMY_RESOLVE`;窗口结束 → `ENEMY_RESOLVE`;窗口内其他手势 → `vibrate('error')`,留在窗口 |
| `ENEMY_RESOLVE` | 结算敌方伤害:`dmg×0.3`(格挡)/`×0.5`(防御)/全值;播受击音 + `vibrate([80])`(格挡成功不震 HIT,已震 confirm);血量档位变化时播 `btl_hp_*` | 玩家 HP≤0 → `DEFEAT`;否则回合数+1 → `PLAYER_SELECT` |
| `FLEE_CHECK` | 非 Boss 战:播 `btl_flee` 脱离;Boss/剧情战:播 `btl_noflee` 回 `PLAYER_SELECT` | → `FLED`(回场景,遭遇不消失)或 `PLAYER_SELECT` |
| `VICTORY` | 播 `btl_win` + 经验/金币获得句(整句分档,见台词表)→ 结算升级(升级播 `btl_levelup` + `vibrate('success')`)→ 写存档 | → 场景探索态 |
| `DEFEAT` | 播 `btl_lose` + 死亡归因句 → 章首(village)重生:HP/MP 回满,等级/金币/装备/flags 保留,本章非 Boss 遭遇重置 | → village 探索态 |
半血叙事钩子:敌人 HP 首次 ≤50% 时在 `CHECK_ENEMY_DEAD` 前插播 `btl_half_<enemy>`(无此语音的敌人跳过)。
## 4. 数值表
### 4.1 玩家
```
maxHp = 20 + 8*(lv-1) // lv1=20, lv3=36, lv5=52
atk = 5 + 2*(lv-1) + 武器atk
def = 护甲def
mp = 3 + lv // 破空斩耗 3
expToNext = lv * 20
dmg = max(1, round((atk攻 - def守) * (0.85~1.15)))
```
### 4.2 敌人(全游戏 8 种 + 3 Boss;第一章用前 4 种 + Boss1)
| 敌人 | 章 | HP | ATK | DEF | EXP | 金币 | 前摇音 sfx id | 前摇特征 | 半血句 |
|---|---|---|---|---|---|---|---|---|---|
| 教学野狼 | 1 | 6 | 3 | 0 | 5 | 0 | `tel_wolf` | 扑击前低吼 | 无 |
| 野狼 | 1 | 10 | 4 | 0 | 8 | 5 | `tel_wolf` | 低吼 | 无 |
| 山贼 | 1 | 16 | 6 | 1 | 12 | 12 | `tel_bandit` | 脚步+拔刀 | `btl_half_bandit` |
| 毒蛛 | 1 | 12 | 7 | 0 | 12 | 8 | `tel_spider` | 高频肢节摩擦 | 无 |
| 寨丁头目 | 1 | 24 | 8 | 2 | 20 | 25 | `tel_elite` | 重甲碰撞 | `btl_half_elite` |
| **Boss 黑风寨主** | 1 | 45 | 10 | 2 | 50 | 100 | `tel_boss1` | 双刀互击两声 | 二阶段:ATK→12,播 `boss1_phase2` |
| 沼泽鳄 | 2 | 30 | 11 | 2 | 22 | 18 | `tel_croc` | 水花+低吼 | 无 |
| 蛊师 | 2 | 22 | 12 | 1 | 26 | 30 | `tel_shaman` | 吟唱(敌方先攻) | 有 |
| 剑傀 | 3 | 38 | 14 | 3 | 40 | 45 | `tel_puppet` | 机括咔哒 | 无 |
| 影卫 | 3 | 30 | 16 | 2 | 45 | 50 | 无声(震动 DANGER 代替) | 寂静 | 有 |
| **Boss 腐沼祭主** | 2 | 70 | 14 | 3 | 90 | 200 | `tel_boss2` | 人声哼鸣 | 二阶段召唤 2 毒蛛 |
| **Boss 白无瞳** | 3 | 110 | 18 | 5 | 150 | 500 | 三阶段完全无声 | 寂静+DANGER | 三次阶段转换 |
经验校验:第一章可得 exp = 5+8+12×2+20+12+12+50 = 131+(教学狼 5 + 野狼 8 + 山贼 24 + 头目/偷袭线 20~24 + Boss 50)≈ 110-135 → 打完第一章玩家 lv3(20+40+60=120 可到 lv4 门槛前),数值节奏成立。
### 4.3 装备与道具(获取节点)
| 物品 | 效果 | 获取 |
|---|---|---|
| 铁剑 | atk+3 | village 商店 100 金(开局 50 金 + 小路调查 15 + 若干战斗;猎人支线后九折 90) |
| 皮甲 | def+2 | village 商店 80 金(九折 72) |
| 金疮药 | 回 40% maxHp | 商店 30 金;枯树洞宝箱 ×2;猎人支线 ×1;叠加上限 5 |
| 青钢剑 | atk+6 | 第二章商店 300 |
| 铁鳞甲 | def+4 | 第二章商店 250 |
| 听风剑 | atk+9,格挡窗口 1.0s→1.3s | 第三章商店 600 |
### 4.4 商店子菜单(village)
进入播 `shop_open` → 左右滑循环:铁剑/皮甲/金疮药/离开 → 单击购买(播物品名+价格档句 `shop_price_<item>`;钱不够播 `shop_poor`;已购装备播 `shop_owned`)→ 购买成功 `sfx/coin` + `vibrate([15,40,15])` → 写存档。
## 5. 探索态手势(复读总设计,施工口径)
左右滑=切选项(播选项名)/ 单击=确认 / 双击=重听场景描述+环境音 / 上滑=状态播报(`st_lv`、`st_hp_*`、`st_gold_*`、`st_equip` 链播,全整句分档)/ 下滑=菜单(继续 / 保存并回大厅 / 重听帮助)。血量档位:>70% `btl_hp_high`("你状态很好")、30-70% `btl_hp_mid`("你受了伤")、<30% `btl_hp_low`("你伤势很重")。
## 6. 存档 JSON(精确定义)
`PUT /api/progress { game: 'rpg', data: ... }` 的 `data`:
```json
{
"schema": 1,
"updatedAt": "2026-08-29T12:00:00Z",
"summary": "第一章·溪流 · Lv3 · 金币120",
"chapter": 1,
"scene": "stream",
"player": {
"lv": 3, "exp": 45,
"hp": 28, "maxHp": 36, "mp": 6,
"gold": 120,
"weapon": "iron_sword",
"armor": "leather",
"potions": 2
},
"flags": {
"tutorial_battle_done": false,
"chest_path": false,
"chest_fork": false,
"hunter_quest": false,
"shop_discount": false,
"enc_bandits_done": false,
"enc_elite_done": false,
"enc_sneak_done": false,
"boss1_done": false
},
"chapters": { "1": "open", "2": "locked", "3": "locked" },
"stats": { "kills": 0, "deaths": 0, "parries": 0, "playSec": 0 }
}
```
字段规则:
| 字段 | 默认 | 说明 |
|---|---|---|
| `schema` | 1 | 升版时 R5+ 写迁移或丢弃重来 |
| `summary` | `"新的旅程"` | 大厅卡片显示;格式 `第X章·场景名 · LvN · 金币M` |
| `chapter`/`scene` | 1 / `village` | 继续游戏落点 |
| `player.*` | lv1 exp0 hp20 maxHp20 mp4 gold50 weapon null armor null potions 1 | 开局 50 金为村长赠送剧情款 |
| `weapon`/`armor` | null | `iron_sword`/`steel_sword`/`wind_sword`;`leather`/`iron_scale` |
| `flags.*` | false | 一次性事件锁 |
| `chapters` | 见上 | `locked`/`open`/`cleared` |
| `stats.*` | 0 | 只增不减,新游戏也保留 |
- 保存时机:场景切换后、战斗结算后、商店交易后、菜单"保存并回大厅"。localStorage `soundgame.progress.rpg` 与服务器双写。
- 进游戏有存档时:播 `sys_continue_ask`("继续上次的旅程?单击继续,右滑开始新游戏。");新游戏清空 data 保留 `stats` 累计。
## 7. 资产需求清单
### 7.1 BGM(5 首,Mureka instrumental,60-90s 无缝循环,沿用归一化工艺)
| id | 用途 | Mureka prompt 草稿(英文) |
|---|---|---|
| `bgm/explore` | 第一章探索底 | "Atmospheric Chinese wuxia ambient, bamboo forest mist, sparse guzheng plucks over low drone, slow tempo 60 bpm, mysterious but calm, no vocals, seamless loop" |
| `bgm/battle` | 普通战斗 | "Tense wuxia battle music, driving taiko drums 100 bpm, sharp erhu stabs, urgent but not chaotic, no vocals, seamless loop" |
| `bgm/boss` | Boss 战 | "Epic dark wuxia boss theme, heavy taiko 120 bpm, dissonant strings, dramatic tension, no vocals, seamless loop" |
| `bgm/village` | 村庄/安全区 | "Warm peaceful Chinese village ambiance music, soft dizi flute melody, gentle plucked strings, cozy 70 bpm, no vocals, seamless loop" |
| `bgm/ending` | 章结算/剧情 | "Melancholic wuxia emotional theme, solo erhu over soft pads, bittersweet, slow, no vocals, seamless loop" |
### 7.2 SFX(35 个;SA3 = Stable Audio 3 生成,SYN = 程序合成)
| 类别 | id | 方式 | prompt 草稿 / 说明 |
|---|---|---|---|
| 敌方前摇 | `tel_wolf` | SA3 | "wolf growl then lunge, short, dry, close mic" |
| | `tel_bandit` | SA3 | "footsteps on dirt then sword unsheathing shing, 1 second" |
| | `tel_spider` | SA3 | "insect legs skittering, high frequency chitin clicks, eerie, 1 second" |
| | `tel_elite` | SA3 | "heavy armor plates clanking, menacing, 1 second" |
| | `tel_boss1` | SA3 | "two metal blades striking each other twice, dramatic ring" |
| | `tel_croc` | SA3 | "water splash with low reptile growl" |
| | `tel_shaman` | SA3 | "low guttural chant humming, ritualistic, dark" |
| | `tel_puppet` | SA3 | "wooden mechanism clicks and winding gears" |
| 战斗 | `atk_swing` | SA3 | "sword whoosh through air, fast, clean" |
| | `atk_hit` | SA3 | "sword hitting flesh, dull impact, not gory" |
| | `parry` | SA3 | "metal parry clang, bright ring, short" |
| | `skill_slash` | SA3 | "powerful sword slash with air rip, dramatic" |
| | `hurt` | SYN | 低频闷击(程序合成,避免与 atk_hit 混淆) |
| | `enemy_die` | SA3 | "body falling to ground with weapon clatter" |
| | `potion` | SA3 | "drinking liquid gulps, glass bottle" |
| | `flee` | SYN | 渐远脚步 |
| 探索/系统 | `step` | SYN | 单步脚步(复用 escape 工艺) |
| | `coin` | SYN | 硬币叮当(短合成) |
| | `chest` | SA3 | "old wooden chest creaking open" |
| | `drink_water` | SA3 | "scooping and drinking stream water" |
| | `rest` | SA3 | "settling down by campfire, fabric rustle" |
| | `levelup` | SYN | 上行钟声琶音(内容音非提示音,允许) |
| | `door_heavy` | SA3 | "heavy wooden gate slowly opening, deep creak" |
| | `ui_deny` | — | **不做**(反馈走震动 error) |
| 场景环境 | `amb_village` | SA3 | "night village ambience, campfire crackle, distant chatter, crickets, loop" |
| | `amb_path` | SA3 | "forest path ambience, wind through leaves, birds distant, loop" |
| | `amb_fork` | SA3 | "forest crossroads ambience, wind split feel, crows, loop" |
| | `amb_chest` | SA3 | "hollow tree hole ambience, drips, muffled wind, loop" |
| | `amb_stream` | SA3 | "flowing stream over rocks, gentle water, loop" |
| | `amb_hunter` | SA3 | "wood cabin ambience, firewood crackle, knife sharpening occasionally, loop" |
| | `amb_deepwood` | SA3 | "deep forest ambience, insects, distant wolf howl, ominous, loop" |
| | `amb_gate` | SA3 | "bandit camp outside gate ambience, torches burning, rowdy distant voices, loop" |
| | `amb_hall` | SA3 | "large hall ambience, fire brazier, echoing space, tense, loop" |
| 二/三章备用 | `tel_croc/tel_shaman/tel_puppet`(已列) + `amb_swamp`、`amb_peak`、`tel_boss2` | SA3 | 沼气泡/山峰风/哼鸣,R4 一起生成 |
| 格挡无声敌人 | (无 sfx,震动 DANGER) | — | 影卫/白无瞳三阶段 |
计数:前摇 8 + 战斗 9 + 探索系统 8 + 环境 9 + Boss2 前摇 1 = **35**(`ui_deny` 不做,无声敌人不占 sfx)。环境音 amb_* 走 bgm 层 loop(低音量 volume 0.5),不占 sfx 并发。
## 8. 与台词表的对应
语音 id 共 114 个(旁白双性别 ~70 + NPC 平铺单音色 ~44),见 [05-rpg-ch1-voice-ids.md](05-rpg-ch1-voice-ids.md)。场景 desc、选项名、战斗播报、商店、状态档位、教学全部覆盖;开发 agent 只许引用表内 id。
+209
View File
@@ -0,0 +1,209 @@
# 05 · 新 RPG《剑与声》完整设计(rpg / Sword & Sound)
> **给开发者的实现要点摘要**
> 1. 纯音频回合制 RPG:节点式场景图(不是网格),左右滑=切换选项、单击=确认/前进、双击=重听、上滑=角色状态、下滑=菜单。回合制战斗:左右滑选指令、单击执行;敌方攻击有 1s 前摇音,前摇内**双击=格挡**(减伤 70%)——这是唯一的即时操作,绑双击规避 tap 延迟。
> 2. 规模按"一轮开发(R5)可完成"控制:3 章、每章 6-8 个场景节点、共 8 种敌人 + 3 个 Boss、装备 6 件、数值一套线性公式。复杂度对标 escape(单文件 game.js + ~120 条语音 ×2 音色)。
> 3. 进度存服务器:存档 JSON 结构见 §7,在场景切换、战斗结束、商店交易后自动保存。
> 4. 第一章《迷雾林》内容全部给出(§5):8 场景、敌人表、对白功能大纲。第二、三章只给结构骨架与 Boss 设定,正文沿用第一章模式由开发 agent 扩写。
> 5. 语音全部整句(禁止数字拼接):血量/等级/金币的播报用分段整句("毫发无损/受了轻伤/伤势很重/命悬一线"),数值细节进上滑状态播报的档位句。
>
> **R2 增补**:施工级文档已就位,以这两份为准——
> - [05-rpg-chapter1.md](05-rpg-chapter1.md):第一章场景图 JSON、战斗状态机转换表、全数值表、存档字段定义、BGM/sfx 资产清单(含 Mureka/SA3 prompt 草稿)。
> - [05-rpg-ch1-voice-ids.md](05-rpg-ch1-voice-ids.md):第一章 120 句台词表(旁白 104×2 性别 + NPC 16 单音色)。
> 本文 §7 的存档结构、§9 的资产数量为设计初稿,若与施工稿不一致,**以 05-rpg-chapter1.md 为准**(主要差异:存档 data 为服务器自由格式 + 必带 `summary` 字段;语音总量 104×2+16)。
---
## 1. 游戏定位与核心循环
- **题材**:武侠/奇幻。玩家是"盲剑客"沈听澜——眼盲,耳即眼。题材与纯音频玩法互为叙事 justification,敌人方位、招式前摇全部天然用声音呈现。
- **单局时长**:一章 15-25 分钟;全三章 60-80 分钟。死亡从章首重生(背包/等级保留),鼓励反复推图。
- **核心循环**:探索场景 → 听环境音与描述 → 选择行动(前进/调查/战斗/商店/休息)→ 回合制战斗(格挡是技巧点)→ 获得经验金币 → 升级换装备 → 打 Boss → 下一章。
## 2. 手势映射(符合全局默认,无需豁免)
### 2.1 探索态
| 手势 | 作用 |
|---|---|
| 左/右滑 | 在当前场景的选项间切换(每切换播选项名 + SWIPE_OK 震动)。选项循环 |
| 单击 | 确认执行当前选项(CONFIRM 震动) |
| 双击 | 重听当前场景描述 + 环境音(重听是探索游戏的生命线) |
| 上滑 | 角色状态播报(等级档位、血量档位、金币档位、装备名,全整句) |
| 下滑 | 菜单:继续 / 保存并回大厅 / 重听帮助 |
### 2.2 战斗态
| 手势 | 作用 |
|---|---|
| 左/右滑 | 切换指令:攻击 → 技能(破空斩,耗内劲)→ 防御(本回合受伤减半)→ 道具(金疮药,回 40% HP)→ 循环 |
| 单击 | 执行当前指令 |
| **双击** | **格挡**:仅在敌方攻击前摇音播放的 1.0s 窗口内有效,成功减伤 70% 并播格挡音;窗口外双击无效(ERROR 震动) |
| 上滑 | 战场状态(己方血量档位 + 敌方血量档位) |
| 下滑 | 尝试逃跑(Boss 战无效,播"避无可避") |
格挡窗口的 300ms tap 延迟问题不存在——格挡绑双击(即时回调),这是全局规范"即时反馈绑滑动/双击"的应用。
## 3. 数值框架(简单可平衡)
### 3.1 玩家
```
等级 lv: 1..10
maxHp = 20 + 8*(lv-1) // lv1=28? 不——lv1=20, lv10=92
atk = 5 + 2*(lv-1) + 武器atk // lv1 裸装=5
def = 护甲def
内劲 mp = 3 + lv // 破空斩耗 3,伤害 = atk*2
升级所需 exp = lv * 20 // lv1→2 需 20,lv9→10 需 180;全三章总 exp ≈ 900
```
伤害公式:`dmg = max(1, round((攻方atk - 守方def) * (0.85 ~ 1.15 随机)))`
格挡:`dmg = max(0, round(dmg * 0.3))`;防御指令:`dmg = round(dmg * 0.5)`(与格挡不叠加)。
### 3.2 敌人表(三章通用模板 + 第一章实例)
| 敌人 | 章 | HP | ATK | DEF | EXP | 金币 | 前摇音特征 |
|---|---|---|---|---|---|---|---|
| 野狼 | 1 | 10 | 4 | 0 | 8 | 5 | 扑击前低吼 |
| 山贼 | 1 | 16 | 6 | 1 | 12 | 12 | 挥刀前的脚步 |
| 毒蛛 | 1 | 12 | 7 | 0 | 12 | 8 | 肢节摩擦(高频) |
| 精英·寨丁头目 | 1 | 24 | 8 | 2 | 20 | 25 | 重甲碰撞 |
| **Boss 黑风寨主** | 1 | 45 | 10 | 2 | 50 | 100 | 双刀互击(两下金属声) |
| 沼泽鳄 / 蛊师 / … | 2 | 按 lv4-6 玩家数值 +30% 调 | | | 18-30 | | |
| **Boss 腐沼祭主** | 2 | 70 | 14 | 3 | 90 | 200 | 吟唱(人声哼鸣) |
| 剑傀 / 影卫 / … | 3 | 按 lv7-9 玩家数值 +30% 调 | | | 35-50 | | |
| **Boss 盲剑宿敌·白无瞳** | 3 | 110 | 18 | 5 | 150 | 500 | 无声!前摇是"寂静"(特殊机制:格挡窗口用震动 DANGER 提示,教学点在 Boss 战开场由旁白讲明) |
第三章 Boss"无声前摇"是全游戏叙事+玩法高潮:耳朵最强的剑客遇到没有声音的敌人,格挡提示从听觉转触觉——呼应全局震动语言。
### 3.3 装备与道具
| 物品 | 类型 | 效果 | 价格 |
|---|---|---|---|
| 铁剑 | 武器 | atk+3 | 100(一章商店) |
| 青钢剑 | 武器 | atk+6 | 300(二章商店) |
| 听风剑 | 武器 | atk+9,格挡窗口 +0.3s | 600(三章商店,特色装) |
| 皮甲 | 护甲 | def+2 | 80 |
| 铁鳞甲 | 护甲 | def+4 | 250 |
| 金疮药 | 道具 | 回复 40% maxHp | 30,可叠 5 瓶 |
平衡校验:lv3 玩家(atk≈9+3=12 铁剑)打野狼 dmg≈11-13 → 一刀秒;打山贼 2 刀;Boss 黑风寨主 45HP 需 ~5 刀,Boss dmg 对皮甲玩家 ≈ 8-10/刀,玩家 36HP 可吃 4 刀,格挡成功只吃 2-3 → 不格挡会死、会格挡能过,技巧有真实收益。
## 4. 场景与探索结构
- 每章 = **有向节点图**(非网格):节点 = 场景,边 = 选项。探索时当前场景播环境音循环底(长底噪用 bgm 层 loop、低音量)+ 首次进入播描述语音。
- 每章结构:`起点(教学/商店) → 主线节点 ×4~5 → Boss`,其中 2 个节点带岔路(一条主线、一条奖励支线:宝箱/支线 NPC/休息点)。
- 战斗触发:进入战斗节点即触发(暗雷不做了——纯音频里"突然遇敌"没有视觉游戏的信息缓冲,全部明雷:场景描述里明说"有杀气")。
- 场景环境音要求:每场景一个专属 amb 循环(同 escape 重设计的做法,Stable Audio 3 生成)。
## 5. 第一章《迷雾林》详细内容
### 5.1 场景列表(8 节点)
| # | 场景 id | 名称 | 环境音 | 选项 | 说明 |
|---|---|---|---|---|---|
| 1 | `village` | 雾口村 | 篝火+人语 | 前进 / 商店 / 休息(回满 HP) / 状态 | 起点兼教学关;商店卖铁剑/皮甲/金疮药 |
| 2 | `path` | 林间小路 | 风+落叶 | 前进 / 调查(找到 15 金币) | 教学:双击重听、上滑状态 |
| 3 | `fork` | 岔路口 | 风分两路(立体声) | 左(宝箱:金疮药×2) / 右(主线) | 教学:左右滑选择 |
| 4 | `stream` | 溪流 | 流水 | 饮水(回满) / 前进 | 回复点;此处触发教学战:野狼×1(教学格挡,必不死) |
| 5 | `hunter` | 老猎人小屋 | 柴火+磨刀 | 对话(支线:获金疮药×1+铁剑九折) / 前进 | NPC 角色音首登场 |
| 6 | `deepwood` | 密林深处 | 虫鸣+远处狼嚎 | 前进(遭遇山贼×2 连战) | 本章难度峰值前 |
| 7 | `gate` | 黑风寨门前 | 火把+人声嘈杂 | 强攻(战寨丁头目) / 绕行(多打一场但 Boss 战前满状态) | 战术选择 |
| 8 | `boss1` | 聚义厅 | 大厅混响+火盆 | Boss 黑风寨主 | 章 Boss;胜利→第二章解锁 |
### 5.2 教学编排(探索与战斗各自交互式)
- 场景 1:旁白教"左右滑选、单击确认",让玩家亲手在商店完成一次"听价→选→买"(给 50 金币启动金)。
- 场景 3:教左右滑选择岔路 + 双击重听。
- 场景 4 教学战(野狼,HP 锁 1 不死保护):旁白分步——"它要扑了,听前摇!"→ 低吼响起 → "双击格挡!"→ 成功/失败都继续(失败只掉 1 血),然后教"左右滑选攻击,单击出剑"。玩家亲手砍出第一刀后教学结束。
- 之后不再教学,上滑随时可重听帮助。
### 5.3 对白大纲(每句注明功能与语气;正文由开发 agent 按语气扩写,整句、≤25字/句)
**序章旁白**(旁白音:Serena/Uncle_Fu,平静叙述)
1. "你的眼睛看不见了。但你的耳朵,比谁都灵。"——立人设,沉稳
2. "黑风寨掳走了你唯一的徒弟。追进迷雾林,把他们连根拔起。"——给动机,冷
3. "先记住怎么用耳朵走路。"——转教学,缓和
**村长**(village,长者音:Uncle_Fu 同款 speaker 不同 instruct?"同一角色 speaker+instruct 固定"——村长=Uncle_Fu+慈和 instruct;旁白 male 版=Uncle_Fu+平静 instruct。同 speaker 不同角色时 instruct 不同是允许的,规范约束的是"同一角色恒定")
4. 问候+送 50 金币:"外乡人,林子里不太平。这点钱拿着防身。"——慈和
5. 商店引导 2 句——功能性,慢速清晰
**老猎人**(hunter,沙哑老者 instruct)
6. 戒备开场:"站住。……哦,是个用耳朵的。"——先冷后奇
7. 支线给予:"寨子里有我旧识。带上这个,别死在路上。"——粗中有暖(功能:给药+折扣 flag)
**寨丁头目**(deepwood/gate,粗哑)
8. 遭遇喝骂 1 句——凶
9. 半血狠话 1 句——凶转慌(功能:血量反馈的叙事化)
**黑风寨主**(boss1,低沉反派)
10. 登场宣言:"盲剑客?正好,省得挖你眼睛。"——狂
11. 半血:"有两下子。可惜你听不懂死期的声音。"——狠(功能:阶段转换信号)
12. 败亡遗言——惊愕→不甘(功能:章结束钩子,暗示白无瞳存在)
**章尾旁白**
13. "寨主倒了。但徒弟不在这里——一张字条,指向南方的腐沼。"——收束+引第二章
## 6. 第二、三章骨架
- **第二章《腐沼泽》**:7 节点。新机制:毒状态(每回合 -3 HP,金疮药可解)、蛊师敌人(先攻)。Boss 腐沼祭主二阶段(半血召唤 2 小蛛)。解锁青钢剑/铁鳞甲。
- **第三章《无瞳峰》**:7 节点。新机制:黑暗战斗(部分敌人无声前摇,靠震动格挡——为终 Boss 铺垫)、听风剑。终 Boss 白无瞳三阶段,终阶段完全无声前摇。
- 两章正文沿用第一章模板(场景表+敌人表+对白功能大纲)由开发 agent 扩写,规模与第一章相当。
## 7. 存档结构(progress JSON)
信封按 00 文档 §4.2(`{schema, game:"rpg", updatedAt, data}`),`data`:
```json
{
"chapter": 1,
"scene": "stream",
"player": {
"lv": 3, "exp": 45,
"hp": 28, "maxHp": 36, "mp": 6,
"gold": 120,
"weapon": "iron_sword", "armor": "leather",
"potions": 2
},
"flags": {
"tutorial_explore_done": true,
"tutorial_battle_done": true,
"hunter_quest": true,
"chest_fork": false,
"shop_discount": true
},
"chapters": { "1": "cleared", "2": "unlocked", "3": "locked" },
"stats": { "kills": 12, "deaths": 1, "parries": 9, "playSec": 1500 }
}
```
- **保存时机**:场景切换后、战斗结算后、商店交易后、菜单选"保存并回大厅"。写 localStorage 并 PUT 服务器(未登录只写本地)。
- **读取**:进游戏检测到存档 → 语音问"继续上次的旅程?单击继续,右滑开始新游戏"。新游戏清空 data 保留 stats 累计。
- **版本**:`schema` 升版时开发 agent 写迁移或丢弃重来(R 轮内允许丢弃,文档标注即可)。
## 8. 战斗回合时序(实现契约)
```
回合开始 → 若敌人存活:播敌人行动意图音(前摇 1.0s,期间双击=格挡判定)
→ 前摇结束结算敌方攻击(应用格挡/防御减伤)→ 播受击/格挡音 + 震动(HIT / CONFIRM)
→ 玩家指令阶段:左右滑切指令(播指令名)→ 单击执行 → 播攻击音 + 敌方受击音
→ 敌方血量档位播报仅在上滑查询时播;半血/死亡自动播叙事短句
→ 循环。玩家 HP≤0 → 战败旁白 → 章首重生(等级金币保留,hp 回满)
```
- 敌方前摇音 = 敌人表"前摇音特征"列的 sfx(每敌人一个专属前摇音,这是"听声辨招"的核心资产,优先级高于环境音)。
- 内劲技能"破空斩":atk*2,mp-3,mp 不足时指令播报"内劲不足"并跳过该选项。
## 9. 资产清单(规模核对)
| 类别 | 数量 | 备注 |
|---|---|---|
| bgm | 5 | 3 章环境底各 1 + 战斗 1 + Boss 1(Mureka,循环) |
| sfx | ~35 | 8 敌人前摇 + 3 Boss 前摇 + 攻击/受击/格挡/技能/买药/开箱/升级 + 8 场景 amb 循环(或 amb 走 bgm 层) |
| voice | ~120 句 ×2 音色 | 旁白 ~30、NPC ~40、教学 ~15、战斗播报 ~20、状态/菜单 ~15;全部整句、角色 speaker+instruct 常量化 |
| 对标 | escape 现版约 40 句语音 + 15 sfx;《剑与声》约为其 3 倍,R4 备资产 + R5 开发一轮可行(场景/敌人/指令全部数据驱动,game.js 单文件预计 800-1000 行) |
## 10. 开发次序建议(R5 一轮内)
1. 数据表(场景图/敌人/物品/对话 id)→ 2. 探索循环 + 菜单 + 状态播报 → 3. 战斗回合机(含格挡窗口)→ 4. 商店/道具/升级 → 5. 存档读写 → 6. 第一章内容灌入 → 7. 第二、三章灌入 → 8. 教学关与打磨。资产(R4 已生成)缺失时用 console.error 静默跳过的引擎特性兜底,先通流程后补音。
+45
View File
@@ -0,0 +1,45 @@
# SoundGame 设计文档(5 轮迭代全记录)
本目录是纯音频游戏合集的设计规范与审查档案。开发 agent 开工前必读对应文档开头的
"给开发者的实现要点摘要";全局规范(00)对任何游戏改动都生效。
后续路线见 [roadmap.md](roadmap.md)。
## 设计规范(施工依据)
| 文档 | 内容 | 对应用户反馈 |
|---|---|---|
| [00-global-ux.md](00-global-ux.md) | 全局交互规范(终审修订版):设置系统、haptics 震动语言(10 震型)、旁白整句化、手势约定与模态菜单、用户系统 API | ⑥⑦ + 全局需求 |
| [01-escape-redesign.md](01-escape-redesign.md) | 《暗室》重设计:引导式开场、三级卡关提示、线索音可重听 | ⑤ |
| [02-simon-redesign.md](02-simon-redesign.md) | 《音序回响》重设计:去 BGM、旁白整句化、交互式教学、复活机制(含双击语义裁定) | ⑥⑦ |
| [03-rhythm-rebuild.md](03-rhythm-rebuild.md) + [03-rhythm-voice-ids.md](03-rhythm-voice-ids.md) | 《节拍行者》重构施工稿:跟随领奏玩法、三首完整谱面、判定契约、31 句台词表 | ⑧ |
| [04-space-rebuild.md](04-space-rebuild.md) + [04-space-voice-ids.md](04-space-voice-ids.md) | 《盲航》重构:交互式演练、同向闪避、60 秒保护期、52 句台词表 | ⑨ |
| [05-rpg.md](05-rpg.md) + [05-rpg-chapter1.md](05-rpg-chapter1.md) + [05-rpg-ch1-voice-ids.md](05-rpg-ch1-voice-ids.md) + [05-rpg-addendum.md](05-rpg-addendum.md) | 新 RPG《剑与声》:总设计 + 第一章施工稿(场景图/战斗状态机/数值/存档/资产 prompt)+ 120 句台词表 + 增补(战斗震动配合、无声前摇协议、层叠规则) | ⑩ |
## 审查档案(各轮落实度与裁定记录)
| 文档 | 内容 |
|---|---|
| [round1-review.md](round1-review.md) | R1 基建审查:6 处出入裁定(haptics 命名、voice 平铺回退、`_settings` 保留键等) |
| [round2-review.md](round2-review.md) | R2 暗室/音序审查:落实度 ~95%;裁决① simon 双击上下文判定、裁决② 全球模态菜单约定 |
| [round3-4-review.md](round3-4-review.md) | R3 节拍/R4 盲航审查:4 项裁定(结算末位语音、menu_main 文案、space 难度不回调、震动总量批准)+ 附裁 A(玩法手势优先的菜单折中模式) |
| [round5-notes.md](round5-notes.md) | 给 RPG 开发 agent 的返工注意级提示(看门狗/会话令牌/末位语境/震动去抖/层叠实现) |
| [roadmap.md](roadmap.md) | 后续路线:RPG 二三章扩展、storyline 打磨(菜单统一/knowTrap 死标记/角色音色)、真机听测 checklist、商业化前置 |
## 5 轮迭代计划总表(已执行完毕)
| 轮次 | 主题 | 结果 |
|---|---|---|
| **R1** | 基建 + 设计 | 设计文档集;引擎 settings/haptics/voice 目录;大厅设置+登录;服务器 API;删 reflex |
| **R2** | 暗室 + 音序重设计 | 两游戏重写落地,语音 142 id ×2 性别;审查 round2-review |
| **R3** | 节拍重构 | 跟随领奏落地(3 曲目解锁制);审查 round3-4-review §1 |
| **R4** | 盲航重构 + RPG 资产 | 盲航重写落地(演练/保护期/同向);RPG 台词表与资产清单交付;审查 round3-4-review §2 |
| **R5** | RPG 开发 + 总打磨 | 进行中;开发提示 round5-notes;后续路线 roadmap |
横切约束(每轮验收都要查):
- 旁白:整句预生成、同角色 speaker+instruct 恒定、female/male 双目录、ogg+m4a 双格式(00 §3)。
- 反馈:操作反馈一律震动(`engine/haptics.js` 10 预定义震型),iOS 静默降级不回退提示音(00 §2)。
- 手势:默认映射 + 豁免表 + 模态菜单约定(00 §5),任何偏离必须在开场交互教学中讲清。
- 教学:禁止"连续旁白讲完就开考";一切教学交互式,做错不惩罚、当场重教。
- 设置:游戏内只读,改动只在大厅页(00 §1)。
- 进度:data 自由格式 + 必带 `summary`,localStorage 兜底 + 服务器同步,`_settings` 为保留键(00 §4)。
+114
View File
@@ -0,0 +1,114 @@
# SoundGame 后续路线图(策划,最终轮)
范围:R5(RPG 收尾)之后的事。分四块:RPG 内容扩展、storyline 打磨、真机听测、商业化前置。
设计基准文档索引见 [README.md](README.md);审查结论见 round1/round2/round3-4-review。
---
## 1. RPG《剑与声》第二/三章扩展计划
第一章模式已定型(场景图 JSON + 战斗状态机 + 台词表三件套),二三章是**内容灌入**而非新开发。
### 1.1 第二章《腐沼泽》(7 节点)
| 项 | 内容 |
|---|---|
| 场景 | 沼口村(商店:青钢剑 300/铁鳞甲 250)→ 腐叶径 → 浅滩(岔路:鳄巢宝箱)→ 瘴气林 → 蛊师祭坛(NPC 支线)→ 祭主殿门 → Boss 腐沼祭主 |
| 新机制 | 毒状态(中毒后每回合 -3 HP,金疮药可解;中毒播报走整句分档);蛊师敌方先攻(B_START → ENEMY_TELEGRAPH,状态机已支持) |
| 敌人 | 沼泽鳄(HP30 ATK11 DEF2)、蛊师(HP22 ATK12 DEF1,先攻)+ 一章怪复用降档 |
| Boss | 腐沼祭主 HP70 ATK14 DEF3;半血二阶段召唤 2 毒蛛(连战队列插入,机制已有) |
| 解锁 | 一章 Boss 胜后 `chapters.2 = open`;章首旁白承接 `evt_ch1_end` 字条 |
### 1.2 第三章《无瞳峰》(7 节点)
| 项 | 内容 |
|---|---|
| 场景 | 山道驿站(商店:听风剑 600,格挡窗口 1.0→1.3s)→ 风啸崖 → 断桥(岔路)→ 剑冢(机关战斗)→ 影廊(影卫首战,无声前摇教学)→ 峰顶 → Boss 白无瞳 |
| 新机制 | 无声前摇(震动协议见 05-rpg-addendum §2,影卫铺垫 → 白无瞳三阶段完全无声);iOS 兜底=防御指令 |
| 敌人 | 剑傀(HP38 ATK14 DEF3)、影卫(HP30 ATK16 DEF2,无声) |
| Boss | 白无瞳 HP110 ATK18 DEF5,三阶段(≤50% / ≤25% 转换,三阶段进场播 `npc_boss3_warn`) |
| 完结 | 终章结算用专属完结句(不复用"前往下一章",round5-notes §3 教训) |
### 1.3 排期与资产
- 台词表:ch2/ch3 各一份(沿用 05-rpg-ch1-voice-ids.md 格式,每章约 60-80 句;最小集见 05-rpg-addendum §5)。策划补发 → 音频总监录制(各 1 轮)。
- BGM:复用现有 4 首 + 每章新增场景 amb(`amb_swamp*`、`amb_peak*`,05-rpg-chapter1 §7.2 已含)。
- 建议节奏:内容扩展 A 轮(ch2 台词+灌入)→ B 轮(ch3 台词+灌入+终章完结句)→ 与真机听测并行。
## 2. storyline《声命线》打磨项(一直没重构,逐项跟进)
| # | 事项 | 现状 | 动作 |
|---|---|---|---|
| 1 | 菜单约定统一 | 现版 down=菜单(左滑重开/右滑单击返回/上滑帮助),是裁决②之前的私有约定 | 统一到 00 §5.2:下滑呼出 → 单击循环 → 双击确认 → 上滑帮助;菜单句 `voice/menu` 需按新口径重录(×2 性别) |
| 2 | knowTrap 死标记修复 | `game.js:229` 只写不读:n09 右滑获得"北区陷阱情报"后无任何分支消费它 | 先读 `docs/games/storyline.md` 剧情树定接入点:在北区相关节点读取 knowTrap,解锁"绕开陷阱"分支或改变死亡结局判定;预计需补录 1-2 句分支台词(角色音:林晚 Serena / 旁白双性别)。若剧情树无合适节点,降级方案=删除该标记与台词里的伏笔句 |
| 3 | 角色音色在双性别目录的处理 | R1 裁定:旁白双性别(male=Uncle_Fu/female=Vivian),角色台词单音色平铺靠引擎回退(round1-report §3),资产侧未复制 | 终案确认:**维持平铺回退**(引擎已支持,零成本);把"平铺回退是正式机制"写进 web-engine-api 的目录约定(开发顺手改)。试听确认点:female 旁白(Vivian)与小满/林晨/广播(同 Vivian 不同 instruct)的区分度 |
| 4 | 提示音替换排查 | `sfx/ui_confirm`、`sfx/ui_menu` 仍在选择确认/菜单处使用 | 按反馈⑦替换:选择确认删 ui_confirm(引擎内置震动已覆盖),菜单打开删 ui_menu;剧情氛围 sfx(water/whistle/wind 等内容音)保留 |
| 5 | 进度 summary | storyline 无存档(单局 5-7 分钟,一次性剧情) | 轻量方案:记录"已达成结局 X/4",summary="已见 2/4 结局";结局解锁存 localStorage+服务器,大厅卡片有内容可显示 |
| 6 | 双性别 QC | 36 id ×2 已生成 | 抽听 female 版旁白与角色音的衔接(旁白切角色时音色跳跃是否出戏) |
## 3. 各游戏存量打磨小项(来自各轮审查遗留)
- rhythm:结算右滑末位修正 + 补录 `no_more_song`;震动 200ms 去抖(round3-4-review §3 裁定①④)。
- escape:PLAY 中双击兜底链末尾加"再双击一次回大厅"二级入口(补录 `menu_hint` ×2 性别,round2-review §5.1)。
- simon:`voice/wrong` 冗余资产清理或启用(round2-review §5.2)。
- 大厅:设置变更试听句(00 §1.2,一直未做)。
- 全合集:平铺旧 voice 文件清理(storyline/rhythm/space 的残留,音频侧执行)。
## 4. 真机听测 checklist(用户拿手机逐项打钩)
通用前置:安卓 Chrome + iOS Safari 各过一遍;戴有线耳机测一轮、蓝牙耳机测一轮(延迟差异);iOS 项注意"无震动"是预期行为。
### 声命线 storyline
- [ ] 旁白切角色音时不出戏(重点:female 旁白 vs 小满/广播的 Vivian 家族)
- [ ] 8 个抉择点左/右滑方向与选项播报一致
- [ ] 4 结局至少打通 2 条,结局后单击重开正常
- [ ] (打磨后)菜单单击循环/双击确认,无误触发重开
### 暗室 escape
- [ ] 引导 G0-G7 全流程不卡(含故意做错 3 次触发直白重述)
- [ ] 9 个房间听环境音能区分储物间/铁柜/电箱/大门(闭眼走一遍)
- [ ] 卡关提示计时手感:60s/120s/180s 出现时机不烦人(真机挂一次 3 分钟)
- [ ] 单击重听环境音:amb 与描述语音的 800ms 间隔自然
- [ ] 用时 4 段播报与实际耗时匹配
### 音序回响 simon
- [ ] 无 BGM 的静默不尴尬(开场 0.5s + 轮次间隙)
- [ ] 播放序列的 beat 轻震与音色对齐(安卓机能感到节拍锚点)
- [ ] 双击主动重听:期望输入是 doubletap 时按答题处理(裁定①场景实测)
- [ ] 第 10 轮后的速度(420ms 下限)可跟
- [ ] 成绩分档句与实际轮数匹配
### 节拍行者 rhythm
- [ ] Perfect ±120ms 窗口:有线耳机能稳定 Perfect,蓝牙耳机 Good 为主(不挫败)
- [ ] tap 补偿:底鼓音(1/3 拍)判定不冤
- [ ] 下滑菜单折中:演奏中误开菜单 / 想开开不了 各试 5 次
- [ ] 菜单暂停后 BGM 断点续播节拍不漂
- [ ] 三首 BGM 与乐器音调性协和、响度平衡(乐器音不被 BGM 淹没)
### 盲航 space
- [ ] 同向闪避直觉性:不测老玩家,找一个没玩过的人 30 秒内能否理解
- [ ] 演练 D1-D4 每步"语音先、whoosh 后"不叠放
- [ ] 保护期 60s 内被撞不掉血且归因句听清
- [ ] 满级段(存活 90s+)密度:过密则把 spawn 下限 1.2→1.35(round3-4 裁定③旋钮)
- [ ] 警报两档渐强 + danger 震动的时间感(撞击前 1s/0.5s)
### 剑与声 rpg(R5 完成后)
- [ ] 格挡窗口:8 种敌人前摇音都能听出并双击成功(重点:头目重甲 vs 野狼低吼的区分)
- [ ] 无声敌人(若 ch3 已做):danger+heartbeat 震动节奏可格挡;iOS 上确认防御指令兜底
- [ ] 战斗 BGM 与场景 amb 切换不突兀;bgm=0 设置下 amb 照播
- [ ] 商店购买/缺钱/已购播报语境正确
- [ ] 存档继续:杀进程重进,summary 与落点正确
## 5. 商业化前要做的事
1. **Stable Audio 3 许可确认**:核对当前账号/模型的商用授权条款(训练数据与生成物的商用权利、署名要求);所有 SA3 生成 sfx 的资产清单(id → 生成日期 → prompt)落盘存档(`docs/audio/asset-manifest.md` 扩展)。
2. **Mureka 曲目版权落盘**:确认 API 生成音乐的商用授权范围与归属条款;把 `tools/mureka_log.md` 整理为正式曲目登记表(曲目 → 生成时间 → 授权凭证/订单号);离线保存原始生成文件与凭证截图。
3. **Qwen3-TTS 许可**:核对模型协议(Apache 2.0 系/通义许可)对生成语音商用的要求;在 About/致谢页列模型名。
4. **字体许可**:封面生成用 NotoSansSC(OFL),保留 OFL 许可证副本,允许商用但需随附声明。
5. **隐私政策**:用户系统当前是局域网玩具级(PIN 明文、token 不过期——web/server/README 已自声明)。公网化前必须:PIN 加盐哈希、token 过期机制、HTTPS、最小化收集声明、账号删除入口;写隐私政策页(收集:用户名+游戏进度;不收集:设备信息/位置)。
6. **付费墙设计预留**(如需):进度 API 加 `entitlements` 字段的位置先留好;不要在现有 data 自由格式里埋付费标记。
7. **合规扫尾**:无年龄分级素材问题(武侠战斗无血腥音效——atk_hit prompt 已注明 not gory,保持);商店/内购若上 iOS 需走 IAP 不能用网页支付(届时再议)。
## 6. 建议顺序
1. R5 收尾(RPG 第一章上线)→ 2. §3 存量打磨小项(半天级)→ 3. 真机听测 §4(用户执行,反馈回来后定参数旋钮)→ 4. storyline §2(与听测并行)→ 5. RPG ch2/ch3 §1 → 6. 商业化 §5(决定公网化时再启动)。
+68
View File
@@ -0,0 +1,68 @@
# R1 落地一致性审查报告(策划,R2 轮)
审查对象:`docs/web-engine-api.md`(R1 变更)、`docs/audio/round1-report.md`、
`web/index.html`(大厅新 UI)、`web/engine/{settings,haptics,audio}.js`、`web/server/server.js`。
对照基准:`docs/design/00-global-ux.md`(设计规范初稿)。
**总体结论:R1 落地质量高,核心语义全部符合设计。发现 6 处出入,均已裁定"以谁为准";
00-global-ux.md 已按本报告完成修订(R2 修订版),后续各轮以修订版 00 文档为唯一基准。**
---
## 出入清单与裁定
### 1. 震动模块命名与震型表 —— 以实现为准
- 设计:`engine/vibrate.js`,常量 `VIB.CONFIRM/ERROR/...`(大写),9 种震型。
- 实现:`engine/haptics.js`,`HAPTIC_PATTERNS` 小写命名(`confirm/error/success/warning/swipe/heartbeat`),6 种预定义 + 自定义数组调用;数值微调(error=[60,40,60] vs 设计 [40,60,40];warning=[30,50,30,50,30] vs [30,40,...])。
- **裁定:以实现为准。** 数值差异在感知阈内。设计侧 PICKUP/HIT/BEAT/DANGER 四种语义震型引擎未预定义——**R3 起游戏层先用自定义数组([15,40,15]/[80]/[8]/[20,50,30,50,40]),建议引擎 R3 补为预定义**(请开发排期)。
- 00 文档 §2 已修订。
### 2. gesture.js 内置基础震动 —— 以实现为准,设计规范随之修订
- 实现:滑动识别成功自动 `swipe`、tap/doubletap 自动 `confirm`(web-engine-api.md:222-224 已写明"游戏层不要重复调用,否则双重震动")。
- 设计初稿要求游戏层对"滑动生效/确认"调 SWIPE_OK/CONFIRM——按实现会导致双重震动。
- **裁定:以实现为准。** 震动分工:手势识别反馈=引擎内置;语义事件(error/success/warning/PICKUP/HIT/BEAT/DANGER)=游戏层。00 文档 §2.3 已修订,01/02/04/05 各篇中"单击确认给 CONFIRM 震动""滑动给 SWIPE_OK"的表述按此映射理解,不逐一改文。
### 3. voice 资产回退逻辑 —— 以实现为准(比设计更优)
- 设计:`voice/<gender>/` 缺失时回退另一性别。
- 实现:`voice/<gender>/` → 回退**平铺** `voice/<id>`(web-engine-api.md:145-148),不做性别回退。
- **裁定:以实现为准。** 音频总监已保证两性别 id 集合一致(round1-report §1),性别回退无必要;平铺回退恰好解决 storyline 角色台词(单音色)的加载问题,设计更简洁。00 文档 §1.1 已修订。
- 附带确认:voice 缓存键带性别前缀(audio.js:239),运行时切换性别不会串缓存——处理正确。
### 4. 设置值格式与服务器同步形式 —— 以实现为准
- 设计:speed/bgm 字符串枚举("normal"/"fast"/"veryfast");独立 `/api/settings` 端点。
- 实现:数值(1.0/1.35/1.7、1.0/0.3/0),`settings.js` 非法值逐项丢弃;设置走进度接口保留键 **`game='_settings'`**(index.html:288)。
- **裁定:以实现为准。** `_settings` 为全局保留键,任何游戏存档不得占用(00 §4.3 已写明)。数值格式更利于引擎直接乘算。
- speed 生效时机为"下一句起"(audio.js:343,创建 source 时读值)——可接受,文档已注明;正在播放的长句不变速不算问题(句子均 ≤25 字短句)。
### 5. 进度 API 形态与存档信封 —— 以实现为准
- 设计:`GET/PUT /api/progress/<game>` 路径段 + 统一信封 `{schema, game, updatedAt, data}`;token 存 `soundgame.token`。
- 实现:`GET /api/progress?game=xxx`、`PUT {game, data}`;data 自由格式(服务器不校验);token 存 `soundgame.auth`(含 username);大厅卡片约定读 **`data.summary`** 字符串。
- **裁定:以实现为准。** 游戏侧 `data` 内仍建议自放 `schema`/`updatedAt` 字段(便于迁移),`summary` 为**必须**(RPG 存档结构已按此定义,05-rpg-chapter1.md §6)。00 文档 §4 已修订。
### 6. 各游戏默认音色性别 —— 以实现为准(全局统一默认 female)
- 音频总监建议(round1-report §5.3):按游戏默认原音色性别(storyline/escape=male,其余=female)。
- 实现:全局单一设置项,默认 female,不分游戏。
- **裁定:以实现为准。** 全局单项更符合"设置系统"的用户心智(用户选一次全合集生效);各游戏原音色差异已由双性别资产覆盖,用户可自切。此条无需改动,仅记录在案。
## 需要转达的事项
1. **给引擎开发**:R3 请在 `haptics.js` 补 4 个预定义震型:`pickup=[15,40,15]`、`hit=[80]`、`beat=[8]`、`danger=[20,50,30,50,40]`(R3 节奏游戏与 R5 RPG 都要用;未补前游戏层传自定义数组,不阻塞)。
2. **给音频总监**:
- R3 录制依据 `docs/design/03-rhythm-voice-ids.md`(31 句 ×2 性别,rhythm 旁白 female=Vivian/male=Uncle_Fu 沿用 R1 分配;grade_*/restart 为重录覆盖)。
- R4 录制依据 `docs/design/05-rpg-ch1-voice-ids.md`(旁白 104×2 + NPC 16 平铺单音色;黑风寨主与村长同为 Uncle_Fu 不同 instruct,请优先试听这两条确认区分度,若混淆寨主改 Dylan 阴狠 instruct)。
- Mureka/SA3 需求:`docs/design/05-rpg-chapter1.md` §7(5 BGM + 35 sfx,含英文 prompt 草稿)与 03-rhythm-rebuild.md §8.4(3 首 BGM 重做 + 7 个乐器音)。
3. **给大厅/UI 开发**:设置变更后的试听句(00 §1.2)列为 R3 打磨项,非阻塞。
4. **资产清理**:平铺旧 voice 文件待 R2 各游戏切换验证完成后由音频侧清理(round1-report §5.4 已排)。
## 无出入确认项(抽查通过)
- 音频解锁流程、三层架构、onComplete 打断语义未变,设计文档依赖的契约完好。
- bgm=0 时 `_playBgm` 短路不加载——与设计"不是静音播放"一致。
- simon BGM 资产已删除(round1-report §5.2),与 02 文档一致;game.js 的 bgm 引用移除属 R2 开发任务。
- 服务器安全声明(局域网玩具级、PIN 明文)已在 web/server/README.md 显著标注,符合家庭内网定位,设计无异议。
+89
View File
@@ -0,0 +1,89 @@
# R2 落地一致性审查报告(策划,R3 轮)
审查对象:`web/escape/game.js`、`web/simon/game.js`(R2 重写版)、`docs/audio/round2-voice-ids.md`、`docs/audio/round2-report.md`、`web/engine/haptics.js`(R2 增补)。
对照基准:`docs/design/01-escape-redesign.md`、`02-simon-redesign.md`、`00-global-ux.md`(R2 修订版)。
**总体结论:两款游戏落实度约 95%,核心设计(引导式开场、三级提示、可重听环境音 / 去 BGM、整句化、交互教学、复活机制)全部落地且工程质量高于设计预期(语音链看门狗、会话令牌、进度 summary 同步)。发现 5 处偏差,全部裁定;两个开发疑问的裁决见 §3。**
---
## 1. 《暗室》落实度核查(对照 01 文档)
| 设计条目 | 落实情况 | 结论 |
|---|---|---|
| G0-G7 引导式脚本(带路到储物间、教移动/撞墙/检查/双击) | 完整实现(g0-g7 + `_advance` 步骤机 + 20s 看门狗防卡死) | ✅ |
| 引导期做错纠正(错 3 次直白重述 + warning 震动)、下滑跳过 | 完整实现(guide_wrong_*/guide_blunt_*/guide_offpath/guide_skip) | ✅ |
| 二周目开场询问(上滑重听引导/单击直接开始) | 实现(REOPEN + reopen_prompt + guideDone localStorage) | ✅ |
| 卡关三级提示 60/120/180s、L3 每 60s 重复、按坐标算路 nav_* | 完整实现(5s 轮询、阶段×级别 12 句 + stuck_ask 前置 + nav 4 句) | ✅ |
| 环境音可重听(单击=重播环境音+描述)、每次进房都播 | 实现(`_inspect` 先播 amb、800ms 后播描述;`_playAmbient` 每次进房播) | ✅ |
| 环境音 3→6 种 + 大门通电切 amb_gate_live | 实现(KEY_ROOMS 4 专属 + 2 空房轮换 + gate_live) | ✅ |
| 撞墙每次播 wall(取消一次性限制) | 实现(含引导 G2) | ✅ |
| 用时 4 段整句替代数字拼接 | 实现(time_band1..4,区间与语音清单一致) | ✅ |
| ui_deny 退役改 error 震动 | 实现 | ✅ |
| 双击兜底:背包 + L2 级明示 | **偏差 1**:关键房间内播 `use_fail` 替代背包播报(非关键房间仍播背包 inv_* + stuck_*_2) | 以实现为准(更合语境),设计文档视为已修订 |
| 菜单并入双击兜底链(00 豁免表原表述) | **偏差 2**:PLAY 阶段无菜单入口;菜单仅在 ENDING 下滑呼出 | 以实现为准(PLAY 中帮助需求已被双击兜底 + 自动卡关提示覆盖),00 豁免表已修订 |
| G2"滑两下碰碰墙(撞墙也计数)" | **偏差 3**:引导期位置固定,两次滑动必然撞墙 | 以实现为准(教学目标就是撞墙反馈,更可靠) |
另确认:震动语义正确(撞墙 error、拾取 pickup、机关 success、提示 warning;gesture.js 内置识别反馈未重复调用);进度存档带 summary 同步服务器(超出设计要求,符合 00 §4.3)。
## 2. 《音序回响》落实度核查(对照 02 文档)
| 设计条目 | 落实情况 | 结论 |
|---|---|---|
| 去 BGM(不加载不播放) | 实现(prewarm 无 bgm,开场静默 0.5s 进教学) | ✅ |
| 轮次整句 round_01..20 + round_more 兜底 | 实现 | ✅ |
| 成绩 6 段整句(0/≤3/≤6/≤9/10-14/≥15) | 实现(score_band1..6 区间一致) | ✅ |
| 交互式教学 T0-T7(教一个练一个、错 3 次直白版、下滑跳过) | 完整实现(t_wrong_*/t_blunt_* 六手势各一 + t_try 等待 + 跳过兜底) | ✅ |
| 播放序列 BEAT 节拍锚点 + 播完 400ms confirm "轮到你了" | 实现(第 1 轮同播 your_turn,之后仅震动) | ✅ |
| 复活机制:初始 1 次、每 4 轮 +1 上限 2、答错自动消耗重播同一序列、双击主动重听 | 完整实现(chances_out/retry_use/relisten/chance_plus 语音齐全) | ✅ |
| 速度曲线 max(420, 700-(r-1)*25) | 实现 | ✅ |
| 序列约束(第 1 轮两音不同 / 前 5 轮禁三连 / 全局禁四连) | 实现(`_violates`,20 次重试抽样) | ✅ |
| 答对每 4 轮才播"答对了",其余仅 success 震动 | 实现 | ✅ |
| 结算:error+hit 震动、sfx/gameover 保留、best 持久化+服务器 summary | 实现 | ✅ |
| 播放/播报阶段手势静默忽略 | 实现(phase 守卫 default 分支) | ✅ |
| 双击语义(输入阶段=主动重听) | **偏差 4**:与序列含 doubletap 冲突,开发按上下文判定(见 §3 裁决①) | 裁决采纳 |
另确认:`wrong` 语音在语音清单中标注"可选",实现未引用(retry_use 已含"错了")——不算缺失,清单口径一致。
## 3. 两个开发疑问的裁决
### 裁决①:simon 双击语义冲突 —— **采纳实现的上下文判定**
实现的规则(game.js:294-297):输入阶段,若当前期望输入恰为 doubletap,双击=答题;否则双击=主动重听(耗机会;机会为 0 时仅提示 chances_out,不判错)。
裁决理由:
1. **序列必须永远可答**。任何让"双击=重听"优先的方案都会使含 doubletap 的序列无法复现,破坏玩法完整性——答题优先是唯一可接受的解释。
2. **边缘情况不自锁**。玩家想重听而当前期望恰是 doubletap 时:双击被当答题——答对则推进(玩家本来就会背到这一位,损失为零);答错则自动消耗机会重听,殊途同归。不存在"想重听却永远做不到"的死锁。
3. **宽容性正确**:机会为 0 时双击非期望手势不判答错、只提示——避免了"想重听却被判错直接结算"的最坏体验。
补充规定(写入 02 文档):重听播报期间 phase 切 roundend 暂停接收输入(实现已做),防止重听语音播完前的手势被吞。
### 裁决②:菜单手势 —— **采纳进 00-global-ux.md,作为全球菜单约定(RPG 豁免)**
开发自拟方案:菜单内 **单击=循环切换选项(播选项名)/ 双击=确认执行 / 上滑=帮助**,下滑或"返回"项关闭。
裁决:**采纳**。理由:
1. 解决了全局约定"单击=确认"与结算页"单击=重开"的冲突——结算页单击已被占用,菜单内单击若仍是确认,玩家想重开时会误触发菜单项。单击循环 + 双击确认把"浏览"与"执行"分离,误操作成本归零。
2. 两款游戏已一致落地,交互范式统一(菜单项 3 个:重新开始/回大厅/返回游戏,菜单打开时先播 menu_open + 第一项)。
3. 单击循环有 ~300ms 引擎延迟,但菜单非即时场景,可接受;双击确认即时,关键的"执行"动作不拖。
配套修订(00 文档 §5.1 已更新):
- 新全球菜单约定:**下滑呼出(模态)→ 单击循环切换(播选项名)→ 双击确认执行 → 上滑=帮助/规则 → 下滑或"返回游戏"项关闭**。菜单打开时暂停计时类玩法。
- **RPG《剑与声》豁免**:探索态菜单保持"左右滑切换 + 单击确认"——RPG 的探索本身就是这个范式(选项列表即游戏本体),菜单与探索同构反而少学一套;战斗态不设菜单。
- **storyline**:R3 打磨轮统一到本约定(现状未审,列入 R3 检查项)。
- **rhythm / space**:R3/R4 施工直接按新约定(03/04 文档中"左右滑切曲目/菜单"的表述以此为准——选曲界面不是模态菜单,仍用左右滑+单击;模态菜单用新约定)。
## 4. 偏差 5:haptics.js 震型增补(引擎侧,R2 已做)
- 引擎已补 `pickup=[15]`、`hit=[80]`、`beat=[8]`、`danger=[25,40,25,40,25]`——正是 round1-review 转达事项①,但 pickup/danger 数值与设计不同(设计 [15,40,15] / [20,50,30,50,40])。
- **裁定:以实现为准**(pickup 单震更轻、不与 error 混淆;danger 节奏更密更接近"逼近"感)。00 文档 §2.2 已更新为实现的 10 种震型全表。
## 5. 遗留问题(转达开发/音频)
1. **escape PLAY 阶段无法回大厅**:结算前想退出只能刷新页面。建议 R3 打磨:PLAY 中双击兜底链末尾追加"要回大厅?再双击一次"二级入口(语音 id `menu_hint` 需补录 1 句 ×2 性别)。非阻塞。
2. **simon `voice/wrong` 已生成但代码未引用**:保留无妨(资产冗余 1 句),或 R3 清理清单标注。
3. **storyline 菜单约定统一**:列入 R3 检查项(裁决②配套)。
4. 两游戏真机听测仍未做(沿用 R1 遗留):escape 提示计时阈值(60/120/180s)与 simon 教学节奏需真机验证手感。
+97
View File
@@ -0,0 +1,97 @@
# R3/R4 落地一致性审查报告(策划,最终轮)
审查对象:`web/rhythm/game.js`(R3 重构)、`web/space/game.js`(R4 重构)。
对照基准:`docs/design/03-rhythm-rebuild.md`(+03-rhythm-voice-ids.md)、`docs/design/04-space-rebuild.md`(+04-space-voice-ids.md)、`00-global-ux.md`。
**总体结论:两款游戏落实度均约 95%+。谱面三首逐音直转零偏差;盲航的演练/保护期/分批引入全部落地且健壮性超设计(`_rearmRunTimers` 断点恢复)。开发上报的 4 个问题裁定见 §3——无推翻性意见,仅 1 处需要一行代码修正(问题①)。**
---
## 1. 《节拍行者》落实度核查(对照 03 文档)
| 设计条目 | 落实 | 结论 |
|---|---|---|
| 谱面三首(90/105/120 BPM,逐乐句拍点) | 逐音核对一致(含 restAfter 休止:曲一前 4 句、曲二前 2 句、曲三不休) | ✅ 零偏差 |
| 判定契约 PERFECT 0.12 / GOOD 0.25 / TAP_DELAY 0.30 / LOOKAHEAD 0.6 / 泵 40ms | 常量一致,Miss 放宽逻辑保留 | ✅ |
| tap 限 1/3 拍、doubletap 曲三前不上反拍 | 谱面核对无违规 | ✅ |
| 教学 T1-T3(做错纠正分"手势错/早/晚/没出手"、2 次失败跳过) | 实现且纠正优先级正确(手势错 > 过早 > 过晚 > 未出手) | ✅ |
| 首次 welcome+BGM 第 6s 起;二周目 welcome_back → 选曲 | 实现(tutorialDone 分支) | ✅ |
| 选曲界面(非模态:左右滑+单击,未解锁播 song_locked) | 实现 | ✅ |
| 计分/评级 S≥0.95 A≥0.85 B≥0.70、解锁规则 B 级解锁下一首 | 实现 | ✅ |
| Miss=不播乐器音(错过=没出声)+ error 震动;Perfect 叠 accent | 实现 | ✅ |
| mid_song 过半播报 | 实现(`_timed` 定时动作) | ✅ |
| 模态菜单(裁决②约定)+ 计时玩法暂停 | 实现且超出设计:BGM 断点续播 + 全部绝对时刻平移暂停时长,节拍网格不漂 | ✅ 超设计 |
| **下滑=菜单 vs 下滑=低音 tom 冲突** | **开发自拟折中**:仅当无悬置/临近(1s 内)的 down 音符时下滑才呼出菜单(`_downNotePending`) | **偏差 1:采纳**(见 §3 附裁 A) |
| 结算单击=再来一遍/右滑=下一首/下滑=回大厅 | 实现,但曲目三右滑播 song_locked 语义错位 | **问题①,见 §3 裁定①** |
| 进度 data 带 summary 同步服务器 | 实现(格式"曲目2 评级A / 已解锁 2/3 首",与设计"《追风》A级 · 已解锁2/3首"措辞略异) | 以实现为准(summary 自由文本) |
| 乐器音 6 + accent 1、BGM 3 首(track90/105/120) | 资产 id 与实现一致 | ✅ |
## 2. 《盲航》落实度核查(对照 04 文档)
| 设计条目 | 落实 | 结论 |
|---|---|---|
| 同向闪避(`s.dir === ob.dir` 判定,窗口/宽限结构不变) | 实现 | ✅ |
| D0-D5 演练:语音先播完再 spawn(不叠放)、做错当场重教、2 次失败 moveon、下滑跳过 | 实现(跳过仅限无演练陨石悬置时,防误触) | ✅ 超设计 |
| 二周目 second_run_ask(上滑重听/单击出发) | 实现 | ✅ |
| 保护期 60s:逼近 4.0s/刷怪 4.0s 恒定、撞击不掉血(训练护盾+protect_<dir> 归因)、protect_end 宣告 | 实现(含菜单/分批引入打断后 `_rearmRunTimers` 补播,过期立即补播) | ✅ 超设计 |
| 单击/双击 20s/45s 分批引入(暂停刷怪、8s 不等待) | 实现(feature 阶段完整,引入前对应手势锁定不可用) | ✅ |
| 死亡即教学:hit_<dir> + 第 2 次 hit_last | 实现 | ✅ |
| 里程碑 10/25/50/100(ms_* 整句) | 实现(confirm 震动,设计未指定震型,合理) | ✅ |
| 结算 time_/dodge_ 各 4 档 + new_record + restart_hint | 实现(区间与设计一致:<30/30-60/60-120/≥120;<5/5-14/15-49/≥50) | ✅ |
| 菜单新约定 + paused 播报 + menu_help 项 | 实现 | ✅ |
| **下滑=闪避 vs 下滑=菜单冲突** | **开发自拟折中**:有下方陨石悬置时下滑永远是闪避,否则呼出菜单 | **偏差 2:采纳**(同附裁 A) |
| 警报两档渐强 + danger 震动同步 | 实现 | ✅ |
| 护盾:格挡 success 震动、shield_ready 充能播报 + pickup | 实现 | ✅ |
| 难度参数(保护期后) | **实现 3.2→1.2s 刷怪 / 2.8→1.8s 逼近,比设计 3.2→1.4 / 3.2→2.0 更陡** | **问题③,见 §3 裁定③** |
| 进度 summary 同步 | 实现("最长存活 M:SS / 躲避 N 颗") | ✅ |
## 3. 开发上报问题的裁定
### 裁定①:rhythm 结算页曲目三右滑播 song_locked —— **确认是语义错位,一行修正 + 低优先补录**
- 设计(03 §8.7):结算"右滑=下一首(若已解锁)"。
- 实现(game.js:429-431):曲目三已是最后一首,`songIdx+1 < unlocked` 恒 false → 播 `song_locked`("这首还没解锁。上一首拿到 B 级以上就能解锁。")——玩家会误以为存在曲目四,**语义错误**。
- **裁定(定论)**:曲目三右滑**不得**播 `song_locked`。修正方案(一行代码,零新资产):曲目三右滑时改播 `restart`("单击再来一遍,右滑下一首,下滑回大厅"——虽提到"右滑下一首"仍不完美,但不误导)。**同时**把 `no_more_song`("这已经是最后一首了。单击再来一遍,下滑回大厅。"×2 性别)列入打磨补录清单,录好后替换临时方案。同类问题通查:所有"下一段/下一首"导航语音都要做末位语境检查(教训写入 round5-notes)。
### 裁定②:space `menu_main` 重录文案"单击切换,双击确认" —— **确认 OK,以重录版为准**
- 我的 04-space-voice-ids.md 原文案是"菜单。左右滑选择,单击确认。"——那是 round2 裁决②**之前**的旧菜单约定,是设计文档过时,不是音频错。
- 重录版与全球菜单约定(00 §5.2)一致,**批准**。已对 03/04 两份台词表做勘误注记。rhythm 的 `menu_main` 同理以重录版为准。
- 附带要求:后续所有台词表在"菜单/系统"组的文案以 00 §5.2 最新约定为唯一来源(RPG ch1 台词表已核对:sys_menu "菜单。左右滑选择,单击确认。"——**RPG 属豁免游戏(探索态菜单=左右滑+单击),该句正确,无需改**。)
### 裁定③:space 难度 3.2→1.2s(比设计 1.4s 陡)—— **定论:不回调,以实现为准**
- 设计(04 §3.2):保护期后刷怪 3.2→1.4s、逼近 3.2→2.0s(我在旧版基础上各抬 0.4s/0.2s)。
- 实现:沿用旧版 3.2→1.2s / 2.8→1.8s,更陡。
- **定论:接受实现,不回调。** 理由:
1. 反馈⑨"没搞懂就死"的病灶在前 60 秒,已由演练+保护期+同向闪避三道防线解决;满级难度影响的是 90 秒后的高手区,不是新手区。
2. 满级 1.2s/1.8s 是"传奇段"的挑战上限,老玩家留存需要它;120s 爬升过程本身是平缓的。
3. 真机听测兜底:把"满级段(存活 90s+)密度手感"列入真机 checklist(roadmap §4);若普遍反映过密,把 spawn 下限 1.2→1.35s 作为唯一旋钮(approach 不动)。
- 04 文档参数表以此裁定为最终值(文档不再改,本审查报告即修订记录)。
### 裁定④:rhythm 震动总量 —— **批准,不算过多;两条收紧项**
对照 00 §2 逐条审:
| 震动点 | 合规性 |
|---|---|
| 领奏每音 `beat`(8ms) | ✅ 00 §2.4.2 明确豁免:BEAT 是节拍信息通道,不是反馈。曲三最快处间隔 250ms(120BPM 八分),不违反 200ms 去抖规则 |
| 玩家出手(gesture.js 内置 swipe/confirm) | ✅ 游戏层未重复调用 |
| Miss `error` | ✅ 语义正确(错过=没出声时的唯一反馈) |
| 16 连击 `success` / 全 Perfect 乐句 [20,40,20] | ✅ 语义正确,频率低 |
| 结算 S/A `success`、解锁 success+[20,40,20] | ✅ 在语音链开始前触发,不违反"长对白播放中不震动" |
**收紧项(打磨级,非阻塞)**:
1. 全 Perfect 乐句 [20,40,20] 与 16 连击 success 同刻触发时会双震——按 00 §2.4.3 的 200ms 去抖规则应只震一次,建议游戏层补一个最小去抖(记录上次震动时刻)。
2. 解锁处 success + [20,40,20] 连震两次同理合并为 [20,40,20] 一次。
## 4. 附裁(开发自拟折中的追认)
**附裁 A:玩法手势与"下滑=菜单"冲突的折中模式 —— 采纳为全球惯例。** rhythm(无临近 down 音符才开菜单)与 space(无下方陨石才开菜单)各自实现了同一思想:**玩法手势优先,菜单让位到"当前语境下该手势无玩法含义"时**。这符合 00 §5 豁免精神且比"固定豁免"更顺滑。写入 00 文档精神(不另改文):后续游戏遇同类冲突按此模式处理;RPG 战斗态"下滑=逃跑"本身就是语境判定,天然兼容。
## 5. 遗留事项汇总(转达)
1. rhythm 结算右滑末位修正(裁定①,一行 + 补录 `no_more_song`)。
2. rhythm 震动去抖两处(裁定④收紧项)。
3. 03/04 台词表 menu_main 勘误已落(本论附带完成)。
4. 真机听测:rhythm 的 Perfect 窗口 ±120ms 蓝牙耳机实测、space 满级段密度(裁定③旋钮)、两者 BGM/sfx 响度平衡——进 roadmap §4 checklist。
+47
View File
@@ -0,0 +1,47 @@
# R5 开发提示(给《剑与声》开发 agent · 返工注意级)
> 定位:RPG 开发已在并行进行。以下**不是推翻性意见**,是 R3/R4 审查中发现的共性问题在 RPG 语境下的对应检查点——如果你的实现已经正确处理,忽略即可;如果需要返工,按对应条目修。设计依据:05-rpg.md / 05-rpg-chapter1.md / 05-rpg-ch1-voice-ids.md / 05-rpg-addendum.md / 00-global-ux.md。
## 1. 已验证可照抄的健壮性模式(escape/simon/rhythm/space 四款一致落地)
1. **语音链看门狗**:所有"语音播完才推进流程"的地方都要双通道(onComplete + 超时兜底,二者先到先触发、只触发一次),参考 simon `_speakGuarded`。RPG 的场景切换、战斗回合开场、商店对话都依赖语音推进,缺看门狗会在资产缺失/解码失败时卡死整局。
2. **会话令牌**:重开/跳场景时 `session++` 使旧计时器与语音链回调失效。RPG 状态多(探索/战斗/商店/菜单),比前四款更需要。
3. **打断即作废**:状态切换入口统一 `stopVoice()`,不要依赖逐条清理。
## 2. 菜单与手势冲突(round2 裁决② + round3-4 附裁 A)
- RPG 是**菜单豁免游戏**:探索态菜单=左右滑切换+单击确认(00 §5.2),战斗态下滑=逃跑不设菜单。不要套用 escape/simon 的"单击循环+双击确认"。
- ch1 台词表 `sys_menu` 文案("菜单。左右滑选择,单击确认。")与豁免口径一致,**不用改**。
- 若你在菜单里加了"保存并回大厅"项:保存成功的反馈=`sys_saved` 语音 + `pickup` 震动,不要用提示音。
## 3. 导航/播报语音的末位语境检查(rhythm 裁定①的教训)
rhythm 在"最后一首"右滑播了"还没解锁"造成语义错位。RPG 同类风险点:
- **章末**:第三章打完 Boss 后播"下一章"类语音会错位——终章结算需单独的完结句(ch3 台词表补发时会含 `evt_ch3_end`,若你先行实现,先用 `btl_win` + `evt_ch1_end` 模式临时顶替,勿复用"前往下一章"句)。
- **场景图边界**:village 的 `back`、boss1 无选项等死端,播报选项时不要念出"返回"之外不存在的去向。
- **商店已购/缺钱的播报**已在台词表(shop_owned/shop_poor),按语境选句,别统一播 shop_poor。
## 4. 震动落点(按 05-rpg-addendum §1 总表 + 两条新收紧)
- 前摇窗口开启震 `beat`(有声敌人)/ `danger`+`heartbeat`(无声敌人)的通道分工**不要混用**:danger 专属于无声威胁。
- 新增收紧(round3-4 裁定④同款):200ms 去抖——格挡成功的 `confirm` 若与 gesture.js 内置双击 confirm 同刻,只震一次;升级 success 与战斗胜利播报前震动同理。
- iOS 无震动:无声敌人不可格挡是已知设计代价(addendum §2.2),**防御指令必须可用且有效**(减伤 50% 不依赖时机),这是 iOS 兜底,不要在任何分支里把防御禁用。
## 5. 音频层叠(addendum §3 的实现要求)
- 场景 amb_* **不走 bgm 层**(会被 bgm=0 设置误杀,且与战斗 BGM 冲突):用 `audio._load` + 手动 BufferSource loop 接 `_sfxBus`,rhythm `_startBgm`/`_scheduleSfx` 的手动总线接法可直接参考。
- 战斗 BGM(bgm/battle、bgm/boss)走正常 bgm 层,受 bgm 设置控制;进出战斗 crossfade 切换即可。
- 注意 decodeAudioData 内存:amb 循环 6-10s、BGM 60-90s,全章节资产控制在 ~30MB 压缩内(web-engine-api §移动浏览器的坑 7)。
## 6. 存档(05-rpg-chapter1.md §6 + 审查共性)
- `summary` 必须随每次保存更新(大厅卡片直接显示);格式"第X章·场景名 · LvN · 金币M"。
- 保存时机四件套:场景切换/战斗结算/商店交易/菜单保存——**战败重生也算战斗结算**,要存(否则死亡丢进度,比死亡本身更劝退)。
- localStorage 与服务器双写,离线静默(四款游戏的 `_putProgress` 模式照抄)。
- 继续/新开二选一(sys_continue_ask):右滑=新游戏是**破坏性操作**,建议加一次确认(单击确认句可用现有 `sys_new_game` 前的自答:"右滑确定重新开始?再右滑确认。"——如需新台词列入 ch2/3 补录,先用现有句拼接流程也行)。
## 7. 教学战(05-rpg-chapter1.md §5.2)
- 锁血保护(HP 最低 1)别忘了在 `_resolve` 类函数里实现为 clamp,不是免疫——教学战要让玩家**看到**掉血才理解格挡的价值。
- 教学句 tut_b1-b5 的触发点:tut_b1 在首次 ENEMY_TELEGRAPH 前、tut_b2 与前摇音**同时**(这是唯一允许语音与前摇叠放的位置,设计如此)、tut_b3 格挡成功后。
+319
View File
@@ -0,0 +1,319 @@
# SoundGame Web 引擎 API 手册
本文档面向在 `web/` 下开发新游戏的 agent/开发者。Web 引擎是 Android 引擎
(`docs/engine-api.md`、`docs/architecture.md`)的移植:同一套 6 手势、
同一套三层音频、同一套 Game 契约。纯 vanilla JS ES module,**无构建步骤、
无 npm 依赖**,任何静态 HTTP 服务器托管即可运行。
总览:
```
web/
├── index.html # 游戏大厅(深色风格,5 个游戏入口 + 用户/设置区)
├── serve.sh # 本地托管脚本(node web/server/server.js)
├── engine/ # 共享引擎 —— 所有游戏复用,勿修改
│ ├── gesture.js # 6 手势识别(触摸+鼠标,内置基础震动反馈)
│ ├── audio.js # Web Audio 三层音频引擎 + 启动遮罩(自动应用用户设置)
│ ├── settings.js # 全局用户设置(voice/speed/bgm,localStorage 持久化)
│ ├── haptics.js # navigator.vibrate 震动封装(预定义震型)
│ ├── game.js # Game / StateMachineGame 基类
│ └── style.css # 全屏黑底 + 封面铺满 + 屏蔽系统手势
├── server/ # 用户系统服务器(静态托管 + 注册/登录/进度 API)
│ ├── server.js
│ └── README.md # API 文档
├── template/ # 最小可运行模板游戏(复制它开工)
│ ├── index.html
│ ├── game.js
│ ├── cover.png
│ └── audio/{bgm,voice,sfx}/ # 含一个测试音 sfx/beep.ogg
└── <游戏名>/ # 每个游戏一个目录(目录约定见下)
```
## 目录约定(每个游戏必须遵守)
```
web/<name>/
├── index.html # 入口页,引用 ../engine/ 下的引擎
├── game.js # 游戏逻辑(导出继承 Game 的类)
├── cover.png # 封面图,1080x1920 竖图,文件名固定
└── audio/
├── bgm/*.ogg # 背景音乐(循环)
├── voice/*.ogg # 对白/语音(平铺,过渡期兼容布局)
├── voice/male/*.ogg # 男声版(settings.voice='male' 时优先加载)
├── voice/female/*.ogg # 女声版(默认,优先加载)
└── sfx/*.ogg # 音效(短促)
```
- 音频 id 形如 `'bgm/theme'`、`'voice/intro'`、`'sfx/hit'`,
`play()` 自动拼 `audio/<id>.ogg`(相对游戏页面)。
- **iOS 兼容**:`.ogg` 在部分 iOS Safari 上无法解码(见下文"移动浏览器的坑")。
id 不带扩展名时引擎依次尝试 `.ogg → .m4a → .mp3 → .wav`,
所以只要给同名文件放一份 `.m4a` 或 `.mp3` 副本即可自动回退,代码不用改。
- 资产缺失只 `console.error` 静默跳过,**不会抛异常**(对齐 Android)。
拼错 id 不会崩,只能靠浏览器 DevTools 控制台发现——调试时务必开着控制台。
## 快速开始
复制模板开工:
```bash
cp -r web/template web/<name>
```
然后改 `web/<name>/index.html` 的标题和 import 类名,替换 `game.js` 逻辑,
放入自己的 `cover.png` 和 `audio/` 资产。入口页的标准接线(模板已写好):
```html
<link rel="stylesheet" href="../engine/style.css">
<meta name="viewport" content="width=device-width, initial-scale=1,
maximum-scale=1, user-scalable=no, viewport-fit=cover">
<div id="game-root"><img class="cover" src="cover.png" alt=""></div>
<script type="module">
import { WebAudioEngine } from '../engine/audio.js';
import { attachGestures } from '../engine/gesture.js';
import { MyGame } from './game.js';
async function main() {
const root = document.getElementById('game-root');
const audio = new WebAudioEngine('audio/');
const game = new MyGame(audio, root);
await audio.showStartOverlay({ cover: 'cover.png', text: '轻触开始' });
game.start(); // 此刻音频已解锁
attachGestures(root, (g) => game.onGesture(g)); // 解锁后才接管手势
}
main();
</script>
```
## 音频解锁流程(必须遵守,否则无声)
iOS Safari 和 Chrome 都要求 AudioContext 必须在一次真实用户手势中创建/恢复。
引擎用启动遮罩解决:
1. 页面加载后 `showStartOverlay()` 显示全屏封面 + "轻触开始"(呼吸动画);
2. 用户第一次点按遮罩 → 引擎在手势回调里 `new AudioContext()` + `resume()`;
3. 遮罩移除,Promise resolve → 你调 `game.start()` 接管。
**任何 `play()` 在解锁前调用只会 console.warn 并跳过。**
解锁后再 `attachGestures`,这样开始那次点按不会被游戏误收。
## engine/audio.js — `WebAudioEngine`
```js
new WebAudioEngine(basePath = 'audio/', opts?)
```
| 构造 opts | 默认 | 含义 |
|---|---|---|
| `bgmVolume` / `voiceVolume` / `sfxVolume` | 1 | 各层基础音量 0~1 |
| `duckVolume` | 0.2 | 对白播放时 BGM duck 到的比例 |
| `fadeMs` | 800 | BGM 交叉淡入时长 ms |
| `duckMs` | 300 | duck / 恢复时长 ms |
方法:
- `unlock(): Promise<void>` — 创建/恢复 AudioContext。已在用户手势中时可用;
一般用 `showStartOverlay()` 代替。
- `showStartOverlay({ cover, text = '轻触开始' }): Promise<void>` —
启动遮罩,resolve 时音频已解锁。
- `play(id, opts?)` — 统一入口,按 id 前缀(`bgm/` `voice/` `sfx/`)分发:
- 公共:`opts.volume`(0~1,乘层音量)
- BGM:`opts.fadeMs`、`opts.loop = true`;同一首重复 `play` 不重启
- voice:`opts.onComplete`(**自然播完**才触发;被 `stopVoice()` 或新 voice
顶掉时**不触发**,对齐 Android——用这一点实现"打断即作废整条语音链");
`opts.queue = true` 排队等当前对白播完(默认 false 直接顶掉)
- sfx:即发即弃,天然支持并发叠加
- `prewarm(ids: string[])` — 预解码一批音频,消除首次播放的网络+解码延迟。
建议 `start()` 里把所有短音效 prewarm 一遍。
(与 Android 差异:Web 端**不需要**音量 0 假播预热,解码缓存即可。)
- `stopBgm(fadeOutMs = 800)` — 淡出后释放 BGM。
- `stopVoice()` — 打断当前对白并清空排队,不触发 onComplete,BGM 恢复。
- `stopAll()` — stopBgm(200) + stopVoice()(sfx 很短,不主动停)。
三层行为(对齐 Android DefaultAudioEngine):
| 层 | 播放器 | 特性 |
|---|---|---|
| BGM | AudioBufferSourceNode(双实例轮换) | 循环;切换时新源 0→满、旧源 →0 交叉淡入,淡出后 stop |
| voice | 单通道 | duck BGM 到 20%(300ms 渐降),播完 300ms 恢复;onComplete 驱动链播 |
| sfx | AudioBufferSourceNode 即发即弃 | 预解码 AudioBuffer 缓存,低延迟,可叠加 |
**用户设置自动生效(settings.js,游戏代码无需改动):**
- `settings.voice`(`'male'|'female'`,默认 `'female'`):voice 层路径解析——
`play('voice/intro')` 依次尝试 `audio/voice/<gender>/intro.{ogg,m4a,mp3,wav}`,
全部找不到再回退平铺的 `audio/voice/intro.*`(过渡期资产未分性别也能播)。
缓存键含性别,游戏中途切换音色下一句即用新性别资产。
- `settings.speed`(`1.0|1.35|1.7`,默认 `1.0`):voice 层
`source.playbackRate`,在创建 source 时读取,**从下一句生效**
(进行中的句子不变速)。
- `settings.bgm`(`1.0|0.3|0`,默认 `1.0`):BGM 层独立增益总线。
改动对**进行中的 BGM 立即生效**(150ms 渐变);调到 `0` 立即淡出停止
且此后 `play('bgm/...')` 只记意图不加载不播放;从 `0` 调回非零时,
若此前有想播的 BGM 会自动补播。
## engine/settings.js — `settings`
全局用户设置单例,localStorage 持久化(key: `'soundgame.settings'`),
同域下大厅与所有游戏页共享;跨标签页通过 `storage` 事件同步。
```js
import { settings } from '../engine/settings.js';
settings.get(); // -> { voice: 'male'|'female', speed: 1|1.35|1.7, bgm: 1|0.3|0 }
settings.set({ speed: 1.35 }); // 部分更新;非法键/值被忽略(console.warn)
const off = settings.subscribe((snapshot, changed) => { ... }); // off() 退订
```
- 非法值永远不会进入存储——读入时逐项校验,坏数据自动回退默认值。
- 游戏侧一般**不需要**直接用本模块:audio.js 已自动应用三项设置。
需要自定义响应(如设置页 UI)时才 `subscribe`。
- 大厅会在用户登录后把设置同步到服务器(`PUT /api/progress {game:'_settings'}`)。
## engine/haptics.js — `vibrate`
`navigator.vibrate` 封装,预定义震型:
| 名称 | 震型 (ms) | 建议用途 |
|---|---|---|
| `confirm` | `[20]` | 点按确认 |
| `error` | `[60,40,60]` | 操作错误 |
| `success` | `[100]` | 过关/成功 |
| `warning` | `[30,50,30,50,30]` | 警告/危险逼近 |
| `swipe` | `[10]` | 滑动识别成功 |
| `heartbeat` | `[40,100,40]` | 心跳/倒计时紧张感 |
| `pickup` | `[15]` | 拾取/获得物品 |
| `hit` | `[80]` | 受击/终结性失败 |
| `beat` | `[8]` | 节拍锚点(最轻,如 simon 序列逐音) |
| `danger` | `[25,40,25,40,25]` | 危险逼近 |
```js
import { vibrate, hapticsSupported } from '../engine/haptics.js';
vibrate('error'); // 预定义名称
vibrate([50, 30, 50]); // 自定义震型
```
- 特性检测 `'vibrate' in navigator`;不支持的平台(iOS Safari)**静默 no-op**。
- gesture.js 已内置基础反馈:滑动→`swipe`,tap→`confirm`,doubletap→`confirm`。
**游戏逻辑层只需在语义事件(error/success/warning/heartbeat 等)处自行调用**,
不要对手势本身重复调 `vibrate('confirm')`/`vibrate('swipe')`,否则会双重震动。
## engine/gesture.js — `attachGestures`
```js
const detach = attachGestures(element, onGesture, config?)
// onGesture: (g: 'up'|'down'|'left'|'right'|'tap'|'doubletap') => void
// detach(): 解除绑定
```
判定参数(`DEFAULT_GESTURE_CONFIG`,与 Android `GestureConfig` 对齐):
| 参数 | 默认 | 含义 |
|---|---|---|
| `swipeMinDistance` | 64 px | 累积位移阈值(≈ Android 64dp,按 160dpi 等效) |
| `swipeMinVelocity` | 400 px/s | 平均速度阈值(位移 / 拖动时长) |
| `doubleTapWindow` | 300 ms | 双击判定窗口 |
| `tapSlop` | 24 px | 位移小于此才算点按 |
- 方向取位移较大的轴,斜滑归一到最近主轴(防斜滑误判)。
- 触摸 + 鼠标双支持(桌面可用鼠标拖拽/点击调试);多指触摸忽略。
- 元素上的 touchstart/touchmove/touchend 全部 `preventDefault`
(防滚动/双指缩放/长按菜单),元素需有 `touch-action: none`
(`style.css` 的 `#game-root` 已带)。
- 手势绑定是全屏捕获层级的:绑在 `#game-root` 上即可覆盖整个屏幕。
- **内置基础震动反馈**(haptics.js):滑动识别成功→`swipe`、tap→`confirm`、
doubletap→`confirm`;不支持震动的平台自动静默。游戏语义反馈
(error/success/warning 等)由游戏层调用 `vibrate()`。
**单击有约 300ms 延迟**:引擎要等双击窗口关闭才能确定是单击,`tap`
回调因此延迟 ~300ms 触发(`doubletap` 和滑动是即时的)。这是引擎级行为,
游戏设计必须绕开——即时反馈(格挡、击打)绑滑动或双击,单击只用于
"继续剧情""检查房间"这类非即时操作(Android 端 reflex/storyline/escape
均如此设计)。
## engine/game.js — `Game` / `StateMachineGame`
```js
import { Game, StateMachineGame } from '../engine/game.js';
class Game {
constructor(audio, root) // audio: WebAudioEngine;root: #game-root 元素
onGesture(g) {} // 手势入口(子类实现)
start() {} // 音频解锁后调用一次(子类实现)
stop() {} // 可选;默认实现 audio.stopAll()
}
```
`StateMachineGame` 是推荐起点(对齐 Android `GameEngine<S>`,线性剧情类适用):
```js
class MyGame extends StateMachineGame {
initialState() { return 'INTRO'; } // 初始状态
transition(state, g) { ...; return state; } // 纯函数,返回原状态 = 无转移
onEnterState(state, prev) { ... } // 副作用:播音频;prev 初始为 null
// setState(next) 可强制切换(计时器、onComplete 驱动)
}
```
`onGesture` 是 final 逻辑:算 `transition`,状态变了才更新并回调
`onEnterState`。`start()` 以 `prev = null` 进一次 `initialState()`。
状态复杂(多维状态、计时器驱动)的游戏直接继承 `Game` 即可——Android 端
三个游戏实际都没用状态机基类。
## 与 Android 引擎的行为差异
| 项 | Android | Web |
|---|---|---|
| sfx 预热 | SoundPool 异步加载,必须音量 0 预播否则首播丢失 | 按需解码 + 缓存,不需要假播;`prewarm()` 只是提前解码 |
| 音频格式 | ogg 全覆盖 | **iOS Safari 可能无法解码 ogg**,引擎自动回退 m4a/mp3/wav;建议双格式 |
| 音频解锁 | 无限制 | 必须先经用户手势(启动遮罩已内置) |
| 滑动阈值 | 64dp(物理密度无关) | 64 CSS px(≈64dp@160dpi,高分屏实际手感略小,可接受) |
| BGM/voice 播放 | ExoPlayer 流式 | 整段 fetch + 解码进内存,**大文件有解码延迟和内存占用**——BGM 建议压到几分钟内、voice 单句切分 |
| 资源缺失 | Log.e 静默跳过 | console.error 静默跳过(行为一致) |
## 移动浏览器的坑(务必读)
1. **自动播放限制**:iOS Safari / Chrome 要求 AudioContext 在用户手势中
resume。引擎的启动遮罩已解决,但不要绕过它——解锁前 `play()` 是无声 no-op。
2. **iOS 不支持 ogg**:Safari 对 Ogg Vorbis 的解码支持很差(随版本变化,
老版本完全不行)。引擎已做扩展名回退,但资产侧要保证有 `.m4a`(AAC)
或 `.mp3` 副本,否则 iOS 上全程无声。生成:`ffmpeg -i in.ogg -c:a aac out.m4a`。
3. **iOS 静音开关**:iPhone 静音模式下 Web Audio 默认**仍然出声**
(AudioContext 不受静音开关控制,这点比 `<audio>` 标签好),
但部分老版本 Safari 行为不一致,真机测试为准。
4. **后台/锁屏**:页面切后台 AudioContext 会被系统挂起,回到前台
需要 `ctx.resume()`(引擎在 unlock 后如被挂起,下次手势时不会自动恢复——
如果游戏需要,可在 `visibilitychange` 里重新 `audio.unlock()`)。
5. **页面滚动/缩放手势干扰**:已用 `touch-action: none` + `preventDefault` +
viewport meta 锁住;iOS Safari 双指捏合缩放在少数版本仍可能触发,
属系统行为无法完全屏蔽。
6. **蓝牙音频延迟**:蓝牙耳机有 100~300ms 输出延迟,节奏类游戏(rhythm)
判定窗口要留余量,并优先建议用户用有线/扬声器。
7. **decodeAudioData 内存**:所有音频解码成 PCM 常驻内存(约为原始压缩
文件的 10 倍体积)。单个游戏音频总量建议控制在 ~30MB 压缩体积内。
## 本地验证
```bash
# 1. JS 语法校验(node 装在 tools/node/,ES module 语法可被识别)
/home/ubuntu/SoundGame/tools/node/bin/node --check web/<name>/game.js
# 2. 起本地服务器(Node 静态 + 用户系统 API,见 web/server/README.md)
bash web/serve.sh
# 打印局域网地址,如 http://192.168.x.x:8000/
# 3. 桌面浏览器打开 http://localhost:8000/<name>/ 用鼠标调试
# (DevTools → Toggle device toolbar 模拟手机尺寸;控制台看引擎日志)
# 4. 手机与电脑连同一 WiFi,浏览器打开局域网地址
# WSL2 需要先在 Windows 做端口转发(serve.sh 注释里有命令)
# 5. curl 冒烟(资源 200 + Content-Type 正确)
curl -sI http://localhost:8000/<name>/index.html
curl -sI http://localhost:8000/<name>/audio/sfx/xxx.ogg
```
听感参数(反应窗口、响度平衡、立体声分离度)必须在真机上戴耳机验证——
这一点和 Android 版一样,模拟器/桌面听不出手感。