export const MAX_LEVEL = 200; export type LevelProgress = { level: number; totalXp: string; xpIntoLevel: string; xpRequired: string; xpToNextLevel: string; percentage: number; isMaxLevel: boolean; }; function asNonNegativeBigInt(value: string) { try { const parsed = BigInt(value); return parsed > BigInt(0) ? parsed : BigInt(0); } catch { return BigInt(0); } } function normalizeLevel(level: number) { if (!Number.isFinite(level)) return 0; return Math.min(MAX_LEVEL, Math.max(0, Math.trunc(level))); } /** XP needed to advance from level n to n + 1. Levels 200+ use the level-199 cap. */ export function xpRequiredForLevel(level: number) { const n = BigInt(Math.min(MAX_LEVEL - 1, normalizeLevel(level))); return BigInt(500) + (BigInt(100) * n) + (BigInt(25) * n * (n + BigInt(1))) / BigInt(2); } /** Cumulative XP needed to reach a level from level 0. */ export function totalXpForLevel(level: number) { const targetLevel = normalizeLevel(level); let total = BigInt(0); for (let currentLevel = 0; currentLevel < targetLevel; currentLevel += 1) { total += xpRequiredForLevel(currentLevel); } return total; } export function buildLevelProgress(level: number, rawTotalXp: string): LevelProgress { const normalizedLevel = normalizeLevel(level); const totalXp = asNonNegativeBigInt(rawTotalXp); const required = xpRequiredForLevel(normalizedLevel); const levelStart = totalXpForLevel(normalizedLevel); const unboundedProgress = totalXp > levelStart ? totalXp - levelStart : BigInt(0); const xpIntoLevel = unboundedProgress > required ? required : unboundedProgress; const basisPoints = required === BigInt(0) ? BigInt(0) : (xpIntoLevel * BigInt(10_000)) / required; return { level: normalizedLevel, totalXp: totalXp.toString(), xpIntoLevel: xpIntoLevel.toString(), xpRequired: required.toString(), xpToNextLevel: (required - xpIntoLevel).toString(), percentage: Number(basisPoints) / 100, isMaxLevel: normalizedLevel >= MAX_LEVEL, }; }