From 9cff54cfcd3315dcb705349130a9e2409b5cf6ed Mon Sep 17 00:00:00 2001 From: Hikari Date: Tue, 31 Mar 2026 15:05:41 -0700 Subject: [PATCH] balance: smooth prestige income cliff, quadratic milestones, exponential combat scaling (#170, #171) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduce income_10 cost 30k→22.5k and income_11 80k→60k (25% cut each) to ease the late-prestige runestone cliff without collapsing the timeline. Change prestige milestone bonus from linear (n×25) to quadratic (n²×25) so high-prestige milestones feel meaningful (P100 = 10k stones). Replace linear prestige combat multiplier (1 + count×0.1) with exponential (4^count) in both the tick engine and server-side boss route. Without this the final boss (2×10^145 HP) was unreachable by ~112 orders of magnitude; base-4 makes it achievable around P190, consistent with the 6-month target. --- apps/api/src/data/prestigeUpgrades.ts | 4 ++-- apps/api/src/routes/boss.ts | 10 ++++++++-- apps/api/src/services/prestige.ts | 4 ++-- apps/api/test/services/prestige.spec.ts | 8 ++++---- apps/web/src/engine/tick.ts | 12 ++++++++---- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/apps/api/src/data/prestigeUpgrades.ts b/apps/api/src/data/prestigeUpgrades.ts index 1982c60..6d33eb0 100644 --- a/apps/api/src/data/prestigeUpgrades.ts +++ b/apps/api/src/data/prestigeUpgrades.ts @@ -96,7 +96,7 @@ export const defaultPrestigeUpgrades: Array = [ id: "income_10", multiplier: 200, name: "Eternal Rune I", - runestonesCost: 30_000, + runestonesCost: 22_500, }, { category: "income", @@ -105,7 +105,7 @@ export const defaultPrestigeUpgrades: Array = [ id: "income_11", multiplier: 500, name: "Eternal Rune II", - runestonesCost: 80_000, + runestonesCost: 60_000, }, // ── Click Power ─────────────────────────────────────────────────────────── { diff --git a/apps/api/src/routes/boss.ts b/apps/api/src/routes/boss.ts index fe41a29..47a4754 100644 --- a/apps/api/src/routes/boss.ts +++ b/apps/api/src/routes/boss.ts @@ -24,6 +24,13 @@ import { updateChallengeProgress } from "../services/dailyChallenges.js"; import { logger } from "../services/logger.js"; import type { HonoEnvironment } from "../types/hono.js"; +/** + * Exponential base for the prestige combat multiplier: Math.pow(base, prestigeCount). + * Replaces the former linear formula (1 + count * 0.1) to enable late-game zone progression. + * Must be kept in sync with prestigeCombatBase in apps/web/src/engine/tick.ts. + */ +const prestigeCombatBase = 4; + const bossRouter = new Hono(); bossRouter.use("*", authMiddleware); @@ -38,8 +45,7 @@ const calculatePartyStats = ( } } - // eslint-disable-next-line stylistic/no-mixed-operators -- prestige count * factor is clear - const prestigeMultiplier = 1 + state.prestige.count * 0.1; + const prestigeMultiplier = Math.pow(prestigeCombatBase, state.prestige.count); // Apply equipped weapon's combat bonus // eslint-disable-next-line capitalized-comments -- v8 ignore diff --git a/apps/api/src/services/prestige.ts b/apps/api/src/services/prestige.ts index 4eb62ab..28d82d9 100644 --- a/apps/api/src/services/prestige.ts +++ b/apps/api/src/services/prestige.ts @@ -159,7 +159,7 @@ const calculateProductionMultiplier = ( /** * Returns the milestone runestone bonus for the given prestige count. - * Every MILESTONE_INTERVAL prestiges awards milestone_number * MILESTONE_RUNESTONES_PER_INTERVAL stones. + * Every MILESTONE_INTERVAL prestiges awards milestone_number² * MILESTONE_RUNESTONES_PER_INTERVAL stones. * @param prestigeCount - The prestige count after the current prestige. * @returns The milestone runestone bonus, or 0 if not a milestone prestige. */ @@ -168,7 +168,7 @@ const calculateMilestoneBonus = (prestigeCount: number): number => { return 0; } const milestoneNumber = prestigeCount / milestoneInterval; - return milestoneNumber * milestoneRunestonesPerInterval; + return milestoneNumber * milestoneNumber * milestoneRunestonesPerInterval; }; /** diff --git a/apps/api/test/services/prestige.spec.ts b/apps/api/test/services/prestige.spec.ts index 977cc65..6b7e689 100644 --- a/apps/api/test/services/prestige.spec.ts +++ b/apps/api/test/services/prestige.spec.ts @@ -151,12 +151,12 @@ describe("calculateMilestoneBonus", () => { expect(calculateMilestoneBonus(5)).toBe(25); }); - it("returns 50 at prestige 10", () => { - expect(calculateMilestoneBonus(10)).toBe(50); + it("returns 100 at prestige 10", () => { + expect(calculateMilestoneBonus(10)).toBe(100); }); - it("returns 75 at prestige 15", () => { - expect(calculateMilestoneBonus(15)).toBe(75); + it("returns 225 at prestige 15", () => { + expect(calculateMilestoneBonus(15)).toBe(225); }); }); diff --git a/apps/web/src/engine/tick.ts b/apps/web/src/engine/tick.ts index 1b17ec1..5733027 100644 --- a/apps/web/src/engine/tick.ts +++ b/apps/web/src/engine/tick.ts @@ -84,6 +84,12 @@ const checkAchievements = (state: GameState): Array => { }); }; +/** + * Exponential base for the prestige combat multiplier: Math.pow(base, prestigeCount). + * Replaces the former linear formula (1 + count * 0.1) to enable late-game zone progression. + */ +export const PRESTIGE_COMBAT_BASE = 4; + /** * Maximum value any resource can accumulate to. Beyond this JS floats lose all useful precision. */ @@ -296,8 +302,7 @@ export const computeEffectiveAdventurerStats = ( const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1; const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1; - // eslint-disable-next-line stylistic/no-mixed-operators -- prestige count * factor is clear - const prestigeCombatMultiplier = 1 + state.prestige.count * 0.1; + const prestigeCombatMultiplier = Math.pow(PRESTIGE_COMBAT_BASE, state.prestige.count); const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1; const echoCombatMultiplier = state.transcendence?.echoCombatMultiplier ?? 1; const craftedGoldMultiplier @@ -378,8 +383,7 @@ export const computePartyCombatPower = (state: GameState): number => { } } - // eslint-disable-next-line stylistic/no-mixed-operators -- prestige count * factor is clear - const prestigeMultiplier = 1 + state.prestige.count * 0.1; + const prestigeMultiplier = Math.pow(PRESTIGE_COMBAT_BASE, state.prestige.count); const equipmentCombatMultiplier = state.equipment. filter((item) => {