// ==UserScript==
// @name Bezas Bazaar
// @namespace http://tampermonkey.net/
// @version 3.0.1
// @description Highlight Torn listings based on manually saved prices (Item Market + Bazaar), using both ID and Name
// @match https://www.torn.com/page.php?sid=ItemMarket*
// @match https://www.torn.com/bazaar.php*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const normalize = str => str?.toLowerCase().replace(/\s+/g, '_');
function getSavedPrice(id, name = null) {
const idKey = `manual_price_${id}`;
const nameKey = name ? `manual_price_${normalizeKey(name)}` : null;
const byId = GM_getValue(idKey);
const byName = nameKey ? GM_getValue(nameKey) : undefined;
return byId !== undefined ? byId : byName;
}
function savePrice(id, name, value) {
if (id) GM_setValue(`manual_price_${id}`, value);
if (name) GM_setValue(`manual_price_${normalizeKey(name)}`, value);
}
function normalizeKey(key) {
return key?.toLowerCase().trim().replace(/\s+/g, '_');
}
GM_addStyle(`
.manual-highlight-good { background-color: #004d00 !important; color: white !important; }
.manual-highlight-warning { background-color: #ffa500 !important; color: black !important; }
.manual-highlight-bad { background-color: #8b0000 !important; color: white !important; }
.manual-highlight-missing { background-color: #9370DB !important; color: white !important; }
.manual-price-diff {
margin-left: 6px; font-size: 12px; font-weight: bold;
color: black; background-color: rgba(255,255,255,0.6);
padding: 1px 4px; border-radius: 4px;
}
#manual-price-modal {
position: fixed; top: 30%; left: 50%;
transform: translate(-50%, -30%);
background: #1e1e1e; color: white;
padding: 20px; border: 2px solid #888;
border-radius: 10px; z-index: 9999; display: none;
}
#manual-price-modal input {
width: 100px; padding: 5px;
font-size: 14px;
}
#manual-price-modal button {
margin-left: 10px;
padding: 5px 10px;
background: #444;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.manual-overlay {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 0;
border-radius: 8px;
opacity: 0.6;
pointer-events: none;
}
.itemTile___cbw7w, .item___GYCYJ {
position: relative;
}
#manual-price-drag-handle {
cursor: move;
user-select: none;
}
`);
// Modal
const modal = document.createElement("div");
modal.id = "manual-price-modal";
modal.innerHTML = `
<div id="manual-price-drag-handle" style="margin-bottom: 10px; font-weight: bold; cursor: move;">⇅ Drag to move</div>
<label>Manual price: $<input type="number" id="manual-price-input" /></label>
<button id="manual-price-save">Save</button>
<button id="manual-price-cancel">Cancel</button>
`;
document.body.appendChild(modal);
makeModalDraggable();
let currentItemId = null;
let currentItemName = null;
document.getElementById("manual-price-save").onclick = () => {
const val = parseInt(document.getElementById("manual-price-input").value);
if (val > 0) {
savePrice(currentItemId, currentItemName, val);
modal.style.display = "none";
highlightAll();
}
};
document.getElementById("manual-price-cancel").onclick = () => modal.style.display = "none";
function showModal(id, name) {
currentItemId = id;
currentItemName = name;
document.getElementById("manual-price-input").value = getSavedPrice(id, name) || '';
modal.style.display = "block";
}
function makeModalDraggable() {
const modal = document.getElementById("manual-price-modal");
const handle = document.getElementById("manual-price-drag-handle");
let offsetX = 0, offsetY = 0, isDragging = false;
handle.addEventListener("mousedown", (e) => {
isDragging = true;
offsetX = e.clientX - modal.offsetLeft;
offsetY = e.clientY - modal.offsetTop;
document.body.style.userSelect = "none";
});
document.addEventListener("mouseup", () => {
isDragging = false;
document.body.style.userSelect = "auto";
});
document.addEventListener("mousemove", (e) => {
if (!isDragging) return;
modal.style.left = `${e.clientX - offsetX}px`;
modal.style.top = `${e.clientY - offsetY}px`;
modal.style.right = "auto";
modal.style.bottom = "auto";
modal.style.transform = "none";
});
}
function addDiff(el, diff) {
if (el.querySelector('.manual-price-diff')) return;
const span = document.createElement('span');
span.className = 'manual-price-diff';
span.textContent = `(${diff > 0 ? '+' : ''}${diff.toFixed(1)}%)`;
el.appendChild(span);
}
function applyHighlight(el, listed, saved, priceEl) {
el.classList.remove('manual-highlight-good', 'manual-highlight-warning', 'manual-highlight-bad', 'manual-highlight-missing');
el.style.setProperty('background-color', '', 'important');
el.style.setProperty('color', '', 'important');
if (saved === undefined) {
el.classList.add('manual-highlight-missing');
el.style.setProperty('background-color', '#9370DB', 'important');
el.style.setProperty('color', '#fff', 'important');
} else {
const diff = ((listed - saved) / saved) * 100;
if (diff < 0) { //Work on getting this customizable % chance, default -5
el.classList.add('manual-highlight-good');
el.style.setProperty('background-color', '#004d00', 'important');
el.style.setProperty('color', '#fff', 'important');
} else if (diff >= 0 && diff <= 3) {
el.classList.add('manual-highlight-warning');
el.style.setProperty('background-color', '#ffa500', 'important');
el.style.setProperty('color', '#000', 'important');
} else {
el.classList.add('manual-highlight-bad');
el.style.setProperty('background-color', '#8b0000', 'important');
el.style.setProperty('color', '#fff', 'important');
}
if (priceEl) addDiff(priceEl, diff);
}
}
function highlightItemTiles() {
document.querySelectorAll('.itemTile___cbw7w').forEach(tile => {
const img = tile.querySelector('img.torn-item');
const priceSpan = tile.querySelector('.priceAndTotal___eEVS7 span');
if (!img || !priceSpan) return;
const idMatch = img.src.match(/\/items\/(\d+)\//);
const itemId = idMatch ? idMatch[1] : null;
if (!itemId) return;
// Use regex to extract the first $amount (ignoring anything in percent)
const fullText = priceSpan.textContent || '';
const match = fullText.match(/\$([\d,]+)/);
const listed = match ? parseInt(match[1].replace(/,/g, '')) : NaN;
const saved = getSavedPrice(itemId);
console.log(`[Tile DEBUG] ID=${itemId}, Raw="${fullText}", Listed=${listed}, Saved=${saved}`);
if (!isNaN(listed)) {
applyHighlight(tile, listed, saved, priceSpan);
}
});
}
function highlightSellerRows(itemId) {
const rows = document.querySelectorAll('li[class*="rowWrapper___"]');
const saved = getSavedPrice(itemId, null);
rows.forEach(row => {
const priceEl = row.querySelector('div[class*="price___"]');
const match = priceEl?.textContent.match(/\$([\d,]+)/);
if (!priceEl || !match) return;
const price = parseInt(match[1].replace(/,/g, ''));
applyHighlight(row, price, saved, priceEl);
});
}
function watchSellerRows(itemId) {
const observer = new MutationObserver(() => {
highlightSellerRows(itemId);
});
observer.observe(document.body, { childList: true, subtree: true });
}
function setupBuyButtons() {
document.querySelectorAll('.itemTile___cbw7w .actionButton___pb_Da').forEach(btn => {
if (btn.dataset.bound === "true") return;
btn.dataset.bound = "true";
btn.addEventListener('click', () => {
const container = btn.closest('.itemTile___cbw7w');
const img = container?.querySelector('img.torn-item');
const nameEl = container?.querySelector('.itemName___3tW7n');
const match = img?.src?.match(/\/items\/(\d+)\//);
const id = match ? match[1] : null;
const name = nameEl?.textContent?.trim() || null;
showModal(id, name);
highlightSellerRows(id);
watchSellerRows(id);
});
});
}
function highlightFullListings() {
const table = document.querySelector('#fullListingsView table');
if (!table || (!currentItemId && !currentItemName)) return;
const saved = getSavedPrice(currentItemId, currentItemName);
table.querySelectorAll('tr').forEach(row => {
const priceEl = row.querySelector('td:first-child');
const match = priceEl?.textContent.match(/\$([\d,]+)/);
if (!match) return;
const listed = parseInt(match[1].replace(/,/g, ''));
applyHighlight(row, listed, saved, priceEl);
});
}
function highlightBazaarPage() {
document.querySelectorAll('.itemsContainner___tVzIR .item___GYCYJ').forEach(item => {
const nameEl = item.querySelector('.description___Y2Nrl .name___B0RW3');
const priceEl = item.querySelector('.description___Y2Nrl .price___dJqda');
if (!nameEl || !priceEl) return;
const itemName = nameEl.textContent.trim();
const rawPrice = priceEl.textContent.match(/\$[\d,]+/);
const listed = rawPrice ? parseInt(rawPrice[0].replace(/[^\d]/g, '')) : NaN;
const saved = getSavedPrice(null, itemName);
console.log(`[Bazaar DEBUG] Name=${itemName}, Raw="${rawPrice}", Listed=${listed}, Saved=${saved}`);
if (!isNaN(listed)) {
applyHighlight(item, listed, saved, priceEl);
console.log(`[Bazaar APPLY] ${itemName} → Listed=${listed}, Saved=${saved}, Diff=${saved ? (((listed - saved) / saved) * 100).toFixed(2) + '%' : 'N/A'}`);
}
const btn = item.querySelector('button[aria-label^="Buy:"]');
if (btn && !btn.dataset.bound) {
btn.dataset.bound = "true";
btn.addEventListener('click', () => showModal(null, itemName));
}
});
}
function highlightAll() {
if (location.href.includes("ItemMarket")) {
highlightItemTiles();
setupBuyButtons();
highlightFullListings();
} else if (location.href.includes("bazaar.php")) {
highlightBazaarPage();
}
}
new MutationObserver(highlightAll).observe(document.body, {
childList: true, subtree: true
});
window.addEventListener('keydown', e => {
if (e.key === "Escape") modal.style.display = "none";
});
highlightAll();
})();