Connor McCutcheon
/ Music
gamepad.mjs
mjs
// @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,
  l3: 10,
  ls: 10,
  r3: 11,
  rs: 11,
  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());
    //console.log(this.sequence);
    // 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();
  }
  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;
      }
    });
  }
  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);
      }
    }
  }
  getAxes() {
    return this._axes;
  }
  getButtons() {
    return this._buttons;
  }
}
// Module-level state store for toggle states
const gamepadStates = new Map();
export const gamepad = (index = 0) => {
  const handler = new GamepadHandler(index);
  const sequenceDetector = new ButtonSequenceDetector(2000);
  // Base signal that polls gamepad state and handles sequence detection
  const baseSignal = signal((t) => {
    handler.poll();
    const axes = handler.getAxes();
    const buttons = handler.getButtons();
    // Add all button inputs to sequence detector
    buttons.forEach((value, i) => {
      sequenceDetector.addInput(i, value);
    });
    return { axes, buttons, t };
  });
  // Create axes patterns
  const axes = {
    x1: baseSignal.fmap((state) => state.axes[0]),
    y1: baseSignal.fmap((state) => state.axes[1]),
    x2: baseSignal.fmap((state) => state.axes[2]),
    y2: baseSignal.fmap((state) => state.axes[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 patterns
  const buttons = Array(16)
    .fill(null)
    .map((_, i) => {
      // Create unique key for this gamepad+button combination
      const stateKey = `gamepad${index}_btn${i}`;
      // Initialize toggle state if it doesn't exist
      if (!gamepadStates.has(stateKey)) {
        gamepadStates.set(stateKey, {
          lastButtonState: 0,
          toggleState: 0,
        });
      }
      // Direct button value pattern (no longer needs to call addInput)
      const btn = baseSignal.fmap((state) => state.buttons[i]);
      // Button toggle pattern with persistent state
      const toggle = baseSignal.fmap((state) => {
        const currentState = state.buttons[i];
        const buttonState = gamepadStates.get(stateKey);
        if (currentState === 1 && buttonState.lastButtonState === 0) {
          // Toggle the state on rising edge
          buttonState.toggleState = buttonState.toggleState === 0 ? 1 : 0;
        }
        buttonState.lastButtonState = currentState;
        return buttonState.toggleState;
      });
      return { value: btn, toggle };
    });
  // Create sequence checker pattern
  const btnSequence = (sequence) => {
    return baseSignal.fmap(() => sequenceDetector.checkSequence(sequence));
  };
  const checkSequence = btnSequence;
  const btnSeq = btnSequence;
  const btnseq = btnSeq;
  // 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,
    btnSequence,
    btnSeq,
    btnseq,
    raw: baseSignal,
  };
};
// Optional: Export for debugging or state management
export const getGamepadStates = () => Object.fromEntries(gamepadStates);
export const clearGamepadStates = () => gamepadStates.clear();
No comments yet.