✨Feature: add town systems, saves, recovery, and level progression
This commit is contained in:
122
src/rules/progression.ts
Normal file
122
src/rules/progression.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user