generated from nhcarrigan/template
bd88eecda5
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.
549 lines
20 KiB
TypeScript
549 lines
20 KiB
TypeScript
/**
|
||
* @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 };
|