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
260 lines
8.1 KiB
Python
260 lines
8.1 KiB
Python
"""
|
||
Character statistics data model.
|
||
|
||
This module defines the Stats dataclass which represents a character's core
|
||
attributes and provides computed properties for derived values like HP and MP.
|
||
"""
|
||
|
||
from dataclasses import dataclass, field, asdict
|
||
from typing import Dict, Any
|
||
|
||
|
||
@dataclass
|
||
class Stats:
|
||
"""
|
||
Character statistics representing core attributes.
|
||
|
||
Attributes:
|
||
strength: Physical power, affects melee damage
|
||
dexterity: Agility and precision, affects initiative and evasion
|
||
constitution: Endurance and health, affects HP and defense
|
||
intelligence: Magical power, affects spell damage and MP
|
||
wisdom: Perception and insight, affects magical resistance
|
||
charisma: Social influence, affects NPC interactions
|
||
luck: Fortune and fate, affects critical hits, loot, and random outcomes
|
||
damage_bonus: Flat damage bonus from equipped weapons (default 0)
|
||
spell_power_bonus: Flat spell power bonus from staves/wands (default 0)
|
||
defense_bonus: Flat defense bonus from equipped armor (default 0)
|
||
resistance_bonus: Flat resistance bonus from equipped armor (default 0)
|
||
|
||
Computed Properties:
|
||
hit_points: Maximum HP = 10 + (constitution × 2)
|
||
mana_points: Maximum MP = 10 + (intelligence × 2)
|
||
damage: Physical damage = int(strength × 0.75) + damage_bonus
|
||
spell_power: Spell power = int(intelligence × 0.75) + spell_power_bonus
|
||
defense: Physical defense = (constitution // 2) + defense_bonus
|
||
resistance: Magical resistance = (wisdom // 2) + resistance_bonus
|
||
"""
|
||
|
||
strength: int = 10
|
||
dexterity: int = 10
|
||
constitution: int = 10
|
||
intelligence: int = 10
|
||
wisdom: int = 10
|
||
charisma: int = 10
|
||
luck: int = 8
|
||
|
||
# Equipment bonus fields (populated by get_effective_stats())
|
||
damage_bonus: int = 0 # From weapons (physical damage)
|
||
spell_power_bonus: int = 0 # From staves/wands (magical damage)
|
||
defense_bonus: int = 0 # From armor
|
||
resistance_bonus: int = 0 # From armor
|
||
|
||
@property
|
||
def hit_points(self) -> int:
|
||
"""
|
||
Calculate maximum hit points based on constitution.
|
||
|
||
Formula: 10 + (constitution × 2)
|
||
|
||
Returns:
|
||
Maximum HP value
|
||
"""
|
||
return 10 + (self.constitution * 2)
|
||
|
||
@property
|
||
def mana_points(self) -> int:
|
||
"""
|
||
Calculate maximum mana points based on intelligence.
|
||
|
||
Formula: 10 + (intelligence × 2)
|
||
|
||
Returns:
|
||
Maximum MP value
|
||
"""
|
||
return 10 + (self.intelligence * 2)
|
||
|
||
@property
|
||
def damage(self) -> int:
|
||
"""
|
||
Calculate total physical damage from strength and equipment.
|
||
|
||
Formula: int(strength * 0.75) + damage_bonus
|
||
|
||
The damage_bonus comes from equipped weapons and is populated
|
||
by Character.get_effective_stats().
|
||
|
||
Returns:
|
||
Total physical damage value
|
||
"""
|
||
return int(self.strength * 0.75) + self.damage_bonus
|
||
|
||
@property
|
||
def spell_power(self) -> int:
|
||
"""
|
||
Calculate spell power from intelligence and equipment.
|
||
|
||
Formula: int(intelligence * 0.75) + spell_power_bonus
|
||
|
||
The spell_power_bonus comes from equipped staves/wands and is
|
||
populated by Character.get_effective_stats().
|
||
|
||
Returns:
|
||
Total spell power value
|
||
"""
|
||
return int(self.intelligence * 0.75) + self.spell_power_bonus
|
||
|
||
@property
|
||
def defense(self) -> int:
|
||
"""
|
||
Calculate physical defense from constitution and equipment.
|
||
|
||
Formula: (constitution // 2) + defense_bonus
|
||
|
||
The defense_bonus comes from equipped armor and is populated
|
||
by Character.get_effective_stats().
|
||
|
||
Returns:
|
||
Physical defense value (damage reduction)
|
||
"""
|
||
return (self.constitution // 2) + self.defense_bonus
|
||
|
||
@property
|
||
def resistance(self) -> int:
|
||
"""
|
||
Calculate magical resistance from wisdom and equipment.
|
||
|
||
Formula: (wisdom // 2) + resistance_bonus
|
||
|
||
The resistance_bonus comes from equipped armor and is populated
|
||
by Character.get_effective_stats().
|
||
|
||
Returns:
|
||
Magical resistance value (spell damage reduction)
|
||
"""
|
||
return (self.wisdom // 2) + self.resistance_bonus
|
||
|
||
@property
|
||
def crit_bonus(self) -> float:
|
||
"""
|
||
Calculate critical hit chance bonus from luck.
|
||
|
||
Formula: luck * 0.5% (0.005)
|
||
|
||
This bonus is added to the weapon's base crit chance.
|
||
The total crit chance is capped at 25% in the DamageCalculator.
|
||
|
||
Returns:
|
||
Crit chance bonus as a decimal (e.g., 0.04 for LUK 8)
|
||
|
||
Examples:
|
||
LUK 8: 0.04 (4% bonus)
|
||
LUK 12: 0.06 (6% bonus)
|
||
"""
|
||
return self.luck * 0.005
|
||
|
||
@property
|
||
def hit_bonus(self) -> float:
|
||
"""
|
||
Calculate hit chance bonus (miss reduction) from luck.
|
||
|
||
Formula: luck * 0.5% (0.005)
|
||
|
||
This reduces the base 10% miss chance. The minimum miss
|
||
chance is hard capped at 5% to prevent frustration.
|
||
|
||
Returns:
|
||
Miss reduction as a decimal (e.g., 0.04 for LUK 8)
|
||
|
||
Examples:
|
||
LUK 8: 0.04 (reduces miss from 10% to 6%)
|
||
LUK 12: 0.06 (reduces miss from 10% to 4%, capped at 5%)
|
||
"""
|
||
return self.luck * 0.005
|
||
|
||
@property
|
||
def lucky_roll_chance(self) -> float:
|
||
"""
|
||
Calculate chance for a "lucky" high damage variance roll.
|
||
|
||
Formula: 5% + (luck * 0.25%)
|
||
|
||
When triggered, damage variance uses 100%-110% instead of 95%-105%.
|
||
This gives LUK characters more frequent high damage rolls.
|
||
|
||
Returns:
|
||
Lucky roll chance as a decimal
|
||
|
||
Examples:
|
||
LUK 8: 0.07 (7% chance for lucky roll)
|
||
LUK 12: 0.08 (8% chance for lucky roll)
|
||
"""
|
||
return 0.05 + (self.luck * 0.0025)
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
"""
|
||
Serialize stats to a dictionary.
|
||
|
||
Returns:
|
||
Dictionary containing all stat values
|
||
"""
|
||
return asdict(self)
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> 'Stats':
|
||
"""
|
||
Deserialize stats from a dictionary.
|
||
|
||
Args:
|
||
data: Dictionary containing stat values
|
||
|
||
Returns:
|
||
Stats instance
|
||
"""
|
||
return cls(
|
||
strength=data.get("strength", 10),
|
||
dexterity=data.get("dexterity", 10),
|
||
constitution=data.get("constitution", 10),
|
||
intelligence=data.get("intelligence", 10),
|
||
wisdom=data.get("wisdom", 10),
|
||
charisma=data.get("charisma", 10),
|
||
luck=data.get("luck", 8),
|
||
damage_bonus=data.get("damage_bonus", 0),
|
||
spell_power_bonus=data.get("spell_power_bonus", 0),
|
||
defense_bonus=data.get("defense_bonus", 0),
|
||
resistance_bonus=data.get("resistance_bonus", 0),
|
||
)
|
||
|
||
def copy(self) -> 'Stats':
|
||
"""
|
||
Create a deep copy of this Stats instance.
|
||
|
||
Returns:
|
||
New Stats instance with same values
|
||
"""
|
||
return Stats(
|
||
strength=self.strength,
|
||
dexterity=self.dexterity,
|
||
constitution=self.constitution,
|
||
intelligence=self.intelligence,
|
||
wisdom=self.wisdom,
|
||
charisma=self.charisma,
|
||
luck=self.luck,
|
||
damage_bonus=self.damage_bonus,
|
||
spell_power_bonus=self.spell_power_bonus,
|
||
defense_bonus=self.defense_bonus,
|
||
resistance_bonus=self.resistance_bonus,
|
||
)
|
||
|
||
def __repr__(self) -> str:
|
||
"""String representation showing all stats and computed properties."""
|
||
return (
|
||
f"Stats(STR={self.strength}, DEX={self.dexterity}, "
|
||
f"CON={self.constitution}, INT={self.intelligence}, "
|
||
f"WIS={self.wisdom}, CHA={self.charisma}, LUK={self.luck}, "
|
||
f"HP={self.hit_points}, MP={self.mana_points}, "
|
||
f"DMG={self.damage}, SP={self.spell_power}, "
|
||
f"DEF={self.defense}, RES={self.resistance}, "
|
||
f"CRIT_BONUS={self.crit_bonus:.1%}, HIT_BONUS={self.hit_bonus:.1%})"
|
||
)
|