@strudel/gamepad/index.mjs
3import { signal } from '@strudel/core';
Button mapping for Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)
31class ButtonSequenceDetector { 32 constructor(timeWindow = 1000) { 33 this.sequence = []; 34 this.timeWindow = timeWindow; 35 this.lastInputTime = 0; 36 this.buttonStates = Array(16).fill(0); // Track previous state of each button 37 // Button mapping for character inputs 38 } 39 40 addInput(buttonIndex, buttonValue) { 41 const currentTime = Date.now();
Only add input on button press (rising edge)
Store the button name instead of index
51 const buttonName = Object.keys(buttonMap).find((key) => buttonMap[key] === buttonIndex) || buttonIndex.toString();
console.log(this.sequence); Keep only inputs within the time window
Convert string input to array if needed
console.log(this.sequence);
Get the last n inputs where n is the target sequence length
86 const lastInputs = this.sequence.slice(-targetSequence.length).map((entry) => entry.input);
Compare sequences
89 return lastInputs.every((input, index) => { 90 const target = sequence[index]; 91 // Check if either the input matches directly or they refer to the same button in the map 92 return ( 93 input === target || 94 buttonMap[input] === buttonMap[target] || 95 // Also check if the numerical index matches 96 buttonMap[input] === parseInt(target) 97 ); 98 }) 99 ? 1 100 : 0; 101 } 102}
104class GamepadHandler { 105 constructor(index = 0) { 106 // Add index parameter 107 this._gamepads = {}; 108 this._activeGamepad = index; // Use provided index 109 this._axes = [0, 0, 0, 0]; 110 this._buttons = Array(16).fill(0); 111 this.setupEventListeners(); 112 } 113 114 setupEventListeners() { 115 window.addEventListener('gamepadconnected', (e) => { 116 this._gamepads[e.gamepad.index] = e.gamepad; 117 if (!this._activeGamepad) { 118 this._activeGamepad = e.gamepad.index; 119 } 120 }); 121 122 window.addEventListener('gamepaddisconnected', (e) => { 123 delete this._gamepads[e.gamepad.index]; 124 if (this._activeGamepad === e.gamepad.index) { 125 this._activeGamepad = Object.keys(this._gamepads)[0] || null; 126 } 127 }); 128 } 129 130 poll() { 131 if (this._activeGamepad !== null) { 132 const gamepad = navigator.getGamepads()[this._activeGamepad]; 133 if (gamepad) { 134 // Update axes (normalized to 0-1 range) 135 this._axes = gamepad.axes.map((axis) => (axis + 1) / 2); 136 // Update buttons 137 this._buttons = gamepad.buttons.map((button) => button.value); 138 } 139 } 140 } 141 142 getAxes() { 143 return this._axes; 144 } 145 getButtons() { 146 return this._buttons; 147 } 148}
Module-level state store for toggle states
151const gamepadStates = new Map();
Base signal that polls gamepad state and handles sequence detection
Add all button inputs to sequence detector
Create axes patterns
Add bipolar versions
Create button patterns
Initialize toggle state if it doesn't exist
Direct button value pattern (no longer needs to call addInput)
201 const btn = baseSignal.fmap((state) => state.buttons[i]);
Button toggle pattern with persistent state
204 const toggle = baseSignal.fmap((state) => { 205 const currentState = state.buttons[i]; 206 const buttonState = gamepadStates.get(stateKey); 207 208 if (currentState === 1 && buttonState.lastButtonState === 0) { 209 // Toggle the state on rising edge 210 buttonState.toggleState = buttonState.toggleState === 0 ? 1 : 0; 211 } 212 213 buttonState.lastButtonState = currentState; 214 return buttonState.toggleState; 215 });
Create sequence checker pattern
Return an object with all controls
229 return { 230 ...axes, 231 buttons, 232 ...Object.fromEntries( 233 Object.entries(buttonMap).flatMap(([key, index]) => [ 234 [key.toLowerCase(), buttons[index].value], 235 [key.toUpperCase(), buttons[index].value], 236 [`tgl${key.toLowerCase()}`, buttons[index].toggle], 237 [`tgl${key.toUpperCase()}`, buttons[index].toggle], 238 ]), 239 ), 240 checkSequence, 241 btnSequence, 242 btnSeq, 243 btnseq, 244 raw: baseSignal, 245 }; 246};