generated from nhcarrigan/template
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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user