Adds random zooms and screen blackouts in Geoguessr for challenge play. Special thanks to Chhote for the idea!!
// ==UserScript==
// @name Geoguessr Random Zoom Mode GeoDK
// @namespace https://geodk.dev/
// @version 1.0.1
// @description Adds random zooms and screen blackouts in Geoguessr for challenge play. Special thanks to Chhote for the idea!!
// @author Dorukhan Bozkurt (geoDK)
// @match https://www.geoguessr.com/*
// @icon https://www.svgrepo.com/show/40039/eye.svg
// @license MIT
// @grant none
// ==/UserScript==
(function () {
'use strict';
let zoomTime = parseFloat(localStorage.getItem('zoomTime')) || 1.0;
let zoomNumber = parseInt(localStorage.getItem('zoomNumber')) || 3;
let zoomDelay = parseFloat(localStorage.getItem('zoomDelay')) || 0;
let zoomEnabled = localStorage.getItem('zoomEnabled') === 'enabled';
const blinkTime = 500; // ms
const scaleMin = 3.0;
const scaleMax = 5.0;
let wasBackdropThereOrLoading = false;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function simulatePanDrag() {
const canvas = document.querySelector('.widget-scene-canvas');
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const startX = rect.left + rect.width / 2;
const startY = rect.top + rect.height / 2;
const dragDistance = 300;
const angle = Math.random() * 2 * Math.PI;
const endX = startX + Math.cos(angle) * dragDistance;
const endY = startY + Math.sin(angle) * dragDistance;
canvas.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: startX, clientY: startY }));
canvas.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: endX, clientY: endY }));
canvas.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: endX, clientY: endY }));
}
function getPanoramaEl() {
return document.querySelector("[data-qa=panorama]");
}
function applyRandomZoom(pano) {
if (!pano) return;
pano.style.transition = 'none';
const randPercent = () => `${Math.random() * 100}%`;
const zoomLevel = scaleMin + Math.random() * (scaleMax - scaleMin);
pano.style.transformOrigin = `${randPercent()} ${randPercent()}`;
pano.style.transform = `scale(${zoomLevel})`;
}
function resetZoom(pano) {
if (!pano) return;
pano.style.transform = 'scale(1)';
pano.style.transformOrigin = 'center center';
}
function hidePanorama() {
const pano = getPanoramaEl();
if (pano) pano.style.filter = 'brightness(0%)';
}
function showPanorama() {
const pano = getPanoramaEl();
if (pano) pano.style.filter = 'brightness(100%)';
}
function isBackdropThereOrLoading() {
return document.querySelector('[class*=overlay_backdrop__]') ||
document.querySelector('[class*=round-starting_wrapper__]') ||
document.querySelector('[class*=fullscreen-spinner_root__]') ||
document.querySelector('[class*=game-starting_container__]');
}
async function handleRoundStart() {
if (!zoomEnabled) return;
const pano = getPanoramaEl();
if (!pano) return;
resetZoom(pano);
showPanorama();
if (zoomDelay > 0) {
hidePanorama();
await sleep(zoomDelay * 1000);
showPanorama();
}
for (let i = 0; i < zoomNumber; i++) {
simulatePanDrag();
applyRandomZoom(pano);
await sleep(zoomTime * 1000);
if (i < zoomNumber - 1) {
hidePanorama();
await sleep(blinkTime);
showPanorama();
resetZoom(pano);
}
}
hidePanorama();
resetZoom(pano);
}
const observer = new MutationObserver(() => {
addSettingsUI();
if (isBackdropThereOrLoading()) {
wasBackdropThereOrLoading = true;
} else if (wasBackdropThereOrLoading) {
wasBackdropThereOrLoading = false;
handleRoundStart();
}
});
observer.observe(document.body, { childList: true, subtree: true });
function addSettingsUI() {
// For party mode settings
const partyPanel = document.querySelector('[class*=party-modal_heading__]');
const partyColumns = document.querySelectorAll('[class*=settings-modal_column__]');
if (partyPanel && partyColumns.length && !document.getElementById('zoomModeSettings')) {
partyColumns[partyColumns.length - 1].insertAdjacentHTML('beforeend', getSettingsHTML(true));
bindSettingHandlers();
return;
}
// For regular game interface
const gameContainer =
document.querySelector('[class*=map-block_mapStatsContainer__]') ||
document.querySelector('[class*=status_container__]') ||
document.querySelector('[class*=game-map_gameMap__]');
if (!gameContainer || document.getElementById('zoomModeSettings')) return;
gameContainer.insertAdjacentHTML('afterend', getSettingsHTML(false));
bindSettingHandlers();
}
function getSettingsHTML(isPartyMode) {
if (isPartyMode) {
return `
<div id="zoomModeSettings" class="toggle-option_wrapper__">
<div class="toggle-option_label__">Random Zoom Mode</div>
<input type="checkbox" id="zoomEnabled" class="toggle_toggle__">
<div class="numeric-option_wrapper__">
<div class="numeric-option_label__">Zoom Time (s)</div>
<input type="number" id="zoomTime" value="${zoomTime}" min="0.1" step="0.1">
</div>
<div class="numeric-option_wrapper__">
<div class="numeric-option_label__">Zoom Number</div>
<input type="number" id="zoomNumber" value="${zoomNumber}" min="1" step="1">
</div>
<div class="numeric-option_wrapper__">
<div class="numeric-option_label__">Start Delay (s)</div>
<input type="number" id="zoomDelay" value="${zoomDelay}" min="0" step="0.1">
</div>
</div>`;
} else {
return `
<div id="zoomModeSettings" style="position: absolute; bottom: 20px; left: 20px; background: rgba(255, 255, 255, 0.05); padding: 12px; border-radius: 12px; z-index: 999; backdrop-filter: blur(4px); color: white; width: 220px; font-family: inherit; box-shadow: 0 0 8px rgba(0,0,0,0.2);">
<h3 style="margin-bottom: 10px; font-size: 16px; text-align: center;">🎥 geoDK's Random Zoom Mode</h3>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<label style="margin-right: 10px;">Enabled</label>
<input type="checkbox" id="zoomEnabled">
</div>
<div style="margin-bottom: 10px;">
<label style="display: block; margin-bottom: 4px;">Zoom Time (s)</label>
<input type="number" id="zoomTime" value="${zoomTime}" min="0.1" step="0.1" style="width: 100%;">
</div>
<div style="margin-bottom: 10px;">
<label style="display: block; margin-bottom: 4px;">Zoom Number</label>
<input type="number" id="zoomNumber" value="${zoomNumber}" min="1" step="1" style="width: 100%;">
</div>
<div>
<label style="display: block; margin-bottom: 4px;">Start Delay (s)</label>
<input type="number" id="zoomDelay" value="${zoomDelay}" min="0" step="0.1" style="width: 100%;">
</div>
</div>`;
}
}
function bindSettingHandlers() {
const enabledEl = document.getElementById('zoomEnabled');
if (!enabledEl) return;
enabledEl.checked = zoomEnabled;
enabledEl.addEventListener('change', e => {
zoomEnabled = e.target.checked;
localStorage.setItem('zoomEnabled', zoomEnabled ? 'enabled' : 'disabled');
});
document.getElementById('zoomTime').addEventListener('change', e => {
zoomTime = parseFloat(e.target.value);
localStorage.setItem('zoomTime', zoomTime);
});
document.getElementById('zoomNumber').addEventListener('change', e => {
zoomNumber = parseInt(e.target.value);
localStorage.setItem('zoomNumber', zoomNumber);
});
document.getElementById('zoomDelay').addEventListener('change', e => {
zoomDelay = parseFloat(e.target.value);
localStorage.setItem('zoomDelay', zoomDelay);
});
}
})();