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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+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() {
+6
View File
@@ -148,6 +148,8 @@ class MockAudioEngine {
this.ctx = new MockAudioContext();
this._sfxBus = { kind: 'sfxBus' };
}
isVoicePlaying() { return this._voice !== null; } // 对齐引擎公开 API(game 侧已改用它)
setRateBoostEnabled() {} // 对齐引擎公开 API(rpg 不调用,保持 mock 完整)
_dur(id) { return 1.0; } // 假 buffer 时长(秒):语音链按虚拟时钟推进
play(id, opts = {}) {
const layer = id.split('/')[0];
@@ -239,6 +241,10 @@ function vibratedError(fromIdx) {
// ================================ 游戏装配 ================================
const { RpgGame } = await import('../game.js');
const { settings } = await import('../../engine/settings.js');
// 向导层关闭:回归测试只验证纯游戏逻辑(guide='full' 的行为由 harness/guide.test.mjs 覆盖)
settings.set({ guide: 'off', voice: 'female', speed: 1.0, bgm: 1.0 });
const audioRegistry = [];
function newGame() {