Starblast Gamepad Script

script that adds gamepad support to starblast.io, you can play gamepad WITH other clients running.

// ==UserScript==
// @name          Starblast Gamepad Script
// @namespace     http://tampermonkey.net/
// @version       4.7
// @description   script that adds gamepad support to starblast.io, you can play gamepad WITH other clients running.
// @author        ঔ✧₱ⱠӾɎɆⱤ Ӿ✧ঔ
// @match         https://starblast.io/*
// @exclude       https://starblast.io/modding.html
// @license       idk what that is
// @grant         none
// @run-at        document-idle
// ==/UserScript==


(() => {
    const DEADZONE = 0.15;
    const TURN_RADIUS = 200;

    let currentShipIndex = 0;
    let currentUpgradeIndex = 1;
    const MAX_UPGRADE_INDEX = 8;

    let activeSelectorType = null; // 'ship', 'upgrade', or null (no selector active)

    const SELECTION_UI_ID = 'gamepad-selection-ui-unique';
    let selectionUI = null;

    const BUTTON_ACTIONS = {
        0:  { type: 'mouse', button: 0 },     // A (0): Fire (Left Click)
        1:  { type: 'key', key: 'alt' },      // B (1): Launch Secondary (Alt)
        2:  { type: 'key', key: 'g' },        // X (2): Simulate 'G' keypress
        3:  { type: 'key', key: 'x' },        // Y (3): Mapped to 'x' key (B3)
        4:  { type: 'key', key: 'v' },        // L1: Throw Gems (v)
        5:  { type: 'key', key: 'tab' },      // R1: Toggle between teams (Tab)
        6:  { type: 'key', key: 'control' },  // L2: RCS Toggle (Control)
        7:  { type: 'mouse', button: 2 },     // R2: Accelerate (Right Click)
        11: { type: 'key', key: 'c' },        // Right Stick Click: Chat (c)
    };
    const lastButtonState = {};

    const getCanvas = () => {
        const canvas = document.querySelector('canvas');
        return canvas;
    };

    function createSelectionUI() {
        if (!selectionUI) {
            selectionUI = document.createElement('div');
            selectionUI.id = SELECTION_UI_ID;
            Object.assign(selectionUI.style, {
                all: 'unset',
                position: 'fixed',
                top: '10px',
                right: '10px',
                padding: '8px 14px',
                backgroundColor: 'rgba(0, 0, 0, 0.85)',
                color: 'cyan', // Default idle color
                fontFamily: 'Arial, sans-serif',
                fontSize: '15px',
                fontWeight: 'bold',
                borderRadius: '10px',
                zIndex: '9999999999999',
                userSelect: 'none',
                pointerEvents: 'none',
                opacity: '1',
                visibility: 'hidden', // Starts hidden, will be shown by pollGamepad
                display: 'block',
                boxShadow: '0 0 10px cyan', // Default idle shadow
                textShadow: '0 0 6px cyan'  // Default idle text shadow
            });
            document.body.appendChild(selectionUI);
        } else if (!document.body.contains(selectionUI)) {
            document.body.appendChild(selectionUI);
        }
    }

    // Handles displaying different states of the UI
    function updateSelectionUI(type, data) {
        if (!selectionUI) return; // Ensure UI element exists

        if (type === 'no_gamepad') { // When no gamepad is connected at all
            selectionUI.style.visibility = 'hidden';
            selectionUI.textContent = '';
        } else if (type === 'connected_idle') { // Gamepad connected, but not in ship/upgrade selection
            selectionUI.textContent = 'Gamepad Connected';
            selectionUI.style.color = 'cyan';
            selectionUI.style.boxShadow = '0 0 10px cyan';
            selectionUI.style.textShadow = '0 0 6px cyan';
            selectionUI.style.visibility = 'visible'; // Ensure visible
        } else if (type === 'ship') {
            selectionUI.textContent = `Ship: ${data}`;
            selectionUI.style.color = '#f0f'; // Magenta for selection
            selectionUI.style.boxShadow = '0 0 10px #f0f';
            selectionUI.style.textShadow = '0 0 6px #f0f';
            selectionUI.style.visibility = 'visible'; // Ensure visible
        } else if (type === 'upgrade') {
            selectionUI.textContent = `Upgrade: ${data}`;
            selectionUI.style.color = '#f0f'; // Magenta for selection
            selectionUI.style.boxShadow = '0 0 10px #f0f';
            selectionUI.style.textShadow = '0 0 6px #f0f';
            selectionUI.style.visibility = 'visible'; // Ensure visible
        }
    }

    function cleanupUnknownUI() {
        const elements = document.body.querySelectorAll('div');
        elements.forEach(el => {
            if (el.id !== SELECTION_UI_ID) {
                const rect = el.getBoundingClientRect();
                const isSmallAndTopLeft = rect.width > 0 && rect.width < 50 &&
                                         rect.height > 0 && rect.height < 50 &&
                                         rect.left >= 0 && rect.left < 20 &&
                                         rect.top >= 0 && rect.top < 20;
                const isEmpty = !el.textContent.trim() && !el.children.length;
                if (isSmallAndTopLeft && isEmpty) {
                    el.remove();
                }
            }
        });
    }

    const dispatchMouseEvent = (type, button) => {
        const canvas = getCanvas();
        const mouseEvent = new MouseEvent(type, {
            bubbles: true,
            cancelable: true,
            button,
            buttons: 1 << button,
            clientX: window.innerWidth / 2,
            clientY: window.innerHeight / 2
        });

        window.dispatchEvent(mouseEvent);
        document.dispatchEvent(mouseEvent);
        if (canvas) {
            canvas.dispatchEvent(mouseEvent);
        }
    };

    const dispatchKeyboardEvent = (type, key) => {
        const canvas = getCanvas();
        let eventOptions = {
            key: key,
            bubbles: true,
            cancelable: true
        };
        switch (key.toLowerCase()) {
            case 'alt':
                eventOptions.keyCode = 18; eventOptions.which = 18; eventOptions.code = 'AltLeft'; eventOptions.altKey = true;
                break;
            case 'shift':
            case 'shiftleft':
                eventOptions.keyCode = 16; eventOptions.which = 16; eventOptions.code = 'ShiftLeft'; eventOptions.shiftKey = true;
                break;
            case 'shiftright':
                eventOptions.keyCode = 16; eventOptions.which = 16; eventOptions.code = 'ShiftRight'; eventOptions.shiftKey = true;
                break;
            case 'control':
                eventOptions.keyCode = 17; eventOptions.which = 17; eventOptions.code = 'ControlLeft'; eventOptions.ctrlKey = true;
                break;
            case 'tab':
                eventOptions.keyCode = 9; eventOptions.which = 9; eventOptions.code = 'Tab';
                break;
            case 'c':
                eventOptions.keyCode = 67; eventOptions.which = 67; eventOptions.code = 'KeyC';
                break;
            case 'v':
                eventOptions.keyCode = 86; eventOptions.which = 86; eventOptions.code = 'KeyV';
                break;
            case 'x':
                eventOptions.keyCode = 88; eventOptions.which = 88; eventOptions.code = 'KeyX';
                break;
            case 'g':
                eventOptions.keyCode = 71; eventOptions.which = 71; eventOptions.code = 'KeyG';
                break;
            case '0': case '1': case '2': case '3': case '4':
            case '5': case '6': case '7': case '8': case '9':
                eventOptions.keyCode = key.charCodeAt(0); eventOptions.which = eventOptions.keyCode; eventOptions.code = `Digit${key}`;
                break;
            default:
                if (key.length === 1) {
                    eventOptions.keyCode = key.toUpperCase().charCodeAt(0); eventOptions.which = eventOptions.keyCode; eventOptions.code = `Key${key.toUpperCase()}`;
                } else {
                    eventOptions.code = key;
                }
                break;
        }
        const keyboardEvent = new KeyboardEvent(type, eventOptions);

        window.dispatchEvent(keyboardEvent);
        document.dispatchEvent(keyboardEvent);
        if (canvas) {
            canvas.dispatchEvent(keyboardEvent);
            canvas.focus();
        }
    };

    const handleButton = (index, pressed) => {
        const action = BUTTON_ACTIONS[index];

        if (!action) return;

        if (action.type === 'mouse') {
            pressed
                ? dispatchMouseEvent("mousedown", action.button)
                : dispatchMouseEvent("mouseup", action.button);
        } else if (action.type === 'key') {
            if (pressed) {
                dispatchKeyboardEvent("keydown", action.key);
            } else {
                setTimeout(() => dispatchKeyboardEvent("keyup", action.key), 100);
            }
        }
    };

    const simulateMouseMove = (deltaX, deltaY) => {
        const canvas = getCanvas();
        if (!canvas) return;

        canvas.dispatchEvent(new MouseEvent("mousemove", {
            bubbles: true,
            cancelable: true,
            button: 0,
            buttons: 0,
            clientX: window.innerWidth / 2 + deltaX,
            clientY: window.innerHeight / 2 + deltaY,
            movementX: deltaX,
            movementY: deltaY
        }));
    };

    const handleTurn = (xAxis, yAxis) => {
        const magnitude = Math.sqrt(xAxis ** 2 + yAxis ** 2);
        if (magnitude < DEADZONE) {
            simulateMouseMove(0, 0);
            return;
        }

        const adjusted = (magnitude - DEADZONE) / (1 - DEADZONE);
        const normX = xAxis / magnitude;
        const normY = yAxis / magnitude;
        const deltaX = normX * adjusted * TURN_RADIUS;
        const deltaY = normY * adjusted * TURN_RADIUS;

        simulateMouseMove(deltaX, deltaY);
    };

    const pollGamepad = () => {
        let gp = null;
        const gamepads = navigator.getGamepads?.();

        if (gamepads) {
            for (let i = 0; i < gamepads.length; i++) {
                const currentGp = gamepads[i];
                if (!gp && currentGp && currentGp.buttons.length > 0 && currentGp.axes.length > 0) {
                    gp = currentGp;
                }
            }
        }

        if (!gp) {
            updateSelectionUI('no_gamepad', null); // Hide UI when no gamepad
            activeSelectorType = null;
            return;
        }

        // If gamepad is connected but no selector is active, show idle status
        if (!activeSelectorType) {
            updateSelectionUI('connected_idle', null);
        }

        const dPadUpPressed = gp.buttons[12] && gp.buttons[12].pressed;
        const dPadDownPressed = gp.buttons[13] && gp.buttons[13].pressed;
        const dPadLeftPressed = gp.buttons[14] && gp.buttons[14].pressed;
        const dPadRightPressed = gp.buttons[15] && gp.buttons[15].pressed;

        // D-pad Up (Ship Selector Toggle/Confirm)
        if (dPadUpPressed && !lastButtonState[12]) {
            if (activeSelectorType === 'ship') {
                // CONFIRM SHIP and EXIT SELECTION by pressing D-pad Up again
                dispatchKeyboardEvent("keydown", currentShipIndex.toString());
                setTimeout(() => dispatchKeyboardEvent("keyup", currentShipIndex.toString()), 100);
                activeSelectorType = null; // Exit selection mode
                // UI will revert to 'connected_idle' in the next pollGamepad cycle
            } else {
                // ENTER SHIP SELECTION (or switch from upgrade to ship)
                if (activeSelectorType === 'upgrade') {
                    activeSelectorType = null; // Exit upgrade mode if active
                }
                currentShipIndex = 0; // Default to ship 0 on entry
                activeSelectorType = 'ship';
                updateSelectionUI('ship', currentShipIndex); // Show ship selection UI
            }
        }

        // D-pad Down (Upgrade Selector Toggle/Confirm)
        if (dPadDownPressed && !lastButtonState[13]) {
            if (activeSelectorType === 'upgrade') {
                // CONFIRM UPGRADE and EXIT SELECTION by pressing D-pad Down again
                dispatchKeyboardEvent("keydown", currentUpgradeIndex.toString());
                setTimeout(() => dispatchKeyboardEvent("keyup", currentUpgradeIndex.toString()), 100);
                activeSelectorType = null; // Exit selection mode
                // UI will revert to 'connected_idle' in the next pollGamepad cycle
            } else {
                // ENTER UPGRADE SELECTION (or switch from ship to upgrade)
                if (activeSelectorType === 'ship') {
                    activeSelectorType = null; // Exit ship mode if active
                }
                currentUpgradeIndex = 1; // Default to upgrade 1 on entry
                activeSelectorType = 'upgrade';
                updateSelectionUI('upgrade', currentUpgradeIndex); // Show upgrade selection UI
            }
        }

        // D-pad Left/Right (Cycle Selector ONLY if a mode is active)
        if ((dPadLeftPressed && !lastButtonState[14]) || (dPadRightPressed && !lastButtonState[15])) {
            if (activeSelectorType === 'upgrade') {
                if (dPadLeftPressed && !lastButtonState[14]) {
                    currentUpgradeIndex = Math.max(1, currentUpgradeIndex - 1);
                } else if (dPadRightPressed && !lastButtonState[15]) {
                    currentUpgradeIndex = Math.min(MAX_UPGRADE_INDEX, currentUpgradeIndex + 1);
                }
                updateSelectionUI('upgrade', currentUpgradeIndex); // ONLY update UI, do NOT apply upgrade yet
            } else if (activeSelectorType === 'ship') {
                currentShipIndex = (currentShipIndex === 0) ? 9 : 0; // Toggle between 0 and 9
                updateSelectionUI('ship', currentShipIndex); // Update UI for new ship
            }
        }

        lastButtonState[12] = dPadUpPressed;
        lastButtonState[13] = dPadDownPressed;
        lastButtonState[14] = dPadLeftPressed;
        lastButtonState[15] = dPadRightPressed;

        gp.buttons.forEach((btn, i) => {
            if (i >= 12 && i <= 15) return; // Skip D-pad buttons
            const pressed = btn.pressed;
            if (pressed !== lastButtonState[i]) {
                handleButton(i, pressed);
                lastButtonState[i] = pressed;
            }
        });
        handleTurn(gp.axes[0], gp.axes.length > 1 ? gp.axes[1] : 0); // Ensure axis 1 exists before using it
    };

    const startLoop = () => {
        createSelectionUI();
        setInterval(cleanupUnknownUI, 1000);

        const loop = () => {
            pollGamepad();
            requestAnimationFrame(loop);
        };
        requestAnimationFrame(loop);
    };

    window.addEventListener('load', () => {
        setTimeout(startLoop, 15000);
    });
})();