GeoGuessr PlonkIt Button

Adds a button that links to the Plonk It page for that respective country, after the round ends

// ==UserScript==
// @name         GeoGuessr PlonkIt Button
// @description  Adds a button that links to the Plonk It page for that respective country, after the round ends
// @version      1.0
// @author       ArunSomasundaram
// @match        *://*.geoguessr.com/*
// @icon         https://www.google.com/s2/favicons?domain=geoguessr.com
// @grant        GM_openInTab
// @run-at       document-start
// @namespace https://gf.qytechs.cn/users/1484321
// ==/UserScript==

(function() {
    'use strict';

    // ============================================================================
    // CONSTANTS AND CONFIGURATION
    // ============================================================================

    /**
     * Mapping of ISO 3166-1 alpha-2 country codes to PlonkIt URL slugs
     */
    const COUNTRY_DICT = {
        'ad': 'andorra', 'ae': 'united-arab-emirates', 'af': 'afghanistan', 'ag': 'antigua-and-barbuda',
        'ai': 'anguilla', 'al': 'albania', 'am': 'armenia', 'ao': 'angola', 'aq': 'antarctica',
        'ar': 'argentina', 'as': 'american-samoa', 'at': 'austria', 'au': 'australia', 'aw': 'aruba',
        'ax': 'aland-islands', 'az': 'azerbaijan', 'ba': 'bosnia-and-herzegovina', 'bb': 'barbados',
        'bd': 'bangladesh', 'be': 'belgium', 'bf': 'burkina-faso', 'bg': 'bulgaria', 'bh': 'bahrain',
        'bi': 'burundi', 'bj': 'benin', 'bl': 'saint-barthelemy', 'bm': 'bermuda', 'bn': 'brunei',
        'bo': 'bolivia', 'bq': 'caribbean-netherlands', 'br': 'brazil', 'bs': 'bahamas', 'bt': 'bhutan',
        'bv': 'bouvet-island', 'bw': 'botswana', 'by': 'belarus', 'bz': 'belize', 'ca': 'canada',
        'cc': 'cocos-keeling-islands', 'cd': 'democratic-republic-of-the-congo', 'cf': 'central-african-republic',
        'cg': 'republic-of-the-congo', 'ch': 'switzerland', 'ci': 'ivory-coast', 'ck': 'cook-islands',
        'cl': 'chile', 'cm': 'cameroon', 'cn': 'china', 'co': 'colombia', 'cr': 'costa-rica',
        'cu': 'cuba', 'cv': 'cape-verde', 'cw': 'curacao', 'cx': 'christmas-island', 'cy': 'cyprus',
        'cz': 'czech-republic', 'de': 'germany', 'dj': 'djibouti', 'dk': 'denmark', 'dm': 'dominica',
        'do': 'dominican-republic', 'dz': 'algeria', 'ec': 'ecuador', 'ee': 'estonia', 'eg': 'egypt',
        'eh': 'western-sahara', 'er': 'eritrea', 'es': 'spain', 'et': 'ethiopia', 'fi': 'finland',
        'fj': 'fiji', 'fk': 'falkland-islands', 'fm': 'micronesia', 'fo': 'faroe-islands',
        'fr': 'france', 'ga': 'gabon', 'gb': 'united-kingdom', 'gd': 'grenada', 'ge': 'georgia',
        'gf': 'french-guiana', 'gg': 'guernsey', 'gh': 'ghana', 'gi': 'gibraltar', 'gl': 'greenland',
        'gm': 'gambia', 'gn': 'guinea', 'gp': 'guadeloupe', 'gq': 'equatorial-guinea', 'gr': 'greece',
        'gs': 'south-georgia-and-south-sandwich-islands', 'gt': 'guatemala', 'gu': 'guam', 'gw': 'guinea-bissau',
        'gy': 'guyana', 'hk': 'hong-kong', 'hm': 'heard-island-and-mcdonald-islands', 'hn': 'honduras',
        'hr': 'croatia', 'ht': 'haiti', 'hu': 'hungary', 'id': 'indonesia', 'ie': 'ireland',
        'il': 'israel', 'im': 'isle-of-man', 'in': 'india', 'io': 'british-indian-ocean-territory',
        'iq': 'iraq', 'ir': 'iran', 'is': 'iceland', 'it': 'italy', 'je': 'jersey', 'jm': 'jamaica',
        'jo': 'jordan', 'jp': 'japan', 'ke': 'kenya', 'kg': 'kyrgyzstan', 'kh': 'cambodia',
        'ki': 'kiribati', 'km': 'comoros', 'kn': 'saint-kitts-and-nevis', 'kp': 'north-korea',
        'kr': 'south-korea', 'kw': 'kuwait', 'ky': 'cayman-islands', 'kz': 'kazakhstan', 'la': 'laos',
        'lb': 'lebanon', 'lc': 'saint-lucia', 'li': 'liechtenstein', 'lk': 'sri-lanka', 'lr': 'liberia',
        'ls': 'lesotho', 'lt': 'lithuania', 'lu': 'luxembourg', 'lv': 'latvia', 'ly': 'libya',
        'ma': 'morocco', 'mc': 'monaco', 'md': 'moldova', 'me': 'montenegro', 'mf': 'saint-martin',
        'mg': 'madagascar', 'mh': 'marshall-islands', 'mk': 'north-macedonia', 'ml': 'mali', 'mm': 'myanmar',
        'mn': 'mongolia', 'mo': 'macau', 'mp': 'northern-mariana-islands', 'mq': 'martinique',
        'mr': 'mauritania', 'ms': 'montserrat', 'mt': 'malta', 'mu': 'mauritius', 'mv': 'maldives',
        'mw': 'malawi', 'mx': 'mexico', 'my': 'malaysia', 'mz': 'mozambique', 'na': 'namibia',
        'nc': 'new-caledonia', 'ne': 'niger', 'nf': 'norfolk-island', 'ng': 'nigeria', 'ni': 'nicaragua',
        'nl': 'netherlands', 'no': 'norway', 'np': 'nepal', 'nr': 'nauru', 'nu': 'niue', 'nz': 'new-zealand',
        'om': 'oman', 'pa': 'panama', 'pe': 'peru', 'pf': 'french-polynesia', 'pg': 'papua-new-guinea',
        'ph': 'philippines', 'pk': 'pakistan', 'pl': 'poland', 'pm': 'saint-pierre-and-miquelon',
        'pn': 'pitcairn-islands', 'pr': 'puerto-rico', 'ps': 'palestine', 'pt': 'portugal', 'pw': 'palau',
        'py': 'paraguay', 'qa': 'qatar', 're': 'reunion', 'ro': 'romania', 'rs': 'serbia', 'ru': 'russia',
        'rw': 'rwanda', 'sa': 'saudi-arabia', 'sb': 'solomon-islands', 'sc': 'seychelles', 'sd': 'sudan',
        'se': 'sweden', 'sg': 'singapore', 'sh': 'saint-helena', 'si': 'slovenia', 'sj': 'svalbard-and-jan-mayen',
        'sk': 'slovakia', 'sl': 'sierra-leone', 'sm': 'san-marino', 'sn': 'senegal', 'so': 'somalia',
        'sr': 'suriname', 'ss': 'south-sudan', 'st': 'sao-tome-and-principe', 'sv': 'el-salvador',
        'sx': 'sint-maarten', 'sy': 'syria', 'sz': 'eswatini', 'tc': 'turks-and-caicos-islands',
        'td': 'chad', 'tf': 'french-southern-and-antarctic-lands', 'tg': 'togo', 'th': 'thailand',
        'tj': 'tajikistan', 'tk': 'tokelau', 'tl': 'east-timor', 'tm': 'turkmenistan', 'tn': 'tunisia',
        'to': 'tonga', 'tr': 'turkey', 'tt': 'trinidad-and-tobago', 'tv': 'tuvalu', 'tw': 'taiwan',
        'tz': 'tanzania', 'ua': 'ukraine', 'ug': 'uganda', 'um': 'united-states-minor-outlying-islands',
        'us': 'united-states', 'uy': 'uruguay', 'uz': 'uzbekistan', 'va': 'vatican-city', 'vc': 'saint-vincent-and-the-grenadines',
        've': 'venezuela', 'vg': 'british-virgin-islands', 'vi': 'united-states-virgin-islands', 'vn': 'vietnam',
        'vu': 'vanuatu', 'wf': 'wallis-and-futuna', 'ws': 'samoa', 'ye': 'yemen', 'yt': 'mayotte',
        'za': 'south-africa', 'zm': 'zambia', 'zw': 'zimbabwe'
    };

    /**
     * Configuration constants
     */
    const CONFIG = {
        BUTTON_ID: 'plonkit-button',
        POLLING_INTERVAL: 1500,
        PLONKIT_BASE_URL: 'https://plonkit.net',
        FLAG_CDN_URL: 'https://flagcdn.com/24x18'
    };

    /**
     * CSS selectors for DOM elements
     */
    const SELECTORS = {
        RESULT_LAYOUT: '[class*="result-layout_root"]',
        PLAY_AGAIN_BUTTON: 'button[data-qa="play-again-button"]',
        GAME_FINISHED: 'div[class*="game-finished_root"]',
        GAME_SUMMARY: 'div[class*="game-summary_root"]'
    };

    // ============================================================================
    // STATE MANAGEMENT
    // ============================================================================

    /**
     * Set to track processed rounds to prevent duplicate button creation
     */
    const processedRounds = new Set();

    /**
     * Track the last URL to detect navigation changes
     */
    let lastUrl = location.href;

    // ============================================================================
    // UTILITY FUNCTIONS
    // ============================================================================

    /**
     * Generates a flag image URL for the given country code
     * @param {string} countryCode - Two-letter ISO country code
     * @returns {string|null} Flag image URL or null if invalid code
     */
    function getFlagImageUrl(countryCode) {
        if (!countryCode || countryCode.length !== 2) {
            return null;
        }
        return `${CONFIG.FLAG_CDN_URL}/${countryCode.toLowerCase()}.png`;
    }

    /**
     * Checks if the current page is in game or challenge mode
     * @returns {boolean} True if in game/challenge mode
     */
    function isGameMode() {
        return /\/(game|challenge)\//.test(location.pathname);
    }

    /**
     * Extracts country code from game round data
     * @param {Object} roundData - Round data from API response
     * @returns {string|null} Country code or null if not found
     */
    function extractCountryCode(roundData) {
        if (!roundData) return null;

        const code = (
            roundData.streakLocationCode ||
            roundData.locationCode ||
            roundData.countryCode ||
            roundData.country
        )?.toLowerCase();

        return code;
    }

    /**
     * Generates PlonkIt URL for a given country code
     * @param {string} countryCode - Two-letter ISO country code
     * @returns {string|null} PlonkIt URL or null if country not supported
     */
    function getPlonkItUrl(countryCode) {
        const slug = COUNTRY_DICT[countryCode.toLowerCase()];
        return slug ? `${CONFIG.PLONKIT_BASE_URL}/${slug}` : null;
    }

    // ============================================================================
    // BUTTON MANAGEMENT
    // ============================================================================

    /**
     * Creates and displays the PlonkIt button with country flag
     * @param {string} url - PlonkIt URL to open
     * @param {string} countryCode - Two-letter ISO country code
     */
    function createPlonkItButton(url, countryCode) {
        removeButton();

        const button = document.createElement('button');
        button.id = CONFIG.BUTTON_ID;

        const flagImageUrl = getFlagImageUrl(countryCode);
        console.log('Creating PlonkIt button for country:', countryCode, 'with flag URL:', flagImageUrl);

        // Set button content with flag image or fallback emoji
        if (flagImageUrl) {
            button.innerHTML = `
                <img src="${flagImageUrl}"
                     alt="${countryCode.toUpperCase()} flag"
                     style="width: 20px; height: 15px; margin-right: 8px; border-radius: 2px; object-fit: cover;"
                     onerror="this.style.display='none'">
                <span>PlonkIt</span>
            `;
        } else {
            button.innerHTML = '<span>🌍 PlonkIt</span>';
        }

        button.classList.add('plonkit-btn');
        button.onclick = () => window.open(url, '_blank');

        // Apply styles
        applyButtonStyles(button);

        document.body.appendChild(button);
    }

    /**
     * Applies CSS styles to the PlonkIt button
     * @param {HTMLElement} button - Button element to style
     */
    function applyButtonStyles(button) {
        // Inject CSS styles if not already present
        if (!document.getElementById('plonkit-styles')) {
            const style = document.createElement('style');
            style.id = 'plonkit-styles';
            style.textContent = `
                .plonkit-btn {
                    position: relative;
                    transition: transform 0.2s ease, box-shadow 0.2s ease;
                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                }

                .plonkit-btn img {
                    flex-shrink: 0;
                    vertical-align: middle;
                }

                .plonkit-btn:hover {
                    transform: scale(1.05);
                    box-shadow: 0 4px 10px rgba(0, 255, 123, 0.4);
                }

                .plonkit-btn::after {
                    position: absolute;
                    bottom: 120%;
                    left: 50%;
                    transform: translateX(-50%);
                    background: #333;
                    color: white;
                    padding: 6px 10px;
                    border-radius: 4px;
                    white-space: nowrap;
                    font-size: 12px;
                    opacity: 0;
                    pointer-events: none;
                    transition: opacity 0.2s ease;
                    z-index: 9999;
                }

                .plonkit-btn:hover::after {
                    opacity: 1;
                }
            `;
            document.head.appendChild(style);
        }

        // Apply inline styles
        button.style.cssText = `
            position: fixed;
            bottom: 20px;
            left: 20px;
            z-index: 9999;
            background-color: #4caf50;
            color: white;
            border: none;
            border-radius: 8px;
            padding: 10px 16px;
            font-size: 14px;
            font-weight: bold;
            cursor: pointer;
            box-shadow: 0 4px 8px rgba(0,0,0,0.3);
            transition: all 0.2s ease;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
        `;

        // Add hover effects
        button.onmouseover = () => button.style.transform = 'scale(1.05)';
        button.onmouseout = () => button.style.transform = 'scale(1)';
    }

    /**
     * Removes the PlonkIt button from the page
     */
    function removeButton() {
        const button = document.getElementById(CONFIG.BUTTON_ID);
        if (button) {
            button.remove();
        }
    }

    // ============================================================================
    // GAME DATA HANDLING
    // ============================================================================

    /**
     * Fetches game data from GeoGuessr API and creates button if appropriate
     */
    async function fetchGameData() {
        if (!isGameMode()) return;

        try {
            const token = location.pathname.split('/').pop().split('?')[0];
            const isChallenge = location.pathname.includes('/challenge/');
            const apiUrl = isChallenge
            ? `https://www.geoguessr.com/api/v3/challenges/${token}/game`
                : `https://www.geoguessr.com/api/v3/games/${token}`;

            const response = await fetch(apiUrl);
            if (!response.ok) return;

            const data = await response.json();
            const round = data.player?.guesses?.length || 0;
            const roundData = data.rounds?.[round - 1];

            if (!roundData) return;

            const countryCode = extractCountryCode(roundData);
            if (!countryCode) return;

            // Check if this round has already been processed
            const gameId = data.token || token;
            const roundKey = `${gameId}-${round}`;
            if (processedRounds.has(roundKey)) return;

            // Create button if country is supported
            const plonkItUrl = getPlonkItUrl(countryCode);
            if (plonkItUrl) {
                createPlonkItButton(plonkItUrl, countryCode);
                processedRounds.add(roundKey);
            }

        } catch (error) {
            console.error('Error fetching GeoGuessr game data:', error);
        }
    }

    // ============================================================================
    // EVENT HANDLING AND INITIALIZATION
    // ============================================================================

    /**
     * Handles URL changes and page state updates
     */
    function handlePageUpdate() {
        if (!isGameMode()) {
            removeButton();
            return;
        }

        // Handle URL changes
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            processedRounds.clear();
            removeButton();
        }

        // Remove button if result overlay disappears (new round or screen change)
        const resultElement = document.querySelector(SELECTORS.RESULT_LAYOUT);
        if (!resultElement) {
            removeButton();
        }

        // Remove button on final screen after all rounds completed
        const finalScreenSelectors = [
            SELECTORS.PLAY_AGAIN_BUTTON,
            SELECTORS.GAME_FINISHED,
            SELECTORS.GAME_SUMMARY
        ];

        const finalSummary = finalScreenSelectors.some(selector =>
                                                       document.querySelector(selector)
                                                      );

        if (finalSummary) {
            removeButton();
            return;
        }

        // Fetch game data and potentially create button
        fetchGameData();
    }

    /**
     * Initialize the script
     */
    function initialize() {
        console.log('✅ GeoGuessr PlonkIt Button script loaded');

        // Set up DOM mutation observer
        const observer = new MutationObserver(handlePageUpdate);
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        // Set up fallback polling mechanism
        setInterval(handlePageUpdate, CONFIG.POLLING_INTERVAL);
    }

    // ============================================================================
    // SCRIPT EXECUTION
    // ============================================================================

    // Initialize the script when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址