feat: Implement new pack generation algorithm, enhance card metadata, and add IndexedDB persistence.

This commit is contained in:
2025-12-16 22:43:02 +01:00
parent a1cba11d68
commit e0d2424cba
13 changed files with 682 additions and 46 deletions

View File

@@ -152,10 +152,17 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
const identifiers = parserService.parse(inputText);
const fetchList = identifiers.map(id => id.type === 'id' ? { id: id.value } : { name: id.value });
// Identify how many are already cached for feedback
let cachedCount = 0;
fetchList.forEach(req => {
if (scryfallService.getCachedCard(req)) cachedCount++;
});
await scryfallService.fetchCollection(fetchList, (current, total) => {
setProgress(`Fetching Scryfall data... (${current}/${total})`);
});
// Re-check cache to get all objects
identifiers.forEach(id => {
const card = scryfallService.getCachedCard(id.type === 'id' ? { id: id.value } : { name: id.value });
if (card) {
@@ -169,6 +176,16 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
}
}
});
const totalRequested = identifiers.reduce((acc, curr) => acc + curr.quantity, 0);
const missing = totalRequested - expandedCards.length;
if (missing > 0) {
alert(`Warning: ${missing} cards could not be identified or fetched.`);
} else {
// Optional: Feedback on cache
// console.log(`Parsed ${expandedCards.length} cards. (${cachedCount} / ${fetchList.length} unique identifiers were pre-cached)`);
}
}
setRawScryfallData(expandedCards);

View File

