Files
SoundGame/web/harness/sessions/teahouse.mjs
T
xsl 4b39ae2e49 feat(new8/batch3-4): cipher/teahouse/keeper/bridge 全套交付——new8 八款全部可交付收官
四泳道并行全流水线(资产→开发→测试→玩家迭代闭环),双双 R1-R2 连续达标裁定可交付:
- cipher《暗码》223 断言:摩尔斯解码嵌叙事/四结局矩阵/暴露度逐NIGHT验算/三层兜底;R2 三修(链首标记音/预警震/结局计数句+4句补录)4/4 全满分档
- teahouse《深夜茶馆》205 断言:泡茶时机窗/经济逐晚对账/多晚弧线;连续 2 轮 4/4 愿意玩 P0=0
- keeper《守塔人》257 断言:时空签名教学-兑现/插叙触发回返/trust×truth 档位/4结局;R2 F1/F2 坐实(震型豁免/首播跳句锁)4/4
- bridge《星环舰长》336 断言:巡检节奏逐毫秒/危机×系统态矩阵/仲裁7场景/忠诚差分/评级验算;R1 当场修F1-F4、R2 F5 菜单顺延保护 4/4 上行

公共:index.html 四卡片+profiles.js 四桶+check_all_assets 登记(预接线)、00-program 八款进度账收账、
gen_voice_dual 四游戏段、README/HANDOVER 阵容更新(11 款)、voice_text.json 四游戏键、mureka 台账(累计$1.26/$2.00)

验证:四游戏 playtest 223/205/257/336 全绿 + guide 122/122 + 全项目 15 游戏资产终检绿
遗留:keeper 三代人声滤波人耳试听(最高优先,ASR 对滤波句失明)、bridge 音频 32MB 超软指引 2MB(压缩候选)
2026-09-14 05:48:13 +08:00

277 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 《深夜茶馆》新手会话剧本。
*
* 剧本:
* - `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('乱按一路也睡下了:结算链完整、零死锁,压力测试收工');
}