MouseHunt - Bulk Map Invites

Easily invite many friends to your maps

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         MouseHunt - Bulk Map Invites
// @author       Tran Situ (tsitu)
// @namespace    https://greasyfork.org/en/users/232363-tsitu
// @version      1.1
// @description  Easily invite many friends to your maps
// @match        http://www.mousehuntgame.com/*
// @match        https://www.mousehuntgame.com/*
// ==/UserScript==

(function() {
  const observerTarget = document.getElementById("overlayPopup");
  if (observerTarget) {
    MutationObserver =
      window.MutationObserver ||
      window.WebKitMutationObserver ||
      window.MozMutationObserver;

    const observer = new MutationObserver(function() {
      // Callback
      const inviteHeader = document.querySelector(
        ".treasureMapPopup-inviteFriend-header"
      );

      // Render if friend invite header is in DOM
      if (inviteHeader) {
        // Disconnect and reconnect later to prevent mutation loop
        observer.disconnect();
        render(observer);
        observer.observe(observerTarget, {
          childList: true,
          subtree: true
        });
      }
    });

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

  function render(observer) {
    const obj = {}; // key = location, value = <a> array
    const friendVal = localStorage.getItem("tsitu-invite-friends") || "false";

    document
      .querySelectorAll(
        ".userSelectorView-userList-group.default a.treasureMapPopup-inviteFriend-row"
      )
      .forEach(el => {
        if (el.children.length === 7) insert(el);
      });

    if (friendVal == "true") {
      document
        .querySelectorAll(
          ".userSelectorView-userList-group.busy a.treasureMapPopup-inviteFriend-row"
        )
        .forEach(el => {
          if (el.children.length === 7) insert(el);
        });
    }

    // Insert location and <a> into obj
    function insert(el) {
      const location = el.children[1].textContent;
      if (obj[location] === undefined) {
        obj[location] = [el];
      } else {
        obj[location].push(el);
      }
    }

    const target = document.querySelector(
      ".treasureMapPopup-map-state.inviteFriends .treasureMapPopup-rightBlock"
    );
    if (target) {
      // Remove master div if it exists
      const existing = document.querySelector(".tsitu-map-invites-div");
      if (existing) existing.remove();

      // Initialize master div + styling
      const div = document.createElement("div");
      div.className = "tsitu-map-invites-div";
      div.style.margin = "0 10px 0 10px";
      div.style.textAlign = "center";

      function clickListener() {
        // Updates localStorage and re-renders
        observer.disconnect();

        localStorage.setItem(
          "tsitu-invite-friends",
          document.querySelector(".tsitu-map-friends-box").checked
        );

        localStorage.setItem(
          "tsitu-invite-sort",
          document.querySelector(".tsitu-loc-sort-box").checked
        );

        localStorage.setItem(
          "tsitu-invite-select",
          document.querySelector(".tsitu-friend-select-box").checked
        );

        render(observer);

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

      // All friends or only those not currently on a map
      const friendType = document.createElement("input");
      friendType.type = "checkbox";
      friendType.className = "tsitu-map-friends-box";
      friendType.name = "tsitu-map-friends";
      friendType.addEventListener("click", clickListener);

      const friendTypeLabel = document.createElement("label");
      friendTypeLabel.className = "tsitu-map-friends-label";
      friendTypeLabel.htmlFor = "tsitu-map-friends";
      friendTypeLabel.innerHTML = "Friends: <b>Not On Map</b> / All";

      // friendType checkmark
      const ftChecked = localStorage.getItem("tsitu-invite-friends") || "false";
      friendType.checked = ftChecked === "true";
      if (friendType.checked) {
        friendTypeLabel.innerHTML = "Friends: Not On Map / <b>All</b>";
      }

      // Sort locations alphabetically or by most hunters
      const locationSort = document.createElement("input");
      locationSort.type = "checkbox";
      locationSort.className = "tsitu-loc-sort-box";
      locationSort.name = "tsitu-loc-sort";
      locationSort.addEventListener("click", clickListener);

      const locationSortLabel = document.createElement("label");
      locationSortLabel.className = "tsitu-loc-sort-label";
      locationSortLabel.htmlFor = "tsitu-loc-sort";
      locationSortLabel.innerHTML = "Sort: <b>Alpha</b> / # Hunters";

      // locationSort checkmark
      const lsChecked = localStorage.getItem("tsitu-invite-sort") || "false";
      locationSort.checked = lsChecked === "true";
      if (locationSort.checked) {
        locationSortLabel.innerHTML = "Sort: Alpha / <b># Hunters</b>";
      }

      // Select friends randomly or by most clues found
      const friendSelect = document.createElement("input");
      friendSelect.type = "checkbox";
      friendSelect.className = "tsitu-friend-select-box";
      friendSelect.name = "tsitu-friend-select";
      friendSelect.addEventListener("click", clickListener);

      const friendSelectLabel = document.createElement("label");
      friendSelectLabel.className = "tsitu-friend-select-label";
      friendSelectLabel.htmlFor = "tsitu-friend-select";
      friendSelectLabel.innerHTML = "Select: <b>Random</b> / # Clues";

      // friendSelect checkmark
      const fsChecked = localStorage.getItem("tsitu-invite-select") || "false";
      friendSelect.checked = fsChecked === "true";
      if (friendSelect.checked) {
        friendSelectLabel.innerHTML = "Select: Random / <b># Clues</b>";
      }

      // Button to click <a>'s
      const goButton = document.createElement("button");
      goButton.className = "button";
      goButton.style.fontSize = "1.7em";
      goButton.style.marginBottom = "5px";
      goButton.innerText = "Go";
      goButton.addEventListener("click", function() {
        observer.disconnect();
        unclickRows();

        // Routine to click up to 8 friends
        const location = document.querySelector(".tsitu-map-loc-dropdown")
          .value;
        if (location) {
          // Cache location name
          localStorage.setItem("tsitu-invite-location", location);

          // Get friend select preference
          const selectVal =
            localStorage.getItem("tsitu-invite-select") == "true";

          if (location === "All") {
            const rawArr = [];
            for (let el of Object.keys(obj)) {
              for (let a of obj[el]) {
                rawArr.push(a);
              }
            }
            let sortArr = [];
            if (selectVal) {
              sortArr = arrayClues(rawArr);
            } else {
              sortArr = arrayShuffle(rawArr);
            }
            const maxIter = sortArr.length > 8 ? 8 : sortArr.length;
            for (let i = 0; i < maxIter; i++) {
              sortArr[i].click();
            }
          } else {
            let sortArr = [];
            if (selectVal) {
              sortArr = arrayClues(obj[location]);
            } else {
              sortArr = arrayShuffle(obj[location]);
            }
            const maxIter = sortArr.length > 8 ? 8 : sortArr.length;
            for (let i = 0; i < maxIter; i++) {
              sortArr[i].click();
            }
          }
        }

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

      // Button to unclick <a>'s
      const undoButton = document.createElement("button");
      undoButton.className = "button";
      undoButton.style.fontSize = "1.3em";
      undoButton.innerText = "↩️";
      undoButton.addEventListener("click", function() {
        observer.disconnect();
        unclickRows();
        observer.observe(observerTarget, {
          childList: true,
          subtree: true
        });
      });

      // Final element manipulation
      div.appendChild(goButton);
      div.appendChild(undoButton);
      div.appendChild(document.createElement("br"));
      div.appendChild(populateDropdown(obj));
      div.appendChild(document.createElement("br"));
      div.appendChild(friendType);
      div.appendChild(friendTypeLabel);
      div.appendChild(document.createElement("br"));
      div.appendChild(locationSort);
      div.appendChild(locationSortLabel);
      div.appendChild(document.createElement("br"));
      div.appendChild(friendSelect);
      div.appendChild(friendSelectLabel);
      target.appendChild(div);
    }
  }

  /**
   * Return <select> dropdown of sorted locations
   * Includes # of hunters per location in parentheses
   * Implicitly handles empty obj
   * @param {object} obj Object with key = location, value = array of <a>
   * @return {<select>} <select> with desired friend inclusion & location sort
   */
  function populateDropdown(obj) {
    // Remove dropdown if it exists
    const existing = document.querySelector(".tsitu-map-loc-dropdown");
    if (existing) existing.remove();

    // Create new dropdown and style it
    const dropdown = document.createElement("select");
    dropdown.className = "tsitu-map-loc-dropdown";
    dropdown.style.width = "100%";
    dropdown.style.marginBottom = "2px";

    // Add initial 'All' location option
    let counter = 0;
    const unsortedKeys = Object.keys(obj);
    unsortedKeys.forEach(el => {
      counter += obj[el].length;
    });

    const allOption = document.createElement("option");
    allOption.textContent = `All (${counter})`;
    allOption.value = "All";
    dropdown.appendChild(allOption);

    // Apply desired sort to location keys
    const sortVal = localStorage.getItem("tsitu-invite-sort") || "false";
    let sortedKeys = unsortedKeys;
    if (sortVal == "false") {
      sortedKeys = unsortedKeys.sort();
    } else if (sortVal == "true") {
      sortedKeys = unsortedKeys.sort(function(a, b) {
        return obj[b].length - obj[a].length;
      });
    }

    // Append <option>'s to the <select>
    for (let loc of sortedKeys) {
      const option = document.createElement("option");
      option.textContent = `${loc} (${obj[loc].length})`;
      option.value = loc;
      dropdown.appendChild(option);
    }

    // Select a dropdown value if available from cache
    const cachedLoc = localStorage.getItem("tsitu-invite-location");
    for (let el of dropdown.options) {
      const loc = el.textContent.split(" (")[0];
      if (loc === cachedLoc) {
        dropdown.value = cachedLoc;
      }
    }

    return dropdown;
  }

  // Routine to unclick invited friend rows
  function unclickRows() {
    // First pass: Try highlighted rows
    const highlighted = document.querySelectorAll(
      ".treasureMapPopup-inviteFriend-row.selected"
    );
    highlighted.forEach(el => el.click());

    // Second pass: Try to match icon images and names
    const selected = document.querySelectorAll(
      ".treasureMapPopup-inviteAction-friendSlot:not(.empty)"
    );
    if (selected.length > 0) {
      // Memoize obj with key = graph icon ID, value = player name
      const memo = {};
      selected.forEach(el => {
        const id = el.style.backgroundImage
          .split(".com/")[1]
          .split("/picture")[0];
        const name = el.title;
        memo[id] = name;
      });

      const memoKeys = Object.keys(memo);
      const rows = document.querySelectorAll(
        ".treasureMapPopup-inviteFriend-row.userSelectorView-user"
      );
      rows.forEach(el => {
        const id = el
          .querySelector(".treasureMapPopup-inviteFriend-profilePic")
          .style.backgroundImage.split(".com/")[1]
          .split("/picture")[0];
        if (memoKeys.indexOf(id) >= 0) {
          const name = el.querySelector(
            ".treasureMapPopup-inviteFriend-friendName"
          ).textContent;
          if (memo[id] === name) {
            el.click();
          }
        }
      });
    }
  }

  /**
   * @param {<a>[]} arr Input <a> array
   * @return {<a>[]} Randomly shuffled <a> array
   */
  function arrayShuffle(arr) {
    // Durstenfeld Shuffle
    let shuffledArr = arr.slice(0);
    for (let i = shuffledArr.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      const temp = shuffledArr[i];
      shuffledArr[i] = shuffledArr[j];
      shuffledArr[j] = temp;
    }

    return shuffledArr;
  }

  /**
   * Returns an <a> array sorted by most clues found
   * @param {<a>[]} arr Input <a> array
   * @return {<a>[]} Sorted <a> array
   */
  function arrayClues(arr) {
    return arr.sort(function(a, b) {
      return (
        parseInt(b.children[2].textContent) -
        parseInt(a.children[2].textContent)
      );
    });
  }
})();