Skip to main content
Glama

Path of Exile 2 Build Optimizer MCP

ehp_calculator.py40.8 kB
""" Effective Health Pool (EHP) Calculator for Path of Exile 2 This module calculates layered defense effectiveness - the single most important metric for build survivability. EHP represents the total raw damage a character can sustain before dying, accounting for ALL defensive layers. Key PoE2 Mechanics: ================== 1. Defense Layer Order (CRITICAL - different from PoE1): - Evasion (chance to avoid hit entirely) - Block (chance to completely block hit) - Armor (reduces physical damage BEFORE resistances - reversed from PoE1!) - Resistances (elemental/chaos damage reduction) - Hit taken on Life/ES pool 2. PoE2-Specific Changes: - Block cap: 50% (not 75% like PoE1) - Chaos damage: Removes 2× ES (not bypass entirely like PoE1) - Armor applies BEFORE resistances for physical hits - Hit size matters: Armor is more effective vs smaller hits 3. EHP Theory: EHP = Raw_HP / (1 - Total_Mitigation) Where Total_Mitigation layers multiplicatively: - Evasion: 30% evade = 0.30 mitigation - Block: 40% block = 0.40 mitigation - Together: 1 - (1 - 0.30) × (1 - 0.40) = 0.58 = 58% total mitigation Author: Claude Date: 2025-10-22 """ import logging from dataclasses import dataclass, field from typing import Dict, List, Optional, Tuple, Any from enum import Enum # Import existing calculators from .defense_calculator import DefenseCalculator, DefenseConstants from .resource_calculator import ResourceCalculator, AttributeStats logger = logging.getLogger(__name__) class DamageType(Enum): """Types of damage in Path of Exile 2.""" PHYSICAL = "physical" FIRE = "fire" COLD = "cold" LIGHTNING = "lightning" CHAOS = "chaos" @dataclass class DefensiveStats: """ Complete defensive statistics for EHP calculation. Attributes: life: Total life pool energy_shield: Total energy shield pool armor: Armor rating (for physical damage reduction) evasion: Evasion rating (for hit avoidance) block_chance: Block chance % (capped at 50% in PoE2) fire_res: Fire resistance % cold_res: Cold resistance % lightning_res: Lightning resistance % chaos_res: Chaos resistance % phys_taken_as_elemental: % of physical taken as elemental (reduces armor effectiveness) """ life: float energy_shield: float = 0.0 armor: float = 0.0 evasion: float = 0.0 block_chance: float = 0.0 fire_res: float = 0.0 cold_res: float = 0.0 lightning_res: float = 0.0 chaos_res: float = 0.0 phys_taken_as_elemental: float = 0.0 def __post_init__(self): """Validate defensive stats.""" if self.life < 0: raise ValueError("Life cannot be negative") if self.energy_shield < 0: raise ValueError("Energy Shield cannot be negative") if self.life + self.energy_shield == 0: raise ValueError("Must have some life or energy shield") @dataclass class ThreatProfile: """ Defines the incoming threat for EHP calculation. Hit size is CRITICAL for armor effectiveness - armor provides more DR against smaller hits and less against larger hits. Attributes: expected_hit_size: Expected raw damage per hit (for armor calculation) attacker_accuracy: Attacker accuracy (for evasion calculation) """ expected_hit_size: float = 1000.0 # Default: moderate hit attacker_accuracy: float = 2000.0 # Default: moderate accuracy @dataclass class EHPResult: """ Result of EHP calculation for a specific damage type. Attributes: damage_type: Type of damage raw_hp: Total life + ES pool evade_mitigation: Mitigation from evasion (0-1) block_mitigation: Mitigation from block (0-1) armor_dr: Damage reduction from armor % (physical only) resistance_dr: Damage reduction from resistance % total_mitigation: Combined mitigation from all layers (0-1) effective_hp: Final EHP value layers_breakdown: Detailed breakdown of each layer's contribution """ damage_type: DamageType raw_hp: float evade_mitigation: float block_mitigation: float armor_dr: float resistance_dr: float total_mitigation: float effective_hp: float layers_breakdown: Dict[str, Any] = field(default_factory=dict) @dataclass class DefenseGap: """ Identifies a defensive weakness. Attributes: gap_type: Type of gap (e.g., "low_resistance", "no_evasion") severity: Severity rating (0-10, where 10 is critical) description: Human-readable description recommendation: Suggested fix current_value: Current value of the defense recommended_value: Recommended target value """ gap_type: str severity: float description: str recommendation: str current_value: float recommended_value: float class EHPCalculator: """ Main EHP calculator implementing PoE2 layered defense mechanics. This calculator is essential for: 1. Build evaluation - comparing defensive effectiveness 2. Gear upgrade analysis - "is this item worth it?" 3. Gap identification - finding defensive weaknesses 4. Content viability - "can I survive endgame?" """ def __init__(self): """Initialize EHP calculator with defense calculator.""" self.defense_calc = DefenseCalculator() logger.info("EHPCalculator initialized for Path of Exile 2") # ============================================================================ # CORE EHP CALCULATIONS # ============================================================================ def calculate_ehp( self, stats: DefensiveStats, damage_type: DamageType, threat: ThreatProfile ) -> EHPResult: """ Calculate Effective Health Pool for a specific damage type. This is the core function that layers all defenses according to PoE2 mechanics. Layer Order (PoE2): 1. Evasion (avoidance) 2. Block (avoidance) 3. Armor (physical DR) - BEFORE resistances in PoE2! 4. Resistances (elemental/chaos DR) 5. Damage taken from Life/ES pool Args: stats: Character defensive statistics damage_type: Type of incoming damage threat: Threat profile (hit size, accuracy) Returns: EHPResult with detailed breakdown Example: >>> calc = EHPCalculator() >>> stats = DefensiveStats(life=5000, energy_shield=2000, armor=10000, ... fire_res=75, block_chance=40) >>> threat = ThreatProfile(expected_hit_size=1000, attacker_accuracy=2000) >>> result = calc.calculate_ehp(stats, DamageType.FIRE, threat) >>> print(f"Fire EHP: {result.effective_hp:.0f}") """ logger.debug(f"Calculating EHP for {damage_type.value} damage") # Step 1: Calculate raw HP pool raw_hp = self._calculate_raw_hp(stats, damage_type) # Step 2: Calculate evasion mitigation (applies to all damage) evade_mitigation = self._calculate_evasion_mitigation(stats, threat) # Step 3: Calculate block mitigation (applies to all damage) block_mitigation = self._calculate_block_mitigation(stats) # Step 4: Calculate armor DR (physical only, BEFORE resistances) armor_dr = 0.0 if damage_type == DamageType.PHYSICAL: armor_dr = self._calculate_armor_dr(stats, threat) # Step 5: Calculate resistance DR resistance_dr = self._calculate_resistance_dr(stats, damage_type) # Step 6: Layer all defenses multiplicatively # Formula: Total_Mitigation = 1 - (1 - evade) × (1 - block) × (1 - armor) × (1 - res) hit_multiplier = (1 - evade_mitigation) * (1 - block_mitigation) # For physical: armor applies before resistance if damage_type == DamageType.PHYSICAL: damage_multiplier = hit_multiplier * (1 - armor_dr) * (1 - resistance_dr) else: damage_multiplier = hit_multiplier * (1 - resistance_dr) total_mitigation = 1 - damage_multiplier # Step 7: Calculate EHP if damage_multiplier > 0: effective_hp = raw_hp / damage_multiplier else: effective_hp = float('inf') # Build detailed breakdown layers_breakdown = { 'raw_hp': raw_hp, 'evasion': { 'mitigation_percent': evade_mitigation * 100, 'multiplier': 1 / (1 - evade_mitigation) if evade_mitigation < 1 else float('inf') }, 'block': { 'chance_percent': stats.block_chance, 'effective_chance_percent': min(stats.block_chance, DefenseConstants.BLOCK_MAX_CHANCE), 'mitigation_percent': block_mitigation * 100, 'multiplier': 1 / (1 - block_mitigation) if block_mitigation < 1 else float('inf') }, 'armor': { 'rating': stats.armor, 'dr_percent': armor_dr * 100, 'vs_hit_size': threat.expected_hit_size, 'multiplier': 1 / (1 - armor_dr) if armor_dr < 1 else float('inf') } if damage_type == DamageType.PHYSICAL else None, 'resistance': { 'value_percent': self._get_resistance_value(stats, damage_type), 'dr_percent': resistance_dr * 100, 'multiplier': 1 / (1 - resistance_dr) if resistance_dr < 1 else float('inf') }, 'combined': { 'total_mitigation_percent': total_mitigation * 100, 'total_multiplier': effective_hp / raw_hp if raw_hp > 0 else 0 } } result = EHPResult( damage_type=damage_type, raw_hp=raw_hp, evade_mitigation=evade_mitigation, block_mitigation=block_mitigation, armor_dr=armor_dr, resistance_dr=resistance_dr, total_mitigation=total_mitigation, effective_hp=effective_hp, layers_breakdown=layers_breakdown ) logger.info( f"{damage_type.value.upper()} EHP: {raw_hp:.0f} raw HP -> " f"{effective_hp:.0f} EHP ({total_mitigation*100:.1f}% mitigation)" ) return result def calculate_all_ehp( self, stats: DefensiveStats, threat: ThreatProfile ) -> Dict[DamageType, EHPResult]: """ Calculate EHP for all damage types. This gives a complete picture of defensive effectiveness. Args: stats: Character defensive statistics threat: Threat profile Returns: Dictionary mapping damage types to EHP results Example: >>> calc = EHPCalculator() >>> stats = DefensiveStats(life=5000, armor=10000, fire_res=75, cold_res=75) >>> threat = ThreatProfile(expected_hit_size=1000) >>> results = calc.calculate_all_ehp(stats, threat) >>> for dmg_type, result in results.items(): ... print(f"{dmg_type.value}: {result.effective_hp:.0f} EHP") """ results = {} for damage_type in DamageType: results[damage_type] = self.calculate_ehp(stats, damage_type, threat) logger.info("Calculated EHP for all damage types") return results # ============================================================================ # HIT SIZE ANALYSIS # ============================================================================ def analyze_armor_vs_hit_sizes( self, stats: DefensiveStats, hit_sizes: Optional[List[float]] = None ) -> Dict[float, Dict[str, float]]: """ Analyze armor effectiveness against different hit sizes. This is CRITICAL in PoE2 because armor is more effective against many small hits than few large hits. Args: stats: Character defensive statistics hit_sizes: List of hit sizes to test (default: common values) Returns: Dictionary mapping hit sizes to armor effectiveness data Example: >>> calc = EHPCalculator() >>> stats = DefensiveStats(life=5000, armor=20000) >>> analysis = calc.analyze_armor_vs_hit_sizes(stats) >>> for hit_size, data in analysis.items(): ... print(f"{hit_size} damage: {data['dr_percent']:.1f}% DR") """ if hit_sizes is None: # Default hit sizes: small, medium, large, huge hit_sizes = [500, 1000, 2000, 5000, 10000] analysis = {} for hit_size in hit_sizes: armor_result = self.defense_calc.calculate_armor_dr(stats.armor, hit_size) # Calculate EHP for this hit size threat = ThreatProfile(expected_hit_size=hit_size, attacker_accuracy=2000) ehp_result = self.calculate_ehp( stats, DamageType.PHYSICAL, threat ) analysis[hit_size] = { 'armor_rating': stats.armor, 'dr_percent': armor_result.damage_reduction_percent, 'effective_damage': armor_result.effective_damage, 'is_capped': armor_result.is_capped, 'physical_ehp': ehp_result.effective_hp, 'ehp_per_1000_armor': (ehp_result.effective_hp / (stats.armor / 1000)) if stats.armor > 0 else 0 } logger.info(f"Analyzed armor effectiveness against {len(hit_sizes)} hit sizes") return analysis def find_armor_breakpoints( self, stats: DefensiveStats, target_dr_values: Optional[List[float]] = None ) -> Dict[float, float]: """ Find armor values needed to achieve target DR against expected hit size. Useful for gear planning: "How much armor do I need for 50% DR?" Args: stats: Character defensive statistics (for context) target_dr_values: List of target DR % values (default: 25, 50, 75) Returns: Dictionary mapping target DR to required armor Example: >>> calc = EHPCalculator() >>> stats = DefensiveStats(life=5000) >>> threat = ThreatProfile(expected_hit_size=2000) >>> breakpoints = calc.find_armor_breakpoints(stats) >>> for dr, armor in breakpoints.items(): ... print(f"{dr}% DR needs {armor:.0f} armor") """ if target_dr_values is None: target_dr_values = [25.0, 50.0, 75.0, 85.0, 90.0] # Use default threat profile's hit size threat = ThreatProfile() hit_size = threat.expected_hit_size breakpoints = {} for target_dr in target_dr_values: if target_dr >= DefenseConstants.ARMOR_MAX_DR: breakpoints[target_dr] = float('inf') else: armor_needed = self.defense_calc.armor_needed_for_dr(target_dr, hit_size) breakpoints[target_dr] = armor_needed logger.info(f"Calculated armor breakpoints for {len(target_dr_values)} DR targets vs {hit_size} hit size") return breakpoints # ============================================================================ # DEFENSE GAP IDENTIFICATION # ============================================================================ def identify_defense_gaps( self, stats: DefensiveStats, threat: ThreatProfile ) -> List[DefenseGap]: """ Identify defensive weaknesses and provide recommendations. This is invaluable for build improvement - shows exactly where your defenses are weak and how to fix them. Checks: 1. Uncapped resistances (critical!) 2. Low HP pool (< 3000 life+ES) 3. No layered defenses (only HP) 4. Block over-investment (>50% wasted) 5. Armor ineffectiveness (poor vs large hits) 6. Chaos resistance gap (common oversight) Args: stats: Character defensive statistics threat: Threat profile Returns: List of DefenseGap objects, sorted by severity (highest first) Example: >>> calc = EHPCalculator() >>> stats = DefensiveStats(life=3000, fire_res=50, chaos_res=-60) >>> gaps = calc.identify_defense_gaps(stats, ThreatProfile()) >>> for gap in gaps: ... print(f"[{gap.severity}/10] {gap.description}") """ gaps = [] # Check 1: Resistance caps (CRITICAL) resistances = { 'fire': (stats.fire_res, DamageType.FIRE), 'cold': (stats.cold_res, DamageType.COLD), 'lightning': (stats.lightning_res, DamageType.LIGHTNING), 'chaos': (stats.chaos_res, DamageType.CHAOS) } for res_name, (res_value, dmg_type) in resistances.items(): cap = DefenseConstants.RESISTANCE_DEFAULT_CAP if res_value < cap: deficit = cap - res_value severity = min(10.0, (deficit / 10.0)) # 10% deficit = 1 severity # Chaos res is less critical if res_name == 'chaos': severity *= 0.5 gaps.append(DefenseGap( gap_type=f"uncapped_{res_name}_resistance", severity=severity, description=f"{res_name.capitalize()} resistance is {deficit:.0f}% below cap", recommendation=f"Increase {res_name} resistance by {deficit:.0f}% to reach {cap}% cap", current_value=res_value, recommended_value=cap )) # Check 2: HP pool (life + ES) total_hp = stats.life + stats.energy_shield min_hp_endgame = 3000.0 if total_hp < min_hp_endgame: deficit = min_hp_endgame - total_hp severity = min(10.0, (deficit / 500.0)) # 500 HP deficit = 1 severity gaps.append(DefenseGap( gap_type="low_hp_pool", severity=severity, description=f"Total HP pool ({total_hp:.0f}) is below recommended minimum ({min_hp_endgame:.0f})", recommendation=f"Increase life and/or energy shield by {deficit:.0f} total", current_value=total_hp, recommended_value=min_hp_endgame )) # Check 3: No layered defenses has_armor = stats.armor >= 5000 has_evasion = stats.evasion >= 3000 has_block = stats.block_chance >= 20 has_es = stats.energy_shield >= 500 defense_layers = sum([has_armor, has_evasion, has_block, has_es]) if defense_layers == 0: gaps.append(DefenseGap( gap_type="no_layered_defenses", severity=8.0, description="No meaningful layered defenses (only relying on resistances and HP)", recommendation="Invest in at least one additional defense layer: armor, evasion, block, or ES", current_value=0, recommended_value=1 )) elif defense_layers == 1: gaps.append(DefenseGap( gap_type="single_defense_layer", severity=4.0, description="Only one defense layer present", recommendation="Consider adding a second defense layer for better survivability", current_value=1, recommended_value=2 )) # Check 4: Block over-investment if stats.block_chance > DefenseConstants.BLOCK_MAX_CHANCE: waste = stats.block_chance - DefenseConstants.BLOCK_MAX_CHANCE severity = min(5.0, waste / 10.0) gaps.append(DefenseGap( gap_type="overcapped_block", severity=severity, description=f"Block chance ({stats.block_chance:.0f}%) exceeds cap ({DefenseConstants.BLOCK_MAX_CHANCE}%), wasting {waste:.0f}%", recommendation=f"Reallocate {waste:.0f}% block chance to other defenses", current_value=stats.block_chance, recommended_value=DefenseConstants.BLOCK_MAX_CHANCE )) # Check 5: Armor effectiveness vs large hits if stats.armor > 0: large_hit = 5000.0 armor_result = self.defense_calc.calculate_armor_dr(stats.armor, large_hit) if armor_result.damage_reduction_percent < 30.0: severity = 5.0 gaps.append(DefenseGap( gap_type="armor_ineffective_vs_large_hits", severity=severity, description=f"Armor provides only {armor_result.damage_reduction_percent:.1f}% DR against large hits ({large_hit:.0f} damage)", recommendation="Consider supplementing armor with other defenses for one-shot protection", current_value=armor_result.damage_reduction_percent, recommended_value=50.0 )) # Check 6: Chaos resistance (special case - common oversight) if stats.chaos_res < 0: severity = min(6.0, abs(stats.chaos_res) / 20.0) gaps.append(DefenseGap( gap_type="negative_chaos_resistance", severity=severity, description=f"Negative chaos resistance ({stats.chaos_res:.0f}%) amplifies chaos damage", recommendation=f"Increase chaos resistance by {abs(stats.chaos_res):.0f}% to reach 0% minimum", current_value=stats.chaos_res, recommended_value=0.0 )) # Check 7: Evasion with no accuracy check if stats.evasion > 0: evasion_result = self.defense_calc.calculate_evasion_chance( stats.evasion, threat.attacker_accuracy ) if evasion_result.evade_chance_percent < 30.0: severity = 3.0 gaps.append(DefenseGap( gap_type="low_evasion_effectiveness", severity=severity, description=f"Evasion provides only {evasion_result.evade_chance_percent:.1f}% evade chance vs expected accuracy", recommendation="Increase evasion rating or consider alternative defenses", current_value=evasion_result.evade_chance_percent, recommended_value=50.0 )) # Sort by severity (highest first) gaps.sort(key=lambda g: g.severity, reverse=True) logger.info(f"Identified {len(gaps)} defense gaps") return gaps # ============================================================================ # COMPARISON & OPTIMIZATION # ============================================================================ def compare_upgrade( self, current_stats: DefensiveStats, upgraded_stats: DefensiveStats, threat: ThreatProfile ) -> Dict[str, Any]: """ Compare EHP before and after a gear/passive upgrade. Essential for answering: "Is this upgrade worth it?" Args: current_stats: Current defensive statistics upgraded_stats: Statistics after upgrade threat: Threat profile Returns: Dictionary with comparison data for all damage types Example: >>> calc = EHPCalculator() >>> current = DefensiveStats(life=5000, armor=10000, fire_res=70) >>> upgraded = DefensiveStats(life=5000, armor=10000, fire_res=75) >>> comparison = calc.compare_upgrade(current, upgraded, ThreatProfile()) >>> print(f"Fire EHP gain: +{comparison['fire']['absolute_gain']:.0f}") """ current_ehp = self.calculate_all_ehp(current_stats, threat) upgraded_ehp = self.calculate_all_ehp(upgraded_stats, threat) comparison = {} for damage_type in DamageType: current = current_ehp[damage_type] upgraded = upgraded_ehp[damage_type] absolute_gain = upgraded.effective_hp - current.effective_hp if current.effective_hp > 0: percent_gain = (absolute_gain / current.effective_hp) * 100 else: percent_gain = float('inf') if absolute_gain > 0 else 0 comparison[damage_type.value] = { 'current_ehp': current.effective_hp, 'upgraded_ehp': upgraded.effective_hp, 'absolute_gain': absolute_gain, 'percent_gain': percent_gain, 'mitigation_increase': (upgraded.total_mitigation - current.total_mitigation) * 100 } # Calculate average improvement valid_gains = [ data['percent_gain'] for data in comparison.values() if data['percent_gain'] != float('inf') ] comparison['summary'] = { 'average_percent_gain': sum(valid_gains) / len(valid_gains) if valid_gains else 0, 'worst_case_type': min(comparison.keys(), key=lambda k: comparison[k]['upgraded_ehp']), 'best_case_type': max(comparison.keys(), key=lambda k: comparison[k]['upgraded_ehp']) } logger.info( f"Upgrade comparison: average {comparison['summary']['average_percent_gain']:.1f}% EHP gain" ) return comparison def calculate_defense_value( self, stats: DefensiveStats, defense_type: str, increase_amount: float, threat: ThreatProfile, target_damage_type: Optional[DamageType] = None ) -> Dict[str, float]: """ Calculate how much EHP gain you'd get from increasing a specific defense. Answers questions like: - "How much EHP would +1000 armor give me?" - "Is +5% fire res better than +500 life?" Args: stats: Current defensive statistics defense_type: Type of defense to increase ('armor', 'life', 'fire_res', etc.) increase_amount: Amount to increase by threat: Threat profile target_damage_type: Specific damage type to check (None = check all) Returns: Dictionary with EHP gain data Example: >>> calc = EHPCalculator() >>> stats = DefensiveStats(life=5000, armor=10000, fire_res=70) >>> value = calc.calculate_defense_value(stats, 'armor', 2000, ThreatProfile()) >>> print(f"+2000 armor = +{value['physical_ehp_gain']:.0f} physical EHP") """ # Create upgraded stats upgraded = DefensiveStats( life=stats.life, energy_shield=stats.energy_shield, armor=stats.armor, evasion=stats.evasion, block_chance=stats.block_chance, fire_res=stats.fire_res, cold_res=stats.cold_res, lightning_res=stats.lightning_res, chaos_res=stats.chaos_res ) # Apply increase if defense_type == 'life': upgraded.life += increase_amount elif defense_type == 'energy_shield': upgraded.energy_shield += increase_amount elif defense_type == 'armor': upgraded.armor += increase_amount elif defense_type == 'evasion': upgraded.evasion += increase_amount elif defense_type == 'block_chance': upgraded.block_chance += increase_amount elif defense_type == 'fire_res': upgraded.fire_res += increase_amount elif defense_type == 'cold_res': upgraded.cold_res += increase_amount elif defense_type == 'lightning_res': upgraded.lightning_res += increase_amount elif defense_type == 'chaos_res': upgraded.chaos_res += increase_amount else: raise ValueError(f"Unknown defense type: {defense_type}") # Calculate comparison comparison = self.compare_upgrade(stats, upgraded, threat) # Format result result = { 'defense_type': defense_type, 'increase_amount': increase_amount, } if target_damage_type: key = target_damage_type.value result['ehp_gain'] = comparison[key]['absolute_gain'] result['percent_gain'] = comparison[key]['percent_gain'] else: # Include all damage types for damage_type in DamageType: key = damage_type.value result[f'{key}_ehp_gain'] = comparison[key]['absolute_gain'] result[f'{key}_percent_gain'] = comparison[key]['percent_gain'] logger.info( f"Defense value: +{increase_amount} {defense_type} = " f"+{result.get('ehp_gain', 'varies')} EHP" ) return result # ============================================================================ # HELPER METHODS # ============================================================================ def _calculate_raw_hp( self, stats: DefensiveStats, damage_type: DamageType ) -> float: """ Calculate raw HP pool accounting for PoE2 chaos mechanics. PoE2 Change: Chaos damage removes 2× ES (not bypass entirely) """ if damage_type == DamageType.CHAOS: # PoE2: Chaos removes ES at 2× rate effective_es = stats.energy_shield / 2.0 return stats.life + effective_es else: return stats.life + stats.energy_shield def _calculate_evasion_mitigation( self, stats: DefensiveStats, threat: ThreatProfile ) -> float: """Calculate mitigation from evasion (chance to avoid hit).""" if stats.evasion == 0: return 0.0 evasion_result = self.defense_calc.calculate_evasion_chance( stats.evasion, threat.attacker_accuracy ) return evasion_result.evade_chance_percent / 100.0 def _calculate_block_mitigation( self, stats: DefensiveStats ) -> float: """Calculate mitigation from block (chance to avoid hit).""" if stats.block_chance == 0: return 0.0 block_result = self.defense_calc.calculate_block_chance(stats.block_chance) return block_result.block_chance_percent / 100.0 def _calculate_armor_dr( self, stats: DefensiveStats, threat: ThreatProfile ) -> float: """Calculate damage reduction from armor (physical only).""" if stats.armor == 0: return 0.0 armor_result = self.defense_calc.calculate_armor_dr( stats.armor, threat.expected_hit_size ) return armor_result.damage_reduction_percent / 100.0 def _calculate_resistance_dr( self, stats: DefensiveStats, damage_type: DamageType ) -> float: """Calculate damage reduction from resistance.""" resistance_value = self._get_resistance_value(stats, damage_type) res_result = self.defense_calc.calculate_resistance_dr(resistance_value) return res_result.damage_reduction_percent / 100.0 def _get_resistance_value( self, stats: DefensiveStats, damage_type: DamageType ) -> float: """Get resistance value for a specific damage type.""" resistance_map = { DamageType.PHYSICAL: 0.0, # No physical resistance DamageType.FIRE: stats.fire_res, DamageType.COLD: stats.cold_res, DamageType.LIGHTNING: stats.lightning_res, DamageType.CHAOS: stats.chaos_res } return resistance_map.get(damage_type, 0.0) # ============================================================================ # CONVENIENCE FUNCTIONS # ============================================================================ def quick_physical_ehp( life: float, armor: float, block_chance: float = 0.0, hit_size: float = 1000.0 ) -> float: """ Quick physical EHP calculation. Args: life: Life pool armor: Armor rating block_chance: Block chance % hit_size: Expected hit size Returns: Physical EHP """ calc = EHPCalculator() stats = DefensiveStats(life=life, armor=armor, block_chance=block_chance) threat = ThreatProfile(expected_hit_size=hit_size) result = calc.calculate_ehp(stats, DamageType.PHYSICAL, threat) return result.effective_hp def quick_elemental_ehp( life: float, resistance: float, block_chance: float = 0.0 ) -> float: """ Quick elemental EHP calculation. Args: life: Life pool resistance: Elemental resistance % block_chance: Block chance % Returns: Elemental EHP """ calc = EHPCalculator() stats = DefensiveStats(life=life, fire_res=resistance, block_chance=block_chance) threat = ThreatProfile() result = calc.calculate_ehp(stats, DamageType.FIRE, threat) return result.effective_hp if __name__ == '__main__': # Example usage and testing logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) print("=" * 80) print("Path of Exile 2 - Effective Health Pool (EHP) Calculator") print("=" * 80) print() # Create calculator calc = EHPCalculator() # Example 1: Balanced defense character print("-" * 80) print("EXAMPLE 1: Balanced Defense Character") print("-" * 80) balanced_stats = DefensiveStats( life=5000, energy_shield=2000, armor=15000, evasion=3000, block_chance=40, fire_res=75, cold_res=75, lightning_res=75, chaos_res=20 ) threat = ThreatProfile(expected_hit_size=2000, attacker_accuracy=2500) all_ehp = calc.calculate_all_ehp(balanced_stats, threat) print(f"\nCharacter Stats:") print(f" Life: {balanced_stats.life:.0f}") print(f" Energy Shield: {balanced_stats.energy_shield:.0f}") print(f" Armor: {balanced_stats.armor:.0f}") print(f" Evasion: {balanced_stats.evasion:.0f}") print(f" Block: {balanced_stats.block_chance:.0f}%") print(f" Fire/Cold/Lightning Res: {balanced_stats.fire_res:.0f}%") print(f" Chaos Res: {balanced_stats.chaos_res:.0f}%") print(f"\nThreat Profile:") print(f" Expected Hit Size: {threat.expected_hit_size:.0f}") print(f" Attacker Accuracy: {threat.attacker_accuracy:.0f}") print(f"\nEffective Health Pool by Damage Type:") for damage_type, result in all_ehp.items(): print(f" {damage_type.value.upper()}: {result.effective_hp:.0f} EHP " f"({result.total_mitigation*100:.1f}% mitigation)") print(f"\nPhysical Damage Breakdown:") phys_result = all_ehp[DamageType.PHYSICAL] print(f" Raw HP: {phys_result.raw_hp:.0f}") print(f" Evasion: {phys_result.evade_mitigation*100:.1f}% mitigation") print(f" Block: {phys_result.block_mitigation*100:.1f}% mitigation") print(f" Armor: {phys_result.armor_dr*100:.1f}% DR vs {threat.expected_hit_size:.0f} hit") print(f" Total: {phys_result.effective_hp:.0f} EHP") # Example 2: Hit size analysis print("\n" + "-" * 80) print("EXAMPLE 2: Armor Effectiveness vs Hit Size") print("-" * 80) hit_analysis = calc.analyze_armor_vs_hit_sizes(balanced_stats) print(f"\nArmor Rating: {balanced_stats.armor:.0f}") print(f"\nEffectiveness by Hit Size:") for hit_size, data in hit_analysis.items(): print(f" {hit_size:.0f} damage: {data['dr_percent']:.1f}% DR -> " f"{data['physical_ehp']:.0f} Physical EHP") # Example 3: Defense gap identification print("\n" + "-" * 80) print("EXAMPLE 3: Defense Gap Identification") print("-" * 80) # Create a character with some issues flawed_stats = DefensiveStats( life=3500, energy_shield=0, armor=5000, evasion=0, block_chance=15, fire_res=60, cold_res=75, lightning_res=55, chaos_res=-30 ) gaps = calc.identify_defense_gaps(flawed_stats, threat) print(f"\nIdentified {len(gaps)} defense gaps:\n") for gap in gaps: print(f"[Severity: {gap.severity:.1f}/10] {gap.gap_type}") print(f" Issue: {gap.description}") print(f" Fix: {gap.recommendation}") print() # Example 4: Upgrade comparison print("-" * 80) print("EXAMPLE 4: Gear Upgrade Comparison") print("-" * 80) current = DefensiveStats( life=5000, energy_shield=1500, armor=12000, fire_res=70, cold_res=75, lightning_res=75, chaos_res=10 ) # Comparing two upgrade options option_a = DefensiveStats( life=5000, energy_shield=1500, armor=15000, # +3000 armor fire_res=70, cold_res=75, lightning_res=75, chaos_res=10 ) option_b = DefensiveStats( life=5000, energy_shield=1500, armor=12000, fire_res=75, # +5% fire res (capped) cold_res=75, lightning_res=75, chaos_res=10 ) print("\nOption A: +3000 Armor") comparison_a = calc.compare_upgrade(current, option_a, threat) for damage_type in ['physical', 'fire']: data = comparison_a[damage_type] print(f" {damage_type.upper()}: {data['current_ehp']:.0f} -> {data['upgraded_ehp']:.0f} " f"(+{data['absolute_gain']:.0f}, +{data['percent_gain']:.1f}%)") print("\nOption B: Fire Res 70% -> 75%") comparison_b = calc.compare_upgrade(current, option_b, threat) for damage_type in ['physical', 'fire']: data = comparison_b[damage_type] print(f" {damage_type.upper()}: {data['current_ehp']:.0f} -> {data['upgraded_ehp']:.0f} " f"(+{data['absolute_gain']:.0f}, +{data['percent_gain']:.1f}%)") # Example 5: Defense value calculation print("\n" + "-" * 80) print("EXAMPLE 5: Defense Value Analysis") print("-" * 80) base_stats = DefensiveStats( life=4500, armor=10000, block_chance=30, fire_res=75, cold_res=75, lightning_res=75, chaos_res=0 ) print("\nHow much EHP would various upgrades provide?") print("(vs 1000 damage hits)") upgrades = [ ('life', 500), ('armor', 5000), ('block_chance', 10), ('fire_res', 5) ] for defense_type, amount in upgrades: value = calc.calculate_defense_value( base_stats, defense_type, amount, ThreatProfile(expected_hit_size=1000) ) print(f"\n+{amount} {defense_type}:") if 'physical_ehp_gain' in value: print(f" Physical: +{value['physical_ehp_gain']:.0f} EHP ({value['physical_percent_gain']:.1f}%)") print(f" Fire: +{value['fire_ehp_gain']:.0f} EHP ({value['fire_percent_gain']:.1f}%)") else: print(f" EHP Gain: +{value['ehp_gain']:.0f} ({value['percent_gain']:.1f}%)") # Example 6: Armor breakpoints print("\n" + "-" * 80) print("EXAMPLE 6: Armor Breakpoints") print("-" * 80) breakpoints = calc.find_armor_breakpoints(base_stats) print(f"\nArmor needed for target DR (vs {ThreatProfile().expected_hit_size:.0f} damage hits):") for dr, armor_needed in breakpoints.items(): if armor_needed == float('inf'): print(f" {dr:.0f}% DR: Cannot reach (exceeds 90% cap)") else: print(f" {dr:.0f}% DR: {armor_needed:.0f} armor") # Example 7: Quick functions print("\n" + "-" * 80) print("EXAMPLE 7: Quick Calculations") print("-" * 80) print("\nQuick Physical EHP:") phys_ehp = quick_physical_ehp(life=5000, armor=20000, block_chance=40, hit_size=2000) print(f" 5000 life, 20000 armor, 40% block vs 2000 hit: {phys_ehp:.0f} EHP") print("\nQuick Elemental EHP:") ele_ehp = quick_elemental_ehp(life=5000, resistance=75, block_chance=40) print(f" 5000 life, 75% res, 40% block: {ele_ehp:.0f} EHP") print("\n" + "=" * 80) print("EHP Calculator Test Complete!") print("=" * 80)

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