TORN: Mission Reward Information

Give some information about mission rewards.

// ==UserScript==
// @name         TORN: Mission Reward Information
// @namespace    dekleinekobini.missionrewardinformatiom
// @version      2.1.2
// @author       DeKleineKobini [2114440]
// @description  Give some information about mission rewards.
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @match        https://www.torn.com/loader.php?sid=missions*
// @connect      tornplayground.eu
// @connect      api.torn.com
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// ==/UserScript==

(o=>{if(typeof GM_addStyle=="function"){GM_addStyle(o);return}const d=document.createElement("style");d.textContent=o,document.head.append(d)})(' .playground__tornapi__api-prompt{margin-bottom:10px}.playground__tornapi__api-prompt header{background-image:linear-gradient(90deg,transparent 50%,rgba(0,0,0,.07) 0px);background-color:#90b02e;background-size:4px;display:flex;align-items:center;color:#fff;font-size:13px;letter-spacing:1px;text-shadow:rgba(0,0,0,.65) 1px 1px 2px;padding:6px 10px;border-radius:5px}.playground__tornapi__api-prompt .playground__tornapi__title{flex-grow:1;box-sizing:border-box}.playground__tornapi__api-prompt .playground__tornapi__save-button{padding:2px 10px;text-shadow:rgba(0,0,0,.05) 1px 1px 2px;cursor:pointer;box-shadow:#ffffff80 0 1px 1px inset,#00000040 0 1px 1px 1px;border:none;border-radius:4px;background-color:#ffffff26;color:#fff}body[data-playground-device=DESKTOP] .mod-description.playground-modified li:nth-child(3):after,body[data-playground-device=DESKTOP] .mod-description.playground-modified li:nth-child(4):after{content:" ";position:absolute;display:block;width:100%;height:1px;bottom:0;left:0;border-bottom:1px solid #000}body[data-playground-device=DESKTOP] .mod-description.playground-modified li:nth-child(3){margin-right:3px}body[data-playground-device=DESKTOP] .mod-description.playground-modified li:nth-child(5):before,body[data-playground-device=DESKTOP] .mod-description.playground-modified li:nth-child(6):before{content:" ";position:absolute;display:block;width:100%;height:1px;top:0;left:0;border-top:1px solid #323232} ');

