Add GamePad Module
This commit is contained in:
parent
726abf76ce
commit
d741b4d17a
9 changed files with 726 additions and 1 deletions
237
packages/gamepad/gamepad.mjs
Normal file
237
packages/gamepad/gamepad.mjs
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
// @strudel/gamepad/index.mjs
|
||||
|
||||
import { signal } from '@strudel/core';
|
||||
|
||||
// Button mapping for Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)
|
||||
export const buttonMap = {
|
||||
a: 0,
|
||||
b: 1,
|
||||
x: 2,
|
||||
y: 3,
|
||||
lb: 4,
|
||||
rb: 5,
|
||||
lt: 6,
|
||||
rt: 7,
|
||||
back: 8,
|
||||
start: 9,
|
||||
u: 12,
|
||||
up: 12,
|
||||
d: 13,
|
||||
down: 13,
|
||||
l: 14,
|
||||
left: 14,
|
||||
r: 15,
|
||||
right: 15,
|
||||
};
|
||||
|
||||
class ButtonSequenceDetector {
|
||||
constructor(timeWindow = 1000) {
|
||||
this.sequence = [];
|
||||
this.timeWindow = timeWindow;
|
||||
this.lastInputTime = 0;
|
||||
this.buttonStates = Array(16).fill(0); // Track previous state of each button
|
||||
// Button mapping for character inputs
|
||||
}
|
||||
|
||||
addInput(buttonIndex, buttonValue) {
|
||||
const currentTime = Date.now();
|
||||
|
||||
// Only add input on button press (rising edge)
|
||||
if (buttonValue === 1 && this.buttonStates[buttonIndex] === 0) {
|
||||
// Clear sequence if too much time has passed
|
||||
if (currentTime - this.lastInputTime > this.timeWindow) {
|
||||
this.sequence = [];
|
||||
}
|
||||
|
||||
// Store the button name instead of index
|
||||
const buttonName = Object.keys(buttonMap).find((key) => buttonMap[key] === buttonIndex) || buttonIndex.toString();
|
||||
|
||||
this.sequence.push({
|
||||
input: buttonName,
|
||||
timestamp: currentTime,
|
||||
});
|
||||
|
||||
this.lastInputTime = currentTime;
|
||||
|
||||
//console.log(this.sequence);
|
||||
// Keep only inputs within the time window
|
||||
this.sequence = this.sequence.filter((entry) => currentTime - entry.timestamp <= this.timeWindow);
|
||||
}
|
||||
|
||||
// Update button state
|
||||
this.buttonStates[buttonIndex] = buttonValue;
|
||||
}
|
||||
|
||||
checkSequence(targetSequence) {
|
||||
if (!Array.isArray(targetSequence) && typeof targetSequence !== 'string') {
|
||||
console.error('ButtonSequenceDetector: targetSequence must be an array or string');
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this.sequence.length < targetSequence.length) return 0;
|
||||
|
||||
// Convert string input to array if needed
|
||||
const sequence =
|
||||
typeof targetSequence === 'string'
|
||||
? targetSequence.toLowerCase().split('')
|
||||
: targetSequence.map((s) => s.toString().toLowerCase());
|
||||
|
||||
// Get the last n inputs where n is the target sequence length
|
||||
const lastInputs = this.sequence.slice(-targetSequence.length).map((entry) => entry.input);
|
||||
|
||||
// Compare sequences
|
||||
return lastInputs.every((input, index) => {
|
||||
const target = sequence[index];
|
||||
// Check if either the input matches directly or they refer to the same button in the map
|
||||
return (
|
||||
input === target ||
|
||||
buttonMap[input] === buttonMap[target] ||
|
||||
// Also check if the numerical index matches
|
||||
buttonMap[input] === parseInt(target)
|
||||
);
|
||||
})
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
|
||||
class GamepadHandler {
|
||||
constructor(index = 0) {
|
||||
// Add index parameter
|
||||
this._gamepads = {};
|
||||
this._activeGamepad = index; // Use provided index
|
||||
this._axes = [0, 0, 0, 0];
|
||||
this._buttons = Array(16).fill(0);
|
||||
this.setupEventListeners();
|
||||
this.startPolling();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
window.addEventListener('gamepadconnected', (e) => {
|
||||
this._gamepads[e.gamepad.index] = e.gamepad;
|
||||
if (!this._activeGamepad) {
|
||||
this._activeGamepad = e.gamepad.index;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('gamepaddisconnected', (e) => {
|
||||
delete this._gamepads[e.gamepad.index];
|
||||
if (this._activeGamepad === e.gamepad.index) {
|
||||
this._activeGamepad = Object.keys(this._gamepads)[0] || null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startPolling() {
|
||||
const poll = () => {
|
||||
if (this._activeGamepad !== null) {
|
||||
const gamepad = navigator.getGamepads()[this._activeGamepad];
|
||||
if (gamepad) {
|
||||
// Update axes (normalized to 0-1 range)
|
||||
this._axes = gamepad.axes.map((axis) => (axis + 1) / 2);
|
||||
// Update buttons
|
||||
this._buttons = gamepad.buttons.map((button) => button.value);
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(poll);
|
||||
};
|
||||
poll();
|
||||
}
|
||||
|
||||
getAxes() {
|
||||
return this._axes;
|
||||
}
|
||||
getButtons() {
|
||||
return this._buttons;
|
||||
}
|
||||
}
|
||||
|
||||
// Store gamepadValues globally
|
||||
export const gamepadValues = {};
|
||||
|
||||
// Replace singleton with factory function
|
||||
export const gamepad = (index = 0) => {
|
||||
const handler = new GamepadHandler(index);
|
||||
const sequenceDetector = new ButtonSequenceDetector(2000);
|
||||
|
||||
// Initialize state for this gamepad if it doesn't exist
|
||||
if (!gamepadValues[index]) {
|
||||
gamepadValues[index] = Array(16).fill(0);
|
||||
}
|
||||
|
||||
// Create signals for this specific gamepad instance
|
||||
const axes = {
|
||||
x1: signal(() => handler.getAxes()[0]),
|
||||
y1: signal(() => handler.getAxes()[1]),
|
||||
x2: signal(() => handler.getAxes()[2]),
|
||||
y2: signal(() => handler.getAxes()[3]),
|
||||
};
|
||||
|
||||
// Add bipolar versions
|
||||
axes.x1_2 = axes.x1.toBipolar();
|
||||
axes.y1_2 = axes.y1.toBipolar();
|
||||
axes.x2_2 = axes.x2.toBipolar();
|
||||
axes.y2_2 = axes.y2.toBipolar();
|
||||
|
||||
// Create button signals
|
||||
const buttons = Array(16)
|
||||
.fill(null)
|
||||
.map((_, i) => {
|
||||
const btn = signal(() => {
|
||||
const value = handler.getButtons()[i];
|
||||
sequenceDetector.addInput(i, value);
|
||||
return value;
|
||||
});
|
||||
let lastButtonState = 0;
|
||||
const toggle = signal(() => {
|
||||
const currentState = handler.getButtons()[i];
|
||||
if (currentState === 1 && lastButtonState === 0) {
|
||||
// Toggle the state
|
||||
const newValue = gamepadValues[index][i] === 0 ? 1 : 0;
|
||||
gamepadValues[index][i] = newValue;
|
||||
// Broadcast the change
|
||||
window.postMessage({
|
||||
type: 'gamepad-toggle',
|
||||
gamepadIndex: index,
|
||||
buttonIndex: i,
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
lastButtonState = currentState;
|
||||
return gamepadValues[index][i];
|
||||
});
|
||||
return { value: btn, toggle };
|
||||
});
|
||||
|
||||
const checkSequence = (sequence) => {
|
||||
return signal(() => sequenceDetector.checkSequence(sequence));
|
||||
};
|
||||
|
||||
// Return an object with all controls
|
||||
return {
|
||||
...axes,
|
||||
buttons,
|
||||
...Object.fromEntries(
|
||||
Object.entries(buttonMap).flatMap(([key, index]) => [
|
||||
[key.toLowerCase(), buttons[index].value],
|
||||
[key.toUpperCase(), buttons[index].value],
|
||||
[`tgl${key.toLowerCase()}`, buttons[index].toggle],
|
||||
[`tgl${key.toUpperCase()}`, buttons[index].toggle],
|
||||
]),
|
||||
),
|
||||
checkSequence,
|
||||
};
|
||||
};
|
||||
|
||||
// Message-based state updates (add this at the bottom of the file)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('message', (e) => {
|
||||
if (e.data.type === 'gamepad-toggle') {
|
||||
const { gamepadIndex, buttonIndex, value } = e.data;
|
||||
if (!gamepadValues[gamepadIndex]) {
|
||||
gamepadValues[gamepadIndex] = Array(16).fill(0);
|
||||
}
|
||||
gamepadValues[gamepadIndex][buttonIndex] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue