FF Scouter V2

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.60
// @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_listValues
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @connect      ffscouter.com
// @license      GPL-3.0
// ==/UserScript==

const FF_VERSION = "2.60";
const API_INTERVAL = 30000;
const FF_TARGET_STALENESS = 24 * 60 * 60 * 1000; // Refresh the target list every day
const TARGET_KEY = "ffscouterv2-targets";
const memberCountdowns = {};
let apiCallInProgressCount = 0;
let currentUserId = null;

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-ff-visible {
              display: flex !important;
            }

            .ff-scouter-ff-hidden {
              display: none !important;
            }

            .ff-scouter-est-visible {
              display: flex !important;
            }

            .ff-scouter-est-hidden {
              display: none !important;
            }

            .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;
            }

            /* FF Scouter CSS Variables */
            body {
                --ff-bg-color: #f0f0f0;
                --ff-alt-bg-color: #fff;
                --ff-border-color: #ccc;
                --ff-input-color: #ccc;
                --ff-text-color: #000;
                --ff-hover-color: #ddd;
                --ff-glow-color: #4CAF50;
                --ff-success-color: #4CAF50;
            }

            body.dark-mode {
                --ff-bg-color: #333;
                --ff-alt-bg-color: #383838;
                --ff-border-color: #444;
                --ff-input-color: #504f4f;
                --ff-text-color: #ccc;
                --ff-hover-color: #555;
                --ff-glow-color: #4CAF50;
                --ff-success-color: #4CAF50;
            }

            .ff-settings-accordion {
                margin: 10px 0;
                padding: 10px;
                background-color: var(--ff-bg-color);
                border: 1px solid var(--ff-border-color);
                border-radius: 5px;
            }

            .ff-settings-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-top: 10px;
                margin-bottom: 10px;
                font-size: 1.2em;
                font-weight: bold;
                color: var(--ff-text-color);
            }

            .ff-settings-header-username {
                display: inline;
                font-style: italic;
                color: var(--ff-success-color);
            }

            .ff-settings-entry {
                display: flex;
                align-items: center;
                gap: 5px;
                margin-top: 10px;
                margin-bottom: 5px;
            }

            .ff-settings-entry p {
                margin: 0;
                color: var(--ff-text-color);
            }

            .ff-settings-input {
                width: 120px;
                padding: 5px;
                background-color: var(--ff-input-color);
                color: var(--ff-text-color);
                border: 1px solid var(--ff-border-color);
                border-radius: 3px;
            }

            .ff-settings-input.ff-blur {
                filter: blur(3px);
                transition: filter 0.5s;
            }

            .ff-settings-input.ff-blur:focus {
                filter: blur(0);
                transition: filter 0.5s;
            }

            .ff-settings-button {
                padding: 5px 10px;
                transition: background-color 0.5s;
                background-color: var(--ff-bg-color);
                cursor: pointer;
                border: 1px solid var(--ff-border-color);
                border-radius: 5px;
                color: var(--ff-text-color);
                margin-right: 10px;
            }

            .ff-settings-button:hover {
                background-color: var(--ff-hover-color);
            }

            .ff-settings-button:last-child {
                margin-right: 0;
            }

            .ff-settings-glow {
                animation: ff-glow 1s infinite alternate;
                border-width: 3px;
            }

            @keyframes ff-glow {
                0% {
                    border-color: var(--ff-border-color);
                }
                100% {
                    border-color: var(--ff-glow-color);
                }
            }

            .ff-api-explanation {
                background-color: var(--ff-alt-bg-color);
                border: 1px solid var(--ff-border-color);
                border-radius: 8px;
                color: var(--ff-text-color);
                margin-bottom: 20px;
            }

            .ff-api-explanation a {
                color: var(--ff-success-color) !important;
                text-decoration: underline;
            }

            .ff-settings-label {
                color: var(--ff-text-color);
            }

            .ff-settings-section-header {
                color: var(--ff-text-color);
                margin-top: 20px;
                margin-bottom: 10px;
                font-weight: bold;
            }

            .ff-settings-entry-large {
                margin-bottom: 15px;
            }

            .ff-settings-entry-small {
                margin-bottom: 10px;
            }

            .ff-settings-entry-section {
                margin-bottom: 20px;
            }

            .ff-settings-label-inline {
                margin-right: 10px;
                min-width: 150px;
                display: inline-block;
            }

            .ff-settings-input-wide {
                width: 200px;
            }

            .ff-settings-input-narrow {
                width: 120px;
            }

            .ff-settings-checkbox {
                margin-right: 8px;
            }

            .ff-settings-button-large {
                padding: 8px 16px;
                font-size: 14px;
                font-weight: bold;
            }

            .ff-settings-button-container {
                margin-bottom: 20px;
                text-align: center;
            }

            .ff-api-explanation-content {
                padding: 12px 16px;
                font-size: 13px;
                line-height: 1.5;
            }
        `);

  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_listValues;
  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] Generic error handler: ", 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] Generic error handler: ", 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_listValues = function () {
      const keys = [];
      for (const key in localStorage) {
        if (localStorage.hasOwnProperty(key)) {
          keys.push(key);
        }
      }
      return keys;
    };
    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_listValues = GM_listValues;
    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(
      "[FF Scouter V2]: 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.style.cursor = "pointer";
    info_line.addEventListener("click", () => {
      if (!key) {
        const limited_key = prompt(
          "[FF Scouter V2]: Enter Limited API Key",
          rD_getValue("limited_key", ""),
        );
        if (limited_key) {
          rD_setValue("limited_key", limited_key);
          key = limited_key;
          window.location.reload();
        }
      } else {
        configure_ranges();
      }
    });

    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 configure_ranges() {
    const values = get_ff_ranges(true);
    let curSetting = "";
    if (values) {
      curSetting = `${values.low},${values.high},${values.max}`;
    }
    const response = prompt(
      "Enter the low, high, and max FF you want to use, separated by commas. Empty resets to default (Default '2,4,8').",
      curSetting,
    );
    // They hit cancel
    if (response == null) {
      return;
    }
    if (response == "") {
      reset_ff_ranges();
      return;
    }
    const split = response.split(",");
    if (split.length != 3) {
      showToast(
        "Incorrect format: FF scouter ranges should be 3 numbers separated by commas [<low>,<high>,<max>]",
      );
      return;
    }
    let low = null;
    try {
      low = parseFloat(split[0]);
    } catch (e) {
      showToast("Incorrect format: FF scouter low value must be a float.");
      return;
    }
    let high = null;
    try {
      high = parseFloat(split[1]);
    } catch (e) {
      showToast("Incorrect format: FF scouter high value must be a float.");
      return;
    }
    let max = null;
    try {
      max = parseFloat(split[2]);
    } catch (e) {
      showToast("Incorrect format: FF scouter max value must be a float.");
      return;
    }

    set_ff_ranges(low, high, max);
  }

  function reset_ff_ranges() {
    rD_deleteValue("ffscouterv2-ranges");
  }

  function set_ff_ranges(low, high, max) {
    rD_setValue(
      "ffscouterv2-ranges",
      JSON.stringify({ low: low, high: high, max: max }),
    );
  }

  function get_ff_ranges(noDefault) {
    const defaultRange = { low: 2, high: 4, max: 8 };
    const rangeUnparsed = rD_getValue("ffscouterv2-ranges");
    if (!rangeUnparsed) {
      if (noDefault) {
        return null;
      }
      return defaultRange;
    }

    try {
      const parsed = JSON.parse(rangeUnparsed);
      return parsed;
    } catch (error) {
      console.log(
        "[FF Scouter V2] Problem parsing configured range, reseting values.",
      );
      reset_ff_ranges();
      if (noDefault) {
        return null;
      }
      return defaultRange;
    }
  }

  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)];

    clean_expired_data();

    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) {
            // If the same request happens in under a second, Torn PDA will return nothing
            return;
          }
          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(
                    "ffscouterv2-" + 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(
                    "ffscouterv2-" + result.player_id,
                    JSON.stringify(cacheObj),
                  );
                }
              }
            });
            callback(player_ids);
          } 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,
          );
        },
      });
    } else {
      callback(player_ids);
    }
  }

  function clean_expired_data() {
    let count = 0;
    for (const key of rD_listValues()) {
      // Try renaming the key to the new name format
      if (key.match(/^\d+$/)) {
        if (rename_if_ffscouter(key)) {
          if (clear_if_expired("ffscouterv2-" + key)) {
            count++;
          }
        }
      }
      if (key.startsWith("ffscouterv2-")) {
        if (clear_if_expired(key)) {
          count++;
        }
      }
    }
    console.log("[FF Scouter V2] Cleaned " + count + " expired values");
  }

  function rename_if_ffscouter(key) {
    const value = rD_getValue(key, null);
    if (value == null) {
      return false;
    }
    var parsed = null;
    try {
      parsed = JSON.parse(value);
    } catch {
      return false;
    }
    if (parsed == null) {
      return false;
    }
    if ((!parsed.value && !parsed.no_data) || !parsed.expiry) {
      return false;
    }

    rD_setValue("ffscouterv2-" + key, value);
    rD_deleteValue(key);
    return true;
  }

  function clear_if_expired(key) {
    const value = rD_getValue(key, null);
    var parsed = null;
    try {
      parsed = JSON.parse(value);
    } catch {
      return false;
    }
    if (
      parsed &&
      (parsed.value || parsed.no_data) &&
      parsed.expiry &&
      parsed.expiry < Date.now()
    ) {
      rD_deleteValue(key);
      return true;
    }
    return false;
  }

  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; 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 > 99) {
      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("ffscouterv2-" + 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()) {
      return cached_ff_response;
    }
    return null;
  }

  function apply_fair_fight_info(_) {
    var ff_li = document.createElement("li");
    ff_li.tabIndex = "0";
    ff_li.classList.add("table-cell");
    ff_li.classList.add("lvl");
    ff_li.classList.add("torn-divider");
    ff_li.classList.add("divider-vertical");
    ff_li.classList.add("c-pointer");
    ff_li.classList.add("ff-scouter-ff-visible");
    ff_li.onclick = () => {
      $(".ff-scouter-ff-visible").each(function (_, value) {
        value.classList.remove("ff-scouter-ff-visible");
        value.classList.add("ff-scouter-ff-hidden");
      });
      $(".ff-scouter-est-hidden").each(function (_, value) {
        value.classList.remove("ff-scouter-est-hidden");
        value.classList.add("ff-scouter-est-visible");
      });
    };
    ff_li.appendChild(document.createTextNode("FF"));
    var est_li = document.createElement("li");
    est_li.tabIndex = "0";
    est_li.classList.add("table-cell");
    est_li.classList.add("lvl");
    est_li.classList.add("torn-divider");
    est_li.classList.add("divider-vertical");
    est_li.classList.add("c-pointer");
    est_li.classList.add("ff-scouter-est-hidden");
    est_li.onclick = () => {
      $(".ff-scouter-ff-hidden").each(function (_, value) {
        value.classList.remove("ff-scouter-ff-hidden");
        value.classList.add("ff-scouter-ff-visible");
      });
      $(".ff-scouter-est-visible").each(function (_, value) {
        value.classList.remove("ff-scouter-est-visible");
        value.classList.add("ff-scouter-est-hidden");
      });
    };
    est_li.appendChild(document.createTextNode("Est"));

    if ($(".table-header > .lvl").length == 0) {
      // The .member-list doesn't have a .lvl, give up
      return;
    }
    $(".table-header > .lvl")[0].after(ff_li, est_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");
      fair_fight_div.classList.add("ff-scouter-ff-visible");

      var estimate_div = document.createElement("div");
      estimate_div.classList.add("table-cell");
      estimate_div.classList.add("lvl");
      estimate_div.classList.add("ff-scouter-est-hidden");

      const cached = get_cached_value(player_id);
      if (cached && cached.value) {
        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;

        if (cached.bs_estimate_human) {
          estimate_div.innerHTML = cached.bs_estimate_human;
        }
      }

      value.nextSibling.after(fair_fight_div, estimate_div);
    });
  }

  function get_cache_misses(player_ids) {
    var unknown_player_ids = [];
    for (const player_id of player_ids) {
      const cached = get_cached_value(player_id);
      if (!cached || !cached.value) {
        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("[FF Scouter V2]: 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("[FF Scouter V2]: 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;
      }
      const matchUserId = anchor.href.match(/.*userId=(?<target_id>\d+)/);
      if (matchUserId) {
        return matchUserId.groups.target_id;
      }
    }

    if (element.nodeName.toLowerCase() === "a") {
      const match = element.href.match(/.*XID=(?<target_id>\d+)/);
      if (match) {
        return match.groups.target_id;
      }
      const matchUserId = element.href.match(/.*userId=(?<target_id>\d+)/);
      if (matchUserId) {
        return matchUserId.groups.target_id;
      }
    }

    return null;
  }

  function ff_to_percent(ff) {
    // The percent is 0-33% 33-66% 66%-100%
    // With configurable ranges there are no guarantees that the sections are linear
    const stored_values = get_ff_ranges();
    const low_ff = stored_values.low;
    const high_ff = stored_values.high;
    const low_mid_percent = 33;
    const mid_high_percent = 66;
    ff = Math.min(ff, stored_values.max);
    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) / (stored_values.max - 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 && 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 = $("<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 && 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 = $("<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/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());
      }
    }
    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);
          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 update_ff_targets() {
    if (!key) {
      return;
    }

    const cached = get_cached_targets(false);
    if (cached) {
      return;
    }

    const url = `${BASE_URL}/api/v1/get-targets?key=${key}&inactiveonly=1&maxff=2.5&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;
    }

    const r = Math.floor(Math.random() * targets.length);
    return targets[r];
  }

  // 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")) {
      console.log("[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 = `<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();
    }
  });

  // 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.log(
        "[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];
      console.log(`[FF Scouter V2] Found local user ID: ${userId}`);
      return userId;
    }

    console.log("[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.log(
        "[FF Scouter V2] Could not find profile wrapper for settings panel",
      );
      return;
    }

    // Check if settings panel already exists
    if (
      profileWrapper.nextElementSibling?.classList.contains(
        "ff-settings-accordion",
      )
    ) {
      console.log("[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";

    // 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 = `
      <strong>Important:</strong> You must use the SAME exact API key that you use on 
      <a href="https://ffscouter.com/" target="_blank">ffscouter.com</a>.
      <br><br>
      If you're not sure which API key you used, go to 
      <a href="https://www.torn.com/preferences.php#tab=api" target="_blank">your API preferences</a> 
      and look for "FFScouter3" in your API key history comments.
    `;
    content.appendChild(apiExplanation);

    // API Key Input
    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);

    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):";
    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);

    // 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";

    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");
      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("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!");

      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";

    saveButton.addEventListener("click", function () {
      const 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 warEnabled = document.getElementById("war-monitor-toggle").checked;
      const debugEnabled = document.getElementById("debug-logs").checked;

      let hasErrors = false;

      if (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);
      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();
      }

      if (warEnabled !== wasWarEnabled) {
        if (!warEnabled) {
          window.dispatchEvent(new Event("FFScouterV2DisableWarMonitor"));
        } else {
          location.reload();
        }
      }

      showToast("Settings saved successfully!");

      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";

    clearCacheBtn.addEventListener("click", 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++;
      }

      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);

    profileWrapper.parentNode.insertBefore(
      settingsPanel,
      profileWrapper.nextSibling,
    );

    console.log("[FF Scouter V2] Settings panel created successfully");
  }

  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";

    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}`;
    }

    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;
      console.log(
        `[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,
      });
    }
  });
}