Sleazy Fork is available in English.

🌺🧸 Unified Display & Points (Above PDA) v3.4.1

Slim top-center toggle: display+inventory + YATA public stock, sets, missing, compact rows. Collapsed shows only "🌺 🧸 Exporter". Fixed under header, does not scroll. 45s refresh.

Versione datata 13/10/2025. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         🌺🧸 Unified Display & Points (Above PDA) v3.4.1
// @namespace    http://tampermonkey.net/
// @version      3.4.2
// @description  Slim top-center toggle: display+inventory + YATA public stock, sets, missing, compact rows. Collapsed shows only "🌺 🧸 Exporter". Fixed under header, does not scroll. 45s refresh.
// @match        https://www.torn.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @connect      api.torn.com
// @connect      yata.yt
// @run-at       document-end
// ==/UserScript==

(function(){
  'use strict';

  const PANEL_ID = 'unified_points_toggle_v3_4_1';
  const REFRESH_MS = 45 * 1000;
  const POINTS_PER_SET = 10;
  const YATA_URL = 'https://yata.yt/api/v1/travel/export/';

  // items: [FullName, {code, flag, short}]
  const FLOWERS_ORDER = [
    ["Dahlia",{code:'MEX',flag:'🇲🇽',short:'Dahlia'}],
    ["Orchid",{code:'HAW',flag:'🏝️',short:'Orchid'}],
    ["African Violet",{code:'SOU',flag:'🇿🇦',short:'A.Violet'}],
    ["Cherry Blossom",{code:'JAP',flag:'🇯🇵',short:'C.Blossom'}],
    ["Peony",{code:'CHI',flag:'🇨🇳',short:'Peony'}],
    ["Ceibo Flower",{code:'ARG',flag:'🇦🇷',short:'Ceibo'}],
    ["Edelweiss",{code:'SWI',flag:'🇨🇭',short:'Edelweiss'}],
    ["Crocus",{code:'CAN',flag:'🇨🇦',short:'Crocus'}],
    ["Heather",{code:'UNI',flag:'🇬🇧',short:'Heather'}],
    ["Tribulus Omanense",{code:'UAE',flag:'🇦🇪',short:'Tribulus'}],
    ["Banana Orchid",{code:'CAY',flag:'🇰🇾',short:'Banana'}]
  ];

  const PLUSHIES_ORDER = [
    ["Sheep Plushie",{code:'B.B',flag:'🏪',short:'Sheep'}],
    ["Teddy Bear Plushie",{code:'B.B',flag:'🏪',short:'Teddy'}],
    ["Kitten Plushie",{code:'B.B',flag:'🏪',short:'Kitten'}],
    ["Jaguar Plushie",{code:'MEX',flag:'🇲🇽',short:'Jaguar'}],
    ["Wolverine Plushie",{code:'CAN',flag:'🇨🇦',short:'Wolverine'}],
    ["Nessie Plushie",{code:'UNI',flag:'🇬🇧',short:'Nessie'}],
    ["Red Fox Plushie",{code:'UNI',flag:'🇬🇧',short:'R.Fox'}],
    ["Monkey Plushie",{code:'ARG',flag:'🇦🇷',short:'Monkey'}],
    ["Chamois Plushie",{code:'SWI',flag:'🇨🇭',short:'Chamois'}],
    ["Panda Plushie",{code:'CHI',flag:'🇨🇳',short:'Panda'}],
    ["Lion Plushie",{code:'SOU',flag:'🇿🇦',short:'Lion'}],
    ["Camel Plushie",{code:'UAE',flag:'🇦🇪',short:'Camel'}],
    ["Stingray Plushie",{code:'CAY',flag:'🇰🇾',short:'Stingray'}]
  ];

  const SPECIAL_DRUG = 'Xanax';
  const SHORT_COUNTRY = { MEX:'MEX', CAN:'CAN', JAP:'JAP', CHI:'CHI', UNI:'GBR', ARG:'ARG', SWI:'SWI', HAW:'HAW', UAE:'UAE', CAY:'CAY', SOU:'S.A', 'B.B':'Torn' };
  const COUNTRY_FULL = { MEX:'Mexico', CAN:'Canada', JAP:'Japan', CHI:'China', UNI:'United Kingdom', ARG:'Argentina', SWI:'Switzerland', HAW:'Hawaii', UAE:'UAE', CAY:'Cayman Islands', SOU:'South Africa', 'B.B':'Torn' };

  // compute dynamic top under PDA nav and keep fixed (no scroll)
  function getPDANavHeight(){
    const nav = document.querySelector('#pda-nav') || document.querySelector('.pda') || document.querySelector('#pda');
    return nav ? nav.offsetHeight : 40;
  }

  // styles: slim toggle (no box), top-center, slim panel width
  GM_addStyle(`
    #${PANEL_ID} { position: fixed; top: ${getPDANavHeight()}px; left: 50%; transform: translateX(-50%); z-index: 999999; pointer-events:auto; font-family: "DejaVu Sans Mono", monospace; font-size:12px; }
    /* toggle button (collapsed only) */
    #${PANEL_ID} .toggle-btn { display:flex; align-items:center; gap:8px; padding:6px 8px; cursor:pointer; color:#dfe7ff; transition:background .12s; border-radius:4px; user-select:none; }
    #${PANEL_ID} .toggle-btn:hover { background: rgba(255,255,255,0.02); }
    /* panel card */
    #${PANEL_ID} .card { margin-top:6px; width: 320px; max-width: 92vw; background: rgba(8,8,8,0.78); color:#e9eef8; border-radius:6px; box-shadow:0 10px 30px rgba(0,0,0,0.6); overflow:hidden; border:1px solid rgba(255,255,255,0.04); }
    /* collapsed hides card */
    #${PANEL_ID}.collapsed .card { display:none; }
    /* compact body */
    #${PANEL_ID} .body { padding:8px; display:block; font-size:12px; line-height:1.06; max-height:68vh; overflow:auto; }
    .tbl-head { display:flex; gap:6px; padding:4px 2px; color:#bfc9d6; font-weight:700; font-size:11px; border-bottom:1px solid rgba(255,255,255,0.03); margin-bottom:6px; }
    .tbl-row { display:flex; gap:6px; align-items:center; padding:4px 2px; white-space:nowrap; border-bottom:1px solid rgba(255,255,255,0.02); }
    .col-dot { flex:0 0 18px; display:flex; align-items:center; justify-content:flex-start; }
    .col-av { flex:0 0 44px; text-align:right; color:#cfe8c6; }
    .col-st { flex:0 0 72px; text-align:right; color:#f7b3b3; }
    .col-mis { flex:0 0 40px; text-align:right; color:#f0d08a; }
    .col-name { flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; color:#e9eef8; }
    .dot { width:10px; height:10px; border-radius:50%; margin-right:6px; flex:0 0 10px; }
    .stock-green{ background:#00c853; } .stock-orange{ background:#ff9800; } .stock-red{ background:#ff1744; } .stock-gray{ background:#9ea6b3; }
    /* bottom info (simple lines) */
    #${PANEL_ID} .bottom { padding:8px; border-top:1px solid rgba(255,255,255,0.03); color:#bfc9d6; font-size:12px; display:flex; flex-direction:column; gap:4px; }
    #${PANEL_ID} .bottom .meta { color:#9ea6b3; font-size:11px; }
    @media (max-width:900px){ #${PANEL_ID}{ left:6px; transform:none; } .col-st{ flex:0 0 56px; } .col-av{ flex:0 0 36px; } .col-mis{ flex:0 0 34px; } .card{ width:92vw; } }
  `);

  // create DOM: toggle label + card
  function buildUI(){
    let root = document.getElementById(PANEL_ID);
    if (root) return root;
    root = document.createElement('div');
    root.id = PANEL_ID;

    root.innerHTML = `
      <div class="toggle-btn" id="${PANEL_ID}-toggle">🌺 🧸 Exporter</div>
      <div class="card" role="region" aria-label="Unified Display & Points">
        <div class="body">
          <div id="${PANEL_ID}-status" style="font-weight:700;margin-bottom:6px;color:#dfe7ff">Waiting...</div>

          <div id="${PANEL_ID}-flowers">
            <div class="tbl-head"><div style="flex:0 0 18px"></div><div class="col-av">AV</div><div class="col-st">STK</div><div class="col-mis">MIS</div><div class="col-name">Flower</div></div>
            <div id="${PANEL_ID}-flowers-rows"></div>
          </div>

          <div id="${PANEL_ID}-plush" style="margin-top:8px;">
            <div class="tbl-head"><div style="flex:0 0 18px"></div><div class="col-av">AV</div><div class="col-st">STK</div><div class="col-mis">MIS</div><div class="col-name">Plushie</div></div>
            <div id="${PANEL_ID}-plush-rows"></div>
          </div>

          <div id="${PANEL_ID}-drugs" style="margin-top:8px;">
            <div class="tbl-head"><div style="flex:0 0 18px"></div><div class="col-av">AV</div><div class="col-st">STK</div><div class="col-mis"></div><div class="col-name">Drugs</div></div>
            <div id="${PANEL_ID}-drugs-rows"></div>
          </div>
        </div>

        <div class="bottom" id="${PANEL_ID}-bottom">
          <div id="${PANEL_ID}-flylines"></div>
          <div class="meta" id="${PANEL_ID}-meta">Points/set: ${POINTS_PER_SET} | Refresh: ${Math.round(REFRESH_MS/1000)}s</div>
        </div>
      </div>
    `;

    document.body.appendChild(root);

    // events: toggle expand/collapse
    const toggle = document.getElementById(`${PANEL_ID}-toggle`);
    toggle.addEventListener('click', () => {
      const root = document.getElementById(PANEL_ID);
      const collapsed = root.classList.toggle('collapsed');
      GM_setValue(`${PANEL_ID}-collapsed`, collapsed);
    });

    // clicking inside card shouldn't collapse
    const card = root.querySelector('.card');
    card.addEventListener('click', (e) => e.stopPropagation());

    // double-click on toggle to refresh quickly
    toggle.addEventListener('dblclick', (e) => { e.stopPropagation(); refreshAll(true); });

    return root;
  }

  // network helper
  function gmGetJson(url, timeout = 14000){
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: 'GET',
        url,
        timeout,
        onload: res => {
          try {
            const txt = (typeof res.response === 'string' && res.response) ? res.response : res.responseText;
            const parsed = txt && txt.length ? JSON.parse(txt) : res.response;
            resolve(parsed);
          } catch(e){ reject(e); }
        },
        onerror: err => reject(err),
        ontimeout: () => reject(new Error('timeout'))
      });
    });
  }

  // Torn display + inventory
  async function fetchTornDisplayInventory(){
    const key = GM_getValue('tornAPIKey', null);
    if (!key) return null;
    const url = `https://api.torn.com/user/?selections=display,inventory&key=${encodeURIComponent(key)}`;
    try {
      const data = await gmGetJson(url);
      if (!data || data.error) return null;
      return aggregateFromApiResponse(data);
    } catch (e) { console.warn('fetchTornDisplayInventory', e); return null; }
  }
  function aggregateFromApiResponse(data){
    const items = {};
    const push = (src) => {
      if (!src) return;
      const entries = Array.isArray(src) ? src : Object.values(src);
      for (const e of entries){
        if (!e) continue;
        const name = e.name || e.item_name || e.title || e.item || null;
        if (!name) continue;
        const qty = Number(e.quantity ?? e.qty ?? e.amount ?? 1) || 0;
        items[name] = (items[name] || 0) + qty;
      }
    };
    push(data.display);
    push(data.inventory);
    return items;
  }
  function fetchDisplayViaDOM(){
    const map = {};
    const els = document.querySelectorAll('.display-item, .item-wrap .item, .dcItem, .display_case_item, .item');
    if (els && els.length){
      els.forEach(el => {
        let name=''; let qty=0;
        const nameEl = el.querySelector('.item-name, .name, .title') || el.querySelector('a') || el;
        if (nameEl) name = (nameEl.innerText || '').trim();
        const qtyEl = el.querySelector('.item-amount, .count, .qty, .quantity') || el.querySelector('.item-qty');
        if (qtyEl) qty = parseInt((qtyEl.innerText||'').replace(/\D/g,'')) || 0;
        if (name) map[name] = (map[name] || 0) + qty;
      });
    }
    return map;
  }

  // YATA public endpoint
  async function fetchYata(){
    try {
      const data = await gmGetJson(YATA_URL, 14000);
      return data || null;
    } catch (e) { console.warn('fetchYata', e); return null; }
  }
  function buildYataMap(yataData){
    const map = {};
    if (!yataData || !yataData.stocks) return map;
    for (const [code, obj] of Object.entries(yataData.stocks)){
      const arr = Array.isArray(obj.stocks) ? obj.stocks : [];
      const m = {};
      for (const it of arr) if (it && it.name) m[it.name] = Number(it.quantity ?? 0) || 0;
      map[String(code).toUpperCase()] = m;
    }
    return map;
  }
  function sumYataFor(itemName, yataMap){
    let total = 0;
    for (const c of Object.keys(yataMap || {})) total += Number(yataMap[c][itemName] || 0);
    return total;
  }
  function bestCountryFor(itemName, yataMap){
    let best = { code:null, qty:0 };
    for (const [code, m] of Object.entries(yataMap || {})){
      const q = Number(m[itemName] || 0);
      if (q > best.qty) best = { code, qty: q };
    }
    return best;
  }

  // compute sets & missing
  function computeForGroup(displayMap, groupOrder){
    const counts = groupOrder.map(([name]) => Number(displayMap[name] || 0));
    const sets = counts.length ? Math.min(...counts) : 0;
    const missing = groupOrder.reduce((acc,[name]) => { const c = Number(displayMap[name]||0); acc[name] = Math.max(0, (sets+1) - c); return acc; }, {});
    const countsMap = groupOrder.reduce((acc,[name])=>{ acc[name]=Number(displayMap[name]||0); return acc; }, {});
    return { sets, countsMap, missing };
  }

  // stock color
  function stockClassByQty(q){
    q = Number(q || 0);
    if (q === 0) return 'stock-gray';
    if (q > 1000) return 'stock-green';
    if (q >= 600) return 'stock-orange';
    return 'stock-red';
  }

  function escapeHtml(s){ if (s == null) return ''; return String(s).replace(/[&<>"']/g, m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m])); }

  // render rows compact: AV | STK (+best code) | MIS | Name (flag + abbr)
  function renderGroupRows(containerId, order, countsMap, yataMap, missingMap){
    const el = document.getElementById(containerId);
    if (!el) return;
    let html = '';
    for (const [name, meta] of order){
      const av = Number(countsMap[name] || 0);
      const stk = sumYataFor(name, yataMap);
      const miss = Number(missingMap[name] || 0);
      const dotClass = stockClassByQty(stk);
      const best = bestCountryFor(name, yataMap);
      const bestCode = best.code ? String(best.code).toUpperCase() : '';
      const codeInfo = bestCode ? ` | ${bestCode}` : '';
      const abbr = meta.code ? meta.code : (SHORT_COUNTRY[meta.code] || '');
      html += `<div class="tbl-row"><div class="col-dot"><div class="dot ${dotClass}"></div></div><div class="col-av">${av}</div><div class="col-st">${stk}${codeInfo}</div><div class="col-mis">${miss>0?miss:'—'}</div><div class="col-name">${escapeHtml(meta.short||name)} ${meta.flag||''} ${meta.code?(' | '+meta.code):''}</div></div>`;
    }
    el.innerHTML = html;
  }

  // pick up to 4 flight suggestion lines: "Fly to UAE for Camel"
  function buildFlyLines(flowersMissing, plushMissing, yataMap){
    const lines = [];
    // iterate flowers then plush: each missing>0 -> find best country
    const collect = (order, missing) => {
      for (const [name, meta] of order){
        if (lines.length >= 6) break;
        const miss = Number(missing[name] || 0);
        if (miss <= 0) continue;
        const best = bestCountryFor(name, yataMap);
        // map code display: prefer SHORT_COUNTRY mapping with special S.A
        const code = best.code ? String(best.code).toUpperCase() : meta.code || '';
        const codeLabel = (code === 'SOU' || code === 'S.A' || code === 'SA' || code === 'S.A.') ? 'S.A' : (SHORT_COUNTRY[code] || code || (meta.code||''));
        const target = codeLabel || (meta.code||'');
        lines.push(`Fly to ${target} for ${meta.short || name}`);
      }
    };
    collect(FLOWERS_ORDER, flowersMissing);
    collect(PLUSHIES_ORDER, plushMissing);
    // if nothing missing, show blank placeholder
    return lines.length ? lines.slice(0,4) : ['Fly to —'];
  }

  // main render
  function renderUI(displayMap, yataData){
    const yataMap = buildYataMap(yataData);
    const flowers = computeForGroup(displayMap, FLOWERS_ORDER);
    const plush = computeForGroup(displayMap, PLUSHIES_ORDER);
    const totalSets = (flowers.sets||0) + (plush.sets||0);
    const totalPoints = totalSets * POINTS_PER_SET;

    const status = document.getElementById(`${PANEL_ID}-status`);
    status.textContent = `Updated: ${new Date().toLocaleTimeString()} — Sets:${totalSets} Points:${totalPoints}`;

    renderGroupRows(`${PANEL_ID}-flowers-rows`, FLOWERS_ORDER, flowers.countsMap, yataMap, flowers.missing);
    renderGroupRows(`${PANEL_ID}-plush-rows`, PLUSHIES_ORDER, plush.countsMap, yataMap, plush.missing);

    const drugsEl = document.getElementById(`${PANEL_ID}-drugs-rows`);
    const xanInv = Number(displayMap[SPECIAL_DRUG] || 0);
    const xanStk = Number(yataMap['SOU']?.[SPECIAL_DRUG] || 0) || Number(yataMap['S.A']?.[SPECIAL_DRUG] || 0);
    const xanDot = stockClassByQty(xanStk);
    drugsEl.innerHTML = `<div class="tbl-row"><div class="col-dot"><div class="dot ${xanDot}"></div></div><div class="col-av">${xanInv}</div><div class="col-st">${xanStk} | SOU</div><div class="col-mis">—</div><div class="col-name">${escapeHtml(SPECIAL_DRUG)} 🇿🇦</div></div>`;

    const flylines = buildFlyLines(flowers.missing, plush.missing, yataMap);
    const flyContainer = document.getElementById(`${PANEL_ID}-flylines`);
    flyContainer.innerHTML = flylines.map(l => `<div>${escapeHtml(l)}</div>`).join('');

    // footer meta remains as configured
  }

  // refresh flow
  let timer = null;
  async function refreshAll(force=false){
    try {
      const statusEl = document.getElementById(`${PANEL_ID}-status`);
      if (statusEl) statusEl.textContent = 'Fetching...';

      const tornPromise = fetchTornDisplayInventory();
      const yataPromise = fetchYata();
      const [displayFromApi, yataData] = await Promise.all([tornPromise, yataPromise]);

      let displayMap = {};
      if (displayFromApi && Object.keys(displayFromApi).length > 0) displayMap = displayFromApi;
      else {
        const domMap = fetchDisplayViaDOM();
        displayMap = domMap || displayFromApi || {};
      }

      renderUI(displayMap, yataData || null);
      if (statusEl) statusEl.textContent = `Updated: ${new Date().toLocaleTimeString()} — Sets and points shown above`;
    } catch (e){
      console.warn('refreshAll err', e);
      const statusEl = document.getElementById(`${PANEL_ID}-status`);
      if (statusEl) statusEl.textContent = 'Update failed';
    }
  }

  // Torn key prompt
  function askTornKey(){
    const cur = GM_getValue('tornAPIKey','');
    const k = prompt('Enter Torn API key (display + inventory permissions):', cur || '');
    if (k !== null) { GM_setValue('tornAPIKey', String(k).trim()); refreshAll(true); }
  }

  // boot
  buildUI();
  // restore collapse state: if collapsed store = true -> hide card
  const wasCollapsed = GM_getValue(`${PANEL_ID}-collapsed`, false);
  if (wasCollapsed) document.getElementById(PANEL_ID).classList.add('collapsed');

  refreshAll(true);
  if (timer) clearInterval(timer);
  timer = setInterval(()=>refreshAll(false), REFRESH_MS);
  window.addEventListener('beforeunload', ()=>{ if (timer) clearInterval(timer); });

  // reposition when header height changes
  function reposition(){
    const r = document.getElementById(PANEL_ID);
    if (!r) return;
    const top = getPDANavHeight();
    r.style.top = top + 'px';
  }
  reposition();
  window.addEventListener('resize', reposition);
  const obs = new MutationObserver(reposition);
  obs.observe(document.documentElement || document.body, { childList:true, subtree:true, attributes:true });

})();