🐭️ MouseHunt - Item Links

Adds a drop rate table from MHCT, links to the MouseHunt wiki, MHCT looter, and Markethunt, as well as various other features to the item view page.

// ==UserScript==
// @name        🐭️ MouseHunt - Item Links
// @description Adds a drop rate table from MHCT, links to the MouseHunt wiki, MHCT looter, and Markethunt, as well as various other features to the item view page.
// @version     2.1.0
// @license     MIT
// @author      bradp
// @namespace   bradp
// @match       https://www.mousehuntgame.com/*
// @icon        https://i.mouse.rip/mh-improved/icon-64.png
// @run-at      document-end
// @grant       none
// @require     https://cdn.jsdelivr.net/npm/script-migration@1.1.1
// ==/UserScript==

var mhui = (() => {
  var __defProp = Object.defineProperty;
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  var __getOwnPropNames = Object.getOwnPropertyNames;
  var __hasOwnProp = Object.prototype.hasOwnProperty;
  var __export = (target, all) => {
    for (var name in all)
      __defProp(target, name, { get: all[name], enumerable: true });
  };
  var __copyProps = (to, from, except, desc) => {
    if (from && typeof from === "object" || typeof from === "function") {
      for (let key of __getOwnPropNames(from))
        if (!__hasOwnProp.call(to, key) && key !== except)
          __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
    }
    return to;
  };
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
  var __async = (__this, __arguments, generator) => {
    return new Promise((resolve, reject) => {
      var fulfilled = (value) => {
        try {
          step(generator.next(value));
        } catch (e) {
          reject(e);
        }
      };
      var rejected = (value) => {
        try {
          step(generator.throw(value));
        } catch (e) {
          reject(e);
        }
      };
      var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
      step((generator = generator.apply(__this, __arguments)).next());
    });
  };

  // src/modules/better-item-view/index.js
  var better_item_view_exports = {};
  __export(better_item_view_exports, {
    default: () => better_item_view_default
  });

  // src/utils/event-registry.js
  var eventsAdded = {};
  var onEvent = (event, callback, remove = false) => {
    if (!eventRegistry) {
      return;
    }
    const id = `${event}-${remove.toString()}-${callback.toString()}`;
    if (eventsAdded[id]) {
      return;
    }
    eventsAdded[id] = true;
    eventRegistry.addEventListener(event, callback, null, remove);
  };

  // src/utils/styles.js
  var addModuleStyles = (styles, identifier = "mh-improved-styles", replace = false) => {
    const existingStyles = document.querySelector(`#${identifier}`);
    styles = Array.isArray(styles) ? styles.join("\n") : styles;
    if (existingStyles) {
      if (replace) {
        existingStyles.innerHTML = styles;
      } else {
        existingStyles.innerHTML += styles;
      }
      return existingStyles;
    }
    const style = document.createElement("style");
    style.id = identifier;
    style.innerHTML = styles;
    document.head.append(style);
    return style;
  };
  var addStyles = (styles, module = false, identifier = "mh-improved-styles") => {
    if (!module) {
      throw new Error("Module ID is required for adding module styles.", module);
    }
    const key = `${identifier}-${module}`;
    let stylesEl = addModuleStyles(styles, key, true);
    onEvent(`mh-improved-settings-changed-${module}`, (enabled) => {
      if (enabled) {
        stylesEl = addModuleStyles(styles, key, true);
      } else if (stylesEl) {
        stylesEl.remove();
      }
    });
  };

  // src/utils/settings.js
  var getSettingDirect = (key = null, defaultValue = null, identifier = "mousehunt-improved-settings") => {
    const settings = JSON.parse(localStorage.getItem(identifier)) || {};
    if (!key) {
      return settings;
    }
    if (!key.includes(".")) {
      if (settings[key] === void 0) {
        return defaultValue;
      }
      return settings[key];
    }
    const groupAndKey = getGroupAndKey(key);
    if (!groupAndKey.group) {
      if (settings[groupAndKey.key] === void 0) {
        return defaultValue;
      }
      return settings[groupAndKey.key];
    }
    const groupSettings = settings[groupAndKey.group] || {};
    if (groupSettings[groupAndKey.key] === void 0) {
      return defaultValue;
    }
    return groupSettings[groupAndKey.key];
  };
  var getGroupAndKey = (key) => {
    const split = key.split(".");
    if (split.length === 1) {
      return {
        group: null,
        key: split[0]
      };
    }
    if (split[0] === "location-huds-enabled") {
      return {
        group: "location-huds-enabled",
        key: split[1]
      };
    }
    return {
      group: `${split[0]}-settings`,
      key: split[1]
    };
  };
  var getSetting = (key, defaultValue = false) => {
    return getSettingDirect(key, defaultValue, "mousehunt-improved-settings");
  };

  // src/utils/elements.js
  var makeElement = (tag, classes = "", text = "", appendTo = null) => {
    const element = document.createElement(tag);
    if (Array.isArray(classes)) {
      classes = classes.join(" ");
    }
    if (classes && classes.length) {
      element.className = classes;
    }
    element.innerHTML = text;
    if (appendTo) {
      appendTo.append(element);
      return appendTo;
    }
    return element;
  };
  var makeLink = (text, href, encodeAsSpace = false) => {
    if (encodeAsSpace) {
      href = href.replaceAll("_", "%20");
    }
    return `<a href="${href}" target="_mouse" class="mousehuntActionButton tiny"><span>${text}</span></a>`;
  };
  var makeTooltip = (options) => {
    if (!options.appendTo) {
      return false;
    }
    const { appendTo, className = "", text = "" } = options;
    const tooltip = makeElement("div", ["PreferencesPage__blackTooltip", "mh-improved-tooltip", className]);
    makeElement("span", "PreferencesPage__blackTooltipText", text, tooltip);
    appendTo.append(tooltip);
    return tooltip;
  };

  // src/utils/db.js
  var database = (databaseName) => __async(void 0, null, function* () {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(`mh-improved-${databaseName}`, 6);
      request.onerror = (event) => {
        reject(event.target.error);
      };
      request.onsuccess = (event) => {
        resolve(event.target.result);
      };
      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        if (!db.objectStoreNames.contains(databaseName)) {
          db.createObjectStore(databaseName, { keyPath: "id" });
        }
      };
    });
  });
  var dbGet = (databaseName, id) => __async(void 0, null, function* () {
    const db = yield database(databaseName);
    const transaction = db.transaction(databaseName, "readonly");
    transaction.onerror = (event) => {
      throw new Error(event.target.error);
    };
    const objectStore = transaction.objectStore(databaseName);
    const request = objectStore.get(id);
    return new Promise((resolve, reject) => {
      request.onsuccess = () => {
        resolve(request.result);
      };
      request.onerror = () => {
        reject(request.error);
      };
      transaction.oncomplete = () => {
        db.close();
      };
    });
  });
  var dbSet = (databaseName, data) => __async(void 0, null, function* () {
    const db = yield database(databaseName);
    const transaction = db.transaction(databaseName, "readwrite");
    const objectStore = transaction.objectStore(databaseName);
    data = {
      data,
      id: data.id || Date.now()
    };
    const request = objectStore.put(data);
    return new Promise((resolve, reject) => {
      request.onsuccess = () => {
        resolve(request.result);
      };
      request.onerror = () => {
        reject(request.error);
      };
      transaction.oncomplete = () => {
        db.close();
      };
    });
  });

  // src/utils/global.js
  var getGlobal = (key) => {
    if (window && window.mhui) {
      return window.mhui[key] || false;
    }
    if ("undefined" !== typeof app && app && app.mhui) {
      return app.mhui[key] || false;
    }
    return false;
  };

  // src/utils/data.js
  var getHeaders = () => {
    return {
      "Content-Type": "application/json",
      "X-MH-Improved": "true",
      "X-MH-Improved-Version": mhImprovedVersion || "unknown",
      "X-MH-Improved-Platform": mhImprovedPlatform || "unknown"
    };
  };

  // src/utils/events.js
  var runCallbacks = (settings, parentNode, callbacks) => {
    Object.keys(settings).forEach((key) => {
      if (parentNode && parentNode.classList && parentNode.classList.contains(settings[key].selector)) {
        settings[key].isVisible = true;
        if (callbacks[key] && callbacks[key].show) {
          callbacks[key].show();
        }
      } else if (settings[key].isVisible) {
        settings[key].isVisible = false;
        if (callbacks[key] && callbacks[key].hide) {
          callbacks[key].hide();
        }
      }
    });
    return settings;
  };
  var overlayMutationObserver = null;
  var overlayCallbacks = [];
  var onOverlayChange = (callbacks) => {
    let overlayData = {
      map: {
        isVisible: false,
        selector: "treasureMapPopup"
      },
      item: {
        isVisible: false,
        selector: "itemViewPopup"
      },
      mouse: {
        isVisible: false,
        selector: "mouseViewPopup"
      },
      image: {
        isVisible: false,
        selector: "largerImage"
      },
      convertible: {
        isVisible: false,
        selector: "convertibleOpenViewPopup"
      },
      adventureBook: {
        isVisible: false,
        selector: "adventureBookPopup"
      },
      marketplace: {
        isVisible: false,
        selector: "marketplaceViewPopup"
      },
      gifts: {
        isVisible: false,
        selector: "giftSelectorViewPopup"
      },
      support: {
        isVisible: false,
        selector: "supportPageContactUsForm"
      },
      premiumShop: {
        isVisible: false,
        selector: "MHCheckout"
      }
    };
    overlayCallbacks.push(callbacks);
    if (overlayMutationObserver) {
      return;
    }
    overlayMutationObserver = true;
    const observer = new MutationObserver(() => {
      overlayCallbacks.forEach((callback) => {
        if (callback.change) {
          callback.change();
        }
        const overlayType = document.querySelector("#overlayPopup");
        if (overlayType && overlayType.classList.length <= 0) {
          return;
        }
        const overlayBg = document.querySelector("#overlayBg");
        if (overlayBg && overlayBg.classList.length > 0) {
          if (callback.show) {
            callback.show();
          }
        } else if (callback.hide) {
          callback.hide();
        }
        overlayData = runCallbacks(overlayData, overlayType, callback);
      });
    });
    const observeTarget = document.querySelector("#overlayPopup");
    if (observeTarget) {
      observer.observe(observeTarget, {
        attributes: true,
        attributeFilter: ["class"]
      });
    }
  };

  // src/utils/maps.js
  var mapper = (key = false) => {
    if (key) {
      const mapperData = getGlobal("mapper");
      if (!mapperData || !mapperData[key]) {
        return false;
      }
      return mapperData[key];
    }
    return getGlobal("mapper");
  };
  var mapData = () => {
    const m = mapper();
    if (!m) {
      return {};
    }
    return m.mapData;
  };
  var getCachedValue = (key) => __async(void 0, null, function* () {
    var _a;
    const value = yield dbGet("ar-cache", key);
    if (!((_a = value == null ? void 0 : value.data) == null ? void 0 : _a.value)) {
      return null;
    }
    return value.data.value;
  });
  var setCachedValue = (key, value) => __async(void 0, null, function* () {
    yield dbSet("ar-cache", { id: key, value });
  });
  var getArForMouse = (id, type = "mouse") => __async(void 0, null, function* () {
    let mhctJson = [];
    const cacheKey = `${type}-${id}`;
    const cachedAr = yield getCachedValue(cacheKey);
    if (cachedAr) {
      return cachedAr;
    }
    const isItem = "item" === type;
    const mhctPath = isItem ? "mhct-item" : "mhct";
    let mhctData = [];
    const data = mapData() || {};
    const mapType = (data == null ? void 0 : data.map_type) || "";
    let url = `https://api.mouse.rip/${mhctPath}/${id}`;
    if (mapType.toLowerCase().includes("halloween")) {
      url = `https://api.mouse.rip/${mhctPath}/${id}-hlw_22`;
    }
    try {
      mhctData = yield fetch(url, { headers: getHeaders() });
    } catch (error) {
      console.error("Error fetching MHCT data:", error);
      yield new Promise((resolve) => setTimeout(resolve, 500));
      try {
        mhctData = yield fetch(url, { headers: getHeaders() });
      } catch (errorRetry) {
        console.error("Error fetching MHCT data:", errorRetry);
        return [];
      }
    }
    if (!mhctData.ok) {
      return [];
    }
    mhctJson = yield mhctData.json();
    if (!mhctJson || mhctJson.length === 0) {
      return [];
    }
    if (isItem) {
      for (const rate of mhctJson) {
        rate.rate = Number.parseInt(rate.drop_pct * 100);
        delete rate.drop_ct;
      }
    }
    if (mhctJson.error) {
      return [];
    }
    mhctJson = mhctJson.filter((rate) => {
      if (rate.rate === 0) {
        return false;
      }
      if (rate.rate === 9999) {
        rate.rate = 1e4;
      }
      return true;
    });
    yield setCachedValue(cacheKey, mhctJson);
    return mhctJson;
  });

  // src/utils/messages.js
  hadAddedErrorStyles = false;

  // src/modules/better-item-view/settings/index.js
  var settings_default = () => __async(void 0, null, function* () {
    return [
      {
        id: "better-item-view.show-drop-rates",
        title: "Show drop rates",
        default: true
      },
      {
        id: "better-item-view.show-item-hover",
        title: "Show item details on hover (in journal)",
        default: true
      }
    ];
  });

  // src/modules/better-item-view/styles.css
  var styles_default = '.itemView-titleContainer{height:26px}.itemView-header-name{display:flex;align-items:center;justify-content:space-between}.mh-item-links{display:flex;justify-content:flex-end;margin-right:-10px}.mh-item-links a{margin-right:5px}.itemView-header-name .mh-item-links span{display:inline-block;font-size:11px;font-weight:400}.itemView-has-mhct .mouse-ar-wrapper{display:grid;grid-template-columns:150px auto 50px;place-items:center stretch;padding:5px;margin:5px 0;font-size:12px}.itemView-has-mhct .has-stages .mouse-ar-wrapper{grid-template-columns:110px 140px auto 50px}.itemView-has-mhct .mouse-ar-wrapper div{padding:0 2px}.itemView-has-mhct .mice-ar-wrapper{margin-right:10px}.mouse-ar-wrapper .stage{font-size:10px}.mouse-ar-wrapper .cheese{font-size:11px}.itemView-has-mhct .ar-header{display:flex;align-items:center;justify-content:space-between;height:26px;padding-bottom:2px;margin-top:10px;margin-bottom:10px;font-size:12px;font-weight:900;border-bottom:1px solid #ccc}.itemView-has-mhct .ar-link{font-size:9px}.itemView-has-mhct .rate{text-align:right}.itemView-has-mhct .mouse-ar-wrapper:nth-child(odd){background-color:#e7e7e7}.itemView-has-mhct .itemView-description{font-weight:500;line-height:19px}.itemView-action.crafting_item b{display:none}.itemView-action.crafting_item:before{content:"This can be used to craft other items!"}.itemViewContainer.map_piece .itemView-action-text.map_piece,.itemViewContainer.base .itemView-action-text.base,.itemViewContainer.weapon .itemView-actio-textn.weapon,.itemViewContainer.bait .itemView-action-text.bait,.itemViewContainer.trinket .itemView-action-text.trinket,.itemViewContainer.potion .itemView-action-text.potion,.itemViewContainer.readiness_item .itemView-action-text.readiness_item,.itemViewContainer.convertible .itemView-action-text.convertible,.itemViewContainer.torn_page .itemView-action-text.torn_page,.itemViewContainer.crafting_item .itemView-action-text.crafting_item,.itemViewContainer.collectible .itemView-action-text.collectible,.itemViewContainer.message_item .itemView-action-text.message_item,.itemViewContainer.bonus_loot .itemView-action-text.bonus_loot,.itemViewContainer.stat .itemView-action-text.stat,.itemViewContainer.quest .itemView-action-text.quest,.itemViewContainer.skin .itemView-action-text.skin{display:none!important}.itemViewContainer .shopCustomization .itemViewStatBlock-stat{display:flex;flex-direction:column;align-items:center}.itemViewContainer .itemViewStatBlock-stat{display:flex;flex-direction:row;align-items:center;justify-content:flex-start}.itemViewContainer .itemViewStatBlock-stat-value{flex:1;text-align:left}.itemViewContainer .itemViewStatBlock-stat.cheeseEffect{font-size:9px;text-align:center}.itemViewContainer .itemViewStatBlock.trinket .itemViewStatBlock-padding{display:flex;flex-direction:column;align-items:stretch;width:100px}.itemViewContainer .itemViewStatBlock.trinket{width:100px;font-size:13px}#overlayPopup.itemViewPopup #jsDialogClose{z-index:1}#overlayPopup.itemViewPopup .itemView-header-classification{right:25px}.itemView-actionContainer{display:flex;flex-wrap:wrap;gap:10px}.itemView-action{border-top:none}.itemViewContainer.potion .inventoryPage-item-recipeOptions li{width:365px}.itemView-character-image{width:auto;height:84px;margin-top:-15px;margin-left:-9px}.itemView-character-name{left:-11px;width:75px;font-size:15px}.itemView-padding{margin-left:70px}.itemView-thumbnail.large{margin-left:-15px}input.itemView-action-convert-quantity{width:50px}.itemViewPopup .itemViewStatBlock-padding{flex-direction:column}.itemView-character .itemView-character-image{transition:all .4s ease-out;transform-origin:bottom}.itemView-character:hover .itemView-character-image{transform:scale(1.2) rotate(-10deg) translate(5px)}.itemView-header-classification{visibility:hidden}.itemView-header-classification span{visibility:visible}.itemViewStatBlock-stat{display:flex;align-items:center}.itemView-sidebar-checklistItem:nth-child(1),.itemView-sidebar-checklistItem:nth-child(2),.itemView-sidebar-checklistItem.checked{display:block}.itemView-sidebar-checklistItem{background:url(https://www.mousehuntgame.com/images/icons/bad_idea.png) 1px 4px no-repeat;background-size:14px}.itemView-partsContainer{display:flex;flex-direction:column;align-items:stretch;padding-top:15px;padding-bottom:10px;margin-top:15px;border-top:1px solid #666}\n';

  // src/modules/better-item-view/index.js
  var getLinkMarkup = (name, id) => {
    return makeLink("MHCT", `https://www.mhct.win/loot.php?item=${id}`, true) + makeLink("Wiki", `https://mhwiki.hitgrab.com/wiki/index.php/${name}`);
  };
  var addLinks = (itemId) => {
    const title = document.querySelector(".itemView-header-name");
    if (!title) {
      return;
    }
    const currentLinks = document.querySelector(".mh-item-links");
    if (currentLinks) {
      currentLinks.remove();
    }
    const div = document.createElement("div");
    div.classList.add("mh-item-links");
    div.innerHTML = getLinkMarkup(title.innerText, itemId);
    title.append(div);
    const values = document.querySelector(".mouseView-values");
    const desc = document.querySelector(".mouseView-descriptionContainer");
    if (values && desc) {
      desc.insertBefore(values, desc.firstChild);
    }
  };
  var updateItemView = () => __async(void 0, null, function* () {
    const itemView = document.querySelector(".itemViewContainer");
    if (!itemView) {
      return;
    }
    const itemId = itemView.getAttribute("data-item-id");
    if (!itemId) {
      return;
    }
    const sidebar = document.querySelector(".itemView-sidebar");
    if (sidebar) {
      const crafting = document.querySelector(".itemView-action.crafting_item");
      if (crafting) {
        sidebar.append(crafting);
      }
      const smashing = document.querySelector(".itemView-partsContainer");
      if (smashing) {
        sidebar.append(smashing);
        if (smashing.getAttribute("data-has-changed-title")) {
          return;
        }
        const smashingTitle = smashing.querySelector("b");
        if (smashingTitle) {
          smashingTitle.innerText = "Hunter's Hammer to get:";
          smashing.setAttribute("data-has-changed-title", "true");
          smashing.innerHtml = smashing.innerHTML.replace("If you smash it, you'll get:", "");
        }
      }
    }
    addLinks(itemId);
    if (!getSetting("better-item-view.show-drop-rates", true)) {
      return;
    }
    const id = Number.parseInt(itemId, 10);
    const ignored = [
      2473,
      // Mina's gift
      823,
      // party charm
      803,
      // chrome charm
      420,
      // king's credits
      1980,
      // king's keys
      585
      // scrambles
    ];
    if (ignored.includes(id)) {
      return;
    }
    let mhctJson = yield getArForMouse(itemId, "item");
    if (!mhctJson || mhctJson === void 0) {
      return;
    }
    itemView.classList.add("mouseview-has-mhct");
    const container = itemView.querySelector(".itemView-padding");
    if (!container) {
      return;
    }
    const arWrapper = makeElement("div", "ar-wrapper");
    const title = makeElement("div", "ar-header");
    const titleText = makeElement("div", "ar-title", "Drop Rates", title);
    makeTooltip({
      appendTo: titleText,
      text: 'The best location and bait, according to data gathered by <a href="https://mhct.win/" target="_blank" rel="noopener noreferrer">MHCT</a>.'
    });
    const link = makeElement("a", "ar-link", "View on MHCT \u2192");
    link.href = `https://www.mhct.win/loot.php?item=${itemId}`;
    link.target = "_mhct";
    title.append(link);
    arWrapper.append(title);
    const itemsArWrapper = makeElement("div", "item-ar-wrapper");
    const hasStages = mhctJson.some((itemAr) => itemAr.stage);
    if (hasStages) {
      itemsArWrapper.classList.add("has-stages");
    }
    mhctJson = mhctJson.filter((itemAr) => Number.parseInt(itemAr.drop_pct, 10) > 0).slice(0, 10);
    mhctJson.forEach((itemAr) => {
      const dropPercent = Number.parseInt(itemAr.drop_pct, 10).toFixed(2);
      if (dropPercent !== "0.00") {
        const itemArWrapper = makeElement("div", "mouse-ar-wrapper");
        makeElement("div", "location", itemAr.location, itemArWrapper);
        if (hasStages) {
          makeElement("div", "stage", itemAr.stage, itemArWrapper);
        }
        makeElement("div", "cheese", itemAr.cheese, itemArWrapper);
        makeElement("div", "rate", `${dropPercent}%`, itemArWrapper);
        itemsArWrapper.append(itemArWrapper);
      }
    });
    if (mhctJson.length > 0) {
      arWrapper.append(itemsArWrapper);
      container.append(arWrapper);
    }
  });
  var init = () => __async(void 0, null, function* () {
    addStyles(styles_default, "better-item-view");
    if (getSetting("better-item-view.show-item-hover", true)) {
    }
    onOverlayChange({ item: { show: updateItemView } });
  });
  var better_item_view_default = {
    id: "better-item-view",
    name: "Better Item View",
    type: "better",
    default: true,
    description: "Updates the styles and shows drop rates, links to MHCT, and MH Wiki.",
    load: init,
    settings: settings_default
  };
  return __toCommonJS(better_item_view_exports);
})();
mhImprovedVersion = "0.0.0-userscript;"
mhImprovedPlatform = "userscript";
mhui.default.load();
migrateUserscript('Item Links', 'https://greasyfork.org/en/scripts/445920-mousehunt-item-links');