feat: mode bar, goddess tab row, themed resource dropdown (chunk 5)

Add Mortal/Goddess/Vampire mode selector bar, dynamic second tab row
that swaps per mode, goddess currencies in the resource bar dropdown
(locked pre-apotheosis), full CSS goddess theme with 300ms fade
transition, and localStorage persistence of the active mode.
This commit is contained in:
2026-04-13 15:59:43 -07:00
committed by Naomi Carrigan
parent 0d36b255ee
commit 96d6759661
4 changed files with 341 additions and 50 deletions
+184 -39
View File
@@ -4,9 +4,10 @@
* @license Naomi's Public License * @license Naomi's Public License
* @author Naomi Carrigan * @author Naomi Carrigan
*/ */
/* eslint-disable max-lines -- Complex layout with many conditional renders */
/* eslint-disable max-lines-per-function -- 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 complexity -- Many tab render paths */
import { type JSX, useState } from "react"; import { type JSX, useEffect, useState } from "react";
import { useGame } from "../../context/gameContext.js"; import { useGame } from "../../context/gameContext.js";
import { ResourceBar } from "../ui/resourceBar.js"; import { ResourceBar } from "../ui/resourceBar.js";
import { AboutPanel } from "./aboutPanel.js"; import { AboutPanel } from "./aboutPanel.js";
@@ -41,6 +42,8 @@ import { StoryToast } from "./storyToast.js";
import { TranscendencePanel } from "./transcendencePanel.js"; import { TranscendencePanel } from "./transcendencePanel.js";
import { UpgradePanel } from "./upgradePanel.js"; import { UpgradePanel } from "./upgradePanel.js";
type Mode = "mortal" | "goddess" | "vampire";
type Tab = type Tab =
| "adventurers" | "adventurers"
| "upgrades" | "upgrades"
@@ -62,6 +65,19 @@ type Tab =
| "story" | "story"
| "debug"; | "debug";
type GoddessTab =
| "goddess-zones"
| "goddess-bosses"
| "goddess-quests"
| "disciples"
| "goddess-equipment"
| "goddess-upgrades"
| "consecration"
| "enlightenment"
| "goddess-crafting"
| "goddess-exploration"
| "goddess-achievements";
const baseTabs: Array<{ id: Tab; label: string }> = [ const baseTabs: Array<{ id: Tab; label: string }> = [
{ id: "adventurers", label: "⚔️ Adventurers" }, { id: "adventurers", label: "⚔️ Adventurers" },
{ id: "upgrades", label: "🔧 Upgrades" }, { id: "upgrades", label: "🔧 Upgrades" },
@@ -84,6 +100,40 @@ const baseTabs: Array<{ id: Tab; label: string }> = [
{ id: "debug", label: "🔧 Debug" }, { id: "debug", label: "🔧 Debug" },
]; ];
const goddessTabs: Array<{ id: GoddessTab; label: string }> = [
{ id: "goddess-zones", label: "🌟 Zones" },
{ id: "goddess-bosses", label: "👁️ Bosses" },
{ id: "goddess-quests", label: "📿 Quests" },
{ id: "disciples", label: "🙏 Disciples" },
{ id: "goddess-equipment", label: "🔮 Equipment" },
{ id: "goddess-upgrades", label: "✨ Upgrades" },
{ id: "consecration", label: "🕯️ Consecration" },
{ id: "enlightenment", label: "💫 Enlightenment" },
{ id: "goddess-crafting", label: "⚗️ Crafting" },
{ id: "goddess-exploration", label: "🌌 Exploration" },
{ id: "goddess-achievements", label: "🏆 Achievements" },
];
const modes: Array<Mode> = [ "mortal", "goddess", "vampire" ];
const modeLabels: Record<Mode, string> = {
goddess: "✨ Goddess",
mortal: "⚔️ Mortal",
vampire: "🧛 Vampire",
};
/**
* Reads the saved active mode from localStorage, defaulting to "mortal".
* @returns The saved mode or "mortal".
*/
const readSavedMode = (): Mode => {
const saved = localStorage.getItem("elysium-active-mode");
if (saved === "goddess" || saved === "vampire") {
return saved;
}
return "mortal";
};
/** /**
* Renders the main game layout with tabs and panels. * Renders the main game layout with tabs and panels.
* @returns The JSX element. * @returns The JSX element.
@@ -104,11 +154,18 @@ const GameLayout = (): JSX.Element => {
dismissLoginBonus, dismissLoginBonus,
schemaOutdated, schemaOutdated,
} = useGame(); } = useGame();
const [ activeMode, setActiveMode ] = useState<Mode>(readSavedMode);
const [ activeTab, setActiveTab ] = useState<Tab>("adventurers"); const [ activeTab, setActiveTab ] = useState<Tab>("adventurers");
const [ activeGoddessTab, setActiveGoddessTab ]
= useState<GoddessTab>("goddess-zones");
const [ editingProfile, setEditingProfile ] = useState(false); const [ editingProfile, setEditingProfile ] = useState(false);
const [ dismissedOutdatedWarning, setDismissedOutdatedWarning ] const [ dismissedOutdatedWarning, setDismissedOutdatedWarning ]
= useState(false); = useState(false);
useEffect(() => {
document.body.classList.toggle("goddess-mode", activeMode === "goddess");
}, [ activeMode ]);
if (isLoading) { if (isLoading) {
return ( return (
<div className="loading-screen"> <div className="loading-screen">
@@ -151,6 +208,11 @@ const GameLayout = (): JSX.Element => {
setDismissedOutdatedWarning(true); setDismissedOutdatedWarning(true);
} }
function handleSetMode(mode: Mode): void {
localStorage.setItem("elysium-active-mode", mode);
setActiveMode(mode);
}
return ( return (
<div className="game-layout"> <div className="game-layout">
<ResourceBar <ResourceBar
@@ -197,55 +259,138 @@ const GameLayout = (): JSX.Element => {
</aside> </aside>
<main className="game-content"> <main className="game-content">
<nav className="tab-bar"> <nav className="mode-bar">
{baseTabs.map((tab) => { {modes.map((mode) => {
const { id: tabId, label } = tab; const apotheosisCount = state.apotheosis?.count ?? 0;
function handleTabClick(): void { const goddessLocked = mode === "goddess" && apotheosisCount === 0;
setActiveTab(tabId); const isLocked = goddessLocked || mode === "vampire";
function handleModeClick(): void {
if (!isLocked) {
handleSetMode(mode);
}
} }
return ( return (
<button <button
className={`tab-button ${ className={`mode-button${activeMode === mode
activeTab === tabId ? " active"
? "active" : ""}${isLocked
: "" ? " locked"
}`} : ""}`}
key={tabId} disabled={isLocked}
onClick={handleTabClick} key={mode}
onClick={handleModeClick}
title={isLocked
? "Not yet unlocked"
: modeLabels[mode]}
type="button" type="button"
> >
{label} {modeLabels[mode]}
{tabId === "codex" && codexBadgeCount > 0 {isLocked
&& <span className="tab-badge">{codexBadgeCount}</span> ? <span className="mode-lock">{"🔒"}</span>
} : null}
{tabId === "story" && storyBadgeCount > 0
&& <span className="tab-badge">{storyBadgeCount}</span>
}
</button> </button>
); );
})} })}
</nav> </nav>
{activeMode === "mortal"
? <nav className="tab-bar">
{baseTabs.map((tab) => {
const { id: tabId, label } = tab;
function handleTabClick(): void {
setActiveTab(tabId);
}
return (
<button
className={`tab-button${activeTab === tabId
? " active"
: ""}`}
key={tabId}
onClick={handleTabClick}
type="button"
>
{label}
{tabId === "codex" && codexBadgeCount > 0
&& <span className="tab-badge">{codexBadgeCount}</span>
}
{tabId === "story" && storyBadgeCount > 0
&& <span className="tab-badge">{storyBadgeCount}</span>
}
</button>
);
})}
</nav>
: <nav className="tab-bar goddess-tab-bar">
{goddessTabs.map((tab) => {
const { id: tabId, label } = tab;
function handleGoddessTabClick(): void {
setActiveGoddessTab(tabId);
}
return (
<button
className={`tab-button${activeGoddessTab === tabId
? " active"
: ""}`}
key={tabId}
onClick={handleGoddessTabClick}
type="button"
>
{label}
</button>
);
})}
</nav>
}
<div className="tab-content"> <div className="tab-content">
{activeTab === "adventurers" && <AdventurerPanel />} {activeMode === "mortal" && activeTab === "adventurers"
{activeTab === "upgrades" && <UpgradePanel />} && <AdventurerPanel />}
{activeTab === "quests" && <QuestPanel />} {activeMode === "mortal" && activeTab === "upgrades"
{activeTab === "bosses" && <BossPanel />} && <UpgradePanel />}
{activeTab === "equipment" && <EquipmentPanel />} {activeMode === "mortal" && activeTab === "quests"
{activeTab === "achievements" && <AchievementPanel />} && <QuestPanel />}
{activeTab === "prestige" && <PrestigePanel />} {activeMode === "mortal" && activeTab === "bosses"
{activeTab === "transcendence" && <TranscendencePanel />} && <BossPanel />}
{activeTab === "apotheosis" && <ApotheosisPanel />} {activeMode === "mortal" && activeTab === "equipment"
{activeTab === "exploration" && <ExplorationPanel />} && <EquipmentPanel />}
{activeTab === "crafting" && <CraftingPanel />} {activeMode === "mortal" && activeTab === "achievements"
{activeTab === "statistics" && <StatisticsPanel />} && <AchievementPanel />}
{activeTab === "daily" && <DailyChallengePanel />} {activeMode === "mortal" && activeTab === "prestige"
{activeTab === "companions" && <CompanionPanel />} && <PrestigePanel />}
{activeTab === "character" && <CharacterSheetPanel />} {activeMode === "mortal" && activeTab === "transcendence"
{activeTab === "story" && <StoryPanel />} && <TranscendencePanel />}
{activeTab === "codex" && <CodexPanel />} {activeMode === "mortal" && activeTab === "apotheosis"
{activeTab === "about" && <AboutPanel />} && <ApotheosisPanel />}
{activeTab === "debug" && <DebugPanel />} {activeMode === "mortal" && activeTab === "exploration"
&& <ExplorationPanel />}
{activeMode === "mortal" && activeTab === "crafting"
&& <CraftingPanel />}
{activeMode === "mortal" && activeTab === "statistics"
&& <StatisticsPanel />}
{activeMode === "mortal" && activeTab === "daily"
&& <DailyChallengePanel />}
{activeMode === "mortal" && activeTab === "companions"
&& <CompanionPanel />}
{activeMode === "mortal" && activeTab === "character"
&& <CharacterSheetPanel />}
{activeMode === "mortal" && activeTab === "story"
&& <StoryPanel />}
{activeMode === "mortal" && activeTab === "codex"
&& <CodexPanel />}
{activeMode === "mortal" && activeTab === "about"
&& <AboutPanel />}
{activeMode === "mortal" && activeTab === "debug"
&& <DebugPanel />}
{activeMode === "goddess"
&& <div className="goddess-placeholder">
<p>{"✨ Goddess panels coming soon..."}</p>
</div>
}
{activeMode === "vampire"
&& <div className="goddess-placeholder">
<p>{"🧛 Vampire panels coming soon..."}</p>
</div>
}
</div> </div>
</main> </main>
</div> </div>
+36 -1
View File
@@ -86,7 +86,8 @@ const ResourceBar = ({
const [ isProfileOpen, setIsProfileOpen ] = useState(false); const [ isProfileOpen, setIsProfileOpen ] = useState(false);
const [ isResourcesOpen, setIsResourcesOpen ] = useState(false); const [ isResourcesOpen, setIsResourcesOpen ] = useState(false);
const { gold, essence, crystals } = resources; const { gold, essence, crystals, prayers, divinity, stardust } = resources;
const hasApotheosis = apotheosisCount > 0;
let partyCombatPower = 0; let partyCombatPower = 0;
let goldPerSecond = 0; let goldPerSecond = 0;
let essencePerSecond = 0; let essencePerSecond = 0;
@@ -251,6 +252,40 @@ const ResourceBar = ({
</span> </span>
<span className="resource-label">{"Combat Power"}</span> <span className="resource-label">{"Combat Power"}</span>
</div> </div>
<hr className="resources-divider" />
<div className={`resource${hasApotheosis
? ""
: " resource-locked"}`}>
<span className="resource-icon">{"🙏"}</span>
<span className="resource-value">
{hasApotheosis
? formatNumber(prayers ?? 0)
: "🔒"}
</span>
<span className="resource-label">{"Prayers"}</span>
</div>
<div className={`resource${hasApotheosis
? ""
: " resource-locked"}`}>
<span className="resource-icon">{"✨"}</span>
<span className="resource-value">
{hasApotheosis
? formatNumber(divinity ?? 0)
: "🔒"}
</span>
<span className="resource-label">{"Divinity"}</span>
</div>
<div className={`resource${hasApotheosis
? ""
: " resource-locked"}`}>
<span className="resource-icon">{"⭐"}</span>
<span className="resource-value">
{hasApotheosis
? formatNumber(stardust ?? 0)
: "🔒"}
</span>
<span className="resource-label">{"Stardust"}</span>
</div>
</div> </div>
: null} : null}
</div> </div>
+109
View File
@@ -4678,3 +4678,112 @@ body::before {
margin-top: 0.75rem; margin-top: 0.75rem;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
} }
/* ===================== MODE BAR ===================== */
.mode-bar {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 1rem;
background: var(--colour-surface-2);
border-bottom: 1px solid var(--colour-border);
overflow-x: auto;
transition: background 0.3s, border-color 0.3s;
}
.mode-button {
background: transparent;
border: 1px solid var(--colour-border);
border-radius: var(--radius);
color: var(--colour-text-muted);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
padding: 0.3rem 1rem;
transition: all 0.2s;
white-space: nowrap;
}
.mode-button:hover:not(:disabled) {
background: var(--colour-surface);
color: var(--colour-text);
}
.mode-button.active {
background: var(--colour-accent);
border-color: var(--colour-accent-light);
color: #fff;
}
.mode-button.locked {
cursor: not-allowed;
opacity: 0.45;
}
.mode-lock {
font-size: 0.75em;
margin-left: 0.3rem;
}
/* ===================== GODDESS THEME ===================== */
body.goddess-mode {
--colour-bg: #03080f;
--colour-surface: #081422;
--colour-surface-2: #0b1c30;
--colour-border: #173554;
--colour-accent: #1d6fa6;
--colour-accent-light: #3b9fd6;
--colour-gold: #f5c842;
--colour-text: #e4f2fc;
--colour-text-muted: #7aadcc;
}
body,
.resource-bar,
.mode-bar,
.tab-bar,
.game-content,
.game-sidebar,
.panel,
.panel-header {
transition:
background-color 0.3s,
background 0.3s,
border-color 0.3s,
color 0.3s;
}
/* ===================== GODDESS TAB BAR ===================== */
.goddess-tab-bar .tab-button.active {
background: linear-gradient(
135deg,
var(--colour-accent),
var(--colour-accent-light)
);
border-color: var(--colour-accent-light);
}
/* ===================== RESOURCE LOCKED STATE ===================== */
.resource-locked {
opacity: 0.5;
}
.resource-locked .resource-value {
color: var(--colour-text-muted);
}
.resources-divider {
border: none;
border-top: 1px solid var(--colour-border);
margin: 0.25rem 0;
}
/* ===================== GODDESS PLACEHOLDER ===================== */
.goddess-placeholder {
align-items: center;
color: var(--colour-text-muted);
display: flex;
font-size: 1.1rem;
justify-content: center;
min-height: 200px;
}
+12 -10
View File
@@ -50,16 +50,18 @@ Branch: `feat/goddess`
- [x] Tests for all 6 routes (100% coverage) - [x] Tests for all 6 routes (100% coverage)
- Lint ✅ · Build ✅ · Tests ✅ (100% coverage) - Lint ✅ · Build ✅ · Tests ✅ (100% coverage)
## Chunk 5 — UI: Resource Bar + Mode/Tab Nav ## Chunk 5 — UI: Resource Bar + Mode/Tab Nav ✅ COMPLETE
- [ ] Add goddess currencies to resource bar dropdown (greyed pre-apotheosis) - [x] Add goddess currencies (Prayers, Divinity, Stardust) to resource bar dropdown — locked icon pre-apotheosis, live values post-apotheosis
- [ ] Add **Mode bar** (Row 1) — `Mortal | Goddess | Vampire` — always visible, locked modes show padlock pre-unlock - [x] Add **Mode bar** (Row 1) — `⚔️ Mortal | Goddess | 🧛 Vampire` — always visible, locked modes show 🔒 and are disabled
- [ ] Add **Tab bar** (Row 2) — swaps entirely based on selected mode: - [x] Add **Tab bar** (Row 2) — swaps entirely based on selected mode
- Mortal: Zones · Quests · Adventurers · Equipment · Upgrades · Prestige · Transcendence · Crafting · Exploration · Achievements · Codex · Story · Daily - Mortal: all existing tabs (unchanged)
- Goddess: Zones · Disciples · Quests · Equipment · Upgrades · Consecration · Enlightenment · Crafting · Exploration · Achievements - Goddess: Zones · Bosses · Quests · Disciples · Equipment · Upgrades · Consecration · Enlightenment · Crafting · Exploration · Achievements
- Vampire: *(future — TBD)* - Vampire: placeholder (future)
- [ ] Persist selected mode in game state (survives page reload) - [x] Mode persists across page reloads via localStorage
- [ ] `.goddess-mode` CSS class toggle on root when Goddess mode selected - [x] `body.goddess-mode` CSS class toggled via `useEffect` when Goddess mode active
- [ ] 300ms CSS fade transition between base and goddess themes - [x] 300ms CSS fade transition on major layout elements
- [x] Goddess theme: dark navy bg, divine blue accent, gold/white accents
- Lint ✅ · Build ✅ · Tests ✅ (100% coverage)
## Chunk 6 — UI: Goddess Panels ## Chunk 6 — UI: Goddess Panels
- [ ] `GoddessZonesPanel` — zones with lock states - [ ] `GoddessZonesPanel` — zones with lock states