Torn Loadout Share

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Torn Loadout Share
// @namespace    loadout
// @version      1.0.5
// @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 log(...args) { if ((LOG_LEVEL >= 1 || W.__loadoutDebug) && 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 TORN_BASE = "https://www.torn.com";
    const CONFIG = {
        firebaseUrl: "https://torn-loadout-share-default-rtdb.europe-west1.firebasedatabase.app",
        imagesBase: `${TORN_BASE}/images`,
    };
    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 FALLBACK_DELAYS_MS = [1500, 3000, 5000];
    const ARMOR_Z_INDEX = { 8: 10, 7: 11, 9: 12, 6: 13, 4: 14 };

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

    function getUrlTargetId() {
        return (W.location?.href?.match(/user2ID=(\d+)/) || [])[1] ?? null;
    }

    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) { log("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") {
                    log("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 {
                            log("firebaseRequest: status", r.status, path);
                            reportError("firebase_request", `HTTP ${r.status}`, { method, path, status: r.status });
                            done(null);
                        }
                    },
                    onerror: (e) => {
                        log("firebaseRequest: onerror", path, e);
                        reportError("firebase_request", "Network error", { method, path, errType: e?.type ?? "unknown" });
                        done(null);
                    },
                });
            } catch (e) {
                log("firebaseRequest: exception", path, e);
                reportError("firebase_request", e, { method, path });
                done(null);
            }
        });
    }

    function reportError(ctx, error, extra = {}) {
        try {
            const errMsg = typeof error === "string" ? error : (error?.message ?? String(error));
            const payload = {
                ctx,
                err: errMsg,
                ts: Date.now(),
                path: (W.location?.pathname ?? "") + (W.location?.search ?? ""),
                user2ID: getUrlTargetId(),
                pid: STATE.attackData?.attackerUser?.userID ?? extra.playerId ?? null,
                tid: STATE.attackData?.defenderUser?.userID ?? extra.targetId ?? null,
            };
            if (extra.method) payload.req = `${extra.method} ${extra.path}`;
            if (extra.status) payload.status = extra.status;
            if (extra.errType) payload.errType = extra.errType;
            if (extra.hasRaw != null) payload.hasRaw = !!extra.hasRaw;
            if (extra.hasLoadoutJson != null) payload.hasLoadoutJson = !!extra.hasLoadoutJson;
            if (extra.loadoutKeys) payload.loadoutKeys = extra.loadoutKeys;
            const stack = error?.stack;
            if (stack) payload.stack = String(stack).slice(0, 400);
            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);
        log("getFromFirebase", key);
        const data = await firebaseRequest("GET", `loadouts/${key}`);
        const loadout = parsePayloadToLoadout(data);
        log("getFromFirebase result", !!data, !!loadout);
        if (loadout) return { loadout, submittedBy: null };
        if (data && typeof data === "object" && (data.raw || data.loadoutJson)) {
            reportError("parse", "parsePayloadToLoadout returned null", { playerId, targetId, hasRaw: !!data.raw, hasLoadoutJson: !!data.loadoutJson });
        }
        return null;
    }

    async function getFromFirebaseByTarget(targetId) {
        log("getFromFirebaseByTarget", targetId);
        const data = await firebaseRequest("GET", `loadouts/by_target/${targetId}`);
        const loadout = parsePayloadToLoadout(data);
        log("getFromFirebaseByTarget result", !!data, !!loadout);
        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_by_target", "parsePayloadToLoadout returned null", { 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 idNum = Number(id);
            if (id == null || id === "" || !Number.isInteger(idNum) || idNum < 1 || idNum > 999999) continue;
            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 getDocAndIframes() {
        const docs = [W.document];
        try {
            for (const iframe of W.document.querySelectorAll("iframe")) {
                try {
                    const doc = iframe.contentDocument || iframe.contentWindow?.document;
                    if (doc && doc !== W.document) docs.push(doc);
                } catch {}
            }
        } catch {}
        return docs;
    }

    const DEFENDER_MARKERS = ["#defender_Primary", "#defender_Secondary", "#defender_Melee", "#defender_Temporary"];
    const WEAPON_MARKERS = ["#attacker_Primary", "#defender_Primary", "#weapon_main"];

    function getDefenderArea() {
        for (const doc of getDocAndIframes()) {
            const marker = queryFirst(doc, DEFENDER_MARKERS);
            if (marker) {
                const owner = marker.closest("[class*='playerArea'], [class*='player___']");
                if (owner) return owner;
            }
        }
        for (const doc of getDocAndIframes()) {
            const areas = doc.querySelectorAll("[class*='playerArea']");
            if (areas.length === 0) continue;
            if (areas.length === 1) return areas[0];
            const withWeapons = [...areas].find((a) => queryFirst(a, WEAPON_MARKERS));
            return withWeapons || areas[1] || areas[0];
        }
        return 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 = `${CONFIG.imagesBase}/items/${item.item_id}/large`;
            const alreadyCorrect = img.src && String(img.src).includes(`/items/${item.item_id}/`);
            if (!alreadyCorrect) {
                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 clearSlot(wrapper, slotLabel, includeLabel, slot) {
        if (!wrapper) return;
        wrapper.className = wrapper.className.split(/\s+/).filter((c) => c && !/^glow-/.test(c)).join(" ");
        wrapper.classList.add("glow-default");
        const border = queryFirst(wrapper, ["[class*='itemBorder']"]);
        if (border) border.className = "itemBorder___mJGqQ glow-default-border";
        const img = queryFirst(wrapper, ["[class*='weaponImage'] img", "img"]);
        if (img) {
            img.removeAttribute("src");
            img.removeAttribute("srcset");
            img.alt = "";
            img.classList.add("blank___RpGQA");
        }
        const top = queryFirst(wrapper, ["[class*='top___']"]);
        if (top) {
            top.innerHTML = includeLabel
                ? `<div class="props___oL_Cw"></div><div class="topMarker___OjRyU"><span class="markerText___HdlDL">${escapeHtml(slotLabel)}</span></div><div class="props___oL_Cw"></div>`
                : `<div class="props___oL_Cw"></div><div class="props___oL_Cw"></div>`;
        }
        const bottom = queryFirst(wrapper, ["[class*='bottom___']"]);
        if (bottom) {
            const ammoInner = slot === 3 ? INFINITY_SVG : slot === 5 ? `<span class="markerText___HdlDL standard___bW8M5">0</span>` : `<span class="markerText___HdlDL">—</span>`;
            bottom.innerHTML = `<div class="props___oL_Cw"><i class="bonus-attachment-item-damage-bonus" aria-label="Damage"></i><span class="bonusInfo___vyqlT">—</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">—</span></div>`;
        }
        const xp = wrapper.querySelector(".tt-weapon-experience");
        if (xp) xp.textContent = "";
        wrapper.setAttribute("aria-label", slotLabel || "Empty");
    }

    function applyWeaponLoadoutToSlots(defenderArea, loadout, hasDefender, hasAttacker, defenderReplacesAttacker) {
        const includeLabel = hasDefender || hasAttacker;
        for (const { slot, label, def, atk, fallback } of SLOT_MAPPINGS) {
            const selectors = defenderReplacesAttacker ? [def, atk, fallback] : (hasDefender ? [def] : hasAttacker ? [atk] : [fallback]);
            let marker = null;
            for (const sel of selectors) {
                marker = defenderArea.querySelector(sel);
                if (marker) break;
            }
            const wrapper = marker?.closest("[class*='weaponWrapper'], [class*='weapon']");
            if (!wrapper) continue;
            if (loadout?.[slot]) {
                renderSlot(wrapper, loadout[slot], label, includeLabel, slot);
            } else if (defenderReplacesAttacker) {
                clearSlot(wrapper, label, includeLabel, slot);
            }
        }
    }

    function renderArmor(defenderArea, loadout) {
        const modelLayers = queryFirst(defenderArea, ["[class*='modelLayers']"]);
        const parent = modelLayers || defenderArea;
        if (!parent) return;

        let overlay = parent.querySelector(".loadout-armor-overlay");
        if (overlay) overlay.remove();
        overlay = W.document.createElement("div");
        overlay.className = "loadout-armor-overlay";
        overlay.style.cssText = `position:absolute;inset:0;pointer-events:none;z-index:4;transform:translateY(${IS_PDA ? "10px" : "20px"});`;

        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(ARMOR_Z_INDEX[slot]);

            const armor = W.document.createElement("div");
            armor.className = "armour___fLnYY";
            const img = W.document.createElement("img");
            img.className = "itemImg___B8FMH";
            img.src = `${CONFIG.imagesBase}/v2/items/model-items/${item.item_id}m.png`;
            img.alt = "";
            armor.appendChild(img);
            container.appendChild(armor);
            overlay.appendChild(container);
        }
        parent.appendChild(overlay);
    }

    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 = `${TORN_BASE}/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 = `${TORN_BASE}/profiles.php?XID=${safeSubmitterId}`;
                        link.textContent = `(shared by ${safeSubmitterId})`;
                    }
                }
            }

            const hasDefender = !!defenderArea.querySelector("#defender_Primary");
            const hasAttacker = !!defenderArea.querySelector("#attacker_Primary");
            const defenderReplacesAttacker = hasAttacker && !hasDefender;

            const hasWeapons = loadout && WEAPON_SLOTS.some((s) => loadout[s]);
            if (hasWeapons) applyWeaponLoadoutToSlots(defenderArea, loadout, hasDefender, hasAttacker, defenderReplacesAttacker);
            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;
            let targetId = STATE.attackData?.defenderUser?.userID ?? getUrlTargetId();
            const urlTargetId = getUrlTargetId();

            if (!targetId) return;
            if (urlTargetId && String(targetId) !== urlTargetId) return;

            let result = playerId ? await getFromFirebase(playerId, targetId) : null;
            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 || {}).slice(0, 10) });
                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;

        const hasNative = hasNativeDefenderLoadout(db.defenderItems);
        if (hasNative && !STATE.uploaded) {
            STATE.uploaded = true;
            whenVisible(() => uploadLoadoutData(db));
        }

        if (hasNative) {
            const nativeLoadout = parseDefenderItemsToLoadout(db.defenderItems);
            if (nativeLoadout && validateLoadout(nativeLoadout)) {
                normalizeLoadout(nativeLoadout);
                renderLoadout(nativeLoadout, null);
            }
        } 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;
        };
    }

    function tryFetchByUrlTarget() {
        if (!getUrlTargetId() || STATE.loadoutRendered || STATE.attackData?.defenderUser?.userID || !getDefenderArea()) return;
        fetchAndRenderLoadout();
    }

    whenVisible(() => {
        FALLBACK_DELAYS_MS.forEach((ms) => W.setTimeout(tryFetchByUrlTarget, ms));
    });

})();