Greasy Fork

来自缓存

Greasy Fork is available in English.

Geoguessr duel guess times & team duels player list

Display guess times, rating changes for duels, and a list of players for team duels

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Geoguessr duel guess times & team duels player list
// @version      1.4.0
// @description  Display guess times, rating changes for duels, and a list of players for team duels
// @match        https://www.geoguessr.com/*
// @author       victheturtle#5159
// @grant        none
// @license      MIT
// @require      http://greasyfork.icu/scripts/460322-geoguessr-styles-scan/code/Geoguessr%20Styles%20Scan.js?version=1151654
// @icon         https://www.svgrepo.com/show/139928/katana.svg
// @namespace    http://greasyfork.icu/users/967692-victheturtle
// ==/UserScript==

let game = {};
let doingRequest = false;
let lastUrlDone = 0;

const green = () => cn("game-summary_healing__");
const red = () => cn("game-summary_damage__");
const grey = () => cn("game-summary_smallText__");
const big_white = () => cn("game-summary_text__");
const summary_table = () => cn("game-summary_playedRounds__");
const summary_line = () => cn("game-summary_playedRound__");
const summary_text = () => cn("game-summary_text__");
const replay_header = () => cn("replay_playedRoundsHeader__");
const color = (diff) => (diff>=0) ? ((diff==0) ? grey() : green()) : red();
const greenOrGrey = (diff) => (diff>0) ? green() : grey();
const replay_compact = () => cn("game-summary_compact__");
const replay_table = () => cn("replay_playedRounds__");
const rounds_header = () => cn("game-summary_playedRoundsHeader__");
const best_guess_value = () => cn("game-summary_bestGuessValue__");
const user_nick_root = () => cn("user-nick_root__");
const user_nick_wrapper = () => cn("user-nick_nickWrapper__");
const user_nick_nick = () => cn("user-nick_nick__");
const user_nick_verified_wrapper = () => cn("user-nick_verifiedWrapper__");
const user_nick_verified = () => cn("user-nick_verified__");
const verified_badge_svg = "/_next/static/images/verified-badge-566f0efd4d90928c6e044cbe588456dc.svg"
const ignore_list = ["633a8a81af04a94fb02d8b1b", "633c8040723d43ea09977ea2"]; // Plonk It bots

const style = document.createElement("style");
document.head.appendChild(style);
style.sheet.insertRule(".GDGTtooltip { position: relative; display: inline-block; }");
style.sheet.insertRule(`.GDGTtooltip .GDGTtooltiptext {
    visibility: hidden; width: 11rem; background-color: black; color: white; text-align: center; padding: 5px 0; border-radius: 6px;
    top: 100%; left: 50%; margin-left: -5.5rem; position: absolute; z-index: 0.5; }`);
style.sheet.insertRule(".GDGTtooltip:hover .GDGTtooltiptext { visibility: visible; }");
style.sheet.insertRule('h1[class*="game-summary_summaryTitle__"] { z-index: 1 }')
style.sheet.insertRule('div[class*="game-summary_mapContainer__"] { z-index: 1 }')

function checkURL() {
    if (location.pathname.includes("duels") && location.pathname.endsWith("/summary") && document.querySelector('[class*="game-summary_playedRounds__"]') != null) return 1;
    if (location.pathname.includes("duels") && location.pathname.endsWith("/replay") && document.querySelector('[class*="replay_playedRoundsHeader__"]') != null) return 2;
    return 0;
};

function round(x) {
    return Math.round(x * 10) / 10;
}

