Geoguessr rating displayer

Adds the competitive rating to usernames

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

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