Source: input/gamepad/Gamepad.js

//------------------------------------------------------------------------------
// Constructor scope
//------------------------------------------------------------------------------

/** 
 * Creates a new Gamepad object.
 * 
 * @constructor
 * @package
 *
 * @class
 * @classdesc
 * 
 * The Gamepad class represents a physical gamepad device and its current 
 * state. Note that the class can not be instantiated by itself, Gamepad 
 * objects are retrieved via an instance of the Gamepads class, which acts as 
 * a handler for all connected devices. Objects of the Gamepad class are used 
 * to read the state of a specific gamepad device, for example for player one, 
 * etc..
 */
rune.input.Gamepad = function() {

    //--------------------------------------------------------------------------
    // Public properties
    //--------------------------------------------------------------------------

    /**
     * The "dead zone" threshold of a joystick. All input data lower than this 
     * value is ignored.
     *
     * @type {number}
     * @default 0.2
     */
    this.threshold = 0.2;

    /**
     * Threshold for when input data from joysticks is to be counted as a 
     * boolean true value.
     *
     * @type {number}
     * @default 0.5
     */
    this.tolerance = 0.5;

    //--------------------------------------------------------------------------
    // Private properties
    //--------------------------------------------------------------------------

    /**
     * Whether the Gamepad object is active or not.
     *
     * @type {boolean}
     * @private
     */
    this.m_active = true;

    /**
     * Input from the left analog stick.
     *
     * @type {rune.geom.Point}
     * @private
     */
    this.m_axesOne = new rune.geom.Point(0, 0);

    /**
     * Input from the right analog stick.
     *
     * @type {rune.geom.Point}
     * @private
     */
    this.m_axesTwo = new rune.geom.Point(0, 0);

    /**
     * Object that represent the current state of the physical device.
     *
     * @type {Object}
     * @private
     */
    this.m_sc = null;

    /**
     * Object that represent the previous state of the physical device, ie that 
     * from the previous update.
     *
     * @type {Object}
     * @private
     */
    this.m_so = null;
};

//------------------------------------------------------------------------------
// Public getter and setter methods
//------------------------------------------------------------------------------

/**
 * Whether the Gamepad object is active (true) or not (false). If false, the 
 * state of the Gamepad object will not be updated.
 *
 * @member {boolean} active
 * @memberof rune.input.Gamepad
 * @instance
 */
Object.defineProperty(rune.input.Gamepad.prototype, "active", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        return this.m_active;
    },
    
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    set : function(value) {
        this.m_active = value;
        this.reset();
    }
});

