MouseHunt - Event Reward Tracker

Tool that tracks bespoke event rewards (e.g. GWH & Birthday) for analysis

  1. // ==UserScript==
  2. // @name MouseHunt - Event Reward Tracker
  3. // @author Tran Situ (tsitu)
  4. // @namespace https://greasyfork.org/en/users/232363-tsitu
  5. // @version 2.0
  6. // @description Tool that tracks bespoke event rewards (e.g. GWH & Birthday) for analysis
  7. // @match http://www.mousehuntgame.com/*
  8. // @match https://www.mousehuntgame.com/*
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. // Generate toast popup UI
  13. const toast = document.createElement("fieldset");
  14. toast.style.visibility = "hidden";
  15. toast.style.position = "fixed";
  16. toast.style.left = "40px";
  17. toast.style.top = "40px";
  18. toast.style.width = "250px";
  19. toast.style.height = "auto";
  20. toast.style.textAlign = "center";
  21. toast.style.fontSize = "16px";
  22. toast.style.border = "2px dotted black";
  23.  
  24. const toastLegend = document.createElement("legend");
  25. toastLegend.style.fontSize = "10px";
  26. toastLegend.innerText = "MH Event Reward Tracker v2.0";
  27.  
  28. const toastText = document.createElement("span");
  29. toast.appendChild(toastText);
  30. toast.appendChild(toastLegend);
  31. document.body.appendChild(toast);
  32.  
  33. /**
  34. * Shows custom toast message
  35. * @param {string} message Message to be displayed inside <span>
  36. * @param {string} color Background color of the toast <fieldset> element
  37. */
  38. function generateToast(message, color) {
  39. toastText.innerText = message;
  40. toast.style.backgroundColor = color;
  41. toast.style.visibility = "visible";
  42. setTimeout(() => {
  43. toast.style.visibility = "hidden";
  44. }, 3000);
  45. }
  46.  
  47. const originalOpen = XMLHttpRequest.prototype.open;
  48. XMLHttpRequest.prototype.open = function() {
  49. this.addEventListener("load", function() {
  50. if (
  51. this.responseURL.indexOf(
  52. "mousehuntgame.com/managers/ajax/events/birthday_factory.php"
  53. ) >= 0 &&
  54. Date.now() < 1585699200000 // (GMT): Wednesday, April 1, 2020 12:00:00 AM [Birthday 2020 definitive end]
  55. ) {
  56. try {
  57. const data = JSON.parse(this.responseText);
  58. if (data) bday2020Parse(data);
  59. } catch (error) {
  60. console.log("Failed to parse server response for Birthday 2020");
  61. console.error(error.stack);
  62. }
  63. }
  64. });
  65.  
  66. this.addEventListener("load", function() {
  67. if (
  68. this.responseURL.indexOf(
  69. "mousehuntgame.com/managers/ajax/events/winter_hunt.php"
  70. ) >= 0 &&
  71. Date.now() < 1579219200000 // (GMT): Friday, January 17, 2020 12:00:00 AM [GWH 2019 definitive end]
  72. ) {
  73. try {
  74. const data = JSON.parse(this.responseText);
  75. if (data) gwh2019Parse(data);
  76. } catch (error) {
  77. console.log("Failed to parse server response for GWH 2019");
  78. console.error(error.stack);
  79. }
  80. }
  81. });
  82.  
  83. originalOpen.apply(this, arguments);
  84. };
  85.  
  86. /**
  87. * Parses Birthday 2020 server response and fires POST to Aard's endpoint
  88. * @param {object} data JSON-parsed server response
  89. */
  90. function bday2020Parse(data) {
  91. const msgs = data.messageData.message_model.messages;
  92. if (msgs && msgs.length > 0) {
  93. for (let msg of msgs) {
  94. const content = msg.messageData.content;
  95. if (content) {
  96. const title = content.title;
  97. if (title.indexOf("claimed my cheese crate") >= 0) {
  98. // Initialize data object
  99. const userID = data.user.user_id;
  100. const obj = {};
  101. obj[userID] = [];
  102.  
  103. // Initialize DOM parsing of HTML string
  104. const body = content.body;
  105. const dom = new DOMParser();
  106. const doc = dom.parseFromString(body, "text/html");
  107.  
  108. doc
  109. .querySelectorAll(".birthday2020ClaimReward-item-details")
  110. .forEach(el => {
  111. const item = el.textContent;
  112. if (
  113. item.indexOf("to unlock") >= 0 ||
  114. item.indexOf("brie+") >= 0
  115. ) {
  116. // Skip locked boxes as well as [Emp] SB+
  117. } else {
  118. obj[userID].push(item);
  119. }
  120. });
  121.  
  122. if (obj[userID].length > 0) {
  123. // Send payload to Google Forms
  124. const xhr = new XMLHttpRequest();
  125. xhr.open(
  126. "POST",
  127. "https://script.google.com/macros/s/AKfycbwdxCoJwShmV5CcUBWpq_8Y6joww29cdIZrH2XO/exec"
  128. );
  129. xhr.setRequestHeader(
  130. "content-type",
  131. "application/x-www-form-urlencoded"
  132. );
  133. xhr.onload = function() {
  134. const response = xhr.responseText;
  135. if (
  136. response.indexOf("success") >= 0 &&
  137. response.indexOf("Thanks") >= 0
  138. ) {
  139. generateToast(
  140. `${Date(
  141. Date.now()
  142. ).toLocaleString()}\n\nBirthday 2020 crate data submitted successfully!`,
  143. "#caf4ae"
  144. );
  145. }
  146. };
  147. xhr.onerror = function() {
  148. console.error(xhr.statusText);
  149. generateToast(
  150. `${Date(
  151. Date.now()
  152. ).toLocaleString()}\n\nBirthday 2020 crate data submission failed`,
  153. "#ffc0cb"
  154. );
  155. };
  156. xhr.send(`crateCheese=${JSON.stringify(obj)}`);
  157. }
  158. }
  159. }
  160. }
  161. }
  162. }
  163.  
  164. /**
  165. * Parses GWH 2019 server response and fires POST to Aard's endpoint
  166. * @param {object} data JSON-parsed server response
  167. */
  168. function gwh2019Parse(data) {
  169. const msgs = data.messageData.message_model.messages;
  170. if (msgs && msgs.length > 0) {
  171. for (let msg of msgs) {
  172. const content = msg.messageData.content;
  173. if (content) {
  174. const title = content.title;
  175. if (title.indexOf("Snow Golem reward") >= 0) {
  176. // Parse location name
  177. let locationName = "N/A";
  178. if (title.indexOf(" from the ") >= 0) {
  179. locationName = title.split("from the ")[1].split("!")[0];
  180. } else {
  181. locationName = title.split("from ")[1].split("!")[0];
  182. }
  183.  
  184. // Miscellaneous tidbits
  185. // const level = title
  186. // .split("claimed a Level ")[1]
  187. // .split(" Snow Golem")[0];
  188. // parseInt(level);
  189. // const journalID =
  190. // msg.messageData.stream_publish_data.params.journal_id;
  191. // msg.messageDate;
  192.  
  193. // Initialize data object
  194. const userID = msg.messageData.stream_publish_data.params.user_id;
  195. const obj = {};
  196. obj[userID] = {};
  197. obj[userID][locationName] = {};
  198.  
  199. // Initialize DOM parsing of HTML string
  200. const body = content.body;
  201. const dom = new DOMParser();
  202. const doc = dom.parseFromString(body, "text/html");
  203. const itemDiv = doc.querySelector(
  204. ".winterHunt2019-claimRewardPopup-content"
  205. );
  206. itemDiv
  207. .querySelectorAll(".winterHunt2019-claimRewardPopup-item")
  208. .forEach(el => {
  209. const rarityEl = el.querySelector(
  210. ".winterHunt2019-claimRewardPopup-item-rarity"
  211. );
  212. const qtyEl = el.querySelector(".quantity");
  213. const itemEl = el.querySelector(
  214. ".winterHunt2019-claimRewardPopup-item-name"
  215. );
  216. if (rarityEl && qtyEl && itemEl) {
  217. let rarity = rarityEl.textContent;
  218. rarity = rarity == "Magical Hat" ? "Hat" : rarity;
  219. rarity = rarity.charAt(0).toUpperCase() + rarity.slice(1);
  220. const quantity = parseInt(
  221. qtyEl.textContent.replace(/,/g, "") // Trim commas (e.g. for gold qty)
  222. );
  223. let item = itemEl.textContent;
  224.  
  225. // Item name edge cases
  226. if (item.indexOf("SUPER|brie") >= 0) item = "SUPER|brie+";
  227.  
  228. // Fixed qty rolls -> Avg/Raw = % Chance
  229. // e.g. total qty of 20 in 40 rolls -> 20/40 = 0.5 avg -> 0.5 / 5 per roll = 10% chance
  230. if (obj[userID][locationName][rarity] === undefined) {
  231. obj[userID][locationName][rarity] = {};
  232. obj[userID][locationName][rarity].count = 1;
  233. obj[userID][locationName][rarity][item] = quantity;
  234. } else {
  235. if (obj[userID][locationName][rarity][item] === undefined) {
  236. obj[userID][locationName][rarity][item] = quantity;
  237. } else {
  238. obj[userID][locationName][rarity][item] += quantity;
  239. }
  240. obj[userID][locationName][rarity].count += 1;
  241. }
  242. }
  243. });
  244.  
  245. if (Object.keys(obj).length > 0) {
  246. // Send payload to Google Forms
  247. const xhr = new XMLHttpRequest();
  248. xhr.open(
  249. "POST",
  250. // "https://cors-anywhere.herokuapp.com/",
  251. "https://script.google.com/macros/s/AKfycbz1JH5rngtwVGoMx_5550TALj4WFdoTxqYbCQjFS-pIGToIA6Q/exec"
  252. );
  253. xhr.setRequestHeader(
  254. "content-type",
  255. "application/x-www-form-urlencoded"
  256. );
  257. xhr.onload = function() {
  258. const response = xhr.responseText;
  259. if (
  260. response.indexOf("success") >= 0 &&
  261. response.indexOf("Thanks") >= 0
  262. ) {
  263. generateToast(
  264. `${Date(
  265. Date.now()
  266. ).toLocaleString()}\n\n[${locationName}]\n\nGolem data submitted successfully!`,
  267. "#caf4ae"
  268. );
  269. }
  270. };
  271. xhr.onerror = function() {
  272. console.error(xhr.statusText);
  273. generateToast(
  274. `${Date(
  275. Date.now()
  276. ).toLocaleString()}\n\nGolem data submission failed`,
  277. "#ffc0cb"
  278. );
  279. };
  280. xhr.send(`golemString=${JSON.stringify(obj)}`);
  281. }
  282. }
  283. }
  284. }
  285. }
  286. }
  287. })();