🌺 🐫 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.

Versión del día 11/10/2025. Echa un vistazo a la versión más reciente.

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