/** * 《音序回响》新手会话剧本:教学六音一步一步学(中间故意做错一次听纠错)、 * 收尾听完机会经济句(round3 G5 重录口径:记不住就向上滑重新听);第 1 轮 * 直接答对(途中有双击音就演示「双击=作答」);第 2 轮开局忘序列僵住 10 秒, * 听到向导「向上滑可以重新听」提示后照做——用掉一次免费重听(round3 G5: * input 双击永远=作答,重听改绑上滑);第 3 轮凭记忆答对,在**整轮边界**收工 * (round2 剧本修正:不在轮中退出)。 */ export function describe(game) { const GN = { up: '上滑', down: '下滑', left: '左滑', right: '右滑', tap: '单击', doubletap: '双击' }; const GESTURES = ['up', 'down', 'left', 'right', 'tap', 'doubletap']; switch (game.phase) { case 'tutorial': return game.teachAwait ? `教学:该做「${GN[GESTURES[game.teachIdx]]}」了` : '教学旁白在播,先听着'; case 'announce': return `第 ${game.round} 轮报数呢,听好了`; case 'playback': return `第 ${game.round} 轮序列演奏中(${game.sequence.length} 个音),记住顺序,先别动手`; case 'input': return `第 ${game.round} 轮轮到你了:已对上 ${game.inputIdx}/${game.sequence.length},照原顺序做手势(双击就是双击那个音,要重听就向上滑)`; case 'roundend': return '刚答完一轮,播报中'; case 'gameover': return `游戏结束,完成 ${game.round - 1} 轮。单击再来一局,下滑开菜单`; case 'menu': return '菜单里:单击切选项,双击确认,上滑听规则'; default: return '开场,稍等一下'; } } export async function session(ctx, mock) { const { game } = ctx; const G = ['up', 'down', 'left', 'right', 'tap', 'doubletap']; // 教学 T0-T6:教一个练一个 for (let i = 0; i < G.length; i++) { await ctx.waitCond(() => game.phase === 'tutorial' && game.teachAwait); ctx.status(`教学第 ${i + 1}/6 个音色`); if (i === 2) { ctx.note('把「左滑」记混了,先来了个右滑'); await ctx.gesture('right'); // 听纠错语音(t_wrong_left) await ctx.waitCond(() => game.phase === 'tutorial' && game.teachAwait); } await ctx.gesture(G[i]); // 照指令做对 if (i === G.length - 1) { // 收尾链(t7 两句 + 机会经济句)播完直达对局——一到 input 就接手, // 不裸 waitIdle(会把 input 的看门狗窗口整段空烧掉,round1 剧本失真根源) await ctx.waitCond(() => game.phase === 'playback' || game.phase === 'input' || game.phase === 'gameover'); } else { await ctx.waitIdle(); } } ctx.status('教学讲完了:免费重听两次、用完扣机会,想重听就向上滑'); // 正式对局:完整打完第 3 轮,在轮边界收工 let relistenDone = false; while (mock.now < 12 * 60 * 1000 && game.phase !== 'gameover') { if (game.phase === 'input') { // 第 1 轮开局先失忆僵 10 秒(还没动过手,input 看门狗在挂):听向导 // 「向上滑可以重新听一遍序列」,照做——round3 G5:重听已改绑上滑。 // 测试钩子把序列摆成无 up 的两音,保证上滑=重听不被「期望恰为 up」豁免干扰。 if (game.round === 1 && !relistenDone && game.inputIdx === 0) { relistenDone = true; if (game.sequence.includes('up')) { game.sequence[0] = 'left'; game.sequence[1] = 'tap'; } ctx.status('第 1 轮有点懵,先僵一会儿'); ctx.silence(10, '想不起来序列了'); // 10s 看门狗到点:nudge + gd_relisten。直接等提示语音开播(input 态无 // BGM、无别的语音,voiceBusy=提示在播;别用「等满 10.6s」——定时刻度会 // 恰好迈过语音结束点,等成第二次升级) await ctx.waitCond(() => ctx.voiceBusy()); ctx.status('听到提示:需要的话,向上滑可以重新听一遍序列'); ctx.note('照提示向上滑,用掉一次免费重听'); await ctx.gesture('up'); await ctx.waitCond(() => game.phase === 'playback' || game.phase === 'input'); continue; } // 第 2 轮开头演示「期望恰为 up 时上滑=作答」(00 §5.3 豁免同款口径) if (game.round === 2 && !game.__upDemoDone && game.inputIdx === 0) { game.__upDemoDone = true; game.sequence[0] = 'up'; ctx.status('这个是上行音——上滑作答(期望恰为 up 时上滑=作答)'); await ctx.gesture('up'); continue; } if (game.round === 3 && game.inputIdx === game.sequence.length - 1) { // round3 G5 演示钩子(13 号文档允许直读/改实例字段):把最后一个期望音 // 摆成 doubletap,保证转写演示到「input 双击永远=作答」 if (game.sequence[game.inputIdx] !== 'doubletap' && !game.__dtDemoDone) { game.__dtDemoDone = true; game.sequence[game.inputIdx] = 'doubletap'; } ctx.status(game.sequence[game.inputIdx] === 'doubletap' ? '这个是双击音——双击作答(round3:input 双击永远=作答)' : '最后一轮最后一个音,打完这轮就收'); } else { ctx.status('轮到我复现了'); } await ctx.gesture(game.sequence[game.inputIdx]); continue; } // 整轮边界收尾:第 3 轮答完(roundend),不等第 4 轮报数 if (game.phase === 'roundend' && game.round === 3) break; // announce/playback/roundend:等阶段推进即可,不裸 waitIdle(防吞掉 input 窗口) const ph = game.phase; const r = game.round; await ctx.waitCond(() => game.phase !== ph || game.round !== r); } ctx.status(`收工:完整打完 ${game.round} 轮,在轮与轮的边界停手(免费重听剩 ${game.chances} 次)`); }