Greasy Fork

MouseHunt - Favorite Setups

Unlimited custom favorite trap setups!

目前为 2020-05-23 提交的版本。查看 最新版本

// ==UserScript==
// @name         MouseHunt - Favorite Setups
// @author       Tran Situ (tsitu)
// @namespace    https://greasyfork.org/en/users/232363-tsitu
// @version      1.0
// @description  Unlimited custom favorite trap setups!
// @grant        GM_addStyle
// @match        http://www.mousehuntgame.com/*
// @match        https://www.mousehuntgame.com/*
// ==/UserScript==

(function () {
  // Sorted from low to high (currently matches top HUD)
  const displayOrder = {
    base: 1,
    weapon: 2,
    bait: 3,
    cheese: 3,
    trinket: 4,
    charm: 4,
    skin: 5
  };

  const originalOpen = XMLHttpRequest.prototype.open;
  XMLHttpRequest.prototype.open = function () {
    this.addEventListener("load", function () {
      if (
        this.responseURL ===
        "https://www.mousehuntgame.com/managers/ajax/users/gettrapcomponents.php"
      ) {
        let data;
        try {
          data = JSON.parse(this.responseText).components;
          if (data && data.length > 0) {
            const ownedItems = JSON.parse(
              localStorage.getItem("tsitu-owned-components")
            ) || {
              bait: {},
              base: {},
              weapon: {},
              trinket: {},
              skin: {}
            };

            data.forEach(el => {
              ownedItems[el.classification][el.name] =
                el.classification === "skin"
                  ? [el.item_id, el.thumbnail, el.component_name]
                  : [el.item_id, el.thumbnail];

              // switch statement for all 5 classifications
              // ^ custom array, last element = image_trap hash if available
              // ^ ideally thumbnail is also just the hash portion, img src can be trivially built dynamically
              // ^ i believe this is for synergy with equipment-preview, so it's not necessary for now
            });
            localStorage.setItem(
              "tsitu-owned-components",
              JSON.stringify(ownedItems)
            );
            localStorage.setItem("favorite-setup-timestamp", Date.now());
            const existing = document.querySelector("#tsitu-fave-setups");
            if (existing) render();
          } else {
            console.log(
              "Invalid components array data from gettrapcomponents.php"
            );
          }
        } catch (error) {
          console.log(
            "Failed to process server response for gettrapcomponents.php"
          );
          console.error(error.stack);
        }
      }
    });
    originalOpen.apply(this, arguments);
  };

  function render() {
    const existing = document.querySelector("#tsitu-fave-setups");
    if (existing) existing.remove();

    const rawData = localStorage.getItem("tsitu-owned-components");
    if (rawData) {
      const data = JSON.parse(rawData);
      const dataKeys = Object.keys(data).sort((a, b) => {
        return displayOrder[a] - displayOrder[b];
      });

      async function batchLoad(
        baitName,
        baseName,
        weaponName,
        trinketName,
        skinName
      ) {
        const diff = {};

        // Diff current setup with proposed batch to minimize server load
        if (data.bait[baitName] && user.bait_name !== baitName) {
          diff.bait = data.bait[baitName][0];
        }
        if (data.base[baseName] && user.base_name !== baseName) {
          diff.base = data.base[baseName][0];
        }
        if (data.weapon[weaponName] && user.weapon_name !== weaponName) {
          diff.weapon = data.weapon[weaponName][0];
        }
        if (data.trinket[trinketName] && user.trinket_name !== trinketName) {
          diff.trinket = data.trinket[trinketName][0];
        }
        // if (
        //   data.skin[skinName] &&
        //   data.skin[skinName][2] === weaponName &&
        //   user.skin_item_id !== data.skin[skinName][0]
        //   // note: this will probably proc every single time... diff AFTER weapon swap?
        // ) {
        //   diff.skin = data.skin[skinName][0];
        // }

        if (baitName === "N/A") diff.bait = "disarm";
        if (trinketName === "N/A") diff.trinket = "disarm";
        // if (skinName === "N/A") diff.skin = "disarm";

        // Cancel if setup isn't changing
        if (Object.keys(diff).length === 0) return;

        function sleep(ms) {
          return new Promise(resolve => setTimeout(resolve, ms));
        }

        const diffKeys = Object.keys(diff).sort((a, b) => {
          return displayOrder[a] - displayOrder[b];
        });

        for (let classification of diffKeys) {
          const id = diff[classification];
          if (id === "disarm") {
            await hg.utils.TrapControl.disarmItem(classification).go();
          } else {
            await hg.utils.TrapControl.armItem(id, classification).go();
          }
          await sleep(420);
        }

        // Deprecated the old method because unable to prevent userinventory.php calls from syncArmedItems (caused by mobile/regular desync)
        // Witnessed up to an 18 request simul-slam (at least +1 increments starting from 3 / n-1 duplicates with 1 response's items[] different)
        // If switching back to a previous setup then things do seem to be cached
        // CBS may investigate at some point, but going to use the new method above for v1.0 and beyond
      }

      const mainDiv = document.createElement("div");
      mainDiv.id = "tsitu-fave-setups";
      mainDiv.style.backgroundColor = "#F5F5F5";
      mainDiv.style.position = "fixed";
      mainDiv.style.zIndex = "42";
      mainDiv.style.left = "5px";
      mainDiv.style.top = "5px";
      mainDiv.style.border = "solid 3px #696969";
      mainDiv.style.borderRadius = "20px";
      mainDiv.style.padding = "10px";
      mainDiv.style.textAlign = "center";

      const mainSpan = document.createElement("span");
      mainSpan.innerText = "Favorite Setups";
      mainSpan.style = "font-weight: bold; font-size: 24px;";

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

      // Build <datalist> dropdowns
      const dataListTable = document.createElement("table");
      for (let rawCategory of dataKeys) {
        let category = rawCategory;
        if (category === "sort") continue;
        if (category === "skin") continue; // note: only show appropriate skins if implementing
        if (category === "bait") category = "cheese";
        if (category === "trinket") category = "charm";

        const dataList = document.createElement("datalist");
        dataList.id = `favorite-setup-datalist-${category}`;
        for (let item of Object.keys(data[rawCategory]).sort()) {
          const option = document.createElement("option");
          option.value = item;
          dataList.appendChild(option);
        }

        const dataListLabel = document.createElement("label");
        dataListLabel.htmlFor = `favorite-setup-input-${category}`;
        dataListLabel.textContent = `Select ${category}: `;

        const dataListInput = document.createElement("input");
        dataListInput.id = `favorite-setup-input-${category}`;
        dataListInput.setAttribute(
          "list",
          `favorite-setup-datalist-${category}`
        );

        const dataListRow = document.createElement("tr");
        const labelCol = document.createElement("td");
        labelCol.style.paddingRight = "8px";
        const inputCol = document.createElement("td");
        labelCol.appendChild(dataList);
        labelCol.appendChild(dataListLabel);
        inputCol.appendChild(dataListInput);
        dataListRow.appendChild(labelCol);
        dataListRow.appendChild(inputCol);
        dataListTable.appendChild(dataListRow);
      }

      const nameSpan = document.createElement("span");
      nameSpan.textContent = "Setup name: ";
      const nameSpanCol = document.createElement("td");
      nameSpanCol.appendChild(nameSpan);

      const nameInput = document.createElement("input");
      nameInput.type = "text";
      nameInput.id = "favorite-setup-name";
      nameInput.required = true;
      nameInput.minLength = 1;
      nameInput.maxLength = 20;
      const nameInputCol = document.createElement("td");
      nameInputCol.appendChild(nameInput);

      const nameRow = document.createElement("tr");
      nameRow.appendChild(nameSpanCol);
      nameRow.appendChild(nameInputCol);

      const saveButton = document.createElement("button");
      saveButton.style.fontSize = "16px";
      saveButton.textContent = "Save this setup";
      saveButton.onclick = function () {
        const bait = document.querySelector("#favorite-setup-input-cheese")
          .value;
        const base = document.querySelector("#favorite-setup-input-base").value;
        const weapon = document.querySelector("#favorite-setup-input-weapon")
          .value;
        const charm = document.querySelector("#favorite-setup-input-charm")
          .value;
        // const skin = document.querySelector("#favorite-setup-input-skin").value;
        const name = document.querySelector("#favorite-setup-name").value;

        if (name.length >= 1 && name.length <= 20) {
          const obj = {};
          obj[name] = {
            bait: "N/A",
            base: "N/A",
            weapon: "N/A",
            trinket: "N/A",
            skin: "N/A"
          };

          if (data.bait[bait] !== undefined) obj[name].bait = bait;
          if (data.base[base] !== undefined) obj[name].base = base;
          if (data.weapon[weapon] !== undefined) obj[name].weapon = weapon;
          if (data.trinket[charm] !== undefined) obj[name].trinket = charm;
          // if (data.skin[skin] !== undefined) obj[name].skin = skin;
          obj[name].sort = -1;

          const storedRaw = localStorage.getItem("favorite-setups-saved");
          if (storedRaw) {
            const storedData = JSON.parse(storedRaw);
            if (storedData[name] !== undefined) {
              if (confirm(`Do you want to overwrite saved setup '${name}'?`)) {
                obj[name].sort = storedData[name].sort;
              } else {
                return;
              }
            }
            storedData[name] = obj[name];
            localStorage.setItem(
              "favorite-setups-saved",
              JSON.stringify(storedData)
            );
          } else {
            localStorage.setItem("favorite-setups-saved", JSON.stringify(obj));
          }
          render();
        } else {
          alert(
            "Please enter a name for your setup that is between 1-20 characters"
          );
        }
      };

      const loadButton = document.createElement("button");
      loadButton.style.fontSize = "12px";
      loadButton.textContent = "Import current setup";
      loadButton.onclick = function () {
        document.querySelector("#favorite-setup-input-cheese").value =
          user.bait_name || "";
        document.querySelector("#favorite-setup-input-base").value =
          user.base_name || "";
        document.querySelector("#favorite-setup-input-weapon").value =
          user.weapon_name || "";
        document.querySelector("#favorite-setup-input-charm").value =
          user.trinket_name || "";
        // if (user.skin_name) {
        //   document.querySelector("#favorite-setup-input-skin").value =
        //     user.skin_name; // not really a thing, gotta use a qS probably or parse from LS ID-name map
        // }
      };

      const resetButton = document.createElement("button");
      resetButton.style.fontSize = "12px";
      resetButton.textContent = "Reset inputs";
      resetButton.onclick = function () {
        document.querySelector("#favorite-setup-input-cheese").value = "";
        document.querySelector("#favorite-setup-input-base").value = "";
        document.querySelector("#favorite-setup-input-weapon").value = "";
        document.querySelector("#favorite-setup-input-charm").value = "";
        // document.querySelector("#favorite-setup-input-skin").value = "";
        document.querySelector("#favorite-setup-name").value = "";
      };

      const buttonSpan = document.createElement("span");
      buttonSpan.style.paddingTop = "8px";
      buttonSpan.style.textAlign = "center";
      buttonSpan.appendChild(saveButton);
      buttonSpan.appendChild(document.createElement("br"));
      buttonSpan.appendChild(document.createElement("br"));
      buttonSpan.appendChild(loadButton);
      buttonSpan.appendChild(document.createTextNode("\u00A0\u00A0"));
      buttonSpan.appendChild(resetButton);

      dataListTable.appendChild(nameRow);
      const dataListDiv = document.createElement("div");
      dataListDiv.appendChild(dataListTable);

      const timeUpdated = document.createElement("span");
      let tsLatestStr = "N/A";
      const tsLatestRaw = localStorage.getItem("favorite-setup-timestamp");
      if (tsLatestRaw) {
        tsLatestStr = new Date(parseInt(tsLatestRaw)).toLocaleString();
      }
      timeUpdated.textContent = `Items last updated: ${tsLatestStr}`;

      const setupTableDiv = document.createElement("div");
      setupTableDiv.style.overflowY = "scroll";
      setupTableDiv.style.height = "38vh";
      const setupTable = document.createElement("table");
      const setupTbody = document.createElement("tbody");
      const tableCaption = document.createElement("caption");
      tableCaption.id = "tsitu-fave-setup-table-caption";
      tableCaption.style.paddingBottom = "5px";
      tableCaption.style.textAlign = "center";
      tableCaption.style.fontSize = "15px";
      tableCaption.style.textDecoration = "underline";
      tableCaption.textContent = "Saved Setups";

      const savedRaw = localStorage.getItem("favorite-setups-saved");
      const savedSetups = JSON.parse(savedRaw) || {};
      const savedSetupSortKeys = Object.keys(savedSetups).sort((a, b) => {
        return savedSetups[a].sort - savedSetups[b].sort;
      });

      if (savedSetupSortKeys.length > 0) {
        setupTable.appendChild(tableCaption);
      }

      // Create setup dropdown selector
      const setupSelector = document.createElement("datalist");
      setupSelector.id = "favorite-setup-selector";
      for (let item of savedSetupSortKeys) {
        const option = document.createElement("option");
        option.value = item;
        setupSelector.appendChild(option);
      }

      const setupSelectorLabel = document.createElement("label");
      setupSelectorLabel.htmlFor = "favorite-setup-selector-input";
      setupSelectorLabel.textContent = `Jump to setup: `;

      const setupSelectorInput = document.createElement("input");
      setupSelectorInput.id = "favorite-setup-selector-input";
      setupSelectorInput.setAttribute("list", "favorite-setup-selector");
      setupSelectorInput.oninput = function () {
        const name = setupSelectorInput.value;
        if (savedSetups[name] !== undefined) {
          const rows = document.querySelectorAll("tr.tsitu-fave-setup-row");
          rows.forEach(el => {
            el.style.backgroundColor = "";
          });

          /**
           * Return row element that matches dropdown setup name
           * @param {string} name Dropdown setup name
           * @return {HTMLElement|false} <tr> that should be highlighted and scrolled to
           */
          function findElement(name) {
            for (let el of rows) {
              const spans = el.querySelectorAll("span");
              if (spans.length === 2) {
                if (name === spans[0].textContent) {
                  return el;
                }
              }
            }

            return false;
          }

          // Calculate index for nth-child
          const targetEl = findElement(name);
          let nthChildValue = 0;
          for (let i = 0; i < rows.length; i++) {
            const el = rows[i];
            if (el === targetEl) {
              nthChildValue = i + 1;
              break;
            }
          }

          // tr:nth-child value (min = 1)
          const scrollRow = document.querySelector(
            `tr.tsitu-fave-setup-row:nth-child(${nthChildValue})`
          );
          if (scrollRow) {
            scrollRow.style.backgroundColor = "#D6EBA1";
            scrollRow.scrollIntoView({
              behavior: "auto",
              block: "nearest",
              inline: "nearest"
            });
          }

          setupSelectorInput.value = "";
        }
      };

      const setupSelectorDiv = document.createElement("div");
      setupSelectorDiv.appendChild(setupSelector);
      setupSelectorDiv.appendChild(setupSelectorLabel);
      setupSelectorDiv.appendChild(setupSelectorInput);

      // TODO: [high] Location tags on setup creation (checkboxes a la best setups)
      // TODO: [med]  Import/export setups (overwrite existing or a "profile" dropdown?) (dropbox or pastebin?)
      // TODO: [med]  Mobile UX for drag & drop as well as scrollable div (jquery-ui-touch-punch did not work for simulating touch events)
      // TODO: [low]  Edge cases like Golem Guardian (basically skin handling except picking 1 is mandatory)
      // TODO: [low]  Skin implementation/checks (in-progress, but either save for later or scrap entirely since use case is minimal)

      savedSetupSortKeys.forEach(name => {
        generateRow(name);
      });

      function generateRow(name) {
        const el = savedSetups[name];
        const elKeys = Object.keys(savedSetups[name]).sort((a, b) => {
          return displayOrder[a] - displayOrder[b];
        });

        const imgSpan = document.createElement("span");
        imgSpan.style.paddingRight = "10px";
        for (let type of elKeys) {
          if (type === "sort") continue;
          if (type === "skin") continue;

          const img = document.createElement("img");
          img.style.height = "40px";
          img.style.width = "40px";
          img.title = el[type];
          if (el[type] === "N/A") {
            if (type === "bait") img.title = "Disarm Bait";
            if (type === "trinket") img.title = "Disarm Charm";
            // if (type === "skin") img.title = "Disarm Skin";
          }
          img.onclick = function () {
            // Mobile tooltip behavior = LOW priority because long pressing works on FF
            // const appendTitle = img.querySelector(".append-title");
            // if (!appendTitle) {
            //   const appendSpan = document.createElement("span");
            //   appendSpan.className = "append-title";
            //   appendSpan.style.position = "absolute";
            //   appendSpan.style.padding = "4px";
            //   // appendSpan.textContent = el[type];
            //   appendSpan.textContent = img.title;
            //   img.append(appendSpan);
            // } else {
            //   appendTitle.remove();
            // }
          };
          img.src =
            "https://www.mousehuntgame.com/images/items/stats/ee8f12ab8e042415063ef4140cefab7b.gif?cv=243";
          if (data[type][el[type]]) img.src = data[type][el[type]][1];
          imgSpan.appendChild(img);
        }

        const nameSpan = document.createElement("span");
        nameSpan.className = "tsitu-fave-setup-namespan";
        nameSpan.style.fontSize = "14px";
        nameSpan.textContent = name;

        const nameImgCol = document.createElement("td");
        nameImgCol.style.padding = "5px 0px 5px 8px";
        nameImgCol.appendChild(nameSpan);
        nameImgCol.appendChild(document.createElement("br"));
        nameImgCol.appendChild(imgSpan);

        const armButton = document.createElement("button");
        armButton.style.fontSize = "14px";
        armButton.style.fontWeight = "bold";
        armButton.textContent = "Arm!";
        armButton.onclick = function () {
          batchLoad(el.bait, el.base, el.weapon, el.trinket, el.skin);
        };

        const editButton = document.createElement("button");
        editButton.style.fontSize = "10px";
        editButton.textContent = "✏️";
        editButton.onclick = function () {
          document.querySelector("#favorite-setup-input-cheese").value =
            el.bait === "N/A" ? "" : el.bait;
          document.querySelector("#favorite-setup-input-base").value =
            el.base === "N/A" ? "" : el.base;
          document.querySelector("#favorite-setup-input-weapon").value =
            el.weapon === "N/A" ? "" : el.weapon;
          document.querySelector("#favorite-setup-input-charm").value =
            el.trinket === "N/A" ? "" : el.trinket;
          // document.querySelector("#favorite-setup-input-skin").value =
          // el.skin === "N/A" ? "" : el.skin;
          document.querySelector("#favorite-setup-name").value = name || "";
        };

        const deleteButton = document.createElement("button");
        deleteButton.style.fontSize = "12px";
        deleteButton.textContent = "x";
        deleteButton.onclick = function () {
          if (confirm(`Delete setup '${name}'?`)) {
            const storedRaw = localStorage.getItem("favorite-setups-saved");
            if (storedRaw) {
              const storedData = JSON.parse(storedRaw);
              if (storedData[name]) delete storedData[name];
              localStorage.setItem(
                "favorite-setups-saved",
                JSON.stringify(storedData)
              );
              render();
            }
          }
        };

        const buttonCol = document.createElement("td");
        buttonCol.style.textAlign = "center";
        buttonCol.style.verticalAlign = "middle";
        buttonCol.style.paddingRight = "10px";
        buttonCol.appendChild(armButton);
        buttonCol.appendChild(document.createTextNode("\u00A0\u00A0"));
        buttonCol.appendChild(editButton);
        buttonCol.appendChild(document.createTextNode("\u00A0\u00A0"));
        buttonCol.appendChild(deleteButton);

        const setupRow = document.createElement("tr");
        setupRow.className = "tsitu-fave-setup-row";
        setupRow.appendChild(nameImgCol);
        setupRow.appendChild(buttonCol);
        setupTbody.appendChild(setupRow);
      }

      const saveSort = document.createElement("button");
      saveSort.innerText = "Save Sort Order";
      saveSort.onclick = function () {
        if (confirm("Are you sure you'd like to save this sort order?")) {
          const storedRaw = localStorage.getItem("favorite-setups-saved");
          if (storedRaw) {
            const storedData = JSON.parse(storedRaw);
            const nameSpans = document.querySelectorAll(
              ".tsitu-fave-setup-namespan"
            );
            if (nameSpans.length === Object.keys(storedData).length) {
              for (let i = 0; i < nameSpans.length; i++) {
                const name = nameSpans[i].textContent;
                if (storedData[name] !== undefined) {
                  storedData[name].sort = i;
                }
              }
              localStorage.setItem(
                "favorite-setups-saved",
                JSON.stringify(storedData)
              );
              render();
            }
          }
        }
      };
      const resetSort = document.createElement("button");
      resetSort.innerText = "Reset Sort Order";
      resetSort.onclick = function () {
        if (
          confirm("Are you sure you'd like to reset to last saved sort order?")
        ) {
          render();
        }
      };
      const sortSpan = document.createElement("span");
      sortSpan.innerText = "Drag & drop to rearrange setup rows (PC only)";

      GM_addStyle(
        ".ui-state-highlight-tsitu { height: 68px; background-color: #FAFFAF; }"
      );
      $(setupTbody).sortable({
        placeholder: "ui-state-highlight-tsitu",
        scroll: true,
        scrollSensitivity: 20,
        scrollSpeed: 10
      });
      setupTable.appendChild(setupTbody);
      setupTableDiv.appendChild(setupTable);
      mainDiv.appendChild(closeButton);
      mainDiv.appendChild(document.createElement("br"));
      mainDiv.appendChild(document.createElement("br"));
      mainDiv.appendChild(mainSpan);
      mainDiv.appendChild(document.createElement("br"));
      mainDiv.appendChild(document.createElement("br"));
      mainDiv.appendChild(timeUpdated);
      mainDiv.appendChild(document.createElement("br"));
      mainDiv.appendChild(document.createElement("br"));
      mainDiv.appendChild(dataListDiv);
      mainDiv.appendChild(document.createElement("br"));
      mainDiv.appendChild(buttonSpan);
      mainDiv.appendChild(document.createElement("br"));
      mainDiv.appendChild(document.createElement("br"));
      mainDiv.appendChild(document.createElement("br"));
      mainDiv.appendChild(setupSelectorDiv);
      mainDiv.appendChild(document.createElement("br"));
      mainDiv.appendChild(setupTableDiv);
      mainDiv.appendChild(document.createElement("br"));
      mainDiv.appendChild(saveSort);
      mainDiv.appendChild(document.createTextNode("\u00A0\u00A0"));
      mainDiv.appendChild(resetSort);
      mainDiv.appendChild(document.createElement("br"));
      mainDiv.appendChild(sortSpan);
      document.body.appendChild(mainDiv);
    } else {
      alert(
        "No owned item data available. Please refresh, click any of the 5 setup-changing boxes, and try again"
      );
    }
  }

  // Inject initial link into UI
  const target = document.querySelector(".mousehuntHud-gameInfo");
  if (target) {
    const link = document.createElement("a");
    link.innerText = "[Favorite Setups]";
    link.addEventListener("click", function () {
      const existing = document.querySelector("#tsitu-fave-setups");
      if (existing) existing.remove();
      else render();
      return false; // Prevent default link clicked behavior
    });
    target.prepend(link);
  }
})();