systems/input.system.js

import {EVENT_CODES, EVENT_TYPES, EventGamepadAnalog, EventGamepadDigital, EventGravityAnalog, EventGravityDigital, EventGyroscopeAnalog, EventGyroscopeDigital, EventPointerAnalog, EventPointerDigital, Stage, System} from '../index.js';

/**
 * Creates input systems.
 *
 * @example
 *
 * const system = new SystemInput({$container});
 * system.initiate();
 * system.tick();
 */
class SystemInput extends System {

    /**
     * @typedef {object} TypeStateInput Statuses of an accepted input.
     * @property {number} $analog The analog value of the input.
     * @property {boolean} $initiate The initiated status of the input.
     * @property {boolean} $persist The persisted status of the input.
     * @property {boolean} $terminate The terminated status of the input.
     * @private
     *
     * @memberof SystemInput
     */

    /**
     * Stores the container.
     * @type {HTMLElement}
     * @private
     */
    $container;

    /**
     * Stores the input events.
     * @type {Array<Event>}
     * @private
     */
    $events;

    /**
     * Stores the statuses of the accepted inputs.
     * @type {Map<string, TypeStateInput>}
     * @private
     */
    $inputs;

    /**
     * Creates a new input system.
     * @param {object} $parameters The given parameters.
     * @param {HTMLElement} $parameters.$container The container on which to attach input events.
     */
    constructor({$container}) {

        super();

        this.$container = $container;
    }

    /**
     * Handles an input being initiated.
     * @param {string} $input The event code of the input.
     * @private
     */
    $handleInputDown($input) {

        const initiate = this.$inputs.has($input) === false;

        /**
         * @type {TypeStateInput}
         */
        const state = {

            $analog: 0,
            $initiate: initiate,
            $persist: true,
            $terminate: false
        };

        this.$inputs.set($input, state);
    }

    /**
     * Handles an input being terminated.
     * @param {string} $input The event code of the input.
     * @private
     */
    $handleInputUp($input) {

        /**
         * @type {TypeStateInput}
         */
        const state = {

            $analog: 0,
            $initiate: false,
            $persist: false,
            $terminate: true
        };

        this.$inputs.set($input, state);
    }

    /**
     * Sets the analog value of the given input.
     * @param {string} $input The event code of the input.
     * @param {number} $value The analog value of the input to set.
     * @private
     */
    $setInputAnalog($input, $value) {

        this.$inputs.get($input).$analog = $value;
    }

    /**
     * Stacks the input events for the next tick.
     * @param {Event} $event The input event to stack.
     * @private
     */
    $stack($event) {

        $event.preventDefault();

        this.$events.push($event);
    }

    /**
     * Gets the persisted status of the given input.
     * @param {string} $input The event code of the input.
     * @returns {boolean}
     * @public
     */
    getInput($input) {

        if (this.$inputs.has($input) === false) {

            return false;
        }

        return this.$inputs.get($input).$persist;
    }

    /**
     * Gets the analog value of the given input.
     * @param {string} $input The event code of the input.
     * @returns {number}
     * @public
     */
    getInputAnalog($input) {

        if (this.$inputs.has($input) === false) {

            return 0;
        }

        return this.$inputs.get($input).$analog;
    }

    /**
     * Gets the initiated status of the given input.
     * @param {string} $input The event code of the input.
     * @returns {boolean}
     * @public
     */
    getInputDown($input) {

        if (this.$inputs.has($input) === false) {

            return false;
        }

        return this.$inputs.get($input).$initiate;
    }

    /**
     * Gets the terminated status of the given input.
     * @param {string} $input The event code of the input.
     * @returns {boolean}
     * @public
     */
    getInputUp($input) {

        if (this.$inputs.has($input) === false) {

            return false;
        }

        return this.$inputs.get($input).$terminate;
    }

