GeoGuessr 5K Counter

singleplayer 5k counter, counts per game and total, resettable

当前为 2025-08-06 提交的版本,查看 最新版本

// ==UserScript==
// @name         GeoGuessr 5K Counter
// @namespace    https://greasyfork.org/en/users/1501889
// @version      1.3
// @description  singleplayer 5k counter, counts per game and total, resettable
// @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
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const stats = {
        total: GM_getValue('geo5k_total', 0),
        currentGame: GM_getValue('geo5k_current', 0),
        lastDetection: 0,
        detectedElements: new Set()
    };

    function saveStats() {
        GM_setValue('geo5k_total', stats.total);
        GM_setValue('geo5k_current', stats.currentGame);
    }

    GM_addStyle(`
        #geo5k-counter {
            position: fixed !important;
            left: 20px;
            top: 75vh;
            background: rgba(30, 30, 30, 0.9);
            color: #f0f0f0;
            padding: 12px 16px;
            border-radius: 8px;
            font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
            font-size: 14px;
            z-index: 99999 !important; /* Higher z-index */
            border: 1px solid rgba(255, 255, 255, 0.1);
            min-width: 180px;
            cursor: move;
            user-select: none;
            backdrop-filter: blur(4px);
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            transition: transform 0.1s ease, box-shadow 0.2s ease;
        }
        #geo5k-counter:hover {
            box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
        }
        #geo5k-counter.dragging {
            cursor: grabbing;
            transform: scale(1.02);
            box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
            transition: transform 0.05s ease, box-shadow 0.05s ease;
        }
        #geo5k-counter-header {
            font-weight: 600;
            margin-bottom: 8px;
            font-size: 15px;
            color: #58a6ff;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }
        #geo5k-counter-header::before {
            content: "✓";
            color: #58a6ff;
            font-weight: bold;
            margin-right: 6px;
        }
        .geo5k-counter-row {
            margin: 6px 0;
            display: flex;
            justify-content: space-between;
        }
        .geo5k-counter-value {
            font-weight: 500;
            color: #ffffff;
        }
        #geo5k-reset-btn {
            background: rgba(255, 255, 255, 0.1);
            color: #ff6b6b;
            border: none;
            border-radius: 4px;
            padding: 2px 8px;
            font-size: 12px;
            cursor: pointer;
            transition: all 0.2s ease;
            margin-left: 8px;
        }
        #geo5k-reset-btn:hover {
            background: rgba(255, 107, 107, 0.2);
            color: #ff4d4d;
        }
        .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() {
        // Only create if doesn't exist or was removed
        let display = document.getElementById('geo5k-counter');
        if (!display) {
            display = document.createElement('div');
            display.id = 'geo5k-counter';
            display.innerHTML = `
                <div id="geo5k-counter-header">
                    <span>5K COUNTER</span>
                    <button id="geo5k-reset-btn" title="Reset Total Count">Reset</button>
                </div>
                <div class="geo5k-counter-row">
                    <span>This Game:</span>
                    <span id="geo5k-current" class="geo5k-counter-value">${stats.currentGame}/5</span>
                </div>
                <div class="geo5k-counter-row">
                    <span>Total:</span>
                    <span id="geo5k-total" class="geo5k-counter-value">${stats.total}</span>
                </div>
            `;
            document.body.appendChild(display);

            document.getElementById('geo5k-reset-btn').addEventListener('click', function(e) {
                e.stopPropagation();
                if (confirm('Are you sure you want to reset your total 5K count?')) {
                    stats.total = 0;
                    saveStats();
                    updateDisplay();
                }
            });

            setupDragging(display);
        }
        return display;
    }

    function setupDragging(display) {
        let isDragging = false;
        let startX, startY, initialX, initialY;
        
        display.addEventListener('mousedown', (e) => {
            if (e.button !== 0 || e.target.id === 'geo5k-reset-btn') return;
            
            isDragging = true;
            startX = e.clientX;
            startY = e.clientY;
            initialX = display.offsetLeft;
            initialY = display.offsetTop;
            
            display.classList.add('dragging');
            display.style.transition = 'none';
            e.preventDefault();
        });

        const 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`;
        };

        const upHandler = () => {
            if (!isDragging) return;
            isDragging = false;
            display.classList.remove('dragging');
            display.style.transition = '';
        };

        document.addEventListener('mousemove', moveHandler, { passive: true });
        document.addEventListener('mouseup', upHandler);
    }

    function updateDisplay() {
        const display = document.getElementById('geo5k-counter');
        if (!display) {
            createDisplay();
            return;
        }
        
        const currentEl = document.getElementById('geo5k-current');
        const totalEl = document.getElementById('geo5k-total');
        if (currentEl) currentEl.textContent = `${stats.currentGame}/5`;
        if (totalEl) totalEl.textContent = stats.total;
    }

    function checkFor5K() {
        // Check all potential score-containing elements
        const elements = document.querySelectorAll('span, div, p, h1, h2, h3, h4, h5, h6, button');
        
        for (const element of elements) {
            if (stats.detectedElements.has(element)) continue;
            
            const text = element.textContent.trim();
            if (text === '5,000' || text === '5000' || text.replace(/\s/g,'') === '5000') {
                stats.detectedElements.add(element);
                handle5KDetection(element);

                setTimeout(() => stats.detectedElements.delete(element), 3000);
                break; // Only process one at a time
            }
        }
    }

    function handle5KDetection(element) {
        const now = Date.now();
        if (now - stats.lastDetection < 1000) return; // 1 second cooldown
        stats.lastDetection = now;
        
        stats.total++;
        stats.currentGame++;
        saveStats();
        updateDisplay();

        element.classList.add('geo5k-highlight');
        setTimeout(() => element.classList.remove('geo5k-highlight'), 500);
 
        if (stats.currentGame >= 5) {
            setTimeout(() => {
                stats.currentGame = 0;
                saveStats();
                updateDisplay();
            }, 3000);
        }
    }

    function checkGameState() {
        const indicators = ['Round 1/5', '1 / 5', 'round 1 of 5'];
        const bodyText = document.body.textContent;
        
        if (indicators.some(indicator => bodyText.includes(indicator))) {
            stats.currentGame = 0;
            stats.detectedElements.clear();
            saveStats();
            updateDisplay();
        }
    }

    function init() {
        // Create display first
        createDisplay();
        updateDisplay();

        setInterval(() => {
            checkFor5K();
            checkGameState();
        }, 200); // Check every 200ms
 
        document.addEventListener('keyup', (e) => {
            if (e.code === 'Space') setTimeout(checkFor5K, 100);
        });
        
        document.addEventListener('click', () => setTimeout(checkFor5K, 100));

        setInterval(() => {
            if (!document.getElementById('geo5k-counter')) {
                createDisplay();
                updateDisplay();
            }
        }, 1000);
    }

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