All files / hooks useActionFeedback.ts

100% Statements 66/66
81.25% Branches 13/16
100% Functions 20/20
100% Lines 61/61

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 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297                                                                                                                                                                                          2x                                                                                         110x     110x 110x 110x 110x 110x 110x     110x 110x 110x 110x     110x 10x 10x       110x         5x             5x       110x           5x               5x       110x 10x 10x     10x 5x       10x 2x         110x 1x 1x 1x 1x         110x 5x 5x     5x 1x       5x 1x         110x 1x 1x 1x 1x         110x 111x   111x 111x     111x 111x         110x 49x   49x 49x 49x   49x 4x   49x 3x         110x                                            
/**
 * useActionFeedback Hook - Player Action Feedback State Management
 * 
 * Manages state for floating damage numbers, combo counter, technique names,
 * and action indicators (Perfect, Critical, Blocked, Dodged).
 *
 * @module hooks/useActionFeedback
 * @category Combat UI
 * @korean 액션피드백
 */
 
import { useCallback, useEffect, useRef, useState } from "react";
import { Position } from "../types";
 
/**
 * Types of action feedback indicators
 */
export type ActionFeedbackType = 
  | "perfect"
  | "critical"
  | "blocked"
  | "dodged"
  | "technique"
  | "combo_milestone";
 
/**
 * Damage number type for color coding
 */
export type DamageType = "normal" | "critical" | "vital";
 
/**
 * Represents a floating damage number
 */
export interface DamageNumber {
  readonly id: string;
  readonly damage: number;
  readonly position: Position;
  readonly type: DamageType;
  readonly timestamp: number;
}
 
/**
 * Represents an action feedback indicator
 */
export interface ActionFeedback {
  readonly id: string;
  readonly type: ActionFeedbackType;
  readonly text: string;
  readonly textKorean: string;
  readonly position: Position;
  readonly timestamp: number;
}
 
/**
 * Action feedback state
 */
export interface ActionFeedbackState {
  readonly damageNumbers: DamageNumber[];
  readonly actionFeedbacks: ActionFeedback[];
  readonly comboCount: number;
  readonly lastHitTime: number;
  readonly currentTechnique: { korean: string; english: string } | null;
  readonly techniqueShowTime: number;
}
 
/**
 * Action feedback actions interface
 */
export interface ActionFeedbackActions {
  readonly addDamageNumber: (damage: number, position: Position, type?: DamageType) => void;
  readonly addActionFeedback: (type: ActionFeedbackType, text: string, textKorean: string, position: Position) => void;
  readonly incrementCombo: () => void;
  readonly resetCombo: () => void;
  readonly showTechnique: (korean: string, english: string) => void;
  readonly hideTechnique: () => void;
  readonly clearExpired: () => void;
}
 
/**
 * Configuration for useActionFeedback hook
 */
export interface UseActionFeedbackConfig {
  /** Duration in ms for damage numbers to display (default: 1500) */
  readonly damageNumberDuration?: number;
  /** Duration in ms for action feedback to display (default: 1200) */
  readonly actionFeedbackDuration?: number;
  /** Duration in ms for technique name to display (default: 2000) */
  readonly techniqueDuration?: number;
  /** Duration in ms before combo resets after no hits (default: 2000) */
  readonly comboResetTime?: number;
}
 
/** Default configuration */
const DEFAULT_CONFIG: Required<UseActionFeedbackConfig> = {
  damageNumberDuration: 1500,
  actionFeedbackDuration: 1200,
  techniqueDuration: 2000,
  comboResetTime: 2000,
};
 
/**
 * useActionFeedback Hook
 * 
 * Manages combat action feedback including:
 * - Floating damage numbers with color coding (normal, critical, vital)
 * - Combo counter with automatic reset
 * - Technique name display (Korean | English)
 * - Action indicators (Perfect, Critical, Blocked, Dodged)
 *
 * @param config - Optional configuration for durations and timing
 * @returns Action feedback state and actions
 *
 * @example
 * ```typescript
 * const { state, actions } = useActionFeedback({
 *   damageNumberDuration: 1500,
 *   comboResetTime: 2000,
 * });
 *
 * // Add damage number
 * actions.addDamageNumber(25, { x: 100, y: 200 }, 'critical');
 *
 * // Show technique name
 * actions.showTechnique('천둥벽력', 'Thunder Strike');
 *
 * // Increment combo
 * actions.incrementCombo();
 * ```
 */
