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.
// ==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();
})();