init commit

This commit is contained in:
2025-11-02 01:14:41 -05:00
commit 7bf81109b3
31 changed files with 2387 additions and 0 deletions

View File

@@ -0,0 +1,181 @@
# hero_catalog.py
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Literal, Any
import yaml # PyYAML
from app.models.enums import HeroClass
from app.models.primitives import Attributes, Resources
TargetType = Literal["self", "ally", "all_allies", "single_enemy", "all_enemies", "jumps_3_targets"]
@dataclass
class StatusSpec:
chance: float
potency: int
duration_turns: int
@dataclass
class SkillRef:
id: str
min_level: int = 1
@dataclass
class SpellDef:
id: str
element: Optional[str] = None
cost_mp: int = 0
power: int = 0
scaling: Dict[str, float] = field(default_factory=dict) # e.g., {"int": 0.9, "luk": 0.03}
variance: float = 0.0
target: TargetType = "single_enemy"
tags: List[str] = field(default_factory=list)
status_chance: Dict[str, StatusSpec] = field(default_factory=dict) # {"burn": StatusSpec(...)}
crit_bonus: Optional[Dict[str, float]] = None # {"chance":0.05,"multiplier":1.5}
@staticmethod
def from_yaml(id_: str, raw: Dict[str, Any]) -> "SpellDef":
sc = {}
for k, v in (raw.get("status_chance") or {}).items():
sc[k] = StatusSpec(**v)
return SpellDef(
id=id_,
element=raw.get("element"),
cost_mp=int((raw.get("cost") or {}).get("mp", 0)),
power=int(raw.get("power", 0)),
scaling={k: float(v) for k, v in (raw.get("scaling") or {}).items()},
variance=float(raw.get("variance", 0.0)),
target=(raw.get("target") or "single_enemy"),
tags=list(raw.get("tags") or []),
status_chance=sc,
crit_bonus=raw.get("crit_bonus"),
)
@dataclass
class SkillDef:
id: str
type: Literal["active", "passive", "buff", "debuff", "utility"] = "active"
cost_mp: int = 0
tags: List[str] = field(default_factory=list)
# Free-form payloads your engine can interpret:
passive_modifiers: Dict[str, Any] = field(default_factory=dict) # e.g., damage_multiplier_vs_tags.fire=1.10
effects: List[Dict[str, Any]] = field(default_factory=list) # e.g., [{"kind":"buff","stat":"int","amount":2,...}]
@staticmethod
def from_yaml(id_: str, raw: Dict[str, Any]) -> "SkillDef":
return SkillDef(
id=id_,
type=(raw.get("type") or "active"),
cost_mp=int((raw.get("cost") or {}).get("mp", 0)),
tags=list(raw.get("tags") or []),
passive_modifiers=dict(raw.get("passive_modifiers") or {}),
effects=list(raw.get("effects") or []),
)
@dataclass
class HeroArchetype:
key: HeroClass
display_name: str
background: str
base_attributes: Attributes = field(default_factory=Attributes)
starting_resources: Resources = field(default_factory=Resources)
starting_skills: List[str] = field(default_factory=list)
starting_spells: List[str] = field(default_factory=list)
skill_trees: Dict[str, List[SkillRef]] = field(default_factory=dict)
spells_by_level: Dict[int, List[str]] = field(default_factory=dict)
bonus_abilities: List[str] = field(default_factory=list)
# Local, per-class catalogs (optional—can be empty if you centralize elsewhere)
skills: Dict[str, SkillDef] = field(default_factory=dict)
spells: Dict[str, SpellDef] = field(default_factory=dict)
class HeroDataRegistry:
"""
In-memory catalog of YAML-defined class sheets.
Keyed by HeroClass (enum).
"""
def __init__(self) -> None:
self._by_class: Dict[HeroClass, HeroArchetype] = {}
self._skills: Dict[str, SkillDef] = {}
self._spells: Dict[str, SpellDef] = {}
def load_dir(self, directory: Path | str) -> None:
directory = Path(directory)
for path in sorted(directory.glob("*.y*ml")):
with path.open("r", encoding="utf-8") as f:
raw = yaml.safe_load(f) or {}
# --- required ---
key_raw = raw.get("key")
if not key_raw:
raise ValueError(f"{path.name}: missing required 'key' field")
key = HeroClass(key_raw) # validate against Enum
# --- optional / nested ---
base_attributes = Attributes(**(raw.get("base_attributes") or {}))
starting_resources = Resources(**(raw.get("starting_resources") or {}))
# trees
raw_trees = raw.get("skill_trees") or {}
trees: Dict[str, List[SkillRef]] = {}
for tree_name, nodes in raw_trees.items():
typed_nodes: List[SkillRef] = []
if nodes:
for node in nodes:
typed_nodes.append(SkillRef(**node))
trees[tree_name] = typed_nodes
# spells by level (keys may be strings in YAML)
sbl_in = raw.get("spells_by_level") or {}
spells_by_level: Dict[int, List[str]] = {}
for lvl_key, spell_list in sbl_in.items():
lvl = int(lvl_key)
spells_by_level[lvl] = list(spell_list or [])
arch = HeroArchetype(
key=key,
display_name=raw.get("display_name", key.value.title()),
background=raw.get("background","A person with an unknown origin"),
base_attributes=base_attributes,
starting_resources=starting_resources,
starting_skills=list(raw.get("starting_skills") or []),
starting_spells=list(raw.get("starting_spells") or []),
skill_trees=trees,
spells_by_level=spells_by_level,
bonus_abilities=list(raw.get("bonus_abilities") or []),
)
# parse local catalogs if present:
skills_raw = raw.get("skills") or {}
for sid, sdef in skills_raw.items():
arch.skills[sid] = SkillDef.from_yaml(sid, sdef)
self._skills[sid] = arch.skills[sid] # promote to global map
spells_raw = raw.get("spells") or {}
for spid, spdef in spells_raw.items():
arch.spells[spid] = SpellDef.from_yaml(spid, spdef)
self._spells[spid] = arch.spells[spid]
self._by_class[key] = arch
# Lookups (prefer global catalogs for cross-class reuse)
def get_spell(self, spell_id: str) -> SpellDef:
try:
return self._spells[spell_id]
except KeyError:
raise KeyError(f"Unknown spell id '{spell_id}'")
def get_skill(self, skill_id: str) -> SkillDef:
try:
return self._skills[skill_id]
except KeyError:
raise KeyError(f"Unknown skill id '{skill_id}'")
def for_class(self, hero_class: HeroClass) -> HeroArchetype:
if hero_class not in self._by_class:
raise KeyError(f"No archetype loaded for class {hero_class.value}")
return self._by_class[hero_class]