abyss《深海回音》: - R1 补 synthesis(5/5 愿意玩 P0=0)+ 补录 R1 缺失裁决材料 - R2 十修全部现场坐实:F1 ping迟到/尾句保护首播制/链末看门狗不拆/回波静默一拍/dv4_dual扣账口播/crit水面变体/看门狗13s/震循环1.1s/震去重/状态链人数句 - 补录语音18段(flock串行,QC无离群);playtest 242→278 - 终裁:4/4 人设满分档,连续 2 轮达标 → 可交付 ✅ detective《雨夜专案》: - R1 取证轮:补跑 live 两会话(认真新手33分钟全弧线/乱按200s三级保底抢救),5/5 愿意玩 P0=0 - R2 十一修全部坐实:intro完整落声/tut不被掐/按轴取句/人名指认/结案确认闸/指路句/选句记忆位/hint轻震/st_wrong当场记账/tut_skip补动词/L2疑点齐分支 - 补录+改词33条(29新增+4重录);playtest 265→279 - 终裁:6/6 愿意玩,连续 2 轮达标 → 可交付 ✅ 公共:00-program 批次2进度账收账、13号文档 R2 断言关键词、gen_voice_dual 两游戏段、voice_text.json 验证:abyss 278/278 + detective 279/279 + guide 122/122 + check_all_assets 全项目全绿
385 lines
19 KiB
JavaScript
385 lines
19 KiB
JavaScript
/**
|
||
* 《雨夜专案》新手会话剧本。
|
||
*
|
||
* 剧本:
|
||
* - `session`(主线):一位新手探长的完整破案弧线——标题开始 → DRILL 七步跟练
|
||
* (换卷宗/听取/重听/状态/长按指证/练习判定)→ 证词板翻听全部六份卷宗 →
|
||
* 逐对比对指证出全部四处矛盾(周先生四连谎)→ 结案陈词先错指小顺听不在场
|
||
* 证明,再正指周先生、答对凶器与动机 → 认罪链与法槌 → 评级结算 → 单击接
|
||
* 新案(pack 翻转,变体真凶小顺)短打一处指证收尾。
|
||
* - `sessionStress`(乱按压力):一位手忙脚乱的好奇宝宝——标题乱划、教学乱按、
|
||
* 证词板狂点乱滑(零进展被看门狗三级抢救)、菜单乱逛只下滑关闭、结案乱答
|
||
* 再修正——一样走到 VERDICT+SETTLE 与新案开局,全程零死锁。
|
||
*
|
||
* 剧本失真红线(参照 dungeon/radio 剧本先例):
|
||
* - 玩家不抢话:出手前等语音链/判定链收尾(waitStep 有界等待状态谓词);
|
||
* - 玩家靠耳朵玩:指证前先翻听卷宗,导航只把「听到的内容」翻译成手势;
|
||
* - BOARD 常驻无进展轮询定时器:禁止裸 waitIdle 空等,必须 waitStep/waitVirtual;
|
||
* - 菜单里只「下滑关闭」,绝不双击执行(防误触「保存并回大厅」结束会话)。
|
||
*/
|
||
|
||
const LISTEN_ITEMS = ['w1', 'w2', 'w3', 'w4', 'scene', 'fore'];
|
||
const ITEM_ORDER = [...LISTEN_ITEMS, 'case'];
|
||
|
||
const ITEM_LABEL = {
|
||
w1: '门卫老葛的证词', w2: '学徒小顺的证词', w3: '账房周先生的证词', w4: '房东温太太的证词',
|
||
scene: '现场勘验', fore: '法医报告', case: '结案陈词',
|
||
};
|
||
|
||
export function describe(game) {
|
||
if (game.menuOpen) return '菜单里:单击切换,双击确认,上滑帮助,下滑关闭';
|
||
if (game._play) return `正在听${ITEM_LABEL[ITEM_ORDER[game._play.itemIdx]]}(单击停止,双击重新听当前句)`;
|
||
switch (game.state) {
|
||
case 'TITLE': return game.run
|
||
? '检测到未结的案子:单击继续,右滑换一桩新案,上滑听案情简介'
|
||
: '标题页:单击开始查案,上滑听案情简介';
|
||
case 'DRILL': return game._drill
|
||
? `教学第 ${game._drill.step} 步,跟着旁白做(下滑可跳过)`
|
||
: '教学中';
|
||
case 'BOARD': {
|
||
const label = ITEM_LABEL[ITEM_ORDER[game.itemIdx]];
|
||
return `证词板上是「${label}」:左右滑换卷宗,单击听取,按住进入指证,上滑状态,下滑菜单`;
|
||
}
|
||
case 'CONFRONT': {
|
||
const cf = game._cf;
|
||
if (!cf) return '指证中';
|
||
if (cf.phase === 'judged') return '判定播报中,先听着';
|
||
const ph = { item1: '选第一份证词', line1: '第一份里逐句听', confirm1: '就这句?单击定下来',
|
||
item2: '选与它矛盾的那份', line2: '第二份里逐句听', confirm2: '就这句?单击定下来' }[cf.phase];
|
||
return `指证中:${ph}(左右滑换,单击确认,双击重听)`;
|
||
}
|
||
case 'FINISH': {
|
||
const fin = game._fin;
|
||
if (!fin) return '结案中';
|
||
const ph = { who: '指认真凶:四位证人选一个', whoConfirm: '就指认他?单击定下来',
|
||
weapon: '第二问:凶器是什么', motive: '第三问:动机是什么' }[fin.phase];
|
||
return `结案三问——${ph}(左右滑选,单击定)`;
|
||
}
|
||
case 'VERDICT': return '裁决链播报中:认罪三段与法槌,手势全部被吞(链锁)';
|
||
case 'SETTLE': return '结算:评级与账目播完,单击接新案(pack 翻转)';
|
||
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);
|
||
}
|
||
}
|
||
|
||
/** 滑到指定卷宗条目(最短环绕方向)。 */
|
||
async function navToItem(ctx, game, itemId) {
|
||
const target = ITEM_ORDER.indexOf(itemId);
|
||
let guard = 0;
|
||
while (game.itemIdx !== target && guard++ < 10) {
|
||
const d = (target - game.itemIdx + ITEM_ORDER.length) % ITEM_ORDER.length;
|
||
await ctx.gesture(d * 2 <= ITEM_ORDER.length ? 'right' : 'left');
|
||
await ctx.waitVirtual(200);
|
||
}
|
||
await ctx.waitVirtual(700); // 签名 earcon → 条目名落声
|
||
}
|
||
|
||
/** 完整听取当前卷宗(单击 → 等链播完回空闲)。 */
|
||
async function listenItem(ctx, mock, game, itemId) {
|
||
await navToItem(ctx, game, itemId);
|
||
ctx.note(`单击,听取${ITEM_LABEL[itemId]}`);
|
||
await ctx.gesture('tap');
|
||
await waitStep(ctx, mock, () => game._play === null && game.state === 'BOARD', 85000);
|
||
}
|
||
|
||
/** 指证内选一句:滑到条目 → 单击 → 滑到句 → 单击确认 → 听完「就这句?」单击定下。 */
|
||
async function cfSelectLine(ctx, mock, game, caseData, lineId) {
|
||
const itemId = caseData.lineItem[lineId];
|
||
const target = LISTEN_ITEMS.indexOf(itemId);
|
||
let guard = 0;
|
||
while ((game._cf?.idx ?? -1) !== target && guard++ < 8) {
|
||
const d = (target - game._cf.idx + LISTEN_ITEMS.length) % LISTEN_ITEMS.length;
|
||
await ctx.gesture(d * 2 <= LISTEN_ITEMS.length ? 'right' : 'left');
|
||
await ctx.waitVirtual(200);
|
||
}
|
||
await ctx.waitVirtual(700);
|
||
ctx.note(`选定「${ITEM_LABEL[itemId]}」——单击确认`);
|
||
await ctx.gesture('tap');
|
||
await ctx.waitVirtual(400);
|
||
const ids = caseData.layout.lines[itemId];
|
||
const li = ids.indexOf(lineId);
|
||
let lg = 0;
|
||
while ((game._cf?.lineIdx ?? -1) !== li && lg++ < 8) {
|
||
const d = (li - game._cf.lineIdx + ids.length) % ids.length;
|
||
await ctx.gesture(d * 2 <= ids.length ? 'right' : 'left');
|
||
await ctx.waitVirtual(400); // 一句一听
|
||
}
|
||
ctx.note('听到那句对不上的了——单击,进入确认');
|
||
await ctx.gesture('tap');
|
||
await waitStep(ctx, mock, () => game._cf && (game._cf.phase === 'confirm1' || game._cf.phase === 'confirm2'), 25000);
|
||
await ctx.waitVirtual(17000); // 重播该句 + 「就这句?」听完
|
||
ctx.note('单击,定下这句');
|
||
await ctx.gesture('tap');
|
||
}
|
||
|
||
/** 从证词板发起一次完整指证(按住进入 + 两侧选句),等判定链收尾回板。
|
||
* 长按带重试:E2 受理条件=无语音在播——播报尾音未排空时补一拍重试。 */
|
||
async function confront(ctx, mock, game, lineA, lineB, noteA, noteB) {
|
||
ctx.note(noteA);
|
||
for (let attempt = 0; attempt < 3 && game.state !== 'CONFRONT'; attempt++) {
|
||
await ctx.gesture('longpress');
|
||
if (game.state !== 'CONFRONT') await ctx.waitVirtual(1500);
|
||
}
|
||
await waitStep(ctx, mock, () => game.state === 'CONFRONT' && game._cf && game._cf.phase === 'item1', 12000);
|
||
await ctx.waitVirtual(9000); // cf_start 听完
|
||
const cs = game._case;
|
||
await cfSelectLine(ctx, mock, game, cs, lineA);
|
||
await ctx.waitVirtual(17500); // 「再选与它矛盾的那份」+ 第一份自动重播
|
||
ctx.note(noteB);
|
||
await cfSelectLine(ctx, mock, game, cs, lineB);
|
||
await waitStep(ctx, mock, () => game.state === 'BOARD' && !game._play, 95000);
|
||
}
|
||
|
||
/** 从证词板走到结案陈词并单击进 FINISH(R2 F5:先过确认闸 fin_pre,闸内再单击才进三问)。 */
|
||
async function enterFinish(ctx, mock, game) {
|
||
await navToItem(ctx, game, 'case');
|
||
ctx.note('翻到结案陈词——单击过确认闸(「就此结案?」),再单击进三问');
|
||
await ctx.gesture('tap');
|
||
await ctx.waitVirtual(3000); // fin_pre 闸句落声
|
||
await ctx.gesture('tap');
|
||
await waitStep(ctx, mock, () => game.state === 'FINISH', 12000);
|
||
}
|
||
|
||
/** 等结案问句链播完(busy 解除 + 首选项报名)。 */
|
||
async function waitQuestion(ctx, mock, game, phase) {
|
||
await waitStep(ctx, mock, () => game._fin && game._fin.phase === phase && !game._fin.busy, 30000);
|
||
await ctx.waitVirtual(1000);
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 主线剧本
|
||
|
||
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, 20000);
|
||
ctx.status('开场三句案情简介 + 「先熟悉证词板」');
|
||
await ctx.waitVirtual(37000);
|
||
ctx.note('右滑——换一份卷宗听听');
|
||
await ctx.gesture('right'); // D0 → D1
|
||
await waitStep(ctx, mock, () => game._drill?.step === 1, 8000);
|
||
await ctx.waitVirtual(10000); // 「单击,听取当前卷宗」+ 回正老葛报名
|
||
ctx.note('单击——听老葛的证词');
|
||
await ctx.gesture('tap'); // D1 真实证词链播
|
||
await waitStep(ctx, mock, () => game._drill?.step === 2, 80000);
|
||
ctx.status('一份证词听完了——「已听一份,还有五份」');
|
||
await ctx.waitVirtual(10000); // 「双击,重新听这一句」
|
||
ctx.note('双击——最后一句再过一遍耳朵');
|
||
await ctx.gesture('doubletap'); // D2 → D3
|
||
await waitStep(ctx, mock, () => game._drill?.step === 3, 15000);
|
||
await ctx.waitVirtual(10000); // 「上滑,随时听进度」
|
||
ctx.note('上滑——听听问了几份、疑点几处');
|
||
await ctx.gesture('up'); // D3 → D4
|
||
await waitStep(ctx, mock, () => game._drill?.step === 4, 45000);
|
||
await ctx.waitVirtual(10000); // 「按住屏幕,进入指证」
|
||
ctx.note('按住屏幕——进入指证');
|
||
await ctx.gesture('longpress'); // D4 → D5(CONFRONT 练习局)
|
||
ctx.status('练习一次指证:判定被教学截获,不算数');
|
||
await ctx.waitVirtual(18000); // cf_start + 练习指令
|
||
const cs1 = game._case;
|
||
await cfSelectLine(ctx, mock, game, cs1, cs1.layout.lines.w2[0]);
|
||
await ctx.waitVirtual(17500);
|
||
await cfSelectLine(ctx, mock, game, cs1, cs1.layout.lines.scene[0]);
|
||
await waitStep(ctx, mock, () => game.state === 'BOARD', 40000);
|
||
ctx.status('教学完了——「指证就是这样,真矛盾要靠耳朵找」');
|
||
|
||
// —— 自由调查:翻听全部六份卷宗 ——
|
||
const cs = game._case;
|
||
for (const id of LISTEN_ITEMS) {
|
||
await listenItem(ctx, mock, game, id);
|
||
ctx.status(`已听 ${game.run.heard.length}/6 份——${ITEM_LABEL[id]}过完耳朵`);
|
||
}
|
||
ctx.note('上滑核对进度:六份都听完了,疑点还没坐实');
|
||
await ctx.gesture('up');
|
||
await ctx.waitVirtual(26000);
|
||
|
||
// —— 指证穿插:四处矛盾逐对比对(周先生四连谎) ——
|
||
const pairs = [
|
||
['z_2', 'wen_2', '周先生说「顺手熄了灯」——先定下这句', '温太太说「十点二十灯还亮着」——对上了!'],
|
||
['z_3', 'w2_3', '周先生说「钥匙贴身带着」', '小顺说「钥匙平日挂柴房铜钩」——对不上!'],
|
||
['z_4', 'fo_3', '周先生说「伤是松木劈柴砸的」', '法医说「创口嵌有铜屑,凶器是铜器」——对不上!'],
|
||
['z_4', 'sc_4', '还是周先生那句「铜秤卖给收废品的了」', '现场勘验说「铜秤立在架上」——对不上!'],
|
||
];
|
||
for (let i = 0; i < pairs.length; i++) {
|
||
const [a, b, na, nb] = pairs[i];
|
||
await confront(ctx, mock, game, a, b, na, nb);
|
||
ctx.status(`疑点 ${game._doubtCount()}/4 坐实——真凶的反应一次比一次慌`);
|
||
}
|
||
ctx.status('四处疑点齐了——「疑点齐了,去结案陈词」');
|
||
|
||
// —— 结案三问:先错指一次听不在场证明,再正指 ——
|
||
await enterFinish(ctx, mock, game);
|
||
await waitQuestion(ctx, mock, game, 'who');
|
||
ctx.note('先怀疑小顺——右滑选他,单击');
|
||
await ctx.gesture('right');
|
||
await ctx.waitVirtual(800);
|
||
await ctx.gesture('tap');
|
||
await ctx.waitVirtual(18000); // 「就指认他?」听完
|
||
ctx.note('单击,定下来');
|
||
await ctx.gesture('tap');
|
||
await waitStep(ctx, mock, () => game.state === 'BOARD', 35000);
|
||
ctx.status('错怪了——小顺宿在城南刘掌柜家,不在场证明成立,回到证词板(无限再机会)');
|
||
|
||
await enterFinish(ctx, mock, game);
|
||
await waitQuestion(ctx, mock, game, 'who');
|
||
ctx.note('证据都指向周先生——右滑两下选他');
|
||
await ctx.gesture('right');
|
||
await ctx.gesture('right');
|
||
await ctx.waitVirtual(800);
|
||
await ctx.gesture('tap');
|
||
await ctx.waitVirtual(18000);
|
||
ctx.note('单击,正式指认');
|
||
await ctx.gesture('tap');
|
||
await waitQuestion(ctx, mock, game, 'weapon');
|
||
ctx.status('第二问:凶器是什么?四个选项一个个听');
|
||
{
|
||
const wpIdx = cs.layout.wpOpts.indexOf('wp_2'); // 「铜秤砣」——铜屑与不见了的秤砣都指向它
|
||
for (let i = 0; i < wpIdx; i++) { await ctx.gesture('right'); await ctx.waitVirtual(400); }
|
||
ctx.note('铜屑不答应别的答案——选「铜秤砣」,单击');
|
||
await ctx.gesture('tap');
|
||
}
|
||
await waitQuestion(ctx, mock, game, 'motive');
|
||
ctx.status('第三问:动机是什么?');
|
||
{
|
||
const mvIdx = cs.layout.mvOpts.indexOf('mv_1'); // 「监守自盗」——库里短的东西就是引线
|
||
for (let i = 0; i < mvIdx; i++) { await ctx.gesture('right'); await ctx.waitVirtual(400); }
|
||
ctx.note('账上短的东西才是引线——选「监守自盗」,单击');
|
||
await ctx.gesture('tap');
|
||
}
|
||
|
||
// —— 裁决:认罪链 + 法槌 + 结算(链锁,手势全吞) ——
|
||
await waitStep(ctx, mock, () => game.state === 'VERDICT', 30000);
|
||
ctx.status('证据链闭合——周先生认罪三段,雨声渐强又落下,法槌落定');
|
||
await waitStep(ctx, mock, () => game.state === 'SETTLE', 150000);
|
||
const gi = game._gradeInfo;
|
||
ctx.status(`案件归档,雨停了——评级 ${gi ? gi.grade : '?'}:疑点 ${gi ? gi.outcome.doubts : '?'}/4,错指 ${gi ? gi.outcome.wrongWho : '?'} 次`);
|
||
ctx.note('单击——接新案(同一场雨里,还有别的案子)');
|
||
await ctx.gesture('tap');
|
||
await waitStep(ctx, mock, () => game.state === 'BOARD' && game.run?.pack === 1, 30000);
|
||
ctx.status('新案开卷:pack 翻转——这回的真凶换了人');
|
||
|
||
// —— 变体案短打:一处指证(小顺的谎)收尾 ——
|
||
await confront(ctx, mock, game, 'b_shun_1', 'wen_2',
|
||
'小顺说「我走的时候熄了灯」——变体案的第一句谎', '温太太还是那句话「灯还亮着」——对上了!');
|
||
ctx.status(`变体案疑点 ${game._doubtCount()}/4——真凶另有其人,收工`);
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 乱按压力剧本
|
||
|
||
export async function sessionStress(ctx, mock) {
|
||
const { game } = ctx;
|
||
let seed = 20260909;
|
||
const rng = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
|
||
|
||
// ① 标题页乱划,然后开始
|
||
for (let i = 0; i < 8 && game.state === 'TITLE'; i++) {
|
||
ctx.status('标题页乱划一气');
|
||
await ctx.gesture(rng() < 0.5 ? 'left' : 'doubletap');
|
||
await ctx.waitVirtual(450);
|
||
}
|
||
await ctx.gesture('tap');
|
||
|
||
// ② 教学:乱按几下,然后下滑跳过(跟不下来也要能走完)
|
||
await waitStep(ctx, mock, () => game.state === 'DRILL' || game.state === 'BOARD', 25000);
|
||
let dg = 0;
|
||
while (game.state === 'DRILL' && dg++ < 30) {
|
||
if (rng() < 0.5) {
|
||
await ctx.gesture(['tap', 'doubletap', 'left', 'right', 'up'][Math.floor(rng() * 5)]);
|
||
await ctx.waitVirtual(450);
|
||
continue;
|
||
}
|
||
ctx.note('下滑,跳过教学');
|
||
await ctx.gesture('down');
|
||
await ctx.waitVirtual(1200);
|
||
}
|
||
await waitStep(ctx, mock, () => game.state === 'BOARD', 25000);
|
||
ctx.status('总算从教学里滚出来了(可能一路错过来的)');
|
||
|
||
// ③ 证词板乱按 185s(零进展——被看门狗三级抢救:earcon → 短句 → 保底矛盾对;
|
||
// 菜单乱逛的挂起时长会被无进展轨剔除,故墙钟跑满 185s 保证 L3 落窗)。
|
||
// 起停对=同拍双 tap(与语音时长解耦:一句都不完成=确定性零进展)。
|
||
const t0 = mock.now;
|
||
const ROT = ['pair', 'right', 'left', 'doubletap', 'left', 'right'];
|
||
let ri = 0;
|
||
while (game.state === 'BOARD' && mock.now - t0 < 185000) {
|
||
const gg = ROT[ri % ROT.length];
|
||
const atCase = game.itemIdx === ITEM_ORDER.length - 1;
|
||
if (gg === 'pair' && !atCase) {
|
||
await ctx.gesture('tap'); // 起播
|
||
await ctx.gesture('tap'); // 立刻停止——一句都听不完
|
||
} else if (atCase) {
|
||
await ctx.gesture('left'); // 不误入结案陈词
|
||
} else {
|
||
await ctx.gesture(gg);
|
||
}
|
||
if (rng() < 0.12) {
|
||
ctx.note('手痒开菜单乱逛——只敢下滑关掉');
|
||
await ctx.gesture('down');
|
||
await ctx.waitVirtual(1500);
|
||
await ctx.gesture('down');
|
||
await ctx.waitVirtual(600);
|
||
continue;
|
||
}
|
||
await ctx.waitVirtual(ri++ % ROT.length === ROT.length - 1 ? 3600 : 1100);
|
||
}
|
||
ctx.status('乱按也被抢救:听到了保底提示直接给出可指证的矛盾对');
|
||
|
||
// ④ 顺着保底提示真指证一处(零死锁底线:核心动词始终可达)
|
||
const cs = game._case;
|
||
if (game.state === 'BOARD' && game._doubtCount() < 4) {
|
||
const contra = cs.contradictions.find((c) => c.hint) || cs.contradictions[0];
|
||
await confront(ctx, mock, game, contra.a, contra.b,
|
||
`照着提示指证:${contra.a} 这句`, `对上 ${contra.b} 这句`);
|
||
ctx.status(`乱按局也能坐实疑点(${game._doubtCount()}/4)`);
|
||
}
|
||
|
||
// ⑤ 结案:乱答一次再答对(错指无限再机会 + 问题不重答只记账)
|
||
let fg = 0;
|
||
while (game.state !== 'SETTLE' && fg++ < 12) {
|
||
await enterFinish(ctx, mock, game);
|
||
await waitQuestion(ctx, mock, game, 'who');
|
||
const whoIdx = fg === 1 ? Math.floor(rng() * 4) : LISTEN_ITEMS.indexOf(cs.culprit);
|
||
for (let i = 0; i < whoIdx; i++) { await ctx.gesture('right'); await ctx.waitVirtual(300); }
|
||
ctx.note(fg === 1 ? '先胡乱指一个听听反应' : '这回指着正主');
|
||
await ctx.gesture('tap');
|
||
await ctx.waitVirtual(18000);
|
||
await ctx.gesture('tap');
|
||
if (game.state === 'FINISH' && game._fin?.phase !== 'who') {
|
||
for (const [phase, want] of [['weapon', cs.questions.weapon.answer], ['motive', cs.questions.motive.answer]]) {
|
||
await waitQuestion(ctx, mock, game, phase);
|
||
const opts = phase === 'weapon' ? cs.layout.wpOpts : cs.layout.mvOpts;
|
||
const idx = opts.indexOf(want);
|
||
for (let i = 0; i < idx; i++) { await ctx.gesture('right'); await ctx.waitVirtual(300); }
|
||
await ctx.gesture('tap');
|
||
await ctx.waitVirtual(1500);
|
||
}
|
||
}
|
||
await waitStep(ctx, mock, () => game.state !== 'FINISH', 40000);
|
||
}
|
||
await waitStep(ctx, mock, () => game.state === 'SETTLE', 150000);
|
||
ctx.status('乱按一路也把案子结了:裁决链完整、评级如实记账');
|
||
|
||
// ⑥ 接新案短程收尾(压力全程零死锁)
|
||
await ctx.gesture('tap');
|
||
await waitStep(ctx, mock, () => game.state === 'BOARD' && game.run, 30000);
|
||
ctx.status('新案照样开局,压力测试收工');
|
||
}
|