第二批反馈:引擎改造(限幅/SoundTouch不变调变速/长按快进/跳句/iOS闪光降级) + 6游戏状态BGM与试玩测试全绿

This commit is contained in:
SoundGame Dev
2026-08-30 16:45:42 +08:00
parent cc0f8c008d
commit 9a915d62c0
1163 changed files with 6346 additions and 208 deletions
+42 -3
View File
@@ -25,6 +25,16 @@
* 菜单(round2-review 全球约定):下滑呼出(模态暂停,BGM 断点续播)→
* 单击循环 → 双击确认 → 上滑帮助。下滑与"下滑=低音 tom"冲突的折中:
* 仅当无悬置/临近的 down 音符时下滑才呼出菜单(见 README)。
*
* 状态 BGM(docs/design/10-feedback2-state-bgm.md §5):
* 选曲界面 = bgm/menu(引擎层淡入 800ms);选定曲目 stopBgm(400) 后按既有
* 网格起 track90/105/120(手动排程不变);暂停菜单保持静默(断点续播,
* 不叠 menu pad);结算评级语音链播完后淡入 bgm/menu 800ms。
* 旁白跳过(docs/design/12-skip-and-longpress-ux.md §1.2):intro/tut/select/
* end 阶段 voice 播放中单击 = audio.skipVoice()(跳下一句,链不断);
* main 演奏阶段单击是演奏手势不跳(mid_song 短句按裁定不跳)。
* 长按快进豁免(12 文档 §2.2:main+tut 应豁免):引擎当前无 per-game
* 开关(audio.js 全局监听 soundgame:longpress),记为已知限制,不改引擎。
*/
import { Game } from '../engine/game.js';
import { vibrate } from '../engine/haptics.js';
@@ -150,7 +160,7 @@ export class RhythmGame extends Game {
start() {
this.audio.prewarm([...ALL_SFX, 'voice/welcome', 'voice/welcome_back',
'voice/tut_1', 'voice/select_hint', ...SONGS.map((s) => s.bgm)]);
'voice/tut_1', 'voice/select_hint', 'bgm/menu', ...SONGS.map((s) => s.bgm)]);
const rid = ++this._runId;
this._cleanup();
if (this._progress.tutorialDone) {
@@ -194,6 +204,9 @@ export class RhythmGame extends Game {
const rid = this._runId;
const buf = await this.audio._load(song.bgm);
if (!buf || rid !== this._runId || !this.audio.ctx) return;
// 暂停发生在 BGM 起点前时 offset 为负:推迟起点而非传负偏移
// (Web Audio 的 start(when, 负 offset) 会抛 IndexSizeError)
if (offset < 0) { t0 -= offset; offset = 0; }
this._stopBgmSrc();
const src = this.audio.ctx.createBufferSource();
src.buffer = buf;
@@ -299,6 +312,10 @@ export class RhythmGame extends Game {
/** 教学应答评估:全部音在窗口内用对手势 → 通过;否则纠正重教。 */
_tutEvaluate(rid) {
if (this._runId !== rid || this.phase !== 'tut') return;
// 评估是一次性的:先废掉触发条件——通过/失败后都要播语音,
// 语音间隙期 phase 仍是 'tut' 且 now ≥ _phaseEnd,泵会每 40ms 重进这里,
// 否则一次通过会级联"连过"剩余步骤(trace 实测:T1 通过直接跳到结算)。
this._phaseEnd = Infinity;
const pass = this._cues.every((cue) => {
const a = this._tutAttempts.find((x) => x.cue === cue);
return a && a.correct && Math.abs(a.dt) <= GOOD;
@@ -341,6 +358,8 @@ export class RhythmGame extends Game {
_enterSelect(rid) {
if (this._runId !== rid) return;
this.phase = 'select';
// 10 文档 §5:选曲界面 = menu pad(引擎层淡入 800ms;bgm=0 时引擎只记意图)
this.audio.play('bgm/menu', { fadeMs: 800 });
this._speak(['voice/select_hint', SONGS[this._songIdx].nameVoice]);
}
@@ -369,6 +388,8 @@ export class RhythmGame extends Game {
_startSong(rid, songIdx) {
if (this._runId !== rid) return;
this._cleanup();
// 10 文档 §5:选定曲目后停 menu pad(选曲/结算态可能在播),再按网格起曲目 BGM
this.audio.stopBgm(400);
this.phase = 'main';
this._songIdx = songIdx;
this._song = SONGS[songIdx];
@@ -413,18 +434,23 @@ export class RhythmGame extends Game {
this._speakGuarded(['voice/tut_skip'], () => this._startSong(rid, 0));
return;
}
// 教学语音播放中单击 = 跳过本句(12 文档 §1.2);无语音时才是跟打手势
if (this._trySkipVoice(g)) return;
this._onTutGesture(g);
return;
case 'select':
if (this._trySkipVoice(g)) return; // 选曲播报可逐句跳过
this._onSelectGesture(rid, g);
return;
case 'main':
// 演奏期单击是演奏手势,永不跳过(mid_song 短句按裁定不可跳)
this._onMainGesture(rid, g);
return;
case 'menu':
this._onMenuGesture(rid, g);
return;
case 'end':
if (this._trySkipVoice(g)) return; // 结算播报可逐句跳过
if (g === 'tap') this._startSong(rid, this._songIdx); // 再来一遍(同曲)
else if (g === 'right') { // 下一首(若已解锁)
if (this._songIdx + 1 < this._progress.unlocked) this._startSong(rid, this._songIdx + 1);
@@ -433,10 +459,22 @@ export class RhythmGame extends Game {
} else if (g === 'down') this._goHome();
return;
default:
return; // intro:不收手势
this._trySkipVoice(g); // intro:只收"单击跳旁白",其余手势不收
return;
}
}
/**
* voice 播放中单击 = 跳过当前句(12 文档 §1.2,链播继续到下一句)。
* 引擎暂无"voice 播放中"公开查询接口,读内部 _voice 令牌判定。
* @returns {boolean} true = 该手势已消费(跳过了一句)
*/
_trySkipVoice(g) {
if (g !== 'tap' || !this.audio._voice) return false;
this.audio.skipVoice();
return true;
}
/** 教学阶段手势:全量记录尝试(评估用),乐器音照播(认识音色)。 */
_onTutGesture(g) {
if (!INST[g] || !this.audio.ctx) return;
@@ -637,7 +675,8 @@ export class RhythmGame extends Game {
}
ids.push('voice/restart');
console.log('[RhythmGame] 结算:', this._stats, '评级:', grade.toUpperCase());
this._speak(ids);
// 10 文档 §5:曲目 BGM 已 0.6s 淡出,结算语音链播完后淡入 menu pad 800ms
this._speak(ids, () => this.audio.play('bgm/menu', { fadeMs: 800 }));
}
// ---------------- 进度存储 ----------------