fix(simon): 音序回响状态透明打磨(玩家代理取证迭代,玩法零改动)
- S2:新增input_open双音柔铃标记「能答了」边界(playback/input边界此前吞手势无标记) - S1/S3/S4:input阶段长按=状态查询(13句status_*分档整句:长度4档×进度4档×账目5档,两本账口径一致,不耗账不闭窗) - S5:announce/roundend非单击手势补error震(00 §5.4三秒反馈律) - S6 P0:gameover下滑进菜单→结算链死锁8.4s修复(结算看守+挂起补播) - S7:your_turn每局一次(原每轮重念) - 取证:live两会话+4人设评审,S清单见research/simon-polish/round1/findings.md - 验证:playtest 156/156 + guide 122 + 终检绿 + 14游戏回归零回退 + live复跑核对局14/14压力局8/8;状态感知2/5→4/5(四席同升),愿意玩改口为是 - 02号文档追加§9修订记录;新增input_open+13句status_*走完整TTS管线(flock串行QC无离群)
This commit is contained in:
+105
-12
@@ -40,6 +40,19 @@
|
||||
* 1→0 播 gd_chances_left0,替换该路径旧「用完了」绝对句,全档位)。
|
||||
* - H4 input 看门狗首级 10s→5s(升级级 25s 与次数不变;非 input 阶段不动)。
|
||||
*
|
||||
* round5(research/simon-polish/round1 取证 S1-S7 → round2 修复,状态透明层;玩法零改动):
|
||||
* - S2:input 开放标记音 sfx/input_open——confirm 震只标记"播放完毕",占用预告/
|
||||
* your_turn 语音链之后才真开放(此前作答被静默吞,live-A 行 108-115);input_open
|
||||
* 在 _toInput 播、游戏层资产不走向导层(guide=off/iOS 无震动也有听觉边界)。
|
||||
* 无语音链的轮次不再叠向导 turn earcon(input_open 独立作边界标记,二选一不叠响)。
|
||||
* - S1/S3/S4:input 阶段长按=状态查询(_statusQuery)——长度 4 档+进度 4 档+重听
|
||||
* 两账 5 档,13 句 status_* 整句预生成(禁运行时拼接);玩家主动查询,三档 guide 全可用。
|
||||
* - S5:announce/roundend 阶段非单击手势给 error 震(playback 维持静默豁免,02 §4.1)。
|
||||
* - S6:结算语音链阶段看守——gameover 瞬间进菜单则挂起,回结算屏补播(_deliverSettle);
|
||||
* 承诺「单击屏幕,再来一局」永远只在结算屏上播(live-B 行 140-174:链在菜单里
|
||||
* 播完 → 承诺是假的 + 关菜单后结算屏死寂 8.4s)。
|
||||
* - S7:your_turn 每局只播一次(第 1 轮每次重播都重念,live-B 行 67/96-98)。
|
||||
*
|
||||
* 手势 → 音色映射:
|
||||
* up 上行琶音 / down 下行低音 / left 左声道弹拨 / right 右声道弹拨 /
|
||||
* tap 底鼓 / doubletap 双连响
|
||||
@@ -77,6 +90,14 @@ const TEACH_BLUNT = {
|
||||
right: 'voice/t_blunt_right', tap: 'voice/t_blunt_tap', doubletap: 'voice/t_blunt_double',
|
||||
};
|
||||
|
||||
// 状态查询分档整句(round5 S1/S3/S4;游戏层资产三档 guide 全可用,全句预生成禁拼接)
|
||||
const STATUS_LEN = ['voice/status_len_short', 'voice/status_len_mid',
|
||||
'voice/status_len_long', 'voice/status_len_xlong']; // 2-4 / 5-8 / 9-12 / 13+ 音
|
||||
const STATUS_PROG = ['voice/status_prog_start', 'voice/status_prog_early',
|
||||
'voice/status_prog_half', 'voice/status_prog_late']; // 0 / ≤⅓ / ≤⅓-⅔ / 收尾
|
||||
const STATUS_ECO = ['voice/status_free2', 'voice/status_free1', 'voice/status_chance2',
|
||||
'voice/status_chance1', 'voice/status_out']; // 下一次重听消耗哪个桶(两账口径)
|
||||
|
||||
const BEST_KEY = 'soundgame.simon.best';
|
||||
const AUTH_KEY = 'soundgame.auth'; // 大厅登录写入(见 web/index.html)
|
||||
|
||||
@@ -115,13 +136,17 @@ export class SimonGame extends Game {
|
||||
this._recordSaid = false;
|
||||
// P2(round1 live):占用组合预告每局每种只播首次(答错时重播,见 _wrongAnswer)
|
||||
this._occAnnounced = new Set();
|
||||
// round5 S7:your_turn 每局只播一次(第 1 轮复活/重听重播不再重念)
|
||||
this._turnHintSaid = false;
|
||||
// round5 S6:待交付结算链(gameover 瞬间进菜单时挂起,回结算屏补播)
|
||||
this._settleChain = null;
|
||||
}
|
||||
|
||||
start() {
|
||||
// 预解码全部短音效与开场语音,消除首次播放延迟
|
||||
this.audio.prewarm([
|
||||
...GESTURES.map((g) => TONE[g]),
|
||||
'sfx/gameover',
|
||||
'sfx/gameover', 'sfx/input_open',
|
||||
'voice/t0_welcome', 'voice/t0_goal', 'voice/skip_hint', 'voice/t_try', 'voice/round_01',
|
||||
]);
|
||||
this._timer(() => this._runTutorial(), 500); // 开场静默 0.5s 后进教学(无 BGM)
|
||||
@@ -151,7 +176,11 @@ export class SimonGame extends Game {
|
||||
this._onMenuGesture(g);
|
||||
break;
|
||||
default:
|
||||
// playback/announce/roundend:纯聆听阶段手势静默忽略(02 §4.1)
|
||||
// playback/announce/roundend:纯聆听/播报阶段手势静默忽略(02 §4.1);
|
||||
// round5 S5:announce/roundend(非 playback)的误触给 error 震——00 §5.4
|
||||
// "做了手势 3 秒内必须有反馈",播报期被静默吞是"不知道自己在哪"的重灾区
|
||||
// (live-A 行 824-830:roundend 下滑开菜单零反馈;playback 维持静默豁免)。
|
||||
if (this.phase === 'announce' || this.phase === 'roundend') vibrate('error');
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -268,6 +297,8 @@ export class SimonGame extends Game {
|
||||
this.freeRelistens = 2; // 免费重听每局重置(round2 live P1)
|
||||
this._recordSaid = false;
|
||||
this._occAnnounced.clear(); // P2:新一局重新封顶(每种组合首次再播)
|
||||
this._turnHintSaid = false; // round5 S7
|
||||
this._settleChain = null; // round5 S6:上一局挂起的结算链随新局作废
|
||||
this._nextRound();
|
||||
}
|
||||
|
||||
@@ -345,8 +376,7 @@ export class SimonGame extends Game {
|
||||
} else {
|
||||
this._timer(() => {
|
||||
if (s !== this._session || this.phase !== 'playback') return;
|
||||
vibrate('confirm'); // "轮到你了"
|
||||
this.guide.turn({}); // turn earcon 每轮泛化(第 1 轮另有 your_turn 语音,下行)
|
||||
vibrate('confirm'); // "播放完毕"("现在能答"由 _toInput 的 input_open 标记)
|
||||
// H2(round4)→ round1 live P2 改口:占用预告每种组合每局只播首次
|
||||
//(_occAnnounced 记录;之后仅答错重听时随 retry_use 重播,见 _wrongAnswer)。
|
||||
// 重听键与答案音共用词表的结构性冲突,换手势无解,改为轮次开场讲清判据。
|
||||
@@ -355,12 +385,19 @@ export class SimonGame extends Game {
|
||||
if (this._occAnnounced.has(ids[0])) ids = [];
|
||||
else this._occAnnounced.add(ids[0]);
|
||||
}
|
||||
if (this.round === 1) ids.push('voice/your_turn');
|
||||
// round5 S7:your_turn 每局只播一次(第 1 轮复活/重听重播不再重念)
|
||||
if (this.round === 1 && !this._turnHintSaid) {
|
||||
ids.push('voice/your_turn');
|
||||
this._turnHintSaid = true;
|
||||
}
|
||||
if (ids.length) {
|
||||
// 有语音链:向导 turn earcon 打头,链尾由 _toInput 的 input_open 收尾
|
||||
this.guide.turn({});
|
||||
this._speakGuarded(ids, () => {
|
||||
if (s === this._session && this.phase === 'playback') this._toInput();
|
||||
}, 2500 * ids.length); // 语音缺失兜底:每句 2.5s
|
||||
} else {
|
||||
// round5 S2:无语音链时不叠 turn earcon——input_open 独立作"能答了"标记
|
||||
this._toInput();
|
||||
}
|
||||
}, 400); // 最后一个音播完 400ms 后
|
||||
@@ -385,6 +422,9 @@ export class SimonGame extends Game {
|
||||
_toInput() {
|
||||
this.phase = 'input';
|
||||
this.inputIdx = 0;
|
||||
// round5 S2:input 开放标记音(游戏层资产,三档 guide/iOS 全可闻)——语音链
|
||||
// (占用预告/your_turn)播完才进这里,此前作答被静默吞(live-A 行 108-115)。
|
||||
this.audio.play('sfx/input_open');
|
||||
// 向导层静默看门狗(round2 F1.2 礼仪重构 + round4 H4/H2):input 阶段无 BGM
|
||||
// 无超时,挂机永久静默。5s → nudge + gd_relisten 提示(H4:首级 10s→5s,
|
||||
// P4「10 秒干等」;升级级 25s 与级数不变);25s 升级**取消免扣机会自动重播
|
||||
@@ -409,10 +449,17 @@ export class SimonGame extends Game {
|
||||
this._relisten();
|
||||
return;
|
||||
}
|
||||
// 未支持手势兜底(round1 live P1:答题阶段长按 9 秒零反馈,玩家以为卡死)——
|
||||
// 即时 error 震,不判错、不耗机会、不进重听。此前 TONE['longpress'] 为
|
||||
// undefined,audio.play(undefined) 直接抛 TypeError(事件回调内静默崩溃,
|
||||
// 表象正是"手势被吞")。longpressend 是同一动作的收尾,静默不重复反馈。
|
||||
// round5 S1/S3/S4:input 阶段长按=状态查询(长度/进度/重听两账,分档整句)——
|
||||
// 长按是六手势之外的唯一空闲键位(键盘=按住空格),玩家主动查询不耗账不判错;
|
||||
// longpressend 是同一动作收尾,静默不重复反馈。
|
||||
if (g === 'longpress') {
|
||||
this._statusQuery();
|
||||
return;
|
||||
}
|
||||
// 其余未支持手势兜底(round1 live P1:无效手势零反馈,玩家以为卡死)——
|
||||
// 即时 error 震,不判错、不耗机会、不进重听(此前 audio.play(undefined) 直接
|
||||
// 抛 TypeError,事件回调内静默崩溃,表象正是"手势被吞")。longpressend 是
|
||||
// 长按的收尾事件,静默不重复反馈(长按本体已在上面走了查询路径)。
|
||||
if (!TONE[g]) {
|
||||
if (g !== 'longpressend') vibrate('error');
|
||||
return;
|
||||
@@ -462,6 +509,27 @@ export class SimonGame extends Game {
|
||||
this._speakGuarded(ids, () => { if (s === this._session) this._playSequence(); });
|
||||
}
|
||||
|
||||
/**
|
||||
* round5 S1/S3/S4:input 阶段长按=问状态(status_* 13 句整句预生成,数字分档,
|
||||
* 禁运行时拼接——00 §3.2)。三句链:本轮长度档(2-4/5-8/9-12/13+ 音)→ 已答
|
||||
* 进度档(0/≤⅓/≤⅔/收尾)→ 重听账目档(下一次重听消耗哪个桶:免费 2/免费 1/
|
||||
* 机会 2/机会 1/两账空)。不耗账、不判错、不清进度;答错消耗路径与主动重听
|
||||
* 路径的分账口径与 _relisten 一致(归因真话)。教学不讲这句——查询是发现式
|
||||
* 辅助(旁白只承诺"记不住就向上滑重听"),主动查询的玩家三档 guide 都能用
|
||||
* (属玩家发起,不走向导插播通道)。
|
||||
*/
|
||||
_statusQuery() {
|
||||
const len = this.sequence.length;
|
||||
const idx = this.inputIdx;
|
||||
vibrate('confirm'); // 收到查询:轻确认震(longpress 无引擎内置震动)
|
||||
const lenIdx = len <= 4 ? 0 : len <= 8 ? 1 : len <= 12 ? 2 : 3;
|
||||
const progIdx = idx === 0 ? 0 : idx * 3 <= len ? 1 : idx * 3 <= len * 2 ? 2 : 3;
|
||||
const ecoIdx = this.freeRelistens >= 2 ? 0 : this.freeRelistens === 1 ? 1
|
||||
: this.chances >= 2 ? 2 : this.chances === 1 ? 3 : 4;
|
||||
// 相位保持 input:查询语音不闭作答窗口(玩家可以边听边继续答)。
|
||||
this._speakGuarded([STATUS_LEN[lenIdx], STATUS_PROG[progIdx], STATUS_ECO[ecoIdx]], null, 7500);
|
||||
}
|
||||
|
||||
_roundClear() {
|
||||
this.phase = 'roundend';
|
||||
const s = this._session;
|
||||
@@ -527,8 +595,15 @@ export class SimonGame extends Game {
|
||||
const ids = ['voice/gameover', this._scoreBandVoice(score)];
|
||||
if (isRecord) ids.push('voice/new_record');
|
||||
ids.push('voice/replay', 'voice/rules_prompt');
|
||||
// round5 S6 阶段看守:结算链只在结算屏交付——gameover 瞬间下滑进菜单时
|
||||
// 链挂起(_deliverSettle 见相位不对不播),回结算屏补播;承诺「单击屏幕,
|
||||
// 再来一局」永远与所处阶段一致(live-B 行 140-174:链在菜单里播完 → 承诺
|
||||
// 在菜单内是假的 → 关菜单后结算屏死寂 8.4s)。重开(_beginGame)作废挂起链。
|
||||
this._settleChain = ids;
|
||||
this._timer(() => {
|
||||
if (s === this._session) this._speak(ids, null); // 链播完停在 gameover,等手势
|
||||
if (s !== this._session) return;
|
||||
this.audio.stopVoice(); // 抢占点火:整链替换重播,listen/ASR 窗口不碎
|
||||
this._deliverSettle();
|
||||
}, 1400);
|
||||
|
||||
// 状态 BGM(10 号文档 simon 表):sfx/gameover(约 0.95s,+700ms 起播)
|
||||
@@ -550,6 +625,24 @@ export class SimonGame extends Game {
|
||||
return 'voice/score_band6';
|
||||
}
|
||||
|
||||
/**
|
||||
* round5 S6:交付结算语音链。只在结算屏(phase==='gameover')播;在菜单则
|
||||
* 挂起(_settleChain 保留),回结算屏(_backToSettle)再补播——链内承诺
|
||||
* (单击重开/上滑规则/下滑菜单)只对听得见它们的相位负责。
|
||||
*/
|
||||
_deliverSettle() {
|
||||
if (this.phase !== 'gameover' || !this._settleChain) return;
|
||||
const ids = this._settleChain;
|
||||
this._settleChain = null;
|
||||
this._speak(ids, null);
|
||||
}
|
||||
|
||||
/** 菜单返回结算页:补挂起的结算链(S6),并落相位。 */
|
||||
_backToSettle() {
|
||||
this.phase = 'gameover';
|
||||
this._deliverSettle();
|
||||
}
|
||||
|
||||
_speakRules() {
|
||||
this._speak(['voice/rules', 'voice/rules_prompt'], null);
|
||||
}
|
||||
@@ -578,12 +671,12 @@ export class SimonGame extends Game {
|
||||
} else if (g === 'up') {
|
||||
this._speakRules();
|
||||
} else if (g === 'down') {
|
||||
this.phase = 'gameover'; // 下滑=关闭菜单回结算页(13 号文档 §7.5 菜单约定)
|
||||
this._backToSettle(); // 下滑=关闭菜单回结算页(13 号文档 §7.5 菜单约定;S6 补播结算链)
|
||||
} else if (g === 'doubletap') {
|
||||
const sel = this._menuIdx;
|
||||
if (sel === 0) this._restart();
|
||||
else if (sel === 1) { this.stop(); window.location.href = '../'; }
|
||||
else this.phase = 'gameover'; // 返回:回到结算页
|
||||
else this._backToSettle(); // 返回:回到结算页(S6 补播结算链)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user