NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Geoguessr Yandex Plugin // @namespace kommu // @description Play Geoguessr with Yandex Panoramas. A big shout out to @MrAmericanMike who help me coding this script and to @Alok who had the idea and help us to test the script // @version 1.0.1 // @include https://www.geoguessr.com/* // @run-at document-start // @updateURL https://openuserjs.org/meta/kommu/Geoguessr_Yandex_Plugin.meta.js // @license MIT // ==/UserScript== const YANDEX_API_KEY = ""; function myLog(...args) { console.log(...args); } function myHighlight(...args) { console.log(`%c${[...args]}`, "color: dodgerblue; font-size: 24px;"); } myLog("Geoguessr Yandex Plugin"); const MAP_NAME_INCLUDES = "Yandex"; const PANORAMA_MAP_NAME_INCLUDES = "Yandex Air Panorama"; const MAPS_API_URL = "https://maps.googleapis.com/maps/api/js?"; let PLAYER = null; let ROUND = 0; let YANDEX_INJECTED = false; let NEW_ROUND_LOADED = false; let COMPASS = null; let PANORAMA_MAP = false; let CURRENT_ROUND_DATA = null; /** * Resolves succesfully after detecting that Google Maps API was loaded in a page * * @returns Promise */ function gmp() { return new Promise((resolve, reject) => { let scriptObserver = new MutationObserver((mutations, observer) => { for (let mutation of mutations) { for (let node of mutation.addedNodes) { if (node.tagName === "SCRIPT" && node.src.startsWith(MAPS_API_URL)) { scriptObserver.disconnect(); scriptObserver = undefined; myLog("Detected Google Maps API"); node.onload = () => resolve(); } } } }); let bodyDone = false; let headDone = false; let injectorObserver = new MutationObserver((mutations, observer) => { if (!bodyDone && document.body) { bodyDone = true; myLog("Body Observer Injected"); scriptObserver && scriptObserver.observe(document.body, { childList: true }); } if (!headDone && document.head) { headDone = true; myLog("Head Observer Injected"); scriptObserver && scriptObserver.observe(document.head, { childList: true }); } if (headDone && bodyDone) { myLog("Body and Head Observers Injected"); observer.disconnect(); } }); injectorObserver.observe(document.documentElement, { childList: true, subtree: true }); }); } /** * Once the Google Maps API was loaded we can do more stuff */ gmp().then(() => { launchObserver(); }); /** * This observer stays alive while the script is running */ function launchObserver() { myHighlight("Main Observer"); const OBSERVER = new MutationObserver((mutations, observer) => { detectGamePage(); }); OBSERVER.observe(document.head, { attributes: true, childList: true, subtree: true }); } /** * Detects if the current page contains /game/ or /challenge/ in it * If it doesn't it sets: * ROUND = 0 * PLAYER = null * COMPASS = null */ function detectGamePage() { const PATHNAME = window.location.pathname; if (PATHNAME.startsWith("/game/") || PATHNAME.startsWith("/challenge/")) { myLog("Game page"); checkRound(); } else { myLog("Not a Game page"); ROUND = 0; PLAYER = null; COMPASS = null; } } /** * Checks for round changes */ function checkRound() { let currentRound = getRoundFromPage(); if (ROUND != currentRound) { myHighlight("New round"); ROUND = currentRound; NEW_ROUND_LOADED = true; COMPASS = null; getMapData(); } } /** * Gets data for the current map and checks if name contains `MAP_NAME_INCLUDES` * If so it continues calling other functions, otherway it does nothing else */ function getMapData() { getSeed().then((data) => { myHighlight("Seed data"); myLog(data); if (data.mapName.includes(MAP_NAME_INCLUDES)) { PANORAMA_MAP = (data.mapName.includes(PANORAMA_MAP_NAME_INCLUDES)); myHighlight("Yandex Map"); hideGoogleCanvas(); hideDefaultZoomControls(); injectYandexCanvas(); CURRENT_ROUND_DATA = data; if (PLAYER) { goToLocation(data); } else { injectYandexScript().then(() => { myLog("Ready to inject player"); injectYandexPlayer(data); }).catch((error) => { myLog(error); }); } } }).catch((error) => { myLog(error); }); } /** * Gets the seed data for the current game * * @returns Promise with seed data as object */ function getSeed() { myLog("getSeed called"); return new Promise((resolve, reject) => { let token = getToken(); let URL; const PATHNAME = window.location.pathname; if (PATHNAME.startsWith("/game/")) { URL = `https://www.geoguessr.com/api/v3/games/${token}`; } else if (PATHNAME.startsWith("/challenge/")) { URL = `https://www.geoguessr.com/api/v3/challenges/${token}/game`; } fetch(URL) .then((response) => response.json()) .then((data) => { resolve(data); }) .catch((error) => { reject(error); }); }); } /** * Gets the token from the current URL * * @returns token */ function getToken() { const PATHNAME = window.location.pathname; if (PATHNAME.startsWith("/game/")) { return PATHNAME.replace("/game/", ""); } else if (PATHNAME.startsWith("/challenge/")) { return PATHNAME.replace("/challenge/", ""); } } /** * Gets the round number from the ongoing game from the page itself * * @returns Round number */ function getRoundFromPage() { const roundData = document.querySelector("div[data-qa='round-number']"); if (roundData) { let roundElement = roundData.querySelector("div:last-child"); if (roundElement) { let round = parseInt(roundElement.innerText.charAt(0)); if (!isNaN(round) && round >= 1 && round <= 5) { return round; } } } else { return ROUND; } } /** * Hides Google Canvas */ function hideGoogleCanvas() { const GOOGLE_MAPS_CANVAS = document.querySelector(".game-layout__panorama-canvas"); GOOGLE_MAPS_CANVAS.style.display = "none"; myLog("Google Canvas hidden"); } /** * Hides default zoom controls and Yandex GOTO link */ function hideDefaultZoomControls() { let style = ` .ymaps-2-1-79-panorama-gotoymaps {display: none !important;} .game-layout__controls {bottom: 8rem !important; left: 1rem !important;} .game-layout__controls .styles_controlGroup__2pd1f, .styles_horizontalControls__ewOLi {display: none !important;} `; let style_element = document.createElement("style"); style_element.innerHTML = style; document.body.appendChild(style_element); } /** * Injects Yandex Canvas */ function injectYandexCanvas() { const GAME_CANVAS = document.querySelector(".game-layout__panorama"); GAME_CANVAS.id = "yandex_player"; myLog("Yandex Canvas injected"); } /** * Injects Yandex Script */ function injectYandexScript() { return new Promise((resolve, reject) => { if (!YANDEX_INJECTED) { if (YANDEX_API_KEY === "") { let canvas = document.getElementById("yandex_player"); canvas.innerHTML = ` <div style="text-align: center;"> <h1 style="margin-top: 80px; font-size: 48px;">YOU NEED YANDEX API KEY<h1> <p><a target="_blank" href="https://yandex.com/dev/maps/jsapi/doc/2.1/quick-start/index.html?from=techmapsmain">Get it here</a></p> <br/> <p>After that you need to add that key into this script in</p> <code>const YANDEX_API_KEY = "";</code> </div> `; reject(); } else { const SCRIPT = document.createElement("script"); SCRIPT.type = "text/javascript"; SCRIPT.async = true; SCRIPT.onload = () => { ymaps.ready(() => { YANDEX_INJECTED = true; myHighlight("Yandex API Loaded"); resolve(); }); } SCRIPT.src = `https://api-maps.yandex.ru/2.1/?lang=en_US&apikey=${YANDEX_API_KEY}`; document.body.appendChild(SCRIPT); } } else { resolve(); } }); } /** * Injects Yandex Player and calls handleReturnToStart */ function injectYandexPlayer(data) { const lat = data.rounds[data.round - 1].lat; const lng = data.rounds[data.round - 1].lng; if (PLAYER === null) { let options = { "direction": [0, 16], "span": [10, 67], "controls": ["zoomControl"] }; if (PANORAMA_MAP) { options.layer = 'yandex#airPanorama'; } ymaps.panorama.createPlayer("yandex_player", [lat, lng], options) .done((player) => { PLAYER = player; PLAYER.events.add("directionchange", (e) => { updateCompass(data); }); myHighlight("Player injected"); }); } else { goToLocation(data); } } /** * Goes to location when PLAYER already exists * * @param {*} data */ function goToLocation(data) { myLog("Going to location"); const lat = data.rounds[data.round - 1].lat; const lng = data.rounds[data.round - 1].lng; let options = {}; if (PANORAMA_MAP) { options.layer = 'yandex#airPanorama'; } PLAYER.moveTo([lat, lng], options); PLAYER.setDirection([0, 16]); PLAYER.setSpan([10, 67]); } /** * Updates the compass to match Yandex Panorama facing */ function updateCompass() { if (!COMPASS) { let compass = document.querySelector("img.compass__indicator"); if (compass != null) { COMPASS = compass; let direction = PLAYER.getDirection()[0] * -1; COMPASS.setAttribute("style", `transform: rotate(${direction}deg);`); } handleReturnToStart(); } else { let direction = PLAYER.getDirection()[0] * -1; COMPASS.setAttribute("style", `transform: rotate(${direction}deg);`); } } /** * Injects behavior to go back to start when the return to start flag is clicked * * @param {*} data */ function handleReturnToStart() { let flag = document.querySelector("button[data-qa='return-to-start']"); if (flag != null) { myLog("Return to start attached"); let FLAG = flag; FLAG.addEventListener("click", () => { goToLocation(CURRENT_ROUND_DATA); }); } }