feat(api): integrate equipment stats into combat damage system

Equipment-Combat Integration:
- Update Stats damage formula from STR//2 to int(STR*0.75) for better scaling
- Add spell_power system for magical weapons (staves, wands)
- Add spell_power_bonus field to Stats model with spell_power property
- Add spell_power field to Item model with is_magical_weapon() method
- Update Character.get_effective_stats() to populate spell_power_bonus

Combatant Model Updates:
- Add weapon property fields (crit_chance, crit_multiplier, damage_type)
- Add elemental weapon support (elemental_damage_type, physical_ratio, elemental_ratio)
- Update serialization to handle new weapon properties

DamageCalculator Refactoring:
- Remove weapon_damage parameter from calculate_physical_damage()
- Use attacker_stats.damage directly (includes weapon bonus)
- Use attacker_stats.spell_power for magical damage calculations

Combat Service Updates:
- Extract weapon properties in _create_combatant_from_character()
- Use stats.damage_bonus for enemy combatants from templates
- Remove hardcoded _get_weapon_damage() method
- Handle elemental weapons with split damage in _execute_attack()

Item Generation Updates:
- Add base_spell_power to BaseItemTemplate dataclass
- Add ARCANE damage type to DamageType enum
- Add magical weapon templates (wizard_staff, arcane_staff, wand, crystal_wand)

Test Updates:
- Update test_stats.py for new damage formula (0.75 scaling)
- Update test_character.py for equipment bonus calculations
- Update test_damage_calculator.py for new API signatures
- Update test_combat_service.py mock fixture for equipped attribute

Tests: 174 passing
This commit is contained in:
2025-11-26 19:54:58 -06:00
parent 4ced1b04df
commit a38906b445
16 changed files with 792 additions and 168 deletions

View File

@@ -248,3 +248,134 @@ def test_repr_includes_combat_bonuses():
assert "CRIT_BONUS=" in repr_str
assert "HIT_BONUS=" in repr_str
# =============================================================================
# Equipment Bonus Fields (Task 2.5)
# =============================================================================
def test_bonus_fields_default_to_zero():
"""Test that equipment bonus fields default to zero."""
stats = Stats()
assert stats.damage_bonus == 0
assert stats.defense_bonus == 0
assert stats.resistance_bonus == 0
def test_damage_property_with_no_bonus():
"""Test damage calculation: int(strength * 0.75) + damage_bonus with no bonus."""
stats = Stats(strength=10)
# int(10 * 0.75) = 7, no bonus
assert stats.damage == 7
stats = Stats(strength=14)
# int(14 * 0.75) = 10, no bonus
assert stats.damage == 10
def test_damage_property_with_bonus():
"""Test damage calculation includes damage_bonus from weapons."""
stats = Stats(strength=10, damage_bonus=15)
# int(10 * 0.75) + 15 = 7 + 15 = 22
assert stats.damage == 22
stats = Stats(strength=14, damage_bonus=8)
# int(14 * 0.75) + 8 = 10 + 8 = 18
assert stats.damage == 18
def test_defense_property_with_bonus():
"""Test defense calculation includes defense_bonus from armor."""
stats = Stats(constitution=10, defense_bonus=10)
# (10 // 2) + 10 = 5 + 10 = 15
assert stats.defense == 15
stats = Stats(constitution=20, defense_bonus=5)
# (20 // 2) + 5 = 10 + 5 = 15
assert stats.defense == 15
def test_resistance_property_with_bonus():
"""Test resistance calculation includes resistance_bonus from armor."""
stats = Stats(wisdom=10, resistance_bonus=8)
# (10 // 2) + 8 = 5 + 8 = 13
assert stats.resistance == 13
stats = Stats(wisdom=14, resistance_bonus=3)
# (14 // 2) + 3 = 7 + 3 = 10
assert stats.resistance == 10
def test_bonus_fields_serialization():
"""Test that bonus fields are included in to_dict()."""
stats = Stats(
strength=15,
damage_bonus=12,
defense_bonus=8,
resistance_bonus=5,
)
data = stats.to_dict()
assert data["damage_bonus"] == 12
assert data["defense_bonus"] == 8
assert data["resistance_bonus"] == 5
def test_bonus_fields_deserialization():
"""Test that bonus fields are restored from from_dict()."""
data = {
"strength": 15,
"damage_bonus": 12,
"defense_bonus": 8,
"resistance_bonus": 5,
}
stats = Stats.from_dict(data)
assert stats.damage_bonus == 12
assert stats.defense_bonus == 8
assert stats.resistance_bonus == 5
def test_bonus_fields_deserialization_defaults():
"""Test that missing bonus fields default to zero on deserialization."""
data = {
"strength": 15,
# No bonus fields
}
stats = Stats.from_dict(data)
assert stats.damage_bonus == 0
assert stats.defense_bonus == 0
assert stats.resistance_bonus == 0
def test_copy_includes_bonus_fields():
"""Test that copy() preserves bonus fields."""
original = Stats(
strength=15,
damage_bonus=10,
defense_bonus=8,
resistance_bonus=5,
)
copy = original.copy()
assert copy.damage_bonus == 10
assert copy.defense_bonus == 8
assert copy.resistance_bonus == 5
# Verify independence
copy.damage_bonus = 20
assert original.damage_bonus == 10
assert copy.damage_bonus == 20
def test_repr_includes_damage():
"""Test that repr includes the damage computed property."""
stats = Stats(strength=10, damage_bonus=15)
repr_str = repr(stats)
assert "DMG=" in repr_str