// ==UserScript==
// @name 🌺 🐫 Points Exporter (Top Center)
// @namespace http://tampermonkey.net/
// @version 3.3.3
// @description Travel page tracker: fixed top-center, reads display + inventory, calculates sets, shows remaining & need, color-codes progress, low-on hints.
// @author Nova
// @match https://www.torn.com/page.php?sid=travel*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function() {
'use strict';
if (!/page\.php\?sid=travel/.test(location.href)) return;
const FLOWERS = {
"Dahlia": { short: "Dahlia", loc: "MX 🇲🇽" },
"Orchid": { short: "Orchid", loc: "HW 🏝️" },
"African Violet": { short: "Violet", loc: "SA 🇿🇦" },
"Cherry Blossom": { short: "Cherry", loc: "JP 🇯🇵" },
"Peony": { short: "Peony", loc: "CN 🇨🇳" },
"Ceibo Flower": { short: "Ceibo", loc: "AR 🇦🇷" },
"Edelweiss": { short: "Edelweiss", loc: "CH 🇨🇭" },
"Crocus": { short: "Crocus", loc: "CA 🇨🇦" },
"Heather": { short: "Heather", loc: "UK 🇬🇧" },
"Tribulus Omanense": { short: "Tribulus", loc: "AE 🇦🇪" },
"Banana Orchid": { short: "Banana", loc: "KY 🇰🇾" }
};
const PLUSHIES = {
"Sheep Plushie": { short: "Sheep", loc: "B.B 🏪" },
"Teddy Bear Plushie": { short: "Teddy", loc: "B.B 🏪" },
"Kitten Plushie": { short: "Kitten", loc: "B.B 🏪" },
"Jaguar Plushie": { short: "Jaguar", loc: "MX 🇲🇽" },
"Wolverine Plushie": { short: "Wolverine", loc: "CA 🇨🇦" },
"Nessie Plushie": { short: "Nessie", loc: "UK 🇬🇧" },
"Red Fox Plushie": { short: "Fox", loc: "UK 🇬🇧" },
"Monkey Plushie": { short: "Monkey", loc: "AR 🇦🇷" },
"Chamois Plushie": { short: "Chamois", loc: "CH 🇨🇭" },
"Panda Plushie": { short: "Panda", loc: "CN 🇨🇳" },
"Lion Plushie": { short: "Lion", loc: "SA 🇿🇦" },
"Camel Plushie": { short: "Camel", loc: "AE 🇦🇪" },
"Stingray Plushie": { short: "Stingray", loc: "KY 🇰🇾" }
};
GM_addStyle(`
#setTrackerPanel {
position: fixed;
top: 8px;
left: 50%;
transform: translateX(-50%);
width: 250px;
background: #0b0b0b;
color: #eaeaea;
font-family: "DejaVu Sans Mono", monospace;
font-size: 9px;
border: 1px solid #444;
border-radius: 6px;
z-index: 999999;
box-shadow: 0 6px 16px rgba(0,0,0,0.5);
max-height: 65vh;
overflow-y: auto;
line-height: 1.1;
}
#setTrackerHeader {
background: #121212;
padding: 4px 6px;
cursor: pointer;
font-weight:700;
font-size:10px;
border-bottom:1px solid #333;
user-select:none;
}
#setTrackerContent { padding:6px; display:none; }
.controls { margin-bottom:6px; }
#setTrackerPanel button {
margin: 2px 4px 6px 0;
font-size:9px;
padding:2px 6px;
background:#171717;
color:#eaeaea;
border:1px solid #333;
border-radius:3px;
cursor:pointer;
}
#setTrackerPanel button:hover { background:#222; }
.summary-line { font-weight:700; margin-bottom:6px; font-size:10px; color:#dfe7ff; }
.low-line { color:#ff4d4d; font-weight:700; margin-bottom:6px; font-size:10px; }
.group-title { font-weight:700; margin-top:6px; margin-bottom:4px; font-size:9.5px; }
ul.item-list { margin:0 0 6px 0; 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 40px; text-align:right; color:#cfe8c6; }
.item-need { flex:0 0 60px; text-align:right; color:#f7b3b3; }
.item-loc { flex:0 0 56px; text-align:right; color:#bcbcbc; font-size:8.5px; }
#tc_status { font-size:9px; color:#bdbdbd; margin-bottom:6px; }
`);
const panel = document.createElement('div');
panel.id = 'setTrackerPanel';
panel.innerHTML = `
<div id="setTrackerHeader">▶ 🌺 🐫 Points Exporter</div>
<div id="setTrackerContent">
<div class="controls">
<button id="tc_refresh">Refresh</button>
<button id="tc_setkey">Set API Key</button>
<button id="tc_resetkey">Reset Key</button>
</div>
<div id="tc_status" class="summary-line">Waiting for API key...</div>
<div id="tc_summary"></div>
<div id="tc_content"></div>
</div>
`;
document.body.appendChild(panel);
const headerEl = panel.querySelector('#setTrackerHeader');
const contentBox = panel.querySelector('#setTrackerContent');
const statusEl = panel.querySelector('#tc_status');
const summaryEl = panel.querySelector('#tc_summary');
const contentEl = panel.querySelector('#tc_content');
headerEl.addEventListener('click', () => {
const open = contentBox.style.display === 'block';
contentBox.style.display = open ? 'none' : 'block';
headerEl.textContent = (open ? '▶' : '▼') + ' 🌺 🐫 Points Exporter';
});
let apiKey = GM_getValue('tornAPIKey', null);
const POLL_INTERVAL_MS = 45000;
let pollHandle = null;
async function askKey(force) {
if (!apiKey || force) {
const k = prompt('Enter your Torn API key (display + inventory):', apiKey || '');
if (k) {
apiKey = k.trim();
GM_setValue('tornAPIKey', apiKey);
}
}
if (apiKey) {
startPolling();
await loadData();
}
}
function startPolling() {
if (pollHandle) return;
pollHandle = setInterval(loadData, POLL_INTERVAL_MS);
}
function stopPolling() {
if (!pollHandle) return;
clearInterval(pollHandle);
pollHandle = null;
}
function aggregate(data) {
const items = {};
const merge = (src) => {
if (!src) return;
const list = Array.isArray(src) ? src : Object.values(src);
for (const e of list) {
if (!e) continue;
const n = e.name || e.item_name;
const q = Number(e.quantity ?? 0);
if (n) items[n] = (items[n] || 0) + q;
}
};
merge(data.display);
merge(data.inventory);
return items;
}
function buildReq(map) {
const names = Object.keys(map);
const short = names.map(n => map[n].short);
const loc = {};
names.forEach(n => loc[map[n].short] = map[n].loc);
return { names, short, loc };
}
const flowers = buildReq(FLOWERS);
const plushies = buildReq(PLUSHIES);
function counts(agg, req, map) {
const c = {};
req.short.forEach(s => c[s] = 0);
req.names.forEach(n => {
const s = map[n].short;
const q = agg[n] || 0;
c[s] = (c[s] || 0) + q;
});
return c;
}
function calc(counts, names) {
const arr = names.map(n => counts[n] || 0);
const sets = arr.length ? Math.min(...arr) : 0;
const rem = {};
names.forEach(n => rem[n] = Math.max(0, (counts[n] || 0) - sets));
return { sets, rem };
}
function lowest(rem, loc) {
let min = Infinity, key = null;
for (const k in rem) if (rem[k] < min) { min = rem[k]; key = k; }
return key ? { short: key, rem: min, loc: loc[key] } : null;
}
function render(items) {
const f = counts(items, flowers, FLOWERS);
const p = counts(items, plushies, PLUSHIES);
const fC = calc(f, flowers.short);
const pC = calc(p, plushies.short);
const total = fC.sets + pC.sets;
const pts = total * 10;
summaryEl.innerHTML = `<div class="summary-line">Sets: ${total} | Points: ${pts}</div>`;
let html = '';
const lowF = lowest(fC.rem, flowers.loc);
if (lowF) html += `<div class="low-line">🛫 Low on ${lowF.short} — go ${lowF.loc}</div>`;
html += `<div class="group-title">Flowers — sets: ${fC.sets}</div><ul class="item-list">`;
flowers.short.forEach(n => {
html += `<li class="item-row"><span class="item-name">${n}</span><span class="item-total">${fC.rem[n]}</span><span class="item-loc">${flowers.loc[n]}</span></li>`;
});
html += `</ul>`;
const lowP = lowest(pC.rem, plushies.loc);
if (lowP) html += `<div class="low-line">🛫 Low on ${lowP.short} — go ${lowP.loc}</div>`;
html += `<div class="group-title">Plushies — sets: ${pC.sets}</div><ul class="item-list">`;
plushies.short.forEach(n => {
html += `<li class="item-row"><span class="item-name">${n}</span><span class="item-total">${pC.rem[n]}</span><span class="item-loc">${plushies.loc[n]}</span></li>`;
});
html += `</ul>`;
contentEl.innerHTML = html;
}
async function loadData() {
summaryEl.innerHTML = '';
contentEl.innerHTML = '';
if (!apiKey) { statusEl.textContent = 'No API key. Prompting...'; await askKey(false); if (!apiKey) return; }
statusEl.textContent = 'Fetching...';
try {
const res = await fetch(`https://api.torn.com/user/?selections=display,inventory&key=${apiKey}`);
const data = await res.json();
if (data.error) { statusEl.textContent = `Error: ${data.error.error}`; return; }
render(aggregate(data));
statusEl.textContent = 'Loaded.';
} catch {
statusEl.textContent = 'Fetch failed.';
}
}
panel.querySelector('#tc_refresh').onclick = () => loadData();
panel.querySelector('#tc_setkey').onclick = () => askKey(true);
panel.querySelector('#tc_resetkey').onclick = () => { GM_setValue('tornAPIKey', null); apiKey = null; statusEl.textContent = 'Key cleared.'; };
if (apiKey) { startPolling(); loadData(); }
else setTimeout(() => askKey(false), 300);
window.addEventListener('beforeunload', () => stopPolling());
})();