Files
elysium/apps/web/src/components/game/gameLayout.tsx
T
hikari bd88eecda5 feat: vampire boss panel and thralls panel with combat simulation
Implements VampireBossPanel (zone filtering, HP bar, battle modal with
rewards/casualties) and VampireThrallsPanel (batch buy with geometric
cost scaling). Wires challengeVampireBoss, dismissVampireBattle, and
buyVampireThrall into GameContext with correct sort-key ordering.
2026-04-16 12:17:45 -07:00

549 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file Game layout component rendering the main game UI.
* @copyright nhcarrigan
* @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 */
/* 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";
import { AboutPanel } from "./aboutPanel.js";
import { AchievementPanel } from "./achievementPanel.js";
import { AchievementToast } from "./achievementToast.js";
import { AdventurerPanel } from "./adventurerPanel.js";
import { ApotheosisPanel } from "./apotheosisPanel.js";
import { BattleModal } from "./battleModal.js";
import { BossPanel } from "./bossPanel.js";
import { CharacterSheetPanel } from "./characterSheetPanel.js";
import { ClickArea } from "./clickArea.js";
import { CodexPanel } from "./codexPanel.js";
import { CodexToast } from "./codexToast.js";
import { CompanionPanel } from "./companionPanel.js";
import { ConsecrationPanel } from "./consecrationPanel.js";
import { CraftingPanel } from "./craftingPanel.js";
import { DailyChallengePanel } from "./dailyChallengePanel.js";
import { DebugPanel } from "./debugPanel.js";
import { DisciplesPanel } from "./disciplesPanel.js";
import { EditProfileModal } from "./editProfileModal.js";
import { EnlightenmentPanel } from "./enlightenmentPanel.js";
import { EquipmentPanel } from "./equipmentPanel.js";
import { ExplorationPanel } from "./explorationPanel.js";
import { GoddessAchievementsPanel } from "./goddessAchievementsPanel.js";
import { GoddessBossPanel } from "./goddessBossPanel.js";
import { GoddessCraftingPanel } from "./goddessCraftingPanel.js";
import { GoddessEquipmentPanel } from "./goddessEquipmentPanel.js";
import { GoddessExplorationPanel } from "./goddessExplorationPanel.js";
import { GoddessQuestsPanel } from "./goddessQuestsPanel.js";
import { GoddessUpgradesPanel } from "./goddessUpgradesPanel.js";
import { GoddessZonesPanel } from "./goddessZonesPanel.js";
import { JoinCommunityModal } from "./joinCommunityModal.js";
import { LoginBonusModal } from "./loginBonusModal.js";
import { MilestoneToast } from "./milestoneToast.js";
import { OfflineModal } from "./offlineModal.js";
import { OutdatedSchemaModal } from "./outdatedSchemaModal.js";
import { PrestigePanel } from "./prestigePanel.js";
import { QuestPanel } from "./questPanel.js";
import { QuestCompleteToast, QuestFailedToast } from "./questToast.js";
import { StatisticsPanel } from "./statisticsPanel.js";
import { StoryPanel } from "./storyPanel.js";
import { StoryToast } from "./storyToast.js";
import { TranscendencePanel } from "./transcendencePanel.js";
import { UpgradePanel } from "./upgradePanel.js";
import { VampireAchievementsPanel } from "./vampireAchievementsPanel.js";
import { VampireBossPanel } from "./vampireBossPanel.js";
import { VampireQuestsPanel } from "./vampireQuestsPanel.js";
import { VampireThrallsPanel } from "./vampireThrallsPanel.js";
import { VampireZonesPanel } from "./vampireZonesPanel.js";
type Mode = "mortal" | "goddess" | "vampire";
type Tab =
| "adventurers"
| "upgrades"
| "quests"
| "bosses"
| "equipment"
| "achievements"
| "prestige"
| "transcendence"
| "apotheosis"
| "statistics"
| "daily"
| "codex"
| "about"
| "exploration"
| "crafting"
| "character"
| "companions"
| "story"
| "debug";
type GoddessTab =
| "goddess-zones"
| "goddess-bosses"
| "goddess-quests"
| "disciples"
| "goddess-equipment"
| "goddess-upgrades"
| "consecration"
| "enlightenment"
| "goddess-crafting"
| "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" },
{ id: "quests", label: "📜 Quests" },
{ id: "bosses", label: "👹 Bosses" },
{ id: "equipment", label: "🗡️ Equipment" },
{ id: "exploration", label: "🗺️ Exploration" },
{ id: "crafting", label: "⚗️ Crafting" },
{ id: "daily", label: "📅 Daily" },
{ id: "prestige", label: "⭐ Prestige" },
{ id: "transcendence", label: "🌌 Transcendence" },
{ id: "apotheosis", label: "✨ Apotheosis" },
{ id: "statistics", label: "📊 Statistics" },
{ id: "companions", label: "👥 Companions" },
{ id: "character", label: "📋 Character" },
{ id: "achievements", label: "🏆 Achievements" },
{ id: "story", label: "📖 Story" },
{ id: "codex", label: "🗺️ Codex" },
{ id: "about", label: "️ About" },
{ 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" },
{ 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.
* @returns The JSX element.
*/
const GameLayout = (): JSX.Element => {
const {
state,
isLoading,
error,
battleResult,
dismissBattle,
lastSavedAt,
isSyncing,
forceSync,
unlockedCodexEntryIds: pendingCodexEntryIds,
unlockedStoryChapterIds: pendingStoryChapterIds,
loginBonus,
dismissLoginBonus,
schemaOutdated,
} = useGame();
const [ activeMode, setActiveMode ] = useState<Mode>(readSavedMode);
const [ activeTab, setActiveTab ] = useState<Tab>("adventurers");
const [ activeGoddessTab, setActiveGoddessTab ]
= useState<GoddessTab>("goddess-zones");
const [ activeVampireTab, setActiveVampireTab ]
= useState<VampireTab>("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) {
return (
<div className="loading-screen">
<p>{"Loading your adventure..."}</p>
</div>
);
}
if (error !== null && error !== "") {
return (
<div className="error-screen">
<p>
{"Error: "}
{error}
</p>
</div>
);
}
if (state === null) {
return (
<div className="loading-screen">
<p>{"Loading..."}</p>
</div>
);
}
const codexBadgeCount = pendingCodexEntryIds.length;
const storyBadgeCount = pendingStoryChapterIds.length;
function handleOpenEditProfile(): void {
setEditingProfile(true);
}
function handleCloseEditProfile(): void {
setEditingProfile(false);
}
function handleDismissOutdated(): void {
setDismissedOutdatedWarning(true);
}
function handleSetMode(mode: Mode): void {
localStorage.setItem("elysium-active-mode", mode);
setActiveMode(mode);
}
return (
<div className="game-layout">
<ResourceBar
apotheosisCount={state.apotheosis?.count ?? 0}
isSyncing={isSyncing}
lastSavedAt={lastSavedAt}
onEditProfile={handleOpenEditProfile}
onForceSync={forceSync}
prestigeCount={state.prestige.count}
resources={state.resources}
runestones={state.prestige.runestones}
transcendenceCount={state.transcendence?.count ?? 0}
/>
<OfflineModal />
<JoinCommunityModal />
{schemaOutdated && !dismissedOutdatedWarning
? <OutdatedSchemaModal onDismiss={handleDismissOutdated} />
: null}
<div className="achievement-toast-container">
<AchievementToast />
<CodexToast />
<MilestoneToast />
<QuestCompleteToast />
<QuestFailedToast />
<StoryToast />
</div>
{loginBonus === null
? null
: <LoginBonusModal bonus={loginBonus} onClose={dismissLoginBonus} />
}
{battleResult === null
? null
: <BattleModal battle={battleResult} onDismiss={dismissBattle} />
}
{editingProfile
? <EditProfileModal onClose={handleCloseEditProfile} />
: null}
<div className="game-main">
<aside className="game-sidebar">
<ClickArea />
<div id="tree-nation-offset-website" />
<p className="game-copyright">{"© NHCarrigan"}</p>
</aside>
<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]}
type="button"
>
{modeLabels[mode]}
{isLocked
? <span className="mode-lock">{"🔒"}</span>
: null}
</button>
);
})}
</nav>
{/* eslint-disable-next-line no-nested-ternary -- Three-way mode switch for tab bar */}
{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>
: activeMode === "goddess"
? <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>
: <nav className="tab-bar vampire-tab-bar">
{vampireTabs.map((tab) => {
const { id: tabId, label } = tab;
function handleVampireTabClick(): void {
setActiveVampireTab(tabId);
}
return (
<button
className={`tab-button${activeVampireTab === tabId
? " active"
: ""}`}
key={tabId}
onClick={handleVampireTabClick}
type="button"
>
{label}
</button>
);
})}
</nav>
}
<div className="tab-content">
{activeMode === "mortal" && activeTab === "adventurers"
&& <AdventurerPanel />}
{activeMode === "mortal" && activeTab === "upgrades"
&& <UpgradePanel />}
{activeMode === "mortal" && activeTab === "quests"
&& <QuestPanel />}
{activeMode === "mortal" && activeTab === "bosses"
&& <BossPanel />}
{activeMode === "mortal" && activeTab === "equipment"
&& <EquipmentPanel />}
{activeMode === "mortal" && activeTab === "achievements"
&& <AchievementPanel />}
{activeMode === "mortal" && activeTab === "prestige"
&& <PrestigePanel />}
{activeMode === "mortal" && activeTab === "transcendence"
&& <TranscendencePanel />}
{activeMode === "mortal" && activeTab === "apotheosis"
&& <ApotheosisPanel />}
{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" && activeGoddessTab === "goddess-zones"
&& <GoddessZonesPanel />}
{activeMode === "goddess" && activeGoddessTab === "goddess-bosses"
&& <GoddessBossPanel />}
{activeMode === "goddess" && activeGoddessTab === "goddess-quests"
&& <GoddessQuestsPanel />}
{activeMode === "goddess" && activeGoddessTab === "disciples"
&& <DisciplesPanel />}
{activeMode === "goddess"
&& activeGoddessTab === "goddess-equipment"
&& <GoddessEquipmentPanel />}
{activeMode === "goddess"
&& activeGoddessTab === "goddess-upgrades"
&& <GoddessUpgradesPanel />}
{activeMode === "goddess" && activeGoddessTab === "consecration"
&& <ConsecrationPanel />}
{activeMode === "goddess" && activeGoddessTab === "enlightenment"
&& <EnlightenmentPanel />}
{activeMode === "goddess"
&& activeGoddessTab === "goddess-crafting"
&& <GoddessCraftingPanel />}
{activeMode === "goddess"
&& activeGoddessTab === "goddess-exploration"
&& <GoddessExplorationPanel />}
{activeMode === "goddess"
&& activeGoddessTab === "goddess-achievements"
&& <GoddessAchievementsPanel />}
{activeMode === "vampire"
&& activeVampireTab === "vampire-zones"
&& <VampireZonesPanel />
}
{activeMode === "vampire"
&& activeVampireTab === "vampire-bosses"
&& <VampireBossPanel />
}
{activeMode === "vampire"
&& activeVampireTab === "vampire-quests"
&& <VampireQuestsPanel />
}
{activeMode === "vampire"
&& activeVampireTab === "thralls"
&& <VampireThrallsPanel />
}
{activeMode === "vampire"
&& activeVampireTab === "vampire-equipment"
&& <div className="vampire-placeholder">
<p>{"🦇 Vampire Equipment coming soon..."}</p>
</div>
}
{activeMode === "vampire"
&& activeVampireTab === "vampire-upgrades"
&& <div className="vampire-placeholder">
<p>{"⚔️ Vampire Upgrades coming soon..."}</p>
</div>
}
{activeMode === "vampire"
&& activeVampireTab === "siring"
&& <div className="vampire-placeholder">
<p>{"🩸 Siring coming soon..."}</p>
</div>
}
{activeMode === "vampire"
&& activeVampireTab === "vampire-awakening"
&& <div className="vampire-placeholder">
<p>{"💀 Vampire Awakening coming soon..."}</p>
</div>
}
{activeMode === "vampire"
&& activeVampireTab === "vampire-crafting"
&& <div className="vampire-placeholder">
<p>{"⚗️ Vampire Crafting coming soon..."}</p>
</div>
}
{activeMode === "vampire"
&& activeVampireTab === "vampire-exploration"
&& <div className="vampire-placeholder">
<p>{"🌑 Vampire Exploration coming soon..."}</p>
</div>
}
{activeMode === "vampire"
&& activeVampireTab === "vampire-achievements"
&& <VampireAchievementsPanel />
}
</div>
</main>
</div>
</div>
);
};
export { GameLayout };