Files
Mana-Loop/src/app/page.tsx
T
Refactoring Agent 5817206351
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m47s
fix: resolve SSR build error with GrimoireTab
- 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
2026-05-03 12:58:50 +02:00

340 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}