第二批反馈:引擎改造(限幅/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
+51 -6
View File
@@ -2,7 +2,13 @@
* 音序回响(Simon)R2 —— 音频版西蒙记忆游戏(按 docs/design/02-simon-redesign.md 实现)。
*
* R2 要点:
* - 无 BGM(不加载不播放,bgm/ 目录已空):只有手势音色 + 语音两条线。
* - 游玩全程无 BGM(用户硬性要求,02 文档既定):教学/对局/序列播放静默;
* 仅结算(gameover)与菜单挂 bgm/menu(volume 0.3,比别的游戏更轻,
* 10 号文档 §4 simon 特例表);单击/菜单重开时 stopBgm(300) 回静默。
* - 单击跳过旁白(12 号文档):voice 播放中单击 = audio.skipVoice()(截断当前句、
* 语音链继续跳下一句);无语音时单击 = 原语义。例外:教学"教一个练一个"的
* 等待输入阶段(teachAwait)单击仍是答题;playback 阶段手势静默忽略、菜单
* 单击循环菜单项(均维持原裁定)。开场 t0 附近插播一次 voice/skip_hint。
* - 旁白全部整句化:轮次 round_01..round_20 + round_more 兜底;成绩 score_band1..6
* 分段整句;删除 di/lun/num_0–9/score_pre/best_pre 拼接体系与旧教学连播。
* - 交互式教学(T0-T7):教一个音色练一次该手势,做对才教下一个;教学期下滑跳过。
@@ -81,12 +87,18 @@ export class SimonGame extends Game {
this.audio.prewarm([
...GESTURES.map((g) => TONE[g]),
'sfx/gameover',
'voice/t0_welcome', 'voice/t0_goal', 'voice/t_try', 'voice/round_01',
'voice/t0_welcome', 'voice/t0_goal', 'voice/skip_hint', 'voice/t_try', 'voice/round_01',
]);
this._timer(() => this._runTutorial(), 500); // 开场静默 0.5s 后进教学(无 BGM)
}
onGesture(g) {
// 单击跳过旁白(12 号文档):voice 播放中单击 = skipVoice()(截断当前句、
// 链播自然进下一句);无语音在播时单击 = 各阶段原语义。跳过不发震动。
if (g === 'tap' && this._tapSkipsVoice()) {
this.audio.skipVoice();
return;
}
switch (this.phase) {
case 'tutorial':
this._onTutorialGesture(g);
@@ -108,11 +120,31 @@ export class SimonGame extends Game {
}
}
/**
* 单击是否应判定为"跳过当前旁白句"。规则:有 voice 在播 + 当前阶段允许跳过。
* - tutorial:仅讲解/反馈语音播放中可跳;teachAwait(等玩家练手势)单击=答题;
* - announce/roundend/gameover:轮次/成绩/结算播报可跳(高轮次玩家刚需);
* - playback:维持"序列播放期手势静默忽略"裁定(含第 1 轮 your_turn);
* - input:答题阶段无语音;menu:单击循环菜单项是原语义,不拦截。
* 读引擎的 _voice 字段(只读)判断是否有对白在播;无公开 API,属既定约定。
*/
_tapSkipsVoice() {
if (!this.audio._voice) return false;
switch (this.phase) {
case 'tutorial': return !this.teachAwait;
case 'announce':
case 'roundend':
case 'gameover': return true;
default: return false;
}
}
// ---------------- 交互式教学(T0-T7) ----------------
_runTutorial() {
this.phase = 'tutorial';
this._speak(['voice/t0_welcome', 'voice/t0_goal'], () => this._teachStep(0));
// t0 附近插播一次 skip_hint("单击可跳过旁白"),每局教学只此一次
this._speak(['voice/t0_welcome', 'voice/skip_hint', 'voice/t0_goal'], () => this._teachStep(0));
}
/** 教第 i 个手势:指令句 → 播音色 → "你试试。" → 等玩家做该手势。 */
@@ -143,7 +175,9 @@ export class SimonGame extends Game {
}
_onTutorialGesture(g) {
if (g === 'down') { // 下滑 = 跳过教学
// 下滑=跳过教学 与 T2"教下滑手势"冲突,按上下文判定(同双击裁定①的思路):
// 等待输入阶段且当前教的就是下滑时,下滑=答题;其余教学时刻下滑=跳过。
if (g === 'down' && !(this.teachAwait && GESTURES[this.teachIdx] === 'down')) {
this._session++;
this._clearTimers();
this.audio.stopVoice();
@@ -194,6 +228,7 @@ export class SimonGame extends Game {
}
_restart() {
this.audio.stopBgm(300); // 停 menu pad,回静默(10 号文档 simon 表)
this.audio.play('sfx/tone_tap'); // 重开确认音
this._beginGame(); // 重开跳过教学
}
@@ -247,9 +282,9 @@ export class SimonGame extends Game {
return false;
}
/** 播放间隔:max(420, 700 - (round-1)*25),第 12 轮才到下限(02 §6.1)。 */
/** 播放间隔:max(420, 700 - (round-1)*26),第 12 轮起到 420ms 下限(02 §6.1 + 13 号验收)。 */
_interval() {
return Math.max(420, 700 - (this.round - 1) * 25);
return Math.max(420, 700 - (this.round - 1) * 26);
}
_playSequence() {
@@ -377,6 +412,14 @@ export class SimonGame extends Game {
this._timer(() => {
if (s === this._session) this._speak(ids, null); // 链播完停在 gameover,等手势
}, 1400);
// 状态 BGM(10 号文档 simon 表):sfx/gameover(约 0.95s,+700ms 起播)
// 播完 1s 后淡入 menu pad 1000ms,音量 0.3。若玩家已进菜单/重开则不补播。
this._timer(() => {
if (s === this._session && this.phase === 'gameover') {
this.audio.play('bgm/menu', { volume: 0.3, fadeMs: 1000 });
}
}, 700 + 950 + 1000);
}
/** 成绩分段整句:0 / 1-3 / 4-6 / 7-9 / 10-14 / ≥15(02 §3.2)。 */
@@ -398,6 +441,8 @@ export class SimonGame extends Game {
_openMenu() {
this._menuIdx = 0;
this.phase = 'menu';
// 状态 BGM:菜单挂 menu pad(volume 0.3,淡入 600ms;与结算同一首,引擎不重启)
this.audio.play('bgm/menu', { volume: 0.3, fadeMs: 600 });
this._speak(['voice/menu_open', 'voice/menu_restart'], null);
}