/** * @file API client for communicating with the Elysium backend. * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan */ /* eslint-disable max-lines -- API client grows with each new endpoint group */ import type { AboutResponse, ApotheosisRequest, ApotheosisResponse, AuthResponse, AwakeningRequest, AwakeningResponse, BossChallengeRequest, BossChallengeResponse, BuyAwakeningUpgradeRequest, BuyAwakeningUpgradeResponse, BuyConsecrationUpgradeRequest, BuyConsecrationUpgradeResponse, BuyEchoUpgradeRequest, BuyEchoUpgradeResponse, BuyEnlightenmentUpgradeRequest, BuyEnlightenmentUpgradeResponse, BuyGoddessUpgradeRequest, BuyGoddessUpgradeResponse, BuyPrestigeUpgradeRequest, BuyPrestigeUpgradeResponse, BuySiringUpgradeRequest, BuySiringUpgradeResponse, BuyVampireUpgradeRequest, BuyVampireUpgradeResponse, ConsecrationRequest, ConsecrationResponse, CraftRecipeRequest, CraftRecipeResponse, EnlightenmentRequest, EnlightenmentResponse, ExploreClaimableResponse, ExploreCollectRequest, ExploreCollectResponse, ExploreStartRequest, ExploreStartResponse, ForceUnlocksResponse, GoddessBossChallengeRequest, GoddessBossChallengeResponse, GoddessCraftRequest, GoddessCraftResponse, GoddessExploreClaimableResponse, GoddessExploreCollectRequest, GoddessExploreCollectResponse, GoddessExploreStartRequest, GoddessExploreStartResponse, LoadResponse, PrestigeRequest, PrestigeResponse, PublicProfileResponse, SaveRequest, SaveResponse, SiringRequest, SiringResponse, SyncNewContentResponse, TranscendenceRequest, TranscendenceResponse, UpdateProfileRequest, UpdateProfileResponse, VampireBossChallengeRequest, VampireBossChallengeResponse, VampireCraftRequest, VampireCraftResponse, VampireExploreClaimableResponse, VampireExploreCollectRequest, VampireExploreCollectResponse, VampireExploreStartRequest, VampireExploreStartResponse, } from "@elysium/types"; const baseUrl = "/api"; /** * Represents a 4xx API error so callers can distinguish expected server * rejections from unexpected failures. ValidationErrors are downgraded to * console.warn and are not forwarded to the error-email pipeline. */ class ValidationError extends Error { public readonly statusCode: number; /** * Creates a new ValidationError. * @param message - The error message from the server response. * @param statusCode - The HTTP status code (4xx) returned by the server. */ public constructor(message: string, statusCode: number) { super(message); this.name = "ValidationError"; this.statusCode = statusCode; } } const getToken = (): string | null => { return globalThis.localStorage.getItem("elysium_token"); }; /* eslint-disable @typescript-eslint/naming-convention -- HTTP header names require specific casing */ const buildHeaders = (): Record => { const token = getToken(); return { "Content-Type": "application/json", ...token !== null && token.length > 0 ? { Authorization: `Bearer ${token}` } : {}, }; }; /* eslint-enable @typescript-eslint/naming-convention -- HTTP header names require specific casing */ const fetchJson = async ( path: string, options?: RequestInit, ): Promise => { const response = await fetch(`${baseUrl}${path}`, { ...options, headers: { ...buildHeaders(), ...options?.headers }, }); if (!response.ok) { /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- JSON error response requires type assertion */ const errorBody = (await response.json().catch(() => { return { error: "Unknown error" }; })) as Record; const message = typeof errorBody.error === "string" ? errorBody.error : "Unknown error"; if (response.status === 401) { globalThis.localStorage.removeItem("elysium_token"); globalThis.localStorage.removeItem("elysium_save_signature"); globalThis.location.href = "/"; } if (response.status >= 400 && response.status < 500) { throw new ValidationError(message, response.status); } throw new Error(message); } /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- JSON response requires type assertion */ return await (response.json() as Promise); }; /** * Fetches the about information from the API. * @returns The about response data. */ const getAbout = async(): Promise => { return await fetchJson("/about"); }; /** * Fetches the Discord OAuth URL from the API. * @returns The authentication URL string. */ const getAuthUrl = async(): Promise => { const data = await fetchJson<{ url: string }>("/auth/url"); return data.url; }; /** * Handles the Discord OAuth callback and stores the auth token. * @param code - The OAuth authorization code from Discord. * @returns The authentication response data. */ const handleAuthCallback = async(code: string): Promise => { const data = await fetchJson(`/auth/callback?code=${code}`); globalThis.localStorage.setItem("elysium_token", data.token); return data; }; /** * Loads the current game state from the server. * @returns The load response containing the game state. */ const loadGame = async(): Promise => { return await fetchJson("/game/load"); }; /** * Resets all game progress on the server. * @returns The load response after reset. */ const resetProgress = async(): Promise => { return await fetchJson("/game/reset", { method: "POST" }); }; /** * Saves the current game state to the server. * @param body - The save request payload containing the game state. * @returns The save response data. */ const saveGame = async(body: SaveRequest): Promise => { return await fetchJson("/game/save", { body: JSON.stringify(body), method: "POST", }); }; /** * Challenges a boss with the current game state. * @param body - The boss challenge request payload. * @returns The boss challenge response data. */ const challengeBoss = async( body: BossChallengeRequest, ): Promise => { return await fetchJson("/boss/challenge", { body: JSON.stringify(body), method: "POST", }); }; /** * Triggers a prestige reset on the server. * @param body - The prestige request payload. * @returns The prestige response data. */ const prestige = async(body: PrestigeRequest): Promise => { return await fetchJson("/prestige", { body: JSON.stringify(body), method: "POST", }); }; /** * Purchases a prestige upgrade on the server. * @param body - The buy prestige upgrade request payload. * @returns The buy prestige upgrade response data. */ const buyPrestigeUpgrade = async( body: BuyPrestigeUpgradeRequest, ): Promise => { return await fetchJson("/prestige/buy-upgrade", { body: JSON.stringify(body), method: "POST", }); }; /** * Triggers a transcendence reset on the server. * @param body - The transcendence request payload. * @returns The transcendence response data. */ const transcend = async( body: TranscendenceRequest, ): Promise => { return await fetchJson("/transcendence", { body: JSON.stringify(body), method: "POST", }); }; /** * Purchases an echo upgrade on the server. * @param body - The buy echo upgrade request payload. * @returns The buy echo upgrade response data. */ const buyEchoUpgrade = async( body: BuyEchoUpgradeRequest, ): Promise => { return await fetchJson("/transcendence/buy-upgrade", { body: JSON.stringify(body), method: "POST", }); }; /** * Triggers an apotheosis reset on the server. * @param body - The apotheosis request payload. * @returns The apotheosis response data. */ const achieveApotheosis = async( body: ApotheosisRequest, ): Promise => { return await fetchJson("/apotheosis", { body: JSON.stringify(body), method: "POST", }); }; /** * Starts an exploration in a given area. * @param body - The exploration start request payload. * @returns The exploration start response data. */ const startExploration = async( body: ExploreStartRequest, ): Promise => { return await fetchJson("/explore/start", { body: JSON.stringify(body), method: "POST", }); }; /** * Collects the rewards from a completed exploration. * @param body - The exploration collect request payload. * @returns The exploration collect response data. */ const collectExploration = async( body: ExploreCollectRequest, ): Promise => { return await fetchJson("/explore/collect", { body: JSON.stringify(body), method: "POST", }); }; /** * Checks whether a given exploration area is ready to claim on the server. * @param areaId - The area ID to check. * @returns Whether the exploration is claimable. */ const checkExplorationClaimable = async( areaId: string, ): Promise => { return await fetchJson( `/explore/claimable?areaId=${encodeURIComponent(areaId)}`, ); }; /** * Crafts a recipe on the server. * @param body - The craft recipe request payload. * @returns The craft recipe response data. */ const craftRecipe = async( body: CraftRecipeRequest, ): Promise => { return await fetchJson("/craft", { body: JSON.stringify(body), method: "POST", }); }; /** * Sends a request to fix any missing unlocks in the player's game state. * @returns The corrected game state and counts of what was unlocked. */ const forceUnlocks = async(): Promise => { return await fetchJson("/debug/force-unlocks", { method: "POST", }); }; /** * Syncs any content added after the player's save was created into their save. * @returns The updated game state and counts of what was added per content type. */ const syncNewContent = async(): Promise => { return await fetchJson("/debug/sync-new-content", { method: "POST", }); }; /** * Performs a complete hard reset of the player's game state via the debug endpoint. * @returns The fresh game state as a LoadResponse. */ const debugHardReset = async(): Promise => { return await fetchJson("/debug/hard-reset", { method: "POST" }); }; /** * Challenges a goddess boss. * @param body - The goddess boss challenge request payload. * @returns The goddess boss challenge response data. */ const challengeGoddessBoss = async( body: GoddessBossChallengeRequest, ): Promise => { return await fetchJson( "/goddess-boss/challenge", { body: JSON.stringify(body), method: "POST" }, ); }; /** * Triggers a consecration reset on the server. * @param body - The consecration request payload. * @returns The consecration response data. */ const consecrate = async( body: ConsecrationRequest, ): Promise => { return await fetchJson("/consecration", { body: JSON.stringify(body), method: "POST", }); }; /** * Purchases a consecration upgrade on the server. * @param body - The buy consecration upgrade request payload. * @returns The buy consecration upgrade response data. */ const buyConsecrationUpgrade = async( body: BuyConsecrationUpgradeRequest, ): Promise => { return await fetchJson( "/consecration/buy-upgrade", { body: JSON.stringify(body), method: "POST" }, ); }; /** * Triggers an enlightenment reset on the server. * @param body - The enlightenment request payload. * @returns The enlightenment response data. */ const enlighten = async( body: EnlightenmentRequest, ): Promise => { return await fetchJson("/enlightenment", { body: JSON.stringify(body), method: "POST", }); }; /** * Purchases an enlightenment upgrade on the server. * @param body - The buy enlightenment upgrade request payload. * @returns The buy enlightenment upgrade response data. */ const buyEnlightenmentUpgrade = async( body: BuyEnlightenmentUpgradeRequest, ): Promise => { return await fetchJson( "/enlightenment/buy-upgrade", { body: JSON.stringify(body), method: "POST" }, ); }; /** * Purchases a goddess upgrade on the server. * @param body - The buy goddess upgrade request payload. * @returns The buy goddess upgrade response data. */ const buyGoddessUpgrade = async( body: BuyGoddessUpgradeRequest, ): Promise => { return await fetchJson("/goddess-upgrade/buy", { body: JSON.stringify(body), method: "POST", }); }; /** * Crafts a goddess recipe on the server. * @param body - The goddess craft request payload. * @returns The goddess craft response data. */ const craftGoddessRecipe = async( body: GoddessCraftRequest, ): Promise => { return await fetchJson("/goddess-craft", { body: JSON.stringify(body), method: "POST", }); }; /** * Starts a goddess exploration in a given area. * @param body - The goddess exploration start request payload. * @returns The goddess exploration start response data. */ const startGoddessExploration = async( body: GoddessExploreStartRequest, ): Promise => { return await fetchJson( "/goddess-explore/start", { body: JSON.stringify(body), method: "POST" }, ); }; /** * Collects the rewards from a completed goddess exploration. * @param body - The goddess exploration collect request payload. * @returns The goddess exploration collect response data. */ const collectGoddessExploration = async( body: GoddessExploreCollectRequest, ): Promise => { return await fetchJson( "/goddess-explore/collect", { body: JSON.stringify(body), method: "PUT" }, ); }; /** * Checks whether a given goddess exploration area is ready to claim on the server. * @param areaId - The area ID to check. * @returns Whether the goddess exploration is claimable. */ const checkGoddessExplorationClaimable = async( areaId: string, ): Promise => { return await fetchJson( `/goddess-explore/claimable?areaId=${encodeURIComponent(areaId)}`, ); }; /** * Challenges a vampire boss. * @param body - The vampire boss challenge request payload. * @returns The vampire boss challenge response data. */ const challengeVampireBoss = async( body: VampireBossChallengeRequest, ): Promise => { return await fetchJson( "/vampire-boss/challenge", { body: JSON.stringify(body), method: "POST" }, ); }; /** * Triggers a siring reset on the server. * @param body - The siring request payload. * @returns The siring response data. */ const sire = async(body: SiringRequest): Promise => { return await fetchJson("/siring", { body: JSON.stringify(body), method: "POST", }); }; /** * Purchases a siring upgrade on the server. * @param body - The buy siring upgrade request payload. * @returns The buy siring upgrade response data. */ const buySiringUpgrade = async( body: BuySiringUpgradeRequest, ): Promise => { return await fetchJson("/siring/buy-upgrade", { body: JSON.stringify(body), method: "POST", }); }; /** * Triggers a vampire awakening reset on the server. * @param body - The awakening request payload. * @returns The awakening response data. */ const awaken = async(body: AwakeningRequest): Promise => { return await fetchJson("/vampire-awakening", { body: JSON.stringify(body), method: "POST", }); }; /** * Purchases a vampire awakening upgrade on the server. * @param body - The buy awakening upgrade request payload. * @returns The buy awakening upgrade response data. */ const buyAwakeningUpgrade = async( body: BuyAwakeningUpgradeRequest, ): Promise => { return await fetchJson( "/vampire-awakening/buy-upgrade", { body: JSON.stringify(body), method: "POST" }, ); }; /** * Purchases a vampire upgrade on the server. * @param body - The buy vampire upgrade request payload. * @returns The buy vampire upgrade response data. */ const buyVampireUpgrade = async( body: BuyVampireUpgradeRequest, ): Promise => { return await fetchJson("/vampire-upgrade/buy", { body: JSON.stringify(body), method: "POST", }); }; /** * Crafts a vampire recipe on the server. * @param body - The vampire craft request payload. * @returns The vampire craft response data. */ const craftVampireRecipe = async( body: VampireCraftRequest, ): Promise => { return await fetchJson("/vampire-craft", { body: JSON.stringify(body), method: "POST", }); }; /** * Starts a vampire exploration in a given area. * @param body - The vampire exploration start request payload. * @returns The vampire exploration start response data. */ const startVampireExploration = async( body: VampireExploreStartRequest, ): Promise => { return await fetchJson( "/vampire-explore/start", { body: JSON.stringify(body), method: "POST" }, ); }; /** * Collects the rewards from a completed vampire exploration. * @param body - The vampire exploration collect request payload. * @returns The vampire exploration collect response data. */ const collectVampireExploration = async( body: VampireExploreCollectRequest, ): Promise => { return await fetchJson( "/vampire-explore/collect", { body: JSON.stringify(body), method: "PUT" }, ); }; /** * Checks whether a given vampire exploration area is ready to claim on the server. * @param areaId - The area ID to check. * @returns Whether the vampire exploration is claimable. */ const checkVampireExplorationClaimable = async( areaId: string, ): Promise => { return await fetchJson( `/vampire-explore/claimable?areaId=${encodeURIComponent(areaId)}`, ); }; /** * Fetches a public player profile by Discord ID. * @param discordId - The Discord ID of the player to look up. * @returns The public profile response data. */ const getPublicProfile = async( discordId: string, ): Promise => { return await fetchJson(`/profile/${discordId}`); }; /** * Updates the current player's profile. * @param body - The update profile request payload. * @returns The update profile response data. */ const updateProfile = async( body: UpdateProfileRequest, ): Promise => { return await fetchJson("/profile", { body: JSON.stringify(body), method: "PUT", }); }; export { ValidationError, achieveApotheosis, awaken, buyAwakeningUpgrade, buyConsecrationUpgrade, buyEchoUpgrade, buyEnlightenmentUpgrade, buyGoddessUpgrade, buyPrestigeUpgrade, buySiringUpgrade, buyVampireUpgrade, challengeBoss, challengeGoddessBoss, challengeVampireBoss, checkExplorationClaimable, checkGoddessExplorationClaimable, checkVampireExplorationClaimable, collectExploration, collectGoddessExploration, collectVampireExploration, consecrate, craftGoddessRecipe, craftRecipe, craftVampireRecipe, debugHardReset, enlighten, forceUnlocks, syncNewContent, getAbout, getAuthUrl, getPublicProfile, handleAuthCallback, loadGame, prestige, resetProgress, saveGame, sire, startExploration, startGoddessExploration, startVampireExploration, transcend, updateProfile, };