Files
Genarrative/server-node/src/modules/progression/playerProgressionService.ts
高物 8a7bd90458
Some checks failed
CI / verify (push) Has been cancelled
1
2026-04-20 11:30:19 +08:00

193 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getLevelBenchmark, MAX_PLAYER_LEVEL } from './levelBenchmarks.js';
type JsonRecord = Record<string, unknown>;
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('')}`;
}