Files
Mana-Loop/src/app/page.tsx
2026-04-03 10:02:19 +00:00

466 lines
19 KiB
TypeScript
Executable File
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 } from 'react';
import { useGameStore, useGameLoop, fmt, fmtDec, getFloorElement, computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, getIncursionStrength, canAffordSpellCost } from '@/lib/game/store';
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
import { getDamageBreakdown } from '@/lib/game/computed-stats';
import { ELEMENTS, GUARDIANS, SPELLS_DEF, PRESTIGE_DEF, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution';
import { getUnifiedEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
import { formatHour } from '@/lib/game/formatting';
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 { RotateCcw } from 'lucide-react';
import { CraftingTab, SpireTab, SpellsTab, LabTab, SkillsTab, StatsTab, EquipmentTab, AttunementsTab, DebugTab, LootTab, AchievementsTab, GolemancyTab } from '@/components/game/tabs';
import { ActionButtons, CalendarDisplay, ManaDisplay, TimeDisplay } from '@/components/game';
// Loot and Achievements moved to separate tabs
import { DebugName } from '@/lib/game/debug-context';
export default function ManaLoopGame() {
const [activeTab, setActiveTab] = useState('spire');
const [isGathering, setIsGathering] = useState(false);
// Game store
const store = useGameStore();
const gameLoop = useGameLoop();
// Computed effects from upgrades and equipment
const upgradeEffects = getUnifiedEffects(store);
// 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 (
<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">
<CardHeader>
<CardTitle className={`text-3xl text-center game-title ${store.victory ? 'text-amber-400' : 'text-red-400'}`}>
{store.victory ? 'VICTORY!' : 'LOOP ENDS'}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-center text-gray-400">
{store.victory
? 'The Awakened One falls! Your power echoes through eternity.'
: 'The time loop resets... but you remember.'}
</p>
<div className="grid grid-cols-2 gap-3">
<div className="p-3 bg-gray-800 rounded">
<div className="text-xl font-bold text-amber-400 game-mono">{fmt(store.loopInsight)}</div>
<div className="text-xs text-gray-400">Insight Gained</div>
</div>
<div className="p-3 bg-gray-800 rounded">
<div className="text-xl font-bold text-blue-400 game-mono">{store.maxFloorReached}</div>
<div className="text-xs text-gray-400">Best Floor</div>
</div>
<div className="p-3 bg-gray-800 rounded">
<div className="text-xl font-bold text-purple-400 game-mono">{store.signedPacts.length}</div>
<div className="text-xs text-gray-400">Pacts Signed</div>
</div>
<div className="p-3 bg-gray-800 rounded">
<div className="text-xl font-bold text-green-400 game-mono">{store.loopCount + 1}</div>
<div className="text-xs text-gray-400">Total Loops</div>
</div>
</div>
<Button
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
size="lg"
onClick={() => store.startNewLoop()}
>
Begin New Loop
</Button>
</CardContent>
</Card>
</div>
);
}
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}
isPaused={store.isPaused}
togglePause={store.togglePause}
/>
</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">
{/* Mana Display */}
<DebugName name="ManaDisplay">
<ManaDisplay
rawMana={store.rawMana}
maxMana={maxMana}
effectiveRegen={effectiveRegen}
meditationMultiplier={meditationMultiplier}
clickMana={clickMana}
isGathering={isGathering}
onGatherStart={handleGatherStart}
onGatherEnd={handleGatherEnd}
elements={store.elements}
/>
</DebugName>
{/* Action Buttons */}
<DebugName name="ActionButtons">
<ActionButtons
currentAction={store.currentAction}
designProgress={store.designProgress}
preparationProgress={store.preparationProgress}
applicationProgress={store.applicationProgress}
setAction={store.setAction}
/>
</DebugName>
{/* Calendar */}
<DebugName name="CalendarDisplay">
<CalendarDisplay
day={store.day}
hour={store.hour}
incursionStrength={incursionStrength}
/>
</DebugName>
{/* Loot and Achievements moved to tabs */}
</div>
{/* Right Panel - Tabs */}
<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">
<DebugName name="SpireTab">
<SpireTab store={store} />
</DebugName>
</TabsContent>
<TabsContent value="attunements">
<DebugName name="AttunementsTab">
<AttunementsTab store={store} />
</DebugName>
</TabsContent>
<TabsContent value="golemancy">
<DebugName name="GolemancyTab">
<GolemancyTab store={store} />
</DebugName>
</TabsContent>
<TabsContent value="skills">
<DebugName name="SkillsTab">
<SkillsTab store={store} />
</DebugName>
</TabsContent>
<TabsContent value="spells">
<DebugName name="SpellsTab">
<SpellsTab store={store} />
</DebugName>
</TabsContent>
<TabsContent value="equipment">
<DebugName name="EquipmentTab">
<EquipmentTab store={store} />
</DebugName>
</TabsContent>
<TabsContent value="crafting">
<DebugName name="CraftingTab">
<CraftingTab store={store} />
</DebugName>
</TabsContent>
<TabsContent value="loot">
<DebugName name="LootTab">
<LootTab store={store} />
</DebugName>
</TabsContent>
<TabsContent value="achievements">
<DebugName name="AchievementsTab">
<AchievementsTab store={store} />
</DebugName>
</TabsContent>
<TabsContent value="lab">
<DebugName name="LabTab">
<LabTab store={store} />
</DebugName>
</TabsContent>
<TabsContent value="stats">
<DebugName name="StatsTab">
<StatsTab
store={store}
upgradeEffects={upgradeEffects}
maxMana={maxMana}
baseRegen={baseRegen}
clickMana={clickMana}
meditationMultiplier={meditationMultiplier}
effectiveRegen={effectiveRegen}
incursionStrength={incursionStrength}
manaCascadeBonus={manaCascadeBonus}
studySpeedMult={studySpeedMult}
studyCostMult={studyCostMult}
/>
</DebugName>
</TabsContent>
<TabsContent value="grimoire">
<DebugName name="GrimoireTab">
{renderGrimoireTab()}
</DebugName>
</TabsContent>
<TabsContent value="debug">
<DebugName name="DebugTab">
<DebugTab store={store} />
</DebugName>
</TabsContent>
</Tabs>
</div>
</main>
</div>
</TooltipProvider>
);
// Grimoire Tab (Prestige)
function renderGrimoireTab() {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Current Status */}
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-xs">Loop Status</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div className="p-3 bg-gray-800/50 rounded">
<div className="text-2xl font-bold text-amber-400 game-mono">{store.loopCount}</div>
<div className="text-xs text-gray-400">Loops Completed</div>
</div>
<div className="p-3 bg-gray-800/50 rounded">
<div className="text-2xl font-bold text-purple-400 game-mono">{fmt(store.insight)}</div>
<div className="text-xs text-gray-400">Current Insight</div>
</div>
<div className="p-3 bg-gray-800/50 rounded">
<div className="text-2xl font-bold text-blue-400 game-mono">{fmt(store.totalInsight)}</div>
<div className="text-xs text-gray-400">Total Insight</div>
</div>
<div className="p-3 bg-gray-800/50 rounded">
<div className="text-2xl font-bold text-green-400 game-mono">{store.memorySlots}</div>
<div className="text-xs text-gray-400">Memory Slots</div>
</div>
</div>
</CardContent>
</Card>
{/* Signed Pacts */}
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-xs">Signed Pacts</CardTitle>
</CardHeader>
<CardContent>
{store.signedPacts.length === 0 ? (
<div className="text-gray-500 text-sm">No pacts signed yet. Defeat guardians to earn pacts.</div>
) : (
<div className="space-y-2">
{store.signedPacts.map((floor) => {
const guardian = GUARDIANS[floor];
if (!guardian) return null;
return (
<div
key={floor}
className="flex items-center justify-between p-2 rounded border"
style={{ borderColor: guardian.color, backgroundColor: `${guardian.color}15` }}
>
<div>
<div className="font-semibold text-sm" style={{ color: guardian.color }}>
{guardian.name}
</div>
<div className="text-xs text-gray-400">Floor {floor}</div>
</div>
<Badge className="bg-amber-900/50 text-amber-300">
{guardian.pact}x multiplier
</Badge>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
{/* Prestige Upgrades */}
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-xs">Insight Upgrades (Permanent)</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{Object.entries(PRESTIGE_DEF).map(([id, def]) => {
const level = store.prestigeUpgrades[id] || 0;
const maxed = level >= def.max;
const canBuy = !maxed && store.insight >= def.cost;
return (
<div
key={id}
className="p-3 rounded border border-gray-700 bg-gray-800/50"
>
<div className="flex items-center justify-between mb-2">
<div className="font-semibold text-amber-400 text-sm">{def.name}</div>
<Badge variant="outline" className="text-xs">
{level}/{def.max}
</Badge>
</div>
<div className="text-xs text-gray-400 italic mb-2">{def.desc}</div>
<Button
size="sm"
variant={canBuy ? 'default' : 'outline'}
className="w-full"
disabled={!canBuy}
onClick={() => store.doPrestige(id)}
>
{maxed ? 'Maxed' : `Upgrade (${fmt(def.cost)} insight)`}
</Button>
</div>
);
})}
</div>
{/* Reset Game Button */}
<div className="mt-4 pt-4 border-t border-gray-700">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-gray-400">Reset All Progress</div>
<div className="text-xs text-gray-500">Clear all data and start fresh</div>
</div>
<Button
size="sm"
variant="outline"
className="border-red-600/50 text-red-400 hover:bg-red-900/20"
onClick={() => {
if (confirm('Are you sure you want to reset ALL progress? This cannot be undone!')) {
store.resetGame();
}
}}
>
<RotateCcw className="w-4 h-4 mr-1" />
Reset
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
}
// Import TooltipProvider
import { TooltipProvider } from '@/components/ui/tooltip';