/** * Tests for the split store architecture * * Tests each store in isolation and integration between stores */ import { describe, it, expect, beforeEach } from 'vitest'; import { useManaStore, useSkillStore, usePrestigeStore, useCombatStore, useUIStore, fmt, fmtDec, computeMaxMana, computeElementMax, computeRegen, computeClickMana, calcDamage, calcInsight, getMeditationBonus, getIncursionStrength, canAffordSpellCost, } from './stores'; import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, PRESTIGE_DEF, getStudySpeedMultiplier, getStudyCostMultiplier, HOURS_PER_TICK, } from './constants'; import type { GameState } from './types'; // ─── Test Fixtures ─────────────────────────────────────────────────────────── // Reset all stores before each test beforeEach(() => { useManaStore.setState({ rawMana: 10, meditateTicks: 0, totalManaGathered: 0, elements: (() => { const elements: Record = {}; Object.keys(ELEMENTS).forEach((k) => { elements[k] = { current: 0, max: 10, unlocked: ['fire', 'water', 'air', 'earth'].includes(k) }; }); return elements; })(), }); useSkillStore.setState({ skills: {}, skillProgress: {}, skillUpgrades: {}, skillTiers: {}, paidStudySkills: {}, currentStudyTarget: null, parallelStudyTarget: null, }); usePrestigeStore.setState({ loopCount: 0, insight: 0, totalInsight: 0, loopInsight: 0, prestigeUpgrades: {}, memorySlots: 3, pactSlots: 1, memories: [], defeatedGuardians: [], signedPacts: [], pactRitualFloor: null, pactRitualProgress: 0, }); useCombatStore.setState({ currentFloor: 1, floorHP: 151, floorMaxHP: 151, maxFloorReached: 1, activeSpell: 'manaBolt', currentAction: 'meditate', castProgress: 0, spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } }, }); useUIStore.setState({ logs: [], paused: false, gameOver: false, victory: false, }); }); // ─── Mana Store Tests ───────────────────────────────────────────────────────── describe('ManaStore', () => { describe('initial state', () => { it('should have correct initial values', () => { const state = useManaStore.getState(); expect(state.rawMana).toBe(10); expect(state.meditateTicks).toBe(0); expect(state.totalManaGathered).toBe(0); }); it('should have base elements unlocked', () => { const state = useManaStore.getState(); expect(state.elements.fire.unlocked).toBe(true); expect(state.elements.water.unlocked).toBe(true); expect(state.elements.air.unlocked).toBe(true); expect(state.elements.earth.unlocked).toBe(true); expect(state.elements.light.unlocked).toBe(false); }); }); describe('raw mana operations', () => { it('should add raw mana', () => { useManaStore.getState().addRawMana(50, 100); expect(useManaStore.getState().rawMana).toBe(60); }); it('should cap at max mana', () => { useManaStore.getState().addRawMana(200, 100); expect(useManaStore.getState().rawMana).toBe(100); }); it('should spend raw mana', () => { const result = useManaStore.getState().spendRawMana(5); expect(result).toBe(true); expect(useManaStore.getState().rawMana).toBe(5); }); it('should fail to spend more than available', () => { const result = useManaStore.getState().spendRawMana(50); expect(result).toBe(false); expect(useManaStore.getState().rawMana).toBe(10); }); }); describe('element operations', () => { it('should convert raw mana to element', () => { useManaStore.getState().addRawMana(90, 1000); // Have 100 mana const result = useManaStore.getState().convertMana('fire', 1); expect(result).toBe(true); expect(useManaStore.getState().elements.fire.current).toBe(1); }); it('should unlock new element', () => { useManaStore.getState().addRawMana(490, 1000); // Have 500 mana const result = useManaStore.getState().unlockElement('light', 500); expect(result).toBe(true); expect(useManaStore.getState().elements.light.unlocked).toBe(true); }); }); }); // ─── Skill Store Tests ─────────────────────────────────────────────────────── describe('SkillStore', () => { describe('study skill', () => { it('should start studying a skill', () => { useManaStore.getState().addRawMana(90, 1000); // Have 100 mana const result = useSkillStore.getState().startStudyingSkill('manaWell', 100); expect(result.started).toBe(true); expect(result.cost).toBe(100); expect(useSkillStore.getState().currentStudyTarget).not.toBeNull(); expect(useSkillStore.getState().currentStudyTarget?.type).toBe('skill'); expect(useSkillStore.getState().currentStudyTarget?.id).toBe('manaWell'); }); it('should not start studying without enough mana', () => { const result = useSkillStore.getState().startStudyingSkill('manaWell', 50); expect(result.started).toBe(false); expect(result.cost).toBe(100); expect(useSkillStore.getState().currentStudyTarget).toBeNull(); }); it('should track paid study skills', () => { useManaStore.getState().addRawMana(90, 1000); useSkillStore.getState().startStudyingSkill('manaWell', 100); expect(useSkillStore.getState().paidStudySkills['manaWell']).toBe(0); }); it('should resume studying for free after payment', () => { useManaStore.getState().addRawMana(90, 1000); // First study attempt const result1 = useSkillStore.getState().startStudyingSkill('manaWell', 100); expect(result1.cost).toBe(100); // Cancel study (simulated) useSkillStore.getState().cancelStudy(0); // Resume should be free const result2 = useSkillStore.getState().startStudyingSkill('manaWell', 0); expect(result2.started).toBe(true); expect(result2.cost).toBe(0); }); }); describe('update study progress', () => { it('should update study progress', () => { useManaStore.getState().addRawMana(90, 1000); useSkillStore.getState().startStudyingSkill('manaWell', 100); const result = useSkillStore.getState().updateStudyProgress(1); expect(result.completed).toBe(false); expect(useSkillStore.getState().currentStudyTarget?.progress).toBe(1); }); it('should complete study when progress reaches required', () => { useManaStore.getState().addRawMana(90, 1000); useSkillStore.getState().startStudyingSkill('manaWell', 100); // manaWell requires 4 hours const result = useSkillStore.getState().updateStudyProgress(4); expect(result.completed).toBe(true); expect(result.target?.id).toBe('manaWell'); expect(useSkillStore.getState().currentStudyTarget).toBeNull(); }); it('should apply study speed multiplier', () => { useManaStore.getState().addRawMana(90, 1000); useSkillStore.getState().setSkillLevel('quickLearner', 5); // 50% faster useSkillStore.getState().startStudyingSkill('manaWell', 100); // The caller should calculate progress with speed multiplier const speedMult = getStudySpeedMultiplier(useSkillStore.getState().skills); const result = useSkillStore.getState().updateStudyProgress(3 * speedMult); // 3 * 1.5 = 4.5 expect(result.completed).toBe(true); }); }); describe('skill level operations', () => { it('should set skill level', () => { useSkillStore.getState().setSkillLevel('manaWell', 5); expect(useSkillStore.getState().skills['manaWell']).toBe(5); }); it('should increment skill level', () => { useSkillStore.getState().setSkillLevel('manaWell', 5); useSkillStore.getState().incrementSkillLevel('manaWell'); expect(useSkillStore.getState().skills['manaWell']).toBe(6); }); }); describe('prerequisites', () => { it('should not start studying without prerequisites', () => { useManaStore.getState().addRawMana(990, 1000); // manaOverflow requires manaWell 3 const result = useSkillStore.getState().startStudyingSkill('manaOverflow', 1000); expect(result.started).toBe(false); }); it('should start studying with prerequisites met', () => { useManaStore.getState().addRawMana(990, 1000); useSkillStore.getState().setSkillLevel('manaWell', 3); const result = useSkillStore.getState().startStudyingSkill('manaOverflow', 1000); expect(result.started).toBe(true); }); }); }); // ─── Prestige Store Tests ──────────────────────────────────────────────────── describe('PrestigeStore', () => { describe('prestige upgrades', () => { it('should purchase prestige upgrade', () => { usePrestigeStore.getState().startNewLoop(1000); // Add 1000 insight const result = usePrestigeStore.getState().doPrestige('manaWell'); expect(result).toBe(true); expect(usePrestigeStore.getState().prestigeUpgrades['manaWell']).toBe(1); expect(usePrestigeStore.getState().insight).toBe(500); // 1000 - 500 cost }); it('should not purchase without enough insight', () => { usePrestigeStore.getState().startNewLoop(100); const result = usePrestigeStore.getState().doPrestige('manaWell'); expect(result).toBe(false); }); }); describe('loop management', () => { it('should increment loop count', () => { usePrestigeStore.getState().incrementLoopCount(); expect(usePrestigeStore.getState().loopCount).toBe(1); usePrestigeStore.getState().incrementLoopCount(); expect(usePrestigeStore.getState().loopCount).toBe(2); }); it('should reset for new loop', () => { usePrestigeStore.getState().startNewLoop(1000); usePrestigeStore.getState().doPrestige('manaWell'); usePrestigeStore.getState().resetPrestigeForNewLoop( 500, // total insight { manaWell: 1 }, // prestige upgrades [], // memories 3 // memory slots ); expect(usePrestigeStore.getState().insight).toBe(500); expect(usePrestigeStore.getState().prestigeUpgrades['manaWell']).toBe(1); expect(usePrestigeStore.getState().defeatedGuardians).toEqual([]); expect(usePrestigeStore.getState().signedPacts).toEqual([]); }); }); describe('guardian pacts', () => { it('should add signed pact', () => { usePrestigeStore.getState().addSignedPact(10); expect(usePrestigeStore.getState().signedPacts).toContain(10); }); it('should add defeated guardian', () => { usePrestigeStore.getState().addDefeatedGuardian(10); expect(usePrestigeStore.getState().defeatedGuardians).toContain(10); }); it('should not add same guardian twice', () => { usePrestigeStore.getState().addDefeatedGuardian(10); usePrestigeStore.getState().addDefeatedGuardian(10); expect(usePrestigeStore.getState().defeatedGuardians.length).toBe(1); }); }); }); // ─── Combat Store Tests ─────────────────────────────────────────────────────── describe('CombatStore', () => { describe('floor operations', () => { it('should advance floor', () => { useCombatStore.getState().advanceFloor(); expect(useCombatStore.getState().currentFloor).toBe(2); expect(useCombatStore.getState().maxFloorReached).toBe(2); }); it('should cap at floor 100', () => { useCombatStore.setState({ currentFloor: 100 }); useCombatStore.getState().advanceFloor(); expect(useCombatStore.getState().currentFloor).toBe(100); }); }); describe('action management', () => { it('should set action', () => { useCombatStore.getState().setAction('climb'); expect(useCombatStore.getState().currentAction).toBe('climb'); useCombatStore.getState().setAction('study'); expect(useCombatStore.getState().currentAction).toBe('study'); }); }); describe('spell management', () => { it('should set active spell', () => { useCombatStore.getState().learnSpell('fireball'); useCombatStore.getState().setSpell('fireball'); expect(useCombatStore.getState().activeSpell).toBe('fireball'); }); it('should learn spell', () => { useCombatStore.getState().learnSpell('fireball'); expect(useCombatStore.getState().spells['fireball']?.learned).toBe(true); }); }); }); // ─── UI Store Tests ─────────────────────────────────────────────────────────── describe('UIStore', () => { describe('log management', () => { it('should add log message', () => { useUIStore.getState().addLog('Test message'); expect(useUIStore.getState().logs).toContain('Test message'); }); it('should clear logs', () => { useUIStore.getState().addLog('Test message'); useUIStore.getState().clearLogs(); expect(useUIStore.getState().logs.length).toBe(0); }); }); describe('pause management', () => { it('should toggle pause', () => { expect(useUIStore.getState().paused).toBe(false); useUIStore.getState().togglePause(); expect(useUIStore.getState().paused).toBe(true); useUIStore.getState().togglePause(); expect(useUIStore.getState().paused).toBe(false); }); it('should set pause state', () => { useUIStore.getState().setPaused(true); expect(useUIStore.getState().paused).toBe(true); }); }); describe('game over state', () => { it('should set game over', () => { useUIStore.getState().setGameOver(true); expect(useUIStore.getState().gameOver).toBe(true); expect(useUIStore.getState().victory).toBe(false); }); it('should set victory', () => { useUIStore.getState().setGameOver(true, true); expect(useUIStore.getState().gameOver).toBe(true); expect(useUIStore.getState().victory).toBe(true); }); }); }); // ─── Integration Tests ───────────────────────────────────────────────────────── describe('Store Integration', () => { describe('skill study flow', () => { it('should complete full study flow', () => { // Setup: give enough mana useManaStore.getState().addRawMana(90, 1000); // Start studying const startResult = useSkillStore.getState().startStudyingSkill('manaWell', 100); expect(startResult.started).toBe(true); expect(startResult.cost).toBe(100); // Deduct mana (simulating UI behavior) if (startResult.cost > 0) { useManaStore.getState().spendRawMana(startResult.cost); } // Set action to study useCombatStore.getState().setAction('study'); expect(useCombatStore.getState().currentAction).toBe('study'); // Update progress until complete const result = useSkillStore.getState().updateStudyProgress(4); expect(result.completed).toBe(true); // Level up skill useSkillStore.getState().setSkillLevel('manaWell', 1); expect(useSkillStore.getState().skills['manaWell']).toBe(1); }); }); describe('mana and prestige interaction', () => { it('should apply prestige mana bonus', () => { // Get prestige upgrade usePrestigeStore.getState().startNewLoop(1000); usePrestigeStore.getState().doPrestige('manaWell'); // Check that prestige upgrade is recorded expect(usePrestigeStore.getState().prestigeUpgrades['manaWell']).toBe(1); // Mana well prestige gives +500 max mana per level const state = { skills: {}, prestigeUpgrades: usePrestigeStore.getState().prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, }; const maxMana = computeMaxMana(state); expect(maxMana).toBe(100 + 500); // Base 100 + 500 from prestige }); }); }); console.log('✅ All store tests defined. Run with: bun test src/lib/game/stores.test.ts');