""" Item system for equipment, consumables, and quest items. This module defines the Item dataclass representing all types of items in the game, including weapons, armor, consumables, and quest items. """ from dataclasses import dataclass, field, asdict from typing import Dict, Any, List, Optional from app.models.enums import ItemType, ItemRarity, DamageType from app.models.effects import Effect @dataclass class Item: """ Represents an item in the game (weapon, armor, consumable, or quest item). Items can provide passive stat bonuses when equipped, have weapon/armor stats, or provide effects when consumed. Attributes: item_id: Unique identifier name: Display name item_type: Category (weapon, armor, consumable, quest_item) rarity: Rarity tier (common, uncommon, rare, epic, legendary) description: Item lore and information value: Gold value for buying/selling is_tradeable: Whether item can be sold on marketplace stat_bonuses: Passive bonuses to stats when equipped Example: {"strength": 5, "constitution": 3} effects_on_use: Effects applied when consumed (consumables only) Weapon-specific attributes: 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 Armor-specific attributes: defense: Physical defense bonus resistance: Magical resistance bonus Requirements (future): required_level: Minimum character level to use required_class: Class restriction (if any) """ item_id: str name: str item_type: ItemType rarity: ItemRarity = ItemRarity.COMMON description: str = "" value: int = 0 is_tradeable: bool = True # Passive bonuses (equipment) stat_bonuses: Dict[str, int] = field(default_factory=dict) # Active effects (consumables) effects_on_use: List[Effect] = field(default_factory=list) # Weapon-specific 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 # Elemental weapon properties (for split damage like Fire Sword) # These enable weapons to deal both physical AND elemental damage elemental_damage_type: Optional[DamageType] = None # Secondary damage type (fire, ice, etc.) physical_ratio: float = 1.0 # Portion of damage that is physical (0.0-1.0) elemental_ratio: float = 0.0 # Portion of damage that is elemental (0.0-1.0) # Armor-specific defense: int = 0 resistance: int = 0 # Requirements (future expansion) required_level: int = 1 required_class: Optional[str] = None # Affix tracking (for procedurally generated items) applied_affixes: List[str] = field(default_factory=list) # List of affix_ids base_template_id: Optional[str] = None # ID of base item template used generated_name: Optional[str] = None # Full generated name with affixes is_generated: bool = False # True if created by item generator def get_display_name(self) -> str: """ Get the item's display name. For generated items, returns the affix-enhanced name. For static items, returns the base name. Returns: Display name string """ return self.generated_name or self.name def is_weapon(self) -> bool: """Check if this item is a weapon.""" return self.item_type == ItemType.WEAPON def is_armor(self) -> bool: """Check if this item is armor.""" return self.item_type == ItemType.ARMOR def is_consumable(self) -> bool: """Check if this item is a consumable.""" return self.item_type == ItemType.CONSUMABLE def is_quest_item(self) -> bool: """Check if this item is a quest item.""" return self.item_type == ItemType.QUEST_ITEM def is_elemental_weapon(self) -> bool: """ Check if this weapon deals elemental damage (split damage). Elemental weapons deal both physical AND elemental damage, calculated separately against DEF and RES. Examples: Fire Sword: 70% physical / 30% fire Frost Blade: 60% physical / 40% ice Lightning Spear: 50% physical / 50% lightning Returns: True if weapon has elemental damage component """ return ( self.is_weapon() and self.elemental_ratio > 0.0 and 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. Args: character_level: Character's current level character_class: Character's class (if class restrictions exist) Returns: True if item can be equipped, False otherwise """ # Check level requirement if character_level < self.required_level: return False # Check class requirement if self.required_class and character_class != self.required_class: return False return True def get_total_stat_bonus(self, stat_name: str) -> int: """ Get the total bonus for a specific stat from this item. Args: stat_name: Name of the stat (e.g., "strength", "intelligence") Returns: Bonus value for that stat (0 if not present) """ return self.stat_bonuses.get(stat_name, 0) def to_dict(self) -> Dict[str, Any]: """ Serialize item to a dictionary. Returns: Dictionary containing all item data """ data = asdict(self) data["item_type"] = self.item_type.value data["rarity"] = self.rarity.value if self.damage_type: data["damage_type"] = self.damage_type.value if self.elemental_damage_type: data["elemental_damage_type"] = self.elemental_damage_type.value data["effects_on_use"] = [effect.to_dict() for effect in self.effects_on_use] # Include display_name for convenience data["display_name"] = self.get_display_name() return data @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'Item': """ Deserialize item from a dictionary. Args: data: Dictionary containing item data Returns: Item instance """ # Convert string values back to enums item_type = ItemType(data["item_type"]) rarity = ItemRarity(data.get("rarity", "common")) damage_type = DamageType(data["damage_type"]) if data.get("damage_type") else None elemental_damage_type = ( DamageType(data["elemental_damage_type"]) if data.get("elemental_damage_type") else None ) # Deserialize effects effects = [] if "effects_on_use" in data and data["effects_on_use"]: effects = [Effect.from_dict(e) for e in data["effects_on_use"]] return cls( item_id=data["item_id"], name=data["name"], item_type=item_type, rarity=rarity, description=data.get("description", ""), value=data.get("value", 0), is_tradeable=data.get("is_tradeable", True), stat_bonuses=data.get("stat_bonuses", {}), effects_on_use=effects, damage=data.get("damage", 0), damage_type=damage_type, crit_chance=data.get("crit_chance", 0.05), crit_multiplier=data.get("crit_multiplier", 2.0), elemental_damage_type=elemental_damage_type, physical_ratio=data.get("physical_ratio", 1.0), elemental_ratio=data.get("elemental_ratio", 0.0), defense=data.get("defense", 0), resistance=data.get("resistance", 0), required_level=data.get("required_level", 1), required_class=data.get("required_class"), # Affix tracking fields applied_affixes=data.get("applied_affixes", []), base_template_id=data.get("base_template_id"), generated_name=data.get("generated_name"), is_generated=data.get("is_generated", False), ) def __repr__(self) -> str: """String representation of the item.""" if self.is_weapon(): if self.is_elemental_weapon(): return ( f"Item({self.name}, elemental_weapon, dmg={self.damage}, " f"phys={self.physical_ratio:.0%}/{self.elemental_damage_type.value}={self.elemental_ratio:.0%}, " f"crit={self.crit_chance*100:.0f}%, value={self.value}g)" ) return ( f"Item({self.name}, weapon, dmg={self.damage}, " f"crit={self.crit_chance*100:.0f}%, value={self.value}g)" ) elif self.is_armor(): return ( f"Item({self.name}, armor, def={self.defense}, " f"res={self.resistance}, value={self.value}g)" ) elif self.is_consumable(): return ( f"Item({self.name}, consumable, " f"effects={len(self.effects_on_use)}, value={self.value}g)" ) else: return f"Item({self.name}, quest_item)"