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 置完成。
136 lines
6.3 KiB
JavaScript
136 lines
6.3 KiB
JavaScript
/**
|
||
* 《剑与声》新手会话剧本(26 号重构版):critical 开场指令「点一下屏幕」→
|
||
* 序章 → 进村把选项听一圈 → 出村去小路(顺路调查一次捡钱)→ 岔路口转去溪流
|
||
* 触发教学战 → 完整打一场(四步:识招→攻击→格挡[窗口内单击]→防御,再自由
|
||
* 击杀),战后收工。26 号口径:格挡 = 前摇窗口内**再单击**(旧「双击格挡」废止);
|
||
* 新手期无双击重听(上滑=状态、菜单=通关第二章解锁,此处均不使用)。
|
||
*
|
||
* 种子 RNG:让教学狼首击必不掉(HP 6、伤害恒 5),保证四步教学完整走完。
|
||
*/
|
||
Math.random = () => 0.4;
|
||
|
||
export function describe(game) {
|
||
const SCENE_NAME = {
|
||
village: '雾口村', path: '林间小路', fork: '岔路口', chest: '枯树洞', stream: '溪流',
|
||
hunter: '猎人小屋', deepwood: '密林深处', gate: '黑风寨门前', boss1: '聚义厅',
|
||
};
|
||
const BAND = { high: '八成以上', mid: '五成上下', low: '三成以下' };
|
||
if (game.phase === 'battle' && game._battle) {
|
||
const b = game._battle;
|
||
if (b.state === 'PLAYER_SELECT' && b.enemy) {
|
||
return `战斗中:你的血量${BAND[game._hpBand(game.player.hp, game.maxHp)]},`
|
||
+ `敌方血量${BAND[game._hpBand(b.enemy.hp, b.enemy.maxHp)]},等你选招式(左右滑切换,单击出手,下滑逃跑)`;
|
||
}
|
||
if (b.state === 'ENEMY_TELEGRAPH') return '战斗中:敌方前摇声响起了——点一下,格挡!';
|
||
return '战斗播报中,先听着';
|
||
}
|
||
switch (game.phase) {
|
||
case 'ask': return '检测到旅程记录:单击继续,右滑开新档';
|
||
case 'intro': return game._introTapped ? '序章旁白在播(单击可跳句)' : '开场指令在播:等风铃响起点一下';
|
||
case 'explore': return `【${SCENE_NAME[game.scene] ?? game.scene}】:左右滑切选项,单击确认,上滑查状态`;
|
||
case 'shop': return '商店里:左右滑看货,单击买,下滑离开';
|
||
case 'menu': return '菜单里:左右滑切选项,单击确认,下滑关闭';
|
||
default: return '……';
|
||
}
|
||
}
|
||
|
||
export async function session(ctx, mock) {
|
||
const { game } = ctx;
|
||
|
||
// 开场:critical 指令「点一下屏幕。」播完(风铃 + 等待声)→ 第一下单击
|
||
await ctx.waitCond(() => game.floor.state === 'YOUR_TURN');
|
||
ctx.status('开场指令听完了,点一下');
|
||
ctx.note('第一句就让我动手,还给了「轮到你了」的一声风铃');
|
||
await ctx.gesture('tap');
|
||
|
||
// 序章听完 → 进村
|
||
await ctx.waitCond(() => game.phase === 'explore');
|
||
ctx.status('进村了,先听场景描述和选项');
|
||
|
||
// 村里(选项:去小路/商店/休息/听状态):把选项听一圈,回绕到首项再出发。
|
||
// 确认(tap)前等选项名念完——报名在播时单击是「跳过这句」,会慢半拍
|
||
for (let i = 0; i < 3; i++) {
|
||
await ctx.gesture('right');
|
||
await ctx.waitVirtual(1800); // 听选项名
|
||
}
|
||
ctx.status('选项听了一圈');
|
||
await ctx.gesture('right'); // 从末项回绕到首个选项——「回到第一个」
|
||
ctx.note('滑到头再滑就兜回来了,有人提醒「回到第一个」,不用自己数');
|
||
await ctx.waitCond(() => !ctx.voiceBusy());
|
||
await ctx.gesture('tap'); // → path
|
||
|
||
// 小路(去岔路口/调查/返回):先调查一次捡钱,再左滑回「去岔路口」
|
||
await ctx.waitCond(() => game.phase === 'explore' && game.scene === 'path');
|
||
ctx.status('林间小路');
|
||
await ctx.gesture('right'); // 调查四周
|
||
await ctx.waitVirtual(1800);
|
||
ctx.note('翻找了一阵,捡到一点钱');
|
||
await ctx.waitCond(() => !ctx.voiceBusy());
|
||
await ctx.gesture('left'); // 回「去岔路口」
|
||
await ctx.waitCond(() => !ctx.voiceBusy());
|
||
await ctx.gesture('tap'); // → fork
|
||
|
||
// 岔路口(去枯树洞/去溪流/返回):右滑一次到「去溪流」
|
||
await ctx.waitCond(() => game.phase === 'explore' && game.scene === 'fork');
|
||
ctx.status('岔路口,水声在东边');
|
||
await ctx.gesture('right');
|
||
await ctx.waitCond(() => !ctx.voiceBusy());
|
||
await ctx.gesture('tap'); // go_stream → 溪流 → 触发教学战
|
||
|
||
// 教学战:四步(识招 → 攻击 → 格挡[窗口内单击] → 防御)+ 自由击杀
|
||
await ctx.waitCond(() => game.phase === 'battle' && game._battle?.state === 'PLAYER_SELECT');
|
||
ctx.status('遭遇教学狼,战斗开始');
|
||
|
||
const CMDS = ['attack', 'skill', 'defend', 'item'];
|
||
const selectCmd = async (cmd) => {
|
||
const b = game._battle;
|
||
while (b && b.cmdIdx !== CMDS.indexOf(cmd)) {
|
||
await ctx.gesture('right');
|
||
await ctx.waitVirtual(1300); // 听指令名
|
||
}
|
||
};
|
||
|
||
// T0:左右滑各切一次(听到指令名就算过关)
|
||
await ctx.gesture('right');
|
||
await ctx.waitVirtual(1300);
|
||
await ctx.gesture('right');
|
||
await ctx.waitVirtual(1300);
|
||
ctx.status('招式名听熟了');
|
||
|
||
// T1:选攻击,单击出剑
|
||
await ctx.waitCond(() => game._battle?.state === 'PLAYER_SELECT');
|
||
await selectCmd('attack');
|
||
ctx.note('单击出手,攻击');
|
||
await ctx.gesture('tap');
|
||
|
||
// T2:格挡(前摇窗口内再单击)——先听「它要扑了,听前摇」「就是现在,点一下挡住」
|
||
await ctx.waitCond(() => game._battle?.state === 'ENEMY_TELEGRAPH');
|
||
ctx.status('前摇声响起了');
|
||
ctx.note('前摇一响就点一下——这回干净挡下了,听到「挡得漂亮」');
|
||
await ctx.gesture('tap');
|
||
|
||
// T3:防御一次
|
||
await ctx.waitCond(() => game._battle?.state === 'PLAYER_SELECT');
|
||
await selectCmd('defend');
|
||
ctx.note('这回合先防御,减半伤害');
|
||
await ctx.gesture('tap');
|
||
|
||
// 自由阶段:攻击到狼死(中间照常听前摇、单击格挡)
|
||
for (let i = 0; i < 8 && game.phase === 'battle'; i++) {
|
||
await ctx.waitCond(() => game._battle?.state === 'PLAYER_SELECT'
|
||
|| game._battle?.state === 'ENEMY_TELEGRAPH' || game.phase !== 'battle');
|
||
if (game.phase !== 'battle') break;
|
||
if (game._battle.state === 'ENEMY_TELEGRAPH') {
|
||
ctx.note('前摇响了,单击格挡');
|
||
await ctx.gesture('tap');
|
||
continue;
|
||
}
|
||
await selectCmd('attack');
|
||
ctx.note('单击,攻击');
|
||
await ctx.gesture('tap');
|
||
}
|
||
|
||
await ctx.waitCond(() => game.phase === 'explore');
|
||
ctx.status('狼打完了,胜利播报(链尾还教了一句「想知道自己在哪,就向上滑」)完回到溪流边。一场完整战斗到此收工');
|
||
}
|