283 lines
11 KiB
TypeScript
283 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState, lazy, Suspense } from 'react';
|
|
import { useShallow } from 'zustand/react/shallow';
|
|
|
|
// Import from new modular stores
|
|
import {
|
|
useGameStore,
|
|
useUIStore,
|
|
useManaStore,
|
|
useCombatStore,
|
|
usePrestigeStore,
|
|
useCraftingStore,
|
|
useDisciplineStore,
|
|
fmt,
|
|
computeMaxMana,
|
|
computeRegen,
|
|
computeClickMana,
|
|
getMeditationBonus,
|
|
getIncursionStrength,
|
|
} from '@/lib/game/stores';
|
|
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
|
|
import { useGameLoop } from '@/lib/game/stores/gameHooks';
|
|
import { getUnifiedEffects } from '@/lib/game/effects';
|
|
import { SPELLS_DEF } from '@/lib/game/constants';
|
|
import { TimeDisplay } from '@/components/game';
|
|
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
|
|
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
|
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
|
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
|
import { DebugName } from '@/components/game/debug/debug-context';
|
|
|
|
// Import extracted components
|
|
import { GameOverScreen } from './components/GameOverScreen';
|
|
import { LeftPanel } from './components/LeftPanel';
|
|
|
|
// Lazy load tab components
|
|
const DisciplinesTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.DisciplinesTab })));
|
|
const SpellsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpellsTab })));
|
|
const StatsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.StatsTab })));
|
|
const AchievementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AchievementsTab })));
|
|
|
|
const TabLoadingFallback = () => <div className="p-4 text-center text-gray-400">Loading...</div>;
|
|
|
|
// ============================================================================
|
|
// Grimoire Tab Component
|
|
// ============================================================================
|
|
|
|
function GrimoireTab() {
|
|
const [grimoireSpells, setGrimoireSpells] = useState<any[]>(() => {
|
|
if (typeof window !== 'undefined' && SPELLS_DEF) {
|
|
return Object.values(SPELLS_DEF).filter((s: any) => s.grimoire);
|
|
}
|
|
return [];
|
|
});
|
|
const loaded = typeof window !== 'undefined';
|
|
|
|
if (!loaded) {
|
|
return <div className="p-4 text-center text-gray-400">Loading grimoire...</div>;
|
|
}
|
|
|
|
if (grimoireSpells.length === 0) {
|
|
return (
|
|
<div className="p-4 text-center text-gray-400">
|
|
No grimoire spells available yet. Defeat guardians to unlock spells.
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const availablePages = Math.ceil(grimoireSpells.length / 12);
|
|
|
|
return (
|
|
<DebugName name="GrimoireTab">
|
|
<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.amount} {
|
|
spell.cost.type === 'element'
|
|
? spell.cost.element
|
|
: 'raw mana'
|
|
}</div>
|
|
<div>Power: {spell.power}</div>
|
|
{spell.effect && <div>Effect: {spell.effect}</div>}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
</DebugName>
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Main Game Component
|
|
// ============================================================================
|
|
|
|
export default function ManaLoopGame() {
|
|
const [selectedManaType, setSelectedManaType] = useState<string>('');
|
|
const [activeTab, setActiveTab] = useState('spells');
|
|
|
|
// ALL hooks must be called before any conditional returns
|
|
useGameLoop();
|
|
|
|
// Use useShallow to combine multi-field subscriptions and reduce re-renders
|
|
const { day, hour, initGame } = useGameStore(useShallow(s => ({ day: s.day, hour: s.hour, initGame: s.initGame })));
|
|
const { prestigeUpgrades, insight, loopInsight } = usePrestigeStore(useShallow(s => ({ prestigeUpgrades: s.prestigeUpgrades, insight: s.insight, loopInsight: s.loopInsight })));
|
|
const { rawMana, meditateTicks } = useManaStore(useShallow(s => ({ rawMana: s.rawMana, meditateTicks: s.meditateTicks })));
|
|
const spireMode = useCombatStore((s) => s.spireMode);
|
|
const gameOver = useUIStore((s) => s.gameOver);
|
|
|
|
// Get equipment state from crafting store
|
|
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
|
|
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
|
|
|
// Derived state
|
|
const upgradeEffects = getUnifiedEffects({
|
|
skillUpgrades: {},
|
|
skillTiers: {},
|
|
equippedInstances,
|
|
equipmentInstances
|
|
});
|
|
|
|
// Compute discipline bonuses from active disciplines
|
|
const disciplineStoreState = useDisciplineStore();
|
|
const disciplineEffects = computeDisciplineEffects(disciplineStoreState as any);
|
|
|
|
const maxMana = computeMaxMana({
|
|
skills: {},
|
|
prestigeUpgrades,
|
|
skillUpgrades: {},
|
|
skillTiers: {}
|
|
}, upgradeEffects as any, disciplineEffects);
|
|
|
|
const baseRegen = computeRegen({
|
|
skills: {},
|
|
prestigeUpgrades,
|
|
skillUpgrades: {},
|
|
skillTiers: {},
|
|
attunements: {},
|
|
}, upgradeEffects as any, disciplineEffects);
|
|
|
|
const clickMana = computeClickMana({
|
|
skills: {},
|
|
}, disciplineEffects);
|
|
|
|
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, (upgradeEffects as any).meditationEfficiency);
|
|
const incursionStrength = getIncursionStrength(day, hour);
|
|
|
|
// Effective regen with incursion penalty
|
|
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
|
|
|
// Mana Cascade bonus
|
|
const manaCascadeBonus = hasSpecial(upgradeEffects as any, SPECIAL_EFFECTS.MANA_CASCADE)
|
|
? Math.floor(maxMana / 100) * 0.1
|
|
: 0;
|
|
|
|
// Mana Waterfall bonus
|
|
const manaWaterfallBonus = hasSpecial(upgradeEffects as any, 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]);
|
|
|
|
const [mounted, setMounted] = useState(false);
|
|
useEffect(() => { setMounted(true); }, []); // eslint-disable-line react-hooks/set-state-in-effect
|
|
|
|
// React to spireMode changes from combat store
|
|
useEffect(() => {
|
|
if (spireMode) {
|
|
setActiveTab('spells'); // eslint-disable-line react-hooks/set-state-in-effect
|
|
}
|
|
}, [spireMode]);
|
|
|
|
// Conditional returns AFTER all hooks
|
|
if (gameOver) {
|
|
return <GameOverScreen day={day} hour={hour} insightGained={loopInsight} totalInsight={insight} />;
|
|
}
|
|
|
|
if (!mounted) return <div className="p-4 text-center text-gray-400">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 />
|
|
|
|
<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="spells" className="text-xs px-2 py-1">🔮 Spells</TabsTrigger>
|
|
<TabsTrigger value="stats" className="text-xs px-2 py-1">📊 Stats</TabsTrigger>
|
|
<TabsTrigger value="disciplines" className="text-xs px-2 py-1">📚 Disciplines</TabsTrigger>
|
|
<TabsTrigger value="grimoire" className="text-xs px-2 py-1">📖 Grimoire</TabsTrigger>
|
|
<TabsTrigger value="achievements" className="text-xs px-2 py-1">🏆 Achievements</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="spells">
|
|
<ErrorBoundary fallback={<div className="p-4 text-red-400">spells tab failed to load.</div>}>
|
|
<Suspense fallback={<TabLoadingFallback />}>
|
|
<SpellsTab />
|
|
</Suspense>
|
|
</ErrorBoundary>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="stats">
|
|
<ErrorBoundary fallback={<div className="p-4 text-red-400">stats tab failed to load.</div>}>
|
|
<Suspense fallback={<TabLoadingFallback />}>
|
|
<StatsTab />
|
|
</Suspense>
|
|
</ErrorBoundary>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="disciplines">
|
|
<ErrorBoundary fallback={<div className="p-4 text-red-400">disciplines tab failed to load.</div>}>
|
|
<Suspense fallback={<TabLoadingFallback />}>
|
|
<DisciplinesTab />
|
|
</Suspense>
|
|
</ErrorBoundary>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="grimoire">
|
|
<GrimoireTab />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="achievements">
|
|
<ErrorBoundary fallback={<div className="p-4 text-red-400">achievements tab failed to load.</div>}>
|
|
<Suspense fallback={<TabLoadingFallback />}>
|
|
<AchievementsTab />
|
|
</Suspense>
|
|
</ErrorBoundary>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</TooltipProvider>
|
|
</ErrorBoundary>
|
|
);
|
|
}
|