Files
elysium/apps/web/src/components/game/GameLayout.tsx
T
hikari db860ee5d3 feat: add companion system with quest-time reduction server validation
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.
2026-03-07 15:59:24 -08:00

150 lines
6.0 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.
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>
);
};