All files / hooks useTouchControls.ts

96.07% Statements 49/51
86.95% Branches 20/23
100% Functions 6/6
100% Lines 48/48

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250                                                                                                                                                                                                                      98x 98x 98x         98x 11x   11x 11x 11x 11x     11x 1x 1x                     98x 8x   8x 8x 8x     8x 8x 8x     8x     8x   5x     5x   3x 2x                 1x                     2x 1x                 1x                   3x   3x                   8x           98x 1x 1x 1x           98x 87x   54x       54x 54x 54x   54x 54x 54x 54x       98x        
/**
 * Touch Controls Hook
 * 
 * Manages touch event handling and gesture recognition for mobile gameplay
 * Provides swipe detection, multi-touch support, and touch-based movement
 * 
 * @module hooks/useTouchControls
 * @category Mobile Controls
 * @korean 터치 컨트롤 훅
 */
 
import { useCallback, useEffect, useRef, useState } from 'react';
 
/**
 * Gesture types supported by the touch control system
 */
export type GestureType =
  | 'swipe-right'
  | 'swipe-left'
  | 'swipe-up'
  | 'swipe-down'
  | 'two-finger-tap'
  | 'tap';
 
/**
 * Gesture event data
 */
export interface GestureEvent {
  /** Type of gesture detected */
  readonly type: GestureType;
  /** Distance of swipe in pixels (for swipe gestures) */
  readonly distance?: number;
  /** Coordinates of touch start */
  readonly startX?: number;
  readonly startY?: number;
  /** Coordinates of touch end */
  readonly endX?: number;
  readonly endY?: number;
}
 
/**
 * Props for useTouchControls hook
 */
export interface UseTouchControlsProps {
  /** Callback when gesture is detected */
  readonly onGesture: (gesture: GestureEvent) => void;
  /** Whether touch input is enabled */
  readonly enabled?: boolean;
  /** Minimum swipe distance in pixels (default: 50) */
  readonly minSwipeDistance?: number;
  /** Maximum time for tap in ms (default: 300) */
  readonly maxTapDuration?: number;
}
 
/**
 * Return type for useTouchControls hook
 */
export interface UseTouchControlsReturn {
  /** Whether a touch is currently active */
  readonly isTouching: boolean;
}
 
/**
 * Custom hook for handling touch controls and gesture recognition
 * 
 * Features:
 * - Swipe detection (horizontal and vertical)
 * - Two-finger tap detection for vital point mode
 * - Single tap detection
 * - Distance calculation for swipe intensity
 * - Configurable thresholds
 * 
 * Gesture Mapping:
 * - Swipe Right: Advance toward opponent
 * - Swipe Left: Retreat from opponent
 * - Swipe Up: High stance mode
 * - Swipe Down: Low stance mode
 * - Two-Finger Tap: Activate vital point targeting mode
 * - Single Tap: Context-specific action
 * 
 * @example
 * ```typescript
 * const { isTouching } = useTouchControls({
 *   onGesture: (gesture) => {
 *     switch (gesture.type) {
 *       case 'swipe-right':
 *         handleAdvance();
 *         break;
 *       case 'two-finger-tap':
 *         activateVitalPointMode();
 *         break;
 *     }
 *   },
 *   enabled: !isPaused,
 *   minSwipeDistance: 50,
 * });
 * ```
 * 
 * @public
 * @korean 터치컨트롤사용
 */
export function useTouchControls({
  onGesture,
  enabled = true,
  minSwipeDistance = 50,
  maxTapDuration = 300,
}: UseTouchControlsProps): UseTouchControlsReturn {
  const touchStartRef = useRef<Touch | null>(null);
  const touchStartTimeRef = useRef<number>(0);
  const [isTouching, setIsTouching] = useState<boolean>(false);
 
  /**
   * Handle touch start event
   */
  const handleTouchStart = useCallback((e: TouchEvent) => {
    Iif (!enabled) return;
 
    const touch = e.touches[0];
    touchStartRef.current = touch;
    touchStartTimeRef.current = Date.now();
    setIsTouching(true);
 
    // Check for two-finger tap immediately
    if (e.touches.length === 2) {
      e.preventDefault();
      onGesture({
        type: 'two-finger-tap',
        startX: touch.clientX,
        startY: touch.clientY,
      });
    }
  }, [enabled, onGesture]);
 
  /**
   * Handle touch end event
   */
  const handleTouchEnd = useCallback((e: TouchEvent) => {
    Iif (!enabled || !touchStartRef.current) return;
 
    const touchEnd = e.changedTouches[0];
    const touchStart = touchStartRef.current;
    const touchDuration = Date.now() - touchStartTimeRef.current;
 
    // Calculate deltas
    const deltaX = touchEnd.clientX - touchStart.clientX;
    const deltaY = touchEnd.clientY - touchStart.clientY;
    const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
 
    // Reset touch state
    setIsTouching(false);
 
    // Detect gesture type
    if (distance >= minSwipeDistance) {
      // Swipe gesture
      e.preventDefault();
 
      // Determine primary direction
      if (Math.abs(deltaX) > Math.abs(deltaY)) {
        // Horizontal swipe
        if (deltaX > 0) {
          onGesture({
            type: 'swipe-right',
            distance,
            startX: touchStart.clientX,
            startY: touchStart.clientY,
            endX: touchEnd.clientX,
            endY: touchEnd.clientY,
          });
        } else {
          onGesture({
            type: 'swipe-left',
            distance,
            startX: touchStart.clientX,
            startY: touchStart.clientY,
            endX: touchEnd.clientX,
            endY: touchEnd.clientY,
          });
        }
      } else {
        // Vertical swipe
        if (deltaY > 0) {
          onGesture({
            type: 'swipe-down',
            distance,
            startX: touchStart.clientX,
            startY: touchStart.clientY,
            endX: touchEnd.clientX,
            endY: touchEnd.clientY,
          });
        } else {
          onGesture({
            type: 'swipe-up',
            distance,
            startX: touchStart.clientX,
            startY: touchStart.clientY,
            endX: touchEnd.clientX,
            endY: touchEnd.clientY,
          });
        }
      }
    E} else if (touchDuration <= maxTapDuration) {
      // Tap gesture (short duration, small distance)
      onGesture({
        type: 'tap',
        startX: touchStart.clientX,
        startY: touchStart.clientY,
        endX: touchEnd.clientX,
        endY: touchEnd.clientY,
      });
    }
 
    // Clear touch start reference
    touchStartRef.current = null;
  }, [enabled, minSwipeDistance, maxTapDuration, onGesture]);
 
  /**
   * Handle touch cancel event
   */
  const handleTouchCancel = useCallback(() => {
    touchStartRef.current = null;
    touchStartTimeRef.current = 0;
    setIsTouching(false);
  }, []);
 
  /**
   * Setup touch event listeners
   */
  useEffect(() => {
    if (!enabled) return;
 
    const options: AddEventListenerOptions = {
      passive: false, // Allow preventDefault for gesture handling
    };
 
    document.addEventListener('touchstart', handleTouchStart, options);
    document.addEventListener('touchend', handleTouchEnd, options);
    document.addEventListener('touchcancel', handleTouchCancel, options);
 
    return () => {
      document.removeEventListener('touchstart', handleTouchStart);
      document.removeEventListener('touchend', handleTouchEnd);
      document.removeEventListener('touchcancel', handleTouchCancel);
    };
  }, [enabled, handleTouchStart, handleTouchEnd, handleTouchCancel]);
 
  return {
    isTouching,
  };
}