166 lines
4.0 KiB
TypeScript
166 lines
4.0 KiB
TypeScript
import { type Button } from "./types";
|
|
import { InputController } from "./controller";
|
|
|
|
const KEYS: Record<string, Button> = {
|
|
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<HTMLButtonElement>("[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<HTMLElement>("[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();
|
|
},
|
|
};
|
|
}
|