fix: align audiovisual and narrative feedback

This commit is contained in:
2026-07-23 23:20:50 +09:00
parent 8cf0886d7d
commit e06b15ce7a
27 changed files with 1368 additions and 142 deletions

View File

@@ -48,7 +48,7 @@ try {
const { campaignBattleRouteEntries } = await server.ssrLoadModule(
'/src/game/state/campaignRouting.ts'
);
const { musicTracks, ambienceTracks } = await server.ssrLoadModule(
const { ambienceTrackGainCompensation, musicTracks, ambienceTracks } = await server.ssrLoadModule(
'/src/game/audio/audioAssets.ts'
);
const { campSkinDefinitions, campSkinBattleArcs } = await server.ssrLoadModule(
@@ -197,7 +197,12 @@ try {
const environment = battleEnvironmentProfiles[battleId];
const expectedAmbienceKey = environment?.soundscape.ambienceKey ?? profile?.arc.audio.defaultAmbienceKey;
const expectedAmbienceVolume = environment?.soundscape.ambienceVolume ??
campaignBattleAmbienceFallbackVolume(profile?.arc.audio.defaultAmbienceVolume);
campaignBattleAmbienceFallbackVolume(
profile?.arc.audio.defaultAmbienceVolume,
profile?.arc.audio.defaultAmbienceKey,
profile?.arc.audio.battleMusicKey,
profile?.arc.audio.anchorMusicVolume
);
assertEqual(battlePlan?.musicKey, profile?.arc.audio.battleMusicKey, `${battleId}: battle music plan`);
assertEqual(battlePlan?.ambienceKey, expectedAmbienceKey, `${battleId}: battle ambience plan`);
assertEqual(battlePlan?.ambienceVolume, expectedAmbienceVolume, `${battleId}: battle ambience volume`);
@@ -218,6 +223,47 @@ try {
'battle ambience fallback count'
);
[
[battleIds[0], 'campfire-ambience', 0.166],
[battleIds[16], 'mountain-wind-ambience', 0.153],
[battleIds[32], 'river-night-ambience', 0.093],
[battleIds[53], 'nanzhong-night-ambience', 0.084]
].forEach(([battleId, ambienceKey, expectedVolume]) => {
const plan = resolveCampaignPresentationSoundscape({ battleId, stage: 'battle' });
assertEqual(plan?.ambienceKey, ambienceKey, `${battleId}: calibrated fallback track`);
assertEqual(plan?.ambienceVolume, expectedVolume, `${battleId}: calibrated fallback target volume`);
});
const quietStoryCampfirePlan = resolveCampaignPresentationSoundscape({ battleId: battleIds[9], stage: 'battle' });
const quietFrontierMountainPlan = resolveCampaignPresentationSoundscape({ battleId: battleIds[54], stage: 'battle' });
assert(
quietStoryCampfirePlan?.musicKey === 'story-dark' &&
quietStoryCampfirePlan.ambienceKey === 'campfire-ambience' &&
quietStoryCampfirePlan.ambienceVolume <= 0.085,
`${quietStoryCampfirePlan?.battleId}: quiet story-dark pairing must attenuate campfire fallback`
);
assert(
quietFrontierMountainPlan?.musicKey === 'camp-frontier' &&
quietFrontierMountainPlan.ambienceKey === 'mountain-wind-ambience' &&
quietFrontierMountainPlan.ambienceVolume <= 0.105,
`${quietFrontierMountainPlan?.battleId}: quiet frontier pairing must attenuate mountain fallback`
);
const redCliffsFirePlan = resolveCampaignPresentationSoundscape({ battleId: battleIds[21], stage: 'battle' });
const yilingFirePlan = resolveCampaignPresentationSoundscape({ battleId: battleIds[45], stage: 'battle' });
const wuzhangSnowPlan = resolveCampaignPresentationSoundscape({ battleId: battleIds[65], stage: 'battle' });
[redCliffsFirePlan, yilingFirePlan].forEach((plan) => {
assert(
plan?.ambienceKey === 'battle-fire' &&
plan.ambienceVolume * ambienceTrackGainCompensation['battle-fire'] <= 0.095,
`${plan?.battleId}: fire crackle effective volume must retain its peak ceiling`
);
});
assert(
wuzhangSnowPlan?.ambienceKey === 'mountain-wind-ambience' &&
wuzhangSnowPlan.ambienceVolume * ambienceTrackGainCompensation['mountain-wind-ambience'] <= 0.08,
`${wuzhangSnowPlan?.battleId}: snow-wind effective volume must retain its gust ceiling`
);
const preservedStoryPlan = resolveCampaignPresentationSoundscape({
battleId: battleIds[0],
stage: 'story',
@@ -274,9 +320,12 @@ try {
'specific-profile-then-arc-fallback',
'battle ambience behavior'
);
assertEqual(campaignPresentationTransitionPolicy.battleAmbienceFallbackScale, 0.58, 'battle ambience fallback scale');
assertEqual(campaignPresentationTransitionPolicy.battleAmbienceFallbackMinimum, 0.05, 'battle ambience fallback minimum');
assertEqual(campaignPresentationTransitionPolicy.battleAmbienceFallbackMaximum, 0.065, 'battle ambience fallback maximum');
assertEqual(campaignPresentationTransitionPolicy.battleAmbienceFallbackScale, 1, 'battle ambience fallback scale');
assertEqual(campaignPresentationTransitionPolicy.battleAmbienceFallbackMinimum, 0.08, 'battle ambience fallback minimum');
assertEqual(campaignPresentationTransitionPolicy.battleAmbienceFallbackMaximum, 0.2, 'battle ambience fallback maximum');
assertEqual(campaignPresentationTransitionPolicy.battleAmbienceTargetRelativeLoudnessLu, -16, 'battle ambience target offset');
assertEqual(campaignPresentationTransitionPolicy.battleAmbienceRelativeLoudnessMinimumLu, -18, 'battle ambience lower bound');
assertEqual(campaignPresentationTransitionPolicy.battleAmbienceRelativeLoudnessMaximumLu, -14, 'battle ambience upper bound');
assert(Object.isFrozen(campaignPresentationTransitionPolicy), 'transition policy must be frozen');
const soundDirectorSource = readFileSync('src/game/audio/SoundDirector.ts', 'utf8');
@@ -334,7 +383,7 @@ try {
console.log(
`Verified ${arcEntries.length} campaign presentation arcs across ${battleIds.length} battles, ` +
`${battleMusicKeys.size} battle music groups, ${battleAmbienceFallbackCount} quiet ambience fallbacks, ` +
`${battleMusicKeys.size} battle music groups, ${battleAmbienceFallbackCount} loudness-calibrated ambience fallbacks, ` +
'all state policies, palettes, motifs, and crossfade constraints.'
);
} finally {