123 lines
3.8 KiB
TypeScript
123 lines
3.8 KiB
TypeScript
import type { ContentPack } from "@/types/content";
|
|
import type { AdventurerState, LevelUpState } from "@/types/state";
|
|
|
|
export const XP_PER_LEVEL = 8;
|
|
export const HP_GAIN_PER_LEVEL = 2;
|
|
export const MAX_ADVENTURER_LEVEL = 10;
|
|
|
|
export type ApplyLevelProgressionOptions = {
|
|
content: ContentPack;
|
|
adventurer: AdventurerState;
|
|
at?: string;
|
|
};
|
|
|
|
export type LevelProgressionResult = {
|
|
adventurer: AdventurerState;
|
|
levelUps: LevelUpState[];
|
|
};
|
|
|
|
export function getXpThresholdForLevel(level: number) {
|
|
if (level <= 1) {
|
|
return 0;
|
|
}
|
|
|
|
return (level - 1) * XP_PER_LEVEL;
|
|
}
|
|
|
|
export function getNextLevelXpThreshold(level: number) {
|
|
return getXpThresholdForLevel(level + 1);
|
|
}
|
|
|
|
export function getLevelForXp(xp: number) {
|
|
if (xp < 0) {
|
|
return 1;
|
|
}
|
|
|
|
return Math.min(MAX_ADVENTURER_LEVEL, Math.floor(xp / XP_PER_LEVEL) + 1);
|
|
}
|
|
|
|
function getUnlockedWeaponManoeuvreIds(content: ContentPack, adventurer: AdventurerState, level: number) {
|
|
const weapon = content.weapons.find((entry) => entry.id === adventurer.weaponId);
|
|
|
|
if (!weapon) {
|
|
throw new Error(`Unknown weapon id: ${adventurer.weaponId}`);
|
|
}
|
|
|
|
return weapon.allowedManoeuvreIds.filter((manoeuvreId) => {
|
|
const manoeuvre = content.manoeuvres.find((entry) => entry.id === manoeuvreId);
|
|
|
|
if (!manoeuvre) {
|
|
throw new Error(`Unknown manoeuvre id: ${manoeuvreId}`);
|
|
}
|
|
|
|
return (manoeuvre.minimumLevel ?? 1) <= level;
|
|
});
|
|
}
|
|
|
|
export function applyLevelProgression(
|
|
options: ApplyLevelProgressionOptions,
|
|
): LevelProgressionResult {
|
|
const nextAdventurer: AdventurerState = {
|
|
...options.adventurer,
|
|
hp: { ...options.adventurer.hp },
|
|
stats: { ...options.adventurer.stats },
|
|
favour: { ...options.adventurer.favour },
|
|
statuses: options.adventurer.statuses.map((status) => ({ ...status })),
|
|
inventory: {
|
|
carried: options.adventurer.inventory.carried.map((entry) => ({ ...entry })),
|
|
equipped: options.adventurer.inventory.equipped.map((entry) => ({ ...entry })),
|
|
stored: options.adventurer.inventory.stored.map((entry) => ({ ...entry })),
|
|
currency: { ...options.adventurer.inventory.currency },
|
|
rationCount: options.adventurer.inventory.rationCount,
|
|
lightSources: options.adventurer.inventory.lightSources.map((entry) => ({ ...entry })),
|
|
},
|
|
progressionFlags: [...options.adventurer.progressionFlags],
|
|
manoeuvreIds: [...options.adventurer.manoeuvreIds],
|
|
};
|
|
const targetLevel = getLevelForXp(nextAdventurer.xp);
|
|
const at = options.at ?? new Date().toISOString();
|
|
const levelUps: LevelUpState[] = [];
|
|
|
|
while (nextAdventurer.level < targetLevel) {
|
|
const previousLevel = nextAdventurer.level;
|
|
const newLevel = previousLevel + 1;
|
|
|
|
nextAdventurer.level = newLevel;
|
|
nextAdventurer.hp.max += HP_GAIN_PER_LEVEL;
|
|
nextAdventurer.hp.current = Math.min(
|
|
nextAdventurer.hp.max,
|
|
nextAdventurer.hp.current + HP_GAIN_PER_LEVEL,
|
|
);
|
|
|
|
const unlockedManoeuvreIds = getUnlockedWeaponManoeuvreIds(
|
|
options.content,
|
|
nextAdventurer,
|
|
newLevel,
|
|
).filter((manoeuvreId) => !nextAdventurer.manoeuvreIds.includes(manoeuvreId));
|
|
|
|
nextAdventurer.manoeuvreIds.push(...unlockedManoeuvreIds);
|
|
|
|
const levelFlag = `level.reached.${newLevel}`;
|
|
if (!nextAdventurer.progressionFlags.includes(levelFlag)) {
|
|
nextAdventurer.progressionFlags.push(levelFlag);
|
|
}
|
|
|
|
levelUps.push({
|
|
previousLevel,
|
|
newLevel,
|
|
at,
|
|
hpGained: HP_GAIN_PER_LEVEL,
|
|
unlockedManoeuvreIds,
|
|
summary:
|
|
unlockedManoeuvreIds.length > 0
|
|
? `Reached level ${newLevel}, gained ${HP_GAIN_PER_LEVEL} max HP, and unlocked ${unlockedManoeuvreIds.length} manoeuvre${unlockedManoeuvreIds.length === 1 ? "" : "s"}.`
|
|
: `Reached level ${newLevel} and gained ${HP_GAIN_PER_LEVEL} max HP.`,
|
|
});
|
|
}
|
|
|
|
return {
|
|
adventurer: nextAdventurer,
|
|
levelUps,
|
|
};
|
|
}
|