Hold Shift to move quickly, matching spacebar trick speed. Drives towards user heading direction, rather than following pano chain.
当前为
// ==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);
})();