controls/KeyboardControls.js

// based on http://keycode.info/

// keyboard handling
const keyCodeTable = {
    3: 'break',
    8: 'backspace', // backspace / delete
    9: 'tab',
    12: 'clear',
    13: 'enter',
    16: 'shift',
    17: 'ctrl',
    18: 'alt',
    19: 'pause/break',
    20: 'caps lock',
    27: 'escape',
    28: 'conversion',
    29: 'non-conversion',
    32: 'space',
    33: 'page up',
    34: 'page down',
    35: 'end',
    36: 'home',
    37: 'left',
    38: 'up',
    39: 'right',
    40: 'down',
    41: 'select',
    42: 'print',
    43: 'execute',
    44: 'Print Screen',
    45: 'insert',
    46: 'delete',
    48: '0',
    49: '1',
    50: '2',
    51: '3',
    52: '4',
    53: '5',
    54: '6',
    55: '7',
    56: '8',
    57: '9',
    58: ':',
    59: 'semicolon (firefox), equals',
    60: '<',
    61: 'equals (firefox)',
    63: 'ß',
    64: '@',
    65: 'a',
    66: 'b',
    67: 'c',
    68: 'd',
    69: 'e',
    70: 'f',
    71: 'g',
    72: 'h',
    73: 'i',
    74: 'j',
    75: 'k',
    76: 'l',
    77: 'm',
    78: 'n',
    79: 'o',
    80: 'p',
    81: 'q',
    82: 'r',
    83: 's',
    84: 't',
    85: 'u',
    86: 'v',
    87: 'w',
    88: 'x',
    89: 'y',
    90: 'z',
    91: 'Windows Key / Left ⌘ / Chromebook Search key',
    92: 'right window key',
    93: 'Windows Menu / Right ⌘',
    96: 'numpad 0',
    97: 'numpad 1',
    98: 'numpad 2',
    99: 'numpad 3',
    100: 'numpad 4',
    101: 'numpad 5',
    102: 'numpad 6',
    103: 'numpad 7',
    104: 'numpad 8',
    105: 'numpad 9',
    106: 'multiply',
    107: 'add',
    108: 'numpad period (firefox)',
    109: 'subtract',
    110: 'decimal point',
    111: 'divide',
    112: 'f1',
    113: 'f2',
    114: 'f3',
    115: 'f4',
    116: 'f5',
    117: 'f6',
    118: 'f7',
    119: 'f8',
    120: 'f9',
    121: 'f10',
    122: 'f11',
    123: 'f12',
    124: 'f13',
    125: 'f14',
    126: 'f15',
    127: 'f16',
    128: 'f17',
    129: 'f18',
    130: 'f19',
    131: 'f20',
    132: 'f21',
    133: 'f22',
    134: 'f23',
    135: 'f24',
    144: 'num lock',
    145: 'scroll lock',
    160: '^',
    161: '!',
    163: '#',
    164: '$',
    165: 'ù',
    166: 'page backward',
    167: 'page forward',
    169: 'closing paren (AZERTY)',
    170: '*',
    171: '~ + * key',
    173: 'minus (firefox), mute/unmute',
    174: 'decrease volume level',
    175: 'increase volume level',
    176: 'next',
    177: 'previous',
    178: 'stop',
    179: 'play/pause',
    180: 'e-mail',
    181: 'mute/unmute (firefox)',
    182: 'decrease volume level (firefox)',
    183: 'increase volume level (firefox)',
    186: 'semi-colon / ñ',
    187: 'equal sign',
    188: 'comma',
    189: 'dash',
    190: 'period',
    191: 'forward slash / ç',
    192: 'grave accent / ñ / æ',
    193: '?, / or °',
    194: 'numpad period (chrome)',
    219: 'open bracket',
    220: 'back slash',
    221: 'close bracket / å',
    222: 'single quote / ø',
    223: '`',
    224: 'left or right ⌘ key (firefox)',
    225: 'altgr',
    226: '< /git >',
    230: 'GNOME Compose Key',
    231: 'ç',
    233: 'XF86Forward',
    234: 'XF86Back',
    240: 'alphanumeric',
    242: 'hiragana/katakana',
    243: 'half-width/full-width',
    244: 'kanji',
    255: 'toggle touchpad'
};

