107 lines
5.8 KiB
JavaScript
107 lines
5.8 KiB
JavaScript
/**
|
||
* 《盲航》新手会话剧本:首局完整演练 D1-D4(第二颗故意超时,听当场重教)→
|
||
* 正式对局躲过保护期(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: '下面' };
|
||
switch (game.phase) {
|
||
case 'ask': return '问我直接出发还是重听演练:单击出发,上滑重听';
|
||
case 'drill': return game.drillOb
|
||
? `演练:${DIR[game.drillOb.dir]}有陨石在逼近,${DIR[game.drillOb.dir]}滑把它拨开`
|
||
: '演练旁白在播,先听';
|
||
case 'playing': {
|
||
const n = game.obstacles.size;
|
||
const protect = performance.now() < game.protectionUntil;
|
||
return n
|
||
? `对局中:${n} 颗陨石在逼近,听方位同向滑开${protect ? '(保护期,撞了也不掉血)' : ''}`
|
||
: `对局中:引擎声平稳,等下一颗${protect ? '(保护期)' : ''}`;
|
||
}
|
||
case 'feature': return game.featureKind === 'boost' ? '暂停了刷怪教新功能:单击试试加速' : '暂停了刷怪教新功能:双击展开护盾';
|
||
case 'menu': return '暂停菜单:单击切选项,双击确认,下滑恢复';
|
||
case 'over': return `坠毁了,存活数据播报中。单击再飞一次,下滑回大厅`;
|
||
default: return '开场,稍等';
|
||
}
|
||
}
|
||
|
||
export async function session(ctx, mock) {
|
||
const { game } = ctx;
|
||
|
||
// 演练 D1-D4:逼近音一响就同向拨开(第二颗故意不出手,听当场重教)。
|
||
//(刷怪链是自驱动的定时器链——禁止裸 waitIdle,等待一律挂条件)
|
||
for (let i = 0; i < 4; i++) {
|
||
await ctx.waitCond(() => game.phase === 'drill' && game.drillIdx === i && !!game.drillOb);
|
||
ctx.status(`演练 ${i + 1}/4:${{ left: '左', right: '右', up: '上', down: '下' }[game.drillOb.dir]}侧有陨石`);
|
||
if (i === 1) {
|
||
ctx.note('这颗犹豫了,没拨开——听当场重教');
|
||
await ctx.waitCond(() => game.phase === 'drill' && !game.drillOb); // 超时 → drill_retry
|
||
await ctx.waitCond(() => game.phase === 'drill' && game.drillIdx === i && !!game.drillOb);
|
||
ctx.note('重教听完,这回马上同向滑');
|
||
await ctx.gesture(game.drillOb.dir);
|
||
continue;
|
||
}
|
||
ctx.note('逼近音一响,同向滑开');
|
||
await ctx.gesture(game.drillOb.dir);
|
||
}
|
||
ctx.status('演练做完了,要出发了');
|
||
|
||
// 正式对局:保护期 60s + 结束后再撑 20s 以上收工。
|
||
// 每颗陨石都撑到 loud 警报档(撞击前 500ms:第二段警报 + 「左!/右!…」方向
|
||
// 呼喊同时响,round3 G7)才同向拨开——仍在 1.5s 判定窗口内,稳。呼喊与警报
|
||
// 同一刻先落,随即滑开(=500ms 阈值恰在 loud 警报定时器触发点上)。
|
||
//(刷怪链是自驱动的定时器链——禁止裸 waitIdle,等待一律挂条件)
|
||
await ctx.waitCond(() => game.phase === 'playing');
|
||
ctx.status('出发。引擎声起来了,保护期里先练手');
|
||
const dodged = new Set();
|
||
const victims = []; // 保护期结束后故意不躲的(前两颗:听归因 + gd_last_life)
|
||
let eaten = 0;
|
||
while ((game.phase === 'playing' || game.phase === 'feature')
|
||
&& performance.now() < game.protectionUntil + 21000) {
|
||
if (game.phase === 'feature') {
|
||
ctx.status('游戏停下来教新功能');
|
||
// 引入句听完就照做(不裸 waitIdle——feature 态之后的刷怪链是自驱动定时器,
|
||
// 裸等会把整局对局连本带利烧穿到坠毁)
|
||
await ctx.waitCond(() => !ctx.voiceBusy());
|
||
ctx.note(game.featureKind === 'boost' ? '照它说的单击加速' : '照它说的双击开护盾');
|
||
await ctx.gesture(game.featureKind === 'boost' ? 'tap' : 'doubletap');
|
||
continue;
|
||
}
|
||
// 被撞的那颗消失 → 归因链听完了,继续
|
||
const eatenIdx = victims.findIndex((v) => !game.obstacles.has(v));
|
||
if (eatenIdx >= 0) {
|
||
victims.splice(eatenIdx, 1);
|
||
ctx.note(eaten === 2 && victims.length === 0
|
||
? '第二次被撞:先听方位归因,又听到「这是最后一次机会,撑住」——这就是全部家底了'
|
||
: '被撞了。先听到方位归因,又听到「同向滑动就能拨开」');
|
||
continue;
|
||
}
|
||
// 撑到 loud 警报档(撞击前 0.5s,第二段警报 + dir 方向呼喊同窗)再出手
|
||
//(=500ms 恰在 loud 警报定时器触发点:呼喊先落,同一拍跟着滑开)
|
||
const target = [...game.obstacles].find((o) => !dodged.has(o)
|
||
&& o.impactAt - performance.now() <= 500);
|
||
if (target) {
|
||
if (eaten < 2 && performance.now() >= game.protectionUntil) {
|
||
eaten++;
|
||
victims.push(target);
|
||
dodged.add(target); // 标记 = 故意不躲
|
||
ctx.status(eaten === 1
|
||
? '保护期结束了,这颗我故意没动'
|
||
: '又有一颗,也故意不躲——想听听第二次被撞会说什么');
|
||
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() <= 500));
|
||
}
|
||
const aliveSec = Math.round((performance.now() - game.runStart) / 1000);
|
||
ctx.status(`保护期结束后撑了 ${aliveSec - 60} 秒、挨了两下还活着;每回撞击前半秒都能听到方向呼喊,先到这`);
|
||
}
|