From 59198db89c9c3c001381ea33581225f11a50d8cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:44:33 +0000 Subject: [PATCH 1/7] Initial plan From 8fbc342f4797fd0f8317d96d4d9e8efab97fc1ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:48:22 +0000 Subject: [PATCH 2/7] Add configurable maxPlayers option with tests Co-authored-by: lunarcloud <1565970+lunarcloud@users.noreply.github.com> --- src/gameinput-maxplayers.test.js | 173 +++++++++++++++++++++++++++++++ src/gameinput-options.js | 8 ++ src/gameinput-options.test.js | 7 ++ src/gameinput.js | 47 +++++---- 4 files changed, 216 insertions(+), 19 deletions(-) create mode 100644 src/gameinput-maxplayers.test.js diff --git a/src/gameinput-maxplayers.test.js b/src/gameinput-maxplayers.test.js new file mode 100644 index 0000000..956bd24 --- /dev/null +++ b/src/gameinput-maxplayers.test.js @@ -0,0 +1,173 @@ +import { describe, it, expect, beforeEach, jest } from '@jest/globals' +import { GameInput } from './gameinput.js' + +describe('GameInput MaxPlayers Configuration', () => { + beforeEach(() => { + // Mock requestAnimationFrame - don't call the callback immediately + global.requestAnimationFrame = jest.fn(() => 1) + global.cancelAnimationFrame = jest.fn() + + // Mock navigator.getGamepads + global.navigator.getGamepads = jest.fn(() => []) + + // Mock window.addEventListener and removeEventListener + global.window = { + addEventListener: jest.fn(), + removeEventListener: jest.fn() + } + }) + + describe('default configuration (4 players)', () => { + it('should create 4 players by default', () => { + const gameInput = new GameInput({ debugStatements: false }) + expect(gameInput.Players.length).toBe(4) + }) + + it('should initialize 4 player indexes correctly', () => { + const gameInput = new GameInput({ debugStatements: false }) + expect(gameInput.Players[0].number).toBe(1) + expect(gameInput.Players[0].index).toBe(0) + expect(gameInput.Players[1].number).toBe(2) + expect(gameInput.Players[1].index).toBe(1) + expect(gameInput.Players[2].number).toBe(3) + expect(gameInput.Players[2].index).toBe(2) + expect(gameInput.Players[3].number).toBe(4) + expect(gameInput.Players[3].index).toBe(3) + }) + + it('should accept valid player indexes 0-3', () => { + const gameInput = new GameInput({ debugStatements: false }) + expect(() => gameInput.getPlayer(0)).not.toThrow() + expect(() => gameInput.getPlayer(1)).not.toThrow() + expect(() => gameInput.getPlayer(2)).not.toThrow() + expect(() => gameInput.getPlayer(3)).not.toThrow() + }) + + it('should reject player index 4 and above', () => { + const gameInput = new GameInput({ debugStatements: false }) + expect(() => gameInput.getPlayer(4)).toThrow('Index out of the 0-3 range!') + }) + + it('should reject negative player indexes', () => { + const gameInput = new GameInput({ debugStatements: false }) + expect(() => gameInput.getPlayer(-1)).toThrow('Index out of the 0-3 range!') + }) + }) + + describe('custom maxPlayers configuration', () => { + it('should create 8 players when maxPlayers is 8', () => { + const gameInput = new GameInput({ debugStatements: false, maxPlayers: 8 }) + expect(gameInput.Players.length).toBe(8) + }) + + it('should initialize 8 player indexes correctly', () => { + const gameInput = new GameInput({ debugStatements: false, maxPlayers: 8 }) + for (let i = 0; i < 8; i++) { + expect(gameInput.Players[i].number).toBe(i + 1) + expect(gameInput.Players[i].index).toBe(i) + } + }) + + it('should accept valid player indexes 0-7 for 8 players', () => { + const gameInput = new GameInput({ debugStatements: false, maxPlayers: 8 }) + for (let i = 0; i < 8; i++) { + expect(() => gameInput.getPlayer(i)).not.toThrow() + } + }) + + it('should reject player index 8 and above for 8 players', () => { + const gameInput = new GameInput({ debugStatements: false, maxPlayers: 8 }) + expect(() => gameInput.getPlayer(8)).toThrow('Index out of the 0-7 range!') + }) + + it('should create 2 players when maxPlayers is 2', () => { + const gameInput = new GameInput({ debugStatements: false, maxPlayers: 2 }) + expect(gameInput.Players.length).toBe(2) + }) + + it('should accept valid player indexes 0-1 for 2 players', () => { + const gameInput = new GameInput({ debugStatements: false, maxPlayers: 2 }) + expect(() => gameInput.getPlayer(0)).not.toThrow() + expect(() => gameInput.getPlayer(1)).not.toThrow() + }) + + it('should reject player index 2 and above for 2 players', () => { + const gameInput = new GameInput({ debugStatements: false, maxPlayers: 2 }) + expect(() => gameInput.getPlayer(2)).toThrow('Index out of the 0-1 range!') + }) + + it('should create 16 players when maxPlayers is 16', () => { + const gameInput = new GameInput({ debugStatements: false, maxPlayers: 16 }) + expect(gameInput.Players.length).toBe(16) + }) + + it('should accept valid player indexes 0-15 for 16 players', () => { + const gameInput = new GameInput({ debugStatements: false, maxPlayers: 16 }) + for (let i = 0; i < 16; i++) { + expect(() => gameInput.getPlayer(i)).not.toThrow() + } + }) + }) + + describe('getGamepad bounds checking', () => { + it('should accept valid player indexes for getGamepad with default 4 players', () => { + const gameInput = new GameInput({ debugStatements: false }) + expect(() => gameInput.getGamepad(0)).not.toThrow() + expect(() => gameInput.getGamepad(1)).not.toThrow() + expect(() => gameInput.getGamepad(2)).not.toThrow() + expect(() => gameInput.getGamepad(3)).not.toThrow() + }) + + it('should reject invalid player indexes for getGamepad with default 4 players', () => { + const gameInput = new GameInput({ debugStatements: false }) + expect(() => gameInput.getGamepad(4)).toThrow('Index out of the 0-3 range!') + expect(() => gameInput.getGamepad(-1)).toThrow('Index out of the 0-3 range!') + }) + + it('should accept valid player indexes for getGamepad with 8 players', () => { + const gameInput = new GameInput({ debugStatements: false, maxPlayers: 8 }) + for (let i = 0; i < 8; i++) { + expect(() => gameInput.getGamepad(i)).not.toThrow() + } + }) + + it('should reject invalid player indexes for getGamepad with 8 players', () => { + const gameInput = new GameInput({ debugStatements: false, maxPlayers: 8 }) + expect(() => gameInput.getGamepad(8)).toThrow('Index out of the 0-7 range!') + expect(() => gameInput.getGamepad(-1)).toThrow('Index out of the 0-7 range!') + }) + }) + + describe('destroy method with maxPlayers', () => { + it('should clear gamepads array for default 4 players', () => { + const gameInput = new GameInput({ debugStatements: false }) + gameInput.destroy() + expect(gameInput.Connection.Gamepads.length).toBe(4) + expect(gameInput.Connection.Gamepads.every(g => g === undefined)).toBe(true) + }) + + it('should clear gamepads array for 8 players', () => { + const gameInput = new GameInput({ debugStatements: false, maxPlayers: 8 }) + gameInput.destroy() + expect(gameInput.Connection.Gamepads.length).toBe(8) + expect(gameInput.Connection.Gamepads.every(g => g === undefined)).toBe(true) + }) + }) + + describe('GamePadMapping initialization', () => { + it('should initialize GamePadMapping for default 4 players', () => { + const gameInput = new GameInput({ debugStatements: false }) + expect(gameInput.Connection.GamePadMapping[0]).toBe(0) + expect(gameInput.Connection.GamePadMapping[1]).toBe(1) + expect(gameInput.Connection.GamePadMapping[2]).toBe(2) + expect(gameInput.Connection.GamePadMapping[3]).toBe(3) + }) + + it('should initialize GamePadMapping for 8 players', () => { + const gameInput = new GameInput({ debugStatements: false, maxPlayers: 8 }) + for (let i = 0; i < 8; i++) { + expect(gameInput.Connection.GamePadMapping[i]).toBe(i) + } + }) + }) +}) diff --git a/src/gameinput-options.js b/src/gameinput-options.js index 186f89d..1644d33 100644 --- a/src/gameinput-options.js +++ b/src/gameinput-options.js @@ -7,6 +7,14 @@ class GameInputOptions { * @type {boolean} */ debugStatements = false + + /** + * Maximum number of players/gamepads to support. + * Defaults to 4 for backward compatibility. + * Modern browsers can support more via navigator.getGamepads(). + * @type {number} + */ + maxPlayers = 4 } export { GameInputOptions } diff --git a/src/gameinput-options.test.js b/src/gameinput-options.test.js index 85a1c9a..6d47427 100644 --- a/src/gameinput-options.test.js +++ b/src/gameinput-options.test.js @@ -6,6 +6,7 @@ describe('GameInputOptions', () => { it('should create options with default values', () => { const options = new GameInputOptions() expect(options.debugStatements).toBe(false) + expect(options.maxPlayers).toBe(4) }) it('should allow setting debugStatements', () => { @@ -13,5 +14,11 @@ describe('GameInputOptions', () => { options.debugStatements = true expect(options.debugStatements).toBe(true) }) + + it('should allow setting maxPlayers', () => { + const options = new GameInputOptions() + options.maxPlayers = 8 + expect(options.maxPlayers).toBe(8) + }) }) }) diff --git a/src/gameinput.js b/src/gameinput.js index d6060f9..2f826fe 100755 --- a/src/gameinput.js +++ b/src/gameinput.js @@ -70,6 +70,12 @@ class GameInput { Specific: GameInputModels // imported } + /** + * Maximum number of players/gamepads to support. + * @type {number} + */ + #maxPlayers = 4 + /** * Get just ASCII text. * @param {string} text input text @@ -91,12 +97,7 @@ class GameInput { * The players. * @type {Array} */ - Players = [ - new GameInputPlayer(this, 1), - new GameInputPlayer(this, 2), - new GameInputPlayer(this, 3), - new GameInputPlayer(this, 4) - ] + Players = [] /** * Connection info @@ -105,17 +106,12 @@ class GameInput { /** * Mapping of player to gamepad index */ - GamePadMapping: { - 0: 0, - 1: 1, - 2: 2, - 3: 3 - }, + GamePadMapping: {}, /** * Cache of actual gamepads. * @type {Array} */ - Gamepads: [undefined, undefined, undefined, undefined] + Gamepads: [] } /** @@ -158,7 +154,7 @@ class GameInput { * Whether we've received the first button press. * @type {Array} */ - #firstPress = [undefined, undefined, undefined, undefined] + #firstPress = [] /** * Callback providing player index and button name. @@ -194,8 +190,21 @@ class GameInput { * @param {GameInputOptions} options constructor options */ constructor (options = undefined) { + this.#maxPlayers = options?.maxPlayers ?? 4 this.debug = options?.debugStatements || false + // Initialize dynamic arrays based on maxPlayers + this.Players = Array.from( + { length: this.#maxPlayers }, + (_, i) => new GameInputPlayer(this, i + 1) + ) + this.#firstPress = Array(this.#maxPlayers).fill(false) + this.Connection.Gamepads = Array(this.#maxPlayers).fill(undefined) + // Initialize GamePadMapping + for (let i = 0; i < this.#maxPlayers; i++) { + this.Connection.GamePadMapping[i] = i + } + this.startUpdateLoop() // Start watching for gamepads joining and leaving @@ -304,8 +313,8 @@ class GameInput { * @returns {GameInputPlayer} Player */ getPlayer (index) { - if (index < 0 || index > 3) - throw new Error('Index out of the 0-3 range!') + if (index < 0 || index >= this.#maxPlayers) + throw new Error(`Index out of the 0-${this.#maxPlayers - 1} range!`) return this.Players[this.Connection.GamePadMapping[index]] } @@ -315,8 +324,8 @@ class GameInput { * @returns {Gamepad} Player's gamepad */ getGamepad (player) { - if (player < 0 || player > 3) - throw new Error('Index out of the 0-3 range!') + if (player < 0 || player >= this.#maxPlayers) + throw new Error(`Index out of the 0-${this.#maxPlayers - 1} range!`) return this.Connection.Gamepads[this.Connection.GamePadMapping[player]] } @@ -376,7 +385,7 @@ class GameInput { } // Clear connection references - this.Connection.Gamepads = [undefined, undefined, undefined, undefined] + this.Connection.Gamepads = Array(this.#maxPlayers).fill(undefined) } /** From eda73b33e1cd5393ed495ebea19842975eefaa0e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:49:05 +0000 Subject: [PATCH 3/7] Document maxPlayers configuration option in README Co-authored-by: lunarcloud <1565970+lunarcloud@users.noreply.github.com> --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index 7a66cbe..2af9726 100755 --- a/README.md +++ b/README.md @@ -43,6 +43,39 @@ import { GameInputSchemaSectionNames, GameInputSchemaButtonNames } from './lib/g const gameInput = new GameInput() ``` +### Configuration Options + +You can configure GameInput by passing options to the constructor: + +```js +const gameInput = new GameInput({ + debugStatements: false, // Enable debug console logs (default: false) + maxPlayers: 8 // Maximum number of players/gamepads (default: 4) +}) +``` + +#### maxPlayers + +By default, GameInput supports up to 4 players/gamepads. Modern browsers can support more gamepads via `navigator.getGamepads()`, and you can increase this limit using the `maxPlayers` option. + +**Use cases:** +- Party games with 8+ players +- Arcade cabinets with multiple controllers +- Esports setups with many simultaneous gamepads +- Testing/development with multiple virtual gamepads + +**Example:** +```js +// Support up to 8 players +const gameInput = new GameInput({ maxPlayers: 8 }) + +// All 8 players are accessible +for (let i = 0; i < 8; i++) { + const player = gameInput.getPlayer(i) + console.log(`Player ${i + 1}:`, player) +} +``` + ### Event-Driven Style ```js gameInput From 1059af862e618b90081111acc89eb3334cac8e4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:54:58 +0000 Subject: [PATCH 4/7] Implement auto-detection and dynamic expansion of player slots Co-authored-by: lunarcloud <1565970+lunarcloud@users.noreply.github.com> --- README.md | 32 +++++--- src/gameinput-maxplayers.test.js | 126 ++++++++++++++++++++++++++----- src/gameinput.js | 60 +++++++++++---- 3 files changed, 174 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 2af9726..e7b3b81 100755 --- a/README.md +++ b/README.md @@ -50,29 +50,37 @@ You can configure GameInput by passing options to the constructor: ```js const gameInput = new GameInput({ debugStatements: false, // Enable debug console logs (default: false) - maxPlayers: 8 // Maximum number of players/gamepads (default: 4) + maxPlayers: 8 // Minimum number of player slots (default: 4, auto-expands as needed) }) ``` #### maxPlayers -By default, GameInput supports up to 4 players/gamepads. Modern browsers can support more gamepads via `navigator.getGamepads()`, and you can increase this limit using the `maxPlayers` option. +GameInput **automatically detects and expands** to support the number of gamepads your browser can handle via `navigator.getGamepads()`. The `maxPlayers` option sets the **minimum** number of player slots to allocate initially (default: 4). -**Use cases:** -- Party games with 8+ players -- Arcade cabinets with multiple controllers -- Esports setups with many simultaneous gamepads -- Testing/development with multiple virtual gamepads +**How it works:** +- On initialization, GameInput checks `navigator.getGamepads().length` and uses the larger of that value or `maxPlayers` (minimum 4) +- When new gamepads connect, the system **automatically expands** to accommodate them +- No manual configuration needed for most use cases - just plug in your gamepads! + +**Use cases for setting maxPlayers:** +- Pre-allocate slots for expected players to avoid reallocation +- Set a higher minimum for party games, arcade cabinets, or esports setups +- Testing/development where you know you'll have multiple gamepads **Example:** ```js -// Support up to 8 players +// Auto-detect (recommended) - automatically expands as gamepads connect +const gameInput = new GameInput() + +// Or pre-allocate for 8+ player games const gameInput = new GameInput({ maxPlayers: 8 }) -// All 8 players are accessible -for (let i = 0; i < 8; i++) { - const player = gameInput.getPlayer(i) - console.log(`Player ${i + 1}:`, player) +// Access all detected players +for (const player of gameInput.Players) { + if (player.model) { + console.log(`Player ${player.number} connected`) + } } ``` diff --git a/src/gameinput-maxplayers.test.js b/src/gameinput-maxplayers.test.js index 956bd24..5a6e8bb 100644 --- a/src/gameinput-maxplayers.test.js +++ b/src/gameinput-maxplayers.test.js @@ -7,9 +7,6 @@ describe('GameInput MaxPlayers Configuration', () => { global.requestAnimationFrame = jest.fn(() => 1) global.cancelAnimationFrame = jest.fn() - // Mock navigator.getGamepads - global.navigator.getGamepads = jest.fn(() => []) - // Mock window.addEventListener and removeEventListener global.window = { addEventListener: jest.fn(), @@ -17,7 +14,46 @@ describe('GameInput MaxPlayers Configuration', () => { } }) + describe('auto-detection from navigator.getGamepads()', () => { + it('should detect and use browser gamepad array length when larger than default', () => { + // Mock navigator.getGamepads with 8 slots + global.navigator.getGamepads = jest.fn(() => new Array(8)) + + const gameInput = new GameInput({ debugStatements: false }) + expect(gameInput.Players.length).toBe(8) + }) + + it('should use minimum of 4 when browser reports 0 slots', () => { + // Mock navigator.getGamepads with empty array + global.navigator.getGamepads = jest.fn(() => []) + + const gameInput = new GameInput({ debugStatements: false }) + expect(gameInput.Players.length).toBe(4) + }) + + it('should use maxPlayers option when larger than detected', () => { + // Mock navigator.getGamepads with 4 slots + global.navigator.getGamepads = jest.fn(() => new Array(4)) + + const gameInput = new GameInput({ debugStatements: false, maxPlayers: 8 }) + expect(gameInput.Players.length).toBe(8) + }) + + it('should use detected length when larger than maxPlayers option', () => { + // Mock navigator.getGamepads with 12 slots + global.navigator.getGamepads = jest.fn(() => new Array(12)) + + const gameInput = new GameInput({ debugStatements: false, maxPlayers: 8 }) + expect(gameInput.Players.length).toBe(12) + }) + }) + describe('default configuration (4 players)', () => { + beforeEach(() => { + // Mock navigator.getGamepads with empty array for default tests + global.navigator.getGamepads = jest.fn(() => []) + }) + it('should create 4 players by default', () => { const gameInput = new GameInput({ debugStatements: false }) expect(gameInput.Players.length).toBe(4) @@ -55,6 +91,11 @@ describe('GameInput MaxPlayers Configuration', () => { }) describe('custom maxPlayers configuration', () => { + beforeEach(() => { + // Reset mock for these tests + global.navigator.getGamepads = jest.fn(() => []) + }) + it('should create 8 players when maxPlayers is 8', () => { const gameInput = new GameInput({ debugStatements: false, maxPlayers: 8 }) expect(gameInput.Players.length).toBe(8) @@ -80,22 +121,6 @@ describe('GameInput MaxPlayers Configuration', () => { expect(() => gameInput.getPlayer(8)).toThrow('Index out of the 0-7 range!') }) - it('should create 2 players when maxPlayers is 2', () => { - const gameInput = new GameInput({ debugStatements: false, maxPlayers: 2 }) - expect(gameInput.Players.length).toBe(2) - }) - - it('should accept valid player indexes 0-1 for 2 players', () => { - const gameInput = new GameInput({ debugStatements: false, maxPlayers: 2 }) - expect(() => gameInput.getPlayer(0)).not.toThrow() - expect(() => gameInput.getPlayer(1)).not.toThrow() - }) - - it('should reject player index 2 and above for 2 players', () => { - const gameInput = new GameInput({ debugStatements: false, maxPlayers: 2 }) - expect(() => gameInput.getPlayer(2)).toThrow('Index out of the 0-1 range!') - }) - it('should create 16 players when maxPlayers is 16', () => { const gameInput = new GameInput({ debugStatements: false, maxPlayers: 16 }) expect(gameInput.Players.length).toBe(16) @@ -110,6 +135,11 @@ describe('GameInput MaxPlayers Configuration', () => { }) describe('getGamepad bounds checking', () => { + beforeEach(() => { + // Reset mock for these tests + global.navigator.getGamepads = jest.fn(() => []) + }) + it('should accept valid player indexes for getGamepad with default 4 players', () => { const gameInput = new GameInput({ debugStatements: false }) expect(() => gameInput.getGamepad(0)).not.toThrow() @@ -139,6 +169,11 @@ describe('GameInput MaxPlayers Configuration', () => { }) describe('destroy method with maxPlayers', () => { + beforeEach(() => { + // Reset mock for these tests + global.navigator.getGamepads = jest.fn(() => []) + }) + it('should clear gamepads array for default 4 players', () => { const gameInput = new GameInput({ debugStatements: false }) gameInput.destroy() @@ -155,6 +190,11 @@ describe('GameInput MaxPlayers Configuration', () => { }) describe('GamePadMapping initialization', () => { + beforeEach(() => { + // Reset mock for these tests + global.navigator.getGamepads = jest.fn(() => []) + }) + it('should initialize GamePadMapping for default 4 players', () => { const gameInput = new GameInput({ debugStatements: false }) expect(gameInput.Connection.GamePadMapping[0]).toBe(0) @@ -170,4 +210,52 @@ describe('GameInput MaxPlayers Configuration', () => { } }) }) + + describe('dynamic expansion on reinitialize', () => { + it('should expand player arrays when more gamepads detected', () => { + // Start with 4 slots + global.navigator.getGamepads = jest.fn(() => new Array(4)) + const gameInput = new GameInput({ debugStatements: false }) + expect(gameInput.Players.length).toBe(4) + + // Simulate 8 gamepads connecting + global.navigator.getGamepads = jest.fn(() => new Array(8)) + gameInput.reinitialize() + + expect(gameInput.Players.length).toBe(8) + expect(gameInput.Connection.Gamepads.length).toBe(8) + }) + + it('should allow access to newly expanded player slots', () => { + // Start with 4 slots + global.navigator.getGamepads = jest.fn(() => new Array(4)) + const gameInput = new GameInput({ debugStatements: false }) + + // Simulate 10 gamepads connecting + global.navigator.getGamepads = jest.fn(() => new Array(10)) + gameInput.reinitialize() + + // Should now accept indexes 0-9 + for (let i = 0; i < 10; i++) { + expect(() => gameInput.getPlayer(i)).not.toThrow() + } + + // Should reject index 10 + expect(() => gameInput.getPlayer(10)).toThrow('Index out of the 0-9 range!') + }) + + it('should preserve existing player data when expanding', () => { + // Start with 4 slots + global.navigator.getGamepads = jest.fn(() => new Array(4)) + const gameInput = new GameInput({ debugStatements: false }) + const firstPlayer = gameInput.Players[0] + + // Simulate 8 gamepads connecting + global.navigator.getGamepads = jest.fn(() => new Array(8)) + gameInput.reinitialize() + + // First player should be the same object + expect(gameInput.Players[0]).toBe(firstPlayer) + }) + }) }) diff --git a/src/gameinput.js b/src/gameinput.js index 2f826fe..734a029 100755 --- a/src/gameinput.js +++ b/src/gameinput.js @@ -190,20 +190,14 @@ class GameInput { * @param {GameInputOptions} options constructor options */ constructor (options = undefined) { - this.#maxPlayers = options?.maxPlayers ?? 4 this.debug = options?.debugStatements || false + // Auto-detect max players from browser's gamepad API, with fallback to configured value or 4 + const detectedMax = GameInput.canUseGamepadAPI() ? navigator.getGamepads().length : 0 + this.#maxPlayers = Math.max(options?.maxPlayers ?? 4, detectedMax, 4) + // Initialize dynamic arrays based on maxPlayers - this.Players = Array.from( - { length: this.#maxPlayers }, - (_, i) => new GameInputPlayer(this, i + 1) - ) - this.#firstPress = Array(this.#maxPlayers).fill(false) - this.Connection.Gamepads = Array(this.#maxPlayers).fill(undefined) - // Initialize GamePadMapping - for (let i = 0; i < this.#maxPlayers; i++) { - this.Connection.GamePadMapping[i] = i - } + this.#initializePlayerArrays(this.#maxPlayers) this.startUpdateLoop() @@ -234,6 +228,37 @@ class GameInput { } } + /** + * Initialize or expand player arrays to support a given number of players. + * @param {number} count - Number of players to support + * @private + */ + #initializePlayerArrays (count) { + const currentLength = this.Players.length + + if (count > currentLength) { + // Expand arrays to support more players + for (let i = currentLength; i < count; i++) { + this.Players.push(new GameInputPlayer(this, i + 1)) + this.#firstPress.push(false) + this.Connection.Gamepads.push(undefined) + this.Connection.GamePadMapping[i] = i + } + this.#maxPlayers = count + } else if (currentLength === 0) { + // Initial setup + this.Players = Array.from( + { length: count }, + (_, i) => new GameInputPlayer(this, i + 1) + ) + this.#firstPress = Array(count).fill(false) + this.Connection.Gamepads = Array(count).fill(undefined) + for (let i = 0; i < count; i++) { + this.Connection.GamePadMapping[i] = i + } + } + } + /** * Add action to "reinitialized" events. * @param {Function} action Action to add. @@ -498,13 +523,22 @@ class GameInput { * Setup gamepads info. */ reinitialize () { + if (GameInput.canUseGamepadAPI()) { + const gamepads = navigator.getGamepads() + + // Dynamically expand player arrays if more gamepads are detected + if (gamepads.length > this.#maxPlayers) { + this.#initializePlayerArrays(gamepads.length) + } + + this.Connection.Gamepads = gamepads + } + // clear gamepad information for (const player of this.Players) player.setModel(undefined) if (GameInput.canUseGamepadAPI()) { - this.Connection.Gamepads = navigator.getGamepads() - for (let i = 0; i < this.Players.length; i++) if (this.Connection.Gamepads.filter(Boolean).length === 0) this.#firstPress[i] = false From 837cc11667d7078f5c8ceced680bcfe2b07febec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 00:35:03 +0000 Subject: [PATCH 5/7] Simplify README documentation for maxPlayers option Co-authored-by: lunarcloud <1565970+lunarcloud@users.noreply.github.com> --- README.md | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e7b3b81..dffbc3a 100755 --- a/README.md +++ b/README.md @@ -50,31 +50,21 @@ You can configure GameInput by passing options to the constructor: ```js const gameInput = new GameInput({ debugStatements: false, // Enable debug console logs (default: false) - maxPlayers: 8 // Minimum number of player slots (default: 4, auto-expands as needed) + maxPlayers: 8 // Force max players (normally auto-detected, 4-8 on supported browsers) }) ``` #### maxPlayers -GameInput **automatically detects and expands** to support the number of gamepads your browser can handle via `navigator.getGamepads()`. The `maxPlayers` option sets the **minimum** number of player slots to allocate initially (default: 4). - -**How it works:** -- On initialization, GameInput checks `navigator.getGamepads().length` and uses the larger of that value or `maxPlayers` (minimum 4) -- When new gamepads connect, the system **automatically expands** to accommodate them -- No manual configuration needed for most use cases - just plug in your gamepads! - -**Use cases for setting maxPlayers:** -- Pre-allocate slots for expected players to avoid reallocation -- Set a higher minimum for party games, arcade cabinets, or esports setups -- Testing/development where you know you'll have multiple gamepads +GameInput supports at least 4 players, up to 8 when detected on supported browsers. The `maxPlayers` option forces a specific maximum, overriding auto-detection. **Example:** ```js -// Auto-detect (recommended) - automatically expands as gamepads connect +// Auto-detect (recommended) const gameInput = new GameInput() -// Or pre-allocate for 8+ player games -const gameInput = new GameInput({ maxPlayers: 8 }) +// Force a specific maximum +const gameInput = new GameInput({ maxPlayers: 16 }) // Access all detected players for (const player of gameInput.Players) { From bbe8ffbfdcd5f495aa3096321ba15326102243ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 00:39:02 +0000 Subject: [PATCH 6/7] Fix TypeScript error: remove @private tag from private identifier Co-authored-by: lunarcloud <1565970+lunarcloud@users.noreply.github.com> --- src/gameinput.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gameinput.js b/src/gameinput.js index 734a029..25f3289 100755 --- a/src/gameinput.js +++ b/src/gameinput.js @@ -231,7 +231,6 @@ class GameInput { /** * Initialize or expand player arrays to support a given number of players. * @param {number} count - Number of players to support - * @private */ #initializePlayerArrays (count) { const currentLength = this.Players.length From 019d75e57d3d76870005ea8f9788739f2d8d5ddc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 01:08:50 +0000 Subject: [PATCH 7/7] Simplify JSDoc types with typedef to fix build errors Co-authored-by: lunarcloud <1565970+lunarcloud@users.noreply.github.com> --- src/gameinput.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/gameinput.js b/src/gameinput.js index 25f3289..4746014 100755 --- a/src/gameinput.js +++ b/src/gameinput.js @@ -140,13 +140,13 @@ class GameInput { /** * Gamepad connected event handler. - * @type {((this: Window, ev: GamepadEvent) => any)|undefined} + * @type {GamepadEventHandler|undefined} */ #gamepadConnectedHandler = undefined /** * Gamepad disconnected event handler. - * @type {((this: Window, ev: GamepadEvent) => any)|undefined} + * @type {GamepadEventHandler|undefined} */ #gamepadDisconnectedHandler = undefined @@ -156,6 +156,11 @@ class GameInput { */ #firstPress = [] + /** + * Gamepad event handler function type. + * @typedef {function(GamepadEvent):any} GamepadEventHandler + */ + /** * Callback providing player index and button name. * @typedef {function(number, import('./gameinput-schema.js').GameInputSchemaSectionName, string):void} ButtonActionFunc