Geoguessr duel guess times & team duels player list

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

Установить этот скрипт?
Рекомендуемый автором скрипт

Вам также может понравится Geoguessr unrounded map stats.

Установить этот скрипт
  1. // ==UserScript==
  2. // @name Geoguessr duel guess times & team duels player list
  3. // @version 1.4.0
  4. // @description Display guess times, rating changes for duels, and a list of players for team duels
  5. // @match https://www.geoguessr.com/*
  6. // @author victheturtle#5159
  7. // @grant none
  8. // @license MIT
  9. // @require https://greasyfork.org/scripts/460322-geoguessr-styles-scan/code/Geoguessr%20Styles%20Scan.js?version=1151654
  10. // @icon https://www.svgrepo.com/show/139928/katana.svg
  11. // @namespace https://greasyfork.org/users/967692-victheturtle
  12. // ==/UserScript==
  13.  
  14. let game = {};
  15. let doingRequest = false;
  16. let lastUrlDone = 0;
  17.  
  18. const green = () => cn("game-summary_healing__");
  19. const red = () => cn("game-summary_damage__");
  20. const grey = () => cn("game-summary_smallText__");
  21. const big_white = () => cn("game-summary_text__");
  22. const summary_table = () => cn("game-summary_playedRounds__");
  23. const summary_line = () => cn("game-summary_playedRound__");
  24. const summary_text = () => cn("game-summary_text__");
  25. const replay_header = () => cn("replay_playedRoundsHeader__");
  26. const color = (diff) => (diff>=0) ? ((diff==0) ? grey() : green()) : red();
  27. const greenOrGrey = (diff) => (diff>0) ? green() : grey();
  28. const replay_compact = () => cn("game-summary_compact__");
  29. const replay_table = () => cn("replay_playedRounds__");
  30. const rounds_header = () => cn("game-summary_playedRoundsHeader__");
  31. const best_guess_value = () => cn("game-summary_bestGuessValue__");
  32. const user_nick_root = () => cn("user-nick_root__");
  33. const user_nick_wrapper = () => cn("user-nick_nickWrapper__");
  34. const user_nick_nick = () => cn("user-nick_nick__");
  35. const user_nick_verified_wrapper = () => cn("user-nick_verifiedWrapper__");
  36. const user_nick_verified = () => cn("user-nick_verified__");
  37. const verified_badge_svg = "/_next/static/images/verified-badge-566f0efd4d90928c6e044cbe588456dc.svg"
  38. const ignore_list = ["633a8a81af04a94fb02d8b1b", "633c8040723d43ea09977ea2"]; // Plonk It bots
  39.  
  40. const style = document.createElement("style");
  41. document.head.appendChild(style);
  42. style.sheet.insertRule(".GDGTtooltip { position: relative; display: inline-block; }");
  43. style.sheet.insertRule(`.GDGTtooltip .GDGTtooltiptext {
  44. visibility: hidden; width: 11rem; background-color: black; color: white; text-align: center; padding: 5px 0; border-radius: 6px;
  45. top: 100%; left: 50%; margin-left: -5.5rem; position: absolute; z-index: 0.5; }`);
  46. style.sheet.insertRule(".GDGTtooltip:hover .GDGTtooltiptext { visibility: visible; }");
  47. style.sheet.insertRule('h1[class*="game-summary_summaryTitle__"] { z-index: 1 }')
  48. style.sheet.insertRule('div[class*="game-summary_mapContainer__"] { z-index: 1 }')
  49.  
  50. function checkURL() {
  51. if (location.pathname.includes("duels") && location.pathname.endsWith("/summary") && document.querySelector('[class*="game-summary_playedRounds__"]') != null) return 1;
  52. if (location.pathname.includes("duels") && location.pathname.endsWith("/replay") && document.querySelector('[class*="replay_playedRoundsHeader__"]') != null) return 2;
  53. return 0;
  54. };
  55.  
  56. function round(x) {
  57. return Math.round(x * 10) / 10;
  58. }
  59.  
  60. function handleTeamDuels(isReplay) {
  61. const result_lines = document.getElementsByClassName(summary_line());
  62.  
  63. const inversion = document.querySelector(`#__next div.${(isReplay) ? replay_header() : rounds_header()} img`).alt.includes(game.teams[1].name);
  64. const roundResults1 = game.teams[inversion ? 1 : 0].roundResults;
  65. const roundResults2 = game.teams[inversion ? 0 : 1].roundResults;
  66. const team1Players = game.teams[inversion ? 1 : 0].players;
  67. const team2Players = game.teams[inversion ? 0 : 1].players;
  68.  
  69. for (let i = 0; i < result_lines.length; i++) {
  70. const time0 = new Date(game.rounds[i].startTime);
  71.  
  72. // Check for no guess
  73. if (roundResults1[i].bestGuess == null) roundResults1[i].bestGuess = {created:NaN};
  74. const time1 = new Date(roundResults1[i].bestGuess.created);
  75. let team1Earliest = time1;
  76.  
  77. // Loop through players to check for earlier guess
  78. team1Players.forEach(player => {
  79. if (player.guesses.length <= i || player.guesses[i].roundNumber != i+1) player.guesses.splice(i,0,{created:NaN});
  80. if (!isNaN(player.guesses[i]).created) {
  81. let tempTime = new Date(player.guesses[i].created);
  82. if ((tempTime - team1Earliest) < 0) {
  83. team1Earliest = tempTime;
  84. }
  85. }
  86. })
  87.  
  88. if (roundResults2[i].bestGuess == null) roundResults2[i].bestGuess = {created:NaN};
  89. const time2 = new Date(roundResults2[i].bestGuess.created);
  90. let team2Earliest = time2;
  91.  
  92. // Loop through players to check for earlier guess
  93. team2Players.forEach(player => {
  94. if (player.guesses.length <= i || player.guesses[i].roundNumber != i+1) player.guesses.splice(i,0,{created:NaN});
  95. if (!isNaN(player.guesses[i]).created) {
  96. let tempTime = new Date(player.guesses[i].created);
  97. if ((tempTime - team2Earliest) < 0) {
  98. team2Earliest = tempTime;
  99. }
  100. }
  101. })
  102.  
  103. // Add tooltip on the line header text to show the full date of the round
  104. const header = result_lines[i].childNodes[0].childNodes[0].childNodes[0];
  105. header.classList.add("GDGTtooltip");
  106. const globalTimeText = `${game.rounds[i].startTime.substr(0, 19)} - ${game.rounds[i].endTime.substr(11, 8)}`.replace("T", "<br/>");
  107. header.innerHTML += `<span class="GDGTtooltiptext">${globalTimeText}</span>`;
  108.  
  109. // If one team didn't guess, then the team that did has a green timestamp, otherwise compare
  110. const text1 = document.createElement("div");
  111. text1.classList.add(grey());
  112. text1.innerText = isNaN(time1) ? "-" : round((time1-time0)/1000.) + " s";
  113. result_lines[i].childNodes[1].appendChild(text1);
  114.  
  115. const text2 = document.createElement("div");
  116. text2.classList.add(grey());
  117. text2.innerText = isNaN(time2) ? "-" : round((time2-time0)/1000.) + " s";
  118. result_lines[i].childNodes[2].appendChild(text2);
  119.  
  120. // Add the earliest guess for each team
  121. const t1EarlyDiv = document.createElement("div");
  122. t1EarlyDiv.classList.add(isNaN(team2Earliest) ? green() : greenOrGrey(team2Earliest-team1Earliest));
  123. t1EarlyDiv.innerText = isNaN(team1Earliest) ? "-" : "Team Earliest: " + round((team1Earliest-time0)/1000.) + " s";
  124. result_lines[i].childNodes[1].appendChild(t1EarlyDiv);
  125.  
  126. const t2EarlyDiv = document.createElement("div");
  127. t2EarlyDiv.classList.add(isNaN(team2Earliest) ? green() : greenOrGrey(team1Earliest-team2Earliest));
  128. t2EarlyDiv.innerText = isNaN(team2Earliest) ? "-" : "Team Earliest: " + round((team2Earliest-time0)/1000.) + " s";
  129. result_lines[i].childNodes[2].appendChild(t2EarlyDiv);
  130. };
  131.  
  132. if (game.options.isRated) {
  133. addRatingChanges(isReplay, true, team1Players[0], team2Players[0]);
  134. }
  135.  
  136. addProfileLinks(isReplay, inversion);
  137. }
  138.  
  139. function handleDuels(isReplay) {
  140. const result_lines = document.getElementsByClassName(summary_line());
  141. const player2_link = document.getElementsByClassName((isReplay) ? replay_header() : rounds_header())[0].children[2].firstChild.href;
  142. const player2_id = player2_link.slice(player2_link.lastIndexOf("/")+1);
  143. const inversion = game.teams[1].players[0].playerId != player2_id && player2_id != "profile";
  144. const player1 = game.teams[inversion ? 1 : 0].players[0];
  145. const player2 = game.teams[inversion ? 0 : 1].players[0];
  146. const guesses1 = player1.guesses;
  147. const guesses2 = player2.guesses;
  148. for (let i = 0; i < result_lines.length; i++) {
  149. const time0 = (typeof game.rounds[i].startTime === "string") ? Date.parse(game.rounds[i].startTime) : game.rounds[i].startTime;
  150.  
  151. // Check for no guess
  152. if (guesses1.length <= i || guesses1[i].roundNumber != i+1) guesses1.splice(i, 0, {created:NaN});
  153. const time1 = (typeof guesses1[i].created === "string") ? Date.parse(guesses1[i].created) : guesses1[i].created;
  154. if (guesses2.length <= i || guesses2[i].roundNumber != i+1) guesses2.splice(i, 0, {created:NaN});
  155. const time2 = (typeof guesses2[i].created === "string") ? Date.parse(guesses2[i].created) : guesses2[i].created;
  156.  
  157. const text1 = document.createElement("div");
  158.  
  159. // Add tooltip on the line header text to show the full date of the round
  160. if (!isReplay) {
  161. const header = result_lines[i].childNodes[0].childNodes[0].childNodes[0];
  162. header.classList.add("GDGTtooltip");
  163. const globalTimeText = `${game.rounds[i].startTime.substr(0, 19)} - ${game.rounds[i].endTime.substr(11, 8)}`.replace("T", "<br/>");
  164. header.innerHTML += `<span class="GDGTtooltiptext">${globalTimeText}</span>`;
  165. }
  166.  
  167. // If one team didn't guess, then the team that did has a green timestamp, otherwise compare
  168. text1.classList.add(isNaN(time2) ? green() : greenOrGrey(time2-time1));
  169. if (isReplay) text1.classList.add(replay_compact());
  170. text1.innerText = isNaN(time1) ? "-" : (time1-time0)/1000. + " s";
  171. result_lines[i].childNodes[1].appendChild(text1);
  172.  
  173. const text2 = document.createElement("div");
  174. text2.classList.add(isNaN(time1) ? green() : greenOrGrey(time1-time2));
  175. if (isReplay) text2.classList.add(replay_compact());
  176. text2.innerText = isNaN(time2) ? "-" : (time2-time0)/1000. + " s";
  177. result_lines[i].childNodes[2].appendChild(text2);
  178. }
  179.  
  180. if (game.options.isRated) {
  181. addRatingChanges(isReplay, false, player1, player2);
  182. }
  183. }
  184.  
  185. function addRatingChanges(isReplay, isTeamDuel, player1, player2) {
  186. const compact = (isReplay) ? " " + replay_compact() : ""
  187. const summary = document.getElementsByClassName((isReplay) ? replay_table() : summary_table())[0];
  188. const newRatingLine = document.createElement("div");
  189. newRatingLine.classList.add(summary_line());
  190. if (isReplay) newRatingLine.classList.add(replay_compact());
  191. // currently, rankedSystemProgress is used, but old summaries don't have this field, for them we need to use competitiveProgress
  192. // also, for solo duels without ranking changes, the progressChange field might be missing, but player.rating is always be there
  193. const progressField = isTeamDuel ? "rankedTeamDuelsProgress" : "rankedSystemProgress";
  194. const legacyField = "competitiveProgress";
  195. const oldRating1 = (player1.progressChange?.[progressField] || player1.progressChange?.[legacyField])?.ratingBefore || 0;
  196. const newRating1 = (player1.progressChange?.[progressField] || player1.progressChange?.[legacyField])?.ratingAfter || 0;
  197. const oldRating2 = (player2.progressChange?.[progressField] || player2.progressChange?.[legacyField])?.ratingBefore || 0;
  198. const newRating2 = (player2.progressChange?.[progressField] || player2.progressChange?.[legacyField])?.ratingAfter || 0;
  199. // we can't rely on player.rating for team duels because this is the duels elo, which is different from the team duels elo
  200. const fallback1 = isTeamDuel ? "unknown" : player1.rating;
  201. const fallback2 = isTeamDuel ? "unknown" : player2.rating;
  202. newRatingLine.innerHTML = `
  203. <div><span><div class="${grey()}${compact}">Rating change</div><div class="${big_white()}${compact}">New rating</div></span></div>
  204. <div><div class="${color(newRating1-oldRating1)}${compact}">${newRating1-oldRating1}</div><div class="${big_white()}${compact}">${newRating1 || fallback1}</div></div>
  205. <div><div class="${color(newRating2-oldRating2)}${compact}">${newRating2-oldRating2}</div><div class="${big_white()}${compact}">${newRating2 || fallback2}</div></div>
  206. <div><div class="${big_white()}${compact}"> </div></div>
  207. <div><div class="${big_white()}${compact}"> </div></div>`;
  208. summary.appendChild(newRatingLine);
  209. };
  210.  
  211. function addProfileLinks(isReplay, inversion) {
  212. const nameMap = {};
  213. const teamMap = {};
  214. const verifiedMap = {};
  215. const gameRef = __NEXT_DATA__.props.pageProps.game
  216. if (!gameRef) return; // you'll have to refresh to get that extra header line
  217. const teamName1 = gameRef.teams[0].name
  218. const teamName2 = gameRef.teams[1].name
  219. gameRef.teams[0].players.map(y => {
  220. nameMap[y.playerId] = y.nick; verifiedMap[y.playerId] = y.isVerified; teamMap[y.playerId] = teamName1;
  221. });
  222. gameRef.teams[1].players.map(y => {
  223. nameMap[y.playerId] = y.nick; verifiedMap[y.playerId] = y.isVerified; teamMap[y.playerId] = teamName2;
  224. });
  225.  
  226. const playerTemplate = (playerId) => `<span class="${best_guess_value()}" style="margin:2px"><div class="${user_nick_root()}">
  227. <div class="${user_nick_wrapper()}">
  228. <div class="${user_nick_nick()}"><a href="/user/${playerId}" style="color:white">${nameMap[playerId]}&nbsp;</a></div>
  229. ${verifiedMap[playerId] ? `<div class="${user_nick_verified_wrapper()}"><img class="${user_nick_verified()}" src="${verified_badge_svg}" alt="Verified user"></div>` : ''}
  230. </div>
  231. </div></span>`;
  232. const teamTemplate = (team) => {
  233. let s = "";
  234. for (let playerId in nameMap) {
  235. if (teamMap[playerId] == team && !ignore_list.includes(playerId)) {
  236. s = s + playerTemplate(playerId);
  237. }
  238. }
  239. return s;
  240. }
  241. const mapTemplate = (mapId, mapName) => `<span class="${best_guess_value()}" style="margin:2px"><div class="${user_nick_root()}">
  242. <div class="${user_nick_wrapper()}">
  243. <div class="${user_nick_nick()}"><a href="/maps/${mapId}" style="color:white">${mapName}&nbsp;</a></div>
  244. </div>
  245. </div></span>`;
  246. const movementTemplate = (text) => `<span class="${summary_text()}" style="margin:2px">${text}</span>`;
  247. const options = gameRef.options;
  248.  
  249. const playersLine = document.createElement("div");
  250. playersLine.classList.add(summary_line());
  251. const rules = {NM: options.movementOptions.forbidMoving, NP: options.movementOptions.forbidRotating, NZ: options.movementOptions.forbidZooming};
  252. const isMoving = !rules.NM && !rules.NP && !rules.NZ
  253. const movementType = (isMoving) ? "Moving" : `N${(rules.NM) ? "M" : ""}${(rules.NP) ? "P" : ""}${(rules.NZ) ? "Z" : ""}`;
  254. playersLine.innerHTML = `
  255. <div><span><div class="${summary_text()}">Players</div></span></div>
  256. <div>${teamTemplate((inversion) ? teamName2 : teamName1)}</div>
  257. <div>${teamTemplate((inversion) ? teamName1 : teamName2)}</div>
  258. <div>${movementTemplate(movementType)}</div>
  259. <div>${mapTemplate(options.map?.slug, options.map?.name || "(private map)")}</div>`;
  260. if (isReplay) {
  261. playersLine.classList.add(replay_compact());
  262. }
  263.  
  264. const summary = document.getElementsByClassName((isReplay) ? replay_table() : summary_table())[0];
  265. summary.insertBefore(playersLine, summary.firstChild);
  266. };
  267.  
  268. function check() {
  269. const split = location.pathname.split("/");
  270. const api_url = `https://game-server.geoguessr.com/api/duels/${(split[2].length > 5) ? split[2] : split[3]}`;
  271. doingRequest = true;
  272. fetch(api_url, {method: "GET", "credentials": "include"})
  273. .then(res => res.json())
  274. .then(json => {
  275. doingRequest = false;
  276. game = json;
  277. const urlType = checkURL()
  278. if (urlType != 0 && lastUrlDone != urlType) {
  279. lastUrlDone = urlType;
  280. scanStyles().then(_ => {
  281. const isReplay = location.pathname.includes("replay");
  282. if (game.options.isTeamDuels) handleTeamDuels(isReplay);
  283. else handleDuels(isReplay);
  284. });
  285. }
  286. }).catch(err => { doingRequest = false; throw(err); });
  287. };
  288.  
  289. function doCheck() {
  290. scanStyles().then(_ => {
  291. const urlType = checkURL()
  292. if (urlType == 0) {
  293. lastUrlDone = 0;
  294. } else if (game != {} && lastUrlDone != urlType && !doingRequest) {
  295. check();
  296. }
  297. });
  298. };
  299.  
  300. new MutationObserver((mutations) => {
  301. if (checkURL() == 0) return;
  302. doCheck();
  303. }).observe(document.body, { subtree: true, childList: true });