// ─── Computed Stats Functions ───────────────────────────────────────── // Extracted from store.ts (lines 362-689) // Full implementations with UnifiedEffects support import type { GameState, SpellCost, StudyTarget } from '../types'; import type { ComputedEffects } from '../upgrade-effects.types'; import type { UnifiedEffects } from '../effects'; import { SPELLS_DEF, GUARDIANS, ELEMENT_OPPOSITES, SKILLS_DEF, HOURS_PER_TICK, TICK_MS, INCURSION_START_DAY, MAX_DAY, ELEMENTS } from '../constants'; import { getUnifiedEffects } from '../effects'; import { getTotalAttunementRegen, getTotalAttunementConversionDrain } from '../data/attunements'; import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects'; // Helper to get effective skill level accounting for tiers function getEffectiveSkillLevel( skills: Record, baseSkillId: string, skillTiers: Record = {} ): { level: number; tier: number; tierMultiplier: number } { const currentTier = skillTiers[baseSkillId] || 1; const tieredSkillId = currentTier > 1 ? `${baseSkillId}_t${currentTier}` : baseSkillId; const level = skills[tieredSkillId] || skills[baseSkillId] || 0; const tierMultiplier = Math.pow(10, currentTier - 1); return { level, tier: currentTier, tierMultiplier }; } export function computeMaxMana( state: GameState, effects?: ComputedEffects | UnifiedEffects ): number { const pu = state.prestigeUpgrades; const skillMult = (effects as any)?.skillLevelMultiplier || 1; const base = 100 + (state.skills.manaWell || 0) * 100 * skillMult + (pu.manaWell || 0) * 500; // Check if we need to compute effects from equipment if (!effects && state.equipmentInstances && state.equippedInstances) { effects = getUnifiedEffects(state as any); } let maxMana: number; if (effects) { maxMana = Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier); } else { maxMana = base; } if (effects && hasSpecial(effects, SPECIAL_EFFECTS.MANA_CONDENSE)) { const totalGathered = state.totalManaGathered || 0; const condensesBonus = Math.floor(totalGathered / 1000); maxMana = Math.floor(maxMana * (1 + condensesBonus * 0.01)); } return maxMana; } export function computeElementMax( state: GameState, effects?: ComputedEffects | UnifiedEffects, element?: string ): number { const pu = state.prestigeUpgrades; const base = 10 + (state.skills.elemAttune || 0) * 50 + (pu.elementalAttune || 0) * 25; let adjustedBase = base; if (element && state.unlockedManaTypeUpgrades) { const typeUpgrades = state.unlockedManaTypeUpgrades.filter(u => u.typeId === element); const totalLevels = typeUpgrades.reduce((sum, u) => sum + u.level, 0); adjustedBase = base + (totalLevels * 10); } if (effects) { let bonus = effects.elementCapBonus || 0; if (element && (effects as UnifiedEffects).perElementCapBonus) { const perElementBonus = (effects as UnifiedEffects).perElementCapBonus[element]; if (perElementBonus) { bonus += perElementBonus; } } return Math.floor((adjustedBase + bonus) * (effects.elementCapMultiplier || 1)); } return adjustedBase; } export function computeRegen( state: GameState, effects?: ComputedEffects | UnifiedEffects ): number { const pu = state.prestigeUpgrades; const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1; const skillMult = (effects as any)?.skillLevelMultiplier || 1; const base = 2 + (state.skills.manaFlow || 0) * 1 * skillMult + (state.skills.manaSpring || 0) * 2 * skillMult + (pu.manaFlow || 0) * 0.5; let regen = base * temporalBonus; const attunementRegen = getTotalAttunementRegen(state.attunements || {}); regen += attunementRegen; if (!effects && state.equipmentInstances && state.equippedInstances) { effects = getUnifiedEffects(state as any); } if (effects) { regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier; } return regen; } export function computeEffectiveRegenForDisplay( state: GameState, effects?: ComputedEffects | UnifiedEffects ): { rawRegen: number; conversionDrain: number; effectiveRegen: number } { const rawRegen = computeRegen(state, effects); const conversionDrain = getTotalAttunementConversionDrain(state.attunements || {}); const effectiveRegen = Math.max(0, rawRegen - conversionDrain); return { rawRegen, conversionDrain, effectiveRegen }; } export function computeEffectiveRegen( state: GameState, effects?: ComputedEffects ): number { let regen = computeRegen(state, effects); const incursionStrength = state.incursionStrength || 0; regen *= (1 - incursionStrength); return regen; } export function computeClickMana( state: GameState, effects?: ComputedEffects | UnifiedEffects ): number { const skillMult = (effects as any)?.skillLevelMultiplier || 1; const base = 1 + (state.skills.manaTap || 0) * 1 * skillMult + (state.skills.manaSurge || 0) * 3 * skillMult; if (!effects && state.equipmentInstances && state.equippedInstances) { effects = getUnifiedEffects(state as any); } if (effects) { return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier); } return base; } function getElementalBonus(spellElem: string, floorElem: string): number { if (spellElem === 'raw') return 1.0; if (spellElem === floorElem) return 1.25; if (ELEMENT_OPPOSITES[floorElem] === spellElem) return 1.5; if (ELEMENT_OPPOSITES[spellElem] === floorElem) return 0.75; return 1.0; } export function calcDamage( state: Pick, spellId: string, floorElem?: string, effects?: ComputedEffects | UnifiedEffects ): number { const sp = SPELLS_DEF[spellId]; if (!sp) return 5; const skills = state.skills; const skillMult = (effects as any)?.skillLevelMultiplier || 1; const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5 * skillMult; const pct = 1 + (skills.arcaneFury || 0) * 0.1 * skillMult; const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15 * skillMult; const critChance = (skills.precision || 0) * 0.05; const pactMult = state.signedPacts.reduce((m, f) => m * ((GUARDIANS as any)[f]?.pact || 1), 1); let damage = baseDmg * pct * pactMult * elemMasteryBonus; if (floorElem) { damage *= getElementalBonus(sp.elem, floorElem); } 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); } 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; let bonus = 1 + Math.min(hours / 4, 0.5); if (hasMeditation && hours >= 4) bonus = 2.5; if (hasDeepTrance && hours >= 6) bonus = 3.0; if (hasVoidMeditation && hours >= 8) bonus = 5.0; 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); } 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; } } export function deductSpellCost( cost: SpellCost, rawMana: number, elements: Record ): { rawMana: number; elements: Record } { const newElements = { ...elements }; if (cost.type === 'raw') { const deductedAmount = Math.min(rawMana, cost.amount); return { rawMana: rawMana - deductedAmount, elements: newElements }; } else if (cost.element && newElements[cost.element]) { const elem = newElements[cost.element]; const deductedAmount = Math.min(elem.current, cost.amount); newElements[cost.element] = { ...elem, current: elem.current - deductedAmount }; return { rawMana, elements: newElements }; } return { rawMana, elements: newElements }; }