fix: hydration mismatch, production Dockerfile, SSR localStorage guard, SpellsTab/SkillsTab/debug store migrations
Build and Publish Mana Loop Docker Image / build-and-publish (push) Has been cancelled

This commit is contained in:
2026-05-06 11:17:12 +02:00
parent 17b3571a18
commit 496d3dde4c
6 changed files with 28 additions and 45 deletions
+15 -8
View File
@@ -1,6 +1,7 @@
'use client'; 'use client';
import { useGameStore, canAffordSpellCost, fmt } from '@/lib/game/stores'; import { canAffordSpellCost, fmt } from '@/lib/game/stores';
import { useCombatStore, useSkillStore, useManaStore } from '@/lib/game/stores';
import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants'; import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
import { useStudyStats } from '@/lib/game/hooks/useGameDerived'; import { useStudyStats } from '@/lib/game/hooks/useGameDerived';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -32,7 +33,13 @@ function formatStudyTime(hours: number): string {
} }
export function SpellsTab() { export function SpellsTab() {
const store = useGameStore(); const spells = useCombatStore((s) => s.spells);
const activeSpell = useCombatStore((s) => s.activeSpell);
const setSpell = useCombatStore((s) => s.setSpell);
const currentStudyTarget = useSkillStore((s) => s.currentStudyTarget);
const setCurrentStudyTarget = useSkillStore((s) => s.setCurrentStudyTarget);
const rawMana = useManaStore((s) => s.rawMana);
const elements = useManaStore((s) => s.elements);
const { studySpeedMult, studyCostMult } = useStudyStats(); const { studySpeedMult, studyCostMult } = useStudyStats();
const spellTiers = [0, 1, 2, 3, 4]; const spellTiers = [0, 1, 2, 3, 4];
@@ -51,13 +58,13 @@ export function SpellsTab() {
<h3 className={`text-lg font-semibold mb-3 ${tierColors[tier]}`}>{tierNames[tier]}</h3> <h3 className={`text-lg font-semibold mb-3 ${tierColors[tier]}`}>{tierNames[tier]}</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{spellsInTier.map(([id, def]) => { {spellsInTier.map(([id, def]) => {
const state = store.spells?.[id]; const state = spells?.[id];
const learned = state?.learned; const learned = state?.learned;
const isStudying = store.currentStudyTarget?.id === id; const isStudying = currentStudyTarget?.id === id;
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem]; const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
const baseStudyTime = def.studyTime || (def.tier * 4); const baseStudyTime = def.studyTime || (def.tier * 4);
const isActive = store.activeSpell === id; const isActive = activeSpell === id;
const canCast = learned && canAffordSpellCost(def.cost, store.rawMana, store.elements); const canCast = learned && canAffordSpellCost(def.cost, rawMana, elements);
// Apply skill modifiers // Apply skill modifiers
const studyTime = baseStudyTime / studySpeedMult; const studyTime = baseStudyTime / studySpeedMult;
@@ -116,7 +123,7 @@ export function SpellsTab() {
<Badge className="bg-green-900/50 text-green-300">Learned</Badge> <Badge className="bg-green-900/50 text-green-300">Learned</Badge>
{isActive && <Badge className="bg-amber-900/50 text-amber-300">Active</Badge>} {isActive && <Badge className="bg-amber-900/50 text-amber-300">Active</Badge>}
{!isActive && ( {!isActive && (
<Button size="sm" variant="outline" onClick={() => store.setSpell(id)}> <Button size="sm" variant="outline" onClick={() => setSpell(id)}>
Set Active Set Active
</Button> </Button>
)} )}
@@ -147,7 +154,7 @@ export function SpellsTab() {
variant={canStudy ? 'default' : 'outline'} variant={canStudy ? 'default' : 'outline'}
disabled={!canStudy} disabled={!canStudy}
className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'} className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'}
onClick={() => store.setCurrentStudy?.(id, 'spell')} onClick={() => setCurrentStudyTarget({ type: 'spell', id, progress: 0, required: studyTime })}
> >
Start Study ({fmt(unlockCost)} mana) Start Study ({fmt(unlockCost)} mana)
</Button> </Button>
@@ -4,7 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { RotateCcw } from 'lucide-react'; import { RotateCcw } from 'lucide-react';
import { fmt } from '@/lib/game/stores'; import { fmt } from '@/lib/game/stores';
import { useCombatStore, useSkillStore, usePrestigeStore, useManaStore } from '@/lib/game/stores'; import { useCombatStore, usePrestigeStore, useManaStore } from '@/lib/game/stores';
export function LoopStatsSection() { export function LoopStatsSection() {
const spells = useCombatStore((s) => s.spells); const spells = useCombatStore((s) => s.spells);
@@ -14,7 +14,7 @@ export function LoopStatsSection() {
const maxFloorReached = useCombatStore((s) => s.maxFloorReached); const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
const totalManaGathered = useManaStore((s) => s.totalManaGathered); const totalManaGathered = useManaStore((s) => s.totalManaGathered);
const loopCount = usePrestigeStore((s) => s.loopCount); const loopCount = usePrestigeStore((s) => s.loopCount);
const memorySlots = useSkillStore((s) => s.memorySlots); const memorySlots = usePrestigeStore((s) => s.memorySlots);
const spellsLearned = Object.values(spells || {}).filter((s: any) => s.learned).length; const spellsLearned = Object.values(spells || {}).filter((s: any) => s.learned).length;
const totalSkillLevels = Object.values(skills || {}).reduce((a: number, b: number) => a + b, 0); const totalSkillLevels = Object.values(skills || {}).reduce((a: number, b: number) => a + b, 0);
@@ -4,12 +4,12 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Sparkles, Unlock } from 'lucide-react'; import { Sparkles, Unlock } from 'lucide-react';
import { ATTUNEMENTS_DEF } from '@/lib/game/data/attunements'; import { ATTUNEMENTS_DEF } from '@/lib/game/data/attunements';
import { usePrestigeStore } from '@/lib/game/stores'; import { useAttunementStore } from '@/lib/game/stores';
export function AttunementDebug() { export function AttunementDebug() {
const attunements = usePrestigeStore((s) => s.attunements); const attunements = useAttunementStore((s) => s.attunements);
const debugUnlockAttunement = usePrestigeStore((s) => s.debugUnlockAttunement); const debugUnlockAttunement = useAttunementStore((s) => s.debugUnlockAttunement);
const debugAddAttunementXP = usePrestigeStore((s) => s.debugAddAttunementXP); const debugAddAttunementXP = useAttunementStore((s) => s.debugAddAttunementXP);
const handleUnlockAttunement = (id: string) => { const handleUnlockAttunement = (id: string) => {
if (debugUnlockAttunement) { if (debugUnlockAttunement) {
+3 -3
View File
@@ -9,15 +9,15 @@ import { ELEMENTS } from '@/lib/game/constants';
export function ElementDebug() { export function ElementDebug() {
const elements = useManaStore((s) => s.elements); const elements = useManaStore((s) => s.elements);
const unlockElement = useManaStore((s) => s.unlockElement); const unlockElement = useManaStore((s) => s.unlockElement);
const debugAddElementalMana = useManaStore((s) => s.debugAddElementalMana); const addElementMana = useManaStore((s) => s.addElementMana);
const handleUnlockElement = (element: string) => { const handleUnlockElement = (element: string) => {
unlockElement(element, 500); unlockElement(element, 500);
}; };
const handleAddElementalMana = (element: string, amount: number) => { const handleAddElementalMana = (element: string, amount: number) => {
if (debugAddElementalMana) { if (addElementMana) {
debugAddElementalMana(element, amount); addElementMana(element, amount);
} }
}; };
+2 -27
View File
@@ -3,7 +3,7 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Bug } from 'lucide-react'; import { Bug } from 'lucide-react';
import { usePrestigeStore, useManaStore, useUIStore } from '@/lib/game/stores'; import { usePrestigeStore, useManaStore, useUIStore, useGameStore } from '@/lib/game/stores';
import { GUARDIANS, ELEMENTS } from '@/lib/game/constants'; import { GUARDIANS, ELEMENTS } from '@/lib/game/constants';
export function PactDebug() { export function PactDebug() {
@@ -59,32 +59,7 @@ export function PactDebug() {
}; };
debugSetPactDetails(newSignedPactDetails); debugSetPactDetails(newSignedPactDetails);
// Unlock mana types addLog(`📜 DEBUG: Pact with ${guardian.name} force-signed!`);
for (const elemId of guardian.unlocksMana) {
if (!elements[elemId]?.unlocked) {
unlockElement(elemId, 500);
}
}
// Check for compound element unlocks
const currentElements = useManaStore.getState().elements;
const unlockedSet = new Set(
Object.entries(currentElements)
.filter(([, e]) => e.unlocked)
.map(([id]) => id)
);
for (const [elemId, elemDef] of Object.entries(ELEMENTS)) {
if (elemDef.recipe && !currentElements[elemId]?.unlocked) {
const canUnlock = elemDef.recipe.every((comp: string) => unlockedSet.has(comp));
if (canUnlock) {
unlockElement(elemId, 500);
addLog(`🔮 ${elemDef.name} mana unlocked through component synergy!`);
}
}
}
addLog(`📜 DEBUG: Pact with ${guardian.name} force-signed! ${guardian.unlocksMana.map(e => ELEMENTS[e]?.name || e).join(', ')} mana unlocked!`);
}; };
// Remove a pact // Remove a pact
+2 -1
View File
@@ -5,7 +5,8 @@ export { CraftingTab } from './CraftingTab';
export { SpireTab } from './SpireTab'; export { SpireTab } from './SpireTab';
export { SpellsTab } from './SpellsTab'; export { SpellsTab } from './SpellsTab';
export { LabTab } from './LabTab'; export { LabTab } from './LabTab';
export { SkillsTab } from './SkillsTab'; // SkillsTab is now exported from src/components/game/index.ts
// export { SkillsTab } from './SkillsTab';
export { StatsTab } from './StatsTab'; export { StatsTab } from './StatsTab';
export { EquipmentTab } from './EquipmentTab'; export { EquipmentTab } from './EquipmentTab';
export { AttunementsTab } from './AttunementsTab'; export { AttunementsTab } from './AttunementsTab';