TORN: Mission Reward Information

Give some information about mission rewards.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

})();