diff --git a/docs/development/CENTRAL.md b/docs/development/CENTRAL.md index 16ba368..885a7ff 100644 --- a/docs/development/CENTRAL.md +++ b/docs/development/CENTRAL.md @@ -6,6 +6,7 @@ The project has successfully migrated from a .NET backend to a Node.js Modular M ## Recent Updates - **[2025-12-14] Core Implementation**: Refactored `gemini-generated.js` into modular services and components. Implemented Cube Manager and Tournament Manager. [Link](./devlog/2025-12-14-194558_core_implementation.md) - **[2025-12-14] Parser Robustness**: Improving `CardParserService` to handle formats without Scryfall IDs (e.g., Arena exports). [Link](./devlog/2025-12-14-210000_fix_parser_robustness.md) +- **[2025-12-14] Set Generation**: Implemented full set fetching and booster box generation (Completed). [Link](./devlog/2025-12-14-211000_set_based_generation.md) ## Active Modules 1. **Cube Manager**: Fully functional (Parsing, Fetching, Pack Generation). diff --git a/docs/development/devlog/2025-12-14-211000_set_based_generation.md b/docs/development/devlog/2025-12-14-211000_set_based_generation.md new file mode 100644 index 0000000..aabed6d --- /dev/null +++ b/docs/development/devlog/2025-12-14-211000_set_based_generation.md @@ -0,0 +1,26 @@ +# Enhancement: Set-Based Pack Generation + +## Status: Completed + +## Summary +Implemented the ability to fetch entire sets from Scryfall and generate booster boxes. + +## Changes +1. **ScryfallService**: + * Added `fetchSets()` to retrieve expansion sets. + * Added `fetchSetCards(setCode)` to retrieve all cards from a set. +2. **PackGeneratorService**: + * Added `generateBoosterBox()` to generate packs without depleting the pool. + * Added `buildTokenizedPack()` for probabilistic generation (R/M + 3U + 10C). +3. **CubeManager UI**: + * Added Toggle for "Custom List" vs "From Expansion". + * Added Set Selection Dropdown. + * Added "Number of Boxes" input. + * Integrated new service methods. + +## Usage +1. Select "From Expansion" tab. +2. Choose a set (e.g., "Vintage Masters"). +3. Choose number of boxes (default 3). +4. Click "Fetch Set". +5. Click "Generate Packs". diff --git a/src/client/src/modules/cube/CubeManager.tsx b/src/client/src/modules/cube/CubeManager.tsx index 9e57e8f..e2ab95f 100644 --- a/src/client/src/modules/cube/CubeManager.tsx +++ b/src/client/src/modules/cube/CubeManager.tsx @@ -1,7 +1,7 @@ import React, { useState, useRef, useEffect } from 'react'; import { Layers, RotateCcw, Box, Check, Loader2, Upload, Eye, EyeOff, LayoutGrid, List, Sliders, Settings } from 'lucide-react'; import { CardParserService } from '../../services/CardParserService'; -import { ScryfallService, ScryfallCard } from '../../services/ScryfallService'; +import { ScryfallService, ScryfallCard, ScryfallSet } from '../../services/ScryfallService'; import { PackGeneratorService, ProcessedPools, SetsMap, Pack, PackGenerationSettings } from '../../services/PackGeneratorService'; import { PackCard } from '../../components/PackCard'; import { TournamentPackView } from '../../components/TournamentPackView'; @@ -40,6 +40,11 @@ export const CubeManager: React.FC = () => { rarityMode: 'peasant' }); + const [sourceMode, setSourceMode] = useState<'upload' | 'set'>('upload'); + const [availableSets, setAvailableSets] = useState([]); + const [selectedSet, setSelectedSet] = useState(''); + const [numBoxes, setNumBoxes] = useState(3); + const fileInputRef = useRef(null); // --- Effects --- @@ -50,6 +55,12 @@ export const CubeManager: React.FC = () => { } }, [filters, rawScryfallData]); + useEffect(() => { + scryfallService.fetchSets().then(sets => { + setAvailableSets(sets.sort((a, b) => new Date(b.released_at).getTime() - new Date(a.released_at).getTime())); + }); + }, [scryfallService]); + // --- Handlers --- const handleFileUpload = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; @@ -94,35 +105,35 @@ export const CubeManager: React.FC = () => { const fetchAndParse = async () => { setLoading(true); setPacks([]); - setProgress('Parsing text...'); + setProgress(sourceMode === 'set' ? 'Fetching set data...' : 'Parsing text...'); setTournamentMode(false); try { - const identifiers = parserService.parse(inputText); + let expandedCards: ScryfallCard[] = []; - // Expand quantity for fetching logic (though service handles it, let's just pass uniques to service) - // The service `fetchCollection` deduplicates automatically. + if (sourceMode === 'set') { + if (!selectedSet) throw new Error("Please select a set."); + const cards = await scryfallService.fetchSetCards(selectedSet, (count) => { + setProgress(`Fetching set cards... (${count})`); + }); + expandedCards = cards; + } else { + const identifiers = parserService.parse(inputText); + const fetchList = identifiers.map(id => id.type === 'id' ? { id: id.value } : { name: id.value }); - // Map identifier interface - const fetchList = identifiers.map(id => id.type === 'id' ? { id: id.value } : { name: id.value }); + await scryfallService.fetchCollection(fetchList, (current, total) => { + setProgress(`Fetching Scryfall data... (${current}/${total})`); + }); - // Fetch - await scryfallService.fetchCollection(fetchList, (current, total) => { - setProgress(`Fetching Scryfall data... (${current}/${total})`); - }); - - // Expand based on original quantities - const expandedCards: ScryfallCard[] = []; - identifiers.forEach(id => { - const card = scryfallService.getCachedCard(id.type === 'id' ? { id: id.value } : { name: id.value }); - if (card) { - for (let i = 0; i < id.quantity; i++) expandedCards.push(card); - } - }); + identifiers.forEach(id => { + const card = scryfallService.getCachedCard(id.type === 'id' ? { id: id.value } : { name: id.value }); + if (card) { + for (let i = 0; i < id.quantity; i++) expandedCards.push(card); + } + }); + } setRawScryfallData(expandedCards); - // Processing happens via useEffect - setLoading(false); setProgress(''); @@ -136,20 +147,21 @@ export const CubeManager: React.FC = () => { const generatePacks = () => { if (!processedData) return; - if (processedData.pools.commons.length === 0 && processedData.pools.uncommons.length === 0 && processedData.pools.rares.length === 0) { - alert(`Not enough valid cards.`); - return; - } - setLoading(true); // Use setTimeout to allow UI to show loading spinner before sync calculation blocks setTimeout(() => { try { - const newPacks = generatorService.generatePacks(processedData.pools, processedData.sets, genSettings); + let newPacks: Pack[] = []; + if (sourceMode === 'set') { + const totalPacks = numBoxes * 36; + newPacks = generatorService.generateBoosterBox(processedData.pools, totalPacks, genSettings); + } else { + newPacks = generatorService.generatePacks(processedData.pools, processedData.sets, genSettings); + } if (newPacks.length === 0) { - alert(`Not enough cards to generate valid packs (minimum required for selected mode).`); + alert(`Not enough cards to generate valid packs.`); } else { setPacks(newPacks); setTournamentMode(false); @@ -173,55 +185,118 @@ export const CubeManager: React.FC = () => { {/* --- LEFT COLUMN: CONTROLS --- */}
-
- -
- + +
+ + {sourceMode === 'upload' ? ( + <> +
+ +
+ + + +
+
+ + {/* Filters */} +
+

+ Import Options +

+
+ + + +
+
+ +