import { readFileSync } from 'node:fs'; import { createServer } from 'vite'; const expectedArcRanges = { 'yellow-turban': [1, 4], 'anti-dong': [5, 6], xuzhou: [7, 9], wandering: [10, 15], wolong: [16, 18], 'red-cliffs': [19, 22], 'jing-yi': [23, 34], 'hanzhong-shuhan': [35, 37], 'jingzhou-crisis': [38, 44], 'yiling-baidi': [45, 46], nanzhong: [47, 54], northern: [55, 66] }; const expectedBattleMusicKeys = { 'yellow-turban': 'militia-theme', 'anti-dong': 'battle-prep', xuzhou: 'story-dark', wandering: 'story-dark', wolong: 'battle-prep', 'red-cliffs': 'camp-grand-strategy', 'jing-yi': 'camp-grand-strategy', 'hanzhong-shuhan': 'battle-prep', 'jingzhou-crisis': 'story-dark', 'yiling-baidi': 'story-dark', nanzhong: 'camp-frontier', northern: 'camp-frontier' }; const expectedStages = ['camp', 'story', 'sortie', 'battle', 'aftermath']; const expectedIntensities = new Set(['quiet', 'measured', 'urgent', 'climactic']); const errors = []; const server = await createServer({ logLevel: 'error', server: { middlewareMode: true, hmr: false }, appType: 'custom' }); try { const presentationModule = await server.ssrLoadModule( '/src/game/data/campaignPresentationProfiles.ts' ); const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts'); const { campaignBattleRouteEntries } = await server.ssrLoadModule( '/src/game/state/campaignRouting.ts' ); const { ambienceTrackGainCompensation, musicTracks, ambienceTracks } = await server.ssrLoadModule( '/src/game/audio/audioAssets.ts' ); const { campSkinDefinitions, campSkinBattleArcs } = await server.ssrLoadModule( '/src/game/data/campSkins.ts' ); const { getCampSoundscape } = await server.ssrLoadModule( '/src/game/data/campSoundscapes.ts' ); const { battleEnvironmentProfiles } = await server.ssrLoadModule( '/src/game/data/battleEnvironmentProfiles.ts' ); const { campaignArcPresentationProfiles, campaignBattlePresentationProfiles, campaignPresentationStatePolicies, campaignPresentationTransitionPolicy, campaignBattleAmbienceFallbackVolume, campaignPresentationArcForOrdinal, campaignPresentationProfileFor, campaignPresentationStateRuleFor, resolveCampaignPresentationSoundscape } = presentationModule; const scenarioBattleIds = Object.keys(battleScenarios); const battleIds = campaignBattleRouteEntries().map((entry) => entry.battleId); const arcEntries = Object.entries(campaignArcPresentationProfiles); const arcIds = Object.keys(expectedArcRanges); const battleMusicKeys = new Set(); let battleAmbienceFallbackCount = 0; assertEqual(battleIds.length, 66, 'campaign battle count'); assertEqual(new Set(battleIds).size, 66, 'canonical campaign route uniqueness'); assertEqual( JSON.stringify(scenarioBattleIds), JSON.stringify(battleIds), 'battle scenario declaration order must match the independent campaign route' ); assertEqual(arcEntries.length, 12, 'campaign presentation arc count'); assertEqual(Object.keys(campaignBattlePresentationProfiles).length, 66, 'battle profile count'); assertEqual(Object.keys(campaignPresentationStatePolicies).length, 7, 'state policy count'); arcEntries.forEach(([arcId, profile], index) => { const expectedRange = expectedArcRanges[arcId]; assert(Boolean(expectedRange), `${arcId}: unexpected presentation arc`); if (!expectedRange) { return; } const [expectedFirst, expectedLast] = expectedRange; const campArc = campSkinBattleArcs.find(({ skinId }) => skinId === arcId); const skin = campSkinDefinitions[arcId]; const campSoundscape = getCampSoundscape(arcId); const statePolicy = campaignPresentationStatePolicies[profile.statePolicyId]; assertEqual(profile.id, arcId, `${arcId}: profile id`); assertEqual(profile.skinId, arcId, `${arcId}: skin id`); assertNonEmptyString(profile.label, `${arcId}: label`); assertEqual(profile.firstBattleOrdinal, expectedFirst, `${arcId}: first battle`); assertEqual(profile.lastBattleOrdinal, expectedLast, `${arcId}: last battle`); assertEqual(campArc?.firstBattleOrdinal, expectedFirst, `${arcId}: camp arc first battle`); assertEqual(campArc?.lastBattleOrdinal, expectedLast, `${arcId}: camp arc last battle`); assertEqual(expectedFirst, index === 0 ? 1 : arcEntries[index - 1][1].lastBattleOrdinal + 1, `${arcId}: contiguous start`); assert(Boolean(skin), `${arcId}: matching camp skin`); assertEqual(profile.palette.accentColor, skin?.accentColor, `${arcId}: accent color`); assertEqual(profile.palette.headerColor, skin?.headerColor, `${arcId}: header color`); assertEqual(profile.palette.washColor, skin?.washColor, `${arcId}: wash color`); assert( Number.isFinite(profile.palette.battleTintColor) && profile.palette.battleTintColor >= 0 && profile.palette.battleTintColor <= 0xffffff, `${arcId}: battle tint color must be a 24-bit color` ); assert( Number.isFinite(profile.palette.battleTintAlpha) && profile.palette.battleTintAlpha >= 0 && profile.palette.battleTintAlpha <= 0.08, `${arcId}: battle tint alpha must stay within the 0..0.08 legibility budget` ); assertEqual(profile.audio.familyId, campSoundscape.music.id, `${arcId}: music family`); assertEqual(profile.audio.anchorMusicKey, campSoundscape.music.trackKey, `${arcId}: anchor music`); assertEqual(profile.audio.anchorMusicVolume, campSoundscape.music.volume, `${arcId}: anchor volume`); assertEqual(profile.audio.defaultAmbienceKey, campSoundscape.ambience.trackKey, `${arcId}: ambience`); assertEqual(profile.audio.defaultAmbienceVolume, campSoundscape.ambience.volume, `${arcId}: ambience volume`); assertEqual(profile.audio.battleMusicKey, expectedBattleMusicKeys[arcId], `${arcId}: battle music`); battleMusicKeys.add(profile.audio.battleMusicKey); [ profile.audio.anchorMusicKey, profile.audio.tensionMusicKey, profile.audio.battleMusicKey, profile.audio.resolutionMusicKey ].forEach((trackKey) => { assert(Boolean(musicTracks[trackKey]), `${arcId}: unregistered music track "${trackKey}"`); }); assert( Boolean(ambienceTracks[profile.audio.defaultAmbienceKey]), `${arcId}: unregistered ambience track "${profile.audio.defaultAmbienceKey}"` ); assert(Array.isArray(profile.motifs) && profile.motifs.length >= 3, `${arcId}: at least three motifs`); assertEqual(new Set(profile.motifs).size, profile.motifs.length, `${arcId}: unique motifs`); profile.motifs.forEach((motif) => assertNonEmptyString(motif, `${arcId}: motif`)); assert(Boolean(statePolicy), `${arcId}: known state policy "${profile.statePolicyId}"`); expectedStages.forEach((stage) => { const rule = statePolicy?.[stage]; assert(Boolean(rule), `${arcId}/${stage}: state rule`); assert(expectedIntensities.has(rule?.intensity), `${arcId}/${stage}: valid intensity`); assert( campaignPresentationStateRuleFor(battleIds[expectedFirst - 1], stage) === rule, `${arcId}/${stage}: state rule lookup` ); }); }); arcIds.forEach((arcId) => { assert(Boolean(campaignArcPresentationProfiles[arcId]), `${arcId}: missing presentation arc`); }); assertEqual(arcEntries.at(-1)?.[1].lastBattleOrdinal, 66, 'final arc coverage'); assert( battleMusicKeys.size >= 4, `battle music must span at least four existing tracks; received ${[...battleMusicKeys].join(', ')}` ); battleIds.forEach((battleId, index) => { const battleOrdinal = index + 1; const profile = campaignBattlePresentationProfiles[battleId]; const expectedArcId = arcEntries.find( ([, arc]) => battleOrdinal >= arc.firstBattleOrdinal && battleOrdinal <= arc.lastBattleOrdinal )?.[0]; assert(Boolean(profile), `${battleId}: presentation profile`); assertEqual(profile?.battleId, battleId, `${battleId}: battle id`); assertEqual(profile?.battleOrdinal, battleOrdinal, `${battleId}: battle ordinal`); assertEqual(profile?.arc.id, expectedArcId, `${battleId}: arc id`); assert(campaignPresentationProfileFor(battleId) === profile, `${battleId}: profile lookup identity`); assert(campaignPresentationArcForOrdinal(battleOrdinal) === profile?.arc, `${battleId}: ordinal lookup identity`); const campPlan = resolveCampaignPresentationSoundscape({ battleId, stage: 'camp' }); assertEqual(campPlan?.musicKey, profile?.arc.audio.anchorMusicKey, `${battleId}: camp music`); assertEqual(campPlan?.ambienceKey, profile?.arc.audio.defaultAmbienceKey, `${battleId}: camp ambience`); const battlePlan = resolveCampaignPresentationSoundscape({ battleId, stage: 'battle' }); const environment = battleEnvironmentProfiles[battleId]; const expectedAmbienceKey = environment?.soundscape.ambienceKey ?? profile?.arc.audio.defaultAmbienceKey; const expectedAmbienceVolume = environment?.soundscape.ambienceVolume ?? 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`); assert(Boolean(battlePlan?.ambienceKey), `${battleId}: every battle must retain a quiet ambience bed`); if (!environment) { battleAmbienceFallbackCount += 1; assert( battlePlan.ambienceVolume >= campaignPresentationTransitionPolicy.battleAmbienceFallbackMinimum && battlePlan.ambienceVolume <= campaignPresentationTransitionPolicy.battleAmbienceFallbackMaximum, `${battleId}: fallback ambience must stay inside the quiet battle mix` ); } assert(battlePlan?.transition === campaignPresentationTransitionPolicy, `${battleId}: transition policy identity`); }); assertEqual( battleAmbienceFallbackCount, battleIds.length - Object.keys(battleEnvironmentProfiles).length, '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', sceneMusicKey: 'title-theme', sceneAmbienceKey: 'battle-rain', sceneAmbienceVolume: 0.11 }); assertEqual(preservedStoryPlan?.musicKey, 'title-theme', 'story scene music preservation'); assertEqual(preservedStoryPlan?.ambienceKey, 'battle-rain', 'story scene ambience preservation'); assertEqual(preservedStoryPlan?.ambienceVolume, 0.11, 'story scene ambience volume preservation'); const foundingStoryPlan = resolveCampaignPresentationSoundscape({ battleId: battleIds[0], stage: 'story', sceneMusicKey: 'militia-theme' }); assertEqual( foundingStoryPlan?.musicKey, campaignBattlePresentationProfiles[battleIds[0]].arc.audio.anchorMusicKey, 'story rally cue follows the founding arc anchor' ); const redCliffsStoryPlan = resolveCampaignPresentationSoundscape({ battleId: battleIds[18], stage: 'story', sceneMusicKey: 'battle-prep' }); assertEqual( redCliffsStoryPlan?.musicKey, expectedBattleMusicKeys['red-cliffs'], 'story preparation cue follows the Red Cliffs battle family' ); const foundingAftermathPlan = resolveCampaignPresentationSoundscape({ battleId: battleIds[0], stage: 'aftermath', sceneMusicKey: 'battle-prep' }); assertEqual( foundingAftermathPlan?.musicKey, campaignBattlePresentationProfiles[battleIds[0]].arc.audio.resolutionMusicKey, 'aftermath preparation cue resolves through the arc theme' ); assertEqual(campaignPresentationProfileFor('unknown-battle'), undefined, 'unknown battle profile'); assertEqual(campaignPresentationStateRuleFor('unknown-battle', 'camp'), undefined, 'unknown state rule'); assertEqual( resolveCampaignPresentationSoundscape({ battleId: 'unknown-battle', stage: 'battle' }), undefined, 'unknown soundscape' ); assertEqual(campaignPresentationTransitionPolicy.sameTrackBehavior, 'keep-current-loop', 'same-track behavior'); assertEqual(campaignPresentationTransitionPolicy.musicCrossfadeMs, 900, 'music crossfade duration'); assertEqual(campaignPresentationTransitionPolicy.ambienceCrossfadeMs, 1200, 'ambience crossfade duration'); assertEqual( campaignPresentationTransitionPolicy.battleAmbienceBehavior, 'specific-profile-then-arc-fallback', 'battle ambience behavior' ); 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'); assert( soundDirectorSource.includes("createLoopChannel('music', 0.22, 900)"), 'SoundDirector music channel must retain the 900ms crossfade' ); assert( soundDirectorSource.includes("createLoopChannel('ambience', 0.08, 1200)"), 'SoundDirector ambience channel must retain the 1200ms crossfade' ); const battleSceneSource = readFileSync('src/game/scenes/BattleScene.ts', 'utf8'); const storySceneSource = readFileSync('src/game/scenes/StoryScene.ts', 'utf8'); const campSceneSource = readFileSync('src/game/scenes/CampScene.ts', 'utf8'); const titleSceneSource = readFileSync('src/game/scenes/TitleScene.ts', 'utf8'); [ 'resolveCampaignPresentationSoundscape({', "stage: 'battle'", 'musicKey: presentationSoundscape?.musicKey', 'presentationBattleId: battleScenario.id', "presentationStage: 'aftermath'", 'campaignPresentationProfileFor(battleScenario.id)', 'battleTintColor' ].forEach((requiredSource) => { assert(battleSceneSource.includes(requiredSource), `BattleScene presentation integration: ${requiredSource}`); }); [ 'presentationBattleId?: string', "presentationStage?: Extract", 'resolveCampaignPresentationSoundscape({', 'this.applyStoryPresentationWash()', 'profile.arc.palette.washColor' ].forEach((requiredSource) => { assert(storySceneSource.includes(requiredSource), `StoryScene presentation integration: ${requiredSource}`); }); [ 'presentationBattleId: flow.nextBattleId', "presentationStage: 'story'", 'presentationBattleId: flow.afterBattleId' ].forEach((requiredSource) => { assert(campSceneSource.includes(requiredSource), `CampScene presentation integration: ${requiredSource}`); }); const prologueOpeningRouteCount = titleSceneSource.split('pages: prologueOpeningPages()').length - 1; [ "nextScene: 'PrologueVillageScene'", "campaign.step === 'prologue-town'", 'pages: prologueBrotherhoodPages()', "nextScene: 'PrologueMilitiaCampScene'", "campaign.step === 'prologue-camp'", 'pages: prologueDeparturePages()', "nextScene: 'BattleScene'", "presentationBattleId: 'first-battle-zhuo-commandery'", "presentationStage: 'story'" ].forEach((requiredSource) => { assert(titleSceneSource.includes(requiredSource), `TitleScene prologue presentation route: ${requiredSource}`); }); assert( prologueOpeningRouteCount >= 2, 'TitleScene must apply the first campaign arc to both new-game and resumed prologue openings' ); if (errors.length > 0) { throw new Error( `Campaign presentation profile verification failed:\n${errors.map((error) => `- ${error}`).join('\n')}` ); } console.log( `Verified ${arcEntries.length} campaign presentation arcs across ${battleIds.length} battles, ` + `${battleMusicKeys.size} battle music groups, ${battleAmbienceFallbackCount} loudness-calibrated ambience fallbacks, ` + 'all state policies, palettes, motifs, and crossfade constraints.' ); } finally { await server.close(); } function assertNonEmptyString(value, context) { assert(typeof value === 'string' && value.trim().length > 0, `${context} must be a non-empty string`); } function assertEqual(actual, expected, context) { assert(Object.is(actual, expected), `${context}: expected ${format(expected)}, received ${format(actual)}`); } function assert(condition, message) { if (!condition) { errors.push(message); } } function format(value) { return value === undefined ? 'undefined' : JSON.stringify(value); }