Initial commit
This commit is contained in:
180
src/components/game/tabs/SpellsTab.tsx
Executable file
180
src/components/game/tabs/SpellsTab.tsx
Executable file
@@ -0,0 +1,180 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user