// ─── Game Store (Refactored) ────────────────────────────────────────────── // Main entry point - imports from modular store components import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import type { GameState, GameAction, ActivityLogEntry } from './types'; import { addActivityLogEntry } from './utils/activity-log'; import { computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, } from './utils/mana-utils'; import { calcDamage, calcInsight, getIncursionStrength, canAffordSpellCost, deductSpellCost, } from './utils/combat-utils'; import { generateFloorState } from './utils/room-utils'; // Re-export formatting functions for backward compatibility export { fmt, fmtDec } from './utils/formatting'; export { getFloorMaxHP, getFloorElement } from './utils/floor-utils'; // Re-export computed stats functions for backward compatibility and tests export { computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, } from './utils/mana-utils'; export { calcDamage, calcInsight, getIncursionStrength, canAffordSpellCost, deductSpellCost, } from './utils/combat-utils'; // ─── Initial State ─────────────────────────────────────────────────────── interface MakeInitialOptions { loopCount?: number; totalInsight?: number; insight?: number; prestigeUpgrades?: Record; } export function makeInitial(opts?: MakeInitialOptions): GameState { return { day: 1, hour: 0, rawMana: 100, maxMana: 100, elements: {}, skills: {}, skillUpgrades: {}, skillTiers: {}, spells: {}, currentAction: 'meditate' as GameAction, currentStudyTarget: null, parallelStudyTarget: null, activeSpell: null, currentFloor: 100, floorHP: 1000, floorMaxHP: 1000, currentRoom: generateFloorState(100), maxFloorReached: 100, paused: false, gameOver: false, victory: false, loopCount: opts?.loopCount ?? 0, totalInsight: opts?.totalInsight ?? 0, insight: opts?.insight ?? 0, loopInsight: 0, prestigeUpgrades: opts?.prestigeUpgrades ?? {}, signedPacts: [], attunements: {}, golemancy: { enabledGolems: [] }, memories: [], memorySlots: 0, log: [], activityLog: [], meditateTicks: 0, totalManaGathered: 0, equippedInstances: {}, equipmentInstances: {}, lootInventory: {}, blueprints: {}, spireMode: false, }; } // ─── Game Store Interface ───────────────────────────────────────────────── export interface GameStore extends GameState { tick: () => void; gatherMana: () => void; setAction: (action: GameAction) => void; addActivityLog: (eventType: string, message: string, details?: ActivityLogEntry['details']) => void; setSpell: (spellId: string) => void; cancelStudy: () => void; convertMana: (element: string, amount: number) => void; unlockElement: (element: string) => void; doPrestige: (id: string, selectedManaType?: string) => void; startNewLoop: () => void; togglePause: () => void; resetGame: () => void; addLog: (message: string) => void; addAttunementXP: (attunementId: string, amount: number) => void; toggleGolem: (golemId: string) => void; setEnabledGolems: (golemIds: string[]) => void; 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; resetFloorHP: () => void; getMaxMana: () => number; getRegen: () => number; getClickMana: () => number; getDamage: (spellId: string) => number; getMeditationMultiplier: () => number; canCastSpell: (spellId: string) => boolean; enterSpireMode: () => void; climbDownFloor: () => void; exitSpireMode: () => void; } // ─── Store Implementation ──────────────────────────────────────────────── 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, {}), canCastSpell: (spellId: string) => { const state = get(); const spell = state.spells?.[spellId]; if (!spell) return false; return true; }, addLog: (message: string) => { set((state) => ({ log: [message, ...(state.log || []).slice(0, 49)], })); }, addActivityLog: (eventType: string, message: string, details?: ActivityLogEntry['details']) => { set((state) => ({ activityLog: addActivityLogEntry(state, eventType, message, details), })); }, tick: () => { const state = get(); if (state.gameOver || state.paused) return; const maxMana = computeMaxMana(state); const baseRegen = computeRegen(state); let hour = state.hour + 1; let day = state.day; if (hour >= 24) { hour -= 24; day += 1; } if (day > 100) { 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; } let rawMana = state.rawMana + baseRegen; rawMana = Math.min(rawMana, maxMana); set({ day, hour, rawMana, meditateTicks: state.currentAction === 'meditate' ? state.meditateTicks + 1 : 0, }); }, gatherMana: () => { const state = get(); const clickMana = computeClickMana(state); const maxMana = computeMaxMana(state); set((s) => ({ rawMana: Math.min(s.rawMana + clickMana, maxMana), totalManaGathered: s.totalManaGathered + clickMana, })); }, setAction: (action: GameAction) => { set({ currentAction: action }); }, setSpell: (spellId: string) => { set({ activeSpell: spellId }); }, cancelStudy: () => { set({ currentStudyTarget: null, currentAction: 'meditate' }); }, convertMana: (element: string, amount: number) => { set((s) => { const elem = s.elements?.[element]; if (!elem || !elem.unlocked) return s; const canConvert = Math.min(amount, Math.floor(s.rawMana / 10), elem.max - elem.current); if (canConvert > 0) { return { rawMana: s.rawMana - canConvert * 10, elements: { ...s.elements, [element]: { ...elem, current: elem.current + canConvert } }, }; } return s; }); }, unlockElement: (element: string) => { set((s) => ({ elements: { ...s.elements, [element]: { ...s.elements[element], unlocked: true } }, })); }, doPrestige: (id: string, selectedManaType?: string) => { set((s) => ({ prestigeUpgrades: { ...s.prestigeUpgrades, [id]: (s.prestigeUpgrades[id] || 0) + 1 }, })); }, startNewLoop: () => { const state = get(); const insightGained = state.loopInsight || 0; set({ ...makeInitial({ loopCount: state.loopCount + 1, totalInsight: (state.totalInsight || 0) + insightGained, insight: (state.insight || 0) + insightGained, prestigeUpgrades: state.prestigeUpgrades, }), }); }, togglePause: () => { set((s) => ({ paused: !s.paused })); }, resetGame: () => { set(makeInitial()); }, addAttunementXP: (attunementId: string, amount: number) => { set((s) => { const attState = s.attunements?.[attunementId]; if (!attState) return s; return { attunements: { ...s.attunements, [attunementId]: { ...attState, experience: attState.experience + amount } }, }; }); }, toggleGolem: (golemId: string) => { set((s) => { const enabledGolems = s.golemancy?.enabledGolems || []; const isEnabled = enabledGolems.includes(golemId); return { golemancy: { ...s.golemancy, enabledGolems: isEnabled ? enabledGolems.filter(id => id !== golemId) : [...enabledGolems, golemId] }, }; }); }, setEnabledGolems: (golemIds: string[]) => { set((s) => ({ golemancy: { ...s.golemancy, enabledGolems: golemIds }, })); }, debugUnlockAttunement: (attunementId: string) => { set((s) => ({ attunements: { ...s.attunements, [attunementId]: { id: attunementId, active: true, level: 1, experience: 0 } }, })); }, debugAddElementalMana: (element: string, amount: number) => { set((s) => { const elem = s.elements?.[element]; if (!elem) return s; return { elements: { ...s.elements, [element]: { ...elem, current: Math.min(elem.current + amount, elem.max) } }, }; }); }, debugSetTime: (day: number, hour: number) => { set({ day, hour }); }, debugAddAttunementXP: (attunementId: string, amount: number) => { get().addAttunementXP(attunementId, amount); }, debugSetFloor: (floor: number) => { set((s) => ({ currentFloor: floor, currentRoom: generateFloorState(floor), floorMaxHP: 100 + floor * 50, floorHP: 100 + floor * 50, })); }, resetFloorHP: () => { set((s) => ({ floorHP: s.floorMaxHP, currentRoom: generateFloorState(s.currentFloor), })); }, enterSpireMode: () => { set({ spireMode: true }); }, climbDownFloor: () => { set((s) => { if (s.currentFloor <= 1) return s; const newFloor = s.currentFloor - 1; return { currentFloor: newFloor, currentRoom: generateFloorState(newFloor), floorMaxHP: 100 + newFloor * 50, floorHP: 100 + newFloor * 50, }; }); }, exitSpireMode: () => { set({ spireMode: false }); }, }), { name: 'mana-loop-game-store', } ) ); // ─── Game Loop Hook ─────────────────────────────────────────────────────────── export function useGameLoop() { const tick = useGameStore((s) => s.tick); return { start: () => { const interval = setInterval(tick, 1000); return () => clearInterval(interval); }, }; }