Torn Display Case Tracker (Clean One-line Rows)

Shows flowers and plushies in one clean line: name | total | (ms X) | LOC (initials + flag). Uses Torn public API key.

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

// ==UserScript==
// @name         Torn Display Case Tracker (Clean One-line Rows)
// @namespace    http://tampermonkey.net/
// @version      2.5
// @description  Shows flowers and plushies in one clean line: name | total | (ms X) | LOC (initials + flag). Uses Torn public API key.
// @author       Nova
// @match        https://www.torn.com/displaycase.php*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function() {
  'use strict';

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

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

  GM_addStyle(`
    #setTrackerPanel {
      position: fixed;
      top: 100px;
      left: 20px;
      width: 300px;
      background: #0b0b0b;
      color: #eaeaea;
      font-family: "DejaVu Sans Mono", "Liberation Mono", monospace;
      font-size: 10px;
      border: 1px solid #444;
      border-radius: 6px;
      z-index: 2147483647;
      box-shadow: 0 6px 18px rgba(0,0,0,0.6);
      max-height: 65vh;
      overflow-y: auto;
      line-height: 1.2;
    }
    #setTrackerHeader {
      background: #121212;
      padding: 5px 6px;
      cursor: pointer;
      font-weight: 700;
      font-size: 12px;
      border-bottom: 1px solid #333;
      user-select: none;
    }
    #setTrackerContent { padding: 6px; display: none; }
    #setTrackerPanel .controls { margin-bottom:6px; }
    #setTrackerPanel button {
      margin: 2px 3px 6px 0;
      font-size: 10px;
      padding: 2px 6px;
      background: #171717;
      color: #eaeaea;
      border: 1px solid #333;
      border-radius: 3px;
      cursor: pointer;
    }
    #setTrackerPanel button:hover { background: #222; }
    .group-title { font-weight:700; margin-top:6px; margin-bottom:4px; }
    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 46px; text-align:right; color:#cfe8c6; }
    .item-ms { flex:0 0 48px; text-align:right; color:#f7b3b3; }
    .item-loc { flex:0 0 60px; text-align:right; color:#bcbcbc; font-size:9px; }
    #tc_status { font-size:11px; color:#bdbdbd; margin-bottom:6px; }
  `);

  const panel = document.createElement('div');
  panel.id = 'setTrackerPanel';
  panel.innerHTML = `
    <div id="setTrackerHeader">▶ Display Case Tracker</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">Waiting for key...</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 ? '▶' : '▼') + ' Display Case Tracker';
  });

  const statusEl = panel.querySelector('#tc_status');
  const contentEl = panel.querySelector('#tc_content');
  panel.querySelector('#tc_refresh').addEventListener('click', () => loadData());
  panel.querySelector('#tc_setkey').addEventListener('click', () => askKey(true));
  panel.querySelector('#tc_resetkey').addEventListener('click', () => {
    GM_setValue('tornAPIKey', null);
    apiKey = null;
    statusEl.textContent = 'Key cleared. Click Set API Key.';
    contentEl.innerHTML = '';
  });

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

  async function askKey(force) {
    if (!apiKey || force) {
      const k = prompt('Enter your Torn PUBLIC API key (public/minimal access):', apiKey || '');
      if (k) {
        apiKey = k.trim();
        GM_setValue('tornAPIKey', apiKey);
      }
    }
    if (apiKey) loadData();
  }

  function aggregateDisplay(data) {
    const items = {};
    const displayRaw = data.display || data.displaycase || data.displayCase || null;
    if (!displayRaw) return items;
    const entries = Array.isArray(displayRaw) ? displayRaw : Object.values(displayRaw);
    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;
    }
    return items;
  }

  function compareMode(required, items) {
    const names = Object.keys(required);
    const counts = names.map(n => items[n] || 0);
    const highest = counts.length ? Math.max(...counts) : 0;
    const diff = {};
    names.forEach(name => {
      const total = items[name] || 0;
      diff[name] = { total, missing: Math.max(0, highest - total), loc: required[name] };
    });
    return { highest, diff };
  }

  function render(items) {
    const flowers = compareMode(FLOWERS, items);
    const plushies = compareMode(PLUSHIES, items);

    let html = '';
    html += `<div class="group-title">Flowers (highest: ${flowers.highest})</div>`;
    html += `<ul class="item-list">`;
    Object.keys(FLOWERS).forEach(name => {
      const d = flowers.diff[name];
      html += `<li class="item-row">
        <span class="item-name">${name}</span>
        <span class="item-total">${d.total}</span>
        <span class="item-ms">(ms ${d.missing})</span>
        <span class="item-loc">${d.loc}</span>
      </li>`;
    });
    html += `</ul>`;

    html += `<div class="group-title">Plushies (highest: ${plushies.highest})</div>`;
    html += `<ul class="item-list">`;
    Object.keys(PLUSHIES).forEach(name => {
      const d = plushies.diff[name];
      html += `<li class="item-row">
        <span class="item-name">${name}</span>
        <span class="item-total">${d.total}</span>
        <span class="item-ms">(ms ${d.missing})</span>
        <span class="item-loc">${d.loc}</span>
      </li>`;
    });
    html += `</ul>`;

    contentEl.innerHTML = html;
  }

  async function loadData() {
    contentEl.innerHTML = '';
    if (!apiKey) {
      statusEl.textContent = 'No API key set. Click "Set API Key".';
      return;
    }
    statusEl.textContent = 'Fetching display via API...';
    try {
      const url = `https://api.torn.com/user/?selections=display&key=${encodeURIComponent(apiKey)}`;
      const res = await fetch(url);
      const data = await res.json();
      if (data.error) {
        statusEl.textContent = `API error: ${data.error.error} (code ${data.error.code})`;
        contentEl.innerHTML = '';
        return;
      }
      const items = aggregateDisplay(data);
      if (Object.keys(items).length === 0) {
        statusEl.textContent = 'No display items found. Key may lack permission.';
        return;
      }
      render(items);
      statusEl.textContent = 'Loaded.';
    } catch (err) {
      statusEl.textContent = 'Fetch failed.';
      contentEl.innerHTML = `<div style="color:#f88;">${err.message}</div>`;
    }
  }

  if (!apiKey) askKey(false);
  else loadData();

})();