    /**
     * Called when the system is being initiated.
     * @public
     */
    onInitiate() {

        this.$events = [];
        this.$inputs = new Map();

        window.addEventListener(EVENT_TYPES.NATIVE.BLUR, this.$stack.bind(this));
        window.addEventListener(EVENT_TYPES.NATIVE.CONTEXT_MENU, this.$stack.bind(this));

        window.addEventListener(EVENT_TYPES.GAMEPAD.GAMEPAD_ANALOG, this.$stack.bind(this));
        window.addEventListener(EVENT_TYPES.GAMEPAD.GAMEPAD_CONNECT, this.$stack.bind(this));
        window.addEventListener(EVENT_TYPES.GAMEPAD.GAMEPAD_DOWN, this.$stack.bind(this));
        window.addEventListener(EVENT_TYPES.GAMEPAD.GAMEPAD_UP, this.$stack.bind(this));

        window.addEventListener(EVENT_TYPES.GRAVITY.GRAVITY_ANALOG, this.$stack.bind(this));
        window.addEventListener(EVENT_TYPES.GRAVITY.GRAVITY_DOWN, this.$stack.bind(this));
        window.addEventListener(EVENT_TYPES.GRAVITY.GRAVITY_UP, this.$stack.bind(this));

        window.addEventListener(EVENT_TYPES.GYROSCOPE.GYROSCOPE_ANALOG, this.$stack.bind(this));
        window.addEventListener(EVENT_TYPES.GYROSCOPE.GYROSCOPE_DOWN, this.$stack.bind(this));
        window.addEventListener(EVENT_TYPES.GYROSCOPE.GYROSCOPE_UP, this.$stack.bind(this));

        window.addEventListener(EVENT_TYPES.POINTER.POINTER_ANALOG, this.$stack.bind(this));
        window.addEventListener(EVENT_TYPES.POINTER.POINTER_DOWN, this.$stack.bind(this));
        window.addEventListener(EVENT_TYPES.POINTER.POINTER_UP, this.$stack.bind(this));

        this.$container.addEventListener(EVENT_TYPES.KEYBOARD.KEY_DOWN, this.$stack.bind(this));
        this.$container.addEventListener(EVENT_TYPES.KEYBOARD.KEY_UP, this.$stack.bind(this));
    }

    /**
     * Called when the system is being terminated.
     * @returns {(undefined | Promise<void>)}
     * @public
     */
    onTerminate() {

        window.removeEventListener(EVENT_TYPES.NATIVE.BLUR, this.$stack.bind(this));
        window.removeEventListener(EVENT_TYPES.NATIVE.CONTEXT_MENU, this.$stack.bind(this));

        window.removeEventListener(EVENT_TYPES.GAMEPAD.GAMEPAD_ANALOG, this.$stack.bind(this));
        window.removeEventListener(EVENT_TYPES.GAMEPAD.GAMEPAD_CONNECT, this.$stack.bind(this));
        window.removeEventListener(EVENT_TYPES.GAMEPAD.GAMEPAD_DOWN, this.$stack.bind(this));
        window.removeEventListener(EVENT_TYPES.GAMEPAD.GAMEPAD_UP, this.$stack.bind(this));

        window.removeEventListener(EVENT_TYPES.GRAVITY.GRAVITY_ANALOG, this.$stack.bind(this));
        window.removeEventListener(EVENT_TYPES.GRAVITY.GRAVITY_DOWN, this.$stack.bind(this));
        window.removeEventListener(EVENT_TYPES.GRAVITY.GRAVITY_UP, this.$stack.bind(this));

        window.removeEventListener(EVENT_TYPES.GYROSCOPE.GYROSCOPE_ANALOG, this.$stack.bind(this));
        window.removeEventListener(EVENT_TYPES.GYROSCOPE.GYROSCOPE_DOWN, this.$stack.bind(this));
        window.removeEventListener(EVENT_TYPES.GYROSCOPE.GYROSCOPE_UP, this.$stack.bind(this));

        window.removeEventListener(EVENT_TYPES.POINTER.POINTER_ANALOG, this.$stack.bind(this));
        window.removeEventListener(EVENT_TYPES.POINTER.POINTER_DOWN, this.$stack.bind(this));
        window.removeEventListener(EVENT_TYPES.POINTER.POINTER_UP, this.$stack.bind(this));

        this.$container.removeEventListener(EVENT_TYPES.KEYBOARD.KEY_DOWN, this.$stack.bind(this));
        this.$container.removeEventListener(EVENT_TYPES.KEYBOARD.KEY_UP, this.$stack.bind(this));

        return undefined;
    }

