From 19f5f5e54f253e175793da7b99d012bbe202cc3a Mon Sep 17 00:00:00 2001 From: Hikari Date: Tue, 31 Mar 2026 13:19:54 -0700 Subject: [PATCH] feat: show projected runestone gain persistently in resource bar Adds computeProjectedRunestones() to the shared tick engine using the correct server-side formula (cbrt, (count+1)^2 threshold). The resource bar now shows a persistent '+N On Prestige' row so players can always see what they would earn. The prestige panel's own preview was also fixed to use the shared helper, replacing a broken local calculation that used sqrt and the wrong threshold formula. Closes #168 --- .../web/src/components/game/prestigePanel.tsx | 42 ++++--------------- apps/web/src/components/ui/resourceBar.tsx | 10 +++++ apps/web/src/engine/tick.ts | 30 +++++++++++++ 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/apps/web/src/components/game/prestigePanel.tsx b/apps/web/src/components/game/prestigePanel.tsx index e12d331..980ca54 100644 --- a/apps/web/src/components/game/prestigePanel.tsx +++ b/apps/web/src/components/game/prestigePanel.tsx @@ -12,25 +12,27 @@ 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, + PRESTIGE_UPGRADES, } from "../../data/prestigeUpgrades.js"; +import { + computeProjectedRunestones, +} from "../../engine/tick.js"; import { cdnImage } from "../../utils/cdn.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. + * Mirrors the server formula: BASE * (count + 1)^2. * @param prestigeCount - The current prestige count. * @returns The required gold to prestige. */ const calculateThreshold = (prestigeCount: number): number => { - return baseThreshold * Math.pow(thresholdScale, prestigeCount); + return baseThreshold * Math.pow(prestigeCount + 1, 2); }; /** @@ -42,32 +44,6 @@ 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", @@ -114,11 +90,7 @@ const PrestigePanel = (): JSX.Element => { const { autoAdventurer, prestige: prestigeData, player } = state; const threshold = calculateThreshold(prestigeData.count); const isEligible = player.totalGoldEarned >= threshold; - const runestonePreview = calculateRunestonePreview( - player.totalGoldEarned, - prestigeData.count, - prestigeData.purchasedUpgradeIds, - ); + const runestonePreview = computeProjectedRunestones(state); const nextMultiplier = calculateProductionMultiplier(prestigeData.count + 1); async function handlePrestige(): Promise { diff --git a/apps/web/src/components/ui/resourceBar.tsx b/apps/web/src/components/ui/resourceBar.tsx index 1743a6f..18ffbf7 100644 --- a/apps/web/src/components/ui/resourceBar.tsx +++ b/apps/web/src/components/ui/resourceBar.tsx @@ -15,6 +15,7 @@ import { computeEssencePerSecond, computeGoldPerSecond, computePartyCombatPower, + computeProjectedRunestones, } from "../../engine/tick.js"; import type { Resource } from "@elysium/types"; @@ -89,10 +90,12 @@ const ResourceBar = ({ let partyCombatPower = 0; let goldPerSecond = 0; let essencePerSecond = 0; + let projectedRunestones = 0; if (state !== null) { partyCombatPower = computePartyCombatPower(state); goldPerSecond = computeGoldPerSecond(state); essencePerSecond = computeEssencePerSecond(state); + projectedRunestones = computeProjectedRunestones(state); } let avatarUrl: string | null = null; @@ -234,6 +237,13 @@ const ResourceBar = ({ {"Runestones"} +
+ {"⭐"} + + {`+${formatNumber(projectedRunestones)}`} + + {"On Prestige"} +
{"⚔️"} diff --git a/apps/web/src/engine/tick.ts b/apps/web/src/engine/tick.ts index ada6501..1b17ec1 100644 --- a/apps/web/src/engine/tick.ts +++ b/apps/web/src/engine/tick.ts @@ -447,6 +447,36 @@ export const computePartyCombatPower = (state: GameState): number => { * companionCombatMult; }; +const basePrestigeThreshold = 1_000_000; +const runestonesPerPrestigeLevelClient = 15; +const maxBaseRunestones = 200; + +/** + * Computes the projected runestone reward if the player were to prestige right now. + * Mirrors the server-side calculateRunestones formula exactly. + * @param state - The current game state. + * @returns The number of runestones the player would earn from a prestige now. + */ +export const computeProjectedRunestones = (state: GameState): number => { + const { count, purchasedUpgradeIds } = state.prestige; + const threshold = basePrestigeThreshold * Math.pow(count + 1, 2); + const base = Math.min( + Math.floor(Math.cbrt(state.player.totalGoldEarned / threshold)) + * runestonesPerPrestigeLevelClient, + maxBaseRunestones, + ); + const gain1Mult = purchasedUpgradeIds.includes("runestone_gain_1") + ? 1.25 + : 1; + const gain2Mult = purchasedUpgradeIds.includes("runestone_gain_2") + ? 1.5 + : 1; + const runestoneMult = gain1Mult * gain2Mult; + /* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- optional chained game state field */ + const echoMult: number = state.transcendence?.echoRunestoneMultiplier ?? 1; + return Math.floor(base * runestoneMult * echoMult); +}; + /** * Pure function — applies one game tick to the state. * DeltaSeconds: time elapsed since last tick.