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)
This commit is contained in:
xsl
2026-09-09 23:39:23 +08:00
parent 3704cd0792
commit ede522dda1
22 changed files with 2370 additions and 275 deletions
+23 -6
View File
@@ -20,8 +20,8 @@
* **自动播放限制**:iOS / Chrome 要求 AudioContext 必须在用户手势中 resume。
* 标准流程(必须遵守,否则无声):
* const audio = new WebAudioEngine('audio/');
* await audio.showStartOverlay({ cover: 'cover.png' }); // 遮罩:封面 + "轻触开始"
* // 此处 AudioContext 已解锁
* await audio.showStartOverlay({ cover: 'cover.png' }); // 遮罩:封面 + 提示文字
* // 此处 AudioContext 已解锁(触摸/鼠标点击或空格/回车 keydown 均可解锁)
* game.start();
*
* 与 Android 的一个差异:Web 端 sfx 是按需解码并缓存 AudioBuffer,解码完成后
@@ -51,7 +51,7 @@
*/
import { settings } from './settings.js';
import { setLongpressBoostEventsEnabled } from './gesture.js';
import { setLongpressBoostEventsEnabled, inputMode } from './gesture.js';
import { SoundTouch, SimpleFilter, WebAudioBufferSource, getWebAudioNode } from './vendor/soundtouch.js';
const EXT_FALLBACKS = ['.ogg', '.m4a', '.mp3', '.wav'];
@@ -152,14 +152,18 @@ export class WebAudioEngine {
}
/**
* 启动遮罩层:全屏封面 + "轻触开始"。第一次点按时解锁音频并移除遮罩。
* 启动遮罩层:全屏封面 + 提示文字。第一次点按/按键时解锁音频并移除遮罩。
* @param {object} [opts]
* @param {string} [opts.cover] 封面图地址(如 'cover.png'),缺省纯黑背景
* @param {string} [opts.text='轻触开始']
* @param {string} [opts.text] 提示文字;缺省按输入模式自适应——
* gesture.inputMode()==='key' →「按空格开始」,否则「轻触开始」
* (settings.input / 最近使用通道决定,见 gesture.js)。
* 显式传 text 的调用完全覆盖(各游戏页现状)。
* @returns {Promise<void>} resolve 时音频已解锁,可安全 start()
*/
showStartOverlay(opts = {}) {
const { cover, text = '轻触开始' } = opts;
const { cover } = opts;
const text = opts.text ?? (inputMode() === 'key' ? '按空格开始' : '轻触开始');
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.id = 'start-overlay';
@@ -180,6 +184,7 @@ export class WebAudioEngine {
const onTap = () => {
if (done) return;
done = true;
if (hasWin) window.removeEventListener('keydown', onKey);
this.unlock().then(() => {
overlay.remove();
resolve();
@@ -195,6 +200,18 @@ export class WebAudioEngine {
overlay.addEventListener('touchend', (e) => { e.preventDefault(); onTap(); }, { passive: false });
// 桌面鼠标 / 辅助技术触发的 click 不受影响,照常解锁
overlay.addEventListener('click', onTap);
// 键盘解锁(PC 平行通道):空格/回车 keydown 同属 user activation,回调
// 内同步发起 unlock() 即可满足自动播放限制。挂 window 而非遮罩(游戏页
// 焦点在 body,keydown 不落在遮罩元素上),解锁后移除。此时 gesture.js
// 尚未 attach(遮罩先于 attachGestures),键盘解锁必须在这里做。
const hasWin = typeof window !== 'undefined' && typeof window.addEventListener === 'function';
const onKey = (e) => {
if (e.repeat) return;
if (e.code !== 'Enter' && e.code !== 'Space') return;
e.preventDefault();
onTap();
};
if (hasWin) window.addEventListener('keydown', onKey);
});
}