Add quality and bonus percentages to the item market, auction house, and bazaars.
// ==UserScript==
// @name OpenMarket
// @namespace https://greasyfork.org/en/scripts/571158-openmarket
// @version 1.0.0
// @description Add quality and bonus percentages to the item market, auction house, and bazaars.
// @author https://www.torn.com/forums.php#p=threads&f=67&t=16469095
// @match https://www.torn.com/page.php?sid=ItemMarket*
// @match https://www.torn.com/amarket.php*
// @match https://www.torn.com/bazaar.php*
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant none
// @run-at document-start
// @license GNU GPLv3
// ==/UserScript==
(function () {
'use strict';
// Choose which pages the script is active on.
// Change "true" to "false" to disable a page.
const CONFIG = {
enableItemMarket: true,
enableAuctionHouse: true,
enableBazaar: true
};
// You can customise the colours however you want. Tiers for quality are lowest to highest.
const COLOURS = {
tiers: {
dark: ['#e4e4e4', '#57efea', '#c286ff', '#ffd700'],
light: ['#717171', '#009590', '#8e19c1', '#e37100']
},
bonus: {
dark: '#e8d1ff',
light: '#370b40'
},
qualityBoxBg: {
dark: 'rgba(0,0,0,0.8)',
light: 'rgba(255,255,255,0.9)'
}
};
// These are the various elements that need to be found/selected within the page source.
// Changes to the game may break these, so finding the new values may be a necessary part
// of debugging the script. If an element is broken, you can right click on it and "inspect"
// to get to the HTML/CSS. This may lead you to the right place, or you might need to dig around.
const SELECTORS = {
itemMarket: {
root: '#item-market-root',
tile: '.itemTile___cbw7w',
imageWrapper: '.imageWrapper___RqvUg',
title: 'div > div.title___bQI0h',
price: '.priceAndTotal___eEVS7 span',
statsContainer: '.properties___QCPEP',
statEntry: '.property___SHm8e',
statIcon: '.icon___ThfN8',
statValue: '.value___cwqHv',
statTypeElement: 'value',
statTypeAttribute: 'aria-label',
bonusIcons: '.bonuses___a8gmz i'
},
auctionHouse: {
root: '#auction-house-tabs',
visibleTab: '#auction-house-tabs .tabContent:not([style*="display: none"])',
tile: 'ul.items-list li',
imageWrapper: '.img-wrap',
title: '.title',
itemHover: '.item-hover',
rarityLine: 'p.t-gray-6',
statsContainer: '.infobonuses',
statEntry: 'span.bonus-attachment',
statIcon: 'i',
statValue: '.label-value',
statTypeElement: 'icon',
statTypeAttribute: 'className',
bonusIcons: '.iconsbonuses .bonus-attachment-icons',
bonusTitleAttr: 'title',
bonusDescAttr: null
},
bazaar: {
root: '#bazaarRoot',
itemsContainer: 'div[class*="itemsContainner"]',
tile: '.item___GYCYJ.item___khvF6',
imageWrapper: '.imgBar___Dbu1b',
image: '.imgContainer___Ec4I5 img',
title: '.description___Y2Nrl',
stockElement: '.amount___K8sOQ',
statsContainer: '.infoBonuses___g8QdG',
statEntry: '.container___Go0NJ',
statIcon: 'i',
statValue: 'span',
statTypeElement: 'icon',
statTypeAttribute: 'className',
bonusIcons: '.iconBonuses____iFjZ i',
bonusTitleAttr: 'data-bonus-attachment-title',
bonusDescAttr: 'data-bonus-attachment-description'
}
};
// When new items are added, or if values are updated, these three sections will need to be adjusted.
// The first number is the item ID. You can find that by searching the item name in the wiki.
// https://wiki.torn.com/
// For example, the item with ID 1 is the hammer, and the wiki shows that its base damage is 17
// and its base accuracy is 55.
// You can check for new items at tornstats by going to items and selecting the last page.
// https://tornstats.com/items
// You can cross reference them with the wiki. Please note, the wiki values are regularly wrong.
// You may have to figure out the actual base values by looking for items on the market, and/or
// doing some maths.
const BASE_DAMAGE_VALUES = {
1: 17, 2: 16, 3: 20, 4: 11, 5: 21, 6: 25, 7: 28, 8: 34, 9: 40, 10: 61,
11: 58, 12: 28, 13: 29, 14: 32, 15: 36, 16: 44, 17: 48, 18: 52, 19: 55, 20: 59,
21: 64, 22: 41, 23: 39, 24: 45, 25: 48, 26: 56, 27: 55, 28: 59, 29: 61, 30: 64,
31: 67, 63: 72, 76: 52, 98: 59, 99: 33, 100: 64, 108: 65, 109: 77, 110: 27, 111: 39,
146: 65, 147: 22, 170: 60, 173: 24, 174: 50, 175: 1, 177: 61, 189: 42, 217: 57, 218: 35,
219: 63, 223: 69, 224: 23, 225: 56, 227: 38, 228: 50, 230: 18, 231: 60, 232: 62, 233: 61,
234: 31, 235: 22, 236: 35, 237: 62, 238: 29, 240: 78, 241: 50, 243: 30, 244: 15, 245: 13,
247: 52, 248: 62, 249: 46, 250: 50, 251: 53, 252: 49, 253: 27, 254: 47, 255: 67, 289: 70,
290: 70, 291: 70, 292: 70, 346: 40, 359: 16, 360: 53, 382: 75, 387: 67, 388: 74, 391: 57,
393: 14, 395: 61, 397: 71, 398: 69, 399: 68, 400: 63, 401: 26, 402: 51, 438: 18, 439: 19,
440: 1, 483: 42, 484: 46, 485: 40, 486: 38, 487: 39, 488: 37, 489: 35, 490: 46, 539: 36,
545: 79, 546: 76, 547: 78, 548: 77, 549: 80, 599: 60, 600: 61, 604: 43, 605: 45, 612: 65,
613: 47, 614: 60, 615: 64, 632: 48, 790: 5, 792: 17, 805: 18, 830: 95, 831: 54, 832: 21,
837: 66, 838: 63, 839: 60, 844: 15, 845: 58, 846: 56, 850: 58, 871: 5, 874: 68, 1053: 41,
1055: 35, 1056: 40, 1152: 76, 1153: 74, 1154: 73, 1155: 70, 1156: 68, 1157: 69, 1158: 62,
1159: 51, 1173: 37, 1231: 29, 1255: 54, 1257: 1, 1296: 27
};
const BASE_ACCURACY_VALUES = {
1: 55, 2: 57, 3: 52, 4: 62, 5: 45, 6: 55, 7: 60, 8: 52, 9: 58, 10: 23,
11: 52, 12: 53, 13: 52, 14: 56, 15: 54, 16: 58, 17: 51, 18: 49, 19: 38, 20: 36,
21: 30, 22: 63, 23: 65, 24: 51, 25: 51, 26: 52, 27: 47, 28: 55, 29: 47, 30: 45,
31: 41, 63: 28, 76: 24, 98: 24, 99: 57, 100: 24, 108: 43, 109: 39, 110: 52, 111: 51,
146: 49, 147: 15, 170: 24, 173: 55, 174: 56, 175: 54, 177: 53, 189: 54, 217: 49, 218: 63,
219: 55, 223: 52, 224: 52, 225: 62, 227: 48, 228: 48, 230: 22, 231: 46, 232: 50, 233: 55,
234: 52, 235: 59, 236: 55, 237: 56, 238: 52, 240: 25, 241: 57, 243: 57, 244: 39, 245: 55,
247: 55, 248: 53, 249: 47, 250: 53, 251: 51, 252: 62, 253: 41, 254: 52, 255: 39, 289: 54,
290: 54, 291: 54, 292: 54, 346: 63, 359: 50, 360: 57, 382: 62, 387: 63, 388: 45, 391: 65,
393: 54, 395: 60, 397: 28, 398: 50, 399: 57, 400: 35, 401: 33, 402: 60, 438: 42, 439: 43,
440: 63, 483: 52, 484: 41, 485: 54, 486: 45, 487: 43, 488: 41, 489: 48, 490: 24, 539: 55,
545: 38, 546: 47, 547: 46, 548: 45, 549: 36, 599: 48, 600: 41, 604: 45, 605: 48, 612: 52,
613: 63, 614: 62, 615: 52, 632: 48, 790: 29, 792: 57, 805: 55, 830: 45, 831: 53, 832: 54,
837: 36, 838: 60, 839: 45, 844: 45, 845: 53, 846: 52, 850: 50, 871: 59, 874: 57, 1053: 65,
1055: 49, 1056: 47, 1152: 42, 1153: 44, 1154: 40, 1155: 45, 1156: 36, 1157: 49, 1158: 39,
1159: 56, 1173: 67, 1231: 59, 1255: 52, 1257: 59, 1296: 58
};
const BASE_ARMOUR_VALUES = {
32: 20, 33: 32, 34: 34, 49: 31, 50: 36, 176: 23, 178: 30, 332: 38, 333: 40, 334: 42,
348: 10, 538: 25, 640: 32, 641: 34, 642: 30, 643: 30, 644: 34, 645: 30, 646: 24, 647: 20,
648: 20, 649: 20, 650: 20, 651: 38, 652: 38, 653: 38, 654: 38, 655: 35, 656: 45, 657: 45,
658: 45, 659: 45, 660: 44, 661: 44, 662: 44, 663: 44, 664: 44, 665: 46, 666: 46, 667: 46,
668: 46, 669: 46, 670: 49, 671: 49, 672: 49, 673: 49, 674: 49, 675: 40, 676: 52, 677: 52,
678: 52, 679: 52, 680: 55, 681: 55, 682: 55, 683: 55, 684: 55, 848: 32, 1164: 38, 1165: 50,
1166: 50, 1167: 50, 1168: 50, 1174: 39, 1307: 53, 1308: 53, 1309: 53, 1310: 53, 1311: 53,
1355: 48, 1356: 48, 1357: 48, 1358: 48, 1359: 48
};
// The following const is a map matching all item IDs to the damage, accuracy and armour values.
// Weapons have damage and accuracy, armour just has armour. The script accounts for that by giving 0
// to the missing values, so it won't have any effect on calculating bonus %. If some kind of wacky item
// is added in the future which somehow has defensive and offensive stats, it could break this.
const BASE_STATS_MAP = Object.keys({
...BASE_DAMAGE_VALUES,
...BASE_ACCURACY_VALUES,
...BASE_ARMOUR_VALUES
}).reduce((map, id) => {
map[id] = {
baseDamage: BASE_DAMAGE_VALUES[id] ?? 0,
baseAccuracy: BASE_ACCURACY_VALUES[id] ?? 0,
baseArmour: BASE_ARMOUR_VALUES[id] ?? 0
};
return map;
}, {});
// Some bonuses have a fixed effect without any varying number. These tend to be weapons
// which were released before ranked war weapons were introduced.
// They are currently sledgehammer, tranquilizer gun, and handbag.
// Some may be missing, and some new things may need to be added here in future.
const FIXED_BONUSES = ['Smash', 'Sleep', 'Storage'];
const NUMBER_REGEX = /(\d+(?:\.\d+)?)/;
const processedElements = new WeakSet();
let darkMode = false;
let currentHandler = null;
// FUNCTIONS WHICH MAY NEED DEBUGGING ACCORDING TO GAME UDPDATES BELOW,
// OR FUNCTIONS WHICH CAN BE ADJUSTED TO PREFERENCE
// This checks the page source to see if it's in dark mode or not. This could changed/renamed.
function checkDarkMode() {
darkMode = document.body.classList.contains('dark-mode');
}
// These percentages can be adjusted to change when quality is considered a higher "tier".
function getTierColour(percent) {
const colours = darkMode ? COLOURS.tiers.dark : COLOURS.tiers.light;
if (percent <= 25) return colours[0];
if (percent <= 50) return colours[1];
if (percent <= 75) return colours[2];
return colours[3];
}
// Helper function for selecting a specific element from the DOM.
function qs(selector, parent = document) {
try {
return parent.querySelector(selector);
} catch (e) {
return null;
}
}
// Helper function for selecting all instances of a specific element from the DOM.
function qsa(selector, parent = document) {
try {
return Array.from(parent.querySelectorAll(selector));
} catch (e) {
return null;
}
}
// This is a generic helper function for creating DOM elements with children.
// Search "create(" for examples of its usage.
function create(tag, attrs = {}, children = []) {
const el = document.createElement(tag);
Object.entries(attrs).forEach(([key, value]) => {
if (key === 'style' && typeof value === 'object') {
Object.assign(el.style, value);
} else if (key === 'dataset' && typeof value === 'object') {
Object.entries(value).forEach(([k, v]) => el.dataset[k] = v);
} else {
el[key] = value;
}
});
children.forEach(child => {
if (typeof child === 'string') {
el.appendChild(document.createTextNode(child));
} else if (child) {
el.appendChild(child);
}
});
return el;
}
// processedElements is a set of elements which have been processed, for avoiding
// duplicates. Necessary now since the script keeps a cache of items.
function isProcessed(el) {
return processedElements.has(el);
}
function markProcessed(el) {
processedElements.add(el);
}
// This returns the "tier" for the item's quality, which will vary for weapons vs armour.
// The maxRange is 100% for armour and 300% for weapons, unless they are adjusted.
// The tier colour can also be adjusted, as well as which percent it falls under.
function getQualityColour(value, maxRange) {
const percent = (value / maxRange) * 100;
return getTierColour(percent);
}
function getBonusColour() {
return darkMode ? COLOURS.bonus.dark : COLOURS.bonus.light;
}
// This is a global stat extraction function which works for the item market, bazaar
// and auction house. This should be fairly future proof if the names of the HTML
// elements change. You can just fix the SELECTORS at the top of the script.
function extractStats(el, selectors) {
let damage = 0, accuracy = 0, armour = 0;
// Check if the container exists. If you're getting negative values
// for quality, the general structure of the item tile elements may have changed.
const container = qs(selectors.statsContainer, el);
if (!container) return { damage, accuracy, armour };
// This whole thing has to be a bit messy because there's no consistency across pages.
qsa(selectors.statEntry, container).forEach(entry => {
const valueEl = qs(selectors.statValue, entry);
const iconEl = qs(selectors.statIcon, entry);
const val = parseFloat(valueEl?.textContent) || 0;
const typeEl = selectors.statTypeElement === 'value' ? valueEl : iconEl;
let typeText = '';
if (selectors.statTypeAttribute === 'className') {
typeText = (typeEl?.className || '').toLowerCase();
} else {
typeText = (typeEl?.getAttribute(selectors.statTypeAttribute) || '').toLowerCase();
}
if (typeText.includes('damage')) damage = val;
else if (typeText.includes('accuracy')) accuracy = val;
// Sometimes the game calls it "armor" and sometimes it calls it "defence".
// They are not sure if they're American or British.
else if (typeText.includes('armor') || typeText.includes('defence')) armour = val;
});
return { damage, accuracy, armour };
}
// The auction house and bazaar store bonus information in the DOM, so we can
// extract it with a shared function.
function extractBonusesFromDOM(el, selectors) {
const bonuses = [];
qsa(selectors.bonusIcons, el).forEach(icon => {
// Auction house gets bonus name + percent from "title", bazaar gets it from
// title AND description. If one of these pages breaks but not the other,
// the elements might need to be changed in SELECTORS, or this function adjusted.
const titleAttr = icon.getAttribute(selectors.bonusTitleAttr) || '';
const descAttr = icon.getAttribute(selectors.bonusDescAttr) || '';
if (!titleAttr && !descAttr) return;
const tmp = create('div');
tmp.innerHTML = titleAttr;
const title = qs('b', tmp)?.textContent || tmp.textContent || '';
const descText = tmp.textContent.replace(title, '').trim() || descAttr;
const valueMatch = descText.match(NUMBER_REGEX);
const value = valueMatch ? parseFloat(valueMatch[1]) : null;
if (title) {
bonuses.push({ title, value, description: descText });
}
});
return bonuses;
}
// Each page tends to have wildly differing ways of storing the info,
// but there are a few consistent aspects which can be grouped in this
// function.
function processItem(el, data) {
if (isProcessed(el)) return;
const { itemID, stats, bonuses, selectors } = data;
// Calculate quality.
const quality = calcQuality(itemID, stats.damage, stats.accuracy, stats.armour);
if (quality === null) return;
// Get title element for bonus insertion.
const titleEl = qs(selectors.title, el);
if (!titleEl) return;
// Remove existing bonus container if present.
const existing = qs('.openmarket-bonuses', el);
if (existing) existing.remove();
// Create bonus container.
const bonusContainer = create('div', {
className: 'openmarket-bonuses',
style: { lineHeight: '1' }
});
// Add bonus elements to container.
bonuses.forEach(bonus => {
bonusContainer.appendChild(createBonusEl(bonus));
});
// Insert bonus container.
if (bonusContainer.children.length > 0) {
if (data.insertBefore) {
titleEl.insertBefore(bonusContainer, data.insertBefore);
} else {
titleEl.appendChild(bonusContainer);
}
}
// Hide elements if specified.
(data.hideElements || []).forEach(hideEl => {
if (hideEl) hideEl.style.display = 'none';
});
// Insert quality box on image wrapper.
const imageWrapper = qs(selectors.imageWrapper, el);
const isArmour = stats.armour > 0;
insertQualityBox(imageWrapper, quality, isArmour ? 100 : 300);
markProcessed(el);
}
function getQualityBoxBg() {
return darkMode ? COLOURS.qualityBoxBg.dark : COLOURS.qualityBoxBg.light;
}
// Quality is not a value provided by the game.
// So this needs to be calculated by using the damage/accuracy/armour values.
// It's not perfectly accurate.
// Perhaps in the future, proper quality values will be given, and this can be removed.
function calcQuality(itemID, damage, accuracy, armour) {
const base = BASE_STATS_MAP[itemID];
if (!base) return null;
if (armour && armour !== 0) {
return ((armour - base.baseArmour) * 20).toFixed(1);
}
return (((damage - base.baseDamage) + (accuracy - base.baseAccuracy)) * 10).toFixed(1);
}
// A simple check to see if the value of the bonus has a percent after it
// in the description. So it doesn't return something like "Disarm: 5%".
// This could be expanded to include more info, like ' turns'.
function getUnitFromDescription(description, value) {
if (!description || value === undefined || value === null) return '';
const pattern = new RegExp(String(value) + '\\s*%');
if (pattern.test(description)) return '%';
return '';
}
// This is a helper function which creates the small element holding the bonus info.
// If the way item info is received changes, this could break. Also new unusual
// items may potentially not be able to have their info extracted properly.
function createBonusEl(bonus) {
const name = bonus.title || '';
const value = bonus.value;
const description = bonus.description || '';
// If this is a "fixed" bonus, just put the name there.
if (FIXED_BONUSES.includes(name)) {
return create('div', {
textContent: name,
style: { color: getBonusColour() }
});
}
let text = name;
// Use the getUnitFromDescription function to see if the description
// simply has a "%" after the number. That function was destined for
// bigger things...
if (value !== undefined && value !== null) {
const unit = getUnitFromDescription(description, value);
text = `${name}: ${value}${unit}`;
}
return create('div', {
textContent: text,
style: { color: getBonusColour() }
});
}
// Helper function for putting the quality element on the item image.
function insertQualityBox(container, value, maxRange) {
if (!container || qs('.openmarket-quality-box', container)) return;
const colour = getQualityColour(parseFloat(value), maxRange);
// Child element containing the text.
const span = create('span', {
className: 'label-value t-overflow',
textContent: `Q ${value}%`
});
// This is the box/background. These numbers can be adjusted
// to change the styling. This could be removed entirely but
// it ensures the text is easy to read.
const box = create('div', {
className: 'openmarket-quality-box',
style: {
position: 'absolute',
top: '2px',
left: '2px',
padding: '1px 3px',
borderRadius: '3px',
fontSize: '11px',
fontWeight: 'bold',
zIndex: '2',
background: getQualityBoxBg(),
color: colour
}
}, [span]);
// I don't remember what problem this solved, perhaps a mobile/PDA thing.
const style = window.getComputedStyle(container);
if (style.position === 'static') {
container.style.position = 'relative';
}
container.appendChild(box);
}
// This is a simple check if the tab/window is in focus.
// According to the rules, only a single tab can have its info used by a script.
// !document.hidden checks if the current TAB is visible.
// document.hasFocus() checks if the current WINDOW has focus.
// From what I can tell, these both need to be included to abide by the rules.
function isPageActive() {
return !document.hidden && document.hasFocus();
}
// This sets up listeners for tab visibility and window focus events,
// runs processing when appropriate.
function setupActiveListeners() {
const onActivate = () => {
if (currentHandler) {
currentHandler.processAll();
}
};
document.addEventListener('visibilitychange', onActivate);
window.addEventListener('focus', onActivate);
}
// ITEM MARKET
const itemMarket = {
// When items are loaded on the item market, the script intercepts a fetch request
// which contains the info for the page to display them. It only contains info
// for newly added items, and since the market redraws everything, old item info
// needs to be kept in a cache. This was a recent change to Torn,
// and it could change again.
// NOTE: The legality of intercepting fetch requests for unviewed tabs was questioned.
// The Torn admin team deemed storing info in a cache is okay if it is not used in any
// way until the appropriate tab is viewed.
// Direct quote: "We agreed that it's fine to go ahead with your plan to keep it in cache,
// just as long as it's not persisted anywhere/repurposed for anything else"
// If you modify this script it is your responsibility to ensure this behaviour remains
// compliant, or there is a potential risk of account deletion.
cache: [],
selectors: SELECTORS.itemMarket,
// Check to see if the stats and price of an item matches what's listed in the cache.
// It's not impossible that items with identical stats and price with different bonuses
// could be mixed up. That seems like a very rare occurence, though.
matchToCache(el) {
// Convert the price into a plain integer. If processing isn't working on the item market
// only, the selectors could have changed.
const price = parseInt(qs(this.selectors.price, el)?.textContent.replace(/[^\d]/g, ''), 10) || 0;
const stats = extractStats(el, this.selectors);
return this.cache.find(item => {
if (item.armor && item.armor > 0) {
return item.minPrice === price && item.armor === stats.armour;
}
return item.minPrice === price && item.damage === stats.damage && item.accuracy === stats.accuracy;
});
},
processAll() {
// Per the rules, only process items if the page is currently visible AND in focus.
if (!isPageActive()) return;
if (this.cache.length === 0) return;
// Tile elements could have been changed by an update if item market isn't working.
qsa(this.selectors.tile).forEach(el => {
const cached = this.matchToCache(el);
if (!cached) return;
processItem(el, {
itemID: cached.itemID,
stats: {
damage: cached.damage || 0,
accuracy: cached.accuracy || 0,
armour: cached.armor || 0
},
bonuses: cached.bonuses || [],
selectors: this.selectors
});
});
},
init() {
const root = qs(this.selectors.root);
if (!root) return;
// Scrolling the page loads in new items, so check for DOM changes and re-process.
const observer = new MutationObserver(() => {
this.processAll();
});
// Observe just the root element of the item market, to avoid excess reprocessing.
// This could probably be made more specific, as to not
// run when unnecessary (eg only reprocessing when the cache is updated).
observer.observe(root, { childList: true, subtree: true });
this.processAll();
}
};
// AUCTION HOUSE
const auctionHouse = {
selectors: SELECTORS.auctionHouse,
processAll() {
if (!isPageActive()) return;
const visibleTab = qs(this.selectors.visibleTab);
if (!visibleTab) return;
qsa(this.selectors.tile, visibleTab).forEach(el => {
// If the auction house isn't working, it could be a selector in this section.
const hoverEl = qs(this.selectors.itemHover, el);
// If a specific item doesn't show up, it could be that the ID isn't in the map yet.
const itemID = hoverEl?.getAttribute('item');
if (!itemID || !BASE_STATS_MAP[itemID]) return;
// Or something went wrong with finding stats, and it returns 0 for all.
const stats = extractStats(el, this.selectors);
if (stats.damage === 0 && stats.accuracy === 0 && stats.armour === 0) return;
const titleEl = qs(this.selectors.title, el);
const rarityLine = qs(this.selectors.rarityLine, titleEl);
processItem(el, {
itemID,
stats,
bonuses: extractBonusesFromDOM(el, this.selectors),
selectors: this.selectors,
hideElements: [rarityLine] // Hide the rarity for tidiness.
});
});
},
init() {
// This function is the same as the item market, but instead of watching for vertical scrolls,
// this checks for going to a new page or switching to a different category.
// The observer is set off each time the counter ticks down,
// but it's only once a second, so whatever.
const root = qs(this.selectors.root);
if (!root) return;
const observer = new MutationObserver(() => {
this.processAll();
});
observer.observe(root, { childList: true, subtree: true });
this.processAll();
}
};
// BAZAAR
const bazaar = {
selectors: SELECTORS.bazaar,
processAll() {
if (!isPageActive()) return;
qsa(this.selectors.tile).forEach(el => {
// Item ID is extracted from each item's image URL and filtered with regex.
const img = qs(this.selectors.image, el);
if (!img) return;
const match = img.src.match(/\/images\/items\/(\d+)\//);
if (!match) return;
const itemID = match[1];
if (!BASE_STATS_MAP[itemID]) return;
const stats = extractStats(el, this.selectors);
if (stats.damage === 0 && stats.accuracy === 0 && stats.armour === 0) return;
const titleEl = qs(this.selectors.title, el);
const stockEl = qs(this.selectors.stockElement, titleEl);
// For the bazaar, insert above the stock element, then hide stock for tidiness.
// There's no reason for Torn to tell us there's 1 in stock for these items anyway.
processItem(el, {
itemID,
stats,
bonuses: extractBonusesFromDOM(el, this.selectors),
selectors: this.selectors,
insertBefore: stockEl,
hideElements: [stockEl]
});
});
},
init() {
const root = qs(this.selectors.root);
if (!root) return;
// The outer observer waits for the bazaar in general to load, the inner observer
// then checks for changes due to scrolling, similar to the item market.
// One observer would probably work fine, but I think there was a reason for this
// at some point. Maybe the extra logic was related to a now fixed bazaar bug with
// large images.
const outerObserver = new MutationObserver((_, obs) => {
const container = qs(this.selectors.itemsContainer, root);
if (!container) return;
const innerObserver = new MutationObserver(() => {
this.processAll();
});
innerObserver.observe(container, { childList: true, subtree: true });
this.processAll();
obs.disconnect();
});
outerObserver.observe(root, { childList: true, subtree: true });
}
};
// FETCH REQUEST INTERCEPTION
function interceptFetch() {
const originalFetch = window.fetch;
// Override the original fetch function to intercept requests.
window.fetch = async (...args) => {
const response = await originalFetch(...args);
// If the URL in the request shows it's related to the item market,
// add items to the cache, checking itemID for dupes.
// If the item market URL changes to no longer contain 'sid=iMarket',
// this could break. This could be adjusted to something more generic
// like 'market' to future-proof it, also could be added to SELECTORS.
const url = args[0] instanceof Request ? args[0].url : String(args[0]);
if (url.includes('sid=iMarket')) {
response.clone().json().then(data => {
if (!data?.items?.length) return;
const existingIds = new Set(itemMarket.cache.map(i => i.listingID));
const newItems = data.items.filter(item => !existingIds.has(item.listingID));
if (newItems.length > 0) {
itemMarket.cache.push(...newItems);
if (isPageActive()) itemMarket.processAll();
}
}).catch(() => { });
}
return response;
};
}
// STYLES
function insertStyles() {
// A handful of page elements need to have some CSS adjusted for a tidier fit.
// Things could be adjusted further if changes to the page layouts are made,
// or if there are conflicts with other scripts. Or just for nicer aesthetics.
const css = `
.itemTile___cbw7w {
padding: 5px 3px 0 !important;
}
.itemTile___cbw7w .title___bQI0h {
padding: 5px 0 0px !important;
}
.openmarket-bonuses div {
font-size: 11px !important;
}
`;
const style = document.createElement('style');
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
}
// INITIALISATION
function init() {
// Insert the style changes and override the fetch function before the page loads.
insertStyles();
interceptFetch();
// Once the DOM is fully loaded, run the script.
document.addEventListener('DOMContentLoaded', () => {
checkDarkMode();
setupActiveListeners();
const href = window.location.href;
if (href.includes('sid=ItemMarket') && CONFIG.enableItemMarket) {
currentHandler = itemMarket;
itemMarket.init();
} else if (href.includes('amarket.php') && CONFIG.enableAuctionHouse) {
currentHandler = auctionHouse;
auctionHouse.init();
} else if (href.includes('bazaar.php') && CONFIG.enableBazaar) {
currentHandler = bazaar;
bazaar.init();
}
// Here, more pages can be added.
// TODO: Add various pages and simply match with https://www.torn.com/* since
// this checks the URL anyway. But URL check should be moved to the very first thing
// the script does.
});
}
init();
})();