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:
@@ -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
|
||||
|
||||
@@ -55,6 +55,7 @@ def mock_character(mock_stats):
|
||||
char.experience = 1000
|
||||
char.gold = 100
|
||||
char.unlocked_skills = ["power_strike"]
|
||||
char.equipped = {} # No equipment by default
|
||||
char.get_effective_stats = Mock(return_value=mock_stats)
|
||||
return char
|
||||
|
||||
|
||||
@@ -267,8 +267,9 @@ class TestPhysicalDamage:
|
||||
|
||||
def test_basic_physical_damage_formula(self):
|
||||
"""Test the basic physical damage formula."""
|
||||
# Formula: (Weapon + STR * 0.75) * Variance - DEF
|
||||
attacker = Stats(strength=14, luck=0) # LUK 0 to ensure no miss
|
||||
# Formula: (stats.damage + ability_power) * Variance - DEF
|
||||
# where stats.damage = int(STR * 0.75) + damage_bonus
|
||||
attacker = Stats(strength=14, luck=0, damage_bonus=8) # Weapon damage in bonus
|
||||
defender = Stats(constitution=10, dexterity=10) # DEF = 5
|
||||
|
||||
# Mock to ensure no miss and no crit, variance = 1.0
|
||||
@@ -278,10 +279,9 @@ class TestPhysicalDamage:
|
||||
result = DamageCalculator.calculate_physical_damage(
|
||||
attacker_stats=attacker,
|
||||
defender_stats=defender,
|
||||
weapon_damage=8,
|
||||
)
|
||||
|
||||
# 8 + (14 * 0.75) = 8 + 10.5 = 18.5 -> 18 - 5 = 13
|
||||
# int(14 * 0.75) + 8 = 10 + 8 = 18, 18 - 5 DEF = 13
|
||||
assert result.total_damage == 13
|
||||
assert result.is_miss is False
|
||||
assert result.is_critical is False
|
||||
@@ -289,7 +289,7 @@ class TestPhysicalDamage:
|
||||
|
||||
def test_physical_damage_miss(self):
|
||||
"""Test that misses deal zero damage."""
|
||||
attacker = Stats(strength=14, luck=0)
|
||||
attacker = Stats(strength=14, luck=0, damage_bonus=8) # Weapon damage in bonus
|
||||
defender = Stats(dexterity=30) # Very high DEX
|
||||
|
||||
# Force a miss
|
||||
@@ -297,7 +297,6 @@ class TestPhysicalDamage:
|
||||
result = DamageCalculator.calculate_physical_damage(
|
||||
attacker_stats=attacker,
|
||||
defender_stats=defender,
|
||||
weapon_damage=8,
|
||||
)
|
||||
|
||||
assert result.is_miss is True
|
||||
@@ -306,7 +305,7 @@ class TestPhysicalDamage:
|
||||
|
||||
def test_physical_damage_critical_hit(self):
|
||||
"""Test critical hit doubles damage."""
|
||||
attacker = Stats(strength=14, luck=20) # High LUK for crit
|
||||
attacker = Stats(strength=14, luck=20, damage_bonus=8) # High LUK for crit, weapon in bonus
|
||||
defender = Stats(constitution=10, dexterity=10)
|
||||
|
||||
# Force hit and crit
|
||||
@@ -315,15 +314,14 @@ class TestPhysicalDamage:
|
||||
result = DamageCalculator.calculate_physical_damage(
|
||||
attacker_stats=attacker,
|
||||
defender_stats=defender,
|
||||
weapon_damage=8,
|
||||
weapon_crit_multiplier=2.0,
|
||||
)
|
||||
|
||||
assert result.is_critical is True
|
||||
# Base: 8 + 14*0.75 = 18.5
|
||||
# Crit applied BEFORE int conversion: 18.5 * 2 = 37
|
||||
# After DEF 5: 37 - 5 = 32
|
||||
assert result.total_damage == 32
|
||||
# Base: int(14 * 0.75) + 8 = 10 + 8 = 18
|
||||
# Crit: 18 * 2 = 36
|
||||
# After DEF 5: 36 - 5 = 31
|
||||
assert result.total_damage == 31
|
||||
assert "critical" in result.message.lower()
|
||||
|
||||
|
||||
@@ -405,7 +403,8 @@ class TestElementalWeaponDamage:
|
||||
def test_split_damage_calculation(self):
|
||||
"""Test 70/30 physical/fire split damage."""
|
||||
# Fire Sword: 70% physical, 30% fire
|
||||
attacker = Stats(strength=14, intelligence=8, luck=0)
|
||||
# Weapon contributes to both damage_bonus (physical) and spell_power_bonus (elemental)
|
||||
attacker = Stats(strength=14, intelligence=8, luck=0, damage_bonus=15, spell_power_bonus=15)
|
||||
defender = Stats(constitution=10, wisdom=10, dexterity=10) # DEF=5, RES=5
|
||||
|
||||
with patch.object(DamageCalculator, 'calculate_hit_chance', return_value=1.0):
|
||||
@@ -414,7 +413,6 @@ class TestElementalWeaponDamage:
|
||||
result = DamageCalculator.calculate_elemental_weapon_damage(
|
||||
attacker_stats=attacker,
|
||||
defender_stats=defender,
|
||||
weapon_damage=15,
|
||||
weapon_crit_chance=0.05,
|
||||
weapon_crit_multiplier=2.0,
|
||||
physical_ratio=0.7,
|
||||
@@ -422,9 +420,10 @@ class TestElementalWeaponDamage:
|
||||
elemental_type=DamageType.FIRE,
|
||||
)
|
||||
|
||||
# Physical: (15 * 0.7) + (14 * 0.75 * 0.7) - 5 = 10.5 + 7.35 - 5 = 12.85 -> 12
|
||||
# Elemental: (15 * 0.3) + (8 * 0.75 * 0.3) - 5 = 4.5 + 1.8 - 5 = 1.3 -> 1
|
||||
# Total: 12 + 1 = 13 (approximately, depends on min damage)
|
||||
# stats.damage = int(14 * 0.75) + 15 = 10 + 15 = 25
|
||||
# stats.spell_power = int(8 * 0.75) + 15 = 6 + 15 = 21
|
||||
# Physical: 25 * 0.7 = 17.5 -> 17 - 5 DEF = 12
|
||||
# Elemental: 21 * 0.3 = 6.3 -> 6 - 5 RES = 1
|
||||
|
||||
assert result.physical_damage > 0
|
||||
assert result.elemental_damage >= 1 # At least minimum damage
|
||||
@@ -433,7 +432,8 @@ class TestElementalWeaponDamage:
|
||||
|
||||
def test_50_50_split_damage(self):
|
||||
"""Test 50/50 physical/elemental split (Lightning Spear)."""
|
||||
attacker = Stats(strength=12, intelligence=12, luck=0)
|
||||
# Same stats and weapon bonuses means similar damage on both sides
|
||||
attacker = Stats(strength=12, intelligence=12, luck=0, damage_bonus=20, spell_power_bonus=20)
|
||||
defender = Stats(constitution=10, wisdom=10, dexterity=10)
|
||||
|
||||
with patch.object(DamageCalculator, 'calculate_hit_chance', return_value=1.0):
|
||||
@@ -442,7 +442,6 @@ class TestElementalWeaponDamage:
|
||||
result = DamageCalculator.calculate_elemental_weapon_damage(
|
||||
attacker_stats=attacker,
|
||||
defender_stats=defender,
|
||||
weapon_damage=20,
|
||||
weapon_crit_chance=0.05,
|
||||
weapon_crit_multiplier=2.0,
|
||||
physical_ratio=0.5,
|
||||
@@ -450,12 +449,12 @@ class TestElementalWeaponDamage:
|
||||
elemental_type=DamageType.LIGHTNING,
|
||||
)
|
||||
|
||||
# Both components should be similar (same stat values)
|
||||
# Both components should be similar (same stat values and weapon bonuses)
|
||||
assert abs(result.physical_damage - result.elemental_damage) <= 2
|
||||
|
||||
def test_elemental_crit_applies_to_both_components(self):
|
||||
"""Test that crit multiplier applies to both damage types."""
|
||||
attacker = Stats(strength=14, intelligence=8, luck=20)
|
||||
attacker = Stats(strength=14, intelligence=8, luck=20, damage_bonus=15, spell_power_bonus=15)
|
||||
defender = Stats(constitution=10, wisdom=10, dexterity=10)
|
||||
|
||||
# Force hit and crit
|
||||
@@ -464,7 +463,6 @@ class TestElementalWeaponDamage:
|
||||
result = DamageCalculator.calculate_elemental_weapon_damage(
|
||||
attacker_stats=attacker,
|
||||
defender_stats=defender,
|
||||
weapon_damage=15,
|
||||
weapon_crit_chance=0.05,
|
||||
weapon_crit_multiplier=2.0,
|
||||
physical_ratio=0.7,
|
||||
@@ -614,8 +612,8 @@ class TestCombatIntegration:
|
||||
|
||||
def test_vanguard_attack_scenario(self):
|
||||
"""Test Vanguard (STR 14) basic attack."""
|
||||
# Vanguard: STR 14, LUK 8
|
||||
vanguard = Stats(strength=14, dexterity=10, constitution=14, luck=8)
|
||||
# Vanguard: STR 14, LUK 8, equipped with Rusty Sword (8 damage)
|
||||
vanguard = Stats(strength=14, dexterity=10, constitution=14, luck=8, damage_bonus=8)
|
||||
goblin = Stats(constitution=10, dexterity=10) # DEF = 5
|
||||
|
||||
with patch.object(DamageCalculator, 'calculate_hit_chance', return_value=1.0):
|
||||
@@ -624,15 +622,14 @@ class TestCombatIntegration:
|
||||
result = DamageCalculator.calculate_physical_damage(
|
||||
attacker_stats=vanguard,
|
||||
defender_stats=goblin,
|
||||
weapon_damage=8, # Rusty sword
|
||||
)
|
||||
|
||||
# 8 + (14 * 0.75) = 18.5 -> 18 - 5 = 13
|
||||
# int(14 * 0.75) + 8 = 10 + 8 = 18, 18 - 5 DEF = 13
|
||||
assert result.total_damage == 13
|
||||
|
||||
def test_arcanist_fireball_scenario(self):
|
||||
"""Test Arcanist (INT 15) Fireball."""
|
||||
# Arcanist: INT 15, LUK 9
|
||||
# Arcanist: INT 15, LUK 9 (no staff equipped, pure ability damage)
|
||||
arcanist = Stats(intelligence=15, dexterity=10, wisdom=12, luck=9)
|
||||
goblin = Stats(wisdom=10, dexterity=10) # RES = 5
|
||||
|
||||
@@ -646,14 +643,15 @@ class TestCombatIntegration:
|
||||
damage_type=DamageType.FIRE,
|
||||
)
|
||||
|
||||
# 12 + (15 * 0.75) = 23.25 -> 23 - 5 = 18
|
||||
# stats.spell_power = int(15 * 0.75) + 0 = 11
|
||||
# 11 + 12 (ability) = 23 - 5 RES = 18
|
||||
assert result.total_damage == 18
|
||||
|
||||
def test_physical_vs_magical_balance(self):
|
||||
"""Test that physical and magical damage are comparable."""
|
||||
# Same-tier characters should deal similar damage
|
||||
vanguard = Stats(strength=14, luck=8) # Melee
|
||||
arcanist = Stats(intelligence=15, luck=9) # Caster
|
||||
vanguard = Stats(strength=14, luck=8, damage_bonus=8) # Melee with weapon
|
||||
arcanist = Stats(intelligence=15, luck=9) # Caster (no staff)
|
||||
target = Stats(constitution=10, wisdom=10, dexterity=10) # DEF=5, RES=5
|
||||
|
||||
with patch.object(DamageCalculator, 'calculate_hit_chance', return_value=1.0):
|
||||
@@ -662,7 +660,6 @@ class TestCombatIntegration:
|
||||
phys_result = DamageCalculator.calculate_physical_damage(
|
||||
attacker_stats=vanguard,
|
||||
defender_stats=target,
|
||||
weapon_damage=8,
|
||||
)
|
||||
magic_result = DamageCalculator.calculate_magical_damage(
|
||||
attacker_stats=arcanist,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user