Torn Loadout Share

Shows shared defender loadouts on Torn attack pages. Firebase backend, no registration.

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

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

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

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

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

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

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

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

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

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name         Torn Loadout Share
// @namespace    loadout
// @version      1.0.2
// @description  Shows shared defender loadouts on Torn attack pages. Firebase backend, no registration.
// @license      MIT
// @supportURL   https://greasyfork.org/en/scripts/570121/feedback
// @homepageURL  https://greasyfork.org/en/scripts/570121-torn-loadout-share
// @match        https://www.torn.com/loader.php?sid=attack&user2ID=*
// @connect      *.firebasedatabase.app
// @run-at       document-start
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// ==/UserScript==

(function () {
    "use strict";

    const W = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
    const IS_PDA = !!(typeof W.flutter_inappwebview !== "undefined" && W.flutter_inappwebview?.callHandler);
    const LOG_LEVEL = 0;
    function dbg(...args) { if (LOG_LEVEL >= 2 && W.console?.log) W.console.log("[Loadout]", ...args); }
    const BONUS_KEY_FIX = { hazarfouse: "hazardous" }; // Torn CSS class typos
    const FIREBASE_FETCH_TIMEOUT_MS = 5000;
    const CONFIG = { firebaseUrl: "https://torn-loadout-share-default-rtdb.europe-west1.firebasedatabase.app" };
    const WEAPON_SLOTS = [1, 2, 3, 5];
    const ARMOR_SLOTS = [8, 7, 9, 6, 4, 10];
    const SLOT_MAPPINGS = [
        { slot: 1, label: "Primary", def: "#defender_Primary", atk: "#attacker_Primary", fallback: "#weapon_main" },
        { slot: 2, label: "Secondary", def: "#defender_Secondary", atk: "#attacker_Secondary", fallback: "#weapon_second" },
        { slot: 3, label: "Melee", def: "#defender_Melee", atk: "#attacker_Melee", fallback: "#weapon_melee" },
        { slot: 5, label: "Temporary", def: "#defender_Temporary", atk: "#attacker_Temporary", fallback: "#weapon_temp" },
    ];
    const DOM_POLL_MS = 100;

    const STATE = { uploaded: false, attackData: null, loadoutRendered: false };

    function getCacheKey(playerId, targetId) {
        return `${playerId}_${targetId}`;
    }

    function validateLoadout(loadout) {
        if (!loadout || typeof loadout !== "object") return false;
        const validSlots = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
        for (const [k, item] of Object.entries(loadout)) {
            const slot = Number(k);
            if (!validSlots.has(slot) || !item || typeof item !== "object") return false;
            const id = item.item_id ?? item.ID ?? item.id;
            if (id == null || !Number.isInteger(Number(id)) || Number(id) < 1 || Number(id) > 999999) return false;
            const d = Number(item.damage ?? item.Damage ?? 0);
            const a = Number(item.accuracy ?? item.Accuracy ?? 0);
            if (!Number.isFinite(d) || d < 0 || d > 1e5) return false;
            if (!Number.isFinite(a) || a < 0 || a > 1e5) return false;
            if (item.mods != null && !Array.isArray(item.mods)) return false;
            if (item.bonuses != null && !Array.isArray(item.bonuses)) return false;
        }
        return Object.keys(loadout).length > 0;
    }

    function normalizeLoadout(loadout) {
        if (!loadout || typeof loadout !== "object") return loadout;
        for (const item of Object.values(loadout)) {
            if (!item || typeof item !== "object") continue;
            if (typeof item.mods === "string") {
                try { item.mods = JSON.parse(item.mods); } catch { item.mods = []; }
            }
            const b = item.bonuses ?? item.Bonuses ?? item.bonus;
            item.bonuses = parseBonuses(b);
            if (!Array.isArray(item.mods)) item.mods = [];
        }
        return loadout;
    }

    function firebaseRequest(method, path, body) {
        const base = CONFIG.firebaseUrl;
        if (!base) { dbg("firebaseRequest: no base URL"); return Promise.resolve(null); }
        return new Promise((resolve) => {
            const url = `${base}/${path}.json`;
            const opts = { method, url };
            if (body != null && method !== "GET") opts.data = JSON.stringify(body);
            let settled = false;
            const timer = W.setTimeout(() => {
                if (settled) return;
                settled = true;
                reportError("firebase_request", "Timeout", { method, path });
                resolve(null);
            }, FIREBASE_FETCH_TIMEOUT_MS);
            const done = (v) => {
                if (settled) return;
                settled = true;
                W.clearTimeout(timer);
                resolve(v);
            };
            try {
                if (typeof GM_xmlhttpRequest !== "function") {
                    dbg("firebaseRequest: GM_xmlhttpRequest not available");
                    done(null);
                    return;
                }
                GM_xmlhttpRequest({
                    ...opts,
                    timeout: FIREBASE_FETCH_TIMEOUT_MS,
                    anonymous: true,
                    onload: (r) => {
                        if (r.status >= 200 && r.status < 300) {
                            const text = r.responseText ?? "";
                            try { done(text ? JSON.parse(text) : null); } catch { done(null); }
                        } else {
                            dbg("firebaseRequest: status", r.status, path);
                            reportError("firebase_request", `HTTP ${r.status}`, { method, path, status: r.status, responsePreview: (r.responseText ?? "").slice(0, 200) });
                            done(null);
                        }
                    },
                    onerror: (e) => {
                        dbg("firebaseRequest: onerror", path, e);
                        reportError("firebase_request", "Network error", { method, path, errorType: e?.type ?? "unknown" });
                        done(null);
                    },
                });
            } catch (e) {
                dbg("firebaseRequest: exception", path, e);
                reportError("firebase_request", e, { method, path });
                done(null);
            }
        });
    }

    function reportError(context, error, extra = {}) {
        try {
            const user2Match = W.location?.href?.match(/user2ID=(\d+)/);
            const payload = {
                page: W.location?.href ?? "",
                path: (W.location?.pathname ?? "") + (W.location?.search ?? ""),
                user2ID: user2Match ? user2Match[1] : null,
                playerId: STATE.attackData?.attackerUser?.userID ?? extra.playerId ?? null,
                targetId: STATE.attackData?.defenderUser?.userID ?? extra.targetId ?? null,
                context,
                error: typeof error === "string" ? error : (error?.message ?? String(error)),
                stack: error?.stack ?? null,
                timestamp: Date.now(),
                ...extra,
            };
            firebaseRequest("POST", "errors", payload);
        } catch (_) { /* avoid recursive failure */ }
    }

    async function putToFirebase(playerId, targetId, raw) {
        const key = getCacheKey(playerId, targetId);
        const payload = { raw, timestamp: Date.now(), playerId, targetId };
        const r1 = await firebaseRequest("PUT", `loadouts/${key}`, payload);
        const r2 = await firebaseRequest("PUT", `loadouts/by_target/${targetId}`, payload);
        if (r1 == null || r2 == null) {
            reportError("upload", "Firebase PUT returned null", { playerId, targetId });
        }
    }

    function parseLoadoutSlots(loadoutNode) {
        if (!loadoutNode || typeof loadoutNode !== "object") return {};
        const slots = {};
        for (const [k, v] of Object.entries(loadoutNode)) {
            if (k === "_") continue;
            if (typeof v === "string") {
                try { slots[k] = JSON.parse(v); } catch { /* skip */ }
            } else if (v && typeof v === "object" && !v["#"]) {
                slots[k] = v;
            }
        }
        return slots;
    }

    function parsePayloadToLoadout(data) {
        if (!data || typeof data !== "object") return null;
        const raw = data.raw;
        if (raw && typeof raw === "object") {
            const defenderItems = raw.defenderItems ?? raw.attackData?.defenderItems;
            const loadout = parseDefenderItemsToLoadout(defenderItems);
            if (loadout && validateLoadout(loadout)) {
                normalizeLoadout(loadout);
                return loadout;
            }
        }
        const lj = data.loadoutJson;
        if (typeof lj === "string") {
            try {
                const parsed = JSON.parse(lj);
                const loadout = parseLoadoutSlots(parsed);
                if (loadout && Object.keys(loadout).length > 0) return loadout;
            } catch {}
        }
        if (lj && typeof lj === "object" && !lj["#"]) {
            const loadout = parseLoadoutSlots(lj);
            if (loadout && Object.keys(loadout).length > 0) return loadout;
        }
        return null;
    }

    async function getFromFirebase(playerId, targetId) {
        const key = getCacheKey(playerId, targetId);
        dbg("getFromFirebase: player_target", key);
        const data = await firebaseRequest("GET", `loadouts/${key}`);
        const loadout = parsePayloadToLoadout(data);
        if (loadout) return { loadout, submittedBy: null };
        if (data && typeof data === "object" && (data.raw || data.loadoutJson)) {
            reportError("parse", "parsePayloadToLoadout returned null for valid-looking data", { playerId, targetId, key, hasRaw: !!data.raw, hasLoadoutJson: !!data.loadoutJson });
        }
        return null;
    }

    async function getFromFirebaseByTarget(targetId) {
        dbg("getFromFirebaseByTarget:", targetId);
        const data = await firebaseRequest("GET", `loadouts/by_target/${targetId}`);
        const loadout = parsePayloadToLoadout(data);
        if (loadout) {
            const id = data?.playerId;
            const submittedBy = id != null && Number.isInteger(Number(id)) && Number(id) > 0 ? String(id) : null;
            return { loadout, submittedBy };
        }
        if (data && typeof data === "object" && (data.raw || data.loadoutJson)) {
            reportError("parse", "parsePayloadToLoadout returned null for valid-looking data (by_target)", { targetId, hasRaw: !!data.raw, hasLoadoutJson: !!data.loadoutJson });
        }
        return null;
    }

    function parseJson(text) {
        try { return JSON.parse(text); } catch { return null; }
    }

    function whenVisible(fn) {
        if (W.document.visibilityState === "visible") { fn(); return; }
        const handler = () => {
            if (W.document.visibilityState !== "visible") return;
            W.document.removeEventListener("visibilitychange", handler);
            fn();
        };
        W.document.addEventListener("visibilitychange", handler);
    }

    function escapeHtml(v) {
        return String(v ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
    }

    function formatFixed2(v) {
        const n = Number(v);
        return Number.isFinite(n) ? n.toFixed(2) : "-";
    }

    function queryFirst(root, selectors) {
        for (const s of selectors) {
            try {
                const n = root.querySelector(s);
                if (n) return n;
            } catch {}
        }
        return null;
    }

    function getItemFromSlot(slot) {
        if (!slot) return null;
        const item = slot?.item?.[0] ?? slot?.item;
        if (!item) return null;
        const id = item.ID ?? item.id ?? item.item_id;
        if (id == null || id === "") return null;
        return item;
    }

    function extractBonusesFromSlot(slot, item) {
        const from = item?.currentBonuses ?? item?.bonuses ?? item?.Bonuses ?? item?.bonus ?? item?.attachments ?? item?.bonus_attachments ?? item?.item_bonuses
            ?? slot?.bonuses ?? slot?.Bonus ?? slot?.bonus ?? slot?.attachments;
        if (Array.isArray(from) && from.length > 0) return from.slice(0, 2);
        if (from && typeof from === "object" && !Array.isArray(from)) {
            const arr = Object.values(from).filter(Boolean);
            if (arr.length > 0) return arr.slice(0, 2);
        }
        if (typeof from === "string") {
            try { const p = JSON.parse(from); return Array.isArray(p) ? p.slice(0, 2) : []; } catch { return []; }
        }
        return [];
    }

    function parseDefenderItemsToLoadout(defenderItems) {
        if (!defenderItems || typeof defenderItems !== "object") return null;
        const loadout = {};
        for (const slotId of [...WEAPON_SLOTS, ...ARMOR_SLOTS]) {
            const slot = defenderItems[slotId] ?? defenderItems[String(slotId)];
            const item = getItemFromSlot(slot);
            if (!item) continue;

            const id = item.ID ?? item.id ?? item.item_id;
            const name = item.name ?? item.item_name ?? item.Name ?? "";
            const damage = Number(item.dmg ?? item.damage ?? item.Damage ?? 0) || 0;
            const accuracy = Number(item.acc ?? item.accuracy ?? item.Accuracy ?? 0) || 0;
            const glowRarity = (item.glowClass || "").replace("glow-", "");
            const rarity = String(item.rarity || item.Rarity || glowRarity || "default").toLowerCase();
            const rawMods = item.currentUpgrades ?? item.mods ?? item.Mods;
            const modArr = Array.isArray(rawMods) ? rawMods : (rawMods && typeof rawMods === "object" ? Object.values(rawMods) : []);
            const mods = modArr.map((m) => (m && typeof m === "object") ? { icon: m.icon ?? m.key, name: m.name ?? m.title ?? "", description: m.description ?? m.desc ?? "" } : m).filter(Boolean);
            const rawBonuses = extractBonusesFromSlot(slot, item);
            const bonuses = rawBonuses.map((b) => (typeof b === "object" && b !== null)
                ? { bonus_key: b.bonus_key ?? b.icon ?? b.key, name: b.name ?? b.title ?? b.description ?? b.desc ?? "" }
                : { bonus_key: String(b), name: "" }).filter((b) => b.bonus_key || b.name);

            loadout[slotId] = {
                item_id: id,
                item_name: name,
                damage,
                accuracy,
                rarity,
                mods: mods.slice(0, 2),
                bonuses: bonuses.slice(0, 2),
            };
        }
        return Object.keys(loadout).length > 0 ? loadout : null;
    }

    function hasNativeDefenderLoadout(defenderItems) {
        if (!defenderItems || typeof defenderItems !== "object") return false;
        return Object.entries(defenderItems).some(([key, slot]) => {
            const k = Number(key);
            return k >= 1 && k <= 9 && slot?.item?.[0]?.ID;
        });
    }

    function getDefenderArea() {
        const marker = queryFirst(W.document, [
            "#defender_Primary",
            "#defender_Secondary",
            "#defender_Melee",
            "#defender_Temporary",
        ]);
        if (marker) {
            const owner = marker.closest("[class*='playerArea'], [class*='player___']");
            if (owner) return owner;
        }
        const areas = W.document.querySelectorAll("[class*='playerArea']");
        return (areas.length > 1 ? areas[1] : areas[0]) ?? null;
    }

    const INFINITY_SVG = `<span class="eternity___QmjtV"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="10" viewBox="0 0 17 10"><g><path d="M 12.3399 1.5 C 10.6799 1.5 9.64995 2.76 8.50995 3.95 C 7.35995 2.76 6.33995 1.5 4.66995 1.5 C 2.89995 1.51 1.47995 2.95 1.48995 4.72 C 1.48995 4.81 1.48995 4.91 1.49995 5 C 1.32995 6.76 2.62995 8.32 4.38995 8.49 C 4.47995 8.49 4.57995 8.5 4.66995 8.5 C 6.32995 8.5 7.35995 7.24 8.49995 6.05 C 9.64995 7.24 10.67 8.5 12.33 8.5 C 14.0999 8.49 15.5199 7.05 15.5099 5.28 C 15.5099 5.19 15.5099 5.09 15.4999 5 C 15.6699 3.24 14.3799 1.68 12.6199 1.51 C 12.5299 1.51 12.4299 1.5 12.3399 1.5 Z M 4.66995 7.33 C 3.52995 7.33 2.61995 6.4 2.61995 5.26 C 2.61995 5.17 2.61995 5.09 2.63995 5 C 2.48995 3.87 3.27995 2.84 4.40995 2.69 C 4.49995 2.68 4.57995 2.67 4.66995 2.67 C 6.01995 2.67 6.83995 3.87 7.79995 5 C 6.83995 6.14 6.01995 7.33 4.66995 7.33 Z M 12.3399 7.33 C 10.99 7.33 10.17 6.13 9.20995 5 C 10.17 3.86 10.99 2.67 12.3399 2.67 C 13.48 2.67 14.3899 3.61 14.3899 4.74 C 14.3899 4.83 14.3899 4.91 14.3699 5 C 14.5199 6.13 13.7299 7.16 12.5999 7.31 C 12.5099 7.32 12.4299 7.33 12.3399 7.33 Z" stroke-width="0"></path></g></svg></span>`;

    function buildIconHtml(icon, title, desc) {
        const raw = icon || "blank-bonus-25";
        const safeIcon = /^[a-zA-Z0-9_-]+$/.test(raw) ? raw : "blank-bonus-25";
        const tooltip = escapeHtml([title, desc].filter(Boolean).join(" - "));
        return `<div class="container___LAqaj" title="${tooltip}"><i class="bonus-attachment-${safeIcon}" title="${tooltip}"></i></div>`;
    }

    function getBonusIconKey(bonus) {
        if (!bonus) return null;
        const raw = bonus.bonus_key ?? bonus.icon ?? "";
        return (BONUS_KEY_FIX[raw] ?? raw) || null;
    }

    function buildSlotIcons(arr, key, name, desc, isBonus) {
        return [0, 1]
            .map((i) => {
                if (!arr?.[i]) return buildIconHtml(null, "", "");
                const iconKey = isBonus ? getBonusIconKey(arr[i]) : (arr[i][key] ?? arr[i].icon);
                return buildIconHtml(iconKey, arr[i][name] ?? arr[i].description, arr[i][desc] ?? arr[i].description);
            })
            .join("");
    }

    function renderSlot(wrapper, item, slotLabel, includeLabel, slot) {
        if (!wrapper || !item) return;

        const rarityGlow = { yellow: "glow-yellow", orange: "glow-orange", red: "glow-red" };
        const glow = rarityGlow[item.rarity] || "glow-default";
        wrapper.className = wrapper.className.split(/\s+/).filter((c) => c && !/^glow-/.test(c)).join(" ");
        wrapper.classList.add(glow);

        const border = queryFirst(wrapper, ["[class*='itemBorder']"]);
        if (border) border.className = `itemBorder___mJGqQ ${glow}-border`;

        const img = queryFirst(wrapper, ["[class*='weaponImage'] img", "img"]);
        if (img && item.item_id) {
            const base = `https://www.torn.com/images/items/${item.item_id}/large`;
            img.src = `${base}.png`;
            img.srcset = `${base}.png 1x, ${base}@2x.png 2x, ${base}@3x.png 3x, ${base}@4x.png 4x`;
            img.alt = item.item_name || "";
            img.classList.remove("blank___RpGQA");
            img.style.objectFit = "contain";
        }

        const top = queryFirst(wrapper, ["[class*='top___']"]);
        if (top) {
            const modIcons = buildSlotIcons(item.mods || [], "icon", "name", "description", false);
            const bonusIcons = buildSlotIcons(item.bonuses || [], "bonus_key", "name", "description", true);
            top.innerHTML = includeLabel
                ? `<div class="props___oL_Cw">${modIcons}</div><div class="topMarker___OjRyU"><span class="markerText___HdlDL">${escapeHtml(slotLabel)}</span></div><div class="props___oL_Cw">${bonusIcons}</div>`
                : `<div class="props___oL_Cw">${modIcons}</div><div class="props___oL_Cw">${bonusIcons}</div>`;
        }

        const bottom = queryFirst(wrapper, ["[class*='bottom___']"]);
        if (bottom) {
            const ammoInner =
                slot === 3
                    ? INFINITY_SVG
                    : slot === 5
                      ? `<span class="markerText___HdlDL standard___bW8M5">1</span>`
                      : `<span class="markerText___HdlDL">Unknown</span>`;
            bottom.innerHTML = `<div class="props___oL_Cw"><i class="bonus-attachment-item-damage-bonus" aria-label="Damage"></i><span class="bonusInfo___vyqlT">${formatFixed2(item.damage)}</span></div><div class="bottomMarker___G1uDs">${ammoInner}</div><div class="props___oL_Cw"><i class="bonus-attachment-item-accuracy-bonus" aria-label="Accuracy"></i><span class="bonusInfo___vyqlT">${formatFixed2(item.accuracy)}</span></div>`;
        }

        let xp = wrapper.querySelector(".tt-weapon-experience");
        if (!xp) {
            xp = W.document.createElement("div");
            xp.className = "tt-weapon-experience";
            wrapper.appendChild(xp);
        }
        xp.textContent = item.item_name || "";
        wrapper.setAttribute("aria-label", item.item_name || "Unknown");
    }

    function renderArmor(defenderArea, loadout) {
        const modelLayers = queryFirst(defenderArea, ["[class*='modelLayers']"]);
        const armorWrap = modelLayers ? queryFirst(modelLayers, ["[class*='armoursWrap']"]) : null;
        if (!armorWrap) return;

        armorWrap.innerHTML = "";
        armorWrap.style.cssText = `position:absolute;inset:0;pointer-events:none;z-index:4;transform:translateY(${IS_PDA ? "10px" : "20px"});`;

        const layerOrder = { 8: 10, 7: 11, 9: 12, 6: 13, 4: 14 };
        for (const slot of [8, 7, 9, 6, 4]) {
            const item = loadout[slot];
            if (!item) continue;

            const container = W.document.createElement("div");
            container.className = "armourContainer___zL52C";
            container.style.zIndex = String(layerOrder[slot]);

            const armor = W.document.createElement("div");
            armor.className = "armour___fLnYY";
            const img = W.document.createElement("img");
            img.className = "itemImg___B8FMH";
            img.src = `https://www.torn.com/images/v2/items/model-items/${item.item_id}m.png`;
            img.alt = "";
            armor.appendChild(img);
            container.appendChild(armor);
            armorWrap.appendChild(container);
        }
    }

    function renderLoadout(loadout, submittedBy) {
        if (!loadout || STATE.loadoutRendered) return;

        const timer = W.setInterval(() => {
            if (STATE.loadoutRendered) {
                W.clearInterval(timer);
                return;
            }
            const defenderArea = getDefenderArea();
            if (!defenderArea) return;

            W.clearInterval(timer);

            const safeSubmitterId = submittedBy && /^\d{1,8}$/.test(String(submittedBy)) ? String(submittedBy) : null;
            if (safeSubmitterId) {
                let badge = W.document.querySelector(".loadout-shared-badge");
                if (!badge) {
                    badge = W.document.createElement("span");
                    badge.className = "loadout-shared-badge";
                    badge.style.cssText = "position:fixed;top:12px;right:12px;z-index:9999;font-size:13px;font-weight:600;color:#c00;background:rgba(0,0,0,0.85);padding:6px 10px;border-radius:6px;box-shadow:0 2px 8px rgba(0,0,0,0.3);";
                    const link = W.document.createElement("a");
                    link.href = `https://www.torn.com/profiles.php?XID=${safeSubmitterId}`;
                    link.target = "_blank";
                    link.rel = "noopener";
                    link.style.cssText = "color:#f44;text-decoration:none;";
                    link.textContent = `(shared by ${safeSubmitterId})`;
                    link.title = "Loadout shared – view submitter";
                    badge.appendChild(link);
                    W.document.body.appendChild(badge);
                } else {
                    const link = badge.querySelector("a");
                    if (link) {
                        link.href = `https://www.torn.com/profiles.php?XID=${safeSubmitterId}`;
                        link.textContent = `(shared by ${safeSubmitterId})`;
                    }
                }
            }

            const hasDefender = !!defenderArea.querySelector("#defender_Primary");
            const hasAttacker = !!defenderArea.querySelector("#attacker_Primary");
            const includeLabel = hasDefender || hasAttacker;

            for (const { slot, label, def, atk, fallback } of SLOT_MAPPINGS) {
                const selector = hasDefender ? def : hasAttacker ? atk : fallback;
                const marker = defenderArea.querySelector(selector);
                const wrapper = marker?.closest("[class*='weaponWrapper'], [class*='weapon']");
                if (wrapper && loadout[slot]) {
                    renderSlot(wrapper, loadout[slot], label, includeLabel, slot);
                }
            }

            renderArmor(defenderArea, loadout);

            const modal = queryFirst(defenderArea, ["[class*='modal']"]);
            if (modal) {
                modal.style.background = "transparent";
                modal.style.backdropFilter = "none";
                modal.style.webkitBackdropFilter = "none";
            }

            STATE.loadoutRendered = true;
        }, DOM_POLL_MS);
    }

    function parseBonuses(b) {
        if (Array.isArray(b) && b.length > 0) return b;
        if (typeof b === "string") {
            try { const arr = JSON.parse(b); return Array.isArray(arr) ? arr : []; } catch { return []; }
        }
        return [];
    }

    async function uploadLoadoutData(raw) {
        try {
            const playerId = raw?.attackerUser?.userID;
            const targetId = raw?.defenderUser?.userID;
            if (!playerId || !targetId) return;
            raw.timestamp = Math.floor(Date.now() / 1000);
            await putToFirebase(playerId, targetId, raw);
        } catch (e) {
            reportError("upload", e, { playerId: raw?.attackerUser?.userID, targetId: raw?.defenderUser?.userID });
        }
    }

    async function fetchAndRenderLoadout() {
        try {
            const playerId = STATE.attackData?.attackerUser?.userID;
            const targetId = STATE.attackData?.defenderUser?.userID;
            if (!playerId || !targetId) return;

            const urlTargetId = (W.location?.href?.match(/user2ID=(\d+)/) || [])[1];
            if (urlTargetId && String(targetId) !== urlTargetId) return;

            let result = await getFromFirebase(playerId, targetId);
            if (!result) result = await getFromFirebaseByTarget(targetId);
            if (!result?.loadout) return;
            if (!validateLoadout(result.loadout)) {
                reportError("validate", "validateLoadout failed", { playerId, targetId, loadoutKeys: Object.keys(result.loadout || {}) });
                return;
            }
            normalizeLoadout(result.loadout);
            renderLoadout(result.loadout, result.submittedBy);
        } catch (e) {
            reportError("fetch_render", e, { playerId: STATE.attackData?.attackerUser?.userID, targetId: STATE.attackData?.defenderUser?.userID });
        }
    }

    function processResponse(data) {
        if (!data || typeof data !== "object") return;
        if (!data.attackerUser && !data.DB?.attackerUser) return;

        const db = data.DB || data;
        const isFirstData = !STATE.attackData;
        STATE.attackData = db;

        if (hasNativeDefenderLoadout(db.defenderItems) && !STATE.uploaded) {
            STATE.uploaded = true;
            whenVisible(() => uploadLoadoutData(db));
        } else if (isFirstData) {
            fetchAndRenderLoadout();
        }
    }

    if (typeof W.fetch === "function") {
        const origFetch = W.fetch;
        W.fetch = async function (...args) {
            const url = typeof args[0] === "string" ? args[0] : args[0]?.url || "";
            if (!url.includes("sid=attackData")) return origFetch.apply(this, args);

            const response = await origFetch.apply(this, args);
            try { response.clone().text().then((text) => processResponse(parseJson(text))); } catch {}
            return response;
        };
    }

})();