Files

149 lines
7.9 KiB
JavaScript
Raw Permalink 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.
/**
* 《音序回响》新手会话剧本(round4):教学六音一步一步学(中间故意做错一次听纠错)、
* 收尾听完机会经济句;对局段制造 round4 H 组断言点——
* - 含 up 的轮次(R1):序列播完先听占用预告 gd_occ_up,僵 5s 验证被占用轮看门狗
* 只发 nudge 不建议重听(H2),期望恰为 up 时上滑=作答;
* - 含 doubletap 的轮次(R2)与 up+doubletap 双占用轮(R6):gd_occ_dt / gd_occ_both;
* - 无尝试超时(R5/R7):干净轮僵 5s → 看门狗建议上滑重听 → 用掉两次免费重听,
* 第 1 次听到「免费重听还剩一次」(H3:余量句与真实余量一致——第 4 轮完成机会
* +1 到 2 后才重听),第 2 次听到「免费重听用完了」;整轮边界收工。
*/
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': {
const occ = [];
if (game.sequence.includes('up')) occ.push('上滑');
if (game.sequence.includes('doubletap')) occ.push('双击');
const occNote = occ.length ? `(这轮${occ.join('和')}是作答,占用了)` : '';
return `第 ${game.round} 轮轮到你了:已对上 ${game.inputIdx}/${game.sequence.length},照原顺序做手势${occNote}(要重听就向上滑)`;
}
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 两句 + 机会经济句)播完直达对局——一到 announce 就接手,
// 不裸 waitIdle(会把 input 的看门狗窗口整段空烧掉,round1 剧本失真根源)
await ctx.waitCond(() => game.phase === 'announce' || game.phase === 'playback'
|| game.phase === 'input' || game.phase === 'gameover');
} else {
await ctx.waitIdle();
}
}
ctx.status('教学讲完了:免费重听两次、用完扣机会,想重听就向上滑');
// 测试钩子(13 号文档允许直改实例字段):每轮报数时点把序列摆成剧本需要的
// 形状——占用音(up/doubletap)摆进指定轮,其余用不含占用音的手势补齐。
const fixSeq = (highlights) => {
const fill = ['down', 'right', 'tap', 'left'];
for (let i = 0; i < game.sequence.length; i++) {
game.sequence[i] = i < highlights.length ? highlights[i] : fill[i % fill.length];
}
};
const waitAnnounce = () => ctx.waitCond(() => game.phase === 'announce' || game.phase === 'gameover');
const answerRound = async () => {
await ctx.waitCond(() => game.phase === 'input' || game.phase === 'gameover');
while (game.phase === 'input') await ctx.gesture(game.sequence[game.inputIdx]);
};
// 「无尝试超时」剧本点(干净轮):僵到 input 看门狗 5s 首级的提示语音开播
//(input 态无 BGM 无别的语音,voiceBusy=提示在播;别盲等固定时长——harness 的
// advance 没有时间上限,会恰好迈过提示语音结束点、一路空烧到 25s 升级)
const stallForAdvice = async (why) => {
ctx.silence(5, why);
await ctx.waitCond(() => ctx.voiceBusy());
};
// R1:含 up 的轮次——占用预告 + 被占用轮看门狗只 nudge(H2)+ 上滑恰为作答
await waitAnnounce();
fixSeq(['up', 'tap']);
await ctx.waitCond(() => game.phase === 'input');
ctx.status('序列一播完就听到「这一轮,向上滑是作答」——重听键这轮被占了');
ctx.silence(5, '第一轮先不动手,看看它催不催');
// 被占用轮 5s 级没有提示语音可等:僵到 25s 升级(turn+confirm 震)作为里程碑
await ctx.waitVirtual(25600);
ctx.note('5 秒一声轻提醒、25 秒又一声加确认震——两级提醒都没建议「向上滑重听」,它知道这轮上滑是作答');
ctx.status('先来一发上滑——期望恰为 up 时上滑就是作答');
await answerRound();
// R2:含 doubletap 的轮次——占用预告 dt(重听建议不受影响,不僵)
await waitAnnounce();
fixSeq(['down', 'doubletap', 'left']);
await ctx.waitCond(() => game.phase === 'input');
ctx.status('这轮预告的是「双击是作答」');
await answerRound();
// R3/R4:无占用轮直答(第 4 轮完成 → 机会 +1,免费重听凑满两次)
await waitAnnounce();
fixSeq([]);
await ctx.waitCond(() => game.phase === 'input');
ctx.status('这轮开场没有占用预告——上滑还能当重听键用');
await answerRound();
await waitAnnounce();
fixSeq([]);
await ctx.waitCond(() => game.phase === 'input');
ctx.status('机会加一之后,免费重听真的有两次了');
await answerRound();
// R5:无尝试超时 + 第 1 次免费重听(余量如实报「还剩一次」,H3)
await waitAnnounce();
fixSeq([]);
await ctx.waitCond(() => game.phase === 'input');
ctx.status('第 5 轮序列有点长,先不动手');
await stallForAdvice('想不起来序列了');
ctx.status('5 秒就来了提示:需要的话,向上滑可以重新听一遍序列');
ctx.note('照提示向上滑——第 1 次免费重听用掉,跟着听到「免费重听还剩一次」');
await ctx.gesture('up');
await ctx.waitCond(() => game.phase === 'playback' || game.phase === 'input');
await answerRound();
// R6:up+doubletap 双占用轮——gd_occ_both + 被占用轮看门狗只 nudge
await waitAnnounce();
fixSeq(['up', 'doubletap']);
await ctx.waitCond(() => game.phase === 'input');
ctx.status('这轮说「向上滑和双击都是作答」——两个键都被占用了');
ctx.silence(5, '轮到我了,先心里过一遍序列');
await ctx.waitVirtual(25600); // 同 R1:两级提醒都只发 earcon/标记,无重听建议
ctx.note('又是两级提醒都没有重听建议——占用轮里那句建议自动闭嘴');
await answerRound();
// R7:无尝试超时 + 第 2 次免费重听(余量归 0,H3)
await waitAnnounce();
fixSeq([]);
await ctx.waitCond(() => game.phase === 'input');
await stallForAdvice('长序列,又想不起来了');
ctx.status('又是那句提示:向上滑可以重新听一遍序列');
ctx.note('向上滑——第 2 次免费重听用掉,这回听到「免费重听用完了。之后重听会花一次机会」');
await ctx.gesture('up');
await ctx.waitCond(() => game.phase === 'playback' || game.phase === 'input');
await answerRound();
// 轮边界收工
await ctx.waitCond(() => game.phase === 'roundend' || game.phase === 'gameover');
ctx.status(`收工:打完 ${game.round} 轮在轮边界停手(免费重听剩 ${game.chances} 次,账目和播的对上了)`);
}