function handleTeamDuels(isReplay) {
    const result_lines = document.getElementsByClassName(summary_line());

    const inversion = document.querySelector(`#__next div.${(isReplay) ? replay_header() : rounds_header()} img`).alt.includes(game.teams[1].name);
    const roundResults1 = game.teams[inversion ? 1 : 0].roundResults;
    const roundResults2 = game.teams[inversion ? 0 : 1].roundResults;
    const team1Players = game.teams[inversion ? 1 : 0].players;
    const team2Players = game.teams[inversion ? 0 : 1].players;

    for (let i = 0; i < result_lines.length; i++) {
        const time0 = new Date(game.rounds[i].startTime);

        // Check for no guess
        if (roundResults1[i].bestGuess == null) roundResults1[i].bestGuess = {created:NaN};
        const time1 = new Date(roundResults1[i].bestGuess.created);
        let team1Earliest = time1;

        // Loop through players to check for earlier guess
        team1Players.forEach(player => {
            if (player.guesses.length <= i || player.guesses[i].roundNumber != i+1) player.guesses.splice(i,0,{created:NaN});
            if (!isNaN(player.guesses[i]).created) {
               let tempTime = new Date(player.guesses[i].created);
               if ((tempTime - team1Earliest) < 0) {
                   team1Earliest = tempTime;
               }
            }
        })

        if (roundResults2[i].bestGuess == null) roundResults2[i].bestGuess = {created:NaN};
        const time2 = new Date(roundResults2[i].bestGuess.created);
        let team2Earliest = time2;

        // Loop through players to check for earlier guess
        team2Players.forEach(player => {
            if (player.guesses.length <= i || player.guesses[i].roundNumber != i+1) player.guesses.splice(i,0,{created:NaN});
            if (!isNaN(player.guesses[i]).created) {
                let tempTime = new Date(player.guesses[i].created);
                if ((tempTime - team2Earliest) < 0) {
                    team2Earliest = tempTime;
                }
            }
        })

        // Add tooltip on the line header text to show the full date of the round
        const header = result_lines[i].childNodes[0].childNodes[0].childNodes[0];
        header.classList.add("GDGTtooltip");
        const globalTimeText = `${game.rounds[i].startTime.substr(0, 19)} - ${game.rounds[i].endTime.substr(11, 8)}`.replace("T", "<br/>");
        header.innerHTML += `<span class="GDGTtooltiptext">${globalTimeText}</span>`;

        // If one team didn't guess, then the team that did has a green timestamp, otherwise compare
        const text1 = document.createElement("div");
        text1.classList.add(grey());
        text1.innerText = isNaN(time1) ? "-" : round((time1-time0)/1000.) + " s";
        result_lines[i].childNodes[1].appendChild(text1);

        const text2 = document.createElement("div");
        text2.classList.add(grey());
        text2.innerText = isNaN(time2) ? "-" : round((time2-time0)/1000.) + " s";
        result_lines[i].childNodes[2].appendChild(text2);

        // Add the earliest guess for each team
        const t1EarlyDiv = document.createElement("div");
        t1EarlyDiv.classList.add(isNaN(team2Earliest) ? green() : greenOrGrey(team2Earliest-team1Earliest));
        t1EarlyDiv.innerText = isNaN(team1Earliest) ? "-" : "Team Earliest: " + round((team1Earliest-time0)/1000.) + " s";
        result_lines[i].childNodes[1].appendChild(t1EarlyDiv);

        const t2EarlyDiv = document.createElement("div");
        t2EarlyDiv.classList.add(isNaN(team2Earliest) ? green() : greenOrGrey(team1Earliest-team2Earliest));
        t2EarlyDiv.innerText = isNaN(team2Earliest) ? "-" : "Team Earliest: " + round((team2Earliest-time0)/1000.) + " s";
        result_lines[i].childNodes[2].appendChild(t2EarlyDiv);
    };

    if (game.options.isRated) {
        addRatingChanges(isReplay, true, team1Players[0], team2Players[0]);
    }

    addProfileLinks(isReplay, inversion);
}

