// ─── Combat Store ───────────────────────────────────────────────────────────── // Handles floors, spells, guardians, combat, and casting import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { createSafeStorage } from '../utils/safe-persist'; import type { GameAction, SpellState, FloorState, GolemancyState, ActivityLogEntry, AchievementState, EquipmentSpellState, ActivityEventType, RuntimeActiveGolem, EnemyState, EquipmentInstance } from '../types'; import { getFloorMaxHP } from '../utils'; import { generateSpireFloorState, getRoomsForFloor } from '../utils/spire-utils'; import { addActivityLogEntry } from '../utils/activity-log'; import { processCombatTick, makeInitialSpells } from './combat-actions'; import { getGuardianForFloor } from '../data/guardian-encounters'; import type { CombatStore } from './combat-state.types'; import { enterDescentMode, advanceRoomOrFloor, onEnterRoomDescend, createEnterSpireMode, } from './combat-descent-actions'; import { onEnterLibraryRoom, tickNonCombatRoom, skipNonCombatRoom, stayLongerInRoom, } from './non-combat-room-actions'; import { addGolemDesign, removeGolemDesign, toggleGolemLoadoutEntry, } from './golemancy-actions'; export const useCombatStore = create()( persist( (set, get) => ({ currentFloor: 1, floorHP: getFloorMaxHP(1), floorMaxHP: getFloorMaxHP(1), maxFloorReached: 0, activeSpell: 'manaBolt', currentAction: 'meditate', castProgress: 0, spireMode: false, // Room system currentRoom: generateSpireFloorState(1, 0, getRoomsForFloor(1, 1 * 12345)), // Spire climbing state clearedFloors: {}, climbDirection: null, isDescending: false, // ─── Spec: Room navigation state ────────────────────────────────────── startFloor: 1, exitFloor: 1, currentRoomIndex: 0, roomsPerFloor: 1, // ─── Spec: Run identity (climbing spec §4.2, §7) ──────────────────── runId: 0, // ─── Spec: Descent tracking state ───────────────────────────────────── descentPeak: null, roomResetState: {}, clearedRooms: {}, isDescentComplete: false, // Golemancy (component-based) golemancy: { golemDesigns: {}, golemLoadout: [], activeGolems: [] as RuntimeActiveGolem[], lastSummonFloor: 0, }, // Equipment spell states equipmentSpellStates: [], // Combat tracking comboHitCount: 0, floorHitCount: 0, // Melee sword progress accumulators (spec §3.1) meleeSwordProgress: {}, // Guardian defensive state guardianShield: 0, guardianShieldMax: 0, guardianBarrier: 0, guardianBarrierMax: 0, // Spells spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 }, }, // Activity Log activityLog: [], // Achievements achievements: { unlocked: [], progress: {}, }, // Stats tracking totalSpellsCast: 0, totalDamageDealt: 0, totalCraftsCompleted: 0, setCurrentFloor: (floor: number) => { set({ currentFloor: floor, floorHP: getFloorMaxHP(floor), floorMaxHP: getFloorMaxHP(floor), }); }, advanceFloor: () => { set((state) => { const newFloor = Math.min(state.currentFloor + 1, 100); return { currentFloor: newFloor, floorHP: getFloorMaxHP(newFloor), floorMaxHP: getFloorMaxHP(newFloor), maxFloorReached: Math.max(state.maxFloorReached, newFloor), castProgress: 0, }; }); }, setFloorHP: (hp: number) => { set({ floorHP: Math.max(0, hp) }); }, setMaxFloorReached: (floor: number) => { set((state) => ({ maxFloorReached: Math.max(state.maxFloorReached, floor), })); }, setAction: (action: GameAction) => { set({ currentAction: action }); }, setSpell: (spellId: string) => { const state = get(); if (state.spells[spellId]?.learned) { set({ activeSpell: spellId }); } }, setCastProgress: (progress: number) => { set({ castProgress: progress }); }, // Room state setCurrentRoom: (room: FloorState) => { set({ currentRoom: room }); }, // Spire climbing setClimbDirection: (direction: 'up' | 'down' | null) => { set({ climbDirection: direction }); }, setClearedFloor: (floor: number, cleared: boolean) => { set((state) => ({ clearedFloors: { ...state.clearedFloors, [floor]: cleared }, })); }, setIsDescending: (descending: boolean) => { set({ isDescending: descending }); }, climbDownFloor: () => { set((s) => { if (s.currentFloor <= 1) return s; const newFloor = s.currentFloor - 1; const seed = newFloor * 12345 + s.runId; const rooms = getRoomsForFloor(newFloor, seed); const newRoom = generateSpireFloorState(newFloor, 0, rooms, s.runId); return { currentFloor: newFloor, currentRoom: newRoom, floorMaxHP: getFloorMaxHP(newFloor), floorHP: getFloorMaxHP(newFloor), castProgress: 0, }; }); }, exitSpireMode: () => { set((s) => { const seed = s.exitFloor * 12345 + s.runId; const rooms = getRoomsForFloor(s.exitFloor, seed); return { spireMode: false, currentAction: 'meditate', climbDirection: null, isDescending: false, currentFloor: s.exitFloor, floorHP: getFloorMaxHP(s.exitFloor), floorMaxHP: getFloorMaxHP(s.exitFloor), currentRoom: generateSpireFloorState(s.exitFloor, 0, rooms, s.runId), castProgress: 0, clearedFloors: {}, clearedRooms: {}, roomResetState: {}, descentPeak: null, isDescentComplete: false, currentRoomIndex: 0, roomsPerFloor: 1, maxFloorReached: Math.max(s.maxFloorReached, 1), golemancy: { ...s.golemancy, activeGolems: [] as RuntimeActiveGolem[] }, }; }); }, startClimbUp: () => set({ climbDirection: 'up', currentAction: 'climb' }), startClimbDown: () => set({ climbDirection: 'down', currentAction: 'climb' }), startPracticing: () => set((s) => s.currentAction !== 'meditate' ? s : { currentAction: 'practicing' }), stopPracticing: () => set((s) => s.currentAction !== 'practicing' ? s : { currentAction: 'meditate' }), // ─── Spec: Descent actions (delegated to combat-descent-actions.ts) ──── enterDescentMode: () => enterDescentMode(get, set), advanceRoomOrFloor: () => advanceRoomOrFloor(get, set), onEnterRoomDescend: () => onEnterRoomDescend(get, set), onEnterLibraryRoom: () => onEnterLibraryRoom(get, set), tickNonCombatRoom: (hours: number) => tickNonCombatRoom(get, set, hours, advanceRoomOrFloor), skipNonCombatRoom: () => skipNonCombatRoom(get, set, advanceRoomOrFloor), stayLongerInRoom: () => stayLongerInRoom(get, set), // Golemancy addGolemDesign: (d) => addGolemDesign(set, d), removeGolemDesign: (id) => removeGolemDesign(set, id), toggleGolemLoadoutEntry: (id) => toggleGolemLoadoutEntry(set, id), enterSpireMode: createEnterSpireMode(get, set), learnSpell: (spellId: string) => { set((state) => ({ spells: { ...state.spells, [spellId]: { learned: true, level: 1, studyProgress: 0 }, }, })); }, setSpellState: (spellId: string, spellState: Partial) => { set((state) => ({ spells: { ...state.spells, [spellId]: { ...(state.spells[spellId] || { learned: false, level: 0, studyProgress: 0 }), ...spellState }, }, })); }, // Activity Log addActivityLog: (eventType: ActivityEventType, message: string, details?: ActivityLogEntry['details']) => { set((state) => ({ activityLog: addActivityLogEntry(state, eventType, message, details), })); }, // Stats incrementSpellsCast: () => { set((state) => ({ totalSpellsCast: state.totalSpellsCast + 1 })); }, addDamageDealt: (damage: number) => { set((state) => ({ totalDamageDealt: state.totalDamageDealt + damage })); }, incrementCraftsCompleted: () => { set((state) => ({ totalCraftsCompleted: state.totalCraftsCompleted + 1 })); }, resetGuardianDefensiveState: () => { set({ guardianShield: 0, guardianShieldMax: 0, guardianBarrier: 0, guardianBarrierMax: 0 }); }, initGuardianDefensiveState: () => { const state = get(); const guardian = getGuardianForFloor(state.currentFloor); if (!guardian) return; set({ guardianShield: guardian.shield ?? 0, guardianShieldMax: guardian.shield ?? 0, guardianBarrier: guardian.barrier ?? 0, guardianBarrierMax: guardian.barrier ?? 0, }); }, processCombatTick: ( rawMana: number, elements: Record, maxMana: number, attackSpeedMult: number, onFloorCleared: (floor: number, wasGuardian: boolean) => void, onDamageDealt: (damage: number) => { rawMana: number; elements: Record }, signedPacts: number[], golemancyState: { activeGolems: RuntimeActiveGolem[] }, golemApplyDamageToRoom: (dmg: number) => { floorHP: number; floorMaxHP: number; roomCleared: boolean }, applyEnemyDefenses: ( dmg: number, enemy: EnemyState | null, roomType: string, addLog: (msg: string) => void, bypassArmor?: boolean, bypassBarrier?: boolean, ) => number, equippedSwords?: Record, ) => { return processCombatTick( get, set, rawMana, elements, maxMana, attackSpeedMult, onFloorCleared, onDamageDealt, signedPacts, golemancyState, golemApplyDamageToRoom, applyEnemyDefenses, equippedSwords, ); }, resetCombat: (startFloor: number, spellsToKeep: string[] = []) => { const startSpells = makeInitialSpells(spellsToKeep); set({ currentFloor: startFloor, floorHP: getFloorMaxHP(startFloor), floorMaxHP: getFloorMaxHP(startFloor), maxFloorReached: startFloor, activeSpell: 'manaBolt', currentAction: 'meditate', castProgress: 0, spells: startSpells, }); }, }), { storage: createSafeStorage(), name: 'mana-loop-combat', version: 1, partialize: (state) => ({ currentFloor: state.currentFloor, maxFloorReached: state.maxFloorReached, spells: state.spells, activeSpell: state.activeSpell, currentAction: state.currentAction, floorHP: state.floorHP, floorMaxHP: state.floorMaxHP, castProgress: state.castProgress, spireMode: state.spireMode, currentRoom: state.currentRoom, clearedFloors: state.clearedFloors, golemancy: state.golemancy, equipmentSpellStates: state.equipmentSpellStates, comboHitCount: state.comboHitCount, floorHitCount: state.floorHitCount, activityLog: state.activityLog, achievements: state.achievements, totalSpellsCast: state.totalSpellsCast, totalDamageDealt: state.totalDamageDealt, totalCraftsCompleted: state.totalCraftsCompleted, guardianShield: state.guardianShield, guardianShieldMax: state.guardianShieldMax, guardianBarrier: state.guardianBarrier, guardianBarrierMax: state.guardianBarrierMax, meleeSwordProgress: state.meleeSwordProgress, runId: state.runId, }), } ) ); export { makeInitialSpells } from './combat-actions';