fix: apply cbrt and cap to runestone formula to prevent AFK windfalls
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m3s
CI / Lint, Build & Test (pull_request) Successful in 1m10s

This commit is contained in:
2026-03-24 20:01:22 -07:00
committed by Naomi Carrigan
parent 9926e7f639
commit 959b86fa8b
2 changed files with 27 additions and 10 deletions
+15 -4
View File
@@ -20,6 +20,13 @@ const runestonesPerPrestigeLevel = 10;
const milestoneInterval = 5; const milestoneInterval = 5;
const milestoneRunestonesPerInterval = 25; const milestoneRunestonesPerInterval = 25;
/*
* Hard cap on the base runestone yield (before multipliers) to prevent
* extreme AFK accumulation from producing game-breaking runestone counts.
* With all upgrades (5.625× max) this caps out at ~281 per prestige.
*/
const maxBaseRunestones = 50;
/** /**
* Calculates the gold threshold required for the next prestige. * Calculates the gold threshold required for the next prestige.
* Formula: BASE * SCALE_FACTOR^prestigeCount — each prestige makes the next threshold harder. * Formula: BASE * SCALE_FACTOR^prestigeCount — each prestige makes the next threshold harder.
@@ -107,7 +114,9 @@ interface RunestoneParameters {
/** /**
* Calculates how many runestones the player earns from a prestige. * Calculates how many runestones the player earns from a prestige.
* Formula: floor(sqrt(totalGoldEarned / threshold)) * RUNESTONES_PER_PRESTIGE_LEVEL * runestoneMultiplier. * Formula: min(floor(cbrt(totalGoldEarned / threshold)) * RUNESTONES_PER_PRESTIGE_LEVEL, MAX_BASE) * multipliers.
* Uses cube root for stronger diminishing returns than sqrt, and caps the base before multipliers
* to prevent extended AFK sessions from producing runestone windfalls.
* @param parameters - The parameters for the runestone calculation. * @param parameters - The parameters for the runestone calculation.
* @param parameters.totalGoldEarned - The total gold earned in the current run. * @param parameters.totalGoldEarned - The total gold earned in the current run.
* @param parameters.prestigeCount - The current prestige count. * @param parameters.prestigeCount - The current prestige count.
@@ -123,9 +132,11 @@ const calculateRunestones = (parameters: RunestoneParameters): number => {
echoRunestoneMultiplier = 1, echoRunestoneMultiplier = 1,
} = parameters; } = parameters;
const threshold = calculatePrestigeThreshold(prestigeCount); const threshold = calculatePrestigeThreshold(prestigeCount);
const base const base = Math.min(
= Math.floor(Math.sqrt(totalGoldEarned / threshold)) Math.floor(Math.cbrt(totalGoldEarned / threshold))
* runestonesPerPrestigeLevel; * runestonesPerPrestigeLevel,
maxBaseRunestones,
);
const runestoneMult = getCategoryMultiplier( const runestoneMult = getCategoryMultiplier(
purchasedUpgradeIds, purchasedUpgradeIds,
"runestones", "runestones",
+12 -6
View File
@@ -99,21 +99,27 @@ describe("isEligibleForPrestige", () => {
describe("calculateRunestones", () => { describe("calculateRunestones", () => {
it("calculates basic runestones formula", () => { it("calculates basic runestones formula", () => {
// floor(sqrt(4_000_000 / 1_000_000)) × 10 = floor(2) × 10 = 20 // floor(cbrt(4_000_000 / 1_000_000)) × 10 = floor(cbrt(4)) × 10 = 1 × 10 = 10
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [] }); const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [] });
expect(result).toBe(20); expect(result).toBe(10);
}); });
it("applies echo runestone multiplier", () => { it("applies echo runestone multiplier", () => {
// floor(sqrt(4) × 10) = 20; × 2 = 40 // floor(cbrt(4)) × 10 = 10; × 2 = 20
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [], echoRunestoneMultiplier: 2 }); const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: [], echoRunestoneMultiplier: 2 });
expect(result).toBe(40); expect(result).toBe(20);
}); });
it("applies purchased runestone upgrade multiplier", () => { it("applies purchased runestone upgrade multiplier", () => {
// With "runestones_1" purchased (multiplier 1.25): floor(20 × 1.25) = 25 // With "runestone_gain_1" purchased (multiplier 1.25): floor(10 × 1.25) = 12
const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: ["runestone_gain_1"] }); const result = calculateRunestones({ totalGoldEarned: 4_000_000, prestigeCount: 0, purchasedUpgradeIds: ["runestone_gain_1"] });
expect(result).toBeGreaterThan(20); expect(result).toBe(12);
});
it("caps base runestones before multipliers", () => {
// cbrt(300_000_000 / 1_000_000) = cbrt(300) ≈ 6.67 → floor = 6 → 6 × 10 = 60, capped at 50
const result = calculateRunestones({ totalGoldEarned: 300_000_000, prestigeCount: 0, purchasedUpgradeIds: [] });
expect(result).toBe(50);
}); });
}); });