GeoGuessr 5K Counter

counts 5ks in singleplayer games

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         GeoGuessr 5K Counter
// @namespace    https://greasyfork.org/en/users/1501889
// @version      1.4
// @description  counts 5ks in singleplayer games
// @author       Clemens
// @match        https://www.geoguessr.com/*
// @icon         https://www.google.com/s2/favicons?domain=geoguessr.com
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @grant        GM_getResourceURL
// @resource     fiveKImage https://cdn.7tv.app/emote/01JWCSDV98Q7BJDT737J3B8JEN/4x.avif
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const stats = {
        total: GM_getValue('geo5k_total', 0),
        minimized: GM_getValue('geo5k_minimized', false),
        width: GM_getValue('geo5k_width', 150),
        height: GM_getValue('geo5k_height', 80),
        trackedElements: new WeakSet()
    };

    function saveStats() {
        GM_setValue('geo5k_total', stats.total);
        GM_setValue('geo5k_minimized', stats.minimized);
        GM_setValue('geo5k_width', stats.width);
        GM_setValue('geo5k_height', stats.height);
    }

    const fiveKImageUrl = GM_getResourceURL('fiveKImage');

    GM_addStyle(`
        #geo5k-counter {
            position: fixed !important;
            left: 20px;
            top: 75vh;
            background: rgba(30, 30, 30, 0.9);
            color: #f0f0f0;
            padding: 8px 12px;
            border-radius: 8px;
            font-family: 'Segoe UI', system-ui, sans-serif;
            font-size: 14px;
            z-index: 99999 !important;
            border: 1px solid rgba(255, 255, 255, 0.1);
            min-width: 140px;
            min-height: 80px;
            width: ${stats.width}px;
            height: ${stats.height}px;
            cursor: move;
            user-select: none;
            backdrop-filter: blur(4px);
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            box-sizing: border-box;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            transition: all 0.2s ease-in-out;
        }

        #geo5k-counter.minimized {
            width: 16px !important;
            height: 16px !important;
            min-width: 16px !important;
            min-height: 16px !important;
            padding: 0 !important;
            background: #f00;
            border-radius: 50% !important;
            display: flex;
            align-items: center;
            justify-content: center;
            overflow: hidden;
            cursor: pointer;
        }

        #geo5k-counter.minimized > * {
            display: none !important;
        }

        #geo5k-top-bar {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        #geo5k-minimize-btn {
            width: 16px;
            height: 16px;
            background: #f00;
            border-radius: 50%;
            cursor: pointer;
            z-index: 1;
        }

        #geo5k-reset-btn {
            background: #f00;
            color: #fff;
            border: none;
            border-radius: 50%;
            width: 16px;
            height: 16px;
            padding: 0;
            font-size: 0;
            cursor: pointer;
            transition: all 0.2s ease;
            position: relative;
        }

        #geo5k-reset-btn::after {
            content: "R";
            font-size: 10px;
            font-weight: bold;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
        }

        #geo5k-reset-btn:hover {
            background: #ff6b6b;
        }

        #geo5k-content {
            display: flex;
            align-items: flex-start;
            justify-content: center;
            gap: 12px;
            margin-top: 4px;
        }

        #geo5k-total {
            font-weight: 500;
            color: #ffffff;
            font-size: 36px;
            line-height: 48px;
        }

        .five-k-image {
            height: 48px;
            width: 48px;
            object-fit: contain;
        }

        #geo5k-resize-handle {
            position: absolute;
            right: 0;
            bottom: 0;
            width: 12px;
            height: 12px;
            cursor: nwse-resize;
            background: linear-gradient(135deg, transparent 50%, rgba(255,255,255,0.2) 50%);
        }

        .geo5k-highlight {
            animation: geo5k-pulse 0.5s;
        }

        @keyframes geo5k-pulse {
            0% { transform: scale(1); }
            50% { transform: scale(1.1); }
            100% { transform: scale(1); }
        }
    `);

    function createDisplay() {
        let display = document.getElementById('geo5k-counter');
        if (!display) {
            display = document.createElement('div');
            display.id = 'geo5k-counter';
            if (stats.minimized) display.classList.add('minimized');

            display.innerHTML = `
                <div id="geo5k-top-bar">
                    <span id="geo5k-minimize-btn"></span>
                    <button id="geo5k-reset-btn" title="Reset Total Count"></button>
                </div>
                <div id="geo5k-content">
                    <img src="${fiveKImageUrl}" class="five-k-image" alt="5K">
                    <span id="geo5k-total">${stats.total}</span>
                </div>
                <div id="geo5k-resize-handle"></div>
            `;
            document.body.appendChild(display);

            document.getElementById('geo5k-reset-btn').addEventListener('click', function(e) {
                e.stopPropagation();
                if (confirm('Reset total 5K count to zero?')) {
                    stats.total = 0;
                    stats.trackedElements = new WeakSet();
                    saveStats();
                    updateDisplay();
                }
            });

            document.getElementById('geo5k-minimize-btn').addEventListener('click', function(e) {
                e.stopPropagation();
                toggleMinimize();
            });

            display.addEventListener('click', function(e) {
                if (stats.minimized) {
                    toggleMinimize();
                }
            });

            setupDragging(display);
            setupResize(display);
        }
        return display;
    }

    function toggleMinimize() {
        stats.minimized = !stats.minimized;
        saveStats();
        const display = document.getElementById('geo5k-counter');
        if (display) {
             display.classList.toggle('minimized', stats.minimized);
        }
    }

    function setupDragging(display) {
        let isDragging = false;
        let startX, startY, initialX, initialY;

        const dragElements = [display, document.getElementById('geo5k-minimize-btn')];

        dragElements.forEach(element => {
            if (!element) return;

            element.addEventListener('mousedown', (e) => {
                if (e.button !== 0 || e.target.id === 'geo5k-resize-handle') return;

                isDragging = true;
                startX = e.clientX;
                startY = e.clientY;
                initialX = display.offsetLeft;
                initialY = display.offsetTop;

                display.style.transition = 'none';
                e.preventDefault();
            });
        });

        function moveHandler(e) {
            if (!isDragging) return;

            const dx = e.clientX - startX;
            const dy = e.clientY - startY;

            const newX = Math.max(0, Math.min(initialX + dx, window.innerWidth - display.offsetWidth));
            const newY = Math.max(0, Math.min(initialY + dy, window.innerHeight - display.offsetHeight));

            display.style.left = `${newX}px`;
            display.style.top = `${newY}px`;
        }

        function upHandler() {
            if (!isDragging) return;
            isDragging = false;
            display.style.transition = '';
        }

        document.addEventListener('mousemove', moveHandler);
        document.addEventListener('mouseup', upHandler);
    }

    function setupResize(display) {
        const resizeHandle = document.getElementById('geo5k-resize-handle');
        let isResizing = false;
        let startX, startY, startWidth, startHeight;
        let debounceTimer;

        resizeHandle.addEventListener('mousedown', function(e) {
            if (stats.minimized) return;

            isResizing = true;
            startX = e.clientX;
            startY = e.clientY;
            startWidth = parseInt(document.defaultView.getComputedStyle(display).width, 10);
            startHeight = parseInt(document.defaultView.getComputedStyle(display).height, 10);

            display.style.transition = 'none';
            e.preventDefault();
            e.stopPropagation();
        });

        function resizeMove(e) {
            if (!isResizing) return;

            const width = Math.max(140, startWidth + e.clientX - startX);
            const height = Math.max(80, startHeight + e.clientY - startY);

            clearTimeout(debounceTimer);

            debounceTimer = setTimeout(() => {
                display.style.width = `${width}px`;
                display.style.height = `${height}px`;
            }, 1);
        }

        function resizeEnd() {
            if (!isResizing) return;
            isResizing = false;

            display.style.transition = '';

            stats.width = parseInt(display.style.width);
            stats.height = parseInt(display.style.height);
            saveStats();
        }

        document.addEventListener('mousemove', resizeMove);
        document.addEventListener('mouseup', resizeEnd);
    }

    function updateDisplay() {
        const totalEl = document.getElementById('geo5k-total');
        if (totalEl) totalEl.textContent = stats.total;
    }

    function scanFor5K() {
        const scoreContainers = document.querySelectorAll('.round-result_pointsIndicatorWrapper__7JxD_');

        for (const container of scoreContainers) {
            if (stats.trackedElements.has(container)) continue;

            const scoreElement = container.querySelector('.shadow-text_root__KeAY1 div div');
            if (!scoreElement) continue;

            const scoreText = scoreElement.textContent.trim();
            if (scoreText === '5,000' || scoreText === '5000') {
                stats.trackedElements.add(container);
                handle5KDetection(scoreElement);
            }
        }
    }

    function handle5KDetection(element) {
        const now = Date.now();
        if (now - stats.lastDetection < 50) return;

        stats.lastDetection = now;
        stats.total++;
        saveStats();
        updateDisplay();

        element.classList.add('geo5k-highlight');
        setTimeout(() => element.classList.remove('geo5k-highlight'), 500);
    }

    function init() {
        createDisplay();
        updateDisplay();

        const fastScanner = setInterval(scanFor5K, 3);

        const observer = new MutationObserver(scanFor5K);
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        window.addEventListener('beforeunload', () => {
            clearInterval(fastScanner);
            observer.disconnect();
        });
    }

    if (document.readyState === 'complete') {
        init();
    } else {
        document.addEventListener('DOMContentLoaded', init);
    }
})();