您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Shows the expected Fair Fight score against targets and faction war status
- // ==UserScript==
- // @name FF Scouter V2
- // @namespace Violentmonkey Scripts
- // @match https://www.torn.com/*
- // @version 2.46
- // @author rDacted, Weav3r, xentac
- // @description Shows the expected Fair Fight score against targets and faction war status
- // @grant GM_xmlhttpRequest
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_deleteValue
- // @grant GM_registerMenuCommand
- // @grant GM_addStyle
- // @connect ffscouter.com
- // ==/UserScript==
- const FF_VERSION = "2.46";
- const API_INTERVAL = 30000;
- const memberCountdowns = {};
- let apiCallInProgressCount = 0;
- let singleton = document.getElementById("ff-scouter-run-once");
- if (!singleton) {
- console.log(`[FF Scouter V2] FF Scouter version ${FF_VERSION} starting`);
- GM_addStyle(`
- .ff-scouter-indicator {
- position: relative;
- display: block;
- padding: 0;
- }
- .ff-scouter-vertical-line-low-upper,
- .ff-scouter-vertical-line-low-lower,
- .ff-scouter-vertical-line-high-upper,
- .ff-scouter-vertical-line-high-lower {
- content: '';
- position: absolute;
- width: 2px;
- height: 30%;
- background-color: black;
- margin-left: -1px;
- }
- .ff-scouter-vertical-line-low-upper {
- top: 0;
- left: calc(var(--arrow-width) / 2 + 33 * (100% - var(--arrow-width)) / 100);
- }
- .ff-scouter-vertical-line-low-lower {
- bottom: 0;
- left: calc(var(--arrow-width) / 2 + 33 * (100% - var(--arrow-width)) / 100);
- }
- .ff-scouter-vertical-line-high-upper {
- top: 0;
- left: calc(var(--arrow-width) / 2 + 66 * (100% - var(--arrow-width)) / 100);
- }
- .ff-scouter-vertical-line-high-lower {
- bottom: 0;
- left: calc(var(--arrow-width) / 2 + 66 * (100% - var(--arrow-width)) / 100);
- }
- .ff-scouter-arrow {
- position: absolute;
- transform: translate(-50%, -50%);
- padding: 0;
- top: 0;
- left: calc(var(--arrow-width) / 2 + var(--band-percent) * (100% - var(--arrow-width)) / 100);
- width: var(--arrow-width);
- object-fit: cover;
- pointer-events: none;
- }
- .last-action-row {
- font-size: 11px;
- color: inherit;
- font-style: normal;
- font-weight: normal;
- text-align: center;
- margin-left: 8px;
- margin-bottom: 2px;
- margin-top: -2px;
- display: block;
- }
- .travel-status {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- gap: 2px;
- min-width: 0;
- overflow: hidden;
- }
- .torn-symbol {
- width: 16px;
- height: 16px;
- fill: currentColor;
- vertical-align: middle;
- flex-shrink: 0;
- }
- .plane-svg {
- width: 14px;
- height: 14px;
- fill: currentColor;
- vertical-align: middle;
- flex-shrink: 0;
- }
- .plane-svg.returning {
- transform: scaleX(-1);
- }
- .country-abbr {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- min-width: 0;
- flex: 0 1 auto;
- vertical-align: bottom;
- }
- `);
- var BASE_URL = "https://ffscouter.com";
- var BLUE_ARROW =
- "https://raw.githubusercontent.com/rDacted2/fair_fight_scouter/main/images/blue-arrow.svg";
- var GREEN_ARROW =
- "https://raw.githubusercontent.com/rDacted2/fair_fight_scouter/main/images/green-arrow.svg";
- var RED_ARROW =
- "https://raw.githubusercontent.com/rDacted2/fair_fight_scouter/main/images/red-arrow.svg";
- var rD_xmlhttpRequest;
- var rD_setValue;
- var rD_getValue;
- var rD_deleteValue;
- var rD_registerMenuCommand;
- // DO NOT CHANGE THIS
- // DO NOT CHANGE THIS
- var apikey = "###PDA-APIKEY###";
- // DO NOT CHANGE THIS
- // DO NOT CHANGE THIS
- if (apikey[0] != "#") {
- console.log("[FF Scouter V2] Adding modifications to support TornPDA");
- rD_xmlhttpRequest = function (details) {
- console.log("[FF Scouter V2] Attempt to make http request");
- if (details.method.toLowerCase() == "get") {
- return PDA_httpGet(details.url)
- .then(details.onload)
- .catch(
- details.onerror ?? ((e) => console.error("[FF Scouter V2] ", e)),
- );
- } else if (details.method.toLowerCase() == "post") {
- return PDA_httpPost(
- details.url,
- details.headers ?? {},
- details.body ?? details.data ?? "",
- )
- .then(details.onload)
- .catch(
- details.onerror ?? ((e) => console.error("[FF Scouter V2] ", e)),
- );
- } else {
- console.log("[FF Scouter V2] What is this? " + details.method);
- }
- };
- rD_setValue = function (name, value) {
- console.log("[FF Scouter V2] Attempted to set " + name);
- return localStorage.setItem(name, value);
- };
- rD_getValue = function (name, defaultValue) {
- var value = localStorage.getItem(name) ?? defaultValue;
- return value;
- };
- rD_deleteValue = function (name) {
- console.log("[FF Scouter V2] Attempted to delete " + name);
- return localStorage.removeItem(name);
- };
- rD_registerMenuCommand = function () {
- console.log("[FF Scouter V2] Disabling GM_registerMenuCommand");
- };
- rD_setValue("limited_key", apikey);
- } else {
- rD_xmlhttpRequest = GM_xmlhttpRequest;
- rD_setValue = GM_setValue;
- rD_getValue = GM_getValue;
- rD_deleteValue = GM_deleteValue;
- rD_registerMenuCommand = GM_registerMenuCommand;
- }
- var key = rD_getValue("limited_key", null);
- var info_line = null;
- rD_registerMenuCommand("Enter Limited API Key", () => {
- let userInput = prompt(
- "Enter Limited API Key",
- rD_getValue("limited_key", ""),
- );
- if (userInput !== null) {
- rD_setValue("limited_key", userInput);
- // Reload page
- window.location.reload();
- }
- });
- function create_text_location() {
- info_line = document.createElement("div");
- info_line.id = "ff-scouter-run-once";
- info_line.style.display = "block";
- info_line.style.clear = "both";
- info_line.style.margin = "5px 0";
- info_line.addEventListener("click", () => {
- if (key === null) {
- const limited_key = prompt(
- "Enter Limited API Key",
- rD_getValue("limited_key", ""),
- );
- if (limited_key) {
- rD_setValue("limited_key", limited_key);
- key = limited_key;
- window.location.reload();
- }
- }
- });
- var h4 = $("h4")[0];
- if (h4.textContent === "Attacking") {
- h4.parentNode.parentNode.after(info_line);
- } else {
- const linksTopWrap = h4.parentNode.querySelector(".links-top-wrap");
- if (linksTopWrap) {
- linksTopWrap.parentNode.insertBefore(
- info_line,
- linksTopWrap.nextSibling,
- );
- } else {
- h4.after(info_line);
- }
- }
- return info_line;
- }
- function set_message(message, error = false) {
- while (info_line.firstChild) {
- info_line.removeChild(info_line.firstChild);
- }
- const textNode = document.createTextNode(message);
- if (error) {
- info_line.style.color = "red";
- } else {
- info_line.style.color = "";
- }
- info_line.appendChild(textNode);
- }
- function update_ff_cache(player_ids, callback) {
- if (!key) {
- return;
- }
- player_ids = [...new Set(player_ids)];
- var unknown_player_ids = get_cache_misses(player_ids);
- if (unknown_player_ids.length > 0) {
- console.log(
- `[FF Scouter V2] Refreshing cache for ${unknown_player_ids.length} ids`,
- );
- var player_id_list = unknown_player_ids.join(",");
- const url = `${BASE_URL}/api/v1/get-stats?key=${key}&targets=${player_id_list}`;
- rD_xmlhttpRequest({
- method: "GET",
- url: url,
- onload: function (response) {
- if (response.status == 200) {
- var ff_response = JSON.parse(response.responseText);
- if (ff_response && ff_response.error) {
- showToast(ff_response.error);
- return;
- }
- var one_hour = 60 * 60 * 1000;
- var expiry = Date.now() + one_hour;
- ff_response.forEach((result) => {
- if (result && result.player_id) {
- if (result.fair_fight === null) {
- let cacheObj = {
- no_data: true,
- expiry: expiry,
- };
- rD_setValue("" + result.player_id, JSON.stringify(cacheObj));
- } else {
- let cacheObj = {
- value: result.fair_fight,
- last_updated: result.last_updated,
- expiry: expiry,
- bs_estimate: result.bs_estimate,
- bs_estimate_human: result.bs_estimate_human,
- };
- rD_setValue("" + result.player_id, JSON.stringify(cacheObj));
- }
- }
- });
- callback(player_ids);
- } else {
- try {
- var err = JSON.parse(response.responseText);
- if (err && err.error) {
- showToast(err.error);
- } else {
- showToast("API request failed.");
- }
- } catch {
- showToast("API request failed.");
- }
- }
- },
- onerror: function (e) {
- console.error("[FF Scouter V2] **** error ", e);
- },
- onabort: function (e) {
- console.error("[FF Scouter V2] **** abort ", e);
- },
- ontimeout: function (e) {
- console.error("[FF Scouter V2] **** timeout ", e);
- },
- });
- } else {
- callback(player_ids);
- }
- }
- function display_fair_fight(target_id, player_id) {
- const response = get_cached_value(target_id);
- if (response) {
- set_fair_fight(response, player_id);
- }
- }
- function get_ff_string(ff_response) {
- const ff = ff_response.value.toFixed(2);
- const now = Date.now() / 1000;
- const age = now - ff_response.last_updated;
- var suffix = "";
- if (age > 14 * 24 * 60 * 60) {
- suffix = "?";
- }
- return `${ff}${suffix}`;
- }
- function get_difficulty_text(ff) {
- if (ff <= 1) {
- return "Extremely easy";
- } else if (ff <= 2) {
- return "Easy";
- } else if (ff <= 3.5) {
- return "Moderately difficult";
- } else if (ff <= 4.5) {
- return "Difficult";
- } else {
- return "May be impossible";
- }
- }
- function get_detailed_message(ff_response, player_id) {
- if (ff_response.no_data || !ff_response.value) {
- return `<span style=\"font-weight: bold; margin-right: 6px;\">FairFight:</span><span style=\"background: #444; color: #fff; font-weight: bold; padding: 2px 6px; border-radius: 4px; display: inline-block;\">No data</span>`;
- }
- const ff_string = get_ff_string(ff_response);
- const difficulty = get_difficulty_text(ff_response.value);
- const now = Date.now() / 1000;
- const age = now - ff_response.last_updated;
- var fresh = "";
- if (age < 24 * 60 * 60) {
- // Pass
- } else if (age < 31 * 24 * 60 * 60) {
- var days = Math.round(age / (24 * 60 * 60));
- if (days == 1) {
- fresh = "(1 day old)";
- } else {
- fresh = `(${days} days old)`;
- }
- } else if (age < 365 * 24 * 60 * 60) {
- var months = Math.round(age / (31 * 24 * 60 * 60));
- if (months == 1) {
- fresh = "(1 month old)";
- } else {
- fresh = `(${months} months old)`;
- }
- } else {
- var years = Math.round(age / (365 * 24 * 60 * 60));
- if (years == 1) {
- fresh = "(1 year old)";
- } else {
- fresh = `(${years} years old)`;
- }
- }
- const background_colour = get_ff_colour(ff_response.value);
- const text_colour = get_contrast_color(background_colour);
- let statDetails = "";
- if (ff_response.bs_estimate_human) {
- statDetails = `<span style=\"font-size: 11px; font-weight: normal; margin-left: 8px; vertical-align: middle; color: #cccccc; font-style: italic;\">Est. Stats: <span>${ff_response.bs_estimate_human}</span></span>`;
- }
- return `<span style=\"font-weight: bold; margin-right: 6px;\">FairFight:</span><span style=\"background: ${background_colour}; color: ${text_colour}; font-weight: bold; padding: 2px 6px; border-radius: 4px; display: inline-block;\">${ff_string} (${difficulty}) ${fresh}</span>${statDetails}`;
- }
- function get_ff_string_short(ff_response, player_id) {
- const ff = ff_response.value.toFixed(2);
- const now = Date.now() / 1000;
- const age = now - ff_response.last_updated;
- if (ff > 9) {
- return `high`;
- }
- var suffix = "";
- if (age > 14 * 24 * 60 * 60) {
- suffix = "?";
- }
- return `${ff}${suffix}`;
- }
- function set_fair_fight(ff_response, player_id) {
- const detailed_message = get_detailed_message(ff_response, player_id);
- info_line.innerHTML = detailed_message;
- }
- function get_members() {
- var player_ids = [];
- $(".table-body > .table-row").each(function () {
- if (!$(this).find(".fallen").length) {
- if (!$(this).find(".fedded").length) {
- $(this)
- .find(".member")
- .each(function (index, value) {
- var url = value.querySelectorAll('a[href^="/profiles"]')[0].href;
- var player_id = url.match(/.*XID=(?<player_id>\d+)/).groups
- .player_id;
- player_ids.push(parseInt(player_id));
- });
- }
- }
- });
- return player_ids;
- }
- function rgbToHex(r, g, b) {
- return (
- "#" +
- ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()
- ); // Convert to hex and return
- }
- function get_ff_colour(value) {
- let r, g, b;
- // Transition from
- // blue - #2828c6
- // to
- // green - #28c628
- // to
- // red - #c62828
- if (value <= 1) {
- // Blue
- r = 0x28;
- g = 0x28;
- b = 0xc6;
- } else if (value <= 3) {
- // Transition from blue to green
- const t = (value - 1) / 2; // Normalize to range [0, 1]
- r = 0x28;
- g = Math.round(0x28 + (0xc6 - 0x28) * t);
- b = Math.round(0xc6 - (0xc6 - 0x28) * t);
- } else if (value <= 5) {
- // Transition from green to red
- const t = (value - 3) / 2; // Normalize to range [0, 1]
- r = Math.round(0x28 + (0xc6 - 0x28) * t);
- g = Math.round(0xc6 - (0xc6 - 0x28) * t);
- b = 0x28;
- } else {
- // Red
- r = 0xc6;
- g = 0x28;
- b = 0x28;
- }
- return rgbToHex(r, g, b); // Return hex value
- }
- function get_contrast_color(hex) {
- // Convert hex to RGB
- const r = parseInt(hex.slice(1, 3), 16);
- const g = parseInt(hex.slice(3, 5), 16);
- const b = parseInt(hex.slice(5, 7), 16);
- // Calculate brightness
- const brightness = r * 0.299 + g * 0.587 + b * 0.114;
- return brightness > 126 ? "black" : "white"; // Return black or white based on brightness
- }
- function get_cached_value(player_id) {
- var cached_ff_response = rD_getValue("" + player_id, null);
- try {
- cached_ff_response = JSON.parse(cached_ff_response);
- } catch {
- cached_ff_response = null;
- }
- if (
- cached_ff_response &&
- cached_ff_response.value &&
- !cached_ff_response.no_data &&
- cached_ff_response.expiry > Date.now()
- ) {
- return cached_ff_response;
- }
- return null;
- }
- function apply_fair_fight_info(_) {
- var header_li = document.createElement("li");
- header_li.tabIndex = "0";
- header_li.classList.add("table-cell");
- header_li.classList.add("lvl");
- header_li.classList.add("torn-divider");
- header_li.classList.add("divider-vertical");
- header_li.classList.add("c-pointer");
- header_li.appendChild(document.createTextNode("FF"));
- $(".table-header > .lvl")[0].after(header_li);
- $(".table-body > .table-row > .member").each(function (_, value) {
- var url = value.querySelectorAll('a[href^="/profiles"]')[0].href;
- var player_id = url.match(/.*XID=(?<player_id>\d+)/).groups.player_id;
- var fair_fight_div = document.createElement("div");
- fair_fight_div.classList.add("table-cell");
- fair_fight_div.classList.add("lvl");
- const cached = get_cached_value(player_id);
- if (cached) {
- const ff = cached.value;
- const ff_string = get_ff_string_short(cached, player_id);
- const background_colour = get_ff_colour(ff);
- const text_colour = get_contrast_color(background_colour);
- fair_fight_div.style.backgroundColor = background_colour;
- fair_fight_div.style.color = text_colour;
- fair_fight_div.style.fontWeight = "bold";
- fair_fight_div.innerHTML = ff_string;
- }
- value.nextSibling.after(fair_fight_div);
- });
- }
- function get_cache_misses(player_ids) {
- var unknown_player_ids = [];
- for (const player_id of player_ids) {
- if (!get_cached_value(player_id)) {
- unknown_player_ids.push(player_id);
- }
- }
- return unknown_player_ids;
- }
- create_text_location();
- const match1 = window.location.href.match(
- /https:\/\/www.torn.com\/profiles.php\?XID=(?<target_id>\d+)/,
- );
- const match2 = window.location.href.match(
- /https:\/\/www.torn.com\/loader.php\?sid=attack&user2ID=(?<target_id>\d+)/,
- );
- const match = match1 ?? match2;
- if (match) {
- // We're on a profile page or an attack page - get the fair fight score
- var target_id = match.groups.target_id;
- update_ff_cache([target_id], function (target_ids) {
- display_fair_fight(target_ids[0], target_id);
- });
- if (!key) {
- set_message("Limited API key needed - click to add");
- }
- } else if (
- window.location.href.startsWith("https://www.torn.com/factions.php")
- ) {
- const torn_observer = new MutationObserver(function () {
- // Find the member table - add a column if it doesn't already have one, for FF scores
- var members_list = $(".members-list")[0];
- if (members_list) {
- torn_observer.disconnect();
- var player_ids = get_members();
- update_ff_cache(player_ids, apply_fair_fight_info);
- }
- });
- torn_observer.observe(document, {
- attributes: false,
- childList: true,
- characterData: false,
- subtree: true,
- });
- if (!key) {
- set_message("Limited API key needed - click to add");
- }
- } else {
- // console.log("Did not match against " + window.location.href);
- }
- function get_player_id_in_element(element) {
- const match = element.parentElement?.href?.match(/.*XID=(?<target_id>\d+)/);
- if (match) {
- return match.groups.target_id;
- }
- const anchors = element.getElementsByTagName("a");
- for (const anchor of anchors) {
- const match = anchor.href.match(/.*XID=(?<target_id>\d+)/);
- if (match) {
- return match.groups.target_id;
- }
- }
- if (element.nodeName.toLowerCase() === "a") {
- const match = element.href.match(/.*XID=(?<target_id>\d+)/);
- if (match) {
- return match.groups.target_id;
- }
- }
- return null;
- }
- function ff_to_percent(ff) {
- // There are 3 key areas, low, medium, high
- // Low is 1-2
- // Medium is 2-4
- // High is 4+
- // If we clip high at 8 then the math becomes easy
- // The percent is 0-33% 33-66% 66%-100%
- const low_ff = 2;
- const high_ff = 4;
- const low_mid_percent = 33;
- const mid_high_percent = 66;
- ff = Math.min(ff, 8);
- var percent;
- if (ff < low_ff) {
- percent = ((ff - 1) / (low_ff - 1)) * low_mid_percent;
- } else if (ff < high_ff) {
- percent =
- ((ff - low_ff) / (high_ff - low_ff)) *
- (mid_high_percent - low_mid_percent) +
- low_mid_percent;
- } else {
- percent =
- ((ff - high_ff) / (8 - high_ff)) * (100 - mid_high_percent) +
- mid_high_percent;
- }
- return percent;
- }
- function show_cached_values(elements) {
- for (const [player_id, element] of elements) {
- element.classList.add("ff-scouter-indicator");
- if (!element.classList.contains("indicator-lines")) {
- element.classList.add("indicator-lines");
- element.style.setProperty("--arrow-width", "20px");
- // Ugly - does removing this break anything?
- element.classList.remove("small");
- element.classList.remove("big");
- //$(element).append($("<div>", { class: "ff-scouter-vertical-line-low-upper" }));
- //$(element).append($("<div>", { class: "ff-scouter-vertical-line-low-lower" }));
- //$(element).append($("<div>", { class: "ff-scouter-vertical-line-high-upper" }));
- //$(element).append($("<div>", { 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 = $("<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 = $("<span></span>", {
- 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 = `<svg class="plane-svg ${isReturning ? "returning" : ""}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
- <path d="M482.3 192c34.2 0 93.7 29 93.7 64c0 36-59.5 64-93.7 64l-116.6 0L265.2 495.9c-5.7 10-16.3 16.1-27.8 16.1l-56.2 0c-10.6 0-18.3-10.2-15.4-20.4l49-171.6L112 320 68.8 377.6c-3 4-7.8 6.4-12.8 6.4l-42 0c-7.8 0-14-6.3-14-14c0-1.3 .2-2.6 .5-3.9L32 256 .5 145.9c-.4-1.3-.5-2.6-.5-3.9c0-7.8 6.3-14 14-14l42 0c5 0 9.8 2.4 12.8 6.4L112 192l102.9 0-49-171.6C162.9 10.2 170.6 0 181.2 0l56.2 0c11.5 0 22.1 6.2 27.8 16.1L365.7 192l116.6 0z"/>
- </svg>`;
- const tornSymbol = `<svg class="torn-symbol" viewBox="0 0 24 24">
- <circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5"/>
- <text x="12" y="16" text-anchor="middle" font-family="Arial" font-weight="bold" font-size="14" fill="currentColor">T</text>
- </svg>`;
- statusEl.innerHTML = `<span class="travel-status">${tornSymbol}${planeSvg}<span class="country-abbr">${abbr}</span></span>`;
- } 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 <a href="https://ffscouter.com" target="_blank" style="color: #fff; text-decoration: underline; font-weight: bold;">ffscouter.com</a> 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);
- }
- }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址