guide层v1: 全局向导交互层落地(状态签名earcon/turn cue/静默看门狗/纠错) + 6游戏接线 + 8句向导语音 + 测试全绿

- engine/guide.js 运行时合成8个签名earcon(方法论L2硬参数),off档零定时器,插播queue不顶对白
- settings: guide三档(full/standard/off默认full) + 语速2.2视障档;audio: isVoicePlaying()/setRateBoostEnabled() 清两个已知问题
- 6游戏按schemes§2接线: storyline看门狗+无效手势纠错、simon免扣重听看门狗、escape turn cue+提前卡关提示、rhythm gd_answer/gd_miss+结算看门狗+快进豁免、space首撞指令提示+结算看门狗、rpg格挡纠错+60s催促+状态签名
- 大厅设置面板+向导文档(14-guide-layer.md)+00-global-ux §6;R5台词表补4条一致性
- 试玩转写含向导事件;6 playtest(guide=off断言零改动)+guide.test(34断言)+资产终检全绿
This commit is contained in:
SoundGame Dev
2026-08-30 22:46:05 +08:00
parent 7eb9e7f025
commit 79dffac01e
69 changed files with 2466 additions and 144 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+32 -1
View File
@@ -33,6 +33,7 @@
* 语义震动(beat/confirm/success/error/warning/pickup/hit)。
*/
import { Game } from '../engine/game.js';
import { Guide } from '../engine/guide.js';
import { vibrate } from '../engine/haptics.js';
const GESTURES = ['up', 'down', 'left', 'right', 'tap', 'doubletap'];
@@ -63,9 +64,15 @@ const TEACH_BLUNT = {
const BEST_KEY = 'soundgame.simon.best';
const AUTH_KEY = 'soundgame.auth'; // 大厅登录写入(见 web/index.html)
// 向导层(schemes.md §2):input 阶段静默看门狗——10s 轻提醒+gd_relisten(不改变
// 重听扣机会规则),25s 免扣机会自动重听一次(每局 2 次)
const GUIDE_INPUT_WATCHDOG_AT = [10000, 25000];
const GUIDE_AUTO_RELISTEN_MAX = 2;
export class SimonGame extends Game {
constructor(audio, root) {
super(audio, root);
this.guide = new Guide(audio); // 全局向导层(guide='off' 时全部静默 no-op)
this.phase = 'idle'; // idle | tutorial | announce | playback | input | roundend | gameover | menu
this.sequence = [];
this.round = 0; // 当前轮次(1 起),序列长度 = round + 1
@@ -80,6 +87,7 @@ export class SimonGame extends Game {
this._menuIdx = 0;
this._session = 0; // 会话令牌:重开后使旧计时器/语音链回调失效
this._timers = [];
this._guideRelistens = 0; // 向导免机会自动重听已用次数(每局重置)
}
start() {
@@ -93,6 +101,7 @@ export class SimonGame extends Game {
}
onGesture(g) {
this.guide.disarm(); // 任何手势复位 input 静默看门狗(guide=off 时 no-op)
// 单击跳过旁白(12 号文档):voice 播放中单击 = skipVoice()(截断当前句、
// 链播自然进下一句);无语音在播时单击 = 各阶段原语义。跳过不发震动。
if (g === 'tap' && this._tapSkipsVoice()) {
@@ -129,7 +138,7 @@ export class SimonGame extends Game {
* 读引擎的 _voice 字段(只读)判断是否有对白在播;无公开 API,属既定约定。
*/
_tapSkipsVoice() {
if (!this.audio._voice) return false;
if (!this.audio.isVoicePlaying()) return false; // 公开 API(原读私有 _voice,清点 §0)
switch (this.phase) {
case 'tutorial': return !this.teachAwait;
case 'announce':
@@ -224,6 +233,7 @@ export class SimonGame extends Game {
this.sequence = [];
this.round = 0;
this.chances = 1;
this._guideRelistens = 0; // 向导免机会自动重听每局重置
this._nextRound();
}
@@ -302,6 +312,7 @@ export class SimonGame extends Game {
this._timer(() => {
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: () => {
@@ -321,6 +332,24 @@ export class SimonGame extends Game {
_toInput() {
this.phase = 'input';
this.inputIdx = 0;
// 向导层静默看门狗(schemes §2,全 6 款最危险的静默等待):input 阶段无 BGM
// 无超时,挂机永久静默。10s → nudge + gd_relisten 提示(不改变重听扣机会
// 规则);25s → 免扣机会自动重听序列一次(每局 2 次),防挂机死等。
this.guide.watchdog({
arm: true,
at: GUIDE_INPUT_WATCHDOG_AT,
say: ['gd_relisten', null],
onEscalate: (n) => { if (n === 2) this._guideAutoRelisten(); },
cond: () => this.phase === 'input',
});
}
/** 向导兜底:免扣机会自动重听当前序列(每局最多 2 次,序列不变进度重输)。 */
_guideAutoRelisten() {
if (this._guideRelistens >= GUIDE_AUTO_RELISTEN_MAX) return;
this._guideRelistens++;
if (this.phase !== 'input') return;
this._playSequence();
}
_onInput(g) {
@@ -392,6 +421,7 @@ export class SimonGame extends Game {
_gameOver() {
this.phase = 'gameover';
this.guide.state('settle'); // 状态签名:结算(与下方 bgm/menu 同点)
const s = this._session;
const score = this.round - 1; // 已完成的轮数
const isRecord = score > this.best;
@@ -441,6 +471,7 @@ export class SimonGame extends Game {
_openMenu() {
this._menuIdx = 0;
this.phase = 'menu';
this.guide.state('menu'); // 状态签名:菜单(与下方 bgm/menu 同点)
// 状态 BGM:菜单挂 menu pad(volume 0.3,淡入 600ms;与结算同一首,引擎不重启)
this.audio.play('bgm/menu', { volume: 0.3, fadeMs: 600 });
this._speak(['voice/menu_open', 'voice/menu_restart'], null);
+3
View File
@@ -212,6 +212,9 @@ const { WebAudioEngine } = await import('../../engine/audio.js');
const { settings } = await import('../../engine/settings.js');
const { SimonGame } = await import('../game.js');
// 向导层关闭:回归测试只验证纯游戏逻辑(guide='full' 的行为由 harness/guide.test.mjs 覆盖)
settings.set({ guide: 'off', voice: 'female', speed: 1.0, bgm: 1.0 });
const audio = new WebAudioEngine('audio/');
const game = new SimonGame(audio, fakeEl());
let gameRef = game; // 手势注入目标(第二场景换新实例时切换)