Torn PDA Loadout Share

PDA-only: replaces defender weapons/armor on Torn attack pages using Firebase loadouts.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         Torn PDA Loadout Share
// @namespace    loadout
// @version      1.0.1
// @description  PDA-only: replaces defender weapons/armor on Torn attack pages using Firebase loadouts.
// @match        https://www.torn.com/loader.php?sid=attack&user2ID=*
// @supportURL   https://greasyfork.org/en/scripts/570408/feedback
// @homepageURL  https://greasyfork.org/en/scripts/570408-torn-pda-loadout-share
// @license      MIT
// @run-at       document-start
// @grant        unsafeWindow
// @connect      torn-loadout-share-default-rtdb.europe-west1.firebasedatabase.app
// ==/UserScript==
 
(function () {
  "use strict";
 
  const W = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
 
  const CONFIG = {
    firebaseUrl: "https://torn-loadout-share-default-rtdb.europe-west1.firebasedatabase.app",
  };
 
  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 to avoid Coconut Bra-like body cosmetics.
  const ARMOR_SLOTS = [8, 7, 9, 6, 4];
  const ARMOR_Z_INDEX = { 8: 10, 7: 11, 9: 12, 6: 13, 4: 14 };
 
  const BONUS_KEY_FIX = { hazarfouse: "hazardous" };
 
  const TORN_BASE = "https://www.torn.com";
 
  function waitForPdaBridge(timeoutMs = 4000) {
    return new Promise((resolve) => {
      const start = Date.now();
      const tick = () => {
        try {
          const bridge = W.flutter_inappwebview;
          if (bridge?.callHandler) return resolve(bridge);
        } catch {}
        if (Date.now() - start > timeoutMs) return resolve(null);
        W.setTimeout(tick, 200);
      };
      tick();
    });
  }
 
  function getUrlTargetId() {
    return (W.location?.href?.match(/user2ID=(\d+)/) || [])[1] ?? null;
  }
 
  function escapeHtml(v) {
    return String(v ?? "")
      .replaceAll("&", "&")
      .replaceAll("<", "&lt;")
      .replaceAll(">", "&gt;")
      .replaceAll('"', "&quot;");
  }
 
  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 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;
  }
 
  function getDefenderArea() {
    // PDA-style: use marker owner or fall back to playerArea selection.
    const marker = queryFirst(W.document, ["#defender_Primary", "#defender_Secondary", "#defender_Melee", "#defender_Temporary"]);
    if (marker) {
      const owner = marker.closest("[class*='playerArea'], [class*='player___']");
      if (owner) return owner;
    }
    const areas = W.document.querySelectorAll("[class*='playerArea']");
    return (areas.length > 1 ? areas[1] : areas[0]) || null;
  }
 
  function queryFirstInDocs(selectors) {
    const docs = getDocAndIframes();
    for (const doc of docs) {
      const n = queryFirst(doc, selectors);
      if (n) return n;
    }
    return null;
  }
 
  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 [];
  }
 
  function normalizeLoadout(loadout) {
    if (!loadout || typeof loadout !== "object") return loadout;
    // Backwards compatibility: older Firebase payloads may include slot 10.
    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;
  }
 
  // PDA-only Firebase GET via Flutter bridge.
  async function firebaseGet(path) {
    const base = CONFIG.firebaseUrl;
    if (!base) return null;
 
    const bridge = await waitForPdaBridge();
    if (!bridge?.callHandler) return null;
 
    const url = `${base}/${path}.json`;
    const headers = {};
    try {
      const r = await bridge.callHandler("PDA_httpGet", url, headers);
      const status = Number(r?.status ?? 0);
      const text = String(r?.responseText ?? "");
      if (!(status >= 200 && status < 300)) return null;
      try {
        return text ? JSON.parse(text) : null;
      } catch {
        return null;
      }
    } 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?.item_bonuses ??
      slot?.bonuses ??
      slot?.Bonus ??
      slot?.bonus;
    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);
    }
    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),
      };
    }
    return Object.keys(loadout).length > 0 ? loadout : null;
  }
 
  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 getBonusIconKey(bonus) {
    if (!bonus) return null;
    const raw = bonus.bonus_key ?? bonus.icon ?? "";
    return BONUS_KEY_FIX[raw] ?? raw;
  }
 
  function buildIconHtml(icon, title, desc) {
    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(" - "));
    return `<div class="container___LAqaj" title="${tooltip}"><i class="bonus-attachment-${safeIcon}" title="${tooltip}"></i></div>`;
  }
 
  function buildSlotIcons(arr, key, name, desc, isBonus) {
    return [0, 1]
      .map((i) => {
        if (!arr?.[i]) return buildIconHtml(null, "", "");
        const iconKey = isBonus ? getBonusIconKey(arr[i]) : arr[i][key];
        return buildIconHtml(iconKey, arr[i][name] ?? arr[i].description, arr[i][desc] ?? arr[i].description);
      })
      .join("");
  }
 
  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 renderSlot(wrapper, item, slotLabel, includeLabel = true, slot = 0) {
    if (!wrapper || !item) return;
 
    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 = `${TORN_BASE}/images/items/${item.item_id}/large`;
      const alreadyCorrect = img.src && String(img.src).includes(`/items/${item.item_id}/`);
      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 || "";
      img.classList.remove("blank___RpGQA");
      img.style.objectFit = "contain";
    }
 
    const top = queryFirst(wrapper, ["[class*='top___']"]);
    if (top) {
      const modIcons = buildSlotIcons(item.mods || [], "icon", "name", "description", false);
      const bonusIcons = buildSlotIcons(item.bonuses || [], "bonus_key", "name", "description", true);
      top.innerHTML = includeLabel
        ? `<div class="props___oL_Cw">${modIcons}</div><div class="topMarker___OjRyU"><span class="markerText___HdlDL">${escapeHtml(slotLabel)}</span></div><div class="props___oL_Cw">${bonusIcons}</div>`
        : `<div class="props___oL_Cw">${modIcons}</div><div class="props___oL_Cw">${bonusIcons}</div>`;
    }
 
    const bottom = queryFirst(wrapper, ["[class*='bottom___']"]);
    if (bottom) {
      const ammoInner = slot === 3 ? INFINITY_SVG : slot === 5 ? `<span class="markerText___HdlDL standard___bW8M5">1</span>` : `<span class="markerText___HdlDL">Unknown</span>`;
      bottom.innerHTML = `<div class="props___oL_Cw"><i class="bonus-attachment-item-damage-bonus" aria-label="Damage"></i><span class="bonusInfo___vyqlT">${formatFixed2(item.damage)}</span></div><div class="bottomMarker___G1uDs">${ammoInner}</div><div class="props___oL_Cw"><i class="bonus-attachment-item-accuracy-bonus" aria-label="Accuracy"></i><span class="bonusInfo___vyqlT">${formatFixed2(item.accuracy)}</span></div>`;
    }
 
    let xp = wrapper.querySelector(".tt-weapon-experience");
    if (!xp) {
      xp = W.document.createElement("div");
      xp.className = "tt-weapon-experience";
      wrapper.appendChild(xp);
    }
    xp.textContent = item.item_name || "";
    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) {
      img.removeAttribute("src");
      img.removeAttribute("srcset");
      img.alt = "";
      img.classList.add("blank___RpGQA");
    }
 
    const top = queryFirst(wrapper, ["[class*='top___']"]);
    if (top) {
      top.innerHTML = includeLabel
        ? `<div class="props___oL_Cw"></div><div class="topMarker___OjRyU"><span class="markerText___HdlDL">${escapeHtml(slotLabel)}</span></div><div class="props___oL_Cw"></div>`
        : `<div class="props___oL_Cw"></div><div class="props___oL_Cw"></div>`;
    }
 
    const bottom = queryFirst(wrapper, ["[class*='bottom___']"]);
    if (bottom) {
      const ammoInner = slot === 3 ? INFINITY_SVG : slot === 5 ? `<span class="markerText___HdlDL standard___bW8M5">0</span>` : `<span class="markerText___HdlDL">—</span>`;
      bottom.innerHTML = `<div class="props___oL_Cw"><i class="bonus-attachment-item-damage-bonus" aria-label="Damage"></i><span class="bonusInfo___vyqlT">—</span></div><div class="bottomMarker___G1uDs">${ammoInner}</div><div class="props___oL_Cw"><i class="bonus-attachment-item-accuracy-bonus" aria-label="Accuracy"></i><span class="bonusInfo___vyqlT">—</span></div>`;
    }
 
    const xp = wrapper.querySelector(".tt-weapon-experience");
    if (xp) xp.textContent = "";
    wrapper.setAttribute("aria-label", slotLabel || "Empty");
  }
 
  function renderArmor(defenderArea, loadout) {
    const modelLayers = queryFirst(defenderArea, ["[class*='modelLayers']"]);
    const parent = modelLayers || defenderArea;
    if (!parent) return;
 
    let overlay = parent.querySelector(".loadout-armor-overlay");
    if (overlay) overlay.remove();
 
    overlay = W.document.createElement("div");
    overlay.className = "loadout-armor-overlay";
    // Visual-only overlay; never block Torn native usemap hover.
    overlay.style.cssText = "position:absolute;inset:0;pointer-events:none;z-index:4;transform:translateY(20px);";
 
    for (const slot of [8, 7, 9, 6, 4]) {
      const item = loadout?.[slot];
      if (!item) continue;
 
      const container = W.document.createElement("div");
      container.className = "armourContainer___zL52C";
      container.style.zIndex = String(ARMOR_Z_INDEX[slot] ?? 14);
 
      const armor = W.document.createElement("div");
      armor.className = "armour___fLnYY";
 
      const img = W.document.createElement("img");
      img.className = "itemImg___B8FMH";
      img.src = `${TORN_BASE}/images/v2/items/model-items/${item.item_id}m.png`;
      img.alt = item.item_name || "";
      img.title = "";
      img.style.pointerEvents = "none";
 
      armor.appendChild(img);
      container.appendChild(armor);
      overlay.appendChild(container);
    }
 
    parent.appendChild(overlay);
  }
 
  function parseLoadoutSlots(loadoutNode) {
    if (!loadoutNode || 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 {}
      } else if (v && typeof v === "object" && !v["#"]) {
        slots[k] = v;
      }
    }
    return slots;
  }
 
  function parsePayloadToLoadout(data) {
    if (!data || typeof data !== "object") return null;
    const raw = data.raw;
    if (raw && typeof raw === "object") {
      const defenderItems = raw.defenderItems ?? raw.attackData?.defenderItems;
      const loadout = parseDefenderItemsToLoadout(defenderItems);
      return loadout;
    }
 
    const lj = data.loadoutJson;
    if (typeof lj === "string") {
      try {
        const parsed = JSON.parse(lj);
        const loadout = parseLoadoutSlots(parsed);
        if (loadout && Object.keys(loadout).length > 0) return loadout;
      } catch {}
    }
 
    if (lj && typeof lj === "object" && !lj["#"]) {
      const loadout = parseLoadoutSlots(lj);
      if (loadout && Object.keys(loadout).length > 0) return loadout;
    }
 
    return null;
  }
 
  async function fetchLoadoutFromFirebase(targetId) {
    const data = await firebaseGet(`loadouts/by_target/${targetId}`);
    if (!data || typeof data !== "object") return null;
 
    const loadout = parsePayloadToLoadout(data);
    if (!loadout) return null;
    normalizeLoadout(loadout);
    return validateLoadout(loadout) ? loadout : null;
  }
 
  function renderLoadout(loadout) {
    if (!loadout) return;
 
    const DOM_POLL_MS = 100;
    const timer = W.setInterval(() => {
      const defenderArea = getDefenderArea();
      if (!defenderArea) return;
 
      W.clearInterval(timer);
 
      const defenderPrimaryPresent = !!queryFirstInDocs(["#defender_Primary"]);
      const attackerPrimaryPresent = !!queryFirstInDocs(["#attacker_Primary"]);
      const attackerSecondaryPresent = !!queryFirstInDocs(["#attacker_Secondary"]);
      const attackerMeleePresent = !!queryFirstInDocs(["#attacker_Melee"]);
      const attackerTempPresent = !!queryFirstInDocs(["#attacker_Temporary"]);
      const genericWeaponMainPresent = !!queryFirstInDocs(["#weapon_main"]);
 
      const hasDefender = defenderPrimaryPresent;
      const hasAttacker = attackerPrimaryPresent || genericWeaponMainPresent;
      // On PDA we don't need weapon slot labels (Primary/Secondary/etc).
      // Torn's icons (mods/bonuses) already indicate the slots.
      const includeLabel = false;
      const defenderReplacesAttacker = hasAttacker && !hasDefender;
 
      const slotMappings = [
        { selector: hasDefender ? "#defender_Primary" : attackerPrimaryPresent ? "#attacker_Primary" : "#weapon_main", slot: 1, label: "Primary" },
        { selector: hasDefender ? "#defender_Secondary" : attackerSecondaryPresent ? "#attacker_Secondary" : "#weapon_second", slot: 2, label: "Secondary" },
        { selector: hasDefender ? "#defender_Melee" : attackerMeleePresent ? "#attacker_Melee" : "#weapon_melee", slot: 3, label: "Melee" },
        { selector: hasDefender ? "#defender_Temporary" : attackerTempPresent ? "#attacker_Temporary" : "#weapon_temp", slot: 5, label: "Temporary" },
      ];
 
      const doApplyWeaponsAndArmor = () => {
        for (const { selector, slot, label } of slotMappings) {
          const marker = queryFirstInDocs([selector]);
          const wrapper = marker?.closest("[class*='weaponWrapper'], [class*='weaponSlot'], [class*='weapon']");
          if (!wrapper) continue;
 
          if (loadout?.[slot]) {
            renderSlot(wrapper, loadout[slot], label, includeLabel, slot);
          } else if (defenderReplacesAttacker) {
            clearSlot(wrapper, label, includeLabel, slot);
          }
        }
 
        renderArmor(defenderArea, loadout);
      };
 
      doApplyWeaponsAndArmor();
 
      if (defenderReplacesAttacker) {
        const endAt = Date.now() + 9000;
        const interval = W.setInterval(() => {
          if (Date.now() > endAt) {
            W.clearInterval(interval);
            return;
          }
          doApplyWeaponsAndArmor();
        }, 250);
      }
    }, DOM_POLL_MS);
  }
 
  const targetId = getUrlTargetId();
  if (!targetId) {
    return;
  }
  (async () => {
    const loadout = await fetchLoadoutFromFirebase(targetId);
    if (!loadout) {
      return;
    }
 
    renderLoadout(loadout);
  })();
})();