// ─── Combat Slice ───────────────────────────────────────────────────────────── // Manages spire climbing, combat, and floor progression import type { StateCreator } from 'zustand'; import type { GameState, GameAction, SpellCost } from '../types'; import { GUARDIANS, SPELLS_DEF, ELEMENTS, ELEMENT_OPPOSITES } from '../constants'; import { getFloorMaxHP, getFloorElement, calcDamage, computePactMultiplier, canAffordSpellCost, deductSpellCost } from './computed'; import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '../upgrade-effects'; export interface CombatSlice { // State currentFloor: number; floorHP: number; floorMaxHP: number; maxFloorReached: number; activeSpell: string; currentAction: GameAction; castProgress: number; // Actions setAction: (action: GameAction) => void; setSpell: (spellId: string) => void; getDamage: (spellId: string) => number; // Internal combat processing processCombat: (deltaHours: number) => Partial; } export const createCombatSlice = ( set: StateCreator['set'], get: () => GameState ): CombatSlice => ({ currentFloor: 1, floorHP: getFloorMaxHP(1), floorMaxHP: getFloorMaxHP(1), maxFloorReached: 1, activeSpell: 'manaBolt', currentAction: 'meditate', castProgress: 0, setAction: (action: GameAction) => { set((state) => ({ currentAction: action, meditateTicks: action === 'meditate' ? state.meditateTicks : 0, })); }, setSpell: (spellId: string) => { const state = get(); if (state.spells[spellId]?.learned) { set({ activeSpell: spellId }); } }, getDamage: (spellId: string) => { const state = get(); const floorElem = getFloorElement(state.currentFloor); return calcDamage(state, spellId, floorElem); }, processCombat: (deltaHours: number) => { const state = get(); if (state.currentAction !== 'climb') return {}; const spellId = state.activeSpell; const spellDef = SPELLS_DEF[spellId]; if (!spellDef) return {}; const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {}); const baseAttackSpeed = 1 + (state.skills.quickCast || 0) * 0.05; const totalAttackSpeed = baseAttackSpeed * effects.attackSpeedMultiplier; const spellCastSpeed = spellDef.castSpeed || 1; const progressPerTick = deltaHours * spellCastSpeed * totalAttackSpeed; let castProgress = (state.castProgress || 0) + progressPerTick; let rawMana = state.rawMana; let elements = state.elements; let totalManaGathered = state.totalManaGathered; let currentFloor = state.currentFloor; let floorHP = state.floorHP; let floorMaxHP = state.floorMaxHP; let maxFloorReached = state.maxFloorReached; let signedPacts = state.signedPacts; let pendingPactOffer = state.pendingPactOffer; const log = [...state.log]; const skills = state.skills; const floorElement = getFloorElement(currentFloor); 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); 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 const maxMana = 100; // Would need proper max mana calculation 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.unshift('✨ Spell Echo! Double damage!'); } // Apply damage floorHP = Math.max(0, floorHP - dmg); castProgress -= 1; if (floorHP <= 0) { const wasGuardian = GUARDIANS[currentFloor]; if (wasGuardian && !signedPacts.includes(currentFloor)) { pendingPactOffer = currentFloor; log.unshift(`⚔️ ${wasGuardian.name} defeated! They offer a pact...`); } else if (!wasGuardian) { if (currentFloor % 5 === 0) { log.unshift(`🏰 Floor ${currentFloor} cleared!`); } } currentFloor = currentFloor + 1; if (currentFloor > 100) currentFloor = 100; floorMaxHP = getFloorMaxHP(currentFloor); floorHP = floorMaxHP; maxFloorReached = Math.max(maxFloorReached, currentFloor); castProgress = 0; } } return { rawMana, elements, totalManaGathered, currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, pendingPactOffer, castProgress, log, }; }, });