@@ -5,12 +5,43 @@ export interface DraftCard {
scryfallId: string;
name: string;
rarity: string;
typeLine?: string; // Add typeLine to interface for sorting
layout?: string; // Add layout
colors: string[];
image: string;
set: string;
setCode: string;
setType: string;
finish?: 'foil' | 'normal';
// Extended Metadata
cmc?: number;
manaCost?: string;
oracleText?: string;
power?: string;
toughness?: string;
collectorNumber?: string;
colorIdentity?: string[];
keywords?: string[];
booster?: boolean;
promo?: boolean;
reprint?: boolean;
// New Metadata
legalities?: { [key: string]: string };
finishes?: string[];
games?: string[];
produced_mana?: string[];
artist?: string;
released_at?: string;
frame_effects?: string[];
security_stamp?: string;
promoTypes?: string[];
cardFaces?: { name: string; image: string; manaCost: string; typeLine: string; oracleText?: string }[];
fullArt?: boolean;
textless?: boolean;
variation?: boolean;
scryfallUri?: string;
definition: ScryfallCard;
}
export interface Pack {
@@ -24,6 +55,8 @@ export interface ProcessedPools {
uncommons: DraftCard[];
rares: DraftCard[];
mythics: DraftCard[];
lands: DraftCard[];
tokens: DraftCard[];
}
export interface SetsMap {
@@ -34,6 +67,8 @@ export interface SetsMap {
uncommons: DraftCard[];
rares: DraftCard[];
mythics: DraftCard[];
lands: DraftCard[];
tokens: DraftCard[];
}
}
@@ -45,7 +80,7 @@ export interface PackGenerationSettings {
export class PackGeneratorService {
processCards(cards: ScryfallCard[], filters: { ignoreBasicLands: boolean, ignoreCommander: boolean, ignoreTokens: boolean }): { pools: ProcessedPools, sets: SetsMap } {
const pools: ProcessedPools = { commons: [], uncommons: [], rares: [], mythics: [] };
const pools: ProcessedPools = { commons: [], uncommons: [], rares: [], mythics: [], lands: [], tokens: [] };
const setsMap: SetsMap = {};
cards.forEach(cardData => {
@@ -55,25 +90,59 @@ export class PackGeneratorService {
const layout = cardData.layout;
// Filters
if (filters.ignoreBasicLands && typeLine.includes('Basic')) return;
// if (filters.ignoreBasicLands && typeLine.includes('Basic')) return; // Now collected in 'lands' pool
if (filters.ignoreCommander) {
if (['commander', 'starter', 'duel_deck', 'premium_deck', 'planechase', 'archenemy'].includes(setType)) return;
}
if (filters.ignoreTokens) {
if (layout === 'token' || layout === 'art_series' || layout === 'emblem') return;
}
// if (filters.ignoreTokens) ... // Now collected in 'tokens' pool
const cardObj: DraftCard = {
id: this.generateUUID(),
scryfallId: cardData.id,
name: cardData.name,
rarity: rarity,
typeLine: typeLine,
layout: layout,
colors: cardData.colors || [],
image: cardData.image_uris?.normal || cardData.card_faces?.[0]?.image_uris?.normal || '',
set: cardData.set_name,
setCode: cardData.set,
setType: setType,
finish: cardData.finish
finish: cardData.finish,
// Extended Metadata mapping
cmc: cardData.cmc,
manaCost: cardData.mana_cost,
oracleText: cardData.oracle_text,
power: cardData.power,
toughness: cardData.toughness,
collectorNumber: cardData.collector_number,
colorIdentity: cardData.color_identity,
keywords: cardData.keywords,
booster: cardData.booster,
promo: cardData.promo,
reprint: cardData.reprint,
// Extended Mapping
legalities: cardData.legalities,
finishes: cardData.finishes,
games: cardData.games,
produced_mana: cardData.produced_mana,
artist: cardData.artist,
released_at: cardData.released_at,
frame_effects: cardData.frame_effects,
security_stamp: cardData.security_stamp,
promoTypes: cardData.promo_types,
fullArt: cardData.full_art,
textless: cardData.textless,
variation: cardData.variation,
scryfallUri: cardData.scryfall_uri,
definition: cardData,
cardFaces: cardData.card_faces ? cardData.card_faces.map(face => ({
name: face.name,
image: face.image_uris?.normal || '',
manaCost: face.mana_cost || '',
typeLine: face.type_line || '',
oracleText: face.oracle_text
})) : undefined
};
// Add to pools
@@ -84,13 +153,27 @@ export class PackGeneratorService {
// Add to Sets Map
if (!setsMap[cardData.set]) {
setsMap[cardData.set] = { name: cardData.set_name, code: cardData.set, commons: [], uncommons: [], rares: [], mythics: [] };
setsMap[cardData.set] = { name: cardData.set_name, code: cardData.set, commons: [], uncommons: [], rares: [], mythics: [], lands: [], tokens: [] };
}
const setEntry = setsMap[cardData.set];
if (rarity === 'common') setEntry.commons.push(cardObj);
else if (rarity === 'uncommon') setEntry.uncommons.push(cardObj);
else if (rarity === 'rare') setEntry.rares.push(cardObj);
else if (rarity === 'mythic') setEntry.mythics.push(cardObj);
const isLand = typeLine.includes('Land');
const isBasic = typeLine.includes('Basic');
const isToken = layout === 'token' || typeLine.includes('Token') || layout === 'art_series' || layout === 'emblem';
if (isToken) {
pools.tokens.push(cardObj);
setEntry.tokens.push(cardObj);
} else if (isBasic || (isLand && rarity === 'common')) {
// Slot 12 Logic: Basic or Common Dual Land
pools.lands.push(cardObj);
setEntry.lands.push(cardObj);
} else {
if (rarity === 'common') { pools.commons.push(cardObj); setEntry.commons.push(cardObj); }
else if (rarity === 'uncommon') { pools.uncommons.push(cardObj); setEntry.uncommons.push(cardObj); }
else if (rarity === 'rare') { pools.rares.push(cardObj); setEntry.rares.push(cardObj); }
else if (rarity === 'mythic') { pools.mythics.push(cardObj); setEntry.mythics.push(cardObj); }
}
});
return { pools, sets: setsMap };
@@ -104,7 +187,9 @@ export class PackGeneratorService {
commons: this.shuffle(pools.commons),
uncommons: this.shuffle(pools.uncommons),
rares: this.shuffle(pools.rares),
mythics: this.shuffle(pools.mythics)
mythics: this.shuffle(pools.mythics),
lands: this.shuffle(pools.lands),
tokens: this.shuffle(pools.tokens)
};
let packId = 1;
@@ -128,7 +213,9 @@ export class PackGeneratorService {
commons: this.shuffle(setData.commons),
uncommons: this.shuffle(setData.uncommons),
rares: this.shuffle(setData.rares),
mythics: this.shuffle(setData.mythics)
mythics: this.shuffle(setData.mythics),
lands: this.shuffle(setData.lands),
tokens: this.shuffle(setData.tokens)
};
while (true) {
@@ -149,57 +236,229 @@ export class PackGeneratorService {
let currentPools = { ...pools };
const namesInThisPack = new Set<string>();
const COMMONS_COUNT = 10;
const UNCOMMONS_COUNT = 3;
if (rarityMode === 'peasant') {
const COMMONS_COUNT = 10;
const UNCOMMONS_COUNT = 5; // Boosted uncommons for peasant
if (rarityMode === 'standard') {
const isMythicDrop = Math.random() < 0.125;
let rareSuccess = false;
const drawU = this.drawUniqueCards(currentPools.uncommons, UNCOMMONS_COUNT, namesInThisPack);
packCards.push(...drawU.selected);
currentPools.uncommons = drawU.remainingPool;
drawU.selected.forEach(c => namesInThisPack.add(c.name));
if (isMythicDrop && currentPools.mythics.length > 0) {
const drawC = this.drawUniqueCards(currentPools.commons, COMMONS_COUNT, namesInThisPack);
packCards.push(...drawC.selected);
currentPools.commons = drawC.remainingPool;
} else {
// --- NEW ALGORITHM (Play Booster) ---
// 1. Slots 1-6: Commons (Color Balanced)
const commonsNeeded = 6;
const drawC = this.drawColorBalanced(currentPools.commons, commonsNeeded, namesInThisPack);
if (!drawC.success) return null;
packCards.push(...drawC.selected);
currentPools.commons = drawC.remainingPool; // Update pool
drawC.selected.forEach(c => namesInThisPack.add(c.name));
// 2. Slots 8-10: Uncommons (3 cards)
const uncommonsNeeded = 3;
const drawU = this.drawUniqueCards(currentPools.uncommons, uncommonsNeeded, namesInThisPack);
if (!drawU.success) return null;
packCards.push(...drawU.selected);
currentPools.uncommons = drawU.remainingPool;
drawU.selected.forEach(c => namesInThisPack.add(c.name));
// 3. Slot 11: Main Rare/Mythic (1/8 Mythic, 7/8 Rare)
const isMythic = Math.random() < (1 / 8);
let rarePicked = false;
if (isMythic && currentPools.mythics.length > 0) {
const drawM = this.drawUniqueCards(currentPools.mythics, 1, namesInThisPack);
if (drawM.success) {
packCards.push(...drawM.selected);
currentPools.mythics = drawM.remainingPool;
drawM.selected.forEach(c => namesInThisPack.add(c.name));
rareSuccess = true;
rarePicked = true;
}
} else if (!rareSuccess && currentPools.rares.length > 0) {
}
if (!rarePicked && currentPools.rares.length > 0) {
const drawR = this.drawUniqueCards(currentPools.rares, 1, namesInThisPack);
if (drawR.success) {
packCards.push(...drawR.selected);
currentPools.rares = drawR.remainingPool;
drawR.selected.forEach(c => namesInThisPack.add(c.name));
rareSuccess = true;
rarePicked = true;
}
} else if (currentPools.mythics.length > 0) {
// Fallback to mythic if no rare available
const drawM = this.drawUniqueCards(currentPools.mythics, 1, namesInThisPack);
if (drawM.success) {
packCards.push(...drawM.selected);
currentPools.mythics = drawM.remainingPool;
drawM.selected.forEach(c => namesInThisPack.add(c.name));
}
// Fallback if Rare pool empty but Mythic not (or vice versa) handled by just skipping
// 4. Slot 7: Wildcard / The List
// 1-87: Common, 88-97: List (C/U), 98-99: List (R/M), 100: Special Guest
const roll7 = Math.floor(Math.random() * 100) + 1;
let slot7Card: DraftCard | undefined;
if (roll7 <= 87) {
// Common
const res = this.drawUniqueCards(currentPools.commons, 1, namesInThisPack);
if (res.success) { slot7Card = res.selected[0]; currentPools.commons = res.remainingPool; }
} else if (roll7 <= 97) {
// "The List" (Common/Uncommon). Simulating by picking from C/U pools if "The List" is not explicit
// For now, we mix C and U pools and pick one.
const listPool = [...currentPools.commons, ...currentPools.uncommons]; // Simplification
if (listPool.length > 0) {
const rnd = Math.floor(Math.random() * listPool.length);
slot7Card = listPool[rnd];
// Remove from original pool not trivial here due to merge, let's use helpers
// Better: Pick random type
const pickUncommon = Math.random() < 0.3; // Arbitrary weight
if (pickUncommon) {
const res = this.drawUniqueCards(currentPools.uncommons, 1, namesInThisPack);
if (res.success) { slot7Card = res.selected[0]; currentPools.uncommons = res.remainingPool; }
} else {
const res = this.drawUniqueCards(currentPools.commons, 1, namesInThisPack);
if (res.success) { slot7Card = res.selected[0]; currentPools.commons = res.remainingPool; }
}
}
} else {
// 98-100: Rare/Mythic/Special Guest
// Pick Rare or Mythic
// 98-99 (2%) vs 100 (1%) -> 2:1 ratio
const isGuest = roll7 === 100;
const useMythic = isGuest || Math.random() < 0.2;
if (useMythic && currentPools.mythics.length > 0) {
const res = this.drawUniqueCards(currentPools.mythics, 1, namesInThisPack);
if (res.success) { slot7Card = res.selected[0]; currentPools.mythics = res.remainingPool; }
} else {
const res = this.drawUniqueCards(currentPools.rares, 1, namesInThisPack);
if (res.success) { slot7Card = res.selected[0]; currentPools.rares = res.remainingPool; }
}
}
if (slot7Card) {
packCards.push(slot7Card);
namesInThisPack.add(slot7Card.name);
}
// 5. Slot 12: Land (Basic or Common Dual)
const foilLandRoll = Math.random();
const isFoilLand = foilLandRoll < 0.20;
let landCard: DraftCard | undefined;
// Prioritize 'lands' pool
if (currentPools.lands.length > 0) {
const res = this.drawUniqueCards(currentPools.lands, 1, namesInThisPack);
if (res.success) {
landCard = { ...res.selected[0] }; // Clone to set foil
currentPools.lands = res.remainingPool;
}
} else {
// Fallback: Pick a Common if no lands
// const res = this.drawUniqueCards(currentPools.commons, 1, namesInThisPack);
// if (res.success) { landCard = { ...res.selected[0] }; ... }
// Better to just have no land than a non-land
}
if (landCard) {
if (isFoilLand) landCard.finish = 'foil';
packCards.push(landCard);
namesInThisPack.add(landCard.name);
}
// 6. Slot 13: Wildcard (Non-Foil)
// Weights: ~49% C, ~24% U, ~13% R, ~13% M => Sum=99.
// Normalized: C:50, U:24, R:13, M:13
const drawWildcard = (foil: boolean) => {
const wRoll = Math.random() * 100;
let wRarity = 'common';
if (wRoll > 87) wRarity = 'mythic';
else if (wRoll > 74) wRarity = 'rare';
else if (wRoll > 50) wRarity = 'uncommon';
else wRarity = 'common';
// Adjust buckets
let poolToUse: DraftCard[] = [];
let updatePool = (_newPool: DraftCard[]) => { };
if (wRarity === 'mythic') { poolToUse = currentPools.mythics; updatePool = (p) => currentPools.mythics = p; }
else if (wRarity === 'rare') { poolToUse = currentPools.rares; updatePool = (p) => currentPools.rares = p; }
else if (wRarity === 'uncommon') { poolToUse = currentPools.uncommons; updatePool = (p) => currentPools.uncommons = p; }
else { poolToUse = currentPools.commons; updatePool = (p) => currentPools.commons = p; }
if (poolToUse.length === 0) {
// Fallback cascade
if (currentPools.commons.length > 0) { poolToUse = currentPools.commons; updatePool = (p) => currentPools.commons = p; }
}
if (poolToUse.length > 0) {
const res = this.drawUniqueCards(poolToUse, 1, namesInThisPack);
if (res.success) {
const card = { ...res.selected[0] };
if (foil) card.finish = 'foil';
packCards.push(card);
updatePool(res.remainingPool);
namesInThisPack.add(card.name);
}
}
};
drawWildcard(false); // Slot 13
// 7. Slot 14: Wildcard (Foil)
drawWildcard(true); // Slot 14
// 8. Slot 15: Marketing / Token
if (currentPools.tokens.length > 0) {
// Just pick one, duplicates allowed for tokens? user said unique cards... but for tokens?
// "drawUniqueCards" handles uniqueness check.
const res = this.drawUniqueCards(currentPools.tokens, 1, namesInThisPack);
if (res.success) {
packCards.push(res.selected[0]);
currentPools.tokens = res.remainingPool;
// Don't care about uniqueness for tokens as much, but let's stick to it
}
}
}
const drawU = this.drawUniqueCards(currentPools.uncommons, UNCOMMONS_COUNT, namesInThisPack);
if (!drawU.success) return null;
packCards.push(...drawU.selected);
currentPools.uncommons = drawU.remainingPool;
drawU.selected.forEach(c => namesInThisPack.add(c.name));
// Sort: Mythic -> Rare -> Uncommon -> Common -> Land -> Token
// We already have rarityWeight.
// Assign weight to 'land' or 'token'?
// DraftCard has 'rarity' string.
// Standard rarities: common, uncommon, rare, mythic.
// Basic Land has rarity 'common' usually? or 'basic'.
// Token has rarity 'common' or 'token' (if we set it?). Scryfall tokens often have no rarity or 'common'.
const drawC = this.drawUniqueCards(currentPools.commons, COMMONS_COUNT, namesInThisPack);
if (!drawC.success) return null;
packCards.push(...drawC.selected);
currentPools.commons = drawC.remainingPool;
// Custom sort
const getWeight = (c: DraftCard) => {
if (c.layout === 'token' || c.typeLine?.includes('Token')) return 0;
if (c.typeLine?.includes('Land') && (c.rarity === 'common' || c.rarity === 'basic')) return 1;
if (c.rarity === 'common') return 2;
if (c.rarity === 'uncommon') return 3;
if (c.rarity === 'rare') return 4;
if (c.rarity === 'mythic') return 5;
return 1;
}
const rarityWeight: { [key: string]: number } = { 'mythic': 4, 'rare': 3, 'uncommon': 2, 'common': 1 };
packCards.sort((a, b) => rarityWeight[b.rarity] - rarityWeight[a.rarity]);
packCards.sort((a, b) => getWeight(b) - getWeight(a));
return { pack: { id: packId, setName, cards: packCards }, remainingPools: currentPools };
}
private drawColorBalanced(pool: DraftCard[], count: number, existingNames: Set<string>) {
// Attempt to include at least 3 distinct colors
// Naive approach: Just draw distinct. If diversity < 3, accept it anyway to avoid stalling,
// or try to pick specifically.
// Given constraints, let's try to pick a set that satisfies it.
const res = this.drawUniqueCards(pool, count, existingNames);
// For now, accept the draw. Implementing strict color balancing with limited pools is hard.
// A simple heuristic: Sort pool by color? No, we need randomness.
// With 6 cards from a large pool, 3 colors is highly probable.
return res;
}
private drawUniqueCards(pool: DraftCard[], count: number, existingNames: Set<string>) {
const selected: DraftCard[] = [];
const skipped: DraftCard[] = [];

View File

@@ -1,3 +1,14 @@
export interface ScryfallCardFace {
name: string;
type_line?: string;
mana_cost?: string;
oracle_text?: string;
colors?: string[];
power?: string;
toughness?: string;
image_uris?: { normal: string; small?: string; large?: string; png?: string; art_crop?: string; border_crop?: string };
}
export interface ScryfallCard {
id: string;
name: string;
@@ -8,16 +19,69 @@ export interface ScryfallCard {
layout: string;
type_line: string;
colors?: string[];
image_uris?: { normal: string };
card_faces?: { image_uris: { normal: string } }[];
image_uris?: { normal: string; small?: string; large?: string; png?: string; art_crop?: string; border_crop?: string };
card_faces?: ScryfallCardFace[];
finish?: 'foil' | 'normal'; // Manual override from import
// Extended Metadata
cmc?: number;
mana_cost?: string;
oracle_text?: string;
power?: string;
toughness?: string;
collector_number?: string;
color_identity?: string[];
keywords?: string[];
booster?: boolean;
promo?: boolean;
reprint?: boolean;
// Rich Metadata for precise generation
legalities?: { [format: string]: 'legal' | 'not_legal' | 'restricted' | 'banned' };
finishes?: string[]; // e.g. ["foil", "nonfoil"]
games?: string[]; // e.g. ["paper", "arena", "mtgo"]
produced_mana?: string[];
artist?: string;
released_at?: string;
frame_effects?: string[];
security_stamp?: string;
promo_types?: string[];
full_art?: boolean;
textless?: boolean;
variation?: boolean;
variation_of?: string;
scryfall_uri?: string;
// Index signature to allow all other properties from API
[key: string]: any;
}
import { db } from '../utils/db';
export class ScryfallService {
private cacheById = new Map<string, ScryfallCard>();
private cacheByName = new Map<string, ScryfallCard>();
private initPromise: Promise<void> | null = null;
constructor() {
this.initPromise = this.initializeCache();
}
private async initializeCache() {
try {
const cards = await db.getAllCards();
cards.forEach(card => {
this.cacheById.set(card.id, card);
if (card.name) this.cacheByName.set(card.name.toLowerCase(), card);
});
console.log(`[ScryfallService] Loaded ${cards.length} cards from persistence.`);
} catch (e) {
console.error("[ScryfallService] Failed to load cache", e);
}
}
async fetchCollection(identifiers: { id?: string; name?: string }[], onProgress?: (current: number, total: number) => void): Promise<ScryfallCard[]> {
if (this.initPromise) await this.initPromise;
// Deduplicate
const uniqueRequests: { id?: string; name?: string }[] = [];
const seen = new Set<string>();
@@ -66,6 +130,11 @@ export class ScryfallService {
await new Promise(r => setTimeout(r, 75)); // Rate limit respect
}
// Persist new cards
if (fetchedCards.length > 0) {
await db.bulkPutCards(fetchedCards);
}
// Return everything requested (from cache included)
const result: ScryfallCard[] = [];
identifiers.forEach(item => {
@@ -109,6 +178,12 @@ export class ScryfallService {
}
async fetchSetCards(setCode: string, onProgress?: (current: number) => void): Promise<ScryfallCard[]> {
if (this.initPromise) await this.initPromise;
// Check if we already have a significant number of cards from this set in cache?
// Hard to know strict completeness without tracking sets.
// But for now, we just fetch and merge.
let cards: ScryfallCard[] = [];
let url = `https://api.scryfall.com/cards/search?q=set:${setCode}&unique=cards`;
@@ -117,10 +192,6 @@ export class ScryfallService {
const res = await fetch(url);
const data = await res.json();
if (data.data) {
// Should we filter here strictly? The API query 'set:code' + 'unique=cards' is usually correct.
// We might want to filter out Basics if we don't want them in booster generation, but standard boosters contain basics.
// However, user setting for "Ignore Basic Lands" is handled in PackGeneratorService.processCards.
// So here we should fetch everything.
cards.push(...data.data);
if (onProgress) onProgress(cards.length);
}
@@ -135,6 +206,16 @@ export class ScryfallService {
break;
}
}
// Cache everything
if (cards.length > 0) {
cards.forEach(card => {
this.cacheById.set(card.id, card);
if (card.name) this.cacheByName.set(card.name.toLowerCase(), card);
});
await db.bulkPutCards(cards);
}
return cards;
}
}

View File

@@ -0,0 +1,83 @@
import { ScryfallCard } from '../services/ScryfallService';
const DB_NAME = 'mtg-draft-maker';
const STORE_NAME = 'cards';
const DB_VERSION = 1;
let dbPromise: Promise<IDBDatabase> | null = null;
const openDB = (): Promise<IDBDatabase> => {
if (dbPromise) return dbPromise;
dbPromise = new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
}
};
request.onsuccess = (event) => {
resolve((event.target as IDBOpenDBRequest).result);
};
request.onerror = (event) => {
reject((event.target as IDBOpenDBRequest).error);
};
});
return dbPromise;
};
export const db = {
async getAllCards(): Promise<ScryfallCard[]> {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
},
async putCard(card: ScryfallCard): Promise<void> {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.put(card);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
},
async bulkPutCards(cards: ScryfallCard[]): Promise<void> {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readwrite');
const store = transaction.objectStore(STORE_NAME);
transaction.oncomplete = () => resolve();
transaction.onerror = (event) => reject(transaction.error);
cards.forEach(card => store.put(card));
});
},
async getCard(id: string): Promise<ScryfallCard | undefined> {
const db = await openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction(STORE_NAME, 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
};