2130d30133
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m33s
- 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
245 lines
10 KiB
TypeScript
Executable File
245 lines
10 KiB
TypeScript
Executable File
'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';
|