round3: G1 gd_answer提前一小节(根因=排程压窗起点) + G2教学降难DDA + G3 count-in修复(根因=未重排调度队列) + G4 escape目标链 + G5 simon双击根治(重听改上滑,重录3句) + G6轮成功/破纪录 + G7 space方向呼喊 + 22语音文件 + guide93断言全绿

This commit is contained in:
SoundGame Dev
2026-08-31 01:40:51 +08:00
parent ec89629eb4
commit 9f8096377e
537 changed files with 1046 additions and 432 deletions
+7 -3
View File
@@ -66,7 +66,11 @@ export async function session(ctx, mock) {
await move('right', '往东走,风声换了个方向');
await move('right');
await move('down', '到了铁柜房,柜子锁着');
await ctx.gesture('doubletap'); // 用钥匙 → unlock_cabinet(第二间解完)
await ctx.waitVirtual(3000);
ctx.status('柜子开了,拿到保险丝——解到第二间,收工');
await ctx.gesture('doubletap'); // 用钥匙 → unlock_cabinet + gd_next_fuse(round3 G4)
await ctx.waitVirtual(4200);
ctx.status('柜子开了,听到「保险丝到手。接下来,去找电箱」——目标又有了');
// round3 G4 剧本:拿到保险丝后再走两步(朝西边的电箱房方向),目标链不断
await move('left', '照新目标往西摸');
await move('left', '再走一步,西边隐隐有电流声——电箱在那边');
ctx.status('解到第二间、照下一目标句又摸了两步,收工');
}
+31 -6
View File
@@ -1,13 +1,16 @@
/**
* 《节拍行者》新手会话剧本:首局完整走教学 T1-T3 → 曲目一(90 BPM);
* 曲中下滑呼出菜单逛一圈再恢复;第一乐句故意整句漏掉(连 2 Miss 触发向导
* 「慢了半拍」提示),之后跟着领奏把一曲打完到结算。
* 《节拍行者》新手会话剧本:首局完整走教学 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._cues.map((c) => c.gesture).join('→')})${game._tutRate < 1 ? '(领奏已放慢 25%)' : ''}`
: '教学准备中,听鼓点';
case 'select': return '选曲:左右滑换曲,单击开始,下滑回大厅';
case 'main': {
@@ -33,6 +36,26 @@ export async function session(ctx, mock) {
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);
@@ -42,10 +65,11 @@ export async function session(ctx, mock) {
await ctx.waitCond(() => game._tutIdx > step || game.phase !== 'tut');
}
await ctx.waitCond(() => game.phase === 'main');
ctx.status('教学完毕,曲目一《启程》90 拍开始');
ctx.status('教学完毕,曲目一《启程》90 拍开始。领奏响之前先听到「现在跟着我打」');
// 曲中逛菜单:下滑呼出(模态暂停)→ 听选项 → 下滑恢复
await ctx.waitVirtual(2500);
//(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('曲中暂停,进了菜单');
@@ -54,6 +78,7 @@ export async function session(ctx, mock) {
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);
+37 -17
View File
@@ -1,10 +1,10 @@
/**
* 《音序回响》新手会话剧本:教学六音一步一步学(中间故意做错一次听纠错)、
* 收尾听完机会经济句(免费重听两次、用完扣机会);第 1 轮直接答对;第 2 轮开局
* 忘序列僵住 10 秒,听到向导「双击可以重新听」提示后 5 秒内照做——用掉一次免费
* 重听(round2:25s 升级已不再自动重播序列,坐着等答案重念的路子废了);第 3 轮
* 凭记忆答对,在**整轮边界**收工(round2 剧本修正:不在轮中退出——round1 转写
* 「第 4 轮报数后再没人说话」与「干晾 85 秒」都是剧本失真,不是游戏失真)。
* 收尾听完机会经济句(round3 G5 重录口径:记不住就向上滑重新听);第 1 轮
* 直接答对(途中有双击音就演示「双击=作答」);第 2 轮开局忘序列僵住 10 秒,
* 听到向导「向上滑可以重新听」提示后照做——用掉一次免费重听(round3 G5:
* input 双击永远=作答,重听改绑上滑);第 3 轮凭记忆答对,在**整轮边界**收工
* (round2 剧本修正:不在轮中退出)。
*/
export function describe(game) {
const GN = { up: '上滑', down: '下滑', left: '左滑', right: '右滑', tap: '单击', doubletap: '双击' };
@@ -16,7 +16,7 @@ export function describe(game) {
: '教学旁白在播,先听着';
case 'announce': return `第 ${game.round} 轮报数呢,听好了`;
case 'playback': return `第 ${game.round} 轮序列演奏中(${game.sequence.length} 个音),记住顺序,先别动手`;
case 'input': return `第 ${game.round} 轮轮到你了:已对上 ${game.inputIdx}/${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 '菜单里:单击切选项,双击确认,上滑听规则';
@@ -47,28 +47,48 @@ export async function session(ctx, mock) {
await ctx.waitIdle();
}
}
ctx.status('教学讲完了,机会经济也交代清楚了:免费重听两次,用完要扣机会');
ctx.status('教学讲完了:免费重听两次、用完扣机会,想重听就向上滑');
// 正式对局:完整打完第 3 轮,在轮边界收工
let relistenDone = false;
while (mock.now < 12 * 60 * 1000 && game.phase !== 'gameover') {
if (game.phase === 'input') {
// 第 2 轮先失忆僵住 10 秒:听向导「双击可以重新听一遍序列」,5 秒内照做。
//(期望输入恰为双击时先答这一下,下一步再演示重听——双击语境判定)
if (game.round === 2 && !relistenDone
&& game.inputIdx <= 1 && game.sequence[game.inputIdx] !== 'doubletap') {
// 第 1 轮开局先失忆僵 10 秒(还没动过手,input 看门狗在挂):听向导
// 「向上滑可以重新听一遍序列」,照做——round3 G5:重听已改绑上滑。
// 测试钩子把序列摆成无 up 的两音,保证上滑=重听不被「期望恰为 up」豁免干扰。
if (game.round === 1 && !relistenDone && game.inputIdx === 0) {
relistenDone = true;
ctx.status('序列有点长,先僵一会儿');
if (game.sequence.includes('up')) { game.sequence[0] = 'left'; game.sequence[1] = 'tap'; }
ctx.status('第 1 轮有点懵,先僵一会儿');
ctx.silence(10, '想不起来序列了');
await ctx.waitVirtual(10600); // 10s 看门狗:nudge + gd_relisten
ctx.status('听到提示:需要的话,双击可以重新听一遍序列');
ctx.note('照提示双击,用掉一次免费重听');
await ctx.gesture('doubletap');
// 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) {
ctx.status('最后一轮最后一个音,打完这轮就收');
// 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('轮到我复现了');
}
+14 -9
View File
@@ -1,9 +1,10 @@
/**
* 《盲航》新手会话剧本:首局完整演练 D1-D4(第二颗故意超时,听当场重教)→
* 正式对局躲过保护期(20s/45s 的加速/护盾引入都照做)→ 保护期结束后故意漏两颗
* (第 1 次听归因+向导「同向滑动就能拨开」,第 2 次听 gd_last_life「最后一次机会」
* ——round2 F2.2:受击经济透明)→ 之后全部躲开,保护期结束后再撑 20 秒以上收工
* (不再像 round1 那样在保护期后 8 秒就撤,也没有第三次被撞)。
* 正式对局躲过保护期(20s/45s 的加速/护盾引入都照做)→ 每颗陨石都撑到 loud
* 警报档(撞击前 500ms:警报 + dir_* 方向呼喊同时响,round3 G7)才拨开 →
* 保护期结束后故意漏两颗(第 1 次听归因+向导「同向滑动就能拨开」,第 2 次听
* gd_last_life「最后一次机会」——round2 F2.2:受击经济透明)→ 之后全部躲开,
* 保护期结束后再撑 20 秒以上收工。
*/
export function describe(game) {
const DIR = { left: '左边', right: '右边', up: '上面', down: '下面' };
@@ -48,7 +49,9 @@ export async function session(ctx, mock) {
ctx.status('演练做完了,要出发了');
// 正式对局:保护期 60s + 结束后再撑 20s 以上收工。
// 每颗陨石生成(呼啸声起)就在判定窗口将开时同向拨开——比盯着 1.5s 判定窗口更稳。
// 每颗陨石都撑到 loud 警报档(撞击前 500ms:第二段警报 + 「左!/右!…」方向
// 呼喊同时响,round3 G7)才同向拨开——仍在 1.5s 判定窗口内,稳。呼喊与警报
// 同一刻先落,随即滑开(=500ms 阈值恰在 loud 警报定时器触发点上)。
//(刷怪链是自驱动的定时器链——禁止裸 waitIdle,等待一律挂条件)
await ctx.waitCond(() => game.phase === 'playing');
ctx.status('出发。引擎声起来了,保护期里先练手');
@@ -75,9 +78,10 @@ export async function session(ctx, mock) {
: '被撞了。先听到方位归因,又听到「同向滑动就能拨开」');
continue;
}
// 只在判定窗口将开未开时出手(警报第一响 = 撞击前 1s,窗口内)
// 撑到 loud 警报档(撞击前 0.5s,第二段警报 + dir 方向呼喊同窗)再出手
//(=500ms 恰在 loud 警报定时器触发点:呼喊先落,同一拍跟着滑开)
const target = [...game.obstacles].find((o) => !dodged.has(o)
&& o.impactAt - performance.now() < 1300);
&& o.impactAt - performance.now() <= 500);
if (target) {
if (eaten < 2 && performance.now() >= game.protectionUntil) {
eaten++;
@@ -89,13 +93,14 @@ export async function session(ctx, mock) {
continue;
}
dodged.add(target);
ctx.note('听到第二段警报和方向呼喊,同向滑开');
await ctx.gesture(target.dir);
continue;
}
await ctx.waitCond(() => game.phase !== 'playing'
|| victims.some((v) => !game.obstacles.has(v))
|| [...game.obstacles].some((o) => !dodged.has(o) && o.impactAt - performance.now() < 1300));
|| [...game.obstacles].some((o) => !dodged.has(o) && o.impactAt - performance.now() <= 500));
}
const aliveSec = Math.round((performance.now() - game.runStart) / 1000);
ctx.status(`保护期结束后撑了 ${aliveSec - 60} 秒、挨了两下还活着(警报也只起一波了),先到这`);
ctx.status(`保护期结束后撑了 ${aliveSec - 60} 秒、挨了两下还活着;每回撞击前半秒都能听到方向呼喊,先到这`);
}