--- a/server/world/level_progression_service.py +++ b/server/world/level_progression_service.py @@ -41,6 +41,10 @@ """Retrieves mathematical benchmark for a specific level.""" return self._benchmarks.get(level) + def get_canonical_benchmark(self, level: int) -> LevelExpBenchmark: + """Retrieves canonical mathematical benchmark for level (1-100).""" + return self._benchmarks.get(level) or self._benchmarks[100 if level >= 100 else 1] + def get_delta_exp(self, level: int) -> int: """Returns experience needed to advance from level to level + 1.""" if level < 1 or level >= 100: @@ -52,15 +56,13 @@ """Returns tiered percentage death penalty ratio for level.""" if level <= 60 or level >= 100: return 0.0 - elif level <= 80: + if level <= 80: return 0.05 - elif level <= 89: + if level <= 89: return 0.10 - elif level <= 98: + if level <= 98: return 0.15 - elif level == 99: - return 0.25 - return 0.0 + return 0.25 if level == 99 else 0.0 def calculate_level_gap_multiplier(self, player_level: int, monster_level: int) -> float: """Calculates experience yield multiplier based on character-monster level gap.""" @@ -78,16 +80,14 @@ if player_id in self._players: return self._players[player_id] level = max(1, min(100, default_level)) - exp_next = self.get_delta_exp(level) - bench = self.get_benchmark(level) - cum_exp = bench.cumulative_exp if bench else 0 + bench = self.get_canonical_benchmark(level) state = PlayerProgressionState( player_id=player_id, level=level, current_exp=0, - exp_to_next_level=exp_next, - cumulative_exp=cum_exp, - lifetime_exp=cum_exp, + exp_to_next_level=0 if level >= 100 else self.get_delta_exp(level), + cumulative_exp=bench.cumulative_exp, + lifetime_exp=bench.cumulative_exp, unspent_talent_points=0, total_talent_points=0, deaths_count=0, @@ -106,14 +106,17 @@ ) -> PlayerProgressionState: """Explicitly sets or updates player state (for test setup and state restoration).""" valid_level = max(1, min(100, level)) + bench = self.get_canonical_benchmark(valid_level) + cum_exp = bench.cumulative_exp + current_exp exp_next = self.get_delta_exp(valid_level) - bench = self.get_benchmark(valid_level) - cum_exp = (bench.cumulative_exp if bench else 0) + current_exp + if valid_level >= 100: + cum_exp = min(bench.cumulative_exp, cum_exp) + exp_next, current_exp = 0, 0 state = PlayerProgressionState( player_id=player_id, level=valid_level, - current_exp=0 if valid_level >= 100 else current_exp, - exp_to_next_level=0 if valid_level >= 100 else exp_next, + current_exp=current_exp, + exp_to_next_level=exp_next, cumulative_exp=cum_exp, lifetime_exp=cum_exp, unspent_talent_points=unspent_talent_points, @@ -148,14 +151,17 @@ **kwargs: Any, ) -> ExpAwardResult: """Calculates and awards monster defeat experience with gap decay and level transitions.""" + if base_exp is not None and base_exp < 0: + base_exp = 0 raw_exp, z_lvl = self._resolve_award_args(arg3, arg4, base_exp, zone_level, monster_level) + raw_exp = max(0, raw_exp) player = self._ensure_player_for_award(player_id, player_level, z_lvl, monster_level) if player.level >= 100: return self._build_level_100_award_result(player, raw_exp, monster_level) gap_mult = self.calculate_level_gap_multiplier(player.level, monster_level) - awarded = int(math.floor(raw_exp * gap_mult)) + awarded = max(0, int(math.floor(raw_exp * gap_mult))) return self._apply_exp_gain(player, awarded, raw_exp, gap_mult, monster_level) def _resolve_award_args( @@ -167,8 +173,7 @@ monster_level: int, ) -> tuple[int, Optional[int]]: """Resolves polymorphic positional and keyword parameters for award_monster_exp.""" - resolved_base = base_exp - resolved_zone = zone_level + resolved_base, resolved_zone = base_exp, zone_level if resolved_base is None and arg4 is not None: resolved_base = arg4 if resolved_zone is None: @@ -178,7 +183,7 @@ if resolved_base is None: bench = self.get_benchmark(monster_level) resolved_base = bench.monster_benchmark_exp if bench else 25 - return resolved_base, resolved_zone + return max(0, resolved_base), resolved_zone def _ensure_player_for_award( self, @@ -189,11 +194,9 @@ ) -> PlayerProgressionState: """Retrieves or initializes player progression state for award operation.""" if player_id not in self._players: - init_level = 1 - if player_level is not None: - init_level = player_level - elif zone_level is not None and zone_level > 1 and zone_level == monster_level: - init_level = zone_level + init_level = player_level if player_level is not None else ( + zone_level if (zone_level is not None and zone_level > 1 and zone_level == monster_level) else 1 + ) return self.get_player_state(player_id, default_level=init_level) return self._players[player_id] @@ -242,18 +245,22 @@ curr_lvl, curr_exp, exp_next, gained = self._compute_level_advancement( prev_level, player.current_exp + exp_awarded ) - unspent = player.unspent_talent_points + gained - total_talents = player.total_talent_points + gained + cum_exp = player.cumulative_exp + exp_awarded + life_exp = player.lifetime_exp + exp_awarded + if curr_lvl >= 100: + max_cum = self.get_canonical_benchmark(100).cumulative_exp + cum_exp, life_exp = min(max_cum, cum_exp), min(max_cum, life_exp) + exp_next, curr_exp = 0, 0 self._players[player.player_id] = PlayerProgressionState( player_id=player.player_id, level=curr_lvl, current_exp=curr_exp, exp_to_next_level=exp_next, - cumulative_exp=player.cumulative_exp + exp_awarded, - lifetime_exp=player.lifetime_exp + exp_awarded, - unspent_talent_points=unspent, - total_talent_points=total_talents, + cumulative_exp=cum_exp, + lifetime_exp=life_exp, + unspent_talent_points=player.unspent_talent_points + gained, + total_talent_points=player.total_talent_points + gained, deaths_count=player.deaths_count, ) @@ -295,9 +302,6 @@ def apply_death_penalty(self, player_id: str) -> DeathPenaltyResult: """Calculates and applies tiered death penalty with zero-floor safe rule.""" player = self.get_player_state(player_id) - if player.level >= 100: - return self._build_death_result(player, 0, 0.0, player.current_exp) - ratio = self.get_death_penalty_ratio(player.level) delta = self.get_delta_exp(player.level) nominal_loss = int(math.floor(delta * ratio))