// ─── Game Store (Coordinator) ───────────────────────────────────────────────── // Manages: day, hour, incursionStrength, containmentWards // Coordinate tick function across all stores import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { TICK_MS, HOURS_PER_TICK, MAX_DAY, SPELLS_DEF, GUARDIANS, getStudySpeedMultiplier } from '../constants'; import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects'; import { computeMaxMana, computeRegen, getFloorElement, getFloorMaxHP, getMeditationBonus, getIncursionStrength, calcInsight, calcDamage, deductSpellCost, canAffordSpellCost, } from '../utils'; import { useUIStore } from './uiStore'; import { usePrestigeStore } from './prestigeStore'; import { useManaStore } from './manaStore'; import { useCombatStore, makeInitialSpells } from './combatStore'; import { useAttunementStore } from './attunementStore'; import { ATTUNEMENTS_DEF, getAttunementConversionRate } from '../data/attunements'; import { createResetGame, createGatherMana } from './gameActions'; import { createStartNewLoop } from './gameLoopActions'; export interface GameCoordinatorState { day: number; hour: number; incursionStrength: number; containmentWards: number; initialized: boolean; } export interface GameCoordinatorStore extends GameCoordinatorState { tick: () => void; resetGame: () => void; togglePause: () => void; startNewLoop: () => void; initGame: () => void; gatherMana: () => void; } const initialState: GameCoordinatorState = { day: 1, hour: 0, incursionStrength: 0, containmentWards: 0, initialized: false, }; export const useGameStore = create()( persist( (set, get) => ({ ...initialState, initGame: () => { set({ initialized: true }); }, tick: () => { const uiState = useUIStore.getState(); if (uiState.gameOver || uiState.paused) return; // Helper for logging const addLog = (msg: string) => useUIStore.getState().addLog(msg); // Get all store states const prestigeState = usePrestigeStore.getState(); const manaState = useManaStore.getState(); const combatState = useCombatStore.getState(); const maxMana = computeMaxMana( { skills: {}, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: {}, skillTiers: {} }, undefined ); const baseRegen = computeRegen( { skills: {}, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, attunement: {} }, undefined ); // Time progression let hour = get().hour + HOURS_PER_TICK; let day = get().day; if (hour >= 24) { hour -= 24; day += 1; } // Check for loop end if (day > MAX_DAY) { const insightGained = calcInsight({ maxFloorReached: combatState.maxFloorReached, totalManaGathered: manaState.totalManaGathered, signedPacts: prestigeState.signedPacts, prestigeUpgrades: prestigeState.prestigeUpgrades, skills: {}, }); addLog(`⏰ The loop ends. Gained ${insightGained} Insight.`); useUIStore.getState().setGameOver(true, false); usePrestigeStore.getState().setLoopInsight(insightGained); set({ day, hour }); return; } // Check for victory if (combatState.maxFloorReached >= 100 && prestigeState.signedPacts.includes(100)) { const insightGained = calcInsight({ maxFloorReached: combatState.maxFloorReached, totalManaGathered: manaState.totalManaGathered, signedPacts: prestigeState.signedPacts, prestigeUpgrades: prestigeState.prestigeUpgrades, skills: {}, }) * 3; addLog(`🏆 VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`); useUIStore.getState().setGameOver(true, true); usePrestigeStore.getState().setLoopInsight(insightGained); return; } // Incursion const incursionStrength = getIncursionStrength(day, hour); // Meditation bonus tracking and regen calculation let meditateTicks = manaState.meditateTicks; let meditationMultiplier = 1; if (combatState.currentAction === 'meditate') { meditateTicks++; meditationMultiplier = getMeditationBonus(meditateTicks, {}, 1); } else { meditateTicks = 0; } // Calculate total attunement conversion per tick (to subtract from regen) const attunementState = useAttunementStore.getState(); let totalConversionPerTick = 0; Object.entries(attunementState.attunements).forEach(([id, state]) => { if (!state.active) return; const def = ATTUNEMENTS_DEF[id]; if (!def || def.conversionRate <= 0 || !def.primaryManaType) return; const scaledRate = getAttunementConversionRate(id, state.level || 1); totalConversionPerTick += scaledRate * HOURS_PER_TICK; }); // Calculate effective regen with incursion, meditation, and attunement conversion const effectiveRegen = Math.max(0, baseRegen * (1 - incursionStrength) * meditationMultiplier - totalConversionPerTick); // Mana regeneration (now includes attunement conversion deduction) let rawMana = Math.min(manaState.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana); let elements = { ...manaState.elements }; // Apply attunement conversion (add to primary mana types) Object.entries(attunementState.attunements).forEach(([id, state]) => { if (!state.active) return; const def = ATTUNEMENTS_DEF[id]; if (!def || def.conversionRate <= 0 || !def.primaryManaType) return; const scaledRate = getAttunementConversionRate(id, state.level || 1); const conversionThisTick = scaledRate * HOURS_PER_TICK; // Add to primary mana type (cost already deducted from regen) if (elements[def.primaryManaType]) { elements[def.primaryManaType].current = Math.min( elements[def.primaryManaType].max, elements[def.primaryManaType].current + conversionThisTick ); } }); let totalManaGathered = manaState.totalManaGathered; // Convert action - delegate to manaStore if (combatState.currentAction === 'convert') { const convertResult = useManaStore.getState().processConvertAction(rawMana); if (convertResult) { rawMana = convertResult.rawMana; elements = convertResult.elements; } } // Pact ritual progress if (prestigeState.pactRitualFloor !== null) { const guardian = GUARDIANS[prestigeState.pactRitualFloor]; if (guardian) { const pactAffinityBonus = 1 - (prestigeState.prestigeUpgrades.pactAffinity || 0) * 0.1; const requiredTime = guardian.pactTime * pactAffinityBonus; const newProgress = prestigeState.pactRitualProgress + HOURS_PER_TICK; if (newProgress >= requiredTime) { addLog(`📜 Pact signed with ${guardian.name}! You have gained their boons.`); usePrestigeStore.getState().addSignedPact(prestigeState.pactRitualFloor); usePrestigeStore.getState().removeDefeatedGuardian(prestigeState.pactRitualFloor); usePrestigeStore.getState().setPactRitualFloor(null); } else { usePrestigeStore.getState().updatePactRitualProgress(newProgress); } } } // Combat - delegate to combatStore if (combatState.currentAction === 'climb') { const combatResult = useCombatStore.getState().processCombatTick( {}, rawMana, elements, maxMana, 1, (floor, wasGuardian) => { if (wasGuardian) { addLog(`⚔️ ${GUARDIANS[floor]?.name || 'Guardian'} defeated! Visit the Grimoire to sign a pact.`); } else if (floor % 5 === 0) { addLog(`🏰 Floor ${floor} cleared!`); } }, (damage) => { // Apply upgrade damage multipliers and bonuses let dmg = damage; // Executioner: +100% damage to enemies below 25% HP if (hasSpecial({}, SPECIAL_EFFECTS.EXECUTIONER) && combatState.floorHP / combatState.floorMaxHP < 0.25) { dmg *= 2; } // Berserker: +50% damage when below 50% mana if (hasSpecial({}, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) { dmg *= 1.5; } return { rawMana, elements, modifiedDamage: dmg }; } ); rawMana = combatResult.rawMana; elements = combatResult.elements; totalManaGathered += combatResult.totalManaGathered || 0; // Log any messages from combat if (combatResult.logMessages) { combatResult.logMessages.forEach(msg => addLog(msg)); } } // Update all stores with new state useManaStore.setState({ rawMana, meditateTicks, totalManaGathered, elements, }); set({ day, hour, incursionStrength, }); }, resetGame: createResetGame(set, initialState), togglePause: () => { useUIStore.getState().togglePause(); }, startNewLoop: createStartNewLoop(set), gatherMana: createGatherMana(), }), { name: 'mana-loop-game-storage', partialize: (state) => ({ day: state.day, hour: state.hour, incursionStrength: state.incursionStrength, containmentWards: state.containmentWards, }), } ) );