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
+76
View File
@@ -0,0 +1,76 @@
/**
* 《节拍行者》新手会话剧本:首局完整走教学 T1-T3 → 曲目一(90 BPM);
* 曲中下滑呼出菜单逛一圈再恢复;第一乐句故意整句漏掉(连 2 Miss 触发向导
* 「慢了半拍」提示),之后跟着领奏把一曲打完到结算。
*/
export function describe(game) {
switch (game.phase) {
case 'intro': return '开场旁白在播,单击可以跳过';
case 'tut': return game._cues.length
? `教学:听领奏,下一小节照样打(${game._cues.map((c) => c.gesture).join('→')})`
: '教学准备中,听鼓点';
case 'select': return '选曲:左右滑换曲,单击开始,下滑回大厅';
case 'main': {
const left = game._cues.filter((c) => !c.judged).length;
return `演奏中(${game._song?.title ?? ''}):还剩 ${left} 个应答音。领奏停了、轮到空拍,就该我出手了`;
}
case 'menu': return '暂停菜单:单击切选项,双击确认,上滑帮助,下滑恢复演奏';
case 'end': return '结算播报中:听完单击再来一遍,右滑下一首,下滑回大厅';
default: return '开场,稍等';
}
}
export async function session(ctx, mock) {
const { game, audio } = ctx;
const t = () => audio.ctx.currentTime;
// 开场:welcome 听完进教学
await ctx.waitCond(() => game.phase === 'tut');
ctx.status('教学开始了,先听领奏');
// 教学 3 步:等领奏排程好,在应答窗口内跟打
//(注意:调度泵 interval 常驻,虚拟时钟永不"空闲"——禁止裸 waitIdle)
for (let step = 0; step < 3; step++) {
await ctx.waitCond(() => game.phase === 'tut' && game._cues.length > 0 && game._tutAttempts.length === 0);
ctx.status(`教学第 ${step + 1}/3 句`);
for (const cue of game._cues) {
const at = cue.expected - (cue.gesture === 'tap' ? -0.28 : 0.04); // tap 补偿引擎 ~300ms 延迟
await ctx.waitCond(() => t() >= at || cue.judged);
if (cue.judged) continue;
await ctx.gesture(cue.gesture);
}
await ctx.waitCond(() => game._tutIdx > step || game.phase !== 'tut');
}
await ctx.waitCond(() => game.phase === 'main');
ctx.status('教学完毕,曲目一《启程》90 拍开始');
// 曲中逛菜单:下滑呼出(模态暂停)→ 听选项 → 下滑恢复
await ctx.waitVirtual(2500);
await ctx.gesture('down');
await ctx.waitCond(() => game.phase === 'menu');
ctx.status('曲中暂停,进了菜单');
await ctx.gesture('tap'); // 切换选项(听到「回大厅」)
await ctx.waitIdle();
ctx.note('还是想打完这首,下滑回去');
await ctx.gesture('down'); // 恢复
await ctx.waitCond(() => game.phase === 'main');
// 第一乐句故意整句不打:连 2 次 Miss → 向导 gd_miss
const firstPhrase = game._cues.filter((c) => c.phrase === 0);
ctx.silence(5, '还在数拍子,第一句没敢出手');
await ctx.waitCond(() => firstPhrase.every((c) => c.judged));
ctx.status('第一句全漏了,听到「慢了半拍,下一小节跟上我就行」');
// 之后逐音跟打(tap 注入留出引擎 ~300ms 回调延迟)
for (const cue of game._cues) {
if (cue.judged) continue;
const at = cue.expected - (cue.gesture === 'tap' ? -0.28 : 0.04);
await ctx.waitCond(() => t() >= at || cue.judged);
if (cue.judged) continue;
await ctx.gesture(cue.gesture);
}
await ctx.waitCond(() => game.phase === 'end');
await ctx.waitIdle();
ctx.status('曲子打完了,结算播报中');
}