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:
+23
-6
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user