complete regen of hero classes, spells, races, etc
This commit is contained in:
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 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)
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
Reference in New Issue
Block a user