""" Base Item Loader Service - YAML-based base item template loading. This service loads base item templates (weapons, armor) from YAML files, providing the foundation for procedural item generation. """ from pathlib import Path from typing import Dict, List, Optional import random import yaml from app.models.affixes import BaseItemTemplate from app.utils.logging import get_logger logger = get_logger(__file__) # Rarity order for comparison RARITY_ORDER = { "common": 0, "uncommon": 1, "rare": 2, "epic": 3, "legendary": 4 } class BaseItemLoader: """ Loads and manages base item templates from YAML configuration files. This allows game designers to define base items without touching code. Templates are organized into weapons.yaml and armor.yaml files. """ def __init__(self, data_dir: Optional[str] = None): """ Initialize the base item loader. Args: data_dir: Path to directory containing base item YAML files Defaults to /app/data/base_items/ """ if data_dir is None: # Default to app/data/base_items relative to this file current_file = Path(__file__) app_dir = current_file.parent.parent # Go up to /app data_dir = str(app_dir / "data" / "base_items") self.data_dir = Path(data_dir) self._weapon_cache: Dict[str, BaseItemTemplate] = {} self._armor_cache: Dict[str, BaseItemTemplate] = {} self._loaded = False logger.info("BaseItemLoader initialized", data_dir=str(self.data_dir)) def _ensure_loaded(self) -> None: """Ensure templates are loaded before any operation.""" if not self._loaded: self.load_all() def load_all(self) -> None: """Load all base item templates from YAML files.""" if not self.data_dir.exists(): logger.warning("Base item data directory not found", path=str(self.data_dir)) return # Load weapons weapons_file = self.data_dir / "weapons.yaml" if weapons_file.exists(): self._load_templates_from_file(weapons_file, "weapons", self._weapon_cache) # Load armor armor_file = self.data_dir / "armor.yaml" if armor_file.exists(): self._load_templates_from_file(armor_file, "armor", self._armor_cache) self._loaded = True logger.info( "Base item templates loaded", weapon_count=len(self._weapon_cache), armor_count=len(self._armor_cache) ) def _load_templates_from_file( self, yaml_file: Path, key: str, cache: Dict[str, BaseItemTemplate] ) -> None: """ Load templates from a YAML file into the cache. Args: yaml_file: Path to the YAML file key: Top-level key in YAML (e.g., "weapons", "armor") cache: Cache dictionary to populate """ try: with open(yaml_file, 'r') as f: data = yaml.safe_load(f) templates_data = data.get(key, {}) for template_id, template_data in templates_data.items(): # Ensure template_id is set template_data["template_id"] = template_id # 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) template_data.setdefault("damage_type", "physical") template_data.setdefault("crit_chance", 0.05) template_data.setdefault("crit_multiplier", 2.0) template_data.setdefault("required_level", 1) template_data.setdefault("drop_weight", 1.0) template_data.setdefault("min_rarity", "common") template = BaseItemTemplate.from_dict(template_data) cache[template.template_id] = template logger.debug( "Templates loaded from file", file=str(yaml_file), count=len(templates_data) ) except Exception as e: logger.error( "Failed to load base item file", file=str(yaml_file), error=str(e) ) def get_template(self, template_id: str) -> Optional[BaseItemTemplate]: """ Get a specific template by ID. Args: template_id: Unique template identifier Returns: BaseItemTemplate instance or None if not found """ self._ensure_loaded() if template_id in self._weapon_cache: return self._weapon_cache[template_id] if template_id in self._armor_cache: return self._armor_cache[template_id] return None def get_eligible_templates( self, item_type: str, rarity: str, character_level: int = 1 ) -> List[BaseItemTemplate]: """ Get all templates eligible for generation. Args: item_type: Type of item ("weapon", "armor") rarity: Target rarity character_level: Player level for eligibility Returns: List of eligible BaseItemTemplate instances """ self._ensure_loaded() # Select the appropriate cache if item_type == "weapon": cache = self._weapon_cache elif item_type == "armor": cache = self._armor_cache else: logger.warning("Unknown item type", item_type=item_type) return [] eligible = [] for template in cache.values(): # Check level requirement if not template.can_drop_for_level(character_level): continue # Check rarity requirement if not template.can_generate_at_rarity(rarity): continue eligible.append(template) return eligible def get_random_template( self, item_type: str, rarity: str, character_level: int = 1 ) -> Optional[BaseItemTemplate]: """ Get a random eligible template, weighted by drop_weight. Args: item_type: Type of item ("weapon", "armor") rarity: Target rarity character_level: Player level for eligibility Returns: Random eligible BaseItemTemplate or None if none available """ eligible = self.get_eligible_templates(item_type, rarity, character_level) if not eligible: logger.warning( "No templates match criteria", item_type=item_type, rarity=rarity, level=character_level ) return None # Weighted random selection based on drop_weight weights = [t.drop_weight for t in eligible] return random.choices(eligible, weights=weights, k=1)[0] def get_all_weapons(self) -> Dict[str, BaseItemTemplate]: """ Get all cached weapon templates. Returns: Dictionary of weapon templates """ self._ensure_loaded() return self._weapon_cache.copy() def get_all_armor(self) -> Dict[str, BaseItemTemplate]: """ Get all cached armor templates. Returns: Dictionary of armor templates """ self._ensure_loaded() return self._armor_cache.copy() def clear_cache(self) -> None: """Clear the template cache, forcing reload on next access.""" self._weapon_cache.clear() self._armor_cache.clear() self._loaded = False logger.debug("Base item template cache cleared") # Global instance for convenience _loader_instance: Optional[BaseItemLoader] = None def get_base_item_loader() -> BaseItemLoader: """ Get the global BaseItemLoader instance. Returns: Singleton BaseItemLoader instance """ global _loader_instance if _loader_instance is None: _loader_instance = BaseItemLoader() return _loader_instance