MouseHunt - TEM Catch Stats

Adds catch/crown statistics next to mouse names on the TEM

当前为 2020-11-04 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name MouseHunt - TEM Catch Stats
  3. // @author Tran Situ (tsitu)
  4. // @namespace https://greasyfork.org/en/users/232363-tsitu
  5. // @version 1.8.4
  6. // @description Adds catch/crown statistics next to mouse names on the TEM
  7. // @grant GM_addStyle
  8. // @match http://www.mousehuntgame.com/*
  9. // @match https://www.mousehuntgame.com/*
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. // Hide scrollbar in TEM to fix many width issues
  14. // https://stackoverflow.com/questions/3296644/hiding-the-scroll-bar-on-an-html-page/54984409#54984409
  15. GM_addStyle(
  16. ".campPage-trap-trapEffectiveness-content { scrollbar-width: none; -ms-overflow-style: none; }" // FF / IE / Edge
  17. );
  18. GM_addStyle(
  19. ".campPage-trap-trapEffectiveness-content::-webkit-scrollbar { width: 0px; }" // Chrome / Safari / Opera
  20. );
  21.  
  22. // Observers are attached to a *specific* element (will DC if removed from DOM)
  23. const observerTarget = document.querySelector(
  24. ".mousehuntHud-page-tabContentContainer"
  25. );
  26. if (observerTarget) {
  27. MutationObserver =
  28. window.MutationObserver ||
  29. window.WebKitMutationObserver ||
  30. window.MozMutationObserver;
  31.  
  32. function mutationCallback() {
  33. const labels = document.getElementsByClassName(
  34. "campPage-trap-trapEffectiveness-difficultyGroup-label"
  35. );
  36. const blueprintContainer = document.querySelector(
  37. ".campPage-trap-blueprintContainer"
  38. );
  39.  
  40. // Render if difficulty labels are in DOM
  41. if (labels.length > 0) {
  42. // Disconnect and reconnect later to prevent infinite mutation loop
  43. observer.disconnect();
  44.  
  45. // Clear out old elements
  46. // Uses static collection instead of live one from getElementsByClassName
  47. document
  48. .querySelectorAll(".tsitu-tem-catches")
  49. .forEach(el => el.remove());
  50.  
  51. render();
  52.  
  53. observer.observe(observerTarget, {
  54. childList: true,
  55. subtree: true
  56. });
  57. }
  58. }
  59.  
  60. const observer = new MutationObserver(function () {
  61. mutationCallback();
  62. });
  63.  
  64. observer.observe(observerTarget, {
  65. childList: true,
  66. subtree: true
  67. });
  68.  
  69. /**
  70. * Zoom Detection
  71. * https://stackoverflow.com/questions/995914/catch-browsers-zoom-event-in-javascript/52008131#52008131
  72. */
  73. let px_ratio =
  74. window.devicePixelRatio ||
  75. window.screen.availWidth / document.documentElement.clientWidth;
  76.  
  77. function isZooming() {
  78. let newPx_ratio =
  79. window.devicePixelRatio ||
  80. window.screen.availWidth / document.documentElement.clientWidth;
  81.  
  82. if (newPx_ratio != px_ratio) {
  83. px_ratio = newPx_ratio;
  84. // console.log("Zooming");
  85. return true;
  86. } else {
  87. // console.log("Resizing");
  88. return false;
  89. }
  90. }
  91.  
  92. window.onresize = function () {
  93. if (isZooming()) {
  94. mutationCallback();
  95. }
  96. };
  97. }
  98.  
  99. function postReq(form) {
  100. return new Promise((resolve, reject) => {
  101. const xhr = new XMLHttpRequest();
  102. xhr.open(
  103. "POST",
  104. `https://www.mousehuntgame.com/managers/ajax/mice/getstat.php?sn=Hitgrab&hg_is_ajax=1&action=get_hunting_stats&uh=${user.unique_hash}`,
  105. true
  106. );
  107. xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
  108. xhr.onreadystatechange = function () {
  109. if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
  110. resolve(this);
  111. }
  112. };
  113. xhr.onerror = function () {
  114. reject(this);
  115. };
  116. xhr.send(form);
  117. });
  118. }
  119.  
  120. function render() {
  121. // Track crown counts
  122. let mouseCount = 0;
  123. const crownYes = {
  124. bronze: [0, "#b75935"],
  125. silver: [0, "#778aa2"],
  126. gold: [0, "#df7113"],
  127. platinum: [0, "#3f4682"],
  128. diamond: [0, "#79aebd"]
  129. };
  130. const crownNo = {
  131. bronze: [0, "#b75935"],
  132. silver: [0, "#778aa2"],
  133. gold: [0, "#df7113"],
  134. platinum: [0, "#3f4682"],
  135. diamond: [0, "#79aebd"]
  136. };
  137.  
  138. // Render crown image and catch number next to mouse name
  139. const rawStore = localStorage.getItem("mh-catch-stats");
  140. if (rawStore) {
  141. const stored = JSON.parse(rawStore);
  142.  
  143. // Clean up ' Mouse' affixes
  144. const newStored = {};
  145. const storedKeys = Object.keys(stored);
  146. storedKeys.forEach(key => {
  147. if (key.indexOf(" Mouse") >= 0) {
  148. // const newKey = key.split(" Mouse")[0];
  149. const newKey = key.replace(/\ mouse$/i, ""); // Doesn't capture Dread Pirate Mousert
  150. newStored[newKey] = stored[key];
  151. } else {
  152. newStored[key] = stored[key];
  153. }
  154. });
  155.  
  156. const rows = document.querySelectorAll(
  157. ".campPage-trap-trapEffectiveness-mouse-name"
  158. );
  159.  
  160. if (rows) {
  161. // Initial auto container height
  162. document
  163. .querySelectorAll(".campPage-trap-trapEffectiveness-mouse")
  164. .forEach(el => {
  165. el.style.height = "auto";
  166. });
  167.  
  168. rows.forEach(row => {
  169. const name = row.textContent;
  170. // const name = "Super Mega Mecha Ultra RoboGold"; // Testing
  171. // row.textContent = name; // Testing
  172.  
  173. const catches = newStored[name];
  174.  
  175. const span = document.createElement("span");
  176. span.className = "tsitu-tem-catches";
  177.  
  178. const outer = document.createElement("span");
  179. outer.className = "mousebox";
  180. outer.style.width = "auto";
  181. outer.style.height = "auto";
  182. outer.style.margin = "0px";
  183. outer.style.paddingRight = "8px";
  184. outer.style.float = "none";
  185.  
  186. const crownImg = document.createElement("img");
  187. crownImg.style.width = "20px";
  188. crownImg.style.height = "20px";
  189. crownImg.style.top = "5px";
  190. crownImg.style.right = "-5px";
  191. crownImg.style.position = "relative";
  192.  
  193. function getCrownSrc(type) {
  194. return `https://www.mousehuntgame.com/images/ui/crowns/crown_${type}.png`;
  195. }
  196.  
  197. if (!catches || catches < 10) {
  198. crownImg.src = getCrownSrc("none");
  199. crownNo.bronze[0] += 1;
  200. crownNo.silver[0] += 1;
  201. crownNo.gold[0] += 1;
  202. crownNo.platinum[0] += 1;
  203. crownNo.diamond[0] += 1;
  204. } else if (catches >= 10 && catches < 100) {
  205. crownImg.src = getCrownSrc("bronze");
  206. crownYes.bronze[0] += 1;
  207. crownNo.silver[0] += 1;
  208. crownNo.gold[0] += 1;
  209. crownNo.platinum[0] += 1;
  210. crownNo.diamond[0] += 1;
  211. } else if (catches >= 100 && catches < 500) {
  212. crownImg.src = getCrownSrc("silver");
  213. crownYes.bronze[0] += 1;
  214. crownYes.silver[0] += 1;
  215. crownNo.gold[0] += 1;
  216. crownNo.platinum[0] += 1;
  217. crownNo.diamond[0] += 1;
  218. } else if (catches >= 500 && catches < 1000) {
  219. crownImg.src = getCrownSrc("gold");
  220. crownYes.bronze[0] += 1;
  221. crownYes.silver[0] += 1;
  222. crownYes.gold[0] += 1;
  223. crownNo.platinum[0] += 1;
  224. crownNo.diamond[0] += 1;
  225. } else if (catches >= 1000 && catches < 2500) {
  226. crownImg.src = getCrownSrc("platinum");
  227. crownYes.bronze[0] += 1;
  228. crownYes.silver[0] += 1;
  229. crownYes.gold[0] += 1;
  230. crownYes.platinum[0] += 1;
  231. crownNo.diamond[0] += 1;
  232. } else if (catches >= 2500) {
  233. crownImg.src = getCrownSrc("diamond");
  234. crownYes.bronze[0] += 1;
  235. crownYes.silver[0] += 1;
  236. crownYes.gold[0] += 1;
  237. crownYes.platinum[0] += 1;
  238. crownYes.diamond[0] += 1;
  239. }
  240.  
  241. mouseCount += 1;
  242. outer.innerText = catches || 0;
  243. outer.appendChild(crownImg);
  244. span.appendChild(document.createElement("br"));
  245. span.appendChild(outer);
  246. row.appendChild(span);
  247. });
  248.  
  249. // Adjust heights after the fact
  250. document
  251. .querySelectorAll(".campPage-trap-trapEffectiveness-mouse")
  252. .forEach(el => {
  253. el.style.height = `${el.offsetHeight + 2}px`;
  254.  
  255. // Height based on mouse name
  256. // const name = el.querySelector(
  257. // ".campPage-trap-trapEffectiveness-mouse-name"
  258. // );
  259. // el.style.height = `${name.clientHeight + 15}px`;
  260.  
  261. // Width based on zoom level
  262. // name.style.width = "auto";
  263. // name.style.maxWidth = `${el.clientWidth - 48}px`;
  264. // TODO: If we can get the border-width at every zoom level then we can just do: width - 6px (padding) - borderWidth
  265. // name.style.maxWidth = "102px";
  266. // name.style.width = "102px";
  267. });
  268. }
  269. }
  270.  
  271. const bottomDiv = document.createElement("div");
  272. bottomDiv.className = "tsitu-tem-catches";
  273. bottomDiv.style.textAlign = "center";
  274.  
  275. // Render 2 crown count rows of icons
  276. const userStatWrapper = document.createElement("table");
  277. userStatWrapper.className = "mousehuntHud-userStat tsitu-tem-catches";
  278. userStatWrapper.style.borderCollapse = "separate";
  279. userStatWrapper.style.borderSpacing = "0 1em";
  280. userStatWrapper.style.marginRight = "3.8em";
  281.  
  282. function generateIcons(obj, row, isYes) {
  283. const descTd = document.createElement("td");
  284. descTd.style.paddingRight = "6px";
  285. descTd.style.fontSize = "13px";
  286. if (isYes) {
  287. descTd.innerText = "✔️";
  288. } else {
  289. descTd.innerText = "❌";
  290. }
  291. row.append(descTd);
  292.  
  293. Object.keys(obj).forEach(key => {
  294. const el = obj[key];
  295. const td = document.createElement("td");
  296. td.style.paddingRight = "6px";
  297.  
  298. const span = document.createElement("div");
  299. span.className = "notification active";
  300. span.style.position = "initial";
  301. span.style.background = el[1];
  302. span.style.width = "14px";
  303. span.style.padding = "3px";
  304. span.style.fontSize = "11px";
  305. span.style.fontWeight = "bold";
  306. span.innerText = el[0];
  307.  
  308. if (isYes) {
  309. span.title = `${el[0]}/${mouseCount} ${key} crowns earned in this TEM configuration`;
  310. } else {
  311. span.title = `${el[0]}/${mouseCount} ${key} crowns missing in this TEM configuration`;
  312. }
  313.  
  314. span.onclick = function (event) {
  315. event.stopPropagation(); // Prevent bubbling up
  316. alert(span.title);
  317. return false;
  318. };
  319.  
  320. td.appendChild(span);
  321. row.append(td);
  322. });
  323. }
  324.  
  325. const yesRow = document.createElement("tr");
  326. const noRow = document.createElement("tr");
  327. generateIcons(crownYes, yesRow, true);
  328. generateIcons(crownNo, noRow, false);
  329. userStatWrapper.appendChild(yesRow);
  330. userStatWrapper.appendChild(noRow);
  331. bottomDiv.appendChild(userStatWrapper);
  332. bottomDiv.appendChild(document.createElement("br"));
  333. bottomDiv.appendChild(document.createElement("br"));
  334.  
  335. // Render 'Refresh Data' button
  336. const refreshButton = document.createElement("button");
  337. refreshButton.id = "tem-catches-refresh-button";
  338. refreshButton.innerText = "Refresh Crown Data";
  339. refreshButton.addEventListener("click", function () {
  340. postReq("sn=Hitgrab&hg_is_ajax=1").then(res => {
  341. parseData(res);
  342. });
  343. });
  344. bottomDiv.appendChild(refreshButton);
  345.  
  346. const timeRaw = localStorage.getItem("mh-catch-stats-timestamp");
  347. if (timeRaw) {
  348. const timeSpan = document.createElement("span");
  349. timeSpan.style.fontSize = "14px";
  350. timeSpan.innerText = `(Last refreshed: ${new Date(
  351. parseInt(timeRaw)
  352. ).toLocaleString()})`;
  353. bottomDiv.appendChild(document.createElement("br"));
  354. bottomDiv.appendChild(document.createElement("br"));
  355. bottomDiv.appendChild(timeSpan);
  356. }
  357.  
  358. const container = document.getElementsByClassName(
  359. "campPage-trap-trapEffectiveness-content"
  360. )[0];
  361. if (container) container.appendChild(bottomDiv);
  362. }
  363.  
  364. /**
  365. * Parse badge endpoint response and write to localStorage
  366. * @param {string} res
  367. */
  368. function parseData(res) {
  369. let response = null;
  370. try {
  371. if (res) {
  372. response = JSON.parse(res.responseText);
  373. const catchData = {};
  374. const resData = response.hunting_stats;
  375. if (resData) {
  376. resData.forEach(mouse => {
  377. catchData[mouse.name] = mouse.num_catches;
  378. });
  379.  
  380. localStorage.setItem("mh-catch-stats", JSON.stringify(catchData));
  381. localStorage.setItem("mh-catch-stats-timestamp", Date.now());
  382.  
  383. // Close and reopen to update badges (prevents infinite render loop)
  384. app.pages.CampPage.closeBlueprintDrawer();
  385. app.pages.CampPage.toggleTrapEffectiveness(true);
  386. }
  387. }
  388. } catch (error) {
  389. console.log("Error while processing POST response");
  390. console.error(error.stack);
  391. }
  392. }
  393. })();