// ==UserScript==
// @name Torn Market Filler
// @namespace https://github.com/SOLiNARY
// @version 0.6
// @description On "Fill" click autofills market item price with lowest market price minus $1 (customizable), fills max quantity, marks checkboxes for guns.
// @author Silmaril [2665762]
// @license MIT License
// @match https://www.torn.com/page.php?sid=ItemMarket*
// @match https://*.torn.com/page.php?sid=ItemMarket*
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @run-at document-idle
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// ==/UserScript==
(async function() {
'use strict';
try {
GM_registerMenuCommand('Set Price Delta', setPriceDelta);
GM_registerMenuCommand('Set Api Key', function() { checkApiKey(false); });
} catch (error) {
console.warn('[TornMarketFiller] Tampermonkey not detected!');
}
const itemUrl = "https://api.torn.com/torn/{itemId}?selections=items&key={apiKey}&comment=MarketFiller";
const marketUrl = "https://api.torn.com/v2/market/{itemId}?selections=itemMarket&key={apiKey}&comment=MarketFiller";
const marketUrlV2 = "https://api.torn.com/v2/market?id={itemId}&selections=itemMarket&key={apiKey}&comment=MarketFiller";
let priceDeltaRaw = localStorage.getItem("silmaril-torn-market-filler-price-delta") ?? localStorage.getItem("silmaril-torn-bazaar-filler-price-delta") ?? '-1[0]';
let apiKey = localStorage.getItem("silmaril-torn-bazaar-filler-apikey") ?? '###PDA-APIKEY###';
let GM_addStyle = function (s) {
let style = document.createElement("style");
style.type = "text/css";
style.innerHTML = s;
document.head.appendChild(style);
};
GM_addStyle(`#item-market-root [class^=addListingWrapper___] [class^=panels___] [class^=priceInputWrapper___]>.input-money-group>.input-money,#item-market-root [class^=viewListingWrapper___] [class^=priceInputWrapper___]>.input-money-group>.input-money{font-size:smaller!important;border-bottom-left-radius:0!important;border-top-left-radius:0!important}.silmaril-market-filler-popup{background:var(--tooltip-bg-color);padding:12px 18px;border-radius:8px;border:1px solid #888;box-shadow:0 4px 18px 0 #0009;color:var(--info-msg-font-color);z-index:99999;position:absolute;font-size:1em!important;line-height:1.5;pointer-events:auto}.silmaril-market-filler-popup-close{position:absolute;top:4px;right:7px;font-size:1em;color:#aaa;cursor:pointer}.silmaril-market-filler-popup-draggable{user-select:none;cursor:move}.silmaril-torn-market-filler-popup-price{cursor:pointer}`);
const pages = { "AddItems": 10, "ViewItems": 20, "Other": 0};
let recentFilledInput = null;
let popupOffsetX = localStorage.getItem("silmaril-torn-market-filler-popup-offset-x") ?? 0, popupOffsetY = 0, isDragging = false;
const marketTaxFactor = 1 - getCurrentMarketTax();
let currentPage = pages.Other;
let holdTimer;
const LOADING_THE_PRICES = 'Loading the prices...';
const isMobileView = window.innerWidth <= 784;
const observerTarget = document.querySelector("#item-market-root");
const observerConfig = { attributes: false, childList: true, characterData: false, subtree: true };
const observer = new MutationObserver(function(mutations) {
mutations.forEach(mutationRaw => {
let mutation = mutationRaw.target;
currentPage = getCurrentPage();
if (currentPage == pages.AddItems){
if (mutation.id && mutation.id.startsWith('headlessui-tabs-panel-')) {
mutation.querySelectorAll('[class*=itemRowWrapper___]:not(.silmaril-market-filler-processed) > [class*=itemRow___]:not([class*=grayedOut___]) [class^=priceInputWrapper___]').forEach(x => AddFillButton(x));
}
if (String(mutation.className).indexOf('priceInputWrapper___') > -1){
AddFillButton(mutation);
}
} else if (currentPage == pages.ViewItems){
if (mutation.className && mutation.className.startsWith('viewListingWrapper___')) {
mutation.querySelectorAll('[class*=itemRowWrapper___]:not(.silmaril-market-filler-processed) > [class*=itemRow___]:not([class*=grayedOut___]) [class^=priceInputWrapper___]').forEach(x => AddFillButton(x));
}
}
});
});
observer.observe(observerTarget, observerConfig);
addCustomFillPopup();
function AddFillButton(itemPriceElement){
if (itemPriceElement.querySelector('.silmaril-market-filler-button') != null){
return;
}
const wrapperParent = findParentByCondition(itemPriceElement, (el) => String(el.className).indexOf('itemRowWrapper___') > -1);
wrapperParent.classList.add('silmaril-market-filler-processed');
let itemIdString = wrapperParent.querySelector('[class^=itemRow___] [type=button][class^=viewInfoButton___]').getAttribute('aria-controls');
let itemImage = wrapperParent.querySelector('[class*=viewInfoButton] img');
let itemId = currentPage == pages.AddItems ? getItemIdFromString(itemIdString) : getItemIdFromImage(itemImage);
const span = document.createElement('span');
span.className = 'silmaril-market-filler-button input-money-symbol';
span.style.position = "relative";
span.setAttribute('data-action-flag', 'fill');
span.addEventListener('click', async function(e) { await handleFillClick(e, itemId) });
span.addEventListener('mousedown', startHold);
span.addEventListener('touchstart', startHold);
span.addEventListener('mouseup', cancelHold);
span.addEventListener('mouseleave', cancelHold);
span.addEventListener('touchend', cancelHold);
span.addEventListener('touchcancel', cancelHold);
const input = document.createElement('input');
input.type = 'button';
input.className = 'wai-btn';
span.appendChild(input);
itemPriceElement.querySelector('.input-money-group').prepend(span);
}
async function GetPrices(itemId){
let requestUrl = priceDeltaRaw.indexOf('[market]') != -1 ? itemUrl : marketUrlV2;
requestUrl = requestUrl
.replace("{itemId}", itemId)
.replace("{apiKey}", apiKey);
return fetch(requestUrl)
.then(response => response.json())
.then(data => {
if (data.error != null){
switch (data.error.code){
case 2:
apiKey = null;
localStorage.setItem("silmaril-torn-bazaar-filler-apikey", null);
console.error("[TornMarketFiller] Incorrect Api Key:", data);
return {"price": 'Wrong API key!', "amount": 0};
case 9:
console.warn("[TornMarketFiller] The API is temporarily disabled, please try again later");
return {"price": 'API is OFF!', "amount": 0};
default:
console.error("[TornMarketFiller] Error:", data.error.error);
return {"price": data.error.error, "amount": 0};
}
}
if (priceDeltaRaw.indexOf('[market]') != -1){
return {"price": data.items[itemId].market_value, "amount": 1};
} else {
if (data.itemmarket.listings[0].price == null){
console.warn("[TornMarketFiller] The API is temporarily disabled, please try again later");
return {"price": 'API is OFF!', "amount": 0};
}
// temporary hotfix to avoid wrong prices
if (data.itemmarket.item.id != itemId){
return {"price": 'API is BROKEN!', "amount": 0};
}
return data.itemmarket.listings;
}
})
.catch(error => {
console.error("[TornMarketFiller] Error fetching data:", error);
return 'Failed!';
});
}
function GetPrice(prices){
if (prices == null){
return 'No prices loaded';
}
if (prices.amount == 0){
return prices.price;
}
if (priceDeltaRaw.indexOf('[market]') != -1) {
prices = Array(prices);
let priceDelta = priceDeltaRaw.indexOf('[') == -1 ? priceDeltaRaw : priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('['));
return Math.round(performOperation(prices[0].price, priceDelta));
} else if (priceDeltaRaw.indexOf('[median]') != -1) {
let priceDelta = priceDeltaRaw.indexOf('[') == -1 ? priceDeltaRaw : priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('['));
return Math.round(performOperation(getMedianPrice(prices), priceDelta));
} else {
let marketSlotOffset = priceDeltaRaw.indexOf('[') == -1 ? 0 : parseInt(priceDeltaRaw.substring(priceDeltaRaw.indexOf('[') + 1, priceDeltaRaw.indexOf(']')));
let priceDeltaWithoutMarketOffset = priceDeltaRaw.indexOf('[') == -1 ? priceDeltaRaw : priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('['));
return Math.round(performOperation(prices[Math.min(marketSlotOffset, prices.length - 1)].price, priceDeltaWithoutMarketOffset));
}
}
async function handleFillClick(event, itemId){
let target = event.currentTarget || event.target;
let priceInputs = target.parentNode.querySelectorAll('input.input-money');
recentFilledInput = priceInputs;
target.title = LOADING_THE_PRICES;
const popup = document.querySelector('.silmaril-market-filler-popup');
if (popup) {
const rect = target.getBoundingClientRect();
if (popupOffsetX == 0){
popupOffsetX = window.scrollX + rect.left - 300;
localStorage.setItem("silmaril-torn-market-filler-popup-offset-x", popupOffsetX);
}
popupOffsetY = window.scrollY + rect.top + 4;
let left = popupOffsetX;
let top = popupOffsetY;
popup.style.display = 'block';
popup.style.visibility = 'hidden';
popup.style.left = `${left}px`;
popup.style.top = `${top}px`;
popup.querySelector('.silmaril-market-filler-popup-body').innerHTML = LOADING_THE_PRICES;
const popupRect = popup.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
if (popupRect.right > viewportWidth) {
left = Math.max(0, viewportWidth - popupRect.width - 10 + scrollX);
}
if (popupRect.left < 0) {
left = 10 + scrollX;
}
if (popupRect.bottom > viewportHeight) {
top = Math.max(0, viewportHeight - popupRect.height - 10 + scrollY);
}
if (popupRect.top < 0) {
top = 10 + scrollY;
}
popup.style.left = `${left}px`;
popup.style.top = `${top}px`;
popup.style.visibility = 'visible';
}
let action = target.getAttribute('data-action-flag');
let prices = await GetPrices(itemId);
const breakdown = GetPricesBreakdown(prices);
target.title = breakdown;
// Thanks to Rosti [2840742] for the help with the prices popup component
showCustomFillPopup(target, breakdown);
let price = action == 'fill' ? GetPrice(prices) : '';
switchActionFlag(target);
let parentRow = findParentByCondition(target, (el) => String(el.className).indexOf('info___') > -1);
let quantityInputs = parentRow.querySelectorAll('[class^=amountInputWrapper___] .input-money-group > .input-money');
if (quantityInputs.length > 0){
if (quantityInputs[0].value.length === 0 || parseInt(quantityInputs[0].value) < 1){
quantityInputs[0].value = action == 'fill' ? Number.MAX_SAFE_INTEGER : 0;
quantityInputs[1].value = action == 'fill' ? Number.MAX_SAFE_INTEGER : 0;
} else {
quantityInputs[0].value = action == 'clear' ? '' : quantityInputs[0].value;
quantityInputs[1].value = action == 'clear' ? '' : quantityInputs[1].value;
}
quantityInputs[0].dispatchEvent(new Event("input", {bubbles: true}));
} else {
let checkbox = parentRow.querySelector('[class^=checkboxWrapper___] > [class^=checkboxContainer___] [type=checkbox]');
if (checkbox && ((action == 'fill' && !checkbox.checked) || (action == 'clear' && checkbox.checked))){
checkbox.click();
}
}
priceInputs.forEach(x => {x.value = price});
priceInputs[0].dispatchEvent(new Event("input", {bubbles: true}));
}
function hideAllFillPopups() {
document.querySelector('.silmaril-market-filler-popup').style.display = 'none';
}
function showCustomFillPopup(targetElem, contentHTML) {
const popup = document.querySelector('.silmaril-market-filler-popup');
popup.querySelector('.silmaril-market-filler-popup-body').innerHTML = contentHTML;
popup.querySelectorAll('.silmaril-torn-market-filler-popup-price').forEach(row => {
row.addEventListener('click', (e) => {
recentFilledInput.forEach(x => {x.value = parseInt(e.target.getAttribute('data-price')) - 1});
recentFilledInput[0].dispatchEvent(new Event("input", {bubbles: true}));
});
});
}
function addCustomFillPopup() {
const popup = document.createElement('div');
popup.className = 'silmaril-market-filler-popup';
popup.style.display = 'none';
popup.style.left = popupOffsetX + 'px';
popup.style.top = '0px';
popup.innerHTML = '<div class="silmaril-market-filler-popup-close" title="Close">×</div><b class="silmaril-market-filler-popup-draggable">Drag from here</b><br><div class="silmaril-market-filler-popup-body"></div>';
popup.querySelector('.silmaril-market-filler-popup-close').onclick = function(){ popup.style.display = 'none'; };
document.body.appendChild(popup);
const dragHandle = popup.querySelector('.silmaril-market-filler-popup-draggable');
dragHandle.addEventListener("mousedown", (e) => {
isDragging = true;
popupOffsetX = e.clientX - popup.offsetLeft;
popupOffsetY = e.clientY - popup.offsetTop;
});
document.addEventListener("mousemove", (e) => {
if (isDragging) {
popup.style.left = (e.clientX - popupOffsetX) + "px";
popup.style.top = (e.clientY - popupOffsetY) + "px";
}
});
document.addEventListener("mouseup", (e) => {
if (isDragging) {
popupOffsetX = e.clientX - popupOffsetX;
popupOffsetY = e.clientY - popupOffsetY;
localStorage.setItem("silmaril-torn-market-filler-popup-offset-x", popupOffsetX);
}
isDragging = false;
});
// Touch events (mobile)
dragHandle.addEventListener("touchstart", (e) => {
isDragging = true;
const touch = e.touches[0];
popupOffsetX = touch.clientX - popup.offsetLeft;
popupOffsetY = touch.clientY - popup.offsetTop;
e.preventDefault();
}, { passive: false });
document.addEventListener("touchmove", (e) => {
if (isDragging) {
const touch = e.touches[0];
popup.style.left = (touch.clientX - popupOffsetX) + "px";
popup.style.top = (touch.clientY - popupOffsetY) + "px";
}
}, { passive: false });
document.addEventListener("touchend", () => {
if (isDragging) {
popupOffsetX = popup.offsetLeft;
popupOffsetY = popup.offsetTop;
localStorage.setItem("silmaril-torn-market-filler-popup-offset-x", popupOffsetX);
}
isDragging = false;
});
}
function getItemIdFromString(string){
const match = string.match(/-(\d+)-/);
if (match) {
const number = match[1];
return number;
} else {
console.error("[TornMarketFiller] ItemId not found!");
return -1;
}
}
function getItemIdFromImage(image){
let numberPattern = /\/(\d+)\//;
let match = image.src.match(numberPattern);
if (match) {
return parseInt(match[1], 10);
} else {
console.error("[TornMarketFiller] ItemId not found!");
return -1;
}
}
function switchActionFlag(target){
switch (target.getAttribute('data-action-flag')){
case 'fill':
target.setAttribute('data-action-flag', 'clear');
break;
case 'clear':
default:
target.setAttribute('data-action-flag', 'fill');
break;
}
}
function findParentByCondition(element, conditionFn){
let currentElement = element;
while (currentElement !== null) {
if (conditionFn(currentElement)) {
return currentElement;
}
currentElement = currentElement.parentElement;
}
return null;
}
function setPriceDelta() {
let userInput = prompt('Enter price delta formula (default: -1[0]):', priceDeltaRaw);
if (userInput !== null) {
priceDeltaRaw = userInput;
localStorage.setItem("silmaril-torn-market-filler-price-delta", userInput);
} else {
console.error("[TornMarketFiller] User cancelled the Price Delta input.");
}
}
function GetPricesBreakdown(prices){
if (prices == null) return "No prices loaded";
if (prices[0] === undefined){
prices = Array(prices);
}
const sb = new StringBuilder();
for (let i = 0; i < Math.min(prices.length, 5); i++){
if(typeof prices[i] !== "object" || prices[i].amount === undefined || prices[i].price === undefined) continue;
sb.append(`<span class="silmaril-torn-market-filler-popup-price" data-price=${prices[i].price}>${prices[i].amount} x ${formatNumberWithCommas(prices[i].price)} (${formatNumberWithCommas(Math.round(prices[i].price * marketTaxFactor))})</span>`);
if (i < Math.min(prices.length, 5)-1){
sb.append('<br>');
}
}
return sb.toString();
}
function performOperation(number, operation) {
const match = operation.match(/^([-+]?)(\d+(?:\.\d+)?)(%)?$/);
if (!match) {
throw new Error('Invalid operation string');
}
const [, operator, operand, isPercentage] = match;
const operandValue = parseFloat(operand);
const adjustedOperand = isPercentage ? (number * operandValue) / 100 : operandValue;
switch (operator) {
case '':
case '+':
return number + adjustedOperand;
case '-':
return number - adjustedOperand;
default:
throw new Error('Invalid operator');
}
}
function formatNumberWithCommas(number) {
return new Intl.NumberFormat('en-US').format(number);
}
function checkApiKey(checkExisting = true) {
if (!checkExisting || apiKey === null || apiKey.indexOf('PDA-APIKEY') > -1 || apiKey.length != 16){
let userInput = prompt("Please enter a PUBLIC Api Key, it will be used to get current bazaar prices:", apiKey ?? '');
if (userInput !== null && userInput.length == 16) {
apiKey = userInput;
localStorage.setItem("silmaril-torn-bazaar-filler-apikey", userInput);
} else {
console.error("[TornMarketFiller] User cancelled the Api Key input.");
}
}
}
function getMedianPrice(items) {
const prices = items.flatMap(item => Array(item.amount).fill(item.price));
prices.sort((a, b) => a - b);
const mid = Math.floor(prices.length / 2);
if (prices.length % 2 === 0) {
return (prices[mid - 1] + prices[mid]) / 2;
} else {
return prices[mid];
}
}
function getCurrentPage(){
if (window.location.href.indexOf('#/addListing') > -1){
return pages.AddItems;
} else if (window.location.href.indexOf('#/viewListing') > -1){
return pages.ViewItems;
} else {
return pages.Other;
}
}
function getCurrentMarketTax() {
const taxFivePercentDate = new Date(2025, 5, 22);
const taxFourPercentDate = new Date(2025, 4, 22);
const taxThreePercentDate = new Date(2025, 3, 22);
const taxTwoPercentDate = new Date(2025, 2, 22);
const taxOnePercentDate = new Date(2025, 1, 22);
const today = parseDate(getTornToday());
switch (true) {
case today >= taxFivePercentDate: return 0.05;
case today >= taxFourPercentDate: return 0.04;
case today >= taxThreePercentDate: return 0.03;
case today >= taxTwoPercentDate: return 0.02;
case today >= taxOnePercentDate: return 0.01;
default: return 0.00;
}
}
function getTornToday() {
const now = document.querySelector('span.server-date-time').textContent.split(' ');
return now[now.length - 1];
}
function parseDate(str) {
const [dd, mm, yy] = str.split('/').map(Number);
const fullYear = yy < 50 ? 2000 + yy : 1900 + yy;
return new Date(fullYear, mm - 1, dd);
}
const startHold = () => {
holdTimer = setTimeout(() => {
setPriceDelta();
checkApiKey(false);
}, 2000);
};
const cancelHold = () => {
clearTimeout(holdTimer);
};
class StringBuilder {
constructor() {
this.parts = [];
}
append(str) {
this.parts.push(str);
return this;
}
toString() {
return this.parts.join('');
}
}
})();