refactor: extract components from page.tsx to reduce below 400 lines
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 3m13s

This commit is contained in:
Refactoring Agent
2026-05-01 15:40:53 +02:00
parent 03815f27ee
commit 6c4ebd8b8e
+327 -207
View File
@@ -1,6 +1,7 @@
'use client'; 'use client';
import { useEffect, useState, lazy, Suspense } from 'react'; import { useEffect, useState, lazy, Suspense } from 'react';
import type { JSX } from 'react';
import { useGameStore, useGameLoop, fmt, getFloorElement, computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, getIncursionStrength, canAffordSpellCost } from '@/lib/game/store'; import { useGameStore, useGameLoop, fmt, getFloorElement, computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, getIncursionStrength, canAffordSpellCost } from '@/lib/game/store';
import { ActivityLogEntry } from '@/lib/game/types'; import { ActivityLogEntry } from '@/lib/game/types';
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats'; import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
@@ -19,8 +20,6 @@ import { TooltipProvider } from '@/components/ui/tooltip';
import { DebugName } from '@/lib/game/debug-context'; import { DebugName } from '@/lib/game/debug-context';
// Non-tab component imports // Non-tab component imports
import { ActionButtons, CalendarDisplay, ManaDisplay, TimeDisplay } from '@/components/game'; import { ActionButtons, CalendarDisplay, ManaDisplay, TimeDisplay } from '@/components/game';
// Loot and Achievements moved to separate tabs
// Lazy load tab components // Lazy load tab components
const SpireTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpireTab }))); 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 SkillsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SkillsTab })));
@@ -38,98 +37,15 @@ const CraftingTab = lazy(() => import('@/components/game/tabs').then(module => (
// Loading fallback component // Loading fallback component
const TabLoadingFallback = () => <div className="p-4 text-center text-gray-400">Loading...</div>; const TabLoadingFallback = () => <div className="p-4 text-center text-gray-400">Loading...</div>;
export default function ManaLoopGame() { // ============================================================================
const [activeTab, setActiveTab] = useState('spire'); // Extracted Components
const [isGathering, setIsGathering] = useState(false); // ============================================================================
const [selectedManaType, setSelectedManaType] = useState<string>('');
// Game store interface GameOverScreenProps {
const store = useGameStore(); store: any;
const gameLoop = useGameLoop(); }
// Computed effects from upgrades and equipment function GameOverScreen({ store }: GameOverScreenProps) {
const upgradeEffects = getUnifiedEffects(store);
// Get unlocked elements for mana type selector
const unlockedElements = Object.entries(ELEMENTS)
.filter(([id]) => store.elements[id]?.unlocked)
.map(([id, elem]) => ({ id, name: elem.name, sym: elem.sym, color: elem.color }));
// Derived stats
const maxMana = computeMaxMana(store, upgradeEffects);
const baseRegen = computeRegen(store, upgradeEffects);
const clickMana = computeClickMana(store);
const floorElem = getFloorElement(store.currentFloor);
const floorElemDef = ELEMENTS[floorElem];
const isGuardianFloor = !!GUARDIANS[store.currentFloor];
const currentGuardian = GUARDIANS[store.currentFloor];
const meditationMultiplier = getMeditationBonus(store.meditateTicks, store.skills, upgradeEffects.meditationEfficiency);
const incursionStrength = getIncursionStrength(store.day, store.hour);
const studySpeedMult = getStudySpeedMultiplier(store.skills);
const studyCostMult = getStudyCostMultiplier(store.skills);
// 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;
// Effective regen
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus) * meditationMultiplier;
// Get all active spells from equipment
const activeEquipmentSpells = getActiveEquipmentSpells(store.equippedInstances, store.equipmentInstances);
// Compute total DPS
const totalDPS = getTotalDPS(store, upgradeEffects, floorElem);
// Auto-gather while holding
useEffect(() => {
if (!isGathering) return;
let lastGatherTime = 0;
const minGatherInterval = 100;
let animationFrameId: number;
const gatherLoop = (timestamp: number) => {
if (timestamp - lastGatherTime >= minGatherInterval) {
store.gatherMana();
lastGatherTime = timestamp;
}
animationFrameId = requestAnimationFrame(gatherLoop);
};
animationFrameId = requestAnimationFrame(gatherLoop);
return () => cancelAnimationFrame(animationFrameId);
}, [isGathering, store]);
// Handle gather button events
const handleGatherStart = () => {
setIsGathering(true);
store.gatherMana();
};
const handleGatherEnd = () => {
setIsGathering(false);
};
// Start game loop
useEffect(() => {
const cleanup = gameLoop.start();
return cleanup;
}, [gameLoop]);
// Check if spell can be cast
const canCastSpell = (spellId: string): boolean => {
const spell = SPELLS_DEF[spellId];
if (!spell) return false;
return canAffordSpellCost(spell.cost, store.rawMana, store.elements);
};
// Game Over Screen
if (store.gameOver) {
return ( return (
<div className="fixed inset-0 game-overlay flex items-center justify-center z-50"> <div className="fixed inset-0 game-overlay flex items-center justify-center z-50">
<Card className="bg-gray-900 border-gray-600 max-w-md w-full mx-4 shadow-2xl"> <Card className="bg-gray-900 border-gray-600 max-w-md w-full mx-4 shadow-2xl">
@@ -175,31 +91,51 @@ export default function ManaLoopGame() {
</Card> </Card>
</div> </div>
); );
}
interface LeftPanelProps {
store: any;
effectiveRegen: number;
incursionStrength: number;
}
function LeftPanel({ store, effectiveRegen, incursionStrength }: LeftPanelProps) {
const [isGathering, setIsGathering] = useState(false);
const handleGatherStart = () => {
setIsGathering(true);
store.gatherMana();
};
const handleGatherEnd = () => {
setIsGathering(false);
};
useEffect(() => {
if (!isGathering) return;
let lastGatherTime = 0;
const minGatherInterval = 100;
let animationFrameId: number;
const gatherLoop = (timestamp: number) => {
if (timestamp - lastGatherTime >= minGatherInterval) {
store.gatherMana();
lastGatherTime = timestamp;
} }
animationFrameId = requestAnimationFrame(gatherLoop);
};
animationFrameId = requestAnimationFrame(gatherLoop);
return () => cancelAnimationFrame(animationFrameId);
}, [isGathering, store]);
const maxMana = computeMaxMana(store, getUnifiedEffects(store));
const clickMana = computeClickMana(store);
const meditationMultiplier = getMeditationBonus(store.meditateTicks, store.skills, getUnifiedEffects(store).meditationEfficiency);
return ( return (
<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={store.day}
hour={store.hour}
insight={store.insight}
/>
</div>
</div>
</header>
{/* Main Content */}
<main className="flex-1 flex flex-col md:flex-row gap-4 p-4">
{/* Left Panel - Mana & Actions */}
<div className="md:w-80 space-y-4 flex-shrink-0"> <div className="md:w-80 space-y-4 flex-shrink-0">
{/* Mana Display */}
<DebugName name="ManaDisplay"> <DebugName name="ManaDisplay">
<ManaDisplay <ManaDisplay
rawMana={store.rawMana} rawMana={store.rawMana}
@@ -214,7 +150,6 @@ export default function ManaLoopGame() {
/> />
</DebugName> </DebugName>
{/* Climb the Spire Button - only show when not in Spire Mode */}
{!store.spireMode && ( {!store.spireMode && (
<DebugName name="ClimbSpireButton"> <DebugName name="ClimbSpireButton">
<Button <Button
@@ -228,7 +163,6 @@ export default function ManaLoopGame() {
</DebugName> </DebugName>
)} )}
{/* Action Buttons - only show when not in Spire Mode */}
{!store.spireMode && ( {!store.spireMode && (
<DebugName name="ActionButtons"> <DebugName name="ActionButtons">
<ActionButtons <ActionButtons
@@ -243,7 +177,6 @@ export default function ManaLoopGame() {
</DebugName> </DebugName>
)} )}
{/* Calendar */}
<DebugName name="CalendarDisplay"> <DebugName name="CalendarDisplay">
<CalendarDisplay <CalendarDisplay
day={store.day} day={store.day}
@@ -251,105 +184,88 @@ export default function ManaLoopGame() {
incursionStrength={incursionStrength} incursionStrength={incursionStrength}
/> />
</DebugName> </DebugName>
</div>
);
}
{/* Loot and Achievements moved to tabs */} interface MainTabsProps {
store: any;
upgradeEffects: any;
maxMana: number;
baseRegen: number;
clickMana: number;
meditationMultiplier: number;
effectiveRegen: number;
incursionStrength: number;
manaCascadeBonus: number;
studySpeedMult: number;
studyCostMult: number;
manaWaterfallBonus: number;
hasManaWaterfall: boolean;
hasFlowSurge: boolean;
hasManaOverflow: boolean;
hasEternalFlow: boolean;
}
function MainTabs({
store,
upgradeEffects,
maxMana,
baseRegen,
clickMana,
meditationMultiplier,
effectiveRegen,
incursionStrength,
manaCascadeBonus,
studySpeedMult,
studyCostMult,
manaWaterfallBonus,
hasManaWaterfall,
hasFlowSurge,
hasManaOverflow,
hasEternalFlow,
}: MainTabsProps) {
const [activeTab, setActiveTab] = useState('spire');
const renderGrimoireTab = (): JSX.Element => {
const grimoireSpells = Object.values(SPELLS_DEF).filter((s: any) => s.grimoire);
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> </div>
{/* Right Panel - Conditional rendering based on Spire Mode */} <ScrollArea className="h-[600px] rounded border border-gray-700 p-4">
{store.spireMode ? ( <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
/* Spire Mode - Simplified UI */ {grimoireSpells.map((spell: any) => (
<div className="flex-1 min-w-0 space-y-4"> <div
<DebugName name="SpireModeUI"> key={spell.id}
<div className="flex items-center justify-between mb-4"> className="p-4 bg-gray-800/50 rounded border border-gray-600 hover:border-gray-500 transition-colors"
<h2 className="text-2xl font-bold game-title text-amber-400"> >
🏔 Spire Mode - Floor {store.currentFloor} <div className="flex items-start justify-between mb-2">
</h2> <span className="font-bold text-gray-100">{spell.name}</span>
<div className="flex gap-2 items-center"> <Badge variant="outline" className="border-gray-600">
{/* Show Climbing indicator when actively climbing */} {spell.element}
{store.currentAction === 'climb' && !store.isDescending && (
<Badge className="bg-green-900/50 text-green-300 border-green-600">
Climbing
</Badge> </Badge>
)} </div>
<Button <p className="text-sm text-gray-400 mb-3">{spell.desc}</p>
variant="outline" <div className="text-xs text-gray-500 space-y-1">
className="border-blue-600/50 text-blue-400 hover:bg-blue-900/20" <div>Cost: {(spell.cost as any[]).map((c: any) => `${c.amount} ${c.type}`).join(', ')}</div>
onClick={() => store.climbDownFloor()} <div>Power: {spell.power}</div>
disabled={store.isDescending} {spell.effect && <div>Effect: {spell.effect}</div>}
>
<ChevronDown className="w-4 h-4 mr-2" />
{store.isDescending ? 'Descending…' : 'Begin Descent'}
</Button>
<Button
variant="default"
className="bg-green-600 hover:bg-green-700"
onClick={() => store.exitSpireMode()}
>
Exit Spire
</Button>
</div> </div>
</div> </div>
))}
<Suspense fallback={<TabLoadingFallback />}> </div>
<SpireTab store={store} simpleMode={true} /> </ScrollArea>
</Suspense> </div>
);
{/* Activity Log for Spire Mode */}
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-xs">Activity Log</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-48">
<div className="space-y-1">
{(store.activityLog || []).slice(0, 50).map((entry: ActivityLogEntry, i) => {
// Style based on event type
const getEventStyle = (eventType: string) => {
switch (eventType) {
case 'enemy_defeated':
case 'floor_cleared':
return 'text-green-400';
case 'damage_dealt':
return 'text-red-400';
case 'dodge':
return 'text-yellow-400';
case 'armor_proc':
return 'text-blue-400';
case 'special_effect':
return 'text-purple-400';
case 'floor_transition':
return 'text-cyan-400';
case 'spell_cast':
return 'text-amber-400';
case 'golem_attack':
return 'text-orange-400';
case 'puzzle_solved':
return 'text-pink-400';
default:
return 'text-gray-300';
}
}; };
return ( return (
<div
key={entry.id}
className={`text-xs ${i === 0 ? 'text-gray-200 font-semibold' : getEventStyle(entry.eventType)}`}
>
{entry.message}
</div>
);
})}
{(store.activityLog || []).length === 0 && (
<div className="text-xs text-gray-500 italic">No activity yet...</div>
)}
</div>
</ScrollArea>
</CardContent>
</Card>
</DebugName>
</div>
) : (
/* Normal Mode - Tabs */
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<Tabs value={activeTab} onValueChange={setActiveTab}> <Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto"> <TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
@@ -461,6 +377,11 @@ export default function ManaLoopGame() {
effectiveRegen={effectiveRegen} effectiveRegen={effectiveRegen}
incursionStrength={incursionStrength} incursionStrength={incursionStrength}
manaCascadeBonus={manaCascadeBonus} manaCascadeBonus={manaCascadeBonus}
manaWaterfallBonus={manaWaterfallBonus}
hasManaWaterfall={hasManaWaterfall}
hasFlowSurge={hasFlowSurge}
hasManaOverflow={hasManaOverflow}
hasEternalFlow={hasEternalFlow}
studySpeedMult={studySpeedMult} studySpeedMult={studySpeedMult}
studyCostMult={studyCostMult} studyCostMult={studyCostMult}
/> />
@@ -483,10 +404,209 @@ export default function ManaLoopGame() {
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>
);
}
export default function ManaLoopGame() {
const [selectedManaType, setSelectedManaType] = useState<string>('');
// Game store
const store: any = useGameStore();
const gameLoop = useGameLoop();
// Computed effects from upgrades and equipment
const upgradeEffects = getUnifiedEffects(store);
// Get unlocked elements for mana type selector
Object.entries(ELEMENTS)
.filter(([id]) => store.elements[id]?.unlocked)
.map(([id, elem]) => ({ id, name: elem.name, sym: elem.sym, color: elem.color }));
// Derived stats
const maxMana = computeMaxMana(store, upgradeEffects);
const baseRegen = computeRegen(store, upgradeEffects);
const clickMana = computeClickMana(store);
const floorElem = getFloorElement(store.currentFloor);
const floorElemDef = ELEMENTS[floorElem];
const isGuardianFloor = !!GUARDIANS[store.currentFloor];
const currentGuardian = GUARDIANS[store.currentFloor];
const meditationMultiplier = getMeditationBonus(store.meditateTicks, store.skills, upgradeEffects.meditationEfficiency);
const incursionStrength = getIncursionStrength(store.day, store.hour);
const studySpeedMult = getStudySpeedMultiplier(store.skills);
const studyCostMult = getStudyCostMultiplier(store.skills);
// 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;
// Special effects flags for mana features
const hasManaWaterfall = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL);
const hasFlowSurge = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.FLOW_SURGE);
const hasManaOverflow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_OVERFLOW);
const hasEternalFlow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.ETERNAL_FLOW);
// Effective regen
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
// Get all active spells from equipment
const activeEquipmentSpells = getActiveEquipmentSpells(store.equippedInstances, store.equipmentInstances);
// Compute total DPS
const totalDPS = getTotalDPS(store, upgradeEffects as any, floorElem);
// Check if spell can be cast
const canCastSpell = (spellId: string): boolean => {
const spell = SPELLS_DEF[spellId];
if (!spell) return false;
return canAffordSpellCost(spell.cost, store.rawMana, store.elements);
};
// Game Over Screen
if (store.gameOver) {
return <GameOverScreen store={store} />;
}
// Start game loop
useEffect(() => {
const cleanup = gameLoop.start();
return cleanup;
}, [gameLoop]);
return (
<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={store.day} hour={store.hour} insight={store.insight} />
</div>
</div>
</header>
{/* Main Content */}
<main className="flex-1 flex flex-col md:flex-row gap-4 p-4">
<LeftPanel store={store} effectiveRegen={effectiveRegen} incursionStrength={incursionStrength} />
{!store.spireMode ? (
<MainTabs
store={store}
upgradeEffects={upgradeEffects}
maxMana={maxMana}
baseRegen={baseRegen}
clickMana={clickMana}
meditationMultiplier={meditationMultiplier}
effectiveRegen={effectiveRegen}
incursionStrength={incursionStrength}
manaCascadeBonus={manaCascadeBonus}
manaWaterfallBonus={manaWaterfallBonus}
hasManaWaterfall={hasManaWaterfall}
hasFlowSurge={hasFlowSurge}
hasManaOverflow={hasManaOverflow}
hasEternalFlow={hasEternalFlow}
studySpeedMult={studySpeedMult}
studyCostMult={studyCostMult}
/>
) : (
/* Spire Mode - Simplified UI */
<div className="flex-1 min-w-0 space-y-4">
<DebugName name="SpireModeUI">
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold game-title text-amber-400">
🏔 Spire Mode - Floor {store.currentFloor}
</h2>
<div className="flex gap-2 items-center">
{store.currentAction === 'climb' && !store.isDescending && (
<Badge className="bg-green-900/50 text-green-300 border-green-600">Climbing</Badge>
)}
<Button
variant="outline"
className="border-blue-600/50 text-blue-400 hover:bg-blue-900/20"
onClick={() => store.climbDownFloor()}
disabled={store.isDescending}
>
<ChevronDown className="w-4 h-4 mr-2" />
{store.isDescending ? 'Descending…' : 'Begin Descent'}
</Button>
<Button
variant="default"
className="bg-green-600 hover:bg-green-700"
onClick={() => store.exitSpireMode()}
>
Exit Spire
</Button>
</div>
</div>
<Suspense fallback={<TabLoadingFallback />}>
<SpireTab store={store} simpleMode={true} />
</Suspense>
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-xs">Activity Log</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-48">
<div className="space-y-1">
{(store.activityLog || []).slice(0, 50).map((entry: ActivityLogEntry, i) => {
const getEventStyle = (eventType: string) => {
switch (eventType) {
case 'enemy_defeated':
case 'floor_cleared':
return 'text-green-400';
case 'damage_dealt':
return 'text-red-400';
case 'dodge':
return 'text-yellow-400';
case 'armor_proc':
return 'text-blue-400';
case 'special_effect':
return 'text-purple-400';
case 'floor_transition':
return 'text-cyan-400';
case 'spell_cast':
return 'text-amber-400';
case 'golem_attack':
return 'text-orange-400';
case 'puzzle_solved':
return 'text-pink-400';
default:
return 'text-gray-300';
}
};
return (
<div
key={entry.id}
className={`text-xs ${i === 0 ? 'text-gray-200 font-semibold' : getEventStyle(entry.eventType)}`}
>
{entry.message}
</div>
);
})}
{(store.activityLog || []).length === 0 && (
<div className="text-xs text-gray-500 italic">No activity yet...</div>
)}
</div>
</ScrollArea>
</CardContent>
</Card>
</DebugName>
</div>
)} )}
</main> </main>
</div> </div>
</TooltipProvider> </TooltipProvider>
); );
} }