feat: add milestone prestige bonuses every 5th prestige

Every 5th prestige awards a scaling runestone windfall:
milestone_number * 25 stones (prestige 5 = 25, 10 = 50, 50 = 250, etc).
Shown in the ascension success message when non-zero.
This commit is contained in:
2026-03-06 23:34:05 -08:00
committed by Naomi Carrigan
parent f84654263e
commit 48bf74e713
5 changed files with 31 additions and 10 deletions
+4 -4
View File
@@ -12,7 +12,7 @@ A running list of planned features and content additions. Strike through items a
- [x] **Daily challenges** — Three rotating objectives each day (e.g. kill X boss, earn X gold this run, complete X quests). Reward bonus crystals. Encourages daily logins even when idling comfortably. - [x] **Daily challenges** — Three rotating objectives each day (e.g. kill X boss, earn X gold this run, complete X quests). Reward bonus crystals. Encourages daily logins even when idling comfortably.
- [ ] **Boss first-kill bounties** — Defeating a boss for the very first time grants a one-time runestone bonus. Rewards exploration and makes conquering a new zone feel extra satisfying. - [x] **Boss first-kill bounties** — Defeating a boss for the very first time grants a one-time runestone bonus. Rewards exploration and makes conquering a new zone feel extra satisfying.
- [ ] **Auto-prestige toggle** — Unlockable via the prestige shop. Automatically prestiges the moment the threshold is reached. Late-game convenience that dedicated players will love. - [ ] **Auto-prestige toggle** — Unlockable via the prestige shop. Automatically prestiges the moment the threshold is reached. Late-game convenience that dedicated players will love.
@@ -24,7 +24,7 @@ A running list of planned features and content additions. Strike through items a
- [ ] **The Codex / Lore Book** — Defeating bosses and completing quests unlocks lore entries about the world. Pure flavour, but gives the world depth and a collection mechanic. Show a ✨ notification when new lore unlocks. - [ ] **The Codex / Lore Book** — Defeating bosses and completing quests unlocks lore entries about the world. Pure flavour, but gives the world depth and a collection mechanic. Show a ✨ notification when new lore unlocks.
- [ ] **Milestone prestige bonuses** — Every 5th prestige, earn a free prestige upgrade or a large runestone windfall. Gives players mini-goals within the prestige loop. - [x] **Milestone prestige bonuses** — Every 5th prestige, earn a free prestige upgrade or a large runestone windfall. Gives players mini-goals within the prestige loop.
--- ---
@@ -41,8 +41,8 @@ A running list of planned features and content additions. Strike through items a
1. ~~Offline earnings~~ 1. ~~Offline earnings~~
2. ~~Statistics panel~~ 2. ~~Statistics panel~~
3. ~~Daily challenges~~ 3. ~~Daily challenges~~
4. Boss first-kill bounties (easy content win) 4. ~~Boss first-kill bounties~~
5. Milestone prestige bonuses (easy content win) 5. ~~Milestone prestige bonuses~~
6. Equipment set bonuses (medium effort) 6. Equipment set bonuses (medium effort)
7. Auto-prestige toggle (prestige shop upgrade) 7. Auto-prestige toggle (prestige shop upgrade)
8. The Codex / Lore Book (flavour, lower priority) 8. The Codex / Lore Book (flavour, lower priority)
+2 -1
View File
@@ -47,7 +47,7 @@ prestigeRouter.post("/", async (context) => {
challengeCrystals = result.crystalsAwarded; challengeCrystals = result.crystalsAwarded;
} }
const { newState, newPrestigeData, runestonesEarned } = buildPostPrestigeState( const { newState, newPrestigeData, runestonesEarned, milestoneRunestones } = buildPostPrestigeState(
state, state,
characterName, characterName,
); );
@@ -81,6 +81,7 @@ prestigeRouter.post("/", async (context) => {
return context.json({ return context.json({
runestones: runestonesEarned, runestones: runestonesEarned,
newPrestigeCount: newPrestigeData.count, newPrestigeCount: newPrestigeData.count,
milestoneRunestones,
}); });
}); });
+16 -3
View File
@@ -9,6 +9,8 @@ import { DEFAULT_PRESTIGE_UPGRADES } from "../data/prestigeUpgrades.js";
const BASE_PRESTIGE_GOLD_THRESHOLD = 1_000_000; const BASE_PRESTIGE_GOLD_THRESHOLD = 1_000_000;
const THRESHOLD_SCALE_FACTOR = 5; const THRESHOLD_SCALE_FACTOR = 5;
const RUNESTONES_PER_PRESTIGE_LEVEL = 10; const RUNESTONES_PER_PRESTIGE_LEVEL = 10;
const MILESTONE_INTERVAL = 5;
const MILESTONE_RUNESTONES_PER_INTERVAL = 25;
/** /**
* Calculates the gold threshold required for the next prestige. * Calculates the gold threshold required for the next prestige.
@@ -65,6 +67,16 @@ export const calculateRunestones = (
export const calculateProductionMultiplier = (prestigeCount: number): number => export const calculateProductionMultiplier = (prestigeCount: number): number =>
Math.pow(1.15, prestigeCount); Math.pow(1.15, prestigeCount);
/**
* Returns the milestone runestone bonus for the given prestige count.
* Every MILESTONE_INTERVAL prestiges awards milestone_number * MILESTONE_RUNESTONES_PER_INTERVAL stones.
*/
export const calculateMilestoneBonus = (newPrestigeCount: number): number => {
if (newPrestigeCount % MILESTONE_INTERVAL !== 0) return 0;
const milestoneNumber = newPrestigeCount / MILESTONE_INTERVAL;
return milestoneNumber * MILESTONE_RUNESTONES_PER_INTERVAL;
};
/** /**
* Generates the reset game state after a prestige. * Generates the reset game state after a prestige.
* Carries over prestige data and runestones; resets everything else. * Carries over prestige data and runestones; resets everything else.
@@ -72,7 +84,7 @@ export const calculateProductionMultiplier = (prestigeCount: number): number =>
export const buildPostPrestigeState = ( export const buildPostPrestigeState = (
currentState: GameState, currentState: GameState,
characterName: string, characterName: string,
): { newState: GameState; newPrestigeData: PrestigeData; runestonesEarned: number } => { ): { newState: GameState; newPrestigeData: PrestigeData; runestonesEarned: number; milestoneRunestones: number } => {
const runestonesEarned = calculateRunestones( const runestonesEarned = calculateRunestones(
currentState.player.totalGoldEarned, currentState.player.totalGoldEarned,
currentState.prestige.count, currentState.prestige.count,
@@ -80,10 +92,11 @@ export const buildPostPrestigeState = (
); );
const newPrestigeCount = currentState.prestige.count + 1; const newPrestigeCount = currentState.prestige.count + 1;
const { purchasedUpgradeIds } = currentState.prestige; const { purchasedUpgradeIds } = currentState.prestige;
const milestoneRunestones = calculateMilestoneBonus(newPrestigeCount);
const newPrestigeData: PrestigeData = { const newPrestigeData: PrestigeData = {
count: newPrestigeCount, count: newPrestigeCount,
runestones: currentState.prestige.runestones + runestonesEarned, runestones: currentState.prestige.runestones + runestonesEarned + milestoneRunestones,
productionMultiplier: calculateProductionMultiplier(newPrestigeCount), productionMultiplier: calculateProductionMultiplier(newPrestigeCount),
purchasedUpgradeIds, purchasedUpgradeIds,
lastPrestigedAt: Date.now(), lastPrestigedAt: Date.now(),
@@ -97,5 +110,5 @@ export const buildPostPrestigeState = (
lastTickAt: Date.now(), lastTickAt: Date.now(),
}; };
return { newState, newPrestigeData, runestonesEarned }; return { newState, newPrestigeData, runestonesEarned, milestoneRunestones };
}; };
@@ -42,7 +42,7 @@ export const PrestigePanel = (): React.JSX.Element => {
const { state, reload, formatNumber, buyPrestigeUpgrade } = useGame(); const { state, reload, formatNumber, buyPrestigeUpgrade } = useGame();
const [characterName, setCharacterName] = useState(""); const [characterName, setCharacterName] = useState("");
const [isPending, setIsPending] = useState(false); const [isPending, setIsPending] = useState(false);
const [result, setResult] = useState<{ runestones: number; count: number } | null>(null); const [result, setResult] = useState<{ runestones: number; count: number; milestoneRunestones: number } | null>(null);
const [prestigeError, setPrestigeError] = useState<string | null>(null); const [prestigeError, setPrestigeError] = useState<string | null>(null);
const [buyingId, setBuyingId] = useState<string | null>(null); const [buyingId, setBuyingId] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<"prestige" | "shop">("prestige"); const [activeTab, setActiveTab] = useState<"prestige" | "shop">("prestige");
@@ -65,7 +65,7 @@ export const PrestigePanel = (): React.JSX.Element => {
setPrestigeError(null); setPrestigeError(null);
try { try {
const data = await prestige({ characterName: characterName.trim() }); const data = await prestige({ characterName: characterName.trim() });
setResult({ runestones: data.runestones, count: data.newPrestigeCount }); setResult({ runestones: data.runestones, count: data.newPrestigeCount, milestoneRunestones: data.milestoneRunestones });
await reload(); await reload();
} catch (err) { } catch (err) {
setPrestigeError(err instanceof Error ? err.message : "Prestige failed"); setPrestigeError(err instanceof Error ? err.message : "Prestige failed");
@@ -176,6 +176,9 @@ export const PrestigePanel = (): React.JSX.Element => {
{result && ( {result && (
<p className="success"> <p className="success">
Ascended to Prestige {result.count}! Earned {formatNumber(result.runestones)} Runestones. Ascended to Prestige {result.count}! Earned {formatNumber(result.runestones)} Runestones.
{result.milestoneRunestones > 0 && (
<> 🎉 Milestone bonus: +{formatNumber(result.milestoneRunestones)} Runestones!</>
)}
</p> </p>
)} )}
</div> </div>
+4
View File
@@ -58,6 +58,8 @@ export interface BossChallengeResponse {
crystals: number; crystals: number;
upgradeIds: string[]; upgradeIds: string[];
equipmentIds: string[]; equipmentIds: string[];
/** Runestone bounty awarded for defeating this boss for the very first time */
bountyRunestones: number;
}; };
casualties?: Array<{ casualties?: Array<{
adventurerId: string; adventurerId: string;
@@ -72,6 +74,8 @@ export interface PrestigeRequest {
export interface PrestigeResponse { export interface PrestigeResponse {
runestones: number; runestones: number;
newPrestigeCount: number; newPrestigeCount: number;
/** Bonus runestones awarded for reaching a milestone prestige (every 5th), 0 if not a milestone */
milestoneRunestones: number;
} }
export interface BuyPrestigeUpgradeRequest { export interface BuyPrestigeUpgradeRequest {