305 lines
15 KiB
TypeScript
305 lines
15 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { ActionButton } from '@/components/ui/action-button';
|
|
import { GameCard } from '@/components/ui/game-card';
|
|
import { SectionHeader } from '@/components/ui/section-header';
|
|
import { StatRow } from '@/components/ui/stat-row';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
|
|
import { Trash2, CheckCircle, AlertTriangle } from 'lucide-react';
|
|
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
|
import type { EquipmentInstance, AppliedEnchantment, LootInventory, EquipmentCraftingProgress, EquipmentSlot } from '@/lib/game/types';
|
|
import { fmt } from '@/lib/game/stores';
|
|
import { useGameStore } from '@/lib/game/stores';
|
|
import { useGameToast } from '@/components/game/GameToast';
|
|
|
|
export interface EnchantmentPreparerProps {
|
|
selectedEquipmentInstance: string | null;
|
|
setSelectedEquipmentInstance: (id: string | null) => void;
|
|
}
|
|
|
|
export function EnchantmentPreparer({
|
|
selectedEquipmentInstance,
|
|
setSelectedEquipmentInstance,
|
|
}: EnchantmentPreparerProps) {
|
|
const showToast = useGameToast();
|
|
const equippedInstances = useGameStore((s) => s.equippedInstances);
|
|
const equipmentInstances = useGameStore((s) => s.equipmentInstances);
|
|
const preparationProgress = useGameStore((s) => s.preparationProgress);
|
|
const rawMana = useGameStore((s) => s.rawMana);
|
|
const skills = useGameStore((s) => s.skills);
|
|
const startPreparing = useGameStore((s) => s.startPreparing);
|
|
const cancelPreparation = useGameStore((s) => s.cancelPreparation);
|
|
|
|
// Get equipped items as array
|
|
const equippedItems = Object.entries(equippedInstances)
|
|
.filter(([, instanceId]) => instanceId && equipmentInstances[instanceId])
|
|
.map(([slot, instanceId]) => ({
|
|
slot: slot as EquipmentSlot,
|
|
instance: equipmentInstances[instanceId!],
|
|
}));
|
|
|
|
// Confirm dialog state
|
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
|
|
|
const handleStartPreparation = () => {
|
|
if (!selectedEquipmentInstance) return;
|
|
const instance = equipmentInstances[selectedEquipmentInstance];
|
|
if (!instance) return;
|
|
|
|
// If item has existing enchantments, show confirm dialog (bug #8)
|
|
if (instance.enchantments.length > 0) {
|
|
setShowConfirmDialog(true);
|
|
} else {
|
|
startPreparingWithToast(selectedEquipmentInstance);
|
|
}
|
|
};
|
|
|
|
const startPreparingWithToast = (instanceId: string) => {
|
|
const instance = equipmentInstances[instanceId];
|
|
startPreparing(instanceId);
|
|
if (instance) {
|
|
showToast('info', 'Preparation Started', `Preparing ${instance.name} for enchantment...`);
|
|
}
|
|
};
|
|
|
|
const confirmPreparation = () => {
|
|
if (selectedEquipmentInstance) {
|
|
startPreparingWithToast(selectedEquipmentInstance);
|
|
setShowConfirmDialog(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
{/* Equipment Selection */}
|
|
<GameCard variant="default">
|
|
<SectionHeader title="Select Equipment to Prepare" />
|
|
{preparationProgress ? (
|
|
<div className="space-y-3">
|
|
<div className="text-sm text-[var(--text-secondary)]">
|
|
Preparing: {equipmentInstances[preparationProgress.equipmentInstanceId]?.name}
|
|
</div>
|
|
<div className="h-3 bg-[var(--bg-sunken)] rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-[var(--color-warning)] transition-all duration-300"
|
|
style={{ width: `${(preparationProgress.progress / preparationProgress.required) * 100}%` }}
|
|
/>
|
|
</div>
|
|
<div className="flex justify-between text-xs text-[var(--text-muted)]">
|
|
<span>{preparationProgress.progress.toFixed(1)}h / {preparationProgress.required.toFixed(1)}h</span>
|
|
<span>Mana paid: {fmt(preparationProgress.manaCostPaid)}</span>
|
|
</div>
|
|
<ActionButton size="sm" variant="outline" onClick={() => {
|
|
cancelPreparation();
|
|
showToast('warning', 'Preparation Cancelled', 'Equipment preparation was cancelled.');
|
|
}}>Cancel</ActionButton>
|
|
</div>
|
|
) : (
|
|
<ScrollArea className="h-64">
|
|
<div className="space-y-2">
|
|
{equippedItems.map(({ slot, instance }) => {
|
|
const hasEnchantments = instance.enchantments.length > 0;
|
|
const isReady = instance.tags?.includes('Ready for Enchantment');
|
|
return (
|
|
<div
|
|
key={instance.instanceId}
|
|
className={`p-3 rounded border cursor-pointer transition-all
|
|
${selectedEquipmentInstance === instance.instanceId
|
|
? 'border-[var(--mana-light)] bg-[var(--mana-light)]/10'
|
|
: 'border-[var(--border-default)] bg-[var(--bg-sunken)]/50 hover:border-[var(--border-default)]'
|
|
}
|
|
${hasEnchantments ? 'border-l-4 border-l-[var(--color-danger)]' : ''}
|
|
${isReady ? 'border-l-4 border-l-[var(--color-success)]' : ''}
|
|
`}
|
|
onClick={() => setSelectedEquipmentInstance(instance.instanceId)}
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-label={`${instance.name}${hasEnchantments ? ' (has enchantments)' : ''}${isReady ? ' (ready for enchantment)' : ''}`}
|
|
>
|
|
<div className="flex justify-between">
|
|
<div>
|
|
<div className="font-semibold text-[var(--text-primary)]">{instance.name}</div>
|
|
<div className="text-xs text-[var(--text-muted)]">{slot}</div>
|
|
{hasEnchantments && (
|
|
<div className="text-xs text-[var(--color-danger)] mt-1">
|
|
<AlertTriangle size={12} className="inline mr-1" />
|
|
{instance.enchantments.length} enchantments - Preparation will remove them
|
|
</div>
|
|
)}
|
|
{isReady && (
|
|
<div className="text-xs text-[var(--color-success)] mt-1">
|
|
<CheckCircle size={12} className="inline mr-1" />
|
|
Ready for Enchantment
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="text-right text-sm">
|
|
<div className="text-[var(--color-success)]">{instance.usedCapacity}/{instance.totalCapacity} cap</div>
|
|
<div className="text-xs text-[var(--text-muted)]">{instance.enchantments.length} enchants</div>
|
|
{/* Requirement: Visual badge for 'Ready for Enchantment' */}
|
|
{isReady && (
|
|
<Badge className="mt-1 bg-[var(--color-success)]/20 text-[var(--color-success)] border-[var(--color-success)]/40">
|
|
<CheckCircle size={10} className="mr-1" />
|
|
Ready
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
{equippedItems.length === 0 && (
|
|
<div className="text-center text-[var(--text-muted)] py-4">No equipped items</div>
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
)}
|
|
</GameCard>
|
|
|
|
{/* Preparation Details */}
|
|
<GameCard variant="default">
|
|
<SectionHeader title="Preparation Details" />
|
|
{!selectedEquipmentInstance ? (
|
|
<div className="text-center text-[var(--text-muted)] py-8">
|
|
Select equipment to prepare
|
|
</div>
|
|
) : preparationProgress ? (
|
|
<div className="text-[var(--text-secondary)]">Preparation in progress...</div>
|
|
) : (
|
|
(() => {
|
|
const instance = equipmentInstances[selectedEquipmentInstance];
|
|
if (!instance) return null;
|
|
const hasEnchantments = instance.enchantments.length > 0;
|
|
const isReady = instance.tags?.includes('Ready for Enchantment');
|
|
const prepTime = 2 + Math.floor(instance.totalCapacity / 50);
|
|
const manaCost = instance.totalCapacity * 10;
|
|
|
|
// Calculate disenchant recovery
|
|
const recoveryRate = 0.1; // Base recovery rate
|
|
const totalRecoverable = instance.enchantments.reduce(
|
|
(sum, e) => sum + Math.floor(e.actualCost * recoveryRate),
|
|
0
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="text-lg font-semibold text-[var(--text-primary)]">{instance.name}</div>
|
|
<Separator className="bg-[var(--border-subtle)]" />
|
|
|
|
{/* Show warning if item has enchantments - Requirement: button reads "Prepare — removes existing enchantments" */}
|
|
{hasEnchantments && !isReady && (
|
|
<div className="p-3 rounded border border-[var(--color-danger)]/50 bg-[var(--color-danger)]/10">
|
|
<div className="text-sm font-semibold text-[var(--color-danger)]">
|
|
<AlertTriangle size={14} className="inline mr-1" />
|
|
Equipment has enchantments
|
|
</div>
|
|
<div className="text-xs text-[var(--text-muted)] mt-1">
|
|
Preparation will remove all existing enchantments and recover some mana.
|
|
</div>
|
|
<div className="flex justify-between text-sm mt-2">
|
|
<span className="text-[var(--text-muted)]">Recoverable Mana:</span>
|
|
<span className="text-[var(--color-success)]">{fmt(totalRecoverable)}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Show ready status */}
|
|
{isReady && (
|
|
<div className="p-3 rounded border border-[var(--color-success)]/50 bg-[var(--color-success)]/10">
|
|
<div className="text-sm font-semibold text-[var(--color-success)]">
|
|
<CheckCircle size={14} className="inline mr-1" />
|
|
Ready for Enchantment
|
|
</div>
|
|
<div className="text-xs text-[var(--text-muted)] mt-1">
|
|
This item has been prepared and is ready for enchantment application.
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-2 text-sm">
|
|
<StatRow
|
|
label="Capacity:"
|
|
value={`${instance.usedCapacity}/${instance.totalCapacity}`}
|
|
highlight="default"
|
|
/>
|
|
<StatRow
|
|
label="Prep Time:"
|
|
value={`${prepTime}h`}
|
|
highlight="default"
|
|
/>
|
|
<StatRow
|
|
label="Mana Cost:"
|
|
value={
|
|
<span className={rawMana < manaCost ? 'text-[var(--color-danger)]' : 'text-[var(--color-success)]'}>
|
|
{fmt(manaCost)}
|
|
</span>
|
|
}
|
|
highlight={rawMana < manaCost ? 'danger' : 'success'}
|
|
/>
|
|
</div>
|
|
|
|
{/* Requirement (bug #8): Confirm dialog before proceeding if item has enchantments */}
|
|
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
|
<AlertDialogTrigger asChild>
|
|
<ActionButton
|
|
className="w-full"
|
|
disabled={rawMana < manaCost || isReady}
|
|
onClick={handleStartPreparation}
|
|
>
|
|
{hasEnchantments ? (
|
|
<>
|
|
<Trash2 size={16} className="mr-2" />
|
|
Prepare — removes existing enchantments ({prepTime}h, {fmt(manaCost)} mana)
|
|
</>
|
|
) : (
|
|
<>Start Preparation ({prepTime}h, {fmt(manaCost)} mana)</>
|
|
)}
|
|
</ActionButton>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent className="bg-[var(--bg-elevated)] border-[var(--border-default)] text-[var(--text-primary)]">
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle className="text-[var(--color-danger)]">
|
|
<AlertTriangle className="inline mr-2" size={18} />
|
|
Confirm Preparation
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription className="text-[var(--text-secondary)]">
|
|
This equipment has {instance.enchantments.length} existing enchantment(s). Preparation will
|
|
<strong className="text-[var(--color-danger)]"> permanently remove</strong> all existing enchantments
|
|
and recover approximately <strong className="text-[var(--color-success)]">{fmt(totalRecoverable)} mana</strong>.
|
|
<div className="mt-2 p-2 bg-[var(--bg-sunken)]/50 rounded text-xs">
|
|
Equipment: {instance.name}<br />
|
|
Enchantments to remove: {instance.enchantments.length}
|
|
</div>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel
|
|
className="bg-[var(--bg-sunken)] border-[var(--border-default)] text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
|
|
onClick={() => setShowConfirmDialog(false)}
|
|
>
|
|
Cancel
|
|
</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
className="bg-[var(--color-danger)] hover:bg-[var(--interactive-danger-hover)] text-white"
|
|
onClick={confirmPreparation}
|
|
>
|
|
Yes, Remove Enchantments & Prepare
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
);
|
|
})()
|
|
)}
|
|
</GameCard>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
EnchantmentPreparer.displayName = 'EnchantmentPreparer';
|