Files
Mana-Loop/src/components/game/tabs/CombatStatsPanel.tsx
T
Refactoring Agent dc38445225
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m11s
refactor: extract components from SpireTab.tsx to reduce below 400 lines
2026-05-01 17:02:32 +02:00

165 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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);
}