Initial commit
All checks were successful
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m45s

This commit is contained in:
Z User
2026-03-28 15:00:03 +00:00
commit 22dfdb5910
134 changed files with 27218 additions and 0 deletions

439
src/app/page.tsx Executable file
View File

@@ -0,0 +1,439 @@
'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 } from '@/components/game/tabs';
import { ActionButtons, CalendarDisplay, ManaDisplay, TimeDisplay } from '@/components/game';
import { LootInventoryDisplay } from '@/components/game/LootInventory';
import { AchievementsDisplay } from '@/components/game/AchievementsDisplay';
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 */}
<ManaDisplay
rawMana={store.rawMana}
maxMana={maxMana}
effectiveRegen={effectiveRegen}
meditationMultiplier={meditationMultiplier}
clickMana={clickMana}
isGathering={isGathering}
onGatherStart={handleGatherStart}
onGatherEnd={handleGatherEnd}
elements={store.elements}
/>
{/* Action Buttons */}
<ActionButtons
currentAction={store.currentAction}
designProgress={store.designProgress}
preparationProgress={store.preparationProgress}
applicationProgress={store.applicationProgress}
setAction={store.setAction}
/>
{/* Calendar */}
<CalendarDisplay
day={store.day}
hour={store.hour}
incursionStrength={incursionStrength}
/>
{/* Loot Inventory */}
<LootInventoryDisplay
inventory={store.lootInventory}
elements={store.elements}
equipmentInstances={store.equipmentInstances}
onDeleteMaterial={store.deleteMaterial}
onDeleteEquipment={store.deleteEquipmentInstance}
/>
{/* Achievements */}
<AchievementsDisplay
achievements={store.achievements}
gameState={{
maxFloorReached: store.maxFloorReached,
totalManaGathered: store.totalManaGathered,
signedPacts: store.signedPacts,
totalSpellsCast: store.totalSpellsCast,
totalDamageDealt: store.totalDamageDealt,
totalCraftsCompleted: store.totalCraftsCompleted,
combo: store.combo,
}}
/>
</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="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="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">
<SpireTab store={store} />
</TabsContent>
<TabsContent value="attunements">
<AttunementsTab store={store} />
</TabsContent>
<TabsContent value="skills">
<SkillsTab store={store} />
</TabsContent>
<TabsContent value="spells">
<SpellsTab store={store} />
</TabsContent>
<TabsContent value="equipment">
<EquipmentTab store={store} />
</TabsContent>
<TabsContent value="crafting">
<CraftingTab store={store} />
</TabsContent>
<TabsContent value="lab">
<LabTab store={store} />
</TabsContent>
<TabsContent value="stats">
<StatsTab
store={store}
upgradeEffects={upgradeEffects}
maxMana={maxMana}
baseRegen={baseRegen}
clickMana={clickMana}
meditationMultiplier={meditationMultiplier}
effectiveRegen={effectiveRegen}
incursionStrength={incursionStrength}
manaCascadeBonus={manaCascadeBonus}
studySpeedMult={studySpeedMult}
studyCostMult={studyCostMult}
/>
</TabsContent>
<TabsContent value="grimoire">
{renderGrimoireTab()}
</TabsContent>
<TabsContent value="debug">
<DebugTab store={store} />
</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';