用户系统(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)
21 KiB
00 · 全局交互规范(Global UX)
给开发者的实现要点摘要
- 设置项(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)。- 所有提示音反馈改为震动:
engine/haptics.js(已实现,震型名小写:confirm/error/success/warning/swipe/heartbeat);iOS Safari 不支持时静默降级,绝不回退到提示音。- 旁白全部整句预生成:禁止数字/碎片拼接(音色漂移根因),每款游戏旁白固定同一
speaker+ 同一instruct常量。- 手势全局约定:上滑=帮助/状态、下滑=菜单、单击=确认/继续、双击=重听、左右滑=选择/移动;以滑动为核心玩法的游戏可偏离(见 §5 豁免表),但必须在开场交互教学中讲清。PC 键盘平行通道(§5.5,P0-A 已实现)不改变这些语义——同一手势宪法、同一状态机,仅多一条输入通路。
- 用户系统:大厅页登录,进度经
web/server/server.jsREST 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,值形如:
{ "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 已落地):
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 绝不用震动的场景
- 长对白播放中(voice 层有音频在播):旁白叙事期间不震动,避免震动机理掩盖语音细节、也让玩家误以为需要操作。例外:教学流程里"现在请向左滑"这类等待玩家操作的指令句,在语音播完后允许一次轻震提示可以操作——震动发生在 voice 结束后,不重叠。注意:教学里玩家出手后引擎已自动震 swipe/confirm,游戏层通常无需再补。
- 序列/乐句播放中需要玩家纯聆听记音高的阶段(simon/节奏游戏领奏时例外:BEAT 本身就是节拍信息通道,见 02/03 文档)。
- 同一 200ms 窗口内多个事件只震一次(实现上去抖)。
- 开场遮罩、大厅页等可视化 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):
- 碎片拼接:
voice/di+voice/num_5+voice/lun拼"第 5 轮"。每个碎片独立生成,音色、气息、响度逐片漂移,接缝明显。 - 同游戏多 instruct / 长短不一:有的句带情感指令有的不带,批次间模型状态不一致。
3.2 规范(所有游戏强制执行)
- 一句一文件,整句生成。凡要播报给玩家的信息,整句一次 TTS 生成,禁止运行时拼接。例外:>40 字的长台词由资产管线在生成期拆句拼接(R1 管线已支持,见 docs/audio/round1-report.md),运行时仍不拼。
- 数字信息改分段整句。需要报数字的场景(轮数、成绩、存活时间、用时、准确率),按取值分段预生成整句:
- 取值少且可数(如 simon 轮数):直接为每个值生成整句,"第一轮"…"第二十轮",超出范围用无数字兜底句。
- 取值连续(如存活秒数、准确率):按区间分段整句,如"不到三十秒""超过一分钟""准确率超过八成五"。宁粗勿拼。
- 各游戏的具体分段在各自设计文档中给出。
- 同游戏同音色:每款游戏的旁白用固定 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/根目录(引擎平铺回退会命中)。 - 短句优先:单句 ≤ 25 个汉字为宜;长信息拆成多句链播(onComplete 串行),每句仍是完整句,不是碎片。
- 生成管线:统一用
tools/gen_voice_dual.py(R1 已建,台词表驱动,female/male 双跑、断点续跑、ogg+m4a 双格式、峰值归一 0.89)。新台词表由策划以"voice-ids 文档"形式交付(id + 全文 + speaker + instruct),音频总监照录进该脚本。 - 验收:自动 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 的两条约定
summary字符串(必须):大厅游戏卡片会直接显示data.summary(已实现,index.htmlloadProgress())。每个游戏的存档 data 里必须维护一句人话摘要,如"第一章·溪流 · Lv3 · 金币120"、"最高纪录:12 轮"。- 保留键
_settings(设置同步用,见 §1.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(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,均已实现并回归):
- 空格也走 300ms 双击窗(与触摸共享窗口,混合设备"触屏 tap + 空格 tap"判 doubletap = 同一虚拟指针语义);回车是唯一的即时双击键—— 确认类操作零延迟,同时保留空格×2 兼容触屏肌肉记忆。
- 不做 Esc 映射:下滑已覆盖菜单语义(§5.1/§5.2),Esc 留给浏览器 自身/未来全屏退出,不进入手势域。
- 守卫:
e.repeat抑制(按住只发一次);映射键 preventDefault (防滚页/焦点激活);表单控件焦点(input/textarea/select/ contenteditable)放行;未映射键(Tab 等)不拦截;tracking 单飞行 指针互斥(与多指忽略一致)。 - 提示随输入模式(settings.input,见 §1.1):三通道永远并行在线, 模式只切提示文案(遮罩「轻触开始」/「按空格开始」),供后续游戏内 提示与语音变体(input-parity 路线 a)接入。
- 遮罩键盘解锁:空格/回车 keydown 即可解锁音频(audio.js showStartOverlay),PC 玩家全程无需鼠标。
- 测试:
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。本节只列与全局约定交叉的部分。
- 四个全局机制(方法论 research/02 §3 的 L3/L4 落地):状态签名 earcon(6 状态+2 功能音,运行时合成,全项目统一词表);回合标记
turn(等待输入开始处);静默看门狗(等待输入无操作→轻提醒→升级上下文提示,用尽即停不轰炸);纠错error(error 震+一句"该做什么")。 - 等级语义:
full=earcon+向导语音+震动;standard=earcon+震动(无向导语音);off=全部 no-op 且不注册定时器。向导语音一律queue:true,绝不顶掉在播对白(A3 信息分层)。 - 与既有约定的边界:向导不改变任何手势映射与玩法判定;向导震动只用既有震型(confirm/error/nudge 复用),不新增语义震;向导 earcon 是系统音(P2 系统音/叙事音分离),不占用 sfx 内容音语义域。
- 看门狗通用参数(各游戏已按此接):第 1 级 8
10s(nudge+一句提示),第 2 级 2025s(上下文提示/复述),之后停止;玩家任意手势复位。escape 三级卡关计时器(60/120/180s)保持独立,不受影响。 - 测试约定:既有 playtest 一律
settings.set({guide:'off'})验证纯玩法逻辑;向导行为由web/harness/guide.test.mjs(full 档)覆盖。