MouseHunt - Mapping Helper

Map interface improvements (invite via Hunter ID, direct send SB+, TEM-based available uncaught map mice)

  1. // ==UserScript==
  2. // @name MouseHunt - Mapping Helper
  3. // @author Tran Situ (tsitu)
  4. // @namespace https://gf.qytechs.cn/en/users/232363-tsitu
  5. // @version 2.7.0
  6. // @description Map interface improvements (invite via Hunter ID, direct send SB+, TEM-based available uncaught map mice)
  7. // @match http://www.mousehuntgame.com/*
  8. // @match https://www.mousehuntgame.com/*
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. // 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. const url = this.responseURL;
  17. let data = this.responseText;
  18. try {
  19. data = JSON.parse(data);
  20. } catch (error) {
  21. // Ignore non-JSON responses
  22. return;
  23. }
  24.  
  25. treasureMapListener(url, data);
  26. temListener(url, data);
  27.  
  28. // Check whether remaining map mice has updated for the map/TEM feature
  29. const rhMap = user.quests.QuestRelicHunter;
  30. if (rhMap && rhMap.maps.length > 0) {
  31. let currentMap;
  32. rhMap.maps.forEach(el => {
  33. if (el.map_id === rhMap.default_map_id) {
  34. currentMap = el; // Set "Active" map
  35. }
  36. });
  37.  
  38. if (currentMap.name.indexOf("Scavenger") < 0) {
  39. const remainStr = currentMap.remaining
  40. ? `${currentMap.map_id}~${currentMap.remaining}`
  41. : `${currentMap.map_id}~${currentMap.num_found}`;
  42. const cacheRemain = localStorage.getItem("tsitu-maptem-remain");
  43. if (cacheRemain) {
  44. if (remainStr !== cacheRemain) {
  45. // Map state has changed - fetch latest data
  46. localStorage.setItem("tsitu-maptem-remain", remainStr);
  47. updateMapData(currentMap.map_id);
  48. } else {
  49. // Map state unchanged - default render
  50. mapTEMRender();
  51. }
  52. } else {
  53. // Initial cache write
  54. localStorage.setItem("tsitu-maptem-remain", remainStr);
  55. updateMapData(currentMap.map_id);
  56. }
  57. } else {
  58. // Scavenger map detected - reset map data
  59. localStorage.removeItem("tsitu-maptem-remain");
  60. localStorage.removeItem("tsitu-maptem-mapmice");
  61. localStorage.removeItem("tsitu-maptem-cache-color");
  62. localStorage.removeItem("tsitu-maptem-cache-data");
  63. mapTEMRender();
  64. }
  65. } else {
  66. // No active maps - reset map data
  67. localStorage.removeItem("tsitu-maptem-remain");
  68. localStorage.removeItem("tsitu-maptem-mapmice");
  69. localStorage.removeItem("tsitu-maptem-cache-color");
  70. localStorage.removeItem("tsitu-maptem-cache-data");
  71. mapTEMRender();
  72. }
  73.  
  74. // Check whether environment/setup has updated for the map/TEM feature
  75. const baitID = user.bait_item_id || 0;
  76. const charmID = user.trinket_item_id || 0;
  77. const envString = `${user.environment_id}~${baitID}~${user.base_item_id}~${user.weapon_item_id}~${charmID}`;
  78. const cacheEnv = localStorage.getItem("tsitu-maptem-env");
  79. const isBatch = localStorage.getItem("tsitu-batch-loading");
  80. if (cacheEnv) {
  81. if (envString !== cacheEnv) {
  82. // Environment/setup has changed - fetch latest data
  83. localStorage.setItem("tsitu-maptem-env", envString);
  84. if (isBatch === "true") {
  85. // Do nothing - Favorite Setups script is in the middle of multiple item armings
  86. } else {
  87. requestTEMData();
  88. }
  89. } else {
  90. // Environment/setup state unchanged - default render
  91. mapTEMRender();
  92. }
  93. } else {
  94. // Initial cache write
  95. localStorage.setItem("tsitu-maptem-env", envString);
  96. requestTEMData();
  97. }
  98. });
  99. originalOpen.apply(this, arguments);
  100. };
  101.  
  102. function treasureMapListener(url, data) {
  103. if (url !== "https://www.mousehuntgame.com/managers/ajax/users/treasuremap_v2.php") {
  104. return;
  105. }
  106.  
  107. try {
  108. const map = data.treasure_map;
  109. if (map) {
  110. const obj = {};
  111. const condensed = {};
  112. condensed.hunters = map.hunters;
  113. condensed.is_complete = map.is_complete;
  114. condensed.is_owner = map.is_owner;
  115. condensed.is_scavenger_hunt = map.is_scavenger_hunt;
  116. condensed.is_wanted_poster = map.is_wanted_poster;
  117. condensed.map_class = map.map_class;
  118. condensed.map_id = map.map_id;
  119. condensed.timestamp = Date.now();
  120. obj[map.name] = condensed;
  121.  
  122. const mapCacheRaw = localStorage.getItem("tsitu-mapping-cache");
  123. if (mapCacheRaw) {
  124. const mapCache = JSON.parse(mapCacheRaw);
  125. mapCache[map.name] = condensed;
  126. localStorage.setItem(
  127. "tsitu-mapping-cache",
  128. JSON.stringify(mapCache)
  129. );
  130. } else {
  131. localStorage.setItem("tsitu-mapping-cache", JSON.stringify(obj));
  132. }
  133.  
  134. const mapEl = document.querySelector(".treasureMapView-mapMenu");
  135. if (mapEl) {
  136. render();
  137. }
  138. }
  139. } catch (error) {
  140. console.log("Server response doesn't contain a valid treasure map");
  141. console.error(error.stack);
  142. }
  143. }
  144.  
  145. function temListener(url, data) {
  146. if (url !== "https://www.mousehuntgame.com/managers/ajax/users/getmiceeffectiveness.php") {
  147. return;
  148. }
  149.  
  150. updateTEMData(data);
  151. }
  152.  
  153. // Queries and caches uncaught mice on an active treasure map
  154. function updateMapData(mapId) {
  155. hg.utils.TreasureMapUtil.getMapInfo(mapId, function (response) {
  156. const missingMice = [];
  157. const mapData = response.treasure_map;
  158. if (mapData.goals.mouse.length > 0 && mapData.hunters.length > 0) {
  159. const completedIDs = [];
  160. mapData.hunters.forEach(el => {
  161. const caughtMice = el.completed_goal_ids.mouse;
  162. if (caughtMice.length > 0) {
  163. caughtMice.forEach(id => completedIDs.push(id));
  164. }
  165. });
  166. mapData.goals.mouse.forEach(el => {
  167. if (completedIDs.indexOf(el.unique_id) < 0) {
  168. missingMice.push(el.name);
  169. }
  170. });
  171. localStorage.setItem(
  172. "tsitu-maptem-mapmice",
  173. JSON.stringify(missingMice)
  174. );
  175. mapTEMCompare();
  176. }
  177. }, function (errorResponse) {
  178. console.log("Error while requesting treasure map details");
  179. console.error(error.stack);
  180. });
  181. }
  182.  
  183. // Requests current mice list from TEM
  184. function requestTEMData() {
  185. postReq(
  186. "https://www.mousehuntgame.com/managers/ajax/users/getmiceeffectiveness.php",
  187. `sn=Hitgrab&hg_is_ajax=1&uh=${user.unique_hash}`
  188. ).then(res => {
  189. try {
  190. const response = JSON.parse(res.responseText);
  191. if (response) updateTEMData(response);
  192. } catch (error) {
  193. console.log("Error while requesting TEM details");
  194. console.error(error.stack);
  195. }
  196. });
  197. }
  198.  
  199. // Parses and caches mice from TEM response
  200. function updateTEMData(response) {
  201. const locationMice = [];
  202. for (let el in response.effectiveness) {
  203. const effMice = response.effectiveness[el].mice;
  204. effMice.forEach(el => {
  205. locationMice.push(el.name);
  206. });
  207. }
  208. localStorage.setItem("tsitu-maptem-temmice", JSON.stringify(locationMice));
  209. mapTEMCompare();
  210. }
  211.  
  212. // Compares cached TEM mice with uncaught map mice
  213. function mapTEMCompare() {
  214. const mapMiceRaw = localStorage.getItem("tsitu-maptem-mapmice");
  215. const temMiceRaw = localStorage.getItem("tsitu-maptem-temmice");
  216.  
  217. if (mapMiceRaw && temMiceRaw) {
  218. const mapMice = JSON.parse(mapMiceRaw);
  219. const temMice = JSON.parse(temMiceRaw);
  220.  
  221. const availableMice = [];
  222. if (mapMice.length === 0) {
  223. // Don't render in this state - remove data reset feature
  224. mapTEMRender("grey", []); // Completed or empty map
  225. } else {
  226. // Derive list of available mice
  227. mapMice.forEach(el => {
  228. if (temMice.indexOf(el) >= 0) availableMice.push(el);
  229. });
  230.  
  231. if (availableMice.length === 0) {
  232. mapTEMRender("red", []); // Map mice remaining but none available with setup
  233. } else {
  234. mapTEMRender("green", availableMice); // Map mice remaining and available with setup
  235. }
  236. }
  237. }
  238. }
  239.  
  240. /**
  241. * Renders icons for the map/TEM feature
  242. * @param {string} color Color of rendered notification icon
  243. * @param {array} data Array of available uncaught mice
  244. */
  245. function mapTEMRender(bgColor, data) {
  246. document.querySelectorAll(".tsitu-maptem").forEach(el => el.remove());
  247.  
  248. if (bgColor) {
  249. localStorage.setItem("tsitu-maptem-cache-color", bgColor);
  250. localStorage.setItem("tsitu-maptem-cache-data", JSON.stringify(data));
  251. }
  252.  
  253. const backgroundColor =
  254. localStorage.getItem("tsitu-maptem-cache-color") || "grey";
  255. const mouseList =
  256. JSON.parse(localStorage.getItem("tsitu-maptem-cache-data")) || [];
  257.  
  258. // Generate notification icon with numeric count, background color, title, and click handler
  259. const userStatWrapper = document.createElement("div");
  260. userStatWrapper.className = "mousehuntHud-userStat tsitu-maptem";
  261.  
  262. const notifDiv = document.createElement("div");
  263. notifDiv.className = "notification active";
  264. notifDiv.style.left = "315px";
  265. notifDiv.style.top = "-30px";
  266. notifDiv.style.background = backgroundColor;
  267. notifDiv.innerText = mouseList.length || 0;
  268.  
  269. if (backgroundColor === "grey") {
  270. return;
  271. // notifDiv.title =
  272. // "You are likely seeing this because:\n\n- Map is complete\n- No active maps\n- Scavenger map (not supported)\n- Initial data has not been fetched\n\nClick OK and then OK again if you'd like to RESET all saved data";
  273. } else if (backgroundColor === "red") {
  274. notifDiv.title =
  275. "No uncaught map mice with this setup (according to TEM)";
  276. } else if (backgroundColor === "green") {
  277. let titleText =
  278. "Uncaught map mice with this setup (according to TEM):\n\n";
  279. mouseList.forEach(el => {
  280. titleText += `- ${el}\n`;
  281. });
  282. notifDiv.title = titleText;
  283. }
  284.  
  285. if (backgroundColor !== "grey") {
  286. notifDiv.onclick = function (event) {
  287. event.stopPropagation(); // Prevent bubbling up
  288. if (
  289. confirm(
  290. `${notifDiv.title}\nClick 'OK' if the icon seems inaccurate and you'd like to force fetch current TEM mice. Otherwise, click 'Cancel'.`
  291. )
  292. ) {
  293. requestTEMData();
  294. }
  295. return false;
  296. };
  297. notifDiv.oncontextmenu = function () {
  298. if (confirm("Toggle map/TEM icon placement?")) {
  299. const placement = localStorage.getItem("tsitu-maptem-placement");
  300. if (!placement) {
  301. localStorage.setItem("tsitu-maptem-placement", "map");
  302. } else if (placement === "map") {
  303. localStorage.setItem("tsitu-maptem-placement", "default");
  304. } else if (placement === "default") {
  305. localStorage.setItem("tsitu-maptem-placement", "map");
  306. }
  307. mapTEMRender(); // Re-render
  308. }
  309. return false;
  310. };
  311. }
  312.  
  313. userStatWrapper.appendChild(notifDiv);
  314. const iconPlacement = localStorage.getItem("tsitu-maptem-placement");
  315. if (iconPlacement === "map") {
  316. target = document.querySelector(
  317. ".mousehuntHud-userStat.treasureMap .icon"
  318. );
  319. notifDiv.style.left = "14px";
  320. notifDiv.style.top = "14px";
  321. if (target) target.appendChild(userStatWrapper);
  322. } else {
  323. const target = document.querySelector(".campPage-trap-baitDetails");
  324. if (target) target.insertAdjacentElement("afterend", userStatWrapper);
  325. }
  326. }
  327. mapTEMRender(); // Initial default render on page load
  328.  
  329. // Renders custom UI elements onto the DOM
  330. function render() {
  331. // Clear out existing custom elements
  332. // Uses static collection instead of live one from getElementsByClassName
  333. document.querySelectorAll(".tsitu-mapping").forEach(el => el.remove());
  334. // document.querySelectorAll(".tsitu-queso-mapper").forEach(el => el.remove());
  335.  
  336. // Parent element that gets inserted at the end
  337. const masterEl = document.createElement("fieldset");
  338. masterEl.className = "tsitu-mapping";
  339. masterEl.style.width = "50%";
  340. masterEl.style.marginLeft = "15px";
  341. masterEl.style.padding = "5px";
  342. masterEl.style.border = "1px";
  343. masterEl.style.borderStyle = "dotted";
  344. const masterElLegend = document.createElement("legend");
  345. masterElLegend.innerText = `Mapping Helper v${GM_info.script.version} by tsitu`;
  346. masterEl.appendChild(masterElLegend);
  347.  
  348. /**
  349. * Refresh button
  350. * Iterate thru QRH.maps array for element matching current map and set its hash to empty string
  351. * This forces a hard refresh via hasCachedMap, which is called in show/showMap
  352. */
  353. const refreshSpan = document.createElement("span");
  354. refreshSpan.className = "tsitu-mapping tsitu-refresh-span";
  355. const refreshTextSpan = document.createElement("span");
  356. refreshTextSpan.innerText = "Refresh";
  357.  
  358. const refreshButton = document.createElement("button");
  359. refreshButton.className = "mousehuntActionButton tiny tsitu-mapping";
  360. refreshButton.style.cursor = "pointer";
  361. refreshButton.style.fontSize = "9px";
  362. refreshButton.style.padding = "2px";
  363. refreshButton.style.margin = "0px 5px 5px 10px";
  364. refreshButton.style.textShadow = "none";
  365. refreshButton.style.display = "inline-block";
  366. refreshButton.appendChild(refreshTextSpan);
  367.  
  368. refreshButton.addEventListener("click", function () {
  369. // Clear cache (is it possible to only do so for just a single map?)
  370. hg.controllers.TreasureMapController.clearMapCache();
  371.  
  372. // Parse map ID
  373. // Note: Unable to get ID for map currently in preview mode
  374. let mapId = -1;
  375. const activeTab = document.querySelector(
  376. ".treasureMapRootView-tab.active"
  377. );
  378. if (activeTab) {
  379. mapId = activeTab.getAttribute("data-type");
  380. }
  381.  
  382. // Close dialog and re-open with either current map or overview
  383. document.getElementById("jsDialogClose").click();
  384. mapId === -1
  385. ? hg.controllers.TreasureMapController.show()
  386. : hg.controllers.TreasureMapController.show(mapId);
  387. });
  388.  
  389. refreshSpan.appendChild(refreshButton);
  390. masterEl.appendChild(refreshSpan);
  391.  
  392. // Utility function that opens supply transfer page and auto-selects SB+
  393. function transferSB(snuid) {
  394. const newWindow = window.open(
  395. `https://www.mousehuntgame.com/supplytransfer.php?fid=${snuid}`
  396. );
  397. newWindow.addEventListener("load", function () {
  398. if (newWindow.supplyTransfer1) {
  399. newWindow.supplyTransfer1.setSelectedItemType("super_brie_cheese");
  400. newWindow.supplyTransfer1.renderTabMenu();
  401. newWindow.supplyTransfer1.render();
  402. }
  403. });
  404. return false;
  405. }
  406.  
  407. // Features that require cache checking
  408. const cacheRaw = localStorage.getItem("tsitu-mapping-cache");
  409. if (cacheRaw) {
  410. const cache = JSON.parse(cacheRaw);
  411. let mapName;
  412.  
  413. const tabHeader = document.querySelector(
  414. ".treasureMapRootView-tab.active"
  415. );
  416. if (tabHeader) {
  417. mapName = tabHeader.querySelector(
  418. ".treasureMapRootView-tab-name"
  419. ).textContent;
  420. } else {
  421. // Tab header disappears when only 1 map is open, so fall back to HUD label
  422. const hudLabel = document.querySelector(
  423. ".mousehuntHud-userStat.treasureMap .label"
  424. );
  425. if (hudLabel) {
  426. mapName = hudLabel.textContent;
  427. }
  428. }
  429.  
  430. const checkPreview = document.querySelector(
  431. ".treasureMapView-previewBar-content"
  432. );
  433. if (checkPreview) {
  434. mapName = checkPreview.textContent
  435. .split("'s ")[1]
  436. .split(".Back to Community")[0];
  437. }
  438.  
  439. if (cache[mapName] !== undefined) {
  440. // Abstract equality comparison because map ID can be number or string
  441. const mapId = tabHeader
  442. ? tabHeader.getAttribute("data-type")
  443. : user.quests.QuestRelicHunter.default_map_id;
  444. if (mapId == cache[mapName].map_id) {
  445. // "Last refreshed" timestamp
  446. if (cache[mapName].timestamp) {
  447. const timeSpan = document.createElement("span");
  448. timeSpan.innerText = `(This map was last refreshed on: ${new Date(
  449. parseInt(cache[mapName].timestamp)
  450. ).toLocaleString()})`;
  451. refreshSpan.appendChild(timeSpan);
  452. }
  453.  
  454. // Invite via Hunter ID (only for map captains)
  455. if (cache[mapName].is_owner) {
  456. const inputLabel = document.createElement("label");
  457. inputLabel.innerText = "Hunter ID: ";
  458. inputLabel.htmlFor = "tsitu-mapping-id-input";
  459.  
  460. const inputField = document.createElement("input");
  461. inputField.setAttribute("name", "tsitu-mapping-id-input");
  462. inputField.setAttribute("data-lpignore", "true"); // Nuke LastPass Autofill
  463. inputField.setAttribute("placeholder", "e.g. 1234567");
  464. inputField.setAttribute("required", true);
  465. inputField.addEventListener("keyup", function (e) {
  466. if (e.keyCode === 13) {
  467. inviteButton.click(); // 'Enter' pressed
  468. }
  469. });
  470.  
  471. const overrideStyle =
  472. "input[name='tsitu-mapping-id-input'] { -webkit-appearance:textfield; -moz-appearance:textfield; appearance:textfield; } input[name='tsitu-mapping-id-input']::-webkit-outer-spin-button, input[name='tsitu-mapping-id-input']::-webkit-inner-spin-button { display:none; -webkit-appearance:none; margin:0; }";
  473. let stylePresent = false;
  474. document.querySelectorAll("style").forEach(style => {
  475. if (style.textContent === overrideStyle) {
  476. stylePresent = true;
  477. }
  478. });
  479. if (!stylePresent) {
  480. const spinOverride = document.createElement("style");
  481. spinOverride.innerHTML = overrideStyle;
  482. document.body.appendChild(spinOverride);
  483. }
  484.  
  485. const inviteButton = document.createElement("button");
  486. inviteButton.style.marginLeft = "5px";
  487. inviteButton.innerText = "Invite";
  488. inviteButton.addEventListener("click", function () {
  489. const rawText = inputField.value.replace(/\D/g, "");
  490. if (rawText.length > 0) {
  491. const hunterId = parseInt(rawText);
  492. if (typeof hunterId === "number" && !isNaN(hunterId)) {
  493. if (hunterId > 0 && hunterId < 99999999) {
  494. postReq(
  495. "https://www.mousehuntgame.com/managers/ajax/pages/friends.php",
  496. `sn=Hitgrab&hg_is_ajax=1&action=community_search_by_id&user_id=${hunterId}&uh=${user.unique_hash}`
  497. ).then(res => {
  498. let response = null;
  499. try {
  500. if (res) {
  501. response = JSON.parse(res.responseText);
  502. const data = response.friend;
  503. if (
  504. data.user_interactions.actions.send_map_invite
  505. .has_maps
  506. ) {
  507. if (
  508. confirm(
  509. `Are you sure you'd like to invite this hunter?\n\nName: ${data.name}\nTitle: ${data.title_name} (${data.title_percent}%)\nLocation: ${data.environment_name}\nLast Active: ${data.last_active_formatted} ago`
  510. )
  511. ) {
  512. hg.utils.TreasureMapUtil.sendInvites(mapId, [data.sn_user_id], function (inviteRes) {
  513. if (inviteRes.success === 1) {
  514. refreshButton.click();
  515. } else {
  516. alert(
  517. "Map invite unsuccessful - may be because map is full"
  518. );
  519. }
  520. }, function (errorResponse) {
  521. alert("Error while inviting hunter to map");
  522. });
  523. }
  524. } else {
  525. if (data.name) {
  526. alert(
  527. `${data.name} cannot to be invited to a map at this time`
  528. );
  529. } else {
  530. alert("Invalid hunter information");
  531. }
  532. }
  533. }
  534. } catch (error) {
  535. alert("Error while requesting hunter information");
  536. console.error(error.stack);
  537. }
  538. });
  539. }
  540. }
  541. }
  542. });
  543.  
  544. const span = document.createElement("span");
  545. span.className = "tsitu-mapping";
  546. span.style.display = "inline-block";
  547. span.style.marginBottom = "10px";
  548. span.style.marginLeft = "10px";
  549. span.appendChild(inputLabel);
  550. span.appendChild(inputField);
  551. span.appendChild(inviteButton);
  552.  
  553. masterEl.insertAdjacentElement(
  554. "afterbegin",
  555. document.createElement("br")
  556. );
  557. masterEl.insertAdjacentElement("afterbegin", span);
  558. }
  559. }
  560.  
  561. const imgIDMap = {};
  562. const idNameMap = {};
  563. cache[mapName].hunters.forEach(el => {
  564. if (el.profile_pic) imgIDMap[el.profile_pic] = el.sn_user_id;
  565. idNameMap[el.sn_user_id] = el.name;
  566. });
  567.  
  568. // Utility function for image hover behavior
  569. function imgHover(img) {
  570. let imgURL;
  571. if (img.src) {
  572. imgURL = img.src;
  573. } else if (img.style.backgroundImage) {
  574. imgURL = img.style.backgroundImage.split('url("')[1].split('")')[0];
  575. }
  576.  
  577. const snuid = imgIDMap[imgURL];
  578. if (snuid) {
  579. const name = idNameMap[snuid];
  580. if (name) {
  581. img.title = `Send SB+ to ${name}`;
  582. }
  583.  
  584. img.href = "#";
  585. img.style.cursor = "pointer";
  586. img.onclick = function () {
  587. transferSB(snuid);
  588. };
  589. img.onmouseenter = function () {
  590. img.style.border = "dashed 1px green";
  591. };
  592. img.onmouseleave = function () {
  593. img.style.border = "";
  594. };
  595. }
  596. }
  597.  
  598. // Hunter container images
  599. document
  600. .querySelectorAll(
  601. ".treasureMapView-hunter:not(.empty) .treasureMapView-hunter-image"
  602. )
  603. .forEach(img => {
  604. imgHover(img);
  605. });
  606.  
  607. // Corkboard message images
  608. document
  609. .querySelectorAll("[data-message-id] .messageBoardView-message-image")
  610. .forEach(img => {
  611. imgHover(img);
  612. });
  613.  
  614. // "x found these mice" images
  615. document
  616. .querySelectorAll(".treasureMapView-block-content-heading-image")
  617. .forEach(img => {
  618. imgHover(img);
  619. });
  620. }
  621. }
  622.  
  623. // Final render
  624. document
  625. .querySelector(".treasureMapView-mapMenu")
  626. .insertAdjacentElement("afterend", masterEl);
  627. }
  628.  
  629. // POST to specified endpoint URL with desired form data
  630. function postReq(url, form) {
  631. return new Promise((resolve, reject) => {
  632. const xhr = new XMLHttpRequest();
  633. xhr.open("POST", url, true);
  634. xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
  635. xhr.onreadystatechange = function () {
  636. if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
  637. resolve(this);
  638. }
  639. };
  640. xhr.onerror = function () {
  641. reject(this);
  642. };
  643. xhr.send(form);
  644. });
  645. }
  646.  
  647. // MutationObserver logic for map UI
  648. // Observers are attached to a *specific* element (will DC if removed from DOM)
  649. const observerTarget = document.getElementById("overlayPopup");
  650. if (observerTarget) {
  651. MutationObserver =
  652. window.MutationObserver ||
  653. window.WebKitMutationObserver ||
  654. window.MozMutationObserver;
  655.  
  656. const observer = new MutationObserver(function () {
  657. // Callback
  658.  
  659. // Render if treasure map popup is available
  660. const mapTab = observerTarget.querySelector(
  661. ".treasureMapRootView-tab.active"
  662. );
  663. const groupLen = document.querySelectorAll(
  664. ".treasureMapView-goals-groups"
  665. ).length;
  666.  
  667. // Prevent conflict with 'Bulk Map Invites'
  668. const inviteHeader = document.querySelector(
  669. ".treasureMapUserSelectorView" // TODO: may need fine tuning once that script is fixed
  670. );
  671.  
  672. if (
  673. mapTab &&
  674. mapTab.className.indexOf("active") >= 0 &&
  675. groupLen > 0 &&
  676. !inviteHeader
  677. ) {
  678. // Disconnect and reconnect later to prevent infinite mutation loop
  679. observer.disconnect();
  680.  
  681. render();
  682.  
  683. observer.observe(observerTarget, {
  684. childList: true,
  685. subtree: true
  686. });
  687. }
  688. });
  689.  
  690. observer.observe(observerTarget, {
  691. childList: true,
  692. subtree: true
  693. });
  694. }
  695.  
  696. // Queso Mapper functionality (deprecated as of v2.0 - may reinstate if in demand)
  697. function quesoMapperFuncDepr() {
  698. // ***
  699. // Check for valid Queso Canyon map name
  700. const mapNameSelector = document.querySelector(
  701. ".treasureMapPopup-header-title.mapName"
  702. );
  703. if (mapNameSelector) {
  704. const split = mapNameSelector.textContent.split("Rare ");
  705. const mapName = split.length === 2 ? split[1] : split[0];
  706.  
  707. if (quesoMaps.indexOf(mapName) >= 0) {
  708. // Queso Mapper toggling
  709. const quesoToggle = document.createElement("input");
  710. quesoToggle.type = "checkbox";
  711. quesoToggle.className = "tsitu-mapping";
  712. quesoToggle.name = "tsitu-queso-toggle";
  713. quesoToggle.addEventListener("click", function () {
  714. localStorage.setItem("tsitu-queso-toggle", quesoToggle.checked);
  715. render();
  716. });
  717.  
  718. const quesoToggleLabel = document.createElement("label");
  719. quesoToggleLabel.className = "tsitu-mapping";
  720. quesoToggleLabel.htmlFor = "tsitu-queso-toggle";
  721. quesoToggleLabel.innerText = "Toggle Queso Mapper functionality";
  722.  
  723. const qtChecked = localStorage.getItem("tsitu-queso-toggle") || "false";
  724. quesoToggle.checked = qtChecked === "true";
  725. if (quesoToggle.checked) {
  726. quesoRender();
  727. }
  728.  
  729. const quesoToggleDiv = document.createElement("div");
  730. quesoToggleDiv.className = "tsitu-queso-mapper";
  731. if (!quesoToggle.checked) quesoToggleDiv.style.marginBottom = "10px";
  732. quesoToggleDiv.appendChild(quesoToggle);
  733. quesoToggleDiv.appendChild(quesoToggleLabel);
  734.  
  735. document
  736. .querySelector(".treasureMapPopup-hunterContainer")
  737. .insertAdjacentElement("afterend", quesoToggleDiv);
  738. }
  739. }
  740. // ***
  741.  
  742. function quesoRender() {
  743. const mapMice = document.querySelectorAll(
  744. "div.treasureMapPopup-goals-group-goal.treasureMapPopup-searchIndex.mouse"
  745. );
  746. if (mapMice.length > 0) {
  747. // Generate DOM elements
  748. const displayDiv = document.createElement("div");
  749. displayDiv.className = "tsitu-queso-mapper";
  750. displayDiv.style.fontSize = "14px";
  751. displayDiv.style.marginBottom = "10px";
  752. displayDiv.innerText = "Preferred Location & Cheese -> ";
  753.  
  754. const cacheSel = localStorage.getItem("tsitu-queso-mapper-sel");
  755. if (cacheSel) {
  756. const cache = JSON.parse(cacheSel);
  757. for (let location in quesoData) {
  758. const locSel = `${classBuilder(location, "loc")}`;
  759. if (cache[locSel] !== undefined) {
  760. const locationSpan = document.createElement("span");
  761. locationSpan.innerText = `${location}: `;
  762. let cheeseCount = 0;
  763. for (let cheese in quesoData[location]) {
  764. const cheeseSel = `${classBuilder(location, cheese)}`;
  765. if (cache[locSel].indexOf(cheeseSel) >= 0) {
  766. const cheeseSpan = document.createElement("span");
  767. let prependStr = "";
  768. if (cheeseCount > 0) prependStr = ", ";
  769.  
  770. const imgSpan = document.createElement("span");
  771. imgSpan.setAttribute(
  772. "style",
  773. `background-image:url('${quesoImg[cheese]}');width:20px;height:20px;display:inline-block;background-size:contain;background-repeat:no-repeat;position:relative;top:4px;`
  774. );
  775.  
  776. let appendStr = "";
  777. if (cheese !== "Standard" && cheese !== "SB+") {
  778. appendStr += " Queso";
  779. }
  780.  
  781. cheeseSpan.innerText = `${prependStr + cheese + appendStr}`;
  782. locationSpan.append(cheeseSpan);
  783. locationSpan.append(document.createTextNode("\u00A0"));
  784. locationSpan.append(imgSpan);
  785. cheeseCount += 1;
  786. }
  787. }
  788. displayDiv.appendChild(locationSpan);
  789. }
  790. }
  791. } else {
  792. displayDiv.style.marginTop = "5px";
  793. displayDiv.innerText = "Preferred Location & Cheese -> N/A";
  794. }
  795.  
  796. const target = document.querySelector(
  797. ".treasureMapPopup-map-stateContainer.viewGoals"
  798. );
  799. if (target) target.insertAdjacentElement("beforebegin", displayDiv);
  800.  
  801. mapMice.forEach(el => {
  802. if (el.className.indexOf("tsitu-queso-mapper-mouse") < 0) {
  803. function listener() {
  804. const span = el.querySelector("span");
  805. if (span) {
  806. const mouse = span.textContent;
  807. const mouseData = quesoMice[mouse];
  808. if (mouseData) {
  809. const toCache = {};
  810. for (let arr of mouseData) {
  811. const locSel = classBuilder(arr[0], "loc");
  812. const cheeseSel = classBuilder(arr[0], arr[1]);
  813. if (toCache[locSel] === undefined) {
  814. toCache[locSel] = [cheeseSel];
  815. } else {
  816. toCache[locSel].push(cheeseSel);
  817. }
  818. localStorage.setItem(
  819. "tsitu-queso-mapper-sel",
  820. JSON.stringify(toCache)
  821. );
  822. render();
  823. }
  824. }
  825. }
  826. }
  827.  
  828. el.addEventListener("mouseover", function () {
  829. listener();
  830. });
  831.  
  832. el.addEventListener("click", function () {
  833. listener();
  834. });
  835. }
  836. el.classList.add("tsitu-queso-mapper-mouse");
  837. });
  838. }
  839.  
  840. function classBuilder(location, cheese) {
  841. let retVal = "";
  842.  
  843. switch (location) {
  844. case "Queso River":
  845. retVal += "river-";
  846. break;
  847. case "Prickly Plains":
  848. retVal += "plains-";
  849. break;
  850. case "Cantera Quarry":
  851. retVal += "quarry-";
  852. break;
  853. case "Cork Collecting":
  854. retVal += "cork-";
  855. break;
  856. case "Pressure Building":
  857. retVal += "pressure-";
  858. break;
  859. case "Small Eruption":
  860. retVal += "small-";
  861. break;
  862. case "Medium Eruption":
  863. retVal += "medium-";
  864. break;
  865. case "Large Eruption":
  866. retVal += "large-";
  867. break;
  868. case "Epic Eruption":
  869. retVal += "epic-";
  870. break;
  871. default:
  872. retVal += location;
  873. }
  874.  
  875. switch (cheese) {
  876. case "Standard":
  877. retVal += "standard";
  878. break;
  879. case "SB+":
  880. retVal += "super";
  881. break;
  882. case "Bland":
  883. retVal += "bland";
  884. break;
  885. case "Mild":
  886. retVal += "mild";
  887. break;
  888. case "Medium":
  889. retVal += "medium";
  890. break;
  891. case "Hot":
  892. retVal += "hot";
  893. break;
  894. case "Flamin'":
  895. retVal += "flamin";
  896. break;
  897. case "Wildfire":
  898. retVal += "wildfire";
  899. break;
  900. default:
  901. retVal += cheese;
  902. }
  903.  
  904. return retVal;
  905. }
  906. }
  907.  
  908. // Valid Queso map variants
  909. const quesoMaps = [
  910. "Queso Canyoneer Treasure Map",
  911. "Queso Geyser Treasure Map",
  912. "Queso Canyon Grand Tour Treasure Map"
  913. ];
  914.  
  915. // Queso cheese image icons
  916. const quesoImg = {
  917. "Standard":
  918. "https://www.mousehuntgame.com/images/items/bait/7e0daa548364166c46c0804e6cb122c6.gif?cv=243",
  919. "SB+":
  920. "https://www.mousehuntgame.com/images/items/bait/d3bb758c09c44c926736bbdaf22ee219.gif?cv=243",
  921. "Bland":
  922. "https://www.mousehuntgame.com/images/items/bait/4752dbfdce202c0d7ad60ce0bacbebae.gif?cv=243",
  923. "Mild":
  924. "https://www.mousehuntgame.com/images/items/bait/7193159aa90c85ba67cbe02d209e565f.gif?cv=243",
  925. "Medium":
  926. "https://www.mousehuntgame.com/images/items/bait/be747798c5e6a7747ba117e9c32a8a1f.gif?cv=243",
  927. "Hot":
  928. "https://www.mousehuntgame.com/images/items/bait/11d1170bc85f37d67e26b0a05902bc3f.gif?cv=243",
  929. "Flamin'":
  930. "https://www.mousehuntgame.com/images/items/bait/5a69c1ea617ba622bd1dd227afb69a68.gif?cv=243",
  931. "Wildfire":
  932. "https://www.mousehuntgame.com/images/items/bait/73891a065f1548e474177165734ce78d.gif?cv=243"
  933. };
  934.  
  935. // Location -> Cheese -> Mouse
  936. const quesoData = {
  937. "Queso River": {
  938. "Standard": [
  939. "Tiny Saboteur",
  940. "Pump Raider",
  941. "Croquet Crusher",
  942. "Queso Extractor"
  943. ],
  944. "SB+": ["Sleepy Merchant"],
  945. "Wildfire": ["Queen Quesada"]
  946. },
  947. "Prickly Plains": {
  948. "Bland": ["Spice Seer", "Old Spice Collector"],
  949. "Mild": ["Spice Farmer", "Granny Spice"],
  950. "Medium": ["Spice Sovereign", "Spice Finder"],
  951. "Hot": ["Spice Raider", "Spice Reaper"],
  952. "Flamin'": ["Inferna, The Engulfed"]
  953. },
  954. "Cantera Quarry": {
  955. "Bland": ["Chip Chiseler", "Tiny Toppler"],
  956. "Mild": ["Ore Chipper", "Rubble Rummager"],
  957. "Medium": ["Nachore Golem", "Rubble Rouser"],
  958. "Hot": ["Grampa Golem", "Fiery Crusher"],
  959. "Flamin'": ["Nachous, The Molten"]
  960. },
  961. "Cork Collecting": {
  962. "Bland": ["Fuzzy Drake"],
  963. "Mild": ["Cork Defender"],
  964. "Medium": ["Burly Bruiser"],
  965. "Hot": ["Horned Cork Hoarder"],
  966. "Flamin'": ["Rambunctious Rain Rumbler", "Corky, the Collector"],
  967. "Wildfire": ["Corkataur"]
  968. },
  969. "Pressure Building": {
  970. "Mild": ["Steam Sailor"],
  971. "Medium": ["Warming Wyvern"],
  972. "Hot": ["Vaporior"],
  973. "Flamin'": ["Pyrehyde"],
  974. "Wildfire": ["Emberstone Scaled"]
  975. },
  976. "Small Eruption": {
  977. Mild: ["Sizzle Pup"],
  978. Medium: ["Sizzle Pup"],
  979. Hot: ["Sizzle Pup"]
  980. // Mild: ["Mild Spicekin", "Sizzle Pup"],
  981. // Medium: ["Sizzle Pup", "Smoldersnap", "Mild Spicekin"],
  982. // Hot: ["Sizzle Pup", "Ignatia"]
  983. },
  984. "Medium Eruption": {
  985. "Medium": ["Bearded Elder"],
  986. "Hot": ["Bearded Elder"],
  987. "Flamin'": ["Bearded Elder"]
  988. // Mild: ["Mild Spicekin"],
  989. // Medium: ["Bearded Elder", "Smoldersnap"],
  990. // Hot: ["Bearded Elder", "Ignatia"],
  991. // "Flamin'": ["Bearded Elder", "Bruticus, the Blazing"]
  992. },
  993. "Large Eruption": {
  994. "Hot": ["Cinderstorm"],
  995. "Flamin'": ["Cinderstorm"]
  996. // Medium: ["Smoldersnap"],
  997. // Hot: ["Cinderstorm", "Ignatia"],
  998. // "Flamin'": ["Cinderstorm", "Bruticus, the Blazing"]
  999. },
  1000. "Epic Eruption": {
  1001. "Flamin'": ["Stormsurge, the Vile Tempest"],
  1002. "Wildfire": ["Kalor'ignis of the Geyser"]
  1003. // Hot: ["Ignatia", "Stormsurge, the Vile Tempest"],
  1004. // "Flamin'": ["Stormsurge, the Vile Tempest", "Bruticus, the Blazing"],
  1005. },
  1006. "Any Eruption": {
  1007. "Mild": ["Mild Spicekin"],
  1008. "Medium": ["Smoldersnap"],
  1009. "Hot": ["Ignatia"],
  1010. "Flamin'": ["Bruticus, the Blazing"]
  1011. }
  1012. };
  1013.  
  1014. // Alternate representation: Mouse -> Location -> Cheese
  1015. const quesoMice = {};
  1016. for (let location in quesoData) {
  1017. for (let cheese in quesoData[location]) {
  1018. const arr = quesoData[location][cheese];
  1019. for (let mouse of arr) {
  1020. if (quesoMice[mouse] === undefined) {
  1021. quesoMice[mouse] = [[location, cheese]];
  1022. } else {
  1023. quesoMice[mouse].push([location, cheese]);
  1024. }
  1025. }
  1026. }
  1027. }
  1028. }
  1029. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址