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
+20 -2
View File
@@ -33,6 +33,7 @@
*/
import { Game } from '../engine/game.js';
import { Guide } from '../engine/guide.js';
import { vibrate } from '../engine/haptics.js';
// ================================ 数据表 ================================
@@ -115,6 +116,7 @@ const PREWARM_SFX = [
export class RpgGame extends Game {
constructor(audio, root) {
super(audio, root);
this.guide = new Guide(audio); // 全局向导层(guide='off' 时全部静默 no-op)
this.phase = 'idle'; // idle | ask | intro | explore | shop | battle | menu | over
this._session = 0; // 会话令牌:大跳转时作废旧计时器与语音链
this._timers = [];
@@ -220,6 +222,7 @@ export class RpgGame extends Game {
// ---------------- 手势 ----------------
onGesture(g) {
this.guide.disarm(); // 任何手势复位 PLAYER_SELECT 看门狗(guide=off 时 no-op)
switch (this.phase) {
case 'ask':
if (g === 'tap') { const s = this._loadSave(); if (s) this._continueGame(s); else this._startNewGame(false); }
@@ -496,6 +499,7 @@ export class RpgGame extends Game {
this.phase = 'menu';
this._menuIdx = 0;
this.audio.play('bgm/menu'); // 模态菜单状态音乐(10 文档 §7:amb 继续,"游戏还在,界面开了")
this.guide.state('menu'); // 状态签名:菜单(与状态 BGM 同点)
this._speak(['voice/sys_menu', `voice/${MENU_ITEMS[0]}`]);
}
@@ -603,6 +607,14 @@ export class RpgGame extends Game {
b.state = 'PLAYER_SELECT';
// 玩家回合的听觉标点(11 文档 §5):turn_player(≤0.4s)→ 留 200ms+ 间隙 → 指令名
this.audio.play('sfx/turn_player');
// 向导层静默看门狗(schemes §2):探索/常规战斗本无卡关计时,PLAYER_SELECT
// 挂机永远静默等——60s 无输入 → nudge + gd_stall(一级即止,自动停止)
this.guide.watchdog({
arm: true,
at: [60000],
say: ['gd_stall'],
cond: () => this.phase === 'battle' && this._battle === b && b.state === 'PLAYER_SELECT',
});
const cmdIdx = b.cmdIdx;
this._after(650, () => {
if (this._battle === b && b.state === 'PLAYER_SELECT' && b.cmdIdx === cmdIdx) {
@@ -674,6 +686,9 @@ export class RpgGame extends Game {
this._enemyResolve();
} else {
vibrate('error'); // 窗口内做错手势:留在窗口
// 向导层(schemes §2):窗口内做错原本仅震动无语音(清点 §6.4 薄弱点)
// → full 档 gd_parry_miss(已震 error,skipHaptic 免重复;onceKey 每局一次)
this.guide.error({ voice: 'gd_parry_miss', onceKey: 'rpg_parry_miss', skipHaptic: true });
}
}
// 其他战斗状态:手势忽略
@@ -931,6 +946,7 @@ export class RpgGame extends Game {
this._save();
this._stopAmb();
this.audio.play('bgm/ending');
this.guide.state('settle'); // 状态签名:章结算(与状态 BGM 同点)
this._speakGuarded(['voice/evt_ch1_end'], () => {
this._enterScene('village');
}, 15000);
@@ -946,12 +962,14 @@ export class RpgGame extends Game {
this._playAmb(`sfx/${SCENES[sceneId].amb}`);
if (sceneId === 'village') this.audio.play('bgm/village', { fadeMs });
else this.audio.play('bgm/explore', { volume: 0.35, fadeMs });
this.guide.state('explore'); // 状态签名:探索(与状态 BGM 同点)
}
/** 战斗音频:普通 bgm/battle / Boss bgm/boss,400ms 快切(urgency,10 文档 §7);amb 停。 */
_switchBattleAudio(isBoss) {
this._stopAmb();
this.audio.play(isBoss ? 'bgm/boss' : 'bgm/battle', { fadeMs: 400 });
this.guide.state('battle'); // 状态签名:战斗(与状态 BGM 同点)
}
/**
@@ -1050,8 +1068,8 @@ export class RpgGame extends Game {
// ---------------- 工具 ----------------
/** 当前是否有对白在播(引擎 voice token;探索态单击=skipVoice 的语境判定,12 文档 §1)。 */
_voicePlaying() { return !!this.audio._voice; }
/** 当前是否有对白在播(探索态单击=skipVoice 的语境判定,12 文档 §1)。 */
_voicePlaying() { return this.audio.isVoicePlaying(); } // 公开 API(原读私有 _voice,清点 §0)
/** R6 首战教学链(tut_battle_1..5)全周期只播一次:跨存档持久化,二周目起跳过。 */
_tutIntroSeen() {