Geoguessr rating displayer

Adds the competitive rating to usernames

当前为 2024-09-25 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Geoguessr rating displayer
  3. // @description Adds the competitive rating to usernames
  4. // @version 1.2.8
  5. // @license MIT
  6. // @author joniber
  7. // @namespace
  8. // @match https://www.geoguessr.com/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=geoguessr.com
  10. // @require https://greasyfork.org/scripts/460322-geoguessr-styles-scan/code/Geoguessr%20Styles%20Scan.js?version=1151668
  11.  
  12. // @namespace https://greasyfork.org/users/1072330
  13. // ==/UserScript==
  14.  
  15. //=====================================================================================\\
  16. // change these values however you like (make sure to hit ctrl+s afterwards) \\
  17. //=====================================================================================\\
  18.  
  19.  
  20.  
  21. const RATING_IN_FRIENDS_TAB = true;
  22. // ^^^^^ set this to 'false' if you don't want to display rating in the friends tab
  23. const RATING_IN_LEADERBOARD = true;
  24. // ^^^^ set this to 'false' if you don't want to display rating in leaderboards
  25. const RATING_IN_MATCHMAKING = true;
  26. // ^^^^ set this to 'false' if you don't want to display rating in matchmaking lobbies
  27. const RATING_IN_INGAME_PAGE = true;
  28. // ^^^^ set this to 'false' if you don't want to display rating ingame
  29.  
  30. const RATING_COLOR_CHANGE = true;
  31. // ^^^^ set this to 'false' if you don't want to change the color based on the ranking
  32.  
  33. const RATING_IN_BREAKDOWN = true;
  34. // ^^^^ set this to 'false' if you don't want to see rating in the breakdown menu
  35.  
  36. const RATING_IN_SUGGESTIONS = true;
  37. // ^^^^ set this to 'false' if you don't want to see rating in the friend suggestion list
  38.  
  39. const RATING_IN_PROFILES = true;
  40. // ^^^^ set this to >>false<< if you don't want to see rating in the profile list
  41. const GAME_MODE_RATING = true;
  42. // ^^^^^ set to false if you don't want to see the game mode ratings
  43.  
  44. const OVERALL_RATING = true;
  45. // ^^^^^ set to false if you don't want to see overall rating
  46.  
  47. //=====================================================================================\\
  48. // don't edit anything after this point unless you know what you're doing \\
  49. //=====================================================================================\\
  50.  
  51. const GEOGUESSR_USER_ENDPOINT = 'https://geoguessr.com/api/v3/users';
  52. const CHAT_ENDPOINT = 'https://game-server.geoguessr.com/api/lobby';
  53.  
  54. const SCRIPT_PREFIX = 'ur__';
  55. const PROFIlE_RATING_ID = SCRIPT_PREFIX + 'profileRating';
  56. const USER_RATING_CLASS = SCRIPT_PREFIX + 'userRating';
  57.  
  58.  
  59.  
  60.  
  61. const stylesUsed = [
  62.  
  63. "user-nick_nickWrapper__",
  64. "distance-player-list_name__",
  65. "countries-player-list_playerName__",
  66. "hud_root__",
  67. "health-bar_player__",
  68. "user-nick_nick__",
  69. "round-score_container__",
  70. "game_hud__",
  71. "chat-friend_name__",
  72. "leaderboard_row__",
  73. "leaderboard_rowColumnCount3__",
  74. "leaderboard_columnContent__",
  75. "leaderboard_alignStart",
  76. "map-highscore_switchContainer__",
  77. "switch_show__",
  78. "map-highscore_userWrapper__",
  79. "results_userLink__",
  80. "friend_name__",
  81. "avatar-lobby_wrapper__",
  82. "avatar-title_titleWrapper__"
  83. ];
  84.  
  85.  
  86.  
  87. const OBSERVER_CONFIG = {
  88. characterDataOldValue: false,
  89. subtree: true,
  90. childList: true,
  91. characterData: false,
  92. };
  93.  
  94. const ERROR_MESSAGE = (wrong) => '${wrong}';
  95.  
  96. function pathMatches(path) {
  97. return location.pathname.match(new RegExp(`^/(?:[^/]+/)?${path}$`));
  98. }
  99.  
  100. function ratingText() {
  101. return `<p
  102. class="${USER_RATING_CLASS}"
  103. style="margin-left: .25rem; margin-right:.25rem; margin-top:0px; display: none;"
  104. onerror="this.style.display = 'none'"
  105. ></p>`;
  106. }
  107.  
  108. function ratingText1() {
  109. return `<p
  110. id="${PROFIlE_RATING_ID}"
  111. style="margin-left: .25rem; margin-right:.25rem; margin-top:0px; display: none;"
  112. onerror="this.style.display = 'none'"
  113. ></p>`;
  114. }
  115.  
  116. let flag = true
  117. let flag1 = false
  118.  
  119. async function fillRating(ratingNumber, userId) {
  120. const userData = await getUserData(userId);
  121. let divisionNumber = userData.divisionNumber;
  122.  
  123. if (divisionNumber && userData.rating) {
  124. if (RATING_COLOR_CHANGE) {
  125. let color;
  126. switch (divisionNumber) {
  127. case 10: /* Bronze */ color = '#ba682e'; break;
  128. case 9: /* Silver III */
  129. case 8: /* Silver II */
  130. case 7: /* Silver I */ color = '#8e8e8e'; break;
  131. case 6: /* Gold III */
  132. case 5: /* Gold II */
  133. case 4: /* Gold I */ color = '#e8ac06'; break;
  134. case 3: /* Master II */
  135. case 2: /* Master I */ color = '#e04781'; break;
  136. case 1: /* Champion */ color = '#a994e3'; break;
  137. default: color = 'hsla(0,0%,100%,.9)';
  138. }
  139. ratingNumber.style.color = color;
  140.  
  141. if(OVERALL_RATING){
  142. ratingNumber.innerHTML = userData.rating;
  143. }
  144. }
  145. if(GAME_MODE_RATING){
  146. if (userData.gameModeRatings && Object.keys(userData.gameModeRatings).length > 0) {
  147. ratingNumber.innerHTML += " (" +
  148. (userData.gameModeRatings.standardDuels || "&ndash;") + "/" +
  149. (userData.gameModeRatings.noMoveDuels || "&ndash;") + "/" +
  150. (userData.gameModeRatings.nmpzDuels || "&ndash;") + ")";
  151. }
  152. }
  153. } else {
  154. // v4 API did not return data, user is either not active in ranked at the moment or in silver who in v4 have no rating
  155. // fall back on v3 and use gray color to indicate inactive rating or silver to display the rating of silver player
  156. if(userData.rating == null && userData.divisionNumber){
  157. //make sure user has v4 data with a rating of null
  158. const playerApiUrl = `https://www.geoguessr.com/api/v3/users/${userId}`;
  159. try {
  160. const response = await fetch(playerApiUrl, { method: "GET", "credentials": "include" });
  161. const data = await response.json();
  162. ratingNumber.innerHTML = data.competitive.rating;
  163. if(RATING_COLOR_CHANGE) {
  164. ratingNumber.style.color = '#8e8e8e';
  165. }
  166. }
  167. catch(error){
  168. console.error('Failed to fetch player data:', error);
  169. return null;
  170. }
  171. }
  172. else if (! (userData.competitive.elo == 0 && userData.competitive.rating == 0)) {
  173. // user has played ranked in the past
  174. ratingNumber.innerHTML = userData.competitive.rating;
  175. if (RATING_COLOR_CHANGE) {
  176. ratingNumber.style.color = 'hsla(0,0%,100%,.9)';
  177. }
  178. }
  179.  
  180. }
  181.  
  182. ratingNumber.style.display = 'inline';
  183. }
  184.  
  185. function retrieveIdFromLink(link) {
  186. if (link.endsWith('/me/profile')) {
  187. const data = document.querySelector('#__NEXT_DATA__').text;
  188. const json = JSON.parse(data);
  189. return json.props.accountProps.account.user.userId;
  190. }
  191. return link.split('/').at(-1);
  192. }
  193.  
  194. function isOtherProfile() {
  195. return pathMatches('user/.+');
  196. }
  197.  
  198. function isOwnProfile() {
  199. return pathMatches('me/profile');
  200. }
  201.  
  202. function isProfile() {
  203. return isOwnProfile() || isOtherProfile();
  204. }
  205.  
  206. function isBattleRoyale() {
  207. return pathMatches('battle-royale/.+');
  208. }
  209.  
  210. function isDuels() {
  211. return pathMatches('duels/.+');
  212. }
  213. function isReplay() {
  214. return pathMatches('duels/.+/replay');
  215. }
  216.  
  217.  
  218. async function getUserData(id) {
  219. const playerApiUrl = `https://www.geoguessr.com/api/v4/ranked-system/progress/${id}`;
  220.  
  221. try {
  222. const response = await fetch(playerApiUrl, { method: "GET", "credentials": "include" });
  223. const data = await response.json();
  224. return data;
  225. } catch (error) {
  226. const playerApiUrl = `https://www.geoguessr.com/api/v3/users/${id}`;
  227. try {
  228. const response = await fetch(playerApiUrl, { method: "GET", "credentials": "include" });
  229. const data = await response.json();
  230. return data;
  231. }
  232. catch(error){
  233. console.error('Failed to fetch player data:', error);
  234. return null;
  235. }
  236. }
  237. }
  238.  
  239. function addRatingToUsername(link) {
  240. if (!link.querySelector(`.${USER_RATING_CLASS}`)) {
  241. const destination = link.querySelector('[class*=user-nick_nickWrapper__]');
  242. destination.insertAdjacentHTML('beforeend', ratingText());
  243. const ratingNumber = destination.lastChild;
  244. if(destination.children[2]){
  245. destination.insertBefore(
  246. ratingNumber,
  247. destination.children[2]
  248. );
  249.  
  250. }
  251.  
  252. fillRating(ratingNumber, retrieveIdFromLink(link.href));
  253. }
  254. }
  255.  
  256. function addRatingToIngameUsername(link) {
  257. if (!link.querySelector(`.${USER_RATING_CLASS}`)) {
  258. const destination = link.querySelector('span');
  259. destination.style.display = 'flex';
  260. destination.innerHTML += '&nbsp;';
  261. destination.insertAdjacentHTML('beforeend', ratingText());
  262. const ratingNumber = destination.lastChild;
  263. if (destination.childElementCount == 4) {
  264. destination.insertBefore(
  265. ratingNumber,
  266. ratingNumber.previousElementSibling.previousElementSibling
  267. );
  268. } else if (destination.childElementCount > 2) {
  269. destination.insertBefore(ratingNumber, ratingNumber.previousElementSibling);
  270. }
  271.  
  272. fillRating(ratingNumber, retrieveIdFromLink(link.href));
  273. }
  274. }
  275.  
  276. let inBattleRoyale = false;
  277. let inDuels = false;
  278. let inReplay = false;
  279. let lastOpenedMapHighscoreTab = 0;
  280. async function getUsersInGame(id) {
  281. const fetchurl = `${CHAT_ENDPOINT}/${id}`;
  282.  
  283. try {
  284. const response = await fetch(fetchurl, { method: "GET", "credentials": "include" });
  285. const data = await response.json();
  286. return data;
  287. } catch (error) {
  288. console.error('Failed to fetch player data:', error);
  289. return null;
  290. }
  291.  
  292. }
  293.  
  294.  
  295. async function onMutationsBr(mutations, observer) {
  296. if (RATING_IN_INGAME_PAGE) {
  297. //battle royale distance
  298. for (const link of document.querySelectorAll('[class*=distance-player-list_name__] a')) {
  299. addRatingToIngameUsername(link);
  300. }
  301.  
  302. // battle royale countries
  303. for (const link of document.querySelectorAll(
  304. '[class*=countries-player-list_playerName__] a'
  305. )) {
  306. addRatingToIngameUsername(link);
  307. }
  308.  
  309.  
  310. }
  311. }
  312.  
  313. function onMutationsDuels(mutations, observer) {
  314. if (RATING_IN_INGAME_PAGE) {
  315. // duels
  316. const hud = document.querySelector('[class*=hud_root__]');
  317.  
  318. const firstPlayerLink = hud.firstChild?.querySelector('[class*=health-bar_player__] a');
  319. if (firstPlayerLink) {
  320. addRatingToUsername(firstPlayerLink);
  321. }
  322.  
  323. const secondPlayerLink = hud.lastChild?.querySelector('[class*=health-bar_player__] a');
  324. if (secondPlayerLink) {
  325. if (addRatingToUsername(secondPlayerLink, 'afterbegin')) {
  326. const name = secondPlayerLink.querySelector('[class*=user-nick_nick__]');
  327. name.innerHTML = '&nbsp;' + name.innerHTML;
  328. }
  329. }
  330.  
  331.  
  332.  
  333.  
  334.  
  335. }
  336. }
  337.  
  338. function onMutationsReplay(mutations, observer) {
  339. if (RATING_IN_INGAME_PAGE) {
  340. // duels
  341. const hud = document.querySelector('[class*=hud_healthBars__]');
  342.  
  343. const firstPlayerLink = hud.firstChild?.querySelector('[class*=health-bar_player__] a');
  344. if (firstPlayerLink) {
  345. addRatingToUsername(firstPlayerLink);
  346. }
  347.  
  348. const secondPlayerLink = hud.lastChild?.querySelector('[class*=health-bar_player__] a');
  349. if (secondPlayerLink) {
  350. if (addRatingToUsername(secondPlayerLink, 'afterbegin')) {
  351. const name = secondPlayerLink.querySelector('[class*=user-nick_nick__]');
  352. name.innerHTML = '&nbsp;' + name.innerHTML;
  353. }
  354. }
  355.  
  356.  
  357.  
  358.  
  359.  
  360. }
  361. }
  362.  
  363.  
  364. async function onMutationsStandard(mutations, observer) {
  365. if (isBattleRoyale() && document.querySelector('[class*=game_hud__] ul') && !inBattleRoyale) {
  366. inBattleRoyale = true;
  367. const brObserver = new MutationObserver(onMutationsBr);
  368. brObserver.observe(document.querySelector('[class*=game_hud__] ul'), OBSERVER_CONFIG);
  369. } else if (isDuels() && document.querySelector('[class*=game_hud__]') && !inDuels) {
  370. inDuels = true;
  371. const duelsObserver = new MutationObserver(onMutationsDuels);
  372. duelsObserver.observe(document.querySelector('[class*=game_hud__]'), OBSERVER_CONFIG);
  373. } else if (inBattleRoyale && !document.querySelector('[class*=game_hud__] ul')) {
  374. inBattleRoyale = false;
  375. } else if (inDuels && !document.querySelector('[class*=game_hud__]')) {
  376. inDuels = false;
  377. }
  378. else if (isReplay() && document.querySelector('[class*=replay_main__]') && !inReplay) {
  379. inReplay = true;
  380. const replayOberserver = new MutationObserver(onMutationsReplay);
  381. replayOberserver.observe(document.querySelector('[class*=replay_main__]'), OBSERVER_CONFIG);
  382.  
  383. }
  384. else if (inReplay && !document.querySelector('[class*=replay_main__]')) {
  385. inReplay = false;
  386. }
  387.  
  388.  
  389. if (inBattleRoyale || inDuels) {
  390. return;
  391. }
  392.  
  393. if (RATING_IN_FRIENDS_TAB) {
  394. // friends tab
  395. for (const link of document.querySelectorAll('[class*=chat-friend_name__] a')) {
  396. addRatingToUsername(link);
  397. }
  398. }
  399.  
  400. if (isProfile() && RATING_IN_PROFILES) {
  401. // user profile
  402. if (!document.querySelector(`#${PROFIlE_RATING_ID}`)) {
  403.  
  404. const destination = document.querySelector("[class*=headline_heading__] [class*=user-nick_nick__]")
  405. destination.insertAdjacentHTML("beforeend", ratingText1())
  406.  
  407. const yk = destination.lastChild
  408.  
  409. fillRating(yk, retrieveIdFromLink(location.href))
  410.  
  411.  
  412. }
  413.  
  414. }
  415.  
  416. if (RATING_IN_LEADERBOARD) {
  417. // map highscore leaderboard
  418. let tabSwitch = document.querySelector('[class*=map-highscore_switchContainer__] div');
  419. if (tabSwitch) {
  420. const openedMapHighscoreTab =
  421. +tabSwitch.firstChild.firstChild.classList.contains('[class*=switch_hide__]');
  422. if (openedMapHighscoreTab != lastOpenedMapHighscoreTab) {
  423. lastOpenedMapHighscoreTab = openedMapHighscoreTab;
  424. for (const link of document.querySelectorAll(
  425. '[class*=map-highscore_userWrapper__] a'
  426. )) {
  427. const rating = link.querySelector(`.${USER_RATING_CLASS}`);
  428. if (rating) {
  429. rating.remove();
  430. }
  431. }
  432. }
  433. let tabSwitch1 = document.querySelector('[class*=map-highscore_switchContainer__]');
  434. const openFriendHighscoreTab = tabSwitch1.lastChild.lastChild
  435. if(openFriendHighscoreTab){
  436. if(openFriendHighscoreTab.firstChild.classList.contains('[class*=switch_show__]') && flag){
  437. flag = false
  438. flag1 = true
  439. for (const link of document.querySelectorAll(
  440. '[class*=map-highscore_userWrapper__] a'
  441. )) {
  442. const rating = link.querySelector(`.${USER_RATING_CLASS}`);
  443. if (rating) {
  444. rating.remove();
  445. }
  446. }
  447. }
  448. }
  449.  
  450. const openFriendHighscoreTab1 = tabSwitch1.lastChild.firstChild
  451. if(openFriendHighscoreTab1){
  452. if(openFriendHighscoreTab1.firstChild.classList.contains('[class*=switch_show__]') && flag1){
  453. flag = true
  454. flag1 = false
  455. for (const link of document.querySelectorAll(
  456. '[class*=map-highscore_userWrapper__] a'
  457. )) {
  458. const rating = link.querySelector(`.${USER_RATING_CLASS}`);
  459. if (rating) {
  460. rating.remove();
  461. }
  462. }
  463. }
  464. }
  465.  
  466. }
  467.  
  468. for (const link of document.querySelectorAll('[class*=map-highscore_userWrapper__] a')) {
  469. addRatingToUsername(link);
  470. }
  471. }
  472.  
  473. if (RATING_IN_BREAKDOWN) {
  474. for (const link of document.querySelectorAll('[class*=results_userLink__] a')) {
  475. addRatingToUsername(link);
  476. }
  477. }
  478.  
  479. if (RATING_IN_SUGGESTIONS) {
  480. for (const link of document.querySelectorAll('[class*=friend_name__] a')) {
  481. addRatingToUsername(link);
  482. }
  483. }
  484.  
  485. if (RATING_IN_MATCHMAKING) {
  486. if(document.querySelector('[class*=avatar-lobby_wrapper__]')){
  487. let lobbyWrapper = document.querySelector('[class*=avatar-lobby_wrapper__]')
  488.  
  489. for (let link of document
  490. .querySelector('[class*=avatar-lobby_wrapper__]')
  491. .querySelectorAll('[class*=avatar-title_titleWrapper__]')) {
  492. if (!link.querySelector(`.${USER_RATING_CLASS}`)) {
  493. if (link.querySelector('[class*=user-nick_nick__]')) {
  494. const lobby = await getUsersInGame(location.href.slice(-36));
  495. for (const player of lobby.players) {
  496. if (!link.querySelector(`.${USER_RATING_CLASS}`)) {
  497.  
  498. const nickElement = link.querySelector('class*=[user-nick_nick__]');
  499. const isPlayerNick = nickElement.textContent.trim() == player.nick;
  500.  
  501. if (isPlayerNick) {
  502. let destination = nickElement
  503. destination.insertAdjacentHTML('beforeend', ratingText());
  504. const ratingNumber = destination.lastChild;
  505. ratingNumber.style.marginLeft = "0px";
  506. if(destination.children[2]){
  507. destination.insertBefore(
  508. ratingNumber,
  509. destination.children[2]
  510. );
  511.  
  512. }
  513.  
  514. fillRating(ratingNumber, player.playerId);
  515. }
  516.  
  517. }
  518. }
  519. }
  520. }
  521. }
  522.  
  523.  
  524. }
  525. }
  526. }
  527.  
  528.  
  529. const observer = new MutationObserver(onMutationsStandard);
  530.  
  531. observer.observe(document.body, OBSERVER_CONFIG);