import { readFileSync } from 'node:fs'; import { createServer } from 'vite'; const expectedProfiles = { 'ninth-battle-xuzhou-gate-night-raid': { profileId: 'xuzhou-night-mist', effect: 'night-mist', ambienceKey: 'river-night-ambience', particleKind: null, hazeKind: 'mist' }, 'twenty-second-battle-red-cliffs-fire': { profileId: 'red-cliffs-firestorm', effect: 'embers-smoke', ambienceKey: 'battle-fire', particleKind: 'ember', hazeKind: 'smoke' }, 'fortieth-battle-han-river-flood': { profileId: 'han-river-flood-rain', effect: 'rain-mist', ambienceKey: 'battle-rain', particleKind: 'rain', hazeKind: 'mist' }, 'forty-sixth-battle-yiling-fire': { profileId: 'yiling-burning-forest', effect: 'embers-smoke', ambienceKey: 'battle-fire', particleKind: 'ember', hazeKind: 'smoke' }, 'sixty-first-battle-hanzhong-rain-defense': { profileId: 'hanzhong-defense-rain', effect: 'steady-rain', ambienceKey: 'battle-rain', particleKind: 'rain', hazeKind: 'mist' }, 'sixty-sixth-battle-wuzhang-final': { profileId: 'wuzhang-snow-wind', effect: 'snow-wind', ambienceKey: 'mountain-wind-ambience', particleKind: 'snow', hazeKind: 'mist' } }; const server = await createServer({ logLevel: 'error', server: { middlewareMode: true, hmr: false }, appType: 'custom' }); try { const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts'); const { ambienceTracks } = await server.ssrLoadModule('/src/game/audio/audioAssets.ts'); const environmentModule = await server.ssrLoadModule('/src/game/data/battleEnvironmentProfiles.ts'); const { battleEnvironmentProfiles, battleEnvironmentProfileBattleIds, battleEnvironmentProfileFor, battleEnvironmentProfileVerification, validateBattleEnvironmentProfiles } = environmentModule; const errors = [...validateBattleEnvironmentProfiles()]; const expectedBattleIds = Object.keys(expectedProfiles).sort(); const actualBattleIds = Object.keys(battleEnvironmentProfiles).sort(); const declaredBattleIds = [...battleEnvironmentProfileBattleIds].sort(); if (JSON.stringify(actualBattleIds) !== JSON.stringify(expectedBattleIds)) { errors.push(`expected exactly six representative profile keys: ${JSON.stringify(actualBattleIds)}`); } if (JSON.stringify(declaredBattleIds) !== JSON.stringify(expectedBattleIds)) { errors.push(`representative battle id declaration is out of sync: ${JSON.stringify(declaredBattleIds)}`); } const profileIds = new Set(); Object.entries(expectedProfiles).forEach(([battleId, expected]) => { const profile = battleEnvironmentProfiles[battleId]; if (!battleScenarios[battleId]) { errors.push(`${battleId}: battle scenario does not exist`); } if (!profile) { errors.push(`${battleId}: missing environment profile`); return; } if (battleEnvironmentProfileFor(battleId) !== profile) { errors.push(`${battleId}: lookup API must return the registered profile object`); } if (profile.id !== expected.profileId || profile.effect !== expected.effect) { errors.push(`${battleId}: expected ${expected.profileId}/${expected.effect}, got ${profile.id}/${profile.effect}`); } if (profile.soundscape?.ambienceKey !== expected.ambienceKey) { errors.push( `${battleId}: expected ambience ${expected.ambienceKey}, got ${profile.soundscape?.ambienceKey ?? null}` ); } if (!ambienceTracks[profile.soundscape?.ambienceKey]) { errors.push(`${battleId}: ambience track is not registered: ${profile.soundscape?.ambienceKey ?? null}`); } if ( !Number.isFinite(profile.soundscape?.ambienceVolume) || profile.soundscape.ambienceVolume <= 0 || profile.soundscape.ambienceVolume > 0.16 ) { errors.push(`${battleId}: ambience volume must stay within 0..0.16`); } if ((profile.particles?.kind ?? null) !== expected.particleKind) { errors.push(`${battleId}: expected particle kind ${expected.particleKind}, got ${profile.particles?.kind ?? null}`); } if ((profile.haze?.kind ?? null) !== expected.hazeKind) { errors.push(`${battleId}: expected haze kind ${expected.hazeKind}, got ${profile.haze?.kind ?? null}`); } if (profileIds.has(profile.id)) { errors.push(`${battleId}: duplicate profile id ${profile.id}`); } profileIds.add(profile.id); if (!Number.isInteger(profile.particles?.count ?? 0) || (profile.particles?.count ?? 0) > 40) { errors.push(`${battleId}: particle fixed-pool budget must be an integer at or below 40`); } if (!Number.isInteger(profile.haze?.count ?? 0) || (profile.haze?.count ?? 0) > 4) { errors.push(`${battleId}: haze fixed-pool budget must be an integer at or below 4`); } if ((profile.particles?.count ?? 0) + (profile.haze?.count ?? 0) + 1 > 45) { errors.push(`${battleId}: total environment object budget exceeds 45`); } if (profile.particles && (!Array.isArray(profile.particles.colors) || profile.particles.colors.length === 0)) { errors.push(`${battleId}: particle colors are required`); } }); if ( battleEnvironmentProfileVerification?.valid !== true || battleEnvironmentProfileVerification?.profileCount !== expectedBattleIds.length || battleEnvironmentProfileVerification?.representativeBattleCount !== expectedBattleIds.length || battleEnvironmentProfileVerification?.maximumParticleCount > 40 || battleEnvironmentProfileVerification?.maximumHazeCount > 4 ) { errors.push(`invalid profile verification summary: ${JSON.stringify(battleEnvironmentProfileVerification)}`); } const battleSceneSource = readFileSync('src/game/scenes/BattleScene.ts', 'utf8'); [ 'battleEnvironmentProfileFor(battleScenario.id)', 'this.drawBattleEnvironment();', 'this.updateBattleEnvironment(delta);', 'this.clearBattleEnvironment();', 'ambienceKey: profile?.soundscape.ambienceKey', 'environment: this.battleEnvironmentDebugState()' ].forEach((requiredSource) => { if (!battleSceneSource.includes(requiredSource)) { errors.push(`BattleScene environment integration is missing: ${requiredSource}`); } }); if (errors.length > 0) { console.error(`Battle environment profile verification failed with ${errors.length} issue(s):`); errors.forEach((error) => console.error(`- ${error}`)); process.exitCode = 1; } else { console.log( `Verified ${expectedBattleIds.length} battle environment profiles, fixed-pool budgets, lookup API, and BattleScene lifecycle/debug integration.` ); } } finally { await server.close(); }