🌺 🐫 Points Exporter

Travel page auto tracker for flowers/plushies. Reads Display+Inventory. Shows short names, remaining(after sets), needed, and source. Color codes progress. Prompts API key on first run. Auto refresh every 45s.

当前为 2025-10-06 提交的版本,查看 最新版本

// ==UserScript==
// @name         🌺 🐫 Points Exporter
// @namespace    http://tampermonkey.net/
// @version      3.3
// @description  Travel page auto tracker for flowers/plushies. Reads Display+Inventory. Shows short names, remaining(after sets), needed, and source. Color codes progress. Prompts API key on first run. Auto refresh every 45s.
// @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 🇲🇽", country: "Mexico" },
    "Orchid":            { short: "Orchid",    loc: "HW 🏝️", country: "Hawaii" },
    "African Violet":    { short: "Violet",    loc: "SA 🇿🇦", country: "South Africa" },
    "Cherry Blossom":    { short: "Cherry",    loc: "JP 🇯🇵", country: "Japan" },
    "Peony":             { short: "Peony",     loc: "CN 🇨🇳", country: "China" },
    "Ceibo Flower":      { short: "Ceibo",     loc: "AR 🇦🇷", country: "Argentina" },
    "Edelweiss":         { short: "Edelweiss", loc: "CH 🇨🇭", country: "Switzerland" },
    "Crocus":            { short: "Crocus",    loc: "CA 🇨🇦", country: "Canada" },
    "Heather":           { short: "Heather",   loc: "UK 🇬🇧", country: "United Kingdom" },
    "Tribulus Omanense": { short: "Tribulus",  loc: "AE 🇦🇪", country: "UAE" },
    "Banana Orchid":     { short: "Banana",    loc: "KY 🇰🇾", country: "Cayman Islands" }
  };

  const PLUSHIES = {
    "Sheep Plushie":     { short: "Sheep",     loc: "B.B 🏪", country: "Torn City" },
    "Teddy Bear Plushie":{ short: "Teddy",     loc: "B.B 🏪", country: "Torn City" },
    "Kitten Plushie":    { short: "Kitten",    loc: "B.B 🏪", country: "Torn City" },
    "Jaguar Plushie":    { short: "Jaguar",    loc: "MX 🇲🇽", country: "Mexico" },
    "Wolverine Plushie": { short: "Wolverine", loc: "CA 🇨🇦", country: "Canada" },
    "Nessie Plushie":    { short: "Nessie",    loc: "UK 🇬🇧", country: "United Kingdom" },
    "Red Fox Plushie":   { short: "Fox",       loc: "UK 🇬🇧", country: "United Kingdom" },
    "Monkey Plushie":    { short: "Monkey",    loc: "AR 🇦🇷", country: "Argentina" },
    "Chamois Plushie":   { short: "Chamois",   loc: "CH 🇨🇭", country: "Switzerland" },
    "Panda Plushie":     { short: "Panda",     loc: "CN 🇨🇳", country: "China" },
    "Lion Plushie":      { short: "Lion",      loc: "SA 🇿🇦", country: "South Africa" },
    "Camel Plushie":     { short: "Camel",     loc: "AE 🇦🇪", country: "UAE" },
    "Stingray Plushie":  { short: "Stingray",  loc: "KY 🇰🇾", country: "Cayman Islands" }
  };

  GM_addStyle(`
    #setTrackerPanel {
      position: fixed;
      top: 100px;
      left: 18px;
      width: 250px;
      background: #0b0b0b;
      color: #eaeaea;
      font-family: "DejaVu Sans Mono", monospace;
      font-size: 9px;
      border: 1px solid #444;
      border-radius: 6px;
      z-index: 2147483647;
      box-shadow: 0 6px 16px rgba(0,0,0,0.5);
      max-height: 65vh;
      overflow-y: auto;
      line-height: 1.2;
    }
    #setTrackerHeader {
      background: #121212;
      padding: 4px 6px;
      cursor: pointer;
      font-weight: 700;
      font-size: 10px;
      border-bottom: 1px solid #333;
      user-select: none;
    }
    #setTrackerContent { padding: 5px; display: none; }
    .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:5px; margin-bottom:3px; font-size:9.5px; }
    ul.item-list { margin:0 0 4px 0; padding:0; list-style:none; }
    li.item-row { display:flex; align-items:center; gap:4px; padding:1px 0; white-space:nowrap; }
    .item-name { flex:1; overflow:hidden; text-overflow:ellipsis; }
    .item-total { flex:0 0 40px; text-align:right; }
    .item-need { flex:0 0 45px; text-align:right; }
    .item-loc { flex:0 0 55px; text-align:right; color:#bcbcbc; font-size:8.5px; }
  `);

  const panel = document.createElement('div');
  panel.id = 'setTrackerPanel';
  panel.innerHTML = `
    <div id="setTrackerHeader">▶ 🌺 🐫 Points Exporter</div>
    <div id="setTrackerContent">
      <div class="summary-line" id="tc_status">Waiting for 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');
  headerEl.addEventListener('click', () => {
    const open = contentBox.style.display === 'block';
    contentBox.style.display = open ? 'none' : 'block';
    headerEl.textContent = (open ? '▶' : '▼') + ' 🌺 🐫 Points Exporter';
  });

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

  let apiKey = GM_getValue('tornAPIKey', null);

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

  async function fetchData() {
    if (!apiKey) { await askKey(false); if (!apiKey) return; }
    statusEl.textContent = 'Fetching data...';
    const url = `https://api.torn.com/user/?selections=display,inventory&key=${encodeURIComponent(apiKey)}`;
    const r = await fetch(url);
    const data = await r.json();
    if (data.error) { statusEl.textContent = data.error.error; return; }
    const all = {};
    const merge = (src) => { Object.values(src || {}).forEach(x => { if (!x.name) return; all[x.name] = (all[x.name] || 0) + (x.quantity || 0); }); };
    merge(data.display); merge(data.inventory);
    render(all);
    statusEl.textContent = 'Loaded.';
  }

  function getCounts(all, map) {
    const res = {}; Object.keys(map).forEach(n => res[map[n].short] = all[n] || 0); return res;
  }

  function render(all) {
    const flowerCounts = getCounts(all, FLOWERS);
    const plushCounts  = getCounts(all, PLUSHIES);

    const fMax = Math.max(...Object.values(flowerCounts), 0);
    const pMax = Math.max(...Object.values(plushCounts), 0);

    const color = (v, max) => {
      if (!max) return 'gray';
      const pct = (v / max) * 100;
      if (pct >= 75) return '#00ff66';
      if (pct >= 40) return '#3399ff';
      return '#ff4444';
    };

    let html = '';

    html += `<div class="group-title">Flowers</div><ul class="item-list">`;
    Object.entries(flowerCounts).forEach(([n, c]) => {
      const need = fMax - c;
      html += `<li class="item-row" style="color:${color(c,fMax)}">
        <span class="item-name">${n}</span>
        <span class="item-total">${c}</span>
        <span class="item-need">(${need} need)</span>
        <span class="item-loc">${FLOWERS[Object.keys(FLOWERS).find(fn=>FLOWERS[fn].short===n)].loc}</span>
      </li>`;
    });
    html += `</ul>`;

    html += `<div class="group-title">Plushies</div><ul class="item-list">`;
    Object.entries(plushCounts).forEach(([n, c]) => {
      const need = pMax - c;
      html += `<li class="item-row" style="color:${color(c,pMax)}">
        <span class="item-name">${n}</span>
        <span class="item-total">${c}</span>
        <span class="item-need">(${need} need)</span>
        <span class="item-loc">${PLUSHIES[Object.keys(PLUSHIES).find(fn=>PLUSHIES[fn].short===n)].loc}</span>
      </li>`;
    });
    html += `</ul>`;

    contentEl.innerHTML = html;
  }

  fetchData();
  setInterval(fetchData, 45000);
})();