round4: H1 escape目标带实时方位 + H2 simon轮次占用预告(根治手势两义) + H3 重听账目真实化(诊断=初始发放1次vs承诺2次) + H4 input看门狗5s + H5 归因真话(gd_miss_none) + 20语音 + guide120断言全绿

This commit is contained in:
SoundGame Dev
2026-08-31 02:51:41 +08:00
parent 6be3d29b78
commit 3d37d4656d
55 changed files with 822 additions and 215 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+57 -18
View File
@@ -26,6 +26,14 @@
* 答题输入的 00 §5.3 豁免同款)。非输入阶段行为不变(gameover/菜单上滑仍是规则/
* 帮助)。同一手势在 input 阶段不再有第二种含义(P1 round2 P0 根治)。
*
* round4(docs/design/14-guide-layer.md「round4 v1.3」,synthesis H2~H4):
* - H2 占用手势预告:每轮序列播放完成后、「轮到你」提示前,序列含 up/doubletap
* 则播 gd_occ_up/dt/both(游戏反馈层全档位,插在轮次链中);被占用轮次 input
* 看门狗只发 nudge earcon 不播重听建议句(say 动态置空)。
* - H3 重听账目真实:主动重听使用时如实报余量(2→1 播 gd_chances_left1、
* 1→0 播 gd_chances_left0,替换该路径旧「用完了」绝对句,全档位)。
* - H4 input 看门狗首级 10s→5s(升级级 25s 与次数不变;非 input 阶段不动)。
*
* 手势 → 音色映射:
* up 上行琶音 / down 下行低音 / left 左声道弹拨 / right 右声道弹拨 /
* tap 底鼓 / doubletap 双连响
@@ -66,10 +74,11 @@ const TEACH_BLUNT = {
const BEST_KEY = 'soundgame.simon.best';
const AUTH_KEY = 'soundgame.auth'; // 大厅登录写入(见 web/index.html)
// 向导层(14 号文档 + round2 synthesis F1.2):input 阶段静默看门狗——10s 轻提醒
// +gd_relisten(不改变重听扣机会规则);25s 升级取消自动重播序列(P1:泄题且与
// 扣机会规则矛盾)→ 改为 confirm 震 + turn earcon(纯标记,无语音)
const GUIDE_INPUT_WATCHDOG_AT = [10000, 25000];
// 向导层(14 号文档 + round2 synthesis F1.2 + round4 H4):input 阶段静默看门狗
// ——首级 5s 轻提醒 +gd_relisten(H4 round4:10s→5s,P4「10 秒干等」;升级级
// 不变、次数不变);25s 升级取消自动重播序列(P1:泄题且与扣机会规则矛盾)→
// 改为 confirm 震 + turn earcon(纯标记,无语音)
const GUIDE_INPUT_WATCHDOG_AT = [5000, 25000];
export class SimonGame extends Game {
constructor(audio, root) {
@@ -321,13 +330,15 @@ export class SimonGame extends Game {
if (s !== this._session || this.phase !== 'playback') return;
vibrate('confirm'); // "轮到你了"
this.guide.turn({}); // turn earcon 每轮泛化(第 1 轮另有 your_turn 语音,下行)
if (this.round === 1) {
this.audio.play('voice/your_turn', {
onComplete: () => {
if (s === this._session && this.phase === 'playback') this._toInput();
},
});
this._timer(() => { if (this.phase === 'playback') this._toInput(); }, 2500); // 兜底
// H2(round4):序列播放完成后、「轮到你」提示前,按本轮序列播占用预告
//(gd_occ_up/dt/both,游戏反馈层全档位,插在轮次链中)——重听键与答案音
// 共用词表的结构性冲突,换手势无解,改为轮次开场讲清判据(P1#1+P2)。
const ids = this._occupancyVoices();
if (this.round === 1) ids.push('voice/your_turn');
if (ids.length) {
this._speakGuarded(ids, () => {
if (s === this._session && this.phase === 'playback') this._toInput();
}, 2500 * ids.length); // 语音缺失兜底:每句 2.5s
} else {
this._toInput();
}
@@ -337,17 +348,33 @@ export class SimonGame extends Game {
step();
}
/**
* H2(round4):本轮占用预告句。序列含 up → gd_occ_up、含 doubletap →
* gd_occ_dt、两者皆有 → gd_occ_both;都不含则不播(空链直达 input)。
*/
_occupancyVoices() {
const hasUp = this.sequence.includes('up');
const hasDt = this.sequence.includes('doubletap');
if (hasUp && hasDt) return ['voice/gd_occ_both'];
if (hasUp) return ['voice/gd_occ_up'];
if (hasDt) return ['voice/gd_occ_dt'];
return [];
}
_toInput() {
this.phase = 'input';
this.inputIdx = 0;
// 向导层静默看门狗(round2 F1.2 礼仪重构):input 阶段无 BGM 无超时,挂机
// 永久静默。10s → nudge + gd_relisten 提示(不改变重听扣机会规则);25s 升级
// **取消免扣机会自动重播序列**(P1:等答案自动重念,双击重听的经济就矛盾了,
// 还顺手拆了记忆难度)→ 只发 confirm 震 + turn earcon(纯标记,无语音)。
// 向导层静默看门狗(round2 F1.2 礼仪重构 + round4 H4/H2):input 阶段无 BGM
// 无超时,挂机永久静默。5s → nudge + gd_relisten 提示(H4:首级 10s→5s,
// P4「10 秒干等」;升级级 25s 与级数不变);25s 升级**取消免扣机会自动重播
// 序列**(P1:等答案自动重念,双击重听的经济就矛盾了,还顺手拆了记忆难度)
// → 只发 confirm 震 + turn earcon(纯标记,无语音)。
// H2:本轮 up 已被占用为作答时,重听建议句不再建议上滑(实现从简:被占用
// 轮次看门狗只发 nudge earcon 不播重听建议句)。
this.guide.watchdog({
arm: true,
at: GUIDE_INPUT_WATCHDOG_AT,
say: ['gd_relisten', null],
say: [this.sequence.includes('up') ? null : 'gd_relisten', null],
onEscalate: (n) => { if (n === 2) this.guide.turn({ haptic: true }); },
cond: () => this.phase === 'input',
});
@@ -371,7 +398,18 @@ export class SimonGame extends Game {
if (this.inputIdx >= this.sequence.length) this._roundClear();
}
/** 主动重听:消耗一次机会,重播当前序列(进度清零重输,序列不变)。 */
/**
* 主动重听:消耗一次机会,重播当前序列(进度清零重输,序列不变)。
* H3(round4):使用时如实报余量——免费重听第 1 次用掉(余 1)播
* gd_chances_left1「免费重听还剩一次。」、第 2 次用掉(余 0)播
* gd_chances_left0「免费重听用完了。之后重听会花一次机会,别怕,慢慢来。」
* (游戏档全级别,替换本路径旧「用完了」绝对句;P1:教学说两次、用一次即报
* 用完了——账目不可知)。账目诊断:教学/演练/看门狗均无隐性扣减(watchdog
* 提示不消耗机会,教学期不触 chances),缺口在「免费两次」中的第二次要打完
* 第 4 轮才发放(每 4 轮 +1、上限 2)——「用完了」因此只在真实余量为 0 时出现。
* 答错自动消耗路径(retry_use + chances_out)与 0 次时再求重听的拒绝路径
* (chances_out)是「答错可以消耗机会重听」的机会账,语义如实、维持不变。
*/
_relisten() {
if (this.chances <= 0) {
vibrate('error');
@@ -381,7 +419,8 @@ export class SimonGame extends Game {
this.chances--;
vibrate('pickup');
const ids = ['voice/relisten'];
if (this.chances === 0) ids.push('voice/chances_out'); // 机会归 0 提示
if (this.chances === 1) ids.push('voice/gd_chances_left1'); // 余 1:还剩一次
else if (this.chances === 0) ids.push('voice/gd_chances_left0'); // 余 0:用完了
const s = this._session;
this.phase = 'roundend'; // 重听播报期间暂停接收输入
this._speakGuarded(ids, () => { if (s === this._session) this._playSequence(); });