// ==UserScript==
// @name 🌺 🐫 Points Exporter
// @namespace http://tampermonkey.net/
// @version 3.3
// @description Travel page auto tracker for flowers/plushies. Reads Display+Inventory. Shows short names, remaining(after sets), needed, and source. Color codes progress. Prompts API key on first run. Auto refresh every 45s.
// @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 🇲🇽", 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" }
};
GM_addStyle(`
#setTrackerPanel {
position: fixed;
top: 100px;
left: 18px;
width: 250px;
background: #0b0b0b;
color: #eaeaea;
font-family: "DejaVu Sans Mono", monospace;
font-size: 9px;
border: 1px solid #444;
border-radius: 6px;
z-index: 2147483647;
box-shadow: 0 6px 16px rgba(0,0,0,0.5);
max-height: 65vh;
overflow-y: auto;
line-height: 1.2;
}
#setTrackerHeader {
background: #121212;
padding: 4px 6px;
cursor: pointer;
font-weight: 700;
font-size: 10px;
border-bottom: 1px solid #333;
user-select: none;
}
#setTrackerContent { padding: 5px; display: none; }
.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:5px; margin-bottom:3px; font-size:9.5px; }
ul.item-list { margin:0 0 4px 0; padding:0; list-style:none; }
li.item-row { display:flex; align-items:center; gap:4px; padding:1px 0; white-space:nowrap; }
.item-name { flex:1; overflow:hidden; text-overflow:ellipsis; }
.item-total { flex:0 0 40px; text-align:right; }
.item-need { flex:0 0 45px; text-align:right; }
.item-loc { flex:0 0 55px; text-align:right; color:#bcbcbc; font-size:8.5px; }
`);
const panel = document.createElement('div');
panel.id = 'setTrackerPanel';
panel.innerHTML = `
<div id="setTrackerHeader">▶ 🌺 🐫 Points Exporter</div>
<div id="setTrackerContent">
<div class="summary-line" id="tc_status">Waiting for 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');
headerEl.addEventListener('click', () => {
const open = contentBox.style.display === 'block';
contentBox.style.display = open ? 'none' : 'block';
headerEl.textContent = (open ? '▶' : '▼') + ' 🌺 🐫 Points Exporter';
});
const statusEl = panel.querySelector('#tc_status');
const summaryEl = panel.querySelector('#tc_summary');
const contentEl = panel.querySelector('#tc_content');
let apiKey = GM_getValue('tornAPIKey', null);
async function askKey(force) {
if (!apiKey || force) {
const k = prompt('Enter your Torn API key (with inventory permission):', apiKey || '');
if (k) { apiKey = k.trim(); GM_setValue('tornAPIKey', apiKey); }
}
}
async function fetchData() {
if (!apiKey) { await askKey(false); if (!apiKey) return; }
statusEl.textContent = 'Fetching data...';
const url = `https://api.torn.com/user/?selections=display,inventory&key=${encodeURIComponent(apiKey)}`;
const r = await fetch(url);
const data = await r.json();
if (data.error) { statusEl.textContent = data.error.error; return; }
const all = {};
const merge = (src) => { Object.values(src || {}).forEach(x => { if (!x.name) return; all[x.name] = (all[x.name] || 0) + (x.quantity || 0); }); };
merge(data.display); merge(data.inventory);
render(all);
statusEl.textContent = 'Loaded.';
}
function getCounts(all, map) {
const res = {}; Object.keys(map).forEach(n => res[map[n].short] = all[n] || 0); return res;
}
function render(all) {
const flowerCounts = getCounts(all, FLOWERS);
const plushCounts = getCounts(all, PLUSHIES);
const fMax = Math.max(...Object.values(flowerCounts), 0);
const pMax = Math.max(...Object.values(plushCounts), 0);
const color = (v, max) => {
if (!max) return 'gray';
const pct = (v / max) * 100;
if (pct >= 75) return '#00ff66';
if (pct >= 40) return '#3399ff';
return '#ff4444';
};
let html = '';
html += `<div class="group-title">Flowers</div><ul class="item-list">`;
Object.entries(flowerCounts).forEach(([n, c]) => {
const need = fMax - c;
html += `<li class="item-row" style="color:${color(c,fMax)}">
<span class="item-name">${n}</span>
<span class="item-total">${c}</span>
<span class="item-need">(${need} need)</span>
<span class="item-loc">${FLOWERS[Object.keys(FLOWERS).find(fn=>FLOWERS[fn].short===n)].loc}</span>
</li>`;
});
html += `</ul>`;
html += `<div class="group-title">Plushies</div><ul class="item-list">`;
Object.entries(plushCounts).forEach(([n, c]) => {
const need = pMax - c;
html += `<li class="item-row" style="color:${color(c,pMax)}">
<span class="item-name">${n}</span>
<span class="item-total">${c}</span>
<span class="item-need">(${need} need)</span>
<span class="item-loc">${PLUSHIES[Object.keys(PLUSHIES).find(fn=>PLUSHIES[fn].short===n)].loc}</span>
</li>`;
});
html += `</ul>`;
contentEl.innerHTML = html;
}
fetchData();
setInterval(fetchData, 45000);
})();