feat(playbook): 26 号交互手册全量落地——剩余 12 款深度重构 + 语音全量重制
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 置完成。
This commit is contained in:
+196
-173
@@ -1,19 +1,21 @@
|
||||
/**
|
||||
* 《深夜茶馆》新手会话剧本。
|
||||
* 《深夜茶馆》新手会话剧本(26 号重构版:经营+时机型,radio/storyline 范式)。
|
||||
*
|
||||
* 剧本:
|
||||
* - `session`(主线):一位新掌柜的完整首晚弧线——标题开张 → 教学七步跟练
|
||||
* (看茶柜/选定/提壶/出汤/端茶/状态)→ 自由营业三位客人(邮差/苏晚/陈拾,
|
||||
* 各泡一盏)→ 打烊对账 → 单击睡下 → 第二晚开场收束。
|
||||
* - `sessionStress`(乱按压力):一位手忙脚乱的新掌柜——标题乱划、教学下滑跳过、
|
||||
* 灶前乱按乱切茶罐(零进展被看门狗三级抢救)、菜单乱逛只下滑关闭、乱手势
|
||||
* 不误判错步——照样泡完一盏茶走到打烊,全程零死锁。
|
||||
* - `session`(主线):一位新掌柜的完整首晚弧线——开场第一句「点一下屏幕」就动手 →
|
||||
* 教学七步跟练(左右滑看柜/单击选定/听水声单击提壶/听滴答单击出汤/单击端茶/
|
||||
* 上滑状态)→ 自由营业三位客人(邮差/苏晚/陈拾,各泡一盏,靠耳朵掐时机)→
|
||||
* 打烊对账 → 上滑重听账目 → 单击睡下 → 第二晚开场收束。
|
||||
* - `sessionStress`(乱按压力):一位手忙脚乱的新掌柜——标题乱划、教学中乱试探
|
||||
* (教学不可跳过,只能跟做)、心急提前提壶被真话归因重教、菜单乱逛只下滑关闭、
|
||||
* 乱手势不误判错步——照样泡完一盏茶走到打烊,全程零死锁。
|
||||
*
|
||||
* 剧本失真红线(detective 剧本先例):
|
||||
* - 玩家不抢话:出手前等语音链收尾(waitStep 有界等待状态谓词);
|
||||
* - 玩家靠耳朵玩:候汤听泡、闷泡听滴答,时机手势跟着声音走;
|
||||
* - 营业态常驻自动推进:禁止裸 waitIdle 空等,必须 waitStep/waitVirtual;
|
||||
* - 菜单里只「下滑关闭」,绝不双击执行(防误触「保存并回大厅」结束会话)。
|
||||
* 剧本失真红线(radio/storyline 剧本先例):
|
||||
* - 玩家不抢话:只在话轮真正交出(floor.state === YOUR_TURN)之后才出手;
|
||||
* critical 句(教学指令/结算链)播完再动,抢拍只会听到 busy;
|
||||
* - 玩家靠耳朵玩:候汤听水声、闷泡听滴答,时机手势跟着声音走(单击即时,无双击);
|
||||
* - 营业态常驻自动推进:禁止裸 waitIdle 空等,必须 waitCond/waitVirtual 有界等待;
|
||||
* - 菜单里只「下滑关闭」,绝不确认「保存并回大厅」(防误触结束会话)。
|
||||
*/
|
||||
|
||||
const TEA_NAME = {
|
||||
@@ -21,77 +23,64 @@ const TEA_NAME = {
|
||||
dianhong: '滇红', puer: '普洱', yinzhen: '白毫银针',
|
||||
};
|
||||
|
||||
/** 26 号 API:真等待态(链收+话轮已交)。 */
|
||||
const atWait = (game) => !game.floor.speaking && game.floor.state === 'YOUR_TURN';
|
||||
|
||||
export function describe(game) {
|
||||
if (game._menuOpen) {
|
||||
if (game._shopOpen) return '打烊置办里:左右滑看项目,单击买定,下滑返回';
|
||||
return '菜单里:单击切换,双击确认,上滑帮助,下滑关闭';
|
||||
if (game._shopOpen) return '打烊置办里:左右滑看项目,单击买定,下滑返回菜单';
|
||||
return '菜单里:左右滑切项,单击确认,上滑重听说明,下滑关闭';
|
||||
}
|
||||
switch (game.state) {
|
||||
case 'TITLE': return game.run
|
||||
? '检测到没打烊的茶馆:单击继续,右滑两次重开新店'
|
||||
: '标题页:单击开店,上滑听店铺概况';
|
||||
case 'OPENING': return '开晚场:点灯与晚数句播报中(单击可跳句)';
|
||||
: '标题页:单击开店(第一句就是动手指令),上滑听店铺概况';
|
||||
case 'OPENING': return '开晚场:晚数句播报中(单击跳句,滑动打断并推进)';
|
||||
case 'SERVE': {
|
||||
if (game._chain) return '堂前:客人进门/点单/反应播报中(单击跳句,双击重听)';
|
||||
return '堂前空闲:单击进灶前,上滑状态,下滑菜单';
|
||||
if (game.floor.speaking) return '堂前:客人进门/点单/反应播报中(单击跳句)';
|
||||
return '堂前空档:上滑问状态,下滑菜单(自动流马上进灶)';
|
||||
}
|
||||
case 'BREW': {
|
||||
const b = game._brew;
|
||||
if (!b) return '灶前';
|
||||
if (game._drill) return `教学中(第 ${game._drill.step} 步):跟着旁白做,下滑可跳过`;
|
||||
if (b.sub === 'TEA') return '选茶:左右滑换茶罐,单击选定,双击重听茶名句';
|
||||
if (b.sub === 'WATER') return '候汤:听水声,泡泡细密时双击提壶';
|
||||
return '闷泡:听滴答,滴答密起来时双击出汤';
|
||||
if (game._drill) return `教学中(第 ${game._drill.step} 步):跟着旁白做(教学不可跳)`;
|
||||
if (b.sub === 'TEA') return '选茶:左右滑换茶罐(茶名句随身),单击选定';
|
||||
if (b.sub === 'WATER') return '候汤:听水声,泡泡密起来时单击提壶';
|
||||
return '闷泡:听滴答,滴答密起来时单击出汤';
|
||||
}
|
||||
case 'CLOSE': return game._closeChainActive
|
||||
? '打烊结算链播报中(账目/房租/章节),手势全部被吞'
|
||||
: '打烊:账报完了,单击睡下,下滑可打烊置办';
|
||||
? '打烊结算链播报中(账目/房租/章节),手势全部 busy 被吞'
|
||||
: '打烊:账报完了,单击睡下,上滑重听账目,下滑菜单(含打烊置办)';
|
||||
default: return '……';
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 会话工具
|
||||
|
||||
/** 有界等待:条件满足或超时即返回。 */
|
||||
async function waitStep(ctx, mock, cond, maxMs = 40000) {
|
||||
const t0 = mock.now;
|
||||
let spins = 0;
|
||||
const spinCap = Math.max(60, Math.floor(maxMs / 50));
|
||||
while (!cond() && mock.now - t0 < maxMs && spins < spinCap) {
|
||||
spins++;
|
||||
await ctx.waitVirtual(100);
|
||||
}
|
||||
}
|
||||
|
||||
/** 等当前语音与链收尾。 */
|
||||
async function quiet(ctx, mock, game, maxMs = 60000) {
|
||||
await waitStep(ctx, mock, () => !game.audio.isVoicePlaying() && !game._chain, maxMs);
|
||||
}
|
||||
|
||||
/** 泡当前客人:选茶(循环到目标)→ 候汤窗内双击 → 闷泡窗内双击。 */
|
||||
async function brewTea(ctx, mock, game, tea, note) {
|
||||
await waitStep(ctx, mock, () => game.state === 'BREW' && game._brew && game._brew.sub === 'TEA' && !game._drill, 90000);
|
||||
/** 泡当前客人:选茶(循环到目标)→ 候汤窗内单击提壶 → 闷泡窗内单击出汤。 */
|
||||
async function brewTea(ctx, game, tea, note) {
|
||||
await ctx.waitCond(() => game.state === 'BREW' && game._brew && game._brew.sub === 'TEA' && !game._drill);
|
||||
const shelf = game._shelf();
|
||||
let guard = 0;
|
||||
while (game._brew && shelf[game._brew.teaIdx] !== tea && guard++ < 10) {
|
||||
while (game._brew && game._brew.teaIdx >= 0 && shelf[game._brew.teaIdx] !== tea && guard++ < 10) {
|
||||
await ctx.gesture('right');
|
||||
await ctx.waitVirtual(2500); // 茶罐名落声
|
||||
await ctx.waitVirtual(2500); // 茶罐名句落声
|
||||
}
|
||||
ctx.note(`茶柜上选定${TEA_NAME[tea]}——单击`);
|
||||
await ctx.gesture('tap');
|
||||
await waitStep(ctx, mock, () => game._brew && game._brew.sub === 'WATER', 20000);
|
||||
await ctx.waitCond(() => game._brew && game._brew.sub === 'WATER');
|
||||
// 候汤:将沸茶 ~3.5s / 滚透茶 ~6.5s(首晚 ×1.3 更宽);听泡变密再提壶
|
||||
const simmer = tea === 'longjing' || tea === 'yinzhen';
|
||||
await ctx.waitVirtual(simmer ? 4200 : 6800);
|
||||
ctx.note(note || '水声对了——双击提壶');
|
||||
await ctx.gesture('doubletap');
|
||||
await waitStep(ctx, mock, () => game._brew && game._brew.sub === 'STEEP', 20000);
|
||||
ctx.note(note || '水声对了——单击提壶');
|
||||
await ctx.gesture('tap');
|
||||
await ctx.waitCond(() => game._brew && game._brew.sub === 'STEEP');
|
||||
// 闷泡:快 7s / 中 11s / 久 15s,滴答密起入窗
|
||||
const steepAt = { longjing: 7000, moli: 7000, yinzhen: 11000, tieguanyin: 11000, dianhong: 15000, puer: 15000 }[tea] || 7000;
|
||||
await ctx.waitVirtual(steepAt - 800);
|
||||
ctx.note('滴答密了——双击出汤');
|
||||
await ctx.gesture('doubletap');
|
||||
await quiet(ctx, mock, game, 90000); // 判定链+反应链收尾
|
||||
ctx.note('滴答密了——单击出汤');
|
||||
await ctx.gesture('tap');
|
||||
await ctx.waitCond(() => !game.audio.isVoicePlaying() && !game.floor.speaking); // 判定链+反应链收尾
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 主线剧本
|
||||
@@ -99,178 +88,212 @@ async function brewTea(ctx, mock, game, tea, note) {
|
||||
export async function session(ctx, mock) {
|
||||
const { game } = ctx;
|
||||
|
||||
// —— 标题:单击开店(首局无存档) ——
|
||||
await waitStep(ctx, mock, () => game.state === 'TITLE', 20000);
|
||||
// —— 开场(§4.10):第一句就是动手指令「点一下屏幕。」——
|
||||
await ctx.waitCond(() => game.floor.state === 'YOUR_TURN');
|
||||
ctx.status('深夜茶馆:巷子最深处,一盏亮到后半夜的灯');
|
||||
ctx.note('单击,开店');
|
||||
await ctx.gesture('tap');
|
||||
ctx.note('第一句就让我动手——点一下');
|
||||
await ctx.gesture('tap'); // 开店 → 「轮到你了」状态声 → OPENING
|
||||
|
||||
// —— 开场 + 教学 DRILL 七步 ——
|
||||
await waitStep(ctx, mock, () => game.state === 'BREW' && game._drill && game._drill.step === 0, 90000);
|
||||
ctx.status('头一晚。开场三句讲清了:泡茶三步,耳朵说了算');
|
||||
await quiet(ctx, mock, game, 30000);
|
||||
// —— 教学 DRILL 七步(单击版:跟旁白做,一次一个动作)——
|
||||
await ctx.waitCond(() => game.state === 'BREW' && game._drill && game._drill.step === 0
|
||||
&& game.floor.state === 'YOUR_TURN');
|
||||
ctx.status('头一晚。老周进门要一盏龙井——旁白带着我一步一步来');
|
||||
ctx.note('右滑——先看茶柜,一罐一罐听');
|
||||
await ctx.gesture('right'); // D0
|
||||
await waitStep(ctx, mock, () => game._drill && game._drill.step === 1, 15000);
|
||||
await ctx.waitVirtual(6000);
|
||||
await ctx.gesture('right'); // D0:换罐即学「单击选定」
|
||||
await ctx.waitCond(() => game._drill && game._drill.step === 1);
|
||||
ctx.note('滑回龙井——老周要的就是它');
|
||||
await ctx.gesture('left');
|
||||
await ctx.waitVirtual(2500);
|
||||
ctx.note('单击,选定这罐龙井');
|
||||
await ctx.gesture('tap'); // D1 → 自动坐水
|
||||
await waitStep(ctx, mock, () => game._drill && game._drill.step === 2, 15000);
|
||||
await ctx.waitVirtual(8000); // tut_water 听完
|
||||
await ctx.gesture('tap'); // D1 → 教学句说完自动坐水
|
||||
await ctx.waitCond(() => game._drill && game._drill.step === 2);
|
||||
await ctx.waitVirtual(3600); // 听觉分层:教学句说完水声才起,等泡泡密起来
|
||||
ctx.status('灶上坐水了。听——泡泡细密起来,就是提壶的时候');
|
||||
await ctx.waitVirtual(2000); // ≈4.2s(首晚宽窗)
|
||||
ctx.note('泡泡细密了——双击,提壶');
|
||||
await ctx.gesture('doubletap'); // D2
|
||||
await waitStep(ctx, mock, () => game._drill && game._drill.step === 3, 20000);
|
||||
await ctx.waitVirtual(8000); // tut_steep 听完 + 闷泡头段
|
||||
ctx.note('滴答声密起来——双击,出汤');
|
||||
await ctx.gesture('doubletap'); // D3
|
||||
await waitStep(ctx, mock, () => game._drill && game._drill.step === 4, 30000);
|
||||
ctx.note('泡泡细密了——单击,提壶');
|
||||
await ctx.gesture('tap'); // D2(窗内)
|
||||
await ctx.waitCond(() => game._drill && game._drill.step === 3);
|
||||
await ctx.waitVirtual(6400); // 闷泡滴答从密到缓,龙井快冲 ~7s
|
||||
ctx.note('滴答声密起来——单击,出汤');
|
||||
await ctx.gesture('tap'); // D3(窗内)
|
||||
await ctx.waitCond(() => game._drill && game._drill.step === 4);
|
||||
ctx.status('这一盏泡得地道。茶好了,端出去');
|
||||
ctx.note('单击,端出去');
|
||||
await ctx.gesture('tap'); // D4
|
||||
await waitStep(ctx, mock, () => game._drill && game._drill.step === 5, 60000);
|
||||
await ctx.waitVirtual(9000); // 反应链 + tut_status
|
||||
await ctx.gesture('tap'); // D4 → 反应链 → 第一盏茶送出
|
||||
await ctx.waitCond(() => game._drill && game._drill.step === 5
|
||||
&& game.floor.state === 'YOUR_TURN');
|
||||
ctx.status('链尾教了「想知道自己在哪,就向上滑」');
|
||||
ctx.note('上滑——听听今晚的局面');
|
||||
await ctx.gesture('up'); // D5
|
||||
await waitStep(ctx, mock, () => game._drill === null, 60000);
|
||||
await ctx.gesture('up'); // D5:状态链(点单句+客人序+口碑+存款)
|
||||
await ctx.waitCond(() => game._drill === null);
|
||||
ctx.status('都会了。教学完成,往后每一晚都这么泡');
|
||||
|
||||
// —— 自由营业:邮差(茉莉)/ 苏晚(滇红)/ 陈拾(茉莉) ——
|
||||
await brewTea(ctx, mock, game, 'moli', '邮差要滚烫的茉莉——双击提壶');
|
||||
// —— 自由营业:邮差(茉莉)/ 苏晚(滇红)/ 陈拾(茉莉)——
|
||||
await brewTea(ctx, game, 'moli', '邮差要滚烫的茉莉——水滚透再提');
|
||||
ctx.status(`邮差喝得很慢,多搁下两文。进账 ${game.run ? game.run.incomeTonight : '—'} 文`);
|
||||
await brewTea(ctx, mock, game, 'dianhong', '苏晚要暖的滇红——滚透再冲');
|
||||
await brewTea(ctx, game, 'dianhong', '苏晚要暖的滇红——滚透再冲');
|
||||
ctx.status(`苏晚说像被窝里塞了个汤婆子。进账 ${game.run ? game.run.incomeTonight : '—'} 文`);
|
||||
await brewTea(ctx, mock, game, 'moli', '陈拾要香的茉莉提神——双击提壶');
|
||||
await brewTea(ctx, game, 'moli', '陈拾要香的茉莉提神');
|
||||
ctx.status(`陈拾把早饭钱花在了这盏茶上。进账 ${game.run ? game.run.incomeTonight : '—'} 文`);
|
||||
|
||||
// —— 上滑对账,然后打烊 ——
|
||||
await waitStep(ctx, mock, () => game.state === 'CLOSE' && game._closeChainActive, 60000);
|
||||
ctx.status('客人走尽了。收壶,打烊——今晚进账听账目链');
|
||||
await waitStep(ctx, mock, () => game._closeDone, 90000);
|
||||
await ctx.waitVirtual(6000); // settle_night 落声
|
||||
// —— 打烊:账目链 → 上滑重听 → 单击睡下 ——
|
||||
await ctx.waitCond(() => game.state === 'CLOSE' && game._closeDone);
|
||||
await ctx.waitCond(() => atWait(game)); // settle_night 落声 + 睡下等待点武装
|
||||
ctx.status('客人走尽了。账报完了——今晚进账都在链上');
|
||||
ctx.note('上滑,再听一遍账目');
|
||||
await ctx.gesture('up');
|
||||
await quiet(ctx, mock, game, 30000);
|
||||
await ctx.waitCond(() => atWait(game));
|
||||
ctx.note('单击,睡下');
|
||||
await ctx.gesture('tap'); // 睡下 → N2
|
||||
await waitStep(ctx, mock, () => game.state === 'OPENING', 20000);
|
||||
await ctx.gesture('tap'); // 睡下 → N2(链尾教「向下滑,可以打开菜单」)
|
||||
await ctx.waitCond(() => game.state === 'OPENING');
|
||||
ctx.status('第二晚。老客们已经认得这条巷子了——今晚的营业收工');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 乱按压力剧本
|
||||
// 目标:全程零死锁、零静默死局,一样能走到打烊睡下。
|
||||
// 剧本特征:乱划乱点、抢拍跳句、心急提前提壶、菜单乱逛,但仍保留「最终能通关」的底线。
|
||||
|
||||
export async function sessionStress(ctx, mock) {
|
||||
const { game } = ctx;
|
||||
let seed = 20260914;
|
||||
const rng = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
|
||||
|
||||
// ① 标题乱划,然后开店
|
||||
// ① 标题乱划(上滑/下滑还没解锁——只听到 invalid 回执和一句解释),然后开店
|
||||
for (let i = 0; i < 6 && game.state === 'TITLE'; i++) {
|
||||
ctx.status('标题页乱划一气');
|
||||
await ctx.gesture(rng() < 0.5 ? 'left' : 'up');
|
||||
await ctx.waitVirtual(600);
|
||||
}
|
||||
ctx.note('不划了——点一下,开店');
|
||||
await ctx.gesture('tap');
|
||||
|
||||
// ② 教学:乱按几下,然后下滑跳过
|
||||
await waitStep(ctx, mock, () => game.state === 'BREW' && game._drill, 60000);
|
||||
let dg = 0;
|
||||
while (game._drill && dg++ < 12) {
|
||||
if (rng() < 0.55) {
|
||||
await ctx.gesture(['tap', 'doubletap', 'left', 'right'][Math.floor(rng() * 4)]);
|
||||
await ctx.waitVirtual(700);
|
||||
continue;
|
||||
}
|
||||
ctx.note('下滑,跳过教学');
|
||||
// ② 教学:乱试探几下(教学不可跳过,下滑只会拿到无效回执),然后跟做
|
||||
await ctx.waitCond(() => game.state === 'BREW' && game._drill && game._drill.step === 0
|
||||
&& game.floor.state === 'YOUR_TURN');
|
||||
for (let i = 0; i < 5 && game._drill && game._drill.step === 0; i++) {
|
||||
ctx.status('教学中乱按——旁白不理我,还在等我跟做');
|
||||
await ctx.gesture(['doubletap', 'down', 'left', 'tap', 'right'][i]);
|
||||
await ctx.waitVirtual(900);
|
||||
}
|
||||
ctx.status('教学跳不过去,只能照做——一次一个动作');
|
||||
await ctx.waitCond(() => game.floor.state === 'YOUR_TURN');
|
||||
if (game._drill && game._drill.step === 0) {
|
||||
await ctx.gesture('right'); // D0
|
||||
await ctx.waitCond(() => game._drill && game._drill.step === 1 && game.floor.state === 'YOUR_TURN');
|
||||
}
|
||||
if (game._drill && game._drill.step === 1) {
|
||||
// 心急提前点/乱滑几下也无妨(D1 只认单击选定)
|
||||
await ctx.gesture(rng() < 0.5 ? 'left' : 'right');
|
||||
await ctx.waitVirtual(2000);
|
||||
await ctx.gesture('tap'); // D1
|
||||
}
|
||||
await ctx.waitCond(() => game._drill && game._drill.step === 2);
|
||||
// ③ 心急提前提壶:窗外单击 → 真话归因 + 重教,再来一次点对
|
||||
ctx.note('水还没响透就点——听归因怎么说');
|
||||
await ctx.waitVirtual(1200);
|
||||
await ctx.gesture('tap');
|
||||
await ctx.waitCond(() => game.floor.state === 'YOUR_TURN');
|
||||
if (game._drill && game._drill.step === 2 && game._brew && game._brew.water === null) {
|
||||
ctx.status('偏早被真话归因了:这一步偏了,水声重起——听密了再提');
|
||||
await ctx.waitVirtual(3600);
|
||||
await ctx.gesture('tap');
|
||||
}
|
||||
await ctx.waitCond(() => game._drill && (game._drill.step === 3 || game._drill.step === 4));
|
||||
if (game._drill && game._drill.step === 3) {
|
||||
await ctx.waitVirtual(6600);
|
||||
ctx.note('滴答密了——单击,出汤');
|
||||
await ctx.gesture('tap'); // D3
|
||||
}
|
||||
await ctx.waitCond(() => game._drill && game._drill.step === 4 && game.floor.state === 'YOUR_TURN');
|
||||
ctx.note('单击,端出去');
|
||||
await ctx.gesture('tap'); // D4
|
||||
await ctx.waitCond(() => game._drill === null || (game._drill && game._drill.step === 5));
|
||||
if (game._drill && game._drill.step === 5) {
|
||||
ctx.note('上滑,把状态链听完');
|
||||
await ctx.gesture('up'); // D5
|
||||
await ctx.waitCond(() => game._drill === null);
|
||||
}
|
||||
ctx.status('乱按也被带着走完了教学——核心动词始终可达');
|
||||
|
||||
// ④ 自由营业第一位:手痒开菜单乱逛(只敢下滑关掉),照客人要的泡
|
||||
await ctx.waitCond(() => game.state === 'BREW' && game._brew && game._brew.sub === 'TEA' && !game._drill
|
||||
&& game.floor.state === 'YOUR_TURN');
|
||||
if (rng() < 0.8) {
|
||||
ctx.note('手痒开菜单乱逛——只敢下滑关掉');
|
||||
await ctx.gesture('down');
|
||||
await ctx.waitVirtual(12000); // F2:听完 tut_skip → sys_help 兜底链再动手
|
||||
await ctx.waitVirtual(2500);
|
||||
await ctx.gesture('right');
|
||||
await ctx.waitVirtual(2200);
|
||||
await ctx.gesture('down'); // 关菜单回灶前
|
||||
await ctx.waitVirtual(800);
|
||||
}
|
||||
await quiet(ctx, mock, game, 20000);
|
||||
ctx.status('跳过教学,自由营业(可能一路错过来的)');
|
||||
|
||||
// ③ 灶前乱按 150s(零进展——被无进展轨三级抢救:earcon → 口诀 → 重播点单)
|
||||
await waitStep(ctx, mock, () => game.state === 'BREW' && game._brew && game._brew.sub === 'TEA' && !game._drill, 90000);
|
||||
const t0 = mock.now;
|
||||
let ri = 0;
|
||||
while (game.state === 'BREW' && mock.now - t0 < 150000) {
|
||||
const gg = ['left', 'right', 'tap', 'doubletap', 'left', 'right'][ri++ % 6];
|
||||
if (gg === 'tap' && game._brew && game._brew.sub !== 'TEA') {
|
||||
await ctx.gesture('doubletap'); // 时机步:乱双击也算出手
|
||||
} else {
|
||||
await ctx.gesture(gg);
|
||||
}
|
||||
if (rng() < 0.10) {
|
||||
ctx.note('手痒开菜单乱逛——只敢下滑关掉');
|
||||
await ctx.gesture('down');
|
||||
await ctx.waitVirtual(2500);
|
||||
await ctx.gesture('down');
|
||||
await ctx.waitVirtual(800);
|
||||
continue;
|
||||
}
|
||||
await ctx.waitVirtual(ri % 6 === 5 ? 4200 : 1300);
|
||||
const want = game._guest ? game._guest.tea : 'longjing';
|
||||
const shelf = game._shelf();
|
||||
let guard = 0;
|
||||
while (game._brew && game._brew.sub === 'TEA' && shelf[game._brew.teaIdx] !== want && guard++ < 10) {
|
||||
await ctx.gesture('right');
|
||||
await ctx.waitVirtual(2500);
|
||||
}
|
||||
ctx.status('乱按也被抢救:听到了口诀和点单句重播——核心动词始终可达');
|
||||
|
||||
// ④ 顺着提示认真泡完当前这盏(零死锁底线)
|
||||
if (game.state === 'BREW' && game._brew) {
|
||||
const want = game._guest ? game._guest.tea : 'longjing';
|
||||
const shelf = game._shelf();
|
||||
let guard = 0;
|
||||
while (game._brew && game._brew.sub === 'TEA' && shelf[game._brew.teaIdx] !== want && guard++ < 10) {
|
||||
await ctx.gesture('right');
|
||||
await ctx.waitVirtual(2500);
|
||||
}
|
||||
if (game._brew && game._brew.sub === 'TEA') {
|
||||
ctx.note(`不折腾了,照客人要的${TEA_NAME[want]}泡`);
|
||||
await ctx.gesture('tap');
|
||||
await waitStep(ctx, mock, () => game._brew && game._brew.sub !== 'TEA', 20000);
|
||||
}
|
||||
if (game._brew && game._brew.sub === 'WATER') {
|
||||
await ctx.waitVirtual((want === 'longjing' || want === 'yinzhen') ? 4200 : 6800);
|
||||
await ctx.gesture('doubletap');
|
||||
await waitStep(ctx, mock, () => game._brew && game._brew.sub === 'STEEP', 20000);
|
||||
}
|
||||
if (game._brew && game._brew.sub === 'STEEP' && game._brew.steep === null) {
|
||||
await ctx.waitVirtual(6500);
|
||||
await ctx.gesture('doubletap');
|
||||
}
|
||||
await quiet(ctx, mock, game, 90000);
|
||||
ctx.status(`这盏泡${game.run && game.run.results[game.run.results.length - 1] === 'good' ? '得地道' : '过去了'}——故事照样走`);
|
||||
if (game._brew && game._brew.sub === 'TEA') {
|
||||
ctx.note(`不折腾了,照客人要的${TEA_NAME[want]}泡`);
|
||||
await ctx.gesture('tap');
|
||||
await ctx.waitCond(() => game._brew && game._brew.sub !== 'TEA');
|
||||
}
|
||||
if (game._brew && game._brew.sub === 'WATER') {
|
||||
// 时机窗内乱滑不判错步(分层铁律:只有回执声,水声照走)——滑两下再提
|
||||
await ctx.gesture('left');
|
||||
await ctx.waitVirtual(400);
|
||||
await ctx.gesture('right');
|
||||
await ctx.waitVirtual((want === 'longjing' || want === 'yinzhen') ? 4200 : 6800);
|
||||
ctx.note('水声对了——单击提壶');
|
||||
await ctx.gesture('tap');
|
||||
await ctx.waitCond(() => game._brew && game._brew.sub === 'STEEP');
|
||||
}
|
||||
if (game._brew && game._brew.sub === 'STEEP' && game._brew.steep === null) {
|
||||
const steepAt = { longjing: 7000, moli: 7000, yinzhen: 11000, tieguanyin: 11000, dianhong: 15000, puer: 15000 }[want] || 7000;
|
||||
await ctx.waitVirtual(steepAt - 800);
|
||||
await ctx.gesture('tap');
|
||||
}
|
||||
await ctx.waitCond(() => !game.audio.isVoicePlaying() && !game.floor.speaking);
|
||||
ctx.status(`这盏泡${game.run && game.run.results[game.run.results.length - 1] === 'good' ? '得地道' : '过去了'}——故事照样走`);
|
||||
|
||||
// ⑤ 后续客人:全部尽快端茶(水闷听天由命),走到打烊
|
||||
let sg = 0;
|
||||
while (game.state !== 'CLOSE' && sg++ < 4) {
|
||||
await waitStep(ctx, mock, () => game.state === 'BREW' && game._brew && game._brew.sub === 'TEA' && !game._drill, 90000);
|
||||
// 最后一位泡完会直接进 CLOSE:等待条件必须带上这个出口
|
||||
await ctx.waitCond(() => (game.state === 'BREW' && game._brew && game._brew.sub === 'TEA' && !game._drill)
|
||||
|| game.state === 'CLOSE');
|
||||
if (game.state !== 'BREW') break;
|
||||
const want = game._guest ? game._guest.tea : 'longjing';
|
||||
const shelf = game._shelf();
|
||||
let guard = 0;
|
||||
while (game._brew && game._brew.sub === 'TEA' && shelf[game._brew.teaIdx] !== want && guard++ < 10) {
|
||||
const w2 = game._guest ? game._guest.tea : 'longjing';
|
||||
const sh2 = game._shelf();
|
||||
let g2 = 0;
|
||||
while (game._brew && game._brew.sub === 'TEA' && sh2[game._brew.teaIdx] !== w2 && g2++ < 10) {
|
||||
await ctx.gesture('right');
|
||||
await ctx.waitVirtual(2200);
|
||||
}
|
||||
await ctx.gesture('tap');
|
||||
await waitStep(ctx, mock, () => game._brew && game._brew.sub === 'WATER', 20000);
|
||||
await ctx.waitVirtual((want === 'longjing' || want === 'yinzhen') ? 4200 : 6800);
|
||||
await ctx.gesture('doubletap');
|
||||
await waitStep(ctx, mock, () => game._brew && game._brew.sub === 'STEEP', 20000);
|
||||
await ctx.waitCond(() => game._brew && game._brew.sub === 'WATER');
|
||||
await ctx.waitVirtual((w2 === 'longjing' || w2 === 'yinzhen') ? 4200 : 6800);
|
||||
await ctx.gesture('tap');
|
||||
await ctx.waitCond(() => game._brew && game._brew.sub === 'STEEP');
|
||||
await ctx.waitVirtual(6500);
|
||||
await ctx.gesture('doubletap');
|
||||
await quiet(ctx, mock, game, 90000);
|
||||
await ctx.gesture('tap');
|
||||
await ctx.waitCond(() => !game.audio.isVoicePlaying() && !game.floor.speaking);
|
||||
}
|
||||
|
||||
// ⑥ 打烊:死窗+链锁内乱按(全被吞),链完单击睡下
|
||||
await waitStep(ctx, mock, () => game.state === 'CLOSE' && game._closeChainActive, 90000);
|
||||
ctx.status('打烊链在播——乱按一气(应该全被吞掉)');
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await ctx.gesture(['tap', 'down', 'left', 'doubletap'][i % 4]);
|
||||
// ⑥ 打烊:结算链播报中乱按(链锁全吞),链完单击睡下
|
||||
await ctx.waitCond(() => game.state === 'CLOSE');
|
||||
ctx.status('打烊链在播——乱按一气(链锁内应该全被吞掉)');
|
||||
// 只在链播中乱按(closeDone 即停:等待态里 tap/doubletap 会真的睡下)
|
||||
for (let i = 0; i < 5 && game._closeChainActive; i++) {
|
||||
await ctx.gesture(['left', 'up', 'down', 'tap', 'right'][i % 5]);
|
||||
await ctx.waitVirtual(500);
|
||||
}
|
||||
await waitStep(ctx, mock, () => game._closeDone, 120000);
|
||||
await ctx.waitVirtual(6000);
|
||||
await ctx.gesture('tap'); // 睡下
|
||||
await waitStep(ctx, mock, () => game.state === 'OPENING', 30000);
|
||||
await ctx.waitCond(() => game._closeDone);
|
||||
await ctx.waitCond(() => atWait(game)); // settle_night 落声 + 睡下等待点武装
|
||||
if (game.state === 'CLOSE') {
|
||||
ctx.note('单击,睡下');
|
||||
await ctx.gesture('tap');
|
||||
}
|
||||
await ctx.waitCond(() => game.state === 'OPENING');
|
||||
ctx.status('乱按一路也睡下了:结算链完整、零死锁,压力测试收工');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user