// ─── Upgrade Effect System ───────────────────────────────────────────────────── // This module handles applying skill upgrade effects to game stats import type { SkillUpgradeChoice, SkillUpgradeEffect } from './types'; import { getUpgradesForSkillAtMilestone, SKILL_EVOLUTION_PATHS } from './skill-evolution'; // ─── Types ─────────────────────────────────────────────────────────────────── export interface ActiveUpgradeEffect { upgradeId: string; skillId: string; milestone: 5 | 10; effect: SkillUpgradeEffect; name: string; desc: string; } export interface ComputedEffects { // Mana effects maxManaMultiplier: number; maxManaBonus: number; regenMultiplier: number; regenBonus: number; clickManaMultiplier: number; clickManaBonus: number; meditationEfficiency: number; spellCostMultiplier: number; conversionEfficiency: number; // Combat effects baseDamageMultiplier: number; baseDamageBonus: number; attackSpeedMultiplier: number; critChanceBonus: number; critDamageMultiplier: number; elementalDamageMultiplier: number; // Study effects studySpeedMultiplier: number; studyCostMultiplier: number; progressRetention: number; instantStudyChance: number; freeStudyChance: number; // Element effects elementCapMultiplier: number; elementCapBonus: number; conversionCostMultiplier: number; doubleCraftChance: number; // Special values permanentRegenBonus: number; // Special effect flags (for game logic to check) specials: Set; // All active upgrades for display activeUpgrades: ActiveUpgradeEffect[]; } // ─── Special Effect IDs ──────────────────────────────────────────────────────── // These are the IDs used in the 'specialId' field of special effects export const SPECIAL_EFFECTS = { // Mana Flow special effects MANA_CASCADE: 'manaCascade', // +0.1 regen per 100 max mana STEADY_STREAM: 'steadyStream', // Regen immune to incursion MANA_TORRENT: 'manaTorrent', // +50% regen when above 75% mana FLOW_SURGE: 'flowSurge', // Clicks restore 2x regen for 1 hour MANA_OVERFLOW: 'manaOverflow', // Raw mana can exceed max by 20% // Mana Well special effects DESPERATE_WELLS: 'desperateWells', // +50% regen when below 25% mana MANA_ECHO: 'manaEcho', // 10% chance double mana from clicks EMERGENCY_RESERVE: 'emergencyReserve', // Keep 10% mana on new loop // Combat special effects FIRST_STRIKE: 'firstStrike', // +15% damage on first attack each floor OVERPOWER: 'overpower', // +50% damage when mana above 80% BERSERKER: 'berserker', // +50% damage when below 50% mana COMBO_MASTER: 'comboMaster', // Every 5th attack deals 3x damage ADRENALINE_RUSH: 'adrenalineRush', // Defeating enemy restores 5% mana // Study special effects QUICK_GRASP: 'quickGrasp', // 5% chance double study progress per hour DEEP_CONCENTRATION: 'deepConcentration', // +20% study speed when mana > 90% QUICK_MASTERY: 'quickMastery', // -20% study time for final 3 levels PARALLEL_STUDY: 'parallelStudy', // Study 2 things at 50% speed STUDY_MOMENTUM: 'studyMomentum', // +5% study speed per consecutive hour KNOWLEDGE_ECHO: 'knowledgeEcho', // 10% chance instant study KNOWLEDGE_TRANSFER: 'knowledgeTransfer', // New skills start at 10% progress MENTAL_CLARITY: 'mentalClarity', // +10% study speed when mana > 75% STUDY_REFUND: 'studyRefund', // 25% mana back on study complete DEEP_UNDERSTANDING: 'deepUnderstanding', // +10% bonus from all skill levels STUDY_RUSH: 'studyRush', // First hour of study is 2x speed CHAIN_STUDY: 'chainStudy', // -5% cost per maxed skill // Element special effects ELEMENTAL_AFFINITY: 'elementalAffinity', // New elements start with 10 capacity EXOTIC_MASTERY: 'exoticMastery', // +20% exotic element damage ELEMENTAL_RESONANCE: 'elementalResonance', // Using element spells restores 1 of that element MANA_CONDUIT: 'manaConduit', // Meditation regenerates elemental mana } as const; // ─── Upgrade Definition Cache ───────────────────────────────────────────────── // Cache all upgrades by ID for quick lookup const upgradeDefinitionsById: Map = new Map(); // Build the cache on first access function buildUpgradeCache(): void { if (upgradeDefinitionsById.size > 0) return; for (const [baseSkillId, path] of Object.entries(SKILL_EVOLUTION_PATHS)) { for (const tierDef of path.tiers) { for (const upgrade of tierDef.upgrades) { upgradeDefinitionsById.set(upgrade.id, upgrade); } } } } // ─── Helper Functions ───────────────────────────────────────────────────────── /** * Get all selected upgrades with their full effect definitions */ export function getActiveUpgrades( skillUpgrades: Record, skillTiers: Record ): ActiveUpgradeEffect[] { buildUpgradeCache(); const result: ActiveUpgradeEffect[] = []; for (const [skillId, upgradeIds] of Object.entries(skillUpgrades)) { for (const upgradeId of upgradeIds) { const upgradeDef = upgradeDefinitionsById.get(upgradeId); if (upgradeDef) { result.push({ upgradeId, skillId, milestone: upgradeDef.milestone, effect: upgradeDef.effect, name: upgradeDef.name, desc: upgradeDef.desc, }); } } } return result; } /** * Compute all active effects from selected upgrades */ export function computeEffects( skillUpgrades: Record, skillTiers: Record ): ComputedEffects { const activeUpgrades = getActiveUpgrades(skillUpgrades, skillTiers); // Start with base values const 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, }; // Apply each upgrade effect for (const upgrade of activeUpgrades) { const { effect } = upgrade; if (effect.type === 'multiplier' && effect.stat && effect.value !== undefined) { // Multiplier effects (multiply the stat) switch (effect.stat) { case 'maxMana': effects.maxManaMultiplier *= effect.value; break; case 'regen': effects.regenMultiplier *= effect.value; break; case 'clickMana': effects.clickManaMultiplier *= effect.value; break; case 'meditationEfficiency': effects.meditationEfficiency *= effect.value; break; case 'spellCost': effects.spellCostMultiplier *= effect.value; break; case 'conversionEfficiency': effects.conversionEfficiency *= effect.value; break; case 'baseDamage': effects.baseDamageMultiplier *= effect.value; break; case 'attackSpeed': effects.attackSpeedMultiplier *= effect.value; break; case 'elementalDamage': effects.elementalDamageMultiplier *= effect.value; break; case 'studySpeed': effects.studySpeedMultiplier *= effect.value; break; case 'elementCap': effects.elementCapMultiplier *= effect.value; break; case 'conversionCost': effects.conversionCostMultiplier *= effect.value; break; case 'costReduction': // For cost reduction, higher is better (less cost) // This is a multiplier on the reduction effectiveness effects.studyCostMultiplier /= effect.value; break; } } else if (effect.type === 'bonus' && effect.stat && effect.value !== undefined) { // Bonus effects (add to the stat) switch (effect.stat) { case 'maxMana': effects.maxManaBonus += effect.value; break; case 'regen': effects.regenBonus += effect.value; break; case 'clickMana': effects.clickManaBonus += effect.value; break; case 'baseDamage': effects.baseDamageBonus += effect.value; break; case 'elementCap': effects.elementCapBonus += effect.value; break; case 'permanentRegen': effects.permanentRegenBonus += effect.value; break; } } else if (effect.type === 'special' && effect.specialId) { // Special effects - add to the set for game logic to check effects.specials.add(effect.specialId); } } return effects; } /** * Check if a special effect is active */ export function hasSpecial(effects: ComputedEffects, specialId: string): boolean { return effects?.specials?.has(specialId) ?? false; } /** * Compute regen with special effects that depend on dynamic values */ export function computeDynamicRegen( effects: ComputedEffects, baseRegen: number, maxMana: number, currentMana: number, incursionStrength: number ): number { let regen = baseRegen; // Mana Cascade: +0.1 regen per 100 max mana if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_CASCADE)) { regen += Math.floor(maxMana / 100) * 0.1; } // Mana Torrent: +50% regen when above 75% mana if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_TORRENT) && currentMana > maxMana * 0.75) { regen *= 1.5; } // Desperate Wells: +50% regen when below 25% mana if (hasSpecial(effects, SPECIAL_EFFECTS.DESPERATE_WELLS) && currentMana < maxMana * 0.25) { regen *= 1.5; } // Steady Stream: Regen immune to incursion if (hasSpecial(effects, SPECIAL_EFFECTS.STEADY_STREAM)) { return regen * effects.regenMultiplier; } // Apply incursion penalty regen *= (1 - incursionStrength); return regen * effects.regenMultiplier; } /** * Compute click mana with special effects */ export function computeDynamicClickMana( effects: ComputedEffects, baseClickMana: number ): number { let clickMana = baseClickMana; // Mana Echo: 10% chance to gain double mana from clicks // Note: The chance is handled in the click handler, this just returns the base // The click handler should check hasSpecial and apply the 10% chance return Math.floor((clickMana + effects.clickManaBonus) * effects.clickManaMultiplier); } /** * Compute damage with special effects */ export function computeDynamicDamage( effects: ComputedEffects, baseDamage: number, floorHPPct: number, currentMana: number, maxMana: number ): number { let damage = baseDamage * effects.baseDamageMultiplier; // Overpower: +50% damage when mana above 80% if (hasSpecial(effects, SPECIAL_EFFECTS.OVERPOWER) && currentMana >= maxMana * 0.8) { damage *= 1.5; } // Berserker: +50% damage when below 50% mana if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && currentMana < maxMana * 0.5) { damage *= 1.5; } // Combo Master: Every 5th attack deals 3x damage // Note: The hit counter is tracked in game state, this just returns the multiplier // The combat handler should check hasSpecial and the hit count return damage + effects.baseDamageBonus; }