From 07e5d4ee4da9dc879b5a4a4cf13a0f3cf3e8b597 Mon Sep 17 00:00:00 2001 From: HumanoidSandvichDispenser Date: Sat, 11 Apr 2026 12:52:55 -0700 Subject: [PATCH] refactor: add common controller interface --- src/App.tsx | 15 ++++++++++- src/features/song-select/SongSelectScreen.tsx | 27 ++++++++++--------- src/input/actions.ts | 1 + src/input/inputBus.ts | 16 +++++++++++ src/input/useGameInput.ts | 7 +++++ src/input/useKeyboardInput.ts | 25 +++++++++++++++++ src/input/usePoseInput.ts | 21 +++++++++++++++ 7 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 src/input/actions.ts create mode 100644 src/input/inputBus.ts create mode 100644 src/input/useGameInput.ts create mode 100644 src/input/useKeyboardInput.ts create mode 100644 src/input/usePoseInput.ts diff --git a/src/App.tsx b/src/App.tsx index fbf4355..8230013 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,20 @@ import { AppRoutes } from './app/AppRoutes' +import { useKeyboardInput } from './input/useKeyboardInput' +import { usePoseInput } from './input/usePoseInput' + +function InputControllers() { + useKeyboardInput() + usePoseInput() + return null +} function App() { - return + return ( + <> + + + + ) } export default App diff --git a/src/features/song-select/SongSelectScreen.tsx b/src/features/song-select/SongSelectScreen.tsx index 4d7cf26..7232ce3 100644 --- a/src/features/song-select/SongSelectScreen.tsx +++ b/src/features/song-select/SongSelectScreen.tsx @@ -1,5 +1,6 @@ -import { useEffect, useMemo, useState } from 'react' +import { useMemo, useState } from 'react' import type { CSSProperties } from 'react' +import { useGameInput } from '../../input/useGameInput' import { ARHud } from './ARHud' import { SONGS } from './song-data' import { SongCarousel } from './SongCarousel' @@ -14,21 +15,21 @@ function wrapIndex(index: number, length: number): number { export function SongSelectScreen() { const [currentIndex, setCurrentIndex] = useState(0) const activeSong = SONGS[currentIndex] + const handleConfirm = () => {} - useEffect(() => { - const onKeyDown = (event: KeyboardEvent) => { - if (event.key === 'ArrowLeft') { + useGameInput((action) => { + if (action === 'LEFT') { setCurrentIndex((prev) => wrapIndex(prev - 1, SONGS.length)) - } - - if (event.key === 'ArrowRight') { - setCurrentIndex((prev) => wrapIndex(prev + 1, SONGS.length)) - } } - window.addEventListener('keydown', onKeyDown) - return () => window.removeEventListener('keydown', onKeyDown) - }, []) + if (action === 'RIGHT') { + setCurrentIndex((prev) => wrapIndex(prev + 1, SONGS.length)) + } + + if (action === 'BUTTON_A') { + handleConfirm() + } + }) const cssVars = useMemo( () => @@ -60,7 +61,7 @@ export function SongSelectScreen() { {}} + onConfirm={handleConfirm} /> diff --git a/src/input/actions.ts b/src/input/actions.ts new file mode 100644 index 0000000..7be4e53 --- /dev/null +++ b/src/input/actions.ts @@ -0,0 +1 @@ +export type GameAction = 'LEFT' | 'RIGHT' | 'UP' | 'DOWN' | 'BUTTON_A' | 'BUTTON_B' diff --git a/src/input/inputBus.ts b/src/input/inputBus.ts new file mode 100644 index 0000000..c0f374c --- /dev/null +++ b/src/input/inputBus.ts @@ -0,0 +1,16 @@ +import type { GameAction } from './actions' + +type GameActionListener = (action: GameAction) => void + +const listeners = new Set() + +export function emit(action: GameAction): void { + listeners.forEach((listener) => listener(action)) +} + +export function subscribe(listener: GameActionListener): () => void { + listeners.add(listener) + return () => { + listeners.delete(listener) + } +} diff --git a/src/input/useGameInput.ts b/src/input/useGameInput.ts new file mode 100644 index 0000000..93cd794 --- /dev/null +++ b/src/input/useGameInput.ts @@ -0,0 +1,7 @@ +import { useEffect } from 'react' +import type { GameAction } from './actions' +import { subscribe } from './inputBus' + +export function useGameInput(onAction: (action: GameAction) => void): void { + useEffect(() => subscribe(onAction), [onAction]) +} diff --git a/src/input/useKeyboardInput.ts b/src/input/useKeyboardInput.ts new file mode 100644 index 0000000..a892aaa --- /dev/null +++ b/src/input/useKeyboardInput.ts @@ -0,0 +1,25 @@ +import { useEffect } from 'react' +import type { GameAction } from './actions' +import { emit } from './inputBus' + +const KEY_TO_ACTION: Partial> = { + ArrowLeft: 'LEFT', + ArrowRight: 'RIGHT', + ArrowUp: 'UP', + ArrowDown: 'DOWN', + Enter: 'BUTTON_A', + Escape: 'BUTTON_B', +} + +export function useKeyboardInput(): void { + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + const action = KEY_TO_ACTION[event.key] + if (!action) return + emit(action) + } + + window.addEventListener('keydown', onKeyDown) + return () => window.removeEventListener('keydown', onKeyDown) + }, []) +} diff --git a/src/input/usePoseInput.ts b/src/input/usePoseInput.ts new file mode 100644 index 0000000..89c4e6f --- /dev/null +++ b/src/input/usePoseInput.ts @@ -0,0 +1,21 @@ +import { useEffect } from 'react' +import { emit } from './inputBus' + +export function usePoseInput(): void { + useEffect(() => { + const emitAction = (action: Parameters[0]) => { + emit(action) + } + + // MediaPipe integration point: + // Convert detected pose/gesture events into GameAction values and call `emitAction(action)`. + // Example mapping idea: swipe-left -> 'LEFT', swipe-right -> 'RIGHT', + // hand-raise -> 'BUTTON_A', cancel gesture -> 'BUTTON_B'. + // Keep all MediaPipe-specific setup/teardown in this hook so UI components remain input-agnostic. + void emitAction + + return () => { + // Cleanup MediaPipe resources/listeners here when integration is added. + } + }, []) +}