Files
Mana-Loop/src/app/page.tsx
T
n8n-gitea 1c7fc8c551
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 2m18s
feat: recreate Crafting Tab with Fabricator and Enchanter sub-tabs
- Add fabricator-recipes.ts with 12 recipes across earth/metal/crystal/sand mana types
- Add FabricatorSubTab with mana-type filtering, recipe cards, materials inventory
- Add EnchanterSubTab integrating existing 3-phase flow (Design → Prepare → Apply)
- Add CraftingTab main component with clsx-based sub-tab system (matches DisciplinesTab pattern)
- Wire into tabs barrel export and page.tsx with lazy loading + DebugName wrapper
- Add 17 tests covering exports, displayNames, recipe data integrity, helpers, file sizes
- All files under 400 lines
2026-05-20 02:32:37 +02:00

363 lines
15 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 { 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 DebugTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.DebugTab })));
const AchievementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AchievementsTab })));
const AttunementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AttunementsTab })));
const PrestigeTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.PrestigeTab })));
const EquipmentTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.EquipmentTab })));
const GolemancyTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GolemancyTab })));
const GuardianPactsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GuardianPactsTab })));
const SpireSummaryTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpireSummaryTab })));
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() {
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="debug" className="text-xs px-2 py-1">🐛 Debug</TabsTrigger>
<TabsTrigger value="attunements" className="text-xs px-2 py-1"> Attunements</TabsTrigger>
<TabsTrigger value="achievements" className="text-xs px-2 py-1">🏆 Achievements</TabsTrigger>
<TabsTrigger value="prestige" className="text-xs px-2 py-1"> Prestige</TabsTrigger>
<TabsTrigger value="equipment" className="text-xs px-2 py-1"> Equipment</TabsTrigger>
<TabsTrigger value="golemancy" className="text-xs px-2 py-1">🗿 Golemancy</TabsTrigger>
<TabsTrigger value="pacts" className="text-xs px-2 py-1">📜 Pacts</TabsTrigger>
<TabsTrigger value="spire" className="text-xs px-2 py-1">🏔 Spire</TabsTrigger>
<TabsTrigger value="crafting" className="text-xs px-2 py-1"> Crafting</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="debug">
<ErrorBoundary fallback={<div className="p-4 text-red-400">debug tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<DebugTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="attunements">
<ErrorBoundary fallback={<div className="p-4 text-red-400">attunements tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<AttunementsTab />
</Suspense>
</ErrorBoundary>
</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>
<TabsContent value="prestige">
<ErrorBoundary fallback={<div className="p-4 text-red-400">prestige tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<PrestigeTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="equipment">
<ErrorBoundary fallback={<div className="p-4 text-red-400">equipment tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<EquipmentTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="golemancy">
<ErrorBoundary fallback={<div className="p-4 text-red-400">golemancy tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<GolemancyTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="pacts">
<ErrorBoundary fallback={<div className="p-4 text-red-400">pacts tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<GuardianPactsTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="spire">
<ErrorBoundary fallback={<div className="p-4 text-red-400">spire tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<SpireSummaryTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="crafting">
<ErrorBoundary fallback={<div className="p-4 text-red-400">crafting tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<CraftingTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
</Tabs>
</div>
</main>
</div>
</TooltipProvider>
</ErrorBoundary>
);
}