// ==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);
});
})();