GeoGuessr PlonkIt Button

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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://greasyfork.org/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();
    }

})();