167 lines
7.8 KiB
TypeScript
Executable File
167 lines
7.8 KiB
TypeScript
Executable File
'use client';
|
||
|
||
import { useGameStore, canAffordSpellCost } from '@/lib/game/store';
|
||
import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
|
||
import { useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Progress } from '@/components/ui/progress';
|
||
|
||
// Format spell cost for display
|
||
function formatSpellCost(cost: { type: 'raw' | 'element'; element?: string; amount: number }): string {
|
||
if (cost.type === 'raw') {
|
||
return `${cost.amount} raw`;
|
||
}
|
||
const elemDef = ELEMENTS[cost.element || ''];
|
||
return `${cost.amount} ${elemDef?.sym || '?'}`;
|
||
}
|
||
|
||
// Get cost color
|
||
function getSpellCostColor(cost: { type: 'raw' | 'element'; element?: string; amount: number }): string {
|
||
if (cost.type === 'raw') {
|
||
return '#60A5FA';
|
||
}
|
||
return ELEMENTS[cost.element || '']?.color || '#9CA3AF';
|
||
}
|
||
|
||
// Format study time
|
||
function formatStudyTime(hours: number): string {
|
||
if (hours < 1) return `${Math.round(hours * 60)}m`;
|
||
return `${hours.toFixed(1)}h`;
|
||
}
|
||
|
||
export function SpellsTab() {
|
||
const store = useGameStore();
|
||
const { studySpeedMult, studyCostMult } = useStudyStats();
|
||
|
||
const spellTiers = [0, 1, 2, 3, 4];
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{spellTiers.map(tier => {
|
||
const spellsInTier = Object.entries(SPELLS_DEF).filter(([, def]) => def.tier === tier);
|
||
if (spellsInTier.length === 0) return null;
|
||
|
||
const tierNames = ['Basic Spells (Raw Mana)', 'Tier 1 - Elemental', 'Tier 2 - Advanced', 'Tier 3 - Master', 'Tier 4 - Legendary'];
|
||
const tierColors = ['text-gray-400', 'text-green-400', 'text-blue-400', 'text-purple-400', 'text-amber-400'];
|
||
|
||
return (
|
||
<div key={tier}>
|
||
<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">
|
||
{spellsInTier.map(([id, def]) => {
|
||
const state = store.spells[id];
|
||
const learned = state?.learned;
|
||
const isStudying = store.currentStudyTarget?.id === id;
|
||
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
|
||
const baseStudyTime = def.studyTime || (def.tier * 4);
|
||
const isActive = store.activeSpell === id;
|
||
const canCast = learned && canAffordSpellCost(def.cost, store.rawMana, store.elements);
|
||
|
||
// Apply skill modifiers
|
||
const studyTime = baseStudyTime / studySpeedMult;
|
||
const unlockCost = Math.floor(def.unlock * studyCostMult);
|
||
|
||
// Can start studying?
|
||
const canStudy = !learned && !isStudying && store.rawMana >= unlockCost;
|
||
|
||
return (
|
||
<Card
|
||
key={id}
|
||
className={`bg-gray-900/80 border-gray-700 ${learned ? '' : 'opacity-75'} ${isStudying ? 'border-purple-500' : ''} ${canCast ? 'ring-1 ring-green-500/30' : ''}`}
|
||
>
|
||
<CardHeader className="pb-2">
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="text-sm game-panel-title" style={{ color: def.elem === 'raw' ? '#60A5FA' : elemDef?.color }}>
|
||
{def.name}
|
||
</CardTitle>
|
||
{def.tier > 0 && (
|
||
<Badge variant="outline" className="text-xs">
|
||
T{def.tier}
|
||
</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 className="mr-2">⚔️ {def.dmg} dmg</span>
|
||
</div>
|
||
|
||
{/* Cost display */}
|
||
<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>
|
||
)}
|
||
|
||
{def.effects && def.effects.length > 0 && (
|
||
<div className="flex gap-1 flex-wrap">
|
||
{def.effects.map((eff, i) => (
|
||
<Badge key={i} variant="outline" className="text-xs">
|
||
{eff.type === 'burn' && `🔥 Burn`}
|
||
{eff.type === 'stun' && `⚡ Stun`}
|
||
{eff.type === 'pierce' && `🎯 Pierce`}
|
||
{eff.type === 'multicast' && `✨ Multicast`}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{learned ? (
|
||
<div className="flex gap-2">
|
||
<Badge className="bg-green-900/50 text-green-300">Learned</Badge>
|
||
{isActive && <Badge className="bg-amber-900/50 text-amber-300">Active</Badge>}
|
||
{!isActive && (
|
||
<Button size="sm" variant="outline" onClick={() => store.setSpell(id)}>
|
||
Set Active
|
||
</Button>
|
||
)}
|
||
</div>
|
||
) : isStudying ? (
|
||
<div className="space-y-1">
|
||
<Progress
|
||
value={Math.min(100, ((state?.studyProgress || 0) / studyTime) * 100)}
|
||
className="h-2 bg-gray-800"
|
||
/>
|
||
<div className="text-xs text-purple-400">
|
||
Studying... {formatStudyTime(state?.studyProgress || 0)}/{formatStudyTime(studyTime)}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-2">
|
||
<div className="text-xs text-gray-500">
|
||
<span className={studySpeedMult > 1 ? 'text-green-400' : ''}>
|
||
Study: {formatStudyTime(studyTime)}{studySpeedMult > 1 && <span className="text-xs ml-1">({Math.round(studySpeedMult * 100)}% speed)</span>}
|
||
</span>
|
||
{' • '}
|
||
<span className={studyCostMult < 1 ? 'text-green-400' : ''}>
|
||
Cost: {fmt(unlockCost)} mana{studyCostMult < 1 && <span className="text-xs ml-1">({Math.round(studyCostMult * 100)}% cost)</span>}
|
||
</span>
|
||
</div>
|
||
<Button
|
||
size="sm"
|
||
variant={canStudy ? 'default' : 'outline'}
|
||
disabled={!canStudy}
|
||
className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'}
|
||
onClick={() => store.startStudyingSpell(id)}
|
||
>
|
||
Start Study ({fmt(unlockCost)} mana)
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|