round3: G1 gd_answer提前一小节(根因=排程压窗起点) + G2教学降难DDA + G3 count-in修复(根因=未重排调度队列) + G4 escape目标链 + G5 simon双击根治(重听改上滑,重录3句) + G6轮成功/破纪录 + G7 space方向呼喊 + 22语音文件 + guide93断言全绿
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.
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.
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.
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.
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.
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.
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.
+115
-40
@@ -35,13 +35,18 @@
|
||||
* main 演奏阶段单击是演奏手势不跳(mid_song 短句按裁定不跳)。
|
||||
* 长按快进豁免(12 文档 §2.2:main 演奏期):audio.setRateBoostEnabled(false)
|
||||
* per-game 开关(引擎已支持),进 main 关、进菜单/结算/退出恢复。
|
||||
* 向导层(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)。
|
||||
* 向导层(docs/design/14-guide-layer.md + round2 synthesis F2.1/F3 + round3 G1~G3):
|
||||
* 每曲首次领奏→应答切换在谱面定时队列直接落 gd_answer(不走 guide queue,保证
|
||||
* 窗口前听到)+ turn earcon;round3 G1 修正排程点:落在首个应答窗口**前整整
|
||||
* 1 小节**(= 领奏小节头;round2 排在窗口起点本身,P4 实测窗口开了才听到),
|
||||
* 菜单恢复对下一个未判定窗口重排同款标记;教学 Miss 首次即归因分档
|
||||
* (gd_miss_early/late/wrong,挂纠正链尾空闲点);连续 2 Miss 播 gd_miss(queue
|
||||
* 插播,同曲一次);暂停断点续播前插 4 拍 count-in(round3 G3:底鼓 sfx 层可辨
|
||||
* 音量 + full 档 gd_count 每局恢复限 1 次 + beat 震;round2 版因 push 后未重排
|
||||
* _sched 被队头卡死从未按时响过;off 档保持即时续播);教学 DDA(round3 G2):
|
||||
* 同一步连 2 次失败该步领奏降速 25% 重演并播 gd_slow(full 档、每教学局 1 次,
|
||||
* off 档维持 retry_skip 原路径);结算链换 _speakGuarded(资产缺失不卡结算 BGM);
|
||||
* 状态签名 earcon 与状态 BGM 同点(select=menu、main=battle、end=settle)。
|
||||
*/
|
||||
import { Game } from '../engine/game.js';
|
||||
import { Guide } from '../engine/guide.js';
|
||||
@@ -159,10 +164,15 @@ export class RhythmGame extends Game {
|
||||
this._tutIdx = 0;
|
||||
this._tutFails = 0;
|
||||
this._tutAttempts = [];
|
||||
this._tutRate = 1.0; // G2:教学领奏速率(0.75 = 降速 25%,做对/离开该步恢复 1.0)
|
||||
this._tutSlowUsed = false; // G2:降速重演每教学局 1 次封顶
|
||||
// 菜单/暂停状态
|
||||
this._menuIdx = 0;
|
||||
this._pauseAt = 0;
|
||||
this._bgmOffset = 0;
|
||||
this._resumeSeq = 0; // G3:恢复令牌(count-in 语音期间再开菜单时作废旧恢复)
|
||||
this._gdCountUsed = false; // G3:gd_count 每局(曲)恢复限 1 次
|
||||
this._answerArmed = false; // G1:gd_answer+turn 交接标记是否已在 _timed 里待响
|
||||
this._consecMiss = 0; // 连续 Miss 计数(向导 gd_miss:连 2 次提示一次)
|
||||
}
|
||||
|
||||
@@ -173,6 +183,9 @@ export class RhythmGame extends Game {
|
||||
'voice/tut_1', 'voice/select_hint', 'bgm/menu', ...SONGS.map((s) => s.bgm)]);
|
||||
const rid = ++this._runId;
|
||||
this._cleanup();
|
||||
this._tutRate = 1.0;
|
||||
this._tutSlowUsed = false; // G2:每教学局重新封顶
|
||||
this._gdCountUsed = false; // G3:每局(曲)恢复限 1 次
|
||||
if (this._progress.tutorialDone) {
|
||||
// 二周目起:缩减开场 → 选曲
|
||||
this.phase = 'intro';
|
||||
@@ -309,10 +322,13 @@ export class RhythmGame extends Game {
|
||||
+ Math.max(0, Math.ceil((now + 1.2 - this._bgmT0) / (4 * spb))) * 4 * spb;
|
||||
this._tutAttempts = [];
|
||||
this._cues = [];
|
||||
// G2(round3):降速只拉伸该步的领奏/应答网格(间隔 ×1/0.75),领奏小节头
|
||||
// 仍对齐 BGM 小节;排程照走 _sched 绝对时间 + 40ms 泵,泵与判定无需感知速率。
|
||||
const stepSpb = spb / this._tutRate;
|
||||
for (const [beat, g] of TUT_STEPS[this._tutIdx].notes) {
|
||||
const t = leadBar + (beat - 1) * spb;
|
||||
const t = leadBar + (beat - 1) * stepSpb;
|
||||
this._sched.push({ time: t, sfx: INST[g], volume: 1 });
|
||||
this._cues.push({ gesture: g, expected: t + 4 * spb, judged: false, phrase: -1 });
|
||||
this._cues.push({ gesture: g, expected: t + 4 * stepSpb, judged: false, phrase: -1 });
|
||||
}
|
||||
this._sortSched();
|
||||
const last = this._cues[this._cues.length - 1];
|
||||
@@ -332,6 +348,7 @@ export class RhythmGame extends Game {
|
||||
return a && a.correct && Math.abs(a.dt) <= GOOD;
|
||||
});
|
||||
if (pass) {
|
||||
this._tutRate = 1.0; // G2:该步做对,恢复正常速度
|
||||
if (this._tutIdx + 1 < TUT_STEPS.length) {
|
||||
this._beginTutorial(rid, this._tutIdx + 1);
|
||||
} else {
|
||||
@@ -344,6 +361,22 @@ export class RhythmGame extends Game {
|
||||
this._tutFails++;
|
||||
vibrate('error');
|
||||
if (this._tutFails >= 2) {
|
||||
// G2(round3 教学降难 DDA):同一步连续 2 次失败 → 该步领奏降速 25% 重演
|
||||
// (每教学局 1 次封顶;full 档先播 gd_slow「我放慢一些,跟着试试。」)——
|
||||
// P4「六连错无出路」:失败后第一次有变慢的路,而不是直接跳步。封顶用尽
|
||||
// 或 off 档(零改动红线)→ 维持 round2 的 retry_skip 跳步原路径。
|
||||
if (this.guide.level() !== 'off' && !this._tutSlowUsed) {
|
||||
this._tutSlowUsed = true;
|
||||
this._tutRate = 0.75;
|
||||
this._tutFails = 0; // 降速重演 = 该步重新给两次机会(再连 2 失败才跳步)
|
||||
if (this.guide.level() === 'full') {
|
||||
this._speakGuarded(['voice/gd_slow'], () => this._tutLead(rid));
|
||||
} else {
|
||||
this._tutLead(rid);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._tutRate = 1.0; // 离开该步(跳步):降速只作用于该步
|
||||
// 同一步失败 2 次跳过,不卡流程(归因已在首次失败时给过,此处不再叠加)
|
||||
this._speakGuarded(['voice/retry_skip'], () => {
|
||||
if (this._tutIdx + 1 < TUT_STEPS.length) this._beginTutorial(rid, this._tutIdx + 1);
|
||||
@@ -440,17 +473,14 @@ export class RhythmGame extends Game {
|
||||
this._timed.push({ time: leadBarT0, fn: () => this.audio.play('voice/mid_song') });
|
||||
}
|
||||
if (i === 0) {
|
||||
// 向导层(round2 F3):每曲首次领奏→应答切换的交接标记。gd_answer 不走
|
||||
// guide 的 queue(对白在播时会排到链尾、错过首个应答窗口)——排进曲谱
|
||||
// 定时队列、在窗口前直接落下;turn earcon 走向导(full 语音,standard
|
||||
// 只听提示音,off 全静)。mid_song 同款先例。
|
||||
this._timed.push({
|
||||
time: leadBarT0 + 4 * spb,
|
||||
fn: () => {
|
||||
if (this.guide.level() === 'full') this.audio.play('voice/gd_answer');
|
||||
this.guide.turn({});
|
||||
},
|
||||
});
|
||||
// 向导层(round3 G1):每次进 main,首个应答窗口**前整整 1 小节**(= 领奏
|
||||
// 小节头)落下 gd_answer+turn 交接标记。round2 F3 的排程点(leadBarT0 +
|
||||
// 4*spb)其实是窗口起点本身——P4 实测(窗口 t+79~95,gd_answer t+93.7)
|
||||
// 窗口开了才听到,起不到「快轮到你了」的预警作用。gd_answer 不走 guide
|
||||
// 的 queue(对白在播时会排到链尾);语音 full 档直达,turn earcon standard
|
||||
// 也发(off 全静不变)。mid_song 同款先例。
|
||||
this._answerArmed = true;
|
||||
this._timed.push({ time: leadBarT0, fn: () => this._fireAnswerCue() });
|
||||
}
|
||||
bar += 2 + (this._song.restAfter[i] || 0);
|
||||
});
|
||||
@@ -460,6 +490,32 @@ export class RhythmGame extends Game {
|
||||
this._startPump(rid);
|
||||
}
|
||||
|
||||
/** G1:gd_answer+turn 交接标记(语音 full 档直达,turn earcon standard 也发)。 */
|
||||
_fireAnswerCue() {
|
||||
this._answerArmed = false;
|
||||
if (this.guide.level() === 'full') this.audio.play('voice/gd_answer');
|
||||
this.guide.turn({});
|
||||
}
|
||||
|
||||
/**
|
||||
* G1:为「下一个未判定的应答窗口」排交接标记,落在窗口前整整 1 小节(= 该句
|
||||
* 领奏小节头;已迫近则尽早在 0.15s 内补上)。_startSong 已为乐句 0 排过;菜单
|
||||
* 恢复(也是一次「进 main」)时若旧标记已消费、且前面还有未判定的窗口,对
|
||||
* 恢复后的首个窗口重排同款标记。上一句结束后的下一句不适用(裁定原文)。
|
||||
*/
|
||||
_armAnswerCue(rid) {
|
||||
if (this._answerArmed) return; // 旧标记仍在 _timed 里,随网格平移照常提前 1 小节响
|
||||
if (this._runId !== rid || !this._song || !this.audio.ctx) return;
|
||||
const now = this.audio.ctx.currentTime;
|
||||
const next = this._cues.find((c) => !c.judged && c.expected > now);
|
||||
if (!next) return;
|
||||
const spb = 60 / this._song.bpm;
|
||||
const t = Math.max(now + 0.15, next.expected - 4 * spb);
|
||||
this._answerArmed = true;
|
||||
this._timed.push({ time: t, fn: () => this._fireAnswerCue() });
|
||||
this._sortSched();
|
||||
}
|
||||
|
||||
// ---------------- 手势与判定 ----------------
|
||||
|
||||
onGesture(g) {
|
||||
@@ -619,8 +675,11 @@ export class RhythmGame extends Game {
|
||||
this.guide.state('menu'); // 状态签名:菜单(模态暂停)
|
||||
// 暂停:停泵、停 BGM(记断点),已排程的 ≤0.6s 音符自然播完
|
||||
if (this._timer) { clearInterval(this._timer); this._timer = null; }
|
||||
this._resumeSeq++; // G3:作废还在等 gd_count 播完的旧恢复
|
||||
this._pauseAt = this.audio.ctx.currentTime;
|
||||
this._bgmOffset = this._pauseAt - this._bgmT0;
|
||||
// 断点续播偏移只在 BGM 真在播时刷新(count-in 语音期间再开菜单时 BGM 未起,
|
||||
// 沿用首次开的偏移,避免把暂停时长算进播放位置)
|
||||
if (this._bgmSrc) this._bgmOffset = this._pauseAt - this._bgmT0;
|
||||
this._stopBgmSrc();
|
||||
this._speak(['voice/menu_main', MENU_ITEMS[0]]);
|
||||
}
|
||||
@@ -641,27 +700,43 @@ export class RhythmGame extends Game {
|
||||
|
||||
_closeMenu(rid) {
|
||||
if (this._runId !== rid) return;
|
||||
const resumeAt = this.audio.ctx.currentTime + 0.1;
|
||||
// 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;
|
||||
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);
|
||||
const token = ++this._resumeSeq; // G3:恢复令牌(语音等待期间再开菜单则作废)
|
||||
const resume = () => {
|
||||
if (this._runId !== rid || token !== this._resumeSeq) return;
|
||||
const resumeAt = this.audio.ctx.currentTime + 0.1;
|
||||
// G3(round3):断点续播前插一整小节 4 拍 count-in——底鼓走 sfx 层独立于
|
||||
// BGM、音量提到可辨(0.7→1.0),每拍带 beat 轻震(_sched 泵同步排),网格
|
||||
// 与 BGM 整体后移一小节,杜绝 P3 实测的「回来 5 秒死寂、第一句报废」。
|
||||
// round2 F3 的 count-in 实际从未按时响过:底鼓 push 进 _sched 后没重排,
|
||||
// 队头是数秒外的领奏音符,40ms 泵的 0.6s 前瞻窗口永远轮不到它——本次修复
|
||||
// push 后 _sortSched()。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;
|
||||
for (let i = 0; i < 4 && countIn > 0; i++) {
|
||||
this._sched.push({ time: resumeAt + i * (countIn / 4), sfx: INST.tap, volume: 1.0 });
|
||||
}
|
||||
this._sortSched(); // G3 修复:count-in 必须重排入队,否则被队头阻塞整段迟到
|
||||
// _startBgm 内部重算 _bgmT0 = bgmAt - offset = 旧网格 + shift(负偏移保护同款成立)
|
||||
this._startBgm(this._song, bgmAt, this._bgmOffset);
|
||||
this._startPump(rid);
|
||||
this._armAnswerCue(rid); // G1:菜单恢复也是一次「进 main」,给下一个窗口补交接标记
|
||||
};
|
||||
// G3:gd_count「四拍之后,继续。」full 档、每局(曲)恢复限 1 次,在 count-in
|
||||
// 之前经语音链串行播出(链完或缺失看门狗兜底后才开始数拍)。
|
||||
if (this.guide.level() === 'full' && !this._gdCountUsed) {
|
||||
this._gdCountUsed = true;
|
||||
this._speakGuarded(['voice/gd_count'], resume);
|
||||
} else {
|
||||
resume();
|
||||
}
|
||||
}
|
||||
|
||||
_goHome() {
|
||||
|
||||
Reference in New Issue
Block a user