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

@@ -452,3 +452,183 @@ def test_character_round_trip_serialization(basic_character):
assert restored.unlocked_skills == basic_character.unlocked_skills
assert "weapon" in restored.equipped
assert restored.equipped["weapon"].item_id == "sword"
# =============================================================================
# Equipment Combat Bonuses (Task 2.5)
# =============================================================================
def test_get_effective_stats_weapon_damage_bonus(basic_character):
"""Test that weapon damage is added to effective stats damage_bonus."""
# Create weapon with damage
weapon = Item(
item_id="iron_sword",
name="Iron Sword",
item_type=ItemType.WEAPON,
description="A sturdy iron sword",
damage=15, # 15 damage
)
basic_character.equipped["weapon"] = weapon
effective = basic_character.get_effective_stats()
# Base strength is 12, so base damage = int(12 * 0.75) = 9
# Weapon damage = 15
# Total damage property = 9 + 15 = 24
assert effective.damage_bonus == 15
assert effective.damage == 24 # int(12 * 0.75) + 15
def test_get_effective_stats_armor_defense_bonus(basic_character):
"""Test that armor defense is added to effective stats defense_bonus."""
# Create armor with defense
armor = Item(
item_id="iron_chestplate",
name="Iron Chestplate",
item_type=ItemType.ARMOR,
description="A sturdy iron chestplate",
defense=10,
resistance=0,
)
basic_character.equipped["chest"] = armor
effective = basic_character.get_effective_stats()
# Base constitution is 14, so base defense = 14 // 2 = 7
# Armor defense = 10
# Total defense property = 7 + 10 = 17
assert effective.defense_bonus == 10
assert effective.defense == 17 # (14 // 2) + 10
def test_get_effective_stats_armor_resistance_bonus(basic_character):
"""Test that armor resistance is added to effective stats resistance_bonus."""
# Create armor with resistance
robe = Item(
item_id="magic_robe",
name="Magic Robe",
item_type=ItemType.ARMOR,
description="An enchanted robe",
defense=2,
resistance=8,
)
basic_character.equipped["chest"] = robe
effective = basic_character.get_effective_stats()
# Base wisdom is 10, so base resistance = 10 // 2 = 5
# Armor resistance = 8
# Total resistance property = 5 + 8 = 13
assert effective.resistance_bonus == 8
assert effective.resistance == 13 # (10 // 2) + 8
def test_get_effective_stats_multiple_armor_pieces(basic_character):
"""Test that multiple armor pieces stack their bonuses."""
# Create multiple armor pieces
helmet = Item(
item_id="iron_helmet",
name="Iron Helmet",
item_type=ItemType.ARMOR,
description="Protects your head",
defense=5,
resistance=2,
)
chestplate = Item(
item_id="iron_chestplate",
name="Iron Chestplate",
item_type=ItemType.ARMOR,
description="Protects your torso",
defense=10,
resistance=3,
)
boots = Item(
item_id="iron_boots",
name="Iron Boots",
item_type=ItemType.ARMOR,
description="Protects your feet",
defense=3,
resistance=1,
)
basic_character.equipped["helmet"] = helmet
basic_character.equipped["chest"] = chestplate
basic_character.equipped["boots"] = boots
effective = basic_character.get_effective_stats()
# Total defense bonus = 5 + 10 + 3 = 18
# Total resistance bonus = 2 + 3 + 1 = 6
assert effective.defense_bonus == 18
assert effective.resistance_bonus == 6
# Base constitution is 14: base defense = 7
# Base wisdom is 10: base resistance = 5
assert effective.defense == 25 # 7 + 18
assert effective.resistance == 11 # 5 + 6
def test_get_effective_stats_weapon_and_armor_combined(basic_character):
"""Test that weapon damage and armor defense/resistance work together."""
# Create weapon
weapon = Item(
item_id="flaming_sword",
name="Flaming Sword",
item_type=ItemType.WEAPON,
description="A sword wreathed in flame",
damage=18,
stat_bonuses={"strength": 3}, # Also has stat bonus
)
# Create armor
armor = Item(
item_id="dragon_armor",
name="Dragon Armor",
item_type=ItemType.ARMOR,
description="Forged from dragon scales",
defense=15,
resistance=10,
stat_bonuses={"constitution": 2}, # Also has stat bonus
)
basic_character.equipped["weapon"] = weapon
basic_character.equipped["chest"] = armor
effective = basic_character.get_effective_stats()
# Weapon: damage=18, +3 STR
# Armor: defense=15, resistance=10, +2 CON
# Base STR=12 -> 12+3=15, damage = int(15 * 0.75) + 18 = 11 + 18 = 29
assert effective.strength == 15
assert effective.damage_bonus == 18
assert effective.damage == 29
# Base CON=14 -> 14+2=16, defense = (16//2) + 15 = 8 + 15 = 23
assert effective.constitution == 16
assert effective.defense_bonus == 15
assert effective.defense == 23
# Base WIS=10, resistance = (10//2) + 10 = 5 + 10 = 15
assert effective.resistance_bonus == 10
assert effective.resistance == 15
def test_get_effective_stats_no_equipment_bonuses(basic_character):
"""Test that bonus fields are zero when no equipment is equipped."""
effective = basic_character.get_effective_stats()
assert effective.damage_bonus == 0
assert effective.defense_bonus == 0
assert effective.resistance_bonus == 0
# Damage/defense/resistance should just be base stat derived values
# Base STR=12, damage = int(12 * 0.75) = 9
assert effective.damage == 9
# Base CON=14, defense = 14 // 2 = 7
assert effective.defense == 7
# Base WIS=10, resistance = 10 // 2 = 5
assert effective.resistance == 5