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.2 (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.textShadow = "none";
  95. refreshButton.style.display = "inline-block";
  96. refreshButton.addEventListener("click", function() {
  97. const mapName = document.querySelector(
  98. ".treasureMapPopup-header-title.mapName"
  99. ).textContent;
  100.  
  101. user.quests.QuestRelicHunter.maps.forEach(el => {
  102. if (el.name === mapName) {
  103. // Reset hash to bust cache
  104. el.hash = "";
  105. }
  106. });
  107.  
  108. // Close map dialog and re-open either with current map, default, or overview
  109. const mapIdEl = document.querySelector("[data-map-id].active");
  110. const mapId = mapIdEl ? mapIdEl.getAttribute("data-map-id") : -1;
  111. document.getElementById("jsDialogClose").click();
  112. mapId === -1
  113. ? hg.views.TreasureMapView.show()
  114. : hg.views.TreasureMapView.show(mapId);
  115. });
  116.  
  117. refreshSpan.appendChild(refreshButton);
  118. document
  119. .querySelector(
  120. ".treasureMapPopup-state.viewMap .treasureMapPopup-header-subtitle"
  121. )
  122. .insertAdjacentElement("afterend", refreshSpan);
  123.  
  124. // Utility handler that opens supply transfer page and selects SB+
  125. function transferSB(snuid) {
  126. const newWindow = window.open(
  127. `https://www.mousehuntgame.com/supplytransfer.php?fid=${snuid}`
  128. );
  129. newWindow.addEventListener("load", function() {
  130. if (newWindow.supplyTransfer1) {
  131. newWindow.supplyTransfer1.setSelectedItemType("super_brie_cheese");
  132. newWindow.supplyTransfer1.renderTabMenu();
  133. newWindow.supplyTransfer1.render();
  134. }
  135. });
  136. return false;
  137. }
  138.  
  139. // Corkboard image click handling
  140. document.querySelectorAll("[data-message-id]").forEach(msg => {
  141. const snuid = msg
  142. .querySelector(".messageBoardView-message-name")
  143. .href.split("snuid=")[1];
  144. const img = msg.querySelector(".messageBoardView-message-image");
  145. img.href = "#";
  146. img.onclick = function() {
  147. transferSB(snuid);
  148. };
  149. });
  150.  
  151. // Hunter container image click handling
  152. document
  153. .querySelectorAll(".treasureMapPopup-hunter:not(.empty)")
  154. .forEach(el => {
  155. const img = el.querySelector(".treasureMapPopup-hunter-image");
  156. const snuid = el.getAttribute("data-snuid");
  157. img.style.cursor = "pointer";
  158. img.onclick = function() {
  159. transferSB(snuid);
  160. };
  161. });
  162.  
  163. // Features that require cache checking
  164. const cacheRaw = localStorage.getItem("tsitu-mapping-cache");
  165. if (cacheRaw) {
  166. const cache = JSON.parse(cacheRaw);
  167. const mapName = document.querySelector(
  168. ".treasureMapPopup-header-title.mapName"
  169. ).textContent;
  170.  
  171. if (cache[mapName] !== undefined) {
  172. const mapIdEl = document.querySelector("[data-map-id].active");
  173. if (mapIdEl) {
  174. // Abstract equality comparison because map ID can be number or string
  175. const mapId = mapIdEl.getAttribute("data-map-id");
  176. if (mapId == cache[mapName].map_id) {
  177. // "Last refreshed" timestamp
  178. const refreshSpan = document.querySelector(".tsitu-refresh-span");
  179. if (refreshSpan && cache[mapName].timestamp) {
  180. const timeSpan = document.createElement("span");
  181. timeSpan.innerText = `(This map was last refreshed on: ${new Date(
  182. parseInt(cache[mapName].timestamp)
  183. ).toLocaleString()})`;
  184. refreshSpan.appendChild(timeSpan);
  185. }
  186.  
  187. // Invite via Hunter ID (only for map captains)
  188. if (cache[mapName].is_owner) {
  189. const inputLabel = document.createElement("label");
  190. inputLabel.innerText = "Hunter ID: ";
  191. inputLabel.htmlFor = "tsitu-mapping-id-input";
  192. inputLabel.setAttribute("class", "tsitu-mapping");
  193.  
  194. const inputField = document.createElement("input");
  195. inputField.setAttribute("type", "text");
  196. inputField.setAttribute("class", "tsitu-mapping");
  197. inputField.setAttribute("name", "tsitu-mapping-id-input");
  198. inputField.setAttribute("data-lpignore", "true"); // Get rid of LastPass Autofill
  199. inputField.setAttribute("size", 10);
  200. inputField.setAttribute("required", true);
  201. inputField.addEventListener("keyup", function(e) {
  202. if (e.keyCode === 13) {
  203. inviteButton.click(); // 'Enter' pressed
  204. }
  205. });
  206.  
  207. const inviteButton = document.createElement("button");
  208. inviteButton.innerText = "Invite";
  209. inviteButton.setAttribute("class", "tsitu-mapping");
  210. inviteButton.addEventListener("click", function() {
  211. const rawText = inputField.value;
  212. if (rawText.length > 0) {
  213. const hunterId = parseInt(rawText);
  214. if (typeof hunterId === "number" && !isNaN(hunterId)) {
  215. if (hunterId > 0 && hunterId < 9999999) {
  216. // console.log(hunterId);
  217. postReq(
  218. "https://www.mousehuntgame.com/managers/ajax/pages/friends.php",
  219. `sn=Hitgrab&hg_is_ajax=1&action=community_search_by_id&user_id=${hunterId}&uh=${
  220. user.unique_hash
  221. }`
  222. ).then(res => {
  223. let response = null;
  224. try {
  225. if (res) {
  226. response = JSON.parse(res.responseText);
  227. // console.log(response);
  228. const data = response.friend;
  229. if (data.has_invitable_map) {
  230. if (
  231. confirm(
  232. `Are you sure you'd like to invite this hunter?\n\nName: ${
  233. data.name
  234. }\nTitle: ${data.title_name} (${
  235. data.title_percent
  236. }%)\nLocation: ${
  237. data.environment_name
  238. }\nLast Active: ${
  239. data.last_active_formatted
  240. } ago`
  241. )
  242. ) {
  243. postReq(
  244. "https://www.mousehuntgame.com/managers/ajax/users/relichunter.php",
  245. `sn=Hitgrab&hg_is_ajax=1&action=send_invites&map_id=${mapId}&snuids%5B%5D=${
  246. data.snuid
  247. }&uh=${user.unique_hash}`
  248. ).then(res2 => {
  249. let inviteRes = null;
  250. try {
  251. if (res2) {
  252. inviteRes = JSON.parse(res2.responseText);
  253. if (inviteRes.success === 1) {
  254. refreshButton.click();
  255. }
  256. }
  257. } catch (error2) {
  258. alert("Error while inviting hunter to map");
  259. console.error(error2.stack);
  260. }
  261. });
  262. }
  263. } else {
  264. alert(
  265. `${
  266. data.name
  267. } cannot to be invited to a map at this time`
  268. );
  269. }
  270. }
  271. } catch (error) {
  272. alert("Error while retrieving hunter information");
  273. console.error(error.stack);
  274. }
  275. });
  276. }
  277. }
  278. }
  279. });
  280.  
  281. // Invited hunters aka pending invites
  282. const invitedArr = cache[mapName].invited_hunters;
  283. let invitedSpan = -1;
  284. if (invitedArr.length > 0) {
  285. invitedSpan = document.createElement("span");
  286. invitedSpan.className = "tsitu-mapping";
  287. invitedSpan.style.marginLeft = "5px";
  288. invitedSpan.innerText = "Pending Invites:";
  289. let count = 1;
  290. invitedArr.forEach(snuid => {
  291. const link = document.createElement("a");
  292. link.innerText = `[${count}]`;
  293. link.href = `https://www.mousehuntgame.com/profile.php?snuid=${snuid}`;
  294. link.target = "_blank";
  295. count += 1;
  296. invitedSpan.appendChild(document.createTextNode("\u00A0"));
  297. invitedSpan.appendChild(link);
  298. });
  299. }
  300.  
  301. const span = document.createElement("span");
  302. span.style.display = "inline-block";
  303. span.style.marginBottom = "10px";
  304. span.appendChild(inputLabel);
  305. span.appendChild(inputField);
  306. span.appendChild(inviteButton);
  307. if (invitedSpan !== -1) span.appendChild(invitedSpan);
  308.  
  309. document
  310. .querySelector(".treasureMapPopup-hunterContainer")
  311. .insertAdjacentElement("afterend", span);
  312. }
  313. }
  314. }
  315.  
  316. // "x caught these mice" image click handling
  317. const groups = cache[mapName].groups;
  318. const format = {};
  319.  
  320. groups.forEach(el => {
  321. if (el.profile_pic !== null) {
  322. format[el.profile_pic] = [el.name, el.snuid];
  323. }
  324. });
  325.  
  326. document
  327. .querySelectorAll(".treasureMapPopup-goals-group-header")
  328. .forEach(group => {
  329. const text = group.textContent.split(":(")[0] + ":";
  330. if (text !== "Uncaught mice in other locations:") {
  331. const img = group.querySelector(
  332. ".treasureMapPopup-goals-group-header-image"
  333. );
  334. if (img) {
  335. const pic = img.style.backgroundImage
  336. .split('url("')[1]
  337. .split('")')[0];
  338. if (format[pic] !== undefined) {
  339. if (format[pic][0] === text) {
  340. img.style.cursor = "pointer";
  341. img.onclick = function() {
  342. const snuid = format[pic][1];
  343. transferSB(snuid);
  344. };
  345. }
  346. }
  347. }
  348. }
  349. });
  350. }
  351. }
  352. }
  353.  
  354. // POST to specified endpoint URL with desired form data
  355. function postReq(url, form) {
  356. return new Promise((resolve, reject) => {
  357. const xhr = new XMLHttpRequest();
  358. xhr.open("POST", url, true);
  359. xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
  360. xhr.onreadystatechange = function() {
  361. if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
  362. resolve(this);
  363. }
  364. };
  365. xhr.onerror = function() {
  366. reject(this);
  367. };
  368. xhr.send(form);
  369. });
  370. }
  371.  
  372. // MutationObserver logic for map UI
  373. // Observers are attached to a *specific* element (will DC if removed from DOM)
  374. const observerTarget = document.getElementById("overlayPopup");
  375. if (observerTarget) {
  376. MutationObserver =
  377. window.MutationObserver ||
  378. window.WebKitMutationObserver ||
  379. window.MozMutationObserver;
  380.  
  381. const observer = new MutationObserver(function() {
  382. // Callback
  383.  
  384. // Render if treasure map popup is available
  385. const mapTab = observerTarget.querySelector("[data-tab=map_mice]");
  386. const groupLen = document.querySelectorAll(
  387. ".treasureMapPopup-goals-groups"
  388. ).length;
  389.  
  390. // Prevent conflict with 'Bulk Map Invites'
  391. const inviteHeader = document.querySelector(
  392. ".treasureMapPopup-inviteFriend-header"
  393. );
  394.  
  395. if (
  396. mapTab &&
  397. mapTab.className.indexOf("active") >= 0 &&
  398. groupLen > 0 &&
  399. !inviteHeader
  400. ) {
  401. // Disconnect and reconnect later to prevent infinite mutation loop
  402. observer.disconnect();
  403.  
  404. render();
  405.  
  406. observer.observe(observerTarget, {
  407. childList: true,
  408. subtree: true
  409. });
  410. }
  411. });
  412.  
  413. observer.observe(observerTarget, {
  414. childList: true,
  415. subtree: true
  416. });
  417. }
  418. })();