import { useCallback, useRef } from 'react'; import { cn } from '@/lib/utils'; import { impactLight } from '@/lib/haptics'; import type { WebxdcHandle } from '@/components/Webxdc'; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- interface GameControlsProps { webxdcHandle: WebxdcHandle | null; className?: string; } // Key mappings for each button. const KEY_MAP = { up: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 }, down: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 }, left: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 }, right: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 }, a: { key: 'x', code: 'KeyX', keyCode: 88 }, b: { key: 'z', code: 'KeyZ', keyCode: 90 }, start: { key: 'Enter', code: 'Enter', keyCode: 13 }, select: { key: 'Shift', code: 'ShiftRight', keyCode: 16 }, } as const; type GameButton = keyof typeof KEY_MAP; /** Buttons that trigger haptic feedback on press. */ const HAPTIC_BUTTONS = new Set(['a', 'b']); /** Trigger a short vibration via the native haptic engine. */ function haptic() { impactLight(); } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- /** * Virtual gamepad: d-pad + A/B + Start/Select. Buttons send synthetic key * events into the webxdc iframe via `webxdc.keyboard` postMessage. */ export function GameControls({ webxdcHandle, className }: GameControlsProps) { const activeKeys = useRef(new Set()); const sendKey = useCallback( (type: 'keydown' | 'keyup', button: GameButton) => { if (!webxdcHandle) return; const { key, code, keyCode } = KEY_MAP[button]; if (type === 'keydown') { if (activeKeys.current.has(code)) return; activeKeys.current.add(code); if (HAPTIC_BUTTONS.has(button)) haptic(); } else { activeKeys.current.delete(code); } webxdcHandle.postMessage({ jsonrpc: '2.0', method: 'webxdc.keyboard', params: { type, key, code, keyCode }, }); }, [webxdcHandle], ); const handlers = (button: GameButton) => ({ onPointerDown: (e: React.PointerEvent) => { e.preventDefault(); (e.target as HTMLElement).setPointerCapture(e.pointerId); sendKey('keydown', button); }, onPointerUp: (e: React.PointerEvent) => { e.preventDefault(); sendKey('keyup', button); }, onPointerCancel: (e: React.PointerEvent) => { e.preventDefault(); sendKey('keyup', button); }, onContextMenu: (e: React.SyntheticEvent) => e.preventDefault(), }); return (
{/* Main controls row: D-pad on left, A/B on right */}
{/* D-Pad */}
{/* Cross background */}
{/* Up */} {/* Down */} {/* Left */} {/* Right */}
{/* A / B buttons */}
{/* Start / Select row */}
); } export default GameControls;