diff --git a/src/client/src/modules/draft/DeckBuilderView.tsx b/src/client/src/modules/draft/DeckBuilderView.tsx index cdb6a8d..77c4ec2 100644 --- a/src/client/src/modules/draft/DeckBuilderView.tsx +++ b/src/client/src/modules/draft/DeckBuilderView.tsx @@ -24,17 +24,29 @@ const normalizeCard = (c: any): DraftCard => ({ image: c.image || c.image_uris?.normal || c.card_faces?.[0]?.image_uris?.normal }); -// Draggable Wrapper for Cards -const DraggableCardWrapper = ({ children, card, source, disabled }: any) => { +const LAND_URL_MAP: Record = { + Plains: "https://cards.scryfall.io/normal/front/d/1/d1ea1858-ad25-4d13-9860-25c898b02c42.jpg", + Island: "https://cards.scryfall.io/normal/front/2/f/2f3069b3-c15c-4399-ab99-c88c0379435b.jpg", + Swamp: "https://cards.scryfall.io/normal/front/1/7/17d0571f-df6c-4b53-912f-9cb4d5a9d224.jpg", + Mountain: "https://cards.scryfall.io/normal/front/f/5/f5383569-42b7-4c07-b67f-2736bc88bd37.jpg", + Forest: "https://cards.scryfall.io/normal/front/1/f/1fa688da-901d-4876-be11-884d6b677271.jpg" +}; + +// Universal Wrapper handling both Pool Cards (Move) and Land Sources (Copy/Ghost) +const UniversalCardWrapper = ({ children, card, source, disabled }: any) => { + const isLand = card.isLandSource; + const dndId = isLand ? `land-source-${card.name}` : card.id; + const dndData = isLand ? { card, type: 'land' } : { card, source }; + const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ - id: card.id, - data: { card, source }, + id: dndId, + data: dndData, disabled }); const style = transform ? { transform: CSS.Translate.toString(transform), - opacity: isDragging ? 0 : 1, + opacity: isDragging ? (isLand ? 0.5 : 0) : 1, zIndex: isDragging ? 999 : undefined } : undefined; @@ -45,28 +57,6 @@ const DraggableCardWrapper = ({ children, card, source, disabled }: any) => { ); }; -// Draggable Wrapper for Lands (Special case: ID is generic until dropped) -const DraggableLandWrapper = ({ children, land }: any) => { - const id = `land-source-${land.name}`; - const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ - id: id, - data: { card: land, type: 'land' } - }); - - // For lands, we want to copy, so don't hide original - const style = transform ? { - transform: CSS.Translate.toString(transform), - zIndex: isDragging ? 999 : undefined, - opacity: isDragging ? 0.5 : 1 // Show ghost - } : undefined; - - return ( -
- {children} -
- ); -}; - // Droppable Zone const DroppableZone = ({ id, children, className }: any) => { const { setNodeRef, isOver } = useDroppable({ id }); @@ -147,13 +137,20 @@ const CardsDisplay: React.FC<{ // Use CSS var for grid if (viewMode === 'list') { - const sorted = [...cards].sort((a, b) => (a.cmc || 0) - (b.cmc || 0)); + const sorted = [...cards].sort((a, b) => { + // Lands always first + if (a.isLandSource && !b.isLandSource) return -1; + if (!a.isLandSource && b.isLandSource) return 1; + // Then CMC + return (a.cmc || 0) - (b.cmc || 0); + }); + return (
{sorted.map(c => ( - + onCardClick(c)} onHover={onHover} /> - + ))}
); @@ -161,9 +158,7 @@ const CardsDisplay: React.FC<{ if (viewMode === 'stack') { return ( -
{/* Allow native scrolling from parent */} - {/* StackView doesn't support DnD yet, so we disable it or handle it differently. - For now, drag from StackView is not implemented, falling back to Click. */} +
( - + {children} - + )} />
@@ -202,7 +197,7 @@ const CardsDisplay: React.FC<{ const isFoil = card.finish === 'foil'; return ( - + - + ); })}
@@ -301,7 +296,7 @@ export const DeckBuilderView: React.FC = ({ initialPool, a const [pool, setPool] = useState(initialPool); const [deck, setDeck] = useState([]); - const [lands, setLands] = useState({ Plains: 0, Island: 0, Swamp: 0, Mountain: 0, Forest: 0 }); + // const [lands, setLands] = useState(...); // REMOVED: Managed directly in deck now const [hoveredCard, setHoveredCard] = useState(null); const [displayCard, setDisplayCard] = useState(null); @@ -362,26 +357,38 @@ export const DeckBuilderView: React.FC = ({ initialPool, a const applySuggestion = () => { if (!landSuggestion) return; - if (availableBasicLands && availableBasicLands.length > 0) { - const newLands: any[] = []; - Object.entries(landSuggestion).forEach(([type, count]) => { - if (count <= 0) return; - const landCard = availableBasicLands.find(l => l.name === type) || availableBasicLands.find(l => l.name.includes(type)); - if (landCard) { - for (let i = 0; i < count; i++) { - const newLand = { - ...landCard, - id: `land-${landCard.scryfallId}-${Date.now()}-${Math.random().toString(36).substr(2, 5)}-${i}`, - image_uris: landCard.image_uris || { normal: landCard.image } - }; - newLands.push(newLand); - } - } - }); - if (newLands.length > 0) setDeck(prev => [...prev, ...newLands]); - } else { - setLands(landSuggestion); - } + + const newLands: any[] = []; + Object.entries(landSuggestion).forEach(([type, count]) => { + if ((count as number) <= 0) return; + + // Find real land from cube or create generic + let landCard = availableBasicLands && availableBasicLands.length > 0 + ? (availableBasicLands.find(l => l.name === type) || availableBasicLands.find(l => l.name.includes(type))) + : null; + + if (!landCard) { + landCard = { + id: `basic-source-${type}`, + name: type, + image_uris: { normal: LAND_URL_MAP[type] }, + typeLine: "Basic Land", + scryfallId: `generic-${type}` + }; + } + + for (let i = 0; i < (count as number); i++) { + const newLand = { + ...landCard, + id: `land-${type}-${Date.now()}-${Math.random().toString(36).substr(2, 5)}-${i}`, + image_uris: landCard.image_uris || { normal: landCard.image || LAND_URL_MAP[type] }, + typeLine: landCard.typeLine || "Basic Land" + }; + newLands.push(newLand); + } + }); + + if (newLands.length > 0) setDeck(prev => [...prev, ...newLands]); }; // --- Actions --- @@ -408,35 +415,10 @@ export const DeckBuilderView: React.FC = ({ initialPool, a } }; - const handleLandChange = (type: string, delta: number) => { - setLands(prev => ({ ...prev, [type]: Math.max(0, prev[type as keyof typeof lands] + delta) })); - }; - const submitDeck = () => { - const genericLandCards = Object.entries(lands).flatMap(([type, count]) => { - const landUrlMap: any = { - Plains: "https://cards.scryfall.io/normal/front/d/1/d1ea1858-ad25-4d13-9860-25c898b02c42.jpg", - Island: "https://cards.scryfall.io/normal/front/2/f/2f3069b3-c15c-4399-ab99-c88c0379435b.jpg", - Swamp: "https://cards.scryfall.io/normal/front/1/7/17d0571f-df6c-4b53-912f-9cb4d5a9d224.jpg", - Mountain: "https://cards.scryfall.io/normal/front/f/5/f5383569-42b7-4c07-b67f-2736bc88bd37.jpg", - Forest: "https://cards.scryfall.io/normal/front/1/f/1fa688da-901d-4876-be11-884d6b677271.jpg" - }; - return Array(count).fill(null).map((_, i) => ({ - id: `basic-${type}-${i}`, - name: type, - image_uris: { normal: landUrlMap[type] }, - typeLine: "Basic Land" - })); - }); - - const fullDeck = [...deck, ...genericLandCards]; - socketService.socket.emit('player_ready', { deck: fullDeck }); + socketService.socket.emit('player_ready', { deck }); }; - const sortedLands = useMemo(() => { - return [...(availableBasicLands || [])].sort((a, b) => a.name.localeCompare(b.name)); - }, [availableBasicLands]); - // --- DnD Handlers --- const sensors = useSensors( useSensor(MouseSensor, { activationConstraint: { distance: 10 } }), @@ -540,66 +522,94 @@ export const DeckBuilderView: React.FC = ({ initialPool, a }, []); // --- Render Functions --- - const renderLandStation = () => ( -
- {/* Header & Advisor */} -
-

Land Station

- {landSuggestion ? ( -
- Advice: -
+ // --- Consolidated Pool Logic --- + const landSourceCards = useMemo(() => { + // If we have specific lands from cube, use them. + if (availableBasicLands && availableBasicLands.length > 0) { + return availableBasicLands.map(land => ({ + ...land, + id: `land-source-${land.name}`, // stable ID for list + isLandSource: true, + // Ensure image is set for display + image: land.image || land.image_uris?.normal + })); + } + + // Otherwise generate generic basics + const types = ['Plains', 'Island', 'Swamp', 'Mountain', 'Forest']; + return types.map(type => ({ + id: `basic-source-${type}`, + name: type, + isLandSource: true, + image: LAND_URL_MAP[type], + typeLine: `Basic Land — ${type}`, + rarity: 'common', + cmc: 0, + set: 'LEA', // Dummy set for visuals + colors: type === 'Plains' ? ['W'] : type === 'Island' ? ['U'] : type === 'Swamp' ? ['B'] : type === 'Mountain' ? ['R'] : ['G'] + })); + }, [availableBasicLands]); + + // Removed displayPool memo to keep them separate + + + + const LandAdvice = () => { + if (!landSuggestion) return null; + return ( +
+
+
+ +
+
+ Recommended Lands +
{Object.entries(landSuggestion).map(([type, count]) => { if ((count as number) <= 0) return null; - const color = type === 'Plains' ? 'text-amber-200' : type === 'Island' ? 'text-blue-200' : type === 'Swamp' ? 'text-purple-200' : type === 'Mountain' ? 'text-red-200' : 'text-emerald-200'; - return {type[0]}:{count as number} + const colorClass = type === 'Plains' ? 'text-yellow-200' : type === 'Island' ? 'text-blue-200' : type === 'Swamp' ? 'text-purple-200' : type === 'Mountain' ? 'text-red-200' : 'text-emerald-200'; + return {count as number} {type} })}
-
- ) : ( - Add spells for advice - )} +
+
+ ); + }; - {/* Land Scroll */} - {availableBasicLands && availableBasicLands.length > 0 ? ( -
- {sortedLands.map((land) => ( - -
addLandToDeck(land)} - onMouseEnter={() => setHoveredCard(land)} - onMouseLeave={() => setHoveredCard(null)} - > - {land.name} -
- + -
-
-
- ))} -
- ) : ( -
- {Object.keys(lands).map(type => ( -
-
{type[0]}
-
- - {lands[type as keyof typeof lands]} - -
+ const LandRow = () => ( +
+ +
+ {landSourceCards.map(land => ( +
addLandToDeck(land)} + onMouseEnter={() => setHoveredCard(land)} + onMouseLeave={() => setHoveredCard(null)} + className="relative group cursor-pointer hover:scale-105 transition-transform" + style={{ width: '85px' }} + > +
+ + {/* Click Only Indicator */} +
- ))} -
- )} +
+ + + ADD + +
+
+ ))} +
+
); @@ -853,7 +863,8 @@ export const DeckBuilderView: React.FC = ({ initialPool, a Card Pool ({pool.length})
- {renderLandStation()} + {/* Land Station Merged into Display */} +
@@ -880,7 +891,7 @@ export const DeckBuilderView: React.FC = ({ initialPool, a Card Pool ({pool.length})
- {renderLandStation()} +
diff --git a/src/client/src/modules/draft/DraftView.tsx b/src/client/src/modules/draft/DraftView.tsx index 027df1e..34eb0b1 100644 --- a/src/client/src/modules/draft/DraftView.tsx +++ b/src/client/src/modules/draft/DraftView.tsx @@ -103,7 +103,14 @@ export const DraftView: React.FC = ({ draftState, currentPlayerI } }, [cardScale]); - const [layout, setLayout] = useState<'vertical' | 'horizontal'>('vertical'); // Default to vertical for consistency + const [layout, setLayout] = useState<'vertical' | 'horizontal'>(() => { + const saved = localStorage.getItem('draft_layout'); + return (saved as 'vertical' | 'horizontal') || 'vertical'; + }); + + useEffect(() => { + localStorage.setItem('draft_layout', layout); + }, [layout]); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => { return localStorage.getItem('draft_sidebarCollapsed') === 'true'; });