diff --git a/.agent/rules/development-folders.md b/.agent/rules/development-folders.md index e5d4a8c..cb51b4b 100644 --- a/.agent/rules/development-folders.md +++ b/.agent/rules/development-folders.md @@ -12,14 +12,10 @@ The project follows a **Modular Monolith** pattern. All backend logic is structu ## 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**: -* **Controllers:** `./src/modules/[ModuleName]/controllers/` (Handle HTTP requests). -* **Routes:** `./src/modules/[ModuleName]/routes/` (Define express/fastify routes). -* **DTOs:** `./src/modules/[ModuleName]/dtos/` (Data Transfer Objects for validation). -* **Static Assets:** `./src/public/` (for module-specific assets if necessary). +## Cards Images folder +* **Cropped Art** `./src/server/public/cards/images/[set]/crop/ +* **Standard Art** `./src/server/public/cards/images/[set]/full/ -## Domain Layer -Shared business logic and database entities reside in shared directories or within the modules themselves, designed to be importable: - -* **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). +## Metadata folder +* **Card Metadata** `./src/server/public/cards/metadata/[set]/ +* **Set Metadata** `./src/server/public/cards/sets/ diff --git a/src/client/dev-dist/sw.js b/src/client/dev-dist/sw.js index f6cbf17..da5f88a 100644 --- a/src/client/dev-dist/sw.js +++ b/src/client/dev-dist/sw.js @@ -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"), { diff --git a/src/client/src/components/CardVisual.tsx b/src/client/src/components/CardVisual.tsx index 735f942..2c26a86 100644 --- a/src/client/src/components/CardVisual.tsx +++ b/src/client/src/components/CardVisual.tsx @@ -62,8 +62,26 @@ export const CardVisual: React.FC = ({ 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 diff --git a/src/client/src/modules/draft/DeckBuilderView.tsx b/src/client/src/modules/draft/DeckBuilderView.tsx index 29054ae..cc99e4a 100644 --- a/src/client/src/modules/draft/DeckBuilderView.tsx +++ b/src/client/src/modules/draft/DeckBuilderView.tsx @@ -482,7 +482,7 @@ export const DeckBuilderView: React.FC = ({ 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 = ({ 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, diff --git a/src/client/src/types/game.ts b/src/client/src/types/game.ts index 7e1f8f6..af0baf9 100644 --- a/src/client/src/types/game.ts +++ b/src/client/src/types/game.ts @@ -56,6 +56,7 @@ export interface CardInstance { png?: string; border_crop?: string; }; + imageArtCrop?: string; } export interface PlayerState { diff --git a/src/server/game/types.ts b/src/server/game/types.ts index 1e9e6b5..2dff4eb 100644 --- a/src/server/game/types.ts +++ b/src/server/game/types.ts @@ -65,6 +65,7 @@ export interface CardObject { setCode?: string; controlledSinceTurn: number; // For Summoning Sickness check definition?: any; + imageArtCrop?: string; } export interface PlayerState { diff --git a/src/server/index.ts b/src/server/index.ts index daa352a..93dcc0a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -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 || '',