storyline/escape/simon 首批之后的推广收官:15 款全部按 26 号模式运行 (发言权/回执/三档提示/迷茫接管/手势阶梯/断点复述),旧「双击重听/格挡/ 菜单确认」等反模式口径全线废除,开场零规则念白(首句即动手)。 游戏层(12 款):rpg(格挡=前摇窗口内再单击)/ rhythm / space / radio / dungeon / abyss / detective / cipher / teahouse / keeper / bridge / hunt(联机切片): - 每款 game.js 接引擎六模块 + makeInputPipeline(doubleTapWindow:0); 四向玩法类 swipeCore + 长按帮助;双击是玩法的(space 护盾 / hunt 旋风斩)按手势阶梯阶段 3 分档恢复双击窗口(doubletapCore) - 每款 playtest 重写:玩法断言 + §6.2 乱点机器人(无声操作 / 被动 原句重复 / 打断失败 = 0);15 款 3900+ 断言全绿 - 12 份 docs/design/26-apply-*.md 施工单 + tools/voice_lines_26/ 台词清单 + voice-ids 文档同步 + 转写会话剧本按新口径重写 引擎层(web/engine/,6 处修复):pipeline 增 doubletapCore;novice teachLine 带 voice/ 前缀(此前教学句被静默丢弃);confusion.record 前置到新手门(锁定手势的乱点也计信号);InvalidHints 播完恢复解释前 状态(根治孤儿 BUSY);WaitPoint takeover(2) 台账去重;台账只在 speak 真正播出时登记。遗留引擎债登记 roadmap §3.1。 语音资产:台词表合并(新增 ~250 系统句 / 改 ~50 旧口径 / 删 ~110 废止句;合并器 tools/merge_voice_lines_26.py + 引用校验器 tools/validate_voice_refs.py),GPU 重录 1536 wav → 4204 ogg/m4a 落地,check_all_assets + validate_voice_refs 双绿;键盘变体同步扩表。 大厅/文档:15 款统一新交互口径(「新交互」徽标退役);README / HANDOVER / rpg README 旧口径修正;roadmap P0-P3 置完成。
115 lines
5.8 KiB
JavaScript
115 lines
5.8 KiB
JavaScript
/**
|
||
* 《盲航》新手会话剧本(26 号 playbook 重构版):首局完整演练 D1-D4(第二颗故意
|
||
* 超时听「没拨到」真话归因 + 当场重教,第三颗错向听纠错)→ 正式对局躲过保护期
|
||
* (新手期无双击/无加速:单击只听一次「现在只要滑。」;挥空听 whiff 轻叹与首次
|
||
* 解释)→ 保护期结束后故意漏两颗(第 1/2 次被锁血吸收仍给方位归因;第 3 次起
|
||
* 实打实掉血听 hit_<dir>,第 4 次追加 hit_last「最后一命」)→ 之后每颗都同向拨开,
|
||
* 再撑 20 秒以上收工(撑到结算的话单击再飞)。
|
||
*/
|
||
export function describe(game) {
|
||
const DIR = { left: '左边', right: '右边', up: '上面', down: '下面' };
|
||
switch (game.phase) {
|
||
case 'reopen': return '问我继续上次的航程还是重开:单击继续,右滑重开';
|
||
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 'over': return '坠毁了,存活数据播报中。单击再飞一次,下滑开菜单';
|
||
case 'menu': return '暂停菜单:左右滑切换,单击确认';
|
||
case 'leaving': return '回大厅确认句在播';
|
||
default: return '开场,稍等';
|
||
}
|
||
}
|
||
|
||
export async function session(ctx, mock) {
|
||
const { game } = ctx;
|
||
|
||
// 演练 D1-D4:逼近音一响就同向拨开。
|
||
// D2 故意不出手(听「没拨到。声音一响就滑。」+ 当场重教);
|
||
// D3 先滑错方向(听纠错句),重教后马上同向滑。
|
||
//(刷怪链是自驱动的定时器链——禁止裸 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); // 超时 → miss + retry
|
||
await ctx.waitCond(() => game.phase === 'drill' && game.drillIdx === i && !!game.drillOb);
|
||
ctx.note('重教听完,这回马上同向滑');
|
||
await ctx.gesture(game.drillOb.dir);
|
||
continue;
|
||
}
|
||
if (i === 2) {
|
||
ctx.note('先滑了个反方向——听一句纠错');
|
||
await ctx.gesture('left'); // D3 是 up:错向当场重教
|
||
await ctx.waitCond(() => game.phase === 'drill' && !game.drillOb);
|
||
await ctx.waitCond(() => game.phase === 'drill' && game.drillIdx === i && !!game.drillOb);
|
||
}
|
||
ctx.note('逼近音一响,同向滑开');
|
||
await ctx.gesture(game.drillOb.dir);
|
||
}
|
||
ctx.status('演练做完了,要出发了');
|
||
|
||
// 正式对局:保护期 60s + 结束后再撑 20s 以上收工。
|
||
// 新手期单击/双击都不可用:单击只听一次「现在只要滑。」;乱滑的方向听 whiff
|
||
// 轻叹与首次解释;保护期撞了听方位教学(不掉血)。
|
||
await ctx.waitCond(() => game.phase === 'playing');
|
||
ctx.status('出发。引擎声起来了,保护期里先练手');
|
||
if (!ctx.voiceBusy()) {
|
||
ctx.note('手贱点了一下屏幕——一声「现在只要滑。」,明白了,只管滑');
|
||
await ctx.gesture('tap');
|
||
}
|
||
const dodged = new Set();
|
||
const victims = []; // 保护期结束后故意不躲的(前两次锁血 + 后两次实撞)
|
||
let eaten = 0;
|
||
while ((game.phase === 'playing' || game.phase === 'over')
|
||
&& performance.now() < game.protectionUntil + 21000) {
|
||
if (game.phase === 'over') break;
|
||
// 被撞的那颗消失 → 归因链听完了,继续
|
||
const eatenIdx = victims.findIndex((v) => !game.obstacles.has(v));
|
||
if (eatenIdx >= 0) {
|
||
victims.splice(eatenIdx, 1);
|
||
ctx.note(eaten <= 2
|
||
? '被撞了但没掉血(新手锁血)——方位归因照样说真话,告诉该往哪边滑'
|
||
: '实打实挨了一下:方位归因 + 「船体只剩最后一次」——这是全部家底了');
|
||
continue;
|
||
}
|
||
// 呼啸一响就同向拨开(呼喊「左!/右!…」与呼啸同窗,round4 R1)
|
||
const target = [...game.obstacles].find((o) => !dodged.has(o));
|
||
if (target) {
|
||
if (eaten < 4 && performance.now() >= game.protectionUntil) {
|
||
eaten++;
|
||
victims.push(target);
|
||
dodged.add(target); // 标记 = 故意不躲
|
||
ctx.status(eaten <= 2
|
||
? '保护期结束了,这颗我故意不动(新手锁血,听归因)'
|
||
: '又一颗,也不躲——想听听真的挨撞会说什么');
|
||
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)));
|
||
}
|
||
if (game.phase === 'over') {
|
||
ctx.status('坠毁了——结算在播:存活时段、躲避段、单击再飞一次');
|
||
await ctx.waitCond(() => game.floor.state === 'YOUR_TURN');
|
||
ctx.note('单击,再飞一次');
|
||
await ctx.gesture('tap');
|
||
return;
|
||
}
|
||
const aliveSec = Math.round((performance.now() - game.runStart) / 1000);
|
||
ctx.status(`保护期结束后撑了 ${aliveSec - 60} 秒、挨了四下还活着;每次撞击都有方位真话,先到这`);
|
||
}
|