[QoL] Improve Input Accuracy by Refactoring Button Handling (#1936)
* refactored inputs-controller for better hold button management * refactored the touch controls file to use a class and add holding button system * added a method to deactivate pressed key for touch on focus lost * better lost focus management
This commit is contained in:
parent
f6ad30b58f
commit
d03c75c2f9
|
@ -314,7 +314,6 @@ export default class BattleScene extends SceneBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
this.inputController.update();
|
|
||||||
this.ui?.update();
|
this.ui?.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import * as Utils from "./utils";
|
import * as Utils from "./utils";
|
||||||
import {deepCopy} from "./utils";
|
import {deepCopy} from "./utils";
|
||||||
import {initTouchControls} from "./touch-controls";
|
|
||||||
import pad_generic from "./configs/inputs/pad_generic";
|
import pad_generic from "./configs/inputs/pad_generic";
|
||||||
import pad_unlicensedSNES from "./configs/inputs/pad_unlicensedSNES";
|
import pad_unlicensedSNES from "./configs/inputs/pad_unlicensedSNES";
|
||||||
import pad_xbox360 from "./configs/inputs/pad_xbox360";
|
import pad_xbox360 from "./configs/inputs/pad_xbox360";
|
||||||
|
@ -21,6 +20,7 @@ import {
|
||||||
import BattleScene from "./battle-scene";
|
import BattleScene from "./battle-scene";
|
||||||
import {SettingGamepad} from "#app/system/settings/settings-gamepad.js";
|
import {SettingGamepad} from "#app/system/settings/settings-gamepad.js";
|
||||||
import {SettingKeyboard} from "#app/system/settings/settings-keyboard";
|
import {SettingKeyboard} from "#app/system/settings/settings-keyboard";
|
||||||
|
import TouchControl from "#app/touch-controls";
|
||||||
|
|
||||||
export interface DeviceMapping {
|
export interface DeviceMapping {
|
||||||
[key: string]: number;
|
[key: string]: number;
|
||||||
|
@ -48,7 +48,7 @@ export interface InterfaceConfig {
|
||||||
custom?: MappingLayout;
|
custom?: MappingLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
const repeatInputDelayMillis = 500;
|
const repeatInputDelayMillis = 250;
|
||||||
|
|
||||||
// Phaser.Input.Gamepad.GamepadPlugin#refreshPads
|
// Phaser.Input.Gamepad.GamepadPlugin#refreshPads
|
||||||
declare module "phaser" {
|
declare module "phaser" {
|
||||||
|
@ -92,7 +92,7 @@ export class InputsController {
|
||||||
private scene: BattleScene;
|
private scene: BattleScene;
|
||||||
public events: Phaser.Events.EventEmitter;
|
public events: Phaser.Events.EventEmitter;
|
||||||
|
|
||||||
private buttonLock: Button;
|
private buttonLock: Button[] = new Array();
|
||||||
private interactions: Map<Button, Map<string, boolean>> = new Map();
|
private interactions: Map<Button, Map<string, boolean>> = new Map();
|
||||||
private configs: Map<string, InterfaceConfig> = new Map();
|
private configs: Map<string, InterfaceConfig> = new Map();
|
||||||
|
|
||||||
|
@ -101,10 +101,10 @@ export class InputsController {
|
||||||
|
|
||||||
private disconnectedGamepads: Array<String> = new Array();
|
private disconnectedGamepads: Array<String> = new Array();
|
||||||
|
|
||||||
private pauseUpdate: boolean = false;
|
|
||||||
|
|
||||||
public lastSource: string = "keyboard";
|
public lastSource: string = "keyboard";
|
||||||
private keys: Array<number> = [];
|
private inputInterval: NodeJS.Timeout[] = new Array();
|
||||||
|
private touchControls: TouchControl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a new instance of the game control system, setting up initial state and configurations.
|
* Initializes a new instance of the game control system, setting up initial state and configurations.
|
||||||
|
@ -181,7 +181,7 @@ export class InputsController {
|
||||||
this.scene.input.keyboard.on("keydown", this.keyboardKeyDown, this);
|
this.scene.input.keyboard.on("keydown", this.keyboardKeyDown, this);
|
||||||
this.scene.input.keyboard.on("keyup", this.keyboardKeyUp, this);
|
this.scene.input.keyboard.on("keyup", this.keyboardKeyUp, this);
|
||||||
}
|
}
|
||||||
initTouchControls(this.events);
|
this.touchControls = new TouchControl(this.scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -192,6 +192,7 @@ export class InputsController {
|
||||||
*/
|
*/
|
||||||
loseFocus(): void {
|
loseFocus(): void {
|
||||||
this.deactivatePressedKey();
|
this.deactivatePressedKey();
|
||||||
|
this.touchControls.deactivatePressedKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -232,47 +233,6 @@ export class InputsController {
|
||||||
this.initChosenLayoutKeyboard(layoutKeyboard);
|
this.initChosenLayoutKeyboard(layoutKeyboard);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the interaction handling by processing input states.
|
|
||||||
* This method gives priority to certain buttons by reversing the order in which they are checked.
|
|
||||||
* This method loops through all button values, checks for valid and timely interactions, and conditionally processes
|
|
||||||
* or ignores them based on the current state of gamepad support and other criteria.
|
|
||||||
*
|
|
||||||
* It handles special conditions such as the absence of gamepad support or mismatches between the source of the input and
|
|
||||||
* the currently chosen gamepad. It also respects the paused state of updates to prevent unwanted input processing.
|
|
||||||
*
|
|
||||||
* If an interaction is valid and should be processed, it emits an 'input_down' event with details of the interaction.
|
|
||||||
*/
|
|
||||||
update(): void {
|
|
||||||
if (this.pauseUpdate) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const b of Utils.getEnumValues(Button).reverse()) {
|
|
||||||
if (
|
|
||||||
this.interactions.hasOwnProperty(b) &&
|
|
||||||
this.repeatInputDurationJustPassed(b as Button) &&
|
|
||||||
this.interactions[b].isPressed
|
|
||||||
) {
|
|
||||||
// Prevents repeating button interactions when gamepad support is disabled.
|
|
||||||
if (
|
|
||||||
(!this.gamepadSupport && this.interactions[b].source === "gamepad") ||
|
|
||||||
(this.interactions[b].source === "gamepad" && this.interactions[b].sourceName && this.interactions[b].sourceName !== this.selectedDevice[Device.GAMEPAD]) ||
|
|
||||||
(this.interactions[b].source === "keyboard" && this.interactions[b].sourceName && this.interactions[b].sourceName !== this.selectedDevice[Device.KEYBOARD])
|
|
||||||
) {
|
|
||||||
// Deletes the last interaction for a button if gamepad is disabled.
|
|
||||||
this.delLastProcessedMovementTime(b as Button);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Emits an event for the button press.
|
|
||||||
this.events.emit("input_down", {
|
|
||||||
controller_type: this.interactions[b].source,
|
|
||||||
button: b,
|
|
||||||
});
|
|
||||||
this.setLastProcessedMovementTime(b as Button, this.interactions[b].source, this.interactions[b].sourceName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the identifiers of all connected gamepads, excluding any that are currently marked as disconnected.
|
* Retrieves the identifiers of all connected gamepads, excluding any that are currently marked as disconnected.
|
||||||
* @returns Array<String> An array of strings representing the IDs of the connected gamepads.
|
* @returns Array<String> An array of strings representing the IDs of the connected gamepads.
|
||||||
|
@ -404,19 +364,24 @@ export class InputsController {
|
||||||
*/
|
*/
|
||||||
keyboardKeyDown(event): void {
|
keyboardKeyDown(event): void {
|
||||||
this.lastSource = "keyboard";
|
this.lastSource = "keyboard";
|
||||||
const keyDown = event.keyCode;
|
|
||||||
this.ensureKeyboardIsInit();
|
this.ensureKeyboardIsInit();
|
||||||
if (this.keys.includes(keyDown)) {
|
const buttonDown = getButtonWithKeycode(this.getActiveConfig(Device.KEYBOARD), event.keyCode);
|
||||||
|
if (buttonDown !== undefined) {
|
||||||
|
if (this.buttonLock.includes(buttonDown)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.keys.push(keyDown);
|
|
||||||
const buttonDown = getButtonWithKeycode(this.getActiveConfig(Device.KEYBOARD), keyDown);
|
|
||||||
if (buttonDown !== undefined) {
|
|
||||||
this.events.emit("input_down", {
|
this.events.emit("input_down", {
|
||||||
controller_type: "keyboard",
|
controller_type: "keyboard",
|
||||||
button: buttonDown,
|
button: buttonDown,
|
||||||
});
|
});
|
||||||
this.setLastProcessedMovementTime(buttonDown, "keyboard", this.selectedDevice[Device.KEYBOARD]);
|
clearInterval(this.inputInterval[buttonDown]);
|
||||||
|
this.inputInterval[buttonDown] = setInterval(() => {
|
||||||
|
this.events.emit("input_down", {
|
||||||
|
controller_type: "keyboard",
|
||||||
|
button: buttonDown,
|
||||||
|
});
|
||||||
|
}, repeatInputDelayMillis);
|
||||||
|
this.buttonLock.push(buttonDown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -427,16 +392,15 @@ export class InputsController {
|
||||||
*/
|
*/
|
||||||
keyboardKeyUp(event): void {
|
keyboardKeyUp(event): void {
|
||||||
this.lastSource = "keyboard";
|
this.lastSource = "keyboard";
|
||||||
const keyDown = event.keyCode;
|
const buttonUp = getButtonWithKeycode(this.getActiveConfig(Device.KEYBOARD), event.keyCode);
|
||||||
this.keys = this.keys.filter(k => k !== keyDown);
|
|
||||||
this.ensureKeyboardIsInit();
|
|
||||||
const buttonUp = getButtonWithKeycode(this.getActiveConfig(Device.KEYBOARD), keyDown);
|
|
||||||
if (buttonUp !== undefined) {
|
if (buttonUp !== undefined) {
|
||||||
this.events.emit("input_up", {
|
this.events.emit("input_up", {
|
||||||
controller_type: "keyboard",
|
controller_type: "keyboard",
|
||||||
button: buttonUp,
|
button: buttonUp,
|
||||||
});
|
});
|
||||||
this.delLastProcessedMovementTime(buttonUp);
|
const index = this.buttonLock.indexOf(buttonUp);
|
||||||
|
this.buttonLock.splice(index, 1);
|
||||||
|
clearInterval(this.inputInterval[buttonUp]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -466,11 +430,25 @@ export class InputsController {
|
||||||
const activeConfig = this.getActiveConfig(Device.GAMEPAD);
|
const activeConfig = this.getActiveConfig(Device.GAMEPAD);
|
||||||
const buttonDown = activeConfig && getButtonWithKeycode(activeConfig, button.index);
|
const buttonDown = activeConfig && getButtonWithKeycode(activeConfig, button.index);
|
||||||
if (buttonDown !== undefined) {
|
if (buttonDown !== undefined) {
|
||||||
|
if (this.buttonLock.includes(buttonDown)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.events.emit("input_down", {
|
this.events.emit("input_down", {
|
||||||
controller_type: "gamepad",
|
controller_type: "gamepad",
|
||||||
button: buttonDown,
|
button: buttonDown,
|
||||||
});
|
});
|
||||||
this.setLastProcessedMovementTime(buttonDown, "gamepad", pad.id);
|
clearInterval(this.inputInterval[buttonDown]);
|
||||||
|
this.inputInterval[buttonDown] = setInterval(() => {
|
||||||
|
if (!this.buttonLock.includes(buttonDown)) {
|
||||||
|
clearInterval(this.inputInterval[buttonDown]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.events.emit("input_down", {
|
||||||
|
controller_type: "gamepad",
|
||||||
|
button: buttonDown,
|
||||||
|
});
|
||||||
|
}, repeatInputDelayMillis);
|
||||||
|
this.buttonLock.push(buttonDown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -497,7 +475,9 @@ export class InputsController {
|
||||||
controller_type: "gamepad",
|
controller_type: "gamepad",
|
||||||
button: buttonUp,
|
button: buttonUp,
|
||||||
});
|
});
|
||||||
this.delLastProcessedMovementTime(buttonUp);
|
const index = this.buttonLock.indexOf(buttonUp);
|
||||||
|
this.buttonLock.splice(index, 1);
|
||||||
|
clearInterval(this.inputInterval[buttonUp]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -540,144 +520,13 @@ export class InputsController {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* repeatInputDurationJustPassed returns true if @param button has been held down long
|
* Deactivates all currently pressed keys.
|
||||||
* enough to fire a repeated input. A button must claim the buttonLock before
|
|
||||||
* firing a repeated input - this is to prevent multiple buttons from firing repeatedly.
|
|
||||||
*/
|
|
||||||
repeatInputDurationJustPassed(button: Button): boolean {
|
|
||||||
if (!this.isButtonLocked(button)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const duration = Date.now() - this.interactions[button].pressTime;
|
|
||||||
if (duration >= repeatInputDelayMillis) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method updates the interaction state to reflect that the button is pressed.
|
|
||||||
*
|
|
||||||
* @param button - The button for which to set the interaction.
|
|
||||||
* @param source - The source of the input (defaults to 'keyboard'). This helps identify the origin of the input, especially useful in environments with multiple input devices.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This method is responsible for updating the interaction state of a button within the `interactions` dictionary. If the button is not already registered, this method returns immediately.
|
|
||||||
* When invoked, it performs the following updates:
|
|
||||||
* - `pressTime`: Sets this to the current time, representing when the button was initially pressed.
|
|
||||||
* - `isPressed`: Marks the button as currently being pressed.
|
|
||||||
* - `source`: Identifies the source device of the input, which can vary across different hardware (e.g., keyboard, gamepad).
|
|
||||||
*
|
|
||||||
* Additionally, this method locks the button (by calling `setButtonLock`) to prevent it from being re-processed until it is released, ensuring that each press is handled distinctly.
|
|
||||||
*/
|
|
||||||
setLastProcessedMovementTime(button: Button, source: String = "keyboard", sourceName?: String): void {
|
|
||||||
if (!this.interactions.hasOwnProperty(button)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setButtonLock(button);
|
|
||||||
this.interactions[button].pressTime = Date.now();
|
|
||||||
this.interactions[button].isPressed = true;
|
|
||||||
this.interactions[button].source = source;
|
|
||||||
this.interactions[button].sourceName = sourceName.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the last interaction for a specified button.
|
|
||||||
*
|
|
||||||
* @param button - The button for which to clear the interaction.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This method resets the interaction details of the button, allowing it to be processed as a new input when pressed again.
|
|
||||||
* If the button is not registered in the `interactions` dictionary, this method returns immediately, otherwise:
|
|
||||||
* - `pressTime` is cleared. This was previously storing the timestamp of when the button was initially pressed.
|
|
||||||
* - `isPressed` is set to false, indicating that the button is no longer being pressed.
|
|
||||||
* - `source` is set to null, which had been indicating the device from which the button input was originating.
|
|
||||||
*
|
|
||||||
* It releases the button lock, which prevents the button from being processed repeatedly until it's explicitly released.
|
|
||||||
*/
|
|
||||||
delLastProcessedMovementTime(button: Button): void {
|
|
||||||
if (!this.interactions.hasOwnProperty(button)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.releaseButtonLock(button);
|
|
||||||
this.interactions[button].pressTime = null;
|
|
||||||
this.interactions[button].isPressed = false;
|
|
||||||
this.interactions[button].source = null;
|
|
||||||
this.interactions[button].sourceName = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deactivates all currently pressed keys and resets their interaction states.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This method is used to reset the state of all buttons within the `interactions` dictionary,
|
|
||||||
* effectively deactivating any currently pressed keys. It performs the following actions:
|
|
||||||
*
|
|
||||||
* - Releases button lock for predefined buttons, allowing them
|
|
||||||
* to be pressed again or properly re-initialized in future interactions.
|
|
||||||
* - Iterates over all possible button values obtained via `Utils.getEnumValues(Button)`, and for
|
|
||||||
* each button:
|
|
||||||
* - Checks if the button is currently registered in the `interactions` dictionary.
|
|
||||||
* - Resets `pressTime` to null, indicating that there is no ongoing interaction.
|
|
||||||
* - Sets `isPressed` to false, marking the button as not currently active.
|
|
||||||
* - Clears the `source` field, removing the record of which device the button press came from.
|
|
||||||
*
|
|
||||||
* This method is typically called when needing to ensure that all inputs are neutralized.
|
|
||||||
*/
|
*/
|
||||||
deactivatePressedKey(): void {
|
deactivatePressedKey(): void {
|
||||||
this.pauseUpdate = true;
|
for (const key of Object.keys(this.inputInterval)) {
|
||||||
this.releaseButtonLock(this.buttonLock);
|
clearInterval(this.inputInterval[key]);
|
||||||
for (const b of Utils.getEnumValues(Button)) {
|
|
||||||
if (this.interactions.hasOwnProperty(b)) {
|
|
||||||
this.interactions[b].pressTime = null;
|
|
||||||
this.interactions[b].isPressed = false;
|
|
||||||
this.interactions[b].source = null;
|
|
||||||
this.interactions[b].sourceName = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.pauseUpdate = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a specific button is currently locked.
|
|
||||||
*
|
|
||||||
* @param button - The button to check for a lock status.
|
|
||||||
* @returns `true` if the button is locked, otherwise `false`.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This method is used to determine if a given button is currently prevented from being processed due to a lock.
|
|
||||||
* It checks against two separate lock variables, allowing for up to two buttons to be locked simultaneously.
|
|
||||||
*/
|
|
||||||
isButtonLocked(button: Button): boolean {
|
|
||||||
return this.buttonLock === button;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a lock on a given button.
|
|
||||||
*
|
|
||||||
* @param button - The button to lock.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This method ensures that a button is not processed multiple times inadvertently.
|
|
||||||
* It checks if the button is already locked.
|
|
||||||
*/
|
|
||||||
setButtonLock(button: Button): void {
|
|
||||||
this.buttonLock = button;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Releases a lock on a specific button, allowing it to be processed again.
|
|
||||||
*
|
|
||||||
* @param button - The button whose lock is to be released.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This method checks lock variable.
|
|
||||||
* If either lock matches the specified button, that lock is cleared.
|
|
||||||
* This action frees the button to be processed again, ensuring it can respond to new inputs.
|
|
||||||
*/
|
|
||||||
releaseButtonLock(button: Button): void {
|
|
||||||
if (this.buttonLock === button) {
|
|
||||||
this.buttonLock = null;
|
|
||||||
}
|
}
|
||||||
|
this.buttonLock = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -751,8 +600,7 @@ export class InputsController {
|
||||||
* @param pressedButton The button that was pressed.
|
* @param pressedButton The button that was pressed.
|
||||||
*/
|
*/
|
||||||
assignBinding(config, settingName, pressedButton): boolean {
|
assignBinding(config, settingName, pressedButton): boolean {
|
||||||
this.pauseUpdate = true;
|
this.deactivatePressedKey();
|
||||||
setTimeout(() => this.pauseUpdate = false, 500);
|
|
||||||
if (config.padType === "keyboard") {
|
if (config.padType === "keyboard") {
|
||||||
return assign(config, settingName, pressedButton);
|
return assign(config, settingName, pressedButton);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -52,11 +52,6 @@ describe("Inputs", () => {
|
||||||
expect(game.inputsHandler.log.length).toBe(4);
|
expect(game.inputsHandler.log.length).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keyboard - test input holding for 1ms - 1 input", async() => {
|
|
||||||
await game.inputsHandler.pressKeyboardKey(cfg_keyboard_qwerty.deviceMapping.KEY_ARROW_UP, 1);
|
|
||||||
expect(game.inputsHandler.log.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keyboard - test input holding for 200ms - 1 input", async() => {
|
it("keyboard - test input holding for 200ms - 1 input", async() => {
|
||||||
await game.inputsHandler.pressKeyboardKey(cfg_keyboard_qwerty.deviceMapping.KEY_ARROW_UP, 200);
|
await game.inputsHandler.pressKeyboardKey(cfg_keyboard_qwerty.deviceMapping.KEY_ARROW_UP, 200);
|
||||||
expect(game.inputsHandler.log.length).toBe(1);
|
expect(game.inputsHandler.log.length).toBe(1);
|
||||||
|
@ -87,6 +82,11 @@ describe("Inputs", () => {
|
||||||
expect(game.inputsHandler.log.length).toBe(1);
|
expect(game.inputsHandler.log.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("gamepad - test input holding for 249ms - 1 input", async() => {
|
||||||
|
await game.inputsHandler.pressGamepadButton(pad_xbox360.deviceMapping.RC_S, 249);
|
||||||
|
expect(game.inputsHandler.log.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("gamepad - test input holding for 300ms - 2 input", async() => {
|
it("gamepad - test input holding for 300ms - 2 input", async() => {
|
||||||
await game.inputsHandler.pressGamepadButton(pad_xbox360.deviceMapping.RC_S, 300);
|
await game.inputsHandler.pressGamepadButton(pad_xbox360.deviceMapping.RC_S, 300);
|
||||||
expect(game.inputsHandler.log.length).toBe(2);
|
expect(game.inputsHandler.log.length).toBe(2);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Phaser from "phaser";
|
||||||
import {InputsController} from "#app/inputs-controller";
|
import {InputsController} from "#app/inputs-controller";
|
||||||
import pad_xbox360 from "#app/configs/inputs/pad_xbox360";
|
import pad_xbox360 from "#app/configs/inputs/pad_xbox360";
|
||||||
import {holdOn} from "#app/test/utils/gameManagerUtils";
|
import {holdOn} from "#app/test/utils/gameManagerUtils";
|
||||||
import {initTouchControls} from "#app/touch-controls";
|
import TouchControl from "#app/touch-controls";
|
||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|
||||||
|
@ -54,10 +54,8 @@ export default class InputsHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
init(): void {
|
init(): void {
|
||||||
setInterval(() => {
|
const touchControl = new TouchControl(this.scene);
|
||||||
this.inputController.update();
|
touchControl.deactivatePressedKey(); //test purpose
|
||||||
});
|
|
||||||
initTouchControls(this.inputController.events);
|
|
||||||
this.events = this.inputController.events;
|
this.events = this.inputController.events;
|
||||||
this.scene.input.gamepad.emit("connected", this.fakePad);
|
this.scene.input.gamepad.emit("connected", this.fakePad);
|
||||||
this.listenInputs();
|
this.listenInputs();
|
||||||
|
|
|
@ -1,25 +1,164 @@
|
||||||
import {Button} from "./enums/buttons";
|
import {Button} from "./enums/buttons";
|
||||||
import EventEmitter = Phaser.Events.EventEmitter;
|
import EventEmitter = Phaser.Events.EventEmitter;
|
||||||
|
import BattleScene from "./battle-scene";
|
||||||
|
|
||||||
// Create a map to store key bindings
|
const repeatInputDelayMillis = 250;
|
||||||
export const keys = new Map<string, string>();
|
|
||||||
// Create a map to store keys that are currently pressed
|
|
||||||
export const keysDown = new Map<string, string>();
|
|
||||||
// Variable to store the ID of the last touched element
|
|
||||||
let lastTouchedId: string;
|
|
||||||
|
|
||||||
/**
|
export default class TouchControl {
|
||||||
|
events: EventEmitter;
|
||||||
|
private buttonLock: string[] = new Array();
|
||||||
|
private inputInterval: NodeJS.Timeout[] = new Array();
|
||||||
|
|
||||||
|
constructor(scene: BattleScene) {
|
||||||
|
this.events = scene.game.events;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
* Initialize touch controls by binding keys to buttons.
|
* Initialize touch controls by binding keys to buttons.
|
||||||
*
|
|
||||||
* @param events - The event emitter for handling input events.
|
|
||||||
*/
|
*/
|
||||||
export function initTouchControls(events: EventEmitter): void {
|
init() {
|
||||||
preventElementZoom(document.querySelector("#dpad"));
|
this.preventElementZoom(document.querySelector("#dpad"));
|
||||||
preventElementZoom(document.querySelector("#apad"));
|
this.preventElementZoom(document.querySelector("#apad"));
|
||||||
// Select all elements with the 'data-key' attribute and bind keys to them
|
// Select all elements with the 'data-key' attribute and bind keys to them
|
||||||
for (const button of document.querySelectorAll("[data-key]")) {
|
for (const button of document.querySelectorAll("[data-key]")) {
|
||||||
// @ts-ignore - Bind the key to the button using the dataset key
|
// @ts-ignore - Bind the key to the button using the dataset key
|
||||||
bindKey(button, button.dataset.key, events);
|
this.bindKey(button, button.dataset.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a node to a specific key to simulate keyboard events on touch.
|
||||||
|
*
|
||||||
|
* @param node - The DOM element to bind the key to.
|
||||||
|
* @param key - The key to simulate.
|
||||||
|
* @param events - The event emitter for handling input events.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This function binds touch events to a node to simulate 'keydown' and 'keyup' keyboard events.
|
||||||
|
* It adds the key to the keys map and tracks the keydown state. When a touch starts, it simulates
|
||||||
|
* a 'keydown' event and adds an 'active' class to the node. When the touch ends, it simulates a 'keyup'
|
||||||
|
* event, removes the keydown state, and removes the 'active' class from the node and the last touched element.
|
||||||
|
*/
|
||||||
|
bindKey(node: HTMLElement, key: string) {
|
||||||
|
node.addEventListener("touchstart", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.touchButtonDown(node, key);
|
||||||
|
});
|
||||||
|
|
||||||
|
node.addEventListener("touchend", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.touchButtonUp(node, key, event.target["id"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
touchButtonDown(node: HTMLElement, key: string) {
|
||||||
|
if (this.buttonLock.includes(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.simulateKeyboardEvent("keydown", key);
|
||||||
|
clearInterval(this.inputInterval[key]);
|
||||||
|
this.inputInterval[key] = setInterval(() => {
|
||||||
|
this.simulateKeyboardEvent("keydown", key);
|
||||||
|
}, repeatInputDelayMillis);
|
||||||
|
this.buttonLock.push(key);
|
||||||
|
node.classList.add("active");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
touchButtonUp(node: HTMLElement, key: string, id: string) {
|
||||||
|
if (!this.buttonLock.includes(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.simulateKeyboardEvent("keyup", key);
|
||||||
|
|
||||||
|
node.classList.remove("active");
|
||||||
|
|
||||||
|
document.getElementById(id)?.classList.remove("active");
|
||||||
|
const index = this.buttonLock.indexOf(key);
|
||||||
|
this.buttonLock.splice(index, 1);
|
||||||
|
clearInterval(this.inputInterval[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulates a keyboard event on the canvas.
|
||||||
|
*
|
||||||
|
* @param eventType - The type of the keyboard event ('keydown' or 'keyup').
|
||||||
|
* @param key - The key to simulate.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This function checks if the key exists in the Button enum. If it does, it retrieves the corresponding button
|
||||||
|
* and emits the appropriate event ('input_down' or 'input_up') based on the event type.
|
||||||
|
*/
|
||||||
|
simulateKeyboardEvent(eventType: string, key: string) {
|
||||||
|
if (!Button.hasOwnProperty(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const button = Button[key];
|
||||||
|
|
||||||
|
switch (eventType) {
|
||||||
|
case "keydown":
|
||||||
|
this.events.emit("input_down", {
|
||||||
|
controller_type: "keyboard",
|
||||||
|
button: button,
|
||||||
|
isTouch: true
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "keyup":
|
||||||
|
this.events.emit("input_up", {
|
||||||
|
controller_type: "keyboard",
|
||||||
|
button: button,
|
||||||
|
isTouch: true
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link https://stackoverflow.com/a/39778831/4622620|Source}
|
||||||
|
*
|
||||||
|
* Prevent zoom on specified element
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
*/
|
||||||
|
preventElementZoom(element: HTMLElement): void {
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
element.addEventListener("touchstart", (event: TouchEvent) => {
|
||||||
|
|
||||||
|
if (!(event.currentTarget instanceof HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTouchTimeStamp = event.timeStamp;
|
||||||
|
const previousTouchTimeStamp = Number(event.currentTarget.dataset.lastTouchTimeStamp) || currentTouchTimeStamp;
|
||||||
|
const timeStampDifference = currentTouchTimeStamp - previousTouchTimeStamp;
|
||||||
|
const fingers = event.touches.length;
|
||||||
|
event.currentTarget.dataset.lastTouchTimeStamp = String(currentTouchTimeStamp);
|
||||||
|
|
||||||
|
if (!timeStampDifference || timeStampDifference > 500 || fingers > 1) {
|
||||||
|
return;
|
||||||
|
} // not double-tap
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (event.target instanceof HTMLElement) {
|
||||||
|
event.target.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivates all currently pressed keys.
|
||||||
|
*/
|
||||||
|
deactivatePressedKey(): void {
|
||||||
|
for (const key of Object.keys(this.inputInterval)) {
|
||||||
|
clearInterval(this.inputInterval[key]);
|
||||||
|
}
|
||||||
|
for (const button of document.querySelectorAll("[data-key]")) {
|
||||||
|
button.classList.remove("active");
|
||||||
|
}
|
||||||
|
this.buttonLock = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,113 +186,3 @@ export function isMobile(): boolean {
|
||||||
})(navigator.userAgent || navigator.vendor || window["opera"]);
|
})(navigator.userAgent || navigator.vendor || window["opera"]);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Simulates a keyboard event on the canvas.
|
|
||||||
*
|
|
||||||
* @param eventType - The type of the keyboard event ('keydown' or 'keyup').
|
|
||||||
* @param key - The key to simulate.
|
|
||||||
* @param events - The event emitter for handling input events.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This function checks if the key exists in the Button enum. If it does, it retrieves the corresponding button
|
|
||||||
* and emits the appropriate event ('input_down' or 'input_up') based on the event type.
|
|
||||||
*/
|
|
||||||
function simulateKeyboardEvent(eventType: string, key: string, events: EventEmitter) {
|
|
||||||
if (!Button.hasOwnProperty(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const button = Button[key];
|
|
||||||
|
|
||||||
switch (eventType) {
|
|
||||||
case "keydown":
|
|
||||||
events.emit("input_down", {
|
|
||||||
controller_type: "keyboard",
|
|
||||||
button: button,
|
|
||||||
isTouch: true
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "keyup":
|
|
||||||
events.emit("input_up", {
|
|
||||||
controller_type: "keyboard",
|
|
||||||
button: button,
|
|
||||||
isTouch: true
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds a node to a specific key to simulate keyboard events on touch.
|
|
||||||
*
|
|
||||||
* @param node - The DOM element to bind the key to.
|
|
||||||
* @param key - The key to simulate.
|
|
||||||
* @param events - The event emitter for handling input events.
|
|
||||||
*
|
|
||||||
* @remarks
|
|
||||||
* This function binds touch events to a node to simulate 'keydown' and 'keyup' keyboard events.
|
|
||||||
* It adds the key to the keys map and tracks the keydown state. When a touch starts, it simulates
|
|
||||||
* a 'keydown' event and adds an 'active' class to the node. When the touch ends, it simulates a 'keyup'
|
|
||||||
* event, removes the keydown state, and removes the 'active' class from the node and the last touched element.
|
|
||||||
*/
|
|
||||||
function bindKey(node: HTMLElement, key: string, events) {
|
|
||||||
keys.set(node.id, key);
|
|
||||||
|
|
||||||
node.addEventListener("touchstart", event => {
|
|
||||||
event.preventDefault();
|
|
||||||
simulateKeyboardEvent("keydown", key, events);
|
|
||||||
keysDown.set(event.target["id"], node.id);
|
|
||||||
node.classList.add("active");
|
|
||||||
});
|
|
||||||
|
|
||||||
node.addEventListener("touchend", event => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const pressedKey = keysDown.get(event.target["id"]);
|
|
||||||
if (pressedKey && keys.has(pressedKey)) {
|
|
||||||
const key = keys.get(pressedKey);
|
|
||||||
simulateKeyboardEvent("keyup", key, events);
|
|
||||||
}
|
|
||||||
|
|
||||||
keysDown.delete(event.target["id"]);
|
|
||||||
node.classList.remove("active");
|
|
||||||
|
|
||||||
if (lastTouchedId) {
|
|
||||||
document.getElementById(lastTouchedId).classList.remove("active");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link https://stackoverflow.com/a/39778831/4622620|Source}
|
|
||||||
*
|
|
||||||
* Prevent zoom on specified element
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
*/
|
|
||||||
function preventElementZoom(element: HTMLElement): void {
|
|
||||||
if (!element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
element.addEventListener("touchstart", (event: TouchEvent) => {
|
|
||||||
|
|
||||||
if (!(event.currentTarget instanceof HTMLElement)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentTouchTimeStamp = event.timeStamp;
|
|
||||||
const previousTouchTimeStamp = Number(event.currentTarget.dataset.lastTouchTimeStamp) || currentTouchTimeStamp;
|
|
||||||
const timeStampDifference = currentTouchTimeStamp - previousTouchTimeStamp;
|
|
||||||
const fingers = event.touches.length;
|
|
||||||
event.currentTarget.dataset.lastTouchTimeStamp = String(currentTouchTimeStamp);
|
|
||||||
|
|
||||||
if (!timeStampDifference || timeStampDifference > 500 || fingers > 1) {
|
|
||||||
return;
|
|
||||||
} // not double-tap
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (event.target instanceof HTMLElement) {
|
|
||||||
event.target.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue