1// @strudel/gamepad/index.mjs 2 3import { signal } from '@strudel/core'; 4 5// Button mapping for Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216) 6export const buttonMap = { 7 a: 0, 8 b: 1, 9 x: 2, 10 y: 3, 11 lb: 4, 12 rb: 5, 13 lt: 6, 14 rt: 7, 15 back: 8, 16 start: 9, 17 l3: 10, 18 ls: 10, 19 r3: 11, 20 rs: 11, 21 u: 12, 22 up: 12, 23 d: 13, 24 down: 13, 25 l: 14, 26 left: 14, 27 r: 15, 28 right: 15, 29}; 30 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(); 42 43 // Only add input on button press (rising edge) 44 if (buttonValue === 1 && this.buttonStates[buttonIndex] === 0) { 45 // Clear sequence if too much time has passed 46 if (currentTime - this.lastInputTime > this.timeWindow) { 47 this.sequence = []; 48 } 49 50 // Store the button name instead of index 51 const buttonName = Object.keys(buttonMap).find((key) => buttonMap[key] === buttonIndex) || buttonIndex.toString(); 52 53 this.sequence.push({ 54 input: buttonName, 55 timestamp: currentTime, 56 }); 57 58 this.lastInputTime = currentTime; 59 60 //console.log(this.sequence); 61 // Keep only inputs within the time window 62 this.sequence = this.sequence.filter((entry) => currentTime - entry.timestamp <= this.timeWindow); 63 } 64 65 // Update button state 66 this.buttonStates[buttonIndex] = buttonValue; 67 } 68 69 checkSequence(targetSequence) { 70 if (!Array.isArray(targetSequence) && typeof targetSequence !== 'string') { 71 console.error('ButtonSequenceDetector: targetSequence must be an array or string'); 72 return 0; 73 } 74 75 if (this.sequence.length < targetSequence.length) return 0; 76 77 // Convert string input to array if needed 78 const sequence = 79 typeof targetSequence === 'string' 80 ? targetSequence.toLowerCase().split('') 81 : targetSequence.map((s) => s.toString().toLowerCase()); 82 83 //console.log(this.sequence); 84 85 // Get the last n inputs where n is the target sequence length 86 const lastInputs = this.sequence.slice(-targetSequence.length).map((entry) => entry.input); 87 88 // 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} 103 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} 149 150// Module-level state store for toggle states 151const gamepadStates = new Map(); 152 153export const gamepad = (index = 0) => { 154 const handler = new GamepadHandler(index); 155 const sequenceDetector = new ButtonSequenceDetector(2000); 156 157 // Base signal that polls gamepad state and handles sequence detection 158 const baseSignal = signal((t) => { 159 handler.poll(); 160 const axes = handler.getAxes(); 161 const buttons = handler.getButtons(); 162 163 // Add all button inputs to sequence detector 164 buttons.forEach((value, i) => { 165 sequenceDetector.addInput(i, value); 166 }); 167 168 return { axes, buttons, t }; 169 }); 170 171 // Create axes patterns 172 const axes = { 173 x1: baseSignal.fmap((state) => state.axes[0]), 174 y1: baseSignal.fmap((state) => state.axes[1]), 175 x2: baseSignal.fmap((state) => state.axes[2]), 176 y2: baseSignal.fmap((state) => state.axes[3]), 177 }; 178 179 // Add bipolar versions 180 axes.x1_2 = axes.x1.toBipolar(); 181 axes.y1_2 = axes.y1.toBipolar(); 182 axes.x2_2 = axes.x2.toBipolar(); 183 axes.y2_2 = axes.y2.toBipolar(); 184 185 // Create button patterns 186 const buttons = Array(16) 187 .fill(null) 188 .map((_, i) => { 189 // Create unique key for this gamepad+button combination 190 const stateKey = `gamepad${index}_btn${i}`; 191 192 // Initialize toggle state if it doesn't exist 193 if (!gamepadStates.has(stateKey)) { 194 gamepadStates.set(stateKey, { 195 lastButtonState: 0, 196 toggleState: 0, 197 }); 198 } 199 200 // Direct button value pattern (no longer needs to call addInput) 201 const btn = baseSignal.fmap((state) => state.buttons[i]); 202 203 // 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 }); 216 217 return { value: btn, toggle }; 218 }); 219 220 // Create sequence checker pattern 221 const btnSequence = (sequence) => { 222 return baseSignal.fmap(() => sequenceDetector.checkSequence(sequence)); 223 }; 224 const checkSequence = btnSequence; 225 const btnSeq = btnSequence; 226 const btnseq = btnSeq; 227 228 // 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}; 247 248// Optional: Export for debugging or state management 249export const getGamepadStates = () => Object.fromEntries(gamepadStates); 250export const clearGamepadStates = () => gamepadStates.clear();