5817206351
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m47s
- Fix GrimoireTab to handle SSR safely (SPELLS_DEF undefined during server-side rendering) - Use useState and useEffect to only access constants on client-side - Build now succeeds consistently
340 lines
13 KiB
TypeScript
340 lines
13 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState, lazy, Suspense } from 'react';
|
||
import type { JSX } from 'react';
|
||
|
||
// Import from new modular stores
|
||
import {
|
||
useGameStore,
|
||
useUIStore,
|
||
useManaStore,
|
||
useSkillStore,
|
||
useCombatStore,
|
||
usePrestigeStore,
|
||
fmt,
|
||
computeMaxMana,
|
||
computeRegen,
|
||
computeClickMana,
|
||
getMeditationBonus,
|
||
getIncursionStrength,
|
||
} from '@/lib/game/stores';
|
||
import { useGameLoop } from '@/lib/game/stores/gameHooks';
|
||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||
import {
|
||
getStudySpeedMultiplier,
|
||
getStudyCostMultiplier,
|
||
SPELLS_DEF,
|
||
ELEMENTS,
|
||
GUARDIANS,
|
||
} from '@/lib/game/constants';
|
||
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
|
||
import { TimeDisplay } from '@/components/game';
|
||
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||
|
||
import { Button } from '@/components/ui/button';
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||
import { RotateCcw, Mountain } from 'lucide-react';
|
||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
||
|
||
// Import extracted components
|
||
import { GameOverScreen } from './components/GameOverScreen';
|
||
import { LeftPanel } from './components/LeftPanel';
|
||
|
||
// Lazy load tab components
|
||
const SpireTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpireTab })));
|
||
const SkillsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SkillsTab })));
|
||
const SpellsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpellsTab })));
|
||
const LabTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.LabTab })));
|
||
const StatsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.StatsTab })));
|
||
const EquipmentTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.EquipmentTab })));
|
||
const AttunementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AttunementsTab })));
|
||
const DebugTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.DebugTab })));
|
||
const LootTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.LootTab })));
|
||
const AchievementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AchievementsTab })));
|
||
const GolemancyTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GolemancyTab })));
|
||
const CraftingTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.CraftingTab })));
|
||
|
||
const TabLoadingFallback = () => <div className="p-4 text-center text-gray-400">Loading...</div>;
|
||
|
||
// ============================================================================
|
||
// Grimoire Tab Component
|
||
// ============================================================================
|
||
|
||
function GrimoireTab() {
|
||
// Handle SSR - dont access SPELLS_DEF during server-side rendering
|
||
// Use state and useEffect to only access on client-side
|
||
const [grimoireSpells, setGrimoireSpells] = useState<any[]>([]);
|
||
|
||
useEffect(() => {
|
||
// Only access SPELLS_DEF on client-side
|
||
if (typeof window !== 'undefined' && SPELLS_DEF) {
|
||
const filtered = Object.values(SPELLS_DEF).filter((s: any) => s.grimoire);
|
||
setGrimoireSpells(filtered);
|
||
}
|
||
}, []);
|
||
|
||
if (!grimoireSpells.length) {
|
||
return <div className="p-4 text-center text-gray-400">Loading grimoire...</div>;
|
||
}
|
||
|
||
const availablePages = Math.ceil(grimoireSpells.length / 12);
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="text-sm text-gray-400">
|
||
<p className="mb-2">A vast tome of arcane knowledge. Study carefully — each spell costs insight to transcribe into your repertoire.</p>
|
||
<p>Available pages: {availablePages}. Spells in grimoire: {grimoireSpells.length}.</p>
|
||
</div>
|
||
|
||
<ScrollArea className="h-[600px] rounded border border-gray-700 p-4">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{grimoireSpells.map((spell: any) => (
|
||
<div
|
||
key={spell.id}
|
||
className="p-4 bg-gray-800/50 rounded border border-gray-600 hover:border-gray-500 transition-colors"
|
||
>
|
||
<div className="flex items-start justify-between mb-2">
|
||
<span className="font-bold text-gray-100">{spell.name}</span>
|
||
<Badge variant="outline" className="border-gray-600">
|
||
{spell.element}
|
||
</Badge>
|
||
</div>
|
||
<p className="text-sm text-gray-400 mb-3">{spell.desc}</p>
|
||
<div className="text-xs text-gray-500 space-y-1">
|
||
<div>Cost: {(spell.cost as any[]).map((c: any) => `${c.amount} ${c.type}`).join(', ')}</div>
|
||
<div>Power: {spell.power}</div>
|
||
{spell.effect && <div>Effect: {spell.effect}</div>}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</ScrollArea>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ============================================================================
|
||
// Main Game Component
|
||
// ============================================================================
|
||
|
||
export default function ManaLoopGame() {
|
||
const [selectedManaType, setSelectedManaType] = useState<string>('');
|
||
const [activeTab, setActiveTab] = useState('spire');
|
||
|
||
// ALL hooks must be called before any conditional returns
|
||
const day = useGameStore((s) => s.day);
|
||
const hour = useGameStore((s) => s.hour);
|
||
const initGame = useGameStore((s) => s.initGame);
|
||
const gameLoop = useGameLoop();
|
||
|
||
const skills = useSkillStore((s) => s.skills);
|
||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
||
|
||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||
const insight = usePrestigeStore((s) => s.insight);
|
||
|
||
const rawMana = useManaStore((s) => s.rawMana);
|
||
const meditateTicks = useManaStore((s) => s.meditateTicks);
|
||
|
||
const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
|
||
const spells = useCombatStore((s) => s.spells);
|
||
|
||
const gameOver = useUIStore((s) => s.gameOver);
|
||
|
||
// Derived state
|
||
const upgradeEffects = getUnifiedEffects({
|
||
skillUpgrades,
|
||
skillTiers,
|
||
equippedInstances: {},
|
||
equipmentInstances: {}
|
||
});
|
||
|
||
const maxMana = computeMaxMana({
|
||
skills,
|
||
prestigeUpgrades,
|
||
skillUpgrades,
|
||
skillTiers
|
||
}, upgradeEffects);
|
||
|
||
const baseRegen = computeRegen({
|
||
skills,
|
||
prestigeUpgrades,
|
||
skillUpgrades,
|
||
skillTiers
|
||
}, upgradeEffects);
|
||
|
||
const clickMana = computeClickMana({
|
||
skills,
|
||
prestigeUpgrades,
|
||
skillUpgrades,
|
||
skillTiers
|
||
});
|
||
|
||
const meditationMultiplier = getMeditationBonus(meditateTicks, skills, upgradeEffects.meditationEfficiency);
|
||
const incursionStrength = getIncursionStrength(day, hour);
|
||
|
||
// Effective regen with incursion penalty
|
||
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
||
|
||
// Mana Cascade bonus
|
||
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE)
|
||
? Math.floor(maxMana / 100) * 0.1
|
||
: 0;
|
||
|
||
// Mana Waterfall bonus
|
||
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL)
|
||
? Math.floor(maxMana / 100) * 0.25
|
||
: 0;
|
||
|
||
// Effective regen
|
||
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
|
||
|
||
// Initialize game on mount
|
||
useEffect(() => {
|
||
initGame();
|
||
}, [initGame]);
|
||
|
||
// Start game loop
|
||
useEffect(() => {
|
||
const cleanup = gameLoop.start();
|
||
return cleanup;
|
||
}, [gameLoop]);
|
||
|
||
// Conditional returns AFTER all hooks
|
||
if (gameOver) {
|
||
return <GameOverScreen store={{ day, hour, insight }} />;
|
||
}
|
||
|
||
if (typeof window === 'undefined') {
|
||
return <div>Loading...</div>;
|
||
}
|
||
|
||
return (
|
||
<ErrorBoundary>
|
||
<TooltipProvider>
|
||
<div className="game-root min-h-screen flex flex-col">
|
||
{/* Header */}
|
||
<header className="sticky top-0 z-50 bg-gradient-to-b from-gray-900 to-gray-900/80 border-b border-gray-700 px-4 py-2">
|
||
<div className="flex items-center justify-between">
|
||
<h1 className="text-xl font-bold game-title tracking-wider">MANA LOOP</h1>
|
||
<div className="flex items-center gap-4">
|
||
<TimeDisplay day={day} hour={hour} insight={insight} />
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
{/* Main Content */}
|
||
<main className="flex-1 flex flex-col md:flex-row gap-4 p-4">
|
||
<LeftPanel
|
||
store={{ rawMana, maxMana, day, hour }}
|
||
effectiveRegen={effectiveRegen}
|
||
incursionStrength={incursionStrength}
|
||
/>
|
||
|
||
<div className="flex-1 min-w-0">
|
||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
|
||
<TabsTrigger value="spire" className="text-xs px-2 py-1">⚔️ Spire</TabsTrigger>
|
||
<TabsTrigger value="attunements" className="text-xs px-2 py-1">✨ Attune</TabsTrigger>
|
||
<TabsTrigger value="golemancy" className="text-xs px-2 py-1">🗿 Golems</TabsTrigger>
|
||
<TabsTrigger value="skills" className="text-xs px-2 py-1">📚 Skills</TabsTrigger>
|
||
<TabsTrigger value="spells" className="text-xs px-2 py-1">🔮 Spells</TabsTrigger>
|
||
<TabsTrigger value="equipment" className="text-xs px-2 py-1">🛡️ Gear</TabsTrigger>
|
||
<TabsTrigger value="crafting" className="text-xs px-2 py-1">🔧 Craft</TabsTrigger>
|
||
<TabsTrigger value="loot" className="text-xs px-2 py-1">💎 Loot</TabsTrigger>
|
||
<TabsTrigger value="achievements" className="text-xs px-2 py-1">🏆 Achieve</TabsTrigger>
|
||
<TabsTrigger value="lab" className="text-xs px-2 py-1">🔬 Lab</TabsTrigger>
|
||
<TabsTrigger value="stats" className="text-xs px-2 py-1">📊 Stats</TabsTrigger>
|
||
<TabsTrigger value="debug" className="text-xs px-2 py-1">🐛 Debug</TabsTrigger>
|
||
<TabsTrigger value="grimoire" className="text-xs px-2 py-1">📖 Grimoire</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="spire">
|
||
<Suspense fallback={<TabLoadingFallback />}>
|
||
<SpireTab />
|
||
</Suspense>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="attunements">
|
||
<Suspense fallback={<TabLoadingFallback />}>
|
||
<AttunementsTab />
|
||
</Suspense>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="golemancy">
|
||
<Suspense fallback={<TabLoadingFallback />}>
|
||
<GolemancyTab />
|
||
</Suspense>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="skills">
|
||
<Suspense fallback={<TabLoadingFallback />}>
|
||
<SkillsTab />
|
||
</Suspense>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="spells">
|
||
<Suspense fallback={<TabLoadingFallback />}>
|
||
<SpellsTab />
|
||
</Suspense>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="equipment">
|
||
<Suspense fallback={<TabLoadingFallback />}>
|
||
<EquipmentTab />
|
||
</Suspense>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="crafting">
|
||
<Suspense fallback={<TabLoadingFallback />}>
|
||
<CraftingTab />
|
||
</Suspense>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="loot">
|
||
<Suspense fallback={<TabLoadingFallback />}>
|
||
<LootTab />
|
||
</Suspense>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="achievements">
|
||
<Suspense fallback={<TabLoadingFallback />}>
|
||
<AchievementsTab />
|
||
</Suspense>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="lab">
|
||
<Suspense fallback={<TabLoadingFallback />}>
|
||
<LabTab />
|
||
</Suspense>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="stats">
|
||
<Suspense fallback={<TabLoadingFallback />}>
|
||
<StatsTab />
|
||
</Suspense>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="debug">
|
||
<Suspense fallback={<TabLoadingFallback />}>
|
||
<DebugTab />
|
||
</Suspense>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="grimoire">
|
||
<GrimoireTab />
|
||
</TabsContent>
|
||
</Tabs>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
</TooltipProvider>
|
||
</ErrorBoundary>
|
||
);
|
||
}
|