您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Display guess times, rating changes for duels, and a list of players for team duels
- // ==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 https://greasyfork.org/scripts/460322-geoguessr-styles-scan/code/Geoguessr%20Styles%20Scan.js?version=1151654
- // @icon https://www.svgrepo.com/show/139928/katana.svg
- // @namespace https://greasyfork.org/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]} </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} </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 });