From 96d6759661cee78add09b41e92c41564c33a3430 Mon Sep 17 00:00:00 2001 From: Hikari Date: Mon, 13 Apr 2026 15:59:43 -0700 Subject: [PATCH] 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. --- apps/web/src/components/game/gameLayout.tsx | 223 ++++++++++++++++---- apps/web/src/components/ui/resourceBar.tsx | 37 +++- apps/web/src/styles.css | 109 ++++++++++ goddess-todo.md | 22 +- 4 files changed, 341 insertions(+), 50 deletions(-) diff --git a/apps/web/src/components/game/gameLayout.tsx b/apps/web/src/components/game/gameLayout.tsx index 90aaabc..ca6dff6 100644 --- a/apps/web/src/components/game/gameLayout.tsx +++ b/apps/web/src/components/game/gameLayout.tsx @@ -4,9 +4,10 @@ * @license Naomi's Public License * @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 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 { ResourceBar } from "../ui/resourceBar.js"; import { AboutPanel } from "./aboutPanel.js"; @@ -41,6 +42,8 @@ import { StoryToast } from "./storyToast.js"; import { TranscendencePanel } from "./transcendencePanel.js"; import { UpgradePanel } from "./upgradePanel.js"; +type Mode = "mortal" | "goddess" | "vampire"; + type Tab = | "adventurers" | "upgrades" @@ -62,6 +65,19 @@ type Tab = | "story" | "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 }> = [ { id: "adventurers", label: "⚔️ Adventurers" }, { id: "upgrades", label: "🔧 Upgrades" }, @@ -84,6 +100,40 @@ const baseTabs: Array<{ id: Tab; label: string }> = [ { 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 = [ "mortal", "goddess", "vampire" ]; + +const modeLabels: Record = { + 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. * @returns The JSX element. @@ -104,11 +154,18 @@ const GameLayout = (): JSX.Element => { dismissLoginBonus, schemaOutdated, } = useGame(); + const [ activeMode, setActiveMode ] = useState(readSavedMode); const [ activeTab, setActiveTab ] = useState("adventurers"); + const [ activeGoddessTab, setActiveGoddessTab ] + = useState("goddess-zones"); const [ editingProfile, setEditingProfile ] = useState(false); const [ dismissedOutdatedWarning, setDismissedOutdatedWarning ] = useState(false); + useEffect(() => { + document.body.classList.toggle("goddess-mode", activeMode === "goddess"); + }, [ activeMode ]); + if (isLoading) { return (
@@ -151,6 +208,11 @@ const GameLayout = (): JSX.Element => { setDismissedOutdatedWarning(true); } + function handleSetMode(mode: Mode): void { + localStorage.setItem("elysium-active-mode", mode); + setActiveMode(mode); + } + return (
{
-
diff --git a/apps/web/src/components/ui/resourceBar.tsx b/apps/web/src/components/ui/resourceBar.tsx index f992b29..4a067b6 100644 --- a/apps/web/src/components/ui/resourceBar.tsx +++ b/apps/web/src/components/ui/resourceBar.tsx @@ -86,7 +86,8 @@ const ResourceBar = ({ const [ isProfileOpen, setIsProfileOpen ] = 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 goldPerSecond = 0; let essencePerSecond = 0; @@ -251,6 +252,40 @@ const ResourceBar = ({ {"Combat Power"}
+
+
+ {"🙏"} + + {hasApotheosis + ? formatNumber(prayers ?? 0) + : "🔒"} + + {"Prayers"} +
+
+ {"✨"} + + {hasApotheosis + ? formatNumber(divinity ?? 0) + : "🔒"} + + {"Divinity"} +
+
+ {"⭐"} + + {hasApotheosis + ? formatNumber(stardust ?? 0) + : "🔒"} + + {"Stardust"} +
: null} diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 8738d15..1c0e9a6 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -4678,3 +4678,112 @@ body::before { margin-top: 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; +} diff --git a/goddess-todo.md b/goddess-todo.md index b5e0a95..6737275 100644 --- a/goddess-todo.md +++ b/goddess-todo.md @@ -50,16 +50,18 @@ Branch: `feat/goddess` - [x] Tests for all 6 routes (100% coverage) - Lint ✅ · Build ✅ · Tests ✅ (100% coverage) -## Chunk 5 — UI: Resource Bar + Mode/Tab Nav -- [ ] Add goddess currencies to resource bar dropdown (greyed pre-apotheosis) -- [ ] Add **Mode bar** (Row 1) — `Mortal | Goddess | Vampire` — always visible, locked modes show padlock pre-unlock -- [ ] 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 - - Goddess: Zones · Disciples · Quests · Equipment · Upgrades · Consecration · Enlightenment · Crafting · Exploration · Achievements - - Vampire: *(future — TBD)* -- [ ] Persist selected mode in game state (survives page reload) -- [ ] `.goddess-mode` CSS class toggle on root when Goddess mode selected -- [ ] 300ms CSS fade transition between base and goddess themes +## Chunk 5 — UI: Resource Bar + Mode/Tab Nav ✅ COMPLETE +- [x] Add goddess currencies (Prayers, Divinity, Stardust) to resource bar dropdown — locked icon pre-apotheosis, live values post-apotheosis +- [x] Add **Mode bar** (Row 1) — `⚔️ Mortal | ✨ Goddess | 🧛 Vampire` — always visible, locked modes show 🔒 and are disabled +- [x] Add **Tab bar** (Row 2) — swaps entirely based on selected mode + - Mortal: all existing tabs (unchanged) + - Goddess: Zones · Bosses · Quests · Disciples · Equipment · Upgrades · Consecration · Enlightenment · Crafting · Exploration · Achievements + - Vampire: placeholder (future) +- [x] Mode persists across page reloads via localStorage +- [x] `body.goddess-mode` CSS class toggled via `useEffect` when Goddess mode active +- [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 - [ ] `GoddessZonesPanel` — zones with lock states