core/player_progression_service library

Classes

LevelDefinition
One rung of the XP curve. level is 1-based; the LAST entry in a curve must have xpToNext == 0 — that's what marks it the max level (nothing further to grind toward). Every other entry needs xpToNext > 0, and the curve must be non-decreasing (a later level never costs less XP to clear than an earlier one) — see validateLevelCurve.
LevelUpEvent
Fired once per level actually crossed by a PlayerProgressionService.grantXp call — a single call that jumps several levels at once fires one of these per level, in ascending order.
PlayerProgressionService
Single source of truth for one player's XP/level — persists PlayerProgressionSnapshot.totalXpEarned (never level/xpIntoLevel directly, both are always re-derived from it and the curve) plus a bounded idempotency ledger, same pattern as EconomyWallet/ RewardTransactionPipeline (FEAT-31/FEAT-42): a corrupt save resets to a fresh, never-exploitable state instead of throwing or trusting a partially-decoded value.
PlayerProgressionSnapshot
Reactive snapshot of one player's progression — level/xpIntoLevel/ xpToNextLevel are always derived fresh from totalXpEarned against the curve (never persisted directly), so they can never drift out of sync with it.

Functions

levelForTotalXp(List<LevelDefinition> curve, int totalXp) → int
Level reached at totalXp against curve (already validated, sorted by level ascending). Caps at the curve's last (max) level regardless of how far totalXp exceeds it.
validateLevelCurve(List<LevelDefinition> levels) → SdkFailure<void>?
Checks a level curve's structural invariants before it's ever used to grant XP — a broken curve is rejected loudly at construction time (PlayerProgressionService's constructor throws) rather than producing silently-wrong level math later.
xpIntoLevelForTotalXp(List<LevelDefinition> curve, int totalXp) → int
XP progress within the current level at totalXp — always 0 once the max level is reached.