Files
SoundGame/docs/design/00-global-ux.md
T
xsl ede522dda1 feat(platform/P0): 无感用户系统 + PC 键盘双端一致(两 agent 并行 + 集成收口)
用户系统(B,游戏零改动):
- POST /api/device 首访 0 交互静默建档(自动玩家一/二…,per-IP 频控);断网降级纯本地照玩
- 大厅档案切换器(切换=flush 上云→快照回灌→倒灌→服务器回拉,profileId 分桶修同设备串档;aria menu 编号可听)
- claim 可选认领(命名+scrypt PIN);旧 users/sessions 单向迁移(PIN 明文即刻哈希,已真实数据验证);login 兼容+claim-merge 桶级 LWW;logout;/server/ 静态封禁
- PUT baseUpdatedAt 守卫(legacy 放行+警告);GET 无参全桶回拉(修 7/8 游戏跨设备只 PUT 不 GET)
- web/profiles.js 分桶纯函数 + api-smoke.mjs 7/7(建档/LWW/claim-merge/迁移/频控/封禁/分桶 H1-H4)

键盘双端(A,游戏代码零改动):
- gesture.js 键盘通道 +137 行:←↑↓→/WASD=滑动、空格=单击(走 300ms 双击窗与触摸共享虚拟指针)、回车=即时 doubletap、按住空格 ≥450ms=长按;repeat 抑制/表单守卫/方向键 preventDefault
- settings.input(auto/touch/key)+ gesture.inputMode();遮罩键盘解锁与文案自适应(按空格开始/轻触开始)
- 8 游戏遮罩改用自适应默认文案;大厅设置面板「操作方式」+页脚键位提示
- live control.mjs 键盘注入通道(CDP dispatchKeyEvent);engine-input.test 61 断言

回归:12 套件全绿(8 playtest + guide + engine-voice + engine-input + api-smoke 7/7);8001 服务器已重启加载新 API,/api/device 端到端验证通过

P1 待排:153 句×双性 TTS 键盘语音变体 / 6 位配对码跨设备 / 认领 UI 打磨(research/platform/00-synthesis.md §3)
2026-09-09 23:39:23 +08:00

