import { useEffect, useRef } from "preact/hooks"; const REPEAT_DELAY = 400; const REPEAT_INTERVAL = 150; const BTN_A = 0; const BTN_B = 1; const BTN_X = 2; const BTN_Y = 3; const DPAD_UP = 12; const DPAD_DOWN = 13; interface GamepadActions { onUp?: () => void; onDown?: () => void; onConfirm?: () => void; onBack?: () => void; onNew?: () => void; onMic?: () => void; onConnected?: (connected: boolean) => void; } export function useGamepad(actions: GamepadActions) { const actionsRef = useRef(actions); actionsRef.current = actions; useEffect(() => { const prevButtons: Record = {}; const heldSince: Record = {}; const lastRepeat: Record = {}; let raf: number; const REPEAT_BTNS = new Set([DPAD_UP, DPAD_DOWN]); const BUTTON_MAP: [number, keyof GamepadActions][] = [ [BTN_A, "onConfirm"], [BTN_B, "onBack"], [BTN_X, "onMic"], [BTN_Y, "onNew"], [DPAD_UP, "onUp"], [DPAD_DOWN, "onDown"], ]; function poll() { const gamepads = navigator.getGamepads(); const gp = gamepads[0]; if (gp) { const now = Date.now(); for (const [idx, actionKey] of BUTTON_MAP) { const pressed = gp.buttons[idx]?.pressed ?? false; const wasPressed = prevButtons[idx] ?? false; if (pressed && !wasPressed) { (actionsRef.current[actionKey] as (() => void) | undefined)?.(); if (REPEAT_BTNS.has(idx)) { heldSince[idx] = now; lastRepeat[idx] = now; } } else if (pressed && wasPressed && REPEAT_BTNS.has(idx)) { if (now - heldSince[idx] > REPEAT_DELAY && now - lastRepeat[idx] > REPEAT_INTERVAL) { (actionsRef.current[actionKey] as (() => void) | undefined)?.(); lastRepeat[idx] = now; } } else if (!pressed && wasPressed) { delete heldSince[idx]; delete lastRepeat[idx]; } prevButtons[idx] = pressed; } } raf = requestAnimationFrame(poll); } function onConnected() { actionsRef.current.onConnected?.(true); } function onDisconnected() { actionsRef.current.onConnected?.(false); } window.addEventListener("gamepadconnected", onConnected); window.addEventListener("gamepaddisconnected", onDisconnected); if (navigator.getGamepads().some(Boolean)) { actionsRef.current.onConnected?.(true); } raf = requestAnimationFrame(poll); return () => { cancelAnimationFrame(raf); window.removeEventListener("gamepadconnected", onConnected); window.removeEventListener("gamepaddisconnected", onDisconnected); }; }, []); }