/**
 * An ID string containing information about the physical device that the 
 * Gamepad object represents. Note that this value is set by the manufacturer 
 * of the current physical device.
 *
 * @member {number} id
 * @memberof rune.input.Gamepad
 * @instance
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "id", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        return (this.m_sc) ? this.m_sc.id : "";
    }
});

/**
 * An integer that is auto-incremented to be unique for each device 
 * currently connected to the system.
 *
 * @member {number} index
 * @memberof rune.input.Gamepad
 * @instance
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "index", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        return (this.m_sc) ? this.m_sc.index : -1;
    }
});

/**
 * Indicates whether the Gamepad object is connected (true) or not (false). 
 * Generally, this value should never be false as long as the Gamepad object 
 * exists.
 *
 * @member {booelan} connected
 * @memberof rune.input.Gamepad
 * @instance
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "connected", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        return (this.m_sc) ? Boolean(this.m_sc.connected) : false;
    }
});

/**
 * A list of the Gamepad object's buttons and their current state. Each button 
 * is indexed by their button ID.
 *
 * @member {Array} buttons
 * @memberof rune.input.Gamepad
 * @instance
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "buttons", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        return (this.m_sc) ? this.m_sc.buttons : [];
    }
});

/**
 * The direction of the left joystick of the input device. The direction is 
 * represented by a Point object that describes the x- and y-axis of the 
 * joystick.
 *
 * @member {rune.geom.Point} stickLeft
 * @memberof rune.input.Gamepad
 * @instance 
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickLeft", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        return this.m_axesOne;
    }
});

/**
 * If the left analog stick of the Gamepad object is facing up.
 *
 * @member {booelan} stickLeftUp
 * @memberof rune.input.Gamepad
 * @instance 
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickLeftUp", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        return (this.m_axesOne.y < -this.tolerance) ? true : false;
    }
});

/**
 * If the left analog stick of the Gamepad object was just facing up.
 *
 * @member {booelan} stickLeftJustUp
 * @memberof rune.input.Gamepad
 * @instance 
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickLeftJustUp", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        if (this.m_so.axes) {
            return ((this.m_axesOne.y < -this.tolerance) && (this.m_so.axes[1] > -this.tolerance)) ? true : false;
        }
        
        return false;
    }
});

/**
 * If the left analog stick of the Gamepad object is facing down.
 *
 * @member {booelan} stickLeftDown
 * @memberof rune.input.Gamepad
 * @instance 
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickLeftDown", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        return (this.m_axesOne.y > this.tolerance) ? true : false;
    }
});

/**
 * If the left analog stick of the Gamepad object was just facing down.
 *
 * @member {booelan} stickLeftJustDown
 * @memberof rune.input.Gamepad
 * @instance 
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickLeftJustDown", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        if (this.m_so.axes) {
            return ((this.m_axesOne.y > this.tolerance) && (this.m_so.axes[1] < this.tolerance)) ? true : false;
        }
        
        return false;
    }
});

/**
 * If the left analog stick of the Gamepad object is facing left.
 *
 * @member {booelan} stickLeftLeft
 * @memberof rune.input.Gamepad
 * @instance 
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickLeftLeft", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        return (this.m_axesOne.x < -this.tolerance) ? true : false;
    }
});

/**
 * If the left analog stick of the Gamepad object was just facing left.
 *
 * @member {booelan} stickLeftJustLeft
 * @memberof rune.input.Gamepad
 * @instance 
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickLeftJustLeft", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        if (this.m_so.axes) {
            return ((this.m_axesOne.x < -this.tolerance) && (this.m_so.axes[0] > -this.tolerance)) ? true : false;
        }
        
        return false;
    }
});

/**
 * If the left analog stick of the Gamepad object is facing right.
 *
 * @member {booelan} stickLeftRight
 * @memberof rune.input.Gamepad
 * @instance 
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickLeftRight", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        return (this.m_axesOne.x > this.tolerance) ? true : false;
    }
});

/**
 * If the left analog stick of the Gamepad object was just facing right.
 *
 * @member {booelan} stickLeftJustRight
 * @memberof rune.input.Gamepad
 * @instance 
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickLeftJustRight", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        if (this.m_so.axes) {
            return ((this.m_axesOne.x > this.tolerance) && (this.m_so.axes[0] < this.tolerance)) ? true : false;
        }
        
        return false;
    }
});

/**
 * The direction of the right joystick of the input device. The direction is 
 * represented by a Point object that describes the x- and y-axis of the 
 * joystick.
 *
 * @member {rune.geom.Point} stickRight
 * @memberof rune.input.Gamepad
 * @instance 
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickRight", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        return this.m_axesTwo;
    }
});

/**
 * If the right analog stick of the Gamepad object is facing up.
 *
 * @member {booelan} stickRightUp
 * @memberof rune.input.Gamepad
 * @instance 
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickRightUp", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        return (this.m_axesTwo.y < -this.tolerance) ? true : false;
    }
});

/**
 * If the right analog stick of the Gamepad object was just facing up.
 *
 * @member {booelan} stickRightJustUp
 * @memberof rune.input.Gamepad
 * @instance 
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickRightJustUp", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        if (this.m_so.axes) {
            return ((this.m_axesTwo.y < -this.tolerance) && (this.m_so.axes[3] > -this.tolerance)) ? true : false;
        }
        
        return false;
    }
});

/**
 * If the right analog stick of the Gamepad object is facing down.
 *
 * @member {booelan} stickRightDown
 * @memberof rune.input.Gamepad
 * @instance 
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickRightDown", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        return (this.m_axesTwo.y > this.tolerance) ? true : false;
    }
});

/**
 * If the right analog stick of the Gamepad object was just facing down.
 *
 * @member {booelan} stickRightJustDown
 * @memberof rune.input.Gamepad
 * @instance 
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickRightJustDown", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        if (this.m_so.axes) {
            return ((this.m_axesTwo.y > this.tolerance) && (this.m_so.axes[3] < this.tolerance)) ? true : false;
        }
        
        return false;
    }
});

/**
 * If the right analog stick of the Gamepad object is facing left.
 *
 * @member {booelan} stickRightLeft
 * @memberof rune.input.Gamepad
 * @instance 
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickRightLeft", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        return (this.m_axesTwo.x < -this.tolerance) ? true : false;
    }
});

/**
 * If the right analog stick of the Gamepad object was just facing left.
 *
 * @member {booelan} stickRightJustLeft
 * @memberof rune.input.Gamepad
 * @instance 
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickRightJustLeft", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        if (this.m_so.axes) {
            return ((this.m_axesTwo.x < -this.tolerance) && (this.m_so.axes[2] > -this.tolerance)) ? true : false;
        }
        
        return false;
    }
});

/**
 * If the right analog stick of the Gamepad object is facing right.
 *
 * @member {booelan} stickRightRight
 * @memberof rune.input.Gamepad
 * @instance
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickRightRight", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        return (this.m_axesTwo.x > this.tolerance) ? true : false;
    }
});

/**
 * If the right analog stick of the Gamepad object was just facing right.
 *
 * @member {booelan} stickRightJustRight
 * @memberof rune.input.Gamepad
 * @instance
 * @readonly
 */
