init commit
This commit is contained in:
181
app/utils/catalogs/hero_catalog.py
Normal file
181
app/utils/catalogs/hero_catalog.py
Normal 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]
|
||||
72
app/utils/catalogs/race_catalog.py
Normal file
72
app/utils/catalogs/race_catalog.py
Normal 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]
|
||||
30
app/utils/catalogs/skill_catalog.py
Normal file
30
app/utils/catalogs/skill_catalog.py
Normal 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)
|
||||
33
app/utils/catalogs/spell_catalog.py
Normal file
33
app/utils/catalogs/spell_catalog.py
Normal 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)
|
||||
Reference in New Issue
Block a user