From 48bf74e71314912957a31af14553cb26b3442799 Mon Sep 17 00:00:00 2001 From: Hikari Date: Fri, 6 Mar 2026 23:34:05 -0800 Subject: [PATCH] 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. --- IDEAS.md | 8 ++++---- apps/api/src/routes/prestige.ts | 3 ++- apps/api/src/services/prestige.ts | 19 ++++++++++++++++--- .../web/src/components/game/PrestigePanel.tsx | 7 +++++-- packages/types/src/interfaces/Api.ts | 4 ++++ 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/IDEAS.md b/IDEAS.md index 36a9cb3..34a3e8d 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -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. -- [ ] **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. @@ -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. -- [ ] **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~~ ✅ 2. ~~Statistics panel~~ ✅ 3. ~~Daily challenges~~ ✅ -4. Boss first-kill bounties (easy content win) -5. Milestone prestige bonuses (easy content win) +4. ~~Boss first-kill bounties~~ ✅ +5. ~~Milestone prestige bonuses~~ ✅ 6. Equipment set bonuses (medium effort) 7. Auto-prestige toggle (prestige shop upgrade) 8. The Codex / Lore Book (flavour, lower priority) diff --git a/apps/api/src/routes/prestige.ts b/apps/api/src/routes/prestige.ts index 449ce84..66b0a09 100644 --- a/apps/api/src/routes/prestige.ts +++ b/apps/api/src/routes/prestige.ts @@ -47,7 +47,7 @@ prestigeRouter.post("/", async (context) => { challengeCrystals = result.crystalsAwarded; } - const { newState, newPrestigeData, runestonesEarned } = buildPostPrestigeState( + const { newState, newPrestigeData, runestonesEarned, milestoneRunestones } = buildPostPrestigeState( state, characterName, ); @@ -81,6 +81,7 @@ prestigeRouter.post("/", async (context) => { return context.json({ runestones: runestonesEarned, newPrestigeCount: newPrestigeData.count, + milestoneRunestones, }); }); diff --git a/apps/api/src/services/prestige.ts b/apps/api/src/services/prestige.ts index a5c64f6..dfb9a40 100644 --- a/apps/api/src/services/prestige.ts +++ b/apps/api/src/services/prestige.ts @@ -9,6 +9,8 @@ import { DEFAULT_PRESTIGE_UPGRADES } from "../data/prestigeUpgrades.js"; const BASE_PRESTIGE_GOLD_THRESHOLD = 1_000_000; const THRESHOLD_SCALE_FACTOR = 5; 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. @@ -65,6 +67,16 @@ export const calculateRunestones = ( export const calculateProductionMultiplier = (prestigeCount: number): number => 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. * Carries over prestige data and runestones; resets everything else. @@ -72,7 +84,7 @@ export const calculateProductionMultiplier = (prestigeCount: number): number => export const buildPostPrestigeState = ( currentState: GameState, characterName: string, -): { newState: GameState; newPrestigeData: PrestigeData; runestonesEarned: number } => { +): { newState: GameState; newPrestigeData: PrestigeData; runestonesEarned: number; milestoneRunestones: number } => { const runestonesEarned = calculateRunestones( currentState.player.totalGoldEarned, currentState.prestige.count, @@ -80,10 +92,11 @@ export const buildPostPrestigeState = ( ); const newPrestigeCount = currentState.prestige.count + 1; const { purchasedUpgradeIds } = currentState.prestige; + const milestoneRunestones = calculateMilestoneBonus(newPrestigeCount); const newPrestigeData: PrestigeData = { count: newPrestigeCount, - runestones: currentState.prestige.runestones + runestonesEarned, + runestones: currentState.prestige.runestones + runestonesEarned + milestoneRunestones, productionMultiplier: calculateProductionMultiplier(newPrestigeCount), purchasedUpgradeIds, lastPrestigedAt: Date.now(), @@ -97,5 +110,5 @@ export const buildPostPrestigeState = ( lastTickAt: Date.now(), }; - return { newState, newPrestigeData, runestonesEarned }; + return { newState, newPrestigeData, runestonesEarned, milestoneRunestones }; }; diff --git a/apps/web/src/components/game/PrestigePanel.tsx b/apps/web/src/components/game/PrestigePanel.tsx index 4cf8143..d25010c 100644 --- a/apps/web/src/components/game/PrestigePanel.tsx +++ b/apps/web/src/components/game/PrestigePanel.tsx @@ -42,7 +42,7 @@ export const PrestigePanel = (): React.JSX.Element => { const { state, reload, formatNumber, buyPrestigeUpgrade } = useGame(); const [characterName, setCharacterName] = useState(""); 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(null); const [buyingId, setBuyingId] = useState(null); const [activeTab, setActiveTab] = useState<"prestige" | "shop">("prestige"); @@ -65,7 +65,7 @@ export const PrestigePanel = (): React.JSX.Element => { setPrestigeError(null); try { 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(); } catch (err) { setPrestigeError(err instanceof Error ? err.message : "Prestige failed"); @@ -176,6 +176,9 @@ export const PrestigePanel = (): React.JSX.Element => { {result && (

Ascended to Prestige {result.count}! Earned {formatNumber(result.runestones)} Runestones. + {result.milestoneRunestones > 0 && ( + <> 🎉 Milestone bonus: +{formatNumber(result.milestoneRunestones)} Runestones! + )}

)} diff --git a/packages/types/src/interfaces/Api.ts b/packages/types/src/interfaces/Api.ts index 519f124..38730fa 100644 --- a/packages/types/src/interfaces/Api.ts +++ b/packages/types/src/interfaces/Api.ts @@ -58,6 +58,8 @@ export interface BossChallengeResponse { crystals: number; upgradeIds: string[]; equipmentIds: string[]; + /** Runestone bounty awarded for defeating this boss for the very first time */ + bountyRunestones: number; }; casualties?: Array<{ adventurerId: string; @@ -72,6 +74,8 @@ export interface PrestigeRequest { export interface PrestigeResponse { runestones: number; newPrestigeCount: number; + /** Bonus runestones awarded for reaching a milestone prestige (every 5th), 0 if not a milestone */ + milestoneRunestones: number; } export interface BuyPrestigeUpgradeRequest {