feat: content expansion, prestige shop, and offline earnings improvements

- Expand content to 18 zones, 72 bosses, 95 quests, 32 adventurer tiers
- Add prestige shop with 24 runestone upgrades across 5 categories
- Add PrestigeUpgrade type, data files, API routes, and frontend panel
- Fix offline earnings to include equipment and runestone multipliers
- Add offline essence calculation alongside offline gold
- Update OfflineModal to display both gold and essence earned
- Add IDEAS.md for tracking planned features
This commit is contained in:
2026-03-06 21:55:42 -08:00
committed by Naomi Carrigan
parent 6bc116a86a
commit 5b4661b398
23 changed files with 2288 additions and 91 deletions
+41 -2
View File
@@ -7,7 +7,12 @@ import {
useRef,
useState,
} from "react";
import { challengeBoss as challengeBossApi, loadGame, saveGame } from "../api/client.js";
import {
buyPrestigeUpgrade as buyPrestigeUpgradeApi,
challengeBoss as challengeBossApi,
loadGame,
saveGame,
} from "../api/client.js";
import { RESOURCE_CAP, applyTick, calculateClickPower } from "../engine/tick.js";
import { formatNumber as formatNumberUtil } from "../utils/format.js";
@@ -47,7 +52,9 @@ interface GameContextValue {
syncError: string | null;
/** Offline gold earned on login */
offlineGold: number;
/** Dismiss the offline gold notification */
/** Offline essence earned on login */
offlineEssence: number;
/** Dismiss the offline earnings notification */
dismissOfflineGold: () => void;
/** Battle result to display in the modal (null when no battle pending) */
battleResult: BattleResult | null;
@@ -63,6 +70,8 @@ interface GameContextValue {
setNumberFormat: (format: NumberFormat) => void;
/** Format a number using the player's chosen notation style */
formatNumber: (value: number) => string;
/** Buy a prestige upgrade from the runestone shop */
buyPrestigeUpgrade: (upgradeId: string) => Promise<void>;
}
const GameContext = createContext<GameContextValue | null>(null);
@@ -74,6 +83,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [offlineGold, setOfflineGold] = useState(0);
const [offlineEssence, setOfflineEssence] = useState(0);
const [battleResult, setBattleResult] = useState<BattleResult | null>(null);
const [newAchievements, setNewAchievements] = useState<Achievement[]>([]);
const [lastSavedAt, setLastSavedAt] = useState<number | null>(null);
@@ -104,6 +114,9 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
if (data.offlineGold > 0) {
setOfflineGold(data.offlineGold);
}
if (data.offlineEssence > 0) {
setOfflineEssence(data.offlineEssence);
}
// Fetch number format preference from profile (fire-and-forget, non-blocking)
void fetch(`/api/profile/${data.state.player.discordId}`)
.then(async (res) => {
@@ -353,6 +366,29 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
});
}, []);
const buyPrestigeUpgrade = useCallback(async (upgradeId: string) => {
try {
const result = await buyPrestigeUpgradeApi({ upgradeId });
setState((prev) => {
if (!prev) return prev;
return {
...prev,
prestige: {
...prev.prestige,
runestones: result.runestonesRemaining,
purchasedUpgradeIds: result.purchasedUpgradeIds,
runestonesIncomeMultiplier: result.runestonesIncomeMultiplier,
runestonesClickMultiplier: result.runestonesClickMultiplier,
runestonesEssenceMultiplier: result.runestonesEssenceMultiplier,
runestonesCrystalMultiplier: result.runestonesCrystalMultiplier,
},
};
});
} catch {
// Silently ignore — server errors shouldn't crash the UI
}
}, []);
const challengeBoss = useCallback(async (bossId: string) => {
if (!stateRef.current) return;
const boss = stateRef.current.bosses.find((b) => b.id === bossId);
@@ -464,6 +500,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
const dismissOfflineGold = useCallback(() => {
setOfflineGold(0);
setOfflineEssence(0);
}, []);
const dismissBattle = useCallback(() => {
@@ -498,6 +535,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
forceSync,
syncError,
offlineGold,
offlineEssence,
dismissOfflineGold,
battleResult,
dismissBattle,
@@ -506,6 +544,7 @@ export const GameProvider = ({ children }: { children: React.ReactNode }): React
numberFormat,
setNumberFormat,
formatNumber: boundFormatNumber,
buyPrestigeUpgrade,
}}
>
{children}