generated from nhcarrigan/template
db860ee5d3
Introduces 10 unlockable companions (Lyra, Finn, Wren, Aldric, Sera, Kael, Zuri, Mira, Vex, Pria), each providing a unique bonus: passive gold, click gold, boss damage, essence income, or quest-time reduction. Quest-time reduction is validated server-side: computeQuestRewards applies the active companion's reduction to the effective duration check, and the income validation budget accounts for passive gold and essence bonuses. Server recomputes unlockedCompanionIds on every save using DB-authoritative lifetime stats and validates the active companion ID. Companion bonuses are also applied in the client tick engine and boss.ts calculatePartyStats.
150 lines
6.0 KiB
TypeScript
150 lines
6.0 KiB
TypeScript
import { 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 { BattleModal } from "./BattleModal.js";
|
||
import { BossPanel } from "./BossPanel.js";
|
||
import { ClickArea } from "./ClickArea.js";
|
||
import { CodexPanel } from "./CodexPanel.js";
|
||
import { CodexToast } from "./CodexToast.js";
|
||
import { EditProfileModal } from "./EditProfileModal.js";
|
||
import { EquipmentPanel } from "./EquipmentPanel.js";
|
||
import { OfflineModal } from "./OfflineModal.js";
|
||
import { PrestigePanel } from "./PrestigePanel.js";
|
||
import { ApotheosisPanel } from "./ApotheosisPanel.js";
|
||
import { TranscendencePanel } from "./TranscendencePanel.js";
|
||
import { QuestPanel } from "./QuestPanel.js";
|
||
import { StatisticsPanel } from "./StatisticsPanel.js";
|
||
import { UpgradePanel } from "./UpgradePanel.js";
|
||
import { DailyChallengePanel } from "./DailyChallengePanel.js";
|
||
import { ExplorationPanel } from "./ExplorationPanel.js";
|
||
import { CharacterSheetPanel } from "./CharacterSheetPanel.js";
|
||
import { CompanionPanel } from "./CompanionPanel.js";
|
||
import { CraftingPanel } from "./CraftingPanel.js";
|
||
import { LoginBonusModal } from "./LoginBonusModal.js";
|
||
|
||
type Tab = "adventurers" | "upgrades" | "quests" | "bosses" | "equipment" | "achievements" | "prestige" | "transcendence" | "apotheosis" | "statistics" | "daily" | "codex" | "about" | "exploration" | "crafting" | "character" | "companions";
|
||
|
||
const BASE_TABS: { 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: "codex", label: "π Codex" },
|
||
{ id: "about", label: "βΉοΈ About" },
|
||
];
|
||
|
||
export const GameLayout = (): React.JSX.Element => {
|
||
const { state, isLoading, error, battleResult, dismissBattle, lastSavedAt, isSyncing, forceSync, newCodexEntryIds, loginBonus, dismissLoginBonus } = useGame();
|
||
const [activeTab, setActiveTab] = useState<Tab>("adventurers");
|
||
const [editingProfile, setEditingProfile] = useState(false);
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="loading-screen">
|
||
<p>Loading your adventure...</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="error-screen">
|
||
<p>Error: {error}</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!state) return <div className="loading-screen"><p>Loading...</p></div>;
|
||
|
||
const profileUrl = `/profile/${state.player.discordId}`;
|
||
|
||
return (
|
||
<div className="game-layout">
|
||
<ResourceBar
|
||
resources={state.resources}
|
||
runestones={state.prestige.runestones}
|
||
prestigeCount={state.prestige.count}
|
||
transcendenceCount={state.transcendence?.count ?? 0}
|
||
apotheosisCount={state.apotheosis?.count ?? 0}
|
||
profileUrl={profileUrl}
|
||
onEditProfile={() => { setEditingProfile(true); }}
|
||
lastSavedAt={lastSavedAt}
|
||
isSyncing={isSyncing}
|
||
onForceSync={forceSync}
|
||
/>
|
||
<OfflineModal />
|
||
<AchievementToast />
|
||
<CodexToast />
|
||
{loginBonus && (
|
||
<LoginBonusModal bonus={loginBonus} onClose={dismissLoginBonus} />
|
||
)}
|
||
{battleResult && (
|
||
<BattleModal battle={battleResult} onDismiss={dismissBattle} />
|
||
)}
|
||
{editingProfile && (
|
||
<EditProfileModal onClose={() => { setEditingProfile(false); }} />
|
||
)}
|
||
|
||
<div className="game-main">
|
||
<aside className="game-sidebar">
|
||
<ClickArea />
|
||
<p className="game-copyright">Β© NHCarrigan</p>
|
||
</aside>
|
||
|
||
<main className="game-content">
|
||
<nav className="tab-bar">
|
||
{BASE_TABS.map((tab) => (
|
||
<button
|
||
key={tab.id}
|
||
className={`tab-button ${activeTab === tab.id ? "active" : ""}`}
|
||
onClick={() => { setActiveTab(tab.id); }}
|
||
type="button"
|
||
>
|
||
{tab.label}
|
||
{tab.id === "codex" && newCodexEntryIds.length > 0 && (
|
||
<span className="tab-badge">{newCodexEntryIds.length}</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 === "codex" && <CodexPanel />}
|
||
{activeTab === "about" && <AboutPanel />}
|
||
</div>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|