Real-time AAV (Average Auction Value) on every Ranked War weapon and armor auction. Shows what comparable gear actually sold for, how the current bid compares, and price trends. From official Torn API completed sales.
// ==UserScript==
// @name Cortex RW Auction Stats (beta)
// @namespace https://torn-cortex.pages.dev/
// @version 2.38.0
// @description Real-time AAV (Average Auction Value) on every Ranked War weapon and armor auction. Shows what comparable gear actually sold for, how the current bid compares, and price trends. From official Torn API completed sales.
// @author Cortex
// @match https://www.torn.com/amarket.php*
// @run-at document-idle
// @homepageURL https://torncortex.com
// @connect api.torn.com
// @connect fonts.googleapis.com
// @connect fonts.gstatic.com
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @license MIT
// @noframes
// ==/UserScript==
//
// COMPLIANCE (Torn API ToS):
// - Valuations use ONLY the official Torn API (api.torn.com). The live auctions
// are read ONLY from the Auction House page you manually loaded + are viewing.
// No other page is read; no non-API request is made to Torn; nothing is scraped
// off-page and no sale-detection/off-page monitoring is performed.
// - Runs only while the Auction House is actively viewed (visible + focused on
// desktop; visible active webview in Torn PDA). Each weapon is valued once and
// cached; requests are queued + spaced, well under Torn's rate limit.
// - Your API key is stored only in your userscript manager (or injected by Torn
// PDA), sent only to api.torn.com, and never logged.
// - Unofficial community tool. It tells you what gear is worth; you decide.
//
// This file is GENERATED from tools/userscript/trade-entry.ts by scripts/build-userscript.mjs.
// Do not edit by hand — run `npm run build:userscript` to regenerate.
"use strict";
(() => {
// src/lib/api/url.ts
var API_HOST = "api.torn.com";
var API_BASE_URL = `https://${API_HOST}/v2`;
var API_COMMENT = "Cortex";
function isTornHost(url) {
try {
return new URL(url).host === API_HOST;
} catch {
return false;
}
}
function buildTornUrl(path, key, comment = API_COMMENT) {
const cleanPath = path.replace(/^\/+/, "");
const separator = cleanPath.includes("?") ? "&" : "?";
return `${API_BASE_URL}/${cleanPath}${separator}key=${encodeURIComponent(key)}&comment=${encodeURIComponent(comment)}`;
}
// src/lib/auth/key.ts
var API_KEY_LENGTH = 16;
var KEY_CHARSET = /^[A-Za-z0-9]+$/;
function isValidKeyFormat(key) {
return typeof key === "string" && key.length === API_KEY_LENGTH && KEY_CHARSET.test(key);
}
// src/lib/market/valuation.ts
function finite(n) {
return typeof n === "number" && Number.isFinite(n);
}
var RW_RARITIES = /* @__PURE__ */ new Set(["yellow", "orange", "red"]);
function hasPerk(x) {
return (x.bonuses ?? []).some((b) => typeof b?.id === "number" && Number.isFinite(b.id));
}
function isRwRarity(rarity) {
return typeof rarity === "string" && RW_RARITIES.has(rarity.toLowerCase());
}
function isRwGearSale(s) {
return hasPerk(s) && isRwRarity(s.rarity);
}
function parseAuctionSales(raw) {
const root = raw && typeof raw === "object" ? raw : {};
const rawSales = Array.isArray(root.auctionhouse) ? root.auctionhouse : [];
const sales = [];
let oldest = null;
let newest = null;
for (const entry of rawSales) {
const price = Number(entry.price);
if (!finite(price) || price <= 0) continue;
const buyer = entry.buyer;
if (!buyer || typeof buyer !== "object") continue;
const ts = Number(entry.timestamp);
if (!finite(ts)) continue;
const item = entry.item ?? {};
const stats = item.stats ?? {};
const rawBonuses = Array.isArray(item.bonuses) ? item.bonuses : [];
const bonuses = [];
for (const b of rawBonuses) {
const id = Number(b.id);
const value = Number(b.value);
const title = typeof b.title === "string" ? b.title : typeof b.name === "string" ? b.name : "";
if (finite(id) && finite(value)) bonuses.push({ id, title, value });
}
sales.push({
saleId: finite(Number(entry.id)) ? Number(entry.id) : null,
price,
timestamp: ts,
quality: finite(Number(stats.quality)) ? Number(stats.quality) : null,
damage: finite(Number(stats.damage)) ? Number(stats.damage) : null,
accuracy: finite(Number(stats.accuracy)) ? Number(stats.accuracy) : null,
armor: finite(Number(stats.armor)) ? Number(stats.armor) : null,
sellerId: finite(Number(entry.seller?.id)) ? Number(entry.seller.id) : null,
buyerId: finite(Number(buyer.id)) ? Number(buyer.id) : null,
rarity: typeof item.rarity === "string" ? item.rarity : null,
bonuses,
uid: finite(Number(item.uid)) ? Number(item.uid) : null,
itemId: finite(Number(item.id)) ? Number(item.id) : null,
name: typeof item.name === "string" ? item.name : null,
type: typeof item.type === "string" ? item.type : null,
subType: typeof item.sub_type === "string" ? item.sub_type : null
});
if (oldest === null || ts < oldest) oldest = ts;
if (newest === null || ts > newest) newest = ts;
}
return { sales, oldestTimestamp: oldest, newestTimestamp: newest };
}
// src/lib/market/weapon-market.ts
var WARLORD_BONUS_ID = 81;
var REVITALIZE_BONUS_ID = 41;
var PERK_CLASS = {
// Utility — improve your gains, no combat effect (the liquid trading perks).
warlord: "Utility",
revitalize: "Utility",
stricken: "Utility",
plunder: "Utility",
proficience: "Utility",
irradiate: "Utility",
// Support — improve your other weapons / help the team take a target down.
eviscerate: "Support",
weaken: "Support",
wither: "Support",
slow: "Support",
cripple: "Support",
motivation: "Support",
suppress: "Support",
paralyze: "Support",
"home run": "Support",
stun: "Support",
disarm: "Support",
// Damage — boost your damage or hit chance in a fight.
achilles: "Damage",
assassinate: "Damage",
backstab: "Damage",
berserk: "Damage",
bleed: "Damage",
blindside: "Damage",
bloodlust: "Damage",
comeback: "Damage",
conserve: "Damage",
crusher: "Damage",
cupid: "Damage",
deadeye: "Damage",
deadly: "Damage",
"double-edged": "Damage",
"double tap": "Damage",
empower: "Damage",
execute: "Damage",
expose: "Damage",
finale: "Damage",
focus: "Damage",
frenzy: "Damage",
fury: "Damage",
grace: "Damage",
parry: "Damage",
penetrate: "Damage",
powerful: "Damage",
puncture: "Damage",
quicken: "Damage",
rage: "Damage",
roshambo: "Damage",
smurf: "Damage",
specialist: "Damage",
"sure shot": "Damage",
throttle: "Damage",
"wind-up": "Damage"
};
function perkClassOf(title) {
if (!title) return null;
return PERK_CLASS[title.trim().toLowerCase()] ?? null;
}
// src/lib/market/bonus-catalog.ts
var BONUS_CATALOG = [
{ id: 50, title: "Achilles" },
{ id: 72, title: "Assassinate" },
{ id: 52, title: "Backstab" },
{ id: 54, title: "Berserk" },
{ id: 57, title: "Bleed" },
{ id: 33, title: "Blindfire" },
{ id: 51, title: "Blindside" },
{ id: 85, title: "Bloodlust" },
{ id: 67, title: "Comeback" },
{ id: 55, title: "Conserve" },
{ id: 45, title: "Cripple" },
{ id: 49, title: "Crusher" },
{ id: 47, title: "Cupid" },
{ id: 63, title: "Deadeye" },
{ id: 62, title: "Deadly" },
{ id: 36, title: "Demoralize" },
{ id: 86, title: "Disarm" },
{ id: 105, title: "Double Tap" },
{ id: 74, title: "Double-edged" },
{ id: 87, title: "Empower" },
{ id: 56, title: "Eviscerate" },
{ id: 75, title: "Execute" },
{ id: 1, title: "Expose" },
{ id: 82, title: "Finale" },
{ id: 79, title: "Focus" },
{ id: 38, title: "Freeze" },
{ id: 80, title: "Frenzy" },
{ id: 64, title: "Fury" },
{ id: 53, title: "Grace" },
{ id: 34, title: "Hazardous" },
{ id: 83, title: "Home run" },
{ id: 115, title: "Immutable", label: "Immutable (Helmet)" },
{ id: 26, title: "Impassable" },
{ id: 17, title: "Impenetrable" },
{ id: 22, title: "Imperviable", label: "Imperviable (Face Mask)" },
{ id: 15, title: "Impregnable" },
{ id: 92, title: "Insurmountable" },
{ id: 91, title: "Invulnerable", label: "Invulnerable (Gas Mask)" },
{ id: 102, title: "Irradiate" },
{ id: 121, title: "Irrepressible", label: "Irrepressible (Respirator)" },
{ id: 112, title: "Kinetokinesis", label: "Kinetokinesis (Hooves)" },
{ id: 89, title: "Lacerate" },
{ id: 61, title: "Motivation" },
{ id: 59, title: "Paralyze" },
{ id: 84, title: "Parry" },
{ id: 101, title: "Penetrate" },
{ id: 21, title: "Plunder" },
{ id: 68, title: "Powerful" },
{ id: 14, title: "Proficience" },
{ id: 66, title: "Puncture" },
{ id: 88, title: "Quicken" },
{ id: 90, title: "Radiation Protection" },
{ id: 65, title: "Rage" },
{ id: 41, title: "Revitalize" },
{ id: 43, title: "Roshambo" },
{ id: 120, title: "Shock" },
{ id: 44, title: "Slow" },
{ id: 104, title: "Smash" },
{ id: 73, title: "Smurf" },
{ id: 71, title: "Specialist" },
{ id: 35, title: "Spray" },
{ id: 37, title: "Storage" },
{ id: 20, title: "Stricken" },
{ id: 58, title: "Stun" },
{ id: 60, title: "Suppress" },
{ id: 78, title: "Sure Shot" },
{ id: 48, title: "Throttle" },
{ id: 103, title: "Toxin" },
{ id: 81, title: "Warlord" },
{ id: 46, title: "Weaken" },
{ id: 76, title: "Wind-up" },
{ id: 42, title: "Wither" }
];
var BONUS_ALIASES = [
{ id: 116, title: "Immutable", label: "Immutable (Apron)" },
{ id: 117, title: "Immutable", label: "Immutable (Pants)" },
{ id: 118, title: "Immutable", label: "Immutable (Boots)" },
{ id: 119, title: "Immutable", label: "Immutable (Gloves)" },
{ id: 97, title: "Imperviable", label: "Imperviable (Body)" },
{ id: 98, title: "Imperviable", label: "Imperviable (Pants)" },
{ id: 99, title: "Imperviable", label: "Imperviable (Boots)" },
{ id: 100, title: "Imperviable", label: "Imperviable (Gloves)" },
{ id: 122, title: "Irrepressible", label: "Irrepressible (Body)" },
{ id: 123, title: "Irrepressible", label: "Irrepressible (Pants)" },
{ id: 124, title: "Irrepressible", label: "Irrepressible (Boots)" },
{ id: 125, title: "Irrepressible", label: "Irrepressible (Gloves)" },
{ id: 93, title: "Invulnerable", label: "Invulnerable (Body)" },
{ id: 94, title: "Invulnerable", label: "Invulnerable (Pants)" },
{ id: 95, title: "Invulnerable", label: "Invulnerable (Boots)" },
{ id: 96, title: "Invulnerable", label: "Invulnerable (Gloves)" },
{ id: 113, title: "Kinetokinesis", label: "Kinetokinesis (Clawshields)" },
{ id: 114, title: "Kinetokinesis", label: "Kinetokinesis (Visage)" }
];
var TITLE_BY_ID = new Map(
[...BONUS_CATALOG, ...BONUS_ALIASES].map((b) => [b.id, b.title])
);
function normalizeBonusName(name) {
return String(name || "").toLowerCase().replace(/[\s-]/g, "");
}
var ID_BY_NORMALIZED_TITLE = new Map(
// Core entries win over aliases (a plain title resolves to its family head id).
[...BONUS_ALIASES, ...BONUS_CATALOG].map((b) => [normalizeBonusName(b.title), b.id])
);
function bonusTitle(id) {
return TITLE_BY_ID.get(id) ?? `Bonus #${id}`;
}
function bonusIdByTitle(name) {
const key = normalizeBonusName(name);
return key ? ID_BY_NORMALIZED_TITLE.get(key) ?? null : null;
}
// src/lib/market/perk-rarity-ranges.ts
var PERK_RARITY_RANGES = [
{ id: 50, title: "Achilles", unit: "percent", yellow: [50, 74], orange: [75, 100], red: [113, 169] },
{ id: 72, title: "Assassinate", unit: "percent", yellow: [50, 70], orange: [70, 100], red: [100, 148] },
{ id: 52, title: "Backstab", unit: "percent", yellow: [30, 43], orange: [37, 64], red: [44, 96] },
{ id: 54, title: "Berserk", unit: "percent", yellow: [20, 35], orange: [36, 58], red: [60, 88] },
{ id: 57, title: "Bleed", unit: "percent", yellow: [20, 30], orange: [31, 46], red: [48, 72] },
{ id: 51, title: "Blindside", unit: "percent", yellow: [25, 39], orange: [40, 62], red: [63, 97] },
{ id: 85, title: "Bloodlust", unit: "percent", yellow: [10, 12], orange: [12, 15], red: [15, 19] },
{ id: 67, title: "Comeback", unit: "percent", yellow: [50, 70], orange: [70, 99], red: [102, 138] },
{ id: 55, title: "Conserve", unit: "percent", yellow: [25, 30], orange: [30, 37], red: [38, 49] },
{ id: 45, title: "Cripple", unit: "percent", yellow: [20, 29], orange: [29, 41], red: [43, 63] },
{ id: 49, title: "Crusher", unit: "percent", yellow: [50, 74], orange: [75, 108], red: [114, 171] },
{ id: 47, title: "Cupid", unit: "percent", yellow: [50, 74], orange: [75, 111], red: [113, 168] },
{ id: 63, title: "Deadeye", unit: "percent", yellow: [25, 45], orange: [45, 74], red: [75, 123] },
{ id: 62, title: "Deadly", unit: "percent", yellow: [2, 3], orange: [4, 6], red: [7, 10] },
{ id: 86, title: "Disarm", unit: "turns", yellow: [3, 5], orange: [5, 9], red: [9, 15] },
{ id: 74, title: "Double-edged", unit: "percent", yellow: [10, 16], orange: [16, 23], red: [25, 40] },
{ id: 105, title: "Double Tap", unit: "percent", yellow: [15, 24], orange: [24, 36], red: [38, 57] },
{ id: 87, title: "Empower", unit: "percent", yellow: [50, 89], orange: [90, 147], red: [152, 245] },
{ id: 56, title: "Eviscerate", unit: "percent", yellow: [15, 19], orange: [19, 25], red: [25, 34] },
{ id: 75, title: "Execute", unit: "percent", yellow: [15, 18], orange: [18, 22], red: [23, 30] },
{ id: 1, title: "Expose", unit: "percent", yellow: [7, 9], orange: [9, 14], red: [14, 20] },
{ id: 82, title: "Finale", unit: "percent", yellow: [10, 12], orange: [12, 15], red: [13, 18] },
{ id: 79, title: "Focus", unit: "percent", yellow: [15, 19], orange: [19, 24], red: [25, 35] },
{ id: 80, title: "Frenzy", unit: "percent", yellow: [5, 7], orange: [7, 10], red: [10, 14] },
{ id: 64, title: "Fury", unit: "percent", yellow: [10, 16], orange: [16, 24], red: [26, 40] },
{ id: 53, title: "Grace", unit: "percent", yellow: [20, 36], orange: [36, 55], red: [60, 84] },
{ id: 83, title: "Home run", unit: "percent", yellow: [50, 59], orange: [60, 74], red: [71, 93] },
// Irradiate rolls in HOURS of radiation and the guide lists orange as unknown ("?"),
// so we encode only what is stated and leave orange null (never fabricate a range).
{ id: 102, title: "Irradiate", unit: "hours", yellow: [1, 1], orange: null, red: [2, 2] },
{ id: 61, title: "Motivation", unit: "percent", yellow: [15, 19], orange: [19, 25], red: [25, 35] },
{ id: 59, title: "Paralyze", unit: "percent", yellow: [5, 8], orange: [8, 12], red: [13, 18] },
{ id: 84, title: "Parry", unit: "percent", yellow: [50, 60], orange: [60, 74], red: [71, 95] },
{ id: 101, title: "Penetrate", unit: "percent", yellow: [25, 30], orange: [30, 37], red: [38, 49] },
{ id: 21, title: "Plunder", unit: "percent", yellow: [20, 26], orange: [26, 35], red: [35, 49] },
{ id: 68, title: "Powerful", unit: "percent", yellow: [15, 22], orange: [22, 32], red: [33, 50] },
{ id: 14, title: "Proficience", unit: "percent", yellow: [20, 28], orange: [28, 39], red: [40, 60] },
// Puncture yellow was nerfed to a flat 15% (patch 422); the guide still lists the
// historical 15-20 range, which older sold gear can carry, so we keep [15, 20].
{ id: 66, title: "Puncture", unit: "percent", yellow: [15, 20], orange: [20, 27], red: [28, 38] },
{ id: 88, title: "Quicken", unit: "percent", yellow: [50, 89], orange: [90, 149], red: [150, 245] },
{ id: 65, title: "Rage", unit: "percent", yellow: [4, 6], orange: [11, 11], red: [11, 18] },
{ id: 41, title: "Revitalize", unit: "percent", yellow: [10, 13], orange: [13, 17], red: [18, 24] },
{ id: 43, title: "Roshambo", unit: "percent", yellow: [50, 73], orange: [75, 107], red: [113, 160] },
{ id: 44, title: "Slow", unit: "percent", yellow: [20, 29], orange: [29, 42], red: [43, 64] },
{ id: 73, title: "Smurf", unit: "percent", yellow: [1, 1], orange: [2, 2], red: [4, 4] },
{ id: 71, title: "Specialist", unit: "percent", yellow: [20, 28], orange: [28, 39], red: [40, 59] },
{ id: 20, title: "Stricken", unit: "percent", yellow: [30, 43], orange: [44, 63], red: [69, 99] },
{ id: 58, title: "Stun", unit: "percent", yellow: [10, 16], orange: [16, 25], red: [25, 40] },
{ id: 60, title: "Suppress", unit: "percent", yellow: [25, 32], orange: [30, 42], red: [43, 51] },
{ id: 78, title: "Sure Shot", unit: "percent", yellow: [3, 4], orange: [5, 8], red: [8, 12] },
{ id: 48, title: "Throttle", unit: "percent", yellow: [50, 74], orange: [75, 111], red: [113, 170] },
{ id: 81, title: "Warlord", unit: "percent", yellow: [15, 20], orange: [20, 27], red: [28, 45] },
{ id: 46, title: "Weaken", unit: "percent", yellow: [20, 29], orange: [29, 42], red: [43, 63] },
{ id: 76, title: "Wind-up", unit: "percent", yellow: [125, 145], orange: [145, 174], red: [175, 221] },
{ id: 42, title: "Wither", unit: "percent", yellow: [20, 29], orange: [29, 42], red: [43, 64] }
];
var BY_ID = new Map(PERK_RARITY_RANGES.map((p) => [p.id, p]));
var finite2 = (n) => typeof n === "number" && Number.isFinite(n);
function normalizeRarity(rarity) {
const r = String(rarity ?? "").toLowerCase();
return r === "yellow" || r === "orange" || r === "red" ? r : null;
}
function rollBandFor(bonusId, rarity) {
const entry = BY_ID.get(bonusId);
if (!entry || entry.unit !== "percent") return null;
const r = normalizeRarity(rarity);
if (!r) return null;
return entry[r] ?? null;
}
function rollBandWidth(bonusId, rarity) {
const band = rollBandFor(bonusId, rarity);
return band ? band[1] - band[0] : null;
}
var POSITION_LABEL = { low: "low roll", mid: "medium roll", high: "high roll" };
function classifyRollPosition(bonusId, rarity, value, tolerance = 0) {
const band = rollBandFor(bonusId, rarity);
if (!band || !finite2(value)) return null;
const [min, max] = band;
const v = value;
const inBand = v >= min - tolerance && v <= max + tolerance;
let position;
if (max <= min) {
position = "high";
} else {
const third = (max - min) / 3;
if (v < min + third) position = "low";
else if (v < min + 2 * third) position = "mid";
else position = "high";
}
return { position, inBand, min, max, label: POSITION_LABEL[position] };
}
function rollBandTolerance(bonusId, rarity, opts) {
const width = rollBandWidth(bonusId, rarity);
if (width == null) return null;
if (opts.valueSensitive(bonusId)) return opts.tightTol;
return Math.max(0, Math.min(opts.defaultTol, Math.floor(width / 2)));
}
// src/lib/market/weapon-perk-value.ts
var DEFAULT_WPV_CONFIG = {
minComps: 4,
minFitComps: 6,
minQualitySpread: 8,
minR2: 0.25,
dealDiscount: 0.1,
resaleTax: 0.05,
minTrendSales: 8,
minTrendHalf: 3,
minTrendDays: 45,
trendFlatThreshold: 0.05,
sellUndercut: 0.02,
outlierMadK: 4,
minOutlierSamples: 6,
qualityBandWidth: 15,
minBandSales: 3,
perkBandTight: 0,
perkBandDefault: 2,
minSurfaceComps: 10,
minPerkSpread: 2,
maxSurfaceCollinearity: 0.9,
surfaceHalfLifeDays: 60,
recencyHalfLifeDays: 45,
itemTypes: ["weapon"]
};
function typeAllowed(sale, config) {
return (config.itemTypes ?? ["weapon"]).includes((sale.type ?? "").toLowerCase());
}
var finite3 = (n) => typeof n === "number" && Number.isFinite(n);
var round = (n) => Math.round(n);
var RARITY_RANK = { grey: 0, gray: 0, yellow: 1, orange: 2, red: 3 };
function dominantRarity(sales) {
const counts = /* @__PURE__ */ new Map();
for (const s of sales) {
const r = typeof s.rarity === "string" ? s.rarity.toLowerCase() : "";
if (r) counts.set(r, (counts.get(r) ?? 0) + 1);
}
if (counts.size === 0) return null;
let best = null;
let bestN = -1;
for (const [r, n] of counts) {
if (n > bestN || n === bestN && (RARITY_RANK[r] ?? -1) > (RARITY_RANK[best ?? ""] ?? -1)) {
best = r;
bestN = n;
}
}
return best;
}
function median(sortedAsc) {
const n = sortedAsc.length;
if (n === 0) return 0;
const mid = Math.floor(n / 2);
return n % 2 ? sortedAsc[mid] : (sortedAsc[mid - 1] + sortedAsc[mid]) / 2;
}
function quantile(sortedAsc, q) {
const n = sortedAsc.length;
if (n === 0) return 0;
if (n === 1) return sortedAsc[0];
const pos = (n - 1) * q;
const lo = Math.floor(pos);
const frac = pos - lo;
return lo + 1 < n ? sortedAsc[lo] + (sortedAsc[lo + 1] - sortedAsc[lo]) * frac : sortedAsc[lo];
}
function weightedMedian(pairs) {
if (pairs.length === 0) return 0;
const sorted = [...pairs].sort((a, b) => a.value - b.value);
const total = sorted.reduce((s, x) => s + (x.weight > 0 ? x.weight : 0), 0);
if (!(total > 0)) return median(sorted.map((x) => x.value));
let cum = 0;
for (const x of sorted) {
cum += Math.max(0, x.weight);
if (cum >= total / 2) return x.value;
}
return sorted[sorted.length - 1].value;
}
function recencyWeightedPrice(sales, halfLifeDays, now = Date.now()) {
const hl = Math.max(1, halfLifeDays) * 86400;
const nowSec = now / 1e3;
const pairs = (sales ?? []).filter((s) => finite3(s.price) && s.price > 0).map((s) => ({ value: s.price, weight: finite3(s.timestamp) ? Math.pow(0.5, Math.max(0, nowSec - s.timestamp) / hl) : 1 }));
return round(weightedMedian(pairs));
}
var MAD_TO_SIGMA = 1.4826;
function rejectPriceOutliers(sales, config = DEFAULT_WPV_CONFIG) {
const valid = (sales ?? []).filter((s) => finite3(s.price) && s.price > 0);
if (valid.length < config.minOutlierSamples) return { kept: valid, removed: [] };
const logs = valid.map((s) => Math.log(s.price)).sort((a, b) => a - b);
const med = median(logs);
const absDev = logs.map((x) => Math.abs(x - med)).sort((a, b) => a - b);
const mad = median(absDev);
if (!(mad > 0)) return { kept: valid, removed: [] };
const threshold = config.outlierMadK * mad * MAD_TO_SIGMA;
const kept = [];
const removed = [];
for (const s of valid) {
if (Math.abs(Math.log(s.price) - med) <= threshold) kept.push(s);
else removed.push(s);
}
if (removed.length > valid.length / 3) return { kept: valid, removed: [] };
return { kept, removed };
}
function fitQualityModel(group, config) {
const pts = group.filter((s) => finite3(s.quality)).map((s) => ({ x: s.quality, y: s.price }));
if (pts.length < config.minFitComps) return null;
const qMin = Math.min(...pts.map((p) => p.x));
const qMax = Math.max(...pts.map((p) => p.x));
if (qMax - qMin < config.minQualitySpread) return null;
const now = Date.now() / 1e3;
const decay = Math.LN2 / (config.recencyHalfLifeDays * 86400);
const weights = group.filter((s) => finite3(s.quality)).map((s) => Math.exp(-decay * Math.max(0, now - s.timestamp)));
const fit = wlsLinearFit(pts, weights);
if (!fit) return null;
return {
n: fit.n,
slope: Math.round(fit.slope),
intercept: Math.round(fit.intercept),
r2: Math.round(fit.r2 * 100) / 100,
qualityMin: Math.round(qMin * 10) / 10,
qualityMax: Math.round(qMax * 10) / 10
};
}
function wlsLinearFit(pts, weights) {
const n = pts.length;
if (n < 3 || weights.length !== n) return null;
let sw = 0, swx = 0, swy = 0, swxx = 0, swxy = 0;
for (let i = 0; i < n; i++) {
const w = weights[i];
sw += w;
swx += w * pts[i].x;
swy += w * pts[i].y;
swxx += w * pts[i].x * pts[i].x;
swxy += w * pts[i].x * pts[i].y;
}
const denom = sw * swxx - swx * swx;
if (Math.abs(denom) < 1e-10) return null;
const slope = (sw * swxy - swx * swy) / denom;
const intercept = (swy - slope * swx) / sw;
let ssTot = 0, ssRes = 0;
const yMean = swy / sw;
for (let i = 0; i < n; i++) {
const w = weights[i];
const pred = slope * pts[i].x + intercept;
ssRes += w * (pts[i].y - pred) ** 2;
ssTot += w * (pts[i].y - yMean) ** 2;
}
const r2 = ssTot > 0 ? 1 - ssRes / ssTot : 0;
if (!Number.isFinite(slope) || !Number.isFinite(intercept)) return null;
return { n, slope, intercept, r2 };
}
function pearson(xs, ys) {
const n = xs.length;
if (n < 2 || ys.length !== n) return null;
let sx = 0, sy = 0;
for (let i = 0; i < n; i++) {
sx += xs[i];
sy += ys[i];
}
const mx = sx / n, my = sy / n;
let sxy = 0, sxx = 0, syy = 0;
for (let i = 0; i < n; i++) {
const dx = xs[i] - mx, dy = ys[i] - my;
sxy += dx * dy;
sxx += dx * dx;
syy += dy * dy;
}
const denom = Math.sqrt(sxx * syy);
return denom > 0 ? sxy / denom : null;
}
function det3(m) {
return m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
}
function solve3(A, b) {
const d = det3(A);
if (!Number.isFinite(d) || Math.abs(d) < 1e-6) return null;
const col = (j) => A.map((row, i) => row.map((v, k) => k === j ? b[i] : v));
const x0 = det3(col(0)) / d;
const x1 = det3(col(1)) / d;
const x2 = det3(col(2)) / d;
return [x0, x1, x2].every((v) => Number.isFinite(v)) ? [x0, x1, x2] : null;
}
var round1 = (n) => Math.round(n * 10) / 10;
function fitPerkQualityModel(group, bonusId, config = DEFAULT_WPV_CONFIG) {
const pts = [];
for (const s of group ?? []) {
if (!finite3(s.price) || s.price <= 0 || !finite3(s.quality)) continue;
const b = (s.bonuses ?? []).find((x) => x.id === bonusId && finite3(x.value));
if (!b) continue;
pts.push({ perk: b.value, q: s.quality, price: s.price, ts: finite3(s.timestamp) ? s.timestamp : 0 });
}
if (pts.length < config.minSurfaceComps) return null;
const perks = pts.map((p) => p.perk);
const quals = pts.map((p) => p.q);
const perkMin = Math.min(...perks), perkMax = Math.max(...perks);
const qMin = Math.min(...quals), qMax = Math.max(...quals);
if (perkMax - perkMin < config.minPerkSpread) return null;
const n = pts.length;
const newestTs = Math.max(...pts.map((p) => p.ts));
const halfLife = Math.max(1, config.surfaceHalfLifeDays) * 86400;
const weightOf = (ts) => newestTs > 0 && ts > 0 ? Math.pow(0.5, (newestTs - ts) / halfLife) : 1;
const qualityMedian = round1(median([...quals].sort((a, b) => a - b)));
const corr = pearson(perks, quals);
if (corr != null && Math.abs(corr) > config.maxSurfaceCollinearity) return null;
if (qMax - qMin >= config.minQualitySpread) {
let Sw = 0, Sx1 = 0, Sx2 = 0, Sy = 0, Sx1x1 = 0, Sx2x2 = 0, Sx1x2 = 0, Sx1y = 0, Sx2y = 0;
for (const p of pts) {
const w = weightOf(p.ts);
Sw += w;
Sx1 += w * p.perk;
Sx2 += w * p.q;
Sy += w * p.price;
Sx1x1 += w * p.perk * p.perk;
Sx2x2 += w * p.q * p.q;
Sx1x2 += w * p.perk * p.q;
Sx1y += w * p.perk * p.price;
Sx2y += w * p.q * p.price;
}
const sol = solve3(
[
[Sw, Sx1, Sx2],
[Sx1, Sx1x1, Sx1x2],
[Sx2, Sx1x2, Sx2x2]
],
[Sy, Sx1y, Sx2y]
);
if (!sol) return null;
const [b0, b1, b2] = sol;
const meanY2 = Sy / Sw;
let ssTot2 = 0, ssRes2 = 0;
for (const p of pts) {
const w = weightOf(p.ts);
const pred = b0 + b1 * p.perk + b2 * p.q;
ssTot2 += w * (p.price - meanY2) ** 2;
ssRes2 += w * (p.price - pred) ** 2;
}
const r22 = ssTot2 === 0 ? 0 : 1 - ssRes2 / ssTot2;
if (!(r22 >= config.minR2)) return null;
return {
n,
perkSlope: Math.round(b1),
qualitySlope: Math.round(b2),
intercept: Math.round(b0),
r2: Math.round(r22 * 100) / 100,
perkMin: round1(perkMin),
perkMax: round1(perkMax),
qualityMin: round1(qMin),
qualityMax: round1(qMax),
qualityMedian
};
}
let Pw = 0, Px = 0, Py = 0, Pxx = 0, Pxy = 0;
for (const p of pts) {
const w = weightOf(p.ts);
Pw += w;
Px += w * p.perk;
Py += w * p.price;
Pxx += w * p.perk * p.perk;
Pxy += w * p.perk * p.price;
}
const denom = Pw * Pxx - Px * Px;
if (!Number.isFinite(denom) || Math.abs(denom) < 1e-6) return null;
const slope = (Pw * Pxy - Px * Py) / denom;
const intercept = (Py - slope * Px) / Pw;
if (!Number.isFinite(slope) || !Number.isFinite(intercept)) return null;
const meanY = Py / Pw;
let ssTot = 0, ssRes = 0;
for (const p of pts) {
const w = weightOf(p.ts);
const pred = intercept + slope * p.perk;
ssTot += w * (p.price - meanY) ** 2;
ssRes += w * (p.price - pred) ** 2;
}
const r2 = ssTot === 0 ? 0 : 1 - ssRes / ssTot;
if (!(r2 >= config.minR2)) return null;
return {
n,
perkSlope: Math.round(slope),
qualitySlope: 0,
// quality flat across the cohort — price is driven by the roll
intercept: Math.round(intercept),
r2: Math.round(r2 * 100) / 100,
perkMin: round1(perkMin),
perkMax: round1(perkMax),
qualityMin: round1(qMin),
qualityMax: round1(qMax),
qualityMedian
};
}
function fairValueOnSurfaceAt(surface, perkValue, quality) {
if (!surface || !finite3(perkValue)) return null;
const p = perkValue;
const span = Math.max(0, surface.perkMax - surface.perkMin);
const margin = Math.max(3, span * 0.25);
let usedPerk = p;
let perkBound = null;
if (p > surface.perkMax) {
if (p > surface.perkMax + margin) return null;
usedPerk = surface.perkMax;
perkBound = surface.perkSlope >= 0 ? "floor" : "ceiling";
} else if (p < surface.perkMin) {
if (p < surface.perkMin - margin) return null;
usedPerk = surface.perkMin;
perkBound = surface.perkSlope >= 0 ? "ceiling" : "floor";
}
const qInRange = finite3(quality) && quality >= surface.qualityMin && quality <= surface.qualityMax;
const q = qInRange ? quality : surface.qualityMedian;
const est = surface.intercept + surface.perkSlope * usedPerk + surface.qualitySlope * q;
if (!finite3(est) || est <= 0) return null;
return { value: Math.round(est), usedTypicalQuality: !qInRange, perkBound };
}
function qualityBands(group, config) {
const width = config.qualityBandWidth > 0 ? config.qualityBandWidth : 15;
const byBand = /* @__PURE__ */ new Map();
for (const s of group) {
if (!finite3(s.quality)) continue;
const band = Math.floor(s.quality / width) * width;
const arr = byBand.get(band);
if (arr) arr.push(s);
else byBand.set(band, [s]);
}
const out = [];
for (const [band, sales] of byBand) {
if (sales.length < config.minBandSales) continue;
out.push({ band, bandWidth: width, sales: sales.length, median: recencyWeightedPrice(sales, config.recencyHalfLifeDays) });
}
out.sort((a, b) => a.band - b.band);
return out;
}
function toRecentSale(sale) {
return {
saleId: finite3(sale.saleId) ? sale.saleId : null,
timestamp: sale.timestamp,
price: round(sale.price),
quality: finite3(sale.quality) ? sale.quality : null,
damage: finite3(sale.damage) ? sale.damage : null,
accuracy: finite3(sale.accuracy) ? sale.accuracy : null,
armor: finite3(sale.armor) ? sale.armor : null,
perks: (sale.bonuses ?? []).map((b) => b.value).filter(finite3)
};
}
function representativeSales(sales, limit = 5) {
const take = Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : 0;
if (take === 0) return [];
const valid = (sales ?? []).filter((s) => finite3(s.timestamp) && finite3(s.price) && s.price > 0);
const perk = (s) => (s.bonuses ?? [])[0]?.value;
const byNewest = [...valid].sort((a, b) => b.timestamp - a.timestamp || (b.saleId ?? -1) - (a.saleId ?? -1) || b.price - a.price);
if (valid.length <= take) return byNewest.map(toRecentSale);
const perks = valid.map(perk).filter(finite3);
const spread = perks.length ? Math.max(...perks) - Math.min(...perks) : 0;
if (perks.length < valid.length || spread < 1) return byNewest.slice(0, take).map(toRecentSale);
const byPerk = [...valid].sort((a, b) => perk(a) - perk(b) || b.timestamp - a.timestamp);
const chosen = /* @__PURE__ */ new Set([byPerk[0], byPerk[byPerk.length - 1]]);
for (const s of byNewest) {
if (chosen.size >= take) break;
chosen.add(s);
}
return [...chosen].sort((a, b) => (perk(a) ?? 0) - (perk(b) ?? 0) || b.timestamp - a.timestamp).slice(0, take).map(toRecentSale);
}
function marketConcentration(sales) {
const withSeller = (sales ?? []).filter((s) => finite3(s.sellerId));
if (withSeller.length < 4) return null;
const sellerCount = /* @__PURE__ */ new Map();
const pairCount = /* @__PURE__ */ new Map();
for (const s of withSeller) {
const sid = s.sellerId;
sellerCount.set(sid, (sellerCount.get(sid) ?? 0) + 1);
if (finite3(s.buyerId)) {
const k = `${sid}:${s.buyerId}`;
pairCount.set(k, (pairCount.get(k) ?? 0) + 1);
}
}
const n = withSeller.length;
const topSeller = Math.max(...sellerCount.values());
const topPair = pairCount.size ? Math.max(...pairCount.values()) : 0;
return {
uniqueSellers: sellerCount.size,
topSellerShare: Math.round(topSeller / n * 100) / 100,
topPairShare: Math.round(topPair / n * 100) / 100,
sales: n
};
}
function concentrationFlag(c) {
if (!c || c.sales < 4) return null;
if (c.topPairShare >= 0.5) return "wash";
if (c.topSellerShare >= 0.6 && c.uniqueSellers <= 3) return "one-seller";
return null;
}
function cohortStats(group, config) {
const prices = group.map((s) => s.price).sort((a, b) => a - b);
const times = group.map((s) => s.timestamp);
const quals = group.map((s) => s.quality).filter(finite3);
const oldest = Math.min(...times);
const newest = Math.max(...times);
const windowDays = newest > oldest ? (newest - oldest) / 86400 : null;
return {
comps: group.length,
windowDays: windowDays != null ? Math.round(windowDays * 10) / 10 : null,
perDay: windowDays && windowDays > 0 ? Math.round(group.length / windowDays * 100) / 100 : null,
medianPrice: median(prices),
recencyMedian: recencyWeightedPrice(group, config.recencyHalfLifeDays),
lowPrice: prices[0],
highPrice: prices[prices.length - 1],
lowRobust: round(quantile(prices, 0.1)),
highRobust: round(quantile(prices, 0.9)),
qualityMin: quals.length ? Math.round(Math.min(...quals) * 10) / 10 : null,
qualityMax: quals.length ? Math.round(Math.max(...quals) * 10) / 10 : null,
qualityMedian: quals.length ? Math.round(median([...quals].sort((a, b) => a - b)) * 10) / 10 : null,
priceSpread: (() => {
const med = median(prices);
return med > 0 ? Math.round((quantile(prices, 0.75) - quantile(prices, 0.25)) / med * 1e3) / 1e3 : 0;
})(),
model: fitQualityModel(group, config),
recentSales: representativeSales(group),
rarity: dominantRarity(group),
bands: qualityBands(group, config),
concentration: marketConcentration(group)
};
}
function buildCohortValue(sales, config = DEFAULT_WPV_CONFIG) {
const clean2 = rejectPriceOutliers(sales, config).kept.filter((s) => finite3(s.price) && s.price > 0 && finite3(s.timestamp));
if (clean2.length === 0) return null;
const stats = cohortStats(clean2, config);
return { ...stats, trend: computePriceTrend(clean2, stats.model, config) };
}
var HIGH_IMPACT_PERK_IDS = /* @__PURE__ */ new Set([WARLORD_BONUS_ID, REVITALIZE_BONUS_ID]);
function perkValueTolerance(bonusId, config = DEFAULT_WPV_CONFIG) {
return HIGH_IMPACT_PERK_IDS.has(bonusId) ? config.perkBandTight : config.perkBandDefault;
}
function effectivePerkTolerance(bonusId, rarity, config = DEFAULT_WPV_CONFIG) {
const ranged = rollBandTolerance(bonusId, rarity, {
defaultTol: config.perkBandDefault,
tightTol: config.perkBandTight,
valueSensitive: (id) => HIGH_IMPACT_PERK_IDS.has(id)
});
return ranged ?? perkValueTolerance(bonusId, config);
}
function valueForRollBand(sales, target, config = DEFAULT_WPV_CONFIG) {
if (!target.rarity || target.perks.length === 0 || target.perks.some((p) => !finite3(p.id) || !finite3(p.value))) return null;
const wantRarity = target.rarity.toLowerCase();
const base = (sales ?? []).filter((s) => {
if (s.itemId !== target.itemId) return false;
if ((s.rarity ?? "").toLowerCase() !== wantRarity) return false;
const bs = (s.bonuses ?? []).filter((b) => finite3(b.id) && finite3(b.value));
return bs.length >= 2 === target.wantDouble;
});
const within = (tol) => base.filter((s) => {
const bs = (s.bonuses ?? []).filter((b) => finite3(b.id) && finite3(b.value));
return target.perks.every((tp) => bs.some((b) => b.id === tp.id && Math.abs(b.value - tp.value) <= tol(tp.id)));
});
const exact = buildCohortValue(within(() => 0), config);
if (exact && exact.comps >= config.minComps) return exact;
const banded = buildCohortValue(within((id) => effectivePerkTolerance(id, target.rarity, config)), config);
return banded && banded.comps >= config.minComps ? banded : null;
}
function fairValueAt(v, quality, config = DEFAULT_WPV_CONFIG) {
const m = v.model;
if (m && m.r2 >= config.minR2 && finite3(quality) && quality >= m.qualityMin && quality <= m.qualityMax) {
const est = m.slope * quality + m.intercept;
if (finite3(est) && est > 0) return { value: est, basis: "model" };
}
if (finite3(quality) && Array.isArray(v.bands) && v.bands.length > 0) {
const q = quality;
let match = v.bands.find((b) => q >= b.band && q < b.band + b.bandWidth);
if (!match) {
match = v.bands.reduce((best, b) => {
const dBest = Math.min(Math.abs(q - best.band), Math.abs(q - (best.band + best.bandWidth)));
const dCur = Math.min(Math.abs(q - b.band), Math.abs(q - (b.band + b.bandWidth)));
return dCur < dBest ? b : best;
});
}
if (match && match.median > 0) return { value: match.median, basis: "quality-band" };
}
return { value: finite3(v.recencyMedian) ? v.recencyMedian : v.medianPrice, basis: "median" };
}
function projectAuctionReturn(fairValue, currentBid, resaleTaxRate = DEFAULT_WPV_CONFIG.resaleTax) {
if (!finite3(fairValue) || fairValue <= 0 || !finite3(currentBid) || currentBid <= 0) return null;
if (!finite3(resaleTaxRate) || resaleTaxRate < 0 || resaleTaxRate >= 1) return null;
const resaleTax = round(fairValue * resaleTaxRate);
const netProceeds = round(fairValue - resaleTax);
const netProfit = netProceeds - round(currentBid);
return {
currentBid: round(currentBid),
fairValue: round(fairValue),
resaleTaxRate,
resaleTax,
netProceeds,
netProfit,
roi: Math.round(netProfit / currentBid * 1e3) / 1e3,
breakEvenBid: netProceeds
};
}
function computePriceTrend(sales, model, config = DEFAULT_WPV_CONFIG) {
const trustworthy = !!model && model.r2 >= config.minR2 && finite3(model.slope);
const usable = (sales ?? []).filter(
(s) => finite3(s.price) && s.price > 0 && finite3(s.timestamp) && (!trustworthy || finite3(s.quality))
);
if (usable.length < config.minTrendSales) return null;
const sorted = [...usable].sort((a, b) => a.timestamp - b.timestamp);
const spanDays = (sorted[sorted.length - 1].timestamp - sorted[0].timestamp) / 86400;
if (!(spanDays >= config.minTrendDays)) return null;
const quals = sorted.map((s) => s.quality).filter(finite3);
const refQuality = quals.length ? median([...quals].sort((a, b) => a - b)) : 0;
const priceOf = (s) => trustworthy && finite3(s.quality) ? s.price + model.slope * (refQuality - s.quality) : s.price;
const mid = Math.floor(sorted.length / 2);
const older = sorted.slice(0, mid);
const recent = sorted.slice(mid);
if (older.length < config.minTrendHalf || recent.length < config.minTrendHalf) return null;
const olderMedian = median(older.map(priceOf).sort((a, b) => a - b));
const recentMedian = median(recent.map(priceOf).sort((a, b) => a - b));
if (!(olderMedian > 0)) return null;
const changePct = (recentMedian - olderMedian) / olderMedian;
const direction = Math.abs(changePct) < config.trendFlatThreshold ? "flat" : changePct > 0 ? "up" : "down";
return {
direction,
changePct: Math.round(changePct * 1e3) / 1e3,
recentMedian: round(recentMedian),
olderMedian: round(olderMedian),
recentSales: recent.length,
olderSales: older.length,
spanDays: Math.round(spanDays),
qualityAdjusted: trustworthy
};
}
function valueByWeaponPerk(sales, config = DEFAULT_WPV_CONFIG) {
const valid = (sales ?? []).filter(
(s) => typeAllowed(s, config) && isRwGearSale(s) && finite3(s.price) && s.price > 0 && finite3(s.timestamp) && finite3(s.itemId)
);
const groups = /* @__PURE__ */ new Map();
const perkOf = /* @__PURE__ */ new Map();
for (const s of valid) {
for (const b of s.bonuses ?? []) {
if (!finite3(b.id)) continue;
const key = `${s.itemId}#${b.id}`;
const arr = groups.get(key);
if (arr) arr.push(s);
else groups.set(key, [s]);
if (!perkOf.has(key)) perkOf.set(key, { bonusId: b.id, title: b.title || `bonus ${b.id}` });
}
}
const out = [];
for (const [key, rawGroup] of groups) {
const group = rejectPriceOutliers(rawGroup, config).kept;
if (group.length < config.minComps) continue;
const meta = perkOf.get(key);
const rep = group[0];
const stats = cohortStats(group, config);
const perkValues = group.map((s) => (s.bonuses ?? []).find((b) => b.id === meta.bonusId)?.value).filter(finite3);
out.push({
itemId: rep.itemId,
weaponName: rep.name ?? `item ${rep.itemId}`,
subType: rep.subType,
bonusId: meta.bonusId,
perkTitle: meta.title,
perkClass: perkClassOf(meta.title),
perkValueMin: perkValues.length ? Math.min(...perkValues) : 0,
perkValueMax: perkValues.length ? Math.max(...perkValues) : 0,
...stats,
trend: computePriceTrend(group, stats.model, config)
});
}
out.sort((a, b) => b.comps - a.comps || a.itemId - b.itemId || a.bonusId - b.bonusId);
return out;
}
function valueByWeaponPerkSegmented(sales, config = DEFAULT_WPV_CONFIG) {
const valid = (sales ?? []).filter(
(s) => typeAllowed(s, config) && isRwGearSale(s) && finite3(s.price) && s.price > 0 && finite3(s.timestamp) && finite3(s.itemId)
);
const groups = /* @__PURE__ */ new Map();
const metaOf = /* @__PURE__ */ new Map();
for (const s of valid) {
const bonuses = (s.bonuses ?? []).filter((b) => finite3(b.id));
const perkCount = bonuses.length;
const doublePerk = perkCount >= 2;
const rarity = typeof s.rarity === "string" && s.rarity ? s.rarity.toLowerCase() : null;
for (const b of bonuses) {
const key = `${s.itemId}#${b.id}#${rarity ?? "unknown"}#${doublePerk ? "D" : "S"}`;
const arr = groups.get(key);
if (arr) arr.push(s);
else groups.set(key, [s]);
if (!metaOf.has(key)) metaOf.set(key, { bonusId: b.id, title: b.title || `bonus ${b.id}`, rarity, doublePerk, perkCount });
}
}
const out = [];
for (const [key, rawGroup] of groups) {
const group = rejectPriceOutliers(rawGroup, config).kept;
if (group.length < config.minComps) continue;
const meta = metaOf.get(key);
const rep = group[0];
const stats = cohortStats(group, config);
const perkValues = group.map((s) => (s.bonuses ?? []).find((b) => b.id === meta.bonusId)?.value).filter(finite3);
out.push({
itemId: rep.itemId,
weaponName: rep.name ?? `item ${rep.itemId}`,
subType: rep.subType,
bonusId: meta.bonusId,
perkTitle: meta.title,
perkClass: perkClassOf(meta.title),
perkValueMin: perkValues.length ? Math.min(...perkValues) : 0,
perkValueMax: perkValues.length ? Math.max(...perkValues) : 0,
...stats,
trend: computePriceTrend(group, stats.model, config),
cohortRarity: meta.rarity,
doublePerk: meta.doublePerk,
// perkCount from the group's actual instances (uniform within the S/D bucket, but
// report the max so a "2+" cohort reads honestly).
perkCount: Math.max(...group.map((s) => (s.bonuses ?? []).filter((b) => finite3(b.id)).length), meta.perkCount),
// Joint perk×quality plane — single-perk cohorts only (a double-perk price
// depends on two perks and cannot be a one-perk plane).
surface: meta.doublePerk ? null : fitPerkQualityModel(group, meta.bonusId, config)
});
}
out.sort((a, b) => b.comps - a.comps || a.itemId - b.itemId || a.bonusId - b.bonusId);
return out;
}
function perkRollSignatureKey(bonuses) {
const normalized = (bonuses ?? []).filter((b) => finite3(b.id) && finite3(b.value)).map((b) => ({ id: b.id, value: Math.round(b.value * 1e3) / 1e3 })).sort((a, b) => a.id - b.id || a.value - b.value);
return normalized.map((b) => `${b.id}:${b.value}`).join("+");
}
function valueByPerkSignature(sales, config = DEFAULT_WPV_CONFIG) {
const valid = (sales ?? []).filter(
(s) => typeAllowed(s, config) && isRwGearSale(s) && finite3(s.price) && s.price > 0 && finite3(s.timestamp) && finite3(s.itemId)
);
const groups = /* @__PURE__ */ new Map();
const metaOf = /* @__PURE__ */ new Map();
for (const s of valid) {
const bs = (s.bonuses ?? []).filter((b) => finite3(b.id) && finite3(b.value)).map((b) => ({ ...b, value: Math.round(b.value * 1e3) / 1e3 })).sort((a, b) => a.id - b.id || a.value - b.value);
if (bs.length === 0) continue;
const rarity = (s.rarity ?? "").toLowerCase();
const key = `${s.itemId}#${rarity}#${perkRollSignatureKey(bs)}`;
const arr = groups.get(key);
if (arr) arr.push(s);
else groups.set(key, [s]);
if (!metaOf.has(key)) {
metaOf.set(key, {
bonusIds: bs.map((b) => b.id),
values: bs.map((b) => b.value),
titles: bs.map((b) => b.title || `bonus ${b.id}`)
});
}
}
const out = [];
for (const [key, rawGroup] of groups) {
const group = rejectPriceOutliers(rawGroup, config).kept;
if (group.length < config.minComps) continue;
const meta = metaOf.get(key);
const rep = group[0];
const stats = cohortStats(group, config);
out.push({
itemId: rep.itemId,
weaponName: rep.name ?? `item ${rep.itemId}`,
subType: rep.subType,
bonusIds: meta.bonusIds,
perkValues: meta.values,
perkTitles: meta.titles,
perkClasses: meta.titles.map((t) => perkClassOf(t)),
...stats,
trend: computePriceTrend(group, stats.model, config)
});
}
out.sort((a, b) => b.bonusIds.length - a.bonusIds.length || b.comps - a.comps || a.itemId - b.itemId);
return out;
}
// src/lib/market/weapon-combat.ts
var finite4 = (n) => typeof n === "number" && Number.isFinite(n);
function parseCalendarEvents(raw) {
const cal = (raw && typeof raw === "object" ? raw.calendar : null) ?? {};
const take = (arr, kind) => (Array.isArray(arr) ? arr : []).filter((e) => finite4(Number(e.start))).map((e) => ({
title: typeof e.title === "string" ? e.title : "",
description: typeof e.description === "string" ? e.description : "",
start: Number(e.start),
end: finite4(Number(e.end)) ? Number(e.end) : Number(e.start),
kind
}));
return [...take(cal.competitions, "competition"), ...take(cal.events, "event")].sort((a, b) => a.start - b.start);
}
var ATTACKING_KEYWORDS = /halloween|trick or treat|elimination|\battack|\bfight|\bwar\b|revive|reviving/i;
function nextAttackingEvent(events, now) {
const upcoming = (events ?? []).filter((e2) => e2.start > now && ATTACKING_KEYWORDS.test(`${e2.title} ${e2.description}`)).sort((a, b) => a.start - b.start);
const e = upcoming[0];
if (!e) return null;
return { title: e.title, start: e.start, daysUntil: Math.max(0, Math.ceil((e.start - now) / 86400)) };
}
// tools/userscript/trade-runtime.ts
var CX_OWNED_ATTR = "data-cx-owned";
function cleanNativeText(value) {
return String(value ?? "").replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim();
}
function nativeText(element) {
const clone = element.cloneNode(true);
clone.querySelectorAll(`[${CX_OWNED_ATTR}]`).forEach((node) => node.remove());
if (clone.hasAttribute(CX_OWNED_ATTR)) return "";
return cleanNativeText(clone.textContent);
}
function findNativeBidCountAnchor(row, bidAreaSelector) {
const bidControl = [...row.querySelectorAll('button, [role="button"]')].find((element) => /^bid$/i.test(nativeText(element)));
if (!bidControl) return null;
const matches = /* @__PURE__ */ new Set();
for (const area of row.querySelectorAll(bidAreaSelector)) {
for (const element of [area, ...area.querySelectorAll("*")]) {
if (element.hasAttribute(CX_OWNED_ATTR) || element.closest('button, [role="button"]')) continue;
if (element.hidden || element.getAttribute("aria-hidden") === "true") continue;
if (/^\d+\s+bids?$/i.test(nativeText(element))) matches.add(element);
}
}
const leaves = [...matches].filter((candidate) => ![...matches].some((other) => other !== candidate && candidate.contains(other)));
return leaves.length === 1 && leaves[0].parentElement != null ? leaves[0] : null;
}
function moduleSelector(local, el = "") {
return `${el}[class^="${local}___"], ${el}[class*=" ${local}___"]`;
}
function parseQualityText(text) {
if (!text) return null;
const m = text.match(/([\d.]+)\s*%?\s*Quality/i) || text.match(/Quality:?\s*([\d.]+)\s*%?/i);
if (!m) return null;
const q = Number(m[1]);
return Number.isFinite(q) && q > 0 && q <= 1e3 ? Math.round(q * 10) / 10 : null;
}
function parseAuctionQuality(row) {
const scope = row.querySelector(".show-item-info") ?? row;
for (const prop of scope.querySelectorAll(moduleSelector("propertyWrapper", "li"))) {
const titleEl = prop.querySelector(moduleSelector("title")) ?? prop.querySelector('[class*="title"]');
if (!/^Quality:?$/i.test(cleanNativeText(titleEl?.textContent))) continue;
for (const v of prop.querySelectorAll(moduleSelector("valueWrapper"))) {
const q2 = parseQualityText(v.getAttribute("aria-label") || v.textContent);
if (q2 != null) return q2;
}
const q = parseQualityText(cleanNativeText(prop.textContent));
if (q != null) return q;
}
const ariaEl = scope.querySelector('[aria-label*="Quality" i]');
return parseQualityText(ariaEl?.getAttribute("aria-label"));
}
function parseAuctionProperty(row, label) {
const scope = row.querySelector(".show-item-info") ?? row;
const num = (s) => {
const m = cleanNativeText(s).match(/(\d{1,4}(?:\.\d+)?)/);
if (!m) return null;
const n = Number(m[1]);
return Number.isFinite(n) && n > 0 && n <= 1e5 ? Math.round(n * 100) / 100 : null;
};
for (const prop of scope.querySelectorAll(moduleSelector("propertyWrapper", "li"))) {
const titleEl = prop.querySelector(moduleSelector("title")) ?? prop.querySelector('[class*="title"]');
if (!label.test(cleanNativeText(titleEl?.textContent))) continue;
for (const v of prop.querySelectorAll(moduleSelector("valueWrapper"))) {
const n2 = num(v.getAttribute("aria-label") || v.textContent);
if (n2 != null) return n2;
}
const n = num(prop.textContent);
if (n != null) return n;
}
return null;
}
function hasCompleteExactSignature(rarity, perkPercents) {
return !!rarity && perkPercents.length > 0 && perkPercents.every((percent) => percent != null && Number.isFinite(percent));
}
function shouldScheduleDeep(state) {
return state.hasJob && !state.exhausted && !state.inFlight && !state.queued && !state.failed && state.needsDeep;
}
function parseNativeBidPrice(row, priceSelector) {
const bidAreas = [...row.querySelectorAll(priceSelector)];
for (const area of [...bidAreas, row]) {
const explicit = nativeText(area).match(/\bbid\s*:\s*\$\s*([\d,]+)/i);
if (!explicit) continue;
const value = Number(explicit[1].replace(/,/g, ""));
if (Number.isFinite(value) && value > 0) return value;
}
for (const area of bidAreas) {
const generic = nativeText(area).match(/\$\s*([\d,]+)/);
if (!generic) continue;
const value = Number(generic[1].replace(/,/g, ""));
if (Number.isFinite(value) && value > 0) return value;
}
return null;
}
var TwoPhaseItemQueue = class {
constructor() {
this.initial = [];
this.deep = [];
}
enqueueInitial(itemId) {
if (!this.has(itemId)) this.initial.push(itemId);
}
enqueueDeep(itemId) {
if (!this.has(itemId)) this.deep.push(itemId);
}
shift() {
const initial = this.initial.shift();
if (initial != null) return { itemId: initial, phase: "initial" };
const deep = this.deep.shift();
return deep != null ? { itemId: deep, phase: "deep" } : null;
}
has(itemId) {
return this.initial.includes(itemId) || this.deep.includes(itemId);
}
removeNotIn(visibleItemIds) {
for (let i = this.initial.length - 1; i >= 0; i--) {
if (!visibleItemIds.has(this.initial[i])) this.initial.splice(i, 1);
}
for (let i = this.deep.length - 1; i >= 0; i--) {
if (!visibleItemIds.has(this.deep[i])) this.deep.splice(i, 1);
}
}
clear() {
this.initial.length = 0;
this.deep.length = 0;
}
get size() {
return this.initial.length + this.deep.length;
}
/** Initial-page work still waiting; the first pass isn't done until this is zero. */
get initialSize() {
return this.initial.length;
}
/** Deep-enrichment work still waiting (runs quietly after the first pass). */
get deepSize() {
return this.deep.length;
}
};
var RequestStartLimiter = class {
constructor(spacingMs, now = Date.now, wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
this.spacingMs = spacingMs;
this.now = now;
this.wait = wait;
this.lastStart = null;
}
async beforeStart(isActive = () => true) {
if (!isActive()) return false;
if (this.lastStart != null) {
const delay = Math.max(0, this.lastStart + this.spacingMs - this.now());
if (delay > 0) await this.wait(delay);
}
if (!isActive()) return false;
this.lastStart = this.now();
return true;
}
};
function finitePositive(value) {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
}
function nullableFinite(value) {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
var RARITY_CODE = { yellow: 1, orange: 2, red: 3 };
var CODE_RARITY = { 1: "yellow", 2: "orange", 3: "red" };
var FIVE_MIN_MS = 5 * 6e4;
function capSales(sales, cap) {
const seen = /* @__PURE__ */ new Set();
const deduped = [];
for (const s of sales) {
if (s.saleId != null) {
if (seen.has(s.saleId)) continue;
seen.add(s.saleId);
}
deduped.push(s);
}
if (deduped.length <= cap) return deduped;
return [...deduped].sort((a, b) => b.timestamp - a.timestamp).slice(0, Math.max(0, cap));
}
function encodeSale(s) {
if (s.saleId == null || !Number.isFinite(s.saleId)) return null;
if (!(s.price > 0) || !Number.isFinite(s.timestamp)) return null;
const code = s.rarity ? RARITY_CODE[s.rarity.toLowerCase()] : void 0;
if (!code) return null;
const bonuses = (s.bonuses ?? []).filter((b) => Number.isFinite(b.id) && Number.isFinite(b.value)).map((b) => [b.id, b.value]);
if (bonuses.length === 0) return null;
return [s.saleId, s.price, s.timestamp, nullableFinite(s.quality), code, bonuses, nullableFinite(s.damage), nullableFinite(s.accuracy), nullableFinite(s.sellerId), nullableFinite(s.buyerId), nullableFinite(s.armor)];
}
function decodeSale(tuple, itemId, name, subType) {
if (!Array.isArray(tuple) || tuple.length < 6) return null;
const [saleId, price, ts, quality, code, bonuses, damage, accuracy, sellerId, buyerId, armor] = tuple;
const p = finitePositive(price);
const t = finitePositive(ts);
const sid = nullableFinite(saleId);
const rarity = CODE_RARITY[Number(code)];
if (p == null || t == null || sid == null || !rarity) return null;
if (!Array.isArray(bonuses) || bonuses.length === 0) return null;
const parsedBonuses = [];
for (const b of bonuses) {
if (!Array.isArray(b) || b.length < 2) return null;
const id = Number(b[0]);
const value = Number(b[1]);
if (!Number.isFinite(id) || !Number.isFinite(value)) return null;
parsedBonuses.push({ id, title: bonusTitle(id), value });
}
return {
saleId: sid,
price: p,
timestamp: t,
quality: nullableFinite(quality),
damage: nullableFinite(damage),
accuracy: nullableFinite(accuracy),
armor: nullableFinite(armor),
sellerId: nullableFinite(sellerId),
buyerId: nullableFinite(buyerId),
rarity,
bonuses: parsedBonuses,
uid: null,
itemId,
name,
type: "Weapon",
subType
};
}
function encodeSalesDb(items, limits) {
const kept = [...items].filter((it) => Number.isFinite(it.itemId) && it.itemId > 0 && Array.isArray(it.sales) && it.sales.length > 0).sort((a, b) => b.at - a.at).slice(0, Math.max(0, limits.maxItems));
const out = kept.map((it) => {
const sales = capSales(it.sales, limits.maxSalesPerItem).map(encodeSale).filter((t) => t != null);
return {
i: it.itemId,
n: it.name ?? null,
s: it.subType ?? null,
c: it.category ?? null,
at: it.at,
to: it.to ?? null,
pp: it.pagesPulled,
ex: it.exhausted === true,
d: sales
};
}).filter((it) => it.d.length > 0);
return JSON.stringify({ v: 4, items: out });
}
function decodeSalesDb(raw, now, limits) {
if (!raw) return [];
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
return [];
}
if (parsed.v !== 4 || !Array.isArray(parsed.items)) return [];
const cutoffSec = Math.floor((now - limits.windowMs) / 1e3);
const futureSec = Math.floor((now + FIVE_MIN_MS) / 1e3);
const items = [];
for (const candidate of parsed.items) {
if (!candidate || typeof candidate !== "object") continue;
const it = candidate;
const itemId = finitePositive(it.i);
const at = nullableFinite(it.at);
if (itemId == null || at == null) continue;
if (at > now + FIVE_MIN_MS || now - at > limits.maxAgeMs) continue;
const name = typeof it.n === "string" ? it.n : null;
const subType = typeof it.s === "string" ? it.s : null;
const category = typeof it.c === "string" ? it.c : null;
const to = nullableFinite(it.to);
if (to != null && (to <= 0 || to > futureSec)) continue;
let pagesPulled = Number(it.pp);
if (!Number.isInteger(pagesPulled) || pagesPulled < 0) pagesPulled = 0;
const rawSales = Array.isArray(it.d) ? it.d : [];
const seen = /* @__PURE__ */ new Set();
const sales = [];
for (const tuple of rawSales) {
const sale = decodeSale(tuple, itemId, name, subType);
if (!sale) continue;
if (sale.timestamp < cutoffSec || sale.timestamp > futureSec) continue;
if (sale.saleId != null) {
if (seen.has(sale.saleId)) continue;
seen.add(sale.saleId);
}
sales.push(sale);
if (sales.length >= limits.maxSalesPerItem) break;
}
if (sales.length === 0) continue;
items.push({ itemId, name, subType, category, at, to, pagesPulled, exhausted: it.ex === true, sales });
if (items.length >= Math.max(0, limits.maxItems)) break;
}
return items;
}
function resolveMatchTier(exactRoll, hasBand, isFloor) {
if (exactRoll) return "exact";
if (hasBand) return "close";
if (isFloor) return "rare";
return "rough";
}
function suppressFlip(tier) {
return tier === "rare";
}
var TIMER_SELECTORS = [
'[class*="timer" i]',
'[class*="countdown" i]',
'[class*="timeLeft" i]',
'[class*="time-left" i]',
'[class*="auctionEnd" i]',
'[class*="expire" i]'
].join(",");
function parseAuctionTimer(row) {
const timerEls = row.querySelectorAll(TIMER_SELECTORS);
for (const el of timerEls) {
const seconds = parseTimerText(cleanNativeText(el.textContent));
if (seconds != null) return seconds;
const label = el.getAttribute("aria-label") || el.getAttribute("data-time") || el.getAttribute("title");
if (label) {
const s = parseTimerText(cleanNativeText(label));
if (s != null) return s;
}
}
const rowText = cleanNativeText(row.textContent);
const timeLeftMatch = rowText.match(/time\s*left\s*:?\s*(?:\u231b|\u23f3)?\s*([\ddhms\s:]+)/i);
if (timeLeftMatch) {
const seconds = parseTimerText(timeLeftMatch[1].trim());
if (seconds != null) return seconds;
}
const bidAreas = row.querySelectorAll('.c-bid-wrap, .bids-wrap, [class*="c-bid"]');
for (const area of bidAreas) {
const seconds = parseTimerText(cleanNativeText(area.textContent));
if (seconds != null) return seconds;
}
return null;
}
function parseTimerText(text) {
if (!text) return null;
const s = text.trim().toLowerCase();
if (!s || s.length > 40) return null;
if (/^<\s*1\s*m/i.test(s)) return 30;
const named = s.match(/(?:(\d+)\s*d)?\s*(?:(\d+)\s*h)?\s*(?:(\d+)\s*m(?:in)?)?\s*(?:(\d+)\s*s)?/);
if (named && (named[1] || named[2] || named[3] || named[4])) {
const days = Number(named[1] || 0);
const hours = Number(named[2] || 0);
const mins = Number(named[3] || 0);
const secs = Number(named[4] || 0);
const total = days * 86400 + hours * 3600 + mins * 60 + secs;
return total > 0 && total < 30 * 86400 ? total : null;
}
const colon = s.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
if (colon) {
if (colon[3] != null) {
const total2 = Number(colon[1]) * 3600 + Number(colon[2]) * 60 + Number(colon[3]);
return total2 > 0 && total2 < 30 * 86400 ? total2 : null;
}
const total = Number(colon[1]) * 60 + Number(colon[2]);
return total > 0 && total < 30 * 86400 ? total : null;
}
return null;
}
function formatTimeRemaining(seconds) {
if (seconds == null || seconds <= 0) return null;
if (seconds < 60) return "< 1m";
const days = Math.floor(seconds / 86400);
const hours = Math.floor(seconds % 86400 / 3600);
const mins = Math.floor(seconds % 3600 / 60);
if (days > 0) return `${days}d ${hours}h`;
if (hours > 0) return `${hours}h ${mins}m`;
return `${mins}m`;
}
function deriveConfidence(comps, recentSales, nowSec) {
const newest = recentSales.length > 0 ? Math.max(...recentSales.map((s) => s.timestamp)) : null;
const freshDays = newest != null ? Math.max(0, Math.round((nowSec - newest) / 86400)) : null;
let tier;
if (comps >= 16 && freshDays != null && freshDays <= 14) {
tier = "solid";
} else if (comps >= 6 && (freshDays == null || freshDays <= 45)) {
tier = "moderate";
} else {
tier = "thin";
}
let label;
if (freshDays != null && freshDays <= 1) {
label = `${comps} comps, today`;
} else if (freshDays != null) {
label = `${comps} comps, ${freshDays}d ago`;
} else {
label = `${comps} comps`;
}
return { comps, freshDays, tier, label };
}
// tools/userscript/trade-entry.ts
var PDA_APIKEY = "###PDA-APIKEY###";
var STORAGE_KEY = "cortex_overlay_key_v1";
var CATALOG_KEY = "cortex_trade_catalog_v4";
var SALES_DB_KEY = "cortex_auction_db_v3";
var LEGACY_SALES_DB_KEY = "cortex_auction_db_v2";
var LEGACY_SALES_CACHE_KEY = "cortex_auction_sales_v1";
var CATALOG_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
var LEGACY_DISABLED_KEY = "cortex_auction_disabled_v1";
var LEGACY_COLLAPSE_KEY = "cortex_auction_collapsed_v1";
var ROOT_ID = "cortex-auction-root";
var STYLE_ID = "cx-auction-style";
var FONT_ID = "cx-auction-font";
var OWNER_EVENT = "cortex-auction-owner";
var THEME_KEY = "cortex_trade_theme_v1";
var BEST_BUYS_KEY = "cortex_trade_bestbuys_v1";
var UI_VERSION = "2.38.0";
var AAV_WORD = "AAV";
var AAV_FULL = "Average Auction Value";
function compareVersions(a, b) {
const left = a.split(".").map(Number);
const right = b.split(".").map(Number);
for (let i = 0; i < Math.max(left.length, right.length); i++) {
const delta = (Number.isFinite(left[i]) ? left[i] : 0) - (Number.isFinite(right[i]) ? right[i] : 0);
if (delta !== 0) return delta;
}
return 0;
}
var PAGE_LIMIT = 100;
var WINDOW_MS = 365 * 24 * 60 * 60 * 1e3;
var REQUEST_SPACING_MS = 700;
var PERSIST_DEBOUNCE_MS = 1500;
var REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1e3;
var LS_DB_LIMITS = {
maxItems: 48,
maxSalesPerItem: 600,
maxAgeMs: 14 * 24 * 60 * 60 * 1e3,
windowMs: WINDOW_MS
};
var IDB_DB_LIMITS = {
maxItems: 72,
maxSalesPerItem: 900,
maxAgeMs: 21 * 24 * 60 * 60 * 1e3,
windowMs: WINDOW_MS
};
var requestLimiter = new RequestStartLimiter(REQUEST_SPACING_MS);
var GEAR_CONFIG = { ...DEFAULT_WPV_CONFIG, itemTypes: ["weapon", "armor"] };
var TORN_KEY_URL = "https://www.torn.com/preferences.php#tab=api";
var PRIVACY_URL = "https://torncortex.com/privacy";
var DATA_POLICY_URL = "https://torncortex.com/data-policy";
function storageGet(name) {
try {
if (typeof GM_getValue === "function") return GM_getValue(name) ?? null;
} catch {
}
try {
return window.localStorage.getItem(name);
} catch {
return null;
}
}
function storageSet(name, value) {
try {
if (typeof GM_setValue === "function") {
GM_setValue(name, value);
return;
}
} catch {
}
try {
window.localStorage.setItem(name, value);
} catch {
}
}
function storageDelete(name) {
try {
if (typeof GM_deleteValue === "function") {
GM_deleteValue(name);
return;
}
} catch {
}
try {
window.localStorage.removeItem(name);
} catch {
}
}
var IDB_NAME = "cortex_trade";
var IDB_STORE = "kv";
function idbAvailable() {
try {
return typeof indexedDB !== "undefined" && indexedDB != null;
} catch {
return false;
}
}
function dbLimits() {
return idbAvailable() ? IDB_DB_LIMITS : LS_DB_LIMITS;
}
function idbOpen() {
return new Promise((resolve) => {
try {
const req = indexedDB.open(IDB_NAME, 1);
req.onupgradeneeded = () => {
const db = req.result;
if (!db.objectStoreNames.contains(IDB_STORE)) db.createObjectStore(IDB_STORE);
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => resolve(null);
req.onblocked = () => resolve(null);
} catch {
resolve(null);
}
});
}
function idbGet(key) {
return idbOpen().then(
(db) => new Promise((resolve) => {
if (!db) return resolve(null);
try {
const tx = db.transaction(IDB_STORE, "readonly");
const req = tx.objectStore(IDB_STORE).get(key);
req.onsuccess = () => resolve(typeof req.result === "string" ? req.result : null);
req.onerror = () => resolve(null);
tx.oncomplete = () => db.close();
} catch {
resolve(null);
}
})
);
}
function idbSet(key, value) {
return idbOpen().then(
(db) => new Promise((resolve) => {
if (!db) return resolve(false);
try {
const tx = db.transaction(IDB_STORE, "readwrite");
tx.objectStore(IDB_STORE).put(value, key);
tx.oncomplete = () => {
db.close();
resolve(true);
};
tx.onerror = () => resolve(false);
tx.onabort = () => resolve(false);
} catch {
resolve(false);
}
})
);
}
function idbDelete(key) {
return idbOpen().then(
(db) => new Promise((resolve) => {
if (!db) return resolve();
try {
const tx = db.transaction(IDB_STORE, "readwrite");
tx.objectStore(IDB_STORE).delete(key);
tx.oncomplete = () => {
db.close();
resolve();
};
tx.onerror = () => resolve();
} catch {
resolve();
}
})
);
}
async function loadDbPayload() {
if (idbAvailable()) {
const fromIdb = await idbGet(SALES_DB_KEY);
if (fromIdb != null) return fromIdb;
const legacy = storageGet(SALES_DB_KEY);
if (legacy != null) {
await idbSet(SALES_DB_KEY, legacy);
return legacy;
}
return null;
}
return storageGet(SALES_DB_KEY);
}
async function saveDbPayload(payload) {
if (idbAvailable()) {
const ok = await idbSet(SALES_DB_KEY, payload);
if (ok) return;
}
storageSet(SALES_DB_KEY, payload);
}
async function deleteDbPayload() {
storageDelete(SALES_DB_KEY);
if (idbAvailable()) await idbDelete(SALES_DB_KEY);
}
function readTheme() {
return storageGet(THEME_KEY) === "light" ? "light" : "dark";
}
function pdaKey() {
return isValidKeyFormat(PDA_APIKEY) ? PDA_APIKEY : null;
}
function getKey() {
const pda = pdaKey();
if (pda) return pda;
const k = storageGet(STORAGE_KEY);
return k && isValidKeyFormat(k) ? k : null;
}
function pdaBridge() {
const w = window;
return w.flutter_inappwebview && typeof w.flutter_inappwebview.callHandler === "function" ? w.flutter_inappwebview : null;
}
async function fetchJson(path, key, isActive = () => true) {
const url = buildTornUrl(path, key);
if (!isTornHost(url)) return null;
if (!await requestLimiter.beforeStart(isActive)) return null;
const bridge = pdaBridge();
try {
if (bridge) {
const res2 = await bridge.callHandler("PDA_httpGet", url, {});
const text = res2?.responseText ?? "";
const data2 = text ? JSON.parse(text) : null;
if (!data2 || data2.error) return null;
return data2;
}
const res = await fetch(url, { method: "GET", credentials: "omit" });
const data = await res.json();
if (data.error) return null;
return data;
} catch {
return null;
}
}
function pageIsActive() {
try {
if (document.visibilityState === "hidden") return false;
if (pdaBridge()) return true;
return typeof document.hasFocus !== "function" || document.hasFocus();
} catch {
return false;
}
}
function hasLayout() {
try {
return typeof document !== "undefined" && document.body != null && document.body.getClientRects().length > 0;
} catch {
return false;
}
}
function rowIsVisible(row) {
return !hasLayout() || row.getClientRects().length > 0;
}
function isDesktopView() {
try {
return !pdaBridge() && typeof window !== "undefined" && window.innerWidth >= 900;
} catch {
return false;
}
}
function ensureFont() {
try {
if (document.getElementById(FONT_ID)) return;
const preconnect = document.createElement("link");
preconnect.rel = "preconnect";
preconnect.href = "https://fonts.gstatic.com";
preconnect.crossOrigin = "anonymous";
const sheet = document.createElement("link");
sheet.id = FONT_ID;
sheet.rel = "stylesheet";
sheet.href = "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap";
(document.head || document.documentElement).append(preconnect, sheet);
} catch {
}
}
function readCatalogCache() {
const raw = storageGet(CATALOG_KEY);
if (!raw) return null;
try {
const parsed = JSON.parse(raw);
if (!parsed.at || Date.now() - parsed.at > CATALOG_TTL_MS || !Array.isArray(parsed.items)) return null;
return parsed.items.map((i) => ({ ...i, subType: i.subType ?? null, category: i.category ?? null, kind: i.kind ?? "weapon", lower: i.name.toLowerCase() }));
} catch {
return null;
}
}
async function fetchCategory(cat, kind, key) {
const raw = await fetchJson(`torn/items?cat=${cat}`, key, pageIsActive);
const items = raw?.items;
const list = Array.isArray(items) ? items : items && typeof items === "object" ? Object.values(items) : [];
return list.filter((it) => it && it.type === cat && it.is_tradable !== false && Number.isFinite(Number(it.id))).map((it) => {
const details = it.details ?? {};
const base = details.base_stats ?? {};
const damage = Number(base.damage);
const accuracy = Number(base.accuracy);
return {
id: Number(it.id),
name: typeof it.name === "string" ? it.name : `item ${it.id}`,
baseDamage: Number.isFinite(damage) ? damage : null,
baseAccuracy: Number.isFinite(accuracy) ? accuracy : null,
subType: typeof it.sub_type === "string" ? it.sub_type : null,
category: typeof details.category === "string" ? details.category : null,
kind
};
});
}
async function loadCatalog(key) {
const cached = readCatalogCache();
if (cached) return sortForMatch(cached);
const weapons = await fetchCategory("Weapon", "weapon", key);
let armor = [];
try {
armor = await fetchCategory("Armor", "armor", key);
} catch {
}
const entries = [...weapons, ...armor];
if (entries.length > 0) storageSet(CATALOG_KEY, JSON.stringify({ at: Date.now(), items: entries }));
return sortForMatch(entries.map((e) => ({ ...e, kind: e.kind ?? "weapon", lower: e.name.toLowerCase() })));
}
function sortForMatch(entries) {
return [...entries].sort((a, b) => b.lower.length - a.lower.length);
}
async function fetchSalesPage(itemId, key, to, isActive = pageIsActive) {
if (!isActive()) return null;
const path = `market/${itemId}/auctionhouse?limit=${PAGE_LIMIT}&sort=desc${to ? `&to=${to}` : ""}`;
const raw = await fetchJson(path, key, isActive);
if (!raw) return null;
const { sales, oldestTimestamp } = parseAuctionSales(raw);
return { sales, oldestTimestamp };
}
function money(n) {
if (!Number.isFinite(n)) return "\u2014";
const s = n < 0 ? "-" : "";
const v = Math.abs(n);
if (v >= 1e9) return `${s}$${(v / 1e9).toFixed(2)}b`;
if (v >= 1e6) return `${s}$${(v / 1e6).toFixed(1)}m`;
if (v >= 1e3) return `${s}$${(v / 1e3).toFixed(1)}k`;
return `${s}$${Math.round(v)}`;
}
function moneyC(n) {
return money(n).replace(/\.0+(?=[a-z]?$)/, "");
}
var TIER_COLOR = { good: "#56d575", fair: "#f0bb3b", over: "#fa6863" };
var AUCTION_ROW_SEL = [
"#types-tab-1 .items-list > li",
'#types-tab-1 li[class*="bg-"]',
"#types-tab-2 .items-list > li",
'#types-tab-2 li[class*="bg-"]',
"#auction-house-tabs .items-list > li",
'#auction-house-tabs li[class*="bg-"]',
'.items-list > li[class*="bg-"]'
].join(",");
var TITLE_SEL = ".item-cont-wrap > .title, .item-cont-wrap .title, .item-cont-wrap .item-name, .item-name";
var BONUS_SPAN_SEL = '.iconsbonuses span[title], [class*="iconsbonuses"] span[title]';
var PRICE_SEL = '.c-bid-wrap, .bids-wrap, [class*="c-bid"]';
var BADGE_ATTR = "data-cx-sig";
var BADGE_CLASS = "cx-auction-badge";
function clean(s) {
return String(s || "").replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim();
}
function normalizePerk(name) {
return String(name || "").toLowerCase().replace(/[\s-]/g, "");
}
function rarityOf(row) {
const blob = `${row.className} ${row.outerHTML.slice(0, 400)}`.toLowerCase();
if (/\b(bg|glow)-red\b|glow-red/.test(blob)) return "red";
if (/\b(bg|glow)-orange\b|glow-orange/.test(blob)) return "orange";
if (/\b(bg|glow)-yellow\b|glow-yellow/.test(blob)) return "yellow";
return null;
}
function parseBonuses(row) {
const out = [];
const spans = row.querySelectorAll(BONUS_SPAN_SEL);
spans.forEach((span) => {
const title = span.getAttribute("title") || "";
let name = "";
const b = title.match(/<b[^>]*>\s*([^<]+?)\s*<\/b>/i);
if (b) name = clean(b[1]);
if (!name) {
const icon = span.querySelector('i[class*="bonus-attachment-"]');
const m = icon && String(icon.className).match(/bonus-attachment-([a-z0-9-]+)/i);
if (m) name = m[1].replace(/-/g, " ");
}
if (!name) return;
const plain = title.replace(/<[^>]+>/g, " ");
const pm = plain.match(/(\d+(?:\.\d+)?)\s*%/);
out.push({ name: name.toLowerCase(), percent: pm ? Number(pm[1]) : null });
});
return out;
}
function parsePrice(row) {
return parseNativeBidPrice(row, PRICE_SEL);
}
var QUALITY_EL_SEL = '[class*="quality" i], [class*="Quality"], [title*="quality" i]';
function parseQuality(row) {
const fromPanel = parseAuctionQuality(row);
if (fromPanel != null) return fromPanel;
const qEl = row.querySelector(QUALITY_EL_SEL);
if (!qEl) return null;
return parseQualityText(clean(`${qEl.getAttribute("title") || ""} ${qEl.textContent || ""}`));
}
function parseRowStats(row) {
return {
damage: parseAuctionProperty(row, /^Damage:?$/i),
accuracy: parseAuctionProperty(row, /^Accuracy:?$/i)
};
}
function main() {
if (window.top !== window.self) return;
if (location.pathname !== "/amarket.php") return;
const existingRoot = document.querySelector(`#${ROOT_ID}`);
const existingVersion = existingRoot?.dataset.cxVersion ?? "";
if (existingRoot && existingVersion && compareVersions(existingVersion, UI_VERSION) >= 0) return;
document.dispatchEvent(new CustomEvent(OWNER_EVENT, { detail: { version: UI_VERSION } }));
document.querySelectorAll(`#${ROOT_ID}`).forEach((n) => n.remove());
document.getElementById(STYLE_ID)?.remove();
storageDelete(LEGACY_DISABLED_KEY);
storageDelete(LEGACY_COLLAPSE_KEY);
storageDelete(LEGACY_SALES_CACHE_KEY);
storageDelete(LEGACY_SALES_DB_KEY);
let catalog = [];
let catalogReady = false;
let catalogLoading = false;
let catalogFailed = false;
const valueCache = /* @__PURE__ */ new Map();
const jobs = /* @__PURE__ */ new Map();
const catalogById = /* @__PURE__ */ new Map();
const requirementsByItem = /* @__PURE__ */ new Map();
const inFlight = /* @__PURE__ */ new Set();
const failedItems = /* @__PURE__ */ new Set();
const queue = new TwoPhaseItemQueue();
let draining = false;
let initialInFlight = 0;
let stopped = false;
let renderTimer = null;
let persistTimer = null;
let observer = null;
let season = null;
function rebuildValue(itemId, at = Date.now()) {
const job = jobs.get(itemId);
if (!job) return;
const cutoffSec = Math.floor((Date.now() - WINDOW_MS) / 1e3);
const sales = job.sales.filter((sale) => sale.timestamp >= cutoffSec);
valueCache.set(itemId, {
at,
sales,
cards: valueByWeaponPerk(sales, GEAR_CONFIG),
broad: valueByWeaponPerkSegmented(sales, GEAR_CONFIG),
sigs: valueByPerkSignature(sales, GEAR_CONFIG)
});
}
let restorePromise = null;
function ensureRestored() {
if (!restorePromise) restorePromise = restoreSalesDb().catch(() => {
});
return restorePromise;
}
async function restoreSalesDb() {
const payload = await loadDbPayload();
if (stopped) return;
for (const saved of decodeSalesDb(payload, Date.now(), dbLimits())) {
const seen = new Set(saved.sales.flatMap((sale) => sale.saleId != null ? [sale.saleId] : []));
jobs.set(saved.itemId, {
itemId: saved.itemId,
sales: saved.sales,
seen,
to: saved.to,
pagesPulled: saved.pagesPulled,
exhausted: saved.exhausted,
completedAt: saved.at,
category: saved.category
});
rebuildValue(saved.itemId, saved.at);
}
}
async function flushSalesDb() {
if (persistTimer) {
clearTimeout(persistTimer);
persistTimer = null;
}
const items = [...jobs.values()].filter((job) => job.completedAt != null && job.sales.length > 0).map((job) => {
const entry = catalogById.get(job.itemId);
const rep = job.sales[0];
return {
itemId: job.itemId,
name: entry?.name ?? rep?.name ?? null,
subType: entry?.subType ?? rep?.subType ?? null,
category: job.category ?? entry?.category ?? null,
at: job.completedAt ?? Date.now(),
to: job.to,
pagesPulled: job.pagesPulled,
exhausted: job.exhausted,
sales: job.sales
};
});
await saveDbPayload(encodeSalesDb(items, dbLimits()));
}
function persistSalesDb() {
if (persistTimer) return;
persistTimer = setTimeout(() => {
persistTimer = null;
if (!stopped) void flushSalesDb();
}, PERSIST_DEBOUNCE_MS);
}
function requirementNeedsDeep(itemId, cached, requirement) {
if (!hasCompleteExactSignature(requirement.rarity, requirement.bonuses.map((bonus) => bonus.percent))) return false;
if (signatureForRow(cached.sigs, requirement.bonuses, requirement.rarity) != null) return false;
const seg = segmentedForRow(cached.broad, requirement.bonuses, requirement.rarity);
if (seg && seg.surface) {
const roll = requirement.bonuses[0]?.percent;
if (roll != null && (roll < seg.surface.perkMin || roll > seg.surface.perkMax)) return true;
return false;
}
if (requirement.bonuses.length === 1) return true;
return rollBandValue(cached.sales, itemId, requirement.bonuses, requirement.rarity) == null;
}
function itemNeedsDeep(itemId) {
const cached = valueCache.get(itemId);
const requirements = requirementsByItem.get(itemId) ?? [];
return !!cached && requirements.some((requirement) => requirementNeedsDeep(itemId, cached, requirement));
}
const deepCount = /* @__PURE__ */ new Map();
const MAX_DEEP_PER_SESSION = 6;
const MAX_DEEP_PULLS_PER_VIEW = 6;
let deepPullsThisView = 0;
let deepViewKey = "";
function enqueue(itemId) {
if (failedItems.has(itemId) || inFlight.has(itemId) || queue.has(itemId)) return;
const cached = valueCache.get(itemId);
if (cached && Date.now() - cached.at < REFRESH_INTERVAL_MS) return;
queue.enqueueInitial(itemId);
void drain();
}
function scheduleDeepWork(visibleItemIds) {
if (!firstPassComplete()) return;
for (const itemId of visibleItemIds) {
if (deepPullsThisView + queue.deepSize >= MAX_DEEP_PULLS_PER_VIEW) break;
const job = jobs.get(itemId);
const needsDeep = itemNeedsDeep(itemId);
if (!shouldScheduleDeep({
hasJob: job != null,
exhausted: job?.exhausted ?? false,
inFlight: inFlight.has(itemId),
queued: queue.has(itemId),
failed: failedItems.has(itemId),
needsDeep
})) continue;
if (job) {
job.completedAt = null;
queue.enqueueDeep(itemId);
}
}
void drain();
}
function canDeepen(job) {
return !job.exhausted && itemNeedsDeep(job.itemId) && job.sales.length < dbLimits().maxSalesPerItem && (deepCount.get(job.itemId) ?? 0) < MAX_DEEP_PER_SESSION;
}
function firstPassComplete() {
return catalogReady && queue.initialSize === 0 && initialInFlight === 0;
}
function deepBudgetLeft() {
return deepPullsThisView < MAX_DEEP_PULLS_PER_VIEW;
}
async function drain() {
if (draining || stopped) return;
draining = true;
try {
while (!stopped && queue.size > 0) {
if (!pageIsActive()) break;
const key = getKey();
if (!key) break;
const next = queue.shift();
if (!next) break;
const { itemId, phase } = next;
if (inFlight.has(itemId)) continue;
let job = jobs.get(itemId);
if (!job) {
job = { itemId, sales: [], seen: /* @__PURE__ */ new Set(), to: null, pagesPulled: 0, exhausted: false, completedAt: null, category: catalogById.get(itemId)?.category ?? null };
jobs.set(itemId, job);
}
if (phase === "deep" && (!canDeepen(job) || !deepBudgetLeft())) {
if (job.completedAt == null) {
job.completedAt = Date.now();
persistSalesDb();
}
continue;
}
inFlight.add(itemId);
if (phase === "initial") initialInFlight++;
job.completedAt = null;
const firstPull = job.pagesPulled === 0;
const cursor = phase === "deep" ? job.to : null;
try {
const page = await fetchSalesPage(itemId, key, cursor, () => !stopped && pageIsActive());
if (stopped) return;
if (!page || !pageIsActive()) {
if (!pageIsActive()) {
if (phase === "initial") queue.enqueueInitial(itemId);
else queue.enqueueDeep(itemId);
} else {
failedItems.add(itemId);
}
continue;
}
failedItems.delete(itemId);
const cutoffSec = Math.floor((Date.now() - WINDOW_MS) / 1e3);
for (const sale of page.sales) {
if (sale.timestamp < cutoffSec) continue;
if (sale.saleId != null && job.seen.has(sale.saleId)) continue;
if (sale.saleId != null) job.seen.add(sale.saleId);
job.sales.push(sale);
}
job.pagesPulled++;
if (phase === "deep") {
deepCount.set(itemId, (deepCount.get(itemId) ?? 0) + 1);
deepPullsThisView++;
}
if (phase === "deep" || firstPull) {
job.to = page.oldestTimestamp;
job.exhausted = page.sales.length === 0 || page.oldestTimestamp == null || page.oldestTimestamp <= cutoffSec;
}
rebuildValue(itemId);
} catch {
if (!pageIsActive()) {
if (phase === "initial") queue.enqueueInitial(itemId);
else queue.enqueueDeep(itemId);
} else {
failedItems.add(itemId);
}
} finally {
inFlight.delete(itemId);
if (phase === "initial") initialInFlight--;
}
annotateAll();
if (!failedItems.has(itemId)) {
if (!canDeepen(job)) {
job.completedAt = Date.now();
persistSalesDb();
} else if (firstPassComplete() && deepBudgetLeft()) {
queue.enqueueDeep(itemId);
}
}
}
} finally {
draining = false;
renderChromeIfDataChanged();
}
}
void ensureRestored();
function injectStyle() {
const prior = document.getElementById(STYLE_ID);
if (prior) prior.remove();
const light = theme === "light";
const bg = light ? "#ffffff" : "#0e0e13";
const base = light ? "#3a3849" : "#c6c6d0";
const border = light ? "rgba(109,63,206,.40)" : "rgba(178,150,255,.40)";
const good = light ? "#0f8a4d" : TIER_COLOR.good;
const fair = light ? "#8a6000" : TIER_COLOR.fair;
const over = light ? "#cc3b3b" : TIER_COLOR.over;
const est = light ? "#6f6d7d" : "#9797a0";
const s = document.createElement("style");
s.id = STYLE_ID;
s.textContent = `
.${BADGE_CLASS}{display:inline-flex!important;align-items:center;gap:4px;margin-left:6px;
height:16px;padding:0 6px;border:1px solid ${border};border-radius:4px;
font:600 10px/1 var(--cx-font,'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif)!important;
font-variant-numeric:tabular-nums;white-space:nowrap;letter-spacing:.01em;vertical-align:middle;color:${base};background:${bg};
box-sizing:border-box;position:relative;z-index:5;overflow:visible;pointer-events:none;}
.${BADGE_CLASS}.cx-good{color:${good};border-color:${good};}
.${BADGE_CLASS}.cx-fair{color:${fair};border-color:${fair};}
.${BADGE_CLASS}.cx-over{color:${over};border-color:${over};}
.${BADGE_CLASS}.cx-est{color:${est};}
.${BADGE_CLASS}.cx-loading{color:${est};border-style:dotted;}
.${BADGE_CLASS}.cx-clickable{pointer-events:auto;cursor:pointer;transition:filter .12s ease;}
.${BADGE_CLASS}.cx-clickable:hover{filter:brightness(1.15);}
.${BADGE_CLASS} .cx-ld{width:5px;height:5px;border-radius:50%;background:currentColor;
will-change:opacity,transform;animation:cx-ld-breathe 1.8s cubic-bezier(.45,0,.55,1) infinite;}
@keyframes cx-ld-breathe{0%{opacity:.3;transform:scale(.7)}50%{opacity:1;transform:scale(1.1)}100%{opacity:.3;transform:scale(.7)}}
@media (prefers-reduced-motion:reduce){.${BADGE_CLASS} .cx-ld{animation:none}}
`;
(document.head || document.documentElement).appendChild(s);
}
function applyTheme(next) {
theme = next;
storageSet(THEME_KEY, next);
host.classList.toggle("cx-light", theme === "light");
injectStyle();
renderChrome();
}
function catalogMatch(titleText) {
const hay = titleText.toLowerCase();
for (const e of catalog) {
if (e.lower.length >= 3 && hay.includes(e.lower)) return e;
}
return null;
}
function cardForBonus(cards, bonuses) {
const matches = [];
for (const b of bonuses) {
matches.push(...cards.filter((card) => normalizePerk(card.perkTitle) === normalizePerk(b.name)));
}
return matches.sort((a, b) => b.comps - a.comps)[0] ?? null;
}
const RARITY_RANK2 = { grey: 0, gray: 0, yellow: 1, orange: 2, red: 3 };
const rankOf = (r) => r && RARITY_RANK2[r.toLowerCase()] != null ? RARITY_RANK2[r.toLowerCase()] : -1;
function broadForRow(segmented, cards, bonuses, rarity) {
const wantDouble = bonuses.length >= 2;
const names = new Set(bonuses.map((b) => normalizePerk(b.name)));
const mine = segmented.filter((c) => names.has(normalizePerk(c.perkTitle)));
const byComps = (arr) => arr.slice().sort((a, b) => b.comps - a.comps)[0] ?? null;
if (rarity) {
const rk = rankOf(rarity);
if (!wantDouble) {
const same = byComps(mine.filter((c) => !c.doublePerk && rankOf(c.cohortRarity) === rk));
if (same) return { value: same, matchedRarity: same.cohortRarity, crossedRarity: false, floor: false };
} else {
const sameSingles = mine.filter((c) => !c.doublePerk && rankOf(c.cohortRarity) === rk);
const bestSingle = sameSingles.slice().sort((a, b) => b.medianPrice - a.medianPrice || b.comps - a.comps)[0];
if (bestSingle) return { value: bestSingle, matchedRarity: bestSingle.cohortRarity, crossedRarity: false, floor: true };
}
const lower = mine.filter((c) => !c.doublePerk && rankOf(c.cohortRarity) >= 0 && rankOf(c.cohortRarity) < rk).sort((a, b) => rankOf(b.cohortRarity) - rankOf(a.cohortRarity) || b.comps - a.comps);
if (lower[0]) return { value: lower[0], matchedRarity: lower[0].cohortRarity, crossedRarity: true, floor: true };
return null;
}
const any = byComps(mine.filter((c) => !c.doublePerk));
if (any) return { value: any, matchedRarity: any.cohortRarity, crossedRarity: true, floor: wantDouble };
const card = cardForBonus(cards, bonuses);
return card ? { value: card, matchedRarity: card.rarity, crossedRarity: true, floor: wantDouble } : null;
}
function rollBandValue(sales, itemId, bonuses, rarity) {
if (!rarity || bonuses.length === 0 || bonuses.some((b) => b.percent == null)) return null;
const perks = bonuses.map((b) => ({ id: bonusIdByTitle(b.name), value: b.percent })).filter((p) => p.id != null);
if (perks.length !== bonuses.length) return null;
const value = valueForRollBand(sales, { itemId, rarity, perks, wantDouble: bonuses.length >= 2 });
if (!value) return null;
return { value, tol: Math.max(...perks.map((p) => effectivePerkTolerance(p.id, rarity))) };
}
function segmentedForRow(segmented, bonuses, rarity) {
if (!rarity || bonuses.length !== 1 || bonuses[0].percent == null) return null;
const rk = rankOf(rarity);
const name = normalizePerk(bonuses[0].name);
return segmented.filter((c) => !c.doublePerk && rankOf(c.cohortRarity) === rk && normalizePerk(c.perkTitle) === name).sort((a, b) => b.comps - a.comps)[0] ?? null;
}
function signatureForRow(sigs, bonuses, rarity) {
if (!rarity || bonuses.length === 0 || bonuses.some((b) => b.percent == null)) return null;
const want = [...bonuses].sort((a, b) => a.name.localeCompare(b.name));
let best = null;
for (const s of sigs) {
if (s.perkTitles.length !== want.length) continue;
if (rarity && s.rarity?.toLowerCase() !== rarity.toLowerCase()) continue;
const have = s.perkTitles.map((title, i) => ({ name: title.toLowerCase(), percent: s.perkValues[i] })).sort((a, b) => a.name.localeCompare(b.name));
const same = have.every(
(x, i) => normalizePerk(x.name) === normalizePerk(want[i].name) && want[i].percent != null && Math.abs(x.percent - want[i].percent) < 1e-3
);
if (same && (!best || s.comps > best.comps)) best = s;
}
return best;
}
const SEASON_WINDOW_DAYS = 45;
function seasonalReason(itemId, bonuses) {
if (!season || season.daysUntil > SEASON_WINDOW_DAYS) return null;
if (bonuses.some((b) => bonusIdByTitle(b.name) === REVITALIZE_BONUS_ID)) return "revitalize";
if ((catalogById.get(itemId)?.category ?? "").toLowerCase() === "melee") return "melee";
return null;
}
function aavSpreadHalf(priceSpread) {
const s = Number.isFinite(priceSpread) ? priceSpread : 0;
return Math.min(0.4, Math.max(0.06, s)) / 2;
}
function resolveRowView(row, weapon, cached, bonuses, rarity, quality, price) {
const exactFieldsAvailable = !!rarity && bonuses.length > 0 && bonuses.every((bonus) => bonus.percent != null);
const exact = signatureForRow(cached.sigs, bonuses, rarity);
const seg = exact ? null : segmentedForRow(cached.broad, bonuses, rarity);
const surface = seg ? fairValueOnSurfaceAt(seg.surface, bonuses[0]?.percent ?? null, quality) : null;
const useSurface = surface != null && seg != null;
const band = exact || useSurface ? null : rollBandValue(cached.sales, weapon.id, bonuses, rarity);
const broad = exact || useSurface || band ? null : broadForRow(cached.broad, cached.cards, bonuses, rarity);
const source = exact ?? (useSurface ? seg : null) ?? band?.value ?? broad?.value ?? null;
if (!source) return null;
let fair;
let typicalQuality = false;
let pricedQuality = null;
if (useSurface) {
fair = { value: surface.value, basis: "perk-quality" };
typicalQuality = surface.usedTypicalQuality;
pricedQuality = typicalQuality ? seg.surface.qualityMedian : quality;
} else {
const fallbackQuality = quality ?? source.qualityMedian;
fair = fairValueAt(source, fallbackQuality);
typicalQuality = quality == null && fallbackQuality != null && fair.basis !== "median";
pricedQuality = fair.basis === "median" ? null : fallbackQuality;
}
const rowStats = parseRowStats(row);
const sane = (v, base) => {
if (v == null) return null;
if (base == null || base <= 0) return v;
return v >= base * 0.4 && v <= base * 2 ? v : null;
};
const rowDamage = weapon.kind === "weapon" ? sane(rowStats.damage, weapon.baseDamage) : null;
const rowAccuracy = weapon.kind === "weapon" ? sane(rowStats.accuracy, weapon.baseAccuracy) : null;
const rowArmor = weapon.kind === "armor" ? parseAuctionProperty(row, /^Armor:?$/i) : null;
const exactRoll = exact != null;
let estimateBound = null;
if (!exactRoll && !useSurface && bonuses.length === 1 && bonuses[0]?.percent != null) {
const rollPct = bonuses[0].percent;
const observedRolls = source.recentSales.map((s) => s.perks[0]).filter((v) => Number.isFinite(v));
if (observedRolls.length >= 3) {
const lo = Math.min(...observedRolls);
const hi = Math.max(...observedRolls);
if (rollPct < lo && source.lowRobust > 0) {
estimateBound = "ceiling";
fair = { value: source.lowRobust, basis: fair.basis };
} else if (rollPct > hi && source.highRobust > 0) {
estimateBound = "floor";
fair = { value: source.highRobust, basis: fair.basis };
}
}
}
const matchTier = resolveMatchTier(exactRoll, useSurface || band != null, broad?.floor === true);
let rollKey;
if (exact) rollKey = perkRollSignatureKey(exact.bonusIds.map((id, i) => ({ id, value: exact.perkValues[i] })));
else if (useSurface) rollKey = `surface:${seg.itemId}:${seg.bonusId}:${seg.cohortRarity ?? "any"}`;
else if (band) rollKey = `band:${rarity}:${bonuses.map((b) => `${normalizePerk(b.name)}${b.percent}`).sort().join("+")}`;
else rollKey = `broad:${broad.value.bonusId}:${broad.matchedRarity ?? "any"}:${broad.floor ? "floor" : "est"}`;
return {
row,
itemId: weapon.id,
weaponName: weapon.name,
rarity,
bonuses,
fair: fair.value,
fairBasis: fair.basis,
evidence: exactRoll ? "verified" : "indicative",
matchTier,
perkTol: band ? band.tol : null,
estimateReason: exactRoll ? null : exactFieldsAvailable ? "sparse" : "missing-details",
crossedRarity: broad?.crossedRarity ?? false,
broadFloor: broad?.floor ?? false,
matchedRarity: exactRoll || useSurface || band ? rarity : broad?.matchedRarity ?? null,
doublePerk: bonuses.length >= 2,
quality,
typicalQuality,
pricedQuality,
perkBound: useSurface ? surface.perkBound : estimateBound,
kind: weapon.kind,
rowDamage,
rowAccuracy,
rowArmor,
price,
projection: suppressFlip(matchTier) ? null : projectAuctionReturn(fair.value, price, 0),
trend: source.trend,
comps: source.comps,
windowDays: source.windowDays,
perDay: source.perDay,
recentSales: source.recentSales,
bands: source.bands,
model: source.model,
qualityMin: source.qualityMin,
qualityMax: source.qualityMax,
lowPrice: source.lowPrice,
highPrice: source.highPrice,
aavLow: Math.round(fair.value * (1 - aavSpreadHalf(source.priceSpread))),
aavHigh: Math.round(fair.value * (1 + aavSpreadHalf(source.priceSpread))),
seasonal: seasonalReason(weapon.id, bonuses),
concentration: concentrationFlag(source.concentration),
dealKey: `${weapon.id}:${rarity ?? "?"}:${bonuses.map((b) => Math.round(b.percent ?? 0)).sort((a, z) => a - z).join("+")}`,
sigKey: `${exactRoll ? "exact" : "estimate"}:${rarity ?? "unknown"}:${rollKey}`,
endsInSeconds: parseAuctionTimer(row),
confidence: deriveConfidence(source.comps, source.recentSales, Math.floor(Date.now() / 1e3)),
isNewRow: initialPassDone && !knownRows.has(row)
};
}
function roiTier(view) {
const vs = bidVsAav(view);
if (!vs) return "fair";
if (vs.below) return vs.pct >= 10 ? "good" : "fair";
return "over";
}
function buildLoadingBadge() {
const el = document.createElement("span");
el.setAttribute(CX_OWNED_ATTR, "auction-loading");
el.className = `${BADGE_CLASS} cx-loading`;
el.append(text("span", "cx-ld", ""), document.createTextNode("Cortex"));
el.title = "Cortex is loading this price from completed sales.";
return el;
}
function buildBadge(view) {
const badge = document.createElement("span");
badge.setAttribute(CX_OWNED_ATTR, "auction-value");
badge.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
pendingFocusRow = view.row;
panelOpen = true;
panelView = "main";
renderChrome();
});
const tier = roiTier(view);
const loose = view.matchTier === "rough" || view.matchTier === "rare" || view.perkBound != null;
badge.className = `${BADGE_CLASS} cx-clickable cx-${tier}${loose ? " cx-est" : ""}`;
const worth = aavText(view);
const vs = bidVsAav(view);
badge.textContent = `${worth}${vs ? ` \xB7 ${vs.pct}% ${vs.below ? "under" : "over"}` : ""}`;
if (view.matchTier === "rare") {
badge.title = `Few sales at this rarity/roll. More common versions around ${money(view.fair)}.`;
return badge;
}
if (view.perkBound === "floor") {
badge.title = `This roll is above every recent sale, so it's worth at least ${money(view.fair)}.`;
return badge;
}
if (view.perkBound === "ceiling") {
badge.title = `This roll is below every recent sale, so it's worth about ${money(view.fair)} at most.`;
return badge;
}
const how = view.matchTier === "exact" ? `${view.comps} sales of this exact weapon, rarity and roll` : view.matchTier === "close" ? view.perkTol != null ? `${view.comps} sales near this roll` : `${view.comps} similar sales` : `${view.comps} ${view.matchedRarity ?? "comparable"} sales`;
const vsNote = vs ? ` Bid ${vs.pct}% ${vs.below ? "below" : "above"}.` : "";
badge.title = `${AAV_WORD} ${money(view.fair)} \xB7 ${how}.${vsNote}`;
return badge;
}
let valued = 0;
let candidates = 0;
let activeKind = null;
const rowViews = /* @__PURE__ */ new Map();
const knownRows = /* @__PURE__ */ new WeakSet();
let initialPassDone = false;
const sessionDeals = /* @__PURE__ */ new Map();
const SESSION_DEAL_TTL_MS = 30 * 60 * 1e3;
let scanStartedAt = null;
let progressTimer = null;
function progressPct() {
return candidates > 0 ? Math.min(100, Math.round(valued / candidates * 100)) : 0;
}
function progressMeta() {
const secs = scanStartedAt ? Math.max(0, (Date.now() - scanStartedAt) / 1e3) : 0;
const elapsed = `${secs.toFixed(1)}s`;
return catalogReady && candidates > 0 ? `${valued} of ${candidates} priced \xB7 ${elapsed}` : elapsed;
}
function renderProgress() {
const wrap = document.createElement("div");
wrap.className = "cx-progress";
const determinate = catalogReady && candidates > 0;
const head = document.createElement("div");
head.className = "cx-progress-head";
head.append(
text("span", "cx-progress-label", determinate ? "Pricing auctions" : "Loading recent sales"),
text("span", "cx-progress-meta", progressMeta())
);
const track = document.createElement("div");
track.className = `cx-progress-track${determinate ? "" : " indet"}`;
const fill = document.createElement("div");
fill.className = "cx-progress-fill";
if (determinate) fill.style.width = `${progressPct()}%`;
track.appendChild(fill);
wrap.append(head, track);
return wrap;
}
function progressTick() {
if (scanState() !== "checking" || !panelOpen) {
stopProgressTimer();
return;
}
const bar = shadow.querySelector(".cx-progress");
if (!bar) return;
const meta = bar.querySelector(".cx-progress-meta");
if (meta) meta.textContent = progressMeta();
const fill = bar.querySelector(".cx-progress-fill");
if (fill && catalogReady && candidates > 0) fill.style.width = `${progressPct()}%`;
}
function stopProgressTimer() {
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
}
function syncScanClock() {
const checking = scanState() === "checking";
if (checking && scanStartedAt == null) scanStartedAt = Date.now();
else if (!checking) scanStartedAt = null;
}
function ensureProgressTimer() {
if (panelOpen && scanState() === "checking") {
if (!progressTimer) progressTimer = setInterval(progressTick, 200);
} else {
stopProgressTimer();
}
}
function annotateRow(row, visibleItemIds) {
if (!rowIsVisible(row)) return;
const titleEl = row.querySelector(TITLE_SEL);
if (!titleEl) return;
const bonuses = parseBonuses(row);
if (bonuses.length === 0) return;
const weapon = catalogMatch(clean(titleEl.textContent));
if (!weapon) return;
activeKind = weapon.kind;
visibleItemIds.add(weapon.id);
const rarity = rarityOf(row);
const requirements = requirementsByItem.get(weapon.id) ?? [];
requirements.push({ bonuses, rarity });
requirementsByItem.set(weapon.id, requirements);
candidates++;
enqueue(weapon.id);
const cached = valueCache.get(weapon.id);
const quality = parseQuality(row);
const price = parsePrice(row);
const view = cached ? resolveRowView(row, weapon, cached, bonuses, rarity, quality, price) : null;
if (!view) {
const sig2 = cached ? `${weapon.id}:none` : `${weapon.id}:loading`;
if (row.getAttribute(BADGE_ATTR) !== sig2) {
row.setAttribute(BADGE_ATTR, sig2);
row.querySelectorAll(`.${BADGE_CLASS}`).forEach((node) => node.remove());
if (!cached) {
const bidCount = findNativeBidCountAnchor(row, PRICE_SEL);
if (bidCount) bidCount.appendChild(buildLoadingBadge());
}
}
return;
}
rowViews.set(row, view);
knownRows.add(row);
if (view.projection && view.projection.roi > 0 && view.matchTier !== "rare") {
const below = view.price != null && view.fair > 0 ? (view.fair - view.price) / view.fair : 0;
sessionDeals.set(view.dealKey, { key: view.dealKey, weapon: view.weaponName, perk: bonusLabel(view.bonuses), kind: view.kind, below, worth: view.fair, seenAt: Date.now() });
}
const sig = `${weapon.id}:${view.sigKey}:${quality ?? ""}:${price ?? ""}:${Math.round(view.fair)}`;
if (row.getAttribute(BADGE_ATTR) !== sig || !row.querySelector(`.${BADGE_CLASS}`)) {
row.setAttribute(BADGE_ATTR, sig);
row.querySelectorAll(`.${BADGE_CLASS}`).forEach((node) => node.remove());
const bidCount = findNativeBidCountAnchor(row, PRICE_SEL);
if (bidCount) {
bidCount.appendChild(buildBadge(view));
}
}
valued++;
}
function annotateAll() {
if (stopped || !catalogReady || !pageIsActive()) return;
if (!enforceSingleRoot()) return;
valued = 0;
candidates = 0;
rowViews.clear();
requirementsByItem.clear();
const visibleItemIds = /* @__PURE__ */ new Set();
const rows = document.querySelectorAll(AUCTION_ROW_SEL);
rows.forEach((row) => {
try {
annotateRow(row, visibleItemIds);
} catch {
}
});
for (const itemId of failedItems) {
if (!visibleItemIds.has(itemId)) failedItems.delete(itemId);
}
queue.removeNotIn(visibleItemIds);
const viewKey = [...visibleItemIds].sort((a, b) => a - b).join(",");
if (viewKey !== deepViewKey) {
deepViewKey = viewKey;
deepPullsThisView = 0;
}
scheduleDeepWork(visibleItemIds);
if (!initialPassDone && firstPassComplete()) initialPassDone = true;
renderChromeIfDataChanged();
}
let theme = readTheme();
const host = document.createElement("div");
host.id = ROOT_ID;
host.dataset.cxVersion = UI_VERSION;
host.classList.toggle("cx-light", theme === "light");
host.classList.toggle("cx-desktop", isDesktopView());
const shadow = host.attachShadow({ mode: "open" });
const cstyle = document.createElement("style");
cstyle.textContent = `
:host{
all:initial;color-scheme:dark;color:var(--cx-text);
--cx-bg:#0e0e13;--cx-head:#131218;--cx-surface:#151419;--cx-surface-2:#101015;
--cx-tile:#121118;--cx-hero-2:#181620;--cx-input:#0b0b0f;
--cx-border:#24242c;--cx-border-2:#30303a;--cx-border-soft:rgba(120,112,150,.20);
--cx-text:#ececf1;--cx-text-2:#c6c6d0;--cx-text-3:#9797a0;--cx-text-muted:#7b7b87;
--cx-accent:#b296ff;--cx-accent-2:#c6b2ff;--cx-accent-bg:#1b1730;--cx-accent-bg-2:#231d3d;--cx-accent-border:#3a3060;--cx-on-accent:#17141f;
--cx-good:#56d575;--cx-bad:#fa6863;--cx-warn:#f0bb3b;
--cx-chip-good:#7fe0a0;--cx-chip-info:#c0acff;--cx-chip-warn:#f0c96a;--cx-chip-muted:#a2a2ac;
--cx-rarity-yellow:#edcc48;--cx-rarity-orange:#fd923e;--cx-rarity-red:#ff6561;--cx-season:#fd923e;
--cx-shadow:0 20px 52px rgba(0,0,0,.55);--cx-tab-shadow:-6px 0 22px rgba(0,0,0,.45);
--cx-scroll:#33333f;
--cx-font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;
--cx-mono:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
font-variant-numeric:tabular-nums;
-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
}
:host(.cx-light){
color-scheme:light;
--cx-bg:#f4f4f7;--cx-head:#ecebf1;--cx-surface:#ffffff;--cx-surface-2:#f6f5fa;
--cx-tile:#f7f6fb;--cx-hero-2:#f2eefb;--cx-input:#ffffff;
--cx-border:#e3e2ea;--cx-border-2:#d2d0dd;--cx-border-soft:rgba(120,112,150,.22);
--cx-text:#171622;--cx-text-2:#3a3849;--cx-text-3:#5b5968;--cx-text-muted:#6f6d7d;
--cx-accent:#6d3fce;--cx-accent-2:#5a2fbf;--cx-accent-bg:#efeafb;--cx-accent-bg-2:#e5dcf8;--cx-accent-border:#d3c6f2;--cx-on-accent:#ffffff;
--cx-good:#0f8a4d;--cx-bad:#cc3b3b;--cx-warn:#8a6000;
--cx-chip-good:#0f8a4d;--cx-chip-info:#6d3fce;--cx-chip-warn:#8a6000;--cx-chip-muted:#5b5968;
--cx-rarity-yellow:#9a7a0a;--cx-rarity-orange:#b7530a;--cx-rarity-red:#c23a34;--cx-season:#b7530a;
--cx-shadow:0 16px 44px rgba(30,25,50,.16);--cx-tab-shadow:-6px 0 20px rgba(30,25,50,.14);
--cx-scroll:#cfccdb;
}
/* Desktop Torn renders this panel far wider than a phone, so its text reads tiny.
Scale the whole panel + tab up (PDA/mobile keeps its native size). */
:host(.cx-desktop) .panel{zoom:1.2;}
:host(.cx-desktop) .tab{zoom:1.15;}
:host(.cx-desktop) .cx-onboard{zoom:1.15;}
*{box-sizing:border-box;}
button,input{font:inherit;}
button{appearance:none;border:0;margin:0;}
.status-dot{width:8px;height:8px;border-radius:50%;flex:none;background:var(--cx-warn);box-shadow:0 0 6px color-mix(in srgb,var(--cx-warn) 55%,transparent);}
.status-dot.checking{animation:cx-status-pulse 1.9s cubic-bezier(.4,0,.6,1) infinite;}
.status-dot.ready{background:var(--cx-good);box-shadow:0 0 6px color-mix(in srgb,var(--cx-good) 60%,transparent);}
@keyframes cx-status-pulse{0%,100%{opacity:.4;transform:scale(.82)}50%{opacity:1;transform:scale(1)}}
/* Expanded loading bar: real fill + count + elapsed while prices come in. */
.cx-progress{margin-bottom:11px;padding:11px 12px;border:1px solid var(--cx-border);border-radius:6px;background:var(--cx-surface);}
.cx-progress-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:8px;}
.cx-progress-label{color:var(--cx-text-2);font-size:11px;font-weight:600;}
.cx-progress-meta{color:var(--cx-text-3);font:500 10px/1 var(--cx-mono);white-space:nowrap;}
.cx-progress-track{position:relative;height:5px;border-radius:2px;background:var(--cx-tile);overflow:hidden;}
.cx-progress-fill{height:100%;width:0;border-radius:2px;background:var(--cx-accent);transition:width .3s ease;}
.cx-progress-track.indet .cx-progress-fill{width:38%;animation:cx-indet 1.25s ease-in-out infinite;}
@keyframes cx-indet{0%{transform:translateX(-110%)}100%{transform:translateX(320%)}}
/* Closed state: one slim right-edge control on the Auction House. */
.tab{position:fixed;right:0;top:50%;transform:translateY(-50%);z-index:2147482000;
display:flex;flex-direction:column;align-items:center;gap:7px;cursor:pointer;
background:var(--cx-head);color:var(--cx-text-2);border:1px solid var(--cx-border-2);border-right:none;
border-radius:6px 0 0 6px;padding:11px 5px;box-shadow:var(--cx-tab-shadow);
font:600 12px/1 var(--cx-font);transition:background .14s,color .14s;}
.tab:hover,.tab:focus-visible{color:var(--cx-text);border-color:var(--cx-accent-border);outline:none;background:var(--cx-accent-bg);}
.tab .cx-arrow{color:var(--cx-accent-2);font-size:15px;line-height:1;}
.tab .cx-name{writing-mode:vertical-rl;text-orientation:upright;letter-spacing:.16em;}
.tab .cx-count{writing-mode:vertical-rl;color:var(--cx-text-3);font-weight:500;font-size:9px;}
.tab .status-dot{margin-bottom:1px;}
.panel{position:fixed;right:8px;bottom:8px;z-index:2147481999;width:min(432px,calc(100vw - 16px));max-height:min(84vh,780px);
display:flex;flex-direction:column;background:var(--cx-bg);color:var(--cx-text);border:1px solid var(--cx-border-2);border-radius:8px;
box-shadow:var(--cx-shadow);font:400 13px/1.5 var(--cx-font);overflow:hidden;}
.panel-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:13px 14px 12px;border-bottom:1px solid var(--cx-border);background:var(--cx-head);}
.panel-title{font-size:14px;font-weight:700;color:var(--cx-text);letter-spacing:-.01em;}
.panel-sub{margin-top:2px;color:var(--cx-text-3);font-size:10px;font-weight:500;}
.head-actions{display:flex;align-items:center;gap:7px;}
.status{display:flex;align-items:center;gap:6px;color:var(--cx-text-3);font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;}
.icon-btn{width:30px;height:30px;border-radius:6px;background:var(--cx-surface);color:var(--cx-text-3);cursor:pointer;font-size:17px;line-height:1;border:1px solid var(--cx-border);transition:background .12s,color .12s,border-color .12s;}
.icon-btn:hover,.icon-btn:focus-visible{color:var(--cx-text);background:var(--cx-accent-bg);border-color:var(--cx-accent-border);outline:none;}
.icon-btn.active{color:var(--cx-accent);background:var(--cx-accent-bg);border-color:var(--cx-accent-border);}
.panel-body{overflow:auto;padding:12px;scrollbar-width:thin;scrollbar-color:var(--cx-scroll) transparent;}
.kpis{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:11px;}
.kpi{padding:9px 10px;border:1px solid var(--cx-border);border-radius:6px;background:var(--cx-surface);min-width:0;}
.kpi b{display:block;color:var(--cx-text);font:700 16px/1.1 var(--cx-mono);}
.kpi span{display:block;margin-top:4px;color:var(--cx-text-3);font-size:8px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.deals{margin-bottom:11px;padding:11px 12px;border:1px solid var(--cx-border);border-radius:6px;background:var(--cx-surface);}
.deals-header{display:flex;align-items:center;gap:6px;width:100%;padding:0;margin:0;background:none;color:var(--cx-text-3);font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;cursor:pointer;border:none;}
.deals-header:hover{color:var(--cx-text-2);}
.deals-arrow{display:inline-block;transition:transform .15s ease;font-size:11px;line-height:1;color:var(--cx-accent);}
.deals-arrow.open{transform:rotate(90deg);}
.deals-content{margin-top:7px;}
.deal{display:grid;grid-template-columns:1fr auto;align-items:center;gap:10px;padding:6px 0;border-top:1px solid var(--cx-border-soft);}
.deal:first-of-type{border-top:none;padding-top:1px;}
.deal-main{min-width:0;}
.deal-w{display:block;color:var(--cx-text-2);font-size:11.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.deal-perk{display:block;margin-top:1px;color:var(--cx-text-3);font-size:9px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.deal-right{text-align:right;white-space:nowrap;}
.deal-roi{color:var(--cx-good);font:700 13px/1.1 var(--cx-mono);}
.deal-v{display:block;margin-top:1px;color:var(--cx-text-muted);font:500 9px/1 var(--cx-mono);}
.notice{padding:12px;margin-bottom:10px;border:1px solid var(--cx-border);border-radius:6px;background:var(--cx-surface);color:var(--cx-text-2);}
.notice .primary{display:block;margin-top:9px;}
.notice strong{display:block;color:var(--cx-text);margin-bottom:3px;}
.season-banner{margin-bottom:11px;padding:9px 11px;border-radius:6px;border:1px solid var(--cx-border);background:var(--cx-surface);color:var(--cx-text-2);font-size:10px;font-weight:500;line-height:1.45;}
.season-banner b{color:var(--cx-text);font-weight:700;}
.cards{display:grid;gap:10px;}
.card{position:relative;border:1px solid var(--cx-border);border-radius:6px;background:var(--cx-surface);padding:12px 13px 13px;overflow:hidden;}
.card.indicative{background:var(--cx-surface-2);}
.c-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;}
.c-id{min-width:0;}
.c-weapon{font-size:13px;font-weight:700;color:var(--cx-text);line-height:1.2;}
.c-perk{margin-top:2px;color:var(--cx-accent);font-size:11px;font-weight:600;}
.c-stats{margin-top:3px;color:var(--cx-text-muted);font:600 9px/1.3 var(--cx-mono);letter-spacing:.02em;}
.c-chips{display:flex;flex-wrap:wrap;gap:4px;justify-content:flex-end;max-width:58%;}
.chip{padding:2px 7px;border-radius:4px;border:1px solid var(--cx-border);background:transparent;color:var(--cx-text-3);font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;white-space:nowrap;}
.chip.verified{color:var(--cx-chip-good);border-color:color-mix(in srgb,var(--cx-good) 40%,transparent);}
.chip.indicative{color:var(--cx-chip-warn);border-color:color-mix(in srgb,var(--cx-warn) 40%,transparent);}
.chip.close{color:var(--cx-chip-info);border-color:color-mix(in srgb,var(--cx-accent) 40%,transparent);}
.chip.season{color:var(--cx-season);border-color:color-mix(in srgb,var(--cx-season) 40%,transparent);}
.chip.rarity.yellow{color:var(--cx-rarity-yellow);border-color:color-mix(in srgb,var(--cx-rarity-yellow) 40%,transparent);}
.chip.rarity.orange{color:var(--cx-rarity-orange);border-color:color-mix(in srgb,var(--cx-rarity-orange) 40%,transparent);}
.chip.rarity.red{color:var(--cx-rarity-red);border-color:color-mix(in srgb,var(--cx-rarity-red) 40%,transparent);}
.chip.none{color:var(--cx-chip-muted);border-color:var(--cx-border);}
.c-tiles{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:11px;}
.tile{position:relative;padding:9px 11px;border-radius:6px;background:var(--cx-tile);border:1px solid var(--cx-border);min-width:0;}
.tile.hero{grid-column:1 / -1;padding:11px 13px;background:var(--cx-surface-2);border-color:var(--cx-border-2);}
.t-label{display:block;color:var(--cx-text-muted);font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.tile.hero .t-label{font-size:9px;color:var(--cx-text-3);letter-spacing:.09em;}
.t-val{display:block;margin-top:3px;color:var(--cx-text);font:700 17px/1.05 var(--cx-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.tile.hero .t-val{margin-top:5px;font-size:27px;font-weight:800;letter-spacing:-.02em;}
.t-val.positive{color:var(--cx-good);}.t-val.negative{color:var(--cx-bad);}.t-val.neutral{color:var(--cx-warn);}
.t-sub{display:block;margin-top:3px;color:var(--cx-text-muted);font-size:9px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.tile.hero .t-sub{font-size:10px;}
.c-trend{margin:10px 0 0;color:var(--cx-text-3);font-size:10px;font-weight:600;}
.c-trend.up,.c-trend.down{color:var(--cx-text-2);}
.c-meta{margin:10px 0 0;color:var(--cx-text-2);font-size:10px;font-weight:600;line-height:1.4;}
.c-note{margin:10px 0 0;padding:9px 11px;border-radius:6px;background:var(--cx-surface-2);border:1px solid var(--cx-border);color:var(--cx-text-2);font-size:10.5px;line-height:1.5;font-weight:400;}
.c-flag{margin:8px 0 0;padding:8px 10px;border-radius:6px;background:var(--cx-surface-2);border:1px solid var(--cx-border);color:var(--cx-text-2);font-size:10px;line-height:1.45;font-weight:500;}
.sales{margin-top:11px;padding-top:10px;border-top:1px solid var(--cx-border);}
.sales-title{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px;color:var(--cx-text-2);font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;}
.sales-title span{color:var(--cx-text-muted);font-size:9px;font-weight:500;text-transform:none;letter-spacing:0;}
.sale{display:grid;grid-template-columns:1fr 3.5rem 2.1rem 2.3rem 2.3rem 2.5rem;align-items:center;gap:6px;padding:4px 0;border-top:1px solid var(--cx-border-soft);color:var(--cx-text-3);font-size:10px;}
.sale.k-armor{grid-template-columns:1fr 3.9rem 2.4rem 3.2rem 3.2rem;}
.sale:first-of-type{border-top:none;}
.sale b{color:var(--cx-text);font:700 11px/1 var(--cx-mono);text-align:right;}
.sale em{color:var(--cx-text-3);font-style:normal;font:500 9px/1 var(--cx-mono);text-align:right;}
.sale .s-perk{color:var(--cx-text-2);}
.sale-h{border-top:none;padding:0 0 3px;color:var(--cx-text-muted);font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;}
.sale-h span{text-align:right;}.sale-h span:first-child{text-align:left;}
.jump{margin-top:10px;width:100%;padding:8px;border-radius:6px;background:var(--cx-accent-bg);color:var(--cx-accent);border:1px solid var(--cx-accent-border);cursor:pointer;font-weight:700;font-size:10px;letter-spacing:.04em;text-transform:uppercase;transition:background .12s,color .12s;}
.jump:hover,.jump:focus-visible{background:var(--cx-accent-bg-2);color:var(--cx-text);outline:none;}
.key{padding:12px;border:1px solid var(--cx-border);border-radius:6px;background:var(--cx-surface);color:var(--cx-text-2);}
.key label{display:block;color:var(--cx-text);font-weight:700;margin-bottom:3px;}.key p{margin:0;color:var(--cx-text-3);font-size:10px;line-height:1.5;}
.key input{width:100%;margin:9px 0 7px;padding:9px;background:var(--cx-input);border:1px solid var(--cx-border);border-radius:6px;color:var(--cx-text);outline:none;}
.key input:focus{border-color:var(--cx-accent);}
.primary{background:var(--cx-accent);color:var(--cx-on-accent);border-radius:6px;padding:8px 13px;font-weight:700;font-size:10px;cursor:pointer;}
.primary:hover,.primary:focus-visible{filter:brightness(1.08);outline:none;}
.error{margin-top:6px;color:var(--cx-bad);font-size:10px;}
.key .get-key{margin:10px 0 4px;display:inline-block;}
.key-trust{display:flex;flex-wrap:wrap;gap:12px;margin-top:11px;padding-top:10px;border-top:1px solid var(--cx-border-soft);}
.key-trust a,.key-trust .linkish{color:var(--cx-accent);font-size:10px;font-weight:600;text-decoration:none;background:none;border:0;padding:0;cursor:pointer;}
.key-trust a:hover,.key-trust .linkish:hover{text-decoration:underline;}
.key-pop{margin:9px 0 0;color:var(--cx-text-3);font-size:10px;line-height:1.55;}
/* First-run flag: a small card just left of the closed tab, pointing at it. */
.cx-onboard{position:fixed;right:46px;top:50%;transform:translateY(-50%);z-index:2147481998;
width:15rem;background:var(--cx-surface);color:var(--cx-text-2);border:1px solid var(--cx-accent-border);
border-radius:8px;padding:12px 13px;box-shadow:var(--cx-shadow);
font:400 12px/1.45 var(--cx-font);animation:cx-onboard-in .28s ease both;}
.cx-onboard::after{content:'';position:absolute;right:-7px;top:50%;transform:translateY(-50%);
border:7px solid transparent;border-left-color:var(--cx-accent-border);border-right:0;}
.cx-onboard .ob-title{color:var(--cx-text);font-weight:700;font-size:12px;margin-bottom:4px;}
.cx-onboard .ob-copy{color:var(--cx-text-3);font-size:10.5px;line-height:1.5;margin:0 0 10px;}
.cx-onboard .ob-actions{display:flex;gap:7px;}
.cx-onboard .ghost,.cx-onboard .primary{padding:7px 11px;border-radius:6px;font-size:10px;font-weight:700;cursor:pointer;}
@keyframes cx-onboard-in{from{opacity:0;transform:translateY(-50%) translateX(8px);}to{opacity:1;transform:translateY(-50%) translateX(0);}}
/* --- Settings view --- */
.settings{display:flex;flex-direction:column;gap:11px;}
.set-card{padding:12px;border:1px solid var(--cx-border);border-radius:6px;background:var(--cx-surface);display:flex;flex-direction:column;gap:9px;}
.set-h{color:var(--cx-text);font-size:11px;font-weight:700;letter-spacing:.02em;text-transform:uppercase;}
.set-copy{margin:0;color:var(--cx-text-3);font-size:10px;line-height:1.5;}
.set-row{display:flex;align-items:center;justify-content:space-between;gap:10px;}
.set-key{padding:12px;border:1px solid var(--cx-border);border-radius:6px;background:var(--cx-surface);display:flex;flex-direction:column;gap:8px;}
.key-row{display:flex;align-items:center;gap:8px;}
.key-ok{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background:color-mix(in srgb,var(--cx-good) 20%,transparent);color:var(--cx-good);font-size:11px;font-weight:700;flex:none;}
.key-mask{font:600 13px/1 var(--cx-mono);color:var(--cx-text-2);letter-spacing:.1em;background:var(--cx-input);border:1px solid var(--cx-border);border-radius:4px;padding:7px 10px;}
.key-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:2px;}
.set-key input{width:100%;padding:9px;background:var(--cx-input);border:1px solid var(--cx-border);border-radius:6px;color:var(--cx-text);outline:none;}
.set-key input:focus{border-color:var(--cx-accent);}
.ghost{background:transparent;color:var(--cx-text-2);border:1px solid var(--cx-border);border-radius:6px;padding:7px 13px;font-weight:600;font-size:10px;cursor:pointer;transition:background .12s,color .12s;}
.ghost:hover,.ghost:focus-visible{background:var(--cx-accent-bg);color:var(--cx-text);border-color:var(--cx-accent-border);outline:none;}
.ghost.danger{color:var(--cx-bad);border-color:color-mix(in srgb,var(--cx-bad) 40%,var(--cx-border));}
.ghost.danger:hover,.ghost.danger:focus-visible{background:color-mix(in srgb,var(--cx-bad) 14%,transparent);color:var(--cx-bad);}
/* segmented control (theme toggle) */
.seg{display:inline-flex;border:1px solid var(--cx-border);border-radius:6px;background:var(--cx-input);overflow:hidden;}
.seg button{padding:6px 13px;font-weight:700;font-size:10px;color:var(--cx-text-3);background:transparent;cursor:pointer;}
.seg button.on{background:var(--cx-accent-bg);color:var(--cx-accent);}
.legend{display:flex;flex-direction:column;gap:0;padding:12px;border:1px solid var(--cx-border);border-radius:6px;background:var(--cx-surface);}
.legend .set-h{margin-bottom:8px;}
.legend-row{display:grid;grid-template-columns:8.5rem 1fr;gap:8px 12px;padding:7px 0;border-top:1px solid var(--cx-border-soft);}
.legend-row:first-of-type{border-top:none;padding-top:0;}
.legend-term{color:var(--cx-text-2);font-size:10px;font-weight:700;line-height:1.35;}
.legend-desc{color:var(--cx-text-3);font-size:10px;line-height:1.45;font-weight:400;}
.set-foot{margin:0;color:var(--cx-text-muted);font-size:9px;line-height:1.5;}
@media (max-width:520px){.panel{right:6px;bottom:6px;width:calc(100vw - 12px);max-height:80vh}.panel-body{padding:9px}.panel-head{padding:12px}.status{font-size:8px}.legend-row{grid-template-columns:1fr}.legend-desc{margin-top:1px}}
@media (prefers-reduced-motion:reduce){.status-dot.checking,.cx-progress-track.indet .cx-progress-fill{animation:none}}
`;
shadow.appendChild(cstyle);
const chromeMount = document.createElement("div");
shadow.appendChild(chromeMount);
let panelOpen = false;
let panelView = "main";
let keyEditing = false;
let pendingFocusRow = null;
let focusCardEl = null;
let lastRenderAt = 0;
let lastRenderedState = null;
let pendingRenderTimer = null;
const RENDER_THROTTLE_MS = 900;
function flashHighlight(el) {
if (typeof el.scrollIntoView === "function") el.scrollIntoView({ behavior: "smooth", block: "center" });
if (typeof el.animate !== "function") return;
const r = "rgba(178,150,255,";
const reduce = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
if (reduce) {
el.animate([{ boxShadow: `0 0 0 3px ${r}.95)` }, { boxShadow: `0 0 0 3px ${r}0)` }], { duration: 2400, easing: "ease-out", fill: "none" });
return;
}
el.animate(
[
{ boxShadow: `0 0 0 0 ${r}0)`, backgroundColor: `${r}0)`, offset: 0 },
{ boxShadow: `0 0 0 4px ${r}.95), 0 0 24px 6px ${r}.55)`, backgroundColor: `${r}.3)`, offset: 0.12 },
{ boxShadow: `0 0 0 2px ${r}.45)`, backgroundColor: `${r}.1)`, offset: 0.48 },
{ boxShadow: `0 0 0 4px ${r}.95), 0 0 24px 6px ${r}.55)`, backgroundColor: `${r}.26)`, offset: 0.72 },
{ boxShadow: `0 0 0 0 ${r}0)`, backgroundColor: `${r}0)`, offset: 1 }
],
{ duration: 2600, easing: "ease-in-out" }
);
}
function enforceSingleRoot() {
const roots = [...document.querySelectorAll(`#${ROOT_ID}`)];
const newerOwner = roots.find(
(node) => node !== host && !!node.dataset.cxVersion && compareVersions(node.dataset.cxVersion, UI_VERSION) >= 0
);
if (newerOwner) return false;
roots.forEach((node) => {
if (node !== host) node.remove();
});
if (!host.isConnected && document.body) document.body.appendChild(host);
return true;
}
function resetValuations() {
valueCache.clear();
jobs.clear();
catalogById.clear();
deepCount.clear();
requirementsByItem.clear();
queue.clear();
failedItems.clear();
catalog = [];
catalogReady = false;
catalogLoading = false;
catalogFailed = false;
rowViews.clear();
sessionDeals.clear();
activeKind = null;
valued = 0;
document.querySelectorAll(`.${BADGE_CLASS}`).forEach((node) => node.remove());
document.querySelectorAll(`[${BADGE_ATTR}]`).forEach((node) => node.removeAttribute(BADGE_ATTR));
}
function retryChecks() {
catalogFailed = false;
failedItems.clear();
renderChrome();
if (catalogReady) annotateAll();
else void boot();
}
function text(tag, className, value) {
const el = document.createElement(tag);
el.className = className;
el.textContent = value;
return el;
}
function scanState() {
if (!getKey()) return "needs-key";
if (catalogFailed && !catalogReady) return "problem";
if (catalogLoading || !catalogReady || queue.initialSize > 0 || initialInFlight > 0) return "checking";
if (failedItems.size > 0) return "problem";
return "ready";
}
function scanStatusLabel(state = scanState()) {
if (state === "ready") return "Ready";
if (state === "checking") return "Checking page";
if (state === "problem") return "Try again";
return "API key needed";
}
function statusDot(state = scanState()) {
return text("span", `status-dot ${state === "ready" ? "ready" : state === "checking" ? "checking" : ""}`, "");
}
function bonusLabel(bonuses) {
return bonuses.map((bonus) => `${bonus.name.replace(/\b\w/g, (c) => c.toUpperCase())}${bonus.percent != null ? ` ${bonus.percent}%` : ""}`).join(" + ");
}
function saleDate(timestamp) {
try {
return new Intl.DateTimeFormat(void 0, { month: "short", day: "numeric" }).format(new Date(timestamp * 1e3));
} catch {
return new Date(timestamp * 1e3).toISOString().slice(5, 10);
}
}
function aavText(view) {
if (view.perkBound === "floor" || view.matchTier === "rare") return `\u2265${money(view.fair)}`;
if (view.perkBound === "ceiling") return `\u2264${money(view.fair)}`;
return money(view.fair);
}
function aavRangeText(view) {
if (view.perkBound === "floor" || view.matchTier === "rare") return `\u2265${money(view.fair)}`;
if (view.perkBound === "ceiling") return `\u2264${money(view.fair)}`;
if (view.matchTier === "exact") return money(view.fair);
return rangeText(view.aavLow, view.aavHigh);
}
function rangeText(low, high) {
const l = moneyC(low), h = moneyC(high);
if (l === h || !(high > low)) return moneyC(Math.round((low + high) / 2));
const lu = l.slice(-1), hu = h.slice(-1);
return lu === hu && /[a-z]/i.test(hu) ? `${l.slice(0, -1)}\u2013${h.slice(1)}` : `${l}\u2013${h}`;
}
function bidVsAav(view) {
if (!view.price || view.price <= 0 || view.fair <= 0) return null;
const gap = (view.fair - view.price) / view.fair;
const below = gap >= 0;
if (view.matchTier === "rare") return null;
if (view.perkBound === "floor" && !below) return null;
if (view.perkBound === "ceiling" && below) return null;
return { pct: Math.round(Math.abs(gap) * 100), below };
}
function vsTileContent(view, vs) {
if (view.price == null) return { val: "\u2014", cls: "", sub: "no bid yet" };
if (vs) {
const cls = vs.below ? vs.pct >= 10 ? "positive" : "neutral" : "negative";
return { val: `${vs.pct}% ${vs.below ? "below" : "above"}`, cls, sub: `${moneyC(Math.abs(view.fair - view.price))} ${vs.below ? "under" : "over"}` };
}
if (view.matchTier === "rare") return { val: "\u2014", cls: "", sub: "too rare to call" };
if (view.perkBound === "floor") return { val: "\u2014", cls: "", sub: "bid is above the floor" };
if (view.perkBound === "ceiling") return { val: "\u2014", cls: "", sub: "bid is under the cap" };
return { val: "\u2014", cls: "", sub: "no bid yet" };
}
function matchChip(view) {
if (view.matchTier === "exact")
return { label: "Direct comps", cls: "verified", title: `${view.comps} past sales of this exact weapon, rarity and roll.` };
if (view.matchTier === "rare")
return { label: "Not enough history", cls: "none", title: "Few sales at this exact version, so this is a floor from more common ones." };
return { label: "Range estimate", cls: "close", title: `From ${view.comps} similar recent sales.` };
}
function rollChips(view) {
if (!view.rarity) return [];
const single = view.bonuses.length === 1;
const out = [];
for (const b of view.bonuses) {
if (b.percent == null) continue;
const id = bonusIdByTitle(b.name);
if (id == null) continue;
const c = classifyRollPosition(id, view.rarity, b.percent);
if (!c) continue;
const cls = c.position === "high" ? "verified" : c.position === "mid" ? "close" : "none";
const bandTxt = c.min === c.max ? `${c.min}%` : `${c.min}\u2013${c.max}%`;
const tierWord = c.label.replace(" roll", "");
const perkName = b.name.replace(/\b\w/g, (ch) => ch.toUpperCase());
const suffix = c.inBand ? "" : " (outside the usual band)";
out.push({
label: single ? `${tierWord} roll` : `${perkName}: ${tierWord}`,
cls,
title: `${perkName} ${b.percent}% is a ${c.label} for ${view.rarity} (usual range ${bandTxt})${suffix}. Higher rolls are worth more.`
});
}
return out;
}
function seasonalChip(view) {
if (!view.seasonal || !season) return null;
const halloween = /hallow|trick|treat/i.test(season.title);
const label = halloween ? "Halloween \u2191" : "Seasonal \u2191";
const what = view.seasonal === "revitalize" ? "Revitalize" : "Melee";
return { label, title: `${what} demand tends to rise before ${season.title} (in ${season.daysUntil}d).` };
}
function chip(label, cls, title = "") {
const el = text("span", `chip ${cls}`, label);
if (title) el.title = title;
return el;
}
function tile(label, value, valueCls = "", sub = "") {
const t = document.createElement("div");
t.className = "tile";
t.append(text("span", "t-label", label), text("b", `t-val ${valueCls}`, value));
if (sub) t.append(text("span", "t-sub", sub));
return t;
}
function renderDecisionCard(view) {
const tier = roiTier(view);
const card = document.createElement("article");
const rare = view.matchTier === "rare";
const loose = view.matchTier === "rough" || rare || view.perkBound != null;
card.className = `card ${tier}${loose ? " indicative" : ""}`;
if (pendingFocusRow && view.row === pendingFocusRow) focusCardEl = card;
const head = document.createElement("div");
head.className = "c-head";
const id = document.createElement("div");
id.className = "c-id";
id.append(text("div", "c-weapon", view.weaponName), text("div", "c-perk", bonusLabel(view.bonuses)));
const q1 = (n) => Math.round(n * 10) / 10;
const statBits = [];
if (view.rowArmor != null) statBits.push(`${q1(view.rowArmor)} armor`);
if (view.rowDamage != null) statBits.push(`${q1(view.rowDamage)} dmg`);
if (view.rowAccuracy != null) statBits.push(`${q1(view.rowAccuracy)} acc`);
if (view.quality != null) statBits.push(`${q1(view.quality)} quality`);
if (statBits.length) id.append(text("div", "c-stats", statBits.join(" \xB7 ")));
const chips = document.createElement("div");
chips.className = "c-chips";
if (view.rarity) chips.appendChild(chip(view.rarity, `rarity ${view.rarity}`));
const mc = matchChip(view);
const mcTitle = `${mc.title}${view.windowDays != null ? ` Window: ${Math.round(view.windowDays)}d.` : ""}`;
chips.appendChild(chip(mc.label, mc.cls, mcTitle));
for (const rc of rollChips(view)) chips.appendChild(chip(rc.label, rc.cls, rc.title));
const sc = seasonalChip(view);
if (sc) chips.appendChild(chip(sc.label, "season", sc.title));
head.append(id, chips);
card.appendChild(head);
const tiles = document.createElement("div");
tiles.className = "c-tiles";
const rareSub = view.doublePerk && !view.crossedRarity && view.matchedRarity ? `at least, from ${view.matchedRarity} sales` : "at least this";
const worthTile = tile(AAV_FULL, aavRangeText(view), "", rare ? rareSub : "");
worthTile.classList.add("hero");
worthTile.title = `${AAV_FULL}. From completed auction sales.`;
const vs = bidVsAav(view);
const vt = vsTileContent(view, vs);
const vsTile = tile("vs AAV", vt.val, vt.cls, vt.sub);
vsTile.title = "Current bid against AAV.";
const timeLeft = formatTimeRemaining(view.endsInSeconds);
const bidTile = tile("Current bid", view.price != null ? money(view.price) : "\u2014", "", timeLeft ? `Ends in ${timeLeft}` : "");
tiles.append(worthTile, vsTile, bidTile);
card.appendChild(tiles);
if (rare) {
const note = view.doublePerk && !view.crossedRarity && view.matchedRarity ? `Floor from single-perk ${view.matchedRarity} sales; a second perk adds to it.` : `Floor from more common versions. Few sales at this rarity.`;
card.appendChild(text("p", "c-note", note));
} else if (view.perkBound === "floor") {
card.appendChild(text("p", "c-note", "This roll is above every recent sale, so the value below is a floor."));
} else if (view.perkBound === "ceiling") {
card.appendChild(text("p", "c-note", "This roll is below every recent sale, so the value below is a cap."));
} else {
const meta = document.createElement("div");
meta.className = "c-meta";
const lines = [];
const fd = view.confidence.freshDays;
const freshPart = fd != null ? fd <= 1 ? `${view.comps} sales found, latest today.` : `${view.comps} sales found, latest ${fd} days ago.` : `${view.comps} sales found.`;
lines.push(freshPart);
if (view.trend) {
const pct = Math.abs(Math.round(view.trend.changePct * 100));
if (view.trend.direction === "up") {
lines.push(`Trending up ${pct}%.`);
} else if (view.trend.direction === "down") {
lines.push(`Trending down ${pct}%.`);
} else {
lines.push("Prices stable.");
}
}
meta.textContent = lines.join(" ");
meta.title = view.trend ? `${view.trend.recentSales} recent vs ${view.trend.olderSales} older sales over ${Math.round(view.trend.spanDays)}d${view.trend.qualityAdjusted ? ", quality-adjusted" : ""}.` : "";
card.appendChild(meta);
}
if (!rare && view.concentration) {
const msg = view.concentration === "wash" ? "Same seller and buyer recur across recent sales." : "Most recent sales are from one seller.";
card.appendChild(text("p", "c-flag", msg));
}
if (view.recentSales.length > 0) {
const sales = document.createElement("div");
sales.className = "sales";
const base = view.recentSales.slice(0, 5);
const perkVals = base.map((s) => s.perks[0]).filter((v) => typeof v === "number");
const isSpread = !rare && view.matchTier !== "exact" && perkVals.length > 1 && Math.max(...perkVals) - Math.min(...perkVals) >= 1;
const shown = isSpread ? [...base].sort((a, b) => (a.perks[0] ?? 0) - (b.perks[0] ?? 0)) : base;
const salesTitle = document.createElement("div");
salesTitle.className = "sales-title";
const titleMain = rare ? "More common sales" : isSpread ? "Sample of sales" : "Recent sales";
const titleSub = isSpread ? `Low to high roll \xB7 ${shown.length} of ${view.comps}` : `Latest ${shown.length} of ${view.comps}`;
salesTitle.append(document.createTextNode(titleMain), text("span", "", titleSub));
sales.appendChild(salesTitle);
const isArmor = view.kind === "armor";
const header = document.createElement("div");
header.className = `sale sale-h${isArmor ? " k-armor" : ""}`;
const headCols = isArmor ? ["", "Price", "Perk", "Armor", "Quality"] : ["", "Price", "Perk", "Dmg", "Acc", "Quality"];
for (const c of headCols) header.append(text("span", "", c));
sales.appendChild(header);
const q12 = (n) => Math.round(n * 10) / 10;
const stat = (n) => n != null ? String(q12(n)) : "\u2014";
for (const sale of shown) {
const row = document.createElement("div");
row.className = `sale${isArmor ? " k-armor" : ""}`;
const perkTxt = sale.perks.length ? sale.perks.map((p) => `${p}%`).join("/") : "\u2014";
row.append(text("span", "", saleDate(sale.timestamp)), text("b", "", money(sale.price)), text("em", "s-perk", perkTxt));
if (isArmor) row.append(text("em", "", stat(sale.armor)), text("em", "", stat(sale.quality)));
else row.append(text("em", "", stat(sale.damage)), text("em", "", stat(sale.accuracy)), text("em", "", stat(sale.quality)));
sales.appendChild(row);
}
card.appendChild(sales);
}
const jump = document.createElement("button");
jump.type = "button";
jump.className = "jump";
jump.textContent = "Show this auction";
jump.addEventListener("click", () => {
panelOpen = false;
renderChrome();
flashHighlight(view.row);
});
card.appendChild(jump);
return card;
}
function renderKeyForm() {
const box = document.createElement("div");
box.className = "key";
const label = document.createElement("label");
label.textContent = "Connect your public API key";
const copy = document.createElement("p");
copy.textContent = "Cortex values the gear on this page from completed Auction House sales via Torn's official API. A read-only public key is all it needs.";
const get = document.createElement("button");
get.type = "button";
get.className = "ghost get-key";
get.textContent = "Get a public key on Torn";
get.addEventListener("click", () => window.open(TORN_KEY_URL, "_blank", "noopener"));
const input = document.createElement("input");
input.type = "password";
input.autocomplete = "off";
input.spellcheck = false;
input.placeholder = "Paste your 16-character key";
input.setAttribute("aria-label", "Torn API key");
const save = document.createElement("button");
save.type = "button";
save.className = "primary";
save.textContent = "Save key";
const err = document.createElement("div");
err.className = "error";
save.addEventListener("click", () => {
const val = input.value.trim();
if (!isValidKeyFormat(val)) {
err.textContent = "Enter a valid 16-character key.";
return;
}
storageSet(STORAGE_KEY, val);
err.textContent = "";
renderChrome();
void boot();
});
const trust = document.createElement("div");
trust.className = "key-trust";
const pop = document.createElement("p");
pop.className = "key-pop";
pop.style.display = "none";
pop.textContent = "Your key stays on this device and is sent only to api.torn.com. Cortex reads completed sales to value gear, and reads only the auction page you have open. Remove it any time in Settings.";
const how = document.createElement("button");
how.type = "button";
how.className = "linkish";
how.textContent = "How your key is used";
how.addEventListener("click", () => {
pop.style.display = pop.style.display === "none" ? "block" : "none";
});
const privacy = document.createElement("a");
privacy.href = PRIVACY_URL;
privacy.target = "_blank";
privacy.rel = "noopener";
privacy.textContent = "Privacy";
const dataPolicy = document.createElement("a");
dataPolicy.href = DATA_POLICY_URL;
dataPolicy.target = "_blank";
dataPolicy.rel = "noopener";
dataPolicy.textContent = "Data policy";
trust.append(how, privacy, dataPolicy);
box.append(label, copy, get, input, save, err, trust, pop);
return box;
}
function renderOnboard() {
const card = document.createElement("div");
card.className = "cx-onboard";
card.setAttribute("role", "note");
card.append(
text("div", "ob-title", "Connect your public API key"),
text("div", "ob-copy", "Cortex values the weapons and armor on this page from real completed sales. Add a read-only key to start.")
);
const actions = document.createElement("div");
actions.className = "ob-actions";
const setup = document.createElement("button");
setup.type = "button";
setup.className = "primary";
setup.textContent = "Set up";
setup.addEventListener("click", () => {
panelOpen = true;
panelView = "main";
renderChrome();
});
const get = document.createElement("button");
get.type = "button";
get.className = "ghost";
get.textContent = "Get a key";
get.addEventListener("click", () => window.open(TORN_KEY_URL, "_blank", "noopener"));
actions.append(setup, get);
card.appendChild(actions);
return card;
}
function renderPanel() {
const panel = document.createElement("section");
panel.className = "panel";
panel.setAttribute("aria-label", "Cortex Auction Analysis");
const head = document.createElement("header");
head.className = "panel-head";
const heading = document.createElement("div");
heading.append(text("div", "panel-title", "Cortex Auction Check"), text("div", "panel-sub", "Auction values from completed sales"));
const headActions = document.createElement("div");
headActions.className = "head-actions";
const state = scanState();
const status = document.createElement("div");
status.className = "status";
status.setAttribute("role", "status");
status.append(statusDot(state), document.createTextNode(scanStatusLabel(state)));
const inSettings = panelView === "settings";
const gear = document.createElement("button");
gear.type = "button";
gear.className = `icon-btn${inSettings ? " active" : ""}`;
gear.textContent = inSettings ? "\u2039" : "\u2699";
gear.title = inSettings ? "Back to auctions" : "Settings & legend";
gear.setAttribute("aria-label", inSettings ? "Back to auctions" : "Open settings and legend");
gear.addEventListener("click", () => {
panelView = inSettings ? "main" : "settings";
keyEditing = false;
renderChrome();
});
const close = document.createElement("button");
close.type = "button";
close.className = "icon-btn";
close.textContent = "\xD7";
close.title = "Close Cortex";
close.setAttribute("aria-label", "Close Cortex");
close.addEventListener("click", () => {
panelOpen = false;
panelView = "main";
keyEditing = false;
renderChrome();
});
headActions.append(status, gear, close);
head.append(heading, headActions);
panel.appendChild(head);
const body = document.createElement("div");
body.className = "panel-body";
if (inSettings) {
body.appendChild(renderSettings());
panel.appendChild(body);
return panel;
}
const key = getKey();
if (!key) {
body.appendChild(renderKeyForm());
panel.appendChild(body);
return panel;
}
if (!catalogReady) {
if (catalogFailed) {
const failedNotice = document.createElement("div");
failedNotice.className = "notice";
failedNotice.append(text("strong", "", "Couldn't load sales"), document.createTextNode("Cortex couldn't reach the official Torn API."));
const retry = document.createElement("button");
retry.type = "button";
retry.className = "primary";
retry.textContent = "Try again";
retry.addEventListener("click", retryChecks);
failedNotice.appendChild(retry);
body.appendChild(failedNotice);
} else {
body.appendChild(renderProgress());
}
panel.appendChild(body);
return panel;
}
if (state === "checking") body.appendChild(renderProgress());
const views = [...rowViews.values()];
if (failedItems.size > 0) {
const failed = document.createElement("div");
failed.className = "notice";
failed.append(text("strong", "", `${failedItems.size} item${failedItems.size === 1 ? "" : "s"} could not be checked`), document.createTextNode("The official Torn API did not return completed sales for every item."));
const retry = document.createElement("button");
retry.type = "button";
retry.className = "primary";
retry.textContent = "Try again";
retry.addEventListener("click", retryChecks);
failed.appendChild(retry);
body.appendChild(failed);
}
const exact = views.filter((view) => view.evidence === "verified").length;
const positive = views.filter((view) => (view.projection?.netProfit ?? 0) > 0).length;
const bestDiscount = views.reduce((max, v) => {
const vs = bidVsAav(v);
return vs && vs.below && vs.pct > max ? vs.pct : max;
}, 0);
const kpis = document.createElement("div");
kpis.className = "kpis";
const kpiData = [
[`${valued}/${candidates}`, "Priced"],
[`${positive}`, "Below AAV"],
[bestDiscount > 0 ? `${bestDiscount}%` : "\u2014", "Biggest discount"]
];
for (const [value, label] of kpiData) {
const kpi = document.createElement("div");
kpi.className = "kpi";
kpi.append(text("b", "", value), text("span", "", label));
kpis.appendChild(kpi);
}
body.appendChild(kpis);
const dealNow = Date.now();
for (const [k, d] of sessionDeals) if (dealNow - d.seenAt >= SESSION_DEAL_TTL_MS) sessionDeals.delete(k);
const topDeals = [...sessionDeals.values()].filter((d) => activeKind == null || d.kind === activeKind).sort((a, b) => b.below - a.below).slice(0, 5);
if (topDeals.length > 0) {
const deals = document.createElement("div");
deals.className = "deals";
const bestBuysOpen = storageGet(BEST_BUYS_KEY) === "open";
const header = document.createElement("button");
header.type = "button";
header.className = "deals-header";
header.textContent = `Best buys (${topDeals.length})`;
header.title = bestBuysOpen ? "Collapse best buys" : "Expand best buys";
const arrow = text("span", `deals-arrow${bestBuysOpen ? " open" : ""}`, "\u203A");
header.prepend(arrow);
const content = document.createElement("div");
content.className = "deals-content";
content.style.display = bestBuysOpen ? "block" : "none";
header.addEventListener("click", () => {
const isOpen = content.style.display !== "none";
content.style.display = isOpen ? "none" : "block";
arrow.className = `deals-arrow${isOpen ? "" : " open"}`;
header.title = isOpen ? "Expand best buys" : "Collapse best buys";
storageSet(BEST_BUYS_KEY, isOpen ? "closed" : "open");
});
for (const d of topDeals) {
const rowEl = document.createElement("div");
rowEl.className = "deal";
const main2 = document.createElement("div");
main2.className = "deal-main";
main2.append(text("span", "deal-w", d.weapon), text("span", "deal-perk", d.perk));
const right = document.createElement("div");
right.className = "deal-right";
right.append(text("b", "deal-roi", `${Math.round(d.below * 100)}% under`), text("span", "deal-v", `AAV ${moneyC(d.worth)}`));
rowEl.append(main2, right);
content.appendChild(rowEl);
}
deals.append(header, content);
body.appendChild(deals);
}
if (season && season.daysUntil <= SEASON_WINDOW_DAYS) {
const banner = document.createElement("div");
banner.className = "season-banner";
banner.append(
text("b", "", season.title),
document.createTextNode(` in ${season.daysUntil}d \xB7 Revitalize and melee demand rises before attacking events.`)
);
banner.title = "Revitalize and melee demand rises before attacking events (guide-backed).";
body.appendChild(banner);
}
if (views.length === 0) {
const empty = document.createElement("div");
empty.className = "notice";
empty.append(text("strong", "", candidates > 0 ? "Checking recent sales" : "No ranked-war weapons here"), document.createTextNode(candidates > 0 ? "Rare rolls may not have a value until more sales come in." : "Cortex values yellow, orange and red weapons that carry a perk."));
body.appendChild(empty);
} else {
views.sort((a, b) => {
const ar = a.projection?.roi ?? Number.NEGATIVE_INFINITY;
const br = b.projection?.roi ?? Number.NEGATIVE_INFINITY;
return br - ar || Number(b.evidence === "verified") - Number(a.evidence === "verified") || b.comps - a.comps;
});
const cards = document.createElement("div");
cards.className = "cards";
views.forEach((view) => cards.appendChild(renderDecisionCard(view)));
body.appendChild(cards);
}
panel.appendChild(body);
return panel;
}
function anonymizeKey(k) {
if (k.length <= 6) return "\u2022".repeat(k.length);
return `${k.slice(0, 3)}${"\u2022".repeat(Math.max(6, k.length - 6))}${k.slice(-3)}`;
}
function renderKeyManager() {
const box = document.createElement("div");
box.className = "set-key";
box.appendChild(text("div", "set-h", "Torn API key"));
if (pdaKey()) {
box.appendChild(text("p", "set-copy", "Provided by Torn PDA on this device."));
box.appendChild(text("div", "key-mask", anonymizeKey(pdaKey())));
return box;
}
const stored = storageGet(STORAGE_KEY);
const hasKey = !!stored && isValidKeyFormat(stored);
if (hasKey && !keyEditing) {
const row = document.createElement("div");
row.className = "key-row";
row.append(text("span", "key-ok", "\u2713"), text("code", "key-mask", anonymizeKey(stored)));
box.appendChild(row);
box.appendChild(text("p", "set-copy", "Stored on this device. Sent only to api.torn.com."));
const actions2 = document.createElement("div");
actions2.className = "key-actions";
const change = document.createElement("button");
change.type = "button";
change.className = "ghost";
change.textContent = "Change key";
change.addEventListener("click", () => {
keyEditing = true;
renderChrome();
});
const forget = document.createElement("button");
forget.type = "button";
forget.className = "ghost danger";
forget.textContent = "Forget key";
forget.addEventListener("click", () => {
storageDelete(STORAGE_KEY);
resetValuations();
keyEditing = false;
renderChrome();
});
actions2.append(change, forget);
box.appendChild(actions2);
return box;
}
box.appendChild(text("p", "set-copy", "A 16-character key. Stored on this device, sent only to api.torn.com."));
const input = document.createElement("input");
input.type = "password";
input.autocomplete = "off";
input.spellcheck = false;
input.placeholder = "16-character API key";
input.setAttribute("aria-label", "Torn API key");
const err = document.createElement("div");
err.className = "error";
const actions = document.createElement("div");
actions.className = "key-actions";
const save = document.createElement("button");
save.type = "button";
save.className = "primary";
save.textContent = "Save key";
save.addEventListener("click", () => {
const val = input.value.trim();
if (!isValidKeyFormat(val)) {
err.textContent = "Enter a valid 16-character key.";
return;
}
storageSet(STORAGE_KEY, val);
keyEditing = false;
renderChrome();
void boot();
});
actions.appendChild(save);
if (hasKey) {
const cancel = document.createElement("button");
cancel.type = "button";
cancel.className = "ghost";
cancel.textContent = "Cancel";
cancel.addEventListener("click", () => {
keyEditing = false;
renderChrome();
});
actions.appendChild(cancel);
}
box.append(input, actions, err);
return box;
}
function renderAppearance() {
const card = document.createElement("div");
card.className = "set-card";
const row = document.createElement("div");
row.className = "set-row";
row.appendChild(text("div", "set-h", "Appearance"));
const seg = document.createElement("div");
seg.className = "seg";
for (const opt of ["dark", "light"]) {
const b = document.createElement("button");
b.type = "button";
b.className = theme === opt ? "on" : "";
b.textContent = opt === "dark" ? "Dark" : "Light";
b.addEventListener("click", () => {
if (theme !== opt) applyTheme(opt);
});
seg.appendChild(b);
}
row.appendChild(seg);
card.appendChild(row);
return card;
}
function renderDataCard() {
const card = document.createElement("div");
card.className = "set-card";
card.appendChild(text("div", "set-h", "Data"));
const saved = [...jobs.values()].filter((j) => j.sales.length > 0).length;
card.appendChild(text("p", "set-copy", `${saved} item${saved === 1 ? "" : "s"} cached on this device. Sales refresh on their own; force it here if you want the newest now.`));
const actions = document.createElement("div");
actions.className = "key-actions";
const refresh = document.createElement("button");
refresh.type = "button";
refresh.className = "ghost";
refresh.textContent = "Refresh now";
refresh.addEventListener("click", () => {
for (const v of rowViews.values()) {
const c = valueCache.get(v.itemId);
if (c) c.at = 0;
}
panelView = "main";
renderChrome();
if (catalogReady) annotateAll();
else void boot();
});
const clear = document.createElement("button");
clear.type = "button";
clear.className = "ghost danger";
clear.textContent = "Clear cached sales";
clear.addEventListener("click", () => {
clearSalesData();
});
actions.append(refresh, clear);
card.appendChild(actions);
return card;
}
function clearSalesData() {
void deleteDbPayload();
jobs.clear();
valueCache.clear();
deepCount.clear();
requirementsByItem.clear();
failedItems.clear();
queue.clear();
rowViews.clear();
sessionDeals.clear();
valued = 0;
document.querySelectorAll(`.${BADGE_CLASS}`).forEach((node) => node.remove());
document.querySelectorAll(`[${BADGE_ATTR}]`).forEach((node) => node.removeAttribute(BADGE_ATTR));
keyEditing = false;
panelView = "main";
renderChrome();
if (catalogReady) annotateAll();
else void boot();
}
function renderSettings() {
const wrap = document.createElement("div");
wrap.className = "settings";
wrap.appendChild(renderAppearance());
wrap.appendChild(renderKeyManager());
wrap.appendChild(renderDataCard());
const legend = document.createElement("div");
legend.className = "legend";
legend.appendChild(text("div", "set-h", "Reference"));
const items = [
[`${AAV_WORD}`, `${AAV_FULL}. Typical sale price from completed auctions.`],
["vs AAV", "Current bid against AAV."],
["Direct comps", "Same item, rarity and roll."],
["Range estimate", "From similar sales; shown as a range."],
["Not enough history", "Floor from more common versions."],
["High / medium / low", "Where the perk % sits in its range."],
["Trend", "Recent sales against older."],
["Seasonal", "Demand around attacking events."],
["Flag", "One dominant seller, or a repeat buyer/seller pair."]
];
for (const [term, desc] of items) {
const row = document.createElement("div");
row.className = "legend-row";
row.append(text("div", "legend-term", term), text("div", "legend-desc", desc));
legend.appendChild(row);
}
wrap.appendChild(legend);
wrap.appendChild(text("p", "set-foot", "Prices come from completed Auction House sales via Torn's official API."));
return wrap;
}
let lastChromeDataSignature = "";
function chromeDataSignature() {
const rows = [...rowViews.values()].map((view) => {
const recent = view.recentSales.map((sale) => `${sale.saleId ?? ""}:${sale.timestamp}:${sale.price}:${sale.quality ?? ""}`).join(",");
return `${view.itemId}:${view.sigKey}:${view.price ?? ""}:${Math.round(view.fair)}:${view.comps}:${view.evidence}:${view.estimateReason ?? ""}:${recent}`;
}).sort().join("|");
return `${scanState()}:${getKey() ? "key" : "no-key"}:${catalogReady}:${valued}:${candidates}:${season?.title ?? ""}:${season?.daysUntil ?? ""}:${rows}`;
}
function renderChromeIfDataChanged() {
if (panelOpen && panelView === "settings") return;
const next = chromeDataSignature();
if (next === lastChromeDataSignature) return;
const state = scanState();
if (state === "checking" && state === lastRenderedState) {
const sinceLast = Date.now() - lastRenderAt;
if (sinceLast < RENDER_THROTTLE_MS) {
if (!pendingRenderTimer) {
pendingRenderTimer = setTimeout(() => {
pendingRenderTimer = null;
if (!stopped) renderChrome();
}, RENDER_THROTTLE_MS - sinceLast);
}
return;
}
}
renderChrome();
}
function renderChrome() {
if (stopped) return;
if (pendingRenderTimer) {
clearTimeout(pendingRenderTimer);
pendingRenderTimer = null;
}
lastRenderAt = Date.now();
lastRenderedState = scanState();
syncScanClock();
ensureProgressTimer();
lastChromeDataSignature = chromeDataSignature();
const prevScroll = shadow.querySelector(".panel-body")?.scrollTop ?? 0;
chromeMount.textContent = "";
if (!panelOpen) {
const state = scanState();
const tab = document.createElement("button");
tab.type = "button";
tab.className = "tab";
tab.title = `${scanStatusLabel(state)}. Open Cortex Auction Check`;
tab.setAttribute("aria-label", `${scanStatusLabel(state)}. Open Cortex Auction Check`);
tab.append(statusDot(state), text("span", "cx-arrow", "\u2039"), text("span", "cx-name", "CORTEX"));
if (getKey() && candidates > 0) tab.append(text("span", "cx-count", `${valued}/${candidates}`));
tab.addEventListener("click", () => {
panelOpen = true;
renderChrome();
});
chromeMount.appendChild(tab);
if (!getKey()) chromeMount.appendChild(renderOnboard());
return;
}
chromeMount.appendChild(renderPanel());
const body = shadow.querySelector(".panel-body");
if (body && prevScroll > 0) body.scrollTop = prevScroll;
if (focusCardEl) {
const target = focusCardEl;
requestAnimationFrame(() => flashHighlight(target));
focusCardEl = null;
pendingFocusRow = null;
}
}
function schedule(delay = 300) {
if (stopped) return;
if (renderTimer) clearTimeout(renderTimer);
renderTimer = setTimeout(() => annotateAll(), delay);
}
let seasonLoaded = false;
async function loadSeason(key) {
if (seasonLoaded || stopped) return;
seasonLoaded = true;
try {
const raw = await fetchJson("torn/calendar", key, pageIsActive);
if (stopped) return;
season = nextAttackingEvent(parseCalendarEvents(raw), Math.floor(Date.now() / 1e3));
if (season) annotateAll();
renderChromeIfDataChanged();
} catch {
}
}
async function boot() {
if (stopped) return;
await ensureRestored();
if (stopped) return;
const key = getKey();
renderChrome();
if (!key || catalogReady || catalogLoading || !pageIsActive()) return;
catalogLoading = true;
catalogFailed = false;
renderChromeIfDataChanged();
try {
catalog = await loadCatalog(key);
if (stopped) return;
catalogReady = catalog.length > 0;
catalogFailed = !catalogReady;
catalogById.clear();
for (const entry of catalog) catalogById.set(entry.id, entry);
void loadSeason(key);
} catch {
if (!stopped) {
catalogReady = false;
catalogFailed = true;
}
} finally {
catalogLoading = false;
}
if (stopped) return;
if (catalogReady) annotateAll();
else renderChromeIfDataChanged();
}
function onPageActive() {
if (stopped || !pageIsActive()) return;
void boot();
void drain();
schedule(200);
}
function teardown() {
if (stopped) return;
stopped = true;
observer?.disconnect();
observer = null;
if (renderTimer) clearTimeout(renderTimer);
renderTimer = null;
if (pendingRenderTimer) clearTimeout(pendingRenderTimer);
pendingRenderTimer = null;
stopProgressTimer();
void flushSalesDb();
queue.clear();
document.removeEventListener("visibilitychange", onPageActive);
document.removeEventListener("click", onPageInteract, true);
window.removeEventListener("focus", onPageActive);
window.removeEventListener("pagehide", onPageHide);
document.removeEventListener(OWNER_EVENT, onNewOwner);
document.querySelectorAll(`.${BADGE_CLASS}`).forEach((node) => node.remove());
document.querySelectorAll(`[${BADGE_ATTR}]`).forEach((node) => node.removeAttribute(BADGE_ATTR));
host.remove();
document.getElementById(STYLE_ID)?.remove();
}
function onPageHide() {
teardown();
}
function onPageInteract(event) {
const target = event.target;
if (target && target.nodeType === 1 && target.closest?.(`#${ROOT_ID}`)) return;
schedule(350);
}
function onNewOwner(event) {
const version = event.detail?.version;
if (version && compareVersions(version, UI_VERSION) >= 0) teardown();
}
ensureFont();
injectStyle();
enforceSingleRoot();
observer = new MutationObserver((mutations) => {
if (stopped) return;
for (const m of mutations) {
const t = m.target;
if (t && t.nodeType === 1 && (t.closest?.(`.${BADGE_CLASS}`) || t.id === ROOT_ID)) continue;
schedule(300);
return;
}
});
observer.observe(document.body, { childList: true, subtree: true });
document.addEventListener("visibilitychange", onPageActive);
document.addEventListener(OWNER_EVENT, onNewOwner);
document.addEventListener("click", onPageInteract, true);
window.addEventListener("focus", onPageActive);
window.addEventListener("pagehide", onPageHide, { once: true });
void boot();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", main);
} else {
main();
}
})();