144 lines
5.2 KiB
TypeScript
Executable File
144 lines
5.2 KiB
TypeScript
Executable File
'use client';
|
|
|
|
import { Progress } from '@/components/ui/progress';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Zap, Flame, Sparkles } from 'lucide-react';
|
|
import type { ComboState } from '@/lib/game/types';
|
|
import { ELEMENTS } from '@/lib/game/constants';
|
|
|
|
interface ComboMeterProps {
|
|
combo: ComboState;
|
|
isClimbing: boolean;
|
|
}
|
|
|
|
export function ComboMeter({ combo, isClimbing }: ComboMeterProps) {
|
|
const comboPercent = Math.min(100, combo.count);
|
|
const multiplierPercent = Math.min(100, ((combo.multiplier - 1) / 2) * 100); // Max 300% = 200% bonus
|
|
|
|
// Combo tier names
|
|
const getComboTier = (count: number): { name: string; color: string } => {
|
|
if (count >= 100) return { name: 'LEGENDARY', color: 'text-amber-400' };
|
|
if (count >= 75) return { name: 'Master', color: 'text-purple-400' };
|
|
if (count >= 50) return { name: 'Expert', color: 'text-blue-400' };
|
|
if (count >= 25) return { name: 'Adept', color: 'text-green-400' };
|
|
if (count >= 10) return { name: 'Novice', color: 'text-cyan-400' };
|
|
return { name: 'Building...', color: 'text-gray-400' };
|
|
};
|
|
|
|
const tier = getComboTier(combo.count);
|
|
const hasElementChain = combo.elementChain.length === 3 && new Set(combo.elementChain).size === 3;
|
|
|
|
if (!isClimbing && combo.count === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Card className="bg-gray-900/80 border-gray-700">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
|
<Zap className="w-4 h-4" />
|
|
Combo Meter
|
|
{combo.count >= 10 && (
|
|
<Badge className={`ml-auto ${tier.color} bg-gray-800`}>
|
|
{tier.name}
|
|
</Badge>
|
|
)}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{/* Combo Count */}
|
|
<div className="space-y-1">
|
|
<div className="flex justify-between items-center text-sm">
|
|
<span className="text-gray-400">Hits</span>
|
|
<span className={`font-bold ${tier.color}`}>
|
|
{combo.count}
|
|
{combo.maxCombo > combo.count && (
|
|
<span className="text-gray-500 text-xs ml-2">max: {combo.maxCombo}</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
<Progress
|
|
value={comboPercent}
|
|
className="h-2 bg-gray-800"
|
|
/>
|
|
</div>
|
|
|
|
{/* Multiplier */}
|
|
<div className="space-y-1">
|
|
<div className="flex justify-between items-center text-sm">
|
|
<span className="text-gray-400">Multiplier</span>
|
|
<span className="font-bold text-amber-400">
|
|
{combo.multiplier.toFixed(2)}x
|
|
</span>
|
|
</div>
|
|
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full rounded-full transition-all duration-300"
|
|
style={{
|
|
width: `${multiplierPercent}%`,
|
|
background: `linear-gradient(90deg, #F59E0B, #EF4444)`,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Element Chain */}
|
|
{combo.elementChain.length > 0 && (
|
|
<div className="space-y-1">
|
|
<div className="flex justify-between items-center text-sm">
|
|
<span className="text-gray-400">Element Chain</span>
|
|
{hasElementChain && (
|
|
<span className="text-green-400 text-xs">+25% bonus!</span>
|
|
)}
|
|
</div>
|
|
<div className="flex gap-1">
|
|
{combo.elementChain.map((elem, i) => {
|
|
const elemDef = ELEMENTS[elem];
|
|
return (
|
|
<div
|
|
key={i}
|
|
className="w-8 h-8 rounded border flex items-center justify-center text-xs"
|
|
style={{
|
|
borderColor: elemDef?.color || '#60A5FA',
|
|
backgroundColor: `${elemDef?.color}20`,
|
|
color: elemDef?.color || '#60A5FA',
|
|
}}
|
|
>
|
|
{elemDef?.sym || '?'}
|
|
</div>
|
|
);
|
|
})}
|
|
{/* Empty slots */}
|
|
{Array.from({ length: 3 - combo.elementChain.length }).map((_, i) => (
|
|
<div
|
|
key={`empty-${i}`}
|
|
className="w-8 h-8 rounded border border-gray-700 bg-gray-800/50 flex items-center justify-center text-gray-600"
|
|
>
|
|
?
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Decay Warning */}
|
|
{isClimbing && combo.count > 0 && combo.decayTimer <= 3 && (
|
|
<div className="text-xs text-red-400 flex items-center gap-1">
|
|
<Flame className="w-3 h-3" />
|
|
Combo decaying soon!
|
|
</div>
|
|
)}
|
|
|
|
{/* Not climbing warning */}
|
|
{!isClimbing && combo.count > 0 && (
|
|
<div className="text-xs text-amber-400 flex items-center gap-1">
|
|
<Sparkles className="w-3 h-3" />
|
|
Resume climbing to maintain combo
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|