[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() {
|
||||
this.inputController.update();
|
||||
this.ui?.update();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import Phaser from "phaser";
|
||||
import * as Utils from "./utils";
|
||||
import {deepCopy} from "./utils";
|
||||
import {initTouchControls} from "./touch-controls";
|
||||
import pad_generic from "./configs/inputs/pad_generic";
|
||||
import pad_unlicensedSNES from "./configs/inputs/pad_unlicensedSNES";
|
||||
import pad_xbox360 from "./configs/inputs/pad_xbox360";
|
||||
|
@ -21,6 +20,7 @@ import {
|
|||
import BattleScene from "./battle-scene";
|
||||
import {SettingGamepad} from "#app/system/settings/settings-gamepad.js";
|
||||
import {SettingKeyboard} from "#app/system/settings/settings-keyboard";
|
||||
import TouchControl from "#app/touch-controls";
|
||||
|
||||
export interface DeviceMapping {
|
||||
[key: string]: number;
|
||||
|
@ -48,7 +48,7 @@ export interface InterfaceConfig {
|
|||
custom?: MappingLayout;
|
||||
}
|
||||
|
||||
const repeatInputDelayMillis = 500;
|
||||
const repeatInputDelayMillis = 250;
|
||||
|
||||
// Phaser.Input.Gamepad.GamepadPlugin#refreshPads
|
||||
declare module "phaser" {
|
||||
|
@ -92,7 +92,7 @@ export class InputsController {
|
|||
private scene: BattleScene;
|
||||
public events: Phaser.Events.EventEmitter;
|
||||
|
||||
private buttonLock: Button;
|
||||
private buttonLock: Button[] = new Array();
|
||||
private interactions: Map<Button, Map<string, boolean>> = new Map();
|
||||
private configs: Map<string, InterfaceConfig> = new Map();
|
||||
|
||||
|
@ -101,10 +101,10 @@ export class InputsController {
|
|||
|
||||
private disconnectedGamepads: Array<String> = new Array();
|
||||
|
||||
private pauseUpdate: boolean = false;
|
||||
|
||||
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.
|
||||
|
@ -181,7 +181,7 @@ export class InputsController {
|
|||
this.scene.input.keyboard.on("keydown", this.keyboardKeyDown, 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 {
|
||||
this.deactivatePressedKey();
|
||||
this.touchControls.deactivatePressedKey();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -232,47 +233,6 @@ export class InputsController {
|
|||
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.
|
||||
* @returns Array<String> An array of strings representing the IDs of the connected gamepads.
|
||||
|
@ -404,19 +364,24 @@ export class InputsController {
|
|||
*/
|
||||
keyboardKeyDown(event): void {
|
||||
this.lastSource = "keyboard";
|
||||
const keyDown = event.keyCode;
|
||||
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;
|
||||
}
|
||||
this.keys.push(keyDown);
|
||||
const buttonDown = getButtonWithKeycode(this.getActiveConfig(Device.KEYBOARD), keyDown);
|
||||
if (buttonDown !== undefined) {
|
||||
this.events.emit("input_down", {
|
||||
controller_type: "keyboard",
|
||||
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 {
|
||||
this.lastSource = "keyboard";
|
||||
const keyDown = event.keyCode;
|
||||
this.keys = this.keys.filter(k => k !== keyDown);
|
||||
this.ensureKeyboardIsInit();
|
||||
const buttonUp = getButtonWithKeycode(this.getActiveConfig(Device.KEYBOARD), keyDown);
|
||||
const buttonUp = getButtonWithKeycode(this.getActiveConfig(Device.KEYBOARD), event.keyCode);
|
||||
if (buttonUp !== undefined) {
|
||||
this.events.emit("input_up", {
|
||||
controller_type: "keyboard",
|
||||
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 buttonDown = activeConfig && getButtonWithKeycode(activeConfig, button.index);
|
||||
if (buttonDown !== undefined) {
|
||||
if (this.buttonLock.includes(buttonDown)) {
|
||||
return;
|
||||
}
|
||||
this.events.emit("input_down", {
|
||||
controller_type: "gamepad",
|
||||
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",
|
||||
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
|
||||
* 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.
|
||||
* Deactivates all currently pressed keys.
|
||||
*/
|
||||
deactivatePressedKey(): void {
|
||||
this.pauseUpdate = true;
|
||||
this.releaseButtonLock(this.buttonLock);
|
||||
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;
|
||||
for (const key of Object.keys(this.inputInterval)) {
|
||||
clearInterval(this.inputInterval[key]);
|
||||
}
|
||||
this.buttonLock = [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -751,8 +600,7 @@ export class InputsController {
|
|||
* @param pressedButton The button that was pressed.
|
||||
*/
|
||||
assignBinding(config, settingName, pressedButton): boolean {
|
||||
this.pauseUpdate = true;
|
||||
setTimeout(() => this.pauseUpdate = false, 500);
|
||||
this.deactivatePressedKey();
|
||||
if (config.padType === "keyboard") {
|
||||
return assign(config, settingName, pressedButton);
|
||||
} else {
|
||||
|
|
|
@ -52,11 +52,6 @@ describe("Inputs", () => {
|
|||
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() => {
|
||||
await game.inputsHandler.pressKeyboardKey(cfg_keyboard_qwerty.deviceMapping.KEY_ARROW_UP, 200);
|
||||
expect(game.inputsHandler.log.length).toBe(1);
|
||||
|
@ -87,6 +82,11 @@ describe("Inputs", () => {
|
|||
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() => {
|
||||
await game.inputsHandler.pressGamepadButton(pad_xbox360.deviceMapping.RC_S, 300);
|
||||
expect(game.inputsHandler.log.length).toBe(2);
|
||||
|
|
|
@ -3,7 +3,7 @@ import Phaser from "phaser";
|
|||
import {InputsController} from "#app/inputs-controller";
|
||||
import pad_xbox360 from "#app/configs/inputs/pad_xbox360";
|
||||
import {holdOn} from "#app/test/utils/gameManagerUtils";
|
||||
import {initTouchControls} from "#app/touch-controls";
|
||||
import TouchControl from "#app/touch-controls";
|
||||
import { JSDOM } from "jsdom";
|
||||
import fs from "fs";
|
||||
|
||||
|
@ -54,10 +54,8 @@ export default class InputsHandler {
|
|||
}
|
||||
|
||||
init(): void {
|
||||
setInterval(() => {
|
||||
this.inputController.update();
|
||||
});
|
||||
initTouchControls(this.inputController.events);
|
||||
const touchControl = new TouchControl(this.scene);
|
||||
touchControl.deactivatePressedKey(); //test purpose
|
||||
this.events = this.inputController.events;
|
||||
this.scene.input.gamepad.emit("connected", this.fakePad);
|
||||
this.listenInputs();
|
||||
|
|
|
@ -1,25 +1,164 @@
|
|||
import {Button} from "./enums/buttons";
|
||||
import EventEmitter = Phaser.Events.EventEmitter;
|
||||
import BattleScene from "./battle-scene";
|
||||
|
||||
// Create a map to store key bindings
|
||||
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;
|
||||
const repeatInputDelayMillis = 250;
|
||||
|
||||
/**
|
||||
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.
|
||||
*
|
||||
* @param events - The event emitter for handling input events.
|
||||
*/
|
||||
export function initTouchControls(events: EventEmitter): void {
|
||||
preventElementZoom(document.querySelector("#dpad"));
|
||||
preventElementZoom(document.querySelector("#apad"));
|
||||
init() {
|
||||
this.preventElementZoom(document.querySelector("#dpad"));
|
||||
this.preventElementZoom(document.querySelector("#apad"));
|
||||
// Select all elements with the 'data-key' attribute and bind keys to them
|
||||
for (const button of document.querySelectorAll("[data-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"]);
|
||||
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