complete regen of hero classes, spells, races, etc

This commit is contained in:
2025-11-02 18:16:00 -06:00
parent 7bf81109b3
commit fd572076e0
34 changed files with 882 additions and 489 deletions

View 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)

View 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"

View File

@@ -2,37 +2,55 @@ from __future__ import annotations
from dataclasses import dataclass, field
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
class PerLevel:
hp_die: str = "1d10"
hp_rule: str = "avg"
mp_die: str = "1d6"
mp_rule: str = "avg"
class AbilityScores:
STR: int = 10
DEX: int = 10
INT: int = 10
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
class Entity:
uuid: str = ""
name: str = ""
race: str = ""
profession: 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
xp: int = 0
xp_to_next_level:int = 100
fame: 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)
ability_scores: Dict[str, int] = field(default_factory=dict)
status: Status = field(default_factory=Status)
ability_scores: AbilityScores = field(default_factory=AbilityScores)
weapons: 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)

View 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
View 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
)