Skip to main content
Glama

Path of Exile 2 Build Optimizer MCP

spell_dps_calculator.py16.7 kB
""" Path of Exile 2 Spell DPS Calculator Calculates accurate spell DPS using PoE2 damage formulas Based on comprehensive research from: - poewiki.net - poe2db.tw - maxroll.gg - mobalytics.gg """ from typing import Dict, List, Optional, Any from dataclasses import dataclass import logging logger = logging.getLogger(__name__) @dataclass class SpellStats: """Spell base statistics""" name: str base_damage_min: float base_damage_max: float damage_effectiveness: float = 1.0 # 100% by default base_crit_chance: float = 0.0 # 0-100% base_cast_time: float = 1.0 # seconds damage_types: List[str] = None # ['fire', 'cold', 'lightning', etc.] def __post_init__(self) -> None: if self.damage_types is None: self.damage_types = [] @dataclass class CharacterModifiers: """Character damage modifiers""" # Increased/Decreased (additive) increased_spell_damage: float = 0.0 # Sum of all % increased increased_cast_speed: float = 0.0 increased_crit_damage: float = 0.0 # More/Less (multiplicative) more_multipliers: List[float] = None # List of more % values (e.g., [25, 30, 20]) # Added damage (flat) added_fire: float = 0.0 added_cold: float = 0.0 added_lightning: float = 0.0 added_chaos: float = 0.0 added_physical: float = 0.0 # Critical stats added_crit_bonus: float = 100.0 # PoE2 base: +100% increased_crit_chance: float = 0.0 # Archmage maximum_mana: float = 0.0 has_archmage: bool = False def __post_init__(self) -> None: if self.more_multipliers is None: self.more_multipliers = [] @dataclass class EnemyStats: """Enemy defensive stats""" fire_resistance: float = 0.0 # 0-100 cold_resistance: float = 0.0 lightning_resistance: float = 0.0 chaos_resistance: float = 0.0 physical_resistance: float = 0.0 # Debuffs (negative resistances) fire_exposure: float = 0.0 # e.g., 20 for -20% res cold_exposure: float = 0.0 lightning_exposure: float = 0.0 # Penetration fire_penetration: float = 0.0 cold_penetration: float = 0.0 lightning_penetration: float = 0.0 # Modifiers is_shocked: bool = False # 20% more damage taken class SpellDPSCalculator: """ Calculates spell DPS using PoE2 formulas Formula: Final Damage = (Base + Added × Effectiveness) × (1 + ΣIncreased) × Π(1 + More) × CritMultiplier × (1 - EffectiveRes) """ # Spell database (expandable) SPELL_DATABASE = { "arc": SpellStats( name="Arc", base_damage_min=10.0, base_damage_max=100.0, # Placeholder - varies by gem level damage_effectiveness=1.0, base_crit_chance=5.0, base_cast_time=0.8, damage_types=["lightning"] ), "spark": SpellStats( name="Spark", base_damage_min=10.0, base_damage_max=100.0, damage_effectiveness=1.0, base_crit_chance=9.0, base_cast_time=0.7, damage_types=["lightning"] ), "fireball": SpellStats( name="Fireball", base_damage_min=20.0, base_damage_max=120.0, damage_effectiveness=1.0, base_crit_chance=6.0, base_cast_time=0.9, damage_types=["fire"] ), # Add more spells as needed } def calculate_dps( self, spell: SpellStats, char_mods: CharacterModifiers, enemy: Optional[EnemyStats] = None ) -> Dict[str, Any]: """ Calculate complete spell DPS Args: spell: Spell base statistics char_mods: Character modifiers enemy: Enemy stats (if None, assumes no resistance) Returns: Dictionary with DPS breakdown """ if enemy is None: enemy = EnemyStats() try: # Step 1: Calculate base damage base_damage = (spell.base_damage_min + spell.base_damage_max) / 2 # Step 2: Add flat damage (with damage effectiveness) added_damage = self._calculate_added_damage(spell, char_mods) total_base_damage = base_damage + added_damage # Step 3: Archmage scaling (if applicable) if char_mods.has_archmage: archmage_bonus = self._calculate_archmage_bonus( char_mods.maximum_mana, total_base_damage ) total_base_damage += archmage_bonus # Step 4: Apply increased/decreased (single additive sum) increased_multiplier = 1.0 + (char_mods.increased_spell_damage / 100.0) damage_after_increased = total_base_damage * increased_multiplier # Step 5: Apply more/less (multiplicative stack) more_multiplier = self._calculate_more_multiplier(char_mods.more_multipliers) damage_after_more = damage_after_increased * more_multiplier # Step 6: Calculate expected damage with crits crit_chance = min(spell.base_crit_chance + char_mods.increased_crit_chance, 100.0) / 100.0 crit_multiplier = self._calculate_crit_multiplier( char_mods.added_crit_bonus, char_mods.increased_crit_damage ) non_crit_damage = damage_after_more * (1.0 - crit_chance) crit_damage = damage_after_more * crit_multiplier * crit_chance expected_hit_damage = non_crit_damage + crit_damage # Step 7: Apply resistance/penetration damage_after_resistance = self._apply_resistances( expected_hit_damage, spell.damage_types, enemy ) # Step 8: Apply Shock if applicable if enemy.is_shocked: damage_after_resistance *= 1.2 # 20% more damage # Step 9: Calculate DPS (damage × casts per second) cast_speed = self._calculate_cast_speed( spell.base_cast_time, char_mods.increased_cast_speed ) dps = damage_after_resistance * cast_speed return { "total_dps": round(dps, 2), "average_hit": round(damage_after_resistance, 2), "casts_per_second": round(cast_speed, 3), "crit_chance": round(crit_chance * 100, 2), "breakdown": { "base_damage": round(base_damage, 2), "added_damage": round(added_damage, 2), "after_increased": round(damage_after_increased, 2), "after_more": round(damage_after_more, 2), "expected_hit": round(expected_hit_damage, 2), "after_resistance": round(damage_after_resistance, 2), "multipliers": { "increased": round(increased_multiplier, 3), "more": round(more_multiplier, 3), "crit": round(crit_multiplier, 3) if crit_chance > 0 else 1.0 } } } except Exception as e: logger.error(f"Error calculating DPS for {spell.name}: {e}", exc_info=True) return { "total_dps": 0, "average_hit": 0, "casts_per_second": 0, "error": str(e) } def _calculate_added_damage(self, spell: SpellStats, char_mods: CharacterModifiers) -> float: """Calculate added damage with damage effectiveness applied. Sums all sources of added flat damage (fire, cold, lightning, chaos, physical) and multiplies by the spell's damage effectiveness. Args: spell: Spell statistics including damage effectiveness char_mods: Character modifiers with added flat damage values Returns: Total added damage after applying damage effectiveness Formula: (Added Fire + Cold + Lightning + Chaos + Physical) × Damage Effectiveness Examples: >>> spell = SpellStats(name="Test", base_damage_min=10, base_damage_max=20, damage_effectiveness=0.8) >>> char_mods = CharacterModifiers(added_fire=50, added_cold=30) >>> calc = SpellDPSCalculator() >>> calc._calculate_added_damage(spell, char_mods) 64.0 """ total_added = ( char_mods.added_fire + char_mods.added_cold + char_mods.added_lightning + char_mods.added_chaos + char_mods.added_physical ) return total_added * spell.damage_effectiveness def _calculate_archmage_bonus(self, max_mana: float, base_damage: float) -> float: """Calculate bonus lightning damage from Archmage support. Archmage adds lightning damage based on maximum mana. The bonus scales at 4% of base damage per 100 maximum mana. Args: max_mana: Character's maximum mana pool base_damage: Base spell damage before modifiers Returns: Additional lightning damage from Archmage, or 0.0 if no mana Formula: Archmage Bonus = (Max Mana / 100) × 0.04 × Base Damage Examples: >>> calc = SpellDPSCalculator() >>> calc._calculate_archmage_bonus(2000, 100) 80.0 >>> calc._calculate_archmage_bonus(0, 100) 0.0 """ if max_mana <= 0: return 0.0 archmage_multiplier = (max_mana / 100.0) * 0.04 # 4% per 100 mana return base_damage * archmage_multiplier def _calculate_more_multiplier(self, more_multipliers: List[float]) -> float: """Calculate total multiplier from all 'more' damage modifiers. 'More' modifiers stack multiplicatively, unlike 'increased' modifiers which stack additively. Each modifier is applied sequentially. Args: more_multipliers: List of more damage percentages (e.g., [25, 30, 20] for +25%, +30%, +20%) Returns: Total multiplicative damage multiplier Formula: Total = (1 + More1/100) × (1 + More2/100) × (1 + More3/100) × ... Examples: >>> calc = SpellDPSCalculator() >>> calc._calculate_more_multiplier([25, 30]) 1.625 >>> calc._calculate_more_multiplier([]) 1.0 """ total = 1.0 for more_percent in more_multipliers: total *= (1.0 + more_percent / 100.0) return total def _calculate_crit_multiplier(self, added_crit_bonus: float, increased_crit: float) -> float: """Calculate critical strike damage multiplier using PoE2 system. In PoE2, critical strikes have a base +100% damage bonus (200% total). The base bonus can be increased through modifiers. Args: added_crit_bonus: Base crit damage bonus (default 100 for PoE2) plus any flat additions increased_crit: Percentage of increased critical strike damage Returns: Total critical strike damage multiplier Formula: Crit Multiplier = 1 + (Added Bonus / 100) × (1 + Increased / 100) Examples: >>> calc = SpellDPSCalculator() >>> calc._calculate_crit_multiplier(100, 0) 2.0 >>> calc._calculate_crit_multiplier(100, 50) 2.5 """ # PoE2 base is +100% (200% total damage on crit) total_bonus = added_crit_bonus / 100.0 # Convert % to decimal increased_mult = 1.0 + (increased_crit / 100.0) return 1.0 + (total_bonus * increased_mult) def _apply_resistances( self, damage: float, damage_types: List[str], enemy: EnemyStats ) -> float: """Apply enemy resistances, exposure, and penetration to damage. Calculates effective resistance after applying exposure (which can go negative) and penetration (which cannot reduce resistance below 0%). Args: damage: Incoming damage before resistance mitigation damage_types: List of damage types (uses first element as primary type) enemy: Enemy defensive statistics Returns: Final damage after resistance mitigation Formula: Effective Resistance = max((Base Resistance - Exposure) - Penetration, 0) Final Damage = Damage × (1 - Effective Resistance / 100) Examples: >>> calc = SpellDPSCalculator() >>> enemy = EnemyStats(fire_resistance=75, fire_exposure=20, fire_penetration=10) >>> calc._apply_resistances(100, ["fire"], enemy) 55.0 """ if not damage_types: return damage # Get primary damage type (first in list) primary_type = damage_types[0].lower() # Get base resistance resistance_map = { "fire": (enemy.fire_resistance, enemy.fire_exposure, enemy.fire_penetration), "cold": (enemy.cold_resistance, enemy.cold_exposure, enemy.cold_penetration), "lightning": (enemy.lightning_resistance, enemy.lightning_exposure, enemy.lightning_penetration), "chaos": (enemy.chaos_resistance, 0, 0), "physical": (enemy.physical_resistance, 0, 0) } if primary_type not in resistance_map: return damage base_res, exposure, penetration = resistance_map[primary_type] # Step 1: Apply exposure (can go negative) res_after_exposure = base_res - exposure # Step 2: Apply penetration (cannot go below 0%) effective_resistance = max(res_after_exposure - penetration, 0.0) # Calculate damage multiplier damage_multiplier = 1.0 - (effective_resistance / 100.0) return damage * damage_multiplier def _calculate_cast_speed(self, base_cast_time: float, increased_cast_speed: float) -> float: """Calculate casts per second from base cast time and modifiers. Applies increased cast speed modifiers to reduce the actual cast time, then converts to casts per second. Args: base_cast_time: Base time per cast in seconds (from spell gem) increased_cast_speed: Total percentage of increased cast speed Returns: Number of casts per second Formula: Actual Cast Time = Base Cast Time / (1 + Increased Cast Speed / 100) Casts Per Second = 1 / Actual Cast Time Examples: >>> calc = SpellDPSCalculator() >>> calc._calculate_cast_speed(0.8, 50) 1.875 >>> calc._calculate_cast_speed(1.0, 0) 1.0 """ cast_speed_multiplier = 1.0 + (increased_cast_speed / 100.0) actual_cast_time = base_cast_time / cast_speed_multiplier return 1.0 / actual_cast_time def get_spell_by_name(self, spell_name: str) -> Optional[SpellStats]: """Retrieve spell statistics from the spell database. Performs case-insensitive lookup of spell data from the internal database. Args: spell_name: Name of the spell to look up (case-insensitive) Returns: SpellStats object if found, None otherwise Examples: >>> calc = SpellDPSCalculator() >>> arc = calc.get_spell_by_name("Arc") >>> arc.name 'Arc' >>> calc.get_spell_by_name("NonexistentSpell") is None True """ return self.SPELL_DATABASE.get(spell_name.lower()) def add_spell_to_database(self, spell: SpellStats) -> None: """Add or update a spell in the spell database. Stores spell statistics in the internal database for later lookup. If a spell with the same name exists, it will be overwritten. Args: spell: SpellStats object to add to the database Examples: >>> calc = SpellDPSCalculator() >>> new_spell = SpellStats( ... name="CustomSpell", ... base_damage_min=50, ... base_damage_max=100, ... damage_effectiveness=1.2 ... ) >>> calc.add_spell_to_database(new_spell) >>> calc.get_spell_by_name("CustomSpell").name 'CustomSpell' """ self.SPELL_DATABASE[spell.name.lower()] = spell

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/HivemindOverlord/poe2-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server