Thunder Virtual Controller

A highly customizable virtual controller for Xbox Cloud Gaming.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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));

})();