89 lines
3.5 KiB
Python
89 lines
3.5 KiB
Python
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] |