feat: Implement game restart, battlefield styling with art crops and tapped stacks, and initial draw fixes.
Some checks failed
Build and Deploy / build (push) Failing after 1m10s
Some checks failed
Build and Deploy / build (push) Failing after 1m10s
This commit is contained in:
@@ -82,7 +82,7 @@ define(['./workbox-5a5d9309'], (function (workbox) { 'use strict';
|
||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.lcefu74575c"
|
||||
"revision": "0.56865l1cj5s"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
|
||||
@@ -443,20 +443,42 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
|
||||
|
||||
const handleReset = () => {
|
||||
if (window.confirm("Are you sure you want to clear this session? All parsed cards and generated packs will be lost.")) {
|
||||
setPacks([]);
|
||||
setInputText('');
|
||||
setRawScryfallData(null);
|
||||
setProcessedData(null);
|
||||
setAvailableLands([]);
|
||||
setSelectedSets([]);
|
||||
localStorage.removeItem('cube_inputText');
|
||||
localStorage.removeItem('cube_rawScryfallData');
|
||||
localStorage.removeItem('cube_selectedSets');
|
||||
localStorage.removeItem('cube_viewMode');
|
||||
localStorage.removeItem('cube_gameTypeFilter');
|
||||
setViewMode('list');
|
||||
setGameTypeFilter('all');
|
||||
// We keep filters and settings as they are user preferences
|
||||
try {
|
||||
console.log("Clearing session...");
|
||||
|
||||
// 1. Reset Parent State (App.tsx)
|
||||
setPacks([]);
|
||||
setAvailableLands([]);
|
||||
|
||||
// 2. Explicitly clear parent persistence keys to ensure they are gone immediately
|
||||
localStorage.removeItem('generatedPacks');
|
||||
localStorage.removeItem('availableLands');
|
||||
|
||||
// 3. Reset Local State
|
||||
setInputText('');
|
||||
setRawScryfallData(null);
|
||||
setProcessedData(null);
|
||||
setSelectedSets([]);
|
||||
|
||||
// 4. Clear Local Persistence
|
||||
localStorage.removeItem('cube_inputText');
|
||||
localStorage.removeItem('cube_rawScryfallData');
|
||||
localStorage.removeItem('cube_selectedSets');
|
||||
localStorage.removeItem('cube_viewMode');
|
||||
localStorage.removeItem('cube_gameTypeFilter');
|
||||
// We can optionally clear source mode, or leave it. Let's leave it for UX continuity or clear it?
|
||||
// Let's clear it to full reset.
|
||||
// localStorage.removeItem('cube_sourceMode');
|
||||
|
||||
// 5. Reset UI Filters/Views to defaults
|
||||
setViewMode('list');
|
||||
setGameTypeFilter('all');
|
||||
|
||||
showToast("Session cleared successfully.", "success");
|
||||
} catch (error) {
|
||||
console.error("Error clearing session:", error);
|
||||
showToast("Failed to clear session fully.", "error");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ interface CardComponentProps {
|
||||
onDragEnd?: (e: React.DragEvent) => void;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
viewMode?: 'normal' | 'cutout';
|
||||
}
|
||||
|
||||
export const CardComponent: React.FC<CardComponentProps> = ({ card, onDragStart, onClick, onContextMenu, onMouseEnter, onMouseLeave, onDrop, onDrag, onDragEnd, style, className }) => {
|
||||
export const CardComponent: React.FC<CardComponentProps> = ({ card, onDragStart, onClick, onContextMenu, onMouseEnter, onMouseLeave, onDrop, onDrag, onDragEnd, style, className, viewMode = 'normal' }) => {
|
||||
const { registerCard, unregisterCard } = useGesture();
|
||||
const cardRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -28,6 +29,16 @@ export const CardComponent: React.FC<CardComponentProps> = ({ card, onDragStart,
|
||||
return () => unregisterCard(card.instanceId);
|
||||
}, [card.instanceId]);
|
||||
|
||||
// Robustly resolve Art Crop
|
||||
let imageSrc = card.imageUrl;
|
||||
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) {
|
||||
imageSrc = card.definition.card_faces[0].image_uris.art_crop;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
@@ -55,7 +66,7 @@ export const CardComponent: React.FC<CardComponentProps> = ({ card, onDragStart,
|
||||
onMouseLeave={onMouseLeave}
|
||||
className={`
|
||||
relative rounded-lg shadow-md cursor-pointer transition-transform hover:scale-105 select-none
|
||||
${card.tapped ? 'rotate-90' : ''}
|
||||
${card.tapped ? 'rotate-45' : ''}
|
||||
${card.zone === 'hand' ? 'w-32 h-44 -ml-12 first:ml-0 hover:z-10 hover:-translate-y-4' : 'w-24 h-32'}
|
||||
${className || ''}
|
||||
`}
|
||||
@@ -64,7 +75,7 @@ export const CardComponent: React.FC<CardComponentProps> = ({ card, onDragStart,
|
||||
<div className="w-full h-full relative overflow-hidden rounded-lg bg-slate-800 border-2 border-slate-700">
|
||||
{!card.faceDown ? (
|
||||
<img
|
||||
src={card.imageUrl}
|
||||
src={imageSrc}
|
||||
alt={card.name}
|
||||
className="w-full h-full object-cover"
|
||||
draggable={false}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
|
||||
import React, { createContext, useContext, useRef, useState, useEffect } from 'react';
|
||||
import { socketService } from '../../services/SocketService';
|
||||
import React, { createContext, useContext, useRef, useState } from 'react';
|
||||
|
||||
interface GestureContextType {
|
||||
registerCard: (id: string, element: HTMLElement) => void;
|
||||
|
||||
@@ -26,10 +26,16 @@ export const SmartButton: React.FC<SmartButtonProps> = ({ gameState, playerId, o
|
||||
actionType = 'CANCEL_YIELD';
|
||||
} else if (isMyPriority) {
|
||||
if (gameState.step === 'declare_attackers') {
|
||||
const count = contextData?.attackers?.length || 0;
|
||||
label = count > 0 ? `Attack with ${count}` : "Skip Combat";
|
||||
colorClass = "bg-red-600 hover:bg-red-500 text-white shadow-[0_0_15px_rgba(239,68,68,0.5)] animate-pulse";
|
||||
actionType = 'DECLARE_ATTACKERS';
|
||||
if (gameState.attackersDeclared) {
|
||||
label = "Pass (to Blockers)";
|
||||
colorClass = "bg-emerald-600 hover:bg-emerald-500 text-white shadow-[0_0_15px_rgba(16,185,129,0.5)] animate-pulse";
|
||||
actionType = 'PASS_PRIORITY';
|
||||
} else {
|
||||
const count = contextData?.attackers?.length || 0;
|
||||
label = count > 0 ? `Attack with ${count}` : "Skip Combat";
|
||||
colorClass = "bg-red-600 hover:bg-red-500 text-white shadow-[0_0_15px_rgba(239,68,68,0.5)] animate-pulse";
|
||||
actionType = 'DECLARE_ATTACKERS';
|
||||
}
|
||||
} else if (gameState.step === 'declare_blockers') {
|
||||
// Todo: blockers context
|
||||
label = "Declare Blockers";
|
||||
|
||||
@@ -215,7 +215,7 @@ export const LobbyManager: React.FC<LobbyManagerProps> = ({ generatedPacks, avai
|
||||
}
|
||||
}, [activeRoom]);
|
||||
|
||||
// Reconnection logic
|
||||
// Reconnection logic (Initial Mount)
|
||||
React.useEffect(() => {
|
||||
const savedRoomId = localStorage.getItem('active_room_id');
|
||||
if (savedRoomId && !activeRoom && playerId) {
|
||||
@@ -246,6 +246,29 @@ export const LobbyManager: React.FC<LobbyManagerProps> = ({ generatedPacks, avai
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Auto-Rejoin on Socket Reconnect (e.g. Server Restart)
|
||||
React.useEffect(() => {
|
||||
const socket = socketService.socket;
|
||||
|
||||
const onConnect = () => {
|
||||
if (activeRoom && playerId) {
|
||||
console.log("Socket reconnected. Attempting to restore session for room:", activeRoom.id);
|
||||
socketService.emitPromise('rejoin_room', { roomId: activeRoom.id, playerId })
|
||||
.then((response: any) => {
|
||||
if (response.success) {
|
||||
console.log("Session restored successfully.");
|
||||
} else {
|
||||
console.warn("Failed to restore session:", response.message);
|
||||
}
|
||||
})
|
||||
.catch(err => console.error("Session restore error:", err));
|
||||
}
|
||||
};
|
||||
|
||||
socket.on('connect', onConnect);
|
||||
return () => { socket.off('connect', onConnect); };
|
||||
}, [activeRoom, playerId]);
|
||||
|
||||
// Listener for room updates to switch view
|
||||
React.useEffect(() => {
|
||||
const socket = socketService.socket;
|
||||
|
||||
@@ -107,9 +107,11 @@ export class PackGeneratorService {
|
||||
layout: layout,
|
||||
colors: cardData.colors || [],
|
||||
image: useLocalImages
|
||||
? `${window.location.origin}/cards/images/${cardData.set}/${cardData.id}.jpg`
|
||||
? `${window.location.origin}/cards/images/${cardData.set}/art_full/${cardData.id}.jpg`
|
||||
: (cardData.image_uris?.normal || cardData.card_faces?.[0]?.image_uris?.normal || ''),
|
||||
imageArtCrop: cardData.image_uris?.art_crop || cardData.card_faces?.[0]?.image_uris?.art_crop || '',
|
||||
imageArtCrop: useLocalImages
|
||||
? `${window.location.origin}/cards/images/${cardData.set}/art_crop/${cardData.id}.jpg`
|
||||
: (cardData.image_uris?.art_crop || cardData.card_faces?.[0]?.image_uris?.art_crop || ''),
|
||||
set: cardData.set_name,
|
||||
setCode: cardData.set,
|
||||
setType: setType,
|
||||
|
||||
@@ -39,8 +39,12 @@ export interface CardInstance {
|
||||
baseToughness?: number; // Base Toughness
|
||||
position: { x: number; y: number; z: number }; // For freeform placement
|
||||
typeLine?: string;
|
||||
types?: string[];
|
||||
supertypes?: string[];
|
||||
subtypes?: string[];
|
||||
oracleText?: string;
|
||||
manaCost?: string;
|
||||
definition?: any;
|
||||
}
|
||||
|
||||
export interface PlayerState {
|
||||
@@ -68,4 +72,6 @@ export interface GameState {
|
||||
stack?: StackObject[];
|
||||
activePlayerId?: string; // Explicitly tracked in strict
|
||||
priorityPlayerId?: string;
|
||||
attackersDeclared?: boolean;
|
||||
blockersDeclared?: boolean;
|
||||
}
|
||||
|
||||
@@ -60,6 +60,14 @@ export class RulesEngine {
|
||||
return true;
|
||||
}
|
||||
|
||||
public startGame() {
|
||||
console.log("RulesEngine: Starting Game...");
|
||||
// Ensure specific setup if needed (life total, etc is done elsewhere)
|
||||
|
||||
// Trigger Initial Draw
|
||||
this.performTurnBasedActions();
|
||||
}
|
||||
|
||||
public castSpell(playerId: string, cardId: string, targets: string[] = [], position?: { x: number, y: number }) {
|
||||
if (this.state.priorityPlayerId !== playerId) throw new Error("Not your priority.");
|
||||
|
||||
@@ -136,6 +144,7 @@ export class RulesEngine {
|
||||
});
|
||||
|
||||
console.log(`Player ${playerId} declared ${attackers.length} attackers.`);
|
||||
this.state.attackersDeclared = true; // Flag for UI/Engine state
|
||||
|
||||
// 508.2. Active Player gets priority
|
||||
// But usually passing happens immediately after declaration in digital?
|
||||
@@ -361,6 +370,21 @@ export class RulesEngine {
|
||||
nextStep = structure[nextPhase][0];
|
||||
}
|
||||
|
||||
// SKIP Logic for Combat
|
||||
// 508.8. If no creatures are declared as attackers... skip declare blockers/combat damage steps.
|
||||
if (this.state.phase === 'combat') {
|
||||
const attackers = Object.values(this.state.cards).filter(c => !!c.attacking);
|
||||
|
||||
// If we are about to enter declare_blockers or combat_damage and NO attackers exist
|
||||
// Note: We check 'attacking' status. If we just finished declare_attackers, we might have reset it?
|
||||
// No, 'attacking' property persists until end of combat.
|
||||
|
||||
if (nextStep === 'declare_blockers' && attackers.length === 0) {
|
||||
console.log("No attackers. Skipping directly to End of Combat.");
|
||||
nextStep = 'end_combat';
|
||||
}
|
||||
}
|
||||
|
||||
// Rule 500.4: Mana empties at end of each step and phase
|
||||
this.emptyManaPools();
|
||||
|
||||
@@ -523,7 +547,7 @@ export class RulesEngine {
|
||||
});
|
||||
}
|
||||
|
||||
private drawCard(playerId: string) {
|
||||
public drawCard(playerId: string) {
|
||||
const library = Object.values(this.state.cards).filter(c => c.ownerId === playerId && c.zone === 'library');
|
||||
if (library.length > 0) {
|
||||
// Draw top card (random for now if not ordered?)
|
||||
@@ -546,6 +570,9 @@ export class RulesEngine {
|
||||
c.modifiers = c.modifiers.filter(m => !m.untilEndOfTurn);
|
||||
}
|
||||
});
|
||||
|
||||
this.state.attackersDeclared = false;
|
||||
this.state.blockersDeclared = false;
|
||||
}
|
||||
|
||||
// --- State Based Actions ---
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface CardObject {
|
||||
|
||||
// Metadata
|
||||
controlledSinceTurn: number; // For Summoning Sickness check
|
||||
definition?: any;
|
||||
}
|
||||
|
||||
export interface PlayerState {
|
||||
@@ -108,6 +109,8 @@ export interface StrictGameState {
|
||||
// Rules State
|
||||
passedPriorityCount: number; // 0..N. If N, advance.
|
||||
landsPlayedThisTurn: number;
|
||||
attackersDeclared?: boolean;
|
||||
blockersDeclared?: boolean;
|
||||
|
||||
maxZ: number; // Visual depth (legacy support)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ScryfallService } from './services/ScryfallService';
|
||||
import { PackGeneratorService } from './services/PackGeneratorService';
|
||||
import { CardParserService } from './services/CardParserService';
|
||||
import { PersistenceManager } from './managers/PersistenceManager';
|
||||
import { RulesEngine } from './game/RulesEngine';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -255,6 +256,11 @@ const draftInterval = setInterval(() => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize Game State (Draw Hands)
|
||||
const engine = new RulesEngine(game);
|
||||
engine.startGame();
|
||||
|
||||
io.to(roomId).emit('game_update', game);
|
||||
}
|
||||
}
|
||||
@@ -471,12 +477,19 @@ io.on('connection', (socket) => {
|
||||
oracleText: card.oracleText || card.oracle_text || '',
|
||||
manaCost: card.manaCost || card.mana_cost || '',
|
||||
keywords: card.keywords || [],
|
||||
power: card.power, // Add Power
|
||||
toughness: card.toughness, // Add Toughness
|
||||
damageMarked: 0,
|
||||
controlledSinceTurn: 0
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize Game State (Draw Hands)
|
||||
const engine = new RulesEngine(game);
|
||||
engine.startGame();
|
||||
|
||||
io.to(room.id).emit('game_update', game);
|
||||
}
|
||||
}
|
||||
@@ -501,11 +514,18 @@ io.on('connection', (socket) => {
|
||||
oracleText: card.oracleText || card.oracle_text || '',
|
||||
manaCost: card.manaCost || card.mana_cost || '',
|
||||
keywords: card.keywords || [],
|
||||
power: card.power,
|
||||
toughness: card.toughness,
|
||||
damageMarked: 0,
|
||||
controlledSinceTurn: 0
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize Game State (Draw Hands)
|
||||
const engine = new RulesEngine(game);
|
||||
engine.startGame();
|
||||
|
||||
callback({ success: true, room, game });
|
||||
io.to(room.id).emit('room_update', room);
|
||||
io.to(room.id).emit('game_update', game);
|
||||
@@ -535,12 +555,19 @@ io.on('connection', (socket) => {
|
||||
oracleText: card.oracleText || card.oracle_text || '',
|
||||
manaCost: card.manaCost || card.mana_cost || '',
|
||||
keywords: card.keywords || [],
|
||||
power: card.power,
|
||||
toughness: card.toughness,
|
||||
damageMarked: 0,
|
||||
controlledSinceTurn: 0
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize Game State (Draw Hands)
|
||||
const engine = new RulesEngine(game);
|
||||
engine.startGame();
|
||||
|
||||
io.to(room.id).emit('game_update', game);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -96,7 +96,7 @@ export class GameManager {
|
||||
return null;
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(`Rule Violation [${action.type}]: ${e.message}`);
|
||||
console.error(`Rule Violation [${action?.type || 'UNKNOWN'}]: ${e.message}`);
|
||||
// TODO: Return error to user?
|
||||
// For now, just logging and not updating state (transactional-ish)
|
||||
return null;
|
||||
@@ -111,16 +111,32 @@ export class GameManager {
|
||||
if (!game) return null;
|
||||
|
||||
// Basic Validation: Ensure actor exists in game (or is host/admin?)
|
||||
if (!game.players[actorId]) return null;
|
||||
if (!game.players[actorId]) {
|
||||
console.warn(`handleAction: Player ${actorId} not found in room ${roomId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`[GameManager] Handling Action: ${action.type} for ${roomId} by ${actorId}`);
|
||||
|
||||
switch (action.type) {
|
||||
case 'UPDATE_LIFE':
|
||||
if (game.players[actorId]) {
|
||||
game.players[actorId].life += (action.amount || 0);
|
||||
}
|
||||
break;
|
||||
case 'MOVE_CARD':
|
||||
this.moveCard(game, action, actorId);
|
||||
break;
|
||||
case 'TAP_CARD':
|
||||
this.tapCard(game, action, actorId);
|
||||
break;
|
||||
// ... (Other cases can be ported if needed)
|
||||
case 'DRAW_CARD':
|
||||
const engine = new RulesEngine(game);
|
||||
engine.drawCard(actorId);
|
||||
break;
|
||||
case 'RESTART_GAME':
|
||||
this.restartGame(roomId);
|
||||
break;
|
||||
}
|
||||
|
||||
return game;
|
||||
@@ -188,8 +204,100 @@ export class GameManager {
|
||||
name: '',
|
||||
...cardData,
|
||||
damageMarked: 0,
|
||||
controlledSinceTurn: 0 // Will be updated on draw/play
|
||||
controlledSinceTurn: 0, // Will be updated on draw/play
|
||||
definition: cardData.definition // Ensure definition is passed
|
||||
};
|
||||
|
||||
// Auto-Parse Types if missing
|
||||
if (card.types.length === 0 && card.typeLine) {
|
||||
const [typePart, subtypePart] = card.typeLine.split('—').map(s => s.trim());
|
||||
const typeWords = typePart.split(' ');
|
||||
|
||||
const supertypeList = ['Legendary', 'Basic', 'Snow', 'World'];
|
||||
const typeList = ['Land', 'Creature', 'Artifact', 'Enchantment', 'Planeswalker', 'Instant', 'Sorcery', 'Tribal', 'Battle', 'Kindred']; // Kindred = Tribal
|
||||
|
||||
card.supertypes = typeWords.filter(w => supertypeList.includes(w));
|
||||
card.types = typeWords.filter(w => typeList.includes(w));
|
||||
|
||||
if (subtypePart) {
|
||||
card.subtypes = subtypePart.split(' ');
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-Parse P/T from cardData if provided specifically as strings or numbers, ensuring numbers
|
||||
if (cardData.power !== undefined) card.basePower = Number(cardData.power);
|
||||
if (cardData.toughness !== undefined) card.baseToughness = Number(cardData.toughness);
|
||||
|
||||
// Set current values to base
|
||||
card.power = card.basePower;
|
||||
card.toughness = card.baseToughness;
|
||||
|
||||
game.cards[card.instanceId] = card;
|
||||
}
|
||||
|
||||
private restartGame(roomId: string) {
|
||||
const game = this.games.get(roomId);
|
||||
if (!game) return;
|
||||
|
||||
// 1. Reset Game Global State
|
||||
game.turnCount = 1;
|
||||
game.phase = 'setup';
|
||||
game.step = 'mulligan';
|
||||
game.stack = [];
|
||||
game.activePlayerId = game.turnOrder[0];
|
||||
game.priorityPlayerId = game.activePlayerId;
|
||||
game.passedPriorityCount = 0;
|
||||
game.landsPlayedThisTurn = 0;
|
||||
game.attackersDeclared = false;
|
||||
game.blockersDeclared = false;
|
||||
game.maxZ = 100;
|
||||
|
||||
// 2. Reset Players
|
||||
Object.keys(game.players).forEach(pid => {
|
||||
const p = game.players[pid];
|
||||
p.life = 20;
|
||||
p.poison = 0;
|
||||
p.energy = 0;
|
||||
p.isActive = (pid === game.activePlayerId);
|
||||
p.hasPassed = false;
|
||||
p.manaPool = { W: 0, U: 0, B: 0, R: 0, G: 0, C: 0 };
|
||||
p.handKept = false;
|
||||
p.mulliganCount = 0;
|
||||
});
|
||||
|
||||
// 3. Reset Cards
|
||||
const tokensToRemove: string[] = [];
|
||||
Object.values(game.cards).forEach(c => {
|
||||
if (c.oracleId.startsWith('token-')) {
|
||||
tokensToRemove.push(c.instanceId);
|
||||
} else {
|
||||
// Move to Library
|
||||
c.zone = 'library';
|
||||
c.tapped = false;
|
||||
c.faceDown = true;
|
||||
c.counters = [];
|
||||
c.modifiers = [];
|
||||
c.damageMarked = 0;
|
||||
c.controlledSinceTurn = 0;
|
||||
c.power = c.basePower;
|
||||
c.toughness = c.baseToughness;
|
||||
c.attachedTo = undefined;
|
||||
c.blocking = undefined;
|
||||
c.attacking = undefined;
|
||||
// Reset position?
|
||||
c.position = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
// Remove tokens
|
||||
tokensToRemove.forEach(id => {
|
||||
delete game.cards[id];
|
||||
});
|
||||
|
||||
console.log(`Game ${roomId} restarted.`);
|
||||
|
||||
// 4. Trigger Start Game (Draw Hands via Rules Engine)
|
||||
const engine = new RulesEngine(game);
|
||||
engine.startGame();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,29 +46,56 @@ export class CardService {
|
||||
imageUrl = card.card_faces[0].image_uris?.normal;
|
||||
}
|
||||
|
||||
if (!imageUrl) continue;
|
||||
|
||||
const filePath = path.join(this.imagesDir, setCode, `${uuid}.jpg`);
|
||||
|
||||
// Check if exists
|
||||
if (await fileStorageManager.exists(filePath)) {
|
||||
continue;
|
||||
// Check for art crop
|
||||
let cropUrl = card.image_uris?.art_crop;
|
||||
if (!cropUrl && card.card_faces && card.card_faces.length > 0) {
|
||||
cropUrl = card.card_faces[0].image_uris?.art_crop;
|
||||
}
|
||||
|
||||
try {
|
||||
// Download
|
||||
const response = await fetch(imageUrl);
|
||||
if (response.ok) {
|
||||
const buffer = await response.arrayBuffer();
|
||||
await fileStorageManager.saveFile(filePath, Buffer.from(buffer));
|
||||
downloadedCount++;
|
||||
console.log(`Cached image: ${setCode}/${uuid}.jpg`);
|
||||
} else {
|
||||
console.error(`Failed to download ${imageUrl}: ${response.statusText}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error downloading image for ${uuid}:`, err);
|
||||
const tasks: Promise<void>[] = [];
|
||||
|
||||
// Task 1: Normal Image (art_full)
|
||||
if (imageUrl) {
|
||||
const filePath = path.join(this.imagesDir, setCode, 'art_full', `${uuid}.jpg`);
|
||||
tasks.push((async () => {
|
||||
if (await fileStorageManager.exists(filePath)) return;
|
||||
try {
|
||||
const response = await fetch(imageUrl);
|
||||
if (response.ok) {
|
||||
const buffer = await response.arrayBuffer();
|
||||
await fileStorageManager.saveFile(filePath, Buffer.from(buffer));
|
||||
downloadedCount++;
|
||||
console.log(`Cached art_full: ${setCode}/${uuid}.jpg`);
|
||||
} else {
|
||||
console.error(`Failed to download art_full ${imageUrl}: ${response.statusText}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error downloading art_full for ${uuid}:`, err);
|
||||
}
|
||||
})());
|
||||
}
|
||||
|
||||
// Task 2: Art Crop (art_crop)
|
||||
if (cropUrl) {
|
||||
const cropPath = path.join(this.imagesDir, setCode, 'art_crop', `${uuid}.jpg`);
|
||||
tasks.push((async () => {
|
||||
if (await fileStorageManager.exists(cropPath)) return;
|
||||
try {
|
||||
const response = await fetch(cropUrl);
|
||||
if (response.ok) {
|
||||
const buffer = await response.arrayBuffer();
|
||||
await fileStorageManager.saveFile(cropPath, Buffer.from(buffer));
|
||||
console.log(`Cached art_crop: ${setCode}/${uuid}.jpg`);
|
||||
} else {
|
||||
console.error(`Failed to download art_crop ${cropUrl}: ${response.statusText}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error downloading art_crop for ${uuid}:`, err);
|
||||
}
|
||||
})());
|
||||
}
|
||||
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user