ASTRIX Zed City Explorer Helper

Zed City Explorer Helper

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         ASTRIX Zed City Explorer Helper
// @namespace    https://www.zed.city/
// @version      1.0.2
// @description  Zed City Explorer Helper
// @author       ASTRIX & Opus 4.5
// @match        https://www.zed.city/*
// @match        https://zed.city/*
// @run-at       document-idle
// @icon         https://www.google.com/s2/favicons?sz=64&domain=zed.city
// @grant        none
// @license      GNU GPLv3
// ==/UserScript==

(function () {
  "use strict";

  const HELPER_CLASS = "zed-explore-helper-box";
  const TRAVELING_BTN_CLASS = "zed-update-inventory-btn";
  const API_ITEMS = "https://api.zed.city/loadItems";
  const API_ADD_ITEM = "https://api.zed.city/vehicleAddItem";
  const API_UNLOAD_ITEM = "https://api.zed.city/unloadItem?vehicle=true";
  const API_CSRF_TOKEN = "https://api.zed.city/csrfToken";

  // Zone requirements for each explore location
  const ZONE_REQUIREMENTS = {
    0: {
      name: "Fuel Depot",
      zones: [
        { zone: 1, name: "Secure Panel", items: [{ name: "Security Card", quantity: 1 }] }
      ]
    },
    1: {
      name: "Construction Yard",
      zones: [
        { zone: 1, name: "Secure Lockup Door", items: [{ name: "Lockpick", quantity: 1 }] },
        { zone: 2, name: "Security Vault", items: [{ name: "Explosives", quantity: 1 }] }
      ]
    },
    2: {
      name: "Demolition Site",
      zones: [
        { zone: 1, name: "Secure Gate", items: [{ name: "Lockpick", quantity: 1 }] }
      ]
    },
    3: {
      name: "Research Facility",
      zones: [
        { zone: 1, name: "Serum Storage", items: [{ name: "Splicer", quantity: 1 }] }
      ]
    },
    4: {
      name: "Reclaim Zone",
      zones: []
    },
    5: {
      name: "Fishing Reserve",
      zones: []
    },
    6: {
      name: "Military Base",
      zones: [
        { zone: 1, name: "Enter Barracks", items: [{ name: "Barracks Key", quantity: 1 }] },
        { zone: 2, name: "Enter Generals Quarters", items: [{ name: "Generals RFID", quantity: 1 }] }
      ]
    },
    7: {
      name: "Data Center",
      zones: [
        { zone: 1, name: "Security Room", items: [{ name: "Lockpick", quantity: 1 }] },
        { zone: 2, name: "Control Room", items: [{ name: "Explosives", quantity: 1 }] }
      ]
    },
    8: {
      name: "Junkyard",
      zones: [
        { zone: 1, name: "Power Backup Generator", items: [{ name: "Battery", quantity: 1 }] },
        { zone: 2, name: "Hack Security Door", items: [{ name: "Splicer", quantity: 1 }] }
      ]
    },
    9: {
      name: "Logging Camp",
      zones: [
        { zone: 1, name: "Construct Log Lifter", items: [{ name: "Nails", quantity: 250 }, { name: "Planks", quantity: 250 }, { name: "Rope", quantity: 5 }] }
      ]
    },
    10: {
      name: "Open Meadow",
      zones: [
        { zone: 1, name: "Create Clearing", items: [{ name: "Hatchet", quantity: 1 }] }
      ]
    },
    11: {
      name: "Oil Refinery",
      zones: [
        { zone: 1, name: "Bypass Security Door", items: [{ name: "Splicer", quantity: 1 }] },
        { zone: 2, name: "Access Hidden Armourer", items: [{ name: "Money", quantity: 10000 }] }
      ]
    },
    12: {
      name: "Industrial Foundry",
      zones: [
        { zone: 1, name: "Picklock Main Doors", items: [{ name: "Lockpick", quantity: 1 }] },
        { zone: 2, name: "Bypass Staff Access", items: [{ name: "Splicer", quantity: 1 }] },
        { zone: 3, name: "Fire Up Forge", items: [{ name: "Fuel", quantity: 10 }] },
        { zone: 3, name: "Repair Control Unit", items: [{ name: "Computer Board", quantity: 1 }] },
        { zone: 3, name: "Repair Toolbench", items: [{ name: "Advanced Tools", quantity: 1 }, { name: "Steel", quantity: 100 }] }
      ]
    },
    13: {
      name: "Abandoned Quarry",
      zones: [
        { zone: 1, name: "Break-in to old mine", items: [{ name: "Splicer", quantity: 1 }] }
      ]
    }
  };

  // Get current explore location ID from URL
  function getExploreLocationId() {
    const match = window.location.pathname.match(/^\/explore\/(\d+)$/);
    return match ? parseInt(match[1]) : null;
  }

  // Get zone requirements for current location
  function getZoneRequirementsForLocation() {
    const locationId = getExploreLocationId();
    if (locationId === 0) {
      return ZONE_REQUIREMENTS[0];
    }
    if (!locationId || !ZONE_REQUIREMENTS[locationId]) return null;
    return ZONE_REQUIREMENTS[locationId];
  }

  // Check if a zone is already unlocked by searching the page DOM
  function getZoneUnlockStatus(zoneName) {
    // Find all divs that might contain the zone name
    const allDivs = document.querySelectorAll("div.col, div[class*='col']");
    
    for (const div of allDivs) {
      // Check if this div contains the zone name (exact or partial match)
      if (div.textContent.trim() === zoneName || div.textContent.includes(zoneName)) {
        // Go to parent row
        const parentRow = div.closest(".row, .tbl-row, [class*='row']");
        if (!parentRow) continue;
        
        // Look for "Unlocked" text in a sibling element
        const siblings = parentRow.querySelectorAll("div, span");
        for (const sibling of siblings) {
          const text = sibling.textContent || "";
          if (text.includes("Unlocked")) {
            // Found unlocked status, now look for countdown timer
            const timerSpan = parentRow.querySelector(".countdown-timer, [class*='countdown']");
            let timeRemaining = "";
            if (timerSpan) {
              timeRemaining = timerSpan.textContent.trim();
            } else {
              // Try to extract time from the text itself (format: "Unlocked (HH:MM:SS)")
              const timeMatch = text.match(/(\d{1,2}:\d{2}:\d{2})/);
              if (timeMatch) {
                timeRemaining = timeMatch[1];
              }
            }
            return { unlocked: true, timeRemaining };
          }
        }
      }
    }
    return { unlocked: false, timeRemaining: "" };
  }

  // Helper function to wait for an element
  function waitForElement(selector, timeout = 10000) {
    return new Promise((resolve, reject) => {
      const element = document.querySelector(selector);
      if (element) {
        resolve(element);
        return;
      }

      const observer = new MutationObserver(() => {
        const el = document.querySelector(selector);
        if (el) {
          observer.disconnect();
          resolve(el);
        }
      });

      observer.observe(document.body, { childList: true, subtree: true });

      setTimeout(() => {
        observer.disconnect();
        reject(new Error(`Timeout waiting for ${selector}`));
      }, timeout);
    });
  }

  // Check if current page is traveling page
  function isTravelingPage() {
    return window.location.pathname === "/traveling";
  }
  let helperObserver = null;
  let itemsCache = null;
  let vehicleItemsCache = null;
  let currencyItemsCache = null;
  let csrfToken = null;
  let refreshInterval = null;
  let currentListContainer = null;
  let currentSearchInput = null;
  let weightDisplay = null;
  let weightProgressBar = null;
  let maxVehicleWeight = 0; // Default max vehicle capacity in kg

  // Get max weight from page element with <small>kg</small>
  function getMaxWeightFromPage() {
    const smallKgElements = document.querySelectorAll("small");
    for (const small of smallKgElements) {
      if (small.textContent.trim() === "kg") {
        const parent = small.parentElement;
        if (parent && parent.classList.contains("col-shrink")) {
          // Text content is like "3.5/38" followed by <small>kg</small>
          const textContent = parent.textContent.replace("kg", "").trim();
          const match = textContent.match(/[\d.]+\/([\d.]+)/);
          if (match) {
            return parseFloat(match[1]) || 38;
          }
        }
      }
    }
    return 38; // Default fallback
  }

  // Calculate total weight of vehicle items
  function calculateVehicleWeight(vehicleItems) {
    let totalWeight = 0;
    vehicleItems.forEach((item) => {
      const weight = parseFloat(item.vars?.weight) || 0;
      const quantity = parseInt(item.quantity) || 1;
      totalWeight += weight * quantity;
    });
    return totalWeight;
  }

  // Update weight display
  function updateWeightDisplay() {
    if (!weightDisplay || !vehicleItemsCache) return;
    maxVehicleWeight = getMaxWeightFromPage();
    const currentWeight = calculateVehicleWeight(vehicleItemsCache);
    weightDisplay.textContent = `${currentWeight.toFixed(
      1
    )}/${maxVehicleWeight}kg`;

    // Update progress bar
    if (weightProgressBar) {
      const percentage =
        maxVehicleWeight > 0
          ? Math.min((currentWeight / maxVehicleWeight) * 100, 100)
          : 0;
      const isOverweight = currentWeight > maxVehicleWeight;
      weightProgressBar.style.width = `${percentage}%`;
      weightProgressBar.style.background = isOverweight
        ? "linear-gradient(90deg, #ef4444 0%, #f87171 100%)"
        : "linear-gradient(90deg, #22c55e 0%, #4ade80 100%)";
    }
  }

  // Fetch CSRF token from API
  async function fetchCsrfToken() {
    if (csrfToken) return csrfToken;
    try {
      const res = await fetch(API_CSRF_TOKEN, { credentials: "include" });
      const json = await res.json();
      if (json.token) {
        csrfToken = json.token;
        return csrfToken;
      }
    } catch (error) {
      // console.log("ZED Explore Helper: Error fetching CSRF token", error);
    }
    return null;
  }

  // Show toast notification in bottom right
  function showToast(message, isSuccess = true) {
    const toast = document.createElement("div");
    toast.style.cssText = `
      position: fixed;
      bottom: 20px;
      right: 20px;
      padding: 12px 20px;
      background: ${isSuccess ? "#22c55e" : "#dc2626"};
      color: white;
      border-radius: 6px;
      font-size: 14px;
      font-weight: 500;
      z-index: 10001;
      box-shadow: 0 4px 12px rgba(0,0,0,0.3);
      animation: slideIn 0.3s ease;
    `;
    toast.textContent = message;
    document.body.appendChild(toast);

    // Add animation keyframes if not exists
    if (!document.getElementById("explore-helper-toast-styles")) {
      const style = document.createElement("style");
      style.id = "explore-helper-toast-styles";
      style.textContent = `
        @keyframes slideIn {
          from { transform: translateX(100%); opacity: 0; }
          to { transform: translateX(0); opacity: 1; }
        }
      `;
      document.head.appendChild(style);
    }

    setTimeout(() => {
      toast.style.animation = "slideIn 0.3s ease reverse";
      setTimeout(() => toast.remove(), 300);
    }, 3000);
  }

  // Show quantity popup modal
  function showQuantityPopup(item, onConfirm, isUnload = false) {
    const maxQty = item.quantity || 1;
    let currentQty = 1;

    // Backdrop
    const backdrop = document.createElement("div");
    backdrop.style.cssText = `
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: rgba(0,0,0,0.7);
      z-index: 10000;
      display: flex;
      align-items: center;
      justify-content: center;
    `;

    // Modal
    const modal = document.createElement("div");
    modal.style.cssText = `
      background: #0d0d0d;
      border: 1px solid #1a1a1a;
      border-radius: 8px;
      padding: 24px;
      min-width: 300px;
      text-align: center;
    `;

    // Item name
    const itemName = document.createElement("div");
    itemName.textContent = item.name;
    itemName.style.cssText =
      "color: #fff; font-size: 18px; font-weight: 500; margin-bottom: 16px;";
    modal.appendChild(itemName);

    // Item image
    const imgContainer = document.createElement("div");
    imgContainer.style.cssText =
      "position: relative; display: inline-block; margin-bottom: 20px;";
    const img = document.createElement("img");
    const codename =
      item.codename || item.name?.toLowerCase().replace(/\s+/g, "_") || "";
    img.src = `https://www.zed.city/items/${codename}.webp`;
    img.style.cssText = "width: 80px; height: 80px; object-fit: contain;";
    imgContainer.appendChild(img);
    if (maxQty > 1) {
      const badge = document.createElement("span");
      badge.textContent = `x${maxQty}`;
      badge.style.cssText = `
        position: absolute;
        bottom: 0;
        right: 0;
        background: rgba(0,0,0,0.8);
        color: #fff;
        font-size: 12px;
        padding: 2px 6px;
        border-radius: 3px;
      `;
      imgContainer.appendChild(badge);
    }
    modal.appendChild(imgContainer);

    // Quantity section
    const qtySection = document.createElement("div");
    qtySection.style.cssText =
      "background: #141414; border-radius: 8px; padding: 16px; margin-bottom: 20px;";

    const qtyLabel = document.createElement("div");
    qtyLabel.textContent = "QUANTITY";
    qtyLabel.style.cssText =
      "color: #888; font-size: 12px; margin-bottom: 12px; letter-spacing: 1px;";
    qtySection.appendChild(qtyLabel);

    // Quantity controls
    const qtyControls = document.createElement("div");
    qtyControls.style.cssText =
      "display: flex; align-items: center; justify-content: center; gap: 0;";

    const minusBtn = document.createElement("button");
    minusBtn.textContent = "<";
    minusBtn.style.cssText = `
      background: #1f1f1f;
      color: #888;
      border: none;
      padding: 10px 20px;
      cursor: pointer;
      font-size: 16px;
      border-radius: 4px 0 0 4px;
    `;

    // Add styles for hiding number input spinners
    if (!document.getElementById("explore-helper-input-styles")) {
      const style = document.createElement("style");
      style.id = "explore-helper-input-styles";
      style.textContent = `
        .explore-helper-qty-input::-webkit-outer-spin-button,
        .explore-helper-qty-input::-webkit-inner-spin-button {
          -webkit-appearance: none;
          margin: 0;
        }
      `;
      document.head.appendChild(style);
    }

    const qtyDisplay = document.createElement("input");
    qtyDisplay.type = "number";
    qtyDisplay.className = "explore-helper-qty-input";
    qtyDisplay.value = currentQty;
    qtyDisplay.min = 1;
    qtyDisplay.max = maxQty;
    qtyDisplay.style.cssText = `
      background: #0a0a0a;
      color: #fff;
      padding: 10px 0;
      font-size: 18px;
      width: 80px;
      text-align: center;
      border: none;
      border-top: 1px solid #222;
      border-bottom: 1px solid #222;
      outline: none;
      -moz-appearance: textfield;
      appearance: textfield;
    `;
    qtyDisplay.addEventListener("input", () => {
      let val = parseInt(qtyDisplay.value) || 1;
      if (val < 1) val = 1;
      if (val > maxQty) val = maxQty;
      currentQty = val;
    });
    qtyDisplay.addEventListener("blur", () => {
      qtyDisplay.value = currentQty;
    });

    const plusBtn = document.createElement("button");
    plusBtn.textContent = ">";
    plusBtn.style.cssText = `
      background: #1f1f1f;
      color: #888;
      border: none;
      padding: 10px 20px;
      cursor: pointer;
      font-size: 16px;
      border-radius: 0 4px 4px 0;
    `;

    minusBtn.addEventListener("click", () => {
      if (currentQty > 1) {
        currentQty--;
        qtyDisplay.value = currentQty;
      }
    });

    plusBtn.addEventListener("click", () => {
      if (currentQty < maxQty) {
        currentQty++;
        qtyDisplay.value = currentQty;
      }
    });

    qtyControls.appendChild(minusBtn);
    qtyControls.appendChild(qtyDisplay);
    qtyControls.appendChild(plusBtn);
    qtySection.appendChild(qtyControls);

    // MAX button
    const maxBtn = document.createElement("button");
    maxBtn.textContent = "MAX";
    maxBtn.style.cssText = `
      background: #1f1f1f;
      color: #888;
      border: none;
      padding: 8px 40px;
      cursor: pointer;
      font-size: 12px;
      border-radius: 20px;
      margin-top: 12px;
    `;
    maxBtn.addEventListener("click", () => {
      currentQty = maxQty;
      qtyDisplay.value = currentQty;
    });
    qtySection.appendChild(maxBtn);
    modal.appendChild(qtySection);

    // Action buttons
    const actionBtns = document.createElement("div");
    actionBtns.style.cssText =
      "display: flex; justify-content: center; gap: 20px; align-items: center;";

    const cancelBtn = document.createElement("button");
    cancelBtn.textContent = "CANCEL";
    cancelBtn.style.cssText = `
      background: transparent;
      color: #ef4444;
      border: none;
      padding: 12px 24px;
      cursor: pointer;
      font-size: 14px;
      font-weight: 500;
    `;
    cancelBtn.addEventListener("click", () => backdrop.remove());

    const actionBtn = document.createElement("button");
    actionBtn.textContent = isUnload ? "UNLOAD ITEM" : "ADD ITEM";
    actionBtn.style.cssText = `
      background: ${isUnload ? "linear-gradient(135deg, #ef4444 0%, #dc2626 100%)" : "linear-gradient(135deg, #22c55e 0%, #16a34a 100%)"};
      color: #fff;
      border: none;
      padding: 12px 24px;
      cursor: pointer;
      font-size: 14px;
      font-weight: 500;
      border-radius: 4px;
    `;
    actionBtn.addEventListener("click", async () => {
      actionBtn.disabled = true;
      actionBtn.textContent = isUnload ? "Unloading..." : "Adding...";
      await onConfirm(currentQty);
      backdrop.remove();
    });

    actionBtns.appendChild(cancelBtn);
    actionBtns.appendChild(actionBtn);
    modal.appendChild(actionBtns);

    backdrop.appendChild(modal);
    backdrop.addEventListener("click", (e) => {
      if (e.target === backdrop) backdrop.remove();
    });

    document.body.appendChild(backdrop);
  }

  // Add item to vehicle via API
  async function addItemToVehicle(item, quantity) {
    try {
      // Get CSRF token first
      const token = await fetchCsrfToken();
      const headers = {
        "Content-Type": "application/json",
      };
      if (token) {
        headers["x-csrf-token"] = token;
      }

      const res = await fetch(API_ADD_ITEM, {
        method: "POST",
        credentials: "include",
        headers,
        body: JSON.stringify({ item: item.id, quantity }),
      });
      const json = await res.json();

      if (json.success) {
        const addedItems = json.reactive_items_data || [];
        if (addedItems.length > 0) {
          const names = addedItems
            .map((i) => `${i.quantity}x ${i.name}`)
            .join(", ");
          showToast(`Added: ${names}`, true);
        } else {
          showToast(`Added ${quantity}x ${item.name}`, true);
        }
        // Refresh items list
        await refreshItemList();
      } else {
        showToast(json.error || "Failed to add item", false);
      }
    } catch (error) {
      // console.log("ZED Explore Helper: Error adding item", error);
      showToast("Error adding item", false);
    }
  }

  // Unload item from vehicle via API
  async function unloadItemFromVehicle(item, quantity) {
    try {
      // Get CSRF token first
      const token = await fetchCsrfToken();
      const headers = {
        "Content-Type": "application/json",
      };
      if (token) {
        headers["x-csrf-token"] = token;
      }

      const res = await fetch(API_UNLOAD_ITEM, {
        method: "POST",
        credentials: "include",
        headers,
        body: JSON.stringify({ item_id: item.id, quantity }),
      });
      const json = await res.json();

      if (json.success) {
        showToast(`Unloaded ${quantity}x ${item.name}`, true);
        // Refresh items list
        await refreshItemList();
      } else {
        showToast(json.error || "Failed to unload item", false);
      }
    } catch (error) {
      // console.log("ZED Explore Helper: Error unloading item", error);
      showToast("Error unloading item", false);
    }
  }

  // Check if current URL matches /explore/{number}
  function isExplorePage() {
    const match = window.location.pathname.match(/^\/explore\/(\d+)$/);
    return match !== null;
  }

  // Find span with "Travel" text
  function findTravelSpan() {
    const spans = document.querySelectorAll("span");
    for (const span of spans) {
      if (span.textContent.trim() === "Travel") {
        // add margin top to the parent of the span
        span.parentElement.style.marginTop = "10px";
        return span;
      }
    }
    return null;
  }

  // Fetch items from API (returns inventory, vehicle, and currency items)
  async function fetchItems(forceRefresh = false) {
    if (!forceRefresh && itemsCache && vehicleItemsCache && currencyItemsCache) {
      return { items: itemsCache, vehicleItems: vehicleItemsCache, currencyItems: currencyItemsCache };
    }
    try {
      const res = await fetch(API_ITEMS, { credentials: "include" });
      if (!res.ok) throw new Error("Failed to fetch items");
      const json = await res.json();
      itemsCache = Array.isArray(json?.items) ? json.items : [];
      vehicleItemsCache = Array.isArray(json?.vehicle_items)
        ? json.vehicle_items
        : [];
      currencyItemsCache = Array.isArray(json?.currency_items)
        ? json.currency_items
        : [];
      updateWeightDisplay();
      return { items: itemsCache, vehicleItems: vehicleItemsCache, currencyItems: currencyItemsCache };
    } catch (error) {
      // console.log("ZED Explore Helper: Error fetching items", error);
      return { items: [], vehicleItems: [], currencyItems: [] };
    }
  }

  // Create zone requirement item row
  function createZoneRequirementRow(zoneReq, reqItem, allItems, vehicleItems, currencyItems = []) {
    // Check if this zone is already unlocked
    const unlockStatus = getZoneUnlockStatus(zoneReq.name);
    const isUnlocked = unlockStatus.unlocked;

    const row = document.createElement("div");
    row.className = "item-row zone-requirement-item";
    row.style.cssText = `
      display: grid;
      grid-template-columns: 1fr 2fr 1fr;
      align-items: center;
      padding: 3px 8px;
      border-bottom: 1px solid rgba(255,255,255,0.1);
      background: ${isUnlocked ? "rgba(34, 197, 94, 0.15)" : "rgba(234, 179, 8, 0.15)"};
    `;

    // Check if this is a currency item (like Money)
    const isMoney = reqItem.name.toLowerCase() === "money";
    const currencyItem = currencyItems.find(
      (i) => i.name.toLowerCase() === reqItem.name.toLowerCase()
    );

    // Find actual item in inventory or vehicle
    const allAvailableItems = [...allItems, ...vehicleItems];
    const actualItem = isMoney ? currencyItem : allAvailableItems.find(
      (i) => i.name.toLowerCase() === reqItem.name.toLowerCase()
    );
    const inventoryItem = allItems.find(
      (i) => i.name.toLowerCase() === reqItem.name.toLowerCase()
    );
    const vehicleItem = vehicleItems.find(
      (i) => i.name.toLowerCase() === reqItem.name.toLowerCase()
    );

    // Item image (left column)
    const imgContainer = document.createElement("div");
    imgContainer.style.cssText =
      "width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;";
    if (actualItem?.image) {
      const img = document.createElement("img");
      img.src = actualItem.image;
      img.alt = reqItem.name;
      img.style.cssText = "max-width: 100%; max-height: 100%; object-fit: contain;";
      imgContainer.appendChild(img);
    } else {
      imgContainer.innerHTML = `<div style="width: 24px; height: 24px; background: rgba(234, 179, 8, 0.3); border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #eab308;">?</div>`;
    }
    row.appendChild(imgContainer);

    // Item name and zone info (center column - always centered)
    const nameContainer = document.createElement("div");
    nameContainer.style.cssText = "display: flex; flex-direction: column; align-items: center; text-align: center;";

    const nameSpan = document.createElement("span");
    nameSpan.className = "item-name";
    nameSpan.style.cssText = `color: ${isUnlocked ? "#22c55e" : "#eab308"}; font-size: 12px; font-weight: 500;`;
    nameSpan.textContent = reqItem.name;
    nameContainer.appendChild(nameSpan);

    const zoneInfo = document.createElement("span");
    zoneInfo.style.cssText = "color: #a8a29e; font-size: 9px;";
    if (isUnlocked) {
      const timeText = unlockStatus.timeRemaining ? ` for ${unlockStatus.timeRemaining}` : "";
      zoneInfo.textContent = `Zone ${zoneReq.zone} - ${zoneReq.name} - ✓ Unlocked${timeText}`;
      zoneInfo.style.color = "#86efac";
    } else {
      zoneInfo.textContent = `Zone ${zoneReq.zone} - ${zoneReq.name} - Required: ${reqItem.quantity}`;
    }
    nameContainer.appendChild(zoneInfo);

    row.appendChild(nameContainer);

    // Right column container (quantity + buttons)
    const rightContainer = document.createElement("div");
    rightContainer.style.cssText = "display: flex; align-items: center; justify-content: flex-end; gap: 8px;";

    // If unlocked, just show "Not Required" label
  
      // Quantity display
      const qtySpan = document.createElement("span");
      const vehicleQty = vehicleItem?.quantity || 0;
      const inventoryQty = inventoryItem?.quantity || 0;
      const currencyQty = currencyItem?.quantity || 0;
      
      // For Money, use currency quantity; for others, use vehicle quantity
      const displayQty = isMoney ? currencyQty : vehicleQty;
      const hasEnough = isMoney ? (currencyQty >= reqItem.quantity) : (vehicleQty >= reqItem.quantity);
      
      qtySpan.style.cssText = `color: ${hasEnough ? "#22c55e" : "#eab308"}; font-size: 11px; min-width: 35px; text-align: right;`;
      qtySpan.textContent = `${displayQty}/${reqItem.quantity}`;
      qtySpan.title = isMoney ? `Money: ${currencyQty}` : `In vehicle: ${vehicleQty}, In inventory: ${inventoryQty}`;
      rightContainer.appendChild(qtySpan);

      if (!isMoney) {
        // Buttons container
        const btnsContainer = document.createElement("div");
        btnsContainer.style.cssText = "display: flex; gap: 4px;";

        const hasInInventory = inventoryItem && inventoryItem.quantity > 0;
        const hasInVehicle = vehicleItem && vehicleItem.quantity > 0;

        // Add button (if item exists in inventory)
        if (hasInInventory) {
          const addBtn = document.createElement("button");
          addBtn.textContent = "Add";
          addBtn.style.cssText = `
            background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
            border: none;
            color: #fff;
            padding: 4px 0;
            border-radius: 3px;
            cursor: pointer;
            font-weight: 500;
            font-size: 10px;
            width: 45px;
            text-align: center;
          `;
          addBtn.addEventListener("click", () => {
            showQuantityPopup(inventoryItem, async (quantity) => {
              await addItemToVehicle(inventoryItem, quantity);
            });
          });
          btnsContainer.appendChild(addBtn);
        }

        // Unload button (if item exists in vehicle)
        if (hasInVehicle) {
          const unloadBtn = document.createElement("button");
          unloadBtn.textContent = "Unload";
          unloadBtn.style.cssText = `
            background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
            border: none;
            color: #fff;
            padding: 4px 0;
            border-radius: 3px;
            cursor: pointer;
            font-weight: 500;
            font-size: 10px;
            width: 45px;
            text-align: center;
          `;
          unloadBtn.addEventListener("click", () => {
            showQuantityPopup(vehicleItem, async (quantity) => {
              await unloadItemFromVehicle(vehicleItem, quantity);
            }, true);
          });
          btnsContainer.appendChild(unloadBtn);
        }

        // Missing label (if item not available anywhere)
        if (!hasInInventory && !hasInVehicle) {
          const missingSpan = document.createElement("span");
          missingSpan.style.cssText = "color: #ef4444; font-size: 9px; width: 60px; text-align: center;";
          missingSpan.textContent = "Missing";
          btnsContainer.appendChild(missingSpan);
        }

        rightContainer.appendChild(btnsContainer);
      }
    

    row.appendChild(rightContainer);
    return row;
  }

  // Refresh and update the item list
  async function refreshItemList() {
    if (!currentListContainer) return;

    const { items, vehicleItems, currencyItems } = await fetchItems(true);
    currentListContainer.innerHTML = "";

    // Add zone requirements first (at the top)
    const zoneReqs = getZoneRequirementsForLocation();
    if (zoneReqs && zoneReqs.zones.length > 0) {
      const zoneSection = document.createElement("div");
      zoneSection.className = "zone-requirements-section";
      zoneSection.style.cssText = "border-bottom: 2px solid rgba(234, 179, 8, 0.3); margin-bottom: 4px;";

      // Section header
      const header = document.createElement("div");
      header.style.cssText = "padding: 4px 8px; font-size: 10px; color: #eab308; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;";
      header.textContent = `🔓 Zone Requirements - ${zoneReqs.name}`;
      zoneSection.appendChild(header);

      zoneReqs.zones.forEach((zoneReq) => {
        zoneReq.items.forEach((reqItem) => {
          zoneSection.appendChild(createZoneRequirementRow(zoneReq, reqItem, items, vehicleItems, currencyItems));
        });
      });
      currentListContainer.appendChild(zoneSection);
    }

    // Add vehicle items (pinned section)
    if (vehicleItems.length > 0) {
      const vehicleSection = document.createElement("div");
      vehicleSection.className = "vehicle-items-section";
      vehicleItems.forEach((item) => {
        vehicleSection.appendChild(createItemRow(item, true));
      });
      currentListContainer.appendChild(vehicleSection);
    }

    // Add inventory items
    if (items.length === 0 && vehicleItems.length === 0 && (!zoneReqs || zoneReqs.zones.length === 0)) {
      currentListContainer.innerHTML =
        "<div style='padding: 20px; text-align: center; color: #888;'>No items found</div>";
    } else {
      items.forEach((item) => {
        currentListContainer.appendChild(createItemRow(item, false));
      });
    }

    // Reapply search filter if there's a query (exclude zone requirements)
    if (currentSearchInput && currentSearchInput.value.trim()) {
      const query = currentSearchInput.value.toLowerCase().trim();
      const rows = currentListContainer.querySelectorAll(".item-row:not(.zone-requirement-item)");
      rows.forEach((row) => {
        const name =
          row.querySelector(".item-name")?.textContent?.toLowerCase() || "";
        row.style.display = name.includes(query) ? "grid" : "none";
      });
    }
  }

  // Create item row element (isVehicleItem = true for items in vehicle)
  function createItemRow(item, isVehicleItem = false) {
    const row = document.createElement("div");
    row.className = isVehicleItem ? "item-row vehicle-item" : "item-row";
    row.style.cssText = `
      display: grid;
      grid-template-columns: 1fr 2fr 1fr;
      align-items: center;
      padding: 3px 8px;
      border-bottom: 1px solid rgba(255,255,255,0.1);
      background: ${isVehicleItem ? "rgba(59, 130, 246, 0.15)" : "transparent"};
    `;

    // Check if item exists in both vehicle and inventory
    const inventoryItem = itemsCache?.find(
      (i) => i.name?.toLowerCase() === item.name?.toLowerCase()
    );
    const vehicleItem = vehicleItemsCache?.find(
      (i) => i.name?.toLowerCase() === item.name?.toLowerCase()
    );
    const hasInInventory = inventoryItem && inventoryItem.quantity > 0;
    const hasInVehicle = vehicleItem && vehicleItem.quantity > 0;

    // Left column: Item image
    const imgContainer = document.createElement("div");
    imgContainer.style.cssText =
      "position: relative; width: 28px; height: 28px;";
    const img = document.createElement("img");
    const codename =
      item.codename || item.name?.toLowerCase().replace(/\s+/g, "_") || "";
    img.src = `https://www.zed.city/items/${codename}.webp`;
    img.alt = item.name || "";
    img.style.cssText = "width: 100%; height: 100%; object-fit: contain;";
    imgContainer.appendChild(img);

    // Quantity badge if > 1
    if (item.quantity && item.quantity > 1) {
      const badge = document.createElement("span");
      badge.textContent = `x${item.quantity}`;
      badge.style.cssText = `
        position: absolute;
        bottom: -2px;
        right: -4px;
        background: rgba(0,0,0,0.8);
        color: #fff;
        font-size: 9px;
        padding: 0px 3px;
        border-radius: 2px;
      `;
      imgContainer.appendChild(badge);
    }
    row.appendChild(imgContainer);

    // Center column: Item name (with quantity if in vehicle, condition if < 100)
    const name = document.createElement("div");
    name.className = "item-name";
    const condition = item.vars?.condition;
    let nameText = item.name || "Unknown";
    // Show quantity in name for vehicle items
    if (isVehicleItem && item.quantity && item.quantity > 1) {
      nameText += ` x ${item.quantity}`;
    }
    if (condition !== undefined && condition < 100) {
      nameText += ` (${condition}%)`;
    }
    name.textContent = nameText;
    name.style.cssText =
      "font-size: 14px; color: #fff; text-align: center;";
    row.appendChild(name);

    // Right column: Buttons container
    const btnsContainer = document.createElement("div");
    btnsContainer.style.cssText = "display: flex; gap: 4px; justify-content: flex-end;";

    // Add button (if item exists in inventory)
    if (hasInInventory) {
      const addBtn = document.createElement("button");
      addBtn.textContent = "Add";
      addBtn.style.cssText = `
        background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
        border: none;
        color: #fff;
        padding: 4px 0;
        border-radius: 3px;
        cursor: pointer;
        font-weight: 500;
        font-size: 10px;
        width: 45px;
        text-align: center;
      `;
      addBtn.addEventListener("click", () => {
        showQuantityPopup(inventoryItem, async (quantity) => {
          await addItemToVehicle(inventoryItem, quantity);
        });
      });
      btnsContainer.appendChild(addBtn);
    }

    // Unload button (if item exists in vehicle)
    if (hasInVehicle) {
      const unloadBtn = document.createElement("button");
      unloadBtn.textContent = "Unload";
      unloadBtn.style.cssText = `
        background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
        border: none;
        color: #fff;
        padding: 4px 0;
        border-radius: 3px;
        cursor: pointer;
        font-weight: 500;
        font-size: 10px;
        width: 45px;
        text-align: center;
      `;
      unloadBtn.addEventListener("click", () => {
        showQuantityPopup(vehicleItem, async (quantity) => {
          await unloadItemFromVehicle(vehicleItem, quantity);
        }, true);
      });
      btnsContainer.appendChild(unloadBtn);
    }

    row.appendChild(btnsContainer);
    return row;
  }

  // Create items list panel
  function createItemsPanel(contentDiv) {
    const panel = document.createElement("div");
    panel.className = "items-panel";
    panel.style.cssText = "display: none; flex-direction: column;";

    // Search bar container
    const searchContainer = document.createElement("div");
    searchContainer.style.cssText = "padding: 8px;";

    const searchInput = document.createElement("input");
    searchInput.type = "text";
    searchInput.placeholder = "Search items...";
    searchInput.style.cssText = `
      width: 100%;
      padding: 8px 12px;
      border: 1px solid rgba(255,255,255,0.2);
      border-radius: 4px;
      background: rgba(0,0,0,0.5);
      color: #fff;
      font-size: 14px;
      outline: none;
      box-sizing: border-box;
    `;
    searchContainer.appendChild(searchInput);
    panel.appendChild(searchContainer);

    // Items list container (max ~5 rows visible, each row ~35px)
    const listContainer = document.createElement("div");
    listContainer.className = "items-list";
    listContainer.style.cssText = "overflow-y: auto; max-height: 276px;";
    panel.appendChild(listContainer);

    // Search functionality (filters items but NOT zone requirements)
    searchInput.addEventListener("input", () => {
      const query = searchInput.value.toLowerCase().trim();
      // Only filter regular item rows, not zone requirements
      const rows = listContainer.querySelectorAll(".item-row:not(.zone-requirement-item)");
      rows.forEach((row) => {
        const name =
          row.querySelector(".item-name")?.textContent?.toLowerCase() || "";
        row.style.display = name.includes(query) ? "grid" : "none";
      });
      // Keep zone requirements section always visible
      const zoneSection = listContainer.querySelector(".zone-requirements-section");
      if (zoneSection) {
        zoneSection.style.display = "block";
      }
    });

    // Store references for global access
    currentListContainer = listContainer;
    currentSearchInput = searchInput;

    return { panel, listContainer, searchInput };
  }

  // Create the explore helper box
  function createExploreHelperBox() {
    const mainContainer = document.createElement("div");
    mainContainer.className = "col-xs-12 q-mt-sm";
    const container = document.createElement("div");
    container.className = `zed-grid has-title has-content full-height ${HELPER_CLASS}`;
    mainContainer.appendChild(container);

    const title = document.createElement("div");
    title.className = "title";
    title.style.cssText =
      "display: flex; flex-direction: column; padding-bottom: 0;";

    // Title row with text and weight
    const titleRow = document.createElement("div");
    titleRow.style.cssText =
      "display: flex; justify-content: space-between; align-items: center; width: 100%;";

    const titleInner = document.createElement("div");
    titleInner.textContent = "Inventory";
    titleRow.appendChild(titleInner);

    // Weight display
    weightDisplay = document.createElement("div");
    weightDisplay.className = "col-shrink subtext-large text-grey-7";
    weightDisplay.style.cssText = "font-size: 12px; color: #888;";
    weightDisplay.textContent = `0/${maxVehicleWeight}kg`;
    titleRow.appendChild(weightDisplay);

    title.appendChild(titleRow);
    container.appendChild(title);

    // Progress bar container - positioned right below title (like STINGER bar)
    const progressContainer = document.createElement("div");
    progressContainer.style.cssText = `
      width: 100%;
      height: 5px;
      background: #202327;
      overflow: hidden;
      position: relative;
    `;

    // Progress bar fill
    weightProgressBar = document.createElement("div");
    weightProgressBar.style.cssText = `
      height: 100%;
      width: 0%;
      position: absolute;
      left: 0;
      top: 0;
      background: linear-gradient(90deg,rgba(34, 197, 94, 0.45) 0%,rgba(74, 222, 128, 0.85) 100%);
      transition: width 0.3s ease, background 0.3s ease;
    `;
    progressContainer.appendChild(weightProgressBar);
    container.insertBefore(progressContainer, container.children[1] || null);

    const content = document.createElement("div");
    content.className = "grid-cont";
    content.style.cssText = "padding: 12px;";

    // Add Items button
    const addItemsBtn = document.createElement("button");
    addItemsBtn.textContent = "Add Items";
    addItemsBtn.style.cssText = `
      background:rgba(60, 139, 64, 0.5);
      color: white;
      border: none;
      padding: 10px 20px;
      border-radius: 4px;
      cursor: pointer;
      font-weight: 500;
      font-size: 14px;
      width: 100%;
    `;
    //hover effect
    addItemsBtn.addEventListener("mouseenter", () => {
      addItemsBtn.style.backgroundColor = "rgba(60, 139, 64, 0.8)";
    });
    addItemsBtn.addEventListener("mouseleave", () => {
      addItemsBtn.style.backgroundColor = "rgba(60, 139, 64, 0.5)";
    });
    addItemsBtn.style.transition = "background 0.3s ease";

    // Create items panel
    const { panel, listContainer } = createItemsPanel(content);
    let panelOpen = false;
    let itemsLoaded = false;

    // Function to load items into list
    async function loadItems() {
      listContainer.innerHTML =
        "<div style='padding: 20px; text-align: center; color: #888;'>Loading items...</div>";
      const { items, vehicleItems, currencyItems } = await fetchItems(true);
      listContainer.innerHTML = "";

      // Add zone requirements first (at the top)
      const zoneReqs = getZoneRequirementsForLocation();
      if (zoneReqs && zoneReqs.zones.length > 0) {
        const zoneSection = document.createElement("div");
        zoneSection.className = "zone-requirements-section";
        zoneSection.style.cssText = "border-bottom: 2px solid rgba(234, 179, 8, 0.3); margin-bottom: 4px;";

        // Section header
        const header = document.createElement("div");
        header.style.cssText = "padding: 4px 8px; font-size: 10px; color: #eab308; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;";
        header.textContent = `🔓 Zone Requirements - ${zoneReqs.name}`;
        zoneSection.appendChild(header);

        zoneReqs.zones.forEach((zoneReq) => {
          zoneReq.items.forEach((reqItem) => {
            zoneSection.appendChild(createZoneRequirementRow(zoneReq, reqItem, items, vehicleItems, currencyItems));
          });
        });
        listContainer.appendChild(zoneSection);
      }

      if (items.length === 0 && vehicleItems.length === 0 && (!zoneReqs || zoneReqs.zones.length === 0)) {
        listContainer.innerHTML =
          "<div style='padding: 20px; text-align: center; color: #888;'>No items found</div>";
      } else {
        // Add vehicle items first (pinned at top with different background)
        vehicleItems.forEach((item) => {
          listContainer.appendChild(createItemRow(item, true));
        });
        // Add inventory items
        items.forEach((item) => {
          listContainer.appendChild(createItemRow(item, false));
        });
        itemsLoaded = true;
      }
    }

    addItemsBtn.addEventListener("click", async () => {
      if (!panelOpen) {
        panelOpen = true;
        panel.style.display = "flex";
        addItemsBtn.style.display = "none"; // Hide button once opened

        if (!itemsLoaded) {
          await loadItems();
        }
        // Start 10-second refresh interval
        if (refreshInterval) clearInterval(refreshInterval);
        refreshInterval = setInterval(refreshItemList, 10000);
      }
    });

    content.appendChild(addItemsBtn);
    content.appendChild(panel);
    container.appendChild(content);

    return mainContainer;
  }

  function insertBox() {
    // Check if box already exists
    if (document.querySelector(`.${HELPER_CLASS}`)) {
      // console.log("ZED Explore Helper: Hello box already exists");
      return;
    }

    const travelSpan = findTravelSpan();
    if (!travelSpan) {
      // console.log("ZED Explore Helper: Travel span not found");
      return;
    }

    // Get parent (should be button)
    const parentButton = travelSpan.parentElement;
    if (!parentButton) {
      // console.log("ZED Explore Helper: Parent button not found");
      return;
    }

    // Get grandparent (parent of button)
    const grandparent = parentButton.parentElement;
    if (!grandparent) {
      // console.log("ZED Explore Helper: Grandparent element not found");
      return;
    }

    // Create and insert the hello box
    const mainBox = createExploreHelperBox();
    grandparent.insertBefore(mainBox, grandparent.firstChild);
    // console.log("ZED Explore Helper: Main box inserted successfully");
  }

  // Initialize the helper
  function init() {
    if (!isExplorePage()) {
      // console.log("ZED Explore Helper: Not an explore page, skipping");
      return;
    }

    // console.log(
    //   "ZED Explore Helper: Initializing on",
    //   window.location.pathname
    // );

    // Wait for the Travel span to appear, then insert the box
    const checkForTravelSpan = () => {
      const travelSpan = findTravelSpan();
      if (travelSpan) {
        insertBox();
        return true;
      }
      return false;
    };

    // Try immediately
    if (checkForTravelSpan()) {
      return;
    }

    // Otherwise, use MutationObserver to wait for it
    const observer = new MutationObserver(() => {
      if (checkForTravelSpan()) {
        observer.disconnect();
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });

    // Timeout after 15 seconds
    setTimeout(() => {
      observer.disconnect();
      // console.log("ZED Explore Helper: Timeout waiting for Travel span");
    }, 15000);
  }

  // Handle traveling page - add Update Inventory button
  function handleTravelingPage() {
    if (!isTravelingPage()) {
      return;
    }

    // Wait for the main-text element to check if it says "You are traveling"
    waitForElement("div.main-text.zed-font.text-no-bg")
      .then(() => {
        const mainTextDiv = document.querySelector(
          "div.main-text.zed-font.text-no-bg"
        );
        if (!mainTextDiv) {
          return;
        }

        const textContent = mainTextDiv.textContent?.trim();
        if (textContent === "You are traveling") {
          // Wait for travel-line element
          waitForElement("div.travel-line")
            .then(() => {
              const travelLineDiv = document.querySelector("div.travel-line");
              if (!travelLineDiv) {
                return;
              }

              // Check if button already exists to avoid duplicates
              const existingButton = travelLineDiv.parentElement?.querySelector(
                `.${TRAVELING_BTN_CLASS}`
              );
              if (existingButton) {
                return;
              }

              // Create the Update Inventory button
              const button = document.createElement("button");
              button.className = TRAVELING_BTN_CLASS;
              button.textContent = "Update Inventory";
              button.style.cssText = `
                background: rgba(60, 139, 64, 0.6);
                color: white;
                border: none;
                border-radius: 4px;
                padding: 5px 10px;
                cursor: pointer;
                margin-bottom: 8px;
                margin-top: 8px;
                font-weight: 500;
                font-size: 12px;
                transition: all 0.3s ease;
              `;
              //hover effect
              button.addEventListener("mouseenter", () => {
                button.style.backgroundColor = "rgba(60, 139, 64, 0.9)";
              });
              button.addEventListener("mouseleave", () => {
                button.style.backgroundColor = "rgba(60, 139, 64, 0.6)";
              });

              // Click handler - refresh the page
              button.addEventListener("click", () => {
                window.location.reload();
              });

              // Insert button before travel-line
              if (travelLineDiv.parentElement) {
                travelLineDiv.parentElement.insertBefore(button, travelLineDiv);
              }
            })
            .catch(() => {
              // Travel line not found
            });
        }
      })
      .catch(() => {
        // Main text not found
      });
  }

  // Track current pathname for SPA navigation
  let currentPathname = window.location.pathname;

  // Handle URL changes (SPA navigation)
  function handleUrlChange() {
    const newPathname = window.location.pathname;
    if (newPathname !== currentPathname) {
      currentPathname = newPathname;
      // console.log("ZED Explore Helper: URL changed to", currentPathname);

      // Clear refresh interval when navigating away
      if (refreshInterval) {
        clearInterval(refreshInterval);
        refreshInterval = null;
      }

      // Reset caches and references
      itemsCache = null;
      vehicleItemsCache = null;
      currentListContainer = null;
      currentSearchInput = null;

      // Remove existing box if navigating away
      const existingBox = document.querySelector(`.${HELPER_CLASS}`);
      if (existingBox) {
        existingBox.remove();
      }

      // Reinitialize if on explore page
      if (isExplorePage()) {
        setTimeout(init, 500);
      }

      // Handle traveling page
      if (isTravelingPage()) {
        setTimeout(handleTravelingPage, 500);
      }
    }
  }

  // Intercept pushState and replaceState for SPA navigation
  const originalPushState = history.pushState;
  const originalReplaceState = history.replaceState;

  history.pushState = function () {
    originalPushState.apply(history, arguments);
    setTimeout(handleUrlChange, 100);
  };

  history.replaceState = function () {
    originalReplaceState.apply(history, arguments);
    setTimeout(handleUrlChange, 100);
  };

  // Listen for browser back/forward navigation
  window.addEventListener("popstate", handleUrlChange);

  // Watch for SPA navigation changes
  helperObserver = new MutationObserver(() => {
    if (window.location.pathname !== currentPathname) {
      handleUrlChange();
    }
    // Re-check for Travel span if on explore page and box doesn't exist
    if (isExplorePage() && !document.querySelector(`.${HELPER_CLASS}`)) {
      const travelSpan = findTravelSpan();
      if (travelSpan) {
        insertBox();
      }
    }
    // Re-check for traveling page button
    if (isTravelingPage() && !document.querySelector(`.${TRAVELING_BTN_CLASS}`)) {
      handleTravelingPage();
    }
  });

  // Run on page load
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () => {
      init();
      handleTravelingPage();
    });
  } else {
    init();
    handleTravelingPage();
  }

  // Start observing for SPA navigation changes
  helperObserver.observe(document.body, {
    childList: true,
    subtree: true,
  });
})();