import { readFileSync } from 'node:fs'; import { createServer } from 'vite'; const server = await createServer({ logLevel: 'error', server: { middlewareMode: true, hmr: false }, appType: 'custom' }); function assert(condition, message) { if (!condition) { throw new Error(message); } } function createStorage() { const values = new Map(); return { values, storage: { getItem: (key) => values.get(key) ?? null, setItem: (key, value) => values.set(key, value) } }; } const fullMotionPreference = () => ({ matches: false }); const reducedMotionPreference = () => ({ matches: true }); try { const motion = await server.ssrLoadModule('/src/game/settings/visualMotion.ts'); assert( motion.preferredVisualMotionMode(fullMotionPreference) === 'full', 'The OS default must use full motion when reduced motion is not requested.' ); assert( motion.preferredVisualMotionMode(reducedMotionPreference) === 'reduced', 'The OS reduced-motion preference must become the default mode.' ); assert( motion.preferredVisualMotionMode(() => { throw new Error('unavailable'); }) === 'full', 'An unavailable media query must safely fall back to full motion.' ); const { values, storage } = createStorage(); assert( motion.loadVisualMotionMode(storage, fullMotionPreference) === 'full', 'A fresh full-motion environment must load full mode.' ); assert( motion.loadVisualMotionMode(storage, reducedMotionPreference) === 'reduced', 'A fresh reduced-motion environment must load reduced mode.' ); for (const mode of motion.visualMotionModes) { motion.saveVisualMotionMode(mode, storage); assert( motion.loadVisualMotionMode(storage, reducedMotionPreference) === mode, `Stored mode ${mode} must override the OS default.` ); } values.set(motion.visualMotionStorageKey, 'invalid'); assert( motion.loadVisualMotionMode(storage, fullMotionPreference) === 'full' && motion.loadVisualMotionMode(storage, reducedMotionPreference) === 'reduced', 'Invalid stored values must normalize to the current OS default.' ); assert( motion.normalizeVisualMotionMode('full', 'reduced') === 'full' && motion.normalizeVisualMotionMode('reduced', 'full') === 'reduced' && motion.normalizeVisualMotionMode('invalid', 'reduced') === 'reduced', 'Visual motion normalization must preserve valid values and replace invalid values.' ); assert( motion.nextVisualMotionMode('full') === 'reduced' && motion.nextVisualMotionMode('reduced') === 'full', 'The two visual motion modes must cycle in display order.' ); assert( motion.visualMotionModeLabel('full') === '보통' && motion.visualMotionModeLabel('reduced') === '줄임', 'Visual motion labels must match the title settings copy.' ); motion.saveVisualMotionMode('reduced', storage); assert( motion.isVisualMotionReduced(storage, fullMotionPreference), 'The shared reduced-motion predicate must honor the stored reduced mode.' ); motion.saveVisualMotionMode('full', storage); assert( !motion.isVisualMotionReduced(storage, reducedMotionPreference), 'The shared reduced-motion predicate must let a stored full mode override the OS preference.' ); const throwingStorage = { getItem: () => { throw new Error('blocked'); }, setItem: () => { throw new Error('blocked'); } }; assert( motion.loadVisualMotionMode(throwingStorage, reducedMotionPreference) === 'reduced', 'Storage read failures must fall back to the OS preference.' ); motion.saveVisualMotionMode('full', throwingStorage); const titleSource = readFileSync('src/game/scenes/TitleScene.ts', 'utf8'); assert( titleSource.includes("from '../settings/visualMotion'") && titleSource.includes('화면 움직임:') && titleSource.includes('nextVisualMotionMode(loadVisualMotionMode())') && titleSource.includes('ui(468)'), 'The title settings panel must expose the persisted motion toggle in its expanded layout.' ); const battleSource = readFileSync('src/game/scenes/BattleScene.ts', 'utf8'); assert( battleSource.includes("import { isVisualMotionReduced } from '../settings/visualMotion';"), 'BattleScene must use the shared visual motion preference.' ); assert( !battleSource.includes("matchMedia('(prefers-reduced-motion: reduce)')"), 'BattleScene must not keep direct reduced-motion media queries.' ); assert( battleSource.includes('if (!reducedMotion && profile.haze)') && battleSource.includes('if (!reducedMotion && profile.particles)'), 'Reduced mode must disable moving battle haze and particles without suppressing tint creation.' ); assert( (battleSource.match(/this\.shakeCamera\(/g) ?? []).length === 3 && (battleSource.match(/this\.cameras\.main\.shake\(/g) ?? []).length === 1 && battleSource.includes('if (isVisualMotionReduced())'), 'Every battle camera shake must pass through the shared reduced-motion guard.' ); const storySource = readFileSync('src/game/scenes/StoryScene.ts', 'utf8'); assert( storySource.includes("import { isVisualMotionReduced } from '../settings/visualMotion';") && storySource.includes('this.visualMotionReduced = isVisualMotionReduced();'), 'StoryScene must load the shared visual motion preference when the scene starts.' ); assert( storySource.includes('const particleCount = this.visualMotionReduced ? 0') && storySource.includes('? Math.min(1, profile.haze.count)') && storySource.includes('if (this.visualMotionReduced) {\n return;\n }'), 'Reduced story motion must stop background panning and particles while retaining one static haze layer.' ); console.info('Verified visual motion defaults, persistence, title controls, and story/battle motion guards.'); } finally { await server.close(); }