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.
+26
-3
@@ -30,8 +30,14 @@
|
||||
*
|
||||
* 语音 id 权威清单:docs/design/04-space-voice-ids.md(52 句双性别);
|
||||
* 旧 num 拼接件、n_ 前缀件、intro1-4/hit1/hit2/gameover/restart 全部弃用。
|
||||
*
|
||||
* 向导层(research/03-schemes/schemes.md §2):对局第 1 次受击(保护期外)追加
|
||||
* 指令式 gd_hit_hint(归因句 hit_<dir> 之外的"该怎么拨");结算链换
|
||||
* _speakGuarded(资产缺失不卡结算 BGM);长按快进对局期豁免
|
||||
* (setRateBoostEnabled);状态签名 earcon 与状态 BGM 同点。
|
||||
*/
|
||||
import { Game } from '../engine/game.js';
|
||||
import { Guide } from '../engine/guide.js';
|
||||
import { vibrate } from '../engine/haptics.js';
|
||||
|
||||
// ---- 方位表:来袭方向 -> 逼近音效 id(闪避手势 = 来袭方向本身,同向) ----
|
||||
@@ -88,6 +94,7 @@ const AUTH_KEY = 'soundgame.auth'; // 大厅登录写入(见 web/index.html)
|
||||
export class SpaceGame extends Game {
|
||||
constructor(audio, root) {
|
||||
super(audio, root);
|
||||
this.guide = new Guide(audio); // 全局向导层(guide='off' 时全部静默 no-op)
|
||||
this.phase = 'idle'; // idle | ask | drill | playing | feature | menu | over
|
||||
this._session = 0; // 会话令牌:重开/跳过时使旧计时器与语音链失效
|
||||
this._timers = [];
|
||||
@@ -140,12 +147,14 @@ export class SpaceGame extends Game {
|
||||
stop() {
|
||||
this._session++;
|
||||
this._clearTimers();
|
||||
this.audio.setRateBoostEnabled(true); // 离开对局,恢复长按快进
|
||||
this.audio.stopAll();
|
||||
}
|
||||
|
||||
// ---------------- 手势 ----------------
|
||||
|
||||
onGesture(g) {
|
||||
this.guide.disarm(); // 任何手势复位向导状态(guide=off 时 no-op)
|
||||
switch (this.phase) {
|
||||
case 'ask':
|
||||
if (g === 'up') { this.audio.stopVoice(); this._startDrill(); }
|
||||
@@ -271,9 +280,11 @@ export class SpaceGame extends Game {
|
||||
this.audio.stopVoice(); // 打断结算/菜单语音链,避免残留到下一局
|
||||
this._resetRunState();
|
||||
this.phase = 'playing';
|
||||
this.audio.setRateBoostEnabled(false); // 对局期豁免长按快进(演奏/闪避冲突)
|
||||
this.runStart = performance.now();
|
||||
this.protectionUntil = this.runStart + PROTECTION_MS;
|
||||
this.audio.play('bgm/engine');
|
||||
this.guide.state('battle'); // 状态签名:对局(与状态 BGM 同点)
|
||||
this._scheduleSpawn(1500); // 首颗稍缓,给玩家进入状态
|
||||
this._after(BOOST_INTRO_MS, () => this._beginFeature('boost'));
|
||||
this._after(SHIELD_INTRO_MS, () => this._beginFeature('shield'));
|
||||
@@ -498,6 +509,11 @@ export class SpaceGame extends Game {
|
||||
const ids = [`voice/hit_${ob.dir}`];
|
||||
if (this.hits === 2) ids.push('voice/hit_last');
|
||||
this._speak(ids);
|
||||
// 向导层(schemes §2):对局第 1 次受击追加指令式提示(hit_<dir> 已归因方位,
|
||||
// 这句补"该怎么拨";已震 hit 免重复 error 震;onceKey 同局一次)
|
||||
if (this.hits === 1) {
|
||||
this.guide.error({ voice: 'gd_hit_hint', onceKey: 'space_hit_hint', skipHaptic: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,6 +531,8 @@ export class SpaceGame extends Game {
|
||||
|
||||
_openMenu() {
|
||||
this.phase = 'menu';
|
||||
this.audio.setRateBoostEnabled(true); // 暂停期恢复长按快进
|
||||
this.guide.state('menu'); // 状态签名:菜单(与状态 BGM 同点)
|
||||
this._clearTimers(); // 模态暂停:刷怪与进行中陨石全部冻结清除
|
||||
this.obstacles.clear();
|
||||
this.swipes = [];
|
||||
@@ -541,6 +559,8 @@ export class SpaceGame extends Game {
|
||||
|
||||
_closeMenu() {
|
||||
this.phase = 'playing';
|
||||
this.audio.setRateBoostEnabled(false); // 回到对局期
|
||||
this.guide.state('battle');
|
||||
this.audio.play('bgm/engine', { fadeMs: 500 }); // 状态 BGM:回对局淡回 engine
|
||||
this._scheduleSpawn(800);
|
||||
this._rearmRunTimers();
|
||||
@@ -555,6 +575,8 @@ export class SpaceGame extends Game {
|
||||
|
||||
_gameOver() {
|
||||
this.phase = 'over';
|
||||
this.guide.state('settle'); // 状态签名:结算(与结算 BGM 同点)
|
||||
this.audio.setRateBoostEnabled(true); // 离开对局,恢复长按快进
|
||||
this._clearTimers();
|
||||
this.obstacles.clear();
|
||||
this.audio.stopBgm(600);
|
||||
@@ -569,8 +591,9 @@ export class SpaceGame extends Game {
|
||||
const ids = [this._timeBandVoice(aliveSec)];
|
||||
if (isRecord) ids.push('voice/new_record');
|
||||
ids.push(this._dodgeBandVoice(this.dodges), 'voice/restart_hint');
|
||||
// 结算语音链播完淡入 menu pad(10 号文档 §6);被跳过的句仍逐句推进链
|
||||
this._speak(ids, () => {
|
||||
// 结算语音链播完淡入 menu pad(10 号文档 §6);被跳过的句仍逐句推进链。
|
||||
// 链换 _speakGuarded(清点 §5.4 缺口):time_* 等资产缺失也能起结算 BGM
|
||||
this._speakGuarded(ids, () => {
|
||||
if (this.phase === 'over') this.audio.play('bgm/menu', { fadeMs: 800 });
|
||||
});
|
||||
});
|
||||
@@ -644,7 +667,7 @@ export class SpaceGame extends Game {
|
||||
|
||||
/** voice 层是否有对白在播(单击=跳过本句的语境判定依据)。 */
|
||||
_voiceBusy() {
|
||||
return !!this.audio._voice;
|
||||
return this.audio.isVoicePlaying(); // 公开 API(原读私有 _voice,清点 §0)
|
||||
}
|
||||
|
||||
/** 语音串行链:自然播完才接下一句;被 stopVoice 打断则整条作废。 */
|
||||
|
||||
@@ -145,6 +145,9 @@ const { WebAudioEngine } = await import('../../engine/audio.js');
|
||||
const { settings } = await import('../../engine/settings.js');
|
||||
const { SpaceGame } = await import('../game.js');
|
||||
|
||||
// 向导层关闭:回归测试只验证纯游戏逻辑(guide='full' 的行为由 harness/guide.test.mjs 覆盖)
|
||||
settings.set({ guide: 'off', voice: 'female', speed: 1.0, bgm: 1.0 });
|
||||
|
||||
// ---------------- 断言与工具 ----------------
|
||||
let passed = 0, failed = 0;
|
||||
function assert(cond, msg) {
|
||||
|
||||
Reference in New Issue
Block a user