round2: 看门狗礼仪F1(不重播/单句升级/idle-skip) + 分档归因F2 + 交接count-in/answer F3 + 教学看表现F4-F5 + 警报去叠F6 + 回绕F7 + 开场瘦身F8 + 10句新语音 + 测试全绿(6playtest零改动+guide52断言+资产终检)

This commit is contained in:
SoundGame Dev
2026-08-31 00:26:25 +08:00
parent 296c996fb9
commit 902461f0f4
65 changed files with 1094 additions and 775 deletions
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.
+33 -9
View File
@@ -137,6 +137,8 @@ export class RpgGame extends Game {
this._ambId = null;
// playSec 计时
this._segStart = null;
// F7:回绕提示去重(同一次菜单打开内每句最多 1 次)
this._wrapShown = new Set();
this._newRun();
}
@@ -257,6 +259,7 @@ export class RpgGame extends Game {
this.scene = id;
this.optIdx = 0;
this.phase = 'explore';
this._wrapShown = new Set(); // F7:场景选项列表重新"打开",回绕提示重新可用
this._switchSceneAudio(id, opts.fadeMs || 800);
this.audio.play('sfx/step', { volume: 0.8 });
@@ -307,11 +310,22 @@ export class RpgGame extends Game {
return false;
}
_speakOption() {
_speakOption(onComplete) {
if (this.phase !== 'explore') return;
const opts = SCENES[this.scene].options;
if (!opts.length) return;
this.audio.play(`voice/opt_${opts[this.optIdx]}`);
this.audio.play(`voice/opt_${opts[this.optIdx]}`, { onComplete });
}
/**
* F7(round2):循环列表切回首项/末项的回绕提示(P1「不报回绕」)。full 档
* queue 跟在本项报名之后;同一次菜单打开内每句最多 1 次(_wrapShown),别每滑必播。
*/
_wrapCue(voiceId) {
if (this.guide.level() !== 'full') return;
if (this._wrapShown.has(voiceId)) return;
this._wrapShown.add(voiceId);
this.audio.play(`voice/${voiceId}`, { queue: true });
}
_onExploreGesture(g) {
@@ -320,7 +334,10 @@ export class RpgGame extends Game {
if (g === 'left' || g === 'right') {
const n = opts.length;
this.optIdx = (this.optIdx + (g === 'right' ? 1 : n - 1)) % n;
this._speakOption();
const wrap = n >= 2 && ((g === 'right' && this.optIdx === 0) || (g === 'left' && this.optIdx === n - 1))
? (this.optIdx === 0 ? 'gd_wrap_first' : 'gd_wrap_last') : null;
// 回绕提示排在本项报名播完之后;被下一次滑动打断则自然作废(不迟到误导)
this._speakOption(wrap ? () => this._wrapCue(wrap) : undefined);
} else if (g === 'tap') {
// voice 播放中单击 = 跳过本句(12 文档 §1:场景 desc/NPC 对白/事件/状态链可跳),
// 语音链跳下一句;无 voice 时单击 = 确认选项(原语义)。
@@ -605,13 +622,14 @@ export class RpgGame extends Game {
const b = this._battle;
if (!b) return;
b.state = 'PLAYER_SELECT';
this._wrapShown = new Set(); // F7:指令循环重新"打开",回绕提示重新可用
// 玩家回合的听觉标点(11 文档 §5):turn_player(≤0.4s)→ 留 200ms+ 间隙 → 指令名
this.audio.play('sfx/turn_player');
// 向导层静默看门狗(schemes §2):探索/常规战斗本无卡关计时,PLAYER_SELECT
// 挂机永远静默等——60s 无输入 → nudge + gd_stall(一级即止,自动停止)
// 向导层静默看门狗(round2 F1.3:60s→30s,等待默认低频但不再干晾一分钟):
// PLAYER_SELECT 挂机永远静默等——30s 无输入 → nudge + gd_stall(一级即止)
this.guide.watchdog({
arm: true,
at: [60000],
at: [30000],
say: ['gd_stall'],
cond: () => this.phase === 'battle' && this._battle === b && b.state === 'PLAYER_SELECT',
});
@@ -666,7 +684,11 @@ export class RpgGame extends Game {
if (b.state === 'PLAYER_SELECT') {
if (g === 'left' || g === 'right') {
b.cmdIdx = (b.cmdIdx + (g === 'right' ? 1 : CMDS.length - 1)) % CMDS.length;
this.audio.play(`voice/cmd_${CMDS[b.cmdIdx]}`);
// F7:指令循环回绕提示(探索菜单同款,full 档,跟在本项报名之后)
const wrap = (g === 'right' && b.cmdIdx === 0) || (g === 'left' && b.cmdIdx === CMDS.length - 1)
? (b.cmdIdx === 0 ? 'gd_wrap_first' : 'gd_wrap_last') : null;
this.audio.play(`voice/cmd_${CMDS[b.cmdIdx]}`,
wrap ? { onComplete: () => this._wrapCue(wrap) } : undefined);
// 教学 T0:切换 2 次(听到指令名切换)即过关
if (b.teachStage === 'menu' && ++b.teachSwipes >= 2) this._teachAdvance('attack');
} else if (g === 'tap') {
@@ -830,10 +852,12 @@ export class RpgGame extends Game {
p.hp -= dmg;
if (b.enc.teach) p.hp = Math.max(1, p.hp); // 教学战 HP 锁底 1(必不死)
console.log(`[RpgGame] 敌方攻击 dmg=${dmg} 格挡=${b.parry} 玩家HP=${p.hp}/${this.maxHp}`);
// 教学 T2:格挡成功或受击均可通过 → 进入防御教学 T3(11 文档 §2)
// 教学 T2:格挡成功或受击均可通过 → 进入防御教学 T3(11 文档 §2)。
// F4(round2):格挡**成功**通过时先插 tut_b2b「挡得漂亮。」确认表现,
// 消除 P1/P4「明明挡住了还说来不及格挡」的歧义(失败路径 tut_b4 不变)。
if (b.teachStage === 'parry') {
b.teachStage = 'defend';
const ids = b.parry ? ['voice/tut_b6'] : ['voice/tut_b4', 'voice/tut_b6'];
const ids = b.parry ? ['voice/tut_b2b', 'voice/tut_b6'] : ['voice/tut_b4', 'voice/tut_b6'];
this._speakGuarded(ids, () => this._playerSelect(), 10000);
this._armTeachWatchdog('defend');
return;