/**
 * This class allows easy usage of device keyboard controls.  Use the method {@link KeyboardControls#bindKey} to
 * generate events whenever a key is pressed.
 *
 * @example
 *    // in the ClientEngine constructor
 *    this.controls = new KeyboardControls(this);
 *    this.controls.bindKey('left', 'left', { repeat: true } );
 *    this.controls.bindKey('right', 'right', { repeat: true } );
 *    this.controls.bindKey('space', 'space');
 *
 */
class KeyboardControls {

    constructor(clientEngine) {

        this.clientEngine = clientEngine;
        this.gameEngine = clientEngine.gameEngine;

        this.setupListeners();

        // keep a reference for key press state
        this.keyState = {};

        // a list of bound keys and their corresponding actions
        this.boundKeys = {};

        this.gameEngine.on('client__preStep', () => {
            for (let keyName of Object.keys(this.boundKeys)) {
                if (this.keyState[keyName] && this.keyState[keyName].isDown) {

                    // handle repeat press
                    if (this.boundKeys[keyName].options.repeat || this.keyState[keyName].count == 0) {

                        // callback to get live parameters if function
                        let parameters = this.boundKeys[keyName].parameters;
                        if (typeof parameters === "function") {
                            parameters = parameters();
                        }

                        // todo movement is probably redundant
                        let inputOptions = Object.assign({
                            movement: true
                        }, parameters || {});
                        this.clientEngine.sendInput(this.boundKeys[keyName].actionName, inputOptions);
                        this.keyState[keyName].count++;
                    }
                }
            }
        });
    }

    setupListeners() {
        document.addEventListener('keydown', (e) => { this.onKeyChange(e, true);});
        document.addEventListener('keyup', (e) => { this.onKeyChange(e, false);});
    }

    /**
     * Bind a keyboard key to a Lance client event.  Each time the key is pressed,
     * an event will be transmitted by the client engine, using {@link ClientEngine#sendInput},
     * and the specified event name.
     *
     * Common key names: up, down, left, right, enter, shift, ctrl, alt,
     * escape, space, page up, page down, end, home, 0..9, a..z, A..Z.
     * For a full list, please check the source link above.
     *
     * @param {String} keys - keyboard key (or array of keys) which will cause the event.
     * @param {String} actionName - the event name
     * @param {Object} options - options object
     * @param {Boolean} options.repeat - if set to true, an event continues to be sent on each game step, while the key is pressed
     * @param {Object/Function} parameters - parameters (or function to get parameters) to be sent to
     *                                       the server with sendInput as the inputOptions
     */
    bindKey(keys, actionName, options, parameters) {
        if (!Array.isArray(keys)) keys = [keys];

        let keyOptions = Object.assign({
            repeat: false
        }, options);

        keys.forEach(keyName => {
            this.boundKeys[keyName] = { actionName, options: keyOptions, parameters: parameters };
        });
    }

    // todo implement unbindKey

    onKeyChange(e, isDown) {
        e = e || window.event;

        let keyName = keyCodeTable[e.keyCode];
        if (keyName && this.boundKeys[keyName]) {
            if (this.keyState[keyName] == null) {
                this.keyState[keyName] = {
                    count: 0
                };
            }
            this.keyState[keyName].isDown = isDown;

            // key up, reset press count
            if (!isDown) this.keyState[keyName].count = 0;

            // keep reference to the last key pressed to avoid duplicates
            this.lastKeyPressed = isDown ? e.keyCode : null;
            // this.renderer.onKeyChange({ keyName, isDown });
            e.preventDefault();
        }
    }
}

export default KeyboardControls;