Torn Item Market — RW Bonus Badges

Show RW bonus name and percentage directly on each Item Market listing (no hover required)

// ==UserScript==
// @name         Torn Item Market — RW Bonus Badges
// @namespace    smitty.torn.com
// @version      1.0
// @description  Show RW bonus name and percentage directly on each Item Market listing (no hover required)
// @author       Smitty
// @match        https://www.torn.com/page.php?sid=ItemMarket*
// @match        https://pda.torn.com/page.php?sid=ItemMarket*
// @run-at       document-idle
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  // Only run on Item Market pages (SPA friendly)
  const isItemMarket = () => {
    const u = new URL(location.href);
    return u.searchParams.get("sid") === "ItemMarket" || document.querySelector("#item-market-root");
  };

  const STYLE_ID = "rw-bonus-badge-style";

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    const css = `
      .rw-badge {
        position: absolute;
        left: 6px;
        bottom: 6px;
        z-index: 5;
        font: 600 11px/1.2 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
        background: rgba(20, 20, 20, 0.85);
        color: #fff;
        padding: 4px 6px;
        border-radius: 4px;
        box-shadow: 0 1px 2px rgba(0,0,0,.25);
        pointer-events: none;
        max-width: 92%;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }
      .rw-badge .rw-bonus {
        margin-right: 6px;
        opacity: 0.9;
      }
      .rw-badge .rw-bonus:last-child { margin-right: 0; }
      .rw-img-wrap-relative {
        position: relative !important;
      }
    `;
    const style = document.createElement("style");
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  // Parse bonus display text from an <i class="bonus-attachment-..."> node
  function parseBonusIcon(iEl) {
    if (!iEl) return null;
    const name = iEl.getAttribute("data-bonus-attachment-title") || iEl.getAttribute("aria-label") || "";
    const desc = iEl.getAttribute("data-bonus-attachment-description") || "";

    // Grab the first percentage like "12%" or "7.5%"
    const m = desc.match(/(\d+(?:\.\d+)?)\s*%/);
    const pct = m ? m[1] : null;

    // Fallback: if no percentage present, just surface the description
    const label = pct ? `${name} ${pct}%` : (name || desc || "").trim();
    return label || null;
  }

  // Build a badge label for one tile
  function buildBadgeText(tile) {
    // Any RW bonus icons live inside a container with a class containing "bonuses"
    const iconContainer = tile.querySelector("div[class*='bonuses']");
    if (!iconContainer) return null;

    const icons = iconContainer.querySelectorAll("i[class^='bonus-attachment-'], i[class*=' bonus-attachment-']");
    if (!icons.length) return null;

    const labels = [];
    icons.forEach(i => {
      const label = parseBonusIcon(i);
      if (label) labels.push(label);
    });

    if (!labels.length) return null;

    // Merge duplicates, keep order
    const seen = new Set();
    const unique = labels.filter(t => (seen.has(t) ? false : seen.add(t)));
    return unique;
  }

  function ensureImageWrapperRelative(tile) {
    // Prefer the image wrapper to anchor the badge
    let imgWrap = tile.querySelector("div[class*='imageWrapper']");
    if (!imgWrap) {
      // fall back to the main tile node
      imgWrap = tile;
    }
    if (!imgWrap.classList.contains("rw-img-wrap-relative")) {
      imgWrap.classList.add("rw-img-wrap-relative");
    }
    return imgWrap;
  }

  function applyBadge(tile) {
    if (!tile || tile.dataset.rwBadgeApplied === "1") return;

    const labels = buildBadgeText(tile);
    // Only add a badge when there is at least one RW icon with parsed text
    if (!labels || labels.length === 0) {
      tile.dataset.rwBadgeApplied = "1";
      return;
    }

    const imgWrap = ensureImageWrapperRelative(tile);

    // Remove any old badge we created
    const existing = imgWrap.querySelector(":scope > .rw-badge");
    if (existing) existing.remove();

    // Create badge
    const badge = document.createElement("div");
    badge.className = "rw-badge";
    badge.title = labels.join(" • ");

    // Cap how many labels we render visually to avoid overflow
    // Always show the first; if more exist, append “+N”
    if (labels.length === 1) {
      badge.textContent = labels[0];
    } else {
      const first = document.createElement("span");
      first.className = "rw-bonus";
      first.textContent = labels[0];
      badge.appendChild(first);

      const more = document.createElement("span");
      more.textContent = `+${labels.length - 1}`;
      badge.appendChild(more);
    }

    imgWrap.appendChild(badge);
    tile.dataset.rwBadgeApplied = "1";
  }

  function scanOnce(root = document) {
    // Each listing tile has a class beginning with "itemTile"
    const tiles = root.querySelectorAll("div[class^='itemTile'], div[class*=' itemTile']");
    tiles.forEach(applyBadge);
  }

  // Observe dynamic content (infinite scroll, filters, SPA route changes)
  function observe() {
    const root = document.getElementById("item-market-root") || document.body;
    const mo = new MutationObserver(muts => {
      let needsScan = false;
      for (const m of muts) {
        if (m.addedNodes && m.addedNodes.length) {
          needsScan = true;
          break;
        }
      }
      if (needsScan) scanOnce(root);
    });
    mo.observe(root, { childList: true, subtree: true });
  }

  function boot() {
    if (!isItemMarket()) return;
    injectStyles();
    scanOnce();
    observe();
  }

  // Handle SPA navigations
  let lastHref = location.href;
  new MutationObserver(() => {
    if (location.href !== lastHref) {
      lastHref = location.href;
      setTimeout(boot, 50);
    }
  }).observe(document, { subtree: true, childList: true });

  // Initial start
  if (document.readyState === "complete" || document.readyState === "interactive") {
    boot();
  } else {
    window.addEventListener("DOMContentLoaded", boot, { once: true });
  }
})();

