Compare commits
2 Commits
ea24b5a206
...
552eba5ba7
| Author | SHA1 | Date | |
|---|---|---|---|
| 552eba5ba7 | |||
| faa79906a8 |
@@ -29,3 +29,6 @@
|
||||
- [Bulk Parse Feedback](./devlog/2025-12-16-231500_bulk_parse_feedback.md): Completed. Updated `CubeManager` to handle metadata generation properly and provide feedback on missing cards.
|
||||
- [Full Metadata Passthrough](./devlog/2025-12-16-234500_full_metadata_passthrough.md): Completed. `DraftCard` now includes a `definition` property containing the complete, raw Scryfall object for future-proofing and advanced algorithm usage.
|
||||
- [Server-Side Caching](./devlog/2025-12-16-235900_server_side_caching.md): Completed. Implemented logic to cache images and metadata on the server upon bulk parsing, and updated client to use local assets.
|
||||
- [Peasant Algorithm Implementation](./devlog/2025-12-16-225700_peasant_algorithm.md): Completed. Implemented Peasant-specific pack generation rules including slot logic for commons, uncommons, lands, and wildcards.
|
||||
- [Multi-Expansion Selection](./devlog/2025-12-16-230500_multi_expansion_selection.md): Completed. Implemented searchable multi-select interface for "From Expansion" pack generation, allowing mixed-set drafts.
|
||||
- [Game Type Filter](./devlog/2025-12-16-231000_game_type_filter.md): Completed. Added Paper/Digital filter to the expansion selection list.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Peasant Algorithm Implementation
|
||||
|
||||
## Overview
|
||||
Implemented the detailed "Peasant" pack generation algorithm in `PackGeneratorService.ts`.
|
||||
|
||||
## Changes
|
||||
- Updated `buildSinglePack` in `PackGeneratorService.ts` to include specific logic for Peasant rarity mode.
|
||||
- Implemented slot-based generation:
|
||||
- Slots 1-6: Commons (Color Balanced)
|
||||
- Slot 7: Common or "The List" (Simulated)
|
||||
- Slots 8-11: Uncommons
|
||||
- Slot 12: Land (20% Foil)
|
||||
- Slot 13: Non-Foil Wildcard (Weighted by rarity)
|
||||
- Slot 14: Foil Wildcard (Weighted by rarity)
|
||||
- Slot 15: Marketing Token
|
||||
|
||||
## Notes
|
||||
- Used existing helper methods `drawColorBalanced` and `drawUniqueCards`.
|
||||
- Simulated "The List" logic using available Common/Uncommon pools as exact "The List" metadata might not be available in standard pools provided to the generator.
|
||||
- Wildcard weights follow the specification (~49% C, ~24% U, ~13% R, ~13% M).
|
||||
@@ -0,0 +1,14 @@
|
||||
# Multi-Expansion Selection
|
||||
|
||||
## Objective
|
||||
Enhanced the "From Expansion" pack generation feature in the Cube Manager to allow users to select multiple expansions and use a searchable interface.
|
||||
|
||||
## Implementation Details
|
||||
1. **Searchable Interface**: Replaced the simple set dropdown with a dedicated set selection UI featuring a search input for fuzzy filtering by set name or code.
|
||||
2. **Multi-Select Capability**: Users can now check multiple sets from the filtered list.
|
||||
3. **Frontend State Refactor**: Migrated `selectedSet` (string) to `selectedSets` (string array) in `CubeManager.tsx`.
|
||||
4. **Fetch Logic Update**: Updated `fetchAndParse` to iterate through all selected sets, fetching card data for each sequentially and combining the results into the parse pool.
|
||||
5. **Generation Logic**: The existing `generateBoosterBox` logic now naturally consumes the combined pool of cards from multiple sets, effectively allowing for "Chaos Drafts" or custom mixed-set environments based on the user's selection.
|
||||
|
||||
## Status
|
||||
Completed. The Cube Manager UI now supports advanced set selection scenarios.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Game Type Filter for Expansion Selection
|
||||
|
||||
## Objective
|
||||
Add a filter to the "From Expansion" set selection to easily distinguish between Paper and Digital (MTGA/MTGO) sets.
|
||||
|
||||
## Implementation Details
|
||||
1. **ScryfallService Update**: Updated `ScryfallSet` interface to include the `digital` boolean property and mapped it in `fetchSets`.
|
||||
2. **CubeManager UI**: Added a toggle filter bar above the set list with three options:
|
||||
* **All**: Shows all sets.
|
||||
* **Paper**: Shows only sets where `digital` is false.
|
||||
* **Digital**: Shows only sets where `digital` is true.
|
||||
3. **Filter Logic**: Integrated the game type filter into the existing search filter logic in `CubeManager`.
|
||||
|
||||
## Status
|
||||
Completed. Users can now filter the expansion list by game type.
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Layers, RotateCcw, Box, Check, Loader2, Upload, LayoutGrid, List, Sliders, Settings, Users, Download, Copy, FileDown, Trash2 } from 'lucide-react';
|
||||
import { Layers, RotateCcw, Box, Check, Loader2, Upload, LayoutGrid, List, Sliders, Settings, Users, Download, Copy, FileDown, Trash2, Search, X } from 'lucide-react';
|
||||
import { CardParserService } from '../../services/CardParserService';
|
||||
import { ScryfallService, ScryfallCard, ScryfallSet } from '../../services/ScryfallService';
|
||||
import { PackGeneratorService, ProcessedPools, SetsMap, Pack, PackGenerationSettings } from '../../services/PackGeneratorService';
|
||||
@@ -92,7 +92,13 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
|
||||
(localStorage.getItem('cube_sourceMode') as 'upload' | 'set') || 'upload'
|
||||
);
|
||||
const [availableSets, setAvailableSets] = useState<ScryfallSet[]>([]);
|
||||
const [selectedSet, setSelectedSet] = useState(() => localStorage.getItem('cube_selectedSet') || '');
|
||||
const [selectedSets, setSelectedSets] = useState<string[]>(() => {
|
||||
const saved = localStorage.getItem('cube_selectedSets');
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
});
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [gameTypeFilter, setGameTypeFilter] = useState<'all' | 'paper' | 'digital'>('all'); // Filter state
|
||||
const [numBoxes, setNumBoxes] = useState<number>(() => {
|
||||
const saved = localStorage.getItem('cube_numBoxes');
|
||||
return saved ? parseInt(saved) : 3;
|
||||
@@ -103,7 +109,7 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
|
||||
useEffect(() => localStorage.setItem('cube_filters', JSON.stringify(filters)), [filters]);
|
||||
useEffect(() => localStorage.setItem('cube_genSettings', JSON.stringify(genSettings)), [genSettings]);
|
||||
useEffect(() => localStorage.setItem('cube_sourceMode', sourceMode), [sourceMode]);
|
||||
useEffect(() => localStorage.setItem('cube_selectedSet', selectedSet), [selectedSet]);
|
||||
useEffect(() => localStorage.setItem('cube_selectedSets', JSON.stringify(selectedSets)), [selectedSets]);
|
||||
useEffect(() => localStorage.setItem('cube_numBoxes', numBoxes.toString()), [numBoxes]);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -144,11 +150,20 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
|
||||
let expandedCards: ScryfallCard[] = [];
|
||||
|
||||
if (sourceMode === 'set') {
|
||||
if (!selectedSet) throw new Error("Please select a set.");
|
||||
const cards = await scryfallService.fetchSetCards(selectedSet, (count) => {
|
||||
setProgress(`Fetching set cards... (${count})`);
|
||||
if (selectedSets.length === 0) throw new Error("Please select at least one set.");
|
||||
|
||||
for (const [index, setCode] of selectedSets.entries()) {
|
||||
// Update progress for set
|
||||
const setInfo = availableSets.find(s => s.code === setCode);
|
||||
const setName = setInfo ? setInfo.name : setCode;
|
||||
|
||||
setProgress(`Fetching ${setName}... (${index + 1}/${selectedSets.length})`);
|
||||
|
||||
const cards = await scryfallService.fetchSetCards(setCode, (_count) => {
|
||||
// Progress handled by outer loop mostly, but we could update strictly if needed.
|
||||
});
|
||||
expandedCards = cards;
|
||||
expandedCards.push(...cards);
|
||||
}
|
||||
} else {
|
||||
const identifiers = parserService.parse(inputText);
|
||||
const fetchList = identifiers.map(id => id.type === 'id' ? { id: id.value } : { name: id.value });
|
||||
@@ -312,10 +327,11 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
|
||||
setInputText('');
|
||||
setRawScryfallData(null);
|
||||
setProcessedData(null);
|
||||
setSelectedSet('');
|
||||
setProcessedData(null);
|
||||
setSelectedSets([]);
|
||||
localStorage.removeItem('cube_inputText');
|
||||
localStorage.removeItem('cube_rawScryfallData');
|
||||
localStorage.removeItem('cube_selectedSet');
|
||||
localStorage.removeItem('cube_selectedSets');
|
||||
// We keep filters and settings as they are user preferences
|
||||
}
|
||||
};
|
||||
@@ -405,20 +421,118 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-semibold text-slate-300 mb-2">Select Expansion</label>
|
||||
<select
|
||||
value={selectedSet}
|
||||
onChange={(e) => setSelectedSet(e.target.value)}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg p-2 text-sm text-slate-300 focus:ring-2 focus:ring-purple-500 outline-none"
|
||||
<label className="block text-sm font-semibold text-slate-300 mb-2">Select Expansions</label>
|
||||
|
||||
<div className="bg-slate-900 border border-slate-700 rounded-lg overflow-hidden">
|
||||
{/* Search Header */}
|
||||
<div className="flex items-center gap-2 p-2 border-b border-slate-700 bg-slate-800/50">
|
||||
<Search className="w-4 h-4 text-slate-500" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder="Search sets..."
|
||||
className="bg-transparent text-xs w-full outline-none text-white placeholder-slate-500 font-medium"
|
||||
disabled={loading}
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button onClick={() => setSearchTerm('')}>
|
||||
<X className="w-3 h-3 text-slate-500 hover:text-white" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Game Type Filter */}
|
||||
<div className="flex border-b border-slate-700 bg-slate-900">
|
||||
<button
|
||||
onClick={() => setGameTypeFilter('all')}
|
||||
className={`flex-1 py-1.5 text-[10px] font-bold uppercase tracking-wider transition-colors ${gameTypeFilter === 'all' ? 'bg-slate-700 text-white' : 'text-slate-500 hover:text-slate-300 hover:bg-slate-800'}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setGameTypeFilter('paper')}
|
||||
className={`flex-1 py-1.5 text-[10px] font-bold uppercase tracking-wider transition-colors ${gameTypeFilter === 'paper' ? 'bg-emerald-900/40 text-emerald-400' : 'text-slate-500 hover:text-emerald-400 hover:bg-slate-800'}`}
|
||||
title="Show only Paper sets"
|
||||
>
|
||||
Paper
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setGameTypeFilter('digital')}
|
||||
className={`flex-1 py-1.5 text-[10px] font-bold uppercase tracking-wider transition-colors ${gameTypeFilter === 'digital' ? 'bg-blue-900/40 text-blue-400' : 'text-slate-500 hover:text-blue-400 hover:bg-slate-800'}`}
|
||||
title="Show only Digital sets (Arena/MTGO)"
|
||||
>
|
||||
Digital
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div className="max-h-60 overflow-y-auto custom-scrollbar p-1 space-y-0.5">
|
||||
{availableSets
|
||||
.filter(s => {
|
||||
// Search Filter
|
||||
const matchesSearch = !searchTerm ||
|
||||
s.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
s.code.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
// Game Type Filter
|
||||
const matchesType =
|
||||
gameTypeFilter === 'all' ? true :
|
||||
gameTypeFilter === 'paper' ? !s.digital :
|
||||
gameTypeFilter === 'digital' ? s.digital : true;
|
||||
|
||||
return matchesSearch && matchesType;
|
||||
})
|
||||
.map(s => {
|
||||
const isSelected = selectedSets.includes(s.code);
|
||||
return (
|
||||
<label
|
||||
key={s.code}
|
||||
className={`flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer transition-colors ${isSelected ? 'bg-purple-900/30 text-purple-200' : 'hover:bg-slate-800 text-slate-400 hover:text-slate-200'}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => {
|
||||
setSelectedSets(prev =>
|
||||
prev.includes(s.code)
|
||||
? prev.filter(c => c !== s.code)
|
||||
: [...prev, s.code]
|
||||
)
|
||||
}}
|
||||
className="w-3.5 h-3.5 rounded border-slate-600 bg-slate-800 text-purple-500 focus:ring-purple-500 focus:ring-offset-0"
|
||||
disabled={loading}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-medium leading-none">{s.name}</span>
|
||||
<span className="text-[10px] opacity-60 font-mono">{s.code.toUpperCase()} • {s.set_type} • {s.released_at?.slice(0, 4)}</span>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
{availableSets.filter(s => !searchTerm || s.name.toLowerCase().includes(searchTerm.toLowerCase()) || s.code.toLowerCase().includes(searchTerm.toLowerCase())).length === 0 && (
|
||||
<div className="p-3 text-center text-xs text-slate-600 italic">
|
||||
No sets found matching "{searchTerm}"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer Stats */}
|
||||
<div className="bg-slate-950 p-2 border-t border-slate-800 flex justify-between items-center">
|
||||
<span className="text-[10px] text-slate-500 font-mono">
|
||||
{selectedSets.length} selected
|
||||
</span>
|
||||
{selectedSets.length > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedSets([])}
|
||||
className="text-[10px] text-red-400 hover:text-red-300 hover:underline"
|
||||
disabled={loading}
|
||||
>
|
||||
<option value="">-- Choose Set --</option>
|
||||
{availableSets.map(s => (
|
||||
<option key={s.code} value={s.code}>
|
||||
{s.name} ({s.code.toUpperCase()}) - {s.set_type}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
Clear Selection
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
@@ -439,10 +553,10 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
|
||||
|
||||
<button
|
||||
onClick={fetchAndParse}
|
||||
disabled={loading || !selectedSet}
|
||||
disabled={loading || selectedSets.length === 0}
|
||||
className={`w-full py-2 mb-4 rounded-lg font-bold flex justify-center items-center gap-2 transition-all ${loading ? 'bg-slate-700 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-500 text-white'}`}
|
||||
>
|
||||
{loading ? <><Loader2 className="w-4 h-4 animate-spin" /> {progress}</> : <><Check className="w-4 h-4" /> 1. Fetch Set</>}
|
||||
{loading ? <><Loader2 className="w-4 h-4 animate-spin" /> {progress}</> : <><Check className="w-4 h-4" /> 1. Fetch {selectedSets.length > 1 ? 'Sets' : 'Set'}</>}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -239,17 +239,138 @@ export class PackGeneratorService {
|
||||
const namesInThisPack = new Set<string>();
|
||||
|
||||
if (rarityMode === 'peasant') {
|
||||
const COMMONS_COUNT = 10;
|
||||
const UNCOMMONS_COUNT = 5; // Boosted uncommons for peasant
|
||||
// 1. Slots 1-6: Commons (Color Balanced)
|
||||
const commonsNeeded = 6;
|
||||
const drawC = this.drawColorBalanced(currentPools.commons, commonsNeeded, namesInThisPack);
|
||||
|
||||
const drawU = this.drawUniqueCards(currentPools.uncommons, UNCOMMONS_COUNT, namesInThisPack);
|
||||
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;
|
||||
} else if (currentPools.commons.length < commonsNeeded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
packCards.push(...drawC.selected);
|
||||
currentPools.commons = drawC.remainingPool;
|
||||
drawC.selected.forEach(c => namesInThisPack.add(c.name));
|
||||
|
||||
// 2. Slot 7: Common / The List
|
||||
// 1-87: Common from Main Set
|
||||
// 88-97: Card from "The List" (Common/Uncommon)
|
||||
// 98-100: Uncommon from "The List"
|
||||
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) {
|
||||
// List (Common/Uncommon). Simulating by picking 50/50 C/U if actual List not available
|
||||
const useUncommon = Math.random() < 0.5;
|
||||
const pool = useUncommon ? currentPools.uncommons : currentPools.commons;
|
||||
// Fallback if one pool is empty
|
||||
const effectivePool = pool.length > 0 ? pool : (useUncommon ? currentPools.commons : currentPools.uncommons);
|
||||
|
||||
if (effectivePool.length > 0) {
|
||||
const res = this.drawUniqueCards(effectivePool, 1, namesInThisPack);
|
||||
if (res.success) {
|
||||
slot7Card = res.selected[0];
|
||||
// Identify which pool to update
|
||||
if (effectivePool === currentPools.uncommons) currentPools.uncommons = res.remainingPool;
|
||||
else currentPools.commons = res.remainingPool;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 98-100: Uncommon (from List or pool)
|
||||
const res = this.drawUniqueCards(currentPools.uncommons, 1, namesInThisPack);
|
||||
if (res.success) { slot7Card = res.selected[0]; currentPools.uncommons = res.remainingPool; }
|
||||
}
|
||||
|
||||
if (slot7Card) {
|
||||
packCards.push(slot7Card);
|
||||
namesInThisPack.add(slot7Card.name);
|
||||
}
|
||||
|
||||
// 3. Slots 8-11: Uncommons (4 cards)
|
||||
const uncommonsNeeded = 4;
|
||||
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);
|
||||
currentPools.uncommons = drawU.remainingPool;
|
||||
drawU.selected.forEach(c => namesInThisPack.add(c.name));
|
||||
|
||||
const drawC = this.drawUniqueCards(currentPools.commons, COMMONS_COUNT, namesInThisPack);
|
||||
packCards.push(...drawC.selected);
|
||||
currentPools.commons = drawC.remainingPool;
|
||||
// 4. Slot 12: Land (Basic or Common Dual)
|
||||
const foilLandRoll = Math.random();
|
||||
const isFoilLand = foilLandRoll < 0.20;
|
||||
let landCard: DraftCard | undefined;
|
||||
|
||||
if (currentPools.lands.length > 0) {
|
||||
const res = this.drawUniqueCards(currentPools.lands, 1, namesInThisPack);
|
||||
if (res.success) {
|
||||
landCard = { ...res.selected[0] };
|
||||
currentPools.lands = res.remainingPool;
|
||||
}
|
||||
}
|
||||
|
||||
if (landCard) {
|
||||
if (isFoilLand) landCard.finish = 'foil';
|
||||
packCards.push(landCard);
|
||||
namesInThisPack.add(landCard.name);
|
||||
}
|
||||
|
||||
// Helper for Wildcards
|
||||
const drawWildcard = (foil: boolean) => {
|
||||
const wRoll = Math.random() * 100;
|
||||
let wRarity = 'common';
|
||||
// ~49% Common, ~24% Uncommon, ~13% Rare, ~13% Mythic
|
||||
if (wRoll > 87) wRarity = 'mythic';
|
||||
else if (wRoll > 74) wRarity = 'rare';
|
||||
else if (wRoll > 50) wRarity = 'uncommon';
|
||||
else wRarity = 'common';
|
||||
|
||||
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; }
|
||||
|
||||
// Fallback
|
||||
if (poolToUse.length === 0) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 5. Slot 13: Non-Foil Wildcard
|
||||
drawWildcard(false);
|
||||
|
||||
// 6. Slot 14: Foil Wildcard
|
||||
drawWildcard(true);
|
||||
|
||||
// 7. Slot 15: Marketing / Token
|
||||
if (currentPools.tokens.length > 0) {
|
||||
const res = this.drawUniqueCards(currentPools.tokens, 1, namesInThisPack);
|
||||
if (res.success) {
|
||||
packCards.push(res.selected[0]);
|
||||
currentPools.tokens = res.remainingPool;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// --- NEW ALGORITHM (Play Booster) ---
|
||||
|
||||
@@ -168,7 +168,8 @@ export class ScryfallService {
|
||||
name: s.name,
|
||||
set_type: s.set_type,
|
||||
released_at: s.released_at,
|
||||
icon_svg_uri: s.icon_svg_uri
|
||||
icon_svg_uri: s.icon_svg_uri,
|
||||
digital: s.digital
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -226,4 +227,5 @@ export interface ScryfallSet {
|
||||
set_type: string;
|
||||
released_at: string;
|
||||
icon_svg_uri: string;
|
||||
digital: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user