# 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