diff --git a/app/game/generators/abilities_factory.py b/app/game/generators/abilities_factory.py index ac3bd18..0cb5875 100644 --- a/app/game/generators/abilities_factory.py +++ b/app/game/generators/abilities_factory.py @@ -37,7 +37,7 @@ def _name(rng: random.Random, theme: Dict) -> str: def _damage_power(tier: int, rng: random.Random) -> float: # 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) return round(base * jitter, 2) diff --git a/app/game/generators/entity_factory.py b/app/game/generators/entity_factory.py index ac9341f..1374203 100644 --- a/app/game/generators/entity_factory.py +++ b/app/game/generators/entity_factory.py @@ -11,27 +11,23 @@ from app.game.systems.leveling import set_level dice = Dice() -# tuning knobs -level_growth = 1.25 +progression = DEFAULT_LEVEL_PROGRESSION - - - -def build_char(name:str, origin_story:str, race_id:str, profession_id:str, fame:int=0, level:int=1) -> Entity: +def build_char(name:str, origin_story:str, race_id:str, profession_id:str,ability_pathway:str,level:int=1) -> Entity: races = RaceRepository() professions = ProfessionRepository() - progression = DEFAULT_LEVEL_PROGRESSION race = races.get(race_id) profession = professions.get(profession_id) + profession.ability_paths e = Entity( uuid = str(uuid.uuid4()), name = name, origin_story = origin_story, - fame = fame, race =race, + ability_pathway=ability_pathway, profession = profession ) diff --git a/app/game/models/entities.py b/app/game/models/entities.py index f4c58ac..e34e0fc 100644 --- a/app/game/models/entities.py +++ b/app/game/models/entities.py @@ -33,6 +33,7 @@ class Entity: origin_story:str = "" race: Race = field(default_factory=Race) profession: Profession = field(default_factory=Profession) + ability_pathway: str = "" level: int = 0 xp: int = 0 diff --git a/app/game/models/professions.py b/app/game/models/professions.py index b45c606..5440958 100644 --- a/app/game/models/professions.py +++ b/app/game/models/professions.py @@ -16,6 +16,7 @@ class Profession: physical_defense_per_level: float magic_attack_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"} 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"), 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"), + ability_paths=(data.get("ability_paths",[])), tags=tags, enemy=enemy, ) diff --git a/app/game/systems/leveling.py b/app/game/systems/leveling.py index 55d8e69..d93ce1b 100644 --- a/app/game/systems/leveling.py +++ b/app/game/systems/leveling.py @@ -1,9 +1,14 @@ # utils/leveling.py +import math +import random from typing import Dict, Any, Callable, Optional, List, Tuple + from app.game.generators.level_progression import LevelProgression from app.game.models.entities import Entity 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: """ 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 entity.xp_to_next_level = prog.xp_for_level(target_level + 1) - spells_list = newly_unlocked_abilities(class_name=entity.profession.name, - path="Hellknight", + # entity get's a random number of spells based on the number of levels gained + 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, - per_tier=1, + per_tier=skills_per_level, primary=entity.profession.primary_stat) - _add_abilities(entity,spells_list) + _add_abilities(entity,skills) _recalc(entity) @@ -46,12 +54,12 @@ def grant_xp(entity:Entity, amount: int, prog: LevelProgression) -> Tuple[int, i if new_level > old_level: for L in range(old_level + 1, new_level + 1): - spells_list = newly_unlocked_abilities(class_name=entity.profession.name, - path="Hellknight", + skills = newly_unlocked_abilities(class_name=entity.profession.name, + path=entity.ability_pathway, level=new_level, per_tier=1, primary=entity.profession.primary_stat) - _add_abilities(entity,spells_list) + _add_abilities(entity,skills) if new_level > old_level: entity.level = new_level @@ -70,6 +78,30 @@ def grant_xp(entity:Entity, amount: int, prog: LevelProgression) -> Tuple[int, i # ---------- 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: for ability in abilities_list: entity.abilities.append(ability) diff --git a/app/game/templates/ability_paths.yaml b/app/game/templates/ability_paths.yaml index 43ab358..01d96cb 100644 --- a/app/game/templates/ability_paths.yaml +++ b/app/game/templates/ability_paths.yaml @@ -10,32 +10,38 @@ Frostbinder: nouns: ["Spike", "Lance", "Shard", "Burst", "Ray"] of_things: ["Frost", "Winter", "Stillness", "Silence", "Cold"] -Bloodborn: +Khaosfire: elements: ["curse", "shadow"] adjectives: ["Tainted", "Cursed", "Corrupted", "Polluted", "Toxic", "Deadly"] nouns: ["Poison", "Infection", "Wound", "Bane", "Plague", "Ravage", "Scourge"] of_things: ["Infectious", "Contagion", "Disease", "Fungus", "Spore", "Seeds"] -Assassin: +Exsanguin: elements: ["wind", "shadow"] adjectives: ["Stealthy", "Deadly", "Ruthless", "Silent", "Lethal", "Fearsome"] nouns: ["Dagger", "Knife", "Poison", "Bolt", "Arrows", "Scimitar", "Twinblade"] of_things: ["Hunter", "Stalker", "Predator", "Ghost", "Shadow", "Silhouette"] -Cleric: +Sanctifier: elements: ["fire", "light"] adjectives: ["Holy", "Divine", "Pure", "Blessed", "Sacred", "Radiant"] nouns: ["Flame", "Lightning", "Bolt", "Sword", "Shield", "Vocation", "Call"] of_things: ["Healing", "Protection", "Blessing", "Salvation", "Redemption", "Mercy"] -Hexist: +Necromantia: elements: ["curse", "shadow"] adjectives: ["Dark", "Malefic", "Sinister", "Maledicta", "Witchlike", "Pestilential"] nouns: ["Spell", "Hex", "Curse", "Influence", "Taint", "Bane", "Malice"] of_things: ["Poison", "Deceit", "Demonic", "Abomination", "Evil", "Sinister"] -Ranger: +Huntsman: elements: ["air", "wind"] adjectives: ["Wild", "Free", "Wanderer", "Survivor", "Huntress", "Skilled"] nouns: ["Arrows", "Rifle", "Bow", "Arrowhead", "Shotgun", "Crossbow", "Pistol"] 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"] \ No newline at end of file diff --git a/app/game/templates/professions/arcanist.yaml b/app/game/templates/professions/arcanist.yaml index 09ed6eb..61de17b 100644 --- a/app/game/templates/professions/arcanist.yaml +++ b/app/game/templates/professions/arcanist.yaml @@ -8,4 +8,5 @@ physical_attack_per_level: 1.1 physical_defense_per_level: 1.1 magic_attack_per_level: 1.7 magic_defense_per_level: 1.6 +ability_paths: ['Frostbinder'] tags: [playable, mage] \ No newline at end of file diff --git a/app/game/templates/professions/assassin.yaml b/app/game/templates/professions/assassin.yaml index d830cbe..bd183c0 100644 --- a/app/game/templates/professions/assassin.yaml +++ b/app/game/templates/professions/assassin.yaml @@ -8,4 +8,5 @@ physical_attack_per_level: 1.8 physical_defense_per_level: 1.4 magic_attack_per_level: 1.2 magic_defense_per_level: 1.1 +ability_paths: ['Exsanguin'] tags: [playable, flex] \ No newline at end of file diff --git a/app/game/templates/professions/bloodborn.yaml b/app/game/templates/professions/bloodborn.yaml index 6eec77d..6430f6c 100644 --- a/app/game/templates/professions/bloodborn.yaml +++ b/app/game/templates/professions/bloodborn.yaml @@ -8,4 +8,5 @@ physical_attack_per_level: 1.8 physical_defense_per_level: 1.4 magic_attack_per_level: 1.2 magic_defense_per_level: 1.1 +ability_paths: ['Khaosfire'] tags: [playable, martial] \ No newline at end of file diff --git a/app/game/templates/professions/cleric.yaml b/app/game/templates/professions/cleric.yaml index eb70059..b7edcd9 100644 --- a/app/game/templates/professions/cleric.yaml +++ b/app/game/templates/professions/cleric.yaml @@ -8,4 +8,5 @@ physical_attack_per_level: 1.1 physical_defense_per_level: 1.1 magic_attack_per_level: 1.7 magic_defense_per_level: 1.6 +ability_paths: ['Sanctifier'] tags: [playable, mage] \ No newline at end of file diff --git a/app/game/templates/professions/guardian.yaml b/app/game/templates/professions/guardian.yaml index 07493fe..8638e71 100644 --- a/app/game/templates/professions/guardian.yaml +++ b/app/game/templates/professions/guardian.yaml @@ -8,4 +8,5 @@ physical_attack_per_level: 1.8 physical_defense_per_level: 1.4 magic_attack_per_level: 1.2 magic_defense_per_level: 1.1 +ability_paths: ['Kratosphere'] tags: [playable, martial] \ No newline at end of file diff --git a/app/game/templates/professions/hexist.yaml b/app/game/templates/professions/hexist.yaml index 889205d..74da7e7 100644 --- a/app/game/templates/professions/hexist.yaml +++ b/app/game/templates/professions/hexist.yaml @@ -8,4 +8,5 @@ physical_attack_per_level: 1.1 physical_defense_per_level: 1.1 magic_attack_per_level: 1.7 magic_defense_per_level: 1.6 +ability_paths: ['Necromantia'] tags: [playable, mage] \ No newline at end of file diff --git a/app/game/templates/professions/ranger.yaml b/app/game/templates/professions/ranger.yaml index 9650e2a..93c7e9b 100644 --- a/app/game/templates/professions/ranger.yaml +++ b/app/game/templates/professions/ranger.yaml @@ -8,4 +8,5 @@ physical_attack_per_level: 1.8 physical_defense_per_level: 1.4 magic_attack_per_level: 1.2 magic_defense_per_level: 1.1 +ability_paths: ['Huntsman'] tags: [playable, flex] \ No newline at end of file diff --git a/app/game/templates/professions/warlock.yaml b/app/game/templates/professions/warlock.yaml index e06ba87..8c1ff25 100644 --- a/app/game/templates/professions/warlock.yaml +++ b/app/game/templates/professions/warlock.yaml @@ -8,4 +8,5 @@ physical_attack_per_level: 1.1 physical_defense_per_level: 1.1 magic_attack_per_level: 1.7 magic_defense_per_level: 1.6 +ability_paths: ['Hellknight'] tags: [playable, mage] \ No newline at end of file diff --git a/docs/char_gen.md b/docs/char_gen.md new file mode 100644 index 0000000..7c40d88 --- /dev/null +++ b/docs/char_gen.md @@ -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. + diff --git a/new_hero.py b/new_hero.py index db81dce..2ccd26c 100644 --- a/new_hero.py +++ b/new_hero.py @@ -12,15 +12,14 @@ player = build_char( name="Philbert", origin_story="I came from a place", race_id="terran", - # profession_id="arcanist", - profession_id="guardian", - level=3 + profession_id="arcanist", + ability_pathway="Frostbinder", + level=50 ) - old, new = grant_xp(player,(156),DEFAULT_LEVEL_PROGRESSION) player_dict = asdict(player) -print(json.dumps(player_dict,indent=True)) +# print(json.dumps(player_dict,indent=True)) exit() # MOVE HIT DICE TO WEAPONS!