MouseHunt - Mapping Helper

Invite players and send SB+ directly from the map interface

目前為 2019-12-03 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         MouseHunt - Mapping Helper
// @author       Tran Situ (tsitu)
// @namespace    https://greasyfork.org/en/users/232363-tsitu
// @version      2.0
// @description  Invite players and send SB+ directly from the map interface
// @match        http://www.mousehuntgame.com/*
// @match        https://www.mousehuntgame.com/*
// ==/UserScript==

(function() {
  // Endpoint listener - caches maps (which come in one at a time)
  const originalOpen = XMLHttpRequest.prototype.open;
  XMLHttpRequest.prototype.open = function() {
    this.addEventListener("load", function() {
      if (
        this.responseURL ===
        "https://www.mousehuntgame.com/managers/ajax/users/treasuremap.php"
      ) {
        try {
          const map = JSON.parse(this.responseText).treasure_map;
          if (map) {
            const obj = {};
            const condensed = {};
            condensed.hunters = map.hunters;
            condensed.is_complete = map.is_complete;
            condensed.is_owner = map.is_owner;
            condensed.is_scavenger_hunt = map.is_scavenger_hunt;
            condensed.is_wanted_poster = map.is_wanted_poster;
            condensed.map_class = map.map_class;
            condensed.map_id = map.map_id;
            condensed.timestamp = Date.now();
            obj[map.name] = condensed;

            const mapCacheRaw = localStorage.getItem("tsitu-mapping-cache");
            if (mapCacheRaw) {
              const mapCache = JSON.parse(mapCacheRaw);
              mapCache[map.name] = condensed;
              localStorage.setItem(
                "tsitu-mapping-cache",
                JSON.stringify(mapCache)
              );
            } else {
              localStorage.setItem("tsitu-mapping-cache", JSON.stringify(obj));
            }

            render();
          }
        } catch (error) {
          console.log("Server response doesn't contain a valid treasure map");
          console.error(error.stack);
        }
      }
    });
    originalOpen.apply(this, arguments);
  };

  // Renders custom UI elements onto the DOM
  function render() {
    // Clear out existing custom elements
    // Uses static collection instead of live one from getElementsByClassName
    document.querySelectorAll(".tsitu-mapping").forEach(el => el.remove());
    // document.querySelectorAll(".tsitu-queso-mapper").forEach(el => el.remove());

    // Parent element that gets inserted at the end
    const masterEl = document.createElement("fieldset");
    masterEl.className = "tsitu-mapping";
    masterEl.style.width = "50%";
    masterEl.style.marginLeft = "15px";
    masterEl.style.padding = "5px";
    masterEl.style.border = "1px";
    masterEl.style.borderStyle = "dotted";
    const masterElLegend = document.createElement("legend");
    masterElLegend.innerText = "Mapping Helper v2.0 by tsitu";
    masterEl.appendChild(masterElLegend);

    /**
     * Refresh button
     * Iterate thru QRH.maps array for element matching current map and set its hash to empty string
     * This forces a hard refresh via hasCachedMap, which is called in show/showMap
     */
    const refreshSpan = document.createElement("span");
    refreshSpan.className = "tsitu-mapping tsitu-refresh-span";
    const refreshTextSpan = document.createElement("span");
    refreshTextSpan.innerText = "Refresh";

    const refreshButton = document.createElement("button");
    refreshButton.className = "mousehuntActionButton tiny tsitu-mapping";
    refreshButton.style.cursor = "pointer";
    refreshButton.style.fontSize = "9px";
    refreshButton.style.padding = "2px";
    refreshButton.style.margin = "0px 5px 5px 10px";
    refreshButton.style.textShadow = "none";
    refreshButton.style.display = "inline-block";
    refreshButton.appendChild(refreshTextSpan);

    refreshButton.addEventListener("click", function() {
      // Clear cache (is it possible to only do so for just a single map?)
      hg.views.TreasureMapManagerView.clearMapCache();

      // Parse map ID from 'Preview' button (should be robust enough)
      let mapId = -1;
      const previewButton = document.querySelector(
        ".treasureMapView-mapMenu-group-actions .mousehuntActionButton.tiny.lightBlue"
      );
      if (previewButton) {
        mapId = previewButton.onclick
          .toString()
          .split("RewardsDialog(")[1]
          .split(");")[0];
      }

      // Close dialog and re-open with either current map or overview
      document.getElementById("jsDialogClose").click();
      mapId === -1
        ? hg.views.TreasureMapManagerView.show()
        : hg.views.TreasureMapManagerView.show(mapId);
    });

    refreshSpan.appendChild(refreshButton);
    masterEl.appendChild(refreshSpan);

    // Utility function that opens supply transfer page and auto-selects SB+
    function transferSB(snuid) {
      const newWindow = window.open(
        `https://www.mousehuntgame.com/supplytransfer.php?fid=${snuid}`
      );
      newWindow.addEventListener("load", function() {
        if (newWindow.supplyTransfer1) {
          newWindow.supplyTransfer1.setSelectedItemType("super_brie_cheese");
          newWindow.supplyTransfer1.renderTabMenu();
          newWindow.supplyTransfer1.render();
        }
      });
      return false;
    }

    // Features that require cache checking
    const cacheRaw = localStorage.getItem("tsitu-mapping-cache");
    if (cacheRaw) {
      const cache = JSON.parse(cacheRaw);
      const checkMap = document.querySelector(
        ".treasureMapManagerView-task.active"
      );
      const checkPreview = document.querySelector(
        ".treasureMapView-previewBar-content"
      );

      let mapName;
      if (checkMap) {
        mapName = checkMap.querySelector(".treasureMapManagerView-task-name")
          .textContent;
      }
      if (checkPreview) {
        mapName = checkPreview.textContent
          .split("'s ")[1]
          .split(".Back to Invites")[0];
      }

      if (cache[mapName] !== undefined) {
        // Must specify <a> because favorite button <div> also matches the selector
        const mapIdEl = document.querySelector("a[data-task-id].active");
        if (mapIdEl) {
          // Abstract equality comparison because map ID can be number or string
          const mapId = mapIdEl.getAttribute("data-task-id");
          if (mapId == cache[mapName].map_id) {
            // "Last refreshed" timestamp
            if (cache[mapName].timestamp) {
              const timeSpan = document.createElement("span");
              timeSpan.innerText = `(This map was last refreshed on: ${new Date(
                parseInt(cache[mapName].timestamp)
              ).toLocaleString()})`;
              refreshSpan.appendChild(timeSpan);
            }

            // Invite via Hunter ID (only for map captains)
            if (cache[mapName].is_owner) {
              const inputLabel = document.createElement("label");
              inputLabel.innerText = "Hunter ID: ";
              inputLabel.htmlFor = "tsitu-mapping-id-input";

              const inputField = document.createElement("input");
              inputField.setAttribute("type", "number");
              inputField.setAttribute("name", "tsitu-mapping-id-input");
              inputField.setAttribute("data-lpignore", "true"); // Get rid of LastPass Autofill
              inputField.setAttribute("min", 1);
              inputField.setAttribute("max", 9999999);
              inputField.setAttribute("placeholder", "e.g. 1234567");
              inputField.setAttribute("required", true);
              inputField.addEventListener("keyup", function(e) {
                if (e.keyCode === 13) {
                  inviteButton.click(); // 'Enter' pressed
                }
              });

              const overrideStyle =
                "input[name='tsitu-mapping-id-input'] { -webkit-appearance:textfield; -moz-appearance:textfield; appearance:textfield; } input[name='tsitu-mapping-id-input']::-webkit-outer-spin-button, input[name='tsitu-mapping-id-input']::-webkit-inner-spin-button { display:none; -webkit-appearance:none; margin:0; }";
              let stylePresent = false;
              document.querySelectorAll("style").forEach(style => {
                if (style.textContent === overrideStyle) {
                  stylePresent = true;
                }
              });
              if (!stylePresent) {
                const spinOverride = document.createElement("style");
                spinOverride.innerHTML = overrideStyle;
                document.body.appendChild(spinOverride);
              }

              const inviteButton = document.createElement("button");
              inviteButton.style.marginLeft = "5px";
              inviteButton.innerText = "Invite";
              inviteButton.addEventListener("click", function() {
                const rawText = inputField.value;
                if (rawText.length > 0) {
                  const hunterId = parseInt(rawText);
                  if (typeof hunterId === "number" && !isNaN(hunterId)) {
                    if (hunterId > 0 && hunterId < 9999999) {
                      postReq(
                        "https://www.mousehuntgame.com/managers/ajax/pages/friends.php",
                        `sn=Hitgrab&hg_is_ajax=1&action=community_search_by_id&user_id=${hunterId}&uh=${user.unique_hash}`
                      ).then(res => {
                        let response = null;
                        try {
                          if (res) {
                            response = JSON.parse(res.responseText);
                            const data = response.friend;
                            if (data.has_invitable_map) {
                              if (
                                confirm(
                                  `Are you sure you'd like to invite this hunter?\n\nName: ${data.name}\nTitle: ${data.title_name} (${data.title_percent}%)\nLocation: ${data.environment_name}\nLast Active: ${data.last_active_formatted} ago`
                                )
                              ) {
                                postReq(
                                  "https://www.mousehuntgame.com/managers/ajax/users/treasuremap.php",
                                  `sn=Hitgrab&hg_is_ajax=1&action=send_invites&map_id=${mapId}&snuids%5B%5D=${data.snuid}&uh=${user.unique_hash}`
                                ).then(res2 => {
                                  let inviteRes = null;
                                  try {
                                    if (res2) {
                                      inviteRes = JSON.parse(res2.responseText);
                                      if (inviteRes.success === 1) {
                                        refreshButton.click();
                                      } else {
                                        alert(
                                          "Map invite unsuccessful - may be because map is full"
                                        );
                                      }
                                    }
                                  } catch (error2) {
                                    alert("Error while inviting hunter to map");
                                    console.error(error2.stack);
                                  }
                                });
                              }
                            } else {
                              if (data.name) {
                                alert(
                                  `${data.name} cannot to be invited to a map at this time`
                                );
                              } else {
                                alert("Invalid hunter information");
                              }
                            }
                          }
                        } catch (error) {
                          alert("Error while requesting hunter information");
                          console.error(error.stack);
                        }
                      });
                    }
                  }
                }
              });

              const span = document.createElement("span");
              span.className = "tsitu-mapping";
              span.style.display = "inline-block";
              span.style.marginBottom = "10px";
              span.style.marginLeft = "10px";
              span.appendChild(inputLabel);
              span.appendChild(inputField);
              span.appendChild(inviteButton);

              masterEl.insertAdjacentElement(
                "afterbegin",
                document.createElement("br")
              );
              masterEl.insertAdjacentElement("afterbegin", span);
            }
          }
        }

        const imgIDMap = {};
        const idNameMap = {};
        cache[mapName].hunters.forEach(el => {
          if (el.profile_pic) imgIDMap[el.profile_pic] = el.sn_user_id;
          idNameMap[el.sn_user_id] = el.name;
        });

        // Utility function for image hover behavior
        function imgHover(img) {
          let imgURL;
          if (img.src) {
            imgURL = img.src;
          } else if (img.style.backgroundImage) {
            imgURL = img.style.backgroundImage.split('url("')[1].split('")')[0];
          }

          const snuid = imgIDMap[imgURL];
          if (snuid) {
            const name = idNameMap[snuid];
            if (name) {
              img.title = `Send SB+ to ${name}`;
            }

            img.href = "#";
            img.style.cursor = "pointer";
            img.onclick = function() {
              transferSB(snuid);
            };
            img.onmouseenter = function() {
              img.style.border = "dashed 1px green";
            };
            img.onmouseleave = function() {
              img.style.border = "";
            };
          }
        }

        // Hunter container images
        document
          .querySelectorAll(
            ".treasureMapView-hunter:not(.empty) .treasureMapView-hunter-image"
          )
          .forEach(img => {
            imgHover(img);
          });

        // Corkboard message images
        document
          .querySelectorAll("[data-message-id] .messageBoardView-message-image")
          .forEach(img => {
            imgHover(img);
          });

        // "x found these mice" images
        document
          .querySelectorAll(".treasureMapView-block-content-heading-image")
          .forEach(img => {
            imgHover(img);
          });
      }
    }

    // Final render
    document
      .querySelector(".treasureMapView-mapMenu")
      .insertAdjacentElement("afterend", masterEl);
  }

  // POST to specified endpoint URL with desired form data
  function postReq(url, form) {
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open("POST", url, 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);
    });
  }

  // MutationObserver logic for map UI
  // Observers are attached to a *specific* element (will DC if removed from DOM)
  const observerTarget = document.getElementById("overlayPopup");
  if (observerTarget) {
    MutationObserver =
      window.MutationObserver ||
      window.WebKitMutationObserver ||
      window.MozMutationObserver;

    const observer = new MutationObserver(function() {
      // Callback

      // Render if treasure map popup is available
      const mapTab = observerTarget.querySelector(
        ".treasureMapManagerView-header-navigation-item.tasks.active"
      );
      const groupLen = document.querySelectorAll(
        ".treasureMapView-goals-groups"
      ).length;

      // Prevent conflict with 'Bulk Map Invites'
      const inviteHeader = document.querySelector(
        ".treasureMapManagerDialogView-inviteFriend-header"
      );

      if (
        mapTab &&
        mapTab.className.indexOf("active") >= 0 &&
        groupLen > 0 &&
        !inviteHeader
      ) {
        // Disconnect and reconnect later to prevent infinite mutation loop
        observer.disconnect();

        render();

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

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

  // Queso Mapper functionality (deprecated as of v2.0 - may reinstate if in demand)
  function quesoMapperFuncDepr() {
    // ***
    // Check for valid Queso Canyon map name
    const mapNameSelector = document.querySelector(
      ".treasureMapPopup-header-title.mapName"
    );
    if (mapNameSelector) {
      const split = mapNameSelector.textContent.split("Rare ");
      const mapName = split.length === 2 ? split[1] : split[0];

      if (quesoMaps.indexOf(mapName) >= 0) {
        // Queso Mapper toggling
        const quesoToggle = document.createElement("input");
        quesoToggle.type = "checkbox";
        quesoToggle.className = "tsitu-mapping";
        quesoToggle.name = "tsitu-queso-toggle";
        quesoToggle.addEventListener("click", function() {
          localStorage.setItem("tsitu-queso-toggle", quesoToggle.checked);
          render();
        });

        const quesoToggleLabel = document.createElement("label");
        quesoToggleLabel.className = "tsitu-mapping";
        quesoToggleLabel.htmlFor = "tsitu-queso-toggle";
        quesoToggleLabel.innerText = "Toggle Queso Mapper functionality";

        const qtChecked = localStorage.getItem("tsitu-queso-toggle") || "false";
        quesoToggle.checked = qtChecked === "true";
        if (quesoToggle.checked) {
          quesoRender();
        }

        const quesoToggleDiv = document.createElement("div");
        quesoToggleDiv.className = "tsitu-queso-mapper";
        if (!quesoToggle.checked) quesoToggleDiv.style.marginBottom = "10px";
        quesoToggleDiv.appendChild(quesoToggle);
        quesoToggleDiv.appendChild(quesoToggleLabel);

        document
          .querySelector(".treasureMapPopup-hunterContainer")
          .insertAdjacentElement("afterend", quesoToggleDiv);
      }
    }
    // ***

    function quesoRender() {
      const mapMice = document.querySelectorAll(
        "div.treasureMapPopup-goals-group-goal.treasureMapPopup-searchIndex.mouse"
      );
      if (mapMice.length > 0) {
        // Generate DOM elements
        const displayDiv = document.createElement("div");
        displayDiv.className = "tsitu-queso-mapper";
        displayDiv.style.fontSize = "14px";
        displayDiv.style.marginBottom = "10px";
        displayDiv.innerText = "Preferred Location & Cheese -> ";

        const cacheSel = localStorage.getItem("tsitu-queso-mapper-sel");
        if (cacheSel) {
          const cache = JSON.parse(cacheSel);
          for (let location in quesoData) {
            const locSel = `${classBuilder(location, "loc")}`;
            if (cache[locSel] !== undefined) {
              const locationSpan = document.createElement("span");
              locationSpan.innerText = `${location}: `;
              let cheeseCount = 0;
              for (let cheese in quesoData[location]) {
                const cheeseSel = `${classBuilder(location, cheese)}`;
                if (cache[locSel].indexOf(cheeseSel) >= 0) {
                  const cheeseSpan = document.createElement("span");
                  let prependStr = "";
                  if (cheeseCount > 0) prependStr = ", ";

                  const imgSpan = document.createElement("span");
                  imgSpan.setAttribute(
                    "style",
                    `background-image:url('${quesoImg[cheese]}');width:20px;height:20px;display:inline-block;background-size:contain;background-repeat:no-repeat;position:relative;top:4px;`
                  );

                  let appendStr = "";
                  if (cheese !== "Standard" && cheese !== "SB+") {
                    appendStr += " Queso";
                  }

                  cheeseSpan.innerText = `${prependStr + cheese + appendStr}`;
                  locationSpan.append(cheeseSpan);
                  locationSpan.append(document.createTextNode("\u00A0"));
                  locationSpan.append(imgSpan);
                  cheeseCount += 1;
                }
              }
              displayDiv.appendChild(locationSpan);
            }
          }
        } else {
          displayDiv.style.marginTop = "5px";
          displayDiv.innerText = "Preferred Location & Cheese -> N/A";
        }

        const target = document.querySelector(
          ".treasureMapPopup-map-stateContainer.viewGoals"
        );
        if (target) target.insertAdjacentElement("beforebegin", displayDiv);

        mapMice.forEach(el => {
          if (el.className.indexOf("tsitu-queso-mapper-mouse") < 0) {
            function listener() {
              const span = el.querySelector("span");
              if (span) {
                const mouse = span.textContent;
                const mouseData = quesoMice[mouse];
                if (mouseData) {
                  const toCache = {};
                  for (let arr of mouseData) {
                    const locSel = classBuilder(arr[0], "loc");
                    const cheeseSel = classBuilder(arr[0], arr[1]);
                    if (toCache[locSel] === undefined) {
                      toCache[locSel] = [cheeseSel];
                    } else {
                      toCache[locSel].push(cheeseSel);
                    }
                    localStorage.setItem(
                      "tsitu-queso-mapper-sel",
                      JSON.stringify(toCache)
                    );
                    render();
                  }
                }
              }
            }

            el.addEventListener("mouseover", function() {
              listener();
            });

            el.addEventListener("click", function() {
              listener();
            });
          }
          el.classList.add("tsitu-queso-mapper-mouse");
        });
      }

      function classBuilder(location, cheese) {
        let retVal = "";

        switch (location) {
          case "Queso River":
            retVal += "river-";
            break;
          case "Prickly Plains":
            retVal += "plains-";
            break;
          case "Cantera Quarry":
            retVal += "quarry-";
            break;
          case "Cork Collecting":
            retVal += "cork-";
            break;
          case "Pressure Building":
            retVal += "pressure-";
            break;
          case "Small Eruption":
            retVal += "small-";
            break;
          case "Medium Eruption":
            retVal += "medium-";
            break;
          case "Large Eruption":
            retVal += "large-";
            break;
          case "Epic Eruption":
            retVal += "epic-";
            break;
          default:
            retVal += location;
        }

        switch (cheese) {
          case "Standard":
            retVal += "standard";
            break;
          case "SB+":
            retVal += "super";
            break;
          case "Bland":
            retVal += "bland";
            break;
          case "Mild":
            retVal += "mild";
            break;
          case "Medium":
            retVal += "medium";
            break;
          case "Hot":
            retVal += "hot";
            break;
          case "Flamin'":
            retVal += "flamin";
            break;
          case "Wildfire":
            retVal += "wildfire";
            break;
          default:
            retVal += cheese;
        }

        return retVal;
      }
    }

    // Valid Queso map variants
    const quesoMaps = [
      "Queso Canyoneer Treasure Map",
      "Queso Geyser Treasure Map",
      "Queso Canyon Grand Tour Treasure Map"
    ];

    // Queso cheese image icons
    const quesoImg = {
      "Standard":
        "https://www.mousehuntgame.com/images/items/bait/7e0daa548364166c46c0804e6cb122c6.gif?cv=243",
      "SB+":
        "https://www.mousehuntgame.com/images/items/bait/d3bb758c09c44c926736bbdaf22ee219.gif?cv=243",
      "Bland":
        "https://www.mousehuntgame.com/images/items/bait/4752dbfdce202c0d7ad60ce0bacbebae.gif?cv=243",
      "Mild":
        "https://www.mousehuntgame.com/images/items/bait/7193159aa90c85ba67cbe02d209e565f.gif?cv=243",
      "Medium":
        "https://www.mousehuntgame.com/images/items/bait/be747798c5e6a7747ba117e9c32a8a1f.gif?cv=243",
      "Hot":
        "https://www.mousehuntgame.com/images/items/bait/11d1170bc85f37d67e26b0a05902bc3f.gif?cv=243",
      "Flamin'":
        "https://www.mousehuntgame.com/images/items/bait/5a69c1ea617ba622bd1dd227afb69a68.gif?cv=243",
      "Wildfire":
        "https://www.mousehuntgame.com/images/items/bait/73891a065f1548e474177165734ce78d.gif?cv=243"
    };

    // Location -> Cheese -> Mouse
    const quesoData = {
      "Queso River": {
        "Standard": [
          "Tiny Saboteur",
          "Pump Raider",
          "Croquet Crusher",
          "Queso Extractor"
        ],
        "SB+": ["Sleepy Merchant"],
        "Wildfire": ["Queen Quesada"]
      },
      "Prickly Plains": {
        "Bland": ["Spice Seer", "Old Spice Collector"],
        "Mild": ["Spice Farmer", "Granny Spice"],
        "Medium": ["Spice Sovereign", "Spice Finder"],
        "Hot": ["Spice Raider", "Spice Reaper"],
        "Flamin'": ["Inferna, The Engulfed"]
      },
      "Cantera Quarry": {
        "Bland": ["Chip Chiseler", "Tiny Toppler"],
        "Mild": ["Ore Chipper", "Rubble Rummager"],
        "Medium": ["Nachore Golem", "Rubble Rouser"],
        "Hot": ["Grampa Golem", "Fiery Crusher"],
        "Flamin'": ["Nachous, The Molten"]
      },
      "Cork Collecting": {
        "Bland": ["Fuzzy Drake"],
        "Mild": ["Cork Defender"],
        "Medium": ["Burly Bruiser"],
        "Hot": ["Horned Cork Hoarder"],
        "Flamin'": ["Rambunctious Rain Rumbler", "Corky, the Collector"],
        "Wildfire": ["Corkataur"]
      },
      "Pressure Building": {
        "Mild": ["Steam Sailor"],
        "Medium": ["Warming Wyvern"],
        "Hot": ["Vaporior"],
        "Flamin'": ["Pyrehyde"],
        "Wildfire": ["Emberstone Scaled"]
      },
      "Small Eruption": {
        Mild: ["Sizzle Pup"],
        Medium: ["Sizzle Pup"],
        Hot: ["Sizzle Pup"]
        // Mild: ["Mild Spicekin", "Sizzle Pup"],
        // Medium: ["Sizzle Pup", "Smoldersnap", "Mild Spicekin"],
        // Hot: ["Sizzle Pup", "Ignatia"]
      },
      "Medium Eruption": {
        "Medium": ["Bearded Elder"],
        "Hot": ["Bearded Elder"],
        "Flamin'": ["Bearded Elder"]
        // Mild: ["Mild Spicekin"],
        // Medium: ["Bearded Elder", "Smoldersnap"],
        // Hot: ["Bearded Elder", "Ignatia"],
        // "Flamin'": ["Bearded Elder", "Bruticus, the Blazing"]
      },
      "Large Eruption": {
        "Hot": ["Cinderstorm"],
        "Flamin'": ["Cinderstorm"]
        // Medium: ["Smoldersnap"],
        // Hot: ["Cinderstorm", "Ignatia"],
        // "Flamin'": ["Cinderstorm", "Bruticus, the Blazing"]
      },
      "Epic Eruption": {
        "Flamin'": ["Stormsurge, the Vile Tempest"],
        "Wildfire": ["Kalor'ignis of the Geyser"]
        // Hot: ["Ignatia", "Stormsurge, the Vile Tempest"],
        // "Flamin'": ["Stormsurge, the Vile Tempest", "Bruticus, the Blazing"],
      },
      "Any Eruption": {
        "Mild": ["Mild Spicekin"],
        "Medium": ["Smoldersnap"],
        "Hot": ["Ignatia"],
        "Flamin'": ["Bruticus, the Blazing"]
      }
    };

    // Alternate representation: Mouse -> Location -> Cheese
    const quesoMice = {};
    for (let location in quesoData) {
      for (let cheese in quesoData[location]) {
        const arr = quesoData[location][cheese];
        for (let mouse of arr) {
          if (quesoMice[mouse] === undefined) {
            quesoMice[mouse] = [[location, cheese]];
          } else {
            quesoMice[mouse].push([location, cheese]);
          }
        }
      }
    }
  }
})();