FF Scouter V3

Shows the expected Fair Fight score against targets and faction war status

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         FF Scouter V3
// @namespace    xentac-v3
// @version      3.0-alpha2
// @author       xentac [3354782], MAVRI [2402357], rDacted [2670953], Weav3r [1853324], Glasnost [1844049]
// @description  Shows the expected Fair Fight score against targets and faction war status
// @license      GPLv3
// @copyright    2026, xentac
// @match        https://www.torn.com/*
// @connect      ffscouter.com
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==

(function () {
  'use strict';

  const n=new Set;const importCSS = async e=>{n.has(e)||(n.add(e),(d=>{const t=document.createElement("style");t.textContent=d,(document.head||document.documentElement).appendChild(t);})(e));};

  var StartTime = ((StartTime2) => {
    StartTime2[StartTime2["DocumentStart"] = 0] = "DocumentStart";
    StartTime2[StartTime2["DocumentBody"] = 1] = "DocumentBody";
    StartTime2[StartTime2["DocumentEnd"] = 2] = "DocumentEnd";
    return StartTime2;
  })(StartTime || {});
  var TornApiError;
  (function(TornApiError2) {
    TornApiError2[TornApiError2["UNKNOWN_ERROR"] = 0] = "UNKNOWN_ERROR";
    TornApiError2[TornApiError2["KEY_EMPTY"] = 1] = "KEY_EMPTY";
    TornApiError2[TornApiError2["INCORRECT_KEY"] = 2] = "INCORRECT_KEY";
    TornApiError2[TornApiError2["WRONG_TYPE"] = 3] = "WRONG_TYPE";
    TornApiError2[TornApiError2["WRONG_FIELDS"] = 4] = "WRONG_FIELDS";
    TornApiError2[TornApiError2["TOO_MANY_REQUESTS"] = 5] = "TOO_MANY_REQUESTS";
    TornApiError2[TornApiError2["INCORRECT_ID"] = 6] = "INCORRECT_ID";
    TornApiError2[TornApiError2["INCORRECT_RELATION"] = 7] = "INCORRECT_RELATION";
    TornApiError2[TornApiError2["IP_BLOCK"] = 8] = "IP_BLOCK";
    TornApiError2[TornApiError2["API_DISABLED"] = 9] = "API_DISABLED";
    TornApiError2[TornApiError2["KEY_FEDERAL_JAIL"] = 10] = "KEY_FEDERAL_JAIL";
    TornApiError2[TornApiError2["KEY_CHANGE_ERROR"] = 11] = "KEY_CHANGE_ERROR";
    TornApiError2[TornApiError2["KEY_READ_ERROR"] = 12] = "KEY_READ_ERROR";
    TornApiError2[TornApiError2["KEY_TEMPORARILY_DISABLED_TO_INACTIVITY"] = 13] = "KEY_TEMPORARILY_DISABLED_TO_INACTIVITY";
    TornApiError2[TornApiError2["DAILY_READ_LIMIT_REACHED"] = 14] = "DAILY_READ_LIMIT_REACHED";
    TornApiError2[TornApiError2["TEMPORARY_ERROR"] = 15] = "TEMPORARY_ERROR";
    TornApiError2[TornApiError2["ACCESS_LEVEL_KEY_NOT_HIGH"] = 16] = "ACCESS_LEVEL_KEY_NOT_HIGH";
    TornApiError2[TornApiError2["BACKEND_ERROR_OCCURRED"] = 17] = "BACKEND_ERROR_OCCURRED";
    TornApiError2[TornApiError2["API_KEY_HAS_BEEN_PAUSED"] = 18] = "API_KEY_HAS_BEEN_PAUSED";
    TornApiError2[TornApiError2["MUST_BE_MIGRATED_TO_CRIMES"] = 19] = "MUST_BE_MIGRATED_TO_CRIMES";
    TornApiError2[TornApiError2["RACE_NOT_YET_FINISHED"] = 20] = "RACE_NOT_YET_FINISHED";
    TornApiError2[TornApiError2["INCORRECT_CATEGORY"] = 21] = "INCORRECT_CATEGORY";
    TornApiError2[TornApiError2["SELECTION_ONLY_AVAILABLE_API_V1"] = 22] = "SELECTION_ONLY_AVAILABLE_API_V1";
    TornApiError2[TornApiError2["SELECTION_ONLY_AVAILABLE_API_V2"] = 23] = "SELECTION_ONLY_AVAILABLE_API_V2";
    TornApiError2[TornApiError2["CLOSED_TEMPORARILY"] = 24] = "CLOSED_TEMPORARILY";
    TornApiError2[TornApiError2["INVALID_STAT_REQUESTED"] = 25] = "INVALID_STAT_REQUESTED";
    TornApiError2[TornApiError2["ONLY_CATEGORY_OR_STATS_CAN"] = 26] = "ONLY_CATEGORY_OR_STATS_CAN";
    TornApiError2[TornApiError2["MUST_BE_MIGRATED_TO_ORGANIZED"] = 27] = "MUST_BE_MIGRATED_TO_ORGANIZED";
    TornApiError2[TornApiError2["INCORRECT_LOG_ID"] = 28] = "INCORRECT_LOG_ID";
    TornApiError2[TornApiError2["CATEGORY_SELECTION_NOT_AVAILABLE_FOR"] = 29] = "CATEGORY_SELECTION_NOT_AVAILABLE_FOR";
  })(TornApiError || (TornApiError = {}));
  class HTTPClient {
    canAbort() {
      return false;
    }
  }
  class AbortableHTTPClient extends HTTPClient {
    canAbort() {
      return true;
    }
  }
  class FetchHTTPClient extends AbortableHTTPClient {
    async getJson(url, timeout = void 0) {
      let response;
      if (timeout !== void 0) {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), timeout);
        response = await fetch(url, { signal: controller.signal });
        clearTimeout(timeoutId);
      } else {
        response = await fetch(url);
      }
      return await response.json();
    }
  }
  class TornApiClient {
    httpClient;
    defaultComment;
    defaultTimeout;
    constructor(options = {}) {
      this.httpClient = options.httpClient ?? new FetchHTTPClient();
      this.defaultComment = options.defaultComment;
      this.defaultTimeout = options.defaultTimeout;
    }
    async getV1({ section, selections, id, params = {}, key, comment, cache, expiry, timeout }) {
      const cached = await cache?.get({
        section,
        selections,
        id,
        params,
        key
      });
      if (cached)
        return cached;
      let url = `https://api.torn.com/${section}/${id ?? ""}`;
      url = this.populateUrl(url, key, selections ?? [], comment, params ?? {});
      if (this.httpClient.canAbort() && typeof timeout === "number") {
        return this.httpClient.getJson(url, timeout).then(addToCache).catch(this.handleError);
      } else {
        return this.httpClient.getJson(url).then(addToCache).catch(this.handleError);
      }
      function addToCache(response) {
        if ("error" in response)
          return response;
        cache?.set({
          section,
          selections,
          id,
          params,
          key
        }, response, expiry ?? Date.now() + 3e4);
        return response;
      }
    }
    async getV2({ section, selections, id, params = {}, key, comment, cache, expiry, timeout }) {
      const cached = await cache?.get({
        section,
        selections,
        id,
        params,
        key
      });
      if (cached)
        return cached;
      let url = `https://api.torn.com/v2/${section}/${id ?? ""}`;
      url = this.populateUrl(url, key, selections ?? [], comment, params ?? {});
      if (this.httpClient.canAbort() && typeof timeout === "number") {
        return this.httpClient.getJson(url, timeout).then(addToCache).catch(this.handleError);
      } else {
        return this.httpClient.getJson(url).then(addToCache).catch(this.handleError);
      }
      function addToCache(response) {
        if ("error" in response)
          return response;
        cache?.set({
          section,
          selections,
          id,
          params,
          key
        }, response, expiry ?? Date.now() + 3e4);
        return response;
      }
    }
    handleError(error) {
      console.error(error);
      return { error: { code: -1, error: generateErrorString(error) } };
      function generateErrorString(e2) {
        switch (typeof e2) {
          case "string":
            return e2;
          case "object": {
            if (e2 instanceof Error)
              return e2.message;
            return JSON.stringify(e2);
          }
          default:
            return e2.toString();
        }
      }
    }
    populateUrl(url, key, selections, comment, params) {
      const allParams = {
        key,
        comment: comment ?? this.defaultComment,
        selections: selections.length ? selections.join(",") : void 0,
        ...params
      };
      const query = Object.entries(allParams).filter((entry) => !!entry[1]).map(([key2, value]) => `${key2}=${value}`).join("&");
      return `${url}?${query}`;
    }
  }
  class Logger {
constructor(prefix = "", level = 0) {
      this.colors = {
        debug: "#7f8c8d",
        info: "#3498db",
        warn: "#f39c12",
        error: "#e74c3c"
      };
      this.prefix = prefix;
      this.level = level;
    }
setLevel(level) {
      this.level = level;
    }
debug(...args) {
      if (this.level <= 0) {
        console.debug(
          `%c${this.formatPrefix("DEBUG")}`,
          `color: ${this.colors.debug}; font-weight: bold`,
          ...args
        );
      }
    }
info(...args) {
      if (this.level <= 1) {
        console.info(
          `%c${this.formatPrefix("INFO")}`,
          `color: ${this.colors.info}; font-weight: bold`,
          ...args
        );
      }
    }
warn(...args) {
      if (this.level <= 2) {
        console.warn(
          `%c${this.formatPrefix("WARN")}`,
          `color: ${this.colors.warn}; font-weight: bold`,
          ...args
        );
      }
    }
error(...args) {
      if (this.level <= 3) {
        console.error(
          `%c${this.formatPrefix("ERROR")}`,
          `color: ${this.colors.error}; font-weight: bold`,
          ...args
        );
      }
    }
group(label, collapsed = false) {
      if (this.level < 4) {
        if (collapsed) {
          console.groupCollapsed(this.formatPrefix(""), label);
        } else {
          console.group(this.formatPrefix(""), label);
        }
      }
    }
groupEnd() {
      if (this.level < 4) {
        console.groupEnd();
      }
    }
child(subPrefix) {
      const childPrefix = this.prefix ? `${this.prefix}:${subPrefix}` : subPrefix;
      return new Logger(childPrefix, this.level);
    }
formatPrefix(level) {
      const prefix = this.prefix ? `[${this.prefix}]` : "";
      return level ? `${prefix} - [${level}]: ` : `${prefix}: `;
    }
  }
  const logger = new Logger("FFSV3");
  const FF_SCOUTER_BASE_URL = "https://ffscouter.com/api/v1";
  new TornApiClient({
    defaultComment: "FFScouterV3",
    defaultTimeout: 30
});
  async function gmRequest(options) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        ...options,
        onload: (response) => resolve(response),
        onerror: (err) => reject(err),
        ontimeout: () => reject(new Error("Timeout making GM_xmlhttpRequest"))
      });
    });
  }
  const make_stats_url = (key, player_ids) => {
    const query = new URLSearchParams([
      ["key", key],
      ["targets", player_ids.toString()]
    ]);
    return `${FF_SCOUTER_BASE_URL}/get-stats?${query.toString()}`;
  };
  function is_ff_success(resp) {
    return resp.code === void 0;
  }
  function is_ff_check_success(resp) {
    return resp.code === void 0;
  }
  class FFApiError extends Error {
    constructor(message, options) {
      super(message, options);
      this.ff_api_limits = options?.ff_api_limits;
      this.ff_api_error = options?.ff_api_error;
    }
  }
  const query_stats = async (key, player_ids, requester = gmRequest) => {
    logger.debug("Calling query_stats with arguments", { key, player_ids });
    const url = make_stats_url(key, player_ids);
    const resp = await requester({
      method: "GET",
      url
    });
    if (!resp) {
      return { result: new Map(), blank: true };
    }
    const limits = parse_limit_headers(resp.responseHeaders);
    let ff_response = null;
    try {
      ff_response = JSON.parse(resp.responseText);
    } catch {
      throw new FFApiError(
        `API request failed. Couldn't parse response. HTTP status code: ${resp.status}`,
        { ff_api_limits: limits }
      );
    }
    if (ff_response == null) {
      throw new FFApiError(
        `API request failed. Response not set. HTTP status code: ${resp.status}`,
        { ff_api_limits: limits }
      );
    }
    if (!is_ff_success(ff_response)) {
      throw new FFApiError(
        `API request failed. Error: ${ff_response.error}; Code: ${ff_response.code}`,
        { ff_api_error: ff_response, ff_api_limits: limits }
      );
    }
    if (resp.status !== 200) {
      throw new FFApiError(
        `API request failed. HTTP status code: ${resp.status}`,
        { ff_api_limits: limits }
      );
    }
    const results = new Map();
    ff_response.forEach((result) => {
      if (result?.player_id) {
        if (!result.fair_fight || !result.last_updated || !result.bs_estimate || !result.bs_estimate_human || !result.bss_public || !result.source) {
          results.set(result.player_id, {
            no_data: true,
            player_id: result.player_id
          });
        } else {
          let distribution;
          if (result.distribution) {
            distribution = {
              last_updated: result.distribution.last_updated,
              distribution_human: result.distribution.distribution_human,
              stats_percentage: {
                strength: result.distribution.stats_percentage?.strength,
                speed: result.distribution.stats_percentage?.speed,
                defense: result.distribution.stats_percentage?.defense,
                dexterity: result.distribution.stats_percentage?.dexterity
              }
            };
          }
          results.set(result.player_id, {
            no_data: false,
            fair_fight: result.fair_fight,
            last_updated: result.last_updated,
            bs_estimate: result.bs_estimate,
            bs_estimate_human: result.bs_estimate_human,
            bss_public: result.bss_public,
            source: result.source ?? "bss",
            premium_insights_available: result.premium_insights_available ?? false,
            distribution,
            player_id: result.player_id
          });
        }
      }
    });
    for (const id of player_ids) {
      if (!results.get(id)) {
        results.set(id, {
          no_data: true,
          player_id: id
        });
      }
    }
    return { result: results, blank: false, limits };
  };
  const parse_limit_headers = (responseHeaders) => {
    const headerLines = responseHeaders.split("\n");
    const headers = new Map();
    for (const line of headerLines) {
      const [key, value] = line.split(":", 2);
      if (!key || !value) {
        continue;
      }
      headers.set(key, value.trim());
    }
    const reset_time_str = headers.get("x-ratelimit-reset-timestamp");
    const remaining_str = headers.get("x-ratelimit-remaining");
    const rate_limit_str = headers.get("x-ratelimit-limit");
    if (reset_time_str && remaining_str && rate_limit_str) {
      const remaining = parseInt(remaining_str, 10);
      const rate_limit = parseInt(rate_limit_str, 10);
      const this_minute = rate_limit - remaining;
      return {
        reset_time: new Date(parseInt(reset_time_str, 10) * 1e3),
        remaining,
        rate_limit,
        this_minute
      };
    }
  };
  const check_key = async (key, requester = gmRequest) => {
    const query = new URLSearchParams([["key", key]]);
    const url = `${FF_SCOUTER_BASE_URL}/check-key?${query.toString()}`;
    const resp = await requester({
      method: "GET",
      url
    });
    if (!resp) {
      return { blank: true };
    }
    const limits = parse_limit_headers(resp.responseHeaders);
    let ff_response = null;
    try {
      ff_response = JSON.parse(resp.responseText);
    } catch {
      throw new FFApiError(
        `API request failed. Couldn't parse response. HTTP status code: ${resp.status}`,
        { ff_api_limits: limits }
      );
    }
    if (ff_response == null) {
      throw new FFApiError(
        `API request failed. Response not set. HTTP status code: ${resp.status}`,
        { ff_api_limits: limits }
      );
    }
    if (!is_ff_check_success(ff_response)) {
      throw new FFApiError(
        `API request failed. Error: ${ff_response.error}; Code: ${ff_response.code}`,
        { ff_api_error: ff_response, ff_api_limits: limits }
      );
    }
    if (resp.status !== 200) {
      throw new FFApiError(
        `API request failed. HTTP status code: ${resp.status}`,
        { ff_api_limits: limits }
      );
    }
    return { result: ff_response, blank: false, limits };
  };
  const make_flights_url = (key, target) => {
    const query = new URLSearchParams([
      ["key", key],
      ["target", target.toString()]
    ]);
    return `${FF_SCOUTER_BASE_URL}/player-flights?${query.toString()}`;
  };
  function is_flight_success(resp) {
    return resp.code === void 0;
  }
  const query_flights = async (key, target, requester = gmRequest) => {
    logger.debug("Calling query_flights with arguments", { key, target });
    const url = make_flights_url(key, target);
    const resp = await requester({
      method: "GET",
      url
    });
    if (!resp) {
      return { blank: true };
    }
    const limits = parse_limit_headers(resp.responseHeaders);
    let ff_response = null;
    try {
      ff_response = JSON.parse(resp.responseText);
    } catch {
      throw new FFApiError(
        `API request failed. Couldn't parse response. HTTP status code: ${resp.status}`,
        { ff_api_limits: limits }
      );
    }
    if (ff_response == null) {
      throw new FFApiError(
        `API request failed. Response not set. HTTP status code: ${resp.status}`,
        { ff_api_limits: limits }
      );
    }
    if (!is_flight_success(ff_response)) {
      throw new FFApiError(
        `API request failed. Error: ${ff_response.error}; Code: ${ff_response.code}`,
        { ff_api_error: ff_response, ff_api_limits: limits }
      );
    }
    if (resp.status !== 200) {
      throw new FFApiError(
        `API request failed. HTTP status code: ${resp.status}`,
        { ff_api_limits: limits }
      );
    }
    return { result: ff_response, blank: false, limits };
  };
  const query_targets = async (key, params, requester = gmRequest) => {
    logger.debug("Calling query_targets with arguments", { key, params });
    const query = new URLSearchParams([["key", key]]);
    if (params.minlevel !== void 0 && params.minlevel !== null) {
      query.append("minlevel", params.minlevel.toString());
    }
    if (params.maxlevel !== void 0 && params.maxlevel !== null) {
      query.append("maxlevel", params.maxlevel.toString());
    }
    if (params.minff !== void 0 && params.minff !== null) {
      query.append("minff", params.minff.toString());
    }
    if (params.maxff !== void 0 && params.maxff !== null) {
      query.append("maxff", params.maxff.toString());
    }
    if (params.inactiveonly !== void 0 && params.inactiveonly !== null) {
      query.append("inactiveonly", params.inactiveonly.toString());
    }
    if (params.factionless !== void 0 && params.factionless !== null) {
      query.append("factionless", params.factionless.toString());
    }
    if (params.limit !== void 0 && params.limit !== null) {
      query.append("limit", params.limit.toString());
    }
    const url = `${FF_SCOUTER_BASE_URL}/get-targets?${query.toString()}`;
    const resp = await requester({
      method: "GET",
      url
    });
    if (!resp) {
      throw new Error("Empty response from get-targets");
    }
    if (resp.status !== 200) {
      let errMessage = `API request failed with HTTP ${resp.status}`;
      try {
        const errJson = JSON.parse(resp.responseText);
        if (errJson?.error) {
          errMessage = errJson.error;
        }
      } catch {
      }
      throw new Error(errMessage);
    }
    const parsed = JSON.parse(resp.responseText);
    if (parsed.error) {
      throw new Error(parsed.error);
    }
    return parsed;
  };
  const instanceOfAny = (object, constructors) => constructors.some((c2) => object instanceof c2);
  let idbProxyableTypes;
  let cursorAdvanceMethods;
  function getIdbProxyableTypes() {
    return idbProxyableTypes || (idbProxyableTypes = [
      IDBDatabase,
      IDBObjectStore,
      IDBIndex,
      IDBCursor,
      IDBTransaction
    ]);
  }
  function getCursorAdvanceMethods() {
    return cursorAdvanceMethods || (cursorAdvanceMethods = [
      IDBCursor.prototype.advance,
      IDBCursor.prototype.continue,
      IDBCursor.prototype.continuePrimaryKey
    ]);
  }
  const transactionDoneMap = new WeakMap();
  const transformCache = new WeakMap();
  const reverseTransformCache = new WeakMap();
  function promisifyRequest(request) {
    const promise = new Promise((resolve, reject) => {
      const unlisten = () => {
        request.removeEventListener("success", success);
        request.removeEventListener("error", error);
      };
      const success = () => {
        resolve(wrap(request.result));
        unlisten();
      };
      const error = () => {
        reject(request.error);
        unlisten();
      };
      request.addEventListener("success", success);
      request.addEventListener("error", error);
    });
    reverseTransformCache.set(promise, request);
    return promise;
  }
  function cacheDonePromiseForTransaction(tx) {
    if (transactionDoneMap.has(tx))
      return;
    const done = new Promise((resolve, reject) => {
      const unlisten = () => {
        tx.removeEventListener("complete", complete);
        tx.removeEventListener("error", error);
        tx.removeEventListener("abort", error);
      };
      const complete = () => {
        resolve();
        unlisten();
      };
      const error = () => {
        reject(tx.error || new DOMException("AbortError", "AbortError"));
        unlisten();
      };
      tx.addEventListener("complete", complete);
      tx.addEventListener("error", error);
      tx.addEventListener("abort", error);
    });
    transactionDoneMap.set(tx, done);
  }
  let idbProxyTraps = {
    get(target, prop, receiver) {
      if (target instanceof IDBTransaction) {
        if (prop === "done")
          return transactionDoneMap.get(target);
        if (prop === "store") {
          return receiver.objectStoreNames[1] ? void 0 : receiver.objectStore(receiver.objectStoreNames[0]);
        }
      }
      return wrap(target[prop]);
    },
    set(target, prop, value) {
      target[prop] = value;
      return true;
    },
    has(target, prop) {
      if (target instanceof IDBTransaction && (prop === "done" || prop === "store")) {
        return true;
      }
      return prop in target;
    }
  };
  function replaceTraps(callback) {
    idbProxyTraps = callback(idbProxyTraps);
  }
  function wrapFunction(func) {
    if (getCursorAdvanceMethods().includes(func)) {
      return function(...args) {
        func.apply(unwrap(this), args);
        return wrap(this.request);
      };
    }
    return function(...args) {
      return wrap(func.apply(unwrap(this), args));
    };
  }
  function transformCachableValue(value) {
    if (typeof value === "function")
      return wrapFunction(value);
    if (value instanceof IDBTransaction)
      cacheDonePromiseForTransaction(value);
    if (instanceOfAny(value, getIdbProxyableTypes()))
      return new Proxy(value, idbProxyTraps);
    return value;
  }
  function wrap(value) {
    if (value instanceof IDBRequest)
      return promisifyRequest(value);
    if (transformCache.has(value))
      return transformCache.get(value);
    const newValue = transformCachableValue(value);
    if (newValue !== value) {
      transformCache.set(value, newValue);
      reverseTransformCache.set(newValue, value);
    }
    return newValue;
  }
  const unwrap = (value) => reverseTransformCache.get(value);
  function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) {
    const request = indexedDB.open(name, version);
    const openPromise = wrap(request);
    if (upgrade) {
      request.addEventListener("upgradeneeded", (event) => {
        upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event);
      });
    }
    if (blocked) {
      request.addEventListener("blocked", (event) => blocked(
event.oldVersion,
        event.newVersion,
        event
      ));
    }
    openPromise.then((db) => {
      if (terminated)
        db.addEventListener("close", () => terminated());
      if (blocking) {
        db.addEventListener("versionchange", (event) => blocking(event.oldVersion, event.newVersion, event));
      }
    }).catch(() => {
    });
    return openPromise;
  }
  function deleteDB(name, { blocked } = {}) {
    const request = indexedDB.deleteDatabase(name);
    if (blocked) {
      request.addEventListener("blocked", (event) => blocked(
event.oldVersion,
        event
      ));
    }
    return wrap(request).then(() => void 0);
  }
  const readMethods = ["get", "getKey", "getAll", "getAllKeys", "count"];
  const writeMethods = ["put", "add", "delete", "clear"];
  const cachedMethods = new Map();
  function getMethod(target, prop) {
    if (!(target instanceof IDBDatabase && !(prop in target) && typeof prop === "string")) {
      return;
    }
    if (cachedMethods.get(prop))
      return cachedMethods.get(prop);
    const targetFuncName = prop.replace(/FromIndex$/, "");
    const useIndex = prop !== targetFuncName;
    const isWrite = writeMethods.includes(targetFuncName);
    if (
!(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || !(isWrite || readMethods.includes(targetFuncName))
    ) {
      return;
    }
    const method = async function(storeName, ...args) {
      const tx = this.transaction(storeName, isWrite ? "readwrite" : "readonly");
      let target2 = tx.store;
      if (useIndex)
        target2 = target2.index(args.shift());
      return (await Promise.all([
        target2[targetFuncName](...args),
        isWrite && tx.done
      ]))[0];
    };
    cachedMethods.set(prop, method);
    return method;
  }
  replaceTraps((oldTraps) => ({
    ...oldTraps,
    get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver),
    has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop)
  }));
  const advanceMethodProps = ["continue", "continuePrimaryKey", "advance"];
  const methodMap = {};
  const advanceResults = new WeakMap();
  const ittrProxiedCursorToOriginalProxy = new WeakMap();
  const cursorIteratorTraps = {
    get(target, prop) {
      if (!advanceMethodProps.includes(prop))
        return target[prop];
      let cachedFunc = methodMap[prop];
      if (!cachedFunc) {
        cachedFunc = methodMap[prop] = function(...args) {
          advanceResults.set(this, ittrProxiedCursorToOriginalProxy.get(this)[prop](...args));
        };
      }
      return cachedFunc;
    }
  };
  async function* iterate(...args) {
    let cursor = this;
    if (!(cursor instanceof IDBCursor)) {
      cursor = await cursor.openCursor(...args);
    }
    if (!cursor)
      return;
    cursor = cursor;
    const proxiedCursor = new Proxy(cursor, cursorIteratorTraps);
    ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor);
    reverseTransformCache.set(proxiedCursor, unwrap(cursor));
    while (cursor) {
      yield proxiedCursor;
      cursor = await (advanceResults.get(proxiedCursor) || cursor.continue());
      advanceResults.delete(proxiedCursor);
    }
  }
  function isIteratorProp(target, prop) {
    return prop === Symbol.asyncIterator && instanceOfAny(target, [IDBIndex, IDBObjectStore, IDBCursor]) || prop === "iterate" && instanceOfAny(target, [IDBIndex, IDBObjectStore]);
  }
  replaceTraps((oldTraps) => ({
    ...oldTraps,
    get(target, prop, receiver) {
      if (isIteratorProp(target, prop))
        return iterate;
      return oldTraps.get(target, prop, receiver);
    },
    has(target, prop) {
      return isIteratorProp(target, prop) || oldTraps.has(target, prop);
    }
  }));
  const STORES = {
    CACHE: "cache",
    FLIGHTS: "flights",
    ANALYTICS: "analytics"
  };
  class FFCache {
    constructor(db_name) {
      this.db = null;
      this.db_version = 3;
      this.cache_interval = 60 * 60 * 1e3;
      this.migrations = new Map([
        [
          1,
          (db, _2) => {
            const store = db.createObjectStore(STORES.CACHE, {
              keyPath: "player_id"
            });
            store.createIndex("expiry", "expiry", {
              unique: false
            });
          }
        ],
        [
          2,
          (db, _2) => {
            const store = db.createObjectStore(STORES.FLIGHTS, {
              keyPath: "player_id"
            });
            store.createIndex("expiry", "expiry", {
              unique: false
            });
          }
        ],
        [
          3,
          (db, _2) => {
            const store = db.createObjectStore(STORES.ANALYTICS, {
              keyPath: "id",
              autoIncrement: true
            });
            store.createIndex("timestamp", "timestamp", {
              unique: false
            });
          }
        ]
      ]);
      this.open = async () => {
        if (this.db) {
          return this.db;
        }
        const cache = this;
        this.db = await openDB(this.db_name, this.db_version, {
          upgrade(db, oldVersion, newVersion, transaction, _event) {
            logger.info("Need to upgrade from", oldVersion, "to", newVersion);
            for (let i2 = (oldVersion ?? 0) + 1; i2 <= cache.db_version; i2++) {
              logger.debug(`Migration: ${i2}`);
              const m2 = cache.migrations.get(i2);
              if (m2) {
                m2(db, transaction);
              } else {
                logger.debug(`Migration not found: ${i2}`);
              }
              logger.debug(`Migration complete: ${i2}`);
            }
          },
          blocking(currentVersion, blockedVersion, _event) {
            logger.debug(
              `Can't open ${blockedVersion} because ${currentVersion} is open. Closing and reopening.`
            );
            cache.db?.close();
          }
});
        return this.db;
      };
      this.close = () => {
        if (this.db) {
          this.db.close();
          this.db = null;
        }
      };
      this.delete_db = async () => {
        this.close();
        await deleteDB(this.db_name, {
          blocked: () => {
            logger.debug("deleteDB blocked callback called!");
          }
        });
        logger.info(`Successfully deleted ${this.db_name} IndexedDB.`);
      };
      this.get = async (player_ids) => {
        const db = await this.open();
        const tx = db.transaction(STORES.CACHE, "readonly");
        const requests = player_ids.map((id) => tx.store.get(id));
        const entries = await Promise.all(requests);
        await tx.done;
        const result = new Map();
        player_ids.forEach((id, idx) => {
          const value = entries[idx];
          result.set(id, !value || value.expiry <= Date.now() ? null : value);
        });
        this.close();
        return result;
      };
      this.update = async (values) => {
        const db = await this.open();
        const tx = db.transaction(STORES.CACHE, "readwrite");
        const values_expiry = values.map((value) => {
          return {
            ...value,
            expiry: Date.now() + this.cache_interval
          };
        });
        const requests = values_expiry.map((value) => {
          return tx.store.put(value);
        });
        await Promise.all(requests);
        await tx.done;
        this.close();
      };
      this.clean_expired = async () => {
        const db = await this.open();
        {
          const tx = db.transaction(STORES.CACHE, "readwrite");
          const index = tx.store.index("expiry");
          const range = IDBKeyRange.upperBound(Date.now());
          const r2 = await index.getAllKeys(range);
          logger.info(`Found ${r2.length} expired values to delete from cache.`);
          await Promise.all(r2.map((id) => tx.store.delete(id)));
          await tx.done;
        }
        {
          const tx = db.transaction(STORES.FLIGHTS, "readwrite");
          const index = tx.store.index("expiry");
          const range = IDBKeyRange.upperBound(Date.now());
          const r2 = await index.getAllKeys(range);
          logger.info(`Found ${r2.length} expired values to delete from flights.`);
          await Promise.all(r2.map((id) => tx.store.delete(id)));
          await tx.done;
        }
        {
          const tx = db.transaction(STORES.ANALYTICS, "readwrite");
          const index = tx.store.index("timestamp");
          const thirty_days_ago = Date.now() - 30 * 24 * 60 * 60 * 1e3;
          const range = IDBKeyRange.upperBound(thirty_days_ago);
          const r2 = await index.getAllKeys(range);
          logger.info(`Found ${r2.length} expired values to delete from analytics.`);
          await Promise.all(r2.map((id) => tx.store.delete(id)));
          await tx.done;
        }
        this.close();
      };
      this.get_flight = async (player_id) => {
        const db = await this.open();
        const tx = db.transaction(STORES.FLIGHTS, "readonly");
        const entry = await tx.store.get(player_id);
        await tx.done;
        this.close();
        if (!entry || entry.expiry <= Date.now()) {
          return null;
        }
        return entry;
      };
      this.update_flight = async (value, cache_interval = 60 * 1e3) => {
        const db = await this.open();
        const tx = db.transaction(STORES.FLIGHTS, "readwrite");
        const value_expiry = {
          ...value,
          expiry: Date.now() + cache_interval
        };
        await tx.store.put(value_expiry);
        await tx.done;
        this.close();
      };
      this.add_analytics = async (entry) => {
        const db = await this.open();
        const tx = db.transaction(STORES.ANALYTICS, "readwrite");
        const value = {
          ...entry,
          timestamp: Date.now()
        };
        await tx.store.add(value);
        await tx.done;
        this.close();
      };
      this.get_analytics = async () => {
        const db = await this.open();
        const tx = db.transaction(STORES.ANALYTICS, "readonly");
        const res = await tx.store.getAll();
        await tx.done;
        this.close();
        return res;
      };
      this.dump = async () => {
        const db = await this.open();
        const tx = db.transaction(STORES.CACHE, "readonly");
        const res = await tx.store.getAll();
        await tx.done;
        this.close();
        return res;
      };
      this.dump_flights = async () => {
        const db = await this.open();
        const tx = db.transaction(STORES.FLIGHTS, "readonly");
        const res = await tx.store.getAll();
        await tx.done;
        this.close();
        return res;
      };
      this.db_name = db_name;
    }
  }
  var Time = ((Time2) => {
    Time2[Time2["Seconds"] = 1e3] = "Seconds";
    Time2[Time2["Minutes"] = 6e4] = "Minutes";
    Time2[Time2["Hours"] = 36e5] = "Hours";
    Time2[Time2["Days"] = 864e5] = "Days";
    Time2[Time2["Weeks"] = 6048e5] = "Weeks";
    Time2[Time2["Years"] = 31536e6] = "Years";
    return Time2;
  })(Time || {});
  class Storage {
constructor(prefix) {
      this.prefix = prefix;
    }
set(key, value, expireConfig) {
      try {
        const item = {
          value,
          expiration: expireConfig ? Date.now() + expireConfig.amount * (expireConfig.unit || 6e4) : null
        };
        localStorage.setItem(this.prefix + key, JSON.stringify(item));
      } catch (error) {
        logger.error(`Error storing item '${key}':`, error);
      }
    }
get(key) {
      try {
        const itemStr = localStorage.getItem(this.prefix + key);
        if (!itemStr) {
          return null;
        }
        let item = null;
        try {
          item = JSON.parse(itemStr);
        } catch {
          item = null;
        }
        if (!item) {
          logger.warn(`Key '${key}' has invalid JSON in it.`);
          this.remove(key);
          return null;
        }
        if (item.expiration && Date.now() > item.expiration) {
          this.remove(key);
          logger.debug(`Key ${key} has expired.`);
          return null;
        }
        return item.value;
      } catch (error) {
        logger.error(`Error retrieving item '${key}':`, error);
        return null;
      }
    }
remove(key) {
      try {
        localStorage.removeItem(this.prefix + key);
      } catch (error) {
        logger.error(`Error removing item [${key}]:`, error);
      }
    }
has(key) {
      return this.get(key) !== null;
    }
clearAll() {
      try {
        Object.keys(localStorage).filter((key) => key.startsWith(this.prefix)).forEach((key) => {
          localStorage.removeItem(key);
        });
      } catch (error) {
        logger.error("Error clearing storage:", error);
      }
    }
  }
  var FactionsColDisplay = ((FactionsColDisplay2) => {
    FactionsColDisplay2["FAIR_FIGHT"] = "fair_fight";
    FactionsColDisplay2["BATTLE_STATS"] = "battle_stats";
    FactionsColDisplay2["NONE"] = "none";
    return FactionsColDisplay2;
  })(FactionsColDisplay || {});
  const CONFIG_DEFAULTS = {
    low_ff_range: 2,
    high_ff_range: 4,
    max_ff_range: 8,
    chain_button_enabled: true,
    chain_link_type: "attack",
    chain_tab_type: "newtab",
    chain_ff_target: 2.5,
    ff_history_enabled: true,
    factions_col_display: "battle_stats",
    debug_logs: false,
    analytics_enabled: false,
    chain_min_level: null,
    chain_max_level: null,
    chain_inactive: true,
    chain_min_ff: null,
    chain_max_ff: 2.5,
    chain_factionless: false
  };
  class FFConfig {
    constructor(name) {
      this.name = name;
      this.storage = new Storage(this.name);
    }
    get key() {
      return this.storage.get(
        "key"
) ?? "";
    }
    set key(key) {
      this.storage.set("key", key);
    }
    get low_ff_range() {
      return this.storage.get(
        "low_ff_range"
) ?? CONFIG_DEFAULTS.low_ff_range;
    }
    set low_ff_range(val) {
      this.storage.set("low_ff_range", val);
    }
    get high_ff_range() {
      return this.storage.get(
        "high_ff_range"
) ?? CONFIG_DEFAULTS.high_ff_range;
    }
    set high_ff_range(val) {
      this.storage.set("high_ff_range", val);
    }
    get max_ff_range() {
      return this.storage.get(
        "max_ff_range"
) ?? CONFIG_DEFAULTS.max_ff_range;
    }
    set max_ff_range(val) {
      this.storage.set("max_ff_range", val);
    }
    get chain_button_enabled() {
      return this.storage.get(
        "chain_button_enabled"
) ?? CONFIG_DEFAULTS.chain_button_enabled;
    }
    set chain_button_enabled(val) {
      this.storage.set("chain_button_enabled", val);
    }
    get chain_link_type() {
      return this.storage.get(
        "chain_link_type"
) ?? CONFIG_DEFAULTS.chain_link_type;
    }
    set chain_link_type(val) {
      this.storage.set("chain_link_type", val);
    }
    get chain_tab_type() {
      return this.storage.get(
        "chain_tab_type"
) ?? CONFIG_DEFAULTS.chain_tab_type;
    }
    set chain_tab_type(val) {
      this.storage.set("chain_tab_type", val);
    }
    get chain_ff_target() {
      return this.storage.get(
        "chain_ff_target"
) ?? CONFIG_DEFAULTS.chain_ff_target;
    }
    set chain_ff_target(val) {
      this.storage.set("chain_ff_target", val);
    }
    get chain_min_level() {
      return this.storage.get(
        "chain_min_level"
) ?? CONFIG_DEFAULTS.chain_min_level;
    }
    set chain_min_level(val) {
      if (val === null) {
        this.storage.remove(
          "chain_min_level"
);
      } else {
        this.storage.set("chain_min_level", val);
      }
    }
    get chain_max_level() {
      return this.storage.get(
        "chain_max_level"
) ?? CONFIG_DEFAULTS.chain_max_level;
    }
    set chain_max_level(val) {
      if (val === null) {
        this.storage.remove(
          "chain_max_level"
);
      } else {
        this.storage.set("chain_max_level", val);
      }
    }
    get chain_inactive() {
      return this.storage.get(
        "chain_inactive"
) ?? CONFIG_DEFAULTS.chain_inactive;
    }
    set chain_inactive(val) {
      this.storage.set("chain_inactive", val);
    }
    get chain_min_ff() {
      return this.storage.get(
        "chain_min_ff"
) ?? CONFIG_DEFAULTS.chain_min_ff;
    }
    set chain_min_ff(val) {
      if (val === null) {
        this.storage.remove(
          "chain_min_ff"
);
      } else {
        this.storage.set("chain_min_ff", val);
      }
    }
    get chain_max_ff() {
      return this.storage.get(
        "chain_max_ff"
) ?? this.storage.get(
        "chain_ff_target"
) ?? CONFIG_DEFAULTS.chain_max_ff;
    }
    set chain_max_ff(val) {
      this.storage.set("chain_max_ff", val);
      this.storage.set("chain_ff_target", val);
    }
    get chain_factionless() {
      return this.storage.get(
        "chain_factionless"
) ?? CONFIG_DEFAULTS.chain_factionless;
    }
    set chain_factionless(val) {
      this.storage.set("chain_factionless", val);
    }
    get ff_history_enabled() {
      return this.storage.get(
        "ff_history_enabled"
) ?? CONFIG_DEFAULTS.ff_history_enabled;
    }
    set ff_history_enabled(val) {
      this.storage.set("ff_history_enabled", val);
    }
    get factions_col_display() {
      return this.storage.get(
        "factions_col_display"
) ?? CONFIG_DEFAULTS.factions_col_display;
    }
    set factions_col_display(val) {
      this.storage.set("factions_col_display", val);
    }
    get debug_logs() {
      return this.storage.get(
        "debug_logs"
) ?? CONFIG_DEFAULTS.debug_logs;
    }
    set debug_logs(val) {
      this.storage.set("debug_logs", val);
    }
    get analytics_enabled() {
      return this.storage.get(
        "analytics_enabled"
) ?? CONFIG_DEFAULTS.analytics_enabled;
    }
    set analytics_enabled(val) {
      this.storage.set("analytics_enabled", val);
    }
    get faction_filter_state() {
      return this.storage.get(
        "faction_filter_state"
) ?? null;
    }
    set faction_filter_state(val) {
      this.storage.set("faction_filter_state", val);
    }
    get faction_filter_collapsed() {
      return this.storage.get(
        "faction_filter_collapsed"
) ?? false;
    }
    set faction_filter_collapsed(val) {
      this.storage.set("faction_filter_collapsed", val);
    }
    get war_filter_state() {
      return this.storage.get(
        "war_filter_state"
) ?? null;
    }
    set war_filter_state(val) {
      this.storage.set("war_filter_state", val);
    }
    get war_filter_collapsed() {
      return this.storage.get(
        "war_filter_collapsed"
) ?? false;
    }
    set war_filter_collapsed(val) {
      this.storage.set("war_filter_collapsed", val);
    }
    get chain_targets() {
      return this.storage.get(
        "chain_targets"
);
    }
    set chain_targets(val) {
      if (val === null) {
        this.storage.remove(
          "chain_targets"
);
      } else {
        this.storage.set("chain_targets", val);
      }
    }
    get chain_target_index() {
      return this.storage.get(
        "chain_target_index"
) ?? 0;
    }
    set chain_target_index(val) {
      this.storage.set("chain_target_index", val);
    }
    reset() {
      this.storage.remove(
        "low_ff_range"
);
      this.storage.remove(
        "high_ff_range"
);
      this.storage.remove(
        "max_ff_range"
);
      this.storage.remove(
        "chain_button_enabled"
);
      this.storage.remove(
        "chain_link_type"
);
      this.storage.remove(
        "chain_tab_type"
);
      this.storage.remove(
        "chain_ff_target"
);
      this.storage.remove(
        "ff_history_enabled"
);
      this.storage.remove(
        "factions_col_display"
);
      this.storage.remove(
        "debug_logs"
);
      this.storage.remove(
        "analytics_enabled"
);
      this.storage.remove(
        "faction_filter_state"
);
      this.storage.remove(
        "faction_filter_collapsed"
);
      this.storage.remove(
        "war_filter_state"
);
      this.storage.remove(
        "war_filter_collapsed"
);
      this.storage.remove(
        "chain_min_level"
);
      this.storage.remove(
        "chain_max_level"
);
      this.storage.remove(
        "chain_inactive"
);
      this.storage.remove(
        "chain_min_ff"
);
      this.storage.remove(
        "chain_max_ff"
);
      this.storage.remove(
        "chain_factionless"
);
      this.storage.remove(
        "chain_targets"
);
      this.storage.remove(
        "chain_target_index"
);
    }
  }
  const ffconfig = new FFConfig("ffsv3-config");
  const DB_NAME = "FFSV3-cache";
  class FFScouter {
    constructor(config, cache) {
      this.cache = new FFCache(DB_NAME);
      this.pending = new Map();
      this.cache_queue = new Set();
      this.cache_delay = 10;
      this.cache_timer = null;
      this.api_queue = new Set();
      this.api_max_batch_size = 200;
      this.api_initial_delay = 100;
      this.api_default_delay = 1e3;
      this.api_timer = null;
      this.api_attempts = 5;
      this.schedule = (fn, delay) => {
        return setTimeout(fn, delay);
      };
      this.clear = (timer) => {
        if (timer) {
          clearTimeout(timer);
        }
      };
      this.get = (player_id) => {
        const p2 = this.pending.get(player_id);
        if (p2) {
          return p2.promise;
        }
        let resolve;
        let reject;
        const promise = new Promise((res, rej) => {
          resolve = res;
          reject = rej;
        });
        this.pending.set(player_id, { promise, resolve, reject, api_attempts: 0 });
        this.enqueue_cache(player_id);
        return promise;
      };
      this.get_flights = async (player_id) => {
        logger.debug(`get_flights called for ${player_id}`);
        let cached = null;
        try {
          cached = await this.cache.get_flight(player_id);
        } catch (err) {
          logger.error("Failed to query flight cache", err);
        }
        if (cached) {
          logger.debug(`Flight cache hit for player ${player_id}`);
          if (cached.rechecking) {
            const now = Date.now();
            if (cached.recheck_until && now >= cached.recheck_until) {
              logger.debug(
                `Rechecking window expired for player ${player_id}. Finalizing no data.`
              );
              const final_response = {
                player_id: cached.player_id,
                current: null,
                recent_flights: cached.recent_flights,
                rechecking: false
              };
              try {
                await this.cache.update_flight(final_response, 10 * 60 * 1e3);
              } catch (err) {
                logger.error("Failed to finalize flight cache", err);
              }
              return final_response;
            }
            if (cached.next_retry_at && now >= cached.next_retry_at) {
              logger.debug(
                `Retrying API call for player ${player_id} during recheck window`
              );
              let response2;
              try {
                response2 = await query_flights(this.config.key, player_id);
              } catch (err) {
                logger.error(
                  `Received error response querying ffscouter player-flights API for ${player_id}:`,
                  err
                );
                throw err;
              }
              if (response2.blank) {
                throw new Error(
                  `Empty flight response returned for player ${player_id}`
                );
              }
              if (response2.result.current) {
                logger.debug(
                  `Flight successfully tracked for player ${player_id} on retry.`
                );
                try {
                  await this.cache.update_flight(response2.result, 60 * 1e3);
                } catch (err) {
                  logger.error("Failed to update flight cache", err);
                }
                return response2.result;
              }
              logger.debug(
                `Player ${player_id} still has no flight. Scheduling next retry.`
              );
              const next_retry_at = Date.now() + 15 * 1e3;
              const updated_response = {
                player_id: cached.player_id,
                current: null,
                recent_flights: response2.result.recent_flights,
                rechecking: true,
                next_retry_at,
                recheck_until: cached.recheck_until
              };
              const remaining_ttl = Math.max(
                0,
                (cached.recheck_until ?? now) - Date.now()
              );
              try {
                await this.cache.update_flight(updated_response, remaining_ttl);
              } catch (err) {
                logger.error("Failed to update flight cache during recheck", err);
              }
              return updated_response;
            }
            return {
              player_id: cached.player_id,
              current: cached.current,
              recent_flights: cached.recent_flights,
              rechecking: true,
              next_retry_at: cached.next_retry_at,
              recheck_until: cached.recheck_until
            };
          }
          return {
            player_id: cached.player_id,
            current: cached.current,
            recent_flights: cached.recent_flights
          };
        }
        logger.debug(`Flight cache miss for player ${player_id}. Querying API.`);
        let response;
        try {
          response = await query_flights(this.config.key, player_id);
        } catch (err) {
          logger.error(
            `Received error response querying ffscouter player-flights API for ${player_id}:`,
            err
          );
          throw err;
        }
        if (response.blank) {
          throw new Error(`Empty flight response returned for player ${player_id}`);
        }
        if (response.result.current) {
          try {
            await this.cache.update_flight(response.result, 60 * 1e3);
          } catch (err) {
            logger.error("Failed to update flight cache", err);
          }
        } else {
          logger.debug(`Start rechecking cycle for player ${player_id}`);
          const now = Date.now();
          const next_retry_at = now + 15 * 1e3;
          const recheck_until = now + 3 * 60 * 1e3;
          const rechecking_response = {
            player_id: response.result.player_id,
            current: null,
            recent_flights: response.result.recent_flights,
            rechecking: true,
            next_retry_at,
            recheck_until
          };
          try {
            await this.cache.update_flight(rechecking_response, 3 * 60 * 1e3);
          } catch (err) {
            logger.error("Failed to update flight cache", err);
          }
          response = { result: rechecking_response, blank: false };
        }
        try {
          await this.cache.clean_expired();
        } catch (err) {
          logger.error("Failed to clean expired cache entries", err);
        }
        return response.result;
      };
      this.complete = () => {
        this.process_cache();
      };
      this.enqueue_cache = (player_id) => {
        logger.debug(`Enqueuing cache ${player_id}`);
        this.cache_queue.add(player_id);
        this.schedule_cache();
      };
      this.schedule_cache = () => {
        if (this.cache_timer) {
          logger.debug(`schedule_cache called but job already scheduled`);
          return;
        }
        logger.debug(
          `schedule_cache called and job scheduled for ${this.cache_delay} ms`
        );
        this.cache_timer = this.schedule(this.process_cache, this.cache_delay);
      };
      this.process_cache = async () => {
        if (this.cache_timer) {
          this.clear(this.cache_timer);
          this.cache_timer = null;
        }
        const ids = Array.from(this.cache_queue);
        this.cache_queue.clear();
        if (ids.length <= 0) {
          return;
        }
        let results;
        try {
          results = await this.cache.get(ids);
        } catch (_2) {
          results = new Map();
        }
        logger.debug("Received results", results);
        for (const id of ids) {
          const v2 = results.get(id);
          if (v2) {
            logger.debug("Id", id, "found in cache. Resolving value.");
            this.resolve(id, v2);
          } else {
            logger.debug("Id", id, "not found in cache. Scheduling api call.");
            this.enqueue_api(id);
          }
        }
      };
      this.clear_cache = () => {
        this.cache.delete_db();
      };
      this.enqueue_api = (player_id) => {
        logger.debug(`Enqueuing api ${player_id}`);
        this.api_queue.add(player_id);
        this.schedule_api();
      };
      this.schedule_api = (delay = this.api_initial_delay) => {
        if (this.api_timer) {
          logger.debug(`schedule_api called but job already scheduled`);
          return;
        }
        logger.debug(`schedule_api called and job scheduled for ${delay} ms`);
        this.api_timer = this.schedule(this.process_api, delay);
      };
      this.process_api = async () => {
        logger.debug("process_api called");
        if (this.api_timer) {
          this.clear(this.api_timer);
          this.api_timer = null;
        }
        let ids = Array.from(this.api_queue);
        if (ids.length > this.api_max_batch_size) {
          ids = ids.slice(0, this.api_max_batch_size);
        }
        for (const id of ids) {
          this.api_queue.delete(id);
        }
        logger.debug(`Processing ${ids} api requests`);
        if (ids.length <= 0) {
          logger.debug("No ids found to query");
          return;
        }
        let next_run = this.api_default_delay;
        let results;
        try {
          logger.debug("Calling query_stats with", this.config.key, ",", ids);
          results = await query_stats(this.config.key, ids);
        } catch (err) {
          logger.error("Received error response querying ffscouter api:", err);
          for (const id of ids) {
            this.reject(id, err);
          }
          const ff_error = err;
          results = {
            result: new Map(),
            blank: true,
            limits: ff_error.ff_api_limits
          };
        }
        logger.debug("Received results", results);
        if (results.blank) {
          for (const id of ids) {
            this.requeue_api(id);
          }
        } else {
          await this.cache.update(Array.from(results.result.values()));
          for (const id of ids) {
            const v2 = results.result.get(id);
            if (v2) {
              logger.debug("Id", id, "found in results. Resolving value.");
              this.resolve(id, v2);
            } else {
              logger.debug("Id", id, "not found in results. Resolving no_data.");
              this.resolve(id, { player_id: id, no_data: true });
            }
          }
        }
        if (results.limits) {
          next_run = this.calculate_next_api_run(results.limits);
        }
        this.schedule_api(next_run);
        await this.cache.clean_expired();
      };
      this.calculate_next_api_run = (limits) => {
        if (limits.remaining <= 0) {
          return limits.reset_time.getTime() - Date.now();
        } else if (limits.reset_time < new Date()) {
          return this.api_initial_delay;
        } else if (limits.rate_limit * 0.75 < limits.remaining) {
          return this.api_default_delay;
        } else {
          const ms_left = limits.reset_time.getTime() - Date.now();
          return ms_left / limits.remaining;
        }
      };
      this.resolve = (id, value) => {
        const entry = this.pending.get(id);
        if (!entry) return;
        entry.resolve(value);
        this.pending.delete(id);
      };
      this.reject = (id, err) => {
        const entry = this.pending.get(id);
        if (!entry) return;
        entry.reject(err);
        this.pending.delete(id);
      };
      this.requeue_api = (id) => {
        const entry = this.pending.get(id);
        if (!entry) return;
        entry.api_attempts++;
        if (entry.api_attempts > this.api_attempts) {
          this.reject(
            id,
            new Error(`Too many failed attempts to get stats for ${id}.`)
          );
          return false;
        }
        this.enqueue_api(id);
        return true;
      };
      this.add_analytics_entry = async (feature, player_id, status) => {
        if (!this.config.analytics_enabled) {
          return;
        }
        try {
          const url = window.location.origin + window.location.pathname;
          const params = window.location.search;
          const hash = window.location.hash;
          await this.cache.add_analytics({
            feature,
            player_id,
            status,
            url,
            params,
            hash
          });
        } catch (err) {
          logger.error("Failed to add analytics entry", err);
        }
      };
      this.get_analytics_entries = async () => {
        try {
          return await this.cache.get_analytics();
        } catch (err) {
          logger.error("Failed to get analytics entries", err);
          return [];
        }
      };
      this.config = config;
      if (cache) {
        this.cache = cache;
      }
    }
  }
  const ffscouter = new FFScouter(ffconfig);
  const HOUR = 60 * 60;
  const DAY = HOUR * 24;
  const OLD_ESTIMATE_INTERVAL = 14 * DAY;
  function format_ff_score(d2) {
    const ff = d2.fair_fight.toFixed(2);
    const now = Date.now() / 1e3;
    const age = now - d2.last_updated;
    var suffix = "";
    if (age > OLD_ESTIMATE_INTERVAL) {
      suffix = "?";
    }
    return `${ff}${suffix}`;
  }
  function format_difficulty_text(d2) {
    if (d2.fair_fight <= 1) {
      return "Extremely easy";
    } else if (d2.fair_fight <= 2) {
      return "Easy";
    } else if (d2.fair_fight <= 3.5) {
      return "Moderately difficult";
    } else if (d2.fair_fight <= 4.5) {
      return "Difficult";
    } else {
      return "May be impossible";
    }
  }
  function format_relative_time(timestamp_sec) {
    const age = Date.now() / 1e3 - timestamp_sec;
    if (age < DAY) {
      return "";
    } else if (age < 31 * DAY) {
      const days = Math.round(age / DAY);
      if (days === 1) {
        return "(1 day old)";
      } else {
        return `(${days} days old)`;
      }
    } else if (age < 365 * DAY) {
      const months = Math.round(age / (31 * DAY));
      if (months === 1) {
        return "(1 month old)";
      } else {
        return `(${months} months old)`;
      }
    } else {
      const years = Math.round(age / (365 * DAY));
      if (years === 1) {
        return "(1 year old)";
      } else {
        return `(${years} years old)`;
      }
    }
  }
  function get_ff_colour(d2) {
    return get_ff_arrow_colour(d2);
  }
  const arrow_gradient3 = [
    "#1734e8",
    "#1788e8",
    "#17dbe8",
    "#17e8a1",
    "#17e84e",
    "#34e817",
    "#88e817",
    "#dbe817",
    "#e8a117",
    "#e84e17",
    "#e81734"
  ];
  function get_ff_arrow_colour(d2) {
    if (d2.no_data) {
      return "#000000";
    }
    let ff = d2.fair_fight;
    if (ff < 1) {
      ff = 1;
    } else if (ff > 5) {
      ff = 5;
    }
    const ratio = Math.floor((ff - 1) / 4 * 10);
    const r2 = arrow_gradient3[ratio];
    return r2 ?? "#000000";
  }
  function get_contrast_color(hex) {
    const r2 = parseInt(hex.slice(1, 3), 16);
    const g2 = parseInt(hex.slice(3, 5), 16);
    const b2 = parseInt(hex.slice(5, 7), 16);
    const brightness = r2 * 0.299 + g2 * 0.587 + b2 * 0.114;
    return brightness > 126 ? "black" : "white";
  }
  function ff_to_percent(d2) {
    const low_ff = ffconfig.low_ff_range;
    const high_ff = ffconfig.high_ff_range;
    const max_ff = ffconfig.max_ff_range;
    const low_mid_percent = 33;
    const mid_high_percent = 66;
    const ff_lower = Math.min(d2.fair_fight, max_ff);
    let percent;
    if (ff_lower < low_ff) {
      percent = (ff_lower - 1) / (low_ff - 1) * low_mid_percent;
    } else if (ff_lower < high_ff) {
      percent = (ff_lower - low_ff) / (high_ff - low_ff) * (mid_high_percent - low_mid_percent) + low_mid_percent;
    } else {
      percent = (ff_lower - high_ff) / (max_ff - high_ff) * (100 - mid_high_percent) + mid_high_percent;
    }
    return percent;
  }
  function format_timestamp(ts) {
    const d2 = new Date(ts * 1e3);
    return `${d2.getHours() < 10 ? "0" : ""}${d2.getHours()}:${d2.getMinutes() < 10 ? "0" : ""}${d2.getMinutes()}:${d2.getSeconds() < 10 ? "0" : ""}${d2.getSeconds()} - ${d2.getDate() < 10 ? "0" : ""}${d2.getDate()}/${d2.getMonth() + 1 < 10 ? "0" : ""}${d2.getMonth() + 1}/${d2.getFullYear() - 2e3}`;
  }
  function parse_suffix_number(valStr) {
    const trimmed = valStr.trim().toLowerCase();
    if (!trimmed) return null;
    const match = trimmed.match(/^([\d.,]+)\s*([kmbt])?$/);
    if (!match) return null;
    const matchStr = match[1];
    if (!matchStr) return null;
    const num = Number(matchStr.replace(/,/g, ""));
    if (Number.isNaN(num)) return null;
    const suffix = match[2];
    if (!suffix) return num;
    const multiplier = {
      k: 1e3,
      m: 1e6,
      b: 1e9,
      t: 1e12
    };
    return num * (multiplier[suffix] ?? 1);
  }
  const ID_PARAMS = ["XID", "user2ID"];
  function extract_id_from_url(url) {
    const parsed = new URL(url);
    const search = new URLSearchParams(parsed.search);
    for (const param of ID_PARAMS) {
      const v2 = search.get(param);
      if (v2) {
        return parseInt(v2, 10);
      }
    }
    return null;
  }
  function torn_page(page, params = {}) {
    const url_match = window.location.href.startsWith(
      `https://www.torn.com/${page}.php`
    );
    const search = new URLSearchParams(window.location.search);
    let sid_match = true;
    let step_match = true;
    if (params.sid) {
      const page_sid = search.get("sid");
      sid_match = page_sid !== null && params.sid === page_sid;
    }
    if (params.step) {
      const page_step = search.get("step");
      step_match = page_step !== null && params.step === page_step;
    }
    return url_match && sid_match && step_match;
  }
  function make_arrow(d2) {
    const fill = get_ff_arrow_colour(d2);
    const div = document.createElement("div");
    div.innerHTML = `<svg version="1.2" id="Layer_1" x="0px" y="0px" width="20" height="13" viewBox="0 0 20 13" xml:space="preserve" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
	<path fill-rule="evenodd" fill="${fill}" stroke="#000000" d="M 0,0 H 13 20 L 10,12 Z" id="path1" style="display:inline;stroke-width:1.50;"/>
</svg>`;
    if (!div.firstChild || !(div.firstChild instanceof SVGElement)) {
      throw new Error(
        "Wasn't able to extract just created SVG out of div element"
      );
    }
    const svg = div.firstChild;
    svg.classList.add("ffsv3-arrow");
    return svg;
  }
  function add_ff_arrow(element, featureName = "Unknown") {
    const player_id = get_player_id_in_element(element);
    if (!player_id) {
      return;
    }
    if (element.querySelector(".ffsv3-gauge") || element.classList.contains("ffsv3-gauge")) {
      ffscouter.add_analytics_entry(featureName, player_id, "ignored");
      return;
    }
    ffscouter.get(player_id).then((d2) => {
      if (d2.no_data) {
        return;
      }
      if (element.querySelector(".ffsv3-gauge") || element.classList.contains("ffsv3-gauge")) {
        ffscouter.add_analytics_entry(featureName, player_id, "ignored");
        return;
      }
      const percent = ff_to_percent(d2);
      element.classList.add("ffsv3-gauge");
      element.style.setProperty("--band-percent", `${percent}`);
      const a2 = element.querySelector(".ffsv3-arrow");
      if (a2) {
        a2.remove();
      }
      element.appendChild(make_arrow(d2));
      ffscouter.add_analytics_entry(featureName, player_id, "applied");
    });
  }
  function has_href(el) {
    return el instanceof HTMLAnchorElement;
  }
  function extract_target_id(href, r2) {
    const match = href.match(r2);
    const groups = match?.groups;
    if (groups?.target_id) {
      return parseInt(groups.target_id, 10);
    }
    return null;
  }
  function get_player_id_in_element(element) {
    const parent = element.parentElement;
    if (has_href(parent)) {
      const xid = extract_target_id(parent.href, /.*XID=(?<target_id>\d+)/);
      if (xid) {
        return xid;
      }
    }
    const anchors = element.getElementsByTagName("a");
    for (const anchor of anchors) {
      const xid = extract_target_id(anchor.href, /.*XID=(?<target_id>\d+)/);
      if (xid) {
        return xid;
      }
      const userid = extract_target_id(anchor.href, /.*userId=(?<target_id>\d+)/);
      if (userid) {
        return userid;
      }
    }
    if (has_href(element)) {
      const xid = extract_target_id(element.href, /.*XID=(?<target_id>\d+)/);
      if (xid) {
        return xid;
      }
      const userid = extract_target_id(
        element.href,
        /.*userId=(?<target_id>\d+)/
      );
      if (userid) {
        return userid;
      }
    }
    return null;
  }
  async function apply_ff_gauge_selector(node_list, featureName = "Unknown") {
    for (const node of node_list) {
      add_ff_arrow(node, featureName);
    }
  }
  async function apply_ff_gauge(element, featureName = "Unknown") {
    if (!(element instanceof HTMLElement)) {
      return;
    }
    add_ff_arrow(element, featureName);
  }
  async function wait_for_element(querySelector, timeout, root) {
    const existingElement = document.querySelector(querySelector);
    if (existingElement) return existingElement;
    return new Promise((resolve) => {
      let timer;
      const observer = new MutationObserver(() => {
        const element = document.querySelector(querySelector);
        if (element) {
          cleanup();
          resolve(element);
        }
      });
      if (!root) {
        root = document.body;
      }
      observer.observe(root, {
        childList: true,
        subtree: true
      });
      if (timeout) {
        timer = setTimeout(() => {
          cleanup();
          resolve(null);
        }, timeout);
      }
      function cleanup() {
        observer.disconnect();
        if (timer) clearTimeout(timer);
      }
    });
  }
  async function wait_for_body(timeout) {
    const body = await wait_for_element(
      "body",
      timeout,
      document.documentElement
    );
    return body !== null;
  }
  function create_info_line() {
    const info_line = document.createElement("div");
    info_line.className = "ffsv3-info-line";
    info_line.style.display = "block";
    info_line.style.clear = "both";
    info_line.style.margin = "5px 0";
    return info_line;
  }
  const CHECK_KEY = "check-key-status";
  class CheckKeyStatus {
    constructor(config, storage) {
      this.check_key_status = async (force = false) => {
        if (!force) {
          const cached = this.storage.get(CHECK_KEY);
          if (cached) {
            return cached;
          }
        }
        let result;
        try {
          result = await check_key(this.config.key);
        } catch (err) {
          logger.error(
            "Received error response querying ffscouter check-key api:",
            err
          );
          result = { blank: true };
        }
        if (result.blank) {
          return null;
        }
        this.storage.set(CHECK_KEY, result.result, {
          amount: 5,
          unit: Time.Minutes
        });
        return result.result;
      };
      this.is_premium = async (force = false) => {
        const status = await this.check_key_status(force);
        if (!status) {
          return false;
        }
        return status.is_premium;
      };
      this.config = config;
      this.storage = storage;
    }
  }
  const check_key_status = new CheckKeyStatus(
    ffconfig,
    new Storage("ffsv3-check")
  );
  const t$2 = globalThis, e$2 = t$2.ShadowRoot && (void 0 === t$2.ShadyCSS || t$2.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, s$2 = Symbol(), o$4 = new WeakMap();
  let n$3 = class n {
    constructor(t2, e2, o2) {
      if (this._$cssResult$ = true, o2 !== s$2) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
      this.cssText = t2, this.t = e2;
    }
    get styleSheet() {
      let t2 = this.o;
      const s2 = this.t;
      if (e$2 && void 0 === t2) {
        const e2 = void 0 !== s2 && 1 === s2.length;
        e2 && (t2 = o$4.get(s2)), void 0 === t2 && ((this.o = t2 = new CSSStyleSheet()).replaceSync(this.cssText), e2 && o$4.set(s2, t2));
      }
      return t2;
    }
    toString() {
      return this.cssText;
    }
  };
  const r$4 = (t2) => new n$3("string" == typeof t2 ? t2 : t2 + "", void 0, s$2), S$1 = (s2, o2) => {
    if (e$2) s2.adoptedStyleSheets = o2.map((t2) => t2 instanceof CSSStyleSheet ? t2 : t2.styleSheet);
    else for (const e2 of o2) {
      const o3 = document.createElement("style"), n3 = t$2.litNonce;
      void 0 !== n3 && o3.setAttribute("nonce", n3), o3.textContent = e2.cssText, s2.appendChild(o3);
    }
  }, c$2 = e$2 ? (t2) => t2 : (t2) => t2 instanceof CSSStyleSheet ? ((t3) => {
    let e2 = "";
    for (const s2 of t3.cssRules) e2 += s2.cssText;
    return r$4(e2);
  })(t2) : t2;
  const { is: i$2, defineProperty: e$1, getOwnPropertyDescriptor: h$1, getOwnPropertyNames: r$3, getOwnPropertySymbols: o$3, getPrototypeOf: n$2 } = Object, a$1 = globalThis, c$1 = a$1.trustedTypes, l$1 = c$1 ? c$1.emptyScript : "", p$1 = a$1.reactiveElementPolyfillSupport, d$1 = (t2, s2) => t2, u$1 = { toAttribute(t2, s2) {
    switch (s2) {
      case Boolean:
        t2 = t2 ? l$1 : null;
        break;
      case Object:
      case Array:
        t2 = null == t2 ? t2 : JSON.stringify(t2);
    }
    return t2;
  }, fromAttribute(t2, s2) {
    let i2 = t2;
    switch (s2) {
      case Boolean:
        i2 = null !== t2;
        break;
      case Number:
        i2 = null === t2 ? null : Number(t2);
        break;
      case Object:
      case Array:
        try {
          i2 = JSON.parse(t2);
        } catch (t3) {
          i2 = null;
        }
    }
    return i2;
  } }, f$1 = (t2, s2) => !i$2(t2, s2), b$1 = { attribute: true, type: String, converter: u$1, reflect: false, useDefault: false, hasChanged: f$1 };
  Symbol.metadata ??= Symbol("metadata"), a$1.litPropertyMetadata ??= new WeakMap();
  let y$1 = class y extends HTMLElement {
    static addInitializer(t2) {
      this._$Ei(), (this.l ??= []).push(t2);
    }
    static get observedAttributes() {
      return this.finalize(), this._$Eh && [...this._$Eh.keys()];
    }
    static createProperty(t2, s2 = b$1) {
      if (s2.state && (s2.attribute = false), this._$Ei(), this.prototype.hasOwnProperty(t2) && ((s2 = Object.create(s2)).wrapped = true), this.elementProperties.set(t2, s2), !s2.noAccessor) {
        const i2 = Symbol(), h2 = this.getPropertyDescriptor(t2, i2, s2);
        void 0 !== h2 && e$1(this.prototype, t2, h2);
      }
    }
    static getPropertyDescriptor(t2, s2, i2) {
      const { get: e2, set: r2 } = h$1(this.prototype, t2) ?? { get() {
        return this[s2];
      }, set(t3) {
        this[s2] = t3;
      } };
      return { get: e2, set(s3) {
        const h2 = e2?.call(this);
        r2?.call(this, s3), this.requestUpdate(t2, h2, i2);
      }, configurable: true, enumerable: true };
    }
    static getPropertyOptions(t2) {
      return this.elementProperties.get(t2) ?? b$1;
    }
    static _$Ei() {
      if (this.hasOwnProperty(d$1("elementProperties"))) return;
      const t2 = n$2(this);
      t2.finalize(), void 0 !== t2.l && (this.l = [...t2.l]), this.elementProperties = new Map(t2.elementProperties);
    }
    static finalize() {
      if (this.hasOwnProperty(d$1("finalized"))) return;
      if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d$1("properties"))) {
        const t3 = this.properties, s2 = [...r$3(t3), ...o$3(t3)];
        for (const i2 of s2) this.createProperty(i2, t3[i2]);
      }
      const t2 = this[Symbol.metadata];
      if (null !== t2) {
        const s2 = litPropertyMetadata.get(t2);
        if (void 0 !== s2) for (const [t3, i2] of s2) this.elementProperties.set(t3, i2);
      }
      this._$Eh = new Map();
      for (const [t3, s2] of this.elementProperties) {
        const i2 = this._$Eu(t3, s2);
        void 0 !== i2 && this._$Eh.set(i2, t3);
      }
      this.elementStyles = this.finalizeStyles(this.styles);
    }
    static finalizeStyles(s2) {
      const i2 = [];
      if (Array.isArray(s2)) {
        const e2 = new Set(s2.flat(1 / 0).reverse());
        for (const s3 of e2) i2.unshift(c$2(s3));
      } else void 0 !== s2 && i2.push(c$2(s2));
      return i2;
    }
    static _$Eu(t2, s2) {
      const i2 = s2.attribute;
      return false === i2 ? void 0 : "string" == typeof i2 ? i2 : "string" == typeof t2 ? t2.toLowerCase() : void 0;
    }
    constructor() {
      super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();
    }
    _$Ev() {
      this._$ES = new Promise((t2) => this.enableUpdating = t2), this._$AL = new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t2) => t2(this));
    }
    addController(t2) {
      (this._$EO ??= new Set()).add(t2), void 0 !== this.renderRoot && this.isConnected && t2.hostConnected?.();
    }
    removeController(t2) {
      this._$EO?.delete(t2);
    }
    _$E_() {
      const t2 = new Map(), s2 = this.constructor.elementProperties;
      for (const i2 of s2.keys()) this.hasOwnProperty(i2) && (t2.set(i2, this[i2]), delete this[i2]);
      t2.size > 0 && (this._$Ep = t2);
    }
    createRenderRoot() {
      const t2 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
      return S$1(t2, this.constructor.elementStyles), t2;
    }
    connectedCallback() {
      this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach((t2) => t2.hostConnected?.());
    }
    enableUpdating(t2) {
    }
    disconnectedCallback() {
      this._$EO?.forEach((t2) => t2.hostDisconnected?.());
    }
    attributeChangedCallback(t2, s2, i2) {
      this._$AK(t2, i2);
    }
    _$ET(t2, s2) {
      const i2 = this.constructor.elementProperties.get(t2), e2 = this.constructor._$Eu(t2, i2);
      if (void 0 !== e2 && true === i2.reflect) {
        const h2 = (void 0 !== i2.converter?.toAttribute ? i2.converter : u$1).toAttribute(s2, i2.type);
        this._$Em = t2, null == h2 ? this.removeAttribute(e2) : this.setAttribute(e2, h2), this._$Em = null;
      }
    }
    _$AK(t2, s2) {
      const i2 = this.constructor, e2 = i2._$Eh.get(t2);
      if (void 0 !== e2 && this._$Em !== e2) {
        const t3 = i2.getPropertyOptions(e2), h2 = "function" == typeof t3.converter ? { fromAttribute: t3.converter } : void 0 !== t3.converter?.fromAttribute ? t3.converter : u$1;
        this._$Em = e2;
        const r2 = h2.fromAttribute(s2, t3.type);
        this[e2] = r2 ?? this._$Ej?.get(e2) ?? r2, this._$Em = null;
      }
    }
    requestUpdate(t2, s2, i2, e2 = false, h2) {
      if (void 0 !== t2) {
        const r2 = this.constructor;
        if (false === e2 && (h2 = this[t2]), i2 ??= r2.getPropertyOptions(t2), !((i2.hasChanged ?? f$1)(h2, s2) || i2.useDefault && i2.reflect && h2 === this._$Ej?.get(t2) && !this.hasAttribute(r2._$Eu(t2, i2)))) return;
        this.C(t2, s2, i2);
      }
      false === this.isUpdatePending && (this._$ES = this._$EP());
    }
    C(t2, s2, { useDefault: i2, reflect: e2, wrapped: h2 }, r2) {
      i2 && !(this._$Ej ??= new Map()).has(t2) && (this._$Ej.set(t2, r2 ?? s2 ?? this[t2]), true !== h2 || void 0 !== r2) || (this._$AL.has(t2) || (this.hasUpdated || i2 || (s2 = void 0), this._$AL.set(t2, s2)), true === e2 && this._$Em !== t2 && (this._$Eq ??= new Set()).add(t2));
    }
    async _$EP() {
      this.isUpdatePending = true;
      try {
        await this._$ES;
      } catch (t3) {
        Promise.reject(t3);
      }
      const t2 = this.scheduleUpdate();
      return null != t2 && await t2, !this.isUpdatePending;
    }
    scheduleUpdate() {
      return this.performUpdate();
    }
    performUpdate() {
      if (!this.isUpdatePending) return;
      if (!this.hasUpdated) {
        if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {
          for (const [t4, s3] of this._$Ep) this[t4] = s3;
          this._$Ep = void 0;
        }
        const t3 = this.constructor.elementProperties;
        if (t3.size > 0) for (const [s3, i2] of t3) {
          const { wrapped: t4 } = i2, e2 = this[s3];
          true !== t4 || this._$AL.has(s3) || void 0 === e2 || this.C(s3, void 0, i2, e2);
        }
      }
      let t2 = false;
      const s2 = this._$AL;
      try {
        t2 = this.shouldUpdate(s2), t2 ? (this.willUpdate(s2), this._$EO?.forEach((t3) => t3.hostUpdate?.()), this.update(s2)) : this._$EM();
      } catch (s3) {
        throw t2 = false, this._$EM(), s3;
      }
      t2 && this._$AE(s2);
    }
    willUpdate(t2) {
    }
    _$AE(t2) {
      this._$EO?.forEach((t3) => t3.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t2)), this.updated(t2);
    }
    _$EM() {
      this._$AL = new Map(), this.isUpdatePending = false;
    }
    get updateComplete() {
      return this.getUpdateComplete();
    }
    getUpdateComplete() {
      return this._$ES;
    }
    shouldUpdate(t2) {
      return true;
    }
    update(t2) {
      this._$Eq &&= this._$Eq.forEach((t3) => this._$ET(t3, this[t3])), this._$EM();
    }
    updated(t2) {
    }
    firstUpdated(t2) {
    }
  };
  y$1.elementStyles = [], y$1.shadowRootOptions = { mode: "open" }, y$1[d$1("elementProperties")] = new Map(), y$1[d$1("finalized")] = new Map(), p$1?.({ ReactiveElement: y$1 }), (a$1.reactiveElementVersions ??= []).push("2.1.2");
  const t$1 = globalThis, i$1 = (t2) => t2, s$1 = t$1.trustedTypes, e = s$1 ? s$1.createPolicy("lit-html", { createHTML: (t2) => t2 }) : void 0, h = "$lit$", o$2 = `lit$${Math.random().toFixed(9).slice(2)}$`, n$1 = "?" + o$2, r$2 = `<${n$1}>`, l = document, c = () => l.createComment(""), a = (t2) => null === t2 || "object" != typeof t2 && "function" != typeof t2, u = Array.isArray, d = (t2) => u(t2) || "function" == typeof t2?.[Symbol.iterator], f = "[ 	\n\f\r]", v = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, _ = /-->/g, m = />/g, p = RegExp(`>|${f}(?:([^\\s"'>=/]+)(${f}*=${f}*(?:[^ 	
\f\r"'\`<>=]|("|')|))|$)`, "g"), g = /'/g, $ = /"/g, y2 = /^(?:script|style|textarea|title)$/i, x = (t2) => (i2, ...s2) => ({ _$litType$: t2, strings: i2, values: s2 }), b = x(1), E = Symbol.for("lit-noChange"), A = Symbol.for("lit-nothing"), C = new WeakMap(), P = l.createTreeWalker(l, 129);
  function V(t2, i2) {
    if (!u(t2) || !t2.hasOwnProperty("raw")) throw Error("invalid template strings array");
    return void 0 !== e ? e.createHTML(i2) : i2;
  }
  const N = (t2, i2) => {
    const s2 = t2.length - 1, e2 = [];
    let n3, l2 = 2 === i2 ? "<svg>" : 3 === i2 ? "<math>" : "", c2 = v;
    for (let i3 = 0; i3 < s2; i3++) {
      const s3 = t2[i3];
      let a2, u2, d2 = -1, f2 = 0;
      for (; f2 < s3.length && (c2.lastIndex = f2, u2 = c2.exec(s3), null !== u2); ) f2 = c2.lastIndex, c2 === v ? "!--" === u2[1] ? c2 = _ : void 0 !== u2[1] ? c2 = m : void 0 !== u2[2] ? (y2.test(u2[2]) && (n3 = RegExp("</" + u2[2], "g")), c2 = p) : void 0 !== u2[3] && (c2 = p) : c2 === p ? ">" === u2[0] ? (c2 = n3 ?? v, d2 = -1) : void 0 === u2[1] ? d2 = -2 : (d2 = c2.lastIndex - u2[2].length, a2 = u2[1], c2 = void 0 === u2[3] ? p : '"' === u2[3] ? $ : g) : c2 === $ || c2 === g ? c2 = p : c2 === _ || c2 === m ? c2 = v : (c2 = p, n3 = void 0);
      const x2 = c2 === p && t2[i3 + 1].startsWith("/>") ? " " : "";
      l2 += c2 === v ? s3 + r$2 : d2 >= 0 ? (e2.push(a2), s3.slice(0, d2) + h + s3.slice(d2) + o$2 + x2) : s3 + o$2 + (-2 === d2 ? i3 : x2);
    }
    return [V(t2, l2 + (t2[s2] || "<?>") + (2 === i2 ? "</svg>" : 3 === i2 ? "</math>" : "")), e2];
  };
  class S {
    constructor({ strings: t2, _$litType$: i2 }, e2) {
      let r2;
      this.parts = [];
      let l2 = 0, a2 = 0;
      const u2 = t2.length - 1, d2 = this.parts, [f2, v2] = N(t2, i2);
      if (this.el = S.createElement(f2, e2), P.currentNode = this.el.content, 2 === i2 || 3 === i2) {
        const t3 = this.el.content.firstChild;
        t3.replaceWith(...t3.childNodes);
      }
      for (; null !== (r2 = P.nextNode()) && d2.length < u2; ) {
        if (1 === r2.nodeType) {
          if (r2.hasAttributes()) for (const t3 of r2.getAttributeNames()) if (t3.endsWith(h)) {
            const i3 = v2[a2++], s2 = r2.getAttribute(t3).split(o$2), e3 = /([.?@])?(.*)/.exec(i3);
            d2.push({ type: 1, index: l2, name: e3[2], strings: s2, ctor: "." === e3[1] ? I : "?" === e3[1] ? L : "@" === e3[1] ? z : H }), r2.removeAttribute(t3);
          } else t3.startsWith(o$2) && (d2.push({ type: 6, index: l2 }), r2.removeAttribute(t3));
          if (y2.test(r2.tagName)) {
            const t3 = r2.textContent.split(o$2), i3 = t3.length - 1;
            if (i3 > 0) {
              r2.textContent = s$1 ? s$1.emptyScript : "";
              for (let s2 = 0; s2 < i3; s2++) r2.append(t3[s2], c()), P.nextNode(), d2.push({ type: 2, index: ++l2 });
              r2.append(t3[i3], c());
            }
          }
        } else if (8 === r2.nodeType) if (r2.data === n$1) d2.push({ type: 2, index: l2 });
        else {
          let t3 = -1;
          for (; -1 !== (t3 = r2.data.indexOf(o$2, t3 + 1)); ) d2.push({ type: 7, index: l2 }), t3 += o$2.length - 1;
        }
        l2++;
      }
    }
    static createElement(t2, i2) {
      const s2 = l.createElement("template");
      return s2.innerHTML = t2, s2;
    }
  }
  function M(t2, i2, s2 = t2, e2) {
    if (i2 === E) return i2;
    let h2 = void 0 !== e2 ? s2._$Co?.[e2] : s2._$Cl;
    const o2 = a(i2) ? void 0 : i2._$litDirective$;
    return h2?.constructor !== o2 && (h2?._$AO?.(false), void 0 === o2 ? h2 = void 0 : (h2 = new o2(t2), h2._$AT(t2, s2, e2)), void 0 !== e2 ? (s2._$Co ??= [])[e2] = h2 : s2._$Cl = h2), void 0 !== h2 && (i2 = M(t2, h2._$AS(t2, i2.values), h2, e2)), i2;
  }
  class R {
    constructor(t2, i2) {
      this._$AV = [], this._$AN = void 0, this._$AD = t2, this._$AM = i2;
    }
    get parentNode() {
      return this._$AM.parentNode;
    }
    get _$AU() {
      return this._$AM._$AU;
    }
    u(t2) {
      const { el: { content: i2 }, parts: s2 } = this._$AD, e2 = (t2?.creationScope ?? l).importNode(i2, true);
      P.currentNode = e2;
      let h2 = P.nextNode(), o2 = 0, n3 = 0, r2 = s2[0];
      for (; void 0 !== r2; ) {
        if (o2 === r2.index) {
          let i3;
          2 === r2.type ? i3 = new k(h2, h2.nextSibling, this, t2) : 1 === r2.type ? i3 = new r2.ctor(h2, r2.name, r2.strings, this, t2) : 6 === r2.type && (i3 = new Z(h2, this, t2)), this._$AV.push(i3), r2 = s2[++n3];
        }
        o2 !== r2?.index && (h2 = P.nextNode(), o2++);
      }
      return P.currentNode = l, e2;
    }
    p(t2) {
      let i2 = 0;
      for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t2, s2, i2), i2 += s2.strings.length - 2) : s2._$AI(t2[i2])), i2++;
    }
  }
  class k {
    get _$AU() {
      return this._$AM?._$AU ?? this._$Cv;
    }
    constructor(t2, i2, s2, e2) {
      this.type = 2, this._$AH = A, this._$AN = void 0, this._$AA = t2, this._$AB = i2, this._$AM = s2, this.options = e2, this._$Cv = e2?.isConnected ?? true;
    }
    get parentNode() {
      let t2 = this._$AA.parentNode;
      const i2 = this._$AM;
      return void 0 !== i2 && 11 === t2?.nodeType && (t2 = i2.parentNode), t2;
    }
    get startNode() {
      return this._$AA;
    }
    get endNode() {
      return this._$AB;
    }
    _$AI(t2, i2 = this) {
      t2 = M(this, t2, i2), a(t2) ? t2 === A || null == t2 || "" === t2 ? (this._$AH !== A && this._$AR(), this._$AH = A) : t2 !== this._$AH && t2 !== E && this._(t2) : void 0 !== t2._$litType$ ? this.$(t2) : void 0 !== t2.nodeType ? this.T(t2) : d(t2) ? this.k(t2) : this._(t2);
    }
    O(t2) {
      return this._$AA.parentNode.insertBefore(t2, this._$AB);
    }
    T(t2) {
      this._$AH !== t2 && (this._$AR(), this._$AH = this.O(t2));
    }
    _(t2) {
      this._$AH !== A && a(this._$AH) ? this._$AA.nextSibling.data = t2 : this.T(l.createTextNode(t2)), this._$AH = t2;
    }
    $(t2) {
      const { values: i2, _$litType$: s2 } = t2, e2 = "number" == typeof s2 ? this._$AC(t2) : (void 0 === s2.el && (s2.el = S.createElement(V(s2.h, s2.h[0]), this.options)), s2);
      if (this._$AH?._$AD === e2) this._$AH.p(i2);
      else {
        const t3 = new R(e2, this), s3 = t3.u(this.options);
        t3.p(i2), this.T(s3), this._$AH = t3;
      }
    }
    _$AC(t2) {
      let i2 = C.get(t2.strings);
      return void 0 === i2 && C.set(t2.strings, i2 = new S(t2)), i2;
    }
    k(t2) {
      u(this._$AH) || (this._$AH = [], this._$AR());
      const i2 = this._$AH;
      let s2, e2 = 0;
      for (const h2 of t2) e2 === i2.length ? i2.push(s2 = new k(this.O(c()), this.O(c()), this, this.options)) : s2 = i2[e2], s2._$AI(h2), e2++;
      e2 < i2.length && (this._$AR(s2 && s2._$AB.nextSibling, e2), i2.length = e2);
    }
    _$AR(t2 = this._$AA.nextSibling, s2) {
      for (this._$AP?.(false, true, s2); t2 !== this._$AB; ) {
        const s3 = i$1(t2).nextSibling;
        i$1(t2).remove(), t2 = s3;
      }
    }
    setConnected(t2) {
      void 0 === this._$AM && (this._$Cv = t2, this._$AP?.(t2));
    }
  }
  class H {
    get tagName() {
      return this.element.tagName;
    }
    get _$AU() {
      return this._$AM._$AU;
    }
    constructor(t2, i2, s2, e2, h2) {
      this.type = 1, this._$AH = A, this._$AN = void 0, this.element = t2, this.name = i2, this._$AM = e2, this.options = h2, s2.length > 2 || "" !== s2[0] || "" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = A;
    }
    _$AI(t2, i2 = this, s2, e2) {
      const h2 = this.strings;
      let o2 = false;
      if (void 0 === h2) t2 = M(this, t2, i2, 0), o2 = !a(t2) || t2 !== this._$AH && t2 !== E, o2 && (this._$AH = t2);
      else {
        const e3 = t2;
        let n3, r2;
        for (t2 = h2[0], n3 = 0; n3 < h2.length - 1; n3++) r2 = M(this, e3[s2 + n3], i2, n3), r2 === E && (r2 = this._$AH[n3]), o2 ||= !a(r2) || r2 !== this._$AH[n3], r2 === A ? t2 = A : t2 !== A && (t2 += (r2 ?? "") + h2[n3 + 1]), this._$AH[n3] = r2;
      }
      o2 && !e2 && this.j(t2);
    }
    j(t2) {
      t2 === A ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t2 ?? "");
    }
  }
  class I extends H {
    constructor() {
      super(...arguments), this.type = 3;
    }
    j(t2) {
      this.element[this.name] = t2 === A ? void 0 : t2;
    }
  }
  class L extends H {
    constructor() {
      super(...arguments), this.type = 4;
    }
    j(t2) {
      this.element.toggleAttribute(this.name, !!t2 && t2 !== A);
    }
  }
  class z extends H {
    constructor(t2, i2, s2, e2, h2) {
      super(t2, i2, s2, e2, h2), this.type = 5;
    }
    _$AI(t2, i2 = this) {
      if ((t2 = M(this, t2, i2, 0) ?? A) === E) return;
      const s2 = this._$AH, e2 = t2 === A && s2 !== A || t2.capture !== s2.capture || t2.once !== s2.once || t2.passive !== s2.passive, h2 = t2 !== A && (s2 === A || e2);
      e2 && this.element.removeEventListener(this.name, this, s2), h2 && this.element.addEventListener(this.name, this, t2), this._$AH = t2;
    }
    handleEvent(t2) {
      "function" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t2) : this._$AH.handleEvent(t2);
    }
  }
  class Z {
    constructor(t2, i2, s2) {
      this.element = t2, this.type = 6, this._$AN = void 0, this._$AM = i2, this.options = s2;
    }
    get _$AU() {
      return this._$AM._$AU;
    }
    _$AI(t2) {
      M(this, t2);
    }
  }
  const B = t$1.litHtmlPolyfillSupport;
  B?.(S, k), (t$1.litHtmlVersions ??= []).push("3.3.3");
  const D = (t2, i2, s2) => {
    const e2 = s2?.renderBefore ?? i2;
    let h2 = e2._$litPart$;
    if (void 0 === h2) {
      const t3 = s2?.renderBefore ?? null;
      e2._$litPart$ = h2 = new k(i2.insertBefore(c(), t3), t3, void 0, s2 ?? {});
    }
    return h2._$AI(t2), h2;
  };
  const s = globalThis;
  class i extends y$1 {
    constructor() {
      super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0;
    }
    createRenderRoot() {
      const t2 = super.createRenderRoot();
      return this.renderOptions.renderBefore ??= t2.firstChild, t2;
    }
    update(t2) {
      const r2 = this.render();
      this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t2), this._$Do = D(r2, this.renderRoot, this.renderOptions);
    }
    connectedCallback() {
      super.connectedCallback(), this._$Do?.setConnected(true);
    }
    disconnectedCallback() {
      super.disconnectedCallback(), this._$Do?.setConnected(false);
    }
    render() {
      return E;
    }
  }
  i._$litElement$ = true, i["finalized"] = true, s.litElementHydrateSupport?.({ LitElement: i });
  const o$1 = s.litElementPolyfillSupport;
  o$1?.({ LitElement: i });
  (s.litElementVersions ??= []).push("4.2.2");
  const t = (t2) => (e2, o2) => {
    void 0 !== o2 ? o2.addInitializer(() => {
      customElements.define(t2, e2);
    }) : customElements.define(t2, e2);
  };
  const o = { attribute: true, type: String, converter: u$1, reflect: false, hasChanged: f$1 }, r$1 = (t2 = o, e2, r2) => {
    const { kind: n3, metadata: i2 } = r2;
    let s2 = globalThis.litPropertyMetadata.get(i2);
    if (void 0 === s2 && globalThis.litPropertyMetadata.set(i2, s2 = new Map()), "setter" === n3 && ((t2 = Object.create(t2)).wrapped = true), s2.set(r2.name, t2), "accessor" === n3) {
      const { name: o2 } = r2;
      return { set(r3) {
        const n4 = e2.get.call(this);
        e2.set.call(this, r3), this.requestUpdate(o2, n4, t2, true, r3);
      }, init(e3) {
        return void 0 !== e3 && this.C(o2, void 0, t2, e3), e3;
      } };
    }
    if ("setter" === n3) {
      const { name: o2 } = r2;
      return function(r3) {
        const n4 = this[o2];
        e2.call(this, r3), this.requestUpdate(o2, n4, t2, true, r3);
      };
    }
    throw Error("Unsupported decorator location: " + n3);
  };
  function n2(t2) {
    return (e2, o2) => "object" == typeof o2 ? r$1(t2, e2, o2) : ((t3, e3, o3) => {
      const r2 = e3.hasOwnProperty(o3);
      return e3.constructor.createProperty(o3, t3), r2 ? Object.getOwnPropertyDescriptor(e3, o3) : void 0;
    })(t2, e2, o2);
  }
  function r(r2) {
    return n2({ ...r2, state: true, attribute: false });
  }
  var __defProp$3 = Object.defineProperty;
  var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
  var __decorateClass$3 = (decorators, target, key, kind) => {
    var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
    for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
      if (decorator = decorators[i2])
        result = (kind ? decorator(target, key, result) : decorator(result)) || result;
    if (kind && result) __defProp$3(target, key, result);
    return result;
  };
  const PREMIUM_UPGRADE_URL$1 = "https://ffscouter.com/premium";
  let FFHeaderLine = class extends i {
    constructor() {
      super(...arguments);
      this.data = null;
      this.is_premium = null;
      this.loading = false;
    }
    createRenderRoot() {
      return this;
    }
async willUpdate(changedProperties) {
      if (changedProperties.has("data") && this.data) {
        this.loading = true;
        try {
          this.is_premium = await check_key_status.is_premium();
        } catch (error) {
          logger.error(error);
        } finally {
          this.loading = false;
        }
      }
    }
    render() {
      if (this.data === null || this.data.no_data) {
        return b`<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 = format_ff_score(this.data);
      const difficulty = format_difficulty_text(this.data);
      const fresh = format_relative_time(this.data.last_updated);
      const background_colour = get_ff_colour(this.data);
      const text_colour = get_contrast_color(background_colour);
      let extraDetailsLine = b``;
      if (this.data.distribution?.distribution_human) {
        const ageStr = format_relative_time(this.data.distribution.last_updated);
        extraDetailsLine = b`<span
        style="display:block; margin-top: 2px; font-size: 12px; font-style: normal;"
        ><span style="font-weight: bold; margin-right: 6px;">Top Stats:</span
        ><span style="font-weight: normal;"
          >${this.data.distribution.distribution_human} ${ageStr}</span
        ></span
      >`;
      } else if (this.loading) {
        extraDetailsLine = b``;
      } else if (this.is_premium === false && this.data.premium_insights_available) {
        extraDetailsLine = b`<span class="ff-premium-upgrade-line"
        ><a
          href="${PREMIUM_UPGRADE_URL$1}"
          target="_blank"
          rel="noopener noreferrer"
          style="font-weight: bold; text-decoration: underline;"
          >Premium Data Available - Upgrade To View</a
        ></span
      >`;
      }
      return b`<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
      ><span
        style="font-size: 11px; font-weight: normal; margin-left: 6px; vertical-align: middle; font-style: italic;"
        >Est. Stats: <span>${this.data.bs_estimate_human}</span></span
      >${extraDetailsLine}`;
    }
  };
  __decorateClass$3([
    n2({ type: Object })
  ], FFHeaderLine.prototype, "data", 2);
  __decorateClass$3([
    r()
  ], FFHeaderLine.prototype, "is_premium", 2);
  __decorateClass$3([
    r()
  ], FFHeaderLine.prototype, "loading", 2);
  FFHeaderLine = __decorateClass$3([
    t("ff-header-line")
  ], FFHeaderLine);
  async function inject_info_line$1(info_line) {
    const h4 = await wait_for_element("h4", 1e4);
    if (!h4) {
      return;
    }
    h4.parentNode?.parentNode?.parentNode?.insertBefore(
      info_line,
      h4.parentNode?.parentNode?.nextSibling
    );
  }
  const Attack = {
    name: "Attack FF display",
    description: "Shows FF on top left of any attack page",
    executionTime: StartTime.DocumentBody,
    async shouldRun() {
      return torn_page("page", { sid: "attack" });
    },
    async run() {
      const player_id = extract_id_from_url(window.location.href);
      if (!player_id) {
        return;
      }
      logger.debug("On the attack page, found player_id", player_id);
      const info_line = create_info_line();
      ffscouter.get(player_id).then(async (data) => {
        const line = document.createElement("ff-header-line");
        line.data = data;
        info_line.appendChild(line);
        inject_info_line$1(info_line);
      });
      ffscouter.complete();
    },
    httpIntercept: {
      before(_url, _init) {
        return void 0;
      },
      after(_bodyText, _response, _ctx) {
        return void 0;
      }
    }
  };
  var __defProp$2 = Object.defineProperty;
  var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
  var __decorateClass$2 = (decorators, target, key, kind) => {
    var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
    for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
      if (decorator = decorators[i2])
        result = (kind ? decorator(target, key, result) : decorator(result)) || result;
    if (kind && result) __defProp$2(target, key, result);
    return result;
  };
  const DEFAULT_STATE = {
    activity: {
      online: true,
      idle: true,
      offline: true
    },
    status: {
      okay: true,
      traveling: true,
      hospital: true,
      jail: true,
      abroad: true
    }
  };
  let FFFactionFilterBox = class extends i {
    constructor() {
      super(...arguments);
      this.mode = "faction";
      this.sortBy = "none";
      this.colDisplay = FactionsColDisplay.FAIR_FIGHT;
      this.activity = { ...DEFAULT_STATE.activity };
      this.status = { ...DEFAULT_STATE.status };
      this.levelMin = null;
      this.levelMax = null;
      this.ffMin = null;
      this.ffMax = null;
      this.statsMin = null;
      this.statsMax = null;
      this.collapsed = false;
      this.onConfigUpdated = () => {
        this.colDisplay = ffconfig.factions_col_display;
        this.requestUpdate();
      };
    }
    createRenderRoot() {
      return this;
    }
    connectedCallback() {
      super.connectedCallback();
      this.loadState();
      window.addEventListener("ff-config-updated", this.onConfigUpdated);
    }
    disconnectedCallback() {
      window.removeEventListener("ff-config-updated", this.onConfigUpdated);
      super.disconnectedCallback();
    }
    loadState() {
      const isWar = this.mode === "war";
      this.collapsed = isWar ? ffconfig.war_filter_collapsed : ffconfig.faction_filter_collapsed;
      this.colDisplay = ffconfig.factions_col_display;
      const parsed = isWar ? ffconfig.war_filter_state : ffconfig.faction_filter_state;
      if (parsed) {
        const savedSortBy = parsed.sortBy ?? "none";
        this.sortBy = savedSortBy === "ff-asc" || savedSortBy === "ff-desc" ? savedSortBy : "none";
        this.activity = { ...DEFAULT_STATE.activity, ...parsed.activity };
        this.status = { ...DEFAULT_STATE.status, ...parsed.status };
        this.levelMin = parsed.levelMin ?? null;
        this.levelMax = parsed.levelMax ?? null;
        this.ffMin = parsed.ffMin ?? null;
        this.ffMax = parsed.ffMax ?? null;
        this.statsMin = parsed.statsMin ?? null;
        this.statsMax = parsed.statsMax ?? null;
      }
      this.dispatchChange();
    }
    onToggle(e2) {
      const details = e2.currentTarget;
      this.collapsed = !details.open;
      if (this.mode === "war") {
        ffconfig.war_filter_collapsed = this.collapsed;
      } else {
        ffconfig.faction_filter_collapsed = this.collapsed;
      }
    }
    saveState() {
      const stateObj = {
        sortBy: this.sortBy,
        activity: this.activity,
        status: this.status,
        levelMin: this.levelMin,
        levelMax: this.levelMax,
        ffMin: this.ffMin,
        ffMax: this.ffMax,
        statsMin: this.statsMin,
        statsMax: this.statsMax
      };
      if (this.mode === "war") {
        ffconfig.war_filter_state = stateObj;
      } else {
        ffconfig.faction_filter_state = stateObj;
      }
    }
    dispatchChange() {
      this.dispatchEvent(
        new CustomEvent("filter-change", {
          detail: {
            sortBy: this.sortBy,
            activity: this.activity,
            status: this.status,
            levelMin: this.levelMin,
            levelMax: this.levelMax,
            ffMin: this.ffMin,
            ffMax: this.ffMax,
            statsMin: this.statsMin ? parse_suffix_number(this.statsMin) : null,
            statsMax: this.statsMax ? parse_suffix_number(this.statsMax) : null
          },
          bubbles: true,
          composed: true
        })
      );
    }
    onSortToggle() {
      if (this.sortBy === "none") {
        this.sortBy = "ff-desc";
      } else if (this.sortBy === "ff-desc") {
        this.sortBy = "ff-asc";
      } else {
        this.sortBy = "none";
      }
      this.saveState();
      this.dispatchChange();
    }
    onDisplayChange(e2) {
      const val = e2.target.value;
      this.colDisplay = val;
      ffconfig.factions_col_display = val;
      window.dispatchEvent(new CustomEvent("ff-config-updated"));
    }
    onActivityChange(key, val) {
      this.activity = {
        ...this.activity,
        [key]: val
      };
      this.saveState();
      this.dispatchChange();
    }
    onStatusChange(key, val) {
      this.status = {
        ...this.status,
        [key]: val
      };
      this.saveState();
      this.dispatchChange();
    }
    onLevelChange(type, valStr) {
      const val = valStr === "" ? null : Number.parseInt(valStr, 10);
      if (type === "min") {
        this.levelMin = val;
      } else {
        this.levelMax = val;
      }
      this.saveState();
      this.dispatchChange();
    }
    onFFChange(type, valStr) {
      const val = valStr === "" ? null : Number.parseFloat(valStr);
      if (type === "min") {
        this.ffMin = val;
      } else {
        this.ffMax = val;
      }
      this.saveState();
      this.dispatchChange();
    }
    onStatsChange(type, valStr) {
      const val = valStr.trim() === "" ? null : valStr;
      if (type === "min") {
        this.statsMin = val;
      } else {
        this.statsMax = val;
      }
      this.saveState();
      this.dispatchChange();
    }
    render() {
      const isEst = this.colDisplay === FactionsColDisplay.BATTLE_STATS;
      const sortText = isEst ? "Stats" : "FF";
      return b`
      <details
        class="ff-filter-box ${this.mode === "war" ? "no-borders" : ""}"
        ?open="${!this.collapsed}"
        @toggle="${this.onToggle}"
      >
        <summary
          style="cursor: pointer; font-weight: bold; font-size: 14px; outline: none; user-select: none;"
        >
          FFScouter Filter & Sort Controls
        </summary>
        <div class="ff-filter-grid" style="margin-top: 12px;">
          <div class="ff-filter-group grp-sort">
            <strong>Sort & Display</strong>
            <div style="display: flex; flex-direction: column; gap: 8px;">
              <button @click="${this.onSortToggle}" style="width: 100%;">
                ${this.sortBy === "none" ? "Sort: Default" : this.sortBy === "ff-asc" ? `Sort: ${sortText} ▲` : `Sort: ${sortText} ▼`}
              </button>
              ${this.mode === "war" ? "" : b`
                    <select
                      id="factions-col-display-filter"
                      .value=${this.colDisplay}
                      @change=${this.onDisplayChange}
                      style="padding: 4px; border: 1px solid var(--ffsv3-border-color); border-radius: 4px; background: var(--ffsv3-alt-bg-color); color: var(--ffsv3-text-color); font-size: 11px; cursor: pointer; height: 32px;"
                    >
                      <option value="fair_fight">Show: FF Score</option>
                      <option value="battle_stats">Show: BS Estimate</option>
                      <option value="none">Show: None (Hide)</option>
                    </select>
                  `}
            </div>
          </div>

          <div class="ff-filter-group grp-activity">
            <strong>Activity</strong>
            <div class="ff-filter-options">
              <label>
                <input
                  type="checkbox"
                  ?checked="${this.activity.online}"
                  @change="${(e2) => this.onActivityChange("online", e2.target.checked)}"
                />
                Online
              </label>
              <label>
                <input
                  type="checkbox"
                  ?checked="${this.activity.idle}"
                  @change="${(e2) => this.onActivityChange("idle", e2.target.checked)}"
                />
                Idle
              </label>
              <label>
                <input
                  type="checkbox"
                  ?checked="${this.activity.offline}"
                  @change="${(e2) => this.onActivityChange("offline", e2.target.checked)}"
                />
                Offline
              </label>
            </div>
          </div>

          <div class="ff-filter-group grp-status">
            <strong>Status</strong>
            <div class="ff-filter-options">
              <label>
                <input
                  type="checkbox"
                  ?checked="${this.status.okay}"
                  @change="${(e2) => this.onStatusChange("okay", e2.target.checked)}"
                />
                Okay
              </label>
              <label>
                <input
                  type="checkbox"
                  ?checked="${this.status.hospital}"
                  @change="${(e2) => this.onStatusChange("hospital", e2.target.checked)}"
                />
                Hospital
              </label>
              <label>
                <input
                  type="checkbox"
                  ?checked="${this.status.jail}"
                  @change="${(e2) => this.onStatusChange("jail", e2.target.checked)}"
                />
                Jail
              </label>
              <label>
                <input
                  type="checkbox"
                  ?checked="${this.status.abroad}"
                  @change="${(e2) => this.onStatusChange("abroad", e2.target.checked)}"
                />
                Abroad
              </label>
              <label>
                <input
                  type="checkbox"
                  ?checked="${this.status.traveling}"
                  @change="${(e2) => this.onStatusChange("traveling", e2.target.checked)}"
                />
                Traveling
              </label>
            </div>
          </div>

          <div class="ff-filter-group grp-level">
            <strong>Level Range</strong>
            <div class="ff-filter-range-inputs">
              <input
                type="number"
                placeholder="Min"
                .value="${this.levelMin !== null ? String(this.levelMin) : ""}"
                @input="${(e2) => this.onLevelChange("min", e2.target.value)}"
              />
              <span>to</span>
              <input
                type="number"
                placeholder="Max"
                .value="${this.levelMax !== null ? String(this.levelMax) : ""}"
                @input="${(e2) => this.onLevelChange("max", e2.target.value)}"
              />
            </div>
          </div>

          <div class="ff-filter-group grp-ff">
            <strong>FF Range</strong>
            <div class="ff-filter-range-inputs">
              <input
                type="number"
                step="0.1"
                placeholder="Min"
                .value="${this.ffMin !== null ? String(this.ffMin) : ""}"
                @input="${(e2) => this.onFFChange("min", e2.target.value)}"
              />
              <span>to</span>
              <input
                type="number"
                step="0.1"
                placeholder="Max"
                .value="${this.ffMax !== null ? String(this.ffMax) : ""}"
                @input="${(e2) => this.onFFChange("max", e2.target.value)}"
              />
            </div>
          </div>

          <div class="ff-filter-group grp-stats">
            <strong>Stats Range</strong>
            <div class="ff-filter-range-inputs">
              <input
                type="text"
                placeholder="Min"
                .value="${this.statsMin !== null ? this.statsMin : ""}"
                @input="${(e2) => this.onStatsChange("min", e2.target.value)}"
              />
              <span>to</span>
              <input
                type="text"
                placeholder="Max"
                .value="${this.statsMax !== null ? this.statsMax : ""}"
                @input="${(e2) => this.onStatsChange("max", e2.target.value)}"
              />
            </div>
          </div>
        </div>
      </details>
    `;
    }
  };
  __decorateClass$2([
    n2({ type: String })
  ], FFFactionFilterBox.prototype, "mode", 2);
  __decorateClass$2([
    r()
  ], FFFactionFilterBox.prototype, "sortBy", 2);
  __decorateClass$2([
    r()
  ], FFFactionFilterBox.prototype, "colDisplay", 2);
  __decorateClass$2([
    r()
  ], FFFactionFilterBox.prototype, "activity", 2);
  __decorateClass$2([
    r()
  ], FFFactionFilterBox.prototype, "status", 2);
  __decorateClass$2([
    r()
  ], FFFactionFilterBox.prototype, "levelMin", 2);
  __decorateClass$2([
    r()
  ], FFFactionFilterBox.prototype, "levelMax", 2);
  __decorateClass$2([
    r()
  ], FFFactionFilterBox.prototype, "ffMin", 2);
  __decorateClass$2([
    r()
  ], FFFactionFilterBox.prototype, "ffMax", 2);
  __decorateClass$2([
    r()
  ], FFFactionFilterBox.prototype, "statsMin", 2);
  __decorateClass$2([
    r()
  ], FFFactionFilterBox.prototype, "statsMax", 2);
  __decorateClass$2([
    r()
  ], FFFactionFilterBox.prototype, "collapsed", 2);
  FFFactionFilterBox = __decorateClass$2([
    t("ff-faction-filter-box")
  ], FFFactionFilterBox);
  const FEATURE_NAME$2 = "faction";
  let isApplying = false;
  function apply_filters_and_sort(membersList, filters) {
    if (isApplying) return;
    isApplying = true;
    try {
      const tbody = membersList.querySelector(".table-body") || membersList.querySelector(".members-list") || membersList;
      const rows = Array.from(
        tbody.querySelectorAll(":scope > .table-row, .enemy, .your")
      );
      for (const row of rows) {
        const activityImg = row.querySelector(".icons img");
        const activity = (activityImg?.getAttribute("alt") || "offline").toLowerCase();
        const allActivityUnchecked = !filters.activity.online && !filters.activity.idle && !filters.activity.offline;
        const matchesActivity = allActivityUnchecked || activity === "online" && filters.activity.online || activity === "idle" && filters.activity.idle || activity === "offline" && filters.activity.offline;
        let status = "okay";
        const statusCell = row.querySelector(".status");
        if (statusCell) {
          if (statusCell.classList.contains("traveling") || statusCell.querySelector(".traveling")) {
            status = "traveling";
          } else if (statusCell.classList.contains("hospital") || statusCell.querySelector(".hospital")) {
            status = "hospital";
          } else if (statusCell.classList.contains("jail") || statusCell.querySelector(".jail")) {
            status = "jail";
          } else if (statusCell.classList.contains("abroad") || statusCell.querySelector(".abroad")) {
            status = "abroad";
          } else {
            status = "okay";
          }
        }
        const allStatusUnchecked = !filters.status.okay && !filters.status.traveling && !filters.status.hospital && !filters.status.jail && !filters.status.abroad;
        const matchesStatus = allStatusUnchecked || status === "okay" && filters.status.okay || status === "traveling" && filters.status.traveling || status === "hospital" && filters.status.hospital || status === "jail" && filters.status.jail || status === "abroad" && filters.status.abroad;
        const levelCell = row.querySelector(".lvl:not(.ffscouter-cell), .level");
        const level = levelCell ? Number.parseInt(levelCell.textContent || "0", 10) : 0;
        const matchesLevel = (filters.levelMin === null || level >= filters.levelMin) && (filters.levelMax === null || level <= filters.levelMax);
        const ffVal = row.dataset["ffValue"] ? (
Number.parseFloat(row.dataset["ffValue"])
        ) : null;
        const matchesFF = ffVal === null || (filters.ffMin === null || ffVal >= filters.ffMin) && (filters.ffMax === null || ffVal <= filters.ffMax);
        const estVal = row.dataset["estValue"] ? (
Number.parseInt(row.dataset["estValue"], 10)
        ) : null;
        const matchesStats = estVal === null || (filters.statsMin === void 0 || filters.statsMin === null || estVal >= filters.statsMin) && (filters.statsMax === void 0 || filters.statsMax === null || estVal <= filters.statsMax);
        if (matchesActivity && matchesStatus && matchesLevel && matchesFF && matchesStats) {
          row.style.display = "";
        } else {
          row.style.display = "none";
        }
      }
      if (filters.sortBy !== "none") {
        const isEst = ffconfig.factions_col_display === FactionsColDisplay.BATTLE_STATS;
        const valKey = isEst ? "estValue" : "ffValue";
        rows.sort((a2, b2) => {
          const getVal = (row) => {
            const dataVal = row.dataset[valKey];
            return dataVal ? Number.parseFloat(dataVal) : -1;
          };
          const valA = getVal(a2);
          const valB = getVal(b2);
          if (filters.sortBy.endsWith("asc")) {
            return valA - valB;
          }
          return valB - valA;
        });
        for (const row of rows) {
          tbody.appendChild(row);
        }
      }
      if (is_filter_active(filters)) {
        membersList.setAttribute("data-ffscouter-active-filter", "true");
      } else {
        membersList.removeAttribute("data-ffscouter-active-filter");
      }
    } finally {
      isApplying = false;
    }
  }
  function is_filter_active(filters) {
    if (filters.sortBy !== "none") return true;
    if (!filters.activity.online || !filters.activity.idle || !filters.activity.offline)
      return true;
    if (!filters.status.okay || !filters.status.traveling || !filters.status.hospital || !filters.status.jail || !filters.status.abroad)
      return true;
    if (filters.levelMin !== null || filters.levelMax !== null) return true;
    if (filters.ffMin !== null || filters.ffMax !== null) return true;
    if (filters.statsMin !== void 0 && filters.statsMin !== null) return true;
    if (filters.statsMax !== void 0 && filters.statsMax !== null) return true;
    return false;
  }
  async function apply_ff_columns(membersList) {
    const headerLvl = membersList.querySelector(".table-header > .lvl") || membersList.querySelector(".white-grad");
    if (!headerLvl) return;
    const isWar = membersList.closest(".faction-war") !== null;
    const colDisplay = isWar ? FactionsColDisplay.NONE : ffconfig.factions_col_display;
    const isEst = colDisplay === FactionsColDisplay.BATTLE_STATS;
    const isNone = colDisplay === FactionsColDisplay.NONE;
    const expectedText = isEst ? "Est" : "FF";
    let headerLi = membersList.querySelector(
      ".ffscouter-header"
    );
    if (isNone) {
      if (headerLi) {
        headerLi.remove();
      }
    } else {
      if (!headerLi) {
        headerLi = document.createElement("li");
        headerLi.tabIndex = 0;
        headerLi.classList.add(
          "table-cell",
          "lvl",
          "torn-divider",
          "divider-vertical",
          "ffscouter-header"
        );
        headerLvl.after(headerLi);
      }
      if (headerLi.textContent !== expectedText) {
        headerLi.textContent = expectedText;
      }
    }
    const rows = Array.from(
      membersList.querySelectorAll(".table-body > .table-row, .enemy, .your")
    );
    const rowPlayers = rows.map((row) => {
      const memberDiv = row.querySelector(".member");
      if (!memberDiv) return null;
      const profileLink = memberDiv.querySelector('a[href^="/profiles"]');
      if (!profileLink || !(profileLink instanceof HTMLAnchorElement))
        return null;
      const url = profileLink.href;
      const match = url.match(/.*XID=(?<player_id>\d+)/);
      if (!match?.groups?.["player_id"]) return null;
      return {
        row,
player_id: Number.parseInt(match.groups["player_id"], 10),
        memberDiv
      };
    }).filter((item) => item !== null);
    if (rowPlayers.length === 0) return;
    const playerIds = rowPlayers.map((p2) => p2.player_id);
    const dataPromises = playerIds.map((id) => ffscouter.get(id));
    ffscouter.complete();
    const dataList = await Promise.all(dataPromises);
    const dataMap = new Map(dataList.map((d2) => [d2.player_id, d2]));
    for (const rp of rowPlayers) {
      let cell = rp.row.querySelector(".ffscouter-cell");
      if (isNone) {
        if (cell) {
          cell.remove();
        }
      } else {
        if (!cell) {
          cell = document.createElement("div");
          cell.classList.add("table-cell", "lvl", "ffscouter-cell");
          const rowLvl = rp.row.querySelector(".lvl");
          if (rowLvl) {
            rowLvl.after(cell);
          } else {
            rp.memberDiv.after(cell);
          }
        }
      }
      const data = dataMap.get(rp.player_id);
      if (data && !data.no_data) {
        rp.row.dataset["ffValue"] = String(data.fair_fight);
        rp.row.dataset["estValue"] = String(data.bs_estimate);
        if (cell) {
          const text = isEst ? data.bs_estimate_human : format_ff_score(data);
          const bg_color = get_ff_colour(data);
          const text_color = get_contrast_color(bg_color);
          cell.style.backgroundColor = bg_color;
          cell.style.color = text_color;
          cell.style.fontWeight = "bold";
          cell.textContent = text;
          if (isEst && data.distribution) {
            const ageStr = format_relative_time(data.distribution.last_updated);
            const agePart = ageStr ? ` ${ageStr}` : "";
            cell.title = `Top Stats: ${data.distribution.distribution_human}${agePart}`;
          } else {
            cell.title = "";
          }
        }
      } else {
        rp.row.dataset["ffValue"] = "";
        rp.row.dataset["estValue"] = "";
        if (cell) {
          cell.textContent = "-";
          cell.style.backgroundColor = "";
          cell.style.color = "";
          cell.style.fontWeight = "";
          cell.title = "";
        }
      }
    }
    const filterBox = membersList.parentNode?.querySelector(
      "ff-faction-filter-box"
    );
    if (filterBox?.activity) {
      apply_filters_and_sort(membersList, {
        sortBy: filterBox.sortBy ?? "none",
        activity: filterBox.activity,
        status: filterBox.status,
        levelMin: filterBox.levelMin ?? null,
        levelMax: filterBox.levelMax ?? null,
        ffMin: filterBox.ffMin ?? null,
        ffMax: filterBox.ffMax ?? null,
        statsMin: filterBox.statsMin ? parse_suffix_number(filterBox.statsMin) : null,
        statsMax: filterBox.statsMax ? parse_suffix_number(filterBox.statsMax) : null
      });
    }
  }
  function inject_filter_box(membersList) {
    const parent = membersList.parentNode;
    if (!parent) return;
    let filterBox = parent.querySelector(
      "ff-faction-filter-box"
    );
    if (!filterBox) {
      filterBox = document.createElement("ff-faction-filter-box");
      filterBox.addEventListener("filter-change", (e2) => {
        apply_filters_and_sort(membersList, e2.detail);
      });
      parent.insertBefore(filterBox, membersList);
    }
  }
  function initialize_features(membersList) {
    inject_filter_box(membersList);
    apply_ff_columns(membersList);
    const target = membersList.querySelector(".table-body") || membersList;
    const attributeObserver = new MutationObserver((mutations) => {
      if (isApplying) return;
      let shouldReapply = false;
      for (const m2 of mutations) {
        if (m2.type === "attributes") {
          if (m2.attributeName === "alt" && m2.target instanceof HTMLImageElement && m2.target.closest(".icons")) {
            shouldReapply = true;
            break;
          }
          if (m2.attributeName === "class" && m2.target instanceof HTMLElement && m2.target.closest(".status")) {
            shouldReapply = true;
            break;
          }
        }
      }
      if (shouldReapply) {
        const filterBox = membersList.parentNode?.querySelector(
          "ff-faction-filter-box"
        );
        if (filterBox?.activity) {
          apply_filters_and_sort(membersList, {
            sortBy: filterBox.sortBy ?? "none",
            activity: filterBox.activity,
            status: filterBox.status,
            levelMin: filterBox.levelMin ?? 1,
            levelMax: filterBox.levelMax ?? 100,
            ffMin: filterBox.ffMin ?? 1,
            ffMax: filterBox.ffMax ?? null,
            statsMin: filterBox.statsMin ? parse_suffix_number(filterBox.statsMin) : null,
            statsMax: filterBox.statsMax ? parse_suffix_number(filterBox.statsMax) : null
          });
        }
      }
    });
    attributeObserver.observe(target, {
      attributes: true,
      attributeFilter: ["class", "alt"],
      subtree: true
    });
    const cleanupInterval = setInterval(() => {
      if (!membersList.isConnected) {
        clearInterval(cleanupInterval);
        attributeObserver.disconnect();
      }
    }, 1e4);
  }
  function setup_faction_features(membersList) {
    const tbody = membersList.querySelector(".table-body");
    const hasRows = tbody?.querySelector(".table-row");
    if (hasRows) {
      initialize_features(membersList);
    } else {
      const loadObserver = new MutationObserver((_mutations, obs) => {
        const currentTbody = membersList.querySelector(".table-body");
        if (currentTbody?.querySelector(".table-row")) {
          obs.disconnect();
          initialize_features(membersList);
        }
      });
      loadObserver.observe(membersList, { childList: true, subtree: true });
      const cleanupInterval = setInterval(() => {
        if (!membersList.isConnected) {
          clearInterval(cleanupInterval);
          loadObserver.disconnect();
        }
      }, 1e4);
    }
  }
  const monitor_member_list = (root = document.body, _dynamic = false) => {
    const membersList = root.classList.contains("members-list") ? root : root.querySelector(".members-list");
    if (membersList) {
      setup_faction_features(membersList);
    } else {
      apply_ff_members_list(root);
      const loadObserver = new MutationObserver((mutations, obs) => {
        const foundList = root.classList.contains("members-list") ? root : root.querySelector(".members-list");
        if (foundList) {
          obs.disconnect();
          setup_faction_features(foundList);
        } else {
          let shouldUpdate = false;
          for (const m2 of mutations) {
            for (const node of m2.addedNodes) {
              if (node instanceof HTMLElement && (node.querySelector(".honor-text-wrap") || node.querySelector(".member"))) {
                shouldUpdate = true;
                break;
              }
            }
            if (shouldUpdate) break;
          }
          if (shouldUpdate) {
            apply_ff_members_list(root);
          }
        }
      });
      loadObserver.observe(root, { childList: true, subtree: true });
      const cleanupInterval = setInterval(() => {
        if (!root.isConnected) {
          clearInterval(cleanupInterval);
          loadObserver.disconnect();
        }
      }, 1e4);
    }
  };
  const apply_ff_members_list = (root = document.body) => {
    const membersList = root.classList.contains("members-list") ? root : root.querySelector(".members-list");
    if (membersList) {
      setup_faction_features(membersList);
      return;
    }
    let found_honor = false;
    for (const bar of root.querySelectorAll(".honor-text-wrap")) {
      apply_ff_gauge(bar, FEATURE_NAME$2);
      found_honor = true;
    }
    if (found_honor) {
      return;
    }
    for (const bar of root.querySelectorAll(".member")) {
      apply_ff_gauge(bar, FEATURE_NAME$2);
    }
    for (const l2 of root.querySelectorAll(".members-list, .chain-attacks-list")) {
      if (l2 instanceof HTMLElement) {
        apply_ff_members_list(l2);
      }
    }
  };
  function setup_war_features(factionWar) {
    const lists = Array.from(
      factionWar.querySelectorAll(".enemy-faction, .your-faction")
    );
    if (lists.length > 0) {
      initialize_war_features(factionWar, lists);
    } else {
      const loadObserver = new MutationObserver((_mutations, obs) => {
        const currentLists = Array.from(
          factionWar.querySelectorAll(".enemy-faction, .your-faction")
        );
        if (currentLists.length > 0) {
          obs.disconnect();
          initialize_war_features(factionWar, currentLists);
        }
      });
      loadObserver.observe(factionWar, { childList: true, subtree: true });
      const cleanupInterval = setInterval(() => {
        if (!factionWar.isConnected) {
          clearInterval(cleanupInterval);
          loadObserver.disconnect();
        }
      }, 1e4);
    }
  }
  function initialize_war_features(factionWar, lists) {
    let filterBox = factionWar.querySelector(
      "ff-faction-filter-box[mode='war']"
    );
    if (!filterBox) {
      filterBox = document.createElement("ff-faction-filter-box");
      filterBox.setAttribute("mode", "war");
      filterBox.addEventListener("filter-change", (e2) => {
        for (const list of lists) {
          apply_filters_and_sort(list, e2.detail);
        }
      });
      factionWar.insertBefore(filterBox, factionWar.firstChild);
    }
    for (const list of lists) {
      setup_war_list(list);
    }
  }
  function setup_war_list(list) {
    const tbody = list.querySelector(".table-body") || list;
    const hasRows = tbody.querySelector(".enemy, .your");
    if (hasRows) {
      initialize_war_list(list);
    } else {
      const loadObserver = new MutationObserver((_mutations, obs) => {
        const currentTbody = list.querySelector(".table-body") || list;
        if (currentTbody.querySelector(".enemy, .your")) {
          obs.disconnect();
          initialize_war_list(list);
        }
      });
      loadObserver.observe(list, { childList: true, subtree: true });
      const cleanupInterval = setInterval(() => {
        if (!list.isConnected) {
          clearInterval(cleanupInterval);
          loadObserver.disconnect();
        }
      }, 1e4);
    }
  }
  function initialize_war_list(list) {
    apply_ff_columns(list);
    const target = list.querySelector(".table-body") || list;
    const attributeObserver = new MutationObserver((mutations) => {
      if (isApplying) return;
      let shouldReapply = false;
      for (const m2 of mutations) {
        if (m2.type === "attributes") {
          if (m2.attributeName === "alt" && m2.target instanceof HTMLImageElement && m2.target.closest(".icons")) {
            shouldReapply = true;
            break;
          }
          if (m2.attributeName === "class" && m2.target instanceof HTMLElement && m2.target.closest(".status")) {
            shouldReapply = true;
            break;
          }
        }
      }
      if (shouldReapply) {
        apply_ff_columns(list);
      }
    });
    attributeObserver.observe(target, {
      attributes: true,
      attributeFilter: ["class", "alt"]
    });
    const cleanupInterval = setInterval(() => {
      if (!list.isConnected) {
        clearInterval(cleanupInterval);
        attributeObserver.disconnect();
      }
    }, 1e4);
  }
  const process_page = () => {
    wait_for_element(".members-list", 1e4).then((node) => {
      if (node instanceof HTMLElement) {
        logger.debug("Found members-list!");
        monitor_member_list(node);
      }
    });
    wait_for_element(".chain-attacks-list", 1e4).then((node) => {
      if (node instanceof HTMLElement) {
        logger.debug("Found chain-attacks-list!");
        monitor_member_list(node, true);
      }
    });
    wait_for_element("#faction_war_list_id", 1e4).then(async (node) => {
      if (!node) {
        return;
      }
      logger.debug("Found faction_war_list_id");
      const descriptions_observer = new MutationObserver(async (mutations) => {
        for (const mutation of mutations) {
          for (const node2 of mutation.addedNodes) {
            if (node2 instanceof HTMLElement && node2.classList.contains("descriptions")) {
              logger.debug(
                "Observed mutation that included adding descriptions",
                node2
              );
              const faction_war = await wait_for_element(".faction-war", 1e4);
              if (faction_war instanceof HTMLElement) {
                setup_war_features(faction_war);
              }
            }
          }
        }
      });
      descriptions_observer.observe(node, { childList: true });
      logger.debug("Set up descriptions observer on", node);
      const existing_descriptions = node.querySelector(".descriptions");
      if (existing_descriptions) {
        const faction_war = await wait_for_element(
          " .faction-war",
          1e4,
          existing_descriptions
        );
        if (faction_war instanceof HTMLElement) {
          setup_war_features(faction_war);
        }
      }
    });
  };
  const Faction = {
    name: "Faction page FF display",
    description: "Shows FF arrows on both your faction and other faction pages.",
    executionTime: StartTime.DocumentBody,
    async shouldRun() {
      return torn_page("factions", { step: "profile" }) || torn_page("factions", { step: "your" });
    },
    async run() {
      window.navigation.addEventListener("navigate", () => {
        process_page();
      });
      window.addEventListener("ff-config-updated", () => {
        const lists = document.querySelectorAll(
          ".members-list, .chain-attacks-list, .enemy-faction, .your-faction"
        );
        for (const list of lists) {
          if (list instanceof HTMLElement) {
            apply_ff_columns(list);
          }
        }
      });
      process_page();
    },
    httpIntercept: {
      before(_url, _init) {
        return void 0;
      },
      after(_bodyText, _response, _ctx) {
        return void 0;
      }
    }
  };
  const FEATURE_NAME$1 = "fallback";
  const Fallback = {
    name: "Fallback mutation observer",
    description: "Catch all mutations and see if we can apply FF data",
    executionTime: StartTime.DocumentBody,
    async shouldRun() {
      return true;
    },
    async run() {
      const check_mutation = async (node) => {
        if (!node.querySelectorAll) {
          return;
        }
        var honor_bars = node.querySelectorAll(
          ".honor-text-wrap"
        );
        var name_elems = node.querySelectorAll(
          ".user.name"
        );
        if (honor_bars.length > 0) {
          await apply_ff_gauge_selector(honor_bars, FEATURE_NAME$1);
        } else {
          if (window.location.href.startsWith("https://www.torn.com/companies.php")) {
            await apply_ff_gauge_selector(
              node.querySelectorAll(".employee"),
              FEATURE_NAME$1
            );
          } else if (window.location.href.startsWith(
            "https://www.torn.com/page.php?sid=competition#/team"
          )) {
            await apply_ff_gauge_selector(
              node.querySelectorAll('[class*="name__"]'),
              FEATURE_NAME$1
            );
          } else if (window.location.href.startsWith("https://www.torn.com/joblist.php")) {
            await apply_ff_gauge_selector(
              node.querySelectorAll(".employee"),
              FEATURE_NAME$1
            );
          } else if (window.location.href.startsWith("https://www.torn.com/messages.php")) {
            await apply_ff_gauge_selector(
              node.querySelectorAll(".name"),
              FEATURE_NAME$1
            );
          } else if (window.location.href.startsWith("https://www.torn.com/index.php")) {
            await apply_ff_gauge_selector(
              node.querySelectorAll(".name"),
              FEATURE_NAME$1
            );
          } else if (window.location.href.startsWith(
            "https://www.torn.com/hospitalview.php"
          )) {
            await apply_ff_gauge_selector(
              node.querySelectorAll(".name"),
              FEATURE_NAME$1
            );
          } else if (window.location.href.startsWith(
            "https://www.torn.com/page.php?sid=UserList"
          )) {
            await apply_ff_gauge_selector(
              node.querySelectorAll(".name"),
              FEATURE_NAME$1
            );
          } else if (window.location.href.startsWith("https://www.torn.com/bounties.php")) {
            await apply_ff_gauge_selector(
              node.querySelectorAll(".target"),
              FEATURE_NAME$1
            );
            await apply_ff_gauge_selector(
              node.querySelectorAll(".listed"),
              FEATURE_NAME$1
            );
          } else if (window.location.href.startsWith(
            "https://www.torn.com/page.php?sid=attackLog"
          )) {
            await apply_ff_gauge_selector(
              node.querySelectorAll("ul.participants-list li"),
              FEATURE_NAME$1
            );
          } else if (window.location.href.startsWith("https://www.torn.com/forums.php")) {
            await apply_ff_gauge_selector(
              node.querySelectorAll(".last-poster"),
              FEATURE_NAME$1
            );
            await apply_ff_gauge_selector(
              node.querySelectorAll(".starter"),
              FEATURE_NAME$1
            );
            await apply_ff_gauge_selector(
              node.querySelectorAll(".last-post"),
              FEATURE_NAME$1
            );
            await apply_ff_gauge_selector(
              node.querySelectorAll(".poster"),
              FEATURE_NAME$1
            );
          } else if (window.location.href.includes("page.php?sid=hof")) {
            await apply_ff_gauge_selector(
              node.querySelectorAll('[class*="userInfoBox__"]'),
              FEATURE_NAME$1
            );
          } else if (name_elems.length > 0) {
            await apply_ff_gauge_selector(name_elems, FEATURE_NAME$1);
          }
        }
        if (window.location.href.startsWith(
          "https://www.torn.com/page.php?sid=ItemMarket"
        )) {
          await apply_ff_gauge_selector(
            node.querySelectorAll(
              "div.bazaar-listing-card div:first-child div:first-child > a"
            ),
            FEATURE_NAME$1
          );
        }
        ffscouter.complete();
      };
      const ff_gauge_observer = new MutationObserver(async (mutations) => {
        for (const mutation of mutations) {
          for (const node of mutation.addedNodes) {
            if (node instanceof Element) {
              check_mutation(node);
            }
          }
        }
      });
      ff_gauge_observer.observe(document, {
        attributes: false,
        childList: true,
        characterData: false,
        subtree: true
      });
    },
    httpIntercept: {
      before(_url, _init) {
        return void 0;
      },
      after(_bodyText, _response, _ctx) {
        return void 0;
      }
    }
  };
  var TOAST_LEVEL = ((TOAST_LEVEL2) => {
    TOAST_LEVEL2[TOAST_LEVEL2["DEBUG"] = 0] = "DEBUG";
    TOAST_LEVEL2[TOAST_LEVEL2["INFO"] = 1] = "INFO";
    TOAST_LEVEL2[TOAST_LEVEL2["WARNING"] = 2] = "WARNING";
    TOAST_LEVEL2[TOAST_LEVEL2["ERROR"] = 3] = "ERROR";
    return TOAST_LEVEL2;
  })(TOAST_LEVEL || {});
  const TOAST_COLOURS = {
    [
      0
]: "blue",
    [
      1
]: "green",
    [
      2
]: "orange",
    [
      3
]: "#c62828"
  };
  function get_toast_colour(level) {
    return TOAST_COLOURS[level];
  }
  function toast(message, level = 1) {
    const existing = document.getElementById("ffscouter-toast");
    if (existing) existing.remove();
    const toast2 = document.createElement("div");
    toast2.id = "ffscouter-toast";
    toast2.style.position = "fixed";
    toast2.style.bottom = "30px";
    toast2.style.left = "50%";
    toast2.style.transform = "translateX(-50%)";
    toast2.style.color = "#fff";
    toast2.style.padding = "8px 16px";
    toast2.style.borderRadius = "8px";
    toast2.style.fontSize = "14px";
    toast2.style.boxShadow = "0 2px 12px rgba(0,0,0,0.2)";
    toast2.style.zIndex = "2147483647";
    toast2.style.opacity = "1";
    toast2.style.transition = "opacity 0.5s";
    toast2.style.display = "flex";
    toast2.style.alignItems = "center";
    toast2.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 = () => toast2.remove();
    toast2.style.background = get_toast_colour(level);
    const msg = document.createElement("span");
    if (message === "Invalid API key. Please sign up at ffscouter.com to use this service") {
      msg.innerHTML = 'FairFight Scouter V2: 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. Register the API key with the script.';
    } else {
      msg.textContent = `FairFight Scouter V2: ${message}`;
    }
    logger.info("[FF Scouter V2] Toast: ", message);
    toast2.appendChild(msg);
    toast2.appendChild(closeBtn);
    document.body.appendChild(toast2);
    setTimeout(() => {
      if (toast2.parentNode) {
        toast2.style.opacity = "0";
        setTimeout(() => toast2.remove(), 500);
      }
    }, 4e3);
  }
  const POLL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
  function get_active_filters() {
    return {
      minlevel: ffconfig.chain_min_level,
      maxlevel: ffconfig.chain_max_level,
      minff: ffconfig.chain_min_ff,
      maxff: ffconfig.chain_max_ff,
      inactive: ffconfig.chain_inactive,
      factionless: ffconfig.chain_factionless
    };
  }
  function filters_changed(a2, b2) {
    return a2.minlevel !== b2.minlevel || a2.maxlevel !== b2.maxlevel || a2.minff !== b2.minff || a2.maxff !== b2.maxff || a2.inactive !== b2.inactive || a2.factionless !== b2.factionless;
  }
  async function update_ff_targets(force = false) {
    const key = ffconfig.key;
    if (!key) {
      logger.debug("API key not set, skipping target fetch");
      return;
    }
    const currentFilters = get_active_filters();
    const cached = ffconfig.chain_targets;
    if (!force && cached && Date.now() < cached.expiry && !filters_changed(cached.filters, currentFilters)) {
      logger.debug("Using cached targets, not expired and filters match");
      return;
    }
    try {
      const response = await query_targets(key, {
        minlevel: currentFilters.minlevel,
        maxlevel: currentFilters.maxlevel,
        minff: currentFilters.minff,
        maxff: currentFilters.maxff,
        inactiveonly: currentFilters.inactive ? 1 : 0,
        factionless: currentFilters.factionless ? 1 : 0,
        limit: 50
      });
      if (response?.targets) {
        ffconfig.chain_targets = {
          targets: response.targets,
          expiry: Date.now() + POLL_INTERVAL_MS,
          filters: currentFilters
        };
        ffconfig.chain_target_index = 0;
        logger.info(
          `Chain targets updated successfully: ${response.targets.length} targets found`
        );
      }
    } catch (err) {
      logger.error("Failed to update chain targets:", err);
    }
  }
  function get_next_target_index(maxLen) {
    const val = ffconfig.chain_target_index;
    let nextVal = val + 1;
    if (nextVal >= maxLen) {
      nextVal = 0;
    }
    ffconfig.chain_target_index = nextVal;
    return val < maxLen ? val : 0;
  }
  function get_random_chain_target() {
    const cached = ffconfig.chain_targets;
    if (!cached || !cached.targets || cached.targets.length === 0) {
      return null;
    }
    const index = get_next_target_index(cached.targets.length);
    return cached.targets[index] ?? null;
  }
  function remove_chain_button() {
    const button = document.getElementById("ff-scouter-chain-btn");
    if (button) {
      button.remove();
    }
  }
  function create_chain_button() {
    if (!ffconfig.chain_button_enabled || !ffconfig.key) {
      remove_chain_button();
      return;
    }
    if (document.getElementById("ff-scouter-chain-btn")) {
      return;
    }
    const button = document.createElement("button");
    button.id = "ff-scouter-chain-btn";
    button.innerHTML = "FF";
    button.style.position = "fixed";
    button.style.top = "32%";
    button.style.right = "0%";
    button.style.zIndex = "9999";
    button.style.backgroundColor = "green";
    button.style.color = "white";
    button.style.border = "none";
    button.style.padding = "6px";
    button.style.borderRadius = "6px";
    button.style.cursor = "pointer";
    button.addEventListener("click", async () => {
      let rando = get_random_chain_target();
      if (!rando) {
        toast("No cached targets found. Fetching...", TOAST_LEVEL.WARNING);
        await update_ff_targets(true);
        rando = get_random_chain_target();
        if (!rando) {
          toast(
            "No targets available matching your criteria.",
            TOAST_LEVEL.ERROR
          );
          return;
        }
      }
      const linkType = ffconfig.chain_link_type;
      const tabType = ffconfig.chain_tab_type;
      let url;
      if (linkType === "profile") {
        url = `https://www.torn.com/profiles.php?XID=${rando.player_id}`;
      } else {
        url = `https://www.torn.com/page.php?sid=attack&user2ID=${rando.player_id}`;
      }
      if (tabType === "sametab") {
        window.location.href = url;
      } else {
        window.open(url, "_blank");
      }
    });
    document.body.appendChild(button);
  }
  const FFButton = {
    name: "FF Target Finder Button",
    description: "Renders the floating green FF button to cycle through potential targets",
    executionTime: StartTime.DocumentBody,
    async shouldRun() {
      return true;
    },
    async run() {
      if (!ffconfig.chain_button_enabled || !ffconfig.key) {
        remove_chain_button();
        return;
      }
      update_ff_targets();
      create_chain_button();
      window.addEventListener("ff-config-updated", async () => {
        if (!ffconfig.chain_button_enabled || !ffconfig.key) {
          remove_chain_button();
          return;
        }
        const cached = ffconfig.chain_targets;
        const currentFilters = get_active_filters();
        if (!cached || filters_changed(cached.filters, currentFilters)) {
          logger.info("Target filters changed, refetching targets immediately");
          await update_ff_targets(true);
        }
        create_chain_button();
      });
    }
  };
  const FEATURE_NAME = "mini-profile";
  const monitor_mini_profile_root = () => {
    const miniprofile = document.querySelector("#profile-mini-root");
    if (miniprofile) {
      logger.debug("profile-mini-root already exists.");
      setup_mini_observer();
      return;
    }
    const mini_body_observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node instanceof HTMLElement && node.id === "profile-mini-root") {
            setup_mini_observer();
            mini_body_observer.disconnect();
          }
        }
      }
    });
    mini_body_observer.observe(document.body, { childList: true });
  };
  const setup_mini_observer = () => {
    const miniroot = document.querySelector("#profile-mini-root");
    if (!miniroot) {
      return;
    }
    const mp_observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        if (mutation.target instanceof HTMLElement && mutation.target.classList.contains("ffsv3-gauge")) {
          return;
        }
        if (Array.from(mutation.addedNodes).some(
          (node) => node instanceof HTMLElement && node.classList.contains("ffsv3-mini-desc")
        )) {
          return;
        }
      }
      const player_id = get_player_id_in_element(miniroot);
      if (!player_id) {
        return;
      }
      ffscouter.get(player_id).then(async (d2) => {
        if (d2.no_data) {
          return;
        }
        logger.debug(
          `Found mini profile update for ${player_id}, adding ff data`
        );
        for (const bar of miniroot.querySelectorAll(".honor-text-wrap")) {
          apply_ff_gauge(bar, FEATURE_NAME);
        }
        miniroot.querySelector(".ffsv3-mini-desc")?.remove();
        const ff_string = format_ff_score(d2);
        const difficulty = format_difficulty_text(d2);
        const fresh = format_relative_time(d2.last_updated);
        const message = `FF ${ff_string} (${difficulty}) ${fresh}`;
        const description = miniroot.querySelector(".description");
        const desc = document.createElement("span");
        desc.classList.add("ffsv3-mini-desc");
        desc.innerText = message;
        description?.appendChild(desc);
      });
      ffscouter.complete();
    });
    mp_observer.observe(miniroot, { childList: true, subtree: true });
  };
  const MiniProfile = {
    name: "Fill mini profile",
    description: "Add FF data to mini profile",
    executionTime: StartTime.DocumentBody,
    async shouldRun() {
      return true;
    },
    async run() {
      monitor_mini_profile_root();
      logger.debug("mini-profile installed");
    },
    httpIntercept: {
      before(_url, _init) {
        return void 0;
      },
      after(_bodyText, _response, _ctx) {
        return void 0;
      }
    }
  };
  async function inject_info_line(info_line) {
    const h4 = await wait_for_element("h4", 1e4);
    if (!h4) {
      return;
    }
    const links_top_wrap = h4.parentNode?.querySelector(".links-top-wrap");
    if (links_top_wrap?.parentNode) {
      links_top_wrap.parentNode.insertBefore(
        info_line,
        links_top_wrap.nextSibling
      );
    } else {
      h4.after(info_line);
    }
  }
  const Profile = {
    name: "Profile FF display",
    description: "Shows FF on top left of any profile page",
    executionTime: StartTime.DocumentBody,
    async shouldRun() {
      return torn_page("profiles");
    },
    async run() {
      const info_line = create_info_line();
      if (!ffconfig.key) {
        info_line.innerHTML = "[FF Scouter V2]: Limited API key needed - enter in FF Scouter Settings below";
        inject_info_line(info_line);
      }
      const player_id = extract_id_from_url(window.location.href);
      if (!player_id) {
        return;
      }
      ffscouter.get(player_id).then(async (data) => {
        const line = document.createElement("ff-header-line");
        line.data = data;
        info_line.appendChild(line);
        inject_info_line(info_line);
      });
      ffscouter.complete();
    },
    httpIntercept: {
      before(_url, _init) {
        return void 0;
      },
      after(_bodyText, _response, _ctx) {
        return void 0;
      }
    }
  };
  var __defProp$1 = Object.defineProperty;
  var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
  var __decorateClass$1 = (decorators, target, key, kind) => {
    var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
    for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
      if (decorator = decorators[i2])
        result = (kind ? decorator(target, key, result) : decorator(result)) || result;
    if (kind && result) __defProp$1(target, key, result);
    return result;
  };
  const PREMIUM_UPGRADE_URL = "https://ffscouter.com/premium";
  const premium_action = b`<a
  href="${PREMIUM_UPGRADE_URL}"
  target="_blank"
  rel="noopener noreferrer"
  style="font-weight: bold; text-decoration: underline;"
  >Upgrade to FFScouter Flight Tracking</a
>`;
  function get_current_time_seconds() {
    if (typeof window.getCurrentTimestamp === "function") {
      return window.getCurrentTimestamp() / 1e3;
    }
    return Date.now() / 1e3;
  }
  function format_duration_human(totalSeconds) {
    const clampedSeconds = Math.max(0, Math.floor(totalSeconds));
    const hours = Math.floor(clampedSeconds / 3600);
    const minutes = Math.floor(clampedSeconds % 3600 / 60);
    const seconds = clampedSeconds % 60;
    const parts = [];
    if (hours > 0) {
      parts.push(`${hours}h`);
    }
    if (hours > 0 || minutes > 0) {
      parts.push(`${minutes}m`);
    }
    parts.push(`${seconds}s`);
    return parts.join(" ");
  }
  function format_tct_time(unixSeconds) {
    if (!Number.isFinite(unixSeconds)) return null;
    const d2 = new Date(unixSeconds * 1e3);
    const hours = String(d2.getUTCHours()).padStart(2, "0");
    const minutes = String(d2.getUTCMinutes()).padStart(2, "0");
    return `${hours}:${minutes}`;
  }
  let FFFlightProfileStatus = class extends i {
    constructor() {
      super(...arguments);
      this.playerId = null;
      this.data = null;
      this.is_premium = null;
      this.loading = false;
      this.error = null;
      this.current_time_seconds = get_current_time_seconds();
      this.fetch_interval = null;
      this.tick_interval = null;
    }
    createRenderRoot() {
      return this;
    }
    connectedCallback() {
      super.connectedCallback();
      this.start_timers();
    }
    disconnectedCallback() {
      this.stop_timers();
      super.disconnectedCallback();
    }
    start_timers() {
      this.stop_timers();
      this.tick_interval = setInterval(() => {
        this.current_time_seconds = get_current_time_seconds();
        if (this.data?.rechecking && this.data.next_retry_at) {
          if (Date.now() >= this.data.next_retry_at) {
            this.fetch_data();
          }
        }
      }, 1e3);
      this.fetch_interval = setInterval(() => {
        if (!this.data?.rechecking) {
          this.fetch_data();
        }
      }, 15e3);
    }
    stop_timers() {
      if (this.tick_interval) {
        clearInterval(this.tick_interval);
        this.tick_interval = null;
      }
      if (this.fetch_interval) {
        clearInterval(this.fetch_interval);
        this.fetch_interval = null;
      }
    }
    async willUpdate(changedProperties) {
      if (changedProperties.has("playerId") && this.playerId) {
        this.data = null;
        this.error = null;
        this.loading = true;
        this.is_premium = null;
        await this.fetch_data();
      }
    }
    async fetch_data() {
      if (!this.playerId) return;
      try {
        if (this.is_premium === null) {
          this.is_premium = await check_key_status.is_premium();
        }
        if (!this.is_premium) {
          this.loading = false;
          return;
        }
        const result = await ffscouter.get_flights(this.playerId);
        this.data = result;
        this.error = null;
      } catch (err) {
        logger.error("Failed to fetch flight data", err);
        if (err instanceof FFApiError) {
          const code = err.ff_api_error?.code;
          if (code === 19) {
            this.is_premium = false;
          } else if (code === 2 || code === 10 || code === 12) {
            this.error = "Invalid API key";
          } else if (code === 20) {
            this.error = "Rate limit exceeded. Retrying...";
          } else {
            this.error = err.ff_api_error?.error || err.message || "Flight tracking unavailable";
          }
        } else {
          this.error = err.message || "Flight tracking unavailable";
        }
      } finally {
        this.loading = false;
      }
    }
    render() {
      if (this.is_premium === false) {
        return b`<div class="ff-scouter-profile-flight-info">
        ${premium_action}
      </div>`;
      }
      let content = b``;
      if (this.error) {
        content = b`<span style="color: #ff6b6b;">Error: ${this.error}</span>`;
      } else if (this.loading && !this.data) {
        content = b`Landing: estimating...`;
      } else {
        const current = this.data?.current;
        if (this.data?.rechecking) {
          const next = this.data.next_retry_at ?? 0;
          const now = Date.now();
          const seconds = Math.max(0, Math.ceil((next - now) / 1e3));
          content = b`No data. Rechecking in ${seconds} seconds.`;
        } else if (!current) {
          content = b`Landing: unavailable for current route`;
        } else {
          const earliest = Number(current.earliest_arrival_time);
          const latest = Number(current.latest_arrival_time);
          if (!Number.isFinite(earliest) || !Number.isFinite(latest)) {
            content = b`Landing: unavailable for current route`;
          } else {
            const nowUnix = this.current_time_seconds;
            const earliestRemaining = Math.max(0, earliest - nowUnix);
            const latestRemaining = Math.max(0, latest - nowUnix);
            const earliestTct = format_tct_time(earliest);
            const latestTct = format_tct_time(latest);
            if (latestRemaining <= 0) {
              content = b`Landing: just landed<br />(${latestTct} TCT latest)`;
            } else if (earliestRemaining <= 0) {
              content = b`Landing: imminent -
              ${format_duration_human(latestRemaining)}<br />(Latest:
              ${latestTct} TCT)`;
            } else {
              content = b`Landing: ${format_duration_human(earliestRemaining)}
              - ${format_duration_human(latestRemaining)}<br />(${earliestTct} -
              ${latestTct} TCT)`;
            }
          }
        }
      }
      return b`<div class="ff-scouter-profile-flight-info">${content}</div>`;
    }
  };
  __decorateClass$1([
    n2({ type: Number })
  ], FFFlightProfileStatus.prototype, "playerId", 2);
  __decorateClass$1([
    n2({ type: Object })
  ], FFFlightProfileStatus.prototype, "data", 2);
  __decorateClass$1([
    r()
  ], FFFlightProfileStatus.prototype, "is_premium", 2);
  __decorateClass$1([
    r()
  ], FFFlightProfileStatus.prototype, "loading", 2);
  __decorateClass$1([
    r()
  ], FFFlightProfileStatus.prototype, "error", 2);
  __decorateClass$1([
    r()
  ], FFFlightProfileStatus.prototype, "current_time_seconds", 2);
  FFFlightProfileStatus = __decorateClass$1([
    t("ff-flight-profile-status")
  ], FFFlightProfileStatus);
  function is_flying(status) {
    return status.classList.contains("travelling");
  }
  const ProfileFlights = {
    name: "Profile flight tracking",
    description: "Display flight estimates on player profiles if they're flying",
    executionTime: StartTime.DocumentBody,
    async shouldRun() {
      return torn_page("profiles");
    },
    async run() {
      const player_id = extract_id_from_url(window.location.href);
      if (!player_id) {
        return;
      }
      const status = await wait_for_element(".profile-status", 1e4);
      if (!status) {
        return;
      }
      const element = document.createElement("ff-flight-profile-status");
      element.playerId = player_id;
      const check_and_update = () => {
        if (is_flying(status)) {
          const description = status.querySelector(".description");
          if (description === null) {
            return;
          }
          if (!description.contains(element)) {
            console.log("Appending child", element);
            description.appendChild(element);
          }
        } else {
          element.remove();
        }
      };
      const status_observer = new MutationObserver(check_and_update);
      status_observer.observe(status, {
        attributes: true,
        attributeFilter: ["class"]
      });
      check_and_update();
    },
    httpIntercept: {
      before(_url, _init) {
        return void 0;
      },
      after(_bodyText, _response, _ctx) {
        return void 0;
      }
    }
  };
  const ProfileHistory = {
    name: "Profile FF history",
    description: "Add a button to all user's profiles to link to ffscouter.com",
    executionTime: StartTime.DocumentBody,
    async shouldRun() {
      return torn_page("profiles");
    },
    async run() {
      const player_id = extract_id_from_url(window.location.href);
      if (!player_id) {
        return;
      }
      if (document.querySelector(".ff-scouter-history-btn")) return;
      const buttonsList = await wait_for_element(
        ".profile-buttons.profile-action .buttons-list",
        1e4
      );
      if (!buttonsList) return;
      const btn = document.createElement("a");
      btn.href = `https://ffscouter.com/player-view?player_id=${player_id}`;
      btn.target = "_blank";
      btn.rel = "noopener noreferrer";
      btn.className = "profile-button";
      btn.title = "View Stats History on FFScouter";
      const container = document.createElement("div");
      container.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="46" height="46" viewBox="640 178 46 46" class="icon___GP196">
  <g fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <!-- Clock face outline representing history/time -->
    <circle cx="663" cy="201" r="17" />
    <!-- Clock hands pointing to 10:10 -->
    <path d="M663,201 L663,191 M663,201 L671,197" />
  </g>
  <g fill="none" stroke-width="1">
    <text x="657" y="212">FF</text>
  </g>
</svg>`;
      for (const node of container.children) {
        btn.appendChild(node);
      }
      const label = document.createElement("span");
      label.className = "ff-history-label";
      label.textContent = "FF\nHistory";
      buttonsList.appendChild(btn);
    },
    httpIntercept: {
      before(_url, _init) {
        return void 0;
      },
      after(_bodyText, _response, _ctx) {
        return void 0;
      }
    }
  };
  var __defProp = Object.defineProperty;
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  var __decorateClass = (decorators, target, key, kind) => {
    var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
    for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
      if (decorator = decorators[i2])
        result = (kind ? decorator(target, key, result) : decorator(result)) || result;
    if (kind && result) __defProp(target, key, result);
    return result;
  };
  let FFSettingsPanel = class extends i {
    constructor() {
      super(...arguments);
      this.apiKey = "";
      this.lowRange = CONFIG_DEFAULTS.low_ff_range;
      this.highRange = CONFIG_DEFAULTS.high_ff_range;
      this.maxRange = CONFIG_DEFAULTS.max_ff_range;
      this.chainButtonEnabled = CONFIG_DEFAULTS.chain_button_enabled;
      this.chainLinkType = CONFIG_DEFAULTS.chain_link_type;
      this.chainTabType = CONFIG_DEFAULTS.chain_tab_type;
      this.chainFFTarget = CONFIG_DEFAULTS.chain_ff_target;
      this.chainMinLevel = CONFIG_DEFAULTS.chain_min_level;
      this.chainMaxLevel = CONFIG_DEFAULTS.chain_max_level;
      this.chainInactive = CONFIG_DEFAULTS.chain_inactive;
      this.chainMinFF = CONFIG_DEFAULTS.chain_min_ff;
      this.chainMaxFF = CONFIG_DEFAULTS.chain_max_ff;
      this.chainFactionless = CONFIG_DEFAULTS.chain_factionless;
      this.ffHistoryEnabled = CONFIG_DEFAULTS.ff_history_enabled;
      this.factionsColDisplay = CONFIG_DEFAULTS.factions_col_display;
      this.debugLogs = CONFIG_DEFAULTS.debug_logs;
      this.analyticsEnabled = CONFIG_DEFAULTS.analytics_enabled;
      this.isPremium = false;
      this.draftApiKey = "";
      this.draftLowRange = CONFIG_DEFAULTS.low_ff_range;
      this.draftHighRange = CONFIG_DEFAULTS.high_ff_range;
      this.draftMaxRange = CONFIG_DEFAULTS.max_ff_range;
      this.draftChainButtonEnabled = CONFIG_DEFAULTS.chain_button_enabled;
      this.draftChainLinkType = CONFIG_DEFAULTS.chain_link_type;
      this.draftChainTabType = CONFIG_DEFAULTS.chain_tab_type;
      this.draftChainFFTarget = CONFIG_DEFAULTS.chain_ff_target;
      this.draftChainMinLevel = CONFIG_DEFAULTS.chain_min_level;
      this.draftChainMaxLevel = CONFIG_DEFAULTS.chain_max_level;
      this.draftChainInactive = CONFIG_DEFAULTS.chain_inactive;
      this.draftChainMinFF = CONFIG_DEFAULTS.chain_min_ff;
      this.draftChainMaxFF = CONFIG_DEFAULTS.chain_max_ff;
      this.draftChainFactionless = CONFIG_DEFAULTS.chain_factionless;
      this.draftFFHistoryEnabled = CONFIG_DEFAULTS.ff_history_enabled;
      this.draftFactionsColDisplay = CONFIG_DEFAULTS.factions_col_display;
      this.draftDebugLogs = CONFIG_DEFAULTS.debug_logs;
      this.draftAnalyticsEnabled = CONFIG_DEFAULTS.analytics_enabled;
      this.rangeError = "";
      this.showSavedMessage = false;
    }
    createRenderRoot() {
      return this;
    }
    connectedCallback() {
      super.connectedCallback();
      this.resetDrafts();
    }
    willUpdate(changedProperties) {
      if (changedProperties.has("apiKey") || changedProperties.has("lowRange") || changedProperties.has("highRange") || changedProperties.has("maxRange") || changedProperties.has("chainButtonEnabled") || changedProperties.has("chainLinkType") || changedProperties.has("chainTabType") || changedProperties.has("chainFFTarget") || changedProperties.has("chainMinLevel") || changedProperties.has("chainMaxLevel") || changedProperties.has("chainInactive") || changedProperties.has("chainMinFF") || changedProperties.has("chainMaxFF") || changedProperties.has("chainFactionless") || changedProperties.has("ffHistoryEnabled") || changedProperties.has("factionsColDisplay") || changedProperties.has("debugLogs") || changedProperties.has("analyticsEnabled")) {
        this.resetDrafts();
      }
    }
    resetDrafts() {
      this.draftApiKey = this.apiKey;
      this.draftLowRange = this.lowRange;
      this.draftHighRange = this.highRange;
      this.draftMaxRange = this.maxRange;
      this.draftChainButtonEnabled = this.chainButtonEnabled;
      this.draftChainLinkType = this.chainLinkType;
      this.draftChainTabType = this.chainTabType;
      this.draftChainFFTarget = this.chainFFTarget;
      this.draftChainMinLevel = this.chainMinLevel;
      this.draftChainMaxLevel = this.chainMaxLevel;
      this.draftChainInactive = this.chainInactive;
      this.draftChainMinFF = this.chainMinFF;
      this.draftChainMaxFF = this.chainMaxFF;
      this.draftChainFactionless = this.chainFactionless;
      this.draftFFHistoryEnabled = this.ffHistoryEnabled;
      this.draftFactionsColDisplay = this.factionsColDisplay;
      this.draftDebugLogs = this.debugLogs;
      this.draftAnalyticsEnabled = this.analyticsEnabled;
    }
    handleSave() {
      const low = this.draftLowRange;
      const high = this.draftHighRange;
      const max = this.draftMaxRange;
      if (Number.isNaN(low) || Number.isNaN(high) || Number.isNaN(max)) {
        this.rangeError = "FF ranges must be valid numbers";
        return;
      }
      if (low <= 0 || high <= 0 || max <= 0) {
        this.rangeError = "FF ranges must be positive numbers";
        return;
      }
      if (low >= high || high >= max) {
        this.rangeError = "FF ranges must be in ascending order: low < high < max";
        return;
      }
      this.rangeError = "";
      this.showSavedMessage = true;
      setTimeout(() => {
        this.showSavedMessage = false;
      }, 3e3);
      this.dispatchEvent(
        new CustomEvent("ff-save", {
          detail: {
            apiKey: this.draftApiKey,
            lowRange: low,
            highRange: high,
            maxRange: max,
            chainButtonEnabled: this.draftChainButtonEnabled,
            chainLinkType: this.draftChainLinkType,
            chainTabType: this.draftChainTabType,
            chainFFTarget: this.draftChainFFTarget,
            chainMinLevel: this.draftChainMinLevel,
            chainMaxLevel: this.draftChainMaxLevel,
            chainInactive: this.draftChainInactive,
            chainMinFF: this.draftChainMinFF,
            chainMaxFF: this.draftChainMaxFF,
            chainFactionless: this.draftChainFactionless,
            ffHistoryEnabled: this.draftFFHistoryEnabled,
            factionsColDisplay: this.draftFactionsColDisplay,
            debugLogs: this.draftDebugLogs,
            analyticsEnabled: this.draftAnalyticsEnabled
          },
          bubbles: true,
          composed: true
        })
      );
    }
    handleReset() {
      if (confirm("Are you sure you want to reset all settings to defaults?")) {
        this.dispatchEvent(
          new CustomEvent("ff-reset", {
            bubbles: true,
            composed: true
          })
        );
      }
    }
    handleClearCache() {
      if (confirm("Are you sure you want to clear all FF Scouter cache?")) {
        this.dispatchEvent(
          new CustomEvent("ff-clear-cache", {
            bubbles: true,
            composed: true
          })
        );
      }
    }
    handleVerify() {
      this.dispatchEvent(
        new CustomEvent("ff-verify", {
          detail: {
            apiKey: this.draftApiKey
          },
          bubbles: true,
          composed: true
        })
      );
    }
    onKeyInput(e2) {
      this.draftApiKey = e2.target.value;
      this.showSavedMessage = false;
    }
    onLowRangeInput(e2) {
      this.draftLowRange = Number(e2.target.value);
      this.showSavedMessage = false;
    }
    onHighRangeInput(e2) {
      this.draftHighRange = Number(e2.target.value);
      this.showSavedMessage = false;
    }
    onMaxRangeInput(e2) {
      this.draftMaxRange = Number(e2.target.value);
      this.showSavedMessage = false;
    }
    onChainButtonChange(e2) {
      this.draftChainButtonEnabled = e2.target.checked;
      this.showSavedMessage = false;
    }
    onChainLinkTypeChange(e2) {
      this.draftChainLinkType = e2.target.value;
      this.showSavedMessage = false;
    }
    onChainTabTypeChange(e2) {
      this.draftChainTabType = e2.target.value;
      this.showSavedMessage = false;
    }
    onChainMinLevelInput(e2) {
      const val = e2.target.value;
      this.draftChainMinLevel = val === "" ? null : Number(val);
      this.showSavedMessage = false;
    }
    onChainMaxLevelInput(e2) {
      const val = e2.target.value;
      this.draftChainMaxLevel = val === "" ? null : Number(val);
      this.showSavedMessage = false;
    }
    onChainInactiveChange(e2) {
      this.draftChainInactive = e2.target.checked;
      this.showSavedMessage = false;
    }
    onChainMinFFInput(e2) {
      const val = e2.target.value;
      this.draftChainMinFF = val === "" ? null : Number(val);
      this.showSavedMessage = false;
    }
    onChainMaxFFInput(e2) {
      const val = e2.target.value;
      const num = Number(val);
      this.draftChainMaxFF = num;
      this.draftChainFFTarget = num;
      this.showSavedMessage = false;
    }
    onChainFactionlessChange(e2) {
      this.draftChainFactionless = e2.target.checked;
      this.showSavedMessage = false;
    }
    onFFHistoryChange(e2) {
      this.draftFFHistoryEnabled = e2.target.checked;
      this.showSavedMessage = false;
    }
    onFactionsColDisplayChange(e2) {
      this.draftFactionsColDisplay = e2.target.value;
      this.showSavedMessage = false;
    }
    onDebugLogsChange(e2) {
      this.draftDebugLogs = e2.target.checked;
      this.showSavedMessage = false;
    }
    onAnalyticsEnabledChange(e2) {
      this.draftAnalyticsEnabled = e2.target.checked;
      this.showSavedMessage = false;
    }
    render() {
      return b`
      <details
        class="accordion ${!this.apiKey ? "glow" : ""} cont-gray border-round"
      >
        <summary style="cursor: pointer; font-weight: bold;">
          FF Scouter Settings
        </summary>

        <div style="margin-top: 15px;">
          <div class="ff-api-explanation">
            <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.
          </div>
          <!-- API Key Input -->
          <div class="input-row">
            <label for="api-key">API Key:</label>
            <input
              id="api-key"
              type="text"
              class="${this.apiKey ? "blur-mode" : ""}"
              placeholder="Paste your key here..."
              .value=${this.draftApiKey}
              @input=${this.onKeyInput}
            />
          </div>
          <div class="input-row-inline">
            <label for="ff-premium-badge">FF Scouter Premium:</label>
            <span
              id="ff-premium-badge"
              class="is_premium_${this.isPremium ? "enabled" : "disabled"}"
              >${this.isPremium ? "Enabled" : "Disabled"}</span
            >
          </div>
          <div class="input-row-inline">
            <button class="torn-btn btn-save" @click=${this.handleVerify}>
              Verify
            </button>
          </div>

          <!-- Ranges Input -->
          <div class="input-row">
            <label>FF Ranges (Low, High, Max):</label>
            <div style="display: flex; gap: 10px; align-items: center;">
              <input
                id="ff-range-low"
                type="number"
                step="0.1"
                class="ff-number"
                .value=${this.draftLowRange.toString()}
                @input=${this.onLowRangeInput}
              />
              <span>&lt;</span>
              <input
                id="ff-range-high"
                type="number"
                step="0.1"
                class="ff-number"
                .value=${this.draftHighRange.toString()}
                @input=${this.onHighRangeInput}
              />
              <span>&lt;</span>
              <input
                id="ff-range-max"
                type="number"
                step="0.1"
                class="ff-number"
                .value=${this.draftMaxRange.toString()}
                @input=${this.onMaxRangeInput}
              />
            </div>
            ${this.rangeError ? b`<div class="error-msg">${this.rangeError}</div>` : ""}
          </div>

          <!-- Feature Toggles -->
          <h3>Feature Toggles:</h3>

          <!-- Chain Button Toggle -->
          <div class="input-row-inline">
            <input
              id="chain-button-toggle"
              type="checkbox"
              .checked=${this.draftChainButtonEnabled}
              @change=${this.onChainButtonChange}
            />
            <label for="chain-button-toggle"
              >Enable Chain Button (Green FF Button)</label
            >
          </div>

          ${this.draftChainButtonEnabled ? b`
                <div style="margin-left: 20px;">
                  <div class="input-row-inline">
                    <label for="chain-link-type">Chain button opens:</label>
                    <select
                      id="chain-link-type"
                      .value=${this.draftChainLinkType}
                      @change=${this.onChainLinkTypeChange}
                    >
                      <option value="attack">Attack page</option>
                      <option value="profile">Profile page</option>
                    </select>
                  </div>

                  <div class="input-row-inline">
                    <label for="chain-tab-type">Open in:</label>
                    <select
                      id="chain-tab-type"
                      .value=${this.draftChainTabType}
                      @change=${this.onChainTabTypeChange}
                    >
                      <option value="newtab">New tab</option>
                      <option value="sametab">Same tab</option>
                    </select>
                  </div>

                  <div class="input-row-inline">
                    <label for="chain-min-level">Min Level:</label>
                    <input
                      id="chain-min-level"
                      type="number"
                      class="ff-number"
                      placeholder="No min"
                      .value=${this.draftChainMinLevel === null ? "" : this.draftChainMinLevel.toString()}
                      @input=${this.onChainMinLevelInput}
                    />
                  </div>

                  <div class="input-row-inline">
                    <label for="chain-max-level">Max Level:</label>
                    <input
                      id="chain-max-level"
                      type="number"
                      class="ff-number"
                      placeholder="No max"
                      .value=${this.draftChainMaxLevel === null ? "" : this.draftChainMaxLevel.toString()}
                      @input=${this.onChainMaxLevelInput}
                    />
                  </div>

                  <div class="input-row-inline">
                    <label for="chain-min-ff">Min FF:</label>
                    <input
                      id="chain-min-ff"
                      type="number"
                      step="0.1"
                      class="ff-number"
                      placeholder="No min"
                      .value=${this.draftChainMinFF === null ? "" : this.draftChainMinFF.toString()}
                      @input=${this.onChainMinFFInput}
                    />
                  </div>

                  <div class="input-row-inline">
                    <label for="chain-max-ff">Max FF:</label>
                    <input
                      id="chain-max-ff"
                      type="number"
                      step="0.1"
                      class="ff-number"
                      placeholder="No max"
                      .value=${this.draftChainMaxFF.toString()}
                      @input=${this.onChainMaxFFInput}
                    />
                  </div>

                  <div class="input-row-inline">
                    <input
                      id="chain-inactive"
                      type="checkbox"
                      .checked=${this.draftChainInactive}
                      @change=${this.onChainInactiveChange}
                    />
                    <label for="chain-inactive">Inactive Only (14+ days offline)</label>
                  </div>

                  <div class="input-row-inline">
                    <input
                      id="chain-factionless"
                      type="checkbox"
                      .checked=${this.draftChainFactionless}
                      @change=${this.onChainFactionlessChange}
                    />
                    <label for="chain-factionless">Factionless Only</label>
                  </div>
                </div>
              ` : ""}

          <!-- FF History Toggle -->
          <div class="input-row-inline">
            <input
              id="ff-history-toggle"
              type="checkbox"
              .checked=${this.draftFFHistoryEnabled}
              @change=${this.onFFHistoryChange}
            />
            <label for="ff-history-toggle"
              >Enable FF History button on profile pages</label
            >
          </div>

          <!-- Factions Column Display -->
          <div class="input-row-inline">
            <label for="factions-col-display">Faction Page Shows:</label>
            <select
              id="factions-col-display"
              .value=${this.draftFactionsColDisplay}
              @change=${this.onFactionsColDisplayChange}
            >
              <option value="fair_fight">FF Score</option>
              <option value="battle_stats">BS Estimate</option>
              <option value="none">None (Hide Column)</option>
            </select>
          </div>

          <!-- Debug Settings -->
          <h3>Debug Settings:</h3>
          <div class="input-row-inline">
            <input
              id="debug-logs"
              type="checkbox"
              .checked=${this.draftDebugLogs}
              @change=${this.onDebugLogsChange}
            />
            <label for="debug-logs">Enable debug logging</label>
          </div>

          <!-- Analytics Toggle -->
          <div class="input-row-inline">
            <input
              id="analytics-toggle"
              type="checkbox"
              .checked=${this.draftAnalyticsEnabled}
              @change=${this.onAnalyticsEnabledChange}
            />
            <label for="analytics-toggle"
              >Enable local analytics logging (last 30 days)</label
            >
          </div>

          <!-- Action Buttons Area -->
          <div
            style="display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 10px; margin-top: 20px;"
          >
            <button class="torn-btn btn-save" @click=${this.handleSave}>
              Save Settings
            </button>
            <button class="torn-btn btn-secondary" @click=${this.handleReset}>
              Reset to Defaults
            </button>
            <button
              class="torn-btn btn-secondary"
              @click=${this.handleClearCache}
            >
              Clear FF Cache
            </button>
            ${this.showSavedMessage ? b`<span style="color: #4CAF50;">✓ Saved!</span>` : ""}
          </div>
        </div>
      </details>
    `;
    }
  };
  __decorateClass([
    n2({ type: String })
  ], FFSettingsPanel.prototype, "apiKey", 2);
  __decorateClass([
    n2({ type: Number })
  ], FFSettingsPanel.prototype, "lowRange", 2);
  __decorateClass([
    n2({ type: Number })
  ], FFSettingsPanel.prototype, "highRange", 2);
  __decorateClass([
    n2({ type: Number })
  ], FFSettingsPanel.prototype, "maxRange", 2);
  __decorateClass([
    n2({ type: Boolean })
  ], FFSettingsPanel.prototype, "chainButtonEnabled", 2);
  __decorateClass([
    n2({ type: String })
  ], FFSettingsPanel.prototype, "chainLinkType", 2);
  __decorateClass([
    n2({ type: String })
  ], FFSettingsPanel.prototype, "chainTabType", 2);
  __decorateClass([
    n2({ type: Number })
  ], FFSettingsPanel.prototype, "chainFFTarget", 2);
  __decorateClass([
    n2({ type: Number })
  ], FFSettingsPanel.prototype, "chainMinLevel", 2);
  __decorateClass([
    n2({ type: Number })
  ], FFSettingsPanel.prototype, "chainMaxLevel", 2);
  __decorateClass([
    n2({ type: Boolean })
  ], FFSettingsPanel.prototype, "chainInactive", 2);
  __decorateClass([
    n2({ type: Number })
  ], FFSettingsPanel.prototype, "chainMinFF", 2);
  __decorateClass([
    n2({ type: Number })
  ], FFSettingsPanel.prototype, "chainMaxFF", 2);
  __decorateClass([
    n2({ type: Boolean })
  ], FFSettingsPanel.prototype, "chainFactionless", 2);
  __decorateClass([
    n2({ type: Boolean })
  ], FFSettingsPanel.prototype, "ffHistoryEnabled", 2);
  __decorateClass([
    n2({ type: String })
  ], FFSettingsPanel.prototype, "factionsColDisplay", 2);
  __decorateClass([
    n2({ type: Boolean })
  ], FFSettingsPanel.prototype, "debugLogs", 2);
  __decorateClass([
    n2({ type: Boolean })
  ], FFSettingsPanel.prototype, "analyticsEnabled", 2);
  __decorateClass([
    n2({ type: Boolean })
  ], FFSettingsPanel.prototype, "isPremium", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftApiKey", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftLowRange", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftHighRange", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftMaxRange", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftChainButtonEnabled", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftChainLinkType", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftChainTabType", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftChainFFTarget", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftChainMinLevel", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftChainMaxLevel", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftChainInactive", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftChainMinFF", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftChainMaxFF", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftChainFactionless", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftFFHistoryEnabled", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftFactionsColDisplay", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftDebugLogs", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "draftAnalyticsEnabled", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "rangeError", 2);
  __decorateClass([
    r()
  ], FFSettingsPanel.prototype, "showSavedMessage", 2);
  FFSettingsPanel = __decorateClass([
    t("ff-settings-panel")
  ], FFSettingsPanel);
  const Settings = {
    name: "FF Scouter settings panel",
    description: "Give users a FF Scouter settings box injected on the profile page",
    executionTime: StartTime.DocumentBody,
    async shouldRun() {
      return torn_page("profiles");
    },
    async run() {
      const panel = document.createElement("ff-settings-panel");
      panel.apiKey = ffconfig.key || "";
      panel.lowRange = ffconfig.low_ff_range;
      panel.highRange = ffconfig.high_ff_range;
      panel.maxRange = ffconfig.max_ff_range;
      panel.chainButtonEnabled = ffconfig.chain_button_enabled;
      panel.chainLinkType = ffconfig.chain_link_type;
      panel.chainTabType = ffconfig.chain_tab_type;
      panel.chainFFTarget = ffconfig.chain_ff_target;
      panel.chainMinLevel = ffconfig.chain_min_level;
      panel.chainMaxLevel = ffconfig.chain_max_level;
      panel.chainInactive = ffconfig.chain_inactive;
      panel.chainMinFF = ffconfig.chain_min_ff;
      panel.chainMaxFF = ffconfig.chain_max_ff;
      panel.chainFactionless = ffconfig.chain_factionless;
      panel.ffHistoryEnabled = ffconfig.ff_history_enabled;
      panel.factionsColDisplay = ffconfig.factions_col_display;
      panel.debugLogs = ffconfig.debug_logs;
      panel.analyticsEnabled = ffconfig.analytics_enabled;
      panel.isPremium = await check_key_status.is_premium(true);
      panel.addEventListener("ff-save", async (e2) => {
        const detail = e2.detail;
        ffconfig.key = detail.apiKey;
        ffconfig.low_ff_range = detail.lowRange;
        ffconfig.high_ff_range = detail.highRange;
        ffconfig.max_ff_range = detail.maxRange;
        ffconfig.chain_button_enabled = detail.chainButtonEnabled;
        ffconfig.chain_link_type = detail.chainLinkType;
        ffconfig.chain_tab_type = detail.chainTabType;
        ffconfig.chain_ff_target = detail.chainFFTarget;
        ffconfig.chain_min_level = detail.chainMinLevel;
        ffconfig.chain_max_level = detail.chainMaxLevel;
        ffconfig.chain_inactive = detail.chainInactive;
        ffconfig.chain_min_ff = detail.chainMinFF;
        ffconfig.chain_max_ff = detail.chainMaxFF;
        ffconfig.chain_factionless = detail.chainFactionless;
        ffconfig.ff_history_enabled = detail.ffHistoryEnabled;
        ffconfig.factions_col_display = detail.factionsColDisplay;
        ffconfig.debug_logs = detail.debugLogs;
        ffconfig.analytics_enabled = detail.analyticsEnabled;
        panel.isPremium = await check_key_status.is_premium(true);
        toast("Settings saved successfully!");
        window.dispatchEvent(new CustomEvent("ff-config-updated"));
      });
      panel.addEventListener("ff-reset", () => {
        ffconfig.reset();
        panel.apiKey = ffconfig.key || "";
        panel.lowRange = ffconfig.low_ff_range;
        panel.highRange = ffconfig.high_ff_range;
        panel.maxRange = ffconfig.max_ff_range;
        panel.chainButtonEnabled = ffconfig.chain_button_enabled;
        panel.chainLinkType = ffconfig.chain_link_type;
        panel.chainTabType = ffconfig.chain_tab_type;
        panel.chainFFTarget = ffconfig.chain_ff_target;
        panel.chainMinLevel = ffconfig.chain_min_level;
        panel.chainMaxLevel = ffconfig.chain_max_level;
        panel.chainInactive = ffconfig.chain_inactive;
        panel.chainMinFF = ffconfig.chain_min_ff;
        panel.chainMaxFF = ffconfig.chain_max_ff;
        panel.chainFactionless = ffconfig.chain_factionless;
        panel.ffHistoryEnabled = ffconfig.ff_history_enabled;
        panel.factionsColDisplay = ffconfig.factions_col_display;
        panel.debugLogs = ffconfig.debug_logs;
        panel.analyticsEnabled = ffconfig.analytics_enabled;
        toast("Settings reset to defaults!");
        window.dispatchEvent(new CustomEvent("ff-config-updated"));
      });
      panel.addEventListener("ff-clear-cache", async () => {
        try {
          ffscouter.clear_cache();
          toast("FF Scouter cache cleared successfully!");
        } catch (err) {
          console.error("Failed to delete IndexedDB cache", err);
          toast("Failed to clear cache database", TOAST_LEVEL.ERROR);
        }
      });
      panel.addEventListener("ff-verify", async (e2) => {
        const detail = e2.detail;
        let result = null;
        try {
          result = await check_key(detail.apiKey);
        } catch (err) {
          toast(`${err}`, TOAST_LEVEL.ERROR);
          return;
        }
        if (result == null || result.blank) {
          toast(
            "Problem querying ffscouter.com API. Please wait a few seconds and try again.",
            TOAST_LEVEL.WARNING
          );
          return;
        }
        let message = `FF Scouter not configured. API key (${result.result.key}) not registered.`;
        let level = TOAST_LEVEL.ERROR;
        if (result.result.is_registered) {
          message = `FF Scouter successfully configured. Don't forget to save! API key (${result.result.key}) was registered on ${format_timestamp(result.result.registered_at)} and last used ${format_timestamp(result.result.last_used)}.`;
          level = TOAST_LEVEL.INFO;
        }
        toast(message, level);
      });
      const profileWrapper = await wait_for_element(".profile-wrapper", 15e3);
      if (!profileWrapper) {
        console.error(
          "[FF Scouter V2] Could not find profile wrapper for settings panel"
        );
        return;
      }
      profileWrapper.parentNode?.insertBefore(panel, profileWrapper.nextSibling);
    },
    httpIntercept: {
      before(_url, _init) {
        return void 0;
      },
      after(_bodyText, _response, _ctx) {
        return void 0;
      }
    }
  };
  const Features = [
    Settings,
    Profile,
    ProfileHistory,
    ProfileFlights,
    Attack,
    Faction,
    MiniProfile,
    FFButton,
    Fallback
  ];
  const pageWindow = unsafeWindow || window;
  let httpInterceptor = null;
  let httpPatched = false;
  function setHttpInterceptor(interceptor) {
    if (httpInterceptor) {
      throw new Error("HTTP interceptor already set. Only one allowed.");
    }
    httpInterceptor = interceptor;
    if (!httpPatched) patchFetch();
  }
  function patchFetch() {
    try {
      const originalFetch = pageWindow.fetch.bind(pageWindow);
      pageWindow.fetch = async function patchedFetch(input, init) {
        if (!httpInterceptor) return originalFetch(input, init);
        const requestIsObj = input instanceof Request;
        let url = requestIsObj ? input.url : typeof input === "string" ? input : input.toString();
        let currentInit = init;
        if (requestIsObj && !currentInit && (httpInterceptor.before || httpInterceptor.after)) {
          const r2 = input;
          currentInit = {
            method: r2.method,
            headers: r2.headers,
            body: r2.method !== "GET" && r2.method !== "HEAD" ? r2.clone().body : void 0,
            credentials: r2.credentials,
            mode: r2.mode,
            cache: r2.cache,
            redirect: r2.redirect,
            referrer: r2.referrer,
            referrerPolicy: r2.referrerPolicy,
            integrity: r2.integrity,
            duplex: r2.duplex
          };
        }
        if (httpInterceptor.before) {
          try {
            const res = httpInterceptor.before(url, currentInit);
            if (typeof res === "string") url = res;
            else if (res) {
              if (res.url) url = res.url;
              if (res.init) currentInit = res.init;
            }
          } catch (err) {
            logger.error("HTTP before interceptor error:", err);
          }
        }
        const finalRequest = requestIsObj ? url === input.url && currentInit === init ? input : new Request(url, currentInit ?? {}) : url;
        const response = await originalFetch(finalRequest, currentInit);
        if (!httpInterceptor.after) return response;
        let bodyText = "";
        try {
          bodyText = await response.clone().text();
        } catch (err) {
          logger.error("Failed reading response body for interceptor:", err);
          return response;
        }
        try {
          const maybeNew = await httpInterceptor.after(bodyText, response, {
            url,
            init: currentInit
          });
          if (typeof maybeNew === "string") {
            return new Response(maybeNew, {
              status: response.status,
              statusText: response.statusText,
              headers: new Headers(response.headers)
            });
          }
        } catch (err) {
          logger.error("HTTP after interceptor error:", err);
        }
        return response;
      };
      httpPatched = true;
      logger.debug("Fetch patched for HTTP interception");
    } catch (err) {
      logger.error("Failed to patch fetch:", err);
    }
  }
  const stylesCss = ".ffsv3-gauge{position:relative;display:block;padding:0}.ffsv3-arrow{position:absolute;transform:translate(-50%,-30%);padding:0;top:0;left:calc(var(--ffsv3-arrow-width) / 2 + var(--band-percent) * (100% - var(--ffsv3-arrow-width)) / 100);width:var(--ffsv3-arrow-width);object-fit:cover;pointer-events:none}.ffsv3-mini-desc{padding:0 5px}body{--ffsv3-bg-color: #f0f0f0;--ffsv3-alt-bg-color: #fff;--ffsv3-border-color: #ccc;--ffsv3-input-color: #ccc;--ffsv3-text-color: #000;--ffsv3-hover-color: #ddd;--ffsv3-glow-color: #4caf50;--ffsv3-success-color: #4caf50;--ffsv3-arrow-width: 20px}body.dark-mode{--ffsv3-bg-color: #333;--ffsv3-alt-bg-color: #383838;--ffsv3-border-color: #444;--ffsv3-input-color: #504f4f;--ffsv3-text-color: #ccc;--ffsv3-hover-color: #555;--ffsv3-glow-color: #4caf50;--ffsv3-success-color: #4caf50}.ff-premium-upgrade-line{display:block;margin-top:4px;line-height:1.3;white-space:nowrap;font-size:12px;font-style:normal}@media(max-width:768px){.ff-premium-upgrade-line{margin-top:6px;line-height:1.35;white-space:normal;overflow-wrap:anywhere}}ff-settings-panel{display:block}ff-settings-panel .accordion{margin:10px 0;padding:15px;background-color:var(--ffsv3-bg-color);border:1px solid var(--ffsv3-border-color);border-radius:5px;color:var(--ffsv3-text-color)}ff-settings-panel .accordion.glow{border-color:var(--ffsv3-glow-color);box-shadow:0 0 8px #4caf5080}ff-settings-panel .input-row{display:flex;flex-direction:column;gap:5px;margin-bottom:15px}ff-settings-panel .input-row-inline{display:flex;align-items:center;gap:10px;margin-bottom:15px}ff-settings-panel .blur-mode{filter:blur(4px);transition:filter .2s ease}ff-settings-panel .blur-mode:hover,ff-settings-panel .blur-mode:focus{filter:blur(0)}ff-settings-panel .error-msg{color:#f33;font-size:13px;margin-top:5px}ff-settings-panel input[type=text],ff-settings-panel input[type=number]{text-align:left;vertical-align:top;width:178px;height:14px;margin-right:8px;padding:9px 10px;line-height:14px;display:inline-block}ff-settings-panel input[type=number].ff-number{width:52px}ff-settings-panel select{box-sizing:border-box;text-align:left;vertical-align:top;width:178px;height:34px;margin-right:8px;padding:8px 10px;line-height:14px;display:inline-block;border:var(--input-border-color);border-radius:5px;font-family:Arial,serif;color:var(--input-color);background:var(--input-background-color)}:root .dark-mode ff-settings-panel select option{background-color:#000;color:var(--input-color)}ff-settings-panel .ff-api-explanation{background-color:var(--ffsv3-alt-bg-color);border:1px solid var(--ffsv3-border-color);border-radius:8px;color:var(--ffsv3-text-color);margin-bottom:20px;padding:12px 16px;font-size:13px;line-height:1.5}ff-settings-panel .ff-api-explanation a{color:var(--ffsv3-success-color);text-decoration:underline}ff-settings-panel .is_premium_enabled{display:inline-block;background:#4caf50;color:#fff;font-size:11px;font-weight:700;padding:2px 8px;border-radius:4px;vertical-align:middle}ff-settings-panel .is_premium_disabled{display:inline-block;background:#c62828;color:#fff;font-size:11px;font-weight:700;padding:2px 8px;border-radius:4px;vertical-align:middle}.profile-status{position:relative}ff-flight-profile-status{position:absolute;right:10px;bottom:2px;z-index:2}.ff-scouter-profile-flight-info{display:inline-block;text-align:right;font-size:11px;line-height:1.25;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.85)}.profile-status .ff-scouter-profile-flight-info a{color:#fff;text-decoration:underline}ff-faction-filter-box{display:block}.ff-filter-box,.ff-filter-box *,.ff-filter-box *:before,.ff-filter-box *:after{box-sizing:border-box!important}.ff-filter-box{background-color:var(--ffsv3-bg-color);border:1px solid var(--ffsv3-border-color);border-radius:8px;padding:12px 16px;margin-bottom:16px;color:var(--ffsv3-text-color);font-family:Arial,sans-serif;box-shadow:0 2px 5px #0000000d}.ff-filter-box.no-borders{background-color:var(--default-bg-panel-color);border:none;border-radius:0;box-shadow:none;padding:0;margin-bottom:0}.ff-filter-box summary{cursor:pointer;font-size:14px;font-weight:700;outline:none;-webkit-user-select:none;user-select:none}.ff-filter-box[open] summary{border-bottom:1px solid var(--ffsv3-border-color);padding-bottom:6px;margin-bottom:12px}.ff-filter-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}.grp-sort{order:1}.grp-level{order:2}.grp-activity{order:3}.grp-status{order:4}.grp-ff{order:5}.grp-stats{order:6}@media(min-width:784px){.ff-filter-grid{grid-template-columns:repeat(3,1fr)}.ff-filter-grid>*{order:0}}.ff-filter-group{display:flex;flex-direction:column;gap:2px}.ff-filter-options{display:flex;flex-direction:column}.ff-filter-options label{display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer}.ff-filter-range-inputs{display:flex;align-items:center;gap:4px}.ff-filter-range-inputs input{flex:1;width:0;min-width:30px;max-width:80px;padding:4px;border:1px solid var(--ffsv3-border-color);border-radius:4px;background:var(--ffsv3-alt-bg-color);color:var(--ffsv3-text-color);font-size:11px;text-align:center}.ff-filter-box button{padding:6px 10px;border:1px solid var(--ffsv3-border-color);border-radius:4px;background:var(--ffsv3-alt-bg-color);color:var(--ffsv3-text-color);font-size:12px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:background-color .2s}.ff-filter-box button:hover{background-color:var(--ffsv3-hover-color)}";
  importCSS(stylesCss);
  const INJECTION_KEY = "__FF_SCOUTER_V3_INJECTED__";
  async function main() {
    const w = window;
    if (w[INJECTION_KEY]) {
      logger.info("Script already injected");
      return;
    }
    w[INJECTION_KEY] = true;
    logger.info("Initializing", "3.0-alpha2");
    unsafeWindow.ffscouter = ffscouter;
    window.ffscouter = ffscouter;
    setHttpInterceptor({

before(url, init) {
        for (const feat of Features) {
          if (feat.httpIntercept?.before) {
            feat.httpIntercept.before(url, init);
          }
        }
        return void 0;
      },
      after(bodyText, response, ctx) {
        for (const feat of Features) {
          if (feat.httpIntercept?.after) {
            feat.httpIntercept.after(bodyText, response, ctx);
          }
        }
        return void 0;
      }
    });
    for (const feat of Features) {
      if (feat.executionTime === StartTime.DocumentStart && await feat.shouldRun())
        feat.run();
    }
    await wait_for_body(1e4);
    for (const feat of Features) {
      if (feat.executionTime === StartTime.DocumentBody && await feat.shouldRun())
        feat.run();
    }
  }
  main();

})();