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:
+27
-5
@@ -31,6 +31,7 @@
|
||||
* (0,2) 储物间·amb_storage (1,2) 空房·amb_wind (2,2) 大门·amb_gate(通电后 amb_gate_live)
|
||||
*/
|
||||
import { Game } from '../engine/game.js';
|
||||
import { Guide } from '../engine/guide.js';
|
||||
import { vibrate } from '../engine/haptics.js';
|
||||
|
||||
const Phase = {
|
||||
@@ -76,6 +77,7 @@ const AUTH_KEY = 'soundgame.auth'; // 大厅登录写入(见 web/index.html)
|
||||
export class EscapeGame extends Game {
|
||||
constructor(audio, root) {
|
||||
super(audio, root);
|
||||
this.guide = new Guide(audio); // 全局向导层(guide='off' 时全部静默 no-op)
|
||||
this._session = 0; // 会话令牌:重开/跳过时使旧计时器与语音链回调失效
|
||||
this._timers = [];
|
||||
this._stuckTimer = null;
|
||||
@@ -103,6 +105,7 @@ export class EscapeGame extends Game {
|
||||
this._lastProgressAt = Date.now();
|
||||
this._stuckShown = new Set(); // 当前进展段内已自动播过的级别 'L1'|'L2'|'L3'
|
||||
this._lastL3At = 0;
|
||||
this._wallBumps = 0; // 连续撞墙计数(向导层提前一级提示用)
|
||||
// 菜单
|
||||
this._menuReturn = Phase.ENDING;
|
||||
this._menuIdx = 0;
|
||||
@@ -118,11 +121,16 @@ export class EscapeGame extends Game {
|
||||
this._clearTimers();
|
||||
this._startStuckTimer();
|
||||
this.audio.play('bgm/drone');
|
||||
this.guide.state('explore'); // 状态签名:探索(与状态 BGM 同点)
|
||||
const guided = localStorage.getItem(GUIDE_DONE_KEY) === '1';
|
||||
if (guided) {
|
||||
// 二周目:上滑重听引导 / 单击直接开始
|
||||
this.phase = Phase.REOPEN;
|
||||
this._playVoice('voice/reopen_prompt', { skippable: false }); // 抉择前提示必须听完
|
||||
// 抉择前提示必须听完;播完给 turn cue(向导层:REOPEN 静默等待的"轮到你")
|
||||
this._playVoice('voice/reopen_prompt', {
|
||||
skippable: false,
|
||||
onComplete: () => this.guide.turn({}),
|
||||
});
|
||||
} else {
|
||||
this._startGuide();
|
||||
}
|
||||
@@ -130,6 +138,7 @@ export class EscapeGame extends Game {
|
||||
|
||||
onGesture(g) {
|
||||
console.log('[EscapeGame] gesture:', g, 'phase=', this.phase, 'pos=', this.x, this.y);
|
||||
this.guide.disarm(); // 任何手势复位向导状态(guide=off 时 no-op)
|
||||
switch (this.phase) {
|
||||
case Phase.REOPEN:
|
||||
if (g === G.UP) { this.audio.stopVoice(); this._startGuide(); }
|
||||
@@ -183,6 +192,7 @@ export class EscapeGame extends Game {
|
||||
this.guideErrs = 0;
|
||||
this.guideSwipes = 0;
|
||||
this._guideBusy = false;
|
||||
this.guide.turn({}); // turn cue:引导步骤解锁输入(W2 播完即静默,向导补标记)
|
||||
if (onDone) onDone();
|
||||
};
|
||||
this._timer(() => {
|
||||
@@ -309,6 +319,7 @@ export class EscapeGame extends Game {
|
||||
this.startElapsedMs = Date.now();
|
||||
this._resetStuck();
|
||||
this.audio.play('bgm/drone');
|
||||
this.guide.state('explore');
|
||||
this._playVoice('voice/replay');
|
||||
this._playAmbient();
|
||||
}
|
||||
@@ -325,7 +336,7 @@ export class EscapeGame extends Game {
|
||||
// voice 播放中:可跳句单击 = 跳过本句(链播继续);不可跳句(关键获得/
|
||||
// L3 带路)单击 = 忽略——若照旧触发检查,800ms 后的描述会顶掉该句。
|
||||
// 无 voice 时单击 = 原检查语义。
|
||||
if (this.audio._voice) {
|
||||
if (this.audio.isVoicePlaying()) {
|
||||
if (this._voiceSkippable) this.audio.skipVoice();
|
||||
break;
|
||||
}
|
||||
@@ -342,6 +353,7 @@ export class EscapeGame extends Game {
|
||||
this.audio.play('sfx/wall');
|
||||
vibrate('error');
|
||||
this._playVoice('voice/wall'); // 每次撞墙都播(取消一次性限制)
|
||||
this._wallBumps++; // 向导层:连续撞墙 = 卡关信号(_checkStuck 提前一级)
|
||||
return;
|
||||
}
|
||||
this.x = nx;
|
||||
@@ -469,6 +481,7 @@ export class EscapeGame extends Game {
|
||||
this._lastProgressAt = Date.now();
|
||||
this._stuckShown = new Set();
|
||||
this._lastL3At = 0;
|
||||
this._wallBumps = 0;
|
||||
}
|
||||
|
||||
/** 进展事件:重置卡关计时与已播级别。 */
|
||||
@@ -480,7 +493,13 @@ export class EscapeGame extends Game {
|
||||
if (this.phase !== Phase.PLAY) return;
|
||||
const idleMs = Date.now() - this._lastProgressAt;
|
||||
const stage = this._stage();
|
||||
if (idleMs >= 180000) {
|
||||
// 向导层(schemes §2):连续撞墙 ≥2 次是明确的卡关信号——向导 full 档时
|
||||
// 三级阈值整体提前一级(L1 即刻、L2 60s、L3 120s);off/standard 保持原阈值。
|
||||
const early = this._wallBumps >= 2 && this.guide.level() === 'full';
|
||||
const T_L1 = early ? 0 : 60000;
|
||||
const T_L2 = early ? 60000 : 120000;
|
||||
const T_L3 = early ? 120000 : 180000;
|
||||
if (idleMs >= T_L3) {
|
||||
// L3 带路:首次或每 60s 重复,按当前坐标重算路线(带路语音不可单击跳过)
|
||||
if (!this._stuckShown.has('L3') || Date.now() - this._lastL3At >= 60000) {
|
||||
this._stuckShown.add('L3');
|
||||
@@ -488,11 +507,11 @@ export class EscapeGame extends Game {
|
||||
vibrate('warning');
|
||||
this._speak([`voice/stuck_${stage}_3`, ...this._navVoices(STAGES[stage].target)], null, { skippable: false });
|
||||
}
|
||||
} else if (idleMs >= 120000 && !this._stuckShown.has('L2')) {
|
||||
} else if (idleMs >= T_L2 && !this._stuckShown.has('L2')) {
|
||||
this._stuckShown.add('L2');
|
||||
vibrate('warning');
|
||||
this._speak([`voice/stuck_${stage}_2`]);
|
||||
} else if (idleMs >= 60000 && !this._stuckShown.has('L1')) {
|
||||
} else if (idleMs >= T_L1 && !this._stuckShown.has('L1')) {
|
||||
this._stuckShown.add('L1');
|
||||
vibrate('warning');
|
||||
this._speak(['voice/stuck_ask', `voice/stuck_${stage}_1`]);
|
||||
@@ -518,6 +537,7 @@ export class EscapeGame extends Game {
|
||||
// 状态 BGM:菜单 = bgm/menu(10 号文档,400ms crossfade;与结算 pad 同音量 0.5,
|
||||
// 同一首引擎不重启,关菜单回 ENDING 时无缝保持)
|
||||
this.audio.play('bgm/menu', { volume: 0.5, fadeMs: 400 });
|
||||
this.guide.state('menu');
|
||||
this._speak(['voice/menu_open', 'voice/menu_restart'], null, { skippable: false });
|
||||
}
|
||||
|
||||
@@ -546,6 +566,7 @@ export class EscapeGame extends Game {
|
||||
this.audio.play('bgm/menu', { volume: 0.5 });
|
||||
} else {
|
||||
this.audio.play('bgm/drone'); // 关闭菜单切回主题曲
|
||||
this.guide.state('explore');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,6 +584,7 @@ export class EscapeGame extends Game {
|
||||
this.audio.play('sfx/gate_open');
|
||||
this.audio.play('sfx/win_chime', { volume: 0.8 });
|
||||
this.audio.stopBgm(2500);
|
||||
this.guide.state('settle'); // 状态签名:结算(逃脱)
|
||||
this._endingBgmStarted = false;
|
||||
const totalSec = Math.floor((Date.now() - this.startElapsedMs) / 1000);
|
||||
this._saveEscape(totalSec);
|
||||
|
||||
@@ -175,6 +175,9 @@ const { WebAudioEngine } = await import('../../engine/audio.js');
|
||||
const { settings } = await import('../../engine/settings.js');
|
||||
const { EscapeGame } = await import('../game.js');
|
||||
|
||||
// 向导层关闭:回归测试只验证纯游戏逻辑(guide='full' 的行为由 harness/guide.test.mjs 覆盖)
|
||||
settings.set({ guide: 'off', voice: 'female', speed: 1.0, bgm: 1.0 });
|
||||
|
||||
// ---------- 断言与辅助 ----------
|
||||
let failures = 0;
|
||||
let passes = 0;
|
||||
|
||||
Reference in New Issue
Block a user