MouseHunt - Mapping Helper

Invite players and send SB+ directly from the map interface

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

  1. // ==UserScript==
  2. // @name MouseHunt - Mapping Helper
  3. // @author Tran Situ (tsitu)
  4. // @namespace https://greasyfork.org/en/users/232363-tsitu
  5. // @version 1.0
  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. // RH endpoint listener - caches maps (which 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. try {
  21. const map = JSON.parse(this.responseText).treasure_map;
  22. if (map) {
  23. const obj = {};
  24. const condensed = {};
  25.  
  26. const condensedGroups = [];
  27. map.groups.forEach(el => {
  28. // TODO: Compress goals array for individual mice/items in the future
  29. const innerObj = {};
  30. innerObj.name = el.name;
  31. innerObj.profile_pic = el.profile_pic;
  32. innerObj.snuid = el.snuid;
  33. condensedGroups.push(innerObj);
  34. });
  35. condensed.groups = condensedGroups;
  36.  
  37. condensed.hunters = map.hunters;
  38. condensed.invited_hunters = map.invited_hunters;
  39. condensed.is_complete = map.is_complete;
  40. condensed.is_owner = map.is_owner;
  41. condensed.is_scavenger_hunt = map.is_scavenger_hunt;
  42. condensed.is_wanted_poster = map.is_wanted_poster;
  43. condensed.map_class = map.map_class;
  44. condensed.map_id = map.map_id;
  45. condensed.timestamp = Date.now();
  46. obj[map.name] = condensed;
  47. // console.log(obj);
  48.  
  49. const mapCacheRaw = localStorage.getItem("tsitu-mapping-cache");
  50. if (mapCacheRaw) {
  51. const mapCache = JSON.parse(mapCacheRaw);
  52. mapCache[map.name] = condensed;
  53. localStorage.setItem(
  54. "tsitu-mapping-cache",
  55. JSON.stringify(mapCache)
  56. );
  57. } else {
  58. localStorage.setItem("tsitu-mapping-cache", JSON.stringify(obj));
  59. }
  60.  
  61. render();
  62. }
  63. } catch (error) {
  64. console.log("Server response doesn't contain a valid treasure map");
  65. console.error(error.stack);
  66. }
  67. }
  68. });
  69. originalOpen.apply(this, arguments);
  70. };
  71.  
  72. // Renders custom UI elements onto the DOM
  73. function render() {
  74. // Clear out existing custom elements
  75. // Uses static collection instead of live one from getElementsByClassName
  76. document.querySelectorAll(".tsitu-mapping").forEach(el => el.remove());
  77.  
  78. /**
  79. * Refresh button
  80. * Iterate thru QRH.maps array for element matching current map and set its hash to empty string
  81. * This forces a hard refresh via hasCachedMap, which is called in show/showMap
  82. */
  83. const refreshSpan = document.createElement("span");
  84. refreshSpan.className = "tsitu-mapping tsitu-refresh-span";
  85. const refreshButton = document.createElement("button");
  86. refreshButton.innerText = "Refresh";
  87. refreshButton.className = "treasureMapPopup-action-button tsitu-mapping";
  88. refreshButton.style.cursor = "pointer";
  89. refreshButton.style.fontSize = "9px";
  90. refreshButton.style.padding = "2px";
  91. refreshButton.style.margin = "3px 5px 0px 0px";
  92. refreshButton.style.textShadow = "none";
  93. refreshButton.style.display = "inline-block";
  94. refreshButton.addEventListener("click", function() {
  95. const mapName = document.querySelector(
  96. ".treasureMapPopup-header-title.mapName"
  97. ).textContent;
  98.  
  99. user.quests.QuestRelicHunter.maps.forEach(el => {
  100. if (el.name === mapName) {
  101. // Reset hash to bust cache
  102. el.hash = "";
  103. }
  104. });
  105.  
  106. // Close map dialog and re-open either with current map, default, or overview
  107. const mapIdEl = document.querySelector("[data-map-id].active");
  108. const mapId = mapIdEl ? mapIdEl.getAttribute("data-map-id") : -1;
  109. document.getElementById("jsDialogClose").click();
  110. mapId === -1
  111. ? hg.views.TreasureMapView.show()
  112. : hg.views.TreasureMapView.show(mapId);
  113. });
  114.  
  115. refreshSpan.appendChild(refreshButton);
  116. document
  117. .querySelector(
  118. ".treasureMapPopup-state.viewMap .treasureMapPopup-header-subtitle"
  119. )
  120. .insertAdjacentElement("afterend", refreshSpan);
  121.  
  122. // Utility handler that opens supply transfer page and selects SB+
  123. function transferSB(snuid) {
  124. const newWindow = window.open(
  125. `https://www.mousehuntgame.com/supplytransfer.php?fid=${snuid}`
  126. );
  127. newWindow.addEventListener("load", function() {
  128. if (newWindow.supplyTransfer1) {
  129. newWindow.supplyTransfer1.setSelectedItemType("super_brie_cheese");
  130. newWindow.supplyTransfer1.renderTabMenu();
  131. newWindow.supplyTransfer1.render();
  132. }
  133. });
  134. return false;
  135. }
  136.  
  137. // Corkboard image click handling
  138. document.querySelectorAll("[data-message-id]").forEach(msg => {
  139. const snuid = msg
  140. .querySelector(".messageBoardView-message-name")
  141. .href.split("snuid=")[1];
  142. const img = msg.querySelector(".messageBoardView-message-image");
  143. img.href = "#";
  144. img.onclick = function() {
  145. transferSB(snuid);
  146. };
  147. });
  148.  
  149. // Hunter container image click handling
  150. document
  151. .querySelectorAll(".treasureMapPopup-hunter:not(.empty)")
  152. .forEach(el => {
  153. const img = el.querySelector(".treasureMapPopup-hunter-image");
  154. const snuid = el.getAttribute("data-snuid");
  155. img.style.cursor = "pointer";
  156. img.onclick = function() {
  157. transferSB(snuid);
  158. };
  159. });
  160.  
  161. // Features that require cache checking
  162. const cacheRaw = localStorage.getItem("tsitu-mapping-cache");
  163. if (cacheRaw) {
  164. const cache = JSON.parse(cacheRaw);
  165. const mapName = document.querySelector(
  166. ".treasureMapPopup-header-title.mapName"
  167. ).textContent;
  168.  
  169. if (cache[mapName] !== undefined) {
  170. // Must specify <a> because favorite button <div> also matches the selector
  171. const mapIdEl = document.querySelector("a[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", "number");
  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("min", 1);
  199. inputField.setAttribute("max", 9999999);
  200. inputField.setAttribute("placeholder", "e.g. 1234567");
  201. inputField.setAttribute("required", true);
  202. inputField.addEventListener("keyup", function(e) {
  203. if (e.keyCode === 13) {
  204. inviteButton.click(); // 'Enter' pressed
  205. }
  206. });
  207.  
  208. const inviteButton = document.createElement("button");
  209. inviteButton.innerText = "Invite";
  210. inviteButton.setAttribute("class", "tsitu-mapping");
  211. inviteButton.addEventListener("click", function() {
  212. const rawText = inputField.value;
  213. if (rawText.length > 0) {
  214. const hunterId = parseInt(rawText);
  215. if (typeof hunterId === "number" && !isNaN(hunterId)) {
  216. if (hunterId > 0 && hunterId < 9999999) {
  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. } else {
  256. alert(
  257. "Map invite unsuccessful - may be because map is full"
  258. );
  259. }
  260. }
  261. } catch (error2) {
  262. alert("Error while inviting hunter to map");
  263. console.error(error2.stack);
  264. }
  265. });
  266. }
  267. } else {
  268. if (data.name) {
  269. alert(
  270. `${
  271. data.name
  272. } cannot to be invited to a map at this time`
  273. );
  274. } else {
  275. alert("Invalid hunter information");
  276. }
  277. }
  278. }
  279. } catch (error) {
  280. alert("Error while requesting hunter information");
  281. console.error(error.stack);
  282. }
  283. });
  284. }
  285. }
  286. }
  287. });
  288.  
  289. // Invited hunters aka pending invites
  290. const invitedArr = cache[mapName].invited_hunters;
  291. const invitedSpan = document.createElement("span");
  292. invitedSpan.className = "tsitu-mapping";
  293. invitedSpan.style.marginLeft = "5px";
  294. invitedSpan.innerText =
  295. invitedArr.length > 0
  296. ? "Pending Invites:"
  297. : "Pending Invites: None";
  298.  
  299. if (invitedArr.length > 0) {
  300. let count = 1;
  301. invitedArr.forEach(snuid => {
  302. const link = document.createElement("a");
  303. link.innerText = `[${count}]`;
  304. link.href = `https://www.mousehuntgame.com/profile.php?snuid=${snuid}`;
  305. link.target = "_blank";
  306. invitedSpan.appendChild(document.createTextNode("\u00A0"));
  307. invitedSpan.appendChild(link);
  308.  
  309. // Prevent text from running past width of dialog (fails when >999)
  310. if (count < 100) {
  311. if (count === 20) {
  312. invitedSpan.appendChild(document.createElement("br"));
  313. } else if ((count - 20) % 30 === 0) {
  314. invitedSpan.appendChild(document.createElement("br"));
  315. }
  316. } else {
  317. if (count === 108) {
  318. invitedSpan.appendChild(document.createElement("br"));
  319. } else if ((count - 108) % 24 === 0) {
  320. invitedSpan.appendChild(document.createElement("br"));
  321. }
  322. }
  323. count += 1;
  324. });
  325.  
  326. // -- Text debugging --
  327. // for (let i = 0; i < 1337; i++) {
  328. // const link = document.createElement("a");
  329. // link.innerText = `[${i + 1}]`;
  330. // link.href = `#`;
  331. // invitedSpan.appendChild(document.createTextNode("\u00A0"));
  332. // invitedSpan.appendChild(link);
  333. // // Prevent text from running past width of dialog (formatting breaks past 1k but meh)
  334. // if (i + 1 < 100) {
  335. // if (i + 1 === 20) {
  336. // invitedSpan.appendChild(document.createElement("br"));
  337. // } else if ((i + 1 - 20) % 30 === 0) {
  338. // invitedSpan.appendChild(document.createElement("br"));
  339. // }
  340. // } else {
  341. // if (i + 1 === 108) {
  342. // invitedSpan.appendChild(document.createElement("br"));
  343. // } else if ((i + 1 - 108) % 24 === 0) {
  344. // invitedSpan.appendChild(document.createElement("br"));
  345. // }
  346. // }
  347. // }
  348. }
  349.  
  350. const span = document.createElement("span");
  351. span.style.display = "inline-block";
  352. span.style.marginBottom = "10px";
  353. span.appendChild(inputLabel);
  354. span.appendChild(inputField);
  355. span.appendChild(inviteButton);
  356. span.appendChild(invitedSpan);
  357.  
  358. document
  359. .querySelector(".treasureMapPopup-hunterContainer")
  360. .insertAdjacentElement("afterend", span);
  361. }
  362. }
  363. }
  364.  
  365. // "x caught these mice" image click handling
  366. const groups = cache[mapName].groups;
  367. const format = {};
  368.  
  369. groups.forEach(el => {
  370. if (el.profile_pic !== null) {
  371. format[el.profile_pic] = [el.name, el.snuid];
  372. }
  373. });
  374.  
  375. document
  376. .querySelectorAll(".treasureMapPopup-goals-group-header")
  377. .forEach(group => {
  378. const text = group.textContent.split(":(")[0] + ":";
  379. if (text !== "Uncaught mice in other locations:") {
  380. const img = group.querySelector(
  381. ".treasureMapPopup-goals-group-header-image"
  382. );
  383. if (img) {
  384. const pic = img.style.backgroundImage
  385. .split('url("')[1]
  386. .split('")')[0];
  387. if (format[pic] !== undefined) {
  388. if (format[pic][0] === text) {
  389. img.style.cursor = "pointer";
  390. img.onclick = function() {
  391. const snuid = format[pic][1];
  392. transferSB(snuid);
  393. };
  394. }
  395. }
  396. }
  397. }
  398. });
  399. }
  400. }
  401. }
  402.  
  403. // POST to specified endpoint URL with desired form data
  404. function postReq(url, form) {
  405. return new Promise((resolve, reject) => {
  406. const xhr = new XMLHttpRequest();
  407. xhr.open("POST", url, true);
  408. xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
  409. xhr.onreadystatechange = function() {
  410. if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
  411. resolve(this);
  412. }
  413. };
  414. xhr.onerror = function() {
  415. reject(this);
  416. };
  417. xhr.send(form);
  418. });
  419. }
  420.  
  421. // MutationObserver logic for map UI
  422. // Observers are attached to a *specific* element (will DC if removed from DOM)
  423. const observerTarget = document.getElementById("overlayPopup");
  424. if (observerTarget) {
  425. MutationObserver =
  426. window.MutationObserver ||
  427. window.WebKitMutationObserver ||
  428. window.MozMutationObserver;
  429.  
  430. const observer = new MutationObserver(function() {
  431. // Callback
  432.  
  433. // Render if treasure map popup is available
  434. const mapTab = observerTarget.querySelector("[data-tab=map_mice]");
  435. const groupLen = document.querySelectorAll(
  436. ".treasureMapPopup-goals-groups"
  437. ).length;
  438.  
  439. // Prevent conflict with 'Bulk Map Invites'
  440. const inviteHeader = document.querySelector(
  441. ".treasureMapPopup-inviteFriend-header"
  442. );
  443.  
  444. if (
  445. mapTab &&
  446. mapTab.className.indexOf("active") >= 0 &&
  447. groupLen > 0 &&
  448. !inviteHeader
  449. ) {
  450. // Disconnect and reconnect later to prevent infinite mutation loop
  451. observer.disconnect();
  452.  
  453. render();
  454.  
  455. observer.observe(observerTarget, {
  456. childList: true,
  457. subtree: true
  458. });
  459. }
  460. });
  461.  
  462. observer.observe(observerTarget, {
  463. childList: true,
  464. subtree: true
  465. });
  466. }
  467. })();