🌺 🐫 Points & Stock Tracker (Display Case + YATA Unified PDA) v4.2

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.

נכון ליום 11-10-2025. ראה הגרסה האחרונה.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);
})();