init commit

This commit is contained in:
2025-11-02 01:14:41 -05:00
commit 7bf81109b3
31 changed files with 2387 additions and 0 deletions

View 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

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

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

View 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: []

View 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

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