Torn Loadout Share

Shared defender loadouts on Torn attack pages (desktop + Torn PDA). Firebase, 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!)

Advertisement:

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

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

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

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

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

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

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

Advertisement:

// ==UserScript==
// @name         Torn Loadout Share
// @namespace    loadout
// @version      1.1.4
// @description  Shared defender loadouts on Torn attack pages (desktop + Torn PDA). Firebase, no registration.
// @license      MIT
// @supportURL   https://greasyfork.org/en/scripts/570314/feedback
// @homepageURL  https://greasyfork.org/en/scripts/570314-torn-loadout-share
// @match        https://www.torn.com/page.php?sid=attack&user2ID=*
// @match        https://www.torn.com/loader.php?sid=attack&user2ID=*
// @connect      torn-loadout-share-default-rtdb.europe-west1.firebasedatabase.app
// @run-at       document-end
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// ==/UserScript==
 
(function () {
    "use strict";

    const W = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
    if (W.__loadoutShareInit) return;
    W.__loadoutShareInit = true;
    const IS_PDA = !!(W.flutter_inappwebview?.callHandler);
    // LOG_LEVEL: 0 = none, 1 = log, 2 = debug
    const LOG_LEVEL = 0;
    let EFFECTIVE_LOG_LEVEL = LOG_LEVEL;
    if (W.__loadoutDebug || W.__loadoutPdaDebug || /[?&]loadout_debug=1/.test(W.location?.search || "")) {
        EFFECTIVE_LOG_LEVEL = Math.max(EFFECTIVE_LOG_LEVEL, 2);
    }
    function log(...args) { if (EFFECTIVE_LOG_LEVEL >= 1 && W.console?.log) W.console.log("[Loadout]", ...args); }
    function dbg(...args) { if (EFFECTIVE_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 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];
    // Slot 10 is alternate helmet/mask, but Torn treats it as body cosmetics on many pages.
    // We intentionally ignore it across parsing/validation/rendering.
    const ARMOR_SLOTS = [8, 7, 9, 6, 4];
    const DOM_POLL_MS = 100;
    const PREFIGHT_REAPPLY_MS = 250;
    const RENDER_MAX_MS = 15000;
    const PREFIGHT_RENDER_MAX_MS = 120000;
    const FALLBACK_DELAYS_MS = [1500, 3000, 5000];
    const UPLOAD_RETRY_DELAYS_MS = [1500, 3500, 7000];
    const ARMOR_Z_INDEX = { 8: 10, 7: 11, 9: 12, 6: 13, 4: 14 };
    const ARMOR_MASK_SLOTS = new Set([8, 6, 4]);
    const FALLBACK_ARMOR_DOM = {
        container: "armourContainer___ftMzt",
        armour: "armour___wqLa7",
        img: "itemImg___r9DqK",
        mask: "mask___rDyND",
    };
    const SILHOUETTES = { 1: "primary", 2: "secondary", 3: "melee", 5: "temporary" };
    const ARMOR_MAP_NAME = "loadout-armor-map";
    const ARMOR_SLOT_AREAS = {
        4: [{ coords: "119,79,99,73,80,96,62,131,54,150,52,167,62,169,79,138,91,118,99,142,95,159,143,161,144,143,148,118,162,141,174,166,187,165,176,129,162,95,140,75" }],
        6: [{ coords: "118,77,104,67,99,52,104,36,118,26,132,32,136,51,133,69" }],
        7: [{ coords: "94,162,145,162,157,204,154,239,150,261,156,275,150,301,136,303,131,283,121,209,109,284,105,300,89,299,85,276,87,257,84,236,85,201" }],
        8: [
            { coords: "87,300,89,322,86,336,78,349,88,354,99,354,104,340,106,325,105,302" },
            { coords: "136,304,153,300,151,318,153,330,160,343,153,352,138,353,132,330" },
        ],
        9: [
            { coords: "48,203,55,192,62,195,67,192,61,172,50,169,44,183,40,203" },
            { coords: "175,171,189,170,196,185,198,200,191,202,184,191,177,196,176,180" },
        ],
    };
 
    const STATE = {
        uploaded: false,
        uploadInProgress: false,
        uploadRetryCount: 0,
        attackData: null,
        loadoutRendered: false,
        applyTimerId: null,
    };
 
    const PROPS_ROW_STYLE = "display:flex;flex-direction:row;align-items:center;justify-content:center;gap:2px;";
    const TOP_ROW_STYLE = "display:flex;flex-direction:row;align-items:center;justify-content:space-between;width:100%;";
    const PANEL_ID = "loadout-defender-panel";
    const PANEL_BTN_ID = "loadout-defender-panel-toggle";
    const LS_KEY_COLLAPSED = "loadout_panel_collapsed_v1";
 
    function bonusToLabel(b) {
        if (!b || typeof b !== "object") return "";
        return String(b.name || b.bonus_key || b.key || "").trim();
    }
 
    function formatWeaponBonuses(item) {
        const b1 = bonusToLabel(item?.bonuses?.[0]);
        const b2 = bonusToLabel(item?.bonuses?.[1]);
        if (!b1 && !b2) return "";
        if (b1 && b2) return ` (${b1} + ${b2})`;
        return ` (${b1 || b2})`;
    }
 
    function buildWeaponRow(item) {
        if (!item) return "";
        const name = escapeHtml(item.item_name || "Unknown");
        const bonuses = formatWeaponBonuses(item);
        return `<div style="color:#f2f5ff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${name}${escapeHtml(bonuses)}</div>`;
    }
 
    function buildArmorRow(item) {
        if (!item) return "";
        const name = escapeHtml(item.item_name || "");
        const bonuses = formatWeaponBonuses(item);
        return `<div style="color:#f2f5ff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${name}${escapeHtml(bonuses)}</div>`;
    }
 
    function renderDefenderPanel(loadout, submittedBy, savedAt) {
        if (!loadout || typeof loadout !== "object") return;
 
        const collapsed = (() => {
            try {
                return W.localStorage?.getItem(LS_KEY_COLLAPSED) === "1";
            } catch {
                return false;
            }
        })();
 
        let panel = W.document.getElementById(PANEL_ID);
        if (!panel) {
            panel = W.document.createElement("div");
            panel.id = PANEL_ID;
            panel.style.cssText =
                "position:fixed;top:12px;right:12px;z-index:999999;" +
                "background:rgba(0,0,0,0.70);color:#fff;" +
                "border:1px solid rgba(255,255,255,0.14);" +
                "border-radius:10px;overflow:hidden;" +
                "backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);";
 
            const btn = W.document.createElement("div");
            btn.id = PANEL_BTN_ID;
            btn.style.cssText =
                "display:flex;align-items:center;justify-content:space-between;" +
                "gap:10px;padding:10px 12px;" +
                "cursor:pointer;background:rgba(15,23,34,0.55);";
 
            const left = W.document.createElement("div");
            left.style.cssText = "font-weight:900;letter-spacing:0.2px;";
            left.textContent = "Defender Loadout";
 
            const right = W.document.createElement("div");
            right.style.cssText = "font-weight:900;color:#7bcf9a;";
            right.textContent = collapsed ? "▸" : "▾";
 
            btn.appendChild(left);
            btn.appendChild(right);
 
            const body = W.document.createElement("div");
            body.id = `${PANEL_ID}-body`;
            body.style.cssText = "padding:10px 12px;display:flex;flex-direction:column;gap:10px;";
 
            panel.appendChild(btn);
            panel.appendChild(body);
 
            W.document.body.appendChild(panel);
 
            btn.onclick = () => {
                try {
                    W.localStorage?.setItem(LS_KEY_COLLAPSED, panel.dataset.collapsed === "1" ? "0" : "1");
                } catch {}
                const nextCollapsed = panel.dataset.collapsed !== "1";
                panel.dataset.collapsed = nextCollapsed ? "1" : "0";
                body.style.display = nextCollapsed ? "none" : "flex";
                right.textContent = nextCollapsed ? "▸" : "▾";
            };
        }
 
        const body = W.document.getElementById(`${PANEL_ID}-body`);
        if (!body) return;
 
        const weaponsOrder = [1, 2, 3, 5];
        const armorOrder = [6, 4, 7, 8, 9];
 
        const weaponsHtml = weaponsOrder.map((slot) => buildWeaponRow(loadout?.[slot])).filter(Boolean).join("");
        const armorHtml = armorOrder.map((slot) => buildArmorRow(loadout?.[slot])).filter(Boolean).join("");

        const metaParts = [];
        const safeSubmitterId = submittedBy && /^\d{1,8}$/.test(String(submittedBy)) ? String(submittedBy) : null;
        if (safeSubmitterId) {
            metaParts.push(
                `<a href="${TORN_BASE}/profiles.php?XID=${safeSubmitterId}" target="_blank" rel="noopener" ` +
                `style="color:#f88;text-decoration:none;">shared by ${escapeHtml(safeSubmitterId)}</a>`
            );
        }
        if (savedAt != null && Number.isFinite(Number(savedAt))) {
            const ms = Number(savedAt) > 1e12 ? Number(savedAt) : Number(savedAt) * 1000;
            metaParts.push(`<span style="color:#aaa;">saved ${escapeHtml(relativeTime(Date.now() - ms))}</span>`);
        }
        const metaHtml = metaParts.length
            ? `<div style="display:flex;flex-direction:column;gap:4px;font-size:11px;font-weight:600;margin-bottom:4px;">${metaParts.join("<br>")}</div>`
            : "";

        body.innerHTML = `
            ${metaHtml}
            <div style="display:flex;flex-direction:column;gap:8px;">
                <div style="font-weight:900;color:#7bcf9a;letter-spacing:0.2px;text-transform:uppercase;font-size:11px;">Weapons</div>
                ${weaponsHtml || `<div style="color:rgba(255,255,255,0.7);font-size:12px;">(none)</div>`}
            </div>
            <div style="display:flex;flex-direction:column;gap:8px;">
                <div style="font-weight:900;color:#7bcf9a;letter-spacing:0.2px;text-transform:uppercase;font-size:11px;">Armor / Model</div>
                ${armorHtml || `<div style="color:rgba(255,255,255,0.7);font-size:12px;">(none)</div>`}
            </div>`;
 
        if (collapsed) {
            panel.dataset.collapsed = "1";
            body.style.display = "none";
            const btnRight = W.document.querySelector(`#${PANEL_BTN_ID} div:last-child`);
            if (btnRight) btnRight.textContent = "▸";
        } else {
            panel.dataset.collapsed = "0";
            body.style.display = "flex";
            const btnRight = W.document.querySelector(`#${PANEL_BTN_ID} div:last-child`);
            if (btnRight) btnRight.textContent = "▾";
        }
    }
 
    function getUrlTargetId() {
        try {
            const u = new URL(W.location.href);
            const v = u.searchParams.get("user2ID");
            if (v && /^\d+$/.test(v)) return v;
        } catch {}
        const m = String(W.location?.href || "").match(/(?:^|[?&])user2ID=(\d+)/);
        return m ? m[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]);
        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;
        // Strip slot 10 early so older Firebase payloads don't trip validation/rendering.
        if (Object.prototype.hasOwnProperty.call(loadout, 10)) delete loadout[10];
        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 headers = {};
            let bodyStr = null;
            if (body != null && method !== "GET") {
                bodyStr = JSON.stringify(body);
                headers["Content-Type"] = "application/json";
            }
            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);
            };
            const parseResponse = (status, text) => {
                if (status >= 200 && status < 300) {
                    try { done(text ? JSON.parse(text) : null); } catch { done(null); }
                } else {
                    log("firebaseRequest: status", status, path);
                    reportError("firebase_request", `HTTP ${status}`, { method, path, status });
                    done(null);
                }
            };
            const runFetch = () => {
                if (typeof W.fetch !== "function") {
                    log("firebaseRequest: no transport available");
                    done(null);
                    return;
                }
                W.fetch(url, { method, headers, ...(bodyStr != null ? { body: bodyStr } : {}) })
                    .then(async (r) => parseResponse(r.status, await r.text()))
                    .catch((e) => {
                        log("firebaseRequest(fetch): error", path, e);
                        reportError("firebase_request", "Network error", { method, path, errType: e?.type ?? "unknown" });
                        done(null);
                    });
            };
            try {
                if (IS_PDA) {
                    waitForPdaBridge(800).then(async (bridge) => {
                        if (bridge?.callHandler) {
                            try {
                                const handler = method === "GET" ? "PDA_httpGet" : "PDA_httpPost";
                                const r = method === "GET"
                                    ? await bridge.callHandler(handler, url, headers)
                                    : await bridge.callHandler(handler, url, headers, bodyStr || "");
                                return parseResponse(Number(r?.status ?? 0), String(r?.responseText ?? ""));
                            } catch {}
                        }
                        runFetch();
                    });
                    return;
                }
                if (typeof GM_xmlhttpRequest === "function") {
                    GM_xmlhttpRequest({
                        method,
                        url,
                        timeout: FIREBASE_FETCH_TIMEOUT_MS,
                        anonymous: true,
                        headers,
                        data: bodyStr,
                        onload: (r) => parseResponse(r.status, r.responseText ?? ""),
                        onerror: (e) => {
                            log("firebaseRequest: onerror", path, e);
                            reportError("firebase_request", "Network error", { method, path, errType: e?.type ?? "unknown" });
                            done(null);
                        },
                    });
                    return;
                }
                runFetch();
            } 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;
            if (extra.retryCount != null) payload.retryCount = Number(extra.retryCount) || 0;
            if (extra.retryDelayMs != null) payload.retryDelayMs = Number(extra.retryDelayMs) || 0;
            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);
        const ok = r1 != null && r2 != null;
        if (!ok) {
            reportError("upload", "Firebase PUT returned null", { playerId, targetId });
        }
        return ok;
    }
 
    function getLoadoutItem(loadout, slot) {
        if (!loadout) return null;
        return loadout[slot] ?? loadout[String(slot)] ?? null;
    }

    function parseLoadoutSlots(loadoutNode) {
        if (loadoutNode == null) return {};
        if (Array.isArray(loadoutNode)) {
            const slots = {};
            loadoutNode.forEach((v, idx) => {
                if (v == null) return;
                const key = String(idx);
                if (typeof v === "string") {
                    try { slots[key] = JSON.parse(v); } catch { /* skip */ }
                } else if (typeof v === "object" && !v["#"]) {
                    slots[key] = v;
                }
            });
            return slots;
        }
        if (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 mergeLoadoutSources(rawLoadout, jsonLoadout) {
        if (!rawLoadout) return jsonLoadout || null;
        if (!jsonLoadout) return rawLoadout;
        const merged = { ...jsonLoadout, ...rawLoadout };
        for (const slot of ARMOR_SLOTS) {
            const jsonItem = getLoadoutItem(jsonLoadout, slot);
            if (jsonItem?.item_id) merged[slot] = merged[String(slot)] = jsonItem;
        }
        for (const slot of WEAPON_SLOTS) {
            const rawItem = getLoadoutItem(rawLoadout, slot);
            if (rawItem?.item_id) merged[slot] = merged[String(slot)] = rawItem;
        }
        return merged;
    }

    function parseLoadoutJsonNode(lj) {
        if (lj == null) return null;
        let node = lj;
        if (typeof lj === "string") {
            try { node = JSON.parse(lj); } catch { return null; }
        }
        if (!node || typeof node !== "object" || node["#"]) return null;
        const loadout = parseLoadoutSlots(node);
        if (!loadout || Object.keys(loadout).length === 0) return null;
        normalizeLoadout(loadout);
        return validateLoadout(loadout) ? loadout : null;
    }

    function parsePayloadToLoadout(data) {
        if (!data || typeof data !== "object") return null;
        let fromRaw = null;
        const raw = data.raw;
        if (raw && typeof raw === "object") {
            const defenderItems = raw.defenderItems ?? raw.attackData?.defenderItems;
            const parsed = parseDefenderItemsToLoadout(defenderItems);
            if (parsed && validateLoadout(parsed)) {
                normalizeLoadout(parsed);
                fromRaw = parsed;
            }
        }
        const fromJson = parseLoadoutJsonNode(data.loadoutJson);
        if (fromRaw && fromJson) {
            const merged = mergeLoadoutSources(fromRaw, fromJson);
            return merged && validateLoadout(merged) ? merged : fromJson || fromRaw;
        }
        return fromJson || fromRaw;
    }
 
    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, savedAt: data?.timestamp ?? 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, savedAt: data?.timestamp ?? null };
        }
        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 waitForPdaBridge(timeoutMs = 4000) {
        return new Promise((resolve) => {
            const start = Date.now();
            const tick = () => {
                try {
                    if (W.flutter_inappwebview?.callHandler) return resolve(W.flutter_inappwebview);
                } catch {}
                if (Date.now() - start > timeoutMs) return resolve(null);
                W.setTimeout(tick, 200);
            };
            tick();
        });
    }

    function relativeTime(ms) {
        const fmt = (n, u) => `${n} ${u}${n > 1 ? "s" : ""} ago`;
        const mins = Math.floor(ms / 60000);
        if (mins < 1) return "just now";
        const hrs = Math.floor(ms / 3600000);
        if (hrs < 1) return fmt(mins, "minute");
        const days = Math.floor(ms / 86400000);
        if (days < 1) return fmt(hrs, "hour");
        const wks = Math.floor(days / 7);
        if (wks < 1) return fmt(days, "day");
        return fmt(Math.floor(days / 30), "month");
    }

    function getSpareMagCount(mods) {
        if (!Array.isArray(mods)) return 2;
        if (mods.some((m) => m?.name === "Extra Magazines x2")) return 4;
        if (mods.some((m) => m?.name === "Extra Magazine")) return 3;
        return 2;
    }

    function buildAmmoInner(item, slot) {
        if (slot === 3) return INFINITY_SVG;
        if (slot === 5) return `<span class="markerText___HdlDL standard___bW8M5">1</span>`;
        const clipSize = item?.clip_size;
        if (clipSize == null || clipSize === "") {
            return `<span class="markerText___HdlDL">—</span>`;
        }
        const spareMags = getSpareMagCount(item?.mods);
        const ammoType = String(item?.ammo_type || "").toLowerCase().replace(/\s+/g, "-");
        const color = ammoType ? `var(--attack-ammo-color-${ammoType}, #ddd)` : "#ddd";
        return `<span class="markerText___HdlDL" style="color:${color}">${clipSize}/${clipSize} (${spareMags})</span>`;
    }

    function queryFirstInDocs(selectors) {
        for (const doc of getDocAndIframes()) {
            const n = queryFirst(doc, selectors);
            if (n) return n;
        }
        return null;
    }

    function getWeaponArea(defenderArea) {
        const area = defenderArea || getDefenderArea();
        if (!IS_PDA) return area;
        const wraps = [];
        for (const doc of getDocAndIframes()) {
            wraps.push(...doc.querySelectorAll("[class*='armoursWrap']"));
        }
        if (wraps.length <= 1) return area;
        const defenderWrap = getArmoursWrap(area) || wraps[wraps.length - 1];
        const wrapIdx = wraps.indexOf(defenderWrap);
        const areas = [];
        for (const doc of getDocAndIframes()) {
            areas.push(...doc.querySelectorAll("[class*='playerArea']"));
        }
        if (wrapIdx >= 0 && areas[wrapIdx]) return areas[wrapIdx];
        return areas[areas.length - 1] ?? area;
    }

    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),
                clip_size: item.clipSize ?? item.clip_size ?? item.clip ?? undefined,
                ammo_type: item.ammoType ?? item.ammo_type ?? item.ammo ?? "",
            };
        }
        return Object.keys(loadout).length > 0 ? loadout : null;
    }
 
    function hasNativeDefenderLoadout(defenderItems) {
        if (!defenderItems || typeof defenderItems !== "object") return false;
        const parsed = parseDefenderItemsToLoadout(defenderItems);
        return !!parsed && Object.keys(parsed).length > 0;
    }
 
    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"];
 
    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) {
                    dbg("getDefenderArea: via defender marker", marker.id);
                    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];
            }
            // New Torn UI: both sides reuse ids like #weapon_main on the wrapper — do NOT use #weapon_main to pick attacker.
            const withDefender = [...areas].find((a) =>
                a.querySelector("[id^='defender_']")
            );
            if (withDefender) {
                dbg("getDefenderArea: playerArea with defender_* ids");
                return withDefender;
            }
            const withAttacker = [...areas].find((a) =>
                a.querySelector("#attacker_Primary, #attacker_Secondary, #attacker_Melee, #attacker_Temporary")
            );
            const chosen = withAttacker ?? areas[1] ?? areas[0];
            dbg("getDefenderArea: fallback two-area", !!withAttacker);
            return chosen;
        }
        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, containerClass) {
        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(" - "));
        const container = containerClass || "container___LAqaj";
        return `<div class="${container}" title="${tooltip}"><i class="bonus-attachment-${safeIcon}" title="${tooltip}"></i></div>`;
    }

    function pickWeaponClass(el, prefix, fallback) {
        if (!el?.classList) return fallback;
        for (const c of el.classList) {
            if (c.startsWith(prefix)) return c;
        }
        return fallback;
    }

    function discoverWeaponSlotClasses(wrapper) {
        const top = queryFirst(wrapper, ["[class*='top___']"]);
        const propsEl = top?.querySelector("[class*='props___']");
        const containerEl = top?.querySelector("[class*='container___']");
        const topMarkerEl = top?.querySelector("[class*='topMarker___']");
        const markerTextEl = topMarkerEl?.querySelector("span");
        const bottom = queryFirst(wrapper, ["[class*='bottom___']"]);
        return {
            props: pickWeaponClass(propsEl, "props___", "props___oL_Cw"),
            container: pickWeaponClass(containerEl, "container___", "container___LAqaj"),
            topMarker: pickWeaponClass(topMarkerEl, "topMarker___", "topMarker___OjRyU"),
            markerText: pickWeaponClass(markerTextEl, "markerText___", "markerText___HdlDL"),
            bonusInfo: pickWeaponClass(bottom?.querySelector("[class*='bonusInfo___']"), "bonusInfo___", "bonusInfo___vyqlT"),
            bottomMarker: pickWeaponClass(bottom?.querySelector("[class*='bottomMarker___']"), "bottomMarker___", "bottomMarker___G1uDs"),
        };
    }
 
    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, containerClass) {
        return [0, 1]
            .map((i) => {
                if (!arr?.[i]) return buildIconHtml(null, "", "", containerClass);
                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, containerClass);
            })
            .join("");
    }
 
    function getWeaponWrapper(marker) {
        if (!marker) return null;
        const cls = marker.className?.toString() || "";
        if (/weaponWrapper|weaponSlot/.test(cls)) return marker;
        return marker.closest("[class*='weaponWrapper'], [class*='weaponSlot']");
    }

    function stripPrefightEmptyStyles(wrapper) {
        if (!wrapper) return;
        wrapper.className = (wrapper.className?.toString() || "")
            .split(/\s+/)
            .filter((c) => c && !/^emptySlot/.test(c))
            .join(" ");
    }

    function stripBlankImgClasses(img) {
        if (!img) return;
        [...img.classList].forEach((c) => {
            if (/^blank___/.test(c)) img.classList.remove(c);
        });
    }

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

        stripPrefightEmptyStyles(wrapper);

        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 itemPath = `/items/${item.item_id}/`;
            const alreadyCorrect = img.src && String(img.src).includes(itemPath);
            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 || "";
            stripBlankImgClasses(img);
            img.style.objectFit = "contain";
        }
 
        const wc = discoverWeaponSlotClasses(wrapper);

        const top = queryFirst(wrapper, ["[class*='top___']"]);
        if (top) {
            const modIcons = buildSlotIcons(item.mods || [], "icon", "name", "description", false, wc.container);
            const bonusIcons = buildSlotIcons(item.bonuses || [], "bonus_key", "name", "description", true, wc.container);
            top.style.cssText = TOP_ROW_STYLE;
            top.innerHTML = includeLabel
                ? `<div class="${wc.props}" style="${PROPS_ROW_STYLE}">${modIcons}</div><div class="${wc.topMarker}"><span class="${wc.markerText}">${escapeHtml(slotLabel)}</span></div><div class="${wc.props}" style="${PROPS_ROW_STYLE}">${bonusIcons}</div>`
                : `<div class="${wc.props}" style="${PROPS_ROW_STYLE}">${modIcons}</div><div class="${wc.props}" style="${PROPS_ROW_STYLE}">${bonusIcons}</div>`;
        }

        const bottom = queryFirst(wrapper, ["[class*='bottom___']"]);
        if (bottom) {
            bottom.style.cssText = TOP_ROW_STYLE;
            bottom.innerHTML = `<div class="${wc.props}" style="${PROPS_ROW_STYLE}"><i class="bonus-attachment-item-damage-bonus" aria-label="Damage"></i><span class="${wc.bonusInfo}">${formatFixed2(item.damage)}</span></div><div class="${wc.bottomMarker}">${buildAmmoInner(item, slot)}</div><div class="${wc.props}" style="${PROPS_ROW_STYLE}"><i class="bonus-attachment-item-accuracy-bonus" aria-label="Accuracy"></i><span class="${wc.bonusInfo}">${formatFixed2(item.accuracy)}</span></div>`;
        }

        const xp = wrapper.querySelector(".tt-weapon-experience");
        if (xp) {
            xp.textContent = "";
            xp.style.display = "none";
        }
        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) {
            const silhouette = SILHOUETTES[slot];
            if (silhouette) {
                img.src = `${TORN_BASE}/images/items/silhouettes/${silhouette}.svg`;
                img.removeAttribute("srcset");
                img.alt = "";
                img.classList.add("blank___RpGQA");
                img.style.objectFit = "";
            } else {
                img.removeAttribute("src");
                img.removeAttribute("srcset");
                img.alt = "";
                img.classList.add("blank___RpGQA");
            }
        }
        const wc = discoverWeaponSlotClasses(wrapper);
        const top = queryFirst(wrapper, ["[class*='top___']"]);
        if (top) {
            top.style.cssText = TOP_ROW_STYLE;
            top.innerHTML = includeLabel
                ? `<div class="${wc.props}" style="${PROPS_ROW_STYLE}"></div><div class="${wc.topMarker}"><span class="${wc.markerText}">${escapeHtml(slotLabel)}</span></div><div class="${wc.props}" style="${PROPS_ROW_STYLE}"></div>`
                : `<div class="${wc.props}" style="${PROPS_ROW_STYLE}"></div><div class="${wc.props}" style="${PROPS_ROW_STYLE}"></div>`;
        }
        const bottom = queryFirst(wrapper, ["[class*='bottom___']"]);
        if (bottom) {
            bottom.style.cssText = TOP_ROW_STYLE;
            const ammoInner = slot === 3 ? INFINITY_SVG : slot === 5 ? `<span class="${wc.markerText} standard___bW8M5">0</span>` : `<span class="${wc.markerText}">—</span>`;
            bottom.innerHTML = `<div class="${wc.props}" style="${PROPS_ROW_STYLE}"><i class="bonus-attachment-item-damage-bonus" aria-label="Damage"></i><span class="${wc.bonusInfo}">—</span></div><div class="${wc.bottomMarker}">${ammoInner}</div><div class="${wc.props}" style="${PROPS_ROW_STYLE}"><i class="bonus-attachment-item-accuracy-bonus" aria-label="Accuracy"></i><span class="${wc.bonusInfo}">—</span></div>`;
        }
        const xp = wrapper.querySelector(".tt-weapon-experience");
        if (xp) {
            xp.textContent = "";
            xp.style.display = "none";
        }
        wrapper.setAttribute("aria-label", slotLabel || "Empty");
    }
 
    function pickArmorClass(doc, prefix, exclude) {
        for (const el of doc.querySelectorAll("[class]")) {
            for (const c of el.classList) {
                if (!c.startsWith(prefix)) continue;
                if (exclude && exclude.test(c)) continue;
                return c;
            }
        }
        return "";
    }

    function discoverArmorDomClasses() {
        for (const doc of getDocAndIframes()) {
            const sample = doc.querySelector("[class*='armourContainer']");
            if (sample) {
                const armour = sample.querySelector("[class*='armour___']");
                const img = sample.querySelector("img[class*='itemImg']");
                const mask = sample.querySelector("[class*='mask___']");
                return {
                    container: String(sample.className || ""),
                    armour: String(armour?.className || ""),
                    img: String(img?.className || ""),
                    mask: String(mask?.className || ""),
                };
            }
            const container = pickArmorClass(doc, "armourContainer___");
            const armour = pickArmorClass(doc, "armour___", /Wrap|Mask/i);
            const img = pickArmorClass(doc, "itemImg___");
            const mask = pickArmorClass(doc, "mask___");
            if (container && armour && img) {
                return { container, armour, img, mask };
            }
        }
        return { ...FALLBACK_ARMOR_DOM };
    }

    function getArmoursWrap(defenderArea) {
        if (defenderArea) {
            const inArea = queryFirst(defenderArea, ["[class*='armoursWrap']"]);
            if (inArea) return inArea;
            const scope = defenderArea.closest("[class*='playerWindow'], [class*='player___']");
            if (scope) {
                const inScope = queryFirst(scope, ["[class*='armoursWrap']"]);
                if (inScope) return inScope;
            }
        }
        for (const doc of getDocAndIframes()) {
            const wraps = [...doc.querySelectorAll("[class*='armoursWrap']")];
            if (wraps.length === 0) continue;
            if (wraps.length === 1) return wraps[0];
            if (defenderArea) {
                for (const w of wraps) {
                    if (defenderArea.compareDocumentPosition(w) & Node.DOCUMENT_POSITION_FOLLOWING) {
                        return w;
                    }
                }
            }
            return wraps[wraps.length - 1];
        }
        return null;
    }

    function setModelImgSrc(img, basePath) {
        img.alt = "";
        img.src = `${basePath}.webp`;
        img.srcset = `${basePath}.webp 1x, ${basePath}@2x.webp 2x, ${basePath}@3x.webp 3x, ${basePath}@4x.webp 4x`;
    }

    function setModelMaskImgSrc(img, basePath) {
        img.alt = "";
        img.src = `${basePath}.webp`;
        img.removeAttribute("srcset");
    }

    function getArmorMaskBase(itemId, slot) {
        if (Number(slot) === 8) return `${TORN_BASE}/images/v2/user_model/masks/head-03m-mask-d`;
        return `${TORN_BASE}/images/v2/user_model/masks/${itemId}m-mask-d`;
    }

    function shouldRenderArmorMask(slot) {
        return ARMOR_MASK_SLOTS.has(Number(slot));
    }

    function getArmorSlotsKey(loadout) {
        if (!loadout) return "";
        return ARMOR_SLOTS.map((slot) => {
            const id = getLoadoutItem(loadout, slot)?.item_id;
            return id ? `${slot}:${id}` : "";
        }).filter(Boolean).join(",");
    }

    function countArmorApplied(armoursWrap) {
        return armoursWrap?.querySelectorAll("[data-loadout-slot]").length ?? 0;
    }

    function isArmorDomCurrent(armoursWrap, loadout) {
        const key = getArmorSlotsKey(loadout);
        if (!armoursWrap || !key) return false;
        if (armoursWrap.dataset.loadoutArmorKey !== key) return false;
        let need = 0;
        for (const slot of ARMOR_SLOTS) {
            const itemId = getLoadoutItem(loadout, slot)?.item_id;
            if (!itemId) continue;
            need++;
            const el = armoursWrap.querySelector(`[data-loadout-slot="${slot}"]`);
            if (!el || String(el.dataset.loadoutItemId) !== String(itemId)) return false;
        }
        return need > 0 && armoursWrap.querySelectorAll("[data-loadout-slot]").length >= need;
    }

    function allowDefenderModelInteraction(defenderArea) {
        const playerWindow = defenderArea?.closest("[class*='playerWindow']");
        if (!playerWindow) return;
        for (const modal of playerWindow.querySelectorAll("[class*='modal']")) {
            modal.style.pointerEvents = "none";
        }
        for (const el of playerWindow.querySelectorAll("[class*='effectsWrap'], [class*='iconsContainer']")) {
            el.style.pointerEvents = "none";
        }
    }

    function setArmorWrapPointerPassthrough(armoursWrap) {
        if (armoursWrap?.dataset.loadoutArmorKey) {
            armoursWrap.style.pointerEvents = "none";
        }
    }

    function pointInPoly(x, y, coordsStr) {
        const coords = coordsStr.split(",").map(Number);
        let inside = false;
        for (let i = 0, j = coords.length - 2; i < coords.length; j = i, i += 2) {
            const xi = coords[i];
            const yi = coords[i + 1];
            const xj = coords[j];
            const yj = coords[j + 1];
            if (((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi)) {
                inside = !inside;
            }
        }
        return inside;
    }

    function getArmorNameAtPoint(x, y, loadout) {
        for (const slot of [4, 6, 7, 8, 9]) {
            const item = getLoadoutItem(loadout, slot);
            if (!item || !ARMOR_SLOT_AREAS[slot]) continue;
            for (const { coords } of ARMOR_SLOT_AREAS[slot]) {
                if (pointInPoly(x, y, coords)) return item.item_name || "";
            }
        }
        return "";
    }

    function getArmorHoverTip(doc) {
        let tip = doc.getElementById("loadout-armor-tip");
        if (!tip) {
            tip = doc.createElement("div");
            tip.id = "loadout-armor-tip";
            tip.style.cssText = "position:fixed;z-index:999999;display:none;pointer-events:none;padding:4px 8px;font-size:11px;font-weight:600;color:#ddd;background:rgba(0,0,0,0.85);border-radius:4px;white-space:nowrap;box-shadow:0 2px 8px rgba(0,0,0,0.35);";
            doc.body.appendChild(tip);
        }
        return tip;
    }

    function bindArmorCursorTooltip(defenderArea, bodyImg, loadout) {
        if (!bodyImg || !loadout) return;
        const scope = defenderArea?.closest("[class*='playerWindow']") ?? defenderArea ?? bodyImg;
        scope._loadoutHoverAbort?.abort();
        const ac = new AbortController();
        scope._loadoutHoverAbort = ac;
        const doc = scope.ownerDocument || W.document;
        const tip = getArmorHoverTip(doc);
        const onMove = (e) => {
            const rect = bodyImg.getBoundingClientRect();
            if (rect.width < 1 || rect.height < 1) {
                tip.style.display = "none";
                return;
            }
            if (e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom) {
                tip.style.display = "none";
                return;
            }
            const x = ((e.clientX - rect.left) / rect.width) * 240;
            const y = ((e.clientY - rect.top) / rect.height) * 384;
            const name = getArmorNameAtPoint(x, y, loadout);
            if (!name) {
                tip.style.display = "none";
                return;
            }
            tip.textContent = name;
            tip.style.left = `${Math.min(e.clientX + 12, W.innerWidth - 160)}px`;
            tip.style.top = `${Math.min(e.clientY + 12, W.innerHeight - 30)}px`;
            tip.style.display = "block";
        };
        const onLeave = () => {
            tip.style.display = "none";
        };
        scope.addEventListener("mousemove", onMove, { signal: ac.signal, capture: true });
        scope.addEventListener("mouseleave", onLeave, { signal: ac.signal, capture: true });
    }

    function renderArmor(defenderArea, loadout) {
        if (!loadout) return 0;
        const armoursWrap = getArmoursWrap(defenderArea);
        if (!armoursWrap) return 0;

        const classes = discoverArmorDomClasses();
        if (!classes?.container || !classes.armour || !classes.img) return 0;

        if (isArmorDomCurrent(armoursWrap, loadout)) {
            setArmorWrapPointerPassthrough(armoursWrap);
            allowDefenderModelInteraction(defenderArea);
            renderArmorHoverMap(defenderArea, loadout);
            return countArmorApplied(armoursWrap);
        }

        const modelRoot = armoursWrap.closest("[class*='modelLayers'], [class*='playerWindow']") || armoursWrap.parentElement;
        const oldOverlay = modelRoot?.querySelector(".loadout-armor-overlay") ?? defenderArea?.querySelector(".loadout-armor-overlay");
        if (oldOverlay) oldOverlay.remove();

        const modelDoc = armoursWrap.ownerDocument || W.document;
        armoursWrap.replaceChildren();

        let applied = 0;
        for (const slot of ARMOR_SLOTS) {
            const item = getLoadoutItem(loadout, slot);
            const itemId = item?.item_id;
            if (!itemId) continue;

            const container = modelDoc.createElement("div");
            container.className = classes.container;
            container.style.zIndex = String(ARMOR_Z_INDEX[slot] ?? 14);
            container.dataset.loadoutSlot = String(slot);
            container.dataset.loadoutItemId = String(itemId);

            const itemName = item.item_name || "";
            container.title = itemName;
            container.dataset.loadoutItemName = itemName;

            if (shouldRenderArmorMask(slot) && classes.mask) {
                const maskWrap = modelDoc.createElement("div");
                maskWrap.className = classes.mask;
                const maskImg = modelDoc.createElement("img");
                maskImg.className = classes.img;
                maskImg.dataset.loadoutMask = "1";
                setModelMaskImgSrc(maskImg, getArmorMaskBase(itemId, slot));
                maskImg.addEventListener("error", () => maskWrap.remove(), { once: true });
                maskWrap.appendChild(maskImg);
                container.appendChild(maskWrap);
            }

            const armour = modelDoc.createElement("div");
            armour.className = classes.armour;
            armour.title = itemName;
            const itemImg = modelDoc.createElement("img");
            itemImg.className = classes.img;
            itemImg.alt = itemName;
            itemImg.title = itemName;
            setModelImgSrc(itemImg, `${TORN_BASE}/images/v2/user_model/items/${itemId}m`);
            armour.appendChild(itemImg);
            container.appendChild(armour);

            armoursWrap.appendChild(container);
            applied++;
        }
        if (applied > 0) {
            armoursWrap.dataset.loadoutArmorKey = getArmorSlotsKey(loadout);
            setArmorWrapPointerPassthrough(armoursWrap);
        }
        allowDefenderModelInteraction(defenderArea);
        renderArmorHoverMap(defenderArea, loadout);
        return applied;
    }

    function removeArmorHoverMap(defenderArea) {
        for (const doc of getDocAndIframes()) {
            doc.querySelector(`map[name="${ARMOR_MAP_NAME}"]`)?.remove();
            doc.querySelectorAll(".loadout-armor-overlay").forEach((el) => el.remove());
            for (const img of doc.querySelectorAll(`img[usemap="#${ARMOR_MAP_NAME}"]`)) {
                img.removeAttribute("usemap");
            }
            doc.getElementById("loadout-armor-tip")?.remove();
            for (const host of doc.querySelectorAll("[class*='playerWindow'], [class*='allLayers']")) {
                host._loadoutHoverAbort?.abort();
                delete host._loadoutHoverAbort;
            }
        }
    }

    function getBodyImageForHover(defenderArea) {
        const scope = defenderArea?.closest("[class*='playerWindow'], [class*='player___']") ?? defenderArea;
        const inScope = queryFirst(scope, ["[class*='bodyImage'] img", "img[src*='user_model/body']"]);
        if (inScope) return inScope;
        const wraps = scope ? [...scope.querySelectorAll("[class*='armoursWrap']")] : [];
        if (wraps.length) {
            const modelRoot = wraps[0].closest("[class*='modelLayers']");
            const inModel = queryFirst(modelRoot, ["[class*='bodyImage'] img", "img[src*='user_model/body']"]);
            if (inModel) return inModel;
        }
        return queryFirst(defenderArea, ["[class*='bodyImage'] img", "img[src*='user_model/body']"])
            ?? queryFirstInDocs(["[class*='bodyImage'] img", "img[src*='user_model/body']"]);
    }

    function renderArmorHoverMap(defenderArea, loadout) {
        if (!loadout) return;
        removeArmorHoverMap(defenderArea);
        const bodyImg = getBodyImageForHover(defenderArea);
        if (!bodyImg) return;
        bodyImg.removeAttribute("usemap");
        bindArmorCursorTooltip(defenderArea, bodyImg, loadout);
    }

    function buildSlotMappings(defenderArea) {
        if (defenderArea.querySelector("#defender_Primary")) {
            return {
                preFight: false,
                includeLabel: true,
                mappings: [
                    { selector: "#defender_Primary", slot: 1, label: "Primary" },
                    { selector: "#defender_Secondary", slot: 2, label: "Secondary" },
                    { selector: "#defender_Melee", slot: 3, label: "Melee" },
                    { selector: "#defender_Temporary", slot: 5, label: "Temporary" },
                ],
            };
        }
        const tempSelector = defenderArea.querySelector("#defender_Temporary") ? "#defender_Temporary" : "#weapon_temp";
        return {
            preFight: true,
            includeLabel: false,
            mappings: [
                { selector: "#weapon_main", slot: 1, label: "Primary" },
                { selector: "#weapon_second", slot: 2, label: "Secondary" },
                { selector: "#weapon_melee", slot: 3, label: "Melee" },
                { selector: tempSelector, slot: 5, label: "Temporary" },
            ],
        };
    }

    function applyLoadoutWeapons(defenderArea, loadout, config) {
        const weaponArea = getWeaponArea(defenderArea);
        let applied = 0;
        let expected = 0;
        for (const { selector, slot, label } of config.mappings) {
            expected++;
            const marker = queryFirst(weaponArea, [selector]) || queryFirstInDocs([selector]);
            const wrapper = getWeaponWrapper(marker);
            if (!wrapper) continue;
            const item = getLoadoutItem(loadout, slot);
            if (item) {
                renderSlot(wrapper, item, label, config.includeLabel, slot);
            } else {
                clearSlot(wrapper, label, config.includeLabel, slot);
            }
            applied++;
        }
        renderArmor(defenderArea, loadout);
        return { applied, expected };
    }

    function stopApplyTimer() {
        if (STATE.applyTimerId) {
            W.clearInterval(STATE.applyTimerId);
            STATE.applyTimerId = null;
        }
    }

    function renderLoadoutMeta(submittedBy, savedAt) {
        if (!IS_PDA) {
            W.document.querySelector(".loadout-shared-meta")?.remove();
            return;
        }
        let meta = W.document.querySelector(".loadout-shared-meta");
        if (!meta) {
            meta = W.document.createElement("div");
            meta.className = "loadout-shared-meta";
            W.document.body.appendChild(meta);
        }
        meta.style.cssText =
            "position:fixed;top:52px;left:12px;z-index:9999;display:flex;flex-direction:column;gap:4px;" +
            "font-size:11px;font-weight:600;color:#ddd;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 parts = [];
        const safeSubmitterId = submittedBy && /^\d{1,8}$/.test(String(submittedBy)) ? String(submittedBy) : null;
        if (safeSubmitterId) {
            parts.push(
                `<a href="${TORN_BASE}/profiles.php?XID=${safeSubmitterId}" target="_blank" rel="noopener" ` +
                `style="color:#f44;text-decoration:none;">shared by ${escapeHtml(safeSubmitterId)}</a>`
            );
        }
        if (savedAt != null && Number.isFinite(Number(savedAt))) {
            const ms = Number(savedAt) > 1e12 ? Number(savedAt) : Number(savedAt) * 1000;
            parts.push(`<span style="color:#aaa;">saved ${escapeHtml(relativeTime(Date.now() - ms))}</span>`);
        }
        if (!parts.length) {
            meta.remove();
            return;
        }
        meta.innerHTML = parts.join("<br>");
    }

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

        const startedAt = Date.now();
        let panelRendered = false;
        let badgeRendered = false;
        let modalAdjusted = false;

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

            const config = buildSlotMappings(defenderArea);
            const { applied, expected } = applyLoadoutWeapons(defenderArea, loadout, config);

            if (!badgeRendered) {
                if (IS_PDA) renderLoadoutMeta(submittedBy, savedAt);
                else W.document.querySelector(".loadout-shared-meta")?.remove();
                badgeRendered = true;
            }

            if (!panelRendered && !IS_PDA) {
                renderDefenderPanel(loadout, submittedBy, savedAt);
                panelRendered = true;
            } else if (!panelRendered) {
                panelRendered = true;
            }

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

            const elapsed = Date.now() - startedAt;
            const allApplied = expected > 0 && applied >= expected;
            const inFightDom = !config.preFight;
            const maxMs = config.preFight ? PREFIGHT_RENDER_MAX_MS : RENDER_MAX_MS;
            const timedOut = elapsed >= maxMs;
            const done = config.preFight ? inFightDom || timedOut : allApplied || inFightDom || timedOut;

            if (done) {
                dbg("renderLoadout: done", { applied, expected, preFight: config.preFight, inFightDom, timedOut });
                STATE.loadoutRendered = true;
                stopApplyTimer();
            }
        }, PREFIGHT_REAPPLY_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 false;
            raw.timestamp = Math.floor(Date.now() / 1000);
            return await putToFirebase(playerId, targetId, raw);
        } catch (e) {
            reportError("upload", e, { playerId: raw?.attackerUser?.userID, targetId: raw?.defenderUser?.userID });
            return false;
        }
    }
 
    function scheduleUploadRetry(raw) {
        if (STATE.uploaded || STATE.uploadInProgress) return;
        STATE.uploadInProgress = true;
        whenVisible(async () => {
            const ok = await uploadLoadoutData(raw);
            if (ok) {
                STATE.uploaded = true;
                STATE.uploadInProgress = false;
                STATE.uploadRetryCount = 0;
                return;
            }
 
            const retryIndex = STATE.uploadRetryCount;
            const delay = UPLOAD_RETRY_DELAYS_MS[Math.min(retryIndex, UPLOAD_RETRY_DELAYS_MS.length - 1)];
            STATE.uploadInProgress = false;
            STATE.uploadRetryCount += 1;
            reportError("upload_retry", "Upload failed, scheduling retry", {
                playerId: raw?.attackerUser?.userID,
                targetId: raw?.defenderUser?.userID,
                retryCount: STATE.uploadRetryCount,
                retryDelayMs: delay,
            });
            W.setTimeout(() => {
                if (!STATE.uploaded && !STATE.uploadInProgress) scheduleUploadRetry(raw);
            }, delay);
        });
    }
 
    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;
            normalizeLoadout(result.loadout);
            if (!validateLoadout(result.loadout)) {
                reportError("validate", "validateLoadout failed", { playerId, targetId, loadoutKeys: Object.keys(result.loadout || {}).slice(0, 10) });
                return;
            }
            renderLoadout(result.loadout, result.submittedBy, result.savedAt);
        } 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;
        const db = data.DB || data;
        if (!db || typeof db !== "object") return;
        const attacker = db.attackerUser ?? data.attackerUser;
        const defender = db.defenderUser ?? data.defenderUser;
        if (!attacker?.userID && !defender?.userID && !db.defenderItems) return;

        const isFirstData = !STATE.attackData;
        STATE.attackData = db;
 
        const hasNative = hasNativeDefenderLoadout(db.defenderItems);
        if (hasNative && !STATE.uploaded && !STATE.uploadInProgress) {
            scheduleUploadRetry(db);
        }
 
        if (hasNative) {
            const nativeLoadout = parseDefenderItemsToLoadout(db.defenderItems);
            dbg("processResponse: native", nativeLoadout ? "ok" : "null", "valid?", !!nativeLoadout && validateLoadout(nativeLoadout));
            if (nativeLoadout && validateLoadout(nativeLoadout)) {
                normalizeLoadout(nativeLoadout);
                renderLoadout(nativeLoadout, null, null);
            }
        } else if (isFirstData) {
            dbg("processResponse: fetchAndRenderLoadout");
            fetchAndRenderLoadout();
        }
    }
 
    if (typeof W.fetch === "function") {
        const origFetch = W.fetch;
        W.fetch = async function (...args) {
            const req = args[0];
            const url =
                typeof req === "string"
                    ? req
                    : req && typeof Request !== "undefined" && req instanceof Request
                      ? req.url
                      : req?.url || "";
            // Torn uses /loader.php?sid=attackData or /page.php?sid=attackData&mode=json (see builds/attack app).
            if (!url.includes("sid=attackData")) return origFetch.apply(this, args);

            const response = await origFetch.apply(this, args);
            if (response.ok) {
                try {
                    response.clone().text().then((text) => {
                        const parsed = parseJson(text);
                        if (parsed) processResponse(parsed);
                    });
                } catch {}
            }
            return response;
        };
    }
 
    function tryFetchByUrlTarget() {
        if (!getUrlTargetId() || STATE.loadoutRendered || STATE.attackData?.defenderUser?.userID) return;
        if (!getDefenderArea() && !getArmoursWrap(null)) return;
        fetchAndRenderLoadout();
    }

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