round2: 看门狗礼仪F1(不重播/单句升级/idle-skip) + 分档归因F2 + 交接count-in/answer F3 + 教学看表现F4-F5 + 警报去叠F6 + 回绕F7 + 开场瘦身F8 + 10句新语音 + 测试全绿(6playtest零改动+guide52断言+资产终检)
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+41
-15
@@ -35,10 +35,13 @@
|
||||
* main 演奏阶段单击是演奏手势不跳(mid_song 短句按裁定不跳)。
|
||||
* 长按快进豁免(12 文档 §2.2:main 演奏期):audio.setRateBoostEnabled(false)
|
||||
* per-game 开关(引擎已支持),进 main 关、进菜单/结算/退出恢复。
|
||||
* 向导层(research/03-schemes/schemes.md §2):每曲首次领奏→应答切换推
|
||||
* gd_answer(turn cue);连续 2 次 Miss 播 gd_miss(queue 插播,同曲一次);
|
||||
* 结算链换 _speakGuarded(资产缺失不卡结算 BGM);状态签名 earcon 与状态
|
||||
* BGM 同点(select=menu、main=battle、end=settle)。
|
||||
* 向导层(docs/design/14-guide-layer.md + round2 synthesis F2.1/F3):每曲首次
|
||||
* 领奏→应答切换在谱面定时队列直接落 gd_answer(不走 guide queue,保证窗口前
|
||||
* 听到)+ turn earcon;教学 Miss 首次即归因分档(gd_miss_early/late/wrong,
|
||||
* 挂纠正链尾空闲点);连续 2 Miss 播 gd_miss(queue 插播,同曲一次);暂停
|
||||
* 断点续播前插 4 拍 count-in(off 档保持即时续播);结算链换 _speakGuarded
|
||||
* (资产缺失不卡结算 BGM);状态签名 earcon 与状态 BGM 同点
|
||||
* (select=menu、main=battle、end=settle)。
|
||||
*/
|
||||
import { Game } from '../engine/game.js';
|
||||
import { Guide } from '../engine/guide.js';
|
||||
@@ -341,7 +344,7 @@ export class RhythmGame extends Game {
|
||||
this._tutFails++;
|
||||
vibrate('error');
|
||||
if (this._tutFails >= 2) {
|
||||
// 同一步失败 2 次跳过,不卡流程
|
||||
// 同一步失败 2 次跳过,不卡流程(归因已在首次失败时给过,此处不再叠加)
|
||||
this._speakGuarded(['voice/retry_skip'], () => {
|
||||
if (this._tutIdx + 1 < TUT_STEPS.length) this._beginTutorial(rid, this._tutIdx + 1);
|
||||
else {
|
||||
@@ -352,13 +355,22 @@ export class RhythmGame extends Game {
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 纠正语音:手势错 > 过早 > 过晚 > 没出手(retry_listen)
|
||||
let correction = 'voice/retry_listen';
|
||||
// F2.1(round2):教学 Miss 首次即归因、按失败原因分档——方向错 gd_miss_wrong /
|
||||
// 抢拍 gd_miss_early / 慢拍 gd_miss_late(无在窗内尝试=没跟上拍,归入慢拍)。
|
||||
// 向导句(full 档)挂在纠正链播完后的空闲点发:idle-only 语义下链中插播会被
|
||||
// 跳过;之后 _tutLead 只排 sfx,不与归因句抢 voice 通道。
|
||||
const wrong = this._tutAttempts.find((a) => !a.correct);
|
||||
const offbeat = this._tutAttempts.find((a) => a.correct && Math.abs(a.dt) > GOOD);
|
||||
const missGrade = wrong ? 'gd_miss_wrong'
|
||||
: offbeat ? (offbeat.dt < 0 ? 'gd_miss_early' : 'gd_miss_late') : 'gd_miss_late';
|
||||
// 纠正语音:手势错 > 过早 > 过晚 > 没出手(retry_listen)
|
||||
let correction = 'voice/retry_listen';
|
||||
if (wrong) correction = 'voice/retry_gesture';
|
||||
else if (offbeat) correction = offbeat.dt < 0 ? 'voice/retry_early' : 'voice/retry_late';
|
||||
this._speakGuarded([correction, 'voice/retry_listen'], () => this._tutLead(rid));
|
||||
this._speakGuarded([correction, 'voice/retry_listen'], () => {
|
||||
this.guide.error({ voice: missGrade, skipHaptic: true });
|
||||
this._tutLead(rid);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------- 选曲 ----------------
|
||||
@@ -428,11 +440,16 @@ export class RhythmGame extends Game {
|
||||
this._timed.push({ time: leadBarT0, fn: () => this.audio.play('voice/mid_song') });
|
||||
}
|
||||
if (i === 0) {
|
||||
// 向导层(schemes §2):每曲首次领奏→应答切换推 gd_answer(turn cue,
|
||||
// 谱面排程处入 _timed,mid_song 同款先例;full 档语音 queue 插播不顶 BGM)
|
||||
// 向导层(round2 F3):每曲首次领奏→应答切换的交接标记。gd_answer 不走
|
||||
// guide 的 queue(对白在播时会排到链尾、错过首个应答窗口)——排进曲谱
|
||||
// 定时队列、在窗口前直接落下;turn earcon 走向导(full 语音,standard
|
||||
// 只听提示音,off 全静)。mid_song 同款先例。
|
||||
this._timed.push({
|
||||
time: leadBarT0 + 4 * spb,
|
||||
fn: () => this.guide.turn({ voice: 'gd_answer' }),
|
||||
fn: () => {
|
||||
if (this.guide.level() === 'full') this.audio.play('voice/gd_answer');
|
||||
this.guide.turn({});
|
||||
},
|
||||
});
|
||||
}
|
||||
bar += 2 + (this._song.restAfter[i] || 0);
|
||||
@@ -625,14 +642,23 @@ export class RhythmGame extends Game {
|
||||
_closeMenu(rid) {
|
||||
if (this._runId !== rid) return;
|
||||
const resumeAt = this.audio.ctx.currentTime + 0.1;
|
||||
const shift = resumeAt - this._pauseAt;
|
||||
// 全部绝对时刻平移暂停时长,节拍网格与 BGM 断点续播保持同步
|
||||
// F3(round2):断点续播前插一整小节 4 拍 count-in(复用底鼓节拍音,走既有
|
||||
// _sched 网格排程 = 每拍一个 inst_kick + beat 轻震),网格与 BGM 整体后移一小节,
|
||||
// 杜绝 P1/P2/P3 实测的「回来 5 秒死寂、第一句报废」。off 档保持原即时续播
|
||||
// (向导层 off = 零改动路径)。
|
||||
const countIn = this.guide.level() !== 'off' ? 4 * (60 / this._song.bpm) : 0;
|
||||
const bgmAt = resumeAt + countIn;
|
||||
const shift = bgmAt - this._pauseAt;
|
||||
// 全部绝对时刻平移(暂停时长 + count-in 小节),节拍网格与 BGM 断点续播保持同步
|
||||
for (const ev of this._sched) ev.time += shift;
|
||||
for (const ev of this._timed) ev.time += shift;
|
||||
for (const cue of this._cues) cue.expected += shift;
|
||||
this._phaseEnd += shift;
|
||||
// _startBgm 内部重算 _bgmT0 = resumeAt - offset = 旧网格 + shift
|
||||
this._startBgm(this._song, resumeAt, this._bgmOffset);
|
||||
for (let i = 0; i < 4 && countIn > 0; i++) {
|
||||
this._sched.push({ time: resumeAt + i * (countIn / 4), sfx: INST.tap, volume: 0.7 });
|
||||
}
|
||||
// _startBgm 内部重算 _bgmT0 = bgmAt - offset = 旧网格 + shift(负偏移保护同款成立)
|
||||
this._startBgm(this._song, bgmAt, this._bgmOffset);
|
||||
this.phase = 'main';
|
||||
this.audio.setRateBoostEnabled(false); // 回到演奏期
|
||||
this._startPump(rid);
|
||||
|
||||
Reference in New Issue
Block a user