", { class: "ff-scouter-vertical-line-high-upper" }));
//$(element).append($("
", { class: "ff-scouter-vertical-line-high-lower" }));
}
const cached = get_cached_value(player_id);
if (cached) {
const percent = ff_to_percent(cached.value);
element.style.setProperty("--band-percent", percent);
$(element).find(".ff-scouter-arrow").remove();
var arrow;
if (percent < 33) {
arrow = BLUE_ARROW;
} else if (percent < 66) {
arrow = GREEN_ARROW;
} else {
arrow = RED_ARROW;
}
const img = $("
![]()
", {
src: arrow,
class: "ff-scouter-arrow",
});
$(element).append(img);
}
}
}
async function apply_ff_gauge(elements) {
// Remove elements which already have the class
elements = elements.filter(
(e) => !e.classList.contains("ff-scouter-indicator"),
);
// Convert elements to a list of tuples
elements = elements.map((e) => {
const player_id = get_player_id_in_element(e);
return [player_id, e];
});
// Remove any elements that don't have an id
elements = elements.filter((e) => e[0]);
if (elements.length > 0) {
// Display cached values immediately
// This is also important to ensure we only iterate the list once
// Then update
// Then re-display after the update
show_cached_values(elements);
const player_ids = elements.map((e) => e[0]);
update_ff_cache(player_ids, () => {
show_cached_values(elements);
});
}
}
async function apply_to_mini_profile(mini) {
// Get the user id, and the details
// Then in profile-container.description append a new span with the text. Win
const player_id = get_player_id_in_element(mini);
if (player_id) {
const response = get_cached_value(player_id);
if (response) {
// Remove any existing elements
$(mini).find(".ff-scouter-mini-ff").remove();
// Minimal, text-only Fair Fight string for mini-profiles
const ff_string = get_ff_string(response);
const difficulty = get_difficulty_text(response.value);
const now = Date.now() / 1000;
const age = now - response.last_updated;
let fresh = "";
if (age < 24 * 60 * 60) {
// Pass
} else if (age < 31 * 24 * 60 * 60) {
var days = Math.round(age / (24 * 60 * 60));
fresh = days === 1 ? "(1 day old)" : `(${days} days old)`;
} else if (age < 365 * 24 * 60 * 60) {
var months = Math.round(age / (31 * 24 * 60 * 60));
fresh = months === 1 ? "(1 month old)" : `(${months} months old)`;
} else {
var years = Math.round(age / (365 * 24 * 60 * 60));
fresh = years === 1 ? "(1 year old)" : `(${years} years old)`;
}
const message = `FF ${ff_string} (${difficulty}) ${fresh}`;
const description = $(mini).find(".description");
const desc = $("
", {
class: "ff-scouter-mini-ff",
});
desc.text(message);
$(description).append(desc);
}
}
}
const ff_gauge_observer = new MutationObserver(async function () {
var honor_bars = $(".honor-text-wrap").toArray();
if (honor_bars.length > 0) {
await apply_ff_gauge($(".honor-text-wrap").toArray());
} else {
if (
window.location.href.startsWith("https://www.torn.com/factions.php")
) {
await apply_ff_gauge($(".member").toArray());
} else if (
window.location.href.startsWith("https://www.torn.com/companies.php")
) {
await apply_ff_gauge($(".employee").toArray());
} else if (
window.location.href.startsWith("https://www.torn.com/joblist.php")
) {
await apply_ff_gauge($(".employee").toArray());
} else if (
window.location.href.startsWith("https://www.torn.com/messages.php")
) {
await apply_ff_gauge($(".name").toArray());
} else if (
window.location.href.startsWith("https://www.torn.com/index.php")
) {
await apply_ff_gauge($(".name").toArray());
} else if (
window.location.href.startsWith("https://www.torn.com/hospitalview.php")
) {
await apply_ff_gauge($(".name").toArray());
} else if (
window.location.href.startsWith(
"https://www.torn.com/page.php?sid=UserList",
)
) {
await apply_ff_gauge($(".name").toArray());
} else if (
window.location.href.startsWith("https://www.torn.com/bounties.php")
) {
await apply_ff_gauge($(".target").toArray());
await apply_ff_gauge($(".listed").toArray());
} else if (
window.location.href.startsWith("https://www.torn.com/forums.php")
) {
await apply_ff_gauge($(".last-poster").toArray());
await apply_ff_gauge($(".starter").toArray());
await apply_ff_gauge($(".last-post").toArray());
await apply_ff_gauge($(".poster").toArray());
} else if (window.location.href.includes("page.php?sid=hof")) {
await apply_ff_gauge($('[class^="userInfoBox__"]').toArray());
}
}
var mini_profiles = $(
'[class^="profile-mini-_userProfileWrapper_"]',
).toArray();
if (mini_profiles.length > 0) {
for (const mini of mini_profiles) {
if (!mini.classList.contains("ff-processed")) {
mini.classList.add("ff-processed");
const player_id = get_player_id_in_element(mini);
apply_to_mini_profile(mini);
update_ff_cache([player_id], () => {
apply_to_mini_profile(mini);
});
}
}
}
});
ff_gauge_observer.observe(document, {
attributes: false,
childList: true,
characterData: false,
subtree: true,
});
function abbreviateCountry(name) {
if (!name) return "";
if (name.trim().toLowerCase() === "switzerland") return "Switz";
const words = name.trim().split(/\s+/);
if (words.length === 1) return words[0];
return words.map((w) => w[0].toUpperCase()).join("");
}
function formatTime(ms) {
let totalSeconds = Math.max(0, Math.floor(ms / 1000));
let hours = String(Math.floor(totalSeconds / 3600)).padStart(2, "0");
let minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart(
2,
"0",
);
let seconds = String(totalSeconds % 60).padStart(2, "0");
return `${hours}:${minutes}:${seconds}`;
}
function fetchFactionData(factionID) {
const url = `https://api.torn.com/v2/faction/${factionID}/members?striptags=true&key=${key}`;
return fetch(url).then((response) => response.json());
}
function updateMemberStatus(li, member) {
if (!member || !member.status) return;
let statusEl = li.querySelector(".status");
if (!statusEl) return;
let lastActionRow = li.querySelector(".last-action-row");
let lastActionText = member.last_action?.relative || "";
if (lastActionRow) {
lastActionRow.textContent = `Last Action: ${lastActionText}`;
} else {
lastActionRow = document.createElement("div");
lastActionRow.className = "last-action-row";
lastActionRow.textContent = `Last Action: ${lastActionText}`;
let lastDiv = Array.from(li.children)
.reverse()
.find((el) => el.tagName === "DIV");
if (lastDiv?.nextSibling) {
li.insertBefore(lastActionRow, lastDiv.nextSibling);
} else {
li.appendChild(lastActionRow);
}
}
// Handle status changes
if (member.status.state === "Okay") {
if (statusEl.dataset.originalHtml) {
statusEl.innerHTML = statusEl.dataset.originalHtml;
delete statusEl.dataset.originalHtml;
}
statusEl.textContent = "Okay";
} else if (member.status.state === "Traveling") {
if (!statusEl.dataset.originalHtml) {
statusEl.dataset.originalHtml = statusEl.innerHTML;
}
let description = member.status.description || "";
let location = "";
let isReturning = false;
if (description.includes("Returning to Torn from ")) {
location = description.replace("Returning to Torn from ", "");
isReturning = true;
} else if (description.includes("Traveling to ")) {
location = description.replace("Traveling to ", "");
}
let abbr = abbreviateCountry(location);
const planeSvg = `
`;
const tornSymbol = `
`;
statusEl.innerHTML = `
${tornSymbol}${planeSvg}${abbr}`;
} else if (member.status.state === "Abroad") {
if (!statusEl.dataset.originalHtml) {
statusEl.dataset.originalHtml = statusEl.innerHTML;
}
let description = member.status.description || "";
if (description.startsWith("In ")) {
let location = description.replace("In ", "");
let abbr = abbreviateCountry(location);
statusEl.textContent = `in ${abbr}`;
}
}
// Update countdown
if (member.status.until && parseInt(member.status.until, 10) > 0) {
memberCountdowns[member.id] = parseInt(member.status.until, 10);
} else {
delete memberCountdowns[member.id];
}
}
function updateFactionStatuses(factionID, container) {
apiCallInProgressCount++;
fetchFactionData(factionID)
.then((data) => {
if (!Array.isArray(data.members)) {
console.warn(
`[FF Scouter V2] No members array for faction ${factionID}`,
);
return;
}
const memberMap = {};
data.members.forEach((member) => {
memberMap[member.id] = member;
});
container.querySelectorAll("li").forEach((li) => {
let profileLink = li.querySelector('a[href*="profiles.php?XID="]');
if (!profileLink) return;
let match = profileLink.href.match(/XID=(\d+)/);
if (!match) return;
let userID = match[1];
updateMemberStatus(li, memberMap[userID]);
});
})
.catch((err) => {
console.error(
"[FF Scouter V2] Error fetching faction data for faction",
factionID,
err,
);
})
.finally(() => {
apiCallInProgressCount--;
});
}
function updateAllMemberTimers() {
const liElements = document.querySelectorAll(
".enemy-faction .members-list li, .your-faction .members-list li",
);
liElements.forEach((li) => {
let profileLink = li.querySelector('a[href*="profiles.php?XID="]');
if (!profileLink) return;
let match = profileLink.href.match(/XID=(\d+)/);
if (!match) return;
let userID = match[1];
let statusEl = li.querySelector(".status");
if (!statusEl) return;
if (memberCountdowns[userID]) {
let remaining = memberCountdowns[userID] * 1000 - Date.now();
if (remaining < 0) remaining = 0;
statusEl.textContent = formatTime(remaining);
}
});
}
function updateAPICalls() {
let enemyFactionLink = document.querySelector(
".opponentFactionName___vhESM",
);
let yourFactionLink = document.querySelector(".currentFactionName___eq7n8");
if (!enemyFactionLink || !yourFactionLink) return;
let enemyFactionIdMatch = enemyFactionLink.href.match(/ID=(\d+)/);
let yourFactionIdMatch = yourFactionLink.href.match(/ID=(\d+)/);
if (!enemyFactionIdMatch || !yourFactionIdMatch) return;
let enemyList = document.querySelector(".enemy-faction .members-list");
let yourList = document.querySelector(".your-faction .members-list");
if (!enemyList || !yourList) return;
updateFactionStatuses(enemyFactionIdMatch[1], enemyList);
updateFactionStatuses(yourFactionIdMatch[1], yourList);
}
function initWarScript() {
let enemyFactionLink = document.querySelector(
".opponentFactionName___vhESM",
);
let yourFactionLink = document.querySelector(".currentFactionName___eq7n8");
if (!enemyFactionLink || !yourFactionLink) return false;
let enemyList = document.querySelector(".enemy-faction .members-list");
let yourList = document.querySelector(".your-faction .members-list");
if (!enemyList || !yourList) return false;
updateAPICalls();
setInterval(updateAPICalls, API_INTERVAL);
console.log(
"[FF Scouter V2] Torn Faction Status Countdown (Real-Time & API Status - Relative Last): Initialized",
);
return true;
}
let warObserver = new MutationObserver((mutations, obs) => {
if (initWarScript()) {
obs.disconnect();
}
});
if (!document.getElementById("FFScouterV2DisableWarMonitor")) {
warObserver.observe(document.body, { childList: true, subtree: true });
const memberTimersInterval = setInterval(updateAllMemberTimers, 1000);
window.addEventListener("FFScouterV2DisableWarMonitor", () => {
console.log(
"[FF Scouter V2] Caught disable event, removing monitoring observer and interval",
);
warObserver.disconnect();
clearInterval(memberTimersInterval);
});
}
// Try to be friendly and detect other war monitoring scripts
const catchOtherScripts = () => {
if (
Array.from(document.querySelectorAll("style")).some(
(style) =>
style.textContent.includes(
'.members-list li:has(div.status[data-twse-highlight="true"])', // Torn War Stuff Enhanced
) ||
style.textContent.includes(".warstuff_highlight") || // Torn War Stuff
style.textContent.includes(".finally-bs-stat"), // wall-battlestats
)
) {
window.dispatchEvent(new Event("FFScouterV2DisableWarMonitor"));
}
};
catchOtherScripts();
setTimeout(catchOtherScripts, 500);
function showToast(message) {
const existing = document.getElementById("ffscouter-toast");
if (existing) existing.remove();
const toast = document.createElement("div");
toast.id = "ffscouter-toast";
toast.style.position = "fixed";
toast.style.bottom = "30px";
toast.style.left = "50%";
toast.style.transform = "translateX(-50%)";
toast.style.background = "#c62828";
toast.style.color = "#fff";
toast.style.padding = "8px 16px";
toast.style.borderRadius = "8px";
toast.style.fontSize = "14px";
toast.style.boxShadow = "0 2px 12px rgba(0,0,0,0.2)";
toast.style.zIndex = "2147483647";
toast.style.opacity = "1";
toast.style.transition = "opacity 0.5s";
toast.style.display = "flex";
toast.style.alignItems = "center";
toast.style.gap = "10px";
// Close button
const closeBtn = document.createElement("span");
closeBtn.textContent = "×";
closeBtn.style.cursor = "pointer";
closeBtn.style.marginLeft = "8px";
closeBtn.style.fontWeight = "bold";
closeBtn.style.fontSize = "18px";
closeBtn.setAttribute("aria-label", "Close");
closeBtn.onclick = () => toast.remove();
const msg = document.createElement("span");
if (
message ===
"Invalid API key. Please sign up at ffscouter.com to use this service"
) {
msg.innerHTML =
'FairFight Scouter: Invalid API key. Please sign up at
ffscouter.com to use this service';
} else {
msg.textContent = `FairFight Scouter: ${message}`;
}
toast.appendChild(msg);
toast.appendChild(closeBtn);
document.body.appendChild(toast);
setTimeout(() => {
if (toast.parentNode) {
toast.style.opacity = "0";
setTimeout(() => toast.remove(), 500);
}
}, 4000);
}
}