Files
erika/lib/leaderboard/level-progress.ts
T
2026-08-06 22:46:47 +07:00

70 lines
2.0 KiB
TypeScript

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,
};
}