generated from nhcarrigan/template
feat: initial prototype — core game systems (#30)
## Summary This PR represents the full v1 prototype, implementing the core game systems for Elysium. - Full idle/clicker RPG loop: resource collection, crafting, boss fights, exploration, and quests - Adventurer hiring with batch size selector and progressive tier cost scaling - Prestige, transcendence, and apotheosis systems with auto-prestige support - Character sheet, titles, leaderboards, companion system, and daily login bonuses - Auto-quest and auto-boss toggles - Discord webhook notifications on prestige/transcendence/apotheosis - Discord role awarded on apotheosis - Responsive design and overarching story/lore system - In-game sound effects and browser notifications for key events - Support link button in the resource bar - Full test coverage (100% on `apps/api` and `packages/types`) - CI pipeline: lint → build → test ## Closes Closes #1 Closes #2 Closes #3 Closes #4 Closes #5 Closes #6 Closes #7 Closes #8 Closes #9 Closes #10 Closes #11 Closes #12 Closes #13 Closes #14 Closes #16 Closes #19 Closes #20 Closes #21 Closes #22 Closes #23 Closes #24 Closes #25 Closes #26 Closes #27 Closes #29 ✨ This issue was created with help from Hikari~ 🌸 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Reviewed-on: #30 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #30.
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* @file Game layout component rendering the main game UI.
|
||||
* @copyright nhcarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
/* 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 { 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 { CraftingPanel } from "./craftingPanel.js";
|
||||
import { DailyChallengePanel } from "./dailyChallengePanel.js";
|
||||
import { EditProfileModal } from "./editProfileModal.js";
|
||||
import { EquipmentPanel } from "./equipmentPanel.js";
|
||||
import { ExplorationPanel } from "./explorationPanel.js";
|
||||
import { LoginBonusModal } from "./loginBonusModal.js";
|
||||
import { OfflineModal } from "./offlineModal.js";
|
||||
import { OutdatedSchemaModal } from "./outdatedSchemaModal.js";
|
||||
import { PrestigePanel } from "./prestigePanel.js";
|
||||
import { QuestPanel } from "./questPanel.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";
|
||||
|
||||
type Tab =
|
||||
| "adventurers"
|
||||
| "upgrades"
|
||||
| "quests"
|
||||
| "bosses"
|
||||
| "equipment"
|
||||
| "achievements"
|
||||
| "prestige"
|
||||
| "transcendence"
|
||||
| "apotheosis"
|
||||
| "statistics"
|
||||
| "daily"
|
||||
| "codex"
|
||||
| "about"
|
||||
| "exploration"
|
||||
| "crafting"
|
||||
| "character"
|
||||
| "companions"
|
||||
| "story";
|
||||
|
||||
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" },
|
||||
];
|
||||
|
||||
/**
|
||||
* 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 [ activeTab, setActiveTab ] = useState<Tab>("adventurers");
|
||||
const [ editingProfile, setEditingProfile ] = useState(false);
|
||||
const [ dismissedOutdatedWarning, setDismissedOutdatedWarning ]
|
||||
= useState(false);
|
||||
|
||||
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 profileUrl = `/profile/${state.player.discordId}`;
|
||||
const codexBadgeCount = pendingCodexEntryIds.length;
|
||||
const storyBadgeCount = pendingStoryChapterIds.length;
|
||||
|
||||
function handleOpenEditProfile(): void {
|
||||
setEditingProfile(true);
|
||||
}
|
||||
|
||||
function handleCloseEditProfile(): void {
|
||||
setEditingProfile(false);
|
||||
}
|
||||
|
||||
function handleDismissOutdated(): void {
|
||||
setDismissedOutdatedWarning(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="game-layout">
|
||||
<ResourceBar
|
||||
apotheosisCount={state.apotheosis?.count ?? 0}
|
||||
isSyncing={isSyncing}
|
||||
lastSavedAt={lastSavedAt}
|
||||
onEditProfile={handleOpenEditProfile}
|
||||
onForceSync={forceSync}
|
||||
prestigeCount={state.prestige.count}
|
||||
profileUrl={profileUrl}
|
||||
resources={state.resources}
|
||||
runestones={state.prestige.runestones}
|
||||
transcendenceCount={state.transcendence?.count ?? 0}
|
||||
/>
|
||||
<OfflineModal />
|
||||
{schemaOutdated && !dismissedOutdatedWarning
|
||||
? <OutdatedSchemaModal onDismiss={handleDismissOutdated} />
|
||||
: null}
|
||||
<AchievementToast />
|
||||
<CodexToast />
|
||||
<StoryToast />
|
||||
{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 />
|
||||
<p className="game-copyright">{"© NHCarrigan"}</p>
|
||||
</aside>
|
||||
|
||||
<main className="game-content">
|
||||
<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>
|
||||
|
||||
<div className="tab-content">
|
||||
{activeTab === "adventurers" && <AdventurerPanel />}
|
||||
{activeTab === "upgrades" && <UpgradePanel />}
|
||||
{activeTab === "quests" && <QuestPanel />}
|
||||
{activeTab === "bosses" && <BossPanel />}
|
||||
{activeTab === "equipment" && <EquipmentPanel />}
|
||||
{activeTab === "achievements" && <AchievementPanel />}
|
||||
{activeTab === "prestige" && <PrestigePanel />}
|
||||
{activeTab === "transcendence" && <TranscendencePanel />}
|
||||
{activeTab === "apotheosis" && <ApotheosisPanel />}
|
||||
{activeTab === "exploration" && <ExplorationPanel />}
|
||||
{activeTab === "crafting" && <CraftingPanel />}
|
||||
{activeTab === "statistics" && <StatisticsPanel />}
|
||||
{activeTab === "daily" && <DailyChallengePanel />}
|
||||
{activeTab === "companions" && <CompanionPanel />}
|
||||
{activeTab === "character" && <CharacterSheetPanel />}
|
||||
{activeTab === "story" && <StoryPanel />}
|
||||
{activeTab === "codex" && <CodexPanel />}
|
||||
{activeTab === "about" && <AboutPanel />}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { GameLayout };
|
||||
Reference in New Issue
Block a user