522079e011
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m16s
- Fix enchantment capacity validation to respect equipment capacity limits - Remove combo system (no longer used) - Remove AUDIT_REPORT.md and GAME_SYSTEMS_ANALYSIS.md files - Add tests for capacity validation, attunement unlocking, and floor HP - Remove combo-related achievements - Fix AchievementsDisplay to not reference combo state - Add capacity display showing current/max in enchantment design UI - Prevent designs that exceed equipment capacity from being created
174 lines
7.9 KiB
TypeScript
Executable File
174 lines
7.9 KiB
TypeScript
Executable File
'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>
|
|
);
|
|
}
|