from __future__ import annotations from dataclasses import dataclass, field from typing import Dict, Optional from app.game.models.enemies import EnemyProfile @dataclass(frozen=True) class Profession: id: str name: str description: str primary_stat: str base_hp: int base_mp: int physical_attack_per_level: float physical_defense_per_level: float magic_attack_per_level: float magic_defense_per_level: float ability_paths: list[str] = field(default_factory=list) tags: list[str] = field(default_factory=list) # e.g., {"playable"}, {"leader","elite"} enemy: Optional[EnemyProfile] = None # ⬅ optional enemy-only tuning @property def is_playable(self) -> bool: return "playable" in self.tags @staticmethod def from_yaml(data: Dict[str, object]) -> "Profession": # ---- validation of required profession fields ---- required = [ "id", "name", "description", "primary_stat", "base_hp", "base_mp", "physical_attack_per_level", "physical_defense_per_level", "magic_attack_per_level", "magic_defense_per_level" ] missing = [k for k in required if k not in data] if missing: raise ValueError(f"Profession missing required fields: {missing}") # ---- cast helpers (robust to str/int/float in YAML) ---- def as_int(x, field_name): try: return int(x) except Exception: raise ValueError(f"{field_name} must be int, got {x!r}") def as_float(x, field_name): try: return float(x) except Exception: raise ValueError(f"{field_name} must be float, got {x!r}") # ---- tags (optional) ---- tags = list(data.get("tags", []) or []) # ---- enemy block (optional) ---- enemy_block = data.get("enemy") enemy: Optional[EnemyProfile] = None if enemy_block: eb = enemy_block or {} # typed extraction with defaults from EnemyProfile enemy = EnemyProfile( level_bias=as_int(eb.get("level_bias", 0), "enemy.level_bias"), level_variance=as_int(eb.get("level_variance", 1), "enemy.level_variance"), min_level=as_int(eb.get("min_level", 1), "enemy.min_level"), max_level=as_int(eb.get("max_level", 999), "enemy.max_level"), hp_mult=as_float(eb.get("hp_mult", 1.0), "enemy.hp_mult"), dmg_mult=as_float(eb.get("dmg_mult", 1.0), "enemy.dmg_mult"), armor_mult=as_float(eb.get("armor_mult", 1.0), "enemy.armor_mult"), speed_mult=as_float(eb.get("speed_mult", 1.0), "enemy.speed_mult"), stat_weights={str(k): float(v) for k, v in (eb.get("stat_weights", {}) or {}).items()}, xp_base=as_int(eb.get("xp_base", 10), "enemy.xp_base"), xp_per_level=as_int(eb.get("xp_per_level", 5), "enemy.xp_per_level"), loot_tier=str(eb.get("loot_tier", "common")), ) # sanity checks if enemy.min_level < 1: raise ValueError("enemy.min_level must be >= 1") if enemy.max_level < enemy.min_level: raise ValueError("enemy.max_level must be >= enemy.min_level") if any(v < 0 for v in (enemy.hp_mult, enemy.dmg_mult, enemy.armor_mult, enemy.speed_mult)): raise ValueError("enemy multipliers must be >= 0") # ---- construct Profession ---- return Profession( id=str(data["id"]), name=str(data["name"]), description=str(data["description"]), primary_stat=str(data["primary_stat"]), base_hp=as_int(data["base_hp"], "base_hp"), base_mp=as_int(data["base_mp"], "base_mp"), physical_attack_per_level=as_float(data["physical_attack_per_level"], "physical_attack_per_level"), physical_defense_per_level=as_float(data["physical_defense_per_level"], "physical_defense_per_level"), magic_attack_per_level=as_float(data["magic_attack_per_level"], "magic_attack_per_level"), magic_defense_per_level=as_float(data["magic_defense_per_level"], "magic_defense_per_level"), ability_paths=(data.get("ability_paths",[])), tags=tags, enemy=enemy, )