// ==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 });
})();