Geoguessr Team Duels Advanced Options

Adds extra options to team duel settings.

  1. // ==UserScript==
  2. // @name Geoguessr Team Duels Advanced Options
  3. // @description Adds extra options to team duel settings.
  4. // @version 0.2.3
  5. // @author macca#8949
  6. // @license MIT
  7. // @match https://www.geoguessr.com/*
  8. // @run-at document-start
  9. // @grant none
  10. // @require https://greasyfork.org/scripts/460322-geoguessr-styles-scan/code/Geoguessr%20Styles%20Scan.js?version=1151654
  11. // @namespace https://greasyfork.org/en/scripts/452579-geoguessr-team-duels-advanced-options
  12. // ==/UserScript==
  13.  
  14. let cachedGameMode = '';
  15. let cachedGameOptions = {};
  16.  
  17. const getGameId = () => {
  18. const scripts = document.getElementsByTagName('script');
  19. for (const script of scripts) {
  20. if (script.src.includes('_buildManifest.js')) {
  21. return script.src.split('/')[5];
  22. }
  23. }
  24. }
  25.  
  26. const gameMode = () => {
  27. if (document.getElementsByClassName(cn('bars_content__')).length < 3) return '';
  28. const fullText = document.getElementsByClassName(cn('bars_content__'))[2].textContent;
  29. return fullText.substring(0, fullText.lastIndexOf(' ')).toLowerCase();
  30. }
  31.  
  32. async function fetchWithCors(url, method, body) {
  33. return await fetch(url, {
  34. "headers": {
  35. "accept": "*/*",
  36. "accept-language": "en-US,en;q=0.8",
  37. "content-type": "application/json",
  38. "sec-fetch-dest": "empty",
  39. "sec-fetch-mode": "cors",
  40. "sec-fetch-site": "same-site",
  41. "sec-gpc": "1",
  42. "x-client": "web"
  43. },
  44. "referrer": "https://www.geoguessr.com/",
  45. "referrerPolicy": "strict-origin-when-cross-origin",
  46. "body": JSON.stringify(body),
  47. "method": method,
  48. "mode": "cors",
  49. "credentials": "same-origin"
  50. })
  51. }
  52.  
  53. window.modifySetting = (e, settingName) => {
  54. let newValue = e.value;
  55. if (settingName === 'multiplierIncrement') {
  56. newValue *= 10;
  57. newValue = Math.round(newValue);
  58. } else {
  59. newValue *= 1; // string to number conversion
  60. newValue = Math.round(newValue);
  61. }
  62.  
  63. // Fetch the game options
  64. fetchWithCors(`https://www.geoguessr.com/_next/data/${getGameId()}/en/party.json`, "GET")
  65. .then((response) => response.json())
  66. .then((data) => {
  67. let gameOptions = data.pageProps.party.gameSettings;
  68. gameOptions[settingName] = newValue;
  69. // Push the updated options
  70. fetchWithCors(`https://www.geoguessr.com/api/v4/parties/v2/game-settings`, "PUT", gameOptions);
  71. });
  72.  
  73. cachedGameMode = gameMode();
  74. cachedGameOptions[settingName] = e.value;
  75. }
  76.  
  77. let optionTextInputInnerHTML = (id, settingName, text, icon, helpText) =>
  78. `<div class="${cn('numeric-option_wrapper__')} advanced-option-setting"><div class="${cn('numeric-option_icon__')}"><img alt="" loading="lazy" width="48" height="48" decoding="async" data-nimg="1" class="${cn('rule-icons_icon__')}" style="color: transparent;" src="${icon}"></div><div class="${cn('numeric-option_label__')}">${text}</div><div><input type="text" id="${id}" onblur="modifySetting(this, '${settingName}')" style="text-align: center; background: rgba(255,255,255,0.1); color: white; border: none; border-radius: 5px; width: 80px;"></div></div><p>${helpText}</p>`;
  79.  
  80. function makeCustomTextInput(elt, id, settingName, text, icon, helpText) {
  81. elt.parentElement.outerHTML = optionTextInputInnerHTML(id, settingName, text, icon, helpText);
  82. return elt;
  83. }
  84.  
  85. function updateValue(inputId, option, gameOptions) {
  86. if (document.querySelector(inputId)) {
  87. if (gameMode() == cachedGameMode && option in cachedGameOptions) {
  88. document.querySelector(inputId).value = cachedGameOptions[option];
  89. } else {
  90. if (option == 'multiplierIncrement') {
  91. document.querySelector(inputId).value = gameOptions[option] / 10;
  92. } else {
  93. document.querySelector(inputId).value = gameOptions[option];
  94. }
  95. }
  96. }
  97. }
  98.  
  99. let observer = new MutationObserver(async (mutations) => {
  100. if (document.querySelector('.advanced-option-setting')) return;
  101.  
  102. await scanStyles();
  103.  
  104. if (window.location.href.includes('party') && (document.getElementsByClassName(cn('slider-option_slider__')) || document.getElementsByClassName(cn('numeric-option_button__')))) {
  105. if (gameMode().includes('duels')) {
  106. let healthEl = document.evaluate('//div[text()="Initial health"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  107. let multiplierEl = document.evaluate('//div[text()="Multiplier increase"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  108. let timeAfterGuessEl = document.evaluate('//div[text()="Timer after guess"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  109. let maxRoundTimeEl = document.evaluate('//div[text()="Max round time"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  110. if (healthEl) {
  111. makeCustomTextInput(healthEl, 'health-input', 'initialHealth', 'Initial health', '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fheart.3a3fd066.png&amp;w=96&amp;q=75', '');
  112. }
  113. if (multiplierEl) {
  114. makeCustomTextInput(multiplierEl, 'increment-input', 'multiplierIncrement', 'Multiplier increase', '/_next/static/media/multipliers-icon.63803925.svg', '(must be between 0.1 and 10)');
  115. }
  116. if (timeAfterGuessEl) {
  117. makeCustomTextInput(timeAfterGuessEl, 'time-after-guess-input', 'timeAfterGuess', 'Timer after guess', '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftime-limit.8a68c82e.png&w=96&q=75', '(must be between 10 and 300 seconds)');
  118. }
  119. if (maxRoundTimeEl) {
  120. makeCustomTextInput(maxRoundTimeEl, 'max-round-time-input', 'maxRoundTime', 'Max round time', '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftime-limit.8a68c82e.png&w=96&q=75', '(0 for no time limit)');
  121. }
  122. } else if (gameMode() == 'city streaks') {
  123. let gameTimeEl = document.evaluate('//div[text()="Game time"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  124. if (gameTimeEl) {
  125. makeCustomTextInput(gameTimeEl, 'time-input', 'duration', 'Game time', '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftime-limit.8a68c82e.png&w=96&q=75', '(must be between 60 and 900 seconds)');
  126. }
  127. } else {
  128. let roundTimeEl = document.evaluate('//div[text()="Round time"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  129. if (roundTimeEl) {
  130. let settingName = 'roundTime';
  131. let helpText = '(must be between 10 and 600 seconds)';
  132. if (gameMode() == 'bullseye') {
  133. settingName = 'bullseyeRoundTime';
  134. helpText = '(must be less than 600 seconds, 0 for no time limit)';
  135. }
  136. makeCustomTextInput(roundTimeEl, 'time-input', settingName, 'Round time', '/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftime-limit.8a68c82e.png&w=96&q=75', helpText);
  137. }
  138. }
  139.  
  140. fetchWithCors(`https://www.geoguessr.com/_next/data/${getGameId()}/en/party.json`, "GET")
  141. .then((response) => response.json())
  142. .then((data) => {
  143. let gameOptions = data.pageProps.party.gameSettings;
  144.  
  145. if (gameMode() == 'city streaks') {
  146. updateValue('#time-input', 'duration', gameOptions);
  147. } else if (gameMode().includes('duels')) {
  148. updateValue('#health-input', 'initialHealth', gameOptions);
  149. updateValue('#increment-input', 'multiplierIncrement', gameOptions);
  150. updateValue('#time-after-guess-input', 'timeAfterGuess', gameOptions);
  151. updateValue('#max-round-time-input', 'maxRoundTime', gameOptions);
  152. } else if (gameMode() == 'bullseye') {
  153. updateValue('#time-input', 'bullseyeRoundTime', gameOptions);
  154. } else {
  155. updateValue('#time-input', 'roundTime', gameOptions);
  156. }
  157. });
  158. }
  159. });
  160.  
  161.  
  162. observer.observe(document.body, {
  163. characterDataOldValue: false,
  164. subtree: true,
  165. childList: true,
  166. characterData: false
  167. });
  168.  
  169. document.addEventListener('keydown', (event) => {
  170. if (event.key == 'Escape' && document.getElementsByClassName(cn('party-modal_heading__'))) {
  171. document.activeElement.blur();
  172. }
  173. });