(function () {
  "use strict";

  // Only run on Item Market pages (SPA friendly)
  const isItemMarket = () => {
    const u = new URL(location.href);
    return u.searchParams.get("sid") === "ItemMarket" || document.querySelector("#item-market-root");
  };

  const STYLE_ID = "rw-bonus-badge-style";

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    const css = `
      .rw-badge {
        position: absolute;
        left: 6px;
        bottom: 6px;
        z-index: 5;
        font: 600 11px/1.2 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
        background: rgba(20, 20, 20, 0.85);
        color: #fff;
        padding: 4px 6px;
        border-radius: 4px;
        box-shadow: 0 1px 2px rgba(0,0,0,.25);
        pointer-events: none;
        max-width: 92%;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }
      .rw-badge .rw-bonus {
        margin-right: 6px;
        opacity: 0.9;
      }
      .rw-badge .rw-bonus:last-child { margin-right: 0; }
      .rw-img-wrap-relative {
        position: relative !important;
      }
    `;
    const style = document.createElement("style");
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  // Parse bonus display text from an <i class="bonus-attachment-..."> node
  function parseBonusIcon(iEl) {
    if (!iEl) return null;
    const name = iEl.getAttribute("data-bonus-attachment-title") || iEl.getAttribute("aria-label") || "";
    const desc = iEl.getAttribute("data-bonus-attachment-description") || "";

    // Grab the first percentage like "12%" or "7.5%"
    const m = desc.match(/(\d+(?:\.\d+)?)\s*%/);
    const pct = m ? m[1] : null;

    // Fallback: if no percentage present, just surface the description
    const label = pct ? `${name} ${pct}%` : (name || desc || "").trim();
    return label || null;
  }

  // Build a badge label for one tile
  function buildBadgeText(tile) {
    // Any RW bonus icons live inside a container with a class containing "bonuses"
    const iconContainer = tile.querySelector("div[class*='bonuses']");
    if (!iconContainer) return null;

    const icons = iconContainer.querySelectorAll("i[class^='bonus-attachment-'], i[class*=' bonus-attachment-']");
    if (!icons.length) return null;

    const labels = [];
    icons.forEach(i => {
      const label = parseBonusIcon(i);
      if (label) labels.push(label);
    });

    if (!labels.length) return null;

    // Merge duplicates, keep order
    const seen = new Set();
    const unique = labels.filter(t => (seen.has(t) ? false : seen.add(t)));
    return unique;
  }

  function ensureImageWrapperRelative(tile) {
    // Prefer the image wrapper to anchor the badge
    let imgWrap = tile.querySelector("div[class*='imageWrapper']");
    if (!imgWrap) {
      // fall back to the main tile node
      imgWrap = tile;
    }
    if (!imgWrap.classList.contains("rw-img-wrap-relative")) {
      imgWrap.classList.add("rw-img-wrap-relative");
    }
    return imgWrap;
  }

  function applyBadge(tile) {
    if (!tile || tile.dataset.rwBadgeApplied === "1") return;

    const labels = buildBadgeText(tile);
    // Only add a badge when there is at least one RW icon with parsed text
    if (!labels || labels.length === 0) {
      tile.dataset.rwBadgeApplied = "1";
      return;
    }

    const imgWrap = ensureImageWrapperRelative(tile);

    // Remove any old badge we created
    const existing = imgWrap.querySelector(":scope > .rw-badge");
    if (existing) existing.remove();

    // Create badge
    const badge = document.createElement("div");
    badge.className = "rw-badge";
    badge.title = labels.join(" • ");

    // Cap how many labels we render visually to avoid overflow
    // Always show the first; if more exist, append “+N”
    if (labels.length === 1) {
      badge.textContent = labels[0];
    } else {
      const first = document.createElement("span");
      first.className = "rw-bonus";
      first.textContent = labels[0];
      badge.appendChild(first);

      const more = document.createElement("span");
      more.textContent = `+${labels.length - 1}`;
      badge.appendChild(more);
    }

    imgWrap.appendChild(badge);
    tile.dataset.rwBadgeApplied = "1";
  }

  function scanOnce(root = document) {
    // Each listing tile has a class beginning with "itemTile"
    const tiles = root.querySelectorAll("div[class^='itemTile'], div[class*=' itemTile']");
    tiles.forEach(applyBadge);
  }

  // Observe dynamic content (infinite scroll, filters, SPA route changes)
  function observe() {
    const root = document.getElementById("item-market-root") || document.body;
    const mo = new MutationObserver(muts => {
      let needsScan = false;
      for (const m of muts) {
        if (m.addedNodes && m.addedNodes.length) {
          needsScan = true;
          break;
        }
      }
      if (needsScan) scanOnce(root);
    });
    mo.observe(root, { childList: true, subtree: true });
  }

  function boot() {
    if (!isItemMarket()) return;
    injectStyles();
    scanOnce();
    observe();
  }

  // Handle SPA navigations
  let lastHref = location.href;
  new MutationObserver(() => {
    if (location.href !== lastHref) {
      lastHref = location.href;
      setTimeout(boot, 50);
    }
  }).observe(document, { subtree: true, childList: true });

  // Initial start
  if (document.readyState === "complete" || document.readyState === "interactive") {
    boot();
  } else {
    window.addEventListener("DOMContentLoaded", boot, { once: true });
  }
})();