Geoguessr Location Finder

Displays GeoGuessr location in a dragable infobox. Press '1' to toggle the info, '2' to switch views. For educational use.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Geoguessr Location Finder
// @namespace    http://tampermonkey.net/
// @version      2.11
// @description  Displays GeoGuessr location in a dragable infobox. Press '1' to toggle the info, '2' to switch views. For educational use.
// @author       WannabeLynx
// @match        https://www.geoguessr.com/*
// @grant        none
// @run-at       document-start
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const SHOW_ON_START = true;
    let currentCoordinates = { lat: null, lng: null };

    let countryInfo = { name: null, code: null };
    let displayMode = 'coords';

    const style = `
        #location-finder-container {
            position: fixed;
            top: 10px;
            left: 10px;
            background-color: rgba(0, 0, 0, 0.85);
            color: white;
            padding: 15px;
            border-radius: 10px;
            z-index: 9999;
            font-family: 'Inter', sans-serif;
            font-size: 14px;
            display: none; /* Initially hidden */
            cursor: move;
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
            border: 1px solid #444;
            min-width: 250px;
        }
        #location-finder-container h3 {
            margin: 0 0 10px 0;
            font-size: 16px;
            text-align: center;
            border-bottom: 1px solid #555;
            padding-bottom: 8px;
        }
        #location-finder-container p {
            margin: 8px 0;
            min-height: 22px; /* Prevent layout shift while loading country */
        }
        #location-finder-container img {
             height: 20px;
             border: 1px solid #555;
             border-radius: 3px;
        }
        #location-finder-container a {
            color: #4da6ff;
            text-decoration: none;
            display: block;
            text-align: center;
            margin-top: 10px;
            background-color: #333;
            padding: 8px 12px;
            border-radius: 5px;
            transition: background-color 0.3s;
        }
        #location-finder-container a:hover {
            background-color: #555;
        }
    `;

    let infoContainer;

    function createUI() {
        if (document.getElementById('location-finder-container')) return;

        const styleSheet = document.createElement("style");
        styleSheet.innerText = style;
        document.head.appendChild(styleSheet);

        infoContainer = document.createElement('div');
        infoContainer.id = 'location-finder-container';
        infoContainer.innerHTML = '<h3>Location Finder</h3><p>Waiting for a new round...</p>';
        document.body.appendChild(infoContainer);

        makeDraggable(infoContainer);
    }

    async function fetchCountryInfo(lat, lng) {
        if (!lat || !lng) return;
        try {
            const response = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&accept-language=en`);
            const data = await response.json();
            if (data && data.address) {
                countryInfo = {
                    name: data.address.country,
                    code: data.address.country_code
                };
                console.log('Location Finder: Found country info', countryInfo);
                if (displayMode === 'country') {
                    updateInfoBox();
                }
            } else {
                 throw new Error("No address data in response.");
            }
        } catch (error) {
            console.error('Location Finder: Error fetching country info:', error);
            countryInfo = { name: 'Could not retrieve country', code: null };
        }
    }

    function updateInfoBox() {
        if (!infoContainer || !currentCoordinates.lat) return;

        const { lat, lng } = currentCoordinates;
        const mapsLink = `https://www.google.com/maps/@${lat},${lng},4z?entry=ttu`;
        let contentHTML = '';

        if (displayMode === 'coords') {
            contentHTML = `
                <p><strong>Latitude:</strong> ${lat.toFixed(6)}</p>
                <p><strong>Longitude:</strong> ${lng.toFixed(6)}</p>
            `;
        } else {
            if (countryInfo.name && countryInfo.code) {
                const flagUrl = `https://flagcdn.com/w40/${countryInfo.code}.png`;
                contentHTML = `
                    <p style="display: flex; align-items: center; gap: 10px;">
                        <img src="${flagUrl}" alt="${countryInfo.name} Flag">
                        <strong>${countryInfo.name}</strong>
                    </p>
                `;
            } else {
                contentHTML = `<p>${countryInfo.name || 'Loading country...'}</p>`;
            }
        }

        infoContainer.innerHTML = `
            <h3>Location Finder</h3>
            ${contentHTML}
            <a href="${mapsLink}" target="_blank">Open in Google Maps</a>
        `;
    }

    function handleCompetitiveMode() {
        if (!infoContainer) createUI();
        infoContainer.innerHTML = `
            <h3>Location Finder</h3>
            <p style="color: #ff6b6b; font-weight: bold;">Competitive mode detected!</p>
            <p>Script is disabled to prevent cheating. Play fair.</p>
        `;
        infoContainer.style.display = 'block';
    }

    function isCompetitiveMode() {
        const url = window.location.href;
        const competitivePaths = ['/duels', '/battle-royale', '/competitive', '/multiplayer'];
        return competitivePaths.some(path => url.includes(path));
    }

    function toggleInfoBox() {
        if (!infoContainer) return;
        infoContainer.style.display = (infoContainer.style.display === 'none') ? 'block' : 'none';
    }

    function makeDraggable(element) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        element.onmousedown = dragMouseDown;

        function dragMouseDown(e) {
            e = e || window.event;
            e.preventDefault();
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }

        function elementDrag(e) {
            e = e || window.event;
            e.preventDefault();
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;
            element.style.top = (element.offsetTop - pos2) + "px";
            element.style.left = (element.offsetLeft - pos1) + "px";
        }

        function closeDragElement() {
            document.onmouseup = null;
            document.onmousemove = null;
        }
    }

    function parseGameData(data) {
        if (!data) return null;
        let roundData = null;
        if (data.rounds && (data.round || data.roundNumber)) {
            roundData = data.rounds[(data.round || data.roundNumber) - 1];
        } else if (data.player && data.player.currentRound) {
            roundData = data.player.currentRound;
        }
        if (roundData) {
            if (roundData.lat && roundData.lng) return { lat: roundData.lat, lng: roundData.lng };
            if (roundData.panorama?.lat && roundData.panorama?.lng) return { lat: roundData.panorama.lat, lng: roundData.panorama.lng };
        }
        return null;
    }

    function setupKeyListeners() {
        document.addEventListener('keydown', (e) => {
            if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;

            if (e.key === '1') {
                e.stopImmediatePropagation();
                toggleInfoBox();
            } else if (e.key === '2') {
                if (currentCoordinates.lat !== null && !isCompetitiveMode()) {
                    e.stopImmediatePropagation();
                    displayMode = displayMode === 'coords' ? 'country' : 'coords';
                    updateInfoBox();
                }
            }
        }, true);
    }

    function interceptFetch() {
        const originalFetch = window.fetch;
        window.fetch = function(input, init) {
            const url = typeof input === 'string' ? input : input.url;

            if (typeof url === 'string' && url.includes('/api/v3/games/')) {
                return originalFetch.apply(this, arguments).then(response => {
                    const clonedResponse = response.clone();
                    clonedResponse.json().then(data => {
                        if (isCompetitiveMode()) {
                            handleCompetitiveMode();
                            return;
                        }
                        const coords = parseGameData(data);
                        if (coords && (coords.lat !== currentCoordinates.lat || coords.lng !== currentCoordinates.lng)) {
                            console.log('Location Finder (fetch): Found new coordinates', coords);
                            currentCoordinates = coords;
                            countryInfo = { name: null, code: null };
                            displayMode = 'coords';

                            updateInfoBox();
                            fetchCountryInfo(coords.lat, coords.lng);

                            if (SHOW_ON_START) {
                                infoContainer.style.display = 'block';
                            }
                        }
                    }).catch(err => {
                        // ignore
                    });
                    return response;
                });
            }
            return originalFetch.apply(this, arguments);
        };
    }

    if (document.readyState === 'loading') {
        window.addEventListener('DOMContentLoaded', createUI);
    } else {
        createUI();
    }

    setupKeyListeners();
    interceptFetch();

})();