您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Hold Shift to move quickly, matching spacebar trick speed. Drives towards user heading direction, rather than following pano chain. Disabled in NM / NMPZ modes.
- // ==UserScript==
- // @name Geoguessr Fast Move
- // @description Hold Shift to move quickly, matching spacebar trick speed. Drives towards user heading direction, rather than following pano chain. Disabled in NM / NMPZ modes.
- // @version 3.0
- // @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/
- // @downloadURL
- // @updateURL
- // ==/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 = 160; // 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
- // Attempt to find the instance via DOM element if necessary
- const el = document.querySelector('[class*="street-view-container_root"]'); // Example generic selector
- if (el && el.__panorama && el.__panorama.addListener) { // Check if it looks like a pano instance
- JCStreetViewInstance = el.__panorama;
- log("Found instance via DOM query fallback.");
- }
- }
- 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
- 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) {
- // Ignore key presses if focused on input fields, text areas, or editable content
- const target = event.target;
- const targetTagName = target.tagName.toLowerCase();
- if (targetTagName === 'input' || targetTagName === 'textarea' || target.isContentEditable) {
- return;
- }
- // Handle Shift key for fast movement
- if (event.key === 'Shift' && !event.repeat) {
- // --- NM/NMPZ Check using 'quickplay-variant' ---
- // Read the quickplay variant from localStorage.
- const quickplayVariant = window.localStorage.getItem('quickplay-variant');
- // If variant is "1" (NM) or "2" (NMPZ), do not activate fast move.
- // Note: localStorage stores values as strings.
- if (quickplayVariant === "1" || quickplayVariant === "2") {
- log(`Shift key ignored: Movement disabled in NM/NMPZ mode (variant: ${quickplayVariant}).`);
- return; // Exit without starting movement
- }
- // Log if variant is not 0, 1, or 2 (or null), but proceed anyway as default
- if (quickplayVariant !== "0" && quickplayVariant !== null) {
- log(`Shift key allowed: Unknown quickplay-variant '${quickplayVariant}'. Assuming moving allowed.`);
- } else if (quickplayVariant === "0") {
- log(`Shift key allowed: Moving game detected (variant: ${quickplayVariant}).`);
- } else {
- log(`Shift key allowed: quickplay-variant not found. Assuming moving allowed.`);
- }
- // --- End NM/NMPZ Check ---
- // If checks pass (i.e., not variant 1 or 2), start the fast movement
- startMoving();
- }
- // Handle manual logging key (if enabled)
- else if (ENABLE_MANUAL_LOG_MODE && event.key === MANUAL_LOG_KEY && !event.repeat && !isLoggingManual) {
- startManualLogging();
- }
- }
- function handleKeyUp(event) {
- // Stop fast movement when Shift key is released
- if (event.key === 'Shift') {
- stopMoving();
- }
- // Stop manual logging when its key is released (if enabled and active)
- 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);
- })();