    /**
     * Called when the system is being updated by one tick update.
     * @param {object} $parameters The given parameters.
     * @param {Stage} $parameters.$stage The stage on which to execute the system.
     * @param {number} $parameters.$timetick The tick duration (in ms).
     * @public
     */
    onTick({$stage, $timetick}) {

        void $stage;
        void $timetick;

        Array.from(this.$inputs.entries()).forEach(([$code, $state]) => {

            if ($state.$terminate === true) {

                this.$inputs.delete($code);

                return;
            }

            if ($code === EVENT_CODES.GAMEPAD_STANDARD.CONNECTED
            || $code === EVENT_CODES.GAMEPAD_STANDARD.DISCONNECTED) {

                this.$handleInputUp($code);

                return;
            }

            if ($state.$initiate === true) {

                $state.$initiate = false;

                return;
            }
        });

        while (this.$events.length > 0) {

            const $event = this.$events.shift();

            if ($event.type === EVENT_TYPES.NATIVE.BLUR) {

                Array.from(this.$inputs.keys()).forEach(($code) => {

                    this.$handleInputUp($code);
                });
            }

            else if ($event instanceof EventGamepadDigital
            && $event.type === EVENT_TYPES.GAMEPAD.GAMEPAD_CONNECT) {

                this.$handleInputDown($event.code);
            }

            else if ($event instanceof EventGamepadDigital
            && $event.type === EVENT_TYPES.GAMEPAD.GAMEPAD_DOWN) {

                this.$handleInputDown($event.code);
            }

            else if ($event instanceof EventGamepadDigital
            && $event.type === EVENT_TYPES.GAMEPAD.GAMEPAD_UP) {

                this.$handleInputUp($event.code);
            }

            else if ($event instanceof EventGamepadAnalog
            && $event.type === EVENT_TYPES.GAMEPAD.GAMEPAD_ANALOG) {

                this.$setInputAnalog($event.code, $event.value);
            }

            else if ($event instanceof EventGravityDigital
            && $event.type === EVENT_TYPES.GRAVITY.GRAVITY_DOWN) {

                this.$handleInputDown($event.code);
            }

            else if ($event instanceof EventGravityDigital
            && $event.type === EVENT_TYPES.GRAVITY.GRAVITY_UP) {

                this.$handleInputUp($event.code);
            }

            else if ($event instanceof EventGravityAnalog
            && $event.type === EVENT_TYPES.GRAVITY.GRAVITY_ANALOG) {

                this.$setInputAnalog($event.code, $event.value);
            }

            else if ($event instanceof EventGyroscopeDigital
            && $event.type === EVENT_TYPES.GYROSCOPE.GYROSCOPE_DOWN) {

                this.$handleInputDown($event.code);
            }

            else if ($event instanceof EventGyroscopeDigital
            && $event.type === EVENT_TYPES.GYROSCOPE.GYROSCOPE_UP) {

                this.$handleInputUp($event.code);
            }

            else if ($event instanceof EventGyroscopeAnalog
            && $event.type === EVENT_TYPES.GYROSCOPE.GYROSCOPE_ANALOG) {

                this.$setInputAnalog($event.code, $event.value);
            }

            else if ($event instanceof KeyboardEvent
            && $event.type === EVENT_TYPES.KEYBOARD.KEY_DOWN) {

                this.$handleInputDown($event.code);
            }

            else if ($event instanceof KeyboardEvent
            && $event.type === EVENT_TYPES.KEYBOARD.KEY_UP) {

                this.$handleInputUp($event.code);
            }

            else if ($event instanceof EventPointerDigital
            && $event.type === EVENT_TYPES.POINTER.POINTER_DOWN) {

                this.$handleInputDown($event.code);
            }

            else if ($event instanceof EventPointerDigital
            && $event.type === EVENT_TYPES.POINTER.POINTER_UP) {

                this.$handleInputUp($event.code);
            }

            else if ($event instanceof EventPointerAnalog
            && $event.type === EVENT_TYPES.POINTER.POINTER_ANALOG) {

                this.$setInputAnalog($event.code, $event.value);
            }
        }
    }
}

export {

    SystemInput
};

export default SystemInput;