Files
healer-man/src/game/progression.ts
T
2026-08-14 15:56:39 -04:00

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