Initial commit
This commit is contained in:
Executable
+173
@@ -0,0 +1,173 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Trophy, Lock, CheckCircle, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import type { AchievementState } from '@/lib/game/types';
|
||||
import { ACHIEVEMENTS, ACHIEVEMENT_CATEGORY_COLORS, getAchievementsByCategory, isAchievementRevealed } from '@/lib/game/data/achievements';
|
||||
import { GameState } from '@/lib/game/types';
|
||||
|
||||
interface AchievementsProps {
|
||||
achievements: AchievementState;
|
||||
gameState: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'totalSpellsCast' | 'totalDamageDealt' | 'totalCraftsCompleted'>;
|
||||
}
|
||||
|
||||
export function AchievementsDisplay({ achievements, gameState }: AchievementsProps) {
|
||||
const [expandedCategory, setExpandedCategory] = useState<string | null>('combat');
|
||||
|
||||
const categories = getAchievementsByCategory();
|
||||
const unlockedCount = achievements.unlocked.length;
|
||||
const totalCount = Object.keys(ACHIEVEMENTS).length;
|
||||
|
||||
// Calculate progress for each achievement
|
||||
const getProgress = (achievementId: string): number => {
|
||||
const achievement = ACHIEVEMENTS[achievementId];
|
||||
if (!achievement) return 0;
|
||||
if (achievements.unlocked.includes(achievementId)) return achievement.requirement.value;
|
||||
|
||||
const { type, subType } = achievement.requirement;
|
||||
|
||||
switch (type) {
|
||||
case 'floor':
|
||||
if (subType === 'noPacts') {
|
||||
return gameState.maxFloorReached >= achievement.requirement.value && gameState.signedPacts.length === 0
|
||||
? achievement.requirement.value
|
||||
: gameState.maxFloorReached;
|
||||
}
|
||||
return gameState.maxFloorReached;
|
||||
case 'spells':
|
||||
return gameState.totalSpellsCast || 0;
|
||||
case 'damage':
|
||||
return gameState.totalDamageDealt || 0;
|
||||
case 'mana':
|
||||
return gameState.totalManaGathered || 0;
|
||||
case 'pact':
|
||||
return gameState.signedPacts.length;
|
||||
case 'craft':
|
||||
return gameState.totalCraftsCompleted || 0;
|
||||
default:
|
||||
return achievements.progress[achievementId] || 0;
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
<Trophy className="w-4 h-4" />
|
||||
Achievements
|
||||
<Badge className="ml-auto bg-amber-900/50 text-amber-300">
|
||||
{unlockedCount} / {totalCount}
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-64">
|
||||
<div className="space-y-2">
|
||||
{Object.entries(categories).map(([category, categoryAchievements]) => (
|
||||
<div key={category} className="space-y-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-between text-xs"
|
||||
onClick={() => setExpandedCategory(expandedCategory === category ? null : category)}
|
||||
>
|
||||
<span style={{ color: ACHIEVEMENT_CATEGORY_COLORS[category] }}>
|
||||
{category.charAt(0).toUpperCase() + category.slice(1)}
|
||||
</span>
|
||||
<span className="text-gray-500">
|
||||
{categoryAchievements.filter(a => achievements.unlocked.includes(a.id)).length} / {categoryAchievements.length}
|
||||
</span>
|
||||
{expandedCategory === category ? (
|
||||
<ChevronUp className="w-4 h-4" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{expandedCategory === category && (
|
||||
<div className="pl-2 space-y-2">
|
||||
{categoryAchievements.map((achievement) => {
|
||||
const isUnlocked = achievements.unlocked.includes(achievement.id);
|
||||
const progress = getProgress(achievement.id);
|
||||
const isRevealed = isAchievementRevealed(achievement, progress);
|
||||
const progressPercent = Math.min(100, (progress / achievement.requirement.value) * 100);
|
||||
|
||||
if (!isRevealed && !isUnlocked) {
|
||||
return (
|
||||
<div key={achievement.id} className="p-2 rounded bg-gray-800/30 border border-gray-700">
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<Lock className="w-4 h-4" />
|
||||
<span className="text-sm">???</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={achievement.id}
|
||||
className={`p-2 rounded border ${
|
||||
isUnlocked
|
||||
? 'bg-amber-900/20 border-amber-600/50'
|
||||
: 'bg-gray-800/30 border-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{isUnlocked ? (
|
||||
<CheckCircle className="w-4 h-4 text-amber-400" />
|
||||
) : (
|
||||
<Trophy className="w-4 h-4 text-gray-500" />
|
||||
)}
|
||||
<span className={`text-sm font-semibold ${isUnlocked ? 'text-amber-300' : 'text-gray-300'}`}>
|
||||
{achievement.name}
|
||||
</span>
|
||||
</div>
|
||||
{achievement.reward.title && isUnlocked && (
|
||||
<Badge className="text-xs bg-purple-900/50 text-purple-300">
|
||||
Title
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-400 mb-2">
|
||||
{achievement.desc}
|
||||
</div>
|
||||
|
||||
{!isUnlocked && (
|
||||
<div className="space-y-1">
|
||||
<Progress value={progressPercent} className="h-1 bg-gray-700" />
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<span>{progress.toLocaleString()} / {achievement.requirement.value.toLocaleString()}</span>
|
||||
<span>{progressPercent.toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isUnlocked && achievement.reward && (
|
||||
<div className="text-xs text-amber-400/70">
|
||||
Reward:
|
||||
{achievement.reward.insight && ` +${achievement.reward.insight} Insight`}
|
||||
{achievement.reward.manaBonus && ` +${achievement.reward.manaBonus} Max Mana`}
|
||||
{achievement.reward.damageBonus && ` +${(achievement.reward.damageBonus * 100).toFixed(0)}% Damage`}
|
||||
{achievement.reward.title && ` "${achievement.reward.title}"`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user