Spaces:
Build error
Build error
// Some parts of this scripts are based on or designed to be compatible-ish with: | |
// https://arpruss.github.io/gamepad.js (MIT Licensed) | |
const ExtensionApi = require("../../util/custom-ext-api-to-core.js"); | |
const Scratch = new ExtensionApi(true); | |
const AXIS_DEADZONE = 0.1; | |
const BUTTON_DEADZONE = 0.05; | |
/** | |
* @param {number|'any'} index 1-indexed index | |
* @returns {Gamepad[]} | |
*/ | |
const getGamepads = (index) => { | |
if (index === 'any') { | |
return navigator.getGamepads().filter(i => i); | |
} | |
const gamepad = navigator.getGamepads()[index - 1]; | |
if (gamepad) { | |
return [gamepad]; | |
} | |
return []; | |
}; | |
/** | |
* @param {Gamepad} gamepad | |
* @param {number|'any'} buttonIndex 1-indexed index | |
* @returns {boolean} false if button does not exist | |
*/ | |
const isButtonPressed = (gamepad, buttonIndex) => { | |
if (buttonIndex === 'any') { | |
return gamepad.buttons.some(i => i.pressed); | |
} | |
const button = gamepad.buttons[buttonIndex - 1]; | |
if (!button) { | |
return false; | |
} | |
return button.pressed; | |
}; | |
/** | |
* @param {Gamepad} gamepad | |
* @param {number} buttonIndex 1-indexed index | |
* @returns {number} 0 if button does not exist | |
*/ | |
const getButtonValue = (gamepad, buttonIndex) => { | |
const button = gamepad.buttons[buttonIndex - 1]; | |
if (!button) { | |
return 0; | |
} | |
const value = button.value; | |
if (value < BUTTON_DEADZONE) { | |
return 0; | |
} | |
return value; | |
}; | |
/** | |
* @param {Gamepad} gamepad | |
* @param {number} axisIndex 1-indexed index | |
* @returns {number} 0 if axis does not exist | |
*/ | |
const getAxisValue = (gamepad, axisIndex) => { | |
const axisValue = gamepad.axes[axisIndex - 1]; | |
if (typeof axisValue !== 'number') { | |
return 0; | |
} | |
if (Math.abs(axisValue) < AXIS_DEADZONE) { | |
return 0; | |
} | |
return axisValue; | |
}; | |
class GamepadExtension { | |
getInfo() { | |
return { | |
id: 'Gamepad', | |
name: 'Gamepad', | |
blocks: [ | |
{ | |
opcode: 'gamepadConnected', | |
blockType: Scratch.BlockType.BOOLEAN, | |
text: 'is gamepad [pad] connected?', | |
arguments: { | |
pad: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1', | |
menu: 'padMenu' | |
} | |
} | |
}, | |
{ | |
opcode: 'buttonDown', | |
blockType: Scratch.BlockType.BOOLEAN, | |
text: 'button [b] on pad [i] pressed?', | |
arguments: { | |
b: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1', | |
menu: 'buttonMenu' | |
}, | |
i: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1', | |
menu: 'padMenu' | |
} | |
} | |
}, | |
{ | |
opcode: 'buttonValue', | |
blockType: Scratch.BlockType.REPORTER, | |
text: 'value of button [b] on pad [i]', | |
arguments: { | |
b: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1', | |
menu: 'buttonMenu' | |
}, | |
i: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1', | |
menu: 'padMenu' | |
} | |
} | |
}, | |
{ | |
opcode: 'axisValue', | |
blockType: Scratch.BlockType.REPORTER, | |
text: 'value of axis [b] on pad [i]', | |
arguments: { | |
b: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1', | |
menu: 'axisMenu' | |
}, | |
i: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1', | |
menu: 'padMenu' | |
}, | |
}, | |
}, | |
'---', | |
{ | |
opcode: 'axisDirection', | |
blockType: Scratch.BlockType.REPORTER, | |
text: 'direction of axes [axis] on pad [pad]', | |
arguments: { | |
axis: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1', | |
menu: 'axesGroupMenu' | |
}, | |
pad: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1', | |
menu: 'padMenu' | |
} | |
} | |
}, | |
{ | |
opcode: 'axisMagnitude', | |
blockType: Scratch.BlockType.REPORTER, | |
text: 'magnitude of axes [axis] on pad [pad]', | |
arguments: { | |
axis: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1', | |
menu: 'axesGroupMenu' | |
}, | |
pad: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1', | |
menu: 'padMenu' | |
} | |
} | |
}, | |
/* | |
{ | |
opcode: 'buttonPressedReleased', | |
blockType: Scratch.BlockType.HAT, | |
text: 'button [b] [pr] of pad [i]', | |
arguments: { | |
b: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1' | |
}, | |
pr: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1', | |
menu: 'pressReleaseMenu' | |
}, | |
i: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1', | |
menu: 'padMenu' | |
}, | |
}, | |
}, | |
{ | |
opcode: 'axisMoved', | |
blockType: Scratch.BlockType.HAT, | |
text: 'axis [b] of pad [i] moved', | |
arguments: { | |
b: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1' | |
}, | |
i: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1', | |
menu: 'padMenu' | |
}, | |
}, | |
}, | |
*/ | |
'---', | |
{ | |
opcode: 'rumble', | |
blockType: Scratch.BlockType.COMMAND, | |
text: 'rumble strong [s] and weak [w] for [t] sec. on pad [i]', | |
arguments: { | |
s: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '0.25' | |
}, | |
w: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '0.5' | |
}, | |
t: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '0.25' | |
}, | |
i: { | |
type: Scratch.ArgumentType.NUMBER, | |
defaultValue: '1', | |
menu: 'padMenu' | |
}, | |
}, | |
}, | |
], | |
menus: { | |
padMenu: { | |
acceptReporters: true, | |
items: [ | |
{ | |
text: 'any', | |
value: 'any' | |
}, | |
{ | |
text: '1', | |
value: '1' | |
}, | |
{ | |
text: '2', | |
value: '2' | |
}, | |
{ | |
text: '3', | |
value: '3' | |
}, | |
{ | |
text: '4', | |
value: '4' | |
} | |
], | |
}, | |
buttonMenu: { | |
acceptReporters: true, | |
items: [ | |
// Based on an Xbox controller | |
{ | |
text: 'any', | |
value: 'any' | |
}, | |
{ | |
text: 'A (1)', | |
value: '1' | |
}, | |
{ | |
text: 'B (2)', | |
value: '2' | |
}, | |
{ | |
text: 'X (3)', | |
value: '3' | |
}, | |
{ | |
text: 'Y (4)', | |
value: '4' | |
}, | |
{ | |
text: 'Left bumper (5)', | |
value: '5' | |
}, | |
{ | |
text: 'Right bumper (6)', | |
value: '6' | |
}, | |
{ | |
text: 'Left trigger (7)', | |
value: '7' | |
}, | |
{ | |
text: 'Right trigger (8)', | |
value: '8' | |
}, | |
{ | |
text: 'Select/View (9)', | |
value: '9' | |
}, | |
{ | |
text: 'Start/Menu (10)', | |
value: '10' | |
}, | |
{ | |
text: 'Left stick (11)', | |
value: '11' | |
}, | |
{ | |
text: 'Right stick (12)', | |
value: '12' | |
}, | |
{ | |
text: 'D-pad up (13)', | |
value: '13' | |
}, | |
{ | |
text: 'D-pad down (14)', | |
value: '14' | |
}, | |
{ | |
text: 'D-pad left (15)', | |
value: '15' | |
}, | |
{ | |
text: 'D-pad right (16)', | |
value: '16' | |
}, | |
] | |
}, | |
axisMenu: { | |
acceptReporters: true, | |
items: [ | |
// Based on an Xbox controller | |
{ | |
text: 'Left stick horizontal (1)', | |
value: '1' | |
}, | |
{ | |
text: 'Left stick vertical (2)', | |
value: '2' | |
}, | |
{ | |
text: 'Right stick horizontal (3)', | |
value: '3' | |
}, | |
{ | |
text: 'Right stick vertical (4)', | |
value: '4' | |
} | |
] | |
}, | |
axesGroupMenu: { | |
acceptReporters: true, | |
items: [ | |
// Based on an Xbox controller | |
{ | |
text: 'Left stick (1 & 2)', | |
value: '1' | |
}, | |
{ | |
text: 'Right stick (3 & 4)', | |
value: '3' | |
} | |
] | |
}, | |
/* | |
pressReleaseMenu: [ | |
{ | |
text: 'press', | |
value: 1 | |
}, | |
{ | |
text: 'release', | |
value: 0 | |
} | |
], | |
*/ | |
} | |
}; | |
} | |
gamepadConnected ({pad}) { | |
return getGamepads(pad).length > 0; | |
} | |
buttonDown ({b, i}) { | |
for (const gamepad of getGamepads(i)) { | |
if (isButtonPressed(gamepad, b)) { | |
return true; | |
} | |
} | |
return false; | |
} | |
buttonValue ({b, i}) { | |
let greatestButton = 0; | |
for (const gamepad of getGamepads(i)) { | |
const value = getButtonValue(gamepad, b); | |
if (value > greatestButton) { | |
greatestButton = value; | |
} | |
} | |
return greatestButton; | |
} | |
axisValue ({b, i}) { | |
let greatestAxis = 0; | |
for (const gamepad of getGamepads(i)) { | |
const axis = getAxisValue(gamepad, b); | |
if (Math.abs(axis) > Math.abs(greatestAxis)) { | |
greatestAxis = axis; | |
} | |
} | |
return greatestAxis; | |
} | |
axisDirection ({axis, pad}) { | |
let greatestMagnitude = 0; | |
let direction = 90; | |
for (const gamepad of getGamepads(pad)) { | |
const horizontalAxis = getAxisValue(gamepad, axis); | |
const verticalAxis = getAxisValue(gamepad, +axis + 1); | |
const magnitude = Math.sqrt(horizontalAxis ** 2 + verticalAxis ** 2); | |
if (magnitude > greatestMagnitude) { | |
greatestMagnitude = magnitude; | |
direction = Math.atan2(verticalAxis, horizontalAxis) * 180 / Math.PI + 90; | |
if (direction < 0) { | |
direction += 360; | |
} | |
} | |
} | |
return direction; | |
} | |
axisMagnitude ({axis, pad}) { | |
let greatestMagnitude = 0; | |
for (const gamepad of getGamepads(pad)) { | |
const horizontalAxis = getAxisValue(gamepad, axis); | |
const verticalAxis = getAxisValue(gamepad, +axis + 1); | |
const magnitude = Math.sqrt(horizontalAxis ** 2 + verticalAxis ** 2); | |
if (magnitude > greatestMagnitude) { | |
greatestMagnitude = magnitude; | |
} | |
} | |
return greatestMagnitude; | |
} | |
rumble ({s, w, t, i}) { | |
const gamepads = getGamepads(i); | |
for (const gamepad of gamepads) { | |
// @ts-ignore | |
if (gamepad.vibrationActuator) { | |
// @ts-ignore | |
gamepad.vibrationActuator.playEffect('dual-rumble', { | |
startDelay: 0, | |
duration: t * 1000, | |
weakMagnitude: w, | |
strongMagnitude: s | |
}); | |
} | |
} | |
} | |
} | |
module.exports = GamepadExtension; |