您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Shows the expected Fair Fight score against targets. Modified to work with new ffscouter.com.
当前为
// ==UserScript== // @name FF Scouter xentac // @namespace Violentmonkey Scripts // @match https://www.torn.com/* // @version 2.3.1 // @author rDacted, xentac // @description Shows the expected Fair Fight score against targets. Modified to work with new ffscouter.com. // @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.3.1"; // This is a standalone version of FF Scouter which has been integrated into TornTools // This version is provided for TornPDA users, or those that don't use TornTools // However I (rDacted) have quit torn, so this script is provided in an unsupported manner // I encourage anyone to re-implement this script if they're willing to provide support to the community // Ensure this code can only ever run once in any page let singleton = document.getElementById("ff-scouter-run-once"); if (!singleton) { console.log(`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); /*height: 40%;*/ width: var(--arrow-width); object-fit: cover; pointer-events: none; /* Allow clicks to pass through */ } `); 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("Adding modifications to support TornPDA"); rD_xmlhttpRequest = function (details) { console.log("Attempt to make http request"); if (details.method.toLowerCase() == "get") { return PDA_httpGet(details.url) .then(details.onload) .catch(details.onerror ?? ((e) => console.error(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(e))); } else { console.log("What is this? " + details.method); } }; rD_setValue = function (name, value) { console.log("Attempted to set " + name); return localStorage.setItem(name, value); }; rD_getValue = function (name, defaultValue) { var value = localStorage.getItem(name) ?? defaultValue; //console.log("Attempted to get " + name + " -> " + value); return value; }; rD_deleteValue = function (name) { console.log("Attempted to delete " + name); return localStorage.removeItem(name); }; rD_registerMenuCommand = function () { console.log("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 = "flex"; // Use flexbox for centering info_line.style.cursor = "pointer"; // Change cursor to pointer info_line.addEventListener("click", () => { if (key === null) { const limited_key = prompt( "Enter Limited API Key", rD_getValue("limited_key", ""), ); if (limited_key) { // Store the API key with rD_setValue rD_setValue("limited_key", limited_key); key = limited_key; // Reload page window.location.reload(); } } }); var h4 = $("h4")[0]; if (h4.textContent === "Attacking") { h4.parentNode.parentNode.after(info_line); } 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; } // Deduplicate player_ids = [...new Set(player_ids)]; // Given a list of players remove any where the cache is already fresh enough // Then make a request for any unknown players and call the callback var unknown_player_ids = get_cache_misses(player_ids); if (unknown_player_ids.length > 0) { console.log(`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}`; //console.log(url); rD_xmlhttpRequest({ method: "GET", url: url, onload: function (response) { if (response.status == 200) { var ff_response = JSON.parse(response.responseText); //console.log(ff_response); if (!("error" in ff_response)) { var one_hour = 60 * 60 * 1000; var expiry = Date.now() + one_hour; ff_response.forEach((result) => { console.log(result); if (result.player_id) { id = result.player_id; // Cache the value //console.log("Caching stats for " + id); result.expiry = expiry; rD_setValue("" + id, JSON.stringify(result)); } }); callback(player_ids); } else { console.log( "FF Scouter failed to get player information. Error message: " + ff_response.error, ); } } else { console.log( "Failed to make request, status code " + response.status, ); } }, onerror: function (e) { console.error("**** error ", e); }, onabort: function (e) { console.error("**** abort ", e); }, ontimeout: function (e) { console.error("**** timeout ", e); }, }); } else { callback(player_ids); } } function get_fair_fight_response(target_id) { var cached_ff_response = rD_getValue("" + target_id, null); try { cached_ff_response = JSON.parse(cached_ff_response); } catch { cached_ff_response = null; } if (cached_ff_response) { if (cached_ff_response.expiry > Date.now()) { return cached_ff_response; } } } function display_fair_fight(target_id) { const response = get_fair_fight_response(target_id); if (response.fair_fight) { set_fair_fight(response); } } function get_ff_string(ff_response) { const ff = ff_response.fair_fight.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_ff_string_short(ff_response) { const ff = ff_response.fair_fight.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 get_detailed_message(ff_response) { const ff_string = get_ff_string(ff_response); 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)`; } } return `Fair Fight ${ff_string} ${fresh}`; } function set_fair_fight(ff_response) { const detailed_message = get_detailed_message(ff_response); set_message(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 apply_fair_fight_info(player_ids) { const fair_fights = new Object(); for (const player_id of player_ids) { 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) { if (cached_ff_response.expiry > Date.now()) { fair_fights[player_id] = cached_ff_response; } } } 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 (index, 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"); // Lookup the fair fight score from cache if (fair_fights[player_id]) { const ff = fair_fights[player_id].fair_fight; const ff_string = get_ff_string_short(fair_fights[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"; var text = document.createTextNode(ff_string); fair_fight_div.appendChild(text); } value.nextSibling.after(fair_fight_div); }); } function get_cache_misses(player_ids) { var unknown_player_ids = []; for (const player_id of player_ids) { 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.expiry < Date.now() || cached_ff_response.age > 7 * 24 * 60 * 60 ) { 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]); }); 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 get_ff(target_id) { const response = get_fair_fight_response(target_id); if (response) { return response.fair_fight; } 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 ff = get_ff(player_id); if (ff && ff.fair_fight) { 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 = $("<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.fair_fight) { // Remove any existing elements $(mini).find(".ff-scouter-mini-ff").remove(); const message = get_detailed_message(response); 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.startsWith("https://www.torn.com/page.php?sid=hof") ) { await apply_ff_gauge($('[class^="userInfoBox__"]').toArray()); } } // Update any mini-profiles // Search for profile-mini-_userProfileWrapper___iIXVW 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, }); }