import { type Button } from "./types"; import { InputController } from "./controller"; const KEYS: Record = { ArrowUp: "up", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right", KeyX: "a", KeyZ: "b", Enter: "start", ShiftLeft: "select", ShiftRight: "select", }; export function bindControls( root: HTMLElement, input: InputController, active: () => boolean, unlock: () => void, ) { const abort = new AbortController(); const options = { signal: abort.signal }; const buttons = [ ...root.querySelectorAll("[data-game-button]"), ]; const refresh = () => buttons.forEach((el) => { const pressed = input.isPressed(el.dataset.gameButton as Button); el.classList.toggle("pressed", pressed); el.setAttribute("aria-pressed", String(pressed)); }); const release = () => { input.releaseAll(); refresh(); }; for (const el of buttons) { const focusedSource = `focused:${el.dataset.gameButton}`; el.addEventListener( "keydown", (event) => { if (!active() || !["Space", "Enter"].includes(event.code)) return; event.preventDefault(); unlock(); input.press(focusedSource, el.dataset.gameButton as Button); refresh(); }, options, ); el.addEventListener( "keyup", (event) => { if (!["Space", "Enter"].includes(event.code)) return; event.preventDefault(); input.release(focusedSource); refresh(); }, options, ); el.addEventListener( "blur", () => { input.release(focusedSource); refresh(); }, options, ); el.addEventListener( "pointerdown", (event) => { if (!active() || (event.pointerType === "mouse" && event.button !== 0)) return; event.preventDefault(); unlock(); el.setPointerCapture(event.pointerId); input.press( `pointer:${event.pointerId}`, el.dataset.gameButton as Button, ); refresh(); }, options, ); // Slide between D-pad directions without lifting the finger. el.addEventListener( "pointermove", (event) => { if (!el.hasPointerCapture(event.pointerId) || !el.closest(".dpad")) return; const target = document .elementFromPoint(event.clientX, event.clientY) ?.closest("[data-game-button]"); if (target?.closest(".dpad")) input.press( `pointer:${event.pointerId}`, target.dataset.gameButton as Button, ); else input.release(`pointer:${event.pointerId}`); refresh(); }, options, ); for (const name of [ "pointerup", "pointercancel", "lostpointercapture", ] as const) { el.addEventListener( name, (event) => { input.release(`pointer:${event.pointerId}`); refresh(); }, options, ); } el.addEventListener( "contextmenu", (event) => event.preventDefault(), options, ); } window.addEventListener( "keydown", (event) => { if (!active() || event.ctrlKey || event.metaKey || event.altKey) return; if ( (event.target as HTMLElement)?.closest( "input, textarea, select, button, a, [contenteditable]", ) ) return; const button = KEYS[event.code]; if (!button) return; event.preventDefault(); unlock(); input.press(`key:${event.code}`, button); refresh(); }, options, ); window.addEventListener( "keyup", (event) => { input.release(`key:${event.code}`); refresh(); }, options, ); window.addEventListener("blur", release, options); document.addEventListener( "visibilitychange", () => { if (document.hidden) release(); }, options, ); return { release, dispose: () => { release(); abort.abort(); }, }; }