// ─── Game Store (Coordinator) ───────────────────────────────────────────────── // Manages: day, hour, incursionStrength, containmentWards // Coordinates 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 { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '../upgrade-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 { useSkillStore } from './skillStore'; import { useCombatStore, makeInitialSpells } from './combatStore'; 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; } 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 skillState = useSkillStore.getState(); const combatState = useCombatStore.getState(); // Compute effects from upgrades const effects = computeEffects(skillState.skillUpgrades || {}, skillState.skillTiers || {}); const maxMana = computeMaxMana( { skills: skillState.skills, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: skillState.skillUpgrades, skillTiers: skillState.skillTiers }, effects ); const baseRegen = computeRegen( { skills: skillState.skills, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: skillState.skillUpgrades, skillTiers: skillState.skillTiers }, effects ); // 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: skillState.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: skillState.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, skillState.skills, effects.meditationEfficiency); } else { meditateTicks = 0; } // Calculate effective regen with incursion and meditation const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier; // Mana regeneration let rawMana = Math.min(manaState.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana); let totalManaGathered = manaState.totalManaGathered; let elements = { ...manaState.elements }; // Study progress - handled by skillStore if (combatState.currentAction === 'study' && skillState.currentStudyTarget) { const studySpeedMult = getStudySpeedMultiplier(skillState.skills); const progressGain = HOURS_PER_TICK * studySpeedMult; const result = useSkillStore.getState().updateStudyProgress(progressGain); if (result.completed && result.target) { if (result.target.type === 'skill') { const skillId = result.target.id; const currentLevel = skillState.skills[skillId] || 0; // Update skill level useSkillStore.getState().incrementSkillLevel(skillId); useSkillStore.getState().clearPaidStudySkill(skillId); useCombatStore.getState().setAction('meditate'); addLog(`✅ ${skillId} Lv.${currentLevel + 1} mastered!`); } else if (result.target.type === 'spell') { const spellId = result.target.id; useCombatStore.getState().learnSpell(spellId); useSkillStore.getState().setCurrentStudyTarget(null); useCombatStore.getState().setAction('meditate'); addLog(`📖 ${SPELLS_DEF[spellId]?.name || spellId} learned!`); } } } // 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( skillState.skills, rawMana, elements, maxMana, effects.attackSpeedMultiplier, (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 * effects.baseDamageMultiplier + effects.baseDamageBonus; // Executioner: +100% damage to enemies below 25% HP if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && combatState.floorHP / combatState.floorMaxHP < 0.25) { dmg *= 2; } // Berserker: +50% damage when below 50% mana if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) { dmg *= 1.5; } // Spell echo - chance to cast again const echoChance = (skillState.skills.spellEcho || 0) * 0.1; if (Math.random() < echoChance) { dmg *= 2; addLog(`✨ Spell Echo! Double damage!`); } 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: () => { // Clear all persisted state localStorage.removeItem('mana-loop-ui-storage'); localStorage.removeItem('mana-loop-prestige-storage'); localStorage.removeItem('mana-loop-mana-storage'); localStorage.removeItem('mana-loop-skill-storage'); localStorage.removeItem('mana-loop-combat-storage'); localStorage.removeItem('mana-loop-game-storage'); const startFloor = 1; useUIStore.getState().resetUI(); usePrestigeStore.getState().resetPrestige(); useManaStore.getState().resetMana({}, {}, {}, {}); useSkillStore.getState().resetSkills(); useCombatStore.getState().resetCombat(startFloor); set({ ...initialState, initialized: true, }); }, togglePause: () => { useUIStore.getState().togglePause(); }, gatherMana: () => { const skillState = useSkillStore.getState(); const manaState = useManaStore.getState(); const prestigeState = usePrestigeStore.getState(); // Compute click mana let cm = 1 + (skillState.skills.manaTap || 0) * 1 + (skillState.skills.manaSurge || 0) * 3; // Mana overflow bonus const overflowBonus = 1 + (skillState.skills.manaOverflow || 0) * 0.25; cm = Math.floor(cm * overflowBonus); const effects = computeEffects(skillState.skillUpgrades || {}, skillState.skillTiers || {}); const max = computeMaxMana( { skills: skillState.skills, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: skillState.skillUpgrades, skillTiers: skillState.skillTiers }, effects ); useManaStore.getState().gatherMana(cm, max); }, startNewLoop: () => { const prestigeState = usePrestigeStore.getState(); const combatState = useCombatStore.getState(); const manaState = useManaStore.getState(); const skillState = useSkillStore.getState(); const insightGained = prestigeState.loopInsight || calcInsight({ maxFloorReached: combatState.maxFloorReached, totalManaGathered: manaState.totalManaGathered, signedPacts: prestigeState.signedPacts, prestigeUpgrades: prestigeState.prestigeUpgrades, skills: skillState.skills, }); const total = prestigeState.insight + insightGained; // Spell preservation is only through prestige upgrade "spellMemory" (purchased with insight) // Not through a skill - that would undermine the insight economy const pu = prestigeState.prestigeUpgrades; const startFloor = 1 + (pu.spireKey || 0) * 2; // Apply saved memories - restore skill levels, tiers, and upgrades const memories = prestigeState.memories || []; const newSkills: Record = {}; const newSkillTiers: Record = {}; const newSkillUpgrades: Record = {}; if (memories.length > 0) { for (const memory of memories) { const tieredSkillId = memory.tier > 1 ? `${memory.skillId}_t${memory.tier}` : memory.skillId; newSkills[tieredSkillId] = memory.level; if (memory.tier > 1) { newSkillTiers[memory.skillId] = memory.tier; } newSkillUpgrades[tieredSkillId] = memory.upgrades || []; } } // Reset and update all stores for new loop useUIStore.setState({ gameOver: false, victory: false, paused: false, logs: ['✨ The loop begins. You start with Mana Bolt. Gather your strength, mage.'], }); usePrestigeStore.getState().resetPrestigeForNewLoop( total, pu, prestigeState.memories, 3 + (pu.deepMemory || 0) ); usePrestigeStore.getState().incrementLoopCount(); useManaStore.getState().resetMana(pu, newSkills, newSkillUpgrades, newSkillTiers); useSkillStore.getState().resetSkills(newSkills, newSkillUpgrades, newSkillTiers); // Reset combat with starting floor and any spells from prestige upgrades const startSpells = makeInitialSpells(); if (pu.spellMemory) { const availableSpells = Object.keys(SPELLS_DEF).filter(s => s !== 'manaBolt'); const shuffled = availableSpells.sort(() => Math.random() - 0.5); for (let i = 0; i < Math.min(pu.spellMemory, shuffled.length); i++) { startSpells[shuffled[i]] = { learned: true, level: 1, studyProgress: 0 }; } } useCombatStore.setState({ currentFloor: startFloor, floorHP: getFloorMaxHP(startFloor), floorMaxHP: getFloorMaxHP(startFloor), maxFloorReached: startFloor, activeSpell: 'manaBolt', currentAction: 'meditate', castProgress: 0, spells: startSpells, }); set({ day: 1, hour: 0, incursionStrength: 0, containmentWards: 0, }); }, }), { name: 'mana-loop-game-storage', partialize: (state) => ({ day: state.day, hour: state.hour, incursionStrength: state.incursionStrength, containmentWards: state.containmentWards, }), } ) ); // Re-export the game loop hook for convenience export function useGameLoop() { const tick = useGameStore((s) => s.tick); return { start: () => { const interval = setInterval(tick, TICK_MS); return () => clearInterval(interval); }, }; }