您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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> ` ); } } })();