/** * @file Prestige eligibility checks, runestone calculations, and post-prestige state builder. * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ /* eslint-disable max-lines-per-function -- buildPostPrestigeState requires constructing a large composite state object */ import { initialGameState } from "../data/initialState.js"; import { defaultPrestigeUpgrades } from "../data/prestigeUpgrades.js"; import type { GameState, PrestigeData, PrestigeUpgradeCategory, } from "@elysium/types"; const basePrestigeGoldThreshold = 1_000_000; const thresholdScaleFactor = 5; const runestonesPerPrestigeLevel = 10; const milestoneInterval = 5; const milestoneRunestonesPerInterval = 25; /** * Calculates the gold threshold required for the next prestige. * Formula: BASE * SCALE_FACTOR^prestigeCount — each prestige makes the next threshold harder. * @param prestigeCount - The current number of prestiges completed. * @param thresholdMultiplier - An optional echo-upgrade multiplier applied to the threshold. * @returns The gold amount required to prestige. */ const calculatePrestigeThreshold = ( prestigeCount: number, thresholdMultiplier = 1, ): number => { return ( basePrestigeGoldThreshold * Math.pow(thresholdScaleFactor, prestigeCount) * thresholdMultiplier ); }; /** * Returns true if the player has earned enough gold to prestige. * @param state - The current game state. * @returns Whether the player is eligible for a prestige reset. */ const isEligibleForPrestige = (state: GameState): boolean => { const thresholdMultiplier = state.transcendence?.echoPrestigeThresholdMultiplier ?? 1; return ( state.player.totalGoldEarned >= calculatePrestigeThreshold(state.prestige.count, thresholdMultiplier) ); }; const getCategoryMultiplier = ( purchasedUpgradeIds: Array, category: PrestigeUpgradeCategory, ): number => { return defaultPrestigeUpgrades.filter((upgrade) => { const matchesCategory = upgrade.category === category; const isPurchased = purchasedUpgradeIds.includes(upgrade.id); return matchesCategory && isPurchased; }).reduce((mult, upgrade) => { return mult * upgrade.multiplier; }, 1); }; /** * Computes all four runestone multipliers from the purchased upgrade IDs. * @param purchasedUpgradeIds - The array of purchased prestige upgrade IDs. * @returns An object containing all four runestone multiplier values. */ const computeRunestoneMultipliers = ( purchasedUpgradeIds: Array, ): { runestonesIncomeMultiplier: number; runestonesClickMultiplier: number; runestonesEssenceMultiplier: number; runestonesCrystalMultiplier: number; } => { return { runestonesClickMultiplier: getCategoryMultiplier( purchasedUpgradeIds, "click", ), runestonesCrystalMultiplier: getCategoryMultiplier( purchasedUpgradeIds, "crystals", ), runestonesEssenceMultiplier: getCategoryMultiplier( purchasedUpgradeIds, "essence", ), runestonesIncomeMultiplier: getCategoryMultiplier( purchasedUpgradeIds, "income", ), }; }; interface RunestoneParameters { totalGoldEarned: number; prestigeCount: number; purchasedUpgradeIds: Array; echoRunestoneMultiplier?: number; } /** * Calculates how many runestones the player earns from a prestige. * Formula: floor(sqrt(totalGoldEarned / threshold)) * RUNESTONES_PER_PRESTIGE_LEVEL * runestoneMultiplier. * @param parameters - The parameters for the runestone calculation. * @param parameters.totalGoldEarned - The total gold earned in the current run. * @param parameters.prestigeCount - The current prestige count. * @param parameters.purchasedUpgradeIds - The purchased prestige upgrade IDs. * @param parameters.echoRunestoneMultiplier - An optional echo-upgrade multiplier. * @returns The number of runestones earned. */ const calculateRunestones = (parameters: RunestoneParameters): number => { const { totalGoldEarned, prestigeCount, purchasedUpgradeIds, echoRunestoneMultiplier = 1, } = parameters; const threshold = calculatePrestigeThreshold(prestigeCount); const base = Math.floor(Math.sqrt(totalGoldEarned / threshold)) * runestonesPerPrestigeLevel; const runestoneMult = getCategoryMultiplier( purchasedUpgradeIds, "runestones", ); return Math.floor(base * runestoneMult * echoRunestoneMultiplier); }; /** * Calculates the new prestige production multiplier. * Formula: 1.15^prestigeCount — exponential scaling per prestige. * @param prestigeCount - The new prestige count. * @returns The production multiplier for the new prestige level. */ const calculateProductionMultiplier = ( prestigeCount: number, ): number => { return Math.pow(1.15, prestigeCount); }; /** * Returns the milestone runestone bonus for the given prestige count. * Every MILESTONE_INTERVAL prestiges awards milestone_number * MILESTONE_RUNESTONES_PER_INTERVAL stones. * @param prestigeCount - The prestige count after the current prestige. * @returns The milestone runestone bonus, or 0 if not a milestone prestige. */ const calculateMilestoneBonus = (prestigeCount: number): number => { if (prestigeCount % milestoneInterval !== 0) { return 0; } const milestoneNumber = prestigeCount / milestoneInterval; return milestoneNumber * milestoneRunestonesPerInterval; }; /** * Generates the reset game state after a prestige. * Carries over prestige data and runestones; resets everything else. * @param currentState - The game state at the time of the prestige. * @param characterName - The player's character name to carry forward. * @returns The new game state, prestige data, and runestone counts. */ const buildPostPrestigeState = ( currentState: GameState, characterName: string, ): { prestigeState: GameState; prestigeData: PrestigeData; runestonesEarned: number; milestoneRunestones: number; } => { const { autoPrestigeEnabled, count: currentPrestigeCount, purchasedUpgradeIds, runestones: currentRunestones, } = currentState.prestige; const echoRunestoneMultiplier = currentState.transcendence?.echoPrestigeRunestoneMultiplier ?? 1; const runestonesEarned = calculateRunestones({ echoRunestoneMultiplier: echoRunestoneMultiplier, prestigeCount: currentPrestigeCount, purchasedUpgradeIds: purchasedUpgradeIds, totalGoldEarned: currentState.player.totalGoldEarned, }); const updatedPrestigeCount = currentPrestigeCount + 1; const milestoneRunestones = calculateMilestoneBonus(updatedPrestigeCount); const prestigeData: PrestigeData = { count: updatedPrestigeCount, lastPrestigedAt: Date.now(), productionMultiplier: calculateProductionMultiplier(updatedPrestigeCount), purchasedUpgradeIds: purchasedUpgradeIds, runestones: currentRunestones + runestonesEarned + milestoneRunestones, ...computeRunestoneMultipliers(purchasedUpgradeIds), ...autoPrestigeEnabled === undefined ? {} : { autoPrestigeEnabled }, }; const freshState = initialGameState(currentState.player, characterName); // Compute current-run contributions to accumulate into lifetime totals const runBossesDefeated = currentState.bosses.filter((boss) => { return boss.status === "defeated"; }).length; const runQuestsCompleted = currentState.quests.filter((quest) => { return quest.status === "completed"; }).length; let runAdventurersRecruited = 0; for (const adventurer of currentState.adventurers) { runAdventurersRecruited = runAdventurersRecruited + adventurer.count; } const runAchievementsUnlocked = currentState.achievements.filter( (achievement) => { return achievement.unlockedAt !== null; }, ).length; const prestigeState: GameState = { ...freshState, // Achievements are permanent — earned achievements survive all prestiges achievements: currentState.achievements, lastTickAt: Date.now(), /* * Fold current-run totals into lifetime stats so the GameState reflects * the true all-time values immediately after prestige. */ player: { ...freshState.player, lifetimeAchievementsUnlocked: freshState.player.lifetimeAchievementsUnlocked + runAchievementsUnlocked, lifetimeAdventurersRecruited: freshState.player.lifetimeAdventurersRecruited + runAdventurersRecruited, lifetimeBossesDefeated: freshState.player.lifetimeBossesDefeated + runBossesDefeated, lifetimeClicks: freshState.player.lifetimeClicks + currentState.player.totalClicks, lifetimeGoldEarned: freshState.player.lifetimeGoldEarned + currentState.player.totalGoldEarned, lifetimeQuestsCompleted: freshState.player.lifetimeQuestsCompleted + runQuestsCompleted, }, prestige: prestigeData, // Codex lore persists across prestiges — players keep their discovered entries ...currentState.codex === undefined ? {} : { codex: currentState.codex }, // Transcendence data is permanent — never wiped by prestige ...currentState.transcendence === undefined ? {} : { transcendence: currentState.transcendence }, // Apotheosis data is eternal — never wiped by prestige ...currentState.apotheosis === undefined ? {} : { apotheosis: currentState.apotheosis }, // Story chapter progress is permanent — survives all resets ...currentState.story === undefined ? {} : { story: currentState.story }, }; return { milestoneRunestones, prestigeData, prestigeState, runestonesEarned, }; }; export { buildPostPrestigeState, calculateMilestoneBonus, calculatePrestigeThreshold, calculateProductionMultiplier, calculateRunestones, computeRunestoneMultipliers, isEligibleForPrestige, };