Advertisement
Guest User

geoguessr_stats_track

a guest
Apr 17th, 2025
202
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 33.35 KB | Gaming | 0 0
  1. // ==UserScript==
  2. // @name GeoGuessr Country Statistics Tracker Enhanced
  3. // @version 1.1.0
  4. // @description Tracks statistics on your country guessing accuracy in GeoGuessr with enhanced UI and point tracking
  5. // @match https://www.geoguessr.com/*
  6. // @author gruen132U
  7. // @license MIT
  8. // @require https://greasyfork.org/scripts/460322-geoguessr-styles-scan/code/Geoguessr%20Styles%20Scan.js?version=1151654
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=geoguessr.com
  10. // @namespace https://greasyfork.org/users/gruen132U
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // Main configuration
  18. const AUTOMATIC = true;
  19. const API_Key = 'INSERT_BIGDATACLOUD_API_KEY_HERE'; // Optional: add your API key for better results
  20. const ERROR_RESP = -1000000;
  21. const MAX_POINTS = 5000; // Maximum points per round in GeoGuessr
  22. let lastGuess = { lat: 91, lng: 0 };
  23.  
  24. // GeoGuessr UI colors for styling
  25. const GEOGUESSR_COLORS = {
  26. primary: '#4ca852', // Green
  27. secondary: '#4d508a', // Purple
  28. dark: '#1a1a1a',
  29. darkGray: '#2d2d2d',
  30. mediumGray: '#444444',
  31. lightGray: '#777777',
  32. lightText: '#ffffff',
  33. lightTextSecondary: '#b3b3b3',
  34. danger: '#d63030' // Red
  35. };
  36.  
  37. // Country code mapping
  38. const CountryDict = {
  39. AF: 'AF', AX: 'FI', AL: 'AL', DZ: 'DZ', AS: 'US', AD: 'AD', AO: 'AO', AI: 'GB', AQ: 'AQ', AG: 'AG',
  40. AR: 'AR', AM: 'AM', AW: 'NL', AU: 'AU', AT: 'AT', AZ: 'AZ', BS: 'BS', BH: 'BH', BD: 'BD', BB: 'BB',
  41. BY: 'BY', BE: 'BE', BZ: 'BZ', BJ: 'BJ', BM: 'GB', BT: 'BT', BO: 'BO', BQ: 'NL', BA: 'BA', BW: 'BW',
  42. BV: 'NO', BR: 'BR', IO: 'GB', BN: 'BN', BG: 'BG', BF: 'BF', BI: 'BI', KH: 'KH', CM: 'CM', CA: 'CA',
  43. CV: 'CV', KY: 'UK', CF: 'CF', TD: 'TD', CL: 'CL', CN: 'CN', CX: 'AU', CC: 'AU', CO: 'CO', KM: 'KM',
  44. CG: 'CG', CD: 'CD', CK: 'NZ', CR: 'CR', CI: 'CI', HR: 'HR', CU: 'CU', CW: 'NL', CY: 'CY', CZ: 'CZ',
  45. DK: 'DK', DJ: 'DJ', DM: 'DM', DO: 'DO', EC: 'EC', EG: 'EG', SV: 'SV', GQ: 'GQ', ER: 'ER', EE: 'EE',
  46. ET: 'ET', FK: 'GB', FO: 'DK', FJ: 'FJ', FI: 'FI', FR: 'FR', GF: 'FR', PF: 'FR', TF: 'FR', GA: 'GA',
  47. GM: 'GM', GE: 'GE', DE: 'DE', GH: 'GH', GI: 'UK', GR: 'GR', GL: 'DK', GD: 'GD', GP: 'FR', GU: 'US',
  48. GT: 'GT', GG: 'GB', GN: 'GN', GW: 'GW', GY: 'GY', HT: 'HT', HM: 'AU', VA: 'VA', HN: 'HN', HK: 'CN',
  49. HU: 'HU', IS: 'IS', IN: 'IN', ID: 'ID', IR: 'IR', IQ: 'IQ', IE: 'IE', IM: 'GB', IL: 'IL', IT: 'IT',
  50. JM: 'JM', JP: 'JP', JE: 'GB', JO: 'JO', KZ: 'KZ', KE: 'KE', KI: 'KI', KR: 'KR', KW: 'KW', KG: 'KG',
  51. LA: 'LA', LV: 'LV', LB: 'LB', LS: 'LS', LR: 'LR', LY: 'LY', LI: 'LI', LT: 'LT', LU: 'LU', MO: 'CN',
  52. MK: 'MK', MG: 'MG', MW: 'MW', MY: 'MY', MV: 'MV', ML: 'ML', MT: 'MT', MH: 'MH', MQ: 'FR', MR: 'MR',
  53. MU: 'MU', YT: 'FR', MX: 'MX', FM: 'FM', MD: 'MD', MC: 'MC', MN: 'MN', ME: 'ME', MS: 'GB', MA: 'MA',
  54. MZ: 'MZ', MM: 'MM', NA: 'NA', NR: 'NR', NP: 'NP', NL: 'NL', AN: 'NL', NC: 'FR', NZ: 'NZ', NI: 'NI',
  55. NE: 'NE', NG: 'NG', NU: 'NZ', NF: 'AU', MP: 'US', NO: 'NO', OM: 'OM', PK: 'PK', PW: 'PW', PS: 'IL',
  56. PA: 'PA', PG: 'PG', PY: 'PY', PE: 'PE', PH: 'PH', PN: 'GB', PL: 'PL', PT: 'PT', PR: 'US', QA: 'QA',
  57. RE: 'FR', RO: 'RO', RU: 'RU', RW: 'RW', BL: 'FR', SH: 'GB', KN: 'KN', LC: 'LC', MF: 'FR', PM: 'FR',
  58. VC: 'VC', WS: 'WS', SM: 'SM', ST: 'ST', SA: 'SA', SN: 'SN', RS: 'RS', SC: 'SC', SL: 'SL', SG: 'SG',
  59. SX: 'NL', SK: 'SK', SI: 'SI', SB: 'SB', SO: 'SO', ZA: 'ZA', GS: 'GB', ES: 'ES', LK: 'LK', SD: 'SD',
  60. SR: 'SR', SJ: 'NO', SZ: 'SZ', SE: 'SE', CH: 'CH', SY: 'SY', TW: 'TW', TJ: 'TJ', TZ: 'TZ', TH: 'TH',
  61. TL: 'TL', TG: 'TG', TK: 'NZ', TO: 'TO', TT: 'TT', TN: 'TN', TR: 'TR', TM: 'TM', TC: 'GB', TV: 'TV',
  62. UG: 'UG', UA: 'UA', AE: 'AE', GB: 'GB', US: 'US', UM: 'US', UY: 'UY', UZ: 'UZ', VU: 'VU', VE: 'VE',
  63. VN: 'VN', VG: 'GB', VI: 'US', WF: 'FR', EH: 'MA', YE: 'YE', ZM: 'ZM', ZW: 'ZW', UK: 'GB'
  64. };
  65.  
  66. // Country code to country name mapping
  67. const countryCodeToName = {
  68. 'AF': 'Afghanistan', 'AL': 'Albania', 'DZ': 'Algeria', 'AD': 'Andorra', 'AO': 'Angola',
  69. 'AG': 'Antigua and Barbuda', 'AR': 'Argentina', 'AM': 'Armenia', 'AU': 'Australia', 'AT': 'Austria',
  70. 'AZ': 'Azerbaijan', 'BS': 'Bahamas', 'BH': 'Bahrain', 'BD': 'Bangladesh', 'BB': 'Barbados',
  71. 'BY': 'Belarus', 'BE': 'Belgium', 'BZ': 'Belize', 'BJ': 'Benin', 'BT': 'Bhutan',
  72. 'BO': 'Bolivia', 'BA': 'Bosnia and Herzegovina', 'BW': 'Botswana', 'BR': 'Brazil', 'BN': 'Brunei',
  73. 'BG': 'Bulgaria', 'BF': 'Burkina Faso', 'BI': 'Burundi', 'KH': 'Cambodia', 'CM': 'Cameroon',
  74. 'CA': 'Canada', 'CV': 'Cape Verde', 'CF': 'Central African Republic', 'TD': 'Chad', 'CL': 'Chile',
  75. 'CN': 'China', 'CO': 'Colombia', 'KM': 'Comoros', 'CG': 'Congo', 'CD': 'DR Congo',
  76. 'CR': 'Costa Rica', 'CI': 'Ivory Coast', 'HR': 'Croatia', 'CU': 'Cuba', 'CY': 'Cyprus',
  77. 'CZ': 'Czech Republic', 'DK': 'Denmark', 'DJ': 'Djibouti', 'DM': 'Dominica', 'DO': 'Dominican Republic',
  78. 'EC': 'Ecuador', 'EG': 'Egypt', 'SV': 'El Salvador', 'GQ': 'Equatorial Guinea', 'ER': 'Eritrea',
  79. 'EE': 'Estonia', 'ET': 'Ethiopia', 'FJ': 'Fiji', 'FI': 'Finland', 'FR': 'France',
  80. 'GA': 'Gabon', 'GM': 'Gambia', 'GE': 'Georgia', 'DE': 'Germany', 'GH': 'Ghana',
  81. 'GR': 'Greece', 'GD': 'Grenada', 'GT': 'Guatemala', 'GN': 'Guinea', 'GW': 'Guinea-Bissau',
  82. 'GY': 'Guyana', 'HT': 'Haiti', 'VA': 'Vatican City', 'HN': 'Honduras', 'HU': 'Hungary',
  83. 'IS': 'Iceland', 'IN': 'India', 'ID': 'Indonesia', 'IR': 'Iran', 'IQ': 'Iraq',
  84. 'IE': 'Ireland', 'IL': 'Israel', 'IT': 'Italy', 'JM': 'Jamaica', 'JP': 'Japan',
  85. 'JO': 'Jordan', 'KZ': 'Kazakhstan', 'KE': 'Kenya', 'KI': 'Kiribati', 'KR': 'South Korea',
  86. 'KW': 'Kuwait', 'KG': 'Kyrgyzstan', 'LA': 'Laos', 'LV': 'Latvia', 'LB': 'Lebanon',
  87. 'LS': 'Lesotho', 'LR': 'Liberia', 'LY': 'Libya', 'LI': 'Liechtenstein', 'LT': 'Lithuania',
  88. 'LU': 'Luxembourg', 'MK': 'North Macedonia', 'MG': 'Madagascar', 'MW': 'Malawi', 'MY': 'Malaysia',
  89. 'MV': 'Maldives', 'ML': 'Mali', 'MT': 'Malta', 'MH': 'Marshall Islands', 'MR': 'Mauritania',
  90. 'MU': 'Mauritius', 'MX': 'Mexico', 'FM': 'Micronesia', 'MD': 'Moldova', 'MC': 'Monaco',
  91. 'MN': 'Mongolia', 'ME': 'Montenegro', 'MA': 'Morocco', 'MZ': 'Mozambique', 'MM': 'Myanmar',
  92. 'NA': 'Namibia', 'NR': 'Nauru', 'NP': 'Nepal', 'NL': 'Netherlands', 'NZ': 'New Zealand',
  93. 'NI': 'Nicaragua', 'NE': 'Niger', 'NG': 'Nigeria', 'NO': 'Norway', 'OM': 'Oman',
  94. 'PK': 'Pakistan', 'PW': 'Palau', 'PS': 'Palestine', 'PA': 'Panama', 'PG': 'Papua New Guinea',
  95. 'PY': 'Paraguay', 'PE': 'Peru', 'PH': 'Philippines', 'PL': 'Poland', 'PT': 'Portugal',
  96. 'QA': 'Qatar', 'RO': 'Romania', 'RU': 'Russia', 'RW': 'Rwanda', 'KN': 'Saint Kitts and Nevis',
  97. 'LC': 'Saint Lucia', 'VC': 'Saint Vincent and the Grenadines', 'WS': 'Samoa', 'SM': 'San Marino',
  98. 'ST': 'Sao Tome and Principe', 'SA': 'Saudi Arabia', 'SN': 'Senegal', 'RS': 'Serbia',
  99. 'SC': 'Seychelles', 'SL': 'Sierra Leone', 'SG': 'Singapore', 'SK': 'Slovakia', 'SI': 'Slovenia',
  100. 'SB': 'Solomon Islands', 'SO': 'Somalia', 'ZA': 'South Africa', 'ES': 'Spain', 'LK': 'Sri Lanka',
  101. 'SD': 'Sudan', 'SR': 'Suriname', 'SZ': 'Eswatini', 'SE': 'Sweden', 'CH': 'Switzerland',
  102. 'SY': 'Syria', 'TW': 'Taiwan', 'TJ': 'Tajikistan', 'TZ': 'Tanzania', 'TH': 'Thailand',
  103. 'TL': 'Timor-Leste', 'TG': 'Togo', 'TO': 'Tonga', 'TT': 'Trinidad and Tobago', 'TN': 'Tunisia',
  104. 'TR': 'Turkey', 'TM': 'Turkmenistan', 'TV': 'Tuvalu', 'UG': 'Uganda', 'UA': 'Ukraine',
  105. 'AE': 'United Arab Emirates', 'GB': 'United Kingdom', 'US': 'United States', 'UY': 'Uruguay',
  106. 'UZ': 'Uzbekistan', 'VU': 'Vanuatu', 'VE': 'Venezuela', 'VN': 'Vietnam', 'YE': 'Yemen',
  107. 'ZM': 'Zambia', 'ZW': 'Zimbabwe', 'AQ': 'Antarctica', 'UK': 'United Kingdom'
  108. };
  109.  
  110. // Stats storage
  111. let countryStats = {
  112. wrongGuesses: {}, // format: { "actualCountry": count }
  113. mistakenIdentities: {}, // format: { "actualCountry": { "guessedAs": count } }
  114. pointsLost: {} // format: { "country": { total: number, count: number } }
  115. };
  116.  
  117. // Load stats from localStorage
  118. function loadStats() {
  119. const savedStats = localStorage.getItem('geoguessrCountryStats');
  120. if (savedStats) {
  121. try {
  122. const parsedStats = JSON.parse(savedStats);
  123. countryStats = {
  124. wrongGuesses: parsedStats.wrongGuesses || {},
  125. mistakenIdentities: parsedStats.mistakenIdentities || {},
  126. pointsLost: parsedStats.pointsLost || {}
  127. };
  128. } catch (e) {
  129. console.error("Error loading stats:", e);
  130. countryStats = { wrongGuesses: {}, mistakenIdentities: {}, pointsLost: {} };
  131. }
  132. }
  133. }
  134.  
  135. // Save stats to localStorage
  136. function saveStats() {
  137. localStorage.setItem('geoguessrCountryStats', JSON.stringify(countryStats));
  138. }
  139.  
  140. // Initialize by loading stats
  141. loadStats();
  142.  
  143. // Check if we're in a game
  144. function checkGameMode() {
  145. const isGame = location.pathname.includes("/game/") || location.pathname.includes("/challenge/");
  146. const isMultiplayer = location.pathname.includes("/battle-royale/") ||
  147. location.pathname.includes("/duels/") ||
  148. location.pathname.includes("/party/");
  149. return isGame && !isMultiplayer;
  150. }
  151.  
  152. // Get country code from coordinates
  153. async function getCountryCode(coords) {
  154. if (coords[0] <= -85.05) return 'AQ';
  155. if (API_Key.toLowerCase().match("^(bdc_)?[a-f0-9]{32}$") != null) {
  156. const api = "https://api.bigdatacloud.net/data/reverse-geocode?latitude="+coords.lat+"&longitude="+coords.lng+"&localityLanguage=en&key="+API_Key;
  157. return await fetch(api)
  158. .then(res => (res.status !== 200) ? ERROR_RESP : res.json())
  159. .then(out => (out === ERROR_RESP) ? ERROR_RESP : CountryDict[out.countryCode]);
  160. } else {
  161. const api = `https://nominatim.openstreetmap.org/reverse.php?lat=${coords.lat}&lon=${coords.lng}&zoom=21&format=jsonv2&accept-language=en`;
  162. return await fetch(api)
  163. .then(res => (res.status !== 200) ? ERROR_RESP : res.json())
  164. .then(out => (out === ERROR_RESP) ? ERROR_RESP : CountryDict[out?.address?.country_code?.toUpperCase()]);
  165. }
  166. }
  167.  
  168. // Record a wrong guess
  169. function recordWrongGuess(actualCountry, guessedCountry) {
  170. // Update wrong guesses count
  171. if (!countryStats.wrongGuesses[actualCountry]) {
  172. countryStats.wrongGuesses[actualCountry] = 0;
  173. }
  174. countryStats.wrongGuesses[actualCountry]++;
  175.  
  176. // Update mistaken identity mapping
  177. if (!countryStats.mistakenIdentities[actualCountry]) {
  178. countryStats.mistakenIdentities[actualCountry] = {};
  179. }
  180. if (!countryStats.mistakenIdentities[actualCountry][guessedCountry]) {
  181. countryStats.mistakenIdentities[actualCountry][guessedCountry] = 0;
  182. }
  183. countryStats.mistakenIdentities[actualCountry][guessedCountry]++;
  184.  
  185. // Save updated stats
  186. saveStats();
  187. }
  188.  
  189. // Record points lost for a country (when country is correct but region is wrong)
  190. function recordPointsLost(country, points) {
  191. if (!countryStats.pointsLost[country]) {
  192. countryStats.pointsLost[country] = { total: 0, count: 0 };
  193. }
  194.  
  195. const pointsLost = MAX_POINTS - points;
  196. countryStats.pointsLost[country].total += pointsLost;
  197. countryStats.pointsLost[country].count += 1;
  198.  
  199. // Save updated stats
  200. saveStats();
  201. }
  202.  
  203. // Calculate distance between two coordinates (haversine formula)
  204. function calculateDistance(lat1, lon1, lat2, lon2) {
  205. const R = 6371; // Earth's radius in km
  206. const dLat = (lat2 - lat1) * Math.PI / 180;
  207. const dLon = (lon2 - lon1) * Math.PI / 180;
  208. const a =
  209. Math.sin(dLat/2) * Math.sin(dLat/2) +
  210. Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
  211. Math.sin(dLon/2) * Math.sin(dLon/2);
  212. const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
  213. return R * c; // Distance in km
  214. }
  215.  
  216. // Check for wrong guesses and record stats
  217. function check() {
  218. const gameTag = location.href.substring(location.href.lastIndexOf('/') + 1);
  219. let apiUrl = "https://www.geoguessr.com/api/v3/games/" + gameTag;
  220.  
  221. if (location.pathname.includes("/challenge/")) {
  222. apiUrl = "https://www.geoguessr.com/api/v3/challenges/" + gameTag + "/game";
  223. }
  224.  
  225. fetch(apiUrl)
  226. .then(res => res.json())
  227. .then((out) => {
  228. const guessCounter = out.player.guesses.length;
  229. const round = out.rounds[guessCounter-1];
  230. const guess = out.player.guesses[guessCounter-1];
  231.  
  232. // Skip if this is the same guess as before
  233. if (guess.lat == lastGuess.lat && guess.lng == lastGuess.lng) return;
  234. lastGuess = guess;
  235.  
  236. Promise.all([getCountryCode(guess), getCountryCode(round)]).then(codes => {
  237. if (codes[0] == ERROR_RESP || codes[1] == ERROR_RESP) {
  238. console.log("Error determining countries");
  239. } else if (codes[0] != codes[1]) {
  240. // Wrong country!
  241. recordWrongGuess(codes[1], codes[0]);
  242. } else {
  243. // Correct country but might have lost points due to distance
  244. const distance = calculateDistance(guess.lat, guess.lng, round.lat, round.lng);
  245. const points = guess.roundScoreInPoints || 0;
  246.  
  247. // If they got less than max points, record the points lost
  248. if (points < MAX_POINTS) {
  249. recordPointsLost(codes[0], points);
  250. }
  251. }
  252. updateStatsDisplay();
  253. });
  254. }).catch(err => { console.error("Error fetching game data:", err); });
  255. }
  256.  
  257. // Check function called when results are shown
  258. function doCheck() {
  259. if (!document.querySelector('div[class*="result-layout_root__"]')) {
  260. sessionStorage.setItem("Checked", 0);
  261. } else if ((sessionStorage.getItem("Checked") || 0) == 0) {
  262. check();
  263. sessionStorage.setItem("Checked", 1);
  264. }
  265. }
  266.  
  267. // Create stats panel UI
  268. function createStatsPanel() {
  269. const panel = document.createElement('div');
  270. panel.id = 'country-stats-panel';
  271. panel.style = `
  272. position: fixed;
  273. bottom: 20px;
  274. right: -350px;
  275. width: 320px;
  276. max-height: 500px;
  277. background-color: ${GEOGUESSR_COLORS.dark};
  278. color: ${GEOGUESSR_COLORS.lightText};
  279. border-radius: 8px;
  280. padding: 15px;
  281. transition: right 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  282. z-index: 9999;
  283. overflow-y: auto;
  284. box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
  285. font-family: 'Roboto', 'Open Sans', Arial, sans-serif;
  286. border: 2px solid ${GEOGUESSR_COLORS.darkGray};
  287. `;
  288.  
  289. panel.innerHTML = `
  290. <div style="display: flex; justify-content: space-between; margin-bottom: 15px; align-items: center;">
  291. <div style="display: flex; align-items: center;">
  292. <img src="https://www.geoguessr.com/favicon-32x32.png" style="width: 20px; height: 20px; margin-right: 8px;">
  293. <h2 style="margin: 0; color: ${GEOGUESSR_COLORS.lightText}; font-size: 18px; font-weight: 500;">Country Statistics</h2>
  294. </div>
  295. <button id="close-stats-btn" style="background: none; border: none; color: ${GEOGUESSR_COLORS.lightText}; cursor: pointer; font-size: 18px; padding: 0; width: 24px; height: 24px; display: flex; justify-content: center; align-items: center;">Γ—</button>
  296. </div>
  297. <div id="stats-content" style="margin-bottom: 10px; max-height: 400px; overflow-y: auto; padding-right: 5px;">
  298. <p>Loading stats...</p>
  299. </div>
  300. <div style="margin-top: 15px; display: flex; justify-content: space-between; align-items: center; border-top: 1px solid ${GEOGUESSR_COLORS.darkGray}; padding-top: 10px;">
  301. <button id="clear-stats-btn" style="background: ${GEOGUESSR_COLORS.danger}; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; transition: all 0.2s ease;">Reset</button>
  302. <div id="stats-mode-switch" style="display: flex; align-items: center;">
  303. <span style="margin-right: 8px; font-size: 14px; color: ${GEOGUESSR_COLORS.lightTextSecondary};">View: </span>
  304. <select id="stats-view-mode" style="background: ${GEOGUESSR_COLORS.mediumGray}; color: white; border: none; padding: 6px 10px; border-radius: 4px; font-size: 13px; outline: none; cursor: pointer;">
  305. <option value="missed">Most Missed</option>
  306. <option value="confused">Confused With</option>
  307. <option value="points">Most Missed Points</option>
  308. </select>
  309. </div>
  310. </div>
  311. `;
  312.  
  313. document.body.appendChild(panel);
  314.  
  315. // Make stats content scrollable with custom styling
  316. const statsContent = document.getElementById('stats-content');
  317. statsContent.style.scrollbarWidth = 'thin';
  318. statsContent.style.scrollbarColor = `${GEOGUESSR_COLORS.lightGray} ${GEOGUESSR_COLORS.dark}`;
  319.  
  320. // Add toggle button
  321. const toggleButton = document.createElement('div');
  322. toggleButton.id = 'toggle-stats-button';
  323. toggleButton.style = `
  324. position: fixed;
  325. bottom: 20px;
  326. right: 20px;
  327. background-color: ${GEOGUESSR_COLORS.primary};
  328. color: white;
  329. padding: 10px 15px;
  330. border-radius: 50px;
  331. cursor: pointer;
  332. z-index: 9998;
  333. font-weight: 500;
  334. font-size: 14px;
  335. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
  336. transition: all 0.2s ease;
  337. display: flex;
  338. align-items: center;
  339. justify-content: center;
  340. `;
  341. toggleButton.innerHTML = 'πŸ“Š Stats';
  342. toggleButton.addEventListener('mouseover', () => {
  343. toggleButton.style.backgroundColor = '#3e8a43'; // Darken on hover
  344. toggleButton.style.transform = 'scale(1.05)';
  345. });
  346. toggleButton.addEventListener('mouseout', () => {
  347. toggleButton.style.backgroundColor = GEOGUESSR_COLORS.primary;
  348. toggleButton.style.transform = 'scale(1)';
  349. });
  350. document.body.appendChild(toggleButton);
  351.  
  352. // Add event listeners
  353. document.getElementById('toggle-stats-button').addEventListener('click', toggleStatsPanel);
  354. document.getElementById('close-stats-btn').addEventListener('click', closeStatsPanel);
  355. document.getElementById('clear-stats-btn').addEventListener('click', clearStats);
  356. document.getElementById('stats-view-mode').addEventListener('change', updateStatsDisplay);
  357.  
  358. // Style the clear button with hover effects
  359. const clearBtn = document.getElementById('clear-stats-btn');
  360. clearBtn.addEventListener('mouseover', () => {
  361. clearBtn.style.backgroundColor = '#b52828'; // Darken red on hover
  362. });
  363. clearBtn.addEventListener('mouseout', () => {
  364. clearBtn.style.backgroundColor = GEOGUESSR_COLORS.danger;
  365. });
  366.  
  367. updateStatsDisplay();
  368. }
  369.  
  370. // Toggle stats panel visibility
  371. function toggleStatsPanel() {
  372. const panel = document.getElementById('country-stats-panel');
  373. if (panel.style.right === '20px') {
  374. panel.style.right = '-350px';
  375. } else {
  376. panel.style.right = '20px';
  377. }
  378. }
  379.  
  380. // Close stats panel
  381. function closeStatsPanel() {
  382. document.getElementById('country-stats-panel').style.right = '-350px';
  383. }
  384.  
  385. // Clear all statistics
  386. function clearStats() {
  387. if (confirm('Are you sure you want to reset all country statistics?')) {
  388. countryStats = { wrongGuesses: {}, mistakenIdentities: {}, pointsLost: {} };
  389. saveStats();
  390. updateStatsDisplay();
  391. }
  392. }
  393.  
  394. // Update the stats display
  395. function updateStatsDisplay() {
  396. const statsContent = document.getElementById('stats-content');
  397. if (!statsContent) return;
  398.  
  399. const viewMode = document.getElementById('stats-view-mode').value;
  400. let content = '';
  401.  
  402. // Shared table styles
  403. const tableStyle = `
  404. width: 100%;
  405. border-collapse: collapse;
  406. border-radius: 4px;
  407. overflow: hidden;
  408. margin-bottom: 10px;
  409. font-size: 14px;
  410. `;
  411.  
  412. const headerStyle = `
  413. background: ${GEOGUESSR_COLORS.darkGray};
  414. text-align: left;
  415. padding: 8px 10px;
  416. font-weight: 500;
  417. color: ${GEOGUESSR_COLORS.lightText};
  418. `;
  419.  
  420. const cellStyle = `
  421. padding: 8px 10px;
  422. border-bottom: 1px solid ${GEOGUESSR_COLORS.darkGray};
  423. `;
  424.  
  425. const rowHoverJS = `
  426. const rows = document.querySelectorAll('tr.data-row');
  427. rows.forEach(row => {
  428. row.addEventListener('mouseover', () => {
  429. row.style.backgroundColor = '${GEOGUESSR_COLORS.mediumGray}';
  430. });
  431. row.addEventListener('mouseout', () => {
  432. row.style.backgroundColor = 'transparent';
  433. });
  434. });
  435. `;
  436.  
  437. if (viewMode === 'missed') {
  438. // Sort countries by most missed
  439. const sortedCountries = Object.entries(countryStats.wrongGuesses)
  440. .sort((a, b) => b[1] - a[1]);
  441.  
  442. if (sortedCountries.length === 0) {
  443. content = `<div style="text-align: center; padding: 20px; color: ${GEOGUESSR_COLORS.lightTextSecondary};">
  444. <div style="font-size: 24px; margin-bottom: 10px;">πŸ“</div>
  445. <p>No wrong guesses recorded yet.</p>
  446. <p style="font-size: 13px;">Play some games to collect statistics!</p>
  447. </div>`;
  448. } else {
  449. content = `<h3 style="margin-top: 0; font-weight: 500; color: ${GEOGUESSR_COLORS.primary};">Countries You Miss Most Often</h3>`;
  450. content += `<table style="${tableStyle}">`;
  451. content += `<tr>
  452. <th style="${headerStyle} width: 60%;">Country</th>
  453. <th style="${headerStyle} width: 40%; text-align: right;">Missed</th>
  454. </tr>`;
  455.  
  456. sortedCountries.forEach(([code, count], index) => {
  457. const countryName = countryCodeToName[code] || code;
  458. const opacity = Math.max(1 - (index * 0.05), 0.5); // Fade out for lower entries
  459.  
  460. content += `<tr class="data-row">
  461. <td style="${cellStyle} opacity: ${opacity};">${countryName}</td>
  462. <td style="${cellStyle} text-align: right; opacity: ${opacity};">${count}</td>
  463. </tr>`;
  464. });
  465.  
  466. content += '</table>';
  467. }
  468. } else if (viewMode === 'confused') {
  469. // Confused with mode
  470. content = `<h3 style="margin-top: 0; font-weight: 500; color: ${GEOGUESSR_COLORS.primary};">Country Confusion Analysis</h3>`;
  471. content += `<p style="font-size: 14px; color: ${GEOGUESSR_COLORS.lightTextSecondary};">Select a country to see what you confuse it with:</p>`;
  472.  
  473. content += `<select id="country-select" style="
  474. width: 100%;
  475. margin-bottom: 15px;
  476. padding: 8px 10px;
  477. background: ${GEOGUESSR_COLORS.mediumGray};
  478. color: ${GEOGUESSR_COLORS.lightText};
  479. border: none;
  480. border-radius: 4px;
  481. outline: none;
  482. cursor: pointer;
  483. font-size: 14px;
  484. ">`;
  485. content += '<option value="">-- Select a country --</option>';
  486.  
  487. const countries = Object.keys(countryStats.mistakenIdentities).sort((a, b) => {
  488. const nameA = countryCodeToName[a] || a;
  489. const nameB = countryCodeToName[b] || b;
  490. return nameA.localeCompare(nameB);
  491. });
  492.  
  493. if (countries.length === 0) {
  494. content = `<div style="text-align: center; padding: 20px; color: ${GEOGUESSR_COLORS.lightTextSecondary};">
  495. <div style="font-size: 24px; margin-bottom: 10px;">πŸ—ΊοΈ</div>
  496. <p>No confusion data available yet.</p>
  497. <p style="font-size: 13px;">Play more games to see which countries you confuse!</p>
  498. </div>`;
  499. } else {
  500. countries.forEach(code => {
  501. const countryName = countryCodeToName[code] || code;
  502. content += `<option value="${code}">${countryName}</option>`;
  503. });
  504.  
  505. content += '</select>';
  506. content += '<div id="confusion-details"></div>';
  507. }
  508. } else if (viewMode === 'points') {
  509. // Most missed points mode (correct country but wrong region)
  510. const pointsData = [];
  511.  
  512. // Calculate average points lost per country
  513. Object.entries(countryStats.pointsLost).forEach(([country, data]) => {
  514. const avgPointsLost = Math.round(data.total / data.count);
  515. pointsData.push({ country, avgPointsLost, occurrences: data.count });
  516. });
  517.  
  518. // Sort by average points lost (descending)
  519. pointsData.sort((a, b) => b.avgPointsLost - a.avgPointsLost);
  520.  
  521. if (pointsData.length === 0) {
  522. content = `<div style="text-align: center; padding: 20px; color: ${GEOGUESSR_COLORS.lightTextSecondary};">
  523. <div style="font-size: 24px; margin-bottom: 10px;">🎯</div>
  524. <p>No points data recorded yet.</p>
  525. <p style="font-size: 13px;">This tracks when you get the country right but miss the exact location.</p>
  526. </div>`;
  527. } else {
  528. content = `<h3 style="margin-top: 0; font-weight: 500; color: ${GEOGUESSR_COLORS.primary};">Most Points Lost by Country</h3>`;
  529. content += `<p style="font-size: 14px; color: ${GEOGUESSR_COLORS.lightTextSecondary};">These are countries where you get the country right but miss the region:</p>`;
  530.  
  531. content += `<table style="${tableStyle}">`;
  532. content += `<tr>
  533. <th style="${headerStyle} width: 40%;">Country</th>
  534. <th style="${headerStyle} width: 30%; text-align: right;">Avg. Points Lost</th>
  535. <th style="${headerStyle} width: 30%; text-align: right;">Occurrences</th>
  536. </tr>`;
  537.  
  538. pointsData.forEach(({ country, avgPointsLost, occurrences }, index) => {
  539. const countryName = countryCodeToName[country] || country;
  540. const opacity = Math.max(1 - (index * 0.05), 0.5); // Fade out for lower entries
  541.  
  542. // Calculate color based on points lost (red gradient)
  543. const pointsPercentage = Math.min(avgPointsLost / 5000, 1);
  544. const red = Math.floor(255 * pointsPercentage);
  545. const green = Math.floor(100 * (1 - pointsPercentage));
  546. const blue = Math.floor(100 * (1 - pointsPercentage));
  547. const pointsColor = `rgb(${red}, ${green}, ${blue})`;
  548.  
  549. content += `<tr class="data-row">
  550. <td style="${cellStyle} opacity: ${opacity};">${countryName}</td>
  551. <td style="${cellStyle} text-align: right; color: ${pointsColor}; opacity: ${opacity};">${avgPointsLost}</td>
  552. <td style="${cellStyle} text-align: right; opacity: ${opacity};">${occurrences}</td>
  553. </tr>`;
  554. });
  555.  
  556. content += '</table>';
  557. }
  558. }
  559.  
  560. statsContent.innerHTML = content;
  561.  
  562. // Add event listener for country select if in confused mode
  563. if (viewMode === 'confused' && Object.keys(countryStats.mistakenIdentities).length > 0) {
  564. const countrySelect = document.getElementById('country-select');
  565. if (countrySelect) {
  566. countrySelect.addEventListener('change', showConfusionDetails);
  567. }
  568. }
  569.  
  570. // Add hover effects for table rows
  571. const script = document.createElement('script');
  572. script.textContent = rowHoverJS;
  573. document.body.appendChild(script);
  574. setTimeout(() => script.remove(), 100); // Clean up script tag after execution
  575. }
  576.  
  577. // Show details about what a country is confused with
  578. function showConfusionDetails() {
  579. const countryCode = document.getElementById('country-select').value;
  580. const detailsDiv = document.getElementById('confusion-details');
  581.  
  582. if (!countryCode || !detailsDiv) {
  583. return;
  584. }
  585.  
  586. const countryName = countryCodeToName[countryCode] || countryCode;
  587. const confusions = countryStats.mistakenIdentities[countryCode];
  588.  
  589. if (!confusions) {
  590. detailsDiv.innerHTML = `<p style="color: ${GEOGUESSR_COLORS.lightTextSecondary};">No data for ${countryName}.</p>`;
  591. return;
  592. }
  593.  
  594. // Sort by most frequent confusions
  595. const sortedConfusions = Object.entries(confusions)
  596. .sort((a, b) => b[1] - a[1]);
  597.  
  598. const tableStyle = `
  599. width: 100%;
  600. border-collapse: collapse;
  601. border-radius: 4px;
  602. overflow: hidden;
  603. margin-top: 10px;
  604. font-size: 14px;
  605. `;
  606.  
  607. const headerStyle = `
  608. background: ${GEOGUESSR_COLORS.darkGray};
  609. text-align: left;
  610. padding: 8px 10px;
  611. font-weight: 500;
  612. color: ${GEOGUESSR_COLORS.lightText};
  613. `;
  614.  
  615. const cellStyle = `
  616. padding: 8px 10px;
  617. border-bottom: 1px solid ${GEOGUESSR_COLORS.darkGray};
  618. `;
  619.  
  620. let content = `<div style="margin-top: 15px; padding-top: 10px; border-top: 1px solid ${GEOGUESSR_COLORS.darkGray};">`;
  621. content += `<h4 style="margin-top: 0; color: ${GEOGUESSR_COLORS.secondary}; font-weight: 500;">When it's ${countryName}, you guess:</h4>`;
  622. content += `<table style="${tableStyle}">`;
  623. content += `<tr>
  624. <th style="${headerStyle} width: 70%;">Country</th>
  625. <th style="${headerStyle} width: 30%; text-align: right;">Times</th>
  626. </tr>`;
  627.  
  628. sortedConfusions.forEach(([code, count], index) => {
  629. const guessName = countryCodeToName[code] || code;
  630. const opacity = Math.max(1 - (index * 0.05), 0.6); // Fade out for lower entries
  631.  
  632. content += `<tr class="data-row">
  633. <td style="${cellStyle} opacity: ${opacity};">${guessName}</td>
  634. <td style="${cellStyle} text-align: right; opacity: ${opacity};">${count}</td>
  635. </tr>`;
  636. });
  637.  
  638. content += '</table></div>';
  639.  
  640. detailsDiv.innerHTML = content;
  641.  
  642. // Add hover effects for table rows
  643. const script = document.createElement('script');
  644. script.textContent = `
  645. const rows = document.querySelectorAll('#confusion-details tr.data-row');
  646. rows.forEach(row => {
  647. row.addEventListener('mouseover', () => {
  648. row.style.backgroundColor = '${GEOGUESSR_COLORS.mediumGray}';
  649. });
  650. row.addEventListener('mouseout', () => {
  651. row.style.backgroundColor = 'transparent';
  652. });
  653. });
  654. `;
  655. document.body.appendChild(script);
  656. setTimeout(() => script.remove(), 100); // Clean up script tag after execution
  657. }
  658.  
  659. // Initialize UI if not already done
  660. function initializeUI() {
  661. if (!document.getElementById('country-stats-panel')) {
  662. createStatsPanel();
  663. }
  664. }
  665.  
  666. // Set up mutation observer to detect when results are shown
  667. let lastDoCheckCall = 0;
  668. new MutationObserver(async (mutations) => {
  669. if (!checkGameMode() || lastDoCheckCall >= (Date.now() - 50)) return;
  670. lastDoCheckCall = Date.now();
  671. await scanStyles();
  672. if (AUTOMATIC) doCheck();
  673. initializeUI();
  674. }).observe(document.body, { subtree: true, childList: true });
  675.  
  676. // Initialize the UI on first load
  677. window.addEventListener('load', initializeUI);
  678. })();
Tags: Geoguessr
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement