Files
Mana-Loop/src/components/game/tabs/SpellsTab.tsx
T
n8n-gitea 2130d30133
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m33s
fix: resolve mana conversion, Spire/Grimoire tab errors, and legacy store references
- Fix mana conversion to deduct from regen instead of mana pool (resolves player stuck at 1 mana below cap)
- Fix Spire Tab error by removing unused legacy import (store-modules/enemy-utils)
- Fix Grimoire Tab error by adding Array.isArray check for effects.map
- Move utility functions from legacy store-modules to utils/ to eliminate legacy dependencies
- Add regression test for mana conversion fix
- Update SpellsTab.tsx imports to use utils instead of legacy stores
2026-05-08 13:48:53 +02:00

245 lines
10 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 { useCombatStore, useCraftingStore, useManaStore, usePrestigeStore } from '@/lib/game/stores';
import { GameCard, ElementBadge } from '@/components/ui';
import { Badge } from '@/components/ui/badge';
import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
import { canAffordSpellCost } from '@/lib/game/utils';
import { formatSpellCost, getSpellCostColor } from '@/lib/game/formatting';
export function SpellsTab() {
// Use modular stores directly
const spells = useCombatStore((s) => s.spells);
const activeSpell = useCombatStore((s) => s.activeSpell);
const setSpell = useCombatStore((s) => s.setSpell);
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
const rawMana = useManaStore((s) => s.rawMana);
const elements = useManaStore((s) => s.elements);
const signedPacts = usePrestigeStore((s) => s.signedPacts);
const unlockedEffects = useCraftingStore((s) => s.unlockedEffects);
// Get spells from equipment
const equipmentSpellIds: string[] = [];
const spellSources: Record<string, string[]> = {};
// Guard against undefined stores during initialization
if (!equippedInstances || !equipmentInstances) {
return (
<DebugName name="SpellsTab">
<div className="p-4 text-center text-[var(--text-muted)]">
Loading spell data...
</div>
</DebugName>);
}
for (const instanceId of Object.values(equippedInstances || {})) {
if (!instanceId) continue;
const instance = equipmentInstances[instanceId];
if (!instance) continue;
for (const ench of instance.enchantments) {
const effectDef = ENCHANTMENT_EFFECTS?.[ench.effectId];
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
const spellId = effectDef.effect.spellId;
if (!equipmentSpellIds.includes(spellId)) {
equipmentSpellIds.push(spellId);
}
if (!spellSources[spellId]) {
spellSources[spellId] = [];
}
spellSources[spellId].push(instance.name);
}
}
}
const canCastSpell = (spellId: string): boolean => {
const spell = SPELLS_DEF[spellId];
if (!spell || !spell.cost) return false;
return canAffordSpellCost(spell.cost, rawMana, elements);
};
const hasPactSpells = signedPacts && signedPacts.length > 0;
return (
<div className="space-y-6">
{/* Equipment-Granted Spells */}
<div className="mb-6">
<h3 className="text-lg font-[var(--font-heading)] font-semibold mb-3 text-[var(--mana-crystal)]">
Known Spells
</h3>
<p className="text-sm text-[var(--text-secondary)] mb-4">
Spells are obtained by enchanting equipment with spell effects.
Visit the Crafting tab to design and apply enchantments.
</p>
{equipmentSpellIds.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{equipmentSpellIds.map(id => {
const def = SPELLS_DEF[id];
if (!def) return null;
const isActive = activeSpell === id;
const canCast = canCastSpell(id);
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
const sources = spellSources[id] || [];
return (
<GameCard
key={id}
className={canCast ? 'ring-1 ring-[var(--color-success)]/30' : ''}
>
<div className="pb-2">
<div className="flex items-center justify-between">
<h4
className="text-sm font-semibold"
style={{ color: def.elem === 'raw' ? 'var(--mana-transfer)' : `var(--mana-${def.elem})` }}
>
{def.name}
</h4>
<Badge className="bg-[var(--bg-elevated)] text-[var(--mana-crystal)] text-xs border border-[var(--mana-crystal)]/30">
Equipment
</Badge>
</div>
</div>
<div className="space-y-2">
<div className="text-xs text-[var(--text-secondary)]">
{def.elem !== 'raw' && (
<span className="mr-2">
<ElementBadge element={def.elem} size="sm" /> {elemDef?.name}
</span>
)}
<span> {def.dmg} dmg</span>
</div>
<div className="text-xs font-[var(--font-mono)]" style={{ color: getSpellCostColor(def.cost) }}>
Cost: {formatSpellCost(def.cost)}
</div>
<div className="text-xs text-[var(--mana-crystal)]/70">From: {sources.join(', ')}</div>
<div className="flex gap-2">
{isActive ? (
<Badge className="bg-[var(--bg-elevated)] text-[var(--color-warning)] border border-[var(--color-warning)]/30">
Active
</Badge>
) : (
<button
className="px-3 py-1 text-xs border border-[var(--border-default)] rounded hover:border-[var(--border-focus)] transition-colors"
onClick={() => setSpell(id)}
>
Set Active
</button>
)}
</div>
</div>
</GameCard>
);
})}
</div>
) : (
<div className="text-center p-8 bg-[var(--bg-sunken)] rounded border border-[var(--border-subtle)]">
<div className="text-[var(--text-muted)] mb-2">No spells known yet</div>
<div className="text-sm text-[var(--text-muted)]">Enchant a staff with a spell effect to gain spells</div>
</div>
)}
</div>
{/* Pact Spells (from guardian defeats) - Empty State */}
{!hasPactSpells && (
<div className="mb-6">
<h3 className="text-lg font-[var(--font-heading)] font-semibold mb-3 text-[var(--color-warning)]">
Pact Spells
</h3>
<div className="text-center p-6 bg-[var(--bg-sunken)] rounded border border-[var(--border-subtle)]">
<p className="text-sm text-[var(--text-muted)]">Defeat guardians and sign pacts to unlock powerful spells</p>
</div>
</div>
)}
{hasPactSpells && (
<div className="mb-6">
<h3 className="text-lg font-[var(--font-heading)] font-semibold mb-3 text-[var(--color-warning)]">
Pact Spells
</h3>
<p className="text-sm text-[var(--text-secondary)] mb-3">Spells earned through guardian pacts appear here.</p>
</div>
)}
{/* Spell Reference - show all available spells for enchanting */}
<div className="mb-6">
<h3 className="text-lg font-[var(--font-heading)] font-semibold mb-3 text-[var(--mana-death)]">
Spell Reference
</h3>
<p className="text-sm text-[var(--text-secondary)] mb-4">
These spells can be applied to equipment through the enchanting system.
Research enchantment effects in the Skills tab to unlock them for designing.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(SPELLS_DEF).map(([id, def]) => {
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
const isUnlocked = unlockedEffects?.includes(`spell_${id}`);
return (
<GameCard
key={id}
variant={isUnlocked ? "default" : "sunken"}
className={isUnlocked ? 'border-[var(--mana-death)]/50' : 'opacity-60'}
>
<div className="pb-2">
<div className="flex items-center justify-between">
<h4
className="text-sm font-semibold"
style={{ color: def.elem === 'raw' ? 'var(--mana-transfer)' : `var(--mana-${def.elem})` }}
>
{def.name}
</h4>
<div className="flex gap-1">
{def.tier > 0 && (
<span className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
T{def.tier}
</span>
)}
{isUnlocked && (
<Badge className="bg-[var(--bg-elevated)] text-[var(--mana-death)] text-xs border border-[var(--mana-death)]/30">
Unlocked
</Badge>
)}
</div>
</div>
</div>
<div className="space-y-2">
<div className="text-xs text-[var(--text-secondary)]">
{def.elem !== 'raw' && (
<span className="mr-2">
<ElementBadge element={def.elem} size="sm" /> {elemDef?.name}
</span>
)}
<span> {def.dmg} dmg</span>
</div>
<div className="text-xs font-[var(--font-mono)]" style={{ color: getSpellCostColor(def.cost) }}>
Cost: {formatSpellCost(def.cost)}
</div>
{def.desc && (
<div className="text-xs text-[var(--text-muted)] italic">{def.desc}</div>
)}
{!isUnlocked && (
<div className="text-xs text-[var(--color-warning)]/70">Research to unlock for enchanting</div>
)}
</div>
</GameCard>
);
})}
</div>
</div>
</div>
);
}
SpellsTab.displayName = "SpellsTab";
import { DebugName } from '@/lib/game/debug-context';