- abyss:game.js 242 断言全绿(剧本A-F六路径/氧气门三形态/sonarGate仲裁/结算死窗/双轨看门狗/radio 8项修复继承);资产 148句+11key变体(电台滤波带内能量47-62%→69-80%) - detective:game.js 265 断言真资产下全绿(矩阵不变式独立重算/指证全路径/评级12组注入/VERDICT死窗/二周目变体);真资产复跑回炉3处实现修复(无进展轨墙钟化/菜单暂停记账/停止播报真停声——mock时长掩盖的正确性bug) - 资产:TTS 539条×2格式、Mureka 7首 $0.21(累计$1.08/$2.00)、SYN12+SA3 19件(QC抓出1例AAC过冲并修复)、封面×2、check_all_assets 10游戏全绿(abyss 298/298、detective 279/279) - 平台接线:大厅双卡片、BUCKETS 增 abyss/detective 桶(H4 断言更新 10 游戏)、voice_text.json 10游戏991句 - 回归:14 套件全绿(10 playtest+guide+engine-voice+engine-input+api-smoke 12/12) - 设计偏差登记:abyss 7条/detective 9条(全为设计内部冲突按承诺清单/归因真话律取优)
229 lines
10 KiB
JavaScript
229 lines
10 KiB
JavaScript
/**
|
|
* 《深海回音》新手会话剧本。
|
|
*
|
|
* 剧本:
|
|
* - `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 menuDone = 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; // 复述没回到武装态就再等一轮
|
|
}
|
|
if (!menuDone) {
|
|
menuDone = true;
|
|
ctx.note('下滑看看菜单有什么');
|
|
await ctx.gesture('down');
|
|
await ctx.waitCond(() => !game.voicePlaying);
|
|
ctx.status('菜单里,切一项听听');
|
|
await ctx.gesture('tap'); // 切一项听听(报名接在当前报名之后,不截断)
|
|
await ctx.waitCond(() => !game.voicePlaying);
|
|
ctx.note('听完了,下滑关闭菜单,回到水下');
|
|
await ctx.gesture('down'); // 下滑关闭(不双击,绝不误触「重开/回大厅」)
|
|
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);
|
|
continue;
|
|
}
|
|
if (game.phase === 'SURFACE') {
|
|
// 上浮结算自动链(扣账/补给/档位)——播完自动进下一潜或签收,不插手
|
|
await ctx.waitCond(() => !game.voicePlaying || game.choiceArmed || game.searchArmed);
|
|
continue;
|
|
}
|
|
// COMMS 叙事等待态(链末=继续):真人听完一句就接话,偶尔走神一次。
|
|
// 不要在这里 waitVirtual 小憩——等待态挂着看门狗定时器,waitVirtual 会
|
|
// 一口气跳到下一个定时器 tick(25s 看门狗),剧本就变成「每句干晾 25s」。
|
|
if (!walkedAway && game.phase === 'COMMS' && !game.choiceArmed) {
|
|
walkedAway = true;
|
|
ctx.silence(25, '听到一半走了神,在想氧气还剩几格');
|
|
await ctx.waitVirtual(25600); // 触发看门狗:nudge +「单击屏幕,继续」
|
|
ctx.status('听到一声轻响和「单击屏幕,继续」,马上点');
|
|
await ctx.gesture('tap');
|
|
continue;
|
|
}
|
|
await ctx.gesture('tap'); // 链末单击 = 继续
|
|
}
|
|
|
|
await ctx.waitCond(() => game.phase === 'ENDING' && game.awaitingRestart);
|
|
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('tap'); // 乱切换
|
|
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);
|
|
continue;
|
|
}
|
|
// COMMS 叙事等待态 / 其他静默帧:乱划一下(无效手势也要有反馈,零静默)
|
|
noiseCount++;
|
|
await ctx.gesture(pick(gestures));
|
|
}
|
|
|
|
await ctx.waitCond(() => game.phase === 'ENDING' && game.awaitingRestart);
|
|
ctx.status(`乱按剧本收尾:脉冲 ${pingCount} 次、${choiceCount} 个抉择、乱试 ${noiseCount} 次、逛菜单 ${menuOpenCount} 次`);
|
|
ctx.note('全程没有任何一次手势被无声吞掉,也没有卡死在某个状态里——结局链照样走完');
|
|
}
|