Shared defender loadouts on Torn attack pages (desktop + Torn PDA). Firebase, no registration.
// ==UserScript==
// @name Torn Loadout Share
// @namespace loadout
// @version 1.1.4
// @description Shared defender loadouts on Torn attack pages (desktop + Torn PDA). Firebase, no registration.
// @license MIT
// @supportURL https://greasyfork.org/en/scripts/570314/feedback
// @homepageURL https://greasyfork.org/en/scripts/570314-torn-loadout-share
// @match https://www.torn.com/page.php?sid=attack&user2ID=*
// @match https://www.torn.com/loader.php?sid=attack&user2ID=*
// @connect torn-loadout-share-default-rtdb.europe-west1.firebasedatabase.app
// @run-at document-end
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// ==/UserScript==
(function () {
"use strict";
const W = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
if (W.__loadoutShareInit) return;
W.__loadoutShareInit = true;
const IS_PDA = !!(W.flutter_inappwebview?.callHandler);
// LOG_LEVEL: 0 = none, 1 = log, 2 = debug
const LOG_LEVEL = 0;
let EFFECTIVE_LOG_LEVEL = LOG_LEVEL;
if (W.__loadoutDebug || W.__loadoutPdaDebug || /[?&]loadout_debug=1/.test(W.location?.search || "")) {
EFFECTIVE_LOG_LEVEL = Math.max(EFFECTIVE_LOG_LEVEL, 2);
}
function log(...args) { if (EFFECTIVE_LOG_LEVEL >= 1 && W.console?.log) W.console.log("[Loadout]", ...args); }
function dbg(...args) { if (EFFECTIVE_LOG_LEVEL >= 2 && W.console?.log) W.console.log("[Loadout]", ...args); }
const BONUS_KEY_FIX = { hazarfouse: "hazardous" }; // Torn CSS class typos
const FIREBASE_FETCH_TIMEOUT_MS = 5000;
const TORN_BASE = "https://www.torn.com";
const CONFIG = {
firebaseUrl: "https://torn-loadout-share-default-rtdb.europe-west1.firebasedatabase.app",
imagesBase: `${TORN_BASE}/images`,
};
const WEAPON_SLOTS = [1, 2, 3, 5];
// Slot 10 is alternate helmet/mask, but Torn treats it as body cosmetics on many pages.
// We intentionally ignore it across parsing/validation/rendering.
const ARMOR_SLOTS = [8, 7, 9, 6, 4];
const DOM_POLL_MS = 100;
const PREFIGHT_REAPPLY_MS = 250;
const RENDER_MAX_MS = 15000;
const PREFIGHT_RENDER_MAX_MS = 120000;
const FALLBACK_DELAYS_MS = [1500, 3000, 5000];
const UPLOAD_RETRY_DELAYS_MS = [1500, 3500, 7000];
const ARMOR_Z_INDEX = { 8: 10, 7: 11, 9: 12, 6: 13, 4: 14 };
const ARMOR_MASK_SLOTS = new Set([8, 6, 4]);
const FALLBACK_ARMOR_DOM = {
container: "armourContainer___ftMzt",
armour: "armour___wqLa7",
img: "itemImg___r9DqK",
mask: "mask___rDyND",
};
const SILHOUETTES = { 1: "primary", 2: "secondary", 3: "melee", 5: "temporary" };
const ARMOR_MAP_NAME = "loadout-armor-map";
const ARMOR_SLOT_AREAS = {
4: [{ coords: "119,79,99,73,80,96,62,131,54,150,52,167,62,169,79,138,91,118,99,142,95,159,143,161,144,143,148,118,162,141,174,166,187,165,176,129,162,95,140,75" }],
6: [{ coords: "118,77,104,67,99,52,104,36,118,26,132,32,136,51,133,69" }],
7: [{ coords: "94,162,145,162,157,204,154,239,150,261,156,275,150,301,136,303,131,283,121,209,109,284,105,300,89,299,85,276,87,257,84,236,85,201" }],
8: [
{ coords: "87,300,89,322,86,336,78,349,88,354,99,354,104,340,106,325,105,302" },
{ coords: "136,304,153,300,151,318,153,330,160,343,153,352,138,353,132,330" },
],
9: [
{ coords: "48,203,55,192,62,195,67,192,61,172,50,169,44,183,40,203" },
{ coords: "175,171,189,170,196,185,198,200,191,202,184,191,177,196,176,180" },
],
};
const STATE = {
uploaded: false,
uploadInProgress: false,
uploadRetryCount: 0,
attackData: null,
loadoutRendered: false,
applyTimerId: null,
};
const PROPS_ROW_STYLE = "display:flex;flex-direction:row;align-items:center;justify-content:center;gap:2px;";
const TOP_ROW_STYLE = "display:flex;flex-direction:row;align-items:center;justify-content:space-between;width:100%;";
const PANEL_ID = "loadout-defender-panel";
const PANEL_BTN_ID = "loadout-defender-panel-toggle";
const LS_KEY_COLLAPSED = "loadout_panel_collapsed_v1";
function bonusToLabel(b) {
if (!b || typeof b !== "object") return "";
return String(b.name || b.bonus_key || b.key || "").trim();
}
function formatWeaponBonuses(item) {
const b1 = bonusToLabel(item?.bonuses?.[0]);
const b2 = bonusToLabel(item?.bonuses?.[1]);
if (!b1 && !b2) return "";
if (b1 && b2) return ` (${b1} + ${b2})`;
return ` (${b1 || b2})`;
}
function buildWeaponRow(item) {
if (!item) return "";
const name = escapeHtml(item.item_name || "Unknown");
const bonuses = formatWeaponBonuses(item);
return `<div style="color:#f2f5ff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${name}${escapeHtml(bonuses)}</div>`;
}
function buildArmorRow(item) {
if (!item) return "";
const name = escapeHtml(item.item_name || "");
const bonuses = formatWeaponBonuses(item);
return `<div style="color:#f2f5ff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${name}${escapeHtml(bonuses)}</div>`;
}
function renderDefenderPanel(loadout, submittedBy, savedAt) {
if (!loadout || typeof loadout !== "object") return;
const collapsed = (() => {
try {
return W.localStorage?.getItem(LS_KEY_COLLAPSED) === "1";
} catch {
return false;
}
})();
let panel = W.document.getElementById(PANEL_ID);
if (!panel) {
panel = W.document.createElement("div");
panel.id = PANEL_ID;
panel.style.cssText =
"position:fixed;top:12px;right:12px;z-index:999999;" +
"background:rgba(0,0,0,0.70);color:#fff;" +
"border:1px solid rgba(255,255,255,0.14);" +
"border-radius:10px;overflow:hidden;" +
"backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);";
const btn = W.document.createElement("div");
btn.id = PANEL_BTN_ID;
btn.style.cssText =
"display:flex;align-items:center;justify-content:space-between;" +
"gap:10px;padding:10px 12px;" +
"cursor:pointer;background:rgba(15,23,34,0.55);";
const left = W.document.createElement("div");
left.style.cssText = "font-weight:900;letter-spacing:0.2px;";
left.textContent = "Defender Loadout";
const right = W.document.createElement("div");
right.style.cssText = "font-weight:900;color:#7bcf9a;";
right.textContent = collapsed ? "▸" : "▾";
btn.appendChild(left);
btn.appendChild(right);
const body = W.document.createElement("div");
body.id = `${PANEL_ID}-body`;
body.style.cssText = "padding:10px 12px;display:flex;flex-direction:column;gap:10px;";
panel.appendChild(btn);
panel.appendChild(body);
W.document.body.appendChild(panel);
btn.onclick = () => {
try {
W.localStorage?.setItem(LS_KEY_COLLAPSED, panel.dataset.collapsed === "1" ? "0" : "1");
} catch {}
const nextCollapsed = panel.dataset.collapsed !== "1";
panel.dataset.collapsed = nextCollapsed ? "1" : "0";
body.style.display = nextCollapsed ? "none" : "flex";
right.textContent = nextCollapsed ? "▸" : "▾";
};
}
const body = W.document.getElementById(`${PANEL_ID}-body`);
if (!body) return;
const weaponsOrder = [1, 2, 3, 5];
const armorOrder = [6, 4, 7, 8, 9];
const weaponsHtml = weaponsOrder.map((slot) => buildWeaponRow(loadout?.[slot])).filter(Boolean).join("");
const armorHtml = armorOrder.map((slot) => buildArmorRow(loadout?.[slot])).filter(Boolean).join("");
const metaParts = [];
const safeSubmitterId = submittedBy && /^\d{1,8}$/.test(String(submittedBy)) ? String(submittedBy) : null;
if (safeSubmitterId) {
metaParts.push(
`<a href="${TORN_BASE}/profiles.php?XID=${safeSubmitterId}" target="_blank" rel="noopener" ` +
`style="color:#f88;text-decoration:none;">shared by ${escapeHtml(safeSubmitterId)}</a>`
);
}
if (savedAt != null && Number.isFinite(Number(savedAt))) {
const ms = Number(savedAt) > 1e12 ? Number(savedAt) : Number(savedAt) * 1000;
metaParts.push(`<span style="color:#aaa;">saved ${escapeHtml(relativeTime(Date.now() - ms))}</span>`);
}
const metaHtml = metaParts.length
? `<div style="display:flex;flex-direction:column;gap:4px;font-size:11px;font-weight:600;margin-bottom:4px;">${metaParts.join("<br>")}</div>`
: "";
body.innerHTML = `
${metaHtml}
<div style="display:flex;flex-direction:column;gap:8px;">
<div style="font-weight:900;color:#7bcf9a;letter-spacing:0.2px;text-transform:uppercase;font-size:11px;">Weapons</div>
${weaponsHtml || `<div style="color:rgba(255,255,255,0.7);font-size:12px;">(none)</div>`}
</div>
<div style="display:flex;flex-direction:column;gap:8px;">
<div style="font-weight:900;color:#7bcf9a;letter-spacing:0.2px;text-transform:uppercase;font-size:11px;">Armor / Model</div>
${armorHtml || `<div style="color:rgba(255,255,255,0.7);font-size:12px;">(none)</div>`}
</div>`;
if (collapsed) {
panel.dataset.collapsed = "1";
body.style.display = "none";
const btnRight = W.document.querySelector(`#${PANEL_BTN_ID} div:last-child`);
if (btnRight) btnRight.textContent = "▸";
} else {
panel.dataset.collapsed = "0";
body.style.display = "flex";
const btnRight = W.document.querySelector(`#${PANEL_BTN_ID} div:last-child`);
if (btnRight) btnRight.textContent = "▾";
}
}
function getUrlTargetId() {
try {
const u = new URL(W.location.href);
const v = u.searchParams.get("user2ID");
if (v && /^\d+$/.test(v)) return v;
} catch {}
const m = String(W.location?.href || "").match(/(?:^|[?&])user2ID=(\d+)/);
return m ? m[1] : null;
}
function getCacheKey(playerId, targetId) {
return `${playerId}_${targetId}`;
}
function validateLoadout(loadout) {
if (!loadout || typeof loadout !== "object") return false;
const validSlots = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9]);
for (const [k, item] of Object.entries(loadout)) {
const slot = Number(k);
if (!validSlots.has(slot) || !item || typeof item !== "object") return false;
const id = item.item_id ?? item.ID ?? item.id;
if (id == null || !Number.isInteger(Number(id)) || Number(id) < 1 || Number(id) > 999999) return false;
const d = Number(item.damage ?? item.Damage ?? 0);
const a = Number(item.accuracy ?? item.Accuracy ?? 0);
if (!Number.isFinite(d) || d < 0 || d > 1e5) return false;
if (!Number.isFinite(a) || a < 0 || a > 1e5) return false;
if (item.mods != null && !Array.isArray(item.mods)) return false;
if (item.bonuses != null && !Array.isArray(item.bonuses)) return false;
}
return Object.keys(loadout).length > 0;
}
function normalizeLoadout(loadout) {
if (!loadout || typeof loadout !== "object") return loadout;
// Strip slot 10 early so older Firebase payloads don't trip validation/rendering.
if (Object.prototype.hasOwnProperty.call(loadout, 10)) delete loadout[10];
for (const item of Object.values(loadout)) {
if (!item || typeof item !== "object") continue;
if (typeof item.mods === "string") {
try { item.mods = JSON.parse(item.mods); } catch { item.mods = []; }
}
const b = item.bonuses ?? item.Bonuses ?? item.bonus;
item.bonuses = parseBonuses(b);
if (!Array.isArray(item.mods)) item.mods = [];
}
return loadout;
}
function firebaseRequest(method, path, body) {
const base = CONFIG.firebaseUrl;
if (!base) { log("firebaseRequest: no base URL"); return Promise.resolve(null); }
return new Promise((resolve) => {
const url = `${base}/${path}.json`;
const headers = {};
let bodyStr = null;
if (body != null && method !== "GET") {
bodyStr = JSON.stringify(body);
headers["Content-Type"] = "application/json";
}
let settled = false;
const timer = W.setTimeout(() => {
if (settled) return;
settled = true;
reportError("firebase_request", "Timeout", { method, path });
resolve(null);
}, FIREBASE_FETCH_TIMEOUT_MS);
const done = (v) => {
if (settled) return;
settled = true;
W.clearTimeout(timer);
resolve(v);
};
const parseResponse = (status, text) => {
if (status >= 200 && status < 300) {
try { done(text ? JSON.parse(text) : null); } catch { done(null); }
} else {
log("firebaseRequest: status", status, path);
reportError("firebase_request", `HTTP ${status}`, { method, path, status });
done(null);
}
};
const runFetch = () => {
if (typeof W.fetch !== "function") {
log("firebaseRequest: no transport available");
done(null);
return;
}
W.fetch(url, { method, headers, ...(bodyStr != null ? { body: bodyStr } : {}) })
.then(async (r) => parseResponse(r.status, await r.text()))
.catch((e) => {
log("firebaseRequest(fetch): error", path, e);
reportError("firebase_request", "Network error", { method, path, errType: e?.type ?? "unknown" });
done(null);
});
};
try {
if (IS_PDA) {
waitForPdaBridge(800).then(async (bridge) => {
if (bridge?.callHandler) {
try {
const handler = method === "GET" ? "PDA_httpGet" : "PDA_httpPost";
const r = method === "GET"
? await bridge.callHandler(handler, url, headers)
: await bridge.callHandler(handler, url, headers, bodyStr || "");
return parseResponse(Number(r?.status ?? 0), String(r?.responseText ?? ""));
} catch {}
}
runFetch();
});
return;
}
if (typeof GM_xmlhttpRequest === "function") {
GM_xmlhttpRequest({
method,
url,
timeout: FIREBASE_FETCH_TIMEOUT_MS,
anonymous: true,
headers,
data: bodyStr,
onload: (r) => parseResponse(r.status, r.responseText ?? ""),
onerror: (e) => {
log("firebaseRequest: onerror", path, e);
reportError("firebase_request", "Network error", { method, path, errType: e?.type ?? "unknown" });
done(null);
},
});
return;
}
runFetch();
} catch (e) {
log("firebaseRequest: exception", path, e);
reportError("firebase_request", e, { method, path });
done(null);
}
});
}
function reportError(ctx, error, extra = {}) {
try {
const errMsg = typeof error === "string" ? error : (error?.message ?? String(error));
const payload = {
ctx,
err: errMsg,
ts: Date.now(),
path: (W.location?.pathname ?? "") + (W.location?.search ?? ""),
user2ID: getUrlTargetId(),
pid: STATE.attackData?.attackerUser?.userID ?? extra.playerId ?? null,
tid: STATE.attackData?.defenderUser?.userID ?? extra.targetId ?? null,
};
if (extra.method) payload.req = `${extra.method} ${extra.path}`;
if (extra.status) payload.status = extra.status;
if (extra.errType) payload.errType = extra.errType;
if (extra.hasRaw != null) payload.hasRaw = !!extra.hasRaw;
if (extra.hasLoadoutJson != null) payload.hasLoadoutJson = !!extra.hasLoadoutJson;
if (extra.loadoutKeys) payload.loadoutKeys = extra.loadoutKeys;
if (extra.retryCount != null) payload.retryCount = Number(extra.retryCount) || 0;
if (extra.retryDelayMs != null) payload.retryDelayMs = Number(extra.retryDelayMs) || 0;
const stack = error?.stack;
if (stack) payload.stack = String(stack).slice(0, 400);
firebaseRequest("POST", "errors", payload);
} catch (_) { /* avoid recursive failure */ }
}
async function putToFirebase(playerId, targetId, raw) {
const key = getCacheKey(playerId, targetId);
const payload = { raw, timestamp: Date.now(), playerId, targetId };
const r1 = await firebaseRequest("PUT", `loadouts/${key}`, payload);
const r2 = await firebaseRequest("PUT", `loadouts/by_target/${targetId}`, payload);
const ok = r1 != null && r2 != null;
if (!ok) {
reportError("upload", "Firebase PUT returned null", { playerId, targetId });
}
return ok;
}
function getLoadoutItem(loadout, slot) {
if (!loadout) return null;
return loadout[slot] ?? loadout[String(slot)] ?? null;
}
function parseLoadoutSlots(loadoutNode) {
if (loadoutNode == null) return {};
if (Array.isArray(loadoutNode)) {
const slots = {};
loadoutNode.forEach((v, idx) => {
if (v == null) return;
const key = String(idx);
if (typeof v === "string") {
try { slots[key] = JSON.parse(v); } catch { /* skip */ }
} else if (typeof v === "object" && !v["#"]) {
slots[key] = v;
}
});
return slots;
}
if (typeof loadoutNode !== "object") return {};
const slots = {};
for (const [k, v] of Object.entries(loadoutNode)) {
if (k === "_") continue;
if (typeof v === "string") {
try { slots[k] = JSON.parse(v); } catch { /* skip */ }
} else if (v && typeof v === "object" && !v["#"]) {
slots[k] = v;
}
}
return slots;
}
function mergeLoadoutSources(rawLoadout, jsonLoadout) {
if (!rawLoadout) return jsonLoadout || null;
if (!jsonLoadout) return rawLoadout;
const merged = { ...jsonLoadout, ...rawLoadout };
for (const slot of ARMOR_SLOTS) {
const jsonItem = getLoadoutItem(jsonLoadout, slot);
if (jsonItem?.item_id) merged[slot] = merged[String(slot)] = jsonItem;
}
for (const slot of WEAPON_SLOTS) {
const rawItem = getLoadoutItem(rawLoadout, slot);
if (rawItem?.item_id) merged[slot] = merged[String(slot)] = rawItem;
}
return merged;
}
function parseLoadoutJsonNode(lj) {
if (lj == null) return null;
let node = lj;
if (typeof lj === "string") {
try { node = JSON.parse(lj); } catch { return null; }
}
if (!node || typeof node !== "object" || node["#"]) return null;
const loadout = parseLoadoutSlots(node);
if (!loadout || Object.keys(loadout).length === 0) return null;
normalizeLoadout(loadout);
return validateLoadout(loadout) ? loadout : null;
}
function parsePayloadToLoadout(data) {
if (!data || typeof data !== "object") return null;
let fromRaw = null;
const raw = data.raw;
if (raw && typeof raw === "object") {
const defenderItems = raw.defenderItems ?? raw.attackData?.defenderItems;
const parsed = parseDefenderItemsToLoadout(defenderItems);
if (parsed && validateLoadout(parsed)) {
normalizeLoadout(parsed);
fromRaw = parsed;
}
}
const fromJson = parseLoadoutJsonNode(data.loadoutJson);
if (fromRaw && fromJson) {
const merged = mergeLoadoutSources(fromRaw, fromJson);
return merged && validateLoadout(merged) ? merged : fromJson || fromRaw;
}
return fromJson || fromRaw;
}
async function getFromFirebase(playerId, targetId) {
const key = getCacheKey(playerId, targetId);
log("getFromFirebase", key);
const data = await firebaseRequest("GET", `loadouts/${key}`);
const loadout = parsePayloadToLoadout(data);
log("getFromFirebase result", !!data, !!loadout);
if (loadout) return { loadout, submittedBy: null, savedAt: data?.timestamp ?? null };
if (data && typeof data === "object" && (data.raw || data.loadoutJson)) {
reportError("parse", "parsePayloadToLoadout returned null", { playerId, targetId, hasRaw: !!data.raw, hasLoadoutJson: !!data.loadoutJson });
}
return null;
}
async function getFromFirebaseByTarget(targetId) {
log("getFromFirebaseByTarget", targetId);
const data = await firebaseRequest("GET", `loadouts/by_target/${targetId}`);
const loadout = parsePayloadToLoadout(data);
log("getFromFirebaseByTarget result", !!data, !!loadout);
if (loadout) {
const id = data?.playerId;
const submittedBy = id != null && Number.isInteger(Number(id)) && Number(id) > 0 ? String(id) : null;
return { loadout, submittedBy, savedAt: data?.timestamp ?? null };
}
if (data && typeof data === "object" && (data.raw || data.loadoutJson)) {
reportError("parse_by_target", "parsePayloadToLoadout returned null", { targetId, hasRaw: !!data.raw, hasLoadoutJson: !!data.loadoutJson });
}
return null;
}
function parseJson(text) {
try { return JSON.parse(text); } catch { return null; }
}
function waitForPdaBridge(timeoutMs = 4000) {
return new Promise((resolve) => {
const start = Date.now();
const tick = () => {
try {
if (W.flutter_inappwebview?.callHandler) return resolve(W.flutter_inappwebview);
} catch {}
if (Date.now() - start > timeoutMs) return resolve(null);
W.setTimeout(tick, 200);
};
tick();
});
}
function relativeTime(ms) {
const fmt = (n, u) => `${n} ${u}${n > 1 ? "s" : ""} ago`;
const mins = Math.floor(ms / 60000);
if (mins < 1) return "just now";
const hrs = Math.floor(ms / 3600000);
if (hrs < 1) return fmt(mins, "minute");
const days = Math.floor(ms / 86400000);
if (days < 1) return fmt(hrs, "hour");
const wks = Math.floor(days / 7);
if (wks < 1) return fmt(days, "day");
return fmt(Math.floor(days / 30), "month");
}
function getSpareMagCount(mods) {
if (!Array.isArray(mods)) return 2;
if (mods.some((m) => m?.name === "Extra Magazines x2")) return 4;
if (mods.some((m) => m?.name === "Extra Magazine")) return 3;
return 2;
}
function buildAmmoInner(item, slot) {
if (slot === 3) return INFINITY_SVG;
if (slot === 5) return `<span class="markerText___HdlDL standard___bW8M5">1</span>`;
const clipSize = item?.clip_size;
if (clipSize == null || clipSize === "") {
return `<span class="markerText___HdlDL">—</span>`;
}
const spareMags = getSpareMagCount(item?.mods);
const ammoType = String(item?.ammo_type || "").toLowerCase().replace(/\s+/g, "-");
const color = ammoType ? `var(--attack-ammo-color-${ammoType}, #ddd)` : "#ddd";
return `<span class="markerText___HdlDL" style="color:${color}">${clipSize}/${clipSize} (${spareMags})</span>`;
}
function queryFirstInDocs(selectors) {
for (const doc of getDocAndIframes()) {
const n = queryFirst(doc, selectors);
if (n) return n;
}
return null;
}
function getWeaponArea(defenderArea) {
const area = defenderArea || getDefenderArea();
if (!IS_PDA) return area;
const wraps = [];
for (const doc of getDocAndIframes()) {
wraps.push(...doc.querySelectorAll("[class*='armoursWrap']"));
}
if (wraps.length <= 1) return area;
const defenderWrap = getArmoursWrap(area) || wraps[wraps.length - 1];
const wrapIdx = wraps.indexOf(defenderWrap);
const areas = [];
for (const doc of getDocAndIframes()) {
areas.push(...doc.querySelectorAll("[class*='playerArea']"));
}
if (wrapIdx >= 0 && areas[wrapIdx]) return areas[wrapIdx];
return areas[areas.length - 1] ?? area;
}
function whenVisible(fn) {
if (W.document.visibilityState === "visible") { fn(); return; }
const handler = () => {
if (W.document.visibilityState !== "visible") return;
W.document.removeEventListener("visibilitychange", handler);
fn();
};
W.document.addEventListener("visibilitychange", handler);
}
function escapeHtml(v) {
return String(v ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
}
function formatFixed2(v) {
const n = Number(v);
return Number.isFinite(n) ? n.toFixed(2) : "-";
}
function queryFirst(root, selectors) {
for (const s of selectors) {
try {
const n = root.querySelector(s);
if (n) return n;
} catch {}
}
return null;
}
function getItemFromSlot(slot) {
if (!slot) return null;
const item = slot?.item?.[0] ?? slot?.item;
if (!item) return null;
const id = item.ID ?? item.id ?? item.item_id;
if (id == null || id === "") return null;
return item;
}
function extractBonusesFromSlot(slot, item) {
const from = item?.currentBonuses ?? item?.bonuses ?? item?.Bonuses ?? item?.bonus ?? item?.attachments ?? item?.bonus_attachments ?? item?.item_bonuses
?? slot?.bonuses ?? slot?.Bonus ?? slot?.bonus ?? slot?.attachments;
if (Array.isArray(from) && from.length > 0) return from.slice(0, 2);
if (from && typeof from === "object" && !Array.isArray(from)) {
const arr = Object.values(from).filter(Boolean);
if (arr.length > 0) return arr.slice(0, 2);
}
if (typeof from === "string") {
try { const p = JSON.parse(from); return Array.isArray(p) ? p.slice(0, 2) : []; } catch { return []; }
}
return [];
}
function parseDefenderItemsToLoadout(defenderItems) {
if (!defenderItems || typeof defenderItems !== "object") return null;
const loadout = {};
for (const slotId of [...WEAPON_SLOTS, ...ARMOR_SLOTS]) {
const slot = defenderItems[slotId] ?? defenderItems[String(slotId)];
const item = getItemFromSlot(slot);
if (!item) continue;
const id = item.ID ?? item.id ?? item.item_id;
const idNum = Number(id);
if (id == null || id === "" || !Number.isInteger(idNum) || idNum < 1 || idNum > 999999) continue;
const name = item.name ?? item.item_name ?? item.Name ?? "";
const damage = Number(item.dmg ?? item.damage ?? item.Damage ?? 0) || 0;
const accuracy = Number(item.acc ?? item.accuracy ?? item.Accuracy ?? 0) || 0;
const glowRarity = (item.glowClass || "").replace("glow-", "");
const rarity = String(item.rarity || item.Rarity || glowRarity || "default").toLowerCase();
const rawMods = item.currentUpgrades ?? item.mods ?? item.Mods;
const modArr = Array.isArray(rawMods) ? rawMods : (rawMods && typeof rawMods === "object" ? Object.values(rawMods) : []);
const mods = modArr.map((m) => (m && typeof m === "object") ? { icon: m.icon ?? m.key, name: m.name ?? m.title ?? "", description: m.description ?? m.desc ?? "" } : m).filter(Boolean);
const rawBonuses = extractBonusesFromSlot(slot, item);
const bonuses = rawBonuses.map((b) => (typeof b === "object" && b !== null)
? { bonus_key: b.bonus_key ?? b.icon ?? b.key, name: b.name ?? b.title ?? b.description ?? b.desc ?? "" }
: { bonus_key: String(b), name: "" }).filter((b) => b.bonus_key || b.name);
loadout[slotId] = {
item_id: id,
item_name: name,
damage,
accuracy,
rarity,
mods: mods.slice(0, 2),
bonuses: bonuses.slice(0, 2),
clip_size: item.clipSize ?? item.clip_size ?? item.clip ?? undefined,
ammo_type: item.ammoType ?? item.ammo_type ?? item.ammo ?? "",
};
}
return Object.keys(loadout).length > 0 ? loadout : null;
}
function hasNativeDefenderLoadout(defenderItems) {
if (!defenderItems || typeof defenderItems !== "object") return false;
const parsed = parseDefenderItemsToLoadout(defenderItems);
return !!parsed && Object.keys(parsed).length > 0;
}
function getDocAndIframes() {
const docs = [W.document];
try {
for (const iframe of W.document.querySelectorAll("iframe")) {
try {
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (doc && doc !== W.document) docs.push(doc);
} catch {}
}
} catch {}
return docs;
}
const DEFENDER_MARKERS = ["#defender_Primary", "#defender_Secondary", "#defender_Melee", "#defender_Temporary"];
function getDefenderArea() {
for (const doc of getDocAndIframes()) {
const marker = queryFirst(doc, DEFENDER_MARKERS);
if (marker) {
const owner = marker.closest("[class*='playerArea'], [class*='player___']");
if (owner) {
dbg("getDefenderArea: via defender marker", marker.id);
return owner;
}
}
}
for (const doc of getDocAndIframes()) {
const areas = doc.querySelectorAll("[class*='playerArea']");
if (areas.length === 0) continue;
if (areas.length === 1) {
return areas[0];
}
// New Torn UI: both sides reuse ids like #weapon_main on the wrapper — do NOT use #weapon_main to pick attacker.
const withDefender = [...areas].find((a) =>
a.querySelector("[id^='defender_']")
);
if (withDefender) {
dbg("getDefenderArea: playerArea with defender_* ids");
return withDefender;
}
const withAttacker = [...areas].find((a) =>
a.querySelector("#attacker_Primary, #attacker_Secondary, #attacker_Melee, #attacker_Temporary")
);
const chosen = withAttacker ?? areas[1] ?? areas[0];
dbg("getDefenderArea: fallback two-area", !!withAttacker);
return chosen;
}
return null;
}
const INFINITY_SVG = `<span class="eternity___QmjtV"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="10" viewBox="0 0 17 10"><g><path d="M 12.3399 1.5 C 10.6799 1.5 9.64995 2.76 8.50995 3.95 C 7.35995 2.76 6.33995 1.5 4.66995 1.5 C 2.89995 1.51 1.47995 2.95 1.48995 4.72 C 1.48995 4.81 1.48995 4.91 1.49995 5 C 1.32995 6.76 2.62995 8.32 4.38995 8.49 C 4.47995 8.49 4.57995 8.5 4.66995 8.5 C 6.32995 8.5 7.35995 7.24 8.49995 6.05 C 9.64995 7.24 10.67 8.5 12.33 8.5 C 14.0999 8.49 15.5199 7.05 15.5099 5.28 C 15.5099 5.19 15.5099 5.09 15.4999 5 C 15.6699 3.24 14.3799 1.68 12.6199 1.51 C 12.5299 1.51 12.4299 1.5 12.3399 1.5 Z M 4.66995 7.33 C 3.52995 7.33 2.61995 6.4 2.61995 5.26 C 2.61995 5.17 2.61995 5.09 2.63995 5 C 2.48995 3.87 3.27995 2.84 4.40995 2.69 C 4.49995 2.68 4.57995 2.67 4.66995 2.67 C 6.01995 2.67 6.83995 3.87 7.79995 5 C 6.83995 6.14 6.01995 7.33 4.66995 7.33 Z M 12.3399 7.33 C 10.99 7.33 10.17 6.13 9.20995 5 C 10.17 3.86 10.99 2.67 12.3399 2.67 C 13.48 2.67 14.3899 3.61 14.3899 4.74 C 14.3899 4.83 14.3899 4.91 14.3699 5 C 14.5199 6.13 13.7299 7.16 12.5999 7.31 C 12.5099 7.32 12.4299 7.33 12.3399 7.33 Z" stroke-width="0"></path></g></svg></span>`;
function buildIconHtml(icon, title, desc, containerClass) {
const raw = icon || "blank-bonus-25";
const safeIcon = /^[a-zA-Z0-9_-]+$/.test(raw) ? raw : "blank-bonus-25";
const tooltip = escapeHtml([title, desc].filter(Boolean).join(" - "));
const container = containerClass || "container___LAqaj";
return `<div class="${container}" title="${tooltip}"><i class="bonus-attachment-${safeIcon}" title="${tooltip}"></i></div>`;
}
function pickWeaponClass(el, prefix, fallback) {
if (!el?.classList) return fallback;
for (const c of el.classList) {
if (c.startsWith(prefix)) return c;
}
return fallback;
}
function discoverWeaponSlotClasses(wrapper) {
const top = queryFirst(wrapper, ["[class*='top___']"]);
const propsEl = top?.querySelector("[class*='props___']");
const containerEl = top?.querySelector("[class*='container___']");
const topMarkerEl = top?.querySelector("[class*='topMarker___']");
const markerTextEl = topMarkerEl?.querySelector("span");
const bottom = queryFirst(wrapper, ["[class*='bottom___']"]);
return {
props: pickWeaponClass(propsEl, "props___", "props___oL_Cw"),
container: pickWeaponClass(containerEl, "container___", "container___LAqaj"),
topMarker: pickWeaponClass(topMarkerEl, "topMarker___", "topMarker___OjRyU"),
markerText: pickWeaponClass(markerTextEl, "markerText___", "markerText___HdlDL"),
bonusInfo: pickWeaponClass(bottom?.querySelector("[class*='bonusInfo___']"), "bonusInfo___", "bonusInfo___vyqlT"),
bottomMarker: pickWeaponClass(bottom?.querySelector("[class*='bottomMarker___']"), "bottomMarker___", "bottomMarker___G1uDs"),
};
}
function getBonusIconKey(bonus) {
if (!bonus) return null;
const raw = bonus.bonus_key ?? bonus.icon ?? "";
return (BONUS_KEY_FIX[raw] ?? raw) || null;
}
function buildSlotIcons(arr, key, name, desc, isBonus, containerClass) {
return [0, 1]
.map((i) => {
if (!arr?.[i]) return buildIconHtml(null, "", "", containerClass);
const iconKey = isBonus ? getBonusIconKey(arr[i]) : (arr[i][key] ?? arr[i].icon);
return buildIconHtml(iconKey, arr[i][name] ?? arr[i].description, arr[i][desc] ?? arr[i].description, containerClass);
})
.join("");
}
function getWeaponWrapper(marker) {
if (!marker) return null;
const cls = marker.className?.toString() || "";
if (/weaponWrapper|weaponSlot/.test(cls)) return marker;
return marker.closest("[class*='weaponWrapper'], [class*='weaponSlot']");
}
function stripPrefightEmptyStyles(wrapper) {
if (!wrapper) return;
wrapper.className = (wrapper.className?.toString() || "")
.split(/\s+/)
.filter((c) => c && !/^emptySlot/.test(c))
.join(" ");
}
function stripBlankImgClasses(img) {
if (!img) return;
[...img.classList].forEach((c) => {
if (/^blank___/.test(c)) img.classList.remove(c);
});
}
function renderSlot(wrapper, item, slotLabel, includeLabel, slot) {
if (!wrapper || !item) return;
stripPrefightEmptyStyles(wrapper);
const rarityGlow = { yellow: "glow-yellow", orange: "glow-orange", red: "glow-red" };
const glow = rarityGlow[item.rarity] || "glow-default";
wrapper.className = wrapper.className.split(/\s+/).filter((c) => c && !/^glow-/.test(c)).join(" ");
wrapper.classList.add(glow);
const border = queryFirst(wrapper, ["[class*='itemBorder']"]);
if (border) border.className = `itemBorder___mJGqQ ${glow}-border`;
const img = queryFirst(wrapper, ["[class*='weaponImage'] img", "img"]);
if (img && item.item_id) {
const base = `${CONFIG.imagesBase}/items/${item.item_id}/large`;
const itemPath = `/items/${item.item_id}/`;
const alreadyCorrect = img.src && String(img.src).includes(itemPath);
if (!alreadyCorrect) {
img.src = `${base}.png`;
img.srcset = `${base}.png 1x, ${base}@2x.png 2x, ${base}@3x.png 3x, ${base}@4x.png 4x`;
}
img.alt = item.item_name || "";
stripBlankImgClasses(img);
img.style.objectFit = "contain";
}
const wc = discoverWeaponSlotClasses(wrapper);
const top = queryFirst(wrapper, ["[class*='top___']"]);
if (top) {
const modIcons = buildSlotIcons(item.mods || [], "icon", "name", "description", false, wc.container);
const bonusIcons = buildSlotIcons(item.bonuses || [], "bonus_key", "name", "description", true, wc.container);
top.style.cssText = TOP_ROW_STYLE;
top.innerHTML = includeLabel
? `<div class="${wc.props}" style="${PROPS_ROW_STYLE}">${modIcons}</div><div class="${wc.topMarker}"><span class="${wc.markerText}">${escapeHtml(slotLabel)}</span></div><div class="${wc.props}" style="${PROPS_ROW_STYLE}">${bonusIcons}</div>`
: `<div class="${wc.props}" style="${PROPS_ROW_STYLE}">${modIcons}</div><div class="${wc.props}" style="${PROPS_ROW_STYLE}">${bonusIcons}</div>`;
}
const bottom = queryFirst(wrapper, ["[class*='bottom___']"]);
if (bottom) {
bottom.style.cssText = TOP_ROW_STYLE;
bottom.innerHTML = `<div class="${wc.props}" style="${PROPS_ROW_STYLE}"><i class="bonus-attachment-item-damage-bonus" aria-label="Damage"></i><span class="${wc.bonusInfo}">${formatFixed2(item.damage)}</span></div><div class="${wc.bottomMarker}">${buildAmmoInner(item, slot)}</div><div class="${wc.props}" style="${PROPS_ROW_STYLE}"><i class="bonus-attachment-item-accuracy-bonus" aria-label="Accuracy"></i><span class="${wc.bonusInfo}">${formatFixed2(item.accuracy)}</span></div>`;
}
const xp = wrapper.querySelector(".tt-weapon-experience");
if (xp) {
xp.textContent = "";
xp.style.display = "none";
}
wrapper.setAttribute("aria-label", item.item_name || "Unknown");
}
function clearSlot(wrapper, slotLabel, includeLabel, slot) {
if (!wrapper) return;
wrapper.className = wrapper.className.split(/\s+/).filter((c) => c && !/^glow-/.test(c)).join(" ");
wrapper.classList.add("glow-default");
const border = queryFirst(wrapper, ["[class*='itemBorder']"]);
if (border) border.className = "itemBorder___mJGqQ glow-default-border";
const img = queryFirst(wrapper, ["[class*='weaponImage'] img", "img"]);
if (img) {
const silhouette = SILHOUETTES[slot];
if (silhouette) {
img.src = `${TORN_BASE}/images/items/silhouettes/${silhouette}.svg`;
img.removeAttribute("srcset");
img.alt = "";
img.classList.add("blank___RpGQA");
img.style.objectFit = "";
} else {
img.removeAttribute("src");
img.removeAttribute("srcset");
img.alt = "";
img.classList.add("blank___RpGQA");
}
}
const wc = discoverWeaponSlotClasses(wrapper);
const top = queryFirst(wrapper, ["[class*='top___']"]);
if (top) {
top.style.cssText = TOP_ROW_STYLE;
top.innerHTML = includeLabel
? `<div class="${wc.props}" style="${PROPS_ROW_STYLE}"></div><div class="${wc.topMarker}"><span class="${wc.markerText}">${escapeHtml(slotLabel)}</span></div><div class="${wc.props}" style="${PROPS_ROW_STYLE}"></div>`
: `<div class="${wc.props}" style="${PROPS_ROW_STYLE}"></div><div class="${wc.props}" style="${PROPS_ROW_STYLE}"></div>`;
}
const bottom = queryFirst(wrapper, ["[class*='bottom___']"]);
if (bottom) {
bottom.style.cssText = TOP_ROW_STYLE;
const ammoInner = slot === 3 ? INFINITY_SVG : slot === 5 ? `<span class="${wc.markerText} standard___bW8M5">0</span>` : `<span class="${wc.markerText}">—</span>`;
bottom.innerHTML = `<div class="${wc.props}" style="${PROPS_ROW_STYLE}"><i class="bonus-attachment-item-damage-bonus" aria-label="Damage"></i><span class="${wc.bonusInfo}">—</span></div><div class="${wc.bottomMarker}">${ammoInner}</div><div class="${wc.props}" style="${PROPS_ROW_STYLE}"><i class="bonus-attachment-item-accuracy-bonus" aria-label="Accuracy"></i><span class="${wc.bonusInfo}">—</span></div>`;
}
const xp = wrapper.querySelector(".tt-weapon-experience");
if (xp) {
xp.textContent = "";
xp.style.display = "none";
}
wrapper.setAttribute("aria-label", slotLabel || "Empty");
}
function pickArmorClass(doc, prefix, exclude) {
for (const el of doc.querySelectorAll("[class]")) {
for (const c of el.classList) {
if (!c.startsWith(prefix)) continue;
if (exclude && exclude.test(c)) continue;
return c;
}
}
return "";
}
function discoverArmorDomClasses() {
for (const doc of getDocAndIframes()) {
const sample = doc.querySelector("[class*='armourContainer']");
if (sample) {
const armour = sample.querySelector("[class*='armour___']");
const img = sample.querySelector("img[class*='itemImg']");
const mask = sample.querySelector("[class*='mask___']");
return {
container: String(sample.className || ""),
armour: String(armour?.className || ""),
img: String(img?.className || ""),
mask: String(mask?.className || ""),
};
}
const container = pickArmorClass(doc, "armourContainer___");
const armour = pickArmorClass(doc, "armour___", /Wrap|Mask/i);
const img = pickArmorClass(doc, "itemImg___");
const mask = pickArmorClass(doc, "mask___");
if (container && armour && img) {
return { container, armour, img, mask };
}
}
return { ...FALLBACK_ARMOR_DOM };
}
function getArmoursWrap(defenderArea) {
if (defenderArea) {
const inArea = queryFirst(defenderArea, ["[class*='armoursWrap']"]);
if (inArea) return inArea;
const scope = defenderArea.closest("[class*='playerWindow'], [class*='player___']");
if (scope) {
const inScope = queryFirst(scope, ["[class*='armoursWrap']"]);
if (inScope) return inScope;
}
}
for (const doc of getDocAndIframes()) {
const wraps = [...doc.querySelectorAll("[class*='armoursWrap']")];
if (wraps.length === 0) continue;
if (wraps.length === 1) return wraps[0];
if (defenderArea) {
for (const w of wraps) {
if (defenderArea.compareDocumentPosition(w) & Node.DOCUMENT_POSITION_FOLLOWING) {
return w;
}
}
}
return wraps[wraps.length - 1];
}
return null;
}
function setModelImgSrc(img, basePath) {
img.alt = "";
img.src = `${basePath}.webp`;
img.srcset = `${basePath}.webp 1x, ${basePath}@2x.webp 2x, ${basePath}@3x.webp 3x, ${basePath}@4x.webp 4x`;
}
function setModelMaskImgSrc(img, basePath) {
img.alt = "";
img.src = `${basePath}.webp`;
img.removeAttribute("srcset");
}
function getArmorMaskBase(itemId, slot) {
if (Number(slot) === 8) return `${TORN_BASE}/images/v2/user_model/masks/head-03m-mask-d`;
return `${TORN_BASE}/images/v2/user_model/masks/${itemId}m-mask-d`;
}
function shouldRenderArmorMask(slot) {
return ARMOR_MASK_SLOTS.has(Number(slot));
}
function getArmorSlotsKey(loadout) {
if (!loadout) return "";
return ARMOR_SLOTS.map((slot) => {
const id = getLoadoutItem(loadout, slot)?.item_id;
return id ? `${slot}:${id}` : "";
}).filter(Boolean).join(",");
}
function countArmorApplied(armoursWrap) {
return armoursWrap?.querySelectorAll("[data-loadout-slot]").length ?? 0;
}
function isArmorDomCurrent(armoursWrap, loadout) {
const key = getArmorSlotsKey(loadout);
if (!armoursWrap || !key) return false;
if (armoursWrap.dataset.loadoutArmorKey !== key) return false;
let need = 0;
for (const slot of ARMOR_SLOTS) {
const itemId = getLoadoutItem(loadout, slot)?.item_id;
if (!itemId) continue;
need++;
const el = armoursWrap.querySelector(`[data-loadout-slot="${slot}"]`);
if (!el || String(el.dataset.loadoutItemId) !== String(itemId)) return false;
}
return need > 0 && armoursWrap.querySelectorAll("[data-loadout-slot]").length >= need;
}
function allowDefenderModelInteraction(defenderArea) {
const playerWindow = defenderArea?.closest("[class*='playerWindow']");
if (!playerWindow) return;
for (const modal of playerWindow.querySelectorAll("[class*='modal']")) {
modal.style.pointerEvents = "none";
}
for (const el of playerWindow.querySelectorAll("[class*='effectsWrap'], [class*='iconsContainer']")) {
el.style.pointerEvents = "none";
}
}
function setArmorWrapPointerPassthrough(armoursWrap) {
if (armoursWrap?.dataset.loadoutArmorKey) {
armoursWrap.style.pointerEvents = "none";
}
}
function pointInPoly(x, y, coordsStr) {
const coords = coordsStr.split(",").map(Number);
let inside = false;
for (let i = 0, j = coords.length - 2; i < coords.length; j = i, i += 2) {
const xi = coords[i];
const yi = coords[i + 1];
const xj = coords[j];
const yj = coords[j + 1];
if (((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi)) {
inside = !inside;
}
}
return inside;
}
function getArmorNameAtPoint(x, y, loadout) {
for (const slot of [4, 6, 7, 8, 9]) {
const item = getLoadoutItem(loadout, slot);
if (!item || !ARMOR_SLOT_AREAS[slot]) continue;
for (const { coords } of ARMOR_SLOT_AREAS[slot]) {
if (pointInPoly(x, y, coords)) return item.item_name || "";
}
}
return "";
}
function getArmorHoverTip(doc) {
let tip = doc.getElementById("loadout-armor-tip");
if (!tip) {
tip = doc.createElement("div");
tip.id = "loadout-armor-tip";
tip.style.cssText = "position:fixed;z-index:999999;display:none;pointer-events:none;padding:4px 8px;font-size:11px;font-weight:600;color:#ddd;background:rgba(0,0,0,0.85);border-radius:4px;white-space:nowrap;box-shadow:0 2px 8px rgba(0,0,0,0.35);";
doc.body.appendChild(tip);
}
return tip;
}
function bindArmorCursorTooltip(defenderArea, bodyImg, loadout) {
if (!bodyImg || !loadout) return;
const scope = defenderArea?.closest("[class*='playerWindow']") ?? defenderArea ?? bodyImg;
scope._loadoutHoverAbort?.abort();
const ac = new AbortController();
scope._loadoutHoverAbort = ac;
const doc = scope.ownerDocument || W.document;
const tip = getArmorHoverTip(doc);
const onMove = (e) => {
const rect = bodyImg.getBoundingClientRect();
if (rect.width < 1 || rect.height < 1) {
tip.style.display = "none";
return;
}
if (e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom) {
tip.style.display = "none";
return;
}
const x = ((e.clientX - rect.left) / rect.width) * 240;
const y = ((e.clientY - rect.top) / rect.height) * 384;
const name = getArmorNameAtPoint(x, y, loadout);
if (!name) {
tip.style.display = "none";
return;
}
tip.textContent = name;
tip.style.left = `${Math.min(e.clientX + 12, W.innerWidth - 160)}px`;
tip.style.top = `${Math.min(e.clientY + 12, W.innerHeight - 30)}px`;
tip.style.display = "block";
};
const onLeave = () => {
tip.style.display = "none";
};
scope.addEventListener("mousemove", onMove, { signal: ac.signal, capture: true });
scope.addEventListener("mouseleave", onLeave, { signal: ac.signal, capture: true });
}
function renderArmor(defenderArea, loadout) {
if (!loadout) return 0;
const armoursWrap = getArmoursWrap(defenderArea);
if (!armoursWrap) return 0;
const classes = discoverArmorDomClasses();
if (!classes?.container || !classes.armour || !classes.img) return 0;
if (isArmorDomCurrent(armoursWrap, loadout)) {
setArmorWrapPointerPassthrough(armoursWrap);
allowDefenderModelInteraction(defenderArea);
renderArmorHoverMap(defenderArea, loadout);
return countArmorApplied(armoursWrap);
}
const modelRoot = armoursWrap.closest("[class*='modelLayers'], [class*='playerWindow']") || armoursWrap.parentElement;
const oldOverlay = modelRoot?.querySelector(".loadout-armor-overlay") ?? defenderArea?.querySelector(".loadout-armor-overlay");
if (oldOverlay) oldOverlay.remove();
const modelDoc = armoursWrap.ownerDocument || W.document;
armoursWrap.replaceChildren();
let applied = 0;
for (const slot of ARMOR_SLOTS) {
const item = getLoadoutItem(loadout, slot);
const itemId = item?.item_id;
if (!itemId) continue;
const container = modelDoc.createElement("div");
container.className = classes.container;
container.style.zIndex = String(ARMOR_Z_INDEX[slot] ?? 14);
container.dataset.loadoutSlot = String(slot);
container.dataset.loadoutItemId = String(itemId);
const itemName = item.item_name || "";
container.title = itemName;
container.dataset.loadoutItemName = itemName;
if (shouldRenderArmorMask(slot) && classes.mask) {
const maskWrap = modelDoc.createElement("div");
maskWrap.className = classes.mask;
const maskImg = modelDoc.createElement("img");
maskImg.className = classes.img;
maskImg.dataset.loadoutMask = "1";
setModelMaskImgSrc(maskImg, getArmorMaskBase(itemId, slot));
maskImg.addEventListener("error", () => maskWrap.remove(), { once: true });
maskWrap.appendChild(maskImg);
container.appendChild(maskWrap);
}
const armour = modelDoc.createElement("div");
armour.className = classes.armour;
armour.title = itemName;
const itemImg = modelDoc.createElement("img");
itemImg.className = classes.img;
itemImg.alt = itemName;
itemImg.title = itemName;
setModelImgSrc(itemImg, `${TORN_BASE}/images/v2/user_model/items/${itemId}m`);
armour.appendChild(itemImg);
container.appendChild(armour);
armoursWrap.appendChild(container);
applied++;
}
if (applied > 0) {
armoursWrap.dataset.loadoutArmorKey = getArmorSlotsKey(loadout);
setArmorWrapPointerPassthrough(armoursWrap);
}
allowDefenderModelInteraction(defenderArea);
renderArmorHoverMap(defenderArea, loadout);
return applied;
}
function removeArmorHoverMap(defenderArea) {
for (const doc of getDocAndIframes()) {
doc.querySelector(`map[name="${ARMOR_MAP_NAME}"]`)?.remove();
doc.querySelectorAll(".loadout-armor-overlay").forEach((el) => el.remove());
for (const img of doc.querySelectorAll(`img[usemap="#${ARMOR_MAP_NAME}"]`)) {
img.removeAttribute("usemap");
}
doc.getElementById("loadout-armor-tip")?.remove();
for (const host of doc.querySelectorAll("[class*='playerWindow'], [class*='allLayers']")) {
host._loadoutHoverAbort?.abort();
delete host._loadoutHoverAbort;
}
}
}
function getBodyImageForHover(defenderArea) {
const scope = defenderArea?.closest("[class*='playerWindow'], [class*='player___']") ?? defenderArea;
const inScope = queryFirst(scope, ["[class*='bodyImage'] img", "img[src*='user_model/body']"]);
if (inScope) return inScope;
const wraps = scope ? [...scope.querySelectorAll("[class*='armoursWrap']")] : [];
if (wraps.length) {
const modelRoot = wraps[0].closest("[class*='modelLayers']");
const inModel = queryFirst(modelRoot, ["[class*='bodyImage'] img", "img[src*='user_model/body']"]);
if (inModel) return inModel;
}
return queryFirst(defenderArea, ["[class*='bodyImage'] img", "img[src*='user_model/body']"])
?? queryFirstInDocs(["[class*='bodyImage'] img", "img[src*='user_model/body']"]);
}
function renderArmorHoverMap(defenderArea, loadout) {
if (!loadout) return;
removeArmorHoverMap(defenderArea);
const bodyImg = getBodyImageForHover(defenderArea);
if (!bodyImg) return;
bodyImg.removeAttribute("usemap");
bindArmorCursorTooltip(defenderArea, bodyImg, loadout);
}
function buildSlotMappings(defenderArea) {
if (defenderArea.querySelector("#defender_Primary")) {
return {
preFight: false,
includeLabel: true,
mappings: [
{ selector: "#defender_Primary", slot: 1, label: "Primary" },
{ selector: "#defender_Secondary", slot: 2, label: "Secondary" },
{ selector: "#defender_Melee", slot: 3, label: "Melee" },
{ selector: "#defender_Temporary", slot: 5, label: "Temporary" },
],
};
}
const tempSelector = defenderArea.querySelector("#defender_Temporary") ? "#defender_Temporary" : "#weapon_temp";
return {
preFight: true,
includeLabel: false,
mappings: [
{ selector: "#weapon_main", slot: 1, label: "Primary" },
{ selector: "#weapon_second", slot: 2, label: "Secondary" },
{ selector: "#weapon_melee", slot: 3, label: "Melee" },
{ selector: tempSelector, slot: 5, label: "Temporary" },
],
};
}
function applyLoadoutWeapons(defenderArea, loadout, config) {
const weaponArea = getWeaponArea(defenderArea);
let applied = 0;
let expected = 0;
for (const { selector, slot, label } of config.mappings) {
expected++;
const marker = queryFirst(weaponArea, [selector]) || queryFirstInDocs([selector]);
const wrapper = getWeaponWrapper(marker);
if (!wrapper) continue;
const item = getLoadoutItem(loadout, slot);
if (item) {
renderSlot(wrapper, item, label, config.includeLabel, slot);
} else {
clearSlot(wrapper, label, config.includeLabel, slot);
}
applied++;
}
renderArmor(defenderArea, loadout);
return { applied, expected };
}
function stopApplyTimer() {
if (STATE.applyTimerId) {
W.clearInterval(STATE.applyTimerId);
STATE.applyTimerId = null;
}
}
function renderLoadoutMeta(submittedBy, savedAt) {
if (!IS_PDA) {
W.document.querySelector(".loadout-shared-meta")?.remove();
return;
}
let meta = W.document.querySelector(".loadout-shared-meta");
if (!meta) {
meta = W.document.createElement("div");
meta.className = "loadout-shared-meta";
W.document.body.appendChild(meta);
}
meta.style.cssText =
"position:fixed;top:52px;left:12px;z-index:9999;display:flex;flex-direction:column;gap:4px;" +
"font-size:11px;font-weight:600;color:#ddd;background:rgba(0,0,0,0.85);" +
"padding:6px 10px;border-radius:6px;box-shadow:0 2px 8px rgba(0,0,0,0.3);";
const parts = [];
const safeSubmitterId = submittedBy && /^\d{1,8}$/.test(String(submittedBy)) ? String(submittedBy) : null;
if (safeSubmitterId) {
parts.push(
`<a href="${TORN_BASE}/profiles.php?XID=${safeSubmitterId}" target="_blank" rel="noopener" ` +
`style="color:#f44;text-decoration:none;">shared by ${escapeHtml(safeSubmitterId)}</a>`
);
}
if (savedAt != null && Number.isFinite(Number(savedAt))) {
const ms = Number(savedAt) > 1e12 ? Number(savedAt) : Number(savedAt) * 1000;
parts.push(`<span style="color:#aaa;">saved ${escapeHtml(relativeTime(Date.now() - ms))}</span>`);
}
if (!parts.length) {
meta.remove();
return;
}
meta.innerHTML = parts.join("<br>");
}
function renderLoadout(loadout, submittedBy, savedAt) {
if (!loadout || STATE.loadoutRendered || STATE.applyTimerId) return;
const startedAt = Date.now();
let panelRendered = false;
let badgeRendered = false;
let modalAdjusted = false;
STATE.applyTimerId = W.setInterval(() => {
if (STATE.loadoutRendered) {
stopApplyTimer();
return;
}
const defenderArea = getDefenderArea();
if (!defenderArea) return;
const config = buildSlotMappings(defenderArea);
const { applied, expected } = applyLoadoutWeapons(defenderArea, loadout, config);
if (!badgeRendered) {
if (IS_PDA) renderLoadoutMeta(submittedBy, savedAt);
else W.document.querySelector(".loadout-shared-meta")?.remove();
badgeRendered = true;
}
if (!panelRendered && !IS_PDA) {
renderDefenderPanel(loadout, submittedBy, savedAt);
panelRendered = true;
} else if (!panelRendered) {
panelRendered = true;
}
if (!modalAdjusted) {
const modal = queryFirst(defenderArea, ["[class*='modal']"]);
if (modal) {
modal.style.background = "transparent";
modal.style.backdropFilter = "none";
modal.style.webkitBackdropFilter = "none";
modalAdjusted = true;
}
}
const elapsed = Date.now() - startedAt;
const allApplied = expected > 0 && applied >= expected;
const inFightDom = !config.preFight;
const maxMs = config.preFight ? PREFIGHT_RENDER_MAX_MS : RENDER_MAX_MS;
const timedOut = elapsed >= maxMs;
const done = config.preFight ? inFightDom || timedOut : allApplied || inFightDom || timedOut;
if (done) {
dbg("renderLoadout: done", { applied, expected, preFight: config.preFight, inFightDom, timedOut });
STATE.loadoutRendered = true;
stopApplyTimer();
}
}, PREFIGHT_REAPPLY_MS);
}
function parseBonuses(b) {
if (Array.isArray(b) && b.length > 0) return b;
if (typeof b === "string") {
try { const arr = JSON.parse(b); return Array.isArray(arr) ? arr : []; } catch { return []; }
}
return [];
}
async function uploadLoadoutData(raw) {
try {
const playerId = raw?.attackerUser?.userID;
const targetId = raw?.defenderUser?.userID;
if (!playerId || !targetId) return false;
raw.timestamp = Math.floor(Date.now() / 1000);
return await putToFirebase(playerId, targetId, raw);
} catch (e) {
reportError("upload", e, { playerId: raw?.attackerUser?.userID, targetId: raw?.defenderUser?.userID });
return false;
}
}
function scheduleUploadRetry(raw) {
if (STATE.uploaded || STATE.uploadInProgress) return;
STATE.uploadInProgress = true;
whenVisible(async () => {
const ok = await uploadLoadoutData(raw);
if (ok) {
STATE.uploaded = true;
STATE.uploadInProgress = false;
STATE.uploadRetryCount = 0;
return;
}
const retryIndex = STATE.uploadRetryCount;
const delay = UPLOAD_RETRY_DELAYS_MS[Math.min(retryIndex, UPLOAD_RETRY_DELAYS_MS.length - 1)];
STATE.uploadInProgress = false;
STATE.uploadRetryCount += 1;
reportError("upload_retry", "Upload failed, scheduling retry", {
playerId: raw?.attackerUser?.userID,
targetId: raw?.defenderUser?.userID,
retryCount: STATE.uploadRetryCount,
retryDelayMs: delay,
});
W.setTimeout(() => {
if (!STATE.uploaded && !STATE.uploadInProgress) scheduleUploadRetry(raw);
}, delay);
});
}
async function fetchAndRenderLoadout() {
try {
const playerId = STATE.attackData?.attackerUser?.userID;
let targetId = STATE.attackData?.defenderUser?.userID ?? getUrlTargetId();
const urlTargetId = getUrlTargetId();
if (!targetId) return;
if (urlTargetId && String(targetId) !== urlTargetId) return;
let result = playerId ? await getFromFirebase(playerId, targetId) : null;
if (!result) result = await getFromFirebaseByTarget(targetId);
if (!result?.loadout) return;
normalizeLoadout(result.loadout);
if (!validateLoadout(result.loadout)) {
reportError("validate", "validateLoadout failed", { playerId, targetId, loadoutKeys: Object.keys(result.loadout || {}).slice(0, 10) });
return;
}
renderLoadout(result.loadout, result.submittedBy, result.savedAt);
} catch (e) {
reportError("fetch_render", e, { playerId: STATE.attackData?.attackerUser?.userID, targetId: STATE.attackData?.defenderUser?.userID });
}
}
function processResponse(data) {
if (!data || typeof data !== "object") return;
const db = data.DB || data;
if (!db || typeof db !== "object") return;
const attacker = db.attackerUser ?? data.attackerUser;
const defender = db.defenderUser ?? data.defenderUser;
if (!attacker?.userID && !defender?.userID && !db.defenderItems) return;
const isFirstData = !STATE.attackData;
STATE.attackData = db;
const hasNative = hasNativeDefenderLoadout(db.defenderItems);
if (hasNative && !STATE.uploaded && !STATE.uploadInProgress) {
scheduleUploadRetry(db);
}
if (hasNative) {
const nativeLoadout = parseDefenderItemsToLoadout(db.defenderItems);
dbg("processResponse: native", nativeLoadout ? "ok" : "null", "valid?", !!nativeLoadout && validateLoadout(nativeLoadout));
if (nativeLoadout && validateLoadout(nativeLoadout)) {
normalizeLoadout(nativeLoadout);
renderLoadout(nativeLoadout, null, null);
}
} else if (isFirstData) {
dbg("processResponse: fetchAndRenderLoadout");
fetchAndRenderLoadout();
}
}
if (typeof W.fetch === "function") {
const origFetch = W.fetch;
W.fetch = async function (...args) {
const req = args[0];
const url =
typeof req === "string"
? req
: req && typeof Request !== "undefined" && req instanceof Request
? req.url
: req?.url || "";
// Torn uses /loader.php?sid=attackData or /page.php?sid=attackData&mode=json (see builds/attack app).
if (!url.includes("sid=attackData")) return origFetch.apply(this, args);
const response = await origFetch.apply(this, args);
if (response.ok) {
try {
response.clone().text().then((text) => {
const parsed = parseJson(text);
if (parsed) processResponse(parsed);
});
} catch {}
}
return response;
};
}
function tryFetchByUrlTarget() {
if (!getUrlTargetId() || STATE.loadoutRendered || STATE.attackData?.defenderUser?.userID) return;
if (!getDefenderArea() && !getArmoursWrap(null)) return;
fetchAndRenderLoadout();
}
whenVisible(() => {
tryFetchByUrlTarget();
FALLBACK_DELAYS_MS.forEach((ms) => W.setTimeout(tryFetchByUrlTarget, ms));
});
})();