Torn PDA integrated panel: flowers, plushies, Xanax, abroad stock, sets, color-coded, polling.
As of
// ==UserScript==
// @name 💫 Points Maker (PDA Full Display)
// @namespace http://tampermonkey.net/
// @version 1.2.8
// @description Torn PDA integrated panel: flowers, plushies, Xanax, abroad stock, sets, color-coded, polling.
// @match https://www.torn.com/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const PANEL_ID = 'points_maker_pda';
const POLL_INTERVAL_MS = 45 * 1000;
const YATA_URL = 'https://yata.yt/api/v1/travel/export/';
const PROM_URL = 'https://api.prombot.co.uk/api/travel';
const SPECIAL_DRUG = 'Xanax';
const MAX_ABROAD = { flowers: 50, plushies: 50, drugs: 50 };
// Flower & Plush data
const FLOWERS = { "Dahlia": { short: "Dahlia", loc: "MX 🇲🇽", country: "Mexico" }, "Orchid": { short: "Orchid", loc: "HW 🏝️", country: "Hawaii" }, "African Violet": { short: "Violet", loc: "SA 🇿🇦", country: "South Africa" }, "Cherry Blossom": { short: "Cherry", loc: "JP 🇯🇵", country: "Japan" }, "Peony": { short: "Peony", loc: "CN 🇨🇳", country: "China" }, "Ceibo Flower": { short: "Ceibo", loc: "AR 🇦🇷", country: "Argentina" }, "Edelweiss": { short: "Edelweiss", loc: "CH 🇨🇭", country: "Switzerland" }, "Crocus": { short: "Crocus", loc: "CA 🇨🇦", country: "Canada" }, "Heather": { short: "Heather", loc: "UK 🇬🇧", country: "United Kingdom" }, "Tribulus Omanense": { short: "Tribulus", loc: "AE 🇦🇪", country: "UAE" }, "Banana Orchid": { short: "Banana", loc: "KY 🇰🇾", country: "Cayman Islands" } };
const PLUSHIES = { "Sheep Plushie": { short: "Sheep", loc: "B.B 🏪", country: "Torn City" }, "Teddy Bear Plushie": { short: "Teddy", loc: "B.B 🏪", country: "Torn City" }, "Kitten Plushie": { short: "Kitten", loc: "B.B 🏪", country: "Torn City" }, "Jaguar Plushie": { short: "Jaguar", loc: "MX 🇲🇽", country: "Mexico" }, "Wolverine Plushie": { short: "Wolverine", loc: "CA 🇨🇦", country: "Canada" }, "Nessie Plushie": { short: "Nessie", loc: "UK 🇬🇧", country: "United Kingdom" }, "Red Fox Plushie": { short: "Fox", loc: "UK 🇬🇧", country: "United Kingdom" }, "Monkey Plushie": { short: "Monkey", loc: "AR 🇦🇷", country: "Argentina" }, "Chamois Plushie": { short: "Chamois", loc: "CH 🇨🇭", country: "Switzerland" }, "Panda Plushie": { short: "Panda", loc: "CN 🇨🇳", country: "China" }, "Lion Plushie": { short: "Lion", loc: "SA 🇿🇦", country: "South Africa" }, "Camel Plushie": { short: "Camel", loc: "AE 🇦🇪", country: "UAE" }, "Stingray Plushie": { short: "Stingray", loc: "KY 🇰🇾", country: "Cayman Islands" } };
const COUNTRY_NAME_TO_CODE = { 'JAPAN': 'JAP', 'MEXICO': 'MEX', 'CANADA': 'CAN', 'CHINA': 'CHI', 'UNITED KINGDOM': 'UNI', 'ARGENTINA': 'ARG', 'SWITZERLAND': 'SWI', 'HAWAII': 'HAW', 'UAE': 'UAE', 'CAYMAN ISLANDS': 'CAY', 'SOUTH AFRICA': 'SOU', 'S.A': 'SOU', 'SA': 'SOU', 'TORN': 'BB', 'B.B': 'BB' };
// Utils
function escapeHtml(s){if(s==null)return'';return String(s).replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));}
function colorForPercent(value,max){if(!max||max===0)return'#bdbdbd';const pct=(value/max)*100;if(pct>=75)return'#00c853';if(pct>=40)return'#3399ff';return'#ff1744';}
// Cache short names and mappings
function buildReq(mapObj){ const fns=Object.keys(mapObj); const shorts=fns.map(fn=>mapObj[fn].short); const locs={}; const countries={}; fns.forEach(fn=>{const s=mapObj[fn].short; locs[s]=mapObj[fn].loc; countries[s]=mapObj[fn].country;}); return { fullNames:fns, shortNames:shorts, locByShort:locs, countryByShort:countries }; }
const flowersReq=buildReq(FLOWERS), plushReq=buildReq(PLUSHIES);
// UI
GM_addStyle(`#${PANEL_ID}{position:fixed;top:42px;left:100px;width:250px;background:#0b0b0b;color:#eaeaea;font-family:monospace;font-size:9px;border:1px solid #444;border-radius:6px;z-index:999999;max-height:65vh;overflow-y:auto;line-height:1.1;} #${PANEL_ID} .header{background:#121212;padding:4px 6px;cursor:pointer;font-weight:700;font-size:10px;border-bottom:1px solid #333;user-select:none;} .summary-line{font-weight:700;margin:6px;font-size:10px;color:#dfe7ff;} .low-line{color:#ff4d4d;font-weight:700;margin:6px;font-size:10px;} .group-title{font-weight:700;margin:6px 6px 2px 6px;font-size:9.5px;} ul.item-list{margin:4px 6px 8px 12px;padding:0;list-style:none;} li.item-row{display:flex;align-items:center;gap:6px;padding:2px 0;white-space:nowrap;} .item-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;} .item-total{flex:0 0 36px;text-align:right;color:#cfe8c6;} .item-av{flex:0 0 60px;text-align:right;color:#f7b3b3;} .item-loc{flex:0 0 36px;text-align:right;color:#bcbcbc;font-size:8.5px;} .stock-green{color:#00c853!important;}.stock-orange{color:#ff9800!important;}.stock-red{color:#ff1744!important;}.stock-gray{color:#9ea6b3!important;}`);
let statusEl, contentEl, summaryEl;
function buildUI(){ if(document.getElementById(PANEL_ID))return; const root=document.createElement('div'); root.id=PANEL_ID; root.innerHTML=`<div id="tc_header" class="header">▶ 💫 Points Maker</div><div id="tc_content_wrapper"><div id="tc_status">Waiting for API key...</div><div id="tc_summary"></div><div id="tc_content"></div></div>`; document.body.appendChild(root); const headerEl=root.querySelector('#tc_header'); const contentWrapper=root.querySelector('#tc_content_wrapper'); let collapsed=GM_getValue(`${PANEL_ID}-collapsed`,false); function updateCollapse(){ headerEl.textContent=(collapsed?'▶ ':'▼ ')+'💫 Points Maker'; contentWrapper.style.display=collapsed?'none':'block';} updateCollapse(); headerEl.addEventListener('click',()=>{ collapsed=!collapsed; GM_setValue(`${PANEL_ID}-collapsed`,collapsed); updateCollapse(); }); statusEl=root.querySelector('#tc_status'); summaryEl=root.querySelector('#tc_summary'); contentEl=root.querySelector('#tc_content'); }
buildUI();
function gmGetJson(url,timeout=14000){ return new Promise((resolve,reject)=>{try{GM_xmlhttpRequest({method:'GET',url,timeout,onload:res=>{try{const txt=(typeof res.response==='string'&&res.response)?res.response:res.responseText; resolve(JSON.parse(txt));}catch(e){reject(e);}},onerror:err=>reject(err),ontimeout:()=>reject(new Error('timeout'))});}catch(e){reject(e);}}); }
function fetchDisplayViaDOM(){ const map={}; const els=document.querySelectorAll('.display-item, .display_case_item, .dcItem, .item-wrap .item'); if(els && els.length){ els.forEach(el=>{let name='';let qty=0; const nameEl=el.querySelector('.item-name, .name, .title')||el.querySelector('a')||el; if(nameEl) name=(nameEl.innerText||'').trim(); const qtyEl=el.querySelector('.item-amount, .count, .qty, .quantity, .item-qty'); if(qtyEl) qty=parseInt((qtyEl.innerText||'').replace(/\D/g,''))||0; if(name) map[name]=(map[name]||0)+qty;}); } return map; }
function parseYata(yataData){ const map={}; if(!yataData||!yataData.stocks)return map; for(const [code,obj] of Object.entries(yataData.stocks)){ const arr=Array.isArray(obj.stocks)?obj.stocks:[]; const m={}; for(const it of arr)if(it&&it.name)m[it.name]=Number(it.quantity??it.qty??0)||0; map[String(code).toUpperCase()]=m; } return map; }
function parseProm(promData){ const map={}; if(!promData)return map; for(const [countryKey,countryVal] of Object.entries(promData)){ if(!countryVal)continue; const up=String(countryKey).trim().toUpperCase(); let code=COUNTRY_NAME_TO_CODE[up]??up; const m={}; if(Array.isArray(countryVal.stocks)){ for(const it of countryVal.stocks)if(it&&it.name)m[it.name]=Number(it.quantity??it.qty??0)||0; } else { for(const [k,v] of Object.entries(countryVal)){ if(v==null)continue; if(typeof v==='object'&&('quantity' in v||'qty' in v||'amount' in v))m[k]=Number(v.quantity??v.qty??v.amount??0)||0; else if(typeof v==='number'||!isNaN(Number(v)))m[k]=Number(v)||0; } } map[code]=m; } return map; }
function sumAcrossCountriesFor(itemName,parsedMap){ if(!parsedMap)return 0; let total=0; for(const c of Object.keys(parsedMap))total+=Number(parsedMap[c][itemName]||0); return total; }
function countsForReq(itemsAgg,req,mapObj){ const counts={}; req.shortNames.forEach(s=>counts[s]=0); req.fullNames.forEach(fn=>{const short=mapObj[fn].short; const q=itemsAgg[fn]||0; counts[short]=(counts[short]||0)+q;}); return counts; }
function calcSetsAndRemainderFromCounts(counts,shortNames){ const countsArr=shortNames.map(n=>counts[n]||0); const sets=countsArr.length?Math.min(...countsArr):0; const remainder={}; shortNames.forEach(n=>remainder[n]=Math.max(0,(counts[n]||0)-sets)); return {sets,remainder}; }
function findLowest(remainder,locMap,countryMap){ const keys=Object.keys(remainder); if(!keys.length)return null; let min=Infinity; keys.forEach(k=>{if(remainder[k]<min)min=remainder[k];}); const allEqual=keys.every(k=>remainder[k]===min); if(allEqual)return null; const key=keys.find(k=>remainder[k]===min); return {short:key,rem:min,loc:locMap[key]||'',country:countryMap[key]||''}; }
function renderUI(itemsAgg,yataRaw,promRaw){
if(!contentEl)return;
const flowerTotals=countsForReq(itemsAgg,flowersReq,FLOWERS);
const plushTotals=countsForReq(itemsAgg,plushReq,PLUSHIES);
const fCalc=calcSetsAndRemainderFromCounts(flowerTotals,flowersReq.shortNames);
const pCalc=calcSetsAndRemainderFromCounts(plushTotals,plushReq.shortNames);
const totalSets=fCalc.sets+pCalc.sets;
const totalPoints=totalSets*10;
const yataMap=parseYata(yataRaw);
const promMap=parseProm(promRaw);
function pickAvFor(fullName,category){
const yataSum=sumAcrossCountriesFor(fullName,yataMap);
const promSum=sumAcrossCountriesFor(fullName,promMap);
let val=0,src='';
if(yataSum>0){val=yataSum;src='Y';}
else if(promSum>0){val=promSum;src='P';}
else {val=null;src=null;}
let col='#f7b3b3'; if(val!=null && category && MAX_ABROAD[category]) col=colorForPercent(val,MAX_ABROAD[category]);
return {val,src,color:col};
}
if(statusEl)statusEl.textContent=`Updated: ${new Date().toLocaleTimeString()} — Sets F:${fCalc.sets} P:${pCalc.sets}`;
if(summaryEl)summaryEl.innerHTML=`<div class="summary-line">Total sets: ${totalSets} | Points: ${totalPoints}</div>`;
const flowerDisplay=fCalc.remainder;
const plushDisplay=pCalc.remainder;
const lowFlower=findLowest(flowerDisplay,flowersReq.locByShort,flowersReq.countryByShort);
const lowPlush=findLowest(plushDisplay,plushReq.locByShort,plushReq.countryByShort);
let html='';
if(lowFlower)html+=`<div class="low-line">🛫 Low on ${escapeHtml(lowFlower.short)} — travel to ${escapeHtml(lowFlower.country)} ${escapeHtml(lowFlower.loc)} and import 🛬</div>`;
html+=`<div class="group-title">Flowers — sets: ${fCalc.sets} | pts: ${fCalc.sets*10}</div><ul class="item-list">`;
flowersReq.fullNames.forEach(full=>{ const short=FLOWERS[full].short; const total=flowerDisplay[short]??0; const picked=pickAvFor(full,'flowers'); const avText=(picked.val!=null&&picked.src)?`${picked.val} Av [${picked.src}]`:'—'; html+=`<li class="item-row" style="color:${picked.color}"><span class="item-name">${escapeHtml(short)}</span><span class="item-total">${total}</span><span class="item-av">(${avText})</span><span class="item-loc">${FLOWERS[full].loc||''}</span></li>`; });
html+='</ul>';
if(lowPlush)html+=`<div class="low-line">🛫 Low on ${escapeHtml(lowPlush.short)} — travel to ${escapeHtml(lowPlush.country)} ${escapeHtml(lowPlush.loc)} and import 🛬</div>`;
html+=`<div class="group-title">Plushies — sets: ${pCalc.sets} | pts: ${pCalc.sets*10}</div><ul class="item-list">`;
plushReq.fullNames.forEach(full=>{ const short=PLUSHIES[full].short; const total=plushDisplay[short]??0; const picked=pickAvFor(full,'plushies'); const avText=(picked.val!=null&&picked.src)?`${picked.val} Av [${picked.src}]`:'—'; html+=`<li class="item-row" style="color:${picked.color}"><span class="item-name">${escapeHtml(short)}</span><span class="item-total">${total}</span><span class="item-av">(${avText})</span><span class="item-loc">${PLUSHIES[full].loc||''}</span></li>`; });
html+='</ul>';
const xanInv=Number(itemsAgg[SPECIAL_DRUG]||0);
const yataX=sumAcrossCountriesFor(SPECIAL_DRUG,yataMap);
const promX=sumAcrossCountriesFor(SPECIAL_DRUG,promMap);
let xanPicked={val:null,src:null,color:'#f7b3b3'};
if(yataX>0)xanPicked={val:yataX,src:'Y',color:colorForPercent(yataX,MAX_ABROAD.drugs)};
else if(promX>0)xanPicked={val:promX,src:'P',color:colorForPercent(promX,MAX_ABROAD.drugs)};
const xanAvText=xanPicked.val!=null&&xanPicked.src?`${xanPicked.val} Av [${xanPicked.src}]`:'—';
html+=`<div class="group-title">Drugs</div><ul class="item-list"><li class="item-row" style="color:${xanPicked.color}"><span class="item-name">${SPECIAL_DRUG}</span><span class="item-total">${xanInv}</span><span class="item-av">(${xanAvText})</span><span class="item-loc">—</span></li></ul>`;
contentEl.innerHTML=html;
}
// Polling & main
let pollTimer=null, isRefreshing=false;
async function refreshAll(){ if(isRefreshing)return; isRefreshing=true; if(statusEl)statusEl.textContent='Fetching...'; try{
const itemsAgg=fetchDisplayViaDOM();
let yataRaw=null, promRaw=null;
try{ yataRaw=await gmGetJson(YATA_URL);}catch(e){}
try{ promRaw=await gmGetJson(PROM_URL);}catch(e){}
renderUI(itemsAgg,yataRaw,promRaw);
}catch(e){if(statusEl)statusEl.textContent='Error fetching'; console.error(e);} finally{ isRefreshing=false; if(pollTimer===null) pollTimer=setTimeout(refreshAll,POLL_INTERVAL_MS); }}
pollTimer=setTimeout(refreshAll,200);
})();