Attack Button on User by Pankace

Places visible attack icon next to usernames

// ==UserScript==
// @name         Attack Button on User by Pankace
// @namespace    http://tampermonkey.net/
// @author       You
// @version      0.2.5
// @description  Places visible attack icon next to usernames
// @match        https://www.torn.com/factions.php*
// @match        https://www.torn.com/preferences.php*
// @match        https://www.torn.com/profiles.php*
// @match        https://www.torn.com/page.php?sid=UserList*
// @grant        GM.xmlHttpRequest
// @run-at       document-end
// @license      WTFPL
// ==/UserScript==

console.log("AttackButton script loaded");

(function(){
    'use strict';

    // Allowed container selectors where we want icons (userlist, factions, profile areas).
    // These are intentionally specific to avoid touching chat widgets.
    const ALLOWED_CONTAINERS = [
        'div.userlist-wrapper', // the user list/search page
        'div#factions', // factions page
        'div#profileroot', // profile pages
        'div.profile-wrapper', // profile wrapper (covers small/large profile)
        'ul.user-info-list-wrap'// user list inner container
    ];

    function getUserIdFromHref(href){
        if(!href || typeof href !== 'string') return null;
        const m = href.match(/[?&](?:XID|userID|user2ID|user)=(\d+)/i);
        return m ? m[1] : null;
    }

    // Check whether element is inside any allowed container
    function isInsideAllowedContainer(el){
        if(!el) return false;
        try {
            for(const sel of ALLOWED_CONTAINERS){
                if(el.closest && el.closest(sel)) return true;
            }
        } catch(e){}
        return false;
    }

    function cleanupOldIcons(){
        try {
            document.querySelectorAll('[data-pank-attack="1"]').forEach(n => {
                if(!n.getAttribute('data-pank-attack-user')) n.remove();
            });
        } catch(e){}
    }

    function buildOverlayIcon(userId){
        const el = document.createElement('span');
        el.setAttribute('data-pank-attack', '1');
        el.setAttribute('data-pank-attack-user', userId);
        el.setAttribute('role', 'button');
        el.setAttribute('tabindex', '0');
        el.title = `Attack user ${userId}`;

        el.style.position = 'absolute';
        el.style.right = '6px';
        el.style.top = '50%';
        el.style.transform = 'translateY(-50%)';
        el.style.zIndex = '9999';
        el.style.display = 'inline-flex';
        el.style.alignItems = 'center';
        el.style.justifyContent = 'center';
        el.style.minWidth = '24px';
        el.style.height = '24px';
        el.style.padding = '0 6px';
        el.style.borderRadius = '4px';
        el.style.fontSize = '14px';
        el.style.lineHeight = '1';
        el.style.cursor = 'pointer';
        el.style.userSelect = 'none';
        el.style.background = 'rgba(255,0,0,0.06)';
        el.style.border = '1px solid rgba(255,0,0,0.12)';
        el.style.color = 'currentColor';
        el.style.boxSizing = 'border-box';
        el.innerText = '🎯';
        el.setAttribute('aria-label', `Attack user ${userId}`);

        el.addEventListener('click', function(ev) {
    ev.preventDefault();
    ev.stopPropagation();
    window.open(`/loader.php?sid=attack&user2ID=${userId}`, '_blank');
}, {capture: true});

el.addEventListener('auxclick', function(ev) {
    if (ev.button === 1) { // middle click
        ev.preventDefault();
        ev.stopPropagation();
        window.open(`/loader.php?sid=attack&user2ID=${userId}`, '_blank');
    }
}, {capture: true});

el.addEventListener('keydown', function(ev){
    if(ev.key === 'Enter' || ev.key === ' '){
        ev.preventDefault();
        ev.stopPropagation();
        window.open(`/loader.php?sid=attack&user2ID=${userId}`, '_blank');
    }
});


        return el;
    }

    // Add one overlay icon inside the anchor (absolute positioned), but only if the anchor is inside an allowed container.
    function ensureOverlayIcon(anchor){
        if(!anchor || anchor.nodeType !== 1) return;

        if(!isInsideAllowedContainer(anchor)) return; // IMPORTANT: don't touch elements outside allowed areas

        // skip avatar-only anchors (they are not username anchors)
        if(anchor.getAttribute && anchor.getAttribute('data-label') === 'avatar') return;

        const id = getUserIdFromHref(anchor.href) || (anchor.dataset && (anchor.dataset.userid || anchor.dataset.userId || anchor.dataset.xid));
        if(!id) return;

        if(anchor.getAttribute('data-pank-processed') === '1') {
            const existing = anchor.querySelector('[data-pank-attack="1"]');
            if(existing && existing.getAttribute('data-pank-attack-user') === id) return;
            anchor.querySelectorAll('[data-pank-attack="1"]').forEach(n => n.remove());
        }

        anchor.setAttribute('data-pank-processed', '1');

        try {
            const cs = window.getComputedStyle(anchor);
            if(!cs || cs.position === 'static') {
                anchor.style.position = 'relative';
            }
        } catch(e){
            try { anchor.style.position = 'relative'; } catch(e){}
        }

        anchor.querySelectorAll('[data-pank-attack="1"]').forEach(n => n.remove());

        const icon = buildOverlayIcon(id);
        try {
            anchor.appendChild(icon);
        } catch(e){
            try { anchor.insertAdjacentElement('afterend', icon); } catch(e){}
        }
    }

    // Process anchors inside a given root/subtree but only if they are inside allowed containers
    function processRoot(root){
        root = root || document;
        const selector = [
            'a[href*="profiles.php?XID="]',
            'a[href*="profiles.php?userID="]',
            'a[href*="profiles.php?user2ID="]',
            'a[href*="profiles.php?user="]',
            'a[data-userid]',
            'a[data-user-id]'
        ].join(',');
        let anchors = [];
        try {
            anchors = root.querySelectorAll ? root.querySelectorAll(selector) : [];
        } catch(e) { anchors = []; }

        for(const a of anchors){
            // ensure it's inside one of the allowed containers (prevents chat)
            if(!isInsideAllowedContainer(a)) continue;
            ensureOverlayIcon(a);
        }
    }

    // Initial setup: clean and run on each allowed container present
    cleanupOldIcons();
    for(const sel of ALLOWED_CONTAINERS){
        try {
            const node = document.querySelector(sel);
            if(node) processRoot(node);
        } catch(e){}
    }
    // also a full-scan fallback (only processes anchors inside allowed containers due to guard)
    processRoot(document);

    // Observe dynamic changes: only process nodes that are inside allowed containers
    const observer = new MutationObserver((mutations)=>{
        for(const m of mutations){
            for(const n of m.addedNodes){
                if(n.nodeType !== 1) continue;
                // If the added node is inside any allowed container - or itself matches a container - process it
                let shouldProcess = false;
                try {
                    for(const sel of ALLOWED_CONTAINERS){
                        if(n.matches && n.matches(sel)) { shouldProcess = true; break; }
                        if(n.closest && n.closest(sel)) { shouldProcess = true; break; }
                    }
                } catch(e){}
                if(shouldProcess) processRoot(n);
            }
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

})();