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
591 lines
21 KiB
Python
591 lines
21 KiB
Python
"""
|
|
Damage Calculator Service
|
|
|
|
A comprehensive, formula-driven damage calculation system for Code of Conquest.
|
|
Handles physical, magical, and elemental damage with LUK stat integration
|
|
for variance, critical hits, and accuracy.
|
|
|
|
Formulas:
|
|
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
|
|
- Crit bonus: Base 5% + (LUK * 0.5%), max 25%
|
|
- Lucky variance: 5% + (LUK * 0.25%) chance for higher damage roll
|
|
"""
|
|
|
|
import random
|
|
from dataclasses import dataclass, field
|
|
from typing import Dict, Any, Optional, List
|
|
|
|
from app.models.stats import Stats
|
|
from app.models.enums import DamageType
|
|
|
|
|
|
class CombatConstants:
|
|
"""
|
|
Combat system tuning constants.
|
|
|
|
These values control the balance of combat mechanics and can be
|
|
adjusted for game balance without modifying formula logic.
|
|
"""
|
|
|
|
# Stat Scaling
|
|
# How much primary stats (STR/INT) contribute to damage
|
|
# 0.75 means STR 14 adds +10.5 damage
|
|
STAT_SCALING_FACTOR: float = 0.75
|
|
|
|
# Hit/Miss System
|
|
BASE_MISS_CHANCE: float = 0.10 # 10% base miss rate
|
|
LUK_MISS_REDUCTION: float = 0.005 # 0.5% per LUK point
|
|
DEX_EVASION_BONUS: float = 0.0025 # 0.25% per DEX above 10
|
|
MIN_MISS_CHANCE: float = 0.05 # Hard cap: 5% minimum miss
|
|
|
|
# Critical Hits
|
|
DEFAULT_CRIT_CHANCE: float = 0.05 # 5% base crit
|
|
LUK_CRIT_BONUS: float = 0.005 # 0.5% per LUK point
|
|
MAX_CRIT_CHANCE: float = 0.25 # 25% cap (before skills)
|
|
DEFAULT_CRIT_MULTIPLIER: float = 2.0
|
|
|
|
# Damage Variance
|
|
BASE_VARIANCE_MIN: float = 0.95 # Minimum variance roll
|
|
BASE_VARIANCE_MAX: float = 1.05 # Maximum variance roll
|
|
LUCKY_VARIANCE_MIN: float = 1.00 # Lucky roll minimum
|
|
LUCKY_VARIANCE_MAX: float = 1.10 # Lucky roll maximum (10% bonus)
|
|
BASE_LUCKY_CHANCE: float = 0.05 # 5% base lucky roll chance
|
|
LUK_LUCKY_BONUS: float = 0.0025 # 0.25% per LUK point
|
|
|
|
# Defense Mitigation
|
|
# Ensures high-DEF targets still take meaningful damage
|
|
MIN_DAMAGE_RATIO: float = 0.20 # 20% of raw always goes through
|
|
MIN_DAMAGE: int = 1 # Absolute minimum damage
|
|
|
|
|
|
@dataclass
|
|
class DamageResult:
|
|
"""
|
|
Result of a damage calculation.
|
|
|
|
Contains the calculated damage values, whether the attack was a crit or miss,
|
|
and a human-readable message for the combat log.
|
|
|
|
Attributes:
|
|
total_damage: Final damage after all calculations
|
|
physical_damage: Physical component (for split damage)
|
|
elemental_damage: Elemental component (for split damage)
|
|
damage_type: Primary damage type (physical, fire, etc.)
|
|
is_critical: Whether the attack was a critical hit
|
|
is_miss: Whether the attack missed entirely
|
|
variance_roll: The variance multiplier that was applied
|
|
raw_damage: Damage before defense mitigation
|
|
message: Human-readable description for combat log
|
|
"""
|
|
|
|
total_damage: int = 0
|
|
physical_damage: int = 0
|
|
elemental_damage: int = 0
|
|
damage_type: DamageType = DamageType.PHYSICAL
|
|
elemental_type: Optional[DamageType] = None
|
|
is_critical: bool = False
|
|
is_miss: bool = False
|
|
variance_roll: float = 1.0
|
|
raw_damage: int = 0
|
|
message: str = ""
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""Serialize damage result to dictionary."""
|
|
return {
|
|
"total_damage": self.total_damage,
|
|
"physical_damage": self.physical_damage,
|
|
"elemental_damage": self.elemental_damage,
|
|
"damage_type": self.damage_type.value if self.damage_type else "physical",
|
|
"elemental_type": self.elemental_type.value if self.elemental_type else None,
|
|
"is_critical": self.is_critical,
|
|
"is_miss": self.is_miss,
|
|
"variance_roll": round(self.variance_roll, 3),
|
|
"raw_damage": self.raw_damage,
|
|
"message": self.message,
|
|
}
|
|
|
|
|
|
class DamageCalculator:
|
|
"""
|
|
Formula-driven damage calculator for combat.
|
|
|
|
This class provides static methods for calculating all types of damage
|
|
in the combat system, including hit/miss chances, critical hits,
|
|
damage variance, and defense mitigation.
|
|
|
|
All formulas integrate the LUK stat for meaningful randomness while
|
|
maintaining a hard cap on miss chance to prevent frustration.
|
|
"""
|
|
|
|
@staticmethod
|
|
def calculate_hit_chance(
|
|
attacker_luck: int,
|
|
defender_dexterity: int,
|
|
skill_bonus: float = 0.0
|
|
) -> float:
|
|
"""
|
|
Calculate hit probability for an attack.
|
|
|
|
Formula:
|
|
miss_chance = max(0.05, 0.10 - (LUK * 0.005) + ((DEX - 10) * 0.0025))
|
|
hit_chance = 1.0 - miss_chance
|
|
|
|
Args:
|
|
attacker_luck: Attacker's LUK stat
|
|
defender_dexterity: Defender's DEX stat
|
|
skill_bonus: Additional hit chance from skills (0.0 to 1.0)
|
|
|
|
Returns:
|
|
Hit probability as a float between 0.0 and 1.0
|
|
|
|
Examples:
|
|
LUK 8, DEX 10: miss = 10% - 4% + 0% = 6%
|
|
LUK 12, DEX 10: miss = 10% - 6% + 0% = 4% -> capped at 5%
|
|
LUK 8, DEX 15: miss = 10% - 4% + 1.25% = 7.25%
|
|
"""
|
|
# Base miss rate
|
|
base_miss = CombatConstants.BASE_MISS_CHANCE
|
|
|
|
# LUK reduces miss chance
|
|
luk_reduction = attacker_luck * CombatConstants.LUK_MISS_REDUCTION
|
|
|
|
# High DEX increases evasion (only DEX above 10 counts)
|
|
dex_above_base = max(0, defender_dexterity - 10)
|
|
dex_evasion = dex_above_base * CombatConstants.DEX_EVASION_BONUS
|
|
|
|
# Calculate final miss chance with hard cap
|
|
miss_chance = base_miss - luk_reduction + dex_evasion - skill_bonus
|
|
miss_chance = max(CombatConstants.MIN_MISS_CHANCE, miss_chance)
|
|
|
|
return 1.0 - miss_chance
|
|
|
|
@staticmethod
|
|
def calculate_crit_chance(
|
|
attacker_luck: int,
|
|
weapon_crit_chance: float = CombatConstants.DEFAULT_CRIT_CHANCE,
|
|
skill_bonus: float = 0.0
|
|
) -> float:
|
|
"""
|
|
Calculate critical hit probability.
|
|
|
|
Formula:
|
|
crit_chance = min(0.25, weapon_crit + (LUK * 0.005) + skill_bonus)
|
|
|
|
Args:
|
|
attacker_luck: Attacker's LUK stat
|
|
weapon_crit_chance: Base crit chance from weapon (default 5%)
|
|
skill_bonus: Additional crit chance from skills
|
|
|
|
Returns:
|
|
Crit probability as a float (capped at 25%)
|
|
|
|
Examples:
|
|
LUK 8, weapon 5%: crit = 5% + 4% = 9%
|
|
LUK 12, weapon 5%: crit = 5% + 6% = 11%
|
|
LUK 12, weapon 10%: crit = 10% + 6% = 16%
|
|
"""
|
|
# LUK bonus to crit
|
|
luk_bonus = attacker_luck * CombatConstants.LUK_CRIT_BONUS
|
|
|
|
# Total crit chance with cap
|
|
total_crit = weapon_crit_chance + luk_bonus + skill_bonus
|
|
|
|
return min(CombatConstants.MAX_CRIT_CHANCE, total_crit)
|
|
|
|
@staticmethod
|
|
def calculate_variance(attacker_luck: int) -> float:
|
|
"""
|
|
Calculate damage variance multiplier with LUK bonus.
|
|
|
|
Hybrid variance system:
|
|
- Base roll: 95% to 105% of damage
|
|
- LUK grants chance for "lucky roll": 100% to 110% instead
|
|
|
|
Args:
|
|
attacker_luck: Attacker's LUK stat
|
|
|
|
Returns:
|
|
Variance multiplier (typically 0.95 to 1.10)
|
|
|
|
Examples:
|
|
LUK 8: 7% chance for lucky roll (100-110%)
|
|
LUK 12: 8% chance for lucky roll
|
|
"""
|
|
# Calculate lucky roll chance
|
|
lucky_chance = (
|
|
CombatConstants.BASE_LUCKY_CHANCE +
|
|
(attacker_luck * CombatConstants.LUK_LUCKY_BONUS)
|
|
)
|
|
|
|
# Roll for lucky variance
|
|
if random.random() < lucky_chance:
|
|
# Lucky roll: higher damage range
|
|
return random.uniform(
|
|
CombatConstants.LUCKY_VARIANCE_MIN,
|
|
CombatConstants.LUCKY_VARIANCE_MAX
|
|
)
|
|
else:
|
|
# Normal roll
|
|
return random.uniform(
|
|
CombatConstants.BASE_VARIANCE_MIN,
|
|
CombatConstants.BASE_VARIANCE_MAX
|
|
)
|
|
|
|
@staticmethod
|
|
def apply_defense(
|
|
raw_damage: int,
|
|
defense: int,
|
|
min_damage_ratio: float = CombatConstants.MIN_DAMAGE_RATIO
|
|
) -> int:
|
|
"""
|
|
Apply defense mitigation with minimum damage guarantee.
|
|
|
|
Ensures at least 20% of raw damage always goes through,
|
|
preventing high-DEF tanks from becoming unkillable.
|
|
Absolute minimum is always 1 damage.
|
|
|
|
Args:
|
|
raw_damage: Damage before defense
|
|
defense: Target's defense value
|
|
min_damage_ratio: Minimum % of raw damage that goes through
|
|
|
|
Returns:
|
|
Final damage after mitigation (minimum 1)
|
|
|
|
Examples:
|
|
raw=20, def=5: 20 - 5 = 15 damage
|
|
raw=20, def=18: max(4, 2) = 4 damage (20% minimum)
|
|
raw=10, def=100: max(2, -90) = 2 damage (20% minimum)
|
|
"""
|
|
# Calculate mitigated damage
|
|
mitigated = raw_damage - defense
|
|
|
|
# Minimum damage is 20% of raw, or 1, whichever is higher
|
|
min_damage = max(CombatConstants.MIN_DAMAGE, int(raw_damage * min_damage_ratio))
|
|
|
|
return max(min_damage, mitigated)
|
|
|
|
@classmethod
|
|
def calculate_physical_damage(
|
|
cls,
|
|
attacker_stats: Stats,
|
|
defender_stats: Stats,
|
|
weapon_crit_chance: float = CombatConstants.DEFAULT_CRIT_CHANCE,
|
|
weapon_crit_multiplier: float = CombatConstants.DEFAULT_CRIT_MULTIPLIER,
|
|
ability_base_power: int = 0,
|
|
skill_hit_bonus: float = 0.0,
|
|
skill_crit_bonus: float = 0.0,
|
|
) -> DamageResult:
|
|
"""
|
|
Calculate physical damage for a melee/ranged attack.
|
|
|
|
Formula:
|
|
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 (includes weapon damage via damage property)
|
|
defender_stats: Defender's Stats (DEX, CON used)
|
|
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
|
|
skill_hit_bonus: Hit chance bonus from skills
|
|
skill_crit_bonus: Crit chance bonus from skills
|
|
|
|
Returns:
|
|
DamageResult with calculated damage and metadata
|
|
"""
|
|
result = DamageResult(damage_type=DamageType.PHYSICAL)
|
|
|
|
# Step 1: Check for miss
|
|
hit_chance = cls.calculate_hit_chance(
|
|
attacker_stats.luck,
|
|
defender_stats.dexterity,
|
|
skill_hit_bonus
|
|
)
|
|
|
|
if random.random() > hit_chance:
|
|
result.is_miss = True
|
|
result.message = "Attack missed!"
|
|
return result
|
|
|
|
# Step 2: Calculate base damage
|
|
# 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)
|
|
result.variance_roll = variance
|
|
damage = base_damage * variance
|
|
|
|
# Step 4: Check for critical hit
|
|
crit_chance = cls.calculate_crit_chance(
|
|
attacker_stats.luck,
|
|
weapon_crit_chance,
|
|
skill_crit_bonus
|
|
)
|
|
|
|
if random.random() < crit_chance:
|
|
result.is_critical = True
|
|
damage *= weapon_crit_multiplier
|
|
|
|
# Store raw damage before defense
|
|
result.raw_damage = int(damage)
|
|
|
|
# Step 5: Apply defense mitigation
|
|
final_damage = cls.apply_defense(int(damage), defender_stats.defense)
|
|
|
|
result.total_damage = final_damage
|
|
result.physical_damage = final_damage
|
|
|
|
# Build message
|
|
crit_text = " CRITICAL HIT!" if result.is_critical else ""
|
|
result.message = f"Dealt {final_damage} physical damage.{crit_text}"
|
|
|
|
return result
|
|
|
|
@classmethod
|
|
def calculate_magical_damage(
|
|
cls,
|
|
attacker_stats: Stats,
|
|
defender_stats: Stats,
|
|
ability_base_power: int,
|
|
damage_type: DamageType = DamageType.FIRE,
|
|
weapon_crit_chance: float = CombatConstants.DEFAULT_CRIT_CHANCE,
|
|
weapon_crit_multiplier: float = CombatConstants.DEFAULT_CRIT_MULTIPLIER,
|
|
skill_hit_bonus: float = 0.0,
|
|
skill_crit_bonus: float = 0.0,
|
|
) -> DamageResult:
|
|
"""
|
|
Calculate magical damage for a spell.
|
|
|
|
Spells CAN critically hit (same formula as physical).
|
|
LUK benefits all classes equally.
|
|
|
|
Formula:
|
|
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 (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.)
|
|
weapon_crit_chance: Crit chance (from focus/staff)
|
|
weapon_crit_multiplier: Crit damage multiplier
|
|
skill_hit_bonus: Hit chance bonus from skills
|
|
skill_crit_bonus: Crit chance bonus from skills
|
|
|
|
Returns:
|
|
DamageResult with calculated damage and metadata
|
|
"""
|
|
result = DamageResult(damage_type=damage_type)
|
|
|
|
# Step 1: Check for miss (spells can miss too)
|
|
hit_chance = cls.calculate_hit_chance(
|
|
attacker_stats.luck,
|
|
defender_stats.dexterity,
|
|
skill_hit_bonus
|
|
)
|
|
|
|
if random.random() > hit_chance:
|
|
result.is_miss = True
|
|
result.message = "Spell missed!"
|
|
return result
|
|
|
|
# Step 2: Calculate base damage
|
|
# 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)
|
|
result.variance_roll = variance
|
|
damage = base_damage * variance
|
|
|
|
# Step 4: Check for critical hit (spells CAN crit)
|
|
crit_chance = cls.calculate_crit_chance(
|
|
attacker_stats.luck,
|
|
weapon_crit_chance,
|
|
skill_crit_bonus
|
|
)
|
|
|
|
if random.random() < crit_chance:
|
|
result.is_critical = True
|
|
damage *= weapon_crit_multiplier
|
|
|
|
# Store raw damage before resistance
|
|
result.raw_damage = int(damage)
|
|
|
|
# Step 5: Apply resistance mitigation
|
|
final_damage = cls.apply_defense(int(damage), defender_stats.resistance)
|
|
|
|
result.total_damage = final_damage
|
|
result.elemental_damage = final_damage
|
|
|
|
# Build message
|
|
crit_text = " CRITICAL HIT!" if result.is_critical else ""
|
|
result.message = f"Dealt {final_damage} {damage_type.value} damage.{crit_text}"
|
|
|
|
return result
|
|
|
|
@classmethod
|
|
def calculate_elemental_weapon_damage(
|
|
cls,
|
|
attacker_stats: Stats,
|
|
defender_stats: Stats,
|
|
weapon_crit_chance: float,
|
|
weapon_crit_multiplier: float,
|
|
physical_ratio: float,
|
|
elemental_ratio: float,
|
|
elemental_type: DamageType,
|
|
ability_base_power: int = 0,
|
|
skill_hit_bonus: float = 0.0,
|
|
skill_crit_bonus: float = 0.0,
|
|
) -> DamageResult:
|
|
"""
|
|
Calculate split damage for elemental weapons (e.g., Fire Sword).
|
|
|
|
Elemental weapons deal both physical AND elemental damage,
|
|
calculated separately against DEF and RES respectively.
|
|
|
|
Formula:
|
|
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:
|
|
- Pure Physical: 100% / 0%
|
|
- Fire Sword: 70% / 30%
|
|
- Frost Blade: 60% / 40%
|
|
- Lightning Spear: 50% / 50%
|
|
|
|
Args:
|
|
attacker_stats: Attacker's Stats (damage and spell_power include equipment)
|
|
defender_stats: Defender's Stats
|
|
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)
|
|
elemental_ratio: Portion of damage that is elemental (0.0-1.0)
|
|
elemental_type: Type of elemental damage
|
|
ability_base_power: Additional base power from ability
|
|
skill_hit_bonus: Hit chance bonus from skills
|
|
skill_crit_bonus: Crit chance bonus from skills
|
|
|
|
Returns:
|
|
DamageResult with split physical/elemental damage
|
|
"""
|
|
result = DamageResult(
|
|
damage_type=DamageType.PHYSICAL,
|
|
elemental_type=elemental_type
|
|
)
|
|
|
|
# Step 1: Check for miss (single roll for entire attack)
|
|
hit_chance = cls.calculate_hit_chance(
|
|
attacker_stats.luck,
|
|
defender_stats.dexterity,
|
|
skill_hit_bonus
|
|
)
|
|
|
|
if random.random() > hit_chance:
|
|
result.is_miss = True
|
|
result.message = "Attack missed!"
|
|
return result
|
|
|
|
# Step 2: Check for critical (single roll applies to both components)
|
|
variance = cls.calculate_variance(attacker_stats.luck)
|
|
result.variance_roll = variance
|
|
|
|
crit_chance = cls.calculate_crit_chance(
|
|
attacker_stats.luck,
|
|
weapon_crit_chance,
|
|
skill_crit_bonus
|
|
)
|
|
is_crit = random.random() < crit_chance
|
|
result.is_critical = is_crit
|
|
crit_mult = weapon_crit_multiplier if is_crit else 1.0
|
|
|
|
# Step 3: Calculate physical component
|
|
# 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
|
|
# 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
|
|
result.physical_damage = phys_final
|
|
result.elemental_damage = elem_final
|
|
result.total_damage = phys_final + elem_final
|
|
result.raw_damage = int(phys_damage + elem_damage)
|
|
|
|
# Build message
|
|
crit_text = " CRITICAL HIT!" if is_crit else ""
|
|
result.message = (
|
|
f"Dealt {result.total_damage} damage "
|
|
f"({phys_final} physical + {elem_final} {elemental_type.value}).{crit_text}"
|
|
)
|
|
|
|
return result
|
|
|
|
@classmethod
|
|
def calculate_aoe_damage(
|
|
cls,
|
|
attacker_stats: Stats,
|
|
defender_stats_list: List[Stats],
|
|
ability_base_power: int,
|
|
damage_type: DamageType = DamageType.FIRE,
|
|
weapon_crit_chance: float = CombatConstants.DEFAULT_CRIT_CHANCE,
|
|
weapon_crit_multiplier: float = CombatConstants.DEFAULT_CRIT_MULTIPLIER,
|
|
skill_hit_bonus: float = 0.0,
|
|
skill_crit_bonus: float = 0.0,
|
|
) -> List[DamageResult]:
|
|
"""
|
|
Calculate AoE spell damage against multiple targets.
|
|
|
|
AoE spells deal FULL damage to all targets (balanced by higher mana costs).
|
|
Each target has independent hit/crit rolls but shares the base calculation.
|
|
|
|
Args:
|
|
attacker_stats: Attacker's Stats
|
|
defender_stats_list: List of defender Stats (one per target)
|
|
ability_base_power: Base power of the AoE spell
|
|
damage_type: Type of magical damage
|
|
weapon_crit_chance: Crit chance from focus/staff
|
|
weapon_crit_multiplier: Crit damage multiplier
|
|
skill_hit_bonus: Hit chance bonus from skills
|
|
skill_crit_bonus: Crit chance bonus from skills
|
|
|
|
Returns:
|
|
List of DamageResult, one per target
|
|
"""
|
|
results = []
|
|
|
|
# Each target gets independent damage calculation
|
|
for defender_stats in defender_stats_list:
|
|
result = cls.calculate_magical_damage(
|
|
attacker_stats=attacker_stats,
|
|
defender_stats=defender_stats,
|
|
ability_base_power=ability_base_power,
|
|
damage_type=damage_type,
|
|
weapon_crit_chance=weapon_crit_chance,
|
|
weapon_crit_multiplier=weapon_crit_multiplier,
|
|
skill_hit_bonus=skill_hit_bonus,
|
|
skill_crit_bonus=skill_crit_bonus,
|
|
)
|
|
results.append(result)
|
|
|
|
return results
|