// ==UserScript==
// @name 🌺 🐫 Points & Stock Tracker (Display Case + YATA Unified PDA) v4.2
// @namespace http://tampermonkey.net/
// @version 4.2.0
// @description Unified PDA panel showing your display case flowers/plushies and YATA foreign shop stock. Auto-refresh 45s. Works while flying. Xanax only in S.A. shown at bottom.
// @author Nova
// @match https://www.torn.com/page.php?sid=travel*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @connect yata.yt
// @connect api.torn.com
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
if (!/page\.php\?sid=travel/.test(location.href)) return;
const YATA_URL = 'https://yata.yt/api/v1/travel/export/';
const REFRESH_MS = 45 * 1000;
const PANEL_WIDTH = 320;
const FLOWERS_MAP = {
"Dahlia": "MX 🇲🇽",
"Orchid": "HW 🏝️",
"African Violet": "SA 🇿🇦",
"Cherry Blossom": "JP 🇯🇵",
"Peony": "CN 🇨🇳",
"Ceibo Flower": "AR 🇦🇷",
"Edelweiss": "CH 🇨🇭",
"Crocus": "CA 🇨🇦",
"Heather": "UK 🇬🇧",
"Tribulus Omanense": "AE 🇦🇪",
"Banana Orchid": "KY 🇰🇾"
};
const PLUSHIES_MAP = {
"Jaguar Plushie": "MX 🇲🇽",
"Wolverine Plushie": "CA 🇨🇦",
"Nessie Plushie": "UK 🇬🇧",
"Red Fox Plushie": "UK 🇬🇧",
"Monkey Plushie": "AR 🇦🇷",
"Chamois Plushie": "CH 🇨🇭",
"Panda Plushie": "CN 🇨🇳",
"Lion Plushie": "SA 🇿🇦",
"Camel Plushie": "AE 🇦🇪",
"Stingray Plushie": "KY 🇰🇾"
};
const COUNTRY_NAMES = {
mex: 'Mexico', cay: 'Cayman Islands', can: 'Canada', haw: 'Hawaii', uni: 'United Kingdom',
arg: 'Argentina', swi: 'Switzerland', jap: 'Japan', chi: 'China', uae: 'UAE', sou: 'South Africa'
};
const TRACKED_SET = new Set([...Object.keys(FLOWERS_MAP), ...Object.keys(PLUSHIES_MAP), "Xanax"]);
GM_addStyle(`
#ptsStockPanel { position:fixed; top:${(document.querySelector('#pda-nav')?.offsetHeight||40)}px; left:18px; width:${PANEL_WIDTH}px; background:#0b0b0b; color:#eaeaea;
font-family:"DejaVu Sans Mono",monospace; font-size:11px; border:1px solid #444; border-radius:6px; z-index:999999; box-shadow:0 6px 16px rgba(0,0,0,0.5);
max-height:70vh; overflow-y:auto; line-height:1.15; }
#ptsHeader { background:#121212; padding:6px 8px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #333; }
#ptsContent { padding:8px; }
.row { display:flex; justify-content:space-between; align-items:center; padding:2px 0; }
.dot { width:10px; height:10px; border-radius:50%; display:inline-block; margin-right:5px; }
.g { background:#00c853; } .y { background:#ffb300; } .r { background:#ff1744; }
.section-title { font-weight:700; border-bottom:1px dashed #222; margin:4px 0; padding-bottom:2px; }
.country-block { border-top:1px dashed #222; margin-top:5px; padding-top:5px; }
.summary-line { font-weight:700; color:#bfc9d6; margin:5px 0; }
`);
const panel = document.createElement('div');
panel.id = 'ptsStockPanel';
panel.innerHTML = `
<div id="ptsHeader">
<div>🌺 🐫 Points & Stock</div>
<div>
<button id="pts_refresh">⟳</button>
<button id="pts_key">🔑</button>
</div>
</div>
<div id="ptsContent">
<div id="pts_status" style="color:#9ea6b3;margin-bottom:5px;">Initializing...</div>
<div class="section-title">My Display Case</div>
<div id="mySummary" class="summary-line"></div>
<div id="myList"></div>
<div class="section-title">Foreign Stock (YATA)</div>
<div id="foreignList"></div>
<div style="font-size:10px;color:#777;margin-top:6px;">Refresh every 45s</div>
</div>`;
document.body.appendChild(panel);
const statusEl = panel.querySelector('#pts_status');
const mySummary = panel.querySelector('#mySummary');
const myList = panel.querySelector('#myList');
const foreignList = panel.querySelector('#foreignList');
const btnKey = panel.querySelector('#pts_key');
const btnRefresh = panel.querySelector('#pts_refresh');
let apiKey = GM_getValue('tornAPIKey', null);
btnKey.onclick = () => {
const key = prompt('Enter your Torn API key (user key with "display" permission):', apiKey || '');
if (key) {
apiKey = key.trim();
GM_setValue('tornAPIKey', apiKey);
statusEl.textContent = 'API key saved.';
refreshAll(true);
}
};
btnRefresh.onclick = () => refreshAll(true);
function gmGetJson(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET', url, responseType: 'json',
onload: res => {
let d = res.response || JSON.parse(res.responseText);
resolve(d);
},
onerror: reject
});
});
}
async function fetchDisplayCase() {
if (!apiKey) throw new Error('No API key');
const url = `https://api.torn.com/user/?selections=display&key=${apiKey}`;
const data = await gmGetJson(url);
if (data.error) throw new Error(data.error.error);
const out = {};
for (const i of Object.values(data.display || {})) {
out[i.name] = (out[i.name] || 0) + i.quantity;
}
return out;
}
async function fetchYata() {
const data = await gmGetJson(YATA_URL);
return data;
}
function dot(q) {
if (q <= 0) return 'r';
if (q <= 10) return 'y';
return 'g';
}
function renderDisplayCase(items) {
const fTotals = {}, pTotals = {};
for (const [name, qty] of Object.entries(items)) {
if (FLOWERS_MAP[name]) fTotals[name] = qty;
if (PLUSHIES_MAP[name]) pTotals[name] = qty;
}
const fSets = Object.keys(FLOWERS_MAP).every(k => items[k] > 0) ? Math.min(...Object.values(fTotals)) : 0;
const pSets = Object.keys(PLUSHIES_MAP).every(k => items[k] > 0) ? Math.min(...Object.values(pTotals)) : 0;
mySummary.textContent = `Sets: ${fSets + pSets} | Points: ${(fSets + pSets) * 10}`;
let html = '<b>Flowers</b><br>';
for (const [n, q] of Object.entries(fTotals)) {
html += `<div class="row"><span class="dot ${dot(q)}"></span>${n} (${q}) <span>${FLOWERS_MAP[n]}</span></div>`;
}
html += '<br><b>Plushies</b><br>';
for (const [n, q] of Object.entries(pTotals)) {
html += `<div class="row"><span class="dot ${dot(q)}"></span>${n} (${q}) <span>${PLUSHIES_MAP[n]}</span></div>`;
}
myList.innerHTML = html;
}
function renderYataStock(yata) {
if (!yata?.stocks) {
foreignList.innerHTML = 'No YATA data';
return;
}
let html = '';
for (const [code, obj] of Object.entries(yata.stocks)) {
const items = obj.stocks.filter(i =>
(FLOWERS_MAP[i.name] || PLUSHIES_MAP[i.name]) ||
(i.name === 'Xanax' && code === 'sou')
);
if (!items.length) continue;
html += `<div class="country-block"><b>${COUNTRY_NAMES[code] || code}</b><br>`;
for (const i of items) {
html += `<div class="row"><span class="dot ${dot(i.quantity)}"></span>${i.name} <span>${i.quantity}</span></div>`;
}
html += '</div>';
}
foreignList.innerHTML = html;
}
async function refreshAll(force=false) {
statusEl.textContent = 'Updating...';
try {
const [disp, yata] = await Promise.allSettled([fetchDisplayCase(), fetchYata()]);
if (disp.status === 'fulfilled') renderDisplayCase(disp.value);
if (yata.status === 'fulfilled') renderYataStock(yata.value);
statusEl.textContent = `Updated ${new Date().toLocaleTimeString()}`;
} catch (e) {
statusEl.textContent = 'Error: ' + e.message;
}
}
refreshAll(true);
setInterval(() => refreshAll(false), REFRESH_MS);
})();