Object.defineProperty(rune.input.Gamepad.prototype, "stickRightJustRight", {
    /**
     * @this rune.input.Gamepad
     * @ignore
     */
    get : function() {
        if (this.m_so.axes) {
            return ((this.m_axesTwo.x > this.tolerance) && (this.m_so.axes[2] < this.tolerance)) ? true : false;
        }
        
        return false;
    }
});

//------------------------------------------------------------------------------
// Public prototype methods (API)
//------------------------------------------------------------------------------

/**
 * Whether a specific button on the input device was just pressed. The button 
 * is identified based on its button ID.
 *
 * @param {number} button Button ID.
 *
 * @returns {boolean}
 */
rune.input.Gamepad.prototype.justPressed = function(button) {
    if (this.m_isButtonInvalid(button)) return false;
    return (this.m_sc.buttons[button].pressed === true && 
            this.m_so.buttons[button].pressed === false) ? true : false;
};

/**
 * Whether a specific button on the input device was just released. The button 
 * is identified based on its button ID.
 *
 * @param {number} button Button ID.
 *
 * @returns {boolean}
 */
rune.input.Gamepad.prototype.justReleased = function(button) {
    if (this.m_isButtonInvalid(button)) return false;
    return (this.m_sc.buttons[button].pressed === false && 
            this.m_so.buttons[button].pressed === true) ? true : false;
};

/**
 * Whether a specific button on the input device is pressed. The button is 
 * identified based on its button ID.
 *
 * @param {number} button Button ID.
 *
 * @returns {boolean}
 */
rune.input.Gamepad.prototype.pressed = function(button) {
    if (this.m_isButtonInvalid(button)) return false;
    return this.m_sc.buttons[button].pressed === true ? true : false;
};

/**
 * Resets the state of the Gamepad object.
 * 
 * @returns {undefined}
 */
rune.input.Gamepad.prototype.reset = function() {
    this.m_disposeState();
};

/**
 * Triggers the gamepad vibration using the specified effect type.
 *
 * @param {number} [duration=250] The duration of the vibration in milliseconds.
 * @param {number} [delay=0] The delay before the vibration starts, in milliseconds.
 * @param {number} [weak=1.0] The intensity of the weak (low-frequency) vibration, ranging from 0.0 to 1.0.
 * @param {number} [strong=1.0] The intensity of the strong (high-frequency) vibration, ranging from 0.0 to 1.0.
 * @param {string} [type="dual-rumble"] The type of vibration effect to use (e.g., "dual-rumble").
 * 
 * @returns {undefined}
 */
