Null Client

Null Client — The Best Client on Bloxd.io And Gives an Smooth Experience For ur Gameplay!.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Null Client
// @namespace    null.client
// @version      1.1
// @match        https://bloxd.io/*
// @match        *://staging.bloxd.io/*
// @match        https://www.bloxdforge.com/studio/play/*
// @match        https://www.crazygames.com/game/bloxdhop-io/*
// @description  Null Client — The Best Client on Bloxd.io And Gives an Smooth Experience For ur Gameplay!.
// @author       Nullscape
// @icon         https://i.postimg.cc/xjzYxS0R/Null-Client.png
// @run-at       document-start
// ==/UserScript==
(function () {
  // Persistence and defaults-
  const STORAGE_KEY = "null_client_v1_state";
  const DEFAULT_STATE = {
    version: "1.8",
    keystrokes: false,
    cps: false,
    ping: false,
    armor: false,
    cape: null, // e.g. "gray" | "dark" | "rainbow" | "custom:0"
    customCapes: [], // { name, dataUrl }
    locked: true,
    positions: {
      keystrokes: { bottom: 18, left: 18 },
      cps: { bottom: 66, left: 18 },
      ping: { bottom: 98, left: 18 },
      armor: { top: 18, right: 18 },
      capeOverlay: { top: 54, right: 40 }
    },
    texture: "",
    fps: {
      scale: 1,
      pauseAnimations: false,
      removeBackgrounds: false
    },
    armorImage: null
  };
  let state = loadState();
 
  function loadState() {
    try {
      const raw = localStorage.getItem(STORAGE_KEY);
      if (!raw) return JSON.parse(JSON.stringify(DEFAULT_STATE));
      const parsed = JSON.parse(raw);
      return Object.assign(JSON.parse(JSON.stringify(DEFAULT_STATE)), parsed);
    } catch (e) {
      return JSON.parse(JSON.stringify(DEFAULT_STATE));
    }
  }
  function saveState() {
    try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {}
  }
 
  // UI root, toggle key
  let uiRoot = null;
  let uiOpen = false;
  document.addEventListener("keydown", e => {
    if (e.code === "ShiftRight") {
      uiOpen = !uiOpen;
      if (uiOpen) showUI(); else hideUI();
    }
  });
 
  function showUI() {
    if (!uiRoot) createUI();
    uiRoot.style.display = "block";
  }
  function hideUI() {
    if (uiRoot) uiRoot.style.display = "none";
  }
 
  // Ensure body exists for @run-at document-start
  let createUICalled = false;
  function createUI() {
    if (createUICalled) return;
    createUICalled = true;
    if (!document.body) {
      document.addEventListener("DOMContentLoaded", createUI, { once: true });
      return;
    }
 
    uiRoot = document.createElement("div");
    uiRoot.style.cssText = "position:fixed; inset:0; z-index:2147483646; display:none;";
    document.body.appendChild(uiRoot);
 
    const shadow = uiRoot.attachShadow({ mode: "open" });
 
    // Natural UI styling
    shadow.innerHTML = `
      <style>
        :host { all: initial; }
        * { box-sizing: border-box; font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial; }
        .frame {
          position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
          width:860px; height:460px;
          background: linear-gradient(180deg, #0f1113, #0b0c0d);
          border-radius:12px;
          display:flex;
          color:#e7eef6;
          box-shadow: 0 10px 40px rgba(2,6,10,0.75);
          overflow:hidden;
          border:1px solid rgba(255,255,255,0.03);
        }
        .sidebar {
          width:170px; background: linear-gradient(180deg,#0b0c0d,#0a0b0c);
          padding:14px; display:flex; flex-direction:column; gap:10px; border-right:1px solid rgba(255,255,255,0.02);
        }
        .logo { display:flex; align-items:center; gap:10px; }
        .logo img { width:34px; height:34px; border-radius:6px; }
        .brand { font-weight:700; font-size:14px; letter-spacing:0.4px; }
        .version { font-size:11px; color:#99a2ad; }
        .nav { margin-top:8px; display:flex; flex-direction:column; gap:6px; }
        .nav button {
          background:transparent; border:1px solid transparent; color:#cfd8df; text-align:left;
          padding:8px 10px; border-radius:8px; cursor:pointer; font-size:13px;
        }
        .nav button.active { background:#121416; border-color:rgba(255,255,255,0.02); color:#fff; box-shadow: inset 0 -1px 0 rgba(255,255,255,0.01); }
        .nav .spacer { flex:1; }
        .main { flex:1; padding:18px; display:flex; flex-direction:column; }
        .header { display:flex; justify-content:space-between; align-items:center; }
        .title { font-weight:700; font-size:16px; }
        .subtitle { color:#98a2ac; font-size:12px; }
        .grid { margin-top:14px; display:flex; gap:12px; flex-wrap:wrap; }
        .card {
          width:240px; min-height:92px;
          background: linear-gradient(180deg,#0e1012,#0b0c0d);
          border-radius:10px; padding:12px; border:1px solid rgba(255,255,255,0.02);
          box-shadow: 0 8px 18px rgba(2,6,10,0.5);
        }
        .card-title { font-weight:700; margin-bottom:8px; font-size:13px; }
        .card-desc { color:#98a2ac; font-size:12px; margin-bottom:8px; }
        .toggle-row { display:flex; align-items:center; justify-content:space-between; background:linear-gradient(180deg,#0b0c0d,#0a0b0c); padding:8px; border-radius:8px; border:1px solid rgba(255,255,255,0.02); }
        .toggle-row .label { font-size:13px; color:#e7eef6; }
        .switch { width:46px; height:24px; border-radius:14px; background:#202427; position:relative; cursor:pointer; border:1px solid rgba(255,255,255,0.03); }
        .switch.on { background:linear-gradient(90deg,#2b9aff,#1fb1ff); }
        .knob { width:20px; height:20px; border-radius:10px; background:#fff; position:absolute; top:2px; left:2px; transition:all .12s ease; box-shadow: 0 3px 8px rgba(0,0,0,0.45); }
        .switch.on .knob { left:24px; }
        .small { font-size:12px; color:#9aa6b0; }
        .footer { margin-top:auto; display:flex; justify-content:space-between; align-items:center; color:#98a2ac; font-size:12px; }
        .btn { padding:6px 10px; border-radius:8px; background:#1c1f22; border:1px solid rgba(255,255,255,0.03); color:#e7eef6; cursor:pointer; }
        .btn:hover { background:#232629; }
      </style>
 
      <div class="frame" id="frame">
        <div class="sidebar">
          <div class="logo"><img src="https://i.postimg.cc/xjzYxS0R/Null-Client.png" alt="logo"><div><div class="brand">Null Client</div><div class="version">v1.8</div></div></div>
          <div class="nav" id="nav">
            <button data-tab="player" class="active">Player</button>
            <button data-tab="accessories">Accessories</button>
            <button data-tab="visual">Visual</button>
            <button data-tab="combat">Combat</button>
            <div class="spacer"></div>
            <button data-tab="settings">Settings</button>
            <button id="closeBtn">Exit</button>
          </div>
        </div>
 
        <div class="main">
          <div class="header"><div><div class="title" id="mainTitle">Player</div><div class="subtitle" id="mainSub">Client-side player widgets</div></div><div><button class="btn" id="hideHelp">Help</button></div></div>
 
          <div class="grid" id="gridArea"></div>
 
          <div class="footer"><div>Right Shift to open/close</div><div class="small">All features are client-side only</div></div>
        </div>
      </div>
    `;
 
    // hook nav
    const nav = shadow.getElementById("nav");
    nav.addEventListener("click", e => {
      const btn = e.target.closest("button");
      if (!btn) return;
      if (btn.id === "closeBtn") { uiOpen = false; hideUI(); return; }
      for (const b of nav.querySelectorAll("button")) b.classList.remove("active");
      btn.classList.add("active");
      const tab = btn.dataset.tab;
      const title = shadow.getElementById("mainTitle");
      const sub = shadow.getElementById("mainSub");
      const grid = shadow.getElementById("gridArea");
      grid.innerHTML = "";
      if (tab === "player") { title.textContent = "Player"; sub.textContent = "Widgets: Keystrokes, CPS, Ping, Armor"; renderPlayerGrid(grid); }
      else if (tab === "accessories") { title.textContent = "Accessories"; sub.textContent = "Client capes & previews"; renderAccessoriesGrid(grid); }
      else if (tab === "visual") { title.textContent = "Visual"; sub.textContent = "Texture pack & FPS boost"; renderVisualGrid(grid); }
      else if (tab === "combat") { title.textContent = "Combat"; sub.textContent = "Cosmetic combat tools (no cheats)"; renderCombatGrid(grid); }
      else if (tab === "settings") { title.textContent = "Settings"; sub.textContent = "Persistence and widget locking"; renderSettingsGrid(grid); }
    });
 
    // initial fill
    const initialGrid = shadow.getElementById("gridArea");
    renderPlayerGrid(initialGrid);
 
    shadow.getElementById("hideHelp").onclick = () => { alert("Help available in Settings or in the included docs."); };
  }
 
  // Grid card renderers
  function makeToggleCard(title, desc, stateKey, onChange) {
    const el = document.createElement("div");
    el.className = "card";
    el.innerHTML = `
      <div class="card-title">${title}</div>
      <div class="card-desc">${desc}</div>
      <div class="toggle-row">
        <div class="label small">Enable</div>
        <div class="switch ${state[stateKey] ? "on" : ""}" data-key="${stateKey}"><div class="knob"></div></div>
      </div>
    `;
    const sw = el.querySelector(".switch");
    sw.addEventListener("click", () => {
      const key = sw.dataset.key;
      state[key] = !state[key];
      saveState();
      sw.classList.toggle("on", !!state[key]);
      if (typeof onChange === "function") onChange(state[key]);
      if (key === "keystrokes") toggleKeystrokes(state[key]);
      if (key === "cps") toggleCPS(state[key]);
      if (key === "ping") togglePing(state[key]);
      if (key === "armor") toggleArmor(state[key]);
    });
    return el;
  }
 
  function renderPlayerGrid(container) {
    container.appendChild(makeToggleCard("CPS Counter", "Counts left and right mouse button clicks per second (format: left | right)", "cps", (v) => {}));
    container.appendChild(makeToggleCard("Ping Counter", "Estimate network latency (client-side)", "ping", (v) => {}));
    container.appendChild(makeToggleCard("Keystrokes", "Shows WASD input (small overlay)", "keystrokes", (v) => {}));
    container.appendChild(makeToggleCard("Armor View", "Local armor preview (client-only)", "armor", (v) => {}));
  }
 
  function renderAccessoriesGrid(container) {
    const card = document.createElement("div");
    card.className = "card";
    card.innerHTML = `
      <div class="card-title">Capes (client-only)</div>
      <div class="card-desc">Only you see these capes — upload or paste URLs to add custom capes.</div>
      <div style="margin-top:8px;">
        <button id="cape-none" class="btn">No Cape</button>
        <button id="cape-gray" class="btn">Gray</button>
        <button id="cape-dark" class="btn">Dark</button>
        <button id="cape-rainbow" class="btn">Rainbow</button>
      </div>
      <div style="margin-top:10px;" class="small">Add custom cape via URL or upload:</div>
      <div style="margin-top:6px;">
        <input id="capeUrl" type="url" placeholder="https://..." style="padding:6px; border-radius:6px; background:#0b0b0b; color:#dfe8ef; border:1px solid rgba(255,255,255,0.03); width:64%;">
        <button id="addUrl" class="btn">Add URL</button>
      </div>
      <div style="margin-top:6px;">
        <input id="capeFile" type="file" accept="image/*" style="color:#dfe8ef;">
        <button id="addFile" class="btn">Upload</button>
      </div>
      <div id="customCapesList" style="margin-top:8px; max-height:120px; overflow:auto;"></div>
    `;
    container.appendChild(card);
 
    function updateList() {
      const list = card.querySelector("#customCapesList");
      list.innerHTML = "";
      if (!state.customCapes || state.customCapes.length === 0) {
        const p = document.createElement("div"); p.className = "small"; p.textContent = "No custom capes added."; list.appendChild(p); return;
      }
      state.customCapes.forEach((c, i) => {
        const row = document.createElement("div");
        row.style.display = "flex"; row.style.alignItems = "center"; row.style.gap = "8px"; row.style.marginTop = "6px";
        const thumb = document.createElement("div"); thumb.style.width="72px"; thumb.style.height="46px"; thumb.style.background = c.dataUrl ? `url(${c.dataUrl}) center/cover no-repeat` : "#0f0f0f"; thumb.style.borderRadius="6px";
        const name = document.createElement("div"); name.textContent = c.name || ("Custom " + (i+1)); name.style.color="#dfe8ef"; name.style.flex="1";
        const sel = document.createElement("button"); sel.textContent = "Select"; sel.className = "btn"; sel.onclick = () => { state.cape = "custom:" + i; saveState(); applyCape(state.cape); showToast("Custom cape selected"); };
        const rem = document.createElement("button"); rem.textContent = "Remove"; rem.className = "btn"; rem.onclick = () => { if (!confirm("Remove custom cape?")) return; state.customCapes.splice(i,1); if ((state.cape||"").startsWith("custom:")) { const cur = parseInt(state.cape.split(":")[1],10); if (cur===i) state.cape=null; else if (cur>i) state.cape="custom:"+(cur-1); } saveState(); updateList(); applyCape(state.cape); };
        row.appendChild(thumb); row.appendChild(name); row.appendChild(sel); row.appendChild(rem);
        list.appendChild(row);
      });
    }
 
    card.querySelector("#cape-none").onclick = () => { state.cape = null; saveState(); applyCape(null); showToast("Cape disabled"); };
    card.querySelector("#cape-gray").onclick = () => { state.cape = "gray"; saveState(); applyCape("gray"); showToast("Gray cape enabled"); };
    card.querySelector("#cape-dark").onclick = () => { state.cape = "dark"; saveState(); applyCape("dark"); showToast("Dark cape enabled"); };
    card.querySelector("#cape-rainbow").onclick = () => { state.cape = "rainbow"; saveState(); applyCape("rainbow"); showToast("Rainbow cape enabled"); };
 
    card.querySelector("#addUrl").onclick = () => {
      const url = (card.querySelector("#capeUrl").value || "").trim();
      if (!url) return alert("Please paste an image URL.");
      state.customCapes.push({ name: url.split("/").pop() || "custom", dataUrl: url });
      saveState(); updateList(); showToast("Custom cape added");
      card.querySelector("#capeUrl").value = "";
    };
    card.querySelector("#addFile").onclick = () => {
      const f = card.querySelector("#capeFile").files[0];
      if (!f) return alert("Choose a file first.");
      const r = new FileReader();
      r.onload = () => {
        state.customCapes.push({ name: f.name, dataUrl: r.result });
        saveState(); updateList(); showToast("Custom cape uploaded");
      };
      r.readAsDataURL(f);
    };
 
    updateList();
  }
 
  function renderVisualGrid(container) {
    const textureCard = document.createElement("div");
    textureCard.className = "card";
    textureCard.innerHTML = `
      <div class="card-title">Texture Pack</div>
      <div class="card-desc">Client-side visual filters (grayscale / high contrast)</div>
      <div style="margin-top:8px;">
        <button id="texReset" class="btn">Reset</button>
        <button id="texMono" class="btn">Monochrome</button>
        <button id="texHigh" class="btn">High Contrast</button>
      </div>
    `;
    container.appendChild(textureCard);
    textureCard.querySelector("#texReset").onclick = () => { state.texture = ""; applyTexture(""); saveState(); };
    textureCard.querySelector("#texMono").onclick = () => { state.texture = "mono"; applyTexture("mono"); saveState(); };
    textureCard.querySelector("#texHigh").onclick = () => { state.texture = "highcontrast"; applyTexture("highcontrast"); saveState(); };
 
    const fpsCard = document.createElement("div");
    fpsCard.className = "card";
    fpsCard.innerHTML = `
      <div class="card-title">FPS Boost</div>
      <div class="card-desc">Lower canvas rendering & pause animations to improve frame-rate</div>
      <div style="margin-top:8px;">
        <button id="fpsReset" class="btn">Reset</button>
        <button id="fps075" class="btn">Scale 0.75</button>
        <button id="fps05" class="btn">Scale 0.5</button>
      </div>
      <div style="margin-top:8px;">
        <label class="small"><input id="fpsPause" type="checkbox"> Pause CSS animations</label><br>
        <label class="small"><input id="fpsBg" type="checkbox"> Remove background images</label>
      </div>
    `;
    container.appendChild(fpsCard);
    fpsCard.querySelector("#fps075").onclick = () => { state.fps.scale = 0.75; applyFPSState(); saveState(); showToast("Canvas scaled 0.75"); };
    fpsCard.querySelector("#fps05").onclick = () => { state.fps.scale = 0.5; applyFPSState(); saveState(); showToast("Canvas scaled 0.5"); };
    fpsCard.querySelector("#fpsReset").onclick = () => { state.fps.scale = 1; applyFPSState(); saveState(); showToast("FPS settings reset"); };
    fpsCard.querySelector("#fpsPause").checked = !!state.fps.pauseAnimations;
    fpsCard.querySelector("#fpsBg").checked = !!state.fps.removeBackgrounds;
    fpsCard.querySelector("#fpsPause").onchange = (e)=> { state.fps.pauseAnimations = e.target.checked; applyFPSState(); saveState(); };
    fpsCard.querySelector("#fpsBg").onchange = (e)=> { state.fps.removeBackgrounds = e.target.checked; applyFPSState(); saveState(); };
  }
 
  function renderCombatGrid(container) {
    const card = document.createElement("div");
    card.className = "card";
    card.innerHTML = `
      <div class="card-title">Combat (Cosmetic-only)</div>
      <div class="card-desc">I will not implement cheats that affect other players. The toggles here are decorative and client-only.</div>
      <div style="margin-top:10px;">
        <div class="toggle-row"><div class="label small">Local target outline</div><div class="switch" data-key="outline"><div class="knob"></div></div></div>
        <div style="height:8px"></div>
        <div class="toggle-row"><div class="label small">Local auto-hotbar visuals</div><div class="switch" data-key="hotbar"><div class="knob"></div></div></div>
      </div>
    `;
    container.appendChild(card);
    card.querySelectorAll(".switch").forEach(s => s.addEventListener("click", () => { s.classList.toggle("on"); alert("This is cosmetic-only and does not affect gameplay."); }));
  }
 
  function renderSettingsGrid(container) {
    const card = document.createElement("div");
    card.className = "card";
    card.innerHTML = `
      <div class="card-title">Settings</div>
      <div class="card-desc">Widget locking, reset and how to install</div>
      <div style="margin-top:8px;">
        <label class="small"><input id="lockWidgets" type="checkbox"> Lock widget positions</label>
        <div style="margin-top:8px;"><button id="resetAll" class="btn">Reset all settings</button></div>
      </div>
      <div style="margin-top:10px;" class="small">Tip: single-click any visible widget to toggle it on/off. Drag widgets when unlocked to reposition them.</div>
    `;
    container.appendChild(card);
    const lockEl = card.querySelector("#lockWidgets");
    lockEl.checked = !!state.locked;
    lockEl.onchange = e => { state.locked = e.target.checked; saveState(); updateLockState(); showToast("Lock " + (state.locked ? "enabled" : "disabled")); };
 
    card.querySelector("#resetAll").onclick = () => {
      if (!confirm("Reset Null Client settings? This will remove custom capes and widget positions.")) return;
      localStorage.removeItem(STORAGE_KEY);
      state = loadState();
      location.reload();
    };
  }
 
  // Widgets implementation
  // Keystrokes overlay
  let keyUI = null;
  const keyState = { w:false,a:false,s:false,d:false };
  function toggleKeystrokes(on) {
    if (on && !keyUI) {
      keyUI = document.createElement("div");
      keyUI.style.cssText = widgetStyle("keystrokes") + "display:flex;gap:6px;align-items:center;justify-content:center;padding:6px 8px;font-size:12px;backdrop-filter: blur(3px);";
      ["W","A","S","D"].forEach(k => {
        const b = document.createElement("div");
        b.textContent = k;
        b.dataset.key = k.toLowerCase();
        Object.assign(b.style, { padding:"6px 8px", background:"#0c0d0e", borderRadius:"6px", color:"#cfe7ff", border:"1px solid rgba(255,255,255,0.03)", minWidth:"20px", textAlign:"center" });
        keyUI.appendChild(b);
      });
      // LMB/RMB small indicators
      const ctrl = document.createElement("div");
      ctrl.style.display="flex"; ctrl.style.flexDirection="column"; ctrl.style.marginLeft="8px"; ctrl.style.gap="4px";
      const lmb = document.createElement("div"); lmb.textContent="LMB"; lmb.style.fontSize="11px"; lmb.style.color="#9fbddf"; lmb.dataset.btn="lmb";
      const rmb = document.createElement("div"); rmb.textContent="RMB"; rmb.style.fontSize="11px"; rmb.style.color="#9fbddf"; rmb.dataset.btn="rmb";
      ctrl.appendChild(lmb); ctrl.appendChild(rmb);
      keyUI.appendChild(ctrl);
 
      document.body.appendChild(keyUI);
      positionWidget(keyUI, "keystrokes");
      attachWidgetClickToggle(keyUI, "keystrokes");
      if (!state.locked) makeDraggable(keyUI, "keystrokes");
 
      window.addEventListener("keydown", keyDownHandler);
      window.addEventListener("keyup", keyUpHandler);
      window.addEventListener("mousedown", mouseDownHandlerForKeys);
      window.addEventListener("mouseup", mouseUpHandlerForKeys);
    } else if (!on && keyUI) {
      keyUI.remove(); keyUI = null;
      window.removeEventListener("keydown", keyDownHandler);
      window.removeEventListener("keyup", keyUpHandler);
      window.removeEventListener("mousedown", mouseDownHandlerForKeys);
      window.removeEventListener("mouseup", mouseUpHandlerForKeys);
    }
  }
  function keyDownHandler(e) {
    const k = e.key.toLowerCase();
    if (["w","a","s","d"].includes(k)) { keyState[k]=true; refreshKeyUI(); }
  }
  function keyUpHandler(e) {
    const k = e.key.toLowerCase();
    if (["w","a","s","d"].includes(k)) { keyState[k]=false; refreshKeyUI(); }
  }
  function refreshKeyUI() {
    if (!keyUI) return;
    for (const child of keyUI.children) {
      const k = child.dataset && child.dataset.key;
      if (!k) continue;
      if (keyState[k]) { child.style.background="#1f6feb"; child.style.color="#fff"; child.style.transform="translateY(-1px)"; }
      else { child.style.background="#0c0d0e"; child.style.color="#cfe7ff"; child.style.transform=""; }
    }
  }
  function mouseDownHandlerForKeys(e) {
    if (!keyUI) return;
    const l = keyUI.querySelector("[data-btn='lmb']");
    const r = keyUI.querySelector("[data-btn='rmb']");
    if (e.button === 0) { if (l) l.style.opacity="1"; }
    if (e.button === 2) { if (r) r.style.opacity="1"; }
  }
  function mouseUpHandlerForKeys(e) {
    if (!keyUI) return;
    const l = keyUI.querySelector("[data-btn='lmb']");
    const r = keyUI.querySelector("[data-btn='rmb']");
    if (e.button === 0) { if (l) l.style.opacity="0.7"; }
    if (e.button === 2) { if (r) r.style.opacity="0.7"; }
  }
 
  // CPS widget shows Left | Right counts per second
  let cpsUI = null;
  let leftClicks = 0;
  let rightClicks = 0;
  document.addEventListener("mousedown", e => {
    if (e.button === 0) leftClicks++;
    else if (e.button === 2) rightClicks++;
  });
  setInterval(() => {
    if (cpsUI) {
      cpsUI.textContent = leftClicks + " | " + rightClicks;
      leftClicks = 0;
      rightClicks = 0;
    } else {
      leftClicks = 0;
      rightClicks = 0;
    }
  }, 1000);
 
  function toggleCPS(on) {
    if (on && !cpsUI) {
      cpsUI = document.createElement("div");
      cpsUI.style.cssText = widgetStyle("cps") + "width:94px;height:30px;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;";
      cpsUI.textContent = "0 | 0";
      document.body.appendChild(cpsUI);
      positionWidget(cpsUI, "cps");
      attachWidgetClickToggle(cpsUI, "cps");
      if (!state.locked) makeDraggable(cpsUI, "cps");
    } else if (!on && cpsUI) {
      cpsUI.remove(); cpsUI = null;
    }
  }
 
  // Ping widget
  let pingUI = null, pingInterval = null;
  async function measurePing() {
    try {
      const url = location.origin + "/favicon.ico";
      const start = performance.now();
      await fetch(url, { method: "HEAD", cache: "no-store" });
      const ms = Math.max(0, Math.round(performance.now() - start));
      if (pingUI) pingUI.textContent = "Ping: " + ms + " ms";
    } catch (err) { if (pingUI) pingUI.textContent = "Ping: —"; }
  }
  function togglePing(on) {
    if (on && !pingUI) {
      pingUI = document.createElement("div");
      pingUI.style.cssText = widgetStyle("ping") + "width:110px;height:28px;display:flex;align-items:center;justify-content:center;font-size:12px;";
      pingUI.textContent = "Ping: ...";
      document.body.appendChild(pingUI);
      positionWidget(pingUI, "ping");
      measurePing();
      pingInterval = setInterval(measurePing, 2000);
      attachWidgetClickToggle(pingUI, "ping");
      if (!state.locked) makeDraggable(pingUI, "ping");
    } else if (!on && pingUI) {
      clearInterval(pingInterval); pingInterval = null;
      pingUI.remove(); pingUI = null;
    }
  }
 
  // Armor view (client-side preview)
  let armorUI = null;
  function toggleArmor(on) {
    if (on && !armorUI) {
      armorUI = document.createElement("div");
      armorUI.style.cssText = widgetStyle("armor") + "width:140px;height:140px;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:8px;";
      const title = document.createElement("div"); title.textContent = "Armor"; title.style.fontSize="12px"; title.style.color="#dbeffb";
      const img = document.createElement("img"); img.style.width="96px"; img.style.height="96px"; img.style.objectFit="cover"; img.style.borderRadius="8px";
      armorUI.appendChild(title); armorUI.appendChild(img);
      document.body.appendChild(armorUI);
      positionWidget(armorUI, "armor");
      updateArmorPreview();
      attachWidgetClickToggle(armorUI, "armor");
      if (!state.locked) makeDraggable(armorUI, "armor");
    } else if (!on && armorUI) {
      armorUI.remove(); armorUI = null;
    }
  }
  function updateArmorPreview() {
    if (!armorUI) return;
    const img = armorUI.querySelector("img");
    let found = null;
    try {
      if (window.player && window.player.avatar && window.player.avatar.armorImage) found = window.player.avatar.armorImage;
      else if (window.game && window.game.player && window.game.player.armorUrl) found = window.game.player.armorUrl;
    } catch (e) {}
    if (!found && state.armorImage) found = state.armorImage;
    if (found) { img.src = found; img.style.background=""; } else { img.src=""; img.style.background="linear-gradient(180deg,#222,#111)"; }
  }
 
  // Cape overlay
  let capeOverlay = null;
  function applyCape(spec) {
    if (capeOverlay) { capeOverlay.remove(); capeOverlay = null; }
    if (!spec) return;
    capeOverlay = document.createElement("div");
    capeOverlay.style.position = "fixed";
    capeOverlay.style.width = "160px"; capeOverlay.style.height = "240px"; capeOverlay.style.pointerEvents = "none";
    capeOverlay.style.zIndex = "2147483645"; capeOverlay.style.borderRadius="7px"; capeOverlay.style.boxShadow="0 8px 20px rgba(0,0,0,0.55)";
    const pos = state.positions.capeOverlay || { top:54, right:40 };
    capeOverlay.style.top = (pos.top||54) + "px"; capeOverlay.style.right = (pos.right||40) + "px";
    if (spec === "gray") capeOverlay.style.background = "linear-gradient(#ddd,#999)";
    else if (spec === "dark") capeOverlay.style.background = "linear-gradient(#111,#333)";
    else if (spec === "rainbow") capeOverlay.style.background = "linear-gradient(90deg, red, orange, yellow, green, blue)";
    else if (spec.startsWith("custom:")) {
      const i = parseInt(spec.split(":")[1],10);
      const obj = state.customCapes[i];
      if (obj && obj.dataUrl) capeOverlay.style.background = `url(${obj.dataUrl}) center/cover no-repeat`;
      else capeOverlay.style.background = "#0f0f0f";
    } else capeOverlay.style.background = "#0f0f0f";
    document.body.appendChild(capeOverlay);
    attachWidgetClickToggle(capeOverlay, "cape");
    if (!state.locked) makeDraggable(capeOverlay, "capeOverlay", { storePositionAs: "capeOverlay" });
  }
 
  // Texture / FPS implementations
  const CLIENT_STYLE_ID = "null-client-style";
  function applyTexture(mode) {
    removeClientStyle();
    if (!mode) return;
    let css = "";
    if (mode === "mono") css = `canvas, img, video, [data-game-canvas] { filter: grayscale(1) contrast(1.2) !important; } body { background:#0f0f0f !important; }`;
    else if (mode === "highcontrast") css = `canvas, img, video, [data-game-canvas] { filter: contrast(1.5) brightness(0.95) !important; } body { background:#0a0a0a !important; }`;
    if (css) {
      const s = document.createElement("style"); s.id = CLIENT_STYLE_ID; s.textContent = css;
      (document.head || document.documentElement).appendChild(s);
    }
  }
  function removeClientStyle() { const old = document.getElementById(CLIENT_STYLE_ID); if (old) old.remove(); }
 
  const FPS_STYLE_ID = "null-client-fps-style";
  const canvasesScaled = new WeakMap();
  function applyFPSState() {
    const scale = (state.fps && state.fps.scale) ? state.fps.scale : 1;
    document.querySelectorAll("canvas").forEach(c => {
      try {
        const meta = canvasesScaled.get(c);
        if (meta) {
          if (scale === 1) {
            c.width = meta.origWidth; c.height = meta.origHeight; c.style.transform = meta.origTransform || ""; c.style.imageRendering = meta.origImageRendering || "";
            canvasesScaled.delete(c);
          } else {
            c.width = Math.round(meta.origWidth * scale); c.height = Math.round(meta.origHeight * scale);
            c.style.transform = `scale(${1/scale})`; c.style.transformOrigin = "0 0"; c.style.imageRendering = "pixelated";
          }
        } else {
          if (scale !== 1) {
            const origW = c.width; const origH = c.height;
            canvasesScaled.set(c, { origWidth: origW, origHeight: origH, origTransform: c.style.transform || "", origImageRendering: c.style.imageRendering || "" });
            c.width = Math.round(origW * scale); c.height = Math.round(origH * scale);
            c.style.transform = `scale(${1/scale})`; c.style.transformOrigin = "0 0"; c.style.imageRendering = "pixelated";
          }
        }
      } catch (e) {}
    });
 
    // CSS toggles
    const old = document.getElementById(FPS_STYLE_ID);
    if (old) old.remove();
    let css = "";
    if (state.fps.pauseAnimations) css += `* { animation-play-state: paused !important; transition: none !important; }`;
    if (state.fps.removeBackgrounds) css += `body, [style*="background"], [style*="background-image"] { background-image: none !important; background: none !important; }`;
    if (css) {
      const s = document.createElement("style"); s.id = FPS_STYLE_ID; s.textContent = css; (document.head || document.documentElement).appendChild(s);
    }
  }
 
  // Widget helpers: style, position, drag, single-click toggles
  function widgetStyle(key) {
    const pos = (state.positions && state.positions[key]) ? state.positions[key] : {};
    const pieces = [];
    if (pos.bottom !== undefined) pieces.push("bottom:" + pos.bottom + "px");
    if (pos.top !== undefined) pieces.push("top:" + pos.top + "px");
    if (pos.left !== undefined) pieces.push("left:" + pos.left + "px");
    if (pos.right !== undefined) pieces.push("right:" + pos.right + "px");
    pieces.push("position:fixed");
    pieces.push("background:rgba(12,13,14,0.85)");
    pieces.push("border-radius:8px");
    pieces.push("border:1px solid rgba(255,255,255,0.03)");
    pieces.push("color:#dff3ff");
    pieces.push("padding:6px");
    return pieces.join(";") + ";";
  }
 
  function positionWidget(el, key) {
    const pos = (state.positions && state.positions[key]) ? state.positions[key] : {};
    if (pos.top !== undefined) el.style.top = pos.top + "px";
    if (pos.bottom !== undefined) el.style.bottom = pos.bottom + "px";
    if (pos.left !== undefined) el.style.left = pos.left + "px";
    if (pos.right !== undefined) el.style.right = pos.right + "px";
  }
 
  const DRAGMAP = new WeakMap();
  function makeDraggable(el, key, opts = {}) {
    removeDraggable(el);
    el.style.cursor = "move";
    const storeKey = opts.storePositionAs || key;
    let dragging = false, startX=0, startY=0, startLeft=0, startTop=0;
    const onDown = (ev) => {
      if (ev.button !== 0) return;
      ev.preventDefault();
      startX = ev.clientX; startY = ev.clientY;
      const rect = el.getBoundingClientRect();
      startLeft = rect.left; startTop = rect.top;
      const onMove = (mv) => {
        mv.preventDefault();
        const dx = mv.clientX - startX, dy = mv.clientY - startY;
        if (!dragging && Math.hypot(dx,dy) > 6) dragging = true;
        if (dragging) {
          const newLeft = Math.max(0, Math.round(startLeft + dx));
          const newTop = Math.max(0, Math.round(startTop + dy));
          el.style.left = newLeft + "px"; el.style.top = newTop + "px";
          el.style.right = ""; el.style.bottom = "";
        }
      };
      const onUp = () => {
        window.removeEventListener("mousemove", onMove);
        window.removeEventListener("mouseup", onUp);
        if (dragging) {
          const b = el.getBoundingClientRect();
          state.positions = state.positions || {};
          state.positions[storeKey] = { top: Math.max(0, Math.round(b.top)), left: Math.max(0, Math.round(b.left)) };
          saveState();
        }
        setTimeout(()=>{ dragging=false; }, 10);
      };
      window.addEventListener("mousemove", onMove);
      window.addEventListener("mouseup", onUp);
    };
    el.addEventListener("mousedown", onDown);
    DRAGMAP.set(el, onDown);
  }
  function removeDraggable(el) {
    const h = DRAGMAP.get(el);
    if (h) { el.removeEventListener("mousedown", h); DRAGMAP.delete(el); }
    el.style.cursor = "";
  }
  function updateLockState() {
    const keys = ["keystrokes","cps","ping","armor","capeOverlay"];
    for (const k of keys) {
      const el = getWidget(k);
      if (!el) continue;
      if (!state.locked) makeDraggable(el, k, { storePositionAs: k === "capeOverlay" ? "capeOverlay" : k });
      else removeDraggable(el);
    }
  }
  function getWidget(key) {
    if (key === "keystrokes") return keyUI;
    if (key === "cps") return cpsUI;
    if (key === "ping") return pingUI;
    if (key === "armor") return armorUI;
    if (key === "capeOverlay") return capeOverlay;
    return null;
  }
 
  // Single-click toggle (distinguish drag vs click)
  function attachWidgetClickToggle(el, widgetKey) {
    if (!el) return;
    let down = null;
    const downHandler = (ev) => { if (ev.button !== 0) return; down = { x: ev.clientX, y: ev.clientY, t: Date.now() }; };
    const upHandler = (ev) => {
      if (!down) return;
      const dx = ev.clientX - down.x, dy = ev.clientY - down.y;
      if (Math.hypot(dx,dy) <= 6 && (Date.now() - down.t) < 500) {
        // treat as click: toggle appropriate feature
        if (widgetKey === "keystrokes") { state.keystrokes = !state.keystrokes; toggleKeystrokes(state.keystrokes); saveState(); showToast("Keystrokes " + (state.keystrokes ? "enabled":"disabled")); }
        else if (widgetKey === "cps") { state.cps = !state.cps; toggleCPS(state.cps); saveState(); showToast("CPS " + (state.cps ? "enabled":"disabled")); }
        else if (widgetKey === "ping") { state.ping = !state.ping; togglePing(state.ping); saveState(); showToast("Ping " + (state.ping ? "enabled":"disabled")); }
        else if (widgetKey === "armor") { state.armor = !state.armor; toggleArmor(state.armor); saveState(); showToast("Armor view " + (state.armor ? "enabled":"disabled")); }
        else if (widgetKey === "cape") {
          if (state.cape) { state.cape = null; applyCape(null); saveState(); showToast("Cape disabled"); }
          else showToast("Select a cape from Accessories tab");
        }
      }
      down = null;
    };
    el.addEventListener("mousedown", downHandler);
    el.addEventListener("mouseup", upHandler);
  }
 
  // Utility: toast message
  let toastEl = null;
  function showToast(msg, ms = 1100) {
    if (!toastEl) {
      toastEl = document.createElement("div");
      toastEl.style.cssText = "position:fixed; left:50%; bottom:26px; transform:translateX(-50%); background:rgba(6,10,12,0.84); color:#e7f6ff; padding:8px 12px; border-radius:8px; z-index:2147483650; font-size:13px;";
      document.body.appendChild(toastEl);
    }
    toastEl.textContent = msg;
    toastEl.style.opacity = "1";
    setTimeout(()=>{ if (toastEl) toastEl.style.opacity = "0"; }, ms);
  }
 
  // Save before unload
  window.addEventListener("beforeunload", saveState);
 
  // Init: apply persisted widgets & settings
  setTimeout(() => {
    if (state.keystrokes) toggleKeystrokes(true);
    if (state.cps) toggleCPS(true);
    if (state.ping) togglePing(true);
    if (state.armor) toggleArmor(true);
    if (state.cape) applyCape(state.cape);
    if (state.texture) applyTexture(state.texture);
    applyFPSState();
    setTimeout(updateLockState, 300);
  }, 400);
 
  // End of userscript
})();