diff --git a/src/plugins/KeyboardMapping.js b/src/plugins/KeyboardMapping.js index f181231c..d588ebd0 100644 --- a/src/plugins/KeyboardMapping.js +++ b/src/plugins/KeyboardMapping.js @@ -74,6 +74,19 @@ const NEW_SOUL_KNIGHT = { description: 'change the weapon', }, ], + dtap: [ + { + keys: { + v: { + x: 95, + y: 20, + description: 'test', + }, + }, + name: 'test', + description: 'test', + }, + ], }; // eslint-disable-next-line no-unused-vars @@ -85,27 +98,61 @@ const MINECRAFT = { initialX: 15, initialY: 64, distanceX: 0, - distanceY: -15, + distanceY: -10, description: 'move up', }, s: { initialX: 15, initialY: 64, distanceX: 0, - distanceY: 15, + distanceY: 10, description: 'move down', }, q: { initialX: 15, initialY: 64, - distanceX: -15, + distanceX: -10, distanceY: 0, description: 'move left', }, d: { initialX: 15, initialY: 64, - distanceX: 15, + distanceX: 10, + distanceY: 0, + description: 'move right', + }, + }, + name: 'character movement', + description: 'dpad used for move the character', + }, + { + keys: { + o: { + initialX: 80, + initialY: 80, + distanceX: 0, + distanceY: -19, + description: 'move up', + }, + l: { + initialX: 80, + initialY: 80, + distanceX: 0, + distanceY: 19, + description: 'move down', + }, + k: { + initialX: 80, + initialY: 80, + distanceX: -19, + distanceY: 0, + description: 'move left', + }, + m: { + initialX: 80, + initialY: 80, + distanceX: 19, distanceY: 0, description: 'move right', }, @@ -186,6 +233,89 @@ const SUBWAY_SURFERS = { ], }; +const LEGENDS = { + dPad: [ + { + keys: { + z: { + initialX: 10, + initialY: 60, + distanceX: 0, + distanceY: -10, + description: 'move up', + }, + s: { + initialX: 10, + initialY: 60, + distanceX: 0, + distanceY: 10, + description: 'move down', + }, + q: { + initialX: 10, + initialY: 60, + distanceX: -10, + distanceY: 0, + description: 'move left', + }, + d: { + initialX: 10, + initialY: 60, + distanceX: 10, + distanceY: 0, + description: 'move right', + }, + }, + name: 'character movement', + description: 'dpad used for move the character', + }, + { + keys: { + o: { + initialX: 80, + initialY: 80, + distanceX: 0, + distanceY: -19, + description: 'move up', + }, + l: { + initialX: 80, + initialY: 80, + distanceX: 0, + distanceY: 19, + description: 'move down', + }, + k: { + initialX: 80, + initialY: 80, + distanceX: -19, + distanceY: 0, + description: 'move left', + }, + m: { + initialX: 80, + initialY: 80, + distanceX: 19, + distanceY: 0, + description: 'move right', + }, + }, + name: 'character movement', + description: 'dpad used for move the character', + }, + ], + tap: [ + { + keys: { + e: { + x: 96, + y: 41, + }, + }, + }, + ], +}; + /** * Instance keyboard plugin.KeyboardMapping * Translate and forward keyboard events to instance. @@ -207,19 +337,24 @@ module.exports = class KeyboardMapping { this.keyboardCallbacks = []; - this.dPadPushed = []; + this.sequences = {}; + this.requestToGenerate = []; + this.currentlyPressedKeys = []; + // internal state this.state = new Proxy( { isActive: false, + /* + * when a dialog is open we want disable listeners for keyMapping, but when dialog is closed we want to re-enable them + * this is the isPaused purpose + */ isPaused: false, - currentlyPressedKeys: [], mappedKeysConfig: {}, workingMappedKeysConfig: {}, }, { set: (state, prop, value) => { - const oldValue = state[prop]; state[prop] = value; switch (prop) { case 'isActive': @@ -237,18 +372,6 @@ module.exports = class KeyboardMapping { case 'mappedKeysConfig': this.setupMappedKeysConfig(); break; - case 'currentlyPressedKeys': - // logic to handle key pressed / unpressed depending on the mappedKeysConfig - if (!value.length) { - // unpressed key - const jsonForReleaseTouch = this.generateTouchEventForRelease( - oldValue.filter((key) => !value.includes(key)), - ); - this.instance.sendEvent(jsonForReleaseTouch); - } else { - this.sendMultiTouch(); - } - break; default: break; } @@ -269,7 +392,7 @@ module.exports = class KeyboardMapping { // Display widget this.renderToolbarButton(); - // activate plugin + // TODO son't do this in production activate plugin this.state.isActive = true; // register api function @@ -304,27 +427,21 @@ module.exports = class KeyboardMapping { 10% of the screen width and height.`, }); + // active trace when click on screen + this.instance.apiManager.registerFunction({ + name: 'enable', + category: 'keyMapping', + fn: (isActive = false) => { + this.state.isActive = isActive; + }, + description: 'Activate the keyMapping plugin.', + }); + // load default config to test purpose TODO delete for production + this.state.mappedKeysConfig = MINECRAFT; + this.state.mappedKeysConfig = SUBWAY_SURFERS; this.state.mappedKeysConfig = NEW_SOUL_KNIGHT; - } - - sendMultiTouch() { - const reversedCurrentlyPressedKeys = this.state.currentlyPressedKeys.reverse(); - const {touchPoints, movePoints} = this.generateTouchEventForPush(reversedCurrentlyPressedKeys); - - const json = {type: 'MULTI_TOUCH', nb: 0, mode: 0, points: []}; - if (touchPoints.length) { - json.nb = touchPoints.length; - json.points = touchPoints; - this.instance.sendEvent(json); - } - - if (movePoints.length) { - json.nb = movePoints.length; - json.points = movePoints; - json.mode = 2; - this.instance.sendEvent(json); - } + this.state.mappedKeysConfig = LEGENDS; } renderToolbarButton() { @@ -359,10 +476,12 @@ module.exports = class KeyboardMapping { // reset this.state.workingMappedKeysConfig = {}; - // Create a working version of the mappedKeysConfig, this will change the structure to be more performant and avoid incepth loop + // create key for sequences + Object.entries(this.state.mappedKeysConfig).forEach(([gestureType, gestureConfig]) => { gestureConfig.forEach((gesture) => { const groupId = generateUID(); + this.sequences[groupId] = []; Object.entries(gesture.keys).forEach(([key, value]) => { this.state.workingMappedKeysConfig[key] = { ...value, @@ -374,6 +493,7 @@ module.exports = class KeyboardMapping { }); }); }); + this.sequences.mouse = []; } /** @@ -382,6 +502,8 @@ module.exports = class KeyboardMapping { * or any other command that remove focus (blur) the page. */ cancelAllPressedKeys() { + // TODO + /* this.state.currentlyPressedKeys.forEach((value) => { const text = ''; const json = { @@ -392,150 +514,347 @@ module.exports = class KeyboardMapping { this.instance.sendEvent(json); }); this.state.currentlyPressedKeys = []; + */ } - onKeyDown(event) { - const key = event.key; + async generateSequences() { + const sequences = Object.values(this.sequences); + + const biggestSequence = sequences + .map((seq) => { + return seq.length; + }) + .sort() + .pop(); + + const sequencesToMerge = []; + Array.from(Array(biggestSequence).keys()).forEach((i) => { + sequences.forEach((seq) => { + // group sequences by index in order to merge sequences of same index into one sequence + if (!seq.length) { + // no sequence for this key + return; + } + if (seq[i]) { + if (!sequencesToMerge[i]) { + sequencesToMerge[i] = []; + } + sequencesToMerge[i].push(seq[i]); + } else { + // if not found take the last element + if (!sequencesToMerge[i]) { + sequencesToMerge[i] = []; + } + sequencesToMerge[i].push(seq[seq.length - 1]); + } + }); + }); + + // Group the sequences in same index of sequencesToMerge array + const mergedSequences = []; + sequencesToMerge.forEach((sequencesToGroup) => { + // TODO calculate mode + mergedSequences.push({ + type: 'MULTI_TOUCH', + nb: sequencesToGroup.length, + mode: sequencesToGroup[0].mode, // TODO ? + points: sequencesToGroup.reduce((acc, val) => { + acc = [...acc, ...val.points]; + return acc; + }, []), + }); + }); - if (this.state.workingMappedKeysConfig[key] && !this.state.currentlyPressedKeys.includes(key)) { - // we need to assign a new array to trigger the setter of the proxy (pass by the proxy's trap) - this.state.currentlyPressedKeys = [...this.state.currentlyPressedKeys, key]; + for (let i = 0; i < mergedSequences.length; i++) { + // Don't await for the first event, games which no need too many data will be faster + if (i % 3 === 0 && i !== 0) { + await new Promise((resolve) => setTimeout(resolve, 1)); + } + this.sendEvent(mergedSequences[i]); } + + console.log('message sent sequence', mergedSequences); + + // keep only last sequence for each key + Object.entries(this.sequences).forEach(([key, value]) => { + if (value.length > 0) { + this.sequences[key] = [value[value.length - 1]]; + } else { + this.sequences[key] = []; + } + }); } - onKeyUp(event) { - const key = event.key; + generateDPADSequence(key) { + // look for the group id + const groupId = this.state.workingMappedKeysConfig[key].groupId; + let initialX = 0; + let initialY = 0; - if (this.state.workingMappedKeysConfig[key]) { - // filter return a new array so the setter of the proxy while be triggered (pass by the proxy's trap) - this.state.currentlyPressedKeys = this.state.currentlyPressedKeys.filter( - (pressedKey) => pressedKey !== key, - ); - } - // Remove the dPadPushed key if in dPadPushed + // look if other pressed keys of this groupId, if yes then empty the sequence and generate a new one + const pressedKeysConfig = Object.values(this.state.workingMappedKeysConfig).filter((value) => { + return value.groupId === groupId && this.currentlyPressedKeys.includes(value.key); + }); - const keyConfig = this.state.workingMappedKeysConfig[key]; - if (keyConfig && keyConfig.type === 'dPad') { - // check if another key of the same dPad is still pressed - const dPadGroupKeys = Object.values(this.state.workingMappedKeysConfig) - .filter((kConfig) => kConfig.groupId === keyConfig.groupId) - .map((kConfig) => kConfig.key); - const dPadGroupKeysPressed = dPadGroupKeys.filter((k) => this.state.currentlyPressedKeys.includes(k)); - if (!dPadGroupKeysPressed.length) { - this.dPadPushed = this.dPadPushed.filter((groupId) => groupId !== keyConfig.groupId); + initialX = this.state.workingMappedKeysConfig[key].initialX; + initialY = this.state.workingMappedKeysConfig[key].initialY; + + const newPosition = { + x: initialX, + y: initialY, + }; + + pressedKeysConfig.forEach((pressedKeyConfig) => { + if (this.state.workingMappedKeysConfig[pressedKeyConfig.key]) { + newPosition.x += pressedKeyConfig.distanceX; + newPosition.y += pressedKeyConfig.distanceY; } + }); + + // if another key is always pressed then initial coord must be from lastpoint + const lastDPADSequence = this.sequences[groupId]?.[this.sequences[groupId].length - 1]; + if (lastDPADSequence) { + // get percent of screen from lastDPADSequence.points.x + const {x, y} = this.calculateCoorToPercent(lastDPADSequence.points[0].x, lastDPADSequence.points[0].y); + // change initialX and initialY + initialX = x; + initialY = y; + } + + // now we have the destination position we need to generate more points, cause some games need like 5 points to trigger a move + const steps = 10; + + // generate the sequences + + // make this only when we start a touch, exclude the current key from the pressedKeysConfig length + if (pressedKeysConfig.length - 1 === 0 && !lastDPADSequence) { + this.sequences[groupId].push({ + type: 'MULTI_TOUCH', + mode: 0, + nb: 1, + points: [this.calculateCoorFromPercent(initialX, initialY)], + }); + } + // get the last sequence of previous dpad position + if (lastDPADSequence) { + this.sequences[groupId].push(lastDPADSequence); + } + for (let i = 0; i <= steps; i++) { + const t = i / steps; + const x = Math.floor(initialX + t * (newPosition.x - initialX)); + const y = Math.floor(initialY + t * (newPosition.y - initialY)); + + this.sequences[groupId].push({ + type: 'MULTI_TOUCH', + mode: 2, + nb: 1, + points: [this.calculateCoorFromPercent(x, y)], + }); } } - /** - * Generate the touch event when a key to send to the vm - * @returns {Object} - the json to send to the vm - */ - generateTouchEventForPush() { - const touchPointsBeforeMove = []; - const touchPoints = []; - const movePoints = []; - - // All keys in currentPressedKeys for a dPad must be address as a single touch event - const dPadAlreadyTriggered = []; - - this.state.currentlyPressedKeys.forEach((key) => { - // All keys in currentPressedKeys for a given dPad are calculate once, as a single touch event - if (dPadAlreadyTriggered.includes(key)) { - return; - } - const keyConfig = this.state.workingMappedKeysConfig[key]; - switch (keyConfig.type) { + generateTouchSequence(key) { + const groupId = this.state.workingMappedKeysConfig[key].groupId; + const x = this.state.workingMappedKeysConfig[key].x; + const y = this.state.workingMappedKeysConfig[key].y; + + this.sequences[groupId].push({ + type: 'MULTI_TOUCH', + mode: 0, + nb: 1, + points: [this.calculateCoorFromPercent(x, y)], + }); + } + + generateSwipeSequence(key) { + const groupId = this.state.workingMappedKeysConfig[key].groupId; + const keyConfig = this.state.workingMappedKeysConfig[key]; + + const newPosition = {x: keyConfig.x, y: keyConfig.y}; + this.sequences[groupId].push({ + type: 'MULTI_TOUCH', + mode: 0, + nb: 1, + points: [this.calculateCoorFromPercent(newPosition.x, newPosition.y)], + }); + + newPosition.x += keyConfig.distanceX; + newPosition.y += keyConfig.distanceY; + + this.sequences[groupId].push({ + type: 'MULTI_TOUCH', + mode: 2, + nb: 1, + points: [this.calculateCoorFromPercent(newPosition.x, newPosition.y)], + }); + } + + async onKeyDown(event) { + const key = event.key; + // if key not found in the config then return + if (!this.state.workingMappedKeysConfig[key]) { + return; + } + + // if key is already pressed then return + if (this.currentlyPressedKeys.includes(key)) { + return; + } + this.currentlyPressedKeys.push(key); + + // generate the sequence for the key pressed + if (this.state.workingMappedKeysConfig[key]) { + switch (this.state.workingMappedKeysConfig[key].type) { case 'dPad': - // eslint-disable-next-line no-case-declarations - const dPadGroupKeys = Object.values(this.state.workingMappedKeysConfig) - .filter((kConfig) => kConfig.groupId === keyConfig.groupId) - .map((kConfig) => kConfig.key); - dPadAlreadyTriggered.push(...dPadGroupKeys); - - // eslint-disable-next-line no-case-declarations - const [dPadT, dPadM] = this.getDPADTouchEvent(keyConfig); - - if (this.dPadPushed.includes(keyConfig.groupId)) { - touchPoints.push(dPadT); - } else { - touchPointsBeforeMove.push(dPadT); - movePoints.push(dPadM); - // also add the groupId to the dPadPushed array to keep track of the dPad pressed - this.dPadPushed.push(keyConfig.groupId); - } + this.generateDPADSequence(key); break; case 'tap': - touchPoints.push(this.getTapTouchEvent(keyConfig)); + this.generateTouchSequence(key); break; case 'swipe': - // eslint-disable-next-line no-case-declarations - const [swipeT, swipeM] = this.getSwipeTouchEvent(keyConfig); - - touchPointsBeforeMove.push(swipeT); - movePoints.push(swipeM); - + this.generateSwipeSequence(key); + break; + case 'dtap': + // Todo better way to manage this ? + this.generateTouchSequence(key); + await Promise.all(this.requestToGenerate); + this.requestToGenerate.push(this.generateSequences()); + await this.onKeyUp(event); + this.generateTouchSequence(key); + await Promise.all(this.requestToGenerate); + this.requestToGenerate.push(this.generateSequences()); + await this.onKeyUp(event); break; default: break; } - }); + } - // adding touchpoins to movePoints to keep static touch when moving - return { - touchPoints: [...touchPointsBeforeMove, ...touchPoints], - movePoints: movePoints.length ? [...touchPoints, ...movePoints] : [], - }; + // wait for other sequence to be sent + await Promise.all(this.requestToGenerate); + this.requestToGenerate.push(this.generateSequences()); } - generateTouchEventForRelease() { - const json = {type: 'MULTI_TOUCH', nb: 0, mode: 1, points: []}; - return json; - } + async onKeyUp(event) { + const key = event.key; + // if key not found in the config then return + if (!this.state.workingMappedKeysConfig[key]) { + return; + } - getDPADTouchEvent(keyConfig) { - // get all touch for this dPad - const dPadGroupedKeys = Object.values(this.state.workingMappedKeysConfig).filter( - (kConfig) => kConfig.groupId === keyConfig.groupId, - ); + const groupId = this.state.workingMappedKeysConfig[key].groupId; - const newPosition = {x: keyConfig.initialX, y: keyConfig.initialY}; + // remove the key from the currentlyPressedKeys + if (this.currentlyPressedKeys.includes(key)) { + this.currentlyPressedKeys = this.currentlyPressedKeys.filter((k) => k !== key); + } - const pressedKeys = dPadGroupedKeys.filter((kConfig) => this.state.currentlyPressedKeys.includes(kConfig.key)); + // if all keys are released then empty this.sequence and send release to vm and return + if (!this.currentlyPressedKeys.length) { + const json = { + type: 'MULTI_TOUCH', + nb: 0, + mode: 1, + points: [], + }; - // calculate the new position of the touch based on the dPad pressed keys - pressedKeys.forEach((pressedKeyConfig) => { - if (this.state.workingMappedKeysConfig[pressedKeyConfig.key]) { - newPosition.x += pressedKeyConfig.distanceX; - newPosition.y += pressedKeyConfig.distanceY; - } - }); + // TODO meilleur inte pour attendre que les event soit traité avant d en evoyer de + await Promise.all(this.requestToGenerate); + this.sendEvent(json); + Object.keys(this.sequences).forEach((k) => { + this.sequences[k] = []; + }); + + return; + } - const touchPointsBeforeMove = this.calculateCoorFromPercent(keyConfig.initialX, keyConfig.initialY); - const movePoints = this.calculateCoorFromPercent(newPosition.x, newPosition.y); + // If another key with the same groupId is pressed then generate new coordonate for dpad and return + if ( + this.currentlyPressedKeys.some( + (k) => + this.state.workingMappedKeysConfig[k].groupId === this.state.workingMappedKeysConfig[key].groupId && + this.state.workingMappedKeysConfig[k].type === 'dPad', + ) + ) { + // update dpad sequence + this.generateDPADSequence(key); + await Promise.all(this.requestToGenerate); + this.requestToGenerate = []; + this.requestToGenerate.push(this.generateSequences()); - if (this.dPadPushed.includes(keyConfig.groupId)) { - return [movePoints]; + return; } - return [touchPointsBeforeMove, movePoints]; + + this.sequences[groupId] = []; + await Promise.all(this.requestToGenerate); + this.requestToGenerate.push(this.generateSequences()); } - getTapTouchEvent(keyConfig) { - return this.calculateCoorFromPercent(keyConfig.x, keyConfig.y); + + async onMouseDown(event) { + this.sequences.mouse.push({ + type: 'MULTI_TOUCH', + mode: 0, + nb: 1, + points: [ + { + x: this.instance.coordinateUtils.getXCoordinate(event), + y: this.instance.coordinateUtils.getYCoordinate(event), + }, + ], + }); + await Promise.all(this.requestToGenerate); + this.requestToGenerate.push(this.generateSequences()); } - getSwipeTouchEvent(keyConfig) { - const newPosition = {x: keyConfig.x, y: keyConfig.y}; + async onMouseUp() { + // if all key are up then send touch up + // TODO refacto this to have a fn which clean up touch + if (this.currentlyPressedKeys.length === 0) { + const json = { + type: 'MULTI_TOUCH', + nb: 0, + mode: 1, + points: [], + }; - const touchPointsBeforeMove = this.calculateCoorFromPercent(keyConfig.x, keyConfig.y); + await Promise.all(this.requestToGenerate); + this.sendEvent(json); + } + // TODO have a fn to push and wait + await Promise.all(this.requestToGenerate); + this.sequences.mouse = []; - newPosition.x += keyConfig.distanceX; - newPosition.y += keyConfig.distanceY; + this.requestToGenerate.push(this.generateSequences()); + } - const movePoints = this.calculateCoorFromPercent(newPosition.x, newPosition.y); + async onMouseMove(event) { + if (event.buttons !== 1) { + return; + } - return [touchPointsBeforeMove, movePoints]; + this.sequences.mouse.push({ + type: 'MULTI_TOUCH', + mode: 2, + nb: 1, + points: [ + { + x: this.instance.coordinateUtils.getXCoordinate(event), + y: this.instance.coordinateUtils.getYCoordinate(event), + }, + ], + }); + await Promise.all(this.requestToGenerate); + this.requestToGenerate.push(this.generateSequences()); } + getTapTouchEvent(keyConfig) { + return this.calculateCoorFromPercent(keyConfig.x, keyConfig.y); + } + + sendEvent(json) { + // Stop all curently events + this.instance.sendEvent(json); + } /** * Bind all event listener callback. */ @@ -548,6 +867,9 @@ module.exports = class KeyboardMapping { this.keyboardCallbacks = [ {event: 'keydown', handler: this.onKeyDown.bind(this), removeListener: null}, {event: 'keyup', handler: this.onKeyUp.bind(this), removeListener: null}, + {event: 'mousedown', handler: this.onMouseDown.bind(this), removeListener: null}, + {event: 'mouseup', handler: this.onMouseUp.bind(this), removeListener: null}, + {event: 'mousemove', handler: this.onMouseMove.bind(this), removeListener: null}, ]; this.instance.root.focus(); @@ -571,29 +893,6 @@ module.exports = class KeyboardMapping { this.keyboardCallbacks.length = 0; } - tilt() { - const json = { - channel: 'sensors', - messages: [ - 'set accelerometer 0:9.81:0', - 'set gyroscope 0:9.81:0', - 'set accelerometer 1:8.81:0', - 'set gyroscope 1:9.81:0', - 'set accelerometer 0:9.81:0', - 'set gyroscope 0:9.81:0', - ], - }; - this.instance.sendEvent(json); - /* - * Array.from(Array(2).keys()).forEach(() => { - * this.instance.sendEvent({type: 'ACCELEROMETER', x: -100, y: -10, z: 0}); - * this.instance.sendEvent({type: 'ACCELEROMETER', x: 100, y: -10, z: 0}); - * this.instance.sendEvent({type: 'ACCELEROMETER', x: 200, y: 0, z: 0}); - * }); - */ - return; - } - activateTrace(isActive) { const debug = (event) => { // create div with x, y coordonates where this.instantce.root element is clicked @@ -699,4 +998,16 @@ module.exports = class KeyboardMapping { return {x: Math.floor(xFromPercent), y: Math.floor(yFromPercent)}; } + + calculateCoorToPercent(x, y) { + const videoSize = this.instance.video.getBoundingClientRect(); + + const xPercent = 100 / ((videoSize.width * this.instance.coordinateUtils.getXRatio()) / x); + const yPercent = + 100 / + (((videoSize.height - this.instance.coordinateUtils.getTopBorder() * 2) * + this.instance.coordinateUtils.getYRatio()) / + y); + return {x: Math.round(xPercent), y: Math.round(yPercent)}; + } };