/** * 《深夜茶馆》新手会话剧本。 * * 剧本: * - `session`(主线):一位新掌柜的完整首晚弧线——标题开张 → 教学七步跟练 * (看茶柜/选定/提壶/出汤/端茶/状态)→ 自由营业三位客人(邮差/苏晚/陈拾, * 各泡一盏)→ 打烊对账 → 单击睡下 → 第二晚开场收束。 * - `sessionStress`(乱按压力):一位手忙脚乱的新掌柜——标题乱划、教学下滑跳过、 * 灶前乱按乱切茶罐(零进展被看门狗三级抢救)、菜单乱逛只下滑关闭、乱手势 * 不误判错步——照样泡完一盏茶走到打烊,全程零死锁。 * * 剧本失真红线(detective 剧本先例): * - 玩家不抢话:出手前等语音链收尾(waitStep 有界等待状态谓词); * - 玩家靠耳朵玩:候汤听泡、闷泡听滴答,时机手势跟着声音走; * - 营业态常驻自动推进:禁止裸 waitIdle 空等,必须 waitStep/waitVirtual; * - 菜单里只「下滑关闭」,绝不双击执行(防误触「保存并回大厅」结束会话)。 */ const TEA_NAME = { longjing: '龙井', moli: '茉莉花茶', tieguanyin: '铁观音', dianhong: '滇红', puer: '普洱', yinzhen: '白毫银针', }; export function describe(game) { if (game._menuOpen) { if (game._shopOpen) return '打烊置办里:左右滑看项目,单击买定,下滑返回'; return '菜单里:单击切换,双击确认,上滑帮助,下滑关闭'; } switch (game.state) { case 'TITLE': return game.run ? '检测到没打烊的茶馆:单击继续,右滑两次重开新店' : '标题页:单击开店,上滑听店铺概况'; case 'OPENING': return '开晚场:点灯与晚数句播报中(单击可跳句)'; case 'SERVE': { if (game._chain) 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 '闷泡:听滴答,滴答密起来时双击出汤'; } case 'CLOSE': return game._closeChainActive ? '打烊结算链播报中(账目/房租/章节),手势全部被吞' : '打烊:账报完了,单击睡下,下滑可打烊置办'; 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); const shelf = game._shelf(); let guard = 0; while (game._brew && shelf[game._brew.teaIdx] !== tea && guard++ < 10) { await ctx.gesture('right'); await ctx.waitVirtual(2500); // 茶罐名落声 } ctx.note(`茶柜上选定${TEA_NAME[tea]}——单击`); await ctx.gesture('tap'); await waitStep(ctx, mock, () => game._brew && game._brew.sub === 'WATER', 20000); // 候汤:将沸茶 ~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); // 闷泡:快 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); // 判定链+反应链收尾 } // ---------------------------------------------------------------- 主线剧本 export async function session(ctx, mock) { const { game } = ctx; // —— 标题:单击开店(首局无存档) —— await waitStep(ctx, mock, () => game.state === 'TITLE', 20000); ctx.status('深夜茶馆:巷子最深处,一盏亮到后半夜的灯'); ctx.note('单击,开店'); await ctx.gesture('tap'); // —— 开场 + 教学 DRILL 七步 —— await waitStep(ctx, mock, () => game.state === 'BREW' && game._drill && game._drill.step === 0, 90000); ctx.status('头一晚。开场三句讲清了:泡茶三步,耳朵说了算'); await quiet(ctx, mock, game, 30000); ctx.note('右滑——先看茶柜,一罐一罐听'); await ctx.gesture('right'); // D0 await waitStep(ctx, mock, () => game._drill && game._drill.step === 1, 15000); await ctx.waitVirtual(6000); ctx.note('单击,选定这罐龙井'); await ctx.gesture('tap'); // D1 → 自动坐水 await waitStep(ctx, mock, () => game._drill && game._drill.step === 2, 15000); await ctx.waitVirtual(8000); // tut_water 听完 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.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 ctx.note('上滑——听听今晚的局面'); await ctx.gesture('up'); // D5 await waitStep(ctx, mock, () => game._drill === null, 60000); ctx.status('都会了。教学完成,往后每一晚都这么泡'); // —— 自由营业:邮差(茉莉)/ 苏晚(滇红)/ 陈拾(茉莉) —— await brewTea(ctx, mock, game, 'moli', '邮差要滚烫的茉莉——双击提壶'); ctx.status(`邮差喝得很慢,多搁下两文。进账 ${game.run ? game.run.incomeTonight : '—'} 文`); await brewTea(ctx, mock, game, 'dianhong', '苏晚要暖的滇红——滚透再冲'); ctx.status(`苏晚说像被窝里塞了个汤婆子。进账 ${game.run ? game.run.incomeTonight : '—'} 文`); await brewTea(ctx, mock, 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 落声 ctx.note('上滑,再听一遍账目'); await ctx.gesture('up'); await quiet(ctx, mock, game, 30000); ctx.note('单击,睡下'); await ctx.gesture('tap'); // 睡下 → N2 await waitStep(ctx, mock, () => game.state === 'OPENING', 20000); ctx.status('第二晚。老客们已经认得这条巷子了——今晚的营业收工'); } // ---------------------------------------------------------------- 乱按压力剧本 export async function sessionStress(ctx, mock) { const { game } = ctx; let seed = 20260914; const rng = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; }; // ① 标题乱划,然后开店 for (let i = 0; i < 6 && game.state === 'TITLE'; i++) { ctx.status('标题页乱划一气'); await ctx.gesture(rng() < 0.5 ? 'left' : 'up'); await ctx.waitVirtual(600); } 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.gesture('down'); await ctx.waitVirtual(12000); // F2:听完 tut_skip → sys_help 兜底链再动手 } 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); } 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' ? '得地道' : '过去了'}——故事照样走`); } // ⑤ 后续客人:全部尽快端茶(水闷听天由命),走到打烊 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); 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) { 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.waitVirtual(6500); await ctx.gesture('doubletap'); await quiet(ctx, mock, game, 90000); } // ⑥ 打烊:死窗+链锁内乱按(全被吞),链完单击睡下 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.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); ctx.status('乱按一路也睡下了:结算链完整、零死锁,压力测试收工'); }