Files
SoundGame/web/harness/sessions/rhythm.mjs
T

102 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 《节拍行者》新手会话剧本:首局完整走教学 T1-T3(第 2 句故意连错两遍,
* 触发 round3 G2 的「降速 25% 重演 + gd_slow」,跟着慢速领奏做对后恢复正常)
* → 曲目一(90 BPM);曲中下滑呼出菜单逛一圈再恢复(round3 G3:恢复先听
* gd_count「四拍之后,继续」,再跟 4 拍底鼓 count-in 进场;首个应答窗口前
* 整整 1 小节有 gd_answer+turn 交接标记);第一乐句故意整句漏掉(连 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('→')})${game._tutRate < 1 ? '(领奏已放慢 25%)' : ''}`
: '教学准备中,听鼓点';
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 句`);
if (step === 1) {
// round3 G2 剧本:这一句故意连错两遍(整句不出手),听它放慢一些再带着打。
// 注意 _tutEvaluate 判定即把 _phaseEnd 置 Infinity 防重入——等待一律挂
// 「持久里程碑」条件(_tutFails/_tutSlowUsed),别挂阶段末时刻。
await ctx.waitCond(() => game._tutFails === 1); // 失败①:没出手
ctx.status('第一遍没敢出手,先听它归因');
await ctx.waitCond(() => game._phaseEnd < 1e8 && t() < game._phaseEnd - 1); // 纠正链播完、重新领奏
ctx.status('又重新领奏了一遍,还是没敢出手');
await ctx.waitCond(() => game._tutSlowUsed === true); // 失败② → 触发降速重演
await ctx.waitCond(() => game._phaseEnd < 1e8 && t() < game._phaseEnd - 1); // gd_slow 播完、慢速领奏已排
ctx.status('听到「我放慢一些,跟着试试」——领奏降速 25% 重演,这回跟着慢拍打');
for (const cue of game._cues) {
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._tutIdx > step || game.phase !== 'tut');
continue;
}
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 拍开始。领奏响之前先听到「现在跟着我打」');
// 曲中逛菜单:下滑呼出(模态暂停)→ 听选项 → 下滑恢复
//(round3 G3:恢复先播「四拍之后,继续」,再跟 4 拍底鼓 count-in 进场)
await ctx.waitCond(() => t() >= game._bgmT0 + 1.5);
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');
ctx.status('听到「四拍之后,继续」,跟着四下底鼓数拍进来了');
// 第一乐句故意整句不打:连 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('曲子打完了,结算播报中');
}