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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+32
-1
@@ -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);
|
||||
|
||||
@@ -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; // 手势注入目标(第二场景换新实例时切换)
|
||||
|
||||
Reference in New Issue
Block a user