import random, hashlib, re from typing import List, Dict, Any from pathlib import Path import yaml from app.game.models.abilities import Ability # ----------------- Helpers ----------------- def _load_path_themes(filepath: str | Path) -> Dict[str, Dict[str, Any]]: """Load PATH_THEMES-style data from a YAML file.""" filepath = Path(filepath) if not filepath.exists(): raise FileNotFoundError(f"Theme file not found: {filepath}") with open(filepath, "r", encoding="utf-8") as f: data = yaml.safe_load(f) or {} if not isinstance(data, dict): raise ValueError("Invalid format: root must be a mapping of paths.") return data def _stable_seed(*parts, version: int) -> int: s = ":".join(str(p) for p in parts) + f":v{version}" return int(hashlib.sha256(s.encode()).hexdigest(), 16) % (2**32) def _slugify(s: str) -> str: return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-") def _pick(rng: random.Random, xs: List[str]) -> str: return rng.choice(xs) def _name(rng: random.Random, theme: Dict) -> str: # Two simple patterns to keep variety but consistency if rng.random() < 0.5: return f"{_pick(rng, theme['adjectives'])} {_pick(rng, theme['nouns'])}" return f"{_pick(rng, theme['nouns'])} of {_pick(rng, theme['of_things'])}" def _damage_power(tier: int, rng: random.Random) -> float: # Linear-with-jitter curve—easy to reason about and scale base = 10 * tier jitter = 1.0 + rng.uniform(-0.08, 0.08) return round(base * jitter, 2) def _mp_cost(tier: int, rng: random.Random) -> int: # Grows gently with tier return max(1, int(round(12 + 1.8 * tier + rng.uniform(-0.5, 0.5)))) # Add more paths as you like themes_filename = Path() / "app" / "game" / "templates" / "ability_paths.yaml" PATH_THEMES = _load_path_themes(themes_filename) # ----------------- Generator ----------------- def generate_abilities_direct_damage(class_name: str,path: str,level: int, primary_stat:str , per_tier: int = 2,content_version: int = 1) -> List[Ability]: if path not in PATH_THEMES: return [] theme = PATH_THEMES[path] rng = random.Random(_stable_seed(class_name, path, level, per_tier, version=content_version)) spells: List[Ability] = [] for tier in range(1, level + 1): for _ in range(per_tier): name = _name(rng, theme) elem = _pick(rng, theme["elements"]) dmg = _damage_power(tier, rng) cost = _mp_cost(tier, rng) coeff = round(1.05 + 0.02 * tier, 2) # mild growth with tier rules = f"Deal {elem} damage (power {dmg}, {primary_stat}+{int(coeff*100)}%)." spell_id = _slugify(f"{class_name}-{path}-t{tier}-{name}") spells.append(Ability( id=spell_id, name=name, class_name=class_name, path=path, tier=tier, element=elem, cost_mp=cost, damage_power=dmg, scaling_stat=primary_stat, scaling_coeff=coeff, rules_text=rules )) return spells # Convenience: “new at this level only” def newly_unlocked_abilities(class_name: str, path: str, level: int, primary:str, per_tier: int = 2, content_version: int = 1, ) -> List[Ability]: return [s for s in generate_abilities_direct_damage(class_name=class_name, path=path,level=level,primary_stat=primary,per_tier=per_tier,content_version=content_version) if s.tier == level]