MouseHunt - QoL Utilities

Miscellaneous utilities to turbo-charge your MH experience

// ==UserScript==
// @name         MouseHunt - QoL Utilities
// @author       Tran Situ (tsitu)
// @namespace    https://greasyfork.org/en/users/232363-tsitu
// @version      1.1.7
// @description  Miscellaneous utilities to turbo-charge your MH experience
// @match        http://www.mousehuntgame.com/*
// @match        https://www.mousehuntgame.com/*
// ==/UserScript==

(function () {
  /**
   * IDEA BANK
   * TODO: Alert when sounding horn after unseen trap check (user set 00, 15, 30, 45) to avoid wasted hunt if TC'd something
   * TODO: Track outbound supply transfers
   */

  // Adds direct hunter ID / snuid navigation popup via button on 'Friends' dropdown
  (function hunterIdNav() {
    document
      .querySelectorAll(".tsitu-hunter-id-nav")
      .forEach(el => el.remove());

    const target = document.querySelector("li .friend_list");
    if (target) {
      const li = document.createElement("li");
      li.className = "tsitu-hunter-id-nav";
      const button = document.createElement("a");
      button.href = "#";
      button.innerText = "ID Navigation";
      button.onclick = function () {
        const existing = document.querySelector("#tsitu-hunter-id-nav-ui");
        if (existing) existing.remove();
        else render();
        return false;
      };
      const icon = document.createElement("div");
      icon.className = "icon";
      button.appendChild(icon);
      li.appendChild(button);
      target.insertAdjacentElement("afterend", li);

      function render() {
        document
          .querySelectorAll("#tsitu-hunter-id-nav-ui")
          .forEach(el => el.remove());

        const div = document.createElement("div");
        div.id = "tsitu-hunter-id-nav-ui";
        div.style.backgroundColor = "#F5F5F5";
        div.style.position = "fixed";
        div.style.zIndex = "9999";
        div.style.left = "22.3vw";
        div.style.top = "28vh";
        div.style.border = "solid 3px #696969";
        div.style.borderRadius = "20px";
        div.style.padding = "10px";
        div.style.textAlign = "center";

        const closeButton = document.createElement("button", {
          id: "close-button"
        });
        closeButton.textContent = "x";
        closeButton.onclick = function () {
          document.body.removeChild(div);
        };

        const table = document.createElement("table");
        table.style.textAlign = "left";
        table.style.borderSpacing = "1em 0";
        const rowHid = document.createElement("tr");
        const rowSnuid = document.createElement("tr");

        const hidRadio = document.createElement("input");
        hidRadio.type = "radio";
        hidRadio.name = "tsitu-hunter-id";
        hidRadio.id = "tsitu-radio-hid";
        hidRadio.defaultChecked = true;
        hidRadio.onchange = function () {
          processRadio();
        };

        const hidRadioLabel = document.createElement("label");
        hidRadioLabel.innerText = "Hunter ID: ";
        hidRadioLabel.htmlFor = "tsitu-radio-hid";

        const hidInput = document.createElement("input");
        hidInput.type = "text";
        hidInput.id = "tsitu-input-hid";
        hidInput.placeholder = "e.g. 3795351";
        hidInput.onkeyup = function (event) {
          if (event.keyCode === 13) {
            goButton.click();
          }
        };

        const colHid = document.createElement("td");
        const colHidInput = document.createElement("td");
        colHid.appendChild(hidRadio);
        colHid.appendChild(document.createTextNode("\u00A0"));
        colHid.appendChild(hidRadioLabel);
        colHidInput.appendChild(hidInput);
        rowHid.appendChild(colHid);
        rowHid.appendChild(colHidInput);

        const snuidRadio = document.createElement("input");
        snuidRadio.type = "radio";
        snuidRadio.name = "tsitu-hunter-id";
        snuidRadio.id = "tsitu-radio-snuid";
        snuidRadio.onchange = function () {
          processRadio();
        };

        const snuidRadioLabel = document.createElement("label");
        snuidRadioLabel.innerText = "SN User ID: ";
        snuidRadioLabel.htmlFor = "tsitu-radio-snuid";

        const snuidInput = document.createElement("input");
        snuidInput.type = "text";
        snuidInput.id = "tsitu-input-snuid";
        snuidInput.placeholder = "e.g. 1062432650";
        snuidInput.onkeyup = function (event) {
          if (event.keyCode === 13) {
            goButton.click();
          }
        };

        const colSnuid = document.createElement("td");
        const colSnuidInput = document.createElement("td");
        colSnuid.appendChild(snuidRadio);
        colSnuid.appendChild(document.createTextNode("\u00A0"));
        colSnuid.appendChild(snuidRadioLabel);
        colSnuidInput.appendChild(snuidInput);
        rowSnuid.appendChild(colSnuid);
        rowSnuid.appendChild(colSnuidInput);

        function processRadio() {
          if (hidRadio.checked) {
            hidInput.disabled = false;
            snuidInput.disabled = true;
            localStorage.setItem("tsitu-id-radio", "hid");
          } else if (snuidRadio.checked) {
            hidInput.disabled = true;
            snuidInput.disabled = false;
            localStorage.setItem("tsitu-id-radio", "snuid");
          }
        }

        const goButton = document.createElement("button");
        goButton.style.fontWeight = "bold";
        goButton.innerText = "Go";
        goButton.onclick = function () {
          if (hidRadio.checked) {
            const val = hidInput.value;
            if (
              val.length > 0 &&
              val.length === parseInt(val).toString().length
            ) {
              const newWindow = window.open(
                `https://www.mousehuntgame.com/profile.php?id=${val}`
              );
            }
          } else if (snuidRadio.checked) {
            const val = snuidInput.value;
            if (
              val.length > 0 &&
              val.length === parseInt(val).toString().length
            ) {
              const newWindow = window.open(
                `https://www.mousehuntgame.com/profile.php?snuid=${val}`
              );
            }
          }
        };

        table.appendChild(rowHid);
        table.appendChild(rowSnuid);
        div.appendChild(closeButton);
        div.appendChild(document.createElement("br"));
        div.appendChild(document.createElement("br"));
        div.appendChild(table);
        div.appendChild(document.createElement("br"));
        div.appendChild(goButton);
        document.body.appendChild(div);

        // Apply cached radio selection
        const radioCache = localStorage.getItem("tsitu-id-radio");
        if (radioCache) {
          if (radioCache === "hid") {
            hidRadio.checked = true;
          } else if (radioCache === "snuid") {
            snuidRadio.checked = true;
          }
        }
        processRadio();
      }
    }
  })();

  // TODO: Claim-time map participants/duster tracker (move to mapping helper?)
  (function mapTracker() {
    const map = temp1;
    if (map.can_claim_reward && map.is_complete) {
      //
    }

    const data = {};
    map.hunters.forEach(hunter => {
      if (hunter.is_active) {
        data[hunter.user_id] = {
          name: hunter.name
        };

        const completedItems = hunter.completed_goal_ids.item;
        const completedMice = hunter.completed_goal_ids.mouse;
        if (completedItems.length > 0) {
          data[hunter.user_id].i = completedItems.length;
        }
        if (completedMice.length > 0) {
          data[hunter.user_id].m = completedMice.length;
        }

        if (hunter.upgrader) {
          data[hunter.user_id].d = true;
        }
      }
    });
    console.log(data);
  });

  (function lockConvertibleButtons() {
    // Observe for 'Special' tab of Inventory
    const observerTarget = document.querySelector(".mousehuntPage-content");
    if (observerTarget) {
      MutationObserver =
        window.MutationObserver ||
        window.WebKitMutationObserver ||
        window.MozMutationObserver;

      const observer = new MutationObserver(function () {
        const isSpecial = observerTarget.querySelector(
          ".mousehuntHud-page-tabContent.special.active"
        );
        if (isSpecial) {
          // Disconnect and reconnect later to prevent infinite mutation loop
          observer.disconnect();

          render();

          observer.observe(observerTarget, {
            childList: true,
            subtree: true
          });
        }
      });

      observer.observe(observerTarget, {
        childList: true,
        subtree: true
      });
    }

    function render() {
      document
        .querySelectorAll(".tsitu-lock-convertible")
        .forEach(el => el.remove());

      const convertibles = document.querySelectorAll(
        ".inventoryPage-item.full.convertible"
      );
      if (convertibles.length > 0) {
        // Apply cached locks on page load
        const cacheRaw = localStorage.getItem("tsitu-convertible-locks");
        if (cacheRaw) {
          const cache = JSON.parse(cacheRaw);
          convertibles.forEach(el => {
            const id = el.getAttribute("data-item-id");
            if (cache.indexOf(id) >= 0) {
              el.querySelectorAll(
                ".inventoryPage-item-content-action .button"
              ).forEach(button => {
                if (!button.classList.contains("disabled")) {
                  button.classList.toggle("disabled");
                }
              });
            }
          });
        }

        // Generate individual item lock buttons
        convertibles.forEach(el => {
          const a = document.createElement("a");
          a.href = "#";
          a.className =
            "inventoryPage-item-larryLexicon tsitu-lock-convertible";
          a.style.right = "22px";
          a.style.height = "15px";
          a.innerText = "🔒";
          a.onclick = function () {
            el.querySelectorAll(
              ".inventoryPage-item-content-action .button"
            ).forEach(button => {
              button.classList.toggle("disabled");
            });
            updateLockCache();
            return false;
          };
          const target = el.querySelector(".inventoryPage-item-name");
          if (target) target.insertAdjacentElement("afterend", a);
        });

        function updateLockCache() {
          const cacheArr = [];
          convertibles.forEach(el => {
            const button = el.querySelector(
              ".inventoryPage-item-content-action .button"
            );
            if (button.classList.contains("disabled")) {
              cacheArr.push(el.getAttribute("data-item-id"));
            }
          });
          localStorage.setItem(
            "tsitu-convertible-locks",
            JSON.stringify(cacheArr)
          );
        }

        // Add buttons for [un]locking entire tabs
        const lockableTabs = [
          "Baskets & Kits",
          "Scrolls, Posters, Assignments",
          "Spring Egg Hunt",
          "Treasure Chests"
        ];
        observerTarget
          .querySelectorAll(".inventoryPage-tagContent-tagGroup.clear-block")
          .forEach(tab => {
            const tabType = tab.getAttribute("data-name");
            if (lockableTabs.indexOf(tabType) >= 0) {
              const span = document.createElement("span");
              span.className =
                "inventoryPage-tagContent-tagTitle tsitu-lock-convertible";
              span.style.margin = "5px 0 5px 0";

              const lockAll = document.createElement("button");
              lockAll.innerText = "Lock Tab";
              lockAll.onclick = function () {
                if (
                  confirm(
                    `Are you sure you'd like to lock all convertibles on this tab?\n\n- ${tabType}`
                  )
                ) {
                  tab
                    .querySelectorAll(
                      ".inventoryPage-item-content-action .button"
                    )
                    .forEach(button => {
                      if (!button.classList.contains("disabled")) {
                        button.classList.toggle("disabled");
                      }
                    });
                  updateLockCache();
                }
              };

              const unlockAll = document.createElement("button");
              unlockAll.innerText = "Unlock Tab";
              unlockAll.onclick = function () {
                if (
                  confirm(
                    `Are you sure you'd like to unlock all convertibles on this tab?\n\n- ${tabType}`
                  )
                ) {
                  tab
                    .querySelectorAll(
                      ".inventoryPage-item-content-action .button"
                    )
                    .forEach(button => {
                      if (button.classList.contains("disabled")) {
                        button.classList.toggle("disabled");
                      }
                    });
                  updateLockCache();
                }
              };

              span.appendChild(lockAll);
              span.appendChild(document.createTextNode("\u00A0\u00A0"));
              span.appendChild(unlockAll);
              const target = tab.querySelector(
                ".inventoryPage-tagContent-tagTitle"
              );
              if (target) target.appendChild(span);
            }
          });
      }
    }
  })();

  // Adds RH location and 'Travel' button directly underneath clue
  (function relicHunterHintTravel() {
    const hintMap = {
      "Standing on the other side of a green and purple portal.":
        "Acolyte Realm",
      "Inside an elaborate one-way trap designed by Plankrun.": "Acolyte Realm",
      "Outside a smoky purple tower.": "Acolyte Realm",
      "Roaming amongst the most powerful of Lich mice.": "Balack's Cove",
      "Lurking in a damp and darkened grotto.": "Balack's Cove",
      "Searching for the best deals in the Burroughs.": "Bazaar",
      "Ducking between stalls and tents and loud merchants.": "Bazaar",
      "Under the pointiest tent in all the Kingdom!": "Bazaar",
      "Hobnobbing with giants.": "Bountiful Beanstalk",
      "Infiltrating a lofty castle.": "Bountiful Beanstalk",
      "Taking a relaxing hike through a forested area.": "Calm Clearing",
      "By a peaceful rock in a grassy clearing.": "Calm Clearing",
      "Tucked behind dense trees where it's quiet and peaceful.":
        "Calm Clearing",
      "Watching the peaceful gathering of tribal mice.": "Cape Clawed",
      "On a small bit of land near a volcano.": "Cape Clawed",
      "Listening for sinister secrets deep underground.": "Catacombs",
      "Walking through dark hallways in search of a Keeper's Candle.":
        "Catacombs",
      "Keeping an eye on the long-arm of the law.": "Claw Shot City",
      "Spitting in a spittoon! Yuck!": "Claw Shot City",
      "Leafing through ancient tomes of knowledge.": "Crystal Library",
      "Expanding knowledge and climbing endless ladders.": "Crystal Library",
      "Ankle deep in rocky, tropical sand.": "Derr Dunes",
      "Tumbling down hills of rreD sand.": "Derr Dunes",
      "Practicing an ancient art with fledgling warriors.": "Dojo",
      "Safely inside the bottom floor of a bamboo building.": "Dojo",
      "Carefully watching the training activities of advanced students.":
        "Dojo",
      "Near the bluE waters of the island.": "Elub Shore",
      "Watching the calm waters of Rodentia while remaining safely ashore.":
        "Elub Shore",
      "Marching through the Sandtail Desert.": "Fiery Warpath",
      "Dodging arrows, spears, swords, and spells!": "Fiery Warpath",
      "Investigating what can be built in a workshop.": "Floating Islands",
      "Searching the skies for treasure.": "Floating Islands",
      "Peering through an oculus.": "Floating Islands",
      "In the clouds above Hollow Heights.": "Floating Islands",
      "Watching hunters' dirigibles fly by.": "Floating Islands",
      "Avoiding falling victim to Sky Pirates.": "Floating Islands",
      "Trapped between two planes of existence.": "Forbidden Grove",
      "Behind heavy stone gates.": "Forbidden Grove",
      "Reaping what she sowed...": "Foreword Farm",
      "Cultivating a hearty yield...": "Foreword Farm",
      "Toiling away in fertile fields...": "Foreword Farm",
      "Carefully navigating a subterranean and humid environment.":
        "Fungal Cavern",
      "Deep inside of an infested, glowing, twisting, unending cave of untold riches...":
        "Fungal Cavern",
      "Tracing the deep patterns of bark growing on ancient towers.":
        "Great Gnarled Tree",
      "By a tree older than Gnawnia itself.": "Great Gnarled Tree",
      "Finding shade in the largest tree in the Kingdom.": "Great Gnarled Tree",
      "Near the loud and low horns and the dinging of bells.": "Harbour",
      "Visiting where many new hunters seek out seafaring mice.": "Harbour",
      "By the sea where there's plenty of fresh air and sunshine.": "Harbour",
      "Where royal strength rewards hunting prowess.": "King's Arms",
      "Under a circular roof atop arm-shared paths.": "King's Arms",
      "Browsing wares available with a most royal currency.": "King's Arms",
      "Climbing up spiralling, menacing stairs.": "King's Gauntlet",
      "Trekking up a massive tower in Valour.": "King's Gauntlet",
      "Atop a tall tower with the perfect view of an Eclipse.":
        "King's Gauntlet",
      "Performing bizarre experiments and chemical reactions.": "Laboratory",
      "Where the powerful and strange breeds of mice first arose.":
        "Laboratory",
      "Amongst brightly glowing potions.": "Laboratory",
      "Waist-deep in a shallow, sparkling pond.": "Lagoon",
      "Amongst sparkling, still water.": "Lagoon",
      "Climbing jagged rocks and slick moss.": "Lagoon",
      "Tending to a most troublesome and dangerous garden.": "Living Garden",
      "Enjoying a drink on the overgrown rooftop patio.": "Living Garden",
      "Looking across vast landscapes and the many horizons of the land.":
        "Mountain",
      "Cutting through the pass to reach the town on the other side.":
        "Mountain",
      "In a treacherous environment where only the toughest of mice survive.":
        "Mountain",
      "Investigating the spirits of slain mice.": "Mousoleum",
      "Surveying where scientists harvest 'spare parts'.": "Mousoleum",
      "Studying the spooky remains of Zombie Mice.": "Mousoleum",
      "Climbing and exploring some long lost ruins.": "Moussu Picchu",
      "Up high upon a weather changing plateau.": "Moussu Picchu",
      "Visiting a walled city that is no stranger to sieges.": "Muridae Market",
      "Keeping a close eye on would-be thieves...": "Muridae Market",
      "Investigating a well-seasoned Gumbo Cheese.": "Nerg Plains",
      "Running through flat fields of greeN.": "Nerg Plains",
      "Reeling it in...": "Prologue Pond",
      "Choosing the right tackle...": "Prologue Pond",
      "Enjoying a quick dip in a cheesy bath.": "Queso River",
      "Sipping delicious liquid cheese from a river.": "Queso River",
      "Protecting her ears from the sound of loud pumps.": "Queso River",
      "Testing out balance on the high seas.": "S.S. Huntington IV",
      "Looking a bit queasy...": "S.S. Huntington IV",
      "Watching the sky and wondering what the weather will bring.":
        "Seasonal Garden",
      "Braving the ever-changing elements.": "Seasonal Garden",
      "Walking along the coldest waters in Gnawnia.": "Slushy Shoreline",
      "At the beachside site of an invasion force!": "Slushy Shoreline",
      "Shivering near the edges of the mainland.": "Slushy Shoreline",
      "Walking along the bottom of the Rodentia Ocean.": "Sunken City",
      "Investigating powerful diving equipment.": "Sunken City",
      "Sitting down at a wooden table...": "Table of Contents",
      "Creating clever characters...": "Table of Contents",
      "Amongst the triumphant trumpets of master hunters of old.":
        "Tournament Hall",
      "Browsing the rewards of competitive champions.": "Tournament Hall",
      "Competing for the limelight amongst the finest champions.":
        "Tournament Hall",
      "Exploring the deep and winding caverns near a technologically-advanced underground city.":
        "Town of Digby",
      "Amongst powerful drills and excavation equipment.": "Town of Digby",
      "Hiding in the shadows while standing in the limelight.": "Town of Digby",
      "Hiding within the hustle and bustle in the city of the crown.":
        "Town of Gnawnia",
      "Trying to spot the King himself.": "Town of Gnawnia",
      "In a town with a dense population.": "Town of Gnawnia",
      "Standing among the ranks of new students out in the field.":
        "Training Grounds",
      "Watching the careful training of artful students.": "Training Grounds",
      "Relaxing in the shade of tall engraved rock.": "Training Grounds",
      "Observing the churning and grinding of the new harvest.": "Windmill",
      "By an agricultural structure once owned by one of Gnawnia's most prosperous farmers.":
        "Windmill",
      "Grinding up hundreds of tiny seeds from a stalky, golden plant.":
        "Windmill"
    };

    // MutationObserver logic for map UI
    // Observers are attached to a *specific* element (will DC if removed from DOM)
    const observerTarget = document.getElementById("overlayPopup");
    if (observerTarget) {
      MutationObserver =
        window.MutationObserver ||
        window.WebKitMutationObserver ||
        window.MozMutationObserver;

      const observer = new MutationObserver(function () {
        // Render if Relic Hunter hint is available
        const rhHint = observerTarget.querySelector(
          ".treasureMapInventoryView-relicHunter-hint"
        );

        // Prevent conflict with 'Mapping Helper'
        const mapTab = observerTarget.querySelector(
          ".treasureMapManagerView-header-navigation-item.tasks.active"
        );

        // Prevent conflict with 'Bulk Map Invites'
        const inviteHeader = document.querySelector(
          // ".treasureMapPopup-inviteFriend-header"
          ".treasureMapManagerDialogView-userSelector"
        );

        if (rhHint && !mapTab && !inviteHeader) {
          // Disconnect and reconnect later to prevent infinite mutation loop
          observer.disconnect();

          render();

          observer.observe(observerTarget, {
            childList: true,
            subtree: true
          });
        }
      });

      observer.observe(observerTarget, {
        childList: true,
        subtree: true
      });
    }

    function render() {
      document.querySelectorAll(".tsitu-rh-helper").forEach(el => el.remove());

      const div = document.createElement("div");
      div.style.textAlign = "center";
      div.className = "tsitu-rh-helper";
      const button = document.createElement("a");
      button.href = "#";
      button.className = "mousehuntActionButton small";
      const locSpan = document.createElement("span");
      locSpan.style.fontSize = "12px";
      locSpan.innerText = "N/A";

      const rhHint = observerTarget.querySelector(
        ".treasureMapInventoryView-relicHunter-hint"
      );
      const hint = rhHint.textContent;
      Object.keys(hintMap).forEach(key => {
        const loc = hintMap[key];
        if (hint == key) {
          locSpan.innerText = `Location: ${loc}`;
          button.onclick = function () {
            const newWindow = window.open(
              "https://www.mousehuntgame.com/travel.php?tab=map"
            );
            newWindow.addEventListener("load", function () {
              const hud = newWindow.document.querySelector(
                ".mousehuntHud-page-tabContent[data-tab='map']"
              );
              if (hud && hud.classList.contains("full")) {
                newWindow.document
                  .querySelectorAll(
                    ".travelPage-map-region-environment-link-name"
                  )
                  .forEach(el => {
                    if (el.textContent == loc) {
                      el.click();
                    }
                  });

                const scrollTo = newWindow.document.querySelector(
                  ".travelPage-map-simpleToggle.full"
                );
                if (scrollTo) {
                  scrollTo.scrollIntoView({
                    behavior: "auto",
                    block: "nearest",
                    inline: "nearest"
                  });
                }
              }
            });
            return false;
          };
        }
      });

      const buttonText = document.createElement("span");
      buttonText.innerText = "Travel";
      button.appendChild(buttonText);

      div.appendChild(button);
      div.appendChild(locSpan);
      rhHint.insertAdjacentElement("afterend", div);
    }
  })();
})();