fix: runestone formula, prestige/transcendence rebalance, exploration fixes, and comprehensive balance audit #135

Merged
naomi merged 53 commits from fix/stones into main 2026-03-31 19:57:53 -07:00
5 changed files with 24 additions and 14 deletions
Showing only changes of commit 9cff54cfcd - Show all commits
+2 -2
View File
@@ -96,7 +96,7 @@ export const defaultPrestigeUpgrades: Array<PrestigeUpgrade> = [
id: "income_10", id: "income_10",
multiplier: 200, multiplier: 200,
name: "Eternal Rune I", name: "Eternal Rune I",
runestonesCost: 30_000, runestonesCost: 22_500,
}, },
{ {
category: "income", category: "income",
@@ -105,7 +105,7 @@ export const defaultPrestigeUpgrades: Array<PrestigeUpgrade> = [
id: "income_11", id: "income_11",
multiplier: 500, multiplier: 500,
name: "Eternal Rune II", name: "Eternal Rune II",
runestonesCost: 80_000, runestonesCost: 60_000,
}, },
// ── Click Power ─────────────────────────────────────────────────────────── // ── Click Power ───────────────────────────────────────────────────────────
{ {
+8 -2
View File
@@ -24,6 +24,13 @@ import { updateChallengeProgress } from "../services/dailyChallenges.js";
import { logger } from "../services/logger.js"; import { logger } from "../services/logger.js";
import type { HonoEnvironment } from "../types/hono.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<HonoEnvironment>(); const bossRouter = new Hono<HonoEnvironment>();
bossRouter.use("*", authMiddleware); bossRouter.use("*", authMiddleware);
@@ -38,8 +45,7 @@ const calculatePartyStats = (
} }
} }
// eslint-disable-next-line stylistic/no-mixed-operators -- prestige count * factor is clear const prestigeMultiplier = Math.pow(prestigeCombatBase, state.prestige.count);
const prestigeMultiplier = 1 + state.prestige.count * 0.1;
// Apply equipped weapon's combat bonus // Apply equipped weapon's combat bonus
// eslint-disable-next-line capitalized-comments -- v8 ignore // eslint-disable-next-line capitalized-comments -- v8 ignore
+2 -2
View File
@@ -159,7 +159,7 @@ const calculateProductionMultiplier = (
/** /**
* Returns the milestone runestone bonus for the given prestige count. * 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. * @param prestigeCount - The prestige count after the current prestige.
* @returns The milestone runestone bonus, or 0 if not a milestone prestige. * @returns The milestone runestone bonus, or 0 if not a milestone prestige.
*/ */
@@ -168,7 +168,7 @@ const calculateMilestoneBonus = (prestigeCount: number): number => {
return 0; return 0;
} }
const milestoneNumber = prestigeCount / milestoneInterval; const milestoneNumber = prestigeCount / milestoneInterval;
return milestoneNumber * milestoneRunestonesPerInterval; return milestoneNumber * milestoneNumber * milestoneRunestonesPerInterval;
}; };
/** /**
+4 -4
View File
@@ -151,12 +151,12 @@ describe("calculateMilestoneBonus", () => {
expect(calculateMilestoneBonus(5)).toBe(25); expect(calculateMilestoneBonus(5)).toBe(25);
}); });
it("returns 50 at prestige 10", () => { it("returns 100 at prestige 10", () => {
expect(calculateMilestoneBonus(10)).toBe(50); expect(calculateMilestoneBonus(10)).toBe(100);
}); });
it("returns 75 at prestige 15", () => { it("returns 225 at prestige 15", () => {
expect(calculateMilestoneBonus(15)).toBe(75); expect(calculateMilestoneBonus(15)).toBe(225);
}); });
}); });
+8 -4
View File
@@ -84,6 +84,12 @@ const checkAchievements = (state: GameState): Array<Achievement> => {
}); });
}; };
/**
* 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. * 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 runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1;
const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1; const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1;
// eslint-disable-next-line stylistic/no-mixed-operators -- prestige count * factor is clear const prestigeCombatMultiplier = Math.pow(PRESTIGE_COMBAT_BASE, state.prestige.count);
const prestigeCombatMultiplier = 1 + state.prestige.count * 0.1;
const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1; const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1;
const echoCombatMultiplier = state.transcendence?.echoCombatMultiplier ?? 1; const echoCombatMultiplier = state.transcendence?.echoCombatMultiplier ?? 1;
const craftedGoldMultiplier 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 = Math.pow(PRESTIGE_COMBAT_BASE, state.prestige.count);
const prestigeMultiplier = 1 + state.prestige.count * 0.1;
const equipmentCombatMultiplier = state.equipment. const equipmentCombatMultiplier = state.equipment.
filter((item) => { filter((item) => {