From 959b86fa8b55c1ef717069648ce8360561b7205f Mon Sep 17 00:00:00 2001 From: Hikari Date: Tue, 24 Mar 2026 20:01:22 -0700 Subject: [PATCH] fix: apply cbrt and cap to runestone formula to prevent AFK windfalls --- apps/api/src/services/prestige.ts | 19 +++++++++++++++---- apps/api/test/services/prestige.spec.ts | 18 ++++++++++++------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/apps/api/src/services/prestige.ts b/apps/api/src/services/prestige.ts index 27cd75a..c9462cf 100644 --- a/apps/api/src/services/prestige.ts +++ b/apps/api/src/services/prestige.ts @@ -20,6 +20,13 @@ const runestonesPerPrestigeLevel = 10; const milestoneInterval = 5; 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. * 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. - * 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.totalGoldEarned - The total gold earned in the current run. * @param parameters.prestigeCount - The current prestige count. @@ -123,9 +132,11 @@ const calculateRunestones = (parameters: RunestoneParameters): number => { echoRunestoneMultiplier = 1, } = parameters; const threshold = calculatePrestigeThreshold(prestigeCount); - const base - = Math.floor(Math.sqrt(totalGoldEarned / threshold)) - * runestonesPerPrestigeLevel; + const base = Math.min( + Math.floor(Math.cbrt(totalGoldEarned / threshold)) + * runestonesPerPrestigeLevel, + maxBaseRunestones, + ); const runestoneMult = getCategoryMultiplier( purchasedUpgradeIds, "runestones", diff --git a/apps/api/test/services/prestige.spec.ts b/apps/api/test/services/prestige.spec.ts index 40657a2..4b98278 100644 --- a/apps/api/test/services/prestige.spec.ts +++ b/apps/api/test/services/prestige.spec.ts @@ -99,21 +99,27 @@ describe("isEligibleForPrestige", () => { describe("calculateRunestones", () => { 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: [] }); - expect(result).toBe(20); + expect(result).toBe(10); }); 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 }); - expect(result).toBe(40); + expect(result).toBe(20); }); 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"] }); - 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); }); });