🌺 🐫 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. Δείτε την τελευταία έκδοση.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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