MouseHunt - Mapping Helper

Invite players and send SB+ directly from the map interface!

当前为 2019-06-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name MouseHunt - Mapping Helper
  3. // @author Tran Situ (tsitu)
  4. // @namespace https://greasyfork.org/en/users/232363-tsitu
  5. // @version 0.1 (beta)
  6. // @description Invite players and send SB+ directly from the map interface!
  7. // @match http://www.mousehuntgame.com/*
  8. // @match https://www.mousehuntgame.com/*
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. // Map endpoint listener - caches encountered data (maps come in one at a time)
  13. const originalOpen = XMLHttpRequest.prototype.open;
  14. XMLHttpRequest.prototype.open = function() {
  15. this.addEventListener("load", function() {
  16. if (
  17. this.responseURL ===
  18. "https://www.mousehuntgame.com/managers/ajax/users/relichunter.php"
  19. ) {
  20. // console.group("Mapping Helper");
  21. try {
  22. const map = JSON.parse(this.responseText).treasure_map;
  23. if (map) {
  24. const obj = {};
  25. const condensed = {};
  26.  
  27. const condensedGroups = [];
  28. map.groups.forEach(el => {
  29. // TODO: Compress goals array if needed for individual mouse/item stuff
  30. const innerObj = {};
  31. innerObj.name = el.name;
  32. innerObj.profile_pic = el.profile_pic;
  33. innerObj.snuid = el.snuid;
  34. condensedGroups.push(innerObj);
  35. });
  36. condensed.groups = condensedGroups;
  37.  
  38. condensed.hunters = map.hunters;
  39. condensed.invited_hunters = map.invited_hunters;
  40. condensed.is_complete = map.is_complete;
  41. condensed.is_owner = map.is_owner;
  42. condensed.is_scavenger_hunt = map.is_scavenger_hunt;
  43. condensed.is_wanted_poster = map.is_wanted_poster;
  44. condensed.map_class = map.map_class;
  45. condensed.map_id = map.map_id;
  46. condensed.timestamp = Date.now();
  47. obj[map.name] = condensed;
  48. // console.log(obj);
  49.  
  50. const mapCacheRaw = localStorage.getItem("tsitu-mapping-cache");
  51. if (mapCacheRaw) {
  52. const mapCache = JSON.parse(mapCacheRaw);
  53. mapCache[map.name] = condensed;
  54. localStorage.setItem(
  55. "tsitu-mapping-cache",
  56. JSON.stringify(mapCache)
  57. );
  58. } else {
  59. localStorage.setItem("tsitu-mapping-cache", JSON.stringify(obj));
  60. }
  61.  
  62. render();
  63. }
  64. } catch (error) {
  65. console.log("Server response doesn't contain a valid treasure map");
  66. console.error(error.stack);
  67. }
  68. // console.groupEnd("Mapping Helper");
  69. }
  70. });
  71. originalOpen.apply(this, arguments);
  72. };
  73.  
  74. // Renders custom UI elements onto the DOM
  75. function render() {
  76. // Clear out existing custom elements
  77. // Uses static collection instead of live one from getElementsByClassName
  78. document.querySelectorAll(".tsitu-mapping").forEach(el => el.remove());
  79.  
  80. /**
  81. * Refresh button
  82. * Iterate thru QRH.maps array for element matching current map and set its hash to empty string
  83. * This forces a hard refresh via hasCachedMap, which is called in show/showMap
  84. */
  85. const refreshSpan = document.createElement("span");
  86. refreshSpan.className = "tsitu-mapping tsitu-refresh-span";
  87. const refreshButton = document.createElement("button");
  88. refreshButton.innerText = "Refresh";
  89. refreshButton.className = "treasureMapPopup-action-button tsitu-mapping";
  90. refreshButton.style.cursor = "pointer";
  91. refreshButton.style.fontSize = "9px";
  92. refreshButton.style.padding = "2px";
  93. refreshButton.style.margin = "3px 5px 0px 0px";
  94. refreshButton.style.display = "inline-block";
  95. refreshButton.addEventListener("click", function() {
  96. const mapName = document.querySelector(
  97. ".treasureMapPopup-header-title.mapName"
  98. ).textContent;
  99.  
  100. user.quests.QuestRelicHunter.maps.forEach(el => {
  101. if (el.name === mapName) {
  102. // Reset hash to bust cache
  103. el.hash = "";
  104. }
  105. });
  106.  
  107. // Close map dialog and re-open either with current map, default, or overview
  108. const mapIdEl = document.querySelector("[data-map-id].active");
  109. const mapId = mapIdEl ? mapIdEl.getAttribute("data-map-id") : -1;
  110. document.getElementById("jsDialogClose").click();
  111. mapId === -1
  112. ? hg.views.TreasureMapView.show()
  113. : hg.views.TreasureMapView.show(mapId);
  114. });
  115.  
  116. refreshSpan.appendChild(refreshButton);
  117. document
  118. .querySelector(
  119. ".treasureMapPopup-state.viewMap .treasureMapPopup-header-subtitle"
  120. )
  121. .insertAdjacentElement("afterend", refreshSpan);
  122.  
  123. // Utility handler that opens supply transfer page and selects SB+
  124. function transferSB(snuid) {
  125. const newWindow = window.open(
  126. `https://www.mousehuntgame.com/supplytransfer.php?fid=${snuid}`
  127. );
  128. newWindow.addEventListener("load", function() {
  129. if (newWindow.supplyTransfer1) {
  130. newWindow.supplyTransfer1.setSelectedItemType("super_brie_cheese");
  131. newWindow.supplyTransfer1.renderTabMenu();
  132. newWindow.supplyTransfer1.render();
  133. }
  134. });
  135. return false;
  136. }
  137.  
  138. // Corkboard image click handling
  139. document.querySelectorAll("[data-message-id]").forEach(msg => {
  140. const snuid = msg
  141. .querySelector(".messageBoardView-message-name")
  142. .href.split("snuid=")[1];
  143. const img = msg.querySelector(".messageBoardView-message-image");
  144. img.href = "#";
  145. img.onclick = function() {
  146. transferSB(snuid);
  147. };
  148. });
  149.  
  150. // Hunter container image click handling
  151. document
  152. .querySelectorAll(".treasureMapPopup-hunter:not(.empty)")
  153. .forEach(el => {
  154. const img = el.querySelector(".treasureMapPopup-hunter-image");
  155. const snuid = el.getAttribute("data-snuid");
  156. img.style.cursor = "pointer";
  157. img.onclick = function() {
  158. transferSB(snuid);
  159. };
  160. });
  161.  
  162. // Features that require cache checking
  163. const cacheRaw = localStorage.getItem("tsitu-mapping-cache");
  164. if (cacheRaw) {
  165. const cache = JSON.parse(cacheRaw);
  166. const mapName = document.querySelector(
  167. ".treasureMapPopup-header-title.mapName"
  168. ).textContent;
  169.  
  170. if (cache[mapName] !== undefined) {
  171. const mapIdEl = document.querySelector("[data-map-id].active");
  172. if (mapIdEl) {
  173. // Abstract equality comparison because map ID can be number or string
  174. const mapId = mapIdEl.getAttribute("data-map-id");
  175. if (mapId == cache[mapName].map_id) {
  176. // "Last refreshed" timestamp
  177. const refreshSpan = document.querySelector(".tsitu-refresh-span");
  178. if (refreshSpan && cache[mapName].timestamp) {
  179. const timeSpan = document.createElement("span");
  180. timeSpan.innerText = `(This map was last refreshed on: ${new Date(
  181. parseInt(cache[mapName].timestamp)
  182. ).toLocaleString()})`;
  183. refreshSpan.appendChild(timeSpan);
  184. }
  185.  
  186. // Invite via Hunter ID (only for map captains)
  187. if (cache[mapName].is_owner) {
  188. const inputLabel = document.createElement("label");
  189. inputLabel.innerText = "Hunter ID: ";
  190. inputLabel.htmlFor = "tsitu-mapping-id-input";
  191. inputLabel.setAttribute("class", "tsitu-mapping");
  192.  
  193. const inputField = document.createElement("input");
  194. inputField.setAttribute("type", "text");
  195. inputField.setAttribute("class", "tsitu-mapping");
  196. inputField.setAttribute("name", "tsitu-mapping-id-input");
  197. inputField.setAttribute("data-lpignore", "true"); // Get rid of LastPass Autofill
  198. inputField.setAttribute("size", 10);
  199. inputField.setAttribute("required", true);
  200. inputField.addEventListener("keyup", function(e) {
  201. if (e.keyCode === 13) {
  202. inviteButton.click(); // 'Enter' pressed
  203. }
  204. });
  205.  
  206. const inviteButton = document.createElement("button");
  207. inviteButton.innerText = "Invite";
  208. inviteButton.setAttribute("class", "tsitu-mapping");
  209. inviteButton.addEventListener("click", function() {
  210. const rawText = inputField.value;
  211. if (rawText.length > 0) {
  212. const hunterId = parseInt(rawText);
  213. if (typeof hunterId === "number" && !isNaN(hunterId)) {
  214. if (hunterId > 0 && hunterId < 9999999) {
  215. // console.log(hunterId);
  216. postReq(
  217. "https://www.mousehuntgame.com/managers/ajax/pages/friends.php",
  218. `sn=Hitgrab&hg_is_ajax=1&action=community_search_by_id&user_id=${hunterId}&uh=${
  219. user.unique_hash
  220. }`
  221. ).then(res => {
  222. let response = null;
  223. try {
  224. if (res) {
  225. response = JSON.parse(res.responseText);
  226. // console.log(response);
  227. const data = response.friend;
  228. if (data.has_invitable_map) {
  229. if (
  230. confirm(
  231. `Are you sure you'd like to invite this hunter?\n\nName: ${
  232. data.name
  233. }\nTitle: ${data.title_name} (${
  234. data.title_percent
  235. }%)\nLocation: ${
  236. data.environment_name
  237. }\nLast Active: ${
  238. data.last_active_formatted
  239. } ago`
  240. )
  241. ) {
  242. postReq(
  243. "https://www.mousehuntgame.com/managers/ajax/users/relichunter.php",
  244. `sn=Hitgrab&hg_is_ajax=1&action=send_invites&map_id=${mapId}&snuids%5B%5D=${
  245. data.snuid
  246. }&uh=${user.unique_hash}`
  247. ).then(res2 => {
  248. let inviteRes = null;
  249. try {
  250. if (res2) {
  251. inviteRes = JSON.parse(res2.responseText);
  252. if (inviteRes.success === 1) {
  253. refreshButton.click();
  254. }
  255. }
  256. } catch (error2) {
  257. alert("Error while inviting hunter to map");
  258. console.error(error2.stack);
  259. }
  260. });
  261. }
  262. } else {
  263. alert(
  264. `${
  265. data.name
  266. } cannot to be invited to a map at this time`
  267. );
  268. }
  269. }
  270. } catch (error) {
  271. alert("Error while retrieving hunter information");
  272. console.error(error.stack);
  273. }
  274. });
  275. }
  276. }
  277. }
  278. });
  279.  
  280. // Invited hunters aka pending invites
  281. const invitedArr = cache[mapName].invited_hunters;
  282. let invitedSpan = -1;
  283. if (invitedArr.length > 0) {
  284. invitedSpan = document.createElement("span");
  285. invitedSpan.className = "tsitu-mapping";
  286. invitedSpan.style.marginLeft = "5px";
  287. invitedSpan.innerText = "Pending Invites:";
  288. let count = 1;
  289. invitedArr.forEach(snuid => {
  290. const link = document.createElement("a");
  291. link.innerText = `[${count}]`;
  292. link.href = `https://www.mousehuntgame.com/profile.php?snuid=${snuid}`;
  293. link.target = "_blank";
  294. count += 1;
  295. invitedSpan.appendChild(document.createTextNode("\u00A0"));
  296. invitedSpan.appendChild(link);
  297. });
  298. }
  299.  
  300. const span = document.createElement("span");
  301. span.style.display = "inline-block";
  302. span.style.marginBottom = "10px";
  303. span.appendChild(inputLabel);
  304. span.appendChild(inputField);
  305. span.appendChild(inviteButton);
  306. if (invitedSpan !== -1) span.appendChild(invitedSpan);
  307.  
  308. document
  309. .querySelector(".treasureMapPopup-hunterContainer")
  310. .insertAdjacentElement("afterend", span);
  311. }
  312. }
  313. }
  314.  
  315. // "x caught these mice" image click handling
  316. const groups = cache[mapName].groups;
  317. const format = {};
  318.  
  319. groups.forEach(el => {
  320. if (el.profile_pic !== null) {
  321. format[el.profile_pic] = [el.name, el.snuid];
  322. }
  323. });
  324.  
  325. document
  326. .querySelectorAll(".treasureMapPopup-goals-group-header")
  327. .forEach(group => {
  328. const text = group.textContent.split(":(")[0] + ":";
  329. if (text !== "Uncaught mice in other locations:") {
  330. const img = group.querySelector(
  331. ".treasureMapPopup-goals-group-header-image"
  332. );
  333. if (img) {
  334. const pic = img.style.backgroundImage
  335. .split('url("')[1]
  336. .split('")')[0];
  337. if (format[pic] !== undefined) {
  338. if (format[pic][0] === text) {
  339. img.style.cursor = "pointer";
  340. img.onclick = function() {
  341. const snuid = format[pic][1];
  342. transferSB(snuid);
  343. };
  344. }
  345. }
  346. }
  347. }
  348. });
  349. }
  350. }
  351. }
  352.  
  353. // POST to specified endpoint URL with desired form data
  354. function postReq(url, form) {
  355. return new Promise((resolve, reject) => {
  356. const xhr = new XMLHttpRequest();
  357. xhr.open("POST", url, true);
  358. xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
  359. xhr.onreadystatechange = function() {
  360. if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
  361. resolve(this);
  362. }
  363. };
  364. xhr.onerror = function() {
  365. reject(this);
  366. };
  367. xhr.send(form);
  368. });
  369. }
  370.  
  371. // MutationObserver logic for map UI
  372. // Observers are attached to a *specific* element (will DC if removed from DOM)
  373. const observerTarget = document.getElementById("overlayPopup");
  374. if (observerTarget) {
  375. MutationObserver =
  376. window.MutationObserver ||
  377. window.WebKitMutationObserver ||
  378. window.MozMutationObserver;
  379.  
  380. const observer = new MutationObserver(function() {
  381. // Callback
  382.  
  383. // Render if treasure map popup is available
  384. const mapTab = observerTarget.querySelector("[data-tab=map_mice]");
  385. const groupLen = document.querySelectorAll(
  386. ".treasureMapPopup-goals-groups"
  387. ).length;
  388.  
  389. // Prevent conflict with 'Bulk Map Invites'
  390. const inviteHeader = document.querySelector(
  391. ".treasureMapPopup-inviteFriend-header"
  392. );
  393.  
  394. if (
  395. mapTab &&
  396. mapTab.className.indexOf("active") >= 0 &&
  397. groupLen > 0 &&
  398. !inviteHeader
  399. ) {
  400. // Disconnect and reconnect later to prevent infinite mutation loop
  401. observer.disconnect();
  402.  
  403. render();
  404.  
  405. observer.observe(observerTarget, {
  406. childList: true,
  407. subtree: true
  408. });
  409. }
  410. });
  411.  
  412. observer.observe(observerTarget, {
  413. childList: true,
  414. subtree: true
  415. });
  416. }
  417. })();