Nova Client

Customizable Mod menu for Survev.io.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Nova Client
// @namespace    https://github.com/karizzmaa/nova-client/
// @version      2.1.3
// @description  Customizable Mod menu for Survev.io.
// @author       karizzmaa
// @match        *://survev.io/*
// @match        *://66.179.254.36/*
// @match        *://185.126.158.61/*
// @match        *://resurviv.biz/*
// @match        *://survev.github.io/survev/*
// @match        *://survivx.org/*
// @match        *://localhost:3000/*
// @match        *://eu-comp.net/*
// @match        *://cursev.io/*
// @match        *://uno.cheap/*
// @exclude      https://survev.io/stats/
// @exclude      https://survev.io/changelog
// @exclude      https://survev.io/privacy
// @exclude      https://survev.io/changelogRec
// @exclude      https://survev.io/hof
// @exclude      https://survev.io/attribution.txt
// @grant        GM_addStyle
// @icon         https://raw.githubusercontent.com/karizzmaa/nova-client/refs/heads/main/icon.png
// ==/UserScript==

(function () {
  "use strict";

  const defaultBackgrounds = [
    {
      id: "b1",
      name: "Turkey",
      data: "https://raw.githubusercontent.com/survev/survev/refs/heads/master/client/public/img/main_splash_turkey_01.png",
      builtIn: true,
    },
    {
      id: "b2",
      name: "Easter",
      data: "https://github.com/survev/survev/blob/master/client/public/img/main_splash_easter.png?raw=true",
      builtIn: true,
    },
    {
      id: "b3",
      name: "Desert",
      data: "https://github.com/survev/survev/blob/master/client/public/img/main_splash_desert_01.png?raw=true",
      builtIn: true,
    },
    {
      id: "b4",
      name: "Halloween",
      data: "https://raw.githubusercontent.com/survev/survev/refs/heads/master/client/public/img/main_splash_halloween.png",
      builtIn: true,
    },
    {
      id: "b5",
      name: "Cobalt",
      data: "https://github.com/survev/survev/blob/master/client/public/img/main_splash_cobalt.png?raw=true",
      builtIn: true,
    },
    {
      id: "b10",
      name: "Main",
      data: "https://raw.githubusercontent.com/survev/survev/refs/heads/master/client/public/img/main_splash.png",
      builtIn: true,
    },
  ];

  const defaultConfig = {
    fps: false,
    ping: false,
    hpAd: false,
    uncap: false,
    fpsLimit: 0,
    glass: true,
    fastMenu: false,
    cleanMenu: false,
    hideAccountBlock: false,
    useClassicLogo: false,
    autoFS: false,
    activeCrosshair: null,
    customCrosshairs: [],
    activeBackground: "b10",
    customBackgrounds: [],
    activeKeybindId: null,
    customKeybinds: [],
    shuffleEnabled: false,
    fpsPos: { top: "60%", left: "10px" },
    pingPos: { top: "65%", left: "10px" },
    hpAdPos: { top: "70%", left: "10px" },
    customLabels: [],
    onlyShowLabelsIngame: false,
    nameRandomizer: false,
    nameInterval: 0.1,
    randomNames: ["Player", "NovaUser", "Pro"],
    autoHideMinimap: false,

  };

  let config = JSON.parse(localStorage.getItem("nova_config")) || defaultConfig;
  config = { ...defaultConfig, ...config };

  if (!config.fpsPos) config.fpsPos = defaultConfig.fpsPos;
  if (!config.pingPos) config.pingPos = defaultConfig.pingPos;
  if (!config.hpAdPos) config.hpAdPos = defaultConfig.hpAdPos;
  if (!config.customLabels) config.customLabels = [];
  if (!config.customKeybinds) config.customKeybinds = [];

  let shuffleInterval = null;

  function saveConfig() {
    localStorage.setItem("nova_config", JSON.stringify(config));
  }

  function updateGameKeybinds(bindString) {
    let gameConfig = JSON.parse(localStorage.getItem("surviv_config")) || {};
    gameConfig.binds = bindString;
    localStorage.setItem("surviv_config", JSON.stringify(gameConfig));
  }

  let fpsDisplay = null;
  let fpsAnimationId = null;
  let pingDisplay = null;
  let hpAdDisplay = null;
  let hpAdInterval = null;
  let ws = null;
  const originalRAF = window.requestAnimationFrame;
  function applyFPSLimiter() {
    if (!config.uncap) {
      window.requestAnimationFrame = originalRAF;
      return;
    }

    const limit = parseInt(config.fpsLimit) || 0;

    if (limit <= 0) {
      window.requestAnimationFrame = (cb) => setTimeout(cb, 1);
      return;
    }

    const frameTime = 1000 / limit;
    let last = 0;

    window.requestAnimationFrame = (cb) => {
      const now = performance.now();
      const delta = now - last;

      if (delta >= frameTime) {
        last = now;
        cb(now);
      } else {
        setTimeout(() => window.requestAnimationFrame(cb), frameTime - delta);
      }
    };
  }

  GM_addStyle(`
        #nova-menu {
            position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.7);
            width: 580px; height: 500px; background: rgba(20, 20, 20, 0.85);
            border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px;
            color: white; font-family: 'Segoe UI', system-ui, sans-serif;
            display: none; flex-direction: column; overflow: hidden; z-index: 10000;
            opacity: 0; box-shadow: 0 25px 50px rgba(0,0,0,0.5); user-select: none;
        }
        .nova-animate { transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease; }
        .nova-glass { backdrop-filter: blur(20px) saturate(180%); background: rgba(20, 20, 20, 0.6) !important; }
        #nova-menu.active { display: flex; opacity: 1; transform: translate(-50%, -50%) scale(1); }
        #nova-header { padding: 14px 20px; background: rgba(255, 255, 255, 0.05); display: flex; justify-content: space-between; align-items: center; cursor: move; }
        #nova-nav { display: flex; gap: 8px; padding: 10px 15px; background: rgba(0, 0, 0, 0.2); border-bottom: 1px solid rgba(255, 255, 255, 0.05); overflow-x: auto; }
        .nav-item { padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 12px; white-space: nowrap; color: rgba(255, 255, 255, 0.6); transition: 0.2s; }
        .nav-item.active { background: rgba(255, 255, 255, 0.12); color: #60cdff; font-weight: 600; }
        #nova-content { padding: 20px; flex-grow: 1; overflow-y: auto; position: relative; }
        .cat-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
        .cat-title { font-size: 20px; font-weight: 700; }
        .item-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; }
        .item-card {
            background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1);
            border-radius: 8px; aspect-ratio: 1/1; display: flex; flex-direction: column;
            align-items: center; justify-content: center; position: relative; cursor: pointer; transition: 0.2s; overflow: hidden;
        }
        .item-card:hover { background: rgba(255,255,255,0.1); border-color: #60cdff; }
        .item-card.active { border: 2px solid #60cdff; background: rgba(96, 205, 255, 0.1); }
        .preview-img { width: 100%; height: 70%; object-fit: cover; pointer-events: none; opacity: 0.8; }
        .xhair-preview { width: 42px; height: 42px; object-fit: contain; margin-bottom: 10px; }
        .item-name { font-size: 11px; margin-top: 5px; opacity: 0.9; text-align: center; padding: 0 5px; }
        .shuffle-btn { width: 32px; height: 32px; cursor: pointer; border-radius: 6px; padding: 6px; transition: 0.2s; background: rgba(255,255,255,0.05); }
        .shuffle-btn:hover { background: rgba(96, 205, 255, 0.2); }
        .shuffle-btn.active { background: #60cdff; }
        .shuffle-icon { width: 100%; height: 100%; transition: filter 0.3s; }
        .inverted-icon { filter: invert(1); }
        .add-btn { border: 2px dashed rgba(255,255,255,0.2); background: transparent; }
        .item-actions { position: absolute; top: 5px; right: 5px; display: flex; gap: 4px; opacity: 0; transition: 0.2s; }
        .item-card:hover .item-actions { opacity: 1; }
        .action-btn { background: rgba(0,0,0,0.6); border-radius: 4px; padding: 2px 5px; font-size: 10px; color: white; }
        .action-btn:hover { background: #60cdff; color: black; }
        .tweak-card { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: rgba(255, 255, 255, 0.04); border-radius: 8px; margin-bottom: 8px; }
        .win-switch { position: relative; display: inline-block; width: 44px; height: 22px; }
        .win-switch input { opacity: 0; width: 0; height: 0; }
        .win-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; border: 2px solid rgba(255, 255, 255, 0.5); transition: .2s; border-radius: 22px; }
        .win-slider:before { position: absolute; content: ""; height: 12px; width: 12px; left: 4px; bottom: 3px; background-color: rgba(255, 255, 255, 0.8); transition: .2s; border-radius: 50%; }
        input:checked + .win-slider { background-color: #60cdff; border-color: #60cdff; }
        input:checked + .win-slider:before { transform: translateX(20px); background-color: #000; }
        .nova-hidden { display: none !important; }
        .nova-aligned-bar { position: relative !important; margin-top: 60px !important; display: flex !important; justify-content: center !important; }
        .move-btn { margin-right: 10px; background: rgba(255,255,255,0.1); border:none; color:white; border-radius:4px; padding: 4px 8px; cursor: pointer; transition:0.2s; font-size:11px;}
        .move-btn:hover { background: #60cdff; color:black; }
        #edit-hud { position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%); background: rgba(20,20,20,0.9); border: 1px solid rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 30px; display: none; gap: 10px; z-index: 10002; backdrop-filter: blur(10px); }
        .hud-btn { padding: 8px 20px; border-radius: 20px; border:none; cursor:pointer; font-weight:600; }
        .hud-btn.reset { background: rgba(255,50,50,0.2); color: #ff6b6b; }
        .hud-btn.done { background: #60cdff; color: black; }
        .nova-label { position: fixed; color: white; font-size: 14px; text-shadow: 1px 1px 2px black; background: rgba(0,0,0,0.3); padding: 3px 8px; border-radius: 5px; z-index: 10001; pointer-events: none; user-select: none; }
        .draggable { pointer-events: auto !important; cursor: grab; border: 2px dashed #60cdff; background: rgba(96,205,255,0.2) !important; }
        .draggable:active { cursor: grabbing; }
        .nova-modal { position: absolute; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.8); display:flex; justify-content:center; align-items:center; z-index: 10; }
        .modal-box { background: #1a1a1a; padding: 20px; border-radius: 12px; border: 1px solid rgba(255,255,255,0.1); width: 300px; display:flex; flex-direction:column; gap:10px; }
        .modal-input { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); color: white; padding: 8px; border-radius: 4px; outline: none; }
        .modal-row { display: flex; justify-content: space-between; align-items: center; }
        .color-picker { width: 50px; height: 30px; border: none; cursor: pointer; }
        .bind-icon { width: 40px; height: 40px; margin-bottom: 10px; opacity: 0.7; }
        .nova-clean-centered {   left: 50% !important;    right: auto !important;    transform: translateX(-50%) !important; display: flex !important;    justify-content: center !important;    align-items: center !important;
}

    `);

  const glassStyleId = "glassmorphism-start-menu-bg-only";
  const glassCSS = `
        #start-menu{
            background:rgba(25,25,25,.45)!important;
            backdrop-filter:blur(14px)saturate(130%);
            -webkit-backdrop-filter:blur(14px)saturate(130%);
            border-radius:18px;
            border:1px solid rgba(255,255,255,.15);
            box-shadow:0 8px 30px rgba(0,0,0,.5),inset 0 0 0 1px rgba(255,255,255,.04)
        }
        #start-menu *{
            backdrop-filter:none!important;
            -webkit-backdrop-filter:none!important
        }`;

  function toggleGlassStyle(enabled) {
    let existing = document.getElementById(glassStyleId);
    if (enabled) {
      if (!existing) {
        const s = document.createElement("style");
        s.id = glassStyleId;
        s.textContent = glassCSS;
        document.head.appendChild(s);
      }
    } else {
      if (existing) existing.remove();
    }
  }

  new MutationObserver(() => {
    if (config.glass && document.querySelector("#start-menu")) {
      toggleGlassStyle(true);
    }
  }).observe(document.documentElement, { childList: true, subtree: true });

  function applyCrosshair(base64) {
    const target =
      document.querySelector("#game-area-wrapper") ||
      document.querySelector("canvas");
    if (target) target.style.cursor = `url(${base64}) 16 16, auto`;
  }

  function applyBackground(url) {
    const bg = document.querySelector("#background");
    if (!bg) return;
    bg.style.backgroundImage = `url("${url}")`;
  }

  function doShuffle() {
    const pool = [...defaultBackgrounds, ...config.customBackgrounds];
    const randomBg = pool[Math.floor(Math.random() * pool.length)];
    config.activeBackground = randomBg.id;
    saveConfig();
    applyBackground(randomBg.data);
  }

  function toggleShuffle(enabled) {
    config.shuffleEnabled = enabled;
    saveConfig();
    if (enabled) {
      if (!shuffleInterval) shuffleInterval = setInterval(doShuffle, 600000);
    } else {
      clearInterval(shuffleInterval);
      shuffleInterval = null;
    }
  }

  function extractBase64(input) {
    const match = input.match(/data:image\/[a-zA-Z]+;base64,[^'")\s]+/);
    return match ? match[0] : input;
  }

  const editHud = document.createElement("div");
  editHud.id = "edit-hud";
  editHud.innerHTML = `<button class="hud-btn reset">Reset</button><button class="hud-btn done">Done</button>`;
  document.body.appendChild(editHud);

  function enterEditMode(element, configKey, defaultPos, onDone) {
    if (!element) return;
    toggleMenu(false);
    editHud.style.display = "flex";
    element.classList.add("draggable");
    let isDragging = false;
    let startX, startY, initialLeft, initialTop;
    const onMouseDown = (e) => {
      isDragging = true;
      startX = e.clientX;
      startY = e.clientY;
      initialLeft = element.offsetLeft;
      initialTop = element.offsetTop;
      e.preventDefault();
    };
    const onMouseMove = (e) => {
      if (!isDragging) return;
      const dx = e.clientX - startX;
      const dy = e.clientY - startY;
      element.style.left = `${initialLeft + dx}px`;
      element.style.top = `${initialTop + dy}px`;
      element.style.transform = "none";
    };
    const onMouseUp = () => {
      isDragging = false;
    };
    element.addEventListener("mousedown", onMouseDown);
    window.addEventListener("mousemove", onMouseMove);
    window.addEventListener("mouseup", onMouseUp);
    editHud.querySelector(".reset").onclick = () => {
      element.style.top = defaultPos.top;
      element.style.left = defaultPos.left;
      if (defaultPos.top.includes("%"))
        element.style.transform = "translateY(-50%)";
    };
    editHud.querySelector(".done").onclick = () => {
      if (configKey) {
        if (typeof configKey === "string") {
          config[configKey] = {
            top: element.style.top,
            left: element.style.left,
          };
        } else if (
          typeof configKey === "object" &&
          configKey.type === "label"
        ) {
          const lbl = config.customLabels.find((l) => l.id === configKey.id);
          if (lbl) {
            lbl.top = element.style.top;
            lbl.left = element.style.left;
          }
        }
        saveConfig();
      }
      element.classList.remove("draggable");
      element.removeEventListener("mousedown", onMouseDown);
      window.removeEventListener("mousemove", onMouseMove);
      window.removeEventListener("mouseup", onMouseUp);
      editHud.style.display = "none";
      toggleMenu(true);
      if (onDone) onDone();
    };
  }

  function toggleFPS(enabled) {
    config.fps = enabled;
    saveConfig();
    if (enabled && !fpsDisplay) {
      fpsDisplay = document.createElement("div");
      fpsDisplay.className = "nova-label";
      fpsDisplay.style.top = config.fpsPos.top;
      fpsDisplay.style.left = config.fpsPos.left;
      if (config.fpsPos.top.includes("%"))
        fpsDisplay.style.transform = "translateY(-50%)";
      document.body.appendChild(fpsDisplay);
      applyLabelContainerMode();
      let times = [];
      const run = () => {
        fpsAnimationId = requestAnimationFrame(() => {
          const now = performance.now();
          while (times.length > 0 && times[0] <= now - 1000) times.shift();
          times.push(now);
          if (fpsDisplay) {
            fpsDisplay.innerHTML = `${times.length} FPS`;
            run();
          }
        });
      };
      run();
    } else if (!enabled && fpsDisplay) {
      fpsDisplay.remove();
      fpsDisplay = null;
      cancelAnimationFrame(fpsAnimationId);
    }
  }

  function togglePing(enabled) {
    config.ping = enabled;
    saveConfig();
    if (enabled && !pingDisplay) {
      pingDisplay = document.createElement("div");
      pingDisplay.className = "nova-label";
      pingDisplay.innerHTML = "Ping: -- ms";
      pingDisplay.style.top = config.pingPos.top;
      pingDisplay.style.left = config.pingPos.left;
      if (config.pingPos.top.includes("%"))
        pingDisplay.style.transform = "translateY(-50%)";
      document.body.appendChild(pingDisplay);
      applyLabelContainerMode();
      initPingSocket();
    } else if (!enabled && pingDisplay) {
      if (ws) ws.close();
      if (pingDisplay) pingDisplay.remove();
      pingDisplay = null;
    }
  }
  function toggleHPAD(enabled) {
    config.hpAd = enabled;
    saveConfig();

    if (enabled && !hpAdDisplay) {
      hpAdDisplay = document.createElement("div");
      hpAdDisplay.className = "nova-label";
      hpAdDisplay.innerHTML = "HP: -- | AD: --";

      hpAdDisplay.style.top = config.hpAdPos.top;
      hpAdDisplay.style.left = config.hpAdPos.left;

      if (config.hpAdPos.top.includes("%"))
        hpAdDisplay.style.transform = "translateY(-50%)";

      document.body.appendChild(hpAdDisplay);
      applyLabelContainerMode();

      hpAdInterval = setInterval(() => {
        const hpEl = document.getElementById("ui-health-actual");
        let hp = 0;
        if (hpEl) hp = parseFloat(hpEl.style.width) || 0;

        const getBoost = (id) => {
          const el = document.querySelector(`#${id} .ui-bar-inner`);
          return el ? parseFloat(el.style.width) || 0 : 0;
        };

        const boost0 = getBoost("ui-boost-counter-0");
        const boost1 = getBoost("ui-boost-counter-1");
        const boost2 = getBoost("ui-boost-counter-2");
        const boost3 = getBoost("ui-boost-counter-3");

        const adr =
          boost0 * 0.25 + boost1 * 0.25 + boost2 * 0.375 + boost3 * 0.125;

        hpAdDisplay.innerHTML = `HP: ${Math.round(hp)} | AD: ${Math.round(adr)}`;
      }, 100);
    } else if (!enabled && hpAdDisplay) {
      clearInterval(hpAdInterval);
      hpAdDisplay.remove();
      hpAdDisplay = null;
    }
  }

  function initPingSocket() {
    const getWsUrl = () => {
      const reg = document.getElementById("server-select-main")?.value || "na";
      const map = { na: "usr", eu: "eur", asia: "asr", sa: "sa" };
      return `wss://${map[reg] || "usr"}.mathsiscoolfun.com:8001/ptc`;
    };
    const startPing = () => {
      if (ws) ws.close();
      ws = new WebSocket(getWsUrl());
      let sendTime;
      ws.onopen = () => {
        ws.send(new ArrayBuffer(1));
        sendTime = Date.now();
      };
      ws.onmessage = () => {
        const diff = Date.now() - sendTime;
        if (pingDisplay) {
          pingDisplay.innerHTML = `Ping: ${diff} ms`;
          pingDisplay.style.color =
            diff > 120 ? "#ff4d4d" : diff > 80 ? "#ffa500" : "white";
        }
        setTimeout(() => {
          if (ws && ws.readyState === 1) {
            sendTime = Date.now();
            ws.send(new ArrayBuffer(1));
          }
        }, 1500);
      };
      ws.onclose = () => {
        if (config.ping && pingDisplay) pingDisplay.innerHTML = "Ping: Offline";
      };
      ws.onerror = () => {
        if (pingDisplay) pingDisplay.innerHTML = "Ping: Error";
      };
    };
    document.addEventListener("click", (e) => {
      if (
        e.target.classList?.contains("btn-green") ||
        e.target.id === "btn-start-team"
      ) {
        setTimeout(startPing, 1000);
      }
    });
  }

  function renderCustomLabels() {
    document.querySelectorAll(".nova-custom-lbl").forEach((e) => e.remove());
    config.customLabels.forEach((lbl) => {
      const el = document.createElement("div");
      el.className = "nova-label nova-custom-lbl";
      el.id = `lbl-${lbl.id}`;
      el.innerText = lbl.text;
      el.style.color = lbl.color;
      if (lbl.bold) el.style.fontWeight = "bold";
      if (lbl.italic) el.style.fontStyle = "italic";
      el.style.top = lbl.top;
      el.style.left = lbl.left;
      document.body.appendChild(el);
    });
  }

  function openLabelCreator() {
    const modal = document.createElement("div");
    modal.className = "nova-modal";
    modal.innerHTML = `
            <div class="modal-box">
                <h3 style="margin:0">Create Label</h3>
                <input class="modal-input" type="text" id="lbl-text" placeholder="Label Text (e.g. Nickname)">
                <div class="modal-row"><span>Color:</span><input type="color" id="lbl-color" value="#ffffff" class="color-picker"></div>
                <div class="modal-row"><label><input type="checkbox" id="lbl-bold"> Bold</label><label><input type="checkbox" id="lbl-italic"> Italic</label></div>
                <div class="modal-row" style="margin-top:10px"><button class="hud-btn reset" id="lbl-cancel">Cancel</button><button class="hud-btn done" id="lbl-save">Create & Place</button></div>
            </div>
        `;
    menu.appendChild(modal);
    modal.querySelector("#lbl-cancel").onclick = () => modal.remove();
    modal.querySelector("#lbl-save").onclick = () => {
      const text = modal.querySelector("#lbl-text").value;
      if (!text) return;
      const newLabel = {
        id: Date.now(),
        text,
        color: modal.querySelector("#lbl-color").value,
        bold: modal.querySelector("#lbl-bold").checked,
        italic: modal.querySelector("#lbl-italic").checked,
        top: "50%",
        left: "50%",
      };
      config.customLabels.push(newLabel);
      saveConfig();
      renderCustomLabels();
      setTimeout(applyLabelContainerMode, 1000);
      modal.remove();
      const el = document.getElementById(`lbl-${newLabel.id}`);
      enterEditMode(
        el,
        { type: "label", id: newLabel.id },
        { top: "50%", left: "50%" },
        () => loadCategory("Labels"),
      );
    };
  }

  function toggleAutoFS(enabled) {
    config.autoFS = enabled;
    saveConfig();
    if (enabled) {
      const fs = () => {
        if (!document.fullscreenElement)
          document.documentElement.requestFullscreen().catch(() => {});
        window.removeEventListener("mousedown", fs);
      };
      window.addEventListener("mousedown", fs);
    }
  }

  function toggleCleanMenu(enabled) {
    config.cleanMenu = enabled;
    saveConfig();

    const targets = [
      "#news-block",
      "#left-column",
      "#social-share-block",
      'a[href*="privacy"]',
      'a[href*="changelog"]',
      ".language-select-wrap",
    ];

    targets.forEach((s) =>
      document
        .querySelectorAll(s)
        .forEach((el) =>
          enabled
            ? el.classList.add("nova-hidden")
            : el.classList.remove("nova-hidden"),
        ),
    );

    const bar = document.getElementById("start-bottom-right");
    const menuEl = document.getElementById("start-menu");

    if (bar && menuEl) {
      if (enabled) {
        bar.classList.add("nova-aligned-bar");
        bar.classList.add("nova-clean-centered");
        menuEl.appendChild(bar);
      } else {
        bar.classList.remove("nova-aligned-bar");
        bar.classList.remove("nova-clean-centered");
        document.body.appendChild(bar);
      }
    }
  }
  function toggleAccountBlock(enabled) {
    config.hideAccountBlock = enabled;
    saveConfig();

    const selector = ".account-block";
    document.querySelectorAll(selector).forEach((el) => {
      if (enabled) {
        el.classList.add("nova-hidden");
      } else {
        el.classList.remove("nova-hidden");
      }
    });
  }
  function toggleClassicLogo(enabled) {
    config.useClassicLogo = enabled;
    saveConfig();

    const OLD_LOGO = "https://survev.io/img/survev_logo_full.png";
    const NEW_LOGO = "https://survev.io/img/surviv_logo_full.png";

    const targetSrc = enabled ? OLD_LOGO : NEW_LOGO;
    const replacementSrc = enabled ? NEW_LOGO : OLD_LOGO;

    document.querySelectorAll("img").forEach((img) => {
      if (img.src === targetSrc) {
        img.src = replacementSrc;
      }
    });

    document.querySelectorAll("*").forEach((el) => {
      const bg = getComputedStyle(el).backgroundImage;
      if (bg.includes(targetSrc)) {
        el.style.backgroundImage = bg.replace(targetSrc, replacementSrc);
      }
    });
  }

  let nameIntervalId = null;

  function applyRandomName() {
    const input = document.querySelector("#player-name-input-solo");
    if (!input || !config.randomNames.length) return;

    const randomName =
      config.randomNames[Math.floor(Math.random() * config.randomNames.length)];

    input.value = randomName;
    input.dispatchEvent(new Event("input", { bubbles: true }));
  }

  function toggleNameRandomizer(enabled) {
    config.nameRandomizer = enabled;
    saveConfig();

    if (enabled) {
      if (nameIntervalId) clearInterval(nameIntervalId);
      nameIntervalId = setInterval(
        () => {
          applyRandomName();
        },
        (parseFloat(config.nameInterval) || 0.1) * 60 * 1000,
      );

      setTimeout(applyRandomName, 1500);
    } else {
      clearInterval(nameIntervalId);
      nameIntervalId = null;
    }
  }
    let autoHideMinimapInterval = null;
let minimapHiddenThisMatch = false;

function toggleAutoHideMinimap(enabled) {
    config.autoHideMinimap = enabled;
    saveConfig();

    if (enabled) {

        autoHideMinimapInterval = setInterval(() => {
            const btn = document.getElementById("ui-map-minimize");

            if (btn && btn.offsetParent !== null) {

                if (!minimapHiddenThisMatch) {
                    btn.click();
                    minimapHiddenThisMatch = true;
                }

            } else {
                minimapHiddenThisMatch = false;
            }

        }, 50);

    } else {

        clearInterval(autoHideMinimapInterval);
        autoHideMinimapInterval = null;
        minimapHiddenThisMatch = false;

    }
}


  function loadCategory(cat) {
    contentArea.innerHTML = `
            <div class="cat-header">
                <div class="cat-title">${cat}</div>
                ${
                  cat === "Backgrounds"
                    ? `
                    <div id="shuffle-trigger" class="shuffle-btn ${config.shuffleEnabled ? "active" : ""}" title="Auto-Shuffle Every 10 Mins">
                        <img src="https://www.svgrepo.com/show/533712/shuffle.svg" class="shuffle-icon ${!config.glass ? "inverted-icon" : ""}">
                    </div>
                `
                    : ""
                }
            </div>
        `;
    const grid = document.createElement("div");
    grid.className = "item-grid";

    if (cat === "Crosshairs") {
      const addBtn = document.createElement("div");
      addBtn.className = "item-card add-btn";
      addBtn.innerHTML = `<span style="font-size:30px">+</span><span class="item-name">Add Crosshair</span>`;
      addBtn.onclick = () => {
        const name = prompt("Name:");
        if (!name) return;
        const input = prompt("Paste Bookmarklet or Base64:");
        if (!input) return;
        config.customCrosshairs.push({
          id: Date.now(),
          name,
          data: extractBase64(input),
        });
        saveConfig();
        loadCategory("Crosshairs");
      };
      grid.appendChild(addBtn);
      config.customCrosshairs.forEach((xh) => {
        const card = document.createElement("div");
        card.className = `item-card ${config.activeCrosshair === xh.id ? "active" : ""}`;
        card.innerHTML = `<img class="xhair-preview" src="${xh.data}"><span class="item-name">${xh.name}</span><div class="item-actions"><div class="action-btn edit">Edit</div><div class="action-btn del">Del</div></div>`;
        card.onclick = () => {
          config.activeCrosshair = xh.id;
          saveConfig();
          applyCrosshair(xh.data);
          loadCategory("Crosshairs");
        };
        card.querySelector(".edit").onclick = (e) => {
          e.stopPropagation();
          const n = prompt("New Name:", xh.name);
          if (n) xh.name = n;
          saveConfig();
          loadCategory("Crosshairs");
        };
        card.querySelector(".del").onclick = (e) => {
          e.stopPropagation();
          config.customCrosshairs = config.customCrosshairs.filter(
            (i) => i.id !== xh.id,
          );
          saveConfig();
          loadCategory("Crosshairs");
        };
        grid.appendChild(card);
      });
      contentArea.appendChild(grid);
    } else if (cat === "Keybinds") {
      const addBtn = document.createElement("div");
      addBtn.className = "item-card add-btn";
      addBtn.innerHTML = `<span style="font-size:30px">+</span><span class="item-name">Add Profile</span>`;
      addBtn.onclick = () => {
        const name = prompt("Profile Name:");
        if (!name) return;
        const bindStr = prompt("Paste Keybind String");
        if (!bindStr) return;
        config.customKeybinds.push({ id: Date.now(), name, data: bindStr });
        saveConfig();
        loadCategory("Keybinds");
      };
      grid.appendChild(addBtn);
      config.customKeybinds.forEach((kb) => {
        const card = document.createElement("div");
        card.className = `item-card ${config.activeKeybindId === kb.id ? "active" : ""}`;
        card.innerHTML = `<img class="bind-icon" src="https://www.svgrepo.com/show/347645/keyboard.svg" style="${!config.glass ? "filter:invert(1)" : ""}"><span class="item-name">${kb.name}</span><div class="item-actions"><div class="action-btn edit">Edit</div><div class="action-btn del">Del</div></div>`;
        card.onclick = () => {
          config.activeKeybindId = kb.id;
          saveConfig();
          updateGameKeybinds(kb.data);
          alert(`Applied Keybind Profile: ${kb.name}`);
          loadCategory("Keybinds");
        };
        card.querySelector(".edit").onclick = (e) => {
          e.stopPropagation();
          const n = prompt("New Name:", kb.name);
          if (n) kb.name = n;
          const d = prompt("New Bind String:", kb.data);
          if (d) kb.data = d;
          saveConfig();
          loadCategory("Keybinds");
        };
        card.querySelector(".del").onclick = (e) => {
          e.stopPropagation();
          config.customKeybinds = config.customKeybinds.filter(
            (i) => i.id !== kb.id,
          );
          saveConfig();
          loadCategory("Keybinds");
        };
        grid.appendChild(card);
      });
      contentArea.appendChild(grid);
    } else if (cat === "Backgrounds") {
      const shuffleBtn = contentArea.querySelector("#shuffle-trigger");
      shuffleBtn.onclick = () => {
        const newState = !config.shuffleEnabled;
        toggleShuffle(newState);
        loadCategory("Backgrounds");
      };
      const addBtn = document.createElement("div");
      addBtn.className = "item-card add-btn";
      addBtn.innerHTML = `<span style="font-size:30px">+</span><span class="item-name">Add Background</span>`;
      addBtn.onclick = () => {
        const choice = confirm("Press OK for URL or Cancel for File Upload");
        const name = prompt("Name:");
        if (!name) return;
        if (choice) {
          const url = prompt("Paste Image URL:");
          if (url) {
            config.customBackgrounds.push({ id: Date.now(), name, data: url });
            saveConfig();
            loadCategory("Backgrounds");
          }
        } else {
          const input = document.createElement("input");
          input.type = "file";
          input.accept = "image/*";
          input.onchange = (e) => {
            const reader = new FileReader();
            reader.onload = () => {
              config.customBackgrounds.push({
                id: Date.now(),
                name,
                data: reader.result,
              });
              saveConfig();
              loadCategory("Backgrounds");
            };
            reader.readAsDataURL(e.target.files[0]);
          };
          input.click();
        }
      };
      grid.appendChild(addBtn);
      [...defaultBackgrounds, ...config.customBackgrounds].forEach((bg) => {
        const card = document.createElement("div");
        card.className = `item-card ${config.activeBackground === bg.id ? "active" : ""}`;
        card.innerHTML = `<img class="preview-img" src="${bg.data}"><span class="item-name">${bg.name}</span>`;
        if (!bg.builtIn) {
          card.innerHTML += `<div class="item-actions"><div class="action-btn edit">Edit</div><div class="action-btn del">Del</div></div>`;
          card.querySelector(".edit").onclick = (e) => {
            e.stopPropagation();
            const n = prompt("New Name:", bg.name);
            if (n) bg.name = n;
            saveConfig();
            loadCategory("Backgrounds");
          };
          card.querySelector(".del").onclick = (e) => {
            e.stopPropagation();
            config.customBackgrounds = config.customBackgrounds.filter(
              (i) => i.id !== bg.id,
            );
            saveConfig();
            loadCategory("Backgrounds");
          };
        }
        card.onclick = () => {
          config.activeBackground = bg.id;
          saveConfig();
          location.reload();
          applyBackground(bg.data);
          loadCategory("Backgrounds");
        };
        grid.appendChild(card);
      });
      contentArea.appendChild(grid);
    } else if (cat === "Labels") {
      contentArea.appendChild(
        createTweak("FPS Counter", "fps", config.fps, toggleFPS, {
          text: "Move",
          action: () => {
            if (!config.fps) {
              toggleFPS(true);
              config.fps = true;
              saveConfig();
              loadCategory("Labels");
            }
            enterEditMode(fpsDisplay, "fpsPos", defaultConfig.fpsPos, () =>
              loadCategory("Labels"),
            );
          },
        }),
      );

      contentArea.appendChild(
        createTweak("Ping / LAT Counter", "ping", config.ping, togglePing, {
          text: "Move",
          action: () => {
            if (!config.ping) {
              togglePing(true);
              config.ping = true;
              saveConfig();
              loadCategory("Labels");
            }
            enterEditMode(pingDisplay, "pingPos", defaultConfig.pingPos, () =>
              loadCategory("Labels"),
            );
          },
        }),
      );

      contentArea.appendChild(
        createTweak("HP + AD Counter", "hpAd", config.hpAd, toggleHPAD, {
          text: "Move",
          action: () => {
            if (!config.hpAd) {
              toggleHPAD(true);
              config.hpAd = true;
              saveConfig();
              loadCategory("Labels");
            }
            enterEditMode(hpAdDisplay, "hpAdPos", defaultConfig.hpAdPos, () =>
              loadCategory("Labels"),
            );
          },
        }),
      );

      const clHeader = document.createElement("div");
      clHeader.className = "cat-header";
      clHeader.style.marginTop = "20px";
      clHeader.innerHTML = `<div class="cat-title" style="font-size:16px">Custom Labels</div>`;

      const addLbl = document.createElement("button");
      addLbl.className = "move-btn";
      addLbl.style.background = "#60cdff";
      addLbl.style.color = "black";
      addLbl.style.fontWeight = "bold";
      addLbl.innerText = "+ Add New";
      addLbl.onclick = openLabelCreator;

      clHeader.appendChild(addLbl);
      contentArea.appendChild(clHeader);

      config.customLabels.forEach((lbl) => {
        const row = document.createElement("div");
        row.className = "tweak-card";
        row.innerHTML = `<span style="color:${lbl.color}">${lbl.text}</span>
                     <div>
                        <button class="move-btn">Move</button>
                        <button class="move-btn" style="background:rgba(255,50,50,0.3);color:#ff6b6b">Del</button>
                     </div>`;

        row.querySelector(".move-btn").onclick = () => {
          const el = document.getElementById(`lbl-${lbl.id}`);
          enterEditMode(
            el,
            { type: "label", id: lbl.id },
            { top: "50%", left: "50%" },
            () => loadCategory("Labels"),
          );
        };

        row.querySelectorAll(".move-btn")[1].onclick = () => {
          config.customLabels = config.customLabels.filter(
            (l) => l.id !== lbl.id,
          );
          saveConfig();
          renderCustomLabels();
          loadCategory("Labels");
        };

        contentArea.appendChild(row);
      });
    } else if (cat === "Gameplay") {
      contentArea.appendChild(
        createTweak("Uncap FPS", "uncap", config.uncap, (v) => {
          config.uncap = v;
          saveConfig();

          if (!v) {
            window.requestAnimationFrame = originalRAF;
          } else {
            applyFPSLimiter();
          }

          loadCategory("Gameplay");
        }),
      );
      if (config.uncap) {
        const limitCard = document.createElement("div");
        limitCard.className = "tweak-card";
        limitCard.style.flexDirection = "column";
        limitCard.style.alignItems = "flex-start";

        limitCard.innerHTML = `
        <span style="margin-bottom:6px;">FPS Limiter</span>
        <input type="number" min="0" value="${config.fpsLimit}"
            style="
                background: transparent;
                border: none;
                border-bottom: 2px solid rgba(255,255,255,0.6);
                color: white;
                outline: none;
                width: 100%;
                padding: 4px 2px;
                font-size: 14px;
            "
        />
        <small style="opacity:0.6;margin-top:4px;">
            leave at 0 for unlimited
        </small>
    `;

        const input = limitCard.querySelector("input");

        input.addEventListener("input", () => {
          config.fpsLimit = parseInt(input.value) || 0;
          saveConfig();
          applyFPSLimiter();
        });

        contentArea.appendChild(limitCard);
      }

      contentArea.appendChild(
        createTweak(
          "Name Randomizer",
          "nameRandomizer",
          config.nameRandomizer,
          (v) => {
            toggleNameRandomizer(v);
            loadCategory("Gameplay");
          },
        ),
      );

      if (config.nameRandomizer) {
        const intervalCard = document.createElement("div");
        intervalCard.className = "tweak-card";
        intervalCard.style.flexDirection = "column";
        intervalCard.style.alignItems = "flex-start";

        intervalCard.innerHTML = `
        <span style="margin-bottom:6px;">Change Interval (minutes)</span>
        <input type="number" min="0.01" step="0.01" value="${config.nameInterval}"
            style="
                background: transparent;
                border: none;
                border-bottom: 2px solid rgba(255,255,255,0.6);
                color: white;
                outline: none;
                width: 100%;
                padding: 4px 2px;
                font-size: 14px;
            "
        />
    `;

        const intervalInput = intervalCard.querySelector("input");
        intervalInput.addEventListener("input", () => {
          config.nameInterval = parseFloat(intervalInput.value) || 0.1;
          saveConfig();
          if (config.nameRandomizer) toggleNameRandomizer(true);
        });

        contentArea.appendChild(intervalCard);

        const namesHeader = document.createElement("div");
        namesHeader.className = "cat-header";
        namesHeader.style.marginTop = "15px";
        namesHeader.innerHTML = `<div class="cat-title" style="font-size:16px">Name Pool</div>`;

        const addBtn = document.createElement("button");
        addBtn.className = "move-btn";
        addBtn.style.background = "#60cdff";
        addBtn.style.color = "black";
        addBtn.style.fontWeight = "bold";
        addBtn.innerText = "+ Add Name";

        addBtn.onclick = () => {
          const name = prompt("Enter Name:");
          if (!name) return;
          config.randomNames.push(name);
          saveConfig();
          loadCategory("Gameplay");
        };

        namesHeader.appendChild(addBtn);
        contentArea.appendChild(namesHeader);

        config.randomNames.forEach((name, index) => {
          const row = document.createElement("div");
          row.className = "tweak-card";
          row.innerHTML = `
            <span>${name}</span>
            <button class="move-btn" style="background:rgba(255,50,50,0.3);color:#ff6b6b">Del</button>
        `;

          row.querySelector(".move-btn").onclick = () => {
            config.randomNames.splice(index, 1);
            saveConfig();
            loadCategory("Gameplay");
          };

          contentArea.appendChild(row);
        });
      }

      contentArea.appendChild(
        createTweak("Auto Fullscreen", "afs", config.autoFS, toggleAutoFS),
      );
      contentArea.appendChild(
        createTweak(
          "Only Show Labels Ingame",
          "onlyShowLabelsIngame",
          config.onlyShowLabelsIngame,
          (v) => {
            config.onlyShowLabelsIngame = v;
            saveConfig();
            applyLabelContainerMode();
          },
        ),
      );
contentArea.appendChild(
    createTweak(
        "AutoHide Minimap",
        "autoHideMinimap",
        config.autoHideMinimap,
        toggleAutoHideMinimap
    )
);

    } else if (cat === "Client") {
      contentArea.appendChild(
        createTweak("Glassmorphism", "glass", config.glass, (v) => {
          config.glass = v;
          saveConfig();
          menu.classList.toggle("nova-glass", v);
          toggleGlassStyle(v); // Toggles the provided Start Menu style
          if (
            document.querySelector(".nav-item.active").dataset.cat ===
            "Backgrounds"
          )
            loadCategory("Backgrounds");
        }),
      );
      contentArea.appendChild(
        createTweak("Fast Menu", "fast", config.fastMenu, (v) => {
          config.fastMenu = v;
          saveConfig();
          menu.classList.toggle("nova-animate", !v);
        }),
      );
    } else if (cat === "Misc") {
      contentArea.appendChild(
        createTweak("Clean Menu", "clean", config.cleanMenu, toggleCleanMenu),
      );
      contentArea.appendChild(
        createTweak(
          "Hide Account Block",
          "hideAccountBlock",
          config.hideAccountBlock,
          toggleAccountBlock,
        ),
      );
      contentArea.appendChild(
        createTweak(
          "Use Classic Logo",
          "useClassicLogo",
          config.useClassicLogo,
          toggleClassicLogo,
        ),
      );
    }
  }

  function createTweak(name, id, checked, callback, extraBtn = null) {
    const card = document.createElement("div");
    card.className = "tweak-card";
    let html = `<span>${name}</span><div style="display:flex; align-items:center;">`;
    if (extraBtn) html += `<button class="move-btn">${extraBtn.text}</button>`;
    html += `<label class="win-switch"><input type="checkbox" ${checked ? "checked" : ""}><span class="win-slider"></span></label></div>`;
    card.innerHTML = html;
    card.querySelector("input").onchange = (e) => callback(e.target.checked);
    if (extraBtn) card.querySelector(".move-btn").onclick = extraBtn.action;
    return card;
  }

  const menu = document.createElement("div");
  menu.id = "nova-menu";
  menu.innerHTML = `
        <div id="nova-header"><span>Nova Client</span><div id="nova-close" style="cursor:pointer">&times;</div></div>
        <div id="nova-nav">
            <div class="nav-item active" data-cat="Labels">Labels</div>
            <div class="nav-item" data-cat="Gameplay">Gameplay</div>
            <div class="nav-item" data-cat="Keybinds">Keybinds</div>
            <div class="nav-item" data-cat="Crosshairs">Crosshairs</div>
            <div class="nav-item" data-cat="Backgrounds">Backgrounds</div>
            <div class="nav-item" data-cat="Client">Client</div>
            <div class="nav-item" data-cat="Misc">Misc</div>
        </div>
    `;
  const contentArea = document.createElement("div");
  contentArea.id = "nova-content";
  menu.appendChild(contentArea);
  document.body.appendChild(menu);

  const toggleMenu = (open) => {
    if (open) {
      menu.style.display = "flex";
      setTimeout(() => menu.classList.add("active"), 10);
    } else {
      menu.classList.remove("active");
      setTimeout(
        () => {
          if (!menu.classList.contains("active")) menu.style.display = "none";
        },
        config.fastMenu ? 0 : 400,
      );
    }
  };

  window.onkeydown = (e) => {
    if (e.code === "ShiftRight") toggleMenu(!menu.classList.contains("active"));
  };
  document.getElementById("nova-close").onclick = () => toggleMenu(false);
  menu.querySelectorAll(".nav-item").forEach((item) => {
    item.onclick = () => {
      menu
        .querySelectorAll(".nav-item")
        .forEach((i) => i.classList.remove("active"));
      item.classList.add("active");
      loadCategory(item.dataset.cat);
    };
  });

  let drag = false,
    ox,
    oy;
  document.getElementById("nova-header").onmousedown = (e) => {
    drag = true;
    ox = e.clientX - menu.offsetLeft;
    oy = e.clientY - menu.offsetTop;
  };
  document.onmousemove = (e) => {
    if (drag) {
      menu.style.left = e.clientX - ox + menu.offsetWidth / 2 + "px";
      menu.style.top = e.clientY - oy + menu.offsetHeight / 2 + "px";
    }
  };
  document.onmouseup = () => (drag = false);
  function isInGame() {
    const gameArea = document.getElementById("game-touch-area");
    if (!gameArea) return false;

    const rect = gameArea.getBoundingClientRect();
    return (
      rect.top >= 0 &&
      rect.left >= 0 &&
      rect.bottom <= window.innerHeight &&
      rect.right <= window.innerWidth
    );
  }

  function applyLabelContainerMode() {
    const container = document.querySelector("#ui-health-container");
    if (!container) return;

    const labels = document.querySelectorAll(".nova-label");

    labels.forEach((label) => {
      if (config.onlyShowLabelsIngame) {
        if (label.parentNode !== container) {
          container.appendChild(label);
        }
      } else {
        if (label.parentNode !== document.body) {
          document.body.appendChild(label);
        }
      }
    });
  }

  loadCategory("Labels");
  if (config.fps) toggleFPS(true);
  if (config.ping) togglePing(true);
  if (config.hpAd) toggleHPAD(true);
  if (config.uncap) applyFPSLimiter();
  if (config.cleanMenu) toggleCleanMenu(true);
  if (config.hideAccountBlock) toggleAccountBlock(true);
  if (config.useClassicLogo) toggleClassicLogo(true);
  if (config.autoFS) toggleAutoFS(true);
  if (config.shuffleEnabled) toggleShuffle(true);
  toggleGlassStyle(config.glass);
  renderCustomLabels();
  if (config.nameRandomizer) {
    toggleNameRandomizer(true);
      if (config.autoHideMinimap) toggleAutoHideMinimap(true);
  }

  if (config.activeCrosshair) {
    const x = config.customCrosshairs.find(
      (i) => i.id === config.activeCrosshair,
    );
    if (x) applyCrosshair(x.data);
  }
  function forceBackground() {
    const bgEl = document.querySelector("#background");
    if (!bgEl) {
      requestAnimationFrame(forceBackground);
      return;
    }

    const b = [...defaultBackgrounds, ...config.customBackgrounds].find(
      (i) => i.id === config.activeBackground,
    );

    if (!b) return;

    bgEl.style.backgroundImage = `url("${b.data}")`;

    new MutationObserver(() => {
      if (bgEl.style.backgroundImage !== `url("${b.data}")`) {
        bgEl.style.backgroundImage = `url("${b.data}")`;
      }
    }).observe(bgEl, { attributes: true, attributeFilter: ["style"] });
  }

  if (config.activeBackground) {
    forceBackground();
  }
  menu.classList.toggle("nova-glass", config.glass);
  menu.classList.toggle("nova-animate", !config.fastMenu);
})();