feat: Prioritize local card image paths over external URLs for all card displays and interactions.

This commit is contained in:
2025-12-19 02:07:56 +01:00
parent 755ae73d9e
commit 312530d0f0
5 changed files with 85 additions and 25 deletions

View File

@@ -16,13 +16,21 @@ interface DeckBuilderViewProps {
} }
// Internal Helper to normalize card data for visuals // Internal Helper to normalize card data for visuals
const normalizeCard = (c: any): DraftCard => ({ const normalizeCard = (c: any): DraftCard => {
...c, const targetId = c.scryfallId || c.id;
finish: c.finish || 'nonfoil', const setCode = c.setCode || c.set;
typeLine: c.typeLine || c.type_line, const localImage = (targetId && setCode)
// Ensure image is top-level for components that expect it ? `/cards/images/${setCode}/full/${targetId}.jpg`
image: c.image || c.image_uris?.normal || c.card_faces?.[0]?.image_uris?.normal : null;
});
return {
...c,
finish: c.finish || 'nonfoil',
typeLine: c.typeLine || c.type_line,
// Ensure image is top-level for components that expect it
image: localImage || c.image || c.image_uris?.normal || c.card_faces?.[0]?.image_uris?.normal
};
};
const LAND_URL_MAP: Record<string, string> = { const LAND_URL_MAP: Record<string, string> = {
Plains: "https://cards.scryfall.io/normal/front/d/1/d1ea1858-ad25-4d13-9860-25c898b02c42.jpg", Plains: "https://cards.scryfall.io/normal/front/d/1/d1ea1858-ad25-4d13-9860-25c898b02c42.jpg",
@@ -416,7 +424,21 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
}; };
const submitDeck = () => { const submitDeck = () => {
socketService.socket.emit('player_ready', { deck }); // Normalize deck images to use local cache before submitting
const preparedDeck = deck.map(c => {
const targetId = c.scryfallId; // DraftCard uses scryfallId for the real ID
const setCode = c.setCode || c.set;
if (targetId && setCode) {
return {
...c,
image: `/cards/images/${setCode}/full/${targetId}.jpg`
};
}
return c;
});
socketService.socket.emit('player_ready', { deck: preparedDeck });
}; };
// --- DnD Handlers --- // --- DnD Handlers ---
@@ -526,13 +548,22 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
const landSourceCards = useMemo(() => { const landSourceCards = useMemo(() => {
// If we have specific lands from cube, use them. // If we have specific lands from cube, use them.
if (availableBasicLands && availableBasicLands.length > 0) { if (availableBasicLands && availableBasicLands.length > 0) {
return availableBasicLands.map(land => ({ return availableBasicLands.map(land => {
...land, const targetId = land.scryfallId || land.id;
id: `land-source-${land.name}`, // stable ID for list const setCode = land.setCode || land.set;
isLandSource: true,
// Ensure image is set for display const localImage = (targetId && setCode)
image: land.image || land.image_uris?.normal ? `/cards/images/${setCode}/full/${targetId}.jpg`
})); : null;
return {
...land,
id: `land-source-${land.name}`, // stable ID for list
isLandSource: true,
// Ensure image is set for display
image: localImage || land.image || land.image_uris?.normal
};
});
} }
// Otherwise generate generic basics // Otherwise generate generic basics

View File

@@ -9,11 +9,21 @@ import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSenso
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
// Helper to normalize card data for visuals // Helper to normalize card data for visuals
const normalizeCard = (c: any) => ({ // Helper to normalize card data for visuals
...c, const normalizeCard = (c: any) => {
finish: c.finish || 'nonfoil', const targetId = c.scryfallId || c.id;
image: c.image || c.image_uris?.normal || c.card_faces?.[0]?.image_uris?.normal const setCode = c.setCode || c.set;
});
const localImage = (targetId && setCode)
? `/cards/images/${setCode}/full/${targetId}.jpg`
: null;
return {
...c,
finish: c.finish || 'nonfoil',
image: localImage || c.image || c.image_uris?.normal || c.card_faces?.[0]?.image_uris?.normal
};
};
// Droppable Wrapper for Pool // Droppable Wrapper for Pool
const PoolDroppable = ({ children, className, style }: any) => { const PoolDroppable = ({ children, className, style }: any) => {
@@ -633,7 +643,8 @@ const DraftCardItem = ({ rawCard, handlePick, setHoveredCard }: any) => {
); );
}; };
const PoolCardItem = ({ card, setHoveredCard, vertical = false }: any) => { const PoolCardItem = ({ card: rawCard, setHoveredCard, vertical = false }: any) => {
const card = normalizeCard(rawCard);
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(setHoveredCard, () => { const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(setHoveredCard, () => {
if (window.matchMedia('(pointer: coarse)').matches) return; if (window.matchMedia('(pointer: coarse)').matches) return;
}, card); }, card);
@@ -649,7 +660,7 @@ const PoolCardItem = ({ card, setHoveredCard, vertical = false }: any) => {
onClick={onClick} onClick={onClick}
> >
<img <img
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal} src={card.image}
alt={card.name} alt={card.name}
className={`${vertical ? 'w-full h-full object-cover' : 'h-full w-auto object-contain'} rounded-lg shadow-lg border border-slate-700/50 group-hover:border-emerald-500/50 group-hover:shadow-emerald-500/20 transition-all`} className={`${vertical ? 'w-full h-full object-cover' : 'h-full w-auto object-contain'} rounded-lg shadow-lg border border-slate-700/50 group-hover:border-emerald-500/50 group-hover:shadow-emerald-500/20 transition-all`}
draggable={false} draggable={false}

View File

@@ -29,9 +29,17 @@ export const CardComponent: React.FC<CardComponentProps> = ({ card, onDragStart,
return () => unregisterCard(card.instanceId); return () => unregisterCard(card.instanceId);
}, [card.instanceId]); }, [card.instanceId]);
// Robustly resolve Art Crop
// Robustly resolve Art Crop // Robustly resolve Art Crop
let imageSrc = card.imageUrl; let imageSrc = card.imageUrl;
if (viewMode === 'cutout' && card.definition) {
if (card.definition && card.definition.set && card.definition.id) {
if (viewMode === 'cutout') {
imageSrc = `/cards/images/${card.definition.set}/crop/${card.definition.id}.jpg`;
} else {
imageSrc = `/cards/images/${card.definition.set}/full/${card.definition.id}.jpg`;
}
} else if (viewMode === 'cutout' && card.definition) {
if (card.definition.image_uris?.art_crop) { if (card.definition.image_uris?.art_crop) {
imageSrc = card.definition.image_uris.art_crop; imageSrc = card.definition.image_uris.art_crop;
} else if (card.definition.card_faces?.[0]?.image_uris?.art_crop) { } else if (card.definition.card_faces?.[0]?.image_uris?.art_crop) {

View File

@@ -501,7 +501,12 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
> >
{hoveredCard && ( {hoveredCard && (
<img <img
src={hoveredCard.imageUrl} src={(() => {
if (hoveredCard.definition?.set && hoveredCard.definition?.id) {
return `/cards/images/${hoveredCard.definition.set}/full/${hoveredCard.definition.id}.jpg`;
}
return hoveredCard.imageUrl;
})()}
alt={hoveredCard.name} alt={hoveredCard.name}
className="w-full h-full object-cover rounded-xl shadow-2xl shadow-black ring-1 ring-white/10" className="w-full h-full object-cover rounded-xl shadow-2xl shadow-black ring-1 ring-white/10"
/> />

View File

@@ -52,7 +52,12 @@ export const ZoneOverlay: React.FC<ZoneOverlayProps> = ({ zoneName, cards, onClo
}} }}
> >
<img <img
src={card.imageUrl || 'https://via.placeholder.com/250x350'} src={(() => {
if (card.definition?.set && card.definition?.id) {
return `/cards/images/${card.definition.set}/full/${card.definition.id}.jpg`;
}
return card.imageUrl || 'https://via.placeholder.com/250x350';
})()}
alt={card.name} alt={card.name}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />