MouseHunt - Location Catch Stats

Shows caught and uncaught mouse breeds for every location

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         MouseHunt - Location Catch Stats
// @author       Tran Situ (tsitu)
// @namespace    https://greasyfork.org/en/users/232363-tsitu
// @version      1.5
// @description  Shows caught and uncaught mouse breeds for every location
// @match        http://www.mousehuntgame.com/*
// @match        https://www.mousehuntgame.com/*
// ==/UserScript==

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

  /**
   * Logic to generate the popup
   */
  function render() {
    const existing = document.querySelector("#tsitu-location-stats");
    if (existing) existing.remove();

    let selfTotal = 0;
    let sumTotal = 0;
    const detailDiv = document.createElement("div");
    detailDiv.style.textAlign = "left";

    // Get cached overall data
    const overallRaw = localStorage.getItem("tsitu-location-overall-stats");
    if (overallRaw) {
      const overall = JSON.parse(overallRaw);

      // Cached detailed stats
      const statsObj =
        JSON.parse(localStorage.getItem("tsitu-location-detailed-stats")) || {};

      // Previously opened details
      const prevOpen =
        JSON.parse(localStorage.getItem("tsitu-location-opened"))["open"] || {};

      const completeArr = [];
      const incompleteArr = [];
      for (let loc of Object.keys(overall)) {
        const data = overall[loc];
        if (data.caught === data.total) {
          completeArr.push(loc);
        } else {
          incompleteArr.push(loc);
        }
      }

      // Create 'Incomplete' <details> element
      const incompleteDetail = document.createElement("details");
      incompleteDetail.style.fontSize = "16px";
      incompleteDetail.open = prevOpen.indexOf("Incomplete") >= 0;

      const incompleteSummary = document.createElement("summary");
      incompleteSummary.innerText = "Incomplete";
      incompleteDetail.appendChild(incompleteSummary);

      // Create 'Expand/Collapse All' checkbox for 'Incomplete'
      const incompleteLabel = document.createElement("label");
      incompleteLabel.className = "tsitu-incomplete-box-label";
      incompleteLabel.htmlFor = "tsitu-incomplete-box";
      incompleteLabel.innerText = "Expand/Collapse All";
      incompleteLabel.style.fontSize = "12px";

      const incompleteBox = document.createElement("input");
      incompleteBox.id = "tsitu-incomplete-box";
      incompleteBox.name = "tsitu-incomplete-box";
      incompleteBox.type = "checkbox";
      incompleteBox.checked =
        localStorage.getItem("tsitu-location-expand-incomplete") == "1";

      incompleteBox.addEventListener("click", function () {
        if (document.querySelector("#tsitu-incomplete-box").checked) {
          document.querySelectorAll(".tsitu-incomplete-detail").forEach(el => {
            el.open = true;
          });
          localStorage.setItem("tsitu-location-expand-incomplete", 1);
        } else {
          document.querySelectorAll(".tsitu-incomplete-detail").forEach(el => {
            el.open = false;
          });
          localStorage.setItem("tsitu-location-expand-incomplete", 0);
        }
        cacheOpenedDetails();
      });

      // Insert 'Incomplete' checkbox
      incompleteSummary.insertAdjacentElement(
        "afterend",
        document.createElement("br")
      );
      incompleteSummary.insertAdjacentElement(
        "afterend",
        document.createElement("br")
      );
      incompleteSummary.insertAdjacentElement("afterend", incompleteLabel);
      incompleteSummary.insertAdjacentElement("afterend", incompleteBox);
      incompleteSummary.insertAdjacentElement(
        "afterend",
        document.createElement("br")
      );

      // Generate incomplete location details
      for (let loc of incompleteArr.sort()) {
        generateDetail(loc, "incomplete");
      }

      // Create 'Complete' <details> element
      const completeDetail = document.createElement("details");
      completeDetail.style.fontSize = "16px";
      completeDetail.open = prevOpen.indexOf("Complete") >= 0;

      const completeSummary = document.createElement("summary");
      completeSummary.innerText = "Complete";
      completeDetail.appendChild(completeSummary);

      // Create 'Expand/Collapse All' checkbox for 'Complete'
      const completeLabel = document.createElement("label");
      completeLabel.className = "tsitu-complete-box-label";
      completeLabel.htmlFor = "tsitu-complete-box";
      completeLabel.innerText = "Expand/Collapse All";
      completeLabel.style.fontSize = "12px";

      const completeBox = document.createElement("input");
      completeBox.id = "tsitu-complete-box";
      completeBox.type = "checkbox";
      completeBox.checked =
        localStorage.getItem("tsitu-location-expand-complete") == "1";

      completeBox.addEventListener("click", function () {
        if (document.querySelector("#tsitu-complete-box").checked) {
          document.querySelectorAll(".tsitu-complete-detail").forEach(el => {
            el.open = true;
          });
          localStorage.setItem("tsitu-location-expand-complete", 1);
        } else {
          document.querySelectorAll(".tsitu-complete-detail").forEach(el => {
            el.open = false;
          });
          localStorage.setItem("tsitu-location-expand-complete", 0);
        }
        cacheOpenedDetails();
      });

      // Insert 'Complete' checkbox
      completeSummary.insertAdjacentElement(
        "afterend",
        document.createElement("br")
      );
      completeSummary.insertAdjacentElement(
        "afterend",
        document.createElement("br")
      );
      completeSummary.insertAdjacentElement("afterend", completeLabel);
      completeSummary.insertAdjacentElement("afterend", completeBox);
      completeSummary.insertAdjacentElement(
        "afterend",
        document.createElement("br")
      );

      // Generate completed location details
      for (let loc of completeArr.sort()) {
        generateDetail(loc, "complete");
      }

      /**
       * Generate inner <details> elements for main "Incomplete" and "Complete"
       * @param {string} loc Location name
       * @param {string} type "incomplete" or "complete"
       */
      function generateDetail(loc, type) {
        const data = overall[loc];
        selfTotal += data.caught;
        sumTotal += data.total;

        const detail = document.createElement("details");
        detail.className =
          type === "complete"
            ? "tsitu-complete-detail"
            : "tsitu-incomplete-detail";
        detail.style.fontSize = "14px";
        detail.style.marginBottom = "5px";

        // Check if previously opened
        if (prevOpen.indexOf(loc) >= 0) {
          detail.open = true;
        }

        const summary = document.createElement("summary");
        summary.innerText = `${loc} (${data.caught} of ${data.total})`;

        const refreshButton = document.createElement("button");
        refreshButton.innerText = "Update";
        refreshButton.addEventListener("click", function () {
          requestLocation(data.type, loc);
        });

        const updateSpan = document.createElement("span");
        updateSpan.style.fontStyle = "italic";
        updateSpan.style.fontSize = "12px";
        updateSpan.innerText = `Updated: ${
          statsObj[loc]
            ? new Date(parseInt(statsObj[loc]["date"])).toLocaleString()
            : "N/A"
        }`;

        // Build body text
        const innerSpan = document.createElement("span");
        let statsText = "\n\n";
        if (statsObj[loc]) {
          let missText = "Not yet caught here: \n";
          const missArr = statsObj[loc]["missing"];

          if (missArr.length === 0) {
            missText =
              "All breeds available in this location\nhave been caught here.\n";
          } else {
            missArr.forEach(el => {
              missText += `- ${el}\n`;
            });
          }

          let caughtText = "";
          const hideCaught =
            localStorage.getItem("tsitu-location-hideCaught") == "1";

          if (!hideCaught) {
            const caughtArr = statsObj[loc]["caught"];
            if (caughtArr.length > 0) {
              caughtText = "Breeds caught here: \n";
              caughtArr.forEach(el => {
                caughtText += `- ${el[0]} (${el[1]})\n`;
              });
              caughtText += "\n";
            }
          }

          statsText += `${missText}\n${caughtText}`;
        }

        innerSpan.innerText += statsText;
        innerSpan.appendChild(refreshButton);
        innerSpan.appendChild(document.createElement("br"));
        innerSpan.appendChild(document.createElement("br"));

        detail.appendChild(summary);
        detail.appendChild(document.createElement("br"));
        detail.appendChild(updateSpan);
        detail.appendChild(innerSpan);

        // Append to the appropriate main <details>
        type === "complete"
          ? completeDetail.appendChild(detail)
          : incompleteDetail.appendChild(detail);
      }

      // Append elements to detailDiv
      detailDiv.appendChild(incompleteDetail);
      detailDiv.appendChild(document.createElement("br"));
      detailDiv.appendChild(completeDetail);
      detailDiv.appendChild(document.createElement("br"));
    }

    // Create and style the main <div>
    const mainDiv = document.createElement("div");
    mainDiv.id = "tsitu-location-stats";
    mainDiv.style.backgroundColor = "#F5F5F5";
    mainDiv.style.position = "absolute";
    mainDiv.style.zIndex = "9999";
    mainDiv.style.left = "35%";
    mainDiv.style.top = "25px";
    mainDiv.style.border = "solid 3px #696969";
    mainDiv.style.borderRadius = "20px";
    mainDiv.style.padding = "10px";
    mainDiv.style.textAlign = "center";

    // Top div styling (close button, title, drag instructions)
    const topDiv = document.createElement("div");

    const titleSpan = document.createElement("span");
    titleSpan.style.fontWeight = "bold";
    titleSpan.style.fontSize = "18px";
    titleSpan.style.textDecoration = "underline";
    titleSpan.style.paddingLeft = "20px";
    titleSpan.innerText = "Location Catch Stats";
    const dragSpan = document.createElement("span");
    dragSpan.innerText = "(Drag title to reposition this popup)";

    const closeButton = document.createElement("button");
    closeButton.style.float = "right";
    closeButton.style.fontSize = "8px";
    closeButton.innerText = "x";
    closeButton.addEventListener("click", function () {
      document.body.removeChild(mainDiv);
    });

    topDiv.appendChild(closeButton);
    topDiv.appendChild(titleSpan);
    topDiv.appendChild(document.createElement("br"));
    topDiv.appendChild(dragSpan);

    const overallUpdateSpan = document.createElement("span");
    const cachedDate = localStorage.getItem("tsitu-location-overall-updated");
    overallUpdateSpan.innerText = `Last updated (overall data): ${
      cachedDate ? new Date(parseInt(cachedDate)).toLocaleString() : "N/A"
    }`;

    const updateOverallButton = document.createElement("button");
    updateOverallButton.innerText = "Update overall completion data";
    updateOverallButton.addEventListener("click", function () {
      cacheOpenedDetails();
      updateOverallData();
    });

    // Checkbox to hide text for caught mice
    const hideCaughtLabel = document.createElement("label");
    hideCaughtLabel.className = "tsitu-hideCaught-box-label";
    hideCaughtLabel.htmlFor = "tsitu-hideCaught-box";
    hideCaughtLabel.innerText = "Hide caught mice";
    hideCaughtLabel.style.fontSize = "12px";

    const hideCaughtBox = document.createElement("input");
    hideCaughtBox.id = "tsitu-hideCaught-box";
    hideCaughtBox.name = "tsitu-hideCaught-box";
    hideCaughtBox.type = "checkbox";
    hideCaughtBox.checked =
      localStorage.getItem("tsitu-location-hideCaught") == "1";

    hideCaughtBox.addEventListener("click", function () {
      if (document.querySelector("#tsitu-hideCaught-box").checked) {
        localStorage.setItem("tsitu-location-hideCaught", 1);
      } else {
        localStorage.setItem("tsitu-location-hideCaught", 0);
      }
      cacheOpenedDetails();
      render();
    });

    // TODO: sumTotal only includes unlocked locations for each user
    const totalSpan = document.createElement("span");
    totalSpan.style.fontSize = "16px";
    totalSpan.innerText = `Total: ${selfTotal} / ${sumTotal}`;

    // Append everything to mainDiv and document
    mainDiv.appendChild(topDiv);
    mainDiv.appendChild(document.createElement("br"));
    mainDiv.appendChild(overallUpdateSpan);
    mainDiv.appendChild(document.createElement("br"));
    mainDiv.appendChild(updateOverallButton);
    mainDiv.appendChild(document.createElement("br"));
    mainDiv.appendChild(document.createElement("br"));
    if (sumTotal > 0) {
      mainDiv.appendChild(totalSpan);
      mainDiv.appendChild(document.createElement("br"));
      mainDiv.appendChild(document.createElement("br"));
    }
    mainDiv.appendChild(hideCaughtBox);
    mainDiv.appendChild(hideCaughtLabel);
    mainDiv.appendChild(document.createElement("br"));
    mainDiv.appendChild(document.createElement("br"));
    mainDiv.appendChild(detailDiv);
    document.body.appendChild(mainDiv);
    dragElement(mainDiv, titleSpan);

    // Reposition popup based on previous dragged location
    const posTop = localStorage.getItem("location-stat-pos-top");
    const posLeft = localStorage.getItem("location-stat-pos-left");
    if (posTop && posLeft) {
      const intTop = parseInt(posTop);
      if (intTop > 0 && intTop < window.innerHeight - 150) {
        mainDiv.style.top = posTop;
      }
      const intLeft = parseInt(posLeft);
      if (intLeft > 0 && intLeft < window.innerWidth - 150) {
        mainDiv.style.left = posLeft;
      }
    }

    // Reposition current location <details> to the top
    let currentDetail;
    let currentLoc = user.environment_name || "N/A";
    if (currentLoc === "Twisted Garden") {
      currentLoc = "Living Garden";
    } else if (currentLoc === "Cursed City") {
      currentLoc = "Lost City";
    } else if (currentLoc === "Sand Crypts") {
      currentLoc = "Sand Dunes";
    }

    document.querySelectorAll("#tsitu-location-stats summary").forEach(el => {
      if (el.textContent.indexOf(currentLoc) >= 0) {
        currentDetail = el.parentElement;
        el.parentElement.remove();
      }
    });

    if (currentDetail) {
      currentDetail.className = "tsitu-current-detail";
      currentDetail.style.textAlign = "left";
      currentDetail.open = true;
      detailDiv.insertAdjacentElement("beforebegin", currentDetail);
      detailDiv.insertAdjacentElement(
        "beforebegin",
        document.createElement("br")
      );
    }
  }

  /**
   * Cache location names for <details> with open = true
   */
  function cacheOpenedDetails() {
    const obj = {};
    const arr = [];

    document
      .querySelectorAll(
        "#tsitu-location-stats details:not(.tsitu-current-detail)"
      )
      .forEach(el => {
        if (el.open) {
          const summary = el.querySelector("summary").textContent;
          if (summary !== "Complete" && summary !== "Incomplete") {
            const loc = summary.split(" (")[0];
            arr.push(loc);
          } else {
            arr.push(summary);
          }
        }
      });

    obj["open"] = arr;
    localStorage.setItem("tsitu-location-opened", JSON.stringify(obj));
  }

  /**
   * Fetch overall/total completion data from getstat.php
   */
  function updateOverallData() {
    const xhr = new XMLHttpRequest();
    xhr.open(
      "POST",
      `https://www.mousehuntgame.com/managers/ajax/pages/page.php?page_class=HunterProfile&page_arguments%5Btab%5D=mice&page_arguments%5Bsub_tab%5D=location&uh=${user.unique_hash}`
    );

    xhr.onload = function () {
      const response = JSON.parse(xhr.responseText);
      const locations =
        response.page.tabs.mice.subtabs[1].mouse_list.categories;
      if (locations) {
        const masterObj = {};

        locations.forEach(el => {
          const obj = {};
          obj["caught"] = el.caught;
          obj["total"] = el.total;
          obj["type"] = el.type;
          masterObj[el.name] = obj;
        });

        localStorage.setItem(
          "tsitu-location-overall-stats",
          JSON.stringify(masterObj)
        );
        localStorage.setItem("tsitu-location-overall-updated", Date.now());
        render();
      }
    };

    xhr.onerror = function () {
      console.error(xhr.statusText);
    };

    xhr.send();
  }

  /**
   * Fetch individual location data from getstat.php
   * @param {number} category Location category string
   * @param {string} locationName
   */
  function requestLocation(category, locationName) {
    const xhr = new XMLHttpRequest();
    xhr.open(
      "POST",
      `https://www.mousehuntgame.com/managers/ajax/mice/mouse_list.php?action=get_environment&category=${category}&user_id=${user.user_id}&display_mode=stats&view=ViewMouseListEnvironments&uh=${user.unique_hash}`,
      true
    );

    xhr.onload = function () {
      const response = JSON.parse(xhr.responseText);
      const stats = response.mouse_list_category.subgroups[0].mice;
      if (stats) {
        const missedArr = [];
        const caughtArr = [];

        stats.forEach(el => {
          const caught =
            typeof el.num_catches === "string"
              ? parseInt(el.num_catches.replace(/,/g, ""))
              : el.num_catches;
          if (caught === 0) {
            missedArr.push(el.name);
          } else if (caught > 0) {
            caughtArr.push([el.name, caught]);
          }
        });

        const obj = {};
        obj["missing"] = missedArr;
        obj["caught"] = caughtArr;
        obj["date"] = Date.now();

        const cacheRaw = localStorage.getItem("tsitu-location-detailed-stats");
        if (cacheRaw) {
          const cache = JSON.parse(cacheRaw);
          cache[locationName] = obj;
          localStorage.setItem(
            "tsitu-location-detailed-stats",
            JSON.stringify(cache)
          );
        } else {
          const cache = {};
          cache[locationName] = obj;
          localStorage.setItem(
            "tsitu-location-detailed-stats",
            JSON.stringify(cache)
          );
        }

        cacheOpenedDetails();
        render();
      }
    };

    xhr.onerror = function () {
      console.error(xhr.statusText);
    };

    xhr.send();
  }

  /**
   * Element dragging functionality
   * @param {HTMLElement} el Element that actually moves
   * @param {HTMLElement} target Element to drag in order to move 'el'
   */
  function dragElement(el, target) {
    var pos1 = 0,
      pos2 = 0,
      pos3 = 0,
      pos4 = 0;

    if (document.getElementById(target.id + "header")) {
      document.getElementById(target.id + "header").onmousedown = dragMouseDown;
    } else {
      target.onmousedown = dragMouseDown;
    }

    function dragMouseDown(e) {
      e = e || window.event;
      pos3 = e.clientX;
      pos4 = e.clientY;
      document.onmouseup = closeDragElement;
      document.onmousemove = elementDrag;
    }

    function elementDrag(e) {
      e = e || window.event;
      pos1 = pos3 - e.clientX;
      pos2 = pos4 - e.clientY;
      pos3 = e.clientX;
      pos4 = e.clientY;
      el.style.top = el.offsetTop - pos2 + "px";
      el.style.left = el.offsetLeft - pos1 + "px";
    }

    function closeDragElement() {
      document.onmouseup = null;
      document.onmousemove = null;
      localStorage.setItem("location-stat-pos-top", el.style.top);
      localStorage.setItem("location-stat-pos-left", el.style.left);
    }
  }
})();