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
This commit is contained in:
2026-03-31 13:19:54 -07:00
committed by Naomi Carrigan
parent 7d1126e8ad
commit 19f5f5e54f
3 changed files with 47 additions and 35 deletions
+7 -35
View File
@@ -12,25 +12,27 @@ import { useState, type JSX } from "react";
import { prestige } from "../../api/client.js"; import { prestige } from "../../api/client.js";
import { useGame } from "../../context/gameContext.js"; import { useGame } from "../../context/gameContext.js";
import { import {
PRESTIGE_UPGRADES,
PRESTIGE_UPGRADE_CATEGORY_LABELS, PRESTIGE_UPGRADE_CATEGORY_LABELS,
PRESTIGE_UPGRADES,
} from "../../data/prestigeUpgrades.js"; } from "../../data/prestigeUpgrades.js";
import {
computeProjectedRunestones,
} from "../../engine/tick.js";
import { cdnImage } from "../../utils/cdn.js"; import { cdnImage } from "../../utils/cdn.js";
import { sendNotification } from "../../utils/notification.js"; import { sendNotification } from "../../utils/notification.js";
import { playSound } from "../../utils/sound.js"; import { playSound } from "../../utils/sound.js";
import type { PrestigeUpgradeCategory } from "@elysium/types"; import type { PrestigeUpgradeCategory } from "@elysium/types";
const baseThreshold = 1_000_000; const baseThreshold = 1_000_000;
const thresholdScale = 5;
const runestonesPerLevel = 10;
/** /**
* Calculates the prestige threshold for a given prestige count. * Calculates the prestige threshold for a given prestige count.
* Mirrors the server formula: BASE * (count + 1)^2.
* @param prestigeCount - The current prestige count. * @param prestigeCount - The current prestige count.
* @returns The required gold to prestige. * @returns The required gold to prestige.
*/ */
const calculateThreshold = (prestigeCount: number): number => { 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); 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<string>,
): 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<PrestigeUpgradeCategory> = [ const categoryOrder: Array<PrestigeUpgradeCategory> = [
"income", "income",
"click", "click",
@@ -114,11 +90,7 @@ const PrestigePanel = (): JSX.Element => {
const { autoAdventurer, prestige: prestigeData, player } = state; const { autoAdventurer, prestige: prestigeData, player } = state;
const threshold = calculateThreshold(prestigeData.count); const threshold = calculateThreshold(prestigeData.count);
const isEligible = player.totalGoldEarned >= threshold; const isEligible = player.totalGoldEarned >= threshold;
const runestonePreview = calculateRunestonePreview( const runestonePreview = computeProjectedRunestones(state);
player.totalGoldEarned,
prestigeData.count,
prestigeData.purchasedUpgradeIds,
);
const nextMultiplier = calculateProductionMultiplier(prestigeData.count + 1); const nextMultiplier = calculateProductionMultiplier(prestigeData.count + 1);
async function handlePrestige(): Promise<void> { async function handlePrestige(): Promise<void> {
@@ -15,6 +15,7 @@ import {
computeEssencePerSecond, computeEssencePerSecond,
computeGoldPerSecond, computeGoldPerSecond,
computePartyCombatPower, computePartyCombatPower,
computeProjectedRunestones,
} from "../../engine/tick.js"; } from "../../engine/tick.js";
import type { Resource } from "@elysium/types"; import type { Resource } from "@elysium/types";
@@ -89,10 +90,12 @@ const ResourceBar = ({
let partyCombatPower = 0; let partyCombatPower = 0;
let goldPerSecond = 0; let goldPerSecond = 0;
let essencePerSecond = 0; let essencePerSecond = 0;
let projectedRunestones = 0;
if (state !== null) { if (state !== null) {
partyCombatPower = computePartyCombatPower(state); partyCombatPower = computePartyCombatPower(state);
goldPerSecond = computeGoldPerSecond(state); goldPerSecond = computeGoldPerSecond(state);
essencePerSecond = computeEssencePerSecond(state); essencePerSecond = computeEssencePerSecond(state);
projectedRunestones = computeProjectedRunestones(state);
} }
let avatarUrl: string | null = null; let avatarUrl: string | null = null;
@@ -234,6 +237,13 @@ const ResourceBar = ({
</span> </span>
<span className="resource-label">{"Runestones"}</span> <span className="resource-label">{"Runestones"}</span>
</div> </div>
<div className="resource">
<span className="resource-icon">{"⭐"}</span>
<span className="resource-value">
{`+${formatNumber(projectedRunestones)}`}
</span>
<span className="resource-label">{"On Prestige"}</span>
</div>
<div className="resource"> <div className="resource">
<span className="resource-icon">{"⚔️"}</span> <span className="resource-icon">{"⚔️"}</span>
<span className="resource-value"> <span className="resource-value">
+30
View File
@@ -447,6 +447,36 @@ export const computePartyCombatPower = (state: GameState): number => {
* companionCombatMult; * 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. * Pure function — applies one game tick to the state.
* DeltaSeconds: time elapsed since last tick. * DeltaSeconds: time elapsed since last tick.