import { describe, it, expect, beforeEach } from 'vitest'; import { useGameStore } from '../stores/gameStore'; import { useManaStore, makeInitialElements } from '../stores/manaStore'; import { useCombatStore } from '../stores/combatStore'; import { usePrestigeStore } from '../stores/prestigeStore'; import { useUIStore } from '../stores/uiStore'; import { useDisciplineStore } from '../stores/discipline-slice'; import { HOURS_PER_TICK, MAX_DAY, INCURSION_START_DAY } from '../constants'; import { getFloorMaxHP } from '../utils'; // ─── Helpers ────────────────────────────────────────────────────────────────── function resetAllStores() { useUIStore.setState({ paused: false, gameOver: false, victory: false, logs: [], }); useGameStore.setState({ day: 1, hour: 0, incursionStrength: 0, containmentWards: 0, initialized: true, }); useManaStore.setState({ rawMana: 100, meditateTicks: 0, totalManaGathered: 0, elements: makeInitialElements(50, {}), }); useCombatStore.setState({ currentFloor: 1, floorHP: getFloorMaxHP(1), floorMaxHP: getFloorMaxHP(1), maxFloorReached: 1, activeSpell: 'manaBolt', currentAction: 'meditate', castProgress: 0, spireMode: false, currentRoom: { roomType: 'combat', enemies: [] }, clearedFloors: {}, climbDirection: null, isDescending: false, golemancy: { enabledGolems: [], summonedGolems: [], activeGolems: [], lastSummonFloor: 0 }, equipmentSpellStates: [], comboHitCount: 0, floorHitCount: 0, spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } }, activityLog: [], achievements: { unlocked: [], progress: {} }, totalSpellsCast: 0, totalDamageDealt: 0, totalCraftsCompleted: 0, }); usePrestigeStore.setState({ loopCount: 0, insight: 0, totalInsight: 0, loopInsight: 0, prestigeUpgrades: {}, pactSlots: 1, defeatedGuardians: [], signedPacts: [], signedPactDetails: {}, pactRitualFloor: null, pactRitualProgress: 0, }); useDisciplineStore.setState({ disciplines: {}, activeIds: [], concurrentLimit: 1, totalXP: 0, }); } // ═══════════════════════════════════════════════════════════════════════════════ // TICK INTEGRATION TESTS // ═══════════════════════════════════════════════════════════════════════════════ describe('Tick Integration', () => { beforeEach(resetAllStores); describe('time progression', () => { it('should advance hour by HOURS_PER_TICK', () => { useGameStore.getState().tick(); expect(useGameStore.getState().hour).toBeCloseTo(HOURS_PER_TICK, 5); }); it('should advance day when hour wraps past 24', () => { // Set hour close to 24 useGameStore.setState({ hour: 23.99 }); useGameStore.getState().tick(); expect(useGameStore.getState().day).toBe(2); expect(useGameStore.getState().hour).toBeCloseTo(23.99 + HOURS_PER_TICK - 24, 5); }); it('should advance multiple hours over many ticks', () => { for (let i = 0; i < 100; i++) { useGameStore.getState().tick(); } const expectedHour = (100 * HOURS_PER_TICK) % 24; const expectedDay = 1 + Math.floor((100 * HOURS_PER_TICK) / 24); expect(useGameStore.getState().day).toBe(expectedDay); expect(useGameStore.getState().hour).toBeCloseTo(expectedHour, 5); }); }); describe('mana regeneration', () => { it('should increase raw mana on tick (base regen)', () => { useManaStore.setState({ rawMana: 50 }); useGameStore.getState().tick(); expect(useManaStore.getState().rawMana).toBeGreaterThan(50); }); it('should cap raw mana at max', () => { useManaStore.setState({ rawMana: 9999 }); useGameStore.getState().tick(); // Max mana with no skills/upgrades is 100 expect(useManaStore.getState().rawMana).toBeLessThanOrEqual(100); }); it('should increase totalManaGathered from meditation regen', () => { // Passive meditation regen should count toward totalManaGathered useManaStore.setState({ rawMana: 50, totalManaGathered: 5 }); useGameStore.getState().tick(); expect(useManaStore.getState().totalManaGathered).toBeGreaterThan(5); }); it('should not count regen toward totalManaGathered when at cap', () => { // Regen that would exceed max mana should not count toward totalManaGathered useManaStore.setState({ rawMana: 100, totalManaGathered: 50 }); useGameStore.getState().tick(); // When rawMana equals or exceeds maxMana, no regen is added to totalManaGathered // Use toBeCloseTo with tolerance for floating point drift from base regen calculations expect(useManaStore.getState().totalManaGathered).toBeLessThanOrEqual(50.05); }); }); describe('incursion penalty', () => { it('should have no incursion before INCURSION_START_DAY', () => { useGameStore.setState({ day: INCURSION_START_DAY - 1, hour: 23 }); useGameStore.getState().tick(); expect(useGameStore.getState().incursionStrength).toBe(0); }); it('should apply incursion after INCURSION_START_DAY', () => { useGameStore.setState({ day: INCURSION_START_DAY, hour: 1 }); useGameStore.getState().tick(); expect(useGameStore.getState().incursionStrength).toBeGreaterThan(0); }); it('should reduce mana regen during incursion', () => { // No incursion: day 1 resetAllStores(); useGameStore.setState({ day: 1, hour: 0 }); useManaStore.setState({ rawMana: 50 }); useGameStore.getState().tick(); const regenNoIncursion = useManaStore.getState().rawMana - 50; // With incursion: day 25 resetAllStores(); useGameStore.setState({ day: 25, hour: 12 }); useManaStore.setState({ rawMana: 50 }); useGameStore.getState().tick(); const regenWithIncursion = useManaStore.getState().rawMana - 50; expect(regenWithIncursion).toBeLessThan(regenNoIncursion); }); }); describe('meditation', () => { it('should increment meditateTicks when action is meditate', () => { useCombatStore.setState({ currentAction: 'meditate' }); useGameStore.getState().tick(); expect(useManaStore.getState().meditateTicks).toBe(1); }); it('should reset meditateTicks when action changes', () => { useCombatStore.setState({ currentAction: 'meditate' }); useGameStore.getState().tick(); useGameStore.getState().tick(); expect(useManaStore.getState().meditateTicks).toBe(2); useCombatStore.setState({ currentAction: 'climb' }); useGameStore.getState().tick(); expect(useManaStore.getState().meditateTicks).toBe(0); }); it('should boost regen with meditation', () => { // Without meditation resetAllStores(); useCombatStore.setState({ currentAction: 'climb' }); useManaStore.setState({ rawMana: 50, meditateTicks: 100 }); useGameStore.getState().tick(); const regenNoMeditate = useManaStore.getState().rawMana - 50; // With meditation (same ticks) resetAllStores(); useCombatStore.setState({ currentAction: 'meditate' }); useManaStore.setState({ rawMana: 50, meditateTicks: 100 }); useGameStore.getState().tick(); const regenMeditate = useManaStore.getState().rawMana - 50; expect(regenMeditate).toBeGreaterThan(regenNoMeditate); }); }); describe('paused / game over', () => { it('should not advance time when paused', () => { useUIStore.setState({ paused: true }); const before = useGameStore.getState().hour; useGameStore.getState().tick(); expect(useGameStore.getState().hour).toBe(before); }); it('should not advance time when game over', () => { useUIStore.setState({ gameOver: true }); const before = useGameStore.getState().hour; useGameStore.getState().tick(); expect(useGameStore.getState().hour).toBe(before); }); it('should not regenerate mana when paused', () => { useUIStore.setState({ paused: true }); useManaStore.setState({ rawMana: 50 }); useGameStore.getState().tick(); expect(useManaStore.getState().rawMana).toBe(50); }); }); describe('loop end', () => { it('should trigger game over when day > MAX_DAY', () => { useGameStore.setState({ day: MAX_DAY, hour: 23.99 }); useGameStore.getState().tick(); expect(useUIStore.getState().gameOver).toBe(true); }); it('should set loopInsight when loop ends', () => { useGameStore.setState({ day: MAX_DAY, hour: 23.99 }); useGameStore.getState().tick(); expect(usePrestigeStore.getState().loopInsight).toBeGreaterThan(0); }); it('should not be victory when loop ends normally', () => { useGameStore.setState({ day: MAX_DAY, hour: 23.99 }); useGameStore.getState().tick(); expect(useUIStore.getState().victory).toBe(false); }); }); describe('victory condition', () => { it('should trigger victory when floor 100 reached with signed pact', () => { useCombatStore.setState({ maxFloorReached: 100 }); usePrestigeStore.setState({ signedPacts: [100] }); useGameStore.getState().tick(); expect(useUIStore.getState().gameOver).toBe(true); expect(useUIStore.getState().victory).toBe(true); }); it('should not trigger victory without signed pact', () => { useCombatStore.setState({ maxFloorReached: 100 }); usePrestigeStore.setState({ signedPacts: [] }); useGameStore.getState().tick(); expect(useUIStore.getState().victory).toBe(false); }); it('should not trigger victory without floor 100', () => { useCombatStore.setState({ maxFloorReached: 99 }); usePrestigeStore.setState({ signedPacts: [100] }); useGameStore.getState().tick(); expect(useUIStore.getState().victory).toBe(false); }); }); describe('pact ritual progress', () => { it('should advance pact ritual progress on tick', () => { usePrestigeStore.setState({ pactRitualFloor: 10, pactRitualProgress: 0 }); useGameStore.getState().tick(); expect(usePrestigeStore.getState().pactRitualProgress).toBeGreaterThan(0); }); it('should not advance pact ritual when not active', () => { usePrestigeStore.setState({ pactRitualFloor: null }); useGameStore.getState().tick(); expect(usePrestigeStore.getState().pactRitualProgress).toBe(0); }); }); describe('multiple ticks', () => { it('should accumulate mana over multiple ticks', () => { useManaStore.setState({ rawMana: 10 }); for (let i = 0; i < 50; i++) { useGameStore.getState().tick(); } expect(useManaStore.getState().rawMana).toBeGreaterThan(10); }); it('should advance time correctly over many ticks', () => { const numTicks = 625; // 625 * 0.04 = 25 hours = 1 day + 1 hour for (let i = 0; i < numTicks; i++) { useGameStore.getState().tick(); } expect(useGameStore.getState().day).toBe(2); expect(useGameStore.getState().hour).toBeCloseTo(1, 5); }); it('should accumulate totalManaGathered from meditation over ticks', () => { useManaStore.setState({ rawMana: 10, totalManaGathered: 42 }); for (let i = 0; i < 10; i++) { useGameStore.getState().tick(); } // Passive meditation regen should now count toward totalManaGathered expect(useManaStore.getState().totalManaGathered).toBeGreaterThan(42); }); }); describe('incursion strength progression', () => { it('should be near 0 at start of incursion day', () => { // At INCURSION_START_DAY hour 0, incursion is 0, but tick advances hour first // so after tick, hour=0.04 and incursion is very small but > 0 useGameStore.setState({ day: INCURSION_START_DAY, hour: 0 }); useGameStore.getState().tick(); // After tick, hour advanced by HOURS_PER_TICK, so incursion is small expect(useGameStore.getState().incursionStrength).toBeGreaterThanOrEqual(0); expect(useGameStore.getState().incursionStrength).toBeLessThan(0.01); }); it('should increase over the course of the incursion', () => { // Early incursion resetAllStores(); useGameStore.setState({ day: INCURSION_START_DAY, hour: 1 }); useGameStore.getState().tick(); const early = useGameStore.getState().incursionStrength; // Late incursion resetAllStores(); useGameStore.setState({ day: MAX_DAY, hour: 23 }); useGameStore.getState().tick(); const late = useGameStore.getState().incursionStrength; expect(late).toBeGreaterThan(early); }); it('should cap at 0.95', () => { useGameStore.setState({ day: MAX_DAY, hour: 23.99 }); useGameStore.getState().tick(); expect(useGameStore.getState().incursionStrength).toBeLessThanOrEqual(0.95); }); }); });