complete regen of hero classes, spells, races, etc
This commit is contained in:
86
app/game/generators/abilities_factory.py
Normal file
86
app/game/generators/abilities_factory.py
Normal file
@@ -0,0 +1,86 @@
|
||||
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 = 50 + 7 * 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]:
|
||||
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]
|
||||
Reference in New Issue
Block a user