generated from nhcarrigan/template
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:
@@ -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">
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user