Combat Backend & Data Models
- Implement Combat Service - Implement Damage Calculator - Implement Effect Processor - Implement Combat Actions - Created Combat API Endpoints
This commit is contained in:
594
api/app/services/damage_calculator.py
Normal file
594
api/app/services/damage_calculator.py
Normal file
@@ -0,0 +1,594 @@
|
||||
"""
|
||||
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: (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
|
||||
|
||||
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_damage: int = 0,
|
||||
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 = Weapon_Base + Ability_Power + (STR * 0.75)
|
||||
Damage = Base * Variance * Crit_Mult - DEF
|
||||
|
||||
Args:
|
||||
attacker_stats: Attacker's Stats (STR, LUK used)
|
||||
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
|
||||
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
|
||||
# Formula: weapon + ability + (STR * scaling_factor)
|
||||
str_bonus = attacker_stats.strength * CombatConstants.STAT_SCALING_FACTOR
|
||||
base_damage = weapon_damage + ability_base_power + str_bonus
|
||||
|
||||
# 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 = Ability_Power + (INT * 0.75)
|
||||
Damage = Base * Variance * Crit_Mult - RES
|
||||
|
||||
Args:
|
||||
attacker_stats: Attacker's Stats (INT, LUK used)
|
||||
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
|
||||
# Formula: ability + (INT * scaling_factor)
|
||||
int_bonus = attacker_stats.intelligence * CombatConstants.STAT_SCALING_FACTOR
|
||||
base_damage = ability_base_power + int_bonus
|
||||
|
||||
# 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_damage: int,
|
||||
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 = (Weapon * PHYS_RATIO + STR * 0.75 * PHYS_RATIO) - DEF
|
||||
Elemental = (Weapon * ELEM_RATIO + INT * 0.75 * 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
|
||||
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)
|
||||
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
|
||||
# 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
|
||||
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
|
||||
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
|
||||
Reference in New Issue
Block a user