feat(platform/P1): 配对码跨设备 + 认领打磨 + 键盘语音双套资产(两 agent 并行 + 集成收口)
配对码与认领(B): - POST /api/pair/start|join:6 位码 10 分钟单次有效、仅存内存零泄露面、5 连错 per-IP 封锁 10 分钟;join=新设备档案按桶 LWW 并入老档案+token 改绑(复用 claim-merge);日志脱敏 - 大厅:生成配对码(speechSynthesis 逐位朗读+aria-live+重听)/从其他设备转入(one-time-code 输入框)/从本机移除/401 过期自愈引导/aria 补全/玩家名可听化 - api-smoke 12/12(配对全流程/频控/封锁/过期自愈/移除/digitsToCn)+ CDP 真浏览器 E2E 22/22 键盘语音双套资产(A,路线 a): - 164 变体句(8 游戏 grep 实测,超出调研预估 153:含轻触/点两下/按住变体)×2 性别=328 生成 656 文件 36.9 分钟;触屏主资产零字节改动(PCM 逐样本比对后还原) - 引擎 _candidates 三跳路由 key→gender→平铺(inputMode 跟随);缓存键 mode|gender|id;engine-input 新增 K9 路由断言(72 条) - check_all_assets 增 key 列(两性别一致且 ⊆ 主集)8 游戏全绿;voice_text.json 增 __key__ 表;live control.mjs key 会话对照键盘措辞验收(集成) - voice-ids 文档:8 游戏键盘变体小节由 KEY_LINES 程序化生成(与台词表逐字断言一致);补齐 storyline/escape/simon 三个历史缺失文档;design README 增 new8 索引 - mock 适配:playtest persona 加 maxTouchPoints=1 保持触屏链断言语义零改动 回归:12 套件全绿(8 playtest + guide 122 + engine-voice 19 + engine-input 72 + api-smoke 12/12) 同批入库:批次 2/3 已完稿设计文档(17-abyss/18-detective/19-cipher 全套,20-22 在途)
This commit is contained in:
+24
-8
@@ -32,6 +32,12 @@
|
||||
* - settings.voice:voice 层按性别解析路径——play('voice/intro') 实际依次尝试
|
||||
* `audio/voice/<gender>/intro.{ogg,m4a,mp3,wav}`,找不到回退平铺的
|
||||
* `audio/voice/intro.*`(过渡期资产未分性别也不会炸)。游戏代码不用改。
|
||||
* - 输入模式(settings.input + gesture.inputMode() 运行时跟随):**生效模式为键盘**
|
||||
* (settings.input==='key',或 auto 且最近使用键盘/纯桌面初始判定)时,voice
|
||||
* 候选链最前面多一跳 `audio/voice/<gender>/key/<id>.*`——键盘措辞变体资产
|
||||
* (P1 双套资产路线 a,仅覆盖含操作词的句子),缺失时自然回退
|
||||
* `voice/<gender>/` 主资产 → 平铺,触屏措辞资产零改动。缓存键带输入模式,
|
||||
* 模式切换后从下一句起用另一套(与性别切换同语义)。
|
||||
* - settings.speed:voice 层语速,**变速不变调**——voice 走 SoundTouch 时间
|
||||
* 拉伸管线(vendor/soundtouch.js,ScriptProcessorNode),从下一句开始生效。
|
||||
* - settings.bgm:BGM 层增益;改动立即作用于进行中的 BGM(150ms 渐变)。
|
||||
@@ -342,18 +348,25 @@ export class WebAudioEngine {
|
||||
* id -> 候选路径列表(带扩展名直接用,否则按回退表依次尝试)。
|
||||
* voice 层按 settings.voice 优先尝试 `voice/<gender>/` 子目录,
|
||||
* 找不到再回退平铺路径(过渡期兼容)。
|
||||
* **键盘变体(P1 路线 a)**:生效输入模式为键盘(gesture.inputMode()==='key',
|
||||
* 即 settings.input==='key' 或 auto 且键盘在跟)时,候选链前置
|
||||
* `voice/<gender>/key/<id>` 一跳——键盘措辞资产只有含操作词的句子,其余
|
||||
* id 此跳 404 后自然落回 `voice/<gender>/` 主资产 → 平铺,游戏无感。
|
||||
*/
|
||||
_candidates(id) {
|
||||
const name = id.slice(id.lastIndexOf('/') + 1);
|
||||
const hasExt = name.includes('.');
|
||||
if (id.startsWith('voice/')) {
|
||||
const genderBase = this.basePath + 'voice/' + settings.get().voice + '/' + id.slice(6);
|
||||
const rest = id.slice(6);
|
||||
const gender = settings.get().voice;
|
||||
const genderBase = this.basePath + 'voice/' + gender + '/' + rest;
|
||||
const flatBase = this.basePath + id;
|
||||
if (hasExt) return [genderBase, flatBase];
|
||||
return [
|
||||
...EXT_FALLBACKS.map((ext) => genderBase + ext),
|
||||
...EXT_FALLBACKS.map((ext) => flatBase + ext),
|
||||
];
|
||||
// key 候选只按「生效模式」动态决定(无独立缓存层,见 _load 的缓存键)
|
||||
const bases = inputMode() === 'key'
|
||||
? [this.basePath + 'voice/' + gender + '/key/' + rest, genderBase, flatBase]
|
||||
: [genderBase, flatBase];
|
||||
if (hasExt) return bases;
|
||||
return bases.flatMap((b) => EXT_FALLBACKS.map((ext) => b + ext));
|
||||
}
|
||||
const base = this.basePath + id;
|
||||
if (hasExt) return [base];
|
||||
@@ -365,8 +378,11 @@ export class WebAudioEngine {
|
||||
* 资产缺失/无法解码时 console.error 并返回 null,不抛异常。
|
||||
*/
|
||||
_load(id) {
|
||||
// voice 层缓存键带性别:切换 settings.voice 后不会复用旧性别的缓存结果
|
||||
const key = id.startsWith('voice/') ? `${settings.get().voice}|${id}` : id;
|
||||
// voice 层缓存键带性别 + 输入模式(key 变体/主资产/平铺是三套不同音频):
|
||||
// 切换 settings.voice 或输入模式后不会复用旧缓存结果,自然从下一句生效
|
||||
const key = id.startsWith('voice/')
|
||||
? `${inputMode() === 'key' ? 'key' : 'main'}|${settings.get().voice}|${id}`
|
||||
: id;
|
||||
if (this._cache.has(key)) return this._cache.get(key);
|
||||
const p = (async () => {
|
||||
if (!this.ctx) return null;
|
||||
|
||||
Reference in New Issue
Block a user