function handleDuels(isReplay) {
    const result_lines = document.getElementsByClassName(summary_line());
    const player2_link = document.getElementsByClassName((isReplay) ? replay_header() : rounds_header())[0].children[2].firstChild.href;
    const player2_id = player2_link.slice(player2_link.lastIndexOf("/")+1);
    const inversion = game.teams[1].players[0].playerId != player2_id && player2_id != "profile";
    const player1 = game.teams[inversion ? 1 : 0].players[0];
    const player2 = game.teams[inversion ? 0 : 1].players[0];
    const guesses1 = player1.guesses;
    const guesses2 = player2.guesses;
    for (let i = 0; i < result_lines.length; i++) {
        const time0 = (typeof game.rounds[i].startTime === "string") ? Date.parse(game.rounds[i].startTime) : game.rounds[i].startTime;

        // Check for no guess
        if (guesses1.length <= i || guesses1[i].roundNumber != i+1) guesses1.splice(i, 0, {created:NaN});
        const time1 = (typeof guesses1[i].created === "string") ? Date.parse(guesses1[i].created) : guesses1[i].created;
        if (guesses2.length <= i || guesses2[i].roundNumber != i+1) guesses2.splice(i, 0, {created:NaN});
        const time2 = (typeof guesses2[i].created === "string") ? Date.parse(guesses2[i].created) : guesses2[i].created;

        const text1 = document.createElement("div");

        // Add tooltip on the line header text to show the full date of the round
        if (!isReplay) {
            const header = result_lines[i].childNodes[0].childNodes[0].childNodes[0];
            header.classList.add("GDGTtooltip");
            const globalTimeText = `${game.rounds[i].startTime.substr(0, 19)} - ${game.rounds[i].endTime.substr(11, 8)}`.replace("T", "<br/>");
            header.innerHTML += `<span class="GDGTtooltiptext">${globalTimeText}</span>`;
        }

        // If one team didn't guess, then the team that did has a green timestamp, otherwise compare
        text1.classList.add(isNaN(time2) ? green() : greenOrGrey(time2-time1));
        if (isReplay) text1.classList.add(replay_compact());
        text1.innerText = isNaN(time1) ? "-" : (time1-time0)/1000. + " s";
        result_lines[i].childNodes[1].appendChild(text1);

        const text2 = document.createElement("div");
        text2.classList.add(isNaN(time1) ? green() : greenOrGrey(time1-time2));
        if (isReplay) text2.classList.add(replay_compact());
        text2.innerText = isNaN(time2) ? "-" : (time2-time0)/1000. + " s";
        result_lines[i].childNodes[2].appendChild(text2);
    }

    if (game.options.isRated) {
        addRatingChanges(isReplay, false, player1, player2);
    }
}

function addRatingChanges(isReplay, isTeamDuel, player1, player2) {
    const compact = (isReplay) ? " " + replay_compact() : ""
    const summary = document.getElementsByClassName((isReplay) ? replay_table() : summary_table())[0];
    const newRatingLine = document.createElement("div");
    newRatingLine.classList.add(summary_line());
    if (isReplay) newRatingLine.classList.add(replay_compact());
    // currently, rankedSystemProgress is used, but old summaries don't have this field, for them we need to use competitiveProgress
    // also, for solo duels without ranking changes, the progressChange field might be missing, but player.rating is always be there
    const progressField = isTeamDuel ? "rankedTeamDuelsProgress" : "rankedSystemProgress";
    const legacyField = "competitiveProgress";
    const oldRating1 = (player1.progressChange?.[progressField] || player1.progressChange?.[legacyField])?.ratingBefore || 0;
    const newRating1 = (player1.progressChange?.[progressField] || player1.progressChange?.[legacyField])?.ratingAfter || 0;
    const oldRating2 = (player2.progressChange?.[progressField] || player2.progressChange?.[legacyField])?.ratingBefore || 0;
    const newRating2 = (player2.progressChange?.[progressField] || player2.progressChange?.[legacyField])?.ratingAfter || 0;
    // we can't rely on player.rating for team duels because this is the duels elo, which is different from the team duels elo
    const fallback1 = isTeamDuel ? "unknown" : player1.rating;
    const fallback2 = isTeamDuel ? "unknown" : player2.rating;
    newRatingLine.innerHTML = `
        <div><span><div class="${grey()}${compact}">Rating change</div><div class="${big_white()}${compact}">New rating</div></span></div>
        <div><div class="${color(newRating1-oldRating1)}${compact}">${newRating1-oldRating1}</div><div class="${big_white()}${compact}">${newRating1 || fallback1}</div></div>
        <div><div class="${color(newRating2-oldRating2)}${compact}">${newRating2-oldRating2}</div><div class="${big_white()}${compact}">${newRating2 || fallback2}</div></div>
        <div><div class="${big_white()}${compact}"> </div></div>
        <div><div class="${big_white()}${compact}"> </div></div>`;
    summary.appendChild(newRatingLine);
};

