🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

Greasy Fork is available in English.

📂 缓存分发状态(共享加速已生效)
🕒 页面同步时间:2026/01/31 05:26:13
🔄 下次更新时间:2026/01/31 06:26:13
手动刷新缓存

Null Client

The #1 Bloxd.io Client - FPS Boost, Widgets, Features & More. Client Updated!

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

// ==UserScript==
// @name         Null Client
// @namespace    null.client
// @version      1.3
// @description  The #1 Bloxd.io Client - FPS Boost, Widgets, Features & More. Client Updated!
// @author       Nullscape
// @match        https://bloxd.io/*
// @match        https://*.bloxd.io/*
// @match        https://now.gg/*bloxd*
// @match        https://www.crazygames.com/game/bloxdhop-io/*
// @icon         https://i.postimg.cc/xjzYxS0R/Null-Client.png
// @license      CC-BY-4.0
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    if (window.NullClientLoaded) return;
    window.NullClientLoaded = true;

    // Tab title manager
    setInterval(() => {
        if (document.hasFocus()) {
            document.title = "🔥Null Client";
        } else {
            document.title = "👋Come back to Bloxd!";
        }
    }, 1000);

    const CONFIG = {
        VERSION: "1.3",
        STORAGE: "null_client_v13",
        THEME: {
            DARK: "#1a1a1a",
            GRAY: "#2a2a2a",
            LIGHT: "#3a3a3a",
            LIGHTER: "#4a4a4a",
            BORDER: "#555555",
            TEXT: "#fff",
            DIM: "#999"
        }
    };

    const DEFAULT_STATE = {
        widgets: {
            fps: false, cps: false, ping: false, coords: false,
            speed: false, keystrokes: false, minimap: false, reach: false
        },
        positions: {
            fps: {top:10,left:10}, cps: {top:40,left:10}, ping: {top:70,left:10},
            coords: {top:100,left:10}, speed: {top:130,left:10},
            keystrokes: {bottom:10,left:10}, minimap: {top:10,right:10}, reach: {top:160,left:10}
        },
        features: {
            fullbright: false, zoom: false, sprint: false, bhop: false,
            fastPlace: false, invCleaner: false, noShake: false,
            hitParticles: false, smoothCamera: false, crosshair: false, reach: false
        },
        performance: {
            particles: false, shadows: false, quality: false,
            render: false, draw: false, boost: false
        },
        shaders: { enabled: false, type: 'morning' },
        crosshair: { style: 'default', size: 10, thickness: 2, gap: 5, color: "#fff", opacity: 0.8 },
        quality: { resolution: '1920x1080' },
        superRank: { enabled: false, cape: null, nametagColor: "#fff" },
        fov: 90,
        fastPlaceDelay: 30,
        reachDist: 4.5,
        locked: true,
        keystrokesMode: "wasd"
    };

    class State {
        constructor() {
            this.data = this.load();
        }
        load() {
            try {
                const saved = localStorage.getItem(CONFIG.STORAGE);
                return saved ? {...DEFAULT_STATE, ...JSON.parse(saved)} : {...DEFAULT_STATE};
            } catch(e) {
                return {...DEFAULT_STATE};
            }
        }
        save() {
            try {
                localStorage.setItem(CONFIG.STORAGE, JSON.stringify(this.data));
            } catch(e) {}
        }
        get(path) {
            const keys = path.split('.');
            let val = this.data;
            for(const k of keys) val = val?.[k];
            return val;
        }
        set(path, value) {
            const keys = path.split('.');
            const last = keys.pop();
            let target = this.data;
            for(const k of keys) target = target[k] = target[k] || {};
            target[last] = value;
            this.save();
        }
    }

    class Game {
        constructor() {
            this.camera = null;
            this.player = null;
            this.renderer = null;
            this.scene = null;
            this.THREE = null;
            this.posHist = [];
            this.velHist = [];
            this.init();
        }
        init() {
            setInterval(() => {
                if(typeof window.THREE !== 'undefined' && !this.THREE) {
                    this.THREE = window.THREE;
                    this.hookCamera();
                }
            }, 100);
        }
        hookCamera() {
            if(!this.THREE?.PerspectiveCamera) return;
            const orig = this.THREE.PerspectiveCamera.prototype.updateProjectionMatrix;
            this.THREE.PerspectiveCamera.prototype.updateProjectionMatrix = function() {
                window.nullGame.camera = this;
                return orig.call(this);
            };
        }
        getCamera() {
            return this.camera || window.camera || window.game?.camera;
        }
        getPlayer() {
            return window.player || window.game?.player || window.localPlayer;
        }
        getRenderer() {
            return this.renderer || window.renderer || window.game?.renderer;
        }
        getScene() {
            return this.scene || window.scene || window.game?.scene;
        }
        getPos() {
            const p = this.getPlayer();
            if(!p) return {x:0,y:0,z:0};
            const pos = p.position || p.body?.position;
            if(pos) {
                const curr = {x:pos.x||0, y:pos.y||0, z:pos.z||0, t:Date.now()};
                this.posHist.push(curr);
                if(this.posHist.length>30) this.posHist.shift();
                return curr;
            }
            return this.posHist[this.posHist.length-1] || {x:0,y:0,z:0};
        }
        getVel() {
            const p = this.getPlayer();
            if(p) {
                const vel = p.velocity || p.body?.velocity;
                if(vel && (vel.x||vel.y||vel.z)) {
                    const curr = {x:vel.x||0, y:vel.y||0, z:vel.z||0, t:Date.now()};
                    this.velHist.push(curr);
                    if(this.velHist.length>20) this.velHist.shift();
                    return curr;
                }
            }
            if(this.posHist.length>=2) {
                const a = this.posHist[this.posHist.length-1];
                const b = this.posHist[this.posHist.length-2];
                const dt = (a.t-b.t)/1000;
                if(dt>0 && dt<0.5) {
                    return {x:(a.x-b.x)/dt, y:(a.y-b.y)/dt, z:(a.z-b.z)/dt};
                }
            }
            return {x:0,y:0,z:0};
        }
        getRot() {
            const p = this.getPlayer();
            return p?.rotation?.y || p?.rotation || 0;
        }
        hasBlock() {
            const p = this.getPlayer();
            if(!p) return false;
            return !!(p.targetBlock || p.target || p.lookingAt);
        }
        getFOV() {
            const c = this.getCamera();
            return c?.fov || 90;
        }
        setFOV(fov) {
            const c = this.getCamera();
            if(c?.fov !== undefined) {
                c.fov = fov;
                c.updateProjectionMatrix?.();
                return true;
            }
            return false;
        }
        getDist() {
            const p = this.getPlayer();
            if(!p) return null;
            const pos = p.position || p.body?.position;
            if(!pos) return null;
            const t = p.targetBlock?.position;
            if(t) {
                return Math.sqrt((t.x-pos.x)**2+(t.y-pos.y)**2+(t.z-pos.z)**2);
            }
            return null;
        }
    }

    class Anim {
        constructor() {
            this.cbs = new Map();
            this.running = false;
            this.last = 0;
        }
        add(name, cb) {
            this.cbs.set(name, cb);
            if(!this.running) this.start();
        }
        remove(name) {
            this.cbs.delete(name);
            if(this.cbs.size === 0) this.stop();
        }
        start() {
            this.running = true;
            this.last = performance.now();
            this.loop();
        }
        stop() {
            this.running = false;
        }
        loop() {
            if(!this.running) return;
            const now = performance.now();
            const delta = now - this.last;
            this.last = now;
            this.cbs.forEach((cb,name) => {
                try { cb(now, delta); } catch(e) {}
            });
            requestAnimationFrame(() => this.loop());
        }
    }

    class Perf {
        constructor(game) {
            this.game = game;
            this.on = false;
            this.orig = {};
        }
        enable(opts) {
            if(this.on) this.disable();
            this.on = true;
            if(opts.particles) this.doParticles();
            if(opts.shadows) this.doShadows();
            if(opts.quality) this.doQuality();
            if(opts.render) this.doRender();
            if(opts.draw) this.doDraw();
            if(opts.boost) this.doBoost();
        }
        disable() {
            if(!this.on) return;
            this.on = false;
            this.restore();
        }
        doParticles() {
            document.querySelectorAll('[class*="particle"]').forEach(el => el.style.display='none');
            const s = this.game.getScene();
            if(s?.children) {
                s.children.forEach(c => {
                    if(c.type==='ParticleSystem'||c.isPoints) c.visible=false;
                });
            }
        }
        doShadows() {
            const r = this.game.getRenderer();
            if(r?.shadowMap) {
                this.orig.shadows = r.shadowMap.enabled;
                r.shadowMap.enabled = false;
            }
            const s = this.game.getScene();
            if(s?.traverse) {
                s.traverse(c => {
                    if(c.castShadow!==undefined) c.castShadow=false;
                    if(c.receiveShadow!==undefined) c.receiveShadow=false;
                });
            }
        }
        doQuality() {
            const r = this.game.getRenderer();
            if(r?.setPixelRatio) {
                this.orig.pixelRatio = r.getPixelRatio?.() || 1;
                r.setPixelRatio(0.5);
            }
        }
        doRender() {
            const r = this.game.getRenderer();
            if(r) {
                if(r.sortObjects!==undefined) {
                    this.orig.sortObjects = r.sortObjects;
                    r.sortObjects = false;
                }
            }
        }
        doDraw() {
            const s = this.game.getScene();
            if(s?.children) {
                s.children.forEach(c => {
                    if(c.type==='EffectComposer') {
                        if(c.enabled!==undefined) {
                            this.orig[`fx_${c.uuid}`] = c.enabled;
                            c.enabled = false;
                        }
                    }
                });
            }
        }
        doBoost() {
            const r = this.game.getRenderer();
            if(r?.context) {
                const gl = r.context;
                gl.hint(gl.FRAGMENT_SHADER_DERIVATIVE_HINT, gl.FASTEST);
                gl.hint(gl.GENERATE_MIPMAP_HINT, gl.FASTEST);
            }
        }
        restore() {
            const r = this.game.getRenderer();
            if(r) {
                if(this.orig.shadows!==undefined && r.shadowMap) r.shadowMap.enabled=this.orig.shadows;
                if(this.orig.pixelRatio!==undefined) r.setPixelRatio?.(this.orig.pixelRatio);
                if(this.orig.sortObjects!==undefined) r.sortObjects=this.orig.sortObjects;
            }
            this.orig = {};
        }
    }

    class Shader {
        constructor(game, state) {
            this.game = game;
            this.state = state;
            this.on = false;
            this.el = null;
        }
        enable() {
            if(this.on) return;
            this.on = true;
            const type = this.state.get('shaders.type');
            if(type==='morning') this.morning();
            else if(type==='sunset') this.sunset();
        }
        disable() {
            if(!this.on) return;
            this.on = false;
            if(this.el) {
                this.el.remove();
                this.el = null;
            }
        }
        morning() {
            this.el = document.createElement('style');
            this.el.textContent = 'canvas{filter:brightness(1.3)contrast(1.1)saturate(1.2)!important}';
            document.head.appendChild(this.el);
        }
        sunset() {
            this.el = document.createElement('style');
            this.el.textContent = 'canvas{filter:brightness(1.2)contrast(1.15)saturate(1.3)sepia(0.2)!important}';
            document.head.appendChild(this.el);
        }
    }

    // Features
    class BHOP {
        constructor(game, anim) {
            this.game = game;
            this.anim = anim;
            this.on = false;
            this.lastJump = 0;
            this.velBuf = [];
        }
        enable() {
            if(this.on) return;
            this.on = true;
            this.anim.add('bhop', (now) => {
                if(!this.on) return;
                const v = this.game.getVel();
                const speed = Math.sqrt(v.x**2+v.z**2);
                if(speed<0.1) { this.velBuf=[]; return; }
                if(now-this.lastJump<100) return;
                this.velBuf.push(v.y);
                if(this.velBuf.length>5) this.velBuf.shift();
                if(this.velBuf.length<3) return;
                const falling = v.y<0;
                const near = Math.abs(v.y)<0.05;
                const dec = this.velBuf.every((vel,i,arr)=>i===0||Math.abs(vel)<=Math.abs(arr[i-1]));
                if(falling && dec && near) {
                    const p = this.game.getPlayer();
                    if(p?.jump) p.jump();
                    this.lastJump = now;
                    this.velBuf = [];
                }
            });
        }
        disable() {
            if(!this.on) return;
            this.on = false;
            this.anim.remove('bhop');
            this.velBuf = [];
        }
    }

    class FastPlace {
        constructor(game, anim) {
            this.game = game;
            this.anim = anim;
            this.on = false;
            this.down = false;
            this.last = 0;
            this.delay = 30;
        }
        enable(delay=30) {
            if(this.on) return;
            this.on = true;
            this.delay = delay;
            document.addEventListener('mousedown', this.handleDown=e=>{ if(e.button===2)this.down=true; });
            document.addEventListener('mouseup', this.handleUp=e=>{ if(e.button===2)this.down=false; });
            this.anim.add('fastplace', (now) => {
                if(!this.on||!this.down) return;
                if(now-this.last<this.delay) return;
                if(!this.game.hasBlock()) return;
                const c = document.querySelector('canvas');
                if(c) {
                    c.dispatchEvent(new MouseEvent('mousedown',{bubbles:true,button:2}));
                    setTimeout(()=>c.dispatchEvent(new MouseEvent('mouseup',{bubbles:true,button:2})),10);
                }
                this.last = now;
            });
        }
        disable() {
            if(!this.on) return;
            this.on = false;
            document.removeEventListener('mousedown', this.handleDown);
            document.removeEventListener('mouseup', this.handleUp);
            this.anim.remove('fastplace');
        }
    }

    class Sprint {
        constructor(game) {
            this.game = game;
            this.on = false;
            this.keys = new Set();
        }
        enable() {
            if(this.on) return;
            this.on = true;
            document.addEventListener('keydown', this.handleDown=e=>{
                const codes = ['KeyW','KeyA','KeyS','KeyD','ArrowUp','ArrowLeft','ArrowDown','ArrowRight'];
                if(codes.includes(e.code)) {
                    this.keys.add(e.code);
                    const p = this.game.getPlayer();
                    if(p && p.sprint!==undefined) p.sprint=true;
                }
            });
            document.addEventListener('keyup', this.handleUp=e=>{
                this.keys.delete(e.code);
                if(this.keys.size===0) {
                    const p = this.game.getPlayer();
                    if(p && p.sprint!==undefined) p.sprint=false;
                }
            });
        }
        disable() {
            if(!this.on) return;
            this.on = false;
            document.removeEventListener('keydown', this.handleDown);
            document.removeEventListener('keyup', this.handleUp);
            this.keys.clear();
        }
    }

    class SmoothZoom {
        constructor(game, state, anim) {
            this.game = game;
            this.state = state;
            this.anim = anim;
            this.on = false;
            this.current = 1;
            this.target = 1;
        }
        enable() {
            if(this.on) return;
            this.on = true;
            const fov = this.game.getFOV();
            this.state.set('fov', fov);
            document.addEventListener('keydown', this.handleDown=e=>{
                if(e.repeat) return;
                const map = {KeyV:2.0, KeyX:0.5, KeyC:1.5, KeyZ:0.75};
                if(map[e.code]) {
                    e.preventDefault();
                    this.target = map[e.code];
                }
            });
            document.addEventListener('keyup', this.handleUp=e=>{
                if(['KeyV','KeyX','KeyC','KeyZ'].includes(e.code)) {
                    e.preventDefault();
                    this.target = 1.0;
                }
            });
            this.anim.add('zoom', () => {
                if(!this.on) return;
                const diff = this.target - this.current;
                if(Math.abs(diff)<0.001) {
                    this.current = this.target;
                } else {
                    this.current += diff * 0.15;
                }
                const origFov = this.state.get('fov') || 90;
                this.game.setFOV(origFov / this.current);
            });
        }
        disable() {
            if(!this.on) return;
            this.on = false;
            document.removeEventListener('keydown', this.handleDown);
            document.removeEventListener('keyup', this.handleUp);
            this.anim.remove('zoom');
            const fov = this.state.get('fov');
            if(fov) this.game.setFOV(fov);
        }
    }

    class HitParticles {
        constructor(anim) {
            this.anim = anim;
            this.on = false;
            this.particles = [];
        }
        enable() {
            if(this.on) return;
            this.on = true;
            this.anim.add('hitparticles', () => {
                if(!this.on) return;
                this.particles = this.particles.filter(p => {
                    const age = (Date.now()-p.created)/1000;
                    p.life = Math.max(0, 1-age*2);
                    if(p.life<=0) {
                        p.el.remove();
                        return false;
                    }
                    const left = parseFloat(p.el.style.left)||50;
                    const top = parseFloat(p.el.style.top)||50;
                    p.el.style.left = (left+p.vx)+'%';
                    p.el.style.top = (top+p.vy)+'%';
                    p.el.style.opacity = p.life;
                    p.vy += 0.1;
                    return true;
                });
            });
            document.addEventListener('mousedown', this.handleDown=e=>{
                if(!this.on||e.button!==0) return;
                const p = document.createElement('div');
                p.style.cssText='position:fixed;left:50%;top:50%;width:3px;height:3px;background:#fff;border-radius:50%;pointer-events:none;z-index:999999';
                document.body.appendChild(p);
                const angle = Math.random()*Math.PI*2;
                const speed = 2+Math.random()*3;
                this.particles.push({
                    el:p, vx:Math.cos(angle)*speed, vy:Math.sin(angle)*speed,
                    life:1, created:Date.now()
                });
            });
        }
        disable() {
            if(!this.on) return;
            this.on = false;
            this.anim.remove('hitparticles');
            document.removeEventListener('mousedown', this.handleDown);
            this.particles.forEach(p=>p.el.remove());
            this.particles = [];
        }
    }

    class CustomCrosshair {
        constructor(state) {
            this.state = state;
            this.on = false;
            this.canvas = null;
        }
        enable() {
            if(this.on) return;
            this.on = true;
            this.canvas = document.createElement('canvas');
            this.canvas.style.cssText='position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:999999';
            this.canvas.width = window.innerWidth;
            this.canvas.height = window.innerHeight;
            const ctx = this.canvas.getContext('2d');
            document.body.appendChild(this.canvas);

            const draw = () => {
                const c = this.state.get('crosshair');
                const cx = this.canvas.width/2;
                const cy = this.canvas.height/2;
                ctx.clearRect(0,0,this.canvas.width,this.canvas.height);

                if(c.style==='circle') {
                    ctx.strokeStyle = c.color;
                    ctx.lineWidth = c.thickness;
                    ctx.globalAlpha = c.opacity;
                    ctx.beginPath();
                    ctx.arc(cx,cy,c.size,0,Math.PI*2);
                    ctx.stroke();
                } else if(c.style==='dot') {
                    ctx.fillStyle = c.color;
                    ctx.globalAlpha = c.opacity;
                    ctx.beginPath();
                    ctx.arc(cx,cy,c.thickness,0,Math.PI*2);
                    ctx.fill();
                } else {
                    ctx.strokeStyle = c.color;
                    ctx.lineWidth = c.thickness;
                    ctx.globalAlpha = c.opacity;
                    ctx.beginPath();
                    ctx.moveTo(cx-c.size-c.gap,cy);
                    ctx.lineTo(cx-c.gap,cy);
                    ctx.stroke();
                    ctx.beginPath();
                    ctx.moveTo(cx+c.gap,cy);
                    ctx.lineTo(cx+c.size+c.gap,cy);
                    ctx.stroke();
                    ctx.beginPath();
                    ctx.moveTo(cx,cy-c.size-c.gap);
                    ctx.lineTo(cx,cy-c.gap);
                    ctx.stroke();
                    ctx.beginPath();
                    ctx.moveTo(cx,cy+c.gap);
                    ctx.lineTo(cx,cy+c.size+c.gap);
                    ctx.stroke();
                }
            };

            draw();
            window.addEventListener('resize', ()=>{
                this.canvas.width=window.innerWidth;
                this.canvas.height=window.innerHeight;
                draw();
            });

            const s = document.createElement('style');
            s.id='null-hide-cross';
            s.textContent='.crosshair,#crosshair{display:none!important}';
            document.head.appendChild(s);
        }
        disable() {
            if(!this.on) return;
            this.on = false;
            if(this.canvas) this.canvas.remove();
            const s = document.getElementById('null-hide-cross');
            if(s) s.remove();
        }
    }

    // Widgets
    class Widgets {
        constructor(state, game, anim) {
            this.state = state;
            this.game = game;
            this.anim = anim;
            this.widgets = new Map();
            this.drag = null;
            this.initDrag();
        }
        initDrag() {
            document.addEventListener('mousemove', e => {
                if(!this.drag) return;
                const dx = e.clientX - this.drag.sx;
                const dy = e.clientY - this.drag.sy;
                if(!this.drag.dragging && Math.hypot(dx,dy)>5) this.drag.dragging=true;
                if(this.drag.dragging) {
                    this.drag.el.style.left = Math.max(0,this.drag.ix+dx)+'px';
                    this.drag.el.style.top = Math.max(0,this.drag.iy+dy)+'px';
                    this.drag.el.style.right='auto';
                    this.drag.el.style.bottom='auto';
                }
            });
            document.addEventListener('mouseup', () => {
                if(!this.drag) return;
                if(this.drag.dragging) {
                    const rect = this.drag.el.getBoundingClientRect();
                    this.state.set(`positions.${this.drag.key}`, {top:Math.round(rect.top),left:Math.round(rect.left)});
                } else if(this.state.get('locked')) {
                    this.toggle(this.drag.key);
                }
                this.drag = null;
            });
        }
        create(key, html, styles={}) {
            this.destroy(key);
            const w = document.createElement('div');
            w.className = `null-widget null-${key}`;
            w.innerHTML = html;
            Object.assign(w.style, {
                position:'fixed',
                background:'rgba(26,26,26,0.95)',
                border:'1px solid #555',
                borderRadius:'6px',
                padding:'8px',
                fontSize:'13px',
                fontFamily:'monospace',
                color:'#fff',
                zIndex:999999,
                userSelect:'none',
                cursor:this.state.get('locked')?'pointer':'move',
                boxShadow:'0 2px 8px rgba(0,0,0,0.5)',
                ...styles
            });
            this.applyPos(w,key);
            w.addEventListener('mousedown', e => {
                if(e.button!==0) return;
                const rect = w.getBoundingClientRect();
                this.drag = {key,el:w,sx:e.clientX,sy:e.clientY,ix:rect.left,iy:rect.top,dragging:false};
            });
            document.body.appendChild(w);
            this.widgets.set(key, w);
            return w;
        }
        destroy(key) {
            if(this.widgets.has(key)) {
                this.widgets.get(key).remove();
                this.widgets.delete(key);
            }
            this.anim.remove(key);
        }
        applyPos(el, key) {
            const pos = this.state.get(`positions.${key}`);
            if(!pos) return;
            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';
        }
        toggle(key) {
            const cur = this.state.get(`widgets.${key}`);
            this.state.set(`widgets.${key}`, !cur);
            !cur ? this.show(key) : this.hide(key);
        }
        show(key) {
            const handlers = {
                fps: () => this.createFPS(),
                cps: () => this.createCPS(),
                ping: () => this.createPing(),
                coords: () => this.createCoords(),
                speed: () => this.createSpeed(),
                keystrokes: () => this.createKeystrokes(),
                minimap: () => this.createMinimap(),
                reach: () => this.createReach()
            };
            if(handlers[key]) handlers[key]();
        }
        hide(key) {
            this.destroy(key);
        }
        createFPS() {
            this.create('fps', '60 FPS', {width:'85px',textAlign:'center',fontWeight:'bold'});
            let frames=0, last=Date.now();
            this.anim.add('fps', () => {
                if(!this.widgets.has('fps')) return;
                frames++;
                const now = Date.now();
                if(now>=last+1500) {
                    const fps = Math.round((frames*1000)/(now-last));
                    this.widgets.get('fps').textContent = `${Math.min(fps,999)} FPS`;
                    frames=0;
                    last=now;
                }
            });
        }
        createCPS() {
            this.create('cps', '0 | 0 CPS', {width:'100px',textAlign:'center',fontWeight:'bold'});
            let clicks = {l:[],r:[]};
            const handler = e => {
                const now = Date.now();
                if(e.button===0) clicks.l.push(now);
                else if(e.button===2) clicks.r.push(now);
            };
            document.addEventListener('mousedown', handler);
            let lastUpdate = Date.now();
            this.anim.add('cps', () => {
                if(!this.widgets.has('cps')) {
                    document.removeEventListener('mousedown',handler);
                    return;
                }
                const now = Date.now();
                if(now-lastUpdate>=1500) {
                    const cutoff = now-1000;
                    clicks.l = clicks.l.filter(t=>t>cutoff);
                    clicks.r = clicks.r.filter(t=>t>cutoff);
                    this.widgets.get('cps').textContent = `${clicks.l.length} | ${clicks.r.length} CPS`;
                    lastUpdate = now;
                }
            });
        }
        createPing() {
            this.create('ping', '... ms', {width:'95px',textAlign:'center'});
            const measure = async () => {
                try {
                    const start = performance.now();
                    await fetch('/favicon.ico?_='+Date.now(),{method:'HEAD',cache:'no-store',mode:'no-cors'});
                    const ping = Math.round(performance.now()-start);
                    if(this.widgets.has('ping')) this.widgets.get('ping').textContent=`${ping} ms`;
                } catch(e) {
                    if(this.widgets.has('ping')) this.widgets.get('ping').textContent='--- ms';
                }
            };
            measure();
            const interval = setInterval(()=>{
                if(this.widgets.has('ping')) measure();
                else clearInterval(interval);
            }, 3000);
        }
        createCoords() {
            this.create('coords', '<div style="opacity:0.6;font-size:10px;margin-bottom:3px">XYZ</div><div>0, 0, 0</div>', {width:'130px',textAlign:'center'});
            let lastUpdate = Date.now();
            this.anim.add('coords', () => {
                if(!this.widgets.has('coords')) return;
                const now = Date.now();
                if(now-lastUpdate>=1500) {
                    const pos = this.game.getPos();
                    const el = this.widgets.get('coords').querySelector('div:last-child');
                    if(el) el.textContent=`${Math.round(pos.x)}, ${Math.round(pos.y)}, ${Math.round(pos.z)}`;
                    lastUpdate = now;
                }
            });
        }
        createSpeed() {
            this.create('speed', '<div style="opacity:0.6;font-size:10px;margin-bottom:3px">SPEED</div><div>0.00 b/s</div>', {width:'105px',textAlign:'center'});
            let lastUpdate = Date.now();
            this.anim.add('speed', () => {
                if(!this.widgets.has('speed')) return;
                const now = Date.now();
                if(now-lastUpdate>=1500) {
                    const vel = this.game.getVel();
                    const speed = Math.min(100, Math.sqrt(vel.x**2+vel.z**2));
                    const el = this.widgets.get('speed').querySelector('div:last-child');
                    if(el) el.textContent=`${speed.toFixed(2)} b/s`;
                    lastUpdate = now;
                }
            });
        }
        createKeystrokes() {
            const mode = this.state.get('keystrokesMode');
            const keys = mode==='wasd'?['W','A','S','D']:['↑','←','↓','→'];
            const html = `
                <div style="display:grid;grid-template-columns:repeat(3,32px);grid-template-rows:repeat(3,32px);gap:3px">
                    <div></div>
                    <div class="key" data-i="0" style="background:#1a1a1a;border:1px solid #555;border-radius:4px;display:flex;align-items:center;justify-content:center;font-weight:bold;color:#666;transition:all 0.1s">${keys[0]}</div>
                    <div></div>
                    <div class="key" data-i="1" style="background:#1a1a1a;border:1px solid #555;border-radius:4px;display:flex;align-items:center;justify-content:center;font-weight:bold;color:#666;transition:all 0.1s">${keys[1]}</div>
                    <div class="key" data-i="2" style="background:#1a1a1a;border:1px solid #555;border-radius:4px;display:flex;align-items:center;justify-content:center;font-weight:bold;color:#666;transition:all 0.1s">${keys[2]}</div>
                    <div class="key" data-i="3" style="background:#1a1a1a;border:1px solid #555;border-radius:4px;display:flex;align-items:center;justify-content:center;font-weight:bold;color:#666;transition:all 0.1s">${keys[3]}</div>
                    <div></div>
                    <div style="grid-column:2;display:flex;gap:4px;margin-top:4px">
                        <div class="mb" data-mb="LMB" style="flex:1;background:#1a1a1a;border:1px solid #555;border-radius:3px;padding:3px 0;text-align:center;font-size:9px;color:#666;transition:all 0.1s">L</div>
                        <div class="mb" data-mb="RMB" style="flex:1;background:#1a1a1a;border:1px solid #555;border-radius:3px;padding:3px 0;text-align:center;font-size:9px;color:#666;transition:all 0.1s">R</div>
                    </div>
                    <div></div>
                </div>
            `;
            this.create('keystrokes', html, {padding:'6px',background:'rgba(10,10,10,0.98)'});
            const w = this.widgets.get('keystrokes');
            const keyMap = mode==='wasd'?['w','a','s','d']:['arrowup','arrowleft','arrowdown','arrowright'];
            const onDown=e=>{
                if(!this.widgets.has('keystrokes')) return;
                const idx = keyMap.indexOf(e.key.toLowerCase());
                if(idx!==-1) {
                    const k = w.querySelector(`[data-i="${idx}"]`);
                    if(k) {
                        k.style.background='#fff';
                        k.style.color='#000';
                        k.style.transform='scale(0.95)';
                    }
                }
            };
            const onUp=e=>{
                if(!this.widgets.has('keystrokes')) return;
                const idx = keyMap.indexOf(e.key.toLowerCase());
                if(idx!==-1) {
                    const k = w.querySelector(`[data-i="${idx}"]`);
                    if(k) {
                        k.style.background='#1a1a1a';
                        k.style.color='#666';
                        k.style.transform='scale(1)';
                    }
                }
            };
            const onMDown=e=>{
                if(!this.widgets.has('keystrokes')) return;
                const btn = e.button===0?'l':e.button===2?'r':null;
                if(btn) {
                    const m = w.querySelector(`[data-mb="${btn}"]`);
                    if(m) {
                        m.style.background='#fff';
                        m.style.color='#000';
                    }
                }
            };
            const onMUp=e=>{
                if(!this.widgets.has('keystrokes')) return;
                const btn = e.button===0?'l':e.button===2?'r':null;
                if(btn) {
                    const m = w.querySelector(`[data-mb="${btn}"]`);
                    if(m) {
                        m.style.background='#1a1a1a';
                        m.style.color='#666';
                    }
                }
            };
            document.addEventListener('keydown', onDown);
            document.addEventListener('keyup', onUp);
            document.addEventListener('mousedown', onMDown);
            document.addEventListener('mouseup', onMUp);
        }
        createMinimap() {
            this.create('minimap', '<div style="font-size:9px;opacity:0.6;margin-bottom:4px">MAP</div><canvas id="minimap-canvas" width="130" height="130" style="border-radius:4px;border:1px solid #555"></canvas>', {width:'150px',padding:'8px',textAlign:'center'});
            let last = 0;
            this.anim.add('minimap', now => {
                if(!this.widgets.has('minimap')) return;
                if(now-last<50) return;
                last = now;
                const c = document.getElementById('minimap-canvas');
                if(!c) return;
                const ctx = c.getContext('2d');
                const w=c.width, h=c.height;
                ctx.fillStyle='#0a0a0a';
                ctx.fillRect(0,0,w,h);
                ctx.strokeStyle='#1a1a1a';
                ctx.lineWidth=1;
                for(let i=0;i<w;i+=20) {
                    ctx.beginPath();
                    ctx.moveTo(i,0);
                    ctx.lineTo(i,h);
                    ctx.stroke();
                }
                for(let i=0;i<h;i+=20) {
                    ctx.beginPath();
                    ctx.moveTo(0,i);
                    ctx.lineTo(w,i);
                    ctx.stroke();
                }
                const cx=w/2, cy=h/2, rot=this.game.getRot();
                ctx.save();
                ctx.translate(cx,cy);
                ctx.rotate(rot);
                ctx.fillStyle='#fff';
                ctx.beginPath();
                ctx.moveTo(0,-8);
                ctx.lineTo(-5,5);
                ctx.lineTo(5,5);
                ctx.closePath();
                ctx.fill();
                ctx.restore();
            });
        }
        createReach() {
            this.create('reach', '<div style="opacity:0.6;font-size:10px;margin-bottom:3px">REACH</div><div>--- blocks</div>', {width:'110px',textAlign:'center'});
            let lastUpdate = Date.now();
            this.anim.add('reach', () => {
                if(!this.widgets.has('reach')) return;
                const now = Date.now();
                if(now-lastUpdate>=1500) {
                    const dist = this.game.getDist();
                    const el = this.widgets.get('reach').querySelector('div:last-child');
                    if(el) el.textContent = dist!==null?`${dist.toFixed(2)} blocks`:'--- blocks';
                    lastUpdate = now;
                }
            });
        }
    }

    // UI Manager
    class UI {
        constructor(client) {
            this.client = client;
            this.root = null;
            this.visible = false;
            this.menuBtn = null;
        }
        async create() {
            await new Promise(r=>setTimeout(r,1000));
            this.createMenuButton();
            this.createMenu();
        }
        createMenuButton() {
            this.menuBtn = document.createElement('div');
            this.menuBtn.innerHTML = 'NULL';
            this.menuBtn.style.cssText = `
                position:fixed;top:10px;right:10px;
                background:rgba(26,26,26,0.95);
                color:#fff;padding:10px 15px;
                border-radius:6px;cursor:pointer;
                font-family:monospace;font-weight:bold;
                font-size:12px;z-index:1000000;
                border:1px solid #555;
                box-shadow:0 2px 8px rgba(0,0,0,0.5);
                user-select:none;
            `;
            this.menuBtn.addEventListener('click', () => this.toggle());
            document.body.appendChild(this.menuBtn);
        }
        createMenu() {
            this.root = document.createElement('div');
            this.root.style.cssText = `position:fixed;inset:0;z-index:999999;display:none;background:rgba(0,0,0,0.8)`;
            const t = CONFIG.THEME;
            this.root.innerHTML = `
                <div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:750px;max-height:80vh;background:${t.GRAY};border:1px solid ${t.BORDER};border-radius:8px;overflow:hidden;display:flex;font-family:sans-serif;color:${t.TEXT};box-shadow:0 10px 40px rgba(0,0,0,0.9)">
                    <div style="width:160px;background:${t.DARK};padding:18px 12px;border-right:1px solid ${t.BORDER}">
                        <div style="display:flex;align-items:center;gap:8px;margin-bottom:18px;padding-bottom:15px;border-bottom:1px solid ${t.BORDER}">
                            <div style="font-size:14px;font-weight:600">Null Client</div>
                            <div style="font-size:9px;opacity:0.5">v${CONFIG.VERSION}</div>
                        </div>
                        <div id="null-nav">
                            <button data-tab="widgets" class="active">Widgets</button>
                            <button data-tab="features">Features</button>
                            <button data-tab="performance">Performance</button>
                            <button data-tab="shaders">☀ Shaders</button>
                            <button data-tab="settings">Settings</button>
                            <button id="close-btn">Close</button>
                        </div>
                    </div>
                    <div style="flex:1;padding:22px;overflow-y:auto" id="null-content"></div>
                </div>
            `;
            document.body.appendChild(this.root);

            const style = document.createElement('style');
            style.textContent = `
                #null-nav button {
                    width:100%;padding:9px;background:transparent;border:none;
                    color:${t.DIM};text-align:left;border-radius:4px;cursor:pointer;
                    font-size:12px;margin-bottom:3px;transition:all 0.2s;
                }
                #null-nav button:hover { background:${t.LIGHT};color:${t.TEXT}; }
                #null-nav button.active { background:${t.LIGHTER};color:${t.TEXT}; }
                #null-content::-webkit-scrollbar { width:8px; }
                #null-content::-webkit-scrollbar-thumb { background:${t.LIGHT};border-radius:4px; }
            `;
            document.head.appendChild(style);

            this.setupHandlers();
            this.switchTab('widgets');
        }
        setupHandlers() {
            document.getElementById('close-btn').onclick = () => this.hide();
            document.querySelectorAll('#null-nav button[data-tab]').forEach(btn => {
                btn.onclick = () => {
                    document.querySelectorAll('#null-nav button').forEach(b=>b.classList.remove('active'));
                    btn.classList.add('active');
                    this.switchTab(btn.dataset.tab);
                };
            });
            document.addEventListener('keydown', e => {
                if(e.code==='ShiftRight') {
                    e.preventDefault();
                    this.toggle();
                }
            });
        }
        show() {
            if(this.root) {
                this.root.style.display='block';
                this.visible=true;
            }
        }
        hide() {
            if(this.root) {
                this.root.style.display='none';
                this.visible=false;
            }
        }
        toggle() {
            this.visible ? this.hide() : this.show();
        }
        switchTab(tab) {
            const tabs = {
                widgets: () => this.renderWidgets(),
                features: () => this.renderFeatures(),
                performance: () => this.renderPerformance(),
                shaders: () => this.renderShaders(),
                settings: () => this.renderSettings()
            };
            if(tabs[tab]) tabs[tab]();
        }
        renderWidgets() {
            const c = document.getElementById('null-content');
            c.innerHTML = '<h2 style="margin-bottom:15px">Widgets</h2><div id="wg" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:12px"></div>';
            const g = document.getElementById('wg');
            const widgets = {fps:'FPS',cps:'CPS',ping:'Ping',coords:'Coords',speed:'Speed',keystrokes:'Keystrokes',minimap:'Minimap',reach:'Reach'};
            Object.entries(widgets).forEach(([k,t]) => {
                const on = this.client.state.get(`widgets.${k}`);
                g.innerHTML += `
                    <div style="background:${CONFIG.THEME.DARK};border:1px solid ${CONFIG.THEME.BORDER};border-radius:6px;padding:14px">
                        <div style="font-size:12px;font-weight:600;margin-bottom:5px">${t}</div>
                        <div style="font-size:10px;opacity:0.5;margin-bottom:10px">Toggle ${t.toLowerCase()}</div>
                        <div style="display:flex;justify-content:space-between;align-items:center">
                            <span style="font-size:10px">Enable</span>
                            <div class="sw ${on?'on':''}" data-w="${k}" style="width:36px;height:18px;border-radius:9px;background:${on?CONFIG.THEME.LIGHTER:CONFIG.THEME.DARKER};position:relative;cursor:pointer;transition:all 0.2s">
                                <div style="position:absolute;top:2px;left:${on?'20px':'2px'};width:14px;height:14px;border-radius:7px;background:#fff;transition:all 0.2s"></div>
                            </div>
                        </div>
                    </div>
                `;
            });
            setTimeout(() => {
                g.querySelectorAll('.sw').forEach(sw => {
                    sw.onclick = () => {
                        const k = sw.dataset.w;
                        const cur = this.client.state.get(`widgets.${k}`);
                        this.client.state.set(`widgets.${k}`, !cur);
                        !cur ? this.client.widgets.show(k) : this.client.widgets.hide(k);
                        this.renderWidgets();
                    };
                });
            },100);
        }
        renderFeatures() {
            const c = document.getElementById('null-content');
            c.innerHTML = '<h2 style="margin-bottom:15px">Features</h2><div id="fg" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:12px"></div>';
            const g = document.getElementById('fg');
            const features = {fullbright:'Fullbright',zoom:'Smooth Zoom',sprint:'Auto Sprint',bhop:'BHOP',fastPlace:'Fast Place',invCleaner:'Inv Cleaner',noShake:'No Camera Shake',hitParticles:'Hit Particles',smoothCamera:'Smooth Camera',crosshair:'Custom Crosshair',reach:'Reach Extender'};
            Object.entries(features).forEach(([k,t]) => {
                const on = this.client.state.get(`features.${k}`);
                g.innerHTML += `
                    <div style="background:${CONFIG.THEME.DARK};border:1px solid ${CONFIG.THEME.BORDER};border-radius:6px;padding:14px">
                        <div style="font-size:12px;font-weight:600;margin-bottom:5px">${t}</div>
                        <div style="font-size:10px;opacity:0.5;margin-bottom:10px">Toggle ${t.toLowerCase()}</div>
                        <div style="display:flex;justify-content:space-between;align-items:center">
                            <span style="font-size:10px">Enable</span>
                            <div class="sw ${on?'on':''}" data-f="${k}" style="width:36px;height:18px;border-radius:9px;background:${on?CONFIG.THEME.LIGHTER:CONFIG.THEME.DARKER};position:relative;cursor:pointer;transition:all 0.2s">
                                <div style="position:absolute;top:2px;left:${on?'20px':'2px'};width:14px;height:14px;border-radius:7px;background:#fff;transition:all 0.2s"></div>
                            </div>
                        </div>
                    </div>
                `;
            });
            g.innerHTML += `
                <div style="background:${CONFIG.THEME.DARK};border:1px solid ${CONFIG.THEME.BORDER};border-radius:6px;padding:14px">
                    <div style="font-size:12px;font-weight:600;margin-bottom:5px">Crosshair Style</div>
                    <select id="cross-sel" style="width:100%;padding:7px;background:${CONFIG.THEME.LIGHT};border:1px solid ${CONFIG.THEME.BORDER};border-radius:4px;color:#fff;font-size:11px;margin-top:8px">
                        <option value="default">Cross (Default)</option>
                        <option value="circle">Circle</option>
                        <option value="dot">Dot</option>
                    </select>
                </div>
                <div style="background:${CONFIG.THEME.DARK};border:1px solid ${CONFIG.THEME.BORDER};border-radius:6px;padding:14px">
                    <div style="font-size:12px;font-weight:600;margin-bottom:5px">Quality</div>
                    <select id="qual-sel" style="width:100%;padding:7px;background:${CONFIG.THEME.LIGHT};border:1px solid ${CONFIG.THEME.BORDER};border-radius:4px;color:#fff;font-size:11px;margin-top:8px">
                        <option value="1920x1080">1080p (Full HD)</option>
                        <option value="2560x1440">2K (1440p)</option>
                        <option value="3200x1800">1800p</option>
                        <option value="3840x2160">4K (2160p)</option>
                    </select>
                </div>
            `;
            setTimeout(() => {
                g.querySelectorAll('.sw').forEach(sw => {
                    sw.onclick = () => {
                        const k = sw.dataset.f;
                        const cur = this.client.state.get(`features.${k}`);
                        this.client.state.set(`features.${k}`, !cur);
                        this.client.toggleFeature(k, !cur);
                        this.renderFeatures();
                    };
                });
                const crossSel = document.getElementById('cross-sel');
                if(crossSel) {
                    crossSel.value = this.client.state.get('crosshair.style');
                    crossSel.onchange = () => {
                        this.client.state.set('crosshair.style', crossSel.value);
                        if(this.client.crosshair?.on) {
                            this.client.crosshair.disable();
                            this.client.crosshair.enable();
                        }
                    };
                }
                const qualSel = document.getElementById('qual-sel');
                if(qualSel) {
                    qualSel.value = this.client.state.get('quality.resolution');
                    qualSel.onchange = () => {
                        this.client.state.set('quality.resolution', qualSel.value);
                        const [w,h] = qualSel.value.split('x').map(Number);
                        const canvas = document.querySelector('canvas');
                        if(canvas) {
                            canvas.style.width = w+'px';
                            canvas.style.height = h+'px';
                        }
                    };
                }
            },100);
        }
        renderPerformance() {
            const c = document.getElementById('null-content');
            c.innerHTML = '<h2 style="margin-bottom:15px">Performance (120-240 FPS Boost)</h2><div id="pg" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:12px"></div>';
            const g = document.getElementById('pg');
            const perf = {particles:'Reduce Particles',shadows:'Disable Shadows',quality:'Lower Quality',render:'Optimize Renderer',draw:'Reduce Draw',boost:'FPS Boost'};
            Object.entries(perf).forEach(([k,t]) => {
                const on = this.client.state.get(`performance.${k}`);
                g.innerHTML += `
                    <div style="background:${CONFIG.THEME.DARK};border:1px solid ${CONFIG.THEME.BORDER};border-radius:6px;padding:14px">
                        <div style="font-size:12px;font-weight:600;margin-bottom:5px">${t}</div>
                        <div style="font-size:10px;opacity:0.5;margin-bottom:10px">Toggle ${t.toLowerCase()}</div>
                        <div style="display:flex;justify-content:space-between;align-items:center">
                            <span style="font-size:10px">Enable</span>
                            <div class="sw ${on?'on':''}" data-p="${k}" style="width:36px;height:18px;border-radius:9px;background:${on?CONFIG.THEME.LIGHTER:CONFIG.THEME.DARKER};position:relative;cursor:pointer;transition:all 0.2s">
                                <div style="position:absolute;top:2px;left:${on?'20px':'2px'};width:14px;height:14px;border-radius:7px;background:#fff;transition:all 0.2s"></div>
                            </div>
                        </div>
                    </div>
                `;
            });
            setTimeout(() => {
                g.querySelectorAll('.sw').forEach(sw => {
                    sw.onclick = () => {
                        const k = sw.dataset.p;
                        const cur = this.client.state.get(`performance.${k}`);
                        this.client.state.set(`performance.${k}`, !cur);
                        this.client.applyPerf();
                        this.renderPerformance();
                    };
                });
            },100);
        }
        renderShaders() {
            const c = document.getElementById('null-content');
            c.innerHTML = `
                <h2 style="margin-bottom:5px">☀ Shaders</h2>
                <div style="color:#f0ad4e;font-size:11px;margin-bottom:15px">⚠ WARNING: May reduce FPS and increase CPU usage</div>
                <div id="sg" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:12px"></div>
            `;
            const g = document.getElementById('sg');
            const on = this.client.state.get('shaders.enabled');
            const type = this.client.state.get('shaders.type');
            g.innerHTML += `
                <div style="background:${CONFIG.THEME.DARK};border:1px solid ${CONFIG.THEME.BORDER};border-radius:6px;padding:14px">
                    <div style="font-size:12px;font-weight:600;margin-bottom:5px">Shaders</div>
                    <div style="font-size:10px;opacity:0.5;margin-bottom:10px">Enable shader system</div>
                    <div style="display:flex;justify-content:space-between;align-items:center">
                        <span style="font-size:10px">Enable</span>
                        <div id="shader-sw" class="sw ${on?'on':''}" style="width:36px;height:18px;border-radius:9px;background:${on?CONFIG.THEME.LIGHTER:CONFIG.THEME.DARKER};position:relative;cursor:pointer;transition:all 0.2s">
                            <div style="position:absolute;top:2px;left:${on?'20px':'2px'};width:14px;height:14px;border-radius:7px;background:#fff;transition:all 0.2s"></div>
                        </div>
                    </div>
                </div>
                <div style="background:${CONFIG.THEME.DARK};border:1px solid ${CONFIG.THEME.BORDER};border-radius:6px;padding:14px">
                    <div style="font-size:12px;font-weight:600;margin-bottom:5px">Shader Type</div>
                    <div style="font-size:10px;opacity:0.5;margin-bottom:10px">Select shader</div>
                    <select id="shader-type" style="width:100%;padding:7px;background:${CONFIG.THEME.LIGHT};border:1px solid ${CONFIG.THEME.BORDER};border-radius:4px;color:#fff;font-size:11px">
                        <option value="morning" ${type==='morning'?'selected':''}>Sunny Morning</option>
                        <option value="sunset" ${type==='sunset'?'selected':''}>Sunset</option>
                    </select>
                </div>
            `;
            setTimeout(() => {
                const sw = document.getElementById('shader-sw');
                if(sw) {
                    sw.onclick = () => {
                        const cur = this.client.state.get('shaders.enabled');
                        this.client.state.set('shaders.enabled', !cur);
                        !cur ? this.client.shaders.enable() : this.client.shaders.disable();
                        this.renderShaders();
                    };
                }
                const sel = document.getElementById('shader-type');
                if(sel) {
                    sel.onchange = () => {
                        this.client.state.set('shaders.type', sel.value);
                        if(this.client.shaders.on) {
                            this.client.shaders.disable();
                            this.client.shaders.enable();
                        }
                    };
                }
            },100);
        }
        renderSettings() {
            const c = document.getElementById('null-content');
            const locked = this.client.state.get('locked');
            c.innerHTML = `
                <h2 style="margin-bottom:15px">Settings</h2>
                <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:12px">
                    <div style="background:${CONFIG.THEME.DARK};border:1px solid ${CONFIG.THEME.BORDER};border-radius:6px;padding:14px">
                        <div style="font-size:12px;font-weight:600;margin-bottom:5px">Widget Lock</div>
                        <div style="font-size:10px;opacity:0.5;margin-bottom:10px">Lock widget positions</div>
                        <div style="display:flex;justify-content:space-between;align-items:center">
                            <span style="font-size:10px" id="lock-lbl">${locked?'Locked':'Unlocked'}</span>
                            <div id="lock-sw" class="sw ${locked?'on':''}" style="width:36px;height:18px;border-radius:9px;background:${locked?CONFIG.THEME.LIGHTER:CONFIG.THEME.DARKER};position:relative;cursor:pointer;transition:all 0.2s">
                                <div style="position:absolute;top:2px;left:${locked?'20px':'2px'};width:14px;height:14px;border-radius:7px;background:#fff;transition:all 0.2s"></div>
                            </div>
                        </div>
                    </div>
                    <div style="background:${CONFIG.THEME.DARK};border:1px solid ${CONFIG.THEME.BORDER};border-radius:6px;padding:14px">
                        <div style="font-size:12px;font-weight:600;margin-bottom:5px">FOV</div>
                        <div style="font-size:10px;opacity:0.5;margin-bottom:10px">Field of view (30-120)</div>
                        <input type="number" id="fov-inp" value="${this.client.state.get('fov')}" min="30" max="120" style="width:100%;padding:7px;background:${CONFIG.THEME.LIGHT};border:1px solid ${CONFIG.THEME.BORDER};border-radius:4px;color:#fff;font-size:11px;margin-top:5px">
                        <button id="fov-btn" style="width:100%;padding:7px;background:${CONFIG.THEME.LIGHTER};border:none;border-radius:4px;color:#fff;cursor:pointer;font-size:11px;margin-top:6px">Apply</button>
                    </div>
                    <div style="background:${CONFIG.THEME.DARK};border:1px solid ${CONFIG.THEME.BORDER};border-radius:6px;padding:14px;grid-column:1/-1">
                        <div style="font-size:12px;font-weight:600;margin-bottom:5px">Reset Settings</div>
                        <div style="font-size:10px;opacity:0.5;margin-bottom:10px">Reset all settings to default</div>
                        <button id="reset-btn" style="width:100%;padding:7px;background:${CONFIG.THEME.LIGHT};border:none;border-radius:4px;color:#fff;cursor:pointer;font-size:11px;margin-top:5px">Reset All</button>
                    </div>
                </div>
            `;
            setTimeout(() => {
                const lockSw = document.getElementById('lock-sw');
                const lockLbl = document.getElementById('lock-lbl');
                if(lockSw && lockLbl) {
                    lockSw.onclick = () => {
                        const cur = this.client.state.get('locked');
                        this.client.state.set('locked', !cur);
                        this.renderSettings();
                    };
                }
                const fovBtn = document.getElementById('fov-btn');
                if(fovBtn) {
                    fovBtn.onclick = () => {
                        const v = parseInt(document.getElementById('fov-inp').value);
                        if(v>=30 && v<=120) {
                            this.client.state.set('fov', v);
                            this.client.game.setFOV(v);
                        }
                    };
                }
                const resetBtn = document.getElementById('reset-btn');
                if(resetBtn) {
                    resetBtn.onclick = () => {
                        if(confirm('Reset all settings?')) {
                            localStorage.removeItem(CONFIG.STORAGE);
                            location.reload();
                        }
                    };
                }
            },100);
        }
    }

    // Main Client
    class NullClient {
        constructor() {
            this.state = new State();
            this.game = new Game();
            this.anim = new Anim();
            this.perf = new Perf(this.game);
            this.shaders = new Shader(this.game, this.state);
            this.widgets = new Widgets(this.state, this.game, this.anim);
            this.ui = new UI(this);

            this.bhop = new BHOP(this.game, this.anim);
            this.fastPlace = new FastPlace(this.game, this.anim);
            this.sprint = new Sprint(this.game);
            this.zoom = new SmoothZoom(this.game, this.state, this.anim);
            this.hitParticles = new HitParticles(this.anim);
            this.crosshair = new CustomCrosshair(this.state);

            window.nullGame = this.game;
        }
        async init() {
            await this.ui.create();
            this.restore();
            this.anim.start();
            console.log('%c[Null Client] v1.3 Ready!', 'color:#5cb85c;font-weight:bold;font-size:14px');
        }
        restore() {
            Object.keys(this.state.get('widgets')).forEach(k => {
                if(this.state.get(`widgets.${k}`)) this.widgets.show(k);
            });
            const features = this.state.get('features');
            Object.keys(features).forEach(k => {
                if(features[k]) this.toggleFeature(k, true);
            });
            this.applyPerf();
            if(this.state.get('shaders.enabled')) this.shaders.enable();
        }
        toggleFeature(k, on) {
            const handlers = {
                fullbright: o => this.applyFullbright(o),
                zoom: o => o ? this.zoom.enable() : this.zoom.disable(),
                sprint: o => o ? this.sprint.enable() : this.sprint.disable(),
                bhop: o => o ? this.bhop.enable() : this.bhop.disable(),
                fastPlace: o => o ? this.fastPlace.enable(this.state.get('fastPlaceDelay')) : this.fastPlace.disable(),
                hitParticles: o => o ? this.hitParticles.enable() : this.hitParticles.disable(),
                crosshair: o => o ? this.crosshair.enable() : this.crosshair.disable()
            };
            if(handlers[k]) handlers[k](on);
        }
        applyFullbright(on) {
            const id = 'null-fullbright';
            const ex = document.getElementById(id);
            if(ex) ex.remove();
            if(on) {
                const s = document.createElement('style');
                s.id = id;
                s.textContent = 'canvas{filter:brightness(1.8)contrast(1.05)!important}';
                document.head.appendChild(s);
            }
        }
        applyPerf() {
            const settings = this.state.get('performance');
            const any = Object.values(settings).some(v => v);
            any ? this.perf.enable(settings) : this.perf.disable();
        }
    }

    // Initialize
    const client = new NullClient();
    setTimeout(() => client.init(), 1000);
    window.NullClient = client;

})();