import {EVENT_CODES, EVENT_TYPES, EventGamepad, EventGamepadAnalog, EventGamepadDigital, INPUT_CODES, MATHEMATICS, UTILS} from '../index.js';
/**
* The ordered list of the axes event codes of the gamepad.
* @type {Array<Array<string>>}
* @constant
* @private
*/
const $GAMEPAD_AXES = [
[INPUT_CODES.GAMEPAD_STANDARD.STICK_LEFT_LEFT, INPUT_CODES.GAMEPAD_STANDARD.STICK_LEFT_RIGHT],
[INPUT_CODES.GAMEPAD_STANDARD.STICK_LEFT_UP, INPUT_CODES.GAMEPAD_STANDARD.STICK_LEFT_DOWN],
[INPUT_CODES.GAMEPAD_STANDARD.STICK_RIGHT_LEFT, INPUT_CODES.GAMEPAD_STANDARD.STICK_RIGHT_RIGHT],
[INPUT_CODES.GAMEPAD_STANDARD.STICK_RIGHT_UP, INPUT_CODES.GAMEPAD_STANDARD.STICK_RIGHT_DOWN]
];
/**
* The ordered list of the buttons event codes of the gamepad.
* @type {Array<string>}
* @constant
* @private
*/
const $GAMEPAD_BUTTONS = [
INPUT_CODES.GAMEPAD_STANDARD.CLUSTER_RIGHT_BUTTON_BOTTOM,
INPUT_CODES.GAMEPAD_STANDARD.CLUSTER_RIGHT_BUTTON_RIGHT,
INPUT_CODES.GAMEPAD_STANDARD.CLUSTER_RIGHT_BUTTON_LEFT,
INPUT_CODES.GAMEPAD_STANDARD.CLUSTER_RIGHT_BUTTON_TOP,
INPUT_CODES.GAMEPAD_STANDARD.CLUSTER_FRONT_BUTTON_TOP_LEFT,
INPUT_CODES.GAMEPAD_STANDARD.CLUSTER_FRONT_BUTTON_TOP_RIGHT,
INPUT_CODES.GAMEPAD_STANDARD.CLUSTER_FRONT_BUTTON_BOTTOM_LEFT,
INPUT_CODES.GAMEPAD_STANDARD.CLUSTER_FRONT_BUTTON_BOTTOM_RIGHT,
INPUT_CODES.GAMEPAD_STANDARD.CLUSTER_CENTER_BUTTON_LEFT,
INPUT_CODES.GAMEPAD_STANDARD.CLUSTER_CENTER_BUTTON_RIGHT,
INPUT_CODES.GAMEPAD_STANDARD.STICK_LEFT_BUTTON,
INPUT_CODES.GAMEPAD_STANDARD.STICK_RIGHT_BUTTON,
INPUT_CODES.GAMEPAD_STANDARD.CLUSTER_LEFT_BUTTON_TOP,
INPUT_CODES.GAMEPAD_STANDARD.CLUSTER_LEFT_BUTTON_BOTTOM,
INPUT_CODES.GAMEPAD_STANDARD.CLUSTER_LEFT_BUTTON_LEFT,
INPUT_CODES.GAMEPAD_STANDARD.CLUSTER_LEFT_BUTTON_RIGHT,
INPUT_CODES.GAMEPAD_STANDARD.CLUSTER_CENTER_BUTTON_CENTER
];
/**
* Creates gamepad extensions.
*
* @example
*
* // minimal
* ExtensionGamepad.activate();
*
* @example
*
* // full
* ExtensionGamepad.activate(deadzone);
*/
class ExtensionGamepad {
/**
* Stores the deadzone of the gamepad axes (in [0, 1] range).
* @type {number}
* @public
* @readonly
* @static
*/
static DEADZONE_GAMEPAD_AXES = 1 / 8;
/**
* Stores the activated status.
* @type {boolean}
* @private
* @static
*/
static $activated = false;
/**
* Stores deadzone of the gamepad axes.
* @type {number}
* @private
*/
$deadzone;
/**
* Stores the index of the last connected gamepad.
* @type {number}
* @private
*/
$indexLastConnected;
/**
* Stores the gamepad state.
* @type {Map<string, boolean>}
* @private
*/
$stateGamepad;
/**
* Stores the unloaded status.
* @type {boolean}
* @private
*/
$unloaded;
/**
* Creates a new gamepad extension.
* @param {number} [$deadzone] The deadzone of the gamepad axes (in [0, 1] range).
* @protected
*/
constructor($deadzone = ExtensionGamepad.DEADZONE_GAMEPAD_AXES) {
this.$update = this.$update.bind(this);
this.$deadzone = $deadzone;
this.$stateGamepad = new Map();
this.$unloaded = false;
[...$GAMEPAD_BUTTONS, ...$GAMEPAD_AXES.flat()].forEach(($code) => {
this.$stateGamepad.set($code, false);
});
window.addEventListener(EVENT_TYPES.NATIVE.BEFORE_UNLOAD, this.$onBeforeUnload.bind(this));
window.addEventListener(EVENT_TYPES.NATIVE.GAMEPAD_CONNECTED, this.$onConnect.bind(this));
window.addEventListener(EVENT_TYPES.NATIVE.GAMEPAD_DISCONNECTED, this.$onDisconnect.bind(this));
window.addEventListener(EVENT_TYPES.GAMEPAD.GAMEPAD_VIBRATE, this.$onVibrate.bind(this));
window.requestAnimationFrame(this.$update);
}
/**
* Activates the extension.
* @param {number} [$deadzone] The deadzone of the gamepad axes (in [0, 1] range).
* @public
* @static
*/
static activate($deadzone = ExtensionGamepad.DEADZONE_GAMEPAD_AXES) {
if (ExtensionGamepad.$activated === true) {
return;
}
new ExtensionGamepad($deadzone);
ExtensionGamepad.$activated = true;
}
/**
* Called when the scope is about to be unloaded.
* @private
*/
$onBeforeUnload() {
this.$unloaded = true;
const gamepads = navigator.getGamepads();
const gamepad = gamepads[this.$indexLastConnected];
if (typeof gamepad === 'undefined') {
return;
}
if (typeof gamepad.vibrationActuator === 'undefined') {
return;
}
gamepad.vibrationActuator.reset()
.catch(UTILS.noop);
}
/**
* Called when the gamepad is connected.
* @param {GamepadEvent} $event The native gamepad connected event.
* @private
*/
$onConnect($event) {
if ($event.gamepad.mapping !== 'standard') {
return;
}
this.$stateGamepad.entries().forEach(([$code, $activated]) => {
if ($activated === true) {
this.$stateGamepad.set($code, false);
window.dispatchEvent(new EventGamepadDigital(EVENT_TYPES.GAMEPAD.GAMEPAD_UP, $code));
}
});
this.$indexLastConnected = $event.gamepad.index;
window.dispatchEvent(new EventGamepadDigital(EVENT_TYPES.GAMEPAD.GAMEPAD_CONNECT, INPUT_CODES.GAMEPAD_STANDARD.CONNECTED));
}
/**
* Called when the gamepad is disconnected.
* @param {GamepadEvent} $event The native gamepad disconnected event.
* @private
*/
$onDisconnect($event) {
if ($event.gamepad.index !== this.$indexLastConnected) {
return;
}
this.$stateGamepad.entries().forEach(([$code, $activated]) => {
if ($activated === true) {
this.$stateGamepad.set($code, false);
window.dispatchEvent(new EventGamepadDigital(EVENT_TYPES.GAMEPAD.GAMEPAD_UP, $code));
}
});
this.$indexLastConnected = undefined;
window.dispatchEvent(new EventGamepadDigital(EVENT_TYPES.GAMEPAD.GAMEPAD_CONNECT, INPUT_CODES.GAMEPAD_STANDARD.DISCONNECTED));
}
/**
* Called when a gamepad vibration is needed.
* @param {Event} $event The gamepad vibrate event.
* @private
*/
$onVibrate($event) {
if (this.$unloaded === true) {
return;
}
const gamepads = navigator.getGamepads();
const gamepad = gamepads[this.$indexLastConnected];
if (typeof gamepad === 'undefined') {
return;
}
if (typeof gamepad.vibrationActuator === 'undefined') {
return;
}
if ($event instanceof EventGamepad
&& $event.code === EVENT_CODES.GAMEPAD_STANDARD.VIBRATE_END) {
gamepad.vibrationActuator.reset()
.catch(UTILS.noop);
return;
}
if ($event instanceof EventGamepad
&& $event.code === EVENT_CODES.GAMEPAD_STANDARD.VIBRATE_START) {
gamepad.vibrationActuator.playEffect('dual-rumble', {
duration: $event.vibration.duration,
startDelay: 0,
strongMagnitude: $event.vibration.intensityFrequencyLow,
weakMagnitude: $event.vibration.intensityFrequencyHigh
})
.catch(UTILS.noop);
return;
}
}
/**
* Updates the state of the gamepad.
* @private
*/
$update() {
const gamepads = navigator.getGamepads();
const gamepad = gamepads[this.$indexLastConnected];
if (gamepad instanceof Gamepad) {
$GAMEPAD_BUTTONS.forEach(($button, $index) => {
const button = gamepad.buttons[$index];
if (button.pressed === true) {
if (this.$stateGamepad.get($button) === false) {
this.$stateGamepad.set($button, true);
}
window.dispatchEvent(new EventGamepadDigital(EVENT_TYPES.GAMEPAD.GAMEPAD_DOWN, $button));
window.dispatchEvent(new EventGamepadAnalog(EVENT_TYPES.GAMEPAD.GAMEPAD_ANALOG, $button, button.value));
}
else {
if (this.$stateGamepad.get($button) === true) {
this.$stateGamepad.set($button, false);
window.dispatchEvent(new EventGamepadDigital(EVENT_TYPES.GAMEPAD.GAMEPAD_UP, $button));
}
}
});
const [stickLeftX, stickLeftY, stickRightX, stickRightY] = gamepad.axes;
const axes = [
[stickLeftX, stickLeftY],
[stickRightX, stickRightY]
]
.map(([$x, $y]) => {
const magnitude = MATHEMATICS.hypotenuse($x, $y);
if (magnitude === 0) {
return [0, 0];
}
const value = MATHEMATICS.clamp(magnitude);
if (value < this.$deadzone) {
return [0, 0];
}
const zone = (value - this.$deadzone) / (1 - this.$deadzone);
return [
($x / magnitude) * zone,
($y / magnitude) * zone
];
})
.flat();
axes.forEach(($direction, $index) => {
const [axeMinimum, axeMaximum] = $GAMEPAD_AXES[$index];
if ($direction < 0) {
if (this.$stateGamepad.get(axeMaximum) === true) {
this.$stateGamepad.set(axeMaximum, false);
window.dispatchEvent(new EventGamepadDigital(EVENT_TYPES.GAMEPAD.GAMEPAD_UP, axeMaximum));
}
this.$stateGamepad.set(axeMinimum, true);
window.dispatchEvent(new EventGamepadDigital(EVENT_TYPES.GAMEPAD.GAMEPAD_DOWN, axeMinimum));
window.dispatchEvent(new EventGamepadAnalog(EVENT_TYPES.GAMEPAD.GAMEPAD_ANALOG, axeMinimum, Math.abs($direction)));
}
else if ($direction > 0) {
if (this.$stateGamepad.get(axeMinimum) === true) {
this.$stateGamepad.set(axeMinimum, false);
window.dispatchEvent(new EventGamepadDigital(EVENT_TYPES.GAMEPAD.GAMEPAD_UP, axeMinimum));
}
this.$stateGamepad.set(axeMaximum, true);
window.dispatchEvent(new EventGamepadDigital(EVENT_TYPES.GAMEPAD.GAMEPAD_DOWN, axeMaximum));
window.dispatchEvent(new EventGamepadAnalog(EVENT_TYPES.GAMEPAD.GAMEPAD_ANALOG, axeMaximum, Math.abs($direction)));
}
else {
if (this.$stateGamepad.get(axeMinimum) === true) {
this.$stateGamepad.set(axeMinimum, false);
window.dispatchEvent(new EventGamepadDigital(EVENT_TYPES.GAMEPAD.GAMEPAD_UP, axeMinimum));
}
if (this.$stateGamepad.get(axeMaximum) === true) {
this.$stateGamepad.set(axeMaximum, false);
window.dispatchEvent(new EventGamepadDigital(EVENT_TYPES.GAMEPAD.GAMEPAD_UP, axeMaximum));
}
}
});
}
window.requestAnimationFrame(this.$update);
}
}
export {
ExtensionGamepad
};
export default ExtensionGamepad;