(function () {
  'use strict';

  function isElement(node) {
    return node.nodeType === Node.ELEMENT_NODE;
  }
  function isHTMLElement(node) {
    return isElement(node) && node instanceof HTMLElement;
  }
  function formatNumber(original, decimals = 2) {
    const pattern = `\\d(?=(\\d{3})+${decimals > 0 ? "\\." : "$"})`;
    return original.toFixed(Math.max(0, ~~decimals)).replace(new RegExp(pattern, "g"), "$&,");
  }
  function notNull(value) {
    return value != null;
  }
  var _GM_xmlhttpRequest = /* @__PURE__ */ (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)();
  function fetchGM(url, options) {
    const method = (options == null ? void 0 : options.method) || "GET";
    return new Promise((resolve, reject) => {
      _GM_xmlhttpRequest({
        method,
        url,
        headers: options == null ? void 0 : options.headers,
        data: options == null ? void 0 : options.body,
        onload: (response) => {
          response.status === 200 ? resolve(JSON.parse(response.responseText)) : reject(new Error(`Request failed with status: ${response.status} - ${response.statusText}`));
        },
        onerror: (response) => reject(new Error(`Request failed with status: ${response.status} - ${response.statusText} or error: ${response.error}`)),
        ontimeout: () => reject(new Error("Request timed out")),
        onabort: () => reject(new Error("Request aborted"))
      });
    });
  }
  function readableErrorMessage(error) {
    return error instanceof TypeError && error.message.includes("Failed to fetch") ? "Couldn't connect to the server." : error instanceof Error ? error.message : error.toString();
  }
  const apiPrompt = "playground__tornapi__api-prompt", title = "playground__tornapi__title", saveButton = "playground__tornapi__save-button", styles = {
    "api-prompt": "playground__tornapi__api-prompt",
    apiPrompt,
    title,
    "save-button": "playground__tornapi__save-button",
    saveButton
  };
  function hasKeyInStorage() {
    return "###PDA-APIKEY###".startsWith("###") ? localStorage.getItem("dkkutils_apikey") !== null : true;
  }
  function getKeyFromStorage() {
    const pdaKey = "###PDA-APIKEY###";
    return pdaKey.startsWith("###") ? localStorage.getItem("dkkutils_apikey") || void 0 : pdaKey;
  }
  function initializeTornAPI() {
    const key = getKeyFromStorage();
    if (key && isValid(key))
      return;
    let selector;
    switch (window.location.pathname) {
      case "/christmas_town.php":
        selector = ".content-wrapper div[id*='root'] > div > div:eq(0)";
        break;
      default:
        selector = ".content-title";
        break;
    }
    const createPrompt = () => {
      if (document.getElementById("dkkapi-prompt"))
        return;
      const title2 = document.createElement("span");
      title2.className = styles.title, title2.textContent = "API Prompt";
      const input = document.createElement("input");
      input.type = "text", input.style.marginRight = "8px";
      const saveButton2 = document.createElement("button");
      saveButton2.className = styles.saveButton, saveButton2.textContent = "Save", saveButton2.addEventListener("click", (event) => {
        event.preventDefault();
        const inputKey = input.value;
        isValid(inputKey) ? (widget.remove(), localStorage.setItem("dkkutils_apikey", inputKey)) : input.value = "";
      });
      const header = document.createElement("header");
      header.appendChild(title2), header.appendChild(input), header.appendChild(saveButton2);
      const widget = document.createElement("div");
      widget.className = styles.apiPrompt, widget.id = "dkkapi-prompt", widget.appendChild(header);
      const clearDiv = document.createElement("div");
      clearDiv.className = "clear";
      const selectorElement = document.querySelector(selector);
      selectorElement.parentNode.insertBefore(widget, selectorElement.nextSibling), selectorElement.parentNode.insertBefore(clearDiv, selectorElement.nextSibling);
    };
    document.querySelector(selector) ? createPrompt() : new MutationObserver((_, observer) => {
      document.querySelector(selector) && (createPrompt(), observer.disconnect());
    }).observe(document, { childList: true, subtree: true });
  }
  function isValid(key) {
    return !key || key === "undefined" || key === null || key === "null" || key === "" ? false : key.length === 16;
  }
  function apiRequest(providedOptions) {
    const options = fillOptions(providedOptions), url = `https://api.torn.com/${options.section}/${options.id}?selections=${options.selections}&comment=${options.comment}&key=${options.key}`;
    return new Promise((resolve, reject) => {
      fetchGM(url).then((data) => resolve(handleApiResponse(data))).catch((reason) => reject({ type: "other", reason }));
    });
  }
  async function handleApiResponse(data) {
    if ("error" in data)
      throw {
        type: "api",
        code: data.error.code,
        message: data.error.error
      };
    return data;
  }
  function isApiError(error) {
    return "type" in error && ["api", "http", "timeout"].includes(error.type);
  }
  function fillOptions(options) {
    let key;
    if ("key" in options && options.key)
      key = options.key;
    else if (hasKeyInStorage())
      key = getKeyFromStorage();
    else
      throw new Error("Missing API key");
    return {
      section: options.section,
      id: options.id ?? "",
      selections: options.selections.join(","),
      key,
      comment: options.comment || "Sandbox"
    };
  }
  const rewardHandlers = [];
  const refreshHandlers = [];
  function setupMissionObservers() {
    new MutationObserver((mutations) => {
      const foundDescription = mutations.flatMap((mutation) => [...mutation.addedNodes]).filter(isHTMLElement).filter((element) => element.classList.contains("show-item-info")).find((element) => !!element);
      if (!foundDescription)
        return;
      const itemElement = document.querySelector(".rewards-list > li.act");
      rewardHandlers.forEach((onReward) => onReward(foundDescription, JSON.parse(itemElement.dataset.ammoInfo)));
    }).observe(document.body, { subtree: true, childList: true });
    refreshHandlers.forEach((onRefresh) => onRefresh());
    ["#viewMissionsRewardsContainer", ".rewards-wrap", ".rewards-slider-underlayer", ".rewards-slider", ".rewards-slider .slide", ".rewards-list"].map((selector) => document.querySelector(selector)).filter(notNull).forEach((element) => {
      new MutationObserver((mutations) => {
        console.log("DKK mission MO", element.className, mutations);
      }).observe(element, { childList: true });
    });
  }
  function registerRewardHandler(handler) {
    rewardHandlers.push(handler);
  }
  function registerRefreshHandler(handler) {
    refreshHandlers.push(handler);
  }
  const BASE_URL = "https://tornplayground.eu/";
  function getWeaponMod(name) {
    return new Promise((resolve, reject) => {
      fetchGM(`${BASE_URL}api/missionrewards/weaponmods/${name}`).then((response) => resolve(response)).catch((error) => {
        if (error.message.includes("404")) {
          resolve(null);
          return;
        }
        reject(readableErrorMessage(error));
      });
    });
  }
  function sendWeaponMods(update) {
    return new Promise((resolve, reject) => {
      fetchGM(`${BASE_URL}api/missionrewards/weaponmods`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(update)
      }).then((response) => resolve(response)).catch((error) => reject(readableErrorMessage(error)));
    });
  }
  function sendSpecialAmmo(update) {
    return new Promise((resolve, reject) => {
      fetchGM(`${BASE_URL}api/missionrewards/ammo`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(update)
      }).then((response) => resolve(response)).catch((error) => reject(readableErrorMessage(error)));
    });
  }
  async function showWeaponModData(name, modInfo) {
    if (modInfo.dataset.wpmInit === "true")
      return;
    modInfo.dataset.wpmInit = "true";
    try {
      const prices = await getWeaponMod(name);
      if (!prices)
        return;
      const priceHtml = `<li><span>Price Range:</span> <span class="bold">${prices.minPrice} - ${prices.maxPrice}</span></li>`;
      const specialHtml = `<li><span>Special Offer Range:</span> <span class="bold">${prices.minSpecialPrice} - ${prices.maxSpecialPrice}</span></li>`;
      const description = modInfo.querySelector(".mod-description");
      description.classList.add("playground-modified");
      description.children[1].insertAdjacentHTML("afterend", priceHtml);
      description.children[2].insertAdjacentHTML("afterend", specialHtml);
    } catch (error) {
      console.error("[MRI] Failed to show weapon mod prices.", error);
    }
  }
  function sendAllData() {
    queryAllMods().forEach(sendWeaponModData);
    querySpecialAmmo().forEach(sendSpecialAmmoData);
  }
  function queryAllMods() {
    return [...document.querySelectorAll(".rewards-list li.mod-wrap[data-ammo-info]")].filter((element) => !element.classList.contains("playground-mod")).map((element) => ({ element, data: JSON.parse(element.dataset.ammoInfo) })).filter((item) => item.data.type === "weaponUpgrade");
  }
  function sendWeaponModData(query) {
    const { name, points } = query.data;
    const isSpecialOffer = query.data.label === "special-offer";
    query.element.classList.add("playground-mod");
    sendWeaponMods({ name, price: points, special: isSpecialOffer }).then((response) => {
      if (response.value) {
        console.log(`[MRI] Your current price for ${name} at ${points} has been recorded.`);
      } else
        console.log(`[MRI] Your current price for ${name} at ${points} has been NOT recorded because it falls within the known range.`);
    }).catch((cause) => {
      console.warn(`[MRI] Failed to record your current price for ${name}.`, cause);
    });
  }
  function querySpecialAmmo() {
    return [...document.querySelectorAll(".rewards-list li.ammo-wrap[data-ammo-info]")].filter((element) => !element.classList.contains("playground-ammo")).map((element) => ({ element, data: JSON.parse(element.dataset.ammoInfo) })).filter((item) => item.data.basicType === "Ammo");
  }
  function sendSpecialAmmoData(query) {
    const { amount, name, ammoType, points: price } = query.data;
    const type = ammoType.toUpperCase().replace(" ", "_");
    query.element.classList.add("playground-ammo");
    sendSpecialAmmo({ name, type, amount, price }).then((response) => {
      if (response.value) {
        console.log(`[MRI] Your current price for ${name} ${type} at ${price} has been recorded.`);
      } else
        console.log(`[MRI] Your current price for ${name} ${type} at ${price} has been NOT recorded because it falls within the known range.`);
    }).catch((cause) => {
      console.warn(`[MRI] Failed to record your current price for ${name} ${type}.`, cause);
    });
  }
  const minTabletSize = 386;
  const maxTabletSize = 784;
  const maxTabletSizeWithoutSidebar = 1e3;
  const minTabletSizeWithoutSidebar = 600;
  function isPageWithoutSidebar() {
    return document.body.classList.contains("without-sidebar") || false;
  }
  function getScreenWidth() {
    return window.innerWidth;
  }
  function getMaxTabletSize() {
    return isPageWithoutSidebar() ? maxTabletSizeWithoutSidebar : maxTabletSize;
  }
  function getMinTabletSize() {
    return isPageWithoutSidebar() ? minTabletSizeWithoutSidebar : minTabletSize;
  }
  function hasSidebar() {
    const hasDesktopScreen = getScreenWidth() > 1e3;
    return hasDesktopScreen && !isPageWithoutSidebar();
  }
  function getCurrentScreenSize() {
    const width = getScreenWidth();
    if (width > getMaxTabletSize()) {
      return "DESKTOP";
    }
    if (width <= getMinTabletSize()) {
      return "MOBILE";
    }
    return "TABLET";
  }
  function updateScreenSize() {
    document.body.dataset.playgroundDevice = getCurrentScreenSize();
    document.body.dataset.playgroundSidebar = `${hasSidebar()}`;
  }
  function setupScreenSize() {
    if (document.body.dataset.playgroundScreenSizeInitialized === "true") {
      return;
    }
    updateScreenSize();
    window.addEventListener("resize", updateScreenSize);
    document.body.dataset.playgroundScreenSizeInitialized = "true";
  }
  initializeTornAPI();
  setupScreenSize();
  registerRefreshHandler(sendAllData);
  registerRewardHandler((element, data) => {
    if (data.type === "weaponUpgrade") {
      showWeaponModData(data.name, element).catch((cause) => console.error("[MRI] Failed to show weapon mod prices.", cause));
    } else if (data.basicType === "Item") {
      showItemInfo(data.points, data.amount);
    } else if (data.basicType === "Ammo") {
      void showAmmoAmount(data.ammoType, data.name);
    } else {
      console.debug("[MRI] Opened another item type.", data);
    }
  });
  setupMissionObservers();
  async function showAmmoAmount(type, size) {
    const owned = await getAmmoAmount(type, size) ?? "api not loaded";
    document.querySelector(".ammo-description").insertAdjacentHTML(
      "beforeend",
      `
        <li>
            <span>Owned:</span>
            <span class="bold">${owned}</span>
        </li>
    `
    );
  }
  async function getAmmoAmount(type, size) {
    const apiAmmo = await apiRequest({ section: "user", selections: ["ammo"] });
    if (isApiError(apiAmmo))
      return void 0;
    const ownedAmmo = apiAmmo.ammo.find((ammo) => ammo.size === size && ammo.type === type);
    return (ownedAmmo == null ? void 0 : ownedAmmo.quantity) ?? 0;
  }
  function showItemInfo(points, amount) {
    if (document.querySelector(".show-item-info .info-wrap"))
      show();
    else {
      new MutationObserver((_, observer) => {
        if (!document.querySelector(".show-item-info"))
          return;
        show();
        observer.disconnect();
      }).observe(document.querySelector(".show-item-info"), { childList: true });
    }
    function show() {
      const valueElement = document.querySelector(".show-item-info li:first-child .desc");
      const value = parseInt(valueElement.innerText.replaceAll("$", "").replaceAll(",", ""), 10);
      const valueCredits = value * amount / points;
      const fields = document.querySelectorAll(".show-item-info .info-cont > li:not(.clear)");
      let field = fields.item(fields.length - 1);
      if (field.innerHTML.length > 0) {
        const newField = document.createElement("li");
        newField.classList.add("t-left");
        field.after(newField);
        field = newField;
      }
      field.insertAdjacentHTML(
        "beforeend",
        `
                <div class='title'>Money / Credit:</div>
                <div class='desc'>${formatNumber(valueCredits)}</div>
                <div class='clear'></div>
            `
      );
    }
  }

})();