Show only selected Bazaar items and display owned quantities from Museum
// ==UserScript==
// @name Torn Bazaar Filter
// @namespace torn-bazaar-filter
// @version 1.3.2
// @description Show only selected Bazaar items and display owned quantities from Museum
// @match https://www.torn.com/bazaar*
// @match https://www.torn.com/page.php?sid=ItemMarket*
// ==/UserScript==
(function () {
'use strict';
/************************************************************
* CONFIGURATION
************************************************************/
const FILTER_ITEMS = [
{ id: 187, name: 'Teddy Bear' },
{ id: 186, name: 'Sheep' },
{ id: 215, name: 'Kitten' },
{ id: 261, name: 'Wolverine' },
{ id: 618, name: 'Stingray' },
{ id: 258, name: 'Jaguar' },
{ id: 273, name: 'Chamois' },
{ id: 268, name: 'Red Fox' },
{ id: 266, name: 'Nessie' },
{ id: 269, name: 'Monkey' },
{ id: 274, name: 'Panda' },
{ id: 281, name: 'Lion' },
{ id: 384, name: 'Camel' },
];
const FILTER_SET = new Set(
FILTER_ITEMS.map(item => item.id)
);
/************************************************************
* PERSISTENT STATE
************************************************************/
const STORAGE_MINIMIZED =
'tbf-panel-minimized';
const STORAGE_FILTER_ENABLED =
'tbf-filter-enabled';
let filterEnabled =
localStorage.getItem(
STORAGE_FILTER_ENABLED
) !== 'false';
let panelMinimized =
localStorage.getItem(
STORAGE_MINIMIZED
) === 'true';
/************************************************************
* RUNTIME STATE
************************************************************/
let panel = null;
let observer = null;
let filterTimeout = null;
let museumIframe = null;
// itemId -> { amount, name }
const inventory = new Map();
/************************************************************
* INITIALIZE INVENTORY
*
* Every configured item starts at 0.
* Museum results overwrite the ones actually owned.
************************************************************/
function initializeInventory() {
inventory.clear();
for (const item of FILTER_ITEMS) {
inventory.set(item.id, {
amount: 0,
name: item.name
});
}
}
/************************************************************
* LOAD MUSEUM
*
* Uses a hidden iframe so Torn's own JavaScript can run.
************************************************************/
function loadMuseumInventory() {
initializeInventory();
console.log(
'[TBF] Loading Museum...'
);
// Remove previous iframe if one exists.
if (museumIframe) {
try {
museumIframe.remove();
} catch (e) {
// Ignore
}
museumIframe = null;
}
const iframe =
document.createElement('iframe');
museumIframe = iframe;
iframe.src =
'https://www.torn.com/museum.php';
Object.assign(
iframe.style,
{
position: 'fixed',
width: '1px',
height: '1px',
left: '-10000px',
top: '-10000px',
opacity: '0',
pointerEvents: 'none',
border: '0'
}
);
document.body.appendChild(iframe);
iframe.addEventListener(
'load',
function () {
console.log(
'[TBF] Museum iframe loaded'
);
/*
* Museum content may be populated
* asynchronously by Torn.
*
* Check every 500ms for up to 10 seconds.
*/
let attempts = 0;
const checkMuseum =
setInterval(
function () {
attempts++;
try {
const museumDocument =
iframe.contentDocument ||
iframe.contentWindow.document;
const museumItems =
museumDocument.querySelectorAll(
'.item-wrapper[itemid]'
);
console.log(
`[TBF] Museum check ${attempts}: ${museumItems.length} items`
);
if (
museumItems.length > 0 ||
attempts >= 20
) {
clearInterval(
checkMuseum
);
processMuseum(
museumDocument
);
/*
* We don't need the iframe anymore.
*/
setTimeout(
() => {
if (
museumIframe === iframe
) {
iframe.remove();
museumIframe =
null;
}
},
100
);
}
} catch (error) {
console.error(
'[TBF] Error reading Museum:',
error
);
if (
attempts >= 20
) {
clearInterval(
checkMuseum
);
}
}
},
500
);
}
);
}
/************************************************************
* PROCESS MUSEUM DOM
************************************************************/
function processMuseum(
museumDocument
) {
console.log(
'[TBF] Processing Museum...'
);
const museumItems =
museumDocument.querySelectorAll(
'.item-wrapper[itemid]'
);
console.log(
`[TBF] Found ${museumItems.length} Museum items`
);
for (
const element of museumItems
) {
const itemId =
Number(
element.getAttribute(
'itemid'
)
);
/*
* Ignore items not in our filter list.
*/
if (
!FILTER_SET.has(itemId)
) {
continue;
}
const amountElement =
element.querySelector(
'.item-amount.qty'
);
const nameElement =
element.querySelector(
'.coll-item-header'
);
let amount = 0;
if (amountElement) {
amount =
parseInt(
amountElement
.textContent
.trim()
.replace(/,/g, ''),
10
) || 0;
}
const configuredItem =
FILTER_ITEMS.find(
item =>
item.id === itemId
);
const name = configuredItem
? configuredItem.name
: `Item ${itemId}`;
inventory.set(
itemId,
{
amount,
name
}
);
console.log(
`[TBF] ${name}: ${amount}`
);
}
console.log(
'[TBF] Final inventory:',
Object.fromEntries(
inventory
)
);
/*
* Update Bazaar.
*/
updateOwnedLabels();
updatePanel();
}
/************************************************************
* GET ITEM ID FROM BAZAAR CARD
************************************************************/
function getItemId(card) {
const image =
card.querySelector(
'img.item-plate'
);
if (!image) {
return null;
}
const src =
image.getAttribute('src') ||
image.getAttribute('srcset');
if (!src) {
return null;
}
/*
* Example:
*
* /images/items/258/large.png
*
* => 258
*/
const match =
src.match(
/\/images\/items\/(\d+)\//
);
return match
? Number(match[1])
: null;
}
/************************************************************
* FIND BAZAAR CARDS
************************************************************/
function getBazaarCards() {
return Array.from(
document.querySelectorAll(
'[data-testid="item-description"]'
)
);
}
/************************************************************
* APPLY FILTER
************************************************************/
function applyFilter() {
const cards =
getBazaarCards();
let shown = 0;
let hidden = 0;
for (
const card of cards
) {
const itemId =
getItemId(card);
/*
* If we can't identify it,
* don't touch it.
*/
if (
itemId === null
) {
card.style.display =
'';
continue;
}
/*
* Filter OFF
*/
if (
!filterEnabled
) {
card.style.display =
'';
removeOwnedLabel(
card
);
continue;
}
/*
* Filter ON:
*
* Only configured items
* remain visible.
*/
if (
FILTER_SET.has(itemId)
) {
card.style.display =
'';
addOwnedLabel(
card,
itemId
);
shown++;
} else {
card.style.display =
'none';
hidden++;
}
}
updatePanel(
shown,
hidden
);
}
/************************************************************
* ADD OWNED LABEL
************************************************************/
function addOwnedLabel(
card,
itemId
) {
let label =
card.querySelector(
'.tbf-owned'
);
if (!label) {
label =
document.createElement(
'div'
);
label.className =
'tbf-owned';
Object.assign(
label.style,
{
marginTop: '4px',
fontSize: '12px',
fontWeight: 'bold',
textAlign: 'center',
opacity: '0.9'
}
);
const description =
card.querySelector(
'[data-testid="description"]'
);
if (description) {
description.appendChild(
label
);
} else {
card.appendChild(
label
);
}
}
const item =
inventory.get(
itemId
);
const amount =
item
? item.amount
: 0;
label.textContent =
`You own: ${amount}`;
}
/************************************************************
* REMOVE OWNED LABEL
************************************************************/
function removeOwnedLabel(
card
) {
const label =
card.querySelector(
'.tbf-owned'
);
if (label) {
label.remove();
}
}
/************************************************************
* UPDATE OWNED LABELS
************************************************************/
function updateOwnedLabels() {
if (!filterEnabled) {
return;
}
for (
const card of getBazaarCards()
) {
const itemId =
getItemId(card);
if (
itemId !== null &&
FILTER_SET.has(itemId)
) {
addOwnedLabel(
card,
itemId
);
}
}
}
/************************************************************
* CREATE PANEL
************************************************************/
function createPanel() {
/*
* Don't create duplicates.
*/
if (
panel &&
document.body.contains(panel)
) {
return;
}
panel =
document.createElement(
'div'
);
panel.id =
'tbf-panel';
Object.assign(
panel.style,
{
position: 'fixed',
top: '120px',
right: '20px',
zIndex: '99999',
background: 'rgba(20,20,20,0.95)',
color: '#fff',
padding: '10px 12px',
borderRadius: '8px',
boxShadow:
'0 2px 10px rgba(0,0,0,0.4)',
fontFamily:
'Arial,sans-serif',
fontSize: '13px',
minWidth: '190px'
}
);
document.body.appendChild(
panel
);
/*
* Restore previous state.
*/
if (
panelMinimized
) {
renderMinimizedPanel();
} else {
renderFullPanel();
}
}
/************************************************************
* FULL PANEL
************************************************************/
function renderFullPanel() {
if (!panel) {
return;
}
Object.assign(
panel.style,
{
minWidth: '190px',
width: '',
height: '',
padding: '10px 12px',
borderRadius: '8px',
display: 'block'
}
);
panel.innerHTML = `
<div style="
display:flex;
justify-content:space-between;
align-items:center;
gap:10px;
margin-bottom:8px;
">
<strong>
Bazaar Filter
</strong>
<div style="
display:flex;
gap:4px;
">
<button
id="tbf-refresh"
type="button"
title="Refresh Museum inventory"
style="
border:0;
background:transparent;
color:#fff;
cursor:pointer;
font-size:15px;
padding:2px 4px;
"
>↻</button>
<button
id="tbf-minimize"
type="button"
title="Minimize"
style="
border:0;
background:transparent;
color:#fff;
cursor:pointer;
font-size:16px;
padding:2px 4px;
"
>−</button>
</div>
</div>
<div style="
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:8px;
">
<span id="tbf-status">
</span>
<button
id="tbf-toggle"
type="button"
style="
border:0;
border-radius:12px;
padding:3px 10px;
cursor:pointer;
font-weight:bold;
"
></button>
</div>
<div id="tbf-items">
</div>
`;
/*
* Minimize
*/
panel
.querySelector(
'#tbf-minimize'
)
.addEventListener(
'click',
minimizePanel
);
/*
* Refresh
*/
panel
.querySelector(
'#tbf-refresh'
)
.addEventListener(
'click',
loadMuseumInventory
);
/*
* Toggle
*/
panel
.querySelector(
'#tbf-toggle'
)
.addEventListener(
'click',
() => {
filterEnabled =
!filterEnabled;
localStorage.setItem(
STORAGE_FILTER_ENABLED,
filterEnabled
);
updateToggle();
applyFilter();
}
);
updateToggle();
updatePanel();
}
/************************************************************
* MINIMIZED PANEL
************************************************************/
function renderMinimizedPanel() {
if (!panel) {
return;
}
Object.assign(
panel.style,
{
minWidth: '0',
width: '32px',
height: '32px',
padding: '4px',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}
);
panel.innerHTML = `
<button
id="tbf-restore"
type="button"
title="Open Bazaar Filter"
style="
border:0;
background:transparent;
color:#fff;
cursor:pointer;
font-size:18px;
font-weight:bold;
padding:2px 5px;
"
>☰</button>
`;
panel
.querySelector(
'#tbf-restore'
)
.addEventListener(
'click',
restorePanel
);
}
/************************************************************
* MINIMIZE
************************************************************/
function minimizePanel() {
if (!panel) {
return;
}
panelMinimized =
true;
localStorage.setItem(
STORAGE_MINIMIZED,
'true'
);
renderMinimizedPanel();
}
/************************************************************
* RESTORE
************************************************************/
function restorePanel() {
if (!panel) {
return;
}
panelMinimized =
false;
localStorage.setItem(
STORAGE_MINIMIZED,
'false'
);
renderFullPanel();
/*
* Opening the panel refreshes
* the Museum quantities.
*/
loadMuseumInventory();
}
/************************************************************
* TOGGLE BUTTON
************************************************************/
function updateToggle() {
if (!panel) {
return;
}
const toggle =
panel.querySelector(
'#tbf-toggle'
);
if (!toggle) {
return;
}
toggle.textContent =
filterEnabled
? 'ON'
: 'OFF';
if (
filterEnabled
) {
toggle.style.background =
'#4caf50';
toggle.style.color =
'#fff';
} else {
toggle.style.background =
'#777';
toggle.style.color =
'#fff';
}
}
/************************************************************
* PANEL CONTENT
************************************************************/
function updatePanel(
shown = 0,
hidden = 0
) {
if (
!panel ||
panelMinimized
) {
return;
}
const status =
panel.querySelector(
'#tbf-status'
);
const itemsContainer =
panel.querySelector(
'#tbf-items'
);
if (
!status ||
!itemsContainer
) {
return;
}
if (
!filterEnabled
) {
status.textContent =
'Filter disabled';
itemsContainer.innerHTML =
'';
return;
}
status.textContent =
`${shown} selected item${shown === 1 ? '' : 's'} shown`;
itemsContainer.innerHTML =
'';
for (
const config of FILTER_ITEMS
) {
const item =
inventory.get(
config.id
);
const row =
document.createElement(
'div'
);
Object.assign(
row.style,
{
display: 'flex',
justifyContent:
'space-between',
gap: '15px',
padding: '2px 0'
}
);
const name =
item
? item.name
: config.name;
const amount =
item
? item.amount
: 0;
row.innerHTML = `
<span>
${escapeHtml(name)}
</span>
<strong>
${amount}
</strong>
`;
itemsContainer.appendChild(
row
);
}
}
/************************************************************
* HTML ESCAPE
************************************************************/
function escapeHtml(
text
) {
const div =
document.createElement(
'div'
);
div.textContent =
text;
return div.innerHTML;
}
/************************************************************
* OBSERVER
*
* Torn can replace parts/all of the page when navigating.
************************************************************/
function setupObserver() {
if (observer) {
return;
}
observer =
new MutationObserver(
function () {
clearTimeout(
filterTimeout
);
filterTimeout =
setTimeout(
function () {
/*
* If Torn removed our panel,
* recreate it.
*/
if (
!document.body.contains(
panel
)
) {
panel = null;
createPanel();
}
/*
* Reapply Bazaar filter.
*/
applyFilter();
},
100
);
}
);
observer.observe(
document.body,
{
childList: true,
subtree: true
}
);
}
/************************************************************
* INITIALIZATION
************************************************************/
function init() {
console.log(
'[TBF] Initializing Bazaar Filter'
);
createPanel();
/*
* Load current Museum quantities.
*/
loadMuseumInventory();
/*
* Filter current Bazaar.
*/
applyFilter();
/*
* Watch for Torn page changes.
*/
setupObserver();
}
/*
* Wait for Torn to render.
*/
setTimeout(
init,
1000
);
})();