Skip to main content
Glama

Path of Exile 2 Build Optimizer MCP

test_damage_calculator.py19.8 kB
""" Unit tests for Path of Exile 2 Damage Calculator Tests comprehensive damage calculation functionality including: - Base damage calculation (weapon and spell) - Damage modifier stacking (increased/more) - Critical strike mechanics - Elemental conversion - DPS calculations - Attack/cast speed """ import unittest import sys import os # Add src to path for imports sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src'))) from calculator.damage_calculator import ( DamageCalculator, DamageRange, DamageComponents, DamageType, Modifier, ModifierType, CriticalStrikeConfig ) class TestDamageRange(unittest.TestCase): """Test DamageRange dataclass.""" def test_valid_damage_range(self): """Test creating a valid damage range.""" damage = DamageRange(10, 20) self.assertEqual(damage.min_damage, 10) self.assertEqual(damage.max_damage, 20) self.assertTrue(damage.is_valid()) def test_average_calculation(self): """Test average damage calculation.""" damage = DamageRange(100, 200) self.assertEqual(damage.average(), 150.0) def test_equal_min_max(self): """Test damage range with equal min and max.""" damage = DamageRange(50, 50) self.assertEqual(damage.average(), 50.0) self.assertTrue(damage.is_valid()) def test_negative_min_raises_error(self): """Test that negative minimum damage raises ValueError.""" with self.assertRaises(ValueError) as context: DamageRange(-10, 20) self.assertIn("negative", str(context.exception).lower()) def test_negative_max_raises_error(self): """Test that negative maximum damage raises ValueError.""" with self.assertRaises(ValueError) as context: DamageRange(10, -20) self.assertIn("negative", str(context.exception).lower()) def test_min_greater_than_max_raises_error(self): """Test that min > max raises ValueError.""" with self.assertRaises(ValueError) as context: DamageRange(100, 50) self.assertIn("cannot exceed", str(context.exception).lower()) def test_scale_multiplier(self): """Test scaling damage by a multiplier.""" damage = DamageRange(10, 20) scaled = damage.scale(2.0) self.assertEqual(scaled.min_damage, 20) self.assertEqual(scaled.max_damage, 40) def test_scale_fractional_multiplier(self): """Test scaling with fractional multiplier.""" damage = DamageRange(100, 200) scaled = damage.scale(0.5) self.assertEqual(scaled.min_damage, 50) self.assertEqual(scaled.max_damage, 100) class TestModifier(unittest.TestCase): """Test Modifier dataclass.""" def test_increased_modifier(self): """Test increased modifier multiplier calculation.""" mod = Modifier(value=50, modifier_type=ModifierType.INCREASED) # get_multiplier returns decimal (0.5 for 50%), not the multiplied value (1.5) self.assertEqual(mod.get_multiplier(), 0.5) def test_more_modifier(self): """Test more modifier multiplier calculation.""" mod = Modifier(value=30, modifier_type=ModifierType.MORE) # get_multiplier returns decimal (0.3 for 30%), not the multiplied value (1.3) self.assertEqual(mod.get_multiplier(), 0.3) def test_reduced_modifier(self): """Test reduced modifier (negative increased).""" mod = Modifier(value=25, modifier_type=ModifierType.REDUCED) # Reduced returns negative decimal (-0.25 for 25% reduced) self.assertEqual(mod.get_multiplier(), -0.25) def test_less_modifier(self): """Test less modifier (negative more).""" mod = Modifier(value=20, modifier_type=ModifierType.LESS) # Less returns negative decimal (-0.2 for 20% less) self.assertEqual(mod.get_multiplier(), -0.2) class TestDamageComponents(unittest.TestCase): """Test DamageComponents container.""" def test_empty_components(self): """Test empty damage components.""" comp = DamageComponents() self.assertEqual(comp.total_average_damage(), 0.0) def test_single_damage_type(self): """Test components with single damage type.""" comp = DamageComponents() comp.add_damage(DamageType.PHYSICAL, DamageRange(50, 100)) self.assertEqual(comp.total_average_damage(), 75.0) def test_multiple_damage_types(self): """Test components with multiple damage types.""" comp = DamageComponents() comp.add_damage(DamageType.PHYSICAL, DamageRange(50, 100)) # avg: 75 comp.add_damage(DamageType.FIRE, DamageRange(20, 40)) # avg: 30 comp.add_damage(DamageType.COLD, DamageRange(10, 20)) # avg: 15 self.assertEqual(comp.total_average_damage(), 120.0) def test_get_damage_by_type(self): """Test retrieving specific damage type.""" comp = DamageComponents() fire_damage = DamageRange(25, 50) comp.add_damage(DamageType.FIRE, fire_damage) result = comp.get_damage_by_type(DamageType.FIRE) self.assertIsNotNone(result) self.assertEqual(result.min_damage, 25) self.assertEqual(result.max_damage, 50) def test_get_nonexistent_damage_type(self): """Test getting damage type that doesn't exist.""" comp = DamageComponents() result = comp.get_damage_by_type(DamageType.CHAOS) self.assertIsNone(result) class TestBaseDamageCalculation(unittest.TestCase): """Test base damage calculation.""" def setUp(self): """Set up test calculator.""" self.calc = DamageCalculator() def test_weapon_damage_only(self): """Test base damage with weapon only.""" weapon = DamageRange(50, 100) result = self.calc.calculate_base_damage(weapon_damage=weapon) # Weapon damage is physical phys_damage = result.get_damage_by_type(DamageType.PHYSICAL) self.assertIsNotNone(phys_damage) self.assertEqual(phys_damage.average(), 75.0) def test_spell_damage_only(self): """Test base damage with spell only.""" spell = DamageRange(80, 120) result = self.calc.calculate_base_damage(spell_base_damage=spell) # Spell damage defaults to physical phys_damage = result.get_damage_by_type(DamageType.PHYSICAL) self.assertIsNotNone(phys_damage) self.assertEqual(phys_damage.average(), 100.0) def test_weapon_with_added_flat_damage(self): """Test weapon with added flat elemental damage.""" weapon = DamageRange(50, 100) # avg: 75 added_damage = [ (DamageType.FIRE, DamageRange(20, 40)), # avg: 30 (DamageType.LIGHTNING, DamageRange(10, 20)) # avg: 15 ] result = self.calc.calculate_base_damage( weapon_damage=weapon, added_flat_damage=added_damage ) # Total = 75 + 30 + 15 = 120 self.assertEqual(result.total_average_damage(), 120.0) def test_no_damage_source_raises_error(self): """Test that providing neither weapon nor spell raises ValueError.""" with self.assertRaises(ValueError) as context: self.calc.calculate_base_damage() self.assertIn("must provide", str(context.exception).lower()) class TestIncreasedModifiers(unittest.TestCase): """Test increased/reduced modifier application.""" def setUp(self): """Set up test calculator.""" self.calc = DamageCalculator() def test_single_increased_modifier(self): """Test applying single increased modifier.""" base = 100 mods = [Modifier(value=50, modifier_type=ModifierType.INCREASED)] result = self.calc.apply_increased_modifiers(base, mods) # 100 * (1 + 0.50) = 150 self.assertEqual(result, 150.0) def test_multiple_increased_modifiers_stack_additively(self): """Test that multiple increased modifiers stack additively.""" base = 100 mods = [ Modifier(value=50, modifier_type=ModifierType.INCREASED), Modifier(value=30, modifier_type=ModifierType.INCREASED), Modifier(value=20, modifier_type=ModifierType.INCREASED) ] result = self.calc.apply_increased_modifiers(base, mods) # 100 * (1 + 0.50 + 0.30 + 0.20) = 100 * 2.0 = 200 self.assertEqual(result, 200.0) def test_reduced_modifier(self): """Test reduced modifier (negative increased).""" base = 100 mods = [Modifier(value=25, modifier_type=ModifierType.REDUCED)] result = self.calc.apply_increased_modifiers(base, mods) # 100 * (1 - 0.25) = 75 self.assertEqual(result, 75.0) def test_increased_and_reduced_mixed(self): """Test mixing increased and reduced modifiers.""" base = 100 mods = [ Modifier(value=100, modifier_type=ModifierType.INCREASED), # +100% Modifier(value=50, modifier_type=ModifierType.REDUCED) # -50% ] result = self.calc.apply_increased_modifiers(base, mods) # 100 * (1 + 1.00 - 0.50) = 100 * 1.5 = 150 self.assertEqual(result, 150.0) def test_no_modifiers(self): """Test with empty modifier list.""" base = 100 result = self.calc.apply_increased_modifiers(base, []) self.assertEqual(result, 100.0) class TestMoreModifiers(unittest.TestCase): """Test more/less modifier application.""" def setUp(self): """Set up test calculator.""" self.calc = DamageCalculator() def test_single_more_modifier(self): """Test applying single more modifier.""" base = 100 mods = [Modifier(value=50, modifier_type=ModifierType.MORE)] result = self.calc.apply_more_modifiers(base, mods) # 100 * 1.50 = 150 self.assertEqual(result, 150.0) def test_multiple_more_modifiers_stack_multiplicatively(self): """Test that multiple more modifiers stack multiplicatively.""" base = 100 mods = [ Modifier(value=50, modifier_type=ModifierType.MORE), # 1.5x Modifier(value=30, modifier_type=ModifierType.MORE), # 1.3x Modifier(value=20, modifier_type=ModifierType.MORE) # 1.2x ] result = self.calc.apply_more_modifiers(base, mods) # 100 * 1.5 * 1.3 * 1.2 = 234 self.assertAlmostEqual(result, 234.0, places=1) def test_less_modifier(self): """Test less modifier (negative more).""" base = 100 mods = [Modifier(value=20, modifier_type=ModifierType.LESS)] result = self.calc.apply_more_modifiers(base, mods) # 100 * 0.80 = 80 self.assertEqual(result, 80.0) def test_more_and_less_mixed(self): """Test mixing more and less modifiers.""" base = 100 mods = [ Modifier(value=100, modifier_type=ModifierType.MORE), # 2.0x Modifier(value=50, modifier_type=ModifierType.LESS) # 0.5x ] result = self.calc.apply_more_modifiers(base, mods) # 100 * 2.0 * 0.5 = 100 self.assertEqual(result, 100.0) def test_no_modifiers(self): """Test with empty modifier list.""" base = 100 result = self.calc.apply_more_modifiers(base, []) self.assertEqual(result, 100.0) class TestMoreVsIncreased(unittest.TestCase): """Test critical difference between MORE and INCREASED modifiers.""" def setUp(self): """Set up test calculator.""" self.calc = DamageCalculator() def test_increased_modifiers_additive(self): """Test that INCREASED modifiers are additive.""" base = 100 mods = [ Modifier(value=50, modifier_type=ModifierType.INCREASED), Modifier(value=50, modifier_type=ModifierType.INCREASED) ] result = self.calc.apply_increased_modifiers(base, mods) # 100 * (1 + 0.50 + 0.50) = 200 (NOT 225) self.assertEqual(result, 200.0) def test_more_modifiers_multiplicative(self): """Test that MORE modifiers are multiplicative.""" base = 100 mods = [ Modifier(value=50, modifier_type=ModifierType.MORE), Modifier(value=50, modifier_type=ModifierType.MORE) ] result = self.calc.apply_more_modifiers(base, mods) # 100 * 1.50 * 1.50 = 225 (NOT 200) self.assertEqual(result, 225.0) class TestCriticalStrike(unittest.TestCase): """Test critical strike calculation.""" def setUp(self): """Set up test calculator.""" self.calc = DamageCalculator() def test_poe2_base_crit_multiplier(self): """Test that PoE2 uses +100% base crit damage (not +150% like PoE1).""" config = CriticalStrikeConfig( crit_chance=100.0, # Always crit crit_multiplier=100 # Base PoE2 crit ) # With 100% crit chance and 100% multiplier, effective is 2.0x self.assertEqual(config.crit_multiplier, 100) self.assertEqual(config.effective_damage_multiplier(), 2.0) def test_crit_with_increased_multiplier(self): """Test critical damage with increased multiplier.""" base_damage = DamageRange(100, 100) config = CriticalStrikeConfig( crit_chance=100.0, # Always crit crit_multiplier=150 # 100% base + 50% increased = 250% total ) result = self.calc.calculate_critical_damage(base_damage, config) # 100 * (1 + 150/100) = 100 * 2.5 = 250 self.assertEqual(result.average(), 250.0) def test_no_crit_with_effective_multiplier(self): """Test that 0% crit chance has 1.0x effective multiplier.""" config = CriticalStrikeConfig( crit_chance=0.0, crit_multiplier=150 ) # With 0% crit, effective multiplier is 1.0 (no bonus) self.assertEqual(config.effective_damage_multiplier(), 1.0) def test_partial_crit_chance(self): """Test average damage with partial crit chance.""" config = CriticalStrikeConfig( crit_chance=50.0, # 50% crit chance crit_multiplier=100 # 2.0x damage on crit ) # Effective multiplier: (0.5 * 1.0) + (0.5 * 2.0) = 0.5 + 1.0 = 1.5 self.assertEqual(config.effective_damage_multiplier(), 1.5) class TestDamageConversion(unittest.TestCase): """Test elemental conversion mechanics.""" def setUp(self): """Set up test calculator.""" self.calc = DamageCalculator() def test_full_physical_to_fire_conversion(self): """Test 100% physical to fire conversion.""" components = DamageComponents() components.add_damage(DamageType.PHYSICAL, DamageRange(100, 100)) conversion = { DamageType.PHYSICAL: { DamageType.FIRE: 100 # 100% conversion (as percentage, not decimal) } } result = self.calc.apply_damage_conversion(components, conversion) # All physical should be converted to fire phys = result.get_damage_by_type(DamageType.PHYSICAL) fire = result.get_damage_by_type(DamageType.FIRE) self.assertIsNone(phys) # No physical left self.assertIsNotNone(fire) self.assertEqual(fire.average(), 100.0) def test_partial_physical_to_fire_conversion(self): """Test 50% physical to fire conversion.""" components = DamageComponents() components.add_damage(DamageType.PHYSICAL, DamageRange(100, 100)) conversion = { DamageType.PHYSICAL: { DamageType.FIRE: 50 # 50% conversion (as percentage, not decimal) } } result = self.calc.apply_damage_conversion(components, conversion) phys = result.get_damage_by_type(DamageType.PHYSICAL) fire = result.get_damage_by_type(DamageType.FIRE) # 50 physical, 50 fire self.assertIsNotNone(phys) self.assertIsNotNone(fire) self.assertEqual(phys.average(), 50.0) self.assertEqual(fire.average(), 50.0) def test_split_conversion(self): """Test converting to multiple elements.""" components = DamageComponents() components.add_damage(DamageType.PHYSICAL, DamageRange(100, 100)) conversion = { DamageType.PHYSICAL: { DamageType.FIRE: 40, # 40% to fire (as percentage) DamageType.LIGHTNING: 30 # 30% to lightning (as percentage) } } result = self.calc.apply_damage_conversion(components, conversion) phys = result.get_damage_by_type(DamageType.PHYSICAL) fire = result.get_damage_by_type(DamageType.FIRE) lightning = result.get_damage_by_type(DamageType.LIGHTNING) # 30 phys, 40 fire, 30 lightning self.assertIsNotNone(phys) self.assertEqual(phys.average(), 30.0) self.assertEqual(fire.average(), 40.0) self.assertEqual(lightning.average(), 30.0) def test_no_conversion(self): """Test with no conversion.""" components = DamageComponents() components.add_damage(DamageType.PHYSICAL, DamageRange(100, 100)) result = self.calc.apply_damage_conversion(components, {}) # Should be unchanged phys = result.get_damage_by_type(DamageType.PHYSICAL) self.assertIsNotNone(phys) self.assertEqual(phys.average(), 100.0) class TestAttackSpeed(unittest.TestCase): """Test attack speed calculation.""" def setUp(self): """Set up test calculator.""" self.calc = DamageCalculator() def test_base_attack_speed(self): """Test attack speed with no modifiers.""" base_attack_time = 1.5 # 1.5 seconds per attack result = self.calc.calculate_attack_speed(base_attack_time, []) # 1 / 1.5 = 0.667 attacks per second self.assertAlmostEqual(result, 0.667, places=3) def test_increased_attack_speed(self): """Test with increased attack speed.""" base_attack_time = 1.0 # 1 second per attack mods = [Modifier(50, ModifierType.INCREASED)] # 50% increased result = self.calc.calculate_attack_speed(base_attack_time, mods) # (1 / 1.0) * (1 + 0.50) = 1.0 * 1.5 = 1.5 attacks per second self.assertEqual(result, 1.5) def test_reduced_attack_speed(self): """Test with reduced attack speed (negative increased).""" base_attack_time = 1.0 # 1 second per attack mods = [Modifier(25, ModifierType.REDUCED)] # 25% reduced result = self.calc.calculate_attack_speed(base_attack_time, mods) # (1 / 1.0) * (1 - 0.25) = 1.0 * 0.75 = 0.75 attacks per second self.assertEqual(result, 0.75) class TestCastSpeed(unittest.TestCase): """Test cast speed calculation.""" def setUp(self): """Set up test calculator.""" self.calc = DamageCalculator() def test_base_cast_speed(self): """Test cast speed with no modifiers.""" base_cast_time = 1.0 # 1 second result = self.calc.calculate_cast_speed(base_cast_time, []) # 1 / 1.0 = 1.0 casts per second self.assertEqual(result, 1.0) def test_increased_cast_speed(self): """Test with increased cast speed.""" base_cast_time = 1.0 mods = [Modifier(100, ModifierType.INCREASED)] # 100% increased = 2x speed result = self.calc.calculate_cast_speed(base_cast_time, mods) # (1 / 1.0) * (1 + 1.0) = 1.0 * 2.0 = 2.0 casts per second self.assertEqual(result, 2.0) if __name__ == '__main__': unittest.main()

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