GeoGuessr Retry Hotkey

Quickly resets the game by navigating to the last visited map and starting a new game.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         GeoGuessr Retry Hotkey
// @namespace    https://www.geoguessr.com/
// @version      1.4
// @description  Quickly resets the game by navigating to the last visited map and starting a new game.
// @author       Shukaaa (aduchi nom)
// @match        https://www.geoguessr.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        window.focus
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // Constants for script metadata, storage keys
    const SCRIPT_NAME = "GeoGuessr Retry Hotkey";
    const LAST_VISITED_MAP_KEY = "lastVisitedMap";
    const PLAY_TRIGGERED_KEY = "playTriggered";
    const RESET_KEY_STORAGE = "resetKey";
    const DELAY_STORAGE = "rh-delay";

    // Retry Hotkey
    let RESET_KEY = localStorage.getItem(RESET_KEY_STORAGE) || "p";
    let DELAY = localStorage.getItem(DELAY_STORAGE) || 50;

    // Helper: Logging function for consistent console outputs
    const log = (message, level = "log") => {
        const levels = {
            log: console.log,
            warn: console.warn
        };
        const logFunction = levels[level] || console.log;
        logFunction(`[${SCRIPT_NAME}] ${message}`);
    };

    // Helper: Save to GM storage
    const saveToStorage = (key, value) => GM_setValue(key, value);

    // Helper: Load from GM storage
    const loadFromStorage = (key, defaultValue = null) => GM_getValue(key, defaultValue);

    // Handle keydown for resetting the game
    const handleKeyDown = (event) => {
        const currentURL = window.location.href;

        // Trigger reset
        if (currentURL.includes("/game/") && event.key === RESET_KEY) {
            const lastVisitedMap = loadFromStorage(LAST_VISITED_MAP_KEY);

            if (lastVisitedMap) {
                log(`'${RESET_KEY.toUpperCase()}' key pressed. Navigating to: ${lastVisitedMap}`);
                saveToStorage(PLAY_TRIGGERED_KEY, true); // Set playTriggered to true
                window.location.href = lastVisitedMap; // Redirect to the map
            } else {
                log("No last map URL found. Reset aborted.", "warn");
            }
        }

        if (!currentURL.includes("/game/")) {
            log("Reset aborted: Not on a game page.", "warn");
        }
    };

    // Automatically click the play button on the map page
    const attemptPlay = () => {
        const currentURL = window.location.href;
        const playTriggered = loadFromStorage(PLAY_TRIGGERED_KEY, false);

        if (currentURL.includes("/maps/") && playTriggered) {
            log("Map page detected with playTriggered=true. Attempting to start a new game...");

            // Try finding the play button container
            const playButtonContainer = document.querySelector("div[class*='map-selector_playButtons']");
            if (playButtonContainer) {
                const playButton = playButtonContainer.querySelector("button");
                if (playButton) {
                    saveToStorage(PLAY_TRIGGERED_KEY, false);
                    playButton.focus();
                    setTimeout(() => {
                        playButton.click();
                    }, DELAY);
                } else {
                    log("'Play' button not found inside container.", "warn");
                }
            } else {
                log("Play button container not found.", "warn");
            }
        }
    };

    // Save the current map URL if on a map page
    const saveCurrentMap = () => {
        const currentURL = window.location.href;
        if (currentURL.includes("/maps/")) {
            log(`Saving current map URL: ${currentURL}`);
            saveToStorage(LAST_VISITED_MAP_KEY, currentURL);
        }
    };

    // Check for URL Changes
    const observeUrlChanges = () => {
        let lastUrl = window.location.href;

        const observer = new MutationObserver(() => {
            const currentUrl = window.location.href;

            if (currentUrl !== lastUrl) {
                log(`URL changed: ${currentUrl}`);
                lastUrl = currentUrl;

                // Save the map URL if it's a map page
                saveCurrentMap();
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    };

    const addConfigurationsToMenuOverlay = () => {
        const SETTINGS_INITIALIZED_ATTR = "data-retry-hotkey-settings-initialized";

        // Utility functions for creating reusable components made by geoguessr
        const cloneComponent = (selector, modifier = (clone) => clone) => {
            const component = document.querySelector(selector);
            if (!component) return null;
            const clone = component.cloneNode(true);
            return modifier(clone);
        };

        const createTitle = (title) =>
        cloneComponent("div[class*='game-menu_headerContainer']", (clone) => {
            clone.childNodes[0].innerHTML = title;
            return clone;
        });

        const createDivider = () => cloneComponent("div[class*='game-menu_divider']");

        const createOptionContainer = (optionName) =>
        cloneComponent("div[class*='game-menu_volumeContainer']", (clone) => {
            clone.removeChild(clone.lastChild);
            clone.childNodes[0].innerHTML = optionName;
            return clone;
        });

        const createButton = (text, onClick) => {
            const button = document.createElement("button");
            button.style.cursor = "pointer";
            button.style.color = "#fff";
            button.style.border = ".0625rem solid var(--ds-color-white-80)";
            button.style.padding = "0.75rem 1.5rem";
            button.style.borderRadius = "3.75rem";
            button.style.marginTop = "1em";
            button.innerHTML = text;
            if (onClick) button.onclick = onClick;
            return button;
        };

        const createBlockquote = (text) => {
            const blockquote = document.createElement("blockquote");
            blockquote.style.borderLeft = ".333rem solid #ccc";
            blockquote.style.color = "#ccc";
            blockquote.style.marginLeft = "0";
            blockquote.style.paddingLeft = "0.5em";
            blockquote.innerHTML = text
            return blockquote
        }

        const initializeSettingsMenu = () => {
            const settingsContainer = document.querySelector("div[class*='game-menu_settingsContainer']");
            if (!settingsContainer || settingsContainer.hasAttribute(SETTINGS_INITIALIZED_ATTR)) return;

            settingsContainer.appendChild(createDivider());
            settingsContainer.appendChild(createTitle("Retry Hotkey Settings"));

            const switchHotkeySetting = createOptionContainer("Hotkey");
            const updateHotkeyButton = createButton("Set new hotkey (Current Hotkey: " + RESET_KEY.toUpperCase() + ")", () => {
                updateHotkeyButton.innerHTML = "Press a new key to set as the hotkey";
                updateHotkeyButton.style.color = "#ccc";
                updateHotkeyButton.disabled = true;

                const handleNewKey = (event) => {
                    RESET_KEY = event.key;
                    localStorage.setItem(RESET_KEY_STORAGE, RESET_KEY);
                    log(`New hotkey set: ${RESET_KEY}`);
                    alert(`New hotkey set to: ${RESET_KEY.toUpperCase()}`);

                    updateHotkeyButton.disabled = false;
                    updateHotkeyButton.style.color = "#fff";
                    updateHotkeyButton.innerHTML = "Set new hotkey (Current Hotkey: " + RESET_KEY.toUpperCase() + ")";
                    document.removeEventListener("keydown", handleNewKey);
                };

                document.addEventListener("keydown", handleNewKey);
            });

            switchHotkeySetting.appendChild(updateHotkeyButton);

            const changeDelaySetting = createOptionContainer("Delay");
            const changeDelayInfoText = createBlockquote("When dealing with bad internet connection or bugs that the match won't automatically start, it can be helpful to increase the delay")
            const changeDelayButton = createButton("Change Delay (Current Delay: " + DELAY + "ms)", () => {
                const newDelay = prompt("Enter new delay in ms")

                if (isNaN(newDelay)) {
                    alert("Input is not a number")
                    return
                }

                DELAY = Number(newDelay)
                localStorage.setItem(DELAY_STORAGE, DELAY);
                log(`New delay set: ${DELAY}`);
                alert(`New delay set to: ${DELAY}`);
                changeDelayButton.innerHTML = "Change Delay (Current Delay: " + DELAY + "ms)"
            });

            changeDelaySetting.appendChild(changeDelayInfoText);
            changeDelaySetting.appendChild(changeDelayButton);

            settingsContainer.appendChild(switchHotkeySetting);
            settingsContainer.appendChild(changeDelaySetting);
            settingsContainer.setAttribute(SETTINGS_INITIALIZED_ATTR, true);
        };

        initializeSettingsMenu();
    };

    const observeSettingsView = () => {
        const observer = new MutationObserver(() => {
            const settingsView = document.querySelector("div[class*='game-menu_inGameMenuOverlay']");
            if (settingsView) {
                log("Settings view loaded.");
                addConfigurationsToMenuOverlay()
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    };

    // Initialize script
    const initialize = () => {
        document.addEventListener("keydown", handleKeyDown); // Add keydown listener
        attemptPlay(); // Check if play needs to be triggered
        saveCurrentMap(); // Save the map URL if relevant
        observeUrlChanges(); // Start observing DOM and URL changes
        observeSettingsView(); // Start observing the menu overlay view to add own settings
    };

    // Run the script
    initialize();
})();