round2: 看门狗礼仪F1(不重播/单句升级/idle-skip) + 分档归因F2 + 交接count-in/answer F3 + 教学看表现F4-F5 + 警报去叠F6 + 回绕F7 + 开场瘦身F8 + 10句新语音 + 测试全绿(6playtest零改动+guide52断言+资产终检)

This commit is contained in:
SoundGame Dev
2026-08-31 00:26:25 +08:00
parent 296c996fb9
commit 902461f0f4
65 changed files with 1094 additions and 775 deletions
+29 -18
View File
@@ -1,7 +1,9 @@
/**
* 《盲航》新手会话剧本:首局完整演练 D1-D4(第二颗故意超时,听当场重教)→
* 正式对局躲过保护期(20s/45s 的加速/护盾引入都照做)→ 保护期结束后故意漏一颗
* (触发归因句 + 向导「同向滑动就能拨开」指令)→ 再正常躲几颗收工。
* 正式对局躲过保护期(20s/45s 的加速/护盾引入都照做)→ 保护期结束后故意漏两颗
* (第 1 次听归因+向导「同向滑动就能拨开」,第 2 次听 gd_last_life「最后一次机会」
* ——round2 F2.2:受击经济透明)→ 之后全部躲开,保护期结束后再撑 20 秒以上收工
* (不再像 round1 那样在保护期后 8 秒就撤,也没有第三次被撞)。
*/
export function describe(game) {
const DIR = { left: '左边', right: '右边', up: '上面', down: '下面' };
@@ -45,46 +47,55 @@ export async function session(ctx, mock) {
}
ctx.status('演练做完了,要出发了');
// 正式对局:保护期 60s + 结束后再躲一小段就收工(不再模拟,避免挂机撞毁)。
// 每颗陨石生成(呼啸声起)就立即同向拨开——比盯着 1.5s 判定窗口更稳。
// 正式对局:保护期 60s + 结束后再撑 20s 以上收工。
// 每颗陨石生成(呼啸声起)就在判定窗口将开时同向拨开——比盯着 1.5s 判定窗口更稳。
//(刷怪链是自驱动的定时器链——禁止裸 waitIdle,等待一律挂条件)
await ctx.waitCond(() => game.phase === 'playing');
ctx.status('出发。引擎声起来了,保护期里先练手');
const dodged = new Set();
let victim = null; // 保护期结束后故意不躲的那颗
let eaten = false;
const victims = []; // 保护期结束后故意不躲的(前两颗:听归因 + gd_last_life)
let eaten = 0;
while ((game.phase === 'playing' || game.phase === 'feature')
&& performance.now() < game.protectionUntil + 8000) {
&& performance.now() < game.protectionUntil + 21000) {
if (game.phase === 'feature') {
ctx.status('游戏停下来教新功能');
await ctx.waitIdle(); // 引入句听完(feature 态无刷怪链,可安全等待空闲)
// 引入句听完就照做(不裸 waitIdle——feature 态之后的刷怪链是自驱动定时器,
// 裸等会把整局对局连本带利烧穿到坠毁)
await ctx.waitCond(() => !ctx.voiceBusy());
ctx.note(game.featureKind === 'boost' ? '照它说的单击加速' : '照它说的双击开护盾');
await ctx.gesture(game.featureKind === 'boost' ? 'tap' : 'doubletap');
continue;
}
if (victim && !game.obstacles.has(victim)) {
ctx.note('被撞了。先听到方位归因,又听到「同向滑动就能拨开」');
victim = null;
// 被撞的那颗消失 → 归因链听完了,继续
const eatenIdx = victims.findIndex((v) => !game.obstacles.has(v));
if (eatenIdx >= 0) {
victims.splice(eatenIdx, 1);
ctx.note(eaten === 2 && victims.length === 0
? '第二次被撞:先听方位归因,又听到「这是最后一次机会,撑住」——这就是全部家底了'
: '被撞了。先听到方位归因,又听到「同向滑动就能拨开」');
continue;
}
// 只在判定窗口将开未开时出手(警报第一响 = 撞击前 1s,窗口内)
const target = [...game.obstacles].find((o) => !dodged.has(o)
&& o.impactAt - performance.now() < 1300);
if (target) {
if (!eaten && performance.now() >= game.protectionUntil) {
eaten = true;
victim = target;
if (eaten < 2 && performance.now() >= game.protectionUntil) {
eaten++;
victims.push(target);
dodged.add(target); // 标记 = 故意不躲
ctx.status('保护期结束了,这颗我故意没动');
ctx.status(eaten === 1
? '保护期结束了,这颗我故意没动'
: '又有一颗,也故意不躲——想听听第二次被撞会说什么');
continue;
}
dodged.add(target);
await ctx.gesture(target.dir);
continue;
}
await ctx.waitCond(() => (game.phase !== 'playing' && game.phase !== 'feature')
|| (victim && !game.obstacles.has(victim))
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));
}
ctx.status('躲过保护期、挨过一次撞,心跳有点快,先到这');
const aliveSec = Math.round((performance.now() - game.runStart) / 1000);
ctx.status(`保护期结束后撑了 ${aliveSec - 60} 秒、挨了两下还活着(警报也只起一波了),先到这`);
}