diff --git a/src/hooks/useSwipeControls.ts b/src/hooks/useSwipeControls.ts index c76c845..824afc4 100644 --- a/src/hooks/useSwipeControls.ts +++ b/src/hooks/useSwipeControls.ts @@ -19,6 +19,7 @@ interface UseSwipeControlsOptions { /** * Hook for detecting swipe gestures on touch devices * Supports directional swipes and fast swipe detection (for hard drop in Tetris) + * Attaches to document for full-screen touch detection */ export const useSwipeControls = ({ onSwipe, @@ -28,78 +29,82 @@ export const useSwipeControls = ({ enabled = true, }: UseSwipeControlsOptions) => { const swipeStateRef = useRef(null); + const onSwipeRef = useRef(onSwipe); + const onTapRef = useRef(onTap); - const handleTouchStart = useCallback((e: TouchEvent) => { + // Keep refs updated + useEffect(() => { + onSwipeRef.current = onSwipe; + onTapRef.current = onTap; + }, [onSwipe, onTap]); + + useEffect(() => { if (!enabled) return; - const touch = e.touches[0]; - swipeStateRef.current = { - startX: touch.clientX, - startY: touch.clientY, - startTime: Date.now(), - }; - }, [enabled]); - const handleTouchEnd = useCallback((e: TouchEvent) => { - if (!enabled || !swipeStateRef.current) return; - - const touch = e.changedTouches[0]; - const { startX, startY, startTime } = swipeStateRef.current; - - const deltaX = touch.clientX - startX; - const deltaY = touch.clientY - startY; - const deltaTime = Date.now() - startTime; - - const absX = Math.abs(deltaX); - const absY = Math.abs(deltaY); - const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - const speed = distance / Math.max(deltaTime, 1); - - // If movement is too small, treat as tap - if (distance < minSwipeDistance) { - onTap?.(); + const handleTouchStart = (e: TouchEvent) => { + const touch = e.touches[0]; + swipeStateRef.current = { + startX: touch.clientX, + startY: touch.clientY, + startTime: Date.now(), + }; + }; + + const handleTouchEnd = (e: TouchEvent) => { + if (!swipeStateRef.current) return; + + const touch = e.changedTouches[0]; + const { startX, startY, startTime } = swipeStateRef.current; + + const deltaX = touch.clientX - startX; + const deltaY = touch.clientY - startY; + const deltaTime = Date.now() - startTime; + + const absX = Math.abs(deltaX); + const absY = Math.abs(deltaY); + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + const speed = distance / Math.max(deltaTime, 1); + + // If movement is too small, treat as tap + if (distance < minSwipeDistance) { + onTapRef.current?.(); + swipeStateRef.current = null; + return; + } + + const isFastSwipe = speed > fastSwipeThreshold; + + // Determine direction based on larger axis + let direction: SwipeDirection; + if (absX > absY) { + direction = deltaX > 0 ? 'right' : 'left'; + } else { + direction = deltaY > 0 ? 'down' : 'up'; + } + + onSwipeRef.current(direction, isFastSwipe); swipeStateRef.current = null; - return; - } - - const isFastSwipe = speed > fastSwipeThreshold; - - // Determine direction based on larger axis - let direction: SwipeDirection; - if (absX > absY) { - direction = deltaX > 0 ? 'right' : 'left'; - } else { - direction = deltaY > 0 ? 'down' : 'up'; - } - - onSwipe(direction, isFastSwipe); - swipeStateRef.current = null; - }, [enabled, minSwipeDistance, fastSwipeThreshold, onSwipe, onTap]); - - const handleTouchCancel = useCallback(() => { - swipeStateRef.current = null; - }, []); - - // Returns handlers to attach to an element - const getSwipeHandlers = useCallback(() => ({ - onTouchStart: (e: React.TouchEvent) => handleTouchStart(e.nativeEvent), - onTouchEnd: (e: React.TouchEvent) => handleTouchEnd(e.nativeEvent), - onTouchCancel: () => handleTouchCancel(), - }), [handleTouchStart, handleTouchEnd, handleTouchCancel]); - - // Alternative: attach to a specific element ref - const bindToElement = useCallback((element: HTMLElement | null) => { - if (!element) return; - - element.addEventListener('touchstart', handleTouchStart, { passive: true }); - element.addEventListener('touchend', handleTouchEnd, { passive: true }); - element.addEventListener('touchcancel', handleTouchCancel, { passive: true }); - - return () => { - element.removeEventListener('touchstart', handleTouchStart); - element.removeEventListener('touchend', handleTouchEnd); - element.removeEventListener('touchcancel', handleTouchCancel); }; - }, [handleTouchStart, handleTouchEnd, handleTouchCancel]); + + const handleTouchCancel = () => { + swipeStateRef.current = null; + }; + + // Attach to document for full-screen gesture detection + document.addEventListener('touchstart', handleTouchStart, { passive: true }); + document.addEventListener('touchend', handleTouchEnd, { passive: true }); + document.addEventListener('touchcancel', handleTouchCancel, { passive: true }); + + return () => { + document.removeEventListener('touchstart', handleTouchStart); + document.removeEventListener('touchend', handleTouchEnd); + document.removeEventListener('touchcancel', handleTouchCancel); + }; + }, [enabled, minSwipeDistance, fastSwipeThreshold]); + + // Legacy handlers for specific element binding (not used with document-level) + const getSwipeHandlers = useCallback(() => ({}), []); + const bindToElement = useCallback(() => () => {}, []); return { getSwipeHandlers, bindToElement }; }; diff --git a/src/pages/Games.tsx b/src/pages/Games.tsx index 3b3b052..1855cb4 100644 --- a/src/pages/Games.tsx +++ b/src/pages/Games.tsx @@ -20,7 +20,7 @@ const games = [ id: 'pacman', name: 'Pac-Man', description: 'Navigate the maze, eat dots, avoid ghosts', - hasMultiplayer: false, + hasMultiplayer: true, ascii: `┌────────┐ │· · ᗣ · │ │ ┌─┐ ┌─┐│ @@ -44,7 +44,7 @@ const games = [ id: 'breakout', name: 'Breakout', description: 'Break bricks with a bouncing ball', - hasMultiplayer: false, + hasMultiplayer: true, ascii: `┌────────┐ │████████│ │▓▓▓▓▓▓▓▓│ @@ -110,9 +110,9 @@ const Games = () => {

- {/* Multiplayer Badge */} + {/* Multiplayer Badge - using primary colors instead of purple */} {game.hasMultiplayer && ( - + 2P )} @@ -123,17 +123,13 @@ const Games = () => { {/* Footer */} -
-

- {'>'} Max score: 4,294,967,296 - (uint32 overflow) -

-

- Arrow keys / WASD to control +

+

+ Games auto-save high scores • 2P games require keyboard

); }; -export default Games; \ No newline at end of file +export default Games;