From d6c09eaefda10dae084897c127d918113db3ff2b Mon Sep 17 00:00:00 2001 From: Nimgoble Date: Sat, 25 Apr 2026 20:25:20 -0600 Subject: [PATCH] feat: [#3728] Added ButtonDown and ButtonUp events (#3729) Closes #3728 ## Changes: -Added buttondown and buttonup event to gamepad -Added unit tests for new events. -Fixed a bug(?) in mock.ts where the buttons array wasn't initializing correctly. --- src/engine/input/gamepad.ts | 16 +++++-- src/spec/__util__/mocks.ts | 18 +++---- src/spec/vitest/gamepad-spec.ts | 83 +++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 17 deletions(-) diff --git a/src/engine/input/gamepad.ts b/src/engine/input/gamepad.ts index 11206bea..3a291cc0 100644 --- a/src/engine/input/gamepad.ts +++ b/src/engine/input/gamepad.ts @@ -13,6 +13,8 @@ export const GamepadEvents = { GamepadConnect: 'connect', GamepadDisconnect: 'disconnect', GamepadButton: 'button', + GamepadButtonDown: 'buttondown', + GamepadButtonUp: 'buttonup', GamepadAxis: 'axis' }; @@ -189,18 +191,22 @@ export class Gamepads { const button = gamepad.buttons[buttonIndex]; const value = button?.value; if (value !== this._oldPads[i]?.getButton(buttonIndex)) { + const exGamepad = this.at(i); + const buttonEnum = buttonIndex in Buttons ? (buttonIndex as Buttons) : Buttons.Unknown; if (button?.pressed) { - this.at(i).updateButton(buttonIndex, value); + exGamepad.updateButton(buttonIndex, value); // Fallback to unknown if not mapped // prettier-ignore - this.at(i).events.emit('button', new GamepadButtonEvent( - buttonIndex in Buttons ? buttonIndex as Buttons : Buttons.Unknown, + exGamepad.events.emit(GamepadEvents.GamepadButton, new GamepadButtonEvent( + buttonEnum, buttonIndex, value, - this.at(i)) + exGamepad) ); + exGamepad.events.emit(GamepadEvents.GamepadButtonDown, new GamepadButtonEvent(buttonEnum, buttonIndex, value, exGamepad)); } else { - this.at(i).updateButton(buttonIndex, 0); + exGamepad.updateButton(buttonIndex, 0); + exGamepad.events.emit(GamepadEvents.GamepadButtonUp, new GamepadButtonEvent(buttonEnum, buttonIndex, 0, exGamepad)); } } } diff --git a/src/spec/__util__/mocks.ts b/src/spec/__util__/mocks.ts index 6cfb2dbb..3063be36 100644 --- a/src/spec/__util__/mocks.ts +++ b/src/spec/__util__/mocks.ts @@ -39,18 +39,12 @@ export namespace Mocks { const mockNavigator = { setGamepads: function (index: number, numAxis: number, numButtons: number) { _internalGamePads[index] = { - axes: Array.apply( - null, - Array(numAxis).map(function () { - return undefined; - }) - ), - buttons: Array.apply( - null, - Array(numButtons).map(function () { - return { pressed: false, value: 0 }; - }) - ), + axes: Array.from({ length: numAxis }).map(function () { + return undefined; + }), + buttons: Array.from({ length: numButtons }).map(function () { + return { pressed: false, value: 0 }; + }), connected: true, index: index, id: 'Mock Gamepad', diff --git a/src/spec/vitest/gamepad-spec.ts b/src/spec/vitest/gamepad-spec.ts index f769d227..b8d0e587 100644 --- a/src/spec/vitest/gamepad-spec.ts +++ b/src/spec/vitest/gamepad-spec.ts @@ -116,25 +116,108 @@ describe('A gamepad', () => { let currentButton = null; let currentValue = null; + let currentButtonDownButton = null; + let currentButtonDownValue = null; + + let currentButtonUpButton = null; + let currentButtonUpValue = null; + gamepad.on('button', (buttonEvent: ex.GamepadButtonEvent) => { currentButton = buttonEvent.button; currentValue = buttonEvent.value; }); + gamepad.on('buttondown', (buttonEvent: ex.GamepadButtonEvent) => { + currentButtonDownButton = buttonEvent.button; + currentButtonDownValue = buttonEvent.value; + }); + + gamepad.on('buttonup', (buttonEvent: ex.GamepadButtonEvent) => { + currentButtonUpButton = buttonEvent.button; + currentButtonUpValue = buttonEvent.value; + }); + expect(currentButton).toBeNull(); expect(currentValue).toBeNull(); + expect(currentButtonDownButton).toBeNull(); + expect(currentButtonDownValue).toBeNull(); + + expect(currentButtonUpButton).toBeNull(); + expect(currentButtonUpValue).toBeNull(); + for (const button in ex.Buttons) { if (typeof button === 'number') { engine.input.gamepads.update(); + //press button nav.setGamepadButton(0, button, 1.0); engine.input.gamepads.update(); expect(currentButton).toBe(button); expect(currentValue).toBe(1.0); + expect(currentButtonDownButton).toBe(button); + expect(currentButtonDownValue).toBe(1.0); + // release button + nav.setGamepadButton(0, button, 0); + engine.input.gamepads.update(); + expect(currentButtonUpButton).toBe(button); + expect(currentButtonUpValue).toBe(0); } } }); + it('should fire button events the correct amount of times', () => { + nav.setGamepads(0, 4, 16); // valid 4 axis > 2 and 16 buttons > 4 + + const gamepad = engine.input.gamepads.at(0); + + let currentButtonCounter = 0; + let currentButtonDownCounter = 0; + let currentButtonUpCounter = 0; + + gamepad.on('button', (buttonEvent: ex.GamepadButtonEvent) => { + currentButtonCounter++; + }); + + gamepad.on('buttondown', (buttonEvent: ex.GamepadButtonEvent) => { + currentButtonDownCounter++; + }); + + gamepad.on('buttonup', (buttonEvent: ex.GamepadButtonEvent) => { + currentButtonUpCounter++; + }); + + expect(currentButtonCounter).toBe(0); + expect(currentButtonDownCounter).toBe(0); + expect(currentButtonUpCounter).toBe(0); + + const button = ex.Buttons.Face1; + engine.input.gamepads.update(); + //press button + nav.setGamepadButton(0, button, 1.0); + engine.input.gamepads.update(); + expect(currentButtonCounter).toBe(1); + expect(currentButtonDownCounter).toBe(1); + expect(gamepad.wasButtonPressed(button)).toBe(true); + expect(gamepad.wasButtonReleased(button)).toBe(false); + expect(currentButtonUpCounter).toBe(0); + //change button pressure + nav.setGamepadButton(0, button, 0.5); + engine.input.gamepads.update(); + expect(currentButtonCounter).toBe(2); + expect(currentButtonDownCounter).toBe(2); + expect(gamepad.wasButtonPressed(button, 0)).toBe(true); + expect(gamepad.wasButtonReleased(button)).toBe(false); + expect(currentButtonUpCounter).toBe(0); + // release button + nav.setGamepadButton(0, button, 0); + engine.input.gamepads.update(); + expect(currentButtonCounter).toBe(2); + expect(currentButtonDownCounter).toBe(2); + expect(gamepad.wasButtonPressed(button)).toBe(false); + expect(gamepad.wasButtonReleased(button)).toBe(true); + expect(currentButtonUpCounter).toBe(1); + }); + it('should fire events on all predefined axis', () => { nav.setGamepads(0, 4, 16); // valid 4 axis > 2 and 16 buttons > 4 -- 2.51.2