From d9d1228172a4b69bc993c926c30c0fa42be2e153 Mon Sep 17 00:00:00 2001 From: Hikari Date: Thu, 16 Apr 2026 10:03:14 -0700 Subject: [PATCH] feat: vampire UI infrastructure - mode bar, tab row, and blood-red theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added vampire API client functions (boss challenge, siring, awakening, upgrades, craft, explore) - Fixed goddess API client URL bugs (/goddess/* β†’ /goddess-boss/challenge, /goddess-upgrade/buy, /goddess-craft, /goddess-explore/*) - Added VampireTab type and vampireTabs array (11 tabs) to GameLayout - Fixed mode unlock chain: vampire after apotheosis, goddess after eternalSovereignty - Added body.vampire-mode CSS class toggle alongside existing goddess-mode toggle - Added vampire tab bar nav with blood-red theme - Replaced single vampire placeholder with per-tab placeholders - Added body.vampire-mode CSS variables (blood-red palette) and .vampire-tab-bar styles - Added Blood/Ichor/Soul Shards resource display to ResourceBar (gated on apotheosis) --- apps/web/src/api/client.ts | 194 ++++++++++++++++++-- apps/web/src/components/game/gameLayout.tsx | 167 ++++++++++++++--- apps/web/src/components/ui/resourceBar.tsx | 37 ++++ apps/web/src/styles.css | 33 ++++ 4 files changed, 392 insertions(+), 39 deletions(-) diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts index 28e3981..0eb1e5e 100644 --- a/apps/web/src/api/client.ts +++ b/apps/web/src/api/client.ts @@ -10,8 +10,12 @@ import type { ApotheosisRequest, ApotheosisResponse, AuthResponse, + AwakeningRequest, + AwakeningResponse, BossChallengeRequest, BossChallengeResponse, + BuyAwakeningUpgradeRequest, + BuyAwakeningUpgradeResponse, BuyConsecrationUpgradeRequest, BuyConsecrationUpgradeResponse, BuyEchoUpgradeRequest, @@ -22,6 +26,10 @@ import type { BuyGoddessUpgradeResponse, BuyPrestigeUpgradeRequest, BuyPrestigeUpgradeResponse, + BuySiringUpgradeRequest, + BuySiringUpgradeResponse, + BuyVampireUpgradeRequest, + BuyVampireUpgradeResponse, ConsecrationRequest, ConsecrationResponse, CraftRecipeRequest, @@ -49,11 +57,22 @@ import type { 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"; @@ -356,10 +375,10 @@ const debugHardReset = async(): Promise => { const challengeGoddessBoss = async( body: GoddessBossChallengeRequest, ): Promise => { - return await fetchJson("/goddess/boss", { - body: JSON.stringify(body), - method: "POST", - }); + return await fetchJson( + "/goddess-boss/challenge", + { body: JSON.stringify(body), method: "POST" }, + ); }; /** @@ -426,7 +445,7 @@ const buyEnlightenmentUpgrade = async( const buyGoddessUpgrade = async( body: BuyGoddessUpgradeRequest, ): Promise => { - return await fetchJson("/goddess/upgrade", { + return await fetchJson("/goddess-upgrade/buy", { body: JSON.stringify(body), method: "POST", }); @@ -440,7 +459,7 @@ const buyGoddessUpgrade = async( const craftGoddessRecipe = async( body: GoddessCraftRequest, ): Promise => { - return await fetchJson("/goddess/craft", { + return await fetchJson("/goddess-craft", { body: JSON.stringify(body), method: "POST", }); @@ -454,10 +473,10 @@ const craftGoddessRecipe = async( const startGoddessExploration = async( body: GoddessExploreStartRequest, ): Promise => { - return await fetchJson("/goddess/explore", { - body: JSON.stringify(body), - method: "POST", - }); + return await fetchJson( + "/goddess-explore/start", + { body: JSON.stringify(body), method: "POST" }, + ); }; /** @@ -468,10 +487,10 @@ const startGoddessExploration = async( const collectGoddessExploration = async( body: GoddessExploreCollectRequest, ): Promise => { - return await fetchJson("/goddess/explore", { - body: JSON.stringify(body), - method: "PUT", - }); + return await fetchJson( + "/goddess-explore/collect", + { body: JSON.stringify(body), method: "PUT" }, + ); }; /** @@ -483,7 +502,142 @@ const checkGoddessExplorationClaimable = async( areaId: string, ): Promise => { return await fetchJson( - `/goddess/explore/claimable?areaId=${encodeURIComponent(areaId)}`, + `/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)}`, ); }; @@ -515,20 +669,28 @@ const updateProfile = async( 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, @@ -541,8 +703,10 @@ export { prestige, resetProgress, saveGame, + sire, startExploration, startGoddessExploration, + startVampireExploration, transcend, updateProfile, }; diff --git a/apps/web/src/components/game/gameLayout.tsx b/apps/web/src/components/game/gameLayout.tsx index 5590bae..344b701 100644 --- a/apps/web/src/components/game/gameLayout.tsx +++ b/apps/web/src/components/game/gameLayout.tsx @@ -7,6 +7,7 @@ /* eslint-disable max-lines -- Complex layout with many conditional renders */ /* eslint-disable max-lines-per-function -- Complex layout with many conditional renders */ /* eslint-disable complexity -- Many tab render paths */ +/* eslint-disable max-statements -- Many state variables for multi-mode tab routing */ import { type JSX, useEffect, useState } from "react"; import { useGame } from "../../context/gameContext.js"; import { ResourceBar } from "../ui/resourceBar.js"; @@ -89,6 +90,19 @@ type GoddessTab = | "goddess-exploration" | "goddess-achievements"; +type VampireTab = + | "vampire-zones" + | "vampire-bosses" + | "vampire-quests" + | "thralls" + | "vampire-equipment" + | "vampire-upgrades" + | "siring" + | "vampire-awakening" + | "vampire-crafting" + | "vampire-exploration" + | "vampire-achievements"; + const baseTabs: Array<{ id: Tab; label: string }> = [ { id: "adventurers", label: "βš”οΈ Adventurers" }, { id: "upgrades", label: "πŸ”§ Upgrades" }, @@ -111,6 +125,20 @@ const baseTabs: Array<{ id: Tab; label: string }> = [ { id: "debug", label: "πŸ”§ Debug" }, ]; +const vampireTabs: Array<{ id: VampireTab; label: string }> = [ + { id: "vampire-zones", label: "πŸ—ΊοΈ Zones" }, + { id: "vampire-bosses", label: "🩸 Bosses" }, + { id: "vampire-quests", label: "πŸ“œ Quests" }, + { id: "thralls", label: "🧟 Thralls" }, + { id: "vampire-equipment", label: "πŸ¦‡ Equipment" }, + { id: "vampire-upgrades", label: "βš”οΈ Upgrades" }, + { id: "siring", label: "🩸 Siring" }, + { id: "vampire-awakening", label: "πŸ’€ Awakening" }, + { id: "vampire-crafting", label: "βš—οΈ Crafting" }, + { id: "vampire-exploration", label: "πŸŒ‘ Exploration" }, + { id: "vampire-achievements", label: "πŸ† Achievements" }, +]; + const goddessTabs: Array<{ id: GoddessTab; label: string }> = [ { id: "goddess-zones", label: "🌟 Zones" }, { id: "goddess-bosses", label: "πŸ‘οΈ Bosses" }, @@ -169,12 +197,15 @@ const GameLayout = (): JSX.Element => { const [ activeTab, setActiveTab ] = useState("adventurers"); const [ activeGoddessTab, setActiveGoddessTab ] = useState("goddess-zones"); + const [ activeVampireTab, setActiveVampireTab ] + = useState("vampire-zones"); const [ editingProfile, setEditingProfile ] = useState(false); const [ dismissedOutdatedWarning, setDismissedOutdatedWarning ] = useState(false); useEffect(() => { document.body.classList.toggle("goddess-mode", activeMode === "goddess"); + document.body.classList.toggle("vampire-mode", activeMode === "vampire"); }, [ activeMode ]); if (isLoading) { @@ -273,8 +304,13 @@ const GameLayout = (): JSX.Element => { + {/* eslint-disable-next-line no-nested-ternary -- Three-way mode switch for tab bar */} {activeMode === "mortal" ? - : + : activeMode === "goddess" + ? + : }
@@ -420,8 +478,69 @@ const GameLayout = (): JSX.Element => { && activeGoddessTab === "goddess-achievements" && } {activeMode === "vampire" - &&
-

{"πŸ§› Vampire panels coming soon..."}

+ && activeVampireTab === "vampire-zones" + &&
+

{"πŸ—ΊοΈ Vampire Zones coming soon..."}

+
+ } + {activeMode === "vampire" + && activeVampireTab === "vampire-bosses" + &&
+

{"🩸 Vampire Bosses coming soon..."}

+
+ } + {activeMode === "vampire" + && activeVampireTab === "vampire-quests" + &&
+

{"πŸ“œ Vampire Quests coming soon..."}

+
+ } + {activeMode === "vampire" + && activeVampireTab === "thralls" + &&
+

{"🧟 Thralls coming soon..."}

+
+ } + {activeMode === "vampire" + && activeVampireTab === "vampire-equipment" + &&
+

{"πŸ¦‡ Vampire Equipment coming soon..."}

+
+ } + {activeMode === "vampire" + && activeVampireTab === "vampire-upgrades" + &&
+

{"βš”οΈ Vampire Upgrades coming soon..."}

+
+ } + {activeMode === "vampire" + && activeVampireTab === "siring" + &&
+

{"🩸 Siring coming soon..."}

+
+ } + {activeMode === "vampire" + && activeVampireTab === "vampire-awakening" + &&
+

{"πŸ’€ Vampire Awakening coming soon..."}

+
+ } + {activeMode === "vampire" + && activeVampireTab === "vampire-crafting" + &&
+

{"βš—οΈ Vampire Crafting coming soon..."}

+
+ } + {activeMode === "vampire" + && activeVampireTab === "vampire-exploration" + &&
+

{"πŸŒ‘ Vampire Exploration coming soon..."}

+
+ } + {activeMode === "vampire" + && activeVampireTab === "vampire-achievements" + &&
+

{"πŸ† Vampire Achievements coming soon..."}

}
diff --git a/apps/web/src/components/ui/resourceBar.tsx b/apps/web/src/components/ui/resourceBar.tsx index 4a067b6..00aafc1 100644 --- a/apps/web/src/components/ui/resourceBar.tsx +++ b/apps/web/src/components/ui/resourceBar.tsx @@ -88,6 +88,9 @@ const ResourceBar = ({ const { gold, essence, crystals, prayers, divinity, stardust } = resources; const hasApotheosis = apotheosisCount > 0; + const blood = resources.blood ?? 0; + const ichor = state?.vampire?.siring.ichor ?? 0; + const soulShards = state?.vampire?.awakening.soulShards ?? 0; let partyCombatPower = 0; let goldPerSecond = 0; let essencePerSecond = 0; @@ -286,6 +289,40 @@ const ResourceBar = ({ {"Stardust"}
+
+
+ {"🩸"} + + {hasApotheosis + ? formatNumber(blood) + : "πŸ”’"} + + {"Blood"} +
+
+ {"πŸ’§"} + + {hasApotheosis + ? formatNumber(ichor) + : "πŸ”’"} + + {"Ichor"} +
+
+ {"πŸ’ "} + + {hasApotheosis + ? formatNumber(soulShards) + : "πŸ”’"} + + {"Soul Shards"} +
: null} diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 2a686a5..c87e8b5 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -5382,3 +5382,36 @@ body, color: var(--colour-text-muted); font-size: 0.85rem; } + +/* ===================== VAMPIRE THEME ===================== */ +body.vampire-mode { + --colour-bg: #1a0a0a; + --colour-surface: #2d1515; + --colour-surface-2: #3d2020; + --colour-border: #5c3d3d; + --colour-accent: #c41e3a; + --colour-accent-light: #e84c3d; + --colour-gold: #d4a574; + --colour-text: #f5e6e6; + --colour-text-muted: #b8a8a8; +} + +/* ===================== VAMPIRE TAB BAR ===================== */ +.vampire-tab-bar .tab-button.active { + background: linear-gradient( + 135deg, + var(--colour-accent), + var(--colour-accent-light) + ); + border-color: var(--colour-accent-light); +} + +/* ===================== VAMPIRE PLACEHOLDER ===================== */ +.vampire-placeholder { + align-items: center; + color: var(--colour-text-muted); + display: flex; + font-size: 1.1rem; + justify-content: center; + min-height: 200px; +}