refactor: add common controller interface
parent
212bda8d5e
commit
07e5d4ee4d
15
src/App.tsx
15
src/App.tsx
|
|
@ -1,7 +1,20 @@
|
||||||
import { AppRoutes } from './app/AppRoutes'
|
import { AppRoutes } from './app/AppRoutes'
|
||||||
|
import { useKeyboardInput } from './input/useKeyboardInput'
|
||||||
|
import { usePoseInput } from './input/usePoseInput'
|
||||||
|
|
||||||
|
function InputControllers() {
|
||||||
|
useKeyboardInput()
|
||||||
|
usePoseInput()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return <AppRoutes />
|
return (
|
||||||
|
<>
|
||||||
|
<InputControllers />
|
||||||
|
<AppRoutes />
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import type { CSSProperties } from 'react'
|
import type { CSSProperties } from 'react'
|
||||||
|
import { useGameInput } from '../../input/useGameInput'
|
||||||
import { ARHud } from './ARHud'
|
import { ARHud } from './ARHud'
|
||||||
import { SONGS } from './song-data'
|
import { SONGS } from './song-data'
|
||||||
import { SongCarousel } from './SongCarousel'
|
import { SongCarousel } from './SongCarousel'
|
||||||
|
|
@ -14,21 +15,21 @@ function wrapIndex(index: number, length: number): number {
|
||||||
export function SongSelectScreen() {
|
export function SongSelectScreen() {
|
||||||
const [currentIndex, setCurrentIndex] = useState(0)
|
const [currentIndex, setCurrentIndex] = useState(0)
|
||||||
const activeSong = SONGS[currentIndex]
|
const activeSong = SONGS[currentIndex]
|
||||||
|
const handleConfirm = () => {}
|
||||||
|
|
||||||
useEffect(() => {
|
useGameInput((action) => {
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
if (action === 'LEFT') {
|
||||||
if (event.key === 'ArrowLeft') {
|
|
||||||
setCurrentIndex((prev) => wrapIndex(prev - 1, SONGS.length))
|
setCurrentIndex((prev) => wrapIndex(prev - 1, SONGS.length))
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'ArrowRight') {
|
|
||||||
setCurrentIndex((prev) => wrapIndex(prev + 1, SONGS.length))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('keydown', onKeyDown)
|
if (action === 'RIGHT') {
|
||||||
return () => window.removeEventListener('keydown', onKeyDown)
|
setCurrentIndex((prev) => wrapIndex(prev + 1, SONGS.length))
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
|
if (action === 'BUTTON_A') {
|
||||||
|
handleConfirm()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const cssVars = useMemo(
|
const cssVars = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -60,7 +61,7 @@ export function SongSelectScreen() {
|
||||||
|
|
||||||
<SongInfoBar
|
<SongInfoBar
|
||||||
song={activeSong}
|
song={activeSong}
|
||||||
onConfirm={() => {}}
|
onConfirm={handleConfirm}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export type GameAction = 'LEFT' | 'RIGHT' | 'UP' | 'DOWN' | 'BUTTON_A' | 'BUTTON_B'
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import type { GameAction } from './actions'
|
||||||
|
|
||||||
|
type GameActionListener = (action: GameAction) => void
|
||||||
|
|
||||||
|
const listeners = new Set<GameActionListener>()
|
||||||
|
|
||||||
|
export function emit(action: GameAction): void {
|
||||||
|
listeners.forEach((listener) => listener(action))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribe(listener: GameActionListener): () => void {
|
||||||
|
listeners.add(listener)
|
||||||
|
return () => {
|
||||||
|
listeners.delete(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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])
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import type { GameAction } from './actions'
|
||||||
|
import { emit } from './inputBus'
|
||||||
|
|
||||||
|
const KEY_TO_ACTION: Partial<Record<string, GameAction>> = {
|
||||||
|
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)
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { emit } from './inputBus'
|
||||||
|
|
||||||
|
export function usePoseInput(): void {
|
||||||
|
useEffect(() => {
|
||||||
|
const emitAction = (action: Parameters<typeof emit>[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.
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue