MouseHunt - Location Catch Stats

Shows caught and uncaught mouse breeds for every location

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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);
    }
  }
})();