function addProfileLinks(isReplay, inversion) {
    const nameMap = {};
    const teamMap = {};
    const verifiedMap = {};
    const gameRef = __NEXT_DATA__.props.pageProps.game
    if (!gameRef) return; // you'll have to refresh to get that extra header line
    const teamName1 = gameRef.teams[0].name
    const teamName2 = gameRef.teams[1].name
    gameRef.teams[0].players.map(y => {
        nameMap[y.playerId] = y.nick; verifiedMap[y.playerId] = y.isVerified; teamMap[y.playerId] = teamName1;
    });
    gameRef.teams[1].players.map(y => {
        nameMap[y.playerId] = y.nick; verifiedMap[y.playerId] = y.isVerified; teamMap[y.playerId] = teamName2;
    });

    const playerTemplate = (playerId) => `<span class="${best_guess_value()}" style="margin:2px"><div class="${user_nick_root()}">
                <div class="${user_nick_wrapper()}">
                  <div class="${user_nick_nick()}"><a href="/user/${playerId}" style="color:white">${nameMap[playerId]}&nbsp;</a></div>
                  ${verifiedMap[playerId] ? `<div class="${user_nick_verified_wrapper()}"><img class="${user_nick_verified()}" src="${verified_badge_svg}" alt="Verified user"></div>` : ''}
                </div>
              </div></span>`;
    const teamTemplate = (team) => {
        let s = "";
        for (let playerId in nameMap) {
            if (teamMap[playerId] == team && !ignore_list.includes(playerId)) {
                s = s + playerTemplate(playerId);
            }
        }
        return s;
    }
    const mapTemplate = (mapId, mapName) => `<span class="${best_guess_value()}" style="margin:2px"><div class="${user_nick_root()}">
                <div class="${user_nick_wrapper()}">
                  <div class="${user_nick_nick()}"><a href="/maps/${mapId}" style="color:white">${mapName}&nbsp;</a></div>
                </div>
              </div></span>`;
    const movementTemplate = (text) => `<span class="${summary_text()}" style="margin:2px">${text}</span>`;
    const options = gameRef.options;

    const playersLine = document.createElement("div");
    playersLine.classList.add(summary_line());
    const rules = {NM: options.movementOptions.forbidMoving, NP: options.movementOptions.forbidRotating, NZ: options.movementOptions.forbidZooming};
    const isMoving = !rules.NM && !rules.NP && !rules.NZ
    const movementType = (isMoving) ? "Moving" : `N${(rules.NM) ? "M" : ""}${(rules.NP) ? "P" : ""}${(rules.NZ) ? "Z" : ""}`;
    playersLine.innerHTML = `
        <div><span><div class="${summary_text()}">Players</div></span></div>
        <div>${teamTemplate((inversion) ? teamName2 : teamName1)}</div>
        <div>${teamTemplate((inversion) ? teamName1 : teamName2)}</div>
        <div>${movementTemplate(movementType)}</div>
        <div>${mapTemplate(options.map?.slug, options.map?.name || "(private map)")}</div>`;
    if (isReplay) {
        playersLine.classList.add(replay_compact());
    }

    const summary = document.getElementsByClassName((isReplay) ? replay_table() : summary_table())[0];
    summary.insertBefore(playersLine, summary.firstChild);
};

function check() {
    const split = location.pathname.split("/");
    const api_url = `https://game-server.geoguessr.com/api/duels/${(split[2].length > 5) ? split[2] : split[3]}`;
    doingRequest = true;
    fetch(api_url, {method: "GET", "credentials": "include"})
    .then(res => res.json())
    .then(json => {
        doingRequest = false;
        game = json;
        const urlType = checkURL()
        if (urlType != 0 && lastUrlDone != urlType) {
            lastUrlDone = urlType;
            scanStyles().then(_ => {
                const isReplay = location.pathname.includes("replay");
                if (game.options.isTeamDuels) handleTeamDuels(isReplay);
                else handleDuels(isReplay);
            });
        }
    }).catch(err => { doingRequest = false; throw(err); });
};

function doCheck() {
    scanStyles().then(_ => {
        const urlType = checkURL()
        if (urlType == 0) {
            lastUrlDone = 0;
        } else if (game != {} && lastUrlDone != urlType && !doingRequest) {
            check();
        }
    });
};

new MutationObserver((mutations) => {
    if (checkURL() == 0) return;
    doCheck();
}).observe(document.body, { subtree: true, childList: true });