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:
@@ -146,14 +146,59 @@ weapons:
|
||||
name: "Wizard Staff"
|
||||
item_type: "weapon"
|
||||
description: "A staff attuned to magical energy"
|
||||
base_damage: 8
|
||||
base_damage: 4
|
||||
base_spell_power: 12
|
||||
base_value: 45
|
||||
damage_type: "physical"
|
||||
damage_type: "arcane"
|
||||
crit_chance: 0.05
|
||||
crit_multiplier: 2.0
|
||||
required_level: 3
|
||||
drop_weight: 0.8
|
||||
|
||||
arcane_staff:
|
||||
template_id: "arcane_staff"
|
||||
name: "Arcane Staff"
|
||||
item_type: "weapon"
|
||||
description: "A powerful staff pulsing with arcane power"
|
||||
base_damage: 6
|
||||
base_spell_power: 18
|
||||
base_value: 90
|
||||
damage_type: "arcane"
|
||||
crit_chance: 0.06
|
||||
crit_multiplier: 2.0
|
||||
required_level: 5
|
||||
drop_weight: 0.6
|
||||
min_rarity: "uncommon"
|
||||
|
||||
# ==================== WANDS ====================
|
||||
wand:
|
||||
template_id: "wand"
|
||||
name: "Wand"
|
||||
item_type: "weapon"
|
||||
description: "A simple magical focus"
|
||||
base_damage: 2
|
||||
base_spell_power: 8
|
||||
base_value: 30
|
||||
damage_type: "arcane"
|
||||
crit_chance: 0.06
|
||||
crit_multiplier: 2.0
|
||||
required_level: 1
|
||||
drop_weight: 1.0
|
||||
|
||||
crystal_wand:
|
||||
template_id: "crystal_wand"
|
||||
name: "Crystal Wand"
|
||||
item_type: "weapon"
|
||||
description: "A wand topped with a magical crystal"
|
||||
base_damage: 3
|
||||
base_spell_power: 14
|
||||
base_value: 60
|
||||
damage_type: "arcane"
|
||||
crit_chance: 0.07
|
||||
crit_multiplier: 2.2
|
||||
required_level: 4
|
||||
drop_weight: 0.8
|
||||
|
||||
# ==================== RANGED ====================
|
||||
shortbow:
|
||||
template_id: "shortbow"
|
||||
|
||||
@@ -207,6 +207,7 @@ class BaseItemTemplate:
|
||||
|
||||
# Base stats
|
||||
base_damage: int = 0
|
||||
base_spell_power: int = 0 # For magical weapons (staves, wands)
|
||||
base_defense: int = 0
|
||||
base_resistance: int = 0
|
||||
base_value: int = 10
|
||||
@@ -276,6 +277,7 @@ class BaseItemTemplate:
|
||||
item_type=data["item_type"],
|
||||
description=data.get("description", ""),
|
||||
base_damage=data.get("base_damage", 0),
|
||||
base_spell_power=data.get("base_spell_power", 0),
|
||||
base_defense=data.get("base_defense", 0),
|
||||
base_resistance=data.get("base_resistance", 0),
|
||||
base_value=data.get("base_value", 10),
|
||||
|
||||
@@ -13,7 +13,7 @@ from app.models.stats import Stats
|
||||
from app.models.items import Item
|
||||
from app.models.skills import PlayerClass, SkillNode
|
||||
from app.models.effects import Effect
|
||||
from app.models.enums import EffectType, StatType
|
||||
from app.models.enums import EffectType, StatType, ItemType
|
||||
from app.models.origins import Origin
|
||||
|
||||
|
||||
@@ -92,7 +92,11 @@ class Character:
|
||||
|
||||
This is the CRITICAL METHOD that combines:
|
||||
1. Base stats (from character)
|
||||
2. Equipment bonuses (from equipped items)
|
||||
2. Equipment bonuses (from equipped items):
|
||||
- stat_bonuses dict applied to corresponding stats
|
||||
- Weapon damage added to damage_bonus
|
||||
- Weapon spell_power added to spell_power_bonus
|
||||
- Armor defense/resistance added to defense_bonus/resistance_bonus
|
||||
3. Skill tree bonuses (from unlocked skills)
|
||||
4. Active effect modifiers (buffs/debuffs)
|
||||
|
||||
@@ -100,18 +104,30 @@ class Character:
|
||||
active_effects: Currently active effects on this character (from combat)
|
||||
|
||||
Returns:
|
||||
Stats instance with all modifiers applied
|
||||
Stats instance with all modifiers applied (including computed
|
||||
damage, defense, resistance properties that incorporate bonuses)
|
||||
"""
|
||||
# Start with a copy of base stats
|
||||
effective = self.base_stats.copy()
|
||||
|
||||
# Apply equipment bonuses
|
||||
for item in self.equipped.values():
|
||||
# Apply stat bonuses from item (e.g., +3 strength)
|
||||
for stat_name, bonus in item.stat_bonuses.items():
|
||||
if hasattr(effective, stat_name):
|
||||
current_value = getattr(effective, stat_name)
|
||||
setattr(effective, stat_name, current_value + bonus)
|
||||
|
||||
# Add weapon damage and spell_power to bonus fields
|
||||
if item.item_type == ItemType.WEAPON:
|
||||
effective.damage_bonus += item.damage
|
||||
effective.spell_power_bonus += item.spell_power
|
||||
|
||||
# Add armor defense and resistance to bonus fields
|
||||
if item.item_type == ItemType.ARMOR:
|
||||
effective.defense_bonus += item.defense
|
||||
effective.resistance_bonus += item.resistance
|
||||
|
||||
# Apply skill tree bonuses
|
||||
skill_bonuses = self._get_skill_bonuses()
|
||||
for stat_name, bonus in skill_bonuses.items():
|
||||
|
||||
@@ -12,7 +12,7 @@ import random
|
||||
from app.models.stats import Stats
|
||||
from app.models.effects import Effect
|
||||
from app.models.abilities import Ability
|
||||
from app.models.enums import CombatStatus, EffectType
|
||||
from app.models.enums import CombatStatus, EffectType, DamageType
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -36,6 +36,12 @@ class Combatant:
|
||||
abilities: Available abilities for this combatant
|
||||
cooldowns: Map of ability_id to turns remaining
|
||||
initiative: Turn order value (rolled at combat start)
|
||||
weapon_crit_chance: Critical hit chance from equipped weapon
|
||||
weapon_crit_multiplier: Critical hit damage multiplier
|
||||
weapon_damage_type: Primary damage type of weapon
|
||||
elemental_damage_type: Secondary damage type for elemental weapons
|
||||
physical_ratio: Portion of damage that is physical (0.0-1.0)
|
||||
elemental_ratio: Portion of damage that is elemental (0.0-1.0)
|
||||
"""
|
||||
|
||||
combatant_id: str
|
||||
@@ -51,6 +57,16 @@ class Combatant:
|
||||
cooldowns: Dict[str, int] = field(default_factory=dict)
|
||||
initiative: int = 0
|
||||
|
||||
# Weapon properties (for combat calculations)
|
||||
weapon_crit_chance: float = 0.05
|
||||
weapon_crit_multiplier: float = 2.0
|
||||
weapon_damage_type: Optional[DamageType] = None
|
||||
|
||||
# Elemental weapon properties (for split damage)
|
||||
elemental_damage_type: Optional[DamageType] = None
|
||||
physical_ratio: float = 1.0
|
||||
elemental_ratio: float = 0.0
|
||||
|
||||
def is_alive(self) -> bool:
|
||||
"""Check if combatant is still alive."""
|
||||
return self.current_hp > 0
|
||||
@@ -228,6 +244,12 @@ class Combatant:
|
||||
"abilities": self.abilities,
|
||||
"cooldowns": self.cooldowns,
|
||||
"initiative": self.initiative,
|
||||
"weapon_crit_chance": self.weapon_crit_chance,
|
||||
"weapon_crit_multiplier": self.weapon_crit_multiplier,
|
||||
"weapon_damage_type": self.weapon_damage_type.value if self.weapon_damage_type else None,
|
||||
"elemental_damage_type": self.elemental_damage_type.value if self.elemental_damage_type else None,
|
||||
"physical_ratio": self.physical_ratio,
|
||||
"elemental_ratio": self.elemental_ratio,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -236,6 +258,15 @@ class Combatant:
|
||||
stats = Stats.from_dict(data["stats"])
|
||||
active_effects = [Effect.from_dict(e) for e in data.get("active_effects", [])]
|
||||
|
||||
# Parse damage types
|
||||
weapon_damage_type = None
|
||||
if data.get("weapon_damage_type"):
|
||||
weapon_damage_type = DamageType(data["weapon_damage_type"])
|
||||
|
||||
elemental_damage_type = None
|
||||
if data.get("elemental_damage_type"):
|
||||
elemental_damage_type = DamageType(data["elemental_damage_type"])
|
||||
|
||||
return cls(
|
||||
combatant_id=data["combatant_id"],
|
||||
name=data["name"],
|
||||
@@ -249,6 +280,12 @@ class Combatant:
|
||||
abilities=data.get("abilities", []),
|
||||
cooldowns=data.get("cooldowns", {}),
|
||||
initiative=data.get("initiative", 0),
|
||||
weapon_crit_chance=data.get("weapon_crit_chance", 0.05),
|
||||
weapon_crit_multiplier=data.get("weapon_crit_multiplier", 2.0),
|
||||
weapon_damage_type=weapon_damage_type,
|
||||
elemental_damage_type=elemental_damage_type,
|
||||
physical_ratio=data.get("physical_ratio", 1.0),
|
||||
elemental_ratio=data.get("elemental_ratio", 0.0),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ class DamageType(Enum):
|
||||
HOLY = "holy" # Holy/divine damage
|
||||
SHADOW = "shadow" # Dark/shadow magic damage
|
||||
POISON = "poison" # Poison damage (usually DoT)
|
||||
ARCANE = "arcane" # Pure magical damage (staves, wands)
|
||||
|
||||
|
||||
class ItemType(Enum):
|
||||
|
||||
@@ -33,7 +33,8 @@ class Item:
|
||||
effects_on_use: Effects applied when consumed (consumables only)
|
||||
|
||||
Weapon-specific attributes:
|
||||
damage: Base weapon damage
|
||||
damage: Base weapon damage (physical/melee/ranged)
|
||||
spell_power: Spell power for staves/wands (boosts spell damage)
|
||||
damage_type: Type of damage (physical, fire, etc.)
|
||||
crit_chance: Probability of critical hit (0.0 to 1.0)
|
||||
crit_multiplier: Damage multiplier on critical hit
|
||||
@@ -62,7 +63,8 @@ class Item:
|
||||
effects_on_use: List[Effect] = field(default_factory=list)
|
||||
|
||||
# Weapon-specific
|
||||
damage: int = 0
|
||||
damage: int = 0 # Physical damage for melee/ranged weapons
|
||||
spell_power: int = 0 # Spell power for staves/wands (boosts spell damage)
|
||||
damage_type: Optional[DamageType] = None
|
||||
crit_chance: float = 0.05 # 5% default critical hit chance
|
||||
crit_multiplier: float = 2.0 # 2x damage on critical hit
|
||||
@@ -136,6 +138,18 @@ class Item:
|
||||
self.elemental_damage_type is not None
|
||||
)
|
||||
|
||||
def is_magical_weapon(self) -> bool:
|
||||
"""
|
||||
Check if this weapon is a spell-casting weapon (staff, wand, tome).
|
||||
|
||||
Magical weapons provide spell_power which boosts spell damage,
|
||||
rather than physical damage for melee/ranged attacks.
|
||||
|
||||
Returns:
|
||||
True if weapon has spell_power (staves, wands, etc.)
|
||||
"""
|
||||
return self.is_weapon() and self.spell_power > 0
|
||||
|
||||
def can_equip(self, character_level: int, character_class: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Check if a character can equip this item.
|
||||
|
||||
@@ -22,12 +22,18 @@ class Stats:
|
||||
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)
|
||||
defense: Physical defense = constitution // 2
|
||||
resistance: Magical resistance = wisdom // 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
|
||||
@@ -38,6 +44,12 @@ class Stats:
|
||||
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:
|
||||
"""
|
||||
@@ -62,29 +74,65 @@ class Stats:
|
||||
"""
|
||||
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.
|
||||
Calculate physical defense from constitution and equipment.
|
||||
|
||||
Formula: constitution // 2
|
||||
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
|
||||
return (self.constitution // 2) + self.defense_bonus
|
||||
|
||||
@property
|
||||
def resistance(self) -> int:
|
||||
"""
|
||||
Calculate magical resistance from wisdom.
|
||||
Calculate magical resistance from wisdom and equipment.
|
||||
|
||||
Formula: wisdom // 2
|
||||
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
|
||||
return (self.wisdom // 2) + self.resistance_bonus
|
||||
|
||||
@property
|
||||
def crit_bonus(self) -> float:
|
||||
@@ -171,6 +219,10 @@ class Stats:
|
||||
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':
|
||||
@@ -188,6 +240,10 @@ class Stats:
|
||||
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:
|
||||
@@ -197,6 +253,7 @@ class Stats:
|
||||
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%})"
|
||||
)
|
||||
|
||||
@@ -109,6 +109,7 @@ class BaseItemLoader:
|
||||
# Set defaults for missing optional fields
|
||||
template_data.setdefault("description", "")
|
||||
template_data.setdefault("base_damage", 0)
|
||||
template_data.setdefault("base_spell_power", 0)
|
||||
template_data.setdefault("base_defense", 0)
|
||||
template_data.setdefault("base_resistance", 0)
|
||||
template_data.setdefault("base_value", 10)
|
||||
|
||||
@@ -571,17 +571,26 @@ class CombatService:
|
||||
message="Invalid or dead target"
|
||||
)
|
||||
|
||||
# Get attacker's weapon damage (or base damage for enemies)
|
||||
weapon_damage = self._get_weapon_damage(attacker)
|
||||
crit_chance = self._get_crit_chance(attacker)
|
||||
|
||||
# Calculate damage using DamageCalculator
|
||||
damage_result = DamageCalculator.calculate_physical_damage(
|
||||
attacker_stats=attacker.stats,
|
||||
defender_stats=target.stats,
|
||||
weapon_damage=weapon_damage,
|
||||
weapon_crit_chance=crit_chance,
|
||||
)
|
||||
# Check if this is an elemental weapon attack
|
||||
if attacker.elemental_ratio > 0.0 and attacker.elemental_damage_type:
|
||||
# Elemental weapon: split damage between physical and elemental
|
||||
damage_result = DamageCalculator.calculate_elemental_weapon_damage(
|
||||
attacker_stats=attacker.stats,
|
||||
defender_stats=target.stats,
|
||||
weapon_crit_chance=attacker.weapon_crit_chance,
|
||||
weapon_crit_multiplier=attacker.weapon_crit_multiplier,
|
||||
physical_ratio=attacker.physical_ratio,
|
||||
elemental_ratio=attacker.elemental_ratio,
|
||||
elemental_type=attacker.elemental_damage_type,
|
||||
)
|
||||
else:
|
||||
# Normal physical attack
|
||||
damage_result = DamageCalculator.calculate_physical_damage(
|
||||
attacker_stats=attacker.stats,
|
||||
defender_stats=target.stats,
|
||||
weapon_crit_chance=attacker.weapon_crit_chance,
|
||||
weapon_crit_multiplier=attacker.weapon_crit_multiplier,
|
||||
)
|
||||
|
||||
# Add target_id to result for tracking
|
||||
damage_result.target_id = target.combatant_id
|
||||
@@ -970,6 +979,25 @@ class CombatService:
|
||||
abilities = ["basic_attack"] # All characters have basic attack
|
||||
abilities.extend(character.unlocked_skills)
|
||||
|
||||
# Extract weapon properties from equipped weapon
|
||||
weapon = character.equipped.get("weapon")
|
||||
weapon_crit_chance = 0.05
|
||||
weapon_crit_multiplier = 2.0
|
||||
weapon_damage_type = DamageType.PHYSICAL
|
||||
elemental_damage_type = None
|
||||
physical_ratio = 1.0
|
||||
elemental_ratio = 0.0
|
||||
|
||||
if weapon and weapon.is_weapon():
|
||||
weapon_crit_chance = weapon.crit_chance
|
||||
weapon_crit_multiplier = weapon.crit_multiplier
|
||||
weapon_damage_type = weapon.damage_type or DamageType.PHYSICAL
|
||||
|
||||
if weapon.is_elemental_weapon():
|
||||
elemental_damage_type = weapon.elemental_damage_type
|
||||
physical_ratio = weapon.physical_ratio
|
||||
elemental_ratio = weapon.elemental_ratio
|
||||
|
||||
return Combatant(
|
||||
combatant_id=character.character_id,
|
||||
name=character.name,
|
||||
@@ -980,6 +1008,12 @@ class CombatService:
|
||||
max_mp=effective_stats.mana_points,
|
||||
stats=effective_stats,
|
||||
abilities=abilities,
|
||||
weapon_crit_chance=weapon_crit_chance,
|
||||
weapon_crit_multiplier=weapon_crit_multiplier,
|
||||
weapon_damage_type=weapon_damage_type,
|
||||
elemental_damage_type=elemental_damage_type,
|
||||
physical_ratio=physical_ratio,
|
||||
elemental_ratio=elemental_ratio,
|
||||
)
|
||||
|
||||
def _create_combatant_from_enemy(
|
||||
@@ -996,7 +1030,9 @@ class CombatService:
|
||||
if instance_index > 0:
|
||||
name = f"{template.name} #{instance_index + 1}"
|
||||
|
||||
stats = template.base_stats
|
||||
# Copy stats and populate damage_bonus with base_damage
|
||||
stats = template.base_stats.copy()
|
||||
stats.damage_bonus = template.base_damage
|
||||
|
||||
return Combatant(
|
||||
combatant_id=combatant_id,
|
||||
@@ -1008,23 +1044,15 @@ class CombatService:
|
||||
max_mp=stats.mana_points,
|
||||
stats=stats,
|
||||
abilities=template.abilities.copy(),
|
||||
weapon_crit_chance=template.crit_chance,
|
||||
weapon_crit_multiplier=2.0,
|
||||
weapon_damage_type=DamageType.PHYSICAL,
|
||||
)
|
||||
|
||||
def _get_weapon_damage(self, combatant: Combatant) -> int:
|
||||
"""Get weapon damage for a combatant."""
|
||||
# For enemies, use base_damage from template
|
||||
if not combatant.is_player:
|
||||
# Base damage stored in combatant data or default
|
||||
return 8 # Default enemy damage
|
||||
|
||||
# For players, would check equipped weapon
|
||||
# TODO: Check character's equipped weapon
|
||||
return 5 # Default unarmed damage
|
||||
|
||||
def _get_crit_chance(self, combatant: Combatant) -> float:
|
||||
"""Get critical hit chance for a combatant."""
|
||||
# Base 5% + LUK bonus
|
||||
return 0.05 + combatant.stats.crit_bonus
|
||||
# Weapon crit chance + LUK bonus
|
||||
return combatant.weapon_crit_chance + combatant.stats.crit_bonus
|
||||
|
||||
def _get_default_target(
|
||||
self,
|
||||
|
||||
@@ -6,9 +6,11 @@ Handles physical, magical, and elemental damage with LUK stat integration
|
||||
for variance, critical hits, and accuracy.
|
||||
|
||||
Formulas:
|
||||
Physical: (Weapon_Base + STR * 0.75) * Variance * Crit_Mult - DEF
|
||||
Magical: (Ability_Base + INT * 0.75) * Variance * Crit_Mult - RES
|
||||
Elemental: Split between physical and magical components
|
||||
Physical: (effective_stats.damage + ability_power) * Variance * Crit_Mult - DEF
|
||||
where effective_stats.damage = int(STR * 0.75) + damage_bonus (from weapon)
|
||||
Magical: (effective_stats.spell_power + ability_power) * Variance * Crit_Mult - RES
|
||||
where effective_stats.spell_power = int(INT * 0.75) + spell_power_bonus (from staff/wand)
|
||||
Elemental: Split between physical and magical components using ratios
|
||||
|
||||
LUK Integration:
|
||||
- Miss reduction: 10% base - (LUK * 0.5%), hard cap at 5% miss
|
||||
@@ -275,7 +277,6 @@ class DamageCalculator:
|
||||
cls,
|
||||
attacker_stats: Stats,
|
||||
defender_stats: Stats,
|
||||
weapon_damage: int = 0,
|
||||
weapon_crit_chance: float = CombatConstants.DEFAULT_CRIT_CHANCE,
|
||||
weapon_crit_multiplier: float = CombatConstants.DEFAULT_CRIT_MULTIPLIER,
|
||||
ability_base_power: int = 0,
|
||||
@@ -286,13 +287,13 @@ class DamageCalculator:
|
||||
Calculate physical damage for a melee/ranged attack.
|
||||
|
||||
Formula:
|
||||
Base = Weapon_Base + Ability_Power + (STR * 0.75)
|
||||
Base = attacker_stats.damage + ability_base_power
|
||||
where attacker_stats.damage = int(STR * 0.75) + damage_bonus
|
||||
Damage = Base * Variance * Crit_Mult - DEF
|
||||
|
||||
Args:
|
||||
attacker_stats: Attacker's Stats (STR, LUK used)
|
||||
attacker_stats: Attacker's Stats (includes weapon damage via damage property)
|
||||
defender_stats: Defender's Stats (DEX, CON used)
|
||||
weapon_damage: Base damage from equipped weapon
|
||||
weapon_crit_chance: Crit chance from weapon (default 5%)
|
||||
weapon_crit_multiplier: Crit damage multiplier (default 2.0x)
|
||||
ability_base_power: Additional base power from ability
|
||||
@@ -317,9 +318,8 @@ class DamageCalculator:
|
||||
return result
|
||||
|
||||
# Step 2: Calculate base damage
|
||||
# Formula: weapon + ability + (STR * scaling_factor)
|
||||
str_bonus = attacker_stats.strength * CombatConstants.STAT_SCALING_FACTOR
|
||||
base_damage = weapon_damage + ability_base_power + str_bonus
|
||||
# attacker_stats.damage already includes: int(STR * 0.75) + damage_bonus (weapon)
|
||||
base_damage = attacker_stats.damage + ability_base_power
|
||||
|
||||
# Step 3: Apply variance
|
||||
variance = cls.calculate_variance(attacker_stats.luck)
|
||||
@@ -371,11 +371,12 @@ class DamageCalculator:
|
||||
LUK benefits all classes equally.
|
||||
|
||||
Formula:
|
||||
Base = Ability_Power + (INT * 0.75)
|
||||
Base = attacker_stats.spell_power + ability_base_power
|
||||
where attacker_stats.spell_power = int(INT * 0.75) + spell_power_bonus
|
||||
Damage = Base * Variance * Crit_Mult - RES
|
||||
|
||||
Args:
|
||||
attacker_stats: Attacker's Stats (INT, LUK used)
|
||||
attacker_stats: Attacker's Stats (includes staff/wand spell_power via spell_power property)
|
||||
defender_stats: Defender's Stats (DEX, WIS used)
|
||||
ability_base_power: Base power of the spell
|
||||
damage_type: Type of magical damage (fire, ice, etc.)
|
||||
@@ -402,9 +403,8 @@ class DamageCalculator:
|
||||
return result
|
||||
|
||||
# Step 2: Calculate base damage
|
||||
# Formula: ability + (INT * scaling_factor)
|
||||
int_bonus = attacker_stats.intelligence * CombatConstants.STAT_SCALING_FACTOR
|
||||
base_damage = ability_base_power + int_bonus
|
||||
# attacker_stats.spell_power already includes: int(INT * 0.75) + spell_power_bonus (staff/wand)
|
||||
base_damage = attacker_stats.spell_power + ability_base_power
|
||||
|
||||
# Step 3: Apply variance
|
||||
variance = cls.calculate_variance(attacker_stats.luck)
|
||||
@@ -442,7 +442,6 @@ class DamageCalculator:
|
||||
cls,
|
||||
attacker_stats: Stats,
|
||||
defender_stats: Stats,
|
||||
weapon_damage: int,
|
||||
weapon_crit_chance: float,
|
||||
weapon_crit_multiplier: float,
|
||||
physical_ratio: float,
|
||||
@@ -459,8 +458,8 @@ class DamageCalculator:
|
||||
calculated separately against DEF and RES respectively.
|
||||
|
||||
Formula:
|
||||
Physical = (Weapon * PHYS_RATIO + STR * 0.75 * PHYS_RATIO) - DEF
|
||||
Elemental = (Weapon * ELEM_RATIO + INT * 0.75 * ELEM_RATIO) - RES
|
||||
Physical = (attacker_stats.damage + ability_power) * PHYS_RATIO - DEF
|
||||
Elemental = (attacker_stats.spell_power + ability_power) * ELEM_RATIO - RES
|
||||
Total = Physical + Elemental
|
||||
|
||||
Recommended Split Ratios:
|
||||
@@ -470,9 +469,8 @@ class DamageCalculator:
|
||||
- Lightning Spear: 50% / 50%
|
||||
|
||||
Args:
|
||||
attacker_stats: Attacker's Stats
|
||||
attacker_stats: Attacker's Stats (damage and spell_power include equipment)
|
||||
defender_stats: Defender's Stats
|
||||
weapon_damage: Base weapon damage
|
||||
weapon_crit_chance: Crit chance from weapon
|
||||
weapon_crit_multiplier: Crit damage multiplier
|
||||
physical_ratio: Portion of damage that is physical (0.0-1.0)
|
||||
@@ -516,17 +514,15 @@ class DamageCalculator:
|
||||
crit_mult = weapon_crit_multiplier if is_crit else 1.0
|
||||
|
||||
# Step 3: Calculate physical component
|
||||
# Physical uses STR scaling
|
||||
phys_base = (weapon_damage + ability_base_power) * physical_ratio
|
||||
str_bonus = attacker_stats.strength * CombatConstants.STAT_SCALING_FACTOR * physical_ratio
|
||||
phys_damage = (phys_base + str_bonus) * variance * crit_mult
|
||||
# attacker_stats.damage includes: int(STR * 0.75) + damage_bonus (weapon)
|
||||
phys_base = (attacker_stats.damage + ability_base_power) * physical_ratio
|
||||
phys_damage = phys_base * variance * crit_mult
|
||||
phys_final = cls.apply_defense(int(phys_damage), defender_stats.defense)
|
||||
|
||||
# Step 4: Calculate elemental component
|
||||
# Elemental uses INT scaling
|
||||
elem_base = (weapon_damage + ability_base_power) * elemental_ratio
|
||||
int_bonus = attacker_stats.intelligence * CombatConstants.STAT_SCALING_FACTOR * elemental_ratio
|
||||
elem_damage = (elem_base + int_bonus) * variance * crit_mult
|
||||
# attacker_stats.spell_power includes: int(INT * 0.75) + spell_power_bonus (staff/wand)
|
||||
elem_base = (attacker_stats.spell_power + ability_base_power) * elemental_ratio
|
||||
elem_damage = elem_base * variance * crit_mult
|
||||
elem_final = cls.apply_defense(int(elem_damage), defender_stats.resistance)
|
||||
|
||||
# Step 5: Combine results
|
||||
|
||||
@@ -317,6 +317,7 @@ class ItemGenerator:
|
||||
|
||||
# Base values from template
|
||||
damage = base_template.base_damage + combined_stats["damage_bonus"]
|
||||
spell_power = base_template.base_spell_power # Magical weapon damage
|
||||
defense = base_template.base_defense + combined_stats["defense_bonus"]
|
||||
resistance = base_template.base_resistance + combined_stats["resistance_bonus"]
|
||||
crit_chance = base_template.crit_chance + combined_stats["crit_chance_bonus"]
|
||||
@@ -353,6 +354,7 @@ class ItemGenerator:
|
||||
stat_bonuses=combined_stats["stat_bonuses"],
|
||||
effects_on_use=[], # Not a consumable
|
||||
damage=damage,
|
||||
spell_power=spell_power, # Magical weapon damage bonus
|
||||
damage_type=DamageType(base_template.damage_type) if damage > 0 else None,
|
||||
crit_chance=crit_chance,
|
||||
crit_multiplier=crit_multiplier,
|
||||
|
||||
Reference in New Issue
Block a user