export function useActionFeedback(config: UseActionFeedbackConfig = {}): {
  state: ActionFeedbackState;
  actions: ActionFeedbackActions;
} {
  const {
    damageNumberDuration,
    actionFeedbackDuration,
    techniqueDuration,
    comboResetTime,
  } = { ...DEFAULT_CONFIG, ...config };
 
  // State
  const [damageNumbers, setDamageNumbers] = useState<DamageNumber[]>([]);
  const [actionFeedbacks, setActionFeedbacks] = useState<ActionFeedback[]>([]);
  const [comboCount, setComboCount] = useState(0);
  const [lastHitTime, setLastHitTime] = useState(0);
  const [currentTechnique, setCurrentTechnique] = useState<{ korean: string; english: string } | null>(null);
  const [techniqueShowTime, setTechniqueShowTime] = useState(0);
 
  // Refs for timers
  const comboResetTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const techniqueTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const cleanupIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const idCounterRef = useRef(0);
 
  // Generate unique ID using counter and timestamp for guaranteed uniqueness
  const generateId = useCallback(() => {
    idCounterRef.current += 1;
    return `feedback_${Date.now()}_${idCounterRef.current}`;
  }, []);
 
  // Add damage number
  const addDamageNumber = useCallback((
    damage: number,
    position: Position,
    type: DamageType = "normal"
  ) => {
    const damageNumber: DamageNumber = {
      id: generateId(),
      damage,
      position,
      type,
      timestamp: Date.now(),
    };
    setDamageNumbers(prev => [...prev, damageNumber]);
  }, [generateId]);
 
  // Add action feedback
  const addActionFeedback = useCallback((
    type: ActionFeedbackType,
    text: string,
    textKorean: string,
    position: Position
  ) => {
    const feedback: ActionFeedback = {
      id: generateId(),
      type,
      text,
      textKorean,
      position,
      timestamp: Date.now(),
    };
    setActionFeedbacks(prev => [...prev, feedback]);
  }, [generateId]);
 
  // Increment combo counter
  const incrementCombo = useCallback(() => {
    setComboCount(prev => prev + 1);
    setLastHitTime(Date.now());
 
    // Clear existing reset timer
    if (comboResetTimerRef.current) {
      clearTimeout(comboResetTimerRef.current);
    }
 
    // Set new reset timer
    comboResetTimerRef.current = setTimeout(() => {
      setComboCount(0);
    }, comboResetTime);
  }, [comboResetTime]);
 
  // Reset combo counter
  const resetCombo = useCallback(() => {
    setComboCount(0);
    Eif (comboResetTimerRef.current) {
      clearTimeout(comboResetTimerRef.current);
      comboResetTimerRef.current = null;
    }
  }, []);
 
  // Show technique name
  const showTechnique = useCallback((korean: string, english: string) => {
    setCurrentTechnique({ korean, english });
    setTechniqueShowTime(Date.now());
 
    // Clear existing timer
    if (techniqueTimerRef.current) {
      clearTimeout(techniqueTimerRef.current);
    }
 
    // Set hide timer
    techniqueTimerRef.current = setTimeout(() => {
      setCurrentTechnique(null);
    }, techniqueDuration);
  }, [techniqueDuration]);
 
  // Hide technique name
  const hideTechnique = useCallback(() => {
    setCurrentTechnique(null);
    Eif (techniqueTimerRef.current) {
      clearTimeout(techniqueTimerRef.current);
      techniqueTimerRef.current = null;
    }
  }, []);
 
  // Clear expired items
  const clearExpired = useCallback(() => {
    const now = Date.now();
 
    setDamageNumbers(prev =>
      prev.filter(d => now - d.timestamp < damageNumberDuration)
    );
 
    setActionFeedbacks(prev =>
      prev.filter(f => now - f.timestamp < actionFeedbackDuration)
    );
  }, [damageNumberDuration, actionFeedbackDuration]);
 
  // Setup cleanup interval
  useEffect(() => {
    cleanupIntervalRef.current = setInterval(clearExpired, 100);
 
    return () => {
      Eif (cleanupIntervalRef.current) {
        clearInterval(cleanupIntervalRef.current);
      }
      if (comboResetTimerRef.current) {
        clearTimeout(comboResetTimerRef.current);
      }
      if (techniqueTimerRef.current) {
        clearTimeout(techniqueTimerRef.current);
      }
    };
  }, [clearExpired]);
 
  return {
    state: {
      damageNumbers,
      actionFeedbacks,
      comboCount,
      lastHitTime,
      currentTechnique,
      techniqueShowTime,
    },
    actions: {
      addDamageNumber,
      addActionFeedback,
      incrementCombo,
      resetCombo,
      showTechnique,
      hideTechnique,
      clearExpired,
    },
  };
}
 
export default useActionFeedback;