input/GamePad.js

import EventEmitter from "../../common/events/EventEmitter.js";
import { fixed } from "../../common/math/Utils.js";

/**
 * @see https://w3c.github.io/gamepad/#dfn-standard-gamepad
 */
class GamePad extends EventEmitter {
    constructor(gamepad) {
        super();

        this.index = gamepad.index;
        this.id = gamepad.id;

        this.buttons = {};
        this.sticks = {};

        this.deadzones = [];
        this.default_deadzone = 0.2;

        /**
         * threshold at which a stick triggers a single directional event 
         */
        this.axeThreshold = 0.5;
        this.directions = {
            "_all": { "up": false, "down": false, "left": false, "right": false }
        };
    }

    /**
     * Set the axis deadzone(s)
     * @param {Number} dz 0-1
     * @param {Number} a optional specify the axis
     */
    setDeadzone(dz, a = null) {
        this.default_deadzone = dz;

        if (s !== null) {
            this.deadzones[a] = dz;
        } else {
            for (let a = 0; a < this.deadzones.length; a++) {
                this.deadzones[a] = dz;
            }
        }
    }

    update(gamepad) {
        if (gamepad.buttons) {
            for (let b = 0; b < gamepad.buttons.length; b++) {
                const btn = gamepad.buttons[b];

                if (typeof this.buttons[b] !== "undefined" && this.buttons[b] !== btn.value) {
                    this.emit("button", {
                        button: b,
                        pressed: btn.pressed,
                        value: btn.value,
                        gamepad: this
                    });
                }

                this.buttons[b] = btn.value;

                if (b === 12) this.updateDirection("button" + b, "up", btn.pressed);
                if (b === 13) this.updateDirection("button" + b, "down", btn.pressed);
                if (b === 14) this.updateDirection("button" + b, "left", btn.pressed);
                if (b === 15) this.updateDirection("button" + b, "right", btn.pressed);
            }
        }

        if (gamepad.axes) {
            if (gamepad.axes.length % 2 !== 0) {
                console.warn("Connected gamepad does not have an even amount of axes");
            }

            for (let s = 0; s < gamepad.axes.length; s += 2) {
                if (typeof this.deadzones[s] === "undefined") {
                    this.deadzones[s] = this.default_deadzone;
                }

                let x = fixed(gamepad.axes[s], 4);
                let y = fixed(gamepad.axes[s + 1], 4);

                if (this.deadzones[s] > 0) {
                    if (Math.abs(x) < this.deadzones[s]) {
                        x = 0;
                    }

                    if (Math.abs(y) < this.deadzones[s]) {
                        y = 0;
                    }

                    //x is no longer a value between 0 and 1, its deadzone - 1, normalize it back to 0 - 1
                    x = ((Math.abs(x) - this.deadzones[s]) * Math.sign(x)) / (1 - this.deadzones[s]);
                    y = ((Math.abs(y) - this.deadzones[s]) * Math.sign(y)) / (1 - this.deadzones[s]);
                }

                if (typeof this.sticks[s] === "undefined") {
                    this.sticks[s] = { x, y };
                } else {
                    if (x !== this.sticks[s].x || y !== this.sticks[s].y) {
                        this.emit("stick", {
                            stick: s,
                            x: x,
                            y: y,
                            gamepad: this
                        });
                    }

                    this.sticks[s].x = x;
                    this.sticks[s].y = y;
                }

                this.updateDirection("stick" + s, "left", x <= -this.axeThreshold);
                this.updateDirection("stick" + s, "right", x >= this.axeThreshold);
                this.updateDirection("stick" + s, "up", y <= -this.axeThreshold);
                this.updateDirection("stick" + s, "down", y >= this.axeThreshold);
            }
        }
    }

    updateDirection(src, dir, val) {
        if (!this.directions[src]) {
            this.directions[src] = { "up": false, "down": false, "left": false, "right": false };
        }

        if (this.directions[src][dir] !== val) {
            this.directions[src][dir] = val;

            let direction_active = false;

            if (val) {
                direction_active = true;
            } else {
                for (const s in this.directions) {
                    if (s === "_all") continue;

                    let source_directions = this.directions[s];
                    if (source_directions[dir]) {
                        direction_active = true;
                        break;
                    }
                }
            }

            if (this.directions["_all"][dir] !== direction_active) {
                this.emit(dir, {
                    src,
                    pressed: direction_active
                });

                this.directions["_all"][dir] = direction_active
            }
        }
    }
}

export default GamePad;