Bazaar listing ( Weaver Site ) + TE Information PC VERSION
// ==UserScript==
// @name Bazaar + TE Info Final (Integrated & Fixed)
// @namespace https://weav3r.dev/
// @version 3.5.1
// @description Bazaar listing ( Weaver Site ) + TE Information PC VERSION
// @author WTV [3281931]
// @match https://www.torn.com/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect weav3r.dev
// @connect tornexchange.com
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// Global State
window._cachedListings = {};
window._marketValueCache = {};
window._currentMarketNetPrice = 0;
// --- CSS ---
GM_addStyle(`
.bazaar-info-container { border: 1px solid #888; margin: 10px 0; padding: 5px; background: #222; color: #fff; border-radius: 4px; }
.bazaar-info-header { font-weight: bold; margin-bottom: 5px; display: flex; flex-wrap: nowrap; justify-content: space-between; align-items: center; font-size: 14px; }
.bazaar-title { flex-grow: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.best-buyer-line { font-weight: bold; margin-bottom: 5px; color: #FFA500; display: flex; flex-wrap: wrap; justify-content: flex-start; align-items: center; gap: 5px; font-size: 14px; }
.best-buyer-line .price-display { color: lime; font-weight: bold; white-space: nowrap; font-size: 16px; }
.best-buyer-line .trader-link { color: #1E90FF; text-decoration: none; font-weight: bold; cursor: pointer; }
.best-buyer-line .te-listings-link { color: #00BFFF; font-size: 14px; text-decoration: none; white-space: nowrap; margin-left: 5px; font-weight: bold; }
.bazaar-item-id { color: #aaa; font-size: 13px; font-weight: bold; white-space: nowrap; margin-left: auto; }
/* 4-Box Filter Grid */
.filter-grid { display: flex; gap: 6px; margin: 8px 0; padding: 5px; border-top: 1px dashed #444; border-bottom: 1px dashed #444; }
.filter-grid input { flex: 1; background: #111; border: 1px solid #666; color: #00FF00; padding: 5px; border-radius: 3px; text-align: center; font-size: 12px; min-width: 0; }
.bazaar-reset-all-btn { background: #444; color: white; border: none; padding: 0 10px; cursor: pointer; font-weight: bold; border-radius: 3px; font-size: 16px; }
.bazaar-market-calc { display: flex; align-items: center; gap: 10px; padding: 8px 0; margin-top: 5px; }
.bazaar-calc-label { font-weight: bold; color: #ddd; font-size: 14px; white-space: nowrap; }
.bazaar-net-profit { font-weight: bold; color: limegreen; font-size: 14px; white-space: nowrap; }
.bazaar-card-container { display: flex; overflow-x: auto; padding: 5px; gap: 5px; min-height: 80px; }
.bazaar-card { border: 1px solid #444; background: #222; color: #eee; padding: 10px; margin: 2px; width: 125px; flex-shrink: 0; display: flex; flex-direction: column; font-size: 15px; gap: 3px; border-radius: 4px; }
.bazaar-card a { font-weight: bold; text-decoration: none; color: #1E90FF; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.diff-text-positive { color: red; font-weight: bold; }
.diff-text-negative { color: limegreen; font-weight: bold; }
.diff-text-neutral { color: gold; font-weight: bold; }
.bazaar-loader { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; margin: 10px auto; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
`);
// --- UTILITIES ---
function cleanName(rawName) {
return rawName.replace(/View Info|Buy Item|Content (collapsed|expanded)/g, '').split('$')[0].trim();
}
async function fetchTornExchangeData(itemId) {
return new Promise(resolve => {
GM_xmlhttpRequest({
method: "GET", url: `https://tornexchange.com/api/best_listing?item_id=${itemId}`,
onload: (r) => {
try {
const d = JSON.parse(r.responseText);
if(d && d.status === 'success') {
window._marketValueCache[itemId] = d.data.te_price;
resolve(d.data);
} else resolve(null);
} catch(e) { resolve(null); }
}
});
});
}
// --- HIGHLIGHT & SCROLL LOGIC ---
const checkAndScroll = () => {
const params = new URLSearchParams(window.location.search);
const targetId = params.get("highlightItem");
if (!targetId || !window.location.href.includes("bazaar.php")) return false;
const targetImg = document.querySelector(`img[src*="/images/items/${targetId}/"]`);
if (targetImg) {
const itemBox = targetImg.closest('li') || targetImg.closest('[class*="itemTile"]') || targetImg.parentElement;
if (itemBox) {
itemBox.style.setProperty("outline", "4px solid #00FF00", "important");
itemBox.style.setProperty("outline-offset", "2px", "important");
itemBox.style.setProperty("background", "rgba(0, 255, 0, 0.1)", "important");
itemBox.scrollIntoView({ behavior: "smooth", block: "center" });
return true;
}
}
return false;
};
// --- UI GENERATION ---
function createInfoContainer(itemName, itemId, teData) {
const container = document.createElement('div');
container.className = 'bazaar-info-container';
container.dataset.itemid = itemId;
const cleaned = cleanName(itemName);
const marketVal = teData?.te_price ? `$${Math.round(teData.te_price).toLocaleString()}` : 'N/A';
const encodedName = encodeURIComponent(cleaned);
const teLink = `https://tornexchange.com/listings?model_name_contains=${encodedName}&order_by=&status=`;
container.innerHTML = `
<div class="bazaar-info-header">
<span class="bazaar-title">${cleaned} (Market Value: ${marketVal})</span>
</div>
<div class="best-buyer-line">
${teData?.price ? `<span>Best Trader: <span class="price-display">$${Math.round(teData.price).toLocaleString()}</span> by <a class="trader-link" href="https://www.torn.com/profiles.php?XID=${teData.trader_id}" target="_blank">${teData.trader}</a></span>` : ''}
<a href="${teLink}" target="_blank" class="te-listings-link">(TE Listings)</a>
<span class="bazaar-item-id">Item #: ${itemId}</span>
</div>
<div class="filter-grid">
<input type="number" class="f-min-p" placeholder="Min $">
<input type="number" class="f-max-p" placeholder="Max $">
<input type="number" class="f-min-q" placeholder="Min Qty">
<input type="number" class="f-max-q" placeholder="Max Qty">
<button class="bazaar-reset-all-btn">↺</button>
</div>
<div class="bazaar-market-calc">
<span class="bazaar-calc-label">Profit Calc Sell Price:</span>
<input type="text" placeholder="Enter Price" class="profit-calc-input" style="width:100px; background:#111; border:1px solid #666; color:#fff; padding:4px;">
<span class="bazaar-calc-label">Net (5%):</span>
<span class="bazaar-net-profit profit-net-display">$0</span>
</div>
<div class="bazaar-card-container"><div class="bazaar-loader"></div></div>
`;
setupListeners(container, itemId);
return container;
}
function setupListeners(container, itemId) {
container.querySelectorAll('.filter-grid input').forEach(input => {
input.addEventListener('input', () => renderCards(itemId, container));
});
const calcInput = container.querySelector(`.profit-calc-input`);
const netSpan = container.querySelector(`.profit-net-display`);
calcInput.addEventListener('input', (e) => {
const val = e.target.value.replace(/[^\d]/g, '');
e.target.value = val ? Number(val).toLocaleString() : '';
window._currentMarketNetPrice = Math.floor(parseInt(val || 0) * 0.95);
netSpan.textContent = `$${window._currentMarketNetPrice.toLocaleString()}`;
renderCards(itemId, container);
});
container.querySelector('.bazaar-reset-all-btn').addEventListener('click', () => {
container.querySelectorAll('input').forEach(i => i.value = '');
window._currentMarketNetPrice = 0;
renderCards(itemId, container);
});
}
function renderCards(itemId, container) {
const listings = window._cachedListings[itemId] || [];
const cardBox = container.querySelector('.bazaar-card-container');
const minP = parseInt(container.querySelector('.f-min-p').value || 0);
const maxP = parseInt(container.querySelector('.f-max-p').value || Infinity);
const minQ = parseInt(container.querySelector('.f-min-q').value || 0);
const maxQ = parseInt(container.querySelector('.f-max-q').value || Infinity);
const marketVal = window._marketValueCache[itemId] || 0;
if (!cardBox) return;
cardBox.innerHTML = '';
let filtered = listings.filter(l =>
l.price >= minP && l.price <= (maxP || Infinity) &&
l.quantity >= minQ && l.quantity <= (maxQ || Infinity)
).sort((a, b) => a.price - b.price);
filtered.forEach(l => {
const card = document.createElement('div');
card.className = 'bazaar-card';
const diff = marketVal ? ((l.price - marketVal) / marketVal * 100).toFixed(1) : 0;
const diffClass = diff < -0.5 ? 'diff-text-negative' : (diff > 0.5 ? 'diff-text-positive' : 'diff-text-neutral');
let marginHTML = '';
if (window._currentMarketNetPrice > 0) {
const margin = ((window._currentMarketNetPrice - l.price) / window._currentMarketNetPrice * 100).toFixed(2);
const marginClass = margin > 0.1 ? 'diff-text-negative' : 'diff-text-positive';
marginHTML = `<div style="font-size:14px"><b>Margin:</b> <span class="${marginClass}">${margin}%</span></div>`;
}
card.innerHTML = `
<a href="https://www.torn.com/bazaar.php?userId=${l.player_id}&highlightItem=${itemId}#/" target="_blank">${l.player_name}</a>
<div><b>Price:</b> $${Math.round(l.price).toLocaleString()}</div>
<div style="display:flex; justify-content:space-between"><span><b>Qty:</b> ${l.quantity}</span><span class="${diffClass}">${diff > 0 ? '+' : ''}${diff}%</span></div>
${marginHTML}
`;
cardBox.appendChild(card);
});
}
// --- MAIN OBSERVER ---
const observer = new MutationObserver(() => {
// Highlight check
checkAndScroll();
// Bazaar Data injection
document.querySelectorAll('[class*="sellerListWrapper"]').forEach(async w => {
if (w.dataset.bazaarProcessed) return;
w.dataset.bazaarProcessed = 'true';
const tile = w.closest('[class*="itemTile"]') || w.previousElementSibling;
const btn = tile?.querySelector('button[aria-controls*="itemInfo"]');
if (!btn) return;
const itemId = btn.getAttribute('aria-controls').split('-').pop();
const itemName = tile.querySelector('div').textContent.trim();
const teData = await fetchTornExchangeData(itemId);
const container = createInfoContainer(itemName, itemId, teData);
w.insertBefore(container, w.firstChild);
GM_xmlhttpRequest({
method: "GET", url: `https://weav3r.dev/api/marketplace/${itemId}`,
onload: (r) => {
const data = JSON.parse(r.responseText);
window._cachedListings[itemId] = data.listings;
renderCards(itemId, container);
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(checkAndScroll, 1000);
})();