""" Item affix system for procedural item generation. This module defines affixes (prefixes and suffixes) that can be attached to items to provide stat bonuses and generate Diablo-style names like "Flaming Dagger of Strength". """ from dataclasses import dataclass, field, asdict from typing import Dict, Any, List, Optional from app.models.enums import AffixType, AffixTier, DamageType, ItemRarity @dataclass class Affix: """ Represents a single item affix (prefix or suffix). Affixes provide stat bonuses and contribute to item naming. Prefixes appear before the item name: "Flaming Dagger" Suffixes appear after the item name: "Dagger of Strength" Attributes: affix_id: Unique identifier (e.g., "flaming", "of_strength") name: Display name for the affix (e.g., "Flaming", "of Strength") affix_type: PREFIX or SUFFIX tier: MINOR, MAJOR, or LEGENDARY (determines bonus magnitude) description: Human-readable description of the affix effect Stat Bonuses: stat_bonuses: Dict mapping stat name to bonus value Example: {"strength": 2, "constitution": 1} defense_bonus: Direct defense bonus resistance_bonus: Direct resistance bonus Weapon Properties (PREFIX only, elemental): damage_bonus: Flat damage bonus added to weapon damage_type: Elemental damage type (fire, ice, etc.) elemental_ratio: Portion of damage converted to elemental (0.0-1.0) crit_chance_bonus: Added to weapon crit chance crit_multiplier_bonus: Added to crit damage multiplier Restrictions: allowed_item_types: Empty list = all types allowed required_rarity: Minimum rarity to roll this affix (for legendary-only) """ affix_id: str name: str affix_type: AffixType tier: AffixTier description: str = "" # Stat bonuses (applies to any item) stat_bonuses: Dict[str, int] = field(default_factory=dict) defense_bonus: int = 0 resistance_bonus: int = 0 # Weapon-specific bonuses damage_bonus: int = 0 damage_type: Optional[DamageType] = None elemental_ratio: float = 0.0 crit_chance_bonus: float = 0.0 crit_multiplier_bonus: float = 0.0 # Restrictions allowed_item_types: List[str] = field(default_factory=list) required_rarity: Optional[str] = None def applies_elemental_damage(self) -> bool: """ Check if this affix converts damage to elemental. Returns: True if affix adds elemental damage component """ return self.damage_type is not None and self.elemental_ratio > 0.0 def is_legendary_only(self) -> bool: """ Check if this affix only rolls on legendary items. Returns: True if affix requires legendary rarity """ return self.required_rarity == "legendary" def can_apply_to(self, item_type: str, rarity: str) -> bool: """ Check if this affix can be applied to an item. Args: item_type: Type of item ("weapon", "armor", etc.) rarity: Item rarity ("common", "rare", "epic", "legendary") Returns: True if affix can be applied, False otherwise """ # Check rarity requirement if self.required_rarity and rarity != self.required_rarity: return False # Check item type restriction if self.allowed_item_types and item_type not in self.allowed_item_types: return False return True def to_dict(self) -> Dict[str, Any]: """ Serialize affix to dictionary. Returns: Dictionary containing all affix data """ data = asdict(self) data["affix_type"] = self.affix_type.value data["tier"] = self.tier.value if self.damage_type: data["damage_type"] = self.damage_type.value return data @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'Affix': """ Deserialize affix from dictionary. Args: data: Dictionary containing affix data Returns: Affix instance """ affix_type = AffixType(data["affix_type"]) tier = AffixTier(data["tier"]) damage_type = DamageType(data["damage_type"]) if data.get("damage_type") else None return cls( affix_id=data["affix_id"], name=data["name"], affix_type=affix_type, tier=tier, description=data.get("description", ""), stat_bonuses=data.get("stat_bonuses", {}), defense_bonus=data.get("defense_bonus", 0), resistance_bonus=data.get("resistance_bonus", 0), damage_bonus=data.get("damage_bonus", 0), damage_type=damage_type, elemental_ratio=data.get("elemental_ratio", 0.0), crit_chance_bonus=data.get("crit_chance_bonus", 0.0), crit_multiplier_bonus=data.get("crit_multiplier_bonus", 0.0), allowed_item_types=data.get("allowed_item_types", []), required_rarity=data.get("required_rarity"), ) def __repr__(self) -> str: """String representation of the affix.""" bonuses = [] if self.stat_bonuses: bonuses.append(f"stats={self.stat_bonuses}") if self.damage_bonus: bonuses.append(f"dmg+{self.damage_bonus}") if self.defense_bonus: bonuses.append(f"def+{self.defense_bonus}") if self.applies_elemental_damage(): bonuses.append(f"{self.damage_type.value}={self.elemental_ratio:.0%}") bonus_str = ", ".join(bonuses) if bonuses else "no bonuses" return f"Affix({self.name}, {self.affix_type.value}, {self.tier.value}, {bonus_str})" @dataclass class BaseItemTemplate: """ Template for base items used in procedural generation. Base items define the foundation (e.g., "Dagger", "Longsword", "Chainmail") that affixes attach to during item generation. Attributes: template_id: Unique identifier (e.g., "dagger", "longsword") name: Display name (e.g., "Dagger", "Longsword") item_type: Category ("weapon", "armor") description: Flavor text for the base item Base Stats: base_damage: Base weapon damage (weapons only) base_defense: Base armor defense (armor only) base_resistance: Base magic resistance (armor only) base_value: Base gold value before rarity/affix modifiers Weapon Properties: damage_type: Primary damage type (usually "physical") crit_chance: Base critical hit chance crit_multiplier: Base critical damage multiplier Generation: required_level: Minimum character level for this template drop_weight: Weighting for random selection (higher = more common) min_rarity: Minimum rarity this template can generate at """ template_id: str name: str item_type: str # "weapon" or "armor" description: str = "" # 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 # Weapon properties damage_type: str = "physical" crit_chance: float = 0.05 crit_multiplier: float = 2.0 # Generation settings required_level: int = 1 drop_weight: float = 1.0 min_rarity: str = "common" def can_generate_at_rarity(self, rarity: str) -> bool: """ Check if this template can generate at a given rarity. Some templates (like greatswords) may only drop at rare+. Args: rarity: Target rarity to check Returns: True if template can generate at this rarity """ rarity_order = ["common", "uncommon", "rare", "epic", "legendary"] min_index = rarity_order.index(self.min_rarity) target_index = rarity_order.index(rarity) return target_index >= min_index def can_drop_for_level(self, character_level: int) -> bool: """ Check if this template can drop for a character level. Args: character_level: Character's current level Returns: True if template can drop for this level """ return character_level >= self.required_level def to_dict(self) -> Dict[str, Any]: """ Serialize template to dictionary. Returns: Dictionary containing all template data """ return asdict(self) @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'BaseItemTemplate': """ Deserialize template from dictionary. Args: data: Dictionary containing template data Returns: BaseItemTemplate instance """ return cls( template_id=data["template_id"], name=data["name"], 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), damage_type=data.get("damage_type", "physical"), crit_chance=data.get("crit_chance", 0.05), crit_multiplier=data.get("crit_multiplier", 2.0), required_level=data.get("required_level", 1), drop_weight=data.get("drop_weight", 1.0), min_rarity=data.get("min_rarity", "common"), ) def __repr__(self) -> str: """String representation of the template.""" if self.item_type == "weapon": return ( f"BaseItemTemplate({self.name}, weapon, dmg={self.base_damage}, " f"crit={self.crit_chance*100:.0f}%, lvl={self.required_level})" ) elif self.item_type == "armor": return ( f"BaseItemTemplate({self.name}, armor, def={self.base_defense}, " f"res={self.base_resistance}, lvl={self.required_level})" ) else: return f"BaseItemTemplate({self.name}, {self.item_type})"