Files
COC_API/app/game/models/professions.py

94 lines
4.2 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
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"),
tags=tags,
enemy=enemy,
)