MTG Draft GIH WR Overlay

Toggle overlay showing Game In Hand win rates for MTG cards on Draftmancer and 17Lands

// ==UserScript==
// @name         MTG Draft GIH WR Overlay
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Toggle overlay showing Game In Hand win rates for MTG cards on Draftmancer and 17Lands
// @author       You
// @match        https://draftmancer.com/*
// @match        https://www.17lands.com/*
// @license      MIT
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    let overlayEnabled = false;
    let cardData = {};
    let scryfallToName = {}; // Map Scryfall ID to card name (Draftmancer only)
    let currentExpansion = null;
    let manualExpansion = null;
    let dataLoaded = false;
    let currentSite = null;

    // Site detection and configuration
    const SITES = {
        DRAFTMANCER: {
            name: 'Draftmancer',
            detect: () => window.location.hostname.includes('draftmancer.com'),
            cardSelector: '.card[data-arena-id]',
            imageContainerSelector: '.card-image',
            expansionDetector: detectExpansionDraftmancer,
            getCardName: getCardNameDraftmancer,
            needsScryfall: true
        },
        SEVENTEENLANDS: {
            name: '17Lands',
            detect: () => window.location.hostname.includes('17lands.com'),
            cardSelector: 'img.sc-iFjrBz[alt]',
            imageContainerSelector: null, // Append directly to parent
            expansionDetector: detectExpansion17Lands,
            getCardName: getCardName17Lands,
            needsScryfall: false
        }
    };

    // Detect which site we're on
    function detectSite() {
        for (const site of Object.values(SITES)) {
            if (site.detect()) {
                currentSite = site;
                console.log(`Detected site: ${site.name}`);
                return site;
            }
        }
        console.error('Unknown site - extension may not work correctly');
        return null;
    }

    // Draftmancer: Fetch Scryfall card names for visible cards using collection endpoint
    async function loadScryfallMapping() {
        if (!currentSite || !currentSite.needsScryfall) return;
        
        try {
            const cardElements = document.querySelectorAll(currentSite.cardSelector);
            const scryfallIds = Array.from(cardElements).map(card => 
                card.getAttribute('data-arena-id')
            ).filter(id => id && !scryfallToName[id]);
            
            if (scryfallIds.length === 0) {
                console.log('All visible cards already mapped');
                return;
            }
            
            console.log(`Fetching Scryfall data for ${scryfallIds.length} cards...`);
            
            // Split into batches of 75 (Scryfall's limit)
            const batches = [];
            for (let i = 0; i < scryfallIds.length; i += 75) {
                batches.push(scryfallIds.slice(i, i + 75));
            }
            
            for (const batch of batches) {
                const identifiers = batch.map(id => ({ id }));
                
                const response = await fetch('https://api.scryfall.com/cards/collection', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ identifiers })
                });
                
                if (!response.ok) {
                    throw new Error(`Scryfall API error: ${response.status}`);
                }
                
                const data = await response.json();
                
                data.data.forEach(card => {
                    scryfallToName[card.id] = card.name;
                });
                
                if (batches.length > 1) {
                    await new Promise(resolve => setTimeout(resolve, 100));
                }
            }
            
            console.log(`Loaded ${Object.keys(scryfallToName).length} Scryfall ID mappings`);
            
        } catch (error) {
            console.error('Error fetching Scryfall data:', error);
        }
    }

    // Draftmancer: Detect expansion from page
    function detectExpansionDraftmancer() {
        const cardPoolIcon = document.querySelector('.card-pool-controls .selected-sets .set-icon[alt]');
        
        if (cardPoolIcon) {
            const alt = cardPoolIcon.getAttribute('alt');
            if (alt) {
                const expansion = alt.toUpperCase();
                console.log(`Detected expansion from card pool: ${expansion}`);
                return expansion;
            }
        }
        
        const selectedSets = document.querySelector('.selected-sets .set-icon[alt]');
        if (selectedSets) {
            const alt = selectedSets.getAttribute('alt');
            if (alt) {
                const expansion = alt.toUpperCase();
                console.log(`Detected expansion from selected-sets: ${expansion}`);
                return expansion;
            }
        }
        
        console.warn('Could not detect expansion from page');
        return null;
    }

    // 17Lands: Detect expansion from URL or page
    function detectExpansion17Lands() {
        // Try to get from URL first (e.g., /draft/FIN)
        const urlMatch = window.location.pathname.match(/\/draft\/([A-Z0-9]+)/i);
        if (urlMatch) {
            const expansion = urlMatch[1].toUpperCase();
            console.log(`Detected expansion from URL: ${expansion}`);
            return expansion;
        }
        
        // Try to find it in the page title or heading
        const heading = document.querySelector('h1');
        if (heading) {
            const text = heading.textContent;
            // Look for common set code patterns
            const setMatch = text.match(/\b([A-Z]{3})\b/);
            if (setMatch) {
                const expansion = setMatch[1].toUpperCase();
                console.log(`Detected expansion from heading: ${expansion}`);
                return expansion;
            }
        }
        
        console.warn('Could not detect expansion from 17Lands page');
        return null;
    }

    // Draftmancer: Get card name from Scryfall mapping
    function getCardNameDraftmancer(cardElement) {
        const scryfallId = cardElement.getAttribute('data-arena-id');
        return scryfallToName[scryfallId];
    }

    // 17Lands: Get card name from alt text
    function getCardName17Lands(cardElement) {
        return cardElement.getAttribute('alt');
    }

    // Fetch card data from 17Lands API
    async function loadCardData(expansion = null) {
        if (!expansion) {
            expansion = currentSite.expansionDetector();
        }
        
        if (!expansion) {
            console.error('Cannot load card data: no expansion detected');
            return;
        }
        
        currentExpansion = expansion;
        
        try {
            console.log(`Fetching card data for ${expansion}...`);
            
            const url = `https://www.17lands.com/card_ratings/data?expansion=${expansion}&format=PremierDraft`;
            
            const response = await fetch(url, {
                method: 'GET',
                headers: {
                    'Accept': 'application/json',
                }
            });
            
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            
            const data = await response.json();
            
            data.forEach(card => {
                if (card.name && card.opening_hand_win_rate !== null) {
                    const id = card.mtga_id || card.arena_id || card.name;
                    
                    cardData[id] = {
                        gihWR: card.opening_hand_win_rate,
                        name: card.name,
                        color: card.color || '',
                        rarity: card.rarity || '',
                        gamesPlayed: card.game_count || 0
                    };
                }
            });
            
            dataLoaded = true;
            console.log(`Loaded data for ${Object.keys(cardData).length} cards from ${expansion}`);
            
            if (overlayEnabled) {
                hideOverlays();
                showOverlays();
            }
            
        } catch (error) {
            console.error('Error fetching card data:', error);
            console.log('Attempting to load from localStorage cache...');
            loadFromCache();
        }
    }

    // Cache data in localStorage
    function cacheData() {
        try {
            const cache = {
                data: cardData,
                expansion: currentExpansion,
                timestamp: Date.now()
            };
            localStorage.setItem('gihWRCache', JSON.stringify(cache));
        } catch (e) {
            console.warn('Failed to cache data:', e);
        }
    }

    // Load from localStorage cache
    function loadFromCache() {
        try {
            const cached = localStorage.getItem('gihWRCache');
            if (cached) {
                const cache = JSON.parse(cached);
                const age = Date.now() - cache.timestamp;
                
                if (age < 24 * 60 * 60 * 1000) {
                    cardData = cache.data;
                    currentExpansion = cache.expansion;
                    dataLoaded = true;
                    console.log(`Loaded ${Object.keys(cardData).length} cards from cache (${currentExpansion})`);
                    return true;
                }
            }
        } catch (e) {
            console.warn('Failed to load cache:', e);
        }
        return false;
    }

    // Create overlay element for a card
    function createOverlay(cardElement) {
        if (!currentSite) return null;
        
        let cardName = currentSite.getCardName(cardElement);
        
        if (!cardName) {
            return null;
        }
        
        // Handle double-faced cards - 17Lands only uses front face
        if (cardName.includes(' // ')) {
            cardName = cardName.split(' // ')[0];
        }
        
        // Look up data by card name
        let data = Object.values(cardData).find(c => c.name === cardName);
        
        if (!data) {
            return null;
        }
        
        if (data.gihWR === null || data.gihWR === undefined) {
            return null;
        }
        
        const overlay = document.createElement('div');
        overlay.className = 'gih-wr-overlay';
        
        const sampleWarning = data.gamesPlayed < 500 ? '⚠️ ' : '';
        
        // Different sizes for different sites
        const fontSize = currentSite === SITES.DRAFTMANCER ? '16px' : '12px';
        const padding = currentSite === SITES.DRAFTMANCER ? '5px 10px' : '3px 6px';
        
        overlay.style.cssText = `
            position: absolute;
            top: 32px;
            right: 8px;
            background: rgba(0, 0, 0, 0.85);
            color: ${getWinrateColor(data.gihWR)};
            padding: ${padding};
            border-radius: 4px;
            font-weight: bold;
            font-size: ${fontSize};
            z-index: 1000;
            pointer-events: none;
            box-shadow: 0 2px 4px rgba(0,0,0,0.3);
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        `;
        overlay.textContent = `${sampleWarning}GIH: ${(data.gihWR * 100).toFixed(1)}%`;
        
        return overlay;
    }

    // Color code based on win rate (colorblind-friendly palette)
    function getWinrateColor(wr) {
        if (wr >= 0.58) return '#60a5fa'; // Blue - excellent
        if (wr >= 0.55) return '#93c5fd'; // Light blue - good
        if (wr >= 0.52) return '#e0e7ff'; // Very light blue/white - above average
        if (wr >= 0.50) return '#fbbf24'; // Amber/yellow - average
        return '#fb923c'; // Orange - below average
    }

    // Toggle overlays on/off
    function toggleOverlays() {
        overlayEnabled = !overlayEnabled;
        
        if (overlayEnabled) {
            showOverlays();
        } else {
            hideOverlays();
        }
        
        console.log(`GIH WR Overlay: ${overlayEnabled ? 'ON' : 'OFF'}`);
    }

    function showOverlays() {
        if (!currentSite) return;
        
        const cards = document.querySelectorAll(currentSite.cardSelector);
        
        console.log(`Looking for cards with selector '${currentSite.cardSelector}'`);
        console.log(`Found ${cards.length} cards`);
        
        cards.forEach(card => {
            // Skip if overlay already exists
            if (card.querySelector('.gih-wr-overlay')) return;
            
            const overlay = createOverlay(card);
            if (overlay) {
                // For 17Lands, we need to wrap the image in a positioned container
                if (currentSite === SITES.SEVENTEENLANDS) {
                    const parent = card.parentElement;
                    if (parent && !parent.querySelector('.gih-wr-overlay')) {
                        parent.style.position = 'relative';
                        parent.appendChild(overlay);
                    }
                } else {
                    // Draftmancer - use the card-image container
                    const imageContainer = card.querySelector(currentSite.imageContainerSelector);
                    if (imageContainer) {
                        imageContainer.style.position = 'relative';
                        imageContainer.appendChild(overlay);
                    }
                }
            }
        });
    }

    function hideOverlays() {
        const overlays = document.querySelectorAll('.gih-wr-overlay');
        overlays.forEach(overlay => overlay.remove());
    }

    // Watch for new cards being added to the DOM
    function observeCards() {
        let debounceTimer = null;
        
        const observer = new MutationObserver((mutations) => {
            if (!overlayEnabled) return;
            
            // Check if any mutations actually added/removed card elements
            const hasCardChanges = mutations.some(mutation => {
                if (mutation.type === 'childList') {
                    const addedCards = Array.from(mutation.addedNodes).some(node => {
                        return node.nodeType === 1 && (
                            node.matches && node.matches(currentSite.cardSelector) ||
                            node.querySelector && node.querySelector(currentSite.cardSelector)
                        );
                    });
                    const removedCards = Array.from(mutation.removedNodes).some(node => {
                        return node.nodeType === 1 && (
                            node.matches && node.matches(currentSite.cardSelector) ||
                            node.querySelector && node.querySelector(currentSite.cardSelector)
                        );
                    });
                    return addedCards || removedCards;
                }
                return false;
            });
            
            if (hasCardChanges) {
                // Debounce to avoid rapid updates
                clearTimeout(debounceTimer);
                debounceTimer = setTimeout(showOverlays, 200);
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // Keyboard shortcut handler
    function handleKeyPress(e) {
        // Toggle with Ctrl+Shift+A (or Cmd+Shift+A on Mac)
        if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'A') {
            e.preventDefault();
            toggleOverlays();
        }
        
        // Reload data with Ctrl+Shift+R
        if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'R') {
            e.preventDefault();
            console.log('Reloading card data...');
            cardData = {};
            scryfallToName = {};
            dataLoaded = false;
            
            const expansion = manualExpansion || currentSite.expansionDetector();
            if (expansion) {
                if (currentSite.needsScryfall) {
                    loadScryfallMapping().then(() => {
                        loadCardData(expansion);
                    });
                } else {
                    loadCardData(expansion);
                }
            }
        }
        
        // Manual set override with Ctrl+Shift+S
        if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'S') {
            e.preventDefault();
            const expansion = prompt('Enter expansion code (e.g., FIN, MH3, BLB):');
            if (expansion) {
                manualExpansion = expansion.toUpperCase();
                console.log(`Manual expansion override set to: ${manualExpansion}`);
                cardData = {};
                scryfallToName = {};
                dataLoaded = false;
                
                if (currentSite.needsScryfall) {
                    loadScryfallMapping().then(() => {
                        loadCardData(manualExpansion);
                    });
                } else {
                    loadCardData(manualExpansion);
                }
            }
        }
    }

    // Initialize
    async function init() {
        // Detect which site we're on
        detectSite();
        
        if (!currentSite) {
            console.error('Could not detect site - extension will not work');
            return;
        }
        
        console.log(`GIH WR Overlay script loaded for ${currentSite.name}.`);
        console.log('Hotkeys:');
        console.log('  Ctrl+Shift+A - Toggle overlay');
        console.log('  Ctrl+Shift+R - Reload data');
        console.log('  Ctrl+Shift+S - Manually set expansion code');
        
        document.addEventListener('keydown', handleKeyPress);
        observeCards();
        
        // Wait for page to load
        await new Promise(resolve => setTimeout(resolve, 1000));
        
        // Detect expansion
        const expansion = manualExpansion || currentSite.expansionDetector();
        
        if (!expansion) {
            console.warn('No expansion detected. Press Ctrl+Shift+S to manually set it.');
            return;
        }
        
        // Load Scryfall mapping only if needed (Draftmancer)
        if (currentSite.needsScryfall) {
            await loadScryfallMapping();
        }
        
        // Try cache first
        const cacheLoaded = loadFromCache();
        
        // Fetch fresh data
        await loadCardData(expansion);
        
        // Cache the fresh data
        if (dataLoaded) {
            cacheData();
        }
    }

    // Wait for page to be ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();