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
+17 -7
View File
@@ -93,7 +93,11 @@ function capture(audio) {
pending.push(`${fmtT()} ♫ BGM 切换 → ${id}${opts?.fadeMs ? `(淡入 ${opts.fadeMs}ms)` : ''}`);
}
} else if (isVoice && id.startsWith('voice/gd_')) {
// 向导语音经事件钩子渲染(▶ 向导语音),此处不重复记行
// 向导语音(round2):实际播出的在本行渲染(含全文);被 idle-skip 跳过的
// 不产生 play 调用,由事件钩子单独标注(见 captureGuide)
pending.push(t
? `${fmtT()} ▶ 向导语音:「${t}」`
: `${fmtT()} ▶ 向导语音 voice/${id.slice(6)}(无台词表文本)`);
} else if (isVoice) {
pending.push(t
? `${fmtT()} ▶ 语音:「${t}」`
@@ -105,15 +109,17 @@ function capture(audio) {
};
}
/** 向导实例接入事件流(每个 earcon/插播语音都转写成玩家可感知的一行)。 */
/** 向导实例接入事件流(每个 earcon / 被 idle-skip 的语音都转写成玩家可感知的一行;
* 实际播出的向导语音已由 audio.play 捕获渲染,此处不再重复记行)。 */
function captureGuide(game) {
if (!game.guide) return;
game.guide.setEventHook((evt) => {
if (evt.voiceId) {
if (!evt.skipped) return; // 播出的由 capture() 渲染,避免重复
const t = textOf(`voice/${evt.voiceId}`);
pending.push(t
? `${fmtT()} ▶ 向导语音:「${t}」`
: `${fmtT()} ▶ 向导语音 ${evt.voiceId}(无台词表文本)`);
? `${fmtT()} ▶ 向导语音(对白在播,本句按 idle-only 跳过未播):「${t}」`
: `${fmtT()} ▶ 向导语音 ${evt.voiceId}(idle-only 跳过未播)`);
} else {
const label = GUIDE_LABEL[evt.name] || '';
pending.push(`${fmtT()} ♪ 向导音:${evt.name}${label ? `(${label})` : ''}`);
@@ -122,8 +128,11 @@ function captureGuide(game) {
}
function drain() {
// 震动/资产缺失按「实际触发时刻」落行(v2 起不再用 drain 时刻近似——
// round2 审计指标要核 danger 震 ≥400ms 去抖、警报 600ms 去重,必须看真实时间)
for (const m of mock.vibrateLog.splice(0)) {
pending.push(`${fmtT()} 〰 震动:${hapticName(m.pattern)}`);
const t = `t+${(m.t / 1000).toFixed(1).padStart(6)}s`;
pending.push(`${t} 〰 震动:${hapticName(m.pattern)}`);
}
// 引擎按 ogg→m4a→mp3→wav、性别目录→平铺 逐个探测;只有全部路径都 404,
// 玩家才是真的什么都没听到(角色音探测性别目录 404 后平铺成功属正常回退)
@@ -131,7 +140,7 @@ function drain() {
const key = m.url.replace(/^audio\//, '').replace(/\.\w+$/, '').replace(/^voice\/(male|female)\//, 'voice/');
const everOk = mock.fetchedLog.some((f) => f.ok
&& f.url.replace(/^audio\//, '').replace(/\.\w+$/, '').replace(/^voice\/(male|female)\//, 'voice/') === key);
if (!everOk) pending.push(`${fmtT()} ⚠ 资产缺失 ${key} → 玩家此刻听到的是静默`);
if (!everOk) pending.push(`t+${(m.t / 1000).toFixed(1).padStart(6)}s ⚠ 资产缺失 ${key} → 玩家此刻听到的是静默`);
}
pending.sort((a, b) => parseFloat(a.match(/t\+(\s*)(\d+)/)?.[2] ?? 0) - parseFloat(b.match(/t\+(\s*)(\d+)/)?.[2] ?? 0));
lines.push(...pending);
@@ -164,7 +173,8 @@ function makeCtx(audio, game, describe) {
status: (tag) => {
const d = describe ? describe(game) : '';
const v = voiceBusy(audio) ? '语音播放中' : (audio._bgm ? `BGM=${audio._bgm?.id ?? '无'}` : '无 BGM');
lines.push(`${fmtT()} 【状态】${tag || ''}${d ? ` ${d}` : ''}(${v})`);
// 语速档随状态行输出(round2 F9:转写需可见语速切换生效)
lines.push(`${fmtT()} 【状态】${tag || ''}${d ? ` ${d}` : ''}(${v},语速${settings.get().speed})`);
},
silence: (sec, why) => {
lines.push(`${fmtT()} ……… 静默 ${sec}s${why ? `(${why})` : ',游戏在等待玩家输入'} ………`);