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]

View File

@@ -0,0 +1,72 @@
# race_catalog.py
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Any
import yaml
from app.models.enums import Race
from app.models.primitives import Attributes, Resources
from app.models.races import RaceSheet, RacialTrait
from app.utils.hero_catalog import SkillDef, SpellDef
class RaceDataRegistry:
"""
In-memory catalog of YAML-defined race sheets.
"""
def __init__(self) -> None:
self._by_race: Dict[Race, RaceSheet] = {}
# promoted global catalogs for cross-use at runtime (optional)
self._skills: Dict[str, SkillDef] = {}
self._spells: Dict[str, SpellDef] = {}
def load_dir(self, directory: str | Path) -> 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 {}
key_raw = raw.get("key")
if not key_raw:
raise ValueError(f"{path.name}: missing required 'key'")
race = Race(key_raw) # validates enum
base_attributes = Attributes(**(raw.get("base_attributes") or {}))
starting_resources = Resources(**(raw.get("starting_resources") or {}))
sheet = RaceSheet(
key=race,
display_name=raw.get("display_name", race.value.title()),
base_attributes=base_attributes,
starting_resources=starting_resources,
starting_skills=list(raw.get("starting_skills") or []),
starting_spells=list(raw.get("starting_spells") or []),
)
# Local skill/spell catalogs (optional)
for sid, sdef in (raw.get("skills") or {}).items():
s = SkillDef.from_yaml(sid, sdef)
sheet.skills[sid] = s
self._skills[sid] = s
for spid, spdef in (raw.get("spells") or {}).items():
sp = SpellDef.from_yaml(spid, spdef)
sheet.spells[spid] = sp
self._spells[spid] = sp
# Traits
traits_raw = raw.get("traits") or []
for t in traits_raw:
sheet.traits.append(RacialTrait(id=t.get("id"), data=dict(t.get("data") or {})))
self._by_race[race] = sheet
def for_race(self, race: Race) -> RaceSheet:
if race not in self._by_race:
raise KeyError(f"No race sheet loaded for {race.value}")
return self._by_race[race]
# Optional global lookups (if you want to fetch a skill from race-only files)
def get_skill(self, skill_id: str) -> SkillDef: return self._skills[skill_id]
def get_spell(self, spell_id: str) -> SpellDef: return self._spells[spell_id]

View File

@@ -0,0 +1,30 @@
# app/utils/skill_registry.py
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional
import yaml
@dataclass
class Skill:
id: str
name: str
description: str
tags: List[str]
max_rank: int = 1
class SkillRegistry:
def __init__(self) -> None:
self.by_id: Dict[str, Skill] = {}
def load_file(self, path: Path) -> None:
data = yaml.safe_load(path.read_text()) or []
for raw in data:
skill = Skill(**raw)
self.by_id[skill.id] = skill
def load_dir(self, root: Path) -> None:
for p in sorted(root.glob("*.y*ml")):
self.load_file(p)
def get(self, skill_id: str) -> Optional[Skill]:
return self.by_id.get(skill_id)

View File

@@ -0,0 +1,33 @@
# app/utils/spell_registry.py
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional
import yaml
@dataclass
class Spell:
id: str
name: str
school: str
element: Optional[str] = None
rank: int = 1
cost_mp: int = 0
description: str = ""
aoe: Optional[str] = None
class SpellRegistry:
def __init__(self) -> None:
self.by_id: Dict[str, Spell] = {}
def load_file(self, path: Path) -> None:
data = yaml.safe_load(path.read_text()) or []
for raw in data:
spell = Spell(**raw)
self.by_id[spell.id] = spell
def load_dir(self, root: Path) -> None:
for p in sorted(root.glob("*.y*ml")):
self.load_file(p)
def get(self, spell_id: str) -> Optional[Spell]:
return self.by_id.get(spell_id)