feat: enhance card identification from image URLs and introduce cropped art support for cards.
All checks were successful
Build and Deploy / build (push) Successful in 2m7s

This commit is contained in:
2025-12-23 01:03:29 +01:00
parent 6edfb8b9e4
commit 9b25d3f0be
7 changed files with 78 additions and 20 deletions

View File

@@ -82,7 +82,7 @@ define(['./workbox-5a5d9309'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "index.html",
"revision": "0.sortnjvj4s8"
"revision": "0.jtdcrepbpeo"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View File

@@ -62,8 +62,26 @@ export const CardVisual: React.FC<CardVisualProps> = ({
let src = card.imageUrl || card.image;
// Use top-level properties if available (common in DraftCard / Game Card objects)
const setCode = card.setCode || card.set || card.definition?.set;
const cardId = card.scryfallId || card.definition?.id;
let setCode = card.setCode || card.set || card.definition?.set;
let cardId = card.scryfallId || card.definition?.id;
// Fallback: Attempt to extract from Image URL if IDs are missing (Fix for legacy/active games)
if ((!setCode || !cardId) && (card.imageUrl || card.image)) {
const url = card.imageUrl || card.image;
if (typeof url === 'string' && url.includes('/cards/images/')) {
const parts = url.split('/cards/images/')[1].split('/');
// Expected formats:
// 1. [set]/full/[id].jpg
// 2. [set]/crop/[id].jpg
if (parts.length >= 2) {
if (!setCode) setCode = parts[0];
if (!cardId) {
const filename = parts[parts.length - 1];
cardId = filename.replace(/\.(jpg|png)(\?.*)?$/, ''); // strip extension and query
}
}
}
}
if (viewMode === 'cutout') {
// Priority 1: Local Cache (standard naming convention) - PREFERRED BY USER

View File

@@ -482,7 +482,7 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, i
landCard = {
id: `basic-source-${type}`,
name: type,
image_uris: { normal: LAND_URL_MAP[type] },
image_uris: { normal: LAND_URL_MAP[type], art_crop: LAND_URL_MAP[type] },
typeLine: "Basic Land",
scryfallId: `generic-${type}`
};
@@ -721,6 +721,7 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, i
name: type,
isLandSource: true,
image: LAND_URL_MAP[type],
imageArtCrop: LAND_URL_MAP[type], // Explicitly add fallback crop
typeLine: `Basic Land — ${type}`,
rarity: 'common',
cmc: 0,

View File

@@ -56,6 +56,7 @@ export interface CardInstance {
png?: string;
border_crop?: string;
};
imageArtCrop?: string;
}
export interface PlayerState {

View File

@@ -65,6 +65,7 @@ export interface CardObject {
setCode?: string;
controlledSinceTurn: number; // For Summoning Sickness check
definition?: any;
imageArtCrop?: string;
}
export interface PlayerState {

View File

@@ -674,16 +674,38 @@ io.on('connection', (socket) => {
const game = gameManager.createGame(room.id, updatedRoom.players);
if (decks) {
Object.entries(decks).forEach(([pid, deck]: [string, any]) => {
// @ts-ignore
deck.forEach(card => {
// Robustly resolve setCode / scryfallId
let setCode = card.setCode || card.set || card.definition?.set;
let scryfallId = card.scryfallId || card.id || card.definition?.id;
// Fallback: Extract from Image URL if missing
if ((!setCode || !scryfallId) && card.imageUrl && card.imageUrl.includes('/cards/images/')) {
const parts = card.imageUrl.split('/cards/images/');
if (parts[1]) {
const pathParts = parts[1].split('/');
// Format: [setCode]/[full|crop]/[id].jpg OR [setCode]/[id].jpg
if (!setCode) setCode = pathParts[0];
if (!scryfallId) {
const filename = pathParts[pathParts.length - 1]; // uuid.jpg
scryfallId = filename.replace(/\.(jpg|png)$/, '');
}
}
}
gameManager.addCardToGame(room.id, {
ownerId: pid,
controllerId: pid,
oracleId: card.oracle_id || card.id || card.definition?.oracle_id,
scryfallId: card.scryfallId || card.id || card.definition?.id,
setCode: card.setCode || card.set || card.definition?.set,
scryfallId: scryfallId,
setCode: setCode,
name: card.name,
imageUrl: card.image_uris?.normal || card.image_uris?.large || card.imageUrl || "",
// IMPORTANT: If we have setCode+scryfallId, we clear imageUrl so client uses local cache logic
imageUrl: (setCode && scryfallId) ? "" : (card.image_uris?.normal || card.image_uris?.large || card.imageUrl || ""),
imageArtCrop: card.image_uris?.art_crop || card.image_uris?.crop || card.imageArtCrop || "",
zone: 'library',
typeLine: card.typeLine || card.type_line || '',
oracleText: card.oracleText || card.oracle_text || '',
@@ -800,14 +822,33 @@ io.on('connection', (socket) => {
[{ p: p1, d: deck1 }, { p: p2, d: deck2 }].forEach(({ p, d }) => {
if (d) {
d.forEach((card: any) => {
// Robustly resolve setCode / scryfallId
let setCode = card.setCode || card.set || card.definition?.set;
let scryfallId = card.scryfallId || card.id || card.definition?.id;
// Fallback: Extract from Image URL if missing
if ((!setCode || !scryfallId) && card.imageUrl && card.imageUrl.includes('/cards/images/')) {
const parts = card.imageUrl.split('/cards/images/');
if (parts[1]) {
const pathParts = parts[1].split('/');
if (!setCode) setCode = pathParts[0];
if (!scryfallId) {
const filename = pathParts[pathParts.length - 1]; // uuid.jpg
scryfallId = filename.replace(/\.(jpg|png)$/, '');
}
}
}
gameManager.addCardToGame(matchId, {
ownerId: p.id,
controllerId: p.id,
oracleId: card.oracle_id || card.id || card.definition?.oracle_id,
scryfallId: card.scryfallId || card.id || card.definition?.id,
setCode: card.setCode || card.set || card.definition?.set,
scryfallId: scryfallId,
setCode: setCode,
name: card.name,
imageUrl: "", // Optimisation: Client hydrates from cache
// IMPORTANT: If we have setCode+scryfallId, we clear imageUrl so client uses local cache logic
imageUrl: (setCode && scryfallId) ? "" : (card.image_uris?.normal || card.image_uris?.large || card.imageUrl || ""),
imageArtCrop: card.image_uris?.art_crop || card.image_uris?.crop || card.imageArtCrop || "",
zone: 'library',
typeLine: card.typeLine || card.type_line || '',
oracleText: card.oracleText || card.oracle_text || '',