/** * @file Prestige panel component for ascending and purchasing runestone upgrades. * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ /* eslint-disable max-lines-per-function -- Complex component with many render paths */ /* eslint-disable complexity -- Many conditional render paths */ /* eslint-disable max-lines -- Large panel with prestige and shop tabs */ /* eslint-disable max-statements -- Prestige panel manages many local state variables */ import { useState, type JSX } from "react"; import { prestige } from "../../api/client.js"; import { useGame } from "../../context/gameContext.js"; import { PRESTIGE_UPGRADES, PRESTIGE_UPGRADE_CATEGORY_LABELS, } from "../../data/prestigeUpgrades.js"; import { sendNotification } from "../../utils/notification.js"; import { playSound } from "../../utils/sound.js"; import type { PrestigeUpgradeCategory } from "@elysium/types"; const baseThreshold = 1_000_000; const thresholdScale = 5; const runestonesPerLevel = 10; /** * Calculates the prestige threshold for a given prestige count. * @param prestigeCount - The current prestige count. * @returns The required gold to prestige. */ const calculateThreshold = (prestigeCount: number): number => { return baseThreshold * Math.pow(thresholdScale, prestigeCount); }; /** * Calculates the production multiplier for a given prestige count. * @param prestigeCount - The number of times the player has prestiged. * @returns The compounding multiplier applied to all income sources. */ const calculateProductionMultiplier = (prestigeCount: number): number => { return Math.pow(1.15, prestigeCount); }; /** * Calculates the runestone preview for a prestige. * @param totalGoldEarned - Total gold earned this run. * @param prestigeCount - The current prestige count. * @param purchasedUpgradeIds - IDs of purchased prestige upgrades. * @returns The predicted runestone reward. */ const calculateRunestonePreview = ( totalGoldEarned: number, prestigeCount: number, purchasedUpgradeIds: Array, ): number => { const threshold = calculateThreshold(prestigeCount); const base = Math.floor(Math.sqrt(totalGoldEarned / threshold)) * runestonesPerLevel; const runestoneMult = PRESTIGE_UPGRADES.filter((upgrade) => { return ( upgrade.category === "runestones" && purchasedUpgradeIds.includes(upgrade.id) ); }).reduce((mult, upgrade) => { return mult * upgrade.multiplier; }, 1); return Math.floor(base * runestoneMult); }; const categoryOrder: Array = [ "income", "click", "essence", "crystals", "runestones", "utility", ]; /** * Renders the prestige panel with ascension and runestone shop tabs. * @returns The JSX element. */ const PrestigePanel = (): JSX.Element => { const { state, reload, formatNumber, buyPrestigeUpgrade, enableNotifications, enableSounds, toggleAutoPrestige, } = useGame(); const [ isPending, setIsPending ] = useState(false); const [ result, setResult ] = useState<{ runestones: number; count: number; milestoneRunestones: number; } | null>(null); const [ prestigeError, setPrestigeError ] = useState(null); const [ buyingId, setBuyingId ] = useState(null); const [ activeTab, setActiveTab ] = useState<"prestige" | "shop">("prestige"); if (state === null) { return (

{"Loading..."}

); } const { prestige: prestigeData, player } = state; const threshold = calculateThreshold(prestigeData.count); const isEligible = player.totalGoldEarned >= threshold; const runestonePreview = calculateRunestonePreview( player.totalGoldEarned, prestigeData.count, prestigeData.purchasedUpgradeIds, ); const nextMultiplier = calculateProductionMultiplier(prestigeData.count + 1); async function handlePrestige(): Promise { setIsPending(true); setPrestigeError(null); try { const data = await prestige({}); setResult({ count: data.newPrestigeCount, milestoneRunestones: data.milestoneRunestones, runestones: data.runestones, }); if (enableSounds) { playSound("prestige"); } if (enableNotifications) { sendNotification( "⭐ Prestige!", `You've reached prestige level ${data.newPrestigeCount.toString()}!`, ); } await reload(); } catch (error_: unknown) { setPrestigeError( error_ instanceof Error ? error_.message : "Prestige failed", ); } finally { setIsPending(false); } } async function handleBuyUpgrade(upgradeId: string): Promise { setBuyingId(upgradeId); try { await buyPrestigeUpgrade(upgradeId); } finally { setBuyingId(null); } } const upgradesByCategory = categoryOrder.map((categoryId) => { const label = PRESTIGE_UPGRADE_CATEGORY_LABELS[categoryId] ?? categoryId; const upgrades = PRESTIGE_UPGRADES.filter((upgrade) => { return upgrade.category === categoryId; }); return { categoryId, label, upgrades }; }); function handlePrestigeClick(): void { void handlePrestige(); } function handleAutoPrestigeToggle(): void { toggleAutoPrestige(); } function handlePrestigeTabClick(): void { setActiveTab("prestige"); } function handleShopTabClick(): void { setActiveTab("shop"); } const progressRatio = player.totalGoldEarned / threshold; const progressPct = (progressRatio * 100).toFixed(1); return (

{"⭐ Prestige"}

{activeTab === "prestige" && <>

{"Prestige resets your progress but grants "} {"Runestones"} {"— permanent currency used for powerful upgrades."} {" Each prestige multiplies your global production by ×1.15"} {" (compounding each run)."}

{"Total gold this run: "} {formatNumber(player.totalGoldEarned)}

{"Required to prestige: "} {formatNumber(threshold)}

{"Prestige count: "} {prestigeData.count}

{"Current production multiplier: "} {"×"} {prestigeData.productionMultiplier.toFixed(2)}

{"After next prestige: "} {"×"} {nextMultiplier.toFixed(2)}

{"Runestones: "} {formatNumber(prestigeData.runestones)}

{isEligible ?

{"Runestones on prestige: "} {"+"} {formatNumber(runestonePreview)}

: null} {isEligible ? null :

{"Progress: "} {formatNumber(player.totalGoldEarned)} {" / "} {formatNumber(threshold)} {" ("} {progressPct} {"%"} {")"}

}
{isEligible ?

{"You are ready to prestige!"}

{prestigeError === null ? null :

{prestigeError}

} {result === null ? null :

{"Ascended to Prestige "} {result.count} {"! Earned "} {formatNumber(result.runestones)} {" Runestones."} {result.milestoneRunestones > 0 && <> {" 🎉 Milestone bonus: +"} {formatNumber(result.milestoneRunestones)} {" Runestones!"} }

}
:

{"Earn "} {formatNumber(threshold - player.totalGoldEarned)} {" more gold to unlock prestige."}

} } {activeTab === "shop" &&

{"Balance: "} {formatNumber(prestigeData.runestones)} {" Runestones"}

{upgradesByCategory.map(({ categoryId, label, upgrades }) => { return (

{label}

{upgrades.map((upgrade) => { const purchased = prestigeData.purchasedUpgradeIds.includes( upgrade.id, ); const canAfford = prestigeData.runestones >= upgrade.runestonesCost; const isLoading = buyingId === upgrade.id; const isAutoPrestigeToggle = upgrade.id === "auto_prestige" && purchased; const autoPrestigeEnabled = prestigeData.autoPrestigeEnabled ?? false; function handleBuyClick(): void { void handleBuyUpgrade(upgrade.id); } return (

{upgrade.name}

{upgrade.description}

{purchased ? "✅ Purchased" : `🔮 ${formatNumber(upgrade.runestonesCost)} Runestones`}

{isAutoPrestigeToggle ? : null} {purchased ? null : }
); })}
); })}
}
); }; export { PrestigePanel };