// ==UserScript==
// @name Pinpointing Duels
// @namespace http://tampermonkey.net/
// @version 1.1.5
// @description Hide health bars, insert score divs, update scores with improved performance, custom tie-breaking, auto-activation on URL change, update round header, and display an end screen for best-of-13 games. Recommended settings: 15 round max, 1m30s round timer, 60s guess timer, 10000000 max health, no damage multipliers, no healing round
// @match https://www.geoguessr.com/*
// @icon https://i.imgur.com/2Rz2axY.png
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// @require https://update.greasyfork.org/scripts/460322/1408713/Geoguessr%20Styles%20Scan.js
// ==/UserScript==
(function() {
GM_registerMenuCommand("Set Best of X", showBestOfPanel);
GM_registerMenuCommand("Set Tie Range", showTieRangePanel);
function showBestOfPanel() {
if (document.getElementById('settings-panel')) return;
const currentValue = GM_getValue('bestOf', 13); // Default to 13
const panel = document.createElement('div');
panel.id = 'settings-panel';
panel.style.cssText = `
position: fixed;
top: 100px;
right: 100px;
background: #171717;
color: white;
padding: 20px;
z-index: 10000;
border-radius: 10px;
font-family: sans-serif;
`;
const label = document.createElement('label');
label.innerText = "Best of: ";
label.style.marginRight = "10px";
const select = document.createElement('select');
for (let x = 3; x <= 30; x += 2) {
const option = document.createElement('option');
option.value = x;
option.innerText = x;
if (x == currentValue) option.selected = true;
select.appendChild(option);
}
const saveBtn = document.createElement('button');
saveBtn.innerText = "Save";
saveBtn.style.margin = "10px";
saveBtn.style.color = "white";
saveBtn.onclick = () => {
GM_setValue('bestOf', parseInt(select.value));
alert('Saved! Best of: ' + select.value);
panel.remove();
};
const cancelBtn = document.createElement('button');
cancelBtn.innerText = "Cancel";
cancelBtn.style.color = "white";
cancelBtn.onclick = () => panel.remove();
panel.appendChild(label);
panel.appendChild(select);
panel.appendChild(saveBtn);
panel.appendChild(cancelBtn);
document.body.appendChild(panel);
}
function showTieRangePanel() {
if (document.getElementById('settings-panel')) return;
const currentValue = GM_getValue('tieRange', 0); // Default to 13
const panel = document.createElement('div');
panel.id = 'settings-panel';
panel.style.cssText = `
position: fixed;
top: 100px;
right: 100px;
background: #171717;
color: white;
padding: 20px;
z-index: 10000;
border-radius: 10px;
font-family: sans-serif;
`;
const label = document.createElement('label');
label.innerText = "Use Tie Range: ";
label.style.marginRight = "10px";
const select = document.createElement('select');
for (let x = 0; x <= 1; x += 1) {
const option = document.createElement('option');
if(x ==0){
option.value = x;
option.innerText = "False";
}
if(x == 1){
option.value = x;
option.innerText = "True";
}
if (x == currentValue) option.selected = true;
select.appendChild(option);
}
const saveBtn = document.createElement('button');
saveBtn.innerText = "Save";
saveBtn.style.margin = "10px";
saveBtn.style.color = "white";
saveBtn.onclick = () => {
GM_setValue('tieRange', parseInt(select.value));
if (select.value == 0){
alert('Saved! Tie Range: False');
}
if (select.value == 1){
alert('Saved! Tie Range: True');
}
panel.remove();
};
const cancelBtn = document.createElement('button');
cancelBtn.innerText = "Cancel";
cancelBtn.style.color = "white";
cancelBtn.onclick = () => panel.remove();
panel.appendChild(label);
panel.appendChild(select);
panel.appendChild(saveBtn);
panel.appendChild(cancelBtn);
document.body.appendChild(panel);
}
'use strict';
// Inject custom CSS for the overlay backdrop and active round wrapper to use your default image.
const customStyles = `
.overlay_backdrop__ueiEF,
.views_activeRoundWrapper__1_J5M {
background-image: url('https://i.imgur.com/vzIi2Wl.jpeg') !important;
background-position: center !important;
background-size: cover !important;
background-repeat: no-repeat !important;
}
`;
const styleElem = document.createElement('style');
styleElem.textContent = customStyles;
document.head.appendChild(styleElem);
const bestOf = GM_getValue('bestOf', 13); // Fallback 13
const winThreshold = Math.ceil(bestOf / 2); //Win Value
const tieRange = GM_getValue('tieRange', 0); // Fallback 13
let leftScore = 0, rightScore = 0;
let gameOver = false; // Flag to ensure the end screen is only shown once
let currentDuel = false; // Flag to ensure the end screen is only shown once
// Extract the logged-in player's ID from __NEXT_DATA__
const getLoggedInUserId = () => {
const element = document.getElementById("__NEXT_DATA__");
if (!element) return null;
let exto = JSON.parse(element.innerText).props.accountProps.account.user.userId
return exto;
};
// Determine which team (0 or 1) the logged-in user belongs to.
const getLoggedInUserTeamIndex = (teams, loggedInUserId) => {
for (let i = 0; i < teams.length; i++) {
if (teams[i].players && teams[i].players.some(player => player.playerId === loggedInUserId)) {
return i;
}
}
return null;
};
// Return one of: "YOU WIN THE ROUND", "OPPONENT WINS THE ROUND", or "TIE" for the last completed round.
const getRoundWinnerText = (response) => {
//No Tie Range Scoring
if (tieRange == 0){
if (!response.teams || response.teams.length < 2 || response.currentRoundNumber < 1) {
return "";
}
const roundIndex = response.currentRoundNumber - 1;
const team0Score = response.teams[0]?.roundResults?.[roundIndex]?.score || 0;
const team1Score = response.teams[1]?.roundResults?.[roundIndex]?.score || 0;
let winningTeam = null;
if (team0Score > team1Score) {
winningTeam = 0;
} else if (team1Score > team0Score) {
winningTeam = 1;
} else if (team0Score === 5000) {
const team0Time = response.teams[0]?.roundResults?.[roundIndex]?.bestGuess?.created || Infinity;
const team1Time = response.teams[1]?.roundResults?.[roundIndex]?.bestGuess?.created || Infinity;
if (team0Time < team1Time) {
winningTeam = 0;
} else if (team1Time < team0Time) {
winningTeam = 1;
}
}
const loggedInUserId = getLoggedInUserId();
const userTeamIndex = getLoggedInUserTeamIndex(response.teams, loggedInUserId);
if (winningTeam === null) {
return "TIE";
} else if (winningTeam === userTeamIndex) {
if (leftScore == winThreshold-1 || rightScore == winThreshold-1) {
return "YOU WIN THE ROUND! MATCH POINT!";
}
else {
return "YOU WIN THE ROUND!"
}
} else {
if (leftScore == winThreshold-1 || rightScore == winThreshold-1) {
return "OPPONENT WINS THE ROUND! MATCH POINT!";
}
else {
return "OPPONENT WINS THE ROUND!"
}
}
}
//Tie Range Scoring
if (tieRange == 1){
let tieDistance = 0
if (!response.teams || response.teams.length < 2 || response.currentRoundNumber < 1) {
return "";
}
const roundIndex = response.currentRoundNumber - 1;
const team0Score = response.teams[0]?.roundResults?.[roundIndex]?.score || 0;
const team1Score = response.teams[1]?.roundResults?.[roundIndex]?.score || 0;
let winningTeam = null;
if (team0Score > team1Score) {
tieDistance = (5000 - team0Score)
if((team0Score - tieDistance) > team1Score){
winningTeam = 0;
}
} else if (team1Score > team0Score) {
tieDistance = (5000 - team1Score)
if((team1Score - tieDistance) > team0Score){
winningTeam = 1;
}
} else if (team0Score === 5000) {
const team0Time = response.teams[0]?.roundResults?.[roundIndex]?.bestGuess?.created || Infinity;
const team1Time = response.teams[1]?.roundResults?.[roundIndex]?.bestGuess?.created || Infinity;
if (team0Time < team1Time) {
winningTeam = 0;
} else if (team1Time < team0Time) {
winningTeam = 1;
}
}
const loggedInUserId = getLoggedInUserId();
const userTeamIndex = getLoggedInUserTeamIndex(response.teams, loggedInUserId);
if (winningTeam === null) {
return "TIE! TIE RANGE: " + tieDistance + " POINTS";
} else if (winningTeam === userTeamIndex) {
if (leftScore == winThreshold-1 || rightScore == winThreshold-1) {
return "YOU WIN THE ROUND! MATCH POINT! TIE RANGE: " + tieDistance + " POINTS";
}
else {
return "YOU WIN THE ROUND! TIE RANGE: " + tieDistance + " POINTS"
}
} else {
if (leftScore == winThreshold-1 || rightScore == winThreshold-1) {
return "OPPONENT WINS THE ROUND! MATCH POINT! TIE RANGE: " + tieDistance + " POINTS";
}
else {
return "OPPONENT WINS THE ROUND! TIE RANGE: " + tieDistance + " POINTS"
}
}
}
};
// Remove unwanted UI elements and ensure our score display exists.
const modifyHealthBars = () => {
const healthContainer = document.querySelector("." + cn("hud_root__"));
if (!healthContainer) return;
document.querySelectorAll('[class*="health-bar_barInner__"]').forEach(bar => bar.style.display = "none");
document.querySelectorAll('[class*="health-bar_slant__"]').forEach(slant => slant.style.display = "none");
document.querySelectorAll("." + cn("health-bar_playerContainer__")).forEach(container => container.style.top = "0.5rem");
document.querySelectorAll("." + cn("health-bar_container__")).forEach(container => container.style.setProperty("--bar-container-width", "15rem"));
document.querySelectorAll("." + cn("health-bar_barInnerContainer__")).forEach(container => container.style.background = "none");
if (!document.getElementById("leftScore") || !document.getElementById("rightScore")) {
createScoreDisplays();
}
};
// Create score display divs.
const createScoreDisplays = () => {
const hudRoot = document.querySelector("." + cn("hud_root__"));
if (!hudRoot) return;
const createScoreDiv = (id, position) => {
const div = document.createElement("div");
div.id = id;
div.innerText = (id === "leftScore") ? leftScore : rightScore;
div.style.cssText = `
padding: 10px 20px;
font-size: 36px;
font-weight: bold;
color: white;
background: linear-gradient(180deg,rgba(131,125,187,.6),rgba(131,125,187,0) 75%),#3c2075;
border-radius: 5px;
text-align: center;
margin: 5px;
position: absolute;
top: 20px;
${position}: 320px;
z-index: 1000;
`;
return div;
};
hudRoot.appendChild(createScoreDiv("leftScore", "left"));
hudRoot.appendChild(createScoreDiv("rightScore", "right"));
};
// Create and display the end screen overlay.
const showEndScreen = () => {
const overlay = document.createElement("div");
overlay.id = "endScreenOverlay";
overlay.style.cssText = `
position: fixed;
top: 0; left: 0;
width: 100vw; height: 100vh;
background: linear-gradient(180deg,rgba(6,43,20,1),rgba(11,65,43,1) 95%),#062b14;
color: #5adb95;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
text-align: center;
`;
const winnerText = (leftScore >= winThreshold) ? "YOU WIN THE GAME!" : "OPPONENT WINS THE GAME!";
const scoreText = `${leftScore}-${rightScore}`;
const winnerElem = document.createElement("div");
winnerElem.innerText = winnerText;
winnerElem.style.cssText = `
font-size: 72pt;
margin-bottom: 20px;
`;
const scoreElem = document.createElement("div");
scoreElem.innerText = scoreText;
scoreElem.style.cssText = `
font-size: 90pt;
margin-bottom: 20px;
color: white;
`;
const messageElem = document.createElement("div");
messageElem.innerText = "Please guess Antarctica for the remaining rounds so the game is saved and you get a summary link.";
messageElem.style.cssText = `
font-size: 24pt;
margin-bottom: 20px;
color: white;
`;
const button = document.createElement("button");
button.innerText = "Okay";
button.className = cn("button_button__") + " " + cn("button_variantPrimary__"); // Add the classes
button.addEventListener("click", () => {
overlay.remove();
});
overlay.appendChild(winnerElem);
overlay.appendChild(scoreElem);
overlay.appendChild(messageElem);
overlay.appendChild(button);
document.body.appendChild(overlay);
};
// Update cumulative scores and update round header text.
const updateScores = (response) => {
if(tieRange == 0){
let newTeam0Score = 0, newTeam1Score = 0;
if (response.teams && response.teams.length >= 2) {
for (let i = 0; i < response.currentRoundNumber; i++) {
const team0RoundScore = response.teams[0]?.roundResults?.[i]?.score || 0;
const team1RoundScore = response.teams[1]?.roundResults?.[i]?.score || 0;
if (team0RoundScore > team1RoundScore) {
newTeam0Score++;
} else if (team1RoundScore > team0RoundScore) {
newTeam1Score++;
} else if (team0RoundScore === 5000) {
const team0Time = response.teams[0]?.roundResults?.[i]?.bestGuess?.created || Infinity;
const team1Time = response.teams[1]?.roundResults?.[i]?.bestGuess?.created || Infinity;
if (team0Time < team1Time) {
newTeam0Score++;
} else if (team1Time < team0Time) {
newTeam1Score++;
}
}
}
const loggedInUserId = getLoggedInUserId();
const teamIndex = getLoggedInUserTeamIndex(response.teams, loggedInUserId);
if (teamIndex === 0) {
leftScore = newTeam0Score;
rightScore = newTeam1Score;
} else if (teamIndex === 1) {
leftScore = newTeam1Score;
rightScore = newTeam0Score;
} else {
leftScore = newTeam0Score;
rightScore = newTeam1Score;
}
const leftScoreEl = document.getElementById("leftScore");
const rightScoreEl = document.getElementById("rightScore");
if (leftScoreEl) leftScoreEl.innerText = leftScore;
if (rightScoreEl) rightScoreEl.innerText = rightScore;
const roundHeader = document.querySelector("." + cn("round-score-header_roundNumber__"));
if (roundHeader) {
roundHeader.innerText = getRoundWinnerText(response);
}
// Check for game over condition (first to 7 wins in a best-of-13 game)
if (!gameOver && (leftScore >= winThreshold || rightScore >= winThreshold)) {
gameOver = true;
showEndScreen();
}
}
}
if(tieRange == 1){
let tieDistance = 0
let newTeam0Score = 0, newTeam1Score = 0;
if (response.teams && response.teams.length >= 2) {
for (let i = 0; i < response.currentRoundNumber && newTeam0Score < winThreshold && newTeam1Score < winThreshold; i++) {
const team0RoundScore = response.teams[0]?.roundResults?.[i]?.score || 0;
const team1RoundScore = response.teams[1]?.roundResults?.[i]?.score || 0;
if (team0RoundScore > team1RoundScore) {
tieDistance = (5000 - team0RoundScore)
if((team0RoundScore - tieDistance) > team1RoundScore){
newTeam0Score++;
}
} else if (team1RoundScore > team0RoundScore) {
tieDistance = (5000 - team1RoundScore)
if((team1RoundScore - tieDistance) > team0RoundScore){
newTeam1Score++;
}
} else if (team0RoundScore === 5000) {
const team0Time = response.teams[0]?.roundResults?.[i]?.bestGuess?.created || Infinity;
const team1Time = response.teams[1]?.roundResults?.[i]?.bestGuess?.created || Infinity;
if (team0Time < team1Time) {
newTeam0Score++;
} else if (team1Time < team0Time) {
newTeam1Score++;
}
}
}
const loggedInUserId = getLoggedInUserId();
const teamIndex = getLoggedInUserTeamIndex(response.teams, loggedInUserId);
if (teamIndex === 0) {
leftScore = newTeam0Score;
rightScore = newTeam1Score;
} else if (teamIndex === 1) {
leftScore = newTeam1Score;
rightScore = newTeam0Score;
} else {
leftScore = newTeam0Score;
rightScore = newTeam1Score;
}
const leftScoreEl = document.getElementById("leftScore");
const rightScoreEl = document.getElementById("rightScore");
if (leftScoreEl) leftScoreEl.innerText = leftScore;
if (rightScoreEl) rightScoreEl.innerText = rightScore;
const roundHeader = document.querySelector("." + cn("round-score-header_roundNumber__"));
if (roundHeader) {
roundHeader.innerText = getRoundWinnerText(response);
}
// Check for game over condition (first to 7 wins in a best-of-13 game)
if (!gameOver && (leftScore >= winThreshold || rightScore >= winThreshold)) {
gameOver = true;
showEndScreen();
}
}
}
};
const fetchDuelData = () => {
const duelId = location.pathname.split("/")[2];
if (!duelId) return;
if (gameOver)
{
if(duelId != currentDuel) {
gameOver = false
}
else {
return
}
}
currentDuel = duelId
fetch(`https://game-server.geoguessr.com/api/duels/${duelId}`, { method: "GET", credentials: "include" })
.then(res => res.json())
.then(updateScores)
.catch(err => {});
};
const observer = new MutationObserver(() => {
requestAnimationFrame(modifyHealthBars);
});
observer.observe(document.body, { childList: true, subtree: true });
if (location.href.includes("duels")) {
scanStyles().then(_ => {
fetchDuelData();
});
}
// Listen for URL changes to auto-activate the script.
(function() {
const _wr = type => {
const orig = history[type];
return function() {
const rv = orig.apply(this, arguments);
window.dispatchEvent(new Event('locationchange'));
return rv;
};
};
history.pushState = _wr("pushState");
history.replaceState = _wr("replaceState");
window.addEventListener('popstate', () => {
window.dispatchEvent(new Event('locationchange'));
});
})();
window.addEventListener('locationchange', function(){
if (location.href.includes("duels")) {
fetchDuelData();
modifyHealthBars();
}
});
setInterval(() => {
if (location.href.includes("duels")) {
fetchDuelData();
}
}, 5000);
})();