Attack Helpers

Makes attacking, walling, and faction scouting easier. TornPDA compatible.

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

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

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

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

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

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

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

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

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

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!)

// ==UserScript==
// @name         Attack Helpers
// @namespace    http://tampermonkey.net/
// @license      NOLICENSE
// @version      2.0.1
// @description  Makes attacking, walling, and faction scouting easier. TornPDA compatible.
// @author       maximate
// @match        https://www.torn.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM.xmlHttpRequest
// @grant        unsafeWindow
// @connect      api.torn.com
// @connect      www.tornstats.com
// @run-at       document-end
// ==/UserScript==

(function () {
    "use strict";

    const PDA_API_KEY = "###PDA-APIKEY###";
    const PDA_KEY_READY = PDA_API_KEY && !/^###.+###$/.test(PDA_API_KEY);
    const pageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;

    const PREFIX = "maximate.attack.";
    const OLD_PREFIX = "miz.";
    const FOUND = "data-maximate-found";
    const IS_PDA = PDA_KEY_READY || typeof PDA_httpGet === "function" || /TornPDA|Torn PDA/i.test(navigator.userAgent);
    const IS_MOBILE = IS_PDA || window.matchMedia("(max-width: 760px)").matches;
    let xhr = null;
    if (typeof GM_xmlhttpRequest === "function") {
        xhr = GM_xmlhttpRequest;
    } else if (typeof GM !== "undefined" && typeof GM.xmlHttpRequest === "function") {
        xhr = GM.xmlHttpRequest;
    }
    const DEFAULT_SETTINGS = {
        battlestats: true,
        wallTimers: true,
        wallHosp: true,
        executeHelper: true,
        fastAttack: false,
        clickableAssists: true,
        liveFilter: true,
        quickActions: true,
        mobileTapStats: true,
        compactMobile: true,
        cacheHours: 24
    };
    const apiStatus = {
        torn: "Not checked",
        tornStats: "Not checked",
        lastError: ""
    };

    const readStorage = (key, fallback = "") => {
        const current = localStorage[`${PREFIX}${key}`];
        if (current !== undefined && current !== null) return current;
        const old = localStorage[`${OLD_PREFIX}${key}`];
        if (old !== undefined && old !== null) return old;
        return fallback;
    };
    const writeStorage = (key, value) => {
        localStorage[`${PREFIX}${key}`] = value;
    };

    const safeJson = (str, fallback = null) => {
        if (str && typeof str === "object") return str;
        try {
            return JSON.parse(str);
        } catch (_e) {
            return fallback;
        }
    };

    const readJson = (key, fallback) => safeJson(readStorage(key, ""), fallback);
    const writeJson = (key, value) => writeStorage(key, JSON.stringify(value));

    const settings = Object.assign({}, DEFAULT_SETTINGS, readJson("settings", {}));
    let API_KEY = readStorage("api-key", PDA_KEY_READY ? PDA_API_KEY : "");
    let initialized = false;

    if (API_KEY && localStorage[`${PREFIX}api-key`] !== API_KEY) writeStorage("api-key", API_KEY);

    const saveSettings = () => writeJson("settings", settings);
    const cacheLifetime = () => Math.max(1, Number(settings.cacheHours) || 24) * 60 * 60 * 1000;

    const formatTime = (seconds) => {
        if (!Number.isFinite(seconds) || seconds < 0) return null;

        const hours = Math.floor(seconds / 3600);
        const minutes = Math.floor((seconds % 3600) / 60);
        const secs = Math.round(seconds % 60);
        const parts = [];

        if (hours) parts.push(`${hours} hr`);
        if (minutes) parts.push(`${minutes} min`);
        parts.push(`${secs} sec`);

        return parts.join(" ");
    };

    const waitForElement = (selector, root = document) => new Promise((resolve) => {
        const existing = root.querySelector(selector);
        if (existing) {
            resolve(existing);
            return;
        }

        const observer = new MutationObserver(() => {
            const node = root.querySelector(selector);
            if (!node) return;
            observer.disconnect();
            resolve(node);
        });

        observer.observe(document.body, { childList: true, subtree: true });
    });

    const watchElements = (selector, action, root = document, intervalMs = 300) => {
        const scan = () => {
            root.querySelectorAll(selector).forEach((node) => {
                if (node.getAttribute(FOUND)) return;
                const retry = action(node);
                if (!retry) node.setAttribute(FOUND, "1");
            });
        };

        scan();
        return setInterval(scan, intervalMs);
    };

    const addStyle = (cssText) => {
        if (typeof GM_addStyle === "function") {
            GM_addStyle(cssText);
            return;
        }

        const style = document.createElement("style");
        style.textContent = cssText;
        (document.head || document.documentElement).appendChild(style);
    };

    const requestText = (url, method = "GET") => new Promise((resolve, reject) => {
        if (typeof PDA_httpGet === "function" && String(method).toUpperCase() === "GET") {
            PDA_httpGet(url)
                .then((data) => resolve(typeof data === "string" ? data : JSON.stringify(data)))
                .catch(reject);
            return;
        }

        if (!xhr) {
            reject(new Error("GM_xmlhttpRequest is unavailable."));
            return;
        }

        xhr({
            method,
            url,
            onload: (response) => resolve(response.responseText),
            onerror: reject
        });
    });

    const stripHtml = (html) => {
        const tmp = document.createElement("div");
        tmp.innerHTML = html;
        return tmp.textContent.replace(/\s+/g, " ").trim();
    };

    const notify = (message) => {
        const existing = document.querySelector(".maximate-toast");
        if (existing) existing.remove();

        const toast = document.createElement("div");
        toast.className = "maximate-toast";
        toast.textContent = message;
        document.body.appendChild(toast);
        setTimeout(() => toast.remove(), 2500);
    };

    const copyText = async (text) => {
        try {
            await navigator.clipboard.writeText(text);
            notify("Copied");
        } catch (_e) {
            prompt("Copy this:", text);
        }
    };

    const currentFactionId = () => {
        const urlMatch = window.location.href.match(/[?&]ID=(\d+)/);
        const facLink = document.querySelector("a[href*='factions.php?step=profile&ID=']");
        const linkMatch = facLink && facLink.href.match(/ID=(\d+)/);
        const fromUrl = urlMatch && urlMatch[1];
        const fromLink = linkMatch && linkMatch[1];
        return fromUrl || fromLink || "";
    };

    const renderStatPopup = (node) => {
        if (!settings.mobileTapStats || !IS_MOBILE || !node || !node.dataset || !node.dataset.statHtml) return;

        let popup = document.querySelector(".maximate-stat-popover");
        if (popup) {
            popup.remove();
            return;
        }

        popup = document.createElement("div");
        popup.className = "maximate-stat-popover";
        popup.innerHTML = `${node.dataset.statHtml}<button type="button">Close</button>`;
        popup.querySelector("button").onclick = () => popup.remove();
        document.body.appendChild(popup);
    };

    const settingLabel = (key) => ({
        battlestats: "Battlestats",
        wallTimers: "Wall timers",
        wallHosp: "Wall/hospital checks",
        executeHelper: "Execute helper",
        fastAttack: "Fast attack",
        clickableAssists: "Clickable assists",
        liveFilter: "Live member filter",
        quickActions: "Target quick actions",
        mobileTapStats: "Tap stat popups",
        compactMobile: "Compact mobile UI"
    }[key] || key);

    const pageLabel = () => ({
        profile: "Profile page",
        loader: "Attack page",
        "attack-log": "Attack log",
        faction: "Faction page"
    }[getPage()] || "No helper features on this page");

    const pageHint = () => ({
        profile: "Profile pages mainly get enabled attack links and mini-profile stats.",
        loader: "Attack pages get wall/hospital status, execute helper, quick actions, assists, and optional fast attack.",
        "attack-log": "Attack logs get readable icon hover titles.",
        faction: "Faction pages get battlestats, wall timers, cache refresh, and live filters."
    }[getPage()] || "Open a faction page, attack page, attack log, or player profile to see feature changes.");

    const showSettingsPanel = () => {
        const oldPanel = document.querySelector(".maximate-settings-panel");
        if (oldPanel) oldPanel.remove();

        const cacheCount = Object.keys((battlestatCache && battlestatCache.battleStats) || {}).length;
        const stampCount = Object.keys((battlestatCache && battlestatCache.factionStamps) || {}).filter((id) => battlestatCache.factionStamps[id]).length;
        const panel = document.createElement("div");
        panel.className = "maximate-settings-panel";
        panel.innerHTML = `
            <div class="maximate-settings-head">
                <strong>War Attack Helpers</strong>
                <button type="button" data-action="close">x</button>
            </div>
            <div class="maximate-settings-status">
                <div><b>Page:</b> ${pageLabel()}</div>
                <div><b>Mode:</b> ${IS_PDA ? "TornPDA/mobile" : "Desktop userscript"}</div>
                <div><b>Key:</b> ${API_KEY ? (PDA_KEY_READY ? "TornPDA key" : "Saved key") : "Missing"}</div>
                <div><b>Torn API:</b> ${apiStatus.torn === "Not checked" ? "Idle until attack page" : apiStatus.torn}</div>
                <div><b>TornStats:</b> ${apiStatus.tornStats === "Not checked" ? "Idle until faction/spy data" : apiStatus.tornStats}</div>
                <div><b>Cache:</b> ${cacheCount} players, ${stampCount} factions</div>
                <div>${pageHint()}</div>
                ${apiStatus.lastError ? `<div class="maximate-error">${apiStatus.lastError}</div>` : ""}
            </div>
            <div class="maximate-settings-grid">
                ${Object.keys(DEFAULT_SETTINGS).filter((key) => typeof DEFAULT_SETTINGS[key] === "boolean").map((key) => `
                    <label><input type="checkbox" data-setting="${key}" ${settings[key] ? "checked" : ""}>${settingLabel(key)}</label>
                `).join("")}
            </div>
            <label class="maximate-cache-hours">Cache hours <input type="number" min="1" max="168" data-setting="cacheHours" value="${settings.cacheHours}"></label>
            <div class="maximate-settings-actions">
                <button type="button" data-action="key">Set key</button>
                <button type="button" data-action="refresh-faction">Refresh faction</button>
                <button type="button" data-action="clear-cache">Clear cache</button>
                <button type="button" data-action="reload">Apply</button>
            </div>
        `;

        panel.addEventListener("change", (evt) => {
            const key = evt.target.dataset.setting;
            if (!key) return;
            settings[key] = evt.target.type === "checkbox" ? evt.target.checked : Number(evt.target.value);
            saveSettings();
        });

        panel.addEventListener("click", async (evt) => {
            const action = evt.target.dataset.action;
            if (!action) return;
            if (action === "close") panel.remove();
            if (action === "reload") window.location.reload();
            if (action === "key") promptForApiKey();
            if (action === "clear-cache") {
                battlestatCache.clearAll();
                notify("Cache cleared");
                showSettingsPanel();
            }
            if (action === "refresh-faction") {
                const factionId = currentFactionId();
                if (!factionId) {
                    notify("No faction found");
                    return;
                }
                delete battlestatCache.factionStamps[factionId];
                await battlestatCache.loadFaction(factionId, true);
                notify("Faction refreshed");
                showSettingsPanel();
            }
        });

        document.body.appendChild(panel);
    };

    const promptForApiKey = () => {
        const input = prompt("Please enter the same API key you use for TornStats:", API_KEY || "");
        if (input) {
            API_KEY = input.trim();
            writeStorage("api-key", API_KEY);
            notify("API key saved");
            if (initialized) window.location.reload();
            else init();
        }
        return false;
    };

    const getPage = () => {
        const href = String(window.location.href);
        if (/https?:\/\/www\.torn\.com\/profiles\.php/i.test(href)) return "profile";
        if (/https?:\/\/www\.torn\.com\/loader\.php\?sid=attack&/i.test(href)) return "loader";
        if (/https?:\/\/www\.torn\.com\/loader\.php\?sid=attackLog&ID=/i.test(href)) return "attack-log";
        if (/https?:\/\/www\.torn\.com\/factions\.php/i.test(href)) return "faction";
        return "";
    };

    const getUserId = () => new URLSearchParams(window.location.search).get("user2ID");

    const injectInitialHtml = () => {
        addStyle(`
            :root .dark-mode [class*=modal__] { background: none !important; }
            span#wall-status { margin: 0 5px; }
            span#hosp-status { display: block; cursor: pointer; }

            @media screen and (max-width: 1000px) {
                .members-cont .bs { display: none; }
            }

            .members-cont .level { width: 27px !important; }
            .members-cont .id { padding-left: 5px !important; width: 28px !important; }
            .members-cont .points { width: 42px !important; }
            .finally-bs-stat { font-family: monospace; }
            .finally-bs-stat > span { display: inline-block; width: 55px; text-align: right; }
            .faction-names { position: relative; }
            .finally-bs-api {
                position: absolute;
                background: var(--main-bg);
                text-align: center;
                left: 0;
                top: 0;
                width: 100%;
                height: 100%;
            }
            .finally-bs-api > * { margin: 0 5px; padding: 5px; }
            .finally-bs-swap {
                position: absolute;
                top: 10px;
                left: 0;
                right: 0;
                margin-left: auto;
                margin-right: auto;
                width: 100px;
                cursor: pointer;
            }
            .finally-bs-activeIcon { display: block !important; }
            .finally-bs-asc {
                border-bottom: 6px solid var(--sort-arrow-border-color);
                border-left: 6px solid transparent;
                border-right: 6px solid transparent;
                border-top: 0 solid transparent;
                height: 0;
                top: -4px;
                width: 0;
            }
            .finally-bs-desc {
                border-bottom: 0 solid transparent;
                border-left: 6px solid transparent;
                border-right: 6px solid transparent;
                border-top: 6px solid var(--sort-arrow-color);
                height: 0;
                top: 1px;
                width: 0;
            }
            .finally-bs-col { text-overflow: clip !important; }
            .raid-members-list .level:not(.bs) { width: 16px !important; }
            body:not(.tt-mobile) .members-list.tt-modified .table-header .member {
                width: calc(34% + 85px) !important;
            }
            .chain-attacks-list .miz-recent-attacks { width: 20px !important; }
            .miz-bs-miniprofile { float: right; color: black; }
            .miz-max-z { z-index: 999999 !important; }
            .miz-name, .miz-attack { text-decoration: none; }
            .miz-attack > div { display: inline; }
            .maximate-float-btn, .maximate-fast-armed {
                position: fixed;
                right: 8px;
                z-index: 999999;
                padding: 7px 9px;
                border: 1px solid #777;
                background: #202020;
                color: #fff;
                border-radius: 4px;
                font-size: 12px;
                line-height: 1;
            }
            .maximate-float-btn { bottom: 78px; }
            .maximate-fast-armed { bottom: 114px; background: #5b1f1f; border-color: #c44; }
            .maximate-fast-armed.is-on { background: #245b1f; border-color: #5c5; }
            .maximate-toast {
                position: fixed;
                left: 50%;
                bottom: 72px;
                transform: translateX(-50%);
                z-index: 1000000;
                background: #202020;
                color: #fff;
                border: 1px solid #777;
                border-radius: 4px;
                padding: 8px 10px;
                font-size: 12px;
            }
            .maximate-settings-panel, .maximate-stat-popover {
                position: fixed;
                z-index: 1000000;
                background: var(--main-bg, #1f1f1f);
                color: var(--default-color, #fff);
                border: 1px solid #666;
                box-shadow: 0 6px 24px rgba(0,0,0,.45);
                border-radius: 6px;
            }
            .maximate-settings-panel {
                right: 8px;
                bottom: 118px;
                width: min(360px, calc(100vw - 16px));
                max-height: calc(100vh - 140px);
                overflow: auto;
                padding: 10px;
                font-size: 13px;
            }
            .maximate-settings-head, .maximate-settings-actions {
                display: flex;
                gap: 6px;
                align-items: center;
                justify-content: space-between;
                margin-bottom: 8px;
            }
            .maximate-settings-head button, .maximate-settings-actions button, .maximate-stat-popover button {
                border: 1px solid #777;
                background: #2a2a2a;
                color: #fff;
                border-radius: 4px;
                padding: 5px 7px;
            }
            .maximate-settings-status {
                display: grid;
                gap: 3px;
                margin-bottom: 8px;
                opacity: .95;
            }
            .maximate-settings-grid {
                display: grid;
                grid-template-columns: 1fr 1fr;
                gap: 6px;
                margin-bottom: 8px;
            }
            .maximate-settings-grid label, .maximate-cache-hours {
                display: flex;
                align-items: center;
                gap: 6px;
            }
            .maximate-cache-hours input { width: 60px; }
            .maximate-error { color: #ff8d8d; }
            .maximate-stat-popover {
                left: 10px;
                right: 10px;
                bottom: 76px;
                padding: 12px;
                font-size: 14px;
                text-align: left;
            }
            .maximate-stat-popover button {
                display: block;
                margin-top: 8px;
                width: 100%;
            }
            .maximate-actions {
                display: inline-flex;
                gap: 4px;
                margin-left: 5px;
                vertical-align: middle;
            }
            .maximate-actions button {
                border: 1px solid #777;
                background: #202020;
                color: #fff;
                border-radius: 4px;
                padding: 2px 5px;
                font-size: 11px;
            }
            .maximate-live-filter {
                margin: 8px 0;
                border: 1px solid rgba(255,255,255,.18);
                background: rgba(22,22,22,.88);
                color: #eee;
                border-radius: 6px;
                overflow: hidden;
                box-shadow: 0 2px 10px rgba(0,0,0,.25);
                font-size: 12px;
            }
            .maximate-live-filter .maximate-filter-title {
                display: flex;
                align-items: center;
                justify-content: space-between;
                gap: 8px;
                padding: 7px 9px;
                background: rgba(255,255,255,.07);
                cursor: pointer;
                font-weight: 700;
            }
            .maximate-live-filter .maximate-filter-count {
                opacity: .78;
                font-weight: 400;
                font-size: 11px;
            }
            .maximate-live-filter .maximate-filter-content {
                display: none;
                grid-template-columns: repeat(3, minmax(0, 1fr));
                gap: 8px;
                padding: 9px;
            }
            .maximate-live-filter.is-open .maximate-filter-content { display: grid; }
            .maximate-filter-group strong {
                display: block;
                margin-bottom: 5px;
                color: #f5f5f5;
                font-size: 11px;
                text-transform: uppercase;
                letter-spacing: .02em;
                opacity: .82;
            }
            .maximate-filter-options {
                display: flex;
                flex-wrap: wrap;
                gap: 5px;
            }
            .maximate-filter-options label {
                display: inline-flex;
                align-items: center;
                gap: 4px;
                border: 1px solid rgba(255,255,255,.18);
                background: rgba(255,255,255,.06);
                border-radius: 4px;
                padding: 4px 6px;
                line-height: 1;
                white-space: nowrap;
                cursor: pointer;
            }
            .maximate-filter-options label:has(input:checked) {
                border-color: #e15d3f;
                background: rgba(225,93,63,.22);
                color: #fff;
            }
            .maximate-filter-options input {
                width: 12px;
                height: 12px;
                margin: 0;
                accent-color: #e15d3f;
            }
            @media screen and (max-width: 760px) {
                .maximate-live-filter .maximate-filter-content {
                    grid-template-columns: 1fr;
                }
            }
            body.maximate-compact .members-list .bs,
            body.maximate-compact .chain-attacks-list .bs {
                width: 32px !important;
                min-width: 32px !important;
                font-size: 11px;
            }
        `);

        if (IS_MOBILE && settings.compactMobile) document.body.classList.add("maximate-compact");

        waitForElement(".header-menu .menu-items .menu-item-link a[href='/discord']").then((menuItem) => {
            menuItem.innerText = "Max";
            menuItem.onclick = showSettingsPanel;
        });

        const settingsButton = document.createElement("button");
        settingsButton.type = "button";
        settingsButton.className = "maximate-float-btn";
        settingsButton.textContent = API_KEY ? "War" : "War key";
        settingsButton.onclick = showSettingsPanel;
        document.body.appendChild(settingsButton);
    };

    const battlestatCache = {
        factionStamps: readJson("cache.ts", safeJson(localStorage["miz.cache.ts"], {})),
        battleStats: readJson("cache.bs", safeJson(localStorage["miz.cache.bs"], {})),

        save() {
            writeJson("cache.bs", this.battleStats);
            writeJson("cache.ts", this.factionStamps);
        },

        get(id) {
            return this.battleStats[String(id)];
        },

        clearAll() {
            this.factionStamps = {};
            this.battleStats = {};
            this.save();
        },

        async loadFaction(id, force = false) {
            if (!API_KEY || !id) return;
            if (!force && this.factionStamps[id] && this.factionStamps[id] + cacheLifetime() > Date.now()) return;

            try {
                const response = await requestText(`https://www.tornstats.com/api/v2/${API_KEY}/spy/faction/${id}`);
                const data = safeJson(response);
                if ((data && data.status === false) || (data && data.error)) {
                    apiStatus.tornStats = "Error";
                    apiStatus.lastError = data.message || (data.error && data.error.error) || "TornStats request failed";
                    return;
                }
                if (!data || !data.faction || !data.faction.members) {
                    apiStatus.tornStats = "No spy data";
                    return;
                }

                apiStatus.tornStats = "OK";
                this.factionStamps[id] = Date.now();
                Object.entries(data.faction.members).forEach(([userId, member]) => {
                    if (member && member.spy) this.battleStats[userId] = member.spy;
                });
                this.save();
            } catch (e) {
                apiStatus.tornStats = "Error";
                apiStatus.lastError = (e && e.message) || "TornStats request failed";
            }
        },

        init() {
            if (!settings.battlestats) return;

            watchElements("a[href^='/factions.php?step=profile&ID=']", async (facLink) => {
                const href = facLink.getAttribute("href");
                const factionId = href && href.replace(/.*?ID=(\d+).*$/, "$1");
                await this.loadFaction(factionId);
            });

            if (getPage() !== "faction") return;

            waitForElement("#top-page-links-list").then((elm) => {
                if (elm.querySelector(".miz-clear-bs-cache")) return;

                const clearCacheButton = document.createElement("a");
                clearCacheButton.href = "#";
                clearCacheButton.className = "miz-clear-bs-cache right t-clear h c-pointer line-h24";
                clearCacheButton.innerHTML = `<span class="icon-wrap svg-icon-wrap"><span class="link-icon-svg"></span></span><span>Clear BS Cache</span>`;
                clearCacheButton.onclick = () => {
                    const facId = window.location.href.replace(/.*?ID=(\d+).*$/, "$1");
                    delete this.factionStamps[facId];
                    this.save();
                    window.location.reload();
                    return false;
                };
                elm.append(clearCacheButton);
            });
        }
    };

    const abbreviateStat = (value, total = false) => {
        let stat = Number.parseInt(value, 10);
        if (!Number.isFinite(stat) || stat === 0) return "N/A";

        const unknown = stat < 0;
        if (unknown) stat = Math.abs(stat);

        const units = ["K", "M", "B", "T", "Q"];
        for (const unit of units) {
            stat /= 1000;
            if (stat > 1000) continue;

            const digits = total ? (stat >= 100 ? 0 : 1) : 2;
            return `${stat.toFixed(digits)}${unit}${unknown ? "+" : ""}`;
        }

        return `${stat.toFixed(1)}Q${unknown ? "+" : ""}`;
    };

    const showBattlestats = (id, node) => {
        if (!id || !node) return;
        if (!settings.battlestats) return;

        if (!API_KEY) {
            node.innerHTML = "No key";
            node.title = "No TornStats API key saved";
            return;
        }

        const spy = battlestatCache.get(id);
        if (!spy) {
            node.innerHTML = apiStatus.tornStats === "Error" ? "API err" : "No spy";
            node.title = apiStatus.lastError || "No cached spy data for this player";
            setTimeout(() => showBattlestats(id, node), 5000);
            return;
        }

        const rawTotal = spy.total > 0
            ? spy.total
            : -(Number(spy.strength || 0) + Number(spy.defense || 0) + Number(spy.speed || 0) + Number(spy.dexterity || 0));

        const stats = {
            total: abbreviateStat(rawTotal, true),
            strength: abbreviateStat(spy.strength),
            defense: abbreviateStat(spy.defense),
            speed: abbreviateStat(spy.speed),
            dexterity: abbreviateStat(spy.dexterity)
        };

        let age = "";
        const difference = (Date.now() / 1000) - Number(spy.timestamp || 0);
        if (spy.timestamp) {
            if (difference > 365 * 24 * 60 * 60) age = `${Math.floor(difference / (365 * 24 * 60 * 60))} years ago`;
            else if (difference > 30 * 24 * 60 * 60) age = `${Math.floor(difference / (30 * 24 * 60 * 60))} months ago`;
            else if (difference > 24 * 60 * 60) age = `${Math.floor(difference / (24 * 60 * 60))} days ago`;
            else if (difference > 60 * 60) age = `${Math.floor(difference / (60 * 60))} hours ago`;
            else if (difference > 60) age = `${Math.floor(difference / 60)} minutes ago`;
            else age = `${Math.floor(difference)} seconds ago`;
        }

        const statHtml = `
            <div class="finally-bs-stat">
                <b>STR</b> <span>${stats.strength}</span><br/>
                <b>DEF</b> <span>${stats.defense}</span><br/>
                <b>SPD</b> <span>${stats.speed}</span><br/>
                <b>DEX</b> <span>${stats.dexterity}</span><br/>
                <hr/>
                <b>TOT</b> <span class="total">${stats.total}</span><br/>
                ${age}
            </div>`;
        node.innerHTML = stats.total;
        node.title = statHtml;
        node.dataset.statHtml = statHtml;
        node.onclick = (evt) => {
            if (!IS_MOBILE || !settings.mobileTapStats) return;
            evt.stopPropagation();
            evt.preventDefault();
            renderStatPopup(node);
        };
    };

    const enableAttack = () => {
        watchElements(".profile-button-attack", (elm) => {
            elm.classList.remove("disabled");
            const attackHref = elm.getAttribute("href");
            elm.href = (attackHref && attackHref.replace("loader2.php?sid=getInAttack&user2ID=", "loader.php?sid=attack&user2ID=")) || elm.href;
            elm.onclick = () => {
                window.location = elm.href;
                return false;
            };

            const miniProfile = elm.closest(".profile-mini-root");
            const target = miniProfile && miniProfile.querySelector(".main-desc");
            if (!target) return;

            const id = elm.href.replace(/.*?user2ID=(\d+)/i, "$1");
            const bsNode = document.createElement("span");
            bsNode.className = "miz-bs-miniprofile";
            target.appendChild(bsNode);
            showBattlestats(id, bsNode);

            if (typeof jQuery !== "undefined") {
                jQuery(bsNode).tooltip({
                    tooltipClass: "white-tooltip miz-max-z",
                    position: {
                        my: "center bottom-20",
                        at: "center top"
                    }
                });
            }
        });
    };

    const checkWallAndHosp = () => {
        if (!settings.wallHosp) return;

        let hospUntil = 0;
        let wasInHosp = false;
        let wasOnWall = false;
        const userId = getUserId();
        if (!userId || !API_KEY) return;

        const updateHospTime = () => {
            const span = document.getElementById("hosp-status");
            if (!span || !wasInHosp) return;

            const time = formatTime(hospUntil - (Date.now() / 1000));
            if (time === null) {
                span.innerHTML = "OUT OF HOSPITAL";
                document.title = "OUT";
                const row = span.parentElement && span.parentElement.parentElement;
                if (row) {
                    row.className = row.className.replace(" red", " green");
                    row.classList.add("miz-refresh");
                }
            } else {
                span.innerHTML = time;
            }
        };

        const checkStatus = async () => {
            const wallSpan = document.getElementById("wall-status");
            if (!wallSpan) return;

            let data = null;
            try {
                const response = await requestText(`https://api.torn.com/user/${userId}?selections=basic,icons&key=${API_KEY}`);
                data = safeJson(response);
            } catch (e) {
                apiStatus.torn = "Error";
                apiStatus.lastError = (e && e.message) || "Torn API request failed";
                return;
            }
            if (!data || data.error) {
                apiStatus.torn = "Error";
                apiStatus.lastError = (data && data.error && data.error.error) || "Torn API request failed";
                return;
            }
            apiStatus.torn = "OK";

            const icon75 = data.icons && data.icons.icon75;
            const icon76 = data.icons && data.icons.icon76;
            const wallIcon = icon75 || icon76 || "";

            if (wallIcon) {
                const wallName = wallIcon.substring(Math.max(0, wallIcon.length - 3));
                wasOnWall = true;
                wallSpan.style.color = "red";
                wallSpan.innerHTML = `ON WALL - <a style="text-decoration: none; color: red;" href="/city.php#terrName=${wallName}">${wallName}</a>`;
            } else if (wasOnWall) {
                wallSpan.style.color = "red";
                wallSpan.innerHTML = "ON WALL";
            } else {
                wallSpan.style.color = "green";
                wallSpan.innerHTML = "OFF WALL";
            }

            if (data.status && data.status.state === "Hospital") {
                hospUntil = Number(data.status.until || 0);
                wasInHosp = true;
            } else if (wasInHosp) {
                hospUntil = 0;
            }
        };

        waitForElement("#defender").then(() => {
            const titleH4 = document.querySelector("h4[class^=title___]");
            const titleDiv = document.querySelector("div[class^=title___]");
            if (titleH4) titleH4.insertAdjacentHTML("afterend", '<span id="wall-status"></span>');
            if (titleDiv) titleDiv.insertAdjacentHTML("beforeend", '<span id="hosp-status"></span>');
            const hospSpan = document.getElementById("hosp-status");
            if (hospSpan) hospSpan.onclick = () => window.location.reload();

            checkStatus();
            setInterval(updateHospTime, 500);
            setInterval(checkStatus, 6000);
        });
    };

    const checkExecute = () => {
        if (!settings.executeHelper) return;

        waitForElement("#attacker .bonus-attachment-execute").then((elm) => {
            const title = elm.getAttribute("title");
            const percent = Number.parseFloat(title && title.replace(/.*\s([0-9]+)%.*/i, "$1"));
            const targetHealthPercent = Number.isFinite(percent) ? percent / 100 : null;
            if (!targetHealthPercent) return;

            setInterval(() => {
                const healthNode = document.querySelectorAll("[id^=player-health-value]")[1];
                const health = healthNode && healthNode.innerText.split("/").map((x) => Number(x.replace(/,/g, "")));
                if (!health || !health[1]) return;
                if (health[0] / health[1] <= targetHealthPercent) {
                    const second = document.getElementById("weapon_second");
                    if (second) second.style.background = "red";
                }
            }, 500);
        });
    };

    const fastAttack = () => {
        if (!settings.fastAttack) return;

        let loading = false;
        let armed = false;
        const armedButton = document.createElement("button");
        armedButton.type = "button";
        armedButton.className = "maximate-fast-armed";
        armedButton.textContent = "Fast off";
        armedButton.onclick = () => {
            armed = !armed;
            armedButton.classList.toggle("is-on", armed);
            armedButton.textContent = armed ? "Fast on" : "Fast off";
        };
        document.body.appendChild(armedButton);

        waitForElement("#defender").then((elm) => {
            const buttonDiv = elm.querySelector("div[class^=dialogButtons_]");
            document.querySelectorAll("#weapon_main, #weapon_second, #weapon_melee, #weapon_temp, #weapon_fists, #weapon_boot").forEach((btn) => {
                btn.addEventListener("click", (evt) => {
                    const attack = buttonDiv && buttonDiv.querySelector(".torn-btn");
                    const label = (attack && attack.innerText.toLowerCase()) || "";
                    if (!armed) return;
                    if (!loading && attack && !evt.target.className.includes("ammo") && (label.startsWith("start fight") || label.startsWith("join"))) {
                        loading = true;
                        attack.click();
                    } else if (!loading && (elm.querySelector("[class^=dialog_] [class*=' red__']") || elm.querySelector("[class^=dialog_] .miz-refresh"))) {
                        loading = true;
                        window.location.reload();
                    }
                });
            });
        });
    };

    const nameLink = () => {
        const userId = getUserId();
        if (!userId) return;

        waitForElement("#defender [class^='userName___']").then((elm) => {
            const name = elm.innerText;
            elm.innerHTML = `<a style="color: var(--attack-header-text-color); text-decoration: none;" href="/profiles.php?XID=${userId}">${name}</a>&nbsp;<span class="miz-stats"></span>`;
            const bsNode = elm.querySelector(".miz-stats");
            showBattlestats(userId, bsNode);
            bsNode.onclick = () => {
                const facChat = document.querySelector("div[class*='_faction_'] textarea");
                if (!facChat) return;
                const hospStatus = document.getElementById("hosp-status");
                facChat.value = `https://www.torn.com/loader.php?sid=attack&user2ID=${userId} - ${bsNode.innerText}: ${(hospStatus && hospStatus.innerText) || ""}`;
                facChat.focus();
            };
        });
    };

    const targetQuickActions = () => {
        if (!settings.quickActions) return;

        const userId = getUserId();
        if (!userId) return;

        waitForElement("#defender [class^='userName___']").then((elm) => {
            if (elm.querySelector(".maximate-actions")) return;

            const attackUrl = `https://www.torn.com/loader.php?sid=attack&user2ID=${userId}`;
            const profileUrl = `https://www.torn.com/profiles.php?XID=${userId}`;
            const actions = document.createElement("span");
            actions.className = "maximate-actions";
            actions.innerHTML = `
                <button type="button" data-action="profile">Profile</button>
                <button type="button" data-action="attack">Attack</button>
                <button type="button" data-action="copy">Copy</button>
                <button type="button" data-action="chat">Chat</button>
            `;
            actions.addEventListener("click", (evt) => {
                evt.stopPropagation();
                evt.preventDefault();

                const action = evt.target.dataset.action;
                if (action === "profile") window.location.href = profileUrl;
                if (action === "attack") window.location.href = attackUrl;
                if (action === "copy") copyText(attackUrl);
                if (action === "chat") {
                    const facChat = document.querySelector("div[class*='_faction_'] textarea");
                    if (!facChat) {
                        copyText(attackUrl);
                        return;
                    }
                    const statsNode = elm.querySelector(".miz-stats");
                    const hospNode = document.getElementById("hosp-status");
                    const stats = (statsNode && statsNode.innerText) || "";
                    const hosp = (hospNode && hospNode.innerText) || "";
                    facChat.value = `${attackUrl}${stats ? ` - ${stats}` : ""}${hosp ? `: ${hosp}` : ""}`;
                    facChat.focus();
                }
            });

            elm.appendChild(actions);
        });
    };

    const clickableAssists = () => {
        if (!settings.clickableAssists) return;

        const oldFetch = pageWindow.fetch;
        const userId = getUserId();
        let attackers = {};

        if (!oldFetch) return;

        pageWindow.fetch = async function (...args) {
            const url = String(args[0]);
            const response = await oldFetch.apply(this, args);
            if (!/sid=attackData&mode=json&step=poll/.test(url)) return response;

            response.clone().json().then((body) => {
                if (!body || !body.DB || !body.DB.currentFightStatistics) return;
                attackers = Object.fromEntries(
                    Object.entries(body.DB.currentFightStatistics).map(([id, value]) => [value.playername, Number.parseInt(id, 10)])
                );
            }).catch(() => {});

            return response;
        };

        const addLinks = (elm) => {
            const startText = elm.innerText;
            for (const [name, id] of Object.entries(attackers)) {
                if (String(id) === String(userId)) continue;

                const bsNode = document.createElement("div");
                showBattlestats(id, bsNode);
                const newText = elm.innerText.replace(name, `<a class="miz-name" href="/profiles.php?XID=${id}">${name}</a>&nbsp;<a href="/loader.php?sid=attack&user2ID=${id}" class="miz-attack">${bsNode.outerHTML}</a>`);
                if (newText !== startText) {
                    elm.innerHTML = newText;
                    return true;
                }
            }
            return false;
        };

        watchElements("#react-root ul[class^='participants'] > li, #react-root ul[aria-describedby='log-header'] > li", (elm) => {
            const name = elm.querySelector("[class^='playername']") || elm.querySelector("span[class^='message'] > span");
            if (!name) return true;
            return !addLinks(name);
        });
    };

    const addLogIconTitles = () => {
        watchElements("[class^='attacking-events-']", (elm) => {
            elm.title = elm.className.replace("attacking-events-", "");
        });
    };

    const wallTimers = () => {
        if (!settings.wallTimers) return;

        const updateTimer = (elm) => {
            const ourCountNode = elm.querySelector("div.member-count.your-count > div.count");
            const theirCountNode = elm.querySelector("div.member-count.enemy-count > div.count");
            const defendingIcon = elm.querySelector("div.member-count.your-count > div.count > i");
            const timerTextNode = elm.querySelector(".timer");
            const scoreNode = elm.querySelector(".score");
            const ourCount = Number.parseInt(ourCountNode && ourCountNode.innerText, 10);
            const theirCount = Number.parseInt(theirCountNode && theirCountNode.innerText, 10);
            const defending = !!(defendingIcon && defendingIcon.classList.contains("shield-icon"));
            const timerText = (timerTextNode && timerTextNode.innerText) || "";
            const timerArray = timerText.split(":").map(Number);
            const score = (scoreNode && scoreNode.innerText.replace(/,/g, "")) || "";
            const match = score.match(/(\d+) ?\/ ?(\d+)/);
            const timerNode = elm.querySelector(".miz-wall-timer");

            if (!timerNode || timerArray.length < 4 || !match || !Number.isFinite(ourCount) || !Number.isFinite(theirCount)) return;

            const timeLeft = timerArray[0] * 86400 + timerArray[1] * 3600 + timerArray[2] * 60 + timerArray[3];
            const scoreCurrent = Number(match[2]) - Number(match[1]);
            const slotsMax = Number(match[2]) * 2 / 100000;
            const diffCount = Math.abs(ourCount - theirCount);
            const durationMin = Math.ceil(scoreCurrent / slotsMax);
            const successCurrent = diffCount ? Math.ceil(scoreCurrent / diffCount) : Infinity;

            let textCurrent = "";
            if (defending) {
                if (durationMin > timeLeft) textCurrent = "WON";
                else textCurrent = ourCount >= theirCount ? `WIN ${formatTime(timeLeft - durationMin)}` : `LOSE ${formatTime(successCurrent) || "&infin;"}`;
            } else if (durationMin > timeLeft) {
                textCurrent = "LOST";
            } else {
                textCurrent = ourCount > theirCount ? `WIN ${formatTime(successCurrent) || "&infin;"}` : `LOSE ${formatTime(timeLeft - durationMin)}`;
            }

            timerNode.innerHTML = textCurrent;
            timerNode.title = durationMin < 0 ? `FAILED: ${formatTime(Math.abs(durationMin))}` : `MIN: ${formatTime(durationMin)}`;
        };

        watchElements("[class^='status-wrap territoryBox']", (elm) => {
            const timerNode = elm.querySelector(".timer");
            if (timerNode) timerNode.insertAdjacentHTML("afterend", '<div class="miz-wall-timer">MWT</div>');
            updateTimer(elm);
            setInterval(() => updateTimer(elm), 1000);
        });
    };

    const factionBattlestats = () => {
        if (!settings.battlestats) return;

        const previousSort = Number.parseInt(readStorage("faction.sort", localStorage.getItem("miz.faction.sort") || ""), 10) || undefined;

        const sortStats = (node, sort) => {
            if (!node) return;

            const sortIcon = node.parentNode.querySelector(".bs > [class*='sortIcon']");
            if (sort) node.finallySort = sort;
            else if (node.finallySort === undefined) node.finallySort = 2;
            else if (++node.finallySort > 2) node.finallySort = sortIcon ? 1 : 0;

            if (sortIcon) {
                sortIcon.classList.toggle("finally-bs-activeIcon", node.finallySort > 0);
                sortIcon.classList.toggle("finally-bs-asc", node.finallySort === 1);
                sortIcon.classList.toggle("finally-bs-desc", node.finallySort === 2);
            }

            const rows = Array.from(node.querySelectorAll(".table-body > .table-row, .your:not(.row-animation-new), .enemy:not(.row-animation-new)"));
            rows.forEach((row, index) => {
                if (row.finallyPos === undefined) row.finallyPos = index;
            });

            rows.sort((a, b) => {
                const linkA = a.querySelector('a[href*="XID"]');
                const linkB = b.querySelector('a[href*="XID"]');
                const idA = linkA && linkA.href.replace(/.*?XID=(\d+)/i, "$1");
                const idB = linkB && linkB.href.replace(/.*?XID=(\d+)/i, "$1");
                const statsA = battlestatCache.get(idA);
                const statsB = battlestatCache.get(idB);
                const totalA = (statsA && statsA.total) || a.finallyPos;
                const totalB = (statsB && statsB.total) || b.finallyPos;

                if (node.finallySort === 1) {
                    if (totalA <= 100) return 1;
                    if (totalB <= 100) return -1;
                    return totalA > totalB ? 1 : -1;
                }
                if (node.finallySort === 2) return totalB > totalA ? 1 : -1;
                return a.finallyPos > b.finallyPos ? 1 : -1;
            }).forEach((row) => row.parentNode.appendChild(row));
        };

        const addHeader = (elm) => {
            const titleNode = elm.querySelector(".table-header") || (elm.parentNode && elm.parentNode.querySelector(".title, .c-pointer"));
            if (!titleNode || titleNode.querySelector(".bs")) return;

            const lvNode = titleNode.querySelector(".level");
            if (lvNode && lvNode.childNodes[0]) lvNode.childNodes[0].nodeValue = "Lv";

            const bsNode = lvNode ? lvNode.cloneNode(true) : document.createElement("li");
            bsNode.classList.add("bs");
            if (bsNode.childNodes[0]) bsNode.childNodes[0].nodeValue = "BS";
            else bsNode.innerHTML = "BS";
            titleNode.insertBefore(bsNode, titleNode.querySelector(".user-icons, .points, .member-icons"));
            bsNode.addEventListener("click", () => sortStats(elm));

            Array.from(titleNode.children).forEach((child, index) => {
                child.addEventListener("click", (e) => {
                    setTimeout(() => {
                        const sortIcon = e.target.querySelector("[class*='sortIcon']");
                        const desc = !sortIcon || sortIcon.className.indexOf("desc") === -1;
                        writeStorage("faction.sort", desc ? index + 1 : -(index + 1));
                    }, 100);
                });
            });

            if (Math.abs(previousSort) === 3) sortStats(elm, previousSort > 0 ? 2 : 1);
        };

        const showStats = (elm) => {
            const idElms = elm.querySelectorAll('a[href*="XID"]');
            if (!idElms.length) return;

            const id = idElms[idElms.length - 1].href.replace(/.*?XID=(\d+)/i, "$1");
            let bsNode = elm.querySelector(".bs");

            if (!bsNode) {
                bsNode = document.createElement("div");
                bsNode.className = "table-cell bs level lvl left iconShow finally-bs-col";
                const iconsNode = elm.querySelector(".user-icons, .member-icons, .points, .respect");
                if (!iconsNode) return;
                iconsNode.parentNode.insertBefore(bsNode, iconsNode);
                bsNode.classList.add("miz-recent-attacks");
                bsNode.addEventListener("click", () => window.open(`loader.php?sid=attack&user2ID=${id}`, "_blank"));
                bsNode.addEventListener("dblclick", () => window.open(`loader.php?sid=attack&user2ID=${id}`, "_blank"));
            }

            showBattlestats(id, bsNode);

            const onlineStatusNodes = elm.querySelectorAll("div[class^='userStatusWrap'], div[class*=' userStatusWrap'], .member.icons li.iconShow");
            const onlineStatusNode = onlineStatusNodes[onlineStatusNodes.length - 1];
            if (onlineStatusNode) onlineStatusNode.addEventListener("click", () => {
                const facChat = document.querySelector("div[class*='_faction_'] textarea");
                if (!facChat) return;
                facChat.value = `https://www.torn.com/loader.php?sid=attack&user2ID=${id} - ${bsNode.innerText}`;
                facChat.focus();
            });
        };

        watchElements(".members-list, ul.chain-attacks-list.recent-attacks, ul.chain-attacks-list.current-attacks", (elm) => {
            addHeader(elm);
            watchElements(".your, .enemy, .table-body > .table-row, :scope > li[class='']", showStats, elm);
        });
    };

    const liveMemberFilter = () => {
        if (!settings.liveFilter) return;

        const selected = (ids) => ids.filter((id) => document.querySelector(`#${id}:checked`)).map((id) => id.replace("miz-", ""));
        const isChecked = (id) => (readStorage(`filter-${id}`, localStorage[`filter-${id}`] || "") === "true") ? ' checked="checked"' : "";
        const checkedCount = () => document.querySelectorAll(".maximate-live-filter input[type=checkbox]:checked").length;

        const filterRow = (row) => {
            if (!row) return;

            const states = selected(["miz-online", "miz-offline", "miz-idle"]);
            const wallStates = selected(["miz-walloff", "miz-walldef", "miz-wallass"]);
            const statuses = selected(["miz-okay", "miz-hospital", "miz-jail", "miz-abroad", "miz-traveling"]);

            const stateCell = row.querySelector("[class*=userStatusWrap__]");
            const state = stateCell && stateCell.id.includes("online") ? "online"
                : stateCell && stateCell.id.includes("offline") ? "offline"
                : stateCell && stateCell.id.includes("idle") ? "idle"
                : null;

            const iconCell = row.querySelector(".table-cell.member-icons.icons");
            const wallState = !(iconCell && iconCell.querySelector("[id^=icon75], [id^=icon76]")) ? "walloff"
                : iconCell.querySelector("[id^=icon75]") ? "walldef"
                : "wallass";

            const statusCell = row.querySelector(".table-cell.status span");
            const status = ["okay", "hospital", "jail", "abroad", "traveling"].find((x) => statusCell && statusCell.classList.contains(x));

            row.style.display = (!states.length || states.includes(state))
                && (!wallStates.length || wallStates.includes(wallState))
                && (!statuses.length || statuses.includes(status))
                ? "flex"
                : "none";
        };

        const filterRows = () => document.querySelectorAll(".members-list > ul.table-body > li.table-row").forEach(filterRow);
        const updateFilterCount = () => {
            const countNode = document.querySelector(".maximate-filter-count");
            if (!countNode) return;
            const count = checkedCount();
            countNode.textContent = count ? `${count} active` : "No filters";
        };

        waitForElement(".faction-info-wrap .members-list").then((elm) => {
            if (document.querySelector(".miz-live-filter")) return;

            const filterDiv = document.createElement("div");
            filterDiv.className = "miz-live-filter maximate-live-filter is-open";
            filterDiv.innerHTML = `
                <div class="maximate-filter-title">
                    <span>War Live Filter</span>
                    <span class="maximate-filter-count">No filters</span>
                </div>
                <div class="maximate-filter-content">
                    <div class="maximate-filter-group">
                        <strong>Activity</strong>
                        <div class="maximate-filter-options">
                            <label><input id="miz-online" type="checkbox"${isChecked("miz-online")}>Online</label>
                            <label><input id="miz-idle" type="checkbox"${isChecked("miz-idle")}>Idle</label>
                            <label><input id="miz-offline" type="checkbox"${isChecked("miz-offline")}>Offline</label>
                        </div>
                    </div>
                    <div class="maximate-filter-group">
                        <strong>Wall Status</strong>
                        <div class="maximate-filter-options">
                            <label><input id="miz-walloff" type="checkbox"${isChecked("miz-walloff")}>Off wall</label>
                            <label><input id="miz-walldef" type="checkbox"${isChecked("miz-walldef")}>Defending</label>
                            <label><input id="miz-wallass" type="checkbox"${isChecked("miz-wallass")}>Assaulting</label>
                        </div>
                    </div>
                    <div class="maximate-filter-group">
                        <strong>Status</strong>
                        <div class="maximate-filter-options">
                            <label><input id="miz-okay" type="checkbox"${isChecked("miz-okay")}>Okay</label>
                            <label><input id="miz-hospital" type="checkbox"${isChecked("miz-hospital")}>Hospital</label>
                            <label><input id="miz-jail" type="checkbox"${isChecked("miz-jail")}>Jail</label>
                            <label><input id="miz-abroad" type="checkbox"${isChecked("miz-abroad")}>Abroad</label>
                            <label><input id="miz-traveling" type="checkbox"${isChecked("miz-traveling")}>Traveling</label>
                        </div>
                    </div>
                </div>`;

            elm.before(filterDiv);

            const observer = new MutationObserver((mutations) => {
                const target = mutations[0] && mutations[0].target;
                const row = target && target.closest && target.closest(".table-row");
                filterRow(row);
            });

            elm.querySelectorAll(".table-row").forEach((node) => {
                observer.observe(node, { attributes: true, childList: true, subtree: true });
            });

            filterDiv.querySelectorAll("input[type=checkbox]").forEach((node) => {
                node.addEventListener("change", (evt) => {
                    writeStorage(`filter-${evt.target.id}`, evt.target.checked);
                    filterRows();
                    updateFilterCount();
                });
            });

            filterDiv.querySelector(".maximate-filter-title").onclick = () => {
                filterDiv.classList.toggle("is-open");
            };

            filterRows();
            updateFilterCount();
        });
    };

    const init = () => {
        if (initialized) return;
        initialized = true;

        battlestatCache.init();
        enableAttack();

        switch (getPage()) {
            case "loader":
                checkWallAndHosp();
                checkExecute();
                nameLink();
                targetQuickActions();
                clickableAssists();
                fastAttack();
                break;
            case "attack-log":
                addLogIconTitles();
                break;
            case "faction":
                wallTimers();
                factionBattlestats();
                liveMemberFilter();
                break;
            default:
                break;
        }
    };

    injectInitialHtml();
    init();
})();