228 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 00 · 全局交互规范(Global UX)
> **给开发者的实现要点摘要**
> 1. 设置项(voice / speed / bgm / guide / input)只在大厅页用 `<select>` 可视化 UI 修改,存 `localStorage["soundgame.settings"]`,由 `engine/settings.js` 管理;游戏启动时只读生效,游戏内不提供修改入口(菜单里只播报当前值)。**R1 已落地,本文 §1/§2/§4 已按实现修订(R2 修订版);R7 增补 guide 设置与向导层(§6,见 14 号文档);P0-A 增补 input 设置(§1.1,见 §5.5)。**
> 2. 所有提示音反馈改为震动:`engine/haptics.js`(**已实现**,震型名小写:`confirm/error/success/warning/swipe/heartbeat`);iOS Safari 不支持时**静默降级,绝不回退到提示音**。
> 3. 旁白全部整句预生成:禁止数字/碎片拼接(音色漂移根因),每款游戏旁白固定同一 `speaker` + 同一 `instruct` 常量。
> 4. 手势全局约定:上滑=帮助/状态、下滑=菜单、单击=确认/继续、双击=重听、左右滑=选择/移动;以滑动为核心玩法的游戏可偏离(见 §5 豁免表),但必须在开场交互教学中讲清。**PC 键盘平行通道(§5.5,P0-A 已实现)不改变这些语义——同一手势宪法、同一状态机,仅多一条输入通路。**
> 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` / `2.2` | **只作用于 voice 层**的 `playbackRate`(创建 voice source 时实时读取,**从下一句开始生效**,正在播的句子不变速)。BGM、sfx 速率不变。TTS 一律按 1.0 语速生成;`instruct` 里禁止出现语速描述词。**2.2 档为视障档**(R7 增补:资深读屏用户听速 250+ WPM,见 research/01-literature/03)。 |
| `bgm` | `1.0`(默认)/ `0.3` / `0` | BGM 层独立增益总线系数。`0` = 不加载、不播放任何 `bgm/` 资产(`_playBgm` 短路),ducking 逻辑保持原样。改设置即时生效(已实现 subscribe 机制)。 |
| `guide` | `"full"`(默认)/ `"standard"` / `"off"` | **向导帮助等级**(R7 增补,engine/guide.js 实时读取,见 §6 与 14 号文档):`full`=签名 earcon+向导语音+震动(新手全托管);`standard`=只留 earcon+震动(无向导语音,尊重专家);`off`=全静且**不挂任何看门狗定时器**(老手/回归测试)。 |
| `input` | `"auto"`(默认)/ `"touch"` / `"key"` | **提示文案输入模式**(P0-A 增补,`gesture.inputMode()` 读取):`auto`=跟随最近使用的通道(键盘 keydown→key,任意 touchstart→touch,会话内无输入时按能力检测);`touch`/`key`=手动覆盖。**只影响提示文案**(如启动遮罩「轻触开始」/「按空格开始」),**不关闭任何输入通道**——三通道永远并行(混合设备)。 |
存储:localStorage key = `soundgame.settings`,值形如:
```json
{ "voice": "female", "speed": 1.0, "bgm": 1.0, "guide": "full" }
```
注意(与设计初稿的差异,**以实现为准**):`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 引擎级延迟:即时判定(格挡、节奏打击)一律绑滑动或双击,单击只用于非即时操作(确认、检查、菜单)。新设计继续遵守。**唯一例外**:rhythm 开启引擎 `immediateTap`(按下即发 tap、双击补发改判),因它的单击就是核心演奏输入;其他游戏不得开(每次按下都先发 tap,会污染滑动/长按语义)。
- 结算画面统一:单击=再来一局,下滑=菜单(回大厅)。教学可跳过:任何游戏的引导阶段,下滑=跳过引导(首次强制完整引导的标志位存进度里)。
### 5.5 键盘平行通道(P0-A 已实现:engine/gesture.js 内置通道)
> 调研与业界依据:[research/platform/02-input-parity.md](../../research/platform/02-input-parity.md)(audiogames.net 社区惯例、A Blind Legend / Blind Drive 双端先例)。
PC 键盘是触屏手势的**平行输入通道**,不是另一套手势宪法:键盘事件在
`attachGestures` 内部喂入与 touch/mouse **同一状态机**,游戏层收到的
7 种手势事件(up/down/left/right/tap/doubletap/longpress/longpressend
+ immediateTap 的 tapcancel)与内置震动**完全同构**——游戏代码零改动,
§5.1-§5.4 的全部手势语义(含各游戏豁免表)原封不动。
**键位表(裁定方案,`e.code`):**
| 手势(§5.1 语义不变) | 键盘主键 | 别名 | 行为差异 |
|---|---|---|---|
| 左滑 / 右滑 | ← / → | A / D | keydown 即发(无位移/速度判定,零延迟) |
| 上滑 / 下滑 | ↑ / ↓ | W / S | 同上 |
| 单击 | 空格 | — | 走完整 tap 状态机:**保留 ~300ms 双击窗**(与触屏单击同感) |
| 双击 | **回车** | 空格×2 | 回车**即时**触发(不等双击窗);空格两连也判双击(状态机路径) |
| 长按 / 长按结束 | 按住空格 ≥450ms / 松开 | — | 与触摸长按完全一致(含旁白快进 window 事件) |
设计裁定(P0-A,均已实现并回归):
1. **空格也走 300ms 双击窗**(与触摸共享窗口,混合设备"触屏 tap + 空格
tap"判 doubletap = 同一虚拟指针语义);**回车是唯一的即时双击键**——
确认类操作零延迟,同时保留空格×2 兼容触屏肌肉记忆。
2. **不做 Esc 映射**:下滑已覆盖菜单语义(§5.1/§5.2),Esc 留给浏览器
自身/未来全屏退出,不进入手势域。
3. **守卫**:`e.repeat` 抑制(按住只发一次);映射键 preventDefault
(防滚页/焦点激活);表单控件焦点(input/textarea/select/
contenteditable)放行;未映射键(Tab 等)不拦截;tracking 单飞行
指针互斥(与多指忽略一致)。
4. **提示随输入模式**(settings.input,见 §1.1):三通道永远并行在线,
模式只切提示文案(遮罩「轻触开始」/「按空格开始」),供后续游戏内
提示与语音变体(input-parity 路线 a)接入。
5. 遮罩键盘解锁:空格/回车 keydown 即可解锁音频(audio.js
showStartOverlay),PC 玩家全程无需鼠标。
6. 测试:`web/harness/engine-input.test.mjs`(61 断言);live 管线
`POST /gesture {"keyboard":true,"key":"ArrowLeft"|"Space"|"Enter"}`
或 `{"type":"tap","keyboard":true}`(CDP Input.dispatchKeyEvent,
会话剧本加 `keyboard:true` 即可键盘复跑)。
## 6. 向导层(R7 增补,已实现:engine/guide.js)
> 设计裁定与台词表全录:[14-guide-layer.md](14-guide-layer.md)。本节只列与全局约定交叉的部分。
1. **四个全局机制**(方法论 research/02 §3 的 L3/L4 落地):状态签名 earcon(6 状态+2 功能音,运行时合成,全项目统一词表);回合标记 `turn`(等待输入开始处);静默看门狗(等待输入无操作→轻提醒→升级上下文提示,用尽即停不轰炸);纠错 `error`(error 震+一句"该做什么")。
2. **等级语义**:`full`=earcon+向导语音+震动;`standard`=earcon+震动(**无向导语音**);`off`=全部 no-op 且不注册定时器。向导语音一律 `queue:true`,**绝不顶掉在播对白**(A3 信息分层)。
3. **与既有约定的边界**:向导不改变任何手势映射与玩法判定;向导震动只用既有震型(confirm/error/nudge 复用),不新增语义震;向导 earcon 是**系统音**(P2 系统音/叙事音分离),不占用 sfx 内容音语义域。
4. **看门狗通用参数**(各游戏已按此接):第 1 级 8~10s(nudge+一句提示),第 2 级 20~25s(上下文提示/复述),之后停止;玩家任意手势复位。escape 三级卡关计时器(60/120/180s)保持独立,不受影响。
5. **测试约定**:既有 playtest 一律 `settings.set({guide:'off'})` 验证纯玩法逻辑;向导行为由 `web/harness/guide.test.mjs`(full 档)覆盖。