181 lines
7.8 KiB
TypeScript
Executable File
181 lines
7.8 KiB
TypeScript
Executable File
'use client';
|
||
|
||
import { Button } from '@/components/ui/button';
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
|
||
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
|
||
import { calcDamage, canAffordSpellCost, fmt } from '@/lib/game/store';
|
||
import { formatSpellCost, getSpellCostColor } from '@/lib/game/formatting';
|
||
|
||
interface SpellsTabProps {
|
||
store: {
|
||
spells: Record<string, { learned: boolean; level: number; studyProgress: number }>;
|
||
equippedInstances: Record<string, string | null>;
|
||
equipmentInstances: Record<string, { instanceId: string; name: string; enchantments: { effectId: string; stacks: number }[] }>;
|
||
activeSpell: string;
|
||
rawMana: number;
|
||
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
|
||
signedPacts: number[];
|
||
unlockedEffects: string[];
|
||
setSpell: (spellId: string) => void;
|
||
};
|
||
}
|
||
|
||
export function SpellsTab({ store }: SpellsTabProps) {
|
||
// Get spells from equipment
|
||
const equipmentSpellIds: string[] = [];
|
||
const spellSources: Record<string, string[]> = {};
|
||
|
||
for (const instanceId of Object.values(store.equippedInstances)) {
|
||
if (!instanceId) continue;
|
||
const instance = store.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) return false;
|
||
return canAffordSpellCost(spell.cost, store.rawMana, store.elements);
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Equipment-Granted Spells */}
|
||
<div className="mb-6">
|
||
<h3 className="text-lg font-semibold mb-3 text-cyan-400">✨ Known Spells</h3>
|
||
<p className="text-sm text-gray-400 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 = store.activeSpell === id;
|
||
const canCast = canCastSpell(id);
|
||
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
|
||
const sources = spellSources[id] || [];
|
||
|
||
return (
|
||
<Card
|
||
key={id}
|
||
className={`bg-gray-900/80 border-cyan-600/50 ${canCast ? 'ring-1 ring-green-500/30' : ''}`}
|
||
>
|
||
<CardHeader className="pb-2">
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="text-sm" style={{ color: def.elem === 'raw' ? '#60A5FA' : elemDef?.color }}>
|
||
{def.name}
|
||
</CardTitle>
|
||
<Badge className="bg-cyan-900/50 text-cyan-300 text-xs">Equipment</Badge>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="space-y-2">
|
||
<div className="text-xs text-gray-400">
|
||
{def.elem !== 'raw' && <span className="mr-2">{elemDef?.sym} {elemDef?.name}</span>}
|
||
<span>⚔️ {def.dmg} dmg</span>
|
||
</div>
|
||
<div className="text-xs game-mono" style={{ color: getSpellCostColor(def.cost) }}>
|
||
Cost: {formatSpellCost(def.cost)}
|
||
</div>
|
||
<div className="text-xs text-cyan-400/70">From: {sources.join(', ')}</div>
|
||
<div className="flex gap-2">
|
||
{isActive ? (
|
||
<Badge className="bg-amber-900/50 text-amber-300">Active</Badge>
|
||
) : (
|
||
<Button size="sm" variant="outline" onClick={() => store.setSpell(id)}>
|
||
Set Active
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
})}
|
||
</div>
|
||
) : (
|
||
<div className="text-center p-8 bg-gray-800/30 rounded border border-gray-700">
|
||
<div className="text-gray-500 mb-2">No spells known yet</div>
|
||
<div className="text-sm text-gray-600">Enchant a staff with a spell effect to gain spells</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Pact Spells (from guardian defeats) */}
|
||
{store.signedPacts.length > 0 && (
|
||
<div className="mb-6">
|
||
<h3 className="text-lg font-semibold mb-3 text-amber-400">🏆 Pact Spells</h3>
|
||
<p className="text-sm text-gray-400 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-semibold mb-3 text-purple-400">📚 Spell Reference</h3>
|
||
<p className="text-sm text-gray-400 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 = store.unlockedEffects?.includes(`spell_${id}`);
|
||
|
||
return (
|
||
<Card
|
||
key={id}
|
||
className={`bg-gray-900/80 border-gray-700 ${isUnlocked ? 'border-purple-500/50' : 'opacity-60'}`}
|
||
>
|
||
<CardHeader className="pb-2">
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="text-sm" style={{ color: def.elem === 'raw' ? '#60A5FA' : elemDef?.color }}>
|
||
{def.name}
|
||
</CardTitle>
|
||
<div className="flex gap-1">
|
||
{def.tier > 0 && <Badge variant="outline" className="text-xs">T{def.tier}</Badge>}
|
||
{isUnlocked && <Badge className="bg-purple-900/50 text-purple-300 text-xs">Unlocked</Badge>}
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="space-y-2">
|
||
<div className="text-xs text-gray-400">
|
||
{def.elem !== 'raw' && <span className="mr-2">{elemDef?.sym} {elemDef?.name}</span>}
|
||
<span>⚔️ {def.dmg} dmg</span>
|
||
</div>
|
||
<div className="text-xs game-mono" style={{ color: getSpellCostColor(def.cost) }}>
|
||
Cost: {formatSpellCost(def.cost)}
|
||
</div>
|
||
{def.desc && (
|
||
<div className="text-xs text-gray-500 italic">{def.desc}</div>
|
||
)}
|
||
{!isUnlocked && (
|
||
<div className="text-xs text-amber-400/70">Research to unlock for enchanting</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|