MouseHunt - TEM Catch Stats

Adds catch/crown statistics next to mouse names on the TEM

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         MouseHunt - TEM Catch Stats
// @author       Tran Situ (tsitu)
// @namespace    https://greasyfork.org/en/users/232363-tsitu
// @version      1.8.5
// @description  Adds catch/crown statistics next to mouse names on the TEM
// @grant        GM_addStyle
// @match        http://www.mousehuntgame.com/*
// @match        https://www.mousehuntgame.com/*
// ==/UserScript==

(function () {
  // Hide scrollbar in TEM to fix many width issues
  // https://stackoverflow.com/questions/3296644/hiding-the-scroll-bar-on-an-html-page/54984409#54984409
  GM_addStyle(
    ".campPage-trap-trapEffectiveness-content { scrollbar-width: none; -ms-overflow-style: none; }" // FF / IE / Edge
  );
  GM_addStyle(
    ".campPage-trap-trapEffectiveness-content::-webkit-scrollbar { width: 0px; }" // Chrome / Safari / Opera
  );

  function mutationCallback() {
    const labels = document.getElementsByClassName(
      "campPage-trap-trapEffectiveness-difficultyGroup-label"
    );

    // Render if difficulty labels are in DOM
    if (labels.length > 0) {
      render();
    }
  }

  // Observers are attached to a *specific* element (will DC if removed from DOM)
  const observerTarget = document.querySelector(
    ".mousehuntHud-page-tabContentContainer"
  );
  const MutationObserver = window.MutationObserver ||
      window.WebKitMutationObserver ||
      window.MozMutationObserver;
  const observer = new MutationObserver(mutationCallback);

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

    /**
     * Zoom Detection
     * https://stackoverflow.com/questions/995914/catch-browsers-zoom-event-in-javascript/52008131#52008131
     */
    let px_ratio =
      window.devicePixelRatio ||
      window.screen.availWidth / document.documentElement.clientWidth;

    function isZooming() {
      let newPx_ratio =
        window.devicePixelRatio ||
        window.screen.availWidth / document.documentElement.clientWidth;

      if (newPx_ratio != px_ratio) {
        px_ratio = newPx_ratio;
        // console.log("Zooming");
        return true;
      } else {
        // console.log("Resizing");
        return false;
      }
    }

    window.onresize = function () {
      if (isZooming()) {
        mutationCallback();
      }
    };
  }

  function postReq(form) {
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open(
        "POST",
        `https://www.mousehuntgame.com/managers/ajax/mice/getstat.php?sn=Hitgrab&hg_is_ajax=1&action=get_hunting_stats&uh=${user.unique_hash}`,
        true
      );
      xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
      xhr.onreadystatechange = function () {
        if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
          resolve(this);
        }
      };
      xhr.onerror = function () {
        reject(this);
      };
      xhr.send(form);
    });
  }

  function render() {
    // Disconnect and reconnect later to prevent infinite mutation loop
    observer.disconnect();

    // Track crown counts
    let mouseCount = 0;
    const crownYes = {
      bronze: [0, "#b75935"],
      silver: [0, "#778aa2"],
      gold: [0, "#df7113"],
      platinum: [0, "#3f4682"],
      diamond: [0, "#79aebd"]
    };
    const crownNo = {
      bronze: [0, "#b75935"],
      silver: [0, "#778aa2"],
      gold: [0, "#df7113"],
      platinum: [0, "#3f4682"],
      diamond: [0, "#79aebd"]
    };

    // Clear out old elements
    // Uses static collection instead of live one from getElementsByClassName
    document
      .querySelectorAll(".tsitu-tem-catches")
      .forEach(el => el.remove());

    // Render crown image and catch number next to mouse name
    const rawStore = localStorage.getItem("mh-catch-stats");
    if (rawStore) {
      const stored = JSON.parse(rawStore);

      // Clean up ' Mouse' affixes
      const newStored = {};
      const storedKeys = Object.keys(stored);
      storedKeys.forEach(key => {
        if (key.indexOf(" Mouse") >= 0) {
          // const newKey = key.split(" Mouse")[0];
          const newKey = key.replace(/\ mouse$/i, ""); // Doesn't capture Dread Pirate Mousert
          newStored[newKey] = stored[key];
        } else {
          newStored[key] = stored[key];
        }
      });

      const rows = document.querySelectorAll(
        ".campPage-trap-trapEffectiveness-mouse-name"
      );

      if (rows) {
        // Initial auto container height
        document
          .querySelectorAll(".campPage-trap-trapEffectiveness-mouse")
          .forEach(el => {
            el.style.height = "auto";
          });

        rows.forEach(row => {
          const name = row.textContent;
          // const name = "Super Mega Mecha Ultra RoboGold"; // Testing
          // row.textContent = name; // Testing

          const catches = newStored[name];

          const span = document.createElement("span");
          span.className = "tsitu-tem-catches";

          const outer = document.createElement("span");
          outer.className = "mousebox";
          outer.style.width = "auto";
          outer.style.height = "auto";
          outer.style.margin = "0px";
          outer.style.paddingRight = "8px";
          outer.style.float = "none";

          const crownImg = document.createElement("img");
          crownImg.style.width = "20px";
          crownImg.style.height = "20px";
          crownImg.style.top = "5px";
          crownImg.style.right = "-5px";
          crownImg.style.position = "relative";

          function getCrownSrc(type) {
            return `https://www.mousehuntgame.com/images/ui/crowns/crown_${type}.png`;
          }

          if (!catches || catches < 10) {
            crownImg.src = getCrownSrc("none");
            crownNo.bronze[0] += 1;
            crownNo.silver[0] += 1;
            crownNo.gold[0] += 1;
            crownNo.platinum[0] += 1;
            crownNo.diamond[0] += 1;
          } else if (catches >= 10 && catches < 100) {
            crownImg.src = getCrownSrc("bronze");
            crownYes.bronze[0] += 1;
            crownNo.silver[0] += 1;
            crownNo.gold[0] += 1;
            crownNo.platinum[0] += 1;
            crownNo.diamond[0] += 1;
          } else if (catches >= 100 && catches < 500) {
            crownImg.src = getCrownSrc("silver");
            crownYes.bronze[0] += 1;
            crownYes.silver[0] += 1;
            crownNo.gold[0] += 1;
            crownNo.platinum[0] += 1;
            crownNo.diamond[0] += 1;
          } else if (catches >= 500 && catches < 1000) {
            crownImg.src = getCrownSrc("gold");
            crownYes.bronze[0] += 1;
            crownYes.silver[0] += 1;
            crownYes.gold[0] += 1;
            crownNo.platinum[0] += 1;
            crownNo.diamond[0] += 1;
          } else if (catches >= 1000 && catches < 2500) {
            crownImg.src = getCrownSrc("platinum");
            crownYes.bronze[0] += 1;
            crownYes.silver[0] += 1;
            crownYes.gold[0] += 1;
            crownYes.platinum[0] += 1;
            crownNo.diamond[0] += 1;
          } else if (catches >= 2500) {
            crownImg.src = getCrownSrc("diamond");
            crownYes.bronze[0] += 1;
            crownYes.silver[0] += 1;
            crownYes.gold[0] += 1;
            crownYes.platinum[0] += 1;
            crownYes.diamond[0] += 1;
          }

          mouseCount += 1;
          outer.innerText = catches || 0;
          outer.appendChild(crownImg);
          span.appendChild(document.createElement("br"));
          span.appendChild(outer);
          row.appendChild(span);
        });

        // Adjust heights after the fact
        document
          .querySelectorAll(".campPage-trap-trapEffectiveness-mouse")
          .forEach(el => {
            el.style.height = `${el.offsetHeight + 2}px`;

            // Height based on mouse name
            // const name = el.querySelector(
            // ".campPage-trap-trapEffectiveness-mouse-name"
            // );
            // el.style.height = `${name.clientHeight + 15}px`;

            // Width based on zoom level
            // name.style.width = "auto";
            // name.style.maxWidth = `${el.clientWidth - 48}px`;
            // TODO: If we can get the border-width at every zoom level then we can just do: width - 6px (padding) - borderWidth
            // name.style.maxWidth = "102px";
            // name.style.width = "102px";
          });
      }
    }

    const bottomDiv = document.createElement("div");
    bottomDiv.className = "tsitu-tem-catches";
    bottomDiv.style.textAlign = "center";

    // Render 2 crown count rows of icons
    const userStatWrapper = document.createElement("table");
    userStatWrapper.className = "mousehuntHud-userStat tsitu-tem-catches";
    userStatWrapper.style.borderCollapse = "separate";
    userStatWrapper.style.borderSpacing = "0 1em";
    userStatWrapper.style.marginRight = "3.8em";

    function generateIcons(obj, row, isYes) {
      const descTd = document.createElement("td");
      descTd.style.paddingRight = "6px";
      descTd.style.fontSize = "13px";
      if (isYes) {
        descTd.innerText = "✔️";
      } else {
        descTd.innerText = "❌";
      }
      row.append(descTd);

      Object.keys(obj).forEach(key => {
        const el = obj[key];
        const td = document.createElement("td");
        td.style.paddingRight = "6px";

        const span = document.createElement("div");
        span.className = "notification active";
        span.style.position = "initial";
        span.style.background = el[1];
        span.style.width = "14px";
        span.style.padding = "3px";
        span.style.fontSize = "11px";
        span.style.fontWeight = "bold";
        span.innerText = el[0];

        if (isYes) {
          span.title = `${el[0]}/${mouseCount} ${key} crowns earned in this TEM configuration`;
        } else {
          span.title = `${el[0]}/${mouseCount} ${key} crowns missing in this TEM configuration`;
        }

        span.onclick = function (event) {
          event.stopPropagation(); // Prevent bubbling up
          alert(span.title);
          return false;
        };

        td.appendChild(span);
        row.append(td);
      });
    }

    const yesRow = document.createElement("tr");
    const noRow = document.createElement("tr");
    generateIcons(crownYes, yesRow, true);
    generateIcons(crownNo, noRow, false);
    userStatWrapper.appendChild(yesRow);
    userStatWrapper.appendChild(noRow);
    bottomDiv.appendChild(userStatWrapper);
    bottomDiv.appendChild(document.createElement("br"));
    bottomDiv.appendChild(document.createElement("br"));

    // Render 'Refresh Data' button
    const refreshButton = document.createElement("button");
    refreshButton.id = "tem-catches-refresh-button";
    refreshButton.innerText = "Refresh Crown Data";
    refreshButton.addEventListener("click", function () {
      postReq("sn=Hitgrab&hg_is_ajax=1").then(res => {
        parseData(res);
      });
    });
    bottomDiv.appendChild(refreshButton);

    const timeRaw = localStorage.getItem("mh-catch-stats-timestamp");
    if (timeRaw) {
      const timeSpan = document.createElement("span");
      timeSpan.style.fontSize = "14px";
      timeSpan.innerText = `(Last refreshed: ${new Date(
        parseInt(timeRaw)
      ).toLocaleString()})`;
      bottomDiv.appendChild(document.createElement("br"));
      bottomDiv.appendChild(document.createElement("br"));
      bottomDiv.appendChild(timeSpan);
    }

    const container = document.getElementsByClassName(
      "campPage-trap-trapEffectiveness-content"
    )[0];
    if (container) container.appendChild(bottomDiv);

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

  /**
   * Parse badge endpoint response and write to localStorage
   * @param {string} res
   */
  function parseData(res) {
    let response = null;
    try {
      if (res) {
        response = JSON.parse(res.responseText);
        const catchData = {};
        const resData = response.hunting_stats;
        if (resData) {
          resData.forEach(mouse => {
            catchData[mouse.name] = mouse.num_catches;
          });

          localStorage.setItem("mh-catch-stats", JSON.stringify(catchData));
          localStorage.setItem("mh-catch-stats-timestamp", Date.now());

          render();
        }
      }
    } catch (error) {
      console.log("Error while processing POST response");
      console.error(error.stack);
    }
  }
})();