Torn Gear Quality Visualizer

This script gives players a quick visual guide to how good a weapon or armor roll is by colour-coding the quality directly onto the item image with a heat map based font colouring.

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

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.

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

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

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!)

// ==UserScript==
// @name         Torn Gear Quality Visualizer
// @namespace    http://tampermonkey.net/
// @version      2.2.3
// @description  This script gives players a quick visual guide to how good a weapon or armor roll is by colour-coding the quality directly onto the item image with a heat map based font colouring.
// @author       Insomnis [3815182]
// @license      MIT
// @match        https://www.torn.com/page.php?sid=ItemMarket*
// @match        https://www.torn.com/bazaar.php*
// @match        https://www.torn.com/item.php*
// @match        https://www.torn.com/dump.php*
// @match        https://www.torn.com/factions.php*
// @match        https://www.torn.com/amarket.php*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  if (window.__TORN_HEAT_RUNNING__) return;
  window.__TORN_HEAT_RUNNING__ = true;

  /** ==================== EMBEDDED LIBRARIES ==================== **/
  // Weapons: native stat ranges (damage + accuracy)
  const WEAPON_STAT_RANGES = {
    // ===== PRIMARY =====
    "9mm Uzi": { dMin:65, dMax:70, aMin:43, aMax:48 },
    "AK-47": { dMin:56, dMax:61, aMin:52, aMax:57 },
    "AK74U": { dMin:46, dMax:51, aMin:41, aMax:46 },
    "ArmaLite M-15A4": { dMin:68, dMax:73, aMin:57, aMax:62 },
    "Benelli M1 Tactical": { dMin:39, dMax:44, aMin:65, aMax:70 },
    "Benelli M4 Super": { dMin:59, dMax:64, aMin:55, aMax:60 },
    "Bushmaster Carbon 15": { dMin:50, dMax:55, aMin:57, aMax:62 },
    "Dual Bushmasters": { dMin:76, dMax:81, aMin:47, aMax:52 },
    "Dual MP5s": { dMin:78, dMax:83, aMin:46, aMax:51 },
    "Dual P90s": { dMin:77, dMax:82, aMin:45, aMax:50 },
    "Dual TMPs": { dMin:79, dMax:84, aMin:40, aMax:45 },
    "Dual Uzis": { dMin:80, dMax:85, aMin:36, aMax:41 },
    "Egg Propelled Launcher": { dMin:64, dMax:69, aMin:24, aMax:29 },
    "Enfield SA-80": { dMin:63, dMax:68, aMin:55, aMax:60 },
    "Gold Plated AK-47": { dMin:75, dMax:80, aMin:62, aMax:67 },
    "Heckler & Koch SL8": { dMin:60, dMax:65, aMin:46, aMax:51 },
    "Ithaca 37": { dMin:49, dMax:54, aMin:62, aMax:67 },
    "Jackhammer": { dMin:69, dMax:74, aMin:52, aMax:57 },
    "M16 A2 Rifle": { dMin:61, dMax:66, aMin:47, aMax:52 },
    "M249 SAW": { dMin:67, dMax:72, aMin:41, aMax:46 },
    "M4A1 Colt Carbine": { dMin:55, dMax:60, aMin:47, aMax:52 },
    "Mag 7": { dMin:56, dMax:61, aMin:62, aMax:67 },
    "Minigun": { dMin:72, dMax:77, aMin:28, aMax:33 },
    "MP 40": { dMin:37, dMax:42, aMin:41, aMax:46 },
    "MP5 Navy": { dMin:45, dMax:50, aMin:51, aMax:56 },
    "Negev NG-5": { dMin:69, dMax:74, aMin:35, aMax:40 },
    "Nock Gun": { dMin:95, dMax:100, aMin:45, aMax:50 },
    "P90": { dMin:48, dMax:53, aMin:51, aMax:56 },
    "PKM": { dMin:76, dMax:79, aMin:49, aMax:51 },
    "Prototype": { dMin:68, dMax:73, aMin:36, aMax:41 },
    "Rheinmetall MG 3": { dMin:66, dMax:71, aMin:36, aMax:41 },
    "Sawed-Off Shotgun": { dMin:41, dMax:46, aMin:63, aMax:68 },
    "SIG 552": { dMin:69, dMax:74, aMin:50, aMax:55 },
    "SKS Carbine": { dMin:46, dMax:51, aMin:47, aMax:52 },
    "Snow Cannon": { dMin:52, dMax:57, aMin:24, aMax:29 },
    "Steyr AUG": { dMin:64, dMax:69, aMin:45, aMax:50 },
    "Stoner 96": { dMin:69, dMax:74, aMin:49, aMax:54 },
    "Tavor TAR-21": { dMin:65, dMax:70, aMin:52, aMax:57 },
    "Thompson": { dMin:39, dMax:44, aMin:43, aMax:48 },
    "Vektor CR-21": { dMin:50, dMax:55, aMin:48, aMax:53 },
    "XM8 Rifle": { dMin:50, dMax:55, aMin:56, aMax:61 },

    // ===== SECONDARY =====
    "Type 98 Anti Tank": { dMin:78, dMax:83, aMin:25, aMax:30 },
    "Beretta 92FS": { dMin:48, dMax:53, aMin:51, aMax:56 },
    "Beretta M9": { dMin:36, dMax:41, aMin:54, aMax:59 },
    "Beretta Pico": { dMin:54, dMax:59, aMin:53, aMax:58 },
    "Blowgun": { dMin:15, dMax:20, aMin:39, aMax:44 },
    "Blunderbuss": { dMin:46, dMax:51, aMin:24, aMax:29 },
    "BT MP9": { dMin:61, dMax:66, aMin:55, aMax:60 },
    "China Lake": { dMin:79, dMax:87, aMin:47, aMax:53 },
    "Cobra Derringer": { dMin:61, dMax:66, aMin:53, aMax:58 },
    "Crossbow": { dMin:35, dMax:40, aMin:63, aMax:68 },
    "Desert Eagle": { dMin:59, dMax:64, aMin:36, aMax:41 },
    "Dual 92G Berettas": { dMin:64, dMax:69, aMin:30, aMax:35 },
    "Fiveseven": { dMin:52, dMax:57, aMin:49, aMax:54 },
    "Flare Gun": { dMin:18, dMax:23, aMin:22, aMax:27 },
    "Glock 17": { dMin:28, dMax:33, aMin:53, aMax:58 },
    "Lorcin 380": { dMin:27, dMax:32, aMin:41, aMax:46 },
    "Luger": { dMin:35, dMax:40, aMin:48, aMax:53 },
    "Magnum": { dMin:55, dMax:60, aMin:38, aMax:43 },
    "MP5k": { dMin:42, dMax:47, aMin:52, aMax:57 },
    "Pink Mac-10": { dMin:74, dMax:79, aMin:45, aMax:50 },
    "Qsz-92": { dMin:62, dMax:67, aMin:53, aMax:58 },
    "Raven MP25": { dMin:29, dMax:34, aMin:52, aMax:57 },
    "Ruger 57": { dMin:32, dMax:37, aMin:56, aMax:61 },
    "S&W M29": { dMin:47, dMax:52, aMin:52, aMax:57 },
    "S&W Revolver": { dMin:42, dMax:47, aMin:54, aMax:59 },
    "Skorpion": { dMin:40, dMax:45, aMin:54, aMax:59 },
    "Springfield 1911": { dMin:33, dMax:38, aMin:57, aMax:62 },
    "Taurus": { dMin:30, dMax:35, aMin:57, aMax:62 },
    "TMP": { dMin:38, dMax:43, aMin:45, aMax:50 },
    "USP": { dMin:44, dMax:49, aMin:58, aMax:63 },

    // ===== SECONDARY (ADDITIONS) =====
    "Flamethrower": { dMin:67, dMax:72, aMin:39, aMax:44 },
    "Harpoon": { dMin:47, dMax:52, aMin:63, aMax:68 },
    "Homemade Pocket Shotgun": { dMin:63, dMax:68, aMin:60, aMax:65 },
    "Milkor MGL": { dMin:74, dMax:79, aMin:39, aMax:44 },
    "RPG Launcher": { dMin:77, dMax:82, aMin:39, aMax:44 },
    "Slingshot": { dMin:14, dMax:18, aMin:54, aMax:59 },
    "Taser": { dMin:1, dMax:5, aMin:54, aMax:59 },
    "Tranquilizer Gun": { dMin:15, dMax:20, aMin:45, aMax:50 },

    // ===== MELEE =====
    "Axe": { dMin: 34, dMax: 39, aMin: 52, aMax: 57 },
    "Baseball Bat": { dMin: 16, dMax: 21, aMin: 57, aMax: 62 },
    "Blood Spattered Sickle": { dMin: 36, dMax: 41, aMin: 55, aMax: 60 },
    "Bone Saw": { dMin: 54, dMax: 59, aMin: 52, aMax: 57 },
    "Bo Staff": { dMin: 13, dMax: 18, aMin: 55, aMax: 60 },
    "Bread Knife": { dMin: 41, dMax: 43, aMin: 65, aMax: 70 },
    "Bug Swatter": { dMin: 5, dMax: 10, aMin: 59, aMax: 64 },
    "Butterfly Knife": { dMin: 24, dMax: 29, aMin: 55, aMax: 60 },
    "Cattle Prod": { dMin: 1, dMax: 6, aMin: 59, aMax: 64 },
    "Chain Whip": { dMin: 31, dMax: 36, aMin: 52, aMax: 57 },
    "Chainsaw": { dMin: 61, dMax: 66, aMin: 23, aMax: 28 },
    "Claymore Sword": { dMin: 57, dMax: 62, aMin: 49, aMax: 54 },
    "Cleaver": { dMin: 51, dMax: 56, aMin: 56, aMax: 61 },
    "Cricket Bat": { dMin: 18, dMax: 23, aMin: 42, aMax: 47 },
    "Crowbar": { dMin: 20, dMax: 25, aMin: 52, aMax: 57 },
    "Dagger": { dMin: 28, dMax: 33, aMin: 60, aMax: 65 },
    "Devil's Pitchfork": { dMin: 61, dMax: 66, aMin: 41, aMax: 46 },
    "Diamond Bladed Knife": { dMin: 60, dMax: 65, aMin: 62, aMax: 67 },
    "Diamond Icicle": { dMin: 45, dMax: 50, aMin: 48, aMax: 53 },
    "Dual Axes": { dMin: 70, dMax: 75, aMin: 54, aMax: 59 },
    "Dual Hammers": { dMin: 70, dMax: 75, aMin: 54, aMax: 59 },
    "Dual Samurai Swords": { dMin: 70, dMax: 75, aMin: 54, aMax: 59 },
    "Dual Scimitars": { dMin: 70, dMax: 75, aMin: 54, aMax: 59 },
    "Duke's Hammer": { dMin: 18, dMax: 18, aMin: 55, aMax: 55 },
    "Fine Chisel": { dMin: 16, dMax: 21, aMin: 50, aMax: 55 },
    "Flail": { dMin: 71, dMax: 76, aMin: 28, aMax: 33 },
    "Frying Pan": { dMin: 19, dMax: 24, aMin: 43, aMax: 48 },
    "Golden Broomstick": { dMin: 60, dMax: 65, aMin: 48, aMax: 53 },
    "Golf Club": { dMin: 29, dMax: 34, aMin: 59, aMax: 64 },
    "Guandao": { dMin: 63, dMax: 68, aMin: 35, aMax: 40 },
    "Hammer": { dMin: 17, dMax: 22, aMin: 55, aMax: 60 },
    "Handbag": { dMin: 67, dMax: 72, aMin: 63, aMax: 68 },
    "Ice Pick": { dMin: 51, dMax: 56, aMin: 60, aMax: 65 },
    "Ivory Walking Cane": { dMin: 53, dMax: 58, aMin: 57, aMax: 62 },
    "Kama": { dMin: 35, dMax: 40, aMin: 55, aMax: 60 },
    "Katana": { dMin: 52, dMax: 57, aMin: 55, aMax: 60 },
    "Kitchen Knife": { dMin: 25, dMax: 30, aMin: 55, aMax: 60 },
    "Knuckle Dusters": { dMin: 11, dMax: 16, aMin: 62, aMax: 67 },
    "Kodachi": { dMin: 62, dMax: 67, aMin: 56, aMax: 61 },
    "Lead Pipe": { dMin: 26, dMax: 31, aMin: 33, aMax: 38 },
    "Leather Bullwhip": { dMin: 27, dMax: 32, aMin: 52, aMax: 57 },
    "Macana": { dMin: 57, dMax: 62, aMin: 65, aMax: 70 },
    "Madball": { dMin: 60, dMax: 65, aMin: 45, aMax: 50 },
    "Meat Hook": { dMin: 62, dMax: 67, aMin: 39, aMax: 44 },
    "Metal Nunchaku": { dMin: 61, dMax: 66, aMin: 60, aMax: 65 },
    "Naval Cutlass": { dMin: 64, dMax: 69, aMin: 52, aMax: 57 },
    "Ninja Claws": { dMin: 39, dMax: 44, aMin: 51, aMax: 56 },
    "Pair of High Heels": { dMin: 40, dMax: 45, aMin: 63, aMax: 68 },
    "Pair of Ice Skates": { dMin: 43, dMax: 48, aMin: 45, aMax: 50 },
    "Pen Knife": { dMin: 21, dMax: 26, aMin: 45, aMax: 50 },
    "Penelope": { dMin: 17, dMax: 17, aMin: 57, aMax: 57 },
    "Petrified Humerus": { dMin: 48, dMax: 53, aMin: 48, aMax: 53 },
    "Pillow": { dMin: 1, dMax: 5.3, aMin: 63, aMax: 68 },
    "Plastic Sword": { dMin: 5, dMax: 10, aMin: 29, aMax: 34 },
    "Poison Umbrella": { dMin: 35, dMax: 40, aMin: 49, aMax: 54 },
    "Riding Crop": { dMin: 21, dMax: 26, aMin: 54, aMax: 59 },
    "Rusty Sword": { dMin: 22, dMax: 27, aMin: 15, aMax: 20 },
    "Sai": { dMin: 29, dMax: 34, aMin: 52, aMax: 57 },
    "Samurai Sword": { dMin: 58, dMax: 63, aMin: 52, aMax: 57 },
    "Scalpel": { dMin: 56, dMax: 61, aMin: 47, aMax: 52 },
    "Scimitar": { dMin: 40, dMax: 45, aMin: 58, aMax: 63 },
    "Sledgehammer": { dMin: 58, dMax: 63, aMin: 50, aMax: 55 },
    "Spear": { dMin: 38, dMax: 43, aMin: 48, aMax: 53 },
    "Swiss Army Knife": { dMin: 23, dMax: 28, aMin: 52, aMax: 57 },
    "Twin Tiger Hooks": { dMin: 50, dMax: 55, aMin: 53, aMax: 58 },
    "Wand of Destruction": { dMin: 60, dMax: 65, aMin: 26, aMax: 31 },
    "Wooden Nunchaku": { dMin: 22, dMax: 27, aMin: 59, aMax: 64 },
    "Wushu Double Axes": { dMin: 53, dMax: 58, aMin: 51, aMax: 56 },
    "Yasukuni Sword": { dMin: 65, dMax: 70, aMin: 49, aMax: 54 },
  };

  // Armor: armor rating min/max (bazaar label is "defence")
  const ARMOR_STAT_RANGES = {
    "Assault Boots": { min:46.00, max:51.00 },
    "Assault Gloves": { min:46.00, max:51.00 },
    "Assault Helmet": { min:46.00, max:51.00 },
    "Assault Pants": { min:46.00, max:51.00 },
    "Assault Body": { min:46.00, max:51.00 },

    "Bulletproof Vest": { min:34.00, max:39.00 },
    "Chain Mail": { min:23.00, max:28.00 },

    "Combat Boots": { min:38.00, max:43.00 },
    "Combat Gloves": { min:38.00, max:43.00 },
    "Combat Helmet": { min:38.00, max:43.00 },
    "Combat Pants": { min:38.00, max:43.00 },
    "Combat Vest": { min:38.00, max:43.00 },

    "Delta Boots": { min:49.00, max:54.00 },
    "Delta Gloves": { min:49.00, max:54.00 },
    "Delta Gas Mask": { min:49.00, max:54.00 },
    "Delta Pants": { min:49.00, max:54.00 },
    "Delta Body": { min:49.00, max:54.00 },

    "Dune Boots": { min:44.00, max:49.00 },
    "Dune Gloves": { min:44.00, max:49.00 },
    "Dune Helmet": { min:44.00, max:49.00 },
    "Dune Pants": { min:44.00, max:49.00 },
    "Dune Vest": { min:44.00, max:49.00 },

    "Construction Helmet": { min:30.00, max:35.00 },

    "EOD Boots": { min:55.00, max:60.00 },
    "EOD Gloves": { min:55.00, max:60.00 },
    "EOD Helmet": { min:55.00, max:60.00 },
    "EOD Pants": { min:55.00, max:60.00 },
    "EOD Apron": { min:55.00, max:60.00 },

    "Flak Jacket": { min:30.00, max:35.00 },
    "Flexible Body Armor": { min:42.00, max:47.00 },
    "Full Body Armor": { min:31.00, max:36.00 },
    "Hazmat Suit": { min:10.00, max:15.00 },
    "Hiking Boots": { min:24.00, max:29.00 },
    "Kevlar Gloves": { min:32.00, max:37.00 },

    "Leather Boots": { min:20.00, max:25.00 },
    "Leather Gloves": { min:20.00, max:25.00 },
    "Leather Helmet": { min:20.00, max:25.00 },
    "Leather Pants": { min:20.00, max:25.00 },
    "Leather Vest": { min:20.00, max:25.00 },

    "Motorcycle Helmet": { min:30.00, max:35.00 },
    "Police Vest": { min:32.00, max:37.00 },

    "Riot Boots": { min:45.00, max:50.00 },
    "Riot Gloves": { min:45.00, max:50.00 },
    "Riot Helmet": { min:35.00, max:40.00 },
    "Riot Pants": { min:45.00, max:50.00 },
    "Riot Body": { min:45.00, max:50.00 },

    "Safety Boots": { min:30.00, max:35.00 },

    "Sentinel Helmet": { min:53.00, max:58.00 },
    "Sentinel Apron": { min:53.00, max:58.00 },
    "Sentinel Pants": { min:53.00, max:58.00 },
    "Sentinel Gloves": { min:53.00, max:58.00 },
    "Sentinel Boots": { min:53.00, max:58.00 },

    "Vanguard Respirator": { min:48.00, max:53.00 },
    "Vanguard Body": { min:48.00, max:53.00 },
    "Vanguard Pants": { min:48.00, max:53.00 },
    "Vanguard Gloves": { min:48.00, max:53.00 },
    "Vanguard Boots": { min:48.00, max:53.00 },

    "Welding Helmet": { min:34.00, max:39.00 },
    "WWII Helmet": { min:34.00, max:39.00 },

    "Liquid Body Armor": { min:40.00, max:45.00 },
    "Marauder Face Mask": { min:40.00, max:45.00 },
    "Marauder Boots": { min:52.00, max:57.00 },
    "Marauder Gloves": { min:52.00, max:57.00 },
    "Marauder Pants": { min:52.00, max:57.00 },
    "Marauder Body": { min:52.00, max:57.00 },
    "Medieval Helmet": { min:25.00, max:30.00 },
    "Outer Tactical Vest": { min:36.00, max:41.00 },
  };


  /** ==================== SCRIPT CONFIG ==================== **/
  const STEP = 0.01;          // 1% increments
  const HEAT_MAX = 2.00;      // anything beyond is clamped to red

  /** ==================== SELECTORS ==================== **/
  // Item Market
  const IM_TILE_SELECTOR = "li.tt-highlight-modified";
  const IM_NAME_SELECTOR = ".name___ukdHN";

  // Bazaar
  const BZ_ITEM_SELECTOR = 'div[data-testid="item"]';
  const BZ_NAME_SELECTOR = 'p[data-testid="name"]';
  const BZ_STATS_SELECTOR = ".infoBonuses___g8QdG";

  // item.php
  const IP_TILE_SELECTOR = "ul.itemsList > li";
  const IP_NAME_SELECTOR = ".title-wrap .name";
  const IP_NAME_SELECTOR_2 = ".title-wrap .name-wrap .t-overflow";
  const IP_CONT_WRAP_SELECTOR = ".cont-wrap";

  // dump.php#/trash
  const DP_TILE_SELECTOR = "ul.items-cont > li";
  const DP_NAME_SELECTOR = ".title-wrap .name-wrap .t-overflow";

  // factions.php armory
  const FA_TILE_SELECTOR = ".armoury-tabs ul.item-list > li";

  // amarket.php
  const AM_TILE_SELECTOR = "div.item-cont-wrap";
  const AM_NAME_SELECTOR = ".item-name";
  const AM_STATS_WRAP_SELECTOR = ".item-bonuses"; // ✅ important

  /** ==================== NORMALIZATION + MAPS ==================== **/
  function normName(name) {
    if (!name) return "";
    return name
      .normalize("NFKC")
      .toLowerCase()
      .replace(/[\u2010-\u2015\u2212]/g, "-")
      .replace(/[\u2018\u2019\u2032]/g, "'")
      .replace(/\s+/g, " ")
      .trim();
  }

  const WEAPON_MAP = (() => {
    const m = Object.create(null);
    for (const [k, v] of Object.entries(WEAPON_STAT_RANGES)) m[normName(k)] = v;
    return m;
  })();

  const ARMOR_MAP = (() => {
    const m = Object.create(null);
    for (const [k, v] of Object.entries(ARMOR_STAT_RANGES)) m[normName(k)] = v;
    return m;
  })();

  /** ==================== UTILS ==================== **/
  function safeText(el) { return (el?.textContent || "").trim(); }

  function clamp(x, lo, hi) {
    if (!Number.isFinite(x)) return lo;
    return Math.max(lo, Math.min(hi, x));
  }

  function quantizeHeat(p) {
    const q = Math.round(p / STEP) * STEP;
    return clamp(q, 0, HEAT_MAX);
  }

  function lerp(a, b, t) { return a + (b - a) * t; }
  function rgb(r, g, b) { return `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`; }

  function colorForHeat(pRaw) {
    const p = clamp(pRaw, 0, HEAT_MAX);

    const gray   = { r:120, g:120, b:120 };
    const blue   = { r: 60, g:140, b:240 };
    const green  = { r: 60, g:200, b: 90 };
    const yellow = { r:244, g:200, b: 42 };
    const orange = { r:245, g:130, b: 35 };
    const red    = { r:220, g: 40, b: 40 };

    if (p <= 0.77) {
      const t = p / 0.77;
      return rgb(lerp(gray.r, blue.r, t), lerp(gray.g, blue.g, t), lerp(gray.b, blue.b, t));
    }
    if (p <= 0.88) {
      const t = (p - 0.77) / (0.88 - 0.77);
      return rgb(lerp(blue.r, green.r, t), lerp(blue.g, green.g, t), lerp(blue.b, green.b, t));
    }
    if (p <= 0.99) {
      const t = (p - 0.88) / (0.99 - 0.88);
      return rgb(lerp(green.r, yellow.r, t), lerp(green.g, yellow.g, t), lerp(green.b, yellow.b, t));
    }
    if (p <= 1.11) {
      const t = (p - 0.99) / (1.11 - 0.99);
      return rgb(lerp(yellow.r, orange.r, t), lerp(yellow.g, orange.g, t), lerp(yellow.b, orange.b, t));
    }
    const t = (p - 1.11) / (2.00 - 1.11);
    return rgb(lerp(orange.r, red.r, t), lerp(orange.g, red.g, t), lerp(orange.b, red.b, t));
  }

  function intrinsicPercent(roll, min, max) {
    const denom = max - min;
    if (!Number.isFinite(roll) || !Number.isFinite(min) || !Number.isFinite(max) || denom <= 0) return null;
    return (roll - min) / denom;
  }

  function toHeat(rawP) {
    if (!Number.isFinite(rawP)) return 0;
    return clamp(rawP, 0, HEAT_MAX);
  }

  /** ==================== BADGE (no borders + removes tooltips) ==================== **/
  const BADGE_CLASS = "torn-heat-quality-badge";
  let cssInjected = false;

  function injectCSSOnce() {
    if (cssInjected) return;
    cssInjected = true;

    const style = document.createElement("style");
    style.textContent = `
      .${BADGE_CLASS} {
        position: absolute;
        top: 4px;
        left: 4px;
        z-index: 10;
        font-size: 12px;
        font-weight: 900;
        line-height: 1;
        padding: 3px 6px;
        border-radius: 6px;
        background: rgba(0, 0, 0, 0.65);
        text-shadow: 0 1px 1px rgba(0,0,0,0.85);
        pointer-events: none;
        user-select: none;
        white-space: nowrap;
      }
    `;
    document.head.appendChild(style);
  }

  function removeQualityTitles(root) {
    if (!root) return;

    const rt = root.getAttribute?.("title") || "";
    if (/^\s*Quality:\s*/i.test(rt)) root.removeAttribute("title");

    const titled = root.querySelectorAll?.("[title]") || [];
    for (const el of titled) {
      const t = el.getAttribute("title") || "";
      if (/^\s*Quality:\s*/i.test(t)) el.removeAttribute("title");
    }
  }

  function formatBadgeTextFromRaw(pRaw) {
    const pct = pRaw * 100;
    return `${Math.round(pct)}%`;
  }

  function ensurePositioned(anchor) {
    const cs = window.getComputedStyle(anchor);
    if (!cs || cs.position === "static") {
      anchor.style.position = "relative";
    }
  }

  function upsertBadge(anchor, pRaw, color) {
    if (!anchor) return;

    injectCSSOnce();
    ensurePositioned(anchor);
    removeQualityTitles(anchor);

    let badge = anchor.querySelector(`.${BADGE_CLASS}`);
    if (!badge) {
      badge = document.createElement("span");
      badge.className = BADGE_CLASS;
      anchor.appendChild(badge);
    }

    badge.textContent = formatBadgeTextFromRaw(pRaw);
    badge.style.color = color;
  }

  function findImageAnchor(root) {
    if (!root) return null;

    const direct =
      root.querySelector('div.imageWrapper___RqvUg') ||
      root.querySelector('div[class*="imageWrapper"]') ||
      root.querySelector('div.imgContainer___Ec4I5[data-testid="img-container"]') ||
      root.querySelector('div[data-testid="img-container"]') ||
      root.querySelector('span.image-wrap') ||
      root.querySelector('div.image-wrap') ||
      root.querySelector('div.img-wrap[data-armoryid]') ||
      root.querySelector('span.item-plate') ||
      root.querySelector('span.img-wrap') ||
      null;

    if (direct) return direct;

    const img = root.querySelector("img");
    if (!img) return null;
    return img.closest("div, span") || img.parentElement || null;
  }

  /** ==================== FIXED STAT READER (from your v2.1.1) ==================== **/
  // ✅ Critical for Auction House: only read the number from the stat row (.bonus-attachment)
  // instead of accidentally picking up other numeric spans in the tile.
  function readStatFromTile(root, iconClass) {
    const icon = root.querySelector(`i.${iconClass}`);
    if (!icon) return null;

    // Best case: icon + value inside the same .bonus-attachment
    const attachment = icon.closest(".bonus-attachment");
    if (attachment) {
      const valEl = attachment.querySelector("span.label-value, span.t-overflow, span");
      const txt = safeText(valEl);
      const v = parseFloat(txt);
      return Number.isFinite(v) ? v : null;
    }

    // Fallback: scan the closest <li> row that contains the icon
    const li = icon.closest("li");
    if (li) {
      const candidates = li.querySelectorAll("span.label-value, span.t-overflow, span");
      for (const sp of candidates) {
        const txt = safeText(sp);
        if (/^\d+(\.\d+)?$/.test(txt)) {
          const v = parseFloat(txt);
          if (Number.isFinite(v)) return v;
        }
      }
    }

    // Last resort: immediate siblings / parent scan
    const near = icon.parentElement?.querySelector("span.label-value, span.t-overflow, span");
    if (near) {
      const txt = safeText(near);
      if (/^\d+(\.\d+)?$/.test(txt)) {
        const v = parseFloat(txt);
        if (Number.isFinite(v)) return v;
      }
    }

    const sib = icon.nextElementSibling;
    if (sib) {
      const txt = safeText(sib);
      if (/^\d+(\.\d+)?$/.test(txt)) {
        const v = parseFloat(txt);
        if (Number.isFinite(v)) return v;
      }
    }

    return null;
  }

  /** ==================== HEAT CALC ==================== **/
  function weaponHeatRaw(d, a, range) {
    const minScore = range.dMin + range.aMin;
    const maxScore = range.dMax + range.aMax;
    return intrinsicPercent(d + a, minScore, maxScore);
  }

  function armorHeatRaw(r, range) {
    return intrinsicPercent(r, range.min, range.max);
  }

  /** ==================== ITEM MARKET PARSING ==================== **/
  function imGetName(tile) {
    const name = safeText(tile.querySelector(IM_NAME_SELECTOR));
    if (name) return name;
    const alt = tile.querySelector("img[alt]")?.getAttribute("alt");
    return (alt || "").trim() || null;
  }

  function imParsePointsFromAria(tile, containsText) {
    const el = tile.querySelector(`[aria-label*="${containsText}"]`);
    if (!el) return null;
    const aria = el.getAttribute("aria-label") || "";
    const val = parseFloat(aria);
    return Number.isFinite(val) ? val : null;
  }

  function imGetRollStats(tile) {
    const damage = imParsePointsFromAria(tile, "damage points");
    const accuracy = imParsePointsFromAria(tile, "accuracy points");
    const armor = imParsePointsFromAria(tile, "armor points");

    const isWeapon = damage !== null && accuracy !== null;
    const isArmor = armor !== null && !isWeapon;

    return { isWeapon, isArmor, damage, accuracy, armor };
  }

  /** ==================== BAZAAR PARSING ==================== **/
  function bzGetName(itemEl) {
    const name = safeText(itemEl.querySelector(BZ_NAME_SELECTOR));
    if (name) return name;

    const imgAlt = itemEl.querySelector("img[alt]")?.getAttribute("alt");
    if (imgAlt) return imgAlt.trim();

    return null;
  }

  function bzFindValue(statsBlock, label) {
    const icon = statsBlock.querySelector(`i[area-label="${label}"], i[aria-label="${label}"]`);
    if (!icon) return null;
    const container = icon.closest("div") || icon.parentElement;
    const valEl = container?.querySelector("span.t-overflow") || icon.nextElementSibling;
    const val = parseFloat(safeText(valEl));
    return Number.isFinite(val) ? val : null;
  }

  function bzGetRollStats(itemEl) {
    const statsBlock = itemEl.querySelector(BZ_STATS_SELECTOR);
    if (!statsBlock) return { isWeapon:false, isArmor:false };

    const damage = bzFindValue(statsBlock, "damage");
    const accuracy = bzFindValue(statsBlock, "accuracy");
    const defence = bzFindValue(statsBlock, "defence");

    const isWeapon = damage !== null && accuracy !== null;
    const isArmor = defence !== null && !isWeapon;

    return { isWeapon, isArmor, damage, accuracy, armor: defence };
  }

  /** ==================== item.php PARSING ==================== **/
  function ipGetName(tileLi) {
    const n2 = safeText(tileLi.querySelector(IP_NAME_SELECTOR_2));
    if (n2) return n2;

    const n = safeText(tileLi.querySelector(IP_NAME_SELECTOR));
    if (n) return n;

    const alt = tileLi.querySelector("img[alt]")?.getAttribute("alt");
    if (alt) return alt.trim();

    return null;
  }

  function ipGetRollStats(tileLi) {
    const cont = tileLi.querySelector(IP_CONT_WRAP_SELECTOR) || tileLi;
    const damage = readStatFromTile(cont, "bonus-attachment-item-damage-bonus");
    const accuracy = readStatFromTile(cont, "bonus-attachment-item-accuracy-bonus");
    const defence = readStatFromTile(cont, "bonus-attachment-item-defence-bonus");

    const isWeapon = damage !== null && accuracy !== null;
    const isArmor = defence !== null && !isWeapon;

    return { isWeapon, isArmor, damage, accuracy, armor: defence };
  }

  /** ==================== dump.php PARSING ==================== **/
  function dpGetName(tileLi) {
    const n = safeText(tileLi.querySelector(DP_NAME_SELECTOR));
    if (n) return n;

    const alt = tileLi.querySelector("img[alt]")?.getAttribute("alt");
    if (alt) return alt.trim();

    return null;
  }

  function dpGetRollStats(tileLi) {
    const damage = readStatFromTile(tileLi, "bonus-attachment-item-damage-bonus");
    const accuracy = readStatFromTile(tileLi, "bonus-attachment-item-accuracy-bonus");
    const defence = readStatFromTile(tileLi, "bonus-attachment-item-defence-bonus");

    const isWeapon = damage !== null && accuracy !== null;
    const isArmor = defence !== null && !isWeapon;

    return { isWeapon, isArmor, damage, accuracy, armor: defence };
  }

  /** ==================== factions.php PARSING ==================== **/
  function faGetName(li) {
    const raw = safeText(li.querySelector(".name.bold, .name"));
    if (!raw) return null;
    return raw.replace(/\s+x\s*\d+.*$/i, "").trim();
  }

  function faGetRollStats(li) {
    const damage = readStatFromTile(li, "bonus-attachment-item-damage-bonus");
    const accuracy = readStatFromTile(li, "bonus-attachment-item-accuracy-bonus");
    const defence = readStatFromTile(li, "bonus-attachment-item-defence-bonus");

    const isWeapon = damage !== null && accuracy !== null;
    const isArmor = defence !== null && !isWeapon;

    return { isWeapon, isArmor, damage, accuracy, armor: defence };
  }

  /** ==================== amarket.php PARSING ==================== **/
  function amGetName(tile) {
    const n = safeText(tile.querySelector(AM_NAME_SELECTOR));
    if (n) return n;

    const alt = tile.querySelector("img[alt]")?.getAttribute("alt");
    if (alt) return alt.trim();

    return null;
  }

  function amGetRollStats(tile) {
    const wrap = tile.querySelector(AM_STATS_WRAP_SELECTOR) || tile;

    const damage = readStatFromTile(wrap, "bonus-attachment-item-damage-bonus");
    const accuracy = readStatFromTile(wrap, "bonus-attachment-item-accuracy-bonus");
    const defence = readStatFromTile(wrap, "bonus-attachment-item-defence-bonus");

    const isWeapon = damage !== null && accuracy !== null;
    const isArmor = defence !== null && !isWeapon;

    return { isWeapon, isArmor, damage, accuracy, armor: defence };
  }

  /** ==================== RECOLOR PASSES ==================== **/
  function recolorItemMarket() {
    const tiles = Array.from(document.querySelectorAll(IM_TILE_SELECTOR));
    for (const tile of tiles) {
      const name = imGetName(tile);
      if (!name) continue;

      const key = normName(name);
      const stats = imGetRollStats(tile);

      if (stats.isWeapon) {
        const range = WEAPON_MAP[key];
        if (!range) continue;

        const pRaw = weaponHeatRaw(stats.damage, stats.accuracy, range);
        if (pRaw === null) continue;

        const heatStep = quantizeHeat(toHeat(pRaw));
        const color = colorForHeat(heatStep);

        const anchor = findImageAnchor(tile);
        if (anchor) upsertBadge(anchor, pRaw, color);

      } else if (stats.isArmor) {
        const range = ARMOR_MAP[key];
        if (!range) continue;

        const pRaw = armorHeatRaw(stats.armor, range);
        if (pRaw === null) continue;

        const heatStep = quantizeHeat(toHeat(pRaw));
        const color = colorForHeat(heatStep);

        const anchor = findImageAnchor(tile);
        if (anchor) upsertBadge(anchor, pRaw, color);
      }
    }
  }

  function recolorBazaar() {
    const items = Array.from(document.querySelectorAll(BZ_ITEM_SELECTOR));
    for (const itemEl of items) {
      const name = bzGetName(itemEl);
      if (!name) continue;

      const key = normName(name);
      const stats = bzGetRollStats(itemEl);
      if (!stats.isWeapon && !stats.isArmor) continue;

      if (stats.isWeapon) {
        const range = WEAPON_MAP[key];
        if (!range) continue;

        const pRaw = weaponHeatRaw(stats.damage, stats.accuracy, range);
        if (pRaw === null) continue;

        const heatStep = quantizeHeat(toHeat(pRaw));
        const color = colorForHeat(heatStep);

        const anchor = findImageAnchor(itemEl);
        if (anchor) upsertBadge(anchor, pRaw, color);

      } else {
        const range = ARMOR_MAP[key];
        if (!range) continue;

        const pRaw = armorHeatRaw(stats.armor, range);
        if (pRaw === null) continue;

        const heatStep = quantizeHeat(toHeat(pRaw));
        const color = colorForHeat(heatStep);

        const anchor = findImageAnchor(itemEl);
        if (anchor) upsertBadge(anchor, pRaw, color);
      }
    }
  }

  function recolorItemPage() {
    const tiles = Array.from(document.querySelectorAll(IP_TILE_SELECTOR));
    for (const li of tiles) {
      const name = ipGetName(li);
      if (!name) continue;

      const key = normName(name);
      const stats = ipGetRollStats(li);
      if (!stats.isWeapon && !stats.isArmor) continue;

      if (stats.isWeapon) {
        const range = WEAPON_MAP[key];
        if (!range) continue;

        const pRaw = weaponHeatRaw(stats.damage, stats.accuracy, range);
        if (pRaw === null) continue;

        const heatStep = quantizeHeat(toHeat(pRaw));
        const color = colorForHeat(heatStep);

        const anchor = findImageAnchor(li);
        if (anchor) upsertBadge(anchor, pRaw, color);

      } else {
        const range = ARMOR_MAP[key];
        if (!range) continue;

        const pRaw = armorHeatRaw(stats.armor, range);
        if (pRaw === null) continue;

        const heatStep = quantizeHeat(toHeat(pRaw));
        const color = colorForHeat(heatStep);

        const anchor = findImageAnchor(li);
        if (anchor) upsertBadge(anchor, pRaw, color);
      }
    }
  }

  function recolorDumpTrash() {
    const tiles = Array.from(document.querySelectorAll(DP_TILE_SELECTOR));
    for (const li of tiles) {
      const name = dpGetName(li);
      if (!name) continue;

      const key = normName(name);
      const stats = dpGetRollStats(li);
      if (!stats.isWeapon && !stats.isArmor) continue;

      if (stats.isWeapon) {
        const range = WEAPON_MAP[key];
        if (!range) continue;

        const pRaw = weaponHeatRaw(stats.damage, stats.accuracy, range);
        if (pRaw === null) continue;

        const heatStep = quantizeHeat(toHeat(pRaw));
        const color = colorForHeat(heatStep);

        const anchor = findImageAnchor(li);
        if (anchor) upsertBadge(anchor, pRaw, color);

      } else {
        const range = ARMOR_MAP[key];
        if (!range) continue;

        const pRaw = armorHeatRaw(stats.armor, range);
        if (pRaw === null) continue;

        const heatStep = quantizeHeat(toHeat(pRaw));
        const color = colorForHeat(heatStep);

        const anchor = findImageAnchor(li);
        if (anchor) upsertBadge(anchor, pRaw, color);
      }
    }
  }

  function recolorFactionArmory() {
    const rows = Array.from(document.querySelectorAll(FA_TILE_SELECTOR));
    for (const li of rows) {
      const name = faGetName(li);
      if (!name) continue;

      const key = normName(name);
      const stats = faGetRollStats(li);
      if (!stats.isWeapon && !stats.isArmor) continue;

      if (stats.isWeapon) {
        const range = WEAPON_MAP[key];
        if (!range) continue;

        const pRaw = weaponHeatRaw(stats.damage, stats.accuracy, range);
        if (pRaw === null) continue;

        const heatStep = quantizeHeat(toHeat(pRaw));
        const color = colorForHeat(heatStep);

        const anchor = findImageAnchor(li);
        if (anchor) upsertBadge(anchor, pRaw, color);

      } else {
        const range = ARMOR_MAP[key];
        if (!range) continue;

        const pRaw = armorHeatRaw(stats.armor, range);
        if (pRaw === null) continue;

        const heatStep = quantizeHeat(toHeat(pRaw));
        const color = colorForHeat(heatStep);

        const anchor = findImageAnchor(li);
        if (anchor) upsertBadge(anchor, pRaw, color);
      }
    }
  }

  function recolorAuctionMarket() {
    const wraps = Array.from(document.querySelectorAll(AM_TILE_SELECTOR));
    for (const wrap of wraps) {
      const name =
        safeText(wrap.querySelector(AM_NAME_SELECTOR)) ||
        wrap.querySelector("img[alt]")?.getAttribute("alt")?.trim() ||
        null;
      if (!name) continue;

      const key = normName(name);

      // ✅ IMPORTANT: only parse stats from the stats wrapper, not the whole tile
      const statsRoot = wrap.querySelector(AM_STATS_WRAP_SELECTOR) || wrap;

      const damage  = readStatFromTile(statsRoot, "bonus-attachment-item-damage-bonus");
      const accuracy= readStatFromTile(statsRoot, "bonus-attachment-item-accuracy-bonus");
      const defence = readStatFromTile(statsRoot, "bonus-attachment-item-defence-bonus");

      const isWeapon = damage !== null && accuracy !== null;
      const isArmor = defence !== null && !isWeapon;
      if (!isWeapon && !isArmor) continue;

      let pRaw = null;
      if (isWeapon) {
        const range = WEAPON_MAP[key];
        if (!range) continue;
        pRaw = weaponHeatRaw(damage, accuracy, range);
      } else {
        const range = ARMOR_MAP[key];
        if (!range) continue;
        pRaw = armorHeatRaw(defence, range);
      }
      if (pRaw === null) continue;

      const heatStep = quantizeHeat(toHeat(pRaw));
      const color = colorForHeat(heatStep);

      const anchor = findImageAnchor(wrap);
      if (anchor) upsertBadge(anchor, pRaw, color);

      // Ensure no lingering Quality: tooltips anywhere in the tile
      removeQualityTitles(wrap);
    }
  }

  /** ==================== MAIN ==================== **/
  function recolorAll() {
    try {
      recolorItemMarket();
      recolorBazaar();
      recolorItemPage();
      recolorDumpTrash();
      recolorFactionArmory();
      recolorAuctionMarket();
    } catch (e) {
      console.error("[Torn Heat] Script error:", e);
    }
  }

  /** ==================== AUTO-UPDATE ==================== **/
  let scheduled = false;
  function schedule() {
    if (scheduled) return;
    scheduled = true;
    requestAnimationFrame(() => {
      scheduled = false;
      recolorAll();
    });
  }

  const observer = new MutationObserver(schedule);
  observer.observe(document.body, { childList:true, subtree:true });

  recolorAll();
})();