rune.input.Gamepad.prototype.vibrate = function(duration, delay, weak, strong, type) {
    if (this.m_sc != null && this.m_sc.vibrationActuator) {
        duration = rune.util.Math.isNum(duration) ? duration : 250;
        if (duration > 0) {
            this.m_sc.vibrationActuator.playEffect(
                type || "dual-rumble", {
                    startDelay: delay || 0,
                    duration: duration || 150,
                    weakMagnitude: weak || 1.0,
                    strongMagnitude: strong || 1.0,
                }
            );
        } else {
            this.m_sc.vibrationActuator.reset();
        }
    }
};

//------------------------------------------------------------------------------
// Internal prototype methods
//------------------------------------------------------------------------------

/**
 * Destroys the Gamepad object and frees up memory allocated by it.
 * 
 * @returns {undefined}
 * @package
 * @ignore
 */
rune.input.Gamepad.prototype.dispose = function() {
    this.vibrate(0);
    this.m_disposeAxes();
    this.m_disposeState();
};

/**
 * Updates the current state of the Gamepad object based on data from the 
 * connected physical device that the object represents.
 *
 * @param {Object} state New gamepad state.
 *
 * @returns {undefined}
 * @package
 * @ignore
 */
rune.input.Gamepad.prototype.update = function(state) {
    this.m_updateState(state);
    this.m_updateAxes();
};

//------------------------------------------------------------------------------
// Private prototype methods
//------------------------------------------------------------------------------

/**
 * Updates the current state of the gamepad.
 *
 * @param {Object} state New gamepad state.
 *
 * @returns {undefined}
 * @private
 */
rune.input.Gamepad.prototype.m_updateState = function(state) {
    this.m_so = this.m_sc;
    this.m_sc = this.m_clone(state);
};

/**
 * Updates joystick values.
 *
 * @returns {undefined}
 * @private
 */
rune.input.Gamepad.prototype.m_updateAxes = function() {
    if (this.m_sc != null && this.m_sc.axes != null) {
        this.m_axesOne.x = this.m_sc.axes[0] || 0;
        this.m_axesOne.y = this.m_sc.axes[1] || 0;
        this.m_axesTwo.x = this.m_sc.axes[2] || 0;
        this.m_axesTwo.y = this.m_sc.axes[3] || 0;

        this.m_axesOne.x = (Math.abs(this.m_axesOne.x) < this.threshold) ? 0 : this.m_axesOne.x;
        this.m_axesOne.y = (Math.abs(this.m_axesOne.y) < this.threshold) ? 0 : this.m_axesOne.y;
        this.m_axesTwo.x = (Math.abs(this.m_axesTwo.x) < this.threshold) ? 0 : this.m_axesTwo.x;
        this.m_axesTwo.y = (Math.abs(this.m_axesTwo.y) < this.threshold) ? 0 : this.m_axesTwo.y;
    }
};

/**
 * Destroy objects that represent the Gamepad Object's joysticks.
 *
 * @returns {undefined}
 * @private
 */
rune.input.Gamepad.prototype.m_disposeAxes = function() {
    this.m_axesOne = null;
    this.m_axesTwo = null;
};

/**
 * Destroys the states of the Gamepad object.
 *
 * @returns {undefined}
 * @private
 */
rune.input.Gamepad.prototype.m_disposeState = function() {
    this.m_so = null;
    this.m_cs = null;
};

/**
 * Tests if a button ID is valid, ie if the ID can be used to read input data.
 *
 * @param {number} button ID of button to test.
 *
 * @returns {boolean}
 * @private
 */
rune.input.Gamepad.prototype.m_isButtonInvalid = function(button) {
    if (this.m_sc == null || this.m_sc.buttons == null || this.m_sc.buttons[button] == null) return true;
    if (this.m_so == null || this.m_so.buttons == null || this.m_so.buttons[button] == null) return true;

    return false;
}

/**
 * Creates a (fast) shallow clone of an object.
 *
 * @param {Object} obj Object to clone.
 *
 * @returns {Object}
 * @private
 */
rune.input.Gamepad.prototype.m_clone = function(obj) {
    var clone = {};
    for (var i in obj) {
        if (obj[i] && typeof obj[i] === "object" && i !== "vibrationActuator") clone[i] = this.m_clone(obj[i]);
        else clone[i] = obj[i];
    }

    return clone;
}