/** * 《深海回音》新手会话剧本。 * * 剧本: * - `session`(主线):一位深潜救援员的完整弧线——听完甲板简报、四潜全下 * (每潜:单击发脉冲 → 听回波 → 通讯 → 抉择)、上浮结算与补给照单全收、 * 走双载第四潜,最后用省下的气下去取船钟,以完整 E1「海燕归钟」收尾 * (全右省气 + p9 左 = §4.5 的 P4 友好默认路径)。 * - `sessionStress`(乱按压力):一位好奇宝宝乱划乱点、快速跳句、菜单里乱逛, * 一样要能走到 ENDING,全程零死锁、零静默死局。 * * 剧本失真红线(参照 radio 剧本先例): * - 玩家不抢话:只在语音链播完(waitCond !voicePlaying)或抉择武装后才出手; * - 玩家不干晾自己:只在 COMMS 叙事等待态偶尔走神触发一次看门狗,链一停就接话; * - 搜索等待挂着看门狗定时器,禁止裸 waitIdle 空等,必须 waitCond 有界等待; * - 通讯里每句之间是「链末=继续」的等待态——真人听完一句就点一下,不机械、不抢跑。 */ const DIVE_LABEL = ['一', '二', '三', '四']; function diveLabel(n) { return n >= 1 && n <= 4 ? DIVE_LABEL[n - 1] : '·'; } export function describe(game) { if (game.leaving) return '已确认保存并回大厅'; if (game.phase === 'ASK') return '进游戏先听到「单击继续上次,右滑新救援」的询问'; if (game.phase === 'MENU') { const names = ['回到水下', '重新开始这次救援', '保存并回大厅']; return `菜单里,当前项「${names[game.menuIdx]}」:左右滑换项,单击确认,上滑重听说明,下滑关闭`; } if (game.phase === 'INTRO') { return game.voicePlaying ? '甲板简报正在播(就一句目标)' : '简报播完了,等单击下潜'; } if (game.phase === 'DIVE') { if (game.searchArmed) return '声呐静了,轮到你了:单击发一枚脉冲'; return game.voicePlaying ? `第${diveLabel(game.diveN)}潜下潜中,调度在报位` : '……'; } if (game.choiceArmed) { return `「请选择」念完了(第${diveLabel(game.diveN)}潜):向左或向右滑做出你的决定`; } if (game.voicePlaying) return `第${diveLabel(game.diveN)}潜通讯中,正在听对方说话`; if (game.phase === 'COMMS') return '对方说完了,等我接话(链末单击继续)'; if (game.phase === 'SURFACE') { return game.voicePlaying ? '上浮结算播报中(扣账/补给/档位)' : '结算链间隙,等单击继续'; } if (game.phase === 'ENDING') { return game.awaitingRestart ? '结局和逐人回响都听完了。单击再来一局,下滑开菜单' : '结局播报中'; } return '……'; } // ---------------------------------------------------------------- 主线剧本 export async function session(ctx, mock) { const { game } = ctx; ctx.note('开局:第一句就让我点屏幕——点完听目标,第一潜开始后声呐会静下来'); let firstPing = false; let firstEcho = false; let invalidTried = false; let menuVisited = false; let walkedAway = false; let choiceIdx = 0; // 抉择路径(全右省气 + p9 左取船钟 → e1 海燕归钟): // p1 直接上 / p2 不等日志 / p3 快切 / p4 直接上浮 / p5 先带小郑 / // p6 约定来接 / p7 快浮 / p8 不问 / p9 用最后的气下去取钟 const choices = [ { dir: 'right', why: '小舟先上来,暗号回头再说' }, { dir: 'right', why: '日志可以补,人不能等' }, { dir: 'right', why: '切割枪快切,老白憋住气' }, { dir: 'right', why: '阀门交给母船,腿要紧' }, { dir: 'right', why: '小郑重伤先走,陈医官下一潜就是你的' }, { dir: 'right', why: '四小时内我下来,等我' }, { dir: 'right', why: '来不及了,直浮!医疗舱的灯先开好' }, { dir: 'right', why: '纪律就是纪律,拉他上浮' }, { dir: 'left', why: '最后一潜。不为救人,为个交代——下去取船钟' }, ]; for (let i = 0; i < 300 && !game.leaving; i++) { // 只等当前语音链播完 / 抉择武装 / 搜索轮到自己;绝不用裸 waitIdle await ctx.waitCond(() => !game.voicePlaying || game.choiceArmed || game.searchArmed); if (game.leaving) break; if (game.searchArmed && game.phase === 'DIVE') { if (!firstPing) { firstPing = true; ctx.status(`第${diveLabel(game.diveN)}潜到深度了,声呐静了`); ctx.note('调度说了:单击,发一枚脉冲——耳朵交给声呐'); } else { ctx.status(`第${diveLabel(game.diveN)}潜,再点一枚`); } await ctx.gesture('tap'); if (!firstEcho) { firstEcho = true; await ctx.waitCond(() => game.phase === 'COMMS' || !game.voicePlaying); if (game.phase === 'COMMS') ctx.status('金属回波后面有杂音——电台接通了,小舟的声音进来了'); } continue; } if (game.choiceArmed) { const pick = choices[choiceIdx]; if (!pick) break; // 抉择走完(p9 之后应已进 ENDING) if (!invalidTried) { invalidTried = true; ctx.note(pick.why + '——但先上滑听一遍潜次和氧气'); await ctx.gesture('up'); await ctx.waitCond(() => !game.voicePlaying); ctx.status('听完状态链(第几潜+已救几人+氧气档+一个动作),抉择等待点回来了'); await ctx.waitCond(() => game.choiceArmed || !game.voicePlaying); if (!game.choiceArmed) continue; // 没回到武装态就再等一轮 } ctx.status('选项念完了,做决定'); ctx.note(pick.why); await ctx.gesture(pick.dir); // 念完即选,不干晾(犹豫会在等待态空烧提示三档) choiceIdx++; continue; } if (game.phase === 'ENDING') { await ctx.waitCond(() => game.awaitingRestart && !game.voicePlaying); if (!menuVisited) { // 第一局打完,下滑菜单此刻才真正解锁(教学句挂在结局链尾) menuVisited = true; ctx.note('听完结局链尾那句「向下滑,可以打开菜单」——去看看菜单'); await ctx.gesture('down'); await ctx.waitCond(() => !game.voicePlaying); ctx.status('菜单说明和第一项「回到水下」报完了'); await ctx.gesture('right'); // 右滑下一项报名(不单击,绝不误触确认) await ctx.waitCond(() => !game.voicePlaying); ctx.status('听完「重新开始这次救援」的报名,不动它'); await ctx.gesture('down'); // 下滑关闭(不确认,绝不误触「重开/回大厅」) await ctx.waitCond(() => game.awaitingRestart && !game.voicePlaying); ctx.status('菜单关了,结局链复述了一遍局面,回到结算'); } break; // 会话收尾(结局听完、菜单逛过) } if (game.phase === 'SURFACE') { // 上浮结算自动链(扣账/补给/档位)——播完自动进下一潜;签收链中段 // (g9_knot → p9)是链末等待,单击继续 await ctx.waitCond(() => !game.voicePlaying || game.choiceArmed || game.searchArmed); if (!game.voicePlaying && !game.choiceArmed && !game.searchArmed) { await ctx.gesture('tap'); // 链真停了(非句间):签收链中段单击继续 } continue; } // COMMS 叙事等待态(链末=继续):真人听完一句就接话,偶尔走神一次。 // 不要在这里 waitVirtual 小憩——等待点挂着静默提示定时器,waitVirtual 会 // 一口气跳到下一档提示(13s full),剧本就变成「每句干晾到提示响」。 if (!walkedAway && game.phase === 'COMMS' && !game.choiceArmed) { walkedAway = true; ctx.silence(14, '听到一半走了神,在想氧气还剩几格'); await ctx.waitVirtual(14600); // 触发等待点第一档提示:「单击,继续。」 ctx.status('听到一句轻提「单击,继续」,马上点'); await ctx.gesture('tap'); continue; } await ctx.gesture('tap'); // 链末单击 = 继续 } await ctx.waitCond(() => game.phase === 'ENDING' && game.awaitingRestart && !game.voicePlaying); ctx.status('结局链走完,尾声逐人点到我每一潜的救与错过'); ctx.note('船钟出水,五个人都在甲板上——会话结束(E1 海燕归钟)'); } // ---------------------------------------------------------------- 乱按压力剧本 // 目标:全程零死锁、零静默死局,一样能走到 ENDING。 // 剧本特征:不读提示、快速跳句、菜单里乱逛、错手势试探,但保留「最终能通关」底线。 export async function sessionStress(ctx, mock) { const { game } = ctx; let seed = 20260910; const rng = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; }; const gestures = ['tap', 'left', 'right', 'up', 'down', 'doubletap']; const pick = (arr) => arr[Math.floor(rng() * arr.length)]; let menuOpenCount = 0; let choiceCount = 0; let pingCount = 0; let noiseCount = 0; for (let i = 0; i < 500; i++) { await ctx.waitCond(() => !game.voicePlaying || game.choiceArmed || game.searchArmed); if (game.leaving) break; if (game.searchArmed && game.phase === 'DIVE') { pingCount++; await ctx.gesture('tap'); // 声呐静着就一直点——单击恒=发脉冲 continue; } if (game.phase === 'MENU') { menuOpenCount++; await ctx.gesture(pick(['left', 'right'])); // 乱逛换项(每项报名,不确认防误触) await ctx.waitCond(() => !game.voicePlaying); await ctx.gesture('down'); // 下滑关闭(不单击确认,避免误触「重开/回大厅」) continue; } if (game.choiceArmed) { choiceCount++; if (rng() < 0.35) { // 好奇宝宝:犹豫时乱试一个不推进剧情的手势(重听/问状态/无效) noiseCount++; await ctx.gesture(pick(['up', 'tap', 'doubletap'])); await ctx.waitCond(() => game.choiceArmed || !game.voicePlaying); } await ctx.gesture(pick(['left', 'right'])); continue; } if (game.phase === 'ENDING') { await ctx.waitCond(() => game.awaitingRestart); break; } if (game.phase === 'SURFACE') { // 结算自动链:等它自己走完;签收链中段链末等待同样乱点一下(零静默) await ctx.waitCond(() => !game.voicePlaying || game.choiceArmed || game.searchArmed); if (!game.voicePlaying && !game.choiceArmed && !game.searchArmed) { await ctx.gesture('tap'); } continue; } // COMMS 叙事等待态 / 其他静默帧:乱划一下(无效手势也要有反馈,零静默) noiseCount++; await ctx.gesture(pick(gestures)); } await ctx.waitCond(() => game.phase === 'ENDING' && game.awaitingRestart); ctx.status(`乱按剧本收尾:脉冲 ${pingCount} 次、${choiceCount} 个抉择、乱试 ${noiseCount} 次、逛菜单 ${menuOpenCount} 次`); ctx.note('全程没有任何一次手势被无声吞掉,也没有卡死在某个状态里——结局链照样走完'); }