MouseHunt - Location Catch Stats

Shows caught and uncaught mouse breeds for every location

// ==UserScript==
// @name         MouseHunt - Location Catch Stats
// @author       Tran Situ (tsitu)
// @namespace    https://gf.qytechs.cn/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);
    }
  }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址