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]
|
||||||
@@ -3,54 +3,50 @@ from typing import Dict, List, Any, Tuple
|
|||||||
|
|
||||||
from app.game.utils.common import Dice
|
from app.game.utils.common import Dice
|
||||||
from app.game.models.entities import Entity
|
from app.game.models.entities import Entity
|
||||||
from app.game.utils.loaders import TemplateStore
|
|
||||||
from app.game.systems.leveling import hp_and_mp_gain
|
from app.game.loaders.races_loader import RaceRepository
|
||||||
|
from app.game.loaders.profession_loader import ProfessionRepository
|
||||||
|
from app.game.generators.level_progression import DEFAULT_LEVEL_PROGRESSION
|
||||||
|
from app.game.systems.leveling import set_level
|
||||||
|
|
||||||
dice = Dice()
|
dice = Dice()
|
||||||
|
|
||||||
def build_char(name:str, origin_story:str, race_id:str, class_id:str, fame:int=0) -> Entity:
|
# tuning knobs
|
||||||
|
level_growth = 1.25
|
||||||
|
|
||||||
templates = TemplateStore()
|
|
||||||
|
|
||||||
# base_ability_scores = {"STR":10,"DEX":10,"CON":10,"INT":10,"WIS":10,"CHA":10, "LUC":10}
|
|
||||||
|
|
||||||
|
def build_char(name:str, origin_story:str, race_id:str, profession_id:str, fame:int=0, level:int=1) -> Entity:
|
||||||
|
|
||||||
|
races = RaceRepository()
|
||||||
|
professions = ProfessionRepository()
|
||||||
|
progression = DEFAULT_LEVEL_PROGRESSION
|
||||||
|
|
||||||
race = templates.race(race_id)
|
race = races.get(race_id)
|
||||||
profession = templates.profession(class_id)
|
profession = professions.get(profession_id)
|
||||||
|
|
||||||
e = Entity(
|
e = Entity(
|
||||||
uuid = str(uuid.uuid4()),
|
uuid = str(uuid.uuid4()),
|
||||||
name = name,
|
name = name,
|
||||||
origin_story = origin_story,
|
origin_story = origin_story,
|
||||||
fame = fame,
|
fame = fame,
|
||||||
level=1,
|
race =race,
|
||||||
race =race.get("name"),
|
profession = profession
|
||||||
profession = profession.get("name")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# assign hit dice
|
# apply race ability scores
|
||||||
e.hit_die = profession.get("hit_die")
|
for stat, delta in vars(race.ability_scores).items():
|
||||||
e.per_level.hp_die = profession.get("per_level",{}).get("hp_rule",{}).get("die","1d10")
|
|
||||||
e.per_level.mp_die = profession.get("per_level",{}).get("mp_rule",{}).get("die","1d10")
|
|
||||||
e.per_level.hp_rule = profession.get("per_level",{}).get("hp_rule",{}).get("rule","avg")
|
|
||||||
e.per_level.mp_rule = profession.get("per_level",{}).get("mp_rule",{}).get("rule","avg")
|
|
||||||
|
|
||||||
# assign hp/mp
|
# Get current score (default to 10 if missing)
|
||||||
e.hp = hp_and_mp_gain(1,e.per_level.hp_die,e.per_level.hp_rule)
|
current = getattr(e.ability_scores, stat, 10)
|
||||||
e.mp = hp_and_mp_gain(1,e.per_level.mp_die,e.per_level.mp_rule)
|
|
||||||
|
|
||||||
for stat, delta in race.get("ability_mods", {}).items():
|
# Apply modifier
|
||||||
e.ability_scores[stat] = e.ability_scores.get(stat, 10) + int(delta)
|
new_value = current + int(delta)
|
||||||
|
|
||||||
# Apply class grants (spells/skills/equipment + MP base)
|
# Update the stat
|
||||||
grants = profession.get("grants", {})
|
setattr(e.ability_scores, stat, new_value)
|
||||||
|
|
||||||
# add spells to char
|
set_level(e,level,progression)
|
||||||
grant_spells = grants.get("spells", []) or []
|
|
||||||
for spell in grant_spells:
|
|
||||||
spell_dict = templates.spell(spell)
|
|
||||||
e.spells.append(spell_dict)
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
# e.skills.extend(grants.get("skills", []) or [])
|
|
||||||
|
|
||||||
return e
|
return e
|
||||||
73
app/game/generators/level_progression.py
Normal file
73
app/game/generators/level_progression.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from bisect import bisect_right
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class LevelThreshold:
|
||||||
|
level: int
|
||||||
|
xp: int # cumulative XP required to *reach* this level
|
||||||
|
rewards: Dict[str, Any] # optional; can be empty
|
||||||
|
|
||||||
|
class LevelProgression:
|
||||||
|
"""
|
||||||
|
Provides XP<->Level mapping. Can be built from a parametric curve or YAML-like dict.
|
||||||
|
"""
|
||||||
|
def __init__(self, thresholds: List[LevelThreshold]):
|
||||||
|
# Normalize, unique-by-level, sorted
|
||||||
|
uniq = {}
|
||||||
|
for t in thresholds:
|
||||||
|
uniq[t.level] = LevelThreshold(level=t.level, xp=int(t.xp), rewards=(t.rewards or {}))
|
||||||
|
levels = sorted(uniq.values(), key=lambda x: x.level)
|
||||||
|
if not levels or levels[0].level != 1 or levels[0].xp != 0:
|
||||||
|
raise ValueError("Progression must start at level 1 with xp=0")
|
||||||
|
object.__setattr__(self, "_levels", levels)
|
||||||
|
object.__setattr__(self, "_xp_list", [t.xp for t in levels])
|
||||||
|
object.__setattr__(self, "_level_list", [t.level for t in levels])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_curve(cls, max_level: int = 100, base_xp: int = 100, growth: float = 1.45) -> "LevelProgression":
|
||||||
|
"""
|
||||||
|
Exponential-ish cumulative thresholds:
|
||||||
|
XP(level L) = round(base_xp * ((growth^(L-1) - 1) / (growth - 1)))
|
||||||
|
Ensures level 1 => 0 XP.
|
||||||
|
"""
|
||||||
|
thresholds: List[LevelThreshold] = []
|
||||||
|
for L in range(1, max_level + 1):
|
||||||
|
if L == 1:
|
||||||
|
xp = 0
|
||||||
|
else:
|
||||||
|
# cumulative sum of geometric series
|
||||||
|
xp = round(base_xp * ((growth ** (L - 1) - 1) / (growth - 1)))
|
||||||
|
thresholds.append(LevelThreshold(level=L, xp=xp, rewards={}))
|
||||||
|
return cls(thresholds)
|
||||||
|
|
||||||
|
# --------- Queries ----------
|
||||||
|
@property
|
||||||
|
def max_level(self) -> int:
|
||||||
|
return self._level_list[-1]
|
||||||
|
|
||||||
|
def xp_for_level(self, level: int) -> int:
|
||||||
|
if level < 1: return 0
|
||||||
|
if level >= self.max_level: return self._xp_list[-1]
|
||||||
|
# Levels are dense and 1-indexed
|
||||||
|
idx = level - 1
|
||||||
|
return self._xp_list[idx]
|
||||||
|
|
||||||
|
def level_for_xp(self, xp: int) -> int:
|
||||||
|
"""
|
||||||
|
Find the highest level where threshold.xp <= xp. Levels are dense.
|
||||||
|
"""
|
||||||
|
if xp < 0: return 1
|
||||||
|
idx = bisect_right(self._xp_list, xp) - 1
|
||||||
|
if idx < 0: return 1
|
||||||
|
return self._level_list[idx]
|
||||||
|
|
||||||
|
def rewards_for_level(self, level: int) -> Dict[str, Any]:
|
||||||
|
if level < 2: return {}
|
||||||
|
if level > self.max_level: level = self.max_level
|
||||||
|
return self._levels[level - 1].rewards or {}
|
||||||
|
|
||||||
|
LEVEL_CURVE = 1.25
|
||||||
|
DEFAULT_LEVEL_PROGRESSION = LevelProgression.from_curve(growth=LEVEL_CURVE)
|
||||||
44
app/game/loaders/profession_loader.py
Normal file
44
app/game/loaders/profession_loader.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Iterable, Optional
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from app.game.models.professions import Profession
|
||||||
|
|
||||||
|
class ProfessionRepository:
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
self.root = Path() / "app" / "game" / "templates" / "professions"
|
||||||
|
self._by_id: Dict[str, Profession] = {}
|
||||||
|
self.load()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _iter_yaml_files(root: Path) -> Iterable[Path]:
|
||||||
|
# Recursively find *.yaml under the root
|
||||||
|
yield from root.rglob("*.yaml")
|
||||||
|
|
||||||
|
def load(self) -> None:
|
||||||
|
for path in self._iter_yaml_files(self.root):
|
||||||
|
with path.open("r", encoding="utf-8") as f:
|
||||||
|
data = yaml.safe_load(f) or {}
|
||||||
|
|
||||||
|
profession = Profession.from_yaml(data)
|
||||||
|
|
||||||
|
if profession.id in self._by_id:
|
||||||
|
raise ValueError(f"Duplicate profession id '{profession.id}' in {path}")
|
||||||
|
|
||||||
|
self._by_id.update({profession.id:profession})
|
||||||
|
|
||||||
|
|
||||||
|
# ----- Queries -----
|
||||||
|
|
||||||
|
def get(self, profession_id: str) -> Optional[Profession]:
|
||||||
|
return self._by_id.get(profession_id)
|
||||||
|
|
||||||
|
def list_all(self) -> list[Profession]:
|
||||||
|
return list(self._by_id.values())
|
||||||
|
|
||||||
|
def list_by_tag(self, tag: str) -> list[Profession]:
|
||||||
|
return [r for r in self._by_id.values() if tag in r.tags]
|
||||||
|
|
||||||
|
def list_by_any_tags(self, tags: set[str]) -> list[Profession]:
|
||||||
|
return [r for r in self._by_id.values() if r.tags & tags]
|
||||||
50
app/game/loaders/races_loader.py
Normal file
50
app/game/loaders/races_loader.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Iterable, Optional
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from app.game.models.races import Race
|
||||||
|
|
||||||
|
class RaceRepository:
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
self.root = Path() / "app" / "game" / "templates" / "races"
|
||||||
|
self._by_id: Dict[str, Race] = {}
|
||||||
|
self.load()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _iter_yaml_files(root: Path) -> Iterable[Path]:
|
||||||
|
# Recursively find *.yaml under the root
|
||||||
|
yield from root.rglob("*.yaml")
|
||||||
|
|
||||||
|
def load(self) -> None:
|
||||||
|
for path in self._iter_yaml_files(self.root):
|
||||||
|
with path.open("r", encoding="utf-8") as f:
|
||||||
|
data = yaml.safe_load(f) or {}
|
||||||
|
|
||||||
|
race = Race.from_yaml(data)
|
||||||
|
|
||||||
|
if race.id in self._by_id:
|
||||||
|
raise ValueError(f"Duplicate Race id '{race.id}' in {path}")
|
||||||
|
|
||||||
|
self._by_id.update({race.id:race})
|
||||||
|
|
||||||
|
|
||||||
|
# ----- Queries -----
|
||||||
|
|
||||||
|
def get(self, race_id: str) -> Optional[Race]:
|
||||||
|
return self._by_id.get(race_id)
|
||||||
|
|
||||||
|
def list_all(self) -> list[Race]:
|
||||||
|
return list(self._by_id.values())
|
||||||
|
|
||||||
|
def list_playable(self) -> list[Race]:
|
||||||
|
return [r for r in self._by_id.values() if r.is_playable]
|
||||||
|
|
||||||
|
def list_non_playable(self) -> list[Race]:
|
||||||
|
return [r for r in self._by_id.values() if not r.is_playable]
|
||||||
|
|
||||||
|
def list_by_tag(self, tag: str) -> list[Race]:
|
||||||
|
return [r for r in self._by_id.values() if tag in r.tags]
|
||||||
|
|
||||||
|
def list_by_any_tags(self, tags: set[str]) -> list[Race]:
|
||||||
|
return [r for r in self._by_id.values() if r.tags & tags]
|
||||||
17
app/game/models/abilities.py
Normal file
17
app/game/models/abilities.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Ability:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
class_name: str
|
||||||
|
path: str
|
||||||
|
tier: int # 1..level; use this as your learn/unlock gate
|
||||||
|
element: str
|
||||||
|
cost_mp: int
|
||||||
|
damage_power: float # abstract power; plug into your combat math
|
||||||
|
scaling_stat: str # e.g., "CHA"
|
||||||
|
scaling_coeff: float# e.g., 1.22 means +122% of stat modifier
|
||||||
|
rules_text: str # readable summary (optional)
|
||||||
27
app/game/models/enemies.py
Normal file
27
app/game/models/enemies.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Dict, Literal
|
||||||
|
|
||||||
|
Difficulty = Literal["easy","standard","tough","elite","boss"]
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class EnemyProfile:
|
||||||
|
# Level selection relative to player
|
||||||
|
level_bias: int = 0 # e.g., +2 spawns slightly above player level
|
||||||
|
level_variance: int = 1 # random wiggle (0..variance)
|
||||||
|
min_level: int = 1
|
||||||
|
max_level: int = 999
|
||||||
|
|
||||||
|
# Global power multipliers (applied after base+per-level growth)
|
||||||
|
hp_mult: float = 1.0
|
||||||
|
dmg_mult: float = 1.0
|
||||||
|
armor_mult: float = 1.0
|
||||||
|
speed_mult: float = 1.0
|
||||||
|
|
||||||
|
# Optional stat emphasis for auto-building enemy ability scores
|
||||||
|
stat_weights: Dict[str, float] = field(default_factory=dict) # e.g. {"STR":1.2,"DEX":1.1}
|
||||||
|
|
||||||
|
# Rewards
|
||||||
|
xp_base: int = 10
|
||||||
|
xp_per_level: int = 5
|
||||||
|
loot_tier: str = "common"
|
||||||
@@ -2,37 +2,55 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Dict, List, Any, Tuple
|
from typing import Dict, List, Any, Tuple
|
||||||
|
|
||||||
|
from app.game.models.races import Race
|
||||||
|
from app.game.models.professions import Profession
|
||||||
|
from app.game.models.abilities import Ability
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PerLevel:
|
class AbilityScores:
|
||||||
hp_die: str = "1d10"
|
STR: int = 10
|
||||||
hp_rule: str = "avg"
|
DEX: int = 10
|
||||||
mp_die: str = "1d6"
|
INT: int = 10
|
||||||
mp_rule: str = "avg"
|
WIS: int = 10
|
||||||
|
CON: int = 10
|
||||||
|
LUK: int = 10
|
||||||
|
CHA: int = 10
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Status:
|
||||||
|
max_hp: int = 0
|
||||||
|
max_mp: int = 0
|
||||||
|
current_hp: int=0
|
||||||
|
current_mp: int=0
|
||||||
|
max_energy_credits: int = 0
|
||||||
|
current_energy_credits: int = 0
|
||||||
|
is_dead = False
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Entity:
|
class Entity:
|
||||||
uuid: str = ""
|
uuid: str = ""
|
||||||
name: str = ""
|
name: str = ""
|
||||||
race: str = ""
|
|
||||||
profession: str = ""
|
|
||||||
origin_story:str = ""
|
origin_story:str = ""
|
||||||
hit_die: str = "1d6"
|
race: Race = field(default_factory=Race)
|
||||||
|
profession: Profession = field(default_factory=Profession)
|
||||||
|
|
||||||
hp: int = 1
|
|
||||||
mp: int = 0
|
|
||||||
energy_credits: int = 0
|
|
||||||
|
|
||||||
level: int = 0
|
level: int = 0
|
||||||
xp: int = 0
|
xp: int = 0
|
||||||
xp_to_next_level:int = 100
|
xp_to_next_level:int = 100
|
||||||
|
|
||||||
fame: int = 0
|
fame: int = 0
|
||||||
alignment: int = 0
|
alignment: int = 0
|
||||||
|
|
||||||
|
physical_attack: int = 1
|
||||||
|
physical_defense: int = 1
|
||||||
|
|
||||||
|
magic_attack: int = 1
|
||||||
|
magic_defense: int = 1
|
||||||
|
|
||||||
per_level: PerLevel = field(default_factory=PerLevel)
|
status: Status = field(default_factory=Status)
|
||||||
ability_scores: Dict[str, int] = field(default_factory=dict)
|
ability_scores: AbilityScores = field(default_factory=AbilityScores)
|
||||||
weapons: List[Dict[str, int]] = field(default_factory=list)
|
weapons: List[Dict[str, int]] = field(default_factory=list)
|
||||||
armor: List[Dict[str, int]] = field(default_factory=list)
|
armor: List[Dict[str, int]] = field(default_factory=list)
|
||||||
spells: List[Dict[str, int]] = field(default_factory=list)
|
abilities: List[Ability] = field(default_factory=list)
|
||||||
skills: List[Dict[str, int]] = field(default_factory=list)
|
skills: List[Dict[str, int]] = field(default_factory=list)
|
||||||
|
|
||||||
|
|||||||
93
app/game/models/professions.py
Normal file
93
app/game/models/professions.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
from app.game.models.enemies import EnemyProfile
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Profession:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
primary_stat: str
|
||||||
|
base_hp: int
|
||||||
|
base_mp: int
|
||||||
|
physical_attack_per_level: float
|
||||||
|
physical_defense_per_level: float
|
||||||
|
magic_attack_per_level: float
|
||||||
|
magic_defense_per_level: float
|
||||||
|
|
||||||
|
tags: list[str] = field(default_factory=list) # e.g., {"playable"}, {"leader","elite"}
|
||||||
|
enemy: Optional[EnemyProfile] = None # ⬅ optional enemy-only tuning
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_playable(self) -> bool:
|
||||||
|
return "playable" in self.tags
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_yaml(data: Dict[str, object]) -> "Profession":
|
||||||
|
# ---- validation of required profession fields ----
|
||||||
|
required = [
|
||||||
|
"id", "name", "description", "primary_stat",
|
||||||
|
"base_hp", "base_mp",
|
||||||
|
"physical_attack_per_level", "physical_defense_per_level",
|
||||||
|
"magic_attack_per_level", "magic_defense_per_level"
|
||||||
|
]
|
||||||
|
missing = [k for k in required if k not in data]
|
||||||
|
if missing:
|
||||||
|
raise ValueError(f"Profession missing required fields: {missing}")
|
||||||
|
|
||||||
|
# ---- cast helpers (robust to str/int/float in YAML) ----
|
||||||
|
def as_int(x, field_name):
|
||||||
|
try: return int(x)
|
||||||
|
except Exception: raise ValueError(f"{field_name} must be int, got {x!r}")
|
||||||
|
def as_float(x, field_name):
|
||||||
|
try: return float(x)
|
||||||
|
except Exception: raise ValueError(f"{field_name} must be float, got {x!r}")
|
||||||
|
|
||||||
|
# ---- tags (optional) ----
|
||||||
|
tags = list(data.get("tags", []) or [])
|
||||||
|
|
||||||
|
# ---- enemy block (optional) ----
|
||||||
|
enemy_block = data.get("enemy")
|
||||||
|
enemy: Optional[EnemyProfile] = None
|
||||||
|
if enemy_block:
|
||||||
|
eb = enemy_block or {}
|
||||||
|
# typed extraction with defaults from EnemyProfile
|
||||||
|
enemy = EnemyProfile(
|
||||||
|
level_bias=as_int(eb.get("level_bias", 0), "enemy.level_bias"),
|
||||||
|
level_variance=as_int(eb.get("level_variance", 1), "enemy.level_variance"),
|
||||||
|
min_level=as_int(eb.get("min_level", 1), "enemy.min_level"),
|
||||||
|
max_level=as_int(eb.get("max_level", 999), "enemy.max_level"),
|
||||||
|
hp_mult=as_float(eb.get("hp_mult", 1.0), "enemy.hp_mult"),
|
||||||
|
dmg_mult=as_float(eb.get("dmg_mult", 1.0), "enemy.dmg_mult"),
|
||||||
|
armor_mult=as_float(eb.get("armor_mult", 1.0), "enemy.armor_mult"),
|
||||||
|
speed_mult=as_float(eb.get("speed_mult", 1.0), "enemy.speed_mult"),
|
||||||
|
stat_weights={str(k): float(v) for k, v in (eb.get("stat_weights", {}) or {}).items()},
|
||||||
|
xp_base=as_int(eb.get("xp_base", 10), "enemy.xp_base"),
|
||||||
|
xp_per_level=as_int(eb.get("xp_per_level", 5), "enemy.xp_per_level"),
|
||||||
|
loot_tier=str(eb.get("loot_tier", "common")),
|
||||||
|
)
|
||||||
|
# sanity checks
|
||||||
|
if enemy.min_level < 1:
|
||||||
|
raise ValueError("enemy.min_level must be >= 1")
|
||||||
|
if enemy.max_level < enemy.min_level:
|
||||||
|
raise ValueError("enemy.max_level must be >= enemy.min_level")
|
||||||
|
if any(v < 0 for v in (enemy.hp_mult, enemy.dmg_mult, enemy.armor_mult, enemy.speed_mult)):
|
||||||
|
raise ValueError("enemy multipliers must be >= 0")
|
||||||
|
|
||||||
|
# ---- construct Profession ----
|
||||||
|
return Profession(
|
||||||
|
id=str(data["id"]),
|
||||||
|
name=str(data["name"]),
|
||||||
|
description=str(data["description"]),
|
||||||
|
primary_stat=str(data["primary_stat"]),
|
||||||
|
base_hp=as_int(data["base_hp"], "base_hp"),
|
||||||
|
base_mp=as_int(data["base_mp"], "base_mp"),
|
||||||
|
physical_attack_per_level=as_float(data["physical_attack_per_level"], "physical_attack_per_level"),
|
||||||
|
physical_defense_per_level=as_float(data["physical_defense_per_level"], "physical_defense_per_level"),
|
||||||
|
magic_attack_per_level=as_float(data["magic_attack_per_level"], "magic_attack_per_level"),
|
||||||
|
magic_defense_per_level=as_float(data["magic_defense_per_level"], "magic_defense_per_level"),
|
||||||
|
tags=tags,
|
||||||
|
enemy=enemy,
|
||||||
|
)
|
||||||
56
app/game/models/races.py
Normal file
56
app/game/models/races.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AbilityScores:
|
||||||
|
STR: int = 0
|
||||||
|
DEX: int = 0
|
||||||
|
INT: int = 0
|
||||||
|
WIS: int = 0
|
||||||
|
CON: int = 0
|
||||||
|
LUK: int = 0
|
||||||
|
CHA: int = 0
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Race:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
alignment: int = 0
|
||||||
|
ability_scores: AbilityScores = field(default_factory=AbilityScores)
|
||||||
|
tags: list[str] = field(default_factory=list) # e.g., {"playable"}, {"hostile","goblinoid"}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_playable(self) -> bool:
|
||||||
|
return "playable" in self.tags
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_yaml(data: Dict[str, object]) -> "Race":
|
||||||
|
# basic validation with helpful errors
|
||||||
|
required = ["id", "name", "description", "alignment", "ability_mods"]
|
||||||
|
missing = [k for k in required if k not in data]
|
||||||
|
if missing:
|
||||||
|
raise ValueError(f"Spell missing required fields: {missing}")
|
||||||
|
|
||||||
|
mods = data.get("ability_mods", {}) or {}
|
||||||
|
|
||||||
|
abs = AbilityScores(
|
||||||
|
STR=int(mods.get("STR", 0)),
|
||||||
|
DEX=int(mods.get("DEX", 0)),
|
||||||
|
INT=int(mods.get("INT", 0)),
|
||||||
|
WIS=int(mods.get("WIS", 0)),
|
||||||
|
CON=int(mods.get("CON", 0)),
|
||||||
|
LUK=int(mods.get("LUK", 0)),
|
||||||
|
CHA=int(mods.get("CHA", 0)),
|
||||||
|
)
|
||||||
|
|
||||||
|
tags = list(data.get("tags", []))
|
||||||
|
|
||||||
|
return Race(
|
||||||
|
id=str(data["id"]),
|
||||||
|
name=str(data["name"]),
|
||||||
|
description=str(data["description"]),
|
||||||
|
ability_scores=abs,
|
||||||
|
tags=tags
|
||||||
|
)
|
||||||
@@ -1,72 +1,105 @@
|
|||||||
|
# utils/leveling.py
|
||||||
from app.utils.logging import get_logger
|
from typing import Dict, Any, Callable, Optional, List, Tuple
|
||||||
|
from app.game.generators.level_progression import LevelProgression
|
||||||
from app.game.utils.common import Dice
|
|
||||||
from app.game.models.entities import Entity
|
from app.game.models.entities import Entity
|
||||||
|
from app.game.generators.abilities_factory import newly_unlocked_abilities
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
def set_level(entity:Entity, target_level: int, prog: LevelProgression) -> None:
|
||||||
|
|
||||||
dice = Dice()
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_xp_and_level(current_xp: int, xp_gain: int, base_xp: int = 100, growth_rate: float = 1.2):
|
|
||||||
"""
|
"""
|
||||||
Calculates new XP and level after gaining experience.
|
Snap an entity to a *specific* level. Sets XP to that level's floor.
|
||||||
|
Optionally applies all level-up rewards from current_level+1 .. target_level (idempotent if you recompute stats from level).
|
||||||
Args:
|
|
||||||
current_xp (int): The player's current total XP.
|
|
||||||
xp_gain (int): The amount of XP gained.
|
|
||||||
base_xp (int): XP required for level 1 → 2.
|
|
||||||
growth_rate (float): Multiplier for each level’s XP requirement.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: {
|
|
||||||
'new_xp': int,
|
|
||||||
'new_level': int,
|
|
||||||
'xp_to_next_level': int,
|
|
||||||
'remaining_xp_to_next': int
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
# Calculate current level based on XP
|
# ensures we never try to go above the max level in the game
|
||||||
level = 1
|
target_level = max(1, min(target_level, prog.max_level))
|
||||||
xp_needed = base_xp
|
current = entity.level
|
||||||
total_xp_for_next = xp_needed
|
|
||||||
|
|
||||||
# Determine current level from current_xp
|
# if not changing levels, just set the xp and move along
|
||||||
while current_xp >= total_xp_for_next:
|
if current == target_level:
|
||||||
level += 1
|
entity.xp = prog.xp_for_level(target_level)
|
||||||
xp_needed = int(base_xp * (growth_rate ** (level - 1)))
|
_recalc(entity)
|
||||||
total_xp_for_next += xp_needed
|
|
||||||
|
|
||||||
# Add new XP
|
# Set final level + floor XP
|
||||||
new_xp = current_xp + xp_gain
|
entity.level = target_level
|
||||||
|
entity.xp = prog.xp_for_level(target_level)
|
||||||
|
|
||||||
# Check if level up(s) occurred
|
# set next level xp as xp needed for next level
|
||||||
new_level = level
|
entity.xp_to_next_level = prog.xp_for_level(target_level + 1)
|
||||||
while new_xp >= total_xp_for_next:
|
|
||||||
new_level += 1
|
|
||||||
xp_needed = int(base_xp * (growth_rate ** (new_level - 1)))
|
|
||||||
total_xp_for_next += xp_needed
|
|
||||||
|
|
||||||
# XP required for next level
|
spells_list = newly_unlocked_abilities(class_name=entity.profession.name,
|
||||||
xp_to_next_level = xp_needed
|
path="Hellknight",
|
||||||
levels_gained = new_level - level
|
level=target_level,
|
||||||
|
per_tier=1,
|
||||||
|
primary=entity.profession.primary_stat)
|
||||||
|
_add_abilities(entity,spells_list)
|
||||||
|
|
||||||
return {
|
_recalc(entity)
|
||||||
"new_xp": new_xp,
|
|
||||||
"new_level": new_level,
|
def grant_xp(entity:Entity, amount: int, prog: LevelProgression) -> Tuple[int, int]:
|
||||||
"xp_to_next_level": xp_to_next_level,
|
"""
|
||||||
"levels_gained": levels_gained
|
Add XP and auto-level if thresholds crossed.
|
||||||
}
|
Returns (old_level, new_level).
|
||||||
|
"""
|
||||||
|
old_level = entity.level or 1
|
||||||
|
entity.xp = entity.xp + int(amount)
|
||||||
|
|
||||||
|
new_level = prog.level_for_xp(entity.xp)
|
||||||
|
|
||||||
|
if new_level > old_level:
|
||||||
|
for L in range(old_level + 1, new_level + 1):
|
||||||
|
spells_list = newly_unlocked_abilities(class_name=entity.profession.name,
|
||||||
|
path="Hellknight",
|
||||||
|
level=new_level,
|
||||||
|
per_tier=1,
|
||||||
|
primary=entity.profession.primary_stat)
|
||||||
|
_add_abilities(entity,spells_list)
|
||||||
|
|
||||||
|
if new_level > old_level:
|
||||||
|
entity.level = new_level
|
||||||
|
_recalc(entity)
|
||||||
|
|
||||||
|
# --- compute XP to next level ---
|
||||||
|
if new_level >= prog.max_level:
|
||||||
|
# Maxed out
|
||||||
|
entity.xp_to_next_level = 0
|
||||||
|
else:
|
||||||
|
next_floor = prog.xp_for_level(new_level + 1)
|
||||||
|
entity.xp_to_next_level = max(0, next_floor - entity.xp)
|
||||||
|
|
||||||
|
|
||||||
|
return old_level, new_level
|
||||||
|
|
||||||
|
# ---------- reward + recalc helpers ----------
|
||||||
|
|
||||||
|
def _add_abilities(entity:Entity, abilities_list:list) -> None:
|
||||||
|
for ability in abilities_list:
|
||||||
|
entity.abilities.append(ability)
|
||||||
|
|
||||||
|
def _recalc(entity:Entity) -> None:
|
||||||
|
"""
|
||||||
|
Recompute derived stats from entity.level + profession.
|
||||||
|
Replace with your actual formulas.
|
||||||
|
"""
|
||||||
|
L = entity.level
|
||||||
|
prof = entity.profession
|
||||||
|
|
||||||
|
# scale attack/defense by per-level gains
|
||||||
|
if prof:
|
||||||
|
base_pa = entity.profession.physical_attack_per_level or 0
|
||||||
|
base_pd = entity.profession.physical_defense_per_level or 0
|
||||||
|
base_ma = entity.profession.magic_attack_per_level or 0
|
||||||
|
base_md = entity.profession.magic_defense_per_level or 0
|
||||||
|
|
||||||
|
entity.physical_attack = round(base_pa + prof.physical_attack_per_level * (L - 1),2)
|
||||||
|
entity.physical_defense = round(base_pd + prof.physical_defense_per_level * (L - 1),2)
|
||||||
|
entity.magic_attack = round(base_ma + prof.magic_attack_per_level * (L - 1),2)
|
||||||
|
entity.magic_defense = round(base_md + prof.magic_defense_per_level * (L - 1),2)
|
||||||
|
|
||||||
|
# HP/MP growth from profession base
|
||||||
|
if prof:
|
||||||
|
entity.status.max_hp = entity.status.max_hp + int(prof.base_hp) + int((L * prof.base_hp) * 0.5)
|
||||||
|
entity.status.max_mp = entity.status.max_mp + int(prof.base_mp) + int((L * prof.base_mp) * 0.5)
|
||||||
|
|
||||||
|
# set current to max if you keep current_hp/mp
|
||||||
|
entity.status.current_hp = entity.status.max_hp
|
||||||
|
entity.status.current_mp = entity.status.max_mp
|
||||||
|
|
||||||
def hp_and_mp_gain(level: int, hit_die: str, rule: str) -> int:
|
|
||||||
rule = (rule or "avg").lower()
|
|
||||||
if level == 1:
|
|
||||||
# Common RPG convention: level 1 gets max die
|
|
||||||
return dice.max_of_die(hit_die)
|
|
||||||
if rule == "max":
|
|
||||||
return dice.max_of_die(hit_die)
|
|
||||||
if rule == "avg":
|
|
||||||
return dice.roll_dice(hit_die)
|
|
||||||
# default fallback
|
|
||||||
return dice.roll_dice(hit_die)
|
|
||||||
41
app/game/templates/ability_paths.yaml
Normal file
41
app/game/templates/ability_paths.yaml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
Hellknight:
|
||||||
|
elements: ["fire", "shadow"]
|
||||||
|
adjectives: ["Infernal", "Hellfire", "Brimstone", "Abyssal", "Profane", "Ironbound"]
|
||||||
|
nouns: ["Edict", "Judgment", "Brand", "Verdict", "Smite", "Chain", "Pact", "Decree"]
|
||||||
|
of_things: ["Dominion", "Torment", "Binding", "Ruin", "Cinders", "Night"]
|
||||||
|
|
||||||
|
Frostbinder:
|
||||||
|
elements: ["ice", "water"]
|
||||||
|
adjectives: ["Frozen", "Glacial", "Frigid", "Shivering", "Icy"]
|
||||||
|
nouns: ["Spike", "Lance", "Shard", "Burst", "Ray"]
|
||||||
|
of_things: ["Frost", "Winter", "Stillness", "Silence", "Cold"]
|
||||||
|
|
||||||
|
Bloodborn:
|
||||||
|
elements: ["curse", "shadow"]
|
||||||
|
adjectives: ["Tainted", "Cursed", "Corrupted", "Polluted", "Toxic", "Deadly"]
|
||||||
|
nouns: ["Poison", "Infection", "Wound", "Bane", "Plague", "Ravage", "Scourge"]
|
||||||
|
of_things: ["Infectious", "Contagion", "Disease", "Fungus", "Spore", "Seeds"]
|
||||||
|
|
||||||
|
Assassin:
|
||||||
|
elements: ["wind", "shadow"]
|
||||||
|
adjectives: ["Stealthy", "Deadly", "Ruthless", "Silent", "Lethal", "Fearsome"]
|
||||||
|
nouns: ["Dagger", "Knife", "Poison", "Bolt", "Arrows", "Scimitar", "Twinblade"]
|
||||||
|
of_things: ["Hunter", "Stalker", "Predator", "Ghost", "Shadow", "Silhouette"]
|
||||||
|
|
||||||
|
Cleric:
|
||||||
|
elements: ["fire", "light"]
|
||||||
|
adjectives: ["Holy", "Divine", "Pure", "Blessed", "Sacred", "Radiant"]
|
||||||
|
nouns: ["Flame", "Lightning", "Bolt", "Sword", "Shield", "Vocation", "Call"]
|
||||||
|
of_things: ["Healing", "Protection", "Blessing", "Salvation", "Redemption", "Mercy"]
|
||||||
|
|
||||||
|
Hexist:
|
||||||
|
elements: ["curse", "shadow"]
|
||||||
|
adjectives: ["Dark", "Malefic", "Sinister", "Maledicta", "Witchlike", "Pestilential"]
|
||||||
|
nouns: ["Spell", "Hex", "Curse", "Influence", "Taint", "Bane", "Malice"]
|
||||||
|
of_things: ["Poison", "Deceit", "Demonic", "Abomination", "Evil", "Sinister"]
|
||||||
|
|
||||||
|
Ranger:
|
||||||
|
elements: ["air", "wind"]
|
||||||
|
adjectives: ["Wild", "Free", "Wanderer", "Survivor", "Huntress", "Skilled"]
|
||||||
|
nouns: ["Arrows", "Rifle", "Bow", "Arrowhead", "Shotgun", "Crossbow", "Pistol"]
|
||||||
|
of_things: ["Wolves", "Forest", "Wilderness", "Hunter", "Tracking", "Ambush"]
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
classes:
|
|
||||||
guardian:
|
|
||||||
name: Guardian
|
|
||||||
description: A protector and defender, skilled in combat and leadership.
|
|
||||||
hit_die: "2d6"
|
|
||||||
per_level:
|
|
||||||
hp_rule: {"die":"1d10", "rule":"max"}
|
|
||||||
mp_rule: {"die":"1d4", "rule":"avg"}
|
|
||||||
grants:
|
|
||||||
spells: []
|
|
||||||
skills: []
|
|
||||||
|
|
||||||
bloodborn:
|
|
||||||
name: Bloodborn
|
|
||||||
description: Born from darkness and violence, they wield power with abandon.
|
|
||||||
hit_die: "2d6"
|
|
||||||
per_level:
|
|
||||||
hp_rule: {"die":"1d10", "rule":"max"}
|
|
||||||
mp_rule: {"die":"1d4", "rule":"avg"}
|
|
||||||
grants:
|
|
||||||
spells: []
|
|
||||||
skills: []
|
|
||||||
|
|
||||||
arcanist:
|
|
||||||
name: Arcanist
|
|
||||||
description: Arcanists are scholars of the arcane, shaping elemental forces through study and sheer intellect. Each Arcanists past is different, some taught in grand academies, others self-taught hermits obsessed with understanding creation.
|
|
||||||
hit_die: "1d6"
|
|
||||||
per_level:
|
|
||||||
hp_rule: {"die":"1d4", "rule":"avg"}
|
|
||||||
mp_rule: {"die":"1d10", "rule":"max"}
|
|
||||||
grants:
|
|
||||||
spells: ["ember_flicker"]
|
|
||||||
skills: []
|
|
||||||
|
|
||||||
hexist:
|
|
||||||
name: Hexist
|
|
||||||
description: Twisted sorcerers who wield dark magic to bend reality to their will.
|
|
||||||
hit_die: "1d10"
|
|
||||||
per_level:
|
|
||||||
hp_rule: {"die":"1d4", "rule":"avg"}
|
|
||||||
mp_rule: {"die":"1d10", "rule":"max"}
|
|
||||||
grants:
|
|
||||||
spells: []
|
|
||||||
skills: []
|
|
||||||
|
|
||||||
assassin:
|
|
||||||
name: Assassin
|
|
||||||
description: Stealthy killers, trained from a young age to strike without warning.
|
|
||||||
hit_die: "2d6"
|
|
||||||
per_level:
|
|
||||||
hp_rule: {"die":"1d10", "rule":"max"}
|
|
||||||
mp_rule: {"die":"1d6", "rule":"avg"}
|
|
||||||
grants:
|
|
||||||
spells: []
|
|
||||||
skills: []
|
|
||||||
|
|
||||||
ranger:
|
|
||||||
name: Ranger
|
|
||||||
description: Skilled hunters who live off the land, tracking prey and evading predators.
|
|
||||||
hit_die: "2d6"
|
|
||||||
per_level:
|
|
||||||
hp_rule: {"die":"1d10", "rule":"max"}
|
|
||||||
mp_rule: {"die":"1d6", "rule":"avg"}
|
|
||||||
grants:
|
|
||||||
spells: []
|
|
||||||
skills: []
|
|
||||||
|
|
||||||
cleric:
|
|
||||||
name: Cleric
|
|
||||||
description: Holy warriors, using faith to heal and protect allies, and smite enemies.
|
|
||||||
hit_die: "3d4"
|
|
||||||
per_level:
|
|
||||||
hp_rule: {"die":"1d10", "rule":"avg"}
|
|
||||||
mp_rule: {"die":"2d6", "rule":"avg"}
|
|
||||||
grants:
|
|
||||||
spells: []
|
|
||||||
skills: []
|
|
||||||
|
|
||||||
warlock:
|
|
||||||
name: Warlock
|
|
||||||
description: Bargainers with dark forces, wielding forbidden magic for mysterious purposes.
|
|
||||||
hit_die: "3d4"
|
|
||||||
per_level:
|
|
||||||
hp_rule: {"die":"1d10", "rule":"avg"}
|
|
||||||
mp_rule: {"die":"2d6", "rule":"avg"}
|
|
||||||
grants:
|
|
||||||
spells: []
|
|
||||||
skills: []
|
|
||||||
11
app/game/templates/professions/arcanist.yaml
Normal file
11
app/game/templates/professions/arcanist.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
id: arcanist
|
||||||
|
name: Arcanist
|
||||||
|
description: Arcanists are scholars of the arcane, shaping elemental forces through study and sheer intellect. Each Arcanists past is different, some taught in grand academies, others self-taught hermits obsessed with understanding creation.
|
||||||
|
primary_stat: INT
|
||||||
|
base_hp: 8
|
||||||
|
base_mp: 14
|
||||||
|
physical_attack_per_level: 1.1
|
||||||
|
physical_defense_per_level: 1.1
|
||||||
|
magic_attack_per_level: 1.7
|
||||||
|
magic_defense_per_level: 1.6
|
||||||
|
tags: [playable, mage]
|
||||||
11
app/game/templates/professions/assassin.yaml
Normal file
11
app/game/templates/professions/assassin.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
id: assassin
|
||||||
|
name: Assassin
|
||||||
|
description: Stealthy killers, trained from a young age to strike without warning.
|
||||||
|
primary_stat: DEX
|
||||||
|
base_hp: 10
|
||||||
|
base_mp: 10
|
||||||
|
physical_attack_per_level: 1.8
|
||||||
|
physical_defense_per_level: 1.4
|
||||||
|
magic_attack_per_level: 1.2
|
||||||
|
magic_defense_per_level: 1.1
|
||||||
|
tags: [playable, flex]
|
||||||
11
app/game/templates/professions/bloodborn.yaml
Normal file
11
app/game/templates/professions/bloodborn.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
id: bloodborn
|
||||||
|
name: Bloodborn
|
||||||
|
description: Born from darkness and violence, they wield power with abandon.
|
||||||
|
primary_stat: STR
|
||||||
|
base_hp: 14
|
||||||
|
base_mp: 5
|
||||||
|
physical_attack_per_level: 1.8
|
||||||
|
physical_defense_per_level: 1.4
|
||||||
|
magic_attack_per_level: 1.2
|
||||||
|
magic_defense_per_level: 1.1
|
||||||
|
tags: [playable, martial]
|
||||||
11
app/game/templates/professions/cleric.yaml
Normal file
11
app/game/templates/professions/cleric.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
id: cleric
|
||||||
|
name: Cleric
|
||||||
|
description: Holy warriors, using faith to heal and protect allies, and smite enemies.
|
||||||
|
primary_stat: WIS
|
||||||
|
base_hp: 8
|
||||||
|
base_mp: 14
|
||||||
|
physical_attack_per_level: 1.1
|
||||||
|
physical_defense_per_level: 1.1
|
||||||
|
magic_attack_per_level: 1.7
|
||||||
|
magic_defense_per_level: 1.6
|
||||||
|
tags: [playable, mage]
|
||||||
25
app/game/templates/professions/general.yaml
Normal file
25
app/game/templates/professions/general.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
id: general
|
||||||
|
name: General
|
||||||
|
description: Tactical leader archetype.
|
||||||
|
primary_stat: INT
|
||||||
|
base_hp: 12
|
||||||
|
base_mp: 4
|
||||||
|
physical_attack_per_level: 1.1
|
||||||
|
physical_defense_per_level: 0.6
|
||||||
|
magic_attack_per_level: 0.8
|
||||||
|
magic_defense_per_level: 0.7
|
||||||
|
tags: [leader, elite]
|
||||||
|
|
||||||
|
enemy:
|
||||||
|
level_bias: 2
|
||||||
|
level_variance: 1
|
||||||
|
min_level: 3
|
||||||
|
max_level: 120
|
||||||
|
hp_mult: 1.25
|
||||||
|
dmg_mult: 1.15
|
||||||
|
armor_mult: 1.1
|
||||||
|
speed_mult: 1.0
|
||||||
|
stat_weights: { INT: 1.2, WIS: 1.1 }
|
||||||
|
xp_base: 25
|
||||||
|
xp_per_level: 8
|
||||||
|
loot_tier: rare
|
||||||
11
app/game/templates/professions/guardian.yaml
Normal file
11
app/game/templates/professions/guardian.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
id: guardian
|
||||||
|
name: Guardian
|
||||||
|
description: A protector and defender, skilled in combat and leadership.
|
||||||
|
primary_stat: STR
|
||||||
|
base_hp: 14
|
||||||
|
base_mp: 5
|
||||||
|
physical_attack_per_level: 1.8
|
||||||
|
physical_defense_per_level: 1.4
|
||||||
|
magic_attack_per_level: 1.2
|
||||||
|
magic_defense_per_level: 1.1
|
||||||
|
tags: [playable, martial]
|
||||||
11
app/game/templates/professions/hexist.yaml
Normal file
11
app/game/templates/professions/hexist.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
id: hexist
|
||||||
|
name: Hexist
|
||||||
|
description: Twisted sorcerers who wield dark magic to bend reality to their will.
|
||||||
|
primary_stat: INT
|
||||||
|
base_hp: 8
|
||||||
|
base_mp: 14
|
||||||
|
physical_attack_per_level: 1.1
|
||||||
|
physical_defense_per_level: 1.1
|
||||||
|
magic_attack_per_level: 1.7
|
||||||
|
magic_defense_per_level: 1.6
|
||||||
|
tags: [playable, mage]
|
||||||
11
app/game/templates/professions/ranger.yaml
Normal file
11
app/game/templates/professions/ranger.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
id: ranger
|
||||||
|
name: Ranger
|
||||||
|
description: Skilled hunters who live off the land, tracking prey and evading predators.
|
||||||
|
primary_stat: DEX
|
||||||
|
base_hp: 10
|
||||||
|
base_mp: 10
|
||||||
|
physical_attack_per_level: 1.8
|
||||||
|
physical_defense_per_level: 1.4
|
||||||
|
magic_attack_per_level: 1.2
|
||||||
|
magic_defense_per_level: 1.1
|
||||||
|
tags: [playable, flex]
|
||||||
11
app/game/templates/professions/warlock.yaml
Normal file
11
app/game/templates/professions/warlock.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
id: warlock
|
||||||
|
name: Warlock
|
||||||
|
description: Bargainers with dark forces, wielding forbidden magic for mysterious purposes.
|
||||||
|
primary_stat: WIS
|
||||||
|
base_hp: 8
|
||||||
|
base_mp: 14
|
||||||
|
physical_attack_per_level: 1.1
|
||||||
|
physical_defense_per_level: 1.1
|
||||||
|
magic_attack_per_level: 1.7
|
||||||
|
magic_defense_per_level: 1.6
|
||||||
|
tags: [playable, mage]
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
races:
|
|
||||||
avaline:
|
|
||||||
name: Avaline
|
|
||||||
description: A good and fair creature that many feel is divine, it's both powerful and intelligent.
|
|
||||||
ability_mods: { STR: 5, DEX: 0, INT: 3, WIS: 1, LUK: -2, CHA: -1, CON: 0}
|
|
||||||
alignment: 100
|
|
||||||
|
|
||||||
beastfolk:
|
|
||||||
name: Beastfolk
|
|
||||||
description: Humanoid creatures who are intelligent and fair natured. Half Terran / Half beast.
|
|
||||||
ability_mods: { STR: 3, DEX: 5, INT: 1, WIS: -2, LUK: -1, CHA: 0, CON: 1}
|
|
||||||
alignment: 50
|
|
||||||
|
|
||||||
dwarf:
|
|
||||||
name: Dwarf
|
|
||||||
description: Dwarves are stout, bearded, and skilled in mining, smithing, and engineering, often living in underground kingdoms
|
|
||||||
ability_mods: { STR: 5, DEX: -2, INT: 0, WIS: -1, LUK: 1, CHA: 1, CON: 3}
|
|
||||||
alignment: 50
|
|
||||||
|
|
||||||
elf:
|
|
||||||
name: Elf
|
|
||||||
description: Elves are mythological beings, often depicted as tall, slender, and agile, with pointed ears, and magical abilities.
|
|
||||||
ability_mods: { STR: -2, DEX: 5, INT: 3, WIS: 0, LUK: 1, CHA: 1, CON: -1}
|
|
||||||
alignment: 50
|
|
||||||
|
|
||||||
draconian:
|
|
||||||
name: Draconian
|
|
||||||
description: Draconians are massive, fire-breathing humanoids with scaly skin and noble heritage
|
|
||||||
ability_mods: { STR: 5, DEX: -1, INT: 3, WIS: 1, LUK: -2, CHA: 0, CON: 1}
|
|
||||||
alignment: 0
|
|
||||||
|
|
||||||
terran:
|
|
||||||
name: Terran
|
|
||||||
description: Common folk of the land, similar to what some have called Human.
|
|
||||||
ability_mods: { STR: 1, DEX: 3, INT: 0, WIS: -2, LUK: 5, CHA: 1, CON: -1}
|
|
||||||
alignment: 0
|
|
||||||
|
|
||||||
hellion:
|
|
||||||
name: Hellion
|
|
||||||
description: A creature from the darkest reaches of the shadow, evil.
|
|
||||||
ability_mods: { STR: 3, DEX: 1, INT: 1, WIS: 0, LUK: -2, CHA: 5, CON: -1}
|
|
||||||
alignment: -50
|
|
||||||
|
|
||||||
vorgath:
|
|
||||||
name: Vorgath
|
|
||||||
description: A monstrous creature that's both powerful and intelligent, it's terrifying.
|
|
||||||
ability_mods: { STR: 5, DEX: 3, INT: 1, WIS: 0, LUK: -1, CHA: -2, CON: 3}
|
|
||||||
alignment: -100
|
|
||||||
14
app/game/templates/races/avaline.yaml
Normal file
14
app/game/templates/races/avaline.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
id: avaline
|
||||||
|
name: Avaline
|
||||||
|
description: A good and fair creature that many feel is divine, it's both powerful and intelligent.
|
||||||
|
alignment: 100
|
||||||
|
ability_mods:
|
||||||
|
STR: 5
|
||||||
|
DEX: 0
|
||||||
|
INT: 3
|
||||||
|
WIS: 1
|
||||||
|
LUK: -2
|
||||||
|
CHA: -1
|
||||||
|
CON: 0
|
||||||
|
tags:
|
||||||
|
- playable
|
||||||
14
app/game/templates/races/beastfolk.yaml
Normal file
14
app/game/templates/races/beastfolk.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
id: beastfolk
|
||||||
|
name: Beastfolk
|
||||||
|
description: Humanoid creatures who are intelligent and fair natured. Half Terran / Half beast.
|
||||||
|
alignment: 50
|
||||||
|
ability_mods:
|
||||||
|
STR: 3
|
||||||
|
DEX: 5
|
||||||
|
INT: 1
|
||||||
|
WIS: -2
|
||||||
|
LUK: -1
|
||||||
|
CHA: 0
|
||||||
|
CON: 1
|
||||||
|
tags:
|
||||||
|
- playable
|
||||||
14
app/game/templates/races/draconian.yaml
Normal file
14
app/game/templates/races/draconian.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
id: draconian
|
||||||
|
name: Draconian
|
||||||
|
description: Draconians are massive, fire-breathing humanoids with scaly skin and noble heritage
|
||||||
|
alignment: 0
|
||||||
|
ability_mods:
|
||||||
|
STR: 5
|
||||||
|
DEX: -1
|
||||||
|
INT: 3
|
||||||
|
WIS: 1
|
||||||
|
LUK: -2
|
||||||
|
CHA: 0
|
||||||
|
CON: 1
|
||||||
|
tags:
|
||||||
|
- playable
|
||||||
14
app/game/templates/races/dwarf.yaml
Normal file
14
app/game/templates/races/dwarf.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
id: dwarf
|
||||||
|
name: Dwarf
|
||||||
|
description: Dwarves are stout, bearded, and skilled in mining, smithing, and engineering, often living in underground kingdoms
|
||||||
|
alignment: 50
|
||||||
|
ability_mods:
|
||||||
|
STR: 5
|
||||||
|
DEX: -2
|
||||||
|
INT: 0
|
||||||
|
WIS: -1
|
||||||
|
LUK: 1
|
||||||
|
CHA: 1
|
||||||
|
CON: 3
|
||||||
|
tags:
|
||||||
|
- playable
|
||||||
14
app/game/templates/races/elf.yaml
Normal file
14
app/game/templates/races/elf.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
id: elf
|
||||||
|
name: Elf
|
||||||
|
description: Elves are mythological beings, often depicted as tall, slender, and agile, with pointed ears, and magical abilities.
|
||||||
|
alignment: 50
|
||||||
|
ability_mods:
|
||||||
|
STR: -2
|
||||||
|
DEX: 5
|
||||||
|
INT: 3
|
||||||
|
WIS: 0
|
||||||
|
LUK: 1
|
||||||
|
CHA: 1
|
||||||
|
CON: -1
|
||||||
|
tags:
|
||||||
|
- playable
|
||||||
14
app/game/templates/races/hellion.yaml
Normal file
14
app/game/templates/races/hellion.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
id: hellion
|
||||||
|
name: Hellion
|
||||||
|
description: A creature from the darkest reaches of the shadow, evil.
|
||||||
|
alignment: -50
|
||||||
|
ability_mods:
|
||||||
|
STR: 3
|
||||||
|
DEX: 1
|
||||||
|
INT: 1
|
||||||
|
WIS: 0
|
||||||
|
LUK: -2
|
||||||
|
CHA: 5
|
||||||
|
CON: -1
|
||||||
|
tags:
|
||||||
|
- playable
|
||||||
14
app/game/templates/races/terran.yaml
Normal file
14
app/game/templates/races/terran.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
id: terran
|
||||||
|
name: Terran
|
||||||
|
description: Common folk of the land, similar to what some have called Human.
|
||||||
|
alignment: 0
|
||||||
|
ability_mods:
|
||||||
|
STR: 1
|
||||||
|
DEX: 3
|
||||||
|
INT: 0
|
||||||
|
WIS: -2
|
||||||
|
LUK: 5
|
||||||
|
CHA: 1
|
||||||
|
CON: -1
|
||||||
|
tags:
|
||||||
|
- playable
|
||||||
14
app/game/templates/races/vorgath.yaml
Normal file
14
app/game/templates/races/vorgath.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
id: vorgath
|
||||||
|
name: Vorgath
|
||||||
|
description: A monstrous creature that's both powerful and intelligent, it's terrifying.
|
||||||
|
alignment: -100
|
||||||
|
ability_mods:
|
||||||
|
STR: 5
|
||||||
|
DEX: 3
|
||||||
|
INT: 1
|
||||||
|
WIS: 0
|
||||||
|
LUK: -1
|
||||||
|
CHA: -2
|
||||||
|
CON: 3
|
||||||
|
tags:
|
||||||
|
- playable
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
spells:
|
|
||||||
ember_flicker:
|
|
||||||
name: Ember Flicker
|
|
||||||
description: Cast a small burst of flame, dealing minor damage to all enemies within range.
|
|
||||||
cost_mp: 1
|
|
||||||
element: fire
|
|
||||||
damage: 2
|
|
||||||
level: 1
|
|
||||||
|
|
||||||
spark_streak:
|
|
||||||
name: Spark Streak
|
|
||||||
description: Create a trail of sparks that deal minor fire damage to all enemies they pass through.
|
|
||||||
cost_mp: 4
|
|
||||||
element: fire
|
|
||||||
damage: 6
|
|
||||||
level: 5
|
|
||||||
|
|
||||||
flameburst:
|
|
||||||
name: Flameburst
|
|
||||||
description: Unleash a powerful blast of flame, dealing significant damage to all enemies within a small radius.
|
|
||||||
cost_mp: 10
|
|
||||||
element: fire
|
|
||||||
damage: 20
|
|
||||||
level: 8
|
|
||||||
|
|
||||||
inferno_strike:
|
|
||||||
name: Inferno Strike
|
|
||||||
description: Deal massive fire damage to a single target, also applying a burning effect that deals additional damage over time.
|
|
||||||
cost_mp: 15
|
|
||||||
element: fire
|
|
||||||
damage: 30
|
|
||||||
level: 12
|
|
||||||
|
|
||||||
blaze_wave:
|
|
||||||
name: Blaze Wave
|
|
||||||
description: Cast a wave of flame that deals moderate damage to all enemies it passes through, also knocking them back slightly.
|
|
||||||
cost_mp: 8
|
|
||||||
element: fire
|
|
||||||
damage: 18
|
|
||||||
level: 15
|
|
||||||
|
|
||||||
flame_tongue:
|
|
||||||
name: Flame Tongue
|
|
||||||
description: Lick your target with a stream of flame, dealing moderate fire damage and potentially applying a burning effect.
|
|
||||||
cost_mp: 12
|
|
||||||
element: fire
|
|
||||||
damage: 25
|
|
||||||
level: 18
|
|
||||||
|
|
||||||
incendiary_cloud:
|
|
||||||
name: Incendiary Cloud
|
|
||||||
description: Release a cloud of flaming particles that deal minor fire damage to all enemies within a medium radius, also causing them to take additional damage if they are not wearing any heat-resistant gear.
|
|
||||||
cost_mp: 18
|
|
||||||
element: fire
|
|
||||||
damage: 35
|
|
||||||
level: 22
|
|
||||||
|
|
||||||
scorching_sapling:
|
|
||||||
name: Scorching Sapling
|
|
||||||
description: Set a target area on fire, dealing moderate fire damage to all enemies within it and also applying a burning effect.
|
|
||||||
cost_mp: 12
|
|
||||||
element: fire
|
|
||||||
damage: 25
|
|
||||||
level: 23
|
|
||||||
|
|
||||||
magma_blast:
|
|
||||||
name: Magma Blast
|
|
||||||
description: Unleash a powerful blast of molten lava that deals significant damage to all enemies within its area of effect, also potentially causing them to become stunned.
|
|
||||||
cost_mp: 20
|
|
||||||
element: fire
|
|
||||||
damage: 40
|
|
||||||
level: 25
|
|
||||||
|
|
||||||
flame_ember:
|
|
||||||
name: Flame Ember
|
|
||||||
description: Cast a burning ember that deals minor fire damage to all enemies it passes through and applies a chance to set them on fire.
|
|
||||||
cost_mp: 6
|
|
||||||
element: fire
|
|
||||||
damage: 10
|
|
||||||
level: 30
|
|
||||||
|
|
||||||
blaze_lash:
|
|
||||||
name: Blaze Lash
|
|
||||||
description: Deal moderate fire damage to a single target, also potentially applying a burning effect and increasing your attack speed for a short duration.
|
|
||||||
cost_mp: 15
|
|
||||||
element: fire
|
|
||||||
damage: 28
|
|
||||||
level: 32
|
|
||||||
|
|
||||||
infernal_flames:
|
|
||||||
name: Infernal Flames
|
|
||||||
description: Cast a massive wave of flame that deals significant damage to all enemies within its area of effect, also potentially causing them to become stunned.
|
|
||||||
cost_mp: 25
|
|
||||||
element: fire
|
|
||||||
damage: 50
|
|
||||||
level: 35
|
|
||||||
|
|
||||||
burning_scorch:
|
|
||||||
name: Burning Scorch
|
|
||||||
description: Deal massive fire damage to a single target, also applying a burning effect and dealing additional damage over time if they are not wearing any heat-resistant gear.
|
|
||||||
cost_mp: 25
|
|
||||||
element: fire
|
|
||||||
damage: 45
|
|
||||||
level: 38
|
|
||||||
|
|
||||||
magma_wave:
|
|
||||||
name: Magma Wave
|
|
||||||
description: Cast a wave of molten lava that deals moderate fire damage to all enemies it passes through, also potentially causing them to become stunned.
|
|
||||||
cost_mp: 20
|
|
||||||
element: fire
|
|
||||||
damage: 40
|
|
||||||
level: 40
|
|
||||||
|
|
||||||
flame_burst:
|
|
||||||
name: Flame Burst
|
|
||||||
description: Unleash a powerful blast of flame that deals significant damage to all enemies within its area of effect, also potentially causing them to become stunned.
|
|
||||||
cost_mp: 22
|
|
||||||
element: fire
|
|
||||||
damage: 45
|
|
||||||
level: 42
|
|
||||||
|
|
||||||
inferno_wing:
|
|
||||||
name: Inferno Wing
|
|
||||||
description: Deal massive fire damage to a single target, also applying a burning effect and dealing additional damage over time if they are not wearing any heat-resistant gear.
|
|
||||||
cost_mp: 30
|
|
||||||
element: fire
|
|
||||||
damage: 60
|
|
||||||
level: 45
|
|
||||||
|
|
||||||
flame_frenzy:
|
|
||||||
name: Flame Frenzy
|
|
||||||
description: Cast a wave of flame that deals moderate damage to all enemies it passes through, also increasing your attack speed for a short duration and potentially applying a burning effect.
|
|
||||||
cost_mp: 25
|
|
||||||
element: fire
|
|
||||||
damage: 45
|
|
||||||
level: 48
|
|
||||||
|
|
||||||
infernal_tongue:
|
|
||||||
name: Infernal Tongue
|
|
||||||
description: Lick your target with a stream of flame that deals moderate fire damage, potentially applying a burning effect and dealing additional damage over time.
|
|
||||||
cost_mp: 28
|
|
||||||
element: fire
|
|
||||||
damage: 50
|
|
||||||
level: 50
|
|
||||||
|
|
||||||
magma_rampart:
|
|
||||||
name: Magma Rampart
|
|
||||||
description: Cast a massive wall of molten lava that deals significant damage to all enemies within its area of effect, also potentially causing them to become stunned.
|
|
||||||
cost_mp: 35
|
|
||||||
element: fire
|
|
||||||
damage: 70
|
|
||||||
level: 55
|
|
||||||
|
|
||||||
blazing_scorch:
|
|
||||||
name: Blazing Scorch
|
|
||||||
description: Deal massive fire damage to a single target, also applying a burning effect and dealing additional damage over time if they are not wearing any heat-resistant gear.
|
|
||||||
cost_mp: 35
|
|
||||||
element: fire
|
|
||||||
damage: 70
|
|
||||||
level: 58
|
|
||||||
|
|
||||||
infernal_cloud:
|
|
||||||
name: Infernal Cloud
|
|
||||||
description: Release a massive cloud of flaming particles that deal significant damage to all enemies within its area of effect, also potentially causing them to become stunned.
|
|
||||||
cost_mp: 40
|
|
||||||
element: fire
|
|
||||||
damage: 90
|
|
||||||
level: 60
|
|
||||||
|
|
||||||
magma_crater:
|
|
||||||
name: Magma Crater
|
|
||||||
description: Cast a massive wave of molten lava that deals significant damage to all enemies within its area of effect, also potentially causing them to become stunned.
|
|
||||||
cost_mp: 40
|
|
||||||
element: fire
|
|
||||||
damage: 90
|
|
||||||
level: 62
|
|
||||||
|
|
||||||
infernal_flare:
|
|
||||||
name: Infernal Flare
|
|
||||||
description: Unleash a massive blast of flame that deals significant damage to all enemies within its area of effect, also potentially causing them to become stunned.
|
|
||||||
cost_mp: 45
|
|
||||||
element: fire
|
|
||||||
damage: 100
|
|
||||||
level: 65
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
from typing import Dict, List, Any, Tuple
|
|
||||||
from dataclasses import fields, is_dataclass
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
class TemplateStore:
|
|
||||||
def __init__(self):
|
|
||||||
|
|
||||||
# incase we have problems later
|
|
||||||
# templates_dir = Path(settings.repo_root) / "coc_api" / "app" / "game" / "templates"
|
|
||||||
templates_dir = Path() / "app" / "game" / "templates"
|
|
||||||
|
|
||||||
races_yml_path = templates_dir / "races.yaml"
|
|
||||||
classes_yml_path = templates_dir / "classes.yaml"
|
|
||||||
spells_path = templates_dir / "spells.yaml"
|
|
||||||
|
|
||||||
with open(races_yml_path, "r", encoding="utf-8") as f:
|
|
||||||
self.races = yaml.safe_load(f)["races"]
|
|
||||||
with open(classes_yml_path, "r", encoding="utf-8") as f:
|
|
||||||
self.classes = yaml.safe_load(f)["classes"]
|
|
||||||
with open(spells_path, "r", encoding="utf-8") as f:
|
|
||||||
self.spells = yaml.safe_load(f)["spells"]
|
|
||||||
|
|
||||||
def race(self, race_id: str) -> Dict[str, Any]:
|
|
||||||
if race_id not in self.races:
|
|
||||||
raise KeyError(f"Unknown race: {race_id}")
|
|
||||||
return self.races[race_id]
|
|
||||||
|
|
||||||
def profession(self, class_id: str) -> Dict[str, Any]:
|
|
||||||
if class_id not in self.classes:
|
|
||||||
raise KeyError(f"Unknown class: {class_id}")
|
|
||||||
return self.classes[class_id]
|
|
||||||
|
|
||||||
def spell(self, spell_id: str) -> Dict[str, Any]:
|
|
||||||
if spell_id not in self.spells:
|
|
||||||
raise KeyError(f"Unknown spell: {spell_id}")
|
|
||||||
return self.spells[spell_id]
|
|
||||||
|
|
||||||
def from_dict(cls, d):
|
|
||||||
"""Recursively reconstruct dataclasses from dicts."""
|
|
||||||
if not is_dataclass(cls):
|
|
||||||
return d
|
|
||||||
kwargs = {}
|
|
||||||
for f in fields(cls):
|
|
||||||
value = d.get(f.name)
|
|
||||||
if is_dataclass(f.type):
|
|
||||||
value = from_dict(f.type, value)
|
|
||||||
kwargs[f.name] = value
|
|
||||||
return cls(**kwargs)
|
|
||||||
26
new_hero.py
26
new_hero.py
@@ -1,32 +1,36 @@
|
|||||||
from app.utils.logging import get_logger
|
from app.utils.logging import get_logger
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
|
import json
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
from app.game.generators.entity_factory import build_char
|
from app.game.generators.entity_factory import build_char
|
||||||
from app.game.systems.leveling import calculate_xp_and_level
|
from app.game.generators.level_progression import DEFAULT_LEVEL_PROGRESSION
|
||||||
|
from app.game.systems.leveling import grant_xp
|
||||||
|
|
||||||
player = build_char(
|
player = build_char(
|
||||||
name="Philbert",
|
name="Philbert",
|
||||||
origin_story="I came from a place",
|
origin_story="I came from a place",
|
||||||
race_id="terran",
|
race_id="terran",
|
||||||
class_id="arcanist"
|
# profession_id="arcanist",
|
||||||
|
profession_id="guardian",
|
||||||
|
level=3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
old, new = grant_xp(player,(156),DEFAULT_LEVEL_PROGRESSION)
|
||||||
|
player_dict = asdict(player)
|
||||||
|
print(json.dumps(player_dict,indent=True))
|
||||||
|
exit()
|
||||||
|
|
||||||
# MOVE HIT DICE TO WEAPONS!
|
# MOVE HIT DICE TO WEAPONS!
|
||||||
# ADD DEFENSE STAT
|
# ADD DEFENSE STAT
|
||||||
# ADD ATTACK STAT - this will help with combat!
|
# ADD ATTACK STAT - this will help with combat!
|
||||||
|
|
||||||
|
|
||||||
new_xp_stats = calculate_xp_and_level(current_xp=player.xp,xp_gain=200)
|
# import json
|
||||||
|
# player_dict = asdict(player)
|
||||||
player.xp = new_xp_stats.get("new_xp")
|
# print(json.dumps(player_dict,indent=True))
|
||||||
player.level = new_xp_stats.get("new_level")
|
|
||||||
player.xp_to_next_level = new_xp_stats.get("xp_to_next_level")
|
|
||||||
|
|
||||||
import json
|
|
||||||
player_dict = asdict(player)
|
|
||||||
print(json.dumps(player_dict,indent=True))
|
|
||||||
|
|
||||||
|
|
||||||
# serialize / deserialize
|
# serialize / deserialize
|
||||||
|
|||||||
Reference in New Issue
Block a user