class_name Progression extends RefCounted ## Levels, experience and what a level is worth. ## ## Pure functions over integers, so the curve can be tuned and tested without a ## server, a character, or a running game. The server is the only thing that ## ever calls the mutating side of this; a client is told its level and takes ## the server's word for it. const MAX_LEVEL := 15 const START_LEVEL := 1 ## Hit points added per level gained. Level 1 is SimConfig.PLAYER_MAX_HP, so a ## capped character has PLAYER_MAX_HP + 14 * this. const HP_PER_LEVEL := 10 ## Experience for the first level-up. The curve is tuned so one full clear of a ## depth-1 dungeon lands a little past this -- the first run should end with a ## level and some change, not exactly on the line. const BASE_XP := 200 ## How sharply the requirement grows. 1.0 would be linear; this makes level 15 a ## long-term goal without making level 2 feel far away. const XP_CURVE := 1.35 ## Experience awarded per kill, by what died. const XP_DRIFTER := 10 const XP_TURRET := 12 const XP_STALKER := 8 const XP_BOSS := 200 ## Experience needed to go from [param level] to the next one. Zero at the cap, ## which is what makes "already maxed" a total-ordering question rather than a ## special case at every call site. static func xp_to_next(level: int) -> int: if level >= MAX_LEVEL: return 0 return int(round(float(BASE_XP) * pow(float(level), XP_CURVE))) ## Total experience to reach [param level] from level 1. static func total_xp_for_level(level: int) -> int: var total := 0 for l in range(START_LEVEL, mini(level, MAX_LEVEL)): total += xp_to_next(l) return total ## The level a given lifetime experience total corresponds to. static func level_for_xp(total_xp: int) -> int: var level := START_LEVEL var spent := 0 while level < MAX_LEVEL: var need := xp_to_next(level) if total_xp - spent < need: break spent += need level += 1 return level ## Progress through the current level, 0..1. Returns 1.0 at the cap so a ## progress bar reads as full rather than empty. static func level_progress(total_xp: int) -> float: var level := level_for_xp(total_xp) if level >= MAX_LEVEL: return 1.0 var into := total_xp - total_xp_for_level(level) var need := xp_to_next(level) return clampf(float(into) / float(maxi(need, 1)), 0.0, 1.0) static func max_hp_for_level(level: int) -> int: var l := clampi(level, START_LEVEL, MAX_LEVEL) return SimConfig.PLAYER_MAX_HP + (l - START_LEVEL) * HP_PER_LEVEL ## Experience for killing an enemy, by content id. Unknown ids award nothing ## rather than a default, so a new enemy that nobody scored is obvious in play ## instead of quietly paying out. static func xp_for_enemy(id: StringName) -> int: match id: Content.ENEMY_DRIFTER: return XP_DRIFTER Content.ENEMY_TURRET: return XP_TURRET Content.ENEMY_STALKER: return XP_STALKER return 0 static func xp_for_boss(_id: StringName) -> int: return XP_BOSS