// ─── Computed Stats and Utility Functions ─────────────────────────────────────── // This module contains all computed stat functions and utility helpers // extracted from the main store for better organization import type { GameState, SpellCost, EquipmentInstance } from './types'; import { GUARDIANS, SPELLS_DEF, FLOOR_ELEM_CYCLE, HOURS_PER_TICK, MAX_DAY, INCURSION_START_DAY, ELEMENT_OPPOSITES, ELEMENTS, TICK_MS, } from './constants'; import type { ComputedEffects } from './upgrade-effects'; import { getUnifiedEffects, type UnifiedEffects } from './effects'; import { EQUIPMENT_TYPES } from './data/equipment'; import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects'; // ─── Default Effects Constant ─────────────────────────────────────────────────── // Default empty effects for when effects aren't provided export 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: [], }; // ─── Number Formatting 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'; } // ─── Floor Functions ──────────────────────────────────────────────────────────── 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]; } // ─── Equipment Spell Helper ───────────────────────────────────────────────────── // Get all spells from equipped caster weapons (staves, wands, etc.) // Returns array of { spellId, equipmentInstanceId } export function getActiveEquipmentSpells( equippedInstances: Record, equipmentInstances: Record ): Array<{ spellId: string; equipmentId: string }> { const spells: Array<{ spellId: string; equipmentId: string }> = []; // Check main hand and off hand for caster equipment const weaponSlots = ['mainHand', 'offHand'] as const; for (const slot of weaponSlots) { const instanceId = equippedInstances[slot]; if (!instanceId) continue; const instance = equipmentInstances[instanceId]; if (!instance) continue; // Check if this is a caster-type equipment const equipType = EQUIPMENT_TYPES[instance.typeId]; if (!equipType || equipType.category !== 'caster') continue; // Get spells from enchantments for (const ench of instance.enchantments) { const effectDef = ENCHANTMENT_EFFECTS[ench.effectId]; if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) { spells.push({ spellId: effectDef.effect.spellId, equipmentId: instanceId, }); } } } return spells; } // ─── Skill Level Helper ───────────────────────────────────────────────────────── // Helper to get effective skill level accounting for tiers export 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 }; } // ─── Computed Stat Functions ──────────────────────────────────────────────────── 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; // 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 | UnifiedEffects ): 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; } // ─── Damage Calculation Helpers ───────────────────────────────────────────────── // Elemental damage bonus: +50% if spell element opposes floor element (super effective) // -25% if spell element matches its own opposite (weak) export 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; } // ─── Insight Calculation ──────────────────────────────────────────────────────── 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 ─────────────────────────────────────────────────────────── // 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; } // ─── Incursion Strength ───────────────────────────────────────────────────────── 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); } // ─── Spell Cost Helpers ───────────────────────────────────────────────────────── // 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 export function deductSpellCost( cost: SpellCost, rawMana: number, elements: Record ): { rawMana: number; elements: Record } { const newElements = { ...elements }; if (cost.type === 'raw') { // Clamp to 0 to prevent negative mana return { rawMana: Math.max(0, rawMana - cost.amount), elements: newElements }; } else if (cost.element && newElements[cost.element]) { newElements[cost.element] = { ...newElements[cost.element], current: Math.max(0, newElements[cost.element].current - cost.amount) }; return { rawMana, elements: newElements }; } return { rawMana, elements: newElements }; } // ─── Damage Breakdown Helper ─────────────────────────────────────────────────── export interface DamageBreakdown { base: number; combatTrainBonus: number; arcaneFuryMult: number; elemMasteryMult: number; guardianBaneMult: number; pactMult: number; precisionChance: number; elemBonus: number; elemBonusText: string; total: number; } export function getDamageBreakdown( state: Pick, activeSpellId: string, floorElem: string, isGuardianFloor: boolean ): DamageBreakdown | null { const spell = SPELLS_DEF[activeSpellId]; if (!spell) return null; const baseDmg = spell.dmg; const combatTrainBonus = (state.skills.combatTrain || 0) * 5; const arcaneFuryMult = 1 + (state.skills.arcaneFury || 0) * 0.1; const elemMasteryMult = 1 + (state.skills.elementalMastery || 0) * 0.15; const guardianBaneMult = isGuardianFloor ? (1 + (state.skills.guardianBane || 0) * 0.2) : 1; const pactMult = state.signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1); const precisionChance = (state.skills.precision || 0) * 0.05; // Elemental bonus let elemBonus = 1.0; let elemBonusText = ''; if (spell.elem !== 'raw' && floorElem) { if (spell.elem === floorElem) { elemBonus = 1.25; elemBonusText = '+25% same element'; } else if (ELEMENTS[spell.elem] && ELEMENT_OPPOSITES[floorElem] === spell.elem) { elemBonus = 1.5; elemBonusText = '+50% super effective'; } } return { base: baseDmg, combatTrainBonus, arcaneFuryMult, elemMasteryMult, guardianBaneMult, pactMult, precisionChance, elemBonus, elemBonusText, total: calcDamage(state, activeSpellId, floorElem) }; } // ─── Total DPS Calculation ───────────────────────────────────────────────────── export function getTotalDPS( state: Pick, upgradeEffects: { attackSpeedMultiplier: number }, floorElem: string ): number { const quickCastBonus = 1 + (state.skills.quickCast || 0) * 0.05; const attackSpeedMult = upgradeEffects.attackSpeedMultiplier; const castsPerSecondMult = HOURS_PER_TICK / (TICK_MS / 1000); const activeEquipmentSpells = getActiveEquipmentSpells( state.equippedInstances, state.equipmentInstances ); let totalDPS = 0; for (const { spellId } of activeEquipmentSpells) { const spell = SPELLS_DEF[spellId]; if (!spell) continue; const spellCastSpeed = spell.castSpeed || 1; const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult; const damagePerCast = calcDamage(state, spellId, floorElem); const castsPerSecond = totalCastSpeed * castsPerSecondMult; totalDPS += damagePerCast * castsPerSecond; } return totalDPS; }