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
All checks were successful
Build and Deploy / build (push) Successful in 2m7s
This commit is contained in:
@@ -12,14 +12,10 @@ The project follows a **Modular Monolith** pattern. All backend logic is structu
|
|||||||
## Backend and Frontend Integration (The Monolith)
|
## Backend and Frontend Integration (The Monolith)
|
||||||
The core server project (e.g., `./src/server` or `./src/app`) contains the entry point (`index.ts` or `main.ts`). Functionality is divided into **Modules**:
|
The core server project (e.g., `./src/server` or `./src/app`) contains the entry point (`index.ts` or `main.ts`). Functionality is divided into **Modules**:
|
||||||
|
|
||||||
* **Controllers:** `./src/modules/[ModuleName]/controllers/` (Handle HTTP requests).
|
## Cards Images folder
|
||||||
* **Routes:** `./src/modules/[ModuleName]/routes/` (Define express/fastify routes).
|
* **Cropped Art** `./src/server/public/cards/images/[set]/crop/
|
||||||
* **DTOs:** `./src/modules/[ModuleName]/dtos/` (Data Transfer Objects for validation).
|
* **Standard Art** `./src/server/public/cards/images/[set]/full/
|
||||||
* **Static Assets:** `./src/public/` (for module-specific assets if necessary).
|
|
||||||
|
|
||||||
## Domain Layer
|
## Metadata folder
|
||||||
Shared business logic and database entities reside in shared directories or within the modules themselves, designed to be importable:
|
* **Card Metadata** `./src/server/public/cards/metadata/[set]/
|
||||||
|
* **Set Metadata** `./src/server/public/cards/sets/
|
||||||
* **Entities:** `./src/modules/[ModuleName]/entities/` (ORM definitions, e.g., TypeORM/Prisma models).
|
|
||||||
* **Services:** `./src/modules/[ModuleName]/services/` (Business logic implementation).
|
|
||||||
* **Interfaces:** `./src/shared/interfaces/` or within the module (Type definitions).
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ define(['./workbox-5a5d9309'], (function (workbox) { 'use strict';
|
|||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.sortnjvj4s8"
|
"revision": "0.jtdcrepbpeo"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|||||||
@@ -62,8 +62,26 @@ export const CardVisual: React.FC<CardVisualProps> = ({
|
|||||||
let src = card.imageUrl || card.image;
|
let src = card.imageUrl || card.image;
|
||||||
|
|
||||||
// Use top-level properties if available (common in DraftCard / Game Card objects)
|
// Use top-level properties if available (common in DraftCard / Game Card objects)
|
||||||
const setCode = card.setCode || card.set || card.definition?.set;
|
let setCode = card.setCode || card.set || card.definition?.set;
|
||||||
const cardId = card.scryfallId || card.definition?.id;
|
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') {
|
if (viewMode === 'cutout') {
|
||||||
// Priority 1: Local Cache (standard naming convention) - PREFERRED BY USER
|
// Priority 1: Local Cache (standard naming convention) - PREFERRED BY USER
|
||||||
|
|||||||
@@ -482,7 +482,7 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, i
|
|||||||
landCard = {
|
landCard = {
|
||||||
id: `basic-source-${type}`,
|
id: `basic-source-${type}`,
|
||||||
name: 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",
|
typeLine: "Basic Land",
|
||||||
scryfallId: `generic-${type}`
|
scryfallId: `generic-${type}`
|
||||||
};
|
};
|
||||||
@@ -721,6 +721,7 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, i
|
|||||||
name: type,
|
name: type,
|
||||||
isLandSource: true,
|
isLandSource: true,
|
||||||
image: LAND_URL_MAP[type],
|
image: LAND_URL_MAP[type],
|
||||||
|
imageArtCrop: LAND_URL_MAP[type], // Explicitly add fallback crop
|
||||||
typeLine: `Basic Land — ${type}`,
|
typeLine: `Basic Land — ${type}`,
|
||||||
rarity: 'common',
|
rarity: 'common',
|
||||||
cmc: 0,
|
cmc: 0,
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export interface CardInstance {
|
|||||||
png?: string;
|
png?: string;
|
||||||
border_crop?: string;
|
border_crop?: string;
|
||||||
};
|
};
|
||||||
|
imageArtCrop?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlayerState {
|
export interface PlayerState {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export interface CardObject {
|
|||||||
setCode?: string;
|
setCode?: string;
|
||||||
controlledSinceTurn: number; // For Summoning Sickness check
|
controlledSinceTurn: number; // For Summoning Sickness check
|
||||||
definition?: any;
|
definition?: any;
|
||||||
|
imageArtCrop?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlayerState {
|
export interface PlayerState {
|
||||||
|
|||||||
@@ -674,16 +674,38 @@ io.on('connection', (socket) => {
|
|||||||
const game = gameManager.createGame(room.id, updatedRoom.players);
|
const game = gameManager.createGame(room.id, updatedRoom.players);
|
||||||
if (decks) {
|
if (decks) {
|
||||||
Object.entries(decks).forEach(([pid, deck]: [string, any]) => {
|
Object.entries(decks).forEach(([pid, deck]: [string, any]) => {
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
deck.forEach(card => {
|
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, {
|
gameManager.addCardToGame(room.id, {
|
||||||
ownerId: pid,
|
ownerId: pid,
|
||||||
controllerId: pid,
|
controllerId: pid,
|
||||||
oracleId: card.oracle_id || card.id || card.definition?.oracle_id,
|
oracleId: card.oracle_id || card.id || card.definition?.oracle_id,
|
||||||
scryfallId: card.scryfallId || card.id || card.definition?.id,
|
scryfallId: scryfallId,
|
||||||
setCode: card.setCode || card.set || card.definition?.set,
|
setCode: setCode,
|
||||||
name: card.name,
|
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',
|
zone: 'library',
|
||||||
typeLine: card.typeLine || card.type_line || '',
|
typeLine: card.typeLine || card.type_line || '',
|
||||||
oracleText: card.oracleText || card.oracle_text || '',
|
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 }) => {
|
[{ p: p1, d: deck1 }, { p: p2, d: deck2 }].forEach(({ p, d }) => {
|
||||||
if (d) {
|
if (d) {
|
||||||
d.forEach((card: any) => {
|
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, {
|
gameManager.addCardToGame(matchId, {
|
||||||
ownerId: p.id,
|
ownerId: p.id,
|
||||||
controllerId: p.id,
|
controllerId: p.id,
|
||||||
oracleId: card.oracle_id || card.id || card.definition?.oracle_id,
|
oracleId: card.oracle_id || card.id || card.definition?.oracle_id,
|
||||||
scryfallId: card.scryfallId || card.id || card.definition?.id,
|
scryfallId: scryfallId,
|
||||||
setCode: card.setCode || card.set || card.definition?.set,
|
setCode: setCode,
|
||||||
name: card.name,
|
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',
|
zone: 'library',
|
||||||
typeLine: card.typeLine || card.type_line || '',
|
typeLine: card.typeLine || card.type_line || '',
|
||||||
oracleText: card.oracleText || card.oracle_text || '',
|
oracleText: card.oracleText || card.oracle_text || '',
|
||||||
|
|||||||
Reference in New Issue
Block a user