storyline/escape/simon 首批之后的推广收官:15 款全部按 26 号模式运行 (发言权/回执/三档提示/迷茫接管/手势阶梯/断点复述),旧「双击重听/格挡/ 菜单确认」等反模式口径全线废除,开场零规则念白(首句即动手)。 游戏层(12 款):rpg(格挡=前摇窗口内再单击)/ rhythm / space / radio / dungeon / abyss / detective / cipher / teahouse / keeper / bridge / hunt(联机切片): - 每款 game.js 接引擎六模块 + makeInputPipeline(doubleTapWindow:0); 四向玩法类 swipeCore + 长按帮助;双击是玩法的(space 护盾 / hunt 旋风斩)按手势阶梯阶段 3 分档恢复双击窗口(doubletapCore) - 每款 playtest 重写:玩法断言 + §6.2 乱点机器人(无声操作 / 被动 原句重复 / 打断失败 = 0);15 款 3900+ 断言全绿 - 12 份 docs/design/26-apply-*.md 施工单 + tools/voice_lines_26/ 台词清单 + voice-ids 文档同步 + 转写会话剧本按新口径重写 引擎层(web/engine/,6 处修复):pipeline 增 doubletapCore;novice teachLine 带 voice/ 前缀(此前教学句被静默丢弃);confusion.record 前置到新手门(锁定手势的乱点也计信号);InvalidHints 播完恢复解释前 状态(根治孤儿 BUSY);WaitPoint takeover(2) 台账去重;台账只在 speak 真正播出时登记。遗留引擎债登记 roadmap §3.1。 语音资产:台词表合并(新增 ~250 系统句 / 改 ~50 旧口径 / 删 ~110 废止句;合并器 tools/merge_voice_lines_26.py + 引用校验器 tools/validate_voice_refs.py),GPU 重录 1536 wav → 4204 ogg/m4a 落地,check_all_assets + validate_voice_refs 双绿;键盘变体同步扩表。 大厅/文档:15 款统一新交互口径(「新交互」徽标退役);README / HANDOVER / rpg README 旧口径修正;roadmap P0-P3 置完成。
118 lines
6.7 KiB
JavaScript
118 lines
6.7 KiB
JavaScript
/**
|
||
* 《节拍行者》鼓手模式新手会话剧本(26 号深度重构版,docs/design/26-apply-rhythm.md):
|
||
* 首局完整走「开场指令 → 摸鼓认区(六区摸遍,区名每区一次)→ 教学 T1-T3 跟打 →
|
||
* 选曲等待点(单击开始)→ 曲目一(90 BPM)」。曲中下滑逛菜单(左右滑换项、
|
||
* 单击确认;恢复 = gd_count + 4 拍底鼓 count-in 进场)、正片第一乐句故意整句漏掉
|
||
* (连 2 Miss → gd_miss 一次),之后逐音跟打(含终曲加花)到结算。
|
||
* 分区敲击带坐标注入(引擎坐标透传契约,24 号文档 §0.3)。
|
||
*/
|
||
const ZC = {
|
||
TL: { x: 50, y: 100 }, TC: { x: 150, y: 100 }, TR: { x: 250, y: 100 },
|
||
BL: { x: 50, y: 500 }, BC: { x: 150, y: 500 }, BR: { x: 250, y: 500 },
|
||
};
|
||
|
||
export function describe(game) {
|
||
switch (game.phase) {
|
||
case 'intro': return '开场指令「点一下屏幕。」在播(critical,抢拍只给 busy 回执)';
|
||
case 'tut':
|
||
if (game._tutStage === 'probe') {
|
||
return `摸鼓认区(ACTION):随便摸,摸到 3 个不同的地方就开始(已摸 ${game._probeHeard.size} 区)`;
|
||
}
|
||
if (game._tutStage === 'idle') return '区名播完,教学第一步即将开始';
|
||
return game._cues.length
|
||
? `教学跟打:听领奏,下一小节照样敲(${game._cues.map((c) => c.zone).join('→')})${game._tutRate < 1 ? '(领奏已放慢 25%)' : ''}`
|
||
: '教学准备中,听数拍';
|
||
case 'select': return '选曲(YOUR_TURN):左右滑换曲听报名,单击开始当前曲';
|
||
case 'main': {
|
||
const left = game._cues.filter((c) => !c.judged).length;
|
||
const fillNext = game._timed.some((e) => e.fn && game._song?.units?.some((u) => u.fill));
|
||
return `演奏中(${game._song?.title ?? ''},ACTION):还剩 ${left} 个应答音。领奏停了、轮到空拍,就该我出手了${fillNext ? ';有一段加花要连打' : ''}`;
|
||
}
|
||
case 'menu': return '暂停菜单(YOUR_TURN):左右滑换项听报名,单击确认,下滑关闭恢复';
|
||
case 'end': return '结算(YOUR_TURN):单击同曲再来,右滑下一首(未解锁会说明)';
|
||
default: return '开场,稍等';
|
||
}
|
||
}
|
||
|
||
export async function session(ctx, mock) {
|
||
const { game, audio } = ctx;
|
||
const t = () => audio.ctx.currentTime;
|
||
// transcribe 的 root 是 {}:补上布局让分区坐标生效(引擎坐标契约的游戏层语义)
|
||
game.root = { getBoundingClientRect: () => ({ left: 0, top: 0, width: 300, height: 600 }) };
|
||
const hit = async (zone) => {
|
||
game.onGesture('tap', ZC[zone]);
|
||
await mock.flush();
|
||
};
|
||
|
||
// 开场(§4.10):第一句就是 critical 指令「点一下屏幕。」→ 摸鼓探针(ACTION)
|
||
await ctx.waitCond(() => game.phase === 'tut' && game._tutStage === 'probe');
|
||
ctx.status('听到「点一下屏幕。」——整面屏幕都是鼓,随便摸');
|
||
ctx.note('背景《启程》无鼓伴奏轻声循环——拍子网格的底,鼓由我来打');
|
||
|
||
// T0 探索:六区全摸一遍(区名每区一次;重复摸只出声),摸到 3 区进阶
|
||
for (const zone of ['TL', 'TC', 'TR', 'BL', 'BC', 'BR']) await hit(zone);
|
||
ctx.status('六面鼓摸遍了:左上踩镲、上中高嗵、右上吊镲、左下军鼓、下中底鼓、右下低嗵');
|
||
await hit('TL'); // 重复摸:只出声不报名
|
||
await hit('BR');
|
||
ctx.status('又摸了两下重复的,只响不报名');
|
||
await ctx.waitCond(() => game._tutStage === 'step' && game._cues.length > 0);
|
||
ctx.status('摸到三个不同的地方,自动开始教学第一步');
|
||
|
||
// 教学 3 步:等领奏排程好,在应答窗口内跟敲
|
||
//(注意:调度泵 interval 常驻,虚拟时钟永不"空闲"——禁止裸 waitIdle)
|
||
for (let step = 0; step < 3; step++) {
|
||
await ctx.waitCond(() => game._tutIdx === step && game.phase === 'tut'
|
||
&& game._cues.length > 0 && game._tutAttempts.length === 0);
|
||
ctx.status(`教学第 ${step + 1}/3 步:听数拍和领奏(示范期点按只给 busy 回执),下一小节照着敲`);
|
||
for (const cue of game._cues) {
|
||
await ctx.waitCond(() => t() >= cue.expected || cue.judged);
|
||
if (cue.judged) continue;
|
||
await hit(cue.zone);
|
||
}
|
||
await ctx.waitCond(() => game._tutIdx > step || game.phase !== 'tut');
|
||
}
|
||
await ctx.waitCond(() => game.phase === 'select');
|
||
ctx.status('教学完毕,进选曲:左右滑换曲听报名,单击开始');
|
||
ctx.note('左右滑换曲、单击开始——新手期没有双击手势');
|
||
|
||
// 选曲:单击开始曲目一
|
||
await ctx.gesture('tap');
|
||
await ctx.waitCond(() => game.phase === 'main');
|
||
ctx.status('曲目一《启程》90 拍开始。先听四下底鼓数拍;首个应答窗前会教「轮到你了」这个声音');
|
||
ctx.note('《启程》无鼓伴奏 BGM 起,四下底鼓数拍对齐小节;演奏期 = ACTION,点按即敲');
|
||
|
||
// 曲中逛菜单:下滑呼出(模态暂停)→ 左右滑换项听报名 → 下滑恢复
|
||
//(恢复 = gd_count「四拍之后,继续」+ 4 拍底鼓 count-in 进场)
|
||
await ctx.waitCond(() => t() >= game._bgmT0 + 1.5);
|
||
await ctx.gesture('down');
|
||
await ctx.waitCond(() => game.phase === 'menu');
|
||
ctx.status('曲中暂停,进了菜单(左右滑换项,单击确认)');
|
||
ctx.note('BGM 在断点冻住——谱面、判定、音乐全部暂停');
|
||
await ctx.waitVirtual(1100); // 菜单 1s 去抖独占期
|
||
await ctx.gesture('left'); // 换到「回大厅」听报名
|
||
ctx.note('还是想打完这首,右滑切回「继续演奏」,再下滑回去');
|
||
await ctx.gesture('right');
|
||
await ctx.gesture('down'); // 关闭恢复
|
||
await ctx.waitCond(() => game.phase === 'main');
|
||
ctx.status('听到「四拍之后,继续」,跟着四下底鼓数拍进来了');
|
||
ctx.note('BGM 从断点续播,节拍网格整体后移了一小节');
|
||
|
||
// 第一乐句故意整句不打:连 2 次 Miss → gd_miss 一次(同曲一次,曲尾 3s 不插)
|
||
const firstPhrase = game._cues.filter((c) => c.phrase === 0);
|
||
ctx.silence(5, '还在数拍子,第一句没敢出手');
|
||
await ctx.waitCond(() => firstPhrase.every((c) => c.judged));
|
||
ctx.status('第一句全漏了,听到一次「慢了半拍,下一小节跟上我就行」');
|
||
|
||
// 之后逐音跟打(含终曲加花)
|
||
for (const cue of game._cues) {
|
||
if (cue.judged) continue;
|
||
await ctx.waitCond(() => t() >= cue.expected || cue.judged);
|
||
if (cue.judged) continue;
|
||
await hit(cue.zone);
|
||
}
|
||
|
||
await ctx.waitCond(() => game.phase === 'end');
|
||
ctx.status('曲子打完了(加花收尾是一记吊镲),结算播报中(单击同曲再来,右滑下一首)');
|
||
ctx.note('曲目 BGM 淡出,结算语音链播完会淡入大厅音乐,然后交出结算等待点');
|
||
}
|