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:
2025-11-26 19:54:58 -06:00
parent 4ced1b04df
commit a38906b445
16 changed files with 792 additions and 168 deletions

View File

@@ -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"

View File

@@ -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),

View File

@@ -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():

View File

@@ -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),
)

View File

@@ -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):

View File

@@ -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.

View File

@@ -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%})"
)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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

View File

@@ -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,