""" Integration tests for combat system. Tests the complete combat flow including damage calculation, effects, and turn order. """ import pytest from app.models.stats import Stats from app.models.items import Item from app.models.effects import Effect from app.models.abilities import Ability from app.models.combat import Combatant, CombatEncounter from app.models.character import Character from app.models.skills import PlayerClass from app.models.enums import ( ItemType, DamageType, EffectType, StatType, AbilityType, CombatStatus, ) @pytest.fixture def warrior_combatant(): """Create a warrior combatant for testing.""" stats = Stats(strength=15, dexterity=10, constitution=14, intelligence=8, wisdom=10, charisma=11) return Combatant( combatant_id="warrior_1", name="Test Warrior", is_player=True, current_hp=stats.hit_points, max_hp=stats.hit_points, current_mp=stats.mana_points, max_mp=stats.mana_points, stats=stats, abilities=["basic_attack", "power_strike"], ) @pytest.fixture def goblin_combatant(): """Create a goblin enemy for testing.""" stats = Stats(strength=8, dexterity=12, constitution=10, intelligence=6, wisdom=8, charisma=6) return Combatant( combatant_id="goblin_1", name="Goblin", is_player=False, current_hp=stats.hit_points, max_hp=stats.hit_points, current_mp=stats.mana_points, max_mp=stats.mana_points, stats=stats, abilities=["basic_attack"], ) def test_combatant_creation(warrior_combatant): """Test creating a Combatant.""" assert warrior_combatant.combatant_id == "warrior_1" assert warrior_combatant.name == "Test Warrior" assert warrior_combatant.is_player == True assert warrior_combatant.is_alive() == True assert warrior_combatant.is_stunned() == False def test_combatant_take_damage(warrior_combatant): """Test taking damage.""" initial_hp = warrior_combatant.current_hp damage_dealt = warrior_combatant.take_damage(10) assert damage_dealt == 10 assert warrior_combatant.current_hp == initial_hp - 10 def test_combatant_take_damage_with_shield(warrior_combatant): """Test taking damage with shield absorption.""" # Add a shield effect shield = Effect( effect_id="shield_1", name="Shield", effect_type=EffectType.SHIELD, duration=3, power=15, ) warrior_combatant.add_effect(shield) initial_hp = warrior_combatant.current_hp # Deal 10 damage - should be fully absorbed by shield damage_dealt = warrior_combatant.take_damage(10) assert damage_dealt == 0 # No HP damage assert warrior_combatant.current_hp == initial_hp def test_combatant_death(warrior_combatant): """Test combatant death.""" assert warrior_combatant.is_alive() == True # Deal massive damage warrior_combatant.take_damage(1000) assert warrior_combatant.is_alive() == False assert warrior_combatant.is_dead() == True def test_combatant_healing(warrior_combatant): """Test healing.""" # Take some damage first warrior_combatant.take_damage(20) damaged_hp = warrior_combatant.current_hp # Heal healed = warrior_combatant.heal(10) assert healed == 10 assert warrior_combatant.current_hp == damaged_hp + 10 def test_combatant_healing_capped_at_max(warrior_combatant): """Test that healing cannot exceed max HP.""" max_hp = warrior_combatant.max_hp # Try to heal beyond max healed = warrior_combatant.heal(1000) assert warrior_combatant.current_hp == max_hp def test_combatant_stun_effect(warrior_combatant): """Test stun effect prevents actions.""" assert warrior_combatant.is_stunned() == False # Add stun effect stun = Effect( effect_id="stun_1", name="Stunned", effect_type=EffectType.STUN, duration=1, power=0, ) warrior_combatant.add_effect(stun) assert warrior_combatant.is_stunned() == True def test_combatant_tick_effects(warrior_combatant): """Test that ticking effects deals damage/healing.""" # Add a DOT effect poison = Effect( effect_id="poison_1", name="Poison", effect_type=EffectType.DOT, duration=3, power=5, ) warrior_combatant.add_effect(poison) initial_hp = warrior_combatant.current_hp # Tick effects results = warrior_combatant.tick_effects() # Should have taken 5 poison damage assert len(results) == 1 assert results[0]["effect_type"] == "dot" assert results[0]["value"] == 5 assert warrior_combatant.current_hp == initial_hp - 5 def test_combatant_effect_expiration(warrior_combatant): """Test that expired effects are removed.""" # Add effect with 1 turn duration dot = Effect( effect_id="burn_1", name="Burning", effect_type=EffectType.DOT, duration=1, power=5, ) warrior_combatant.add_effect(dot) assert len(warrior_combatant.active_effects) == 1 # Tick - effect should expire results = warrior_combatant.tick_effects() assert results[0]["expired"] == True assert len(warrior_combatant.active_effects) == 0 # Removed def test_ability_mana_cost(warrior_combatant): """Test ability mana cost and usage.""" ability = Ability( ability_id="fireball", name="Fireball", description="Fiery explosion", ability_type=AbilityType.SPELL, base_power=30, damage_type=DamageType.FIRE, mana_cost=15, ) initial_mp = warrior_combatant.current_mp # Check if can use assert warrior_combatant.can_use_ability("fireball", ability) == False # Not in ability list warrior_combatant.abilities.append("fireball") assert warrior_combatant.can_use_ability("fireball", ability) == True # Use ability warrior_combatant.use_ability_cost(ability, "fireball") assert warrior_combatant.current_mp == initial_mp - 15 def test_ability_cooldown(warrior_combatant): """Test ability cooldowns.""" ability = Ability( ability_id="power_strike", name="Power Strike", description="Powerful attack", ability_type=AbilityType.SKILL, base_power=20, cooldown=3, ) warrior_combatant.abilities.append("power_strike") # Can use initially assert warrior_combatant.can_use_ability("power_strike", ability) == True # Use ability warrior_combatant.use_ability_cost(ability, "power_strike") # Now on cooldown assert "power_strike" in warrior_combatant.cooldowns assert warrior_combatant.cooldowns["power_strike"] == 3 assert warrior_combatant.can_use_ability("power_strike", ability) == False # Tick cooldown warrior_combatant.tick_cooldowns() assert warrior_combatant.cooldowns["power_strike"] == 2 # Tick more warrior_combatant.tick_cooldowns() warrior_combatant.tick_cooldowns() # Should be available again assert "power_strike" not in warrior_combatant.cooldowns assert warrior_combatant.can_use_ability("power_strike", ability) == True def test_combat_encounter_initialization(warrior_combatant, goblin_combatant): """Test initializing a combat encounter.""" encounter = CombatEncounter( encounter_id="combat_001", combatants=[warrior_combatant, goblin_combatant], ) encounter.initialize_combat() # Should have turn order assert len(encounter.turn_order) == 2 assert encounter.round_number == 1 assert encounter.status == CombatStatus.ACTIVE # Both combatants should have initiative assert warrior_combatant.initiative > 0 assert goblin_combatant.initiative > 0 def test_combat_turn_advancement(warrior_combatant, goblin_combatant): """Test advancing turns in combat.""" encounter = CombatEncounter( encounter_id="combat_001", combatants=[warrior_combatant, goblin_combatant], ) encounter.initialize_combat() # Get first combatant first = encounter.get_current_combatant() assert first is not None # Advance turn encounter.advance_turn() # Should be second combatant now second = encounter.get_current_combatant() assert second is not None assert second.combatant_id != first.combatant_id # Advance again - should cycle back to first and increment round encounter.advance_turn() assert encounter.round_number == 2 third = encounter.get_current_combatant() assert third.combatant_id == first.combatant_id def test_combat_victory_condition(warrior_combatant, goblin_combatant): """Test victory condition detection.""" encounter = CombatEncounter( encounter_id="combat_001", combatants=[warrior_combatant, goblin_combatant], ) encounter.initialize_combat() # Kill the goblin goblin_combatant.current_hp = 0 # Check end condition status = encounter.check_end_condition() assert status == CombatStatus.VICTORY assert encounter.status == CombatStatus.VICTORY def test_combat_defeat_condition(warrior_combatant, goblin_combatant): """Test defeat condition detection.""" encounter = CombatEncounter( encounter_id="combat_001", combatants=[warrior_combatant, goblin_combatant], ) encounter.initialize_combat() # Kill the warrior warrior_combatant.current_hp = 0 # Check end condition status = encounter.check_end_condition() assert status == CombatStatus.DEFEAT assert encounter.status == CombatStatus.DEFEAT def test_combat_start_turn_processing(warrior_combatant): """Test start_turn() processes effects and cooldowns.""" encounter = CombatEncounter( encounter_id="combat_001", combatants=[warrior_combatant], ) # Initialize combat to set turn order encounter.initialize_combat() # Add a DOT effect poison = Effect( effect_id="poison_1", name="Poison", effect_type=EffectType.DOT, duration=3, power=5, ) warrior_combatant.add_effect(poison) # Add a cooldown warrior_combatant.cooldowns["power_strike"] = 2 initial_hp = warrior_combatant.current_hp # Start turn results = encounter.start_turn() # Effects should have ticked assert len(results) == 1 assert warrior_combatant.current_hp == initial_hp - 5 # Cooldown should have decreased assert warrior_combatant.cooldowns["power_strike"] == 1 def test_combat_logging(warrior_combatant, goblin_combatant): """Test combat log entries.""" encounter = CombatEncounter( encounter_id="combat_001", combatants=[warrior_combatant, goblin_combatant], ) encounter.log_action("attack", "warrior_1", "Warrior attacks Goblin for 10 damage") assert len(encounter.combat_log) == 1 assert encounter.combat_log[0]["action_type"] == "attack" assert encounter.combat_log[0]["combatant_id"] == "warrior_1" assert "Warrior attacks Goblin" in encounter.combat_log[0]["message"] def test_ability_damage_calculation(): """Test ability power calculation with stat scaling.""" stats = Stats(strength=20, intelligence=16) # Physical ability scaling with strength physical = Ability( ability_id="cleave", name="Cleave", description="Powerful strike", ability_type=AbilityType.SKILL, base_power=15, scaling_stat=StatType.STRENGTH, scaling_factor=0.5, ) power = physical.calculate_power(stats) # 15 (base) + (20 strength × 0.5) = 15 + 10 = 25 assert power == 25 # Magical ability scaling with intelligence magical = Ability( ability_id="fireball", name="Fireball", description="Fire spell", ability_type=AbilityType.SPELL, base_power=20, scaling_stat=StatType.INTELLIGENCE, scaling_factor=0.5, ) power = magical.calculate_power(stats) # 20 (base) + (16 intelligence × 0.5) = 20 + 8 = 28 assert power == 28 def test_full_combat_simulation(): """Integration test: Full combat simulation with all systems.""" # Create warrior warrior_stats = Stats(strength=15, constitution=14) warrior = Combatant( combatant_id="hero", name="Hero", is_player=True, current_hp=warrior_stats.hit_points, max_hp=warrior_stats.hit_points, current_mp=warrior_stats.mana_points, max_mp=warrior_stats.mana_points, stats=warrior_stats, ) # Create goblin goblin_stats = Stats(strength=8, constitution=10) goblin = Combatant( combatant_id="goblin", name="Goblin", is_player=False, current_hp=goblin_stats.hit_points, max_hp=goblin_stats.hit_points, current_mp=goblin_stats.mana_points, max_mp=goblin_stats.mana_points, stats=goblin_stats, ) # Create encounter encounter = CombatEncounter( encounter_id="test_combat", combatants=[warrior, goblin], ) encounter.initialize_combat() # Verify setup assert encounter.status == CombatStatus.ACTIVE assert len(encounter.turn_order) == 2 assert warrior.is_alive() and goblin.is_alive() # Simulate turns until combat ends max_turns = 50 # Increased to ensure combat completes turn_count = 0 while encounter.status == CombatStatus.ACTIVE and turn_count < max_turns: # Get current combatant current = encounter.get_current_combatant() # Start turn (tick effects) encounter.start_turn() if current and current.is_alive() and not current.is_stunned(): # Simple AI: deal damage to opponent if current.combatant_id == "hero": target = goblin else: target = warrior # Calculate simple attack damage: strength / 2 - target defense damage = max(1, (current.stats.strength // 2) - target.stats.defense) target.take_damage(damage) encounter.log_action( "attack", current.combatant_id, f"{current.name} attacks {target.name} for {damage} damage", ) # Check for combat end encounter.check_end_condition() # Advance turn encounter.advance_turn() turn_count += 1 # Combat should have ended assert encounter.status in [CombatStatus.VICTORY, CombatStatus.DEFEAT] assert len(encounter.combat_log) > 0