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
const normalizeCard = (c: any): DraftCard => ({
const normalizeCard = (c: any): DraftCard => {
const targetId = c.scryfallId || c.id;
const setCode = c.setCode || c.set;
const localImage = (targetId && setCode)
? `/cards/images/${setCode}/full/${targetId}.jpg`
: null;
return {
...c,
finish: c.finish || 'nonfoil',
typeLine: c.typeLine || c.type_line,
// Ensure image is top-level for components that expect it
image: c.image || c.image_uris?.normal || c.card_faces?.[0]?.image_uris?.normal
});
image: localImage || c.image || c.image_uris?.normal || c.card_faces?.[0]?.image_uris?.normal
};
};
const LAND_URL_MAP: Record<string, string> = {
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 = () => {
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 ---
@@ -526,13 +548,22 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
const landSourceCards = useMemo(() => {
// If we have specific lands from cube, use them.
if (availableBasicLands && availableBasicLands.length > 0) {
return availableBasicLands.map(land => ({
return availableBasicLands.map(land => {
const targetId = land.scryfallId || land.id;
const setCode = land.setCode || land.set;
const localImage = (targetId && setCode)
? `/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: land.image || land.image_uris?.normal
}));
image: localImage || land.image || land.image_uris?.normal
};
});
}
// Otherwise generate generic basics

View File

@@ -9,11 +9,21 @@ import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSenso
import { CSS } from '@dnd-kit/utilities';
// Helper to normalize card data for visuals
const normalizeCard = (c: any) => ({
// Helper to normalize card data for visuals
const normalizeCard = (c: any) => {
const targetId = c.scryfallId || c.id;
const setCode = c.setCode || c.set;
const localImage = (targetId && setCode)
? `/cards/images/${setCode}/full/${targetId}.jpg`
: null;
return {
...c,
finish: c.finish || 'nonfoil',
image: c.image || c.image_uris?.normal || c.card_faces?.[0]?.image_uris?.normal
});
image: localImage || c.image || c.image_uris?.normal || c.card_faces?.[0]?.image_uris?.normal
};
};
// Droppable Wrapper for Pool
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, () => {
if (window.matchMedia('(pointer: coarse)').matches) return;
}, card);
@@ -649,7 +660,7 @@ const PoolCardItem = ({ card, setHoveredCard, vertical = false }: any) => {
onClick={onClick}
>
<img
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal}
src={card.image}
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`}
draggable={false}

View File

@@ -29,9 +29,17 @@ export const CardComponent: React.FC<CardComponentProps> = ({ card, onDragStart,
return () => unregisterCard(card.instanceId);
}, [card.instanceId]);
// Robustly resolve Art Crop
// Robustly resolve Art Crop
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) {
imageSrc = card.definition.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 && (
<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}
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
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}
className="w-full h-full object-cover"
/>