Pinpointing Duels

The script that makes pinpointing matter. Read more at the GeoClassics discord server or check out Pinpointing Tournaments on twitch.tv/GeoClassics.

// ==UserScript==
// @name         Pinpointing Duels
// @namespace    http://tampermonkey.net/
// @version      2.0.6
// @description  The script that makes pinpointing matter. Read more at the GeoClassics discord server or check out Pinpointing Tournaments on twitch.tv/GeoClassics.
// @match        https://www.geoguessr.com/*
// @icon         https://i.imgur.com/eKp3nIa.png
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// @require      https://update.gf.qytechs.cn/scripts/460322/1408713/Geoguessr%20Styles%20Scan.js
// ==/UserScript==

(function() {

    GM_registerMenuCommand("Settings", showSettingsPanel);

function showSettingsPanel() {
    if (document.getElementById('settings-panel')) return;

    const firstToValue = GM_getValue('firstTo', 10);
    const tieRangeValue = GM_getValue('tieRange', 0);
    const horizontalValue = GM_getValue('scoreBoxOffset', 30);
    const verticalValue = GM_getValue('scoreBoxTop', 86);

    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;
        width: 300px;
    `;

    // BEST OF
    const firstToLabel = document.createElement('label');
    firstToLabel.innerText = "First to:";
    const firstToSelect = document.createElement('select');
    for (let x = 3; x <= 30; x += 1) {
        const option = document.createElement('option');
        option.value = x;
        option.innerText = x;
        if (x == firstToValue) option.selected = true;
        firstToSelect.appendChild(option);
    }

    // TIE RANGE
    const tieLabel = document.createElement('label');
    tieLabel.innerText = "Tie Range Mode:";
    const tieSelect = document.createElement('select');
    [
        { value: 0, label: "Disabled" },
        { value: 1, label: "Full Tie Range" },
        { value: 2, label: "Half Tie Range" }
    ].forEach(({ value, label }) => {
        const option = document.createElement('option');
        option.value = value;
        option.innerText = label;
        if (value == tieRangeValue) option.selected = true;
        tieSelect.appendChild(option);
    });

    // HORIZONTAL POSITION
    const hLabel = document.createElement('label');
    hLabel.innerText = `Score Board Horizontal Position: ${horizontalValue}%`;
    hLabel.style.display = "block";
    const hSlider = document.createElement('input');
    hSlider.type = "range";
    hSlider.min = "0";
    hSlider.max = "100";
    hSlider.value = horizontalValue;
    hSlider.style.width = "100%";
    hSlider.addEventListener('input', () => {
        hLabel.innerText = `Score Board Horizontal Position: ${hSlider.value}%`;
        updateScoreBoxPosition(parseInt(hSlider.value), parseInt(vSlider.value));
    });

    // VERTICAL POSITION
    const vLabel = document.createElement('label');
    vLabel.innerText = `Score Board Vertical Position: ${verticalValue}px`;
    vLabel.style.display = "block";
    const vSlider = document.createElement('input');
    vSlider.type = "range";
    vSlider.min = "0";
    vSlider.max = "300";
    vSlider.value = verticalValue;
    vSlider.style.width = "100%";
    vSlider.addEventListener('input', () => {
        vLabel.innerText = `Score Board Vertical Position: ${vSlider.value}px`;
        updateScoreBoxPosition(parseInt(hSlider.value), parseInt(vSlider.value));
    });

    // Buttons
    const saveBtn = document.createElement('button');
    saveBtn.innerText = "Save";
    saveBtn.style.margin = "10px";
    saveBtn.style.cursor = "pointer";
    saveBtn.style.color = "white";
    saveBtn.onclick = () => {
        firstTo = parseInt(firstToSelect.value);
        tieRange = parseInt(tieSelect.value);
        winThreshold = firstTo;

        GM_setValue('firstTo', firstTo);
        GM_setValue('tieRange', tieRange);
        GM_setValue('scoreBoxOffset', parseInt(hSlider.value));
        GM_setValue('scoreBoxTop', parseInt(vSlider.value));

        alert("Settings saved!");
        panel.remove();
    };

    const cancelBtn = document.createElement('button');
    cancelBtn.innerText = "Cancel";
    cancelBtn.style.cursor = "pointer";
    cancelBtn.style.color = "white";
    cancelBtn.onclick = () => panel.remove();

    // Add to DOM
    panel.append(
        firstToLabel, firstToSelect,
        document.createElement("br"),
        document.createElement("br"),
        tieLabel, tieSelect,
        document.createElement("br"),
        document.createElement("br"),
        hLabel, hSlider,
        document.createElement("br"),
        document.createElement("br"),
        vLabel, vSlider,
        document.createElement("br"),
        document.createElement("br"),
        saveBtn, cancelBtn
    );

    document.body.appendChild(panel);
}

function updateScoreBoxPosition(horizontalPercent, verticalPx) {
    const leftEl = document.getElementById("leftScore");
    const rightEl = document.getElementById("rightScore");
    if (leftEl) {
        leftEl.style.left = `${horizontalPercent}%`;
        leftEl.style.top = `${verticalPx}px`;
    }
    if (rightEl) {
        rightEl.style.right = `${horizontalPercent}%`;
        rightEl.style.top = `${verticalPx}px`;
    }
}

function showResultDisplay(message) {
    const container = document.querySelector("." + cn("round-score-header_roundNumber__"));
    if (!container) return;

    // Avoid duplicates
    if (document.getElementById("roundDisplayMessage")) return;

    const wrapper = document.createElement("div");
    wrapper.id = "roundDisplayMessage";
    wrapper.innerHTML = `
        <div style="padding: 20px 0; margin: 0 auto; font-size: 32px; line-height: 48px; text-align: center; color: #f2f2f2;">${message}</div>
    `;
    container.insertBefore(wrapper, container.firstChild);
}

function showPenaltyDisplay(message) {
    const container = document.querySelector("." + cn("round-score-header_roundNumber__"));
    if (!container) return;

    // Avoid duplicates
    if (document.getElementById("roundDisplayPenalty")) return;

    const wrapper = document.createElement("div");
    wrapper.id = "roundDisplayPenalty";
    wrapper.innerHTML = `
        <div style="padding: 20px 0; margin: 24px auto; font-size: 24px; line-height: 24px; text-align: center; color: #f2f2f2;">SCORE 0 FOR GUESSING EARLY AND MISSING 5K: ${message}</div>
    `;
    container.insertBefore(wrapper, container.firstChild);
}


    '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);

    let firstTo = GM_getValue('firstTo', 10); // Fallback 10
    let winThreshold = firstTo; //Win Value
    let tieRange = GM_getValue('tieRange', 0);

    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;
    };


    // 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("duels_hud__"));
        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);
    };

    // Flash the score element by toggling backgrounds every 0.5s for 5s.
    const flashScore = (scoreElement) => {
        const originalBackground = scoreElement.style.background;
        const flashBackground = "linear-gradient(180deg,rgba(90,219,149,1),rgba(60,200,110,1) 95%),#5adb95";
        let flashCount = 0;
        const interval = setInterval(() => {
            // Toggle the background on odd/even counts.
            scoreElement.style.background = (flashCount % 2 === 0) ? flashBackground : originalBackground;
            flashCount++;
            if (flashCount >= 10) { // 10 toggles * 0.5s = 5 seconds total.
                clearInterval(interval);
                scoreElement.style.background = originalBackground;
            }
        }, 500);
    };


const updateScores = (response) => {
    let team0Score = 0, team1Score = 0;
    let newTeam0Score = 0, newTeam1Score = 0;
    let tieDistance = 0;
    const timeAfterGuess = response.options.roundTime;
    const maxRoundTime = response.options.maxRoundTime;

    if (response.teams && response.teams.length >= 2) {
        const loggedInUserId = getLoggedInUserId();
        const teamIndex = getLoggedInUserTeamIndex(response.teams, loggedInUserId);

        for (let i = 0; i < response.currentRoundNumber && newTeam0Score < winThreshold && newTeam1Score < winThreshold; i++) {
            if (!response.rounds[i].hasProcessedRoundTimeout) continue;
            const oldDisplayMessage = document.getElementById("roundDisplayMessage");
            if (oldDisplayMessage) oldDisplayMessage.remove();
            const oldPenaltyMessage = document.getElementById("roundDisplayPenalty");
            if (oldPenaltyMessage) oldPenaltyMessage.remove();
            let message = '', tieMessage = '', winMessage = '';

            let roundStartTime = new Date(response.rounds[i]?.startTime)/1000 || Infinity;
            let roundEndTime = new Date(response.rounds[i]?.endTime)/1000 || Infinity;
            let team0RoundScore = 0,team1RoundScore = 0;
            let team0Time = Infinity, team1Time = Infinity;
            let team0BestScore = 0, team1BestScore = 0;

            for (let j = 0; j < response.teams[0]?.players.length; j++){
                let currentGuess;
                response.teams[0].players[j].guesses.forEach((guess) => {if (guess.roundNumber == i+1) currentGuess = guess;});
                if (currentGuess === undefined) continue;
                let guessTime = new Date(currentGuess.created)/1000 || Infinity;
                let score = Math.round(5000*Math.exp(-10*currentGuess.distance/response.options?.map?.maxErrorDistance)) || 0;
                score = currentGuess.distance < 25 ? 5000 : score;
                if (guessTime < team0Time && guessTime < roundEndTime) {
                    team0Time = guessTime; team0RoundScore = score;
                } else if (score > team0BestScore) {
                    team0BestScore = score;
                }
            }
            if (team0Time > roundEndTime) team0RoundScore = team0BestScore;
            for (let j = 0; j < response.teams[1]?.players.length; j++){
                let currentGuess;
                response.teams[1].players[j].guesses.forEach((guess) => {if (guess.roundNumber == i+1) currentGuess = guess;});
                if (currentGuess === undefined) continue;
                let guessTime = new Date(currentGuess.created)/1000 || Infinity;
                let score = Math.round(5000*Math.exp(-10*currentGuess.distance/response.options?.map?.maxErrorDistance)) || 0;
                score = currentGuess.distance < 25 ? 5000 : score;
                if (guessTime < team1Time && guessTime < roundEndTime) {
                    team1Time = guessTime; team1RoundScore = score;
                } else if (score > team1BestScore) {
                    team1BestScore = score;
                }
            }
            if (team1Time > roundEndTime) team1RoundScore = team1BestScore;


            if (team0RoundScore < 5000 && team0Time < team1Time && team0Time < (roundStartTime + maxRoundTime - timeAfterGuess)) {
                showPenaltyDisplay(teamIndex === 0 ? "YOU" : "YOUR OPPONENT");
                team0RoundScore = 0;
            } else if (team1RoundScore < 5000 && team1Time < team0Time && team1Time < (roundStartTime + maxRoundTime - timeAfterGuess)) {
                showPenaltyDisplay(teamIndex === 1 ? "YOU" : "YOUR OPPONENT");
                team1RoundScore = 0;
            } // Score counted as 0 if penalty for early sending

            team0Score = newTeam0Score; team1Score = newTeam1Score;
            const highestScore = team0RoundScore > team1RoundScore ? team0RoundScore : team1RoundScore;
            if (tieRange > 0) tieDistance = Math.floor((5000 - highestScore) / tieRange);

            if (team0RoundScore === 5000 && team1RoundScore === 5000) {
                if (team0Time < team1Time) newTeam0Score++;
                else if (team1Time < team0Time) newTeam1Score++;
                message = `1 POINT FOR FASTEST 5k`;
            } else if (team0RoundScore == 5000 || team1RoundScore == 5000) {
                 if (team0RoundScore == 5000) newTeam0Score += 2; else newTeam1Score += 2;
                message = `2 POINTS FOR SOLO 5K`;
            } else if (team0RoundScore > team1RoundScore + tieDistance || team1RoundScore > team0RoundScore + tieDistance) {
                if (team0RoundScore > team1RoundScore + tieDistance) newTeam0Score++; else newTeam1Score ++;
                message = `1 POINT FOR CLOSEST GUESS`;
                if (tieRange>0) tieMessage = `<br><span style="font-size: 14px;">TIE RANGE: ${tieDistance} POINTS</span>`;
            } else {
                message = `TIE!`;
                if (tieRange>0) tieMessage = `<br><span style="font-size: 14px;">TIE RANGE: ${tieDistance} POINTS</span>`;
            }
            if (message !== `TIE!`) winMessage = (teamIndex === 0) === (newTeam0Score > team0Score) ? "YOU WIN THE ROUND! <br>" : "YOUR OPPONENT WINS THE ROUND! <br>";
            const isMatchPoint = newTeam0Score >= winThreshold - 2 || newTeam1Score >= winThreshold - 2;
            showResultDisplay(`${winMessage} ${message} ${isMatchPoint ? "<br>MATCH POINT!" : "" } ${tieMessage}`);
        }

        const leftScoreChanged = (teamIndex === 0 ? newTeam0Score !== leftScore : newTeam1Score !== leftScore);
        const rightScoreChanged = (teamIndex === 0 ? newTeam1Score !== rightScore : newTeam0Score !== rightScore);

        if (teamIndex === 0) {
            leftScore = newTeam0Score;
            rightScore = newTeam1Score;
        } else if (teamIndex === 1) {
            leftScore = newTeam1Score;
            rightScore = newTeam0Score;
        } else {
            leftScore = newTeam0Score;
            rightScore = newTeam1Score;
        }
        const isMatchPoint = leftScore >= winThreshold - 2 || rightScore >= winThreshold - 2;
        const leftScoreEl = document.getElementById("leftScore");
        const rightScoreEl = document.getElementById("rightScore");
        if (leftScoreEl) {
            leftScoreEl.innerText = leftScore;
            if (leftScoreChanged) flashScore(leftScoreEl);
        }
        if (rightScoreEl) {
            rightScoreEl.innerText = rightScore;
            if (rightScoreChanged) flashScore(rightScoreEl);
        }

        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/")) {
            scanStyles().then(_ => {
                fetchDuelData();
                modifyHealthBars();
            });
        }
    });
    setInterval(() => {
        if (location.href.includes("/duels/")) {
            scanStyles().then(_ => {
                fetchDuelData();
            });
        }
    }, 5000);

        if (location.href.includes("/team-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("/team-duels/")) {
            scanStyles().then(_ => {
                fetchDuelData();
                modifyHealthBars();
            });
        }
    });
    setInterval(() => {
        if (location.href.includes("/team-duels/")) {
            scanStyles().then(_ => {
                fetchDuelData();
            });
        }
    }, 2500);

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址