import { getLevelBenchmark, MAX_PLAYER_LEVEL } from './levelBenchmarks.js'; type JsonRecord = Record; export type PlayerProgressionGrantSource = 'quest' | 'hostile_npc'; export interface PlayerProgressionState { level: number; currentLevelXp: number; totalXp: number; xpToNextLevel: number; pendingLevelUps?: number; lastGrantedSource?: PlayerProgressionGrantSource | null; } export interface PlayerExperienceGrantResult { state: PlayerProgressionState; grantedXp: number; previousLevel: number; nextLevel: number; levelUps: number; leveledUp: boolean; reachedMaxLevel: boolean; } function isRecord(value: unknown): value is JsonRecord { return typeof value === 'object' && value !== null && !Array.isArray(value); } function clampNonNegativeInteger(value: unknown) { if (typeof value !== 'number' || !Number.isFinite(value)) { return 0; } return Math.max(0, Math.floor(value)); } function clampLevel(value: unknown) { if (typeof value !== 'number' || !Number.isFinite(value)) { return 1; } return Math.min(MAX_PLAYER_LEVEL, Math.max(1, Math.floor(value))); } function normalizeLastGrantedSource(value: unknown) { return value === 'quest' || value === 'hostile_npc' ? value : null; } function resolveLevelFromTotalXp(totalXp: number) { let resolvedLevel = 1; for (let level = 2; level <= MAX_PLAYER_LEVEL; level += 1) { if (totalXp < getLevelBenchmark(level).cumulativeXpRequired) { break; } resolvedLevel = level; } return resolvedLevel; } function buildProgressionStateFromTotalXp( totalXp: number, lastGrantedSource: PlayerProgressionGrantSource | null = null, ): PlayerProgressionState { const normalizedTotalXp = clampNonNegativeInteger(totalXp); const level = resolveLevelFromTotalXp(normalizedTotalXp); const benchmark = getLevelBenchmark(level); if (level >= MAX_PLAYER_LEVEL) { return { level, currentLevelXp: 0, totalXp: normalizedTotalXp, xpToNextLevel: 0, pendingLevelUps: 0, lastGrantedSource, }; } return { level, currentLevelXp: Math.max( 0, normalizedTotalXp - benchmark.cumulativeXpRequired, ), totalXp: normalizedTotalXp, xpToNextLevel: benchmark.xpToNextLevel, pendingLevelUps: 0, lastGrantedSource, }; } export function createInitialPlayerProgressionState(): PlayerProgressionState { return buildProgressionStateFromTotalXp(0); } export function normalizePlayerProgressionState( value: unknown, ): PlayerProgressionState { if (!isRecord(value)) { return createInitialPlayerProgressionState(); } const explicitLevel = clampLevel(value.level); const explicitCurrentLevelXp = clampNonNegativeInteger(value.currentLevelXp); const totalXp = clampNonNegativeInteger(value.totalXp); const hasExplicitProgress = explicitLevel > 1 || explicitCurrentLevelXp > 0; const derivedTotalXp = totalXp > 0 || !hasExplicitProgress ? totalXp : getLevelBenchmark(explicitLevel).cumulativeXpRequired + Math.min( explicitCurrentLevelXp, getLevelBenchmark(explicitLevel).xpToNextLevel, ); return { ...buildProgressionStateFromTotalXp( derivedTotalXp, normalizeLastGrantedSource(value.lastGrantedSource), ), pendingLevelUps: clampNonNegativeInteger(value.pendingLevelUps), }; } export function grantPlayerExperience( value: unknown, amount: number, options: { source: PlayerProgressionGrantSource; }, ): PlayerExperienceGrantResult { const currentState = normalizePlayerProgressionState(value); const grantedXp = clampNonNegativeInteger(amount); if (grantedXp <= 0) { return { state: { ...currentState, pendingLevelUps: 0, }, grantedXp: 0, previousLevel: currentState.level, nextLevel: currentState.level, levelUps: 0, leveledUp: false, reachedMaxLevel: currentState.level >= MAX_PLAYER_LEVEL, }; } const nextState = buildProgressionStateFromTotalXp( currentState.totalXp + grantedXp, options.source, ); const levelUps = Math.max(0, nextState.level - currentState.level); return { state: { ...nextState, pendingLevelUps: 0, }, grantedXp, previousLevel: currentState.level, nextLevel: nextState.level, levelUps, leveledUp: levelUps > 0, reachedMaxLevel: nextState.level >= MAX_PLAYER_LEVEL, }; } export function buildExperienceGrantResultText( result: PlayerExperienceGrantResult, ) { if (result.grantedXp <= 0) { return ''; } const parts = [`经验 +${result.grantedXp}`]; if (result.leveledUp) { parts.push( result.levelUps > 1 ? `连升 ${result.levelUps} 级,达到 Lv.${result.nextLevel}` : `升至 Lv.${result.nextLevel}`, ); } return `${parts.join(',')}。`; }