96 lines
4.3 KiB
Python
96 lines
4.3 KiB
Python
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,
|
|
)
|