Geoguessr Fast Move v2.17

Hold Shift to move quickly, matching spacebar trick speed. Drives towards user heading direction, rather than following pano chain.

当前为 2025-04-28 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Geoguessr Fast Move v2.17
// @description  Hold Shift to move quickly, matching spacebar trick speed. Drives towards user heading direction, rather than following pano chain.
// @version      2.17
// @author       James C
// @match        *://*.geoguessr.com/*
// @run-at       document-start
// @icon         https://www.google.com/s2/favicons?domain=geoguessr.com
// @grant        none
// @license      MIT
// @namespace    http://tampermonkey.net/
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const MOVE_ATTEMPT_DELAY_MS = 10; // Delay AFTER successful pano_changed before attempting next move. (Lower = potentially faster but might need more retries)
    const MAX_ANGLE_DIFF = 75;        // Angle tolerance for choosing next link. Higher = follows curves easier, Lower = stricter path.
    const ENABLE_LOGGING = false;     // SET TO FALSE for normal use. Set true for debugging/timing.
    const ENABLE_MANUAL_LOG_MODE = false; // SET TO FALSE for normal use. Set true + hold Ctrl to time manual moves.
    const MANUAL_LOG_KEY = 'Control'; // Key to hold for manual logging mode.
    const STUCK_RETRY_DELAY_MS = 25;  // Delay before retrying if no link OR only self-link found. (Lower = recovers faster)
    const INSTANCE_WAIT_TIMEOUT = 3000; // Max time (ms) to wait for StreetView instance on startup.
    const INSTANCE_CHECK_INTERVAL = 100; // How often (ms) to check for instance if initially missing.
    // --- End Configuration ---

    let JCStreetViewInstance = null;
    let isMoving = false;
    let previousPanoId = null;
    let googleMapsApiLoaded = false;
    let scriptStartTime = null;
    let scriptMoveCounter = 0;
    let isLoggingManual = false;
    let manualStartTime = null;
    let manualMoveCounter = 0;
    let lastLoggedManualPano = null;
    let isWaitingForInstance = false;
    let moveTimeoutId = null; // Stores setTimeout ID for next attempt

    // Logging functions
    function log(...args) { if (ENABLE_LOGGING) console.log('[FastMove]', ...args); }
    function errorLog(...args) { console.error('[FastMove]', ...args); } // Always log errors

    log("Script starting execution.");

    // --- Google Maps API Injection & Override ---
    function findAndOverrideGoogleMaps(overrider) {
        log("Setting up MutationObserver...");
        const observer = new MutationObserver((mutations, obs) => {
            const googleScript = mutations.flatMap(m => Array.from(m.addedNodes)).find(node => node.tagName === 'SCRIPT' && node.src?.startsWith('https://maps.googleapis.com/'));
            if (googleScript) {
                 log("Google Maps API script tag found:", googleScript.src);
                 const oldOnload = googleScript.onload;
                 googleScript.onload = (event) => {
                      log("Google Maps API script onload event fired.");
                      if (window.google && window.google.maps && !googleMapsApiLoaded) {
                           googleMapsApiLoaded = true; obs.disconnect(); log("MutationObserver disconnected.");
                           try { log("Calling API overrider function."); overrider(window.google); } catch (e) { errorLog("Error during override call:", e); }
                      } else if (!googleMapsApiLoaded) { log("WARNING: onload fired but maps not found yet."); }
                      if (typeof oldOnload === 'function') { try { oldOnload.call(googleScript, event); } catch (e) { errorLog("Error calling original onload:", e); } }
                 };
                 if (window.google && window.google.maps && !googleMapsApiLoaded) {
                     log("Google Maps API likely already loaded...");
                     if(googleScript.onload !== oldOnload || typeof oldOnload !== 'function') { googleScript.onload(null); } else { log("Manual trigger skipped."); }
                 } else if (!googleMapsApiLoaded) { log("API object not present yet..."); }
            }
        });
        observer.observe(document.documentElement, { childList: true, subtree: true });
    }
    function overrideStreetViewPanorama(google) {
        log("Attempting to override google.maps.StreetViewPanorama...");
        if (!google || !google.maps || !google.maps.StreetViewPanorama) { errorLog("Cannot override: google.maps.StreetViewPanorama not found!"); return; }
        const original = google.maps.StreetViewPanorama;
        google.maps.StreetViewPanorama = class CustomStreetViewPanorama extends original {
            constructor(...args) {
                log(">>> Custom StreetViewPanorama constructor CALLED.");
                super(...args);
                JCStreetViewInstance = this;
                log(">>> JCStreetViewInstance ASSIGNED:", JCStreetViewInstance ? "Success" : "Failed");
                this.addListener('pano_changed', handlePanoChange);
                this.addListener('position_changed', () => {}); // Can add logging here if needed
                log("Listeners added.");
            }
        };
        log("StreetViewPanorama overridden successfully.");
    }

    // --- Pano Change Handler (Drives the loop) ---
    function handlePanoChange() {
        const newPanoId = JCStreetViewInstance?.getPano();
        if (!newPanoId) return;

        // Log manual move if applicable
        if (ENABLE_MANUAL_LOG_MODE && isLoggingManual && newPanoId !== lastLoggedManualPano) {
            manualMoveCounter++; const elapsed = manualStartTime ? Date.now() - manualStartTime : 0;
            log(`Manual Move #${manualMoveCounter}: -> ${newPanoId} (Elapsed: ${elapsed}ms)`);
            lastLoggedManualPano = newPanoId;
        }

        // If the script is running, schedule the next move attempt
        if (isMoving) {
            log(`Pano Change Confirmed: ${newPanoId}. Scheduling next move attempt.`);
            if (moveTimeoutId) clearTimeout(moveTimeoutId);
            moveTimeoutId = setTimeout(attemptMove, MOVE_ATTEMPT_DELAY_MS);
        }
    }

    // --- Start/Stop Script Movement ---
    function startMoving() {
        if (isMoving || isLoggingManual || isWaitingForInstance) return; // Prevent multiple starts/interference
        if (!JCStreetViewInstance) {
            log("Instance not ready on Shift press. Waiting...");
            isWaitingForInstance = true;
            const waitStartTime = Date.now();
            const intervalId = setInterval(() => {
                if (!JCStreetViewInstance) { // Try DOM query as fallback
                    const el = document.querySelectorAll('.street-view-container_root__LKbmk')[0]; // Adjust if selector changes
                    if (el?.__panorama) JCStreetViewInstance = el.__panorama;
                }
                if (JCStreetViewInstance) { // Instance found!
                    clearInterval(intervalId); isWaitingForInstance = false;
                    log(`Instance ready after ${Date.now() - waitStartTime}ms.`);
                    executeStartMoving();
                } else if (Date.now() - waitStartTime > INSTANCE_WAIT_TIMEOUT) { // Timeout
                    clearInterval(intervalId); isWaitingForInstance = false;
                    errorLog(`Instance not found after ${INSTANCE_WAIT_TIMEOUT}ms timeout. Cannot start moving.`);
                }
            }, INSTANCE_CHECK_INTERVAL);
            return; // Wait for interval to succeed or fail
        }
        executeStartMoving(); // Instance was ready immediately
    }

    function executeStartMoving() {
         if (!JCStreetViewInstance) { errorLog("ExecuteStartMoving: Instance missing!"); return; } // Final safety check
        log("Game settings check skipped."); // Assuming unreliable setting
        isMoving = true;
        previousPanoId = null; // Reset previous ID
        scriptMoveCounter = 0;
        scriptStartTime = Date.now();
        const initialPano = JCStreetViewInstance.getPano();
        log(`SCRIPT Movement STARTED. Time: ${scriptStartTime}. Initial Pano: ${initialPano || 'Unknown'}`);
        log("Triggering first move attempt.");
        attemptMove(); // Start the event chain
    }

    function stopMoving() {
        if (!isMoving) return; // Only stop if actually moving
        const endTime = Date.now();
        const duration = scriptStartTime ? endTime - scriptStartTime : 0;
        isMoving = false;
        if (moveTimeoutId) { // Clear any pending move attempt
            clearTimeout(moveTimeoutId);
            moveTimeoutId = null;
            log("Pending move attempt cleared.");
        }
        previousPanoId = null;
        log(`SCRIPT Movement STOPPED. Time: ${endTime}. Duration: ${duration}ms. Successful moves: ${scriptMoveCounter}.`);
        scriptStartTime = null;
    }

    // --- Core Movement Logic ---
    function attemptMove() {
        moveTimeoutId = null; // Clear ID, as we are executing this attempt

        // Check essential conditions
        if (!isMoving || !JCStreetViewInstance) { if (isMoving) stopMoving(); return; }

        try {
            const currentPano = JCStreetViewInstance.getPano();
            if (!currentPano) { log("AttemptMove: Failed to get currentPano."); scheduleRetry(); return; } // Retry if pano ID fails

            const pov = JCStreetViewInstance.getPov();
            if (!pov) { log(`AttemptMove: Failed to get POV for ${currentPano}.`); scheduleRetry(); return; } // Retry if POV fails

            if (typeof JCStreetViewInstance.getLinks !== 'function') { errorLog("getLinks not a function!"); stopMoving(); return; } // Fatal error
            const links = JCStreetViewInstance.getLinks();

            let currentHeading = pov.heading; currentHeading = (currentHeading % 360 + 360) % 360;
            let bestLink = null; let minDiff = MAX_ANGLE_DIFF;
            let foundValidLink = false;

            log(`Attempting move from ${currentPano}. Heading: ${currentHeading.toFixed(1)}`);

            if (Array.isArray(links)) {
                for (const link of links) {
                    if (!link || typeof link.pano !== 'string' || typeof link.heading !== 'number') continue;
                    // Skip immediate U-turn
                    if (link.pano === previousPanoId) { log(`  Skipping link to previous: ${link.pano}`); continue; }

                    let linkHeading = link.heading; linkHeading = (linkHeading % 360 + 360) % 360;
                    let diff = Math.abs(currentHeading - linkHeading); if (diff > 180) diff = 360 - diff;

                    if (diff < minDiff) {
                        minDiff = diff; bestLink = link;
                         log(`  Found potential link: ${link.pano} (Diff: ${minDiff.toFixed(1)})`);
                    }
                }
            } else { log(`  No links array found for ${currentPano}.`); }

            // --- Decision ---
            if (bestLink) {
                // Avoid moving to the *exact same* pano (can happen with API glitches)
                if (bestLink.pano === currentPano) {
                    log(`Script Move: Avoided self-move ${currentPano}`);
                    // Continue to retry logic below
                } else {
                    // Execute the valid move
                    log(`>>> Executing Move: ${currentPano} -> ${bestLink.pano} (Diff: ${minDiff.toFixed(1)})`);
                    scriptMoveCounter++;
                    previousPanoId = currentPano; // Record the pano we are leaving
                    JCStreetViewInstance.setPano(bestLink.pano); // Trigger the move
                    foundValidLink = true; // Mark success
                    // The handlePanoChange listener will schedule the next attempt
                }
            } else {
                log(`Script Move: No suitable link found from ${currentPano} within ${MAX_ANGLE_DIFF} degrees.`);
                // Continue to retry logic below
            }

            // Schedule a retry if no valid move was executed
            if (!foundValidLink) {
                scheduleRetry();
            }

        } catch (e) { errorLog("Error during move attempt:", e); stopMoving(); }
    }

    // Helper to schedule a retry if still moving
    function scheduleRetry() {
        if (isMoving) {
            log(`Scheduling retry attempt in ${STUCK_RETRY_DELAY_MS}ms`);
            if (moveTimeoutId) clearTimeout(moveTimeoutId); // Clear existing timeout first
            moveTimeoutId = setTimeout(attemptMove, STUCK_RETRY_DELAY_MS);
        }
    }

    // --- Manual Logging & Key Listeners ---
    function startManualLogging() { if (!ENABLE_MANUAL_LOG_MODE || isLoggingManual || isMoving) return; if (!JCStreetViewInstance) { errorLog("Instance not ready."); return; } isLoggingManual = true; manualMoveCounter = 0; manualStartTime = Date.now(); lastLoggedManualPano = JCStreetViewInstance.getPano(); log(`MANUAL Logging STARTED...`); }
    function stopManualLogging() { if (!ENABLE_MANUAL_LOG_MODE || !isLoggingManual) return; const endTime = Date.now(); const duration = manualStartTime ? endTime - manualStartTime : 0; isLoggingManual = false; log(`MANUAL Logging STOPPED. Duration: ${duration}ms. Moves: ${manualMoveCounter}.`); manualStartTime = null; lastLoggedManualPano = null; }
    function handleKeyDown(event) { const target = event.target.tagName.toLowerCase(); if (target === 'input' || target === 'textarea' || event.target.isContentEditable) return; if (event.key === 'Shift' && !event.repeat) { startMoving(); } else if (ENABLE_MANUAL_LOG_MODE && event.key === MANUAL_LOG_KEY && !event.repeat && !isLoggingManual) { startManualLogging(); } }
    function handleKeyUp(event) { if (event.key === 'Shift') { stopMoving(); } else if (ENABLE_MANUAL_LOG_MODE && event.key === MANUAL_LOG_KEY && isLoggingManual) { stopManualLogging(); } }

    // --- Initialization ---
    log("Attempting to find and override Google Maps API...");
    findAndOverrideGoogleMaps(overrideStreetViewPanorama);
    log("Adding keydown/keyup event listeners to window.");
    window.addEventListener('keydown', handleKeyDown);
    window.addEventListener('keyup', handleKeyUp);

})();