// ==UserScript==
// @name GeoGuessr 5K Counter
// @namespace https://greasyfork.org/en/users/1501889
// @version 1.4
// @description counts 5ks in singleplayer games
// @author Clemens
// @match https://www.geoguessr.com/*
// @icon https://www.google.com/s2/favicons?domain=geoguessr.com
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_getResourceURL
// @resource fiveKImage https://cdn.7tv.app/emote/01JWCSDV98Q7BJDT737J3B8JEN/4x.avif
// @run-at document-end
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const stats = {
total: GM_getValue('geo5k_total', 0),
minimized: GM_getValue('geo5k_minimized', false),
width: GM_getValue('geo5k_width', 150),
height: GM_getValue('geo5k_height', 80),
trackedElements: new WeakSet()
};
function saveStats() {
GM_setValue('geo5k_total', stats.total);
GM_setValue('geo5k_minimized', stats.minimized);
GM_setValue('geo5k_width', stats.width);
GM_setValue('geo5k_height', stats.height);
}
const fiveKImageUrl = GM_getResourceURL('fiveKImage');
GM_addStyle(`
#geo5k-counter {
position: fixed !important;
left: 20px;
top: 75vh;
background: rgba(30, 30, 30, 0.9);
color: #f0f0f0;
padding: 8px 12px;
border-radius: 8px;
font-family: 'Segoe UI', system-ui, sans-serif;
font-size: 14px;
z-index: 99999 !important;
border: 1px solid rgba(255, 255, 255, 0.1);
min-width: 140px;
min-height: 80px;
width: ${stats.width}px;
height: ${stats.height}px;
cursor: move;
user-select: none;
backdrop-filter: blur(4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: all 0.2s ease-in-out;
}
#geo5k-counter.minimized {
width: 16px !important;
height: 16px !important;
min-width: 16px !important;
min-height: 16px !important;
padding: 0 !important;
background: #f00;
border-radius: 50% !important;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
cursor: pointer;
}
#geo5k-counter.minimized > * {
display: none !important;
}
#geo5k-top-bar {
display: flex;
justify-content: space-between;
align-items: center;
}
#geo5k-minimize-btn {
width: 16px;
height: 16px;
background: #f00;
border-radius: 50%;
cursor: pointer;
z-index: 1;
}
#geo5k-reset-btn {
background: #f00;
color: #fff;
border: none;
border-radius: 50%;
width: 16px;
height: 16px;
padding: 0;
font-size: 0;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
#geo5k-reset-btn::after {
content: "R";
font-size: 10px;
font-weight: bold;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#geo5k-reset-btn:hover {
background: #ff6b6b;
}
#geo5k-content {
display: flex;
align-items: flex-start;
justify-content: center;
gap: 12px;
margin-top: 4px;
}
#geo5k-total {
font-weight: 500;
color: #ffffff;
font-size: 36px;
line-height: 48px;
}
.five-k-image {
height: 48px;
width: 48px;
object-fit: contain;
}
#geo5k-resize-handle {
position: absolute;
right: 0;
bottom: 0;
width: 12px;
height: 12px;
cursor: nwse-resize;
background: linear-gradient(135deg, transparent 50%, rgba(255,255,255,0.2) 50%);
}
.geo5k-highlight {
animation: geo5k-pulse 0.5s;
}
@keyframes geo5k-pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
`);
function createDisplay() {
let display = document.getElementById('geo5k-counter');
if (!display) {
display = document.createElement('div');
display.id = 'geo5k-counter';
if (stats.minimized) display.classList.add('minimized');
display.innerHTML = `
<div id="geo5k-top-bar">
<span id="geo5k-minimize-btn"></span>
<button id="geo5k-reset-btn" title="Reset Total Count"></button>
</div>
<div id="geo5k-content">
<img src="${fiveKImageUrl}" class="five-k-image" alt="5K">
<span id="geo5k-total">${stats.total}</span>
</div>
<div id="geo5k-resize-handle"></div>
`;
document.body.appendChild(display);
document.getElementById('geo5k-reset-btn').addEventListener('click', function(e) {
e.stopPropagation();
if (confirm('Reset total 5K count to zero?')) {
stats.total = 0;
stats.trackedElements = new WeakSet();
saveStats();
updateDisplay();
}
});
document.getElementById('geo5k-minimize-btn').addEventListener('click', function(e) {
e.stopPropagation();
toggleMinimize();
});
display.addEventListener('click', function(e) {
if (stats.minimized) {
toggleMinimize();
}
});
setupDragging(display);
setupResize(display);
}
return display;
}
function toggleMinimize() {
stats.minimized = !stats.minimized;
saveStats();
const display = document.getElementById('geo5k-counter');
if (display) {
display.classList.toggle('minimized', stats.minimized);
}
}
function setupDragging(display) {
let isDragging = false;
let startX, startY, initialX, initialY;
const dragElements = [display, document.getElementById('geo5k-minimize-btn')];
dragElements.forEach(element => {
if (!element) return;
element.addEventListener('mousedown', (e) => {
if (e.button !== 0 || e.target.id === 'geo5k-resize-handle') return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
initialX = display.offsetLeft;
initialY = display.offsetTop;
display.style.transition = 'none';
e.preventDefault();
});
});
function moveHandler(e) {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
const newX = Math.max(0, Math.min(initialX + dx, window.innerWidth - display.offsetWidth));
const newY = Math.max(0, Math.min(initialY + dy, window.innerHeight - display.offsetHeight));
display.style.left = `${newX}px`;
display.style.top = `${newY}px`;
}
function upHandler() {
if (!isDragging) return;
isDragging = false;
display.style.transition = '';
}
document.addEventListener('mousemove', moveHandler);
document.addEventListener('mouseup', upHandler);
}
function setupResize(display) {
const resizeHandle = document.getElementById('geo5k-resize-handle');
let isResizing = false;
let startX, startY, startWidth, startHeight;
let debounceTimer;
resizeHandle.addEventListener('mousedown', function(e) {
if (stats.minimized) return;
isResizing = true;
startX = e.clientX;
startY = e.clientY;
startWidth = parseInt(document.defaultView.getComputedStyle(display).width, 10);
startHeight = parseInt(document.defaultView.getComputedStyle(display).height, 10);
display.style.transition = 'none';
e.preventDefault();
e.stopPropagation();
});
function resizeMove(e) {
if (!isResizing) return;
const width = Math.max(140, startWidth + e.clientX - startX);
const height = Math.max(80, startHeight + e.clientY - startY);
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
display.style.width = `${width}px`;
display.style.height = `${height}px`;
}, 1);
}
function resizeEnd() {
if (!isResizing) return;
isResizing = false;
display.style.transition = '';
stats.width = parseInt(display.style.width);
stats.height = parseInt(display.style.height);
saveStats();
}
document.addEventListener('mousemove', resizeMove);
document.addEventListener('mouseup', resizeEnd);
}
function updateDisplay() {
const totalEl = document.getElementById('geo5k-total');
if (totalEl) totalEl.textContent = stats.total;
}
function scanFor5K() {
const scoreContainers = document.querySelectorAll('.round-result_pointsIndicatorWrapper__7JxD_');
for (const container of scoreContainers) {
if (stats.trackedElements.has(container)) continue;
const scoreElement = container.querySelector('.shadow-text_root__KeAY1 div div');
if (!scoreElement) continue;
const scoreText = scoreElement.textContent.trim();
if (scoreText === '5,000' || scoreText === '5000') {
stats.trackedElements.add(container);
handle5KDetection(scoreElement);
}
}
}
function handle5KDetection(element) {
const now = Date.now();
if (now - stats.lastDetection < 50) return;
stats.lastDetection = now;
stats.total++;
saveStats();
updateDisplay();
element.classList.add('geo5k-highlight');
setTimeout(() => element.classList.remove('geo5k-highlight'), 500);
}
function init() {
createDisplay();
updateDisplay();
const fastScanner = setInterval(scanFor5K, 3);
const observer = new MutationObserver(scanFor5K);
observer.observe(document.body, {
childList: true,
subtree: true
});
window.addEventListener('beforeunload', () => {
clearInterval(fastScanner);
observer.disconnect();
});
}
if (document.readyState === 'complete') {
init();
} else {
document.addEventListener('DOMContentLoaded', init);
}
})();