Shows the expected Fair Fight score against targets and faction war status
// ==UserScript==
// @name FF Scouter V2
// @namespace Violentmonkey Scripts
// @version 3.0.1
// @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));};
importCSS(" ._ffscouter-info-line__label_xi5zk_1{font-weight:700;margin-right:6px}._ffscouter-info-line__badge_xi5zk_8{font-weight:700;padding:2px 6px;border-radius:4px;display:inline-block}._ffscouter-info-line__premium-upgrade_xi5zk_15{display:block;margin-top:4px;line-height:1.3;white-space:nowrap;font-size:12px;font-style:normal}@media(max-width:768px){._ffscouter-info-line__premium-upgrade_xi5zk_15{margin-top:6px;line-height:1.35;white-space:normal;overflow-wrap:anywhere}}._ff-filter-box_ursux_1,._ff-filter-box_ursux_1 *,._ff-filter-box_ursux_1 *:before,._ff-filter-box_ursux_1 *:after{box-sizing:border-box!important}._ff-filter-box_ursux_1{background-color:var(--ffscouter-bg-color);border:1px solid var(--ffscouter-border-color);border-radius:8px;padding:12px 16px;margin-bottom:16px;color:var(--ffscouter-text-color);font-family:Arial,sans-serif;box-shadow:0 2px 5px #0000000d}._ff-filter-box_ursux_1._ff-filter-box--no-borders_ursux_19{background-color:var(--default-bg-panel-color);border-top:1px solid var(--ffscouter-border-color);border-bottom:1px solid var(--ffscouter-border-color);border-left:none;border-right:none;border-radius:0;box-shadow:none;padding:12px 10px;margin:0}._ff-filter-box_ursux_1 summary{cursor:pointer;font-size:14px;font-weight:700;outline:none;-webkit-user-select:none;user-select:none}._ff-filter-box_ursux_1[open] summary{border-bottom:1px solid var(--ffscouter-border-color);padding-bottom:6px;margin-bottom:12px}._ff-filter-box_ursux_1 summary:focus-visible{outline:2px solid var(--ffscouter-glow-color);outline-offset:2px}._ff-filter-box__header_ursux_52{display:inline-flex;justify-content:space-between;align-items:center;width:calc(100% - 24px);vertical-align:middle}._ff-filter-box__header-actions_ursux_60{display:flex;gap:6px;align-items:center}._ff-filter-box_ursux_1 ._ff-filter-box__action-btn_ursux_66{background:var(--ffscouter-alt-bg-color);border:1px solid var(--ffscouter-border-color);border-radius:4px;color:var(--ffscouter-text-color);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;transition:background-color .2s,color .2s,opacity .2s}._ff-filter-box_ursux_1 ._ff-filter-box__action-btn_ursux_66:hover{background-color:var(--ffscouter-hover-color)}._ff-filter-box_ursux_1 ._ff-filter-box__action-btn_ursux_66._ff-filter-box__action-btn--active_ursux_88{color:var(--ffscouter-text-color);opacity:1}._ff-filter-box_ursux_1 ._ff-filter-box__action-btn_ursux_66._ff-filter-box__action-btn--inactive_ursux_93{color:var(--ffscouter-text-color);opacity:.4}._ff-filter-box_ursux_1 ._ff-filter-box__action-btn_ursux_66 svg{width:14px;height:14px;fill:currentColor}._ff-filter-box_ursux_1 ._ff-filter-box__action-btn_ursux_66._ff-filter-box__action-btn--reset_ursux_104 svg{transition:transform .25s ease-in-out}._ff-filter-box_ursux_1 ._ff-filter-box__action-btn_ursux_66._ff-filter-box__action-btn--reset_ursux_104:hover svg{transform:rotate(-180deg)}._ff-filter-box__grid_ursux_114{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-top:12px}._ff-filter-box__group--sort_ursux_121{order:1}._ff-filter-box__group--level_ursux_125{order:2}._ff-filter-box__group--activity_ursux_129{order:3}._ff-filter-box__group--status_ursux_133{order:4}._ff-filter-box__group--ff_ursux_137{order:5}._ff-filter-box__group--stats_ursux_141{order:6}._ff-filter-box__group--last-action_ursux_145{order:7}._ff-filter-box__group--columns_ursux_149{order:8}@media(min-width:784px){._ff-filter-box__grid_ursux_114{grid-template-columns:repeat(3,1fr)}._ff-filter-box__grid_ursux_114>*{order:0}}._ff-filter-box__group_ursux_121{display:flex;flex-direction:column;gap:2px}._ff-filter-box__sort-controls_ursux_171{display:flex;flex-direction:column;gap:8px}._ff-filter-box__sort-controls_ursux_171 ._ff-filter-box__sort-btn_ursux_177{width:100%}._ff-filter-box__sort-controls_ursux_171 ._ff-filter-box__compare-btn_ursux_181{width:100%;height:32px}._ff-filter-box__display-select_ursux_186{padding:4px;border:1px solid var(--ffscouter-border-color);border-radius:4px;background:var(--ffscouter-alt-bg-color);color:var(--ffscouter-text-color);font-size:11px;cursor:pointer;height:32px}._ff-filter-box__options_ursux_197{display:flex;flex-direction:column}._ff-filter-box__options_ursux_197 label{display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer}._ff-filter-box__range-inputs_ursux_210{display:flex;align-items:center;gap:4px}._ff-filter-box__range-inputs_ursux_210 input{flex:1;width:0;min-width:30px;max-width:80px;padding:4px;border:1px solid var(--ffscouter-border-color);border-radius:4px;background:var(--ffscouter-alt-bg-color);color:var(--ffscouter-text-color);font-size:11px;text-align:center}._ff-filter-box_ursux_1 button{padding:6px 10px;border:1px solid var(--ffscouter-border-color);border-radius:4px;background:var(--ffscouter-alt-bg-color);color:var(--ffscouter-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_ursux_1 button:hover{background-color:var(--ffscouter-hover-color)}._ff-settings-panel__accordion_6bhvd_1{margin:10px 0;padding:15px;background-color:var(--ffscouter-bg-color);border:1px solid var(--ffscouter-border-color);border-radius:5px;color:var(--ffscouter-text-color)}._ff-settings-panel__accordion_6bhvd_1._ff-settings-panel__accordion--glow_6bhvd_10{border-color:var(--ffscouter-glow-color);box-shadow:0 0 8px #4caf5080}._ff-settings-panel__accordion_6bhvd_1 summary{cursor:pointer;font-weight:700}._ff-settings-panel__body_6bhvd_20{margin-top:15px}._ff-settings-panel__input-row_6bhvd_24{display:flex;flex-direction:column;gap:5px;margin-bottom:15px}._ff-settings-panel__range-row_6bhvd_32{display:flex;gap:10px;align-items:center}._ff-settings-panel__blur_6bhvd_38{filter:blur(4px);transition:filter .2s ease}._ff-settings-panel__blur_6bhvd_38:hover,._ff-settings-panel__blur_6bhvd_38:focus{filter:blur(0)}._ff-settings-panel__error-msg_6bhvd_48{color:#f33;font-size:13px;margin-top:5px}._ff-settings-panel__accordion_6bhvd_1 input[type=text],._ff-settings-panel__accordion_6bhvd_1 input[type=number]{box-sizing:border-box!important;text-align:left;vertical-align:top;width:178px;height:34px!important;margin-right:8px;padding:9px 10px;line-height:14px;display:inline-block}._ff-settings-panel__accordion_6bhvd_1 input[type=number]._ff-settings-panel__number_6bhvd_67{width:80px}._ff-settings-panel__accordion_6bhvd_1 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, 1px solid var(--ffscouter-border-color));border-radius:5px;font-family:Arial,serif;color:var(--input-color, var(--ffscouter-text-color));background:var(--input-background-color, var(--ffscouter-alt-bg-color))}.dark-mode ._ff-settings-panel__accordion_6bhvd_1 select option{background-color:#000;color:var(--input-color)}._ff-settings-panel__api-explanation_6bhvd_94{color:var(--ffscouter-text-color);margin-bottom:20px;font-size:13px;line-height:1.5}._ff-settings-panel__accordion_6bhvd_1 a{color:var(--ffscouter-success-color);text-decoration:underline}._ff-settings-panel__premium-badge_6bhvd_107{display:inline-block;color:#fff;font-size:11px;font-weight:700;padding:2px 8px;border-radius:4px;vertical-align:middle}._ff-settings-panel__premium-badge--enabled_6bhvd_117{background:#4caf50}._ff-settings-panel__premium-badge--disabled_6bhvd_121{background:#c62828}._ff-settings-panel__premium-badge--unknown_6bhvd_125{background:#f39c12}._ff-settings-panel__section_6bhvd_138{display:grid;grid-template-columns:1fr;gap:12px;margin-bottom:15px}@media(min-width:784px){._ff-settings-panel__section_6bhvd_138{grid-template-columns:repeat(3,1fr)}}._ff-settings-panel__span_6bhvd_152{grid-column:1 / -1;margin-bottom:0}._ff-settings-panel__cell_6bhvd_160{display:flex;flex-direction:column;gap:5px;min-width:0;margin-bottom:0}._ff-settings-panel__cell_6bhvd_160._ff-settings-panel__cell--checkbox_6bhvd_169{flex-direction:row;align-items:flex-start;gap:10px}._ff-settings-panel__cell_6bhvd_160 input[type=text]{width:100%;margin-right:0}._ff-settings-panel__cell_6bhvd_160 select{width:auto;max-width:100%;margin-right:0}._ff-settings-panel__api-block_6bhvd_193{display:flex;flex-direction:column;gap:10px}._ff-settings-panel__api-block_6bhvd_193 ._ff-settings-panel__cell_6bhvd_160 input[type=text]{max-width:360px}._ff-settings-panel__api-status-row_6bhvd_203{display:flex;flex-wrap:wrap;align-items:center;gap:10px}._ff-settings-panel__chain-suboptions_6bhvd_213{border-left:2px solid var(--ffscouter-border-color);padding-left:8px;margin-top:10px;grid-template-columns:repeat(2,1fr)}._ff-settings-panel__chain-suboptions_6bhvd_213 ._ff-settings-panel__chain-wide_6bhvd_218{grid-column:1 / -1}@media(min-width:784px){._ff-settings-panel__chain-suboptions_6bhvd_213{padding-left:16px;grid-template-columns:repeat(3,1fr)}._ff-settings-panel__chain-suboptions_6bhvd_213 ._ff-settings-panel__chain-wide_6bhvd_218{grid-column:auto}}._ff-settings-panel__group_6bhvd_242{background-color:var(--ffscouter-alt-bg-color);border:1px solid var(--ffscouter-border-color);border-radius:5px;padding:12px;margin-bottom:15px}._ff-settings-panel__group_6bhvd_242 h4{margin:0 0 12px}._ff-settings-panel__marker-size_6bhvd_256,._ff-settings-panel__marker-border-width_6bhvd_257{display:flex;flex-direction:column;gap:5px}._ff-settings-panel__marker-size-controls_6bhvd_263{display:flex;flex-wrap:wrap;align-items:center;gap:10px}._ff-settings-panel__marker-size-controls_6bhvd_263 input[type=range]{flex:1 1 120px;min-width:120px}._ff-settings-panel__color-scheme_6bhvd_276{display:flex;flex-direction:column;gap:5px}._ff-settings-panel__color-scheme-controls_6bhvd_282{display:flex;flex-wrap:wrap;align-items:center;gap:10px}._ff-settings-panel__accordion_6bhvd_1 .ffscouter-swatch-row{flex-wrap:wrap}._ff-settings-panel__actions_6bhvd_295{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:10px;margin-top:20px}._ff-settings-panel__saved-msg_6bhvd_304{color:#4caf50} ");
var StartTime = ((StartTime2) => {
StartTime2[StartTime2["DocumentStart"] = 0] = "DocumentStart";
StartTime2[StartTime2["DocumentBody"] = 1] = "DocumentBody";
StartTime2[StartTime2["DocumentEnd"] = 2] = "DocumentEnd";
return StartTime2;
})(StartTime || {});
let _react$1;
function getReact$1() {
return _react$1 ??= unsafeWindow.React;
}
const FRAGMENT_SENTINEL = Symbol("ReactFragment");
const Fragment = FRAGMENT_SENTINEL;
function jsx(type, { children, ...props }, key) {
const R = getReact$1();
const realType = type === FRAGMENT_SENTINEL ? R.Fragment : type;
if (key !== void 0) props.key = key;
if (children === void 0) {
return R.createElement(realType, props);
}
return Array.isArray(children) ? R.createElement(realType, props, ...children) : R.createElement(realType, props, children);
}
const jsxs = jsx;
function isInPDA() {
return typeof window !== "undefined" && typeof window.PDA_httpGet === "function";
}
var LogLevel = ((LogLevel2) => {
LogLevel2[LogLevel2["DEBUG"] = 0] = "DEBUG";
LogLevel2[LogLevel2["INFO"] = 1] = "INFO";
LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
LogLevel2[LogLevel2["ERROR"] = 3] = "ERROR";
LogLevel2[LogLevel2["NONE"] = 4] = "NONE";
return LogLevel2;
})(LogLevel || {});
class Logger {
constructor(prefix = "", defaultLevel = 1, state = {}) {
this.isPDA = false;
this.colors = {
debug: "#7f8c8d",
info: "#3498db",
warn: "#f39c12",
error: "#e74c3c"
};
this.prefix = prefix;
this.defaultLevel = defaultLevel;
this.state = state;
this.detectPDA();
}
detectPDA() {
this.isPDA = isInPDA();
}
setLevel(level) {
this.state.explicitLevel = level;
}
getLevel() {
return this.state.explicitLevel !== void 0 ? this.state.explicitLevel : this.defaultLevel;
}
debug(...args) {
if (this.getLevel() <= 0) {
if (this.isPDA) {
console.log(`${this.formatPrefix("DEBUG")}`, ...this.formatArgs(args));
} else {
console.log(
`%c${this.formatPrefix("DEBUG")}`,
`color: ${this.colors.debug}; font-weight: bold`,
...args
);
}
}
}
info(...args) {
if (this.getLevel() <= 1) {
if (this.isPDA) {
console.info(`${this.formatPrefix("INFO")}`, ...this.formatArgs(args));
} else {
console.info(
`%c${this.formatPrefix("INFO")}`,
`color: ${this.colors.info}; font-weight: bold`,
...args
);
}
}
}
warn(...args) {
if (this.getLevel() <= 2) {
if (this.isPDA) {
console.warn(`${this.formatPrefix("WARN")}`, ...this.formatArgs(args));
} else {
console.warn(
`%c${this.formatPrefix("WARN")}`,
`color: ${this.colors.warn}; font-weight: bold`,
...args
);
}
}
}
error(...args) {
if (this.getLevel() <= 3) {
if (this.isPDA) {
console.error(
`${this.formatPrefix("ERROR")}`,
...this.formatArgs(args)
);
} else {
console.error(
`%c${this.formatPrefix("ERROR")}`,
`color: ${this.colors.error}; font-weight: bold`,
...args
);
}
}
}
group(label, collapsed = false) {
if (this.getLevel() < 4) {
if (collapsed) {
console.groupCollapsed(this.formatPrefix(""), label);
} else {
console.group(this.formatPrefix(""), label);
}
}
}
groupEnd() {
if (this.getLevel() < 4) {
console.groupEnd();
}
}
child(subPrefix) {
const childPrefix = this.prefix ? `${this.prefix}:${subPrefix}` : subPrefix;
return new Logger(childPrefix, this.defaultLevel, this.state);
}
formatPrefix(level) {
const prefix = this.prefix ? `[${this.prefix}]` : "";
return level ? `${prefix} - [${level}]: ` : `${prefix}: `;
}
formatArgs(args) {
return args.map((arg) => {
if (isDOMNode(arg)) {
return formatDOMNode(arg);
}
if (typeof arg === "object" && arg !== null) {
try {
const seen = new WeakSet();
return JSON.stringify(
arg,
(_key, val) => {
if (typeof val === "object" && val !== null) {
if (seen.has(val)) {
return "[Circular]";
}
seen.add(val);
}
if (isDOMNode(val)) {
return formatDOMNode(val);
}
if (val instanceof Error) {
return {
message: val.message,
stack: val.stack,
name: val.name
};
}
return val;
},
2
);
} catch {
return String(arg);
}
}
return arg;
});
}
}
function isDOMNode(val) {
return typeof val === "object" && val !== null && "nodeType" in val && typeof val.nodeType === "number" && "nodeName" in val && typeof val.nodeName === "string";
}
function formatDOMNode(node) {
if (node.nodeType === 1) {
const tagName = node.tagName.toLowerCase();
const id = node.id ? `#${node.id}` : "";
const classes = node.className && typeof node.className === "string" && node.className.trim() ? `.${node.className.trim().split(/\s+/).join(".")}` : "";
return `<${tagName}${id}${classes}>`;
}
return `[Node: ${node.nodeName}]`;
}
const logger = new Logger(
"FFSV2",
0
);
const log$i = logger.child("storage");
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) {
log$i.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) {
log$i.warn(`Key '${key}' has invalid JSON in it.`);
this.remove(key);
return null;
}
if (item.expiration && Date.now() > item.expiration) {
this.remove(key);
log$i.debug(`Key ${key} has expired.`);
return null;
}
return item.value;
} catch (error) {
log$i.error(`Error retrieving item '${key}':`, error);
return null;
}
}
remove(key) {
try {
localStorage.removeItem(this.prefix + key);
} catch (error) {
log$i.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) {
log$i.error("Error clearing storage:", error);
}
}
}
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(e) {
switch (typeof e) {
case "string":
return e;
case "object": {
if (e instanceof Error)
return e.message;
return JSON.stringify(e);
}
default:
return e.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}`;
}
}
var FactionsColDisplay = ((FactionsColDisplay2) => {
FactionsColDisplay2["FAIR_FIGHT"] = "fair_fight";
FactionsColDisplay2["BATTLE_STATS"] = "battle_stats";
FactionsColDisplay2["NONE"] = "none";
return FactionsColDisplay2;
})(FactionsColDisplay || {});
var GaugeMarkerType = ((GaugeMarkerType2) => {
GaugeMarkerType2["ARROW"] = "arrow";
GaugeMarkerType2["BUBBLE_FF"] = "bubble_ff";
GaugeMarkerType2["BUBBLE_ESTIMATE"] = "bubble_estimate";
return GaugeMarkerType2;
})(GaugeMarkerType || {});
var ColorScheme = ((ColorScheme2) => {
ColorScheme2["CLASSIC"] = "classic";
ColorScheme2["COOL_DIVERGING"] = "cool_diverging";
ColorScheme2["NEON"] = "neon";
ColorScheme2["COLORBLIND_SAFE"] = "colorblind_safe";
ColorScheme2["GRAYSCALE"] = "grayscale";
ColorScheme2["GREEN_YELLOW_RED"] = "green_yellow_red";
ColorScheme2["BLUE_YELLOW_RED"] = "blue_yellow_red";
ColorScheme2["PLASMA"] = "plasma";
ColorScheme2["CUSTOM"] = "custom";
return ColorScheme2;
})(ColorScheme || {});
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",
war_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,
gauge_marker_type: "arrow",
gauge_marker_scale: 100,
gauge_marker_border_width: 1,
war_quick_attack_action: "new_tab",
network_interception_enabled: false,
status_attack_links_enabled: true,
debug_disable_pda_http: false,
color_scheme: "classic",
custom_colors: null
};
class FFConfig {
constructor(name) {
this.name = name;
this.storage = new Storage(this.name);
logger.setLevel(this.debug_logs ? LogLevel.DEBUG : LogLevel.INFO);
}
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 war_quick_attack_action() {
return this.storage.get(
"war_quick_attack_action"
) ?? CONFIG_DEFAULTS.war_quick_attack_action;
}
set war_quick_attack_action(val) {
this.storage.set("war_quick_attack_action", 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 war_col_display() {
return this.storage.get(
"war_col_display"
) ?? CONFIG_DEFAULTS.war_col_display;
}
set war_col_display(val) {
this.storage.set("war_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);
logger.setLevel(val ? LogLevel.DEBUG : LogLevel.INFO);
}
get analytics_enabled() {
return this.storage.get(
"analytics_enabled"
) ?? CONFIG_DEFAULTS.analytics_enabled;
}
set analytics_enabled(val) {
this.storage.set("analytics_enabled", val);
}
get network_interception_enabled() {
return this.storage.get(
"network_interception_enabled"
) ?? CONFIG_DEFAULTS.network_interception_enabled;
}
set network_interception_enabled(val) {
this.storage.set("network_interception_enabled", val);
}
get status_attack_links_enabled() {
return this.storage.get(
"status_attack_links_enabled"
) ?? CONFIG_DEFAULTS.status_attack_links_enabled;
}
set status_attack_links_enabled(val) {
this.storage.set("status_attack_links_enabled", val);
}
get debug_disable_pda_http() {
return this.storage.get(
"debug_disable_pda_http"
) ?? CONFIG_DEFAULTS.debug_disable_pda_http;
}
set debug_disable_pda_http(val) {
this.storage.set("debug_disable_pda_http", val);
}
get gauge_marker_type() {
return this.storage.get(
"gauge_marker_type"
) ?? CONFIG_DEFAULTS.gauge_marker_type;
}
set gauge_marker_type(val) {
this.storage.set("gauge_marker_type", val);
}
get gauge_marker_scale() {
return this.storage.get(
"gauge_marker_scale"
) ?? CONFIG_DEFAULTS.gauge_marker_scale;
}
set gauge_marker_scale(val) {
this.storage.set("gauge_marker_scale", val);
}
get gauge_marker_border_width() {
return this.storage.get(
"gauge_marker_border_width"
) ?? CONFIG_DEFAULTS.gauge_marker_border_width;
}
set gauge_marker_border_width(val) {
this.storage.set("gauge_marker_border_width", val);
}
get color_scheme() {
return this.storage.get(
"color_scheme"
) ?? CONFIG_DEFAULTS.color_scheme;
}
set color_scheme(val) {
this.storage.set("color_scheme", val);
}
get custom_colors() {
return this.storage.get(
"custom_colors"
) ?? CONFIG_DEFAULTS.custom_colors;
}
set custom_colors(val) {
if (val === null) {
this.storage.remove(
"custom_colors"
);
} else {
this.storage.set("custom_colors", 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(
"war_col_display"
);
this.storage.remove(
"debug_logs"
);
this.storage.remove(
"analytics_enabled"
);
this.storage.remove(
"network_interception_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"
);
this.storage.remove(
"gauge_marker_type"
);
this.storage.remove(
"gauge_marker_scale"
);
this.storage.remove(
"gauge_marker_border_width"
);
this.storage.remove(
"war_quick_attack_action"
);
this.storage.remove(
"status_attack_links_enabled"
);
this.storage.remove(
"debug_disable_pda_http"
);
this.storage.remove(
"color_scheme"
);
this.storage.remove(
"custom_colors"
);
}
}
const ffconfig = new FFConfig("ffsv3-config");
const FF_SCOUTER_BASE_URL = "https://ffscouter.com/api/v1";
new TornApiClient({
defaultComment: `FFScouterV2-${"3.0.1"}`,
defaultTimeout: 30
});
async function gmRequest(options) {
if (isInPDA() && !ffconfig.debug_disable_pda_http) {
const url = options.url;
const headers = options.headers ?? {};
const method = (options.method ?? "GET").toUpperCase();
const pdaResp = method === "POST" ? await window.PDA_httpPost(url, headers, options.data) : await window.PDA_httpGet(url, headers);
return pdaResp;
}
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 {
logger.warn(
`query_stats: unparseable response. status=${resp.status}, body=${resp.responseText?.substring(0, 200)}, url=${url}`
);
throw new FFApiError(
`API request failed. Couldn't parse response. HTTP status code: ${resp.status}`,
{ ff_api_limits: limits }
);
}
if (ff_response == null) {
logger.warn(
`query_stats: null response after parse. status=${resp.status}, url=${url}`
);
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) {
logger.warn(
`query_stats: unexpected HTTP status. status=${resp.status}, body=${resp.responseText?.substring(0, 200)}, url=${url}`
);
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) => {
if (typeof responseHeaders !== "string") {
return void 0;
}
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) => {
if (!key) {
return { blank: true };
}
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 {
logger.warn(
`check_key: unparseable response. status=${resp.status}, body=${resp.responseText?.substring(0, 200)}, url=${url}`
);
throw new FFApiError(
`API request failed. Couldn't parse response. HTTP status code: ${resp.status}`,
{ ff_api_limits: limits }
);
}
if (ff_response == null) {
logger.warn(
`check_key: null response after parse. status=${resp.status}, url=${url}`
);
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) {
logger.warn(
`check_key: unexpected HTTP status. status=${resp.status}, body=${resp.responseText?.substring(0, 200)}, url=${url}`
);
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 {
logger.warn(
`query_flights: unparseable response. status=${resp.status}, body=${resp.responseText?.substring(0, 200)}, url=${url}`
);
throw new FFApiError(
`API request failed. Couldn't parse response. HTTP status code: ${resp.status}`,
{ ff_api_limits: limits }
);
}
if (ff_response == null) {
logger.warn(
`query_flights: null response after parse. status=${resp.status}, url=${url}`
);
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) {
logger.warn(
`query_flights: unexpected HTTP status. status=${resp.status}, body=${resp.responseText?.substring(0, 200)}, url=${url}`
);
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 log$h = logger.child("api");
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) {
log$h.error(
"Received error response querying ffscouter check-key api:",
err
);
throw err;
}
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) => {
try {
const status = await this.check_key_status(force);
if (!status) return null;
return status.is_premium;
} catch (err) {
log$h.warn("Failed to check premium status:", err);
return null;
}
};
this.clear = () => {
this.storage.remove(CHECK_KEY);
};
this.config = config;
this.storage = storage;
}
}
const check_key_status = new CheckKeyStatus(
ffconfig,
new Storage("ffsv3-check")
);
const instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c);
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 log$g = logger.child("storage");
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.last_clean = 0;
this.active_operations = 0;
this.close_timer = null;
this.open_promise = null;
this.channel = null;
this.state = "CLOSED";
this.deletion_promise = null;
this.resolve_deletion = null;
this.migrations = new Map([
[
1,
(db, _) => {
const store = db.createObjectStore(STORES.CACHE, {
keyPath: "player_id"
});
store.createIndex("expiry", "expiry", {
unique: false
});
}
],
[
2,
(db, _) => {
const store = db.createObjectStore(STORES.FLIGHTS, {
keyPath: "player_id"
});
store.createIndex("expiry", "expiry", {
unique: false
});
}
],
[
3,
(db, _) => {
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;
}
if (this.open_promise) {
return this.open_promise;
}
if (typeof BroadcastChannel !== "undefined" && !this.channel) {
this.channel = new BroadcastChannel(`ffcache-channel-${this.db_name}`);
this.channel.onmessage = (event) => {
this.handle_broadcast(event.data);
};
if (typeof this.channel.unref === "function") {
this.channel.unref();
}
}
const cache = this;
this.open_promise = (async () => {
try {
const db = await openDB(this.db_name, this.db_version, {
upgrade(db2, oldVersion, newVersion, transaction, _event) {
log$g.info("Need to upgrade from", oldVersion, "to", newVersion);
for (let i = (oldVersion ?? 0) + 1; i <= cache.db_version; i++) {
log$g.debug(`Migration: ${i}`);
const m2 = cache.migrations.get(i);
if (m2) {
m2(db2, transaction);
} else {
log$g.debug(`Migration not found: ${i}`);
}
log$g.debug(`Migration complete: ${i}`);
}
},
blocking(currentVersion, blockedVersion, event) {
log$g.debug(
`Can't open ${blockedVersion} because ${currentVersion} is open. Closing.`
);
cache.close();
if (event?.target && typeof event.target.close === "function") {
event.target.close();
}
}
});
cache.db = db;
cache.state = "OPEN";
return db;
} finally {
cache.open_promise = null;
}
})();
return this.open_promise;
};
this.close = () => {
if (this.db) {
this.db.close();
this.db = null;
}
this.state = "CLOSED";
};
this.start_op = async () => {
if (this.state === "DELETING_LOCAL" || this.state === "DELETING_REMOTE") {
await this.wait_for_deletion_complete();
}
this.active_operations++;
if (this.close_timer) {
clearTimeout(this.close_timer);
this.close_timer = null;
}
return await this.open();
};
this.end_op = () => {
this.active_operations = Math.max(0, this.active_operations - 1);
if (this.active_operations === 0) {
if (this.close_timer) {
clearTimeout(this.close_timer);
}
this.close_timer = setTimeout(() => {
this.close();
this.close_timer = null;
}, 1e3);
}
};
this.delete_db = async () => {
if (this.close_timer) {
clearTimeout(this.close_timer);
this.close_timer = null;
}
this.state = "DELETING_LOCAL";
this.channel?.postMessage({ type: "deleting" });
await this.wait_for_active_ops();
this.close();
try {
await deleteDB(this.db_name, {
blocked: () => {
log$g.debug("deleteDB blocked callback called!");
}
});
log$g.info(`Successfully deleted ${this.db_name} IndexedDB.`);
} finally {
this.channel?.postMessage({ type: "deleted" });
this.state = "CLOSED";
if (this.resolve_deletion) {
this.resolve_deletion();
this.resolve_deletion = null;
this.deletion_promise = null;
}
if (this.channel) {
this.channel.close();
this.channel = null;
}
}
};
this.handle_broadcast = (data) => {
if (data && typeof data === "object") {
if (data.type === "deleting") {
this.state = "DELETING_REMOTE";
if (!this.deletion_promise) {
this.deletion_promise = new Promise((resolve) => {
this.resolve_deletion = resolve;
});
}
this.close();
} else if (data.type === "deleted") {
this.state = "CLOSED";
if (this.resolve_deletion) {
this.resolve_deletion();
this.resolve_deletion = null;
this.deletion_promise = null;
}
}
}
};
this.wait_for_deletion_complete = async () => {
while (this.state === "DELETING_LOCAL" || this.state === "DELETING_REMOTE") {
if (this.deletion_promise) {
await this.deletion_promise;
} else {
await new Promise((resolve) => setTimeout(resolve, 10));
}
}
};
this.wait_for_active_ops = async () => {
if (this.open_promise) {
try {
await this.open_promise;
} catch {
}
}
while (this.active_operations > 0) {
await new Promise((resolve) => setTimeout(resolve, 10));
}
};
this.get = async (player_ids) => {
const db = await this.start_op();
try {
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);
});
return result;
} finally {
this.end_op();
}
};
this.update = async (values) => {
const db = await this.start_op();
try {
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;
} finally {
this.end_op();
}
};
this.clean_expired = (force = false) => {
const now = Date.now();
if (!force && now - this.last_clean < 60 * 60 * 1e3) {
return Promise.resolve();
}
this.last_clean = now;
const runClean = async () => {
const db = await this.start_op();
try {
{
const tx = db.transaction(STORES.CACHE, "readwrite");
const index2 = tx.store.index("expiry");
const range = IDBKeyRange.upperBound(Date.now());
const r = await index2.getAllKeys(range);
log$g.info(`Found ${r.length} expired values to delete from cache.`);
await Promise.all(r.map((id) => tx.store.delete(id)));
await tx.done;
}
{
const tx = db.transaction(STORES.FLIGHTS, "readwrite");
const index2 = tx.store.index("expiry");
const range = IDBKeyRange.upperBound(Date.now());
const r = await index2.getAllKeys(range);
log$g.info(`Found ${r.length} expired values to delete from flights.`);
await Promise.all(r.map((id) => tx.store.delete(id)));
await tx.done;
}
{
const tx = db.transaction(STORES.ANALYTICS, "readwrite");
const index2 = tx.store.index("timestamp");
const thirty_days_ago = Date.now() - 30 * 24 * 60 * 60 * 1e3;
const range = IDBKeyRange.upperBound(thirty_days_ago);
const r = await index2.getAllKeys(range);
log$g.info(
`Found ${r.length} expired values to delete from analytics.`
);
await Promise.all(r.map((id) => tx.store.delete(id)));
await tx.done;
}
} finally {
this.end_op();
}
};
if (typeof window !== "undefined" && "requestIdleCallback" in window) {
return new Promise((resolve, reject) => {
window.requestIdleCallback(() => {
runClean().then(resolve, reject);
});
});
}
return runClean();
};
this.get_flight = async (player_id) => {
const db = await this.start_op();
try {
const tx = db.transaction(STORES.FLIGHTS, "readonly");
const entry = await tx.store.get(player_id);
await tx.done;
if (!entry || entry.expiry <= Date.now()) {
return null;
}
return entry;
} finally {
this.end_op();
}
};
this.update_flight = async (value, cache_interval = 60 * 1e3) => {
const db = await this.start_op();
try {
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;
} finally {
this.end_op();
}
};
this.delete_flight = async (player_id) => {
const db = await this.start_op();
try {
const tx = db.transaction(STORES.FLIGHTS, "readwrite");
await tx.store.delete(player_id);
await tx.done;
} finally {
this.end_op();
}
};
this.add_analytics = async (entry) => {
const db = await this.start_op();
try {
const tx = db.transaction(STORES.ANALYTICS, "readwrite");
const value = {
...entry,
timestamp: Date.now()
};
await tx.store.add(value);
await tx.done;
} finally {
this.end_op();
}
};
this.get_analytics = async () => {
const db = await this.start_op();
try {
const tx = db.transaction(STORES.ANALYTICS, "readonly");
const res = await tx.store.getAll();
await tx.done;
return res;
} finally {
this.end_op();
}
};
this.clear_analytics = async () => {
const db = await this.start_op();
try {
const tx = db.transaction(STORES.ANALYTICS, "readwrite");
await tx.store.clear();
await tx.done;
} finally {
this.end_op();
}
};
this.dump = async () => {
const db = await this.start_op();
try {
const tx = db.transaction(STORES.CACHE, "readonly");
const res = await tx.store.getAll();
await tx.done;
return res;
} finally {
this.end_op();
}
};
this.dump_flights = async () => {
const db = await this.start_op();
try {
const tx = db.transaction(STORES.FLIGHTS, "readonly");
const res = await tx.store.getAll();
await tx.done;
return res;
} finally {
this.end_op();
}
};
this.db_name = db_name;
if (typeof BroadcastChannel !== "undefined") {
this.channel = new BroadcastChannel(`ffcache-channel-${db_name}`);
this.channel.onmessage = (event) => {
this.handle_broadcast(event.data);
};
if (typeof this.channel.unref === "function") {
this.channel.unref();
}
}
}
}
const log$f = logger.child("api");
const DB_NAME = "FFSV3-cache";
const RECHECK_RETRY_DELAY = 60 * 1e3;
const RECHECK_WINDOW_DURATION = 3 * 60 * 1e3;
const FINALIZED_NO_FLIGHT_TTL = 30 * 60 * 1e3;
const FLIGHT_PACING_DELAY = 1e3;
const GLOBAL_BUDGET_RESERVE = 50;
function getParamFast(queryString, paramName) {
if (!queryString) return null;
const target = `${paramName}=`;
if (!queryString.includes(target)) return null;
let startIdx = 0;
if (queryString.charCodeAt(0) === 63) {
startIdx = 1;
}
let pos = queryString.indexOf(target, startIdx);
while (pos !== -1) {
if (pos === startIdx || queryString.charCodeAt(pos - 1) === 38 ||
queryString.charCodeAt(pos - 1) === 63) {
const valStart = pos + target.length;
let valEnd = queryString.indexOf("&", valStart);
if (valEnd === -1) {
valEnd = queryString.length;
}
const rawVal = queryString.substring(valStart, valEnd);
if (rawVal.indexOf("%") === -1 && rawVal.indexOf("+") === -1) {
return rawVal;
}
try {
return decodeURIComponent(rawVal.replace(/\+/g, " "));
} catch {
return rawVal;
}
}
pos = queryString.indexOf(target, pos + 1);
}
return null;
}
class FFScouter {
constructor(config, cache) {
this.cache = new FFCache(DB_NAME);
this.pending = new Map();
this.flight_queue = [];
this.flight_timer = null;
this.flight_recheck_until = new Map();
this.pending_flights = 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 p = this.pending.get(player_id);
if (p) {
return p.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 });
if (!this.config.key) {
this.resolve(player_id, { player_id, no_data: true });
return promise;
}
this.enqueue_cache(player_id);
return promise;
};
this.clear_flight_cache = async (player_id) => {
try {
await this.cache.delete_flight(player_id);
} catch (err) {
log$f.error("Failed to delete flight from cache", err);
}
};
this.calculate_flight_cache_ttl = (result) => {
if (result.current) {
const now = Date.now();
const latest_arrival_time_ms = result.current.latest_arrival_time * 1e3;
const time_remaining = latest_arrival_time_ms - now;
if (time_remaining > 0) {
const segment = Math.floor(time_remaining / 2);
const min_ttl = 60 * 1e3;
return Math.max(min_ttl, segment);
}
}
return FINALIZED_NO_FLIGHT_TTL;
};
this.enqueue_flight_api = (player_id, recheck_until) => {
let resolve;
let reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
if (recheck_until !== void 0) {
this.flight_recheck_until.set(player_id, recheck_until);
}
const pending = this.pending_flights.get(player_id);
if (pending) {
pending.push({ resolve, reject });
return promise;
}
this.pending_flights.set(player_id, [{ resolve, reject }]);
this.flight_queue.push(player_id);
this.schedule_flight_processor();
return promise;
};
this.schedule_flight_processor = (delay = 0) => {
if (this.flight_timer) {
return;
}
this.flight_timer = this.schedule(this.process_flight_queue, delay);
};
this.process_flight_queue = async () => {
this.flight_timer = null;
if (this.flight_queue.length === 0) {
return;
}
if (this.last_limits && this.last_limits.reset_time > new Date() && this.last_limits.remaining <= GLOBAL_BUDGET_RESERVE) {
log$f.warn(
`Total API quota <= ${GLOBAL_BUDGET_RESERVE}. Deferring flight status checks to prioritize stats.`
);
this.schedule_flight_processor(5e3);
return;
}
const player_id = this.flight_queue.shift();
if (player_id === void 0) {
return;
}
const pending = this.pending_flights.get(player_id);
if (!pending) {
this.schedule_flight_processor(0);
return;
}
log$f.debug(`Querying paced flight API for player ${player_id}`);
try {
const response = await query_flights(this.config.key, player_id);
if (response.blank) {
throw new Error(
`Empty flight response returned for player ${player_id}`
);
}
if (response.limits) {
this.last_limits = response.limits;
}
let finalResult = response.result;
if (response.result.current) {
try {
const ttl = this.calculate_flight_cache_ttl(response.result);
await this.cache.update_flight(response.result, ttl);
} catch (err) {
log$f.error("Failed to update flight cache", err);
}
} else {
log$f.debug(`Start rechecking cycle for player ${player_id}`);
const now = Date.now();
const next_retry_at = now + RECHECK_RETRY_DELAY;
const existing_recheck_until = this.flight_recheck_until.get(player_id);
const recheck_until = existing_recheck_until ?? now + RECHECK_WINDOW_DURATION;
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 {
const remaining_ttl = Math.max(0, recheck_until - now);
await this.cache.update_flight(rechecking_response, remaining_ttl);
} catch (err) {
log$f.error("Failed to update flight cache during recheck", err);
}
finalResult = rechecking_response;
}
for (const job of pending) {
job.resolve(finalResult);
}
} catch (err) {
log$f.error(`Paced flight API query failed for ${player_id}:`, err);
const apiErr = err;
if (apiErr?.ff_api_limits) {
this.last_limits = apiErr.ff_api_limits;
}
for (const job of pending) {
job.reject(err);
}
} finally {
this.pending_flights.delete(player_id);
this.flight_recheck_until.delete(player_id);
try {
await this.cache.clean_expired();
} catch (err) {
log$f.error("Failed to clean expired cache entries", err);
}
if (this.flight_queue.length > 0) {
this.schedule_flight_processor(FLIGHT_PACING_DELAY);
}
}
};
this.get_flights = async (player_id) => {
log$f.debug(`get_flights called for ${player_id}`);
if (!this.config.key) {
return {
player_id,
current: null,
recent_flights: []
};
}
let cached = null;
try {
cached = await this.cache.get_flight(player_id);
} catch (err) {
log$f.error("Failed to query flight cache", err);
}
if (cached) {
log$f.debug(`Flight cache hit for player ${player_id}`);
if (cached.rechecking) {
const now = Date.now();
if (cached.recheck_until && now >= cached.recheck_until) {
log$f.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,
FINALIZED_NO_FLIGHT_TTL
);
} catch (err) {
log$f.error("Failed to finalize flight cache", err);
}
return final_response;
}
if (cached.next_retry_at && now >= cached.next_retry_at) {
log$f.debug(
`Retrying API call for player ${player_id} during recheck window`
);
const result2 = await this.enqueue_flight_api(
player_id,
cached.recheck_until
);
return result2;
}
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
};
}
log$f.debug(`Flight cache miss for player ${player_id}. Querying API paced.`);
const result = await this.enqueue_flight_api(player_id);
return result;
};
this.complete = () => {
this.process_cache();
};
this.enqueue_cache = (player_id) => {
log$f.debug(`Enqueuing cache ${player_id}`);
this.cache_queue.add(player_id);
this.schedule_cache();
};
this.schedule_cache = () => {
if (this.cache_timer) {
log$f.debug(`schedule_cache called but job already scheduled`);
return;
}
log$f.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 (_) {
results = new Map();
}
log$f.debug(`Received ${results.size} cache results`);
for (const id of ids) {
const v = results.get(id);
if (v) {
log$f.debug("Id", id, "found in cache. Resolving value.");
this.resolve(id, v);
} else {
log$f.debug("Id", id, "not found in cache. Scheduling api call.");
this.enqueue_api(id);
}
}
};
this.clear_cache = () => {
this.cache.delete_db();
check_key_status.clear();
};
this.enqueue_api = (player_id) => {
log$f.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) {
log$f.debug(`schedule_api called but job already scheduled`);
return;
}
log$f.debug(`schedule_api called and job scheduled for ${delay} ms`);
this.api_timer = this.schedule(this.process_api, delay);
};
this.process_api = async () => {
log$f.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);
}
log$f.debug(`Processing ${ids} api requests`);
if (ids.length <= 0) {
log$f.debug("No ids found to query");
return;
}
let next_run = this.api_default_delay;
let results;
try {
log$f.debug(`Calling query_stats with key=*** ids=[${ids}]`);
results = await query_stats(this.config.key, ids);
} catch (err) {
log$f.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
};
}
log$f.debug(
`Received api results: blank=${results.blank}, count=${results.result.size}`
);
if (results.blank) {
for (const id of ids) {
this.requeue_api(id);
}
} else {
try {
await this.cache.update(Array.from(results.result.values()));
} catch (err) {
log$f.error("Failed to update cache", err);
}
for (const id of ids) {
const v = results.result.get(id);
if (v) {
log$f.debug("Id", id, "found in results. Resolving value.");
this.resolve(id, v);
} else {
log$f.debug("Id", id, "not found in results. Resolving no_data.");
this.resolve(id, { player_id: id, no_data: true });
}
}
}
if (results.limits) {
this.last_limits = 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) {
log$f.error("Failed to add analytics entry", err);
}
};
this.get_analytics_entries = async () => {
try {
return await this.cache.get_analytics();
} catch (err) {
log$f.error("Failed to get analytics entries", err);
return [];
}
};
this.get_aggregated_analytics = async () => {
const entries = await this.get_analytics_entries();
const aggregationMap = new Map();
for (const entry of entries) {
let param = "";
if (entry.params) {
param = getParamFast(entry.params, "sid") || getParamFast(entry.params, "step") || "";
}
if (!param && entry.hash) {
let hashClean = entry.hash;
if (hashClean.startsWith("#/")) {
hashClean = hashClean.substring(2);
} else if (hashClean.startsWith("#") || hashClean.startsWith("/")) {
hashClean = hashClean.substring(1);
}
if (!hashClean.startsWith("!") && !hashClean.startsWith("?")) {
hashClean = `?${hashClean}`;
}
param = getParamFast(hashClean, "sid") || getParamFast(hashClean, "step") || "";
}
const key = `${entry.url}|${param}|${entry.feature}|${entry.status}`;
const existing = aggregationMap.get(key);
if (existing) {
existing.count++;
} else {
aggregationMap.set(key, {
url: entry.url,
param: param || "-",
feature: entry.feature,
status: entry.status,
count: 1
});
}
}
return Array.from(aggregationMap.values());
};
this.clear_analytics = async () => {
try {
await this.cache.clear_analytics();
} catch (err) {
log$f.error("Failed to clear analytics entries", err);
}
};
this.config = config;
if (cache) {
this.cache = cache;
}
}
get analytics_enabled() {
return this.config.analytics_enabled;
}
}
const ffscouter = new FFScouter(ffconfig);
const unit = Object.create(null);
const m = 6e4, h = m * 60, d = h * 24, y = d * 365.25;
unit.year = unit.yr = unit.y = y;
unit.month = unit.mo = unit.mth = y / 12;
unit.week = unit.wk = unit.w = d * 7;
unit.day = unit.d = d;
unit.hour = unit.hr = unit.h = h;
unit.minute = unit.min = unit.m = m;
unit.second = unit.sec = unit.s = 1e3;
unit.millisecond = unit.millisec = unit.ms = 1;
unit.microsecond = unit.microsec = unit.us = unit.µs = 1e-3;
unit.nanosecond = unit.nanosec = unit.ns = 1e-6;
unit.group = ",";
unit.decimal = ".";
unit.placeholder = " _";
const durationRE = /((?:\d{1,16}(?:\.\d{1,16})?|\.\d{1,16})(?:[eE][-+]?\d{1,4})?)\s?([\p{L}]{0,14})/gu;
parse.unit = unit;
function parse(str = "", format = "ms") {
let result = null, prevUnits;
String(str).replace(new RegExp(`(\\d)[${parse.unit.placeholder}${parse.unit.group}](\\d)`, "g"), "$1$2").replace(parse.unit.decimal, ".").replace(durationRE, (_, n, units) => {
if (!units) {
if (prevUnits) {
for (const u in parse.unit) if (parse.unit[u] < prevUnits) {
units = u;
break;
}
} else units = format;
} else units = units.toLowerCase();
prevUnits = units = parse.unit[units] || parse.unit[units.replace(/s$/, "")];
if (units) result = (result || 0) + n * units;
});
return result && result / (parse.unit[format] || 1) * (str[0] === "-" ? -1 : 1);
}
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 FF_ARROW_VIEWBOX = "0 0 20 13";
const FF_ARROW_PATH_D = "M 0,0 H 13 20 L 10,12 Z";
const NO_DATA_COLOR = "#000000";
const BUILTIN_PALETTES = {
[ColorScheme.CLASSIC]: [
"#1734e8",
"#1788e8",
"#17dbe8",
"#17e8a1",
"#17e84e",
"#34e817",
"#88e817",
"#dbe817",
"#e8a117",
"#e84e17",
"#e81734"
],
[ColorScheme.COOL_DIVERGING]: [
"#2166ac",
"#2080a2",
"#1f9497",
"#1e8d75",
"#1c8254",
"#1b7837",
"#2e8b1e",
"#6c9e21",
"#b1aa23",
"#c47525",
"#d73027"
],
[ColorScheme.NEON]: [
"#0c50ff",
"#0cb1ff",
"#0cffec",
"#0cff8a",
"#0cff29",
"#50ff0c",
"#b1ff0c",
"#ffec0c",
"#ff8a0c",
"#ff290c",
"#ff0c50"
],
[ColorScheme.COLORBLIND_SAFE]: [
"#440154",
"#481a6c",
"#472f7d",
"#414487",
"#39568c",
"#2a788e",
"#21908d",
"#22a884",
"#42be71",
"#a8db34",
"#fde725"
],
[ColorScheme.GRAYSCALE]: [
"#f0f0f0",
"#e0e0e0",
"#cccccc",
"#b3b3b3",
"#999999",
"#808080",
"#666666",
"#4d4d4d",
"#333333",
"#1a1a1a",
"#000000"
],
[ColorScheme.GREEN_YELLOW_RED]: [
"#73bf69",
"#8ec55c",
"#a9cb50",
"#c4d243",
"#dfd837",
"#fade2a",
"#f8c034",
"#f7a23e",
"#f58548",
"#f46752",
"#f2495c"
],
[ColorScheme.BLUE_YELLOW_RED]: [
"#1f60c4",
"#4c7ebb",
"#799db3",
"#a5bbaa",
"#d2daa2",
"#fff899",
"#f3cb83",
"#e79e6d",
"#dc7056",
"#d04340",
"#c4162a"
],
[ColorScheme.PLASMA]: [
"#0d0887",
"#41049d",
"#6a00a8",
"#8f0da4",
"#b12a90",
"#cc4778",
"#e16462",
"#f2844b",
"#fca636",
"#fcce25",
"#f0f921"
]
};
function is_valid_custom_palette(colors) {
return colors !== null && colors.length === 11 && colors.every((c) => typeof c === "string");
}
function get_palette_for_scheme(scheme, customColors = null) {
if (scheme === ColorScheme.CUSTOM) {
if (is_valid_custom_palette(customColors)) {
return customColors;
}
return BUILTIN_PALETTES[ColorScheme.CLASSIC];
}
return BUILTIN_PALETTES[scheme];
}
function get_active_palette() {
return get_palette_for_scheme(ffconfig.color_scheme, ffconfig.custom_colors);
}
function get_ff_arrow_colour(d2) {
if (d2.no_data) {
return NO_DATA_COLOR;
}
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 r = get_active_palette()[ratio];
return r ?? NO_DATA_COLOR;
}
function get_contrast_color(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const brightness = r * 0.299 + g * 0.587 + b * 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);
}
function parse_duration_to_seconds(valStr) {
const trimmed = valStr.trim();
if (!trimmed) return null;
return parse(trimmed, "s");
}
let _react;
function getReact() {
return _react ??= unsafeWindow.React;
}
new Proxy({}, {
get(_, prop) {
return getReact()[prop];
}
});
const useState = ((...args) => getReact().useState(
...args
));
const useEffect = ((...args) => getReact().useEffect(
...args
));
const useRef = ((...args) => getReact().useRef(
...args
));
const useImperativeHandle = ((...args) => getReact().useImperativeHandle(
...args
));
const createElement = ((...args) => getReact().createElement(
...args
));
new Proxy(
{},
{
get: (_, prop) => getReact().Fragment[prop]
}
);
new Proxy(
{},
{
get: (_, prop) => getReact().StrictMode[prop]
}
);
new Proxy(
{},
{
get: (_, prop) => getReact().Suspense[prop]
}
);
new Proxy(
{},
{
get: (_, prop) => getReact().Children[prop]
}
);
const styles$2 = {
"ffscouter-info-line__label": "_ffscouter-info-line__label_xi5zk_1",
"ffscouter-info-line__badge": "_ffscouter-info-line__badge_xi5zk_8",
"ffscouter-info-line__premium-upgrade": "_ffscouter-info-line__premium-upgrade_xi5zk_15"
};
const log$e = logger.child("ui");
const PREMIUM_UPGRADE_URL$1 = "https://ffscouter.com/premium";
function FFHeaderLine({ playerId }) {
const [data, setData] = useState(null);
const [isPremium, setIsPremium] = useState(null);
const [premiumLoading, setPremiumLoading] = useState(false);
useEffect(() => {
let cancelled = false;
ffscouter.get(playerId).then((result) => {
if (!cancelled) setData(result);
}).catch((err) => {
log$e.error(err);
});
return () => {
cancelled = true;
};
}, [playerId]);
useEffect(() => {
if (!data) return;
setPremiumLoading(true);
let cancelled = false;
check_key_status.is_premium().then((premium) => {
if (!cancelled) setIsPremium(premium);
}).catch((err) => {
log$e.error(err);
}).finally(() => {
if (!cancelled) setPremiumLoading(false);
});
return () => {
cancelled = true;
};
}, [data]);
if (data === null) {
return jsxs(Fragment, { children: [
jsx("span", { className: styles$2["ffscouter-info-line__label"], children: "FairFight:" }),
jsx("span", { style: { fontStyle: "italic" }, children: "Loading..." })
] });
}
if (data.no_data) {
return jsxs(Fragment, { children: [
jsx("span", { className: styles$2["ffscouter-info-line__label"], children: "FairFight:" }),
jsx(
"span",
{
className: styles$2["ffscouter-info-line__badge"],
style: { background: "#444", color: "#fff" },
children: "No data"
}
)
] });
}
const ffString = format_ff_score(data);
const difficulty = format_difficulty_text(data);
const fresh = format_relative_time(data.last_updated);
const backgroundColor = get_ff_colour(data);
const textColor = get_contrast_color(backgroundColor);
let extraDetailsLine = null;
if (data.distribution?.distribution_human) {
const ageStr = format_relative_time(data.distribution.last_updated);
extraDetailsLine = jsxs(
"span",
{
style: {
display: "block",
marginTop: "2px",
fontSize: "12px",
fontStyle: "normal"
},
children: [
jsx("span", { className: styles$2["ffscouter-info-line__label"], children: "Top Stats:" }),
jsxs("span", { style: { fontWeight: "normal" }, children: [
data.distribution.distribution_human,
" ",
ageStr
] })
]
}
);
} else if (premiumLoading) {
extraDetailsLine = null;
} else if (isPremium === false && data.premium_insights_available) {
extraDetailsLine = jsx("span", { className: styles$2["ffscouter-info-line__premium-upgrade"], children: jsx(
"a",
{
href: PREMIUM_UPGRADE_URL$1,
target: "_blank",
rel: "noopener noreferrer",
style: { fontWeight: "bold", textDecoration: "underline" },
children: "Premium Data Available - Upgrade To View"
}
) });
}
return jsxs(Fragment, { children: [
jsx("span", { className: styles$2["ffscouter-info-line__label"], children: "FairFight:" }),
jsxs(
"span",
{
className: styles$2["ffscouter-info-line__badge"],
style: { background: backgroundColor, color: textColor },
children: [
ffString,
" (",
difficulty,
") ",
fresh
]
}
),
jsxs(
"span",
{
style: {
fontSize: "11px",
fontWeight: "normal",
marginLeft: "6px",
verticalAlign: "middle",
fontStyle: "italic"
},
children: [
"Est. Stats: ",
jsx("span", { children: data.bs_estimate_human })
]
}
),
extraDetailsLine
] });
}
const log$d = logger.child("dom");
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 v = search.get(param);
if (v) {
return parseInt(v, 10);
}
}
return null;
}
function torn_page(page, params = {}, match_hash = []) {
const url_match = window.location.href.startsWith(
`https://www.torn.com/${page}.php`
);
if (!url_match) {
return false;
}
const search = new URLSearchParams(window.location.search);
let sid_match = true;
let step_match = true;
let page_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;
}
if (params.page) {
const page_page = search.get("page");
page_match = page_page !== null && params.page === page_page;
}
if (!sid_match || !step_match || !page_match) {
return false;
}
let hash_match = false;
if (match_hash.length === 0) {
hash_match = true;
} else {
const hash = window.location.hash;
for (const h2 of match_hash) {
if (h2.endsWith("*")) {
const stripped = h2.substring(0, h2.length - 1);
if (hash.startsWith(stripped)) {
hash_match = true;
break;
}
}
if (hash === h2) {
hash_match = true;
break;
}
}
}
return sid_match && step_match && page_match && hash_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="${FF_ARROW_VIEWBOX}" 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="${FF_ARROW_PATH_D}" id="path1" style="display:inline;stroke-width:${ffconfig.gauge_marker_border_width};"/>
</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("ffscouter-arrow");
return svg;
}
function make_marker(d2) {
const markerType = ffconfig.gauge_marker_type;
if (markerType === GaugeMarkerType.BUBBLE_FF || markerType === GaugeMarkerType.BUBBLE_ESTIMATE) {
const fill = get_ff_arrow_colour(d2);
const contrastColor = get_contrast_color(fill);
const bubble = document.createElement("div");
bubble.classList.add("ffscouter-bubble");
bubble.style.backgroundColor = fill;
bubble.style.color = contrastColor;
bubble.style.borderWidth = `${ffconfig.gauge_marker_border_width * (ffconfig.gauge_marker_scale / 100)}px`;
if (markerType === GaugeMarkerType.BUBBLE_FF) {
bubble.textContent = d2.fair_fight.toFixed(2);
} else {
bubble.textContent = d2.bs_estimate_human || "N/A";
}
return bubble;
}
return make_arrow(d2);
}
function add_ff_arrow(element, featureName = "Unknown") {
const player_id = get_player_id_in_element(element);
if (!player_id) {
return;
}
if (element.querySelector(".ffscouter-gauge") || element.classList.contains("ffscouter-gauge")) {
ffscouter.add_analytics_entry(featureName, player_id, "ignored");
return;
}
ffscouter.get(player_id).then((d2) => {
if (d2.no_data) {
return;
}
if (element.querySelector(".ffscouter-gauge") || element.classList.contains("ffscouter-gauge")) {
ffscouter.add_analytics_entry(featureName, player_id, "ignored");
return;
}
const percent = ff_to_percent(d2);
element.classList.add("ffscouter-gauge");
element.style.setProperty("--band-percent", `${percent}`);
document.body.style.setProperty(
"--ffscouter-marker-scale",
`${ffconfig.gauge_marker_scale / 100}`
);
const a = element.querySelector(".ffscouter-arrow, .ffscouter-bubble");
if (a) {
a.remove();
}
element.appendChild(make_marker(d2));
ffscouter.add_analytics_entry(featureName, player_id, "applied");
});
}
function has_href(el) {
return el instanceof HTMLAnchorElement;
}
function extract_target_id(href, r) {
const match = href.match(r);
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;
}
}
const parent_anchor = element.closest("a");
if (parent_anchor) {
const xid = extract_target_id(
parent_anchor.href,
/.*XID=(?<target_id>\d+)/
);
if (xid) {
return xid;
}
const userid = extract_target_id(
parent_anchor.href,
/.*userId=(?<target_id>\d+)/
);
if (userid) {
return userid;
}
}
return null;
}
const seenUnknownActivityLabels = new Set();
function get_activity_status(row) {
const wrap2 = row.querySelector('[class*="userStatusWrap"]');
const label = wrap2?.getAttribute("aria-label") || "";
const match = label.match(/ is (online|offline|idle)$/i);
const status = match?.[1];
if (!status) {
if (!seenUnknownActivityLabels.has(label)) {
seenUnknownActivityLabels.add(label);
log$d.warn(`Unrecognized activity aria-label: "${label}"`);
}
return "unknown";
}
return status.toLowerCase();
}
function apply_ff_gauge_selector(node_list, featureName = "Unknown") {
for (const node of node_list) {
add_ff_arrow(node, featureName);
}
}
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;
}
const CREATE_ELEMENT_RETRY_DELAYS_MS = [0, 50, 150];
async function create_ff_element(tagName) {
const ctor = customElements.get(tagName);
for (const delay of CREATE_ELEMENT_RETRY_DELAYS_MS) {
if (delay > 0) {
await new Promise((resolve) => setTimeout(resolve, delay));
}
const element = document.createElement(tagName);
if (!ctor || element instanceof ctor) {
return element;
}
log$d.warn(`<${tagName}> construction produced a fallback element; retrying`);
}
log$d.error(
`Failed to construct a working <${tagName}> after multiple attempts`
);
return null;
}
class MonitorElements {
constructor(node_matcher, handler, root, continuous, options, _timeout) {
this.options = {
target: false,
added: false,
removed: false
};
this.started = false;
this.timer = null;
this.start = () => {
if (this.started) {
return;
}
let initial = false;
for (const child of this.root.childNodes) {
if (child instanceof HTMLElement && this.node_matcher(child)) {
this.handler({ added: child });
initial = true;
}
}
if (!this.continuous && initial) {
this.started = false;
return;
}
this.observer.observe(this.root, { childList: true });
this.timer = setInterval(() => {
if (!this.root.isConnected) {
this.cleanup();
}
}, 1e4);
this.started = true;
};
this.cleanup = () => {
this.observer.disconnect();
if (this.timer) {
clearInterval(this.timer);
}
this.timer = null;
};
this.node_matcher = node_matcher;
this.handler = handler;
this.root = root;
this.continuous = continuous;
this.options = options;
this.observer = new MutationObserver(async (mutations) => {
for (const mutation of mutations) {
if (this.options.target && mutation.target instanceof HTMLElement && this.node_matcher(mutation.target)) {
this.handler({ target: mutation.target });
}
if (this.options.added) {
for (const node of mutation.addedNodes) {
if (node instanceof HTMLElement && this.node_matcher(node)) {
this.handler({ added: node });
}
}
}
if (this.options.removed) {
for (const node of mutation.removedNodes) {
if (node instanceof HTMLElement && this.node_matcher(node)) {
this.handler({ removed: node });
}
}
}
}
});
}
}
async function getLocalUserId() {
const name = await wait_for_element(
".settings-menu > .link > a:first-child",
15e3
);
if (!name || !name.href) {
log$d.debug("Failed to find the XID element.");
return null;
}
try {
const params = new URL(name.href).searchParams;
return params.get("XID");
} catch {
log$d.debug("User XID is malformed");
return null;
}
}
function create_info_line() {
const info_line = document.createElement("div");
info_line.className = "ffscouter-info-line";
info_line.style.display = "block";
info_line.style.clear = "both";
info_line.style.margin = "5px 0";
return info_line;
}
function on_navigation(callback) {
const nav = window.navigation;
if (nav) {
nav.addEventListener("currententrychange", callback);
return () => {
nav.removeEventListener("currententrychange", callback);
};
}
const delayedCallback = () => {
setTimeout(callback, 0);
};
window.addEventListener("popstate", delayedCallback);
window.addEventListener("hashchange", delayedCallback);
return () => {
window.removeEventListener("popstate", delayedCallback);
window.removeEventListener("hashchange", delayedCallback);
};
}
function get_attack_url(playerId) {
return `https://www.torn.com/page.php?sid=attack&user2ID=${playerId}`;
}
function open_attack_link(playerId, options) {
const url = get_attack_url(playerId);
const shouldOpenInNewTab = options?.openInNewTab !== void 0 ? options.openInNewTab : ffconfig.war_quick_attack_action === "new_tab";
if (shouldOpenInNewTab) {
window.open(url, "_blank");
} else {
window.location.href = url;
}
}
const SORT_ICON_CLASS_SETS = {
sortIcon___wbOOi: {
sortIcon: "sortIcon___wbOOi",
activeIcon: "activeIcon___wmLLe",
desc: "desc___wkA0A",
asc: "asc___y_atw"
},
sortIcon___LNQ9D: {
sortIcon: "sortIcon___LNQ9D",
activeIcon: "activeIcon___SwNJj",
desc: "desc___ZvHWf",
asc: "asc___YAXFZ"
},
sortIcon___pMyNX: {
sortIcon: "sortIcon___pMyNX",
activeIcon: "activeIcon___dw8TK",
desc: "desc___TLpPc",
asc: "asc___Q3bz5"
}
};
function detect_sort_icon_classes(root) {
const existing = root.querySelector(
"[class*='sortIcon___']:not(.ffscouter-sort-icon)"
);
if (!existing) return null;
const sortIcon = Array.from(existing.classList).find((c) => c.startsWith("sortIcon___")) ?? "";
const classes = SORT_ICON_CLASS_SETS[sortIcon];
if (!classes) return null;
const tab = Array.from(existing.parentElement?.classList ?? []).find(
(c) => c.startsWith("tab___")
) ?? "";
return { ...classes, tab };
}
let _reactDOM;
function getReactDOM() {
return _reactDOM ??= unsafeWindow.ReactDOM;
}
new Proxy({}, {
get(_, prop) {
return getReactDOM()[prop];
}
});
const createRoot = ((...args) => getReactDOM().createRoot(...args));
function mountComponent(element, parent) {
const container = document.createElement("div");
container.style.display = "contents";
parent.appendChild(container);
const root = createRoot(container);
root.render(element);
return root;
}
const log$c = logger.child("feature:attack");
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 index$d = {
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;
}
log$c.debug("On the attack page, found player_id", player_id);
const info_line = create_info_line();
mountComponent(
createElement(FFHeaderLine, { playerId: player_id }),
info_line
);
inject_info_line$1(info_line);
},
httpIntercept: {
before(_url, _init) {
return void 0;
},
after(_bodyText, _response, _ctx) {
return void 0;
}
}
};
const __vite_glob_0_0 = Object.freeze( Object.defineProperty({
__proto__: null,
default: index$d
}, Symbol.toStringTag, { value: "Module" }));
const _deprecatedStub = null;
const __vite_glob_0_1 = Object.freeze( Object.defineProperty({
__proto__: null,
default: _deprecatedStub
}, Symbol.toStringTag, { value: "Module" }));
const styles$1 = {
"ff-filter-box": "_ff-filter-box_ursux_1",
"ff-filter-box--no-borders": "_ff-filter-box--no-borders_ursux_19",
"ff-filter-box__header": "_ff-filter-box__header_ursux_52",
"ff-filter-box__header-actions": "_ff-filter-box__header-actions_ursux_60",
"ff-filter-box__action-btn": "_ff-filter-box__action-btn_ursux_66",
"ff-filter-box__action-btn--active": "_ff-filter-box__action-btn--active_ursux_88",
"ff-filter-box__action-btn--inactive": "_ff-filter-box__action-btn--inactive_ursux_93",
"ff-filter-box__action-btn--reset": "_ff-filter-box__action-btn--reset_ursux_104",
"ff-filter-box__grid": "_ff-filter-box__grid_ursux_114",
"ff-filter-box__group--sort": "_ff-filter-box__group--sort_ursux_121",
"ff-filter-box__group--level": "_ff-filter-box__group--level_ursux_125",
"ff-filter-box__group--activity": "_ff-filter-box__group--activity_ursux_129",
"ff-filter-box__group--status": "_ff-filter-box__group--status_ursux_133",
"ff-filter-box__group--ff": "_ff-filter-box__group--ff_ursux_137",
"ff-filter-box__group--stats": "_ff-filter-box__group--stats_ursux_141",
"ff-filter-box__group--last-action": "_ff-filter-box__group--last-action_ursux_145",
"ff-filter-box__group--columns": "_ff-filter-box__group--columns_ursux_149",
"ff-filter-box__group": "_ff-filter-box__group_ursux_121",
"ff-filter-box__sort-controls": "_ff-filter-box__sort-controls_ursux_171",
"ff-filter-box__sort-btn": "_ff-filter-box__sort-btn_ursux_177",
"ff-filter-box__compare-btn": "_ff-filter-box__compare-btn_ursux_181",
"ff-filter-box__display-select": "_ff-filter-box__display-select_ursux_186",
"ff-filter-box__options": "_ff-filter-box__options_ursux_197",
"ff-filter-box__range-inputs": "_ff-filter-box__range-inputs_ursux_210"
};
const cls$1 = {
box: styles$1["ff-filter-box"],
boxNoBorders: styles$1["ff-filter-box--no-borders"],
header: styles$1["ff-filter-box__header"],
headerActions: styles$1["ff-filter-box__header-actions"],
actionBtn: styles$1["ff-filter-box__action-btn"],
actionBtnActive: styles$1["ff-filter-box__action-btn--active"],
actionBtnInactive: styles$1["ff-filter-box__action-btn--inactive"],
actionBtnReset: styles$1["ff-filter-box__action-btn--reset"],
grid: styles$1["ff-filter-box__grid"],
group: styles$1["ff-filter-box__group"],
groupSort: styles$1["ff-filter-box__group--sort"],
groupLevel: styles$1["ff-filter-box__group--level"],
groupActivity: styles$1["ff-filter-box__group--activity"],
groupStatus: styles$1["ff-filter-box__group--status"],
groupFf: styles$1["ff-filter-box__group--ff"],
groupStats: styles$1["ff-filter-box__group--stats"],
groupLastAction: styles$1["ff-filter-box__group--last-action"],
groupColumns: styles$1["ff-filter-box__group--columns"],
sortControls: styles$1["ff-filter-box__sort-controls"],
sortBtn: styles$1["ff-filter-box__sort-btn"],
compareBtn: styles$1["ff-filter-box__compare-btn"],
displaySelect: styles$1["ff-filter-box__display-select"],
options: styles$1["ff-filter-box__options"],
rangeInputs: styles$1["ff-filter-box__range-inputs"]
};
const DEFAULT_HIDDEN_COLUMNS = { level: false, status: false, score: false };
const DEFAULT_STATE = {
sortBy: "none",
filterEnabled: true,
activity: { online: true, idle: true, offline: true },
status: {
okay: true,
traveling: true,
hospital: true,
jail: true,
abroad: true,
federal: true,
fallen: true
},
levelMin: null,
levelMax: null,
ffMin: null,
ffMax: null,
statsMin: null,
statsMax: null,
lastActionMin: null,
lastActionMax: null,
hiddenColumns: DEFAULT_HIDDEN_COLUMNS
};
function getFilterBoxHandle(el) {
if (!el) return null;
return el.__ffHandle ?? null;
}
function isMobileView() {
return typeof window !== "undefined" && window.innerWidth < 784;
}
function FFFactionFilterBox({
mode,
onFilterChange,
ref,
initialHasLastActionData = false,
onReady
}) {
const [filterState, setFilterState] = useState(
() => DEFAULT_STATE
);
const [collapsed, setCollapsed] = useState(
() => mode === "war" ? ffconfig.war_filter_collapsed : ffconfig.faction_filter_collapsed
);
const [colDisplay, setColDisplay] = useState(
FactionsColDisplay.FAIR_FIGHT
);
const [hasLastActionData, setHasLastActionData] = useState(
initialHasLastActionData
);
const filterStateRef = useRef(filterState);
filterStateRef.current = filterState;
const collapsedRef = useRef(collapsed);
collapsedRef.current = collapsed;
const hasLastActionDataRef = useRef(hasLastActionData);
hasLastActionDataRef.current = hasLastActionData;
const modeRef = useRef(mode);
modeRef.current = mode;
const onFilterChangeRef = useRef(onFilterChange);
onFilterChangeRef.current = onFilterChange;
const debounceTimerRef = useRef(null);
const wasMobileRef = useRef(isMobileView());
const rootRef = useRef(null);
const applyStatePatch = (patch) => {
const next = { ...filterStateRef.current, ...patch };
filterStateRef.current = next;
setFilterState(next);
};
const getFilterSnapshot = () => {
const s2 = filterStateRef.current;
return {
sortBy: s2.sortBy,
filterEnabled: s2.filterEnabled,
activity: s2.activity,
status: modeRef.current === "war" ? { ...s2.status, fallen: false } : s2.status,
levelMin: s2.levelMin,
levelMax: s2.levelMax,
ffMin: s2.ffMin,
ffMax: s2.ffMax,
statsMin: s2.statsMin ? parse_suffix_number(s2.statsMin) : null,
statsMax: s2.statsMax ? parse_suffix_number(s2.statsMax) : null,
lastActionMinSec: s2.lastActionMin ? parse_duration_to_seconds(s2.lastActionMin) : null,
lastActionMaxSec: s2.lastActionMax ? parse_duration_to_seconds(s2.lastActionMax) : null
};
};
const dispatchChange = () => {
onFilterChangeRef.current(getFilterSnapshot());
};
const saveState = (state) => {
const isWar = modeRef.current === "war";
const isMobile = isMobileView();
const existing = isWar ? ffconfig.war_filter_state : ffconfig.faction_filter_state;
const savedHiddenColumns = existing?.hiddenColumns;
const savedHiddenColumnsMobile = existing?.hiddenColumnsMobile;
const hiddenColumnsToSave = isMobile ? savedHiddenColumns ?? DEFAULT_HIDDEN_COLUMNS : state.hiddenColumns ?? DEFAULT_HIDDEN_COLUMNS;
const hiddenColumnsMobileToSave = isMobile ? state.hiddenColumns ?? DEFAULT_HIDDEN_COLUMNS : savedHiddenColumnsMobile ?? {
level: true,
status: false,
score: false
};
const toSave = {
...state,
hiddenColumns: hiddenColumnsToSave,
hiddenColumnsMobile: hiddenColumnsMobileToSave
};
if (isWar) {
ffconfig.war_filter_state = toSave;
} else {
ffconfig.faction_filter_state = toSave;
}
};
const executeChangeImmediately = () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
}
saveState(filterStateRef.current);
dispatchChange();
};
const queueChange = () => {
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = setTimeout(() => {
saveState(filterStateRef.current);
dispatchChange();
debounceTimerRef.current = null;
}, 250);
};
const loadState = () => {
const isWar = modeRef.current === "war";
const isMobile = isMobileView();
const newCollapsed = isWar ? ffconfig.war_filter_collapsed : ffconfig.faction_filter_collapsed;
setCollapsed(newCollapsed);
setColDisplay(
isWar ? ffconfig.war_col_display : ffconfig.factions_col_display
);
const parsed = isWar ? ffconfig.war_filter_state : ffconfig.faction_filter_state;
let next;
if (parsed) {
const savedSortBy = parsed.sortBy ?? "none";
const hiddenColumns2 = isMobile ? {
level: parsed.hiddenColumnsMobile?.level ?? true,
status: parsed.hiddenColumnsMobile?.status ?? DEFAULT_HIDDEN_COLUMNS.status,
score: parsed.hiddenColumnsMobile?.score ?? DEFAULT_HIDDEN_COLUMNS.score
} : {
level: parsed.hiddenColumns?.level ?? DEFAULT_HIDDEN_COLUMNS.level,
status: parsed.hiddenColumns?.status ?? DEFAULT_HIDDEN_COLUMNS.status,
score: parsed.hiddenColumns?.score ?? DEFAULT_HIDDEN_COLUMNS.score
};
next = {
sortBy: savedSortBy === "ff-asc" || savedSortBy === "ff-desc" ? savedSortBy : "none",
filterEnabled: parsed.filterEnabled ?? true,
activity: { ...DEFAULT_STATE.activity, ...parsed.activity },
status: { ...DEFAULT_STATE.status, ...parsed.status },
levelMin: parsed.levelMin ?? null,
levelMax: parsed.levelMax ?? null,
ffMin: parsed.ffMin ?? null,
ffMax: parsed.ffMax ?? null,
statsMin: parsed.statsMin ?? null,
statsMax: parsed.statsMax ?? null,
lastActionMin: parsed.lastActionMin ?? null,
lastActionMax: parsed.lastActionMax ?? null,
hiddenColumns: hiddenColumns2
};
} else {
next = {
...DEFAULT_STATE,
hiddenColumns: {
level: isMobile,
status: DEFAULT_HIDDEN_COLUMNS.status,
score: DEFAULT_HIDDEN_COLUMNS.score
}
};
}
filterStateRef.current = next;
setFilterState(next);
onFilterChangeRef.current(getFilterSnapshot());
};
useEffect(() => {
loadState();
const onConfigUpdated = () => {
setColDisplay(
modeRef.current === "war" ? ffconfig.war_col_display : ffconfig.factions_col_display
);
};
const onResize = () => {
const isMobile = isMobileView();
if (isMobile !== wasMobileRef.current) {
wasMobileRef.current = isMobile;
loadState();
}
};
window.addEventListener("ff-config-updated", onConfigUpdated);
window.addEventListener("resize", onResize);
onReady?.();
return () => {
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
window.removeEventListener("ff-config-updated", onConfigUpdated);
window.removeEventListener("resize", onResize);
};
}, []);
useEffect(() => {
if (mode !== "war") return;
const target = rootRef.current?.closest(".faction-war");
if (!(target instanceof HTMLElement)) return;
const cols = filterState.hiddenColumns ?? DEFAULT_HIDDEN_COLUMNS;
for (const [col, isHidden] of Object.entries(cols)) {
if (isHidden) {
target.setAttribute(`data-ffscouter-hide-${col}`, "true");
} else {
target.removeAttribute(`data-ffscouter-hide-${col}`);
}
}
}, [filterState.hiddenColumns, mode]);
useImperativeHandle(
ref,
() => ({
setSortBy(val) {
applyStatePatch({ sortBy: val });
executeChangeImmediately();
},
getFilterSnapshot,
get sortBy() {
return filterStateRef.current.sortBy;
},
get activity() {
return filterStateRef.current.activity;
},
get hasLastActionData() {
return hasLastActionDataRef.current;
},
setHasLastActionData(val) {
hasLastActionDataRef.current = val;
setHasLastActionData(val);
},
setFilterState(patch) {
applyStatePatch(patch);
dispatchChange();
},
dispatchChange,
get ready() {
return true;
}
}),
[]
);
const onToggle = (e) => {
const newCollapsed = !e.currentTarget.open;
if (newCollapsed === collapsedRef.current) return;
setCollapsed(newCollapsed);
if (mode === "war") {
ffconfig.war_filter_collapsed = newCollapsed;
} else {
ffconfig.faction_filter_collapsed = newCollapsed;
}
};
const onSortToggle = () => {
const next = filterStateRef.current.sortBy === "none" ? "ff-desc" : filterStateRef.current.sortBy === "ff-desc" ? "ff-asc" : "none";
applyStatePatch({ sortBy: next });
executeChangeImmediately();
};
const onDisplayChange = (e) => {
const val = e.target.value;
setColDisplay(val);
if (mode === "war") {
ffconfig.war_col_display = val;
} else {
ffconfig.factions_col_display = val;
}
window.dispatchEvent(new CustomEvent("ff-config-updated"));
executeChangeImmediately();
};
const onToggleFilter = (e) => {
e.preventDefault();
e.stopPropagation();
applyStatePatch({ filterEnabled: !filterStateRef.current.filterEnabled });
executeChangeImmediately();
};
const onResetFilters = (e) => {
e.preventDefault();
e.stopPropagation();
applyStatePatch({
activity: { online: true, idle: true, offline: true },
status: {
okay: true,
traveling: true,
hospital: true,
jail: true,
abroad: true,
federal: true,
fallen: true
},
levelMin: null,
levelMax: null,
ffMin: null,
ffMax: null,
statsMin: null,
statsMax: null,
lastActionMin: null,
lastActionMax: null
});
executeChangeImmediately();
};
const onActivityChange = (key, val) => {
applyStatePatch({
activity: { ...filterStateRef.current.activity, [key]: val }
});
executeChangeImmediately();
};
const onStatusChange = (key, val) => {
applyStatePatch({
status: { ...filterStateRef.current.status, [key]: val }
});
executeChangeImmediately();
};
const onLevelChange = (type, valStr) => {
const val = valStr === "" ? null : Number.parseInt(valStr, 10);
applyStatePatch(type === "min" ? { levelMin: val } : { levelMax: val });
queueChange();
};
const onFFChange = (type, valStr) => {
const val = valStr === "" ? null : Number.parseFloat(valStr);
applyStatePatch(type === "min" ? { ffMin: val } : { ffMax: val });
queueChange();
};
const onStatsChange = (type, valStr) => {
const val = valStr.trim() === "" ? null : valStr;
applyStatePatch(type === "min" ? { statsMin: val } : { statsMax: val });
queueChange();
};
const onLastActionChange = (type, valStr) => {
const val = valStr.trim() === "" ? null : valStr;
applyStatePatch(
type === "min" ? { lastActionMin: val } : { lastActionMax: val }
);
queueChange();
};
const onColumnVisibilityChange = (key, val) => {
applyStatePatch({
hiddenColumns: {
...filterStateRef.current.hiddenColumns ?? DEFAULT_HIDDEN_COLUMNS,
[key]: val
}
});
executeChangeImmediately();
};
const onCompareActivity = () => {
const container = rootRef.current?.closest(".faction-war");
const links = container ? Array.from(
container.querySelectorAll('a[href*="step=profile"]')
) : [];
const seen = new Set();
const ids = [];
const tryExtract = (link) => {
try {
const url = new URL(link.href, window.location.origin);
const id = url.searchParams.get("ID");
if (id && !seen.has(id)) {
seen.add(id);
ids.push(id);
}
} catch {
const match = (link.getAttribute("href") || "").match(/[?&]ID=(\d+)/i);
if (match?.[1]) {
const id = match[1];
if (!seen.has(id)) {
seen.add(id);
ids.push(id);
}
}
}
};
for (const link of links) tryExtract(link);
if (ids.length < 2) {
const docLinks = Array.from(
document.querySelectorAll('a[href*="step=profile"]')
);
for (const link of docLinks) tryExtract(link);
}
if (ids.length < 2) {
console.warn("Could not find faction IDs to compare activity.");
return;
}
const now = new Date();
const start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1e3);
const formatUTC = (d2) => {
const y2 = d2.getUTCFullYear();
const m2 = String(d2.getUTCMonth() + 1).padStart(2, "0");
const day = String(d2.getUTCDate()).padStart(2, "0");
const h2 = String(d2.getUTCHours()).padStart(2, "0");
const min = String(d2.getUTCMinutes()).padStart(2, "0");
return `${y2}-${m2}-${day}T${h2}:${min}`;
};
const factionId1 = ids[0];
const factionId2 = ids[1];
const scouterUrl = `https://ffscouter.com/faction-activity-comparison?faction_id_1=${factionId1}&faction_id_2=${factionId2}&start_at=${encodeURIComponent(formatUTC(start))}&end_at=${encodeURIComponent(formatUTC(now))}&bucket_minutes=5`;
window.open(scouterUrl, "_blank");
};
const s = filterState;
const hiddenColumns = s.hiddenColumns ?? DEFAULT_HIDDEN_COLUMNS;
const isEst = colDisplay === FactionsColDisplay.BATTLE_STATS;
const sortText = isEst ? "Stats" : "FF";
return jsxs(
"details",
{
ref: rootRef,
className: `${cls$1.box}${mode === "war" ? ` ${cls$1.boxNoBorders}` : ""}`,
open: !collapsed,
onToggle,
children: [
jsx("summary", { children: jsxs("div", { className: cls$1.header, children: [
jsx("span", { children: "FFScouter Filter & Sort Controls" }),
jsxs(
"div",
{
className: cls$1.headerActions,
onClick: (e) => {
e.preventDefault();
e.stopPropagation();
},
children: [
jsx(
"button",
{
type: "button",
className: `${cls$1.actionBtn} ${s.filterEnabled ? cls$1.actionBtnActive : cls$1.actionBtnInactive}`,
title: s.filterEnabled ? "Turn off filtering" : "Turn on filtering",
onClick: onToggleFilter,
children: s.filterEnabled ? jsx("svg", { viewBox: "0 0 16 16", "aria-hidden": "true", children: jsx("path", { d: "M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.124.318l-4.5 5.5v4.682a.5.5 0 0 1-.168.373l-2.5 2a.5.5 0 0 1-.832-.373v-6.682l-4.5-5.5A.5.5 0 0 1 1.5 3.5v-2z" }) }) : jsxs("svg", { viewBox: "0 0 16 16", "aria-hidden": "true", children: [
jsx("path", { d: "M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.124.318l-4.5 5.5v4.682a.5.5 0 0 1-.168.373l-2.5 2a.5.5 0 0 1-.832-.373v-6.682l-4.5-5.5A.5.5 0 0 1 1.5 3.5v-2z" }),
jsx(
"line",
{
x1: "1.5",
y1: "14.5",
x2: "14.5",
y2: "1.5",
stroke: "currentColor",
strokeWidth: "1.5"
}
)
] })
}
),
jsx(
"button",
{
type: "button",
className: `${cls$1.actionBtn} ${cls$1.actionBtnReset}`,
title: "Reset filters to default",
onClick: onResetFilters,
children: jsxs("svg", { viewBox: "0 0 16 16", "aria-hidden": "true", children: [
jsx(
"path",
{
fillRule: "evenodd",
d: "M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"
}
),
jsx("path", { d: "M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" })
] })
}
)
]
}
)
] }) }),
jsxs("div", { className: cls$1.grid, children: [
jsxs("div", { className: `${cls$1.group} ${cls$1.groupSort}`, children: [
jsx("strong", { children: "Sort & Display" }),
jsxs("div", { className: cls$1.sortControls, children: [
jsx(
"button",
{
type: "button",
id: "sort-toggle-btn",
className: cls$1.sortBtn,
onClick: onSortToggle,
children: s.sortBy === "none" ? "Sort: Default" : s.sortBy === "ff-asc" ? `Sort: ${sortText} ▲` : `Sort: ${sortText} ▼`
}
),
jsxs(
"select",
{
id: mode === "war" ? "war-col-display-filter" : "factions-col-display-filter",
value: colDisplay,
onChange: onDisplayChange,
className: cls$1.displaySelect,
children: [
jsx("option", { value: "fair_fight", children: "Show: FF Score" }),
jsx("option", { value: "battle_stats", children: "Show: BS Estimate" }),
jsx("option", { value: "none", children: "Show: None (Hide)" })
]
}
),
mode === "war" && jsx(
"button",
{
type: "button",
id: "compare-faction-activity-btn",
className: cls$1.compareBtn,
onClick: onCompareActivity,
children: "Compare Activity"
}
)
] })
] }),
jsxs("div", { className: `${cls$1.group} ${cls$1.groupActivity}`, children: [
jsx("strong", { children: "Activity" }),
jsx("div", { className: cls$1.options, children: [
["online", "Online"],
["idle", "Idle"],
["offline", "Offline"]
].map(([key, label]) => jsxs("label", { children: [
jsx(
"input",
{
type: "checkbox",
checked: s.activity[key],
onChange: (e) => onActivityChange(key, e.target.checked)
}
),
label
] }, key)) })
] }),
jsxs("div", { className: `${cls$1.group} ${cls$1.groupStatus}`, children: [
jsx("strong", { children: "Status" }),
jsx("div", { className: cls$1.options, children: [
["okay", "Okay"],
["hospital", "Hospital"],
["jail", "Jail"],
["abroad", "Abroad"],
["traveling", "Traveling"],
["federal", "Fedded"],
["fallen", "Fallen"]
].filter(([key]) => mode !== "war" || key !== "fallen").map(([key, label]) => jsxs("label", { children: [
jsx(
"input",
{
type: "checkbox",
checked: s.status[key],
onChange: (e) => onStatusChange(key, e.target.checked)
}
),
label
] }, key)) })
] }),
jsxs("div", { className: `${cls$1.group} ${cls$1.groupLevel}`, children: [
jsx("strong", { children: "Level Range" }),
jsxs("div", { className: cls$1.rangeInputs, children: [
jsx(
"input",
{
type: "number",
placeholder: "Min",
value: s.levelMin !== null ? String(s.levelMin) : "",
onChange: (e) => onLevelChange("min", e.target.value)
}
),
jsx("span", { children: "to" }),
jsx(
"input",
{
type: "number",
placeholder: "Max",
value: s.levelMax !== null ? String(s.levelMax) : "",
onChange: (e) => onLevelChange("max", e.target.value)
}
)
] })
] }),
jsxs("div", { className: `${cls$1.group} ${cls$1.groupFf}`, children: [
jsx("strong", { children: "FF Range" }),
jsxs("div", { className: cls$1.rangeInputs, children: [
jsx(
"input",
{
type: "number",
step: "0.1",
placeholder: "Min",
value: s.ffMin !== null ? String(s.ffMin) : "",
onChange: (e) => onFFChange("min", e.target.value)
}
),
jsx("span", { children: "to" }),
jsx(
"input",
{
type: "number",
step: "0.1",
placeholder: "Max",
value: s.ffMax !== null ? String(s.ffMax) : "",
onChange: (e) => onFFChange("max", e.target.value)
}
)
] })
] }),
jsxs("div", { className: `${cls$1.group} ${cls$1.groupStats}`, children: [
jsx("strong", { children: "Stats Range" }),
jsxs("div", { className: cls$1.rangeInputs, children: [
jsx(
"input",
{
type: "text",
placeholder: "Min",
value: s.statsMin !== null ? s.statsMin : "",
onChange: (e) => onStatsChange("min", e.target.value)
}
),
jsx("span", { children: "to" }),
jsx(
"input",
{
type: "text",
placeholder: "Max",
value: s.statsMax !== null ? s.statsMax : "",
onChange: (e) => onStatsChange("max", e.target.value)
}
)
] })
] }),
mode === "war" && hasLastActionData && jsxs("div", { className: `${cls$1.group} ${cls$1.groupLastAction}`, children: [
jsx("strong", { children: "Last Action Range" }),
jsxs("div", { className: cls$1.rangeInputs, children: [
jsx(
"input",
{
type: "text",
placeholder: "Min",
title: 'e.g. "10m", "1h", "4h2m15s"',
value: s.lastActionMin !== null ? s.lastActionMin : "",
onChange: (e) => onLastActionChange("min", e.target.value)
}
),
jsx("span", { children: "to" }),
jsx(
"input",
{
type: "text",
placeholder: "Max",
title: 'e.g. "10m", "1h", "4h2m15s"',
value: s.lastActionMax !== null ? s.lastActionMax : "",
onChange: (e) => onLastActionChange("max", e.target.value)
}
)
] })
] }),
mode === "war" && jsxs("div", { className: `${cls$1.group} ${cls$1.groupColumns}`, children: [
jsx("strong", { children: "Visible Columns" }),
jsx("div", { className: cls$1.options, children: [
["level", "Level"],
["status", "Status"],
["score", "Score"]
].map(([key, label]) => jsxs("label", { children: [
jsx(
"input",
{
type: "checkbox",
checked: !hiddenColumns[key],
onChange: (e) => onColumnVisibilityChange(key, !e.target.checked)
}
),
label
] }, key)) })
] })
] })
]
}
);
}
function get_current_time_seconds() {
if (typeof window.getCurrentTimestamp === "function") {
return window.getCurrentTimestamp() / 1e3;
}
return Date.now() / 1e3;
}
const isApplying = new WeakMap();
function is_applying(list) {
return isApplying.get(list) ?? false;
}
function apply_filters_and_sort(membersList, filters) {
if (isApplying.get(membersList)) return;
isApplying.set(membersList, 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) {
if (filters.filterEnabled === false) {
show_row(row);
continue;
}
const activity = get_activity_status(row);
const allActivityUnchecked = !filters.activity.online && !filters.activity.idle && !filters.activity.offline;
const matchesActivity = allActivityUnchecked || activity === "unknown" || activity === "online" && filters.activity.online || activity === "idle" && filters.activity.idle || activity === "offline" && filters.activity.offline;
if (!matchesActivity) {
hide_row(row);
continue;
}
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 if (statusCell.classList.contains("federal") || statusCell.querySelector(".federal")) {
status = "federal";
} else if (statusCell.classList.contains("fallen") || statusCell.querySelector(".fallen")) {
status = "fallen";
} else {
status = "okay";
}
}
const allStatusUnchecked = !filters.status.okay && !filters.status.traveling && !filters.status.hospital && !filters.status.jail && !filters.status.abroad && !filters.status.federal && !filters.status.fallen;
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 || status === "federal" && filters.status.federal || status === "fallen" && filters.status.fallen;
if (!matchesStatus) {
hide_row(row);
continue;
}
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);
if (!matchesLevel) {
hide_row(row);
continue;
}
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);
if (!matchesFF) {
hide_row(row);
continue;
}
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 (!matchesStats) {
hide_row(row);
continue;
}
const lastActionRaw = row.dataset["twseLastActionTimestamp"];
const lastActionTs = lastActionRaw ? Number.parseInt(lastActionRaw, 10) : null;
const hasLastActionData = lastActionTs !== null && !Number.isNaN(lastActionTs) && lastActionTs !== 0;
const lastActionAge = hasLastActionData ? get_current_time_seconds() - lastActionTs : null;
const matchesLastAction = lastActionAge === null || (filters.lastActionMinSec === void 0 || filters.lastActionMinSec === null || lastActionAge >= filters.lastActionMinSec) && (filters.lastActionMaxSec === void 0 || filters.lastActionMaxSec === null || lastActionAge <= filters.lastActionMaxSec);
if (!matchesLastAction) {
hide_row(row);
continue;
}
if (matchesActivity && matchesStatus && matchesLevel && matchesFF && matchesStats && matchesLastAction) {
show_row(row);
} else {
hide_row(row);
}
}
if (filters.sortBy !== "none") {
const isEst = filters.colDisplay === FactionsColDisplay.BATTLE_STATS;
const valKey = isEst ? "estValue" : "ffValue";
rows.sort((a, b) => {
const getVal = (row) => {
const dataVal = row.dataset[valKey];
return dataVal ? Number.parseFloat(dataVal) : -1;
};
const valA = getVal(a);
const valB = getVal(b);
if (filters.sortBy.endsWith("asc")) {
return valA - valB;
}
return valB - valA;
});
for (const row of rows) {
const extra_tt_rows = [];
let next_sibling = row.nextElementSibling;
while (next_sibling && !next_sibling?.classList.contains("table-row") && !next_sibling?.classList.contains("enemy") && !next_sibling?.classList.contains("your")) {
if (next_sibling.classList.contains("tt-last-action") || next_sibling.classList.contains("tt-stats-estimate")) {
extra_tt_rows.push(next_sibling);
}
next_sibling = next_sibling.nextElementSibling;
}
tbody.appendChild(row);
for (const r of extra_tt_rows) {
tbody.appendChild(r);
}
}
}
if (is_filter_active(filters)) {
tbody.setAttribute("data-ffscouter-active-filter", "true");
} else {
tbody.removeAttribute("data-ffscouter-active-filter");
}
} finally {
isApplying.set(membersList, false);
}
}
function update_header_sort_indicator(list, sortBy) {
const header = list.querySelector(".ffscouter-header");
if (!header) return;
if (sortBy === "none") {
header.removeAttribute("data-ffscouter-sort");
header.querySelector(".ffscouter-sort-icon")?.remove();
return;
}
header.setAttribute(
"data-ffscouter-sort",
sortBy === "ff-asc" ? "asc" : "desc"
);
const classes = detect_sort_icon_classes(list);
if (!classes) return;
if (classes.tab) header.classList.add(classes.tab);
let icon = header.querySelector(".ffscouter-sort-icon");
if (!icon) {
icon = document.createElement("div");
icon.classList.add("ffscouter-sort-icon", classes.sortIcon);
if (classes.activeIcon) icon.classList.add(classes.activeIcon);
header.appendChild(icon);
}
icon.classList.remove(classes.asc, classes.desc);
icon.classList.add(sortBy === "ff-asc" ? classes.asc : classes.desc);
}
function is_tt_extra_row(el) {
return el.classList.contains("tt-last-action") || el.classList.contains("tt-stats-estimate");
}
function toggle_tt_siblings(row, hidden) {
let next = row.nextElementSibling;
while (next && !next.classList.contains("table-row") && !next.classList.contains("enemy") && !next.classList.contains("your")) {
if (is_tt_extra_row(next)) {
if (hidden) {
next.setAttribute("data-ffscouter-hidden", "");
} else {
next.removeAttribute("data-ffscouter-hidden");
}
}
next = next.nextElementSibling;
}
}
function hide_row(row) {
row.setAttribute("data-ffscouter-hidden", "");
toggle_tt_siblings(row, true);
}
function show_row(row) {
row.removeAttribute("data-ffscouter-hidden");
toggle_tt_siblings(row, false);
}
function is_filter_active(filters) {
if (filters.sortBy !== "none") return true;
return false;
}
const log$b = logger.child("feature:faction");
async function poll_traveling_flights(membersList) {
const rows = Array.from(
membersList.querySelectorAll(".enemy, .your")
);
const travelingPlayers = rows.map((row) => {
const player_id = get_player_id_in_element(row);
if (!player_id) return null;
const statusCell = row.querySelector(".status");
const statusText = statusCell?.textContent?.trim() || "";
const isTraveling = statusText === "Traveling";
return { row, player_id, isTraveling };
}).filter((item) => item !== null);
for (const p of travelingPlayers) {
if (!p.isTraveling) {
p.row.removeAttribute("data-earliest-arrival");
p.row.removeAttribute("data-latest-arrival");
ffscouter.clear_flight_cache(p.player_id);
}
}
const traveling = travelingPlayers.filter((p) => p.isTraveling);
if (traveling.length === 0) return;
const isPremium = await check_key_status.is_premium();
if (!isPremium) {
for (const p of traveling) {
p.row.removeAttribute("data-earliest-arrival");
p.row.removeAttribute("data-latest-arrival");
}
return;
}
await Promise.all(
traveling.map(async (p) => {
try {
const flights = await ffscouter.get_flights(p.player_id);
const current = flights?.current;
if (current) {
const earliest = current.earliest_arrival_time;
const latest = current.latest_arrival_time;
p.row.setAttribute("data-earliest-arrival", String(earliest));
p.row.setAttribute("data-latest-arrival", String(latest));
} else {
p.row.removeAttribute("data-earliest-arrival");
p.row.removeAttribute("data-latest-arrival");
}
} catch (err) {
log$b.error(`Failed to fetch flights for player ${p.player_id}`, err);
}
})
);
}
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 ? ffconfig.war_col_display : ffconfig.factions_col_display;
const isEst = colDisplay === FactionsColDisplay.BATTLE_STATS;
const isNone = colDisplay === FactionsColDisplay.NONE;
const expectedText = isEst ? "Est" : "FF";
const factionWar = membersList.closest(".faction-war");
if (factionWar) {
factionWar.setAttribute("data-ffscouter-col-display", colDisplay);
}
let headerLi = membersList.querySelector(
".ffscouter-header"
);
if (isNone) {
if (headerLi) {
headerLi.remove();
}
} else {
if (headerLi && (isWar && headerLi.tagName !== "DIV" || !isWar && headerLi.tagName !== "LI")) {
headerLi.remove();
headerLi = null;
}
if (!headerLi) {
if (isWar) {
const headerLvlEl = membersList.querySelector(
".white-grad > .level"
);
if (headerLvlEl) {
headerLi = document.createElement("div");
headerLi.classList.add("left", "level", "ffscouter-header");
headerLvlEl.after(headerLi);
}
} else {
headerLi = document.createElement("li");
headerLi.tabIndex = 0;
headerLi.classList.add(
"table-cell",
"lvl",
"torn-divider",
"divider-vertical",
"ffscouter-header"
);
headerLvl.after(headerLi);
}
}
if (headerLi && 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((p) => p.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");
if (isWar) {
cell.classList.add("left", "level", "ffscouter-cell");
const levelEl = rp.row.querySelector(".level, [class*='level__']");
if (levelEl) {
levelEl.after(cell);
} else {
rp.memberDiv.after(cell);
}
} else {
cell.classList.add("table-cell", "lvl", "ffscouter-cell");
const rowLvl = rp.row.querySelector(".lvl");
if (rowLvl) {
rowLvl.after(cell);
} else {
rp.memberDiv.after(cell);
}
}
}
cell.style.cursor = "pointer";
cell.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
const forceNewTab = e.ctrlKey || e.metaKey || e.button === 1;
open_attack_link(rp.player_id, {
openInNewTab: forceNewTab ? true : void 0
});
};
}
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 boxEl = (membersList.closest(".faction-war") || membersList.parentNode)?.querySelector("[data-ff-filter-box]");
const handle = getFilterBoxHandle(boxEl);
if (handle?.activity) {
apply_filters_and_sort(membersList, {
...handle.getFilterSnapshot(),
colDisplay
});
}
update_header_sort_indicator(membersList, handle?.sortBy ?? "none");
poll_traveling_flights(membersList);
}
const log$a = logger.child("feature:faction");
const FEATURE_NAME$4 = "faction";
function cleanup_when_detached(el, dispose) {
const cleanupInterval = setInterval(() => {
if (!el.isConnected) {
clearInterval(cleanupInterval);
dispose();
}
}, 1e4);
}
function run_when_ready(root, isReady, onReady) {
if (isReady()) {
onReady();
return;
}
const loadObserver = new MutationObserver((_mutations, obs) => {
if (isReady()) {
obs.disconnect();
onReady();
}
});
loadObserver.observe(root, { childList: true, subtree: true });
cleanup_when_detached(root, () => loadObserver.disconnect());
}
function mountFilterBox(mode, onFilterChange) {
const container = document.createElement("div");
container.style.display = "contents";
container.setAttribute("data-ff-filter-box", "true");
container.setAttribute("data-mode", mode);
const ref = { current: null };
let ready = false;
let hasLastActionDataProp = false;
const pending = [];
const runOrBuffer = (fn) => {
if (ready && ref.current) {
fn(ref.current);
} else {
pending.push(fn);
}
};
const onReady = () => {
ready = true;
if (ref.current && pending.length > 0) {
for (const fn of pending) fn(ref.current);
pending.length = 0;
}
};
const handle = {
get ready() {
return ready;
},
get sortBy() {
return ready ? ref.current?.sortBy ?? "none" : "none";
},
get activity() {
return ready && ref.current ? ref.current.activity : { online: true, idle: true, offline: true };
},
get hasLastActionData() {
return ready && ref.current ? ref.current.hasLastActionData : hasLastActionDataProp;
},
setSortBy: (val) => runOrBuffer((h2) => h2.setSortBy(val)),
getFilterSnapshot: () => ready && ref.current ? ref.current.getFilterSnapshot() : {
sortBy: "none",
filterEnabled: true,
activity: { online: true, idle: true, offline: true },
status: {
okay: true,
traveling: true,
hospital: true,
jail: true,
abroad: true,
federal: true,
fallen: true
},
levelMin: null,
levelMax: null,
ffMin: null,
ffMax: null,
statsMin: null,
statsMax: null,
lastActionMinSec: null,
lastActionMaxSec: null
},
setHasLastActionData: (val) => {
hasLastActionDataProp = val;
runOrBuffer((h2) => h2.setHasLastActionData(val));
},
setFilterState: (patch) => runOrBuffer((h2) => h2.setFilterState(patch)),
dispatchChange: () => runOrBuffer((h2) => h2.dispatchChange())
};
container.__ffHandle = handle;
createRoot(container).render(
createElement(FFFactionFilterBox, {
mode,
onFilterChange,
ref,
initialHasLastActionData: hasLastActionDataProp,
onReady
})
);
return container;
}
function update_last_action_visibility(list) {
const scope = list.closest(".faction-war") || list;
const boxEl = (list.closest(".faction-war") || list.parentNode)?.querySelector("[data-ff-filter-box]");
const handle = getFilterBoxHandle(boxEl);
if (!handle) return;
handle.setHasLastActionData(
!!scope.querySelector("[data-twse-last-action-timestamp]")
);
}
function setup_reapply_watcher(list, observeTarget, getColDisplay) {
update_last_action_visibility(list);
let rafPending = false;
const attributeObserver = new MutationObserver((mutations) => {
if (is_applying(list)) return;
let shouldReapply = false;
for (const m2 of mutations) {
if (m2.type === "attributes") {
if (m2.attributeName === "aria-label" && m2.target instanceof HTMLElement && m2.target.closest('[class*="userStatusWrap"]')) {
shouldReapply = true;
break;
}
if (m2.attributeName === "class" && m2.target instanceof HTMLElement && m2.target.closest(".status")) {
shouldReapply = true;
break;
}
if (m2.attributeName === "data-twse-last-action-timestamp") {
shouldReapply = true;
break;
}
}
}
if (shouldReapply && !rafPending) {
rafPending = true;
requestAnimationFrame(() => {
rafPending = false;
update_last_action_visibility(list);
const boxEl = (list.closest(".faction-war") || list.parentNode)?.querySelector("[data-ff-filter-box]");
const handle = getFilterBoxHandle(boxEl);
if (handle?.activity) {
apply_filters_and_sort(list, {
...handle.getFilterSnapshot(),
colDisplay: getColDisplay()
});
}
});
}
});
attributeObserver.observe(observeTarget, {
attributes: true,
attributeFilter: ["class", "aria-label", "data-twse-last-action-timestamp"],
subtree: true
});
const flightInterval = setInterval(() => {
poll_traveling_flights(list);
update_last_action_visibility(list);
}, 3e4);
cleanup_when_detached(list, () => {
clearInterval(flightInterval);
attributeObserver.disconnect();
});
}
function inject_filter_box(membersList) {
const parent = membersList.parentNode;
if (!parent) return;
if (parent.querySelector("[data-ff-filter-box]")) return;
const container = mountFilterBox("faction", (snapshot) => {
apply_filters_and_sort(membersList, {
...snapshot,
colDisplay: ffconfig.factions_col_display
});
update_header_sort_indicator(membersList, snapshot.sortBy);
});
parent.insertBefore(container, membersList);
}
function initialize_features(membersList) {
if (membersList.hasAttribute("data-ffscouter-initialized")) return;
membersList.setAttribute("data-ffscouter-initialized", "true");
inject_filter_box(membersList);
setup_header_click(membersList, ".table-header", "[role='button']");
apply_ff_columns(membersList);
const target = membersList.querySelector(".table-body") || membersList;
setup_reapply_watcher(
membersList,
target,
() => ffconfig.factions_col_display
);
}
function setup_faction_features(membersList) {
run_when_ready(
membersList,
() => !!membersList.querySelector(".table-body")?.querySelector(".table-row"),
() => initialize_features(membersList)
);
}
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 });
cleanup_when_detached(root, () => loadObserver.disconnect());
}
};
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;
}
apply_ff_gauge_selector(
root.querySelectorAll(".honor-text-wrap"),
FEATURE_NAME$4
);
apply_ff_gauge_selector(root.querySelectorAll(".member"), FEATURE_NAME$4);
for (const l of root.querySelectorAll(".members-list, .chain-attacks-list")) {
if (l instanceof HTMLElement) {
apply_ff_members_list(l);
}
}
};
function setup_header_click(list, headerAreaSelector, nativeTabSelector) {
if (list.hasAttribute("data-ffscouter-header-click")) return;
list.setAttribute("data-ffscouter-header-click", "true");
list.addEventListener(
"click",
(e) => {
const target = e.target;
if (!target.closest(headerAreaSelector)) return;
const scope = list.closest(".faction-war") ?? list.parentElement;
const handle = getFilterBoxHandle(
scope?.querySelector("[data-ff-filter-box]")
);
if (!handle) return;
if (target.closest(".ffscouter-header")) {
e.preventDefault();
e.stopPropagation();
const newSort = handle.sortBy === "ff-desc" ? "ff-asc" : "ff-desc";
handle.setSortBy(newSort);
} else if (target.closest(nativeTabSelector)) {
if (handle.sortBy !== "none") {
handle.setSortBy("none");
}
}
},
{ capture: true }
);
}
function setup_war_features(factionWar) {
run_when_ready(
factionWar,
() => factionWar.querySelectorAll(".enemy-faction, .your-faction").length > 0,
() => initialize_war_features(
factionWar,
Array.from(
factionWar.querySelectorAll(".enemy-faction, .your-faction")
)
)
);
}
function initialize_war_features(factionWar, lists) {
if (factionWar.hasAttribute("data-ffscouter-initialized")) return;
factionWar.setAttribute("data-ffscouter-initialized", "true");
if (!factionWar.querySelector("[data-ff-filter-box][data-mode='war']")) {
const container = mountFilterBox("war", (snapshot) => {
const currentLists = Array.from(
factionWar.querySelectorAll(".enemy-faction, .your-faction")
);
const colDisplay = ffconfig.war_col_display;
for (const list of currentLists) {
apply_filters_and_sort(list, { ...snapshot, colDisplay });
update_header_sort_indicator(list, snapshot.sortBy);
}
});
factionWar.insertBefore(container, factionWar.firstChild);
}
for (const list of lists) {
setup_war_list(list);
}
}
function setup_war_list(list) {
run_when_ready(
list,
() => !!list.querySelector(".enemy, .your"),
() => initialize_war_list(list)
);
}
function initialize_war_list(list) {
setup_header_click(list, ".white-grad", "[class*='tab___']");
apply_ff_columns(list);
setup_reapply_watcher(list, list, () => ffconfig.war_col_display);
}
const process_page = () => {
wait_for_element(".members-list", 1e4).then((node) => {
if (node instanceof HTMLElement) {
log$a.debug("Found members-list!");
monitor_member_list(node);
}
});
wait_for_element(".chain-attacks-list", 1e4).then((node) => {
if (node instanceof HTMLElement) {
log$a.debug("Found chain-attacks-list!");
monitor_member_list(node, true);
}
});
wait_for_element("#faction_war_list_id", 1e4).then(async (node) => {
if (!node) {
return;
}
log$a.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")) {
log$a.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 });
log$a.debug(
`Set up descriptions observer on <${node.tagName.toLowerCase()}> .${[...node.classList].join(".")}`
);
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);
}
}
});
};
function should_run_faction() {
if (torn_page("factions", { step: "profile" })) {
return true;
}
if (torn_page("factions", { step: "your" })) {
if (window.location.hash === "" || window.location.hash === "#" || window.location.hash === "#/" || window.location.hash.startsWith("#/war/") || window.location.hash === "#/tab=info") {
return true;
}
}
return false;
}
const index$c = {
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");
},
async run() {
on_navigation(() => {
if (should_run_faction()) {
process_page();
}
});
window.addEventListener("ff-config-updated", () => {
if (should_run_faction()) {
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);
}
}
}
});
if (should_run_faction()) {
process_page();
}
}
};
const __vite_glob_0_2 = Object.freeze( Object.defineProperty({
__proto__: null,
default: index$c,
getFilterBoxHandle,
initialize_features,
setup_war_features,
should_run_faction
}, Symbol.toStringTag, { value: "Module" }));
const log$9 = logger.child("feature:fallback");
const FEATURE_NAME_HONOR_BAR = "fallback-honor-bar";
const FEATURE_NAME_USER_NAME = "fallback-user-name";
const FEATURE_NAME$3 = "fallback";
function is_excluded_page() {
switch (true) {
case torn_page("gym"):
case torn_page("item"):
case torn_page("city"):
case torn_page("casino"):
case torn_page("calendar"):
case torn_page("preferences"):
case torn_page("estateagents"):
case torn_page("profiles"):
case torn_page("pc"):
case torn_page("citystats"):
case torn_page("usersonline"):
case torn_page("displaycase"):
case torn_page("bank"):
case torn_page("loan"):
case torn_page("donator"):
case torn_page("token_shop"):
case torn_page("freebies"):
case torn_page("bigalgunshop"):
case torn_page("shops"):
case torn_page("joblisting"):
case torn_page("messageinc"):
case torn_page("comics"):
case torn_page("archives"):
case torn_page("rules"):
case torn_page("credits"):
case torn_page("committee"):
case torn_page("church"):
case torn_page("christmas_town"):
case torn_page("index", { page: "hunting" }):
case torn_page("index", { page: "bank" }):
case torn_page("page", { sid: "slotsLastRolls" }):
case torn_page("page", { sid: "rouletteLastSpins" }):
case torn_page("page", { sid: "highlowLastGames" }):
case torn_page("page", { sid: "kenoLastGames" }):
case torn_page("page", { sid: "crapsLastRolls" }):
case torn_page("page", { sid: "blackjackLastGames" }):
case torn_page("page", { sid: "spinTheWheelLastSpins" }):
case torn_page("page", { sid: "bunker" }):
case torn_page("page", { sid: "points" }):
case torn_page("page", { sid: "itemsMods" }):
case torn_page("page", { sid: "keepsakes" }):
case torn_page("page", { sid: "ammo" }):
case torn_page("page", { sid: "awards" }):
case torn_page("page", { sid: "log" }):
case torn_page("page", { sid: "events" }):
case torn_page("page", { sid: "crimes" }):
case torn_page("page", { sid: "crimesRecord" }):
case torn_page("page", { sid: "factionWarfare" }):
case torn_page("page", { sid: "travel" }):
case torn_page("page", { sid: "missions" }):
case torn_page("page", { sid: "stocks" }):
case torn_page("page", { sid: "slots" }):
case torn_page("page", { sid: "roulette" }):
case torn_page("page", { sid: "highlow" }):
case torn_page("page", { sid: "keno" }):
case torn_page("page", { sid: "craps" }):
case torn_page("page", { sid: "bookie" }):
case torn_page("page", { sid: "blackjack" }):
case torn_page("page", { sid: "spinTheWheel" }):
case torn_page("page", { sid: "education" }):
case torn_page("page", { sid: "itemMarket" }):
return true;
default:
if (torn_page("factions", { step: "your" })) {
const hash = window.location.hash;
if (!(hash.startsWith("#/war/") || hash === "#/tab=info" || hash.startsWith("#/tab=controls"))) {
return true;
}
}
return false;
}
}
async function find_mutation_target() {
const content_wrapper = await wait_for_element(".content-wrapper", 1e4);
if (content_wrapper) {
return content_wrapper;
}
await wait_for_body(1e4);
return document.body;
}
const index$b = {
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 IGNORED_TAGS = new Set([
"SCRIPT",
"STYLE",
"LINK",
"META",
"SVG",
"PATH",
"BR",
"HR",
"HEAD",
"TITLE"
]);
const get_page_selectors = () => {
const href = window.location.href;
let page_specific = [];
if (href.startsWith("https://www.torn.com/companies.php")) {
page_specific = [".employee", ".director"];
} else if (href.startsWith("https://www.torn.com/page.php?sid=competition#/team")) {
page_specific = ['[class*="name__"]'];
} else if (href.startsWith("https://www.torn.com/joblist.php")) {
page_specific = [".employee", ".director"];
} else if (torn_page("messages") || torn_page("index") || torn_page("hospitalview") || torn_page("page", { sid: "UserList" })) {
page_specific = [".name"];
} else if (href.startsWith("https://www.torn.com/bounties.php")) {
page_specific = [".target, .listed"];
} else if (href.startsWith("https://www.torn.com/page.php?sid=attackLog")) {
page_specific = ["ul.participants-list li"];
} else if (href.startsWith("https://www.torn.com/forums.php")) {
page_specific = [".last-poster, .starter, .last-post, .poster"];
} else if (href.includes("page.php?sid=hof") || torn_page("factions", { step: "profile" }) || torn_page("factions", { step: "your" }, [
"",
"#",
"#/",
"#/tab=info",
"#/war/*"
])) {
page_specific = ['[class*="userInfoBox__"]'];
}
if (page_specific.length > 0) {
const combined = [".honor-text-wrap", ...page_specific].join(", ");
return {
has_page_specific: true,
page_specific_selectors: page_specific,
combined_selector: combined
};
}
return {
has_page_specific: false,
page_specific_selectors: [],
combined_selector: ".honor-text-wrap, .user.name"
};
};
let current_config = get_page_selectors();
let is_observing = false;
const check_mutation = async (node) => {
if (!node.querySelectorAll || IGNORED_TAGS.has(node.tagName)) {
return;
}
if (!node.matches(current_config.combined_selector) && !node.querySelector(current_config.combined_selector)) {
return;
}
const honor_bars = node.querySelectorAll(
".honor-text-wrap"
);
if (honor_bars.length > 0) {
apply_ff_gauge_selector(honor_bars, FEATURE_NAME_HONOR_BAR);
} else if (current_config.has_page_specific) {
for (const selector of current_config.page_specific_selectors) {
apply_ff_gauge_selector(
node.querySelectorAll(selector),
FEATURE_NAME$3
);
}
} else {
const name_elems = node.querySelectorAll(
".user.name"
);
if (name_elems.length > 0) {
apply_ff_gauge_selector(name_elems, FEATURE_NAME_USER_NAME);
}
}
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);
}
}
}
});
const update_observer_state = async () => {
const excluded = is_excluded_page();
if (excluded) {
if (is_observing) {
ff_gauge_observer.disconnect();
is_observing = false;
log$9.debug("Disconnected fallback MutationObserver (excluded page)");
}
} else {
current_config = get_page_selectors();
if (!is_observing) {
const target = await find_mutation_target();
ff_gauge_observer.observe(target, {
attributes: false,
childList: true,
characterData: false,
subtree: true
});
is_observing = true;
log$9.debug("Connected fallback MutationObserver (included page)");
if (target) {
check_mutation(target);
}
}
}
};
on_navigation(() => {
log$9.debug("Navigation detected, re-evaluating fallback observer state");
update_observer_state();
});
update_observer_state();
},
httpIntercept: {
before(_url, _init) {
return void 0;
},
after(_bodyText, _response, _ctx) {
return void 0;
}
}
};
const __vite_glob_0_3 = Object.freeze( Object.defineProperty({
__proto__: null,
default: index$b
}, Symbol.toStringTag, { value: "Module" }));
const log$8 = logger.child("ui");
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("button");
closeBtn.textContent = "×";
closeBtn.style.cursor = "pointer";
closeBtn.style.marginLeft = "8px";
closeBtn.style.fontWeight = "bold";
closeBtn.style.fontSize = "18px";
closeBtn.style.background = "none";
closeBtn.style.border = "none";
closeBtn.style.color = "inherit";
closeBtn.style.padding = "0";
closeBtn.style.lineHeight = "1";
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}`;
}
log$8.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 log$7 = logger.child("feature:ff-button");
const CACHE_LIFETIME_MS = 7 * 24 * 60 * 60 * 1e3;
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(a, b) {
return a.minlevel !== b.minlevel || a.maxlevel !== b.maxlevel || a.minff !== b.minff || a.maxff !== b.maxff || a.inactive !== b.inactive || a.factionless !== b.factionless;
}
async function update_ff_targets(force = false) {
const key = ffconfig.key;
if (!key) {
log$7.debug("API key not set, skipping target fetch");
return;
}
const currentFilters = get_active_filters();
const cached = ffconfig.chain_targets;
const hasNoCacheOrExpired = !cached || Date.now() > cached.expiry;
const filtersChanged = cached && filters_changed(cached.filters, currentFilters);
const timeToRefresh = cached && (!cached.last_updated || Date.now() - cached.last_updated > POLL_INTERVAL_MS);
if (!force && !hasNoCacheOrExpired && !filtersChanged && !timeToRefresh) {
log$7.debug(
"Using cached targets, not expired, filters match, and not time to poll yet"
);
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() + CACHE_LIFETIME_MS,
last_updated: Date.now(),
filters: currentFilters
};
ffconfig.chain_target_index = 0;
log$7.info(
`Chain targets updated successfully: ${response.targets.length} targets found`
);
}
} catch (err) {
log$7.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 index2 = get_next_target_index(cached.targets.length);
return cached.targets[index2] ?? null;
}
function remove_chain_button() {
const button = document.getElementById("ff-scouter-chain-btn");
if (button) {
button.remove();
}
}
function update_anchor_attributes(anchor) {
const cached = ffconfig.chain_targets;
if (!cached || !cached.targets || cached.targets.length === 0) {
anchor.href = "#";
anchor.removeAttribute("target");
return;
}
const idx = ffconfig.chain_target_index;
const currentTarget = cached.targets[idx < cached.targets.length ? idx : 0];
if (!currentTarget) {
anchor.href = "#";
anchor.removeAttribute("target");
return;
}
const linkType = ffconfig.chain_link_type;
anchor.href = linkType === "profile" ? `https://www.torn.com/profiles.php?XID=${currentTarget.player_id}` : get_attack_url(currentTarget.player_id);
anchor.target = ffconfig.chain_tab_type === "sametab" ? "_self" : "_blank";
}
function create_chain_button() {
if (!ffconfig.chain_button_enabled || !ffconfig.key) {
remove_chain_button();
return;
}
const existing = document.getElementById(
"ff-scouter-chain-btn"
);
if (existing) {
update_anchor_attributes(existing);
return;
}
const anchor = document.createElement("a");
anchor.id = "ff-scouter-chain-btn";
anchor.innerHTML = "FF";
anchor.style.position = "fixed";
anchor.style.top = "32%";
anchor.style.right = "0%";
anchor.style.zIndex = "9999";
anchor.style.backgroundColor = "green";
anchor.style.color = "white";
anchor.style.border = "none";
anchor.style.padding = "6px";
anchor.style.borderRadius = "6px";
anchor.style.cursor = "pointer";
anchor.style.display = "block";
anchor.style.textDecoration = "none";
update_anchor_attributes(anchor);
const handler = async (e) => {
if (e instanceof KeyboardEvent) {
if (e.key !== "Enter") {
return;
}
} else if (e instanceof MouseEvent) {
if (e.button !== 0 && e.button !== 1 && e.button !== 2) {
return;
}
}
const cached = ffconfig.chain_targets;
if (!cached || !cached.targets || cached.targets.length === 0) {
e.preventDefault();
const isPrimary = e instanceof MouseEvent && e.button === 0 || e instanceof KeyboardEvent;
if (isPrimary) {
toast("No cached targets found. Fetching...", TOAST_LEVEL.WARNING);
update_ff_targets(true).then(() => {
const newCached = ffconfig.chain_targets;
if (!newCached || !newCached.targets || newCached.targets.length === 0) {
toast(
"No targets available matching your criteria.",
TOAST_LEVEL.ERROR
);
return;
}
update_anchor_attributes(anchor);
toast("Targets loaded. Click to navigate!", TOAST_LEVEL.INFO);
});
}
return;
}
get_next_target_index(cached.targets.length);
update_anchor_attributes(anchor);
};
anchor.addEventListener("mousedown", handler);
anchor.addEventListener("keydown", handler);
document.body.appendChild(anchor);
}
const index$a = {
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().then(() => {
const button = document.getElementById(
"ff-scouter-chain-btn"
);
if (button) {
update_anchor_attributes(button);
}
});
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)) {
log$7.info("Target filters changed, refetching targets immediately");
await update_ff_targets(true);
}
create_chain_button();
});
}
};
const __vite_glob_0_4 = Object.freeze( Object.defineProperty({
__proto__: null,
CACHE_LIFETIME_MS,
POLL_INTERVAL_MS,
create_chain_button,
default: index$a,
filters_changed,
get_active_filters,
get_next_target_index,
get_random_chain_target,
remove_chain_button,
update_anchor_attributes,
update_ff_targets
}, Symbol.toStringTag, { value: "Module" }));
const FEATURE_NAME$2 = "item_market";
const log$6 = logger.child(`feature:${FEATURE_NAME$2}`);
const index$9 = {
name: "Item market FF display",
description: "Shows FF on the item market page",
executionTime: StartTime.DocumentBody,
async shouldRun() {
return torn_page("page", { sid: "ItemMarket" });
},
async run() {
const root = await wait_for_element('[class*="marketWrapper__"', 1e4);
if (!root) {
return;
}
log$6.info("Found item list wrapper!");
log$6.debug("Root element:", root);
const process = () => {
apply_ff_gauge_selector(
root.querySelectorAll(
"div.bazaar-listing-card div:first-child div:first-child > a"
),
FEATURE_NAME$2
);
apply_ff_gauge_selector(
root.querySelectorAll(".bazaar-card a"),
FEATURE_NAME$2
);
apply_ff_gauge_selector(
root.querySelectorAll(".bazaar-card .bazaar-card-name"),
FEATURE_NAME$2
);
apply_ff_gauge_selector(
root.querySelectorAll(".honor-text-wrap"),
FEATURE_NAME$2
);
apply_ff_gauge_selector(
root.querySelectorAll('[class*="userInfoWrapper__"]'),
FEATURE_NAME$2
);
};
const observer = new MutationObserver(process);
observer.observe(root, {
childList: true,
subtree: true
});
},
httpIntercept: {
before(_url, _init) {
return void 0;
},
after(_bodyText, _response, _ctx) {
return void 0;
}
}
};
const __vite_glob_0_5 = Object.freeze( Object.defineProperty({
__proto__: null,
default: index$9
}, Symbol.toStringTag, { value: "Module" }));
const log$5 = logger.child("ui");
const PREMIUM_UPGRADE_URL = "https://ffscouter.com/premium";
function format_duration_human(totalSeconds, compact) {
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`);
if (compact) {
return parts.join("");
}
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}`;
}
function FFFlightProfileStatus({ playerId, compact = false }) {
const [data, setData] = useState(null);
const [isPremium, setIsPremium] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [currentTimeSeconds, setCurrentTimeSeconds] = useState(
get_current_time_seconds
);
const [prevPlayerId, setPrevPlayerId] = useState(playerId);
if (playerId !== prevPlayerId) {
setPrevPlayerId(playerId);
setData(null);
setError(null);
setLoading(true);
setIsPremium(null);
}
const dataRef = useRef(null);
dataRef.current = data;
useEffect(() => {
if (!playerId) return;
let active = true;
const fetchData = async () => {
if (!active) return;
let isPremiumResult;
try {
isPremiumResult = await check_key_status.is_premium();
} catch (err) {
log$5.error("Failed to check premium status", err);
if (active) setLoading(false);
return;
}
if (!active) return;
setIsPremium(isPremiumResult);
if (!isPremiumResult) {
setLoading(false);
return;
}
try {
const result = await ffscouter.get_flights(playerId);
if (!active) return;
setData(result);
setError(null);
} catch (err) {
if (!active) return;
log$5.error("Failed to fetch flight data", err);
if (err instanceof FFApiError) {
const code = err.ff_api_error?.code;
if (code === 19) {
setIsPremium(false);
} else if (code === 2 || code === 10 || code === 12) {
setError("Invalid API key");
} else if (code === 20) {
setError("Rate limit exceeded. Retrying...");
} else {
setError(
err.ff_api_error?.error ?? err.message ?? "Flight tracking unavailable"
);
}
} else {
setError(
err instanceof Error ? err.message : "Flight tracking unavailable"
);
}
} finally {
if (active) setLoading(false);
}
};
fetchData();
const tick = setInterval(() => {
setCurrentTimeSeconds(get_current_time_seconds());
const d2 = dataRef.current;
if (d2?.rechecking && d2.next_retry_at && Date.now() >= d2.next_retry_at) {
fetchData();
}
}, 1e3);
const fetchInterval = setInterval(() => {
if (!dataRef.current?.rechecking) {
fetchData();
}
}, 3e4);
return () => {
active = false;
clearInterval(tick);
clearInterval(fetchInterval);
};
}, [playerId]);
if (!playerId) return null;
if (isPremium === false) {
return jsx("div", { className: "ff-scouter-profile-flight-info", children: jsx(
"a",
{
href: PREMIUM_UPGRADE_URL,
target: "_blank",
rel: "noopener noreferrer",
style: { fontWeight: "bold", textDecoration: "underline" },
children: "Upgrade to FFScouter Flight Tracking"
}
) });
}
let content;
if (error) {
content = jsxs("span", { style: { color: "#ff6b6b" }, children: [
"Error: ",
error
] });
} else if (loading && !data) {
content = compact ? "Estimating..." : "Landing: estimating...";
} else {
const current = data?.current;
if (data?.rechecking) {
const next = data.next_retry_at ?? 0;
const seconds = Math.max(0, Math.ceil((next - Date.now()) / 1e3));
content = compact ? jsxs(Fragment, { children: [
"No data.",
jsx("br", {}),
"Rechecking..."
] }) : `No data. Rechecking in ${seconds} seconds.`;
} else if (!current || !current.earliest_arrival_time && !current.latest_arrival_time) {
content = compact ? "Unavailable" : "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 = compact ? "Unavailable" : "Landing: unavailable for current route";
} else {
const earliestRemaining = earliest - currentTimeSeconds;
const latestRemaining = latest - currentTimeSeconds;
const earliestTct = format_tct_time(earliest);
const latestTct = format_tct_time(latest);
if (latestRemaining <= -5 * 60) {
content = compact ? "Late" : jsxs(Fragment, { children: [
"Landing: Late, probably flight delayed.",
jsx("br", {}),
"(",
latestTct,
" TCT latest)"
] });
} else if (latestRemaining <= 0) {
content = compact ? jsxs(Fragment, { children: [
"Just landed",
jsx("br", {}),
"(Latest: ",
latestTct,
" TCT)"
] }) : jsxs(Fragment, { children: [
"Landing: just landed",
jsx("br", {}),
"(",
latestTct,
" TCT latest)"
] });
} else if (earliestRemaining <= 0) {
content = compact ? jsxs(Fragment, { children: [
"Imminent",
jsx("br", {}),
format_duration_human(latestRemaining, compact),
jsx("br", {})
] }) : jsxs(Fragment, { children: [
"Landing: imminent -",
" ",
format_duration_human(latestRemaining, compact),
jsx("br", {}),
"(Latest: ",
latestTct,
" TCT)"
] });
} else {
content = compact ? jsxs(Fragment, { children: [
format_duration_human(earliestRemaining, compact),
jsx("br", {}),
format_duration_human(latestRemaining, compact)
] }) : jsxs(Fragment, { children: [
"Landing: ",
format_duration_human(earliestRemaining, compact),
" -",
" ",
format_duration_human(latestRemaining, compact),
jsx("br", {}),
"(",
earliestTct,
" - ",
latestTct,
" TCT)"
] });
}
}
}
}
return jsx("div", { className: "ff-scouter-profile-flight-info", children: content });
}
const log$4 = logger.child("feature:mini-profile-flights");
const FLIGHT_CONTAINER_CLASS = "ff-flight-element";
function is_flying$1(status) {
return status.classList.contains("travelling");
}
const monitor_mini_profile_root$1 = () => {
const miniprofile = document.querySelector("#profile-mini-root");
if (miniprofile) {
log$4.debug("profile-mini-root already exists.");
setup_mini_flight_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_flight_observer();
mini_body_observer.disconnect();
}
}
}
});
mini_body_observer.observe(document.body, { childList: true });
};
const setup_mini_flight_observer = async () => {
const miniroot = document.querySelector("#profile-mini-root");
if (!miniroot) {
return;
}
const container = document.createElement("div");
container.classList.add(FLIGHT_CONTAINER_CLASS);
let root = null;
let lastPlayerId = null;
const renderForPlayer = (player_id2) => {
if (!root) {
root = createRoot(container);
}
root.render(
createElement(FFFlightProfileStatus, {
playerId: player_id2,
compact: true
})
);
};
const mp_observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (Array.from(mutation.addedNodes).some(
(node) => node instanceof HTMLElement && (node.classList.contains(FLIGHT_CONTAINER_CLASS) || node.classList.contains("ff-scouter-profile-flight-info"))
)) {
return;
}
}
const player_id2 = get_player_id_in_element(miniroot);
if (!player_id2) {
lastPlayerId = null;
return;
}
if (player_id2 === lastPlayerId) {
return;
}
lastPlayerId = player_id2;
const status2 = miniroot.querySelector(".profile-container");
if (!status2) {
return;
}
renderForPlayer(player_id2);
if (is_flying$1(status2)) {
const description = status2.querySelector(".description");
if (description && !description.contains(container)) {
log$4.debug(
`Player ${player_id2} is flying, adding flight tracker to mini-profile`
);
description.appendChild(container);
}
} else {
container.remove();
ffscouter.clear_flight_cache(player_id2);
}
});
mp_observer.observe(miniroot, { childList: true, subtree: true });
const player_id = get_player_id_in_element(miniroot);
const status = miniroot.querySelector(".profile-status");
if (player_id && status) {
renderForPlayer(player_id);
lastPlayerId = player_id;
if (is_flying$1(status)) {
const description = status.querySelector(".description");
if (description && !description.contains(container)) {
description.appendChild(container);
}
}
}
};
const index$8 = {
name: "Mini profile flight tracking",
description: "Display flight estimates on player mini-profiles if they're flying",
executionTime: StartTime.DocumentBody,
async shouldRun() {
return true;
},
async run() {
monitor_mini_profile_root$1();
log$4.debug("mini-profile-flights installed");
},
httpIntercept: {
before(_url, _init) {
return void 0;
},
after(_bodyText, _response, _ctx) {
return void 0;
}
}
};
const __vite_glob_0_6 = Object.freeze( Object.defineProperty({
__proto__: null,
default: index$8
}, Symbol.toStringTag, { value: "Module" }));
const log$3 = logger.child("feature:mini-profile");
const FEATURE_NAME$1 = "mini-profile";
const monitor_mini_profile_root = () => {
const miniprofile = document.querySelector("#profile-mini-root");
if (miniprofile) {
log$3.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;
}
let lastPlayerId = null;
const mp_observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.target instanceof HTMLElement && mutation.target.classList.contains("ffscouter-gauge")) {
return;
}
if (Array.from(mutation.addedNodes).some(
(node) => node instanceof HTMLElement && node.classList.contains("ffscouter-mini-desc")
)) {
return;
}
}
const player_id = get_player_id_in_element(miniroot);
if (!player_id) {
lastPlayerId = null;
return;
}
if (player_id === lastPlayerId) {
return;
}
lastPlayerId = player_id;
ffscouter.get(player_id).then(async (d2) => {
const current_player_id = get_player_id_in_element(miniroot);
if (current_player_id !== player_id) {
return;
}
if (d2.no_data) {
return;
}
log$3.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$1);
}
miniroot.querySelector(".ffscouter-mini-desc")?.remove();
const ff_string = format_ff_score(d2);
const fresh = format_relative_time(d2.last_updated);
const message = `FF ${ff_string} ${fresh}`;
const lastaction = miniroot.querySelector(".last-action");
const desc = document.createElement("span");
desc.classList.add("ffscouter-mini-desc");
desc.innerText = message;
lastaction?.appendChild(document.createElement("br"));
lastaction?.appendChild(desc);
});
ffscouter.complete();
});
mp_observer.observe(miniroot, { childList: true, subtree: true });
};
const index$7 = {
name: "Fill mini profile",
description: "Add FF data to mini profile",
executionTime: StartTime.DocumentBody,
async shouldRun() {
return true;
},
async run() {
monitor_mini_profile_root();
log$3.debug("mini-profile installed");
},
httpIntercept: {
before(_url, _init) {
return void 0;
},
after(_bodyText, _response, _ctx) {
return void 0;
}
}
};
const __vite_glob_0_7 = Object.freeze( Object.defineProperty({
__proto__: null,
default: index$7
}, Symbol.toStringTag, { value: "Module" }));
function is_flying(status) {
return status.classList.contains("travelling");
}
const index$6 = {
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 container = document.createElement("div");
container.classList.add("ff-flight-element");
createRoot(container).render(
createElement(FFFlightProfileStatus, { playerId: player_id })
);
const check_and_update = () => {
if (is_flying(status)) {
const description = status.querySelector(".description");
if (description === null) {
return;
}
if (!description.contains(container)) {
description.appendChild(container);
}
} else {
container.remove();
ffscouter.clear_flight_cache(player_id);
}
};
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 __vite_glob_0_8 = Object.freeze( Object.defineProperty({
__proto__: null,
default: index$6
}, Symbol.toStringTag, { value: "Module" }));
const index$5 = {
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 (!ffconfig.ff_history_enabled) 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 ff-scouter-history-btn";
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;
}
}
};
const __vite_glob_0_9 = Object.freeze( Object.defineProperty({
__proto__: null,
default: index$5
}, Symbol.toStringTag, { value: "Module" }));
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 index$4 = {
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);
return;
}
const player_id = extract_id_from_url(window.location.href);
if (!player_id) {
return;
}
mountComponent(
createElement(FFHeaderLine, { playerId: player_id }),
info_line
);
inject_info_line(info_line);
},
httpIntercept: {
before(_url, _init) {
return void 0;
},
after(_bodyText, _response, _ctx) {
return void 0;
}
}
};
const __vite_glob_0_10 = Object.freeze( Object.defineProperty({
__proto__: null,
default: index$4
}, Symbol.toStringTag, { value: "Module" }));
const FEATURE_NAME = "rr";
const index$3 = {
name: "Russian Roulette FF display",
description: "Shows FF on the Russian Roulette page",
executionTime: StartTime.DocumentBody,
async shouldRun() {
return torn_page("page", { sid: "russianRoulette" });
},
async run() {
const rows_wrapper = await wait_for_element(
'[class*="rowsWrap__"]',
2e4
);
if (!rows_wrapper || !(rows_wrapper instanceof HTMLElement)) {
return;
}
const row_matcher = (node) => {
const user_info_wrapper = node.querySelector(
'.honor-text-wrap, [class*="userInfoBlock__"]'
);
return user_info_wrapper !== null;
};
const row_handler = (options) => {
if (!options.added) {
return;
}
const honor_bar = options.added.querySelector(".honor-text-wrap");
if (honor_bar) {
apply_ff_gauge(honor_bar, FEATURE_NAME);
return;
}
const user_info_wrapper = options.added.querySelector(
'[class*="userInfoBlock__"]'
);
if (user_info_wrapper) {
apply_ff_gauge(user_info_wrapper, FEATURE_NAME);
return;
}
};
const rows_monitor = new MonitorElements(
row_matcher,
row_handler,
rows_wrapper,
true,
{ added: true },
0
);
rows_monitor.start();
}
};
const __vite_glob_0_11 = Object.freeze( Object.defineProperty({
__proto__: null,
default: index$3
}, Symbol.toStringTag, { value: "Module" }));
const styles = {
"ff-settings-panel__accordion": "_ff-settings-panel__accordion_6bhvd_1",
"ff-settings-panel__accordion--glow": "_ff-settings-panel__accordion--glow_6bhvd_10",
"ff-settings-panel__body": "_ff-settings-panel__body_6bhvd_20",
"ff-settings-panel__input-row": "_ff-settings-panel__input-row_6bhvd_24",
"ff-settings-panel__range-row": "_ff-settings-panel__range-row_6bhvd_32",
"ff-settings-panel__blur": "_ff-settings-panel__blur_6bhvd_38",
"ff-settings-panel__error-msg": "_ff-settings-panel__error-msg_6bhvd_48",
"ff-settings-panel__number": "_ff-settings-panel__number_6bhvd_67",
"ff-settings-panel__api-explanation": "_ff-settings-panel__api-explanation_6bhvd_94",
"ff-settings-panel__premium-badge": "_ff-settings-panel__premium-badge_6bhvd_107",
"ff-settings-panel__premium-badge--enabled": "_ff-settings-panel__premium-badge--enabled_6bhvd_117",
"ff-settings-panel__premium-badge--disabled": "_ff-settings-panel__premium-badge--disabled_6bhvd_121",
"ff-settings-panel__premium-badge--unknown": "_ff-settings-panel__premium-badge--unknown_6bhvd_125",
"ff-settings-panel__section": "_ff-settings-panel__section_6bhvd_138",
"ff-settings-panel__span": "_ff-settings-panel__span_6bhvd_152",
"ff-settings-panel__cell": "_ff-settings-panel__cell_6bhvd_160",
"ff-settings-panel__cell--checkbox": "_ff-settings-panel__cell--checkbox_6bhvd_169",
"ff-settings-panel__api-block": "_ff-settings-panel__api-block_6bhvd_193",
"ff-settings-panel__api-status-row": "_ff-settings-panel__api-status-row_6bhvd_203",
"ff-settings-panel__chain-suboptions": "_ff-settings-panel__chain-suboptions_6bhvd_213",
"ff-settings-panel__chain-wide": "_ff-settings-panel__chain-wide_6bhvd_218",
"ff-settings-panel__group": "_ff-settings-panel__group_6bhvd_242",
"ff-settings-panel__marker-size": "_ff-settings-panel__marker-size_6bhvd_256",
"ff-settings-panel__marker-border-width": "_ff-settings-panel__marker-border-width_6bhvd_257",
"ff-settings-panel__marker-size-controls": "_ff-settings-panel__marker-size-controls_6bhvd_263",
"ff-settings-panel__color-scheme": "_ff-settings-panel__color-scheme_6bhvd_276",
"ff-settings-panel__color-scheme-controls": "_ff-settings-panel__color-scheme-controls_6bhvd_282",
"ff-settings-panel__actions": "_ff-settings-panel__actions_6bhvd_295",
"ff-settings-panel__saved-msg": "_ff-settings-panel__saved-msg_6bhvd_304"
};
const cls = {
accordion: styles["ff-settings-panel__accordion"],
accordionGlow: styles["ff-settings-panel__accordion--glow"],
body: styles["ff-settings-panel__body"],
group: styles["ff-settings-panel__group"],
section: styles["ff-settings-panel__section"],
span: styles["ff-settings-panel__span"],
cell: styles["ff-settings-panel__cell"],
cellCheckbox: styles["ff-settings-panel__cell--checkbox"],
apiExplanation: styles["ff-settings-panel__api-explanation"],
apiBlock: styles["ff-settings-panel__api-block"],
apiStatusRow: styles["ff-settings-panel__api-status-row"],
premiumBadge: styles["ff-settings-panel__premium-badge"],
premiumBadgeEnabled: styles["ff-settings-panel__premium-badge--enabled"],
premiumBadgeDisabled: styles["ff-settings-panel__premium-badge--disabled"],
premiumBadgeUnknown: styles["ff-settings-panel__premium-badge--unknown"],
blur: styles["ff-settings-panel__blur"],
number: styles["ff-settings-panel__number"],
markerSize: styles["ff-settings-panel__marker-size"],
markerBorderWidth: styles["ff-settings-panel__marker-border-width"],
markerSizeControls: styles["ff-settings-panel__marker-size-controls"],
colorScheme: styles["ff-settings-panel__color-scheme"],
colorSchemeControls: styles["ff-settings-panel__color-scheme-controls"],
inputRow: styles["ff-settings-panel__input-row"],
rangeRow: styles["ff-settings-panel__range-row"],
errorMsg: styles["ff-settings-panel__error-msg"],
chainSuboptions: styles["ff-settings-panel__chain-suboptions"],
chainWide: styles["ff-settings-panel__chain-wide"],
actions: styles["ff-settings-panel__actions"],
savedMsg: styles["ff-settings-panel__saved-msg"]
};
const DEFAULT_VALUES = {
apiKey: "",
lowRange: CONFIG_DEFAULTS.low_ff_range,
highRange: CONFIG_DEFAULTS.high_ff_range,
maxRange: CONFIG_DEFAULTS.max_ff_range,
chainButtonEnabled: CONFIG_DEFAULTS.chain_button_enabled,
chainLinkType: CONFIG_DEFAULTS.chain_link_type,
chainTabType: CONFIG_DEFAULTS.chain_tab_type,
chainFFTarget: CONFIG_DEFAULTS.chain_ff_target,
chainMinLevel: CONFIG_DEFAULTS.chain_min_level,
chainMaxLevel: CONFIG_DEFAULTS.chain_max_level,
chainInactive: CONFIG_DEFAULTS.chain_inactive,
chainMinFF: CONFIG_DEFAULTS.chain_min_ff,
chainMaxFF: CONFIG_DEFAULTS.chain_max_ff,
chainFactionless: CONFIG_DEFAULTS.chain_factionless,
ffHistoryEnabled: CONFIG_DEFAULTS.ff_history_enabled,
factionsColDisplay: CONFIG_DEFAULTS.factions_col_display,
warColDisplay: CONFIG_DEFAULTS.war_col_display,
debugLogs: CONFIG_DEFAULTS.debug_logs,
analyticsEnabled: CONFIG_DEFAULTS.analytics_enabled,
networkInterceptionEnabled: CONFIG_DEFAULTS.network_interception_enabled,
gaugeMarkerType: CONFIG_DEFAULTS.gauge_marker_type,
gaugeMarkerScale: CONFIG_DEFAULTS.gauge_marker_scale,
gaugeMarkerBorderWidth: CONFIG_DEFAULTS.gauge_marker_border_width,
colorScheme: CONFIG_DEFAULTS.color_scheme,
warQuickAttackAction: CONFIG_DEFAULTS.war_quick_attack_action,
statusAttackLinksEnabled: CONFIG_DEFAULTS.status_attack_links_enabled,
debugDisablePdaHttp: CONFIG_DEFAULTS.debug_disable_pda_http,
isPremium: null
};
function SettingsPanelComponent({
props,
drafts,
isPremium,
rangeError,
showSavedMessage,
onChange,
onApiKeyBlur,
onVerify,
onSave,
onReset,
onClearCache,
onRendered
}) {
useEffect(() => {
onRendered();
});
const previewColor = get_palette_for_scheme(drafts.colorScheme)[5] ?? "#888888";
return jsxs(
"details",
{
className: `${cls.accordion}${!props.apiKey ? ` ${cls.accordionGlow}` : ""} cont-gray border-round`,
children: [
jsx("summary", { children: "FF Scouter Settings" }),
jsxs("div", { className: cls.body, children: [
jsxs("div", { className: cls.group, children: [
jsx("h4", { children: "API Key & Premium" }),
jsxs("div", { className: cls.section, children: [
jsxs("div", { className: `${cls.apiExplanation} ${cls.span}`, children: [
jsx("strong", { children: "Important:" }),
" You must use the SAME exact API key that you use on",
" ",
jsx("a", { href: "https://ffscouter.com/", target: "_blank", rel: "noreferrer", children: "ffscouter.com" }),
". ",
jsx("br", {}),
jsx("br", {}),
"If you're not sure which API key you used, go to",
" ",
jsx(
"a",
{
href: "https://www.torn.com/preferences.php#tab=api",
target: "_blank",
rel: "noreferrer",
children: "your API preferences"
}
),
" ",
'and look for "FFScouter3" in your API key history comments.'
] }),
jsxs("div", { className: `${cls.span} ${cls.apiBlock}`, children: [
jsxs("div", { className: cls.cell, children: [
jsx("label", { htmlFor: "api-key", children: "API Key:" }),
jsx(
"input",
{
id: "api-key",
type: "text",
className: props.apiKey ? cls.blur : "",
placeholder: "Paste your key here...",
value: drafts.apiKey,
onChange,
onBlur: onApiKeyBlur
}
)
] }),
jsxs("div", { className: cls.apiStatusRow, children: [
jsx("label", { htmlFor: "ff-premium-badge", children: "FF Scouter Premium:" }),
jsx(
"span",
{
id: "ff-premium-badge",
className: `${cls.premiumBadge} ${isPremium === null ? cls.premiumBadgeUnknown : isPremium ? cls.premiumBadgeEnabled : cls.premiumBadgeDisabled}`,
children: isPremium === null ? "Unknown" : isPremium ? "Enabled" : "Disabled"
}
),
jsx(
"button",
{
type: "button",
className: "torn-btn btn-save",
onClick: onVerify,
children: "Verify"
}
)
] })
] })
] })
] }),
jsxs("div", { className: cls.group, children: [
jsx("h4", { children: "Gauge Marker Settings" }),
jsxs("div", { className: cls.section, children: [
jsxs("div", { className: cls.cell, children: [
jsx("label", { htmlFor: "gauge-marker-type", children: "Gauge Marker Style:" }),
jsxs(
"select",
{
id: "gauge-marker-type",
value: drafts.gaugeMarkerType,
onChange,
children: [
jsx("option", { value: "arrow", children: "Arrow (Default)" }),
jsx("option", { value: "bubble_ff", children: "Bubble (FF Score)" }),
jsx("option", { value: "bubble_estimate", children: "Bubble (BS Estimate)" })
]
}
)
] }),
jsxs("div", { className: `${cls.span} ${cls.markerSize}`, children: [
jsx("label", { htmlFor: "gauge-marker-scale", children: "Marker Size:" }),
jsxs("div", { className: cls.markerSizeControls, children: [
jsx(
"input",
{
id: "gauge-marker-scale",
type: "range",
min: "50",
max: "200",
step: "5",
value: drafts.gaugeMarkerScale,
onChange
}
),
jsx(
"input",
{
id: "gauge-marker-scale-number",
type: "number",
min: "50",
max: "200",
step: "5",
className: cls.number,
value: drafts.gaugeMarkerScale,
onChange
}
),
jsx("span", { children: "%" })
] })
] }),
jsxs("div", { className: `${cls.span} ${cls.markerBorderWidth}`, children: [
jsx("label", { htmlFor: "gauge-marker-border-width", children: "Border Thickness:" }),
jsxs("div", { className: cls.markerSizeControls, children: [
jsx(
"input",
{
id: "gauge-marker-border-width",
type: "range",
min: "0",
max: "3",
step: "0.5",
value: drafts.gaugeMarkerBorderWidth,
onChange
}
),
jsx(
"input",
{
id: "gauge-marker-border-width-number",
type: "number",
min: "0",
max: "3",
step: "0.5",
className: cls.number,
value: drafts.gaugeMarkerBorderWidth,
onChange
}
),
jsx("span", { children: "px" }),
jsxs(
"div",
{
className: "ffscouter-marker-preview",
style: {
"--ffscouter-marker-scale": drafts.gaugeMarkerScale / 100
},
children: [
jsxs(
"svg",
{
className: "ffscouter-preview-arrow",
viewBox: FF_ARROW_VIEWBOX,
children: [
jsx("title", { children: "Preview Gauge Marker" }),
jsx(
"path",
{
fillRule: "evenodd",
fill: previewColor,
stroke: "#000000",
strokeWidth: drafts.gaugeMarkerBorderWidth,
d: FF_ARROW_PATH_D
}
)
]
}
),
jsx(
"div",
{
className: "ffscouter-preview-bubble",
style: {
backgroundColor: previewColor,
color: get_contrast_color(previewColor),
borderWidth: `${drafts.gaugeMarkerBorderWidth * (drafts.gaugeMarkerScale / 100)}px`
},
children: "2.34"
}
)
]
}
)
] })
] }),
jsxs("div", { className: `${cls.span} ${cls.colorScheme}`, children: [
jsx("label", { htmlFor: "color-scheme", children: "Color Scheme:" }),
jsxs("div", { className: cls.colorSchemeControls, children: [
jsxs(
"select",
{
id: "color-scheme",
value: drafts.colorScheme,
onChange,
children: [
jsx("option", { value: "classic", children: "Classic (Default)" }),
jsx("option", { value: "cool_diverging", children: "Cool Diverging" }),
jsx("option", { value: "neon", children: "Neon" }),
jsx("option", { value: "colorblind_safe", children: "Colorblind-Safe" }),
jsx("option", { value: "grayscale", children: "Grayscale" }),
jsx("option", { value: "green_yellow_red", children: "Green-Yellow-Red" }),
jsx("option", { value: "blue_yellow_red", children: "Blue-Yellow-Red" }),
jsx("option", { value: "plasma", children: "Plasma" })
]
}
),
jsx("div", { className: "ffscouter-swatch-row", children: get_palette_for_scheme(drafts.colorScheme).map((color) => jsxs(
"svg",
{
className: "ffscouter-swatch",
viewBox: FF_ARROW_VIEWBOX,
children: [
jsxs("title", { children: [
color,
" swatch"
] }),
jsx(
"path",
{
fillRule: "evenodd",
fill: color,
stroke: "#000000",
strokeWidth: "1.5",
d: FF_ARROW_PATH_D
}
)
]
},
color
)) })
] })
] }),
jsxs("div", { className: `${cls.inputRow} ${cls.span}`, children: [
jsx("label", { htmlFor: "ff-range-low", children: "FF Ranges (Low, High, Max):" }),
jsxs("div", { className: cls.rangeRow, children: [
jsx(
"input",
{
id: "ff-range-low",
type: "number",
step: "0.1",
className: cls.number,
value: drafts.lowRange,
onChange
}
),
jsx("span", { children: "<" }),
jsx(
"input",
{
id: "ff-range-high",
type: "number",
step: "0.1",
className: cls.number,
value: drafts.highRange,
onChange
}
),
jsx("span", { children: "<" }),
jsx(
"input",
{
id: "ff-range-max",
type: "number",
step: "0.1",
className: cls.number,
value: drafts.maxRange,
onChange
}
)
] }),
rangeError && jsx("div", { className: cls.errorMsg, children: rangeError })
] })
] })
] }),
jsxs("div", { className: cls.group, children: [
jsx("h4", { children: "Feature Toggles" }),
jsxs("div", { className: cls.section, children: [
jsxs("div", { className: `${cls.span} ff-chain-block`, children: [
jsxs("div", { className: `${cls.cell} ${cls.cellCheckbox}`, children: [
jsx(
"input",
{
id: "chain-button-toggle",
type: "checkbox",
checked: drafts.chainButtonEnabled,
onChange
}
),
jsx("label", { htmlFor: "chain-button-toggle", children: "Enable Chain Button (Green FF Button)" })
] }),
drafts.chainButtonEnabled && jsxs("div", { className: `${cls.section} ${cls.chainSuboptions}`, children: [
jsxs("div", { className: `${cls.cell} ${cls.chainWide}`, children: [
jsx("label", { htmlFor: "chain-link-type", children: "Chain button opens:" }),
jsxs(
"select",
{
id: "chain-link-type",
value: drafts.chainLinkType,
onChange,
children: [
jsx("option", { value: "attack", children: "Attack page" }),
jsx("option", { value: "profile", children: "Profile page" })
]
}
)
] }),
jsxs("div", { className: `${cls.cell} ${cls.chainWide}`, children: [
jsx("label", { htmlFor: "chain-tab-type", children: "Open in:" }),
jsxs(
"select",
{
id: "chain-tab-type",
value: drafts.chainTabType,
onChange,
children: [
jsx("option", { value: "newtab", children: "New tab" }),
jsx("option", { value: "sametab", children: "Same tab" })
]
}
)
] }),
jsxs("div", { className: cls.cell, children: [
jsx("label", { htmlFor: "chain-min-level", children: "Min Level:" }),
jsx(
"input",
{
id: "chain-min-level",
type: "number",
className: cls.number,
placeholder: "No min",
value: drafts.chainMinLevel === null ? "" : drafts.chainMinLevel,
onChange
}
)
] }),
jsxs("div", { className: cls.cell, children: [
jsx("label", { htmlFor: "chain-max-level", children: "Max Level:" }),
jsx(
"input",
{
id: "chain-max-level",
type: "number",
className: cls.number,
placeholder: "No max",
value: drafts.chainMaxLevel === null ? "" : drafts.chainMaxLevel,
onChange
}
)
] }),
jsxs("div", { className: cls.cell, children: [
jsx("label", { htmlFor: "chain-min-ff", children: "Min FF:" }),
jsx(
"input",
{
id: "chain-min-ff",
type: "number",
step: "0.1",
className: cls.number,
placeholder: "No min",
value: drafts.chainMinFF === null ? "" : drafts.chainMinFF,
onChange
}
)
] }),
jsxs("div", { className: cls.cell, children: [
jsx("label", { htmlFor: "chain-max-ff", children: "Max FF:" }),
jsx(
"input",
{
id: "chain-max-ff",
type: "number",
step: "0.1",
className: cls.number,
placeholder: "No max",
value: drafts.chainMaxFF,
onChange
}
)
] }),
jsxs(
"div",
{
className: `${cls.cell} ${cls.cellCheckbox} ${cls.chainWide}`,
children: [
jsx(
"input",
{
id: "chain-inactive",
type: "checkbox",
checked: drafts.chainInactive,
onChange
}
),
jsx("label", { htmlFor: "chain-inactive", children: "Inactive Only (14+ days offline)" })
]
}
),
jsxs(
"div",
{
className: `${cls.cell} ${cls.cellCheckbox} ${cls.chainWide}`,
children: [
jsx(
"input",
{
id: "chain-factionless",
type: "checkbox",
checked: drafts.chainFactionless,
onChange
}
),
jsx("label", { htmlFor: "chain-factionless", children: "Factionless Only" })
]
}
)
] })
] }),
jsxs("div", { className: cls.cell, children: [
jsx("label", { htmlFor: "factions-col-display", children: "Faction Page Shows:" }),
jsxs(
"select",
{
id: "factions-col-display",
value: drafts.factionsColDisplay,
onChange,
children: [
jsx("option", { value: "fair_fight", children: "FF Score" }),
jsx("option", { value: "battle_stats", children: "BS Estimate" }),
jsx("option", { value: "none", children: "None (Hide Column)" })
]
}
)
] }),
jsxs("div", { className: cls.cell, children: [
jsx("label", { htmlFor: "war-col-display", children: "War Page Shows:" }),
jsxs(
"select",
{
id: "war-col-display",
value: drafts.warColDisplay,
onChange,
children: [
jsx("option", { value: "fair_fight", children: "FF Score" }),
jsx("option", { value: "battle_stats", children: "BS Estimate" }),
jsx("option", { value: "none", children: "None (Hide Column)" })
]
}
)
] }),
jsxs("div", { className: `${cls.cell} ${cls.cellCheckbox}`, children: [
jsx(
"input",
{
id: "status-attack-links-toggle",
type: "checkbox",
checked: drafts.statusAttackLinksEnabled,
onChange
}
),
jsx("label", { htmlFor: "status-attack-links-toggle", children: "Enable online status indicator quick attack links" })
] }),
jsxs("div", { className: cls.cell, children: [
jsx("label", { htmlFor: "war-quick-attack-action", children: "Quick Attack Action:" }),
jsxs(
"select",
{
id: "war-quick-attack-action",
value: drafts.warQuickAttackAction,
onChange,
children: [
jsx("option", { value: "new_tab", children: "New Tab" }),
jsx("option", { value: "current", children: "Same Tab" })
]
}
)
] }),
jsxs("div", { className: `${cls.cell} ${cls.cellCheckbox}`, children: [
jsx(
"input",
{
id: "ff-history-toggle",
type: "checkbox",
checked: drafts.ffHistoryEnabled,
onChange
}
),
jsx("label", { htmlFor: "ff-history-toggle", children: "Enable FF History button on profile pages" })
] }),
jsx("div", { className: `${cls.span} ff-deprecation-note`, children: jsxs("span", { children: [
"War Monitor is no longer supported. Use",
" ",
jsx(
"a",
{
target: "_blank",
href: "https://greasyfork.org/en/scripts/529238-torn-war-stuff-enhanced",
rel: "noreferrer",
children: "Torn War Stuff Enhanced"
}
),
" ",
"instead."
] }) })
] })
] }),
jsxs("div", { className: cls.group, children: [
jsx("h4", { children: "Debug Settings" }),
jsxs("div", { className: cls.section, children: [
jsxs("div", { className: `${cls.cell} ${cls.cellCheckbox}`, children: [
jsx(
"input",
{
id: "debug-logs",
type: "checkbox",
checked: drafts.debugLogs,
onChange
}
),
jsx("label", { htmlFor: "debug-logs", children: "Enable debug logging" })
] }),
jsxs("div", { className: `${cls.cell} ${cls.cellCheckbox}`, children: [
jsx(
"input",
{
id: "analytics-toggle",
type: "checkbox",
checked: drafts.analyticsEnabled,
onChange
}
),
jsx("label", { htmlFor: "analytics-toggle", children: "Enable local analytics logging (last 30 days)" })
] }),
jsxs("div", { className: `${cls.cell} ${cls.cellCheckbox}`, children: [
jsx(
"input",
{
id: "network-interception-toggle",
type: "checkbox",
checked: drafts.networkInterceptionEnabled,
onChange
}
),
jsx("label", { htmlFor: "network-interception-toggle", children: "Enable network request interception (Fetch/XHR/WS)" })
] }),
jsxs("div", { className: `${cls.cell} ${cls.cellCheckbox}`, children: [
jsx(
"input",
{
id: "debug-disable-pda-http",
type: "checkbox",
checked: drafts.debugDisablePdaHttp,
onChange
}
),
jsx("label", { htmlFor: "debug-disable-pda-http", children: "Disable PDA native HTTP (use GM_xmlhttpRequest instead)" })
] })
] })
] }),
jsxs("div", { className: cls.actions, children: [
jsx("button", { type: "button", className: "torn-btn btn-save", onClick: onSave, children: "Save Settings" }),
jsx(
"button",
{
type: "button",
className: "torn-btn btn-secondary",
onClick: onReset,
children: "Reset to Defaults"
}
),
jsx(
"button",
{
type: "button",
className: "torn-btn btn-secondary",
onClick: onClearCache,
children: "Clear FF Cache"
}
),
showSavedMessage && jsx("span", { className: cls.savedMsg, children: "✓ Saved!" })
] })
] })
]
}
);
}
class FFSettingsPanel extends HTMLElement {
constructor() {
super();
this._props = { ...DEFAULT_VALUES };
this._drafts = { ...DEFAULT_VALUES };
this._rangeError = "";
this._showSavedMessage = false;
this._root = null;
this._updatePromise = Promise.resolve();
this._resolveUpdate = null;
this.handleChange = (e) => {
const target = e.target;
if (!target) return;
this._showSavedMessage = false;
const id = target.id;
if (id === "api-key") {
this._drafts.apiKey = target.value;
} else if (id === "gauge-marker-scale" || id === "gauge-marker-scale-number") {
const raw = Number(target.value);
if (!Number.isNaN(raw)) {
this._drafts.gaugeMarkerScale = Math.min(200, Math.max(50, raw));
}
} else if (id === "gauge-marker-border-width" || id === "gauge-marker-border-width-number") {
const raw = Number(target.value);
if (!Number.isNaN(raw)) {
this._drafts.gaugeMarkerBorderWidth = Math.min(3, Math.max(0, raw));
}
} else if (id === "ff-range-low") {
this._drafts.lowRange = Number(target.value);
} else if (id === "ff-range-high") {
this._drafts.highRange = Number(target.value);
} else if (id === "ff-range-max") {
this._drafts.maxRange = Number(target.value);
} else if (id === "chain-min-level") {
this._drafts.chainMinLevel = target.value === "" ? null : Number(target.value);
} else if (id === "chain-max-level") {
this._drafts.chainMaxLevel = target.value === "" ? null : Number(target.value);
} else if (id === "chain-min-ff") {
this._drafts.chainMinFF = target.value === "" ? null : Number(target.value);
} else if (id === "chain-max-ff") {
const num = Number(target.value);
this._drafts.chainMaxFF = num;
this._drafts.chainFFTarget = num;
} else if (id === "gauge-marker-type") {
this._drafts.gaugeMarkerType = target.value;
} else if (id === "color-scheme") {
this._drafts.colorScheme = target.value;
} else if (id === "chain-link-type") {
this._drafts.chainLinkType = target.value;
} else if (id === "chain-tab-type") {
this._drafts.chainTabType = target.value;
} else if (id === "war-quick-attack-action") {
this._drafts.warQuickAttackAction = target.value;
} else if (id === "factions-col-display") {
this._drafts.factionsColDisplay = target.value;
} else if (id === "war-col-display") {
this._drafts.warColDisplay = target.value;
} else if (id === "chain-button-toggle") {
this._drafts.chainButtonEnabled = target.checked;
} else if (id === "chain-inactive") {
this._drafts.chainInactive = target.checked;
} else if (id === "chain-factionless") {
this._drafts.chainFactionless = target.checked;
} else if (id === "status-attack-links-toggle") {
this._drafts.statusAttackLinksEnabled = target.checked;
} else if (id === "ff-history-toggle") {
this._drafts.ffHistoryEnabled = target.checked;
} else if (id === "debug-logs") {
this._drafts.debugLogs = target.checked;
} else if (id === "analytics-toggle") {
this._drafts.analyticsEnabled = target.checked;
} else if (id === "network-interception-toggle") {
this._drafts.networkInterceptionEnabled = target.checked;
} else if (id === "debug-disable-pda-http") {
this._drafts.debugDisablePdaHttp = target.checked;
}
this.render();
};
this.handleApiKeyBlur = (e) => {
this._showSavedMessage = false;
const val = e.target.value.trim();
this._drafts.apiKey = val;
this.dispatchEvent(
new CustomEvent("ff-save-key", {
detail: { apiKey: val },
bubbles: true,
composed: true
})
);
this.render();
};
this.resetDrafts();
}
connectedCallback() {
this._root = createRoot(this);
this.render();
}
disconnectedCallback() {
this._root?.unmount();
this._root = null;
}
get updateComplete() {
return this._updatePromise;
}
resetDrafts() {
this._drafts = {
apiKey: this._props.apiKey,
lowRange: this._props.lowRange,
highRange: this._props.highRange,
maxRange: this._props.maxRange,
chainButtonEnabled: this._props.chainButtonEnabled,
chainLinkType: this._props.chainLinkType,
chainTabType: this._props.chainTabType,
chainFFTarget: this._props.chainFFTarget,
chainMinLevel: this._props.chainMinLevel,
chainMaxLevel: this._props.chainMaxLevel,
chainInactive: this._props.chainInactive,
chainMinFF: this._props.chainMinFF,
chainMaxFF: this._props.chainMaxFF,
chainFactionless: this._props.chainFactionless,
ffHistoryEnabled: this._props.ffHistoryEnabled,
factionsColDisplay: this._props.factionsColDisplay,
warColDisplay: this._props.warColDisplay,
debugLogs: this._props.debugLogs,
analyticsEnabled: this._props.analyticsEnabled,
networkInterceptionEnabled: this._props.networkInterceptionEnabled,
gaugeMarkerType: this._props.gaugeMarkerType,
gaugeMarkerScale: this._props.gaugeMarkerScale,
gaugeMarkerBorderWidth: this._props.gaugeMarkerBorderWidth,
colorScheme: this._props.colorScheme,
warQuickAttackAction: this._props.warQuickAttackAction,
statusAttackLinksEnabled: this._props.statusAttackLinksEnabled,
debugDisablePdaHttp: this._props.debugDisablePdaHttp
};
}
render() {
if (!this._root) return;
if (!this._resolveUpdate) {
this._updatePromise = new Promise((resolve) => {
this._resolveUpdate = resolve;
});
}
this._root.render(
createElement(SettingsPanelComponent, {
props: this._props,
drafts: this._drafts,
isPremium: this._props.isPremium,
rangeError: this._rangeError,
showSavedMessage: this._showSavedMessage,
onChange: this.handleChange,
onApiKeyBlur: this.handleApiKeyBlur,
onVerify: () => {
this.dispatchEvent(
new CustomEvent("ff-verify", {
detail: { apiKey: this._drafts.apiKey },
bubbles: true,
composed: true
})
);
},
onSave: () => {
this.handleSave();
},
onReset: () => {
if (confirm("Are you sure you want to reset all settings to defaults?")) {
this.dispatchEvent(
new CustomEvent("ff-reset", {
bubbles: true,
composed: true
})
);
}
},
onClearCache: () => {
if (confirm("Are you sure you want to clear all FF Scouter cache?")) {
this.dispatchEvent(
new CustomEvent("ff-clear-cache", {
bubbles: true,
composed: true
})
);
}
},
onRendered: () => {
if (this._resolveUpdate) {
this._resolveUpdate();
this._resolveUpdate = null;
}
}
})
);
}
handleSave() {
const low = this._drafts.lowRange;
const high = this._drafts.highRange;
const max = this._drafts.maxRange;
if (Number.isNaN(low) || Number.isNaN(high) || Number.isNaN(max)) {
this._rangeError = "FF ranges must be valid numbers";
this.render();
return;
}
if (low <= 0 || high <= 0 || max <= 0) {
this._rangeError = "FF ranges must be positive numbers";
this.render();
return;
}
if (low >= high || high >= max) {
this._rangeError = "FF ranges must be in ascending order: low < high < max";
this.render();
return;
}
this._rangeError = "";
this._showSavedMessage = true;
this.render();
setTimeout(() => {
this._showSavedMessage = false;
this.render();
}, 3e3);
this.dispatchEvent(
new CustomEvent("ff-save", {
detail: {
apiKey: this._drafts.apiKey,
lowRange: low,
highRange: high,
maxRange: max,
chainButtonEnabled: this._drafts.chainButtonEnabled,
chainLinkType: this._drafts.chainLinkType,
chainTabType: this._drafts.chainTabType,
chainFFTarget: this._drafts.chainFFTarget,
chainMinLevel: this._drafts.chainMinLevel,
chainMaxLevel: this._drafts.chainMaxLevel,
chainInactive: this._drafts.chainInactive,
chainMinFF: this._drafts.chainMinFF,
chainMaxFF: this._drafts.chainMaxFF,
chainFactionless: this._drafts.chainFactionless,
ffHistoryEnabled: this._drafts.ffHistoryEnabled,
factionsColDisplay: this._drafts.factionsColDisplay,
warColDisplay: this._drafts.warColDisplay,
debugLogs: this._drafts.debugLogs,
analyticsEnabled: this._drafts.analyticsEnabled,
networkInterceptionEnabled: this._drafts.networkInterceptionEnabled,
gaugeMarkerType: this._drafts.gaugeMarkerType,
gaugeMarkerScale: this._drafts.gaugeMarkerScale,
gaugeMarkerBorderWidth: this._drafts.gaugeMarkerBorderWidth,
colorScheme: this._drafts.colorScheme,
warQuickAttackAction: this._drafts.warQuickAttackAction,
statusAttackLinksEnabled: this._drafts.statusAttackLinksEnabled,
debugDisablePdaHttp: this._drafts.debugDisablePdaHttp
},
bubbles: true,
composed: true
})
);
}
get apiKey() {
return this._props.apiKey;
}
set apiKey(val) {
this._props.apiKey = val;
this._drafts.apiKey = val;
this.render();
}
get lowRange() {
return this._props.lowRange;
}
set lowRange(val) {
this._props.lowRange = val;
this._drafts.lowRange = val;
this.render();
}
get highRange() {
return this._props.highRange;
}
set highRange(val) {
this._props.highRange = val;
this._drafts.highRange = val;
this.render();
}
get maxRange() {
return this._props.maxRange;
}
set maxRange(val) {
this._props.maxRange = val;
this._drafts.maxRange = val;
this.render();
}
get chainButtonEnabled() {
return this._props.chainButtonEnabled;
}
set chainButtonEnabled(val) {
this._props.chainButtonEnabled = val;
this._drafts.chainButtonEnabled = val;
this.render();
}
get chainLinkType() {
return this._props.chainLinkType;
}
set chainLinkType(val) {
this._props.chainLinkType = val;
this._drafts.chainLinkType = val;
this.render();
}
get chainTabType() {
return this._props.chainTabType;
}
set chainTabType(val) {
this._props.chainTabType = val;
this._drafts.chainTabType = val;
this.render();
}
get chainFFTarget() {
return this._props.chainFFTarget;
}
set chainFFTarget(val) {
this._props.chainFFTarget = val;
this._drafts.chainFFTarget = val;
this.render();
}
get chainMinLevel() {
return this._props.chainMinLevel;
}
set chainMinLevel(val) {
this._props.chainMinLevel = val;
this._drafts.chainMinLevel = val;
this.render();
}
get chainMaxLevel() {
return this._props.chainMaxLevel;
}
set chainMaxLevel(val) {
this._props.chainMaxLevel = val;
this._drafts.chainMaxLevel = val;
this.render();
}
get chainInactive() {
return this._props.chainInactive;
}
set chainInactive(val) {
this._props.chainInactive = val;
this._drafts.chainInactive = val;
this.render();
}
get chainMinFF() {
return this._props.chainMinFF;
}
set chainMinFF(val) {
this._props.chainMinFF = val;
this._drafts.chainMinFF = val;
this.render();
}
get chainMaxFF() {
return this._props.chainMaxFF;
}
set chainMaxFF(val) {
this._props.chainMaxFF = val;
this._drafts.chainMaxFF = val;
this.render();
}
get chainFactionless() {
return this._props.chainFactionless;
}
set chainFactionless(val) {
this._props.chainFactionless = val;
this._drafts.chainFactionless = val;
this.render();
}
get ffHistoryEnabled() {
return this._props.ffHistoryEnabled;
}
set ffHistoryEnabled(val) {
this._props.ffHistoryEnabled = val;
this._drafts.ffHistoryEnabled = val;
this.render();
}
get factionsColDisplay() {
return this._props.factionsColDisplay;
}
set factionsColDisplay(val) {
this._props.factionsColDisplay = val;
this._drafts.factionsColDisplay = val;
this.render();
}
get warColDisplay() {
return this._props.warColDisplay;
}
set warColDisplay(val) {
this._props.warColDisplay = val;
this._drafts.warColDisplay = val;
this.render();
}
get debugLogs() {
return this._props.debugLogs;
}
set debugLogs(val) {
this._props.debugLogs = val;
this._drafts.debugLogs = val;
this.render();
}
get analyticsEnabled() {
return this._props.analyticsEnabled;
}
set analyticsEnabled(val) {
this._props.analyticsEnabled = val;
this._drafts.analyticsEnabled = val;
this.render();
}
get networkInterceptionEnabled() {
return this._props.networkInterceptionEnabled;
}
set networkInterceptionEnabled(val) {
this._props.networkInterceptionEnabled = val;
this._drafts.networkInterceptionEnabled = val;
this.render();
}
get gaugeMarkerType() {
return this._props.gaugeMarkerType;
}
set gaugeMarkerType(val) {
this._props.gaugeMarkerType = val;
this._drafts.gaugeMarkerType = val;
this.render();
}
get gaugeMarkerScale() {
return this._props.gaugeMarkerScale;
}
set gaugeMarkerScale(val) {
this._props.gaugeMarkerScale = val;
this._drafts.gaugeMarkerScale = val;
this.render();
}
get gaugeMarkerBorderWidth() {
return this._props.gaugeMarkerBorderWidth;
}
set gaugeMarkerBorderWidth(val) {
this._props.gaugeMarkerBorderWidth = val;
this._drafts.gaugeMarkerBorderWidth = val;
this.render();
}
get colorScheme() {
return this._props.colorScheme;
}
set colorScheme(val) {
this._props.colorScheme = val;
this._drafts.colorScheme = val;
this.render();
}
get warQuickAttackAction() {
return this._props.warQuickAttackAction;
}
set warQuickAttackAction(val) {
this._props.warQuickAttackAction = val;
this._drafts.warQuickAttackAction = val;
this.render();
}
get statusAttackLinksEnabled() {
return this._props.statusAttackLinksEnabled;
}
set statusAttackLinksEnabled(val) {
this._props.statusAttackLinksEnabled = val;
this._drafts.statusAttackLinksEnabled = val;
this.render();
}
get debugDisablePdaHttp() {
return this._props.debugDisablePdaHttp;
}
set debugDisablePdaHttp(val) {
this._props.debugDisablePdaHttp = val;
this._drafts.debugDisablePdaHttp = val;
this.render();
}
get isPremium() {
return this._props.isPremium;
}
set isPremium(val) {
this._props.isPremium = val;
this.render();
}
get draftApiKey() {
return this._drafts.apiKey;
}
set draftApiKey(val) {
this._drafts.apiKey = val;
this.render();
}
get draftLowRange() {
return this._drafts.lowRange;
}
set draftLowRange(val) {
this._drafts.lowRange = val;
this.render();
}
get draftHighRange() {
return this._drafts.highRange;
}
set draftHighRange(val) {
this._drafts.highRange = val;
this.render();
}
get draftMaxRange() {
return this._drafts.maxRange;
}
set draftMaxRange(val) {
this._drafts.maxRange = val;
this.render();
}
get draftChainButtonEnabled() {
return this._drafts.chainButtonEnabled;
}
set draftChainButtonEnabled(val) {
this._drafts.chainButtonEnabled = val;
this.render();
}
get draftChainLinkType() {
return this._drafts.chainLinkType;
}
set draftChainLinkType(val) {
this._drafts.chainLinkType = val;
this.render();
}
get draftChainTabType() {
return this._drafts.chainTabType;
}
set draftChainTabType(val) {
this._drafts.chainTabType = val;
this.render();
}
get draftChainFFTarget() {
return this._drafts.chainFFTarget;
}
set draftChainFFTarget(val) {
this._drafts.chainFFTarget = val;
this.render();
}
get draftChainMinLevel() {
return this._drafts.chainMinLevel;
}
set draftChainMinLevel(val) {
this._drafts.chainMinLevel = val;
this.render();
}
get draftChainMaxLevel() {
return this._drafts.chainMaxLevel;
}
set draftChainMaxLevel(val) {
this._drafts.chainMaxLevel = val;
this.render();
}
get draftChainInactive() {
return this._drafts.chainInactive;
}
set draftChainInactive(val) {
this._drafts.chainInactive = val;
this.render();
}
get draftChainMinFF() {
return this._drafts.chainMinFF;
}
set draftChainMinFF(val) {
this._drafts.chainMinFF = val;
this.render();
}
get draftChainMaxFF() {
return this._drafts.chainMaxFF;
}
set draftChainMaxFF(val) {
this._drafts.chainMaxFF = val;
this.render();
}
get draftChainFactionless() {
return this._drafts.chainFactionless;
}
set draftChainFactionless(val) {
this._drafts.chainFactionless = val;
this.render();
}
get draftFFHistoryEnabled() {
return this._drafts.ffHistoryEnabled;
}
set draftFFHistoryEnabled(val) {
this._drafts.ffHistoryEnabled = val;
this.render();
}
get draftFactionsColDisplay() {
return this._drafts.factionsColDisplay;
}
set draftFactionsColDisplay(val) {
this._drafts.factionsColDisplay = val;
this.render();
}
get draftWarColDisplay() {
return this._drafts.warColDisplay;
}
set draftWarColDisplay(val) {
this._drafts.warColDisplay = val;
this.render();
}
get draftDebugLogs() {
return this._drafts.debugLogs;
}
set draftDebugLogs(val) {
this._drafts.debugLogs = val;
this.render();
}
get draftAnalyticsEnabled() {
return this._drafts.analyticsEnabled;
}
set draftAnalyticsEnabled(val) {
this._drafts.analyticsEnabled = val;
this.render();
}
get draftNetworkInterceptionEnabled() {
return this._drafts.networkInterceptionEnabled;
}
set draftNetworkInterceptionEnabled(val) {
this._drafts.networkInterceptionEnabled = val;
this.render();
}
get draftGaugeMarkerType() {
return this._drafts.gaugeMarkerType;
}
set draftGaugeMarkerType(val) {
this._drafts.gaugeMarkerType = val;
this.render();
}
get draftGaugeMarkerScale() {
return this._drafts.gaugeMarkerScale;
}
set draftGaugeMarkerScale(val) {
this._drafts.gaugeMarkerScale = val;
this.render();
}
get draftGaugeMarkerBorderWidth() {
return this._drafts.gaugeMarkerBorderWidth;
}
set draftGaugeMarkerBorderWidth(val) {
this._drafts.gaugeMarkerBorderWidth = val;
this.render();
}
get draftColorScheme() {
return this._drafts.colorScheme;
}
set draftColorScheme(val) {
this._drafts.colorScheme = val;
this.render();
}
get draftWarQuickAttackAction() {
return this._drafts.warQuickAttackAction;
}
set draftWarQuickAttackAction(val) {
this._drafts.warQuickAttackAction = val;
this.render();
}
get draftStatusAttackLinksEnabled() {
return this._drafts.statusAttackLinksEnabled;
}
set draftStatusAttackLinksEnabled(val) {
this._drafts.statusAttackLinksEnabled = val;
this.render();
}
get draftDebugDisablePdaHttp() {
return this._drafts.debugDisablePdaHttp;
}
set draftDebugDisablePdaHttp(val) {
this._drafts.debugDisablePdaHttp = val;
this.render();
}
}
customElements.define("ff-settings-panel", FFSettingsPanel);
const V2_PREFIX = "ffscouterv2-";
const V3_PREFIX = "ffsv3-config";
const V2_IDB_NAME = "ffscouter-cache";
function v2_get(key) {
return localStorage.getItem(V2_PREFIX + key);
}
function v3_has(key) {
return localStorage.getItem(V3_PREFIX + key) !== null;
}
function v3_set(key, value) {
localStorage.setItem(
V3_PREFIX + key,
JSON.stringify({ value, expiration: null })
);
}
function migrate_bool(old_key, new_key) {
if (v3_has(new_key)) return;
const v = v2_get(old_key);
if (v !== null) v3_set(new_key, v === "true");
}
function migrate_string(old_key, new_key, valid) {
if (v3_has(new_key)) return;
const v = v2_get(old_key);
if (v !== null && valid.includes(v)) v3_set(new_key, v);
}
function migrate_float(old_key, new_key) {
if (v3_has(new_key)) return;
const v = v2_get(old_key);
if (v !== null) {
const n = parseFloat(v);
if (!Number.isNaN(n)) v3_set(new_key, n);
}
}
function run_migration() {
if (!v3_has("key")) {
const old_key = localStorage.getItem("limited_key");
if (old_key) v3_set("key", old_key);
}
migrate_bool("debug-logs", "debug_logs");
migrate_bool("ff-history-enabled", "ff_history_enabled");
migrate_bool("chain-button-enabled", "chain_button_enabled");
migrate_string("factions-col-display", "factions_col_display", [
"fair_fight",
"battle_stats",
"none"
]);
migrate_string("chain-link-type", "chain_link_type", ["attack", "profile"]);
migrate_string("chain-tab-type", "chain_tab_type", ["newtab", "sametab"]);
migrate_float("chain-ff-target", "chain_ff_target");
if (!v3_has("low_ff_range") && !v3_has("high_ff_range") && !v3_has("max_ff_range")) {
const raw = localStorage.getItem("ffscouterv2-ranges");
if (raw) {
try {
const { low, high, max } = JSON.parse(raw);
if (typeof low === "number") v3_set("low_ff_range", low);
if (typeof high === "number") v3_set("high_ff_range", high);
if (typeof max === "number") v3_set("max_ff_range", max);
} catch {
}
}
}
if (!v3_has("faction_filter_state")) {
const col_display = v2_get("factions-col-display") ?? "battle_stats";
const sort_key = col_display === "fair_fight" ? "factions-ff-sort-order" : "factions-est-sort-order";
const sort_order = v2_get(sort_key);
if (sort_order === "asc" || sort_order === "desc") {
v3_set("faction_filter_state", {
sortBy: sort_order === "asc" ? "ff-asc" : "ff-desc"
});
}
}
}
function clear_v2_data() {
localStorage.removeItem("limited_key");
const to_delete = Object.keys(localStorage).filter(
(k) => k.startsWith(V2_PREFIX)
);
for (const key of to_delete) {
localStorage.removeItem(key);
}
try {
indexedDB.deleteDatabase(V2_IDB_NAME);
} catch {
}
}
const index$2 = {
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 = await create_ff_element("ff-settings-panel");
if (!panel) {
toast(
"FF Scouter settings failed to load. This may be caused by a conflicting browser extension (e.g. AdBlocker Ultimate).",
TOAST_LEVEL.ERROR
);
return;
}
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.warColDisplay = ffconfig.war_col_display;
panel.debugLogs = ffconfig.debug_logs;
panel.analyticsEnabled = ffconfig.analytics_enabled;
panel.networkInterceptionEnabled = ffconfig.network_interception_enabled;
panel.gaugeMarkerType = ffconfig.gauge_marker_type;
panel.gaugeMarkerScale = ffconfig.gauge_marker_scale;
panel.gaugeMarkerBorderWidth = ffconfig.gauge_marker_border_width;
panel.colorScheme = ffconfig.color_scheme;
panel.warQuickAttackAction = ffconfig.war_quick_attack_action;
panel.statusAttackLinksEnabled = ffconfig.status_attack_links_enabled;
panel.debugDisablePdaHttp = ffconfig.debug_disable_pda_http;
panel.addEventListener("ff-save", async (e) => {
const detail = e.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.war_col_display = detail.warColDisplay;
ffconfig.debug_logs = detail.debugLogs;
if (detail.debugLogs) {
logger.setLevel(LogLevel.DEBUG);
} else {
logger.setLevel(LogLevel.INFO);
}
ffconfig.analytics_enabled = detail.analyticsEnabled;
ffconfig.network_interception_enabled = detail.networkInterceptionEnabled;
ffconfig.gauge_marker_type = detail.gaugeMarkerType;
ffconfig.gauge_marker_scale = detail.gaugeMarkerScale;
ffconfig.gauge_marker_border_width = detail.gaugeMarkerBorderWidth;
document.body.style.setProperty(
"--ffscouter-marker-scale",
`${detail.gaugeMarkerScale / 100}`
);
ffconfig.color_scheme = detail.colorScheme;
ffconfig.war_quick_attack_action = detail.warQuickAttackAction;
ffconfig.status_attack_links_enabled = detail.statusAttackLinksEnabled;
ffconfig.debug_disable_pda_http = detail.debugDisablePdaHttp;
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.warColDisplay = ffconfig.war_col_display;
panel.debugLogs = ffconfig.debug_logs;
panel.analyticsEnabled = ffconfig.analytics_enabled;
panel.networkInterceptionEnabled = ffconfig.network_interception_enabled;
panel.gaugeMarkerType = ffconfig.gauge_marker_type;
panel.gaugeMarkerScale = ffconfig.gauge_marker_scale;
panel.gaugeMarkerBorderWidth = ffconfig.gauge_marker_border_width;
document.body.style.setProperty(
"--ffscouter-marker-scale",
`${ffconfig.gauge_marker_scale / 100}`
);
panel.colorScheme = ffconfig.color_scheme;
panel.warQuickAttackAction = ffconfig.war_quick_attack_action;
panel.statusAttackLinksEnabled = ffconfig.status_attack_links_enabled;
panel.debugDisablePdaHttp = ffconfig.debug_disable_pda_http;
toast("Settings reset to defaults!");
window.dispatchEvent(new CustomEvent("ff-config-updated"));
});
panel.addEventListener("ff-clear-cache", async () => {
try {
ffscouter.clear_cache();
clear_v2_data();
toast("FF Scouter cache cleared successfully!");
} catch (err) {
logger.error("Failed to delete IndexedDB cache", err);
toast("Failed to clear cache database", TOAST_LEVEL.ERROR);
}
});
panel.addEventListener("ff-save-key", async (e) => {
const detail = e.detail;
ffconfig.key = detail.apiKey;
panel.apiKey = detail.apiKey;
panel.isPremium = await check_key_status.is_premium(true);
toast("API key saved successfully!");
window.dispatchEvent(new CustomEvent("ff-config-updated"));
});
panel.addEventListener("ff-verify", async (e) => {
const detail = e.detail;
if (!detail.apiKey) {
toast("Please enter an API key.", TOAST_LEVEL.ERROR);
return;
}
panel.isPremium = null;
let result = null;
try {
result = await check_key(detail.apiKey);
} catch (err) {
panel.isPremium = null;
toast(`${err}`, TOAST_LEVEL.ERROR);
return;
}
if (result == null || result.blank) {
panel.isPremium = null;
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. 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;
ffconfig.key = detail.apiKey;
panel.apiKey = detail.apiKey;
panel.isPremium = result.result.is_premium;
check_key_status.clear();
} else {
panel.isPremium = false;
}
toast(message, level);
});
const profileWrapper = await wait_for_element(".profile-wrapper", 15e3);
if (!profileWrapper) {
logger.error("Could not find profile wrapper for settings panel");
return;
}
profileWrapper.parentNode?.insertBefore(panel, profileWrapper.nextSibling);
check_key_status.is_premium(true).then((result) => {
panel.isPremium = result;
});
},
httpIntercept: {
before(_url, _init) {
return void 0;
},
after(_bodyText, _response, _ctx) {
return void 0;
}
}
};
const __vite_glob_0_12 = Object.freeze( Object.defineProperty({
__proto__: null,
default: index$2
}, Symbol.toStringTag, { value: "Module" }));
const log$2 = logger.child("feature:status-attack");
function updateBodyClass() {
const isEnabled = ffconfig.status_attack_links_enabled;
if (isEnabled) {
document.body.setAttribute("data-ff-status-attack-enabled", "true");
} else {
document.body.removeAttribute("data-ff-status-attack-enabled");
}
}
function isForumStatusIcon(el) {
const title = (el.getAttribute("title") || "").toLowerCase();
const ariaLabel = (el.querySelector("a")?.getAttribute("aria-label") || "").toLowerCase();
return title.includes("online") || title.includes("offline") || title.includes("away") || title.includes("idle") || ariaLabel.includes("online") || ariaLabel.includes("offline") || ariaLabel.includes("away") || ariaLabel.includes("idle");
}
function labelForumStatuses(root = document.body) {
const elements = root.querySelectorAll(
'li[id^="icon"][id*="___"].iconShow:not(.ffscouter-forum-status)'
);
for (const el of elements) {
if (el instanceof HTMLElement && isForumStatusIcon(el)) {
el.classList.add("ffscouter-forum-status");
}
}
}
let forumObserver = null;
function setupForumObserver() {
if (forumObserver) {
forumObserver.disconnect();
forumObserver = null;
}
if (!torn_page("forums")) return;
labelForumStatuses();
forumObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof Element) {
labelForumStatuses(node);
}
}
}
});
const content = document.querySelector(".content-wrapper") || document.body;
forumObserver.observe(content, {
childList: true,
subtree: true
});
log$2.debug("Forum status observer connected");
}
let localUserId = null;
async function fetchLocalUserId() {
try {
const idStr = await getLocalUserId();
if (idStr) {
localUserId = parseInt(idStr, 10);
log$2.debug("Logged-in user ID cached:", localUserId);
}
} catch (err) {
log$2.error("Failed to retrieve logged-in user ID", err);
}
}
function handleStatusClick(e) {
if (!ffconfig.status_attack_links_enabled) return;
const target = e.target;
const statusEl = target.closest(`
[class*="userStatusWrap__"],
li[id^="icon"][id*="-profile-"].user-status-16-Online,
li[id^="icon"][id*="-profile-"].user-status-16-Away,
li[id^="icon"][id*="-profile-"].user-status-16-Offline,
#profile-mini-root li[id^="icon"][id*="-mini-profile-"].user-status-16-Online,
#profile-mini-root li[id^="icon"][id*="-mini-profile-"].user-status-16-Away,
#profile-mini-root li[id^="icon"][id*="-mini-profile-"].user-status-16-Offline,
li[id^="icon"][id*="___"].iconShow.ffscouter-forum-status
`);
if (!statusEl) return;
let playerId = null;
const idAttr = statusEl.id || "";
if (idAttr?.includes("-profile-")) {
const match = idAttr.match(/profile-(\d+)$/);
if (match?.[1]) {
playerId = parseInt(match[1], 10);
}
}
if (!playerId && idAttr && idAttr.includes("-mini-profile-")) {
const match = idAttr.match(/mini-profile-(\d+)$/);
if (match?.[1]) {
playerId = parseInt(match[1], 10);
}
}
if (!playerId && torn_page("factions")) {
const row = statusEl.closest(".table-row, .enemy, .your, .member");
if (row) {
playerId = get_player_id_in_element(row);
}
}
if (!playerId && torn_page("forums")) {
const container = statusEl.closest(`
[class*="poster"], .poster,
[class*="lastPost"], .lastPost,
[class*="last-poster"], .last-poster,
[class*="last-post"], .last-post,
[class*="starter"], .starter
`);
if (container) {
playerId = get_player_id_in_element(container);
}
}
if (!playerId && torn_page("profiles")) {
const match = window.location.href.match(/XID=(\d+)/);
if (match?.[1]) {
playerId = parseInt(match[1], 10);
}
}
if (!playerId) {
const miniRoot = document.getElementById("profile-mini-root");
if (miniRoot?.contains(statusEl)) {
playerId = get_player_id_in_element(miniRoot);
}
}
if (!playerId) {
log$2.debug("Failed to extract playerId from status icon click");
return;
}
if (localUserId && playerId === localUserId) {
log$2.debug("Bypassing click-to-attack: clicked own status icon.");
return;
}
e.preventDefault();
e.stopPropagation();
log$2.debug("Initiating attack on user:", playerId);
const forceNewTab = e.ctrlKey || e.metaKey || e.button === 1;
open_attack_link(playerId, {
openInNewTab: forceNewTab ? true : void 0
});
}
const index$1 = {
name: "Online Status Attack Links",
description: "Converts online/idle/offline status indicators into clickable quick-attack links.",
executionTime: StartTime.DocumentBody,
async shouldRun() {
return true;
},
async run() {
updateBodyClass();
fetchLocalUserId();
const initPage = () => {
setupForumObserver();
};
document.body.addEventListener("click", handleStatusClick, true);
window.addEventListener("ff-config-updated", () => {
updateBodyClass();
});
if (typeof window !== "undefined") {
const originalPushState = window.history.pushState;
const originalReplaceState = window.history.replaceState;
window.history.pushState = function(...args) {
originalPushState.apply(this, args);
setTimeout(initPage, 100);
};
window.history.replaceState = function(...args) {
originalReplaceState.apply(this, args);
setTimeout(initPage, 100);
};
window.addEventListener("popstate", () => {
setTimeout(initPage, 100);
});
}
initPage();
log$2.debug("Online Status Attack Links feature installed successfully.");
}
};
const __vite_glob_0_13 = Object.freeze( Object.defineProperty({
__proto__: null,
default: index$1
}, Symbol.toStringTag, { value: "Module" }));
const log$1 = logger.child("feature:test-feature");
const index = {
name: "Test Feature!",
description: "It's literally a test feature :P",
executionTime: StartTime.DocumentStart,
async shouldRun() {
return true;
},
async run() {
log$1.info("hello world but from feature");
},
httpIntercept: {
before(_url, _init) {
return void 0;
},
after(_bodyText, _response, _ctx) {
return void 0;
}
}
};
const __vite_glob_0_14 = Object.freeze( Object.defineProperty({
__proto__: null,
default: index
}, Symbol.toStringTag, { value: "Module" }));
const modules = Object.assign({
"./attack/index.ts": __vite_glob_0_0,
"./deprecation-notice/index.ts": __vite_glob_0_1,
"./faction/index.ts": __vite_glob_0_2,
"./fallback/index.ts": __vite_glob_0_3,
"./ff-button/index.ts": __vite_glob_0_4,
"./item_market/index.ts": __vite_glob_0_5,
"./mini-profile-flights/index.ts": __vite_glob_0_6,
"./mini-profile/index.ts": __vite_glob_0_7,
"./profile-flights/index.ts": __vite_glob_0_8,
"./profile-history/index.ts": __vite_glob_0_9,
"./profile/index.ts": __vite_glob_0_10,
"./rr/index.ts": __vite_glob_0_11,
"./settings/index.ts": __vite_glob_0_12,
"./status-attack/index.ts": __vite_glob_0_13,
"./test-feature/index.ts": __vite_glob_0_14
});
const Features = Object.values(modules).map((mod) => mod.default).filter(
(feat) => !!feat && "name" in feat && feat.name !== "Test Feature!"
);
const httpInterceptors = [];
function registerHttpInterceptor(interceptor) {
httpInterceptors.push(interceptor);
httpInterceptors.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
}
const stylesCss = ".ffscouter-gauge{position:relative;display:block;padding:0}.ffscouter-arrow,.ffscouter-preview-arrow{width:var(--ffscouter-arrow-width);object-fit:cover;pointer-events:none}.ffscouter-arrow{position:absolute;transform:translate(-50%,-30%);padding:0;top:0;left:calc(var(--ffscouter-arrow-width) / 2 + var(--band-percent) * (100% - var(--ffscouter-arrow-width)) / 100)}.ffscouter-preview-arrow{display:inline-block;vertical-align:middle}.ffscouter-bubble,.ffscouter-preview-bubble{min-width:2.5882em;height:1.6471em;line-height:1.4118;border:1px solid rgba(0,0,0,.4);border-radius:999px;font-size:var(--ffscouter-bubble-font-size);font-weight:700;font-family:Geneva,Arial,sans-serif;text-align:center;padding:0 .4706em;box-sizing:border-box;white-space:nowrap;display:inline-flex;align-items:center;justify-content:center;text-shadow:0 1px 1px rgba(0,0,0,.5);box-shadow:0 1px 2px #0000004d}.ffscouter-bubble{position:absolute;transform:translate(-50%,-30%);top:0;left:calc(var(--ffscouter-arrow-width) / 2 + var(--band-percent) * (100% - var(--ffscouter-arrow-width)) / 100);pointer-events:none;z-index:10}.ffscouter-preview-bubble{vertical-align:middle}.ffscouter-marker-preview{display:inline-flex;align-items:center;gap:10px;--ffscouter-arrow-width: calc(20px * var(--ffscouter-marker-scale));--ffscouter-bubble-font-size: calc(8.5px * var(--ffscouter-marker-scale))}.ffscouter-mini-desc{padding:0 5px}.ffscouter-swatch-row{display:inline-flex;gap:3px}.ffscouter-swatch{display:inline-block;width:20px;height:13px}body{--ffscouter-bg-color: #f0f0f0;--ffscouter-alt-bg-color: #fff;--ffscouter-border-color: #ccc;--ffscouter-input-color: #ccc;--ffscouter-text-color: #000;--ffscouter-hover-color: #ddd;--ffscouter-glow-color: #4caf50;--ffscouter-success-color: #4caf50;--ffscouter-marker-scale: 1;--ffscouter-arrow-width: calc(20px * var(--ffscouter-marker-scale));--ffscouter-bubble-font-size: calc(8.5px * var(--ffscouter-marker-scale))}body.dark-mode{--ffscouter-bg-color: #333;--ffscouter-alt-bg-color: #383838;--ffscouter-border-color: #444;--ffscouter-input-color: #504f4f;--ffscouter-text-color: #ccc;--ffscouter-hover-color: #555;--ffscouter-glow-color: #4caf50;--ffscouter-success-color: #4caf50}ff-settings-panel{display:block}.profile-status{position:relative}.ff-flight-element{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}.faction-war .ffscouter-cell{float:left!important;width:32px!important;height:20px!important;font-size:11px!important;font-weight:700!important;border-radius:3px!important;box-sizing:border-box!important;margin:7px 4px!important;padding:0!important;text-align:center!important;line-height:20px!important;z-index:10!important}.ffscouter-cell{cursor:pointer!important}.faction-war .ffscouter-header,.table-header .ffscouter-header{float:left!important;width:38px!important;font-size:12px!important;font-weight:700!important;padding:0!important;text-align:center!important;background-color:transparent!important;cursor:pointer!important}.faction-war:has(.ffscouter-header[data-ffscouter-sort]) [class*=sortIcon___]:not(.ffscouter-sort-icon),.members-list:has(.ffscouter-header[data-ffscouter-sort]) [class*=sortIcon___]:not(.ffscouter-sort-icon){visibility:hidden!important}[data-ffscouter-hidden]{display:none!important}.faction-war[data-ffscouter-hide-level=true] .level:not(.ffscouter-cell):not(.ffscouter-header){display:none!important}.faction-war[data-ffscouter-hide-status=true] .status,.faction-war[data-ffscouter-hide-score=true] .points{display:none!important}.faction-war[data-ffscouter-col-display=fair_fight]:not([data-ffscouter-hide-level=true]) .level:not(.ffscouter-cell):not(.ffscouter-header),.faction-war[data-ffscouter-col-display=battle_stats]:not([data-ffscouter-hide-level=true]) .level:not(.ffscouter-cell):not(.ffscouter-header){width:29px!important}.faction-war[data-ffscouter-col-display=fair_fight]:not([data-ffscouter-hide-level=true]) .status,.faction-war[data-ffscouter-col-display=battle_stats]:not([data-ffscouter-hide-level=true]) .status{width:50px!important}.faction-war[data-ffscouter-col-display=fair_fight]:not([data-ffscouter-hide-level=true]) .points,.faction-war[data-ffscouter-col-display=battle_stats]:not([data-ffscouter-hide-level=true]) .points{width:38px!important}.members-list li.enemy:has(>.tt-stats-estimate),.members-list li.your:has(>.tt-stats-estimate),.members-list li.enemy:has(>div.clear~*),.members-list li.your:has(>div.clear~*){padding-bottom:22px!important;position:relative!important}.members-list li.enemy>.tt-stats-estimate,.members-list li.your>.tt-stats-estimate,.members-list li.enemy>div.clear~*,.members-list li.your>div.clear~*{position:absolute!important;bottom:2px!important;left:10px!important;height:18px!important;line-height:18px!important;font-size:11px!important;width:calc(100% - 20px)!important;display:block!important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}body[data-ff-status-attack-enabled=true] [class*=userStatusWrap__],body[data-ff-status-attack-enabled=true] li[id^=icon][id*=-profile-].user-status-16-Online,body[data-ff-status-attack-enabled=true] li[id^=icon][id*=-profile-].user-status-16-Away,body[data-ff-status-attack-enabled=true] li[id^=icon][id*=-profile-].user-status-16-Offline,body[data-ff-status-attack-enabled=true] #profile-mini-root li[id^=icon][id*=-mini-profile-].user-status-16-Online,body[data-ff-status-attack-enabled=true] #profile-mini-root li[id^=icon][id*=-mini-profile-].user-status-16-Away,body[data-ff-status-attack-enabled=true] #profile-mini-root li[id^=icon][id*=-mini-profile-].user-status-16-Offline,body[data-ff-status-attack-enabled=true] li[id^=icon][id*=___].iconShow.ffscouter-forum-status{cursor:crosshair!important}.d .job-lists-wrap .item>li.company,.d .job-lists-wrap .item>li.director,.d .job-lists-wrap .item>li.salary,.d .job-lists-wrap .item>li.ranks{margin-bottom:0!important;padding-bottom:0!important}";
importCSS(stylesCss);
const log = logger.child("boot");
const INJECTION_KEY = "__FF_SCOUTER_V2_INJECTED__";
async function safeShouldRun(feat) {
try {
return await feat.shouldRun();
} catch (err) {
log.error(`shouldRun() threw for feature "${feat.name}"`, err);
return false;
}
}
function safeRun(feat) {
feat.run().catch((err) => {
log.error(`run() threw for feature "${feat.name}"`, err);
});
}
async function main() {
if (document.documentElement.hasAttribute(INJECTION_KEY)) {
log.info("Script already injected");
return;
}
document.documentElement.setAttribute(INJECTION_KEY, "1");
log.info("Initializing", "3.0.1");
run_migration();
if (ffscouter.analytics_enabled) {
if (typeof unsafeWindow !== "undefined") {
unsafeWindow.ffscouter = ffscouter;
}
window.ffscouter = ffscouter;
}
for (const feat of Features) {
if (feat.executionTime === StartTime.DocumentStart && await safeShouldRun(feat)) {
if (feat.httpIntercept) {
feat.httpIntercept.name = feat.name;
registerHttpInterceptor(feat.httpIntercept);
}
safeRun(feat);
}
}
await wait_for_body(1e4);
for (const feat of Features) {
if (feat.executionTime === StartTime.DocumentBody && await safeShouldRun(feat)) {
if (feat.httpIntercept) {
feat.httpIntercept.name = feat.name;
registerHttpInterceptor(feat.httpIntercept);
}
safeRun(feat);
}
}
}
main();
})();