NPO FF

Friendly Fire Protection in Browser and PDA

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         NPO FF
// @namespace    https://www.torn.com/profiles.php?XID=3833584
// @version      2025-09-29
// @description  Friendly Fire Protection in Browser and PDA
// @author       -Thelemite [3833584]
// @match        https://www.torn.com/profiles.php*
// @match        https://torn.com/profiles.php*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @run-at       document-end
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const Allies = [
        { id: "10610", name: "NPO - Strength" },        // -- INDEX 0
        { id: "44758", name: "NPO - Prosperity" },      // -- INDEX 1
        { id: "12645", name: "NPO - Endurance" },       // -- INDEX 2
        { id: "14052", name: "NPO - Serenity" },        // -- INDEX 3
        { id: "18714", name: "NPO - Peace" },           // -- INDEX 4
        { id: "26885", name: "NPO - Valour" },          // -- INDEX 5

        { id: "19", name: "39th Street Killers" },      // -- INDEX 6
        { id: "16312", name: "39th Street Killers X" }, // -- INDEX 7
        { id: "7049", name: "39th Street Healers" },    // -- INDEX 8
        { id: "22680", name: "39th Street Reapers" },   // -- INDEX 9
        { id: "31764", name: "39th Street Warriors" },  // -- INDEX 10
        { id: "36691", name: "Rabid Chihuahuas" },      // -- INDEX 11
        { id: "11162", name: "InQuest" },               // -- INDEX 12
        { id: "7197", name: "HeLa" },                   // -- INDEX 13
        { id: "30009", name: "White Rabbits" }          // -- INDEX 14
    ];

    // Utility: wait for a selector to appear (handles PDA late DOM)
    function waitForSelector(selector, { root = document, timeout = 10000 } = {}) {
        return new Promise((resolve) => {
            const el = root.querySelector(selector);
            if (el) return resolve(el);

            const obs = new MutationObserver(() => {
                const found = root.querySelector(selector);
                if (found) { obs.disconnect(); resolve(found); }
            });

            obs.observe(root.documentElement || root, { childList: true, subtree: true });

            if (timeout > 0) {
                setTimeout(() => { obs.disconnect(); resolve(null); }, timeout);
            }
        });
    }

    // Extract factionId from /factions.php?step=profile&ID=... links
    function getFactionId() {
        const anchors = document.querySelectorAll('a[href*="/factions.php?step=profile&ID="]');
        for (const a of anchors) {
            try {
                const url = new URL(a.getAttribute('href'), location.href);
                if (url.pathname.endsWith('/factions.php') && url.searchParams.get('step') === 'profile') {
                    const id = url.searchParams.get('ID');
                    if (id) return String(id).trim();
                }
            } catch { /* ignore malformed hrefs */ }
        }
        return null;
    }

    // (Optional) userId, if you need it later
    function getUserIdFromAttackBtn(btn) {
        const id = btn?.id ?? '';
        const parts = id.split('-');
        return parts.length ? parts[parts.length - 1] : null;
    }

    // Update decorateAndIntercept to accept allyName
    function decorateAndIntercept(attackBtn, factionId, allyName) {
        if (!attackBtn) return;
        if (attackBtn.dataset.allyDecorated === '1') return;
        attackBtn.dataset.allyDecorated = '1';

        // Positioning for overlay
        const cs = getComputedStyle(attackBtn);
        if (cs.position === 'static') attackBtn.style.position = 'relative';

        // Green X overlay (slightly smaller for mobile)
        const x = document.createElement('span');
        x.textContent = '✕';
        x.setAttribute('aria-hidden', 'true');
        x.style.position = 'absolute';
        x.style.top = '2px';
        x.style.right = '2px';
        x.style.fontWeight = '900';
        x.style.fontSize = '36px';
        x.style.lineHeight = '1';
        x.style.padding = '2px 4px';
        x.style.borderRadius = '4px';
        x.style.background = 'rgba(0, 128, 0, 0.15)';
        x.style.color = '#0f0';
        x.style.pointerEvents = 'none';
        x.title = `Ally faction (${allyName}) – confirm before attacking`;
        attackBtn.appendChild(x);

        // Confirm dialog allowing proceed
        const onAttemptAttack = (e) => {
            e.preventDefault();
            e.stopPropagation();

            const proceed = confirm(
                `This player is in an allied faction (${allyName}).\n\nAre you sure you want to attack?`
            );
            if (!proceed) return;

            const href = attackBtn.getAttribute('href');
            if (!href) return;

            // Respect modifier keys / middle click
            if (e.metaKey || e.ctrlKey || e.button === 1) {
                window.open(href, '_blank');
            } else {
                window.location.href = href;
            }
        };

        attackBtn.addEventListener('click', onAttemptAttack, { capture: true });
    }

    async function init() {
        // Wait for either: faction link appears OR just proceed after a beat
        await waitForSelector('a[href*="/factions.php?step=profile&ID="]', { timeout: 5000 });
        const factionId = getFactionId();

        const allyObj = Allies.find(a => String(a.id) === String(factionId));
        const isAlly = !!allyObj;

        // Log for debugging
        const attackBtnNow = document.querySelector('a.profile-button-attack');
        const userId = getUserIdFromAttackBtn(attackBtnNow);
        console.log(`NPO FF: User:${userId} Faction:${factionId} IsAlly:${isAlly}`);

        if (!isAlly) return;

        // Ensure we catch the attack button even if it renders later
        const attackBtn = await waitForSelector('a.profile-button-attack', { timeout: 8000 });
        if (!attackBtn) return;

        decorateAndIntercept(attackBtn, factionId, allyObj.name);
    }

    // Run at document-end, plus handle full load as a fallback
    init();
    window.addEventListener('load', init, { once: true });
})();