", { class: "ff-scouter-vertical-line-high-upper" }));
//$(element).append($("
", { class: "ff-scouter-vertical-line-high-lower" }));
}
const cached = cached_values[parseInt(player_id)];
if (cached && cached.value) {
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]);
await 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 = await get_cached_value(player_id);
if (response && response.value) {
// 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();
var name_elems = $(".user.name");
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/page.php?sid=competition#/team",
)
) {
await apply_ff_gauge($(".name___H_bss").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/loader.php?sid=attackLog",
)
) {
const participants = $("ul.participants-list li").toArray();
if (participants > 100) {
return;
}
await apply_ff_gauge(participants);
} 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());
} else if (name_elems.length > 0) {
// Fallback for anyone without honor bars enabled
await apply_ff_gauge($(".user.name").toArray());
}
}
if (
window.location.href.startsWith(
"https://www.torn.com/page.php?sid=ItemMarket",
)
) {
await apply_ff_gauge(
$(
"div.bazaar-listing-card div:first-child div:first-child > a",
).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);
await update_ff_cache([player_id], () => {
apply_to_mini_profile(mini);
});
}
}
}
});
ff_gauge_observer.observe(document, {
attributes: false,
childList: true,
characterData: false,
subtree: true,
});
function get_cached_targets(staleok) {
const value = rD_getValue(TARGET_KEY);
if (!value) {
return null;
}
let parsed = null;
try {
parsed = JSON.parse(value);
} catch {
return null;
}
if (parsed == null) {
return null;
}
if (staleok) {
return parsed.targets;
}
if (parsed.last_updated + FF_TARGET_STALENESS > new Date()) {
// Old cache, return nothing
return null;
}
return parsed.targets;
}
function get_next_target_index() {
const value = Number(rD_getValue(TARGET_INDEX_KEY, 0));
rD_setValue(TARGET_INDEX_KEY, value + 1);
return value;
}
function reset_next_target_index() {
rD_setValue(TARGET_INDEX_KEY, 0);
}
function update_ff_targets() {
if (!key) {
return;
}
const cached = get_cached_targets(false);
if (cached) {
return;
}
const chain_ff_target = ffSettingsGet("chain-ff-target") || "2.5";
const url = `${BASE_URL}/api/v1/get-targets?key=${key}&inactiveonly=1&maxff=${chain_ff_target}&limit=50`;
console.log("[FF Scouter V2] Refreshing chain list");
rD_xmlhttpRequest({
method: "GET",
url: url,
onload: function (response) {
if (!response) {
return;
}
if (response.status == 200) {
var ff_response = JSON.parse(response.responseText);
if (ff_response && ff_response.error) {
showToast(ff_response.error);
return;
}
if (ff_response.targets) {
const result = {
targets: ff_response.targets,
last_updated: new Date(),
};
rD_setValue(TARGET_KEY, JSON.stringify(result));
console.log("[FF Scouter V2] Chain list updated successfully");
}
} else {
try {
var err = JSON.parse(response.responseText);
if (err && err.error) {
showToast(
"API request failed. Error: " +
err.error +
"; Code: " +
err.code,
);
} else {
showToast(
"API request failed. HTTP status code: " + response.status,
);
}
} catch {
showToast(
"API request failed. HTTP status code: " + response.status,
);
}
}
},
onerror: function (e) {
console.error("[FF Scouter V2] **** error ", e, "; Stack:", e.stack);
},
onabort: function (e) {
console.error("[FF Scouter V2] **** abort ", e, "; Stack:", e.stack);
},
ontimeout: function (e) {
console.error("[FF Scouter V2] **** timeout ", e, "; Stack:", e.stack);
},
});
}
function get_random_chain_target() {
const targets = get_cached_targets(true);
if (!targets) {
return null;
}
let index = get_next_target_index();
if (index >= targets.length) {
index = 0;
reset_next_target_index();
}
return targets[index];
}
function clear_cached_targets() {
rD_deleteValue(TARGET_KEY);
}
// Chain button stolen from https://greasyfork.org/en/scripts/511916-random-target-finder
function create_chain_button() {
// Check if chain button is enabled in settings
if (!ffSettingsGetToggle("chain-button-enabled")) {
ffdebug("[FF Scouter V2] Chain button disabled in settings");
return;
}
const button = document.createElement("button");
button.innerHTML = "FF";
button.style.position = "fixed";
//button.style.top = '10px';
//button.style.right = '10px';
button.style.top = "32%"; // Adjusted to center vertically
button.style.right = "0%"; // Center horizontally
//button.style.transform = 'translate(-50%, -50%)'; // Center the button properly
button.style.zIndex = "9999";
// Add CSS styles for a green background
button.style.backgroundColor = "green";
button.style.color = "white";
button.style.border = "none";
button.style.padding = "6px";
button.style.borderRadius = "6px";
button.style.cursor = "pointer";
// Add a click event listener to open Google in a new tab
button.addEventListener("click", function () {
let rando = get_random_chain_target();
if (!rando) {
return;
}
const linkType = ffSettingsGet("chain-link-type") || "attack";
const tabType = ffSettingsGet("chain-tab-type") || "newtab";
let profileLink;
if (linkType === "profile") {
profileLink = `https://www.torn.com/profiles.php?XID=${rando.player_id}`;
} else {
profileLink = `https://www.torn.com/loader.php?sid=attack&user2ID=${rando.player_id}`;
}
if (tabType === "sametab") {
window.location.href = profileLink;
} else {
window.open(profileLink, "_blank");
}
});
// Add the button to the page
document.body.appendChild(button);
}
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();
}
});
// Only initialize war monitoring if enabled in settings
if (
!document.getElementById("FFScouterV2DisableWarMonitor") &&
ffSettingsGetToggle("war-monitor-enabled")
) {
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 waitForElement(querySelector, timeout = 15000) {
return new Promise((resolve) => {
// Check if element already exists
const existingElement = document.querySelector(querySelector);
if (existingElement) {
return resolve(existingElement);
}
// Set up observer to watch for element
const observer = new MutationObserver(() => {
const element = document.querySelector(querySelector);
if (element) {
observer.disconnect();
if (timer) {
clearTimeout(timer);
}
resolve(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
// Set up timeout
const timer = setTimeout(() => {
observer.disconnect();
resolve(null);
}, timeout);
});
}
async function getLocalUserId() {
const profileLink = await waitForElement(
".settings-menu > .link > a:first-child",
15000,
);
if (!profileLink) {
console.error(
"[FF Scouter V2] Could not find profile link in settings menu",
);
return null;
}
const match = profileLink.href.match(/XID=(\d+)/);
if (match) {
const userId = match[1];
ffdebug(`[FF Scouter V2] Found local user ID: ${userId}`);
return userId;
}
console.error(
"[FF Scouter V2] Could not extract user ID from profile link",
);
return null;
}
function getCurrentUserId() {
return currentUserId;
}
// Settings management utilities
function ffSettingsGet(key) {
return rD_getValue(`ffscouterv2-${key}`, null);
}
function ffSettingsSet(key, value) {
rD_setValue(`ffscouterv2-${key}`, value);
}
function ffSettingsGetToggle(key) {
return ffSettingsGet(key) === "true";
}
function ffSettingsSetToggle(key, value) {
ffSettingsSet(key, value.toString());
}
async function createSettingsPanel() {
// Check if we're on the user's own profile page
const pageId = window.location.href.match(/XID=(\d+)/)?.[1];
if (!pageId || pageId !== currentUserId) {
return;
}
// Wait for profile wrapper to be available
const profileWrapper = await waitForElement(".profile-wrapper", 15000);
if (!profileWrapper) {
console.error(
"[FF Scouter V2] Could not find profile wrapper for settings panel",
);
return;
}
// Check if settings panel already exists
if (document.querySelector(".ff-settings-accordion")) {
ffdebug("[FF Scouter V2] Settings panel already exists");
return;
}
// Get current user data for display
const userName =
profileWrapper.querySelector(".user-name")?.textContent ||
profileWrapper.querySelector(".profile-name")?.textContent ||
profileWrapper.querySelector("h1")?.textContent ||
"User";
// Create the settings panel
const settingsPanel = document.createElement("details");
settingsPanel.className = "ff-settings-accordion";
profileWrapper.parentNode.insertBefore(
settingsPanel,
profileWrapper.nextSibling,
);
// Add glow effect if API key is not set
if (!key) {
settingsPanel.classList.add("ff-settings-glow");
}
// Create summary
const summary = document.createElement("summary");
summary.textContent = "FF Scouter Settings";
settingsPanel.appendChild(summary);
// Create main content div
const content = document.createElement("div");
// API Key Explanation
const apiExplanation = document.createElement("div");
apiExplanation.className = "ff-api-explanation ff-api-explanation-content";
apiExplanation.innerHTML = `
Important: You must use the SAME exact API key that you use on
ffscouter.com.
If you're not sure which API key you used, go to
your API preferences
and look for "FFScouter3" in your API key history comments.
`;
content.appendChild(apiExplanation);
// API Key Input
if (apikey[0] == "#") {
const apiKeyDiv = document.createElement("div");
apiKeyDiv.className = "ff-settings-entry ff-settings-entry-large";
const apiKeyLabel = document.createElement("label");
apiKeyLabel.setAttribute("for", "ff-api-key");
apiKeyLabel.textContent = "FF Scouter API Key:";
apiKeyLabel.className = "ff-settings-label ff-settings-label-inline";
apiKeyDiv.appendChild(apiKeyLabel);
const apiKeyInput = document.createElement("input");
apiKeyInput.type = "text";
apiKeyInput.id = "ff-api-key";
apiKeyInput.placeholder = "Paste your key here...";
apiKeyInput.className = "ff-settings-input ff-settings-input-wide";
apiKeyInput.value = key || "";
// Add blur class if key exists
if (key) {
apiKeyInput.classList.add("ff-blur");
}
apiKeyInput.addEventListener("focus", function () {
this.classList.remove("ff-blur");
});
apiKeyInput.addEventListener("blur", function () {
if (this.value) {
this.classList.add("ff-blur");
}
});
apiKeyInput.addEventListener("change", function () {
const newKey = this.value;
if (typeof newKey !== "string") {
return;
}
if (newKey && newKey.length < 10) {
this.style.outline = "1px solid red";
return;
}
this.style.outline = "none";
if (newKey === key) return;
rD_setValue("limited_key", newKey);
key = newKey;
if (newKey) {
this.classList.add("ff-blur");
settingsPanel.classList.remove("ff-settings-glow");
} else {
settingsPanel.classList.add("ff-settings-glow");
}
});
apiKeyDiv.appendChild(apiKeyInput);
content.appendChild(apiKeyDiv);
} else {
const apiKeyDiv = document.createElement("div");
apiKeyDiv.className = "ff-settings-entry ff-settings-entry-large";
const apiKeyLabel = document.createElement("label");
apiKeyLabel.setAttribute("for", "ff-api-key");
apiKeyLabel.textContent = "FF Scouter API Key:";
apiKeyLabel.className = "ff-settings-label ff-settings-label-inline";
apiKeyDiv.appendChild(apiKeyLabel);
const apiKeyInput = document.createElement("label");
apiKeyInput.textContent = "Code entered in Torn PDA User Scripts";
apiKeyInput.className = "ff-settings-label ff-settings-label-inline";
apiKeyDiv.appendChild(apiKeyInput);
content.appendChild(apiKeyDiv);
}
const rangesDiv = document.createElement("div");
rangesDiv.className = "ff-settings-entry ff-settings-entry-large";
const rangesLabel = document.createElement("label");
rangesLabel.setAttribute("for", "ff-ranges");
rangesLabel.textContent =
"FF Ranges (Low, High, Max) -- affects the color and positions of the arrows over player's honor bars:";
rangesLabel.className = "ff-settings-label ff-settings-label-inline";
rangesDiv.appendChild(rangesLabel);
const rangesInput = document.createElement("input");
rangesInput.type = "text";
rangesInput.id = "ff-ranges";
rangesInput.placeholder = "2,4,8";
rangesInput.className = "ff-settings-input ff-settings-input-narrow";
// Set current values
const currentRanges = get_ff_ranges(true);
if (currentRanges) {
rangesInput.value = `${currentRanges.low},${currentRanges.high},${currentRanges.max}`;
}
rangesInput.addEventListener("change", function () {
const value = this.value;
if (value === "") {
reset_ff_ranges();
this.style.outline = "none";
return;
}
const parts = value.split(",").map((p) => p.trim());
if (parts.length !== 3) {
this.style.outline = "1px solid red";
showToast(
"Incorrect format: FF ranges should be exactly 3 numbers separated by commas [low,high,max]",
);
return;
}
try {
const low = parseFloat(parts[0]);
const high = parseFloat(parts[1]);
const max = parseFloat(parts[2]);
if (isNaN(low) || isNaN(high) || isNaN(max)) {
throw new Error("Invalid numbers");
}
if (low <= 0 || high <= 0 || max <= 0) {
this.style.outline = "1px solid red";
showToast("FF ranges must be positive numbers");
return;
}
if (low >= high || high >= max) {
this.style.outline = "1px solid red";
showToast("FF ranges must be in ascending order: low < high < max");
return;
}
set_ff_ranges(low, high, max);
this.style.outline = "none";
showToast("FF ranges updated successfully!");
} catch (e) {
this.style.outline = "1px solid red";
showToast("Invalid numbers in FF ranges");
}
});
rangesDiv.appendChild(rangesInput);
content.appendChild(rangesDiv);
// Feature Toggles
const featuresLabel = document.createElement("p");
featuresLabel.textContent = "Feature toggles:";
featuresLabel.className = "ff-settings-section-header";
content.appendChild(featuresLabel);
// Chain Button Toggle
const chainToggleDiv = document.createElement("div");
chainToggleDiv.className = "ff-settings-entry ff-settings-entry-small";
const chainToggle = document.createElement("input");
chainToggle.type = "checkbox";
chainToggle.id = "chain-button-toggle";
chainToggle.checked = ffSettingsGetToggle("chain-button-enabled");
chainToggle.className = "ff-settings-checkbox";
const chainLabel = document.createElement("label");
chainLabel.setAttribute("for", "chain-button-toggle");
chainLabel.textContent = "Enable Chain Button (Green FF Button)";
chainLabel.className = "ff-settings-label";
chainLabel.style.cursor = "pointer";
chainToggleDiv.appendChild(chainToggle);
chainToggleDiv.appendChild(chainLabel);
content.appendChild(chainToggleDiv);
const chainLinkTypeDiv = document.createElement("div");
chainLinkTypeDiv.className = "ff-settings-entry ff-settings-entry-small";
chainLinkTypeDiv.style.marginLeft = "20px";
const chainLinkTypeLabel = document.createElement("label");
chainLinkTypeLabel.textContent = "Chain button opens:";
chainLinkTypeLabel.className = "ff-settings-label ff-settings-label-inline";
chainLinkTypeDiv.appendChild(chainLinkTypeLabel);
const chainLinkTypeSelect = document.createElement("select");
chainLinkTypeSelect.id = "chain-link-type";
chainLinkTypeSelect.className = "ff-settings-input";
const attackOption = document.createElement("option");
attackOption.value = "attack";
attackOption.textContent = "Attack page";
chainLinkTypeSelect.appendChild(attackOption);
const profileOption = document.createElement("option");
profileOption.value = "profile";
profileOption.textContent = "Profile page";
chainLinkTypeSelect.appendChild(profileOption);
chainLinkTypeSelect.value = ffSettingsGet("chain-link-type") || "attack";
chainLinkTypeDiv.appendChild(chainLinkTypeSelect);
content.appendChild(chainLinkTypeDiv);
const chainTabTypeDiv = document.createElement("div");
chainTabTypeDiv.className = "ff-settings-entry ff-settings-entry-small";
chainTabTypeDiv.style.marginLeft = "20px";
const chainTabTypeLabel = document.createElement("label");
chainTabTypeLabel.textContent = "Open in:";
chainTabTypeLabel.className = "ff-settings-label ff-settings-label-inline";
chainTabTypeDiv.appendChild(chainTabTypeLabel);
const chainTabTypeSelect = document.createElement("select");
chainTabTypeSelect.id = "chain-tab-type";
chainTabTypeSelect.className = "ff-settings-input";
const newTabOption = document.createElement("option");
newTabOption.value = "newtab";
newTabOption.textContent = "New tab";
chainTabTypeSelect.appendChild(newTabOption);
const sameTabOption = document.createElement("option");
sameTabOption.value = "sametab";
sameTabOption.textContent = "Same tab";
chainTabTypeSelect.appendChild(sameTabOption);
chainTabTypeSelect.value = ffSettingsGet("chain-tab-type") || "newtab";
chainTabTypeDiv.appendChild(chainTabTypeSelect);
content.appendChild(chainTabTypeDiv);
const chainFFTargetDiv = document.createElement("div");
chainFFTargetDiv.className = "ff-settings-entry ff-settings-entry-small";
chainFFTargetDiv.style.marginLeft = "20px";
const chainFFTargetLabel = document.createElement("label");
chainFFTargetLabel.setAttribute("for", "chain-ff-target");
chainFFTargetLabel.textContent =
"FF target (Maximum FF the chain button should open)";
chainFFTargetLabel.className = "ff-settings-label ff-settings-label-inline";
chainFFTargetDiv.appendChild(chainFFTargetLabel);
const chainFFTargetInput = document.createElement("input");
chainFFTargetInput.id = "chain-ff-target";
chainFFTargetInput.className = "ff-settings-input";
chainFFTargetInput.value = ffSettingsGet("chain-ff-target") || "2.5";
chainFFTargetDiv.appendChild(chainFFTargetInput);
content.appendChild(chainFFTargetDiv);
// War Monitor Toggle
const warToggleDiv = document.createElement("div");
warToggleDiv.className = "ff-settings-entry ff-settings-entry-section";
const warToggle = document.createElement("input");
warToggle.type = "checkbox";
warToggle.id = "war-monitor-toggle";
warToggle.checked = ffSettingsGetToggle("war-monitor-enabled");
warToggle.className = "ff-settings-checkbox";
const warLabel = document.createElement("label");
warLabel.setAttribute("for", "war-monitor-toggle");
warLabel.textContent = "Enable War Monitor (Faction Status)";
warLabel.className = "ff-settings-label";
warLabel.style.cursor = "pointer";
warToggleDiv.appendChild(warToggle);
warToggleDiv.appendChild(warLabel);
content.appendChild(warToggleDiv);
const saveButtonDiv = document.createElement("div");
saveButtonDiv.className = "ff-settings-button-container";
const resetButton = document.createElement("button");
resetButton.textContent = "Reset to Defaults";
resetButton.className =
"ff-settings-button ff-settings-button-large torn-btn btn-big";
resetButton.addEventListener("click", function () {
const confirmed = confirm(
"Are you sure you want to reset all settings to their default values?",
);
if (!confirmed) return;
reset_ff_ranges();
ffSettingsSetToggle("chain-button-enabled", true);
ffSettingsSet("chain-link-type", "attack");
ffSettingsSet("chain-tab-type", "newtab");
ffSettingsSet("chain-ff-target", "2.5");
ffSettingsSetToggle("war-monitor-enabled", true);
ffSettingsSetToggle("debug-logs", false);
document.getElementById("ff-ranges").value = "";
document.getElementById("chain-button-toggle").checked = true;
document.getElementById("chain-link-type").value = "attack";
document.getElementById("chain-tab-type").value = "newtab";
document.getElementById("chain-ff-target").value = "2.5";
document.getElementById("war-monitor-toggle").checked = true;
document.getElementById("debug-logs").checked = false;
document.getElementById("ff-ranges").style.outline = "none";
const existingButtons = Array.from(
document.querySelectorAll("button"),
).filter(
(btn) =>
btn.textContent === "FF" &&
btn.style.position === "fixed" &&
btn.style.backgroundColor === "green",
);
existingButtons.forEach((btn) => btn.remove());
create_chain_button();
showToast("Settings reset to defaults!", TOAST_LOG);
this.style.backgroundColor = "var(--ff-success-color)";
setTimeout(() => {
this.style.backgroundColor = "";
}, 1000);
});
const saveButton = document.createElement("button");
saveButton.textContent = "Save Settings";
saveButton.className =
"ff-settings-button ff-settings-button-large torn-btn btn-big";
saveButton.addEventListener("click", function () {
let apiKey = null;
if (document.getElementById("ff-api-key")) {
apiKey = document.getElementById("ff-api-key").value;
}
const ranges = document.getElementById("ff-ranges").value;
const chainEnabled = document.getElementById(
"chain-button-toggle",
).checked;
const chainLinkType = document.getElementById("chain-link-type").value;
const chainTabType = document.getElementById("chain-tab-type").value;
const chainFFTarget = document.getElementById("chain-ff-target").value;
const warEnabled = document.getElementById("war-monitor-toggle").checked;
const debugEnabled = document.getElementById("debug-logs").checked;
let hasErrors = false;
// In Torn PDA we hide the api key field because we read it from the script page
if (document.getElementById("ff-api-key") && apiKey !== key) {
rD_setValue("limited_key", apiKey);
key = apiKey;
if (apiKey) {
settingsPanel.classList.remove("ff-settings-glow");
document.getElementById("ff-api-key").classList.add("ff-blur");
} else {
settingsPanel.classList.add("ff-settings-glow");
}
}
const rangesInput = document.getElementById("ff-ranges");
if (ranges === "") {
reset_ff_ranges();
rangesInput.style.outline = "none";
} else {
const parts = ranges.split(",").map((p) => p.trim());
if (parts.length !== 3) {
rangesInput.style.outline = "1px solid red";
showToast(
"FF ranges must be exactly 3 numbers separated by commas [low,high,max]",
);
hasErrors = true;
} else {
try {
const low = parseFloat(parts[0]);
const high = parseFloat(parts[1]);
const max = parseFloat(parts[2]);
if (isNaN(low) || isNaN(high) || isNaN(max)) {
rangesInput.style.outline = "1px solid red";
showToast("FF ranges must be valid numbers");
hasErrors = true;
} else if (low <= 0 || high <= 0 || max <= 0) {
rangesInput.style.outline = "1px solid red";
showToast("FF ranges must be positive numbers");
hasErrors = true;
} else if (low >= high || high >= max) {
rangesInput.style.outline = "1px solid red";
showToast(
"FF ranges must be in ascending order: low < high < max",
);
hasErrors = true;
} else {
set_ff_ranges(low, high, max);
rangesInput.style.outline = "none";
}
} catch (e) {
rangesInput.style.outline = "1px solid red";
showToast("Invalid FF ranges format");
hasErrors = true;
}
}
}
if (hasErrors) {
return;
}
const wasChainEnabled = ffSettingsGetToggle("chain-button-enabled");
const wasWarEnabled = ffSettingsGetToggle("war-monitor-enabled");
ffSettingsSetToggle("chain-button-enabled", chainEnabled);
ffSettingsSet("chain-link-type", chainLinkType);
ffSettingsSet("chain-tab-type", chainTabType);
ffSettingsSet("chain-ff-target", chainFFTarget);
ffSettingsSetToggle("war-monitor-enabled", warEnabled);
ffSettingsSetToggle("debug-logs", debugEnabled);
const existingButtons = Array.from(
document.querySelectorAll("button"),
).filter(
(btn) =>
btn.textContent === "FF" &&
btn.style.position === "fixed" &&
btn.style.backgroundColor === "green",
);
if (!chainEnabled) {
existingButtons.forEach((btn) => btn.remove());
} else if (chainEnabled !== wasChainEnabled) {
if (existingButtons.length === 0) {
create_chain_button();
}
} else {
existingButtons.forEach((btn) => btn.remove());
create_chain_button();
}
clear_cached_targets();
update_ff_targets();
if (warEnabled !== wasWarEnabled) {
if (!warEnabled) {
window.dispatchEvent(new Event("FFScouterV2DisableWarMonitor"));
} else {
location.reload();
}
}
showToast("Settings saved successfully!", TOAST_LOG);
this.style.backgroundColor = "var(--ff-success-color)";
setTimeout(() => {
this.style.backgroundColor = "";
}, 1000);
});
saveButtonDiv.appendChild(resetButton);
saveButtonDiv.appendChild(saveButton);
content.appendChild(saveButtonDiv);
const cacheLabel = document.createElement("p");
cacheLabel.textContent = "Cache management:";
cacheLabel.className = "ff-settings-section-header";
content.appendChild(cacheLabel);
const cacheButtonDiv = document.createElement("div");
cacheButtonDiv.className = "ff-settings-button-container";
const clearCacheBtn = document.createElement("button");
clearCacheBtn.textContent = "Clear FF Cache";
clearCacheBtn.className = "ff-settings-button torn-btn btn-big";
clearCacheBtn.addEventListener("click", async function () {
const confirmed = confirm(
"Are you sure you want to clear all FF Scouter cache?",
);
if (!confirmed) return;
let count = 0;
const keysToRemove = [];
for (const key of rD_listValues()) {
if (
key.startsWith("ffscouterv2-") &&
!key.includes("limited_key") &&
!key.includes("ranges")
) {
keysToRemove.push(key);
}
}
for (const key of keysToRemove) {
rD_deleteValue(key);
count++;
}
await ffcache.delete_db();
showToast(`Cleared ${count} cached items`);
});
cacheButtonDiv.appendChild(clearCacheBtn);
content.appendChild(cacheButtonDiv);
const debugLabel = document.createElement("p");
debugLabel.textContent = "Debug settings:";
debugLabel.className = "ff-settings-section-header";
content.appendChild(debugLabel);
const debugToggleDiv = document.createElement("div");
debugToggleDiv.className = "ff-settings-entry ff-settings-entry-small";
const debugToggle = document.createElement("input");
debugToggle.type = "checkbox";
debugToggle.id = "debug-logs";
debugToggle.checked = ffSettingsGetToggle("debug-logs");
debugToggle.className = "ff-settings-checkbox";
const debugToggleLabel = document.createElement("label");
debugToggleLabel.setAttribute("for", "debug-logs");
debugToggleLabel.textContent = "Enable debug logging";
debugToggleLabel.className = "ff-settings-label";
debugToggleLabel.style.cursor = "pointer";
debugToggleDiv.appendChild(debugToggle);
debugToggleDiv.appendChild(debugToggleLabel);
content.appendChild(debugToggleDiv);
settingsPanel.appendChild(content);
ffdebug("[FF Scouter V2] Settings panel created successfully");
}
function showToast(message, level) {
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.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";
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();
switch (level) {
case TOAST_LOG:
toast.style.background = "green";
break;
case TOAST_ERROR:
default:
toast.style.background = "#c62828";
break;
}
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}`;
}
console.log("[FF Scouter V2] Toast: ", message);
toast.appendChild(msg);
toast.appendChild(closeBtn);
document.body.appendChild(toast);
setTimeout(() => {
if (toast.parentNode) {
toast.style.opacity = "0";
setTimeout(() => toast.remove(), 500);
}
}, 4000);
}
create_chain_button();
update_ff_targets();
getLocalUserId().then((userId) => {
if (userId) {
currentUserId = userId;
ffdebug(`[FF Scouter V2] Current user ID initialized: ${currentUserId}`);
createSettingsPanel();
const profileObserver = new MutationObserver(() => {
const pageId = window.location.href.match(/XID=(\d+)/)?.[1];
if (
pageId === currentUserId &&
window.location.pathname === "/profiles.php"
) {
createSettingsPanel();
}
});
profileObserver.observe(document.body, {
childList: true,
subtree: true,
});
}
});
}