OC Reward Values

Shows the real market value of completed-OC reward items (the paintings/weapons Torn prices at $0 or a stale catalog price) and a per-OC "Items total", reading live item-market prices straight from Torn with YOUR own API key. Talks only to api.torn.com. Works in Torn PDA.

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         OC Reward Values
// @namespace    RussianRob
// @version      1.3.1
// @description  Shows the real market value of completed-OC reward items (the paintings/weapons Torn prices at $0 or a stale catalog price) and a per-OC "Items total", reading live item-market prices straight from Torn with YOUR own API key. Talks only to api.torn.com. Works in Torn PDA.
// @author       RussianRob
// @license      MIT
// @match        https://www.torn.com/factions.php*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @connect      api.torn.com
// ==/UserScript==

(function () {
  "use strict";
  const SCRIPT_VERSION = "1.3.1";
  const KEY_STORE  = "ocwk_torn_api_key";
  const CACHE_KEY  = "ocwk_listings_v3";
  const TTL_MS     = 10 * 60 * 1000;
  const FETCH_GAP  = 350;
  const API_BASE   = "https://api.torn.com/v2/market/";
  const BAR_ID     = "ocw-keybar";

  let _apiKey = "";
  let _byId = {};
  let _byName = {};
  let _cache = {};
  let _fetching = false;
  let _state = "none";
  let _expanded = false;
  let _barSig = null;
  let _idName = {};

  const fmt = (n) => "$" + Number(n).toLocaleString("en-US");
  function isVisible(el) { return !!(el && el.getClientRects && el.getClientRects().length > 0); }

  function loadKey()  { try { _apiKey = (GM_getValue(KEY_STORE, "") || "").trim(); } catch (_) { _apiKey = ""; } }
  function saveKey(k) { _apiKey = String(k || "").trim(); try { GM_setValue(KEY_STORE, _apiKey); } catch (_) {} }
  function clearKey() { _apiKey = ""; try { GM_setValue(KEY_STORE, ""); } catch (_) {} }
  function loadCache() {
    try { const raw = GM_getValue(CACHE_KEY, ""); if (raw) { const o = JSON.parse(raw); if (o && typeof o === "object") _cache = o; } } catch (_) { _cache = {}; }
    const now = Date.now();
    for (const [id, e] of Object.entries(_cache)) {
      if (e && now - (e.at || 0) < TTL_MS) { _byId[id] = Number(e.price) || 0; if (e.name && e.price > 0) _byName[String(e.name).toLowerCase()] = e.price; }
    }
  }
  function saveCache() { try { GM_setValue(CACHE_KEY, JSON.stringify(_cache)); } catch (_) {} }

  function fetchItem(id, cb) {
    const url = API_BASE + id + "/itemmarket?key=" + encodeURIComponent(_apiKey) + "&comment=oc-item-worth";
    try {
      GM_xmlhttpRequest({
        method: "GET", url, timeout: 15000,
        onload: (resp) => {
          let d = null; try { d = JSON.parse(resp.responseText); } catch (_) {}
          if (!d) { cb("parse"); return; }
          if (d.error) { cb(d.error.code); return; }
          const im = d.itemmarket || {};
          const name = (im.item && im.item.name) || (_cache[id] && _cache[id].name) || null;
          const avg = Number(im.item && im.item.average_price) || 0;
          const HARD_CAP = 100000000000;
          const cap = avg > 0 ? Math.min(avg * 5, HARD_CAP) : HARD_CAP;
          const listings = Array.isArray(im.listings) ? im.listings : [];
          let lowest = 0;
          for (const l of listings) { const p = Number(l && l.price) || 0; if (p > 0 && p <= cap && (lowest === 0 || p < lowest)) lowest = p; }
          if (lowest === 0 && avg > 0) lowest = avg;
          _cache[id] = { name, price: lowest, at: Date.now() };
          _byId[id] = lowest;
          if (name && lowest > 0) _byName[name.toLowerCase()] = lowest;
          cb(null);
        },
        onerror: () => cb("net"),
        ontimeout: () => cb("timeout"),
      });
    } catch (_) { cb("net"); }
  }
  function fetchMissing(ids, done) {
    if (_fetching) { done(); return; }
    const need = ids.filter((id) => !(id in _byId));
    if (!need.length) { if (_apiKey && _state !== "ok" && !TERMINAL_ERR[_state]) { _state = "ok"; renderBar(); } done(); return; }
    _fetching = true;
    if (_state !== "ok") { _state = "checking"; renderBar(); }
    let i = 0;
    const step = () => {
      if (i >= need.length) { _fetching = false; saveCache(); if (_state === "checking") { _state = "ok"; renderBar(); } done(); schedule(); return; }
      const id = need[i++];
      fetchItem(id, (err) => {
        if (err === 2 || err === 1) { _state = "invalid"; _fetching = false; renderBar(); return; }
        if (err === 16) { _state = "lowaccess"; _fetching = false; renderBar(); return; }
        if (err === 18) { _state = "paused"; _fetching = false; renderBar(); return; }
        if (err === 5) { _state = "ratelimited"; saveCache(); renderBar(); setTimeout(() => { _fetching = false; schedule(); }, 5000); return; }
        if (err == null && _state !== "ok") { _state = "ok"; renderBar(); }
        setTimeout(step, FETCH_GAP);
      });
    };
    step();
  }

  const WORTH_RE = /worth\s*\$?\s*([\d,]+)/i;
  function rewriteWorth() {
    for (const [id, nm] of Object.entries(_idName)) { const v = _byId[id]; if (nm && v > 0) _byName[nm.toLowerCase()] = v; }
    if (!_byName || !Object.keys(_byName).length) return;
    const w1 = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
    const hits = []; let n;
    while ((n = w1.nextNode())) { if (WORTH_RE.test(n.nodeValue || "") && isVisible(n.parentElement)) hits.push(n); }
    for (const tn of hits) {
      const txt = tn.nodeValue;
      let name = null, qty = 1;
      const m = txt.match(/(\d+)\s*x?\s+(.+?)\s+worth\s*\$?\s*[\d,]+/i);
      if (m) { qty = parseInt(m[1], 10) || 1; name = m[2].trim(); }
      else {
        const ancText = (tn.parentElement && tn.parentElement.textContent || "").toLowerCase();
        for (const k of Object.keys(_byName)) { if (ancText.includes(k)) { name = k; const qm = ancText.match(/(\d+)\s*x/); if (qm) qty = parseInt(qm[1], 10) || 1; break; } }
      }
      if (!name) continue;
      const unit = _byName[name.toLowerCase()];
      if (!(unit > 0)) continue;
      const want = unit * qty;
      const shown = parseInt((txt.match(WORTH_RE)[1] || "0").replace(/,/g, ""), 10);
      if (shown === want) continue;
      tn.nodeValue = txt.replace(WORTH_RE, () => "worth " + fmt(want));
    }
  }

  function scanRewardLists() {
    const lists = new Map();
    const imgs = document.querySelectorAll('img.torn-item, img[src*="/images/items/"]');
    for (const img of imgs) {
      if (!isVisible(img)) continue;
      const m = (img.getAttribute("src") || img.src || "").match(/\/items\/(\d+)\b/);
      if (!m) continue;
      const ul = img.closest('[class*="reward___"]');
      if (!ul) continue;
      const id = m[1];
      const container = img.closest('[class*="container___"]') || img.parentElement;
      const nm = (container && container.getAttribute && container.getAttribute("aria-label")) || img.getAttribute("alt") || "";
      if (nm) _idName[id] = nm;
      const qEl = container && container.querySelector('[class*="quantityContainer"]');
      let qty = 1;
      if (qEl) { const q = parseInt((qEl.textContent || "").replace(/[^\d]/g, ""), 10); if (q > 0) qty = q; }
      let g = lists.get(ul); if (!g) { g = new Map(); lists.set(ul, g); }
      if (!g.has(id) || qty > g.get(id)) g.set(id, qty);
    }
    return lists;
  }
  function injectTotals() {
    document.querySelectorAll(".ocw-oc-total").forEach((el) => {
      const prev = el.previousElementSibling;
      if (!(prev && prev.matches && prev.matches('[class*="reward___"]'))) el.remove();
    });
    const lists = scanRewardLists();
    for (const [ul, byId] of lists) {
      if (!document.contains(ul)) continue;
      let sum = 0;
      for (const [id, qty] of byId) { const v = _byId[id]; if (v > 0) sum += v * qty; }
      if (!(sum > 0)) continue;
      let el = ul.nextElementSibling;
      if (!(el && el.classList && el.classList.contains("ocw-oc-total"))) {
        el = document.createElement("div");
        el.className = "ocw-oc-total";
        el.style.cssText = "margin:5px 2px 3px;font-size:12px;font-weight:700;color:#46d369;letter-spacing:.2px;font-family:inherit;";
        ul.insertAdjacentElement("afterend", el);
      }
      const label = "💰 Items total: " + fmt(sum);
      if (el.textContent !== label) el.textContent = label;
    }
  }

  function statusText() {
    switch (_state) {
      case "ok":          return "✓ OC prices: your Torn key";
      case "checking":    return "⏳ Checking key…";
      case "invalid":     return "⚠ Invalid API key — tap to fix";
      case "lowaccess":   return "⚠ Key access too low — tap to fix";
      case "paused":      return "⚠ API key paused — tap to fix";
      case "ratelimited": return "⏳ Rate-limited — retrying…";
      default:            return "🔑 Set a Torn API key to value OC rewards";
    }
  }
  function collapsedHTML() {
    const hint = _apiKey ? "change" : "tap to set";
    return '<span data-action="toggle" style="cursor:pointer;display:inline-flex;align-items:center;gap:8px">' +
      '<span>' + statusText() + '</span>' +
      '<span style="opacity:.55;font-size:11px">' + hint + '</span>' +
      '<span style="opacity:.4;font-size:10px">v' + SCRIPT_VERSION + '</span></span>';
  }
  function panelHTML() {
    const cur = (_apiKey || "").replace(/[^A-Za-z0-9]/g, "");
    let note = "";
    if (_state === "invalid")   note = '<div style="margin-top:4px;color:#ff6b6b">Key was rejected — paste a valid one.</div>';
    if (_state === "lowaccess") note = '<div style="margin-top:4px;color:#ff6b6b">This key\'s access level is too low. A Limited Access key works.</div>';
    if (_state === "paused")    note = '<div style="margin-top:4px;color:#ff6b6b">This key is paused in your Torn API settings.</div>';
    return '' +
      '<div style="font-weight:700;margin-bottom:5px">OC item prices — Torn API key</div>' +
      '<input id="ocw-keyinput" type="text" autocomplete="off" spellcheck="false" placeholder="Paste Torn API key" value="' + cur + '" ' +
        'style="width:240px;max-width:70%;padding:4px 6px;border-radius:4px;border:1px solid #555;background:#1a1a1a;color:#eee;font-size:12px">' +
      '<div style="margin-top:6px;display:flex;gap:6px;flex-wrap:wrap">' +
        '<button data-action="save"  style="padding:3px 12px;border-radius:4px;border:0;background:#2e7d32;color:#fff;font-weight:700;cursor:pointer">Save</button>' +
        '<button data-action="clear" style="padding:3px 12px;border-radius:4px;border:0;background:#555;color:#fff;cursor:pointer">Clear</button>' +
        '<button data-action="close" style="padding:3px 12px;border-radius:4px;border:0;background:#333;color:#bbb;cursor:pointer">Close</button>' +
      '</div>' +
      note +
      '<div style="margin-top:6px;opacity:.65;font-size:10px;line-height:1.35">A <b>Limited Access</b> key is enough. Stored only in this browser; sent only to api.torn.com. Create one in Torn → Settings → API Keys.</div>';
  }

  function findTabStrip() {
    const root = document.getElementById("faction-crimes-root");
    if (!root) return null;
    const cands = root.querySelectorAll('[class*="buttonsContainer"], [class*="tabs"], ul, nav');
    for (const c of cands) {
      const t = c.textContent || "";
      if (/\bCompleted\b/.test(t) && (/\bRecruiting\b/.test(t) || /\bPlanning\b/.test(t))) return c;
    }
    return root.querySelector('[class*="buttonsContainer"]');
  }
  function dockBar() {
    let bar = document.getElementById(BAR_ID);
    if (!bar) {
      bar = document.createElement("div");
      bar.id = BAR_ID;
      bar.style.cssText = "margin:7px 0;padding:7px 11px;border-radius:8px;background:rgba(28,28,28,.55);border:1px solid #383838;color:#ddd;font-family:inherit;font-size:12px";
      bar.addEventListener("click", onBarClick);
      _barSig = null;
    }
    const strip = findTabStrip();
    if (strip && strip.parentElement) {
      if (!bar.isConnected || bar.parentElement !== strip.parentElement) strip.parentElement.insertBefore(bar, strip.nextElementSibling);
    } else {
      const ul = document.querySelector('[class*="reward___"]');
      const top = ul ? (ul.closest('[class*="wrapper___"]') || ul) : null;
      if (top && top.parentElement) { if (!bar.isConnected || bar.parentElement !== top.parentElement) top.parentElement.insertBefore(bar, top); }
      else if (bar.parentElement !== document.body) { document.body.appendChild(bar); bar.style.position = "fixed"; bar.style.left = "8px"; bar.style.bottom = "8px"; bar.style.zIndex = "2147483600"; }
    }
    renderBar();
    return bar;
  }
  function removeBar() { const b = document.getElementById(BAR_ID); if (b) b.remove(); _barSig = null; _expanded = false; }
  function onBarClick(e) {
    const t = e.target;
    const act = t && t.getAttribute && t.getAttribute("data-action");
    if (act === "save") {
      const inp = document.getElementById("ocw-keyinput");
      saveKey(inp ? inp.value : "");
      _expanded = false; _barSig = null;
      if (_apiKey) { _byId = {}; _byName = {}; _state = "checking"; renderBar(); schedule(); }
      else { _state = "none"; renderBar(); }
      return;
    }
    if (act === "clear") {
      clearKey(); _byId = {}; _byName = {}; _expanded = false; _state = "none"; _barSig = null;
      document.querySelectorAll(".ocw-oc-total").forEach((el) => el.remove());
      renderBar();
      return;
    }
    if (act === "close") { _expanded = false; _barSig = null; renderBar(); return; }
    _expanded = !_expanded; _barSig = null; renderBar();
  }
  function renderBar() {
    const bar = document.getElementById(BAR_ID);
    if (!bar) return;
    if (_expanded) {
      const sig = "exp:" + _state;
      if (_barSig === sig) return;
      const prev = document.getElementById("ocw-keyinput");
      const pv = prev ? prev.value : null, ps = prev ? prev.selectionStart : null, pe = prev ? prev.selectionEnd : null;
      _barSig = sig; bar.innerHTML = panelHTML();
      const inp = document.getElementById("ocw-keyinput");
      if (inp) { if (pv != null) inp.value = pv; try { inp.focus(); if (ps != null) inp.setSelectionRange(ps, pe); } catch (_) {} }
      return;
    }
    const sig = "col:" + statusText() + (_apiKey ? ":k" : "");
    if (_barSig === sig) return;
    _barSig = sig;
    bar.innerHTML = collapsedHTML();
  }

  const TERMINAL_ERR = { invalid: 1, lowaccess: 1, paused: 1 };
  function tick() {
    const lists = scanRewardLists();
    if (!lists.size) { removeBar(); return; }
    dockBar();
    if (!_apiKey) { injectTotals(); return; }
    if (!TERMINAL_ERR[_state]) {
      const ids = new Set();
      for (const byId of lists.values()) for (const id of byId.keys()) ids.add(id);
      fetchMissing(Array.from(ids), () => { rewriteWorth(); injectTotals(); });
    }
    rewriteWorth(); injectTotals();
  }
  let _pending = null;
  function schedule() { if (_pending) return; _pending = setTimeout(() => { _pending = null; try { tick(); } catch (_) {} }, 300); }

  try {
    if (typeof GM_registerMenuCommand === "function") {
      GM_registerMenuCommand("Set OC API key (Torn)", () => {
        const k = prompt("Paste your Torn API key (Limited Access is enough):", _apiKey || "");
        if (k != null) { saveKey(k); _byId = {}; _byName = {}; _state = _apiKey ? "checking" : "none"; _barSig = null; schedule(); }
      });
      GM_registerMenuCommand("Clear OC API key", () => { clearKey(); _byId = {}; _byName = {}; _state = "none"; _barSig = null; schedule(); });
    }
  } catch (_) {}

  loadKey();
  loadCache();
  if (_apiKey) _state = "ok";
  tick();
  try { new MutationObserver(schedule).observe(document.body, { childList: true, subtree: true }); } catch (_) {}
  setInterval(() => { if (!document.getElementById(BAR_ID) && document.querySelector('[class*="reward___"]')) schedule(); }, 2000);
})();