// ==UserScript==
// @name Bazaar Auto-Sorter
// @namespace https://torn.com/
// @version 1.0
// @author swervelord [3637232]
// @description Sort your bazaar items by *Type* or *Price*
// @match https://www.torn.com/bazaar.php*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @connect api.torn.com
// ==/UserScript==
/*************************
* CONFIG & CONSTANTS *
*************************/
const API_BASE = "https://api.torn.com/";
const ITEMS_ENDPOINT = "torn/?selections=items&key=";
const CALLS_PER_MIN_LIMIT = 90;
const CALL_WINDOW_MS = 60_000;
/*************************
* RATE‑LIMIT HELPERS *
*************************/
let callTimestamps = [];
function canCall () {
const now = Date.now();
callTimestamps = callTimestamps.filter(t => now - t < CALL_WINDOW_MS);
return callTimestamps.length < CALLS_PER_MIN_LIMIT;
}
function registerCall () { callTimestamps.push(Date.now()); }
function apiGet (url) {
return new Promise((resolve, reject) => {
const attempt = () => {
if (!canCall()) return setTimeout(attempt, 400);
registerCall();
GM_xmlhttpRequest({
method: 'GET', url,
onload: r => { try { resolve(JSON.parse(r.responseText)); } catch (e) { reject(e); } },
onerror: reject
});
};
attempt();
});
}
/*************************
* API KEY CACHE *
*************************/
async function getApiKey () {
let key = GM_getValue('tornApiKey', '');
while (!/^[a-zA-Z0-9]{16,}$/.test(key)) {
key = prompt('Enter your Torn API key (16+ chars)', '') || '';
if (!/^[a-zA-Z0-9]{16,}$/.test(key.trim())) {
alert('Invalid API key, try again.');
key = '';
} else {
key = key.trim();
GM_setValue('tornApiKey', key);
}
}
return key;
}
/*************************
* STYLES *
*************************/
const style = document.createElement('style');
style.textContent = `
#bsz-controls{display:flex;justify-content:center;gap:12px;margin:12px auto;width:100%;max-width:780px}
#bsz-controls button{background:#333;color:#fff;border:1px solid #0ff;box-shadow:0 0 6px #0ff;padding:6px 14px;border-radius:4px;font-weight:600;cursor:pointer}
#bsz-controls button:disabled{opacity:.5;cursor:not-allowed}
#bsz-preview{position:fixed;top:90px;left:10px;width:480px;max-height:80vh;overflow:auto;border:1px solid #0ff;background:#222;color:#fff;font-size:13px;box-shadow:0 0 8px #0ff;padding:6px 10px;border-radius:4px;z-index:9999;display:none}
`;document.head.appendChild(style);
/*************************
* UI COMPONENTS *
*************************/
const controls = document.createElement('div');controls.id='bsz-controls';
const btnType = Object.assign(document.createElement('button'),{innerText:'Sort by Type'});
const btnPrice = Object.assign(document.createElement('button'),{innerText:'Sort by Price'});
controls.append(btnType,btnPrice);
const preview=document.createElement('div');preview.id='bsz-preview';document.body.appendChild(preview);
/*************************
* HELPERS *
*************************/
function sleep(ms){return new Promise(r=>setTimeout(r,ms));}
function getRowPrice(row){const h=row.querySelector('.price___DoKP7 input[type="hidden"]');return h?parseInt(h.value,10):0;}
function getCurrentRows(){
const rows=[...document.querySelectorAll("div[aria-label='grid'] .row___n2Uxh")];
return rows.map(r=>{
const name=r.querySelector('.item___jLJcf').getAttribute('aria-label');
const dmg = r.querySelector('.damageBonusIcon___MSlMm+span')?.innerText||'';
const acc = r.querySelector('.accuracyBonusIcon___p4tfD+span')?.innerText||'';
return {el:r,name,price:getRowPrice(r),dmg,acc};
});
}
/*************************
* PREVIEW RENDERING *
*************************/
function buildPreview(rows, sortByPrice=false){
const grouped={};
for(const r of rows){
const key=r.type||'Unknown';
if(!grouped[key])grouped[key]=[];
grouped[key].push(r);
}
const html=['<div style="padding:6px 4px">'];
for(const type of Object.keys(grouped).sort()){
html.push(`<div style="color:#0ff;font-size:16px;font-weight:bold;margin-top:10px;text-decoration:underline">${type}</div>`);
html.push('<ul style="list-style:none;padding-left:12px;margin:6px 0 10px">');
const sortFn=sortByPrice?(a,b)=>b.price-a.price:(a,b)=>a.name.localeCompare(b.name);
for(const r of grouped[type].sort(sortFn)){
const extra=sortByPrice?` - $${r.price.toLocaleString()}`:(r.dmg||r.acc?` (${r.dmg}/${r.acc})`:"");
html.push(`<li style="color:#fff;padding:2px 0">${r.name}${extra}</li>`);
}
html.push('</ul>');
}
html.push('</div>');
preview.innerHTML=html.join('');
preview.style.display='block';
}
/*************************
* ITEM DIRECTORY MAP *
*************************/
let itemDirectory=null;
async function ensureItemDirectory(key){
if(itemDirectory) return itemDirectory;
const data=await apiGet(`${API_BASE}${ITEMS_ENDPOINT}${key}`);
itemDirectory=new Map(Object.values(data.items).map(itm=>[itm.name,itm.type]));
return itemDirectory;
}
/*************************
* DRAG SIMULATION *
*************************/
async function performDrag(itemEl,targetIdx){
const handle=itemEl.querySelector('.draggableIconContainer___zlpp_ i, .draggableIconContainer___zlpp_');
if(!handle) return false;
const rows=[...itemEl.parentElement.children];
const tgtRow=rows[targetIdx];
if(!tgtRow||tgtRow===itemEl) return false;
const sRect=handle.getBoundingClientRect();
const tRect=tgtRow.getBoundingClientRect();
const dx=tRect.left+10-sRect.left;
const dy=tRect.top +10-sRect.top;
const sim=(type,x,y)=>document.dispatchEvent(new MouseEvent(type,{bubbles:true,cancelable:true,clientX:x,clientY:y}));
handle.dispatchEvent(new MouseEvent('mousedown',{bubbles:true,cancelable:true,clientX:sRect.left+5,clientY:sRect.top+5}));
sim('mousemove',sRect.left+5+dx/2,sRect.top+5+dy/2);await sleep(15);
sim('mousemove',sRect.left+5+dx, sRect.top+5+dy); await sleep(15);
sim('mouseup', sRect.left+5+dx, sRect.top+5+dy); await sleep(45);
return true;
}
/*************************
* SORT LOGIC *
*************************/
async function sortByType(){
btnType.disabled=true;
try{
const key=await getApiKey();
const dir=await ensureItemDirectory(key);
let rows=getCurrentRows();
rows.forEach(r=>r.type=dir.get(r.name)||'Unknown');
rows.sort((a,b)=>a.type.localeCompare(b.type)||a.name.localeCompare(b.name)||a.dmg.localeCompare(b.dmg)||a.acc.localeCompare(b.acc));
buildPreview(rows,false);
for(let i=0;i<rows.length;i++){
const current=getCurrentRows();
const tgt=rows[i];
const idx=current.findIndex(r=>r.name===tgt.name&&r.dmg===tgt.dmg&&r.acc===tgt.acc);
if(idx===-1||idx===i) continue;
await performDrag(current[idx].el,i);
}
}catch(e){alert('Sort failed: '+e.message);console.error(e);}finally{btnType.disabled=false;}
}
async function sortByPrice(){
btnPrice.disabled=true;
try{
let rows=getCurrentRows();
const key=await getApiKey();const dir=await ensureItemDirectory(key);rows.forEach(r=>r.type=dir.get(r.name)||'Unknown');
rows.sort((a,b)=>b.price-a.price);
buildPreview(rows,true);
for(let i=0;i<rows.length;i++){
const current=getCurrentRows();
const tgt=rows[i];
const idx=current.findIndex(r=>r.name===tgt.name&&r.price===tgt.price&&r.dmg===tgt.dmg&&r.acc===tgt.acc);
if(idx===-1||idx===i) continue;
await performDrag(current[idx].el,i);
}
}catch(e){alert('Sort failed: '+e.message);console.error(e);}finally{btnPrice.disabled=false;}
}
/*************************
* BOOTSTRAP *
*************************/
(async function init(){
const waitFor=sel=>new Promise(res=>{const int=setInterval(()=>{const el=document.querySelector(sel);if(el){clearInterval(int);res(el);}},500);});
const header=await waitFor('ul.cellsHeader___hZgkv');
header.parentElement.insertBefore(controls,header);
})();
btnType.addEventListener('click',sortByType);
btnPrice.addEventListener('click',sortByPrice);