138 lines
5.2 KiB
Python
138 lines
5.2 KiB
Python
# 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.
|
||
Optionally applies all level-up rewards from current_level+1 .. target_level (idempotent if you recompute stats from level).
|
||
"""
|
||
# ensures we never try to go above the max level in the game
|
||
target_level = max(1, min(target_level, prog.max_level))
|
||
current = entity.level
|
||
|
||
# if not changing levels, just set the xp and move along
|
||
if current == target_level:
|
||
entity.xp = prog.xp_for_level(target_level)
|
||
_recalc(entity)
|
||
|
||
# Set final level + floor XP
|
||
entity.level = target_level
|
||
entity.xp = prog.xp_for_level(target_level)
|
||
|
||
# set next level xp as xp needed for next level
|
||
entity.xp_to_next_level = prog.xp_for_level(target_level + 1)
|
||
|
||
# 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=skills_per_level,
|
||
primary=entity.profession.primary_stat)
|
||
_add_abilities(entity,skills)
|
||
|
||
_recalc(entity)
|
||
|
||
def grant_xp(entity:Entity, amount: int, prog: LevelProgression) -> Tuple[int, int]:
|
||
"""
|
||
Add XP and auto-level if thresholds crossed.
|
||
Returns (old_level, new_level).
|
||
"""
|
||
old_level = entity.level or 1
|
||
entity.xp = entity.xp + int(amount)
|
||
|
||
new_level = prog.level_for_xp(entity.xp)
|
||
|
||
if new_level > old_level:
|
||
for L in range(old_level + 1, new_level + 1):
|
||
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,skills)
|
||
|
||
if new_level > old_level:
|
||
entity.level = new_level
|
||
_recalc(entity)
|
||
|
||
# --- compute XP to next level ---
|
||
if new_level >= prog.max_level:
|
||
# Maxed out
|
||
entity.xp_to_next_level = 0
|
||
else:
|
||
next_floor = prog.xp_for_level(new_level + 1)
|
||
entity.xp_to_next_level = max(0, next_floor - entity.xp)
|
||
|
||
|
||
return old_level, new_level
|
||
|
||
# ---------- 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)
|
||
|
||
def _recalc(entity:Entity) -> None:
|
||
"""
|
||
Recompute derived stats from entity.level + profession.
|
||
Replace with your actual formulas.
|
||
"""
|
||
L = entity.level
|
||
prof = entity.profession
|
||
|
||
# scale attack/defense by per-level gains
|
||
if prof:
|
||
base_pa = entity.profession.physical_attack_per_level or 0
|
||
base_pd = entity.profession.physical_defense_per_level or 0
|
||
base_ma = entity.profession.magic_attack_per_level or 0
|
||
base_md = entity.profession.magic_defense_per_level or 0
|
||
|
||
entity.physical_attack = round(base_pa + prof.physical_attack_per_level * (L - 1),2)
|
||
entity.physical_defense = round(base_pd + prof.physical_defense_per_level * (L - 1),2)
|
||
entity.magic_attack = round(base_ma + prof.magic_attack_per_level * (L - 1),2)
|
||
entity.magic_defense = round(base_md + prof.magic_defense_per_level * (L - 1),2)
|
||
|
||
# HP/MP growth from profession base
|
||
if prof:
|
||
entity.status.max_hp = entity.status.max_hp + int(prof.base_hp) + int((L * prof.base_hp) * 0.5)
|
||
entity.status.max_mp = entity.status.max_mp + int(prof.base_mp) + int((L * prof.base_mp) * 0.5)
|
||
|
||
# set current to max if you keep current_hp/mp
|
||
entity.status.current_hp = entity.status.max_hp
|
||
entity.status.current_mp = entity.status.max_mp
|
||
|