TORN: Mission Reward Information

Give some information about mission rewards.

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 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>
            `
      );
    }
  }

})();