mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-23 21:47:16 -04:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b48321a84c | |||
| 827b659592 | |||
| 4553408b28 | |||
| 42eb33440c | |||
| 87768198a0 | |||
| c24a87df48 | |||
| 2d63f36201 | |||
| 43ac2d3d72 |
@@ -14,7 +14,12 @@ npm i @strudel/gamepad --save
|
|||||||
import { gamepad } from '@strudel/gamepad';
|
import { gamepad } from '@strudel/gamepad';
|
||||||
|
|
||||||
// Initialize gamepad (optional index parameter, defaults to 0)
|
// Initialize gamepad (optional index parameter, defaults to 0)
|
||||||
const pad = gamepad(0);
|
const pad = gamepad(0); // Default mapping is XBOX {a: 0, b: 1, x: 2, y: 3}
|
||||||
|
|
||||||
|
//const pad = gamepad(0, 'XBOX'); // XBOX button mapping {a: 0, b: 1, x: 2, y: 3}
|
||||||
|
//const pad = gamepad(0, 'NES'); // Nintendo button mapping {a: 1, b: 0, x: 3, y: 2}
|
||||||
|
//const pad = gamepad(0, {a: 0, b: 1, x: 2, y: 3}); // Custom mapping
|
||||||
|
|
||||||
|
|
||||||
// Use gamepad inputs in patterns
|
// Use gamepad inputs in patterns
|
||||||
const pattern = sequence([
|
const pattern = sequence([
|
||||||
@@ -86,6 +91,7 @@ $: sound("hadoken").gain(pad.checkSequence(HADOKEN))
|
|||||||
## Multiple Gamepads
|
## Multiple Gamepads
|
||||||
|
|
||||||
You can connect multiple gamepads by specifying the gamepad index:
|
You can connect multiple gamepads by specifying the gamepad index:
|
||||||
|
Make sure to press buttons on all connected gamepads before hitting play, so the browser can properly detect them.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const pad1 = gamepad(0); // First gamepad
|
const pad1 = gamepad(0); // First gamepad
|
||||||
|
|||||||
@@ -107,9 +107,32 @@ $: s("free_hadouken -").slow(2)
|
|||||||
samples({free_hadouken: 'https://cdn.freesound.org/previews/67/67674_111920-lq.mp3'})
|
samples({free_hadouken: 'https://cdn.freesound.org/previews/67/67674_111920-lq.mp3'})
|
||||||
`} />
|
`} />
|
||||||
|
|
||||||
## Multiple Gamepads
|
### Button Sequences
|
||||||
|
|
||||||
|
### Button Mappings
|
||||||
|
|
||||||
|
The gamepad module supports different button mapping configurations to accommodate various gamepad layouts:
|
||||||
|
|
||||||
|
- **XBOX** (Default): Standard Xbox-style mapping where A=0, B=1, X=2, Y=3
|
||||||
|
- **NES**: Nintendo-style mapping where B=0, A=1, Y=2, X=3
|
||||||
|
- **Custom**: Define your own button mapping by passing an object with button assignments
|
||||||
|
|
||||||
|
You can specify the mapping when initializing the gamepad:
|
||||||
|
|
||||||
|
<MiniRepl
|
||||||
|
client:idle
|
||||||
|
tune={`
|
||||||
|
const pad1 = gamepad(0); // Default mapping is XBOX {a: 0, b: 1, x: 2, y: 3}
|
||||||
|
const pad2 = gamepad(1, 'XBOX'); // XBOX button mapping {a: 0, b: 1, x: 2, y: 3}
|
||||||
|
const pad3 = gamepad(2, 'NES'); // Nintendo button mapping {a: 1, b: 0, x: 3, y: 2}
|
||||||
|
const pad4 = gamepad(3, {a: 0, b: 1, x: 2, y: 3}); // Custom mapping
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### Multiple Gamepads
|
||||||
|
|
||||||
Strudel supports multiple gamepads. You can specify the gamepad index to connect to different devices.
|
Strudel supports multiple gamepads. You can specify the gamepad index to connect to different devices.
|
||||||
|
Make sure to press buttons on all connected gamepads before hitting play, so the browser can properly detect them.
|
||||||
|
|
||||||
<MiniRepl
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
|
|||||||
+119
-28
@@ -1,35 +1,62 @@
|
|||||||
// @strudel/gamepad/index.mjs
|
// @strudel/gamepad/index.mjs
|
||||||
|
|
||||||
import { signal } from '@strudel/core';
|
import { signal } from '@strudel/core';
|
||||||
|
import { logger } from '@strudel/core';
|
||||||
|
|
||||||
// Button mapping for Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)
|
// Button mapping for Logitech Dual Action (STANDARD GAMEPAD Vendor: 046d Product: c216)
|
||||||
export const buttonMap = {
|
|
||||||
a: 0,
|
const buttonMapSettings = {
|
||||||
b: 1,
|
XBOX: {
|
||||||
x: 2,
|
// XBOX mapping default
|
||||||
y: 3,
|
a: 0,
|
||||||
lb: 4,
|
b: 1,
|
||||||
rb: 5,
|
x: 2,
|
||||||
lt: 6,
|
y: 3,
|
||||||
rt: 7,
|
lb: 4,
|
||||||
back: 8,
|
rb: 5,
|
||||||
start: 9,
|
lt: 6,
|
||||||
u: 12,
|
rt: 7,
|
||||||
up: 12,
|
back: 8,
|
||||||
d: 13,
|
start: 9,
|
||||||
down: 13,
|
u: 12,
|
||||||
l: 14,
|
up: 12,
|
||||||
left: 14,
|
d: 13,
|
||||||
r: 15,
|
down: 13,
|
||||||
right: 15,
|
l: 14,
|
||||||
|
left: 14,
|
||||||
|
r: 15,
|
||||||
|
right: 15,
|
||||||
|
},
|
||||||
|
NES: {
|
||||||
|
// Nintendo mapping
|
||||||
|
a: 1,
|
||||||
|
b: 0,
|
||||||
|
x: 3,
|
||||||
|
y: 2,
|
||||||
|
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 {
|
class ButtonSequenceDetector {
|
||||||
constructor(timeWindow = 1000) {
|
constructor(timeWindow = 1000, mapping) {
|
||||||
this.sequence = [];
|
this.sequence = [];
|
||||||
this.timeWindow = timeWindow;
|
this.timeWindow = timeWindow;
|
||||||
this.lastInputTime = 0;
|
this.lastInputTime = 0;
|
||||||
this.buttonStates = Array(16).fill(0); // Track previous state of each button
|
this.buttonStates = Array(16).fill(0); // Track previous state of each button
|
||||||
|
this.buttonMap = mapping;
|
||||||
// Button mapping for character inputs
|
// Button mapping for character inputs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +71,8 @@ class ButtonSequenceDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store the button name instead of index
|
// Store the button name instead of index
|
||||||
const buttonName = Object.keys(buttonMap).find((key) => buttonMap[key] === buttonIndex) || buttonIndex.toString();
|
const buttonName =
|
||||||
|
Object.keys(this.buttonMap).find((key) => this.buttonMap[key] === buttonIndex) || buttonIndex.toString();
|
||||||
|
|
||||||
this.sequence.push({
|
this.sequence.push({
|
||||||
input: buttonName,
|
input: buttonName,
|
||||||
@@ -87,9 +115,9 @@ class ButtonSequenceDetector {
|
|||||||
// Check if either the input matches directly or they refer to the same button in the map
|
// Check if either the input matches directly or they refer to the same button in the map
|
||||||
return (
|
return (
|
||||||
input === target ||
|
input === target ||
|
||||||
buttonMap[input] === buttonMap[target] ||
|
this.buttonMap[input] === this.buttonMap[target] ||
|
||||||
// Also check if the numerical index matches
|
// Also check if the numerical index matches
|
||||||
buttonMap[input] === parseInt(target)
|
this.buttonMap[input] === parseInt(target)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
? 1
|
? 1
|
||||||
@@ -98,9 +126,10 @@ class ButtonSequenceDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class GamepadHandler {
|
class GamepadHandler {
|
||||||
constructor(index = 0) {
|
constructor(index = 0, mapping) {
|
||||||
// Add index parameter
|
// Add index parameter
|
||||||
this._gamepads = {};
|
this._gamepads = {};
|
||||||
|
this._mapping = mapping;
|
||||||
this._activeGamepad = index; // Use provided index
|
this._activeGamepad = index; // Use provided index
|
||||||
this._axes = [0, 0, 0, 0];
|
this._axes = [0, 0, 0, 0];
|
||||||
this._buttons = Array(16).fill(0);
|
this._buttons = Array(16).fill(0);
|
||||||
@@ -143,12 +172,66 @@ class GamepadHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add utility function to list all connected gamepads
|
||||||
|
export const listGamepads = () => {
|
||||||
|
const gamepads = navigator.getGamepads();
|
||||||
|
const connectedGamepads = Array.from(gamepads)
|
||||||
|
.filter((gp) => gp !== null)
|
||||||
|
.map((gp) => ({
|
||||||
|
index: gp.index,
|
||||||
|
id: gp.id,
|
||||||
|
mapping: gp.mapping,
|
||||||
|
buttons: gp.buttons.length,
|
||||||
|
axes: gp.axes.length,
|
||||||
|
connected: gp.connected,
|
||||||
|
timestamp: gp.timestamp,
|
||||||
|
}));
|
||||||
|
// Format the gamepads info into a readable string
|
||||||
|
const gamepadsInfo = connectedGamepads.map((gp) => `${gp.index}: ${gp.id}`).join('\n');
|
||||||
|
|
||||||
|
logger(`[gamepad] available gamepads:\n${gamepadsInfo}`);
|
||||||
|
return connectedGamepads;
|
||||||
|
};
|
||||||
|
|
||||||
// Module-level state store for toggle states
|
// Module-level state store for toggle states
|
||||||
const gamepadStates = new Map();
|
const gamepadStates = new Map();
|
||||||
|
|
||||||
export const gamepad = (index = 0) => {
|
export const gamepad = (index = 0, mapping = 'XBOX') => {
|
||||||
const handler = new GamepadHandler(index);
|
// list connected gamepads
|
||||||
const sequenceDetector = new ButtonSequenceDetector(2000);
|
const connectedGamepads = listGamepads();
|
||||||
|
|
||||||
|
// Check if the requested gamepad index exists
|
||||||
|
const requestedGamepad = connectedGamepads.find((gp) => gp.index === index);
|
||||||
|
if (!requestedGamepad) {
|
||||||
|
throw new Error(
|
||||||
|
`[gamepad] gamepad at index ${index} not found. available gamepads: ${connectedGamepads.map((gp) => gp.index).join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle button mapping
|
||||||
|
let buttonMap = buttonMapSettings.XBOX;
|
||||||
|
|
||||||
|
if (typeof mapping === 'string') {
|
||||||
|
buttonMap = buttonMapSettings[mapping.toUpperCase()];
|
||||||
|
} else if (typeof mapping === 'object') {
|
||||||
|
buttonMap = { ...buttonMapSettings.XBOX, ...mapping };
|
||||||
|
// Check that all mapping values are valid button indices
|
||||||
|
const maxButtons = requestedGamepad.buttons; // Standard gamepad has 16 buttons
|
||||||
|
for (const [key, value] of Object.entries(mapping)) {
|
||||||
|
if (typeof value !== 'number' || value < 0 || value >= maxButtons) {
|
||||||
|
throw new Error(
|
||||||
|
`[gamepad] invalid button mapping for '${key}': ${value}. Must be a number between 0 and ${maxButtons - 1}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!buttonMap) {
|
||||||
|
throw new Error(`[gamepad] button mapping '${mapping}' not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = new GamepadHandler(index, buttonMap);
|
||||||
|
const sequenceDetector = new ButtonSequenceDetector(2000, buttonMap);
|
||||||
|
|
||||||
// Base signal that polls gamepad state and handles sequence detection
|
// Base signal that polls gamepad state and handles sequence detection
|
||||||
const baseSignal = signal((t) => {
|
const baseSignal = signal((t) => {
|
||||||
@@ -218,8 +301,14 @@ export const gamepad = (index = 0) => {
|
|||||||
return baseSignal.fmap(() => sequenceDetector.checkSequence(sequence));
|
return baseSignal.fmap(() => sequenceDetector.checkSequence(sequence));
|
||||||
};
|
};
|
||||||
const checkSequence = btnSequence;
|
const checkSequence = btnSequence;
|
||||||
|
const sequence = btnSequence;
|
||||||
const btnSeq = btnSequence;
|
const btnSeq = btnSequence;
|
||||||
const btnseq = btnSeq;
|
const btnseq = btnSequence;
|
||||||
|
const seq = btnSequence;
|
||||||
|
|
||||||
|
logger(
|
||||||
|
`[gamepad] connected to gamepad ${index} (${requestedGamepad.id}) with ${typeof mapping === 'object' ? 'custom' : mapping} mapping`,
|
||||||
|
);
|
||||||
|
|
||||||
// Return an object with all controls
|
// Return an object with all controls
|
||||||
return {
|
return {
|
||||||
@@ -234,9 +323,11 @@ export const gamepad = (index = 0) => {
|
|||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
checkSequence,
|
checkSequence,
|
||||||
|
sequence,
|
||||||
btnSequence,
|
btnSequence,
|
||||||
btnSeq,
|
btnSeq,
|
||||||
btnseq,
|
btnseq,
|
||||||
|
seq,
|
||||||
raw: baseSignal,
|
raw: baseSignal,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user