init commit
This commit is contained in:
56
app/game/generators/entity_factory.py
Normal file
56
app/game/generators/entity_factory.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import uuid
|
||||
from typing import Dict, List, Any, Tuple
|
||||
|
||||
from app.game.utils.common import Dice
|
||||
from app.game.models.entities import Entity
|
||||
from app.game.utils.loaders import TemplateStore
|
||||
from app.game.systems.leveling import hp_and_mp_gain
|
||||
|
||||
dice = Dice()
|
||||
|
||||
def build_char(name:str, origin_story:str, race_id:str, class_id:str, fame:int=0) -> Entity:
|
||||
|
||||
templates = TemplateStore()
|
||||
|
||||
# base_ability_scores = {"STR":10,"DEX":10,"CON":10,"INT":10,"WIS":10,"CHA":10, "LUC":10}
|
||||
|
||||
race = templates.race(race_id)
|
||||
profession = templates.profession(class_id)
|
||||
|
||||
e = Entity(
|
||||
uuid = str(uuid.uuid4()),
|
||||
name = name,
|
||||
origin_story = origin_story,
|
||||
fame = fame,
|
||||
level=1,
|
||||
race =race.get("name"),
|
||||
profession = profession.get("name")
|
||||
)
|
||||
|
||||
# assign hit dice
|
||||
e.hit_die = profession.get("hit_die")
|
||||
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
|
||||
e.hp = hp_and_mp_gain(1,e.per_level.hp_die,e.per_level.hp_rule)
|
||||
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():
|
||||
e.ability_scores[stat] = e.ability_scores.get(stat, 10) + int(delta)
|
||||
|
||||
# Apply class grants (spells/skills/equipment + MP base)
|
||||
grants = profession.get("grants", {})
|
||||
|
||||
# add spells to char
|
||||
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
|
||||
38
app/game/models/entities.py
Normal file
38
app/game/models/entities.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Any, Tuple
|
||||
|
||||
@dataclass
|
||||
class PerLevel:
|
||||
hp_die: str = "1d10"
|
||||
hp_rule: str = "avg"
|
||||
mp_die: str = "1d6"
|
||||
mp_rule: str = "avg"
|
||||
|
||||
@dataclass
|
||||
class Entity:
|
||||
uuid: str = ""
|
||||
name: str = ""
|
||||
race: str = ""
|
||||
profession: str = ""
|
||||
origin_story:str = ""
|
||||
hit_die: str = "1d6"
|
||||
|
||||
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
|
||||
|
||||
per_level: PerLevel = field(default_factory=PerLevel)
|
||||
ability_scores: Dict[str, int] = field(default_factory=dict)
|
||||
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)
|
||||
skills: List[Dict[str, int]] = field(default_factory=list)
|
||||
|
||||
72
app/game/systems/leveling.py
Normal file
72
app/game/systems/leveling.py
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
from app.utils.logging import get_logger
|
||||
|
||||
from app.game.utils.common import Dice
|
||||
from app.game.models.entities import Entity
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
level = 1
|
||||
xp_needed = base_xp
|
||||
total_xp_for_next = xp_needed
|
||||
|
||||
# Determine current level from current_xp
|
||||
while current_xp >= total_xp_for_next:
|
||||
level += 1
|
||||
xp_needed = int(base_xp * (growth_rate ** (level - 1)))
|
||||
total_xp_for_next += xp_needed
|
||||
|
||||
# Add new XP
|
||||
new_xp = current_xp + xp_gain
|
||||
|
||||
# Check if level up(s) occurred
|
||||
new_level = level
|
||||
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
|
||||
xp_to_next_level = xp_needed
|
||||
levels_gained = new_level - level
|
||||
|
||||
return {
|
||||
"new_xp": new_xp,
|
||||
"new_level": new_level,
|
||||
"xp_to_next_level": xp_to_next_level,
|
||||
"levels_gained": levels_gained
|
||||
}
|
||||
|
||||
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)
|
||||
88
app/game/templates/classes.yaml
Normal file
88
app/game/templates/classes.yaml
Normal file
@@ -0,0 +1,88 @@
|
||||
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: []
|
||||
48
app/game/templates/races.yaml
Normal file
48
app/game/templates/races.yaml
Normal file
@@ -0,0 +1,48 @@
|
||||
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
|
||||
184
app/game/templates/spells.yaml
Normal file
184
app/game/templates/spells.yaml
Normal file
@@ -0,0 +1,184 @@
|
||||
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
|
||||
24
app/game/utils/common.py
Normal file
24
app/game/utils/common.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from __future__ import annotations
|
||||
import math
|
||||
import random
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
logger = getLogger(__file__)
|
||||
|
||||
class Dice:
|
||||
def roll_dice(self, spec: str = "1d6"):
|
||||
# supports "1d10", "2d6" etc
|
||||
total = 0
|
||||
try:
|
||||
n, d = map(int, spec.lower().split("d"))
|
||||
for _ in range(0,n,1):
|
||||
total += random.randint(1,d)
|
||||
return total
|
||||
except Exception as e:
|
||||
logger.error(f"Unable to roll dice spec: {spec} - please use 1d10 or similar")
|
||||
return 0
|
||||
|
||||
def max_of_die(self, spec: str) -> int:
|
||||
n, d = map(int, spec.lower().split("d"))
|
||||
return n * d
|
||||
50
app/game/utils/loaders.py
Normal file
50
app/game/utils/loaders.py
Normal file
@@ -0,0 +1,50 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user