generated from nhcarrigan/template
feat: expansion coming-soon preview with save-safe display data
Add an expansion preview system so the Goddess and Vampire panels render their full content (zones, bosses, thralls, achievements, etc.) even when the expansion has not been unlocked, with all interactive elements visually disabled. - API /load now returns expansionPreview alongside game state, populated from initialGoddessState() and initialVampireState() — never part of the saved blob - LoadResponse type updated with expansionPreview field - gameContext exposes goddessPreview and vampirePreview, stored in separate state vars that never touch stateReference so saves are never polluted - gameLayout applies expansion-preview CSS class when viewing a locked expansion with preview data available, and shows the coming-soon banner - All 22 expansion panels updated to use state.vampire ?? vampirePreview and state.goddess ?? goddessPreview for display - CSS disables all buttons/inputs/selects inside .expansion-preview - apotheosis service patched to never auto-initialise goddess state — expansion remains locked until explicitly released
This commit is contained in:
+53
-21
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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<BatchSize>(() => {
|
||||
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 (
|
||||
<section className="panel">
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -231,6 +231,8 @@ const GameLayout = (): JSX.Element => {
|
||||
loginBonus,
|
||||
dismissLoginBonus,
|
||||
schemaOutdated,
|
||||
goddessPreview,
|
||||
vampirePreview,
|
||||
} = useGame();
|
||||
const [ activeMode, setActiveMode ] = useState<Mode>(readSavedMode);
|
||||
const [ activeTab, setActiveTab ] = useState<Tab>("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 => {
|
||||
<main className="game-content">
|
||||
<nav className="mode-bar">
|
||||
{modes.map((mode) => {
|
||||
const apotheosisCount = state.apotheosis?.count ?? 0;
|
||||
const eternalSovereigntyCount
|
||||
= state.vampire?.eternalSovereignty.count ?? 0;
|
||||
const vampireLocked
|
||||
= mode === "vampire" && apotheosisCount === 0;
|
||||
const goddessLocked
|
||||
= mode === "goddess" && eternalSovereigntyCount === 0;
|
||||
const isLocked = vampireLocked || goddessLocked;
|
||||
function handleModeClick(): void {
|
||||
if (!isLocked) {
|
||||
handleSetMode(mode);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<button
|
||||
className={`mode-button${activeMode === mode
|
||||
? " active"
|
||||
: ""}${isLocked
|
||||
? " locked"
|
||||
: ""}`}
|
||||
disabled={isLocked}
|
||||
key={mode}
|
||||
onClick={handleModeClick}
|
||||
title={isLocked
|
||||
? "Not yet unlocked"
|
||||
: modeLabels[mode]}
|
||||
title={modeLabels[mode]}
|
||||
type="button"
|
||||
>
|
||||
{modeLabels[mode]}
|
||||
{isLocked
|
||||
? <span className="mode-lock">{"🔒"}</span>
|
||||
: null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -450,7 +441,16 @@ const GameLayout = (): JSX.Element => {
|
||||
</nav>
|
||||
}
|
||||
|
||||
<div className="tab-content">
|
||||
<div className={`tab-content${
|
||||
isExpansionPreview
|
||||
? " expansion-preview"
|
||||
: ""
|
||||
}`}>
|
||||
{activeMode !== "mortal"
|
||||
&& <div className="expansion-coming-soon">
|
||||
<p>{"✨ Expansion Coming Soon~"}</p>
|
||||
</div>
|
||||
}
|
||||
{activeMode === "mortal" && activeTab === "adventurers"
|
||||
&& <AdventurerPanel />}
|
||||
{activeMode === "mortal" && activeTab === "upgrades"
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -305,6 +305,7 @@ const GoddessBossPanel = (): JSX.Element => {
|
||||
dismissGoddessBattle,
|
||||
formatNumber,
|
||||
formatInteger,
|
||||
goddessPreview,
|
||||
} = useGame();
|
||||
|
||||
const [ challengingBossId, setChallengingBossId ] = useState<string | null>(
|
||||
@@ -322,7 +323,7 @@ const GoddessBossPanel = (): JSX.Element => {
|
||||
);
|
||||
}
|
||||
|
||||
const { goddess } = state;
|
||||
const goddess = state.goddess ?? goddessPreview;
|
||||
|
||||
if (goddess === undefined) {
|
||||
return (
|
||||
|
||||
@@ -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<string, string> = {
|
||||
* @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 ?? [];
|
||||
|
||||
|
||||
@@ -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<TabFilter>("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
|
||||
|
||||
@@ -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 ?? [];
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<section className="panel">
|
||||
|
||||
@@ -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 <div className="panel"><p>{"Loading..."}</p></div>;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -306,6 +306,7 @@ const VampireBossPanel = (): JSX.Element => {
|
||||
dismissVampireBattle,
|
||||
formatNumber,
|
||||
formatInteger,
|
||||
vampirePreview,
|
||||
} = useGame();
|
||||
|
||||
const [ challengingBossId, setChallengingBossId ] = useState<string | null>(
|
||||
@@ -323,7 +324,7 @@ const VampireBossPanel = (): JSX.Element => {
|
||||
);
|
||||
}
|
||||
|
||||
const { vampire } = state;
|
||||
const vampire = state.vampire ?? vampirePreview;
|
||||
|
||||
if (vampire === undefined) {
|
||||
return (
|
||||
|
||||
@@ -24,7 +24,7 @@ const bonusLabel: Record<string, string> = {
|
||||
* @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 (
|
||||
|
||||
@@ -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<TabFilter>("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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
<section className="panel">
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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<BatchSize>(() => {
|
||||
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 (
|
||||
<section className="panel">
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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<string>
|
||||
>([]);
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user