Torn display + abroad stock panel. Flight-type aware. Depletion & restock prediction. Next-item fallback.
// ==UserScript==
// @name 💫 Points Maker (Depletion + Restock + Flight)
// @namespace http://tampermonkey.net/
// @version 1.3
// @description Torn display + abroad stock panel. Flight-type aware. Depletion & restock prediction. Next-item fallback.
// @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';
// CONFIG
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 };
const FLIGHT_TIMES = { // in minutes
'Mexico': { standard: 26, airstrip: 18, wlt: 13, business: 8 },
'Cayman Islands': { standard: 35, airstrip: 25, wlt: 18, business: 11 },
'Canada': { standard: 41, airstrip: 29, wlt: 20, business: 12 },
'Hawaii': { standard: 134, airstrip: 94, wlt: 67, business: 40 },
'United Kingdom': { standard: 159, airstrip: 111, wlt: 80, business: 48 },
'Argentina': { standard: 167, airstrip: 117, wlt: 83, business: 50 },
'Switzerland': { standard: 175, airstrip: 123, wlt: 88, business: 53 },
'Japan': { standard: 225, airstrip: 158, wlt: 113, business: 68 },
'China': { standard: 242, airstrip: 169, wlt: 121, business: 72 },
'UAE': { standard: 271, airstrip: 190, wlt: 135, business: 81 },
'South Africa': { standard: 297, airstrip: 208, wlt: 149, business: 89 },
};
const RESTOCK_HOURS = {
"Camel Plushie": 0.37, "Panda Plushie": 0.30, "Peony": 1.87, "Tribulus Omanense": 1.78,
"Lion Plushie": 0.32, "African Violet": 1.63, "Jaguar Plushie": 0.45, "Cherry Blossom": 2.02,
"Monkey Plushie": 0.38, "Heather": 2.15, "Ceibo Flower": 2.15, "Red Fox Plushie": 0.47,
"Nessie Plushie": 0.38, "Stingray Plushie": 0.37, "Wolverine Plushie": 0.38, "Banana Orchid": 1.58,
"Orchid": 1.97, "Crocus": 1.90, "Chamois Plushie": 0.35, "Dahlia": 1.88, "Edelweiss": 1.67
};
const DEPLETION_RATES = {
// placeholder values, can be adjusted with actual rates
"Camel Plushie": 0.1, "Panda Plushie": 0.2, "Peony": 0.3, "Tribulus Omanense": 0.3,
"Lion Plushie": 0.1, "African Violet": 0.2, "Jaguar Plushie": 0.2, "Cherry Blossom": 0.3,
"Monkey Plushie": 0.1, "Heather": 0.3, "Ceibo Flower": 0.3, "Red Fox Plushie": 0.1,
"Nessie Plushie": 0.1, "Stingray Plushie": 0.1, "Wolverine Plushie": 0.1, "Banana Orchid": 0.3,
"Orchid": 0.3, "Crocus": 0.3, "Chamois Plushie": 0.1, "Dahlia": 0.3, "Edelweiss": 0.3,
"Xanax": 0.1
};
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';
}
function stockClassByQty(q) { q = Number(q || 0); if (q === 0) return 'stock-red'; if (q >= 1500) return 'stock-green'; if (q >= 321 && q <= 749) return 'stock-orange'; return 'stock-gray'; }
// BUILD REQUIRED LISTS
function buildRequiredList(mapObj) {
const fullNames = Object.keys(mapObj);
const shortNames = fullNames.map(fn => mapObj[fn].short);
const locByShort = {};
const countryByShort = {};
fullNames.forEach(fn => { const s = mapObj[fn].short; locByShort[s] = mapObj[fn].loc; countryByShort[s] = mapObj[fn].country; });
return { fullNames, shortNames, locByShort, countryByShort };
}
const flowersReq = buildRequiredList(FLOWERS);
const plushReq = buildRequiredList(PLUSHIES);
// STYLE
GM_addStyle(`
#${PANEL_ID} { position: fixed; top: 42px; left: 100px; width: 300px; background: #0b0b0b; color: #eaeaea; font-family: "DejaVu Sans Mono", 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; display:flex; align-items:center; gap:6px; }
#${PANEL_ID} .controls { padding:6px; display:flex; gap:6px; flex-wrap:wrap; }
#${PANEL_ID} .controls select, #${PANEL_ID} .controls button { font-size:9px; padding:2px 6px; background:#171717; color:#eaeaea; border:1px solid #333; border-radius:3px; cursor:pointer; }
#${PANEL_ID} .controls select:hover, #${PANEL_ID} .controls button:hover { background:#222; }
#${PANEL_ID} .summary-line { font-weight:700; margin:6px; font-size:10px; color:#dfe7ff; }
#${PANEL_ID} .low-line { color:#ff4d4d; font-weight:700; margin:6px; font-size:10px; }
#${PANEL_ID} .group-title { font-weight:700; margin:6px 6px 2px 6px; font-size:9.5px; }
#${PANEL_ID} ul.item-list { margin:4px 6px 8px 12px; padding:0; list-style:none; }
#${PANEL_ID} li.item-row { display:flex; align-items:center; gap:6px; padding:2px 0; white-space:nowrap; }
#${PANEL_ID} .item-name { flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; }
#${PANEL_ID} .item-total { flex:0 0 36px; text-align:right; color:#cfe8c6; }
#${PANEL_ID} .item-av { flex:0 0 60px; text-align:right; color:#f7b3b3; }
#${PANEL_ID} .item-loc { flex:0 0 36px; text-align:right; color:#bcbcbc; font-size:8.5px; }
#${PANEL_ID} #tc_status { font-size:9px; color:#bdbdbd; margin:6px; }
.stock-green{ color:#00c853 !important; } .stock-orange{ color:#ff9800 !important; } .stock-red{ color:#ff1744 !important; } .stock-gray{ color:#9ea6b3 !important; }
`);
// UI BUILD
let statusEl, summaryEl, contentEl;
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_controls" class="controls">
<button id="tc_refresh">Refresh</button>
Flight Type:
<select id="flightTypeSelect">
<option value="standard">Standard</option>
<option value="airstrip">Airstrip</option>
<option value="wlt">WLT</option>
<option value="business">Business</option>
</select>
<button id="tc_setkey">Set API Key</button>
<button id="tc_resetkey">Reset Key</button>
</div>
<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(); });
root.querySelector('#tc_refresh').addEventListener('click', () => refreshAll(true));
root.querySelector('#flightTypeSelect').addEventListener('change', (e) => { GM_setValue('selectedFlightType', e.target.value); refreshAll(true); });
root.querySelector('#tc_setkey').addEventListener('click', () => askKey(true));
root.querySelector('#tc_resetkey').addEventListener('click', () => { GM_setValue('tornAPIKey', null); apiKey = null; if(statusEl) statusEl.textContent='Key cleared'; if(summaryEl) summaryEl.innerHTML=''; if(contentEl) contentEl.innerHTML=''; stopPolling(); });
statusEl = root.querySelector('#tc_status');
summaryEl = root.querySelector('#tc_summary');
contentEl = root.querySelector('#tc_content');
// load flight type
const ft = GM_getValue('selectedFlightType','standard');
root.querySelector('#flightTypeSelect').value = ft;
}
buildUI();
// NETWORK
function gmGetJson(url, timeout = 14000) {
return new Promise((resolve, reject) => {
try {
GM_xmlhttpRequest({
method: 'GET',
url,
timeout,
onload: res => { try { resolve(JSON.parse(res.responseText || res.response)); } catch(e){reject(e);} },
onerror: err => reject(err),
ontimeout: () => reject(new Error('timeout'))
});
} catch (e) { reject(e); }
});
}
// DISPLAY & DOM
function aggregateFromApiResponse(data) {
const items = {};
const push = (src) => {
if(!src) return;
const entries = Array.isArray(src) ? src : Object.values(src);
for(const e of entries){
if(!e) continue;
const name = e.name || e.item_name || e.title || e.item || null;
if(!name) continue;
const qty = Number(e.quantity ?? e.qty ?? e.amount ?? 1) || 0;
items[name] = (items[name] || 0) + qty;
}
};
push(data.display); push(data.inventory);
return items;
}
async function fetchTornDisplayInventory() {
const key = GM_getValue('tornAPIKey', null); if(!key) return null;
const url = `https://api.torn.com/user/?selections=display,inventory&key=${encodeURIComponent(key)}`;
try { const data = await gmGetJson(url); if(!data || data.error) return null; return aggregateFromApiResponse(data); }
catch(e){console.warn('fetchTornDisplayInventory', e); return null;}
}
function fetchDisplayViaDOM() {
const map={};
document.querySelectorAll('.display-item, .display_case_item, .dcItem, .item-wrap .item').forEach(el=>{
let name=''; let qty=0;
const nameEl = el.querySelector('.item-name, .name, .title'); if(nameEl) name = nameEl.textContent.trim();
const qtyEl = el.querySelector('.quantity, .qty'); if(qtyEl) qty = Number(qtyEl.textContent.trim().replace(/[^\d]/g,''))||1;
if(name) map[name] = qty;
});
return map;
}
function predictStockAtArrival(itemName, currentQty, flightMinutes) {
const depRate = DEPLETION_RATES[itemName] || 0.1;
const restockH = RESTOCK_HOURS[itemName] || 0.5;
const arrivalH = flightMinutes / 60;
let expected = Math.max(0, currentQty - depRate * arrivalH * MAX_ABROAD.flowers); // simplified factor
if(arrivalH >= restockH) expected = Math.max(expected, MAX_ABROAD.flowers); // reset stock after restock
return expected;
}
function selectNextAvailableItem(itemsObj, flightMins) {
// itemsObj: { name: currentStock }
let sortedItems = Object.keys(itemsObj).sort((a,b)=> (itemsObj[a]-DEPLETION_RATES[a]*flightMins/60) - (itemsObj[b]-DEPLETION_RATES[b]*flightMins/60));
for(const item of sortedItems){
const qty = predictStockAtArrival(item, itemsObj[item], flightMins);
if(qty>0) return item;
}
return null;
}
async function refreshAll(force=false){
if(!statusEl || !contentEl) return;
statusEl.textContent='Fetching display...';
const flightType = GM_getValue('selectedFlightType','standard');
const flightMinutes = FLIGHT_TIMES['Mexico'][flightType] || 26; // placeholder
const inventory = await fetchTornDisplayInventory() || fetchDisplayViaDOM();
if(!inventory) { statusEl.textContent='No data'; return; }
const nextFlower = selectNextAvailableItem(flowersReq.fullNames.reduce((acc,n)=>{acc[n]=inventory[n]||0;return acc;},{}), flightMinutes);
const nextPlushie = selectNextAvailableItem(plushReq.fullNames.reduce((acc,n)=>{acc[n]=inventory[n]||0;return acc;},{}), flightMinutes);
contentEl.innerHTML = `
<div class="summary-line">Next Flower: ${nextFlower||'None'} | Next Plushie: ${nextPlushie||'None'} | Flight: ${flightType}</div>
`;
statusEl.textContent='Updated';
}
function askKey(force=false){
const current = GM_getValue('tornAPIKey', '');
const val = prompt('Enter Torn API Key:', current||'');
if(val) { GM_setValue('tornAPIKey', val); refreshAll(true); }
}
function stopPolling(){ clearInterval(window.pointsMakerInterval); }
window.pointsMakerInterval = setInterval(refreshAll, POLL_INTERVAL_MS);
refreshAll();
})();