diff --git a/apps/api/src/routes/game.ts b/apps/api/src/routes/game.ts index 575e6ce..9a92e32 100644 --- a/apps/api/src/routes/game.ts +++ b/apps/api/src/routes/game.ts @@ -20,7 +20,11 @@ import { import { Hono } from "hono"; import { defaultBosses } from "../data/bosses.js"; import { defaultEquipmentSets } from "../data/equipmentSets.js"; -import { initialGameState } from "../data/initialState.js"; +import { + initialGameState, + initialGoddessState, + initialVampireState, +} from "../data/initialState.js"; import { dailyRewards } from "../data/loginBonus.js"; import { defaultQuests } from "../data/quests.js"; import { currentSchemaVersion } from "../data/schemaVersion.js"; @@ -1152,17 +1156,29 @@ gameRouter.get("/load", async(context) => { const signature = secret === undefined ? undefined : computeHmac(JSON.stringify(freshState), secret); + const { inGuild, loginStreak } = playerRecord; + const loginBonus = null; + const offlineEssence = 0; + const offlineGold = 0; + const offlineSeconds = 0; + const schemaOutdated = false; + const state = freshState; + const expansionPreview = { + goddess: initialGoddessState(), + vampire: initialVampireState(), + }; return context.json({ - currentSchemaVersion: currentSchemaVersion, - inGuild: playerRecord.inGuild, - loginBonus: null, - loginStreak: playerRecord.loginStreak, - offlineEssence: 0, - offlineGold: 0, - offlineSeconds: 0, - schemaOutdated: false, - signature: signature, - state: freshState, + currentSchemaVersion, + expansionPreview, + inGuild, + loginBonus, + loginStreak, + offlineEssence, + offlineGold, + offlineSeconds, + schemaOutdated, + signature, + state, }); } @@ -1294,8 +1310,13 @@ gameRouter.get("/load", async(context) => { ? undefined : computeHmac(JSON.stringify(state), secret); const inGuild = playerRecord?.inGuild ?? false; + const expansionPreview = { + goddess: initialGoddessState(), + vampire: initialVampireState(), + }; return context.json({ currentSchemaVersion, + expansionPreview, inGuild, loginBonus, loginStreak, @@ -1524,17 +1545,28 @@ gameRouter.post("/reset", async(context) => { const signature = secret === undefined ? undefined : computeHmac(JSON.stringify(freshState), secret); - + const { loginStreak } = playerRecord; + const loginBonus = null; + const offlineEssence = 0; + const offlineGold = 0; + const offlineSeconds = 0; + const schemaOutdated = false; + const state = freshState; + const expansionPreview = { + goddess: initialGoddessState(), + vampire: initialVampireState(), + }; return context.json({ - currentSchemaVersion: currentSchemaVersion, - loginBonus: null, - loginStreak: playerRecord.loginStreak, - offlineEssence: 0, - offlineGold: 0, - offlineSeconds: 0, - schemaOutdated: false, - signature: signature, - state: freshState, + currentSchemaVersion, + expansionPreview, + loginBonus, + loginStreak, + offlineEssence, + offlineGold, + offlineSeconds, + schemaOutdated, + signature, + state, }); } catch (error) { void logger.error( diff --git a/apps/api/src/services/apotheosis.ts b/apps/api/src/services/apotheosis.ts index a628d53..4bba0f5 100644 --- a/apps/api/src/services/apotheosis.ts +++ b/apps/api/src/services/apotheosis.ts @@ -4,7 +4,7 @@ * @license Naomi's Public License * @author Naomi Carrigan */ -import { initialGameState, initialGoddessState } from "../data/initialState.js"; +import { initialGameState } from "../data/initialState.js"; import { defaultTranscendenceUpgrades, } from "../data/transcendenceUpgrades.js"; @@ -48,13 +48,11 @@ const buildPostApotheosisState = ( const freshState = initialGameState(currentState.player, characterName); - // Goddess state: initialised on first apotheosis, preserved on subsequent resets - let goddessSpread: object = {}; - if (apotheosisCount === 1) { - goddessSpread = { goddess: initialGoddessState() }; - } else if (currentState.goddess !== undefined) { - goddessSpread = { goddess: currentState.goddess }; - } + // Goddess state: preserved across resets; never auto-initialised (expansion not yet live) + const goddessSpread: object + = currentState.goddess === undefined + ? {} + : { goddess: currentState.goddess }; const updatedState: GameState = { ...freshState, diff --git a/apps/web/src/components/game/consecrationPanel.tsx b/apps/web/src/components/game/consecrationPanel.tsx index c7215b7..9bcb278 100644 --- a/apps/web/src/components/game/consecrationPanel.tsx +++ b/apps/web/src/components/game/consecrationPanel.tsx @@ -308,6 +308,7 @@ const ConsecrationPanel = (): JSX.Element => { buyConsecrationUpgrade, showConsecrationToast, dismissConsecrationToast, + goddessPreview, } = useGame(); const [ isPending, setIsPending ] = useState(false); @@ -327,7 +328,7 @@ const ConsecrationPanel = (): JSX.Element => { ); } - const { goddess } = state; + const goddess = state.goddess ?? goddessPreview; if (goddess === undefined) { return ( diff --git a/apps/web/src/components/game/disciplesPanel.tsx b/apps/web/src/components/game/disciplesPanel.tsx index 393e5cc..239a25b 100644 --- a/apps/web/src/components/game/disciplesPanel.tsx +++ b/apps/web/src/components/game/disciplesPanel.tsx @@ -199,7 +199,7 @@ const DiscipleCard = ({ * @returns The JSX element. */ const DisciplesPanel = (): JSX.Element => { - const { state, formatNumber } = useGame(); + const { state, formatNumber, goddessPreview } = useGame(); const [ selectedBatch, setSelectedBatch ] = useState(() => { return parseBatchSize(localStorage.getItem("elysium_disciple_batch")); }); @@ -212,7 +212,7 @@ const DisciplesPanel = (): JSX.Element => { ); } - const goddessState = state.goddess; + const goddessState = state.goddess ?? goddessPreview; if (goddessState === undefined) { return (
diff --git a/apps/web/src/components/game/enlightenmentPanel.tsx b/apps/web/src/components/game/enlightenmentPanel.tsx index 6821be1..ad2c6b0 100644 --- a/apps/web/src/components/game/enlightenmentPanel.tsx +++ b/apps/web/src/components/game/enlightenmentPanel.tsx @@ -207,6 +207,7 @@ const EnlightenmentPanel = (): JSX.Element => { buyEnlightenmentUpgrade, showEnlightenmentToast, dismissEnlightenmentToast, + goddessPreview, } = useGame(); const [ isPending, setIsPending ] = useState(false); @@ -226,7 +227,7 @@ const EnlightenmentPanel = (): JSX.Element => { ); } - const { goddess } = state; + const goddess = state.goddess ?? goddessPreview; if (goddess === undefined) { return ( diff --git a/apps/web/src/components/game/gameLayout.tsx b/apps/web/src/components/game/gameLayout.tsx index 7d1610a..d2550ee 100644 --- a/apps/web/src/components/game/gameLayout.tsx +++ b/apps/web/src/components/game/gameLayout.tsx @@ -231,6 +231,8 @@ const GameLayout = (): JSX.Element => { loginBonus, dismissLoginBonus, schemaOutdated, + goddessPreview, + vampirePreview, } = useGame(); const [ activeMode, setActiveMode ] = useState(readSavedMode); const [ activeTab, setActiveTab ] = useState("adventurers"); @@ -276,6 +278,13 @@ const GameLayout = (): JSX.Element => { const codexBadgeCount = pendingCodexEntryIds.length; const storyBadgeCount = pendingStoryChapterIds.length; + const isGoddessPreview = activeMode === "goddess" + && state.goddess === undefined + && goddessPreview !== undefined; + const isVampirePreview = activeMode === "vampire" + && state.vampire === undefined + && vampirePreview !== undefined; + const isExpansionPreview = isGoddessPreview || isVampirePreview; function handleOpenEditProfile(): void { setEditingProfile(true); @@ -342,38 +351,20 @@ const GameLayout = (): JSX.Element => {
} -
+
+ {activeMode !== "mortal" + &&
+

{"✨ Expansion Coming Soon~"}

+
+ } {activeMode === "mortal" && activeTab === "adventurers" && } {activeMode === "mortal" && activeTab === "upgrades" diff --git a/apps/web/src/components/game/goddessAchievementsPanel.tsx b/apps/web/src/components/game/goddessAchievementsPanel.tsx index 9362433..68939fe 100644 --- a/apps/web/src/components/game/goddessAchievementsPanel.tsx +++ b/apps/web/src/components/game/goddessAchievementsPanel.tsx @@ -178,7 +178,7 @@ const GoddessAchievementCard = ({ * @returns The JSX element. */ const GoddessAchievementsPanel = (): JSX.Element => { - const { state, formatNumber } = useGame(); + const { state, formatNumber, goddessPreview } = useGame(); const [ showLocked, setShowLocked ] = useState(true); if (state === null) { @@ -189,7 +189,7 @@ const GoddessAchievementsPanel = (): JSX.Element => { ); } - const { goddess } = state; + const goddess = state.goddess ?? goddessPreview; if (goddess === undefined) { return ( diff --git a/apps/web/src/components/game/goddessBossPanel.tsx b/apps/web/src/components/game/goddessBossPanel.tsx index 7c5fd16..b671381 100644 --- a/apps/web/src/components/game/goddessBossPanel.tsx +++ b/apps/web/src/components/game/goddessBossPanel.tsx @@ -305,6 +305,7 @@ const GoddessBossPanel = (): JSX.Element => { dismissGoddessBattle, formatNumber, formatInteger, + goddessPreview, } = useGame(); const [ challengingBossId, setChallengingBossId ] = useState( @@ -322,7 +323,7 @@ const GoddessBossPanel = (): JSX.Element => { ); } - const { goddess } = state; + const goddess = state.goddess ?? goddessPreview; if (goddess === undefined) { return ( diff --git a/apps/web/src/components/game/goddessCraftingPanel.tsx b/apps/web/src/components/game/goddessCraftingPanel.tsx index b7cb9a1..e9b91f5 100644 --- a/apps/web/src/components/game/goddessCraftingPanel.tsx +++ b/apps/web/src/components/game/goddessCraftingPanel.tsx @@ -6,6 +6,7 @@ */ /* eslint-disable max-lines-per-function -- Complex component with many render paths */ /* eslint-disable max-nested-callbacks -- Nested recipe/material maps require nesting */ +/* eslint-disable complexity -- Expansion preview fallback adds necessary branching */ import { type JSX, useState } from "react"; import { useGame } from "../../context/gameContext.js"; @@ -25,7 +26,7 @@ const bonusLabel: Record = { * @returns The JSX element. */ const GoddessCraftingPanel = (): JSX.Element => { - const { state, craftGoddessRecipe, formatNumber } = useGame(); + const { state, craftGoddessRecipe, formatNumber, goddessPreview } = useGame(); const [ activeZoneId, setActiveZoneId ] = useState(() => { return ( sessionStorage.getItem("elysium_goddess_craft_zone") @@ -42,7 +43,7 @@ const GoddessCraftingPanel = (): JSX.Element => { ); } - const { goddess } = state; + const goddess = state.goddess ?? goddessPreview; const playerMaterials = goddess?.exploration.materials ?? []; const craftedIds = goddess?.exploration.craftedRecipeIds ?? []; diff --git a/apps/web/src/components/game/goddessEquipmentPanel.tsx b/apps/web/src/components/game/goddessEquipmentPanel.tsx index 0e915fe..abac683 100644 --- a/apps/web/src/components/game/goddessEquipmentPanel.tsx +++ b/apps/web/src/components/game/goddessEquipmentPanel.tsx @@ -191,7 +191,7 @@ type TabFilter = "all" | GoddessEquipmentType; * @returns The JSX element. */ export const GoddessEquipmentPanel = (): JSX.Element => { - const { state, formatNumber } = useGame(); + const { state, formatNumber, goddessPreview } = useGame(); const [ activeTab, setActiveTab ] = useState("all"); if (state === null) { @@ -202,7 +202,8 @@ export const GoddessEquipmentPanel = (): JSX.Element => { const divinity = state.resources.divinity ?? 0; const stardust = state.resources.stardust ?? 0; - const equipment = state.goddess?.equipment ?? []; + const goddess = state.goddess ?? goddessPreview; + const equipment = goddess?.equipment ?? []; const filteredEquipment = activeTab === "all" ? equipment diff --git a/apps/web/src/components/game/goddessExplorationPanel.tsx b/apps/web/src/components/game/goddessExplorationPanel.tsx index 451313c..ae2d3da 100644 --- a/apps/web/src/components/game/goddessExplorationPanel.tsx +++ b/apps/web/src/components/game/goddessExplorationPanel.tsx @@ -85,6 +85,7 @@ const GoddessExplorationPanel = (): JSX.Element => { startGoddessExploration, collectGoddessExploration, formatNumber, + goddessPreview, } = useGame(); const [ activeZoneId, setActiveZoneId ] = useState(() => { return ( @@ -160,7 +161,7 @@ const GoddessExplorationPanel = (): JSX.Element => { ); } - const { goddess } = state; + const goddess = state.goddess ?? goddessPreview; const explorationState = goddess?.exploration; const goddessZones = goddess?.zones ?? []; diff --git a/apps/web/src/components/game/goddessQuestsPanel.tsx b/apps/web/src/components/game/goddessQuestsPanel.tsx index 2dc233f..6846c4f 100644 --- a/apps/web/src/components/game/goddessQuestsPanel.tsx +++ b/apps/web/src/components/game/goddessQuestsPanel.tsx @@ -141,7 +141,7 @@ const GoddessQuestCard = ({ * @returns The JSX element. */ const GoddessQuestsPanel = (): JSX.Element => { - const { state } = useGame(); + const { state, goddessPreview } = useGame(); const [ activeZoneId, setActiveZoneId ] = useState(() => { return sessionStorage.getItem("elysium_goddess_quest_zone") ?? "goddess_celestial_garden"; @@ -155,7 +155,7 @@ const GoddessQuestsPanel = (): JSX.Element => { ); } - const goddessState = state.goddess; + const goddessState = state.goddess ?? goddessPreview; if (goddessState === undefined) { return (
diff --git a/apps/web/src/components/game/goddessUpgradesPanel.tsx b/apps/web/src/components/game/goddessUpgradesPanel.tsx index 949516b..919ff0c 100644 --- a/apps/web/src/components/game/goddessUpgradesPanel.tsx +++ b/apps/web/src/components/game/goddessUpgradesPanel.tsx @@ -164,7 +164,7 @@ const GoddessUpgradeCard = ({ * @returns The JSX element. */ export const GoddessUpgradesPanel = (): JSX.Element => { - const { state, formatNumber } = useGame(); + const { state, formatNumber, goddessPreview } = useGame(); if (state === null) { return

{"Loading..."}

; @@ -174,7 +174,8 @@ export const GoddessUpgradesPanel = (): JSX.Element => { const divinity = state.resources.divinity ?? 0; const stardust = state.resources.stardust ?? 0; - const upgrades = state.goddess?.upgrades ?? []; + const goddess = state.goddess ?? goddessPreview; + const upgrades = goddess?.upgrades ?? []; const purchased = upgrades.filter((upgrade) => { return upgrade.purchased; diff --git a/apps/web/src/components/game/goddessZonesPanel.tsx b/apps/web/src/components/game/goddessZonesPanel.tsx index 9c98de1..4e03ba9 100644 --- a/apps/web/src/components/game/goddessZonesPanel.tsx +++ b/apps/web/src/components/game/goddessZonesPanel.tsx @@ -81,7 +81,7 @@ const GoddessZoneCard = ({ * @returns The JSX element. */ const GoddessZonesPanel = (): JSX.Element => { - const { state } = useGame(); + const { state, goddessPreview } = useGame(); if (state === null) { return ( @@ -91,7 +91,7 @@ const GoddessZonesPanel = (): JSX.Element => { ); } - const { goddess } = state; + const goddess = state.goddess ?? goddessPreview; if (goddess === undefined) { return ( diff --git a/apps/web/src/components/game/vampireAchievementsPanel.tsx b/apps/web/src/components/game/vampireAchievementsPanel.tsx index d644b8f..a5449c0 100644 --- a/apps/web/src/components/game/vampireAchievementsPanel.tsx +++ b/apps/web/src/components/game/vampireAchievementsPanel.tsx @@ -178,7 +178,7 @@ const VampireAchievementCard = ({ * @returns The JSX element. */ const VampireAchievementsPanel = (): JSX.Element => { - const { state, formatNumber } = useGame(); + const { state, formatNumber, vampirePreview } = useGame(); const [ showLocked, setShowLocked ] = useState(true); if (state === null) { @@ -189,7 +189,7 @@ const VampireAchievementsPanel = (): JSX.Element => { ); } - const { vampire } = state; + const vampire = state.vampire ?? vampirePreview; if (vampire === undefined) { return ( diff --git a/apps/web/src/components/game/vampireAwakeningPanel.tsx b/apps/web/src/components/game/vampireAwakeningPanel.tsx index 66f843c..4080cac 100644 --- a/apps/web/src/components/game/vampireAwakeningPanel.tsx +++ b/apps/web/src/components/game/vampireAwakeningPanel.tsx @@ -189,6 +189,7 @@ const VampireAwakeningPanel = (): JSX.Element => { formatInteger, awaken, buyAwakeningUpgrade, + vampirePreview, } = useGame(); const [ isPending, setIsPending ] = useState(false); @@ -208,7 +209,7 @@ const VampireAwakeningPanel = (): JSX.Element => { ); } - const { vampire } = state; + const vampire = state.vampire ?? vampirePreview; if (vampire === undefined) { return ( diff --git a/apps/web/src/components/game/vampireBossPanel.tsx b/apps/web/src/components/game/vampireBossPanel.tsx index 66dd52a..6a0cc0a 100644 --- a/apps/web/src/components/game/vampireBossPanel.tsx +++ b/apps/web/src/components/game/vampireBossPanel.tsx @@ -306,6 +306,7 @@ const VampireBossPanel = (): JSX.Element => { dismissVampireBattle, formatNumber, formatInteger, + vampirePreview, } = useGame(); const [ challengingBossId, setChallengingBossId ] = useState( @@ -323,7 +324,7 @@ const VampireBossPanel = (): JSX.Element => { ); } - const { vampire } = state; + const vampire = state.vampire ?? vampirePreview; if (vampire === undefined) { return ( diff --git a/apps/web/src/components/game/vampireCraftingPanel.tsx b/apps/web/src/components/game/vampireCraftingPanel.tsx index 34397f7..146de8b 100644 --- a/apps/web/src/components/game/vampireCraftingPanel.tsx +++ b/apps/web/src/components/game/vampireCraftingPanel.tsx @@ -24,7 +24,7 @@ const bonusLabel: Record = { * @returns The JSX element. */ const VampireCraftingPanel = (): JSX.Element => { - const { state, craftVampireRecipe, formatNumber } = useGame(); + const { state, craftVampireRecipe, formatNumber, vampirePreview } = useGame(); const [ activeZoneId, setActiveZoneId ] = useState(() => { return ( sessionStorage.getItem("elysium_vampire_craft_zone") @@ -41,7 +41,7 @@ const VampireCraftingPanel = (): JSX.Element => { ); } - const { vampire } = state; + const vampire = state.vampire ?? vampirePreview; if (vampire === undefined) { return ( diff --git a/apps/web/src/components/game/vampireEquipmentPanel.tsx b/apps/web/src/components/game/vampireEquipmentPanel.tsx index e24e47d..5c4ff37 100644 --- a/apps/web/src/components/game/vampireEquipmentPanel.tsx +++ b/apps/web/src/components/game/vampireEquipmentPanel.tsx @@ -192,7 +192,7 @@ type TabFilter = "all" | VampireEquipmentType; * @returns The JSX element. */ const VampireEquipmentPanel = (): JSX.Element => { - const { state, formatNumber } = useGame(); + const { state, formatNumber, vampirePreview } = useGame(); const [ activeTab, setActiveTab ] = useState("all"); if (state === null) { @@ -203,7 +203,8 @@ const VampireEquipmentPanel = (): JSX.Element => { ); } - const { resources, vampire } = state; + const { resources } = state; + const vampire = state.vampire ?? vampirePreview; if (vampire === undefined) { return ( diff --git a/apps/web/src/components/game/vampireExplorationPanel.tsx b/apps/web/src/components/game/vampireExplorationPanel.tsx index 4191b42..907da34 100644 --- a/apps/web/src/components/game/vampireExplorationPanel.tsx +++ b/apps/web/src/components/game/vampireExplorationPanel.tsx @@ -85,6 +85,7 @@ const VampireExplorationPanel = (): JSX.Element => { startVampireExploration, collectVampireExploration, formatNumber, + vampirePreview, } = useGame(); const [ activeZoneId, setActiveZoneId ] = useState(() => { return ( @@ -160,7 +161,7 @@ const VampireExplorationPanel = (): JSX.Element => { ); } - const { vampire } = state; + const vampire = state.vampire ?? vampirePreview; if (vampire === undefined) { return ( diff --git a/apps/web/src/components/game/vampireQuestsPanel.tsx b/apps/web/src/components/game/vampireQuestsPanel.tsx index 4d8b29b..7786797 100644 --- a/apps/web/src/components/game/vampireQuestsPanel.tsx +++ b/apps/web/src/components/game/vampireQuestsPanel.tsx @@ -6,6 +6,7 @@ */ /* eslint-disable max-lines-per-function -- Complex component with many render paths */ /* eslint-disable react/no-multi-comp -- QuestCard sub-component is tightly coupled */ +/* eslint-disable complexity -- Expansion preview fallback adds necessary branching */ import { useState, type JSX } from "react"; import { useGame } from "../../context/gameContext.js"; import type { @@ -140,7 +141,7 @@ const VampireQuestCard = ({ * @returns The JSX element. */ const VampireQuestsPanel = (): JSX.Element => { - const { state, toggleVampireAutoQuest } = useGame(); + const { state, toggleVampireAutoQuest, vampirePreview } = useGame(); const [ activeZoneId, setActiveZoneId ] = useState(() => { return sessionStorage.getItem("elysium_vampire_quest_zone") ?? "vampire_haunted_catacombs"; @@ -154,7 +155,7 @@ const VampireQuestsPanel = (): JSX.Element => { ); } - const vampireState = state.vampire; + const vampireState = state.vampire ?? vampirePreview; if (vampireState === undefined) { return (
diff --git a/apps/web/src/components/game/vampireSiringPanel.tsx b/apps/web/src/components/game/vampireSiringPanel.tsx index ee39fef..ecd2b32 100644 --- a/apps/web/src/components/game/vampireSiringPanel.tsx +++ b/apps/web/src/components/game/vampireSiringPanel.tsx @@ -305,6 +305,7 @@ const VampireSiringPanel = (): JSX.Element => { formatNumber, sire, buySiringUpgrade, + vampirePreview, } = useGame(); const [ isPending, setIsPending ] = useState(false); @@ -324,7 +325,7 @@ const VampireSiringPanel = (): JSX.Element => { ); } - const { vampire } = state; + const vampire = state.vampire ?? vampirePreview; if (vampire === undefined) { return ( diff --git a/apps/web/src/components/game/vampireThrallsPanel.tsx b/apps/web/src/components/game/vampireThrallsPanel.tsx index 3d75215..97474a6 100644 --- a/apps/web/src/components/game/vampireThrallsPanel.tsx +++ b/apps/web/src/components/game/vampireThrallsPanel.tsx @@ -198,7 +198,9 @@ const ThrallCard = ({ * @returns The JSX element. */ const VampireThrallsPanel = (): JSX.Element => { - const { state, formatNumber, toggleVampireAutoThrall } = useGame(); + const { + state, formatNumber, toggleVampireAutoThrall, vampirePreview, + } = useGame(); const [ selectedBatch, setSelectedBatch ] = useState(() => { return parseBatchSize(localStorage.getItem("elysium_thrall_batch")); }); @@ -211,7 +213,7 @@ const VampireThrallsPanel = (): JSX.Element => { ); } - const vampireState = state.vampire; + const vampireState = state.vampire ?? vampirePreview; if (vampireState === undefined) { return (
diff --git a/apps/web/src/components/game/vampireUpgradesPanel.tsx b/apps/web/src/components/game/vampireUpgradesPanel.tsx index d6255db..9d54be1 100644 --- a/apps/web/src/components/game/vampireUpgradesPanel.tsx +++ b/apps/web/src/components/game/vampireUpgradesPanel.tsx @@ -167,7 +167,7 @@ const VampireUpgradeCard = ({ * @returns The JSX element. */ const VampireUpgradesPanel = (): JSX.Element => { - const { state, formatNumber } = useGame(); + const { state, formatNumber, vampirePreview } = useGame(); if (state === null) { return ( @@ -177,7 +177,8 @@ const VampireUpgradesPanel = (): JSX.Element => { ); } - const { resources, vampire } = state; + const { resources } = state; + const vampire = state.vampire ?? vampirePreview; if (vampire === undefined) { return ( diff --git a/apps/web/src/components/game/vampireZonesPanel.tsx b/apps/web/src/components/game/vampireZonesPanel.tsx index e42826f..cc0450e 100644 --- a/apps/web/src/components/game/vampireZonesPanel.tsx +++ b/apps/web/src/components/game/vampireZonesPanel.tsx @@ -81,7 +81,7 @@ const VampireZoneCard = ({ * @returns The JSX element. */ const VampireZonesPanel = (): JSX.Element => { - const { state } = useGame(); + const { state, vampirePreview } = useGame(); if (state === null) { return ( @@ -91,7 +91,7 @@ const VampireZonesPanel = (): JSX.Element => { ); } - const { vampire } = state; + const vampire = state.vampire ?? vampirePreview; if (vampire === undefined) { return ( diff --git a/apps/web/src/context/gameContext.tsx b/apps/web/src/context/gameContext.tsx index e2be73a..ac79ee6 100644 --- a/apps/web/src/context/gameContext.tsx +++ b/apps/web/src/context/gameContext.tsx @@ -15,6 +15,7 @@ import { STORY_CHAPTERS, type Achievement, type ApotheosisResponse, + type AwakeningResponse, type BossChallengeResponse, type ConsecrationResponse, type EnlightenmentResponse, @@ -22,14 +23,15 @@ import { type GameState, type GoddessBossChallengeResponse, type GoddessExploreCollectResponse, - type AwakeningResponse, - type SiringResponse, - type VampireBossChallengeResponse, - type VampireExploreCollectResponse, + type GoddessState, type LoginBonusResult, type NumberFormat, type Quest, + type SiringResponse, type TranscendenceResponse, + type VampireBossChallengeResponse, + type VampireExploreCollectResponse, + type VampireState, computeUnlockedCompanionIds, isStoryChapterUnlocked, } from "@elysium/types"; @@ -865,6 +867,18 @@ interface GameContextValue { * Toggle the vampire auto-thrall setting on/off. */ toggleVampireAutoThrall: ()=> void; + + /** + * Initial goddess state for expansion preview display. + * Never saved to game state. + */ + goddessPreview: GoddessState | undefined; + + /** + * Initial vampire state for expansion preview display. + * Never saved to game state. + */ + vampirePreview: VampireState | undefined; } export interface BattleResult { @@ -953,6 +967,12 @@ export const GameProvider = ({ const [ saveSchemaVersion, setSaveSchemaVersion ] = useState(0); const [ currentSchemaVersion, setCurrentSchemaVersion ] = useState(0); const [ inGuild, setInGuild ] = useState(false); + const [ goddessPreview, setGoddessPreview ] = useState< + GoddessState | undefined + >(undefined); + const [ vampirePreview, setVampirePreview ] = useState< + VampireState | undefined + >(undefined); const [ unlockedCodexEntryIds, setUnlockedCodexEntryIds ] = useState< Array >([]); @@ -991,6 +1011,8 @@ export const GameProvider = ({ setSaveSchemaVersion(data.state.schemaVersion ?? 0); setCurrentSchemaVersion(data.currentSchemaVersion); setInGuild(data.inGuild); + setGoddessPreview(data.expansionPreview.goddess); + setVampirePreview(data.expansionPreview.vampire); // Fetch number format preference from profile (fire-and-forget, non-blocking) void fetch(`/api/profile/${data.state.player.discordId}`). @@ -1051,6 +1073,8 @@ export const GameProvider = ({ setSaveSchemaVersion(data.state.schemaVersion ?? 0); setCurrentSchemaVersion(data.currentSchemaVersion); setInGuild(data.inGuild); + setGoddessPreview(data.expansionPreview.goddess); + setVampirePreview(data.expansionPreview.vampire); } catch (error_: unknown) { setError( error_ instanceof Error @@ -3577,6 +3601,8 @@ export const GameProvider = ({ setOfflineGold(0); setOfflineEssence(0); setLoginBonus(null); + setGoddessPreview(data.expansionPreview.goddess); + setVampirePreview(data.expansionPreview.vampire); if (data.signature !== undefined) { signatureReference.current = data.signature; localStorage.setItem("elysium_save_signature", data.signature); @@ -3697,6 +3723,8 @@ export const GameProvider = ({ setOfflineGold(0); setOfflineEssence(0); setLoginBonus(null); + setGoddessPreview(data.expansionPreview.goddess); + setVampirePreview(data.expansionPreview.vampire); if (data.signature !== undefined) { signatureReference.current = data.signature; localStorage.setItem("elysium_save_signature", data.signature); @@ -3796,6 +3824,7 @@ export const GameProvider = ({ formatInteger, formatNumber, goddessBattleResult, + goddessPreview, handleClick, inGuild, isLoading, @@ -3841,6 +3870,7 @@ export const GameProvider = ({ unlockedCodexEntryIds, unlockedStoryChapterIds, vampireBattleResult, + vampirePreview, }; }, [ apotheosis, @@ -3907,6 +3937,7 @@ export const GameProvider = ({ formatInteger, formatNumber, goddessBattleResult, + goddessPreview, handleClick, inGuild, isLoading, @@ -3951,6 +3982,7 @@ export const GameProvider = ({ unlockedCodexEntryIds, unlockedStoryChapterIds, vampireBattleResult, + vampirePreview, ]); return ( diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index c87e8b5..b9a7d72 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -5415,3 +5415,29 @@ body.vampire-mode { justify-content: center; min-height: 200px; } + +/* ===================== EXPANSION COMING SOON ===================== */ +.expansion-coming-soon { + background: linear-gradient( + 135deg, + var(--colour-surface), + var(--colour-surface-2) + ); + border: 2px solid var(--colour-accent); + border-radius: var(--radius-lg); + color: var(--colour-accent-light); + font-size: 1.2rem; + font-weight: 600; + margin-bottom: 1.5rem; + padding: 1rem 1.5rem; + text-align: center; +} + +/* ===================== EXPANSION PREVIEW ===================== */ +.expansion-preview button, +.expansion-preview input, +.expansion-preview select { + cursor: not-allowed; + opacity: 0.4; + pointer-events: none; +} diff --git a/packages/types/src/interfaces/api.ts b/packages/types/src/interfaces/api.ts index b4d8b30..703afb0 100644 --- a/packages/types/src/interfaces/api.ts +++ b/packages/types/src/interfaces/api.ts @@ -11,9 +11,11 @@ import type { EquipmentType, } from "./equipment.js"; import type { GameState } from "./gameState.js"; +import type { GoddessState } from "./goddessState.js"; import type { Player } from "./player.js"; import type { ProfileSettings } from "./profileSettings.js"; import type { CompletedChapter } from "./story.js"; +import type { VampireState } from "./vampireState.js"; interface AuthResponse { token: string; @@ -114,6 +116,14 @@ interface LoadResponse { * The current expected schema version from the server. */ currentSchemaVersion: number; + + /** + * Initial expansion states for preview display — never saved to game state. + */ + expansionPreview: { + goddess: GoddessState; + vampire: VampireState; + }; } interface BossChallengeRequest {