Compare commits
2 Commits
418e9e4507
...
4e36157115
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e36157115 | |||
| 139aca6f4f |
@@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
trigger: always_on
|
|
||||||
---
|
|
||||||
|
|
||||||
Valid for all generations:
|
|
||||||
- If foils are not available in the pool, ignore the foil generation
|
|
||||||
|
|
||||||
STANDARD GENERATION:
|
|
||||||
|
|
||||||
Slots 1-6 (Commons): 6 Common cards. Ensure color balance (attempt to include at least 3 distinct colors).
|
|
||||||
Slot 7 (Common/List Slot):
|
|
||||||
- Roll a d100.
|
|
||||||
- 1-87: 1 Common from Main Set.
|
|
||||||
- 88-97: 1 Card from "The List" (Common/Uncommon reprint).
|
|
||||||
- 98-99: 1 Rare/Mythic from "The List".
|
|
||||||
- 100: 1 Special Guest (High Value).
|
|
||||||
Slots 8-10 (Uncommons): 3 Uncommon cards.
|
|
||||||
Slot 11 (Main Rare Slot):
|
|
||||||
- Roll 1d8.
|
|
||||||
- If 1-7: Rare.
|
|
||||||
- If 8: Mythic Rare.
|
|
||||||
Slot 12 (Land): 1 Basic or Common Dual Land (20% chance of Foil).
|
|
||||||
Slot 13 (Non-Foil Wildcard):
|
|
||||||
- Can be any rarity (Common, Uncommon, Rare, Mythic).
|
|
||||||
- Use weighted probability: ~49% Common, ~24% Uncommon, ~13% Rare, ~13% Mythic (simplified for simulation).
|
|
||||||
Slot 14 (Foil Wildcard):
|
|
||||||
- Same rarity weights as Slot 13, but the card must be Foil.
|
|
||||||
Slot 15 (Marketing): Token or Art Card.
|
|
||||||
|
|
||||||
PEASANT GENERATION:
|
|
||||||
|
|
||||||
Slots 1-6 (Commons): 6 Common cards. Ensure color balance (attempt to include at least 3 distinct colors).
|
|
||||||
Slot 7 (Common/List Slot):
|
|
||||||
- Roll a d100.
|
|
||||||
- 1-87: 1 Common from Main Set.
|
|
||||||
- 88-97: 1 Card from "The List" (Common/Uncommon reprint).
|
|
||||||
- 98-100: 1 Uncommon from "The List".
|
|
||||||
Slots 8-11 (Uncommons): 4 Uncommon cards.
|
|
||||||
Slot 12 (Land): 1 Basic or Common Dual Land (20% chance of Foil).
|
|
||||||
Slot 13 (Non-Foil Wildcard):
|
|
||||||
- Can be any rarity (Common, Uncommon, Rare, Mythic).
|
|
||||||
- Use weighted probability: ~49% Common, ~24% Uncommon, ~13% Rare, ~13% Mythic (simplified for simulation).
|
|
||||||
Slot 14 (Foil Wildcard):
|
|
||||||
- Same rarity weights as Slot 13, but the card must be Foil.
|
|
||||||
Slot 15 (Marketing): Token or Art Card.
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
Slots 1-6 (Commons): 6 Common cards. Ensure color balance (attempt to include at least 3 distinct colors).
|
||||||
|
Slot 7 (Common/List Slot):
|
||||||
|
- Roll a d100.
|
||||||
|
- 1-87: 1 Common from Main Set.
|
||||||
|
- 88-97: 1 Card from "The List" (Common/Uncommon reprint).
|
||||||
|
- 98-100: 1 Uncommon from "The List".
|
||||||
|
Slots 8-11 (Uncommons): 4 Uncommon cards.
|
||||||
|
Slot 12 (Land): 1 Basic or Common Dual Land (20% chance of Foil).
|
||||||
|
Slot 13 (Non-Foil Wildcard):
|
||||||
|
- Can be any rarity (Common, Uncommon, Rare, Mythic).
|
||||||
|
- Use weighted probability: ~62% Common, ~37% Uncommon.
|
||||||
|
- Can be a card from the child sets.
|
||||||
|
Slot 14 (Foil Wildcard):
|
||||||
|
- Same rarity weights as Slot 13, but the card must be Foil.
|
||||||
|
Slot 15 (Marketing): Token or Art Card.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
Slots 1-6 (Commons): 6 Common cards. Ensure color balance (attempt to include at least 3 distinct colors).
|
||||||
|
Slot 7 (Common/List Slot):
|
||||||
|
- Roll a d100.
|
||||||
|
- 1-87: 1 Common from Main Set.
|
||||||
|
- 88-97: 1 Card from "The List" (Common/Uncommon reprint).
|
||||||
|
- 98-99: 1 Rare/Mythic from "The List".
|
||||||
|
- 100: 1 Special Guest (High Value).
|
||||||
|
Slots 8-10 (Uncommons): 3 Uncommon cards.
|
||||||
|
Slot 11 (Main Rare Slot):
|
||||||
|
- Roll 1d8.
|
||||||
|
- If 1-7: Rare.
|
||||||
|
- If 8: Mythic Rare.
|
||||||
|
Slot 12 (Land): 1 Basic or Common Dual Land (20% chance of Foil).
|
||||||
|
Slot 13 (Non-Foil Wildcard):
|
||||||
|
- Can be any rarity (Common, Uncommon, Rare, Mythic).
|
||||||
|
- Use weighted probability: ~49% Common, ~24% Uncommon, ~13% Rare, ~13% Mythic.
|
||||||
|
- Can be a card from the child sets.
|
||||||
|
Slot 14 (Foil Wildcard):
|
||||||
|
- Same rarity weights as Slot 13, but the card must be Foil.
|
||||||
|
Slot 15 (Marketing): Token or Art Card.
|
||||||
@@ -82,7 +82,7 @@ define(['./workbox-5a5d9309'], (function (workbox) { 'use strict';
|
|||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.rc445urejpk"
|
"revision": "0.g6k3e4tvo1g"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|||||||
@@ -144,7 +144,12 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rawScryfallData) {
|
if (rawScryfallData) {
|
||||||
// Use local images: true
|
// Use local images: true
|
||||||
const result = generatorService.processCards(rawScryfallData, filters, true);
|
const setsMetadata = availableSets.reduce((acc, set) => {
|
||||||
|
acc[set.code] = { parent_set_code: set.parent_set_code };
|
||||||
|
return acc;
|
||||||
|
}, {} as { [code: string]: { parent_set_code?: string } });
|
||||||
|
|
||||||
|
const result = generatorService.processCards(rawScryfallData, filters, true, setsMetadata);
|
||||||
setProcessedData(result);
|
setProcessedData(result);
|
||||||
}
|
}
|
||||||
}, [filters, rawScryfallData]);
|
}, [filters, rawScryfallData]);
|
||||||
@@ -217,12 +222,70 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
|
|||||||
|
|
||||||
if (sourceMode === 'set') {
|
if (sourceMode === 'set') {
|
||||||
// Fetch set by set
|
// Fetch set by set
|
||||||
for (const [index, setCode] of selectedSets.entries()) {
|
// Fetch sets (Grouping Main + Subsets)
|
||||||
setProgress(`Fetching set ${setCode.toUpperCase()} (${index + 1}/${selectedSets.length})...`);
|
// We iterate through selectedSets. If a set has children also in selectedSets (or auto-detected), we fetch them together.
|
||||||
const response = await fetch(`/api/sets/${setCode}/cards`);
|
// We need to avoid fetching the child set again if it was covered by the parent.
|
||||||
if (!response.ok) throw new Error(`Failed to fetch set ${setCode}`);
|
|
||||||
|
const processedSets = new Set<string>();
|
||||||
|
|
||||||
|
// We already have `effectiveSelectedSets` which includes auto-added ones.
|
||||||
|
// Let's re-derive effective logic locally for fetching.
|
||||||
|
const allSetsToProcess = [...selectedSets];
|
||||||
|
const linkedSubsets = availableSets.filter(s =>
|
||||||
|
s.parent_set_code &&
|
||||||
|
selectedSets.includes(s.parent_set_code) &&
|
||||||
|
s.code.length === 3 && // 3-letter code filter
|
||||||
|
!selectedSets.includes(s.code)
|
||||||
|
).map(s => s.code);
|
||||||
|
allSetsToProcess.push(...linkedSubsets);
|
||||||
|
|
||||||
|
let totalCards = 0;
|
||||||
|
let setIndex = 0;
|
||||||
|
|
||||||
|
for (const setCode of allSetsToProcess) {
|
||||||
|
if (processedSets.has(setCode)) continue;
|
||||||
|
|
||||||
|
// Check if this is a Main Set that has children in our list
|
||||||
|
// OR if it's a child that should be fetched with its parent?
|
||||||
|
// Actually, we should look for Main Sets first.
|
||||||
|
|
||||||
|
let currentMain = setCode;
|
||||||
|
let currentRelated: string[] = [];
|
||||||
|
|
||||||
|
// Find children of this set in our list
|
||||||
|
const children = allSetsToProcess.filter(s => {
|
||||||
|
const meta = availableSets.find(as => as.code === s);
|
||||||
|
return meta && meta.parent_set_code === currentMain;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also check if this set IS a child, and its parent is NOT in the list?
|
||||||
|
// If parent IS in the list, we skip this iteration and let the parent handle it?
|
||||||
|
const meta = availableSets.find(as => as.code === currentMain);
|
||||||
|
if (meta && meta.parent_set_code && allSetsToProcess.includes(meta.parent_set_code)) {
|
||||||
|
// This is a child, and we are processing the parent elsewhere. Skip.
|
||||||
|
// But wait, the loop order is undefined.
|
||||||
|
// Safest: always fetch by Main Set if possible.
|
||||||
|
// If we encounter a Child whose parent is in the list, we skip.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (children.length > 0) {
|
||||||
|
currentRelated = children;
|
||||||
|
currentRelated.forEach(c => processedSets.add(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
processedSets.add(currentMain);
|
||||||
|
setIndex++;
|
||||||
|
|
||||||
|
setProgress(`Fetching set ${currentMain.toUpperCase()} ${currentRelated.length > 0 ? `(+ ${currentRelated.join(', ').toUpperCase()})` : ''}...`);
|
||||||
|
|
||||||
|
const queryParams = currentRelated.length > 0 ? `?related=${currentRelated.join(',')}` : '';
|
||||||
|
const response = await fetch(`/api/sets/${currentMain}/cards${queryParams}`);
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`Failed to fetch set ${currentMain}`);
|
||||||
const cards: ScryfallCard[] = await response.json();
|
const cards: ScryfallCard[] = await response.json();
|
||||||
currentCards.push(...cards);
|
setRawScryfallData(prev => [...(prev || []), ...cards]);
|
||||||
|
totalCards += cards.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -249,10 +312,20 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
|
|||||||
// --- Step 2: Generate ---
|
// --- Step 2: Generate ---
|
||||||
setProgress('Generating packs on server...');
|
setProgress('Generating packs on server...');
|
||||||
|
|
||||||
|
// Re-calculation of effective sets for Payload is safe to match.
|
||||||
|
const payloadSetCodes = [...selectedSets];
|
||||||
|
const linkedPayload = availableSets.filter(s =>
|
||||||
|
s.parent_set_code &&
|
||||||
|
selectedSets.includes(s.parent_set_code) &&
|
||||||
|
s.code.length === 3 && // 3-letter code filter
|
||||||
|
!selectedSets.includes(s.code)
|
||||||
|
).map(s => s.code);
|
||||||
|
payloadSetCodes.push(...linkedPayload);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
cards: sourceMode === 'upload' ? currentCards : [], // For set mode, we let server refetch or handle it
|
cards: sourceMode === 'upload' ? currentCards : [], // For set mode, we let server refetch or handle it
|
||||||
sourceMode,
|
sourceMode,
|
||||||
selectedSets,
|
selectedSets: payloadSetCodes,
|
||||||
settings: {
|
settings: {
|
||||||
...genSettings,
|
...genSettings,
|
||||||
withReplacement: sourceMode === 'set'
|
withReplacement: sourceMode === 'set'
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export interface ProcessedPools {
|
|||||||
mythics: DraftCard[];
|
mythics: DraftCard[];
|
||||||
lands: DraftCard[];
|
lands: DraftCard[];
|
||||||
tokens: DraftCard[];
|
tokens: DraftCard[];
|
||||||
|
specialGuests: DraftCard[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetsMap {
|
export interface SetsMap {
|
||||||
@@ -71,6 +72,7 @@ export interface SetsMap {
|
|||||||
mythics: DraftCard[];
|
mythics: DraftCard[];
|
||||||
lands: DraftCard[];
|
lands: DraftCard[];
|
||||||
tokens: DraftCard[];
|
tokens: DraftCard[];
|
||||||
|
specialGuests: DraftCard[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,10 +84,11 @@ export interface PackGenerationSettings {
|
|||||||
|
|
||||||
export class PackGeneratorService {
|
export class PackGeneratorService {
|
||||||
|
|
||||||
processCards(cards: ScryfallCard[], filters: { ignoreBasicLands: boolean, ignoreCommander: boolean, ignoreTokens: boolean }, useLocalImages: boolean = false): { pools: ProcessedPools, sets: SetsMap } {
|
processCards(cards: ScryfallCard[], filters: { ignoreBasicLands: boolean, ignoreCommander: boolean, ignoreTokens: boolean }, useLocalImages: boolean = false, setsMetadata: { [code: string]: { parent_set_code?: string } } = {}): { pools: ProcessedPools, sets: SetsMap } {
|
||||||
const pools: ProcessedPools = { commons: [], uncommons: [], rares: [], mythics: [], lands: [], tokens: [] };
|
const pools: ProcessedPools = { commons: [], uncommons: [], rares: [], mythics: [], lands: [], tokens: [], specialGuests: [] };
|
||||||
const setsMap: SetsMap = {};
|
const setsMap: SetsMap = {};
|
||||||
|
|
||||||
|
// 1. First Pass: Organize into SetsMap
|
||||||
cards.forEach(cardData => {
|
cards.forEach(cardData => {
|
||||||
const rarity = cardData.rarity;
|
const rarity = cardData.rarity;
|
||||||
const typeLine = cardData.type_line || '';
|
const typeLine = cardData.type_line || '';
|
||||||
@@ -159,10 +162,11 @@ export class PackGeneratorService {
|
|||||||
else if (rarity === 'uncommon') pools.uncommons.push(cardObj);
|
else if (rarity === 'uncommon') pools.uncommons.push(cardObj);
|
||||||
else if (rarity === 'rare') pools.rares.push(cardObj);
|
else if (rarity === 'rare') pools.rares.push(cardObj);
|
||||||
else if (rarity === 'mythic') pools.mythics.push(cardObj);
|
else if (rarity === 'mythic') pools.mythics.push(cardObj);
|
||||||
|
else pools.specialGuests.push(cardObj); // Catch-all for special/bonus
|
||||||
|
|
||||||
// Add to Sets Map
|
// Add to Sets Map
|
||||||
if (!setsMap[cardData.set]) {
|
if (!setsMap[cardData.set]) {
|
||||||
setsMap[cardData.set] = { name: cardData.set_name, code: cardData.set, commons: [], uncommons: [], rares: [], mythics: [], lands: [], tokens: [] };
|
setsMap[cardData.set] = { name: cardData.set_name, code: cardData.set, commons: [], uncommons: [], rares: [], mythics: [], lands: [], tokens: [], specialGuests: [] };
|
||||||
}
|
}
|
||||||
const setEntry = setsMap[cardData.set];
|
const setEntry = setsMap[cardData.set];
|
||||||
|
|
||||||
@@ -182,6 +186,43 @@ export class PackGeneratorService {
|
|||||||
else if (rarity === 'uncommon') { pools.uncommons.push(cardObj); setEntry.uncommons.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 === 'rare') { pools.rares.push(cardObj); setEntry.rares.push(cardObj); }
|
||||||
else if (rarity === 'mythic') { pools.mythics.push(cardObj); setEntry.mythics.push(cardObj); }
|
else if (rarity === 'mythic') { pools.mythics.push(cardObj); setEntry.mythics.push(cardObj); }
|
||||||
|
else { pools.specialGuests.push(cardObj); setEntry.specialGuests.push(cardObj); } // Catch-all for special/bonus
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Second Pass: Merge Subsets (Masterpieces) into Parents
|
||||||
|
Object.keys(setsMap).forEach(setCode => {
|
||||||
|
const meta = setsMetadata[setCode];
|
||||||
|
if (meta && meta.parent_set_code) {
|
||||||
|
const parentCode = meta.parent_set_code;
|
||||||
|
if (setsMap[parentCode]) {
|
||||||
|
const parentSet = setsMap[parentCode];
|
||||||
|
const childSet = setsMap[setCode];
|
||||||
|
|
||||||
|
// Move ALL cards from child set to parent's 'specialGuests' pool
|
||||||
|
// We iterate all pools of the child set
|
||||||
|
const allChildCards = [
|
||||||
|
...childSet.commons,
|
||||||
|
...childSet.uncommons,
|
||||||
|
...childSet.rares,
|
||||||
|
...childSet.mythics,
|
||||||
|
...childSet.specialGuests, // Include explicit specials
|
||||||
|
// ...childSet.lands, // usually keeps land separate? or special lands?
|
||||||
|
// Let's treat everything non-token as special guest candidate
|
||||||
|
];
|
||||||
|
|
||||||
|
parentSet.specialGuests.push(...allChildCards);
|
||||||
|
pools.specialGuests.push(...allChildCards);
|
||||||
|
|
||||||
|
// IMPORTANT: If we are in 'by_set' mode, we might NOT want to generate packs for the child set anymore?
|
||||||
|
// Or we leave them there but they are ALSO in the parent's special pool?
|
||||||
|
// The request implies "merged".
|
||||||
|
// If we leave them in setsMap under their own code, they will generate their own packs in 'by_set' mode.
|
||||||
|
// If the user selected BOTH, they probably want the "Special Guest" experience AND maybe separate packs?
|
||||||
|
// Usually "Drafting WOT" separately is possible.
|
||||||
|
// But "Drafting WOE" should include "WOT".
|
||||||
|
// So copying is correct.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -198,7 +239,8 @@ export class PackGeneratorService {
|
|||||||
rares: this.shuffle(pools.rares),
|
rares: this.shuffle(pools.rares),
|
||||||
mythics: this.shuffle(pools.mythics),
|
mythics: this.shuffle(pools.mythics),
|
||||||
lands: this.shuffle(pools.lands),
|
lands: this.shuffle(pools.lands),
|
||||||
tokens: this.shuffle(pools.tokens)
|
tokens: this.shuffle(pools.tokens),
|
||||||
|
specialGuests: this.shuffle(pools.specialGuests)
|
||||||
};
|
};
|
||||||
|
|
||||||
let packId = 1;
|
let packId = 1;
|
||||||
@@ -224,7 +266,8 @@ export class PackGeneratorService {
|
|||||||
rares: this.shuffle(setData.rares),
|
rares: this.shuffle(setData.rares),
|
||||||
mythics: this.shuffle(setData.mythics),
|
mythics: this.shuffle(setData.mythics),
|
||||||
lands: this.shuffle(setData.lands),
|
lands: this.shuffle(setData.lands),
|
||||||
tokens: this.shuffle(setData.tokens)
|
tokens: this.shuffle(setData.tokens),
|
||||||
|
specialGuests: this.shuffle(setData.specialGuests)
|
||||||
};
|
};
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -251,10 +294,6 @@ export class PackGeneratorService {
|
|||||||
const drawC = this.drawColorBalanced(currentPools.commons, commonsNeeded, namesInThisPack);
|
const drawC = this.drawColorBalanced(currentPools.commons, commonsNeeded, namesInThisPack);
|
||||||
|
|
||||||
if (!drawC.success && currentPools.commons.length >= commonsNeeded) {
|
if (!drawC.success && currentPools.commons.length >= commonsNeeded) {
|
||||||
// If we have enough cards but failed strict color balancing, we might accept it or fail.
|
|
||||||
// Standard algo returns null on failure. Let's do same to be safe, or just accept partial.
|
|
||||||
// Given "Naive approach" in drawColorBalanced, if it returns success=false but has cards, it meant it couldn't find unique ones?
|
|
||||||
// drawUniqueCards (called by drawColorBalanced) checks if we have enough cards.
|
|
||||||
return null;
|
return null;
|
||||||
} else if (currentPools.commons.length < commonsNeeded) {
|
} else if (currentPools.commons.length < commonsNeeded) {
|
||||||
return null;
|
return null;
|
||||||
@@ -265,9 +304,9 @@ export class PackGeneratorService {
|
|||||||
drawC.selected.forEach(c => namesInThisPack.add(c.name));
|
drawC.selected.forEach(c => namesInThisPack.add(c.name));
|
||||||
|
|
||||||
// 2. Slot 7: Common / The List
|
// 2. Slot 7: Common / The List
|
||||||
// 1-87: Common from Main Set
|
// 1-87: 1 Common from Main Set.
|
||||||
// 88-97: Card from "The List" (Common/Uncommon)
|
// 88-97: 1 Card from "The List" (Common/Uncommon reprint).
|
||||||
// 98-100: Uncommon from "The List"
|
// 98-100: 1 Uncommon from "The List".
|
||||||
const roll7 = Math.floor(Math.random() * 100) + 1;
|
const roll7 = Math.floor(Math.random() * 100) + 1;
|
||||||
let slot7Card: DraftCard | undefined;
|
let slot7Card: DraftCard | undefined;
|
||||||
|
|
||||||
@@ -276,25 +315,30 @@ export class PackGeneratorService {
|
|||||||
const res = this.drawUniqueCards(currentPools.commons, 1, namesInThisPack);
|
const res = this.drawUniqueCards(currentPools.commons, 1, namesInThisPack);
|
||||||
if (res.success) { slot7Card = res.selected[0]; currentPools.commons = res.remainingPool; }
|
if (res.success) { slot7Card = res.selected[0]; currentPools.commons = res.remainingPool; }
|
||||||
} else if (roll7 <= 97) {
|
} else if (roll7 <= 97) {
|
||||||
// List (Common/Uncommon). Simulating by picking 50/50 C/U if actual List not available
|
// List (Common/Uncommon). Use SpecialGuests or 50/50 fallback
|
||||||
const useUncommon = Math.random() < 0.5;
|
if (currentPools.specialGuests.length > 0) {
|
||||||
const pool = useUncommon ? currentPools.uncommons : currentPools.commons;
|
const res = this.drawUniqueCards(currentPools.specialGuests, 1, namesInThisPack);
|
||||||
// Fallback if one pool is empty
|
if (res.success) { slot7Card = res.selected[0]; currentPools.specialGuests = res.remainingPool; }
|
||||||
const effectivePool = pool.length > 0 ? pool : (useUncommon ? currentPools.commons : currentPools.uncommons);
|
} else {
|
||||||
|
// Fallback
|
||||||
if (effectivePool.length > 0) {
|
const pool = Math.random() < 0.5 ? currentPools.commons : currentPools.uncommons;
|
||||||
const res = this.drawUniqueCards(effectivePool, 1, namesInThisPack);
|
const res = this.drawUniqueCards(pool, 1, namesInThisPack);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
slot7Card = res.selected[0];
|
slot7Card = res.selected[0];
|
||||||
// Identify which pool to update
|
if (pool === currentPools.commons) currentPools.commons = res.remainingPool;
|
||||||
if (effectivePool === currentPools.uncommons) currentPools.uncommons = res.remainingPool;
|
else currentPools.uncommons = res.remainingPool;
|
||||||
else currentPools.commons = res.remainingPool;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 98-100: Uncommon (from List or pool)
|
// 98-100: Uncommon from "The List"
|
||||||
const res = this.drawUniqueCards(currentPools.uncommons, 1, namesInThisPack);
|
if (currentPools.specialGuests.length > 0) {
|
||||||
if (res.success) { slot7Card = res.selected[0]; currentPools.uncommons = res.remainingPool; }
|
const res = this.drawUniqueCards(currentPools.specialGuests, 1, namesInThisPack);
|
||||||
|
if (res.success) { slot7Card = res.selected[0]; currentPools.specialGuests = res.remainingPool; }
|
||||||
|
} else {
|
||||||
|
// Fallback
|
||||||
|
const res = this.drawUniqueCards(currentPools.uncommons, 1, namesInThisPack);
|
||||||
|
if (res.success) { slot7Card = res.selected[0]; currentPools.uncommons = res.remainingPool; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slot7Card) {
|
if (slot7Card) {
|
||||||
@@ -305,7 +349,6 @@ export class PackGeneratorService {
|
|||||||
// 3. Slots 8-11: Uncommons (4 cards)
|
// 3. Slots 8-11: Uncommons (4 cards)
|
||||||
const uncommonsNeeded = 4;
|
const uncommonsNeeded = 4;
|
||||||
const drawU = this.drawUniqueCards(currentPools.uncommons, uncommonsNeeded, namesInThisPack);
|
const drawU = this.drawUniqueCards(currentPools.uncommons, uncommonsNeeded, namesInThisPack);
|
||||||
// We accept partial if pool depleted to avoid crashing, but standard behavior is usually strict.
|
|
||||||
packCards.push(...drawU.selected);
|
packCards.push(...drawU.selected);
|
||||||
currentPools.uncommons = drawU.remainingPool;
|
currentPools.uncommons = drawU.remainingPool;
|
||||||
drawU.selected.forEach(c => namesInThisPack.add(c.name));
|
drawU.selected.forEach(c => namesInThisPack.add(c.name));
|
||||||
@@ -329,25 +372,19 @@ export class PackGeneratorService {
|
|||||||
namesInThisPack.add(landCard.name);
|
namesInThisPack.add(landCard.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper for Wildcards
|
// Helper for Wildcards (Peasant)
|
||||||
const drawWildcard = (foil: boolean) => {
|
const drawWildcard = (foil: boolean) => {
|
||||||
|
// ~62% Common, ~37% Uncommon
|
||||||
const wRoll = Math.random() * 100;
|
const wRoll = Math.random() * 100;
|
||||||
let wRarity = 'common';
|
let wRarity = 'common';
|
||||||
// ~49% Common, ~24% Uncommon, ~13% Rare, ~13% Mythic
|
if (wRoll > 62) wRarity = 'uncommon';
|
||||||
if (wRoll > 87) wRarity = 'mythic';
|
|
||||||
else if (wRoll > 74) wRarity = 'rare';
|
|
||||||
else if (wRoll > 50) wRarity = 'uncommon';
|
|
||||||
else wRarity = 'common';
|
|
||||||
|
|
||||||
let poolToUse: DraftCard[] = [];
|
let poolToUse: DraftCard[] = [];
|
||||||
let updatePool = (_newPool: DraftCard[]) => { };
|
let updatePool = (_newPool: DraftCard[]) => { };
|
||||||
|
|
||||||
if (wRarity === 'mythic') { poolToUse = currentPools.mythics; updatePool = (p) => currentPools.mythics = p; }
|
if (wRarity === 'uncommon') { poolToUse = currentPools.uncommons; updatePool = (p) => currentPools.uncommons = 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; }
|
else { poolToUse = currentPools.commons; updatePool = (p) => currentPools.commons = p; }
|
||||||
|
|
||||||
// Fallback
|
|
||||||
if (poolToUse.length === 0) {
|
if (poolToUse.length === 0) {
|
||||||
if (currentPools.commons.length > 0) { poolToUse = currentPools.commons; updatePool = (p) => currentPools.commons = p; }
|
if (currentPools.commons.length > 0) { poolToUse = currentPools.commons; updatePool = (p) => currentPools.commons = p; }
|
||||||
}
|
}
|
||||||
@@ -380,14 +417,14 @@ export class PackGeneratorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// --- NEW ALGORITHM (Play Booster) ---
|
// --- NEW ALGORITHM (Standard / Play Booster) ---
|
||||||
|
|
||||||
// 1. Slots 1-6: Commons (Color Balanced)
|
// 1. Slots 1-6: Commons (Color Balanced)
|
||||||
const commonsNeeded = 6;
|
const commonsNeeded = 6;
|
||||||
const drawC = this.drawColorBalanced(currentPools.commons, commonsNeeded, namesInThisPack);
|
const drawC = this.drawColorBalanced(currentPools.commons, commonsNeeded, namesInThisPack);
|
||||||
if (!drawC.success) return null;
|
if (!drawC.success) return null;
|
||||||
packCards.push(...drawC.selected);
|
packCards.push(...drawC.selected);
|
||||||
currentPools.commons = drawC.remainingPool; // Update pool
|
currentPools.commons = drawC.remainingPool;
|
||||||
drawC.selected.forEach(c => namesInThisPack.add(c.name));
|
drawC.selected.forEach(c => namesInThisPack.add(c.name));
|
||||||
|
|
||||||
// 2. Slots 8-10: Uncommons (3 cards)
|
// 2. Slots 8-10: Uncommons (3 cards)
|
||||||
@@ -399,7 +436,7 @@ export class PackGeneratorService {
|
|||||||
drawU.selected.forEach(c => namesInThisPack.add(c.name));
|
drawU.selected.forEach(c => namesInThisPack.add(c.name));
|
||||||
|
|
||||||
// 3. Slot 11: Main Rare/Mythic (1/8 Mythic, 7/8 Rare)
|
// 3. Slot 11: Main Rare/Mythic (1/8 Mythic, 7/8 Rare)
|
||||||
const isMythic = Math.random() < (1 / 8);
|
const isMythic = Math.random() < 0.125;
|
||||||
let rarePicked = false;
|
let rarePicked = false;
|
||||||
|
|
||||||
if (isMythic && currentPools.mythics.length > 0) {
|
if (isMythic && currentPools.mythics.length > 0) {
|
||||||
@@ -422,10 +459,11 @@ export class PackGeneratorService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback if Rare pool empty but Mythic not (or vice versa) handled by just skipping
|
// 4. Slot 7: Common / The List / Special Guest
|
||||||
|
// 1-87: 1 Common from Main Set.
|
||||||
// 4. Slot 7: Wildcard / The List
|
// 88-97: 1 Card from "The List" (Common/Uncommon reprint).
|
||||||
// 1-87: Common, 88-97: List (C/U), 98-99: List (R/M), 100: Special Guest
|
// 98-99: 1 Rare/Mythic from "The List".
|
||||||
|
// 100: 1 Special Guest (High Value).
|
||||||
const roll7 = Math.floor(Math.random() * 100) + 1;
|
const roll7 = Math.floor(Math.random() * 100) + 1;
|
||||||
let slot7Card: DraftCard | undefined;
|
let slot7Card: DraftCard | undefined;
|
||||||
|
|
||||||
@@ -434,36 +472,42 @@ export class PackGeneratorService {
|
|||||||
const res = this.drawUniqueCards(currentPools.commons, 1, namesInThisPack);
|
const res = this.drawUniqueCards(currentPools.commons, 1, namesInThisPack);
|
||||||
if (res.success) { slot7Card = res.selected[0]; currentPools.commons = res.remainingPool; }
|
if (res.success) { slot7Card = res.selected[0]; currentPools.commons = res.remainingPool; }
|
||||||
} else if (roll7 <= 97) {
|
} else if (roll7 <= 97) {
|
||||||
// "The List" (Common/Uncommon). Simulating by picking from C/U pools if "The List" is not explicit
|
// List (Common/Uncommon)
|
||||||
// For now, we mix C and U pools and pick one.
|
if (currentPools.specialGuests.length > 0) {
|
||||||
const listPool = [...currentPools.commons, ...currentPools.uncommons]; // Simplification
|
const res = this.drawUniqueCards(currentPools.specialGuests, 1, namesInThisPack);
|
||||||
if (listPool.length > 0) {
|
if (res.success) { slot7Card = res.selected[0]; currentPools.specialGuests = res.remainingPool; }
|
||||||
const rnd = Math.floor(Math.random() * listPool.length);
|
} else {
|
||||||
slot7Card = listPool[rnd];
|
const pool = Math.random() < 0.5 ? currentPools.commons : currentPools.uncommons;
|
||||||
// Remove from original pool not trivial here due to merge, let's use helpers
|
const res = this.drawUniqueCards(pool, 1, namesInThisPack);
|
||||||
// Better: Pick random type
|
if (res.success) {
|
||||||
const pickUncommon = Math.random() < 0.3; // Arbitrary weight
|
slot7Card = res.selected[0];
|
||||||
if (pickUncommon) {
|
if (pool === currentPools.commons) currentPools.commons = res.remainingPool;
|
||||||
const res = this.drawUniqueCards(currentPools.uncommons, 1, namesInThisPack);
|
else currentPools.uncommons = res.remainingPool;
|
||||||
if (res.success) { slot7Card = res.selected[0]; currentPools.uncommons = res.remainingPool; }
|
}
|
||||||
} else {
|
}
|
||||||
const res = this.drawUniqueCards(currentPools.commons, 1, namesInThisPack);
|
} else if (roll7 <= 99) {
|
||||||
if (res.success) { slot7Card = res.selected[0]; currentPools.commons = res.remainingPool; }
|
// List (Rare/Mythic)
|
||||||
|
if (currentPools.specialGuests.length > 0) {
|
||||||
|
const res = this.drawUniqueCards(currentPools.specialGuests, 1, namesInThisPack);
|
||||||
|
if (res.success) { slot7Card = res.selected[0]; currentPools.specialGuests = res.remainingPool; }
|
||||||
|
} else {
|
||||||
|
const pool = Math.random() < 0.125 ? currentPools.mythics : currentPools.rares;
|
||||||
|
const res = this.drawUniqueCards(pool, 1, namesInThisPack);
|
||||||
|
if (res.success) {
|
||||||
|
slot7Card = res.selected[0];
|
||||||
|
if (pool === currentPools.mythics) currentPools.mythics = res.remainingPool;
|
||||||
|
else currentPools.rares = res.remainingPool;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 98-100: Rare/Mythic/Special Guest
|
// 100: Special Guest
|
||||||
// Pick Rare or Mythic
|
if (currentPools.specialGuests.length > 0) {
|
||||||
// 98-99 (2%) vs 100 (1%) -> 2:1 ratio
|
const res = this.drawUniqueCards(currentPools.specialGuests, 1, namesInThisPack);
|
||||||
const isGuest = roll7 === 100;
|
if (res.success) { slot7Card = res.selected[0]; currentPools.specialGuests = res.remainingPool; }
|
||||||
const useMythic = isGuest || Math.random() < 0.2;
|
} else {
|
||||||
|
// Fallback Mythic
|
||||||
if (useMythic && currentPools.mythics.length > 0) {
|
|
||||||
const res = this.drawUniqueCards(currentPools.mythics, 1, namesInThisPack);
|
const res = this.drawUniqueCards(currentPools.mythics, 1, namesInThisPack);
|
||||||
if (res.success) { slot7Card = res.selected[0]; currentPools.mythics = res.remainingPool; }
|
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; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,7 +532,6 @@ export class PackGeneratorService {
|
|||||||
// Fallback: Pick a Common if no lands
|
// Fallback: Pick a Common if no lands
|
||||||
// const res = this.drawUniqueCards(currentPools.commons, 1, namesInThisPack);
|
// const res = this.drawUniqueCards(currentPools.commons, 1, namesInThisPack);
|
||||||
// if (res.success) { landCard = { ...res.selected[0] }; ... }
|
// if (res.success) { landCard = { ...res.selected[0] }; ... }
|
||||||
// Better to just have no land than a non-land
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (landCard) {
|
if (landCard) {
|
||||||
@@ -498,8 +541,7 @@ export class PackGeneratorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 6. Slot 13: Wildcard (Non-Foil)
|
// 6. Slot 13: Wildcard (Non-Foil)
|
||||||
// Weights: ~49% C, ~24% U, ~13% R, ~13% M => Sum=99.
|
// Weights: ~49% C, ~24% U, ~13% R, ~13% M
|
||||||
// Normalized: C:50, U:24, R:13, M:13
|
|
||||||
const drawWildcard = (foil: boolean) => {
|
const drawWildcard = (foil: boolean) => {
|
||||||
const wRoll = Math.random() * 100;
|
const wRoll = Math.random() * 100;
|
||||||
let wRarity = 'common';
|
let wRarity = 'common';
|
||||||
@@ -508,7 +550,6 @@ export class PackGeneratorService {
|
|||||||
else if (wRoll > 50) wRarity = 'uncommon';
|
else if (wRoll > 50) wRarity = 'uncommon';
|
||||||
else wRarity = 'common';
|
else wRarity = 'common';
|
||||||
|
|
||||||
// Adjust buckets
|
|
||||||
let poolToUse: DraftCard[] = [];
|
let poolToUse: DraftCard[] = [];
|
||||||
let updatePool = (_newPool: DraftCard[]) => { };
|
let updatePool = (_newPool: DraftCard[]) => { };
|
||||||
|
|
||||||
@@ -518,7 +559,6 @@ export class PackGeneratorService {
|
|||||||
else { poolToUse = currentPools.commons; updatePool = (p) => currentPools.commons = p; }
|
else { poolToUse = currentPools.commons; updatePool = (p) => currentPools.commons = p; }
|
||||||
|
|
||||||
if (poolToUse.length === 0) {
|
if (poolToUse.length === 0) {
|
||||||
// Fallback cascade
|
|
||||||
if (currentPools.commons.length > 0) { poolToUse = currentPools.commons; updatePool = (p) => currentPools.commons = p; }
|
if (currentPools.commons.length > 0) { poolToUse = currentPools.commons; updatePool = (p) => currentPools.commons = p; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,26 +581,15 @@ export class PackGeneratorService {
|
|||||||
|
|
||||||
// 8. Slot 15: Marketing / Token
|
// 8. Slot 15: Marketing / Token
|
||||||
if (currentPools.tokens.length > 0) {
|
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);
|
const res = this.drawUniqueCards(currentPools.tokens, 1, namesInThisPack);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
packCards.push(res.selected[0]);
|
packCards.push(res.selected[0]);
|
||||||
currentPools.tokens = res.remainingPool;
|
currentPools.tokens = res.remainingPool;
|
||||||
// Don't care about uniqueness for tokens as much, but let's stick to it
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort: Mythic -> Rare -> Uncommon -> Common -> Land -> Token
|
// 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'.
|
|
||||||
|
|
||||||
// Custom sort
|
|
||||||
const getWeight = (c: DraftCard) => {
|
const getWeight = (c: DraftCard) => {
|
||||||
if (c.layout === 'token' || c.typeLine?.includes('Token')) return 0;
|
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.typeLine?.includes('Land') && (c.rarity === 'common' || c.rarity === 'basic')) return 1;
|
||||||
|
|||||||
@@ -162,14 +162,16 @@ export class ScryfallService {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.data) {
|
if (data.data) {
|
||||||
return data.data.filter((s: any) =>
|
return data.data.filter((s: any) =>
|
||||||
['core', 'expansion', 'masters', 'draft_innovation'].includes(s.set_type)
|
['core', 'expansion', 'masters', 'draft_innovation', 'commander', 'funny', 'masterpiece', 'eternal'].includes(s.set_type)
|
||||||
).map((s: any) => ({
|
).map((s: any) => ({
|
||||||
code: s.code,
|
code: s.code,
|
||||||
name: s.name,
|
name: s.name,
|
||||||
set_type: s.set_type,
|
set_type: s.set_type,
|
||||||
released_at: s.released_at,
|
released_at: s.released_at,
|
||||||
icon_svg_uri: s.icon_svg_uri,
|
icon_svg_uri: s.icon_svg_uri,
|
||||||
digital: s.digital
|
digital: s.digital,
|
||||||
|
parent_set_code: s.parent_set_code,
|
||||||
|
card_count: s.card_count
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -178,7 +180,7 @@ export class ScryfallService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSetCards(setCode: string, onProgress?: (current: number) => void): Promise<ScryfallCard[]> {
|
async fetchSetCards(setCode: string, relatedSets: string[] = [], onProgress?: (current: number) => void): Promise<ScryfallCard[]> {
|
||||||
if (this.initPromise) await this.initPromise;
|
if (this.initPromise) await this.initPromise;
|
||||||
|
|
||||||
// Check if we already have a significant number of cards from this set in cache?
|
// Check if we already have a significant number of cards from this set in cache?
|
||||||
@@ -186,7 +188,9 @@ export class ScryfallService {
|
|||||||
// But for now, we just fetch and merge.
|
// But for now, we just fetch and merge.
|
||||||
|
|
||||||
let cards: ScryfallCard[] = [];
|
let cards: ScryfallCard[] = [];
|
||||||
let url = `https://api.scryfall.com/cards/search?q=set:${setCode}&unique=cards`;
|
const setClause = `e:${setCode}` + relatedSets.map(s => ` OR e:${s}`).join('');
|
||||||
|
// User requested pattern: (e:main or e:sub) and is:booster unique=prints
|
||||||
|
let url = `https://api.scryfall.com/cards/search?q=(${setClause}) unique=prints is:booster`;
|
||||||
|
|
||||||
while (url) {
|
while (url) {
|
||||||
try {
|
try {
|
||||||
@@ -228,4 +232,6 @@ export interface ScryfallSet {
|
|||||||
released_at: string;
|
released_at: string;
|
||||||
icon_svg_uri: string;
|
icon_svg_uri: string;
|
||||||
digital: boolean;
|
digital: boolean;
|
||||||
|
parent_set_code?: string;
|
||||||
|
card_count: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ app.get('/api/sets', async (_req: Request, res: Response) => {
|
|||||||
|
|
||||||
app.get('/api/sets/:code/cards', async (req: Request, res: Response) => {
|
app.get('/api/sets/:code/cards', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const cards = await scryfallService.fetchSetCards(req.params.code);
|
const related = req.query.related ? (req.query.related as string).split(',') : [];
|
||||||
|
const cards = await scryfallService.fetchSetCards(req.params.code, related);
|
||||||
|
|
||||||
// Implicitly cache images for these cards so local URLs work
|
// Implicitly cache images for these cards so local URLs work
|
||||||
if (cards.length > 0) {
|
if (cards.length > 0) {
|
||||||
@@ -218,7 +219,18 @@ app.post('/api/packs/generate', async (req: Request, res: Response) => {
|
|||||||
ignoreTokens: false
|
ignoreTokens: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const { pools, sets } = packGeneratorService.processCards(poolCards, activeFilters);
|
// Fetch metadata for merging subsets
|
||||||
|
const allSets = await scryfallService.fetchSets();
|
||||||
|
const setsMetadata: { [code: string]: { parent_set_code?: string } } = {};
|
||||||
|
if (allSets && Array.isArray(allSets)) {
|
||||||
|
allSets.forEach((s: any) => {
|
||||||
|
if (selectedSets && selectedSets.includes(s.code)) {
|
||||||
|
setsMetadata[s.code] = { parent_set_code: s.parent_set_code };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pools, sets } = packGeneratorService.processCards(poolCards, activeFilters, setsMetadata);
|
||||||
|
|
||||||
// Extract available basic lands for deck building
|
// Extract available basic lands for deck building
|
||||||
const basicLands = pools.lands.filter(c => c.typeLine?.includes('Basic'));
|
const basicLands = pools.lands.filter(c => c.typeLine?.includes('Basic'));
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export interface ProcessedPools {
|
|||||||
mythics: DraftCard[];
|
mythics: DraftCard[];
|
||||||
lands: DraftCard[];
|
lands: DraftCard[];
|
||||||
tokens: DraftCard[];
|
tokens: DraftCard[];
|
||||||
|
specialGuests: DraftCard[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetsMap {
|
export interface SetsMap {
|
||||||
@@ -46,6 +47,7 @@ export interface SetsMap {
|
|||||||
mythics: DraftCard[];
|
mythics: DraftCard[];
|
||||||
lands: DraftCard[];
|
lands: DraftCard[];
|
||||||
tokens: DraftCard[];
|
tokens: DraftCard[];
|
||||||
|
specialGuests: DraftCard[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,9 +59,9 @@ export interface PackGenerationSettings {
|
|||||||
|
|
||||||
export class PackGeneratorService {
|
export class PackGeneratorService {
|
||||||
|
|
||||||
processCards(cards: ScryfallCard[], filters: { ignoreBasicLands: boolean, ignoreCommander: boolean, ignoreTokens: boolean }): { pools: ProcessedPools, sets: SetsMap } {
|
processCards(cards: ScryfallCard[], filters: { ignoreBasicLands: boolean, ignoreCommander: boolean, ignoreTokens: boolean }, setsMetadata: { [code: string]: { parent_set_code?: string } } = {}): { pools: ProcessedPools, sets: SetsMap } {
|
||||||
console.time('processCards');
|
console.time('processCards');
|
||||||
const pools: ProcessedPools = { commons: [], uncommons: [], rares: [], mythics: [], lands: [], tokens: [] };
|
const pools: ProcessedPools = { commons: [], uncommons: [], rares: [], mythics: [], lands: [], tokens: [], specialGuests: [] };
|
||||||
const setsMap: SetsMap = {};
|
const setsMap: SetsMap = {};
|
||||||
|
|
||||||
let processedCount = 0;
|
let processedCount = 0;
|
||||||
@@ -118,10 +120,11 @@ export class PackGeneratorService {
|
|||||||
else if (rarity === 'uncommon') pools.uncommons.push(cardObj);
|
else if (rarity === 'uncommon') pools.uncommons.push(cardObj);
|
||||||
else if (rarity === 'rare') pools.rares.push(cardObj);
|
else if (rarity === 'rare') pools.rares.push(cardObj);
|
||||||
else if (rarity === 'mythic') pools.mythics.push(cardObj);
|
else if (rarity === 'mythic') pools.mythics.push(cardObj);
|
||||||
|
else pools.specialGuests.push(cardObj);
|
||||||
|
|
||||||
// Add to Sets Map
|
// Add to Sets Map
|
||||||
if (!setsMap[cardData.set]) {
|
if (!setsMap[cardData.set]) {
|
||||||
setsMap[cardData.set] = { name: cardData.set_name, code: cardData.set, commons: [], uncommons: [], rares: [], mythics: [], lands: [], tokens: [] };
|
setsMap[cardData.set] = { name: cardData.set_name, code: cardData.set, commons: [], uncommons: [], rares: [], mythics: [], lands: [], tokens: [], specialGuests: [] };
|
||||||
}
|
}
|
||||||
const setEntry = setsMap[cardData.set];
|
const setEntry = setsMap[cardData.set];
|
||||||
|
|
||||||
@@ -147,11 +150,38 @@ export class PackGeneratorService {
|
|||||||
else if (rarity === 'uncommon') { pools.uncommons.push(cardObj); setEntry.uncommons.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 === 'rare') { pools.rares.push(cardObj); setEntry.rares.push(cardObj); }
|
||||||
else if (rarity === 'mythic') { pools.mythics.push(cardObj); setEntry.mythics.push(cardObj); }
|
else if (rarity === 'mythic') { pools.mythics.push(cardObj); setEntry.mythics.push(cardObj); }
|
||||||
|
else { pools.specialGuests.push(cardObj); setEntry.specialGuests.push(cardObj); }
|
||||||
}
|
}
|
||||||
|
|
||||||
processedCount++;
|
processedCount++;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 2. Second Pass: Merge Subsets (Masterpieces) into Parents
|
||||||
|
Object.keys(setsMap).forEach(setCode => {
|
||||||
|
const meta = setsMetadata[setCode];
|
||||||
|
if (meta && meta.parent_set_code) {
|
||||||
|
const parentCode = meta.parent_set_code;
|
||||||
|
if (setsMap[parentCode]) {
|
||||||
|
const parentSet = setsMap[parentCode];
|
||||||
|
const childSet = setsMap[setCode];
|
||||||
|
|
||||||
|
const allChildCards = [
|
||||||
|
...childSet.commons,
|
||||||
|
...childSet.uncommons,
|
||||||
|
...childSet.rares,
|
||||||
|
...childSet.mythics,
|
||||||
|
...childSet.specialGuests
|
||||||
|
];
|
||||||
|
|
||||||
|
parentSet.specialGuests.push(...allChildCards);
|
||||||
|
pools.specialGuests.push(...allChildCards);
|
||||||
|
|
||||||
|
// Remove child set from map so we don't generate separate packs for it
|
||||||
|
delete setsMap[setCode];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`[PackGenerator] Processed ${processedCount} cards.`);
|
console.log(`[PackGenerator] Processed ${processedCount} cards.`);
|
||||||
console.timeEnd('processCards');
|
console.timeEnd('processCards');
|
||||||
return { pools, sets: setsMap };
|
return { pools, sets: setsMap };
|
||||||
@@ -176,7 +206,8 @@ export class PackGeneratorService {
|
|||||||
rares: this.shuffle([...pools.rares]),
|
rares: this.shuffle([...pools.rares]),
|
||||||
mythics: this.shuffle([...pools.mythics]),
|
mythics: this.shuffle([...pools.mythics]),
|
||||||
lands: this.shuffle([...pools.lands]),
|
lands: this.shuffle([...pools.lands]),
|
||||||
tokens: this.shuffle([...pools.tokens])
|
tokens: this.shuffle([...pools.tokens]),
|
||||||
|
specialGuests: this.shuffle([...pools.specialGuests])
|
||||||
};
|
};
|
||||||
|
|
||||||
// Log pool sizes
|
// Log pool sizes
|
||||||
@@ -197,7 +228,8 @@ export class PackGeneratorService {
|
|||||||
rares: this.shuffle([...pools.rares]),
|
rares: this.shuffle([...pools.rares]),
|
||||||
mythics: this.shuffle([...pools.mythics]),
|
mythics: this.shuffle([...pools.mythics]),
|
||||||
lands: this.shuffle([...pools.lands]),
|
lands: this.shuffle([...pools.lands]),
|
||||||
tokens: this.shuffle([...pools.tokens])
|
tokens: this.shuffle([...pools.tokens]),
|
||||||
|
specialGuests: this.shuffle([...pools.specialGuests])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +288,8 @@ export class PackGeneratorService {
|
|||||||
rares: this.shuffle([...data.rares]),
|
rares: this.shuffle([...data.rares]),
|
||||||
mythics: this.shuffle([...data.mythics]),
|
mythics: this.shuffle([...data.mythics]),
|
||||||
lands: this.shuffle([...data.lands]),
|
lands: this.shuffle([...data.lands]),
|
||||||
tokens: this.shuffle([...data.tokens])
|
tokens: this.shuffle([...data.tokens]),
|
||||||
|
specialGuests: this.shuffle([...data.specialGuests])
|
||||||
};
|
};
|
||||||
|
|
||||||
let packsGeneratedForSet = 0;
|
let packsGeneratedForSet = 0;
|
||||||
@@ -276,7 +309,8 @@ export class PackGeneratorService {
|
|||||||
rares: this.shuffle([...data.rares]),
|
rares: this.shuffle([...data.rares]),
|
||||||
mythics: this.shuffle([...data.mythics]),
|
mythics: this.shuffle([...data.mythics]),
|
||||||
lands: this.shuffle([...data.lands]),
|
lands: this.shuffle([...data.lands]),
|
||||||
tokens: this.shuffle([...data.tokens])
|
tokens: this.shuffle([...data.tokens]),
|
||||||
|
specialGuests: this.shuffle([...data.specialGuests])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,8 +339,7 @@ export class PackGeneratorService {
|
|||||||
const packCards: DraftCard[] = [];
|
const packCards: DraftCard[] = [];
|
||||||
const namesInPack = new Set<string>();
|
const namesInPack = new Set<string>();
|
||||||
|
|
||||||
// Standard: 14 cards exactly. Peasant: 13 cards exactly.
|
const targetSize = 14;
|
||||||
const targetSize = rarityMode === 'peasant' ? 13 : 14;
|
|
||||||
|
|
||||||
// Helper to abstract draw logic
|
// Helper to abstract draw logic
|
||||||
const draw = (pool: DraftCard[], count: number, poolKey: keyof ProcessedPools) => {
|
const draw = (pool: DraftCard[], count: number, poolKey: keyof ProcessedPools) => {
|
||||||
@@ -322,90 +355,183 @@ export class PackGeneratorService {
|
|||||||
return result.selected;
|
return result.selected;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Commons (6)
|
if (rarityMode === 'peasant') {
|
||||||
draw(pools.commons, 6, 'commons');
|
// 1. Commons (6) - Color Balanced
|
||||||
|
// Using drawColorBalanced helper
|
||||||
|
const drawC = this.drawColorBalanced(pools.commons, 6, namesInPack, withReplacement);
|
||||||
|
if (drawC.selected.length > 0) {
|
||||||
|
packCards.push(...drawC.selected);
|
||||||
|
if (!withReplacement) {
|
||||||
|
pools.commons = drawC.remainingPool;
|
||||||
|
drawC.selected.forEach(c => namesInPack.add(c.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Slot 7: Common / The List
|
||||||
|
// 1-87: Common
|
||||||
|
// 88-97: List (C/U)
|
||||||
|
// 98-100: List (U)
|
||||||
|
const roll7 = Math.floor(Math.random() * 100) + 1;
|
||||||
|
const hasGuests = pools.specialGuests.length > 0;
|
||||||
|
|
||||||
|
if (roll7 <= 87) {
|
||||||
|
draw(pools.commons, 1, 'commons');
|
||||||
|
} else if (roll7 <= 97) {
|
||||||
|
// List (C/U) - Fallback logic
|
||||||
|
if (hasGuests) draw(pools.specialGuests, 1, 'specialGuests');
|
||||||
|
else {
|
||||||
|
// 50/50 fallback
|
||||||
|
const useU = Math.random() < 0.5;
|
||||||
|
if (useU) draw(pools.uncommons, 1, 'uncommons');
|
||||||
|
else draw(pools.commons, 1, 'commons');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 98-100: List (U)
|
||||||
|
if (hasGuests) draw(pools.specialGuests, 1, 'specialGuests');
|
||||||
|
else draw(pools.uncommons, 1, 'uncommons');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Uncommons (4)
|
||||||
|
draw(pools.uncommons, 4, 'uncommons');
|
||||||
|
|
||||||
|
// 4. Land (Slot 12)
|
||||||
|
const isFoilLand = Math.random() < 0.2;
|
||||||
|
const landPicks = draw(pools.lands, 1, 'lands');
|
||||||
|
if (landPicks.length > 0 && isFoilLand) {
|
||||||
|
const idx = packCards.indexOf(landPicks[0]);
|
||||||
|
if (idx !== -1) {
|
||||||
|
packCards[idx] = { ...packCards[idx], finish: 'foil' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Wildcards (Slot 13 & 14)
|
||||||
|
// Peasant weights: ~62% Common, ~37% Uncommon
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
const isFoil = i === 1;
|
||||||
|
const wRoll = Math.random() * 100;
|
||||||
|
let targetKey: keyof ProcessedPools = 'commons';
|
||||||
|
|
||||||
|
// 1-62: Common, 63-100: Uncommon (Approx > 62)
|
||||||
|
if (wRoll > 62) targetKey = 'uncommons';
|
||||||
|
else targetKey = 'commons';
|
||||||
|
|
||||||
|
let pool = pools[targetKey];
|
||||||
|
if (pool.length === 0) {
|
||||||
|
// Fallback
|
||||||
|
targetKey = 'commons';
|
||||||
|
pool = pools.commons;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = this.drawCards(pool, 1, namesInPack, withReplacement);
|
||||||
|
if (res.selected.length > 0) {
|
||||||
|
const card = { ...res.selected[0] };
|
||||||
|
if (isFoil) card.finish = 'foil';
|
||||||
|
packCards.push(card);
|
||||||
|
if (!withReplacement) {
|
||||||
|
// @ts-ignore
|
||||||
|
pools[targetKey] = res.remainingPool;
|
||||||
|
namesInPack.add(card.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Slot 7 (Common or List)
|
|
||||||
const roll7 = Math.random() * 100;
|
|
||||||
if (roll7 < 87) {
|
|
||||||
// Common
|
|
||||||
draw(pools.commons, 1, 'commons');
|
|
||||||
} else {
|
} else {
|
||||||
// Uncommon/List
|
// STANDARD MODE
|
||||||
// If pool empty, try fallback if standard? No, strict as per previous instruction.
|
|
||||||
draw(pools.uncommons, 1, 'uncommons');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Uncommons (3 or 4 dependent on PEASANT vs STANDARD)
|
// 1. Commons (6)
|
||||||
const uNeeded = rarityMode === 'peasant' ? 4 : 3;
|
const drawC = this.drawColorBalanced(pools.commons, 6, namesInPack, withReplacement);
|
||||||
draw(pools.uncommons, uNeeded, 'uncommons');
|
if (drawC.selected.length > 0) {
|
||||||
|
packCards.push(...drawC.selected);
|
||||||
|
if (!withReplacement) {
|
||||||
|
pools.commons = drawC.remainingPool;
|
||||||
|
drawC.selected.forEach(c => namesInPack.add(c.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Rare/Mythic (Standard Only)
|
// 2. Slot 7 (Common / List / Guest)
|
||||||
if (rarityMode === 'standard') {
|
// 1-87: Common
|
||||||
|
// 88-97: List (C/U)
|
||||||
|
// 98-99: List (R/M)
|
||||||
|
// 100: Special Guest
|
||||||
|
const roll7 = Math.floor(Math.random() * 100) + 1; // 1-100
|
||||||
|
const hasGuests = pools.specialGuests.length > 0;
|
||||||
|
|
||||||
|
if (roll7 <= 87) {
|
||||||
|
draw(pools.commons, 1, 'commons');
|
||||||
|
} else if (roll7 <= 97) {
|
||||||
|
// List C/U
|
||||||
|
if (hasGuests) draw(pools.specialGuests, 1, 'specialGuests');
|
||||||
|
else {
|
||||||
|
if (Math.random() < 0.5) draw(pools.uncommons, 1, 'uncommons');
|
||||||
|
else draw(pools.commons, 1, 'commons');
|
||||||
|
}
|
||||||
|
} else if (roll7 <= 99) {
|
||||||
|
// List R/M
|
||||||
|
if (hasGuests) draw(pools.specialGuests, 1, 'specialGuests');
|
||||||
|
else {
|
||||||
|
if (Math.random() < 0.5) draw(pools.mythics, 1, 'mythics');
|
||||||
|
else draw(pools.rares, 1, 'rares');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 100: Special Guest
|
||||||
|
if (hasGuests) draw(pools.specialGuests, 1, 'specialGuests');
|
||||||
|
else draw(pools.mythics, 1, 'mythics'); // Fallback to Mythic
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Uncommons (3)
|
||||||
|
draw(pools.uncommons, 3, 'uncommons');
|
||||||
|
|
||||||
|
// 4. Main Rare/Mythic (Slot 11)
|
||||||
const isMythic = Math.random() < 0.125;
|
const isMythic = Math.random() < 0.125;
|
||||||
let pickedR = false;
|
let pickedR = false;
|
||||||
|
|
||||||
if (isMythic && pools.mythics.length > 0) {
|
if (isMythic && pools.mythics.length > 0) {
|
||||||
const sel = draw(pools.mythics, 1, 'mythics');
|
const sel = draw(pools.mythics, 1, 'mythics');
|
||||||
if (sel.length) pickedR = true;
|
if (sel.length) pickedR = true;
|
||||||
}
|
}
|
||||||
|
if (!pickedR) {
|
||||||
if (!pickedR && pools.rares.length > 0) {
|
|
||||||
draw(pools.rares, 1, 'rares');
|
draw(pools.rares, 1, 'rares');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Land
|
// 5. Land (Slot 12)
|
||||||
const isFoilLand = Math.random() < 0.2;
|
const isFoilLand = Math.random() < 0.2;
|
||||||
if (pools.lands.length > 0) {
|
const landPicks = draw(pools.lands, 1, 'lands');
|
||||||
// For lands, we generally want random basic lands anyway even in finite cubes if possible?
|
if (landPicks.length > 0 && isFoilLand) {
|
||||||
// But adhering to 'withReplacement' logic strictly.
|
const idx = packCards.indexOf(landPicks[0]);
|
||||||
const res = this.drawCards(pools.lands, 1, namesInPack, withReplacement);
|
if (idx !== -1) {
|
||||||
if (res.selected.length) {
|
packCards[idx] = { ...packCards[idx], finish: 'foil' };
|
||||||
const l = { ...res.selected[0] };
|
|
||||||
if (isFoilLand) l.finish = 'foil';
|
|
||||||
packCards.push(l);
|
|
||||||
if (!withReplacement) {
|
|
||||||
pools.lands = res.remainingPool;
|
|
||||||
namesInPack.add(l.name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Wildcards (2 slots) + Foil Wildcard
|
// 6. Wildcards (Slot 13 & 14)
|
||||||
for (let i = 0; i < 2; i++) {
|
// Standard weights: ~49% C, ~24% U, ~13% R, ~13% M
|
||||||
const isFoil = i === 1; // 2nd is foil
|
for (let i = 0; i < 2; i++) {
|
||||||
const wRoll = Math.random() * 100;
|
const isFoil = i === 1;
|
||||||
let targetPool = pools.commons;
|
const wRoll = Math.random() * 100;
|
||||||
let targetKey: keyof ProcessedPools = 'commons';
|
let targetKey: keyof ProcessedPools = 'commons';
|
||||||
|
|
||||||
if (rarityMode === 'peasant') {
|
if (wRoll > 87) targetKey = 'mythics';
|
||||||
if (wRoll > 60) { targetPool = pools.uncommons; targetKey = 'uncommons'; }
|
else if (wRoll > 74) targetKey = 'rares';
|
||||||
else { targetPool = pools.commons; targetKey = 'commons'; }
|
else if (wRoll > 50) targetKey = 'uncommons';
|
||||||
} else {
|
|
||||||
if (wRoll > 87) { targetPool = pools.mythics; targetKey = 'mythics'; }
|
|
||||||
else if (wRoll > 74) { targetPool = pools.rares; targetKey = 'rares'; }
|
|
||||||
else if (wRoll > 50) { targetPool = pools.uncommons; targetKey = 'uncommons'; }
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = this.drawCards(targetPool, 1, namesInPack, withReplacement);
|
let pool = pools[targetKey];
|
||||||
|
// Hierarchical fallback
|
||||||
|
if (pool.length === 0) {
|
||||||
|
if (targetKey === 'mythics' && pools.rares.length) targetKey = 'rares';
|
||||||
|
if ((targetKey === 'rares' || targetKey === 'mythics') && pools.uncommons.length) targetKey = 'uncommons';
|
||||||
|
if (targetKey !== 'commons' && pools.commons.length) targetKey = 'commons';
|
||||||
|
pool = pools[targetKey];
|
||||||
|
}
|
||||||
|
|
||||||
// FALLBACK LOGIC for Wildcards (Standard Only mostly)
|
const res = this.drawCards(pool, 1, namesInPack, withReplacement);
|
||||||
// If we failed to get a card from target pool (e.g. rolled Mythic but set has none), try lower rarity
|
if (res.selected.length > 0) {
|
||||||
if (!res.success && rarityMode === 'standard') {
|
const card = { ...res.selected[0] };
|
||||||
if (targetKey === 'mythics' && pools.rares.length) { res = this.drawCards(pools.rares, 1, namesInPack, withReplacement); targetKey = 'rares'; }
|
if (isFoil) card.finish = 'foil';
|
||||||
else if (targetKey === 'rares' && pools.uncommons.length) { res = this.drawCards(pools.uncommons, 1, namesInPack, withReplacement); targetKey = 'uncommons'; }
|
packCards.push(card);
|
||||||
else if (targetKey === 'uncommons' && pools.commons.length) { res = this.drawCards(pools.commons, 1, namesInPack, withReplacement); targetKey = 'commons'; }
|
if (!withReplacement) {
|
||||||
}
|
// @ts-ignore
|
||||||
|
pools[targetKey] = res.remainingPool;
|
||||||
if (res.selected.length) {
|
namesInPack.add(card.name);
|
||||||
const c = { ...res.selected[0] };
|
}
|
||||||
if (isFoil) c.finish = 'foil';
|
|
||||||
packCards.push(c);
|
|
||||||
if (!withReplacement) {
|
|
||||||
// @ts-ignore
|
|
||||||
pools[targetKey] = res.remainingPool;
|
|
||||||
namesInPack.add(c.name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -428,21 +554,21 @@ export class PackGeneratorService {
|
|||||||
|
|
||||||
packCards.sort((a, b) => getWeight(b) - getWeight(a));
|
packCards.sort((a, b) => getWeight(b) - getWeight(a));
|
||||||
|
|
||||||
// ENFORCE SIZE STRICTLY
|
if (packCards.length < targetSize) {
|
||||||
const finalCards = packCards.slice(0, targetSize);
|
|
||||||
|
|
||||||
// Strict Validation
|
|
||||||
if (finalCards.length < targetSize) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: packId,
|
id: packId,
|
||||||
setName: setName,
|
setName: setName,
|
||||||
cards: finalCards
|
cards: packCards
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private drawColorBalanced(pool: DraftCard[], count: number, existingNames: Set<string>, withReplacement: boolean) {
|
||||||
|
return this.drawCards(pool, count, existingNames, withReplacement);
|
||||||
|
}
|
||||||
|
|
||||||
// Unified Draw Method
|
// Unified Draw Method
|
||||||
private drawCards(pool: DraftCard[], count: number, existingNames: Set<string>, withReplacement: boolean) {
|
private drawCards(pool: DraftCard[], count: number, existingNames: Set<string>, withReplacement: boolean) {
|
||||||
if (pool.length === 0) return { selected: [], remainingPool: pool, success: false };
|
if (pool.length === 0) return { selected: [], remainingPool: pool, success: false };
|
||||||
|
|||||||
@@ -193,13 +193,15 @@ export class ScryfallService {
|
|||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
|
|
||||||
const sets = data.data
|
const sets = data.data
|
||||||
.filter((s: any) => ['core', 'expansion', 'masters', 'draft_innovation', 'commander', 'funny'].includes(s.set_type))
|
.filter((s: any) => ['core', 'expansion', 'masters', 'draft_innovation', 'commander', 'funny', 'masterpiece', 'eternal'].includes(s.set_type))
|
||||||
.map((s: any) => ({
|
.map((s: any) => ({
|
||||||
code: s.code,
|
code: s.code,
|
||||||
name: s.name,
|
name: s.name,
|
||||||
set_type: s.set_type,
|
set_type: s.set_type,
|
||||||
released_at: s.released_at,
|
released_at: s.released_at,
|
||||||
digital: s.digital
|
digital: s.digital,
|
||||||
|
parent_set_code: s.parent_set_code,
|
||||||
|
card_count: s.card_count
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return sets;
|
return sets;
|
||||||
@@ -209,7 +211,7 @@ export class ScryfallService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSetCards(setCode: string): Promise<ScryfallCard[]> {
|
async fetchSetCards(setCode: string, relatedSets: string[] = []): Promise<ScryfallCard[]> {
|
||||||
const setHash = setCode.toLowerCase();
|
const setHash = setCode.toLowerCase();
|
||||||
const setCachePath = path.join(SETS_DIR, `${setHash}.json`);
|
const setCachePath = path.join(SETS_DIR, `${setHash}.json`);
|
||||||
|
|
||||||
@@ -226,26 +228,30 @@ export class ScryfallService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[ScryfallService] Fetching cards for set ${setCode} from API...`);
|
console.log(`[ScryfallService] Fetching cards for set ${setCode} (related: ${relatedSets.join(',')}) from API...`);
|
||||||
let allCards: ScryfallCard[] = [];
|
let allCards: ScryfallCard[] = [];
|
||||||
let url = `https://api.scryfall.com/cards/search?q=set:${setCode}&unique=cards`;
|
|
||||||
|
// Construct Composite Query: (e:main OR e:sub1 OR e:sub2) is:booster unique=prints
|
||||||
|
const setClause = `e:${setCode}` + relatedSets.map(s => ` OR e:${s}`).join('');
|
||||||
|
let url = `https://api.scryfall.com/cards/search?q=(${setClause}) unique=prints is:booster`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (url) {
|
while (url) {
|
||||||
console.log(`[ScryfallService] Requesting: ${url}`);
|
console.log(`[ScryfallService] [API CALL] Requesting: ${url}`);
|
||||||
const r = await fetch(url);
|
const resp = await fetch(url);
|
||||||
if (!r.ok) {
|
console.log(`[ScryfallService] [API RESPONSE] Status: ${resp.status}`);
|
||||||
if (r.status === 404) {
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
if (resp.status === 404) {
|
||||||
console.warn(`[ScryfallService] 404 Not Found for URL: ${url}. Assuming set has no cards.`);
|
console.warn(`[ScryfallService] 404 Not Found for URL: ${url}. Assuming set has no cards.`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
const errBody = await resp.text();
|
||||||
const errBody = await r.text();
|
console.error(`[ScryfallService] Error fetching ${url}: ${resp.status} ${resp.statusText}`, errBody);
|
||||||
console.error(`[ScryfallService] Error fetching ${url}: ${r.status} ${r.statusText}`, errBody);
|
throw new Error(`Failed to fetch set: ${resp.statusText} (${resp.status}) - ${errBody}`);
|
||||||
throw new Error(`Failed to fetch set: ${r.statusText} (${r.status}) - ${errBody}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const d = await r.json();
|
const d = await resp.json();
|
||||||
|
|
||||||
if (d.data) {
|
if (d.data) {
|
||||||
allCards.push(...d.data);
|
allCards.push(...d.data);
|
||||||
|
|||||||
Reference in New Issue
Block a user