70 lines
2.0 KiB
TypeScript
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,
|
|
};
|
|
}
|