510 lines
14 KiB
Python
510 lines
14 KiB
Python
"""
|
||
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
|