", { class: "ff-scouter-vertical-line-high-upper" }));
//$(element).append($("
", { class: "ff-scouter-vertical-line-high-lower" }));
}
const ff = get_ff(player_id);
if (ff) {
const percent = ff_to_percent(ff);
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_fair_fight_response(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(`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("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("Torn Faction Status Countdown (Real-Time & API Status - Relative Last): Initialized");
return true;
}
let warObserver = new MutationObserver((mutations, obs) => {
if (initWarScript()) {
obs.disconnect();
}
});
warObserver.observe(document.body, { childList: true, subtree: true });
setInterval(updateAllMemberTimers, 1000);
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);
}
}