// ─── Game Store ─────────────────────────────────────────────────────────────── import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import type { GameState, GameAction, StudyTarget, SpellCost, SkillUpgradeChoice, EquipmentSlot, EquipmentInstance, EnchantmentDesign, DesignEffect, AttunementState } from './types'; import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, PRESTIGE_DEF, FLOOR_ELEM_CYCLE, BASE_UNLOCKED_ELEMENTS, TICK_MS, HOURS_PER_TICK, MAX_DAY, INCURSION_START_DAY, MANA_PER_ELEMENT, getStudySpeedMultiplier, getStudyCostMultiplier, ELEMENT_OPPOSITES, EFFECT_RESEARCH_MAPPING, BASE_UNLOCKED_EFFECTS, ENCHANTING_UNLOCK_EFFECTS, } from './constants'; import { computeEffects, hasSpecial, SPECIAL_EFFECTS, type ComputedEffects } from './upgrade-effects'; import { computeAllEffects, getUnifiedEffects, computeEquipmentEffects, type UnifiedEffects } from './effects'; import { SKILL_EVOLUTION_PATHS } from './skill-evolution'; import { createStartingEquipment, processCraftingTick, getSpellsFromEquipment, type CraftingActions } from './crafting-slice'; import { EQUIPMENT_TYPES } from './data/equipment'; import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects'; import { ATTUNEMENTS_DEF, getTotalAttunementRegen, getAttunementConversionRate, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from './data/attunements'; // Default empty effects for when effects aren't provided const DEFAULT_EFFECTS: ComputedEffects = { maxManaMultiplier: 1, maxManaBonus: 0, regenMultiplier: 1, regenBonus: 0, clickManaMultiplier: 1, clickManaBonus: 0, meditationEfficiency: 1, spellCostMultiplier: 1, conversionEfficiency: 1, baseDamageMultiplier: 1, baseDamageBonus: 0, attackSpeedMultiplier: 1, critChanceBonus: 0, critDamageMultiplier: 1.5, elementalDamageMultiplier: 1, studySpeedMultiplier: 1, studyCostMultiplier: 1, progressRetention: 0, instantStudyChance: 0, freeStudyChance: 0, elementCapMultiplier: 1, elementCapBonus: 0, conversionCostMultiplier: 1, doubleCraftChance: 0, permanentRegenBonus: 0, specials: new Set(), activeUpgrades: [], }; // ─── Helper Functions ───────────────────────────────────────────────────────── export function fmt(n: number): string { if (!isFinite(n) || isNaN(n)) return '0'; if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B'; if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M'; if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K'; return Math.floor(n).toString(); } export function fmtDec(n: number, d: number = 1): string { return isFinite(n) ? n.toFixed(d) : '0'; } export function getFloorMaxHP(floor: number): number { if (GUARDIANS[floor]) return GUARDIANS[floor].hp; // Improved scaling: slower early game, faster late game const baseHP = 100; const floorScaling = floor * 50; const exponentialScaling = Math.pow(floor, 1.7); return Math.floor(baseHP + floorScaling + exponentialScaling); } export function getFloorElement(floor: number): string { return FLOOR_ELEM_CYCLE[(floor - 1) % 8]; } // ─── Computed Stats Functions ───────────────────────────────────────────────── // Helper to get effective skill level accounting for tiers function getEffectiveSkillLevel( skills: Record, baseSkillId: string, skillTiers: Record = {} ): { level: number; tier: number; tierMultiplier: number } { // Find the highest tier the player has for this base skill const currentTier = skillTiers[baseSkillId] || 1; // Look for the tiered skill ID (e.g., manaFlow_t2) const tieredSkillId = currentTier > 1 ? `${baseSkillId}_t${currentTier}` : baseSkillId; const level = skills[tieredSkillId] || skills[baseSkillId] || 0; // Tier multiplier: each tier is 10x more powerful const tierMultiplier = Math.pow(10, currentTier - 1); return { level, tier: currentTier, tierMultiplier }; } export function computeMaxMana( state: Pick, effects?: ComputedEffects | UnifiedEffects ): number { const pu = state.prestigeUpgrades; const base = 100 + (state.skills.manaWell || 0) * 100 + (pu.manaWell || 0) * 500; // If effects not provided, compute unified effects (includes equipment) if (!effects && state.equipmentInstances && state.equippedInstances) { effects = getUnifiedEffects(state as any); } // Apply effects if available (now includes equipment bonuses) if (effects) { return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier); } return base; } export function computeElementMax( state: Pick, effects?: ComputedEffects ): number { const pu = state.prestigeUpgrades; const base = 10 + (state.skills.elemAttune || 0) * 50 + (pu.elementalAttune || 0) * 25; // Apply upgrade effects if provided if (effects) { return Math.floor((base + effects.elementCapBonus) * effects.elementCapMultiplier); } return base; } export function computeRegen( state: Pick, effects?: ComputedEffects | UnifiedEffects ): number { const pu = state.prestigeUpgrades; const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1; const base = 2 + (state.skills.manaFlow || 0) * 1 + (state.skills.manaSpring || 0) * 2 + (pu.manaFlow || 0) * 0.5; let regen = base * temporalBonus; // Add attunement raw mana regen const attunementRegen = getTotalAttunementRegen(state.attunements || {}); regen += attunementRegen; // If effects not provided, compute unified effects (includes equipment) if (!effects && state.equipmentInstances && state.equippedInstances) { effects = getUnifiedEffects(state as any); } // Apply effects if available (now includes equipment bonuses) if (effects) { regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier; } return regen; } /** * Compute regen with dynamic special effects (needs current mana, max mana, incursion) */ export function computeEffectiveRegen( state: Pick, effects?: ComputedEffects ): number { // Base regen from existing function let regen = computeRegen(state, effects); const maxMana = computeMaxMana(state, effects); const currentMana = state.rawMana; const incursionStrength = state.incursionStrength || 0; // Apply incursion penalty regen *= (1 - incursionStrength); return regen; } export function computeClickMana( state: Pick, effects?: ComputedEffects | UnifiedEffects ): number { const base = 1 + (state.skills.manaTap || 0) * 1 + (state.skills.manaSurge || 0) * 3; // If effects not provided, compute unified effects (includes equipment) if (!effects && state.equipmentInstances && state.equippedInstances) { effects = getUnifiedEffects(state as any); } // Apply effects if available (now includes equipment bonuses) if (effects) { return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier); } return base; } // Elemental damage bonus: +50% if spell element opposes floor element (super effective) // -25% if spell element matches its own opposite (weak) function getElementalBonus(spellElem: string, floorElem: string): number { if (spellElem === 'raw') return 1.0; // Raw mana has no elemental bonus if (spellElem === floorElem) return 1.25; // Same element: +25% damage // Check for super effective first: spell is the opposite of floor // e.g., casting water (opposite of fire) at fire floor = super effective if (ELEMENT_OPPOSITES[floorElem] === spellElem) return 1.5; // Super effective: +50% damage // Check for weak: spell's opposite matches floor // e.g., casting fire (whose opposite is water) at water floor = weak if (ELEMENT_OPPOSITES[spellElem] === floorElem) return 0.75; // Weak: -25% damage return 1.0; // Neutral } export function calcDamage( state: Pick, spellId: string, floorElem?: string ): number { const sp = SPELLS_DEF[spellId]; if (!sp) return 5; const skills = state.skills; const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5; const pct = 1 + (skills.arcaneFury || 0) * 0.1; // Elemental mastery bonus const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15; // Guardian bane bonus const guardianBonus = floorElem && GUARDIANS[Object.values(GUARDIANS).find(g => g.element === floorElem)?.hp ? 0 : 0] ? 1 + (skills.guardianBane || 0) * 0.2 : 1; const critChance = (skills.precision || 0) * 0.05; const pactMult = state.signedPacts.reduce( (m, f) => m * (GUARDIANS[f]?.pact || 1), 1 ); let damage = baseDmg * pct * pactMult * elemMasteryBonus; // Apply elemental bonus if floor element provided if (floorElem) { damage *= getElementalBonus(sp.elem, floorElem); } // Apply crit if (Math.random() < critChance) { damage *= 1.5; } return damage; } export function calcInsight(state: Pick): number { const pu = state.prestigeUpgrades; const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1; const mult = (1 + (pu.insightAmp || 0) * 0.25) * skillBonus; return Math.floor( (state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150) * mult ); } // Meditation bonus now affects regen rate directly export function getMeditationBonus(meditateTicks: number, skills: Record, meditationEfficiency: number = 1): number { const hasMeditation = skills.meditation === 1; const hasDeepTrance = skills.deepTrance === 1; const hasVoidMeditation = skills.voidMeditation === 1; const hours = meditateTicks * HOURS_PER_TICK; // Base meditation: ramps up over 4 hours to 1.5x let bonus = 1 + Math.min(hours / 4, 0.5); // With Meditation Focus: up to 2.5x after 4 hours if (hasMeditation && hours >= 4) { bonus = 2.5; } // With Deep Trance: up to 3.0x after 6 hours if (hasDeepTrance && hours >= 6) { bonus = 3.0; } // With Void Meditation: up to 5.0x after 8 hours if (hasVoidMeditation && hours >= 8) { bonus = 5.0; } // Apply meditation efficiency from upgrades (Deep Wellspring, etc.) bonus *= meditationEfficiency; return bonus; } export function getIncursionStrength(day: number, hour: number): number { if (day < INCURSION_START_DAY) return 0; const totalHours = (day - INCURSION_START_DAY) * 24 + hour; const maxHours = (MAX_DAY - INCURSION_START_DAY) * 24; return Math.min(0.95, (totalHours / maxHours) * 0.95); } // Check if player can afford spell cost export function canAffordSpellCost( cost: SpellCost, rawMana: number, elements: Record ): boolean { if (cost.type === 'raw') { return rawMana >= cost.amount; } else { const elem = elements[cost.element || '']; return elem && elem.unlocked && elem.current >= cost.amount; } } // Deduct spell cost from appropriate mana pool function deductSpellCost( cost: SpellCost, rawMana: number, elements: Record ): { rawMana: number; elements: Record } { const newElements = { ...elements }; if (cost.type === 'raw') { return { rawMana: rawMana - cost.amount, elements: newElements }; } else if (cost.element && newElements[cost.element]) { newElements[cost.element] = { ...newElements[cost.element], current: newElements[cost.element].current - cost.amount }; return { rawMana, elements: newElements }; } return { rawMana, elements: newElements }; } // ─── Initial State Factory ──────────────────────────────────────────────────── function makeInitial(overrides: Partial = {}): GameState { const pu = overrides.prestigeUpgrades || {}; const startFloor = 1 + (pu.spireKey || 0) * 2; const elemMax = computeElementMax({ skills: overrides.skills || {}, prestigeUpgrades: pu }); const elements: Record = {}; Object.keys(ELEMENTS).forEach((k) => { const isUnlocked = BASE_UNLOCKED_ELEMENTS.includes(k); let startAmount = 0; // Start with some elemental mana if elemStart upgrade if (isUnlocked && pu.elemStart) { startAmount = pu.elemStart * 5; } elements[k] = { current: overrides.elements?.[k]?.current ?? startAmount, max: elemMax, unlocked: isUnlocked, }; }); // Starting raw mana const startRawMana = 10 + (pu.manaWell || 0) * 500 + (pu.quickStart || 0) * 100; // Create starting equipment (staff with mana bolt, clothes) const startingEquipment = createStartingEquipment(); // Get spells from starting equipment const equipmentSpells = getSpellsFromEquipment( startingEquipment.equipmentInstances, Object.values(startingEquipment.equippedInstances) ); // Starting spells - now come from equipment instead of being learned directly const startSpells: Record = {}; // Add spells from equipment for (const spellId of equipmentSpells) { startSpells[spellId] = { learned: true, level: 1, studyProgress: 0 }; } // Add random starting spells from spell memory upgrade (pact spells) if (pu.spellMemory) { const availableSpells = Object.keys(SPELLS_DEF).filter(s => !startSpells[s]); 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 }; } } // Starting attunements - player begins with Enchanter const startingAttunements: Record = { enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 }, }; // Add any attunements from previous loops (for persistence) if (overrides.attunements) { Object.entries(overrides.attunements).forEach(([id, state]) => { if (id !== 'enchanter') { startingAttunements[id] = state; } }); } // Unlock transference element for Enchanter attunement if (elements['transference']) { elements['transference'] = { ...elements['transference'], unlocked: true }; } return { day: 1, hour: 0, loopCount: overrides.loopCount || 0, gameOver: false, victory: false, paused: false, rawMana: startRawMana, meditateTicks: 0, totalManaGathered: overrides.totalManaGathered || 0, // Attunements (class-like system) attunements: startingAttunements, elements: elements as Record, currentFloor: startFloor, floorHP: getFloorMaxHP(startFloor), floorMaxHP: getFloorMaxHP(startFloor), maxFloorReached: startFloor, signedPacts: [], activeSpell: 'manaBolt', currentAction: 'meditate', castProgress: 0, combo: { count: 0, maxCombo: 0, multiplier: 1, elementChain: [], decayTimer: 0, }, spells: startSpells, skills: overrides.skills || {}, skillProgress: {}, skillUpgrades: overrides.skillUpgrades || {}, skillTiers: overrides.skillTiers || {}, parallelStudyTarget: null, // Achievements achievements: { unlocked: [], progress: {}, }, // Stats tracking totalSpellsCast: 0, totalDamageDealt: 0, totalCraftsCompleted: 0, // New equipment system equippedInstances: startingEquipment.equippedInstances, equipmentInstances: startingEquipment.equipmentInstances, enchantmentDesigns: [], designProgress: null, preparationProgress: null, applicationProgress: null, equipmentCraftingProgress: null, unlockedEffects: [...BASE_UNLOCKED_EFFECTS], // Start with mana bolt only equipmentSpellStates: [], // Legacy equipment (for backward compatibility) equipment: { mainHand: null, offHand: null, head: null, body: null, hands: null, accessory: null, }, inventory: [], blueprints: {}, // Loot inventory lootInventory: { materials: {}, blueprints: [], }, schedule: [], autoSchedule: false, studyQueue: [], craftQueue: [], currentStudyTarget: null, insight: overrides.insight || 0, totalInsight: overrides.totalInsight || 0, prestigeUpgrades: pu, memorySlots: 3 + (pu.deepMemory || 0), memories: overrides.memories || [], incursionStrength: 0, containmentWards: 0, log: ['✨ The loop begins. You start with a Basic Staff (Mana Bolt) and civilian clothes. Gather your strength, mage.'], loopInsight: 0, }; } // ─── Game Store ─────────────────────────────────────────────────────────────── interface GameStore extends GameState, CraftingActions { // Actions tick: () => void; gatherMana: () => void; setAction: (action: GameAction) => void; setSpell: (spellId: string) => void; startStudyingSkill: (skillId: string) => void; startStudyingSpell: (spellId: string) => void; startParallelStudySkill: (skillId: string) => void; cancelStudy: () => void; cancelParallelStudy: () => void; convertMana: (element: string, amount: number) => void; unlockElement: (element: string) => void; craftComposite: (target: string) => void; doPrestige: (id: string) => void; startNewLoop: () => void; togglePause: () => void; resetGame: () => void; addLog: (message: string) => void; selectSkillUpgrade: (skillId: string, upgradeId: string) => void; deselectSkillUpgrade: (skillId: string, upgradeId: string) => void; commitSkillUpgrades: (skillId: string, upgradeIds: string[]) => void; tierUpSkill: (skillId: string) => void; // Attunement XP and leveling addAttunementXP: (attunementId: string, amount: number) => void; // Debug functions debugUnlockAttunement: (attunementId: string) => void; debugAddElementalMana: (element: string, amount: number) => void; debugSetTime: (day: number, hour: number) => void; debugAddAttunementXP: (attunementId: string, amount: number) => void; debugSetFloor: (floor: number) => void; // Computed getters getMaxMana: () => number; getRegen: () => number; getClickMana: () => number; getDamage: (spellId: string) => number; getMeditationMultiplier: () => number; canCastSpell: (spellId: string) => boolean; getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => { available: SkillUpgradeChoice[]; selected: string[] }; } export const useGameStore = create()( persist( (set, get) => ({ ...makeInitial(), getMaxMana: () => computeMaxMana(get()), getRegen: () => computeRegen(get()), getClickMana: () => computeClickMana(get()), getDamage: (spellId: string) => calcDamage(get(), spellId), getMeditationMultiplier: () => getMeditationBonus(get().meditateTicks, get().skills), canCastSpell: (spellId: string) => { const state = get(); const spell = SPELLS_DEF[spellId]; if (!spell || !state.spells[spellId]?.learned) return false; return canAffordSpellCost(spell.cost, state.rawMana, state.elements); }, addLog: (message: string) => { set((state) => ({ log: [message, ...state.log.slice(0, 49)], })); }, tick: () => { const state = get(); if (state.gameOver || state.paused) return; // Compute unified effects (includes skill upgrades AND equipment enchantments) const effects = getUnifiedEffects(state); const maxMana = computeMaxMana(state, effects); const baseRegen = computeRegen(state, effects); // Time progression let hour = state.hour + HOURS_PER_TICK; let day = state.day; if (hour >= 24) { hour -= 24; day += 1; } // Check for loop end if (day > MAX_DAY) { const insightGained = calcInsight(state); set({ day, hour, gameOver: true, victory: false, loopInsight: insightGained, log: [`⏰ The loop ends. Gained ${insightGained} Insight.`, ...state.log.slice(0, 49)], }); return; } // Check for victory if (state.maxFloorReached >= 100 && state.signedPacts.includes(100)) { const insightGained = calcInsight(state) * 3; set({ gameOver: true, victory: true, loopInsight: insightGained, log: [`🏆 VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`, ...state.log.slice(0, 49)], }); return; } // Incursion const incursionStrength = getIncursionStrength(day, hour); // Meditation bonus tracking and regen calculation let meditateTicks = state.meditateTicks; let meditationMultiplier = 1; if (state.currentAction === 'meditate') { meditateTicks++; meditationMultiplier = getMeditationBonus(meditateTicks, state.skills); } else { meditateTicks = 0; } // Calculate effective regen with incursion and meditation const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier; // Mana regeneration let rawMana = Math.min(state.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana); let totalManaGathered = state.totalManaGathered; // Attunement mana conversion - convert raw mana to attunement's primary mana type let elements = state.elements; if (state.attunements) { Object.entries(state.attunements).forEach(([attId, attState]) => { if (!attState.active) return; const attDef = ATTUNEMENTS_DEF[attId]; if (!attDef || !attDef.primaryManaType || attDef.conversionRate <= 0) return; const elem = elements[attDef.primaryManaType]; if (!elem || !elem.unlocked) return; // Get level-scaled conversion rate const scaledConversionRate = getAttunementConversionRate(attId, attState.level || 1); // Convert raw mana to primary type const conversionAmount = scaledConversionRate * HOURS_PER_TICK; const actualConversion = Math.min(conversionAmount, rawMana, elem.max - elem.current); if (actualConversion > 0) { rawMana -= actualConversion; elements = { ...elements, [attDef.primaryManaType]: { ...elem, current: elem.current + actualConversion, }, }; } }); } // Study progress let currentStudyTarget = state.currentStudyTarget; let skills = state.skills; let skillProgress = state.skillProgress; let spells = state.spells; let log = state.log; let unlockedEffects = state.unlockedEffects; if (state.currentAction === 'study' && currentStudyTarget) { const studySpeedMult = getStudySpeedMultiplier(skills); const progressGain = HOURS_PER_TICK * studySpeedMult; currentStudyTarget = { ...currentStudyTarget, progress: currentStudyTarget.progress + progressGain, }; // Check if study is complete if (currentStudyTarget.progress >= currentStudyTarget.required) { if (currentStudyTarget.type === 'skill') { const skillId = currentStudyTarget.id; const currentLevel = skills[skillId] || 0; const newLevel = currentLevel + 1; skills = { ...skills, [skillId]: newLevel }; skillProgress = { ...skillProgress, [skillId]: 0 }; log = [`✅ ${SKILLS_DEF[skillId]?.name} Lv.${newLevel} mastered!`, ...log.slice(0, 49)]; // Check if this skill unlocks effects (research skills) const effectsToUnlock = EFFECT_RESEARCH_MAPPING[skillId]; if (effectsToUnlock && newLevel >= (SKILLS_DEF[skillId]?.max || 1)) { const newEffects = effectsToUnlock.filter(e => !unlockedEffects.includes(e)); if (newEffects.length > 0) { unlockedEffects = [...unlockedEffects, ...newEffects]; log = [`🔬 Unlocked ${newEffects.length} new enchantment effect(s)!`, ...log.slice(0, 49)]; } } // Special case: When enchanting skill reaches level 1, unlock mana bolt if (skillId === 'enchanting' && newLevel >= 1) { const enchantingEffects = ENCHANTING_UNLOCK_EFFECTS.filter(e => !unlockedEffects.includes(e)); if (enchantingEffects.length > 0) { unlockedEffects = [...unlockedEffects, ...enchantingEffects]; log = [`✨ Enchantment design unlocked! Mana Bolt effect available.`, ...log.slice(0, 49)]; } } } else if (currentStudyTarget.type === 'spell') { // Spells can no longer be studied directly - they come from equipment // This branch is kept for backward compatibility but should not be used const spellId = currentStudyTarget.id; spells = { ...spells, [spellId]: { learned: true, level: 1, studyProgress: 0 } }; log = [`📖 ${SPELLS_DEF[spellId]?.name} learned!`, ...log.slice(0, 49)]; } currentStudyTarget = null; } } // Convert action - auto convert mana if (state.currentAction === 'convert') { const unlockedElements = Object.entries(elements) .filter(([, e]) => e.unlocked && e.current < e.max); if (unlockedElements.length > 0 && rawMana >= MANA_PER_ELEMENT) { // Sort by space available (descending) unlockedElements.sort((a, b) => (b[1].max - b[1].current) - (a[1].max - a[1].current)); const [targetId, targetState] = unlockedElements[0]; const canConvert = Math.min( Math.floor(rawMana / MANA_PER_ELEMENT), targetState.max - targetState.current ); if (canConvert > 0) { rawMana -= canConvert * MANA_PER_ELEMENT; elements = { ...elements, [targetId]: { ...targetState, current: targetState.current + canConvert } }; } } } // Combat - uses cast speed and spell casting let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress } = state; const floorElement = getFloorElement(currentFloor); if (state.currentAction === 'climb') { const spellId = state.activeSpell; const spellDef = SPELLS_DEF[spellId]; if (spellDef) { // Compute attack speed from quickCast skill and upgrades const baseAttackSpeed = 1 + (state.skills.quickCast || 0) * 0.05; const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier; // Get spell cast speed (casts per hour, default 1) const spellCastSpeed = spellDef.castSpeed || 1; // Effective casts per tick = spellCastSpeed * totalAttackSpeed const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed; // Accumulate cast progress castProgress = (castProgress || 0) + progressPerTick; // Process complete casts while (castProgress >= 1 && canAffordSpellCost(spellDef.cost, rawMana, elements)) { // Deduct cost const afterCost = deductSpellCost(spellDef.cost, rawMana, elements); rawMana = afterCost.rawMana; elements = afterCost.elements; totalManaGathered += spellDef.cost.amount; // Calculate damage let dmg = calcDamage(state, spellId, floorElement); // Apply upgrade damage multipliers and bonuses dmg = dmg * effects.baseDamageMultiplier + effects.baseDamageBonus; // Executioner: +100% damage to enemies below 25% HP if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && floorHP / 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 = (skills.spellEcho || 0) * 0.1; if (Math.random() < echoChance) { dmg *= 2; log = [`✨ Spell Echo! Double damage!`, ...log.slice(0, 49)]; } // Lifesteal effect const lifestealEffect = spellDef.effects?.find(e => e.type === 'lifesteal'); if (lifestealEffect) { const healAmount = dmg * lifestealEffect.value; rawMana = Math.min(rawMana + healAmount, maxMana); } // Apply damage floorHP = Math.max(0, floorHP - dmg); // Reduce cast progress by 1 (one cast completed) castProgress -= 1; if (floorHP <= 0) { // Floor cleared const wasGuardian = GUARDIANS[currentFloor]; if (wasGuardian && !signedPacts.includes(currentFloor)) { signedPacts = [...signedPacts, currentFloor]; log = [`⚔️ ${wasGuardian.name} defeated! Pact signed! (${wasGuardian.pact}x)`, ...log.slice(0, 49)]; } else if (!wasGuardian) { if (currentFloor % 5 === 0) { log = [`🏰 Floor ${currentFloor} cleared!`, ...log.slice(0, 49)]; } } currentFloor = currentFloor + 1; if (currentFloor > 100) { currentFloor = 100; } floorMaxHP = getFloorMaxHP(currentFloor); floorHP = floorMaxHP; maxFloorReached = Math.max(maxFloorReached, currentFloor); // Reset cast progress on floor change castProgress = 0; } } } else { // Not enough mana - pause casting (keep progress) castProgress = castProgress || 0; } } // Process crafting actions (design, prepare, enchant) const craftingUpdates = processCraftingTick( { ...state, rawMana, log, currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress, incursionStrength, currentStudyTarget, skills, skillProgress, spells, elements, meditateTicks, }, { rawMana, log } ); // Apply crafting updates if (craftingUpdates.rawMana !== undefined) rawMana = craftingUpdates.rawMana; if (craftingUpdates.log !== undefined) log = craftingUpdates.log; if (craftingUpdates.currentAction !== undefined) { set({ ...craftingUpdates, day, hour, rawMana, meditateTicks, totalManaGathered, currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, incursionStrength, currentStudyTarget, skills, skillProgress, spells, elements, log, castProgress, }); return; } set({ day, hour, rawMana, meditateTicks, totalManaGathered, currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, incursionStrength, currentStudyTarget, skills, skillProgress, spells, elements, unlockedEffects, log, castProgress, ...craftingUpdates, }); }, gatherMana: () => { const state = get(); // Compute unified effects for click mana const effects = getUnifiedEffects(state); let cm = computeClickMana(state, effects); // Mana overflow bonus const overflowBonus = 1 + (state.skills.manaOverflow || 0) * 0.25; cm = Math.floor(cm * overflowBonus); const max = computeMaxMana(state, effects); set({ rawMana: Math.min(state.rawMana + cm, max), totalManaGathered: state.totalManaGathered + cm, }); }, setAction: (action: GameAction) => { set((state) => ({ currentAction: action, meditateTicks: action === 'meditate' ? state.meditateTicks : 0, })); }, setSpell: (spellId: string) => { const state = get(); // Can only set learned spells if (state.spells[spellId]?.learned) { set({ activeSpell: spellId }); } }, startStudyingSkill: (skillId: string) => { const state = get(); const sk = SKILLS_DEF[skillId]; if (!sk) return; const currentLevel = state.skills[skillId] || 0; if (currentLevel >= sk.max) return; // Check prerequisites if (sk.req) { for (const [r, rl] of Object.entries(sk.req)) { if ((state.skills[r] || 0) < rl) return; } } // Check mana cost (with focused mind reduction) const costMult = getStudyCostMultiplier(state.skills); const cost = Math.floor(sk.base * (currentLevel + 1) * costMult); if (state.rawMana < cost) return; // Start studying set({ rawMana: state.rawMana - cost, currentAction: 'study', currentStudyTarget: { type: 'skill', id: skillId, progress: state.skillProgress[skillId] || 0, required: sk.studyTime, }, log: [`📚 Started studying ${sk.name}...`, ...state.log.slice(0, 49)], }); }, startStudyingSpell: (spellId: string) => { const state = get(); const sp = SPELLS_DEF[spellId]; if (!sp || state.spells[spellId]?.learned) return; // Check mana cost (with focused mind reduction) const costMult = getStudyCostMultiplier(state.skills); const cost = Math.floor(sp.unlock * costMult); if (state.rawMana < cost) return; const studyTime = sp.studyTime || (sp.tier * 4); // Default study time based on tier // Start studying set({ rawMana: state.rawMana - cost, currentAction: 'study', currentStudyTarget: { type: 'spell', id: spellId, progress: state.spells[spellId]?.studyProgress || 0, required: studyTime, }, spells: { ...state.spells, [spellId]: { ...(state.spells[spellId] || { learned: false, level: 0 }), studyProgress: state.spells[spellId]?.studyProgress || 0 }, }, log: [`📚 Started studying ${sp.name}...`, ...state.log.slice(0, 49)], }); }, cancelStudy: () => { const state = get(); if (!state.currentStudyTarget) return; // Knowledge retention bonus const retentionBonus = 1 + (state.skills.knowledgeRetention || 0) * 0.2; const savedProgress = Math.min( state.currentStudyTarget.progress, state.currentStudyTarget.required * retentionBonus ); // Save progress if (state.currentStudyTarget.type === 'skill') { set({ currentStudyTarget: null, currentAction: 'meditate', skillProgress: { ...state.skillProgress, [state.currentStudyTarget.id]: savedProgress, }, log: [`📖 Study interrupted. Progress saved.`, ...state.log.slice(0, 49)], }); } else if (state.currentStudyTarget.type === 'spell') { set({ currentStudyTarget: null, currentAction: 'meditate', spells: { ...state.spells, [state.currentStudyTarget.id]: { ...(state.spells[state.currentStudyTarget.id] || { learned: false, level: 0 }), studyProgress: savedProgress, }, }, log: [`📖 Study interrupted. Progress saved.`, ...state.log.slice(0, 49)], }); } }, convertMana: (element: string, amount: number = 1) => { const state = get(); const e = state.elements[element]; if (!e?.unlocked) return; const cost = MANA_PER_ELEMENT * amount; if (state.rawMana < cost) return; if (e.current >= e.max) return; const canConvert = Math.min( amount, Math.floor(state.rawMana / MANA_PER_ELEMENT), e.max - e.current ); set({ rawMana: state.rawMana - canConvert * MANA_PER_ELEMENT, elements: { ...state.elements, [element]: { ...e, current: e.current + canConvert }, }, }); }, unlockElement: (element: string) => { const state = get(); if (state.elements[element]?.unlocked) return; const cost = 500; if (state.rawMana < cost) return; set({ rawMana: state.rawMana - cost, elements: { ...state.elements, [element]: { ...state.elements[element], unlocked: true }, }, log: [`✨ ${ELEMENTS[element].name} affinity unlocked!`, ...state.log.slice(0, 49)], }); }, craftComposite: (target: string) => { const state = get(); const edef = ELEMENTS[target]; if (!edef?.recipe) return; const recipe = edef.recipe; const costs: Record = {}; recipe.forEach((r) => { costs[r] = (costs[r] || 0) + 1; }); // Check ingredients for (const [r, amt] of Object.entries(costs)) { if ((state.elements[r]?.current || 0) < amt) return; } const newElems = { ...state.elements }; for (const [r, amt] of Object.entries(costs)) { newElems[r] = { ...newElems[r], current: newElems[r].current - amt }; } // Elemental crafting bonus const craftBonus = 1 + (state.skills.elemCrafting || 0) * 0.25; const outputAmount = Math.floor(craftBonus); const effects = getUnifiedEffects(state); const elemMax = computeElementMax(state, effects); newElems[target] = { ...(newElems[target] || { current: 0, max: elemMax, unlocked: false }), current: (newElems[target]?.current || 0) + outputAmount, max: elemMax, unlocked: true, }; set({ elements: newElems, log: [`🧪 Crafted ${outputAmount} ${ELEMENTS[target].name} mana!`, ...state.log.slice(0, 49)], }); }, doPrestige: (id: string) => { const state = get(); const pd = PRESTIGE_DEF[id]; if (!pd) return; const lvl = state.prestigeUpgrades[id] || 0; if (lvl >= pd.max || state.insight < pd.cost) return; const newPU = { ...state.prestigeUpgrades, [id]: lvl + 1 }; set({ insight: state.insight - pd.cost, prestigeUpgrades: newPU, memorySlots: id === 'deepMemory' ? state.memorySlots + 1 : state.memorySlots, log: [`⭐ ${pd.name} upgraded to Lv.${lvl + 1}!`, ...state.log.slice(0, 49)], }); }, startNewLoop: () => { const state = get(); const insightGained = state.loopInsight || calcInsight(state); const total = state.insight + insightGained; // Keep some spells through temporal memory let spellsToKeep: string[] = []; if (state.skills.temporalMemory) { const learnedSpells = Object.entries(state.spells) .filter(([, s]) => s.learned) .map(([id]) => id); spellsToKeep = learnedSpells.slice(0, state.skills.temporalMemory); } const newState = makeInitial({ loopCount: state.loopCount + 1, insight: total, totalInsight: (state.totalInsight || 0) + insightGained, prestigeUpgrades: state.prestigeUpgrades, memories: state.memories, skills: state.skills, // Keep skills through temporal memory for now }); // Add kept spells if (spellsToKeep.length > 0) { spellsToKeep.forEach(spellId => { newState.spells[spellId] = { learned: true, level: 1, studyProgress: 0 }; }); } set(newState); }, togglePause: () => { set((state) => ({ paused: !state.paused })); }, resetGame: () => { // Clear localStorage and reset localStorage.removeItem('mana-loop-storage'); set(makeInitial()); }, selectSkillUpgrade: (skillId: string, upgradeId: string) => { set((state) => { const current = state.skillUpgrades?.[skillId] || []; if (current.includes(upgradeId)) return state; if (current.length >= 2) return state; // Max 2 upgrades per milestone return { skillUpgrades: { ...state.skillUpgrades, [skillId]: [...current, upgradeId], }, }; }); }, deselectSkillUpgrade: (skillId: string, upgradeId: string) => { set((state) => { const current = state.skillUpgrades?.[skillId] || []; return { skillUpgrades: { ...state.skillUpgrades, [skillId]: current.filter(id => id !== upgradeId), }, }; }); }, commitSkillUpgrades: (skillId: string, upgradeIds: string[]) => { set((state) => ({ skillUpgrades: { ...state.skillUpgrades, [skillId]: upgradeIds, }, })); }, tierUpSkill: (skillId: string) => { const state = get(); const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId; const currentTier = state.skillTiers?.[baseSkillId] || 1; const nextTier = currentTier + 1; if (nextTier > 5) return; // Max tier is 5 const nextTierSkillId = `${baseSkillId}_t${nextTier}`; const currentLevel = state.skills[skillId] || 0; set({ skillTiers: { ...state.skillTiers, [baseSkillId]: nextTier, }, skills: { ...state.skills, [nextTierSkillId]: currentLevel, // Carry over level to new tier [skillId]: 0, // Reset old tier }, skillUpgrades: { ...state.skillUpgrades, [nextTierSkillId]: [], // Start fresh with upgrades for new tier }, log: [`🌟 ${SKILLS_DEF[baseSkillId]?.name || baseSkillId} evolved to Tier ${nextTier}!`, ...state.log.slice(0, 49)], }); }, startParallelStudySkill: (skillId: string) => { const state = get(); if (state.parallelStudyTarget) return; // Already have parallel study if (!state.currentStudyTarget) return; // Need primary study const sk = SKILLS_DEF[skillId]; if (!sk) return; const currentLevel = state.skills[skillId] || 0; if (currentLevel >= sk.max) return; // Can't study same thing in parallel if (state.currentStudyTarget.id === skillId) return; set({ parallelStudyTarget: { type: 'skill', id: skillId, progress: state.skillProgress[skillId] || 0, required: sk.studyTime, }, log: [`📚 Started parallel study of ${sk.name}... (50% speed)`, ...state.log.slice(0, 49)], }); }, cancelParallelStudy: () => { set((state) => { if (!state.parallelStudyTarget) return state; return { parallelStudyTarget: null, log: ['📖 Parallel study cancelled.', ...state.log.slice(0, 49)], }; }); }, getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => { const state = get(); const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId; const tier = state.skillTiers?.[baseSkillId] || 1; const path = SKILL_EVOLUTION_PATHS[baseSkillId]; if (!path) return { available: [], selected: [] }; const tierDef = path.tiers.find(t => t.tier === tier); if (!tierDef) return { available: [], selected: [] }; const available = tierDef.upgrades.filter(u => u.milestone === milestone); const selected = state.skillUpgrades?.[skillId]?.filter(id => available.some(u => u.id === id) ) || []; return { available, selected }; }, // ─── Crafting Actions (from crafting slice) ───────────────────────────────── createEquipmentInstance: (typeId: string) => { const type = EQUIPMENT_TYPES[typeId]; if (!type) return null; const instanceId = `equip_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const instance: EquipmentInstance = { instanceId, typeId, name: type.name, enchantments: [], usedCapacity: 0, totalCapacity: type.baseCapacity, rarity: 'common', quality: 100, }; set((state) => ({ equipmentInstances: { ...state.equipmentInstances, [instanceId]: instance, }, })); return instanceId; }, equipItem: (instanceId: string, slot: EquipmentSlot) => { const state = get(); const instance = state.equipmentInstances[instanceId]; if (!instance) return false; const type = EQUIPMENT_TYPES[instance.typeId]; if (!type) return false; // Check if equipment can go in this slot const validSlots = type.category === 'accessory' ? ['accessory1', 'accessory2'] : [type.slot]; if (!validSlots.includes(slot)) return false; // Check if slot is occupied const currentEquipped = state.equippedInstances[slot]; if (currentEquipped === instanceId) return true; // Already equipped here // If this item is equipped elsewhere, unequip it first let newEquipped = { ...state.equippedInstances }; for (const [s, id] of Object.entries(newEquipped)) { if (id === instanceId) { newEquipped[s as EquipmentSlot] = null; } } // Equip to new slot newEquipped[slot] = instanceId; set(() => ({ equippedInstances: newEquipped })); return true; }, unequipItem: (slot: EquipmentSlot) => { set((state) => ({ equippedInstances: { ...state.equippedInstances, [slot]: null, }, })); }, deleteEquipmentInstance: (instanceId: string) => { set((state) => { // First unequip if equipped let newEquipped = { ...state.equippedInstances }; for (const [slot, id] of Object.entries(newEquipped)) { if (id === instanceId) { newEquipped[slot as EquipmentSlot] = null; } } // Remove from instances const newInstances = { ...state.equipmentInstances }; delete newInstances[instanceId]; return { equippedInstances: newEquipped, equipmentInstances: newInstances, }; }); }, startDesigningEnchantment: (name: string, equipmentTypeId: string, effects: DesignEffect[]) => { const state = get(); // Check if player has enchanting skill const enchantingLevel = state.skills.enchanting || 0; if (enchantingLevel < 1) return false; // Validate effects for equipment category const type = EQUIPMENT_TYPES[equipmentTypeId]; if (!type) return false; for (const eff of effects) { const effectDef = ENCHANTMENT_EFFECTS[eff.effectId]; if (!effectDef) return false; if (!effectDef.allowedEquipmentCategories.includes(type.category)) return false; if (eff.stacks > effectDef.maxStacks) return false; } // Calculate capacity cost const efficiencyBonus = (state.skills.efficientEnchant || 0) * 0.05; const totalCapacityCost = effects.reduce((total, eff) => total + calculateEffectCapacityCost(eff.effectId, eff.stacks, efficiencyBonus), 0); // Calculate design time let designTime = 1; for (const eff of effects) { designTime += 0.5 * eff.stacks; } // Store pending design in designProgress set(() => ({ currentAction: 'design', designProgress: { designId: `design_${Date.now()}`, progress: 0, required: designTime, }, // Store design data temporarily log: [`🔮 Designing enchantment: ${name}...`, ...state.log.slice(0, 49)], })); return true; }, cancelDesign: () => { set(() => ({ currentAction: 'meditate', designProgress: null, })); }, saveDesign: (design: EnchantmentDesign) => { set((state) => ({ enchantmentDesigns: [...state.enchantmentDesigns, design], designProgress: null, currentAction: 'meditate', log: [`📜 Enchantment design "${design.name}" saved!`, ...state.log.slice(0, 49)], })); }, deleteDesign: (designId: string) => { set((state) => ({ enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== designId), })); }, startPreparing: (equipmentInstanceId: string) => { const state = get(); const instance = state.equipmentInstances[equipmentInstanceId]; if (!instance) return false; // Prep time: 2 hours base + 1 hour per 50 capacity const prepTime = 2 + Math.floor(instance.totalCapacity / 50); const manaCost = instance.totalCapacity * 10; if (state.rawMana < manaCost) return false; set(() => ({ currentAction: 'prepare', preparationProgress: { equipmentInstanceId, progress: 0, required: prepTime, manaCostPaid: 0, }, log: [`⚙️ Preparing ${instance.name} for enchanting...`, ...state.log.slice(0, 49)], })); return true; }, cancelPreparation: () => { set(() => ({ currentAction: 'meditate', preparationProgress: null, })); }, startApplying: (equipmentInstanceId: string, designId: string) => { const state = get(); const instance = state.equipmentInstances[equipmentInstanceId]; const design = state.enchantmentDesigns.find(d => d.id === designId); if (!instance || !design) return false; // Check capacity if (instance.usedCapacity + design.totalCapacityUsed > instance.totalCapacity) { return false; } // Application time: 2 hours base + 1 hour per effect stack const applicationTime = 2 + design.effects.reduce((total, eff) => total + eff.stacks, 0); const manaPerHour = 20 + design.effects.reduce((total, eff) => total + eff.stacks * 5, 0); set(() => ({ currentAction: 'enchant', applicationProgress: { equipmentInstanceId, designId, progress: 0, required: applicationTime, manaPerHour, paused: false, manaSpent: 0, }, log: [`✨ Applying "${design.name}" to ${instance.name}...`, ...state.log.slice(0, 49)], })); return true; }, pauseApplication: () => { set((state) => { if (!state.applicationProgress) return {}; return { applicationProgress: { ...state.applicationProgress, paused: true, }, }; }); }, resumeApplication: () => { set((state) => { if (!state.applicationProgress) return {}; return { applicationProgress: { ...state.applicationProgress, paused: false, }, }; }); }, cancelApplication: () => { set(() => ({ currentAction: 'meditate', applicationProgress: null, })); }, disenchantEquipment: (instanceId: string) => { const state = get(); const instance = state.equipmentInstances[instanceId]; if (!instance || instance.enchantments.length === 0) return; const disenchantLevel = state.skills.disenchanting || 0; const recoveryRate = 0.1 + disenchantLevel * 0.2; // 10% base + 20% per level let totalRecovered = 0; for (const ench of instance.enchantments) { totalRecovered += Math.floor(ench.actualCost * recoveryRate); } set((state) => ({ rawMana: state.rawMana + totalRecovered, equipmentInstances: { ...state.equipmentInstances, [instanceId]: { ...instance, enchantments: [], usedCapacity: 0, }, }, log: [`✨ Disenchanted ${instance.name}, recovered ${totalRecovered} mana.`, ...state.log.slice(0, 49)], })); }, getEquipmentSpells: () => { const state = get(); const spells: string[] = []; for (const instanceId of Object.values(state.equippedInstances)) { if (!instanceId) continue; const instance = state.equipmentInstances[instanceId]; if (!instance) continue; for (const ench of instance.enchantments) { const effectDef = ENCHANTMENT_EFFECTS[ench.effectId]; if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) { spells.push(effectDef.effect.spellId); } } } return [...new Set(spells)]; // Remove duplicates }, getEquipmentEffects: () => { const state = get(); const effects: Record = {}; for (const instanceId of Object.values(state.equippedInstances)) { if (!instanceId) continue; const instance = state.equipmentInstances[instanceId]; if (!instance) continue; for (const ench of instance.enchantments) { const effectDef = ENCHANTMENT_EFFECTS[ench.effectId]; if (!effectDef) continue; if (effectDef.effect.type === 'bonus' && effectDef.effect.stat && effectDef.effect.value) { effects[effectDef.effect.stat] = (effects[effectDef.effect.stat] || 0) + effectDef.effect.value * ench.stacks; } } } return effects; }, getAvailableCapacity: (instanceId: string) => { const state = get(); const instance = state.equipmentInstances[instanceId]; if (!instance) return 0; return instance.totalCapacity - instance.usedCapacity; }, // Attunement XP and leveling addAttunementXP: (attunementId: string, amount: number) => { const state = get(); const attState = state.attunements[attunementId]; if (!attState?.active) return; let newXP = attState.experience + amount; let newLevel = attState.level; // Check for level ups while (newLevel < MAX_ATTUNEMENT_LEVEL) { const xpNeeded = getAttunementXPForLevel(newLevel + 1); if (newXP >= xpNeeded) { newXP -= xpNeeded; newLevel++; } else { break; } } // Cap XP at max level if (newLevel >= MAX_ATTUNEMENT_LEVEL) { newXP = 0; } set({ attunements: { ...state.attunements, [attunementId]: { ...attState, level: newLevel, experience: newXP, }, }, log: newLevel > attState.level ? [`🌟 ${attunementId} attunement reached Level ${newLevel}!`, ...state.log.slice(0, 49)] : state.log, }); }, // Debug functions debugUnlockAttunement: (attunementId: string) => { const state = get(); const def = ATTUNEMENTS_DEF[attunementId]; if (!def) return; set({ attunements: { ...state.attunements, [attunementId]: { id: attunementId, active: true, level: 1, experience: 0, }, }, // Unlock the primary mana type if applicable elements: def.primaryManaType && state.elements[def.primaryManaType] ? { ...state.elements, [def.primaryManaType]: { ...state.elements[def.primaryManaType], unlocked: true, }, } : state.elements, log: [`🔓 Debug: Unlocked ${def.name} attunement!`, ...state.log.slice(0, 49)], }); }, debugAddElementalMana: (element: string, amount: number) => { const state = get(); const elem = state.elements[element]; if (!elem?.unlocked) return; set({ elements: { ...state.elements, [element]: { ...elem, current: Math.min(elem.current + amount, elem.max * 10), // Allow overflow }, }, }); }, debugSetTime: (day: number, hour: number) => { set({ day, hour, incursionStrength: getIncursionStrength(day, hour), }); }, debugAddAttunementXP: (attunementId: string, amount: number) => { const state = get(); const attState = state.attunements[attunementId]; if (!attState) return; let newXP = attState.experience + amount; let newLevel = attState.level; while (newLevel < MAX_ATTUNEMENT_LEVEL) { const xpNeeded = getAttunementXPForLevel(newLevel + 1); if (newXP >= xpNeeded) { newXP -= xpNeeded; newLevel++; } else { break; } } set({ attunements: { ...state.attunements, [attunementId]: { ...attState, level: newLevel, experience: newXP, }, }, }); }, debugSetFloor: (floor: number) => { const state = get(); set({ currentFloor: floor, floorHP: getFloorMaxHP(floor), floorMaxHP: getFloorMaxHP(floor), maxFloorReached: Math.max(state.maxFloorReached, floor), }); }, }), { name: 'mana-loop-storage', version: 2, migrate: (persistedState: unknown, version: number) => { const state = persistedState as Record; // Migration from version 0/1 to version 2 - add missing fields if (version < 2) { return { ...state, castProgress: state.castProgress ?? 0, skillUpgrades: state.skillUpgrades ?? {}, skillTiers: state.skillTiers ?? {}, parallelStudyTarget: state.parallelStudyTarget ?? null, }; } return state; }, partialize: (state) => ({ day: state.day, hour: state.hour, loopCount: state.loopCount, rawMana: state.rawMana, meditateTicks: state.meditateTicks, totalManaGathered: state.totalManaGathered, attunements: state.attunements, elements: state.elements, currentFloor: state.currentFloor, floorHP: state.floorHP, floorMaxHP: state.floorMaxHP, maxFloorReached: state.maxFloorReached, signedPacts: state.signedPacts, activeSpell: state.activeSpell, currentAction: state.currentAction, castProgress: state.castProgress, combo: state.combo, spells: state.spells, skills: state.skills, skillProgress: state.skillProgress, skillUpgrades: state.skillUpgrades, skillTiers: state.skillTiers, currentStudyTarget: state.currentStudyTarget, parallelStudyTarget: state.parallelStudyTarget, achievements: state.achievements, totalSpellsCast: state.totalSpellsCast, totalDamageDealt: state.totalDamageDealt, totalCraftsCompleted: state.totalCraftsCompleted, insight: state.insight, totalInsight: state.totalInsight, prestigeUpgrades: state.prestigeUpgrades, memorySlots: state.memorySlots, memories: state.memories, log: state.log, // Equipment system equippedInstances: state.equippedInstances, equipmentInstances: state.equipmentInstances, enchantmentDesigns: state.enchantmentDesigns, designProgress: state.designProgress, preparationProgress: state.preparationProgress, applicationProgress: state.applicationProgress, equipmentCraftingProgress: state.equipmentCraftingProgress, unlockedEffects: state.unlockedEffects, // Loot inventory lootInventory: state.lootInventory, }), } ) ); // ─── Game Loop Hook ─────────────────────────────────────────────────────────── export function useGameLoop() { const tick = useGameStore((s) => s.tick); // Use useEffect in the component that uses this hook return { start: () => { const interval = setInterval(tick, TICK_MS); return () => clearInterval(interval); }, }; }