Pinpointing Duels

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

// ==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);

})();