第二批反馈:引擎改造(限幅/SoundTouch不变调变速/长按快进/跳句/iOS闪光降级) + 6游戏状态BGM与试玩测试全绿
This commit is contained in:
+42
-3
@@ -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 }));
|
||||
}
|
||||
|
||||
// ---------------- 进度存储 ----------------
|
||||
|
||||
Reference in New Issue
Block a user