"use client"; import { useCallback, useState } from "react"; interface KeyBinding { key: string; code: string; } const LEFT_FLIPPER: KeyBinding = { key: "Shift", code: "ShiftLeft" }; const RIGHT_FLIPPER: KeyBinding = { key: "Shift", code: "ShiftRight" }; const PLUNGER: KeyBinding = { key: "Enter", code: "Enter" }; const INSERT_COIN: KeyBinding = { key: "4", code: "Digit4" }; const START_GAME: KeyBinding = { key: "1", code: "Digit1" }; function dispatchKey(type: "keydown" | "keyup", binding: KeyBinding) { // SDL3's Emscripten video backend listens on the default "#window" target, // so a real KeyboardEvent dispatched on window reaches the engine the same // way a physical keypress would (see @valknar/vpinball-wasm's own // touch-controls.js, which this bar replaces with app-styled UI). window.dispatchEvent(new KeyboardEvent(type, { key: binding.key, code: binding.code, bubbles: true })); } function isTouchDevice(): boolean { return typeof window !== "undefined" && ("ontouchstart" in window || navigator.maxTouchPoints > 0); } function ControlButton({ binding, label, size, }: { binding: KeyBinding; label: string; size: "lg" | "md" | "sm"; }) { const [pressed, setPressed] = useState(false); const onDown = useCallback( (e: React.PointerEvent) => { e.preventDefault(); e.currentTarget.setPointerCapture(e.pointerId); setPressed(true); dispatchKey("keydown", binding); }, [binding], ); const onUp = useCallback(() => { setPressed(false); dispatchKey("keyup", binding); }, [binding]); const sizeClasses = size === "lg" ? "h-20 w-20 text-3xl" : size === "md" ? "h-14 w-14 text-lg" : "h-12 w-14 font-mono text-[11px] tracking-widest"; return ( ); } export interface TouchControlBarProps { active: boolean; } /** * Coin-door-styled bottom control bar for mobile play. Replaces * @valknar/vpinball-wasm's default floating overlay buttons with UI that * matches the cabinet look and puts the flippers at the screen's bottom * corners, in thumb reach when holding the device landscape. */ export default function TouchControlBar({ active }: TouchControlBarProps) { // Computed once at mount (matches usePinballInstance's hasWebGL2 pattern) — // touch support doesn't change over a session, so this doesn't need an // effect/listener, just a lazy initial state. const [isTouch] = useState(isTouchDevice); if (!active || !isTouch) return null; return (
); }