// ==UserScript==
// @name 🌺 🐫 YATA Travel Stock — Flowers, Plushies, Xanax (Above PDA)
// @namespace http://tampermonkey.net/
// @version 1.2.0
// @description Shows foreign stock (flowers, plushies, and Xanax in SA) from YATA export. Refreshes every 20s. Above PDA.
// @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
// @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 POLL_MS = 20 * 1000;
const FLOWERS = [
"Dahlia", "Orchid", "African Violet", "Cherry Blossom",
"Peony", "Ceibo Flower", "Edelweiss", "Crocus",
"Heather", "Tribulus Omanense", "Banana Orchid"
];
const PLUSHIES = [
"Sheep Plushie", "Teddy Bear Plushie", "Kitten Plushie",
"Jaguar Plushie", "Wolverine Plushie", "Nessie Plushie",
"Red Fox Plushie", "Monkey Plushie", "Chamois Plushie",
"Panda Plushie", "Lion Plushie", "Camel Plushie",
"Stingray Plushie"
];
const TRACKED = new Set([...FLOWERS, ...PLUSHIES, "Xanax"]);
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'
};
function getPDANavHeight() {
const nav = document.querySelector('#pda-nav') || document.querySelector('.pda');
return nav ? nav.offsetHeight : 40;
}
GM_addStyle(`
#yataStockPanel {
position: fixed;
top: ${getPDANavHeight()}px;
left: 18px;
width: 320px;
background: #080808;
color: #e9eef6;
font-family: "DejaVu Sans Mono", monospace;
font-size: 11px;
border: 1px solid #333;
border-radius: 6px;
z-index: 999999;
box-shadow: 0 6px 18px rgba(0,0,0,0.6);
max-height: 70vh;
overflow-y: auto;
line-height: 1.1;
padding-bottom:6px;
}
#yataHeader {
background:#101010;
padding:6px 8px;
cursor:pointer;
font-weight:700;
font-size:12px;
border-bottom:1px solid #2b2b2b;
user-select:none;
}
#yataContent { padding:8px; display:block; }
.yata-controls { margin-bottom:8px; }
.yata-controls button {
margin:2px 6px 6px 0;
font-size:11px;
padding:4px 8px;
background:#121212;
color:#e9eef6;
border:1px solid #2b2b2b;
border-radius:4px;
cursor:pointer;
}
.yata-controls button:hover { background:#1b1b1b; }
#yataStatus { font-size:11px; color:#bdbdbd; margin-bottom:8px; }
.country-block { margin-bottom:8px; border-top:1px dashed #222; padding-top:6px; }
.country-title { font-weight:700; margin-bottom:4px; display:flex; justify-content:space-between; align-items:center; }
.country-title .ct-left { font-size:12px; }
.country-upd { font-size:10px; color:#9ea6b3; }
.item-row { display:flex; justify-content:space-between; gap:8px; padding:2px 0; align-items:center; }
.item-name { flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; }
.qty { width:64px; text-align:right; font-weight:700; }
.cost { width:56px; text-align:right; color:#aeb7c4; font-size:11px; }
.status-dot { width:10px; height:10px; border-radius:50%; display:inline-block; margin-right:6px; vertical-align:middle; }
.dot-green { background:#00c853; }
.dot-yellow { background:#ffb300; }
.dot-red { background:#ff1744; }
.small-note { font-size:11px; color:#9ea6b3; margin-top:6px; }
`);
const panel = document.createElement('div');
panel.id = 'yataStockPanel';
panel.innerHTML = `
<div id="yataHeader">▶ 🌺 🐫 YATA Stock (Flowers & Plushies)</div>
<div id="yataContent">
<div class="yata-controls">
<button id="yata_refresh">Refresh</button>
<button id="yata_toggle_all">Collapse</button>
</div>
<div id="yataStatus">Loading...</div>
<div id="yataList"></div>
<div class="small-note">Data from yata.yt · flowers, plushies, and Xanax (SA) only · refresh 20s</div>
</div>
`;
document.body.appendChild(panel);
const header = panel.querySelector('#yataHeader');
const content = panel.querySelector('#yataContent');
const statusEl = panel.querySelector('#yataStatus');
const listEl = panel.querySelector('#yataList');
const btnRefresh = panel.querySelector('#yata_refresh');
const btnToggle = panel.querySelector('#yata_toggle_all');
let collapsed = GM_getValue('yata_collapsed', false);
function updateCollapseUI() {
content.style.display = collapsed ? 'none' : 'block';
header.textContent = (collapsed ? '▶' : '▼') + ' 🌺 🐫 YATA Stock (Flowers & Plushies)';
btnToggle.textContent = collapsed ? 'Expand' : 'Collapse';
GM_setValue('yata_collapsed', collapsed);
}
updateCollapseUI();
header.addEventListener('click', () => { collapsed = !collapsed; updateCollapseUI(); });
btnToggle.addEventListener('click', () => { collapsed = !collapsed; updateCollapseUI(); });
btnRefresh.addEventListener('click', () => fetchAndRender(true));
function qtyClass(q) {
if (q <= 0) return 'dot-red';
if (q <= 10) return 'dot-yellow';
return 'dot-green';
}
function gmFetchJson(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
responseType: 'json',
onload: (res) => {
let data = res.response;
if (!data && res.responseText) {
try { data = JSON.parse(res.responseText); } catch(e){}
}
if (!data) reject(new Error('No JSON'));
else resolve(data);
},
onerror: reject
});
});
}
function renderExport(data) {
if (!data || !data.stocks) {
statusEl.textContent = 'No stock data.';
listEl.innerHTML = '';
return;
}
const ts = data.timestamp ? new Date(data.timestamp * 1000) : null;
statusEl.textContent = ts ? `Last payload: ${ts.toUTCString()}` : 'Live data';
let html = '';
Object.keys(data.stocks).forEach(code => {
const c = data.stocks[code];
if (!c) return;
const name = COUNTRY_NAMES[code] || code.toUpperCase();
const upd = c.update ? new Date(c.update * 1000) : null;
const items = Array.isArray(c.stocks) ? c.stocks.filter(it => {
if (TRACKED.has(it.name)) {
if (it.name === "Xanax") return code === "sou"; // only SA
return true;
}
return false;
}) : [];
if (!items.length) return;
html += `<div class="country-block" data-country="${code}">
<div class="country-title">
<div class="ct-left">${name} <span class="country-upd">${upd ? upd.toUTCString() : ''}</span></div>
<div class="ct-right">${code}</div>
</div>`;
items.forEach(it => {
const q = Number(it.quantity ?? 0);
const cost = it.cost != null ? Number(it.cost) : null;
const dot = `<span class="status-dot ${qtyClass(q)}"></span>`;
html += `<div class="item-row">
<div class="item-name">${dot}${escapeHtml(it.name)}</div>
<div class="cost">${cost != null ? formatNumber(cost) : '-'}</div>
<div class="qty">${q}</div>
</div>`;
});
html += `</div>`;
});
listEl.innerHTML = html || '<div style="color:#999;">No tracked items found.</div>';
}
function escapeHtml(s) {
return s ? s.replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"'}[m])) : '';
}
function formatNumber(n) {
return n.toLocaleString('en-US');
}
let pollHandle = null;
let lastPayloadTimestamp = 0;
async function fetchAndRender(force=false) {
try {
statusEl.textContent = 'Fetching YATA export...';
const data = await gmFetchJson(YATA_URL);
if (!force && data.timestamp === lastPayloadTimestamp) return;
lastPayloadTimestamp = data.timestamp;
renderExport(data);
} catch (err) {
statusEl.textContent = 'Fetch error: ' + (err.message || err);
listEl.innerHTML = '';
}
}
function startPolling() {
if (pollHandle) return;
fetchAndRender(true);
pollHandle = setInterval(() => fetchAndRender(false), POLL_MS);
}
startPolling();
window.addEventListener('beforeunload', () => clearInterval(pollHandle));
})();