Fix build errors and add Equipment tab
Some checks failed
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m7s
Some checks failed
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m7s
- Create missing StudyProgress component for SkillsTab - Create missing UpgradeDialog component for SkillsTab - Add EquipmentTab with gear selection interface - Shows all 8 equipment slots with equipped items - Displays inventory of unequipped items - Allows equipping/unequipping gear - Shows equipment stats summary and active effects
This commit is contained in:
@@ -12,7 +12,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
import { CraftingTab, SpireTab, SpellsTab, LabTab, SkillsTab, StatsTab } from '@/components/game/tabs';
|
||||
import { CraftingTab, SpireTab, SpellsTab, LabTab, SkillsTab, StatsTab, EquipmentTab } from '@/components/game/tabs';
|
||||
import { ComboMeter, ActionButtons, CalendarDisplay, ManaDisplay, TimeDisplay } from '@/components/game';
|
||||
import { LootInventoryDisplay } from '@/components/game/LootInventory';
|
||||
import { AchievementsDisplay } from '@/components/game/AchievementsDisplay';
|
||||
@@ -226,10 +226,11 @@ export default function ManaLoopGame() {
|
||||
{/* Right Panel - Tabs */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid grid-cols-7 w-full mb-4">
|
||||
<TabsList className="grid grid-cols-8 w-full mb-4">
|
||||
<TabsTrigger value="spire">⚔️ Spire</TabsTrigger>
|
||||
<TabsTrigger value="skills">📚 Skills</TabsTrigger>
|
||||
<TabsTrigger value="spells">✨ Spells</TabsTrigger>
|
||||
<TabsTrigger value="equipment">🛡️ Gear</TabsTrigger>
|
||||
<TabsTrigger value="crafting">🔧 Craft</TabsTrigger>
|
||||
<TabsTrigger value="lab">🔬 Lab</TabsTrigger>
|
||||
<TabsTrigger value="stats">📊 Stats</TabsTrigger>
|
||||
@@ -256,6 +257,10 @@ export default function ManaLoopGame() {
|
||||
<SpellsTab store={store} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="equipment">
|
||||
<EquipmentTab store={store} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="crafting">
|
||||
<CraftingTab store={store} />
|
||||
</TabsContent>
|
||||
|
||||
393
src/components/game/tabs/EquipmentTab.tsx
Normal file
393
src/components/game/tabs/EquipmentTab.tsx
Normal file
@@ -0,0 +1,393 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
EQUIPMENT_TYPES,
|
||||
EQUIPMENT_SLOTS,
|
||||
getEquipmentBySlot,
|
||||
type EquipmentSlot,
|
||||
type EquipmentType,
|
||||
} from '@/lib/game/data/equipment';
|
||||
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
|
||||
import { fmt } from '@/lib/game/store';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger
|
||||
} from '@/components/ui/tooltip';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import type { GameStore, EquipmentInstance } from '@/lib/game/types';
|
||||
|
||||
export interface EquipmentTabProps {
|
||||
store: GameStore;
|
||||
}
|
||||
|
||||
// Slot display names
|
||||
const SLOT_NAMES: Record<EquipmentSlot, string> = {
|
||||
mainHand: 'Main Hand',
|
||||
offHand: 'Off Hand',
|
||||
head: 'Head',
|
||||
body: 'Body',
|
||||
hands: 'Hands',
|
||||
feet: 'Feet',
|
||||
accessory1: 'Accessory 1',
|
||||
accessory2: 'Accessory 2',
|
||||
};
|
||||
|
||||
// Slot icons
|
||||
const SLOT_ICONS: Record<EquipmentSlot, string> = {
|
||||
mainHand: '⚔️',
|
||||
offHand: '🛡️',
|
||||
head: '🎩',
|
||||
body: '👕',
|
||||
hands: '🧤',
|
||||
feet: '👢',
|
||||
accessory1: '💍',
|
||||
accessory2: '📿',
|
||||
};
|
||||
|
||||
// Rarity colors
|
||||
const RARITY_COLORS: Record<string, string> = {
|
||||
common: 'border-gray-500 bg-gray-800/30',
|
||||
uncommon: 'border-green-500 bg-green-900/20',
|
||||
rare: 'border-blue-500 bg-blue-900/20',
|
||||
epic: 'border-purple-500 bg-purple-900/20',
|
||||
legendary: 'border-amber-500 bg-amber-900/20',
|
||||
mythic: 'border-red-500 bg-red-900/20',
|
||||
};
|
||||
|
||||
const RARITY_TEXT_COLORS: Record<string, string> = {
|
||||
common: 'text-gray-300',
|
||||
uncommon: 'text-green-400',
|
||||
rare: 'text-blue-400',
|
||||
epic: 'text-purple-400',
|
||||
legendary: 'text-amber-400',
|
||||
mythic: 'text-red-400',
|
||||
};
|
||||
|
||||
export function EquipmentTab({ store }: EquipmentTabProps) {
|
||||
const [selectedSlot, setSelectedSlot] = useState<EquipmentSlot | null>(null);
|
||||
|
||||
// Get unequipped items
|
||||
const equippedIds = new Set(Object.values(store.equippedInstances).filter(Boolean));
|
||||
const unequippedItems = Object.values(store.equipmentInstances).filter(
|
||||
(inst) => !equippedIds.has(inst.instanceId)
|
||||
);
|
||||
|
||||
// Equip an item to a slot
|
||||
const handleEquip = (instanceId: string, slot: EquipmentSlot) => {
|
||||
store.equipItem(instanceId, slot);
|
||||
setSelectedSlot(null);
|
||||
};
|
||||
|
||||
// Unequip from a slot
|
||||
const handleUnequip = (slot: EquipmentSlot) => {
|
||||
store.unequipItem(slot);
|
||||
};
|
||||
|
||||
// Get items that can be equipped in a slot
|
||||
const getEquippableItems = (slot: EquipmentSlot): EquipmentInstance[] => {
|
||||
const equipmentTypes = getEquipmentBySlot(slot);
|
||||
const typeIds = new Set(equipmentTypes.map((t) => t.id));
|
||||
|
||||
return unequippedItems.filter((inst) => typeIds.has(inst.typeId));
|
||||
};
|
||||
|
||||
// Get all items that can go in a slot (including accessories that can go in either accessory slot)
|
||||
const getItemsForSlot = (slot: EquipmentSlot): EquipmentInstance[] => {
|
||||
if (slot === 'accessory1' || slot === 'accessory2') {
|
||||
// Accessories can go in either slot
|
||||
const accessoryTypes = EQUIPMENT_TYPES;
|
||||
const accessoryTypeIds = Object.values(accessoryTypes)
|
||||
.filter((t) => t.category === 'accessory')
|
||||
.map((t) => t.id);
|
||||
|
||||
return unequippedItems.filter((inst) => accessoryTypeIds.includes(inst.typeId));
|
||||
}
|
||||
return getEquippableItems(slot);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Equipment Slots */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs">
|
||||
Equipped Gear
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{EQUIPMENT_SLOTS.map((slot) => {
|
||||
const instanceId = store.equippedInstances[slot];
|
||||
const instance = instanceId ? store.equipmentInstances[instanceId] : null;
|
||||
const equipmentType = instance ? EQUIPMENT_TYPES[instance.typeId] : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={slot}
|
||||
className={`p-3 rounded border ${
|
||||
instance
|
||||
? RARITY_COLORS[instance.rarity]
|
||||
: 'border-gray-700 bg-gray-800/30'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{SLOT_ICONS[slot]}</span>
|
||||
<span className="text-sm font-semibold text-gray-300">
|
||||
{SLOT_NAMES[slot]}
|
||||
</span>
|
||||
</div>
|
||||
{instance && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 text-xs text-red-400 hover:text-red-300 hover:bg-red-900/20"
|
||||
onClick={() => handleUnequip(slot)}
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{instance ? (
|
||||
<div className="space-y-1">
|
||||
<div className={`font-semibold text-sm ${RARITY_TEXT_COLORS[instance.rarity]}`}>
|
||||
{instance.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
Capacity: {instance.usedCapacity}/{instance.totalCapacity}
|
||||
</div>
|
||||
{instance.enchantments.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{instance.enchantments.map((ench, i) => {
|
||||
const effect = ENCHANTMENT_EFFECTS[ench.effectId];
|
||||
return (
|
||||
<TooltipProvider key={i}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs cursor-help"
|
||||
>
|
||||
{effect?.name || ench.effectId}
|
||||
{ench.stacks > 1 && ` x${ench.stacks}`}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{effect?.description || 'Unknown effect'}</p>
|
||||
<p className="text-gray-400 text-xs">
|
||||
Category: {effect?.category || 'unknown'}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-500 italic">
|
||||
Empty
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Inventory */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs">
|
||||
Equipment Inventory ({unequippedItems.length} items)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{unequippedItems.length === 0 ? (
|
||||
<div className="text-gray-500 text-sm text-center py-4">
|
||||
No unequipped items. Craft new gear in the Crafting tab.
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 max-h-96 overflow-y-auto">
|
||||
{unequippedItems.map((instance) => {
|
||||
const equipmentType = EQUIPMENT_TYPES[instance.typeId];
|
||||
const validSlots = equipmentType
|
||||
? (equipmentType.category === 'accessory'
|
||||
? ['accessory1', 'accessory2'] as EquipmentSlot[]
|
||||
: [equipmentType.slot])
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={instance.instanceId}
|
||||
className={`p-3 rounded border ${RARITY_COLORS[instance.rarity]}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<div className={`font-semibold text-sm ${RARITY_TEXT_COLORS[instance.rarity]}`}>
|
||||
{instance.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{equipmentType?.description}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{equipmentType?.category || 'unknown'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-400 space-y-1 mb-2">
|
||||
<div>
|
||||
Capacity: {instance.usedCapacity}/{instance.totalCapacity}
|
||||
{instance.quality < 100 && (
|
||||
<span className="text-yellow-500 ml-1">
|
||||
(Quality: {instance.quality}%)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{instance.enchantments.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{instance.enchantments.map((ench, i) => {
|
||||
const effect = ENCHANTMENT_EFFECTS[ench.effectId];
|
||||
return (
|
||||
<Badge
|
||||
key={i}
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
>
|
||||
{effect?.name || ench.effectId}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{validSlots.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
onValueChange={(value) =>
|
||||
handleEquip(instance.instanceId, value as EquipmentSlot)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="Equip to..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{validSlots.map((slot) => (
|
||||
<SelectItem
|
||||
key={slot}
|
||||
value={slot}
|
||||
className="text-xs"
|
||||
>
|
||||
{SLOT_ICONS[slot]} {SLOT_NAMES[slot]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 text-xs text-red-400 hover:text-red-300 hover:bg-red-900/20"
|
||||
onClick={() => store.deleteEquipmentInstance(instance.instanceId)}
|
||||
>
|
||||
🗑️
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Delete this item</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Equipment Stats Summary */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs">
|
||||
Equipment Stats Summary
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-2xl font-bold text-amber-400 game-mono">
|
||||
{Object.values(store.equipmentInstances).length}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">Total Items</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-2xl font-bold text-green-400 game-mono">
|
||||
{equippedIds.size}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">Equipped</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-2xl font-bold text-blue-400 game-mono">
|
||||
{unequippedItems.length}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">In Inventory</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-2xl font-bold text-purple-400 game-mono">
|
||||
{Object.values(store.equipmentInstances).reduce(
|
||||
(sum, inst) => sum + inst.enchantments.length,
|
||||
0
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">Total Enchantments</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Effects from Equipment */}
|
||||
<div className="mt-4">
|
||||
<div className="text-sm text-gray-400 mb-2">Active Effects from Equipment:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(() => {
|
||||
const effects = store.getEquipmentEffects();
|
||||
const effectEntries = Object.entries(effects).filter(([, v]) => v > 0);
|
||||
|
||||
if (effectEntries.length === 0) {
|
||||
return <span className="text-gray-500 text-sm">No active effects</span>;
|
||||
}
|
||||
|
||||
return effectEntries.map(([stat, value]) => (
|
||||
<Badge key={stat} variant="outline" className="text-xs">
|
||||
{stat}: +{fmt(value)}
|
||||
</Badge>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
src/components/game/tabs/StudyProgress.tsx
Normal file
73
src/components/game/tabs/StudyProgress.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||
import { formatStudyTime } from '@/lib/game/formatting';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import type { StudyTarget } from '@/lib/game/types';
|
||||
|
||||
export interface StudyProgressProps {
|
||||
currentStudyTarget: StudyTarget;
|
||||
skills: Record<string, number>;
|
||||
studySpeedMult: number;
|
||||
cancelStudy: () => void;
|
||||
}
|
||||
|
||||
export function StudyProgress({
|
||||
currentStudyTarget,
|
||||
skills,
|
||||
studySpeedMult,
|
||||
cancelStudy
|
||||
}: StudyProgressProps) {
|
||||
const { id, progress, required } = currentStudyTarget;
|
||||
|
||||
// Get skill name
|
||||
const baseId = id.includes('_t') ? id.split('_t')[0] : id;
|
||||
const skillDef = SKILLS_DEF[baseId];
|
||||
const skillName = skillDef?.name || id;
|
||||
|
||||
// Get current level
|
||||
const currentLevel = skills[id] || skills[baseId] || 0;
|
||||
|
||||
// Calculate progress percentage
|
||||
const progressPercent = Math.min((progress / required) * 100, 100);
|
||||
|
||||
// Estimated completion
|
||||
const remainingHours = required - progress;
|
||||
const effectiveSpeed = studySpeedMult;
|
||||
const realTimeRemaining = remainingHours / effectiveSpeed;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-purple-300 font-semibold">{skillName}</span>
|
||||
<span className="text-gray-400 ml-2">
|
||||
Level {currentLevel} → {currentLevel + 1}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={cancelStudy}
|
||||
className="text-xs"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs text-gray-400">
|
||||
<span>{formatStudyTime(progress)} / {formatStudyTime(required)}</span>
|
||||
<span>{progressPercent.toFixed(1)}%</span>
|
||||
</div>
|
||||
<Progress value={progressPercent} className="h-2" />
|
||||
{studySpeedMult > 1 && (
|
||||
<div className="text-xs text-green-400">
|
||||
Speed: {(studySpeedMult * 100).toFixed(0)}% • ETA: {formatStudyTime(realTimeRemaining)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
116
src/components/game/tabs/UpgradeDialog.tsx
Normal file
116
src/components/game/tabs/UpgradeDialog.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
|
||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||
import { getUpgradesForSkillAtMilestone, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||
|
||||
export interface UpgradeDialogProps {
|
||||
open: boolean;
|
||||
skillId: string | null;
|
||||
milestone: 5 | 10;
|
||||
pendingSelections: string[];
|
||||
available: SkillUpgradeChoice[];
|
||||
alreadySelected: string[];
|
||||
onToggle: (upgradeId: string) => void;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function UpgradeDialog({
|
||||
open,
|
||||
skillId,
|
||||
milestone,
|
||||
pendingSelections,
|
||||
available,
|
||||
alreadySelected,
|
||||
onToggle,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
onOpenChange,
|
||||
}: UpgradeDialogProps) {
|
||||
if (!skillId) return null;
|
||||
|
||||
// Get skill name
|
||||
const baseId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
|
||||
const skillDef = SKILLS_DEF[baseId];
|
||||
const skillName = skillDef?.name || skillId;
|
||||
|
||||
const currentSelections = pendingSelections.length > 0 ? pendingSelections : alreadySelected;
|
||||
const canConfirm = currentSelections.length === 2;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md bg-gray-900 border-purple-600/50">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-amber-400">
|
||||
Level {milestone} Milestone: {skillName}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Choose 2 upgrades for this skill. These choices are permanent.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-2 py-4">
|
||||
{available.map((upgrade) => {
|
||||
const isSelected = currentSelections.includes(upgrade.id);
|
||||
const canSelect = isSelected || currentSelections.length < 2;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={upgrade.id}
|
||||
onClick={() => canSelect && onToggle(upgrade.id)}
|
||||
className={`p-3 rounded border cursor-pointer transition-all ${
|
||||
isSelected
|
||||
? 'border-amber-500 bg-amber-900/30'
|
||||
: canSelect
|
||||
? 'border-gray-700 hover:border-gray-500 bg-gray-800/30'
|
||||
: 'border-gray-800 bg-gray-900/30 opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={`font-semibold text-sm ${isSelected ? 'text-amber-300' : 'text-gray-200'}`}>
|
||||
{upgrade.name}
|
||||
</span>
|
||||
{isSelected && (
|
||||
<Badge className="bg-amber-600/50 text-amber-200 text-xs">Selected</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-1">{upgrade.desc}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{available.length === 0 && (
|
||||
<div className="text-center text-gray-500 py-4">
|
||||
No upgrades available at this milestone.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex gap-2 sm:gap-0">
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-amber-600 hover:bg-amber-700"
|
||||
disabled={!canConfirm}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Confirm ({currentSelections.length}/2)
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -7,3 +7,4 @@ export { SpellsTab } from './SpellsTab';
|
||||
export { LabTab } from './LabTab';
|
||||
export { SkillsTab } from './SkillsTab';
|
||||
export { StatsTab } from './StatsTab';
|
||||
export { EquipmentTab } from './EquipmentTab';
|
||||
|
||||
Reference in New Issue
Block a user