Geoguessr Fast Move

Hold Shift to move quickly, matching spacebar trick speed. Drives towards user heading direction, rather than following pano chain. Disabled in NM / NMPZ modes.

  1. // ==UserScript==
  2. // @name Geoguessr Fast Move
  3. // @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.
  4. // @version 3.0
  5. // @author James C
  6. // @match *://*.geoguessr.com/*
  7. // @run-at document-start
  8. // @icon https://www.google.com/s2/favicons?domain=geoguessr.com
  9. // @grant none
  10. // @license MIT
  11. // @namespace http://tampermonkey.net/
  12. // @downloadURL
  13. // @updateURL
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. // --- Configuration ---
  20. const MOVE_ATTEMPT_DELAY_MS = 10; // Delay AFTER successful pano_changed before attempting next move. (Lower = potentially faster but might need more retries)
  21. const MAX_ANGLE_DIFF = 160; // Angle tolerance for choosing next link. Higher = follows curves easier, Lower = stricter path.
  22. const ENABLE_LOGGING = false; // SET TO FALSE for normal use. Set true for debugging/timing.
  23. const ENABLE_MANUAL_LOG_MODE = false; // SET TO FALSE for normal use. Set true + hold Ctrl to time manual moves.
  24. const MANUAL_LOG_KEY = 'Control'; // Key to hold for manual logging mode.
  25. const STUCK_RETRY_DELAY_MS = 25; // Delay before retrying if no link OR only self-link found. (Lower = recovers faster)
  26. const INSTANCE_WAIT_TIMEOUT = 3000; // Max time (ms) to wait for StreetView instance on startup.
  27. const INSTANCE_CHECK_INTERVAL = 100; // How often (ms) to check for instance if initially missing.
  28. // --- End Configuration ---
  29.  
  30. let JCStreetViewInstance = null;
  31. let isMoving = false;
  32. let previousPanoId = null;
  33. let googleMapsApiLoaded = false;
  34. let scriptStartTime = null;
  35. let scriptMoveCounter = 0;
  36. let isLoggingManual = false;
  37. let manualStartTime = null;
  38. let manualMoveCounter = 0;
  39. let lastLoggedManualPano = null;
  40. let isWaitingForInstance = false;
  41. let moveTimeoutId = null; // Stores setTimeout ID for next attempt
  42.  
  43. // Logging functions
  44. function log(...args) { if (ENABLE_LOGGING) console.log('[FastMove]', ...args); }
  45. function errorLog(...args) { console.error('[FastMove]', ...args); } // Always log errors
  46.  
  47. log("Script starting execution.");
  48.  
  49. // --- Google Maps API Injection & Override ---
  50. function findAndOverrideGoogleMaps(overrider) {
  51. log("Setting up MutationObserver...");
  52. const observer = new MutationObserver((mutations, obs) => {
  53. const googleScript = mutations.flatMap(m => Array.from(m.addedNodes)).find(node => node.tagName === 'SCRIPT' && node.src?.startsWith('https://maps.googleapis.com/'));
  54. if (googleScript) {
  55. log("Google Maps API script tag found:", googleScript.src);
  56. const oldOnload = googleScript.onload;
  57. googleScript.onload = (event) => {
  58. log("Google Maps API script onload event fired.");
  59. if (window.google && window.google.maps && !googleMapsApiLoaded) {
  60. googleMapsApiLoaded = true; obs.disconnect(); log("MutationObserver disconnected.");
  61. try { log("Calling API overrider function."); overrider(window.google); } catch (e) { errorLog("Error during override call:", e); }
  62. } else if (!googleMapsApiLoaded) { log("WARNING: onload fired but maps not found yet."); }
  63. if (typeof oldOnload === 'function') { try { oldOnload.call(googleScript, event); } catch (e) { errorLog("Error calling original onload:", e); } }
  64. };
  65. if (window.google && window.google.maps && !googleMapsApiLoaded) {
  66. log("Google Maps API likely already loaded...");
  67. if(googleScript.onload !== oldOnload || typeof oldOnload !== 'function') { googleScript.onload(null); } else { log("Manual trigger skipped."); }
  68. } else if (!googleMapsApiLoaded) { log("API object not present yet..."); }
  69. }
  70. });
  71. observer.observe(document.documentElement, { childList: true, subtree: true });
  72. }
  73. function overrideStreetViewPanorama(google) {
  74. log("Attempting to override google.maps.StreetViewPanorama...");
  75. if (!google || !google.maps || !google.maps.StreetViewPanorama) { errorLog("Cannot override: google.maps.StreetViewPanorama not found!"); return; }
  76. const original = google.maps.StreetViewPanorama;
  77. google.maps.StreetViewPanorama = class CustomStreetViewPanorama extends original {
  78. constructor(...args) {
  79. log(">>> Custom StreetViewPanorama constructor CALLED.");
  80. super(...args);
  81. JCStreetViewInstance = this;
  82. log(">>> JCStreetViewInstance ASSIGNED:", JCStreetViewInstance ? "Success" : "Failed");
  83. this.addListener('pano_changed', handlePanoChange);
  84. this.addListener('position_changed', () => {}); // Can add logging here if needed
  85. log("Listeners added.");
  86. }
  87. };
  88. log("StreetViewPanorama overridden successfully.");
  89. }
  90.  
  91. // --- Pano Change Handler (Drives the loop) ---
  92. function handlePanoChange() {
  93. const newPanoId = JCStreetViewInstance?.getPano();
  94. if (!newPanoId) return;
  95.  
  96. // Log manual move if applicable
  97. if (ENABLE_MANUAL_LOG_MODE && isLoggingManual && newPanoId !== lastLoggedManualPano) {
  98. manualMoveCounter++; const elapsed = manualStartTime ? Date.now() - manualStartTime : 0;
  99. log(`Manual Move #${manualMoveCounter}: -> ${newPanoId} (Elapsed: ${elapsed}ms)`);
  100. lastLoggedManualPano = newPanoId;
  101. }
  102.  
  103. // If the script is running, schedule the next move attempt
  104. if (isMoving) {
  105. log(`Pano Change Confirmed: ${newPanoId}. Scheduling next move attempt.`);
  106. if (moveTimeoutId) clearTimeout(moveTimeoutId);
  107. moveTimeoutId = setTimeout(attemptMove, MOVE_ATTEMPT_DELAY_MS);
  108. }
  109. }
  110.  
  111. // --- Start/Stop Script Movement ---
  112. function startMoving() {
  113. if (isMoving || isLoggingManual || isWaitingForInstance) return; // Prevent multiple starts/interference
  114. if (!JCStreetViewInstance) {
  115. log("Instance not ready on Shift press. Waiting...");
  116. isWaitingForInstance = true;
  117. const waitStartTime = Date.now();
  118. const intervalId = setInterval(() => {
  119. if (!JCStreetViewInstance) { // Try DOM query as fallback
  120. // Attempt to find the instance via DOM element if necessary
  121. const el = document.querySelector('[class*="street-view-container_root"]'); // Example generic selector
  122. if (el && el.__panorama && el.__panorama.addListener) { // Check if it looks like a pano instance
  123. JCStreetViewInstance = el.__panorama;
  124. log("Found instance via DOM query fallback.");
  125. }
  126. }
  127. if (JCStreetViewInstance) { // Instance found!
  128. clearInterval(intervalId); isWaitingForInstance = false;
  129. log(`Instance ready after ${Date.now() - waitStartTime}ms.`);
  130. executeStartMoving();
  131. } else if (Date.now() - waitStartTime > INSTANCE_WAIT_TIMEOUT) { // Timeout
  132. clearInterval(intervalId); isWaitingForInstance = false;
  133. errorLog(`Instance not found after ${INSTANCE_WAIT_TIMEOUT}ms timeout. Cannot start moving.`);
  134. }
  135. }, INSTANCE_CHECK_INTERVAL);
  136. return; // Wait for interval to succeed or fail
  137. }
  138. executeStartMoving(); // Instance was ready immediately
  139. }
  140.  
  141. function executeStartMoving() {
  142. if (!JCStreetViewInstance) { errorLog("ExecuteStartMoving: Instance missing!"); return; } // Final safety check
  143. isMoving = true;
  144. previousPanoId = null; // Reset previous ID
  145. scriptMoveCounter = 0;
  146. scriptStartTime = Date.now();
  147. const initialPano = JCStreetViewInstance.getPano();
  148. log(`SCRIPT Movement STARTED. Time: ${scriptStartTime}. Initial Pano: ${initialPano || 'Unknown'}`);
  149. log("Triggering first move attempt.");
  150. attemptMove(); // Start the event chain
  151. }
  152.  
  153. function stopMoving() {
  154. if (!isMoving) return; // Only stop if actually moving
  155. const endTime = Date.now();
  156. const duration = scriptStartTime ? endTime - scriptStartTime : 0;
  157. isMoving = false;
  158. if (moveTimeoutId) { // Clear any pending move attempt
  159. clearTimeout(moveTimeoutId);
  160. moveTimeoutId = null;
  161. log("Pending move attempt cleared.");
  162. }
  163. previousPanoId = null;
  164. log(`SCRIPT Movement STOPPED. Time: ${endTime}. Duration: ${duration}ms. Successful moves: ${scriptMoveCounter}.`);
  165. scriptStartTime = null;
  166. }
  167.  
  168. // --- Core Movement Logic ---
  169. function attemptMove() {
  170. moveTimeoutId = null; // Clear ID, as we are executing this attempt
  171.  
  172. // Check essential conditions
  173. if (!isMoving || !JCStreetViewInstance) { if (isMoving) stopMoving(); return; }
  174.  
  175. try {
  176. const currentPano = JCStreetViewInstance.getPano();
  177. if (!currentPano) { log("AttemptMove: Failed to get currentPano."); scheduleRetry(); return; } // Retry if pano ID fails
  178.  
  179. const pov = JCStreetViewInstance.getPov();
  180. if (!pov) { log(`AttemptMove: Failed to get POV for ${currentPano}.`); scheduleRetry(); return; } // Retry if POV fails
  181.  
  182. if (typeof JCStreetViewInstance.getLinks !== 'function') { errorLog("getLinks not a function!"); stopMoving(); return; } // Fatal error
  183. const links = JCStreetViewInstance.getLinks();
  184.  
  185. let currentHeading = pov.heading; currentHeading = (currentHeading % 360 + 360) % 360;
  186. let bestLink = null; let minDiff = MAX_ANGLE_DIFF;
  187. let foundValidLink = false;
  188.  
  189. log(`Attempting move from ${currentPano}. Heading: ${currentHeading.toFixed(1)}`);
  190.  
  191. if (Array.isArray(links)) {
  192. for (const link of links) {
  193. if (!link || typeof link.pano !== 'string' || typeof link.heading !== 'number') continue;
  194. // Skip immediate U-turn
  195. if (link.pano === previousPanoId) { log(` Skipping link to previous: ${link.pano}`); continue; }
  196.  
  197. let linkHeading = link.heading; linkHeading = (linkHeading % 360 + 360) % 360;
  198. let diff = Math.abs(currentHeading - linkHeading); if (diff > 180) diff = 360 - diff;
  199.  
  200. if (diff < minDiff) {
  201. minDiff = diff; bestLink = link;
  202. log(` Found potential link: ${link.pano} (Diff: ${minDiff.toFixed(1)})`);
  203. }
  204. }
  205. } else { log(` No links array found for ${currentPano}.`); }
  206.  
  207. // --- Decision ---
  208. if (bestLink) {
  209. // Avoid moving to the *exact same* pano (can happen with API glitches)
  210. if (bestLink.pano === currentPano) {
  211. log(`Script Move: Avoided self-move ${currentPano}`);
  212. // Continue to retry logic below
  213. } else {
  214. // Execute the valid move
  215. log(`>>> Executing Move: ${currentPano} -> ${bestLink.pano} (Diff: ${minDiff.toFixed(1)})`);
  216. scriptMoveCounter++;
  217. previousPanoId = currentPano; // Record the pano we are leaving
  218. JCStreetViewInstance.setPano(bestLink.pano); // Trigger the move
  219. foundValidLink = true; // Mark success
  220. // The handlePanoChange listener will schedule the next attempt
  221. }
  222. } else {
  223. log(`Script Move: No suitable link found from ${currentPano} within ${MAX_ANGLE_DIFF} degrees.`);
  224. // Continue to retry logic below
  225. }
  226.  
  227. // Schedule a retry if no valid move was executed
  228. if (!foundValidLink) {
  229. scheduleRetry();
  230. }
  231.  
  232. } catch (e) { errorLog("Error during move attempt:", e); stopMoving(); }
  233. }
  234.  
  235. // Helper to schedule a retry if still moving
  236. function scheduleRetry() {
  237. if (isMoving) {
  238. log(`Scheduling retry attempt in ${STUCK_RETRY_DELAY_MS}ms`);
  239. if (moveTimeoutId) clearTimeout(moveTimeoutId); // Clear existing timeout first
  240. moveTimeoutId = setTimeout(attemptMove, STUCK_RETRY_DELAY_MS);
  241. }
  242. }
  243.  
  244. // --- Manual Logging & Key Listeners ---
  245. 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...`); }
  246. 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; }
  247.  
  248. function handleKeyDown(event) {
  249. // Ignore key presses if focused on input fields, text areas, or editable content
  250. const target = event.target;
  251. const targetTagName = target.tagName.toLowerCase();
  252. if (targetTagName === 'input' || targetTagName === 'textarea' || target.isContentEditable) {
  253. return;
  254. }
  255.  
  256. // Handle Shift key for fast movement
  257. if (event.key === 'Shift' && !event.repeat) {
  258. // --- NM/NMPZ Check using 'quickplay-variant' ---
  259. // Read the quickplay variant from localStorage.
  260. const quickplayVariant = window.localStorage.getItem('quickplay-variant');
  261.  
  262. // If variant is "1" (NM) or "2" (NMPZ), do not activate fast move.
  263. // Note: localStorage stores values as strings.
  264. if (quickplayVariant === "1" || quickplayVariant === "2") {
  265. log(`Shift key ignored: Movement disabled in NM/NMPZ mode (variant: ${quickplayVariant}).`);
  266. return; // Exit without starting movement
  267. }
  268. // Log if variant is not 0, 1, or 2 (or null), but proceed anyway as default
  269. if (quickplayVariant !== "0" && quickplayVariant !== null) {
  270. log(`Shift key allowed: Unknown quickplay-variant '${quickplayVariant}'. Assuming moving allowed.`);
  271. } else if (quickplayVariant === "0") {
  272. log(`Shift key allowed: Moving game detected (variant: ${quickplayVariant}).`);
  273. } else {
  274. log(`Shift key allowed: quickplay-variant not found. Assuming moving allowed.`);
  275. }
  276. // --- End NM/NMPZ Check ---
  277.  
  278. // If checks pass (i.e., not variant 1 or 2), start the fast movement
  279. startMoving();
  280. }
  281. // Handle manual logging key (if enabled)
  282. else if (ENABLE_MANUAL_LOG_MODE && event.key === MANUAL_LOG_KEY && !event.repeat && !isLoggingManual) {
  283. startManualLogging();
  284. }
  285. }
  286.  
  287. function handleKeyUp(event) {
  288. // Stop fast movement when Shift key is released
  289. if (event.key === 'Shift') {
  290. stopMoving();
  291. }
  292. // Stop manual logging when its key is released (if enabled and active)
  293. else if (ENABLE_MANUAL_LOG_MODE && event.key === MANUAL_LOG_KEY && isLoggingManual) {
  294. stopManualLogging();
  295. }
  296. }
  297.  
  298. // --- Initialization ---
  299. log("Attempting to find and override Google Maps API...");
  300. findAndOverrideGoogleMaps(overrideStreetViewPanorama);
  301. log("Adding keydown/keyup event listeners to window.");
  302. window.addEventListener('keydown', handleKeyDown);
  303. window.addEventListener('keyup', handleKeyUp);
  304.  
  305. })();