165 lines
7.6 KiB
TypeScript
165 lines
7.6 KiB
TypeScript
'use client';
|
||
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Tooltip, TooltipProvider, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
|
||
import { Zap, Shield, ShieldCheck, Wind, Heart, Mountain, BookOpen } from 'lucide-react';
|
||
import { ELEMENTS } from '@/lib/game/constants';
|
||
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
|
||
import type { CombatStatsPanelProps } from '@/lib/game/types';
|
||
|
||
export function CombatStatsPanel({
|
||
activeEquipmentSpells,
|
||
store,
|
||
totalDPS,
|
||
calcDamage,
|
||
formatSpellCost,
|
||
getSpellCostColor,
|
||
SPELLS_DEF,
|
||
upgradeEffects,
|
||
canCastSpell,
|
||
studySpeedMult,
|
||
storeCurrentAction,
|
||
}: CombatStatsPanelProps) {
|
||
const activeGolems = store.golemancy.summonedGolems;
|
||
|
||
return (
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-amber-400 game-panel-title text-xs">Combat Stats</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
<div className="text-sm text-gray-400">
|
||
Total DPS: <span className="text-amber-400 font-semibold">{storeCurrentAction === 'climb' && activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'}</span>
|
||
</div>
|
||
|
||
{activeEquipmentSpells.length > 0 && (
|
||
<div className="space-y-2">
|
||
<div className="text-xs text-gray-500 font-semibold uppercase tracking-wider">Active Spells</div>
|
||
{activeEquipmentSpells.map(({ spellId, equipmentId }) => {
|
||
const spellDef = SPELLS_DEF[spellId];
|
||
if (!spellDef) return null;
|
||
const spellState = store.equipmentSpellStates?.find(
|
||
s => s.spellId === spellId && s.sourceEquipment === equipmentId
|
||
);
|
||
const progress = spellState?.castProgress || 0;
|
||
const canCast = canCastSpell(spellId);
|
||
|
||
return (
|
||
<div key={`${spellId}-${equipmentId}`} className="p-2 bg-gray-800/50 rounded border border-gray-700">
|
||
<div className="flex items-center justify-between mb-1">
|
||
<span className="text-xs font-semibold" style={{ color: spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color }}>
|
||
{spellDef.name}
|
||
{spellDef.tier === 0 && <Badge className="ml-2 bg-gray-600 text-gray-200 text-xs">Basic</Badge>}
|
||
{spellDef.tier >= 4 && <Badge className="ml-2 bg-amber-600 text-amber-100 text-xs">Legendary</Badge>}
|
||
</span>
|
||
<span className={`text-xs ${canCast ? 'text-green-400' : 'text-red-400'}`}>
|
||
{canCast ? '✓' : '✗'}
|
||
</span>
|
||
</div>
|
||
<div className="text-xs text-gray-400 mb-1">
|
||
⚔️ {fmt(calcDamage(store, spellId))} dmg • {' '}
|
||
<span style={{ color: getSpellCostColor(spellDef.cost) }}>
|
||
{formatSpellCost(spellDef.cost)}
|
||
</span>
|
||
{' '}• ⚡ {fmt(Math.floor(calcDamage(store, spellId) * (spellDef.castSpeed || 1)))} dmg/hr
|
||
</div>
|
||
{storeCurrentAction === 'climb' && (
|
||
<div className="space-y-0.5">
|
||
<div className="flex justify-between text-xs text-gray-500">
|
||
<span>Cast</span>
|
||
<span>{(progress * 100).toFixed(0)}%</span>
|
||
</div>
|
||
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
|
||
<div
|
||
className="h-full rounded-full transition-all duration-300"
|
||
style={{
|
||
width: `${Math.min(100, progress * 100)}%`,
|
||
background: `linear-gradient(90deg, ${spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color}99, ${spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color})`,
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
{activeGolems.length > 0 && (
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2 text-xs text-gray-500 font-semibold uppercase tracking-wider">
|
||
<Mountain className="w-3 h-3" />
|
||
Active Golems
|
||
</div>
|
||
{activeGolems.map((summoned) => {
|
||
const golemDef = GOLEMS_DEF[summoned.golemId];
|
||
if (!golemDef) return null;
|
||
const elemColor = ELEMENTS[golemDef.baseManaType]?.color || '#888';
|
||
const damage = getGolemDamage(summoned.golemId, store.skills);
|
||
const attackSpeed = getGolemAttackSpeed(summoned.golemId, store.skills);
|
||
|
||
return (
|
||
<div key={summoned.golemId} className="p-2 bg-gray-800/50 rounded border border-gray-700">
|
||
<div className="flex items-center justify-between mb-1">
|
||
<div className="flex items-center gap-2">
|
||
<Mountain className="w-3 h-3" style={{ color: elemColor }} />
|
||
<span className="text-xs font-semibold" style={{ color: elemColor }}>
|
||
{golemDef.name}
|
||
</span>
|
||
</div>
|
||
{golemDef.isAoe && (
|
||
<Badge variant="outline" className="text-xs py-0">AOE {golemDef.aoeTargets}</Badge>
|
||
)}
|
||
</div>
|
||
<div className="text-xs text-gray-400">
|
||
⚔️ {damage} DMG • ⚡ {attackSpeed.toFixed(1)}/hr
|
||
</div>
|
||
{storeCurrentAction === 'climb' && summoned.attackProgress > 0 && (
|
||
<div className="space-y-0.5 mt-1">
|
||
<div className="flex justify-between text-xs text-gray-500">
|
||
<span>Attack</span>
|
||
<span>{Math.min(100, (summoned.attackProgress * 100)).toFixed(0)}%</span>
|
||
</div>
|
||
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
|
||
<div
|
||
className="h-full rounded-full transition-all duration-300"
|
||
style={{
|
||
width: `${Math.min(100, summoned.attackProgress * 100)}%`,
|
||
background: `linear-gradient(90deg, ${elemColor}99, ${elemColor})`,
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
<div className="pt-2 border-t border-gray-700">
|
||
<div className="text-xs text-gray-500 mb-1">Study Speed: {Math.round(studySpeedMult * 100)}%</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
function fmt(value: number): string {
|
||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
||
return value.toFixed(0);
|
||
}
|
||
|
||
function fmtDec(value: number): string {
|
||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
||
return value.toFixed(0);
|
||
}
|