Weapon Bonus View PC

Display weapon/armor quality and bonuses directly in Torn market-style pages.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Weapon Bonus View PC
// @namespace    Weapon Bonus View PC
// @version      4.0.04
// @description  Display weapon/armor quality and bonuses directly in Torn market-style pages.
// @author       Maximate
// @match        https://www.torn.com/*
// @grant        GM_getValue
// @grant        GM.getValue
// @grant        GM_setValue
// @grant        GM.setValue
// @grant        GM_deleteValue
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM.xmlHttpRequest
// @connect      btrmmuuoofbonmuwrkzg.supabase.co
// @connect      weav3r.dev
// ==/UserScript==

(function () {
  "use strict";

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

  const App = {
    prefix: "[Weapon Bonus View]",
    state: {
      storage: {},
      requestHistory: [],
      cacheRecord: new Map(),
      floatingNodes: [],
      observers: [],
      intervals: [],
      bazaarCache: {},
      lastBazaarOrder: "",
      lastBazaarSearch: "",
      bazaarUpdating: false,
      interceptXMLInstalled: false,
      interceptFetchInstalled: false,
      context: {
        inItemList: false,
        inFactionArmoury: false,
        inCabinet: false,
      },
    },
  };

  const log = (msg) => console.log(`${App.prefix} ${msg}`);

  const GMX = {
    async get(key, fallback) {
      if (typeof GM !== "undefined" && GM.getValue) return GM.getValue(key, fallback);
      if (typeof GM_getValue === "function") return Promise.resolve(GM_getValue(key, fallback));
      return fallback;
    },
    async set(key, value) {
      if (typeof GM !== "undefined" && GM.setValue) return GM.setValue(key, value);
      if (typeof GM_setValue === "function") return Promise.resolve(GM_setValue(key, value));
    },
    style(css) {
      if (typeof GM_addStyle === "function") return GM_addStyle(css);
      const style = document.createElement("style");
      style.textContent = css;
      document.head.appendChild(style);
    },
  };

  const Data = {
    weaponIds: new Set([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,63,76,98,99,100,108,109,110,111,146,147,170,173,174,175,177,189,217,218,219,223,224,225,227,228,230,231,232,233,234,235,236,237,238,240,241,243,244,245,247,248,249,250,251,252,253,254,255,289,290,291,292,346,359,360,382,387,388,391,393,395,397,398,399,400,401,402,438,439,440,483,484,485,486,487,488,489,490,539,545,546,547,548,549,599,600,604,605,612,613,614,615,632,790,792,805,830,831,832,837,838,839,844,845,846,850,871,874,1053,1055,1056,1152,1153,1154,1155,1156,1157,1158,1159,1231,1255,1257,1296]),
    armorIds: new Set([32,33,34,49,50,176,178,332,333,334,348,538,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,848,1307,1308,1309,1310,1311,1355,1356,1357,1358,1359]),
    filterTypes: new Set(["Melee", "Secondary", "Primary", "Armor"]),
    itemNameToId: {"Hammer":1,"Baseball Bat":2,"Crowbar":3,"Knuckle Dusters":4,"Pen Knife":5,"Kitchen Knife":6,"Dagger":7,"Axe":8,"Scimitar":9,"Chainsaw":10,"Samurai Sword":11,"Glock 17":12,"Raven MP25":13,"Ruger 57":14,"Beretta M9":15,"USP":16,"Beretta 92FS":17,"Fiveseven":18,"Magnum":19,"Desert Eagle":20,"Dual 92G Berettas":21,"Sawed-Off Shotgun":22,"Benelli M1 Tactical":23,"MP5 Navy":24,"P90":25,"AK-47":26,"M4A1 Colt Carbine":27,"Benelli M4 Super":28,"M16 A2 Rifle":29,"Steyr AUG":30,"M249 SAW":31,"Leather Vest":32,"Police Vest":33,"Bulletproof Vest":34,"Full Body Armor":49,"Outer Tactical Vest":50,"Minigun":63,"Snow Cannon":76,"Neutrilux 2000":98,"Springfield 1911":99,"Egg Propelled Launcher":100,"9mm Uzi":108,"RPG Launcher":109,"Leather Bullwhip":110,"Ninja Claws":111,"Yasukuni Sword":146,"Rusty Sword":147,"Wand of Destruction":170,"Butterfly Knife":173,"XM8 Rifle":174,"Taser":175,"Chain Mail":176,"Cobra Derringer":177,"Flak Jacket":178,"S&W Revolver":189,"Claymore Sword":217,"Crossbow":218,"Enfield SA-80":219,"Jackhammer":223,"Swiss Army Knife":224,"Mag 7":225,"Spear":227,"Vektor CR-21":228,"Flare Gun":230,"Heckler & Koch SL8":231,"SIG 550":232,"BT MP9":233,"Chain Whip":234,"Wooden Nunchaku":235,"Kama":236,"Kodachi":237,"Sai":238,"Type 98 Anti Tank":240,"Bushmaster Carbon 15":241,"Taurus":243,"Blowgun":244,"Bo Staff":245,"Katana":247,"Qsz-92":248,"SKS Carbine":249,"Twin Tiger Hooks":250,"Wushu Double Axes":251,"Ithaca 37":252,"Lorcin 380":253,"S&W M29":254,"Flamethrower":255,"Dual Axes":289,"Dual Hammers":290,"Dual Scimitars":291,"Dual Samurai Swords":292,"Combat Vest":332,"Liquid Body Armor":333,"Flexible Body Armor":334,"Pair of High Heels":346,"Hazmat Suit":348,"Fine Chisel":359,"Ivory Walking Cane":360,"Gold Plated AK-47":382,"Handbag":387,"Pink Mac-10":388,"Macana":391,"Slingshot":393,"Metal Nunchaku":395,"Flail":397,"SIG 552":398,"ArmaLite M-15A4":399,"Guandao":400,"Lead Pipe":401,"Ice Pick":402,"Cricket Bat":438,"Frying Pan":439,"Pillow":440,"MP5k":483,"AK74U":484,"Skorpion":485,"TMP":486,"Thompson":487,"MP 40":488,"Luger":489,"Blunderbuss":490,"Medieval Helmet":538,"Blood Spattered Sickle":539,"Dual TMPs":545,"Dual Bushmasters":546,"Dual MP5s":547,"Dual P90s":548,"Dual Uzis":549,"Golden Broomstick":599,"Devil's Pitchfork":600,"Pair of Ice Skates":604,"Diamond Icicle":605,"Tavor TAR-21":612,"Harpoon":613,"Diamond Bladed Knife":614,"Naval Cutlass":615,"Petrified Humerus":632,"Kevlar Gloves":640,"WWII Helmet":641,"Motorcycle Helmet":642,"Construction Helmet":643,"Welding Helmet":644,"Safety Boots":645,"Hiking Boots":646,"Leather Helmet":647,"Leather Pants":648,"Leather Boots":649,"Leather Gloves":650,"Combat Helmet":651,"Combat Pants":652,"Combat Boots":653,"Combat Gloves":654,"Riot Helmet":655,"Riot Body":656,"Riot Pants":657,"Riot Boots":658,"Riot Gloves":659,"Dune Helmet":660,"Dune Vest":661,"Dune Pants":662,"Dune Boots":663,"Dune Gloves":664,"Assault Helmet":665,"Assault Body":666,"Assault Pants":667,"Assault Boots":668,"Assault Gloves":669,"Delta Gas Mask":670,"Delta Body":671,"Delta Pants":672,"Delta Boots":673,"Delta Gloves":674,"Marauder Face Mask":675,"Marauder Body":676,"Marauder Pants":677,"Marauder Boots":678,"Marauder Gloves":679,"EOD Helmet":680,"EOD Apron":681,"EOD Pants":682,"EOD Boots":683,"EOD Gloves":684,"Plastic Sword":790,"Penelope":792,"Duke's Hammer":805,"Nock Gun":830,"Beretta Pico":831,"Riding Crop":832,"Rheinmetall MG 3":837,"Homemade Pocket Shotgun":838,"Madball":839,"Tranquilizer Gun":844,"Bolt Gun":845,"Scalpel":846,"Kevlar Lab Coat":848,"Sledgehammer":850,"Bug Swatter":871,"Prototype":874,"Bread Knife":1053,"Poison Umbrella":1055,"Millwall Brick":1056,"SMAW Launcher":1152,"China Lake":1153,"Milkor MGL":1154,"PKM":1155,"Negev NG-5":1156,"Stoner 96":1157,"Meat Hook":1158,"Cleaver":1159,"Golf Club":1231,"Bone Saw":1255,"Cattle Prod":1257,"Ban Hammer":1296,"Sentinel Helmet":1307,"Sentinel Apron":1308,"Sentinel Pants":1309,"Sentinel Boots":1310,"Sentinel Gloves":1311,"Vanguard Respirator":1355,"Vanguard Body":1356,"Vanguard Pants":1357,"Vanguard Boots":1358,"Vanguard Gloves":1359},
    bonusClassExceptions: {"poison":"poisoned","demoralize":"demoralized","freeze":"frozen","hazardous":"hazarfouse","impassable":"full-block","radiation protection":"item-radiation-bonus","immutable":"sentinel","invulnerable":"negative-status_mitigation","imperviable":"life-bonus","impenetrable":"bullets-protection","insurmountable":"quarter-life-damage-mitigation","impregnable":"melee-protection","burn":"burning","proficience":"experience","eviscerate":"evicerate","double-edged":"doubleedged"},
  };

  const Config = {
    key: "weapon_bonus_view_config",
    defaults: {
      API: "",
      ENABLE_BAZAAR_UPDATE: true,
      LIMIT_PER_REFRESH: 20,
      STORAGE_KEY: "weapon_bonus_view_cache",
      COLOR_SET: {
        defaultBlue: "#00A9F9",
        defaultRed: "#E54C19",
        defaultGreen: "#82c91e",
        defaultPurple: "#b072ef",
        defaultYellow: "#CA9800",
        defaultOrange: "#E67700",
        normal: { bonusBg: "#FFF", bonusFont: "#333" },
        dark: { bonusBg: "#444", bonusFont: "#ddd" },
      },
    },
    value: null,
    load() {
      let saved = {};
      try {
        const raw = localStorage.getItem(this.key);
        if (raw) saved = JSON.parse(raw);
      } catch {}
      let api = localStorage.getItem("APIKey") || "";
      const pdaKey = "###PDA-APIKEY###";
      if (pdaKey.charAt(0) !== "#" && !api) api = pdaKey;
      if (api && !localStorage.getItem("APIKey")) localStorage.setItem("APIKey", api);
      this.value = { ...this.defaults, ...saved, API: api || saved.API || "" };
      localStorage.setItem(this.key, JSON.stringify(this.value));
      if (!this.value.API) log("No valid API, please insert a public API");
    },
  };

  const Store = {
    async init() { App.state.storage = await GMX.get(Config.value.STORAGE_KEY, {}); },
    async save() { await GMX.set(Config.value.STORAGE_KEY, App.state.storage); },
    get(id) { return App.state.storage[String(id)]; },
    async set(id, value) { App.state.storage[String(id)] = value; await this.save(); },
  };

  const Util = {
    isMobile() { return window.innerWidth <= 768; },
    isSupportedItem(itemId) { return Data.weaponIds.has(itemId) || Data.armorIds.has(itemId); },
    getItemType(itemId) { if (Data.weaponIds.has(itemId)) return "weapon"; if (Data.armorIds.has(itemId)) return "armor"; return null; },
    clearFloatingNodes() { for (const node of App.state.floatingNodes) if (node?.remove) node.remove(); App.state.floatingNodes = []; },
    setContext(partial) { App.state.context = { inItemList: false, inFactionArmoury: false, inCabinet: false, ...partial }; },
    updateCacheRecord(armouryId, item, updateFn) { if (armouryId) App.state.cacheRecord.set(String(armouryId), { item, updateFn }); },
    invokeCacheUpdate(armouryId) { const record = App.state.cacheRecord.get(String(armouryId)); if (record) record.updateFn(record.item); },
    safeText(el) { return el && el.length ? el.text().trim() : ""; },
    isLocked(item) { return item.attr("data-wbv-state") === "pending"; },
    markPending(item) { item.attr("data-wbv-state", "pending"); },
    markDone(item) { item.attr("data-wbv-state", "done"); },
    clearMark(item) { item.removeAttr("data-wbv-state"); },
    appendUnique(target, node, item) {
      if (!target || !target.length || !node) {
        Util.clearMark(item);
        return false;
      }
      target.find("> div.item-detail").remove();
      target.append(node);
      Util.markDone(item);
      return true;
    },
    placeFloatingNode(item, node) {
      if (!item || !item.length || !node) return false;
      const rowOffset = item.offset();
      if (!rowOffset) return false;
      node.css({
        position: "absolute",
        left: `${rowOffset.left + 120}px`,
        top: `${rowOffset.top + 6}px`,
        zIndex: 100002,
        pointerEvents: "none",
      });
      $("body").append(node);
      App.state.floatingNodes.push(node);
      Util.markDone(item);
      return true;
    },
    onHashChange() { window.addEventListener("hashchange", () => { App.state.cacheRecord.clear(); Util.clearFloatingNodes(); }); },
  };

  const Api = {
    async getItemDetails(armouryId) {
      const key = String(armouryId);
      if (!key) return null;
      const cached = Store.get(key);
      if (cached) return cached;
      if (App.state.requestHistory.includes(key)) return null;
      if (App.state.requestHistory.length >= Config.value.LIMIT_PER_REFRESH && App.state.context.inItemList) return null;
      if (!Config.value.API) return null;
      App.state.requestHistory.push(key);
      try {
        const response = await $.ajax({ url: `https://api.torn.com/torn/${key}?selections=itemdetails&key=${Config.value.API}` });
        const info = response?.itemdetails;
        if (!info) return null;
        await Store.set(key, info);
        return info;
      } catch { return null; }
    },
    async cacheInventoryResponse(resp) {
      const { itemName, itemID, armoryID, extras } = resp || {};
      if (!armoryID || !extras) return null;
      let quality, accuracy, damage, armor, bonuses;
      for (const key in extras) {
        const extra = extras[key];
        if (!extra) continue;
        if (extra.title === "Quality") quality = extra.value.replace("%", "");
        else if (extra.title === "Armor") armor = extra.value;
        else if (extra.title === "Damage") damage = extra.value;
        else if (extra.title === "Accuracy") accuracy = extra.value;
        else if (extra.title === "Bonus") {
          bonuses ||= {};
          const description = extra.descTitle?.split("<br/>")?.[1] || "";
          bonuses[extra.value] = { icon: extra.icon, description, bonus: extra.value, value: parseInt(String(extra.rawValue || "").replace("%", ""), 10) };
        }
      }
      const info = { ID: itemID, UID: armoryID, name: itemName, quality };
      if (armor) info.armor = parseFloat(armor);
      if (damage) info.damage = parseFloat(damage);
      if (accuracy) info.accuracy = parseFloat(accuracy);
      if (bonuses) info.bonuses = bonuses;
      await Store.set(armoryID, info);
      return armoryID;
    },
  };

  const UI = {
    handlePercent(description, value) { const idx = description.indexOf(value); return idx >= 0 && description[idx + String(value).length] === "%" ? `${value}%` : value; },
    getBonusIconClass(name) { const normalized = String(name || "").toLowerCase(); const mapped = Data.bonusClassExceptions[normalized] || normalized; return `bonus-attachment-${mapped}`; },
    extractBonuses(bonuses) {
      if (!bonuses) return null;
      const out = [];
      const seen = new Set();
      for (const key in bonuses) {
        const item = bonuses[key];
        if (!item) continue;
        const { bonus, value, description, icon } = item;
        if (!description || description.indexOf(value) === -1) continue;
        const displayValue = UI.handlePercent(description, value);
        const dedupeKey = `${String(bonus).toLowerCase()}|${displayValue}|${description}`;
        if (seen.has(dedupeKey)) continue;
        seen.add(dedupeKey);
        out.push({ bonus, value: displayValue, description, html: `<i class="${icon || UI.getBonusIconClass(bonus)}" title="${description}"></i><font color="${Config.value.COLOR_SET.defaultPurple}" style="margin-left:5px;">${displayValue}</font>` });
      }
      return out.length ? out : null;
    },
    getQualityColor(quality, rarity = 0) {
      const colors = Config.value.COLOR_SET;
      if (rarity === 1 || rarity === "Yellow") return colors.defaultYellow;
      if (rarity === 2 || rarity === "Orange") return colors.defaultOrange;
      if (rarity === 3 || rarity === "Red") return colors.defaultRed;
      if (quality < 20) return colors.defaultRed;
      if (quality < 50) return colors.defaultYellow;
      return colors.defaultGreen;
    },
    createItemDetailsNode(itemDetails, isBazaar = false) {
      const { damage, accuracy, quality, armor, bonuses, rarity } = itemDetails;
      const q = parseFloat(quality || 0);
      const qualityColor = UI.getQualityColor(q, rarity);
      const qualityHtml = `<font color="${qualityColor}" style="margin-left:5px;" title="Quality: ${q}%">Q:${q.toFixed(2)}%</font>`;
      const bonusDetails = UI.extractBonuses(bonuses);
      if (bonusDetails) {
        if (App.state.context.inFactionArmoury || App.state.context.inItemList || App.state.context.inCabinet) {
          const wrapper = $(`<div class="item-detail" style="display:flex;align-items:center;"></div>`);
          const bonus = bonusDetails[0];
          const tag = $(`<div class="message icon-text border-round" title="${bonus.description}">${bonus.bonus} ${bonus.value}</div>`);
          tag.css({ display: "flex", height: "20px", lineHeight: "20px", padding: "0 4px", marginLeft: "5px", fontWeight: "bold", textAlign: "center", backgroundColor: qualityColor, color: "#eee" });
          wrapper.append(tag);
          return wrapper;
        }
        if (Util.isMobile() || isBazaar) return $(`<div class="item-detail" style="display:flex;align-items:center;">${bonusDetails[0].html}</div>`);
        return $(`<div class="item-detail" style="display:flex;align-items:center;">${bonusDetails[0].html}${qualityHtml}</div>`);
      }
      if ((Util.isMobile() && !App.state.context.inFactionArmoury) || isBazaar || App.state.context.inCabinet) return $(`<div class="item-detail" style="display:flex;align-items:center;">${qualityHtml}</div>`);
      if (armor) return $(`<div class="item-detail" style="display:flex;align-items:center;"><i class="bonus-attachment-item-defence-bonus"></i><font color="${Config.value.COLOR_SET.defaultGreen}">${parseFloat(armor).toFixed(2)}</font>${qualityHtml}</div>`);
      return $(`<div class="item-detail" style="display:flex;align-items:center;"><i class="bonus-attachment-item-damage-bonus"></i><font color="${Config.value.COLOR_SET.defaultRed}">${parseFloat(damage || 0).toFixed(2)}</font><i class="bonus-attachment-item-accuracy-bonus" style="margin-left:5px;"></i><font color="${Config.value.COLOR_SET.defaultBlue}">${parseFloat(accuracy || 0).toFixed(2)}</font>${qualityHtml}</div>`);
    },
    addStyles() {
      if (document.getElementById("weapon-bonus-view-styles")) return;
      GMX.style(`
        .top-bonuses, .bottom-bonuses { position: absolute; left: 3px; display: flex; flex-flow: row; justify-content: space-between; width: 100px; }
        .top-bonuses { top: -5px; }
        .bottom-bonuses { bottom: -5px; }
        body.dark-mode .bonus-attachment, body.dark-mode .armory-bg { background: ${Config.value.COLOR_SET.dark.bonusBg} !important; color: ${Config.value.COLOR_SET.dark.bonusFont} !important; }
        .armory-bg { background: ${Config.value.COLOR_SET.normal.bonusBg}; color: ${Config.value.COLOR_SET.normal.bonusFont}; }
        .bonus-attachment { background: ${Config.value.COLOR_SET.normal.bonusBg}; color: ${Config.value.COLOR_SET.normal.bonusFont}; box-shadow: 0 1px 0 #00000003; border-radius: 7px; padding: 2px; margin-right: 1px; display: flex; align-items: center; float: left; }
        .icon-attachment { float: left; }
      `);
      const marker = document.createElement("div");
      marker.id = "weapon-bonus-view-styles";
      marker.style.display = "none";
      document.body.appendChild(marker);
    },
  };

  const Page = {
    observe(target, callback) { if (!target) return; const obs = new MutationObserver(callback); obs.observe(target, { subtree: true, childList: true }); App.state.observers.push(obs); },
    interceptXML() {
      if (App.state.interceptXMLInstalled) return;
      App.state.interceptXMLInstalled = true;
      const originalOpen = XMLHttpRequest.prototype.open;
      XMLHttpRequest.prototype.open = function (...args) {
        this.addEventListener("readystatechange", function () {
          if (this.readyState !== 4) return;
          try {
            const url = new URL(this.responseURL);
            const params = new URLSearchParams(url.search);
            if (url.pathname === "/page.php" && params.get("sid") === "inventory") {
              Api.cacheInventoryResponse(JSON.parse(this.response)).then((armouryId) => { if (armouryId) Util.invokeCacheUpdate(armouryId); }).catch(() => {});
            }
          } catch {}
        }, false);
        return originalOpen.apply(this, args);
      };
    },
    interceptFetch() {
      if (App.state.interceptFetchInstalled) return;
      App.state.interceptFetchInstalled = true;
      const targetWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
      const originalFetch = targetWindow.fetch;
      targetWindow.fetch = async (...args) => {
        const response = await originalFetch(...args);
        try {
          const requestUrl = typeof args[0] === "string" ? args[0] : args[0]?.url;
          if (!requestUrl) return response;
          const url = new URL(requestUrl, location.origin);
          const params = new URLSearchParams(url.search);
          if (url.pathname === "/bazaar.php" && params.get("sid") === "bazaarData") {
            const order = params.get("order") || "";
            const searchName = params.get("searchname") || "";
            if (order !== App.state.lastBazaarOrder) { App.state.lastBazaarOrder = order; App.state.bazaarCache = {}; }
            if (searchName !== App.state.lastBazaarSearch) { App.state.lastBazaarSearch = searchName; App.state.bazaarCache = {}; }
            const cloned = response.clone();
            const json = await cloned.json();
            Modules.userBazaar.handleResponse(params, json);
          }
        } catch {}
        return response;
      };
    },
  };

  const PriceChecker = {
    config: {
      supabaseUrl: "https://btrmmuuoofbonmuwrkzg.supabase.co",
      supabaseAnonKey: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJ0cm1tdXVvb2Zib25tdXdya3pnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njg4NTEzMTgsImV4cCI6MjA4NDQyNzMxOH0.E-s0k46BORXLICAvxtEpqoM3Qmh4-TRLaJAwXO6wJTY",
      marketApi: "https://weav3r.dev/api/ranked-weapons",
      cacheKey: "wbv_price_checker_cache",
      cacheTtl: 5 * 60 * 1000,
      maxCacheEntries: 50,
      settingsKey: "wbv_price_checker_settings",
    },
    state: {
      open: false,
      eventsBound: false,
      activeTab: "history",
      loading: false,
      marketLoading: false,
      error: "",
      marketError: "",
      results: [],
      marketResults: [],
      total: 0,
      marketTotal: 0,
      offset: 0,
      itemType: "weapon",
      filters: { itemName: "", bonus1Id: "", bonus1Min: "", bonus1Max: "", bonus2Id: "", bonus2Min: "", bonus2Max: "", qualityMin: "", qualityMax: "" },
    },
    bonusData: [
      {id:50,title:"Achilles"},{id:72,title:"Assassinate"},{id:52,title:"Backstab"},{id:54,title:"Berserk"},{id:57,title:"Bleed"},{id:33,title:"Blindfire"},{id:51,title:"Blindside"},{id:85,title:"Bloodlust"},{id:67,title:"Comeback"},{id:55,title:"Conserve"},{id:45,title:"Cripple"},{id:49,title:"Crusher"},{id:47,title:"Cupid"},{id:63,title:"Deadeye"},{id:62,title:"Deadly"},{id:36,title:"Demoralize"},{id:86,title:"Disarm"},{id:105,title:"Double Tap"},{id:74,title:"Double-edged"},{id:87,title:"Empower"},{id:56,title:"Eviscerate"},{id:75,title:"Execute"},{id:1,title:"Expose"},{id:82,title:"Finale"},{id:79,title:"Focus"},{id:38,title:"Freeze"},{id:80,title:"Frenzy"},{id:64,title:"Fury"},{id:53,title:"Grace"},{id:34,title:"Hazardous"},{id:83,title:"Home run"},{id:115,title:"Immutable"},{id:26,title:"Impassable"},{id:17,title:"Impenetrable"},{id:22,title:"Imperviable"},{id:15,title:"Impregnable"},{id:92,title:"Insurmountable"},{id:91,title:"Invulnerable"},{id:102,title:"Irradiate"},{id:121,title:"Irrepressible"},{id:112,title:"Kinetokinesis"},{id:89,title:"Lacerate"},{id:61,title:"Motivation"},{id:59,title:"Paralyze"},{id:84,title:"Parry"},{id:101,title:"Penetrate"},{id:21,title:"Plunder"},{id:68,title:"Powerful"},{id:14,title:"Proficience"},{id:66,title:"Puncture"},{id:88,title:"Quicken"},{id:90,title:"Radiation Protection"},{id:65,title:"Rage"},{id:41,title:"Revitalize"},{id:43,title:"Roshambo"},{id:120,title:"Shock"},{id:44,title:"Slow"},{id:104,title:"Smash"},{id:73,title:"Smurf"},{id:71,title:"Specialist"},{id:35,title:"Spray"},{id:37,title:"Storage"},{id:20,title:"Stricken"},{id:58,title:"Stun"},{id:60,title:"Suppress"},{id:78,title:"Sure Shot"},{id:48,title:"Throttle"},{id:103,title:"Toxin"},{id:81,title:"Warlord"},{id:46,title:"Weaken"},{id:76,title:"Wind-up"},{id:42,title:"Wither"},
    ],
    bonusMap: {},
    defaults: { bonusTolerance: 10, qualityTolerance: 10, disableBonusValueFilter: false, disableQualityFilter: false },
    escape(value) {
      return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
    },
    getSettings() {
      try { return { ...this.defaults, ...(JSON.parse(localStorage.getItem(this.config.settingsKey) || "{}")) }; } catch { return { ...this.defaults }; }
    },
    saveSettings(next) { localStorage.setItem(this.config.settingsKey, JSON.stringify({ ...this.getSettings(), ...next })); },
    getCacheStore() {
      try { return JSON.parse(localStorage.getItem(this.config.cacheKey) || "{}"); } catch { return {}; }
    },
    setCacheStore(store) { try { localStorage.setItem(this.config.cacheKey, JSON.stringify(store)); } catch {} },
    getCached(key) {
      const store = this.getCacheStore();
      const entry = store[key];
      if (entry && Date.now() - entry.ts < this.config.cacheTtl) return entry.data;
      if (entry) { delete store[key]; this.setCacheStore(store); }
      return null;
    },
    setCached(key, data) {
      const store = this.getCacheStore();
      const keys = Object.keys(store);
      if (keys.length >= this.config.maxCacheEntries) keys.sort((a, b) => store[a].ts - store[b].ts).slice(0, 10).forEach((k) => delete store[k]);
      store[key] = { ts: Date.now(), data };
      this.setCacheStore(store);
    },
    async request(options) {
      const useGM = typeof GM_xmlhttpRequest === "function" ? GM_xmlhttpRequest : (typeof GM !== "undefined" && typeof GM.xmlHttpRequest === "function" ? GM.xmlHttpRequest.bind(GM) : null);
      if (useGM) {
        return new Promise((resolve, reject) => {
          useGM({
            method: options.method || "GET",
            url: options.url,
            headers: options.headers,
            data: options.body,
            timeout: 15000,
            onload: (response) => response.status >= 200 && response.status < 300 ? resolve(response.responseText) : reject(new Error(`API error: ${response.status}`)),
            onerror: () => reject(new Error("Network error")),
            ontimeout: () => reject(new Error("Request timeout")),
          });
        });
      }
      const response = await fetch(options.url, { method: options.method || "GET", headers: options.headers, body: options.body });
      if (!response.ok) throw new Error(`API error: ${response.status}`);
      return response.text();
    },
    async requestJson(options) {
      const text = await this.request(options);
      try { return JSON.parse(text); } catch { throw new Error("Parse error"); }
    },
    formatPrice(p) {
      const value = Number(p || 0);
      if (value >= 1e9) return `$${(value / 1e9).toFixed(2)}B`;
      if (value >= 1e6) return `$${(value / 1e6).toFixed(2)}M`;
      if (value >= 1e3) return `$${(value / 1e3).toFixed(1)}K`;
      return `$${value.toLocaleString()}`;
    },
    formatDate(ts) {
      const d = new Date(Number(ts) * 1000);
      if (Number.isNaN(d.getTime())) return "?";
      const diff = Math.floor((Date.now() - d.getTime()) / 86400000);
      if (diff === 0) return "Today";
      if (diff === 1) return "Yesterday";
      if (diff < 7) return `${diff}d ago`;
      return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
    },
    getBonusId(name) {
      if (!name) return null;
      const normalized = String(name).toLowerCase().replace(/[\s-]/g, "");
      return this.bonusMap[normalized] || this.bonusMap[String(name).toLowerCase()] || null;
    },
    getBonusName(id) { return this.bonusMap[id] || `Bonus #${id}`; },
    extractBonuses(itemDetail) {
      const bonuses = itemDetail?.bonuses;
      if (!bonuses) return [];
      const out = [];
      for (const key in bonuses) {
        const current = bonuses[key];
        if (!current?.bonus) continue;
        const id = this.getBonusId(current.bonus);
        if (!id) continue;
        const rawValue = current.value != null ? Number(current.value) : null;
        out.push({ id, value: Number.isFinite(rawValue) ? rawValue : null });
      }
      return out.slice(0, 2);
    },
    calcRange(value, tolerance) {
      if (value == null || value === "") return { min: "", max: "" };
      const numeric = Number(value);
      if (!Number.isFinite(numeric)) return { min: "", max: "" };
      if (tolerance === 0) return { min: String(numeric), max: String(numeric) };
      const factor = tolerance / 100;
      return { min: String(Math.floor(numeric * (1 - factor))), max: String(Math.ceil(numeric * (1 + factor))) };
    },
    prepareFilters(itemDetail, itemType) {
      const bonuses = this.extractBonuses(itemDetail);
      const settings = this.getSettings();
      this.state.results = [];
      this.state.marketResults = [];
      this.state.total = 0;
      this.state.marketTotal = 0;
      this.state.error = "";
      this.state.marketError = "";
      this.state.filters.itemName = itemDetail?.name || "";
      this.state.itemType = itemType || "weapon";
      this.state.offset = 0;
      this.state.filters.bonus1Id = bonuses[0]?.id ? String(bonuses[0].id) : "";
      Object.assign(this.state.filters, this.calcBonusFields("bonus1", bonuses[0]?.value, settings.bonusTolerance));
      this.state.filters.bonus2Id = bonuses[1]?.id ? String(bonuses[1].id) : "";
      Object.assign(this.state.filters, this.calcBonusFields("bonus2", bonuses[1]?.value, settings.bonusTolerance));
      const qualityRange = this.calcRange(itemDetail?.quality, settings.qualityTolerance);
      this.state.filters.qualityMin = qualityRange.min;
      this.state.filters.qualityMax = qualityRange.max;
    },
    calcBonusFields(prefix, value, tolerance) {
      const range = this.calcRange(value, tolerance);
      return { [`${prefix}Min`]: range.min, [`${prefix}Max`]: range.max };
    },
    ensureModal() {
      if (document.getElementById("wbv-price-modal")) return;
      const styleId = "wbv-price-checker-styles";
      if (!document.getElementById(styleId)) {
        GMX.style(`
          #wbv-price-overlay{position:fixed;inset:0;z-index:100099;background:rgba(0,0,0,.62);backdrop-filter:blur(2px);display:none;}
          #wbv-price-overlay.open{display:block;}
          #wbv-price-modal{position:fixed;inset:5vh auto auto 50%;transform:translateX(-50%);width:min(860px,94vw);max-height:90vh;z-index:100100;background:#1e1f22;color:#dbdee1;border:1px solid #111214;border-radius:14px;display:none;flex-direction:column;box-shadow:0 24px 60px rgba(0,0,0,.55);overflow:hidden;font-family:Whitney,"Helvetica Neue",Helvetica,Arial,sans-serif;}
          #wbv-price-modal.open{display:flex;} #wbv-price-modal .wbv-pc-head{display:flex;justify-content:space-between;align-items:center;padding:14px 16px;background:#1a1b1e;border-bottom:1px solid rgba(255,255,255,.04);}
          #wbv-price-modal .wbv-pc-title{display:flex;flex-direction:column;gap:2px;} #wbv-price-modal .wbv-pc-title strong{font-size:17px;color:#f2f3f5;} #wbv-price-modal .wbv-pc-sub{font-size:12px;color:#949ba4;}
          #wbv-price-modal .wbv-pc-tabs{display:flex;gap:8px;padding:10px 12px 0;background:#1e1f22;} #wbv-price-modal .wbv-pc-tab{flex:1;padding:9px 10px;background:#2b2d31;border:none;border-radius:8px;color:#b5bac1;font-weight:700;cursor:pointer;}
          #wbv-price-modal .wbv-pc-tab.active{color:#fff;background:#404249;} #wbv-price-modal .wbv-pc-body{padding:12px;overflow:auto;} #wbv-price-modal .wbv-pc-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;padding:10px;border-radius:12px;background:#232428;border:1px solid rgba(255,255,255,.04);}
          #wbv-price-modal .wbv-pc-grid input,#wbv-price-modal .wbv-pc-grid select{width:100%;box-sizing:border-box;padding:9px 10px;border:1px solid #111214;border-radius:8px;background:#111214;color:#f2f3f5;}
          #wbv-price-modal .wbv-pc-actions{display:flex;gap:8px;align-items:center;margin-top:10px;flex-wrap:wrap;} #wbv-price-modal .wbv-pc-btn,#wbv-price-modal .wbv-pc-close,#wbv-price-modal .wbv-pc-small{padding:8px 11px;border:none;border-radius:8px;background:#5865f2;color:#fff;font-weight:700;cursor:pointer;}
          #wbv-price-modal .wbv-pc-close{background:#2b2d31;color:#dbdee1;} #wbv-price-modal .wbv-pc-small{background:#2b2d31;padding:7px 10px;font-size:12px;} #wbv-price-modal .wbv-pc-tog{display:flex;gap:12px;flex-wrap:wrap;font-size:12px;color:#b5bac1;}
          #wbv-price-modal .wbv-pc-stream{display:flex;flex-direction:column;gap:2px;margin-top:12px;} #wbv-price-modal .wbv-pc-item{display:grid;grid-template-columns:40px 1fr;gap:10px;padding:10px 8px;border-radius:10px;} #wbv-price-modal .wbv-pc-item:hover{background:rgba(255,255,255,.03);}
          #wbv-price-modal .wbv-pc-avatar{width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#5865f2,#3ba55d);display:flex;align-items:center;justify-content:center;color:#fff;font-size:12px;font-weight:700;} #wbv-price-modal .wbv-pc-content{min-width:0;}
          #wbv-price-modal .wbv-pc-line{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;} #wbv-price-modal .wbv-pc-name{font-size:14px;font-weight:700;color:#f2f3f5;} #wbv-price-modal .wbv-pc-time{font-size:12px;color:#949ba4;}
          #wbv-price-modal .wbv-pc-meta{font-size:13px;color:#b5bac1;margin-top:3px;line-height:1.45;} #wbv-price-modal .wbv-pc-badges{display:flex;gap:6px;flex-wrap:wrap;margin-top:7px;}
          #wbv-price-modal .wbv-pc-bonus{display:inline-flex;align-items:center;padding:3px 8px;border-radius:999px;background:#2b2d31;font-size:11px;color:#f2f3f5;} #wbv-price-modal .wbv-pc-price{margin-top:7px;font-size:13px;color:#949cf7;font-weight:700;}
          #wbv-price-modal .wbv-pc-foot{display:flex;justify-content:space-between;align-items:center;padding:11px 12px;border-top:1px solid rgba(255,255,255,.04);background:#1a1b1e;} #wbv-price-modal .wbv-pc-empty,#wbv-price-modal .wbv-pc-loading{padding:14px 6px;color:#949ba4;font-size:13px;} #wbv-price-modal .wbv-pc-error{color:#ff8787;padding:10px 2px;} .wbv-price-check{margin-left:6px;padding:4px 9px;border:none;border-radius:999px;background:#5865f2;color:#fff;font-size:11px;line-height:1.2;cursor:pointer;font-weight:700;}
        `);
        const marker = document.createElement("div");
        marker.id = styleId;
        marker.style.display = "none";
        document.body.appendChild(marker);
      }
      const overlay = document.createElement("div");
      overlay.id = "wbv-price-overlay";
      document.body.appendChild(overlay);
      const modal = document.createElement("div");
      modal.id = "wbv-price-modal";
      document.body.appendChild(modal);
      this.bindEvents();
    },
    bindEvents() {
      if (this.state.eventsBound) return;
      this.state.eventsBound = true;
      document.addEventListener("keydown", (event) => {
        if (event.key === "Escape" && this.state.open) this.close();
      });
      document.addEventListener("click", (event) => {
        if (event.target && event.target.id === "wbv-price-overlay" && this.state.open) this.close();
      });
    },
    attachButton(target, itemDetail, itemType) {
      if (!target || !target.length || !itemDetail?.name) return;
      if (target.find(".wbv-price-check").length) return;
      const button = $('<button class="wbv-price-check" type="button">Price</button>');
      button.on("click", (event) => {
        event.preventDefault();
        event.stopPropagation();
        this.prepareFilters(itemDetail, itemType);
        this.open();
      });
      target.append(button);
    },
    open() { this.ensureModal(); this.state.open = true; this.state.activeTab = "history"; this.render(); this.searchHistory(); },
    close() {
      this.state.open = false;
      const modal = document.getElementById("wbv-price-modal");
      const overlay = document.getElementById("wbv-price-overlay");
      if (modal) modal.classList.remove("open");
      if (overlay) overlay.classList.remove("open");
    },
    render() {
      this.ensureModal();
      const modal = document.getElementById("wbv-price-modal");
      if (!modal) return;
      const overlay = document.getElementById("wbv-price-overlay");
      if (!this.state.open) {
        modal.classList.remove("open");
        if (overlay) overlay.classList.remove("open");
        return;
      }
      modal.classList.add("open");
      if (overlay) overlay.classList.add("open");
      const settings = this.getSettings();
      const options = this.bonusData.slice().sort((a, b) => a.title.localeCompare(b.title)).map((bonus) => `<option value="${bonus.id}">${this.escape(bonus.title)}</option>`).join("");
      const results = this.state.activeTab === "history" ? this.renderHistoryResults() : this.renderMarketResults();
      modal.innerHTML = `<div class="wbv-pc-head"><div class="wbv-pc-title"><strong>Price Check</strong><span class="wbv-pc-sub">Search history and live listings in a cleaner feed view</span></div><button class="wbv-pc-close" type="button" id="wbv-pc-close">Close</button></div><div class="wbv-pc-tabs"><button class="wbv-pc-tab ${this.state.activeTab === "history" ? "active" : ""}" data-tab="history" type="button">History</button><button class="wbv-pc-tab ${this.state.activeTab === "market" ? "active" : ""}" data-tab="market" type="button">Market</button></div><div class="wbv-pc-body"><div class="wbv-pc-grid"><input id="wbv-pc-item-name" placeholder="Item name" value="${this.escape(this.state.filters.itemName)}"><select id="wbv-pc-bonus1"><option value="">Any Bonus 1</option>${options}</select><input id="wbv-pc-bonus1-min" placeholder="Bonus 1 min" value="${this.escape(this.state.filters.bonus1Min)}" ${settings.disableBonusValueFilter ? "disabled" : ""}><input id="wbv-pc-bonus1-max" placeholder="Bonus 1 max" value="${this.escape(this.state.filters.bonus1Max)}" ${settings.disableBonusValueFilter ? "disabled" : ""}><select id="wbv-pc-bonus2"><option value="">Any Bonus 2</option>${options}</select><input id="wbv-pc-bonus2-min" placeholder="Bonus 2 min" value="${this.escape(this.state.filters.bonus2Min)}" ${settings.disableBonusValueFilter ? "disabled" : ""}><input id="wbv-pc-bonus2-max" placeholder="Bonus 2 max" value="${this.escape(this.state.filters.bonus2Max)}" ${settings.disableBonusValueFilter ? "disabled" : ""}><input id="wbv-pc-quality-min" placeholder="Quality min" value="${this.escape(this.state.filters.qualityMin)}" ${settings.disableQualityFilter ? "disabled" : ""}><input id="wbv-pc-quality-max" placeholder="Quality max" value="${this.escape(this.state.filters.qualityMax)}" ${settings.disableQualityFilter ? "disabled" : ""}></div><div class="wbv-pc-actions"><button class="wbv-pc-btn" id="wbv-pc-search" type="button">${this.state.activeTab === "history" ? "Search History" : "Search Market"}</button><div class="wbv-pc-tog"><label><input type="checkbox" id="wbv-pc-disable-bonus" ${settings.disableBonusValueFilter ? "checked" : ""}> Disable bonus values</label><label><input type="checkbox" id="wbv-pc-disable-quality" ${settings.disableQualityFilter ? "checked" : ""}> Disable quality</label></div></div><div class="wbv-pc-stream">${results}</div></div><div class="wbv-pc-foot"><span>${this.state.activeTab === "history" ? `${Number(this.state.total || 0).toLocaleString()} results` : `${Number(this.state.marketTotal || 0).toLocaleString()} listings`}</span>${this.state.activeTab === "history" ? `<div><button class="wbv-pc-small" id="wbv-pc-prev" type="button" ${this.state.offset === 0 ? "disabled" : ""}>Prev</button> <button class="wbv-pc-small" id="wbv-pc-next" type="button" ${(this.state.offset + 20) >= this.state.total ? "disabled" : ""}>Next</button></div>` : `<a href="https://weav3r.dev/" target="_blank" rel="noopener noreferrer" style="color:#adb5bd;">TornW3B</a>`}</div>`;
      $("#wbv-pc-bonus1").val(this.state.filters.bonus1Id);
      $("#wbv-pc-bonus2").val(this.state.filters.bonus2Id);
      modal.querySelector("#wbv-pc-close").onclick = () => this.close();
      modal.querySelectorAll(".wbv-pc-tab").forEach((tab) => {
        tab.onclick = () => { this.state.activeTab = tab.dataset.tab; this.render(); if (this.state.activeTab === "market" && !this.state.marketResults.length && !this.state.marketLoading) this.searchMarket(); };
      });
      modal.querySelector("#wbv-pc-search").onclick = () => { this.syncFiltersFromDom(); if (this.state.activeTab === "history") { this.state.offset = 0; this.searchHistory(); } else { this.searchMarket(); } };
      modal.querySelector("#wbv-pc-disable-bonus").onchange = (event) => { this.saveSettings({ disableBonusValueFilter: event.target.checked }); this.render(); };
      modal.querySelector("#wbv-pc-disable-quality").onchange = (event) => { this.saveSettings({ disableQualityFilter: event.target.checked }); this.render(); };
      const prev = modal.querySelector("#wbv-pc-prev");
      const next = modal.querySelector("#wbv-pc-next");
      if (prev) prev.onclick = () => { this.state.offset = Math.max(0, this.state.offset - 20); this.searchHistory(); };
      if (next) next.onclick = () => { this.state.offset += 20; this.searchHistory(); };
    },
    syncFiltersFromDom() {
      this.state.filters.itemName = $("#wbv-pc-item-name").val().trim();
      this.state.filters.bonus1Id = $("#wbv-pc-bonus1").val();
      this.state.filters.bonus1Min = $("#wbv-pc-bonus1-min").val();
      this.state.filters.bonus1Max = $("#wbv-pc-bonus1-max").val();
      this.state.filters.bonus2Id = $("#wbv-pc-bonus2").val();
      this.state.filters.bonus2Min = $("#wbv-pc-bonus2-min").val();
      this.state.filters.bonus2Max = $("#wbv-pc-bonus2-max").val();
      this.state.filters.qualityMin = $("#wbv-pc-quality-min").val();
      this.state.filters.qualityMax = $("#wbv-pc-quality-max").val();
    },
    buildHistoryBody() {
      const body = { limit: 20, offset: this.state.offset, sort_by: "timestamp", sort_order: "desc" };
      const settings = this.getSettings();
      const filters = this.state.filters;
      if (filters.itemName) body.item_name = filters.itemName;
      if (filters.bonus1Id) {
        body.bonus1_id = parseInt(filters.bonus1Id, 10);
        if (!settings.disableBonusValueFilter) {
          if (filters.bonus1Min) body.bonus1_value_min = parseFloat(filters.bonus1Min);
          if (filters.bonus1Max) body.bonus1_value_max = parseFloat(filters.bonus1Max);
        }
      }
      if (filters.bonus2Id) {
        body.bonus2_id = parseInt(filters.bonus2Id, 10);
        if (!settings.disableBonusValueFilter) {
          if (filters.bonus2Min) body.bonus2_value_min = parseFloat(filters.bonus2Min);
          if (filters.bonus2Max) body.bonus2_value_max = parseFloat(filters.bonus2Max);
        }
      }
      if (!settings.disableQualityFilter) {
        if (filters.qualityMin) body.quality_min = parseFloat(filters.qualityMin);
        if (filters.qualityMax) body.quality_max = parseFloat(filters.qualityMax);
      }
      return body;
    },
    async searchHistory() {
      this.state.loading = true;
      this.state.error = "";
      this.render();
      try {
        const body = this.buildHistoryBody();
        const cacheKey = JSON.stringify(body);
        const cached = this.getCached(cacheKey);
        const data = cached || await this.requestJson({ method: "POST", url: `${this.config.supabaseUrl}/functions/v1/search-auctions`, headers: { "Content-Type": "application/json", apikey: this.config.supabaseAnonKey, Authorization: `Bearer ${this.config.supabaseAnonKey}` }, body: JSON.stringify(body) });
        if (!cached) this.setCached(cacheKey, data);
        this.state.results = data.auctions || [];
        this.state.total = data.total || 0;
      } catch (error) {
        this.state.error = error.message || "Search failed";
        this.state.results = [];
        this.state.total = 0;
      }
      this.state.loading = false;
      this.render();
    },
    buildMarketParams() {
      const params = new URLSearchParams();
      const filters = this.state.filters;
      const settings = this.getSettings();
      if (filters.itemName) params.set(this.state.itemType === "armor" ? "armorPiece" : "weaponName", filters.itemName);
      if (filters.bonus1Id) {
        params.set("bonus1", this.getBonusName(filters.bonus1Id));
        if (!settings.disableBonusValueFilter) {
          if (filters.bonus1Min) params.set("minBonus1Value", filters.bonus1Min);
          if (filters.bonus1Max) params.set("maxBonus1Value", filters.bonus1Max);
        }
      }
      if (filters.bonus2Id) {
        params.set("bonus2", this.getBonusName(filters.bonus2Id));
        if (!settings.disableBonusValueFilter) {
          if (filters.bonus2Min) params.set("minBonus2Value", filters.bonus2Min);
          if (filters.bonus2Max) params.set("maxBonus2Value", filters.bonus2Max);
        }
      }
      if (!settings.disableQualityFilter) {
        if (filters.qualityMin) params.set("minQuality", filters.qualityMin);
        if (filters.qualityMax) params.set("maxQuality", filters.qualityMax);
      }
      params.set("sortField", "price");
      params.set("sortDirection", "asc");
      params.set("tab", this.state.itemType === "armor" ? "armor" : "weapons");
      return params;
    },
    async searchMarket() {
      this.state.marketLoading = true;
      this.state.marketError = "";
      this.render();
      try {
        const data = await this.requestJson({ method: "GET", url: `${this.config.marketApi}?${this.buildMarketParams().toString()}` });
        this.state.marketResults = data.weapons || data.armor || data.items || [];
        this.state.marketTotal = data.total_count || this.state.marketResults.length || 0;
      } catch (error) {
        this.state.marketError = error.message || "Market search failed";
        this.state.marketResults = [];
        this.state.marketTotal = 0;
      }
      this.state.marketLoading = false;
      this.render();
    },
    renderHistoryResults() {
      if (this.state.loading) return "<div>Searching history...</div>";
      if (this.state.error) return `<div class="wbv-pc-error">${this.escape(this.state.error)}</div>`;
      if (!this.state.results.length) return "<div>No matching sales found.</div>";
      return this.state.results.map((item) => {
        const bonuses = (item.bonus_values || []).map((bonus) => `<span class="wbv-pc-bonus">${bonus.bonus_value != null ? `${this.escape(bonus.bonus_value)}% ` : ""}${this.escape(this.getBonusName(bonus.bonus_id))}</span>`).join("");
        return `<div class="wbv-pc-item"><div><strong>${this.escape(item.item_name)}</strong></div><div class="wbv-pc-meta">Quality: ${this.escape(Number(item.stat_quality || 0).toFixed(1))}%${item.stat_damage != null ? ` · DMG: ${this.escape(Number(item.stat_damage).toFixed(1))}` : ""}${item.stat_accuracy != null ? ` · ACC: ${this.escape(Number(item.stat_accuracy).toFixed(1))}` : ""}${item.stat_armor != null ? ` · Armor: ${this.escape(Number(item.stat_armor).toFixed(1))}` : ""}</div>${bonuses ? `<div>${bonuses}</div>` : ""}<div class="wbv-pc-meta">${this.formatPrice(item.price)} · ${this.formatDate(item.timestamp)}</div></div>`;
      }).join("");
    },
    renderMarketResults() {
      if (this.state.marketLoading) return "<div>Searching market...</div>";
      if (this.state.marketError) return `<div class="wbv-pc-error">${this.escape(this.state.marketError)}</div>`;
      if (!this.state.marketResults.length) return "<div>No similar items found on the market.</div>";
      return this.state.marketResults.map((item) => {
        const entries = item.bonuses ? Object.values(item.bonuses) : [];
        const bonuses = entries.map((bonus) => `<span class="wbv-pc-bonus">${bonus.value ? `${this.escape(bonus.value)}% ` : ""}${this.escape(bonus.bonus)}</span>`).join("");
        return `<div class="wbv-pc-item"><div><strong>${this.escape(item.itemName)}</strong></div><div class="wbv-pc-meta">Quality: ${this.escape(Number(item.quality || 0).toFixed(1))}%${item.damage != null ? ` · DMG: ${this.escape(Number(item.damage).toFixed(1))}` : ""}${item.accuracy != null ? ` · ACC: ${this.escape(Number(item.accuracy).toFixed(1))}` : ""}${item.rarity ? ` · ${this.escape(item.rarity)}` : ""}</div>${bonuses ? `<div>${bonuses}</div>` : ""}<div class="wbv-pc-meta">${this.formatPrice(item.price)}</div></div>`;
      }).join("");
    },
    getAvatarText(name) {
      const text = String(name || "?").trim();
      const words = text.split(/\s+/).filter(Boolean);
      if (words.length >= 2) return this.escape(`${words[0][0]}${words[1][0]}`.toUpperCase());
      return this.escape(text.slice(0, 2).toUpperCase() || "?");
    },
    renderHistoryResults() {
      if (this.state.loading) return '<div class="wbv-pc-loading">Searching history...</div>';
      if (this.state.error) return `<div class="wbv-pc-error">${this.escape(this.state.error)}</div>`;
      if (!this.state.results.length) return '<div class="wbv-pc-empty">No matching sales found.</div>';
      return this.state.results.map((item) => {
        const bonuses = (item.bonus_values || []).map((bonus) => `<span class="wbv-pc-bonus">${bonus.bonus_value != null ? `${this.escape(bonus.bonus_value)}% ` : ""}${this.escape(this.getBonusName(bonus.bonus_id))}</span>`).join("");
        return `<div class="wbv-pc-item"><div class="wbv-pc-avatar">${this.getAvatarText(item.item_name)}</div><div class="wbv-pc-content"><div class="wbv-pc-line"><span class="wbv-pc-name">${this.escape(item.item_name)}</span><span class="wbv-pc-time">${this.escape(this.formatDate(item.timestamp))}</span></div><div class="wbv-pc-meta">Quality ${this.escape(Number(item.stat_quality || 0).toFixed(1))}%${item.stat_damage != null ? ` | DMG ${this.escape(Number(item.stat_damage).toFixed(1))}` : ""}${item.stat_accuracy != null ? ` | ACC ${this.escape(Number(item.stat_accuracy).toFixed(1))}` : ""}${item.stat_armor != null ? ` | Armor ${this.escape(Number(item.stat_armor).toFixed(1))}` : ""}</div>${bonuses ? `<div class="wbv-pc-badges">${bonuses}</div>` : ""}<div class="wbv-pc-price">${this.formatPrice(item.price)}</div></div></div>`;
      }).join("");
    },
    renderMarketResults() {
      if (this.state.marketLoading) return '<div class="wbv-pc-loading">Searching market...</div>';
      if (this.state.marketError) return `<div class="wbv-pc-error">${this.escape(this.state.marketError)}</div>`;
      if (!this.state.marketResults.length) return '<div class="wbv-pc-empty">No similar items found on the market.</div>';
      return this.state.marketResults.map((item) => {
        const entries = item.bonuses ? Object.values(item.bonuses) : [];
        const bonuses = entries.map((bonus) => `<span class="wbv-pc-bonus">${bonus.value ? `${this.escape(bonus.value)}% ` : ""}${this.escape(bonus.bonus)}</span>`).join("");
        const marketMeta = item.playerName ? `Bazaar | ${this.escape(item.playerName)}` : "Item Market";
        return `<div class="wbv-pc-item"><div class="wbv-pc-avatar">${this.getAvatarText(item.itemName)}</div><div class="wbv-pc-content"><div class="wbv-pc-line"><span class="wbv-pc-name">${this.escape(item.itemName)}</span><span class="wbv-pc-time">${marketMeta}</span></div><div class="wbv-pc-meta">Quality ${this.escape(Number(item.quality || 0).toFixed(1))}%${item.damage != null ? ` | DMG ${this.escape(Number(item.damage).toFixed(1))}` : ""}${item.accuracy != null ? ` | ACC ${this.escape(Number(item.accuracy).toFixed(1))}` : ""}${item.rarity ? ` | ${this.escape(item.rarity)}` : ""}</div>${bonuses ? `<div class="wbv-pc-badges">${bonuses}</div>` : ""}<div class="wbv-pc-price">${this.formatPrice(item.price)}</div></div></div>`;
      }).join("");
    },
    init() {
      this.bonusData.forEach((bonus) => {
        this.bonusMap[bonus.id] = bonus.title;
        this.bonusMap[bonus.title.toLowerCase()] = bonus.id;
        this.bonusMap[bonus.title.toLowerCase().replace(/[\s-]/g, "")] = bonus.id;
      });
      this.ensureModal();
    },
  };

  const Modules = {
    async injectCommon(item, armouryId, updater, opts = {}) {
      if (!armouryId || item.find("div.item-detail").length || Util.isLocked(item)) return null;
      Util.markPending(item);
      Util.updateCacheRecord(armouryId, item, updater);
      const itemDetail = await Api.getItemDetails(armouryId);
      if (!itemDetail || item.find("div.item-detail").length) {
        Util.clearMark(item);
        return null;
      }
      return UI.createItemDetailsNode(itemDetail, opts.isBazaar || false);
    },
    imarket: {
      async updateSearch(item) {
        Util.setContext({ inItemList: false });
        if (item.find("div.item-detail").length) return;
        const detail = item.find("li.item-t");
        const itemId = parseInt(detail.attr("itemid"), 10);
        if (!itemId || !Util.isSupportedItem(itemId)) return;
        const armouryId = detail.attr("data-armoury");
        const node = await Modules.injectCommon(item, armouryId, Modules.imarket.updateSearch);
        if (!node) return;
        let appendTarget;
        if (Util.isMobile()) {
          appendTarget = item.find("li.cost");
          if (!appendTarget.length) return;
          node.css({ marginLeft: "30px" });
        } else {
          appendTarget = item.find("li.item-t span.t-hide");
          if (!appendTarget.length) return;
          const setPosition = () => node.css({ marginLeft: "60px", position: "absolute", left: `${item.offset().left + item.width() - 60}px`, background: "#F2F2F2", width: "180px" });
          setPosition();
          window.addEventListener("resize", setPosition);
        }
        appendTarget.css({ display: "flex", alignItems: "center" });
        Util.appendUnique(appendTarget, node, item);
        PriceChecker.attachButton(appendTarget, Store.get(armouryId), Util.getItemType(itemId) || "weapon");
      },
      async updateDirect(item) {
        Util.setContext({ inItemList: false });
        if (item.find("div.item-detail").length) return;
        const link = item.find("a.view-link");
        const itemId = parseInt(link.attr("itemid"), 10);
        if (!itemId || !Util.isSupportedItem(itemId)) return;
        const armouryId = link.attr("data-armoury");
        const isBazaar = item.parent().attr("class") === "private-bazaar" && Util.isMobile();
        const node = await Modules.injectCommon(item, armouryId, Modules.imarket.updateDirect, { isBazaar });
        if (!node) return;
        if (Util.isMobile()) {
          const appendTarget = item.find("li.cost");
          if (!appendTarget.length) return;
          node.css({ marginLeft: "3px" });
          appendTarget.css({ display: "flex", alignItems: "center" });
          Util.appendUnique(appendTarget, node, item);
          PriceChecker.attachButton(appendTarget, Store.get(armouryId), Util.getItemType(itemId) || "weapon");
          return;
        }
        const offsetLeft = item.offset().left + item.width();
        if (offsetLeft <= 0) return;
        const setPosition = () => node.css({ marginLeft: "10px", position: "absolute", left: `${offsetLeft - 5}px`, background: "#FFF", width: "180px", zIndex: 100001, height: `${item.height()}px`, top: `${item.offset().top}px`, fontSize: "12px", boxSizing: "border-box", paddingLeft: "15px" });
        setPosition();
        window.addEventListener("resize", setPosition);
        App.state.floatingNodes.push(node);
        $("body").append(node);
        Util.markDone(item);
        PriceChecker.attachButton(item.find("a.view-link").parent(), Store.get(armouryId), Util.getItemType(itemId) || "weapon");
      },
      init() {
        const root = $("div#item-market-main-wrap")[0] || document.querySelector(".shop-market-page") || document.querySelector("[class*='item-market']") || document.querySelector("div.content-wrapper");
        if (!root) return;
        const scan = (scope) => {
          $(scope).find("ul.items ul.item, ul.items > li > ul.item").each(function () { Modules.imarket.updateDirect($(this)); });
          $(scope).find(".shop-market-page .show-item-info, .show-item-info").each(function () { Modules.imarket.updateSearch($(this)); });
        };
        scan(root);
        const warmScan = setInterval(() => {
          if (!document.body.contains(root)) {
            clearInterval(warmScan);
            return;
          }
          scan(root);
        }, 1200);
        App.state.intervals.push(warmScan);
        Page.observe(root, (mutations) => {
          for (const mut of mutations) for (const node of mut.addedNodes) {
            if (node.tagName === "UL" && $(node).hasClass("items")) {
              Util.clearFloatingNodes();
              $(node).find("ul.item").each(function () { Modules.imarket.updateDirect($(this)); });
            } else if ($(node).hasClass("shop-market-page")) {
              $(node).find(".show-item-info").each(function () { Modules.imarket.updateSearch($(this)); });
            } else {
              scan(node);
            }
          }
        });
      },
    },
    item: {
      async update(item) {
        Util.setContext({ inItemList: true });
        if (item.find("div.item-detail").length) return;
        const itemId = parseInt(item.attr("data-item"), 10);
        const armouryId = item.attr("data-armoryid") || item.attr("data-id");
        if (!itemId || !Util.isSupportedItem(itemId)) return;
        const node = await Modules.injectCommon(item, armouryId, Modules.item.update);
        if (!node) return;
        let appendTarget = item.find("span.name-wrap span.name");
        if (!appendTarget.length) appendTarget = item.find("li.desc");
        if (!appendTarget.length) return;
        appendTarget.css({ display: "flex", alignItems: "center" });
        Util.appendUnique(appendTarget, node, item);
        PriceChecker.attachButton(appendTarget, Store.get(armouryId), Util.getItemType(itemId) || "weapon");
      },
      init() {
        const root = $("div.items-wrap")[0];
        if (!root) return;
        $(root).find("li[data-armoryid], li[data-id][data-item]").each(function () { Modules.item.update($(this)); });
        Page.observe(root, (mutations) => {
          for (const mut of mutations) for (const node of mut.addedNodes) if (node.tagName === "LI" && $(node).attr("data-armoryid")) Modules.item.update($(node));
        });
      },
    },
    factionArmoury: {
      isMatch() { return location.href.includes("factions.php") && location.href.includes("armoury"); },
      findFirstNumber(value) {
        if (!value) return null;
        const match = String(value).match(/(\d{3,})/);
        return match ? match[1] : null;
      },
      isSupportedRow(item) {
        const rowText = item.text().toLowerCase();
        return rowText.includes("primary") || rowText.includes("secondary") || rowText.includes("melee") || rowText.includes("armor");
      },
      getRefs(item) {
        const itemSource = item.find("[data-itemid], [data-itemId], [itemid], [data-item], div.img-wrap").first();
        const itemId = parseInt(
          (itemSource.length
            ? (itemSource.attr("data-itemid") || itemSource.attr("data-itemId") || itemSource.attr("itemid") || itemSource.attr("data-item"))
            : null),
          10
        );

        let armouryId =
          item.attr("data-armoryid") ||
          item.attr("data-armoryId") ||
          item.attr("data-armouryid") ||
          item.attr("armoryid") ||
          item.attr("armouryid") ||
          null;

        if (!armouryId) {
          item.find("*").each(function () {
            if (armouryId) return false;
            const el = $(this);
            armouryId =
              el.attr("data-armoryid") ||
              el.attr("data-armoryId") ||
              el.attr("data-armouryid") ||
              el.attr("armoryid") ||
              el.attr("armouryid") ||
              el.attr("data-id") ||
              el.attr("data-key") ||
              el.attr("data-reactid") ||
              Modules.factionArmoury.findFirstNumber(el.attr("href")) ||
              Modules.factionArmoury.findFirstNumber(el.attr("onclick")) ||
              null;
          });
        }

        if (!itemId || !armouryId) return null;
        return { itemId, armouryId };
      },
      async update(item) {
        Util.setContext({ inFactionArmoury: true });
        if (item.attr("data-wbv-state") === "pending") return;
        const refs = Modules.factionArmoury.getRefs(item);
        if (!refs) return;
        const { itemId, armouryId } = refs;
        const supported = itemId ? Util.isSupportedItem(itemId) : Modules.factionArmoury.isSupportedRow(item);
        if (!supported || !armouryId) return;
        const node = await Modules.injectCommon(item, armouryId, Modules.factionArmoury.update);
        if (!node) return;
        const itemDetail = Store.get(armouryId);
        if (!Util.isMobile()) {
          node.css({ marginLeft: "0" });
          if (!itemDetail?.bonuses) node.addClass("armory-bg");
          Util.placeFloatingNode(item, node);
          PriceChecker.attachButton(item, itemDetail, itemDetail?.armor !== undefined && itemDetail?.armor !== null ? "armor" : "weapon");
          return;
        }
        let appendTarget = item.find("td").eq(1);
        if (!appendTarget.length) appendTarget = item.find("div.name").first();
        if (!appendTarget.length) appendTarget = item.find("td").eq(0);
        if (!appendTarget.length) appendTarget = item.find("a").first().parent();
        if (!appendTarget.length) {
          Util.clearMark(item);
          return;
        }
        appendTarget.css({ display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap" });
        Util.appendUnique(appendTarget, node, item);
        PriceChecker.attachButton(appendTarget, itemDetail, itemDetail?.armor !== undefined && itemDetail?.armor !== null ? "armor" : "weapon");
      },
      scan(root) { $(root).find("ul.item-list li, table tr, tbody tr, div[class*='row']").each(function () { Modules.factionArmoury.update($(this)); }); },
      heal(root) {
        $(root).find("ul.item-list li, table tr, tbody tr, div[class*='row']").each(function () {
          const row = $(this);
          if (Util.isMobile()) {
            if (!row.find("div.item-detail").length && row.attr("data-wbv-state") === "done") {
              Util.clearMark(row);
            }
          } else if (row.attr("data-wbv-state") === "done") {
            Util.clearMark(row);
          }
          Modules.factionArmoury.update(row);
        });
      },
      init() {
        UI.addStyles();
        const attach = (root) => {
          if (!root || root.dataset.wbvObserved === "1") return;
          root.dataset.wbvObserved = "1";
          Modules.factionArmoury.scan(root);
          const healInterval = setInterval(() => {
            if (!document.body.contains(root)) {
              clearInterval(healInterval);
              return;
            }
            Modules.factionArmoury.heal(root);
          }, 700);
          App.state.intervals.push(healInterval);
          Page.observe(root, (mutations) => {
            for (const mut of mutations) for (const node of mut.addedNodes) {
              const $node = $(node);
              if ($node.is("ul.item-list")) {
                $node.find("li").each(function () { Modules.factionArmoury.update($(this)); });
              } else if ($node.is("tr")) {
                Modules.factionArmoury.update($node);
              } else if ($node.is("li")) {
                Modules.factionArmoury.update($node);
              } else {
                const parentRow = $node.closest("tr, li");
                if (parentRow.length) {
                  Modules.factionArmoury.update(parentRow);
                }
                $node.find("ul.item-list li, table tr, tbody tr, div[class*='row']").each(function () { Modules.factionArmoury.update($(this)); });
              }
            }
          });
        };
        const tryAttach = () => {
          const root = $("div#faction-armoury")[0] || document.querySelector("[id='faction-armoury']") || document.querySelector("div.content-wrapper");
          if (root) { attach(root); return true; }
          return false;
        };
        if (tryAttach()) return;
        const wait = setInterval(() => { if (tryAttach()) clearInterval(wait); }, 500);
      },
    },
    bigAlGunShop: {
      init() {
        const root = $("div.sell-list-wrap")[0];
        if (!root) return;
        Page.observe(root, (mutations) => {
          for (const mut of mutations) for (const node of mut.addedNodes) if (node.tagName === "LI" && $(node).attr("data-id")) Modules.item.update($(node));
        });
        $("div.sell-list-wrap li[data-item]:not([data-id=''])").each(function () { Modules.item.update($(this)); });
      },
    },
    bazaarManage: {
      async update(item) {
        Util.setContext({ inItemList: true });
        if (item.find("div.item-detail").length) return;
        const itemName = Util.safeText(item.find("span.t-overflow"));
        const itemId = Data.itemNameToId[itemName];
        if (!itemId || !Util.isSupportedItem(itemId)) return;
        const reactId = item.attr("data-reactid") || "";
        const match = /(\$[\d]+)/.exec(reactId);
        if (!match) return;
        const armouryId = match[0].replace("$", "");
        const node = await Modules.injectCommon(item, armouryId, Modules.bazaarManage.update);
        if (!node) return;
        const appendTarget = item.find("div.name-wrap");
        if (!appendTarget.length) return;
        appendTarget.css({ display: "flex", alignItems: "center" });
        Util.appendUnique(appendTarget, node, item);
        PriceChecker.attachButton(appendTarget, Store.get(armouryId), Util.getItemType(itemId) || "weapon");
      },
      init(rootSelector) {
        const root = $(rootSelector)[0];
        if (!root) return;
        $(root).find("li.clearfix[data-reactid]").each(function () { Modules.bazaarManage.update($(this)); });
        Page.observe(root, (mutations) => {
          for (const mut of mutations) for (const node of mut.addedNodes) if (node.tagName === "LI" && $(node).hasClass("clearfix") && $(node).attr("data-reactid")) Modules.bazaarManage.update($(node));
        });
      },
    },
    displayCase: {
      async update(item) {
        Util.setContext({ inCabinet: true });
        if (item.find(".item-detail").length || item.find(".top-bonuses").length) return;
        const hover = item.find("div.item-hover");
        const itemId = parseInt(hover.attr("itemid"), 10);
        if (!itemId || !Util.getItemType(itemId)) return;
        const armouryId = hover.attr("armouryid");
        if (!armouryId || armouryId === "0") return;
        const node = await Modules.injectCommon(item, armouryId, Modules.displayCase.update);
        if (!node) return;
        node.css({ position: "absolute", left: "0", top: "35px" });
        node.addClass("armory-bg");
        node.find("div.message").css({ height: "15px", lineHeight: "15px", padding: "0 5px" });
        item.append(node);
        PriceChecker.attachButton(item, Store.get(armouryId), Util.getItemType(itemId) || "weapon");
      },
      init() {
        UI.addStyles();
        const root = $("div.content-wrapper")[0];
        if (!root) return;
        $(root).find("ul.display-cabinet li").each(function () { Modules.displayCase.update($(this)); });
        Page.observe(root, (mutations) => {
          for (const mut of mutations) for (const node of mut.addedNodes) if ($(node).hasClass("display-main-page")) $(node).find("ul.display-cabinet li").each(function () { Modules.displayCase.update($(this)); });
        });
      },
    },
    userBazaar: {
      handleBonus(title) { const match = /<b>(.*?)<\/b>.*?(\d+)(%)?/.exec(title || ""); return match ? [match[1], `${match[2]}${match[3] || ""}`] : null; },
      updateItem(row, itemIdx) {
        if (!row.length) return;
        const item = App.state.bazaarCache[String(itemIdx)];
        if (!item) return;
        const target = row.find(`div[class^="imgBar"]`);
        if (!target.length || row.find("div.top-bonuses").length) return;
        const { damage, accuracy, quality, arm, coverage, bonuses, category, rarity } = item;
        if (!Data.filterTypes.has(category)) return;
        const top = $(`<div class="top-bonuses"></div>`);
        const bottom = $(`<div class="bottom-bonuses"></div>`);
        if (bonuses && bonuses[0] && bonuses[0].title) {
          for (const bonus of bonuses) {
            if (!bonus.title) continue;
            const info = Modules.userBazaar.handleBonus(bonus.title);
            if (!info) continue;
            const box = $(`<div class="bonus-attachment"></div>`);
            const inner = $(`<div class="message icon-text border-round" title="${bonus.title}"><i class="${bonus.class}" title="${bonus.title}"></i> ${info[1]}</div>`);
            inner.css({ color: UI.getQualityColor(quality, rarity) });
            box.append(inner);
            top.append(box);
          }
        } else {
          const box = $(`<div class="bonus-attachment"></div>`);
          const val = $(`<font class="label-value t-overflow">Q:${quality}%</font>`);
          val.css({ color: UI.getQualityColor(quality, rarity) });
          box.append(val);
          top.append(box);
        }
        if (arm !== "0") {
          const armorBox = $(`<div class="bonus-attachment"></div>`);
          armorBox.append($(`<i class="bonus-attachment-item-defence-bonus"></i>`), $(`<span class="label-value t-overflow">${arm}</span>`));
          bottom.append(armorBox);
          const coverageBox = $(`<div class="bonus-attachment"></div>`);
          let coverageData = {};
          try { coverageData = JSON.parse(coverage); } catch {}
          let coverageHtml = "";
          for (const bodyPart in coverageData) coverageHtml += `<b>${bodyPart}:</b> ${coverageData[bodyPart]}%<br>`;
          coverageBox.append($(`<i class="bonus-attachment-item-coverage-bonus"></i>`), $(`<span class="label-value t-overflow" title='${coverageHtml}'>${coverageData["Full Body Coverage"] || ""}%</span>`));
          bottom.append(coverageBox);
        } else {
          const damageBox = $(`<div class="bonus-attachment"></div>`);
          damageBox.append($(`<i class="bonus-attachment-item-damage-bonus"></i>`), $(`<span class="label-value t-overflow">${damage}</span>`));
          const accBox = $(`<div class="bonus-attachment"></div>`);
          accBox.append($(`<i class="bonus-attachment-item-accuracy-bonus"></i>`), $(`<span class="label-value t-overflow">${accuracy}</span>`));
          bottom.append(damageBox, accBox);
        }
        target.append(top, bottom);
        row.attr("data-idx", itemIdx);
      },
      updateVisibleRows() {
        if (App.state.bazaarUpdating) return;
        App.state.bazaarUpdating = true;
        try {
          const children = $("[class^='rowItems___']").children();
          if (!children.length) return;
          const firstUpdated = children.filter("[data-idx]").eq(0);
          const lastUpdated = children.filter("[data-idx]").last();
          let first = firstUpdated.attr("data-idx");
          let last = lastUpdated.attr("data-idx");
          let current;
          let currentIdx = 0;
          if (!first && !last) {
            const countLimit = 6;
            while (true) {
              let mismatch = false;
              for (let i = 0; i < countLimit; i++) {
                const cached = App.state.bazaarCache[String(currentIdx + i)];
                current = children.eq(i);
                if (!current.length || !cached) break;
                const currentPrice = current.find("p[class*=price]").text().split("  ")[0].replaceAll(",", "").replace("$", "");
                const currentName = current.find("p[class*=name]").text().trim();
                if (String(currentPrice) !== String(cached.price) || String(currentName) !== String(cached.name)) { mismatch = true; break; }
              }
              if (!mismatch) break;
              currentIdx += 1;
              if (currentIdx > 50) break;
            }
            children.each(function () { Modules.userBazaar.updateItem($(this), currentIdx++); });
          } else {
            if (first) {
              currentIdx = children.index(firstUpdated);
              first = parseInt(first, 10);
              while (first && --first >= 0 && --currentIdx >= 0) Modules.userBazaar.updateItem(children.eq(currentIdx), first);
            }
            if (last) {
              currentIdx = children.index(lastUpdated);
              last = parseInt(last, 10);
              while (last && ++last < Object.keys(App.state.bazaarCache).length && ++currentIdx < children.length) Modules.userBazaar.updateItem(children.eq(currentIdx), last);
            }
          }
        } finally { App.state.bazaarUpdating = false; }
      },
      handleResponse(params, resp) {
        if (!resp?.list) return;
        const start = parseInt(params.get("start") || "0", 10);
        const list = resp.list;
        for (let i = 0; i < list.length; i++) App.state.bazaarCache[String(start + i)] = list[i];
        const interval = setInterval(() => {
          if ($(`div[class^="imgBar"]`).length) {
            clearInterval(interval);
            setTimeout(() => Modules.userBazaar.updateVisibleRows(), 0);
          }
        }, 300);
      },
      init() {
        UI.addStyles();
        Page.interceptFetch();
        const waitForGrid = setInterval(() => {
          if ($(`div[class^="imgBar"]`).length) {
            clearInterval(waitForGrid);
            const root = $("div.ReactVirtualized__Grid__innerScrollContainer")[0];
            if (!root) return;
            Page.observe(root, (mutations) => {
              for (const mut of mutations) for (const node of mut.addedNodes) if (node.classList && node.classList.contains("row___LkdFI")) Modules.userBazaar.updateVisibleRows();
            });
          }
        }, 300);
      },
    },
  };

  const Router = {
    init() {
      const href = location.href;
      const isItemMarket = href.includes("imarket.php") || href.includes("page.php?sid=ItemMarket");
      let needXML = false;
      if (href === "https://www.torn.com/bazaar.php#/add") { needXML = true; Modules.bazaarManage.init("div#bazaarRoot"); }
      else if (href === "https://www.torn.com/imarket.php#/p=addl") { needXML = true; Modules.bazaarManage.init("div#item-market-main-wrap"); }
      else if (isItemMarket) { needXML = true; Modules.imarket.init(); }
      else if (href.includes("item.php")) { needXML = true; Modules.item.init(); }
      else if (Modules.factionArmoury.isMatch()) { needXML = true; Modules.factionArmoury.init(); }
      else if (href.includes("bigalgunshop.php")) { needXML = true; Modules.bigAlGunShop.init(); }
      else if (href.includes("displaycase.php")) { needXML = true; Modules.displayCase.init(); }
      else if (href.startsWith("https://www.torn.com/bazaar.php?userId=") && Config.value.ENABLE_BAZAAR_UPDATE) { needXML = true; Modules.userBazaar.init(); }
      if (needXML) Page.interceptXML();
    },
  };

  async function init() {
    Config.load();
    await Store.init();
    PriceChecker.init();
    Util.onHashChange();
    Router.init();
    console.log("%cWeapon%c Bonus %cView starts.", "font-size:30px;font-weight:600;color:#00A9F9;", "font-size:30px;font-weight:600;color:#000;", "font-size:30px;");
  }

  init();
})();