Thunder Virtual Controller

A highly customizable virtual controller for Xbox Cloud Gaming.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Thunder Virtual Controller
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  A highly customizable virtual controller for Xbox Cloud Gaming.
// @author       Navneetkrh
// @license MIT
// @match        https://www.xbox.com/*/play*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    console.log("[Thunder Controller] v1.0 Loaded.");

    // --- 1. CONFIGURATION ---
    const DEFAULT_CONFIG = {
        // Visuals
        opacity: 1.0,        
        scale: 1.0,

        // Vibration
        vibrationEnabled: true,
        vibrationStrength: 1.0,

        // POSITIONS (Global Groups)
        leftX: 40, leftY: 40,
        rightX: 40, rightY: 40,
        centerY: 20,

        // INDIVIDUAL OFFSETS
        off_stick_x: 0, off_stick_y: 0,
        off_dpad_x: 0, off_dpad_y: 0,
        off_ls_x: 0, off_ls_y: 0,
        off_lt_x: 0, off_lt_y: 0,
        off_lb_x: 0, off_lb_y: 0,
        off_view_x: 0, off_view_y: 0,

        off_abxy_x: 0, off_abxy_y: 0,
        off_rs_x: 0, off_rs_y: 0,
        off_rt_x: 0, off_rt_y: 0,
        off_rb_x: 0, off_rb_y: 0,
        off_menu_x: 0, off_menu_y: 0,
        
        off_guide_x: 0, off_guide_y: 0,

        // Touch
        swipeSensX: 15.0, 
        swipeSensY: 15.0,
        
        // Physics
        deadzoneCounterweight: 0.15, 
        noiseGate: 0.02,             

        // Gyro
        gyroEnabled: false,
        gyroSensX: 1.5,
        gyroSensY: 1.5,
        invertGyroX: true,
        invertGyroY: false,
        
        // Tuning
        deadzone: 0.10,
        responseCurve: 1.0
    };

    let config = { ...DEFAULT_CONFIG };
    let activeTab = 'Layout'; 
    let isEditing = false;

    const ID_MAP = {
        'elem-stick': ['off_stick_x', 'off_stick_y'],
        'grp-dpad':   ['off_dpad_x', 'off_dpad_y'],
        'btn-10':     ['off_ls_x', 'off_ls_y'],
        'btn-6':      ['off_lt_x', 'off_lt_y'], 
        'btn-4':      ['off_lb_x', 'off_lb_y'], 
        'btn-8':      ['off_view_x', 'off_view_y'],
        'grp-abxy':   ['off_abxy_x', 'off_abxy_y'],
        'btn-11':     ['off_rs_x', 'off_rs_y'],
        'btn-7':      ['off_rt_x', 'off_rt_y'], 
        'btn-5':      ['off_rb_x', 'off_rb_y'], 
        'btn-9':      ['off_menu_x', 'off_menu_y'],
        'btn-16':     ['off_guide_x', 'off_guide_y']
    };

    try {
        const saved = localStorage.getItem("bx-controller-v33-2");
        if (saved) config = { ...DEFAULT_CONFIG, ...JSON.parse(saved) };
    } catch (e) { console.error(e); }

    function saveConfig() {
        localStorage.setItem("bx-controller-v33-2", JSON.stringify(config));
        applyLayout();
    }

    // --- 2. CONTROLLER EMULATION ---
    const virtualGamepad = {
        id: "Xbox 360 Controller (Standard)",
        index: 0, 
        connected: true, 
        timestamp: 0, 
        mapping: "standard",
        axes: [0, 0, 0, 0], 
        buttons: new Array(17).fill(null).map(() => ({ pressed: false, value: 0 })),
        
        vibrationActuator: {
            type: "dual-rumble",
            playEffect: (type, params) => {
                if (!config.vibrationEnabled || !navigator.vibrate) return Promise.resolve();
                const intensity = Math.max(params.weakMagnitude || 0, params.strongMagnitude || 0);
                if (intensity > 0.05) {
                    const scaledIntensity = intensity * config.vibrationStrength;
                    if (scaledIntensity > 0.1) navigator.vibrate(Math.min(params.duration, 200)); 
                } else {
                    navigator.vibrate(0);
                }
                return Promise.resolve("success");
            },
            reset: () => {
                if (navigator.vibrate) navigator.vibrate(0);
                return Promise.resolve("success");
            }
        }
    };

    const originalGetGamepads = navigator.getGamepads;
    navigator.getGamepads = function() { return [virtualGamepad, null, null, null]; };

    function triggerConnectionEvent() {
        const event = new Event("gamepadconnected");
        event.gamepad = virtualGamepad;
        window.dispatchEvent(event);
        console.log("[Thunder Controller] Connected.");
    }
    
    if (document.readyState === "complete") {
        triggerConnectionEvent();
    } else {
        window.addEventListener('load', triggerConnectionEvent);
    }
    setTimeout(triggerConnectionEvent, 3000);


    // --- 3. INPUT STATE ---
    let inputState = {
        touchX: 0, touchY: 0,
        gyroX: 0, gyroY: 0,
        lastTouchX: null, lastTouchY: null,
        stopTimeoutId: null
    };


    // --- 4. PHYSICS ENGINE ---
    function applyPhysics(x, y) {
        if (Math.abs(x) < config.noiseGate) x = 0;
        if (Math.abs(y) < config.noiseGate) y = 0;

        let len = Math.sqrt(x*x + y*y);
        if (len > 0 && len < config.deadzoneCounterweight) {
            let boost = config.deadzoneCounterweight / len;
            x *= boost;
            y *= boost;
        }

        if (config.responseCurve !== 1.0) {
            if (Math.abs(x) > 0) x = Math.sign(x) * Math.pow(Math.abs(x), config.responseCurve);
            if (Math.abs(y) > 0) y = Math.sign(y) * Math.pow(Math.abs(y), config.responseCurve);
        }

        return { x, y };
    }


    // --- 5. GYRO LOGIC ---
    function handleMotion(e) {
        if (!config.gyroEnabled || !e.rotationRate) return;
        const rate = e.rotationRate;
        let dX = 0, dY = 0;

        if (window.innerWidth > window.innerHeight) {
            dX = rate.alpha || 0;
            dY = rate.beta || 0;
            if (Math.abs(window.orientation) === 90) dY = -dY;
        } else {
            dX = rate.gamma || 0;
            dY = rate.beta || 0;
        }

        const baseDivisor = 150;
        let rawX = (dX / baseDivisor) * config.gyroSensX;
        let rawY = (dY / baseDivisor) * config.gyroSensY;

        if (config.invertGyroX) rawX = -rawX;
        if (config.invertGyroY) rawY = -rawY;

        const phys = applyPhysics(rawX, rawY);
        
        inputState.gyroX = Math.max(-1, Math.min(1, phys.x));
        inputState.gyroY = Math.max(-1, Math.min(1, phys.y));
    }
    window.addEventListener("devicemotion", handleMotion, true);


    // --- 6. VELOCITY LOGIC ---
    function processVelocityInput(dx, dy) {
        let vx = (dx * 0.05) * config.swipeSensX; 
        let vy = (dy * 0.05) * config.swipeSensY;

        const phys = applyPhysics(vx, vy);

        inputState.touchX = phys.x;
        inputState.touchY = phys.y;

        if (inputState.stopTimeoutId) clearTimeout(inputState.stopTimeoutId);
        inputState.stopTimeoutId = setTimeout(() => {
            inputState.touchX = 0;
            inputState.touchY = 0;
        }, 50); 
    }


    // --- 7. MIXER LOOP ---
    function updateControllerFrame() {
        let finalX = inputState.touchX + inputState.gyroX;
        let finalY = inputState.touchY + inputState.gyroY;

        if (Math.abs(finalX) < config.deadzone) finalX = 0;
        if (Math.abs(finalY) < config.deadzone) finalY = 0;

        if (finalX > 1) finalX = 1; if (finalX < -1) finalX = -1;
        if (finalY > 1) finalY = 1; if (finalY < -1) finalY = -1;

        virtualGamepad.axes[2] = finalX;
        virtualGamepad.axes[3] = finalY;
        virtualGamepad.timestamp = performance.now();

        requestAnimationFrame(updateControllerFrame);
    }
    requestAnimationFrame(updateControllerFrame);


    // --- 8. UI HELPERS ---
    function createEl(type, id, css) {
        const el = document.createElement(type);
        el.id = id;
        if (css.className) { el.className = css.className; delete css.className; }
        Object.assign(el.style, {
            position: 'absolute', display: 'flex', justifyContent: 'center', alignItems: 'center',
            userSelect: 'none', touchAction: 'none', ...css
        });
        return el;
    }

    function createButton(text, id, css) {
        const btn = createEl('div', id, {
            backgroundColor: 'rgba(255, 255, 255, 0.15)', color: 'white', borderRadius: '50%',
            fontFamily: 'sans-serif', fontWeight: 'bold', pointerEvents: 'auto',
            border: '1px solid rgba(255, 255, 255, 0.3)', backdropFilter: 'blur(2px)', ...css
        });
        if(text) btn.innerText = text;
        return btn;
    }

    function showToast(message) {
        const toast = document.createElement('div');
        toast.innerText = message;
        Object.assign(toast.style, {
            position: 'fixed', bottom: '80px', left: '50%', transform: 'translateX(-50%)',
            backgroundColor: 'rgba(16, 124, 16, 0.9)', color: 'white', padding: '10px 20px',
            borderRadius: '20px', fontFamily: 'sans-serif', fontSize: '14px', zIndex: '9999999',
            boxShadow: '0 4px 10px rgba(0,0,0,0.5)', opacity: '0', transition: 'opacity 0.3s ease'
        });
        document.body.appendChild(toast);
        setTimeout(() => toast.style.opacity = '1', 10);
        setTimeout(() => {
            toast.style.opacity = '0';
            setTimeout(() => document.body.removeChild(toast), 300);
        }, 2000);
    }

    // --- DYNAMIC LAYOUT ENGINE ---
    function applyLayout() {
        const layer = document.getElementById('bx-controls-layer');
        if (!layer) return;

        layer.style.opacity = config.opacity;

        layer.querySelectorAll('.scalable-group').forEach(g => {
            let s = config.scale;
            let baseTransform = (g.id === 'grp-center') ? 'translateX(-50%)' : '';
            g.style.transform = `${baseTransform} scale(${s})`;
        });

        const leftGroup = document.getElementById('grp-left');
        const rightGroup = document.getElementById('grp-right');
        const centerGroup = document.getElementById('grp-center');
        
        if (leftGroup) {
            leftGroup.style.left = `${config.leftX}px`;
            leftGroup.style.bottom = `${config.leftY}px`;
        }
        if (rightGroup) {
            rightGroup.style.right = `${config.rightX}px`;
            rightGroup.style.bottom = `${config.rightY}px`;
        }
        if (centerGroup) {
            centerGroup.style.top = `${config.centerY}px`;
        }

        const setPos = (id, xKey, yKey) => {
            const el = document.getElementById(id);
            if(el) el.style.transform = `translate(${config[xKey]}px, ${-config[yKey]}px)`;
        };

        for (const [id, keys] of Object.entries(ID_MAP)) {
            setPos(id, keys[0], keys[1]);
        }
    }

    // --- DRAG & DROP LOGIC ---
    let dragTarget = null;
    let dragStartMouseX = 0, dragStartMouseY = 0;
    let dragStartConfigX = 0, dragStartConfigY = 0;

    function enableDrag(el) {
        el.addEventListener('touchstart', onDragStart, {passive: false});
        el.addEventListener('mousedown', onDragStart);
    }

    function onDragStart(e) {
        if (!isEditing) return;
        e.preventDefault(); e.stopPropagation();

        const id = e.currentTarget.id;
        if (!ID_MAP[id]) return;

        dragTarget = e.currentTarget;
        const clientX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
        const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
        
        dragStartMouseX = clientX;
        dragStartMouseY = clientY;
        
        const keys = ID_MAP[id];
        dragStartConfigX = config[keys[0]];
        dragStartConfigY = config[keys[1]];

        document.addEventListener('touchmove', onDragMove, {passive: false});
        document.addEventListener('touchend', onDragEnd);
        document.addEventListener('mousemove', onDragMove);
        document.addEventListener('mouseup', onDragEnd);
        
        dragTarget.style.filter = "drop-shadow(0 0 5px yellow)";
    }

    function onDragMove(e) {
        if (!dragTarget) return;
        e.preventDefault();

        const clientX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
        const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;

        const deltaX = (clientX - dragStartMouseX) / config.scale;
        const deltaY = (clientY - dragStartMouseY) / config.scale;

        const keys = ID_MAP[dragTarget.id];
        config[keys[0]] = dragStartConfigX + deltaX;
        config[keys[1]] = dragStartConfigY - deltaY;

        dragTarget.style.transform = `translate(${config[keys[0]]}px, ${-config[keys[1]]}px)`;
    }

    function onDragEnd(e) {
        if (dragTarget) {
            dragTarget.style.filter = "";
            dragTarget = null;
            saveConfig();
        }
        document.removeEventListener('touchmove', onDragMove);
        document.removeEventListener('touchend', onDragEnd);
        document.removeEventListener('mousemove', onDragMove);
        document.removeEventListener('mouseup', onDragEnd);
    }


    // --- 9. MAIN UI ---
    function initUI() {
        if (document.getElementById('bx-virtual-root')) return;
        
        const root = createEl('div', 'bx-virtual-root', { width: '100%', height: '100%', top: '0', left: '0', position: 'fixed', zIndex: '999999', pointerEvents: 'none' });
        document.body.appendChild(root);
        
        const controlsLayer = createEl('div', 'bx-controls-layer', { id: 'bx-controls-layer', width: '100%', height: '100%', top: '0', left: '0', pointerEvents: 'none' });
        root.appendChild(controlsLayer);

        const sysLayer = createEl('div', 'bx-sys-layer', { 
            bottom: '10px', left: '50%', transform: 'translateX(-50%)', 
            display: 'flex', gap: '20px', padding: '0', 
            backgroundColor: 'transparent', 
            pointerEvents: 'auto', zIndex: '1500'
        });
        root.appendChild(sysLayer); 
        
        const btnStyle = { position: 'relative', width: '40px', height: '40px', fontSize: '20px', backgroundColor: 'rgba(0,0,0,0.5)', border: '1px solid #555', borderRadius: '50%' };
        
        const hideBtn = createButton("👁️", "btn-hide", btnStyle);
        let areControlsVisible = true;
        hideBtn.onclick = () => { 
            areControlsVisible = !areControlsVisible; 
            controlsLayer.style.display = areControlsVisible ? 'block' : 'none';
            if (!areControlsVisible) {
                inputState.touchX = 0; inputState.touchY = 0;
                inputState.gyroX = 0; inputState.gyroY = 0;
                virtualGamepad.axes = [0,0,0,0];
            }
            hideBtn.innerText = areControlsVisible ? "👁️" : "🚫";
            hideBtn.style.opacity = areControlsVisible ? "1.0" : "0.5";
        };
        sysLayer.appendChild(hideBtn);

        const editBtn = createButton("✏️", "btn-edit", btnStyle);
        editBtn.onclick = () => {
            isEditing = !isEditing;
            editBtn.style.backgroundColor = isEditing ? '#107c10' : 'rgba(0,0,0,0.5)';
            if(isEditing) {
                showToast("Edit Mode: Drag buttons to move");
                controlsLayer.classList.add('editing-mode');
            } else {
                controlsLayer.classList.remove('editing-mode');
                saveConfig();
            }
        };
        sysLayer.appendChild(editBtn);

        const gearBtn = createButton("⚙️", "btn-settings", btnStyle);
        sysLayer.appendChild(gearBtn);

        const fullBtn = createButton("⛶", "btn-full", btnStyle);
        fullBtn.onclick = () => {
            if (!document.fullscreenElement) {
                document.documentElement.requestFullscreen().catch(e => console.log(e));
                fullBtn.innerText = "><";
            } else {
                document.exitFullscreen();
                fullBtn.innerText = "⛶";
            }
        };
        sysLayer.appendChild(fullBtn);


        // --- SETTINGS PANEL ---
        const settingsWrapper = createEl('div', 'bx-settings-wrapper', {
            top: '0', left: '0', width: '100%', height: '100%', 
            display: 'none', justifyContent: 'center', alignItems: 'center',
            backgroundColor: 'rgba(0,0,0,0.5)', pointerEvents: 'auto', zIndex: '2000'
        });
        root.appendChild(settingsWrapper);

        const settingsPanel = createEl('div', 'bx-settings-panel', {
            position: 'relative', width: '400px', maxHeight: '90vh',
            backgroundColor: 'rgba(20,20,20,0.95)', borderRadius: '12px', border: '1px solid #555', 
            backdropFilter: 'blur(10px)', display: 'flex', flexDirection: 'column', 
            color: 'white', fontFamily: 'sans-serif', 
            boxShadow: '0 10px 30px rgba(0,0,0,0.8)'
        });
        
        settingsPanel.addEventListener('touchstart', (e) => e.stopPropagation());
        settingsPanel.addEventListener('touchmove', (e) => e.stopPropagation());
        settingsPanel.addEventListener('touchend', (e) => e.stopPropagation());
        settingsWrapper.appendChild(settingsPanel);
        
        function addControlRow(container, label, key, min, max, step) {
            const row = document.createElement('div');
            row.style.marginBottom = '12px';
            const header = document.createElement('div');
            header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.marginBottom = '4px';
            const txt = document.createElement('span'); txt.innerText = label; txt.style.fontSize = '14px'; txt.style.color = '#ccc';
            const numInput = document.createElement('input');
            numInput.type = 'number'; numInput.value = config[key]; numInput.step = step;
            numInput.style.width = '60px'; numInput.style.background = '#333'; numInput.style.color = 'white'; numInput.style.border = '1px solid #555'; numInput.style.borderRadius = '4px'; numInput.style.textAlign = 'center';
            const rangeInput = document.createElement('input');
            rangeInput.type = 'range'; rangeInput.min = min; rangeInput.max = max; rangeInput.step = step; rangeInput.value = config[key];
            rangeInput.style.width = '100%'; rangeInput.style.marginTop = '2px';

            rangeInput.oninput = (e) => { numInput.value = e.target.value; config[key] = parseFloat(e.target.value); saveConfig(); };
            numInput.onchange = (e) => { rangeInput.value = e.target.value; config[key] = parseFloat(e.target.value); saveConfig(); };

            header.appendChild(txt); header.appendChild(numInput);
            row.appendChild(header); row.appendChild(rangeInput);
            container.appendChild(row);
        }

        function addCheckRow(container, label, key) {
            const row = document.createElement('div');
            row.style.display = 'flex'; row.style.justifyContent = 'space-between'; row.style.alignItems = 'center'; row.style.marginBottom = '10px';
            const txt = document.createElement('span'); txt.innerText = label; txt.style.fontSize = '14px';
            const chk = document.createElement('input'); chk.type = 'checkbox'; chk.checked = config[key];
            chk.style.width = '20px'; chk.style.height = '20px';
            chk.onchange = (e) => { config[key] = e.target.checked; saveConfig(); };
            row.appendChild(txt); row.appendChild(chk);
            container.appendChild(row);
        }

        function renderSettings() {
            settingsPanel.innerHTML = '';
            
            const header = document.createElement('div');
            header.style.padding = '15px'; header.style.borderBottom = '1px solid #444'; header.style.textAlign = 'center';
            header.innerHTML = '<h3 style="margin:0; font-size:18px;">Controller Settings</h3>';
            settingsPanel.appendChild(header);

            const tabsBar = document.createElement('div');
            tabsBar.style.display = 'flex'; tabsBar.style.gap = '5px'; tabsBar.style.borderBottom = '1px solid #444';
            const tabs = ['General', 'Layout', 'Touch', 'Physics', 'Gyro'];
            
            tabs.forEach(tabName => {
                const tab = document.createElement('div');
                tab.innerText = tabName;
                tab.style.flex = '1'; tab.style.padding = '10px 0'; tab.style.textAlign = 'center'; tab.style.cursor = 'pointer'; tab.style.fontSize = '12px';
                if(activeTab === tabName) {
                    tab.style.backgroundColor = '#2a2a2a'; tab.style.borderBottom = '2px solid #107c10'; tab.style.color = '#fff';
                } else {
                    tab.style.color = '#888';
                }
                tab.onclick = () => { activeTab = tabName; renderSettings(); };
                tabsBar.appendChild(tab);
            });
            settingsPanel.appendChild(tabsBar);

            const content = document.createElement('div');
            content.style.padding = '15px'; content.style.overflowY = 'auto'; content.style.flex = '1';
            
            if(activeTab === 'General') {
                addControlRow(content, "Global Scale", "scale", 0.5, 1.5, 0.1);
                addControlRow(content, "Global Opacity", "opacity", 0.1, 1.0, 0.1);
                addCheckRow(content, "Enable Vibration", "vibrationEnabled");
                addControlRow(content, "Vibration Strength", "vibrationStrength", 0.0, 1.0, 0.1);
                
                content.appendChild(document.createElement('hr')).style.borderColor = '#333';
                
                const btnRow = document.createElement('div');
                btnRow.style.display = 'flex'; btnRow.style.gap = '10px'; btnRow.style.marginTop = '10px';
                
                const createActionBtn = (txt, action, bg = '#222') => {
                    const b = document.createElement('button'); b.innerText = txt;
                    b.style.flex = '1'; b.style.padding = '8px'; b.style.backgroundColor = bg; b.style.color = 'white'; b.style.border = '1px solid #555'; b.style.borderRadius = '4px';
                    b.onclick = action; return b;
                };

                btnRow.appendChild(createActionBtn("Export", () => {
                    navigator.clipboard.writeText(JSON.stringify(config)).then(() => showToast("Copied!"));
                }));
                
                btnRow.appendChild(createActionBtn("Import", () => {
                    const data = prompt("Paste config:");
                    if(data) {
                        try {
                            const parsed = JSON.parse(data);
                            config = { ...DEFAULT_CONFIG, ...parsed };
                            saveConfig();
                            renderSettings(); 
                            showToast("Loaded!");
                        } catch(e) { alert("Invalid!"); }
                    }
                }));

                content.appendChild(btnRow);
                
                const resetBtn = createActionBtn("Reset to Defaults", () => {
                    if(confirm("Reset all?")) {
                        config = { ...DEFAULT_CONFIG };
                        saveConfig();
                        renderSettings();
                        showToast("Reset!");
                    }
                }, '#500');
                resetBtn.style.width = '100%'; resetBtn.style.marginTop = '10px';
                content.appendChild(resetBtn);
            }
            else if(activeTab === 'Layout') {
                content.innerHTML += '<p style="font-size:12px; color:#aaa; text-align:center;">Tip: Use ✏️ Pencil to drag & drop buttons!</p>';
                
                content.appendChild(document.createElement('div')).style.cssText = "font-size:11px; color:#aaa; margin-top:5px; font-weight:bold; border-bottom:1px solid #444; padding-bottom:2px;"; content.lastChild.innerText = "LEFT HAND";
                addControlRow(content, "Group X", "leftX", 0, 1000, 5);
                addControlRow(content, "Group Y", "leftY", 0, 800, 5);
                addControlRow(content, "Stick X", "off_stick_x", -300, 300, 5);
                addControlRow(content, "Stick Y", "off_stick_y", -300, 300, 5);
                addControlRow(content, "D-Pad X", "off_dpad_x", -300, 300, 5);
                addControlRow(content, "D-Pad Y", "off_dpad_y", -300, 300, 5);
                addControlRow(content, "LS (Click) X", "off_ls_x", -300, 300, 5);
                addControlRow(content, "LS (Click) Y", "off_ls_y", -300, 300, 5);
                addControlRow(content, "View X", "off_view_x", -300, 300, 5);
                addControlRow(content, "View Y", "off_view_y", -300, 300, 5);
                addControlRow(content, "LB Offset X", "off_lb_x", -300, 300, 5);
                addControlRow(content, "LB Offset Y", "off_lb_y", -300, 300, 5);
                addControlRow(content, "LT Offset X", "off_lt_x", -300, 300, 5);
                addControlRow(content, "LT Offset Y", "off_lt_y", -300, 300, 5);
                
                content.appendChild(document.createElement('div')).style.cssText = "font-size:11px; color:#aaa; margin-top:15px; font-weight:bold; border-bottom:1px solid #444; padding-bottom:2px;"; content.lastChild.innerText = "RIGHT HAND";
                addControlRow(content, "Group X", "rightX", 0, 1000, 5);
                addControlRow(content, "Group Y", "rightY", 0, 800, 5);
                addControlRow(content, "ABXY X", "off_abxy_x", -300, 300, 5);
                addControlRow(content, "ABXY Y", "off_abxy_y", -300, 300, 5);
                addControlRow(content, "RS (Click) X", "off_rs_x", -300, 300, 5);
                addControlRow(content, "RS (Click) Y", "off_rs_y", -300, 300, 5);
                addControlRow(content, "Menu X", "off_menu_x", -300, 300, 5);
                addControlRow(content, "Menu Y", "off_menu_y", -300, 300, 5);
                addControlRow(content, "RB Offset X", "off_rb_x", -300, 300, 5);
                addControlRow(content, "RB Offset Y", "off_rb_y", -300, 300, 5);
                addControlRow(content, "RT Offset X", "off_rt_x", -300, 300, 5);
                addControlRow(content, "RT Offset Y", "off_rt_y", -300, 300, 5);

                content.appendChild(document.createElement('div')).style.cssText = "font-size:11px; color:#aaa; margin-top:15px; font-weight:bold; border-bottom:1px solid #444; padding-bottom:2px;"; content.lastChild.innerText = "CENTER";
                addControlRow(content, "Group Y", "centerY", 0, 200, 5);
                addControlRow(content, "Guide X", "off_guide_x", -300, 300, 5);
                addControlRow(content, "Guide Y", "off_guide_y", -300, 300, 5);
            } 
            else if(activeTab === 'Touch') {
                addControlRow(content, "X Sensitivity", "swipeSensX", 0.1, 50.0, 0.1);
                addControlRow(content, "Y Sensitivity", "swipeSensY", 0.1, 50.0, 0.1);
            } 
            else if(activeTab === 'Physics') {
                addControlRow(content, "Noise Gate", "noiseGate", 0.0, 0.1, 0.01);
                addControlRow(content, "Min Speed (Kick)", "deadzoneCounterweight", 0.0, 0.5, 0.01);
                addControlRow(content, "Deadzone", "deadzone", 0.0, 0.5, 0.01);
                addControlRow(content, "Response Curve", "responseCurve", 0.2, 1.0, 0.1);
            } 
            else if(activeTab === 'Gyro') {
                addCheckRow(content, "Enable Gyroscope", "gyroEnabled");
                addControlRow(content, "Gyro X Sens", "gyroSensX", 0.1, 30.0, 0.1);
                addControlRow(content, "Gyro Y Sens", "gyroSensY", 0.1, 30.0, 0.1);
                addCheckRow(content, "Invert X", "invertGyroX");
                addCheckRow(content, "Invert Y", "invertGyroY");
            }
            
            settingsPanel.appendChild(content);

            const footer = document.createElement('div');
            footer.style.padding = '10px 15px'; footer.style.borderTop = '1px solid #444';
            const close = document.createElement('button'); close.innerText = "Close"; 
            close.style.width = '100%'; close.style.padding = '10px'; close.style.backgroundColor = '#333'; close.style.color = 'white'; close.style.border = 'none'; close.style.borderRadius = '5px';
            close.onclick = () => settingsWrapper.style.display = 'none';
            footer.appendChild(close);
            settingsPanel.appendChild(footer);
        }
        
        renderSettings();
        gearBtn.onclick = () => { renderSettings(); settingsWrapper.style.display = 'flex'; };


        // =========================================================================
        // --- CONTROLS LAYER ---
        // =========================================================================
        
        const makeGroup = (cls, css, origin) => {
            return createEl('div', cls, { 
                className: 'game-controls scalable-group', 
                ...css, 
                pointerEvents: 'none', 
                'data-origin': origin,
                zIndex: '1000'
            });
        };

        // LEFT SIDE
        const leftGroup = makeGroup('grp-left', { bottom: '40px', left: '40px', width: '250px', height: '250px' }, 'bottom left');
        leftGroup.id = 'grp-left'; 
        controlsLayer.appendChild(leftGroup);

        const lStick = createButton("", "stick-left", { bottom: '0', left: '0', width: '120px', height: '120px', border: '2px dashed rgba(255,255,255,0.3)' });
        lStick.id = 'elem-stick'; enableDrag(lStick);
        const lKnob = createButton("", "knob-left", { width: '50px', height: '50px', background: 'rgba(255,255,255,0.5)', position: 'relative', border: 'none', pointerEvents: 'none' });
        lStick.appendChild(lKnob);
        leftGroup.appendChild(lStick);

        const lsBtn = createButton("LS", "btn-10", {
            bottom: '125px', left: '35px', width: '50px', height: '30px', borderRadius: '8px',
            backgroundColor: 'rgba(50,50,50,0.8)', fontSize: '12px', zIndex: '1000', className: 'game-controls'
        });
        enableDrag(lsBtn); leftGroup.appendChild(lsBtn);

        const dpadBtn = (txt, id, t, l) => createButton(txt, id, { width: '35px', height: '35px', top: t, left: l, borderRadius: '5px', backgroundColor: 'rgba(40,40,40,0.8)' });
        const dpadGroup = createEl('div', 'grp-dpad', { bottom: '0', left: '140px', width: '110px', height: '110px', pointerEvents: 'auto' });
        dpadGroup.id = 'grp-dpad'; enableDrag(dpadGroup);
        dpadGroup.appendChild(dpadBtn("↑", "btn-12", '0px', '37.5px'));
        dpadGroup.appendChild(dpadBtn("↓", "btn-13", '75px', '37.5px'));
        dpadGroup.appendChild(dpadBtn("←", "btn-14", '37.5px', '0px'));
        dpadGroup.appendChild(dpadBtn("→", "btn-15", '37.5px', '75px'));
        leftGroup.appendChild(dpadGroup);

        const lb = createButton("LB", "btn-4", { bottom: '160px', left: '0px', width: '70px', height: '35px', borderRadius: '8px', fontSize: '12px', borderTopLeftRadius:'15px', className: 'game-controls' });
        enableDrag(lb); leftGroup.appendChild(lb);
        const lt = createButton("LT", "btn-6", { bottom: '200px', left: '0px', width: '70px', height: '40px', borderRadius: '8px', fontSize: '12px', borderBottomLeftRadius:'15px', className: 'game-controls' });
        enableDrag(lt); leftGroup.appendChild(lt);
        
        const view = createButton("⧉", "btn-8", { bottom: '160px', left: '140px', width: '40px', height: '40px', backgroundColor: 'rgba(50,50,50,0.8)', fontSize: '16px', className: 'game-controls' });
        enableDrag(view); leftGroup.appendChild(view);


        // RIGHT SIDE
        const rightGroup = makeGroup('grp-right', { bottom: '40px', right: '40px', width: '200px', height: '200px' }, 'bottom right');
        rightGroup.id = 'grp-right';
        controlsLayer.appendChild(rightGroup);

        const abxyGroup = createEl('div', 'grp-abxy', { bottom: '20px', right: '0', width: '150px', height: '150px', pointerEvents: 'auto' });
        abxyGroup.id = 'grp-abxy'; enableDrag(abxyGroup);
        const faceBtn = (txt, id, col, t, l) => createButton(txt, id, { width: '50px', height: '50px', backgroundColor: col, top: t, left: l, fontSize: '18px', border: '1px solid rgba(255,255,255,0.2)' });
        abxyGroup.appendChild(faceBtn("Y", "btn-3", "rgba(240, 200, 0, 0.3)", "0px", "50px"));
        abxyGroup.appendChild(faceBtn("B", "btn-1", "rgba(255, 50, 50, 0.3)", "50px", "100px"));
        abxyGroup.appendChild(faceBtn("A", "btn-0", "rgba(50, 200, 50, 0.3)", "100px", "50px"));
        abxyGroup.appendChild(faceBtn("X", "btn-2", "rgba(50, 50, 255, 0.3)", "50px", "0px"));
        rightGroup.appendChild(abxyGroup);

        const rsBtn = createButton("RS", "btn-11", {
            bottom: '20px', right: '160px', width: '50px', height: '30px', borderRadius: '8px',
            backgroundColor: 'rgba(50,50,50,0.8)', fontSize: '12px', zIndex: '1000', className: 'game-controls'
        });
        enableDrag(rsBtn); rightGroup.appendChild(rsBtn);

        const rb = createButton("RB", "btn-5", { bottom: '160px', right: '0px', width: '70px', height: '35px', borderRadius: '8px', fontSize: '12px', borderTopRightRadius:'15px', className: 'game-controls' });
        enableDrag(rb); rightGroup.appendChild(rb);
        
        const rt = createButton("RT", "btn-7", { bottom: '200px', right: '0px', width: '70px', height: '40px', borderRadius: '8px', fontSize: '12px', borderBottomRightRadius:'15px', className: 'game-controls' });
        enableDrag(rt); rightGroup.appendChild(rt);

        const menu = createButton("☰", "btn-9", { bottom: '160px', right: '100px', width: '40px', height: '40px', backgroundColor: 'rgba(50,50,50,0.8)', fontSize: '16px', className: 'game-controls' });
        enableDrag(menu); rightGroup.appendChild(menu);


        // TOUCHPAD
        const rTouchpad = createEl('div', "touchpad-right", { 
            className: 'game-controls', 
            top: '0', right: '0', width: '50%', height: '100%', 
            zIndex: '500', display: 'block', pointerEvents: 'auto' 
        });
        const rFloatKnob = createButton("", "knob-float", { width: '50px', height: '50px', border: '2px solid rgba(255,255,255,0.8)', position: 'absolute', display: 'none', pointerEvents: 'none' });
        rTouchpad.appendChild(rFloatKnob);
        controlsLayer.appendChild(rTouchpad);

        // CENTER MENU
        const centerGroup = makeGroup('grp-center', { top: '20px', left: '50%', width: '60px', height: '60px', transform: 'translateX(-50%)' }, 'top center');
        centerGroup.id = 'grp-center';
        const guide = createButton("⨂", "btn-16", { top: '0', left: '0', width: '45px', height: '45px', backgroundColor: '#107c10', border: '1px solid #fff', className: 'game-controls' });
        enableDrag(guide); centerGroup.appendChild(guide);
        controlsLayer.appendChild(centerGroup);


        // --- LOGIC ---

        function handleBtn(id, pressed) {
            if(isEditing) return;
            const index = parseInt(id.split('-')[1]);
            virtualGamepad.buttons[index].pressed = pressed;
            virtualGamepad.buttons[index].value = pressed ? 1.0 : 0.0;
            const el = document.getElementById(id);
            if(el) {
                if(pressed) {
                    el.style.transform = (el.style.transform || '') + " scale(0.9)";
                    el.style.filter = "brightness(1.5)";
                } else {
                    el.style.transform = el.style.transform.replace(" scale(0.9)", "");
                    el.style.filter = "none";
                }
            }
        }

        controlsLayer.querySelectorAll('[id^="btn-"]').forEach(btn => {
            const press = (e) => { if(!isEditing) { e.preventDefault(); e.stopPropagation(); handleBtn(btn.id, true); } };
            const release = (e) => { if(!isEditing) { e.preventDefault(); e.stopPropagation(); handleBtn(btn.id, false); } };
            btn.addEventListener('touchstart', press); btn.addEventListener('touchend', release);
            btn.addEventListener('mousedown', press); btn.addEventListener('mouseup', release);
        });

        // Left Stick
        const handleFixedStick = (e, axisX, axisY, knob) => {
            if(isEditing) return;
            e.preventDefault(); e.stopPropagation();
            const touch = e.targetTouches ? e.targetTouches[0] : e;
            const rect = knob.parentElement.getBoundingClientRect();
            let nx = ((touch.clientX - rect.left) - (rect.width / 2)) / (rect.width / 2);
            let ny = ((touch.clientY - rect.top) - (rect.height / 2)) / (rect.height / 2);
            const dist = Math.sqrt(nx*nx + ny*ny);
            if (dist > 1) { nx /= dist; ny /= dist; }
            if(axisX === 0) { virtualGamepad.axes[0] = nx; virtualGamepad.axes[1] = ny; }
            else { inputState.touchX = nx; inputState.touchY = ny; }
            knob.style.transform = `translate(${nx * 25}px, ${ny * 25}px)`;
        };

        const resetStick = (e, axisX, axisY, knob) => {
            if(isEditing) return;
            e.preventDefault();
            if (axisX === 0) { virtualGamepad.axes[0] = 0; virtualGamepad.axes[1] = 0; }
            knob.style.transform = `translate(0px, 0px)`;
        };

        lStick.addEventListener('touchmove', (e) => handleFixedStick(e, 0, 1, lKnob));
        lStick.addEventListener('touchend', (e) => resetStick(e, 0, 1, lKnob));

        // Right Swipe
        rTouchpad.addEventListener('touchstart', (e) => {
            if(isEditing) return;
            e.preventDefault();
            const touch = e.targetTouches[0];
            inputState.lastTouchX = touch.clientX; inputState.lastTouchY = touch.clientY;
            rFloatKnob.style.display = 'block'; 
            rFloatKnob.style.left = (touch.clientX - 25) + 'px'; rFloatKnob.style.top = (touch.clientY - 25) + 'px';
        });

        rTouchpad.addEventListener('touchmove', (e) => {
            if(isEditing) return;
            e.preventDefault();
            const touch = e.targetTouches[0];
            if (inputState.lastTouchX !== null) {
                let dx = touch.clientX - inputState.lastTouchX;
                let dy = touch.clientY - inputState.lastTouchY;
                processVelocityInput(dx, dy);
                rFloatKnob.style.left = (touch.clientX - 25) + 'px'; rFloatKnob.style.top = (touch.clientY - 25) + 'px';
            }
            inputState.lastTouchX = touch.clientX; inputState.lastTouchY = touch.clientY;
        });
        
        rTouchpad.addEventListener('touchend', (e) => {
            if(isEditing) return;
            e.preventDefault();
            rFloatKnob.style.display = 'none';
        });

        // Initial Layout
        applyLayout();
    }

    window.addEventListener('load', () => setTimeout(initUI, 1000));

})();