131 lines
4.2 KiB
TypeScript
131 lines
4.2 KiB
TypeScript
import {
|
|
type ClassId,
|
|
type RaceId,
|
|
} from "../app/characterCatalog";
|
|
import azerothCoreReference from "./generated/azerothCoreReference.json";
|
|
import { EMPTY_ITEM_STATS } from "./itemStats";
|
|
import { deriveCharacterStats } from "./wotlkStats";
|
|
|
|
export const MIN_PLAYER_LEVEL = 1;
|
|
export const MAX_PLAYER_LEVEL = 80;
|
|
|
|
export interface ProgressionSnapshot {
|
|
readonly level: number;
|
|
readonly xp: number;
|
|
}
|
|
|
|
export interface ExperienceAwardResult extends ProgressionSnapshot {
|
|
readonly levelsGained: number;
|
|
readonly xpToNext: number;
|
|
readonly reachedLevelCap: boolean;
|
|
}
|
|
|
|
interface AzerothCoreProgressionReference {
|
|
readonly player: {
|
|
readonly xpForLevel: readonly (number | null)[];
|
|
};
|
|
}
|
|
|
|
const AZEROTHCORE_XP = (
|
|
azerothCoreReference as AzerothCoreProgressionReference
|
|
).player.xpForLevel;
|
|
|
|
export function clampPlayerLevel(level: number): number {
|
|
if (!Number.isFinite(level)) return MIN_PLAYER_LEVEL;
|
|
return Math.max(MIN_PLAYER_LEVEL, Math.min(MAX_PLAYER_LEVEL, Math.trunc(level)));
|
|
}
|
|
|
|
export function legacyHealerManXpRequiredForNextLevel(level: number): number {
|
|
const safeLevel = clampPlayerLevel(level);
|
|
if (safeLevel >= MAX_PLAYER_LEVEL) return 0;
|
|
return 100 + (safeLevel - 1) * 50;
|
|
}
|
|
|
|
/** Exact AzerothCore `player_xp_for_level` threshold for the pinned snapshot. */
|
|
export function xpRequiredForNextLevel(level: number): number {
|
|
const safeLevel = clampPlayerLevel(level);
|
|
if (safeLevel >= MAX_PLAYER_LEVEL) return 0;
|
|
const threshold = AZEROTHCORE_XP[safeLevel - 1];
|
|
return Number.isFinite(threshold) && Number(threshold) > 0
|
|
? Number(threshold)
|
|
: legacyHealerManXpRequiredForNextLevel(safeLevel);
|
|
}
|
|
|
|
/**
|
|
* Converts the established encounter reward into the authoritative XP scale
|
|
* without changing the fraction of a level earned by that reward.
|
|
*/
|
|
export function healerManExperienceAward(amount: number, level: number): number {
|
|
const safeAmount = Number.isFinite(amount) ? Math.max(0, amount) : 0;
|
|
const legacyThreshold = legacyHealerManXpRequiredForNextLevel(level);
|
|
const serverThreshold = xpRequiredForNextLevel(level);
|
|
if (legacyThreshold <= 0 || serverThreshold <= 0) return 0;
|
|
return Math.max(0, Math.round(safeAmount / legacyThreshold * serverThreshold));
|
|
}
|
|
|
|
export function normalizeProgression(level: number, xp: number): ProgressionSnapshot {
|
|
let nextLevel = clampPlayerLevel(level);
|
|
let nextXp = Number.isFinite(xp) ? Math.max(0, Math.trunc(xp)) : 0;
|
|
|
|
while (nextLevel < MAX_PLAYER_LEVEL) {
|
|
const threshold = xpRequiredForNextLevel(nextLevel);
|
|
if (nextXp < threshold) break;
|
|
nextXp -= threshold;
|
|
nextLevel += 1;
|
|
}
|
|
|
|
if (nextLevel >= MAX_PLAYER_LEVEL) nextXp = 0;
|
|
return { level: nextLevel, xp: nextXp };
|
|
}
|
|
|
|
export function awardExperience(
|
|
progression: ProgressionSnapshot,
|
|
amount: number,
|
|
): ExperienceAwardResult {
|
|
const starting = normalizeProgression(progression.level, progression.xp);
|
|
const award = Number.isFinite(amount) ? Math.max(0, Math.trunc(amount)) : 0;
|
|
const normalized = normalizeProgression(starting.level, starting.xp + award);
|
|
return {
|
|
...normalized,
|
|
levelsGained: normalized.level - starting.level,
|
|
xpToNext: xpRequiredForNextLevel(normalized.level),
|
|
reachedLevelCap: normalized.level >= MAX_PLAYER_LEVEL,
|
|
};
|
|
}
|
|
|
|
export function totalExperienceForLevel(level: number): number {
|
|
const target = clampPlayerLevel(level);
|
|
let total = 0;
|
|
for (let current = MIN_PLAYER_LEVEL; current < target; current += 1) {
|
|
total += xpRequiredForNextLevel(current);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
export function maxHealthForLevel(
|
|
classId: ClassId,
|
|
level: number,
|
|
raceId: RaceId = "human",
|
|
): number {
|
|
return deriveCharacterStats(
|
|
classId,
|
|
clampPlayerLevel(level),
|
|
EMPTY_ITEM_STATS,
|
|
raceId,
|
|
).health;
|
|
}
|
|
|
|
export function playerCombatPower(level: number): number {
|
|
return 12 + (clampPlayerLevel(level) - 1) * 3;
|
|
}
|
|
|
|
export function scaledCombatAmount(baseCoefficient: number, level: number): number {
|
|
return Math.max(0, Math.round(baseCoefficient * playerCombatPower(level)));
|
|
}
|
|
|
|
export function levelProgressPercent(level: number, xp: number): number {
|
|
const threshold = xpRequiredForNextLevel(level);
|
|
if (threshold <= 0) return 100;
|
|
return Math.max(0, Math.min(100, (Math.max(0, xp) / threshold) * 100));
|
|
}
|