finished out skills per level, added skill trees to professions templates
This commit is contained in:
@@ -37,7 +37,7 @@ def _name(rng: random.Random, theme: Dict) -> str:
|
|||||||
|
|
||||||
def _damage_power(tier: int, rng: random.Random) -> float:
|
def _damage_power(tier: int, rng: random.Random) -> float:
|
||||||
# Linear-with-jitter curve—easy to reason about and scale
|
# Linear-with-jitter curve—easy to reason about and scale
|
||||||
base = 50 + 7 * tier
|
base = 10 * tier
|
||||||
jitter = 1.0 + rng.uniform(-0.08, 0.08)
|
jitter = 1.0 + rng.uniform(-0.08, 0.08)
|
||||||
return round(base * jitter, 2)
|
return round(base * jitter, 2)
|
||||||
|
|
||||||
|
|||||||
@@ -11,27 +11,23 @@ from app.game.systems.leveling import set_level
|
|||||||
|
|
||||||
dice = Dice()
|
dice = Dice()
|
||||||
|
|
||||||
# tuning knobs
|
progression = DEFAULT_LEVEL_PROGRESSION
|
||||||
level_growth = 1.25
|
|
||||||
|
|
||||||
|
def build_char(name:str, origin_story:str, race_id:str, profession_id:str,ability_pathway:str,level:int=1) -> Entity:
|
||||||
|
|
||||||
|
|
||||||
def build_char(name:str, origin_story:str, race_id:str, profession_id:str, fame:int=0, level:int=1) -> Entity:
|
|
||||||
|
|
||||||
races = RaceRepository()
|
races = RaceRepository()
|
||||||
professions = ProfessionRepository()
|
professions = ProfessionRepository()
|
||||||
progression = DEFAULT_LEVEL_PROGRESSION
|
|
||||||
|
|
||||||
race = races.get(race_id)
|
race = races.get(race_id)
|
||||||
profession = professions.get(profession_id)
|
profession = professions.get(profession_id)
|
||||||
|
profession.ability_paths
|
||||||
|
|
||||||
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,
|
|
||||||
race =race,
|
race =race,
|
||||||
|
ability_pathway=ability_pathway,
|
||||||
profession = profession
|
profession = profession
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class Entity:
|
|||||||
origin_story:str = ""
|
origin_story:str = ""
|
||||||
race: Race = field(default_factory=Race)
|
race: Race = field(default_factory=Race)
|
||||||
profession: Profession = field(default_factory=Profession)
|
profession: Profession = field(default_factory=Profession)
|
||||||
|
ability_pathway: str = ""
|
||||||
|
|
||||||
level: int = 0
|
level: int = 0
|
||||||
xp: int = 0
|
xp: int = 0
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class Profession:
|
|||||||
physical_defense_per_level: float
|
physical_defense_per_level: float
|
||||||
magic_attack_per_level: float
|
magic_attack_per_level: float
|
||||||
magic_defense_per_level: float
|
magic_defense_per_level: float
|
||||||
|
ability_paths: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
tags: list[str] = field(default_factory=list) # e.g., {"playable"}, {"leader","elite"}
|
tags: list[str] = field(default_factory=list) # e.g., {"playable"}, {"leader","elite"}
|
||||||
enemy: Optional[EnemyProfile] = None # ⬅ optional enemy-only tuning
|
enemy: Optional[EnemyProfile] = None # ⬅ optional enemy-only tuning
|
||||||
@@ -88,6 +89,7 @@ class Profession:
|
|||||||
physical_defense_per_level=as_float(data["physical_defense_per_level"], "physical_defense_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_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"),
|
magic_defense_per_level=as_float(data["magic_defense_per_level"], "magic_defense_per_level"),
|
||||||
|
ability_paths=(data.get("ability_paths",[])),
|
||||||
tags=tags,
|
tags=tags,
|
||||||
enemy=enemy,
|
enemy=enemy,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
# utils/leveling.py
|
# utils/leveling.py
|
||||||
|
import math
|
||||||
|
import random
|
||||||
from typing import Dict, Any, Callable, Optional, List, Tuple
|
from typing import Dict, Any, Callable, Optional, List, Tuple
|
||||||
|
|
||||||
from app.game.generators.level_progression import LevelProgression
|
from app.game.generators.level_progression import LevelProgression
|
||||||
from app.game.models.entities import Entity
|
from app.game.models.entities import Entity
|
||||||
from app.game.generators.abilities_factory import newly_unlocked_abilities
|
from app.game.generators.abilities_factory import newly_unlocked_abilities
|
||||||
|
|
||||||
|
GLOBAL_CHANCE_PER_LEVEL = 0.3
|
||||||
|
|
||||||
def set_level(entity:Entity, target_level: int, prog: LevelProgression) -> None:
|
def set_level(entity:Entity, target_level: int, prog: LevelProgression) -> None:
|
||||||
"""
|
"""
|
||||||
Snap an entity to a *specific* level. Sets XP to that level's floor.
|
Snap an entity to a *specific* level. Sets XP to that level's floor.
|
||||||
@@ -25,12 +30,15 @@ def set_level(entity:Entity, target_level: int, prog: LevelProgression) -> None:
|
|||||||
# set next level xp as xp needed for next level
|
# set next level xp as xp needed for next level
|
||||||
entity.xp_to_next_level = prog.xp_for_level(target_level + 1)
|
entity.xp_to_next_level = prog.xp_for_level(target_level + 1)
|
||||||
|
|
||||||
spells_list = newly_unlocked_abilities(class_name=entity.profession.name,
|
# entity get's a random number of spells based on the number of levels gained
|
||||||
path="Hellknight",
|
skills_per_level = calculate_skills_gained(current,target_level)
|
||||||
|
|
||||||
|
skills = newly_unlocked_abilities(class_name=entity.profession.name,
|
||||||
|
path=entity.ability_pathway,
|
||||||
level=target_level,
|
level=target_level,
|
||||||
per_tier=1,
|
per_tier=skills_per_level,
|
||||||
primary=entity.profession.primary_stat)
|
primary=entity.profession.primary_stat)
|
||||||
_add_abilities(entity,spells_list)
|
_add_abilities(entity,skills)
|
||||||
|
|
||||||
_recalc(entity)
|
_recalc(entity)
|
||||||
|
|
||||||
@@ -46,12 +54,12 @@ def grant_xp(entity:Entity, amount: int, prog: LevelProgression) -> Tuple[int, i
|
|||||||
|
|
||||||
if new_level > old_level:
|
if new_level > old_level:
|
||||||
for L in range(old_level + 1, new_level + 1):
|
for L in range(old_level + 1, new_level + 1):
|
||||||
spells_list = newly_unlocked_abilities(class_name=entity.profession.name,
|
skills = newly_unlocked_abilities(class_name=entity.profession.name,
|
||||||
path="Hellknight",
|
path=entity.ability_pathway,
|
||||||
level=new_level,
|
level=new_level,
|
||||||
per_tier=1,
|
per_tier=1,
|
||||||
primary=entity.profession.primary_stat)
|
primary=entity.profession.primary_stat)
|
||||||
_add_abilities(entity,spells_list)
|
_add_abilities(entity,skills)
|
||||||
|
|
||||||
if new_level > old_level:
|
if new_level > old_level:
|
||||||
entity.level = new_level
|
entity.level = new_level
|
||||||
@@ -70,6 +78,30 @@ def grant_xp(entity:Entity, amount: int, prog: LevelProgression) -> Tuple[int, i
|
|||||||
|
|
||||||
# ---------- reward + recalc helpers ----------
|
# ---------- reward + recalc helpers ----------
|
||||||
|
|
||||||
|
def calculate_skills_gained(current_level: int, target_level: int, chance_per_level: float = GLOBAL_CHANCE_PER_LEVEL) -> int:
|
||||||
|
"""
|
||||||
|
Returns the number of new skills a player might gain
|
||||||
|
when leveling from current_level to target_level.
|
||||||
|
|
||||||
|
chance_per_level: probability (0–1) of gaining a skill per level.
|
||||||
|
Guarantees at least 2 skills for small level gains.
|
||||||
|
"""
|
||||||
|
levels_gained = max(0, target_level - current_level)
|
||||||
|
if levels_gained == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Simulate random gain per level
|
||||||
|
gained = sum(1 for _ in range(levels_gained) if random.random() < chance_per_level)
|
||||||
|
|
||||||
|
# Guarantee at least 2 skills if the player barely leveled
|
||||||
|
if levels_gained < 3:
|
||||||
|
gained = max(gained, 2)
|
||||||
|
|
||||||
|
# Smooth scaling: for huge jumps, don't explode linearly
|
||||||
|
# (cap roughly at 40% of total levels gained)
|
||||||
|
cap = math.ceil(levels_gained * 0.4)
|
||||||
|
return min(gained, cap)
|
||||||
|
|
||||||
def _add_abilities(entity:Entity, abilities_list:list) -> None:
|
def _add_abilities(entity:Entity, abilities_list:list) -> None:
|
||||||
for ability in abilities_list:
|
for ability in abilities_list:
|
||||||
entity.abilities.append(ability)
|
entity.abilities.append(ability)
|
||||||
|
|||||||
@@ -10,32 +10,38 @@ Frostbinder:
|
|||||||
nouns: ["Spike", "Lance", "Shard", "Burst", "Ray"]
|
nouns: ["Spike", "Lance", "Shard", "Burst", "Ray"]
|
||||||
of_things: ["Frost", "Winter", "Stillness", "Silence", "Cold"]
|
of_things: ["Frost", "Winter", "Stillness", "Silence", "Cold"]
|
||||||
|
|
||||||
Bloodborn:
|
Khaosfire:
|
||||||
elements: ["curse", "shadow"]
|
elements: ["curse", "shadow"]
|
||||||
adjectives: ["Tainted", "Cursed", "Corrupted", "Polluted", "Toxic", "Deadly"]
|
adjectives: ["Tainted", "Cursed", "Corrupted", "Polluted", "Toxic", "Deadly"]
|
||||||
nouns: ["Poison", "Infection", "Wound", "Bane", "Plague", "Ravage", "Scourge"]
|
nouns: ["Poison", "Infection", "Wound", "Bane", "Plague", "Ravage", "Scourge"]
|
||||||
of_things: ["Infectious", "Contagion", "Disease", "Fungus", "Spore", "Seeds"]
|
of_things: ["Infectious", "Contagion", "Disease", "Fungus", "Spore", "Seeds"]
|
||||||
|
|
||||||
Assassin:
|
Exsanguin:
|
||||||
elements: ["wind", "shadow"]
|
elements: ["wind", "shadow"]
|
||||||
adjectives: ["Stealthy", "Deadly", "Ruthless", "Silent", "Lethal", "Fearsome"]
|
adjectives: ["Stealthy", "Deadly", "Ruthless", "Silent", "Lethal", "Fearsome"]
|
||||||
nouns: ["Dagger", "Knife", "Poison", "Bolt", "Arrows", "Scimitar", "Twinblade"]
|
nouns: ["Dagger", "Knife", "Poison", "Bolt", "Arrows", "Scimitar", "Twinblade"]
|
||||||
of_things: ["Hunter", "Stalker", "Predator", "Ghost", "Shadow", "Silhouette"]
|
of_things: ["Hunter", "Stalker", "Predator", "Ghost", "Shadow", "Silhouette"]
|
||||||
|
|
||||||
Cleric:
|
Sanctifier:
|
||||||
elements: ["fire", "light"]
|
elements: ["fire", "light"]
|
||||||
adjectives: ["Holy", "Divine", "Pure", "Blessed", "Sacred", "Radiant"]
|
adjectives: ["Holy", "Divine", "Pure", "Blessed", "Sacred", "Radiant"]
|
||||||
nouns: ["Flame", "Lightning", "Bolt", "Sword", "Shield", "Vocation", "Call"]
|
nouns: ["Flame", "Lightning", "Bolt", "Sword", "Shield", "Vocation", "Call"]
|
||||||
of_things: ["Healing", "Protection", "Blessing", "Salvation", "Redemption", "Mercy"]
|
of_things: ["Healing", "Protection", "Blessing", "Salvation", "Redemption", "Mercy"]
|
||||||
|
|
||||||
Hexist:
|
Necromantia:
|
||||||
elements: ["curse", "shadow"]
|
elements: ["curse", "shadow"]
|
||||||
adjectives: ["Dark", "Malefic", "Sinister", "Maledicta", "Witchlike", "Pestilential"]
|
adjectives: ["Dark", "Malefic", "Sinister", "Maledicta", "Witchlike", "Pestilential"]
|
||||||
nouns: ["Spell", "Hex", "Curse", "Influence", "Taint", "Bane", "Malice"]
|
nouns: ["Spell", "Hex", "Curse", "Influence", "Taint", "Bane", "Malice"]
|
||||||
of_things: ["Poison", "Deceit", "Demonic", "Abomination", "Evil", "Sinister"]
|
of_things: ["Poison", "Deceit", "Demonic", "Abomination", "Evil", "Sinister"]
|
||||||
|
|
||||||
Ranger:
|
Huntsman:
|
||||||
elements: ["air", "wind"]
|
elements: ["air", "wind"]
|
||||||
adjectives: ["Wild", "Free", "Wanderer", "Survivor", "Huntress", "Skilled"]
|
adjectives: ["Wild", "Free", "Wanderer", "Survivor", "Huntress", "Skilled"]
|
||||||
nouns: ["Arrows", "Rifle", "Bow", "Arrowhead", "Shotgun", "Crossbow", "Pistol"]
|
nouns: ["Arrows", "Rifle", "Bow", "Arrowhead", "Shotgun", "Crossbow", "Pistol"]
|
||||||
of_things: ["Wolves", "Forest", "Wilderness", "Hunter", "Tracking", "Ambush"]
|
of_things: ["Wolves", "Forest", "Wilderness", "Hunter", "Tracking", "Ambush"]
|
||||||
|
|
||||||
|
Kratosphere:
|
||||||
|
elements: ["stone", "earth"]
|
||||||
|
adjectives: ["Rocky", "Solid", "Unyielding", "Resilient", "Firm"]
|
||||||
|
nouns: ["Shield", "Spike", "Breach", "Barrier", "Fortress"]
|
||||||
|
of_things: ["Stonework", "Rockfall", "Stonefall", "Earthquake", "Bedrock"]
|
||||||
@@ -8,4 +8,5 @@ physical_attack_per_level: 1.1
|
|||||||
physical_defense_per_level: 1.1
|
physical_defense_per_level: 1.1
|
||||||
magic_attack_per_level: 1.7
|
magic_attack_per_level: 1.7
|
||||||
magic_defense_per_level: 1.6
|
magic_defense_per_level: 1.6
|
||||||
|
ability_paths: ['Frostbinder']
|
||||||
tags: [playable, mage]
|
tags: [playable, mage]
|
||||||
@@ -8,4 +8,5 @@ physical_attack_per_level: 1.8
|
|||||||
physical_defense_per_level: 1.4
|
physical_defense_per_level: 1.4
|
||||||
magic_attack_per_level: 1.2
|
magic_attack_per_level: 1.2
|
||||||
magic_defense_per_level: 1.1
|
magic_defense_per_level: 1.1
|
||||||
|
ability_paths: ['Exsanguin']
|
||||||
tags: [playable, flex]
|
tags: [playable, flex]
|
||||||
@@ -8,4 +8,5 @@ physical_attack_per_level: 1.8
|
|||||||
physical_defense_per_level: 1.4
|
physical_defense_per_level: 1.4
|
||||||
magic_attack_per_level: 1.2
|
magic_attack_per_level: 1.2
|
||||||
magic_defense_per_level: 1.1
|
magic_defense_per_level: 1.1
|
||||||
|
ability_paths: ['Khaosfire']
|
||||||
tags: [playable, martial]
|
tags: [playable, martial]
|
||||||
@@ -8,4 +8,5 @@ physical_attack_per_level: 1.1
|
|||||||
physical_defense_per_level: 1.1
|
physical_defense_per_level: 1.1
|
||||||
magic_attack_per_level: 1.7
|
magic_attack_per_level: 1.7
|
||||||
magic_defense_per_level: 1.6
|
magic_defense_per_level: 1.6
|
||||||
|
ability_paths: ['Sanctifier']
|
||||||
tags: [playable, mage]
|
tags: [playable, mage]
|
||||||
@@ -8,4 +8,5 @@ physical_attack_per_level: 1.8
|
|||||||
physical_defense_per_level: 1.4
|
physical_defense_per_level: 1.4
|
||||||
magic_attack_per_level: 1.2
|
magic_attack_per_level: 1.2
|
||||||
magic_defense_per_level: 1.1
|
magic_defense_per_level: 1.1
|
||||||
|
ability_paths: ['Kratosphere']
|
||||||
tags: [playable, martial]
|
tags: [playable, martial]
|
||||||
@@ -8,4 +8,5 @@ physical_attack_per_level: 1.1
|
|||||||
physical_defense_per_level: 1.1
|
physical_defense_per_level: 1.1
|
||||||
magic_attack_per_level: 1.7
|
magic_attack_per_level: 1.7
|
||||||
magic_defense_per_level: 1.6
|
magic_defense_per_level: 1.6
|
||||||
|
ability_paths: ['Necromantia']
|
||||||
tags: [playable, mage]
|
tags: [playable, mage]
|
||||||
@@ -8,4 +8,5 @@ physical_attack_per_level: 1.8
|
|||||||
physical_defense_per_level: 1.4
|
physical_defense_per_level: 1.4
|
||||||
magic_attack_per_level: 1.2
|
magic_attack_per_level: 1.2
|
||||||
magic_defense_per_level: 1.1
|
magic_defense_per_level: 1.1
|
||||||
|
ability_paths: ['Huntsman']
|
||||||
tags: [playable, flex]
|
tags: [playable, flex]
|
||||||
@@ -8,4 +8,5 @@ physical_attack_per_level: 1.1
|
|||||||
physical_defense_per_level: 1.1
|
physical_defense_per_level: 1.1
|
||||||
magic_attack_per_level: 1.7
|
magic_attack_per_level: 1.7
|
||||||
magic_defense_per_level: 1.6
|
magic_defense_per_level: 1.6
|
||||||
|
ability_paths: ['Hellknight']
|
||||||
tags: [playable, mage]
|
tags: [playable, mage]
|
||||||
291
docs/char_gen.md
Normal file
291
docs/char_gen.md
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
# 🧩 Code of Conquest — Character Generation (Flask API)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document describes how **character generation** operates inside the Flask API.
|
||||||
|
The goal is to produce a fully realized `Entity` (hero, enemy, or NPC) based on **race**, **profession**, **abilities**, and **level** using YAML-defined data and procedural logic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Directory Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── blueprints/
|
||||||
|
│ ├── main.py # Root routes
|
||||||
|
│ └── char.py # Character creation and progression endpoints
|
||||||
|
│
|
||||||
|
├── game/
|
||||||
|
│ ├── generators/ # Procedural content
|
||||||
|
│ │ ├── abilities_factory.py # Generates abilities / skills
|
||||||
|
│ │ ├── entity_factory.py # Builds complete entity objects
|
||||||
|
│ │ ├── level_progression.py # XP and level curve logic
|
||||||
|
│ │ └── ... (compiled files)
|
||||||
|
│ │
|
||||||
|
│ ├── loaders/ # YAML loaders for static templates
|
||||||
|
│ │ ├── profession_loader.py
|
||||||
|
│ │ ├── races_loader.py
|
||||||
|
│ │ └── spells_loader.py # May be deprecated in favor of skills
|
||||||
|
│ │
|
||||||
|
│ ├── models/ # Dataclasses for core game objects
|
||||||
|
│ │ ├── abilities.py
|
||||||
|
│ │ ├── entities.py
|
||||||
|
│ │ ├── professions.py
|
||||||
|
│ │ ├── races.py
|
||||||
|
│ │ └── enemies.py
|
||||||
|
│ │
|
||||||
|
│ ├── systems/
|
||||||
|
│ │ └── leveling.py # Handles XP gain, set_level, and skill growth
|
||||||
|
│ │
|
||||||
|
│ ├── templates/
|
||||||
|
│ │ ├── ability_paths.yaml # Defines thematic skill sets (paths)
|
||||||
|
│ │ ├── professions/*.yaml # Defines all professions
|
||||||
|
│ │ └── races/*.yaml # Defines all playable races
|
||||||
|
│ │
|
||||||
|
│ └── utils/
|
||||||
|
│ └── common.py
|
||||||
|
│
|
||||||
|
├── models/ # Shared models for the Flask layer
|
||||||
|
│ ├── hero.py
|
||||||
|
│ ├── enums.py
|
||||||
|
│ └── primitives.py
|
||||||
|
│
|
||||||
|
└── utils/
|
||||||
|
├── catalogs/ # Compiled catalog data for fast lookups
|
||||||
|
│ ├── race_catalog.py
|
||||||
|
│ ├── hero_catalog.py
|
||||||
|
│ └── skill_catalog.py
|
||||||
|
├── api_response.py
|
||||||
|
└── settings.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧬 Entity Generation Pipeline
|
||||||
|
|
||||||
|
### 1. `entity_factory.build_entity()`
|
||||||
|
|
||||||
|
Creates a base entity by combining **Race** and **Profession** definitions.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
1. Load race + profession data from YAML via loaders.
|
||||||
|
2. Apply racial ability modifiers.
|
||||||
|
3. Apply profession base stats and per-level growth rates.
|
||||||
|
4. Initialize HP, MP, XP, and skill lists.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. `leveling.set_level(entity, target_level)`
|
||||||
|
|
||||||
|
Handles level setting and skill gain.
|
||||||
|
|
||||||
|
* Calculates XP using `level_progression.py`.
|
||||||
|
* Calls `calculate_skills_gained()` (randomized 30% chance per level).
|
||||||
|
* Generates new skills via `abilities_factory`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧝 Races
|
||||||
|
|
||||||
|
Located in `app/game/templates/races/`.
|
||||||
|
|
||||||
|
Each race defines:
|
||||||
|
|
||||||
|
* Base ability modifiers (STR, DEX, CON, INT, WIS, CHA)
|
||||||
|
* Optional descriptive text
|
||||||
|
* Tags for gameplay classification
|
||||||
|
|
||||||
|
Example (`elf.yaml`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Elf
|
||||||
|
description: Graceful and attuned to magic.
|
||||||
|
ability_mods:
|
||||||
|
DEX: 2
|
||||||
|
INT: 1
|
||||||
|
CON: -1
|
||||||
|
tags: ["playable"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚔️ Professions
|
||||||
|
|
||||||
|
Located in `app/game/templates/professions/`.
|
||||||
|
|
||||||
|
Each profession file defines:
|
||||||
|
|
||||||
|
* Base HP / MP
|
||||||
|
* Scaling per level
|
||||||
|
* Primary stat (INT, WIS, etc.)
|
||||||
|
* Flavor / lore fields
|
||||||
|
|
||||||
|
Example (`warlock.yaml`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
id: warlock
|
||||||
|
name: Warlock
|
||||||
|
description: A wielder of forbidden demonic power.
|
||||||
|
primary_stat: CHA
|
||||||
|
base_hp: 40
|
||||||
|
base_mp: 80
|
||||||
|
physical_attack_per_level: 1.0
|
||||||
|
physical_defense_per_level: 0.5
|
||||||
|
magic_attack_per_level: 2.5
|
||||||
|
magic_defense_per_level: 1.5
|
||||||
|
tags: ["caster","dark"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 Ability / Skill Generation
|
||||||
|
|
||||||
|
### Path Themes
|
||||||
|
|
||||||
|
Defined in `templates/ability_paths.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Hellknight:
|
||||||
|
elements: ["fire", "shadow"]
|
||||||
|
adjectives: ["Infernal", "Hellfire", "Brimstone", "Abyssal"]
|
||||||
|
nouns: ["Edict", "Judgment", "Brand", "Smite"]
|
||||||
|
of_things: ["Dominion", "Torment", "Cinders", "Night"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generator Function
|
||||||
|
|
||||||
|
Located in `abilities_factory.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def generate_spells_direct_damage(
|
||||||
|
class_name: str,
|
||||||
|
path: str,
|
||||||
|
level: int,
|
||||||
|
per_tier: int = 2,
|
||||||
|
content_version: int = 1,
|
||||||
|
primary: str = "INT"
|
||||||
|
) -> List[Spell]:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
* Deterministic random seed based on `(class, path, level, version)`
|
||||||
|
* Generates direct-damage only
|
||||||
|
* Scales power and cost by level
|
||||||
|
* Names and effects are thematically consistent
|
||||||
|
|
||||||
|
### Example Output
|
||||||
|
|
||||||
|
```
|
||||||
|
Infernal Judgment — Deal fire damage (power 120, CHA+125%). MP 30, CD 1
|
||||||
|
Brand of Cinders — Deal shadow damage (power 118, CHA+125%). MP 30, CD 0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Level Progression
|
||||||
|
|
||||||
|
Handled in `level_progression.py`.
|
||||||
|
|
||||||
|
* Defines XP thresholds per level (exponential curve)
|
||||||
|
* Provides helper: `get_xp_for_level(level)`
|
||||||
|
* Ensures `entity.xp_to_next_level` is always accurate after XP updates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎲 Skill Gain Logic
|
||||||
|
|
||||||
|
Implemented in `systems/leveling.py`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def calculate_skills_gained(current_level, target_level, chance_per_level=0.3):
|
||||||
|
levels_gained = max(0, target_level - current_level)
|
||||||
|
if levels_gained == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
gained = sum(1 for _ in range(levels_gained) if random.random() < chance_per_level)
|
||||||
|
if levels_gained < 3:
|
||||||
|
gained = max(gained, 2)
|
||||||
|
|
||||||
|
cap = math.ceil(levels_gained * 0.4)
|
||||||
|
return min(gained, cap)
|
||||||
|
```
|
||||||
|
|
||||||
|
* **30% random chance** per level to gain a new skill
|
||||||
|
* Guarantees **≥ 2** if leveling fewer than 3 levels
|
||||||
|
* Caps maximum growth to **40%** of total levels gained
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧮 XP / Level Curve Example
|
||||||
|
|
||||||
|
| Level | XP Needed (example) | XP Delta | Notes |
|
||||||
|
| ----- | ------------------- | -------- | ----------------- |
|
||||||
|
| 1 | 0 | — | Starting level |
|
||||||
|
| 2 | 100 | +100 | Fast early growth |
|
||||||
|
| 5 | 625 | +175 | Moderate |
|
||||||
|
| 10 | 2500 | +350 | Midgame plateau |
|
||||||
|
| 20 | 10000 | +750 | Late-game scaling |
|
||||||
|
|
||||||
|
*(Values may differ depending on `level_progression.py` formula)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ API Integration
|
||||||
|
|
||||||
|
### Blueprint: `char.py`
|
||||||
|
|
||||||
|
Handles player-facing endpoints such as:
|
||||||
|
|
||||||
|
* `POST /char/create` → build entity from race + profession
|
||||||
|
* `POST /char/level` → level up existing entity and add new skills
|
||||||
|
* `GET /char/:id` → return current character stats, XP, and skills
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@char_bp.route("/create", methods=["POST"])
|
||||||
|
def create_character():
|
||||||
|
data = request.json
|
||||||
|
entity = build_entity(data["race"], data["profession"])
|
||||||
|
return jsonify(entity.to_dict())
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Data Flow Summary
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[API Request] --> B[char.py Blueprint]
|
||||||
|
B --> C[entity_factory.build_entity()]
|
||||||
|
C --> D[races_loader / profession_loader]
|
||||||
|
C --> E[level_progression]
|
||||||
|
C --> F[abilities_factory]
|
||||||
|
F --> G[ability_paths.yaml]
|
||||||
|
C --> H[Entity Model]
|
||||||
|
H --> I[API Response JSON]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧾 Design Notes
|
||||||
|
|
||||||
|
* All YAML templates are static assets — easy to edit without code changes.
|
||||||
|
* Procedural factories (`abilities_factory`, `entity_factory`) ensure deterministic generation for the same input.
|
||||||
|
* Systems (`leveling.py`) handle simulation and logic, isolated from API routes.
|
||||||
|
* `utils/catalogs` may eventually cache or precompute available skills, professions, and races for faster response times.
|
||||||
|
* The entire pipeline can operate **statelessly** inside Flask routes or **persisted** in a database later.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔮 Future Ideas
|
||||||
|
|
||||||
|
* Add **passive traits** per race or profession.
|
||||||
|
* Implement **rarity** or **tiered skills** (common → legendary).
|
||||||
|
* Generate **enemy entities** using the same system with difficulty scaling.
|
||||||
|
* Add a **training system** or **skill mastery** mechanic tied to XP.
|
||||||
|
* Create **hero_catalog.py** entries for pre-built templates used in story mode.
|
||||||
|
|
||||||
@@ -12,15 +12,14 @@ 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",
|
||||||
# profession_id="arcanist",
|
profession_id="arcanist",
|
||||||
profession_id="guardian",
|
ability_pathway="Frostbinder",
|
||||||
level=3
|
level=50
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
old, new = grant_xp(player,(156),DEFAULT_LEVEL_PROGRESSION)
|
old, new = grant_xp(player,(156),DEFAULT_LEVEL_PROGRESSION)
|
||||||
player_dict = asdict(player)
|
player_dict = asdict(player)
|
||||||
print(json.dumps(player_dict,indent=True))
|
# print(json.dumps(player_dict,indent=True))
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
# MOVE HIT DICE TO WEAPONS!
|
# MOVE HIT DICE TO WEAPONS!
|
||||||
|
|||||||
Reference in New Issue
Block a user