🌺 🐫 Points Exporter (Top Center)

Travel page tracker: fixed top-center, reads display + inventory, calculates sets, shows remaining & need, color-codes progress, low-on hints.

La data de 12-10-2025. Vezi ultima versiune.

// ==UserScript==
// @name         🌺 🐫 Points Exporter (Top Center)
// @namespace    http://tampermonkey.net/
// @version      3.3.3
// @description  Travel page tracker: fixed top-center, reads display + inventory, calculates sets, shows remaining & need, color-codes progress, low-on hints.
// @author       Nova
// @match        https://www.torn.com/page.php?sid=travel*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function() {
  'use strict';

  if (!/page\.php\?sid=travel/.test(location.href)) return;

  const FLOWERS = {
    "Dahlia": { short: "Dahlia", loc: "MX 🇲🇽" },
    "Orchid": { short: "Orchid", loc: "HW 🏝️" },
    "African Violet": { short: "Violet", loc: "SA 🇿🇦" },
    "Cherry Blossom": { short: "Cherry", loc: "JP 🇯🇵" },
    "Peony": { short: "Peony", loc: "CN 🇨🇳" },
    "Ceibo Flower": { short: "Ceibo", loc: "AR 🇦🇷" },
    "Edelweiss": { short: "Edelweiss", loc: "CH 🇨🇭" },
    "Crocus": { short: "Crocus", loc: "CA 🇨🇦" },
    "Heather": { short: "Heather", loc: "UK 🇬🇧" },
    "Tribulus Omanense": { short: "Tribulus", loc: "AE 🇦🇪" },
    "Banana Orchid": { short: "Banana", loc: "KY 🇰🇾" }
  };

  const PLUSHIES = {
    "Sheep Plushie": { short: "Sheep", loc: "B.B 🏪" },
    "Teddy Bear Plushie": { short: "Teddy", loc: "B.B 🏪" },
    "Kitten Plushie": { short: "Kitten", loc: "B.B 🏪" },
    "Jaguar Plushie": { short: "Jaguar", loc: "MX 🇲🇽" },
    "Wolverine Plushie": { short: "Wolverine", loc: "CA 🇨🇦" },
    "Nessie Plushie": { short: "Nessie", loc: "UK 🇬🇧" },
    "Red Fox Plushie": { short: "Fox", loc: "UK 🇬🇧" },
    "Monkey Plushie": { short: "Monkey", loc: "AR 🇦🇷" },
    "Chamois Plushie": { short: "Chamois", loc: "CH 🇨🇭" },
    "Panda Plushie": { short: "Panda", loc: "CN 🇨🇳" },
    "Lion Plushie": { short: "Lion", loc: "SA 🇿🇦" },
    "Camel Plushie": { short: "Camel", loc: "AE 🇦🇪" },
    "Stingray Plushie": { short: "Stingray", loc: "KY 🇰🇾" }
  };

  GM_addStyle(`
    #setTrackerPanel {
      position: fixed;
      top: 8px;
      left: 50%;
      transform: translateX(-50%);
      width: 250px;
      background: #0b0b0b;
      color: #eaeaea;
      font-family: "DejaVu Sans Mono", monospace;
      font-size: 9px;
      border: 1px solid #444;
      border-radius: 6px;
      z-index: 999999;
      box-shadow: 0 6px 16px rgba(0,0,0,0.5);
      max-height: 65vh;
      overflow-y: auto;
      line-height: 1.1;
    }
    #setTrackerHeader {
      background: #121212;
      padding: 4px 6px;
      cursor: pointer;
      font-weight:700;
      font-size:10px;
      border-bottom:1px solid #333;
      user-select:none;
    }
    #setTrackerContent { padding:6px; display:none; }
    .controls { margin-bottom:6px; }
    #setTrackerPanel button {
      margin: 2px 4px 6px 0;
      font-size:9px;
      padding:2px 6px;
      background:#171717;
      color:#eaeaea;
      border:1px solid #333;
      border-radius:3px;
      cursor:pointer;
    }
    #setTrackerPanel button:hover { background:#222; }
    .summary-line { font-weight:700; margin-bottom:6px; font-size:10px; color:#dfe7ff; }
    .low-line { color:#ff4d4d; font-weight:700; margin-bottom:6px; font-size:10px; }
    .group-title { font-weight:700; margin-top:6px; margin-bottom:4px; font-size:9.5px; }
    ul.item-list { margin:0 0 6px 0; padding:0; list-style:none; }
    li.item-row { display:flex; align-items:center; gap:6px; padding:2px 0; white-space:nowrap; }
    .item-name { flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; }
    .item-total { flex:0 0 40px; text-align:right; color:#cfe8c6; }
    .item-need { flex:0 0 60px; text-align:right; color:#f7b3b3; }
    .item-loc { flex:0 0 56px; text-align:right; color:#bcbcbc; font-size:8.5px; }
    #tc_status { font-size:9px; color:#bdbdbd; margin-bottom:6px; }
  `);

  const panel = document.createElement('div');
  panel.id = 'setTrackerPanel';
  panel.innerHTML = `
    <div id="setTrackerHeader">▶ 🌺 🐫 Points Exporter</div>
    <div id="setTrackerContent">
      <div class="controls">
        <button id="tc_refresh">Refresh</button>
        <button id="tc_setkey">Set API Key</button>
        <button id="tc_resetkey">Reset Key</button>
      </div>
      <div id="tc_status" class="summary-line">Waiting for API key...</div>
      <div id="tc_summary"></div>
      <div id="tc_content"></div>
    </div>
  `;
  document.body.appendChild(panel);

  const headerEl = panel.querySelector('#setTrackerHeader');
  const contentBox = panel.querySelector('#setTrackerContent');
  const statusEl = panel.querySelector('#tc_status');
  const summaryEl = panel.querySelector('#tc_summary');
  const contentEl = panel.querySelector('#tc_content');

  headerEl.addEventListener('click', () => {
    const open = contentBox.style.display === 'block';
    contentBox.style.display = open ? 'none' : 'block';
    headerEl.textContent = (open ? '▶' : '▼') + ' 🌺 🐫 Points Exporter';
  });

  let apiKey = GM_getValue('tornAPIKey', null);
  const POLL_INTERVAL_MS = 45000;
  let pollHandle = null;

  async function askKey(force) {
    if (!apiKey || force) {
      const k = prompt('Enter your Torn API key (display + inventory):', apiKey || '');
      if (k) {
        apiKey = k.trim();
        GM_setValue('tornAPIKey', apiKey);
      }
    }
    if (apiKey) {
      startPolling();
      await loadData();
    }
  }

  function startPolling() {
    if (pollHandle) return;
    pollHandle = setInterval(loadData, POLL_INTERVAL_MS);
  }
  function stopPolling() {
    if (!pollHandle) return;
    clearInterval(pollHandle);
    pollHandle = null;
  }

  function aggregate(data) {
    const items = {};
    const merge = (src) => {
      if (!src) return;
      const list = Array.isArray(src) ? src : Object.values(src);
      for (const e of list) {
        if (!e) continue;
        const n = e.name || e.item_name;
        const q = Number(e.quantity ?? 0);
        if (n) items[n] = (items[n] || 0) + q;
      }
    };
    merge(data.display);
    merge(data.inventory);
    return items;
  }

  function buildReq(map) {
    const names = Object.keys(map);
    const short = names.map(n => map[n].short);
    const loc = {};
    names.forEach(n => loc[map[n].short] = map[n].loc);
    return { names, short, loc };
  }

  const flowers = buildReq(FLOWERS);
  const plushies = buildReq(PLUSHIES);

  function counts(agg, req, map) {
    const c = {};
    req.short.forEach(s => c[s] = 0);
    req.names.forEach(n => {
      const s = map[n].short;
      const q = agg[n] || 0;
      c[s] = (c[s] || 0) + q;
    });
    return c;
  }

  function calc(counts, names) {
    const arr = names.map(n => counts[n] || 0);
    const sets = arr.length ? Math.min(...arr) : 0;
    const rem = {};
    names.forEach(n => rem[n] = Math.max(0, (counts[n] || 0) - sets));
    return { sets, rem };
  }

  function lowest(rem, loc) {
    let min = Infinity, key = null;
    for (const k in rem) if (rem[k] < min) { min = rem[k]; key = k; }
    return key ? { short: key, rem: min, loc: loc[key] } : null;
  }

  function render(items) {
    const f = counts(items, flowers, FLOWERS);
    const p = counts(items, plushies, PLUSHIES);
    const fC = calc(f, flowers.short);
    const pC = calc(p, plushies.short);
    const total = fC.sets + pC.sets;
    const pts = total * 10;
    summaryEl.innerHTML = `<div class="summary-line">Sets: ${total} | Points: ${pts}</div>`;
    let html = '';
    const lowF = lowest(fC.rem, flowers.loc);
    if (lowF) html += `<div class="low-line">🛫 Low on ${lowF.short} — go ${lowF.loc}</div>`;
    html += `<div class="group-title">Flowers — sets: ${fC.sets}</div><ul class="item-list">`;
    flowers.short.forEach(n => {
      html += `<li class="item-row"><span class="item-name">${n}</span><span class="item-total">${fC.rem[n]}</span><span class="item-loc">${flowers.loc[n]}</span></li>`;
    });
    html += `</ul>`;
    const lowP = lowest(pC.rem, plushies.loc);
    if (lowP) html += `<div class="low-line">🛫 Low on ${lowP.short} — go ${lowP.loc}</div>`;
    html += `<div class="group-title">Plushies — sets: ${pC.sets}</div><ul class="item-list">`;
    plushies.short.forEach(n => {
      html += `<li class="item-row"><span class="item-name">${n}</span><span class="item-total">${pC.rem[n]}</span><span class="item-loc">${plushies.loc[n]}</span></li>`;
    });
    html += `</ul>`;
    contentEl.innerHTML = html;
  }

  async function loadData() {
    summaryEl.innerHTML = '';
    contentEl.innerHTML = '';
    if (!apiKey) { statusEl.textContent = 'No API key. Prompting...'; await askKey(false); if (!apiKey) return; }
    statusEl.textContent = 'Fetching...';
    try {
      const res = await fetch(`https://api.torn.com/user/?selections=display,inventory&key=${apiKey}`);
      const data = await res.json();
      if (data.error) { statusEl.textContent = `Error: ${data.error.error}`; return; }
      render(aggregate(data));
      statusEl.textContent = 'Loaded.';
    } catch {
      statusEl.textContent = 'Fetch failed.';
    }
  }

  panel.querySelector('#tc_refresh').onclick = () => loadData();
  panel.querySelector('#tc_setkey').onclick = () => askKey(true);
  panel.querySelector('#tc_resetkey').onclick = () => { GM_setValue('tornAPIKey', null); apiKey = null; statusEl.textContent = 'Key cleared.'; };

  if (apiKey) { startPolling(); loadData(); }
  else setTimeout(() => askKey(false), 300);
  window.addEventListener('beforeunload', () => stopPolling());
})();