/** * 《回声地窟》新手会话剧本。 * * 规则: * - 主线剧本 `session`:一位盲眼新手玩家的完整弧线——标题开始 → DRILL 九步逐项跟练 * (脉冲/前进/转身/重发/状态/菜单/教学战/下潜)→ EXPLORE 脉冲探路、明雷战斗 → * 逐层下潜到窟主面前 → 力战而亡听死亡归因与元进度入账 → 第二周目(遗产生效、 * 不再进教学)短程收尾,完整展示死亡-重开循环。 * - 乱按压力剧本 `sessionStress`:一位手忙脚乱的好奇宝宝——标题乱划、教学乱按、 * 狂点脉冲、战斗乱滑乱逃、菜单乱逛后关掉——一样要走到死亡-重开循环,全程零死锁。 * * 剧本失真红线(参照 radio/storyline 剧本先例): * - 玩家不抢话:出手前等语音链播完(waitCond !voiceBusy); * - 玩家靠耳朵玩游戏:走位前先脉冲听回声,导航只把「听到的方向」翻译成手势; * - 绝不用 waitIdle 空等怪物/前摇(有定时器在跑,必须 waitCond/waitVirtual 有界等待); * - 菜单里只按「下滑关闭」,绝不双击执行(防止误触「保存并回大厅」结束会话)。 */ const ORDER = ['N', 'E', 'S', 'W']; const DIRS = { N: { dx: 0, dy: -1 }, E: { dx: 1, dy: 0 }, S: { dx: 0, dy: 1 }, W: { dx: -1, dy: 0 } }; const HP_BAND = { 1: '毫发无损', 2: '受了点伤', 3: '伤势不轻', 4: '命悬一线' }; export function describe(game) { if (game.menuOpen) return '菜单里:单击切换,双击确认,上滑帮助,下滑关闭'; if (game.state === 'BATTLE' && game._battle) { const b = game._battle; if (b.state === 'telegraph') return '敌方前摇声响起了——双击,格挡!'; if (b.state === 'player') { return `战斗中:你的血量${HP_BAND[game._hpBand(game.run.hp, game._maxHp())]},` + `等你出手(单击挥击,按住屏幕逃跑)`; } return '战斗播报中,先听着'; } switch (game.state) { case 'TITLE': return game.run ? '检测到探窟记录:单击继续,右滑重掷新周目,上滑听元进度' : '标题页:单击开始探窟,上滑听元进度'; case 'DRILL': return game._drill ? `教学演练第 ${game._drill.step} 步,跟着旁白做` : '教学演练中'; case 'EXPLORE': return game.run ? `第 ${game.run.floor} 层黑暗里:单击发脉冲用回声看路,按住前进,左右滑转身,上滑状态,下滑菜单` : '……'; case 'DESCEND': return '顺着楼梯,往下潜……'; case 'SETTLE': return '结算:死亡归因与元进度入账,单击再探一次'; default: return '……'; } } // ---------------------------------------------------------------- 会话工具 /** 有界等待:条件满足或超时即返回。时钟冻结(无定时器)时按迭代数兜底,绝不忙等挂死。 */ async function waitStep(ctx, mock, cond, maxMs = 30000) { 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); } } /** 转身面向 dir(左右滑,听转身声)。 */ async function faceDir(ctx, game, dir) { const d = (ORDER.indexOf(dir) - ORDER.indexOf(game.run.face) + 4) % 4; const gs = d === 3 ? 'left' : 'right'; for (let i = 0; i < (d === 3 ? 1 : d); i++) { await ctx.gesture(gs); await ctx.waitVirtual(150); } } /** BFS 寻路(避开怪物格):把「听来的方位感」翻译成一步步的手势。 */ function bfsPath(floor, from, to, blocked) { const S = floor.size; const startK = from.x + ',' + from.y; if (from.x === to.x && from.y === to.y) return []; const prev = new Map(); const seen = new Set([startK]); const q = [[from.x, from.y]]; while (q.length) { const [x, y] = q.shift(); for (const [dir, d] of Object.entries(DIRS)) { const nx = x + d.dx, ny = y + d.dy, k = nx + ',' + ny; if (nx < 0 || ny < 0 || nx >= S || ny >= S) continue; // 目标格即使被怪占着也要能走到(明雷:踏上窟主的格子触发遭遇) if (floor.grid[ny][nx] !== 1 || seen.has(k) || (blocked.has(k) && !(nx === to.x && ny === to.y))) continue; seen.add(k); prev.set(k, { x, y, dir }); if (nx === to.x && ny === to.y) { q.length = 0; break; } q.push([nx, ny]); } } const steps = []; let cur = to.x + ',' + to.y; if (!prev.has(cur)) return steps; while (cur !== startK) { const p = prev.get(cur); const [sx, sy] = cur.split(',').map(Number); steps.unshift({ x: sx, y: sy, dir: p.dir }); cur = p.x + ',' + p.y; } return steps; } const mobCells = (floor) => new Set(floor.mobs.map((m) => m.x + ',' + m.y)); /** 认真打法:前摇声响就双击格挡,轮到自己就单击挥击,直到战斗结束。 */ async function fight(ctx, game) { let guard = 0; while (game.state === 'BATTLE' && game._battle && guard < 120) { guard++; const b = game._battle; if (game.menuOpen) { await ctx.gesture('down'); await ctx.waitVirtual(600); continue; } if (b.state === 'telegraph') { ctx.note('前摇声响——双击,格挡'); await ctx.gesture('doubletap'); await ctx.waitVirtual(300); continue; } if (b.state === 'player') { ctx.note('单击,挥击'); await ctx.gesture('tap'); await ctx.waitVirtual(500); continue; } await ctx.waitVirtual(400); } await ctx.waitVirtual(3000); // 胜利链播完 } /** 力战而亡:不逃跑、顾不上格挡,喘着气隔回合挥拳——窟主的刀比你的拳快(死亡归因路线)。 */ async function fightToDeath(ctx, game) { let guard = 0; let turn = 0; while (game.state === 'BATTLE' && guard < 160) { guard++; const b = game._battle; if (!b) break; if (b.state === 'player') { if (turn % 2 === 0) { ctx.note('咬牙单击——挡不住了也要打'); await ctx.gesture('tap'); } else { ctx.note('喘一口气,再挥'); } turn++; await ctx.waitVirtual(500); continue; } await ctx.waitVirtual(400); } } /** 探索导航:脉冲听路 → 一步步走向目标格;路上撞进小怪就打完再走, * 撞进窟主(deathOnBoss)就转为力战而亡。 * R2 伪影修正:until='stairs' 时以「踏上楼梯格」为到站条件(R1 剧本按坐标 * 判到站,长按多走/被怪打断时提前收工,导致「单击下潜」点在半路变成脉冲—— * 全程零下楼演出、165 脉冲 0 次铁器回声的转写伪影根源);D9 起脉冲常开 * (玩家靠耳朵玩游戏红线:走位前先听回声)。 */ async function navigate(ctx, mock, game, target, { pulse = true, deathOnBoss = false, until = 'cell' } = {}) { for (let guard = 0; guard < 220; guard++) { if (game.state === 'BATTLE') { if (deathOnBoss && game._battle?.boss) { await fightToDeath(ctx, game); continue; } await fight(ctx, game); continue; } if (game.state !== 'EXPLORE' && game.state !== 'DRILL') return; if (!game.run) return; if (until === 'stairs' ? !!game._onStairs : (game.run.pos.x === target.x && game.run.pos.y === target.y)) return; if (pulse) { ctx.note('单击发脉冲,听回声辨路'); await ctx.gesture('tap'); await ctx.waitVirtual(2000); // 听完回声组再动身 } const path = bfsPath(game._floor, game.run.pos, target, mobCells(game._floor)); const step = (path[0] || bfsPath(game._floor, game.run.pos, target, new Set())[0]); // 无避怪路就硬走:撞进怪=明雷遭遇,打完再走 if (!step) { if (process.env.DUNGEON_SESS_DEBUG) mock.realLog('[sess-debug] !step @', JSON.stringify(game.run.pos), 'target=', JSON.stringify(target), 'onStairs=', game._onStairs, 'floor=', game.run.floor, 'mobs=', JSON.stringify(game._floor.mobs.map((m) => [m.type, m.x, m.y]))); await ctx.waitVirtual(800); continue; } // 被游走的怪堵住了:等它走开 if (process.env.DUNGEON_SESS_DEBUG && game.run.floor >= 2) { mock.realLog('[sess-debug] iter', JSON.stringify(game.run.pos), game.run.face, 'target=', JSON.stringify(target), 'state=', game.state, 'walk=', !!game._walk, 'step=', JSON.stringify(step || null), 'menu=', game.menuOpen, 't=', mock.now); } await faceDir(ctx, game, step.dir); if (game.state === 'BATTLE') continue; // 走位途中被扑进来的怪打断:先打完再走(防长按变逃跑) await ctx.gesture('longpress'); // 立即松开,走「恰 1 格」。R2 修正:原来的 waitVirtual(120) 在虚拟时钟里会 // 越过 +500ms 的下一步计时器(advance 至少推进到下一个到期定时器),每按一次 // 多走 1+ 格——到站判断永远踩不准楼梯格,剧本在楼梯两侧来回震荡 20 分钟 //(R1 转写「165 脉冲 0 铁器、两次下楼零演出」的另一根源)。 await ctx.gesture('longpressend'); await ctx.waitVirtual(200); } } /** 到站下潜:只有真踩在楼梯格上才单击(R2 伪影修正:半路的单击只是浪费的脉冲)。 * DRILL(D9) 也接受——教学最后一击 = 踏楼梯单击下潜,是设计好的出教学路径。 */ async function tapDescend(ctx, mock, game) { if (!game._onStairs || (game.state !== 'EXPLORE' && game.state !== 'DRILL')) return false; ctx.note('铁器就在脚下——单击,下潜'); await ctx.gesture('tap'); await waitStep(ctx, mock, () => game.state !== 'EXPLORE' && game.state !== 'DRILL', 4000); return true; } // ---------------------------------------------------------------- 主线剧本 export async function session(ctx, mock) { const { game } = ctx; // —— 标题:单击开始探窟(首局无存档) —— await waitStep(ctx, mock, () => game.state === 'TITLE'); ctx.status('盲眼探窟者睁开了耳朵,准备下窟'); ctx.note('单击,开始'); await ctx.gesture('tap'); // —— DRILL 九步逐项跟练 —— await waitStep(ctx, mock, () => game.state === 'DRILL' && game._drill?.step === 0); ctx.status('教学开始:三句开场白'); await waitStep(ctx, mock, () => !ctx.voiceBusy(), 12000); ctx.note('单击回应「先学会用耳朵看」'); await ctx.gesture('tap'); // D0 → D1 await waitStep(ctx, mock, () => game._drill?.step === 1); ctx.note('单击——人生第一声脉冲'); await ctx.gesture('tap'); // D1 脉冲 await waitStep(ctx, mock, () => game._drill?.step === 2); ctx.status('回声回来了,听墙的距离'); await ctx.waitVirtual(2000); await waitStep(ctx, mock, () => !ctx.voiceBusy(), 8000); ctx.note('单击继续'); await ctx.gesture('tap'); // D2 → D3 await waitStep(ctx, mock, () => game._drill?.step === 3); await waitStep(ctx, mock, () => !ctx.voiceBusy(), 8000); ctx.note('单击继续(侧向规则听完了)'); await ctx.gesture('tap'); // D3 → D4 await waitStep(ctx, mock, () => game._drill?.step === 4); ctx.note('按住屏幕不放——脚步声起,往前走'); await ctx.gesture('longpress'); await ctx.waitVirtual(1400); // 走满 2 格 ctx.note('松开,停下'); await ctx.gesture('longpressend'); await waitStep(ctx, mock, () => game._drill?.step === 5); ctx.note('左滑,转身'); await ctx.gesture('left'); await ctx.waitVirtual(400); ctx.note('右滑,转回来'); await ctx.gesture('right'); await waitStep(ctx, mock, () => game._drill?.step === 6); ctx.note('双击——再发一次脉冲,不要钱'); await ctx.gesture('doubletap'); // D6 重发 await waitStep(ctx, mock, () => game._drill?.step === 7); await ctx.waitVirtual(2000); ctx.note('上滑,听自己的状态'); await ctx.gesture('up'); await ctx.waitVirtual(5800); // 五段状态链 ctx.note('下滑,打开菜单'); await ctx.gesture('down'); await ctx.waitVirtual(1600); ctx.note('再下滑,关上菜单'); await ctx.gesture('down'); await waitStep(ctx, mock, () => game._drill?.step === 8); // —— D8 教学战:盲眼蝠 ATK=0,伤不到你 —— ctx.status('「前面有东西!放心,它伤不到你」——等它扑上来'); await waitStep(ctx, mock, () => game.state === 'BATTLE' && game._battle, 25000); ctx.status('教学怪扑到了面前'); await waitStep(ctx, mock, () => game._battle?.state === 'telegraph', 15000); ctx.note('听到它的动静——双击,格挡'); await ctx.gesture('doubletap'); await waitStep(ctx, mock, () => game._battle?.state === 'player', 15000); ctx.note('单击挥拳,两拳放倒它'); for (let i = 0; i < 6 && game.state === 'BATTLE'; i++) { if (game._battle?.state === 'telegraph') { await ctx.gesture('doubletap'); await ctx.waitVirtual(300); } else if (game._battle?.state === 'player') { await ctx.gesture('tap'); await ctx.waitVirtual(400); } else await ctx.waitVirtual(400); } await waitStep(ctx, mock, () => game._drill?.step === 9, 20000); ctx.status('教学战获胜——「打中了,就是这样」'); // —— D9:听铁器的回声,自己走到楼梯(脉冲常开:靠耳朵找到铁器再落脚) —— ctx.note('单击,听听楼梯的铁器回声'); await ctx.gesture('tap'); await ctx.waitVirtual(2200); await navigate(ctx, mock, game, game._floor.stairs, { until: 'stairs' }); ctx.note('踏上楼梯格——「楼梯就在脚下」'); if (!(await tapDescend(ctx, mock, game))) { ctx.note('(还没到楼梯,继续朝铁器回声摸过去)'); await navigate(ctx, mock, game, game._floor.stairs, { until: 'stairs' }); await tapDescend(ctx, mock, game); } await waitStep(ctx, mock, () => game.state === 'EXPLORE' && game.run?.floor === 2, 20000); ctx.status(game.run?.floor === 2 ? '教学完了。第二层,真正的地窟从这里开始' : '下潜尚未完成——先把楼梯找到'); // —— EXPLORE:脉冲探路 → 明雷战斗 → 逐层下潜到第 3 层(每层走一步验一步, // 被怪打断/没踩准则再摸一轮——R2 修正:单发 navigate+tapDescend 任何一环 // 被打断就整段脱轨,这是 R1 转写下潜零演出的第三个根源) —— while (game.run && game.run.floor < 3 && game.state !== 'SETTLE') { const from = game.run.floor; ctx.note(`单击脉冲,探一探第 ${from} 层`); await ctx.gesture('tap'); // 若正踩在楼梯上(D9 收尾),这一击就是下潜 await ctx.waitVirtual(2200); await navigate(ctx, mock, game, game._floor.stairs, { until: 'stairs' }); if (!(await tapDescend(ctx, mock, game))) { ctx.note('(还没踩准楼梯,朝铁器回声再摸一轮)'); await navigate(ctx, mock, game, game._floor.stairs, { until: 'stairs' }); if (!(await tapDescend(ctx, mock, game)) && game._onStairs) { ctx.note('铁器就在脚下——单击,下潜'); await ctx.gesture('tap'); } } await waitStep(ctx, mock, () => !game.run || game.run.floor > from || game.state === 'SETTLE', 20000); const done = game.run && game.run.floor === from + 1; ctx.status(!done ? `下潜尚未完成(还在第 ${game.run?.floor ?? '?'} 层)` : game.run.floor === 3 ? '第三层——窟主的地界' : `下潜成功,这是第 ${game.run.floor} 层`); if (!done) break; // 两轮都没下去:不空转,如实记「没下去」 } // —— L3:走向楼梯,窟主石甲蟹就守在上面 → 力战而亡 —— for (let guard = 0; guard < 3 && game.run && game.run.floor === 3 && game.state !== 'SETTLE'; guard++) { ctx.note('脉冲听路,朝楼梯摸过去'); await ctx.gesture('tap'); await ctx.waitVirtual(2200); await navigate(ctx, mock, game, game._floor.stairs || game._floor.throne, { deathOnBoss: true, until: 'stairs' }); if (game.state === 'BATTLE' && game._battle?.boss) { await fightToDeath(ctx, game); continue; } if (game._onStairs && game.state === 'EXPLORE') { await tapDescend(ctx, mock, game); continue; } break; } // —— SETTLE:死亡归因 + 元进度入账 —— await waitStep(ctx, mock, () => game.state === 'SETTLE', 60000); if (game.state === 'SETTLE') { ctx.status('倒下了。死亡归因链:死于谁、哪里没做好、带走了多少金币'); ctx.note('听完归因三问和「下个周目,你会更强」'); await ctx.waitVirtual(10000); ctx.note('单击——再探一次'); await ctx.gesture('tap'); // → TITLE(元进度入账后) await waitStep(ctx, mock, () => game.state === 'TITLE', 10000); } else { ctx.note('这一把竟然没死——那就继续往下走'); } // —— 第二周目:遗产继承、不再进教学(只有真回到 TITLE 才单击开局) —— if (game.state === 'TITLE') { ctx.note('单击,开始新周目'); await ctx.gesture('tap'); await waitStep(ctx, mock, () => game.state === 'EXPLORE' && game.run, 30000); ctx.status('新周目直落探索:遗产生效,不用再上课'); ctx.note('上滑,听新周目的局面复述'); await ctx.gesture('up'); await ctx.waitVirtual(6500); ctx.note('单击脉冲,听听熟悉的回声'); await ctx.gesture('tap'); await ctx.waitVirtual(2500); } ctx.status('第二周目短程收尾——死亡-重开循环跑通,收工'); } // ---------------------------------------------------------------- 乱按压力剧本 export async function sessionStress(ctx, mock) { const { game } = ctx; let seed = 20260901; const rng = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; }; const pick = (arr) => arr[Math.floor(rng() * arr.length)]; const NOISE = ['tap', 'tap', 'doubletap', 'left', 'right', 'up']; // down/longpress 单独管,防误触 /** 乱按的安全阀:菜单里只准「下滑关闭」,绝不双击执行。 */ const noise = async () => { if (game.menuOpen) { ctx.note('菜单乱逛完了,下滑关掉'); await ctx.gesture('down'); await ctx.waitVirtual(500); return; } if (rng() < 0.15) { // 偶尔长按(探索=前进,战斗=尝试逃跑,Boss 会拒绝) await ctx.gesture('longpress'); await ctx.waitVirtual(300); await ctx.gesture('longpressend'); await ctx.waitVirtual(200); return; } await ctx.gesture(pick(NOISE)); await ctx.waitVirtual(450); }; // ① 标题页乱划一通,然后开始 for (let i = 0; i < 10 && game.state === 'TITLE'; i++) { ctx.status('标题页乱划'); await noise(); } await ctx.gesture('tap'); // ② 教学期:一半乱按,但每步最后一定做对(跟不下来也要能走完) await waitStep(ctx, mock, () => game.state === 'DRILL' || game.state === 'EXPLORE', 20000); ctx.status('教学里乱按一气,旁白说到哪按到哪'); const RIGHT = { 0: 'tap', 1: 'tap', 2: 'tap', 3: 'tap', 4: 'longpress', 5: 'left', 6: 'doubletap', 9: 'tap' }; let stepGuard = 0; while (game.state === 'DRILL' && game._drill && stepGuard < 320) { stepGuard++; const step = game._drill.step; if (game.menuOpen) { await ctx.gesture('down'); await ctx.waitVirtual(500); continue; } if (game.state === 'BATTLE') { await chaosFight(ctx, game, rng, pick); continue; } if (rng() < 0.45) { await noise(); // 乱按:会吃 error 震、会走错路,但不死锁 continue; } if (step === 7) { // 上滑 + 开菜单再关 if (!game._drill.upDone) { await ctx.gesture('up'); await ctx.waitVirtual(5800); } else { await ctx.gesture('down'); await ctx.waitVirtual(1400); await ctx.gesture('down'); await ctx.waitVirtual(600); } continue; } if (step === 8) { // 等教学怪扑上来,交给乱打 await waitStep(ctx, mock, () => game.state === 'BATTLE' || game._drill?.step === 9, 25000); if (game.state === 'BATTLE') await chaosFight(ctx, game, rng, pick); continue; } if (step === 9) { // D9:乱按到这一步也得真走到楼梯,否则会卡死在聆听里 if (game._onStairs) { await ctx.gesture('tap'); // 单击下潜 await waitStep(ctx, mock, () => game.state !== 'DRILL', 15000); continue; } const st = game._floor.stairs || game._floor.throne; const stp = (bfsPath(game._floor, game.run.pos, st, mobCells(game._floor))[0] || bfsPath(game._floor, game.run.pos, st, new Set())[0]); if (!stp) { await ctx.gesture('tap'); await ctx.waitVirtual(1500); continue; } const d = (ORDER.indexOf(stp.dir) - ORDER.indexOf(game.run.face) + 4) % 4; await ctx.gesture(d === 3 ? 'left' : 'right'); await ctx.waitVirtual(200); await ctx.gesture('longpress'); await ctx.waitVirtual(120); await ctx.gesture('longpressend'); await ctx.waitVirtual(250); continue; } const gs = RIGHT[step]; if (!gs) { await ctx.waitVirtual(300); continue; } await ctx.gesture(gs); if (step === 4) { await ctx.waitVirtual(1400); await ctx.gesture('longpressend'); } await ctx.waitVirtual(500); } await waitStep(ctx, mock, () => game.state === 'EXPLORE' || game.state === 'BATTLE' || game.state === 'SETTLE', 25000); ctx.status('总算从教学里滚出来了(可能跳了课,可能一路错过来的)'); // ③+④ 探索/下潜/送死:一路狂点脉冲、乱走乱打,直到死一次为止(零死锁底线=SETTLE) let chaosGuard = 0; while (game.run && game.state !== 'SETTLE' && chaosGuard < 500) { chaosGuard++; if (game.state === 'DRILL') { // 乱按按出「跳过教学」(下滑在 DRILL 的原义),绝不在教学里空转 await ctx.gesture('down'); await ctx.waitVirtual(2500); continue; } if (game.state === 'BATTLE') { const b = game._battle; if (b?.boss || game.run.floor >= 3) { ctx.status('乱打:不格挡、逃不掉(Boss),一直挥拳'); await fightToDeath(ctx, game); continue; } await chaosFight(ctx, game, rng, pick); continue; } if (game.menuOpen) { await ctx.gesture('down'); await ctx.waitVirtual(400); continue; } if (game.state !== 'EXPLORE') { await ctx.waitVirtual(400); continue; } if (game._onStairs && !game._floor.mobs.some((m) => m.x === game.run.pos.x && m.y === game.run.pos.y)) { ctx.note('正好站在楼梯上——单击下潜'); await ctx.gesture('tap'); await waitStep(ctx, mock, () => game.state === 'EXPLORE' && game.run.floor >= 2, 15000); continue; } const r = rng(); if (r < 0.3) { // 狂点脉冲:冷却期就是一串 error 震,游戏不炸 ctx.note('手痒连点三下脉冲'); await ctx.gesture('tap'); await ctx.gesture('tap'); await ctx.gesture('tap'); await ctx.waitVirtual(1600); continue; } if (r < 0.45) { await noise(); continue; } // 正经走两步:脉冲听路 → 朝楼梯/王座挪 await ctx.gesture('tap'); await ctx.waitVirtual(1800); const st = game._floor.stairs || game._floor.throne; const step = (bfsPath(game._floor, game.run.pos, st, mobCells(game._floor))[0] || bfsPath(game._floor, game.run.pos, st, new Set())[0]); if (!step) { await ctx.waitVirtual(800); continue; } const d = (ORDER.indexOf(step.dir) - ORDER.indexOf(game.run.face) + 4) % 4; await ctx.gesture(d === 3 ? 'left' : 'right'); await ctx.waitVirtual(200); await ctx.gesture('longpress'); await ctx.waitVirtual(120); await ctx.gesture('longpressend'); await ctx.waitVirtual(250); } // ⑤ SETTLE → TITLE → 新周目,短程收尾 await waitStep(ctx, mock, () => game.state === 'SETTLE', 20000); if (game.state === 'SETTLE') { ctx.status('乱打也被归因得明明白白,元进度照单全收'); await ctx.waitVirtual(4000); await ctx.gesture('tap'); // → TITLE await waitStep(ctx, mock, () => game.state === 'TITLE', 10000); await noise(); // 标题页再乱划一下 await ctx.gesture('tap'); // 新周目 await waitStep(ctx, mock, () => game.state === 'EXPLORE' && game.run, 25000); ctx.status('第二周目照样开局,压力测试全程零死锁,收工'); } else { ctx.note('这一把居然没死——压力有限,收工'); } } /** 乱打:大多数时候出手,偶尔乱滑/上滑/试图逃跑;前摇里手忙脚乱。 */ async function chaosFight(ctx, game, rng, pick) { let guard = 0; while (game.state === 'BATTLE' && game._battle && guard < 160) { guard++; const b = game._battle; if (game.menuOpen) { await ctx.gesture('down'); await ctx.waitVirtual(500); continue; } if (b.state === 'telegraph') { if (rng() < 0.25) { ctx.note('手忙脚乱中碰巧双击了一下'); await ctx.gesture('doubletap'); await ctx.waitVirtual(300); } else { await ctx.waitVirtual(400); } continue; } if (b.state === 'player') { const r = rng(); if (r < 0.62) { ctx.note('单击,抡空也打'); await ctx.gesture('tap'); } else if (r < 0.72) { ctx.note('慌了,按住屏幕想跑'); await ctx.gesture('longpress'); await ctx.waitVirtual(300); await ctx.gesture('longpressend'); } else if (r < 0.88) { await ctx.gesture(pick(['left', 'right'])); } else { await ctx.gesture('up'); } await ctx.waitVirtual(500); continue; } await ctx.waitVirtual(400); } await ctx.waitVirtual(3000); }