blacktrigram 0.7.6 → 0.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/ARCHITECTURE.md +2404 -0
  2. package/COMBAT_ARCHITECTURE.md +3322 -0
  3. package/CONTROLS.md +639 -0
  4. package/CRA-ASSESSMENT.md +508 -0
  5. package/DATA_MODEL.md +675 -0
  6. package/ISMS_REFERENCE_MAPPING.md +513 -0
  7. package/README.md +1 -1
  8. package/SECURITY_ARCHITECTURE.md +1160 -0
  9. package/THREAT_MODEL.md +1163 -0
  10. package/lib/audio/AudioAssetLoader.js.map +1 -1
  11. package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
  12. package/lib/components/screens/combat/hooks/useCombatAudio.js.map +1 -1
  13. package/lib/components/screens/intro/IntroScreen3D.js +1 -1
  14. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  15. package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
  16. package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
  17. package/lib/components/shared/ui/SplashScreen.js +2 -2
  18. package/lib/data/archetypeClothing.js +1 -1
  19. package/lib/data/archetypePhysicalAttributes.js +158 -1
  20. package/lib/data/archetypePhysicalAttributes.js.map +1 -1
  21. package/lib/data/index.d.ts +14 -0
  22. package/lib/data/index.d.ts.map +1 -0
  23. package/lib/data/index.js +43 -0
  24. package/lib/data/index.js.map +1 -0
  25. package/lib/data/techniqueMappings.js +47 -2
  26. package/lib/data/techniqueMappings.js.map +1 -1
  27. package/lib/data/techniques.js +1 -1
  28. package/lib/hooks/index.d.ts +29 -0
  29. package/lib/hooks/index.d.ts.map +1 -0
  30. package/lib/hooks/index.js +53 -0
  31. package/lib/hooks/index.js.map +1 -0
  32. package/lib/hooks/useCombatTimer.js.map +1 -1
  33. package/lib/hooks/useDebounce.js +52 -0
  34. package/lib/hooks/useDebounce.js.map +1 -0
  35. package/lib/hooks/usePauseMenu.js +60 -0
  36. package/lib/hooks/usePauseMenu.js.map +1 -0
  37. package/lib/hooks/useResponsiveLayout.js +160 -0
  38. package/lib/hooks/useResponsiveLayout.js.map +1 -0
  39. package/lib/hooks/useRoundTransition.js.map +1 -1
  40. package/lib/hooks/useTechniqueSelection.js.map +1 -1
  41. package/lib/hooks/useThrottle.js.map +1 -1
  42. package/lib/hooks/useWebGLContextLossHandler.js +36 -1
  43. package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
  44. package/lib/hooks/useWindowSize.js +19 -1
  45. package/lib/hooks/useWindowSize.js.map +1 -1
  46. package/lib/index.d.ts +2 -0
  47. package/lib/index.d.ts.map +1 -1
  48. package/lib/index.js +3 -1
  49. package/package.json +22 -6
@@ -734,6 +734,6 @@ function getTechniqueById(techniqueId) {
734
734
  ].find((tech) => tech.id === techniqueId);
735
735
  }
736
736
  //#endregion
737
- export { getTechniqueById, getTechniquesForStanceAndArchetype };
737
+ export { AMSALJA_TECHNIQUES, HACKER_TECHNIQUES, JEONGBO_YOWON_TECHNIQUES, JOJIK_POKRYEOKBAE_TECHNIQUES, MUSA_TECHNIQUES, getTechniqueById, getTechniquesForArchetype, getTechniquesForStanceAndArchetype };
738
738
 
739
739
  //# sourceMappingURL=techniques.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Custom React hooks for Black Trigram (흑괘)
3
+ *
4
+ * Provides reusable hooks for combat mechanics, animations, input handling,
5
+ * layout management, and performance optimization.
6
+ *
7
+ * @module hooks
8
+ * @category Hooks
9
+ */
10
+ export * from "./useActionFeedback";
11
+ export * from "./useBalanceAnimations";
12
+ export * from "./useCombatTimer";
13
+ export * from "./useDebounce";
14
+ export * from "./useHUDLayout";
15
+ export * from "./useHandPoseTransitions";
16
+ export * from "./useKeyboardControls";
17
+ export * from "./useMatchCountdown";
18
+ export * from "./useMuscleActivation";
19
+ export * from "./usePauseMenu";
20
+ export * from "./usePlayerAnimation";
21
+ export * from "./useResponsiveLayout";
22
+ export * from "./useRoundTransition";
23
+ export * from "./useSkeletalAnimation";
24
+ export * from "./useTechniqueSelection";
25
+ export * from "./useThrottle";
26
+ export * from "./useTouchControls";
27
+ export * from "./useWebGLContextLossHandler";
28
+ export * from "./useWindowSize";
29
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,iBAAiB,CAAC"}
@@ -0,0 +1,53 @@
1
+ import { __exportAll } from "../_virtual/_rolldown/runtime.js";
2
+ import useActionFeedback from "./useActionFeedback.js";
3
+ import { useCombatTimer } from "./useCombatTimer.js";
4
+ import { useKeyboardControls } from "./useKeyboardControls.js";
5
+ import { usePlayerAnimation } from "./usePlayerAnimation.js";
6
+ import useRoundTransition from "./useRoundTransition.js";
7
+ import useTechniqueSelection from "./useTechniqueSelection.js";
8
+ import { getWebGLContextState, isWebGL2Available, isWebGLAvailable, useWebGLContextLossHandler } from "./useWebGLContextLossHandler.js";
9
+ import useMatchCountdown from "./useMatchCountdown.js";
10
+ import useThrottle from "./useThrottle.js";
11
+ import { useTouchControls } from "./useTouchControls.js";
12
+ import { useBalanceAnimations } from "./useBalanceAnimations.js";
13
+ import { useHandPoseTransitions } from "./useHandPoseTransitions.js";
14
+ import { useMuscleActivation } from "./useMuscleActivation.js";
15
+ import { useSkeletalAnimation } from "./useSkeletalAnimation.js";
16
+ import { useHUDLayout } from "./useHUDLayout.js";
17
+ import useDebounce from "./useDebounce.js";
18
+ import { usePauseMenu } from "./usePauseMenu.js";
19
+ import { BREAKPOINTS, useContentArea, useResponsiveLayout } from "./useResponsiveLayout.js";
20
+ import useWindowSize, { useIsMobile } from "./useWindowSize.js";
21
+ //#region src/hooks/index.ts
22
+ var hooks_exports = /* @__PURE__ */ __exportAll({
23
+ BREAKPOINTS: () => BREAKPOINTS,
24
+ MIN_CONTEXT_RECOVERY_DELAY: () => 100,
25
+ getWebGLContextState: () => getWebGLContextState,
26
+ isWebGL2Available: () => isWebGL2Available,
27
+ isWebGLAvailable: () => isWebGLAvailable,
28
+ useActionFeedback: () => useActionFeedback,
29
+ useBalanceAnimations: () => useBalanceAnimations,
30
+ useCombatTimer: () => useCombatTimer,
31
+ useContentArea: () => useContentArea,
32
+ useDebounce: () => useDebounce,
33
+ useHUDLayout: () => useHUDLayout,
34
+ useHandPoseTransitions: () => useHandPoseTransitions,
35
+ useIsMobile: () => useIsMobile,
36
+ useKeyboardControls: () => useKeyboardControls,
37
+ useMatchCountdown: () => useMatchCountdown,
38
+ useMuscleActivation: () => useMuscleActivation,
39
+ usePauseMenu: () => usePauseMenu,
40
+ usePlayerAnimation: () => usePlayerAnimation,
41
+ useResponsiveLayout: () => useResponsiveLayout,
42
+ useRoundTransition: () => useRoundTransition,
43
+ useSkeletalAnimation: () => useSkeletalAnimation,
44
+ useTechniqueSelection: () => useTechniqueSelection,
45
+ useThrottle: () => useThrottle,
46
+ useTouchControls: () => useTouchControls,
47
+ useWebGLContextLossHandler: () => useWebGLContextLossHandler,
48
+ useWindowSize: () => useWindowSize
49
+ });
50
+ //#endregion
51
+ export { hooks_exports };
52
+
53
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/hooks/index.ts"],"sourcesContent":["/**\n * Custom React hooks for Black Trigram (흑괘)\n *\n * Provides reusable hooks for combat mechanics, animations, input handling,\n * layout management, and performance optimization.\n *\n * @module hooks\n * @category Hooks\n */\n\nexport * from \"./useActionFeedback\";\nexport * from \"./useBalanceAnimations\";\nexport * from \"./useCombatTimer\";\nexport * from \"./useDebounce\";\nexport * from \"./useHUDLayout\";\nexport * from \"./useHandPoseTransitions\";\nexport * from \"./useKeyboardControls\";\nexport * from \"./useMatchCountdown\";\nexport * from \"./useMuscleActivation\";\nexport * from \"./usePauseMenu\";\nexport * from \"./usePlayerAnimation\";\nexport * from \"./useResponsiveLayout\";\nexport * from \"./useRoundTransition\";\nexport * from \"./useSkeletalAnimation\";\nexport * from \"./useTechniqueSelection\";\nexport * from \"./useThrottle\";\nexport * from \"./useTouchControls\";\nexport * from \"./useWebGLContextLossHandler\";\nexport * from \"./useWindowSize\";\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"useCombatTimer.js","names":[],"sources":["../../src/hooks/useCombatTimer.ts"],"sourcesContent":["/**\n * useCombatTimer - Hook for managing combat round timer\n *\n * Korean: 전투 라운드 타이머 훅 (Combat Round Timer Hook)\n *\n * Manages countdown timer for combat rounds with:\n * - Pause/resume support\n * - Warning thresholds at 10s and 5s\n * - Audio alerts for warnings\n * - Time's up callback\n *\n * @module hooks/useCombatTimer\n * @category Combat Hooks\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { useAudio } from \"../audio/AudioProvider\";\n\n/**\n * Timer warning level indicating urgency\n */\nexport type TimerWarningLevel = \"none\" | \"warning\" | \"urgent\";\n\n/**\n * Configuration for the combat timer hook\n */\nexport interface UseCombatTimerConfig {\n /** Initial time in seconds */\n readonly initialTime: number;\n /** Whether the timer is paused */\n readonly isPaused: boolean;\n /** Callback when timer reaches 0 */\n readonly onTimeUp: () => void;\n /** Warning threshold in seconds (default: 10) */\n readonly warningThreshold?: number;\n /** Urgent warning threshold in seconds (default: 5) */\n readonly urgentThreshold?: number;\n /** Optional key to force timer reset (e.g., round number) */\n readonly resetKey?: string;\n}\n\n/**\n * Return value from useCombatTimer hook\n */\nexport interface UseCombatTimerReturn {\n /** Current time remaining in seconds */\n readonly timeRemaining: number;\n /** Current warning level */\n readonly warningLevel: TimerWarningLevel;\n /** Whether timer has reached 0 */\n readonly isTimeUp: boolean;\n /** Formatted time string (MM:SS) */\n readonly formattedTime: string;\n}\n\n/**\n * Format seconds into MM:SS format\n * @param seconds - Time in seconds\n * @returns Formatted string (e.g., \"03:45\", \"00:05\")\n */\nfunction formatTime(seconds: number): string {\n const minutes = Math.floor(seconds / 60);\n const remainingSeconds = Math.floor(seconds % 60);\n return `${minutes.toString().padStart(2, \"0\")}:${remainingSeconds\n .toString()\n .padStart(2, \"0\")}`;\n}\n\n/**\n * Get warning level based on time remaining\n */\nfunction getWarningLevel(\n timeRemaining: number,\n warningThreshold: number,\n urgentThreshold: number,\n): TimerWarningLevel {\n if (timeRemaining <= urgentThreshold) {\n return \"urgent\";\n }\n if (timeRemaining <= warningThreshold) {\n return \"warning\";\n }\n return \"none\";\n}\n\n/**\n * useCombatTimer Hook\n *\n * Manages combat round countdown timer with pause support and audio warnings.\n *\n * Features:\n * - Counts down from initial time to 0\n * - Pauses/resumes based on isPaused prop\n * - Plays audio warning at warning threshold (default 10s)\n * - Plays urgent audio warning at urgent threshold (default 5s)\n * - Calls onTimeUp when timer reaches 0\n * - Provides formatted time string (MM:SS)\n * - Returns current warning level for UI styling\n *\n * Korean: 전투 라운드 타이머 관리 훅\n *\n * @example\n * ```tsx\n * const { timeRemaining, warningLevel, formattedTime } = useCombatTimer({\n * initialTime: 180, // 3 minutes\n * isPaused: false,\n * onTimeUp: () => handleRoundEnd(),\n * warningThreshold: 10,\n * urgentThreshold: 5,\n * });\n * ```\n */\nexport function useCombatTimer(\n config: UseCombatTimerConfig,\n): UseCombatTimerReturn {\n const {\n initialTime,\n isPaused,\n onTimeUp,\n warningThreshold = 10,\n urgentThreshold = 5,\n resetKey,\n } = config;\n\n const audio = useAudio();\n const [timeRemaining, setTimeRemaining] = useState(initialTime);\n const [isTimeUp, setIsTimeUp] = useState(false);\n const lastWarningRef = useRef<TimerWarningLevel>(\"none\");\n const intervalRef = useRef<NodeJS.Timeout | null>(null);\n\n // Reset timer when initialTime or resetKey changes (new round)\n useEffect(() => {\n setTimeRemaining(initialTime);\n setIsTimeUp(false);\n lastWarningRef.current = \"none\";\n }, [initialTime, resetKey]);\n\n // Timer countdown logic\n useEffect(() => {\n // Don't run if paused or time is up\n if (isPaused || isTimeUp) {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n intervalRef.current = null;\n }\n return;\n }\n\n // Track start time for precise elapsed time calculation\n const startTimeRef = Date.now();\n const startingTimeRemaining = timeRemaining;\n\n // Start interval\n intervalRef.current = setInterval(() => {\n const elapsed = (Date.now() - startTimeRef) / 1000;\n const next = Math.max(0, startingTimeRemaining - elapsed);\n\n setTimeRemaining(next);\n\n // Check if time just reached 0\n if (next <= 0 && !isTimeUp) {\n setIsTimeUp(true);\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n intervalRef.current = null;\n }\n onTimeUp();\n }\n }, 100);\n\n return () => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n intervalRef.current = null;\n }\n };\n // timeRemaining is intentionally excluded - we capture the starting value once\n // and count down from there, not restart the interval on every tick\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isPaused, isTimeUp, onTimeUp]);\n\n // Warning level calculation\n const warningLevel = getWarningLevel(\n timeRemaining,\n warningThreshold,\n urgentThreshold,\n );\n\n // Audio warnings - timeRemaining checked via warningLevel which captures threshold transitions\n useEffect(() => {\n if (!audio.isAudioReady) return;\n if (isPaused) return;\n\n // Play warning sound at threshold\n if (\n warningLevel === \"warning\" &&\n lastWarningRef.current === \"none\" &&\n timeRemaining <= warningThreshold &&\n timeRemaining > urgentThreshold\n ) {\n audio.playSFX(\"attack_light\"); // Placeholder - will be timer_warning_10s\n lastWarningRef.current = \"warning\";\n }\n\n // Play urgent warning sound at urgent threshold\n if (\n warningLevel === \"urgent\" &&\n lastWarningRef.current !== \"urgent\" &&\n timeRemaining <= urgentThreshold &&\n timeRemaining > 0\n ) {\n audio.playSFX(\"attack_heavy\"); // Placeholder - will be timer_warning_5s\n lastWarningRef.current = \"urgent\";\n }\n }, [\n warningLevel,\n timeRemaining,\n audio,\n isPaused,\n warningThreshold,\n urgentThreshold,\n ]);\n\n // Format time for display\n const formattedTime = formatTime(timeRemaining);\n\n return {\n timeRemaining,\n warningLevel,\n isTimeUp,\n formattedTime,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA4DA,SAAS,WAAW,SAAyB;CAC3C,MAAM,UAAU,KAAK,MAAM,UAAU,GAAG;CACxC,MAAM,mBAAmB,KAAK,MAAM,UAAU,GAAG;AACjD,QAAO,GAAG,QAAQ,UAAU,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,iBAC9C,UAAU,CACV,SAAS,GAAG,IAAI;;;;;AAMrB,SAAS,gBACP,eACA,kBACA,iBACmB;AACnB,KAAI,iBAAiB,gBACnB,QAAO;AAET,KAAI,iBAAiB,iBACnB,QAAO;AAET,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BT,SAAgB,eACd,QACsB;CACtB,MAAM,EACJ,aACA,UACA,UACA,mBAAmB,IACnB,kBAAkB,GAClB,aACE;CAEJ,MAAM,QAAQ,UAAU;CACxB,MAAM,CAAC,eAAe,oBAAoB,SAAS,YAAY;CAC/D,MAAM,CAAC,UAAU,eAAe,SAAS,MAAM;CAC/C,MAAM,iBAAiB,OAA0B,OAAO;CACxD,MAAM,cAAc,OAA8B,KAAK;AAGvD,iBAAgB;AACd,mBAAiB,YAAY;AAC7B,cAAY,MAAM;AAClB,iBAAe,UAAU;IACxB,CAAC,aAAa,SAAS,CAAC;AAG3B,iBAAgB;AAEd,MAAI,YAAY,UAAU;AACxB,OAAI,YAAY,SAAS;AACvB,kBAAc,YAAY,QAAQ;AAClC,gBAAY,UAAU;;AAExB;;EAIF,MAAM,eAAe,KAAK,KAAK;EAC/B,MAAM,wBAAwB;AAG9B,cAAY,UAAU,kBAAkB;GACtC,MAAM,WAAW,KAAK,KAAK,GAAG,gBAAgB;GAC9C,MAAM,OAAO,KAAK,IAAI,GAAG,wBAAwB,QAAQ;AAEzD,oBAAiB,KAAK;AAGtB,OAAI,QAAQ,KAAK,CAAC,UAAU;AAC1B,gBAAY,KAAK;AACjB,QAAI,YAAY,SAAS;AACvB,mBAAc,YAAY,QAAQ;AAClC,iBAAY,UAAU;;AAExB,cAAU;;KAEX,IAAI;AAEP,eAAa;AACX,OAAI,YAAY,SAAS;AACvB,kBAAc,YAAY,QAAQ;AAClC,gBAAY,UAAU;;;IAMzB;EAAC;EAAU;EAAU;EAAS,CAAC;CAGlC,MAAM,eAAe,gBACnB,eACA,kBACA,gBACD;AAGD,iBAAgB;AACd,MAAI,CAAC,MAAM,aAAc;AACzB,MAAI,SAAU;AAGd,MACE,iBAAiB,aACjB,eAAe,YAAY,UAC3B,iBAAiB,oBACjB,gBAAgB,iBAChB;AACA,SAAM,QAAQ,eAAe;AAC7B,kBAAe,UAAU;;AAI3B,MACE,iBAAiB,YACjB,eAAe,YAAY,YAC3B,iBAAiB,mBACjB,gBAAgB,GAChB;AACA,SAAM,QAAQ,eAAe;AAC7B,kBAAe,UAAU;;IAE1B;EACD;EACA;EACA;EACA;EACA;EACA;EACD,CAAC;AAKF,QAAO;EACL;EACA;EACA;EACA,eANoB,WAAW,cAAc;EAO9C"}
1
+ {"version":3,"file":"useCombatTimer.js","names":[],"sources":["../../src/hooks/useCombatTimer.ts"],"sourcesContent":["/**\n * useCombatTimer - Hook for managing combat round timer\n *\n * Korean: 전투 라운드 타이머 훅 (Combat Round Timer Hook)\n *\n * Manages countdown timer for combat rounds with:\n * - Pause/resume support\n * - Warning thresholds at 10s and 5s\n * - Audio alerts for warnings\n * - Time's up callback\n *\n * @module hooks/useCombatTimer\n * @category Combat Hooks\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { useAudio } from \"../audio/AudioProvider\";\n\n/**\n * Timer warning level indicating urgency\n */\nexport type TimerWarningLevel = \"none\" | \"warning\" | \"urgent\";\n\n/**\n * Configuration for the combat timer hook\n */\nexport interface UseCombatTimerConfig {\n /** Initial time in seconds */\n readonly initialTime: number;\n /** Whether the timer is paused */\n readonly isPaused: boolean;\n /** Callback when timer reaches 0 */\n readonly onTimeUp: () => void;\n /** Warning threshold in seconds (default: 10) */\n readonly warningThreshold?: number;\n /** Urgent warning threshold in seconds (default: 5) */\n readonly urgentThreshold?: number;\n /** Optional key to force timer reset (e.g., round number) */\n readonly resetKey?: string;\n}\n\n/**\n * Return value from useCombatTimer hook\n */\nexport interface UseCombatTimerReturn {\n /** Current time remaining in seconds */\n readonly timeRemaining: number;\n /** Current warning level */\n readonly warningLevel: TimerWarningLevel;\n /** Whether timer has reached 0 */\n readonly isTimeUp: boolean;\n /** Formatted time string (MM:SS) */\n readonly formattedTime: string;\n}\n\n/**\n * Format seconds into MM:SS format\n * @param seconds - Time in seconds\n * @returns Formatted string (e.g., \"03:45\", \"00:05\")\n */\nfunction formatTime(seconds: number): string {\n const minutes = Math.floor(seconds / 60);\n const remainingSeconds = Math.floor(seconds % 60);\n return `${minutes.toString().padStart(2, \"0\")}:${remainingSeconds\n .toString()\n .padStart(2, \"0\")}`;\n}\n\n/**\n * Get warning level based on time remaining\n */\nfunction getWarningLevel(\n timeRemaining: number,\n warningThreshold: number,\n urgentThreshold: number,\n): TimerWarningLevel {\n if (timeRemaining <= urgentThreshold) {\n return \"urgent\";\n }\n if (timeRemaining <= warningThreshold) {\n return \"warning\";\n }\n return \"none\";\n}\n\n/**\n * useCombatTimer Hook\n *\n * Manages combat round countdown timer with pause support and audio warnings.\n *\n * Features:\n * - Counts down from initial time to 0\n * - Pauses/resumes based on isPaused prop\n * - Plays audio warning at warning threshold (default 10s)\n * - Plays urgent audio warning at urgent threshold (default 5s)\n * - Calls onTimeUp when timer reaches 0\n * - Provides formatted time string (MM:SS)\n * - Returns current warning level for UI styling\n *\n * Korean: 전투 라운드 타이머 관리 훅\n *\n * @example\n * ```tsx\n * const { timeRemaining, warningLevel, formattedTime } = useCombatTimer({\n * initialTime: 180, // 3 minutes\n * isPaused: false,\n * onTimeUp: () => handleRoundEnd(),\n * warningThreshold: 10,\n * urgentThreshold: 5,\n * });\n * ```\n */\nexport function useCombatTimer(\n config: UseCombatTimerConfig,\n): UseCombatTimerReturn {\n const {\n initialTime,\n isPaused,\n onTimeUp,\n warningThreshold = 10,\n urgentThreshold = 5,\n resetKey,\n } = config;\n\n const audio = useAudio();\n const [timeRemaining, setTimeRemaining] = useState(initialTime);\n const [isTimeUp, setIsTimeUp] = useState(false);\n const lastWarningRef = useRef<TimerWarningLevel>(\"none\");\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n // Reset timer when initialTime or resetKey changes (new round)\n useEffect(() => {\n setTimeRemaining(initialTime);\n setIsTimeUp(false);\n lastWarningRef.current = \"none\";\n }, [initialTime, resetKey]);\n\n // Timer countdown logic\n useEffect(() => {\n // Don't run if paused or time is up\n if (isPaused || isTimeUp) {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n intervalRef.current = null;\n }\n return;\n }\n\n // Track start time for precise elapsed time calculation\n const startTimeRef = Date.now();\n const startingTimeRemaining = timeRemaining;\n\n // Start interval\n intervalRef.current = setInterval(() => {\n const elapsed = (Date.now() - startTimeRef) / 1000;\n const next = Math.max(0, startingTimeRemaining - elapsed);\n\n setTimeRemaining(next);\n\n // Check if time just reached 0\n if (next <= 0 && !isTimeUp) {\n setIsTimeUp(true);\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n intervalRef.current = null;\n }\n onTimeUp();\n }\n }, 100);\n\n return () => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n intervalRef.current = null;\n }\n };\n // timeRemaining is intentionally excluded - we capture the starting value once\n // and count down from there, not restart the interval on every tick\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isPaused, isTimeUp, onTimeUp]);\n\n // Warning level calculation\n const warningLevel = getWarningLevel(\n timeRemaining,\n warningThreshold,\n urgentThreshold,\n );\n\n // Audio warnings - timeRemaining checked via warningLevel which captures threshold transitions\n useEffect(() => {\n if (!audio.isAudioReady) return;\n if (isPaused) return;\n\n // Play warning sound at threshold\n if (\n warningLevel === \"warning\" &&\n lastWarningRef.current === \"none\" &&\n timeRemaining <= warningThreshold &&\n timeRemaining > urgentThreshold\n ) {\n audio.playSFX(\"attack_light\"); // Placeholder - will be timer_warning_10s\n lastWarningRef.current = \"warning\";\n }\n\n // Play urgent warning sound at urgent threshold\n if (\n warningLevel === \"urgent\" &&\n lastWarningRef.current !== \"urgent\" &&\n timeRemaining <= urgentThreshold &&\n timeRemaining > 0\n ) {\n audio.playSFX(\"attack_heavy\"); // Placeholder - will be timer_warning_5s\n lastWarningRef.current = \"urgent\";\n }\n }, [\n warningLevel,\n timeRemaining,\n audio,\n isPaused,\n warningThreshold,\n urgentThreshold,\n ]);\n\n // Format time for display\n const formattedTime = formatTime(timeRemaining);\n\n return {\n timeRemaining,\n warningLevel,\n isTimeUp,\n formattedTime,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA4DA,SAAS,WAAW,SAAyB;CAC3C,MAAM,UAAU,KAAK,MAAM,UAAU,GAAG;CACxC,MAAM,mBAAmB,KAAK,MAAM,UAAU,GAAG;AACjD,QAAO,GAAG,QAAQ,UAAU,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,iBAC9C,UAAU,CACV,SAAS,GAAG,IAAI;;;;;AAMrB,SAAS,gBACP,eACA,kBACA,iBACmB;AACnB,KAAI,iBAAiB,gBACnB,QAAO;AAET,KAAI,iBAAiB,iBACnB,QAAO;AAET,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BT,SAAgB,eACd,QACsB;CACtB,MAAM,EACJ,aACA,UACA,UACA,mBAAmB,IACnB,kBAAkB,GAClB,aACE;CAEJ,MAAM,QAAQ,UAAU;CACxB,MAAM,CAAC,eAAe,oBAAoB,SAAS,YAAY;CAC/D,MAAM,CAAC,UAAU,eAAe,SAAS,MAAM;CAC/C,MAAM,iBAAiB,OAA0B,OAAO;CACxD,MAAM,cAAc,OAA8C,KAAK;AAGvE,iBAAgB;AACd,mBAAiB,YAAY;AAC7B,cAAY,MAAM;AAClB,iBAAe,UAAU;IACxB,CAAC,aAAa,SAAS,CAAC;AAG3B,iBAAgB;AAEd,MAAI,YAAY,UAAU;AACxB,OAAI,YAAY,SAAS;AACvB,kBAAc,YAAY,QAAQ;AAClC,gBAAY,UAAU;;AAExB;;EAIF,MAAM,eAAe,KAAK,KAAK;EAC/B,MAAM,wBAAwB;AAG9B,cAAY,UAAU,kBAAkB;GACtC,MAAM,WAAW,KAAK,KAAK,GAAG,gBAAgB;GAC9C,MAAM,OAAO,KAAK,IAAI,GAAG,wBAAwB,QAAQ;AAEzD,oBAAiB,KAAK;AAGtB,OAAI,QAAQ,KAAK,CAAC,UAAU;AAC1B,gBAAY,KAAK;AACjB,QAAI,YAAY,SAAS;AACvB,mBAAc,YAAY,QAAQ;AAClC,iBAAY,UAAU;;AAExB,cAAU;;KAEX,IAAI;AAEP,eAAa;AACX,OAAI,YAAY,SAAS;AACvB,kBAAc,YAAY,QAAQ;AAClC,gBAAY,UAAU;;;IAMzB;EAAC;EAAU;EAAU;EAAS,CAAC;CAGlC,MAAM,eAAe,gBACnB,eACA,kBACA,gBACD;AAGD,iBAAgB;AACd,MAAI,CAAC,MAAM,aAAc;AACzB,MAAI,SAAU;AAGd,MACE,iBAAiB,aACjB,eAAe,YAAY,UAC3B,iBAAiB,oBACjB,gBAAgB,iBAChB;AACA,SAAM,QAAQ,eAAe;AAC7B,kBAAe,UAAU;;AAI3B,MACE,iBAAiB,YACjB,eAAe,YAAY,YAC3B,iBAAiB,mBACjB,gBAAgB,GAChB;AACA,SAAM,QAAQ,eAAe;AAC7B,kBAAe,UAAU;;IAE1B;EACD;EACA;EACA;EACA;EACA;EACA;EACD,CAAC;AAKF,QAAO;EACL;EACA;EACA;EACA,eANoB,WAAW,cAAc;EAO9C"}
@@ -0,0 +1,52 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
2
+ //#region src/hooks/useDebounce.ts
3
+ /**
4
+ * useDebounce Hook
5
+ *
6
+ * Debounces a function to execute only after a delay since the last call.
7
+ * Useful for search inputs, resize handlers, and other delayed actions.
8
+ *
9
+ * Uses a ref pattern to ensure the latest callback is always called
10
+ * without recreating the debounced function on every render.
11
+ *
12
+ * @module hooks/useDebounce
13
+ * @category Performance
14
+ * @korean 디바운스 훅
15
+ */
16
+ /**
17
+ * Hook to debounce a callback function
18
+ *
19
+ * @param callback - Function to debounce
20
+ * @param delay - Delay in milliseconds before execution
21
+ * @returns Debounced function
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * const handleSearch = useDebounce((query: string) => {
26
+ * // Perform search
27
+ * }, 300);
28
+ * ```
29
+ */
30
+ function useDebounce(callback, delay) {
31
+ const timeoutRef = useRef(null);
32
+ const callbackRef = useRef(callback);
33
+ useLayoutEffect(() => {
34
+ callbackRef.current = callback;
35
+ });
36
+ useEffect(() => {
37
+ return () => {
38
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
39
+ };
40
+ }, []);
41
+ return useCallback((...args) => {
42
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
43
+ timeoutRef.current = setTimeout(() => {
44
+ callbackRef.current(...args);
45
+ timeoutRef.current = null;
46
+ }, delay);
47
+ }, [delay]);
48
+ }
49
+ //#endregion
50
+ export { useDebounce as default };
51
+
52
+ //# sourceMappingURL=useDebounce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDebounce.js","names":[],"sources":["../../src/hooks/useDebounce.ts"],"sourcesContent":["/**\n * useDebounce Hook\n * \n * Debounces a function to execute only after a delay since the last call.\n * Useful for search inputs, resize handlers, and other delayed actions.\n * \n * Uses a ref pattern to ensure the latest callback is always called\n * without recreating the debounced function on every render.\n * \n * @module hooks/useDebounce\n * @category Performance\n * @korean 디바운스 훅\n */\n\nimport { useCallback, useRef, useLayoutEffect, useEffect } from 'react';\n\n/**\n * Hook to debounce a callback function\n * \n * @param callback - Function to debounce\n * @param delay - Delay in milliseconds before execution\n * @returns Debounced function\n * \n * @example\n * ```tsx\n * const handleSearch = useDebounce((query: string) => {\n * // Perform search\n * }, 300);\n * ```\n */\nexport function useDebounce<T extends (...args: unknown[]) => void>(\n callback: T,\n delay: number\n): (...args: Parameters<T>) => void {\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const callbackRef = useRef(callback);\n \n // Keep callback ref up to date\n useLayoutEffect(() => {\n callbackRef.current = callback;\n });\n \n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n return useCallback(\n (...args: Parameters<T>) => {\n // Clear existing timeout\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n\n // Set new timeout\n timeoutRef.current = setTimeout(() => {\n callbackRef.current(...args);\n timeoutRef.current = null;\n }, delay);\n },\n [delay]\n );\n}\n\nexport default useDebounce;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,YACd,UACA,OACkC;CAClC,MAAM,aAAa,OAA6C,KAAK;CACrE,MAAM,cAAc,OAAO,SAAS;AAGpC,uBAAsB;AACpB,cAAY,UAAU;GACtB;AAGF,iBAAgB;AACd,eAAa;AACX,OAAI,WAAW,QACb,cAAa,WAAW,QAAQ;;IAGnC,EAAE,CAAC;AAEN,QAAO,aACJ,GAAG,SAAwB;AAE1B,MAAI,WAAW,QACb,cAAa,WAAW,QAAQ;AAIlC,aAAW,UAAU,iBAAiB;AACpC,eAAY,QAAQ,GAAG,KAAK;AAC5B,cAAW,UAAU;KACpB,MAAM;IAEX,CAAC,MAAM,CACR"}
@@ -0,0 +1,60 @@
1
+ import { useCallback, useState } from "react";
2
+ //#region src/hooks/usePauseMenu.ts
3
+ /**
4
+ * usePauseMenu Hook - State management for pause menu
5
+ *
6
+ * Manages:
7
+ * - Pause menu visibility
8
+ * - Active submenu (controls, settings)
9
+ * - Confirmation dialog state
10
+ */
11
+ var defaultConfirmDialog = {
12
+ isOpen: false,
13
+ title: "",
14
+ titleKorean: "",
15
+ message: "",
16
+ messageKorean: "",
17
+ onConfirm: () => {}
18
+ };
19
+ /**
20
+ * Hook for managing pause menu state
21
+ */
22
+ function usePauseMenu() {
23
+ const [activeSubmenu, setActiveSubmenu] = useState("main");
24
+ const [confirmDialog, setConfirmDialog] = useState(defaultConfirmDialog);
25
+ const showControls = useCallback(() => {
26
+ setActiveSubmenu("controls");
27
+ }, []);
28
+ const showSettings = useCallback(() => {
29
+ setActiveSubmenu("settings");
30
+ }, []);
31
+ const closeSubmenu = useCallback(() => {
32
+ setActiveSubmenu("main");
33
+ }, []);
34
+ const openConfirmDialog = useCallback((config) => {
35
+ setConfirmDialog({
36
+ ...config,
37
+ isOpen: true
38
+ });
39
+ }, []);
40
+ const closeConfirmDialog = useCallback(() => {
41
+ setConfirmDialog(defaultConfirmDialog);
42
+ }, []);
43
+ return {
44
+ activeSubmenu,
45
+ confirmDialog,
46
+ showControls,
47
+ showSettings,
48
+ closeSubmenu,
49
+ openConfirmDialog,
50
+ closeConfirmDialog,
51
+ confirmAndClose: useCallback(() => {
52
+ if (confirmDialog.onConfirm) confirmDialog.onConfirm();
53
+ closeConfirmDialog();
54
+ }, [confirmDialog, closeConfirmDialog])
55
+ };
56
+ }
57
+ //#endregion
58
+ export { usePauseMenu };
59
+
60
+ //# sourceMappingURL=usePauseMenu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePauseMenu.js","names":[],"sources":["../../src/hooks/usePauseMenu.ts"],"sourcesContent":["/**\n * usePauseMenu Hook - State management for pause menu\n * \n * Manages:\n * - Pause menu visibility\n * - Active submenu (controls, settings)\n * - Confirmation dialog state\n */\n\nimport { useCallback, useState } from \"react\";\n\nexport type PauseSubmenu = \"main\" | \"controls\" | \"settings\" | null;\n\nexport interface ConfirmDialogState {\n readonly isOpen: boolean;\n readonly title: string;\n readonly titleKorean: string;\n readonly message: string;\n readonly messageKorean: string;\n readonly onConfirm: () => void;\n}\n\nexport interface UsePauseMenuResult {\n readonly activeSubmenu: PauseSubmenu;\n readonly confirmDialog: ConfirmDialogState;\n readonly showControls: () => void;\n readonly showSettings: () => void;\n readonly closeSubmenu: () => void;\n readonly openConfirmDialog: (config: Omit<ConfirmDialogState, \"isOpen\">) => void;\n readonly closeConfirmDialog: () => void;\n readonly confirmAndClose: () => void;\n}\n\nconst defaultConfirmDialog: ConfirmDialogState = {\n isOpen: false,\n title: \"\",\n titleKorean: \"\",\n message: \"\",\n messageKorean: \"\",\n onConfirm: () => {},\n};\n\n/**\n * Hook for managing pause menu state\n */\nexport function usePauseMenu(): UsePauseMenuResult {\n const [activeSubmenu, setActiveSubmenu] = useState<PauseSubmenu>(\"main\");\n const [confirmDialog, setConfirmDialog] = useState<ConfirmDialogState>(defaultConfirmDialog);\n\n const showControls = useCallback(() => {\n setActiveSubmenu(\"controls\");\n }, []);\n\n const showSettings = useCallback(() => {\n setActiveSubmenu(\"settings\");\n }, []);\n\n const closeSubmenu = useCallback(() => {\n setActiveSubmenu(\"main\");\n }, []);\n\n const openConfirmDialog = useCallback(\n (config: Omit<ConfirmDialogState, \"isOpen\">) => {\n setConfirmDialog({\n ...config,\n isOpen: true,\n });\n },\n []\n );\n\n const closeConfirmDialog = useCallback(() => {\n setConfirmDialog(defaultConfirmDialog);\n }, []);\n\n const confirmAndClose = useCallback(() => {\n if (confirmDialog.onConfirm) {\n confirmDialog.onConfirm();\n }\n closeConfirmDialog();\n }, [confirmDialog, closeConfirmDialog]);\n\n return {\n activeSubmenu,\n confirmDialog,\n showControls,\n showSettings,\n closeSubmenu,\n openConfirmDialog,\n closeConfirmDialog,\n confirmAndClose,\n };\n}\n"],"mappings":";;;;;;;;;;AAiCA,IAAM,uBAA2C;CAC/C,QAAQ;CACR,OAAO;CACP,aAAa;CACb,SAAS;CACT,eAAe;CACf,iBAAiB;CAClB;;;;AAKD,SAAgB,eAAmC;CACjD,MAAM,CAAC,eAAe,oBAAoB,SAAuB,OAAO;CACxE,MAAM,CAAC,eAAe,oBAAoB,SAA6B,qBAAqB;CAE5F,MAAM,eAAe,kBAAkB;AACrC,mBAAiB,WAAW;IAC3B,EAAE,CAAC;CAEN,MAAM,eAAe,kBAAkB;AACrC,mBAAiB,WAAW;IAC3B,EAAE,CAAC;CAEN,MAAM,eAAe,kBAAkB;AACrC,mBAAiB,OAAO;IACvB,EAAE,CAAC;CAEN,MAAM,oBAAoB,aACvB,WAA+C;AAC9C,mBAAiB;GACf,GAAG;GACH,QAAQ;GACT,CAAC;IAEJ,EAAE,CACH;CAED,MAAM,qBAAqB,kBAAkB;AAC3C,mBAAiB,qBAAqB;IACrC,EAAE,CAAC;AASN,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA,iBAfsB,kBAAkB;AACxC,OAAI,cAAc,UAChB,eAAc,WAAW;AAE3B,uBAAoB;KACnB,CAAC,eAAe,mBAAmB,CAAC;EAWtC"}
@@ -0,0 +1,160 @@
1
+ import { RESPONSIVE_BREAKPOINTS, calculateResponsiveValues, createTransitionString, getScreenSize } from "../systems/ResponsiveScaling.js";
2
+ import { getSafeAreaInsets, shouldUseMobileControls } from "../utils/deviceDetection.js";
3
+ import { useMemo } from "react";
4
+ //#region src/hooks/useResponsiveLayout.ts
5
+ /**
6
+ * useResponsiveLayout Hook - Enhanced Responsive Layout System
7
+ *
8
+ * Provides comprehensive responsive layout values for all screen sizes with:
9
+ * - Five screen size categories (mobile, tablet, desktop, large, xlarge)
10
+ * - Proportional font scaling (0.8x-1.4x) with 14-24px readability constraints
11
+ * - Proportional spacing scaling (0.5x-1.5x)
12
+ * - Smooth transitions for resize operations (300ms ease-in-out)
13
+ * - Touch-friendly element sizing following iOS Human Interface Guidelines
14
+ * - Safe area insets for notch and home indicator
15
+ *
16
+ * Uses robust device detection combining user-agent and screen size to ensure
17
+ * mobile controls are shown on all mobile devices, including high-resolution phones.
18
+ *
19
+ * Features:
20
+ * - Touch target minimum: 44x44px (iOS guideline)
21
+ * - Safe area insets for notch and home indicator
22
+ * - Responsive font sizes (14px minimum body text)
23
+ * - Optimized spacing for mobile vs desktop
24
+ * - Portrait and landscape mode support
25
+ * - Tablet breakpoint support (768-1024px)
26
+ *
27
+ * @module hooks/useResponsiveLayout
28
+ * @category Mobile UI
29
+ * @korean 반응형레이아웃훅
30
+ */
31
+ /**
32
+ * Breakpoints for responsive design
33
+ * @deprecated Use RESPONSIVE_BREAKPOINTS from ResponsiveScaling instead
34
+ */
35
+ var BREAKPOINTS = {
36
+ MOBILE_SMALL: 375,
37
+ MOBILE: RESPONSIVE_BREAKPOINTS.MOBILE,
38
+ TABLET: RESPONSIVE_BREAKPOINTS.TABLET,
39
+ DESKTOP: RESPONSIVE_BREAKPOINTS.LARGE
40
+ };
41
+ /**
42
+ * Calculate responsive layout values based on viewport dimensions
43
+ *
44
+ * Enhanced with centralized responsive scaling system:
45
+ * - Five screen size categories (mobile, tablet, desktop, large, xlarge)
46
+ * - Proportional font scaling (0.8x-1.4x) with 14-24px constraints
47
+ * - Proportional spacing scaling (0.5x-1.5x)
48
+ * - Smooth CSS transitions for resize operations
49
+ *
50
+ * Optimized for:
51
+ * - iPhone SE (375x667) - mobile
52
+ * - iPhone 11/12/13 (414x896) - mobile
53
+ * - iPhone 14 Pro Max (430x932) - mobile
54
+ * - iPad (768x1024) - tablet
55
+ * - iPad Pro (1024x1366) - desktop
56
+ * - Standard Desktop (1280x800) - desktop
57
+ * - HD Display (1920x1080) - xlarge
58
+ * - 4K Display (2560x1440) - xlarge
59
+ *
60
+ * @param width - Viewport width in pixels
61
+ * @param height - Viewport height in pixels
62
+ * @returns Complete responsive layout configuration
63
+ *
64
+ * @example
65
+ * ```tsx
66
+ * const layout = useResponsiveLayout(375, 667);
67
+ *
68
+ * <div style={{
69
+ * padding: layout.spacing.md,
70
+ * fontSize: layout.fontSize.body,
71
+ * minHeight: layout.touchTarget.small,
72
+ * transition: layout.transition,
73
+ * }}>
74
+ * Responsive content
75
+ * </div>
76
+ * ```
77
+ *
78
+ * @public
79
+ * @korean 반응형레이아웃사용
80
+ */
81
+ function useResponsiveLayout(width, height) {
82
+ return useMemo(() => {
83
+ const screenSize = getScreenSize(width);
84
+ const isMobile = shouldUseMobileControls();
85
+ const isSmallMobile = width <= BREAKPOINTS.MOBILE_SMALL;
86
+ const isTablet = screenSize === "tablet";
87
+ const isDesktop = screenSize === "desktop" || screenSize === "large" || screenSize === "xlarge";
88
+ const isLandscape = width > height;
89
+ const deviceInsets = getSafeAreaInsets();
90
+ const safeArea = {
91
+ top: deviceInsets.top,
92
+ bottom: deviceInsets.bottom,
93
+ left: isLandscape ? deviceInsets.left : 0,
94
+ right: isLandscape ? deviceInsets.right : 0
95
+ };
96
+ const touchTarget = {
97
+ small: 44,
98
+ medium: isSmallMobile ? 50 : 60,
99
+ large: isSmallMobile ? 60 : 80
100
+ };
101
+ const responsiveValues = calculateResponsiveValues(width);
102
+ return {
103
+ isMobile,
104
+ isSmallMobile,
105
+ isTablet,
106
+ isDesktop,
107
+ isLandscape,
108
+ screenSize,
109
+ safeArea,
110
+ touchTarget,
111
+ fontSize: {
112
+ small: responsiveValues.fontSize.small,
113
+ body: responsiveValues.fontSize.body,
114
+ title: responsiveValues.fontSize.title,
115
+ hero: responsiveValues.fontSize.hero,
116
+ hud: responsiveValues.fontSize.hud
117
+ },
118
+ spacing: {
119
+ xs: responsiveValues.spacing.xs,
120
+ sm: responsiveValues.spacing.sm,
121
+ md: responsiveValues.spacing.md,
122
+ lg: responsiveValues.spacing.lg,
123
+ xl: responsiveValues.spacing.xl
124
+ },
125
+ transition: createTransitionString(),
126
+ viewport: {
127
+ width,
128
+ height
129
+ }
130
+ };
131
+ }, [width, height]);
132
+ }
133
+ /**
134
+ * Calculate available content area after safe areas and HUD
135
+ *
136
+ * @param layout - Responsive layout configuration
137
+ * @param hudHeight - Height reserved for HUD elements
138
+ * @param controlsHeight - Height reserved for control elements
139
+ * @returns Available content dimensions
140
+ */
141
+ function useContentArea(layout, hudHeight = 0, controlsHeight = 0) {
142
+ return useMemo(() => {
143
+ const totalVerticalReserved = layout.safeArea.top + layout.safeArea.bottom + hudHeight + controlsHeight + layout.spacing.md * 2;
144
+ const totalHorizontalReserved = layout.safeArea.left + layout.safeArea.right + layout.spacing.md * 2;
145
+ return {
146
+ width: layout.viewport.width - totalHorizontalReserved,
147
+ height: layout.viewport.height - totalVerticalReserved,
148
+ x: layout.safeArea.left + layout.spacing.md,
149
+ y: layout.safeArea.top + hudHeight + layout.spacing.md
150
+ };
151
+ }, [
152
+ layout,
153
+ hudHeight,
154
+ controlsHeight
155
+ ]);
156
+ }
157
+ //#endregion
158
+ export { BREAKPOINTS, useContentArea, useResponsiveLayout };
159
+
160
+ //# sourceMappingURL=useResponsiveLayout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useResponsiveLayout.js","names":[],"sources":["../../src/hooks/useResponsiveLayout.ts"],"sourcesContent":["/**\n * useResponsiveLayout Hook - Enhanced Responsive Layout System\n * \n * Provides comprehensive responsive layout values for all screen sizes with:\n * - Five screen size categories (mobile, tablet, desktop, large, xlarge)\n * - Proportional font scaling (0.8x-1.4x) with 14-24px readability constraints\n * - Proportional spacing scaling (0.5x-1.5x)\n * - Smooth transitions for resize operations (300ms ease-in-out)\n * - Touch-friendly element sizing following iOS Human Interface Guidelines\n * - Safe area insets for notch and home indicator\n * \n * Uses robust device detection combining user-agent and screen size to ensure\n * mobile controls are shown on all mobile devices, including high-resolution phones.\n * \n * Features:\n * - Touch target minimum: 44x44px (iOS guideline)\n * - Safe area insets for notch and home indicator\n * - Responsive font sizes (14px minimum body text)\n * - Optimized spacing for mobile vs desktop\n * - Portrait and landscape mode support\n * - Tablet breakpoint support (768-1024px)\n * \n * @module hooks/useResponsiveLayout\n * @category Mobile UI\n * @korean 반응형레이아웃훅\n */\n\nimport { useMemo } from 'react';\nimport { shouldUseMobileControls, getSafeAreaInsets as getDeviceSafeAreaInsets } from '../utils/deviceDetection';\nimport {\n RESPONSIVE_BREAKPOINTS,\n getScreenSize,\n calculateResponsiveValues,\n createTransitionString,\n} from '../systems/ResponsiveScaling';\n\nimport type { ScreenSize } from '../systems/ResponsiveScaling';\n\n/**\n * Breakpoints for responsive design\n * @deprecated Use RESPONSIVE_BREAKPOINTS from ResponsiveScaling instead\n */\nexport const BREAKPOINTS = {\n /** Extra small mobile devices (iPhone SE) */\n MOBILE_SMALL: 375,\n /** Standard mobile devices */\n MOBILE: RESPONSIVE_BREAKPOINTS.MOBILE,\n /** Tablet devices */\n TABLET: RESPONSIVE_BREAKPOINTS.TABLET,\n /** Desktop devices */\n DESKTOP: RESPONSIVE_BREAKPOINTS.LARGE,\n} as const;\n\n/**\n * Touch target sizes following iOS Human Interface Guidelines\n */\nexport interface TouchTargets {\n /** Minimum touch target (44x44px iOS guideline) */\n readonly small: number;\n /** Medium touch target for primary actions */\n readonly medium: number;\n /** Large touch target for critical actions */\n readonly large: number;\n}\n\n/**\n * Safe area insets for mobile devices\n * Accounts for notch, status bar, and home indicator\n */\nexport interface SafeAreaInsets {\n /** Top inset (status bar, notch) */\n readonly top: number;\n /** Bottom inset (home indicator) */\n readonly bottom: number;\n /** Left inset (landscape mode) */\n readonly left: number;\n /** Right inset (landscape mode) */\n readonly right: number;\n}\n\n/**\n * Responsive font sizes\n */\nexport interface FontSizes {\n /** Small text (12-14px) */\n readonly small: number;\n /** Body text (14-16px minimum for mobile) */\n readonly body: number;\n /** Title text (18-24px) */\n readonly title: number;\n /** Hero text (24-36px) */\n readonly hero: number;\n /** HUD text (16-20px for important combat info) */\n readonly hud: number;\n}\n\n/**\n * Responsive spacing scale\n */\nexport interface Spacing {\n /** Extra small (4-8px) */\n readonly xs: number;\n /** Small (8-12px) */\n readonly sm: number;\n /** Medium (12-16px) */\n readonly md: number;\n /** Large (16-24px) */\n readonly lg: number;\n /** Extra large (24-32px) */\n readonly xl: number;\n}\n\n/**\n * Complete responsive layout configuration\n */\nexport interface ResponsiveLayout {\n /** Whether current viewport is mobile-sized */\n readonly isMobile: boolean;\n /** Whether current viewport is small mobile (< 400px) */\n readonly isSmallMobile: boolean;\n /** Whether current viewport is tablet-sized */\n readonly isTablet: boolean;\n /** Whether current viewport is desktop or larger */\n readonly isDesktop: boolean;\n /** Whether in landscape orientation */\n readonly isLandscape: boolean;\n /** Current screen size category (mobile, tablet, desktop, large, xlarge) */\n readonly screenSize: ScreenSize;\n /** Safe area insets for mobile devices */\n readonly safeArea: SafeAreaInsets;\n /** Touch target sizes */\n readonly touchTarget: TouchTargets;\n /** Font sizes */\n readonly fontSize: FontSizes;\n /** Spacing scale */\n readonly spacing: Spacing;\n /** CSS transition string for smooth resizing */\n readonly transition: string;\n /** Current viewport dimensions */\n readonly viewport: {\n readonly width: number;\n readonly height: number;\n };\n}\n\n/**\n * Calculate responsive layout values based on viewport dimensions\n * \n * Enhanced with centralized responsive scaling system:\n * - Five screen size categories (mobile, tablet, desktop, large, xlarge)\n * - Proportional font scaling (0.8x-1.4x) with 14-24px constraints\n * - Proportional spacing scaling (0.5x-1.5x)\n * - Smooth CSS transitions for resize operations\n * \n * Optimized for:\n * - iPhone SE (375x667) - mobile\n * - iPhone 11/12/13 (414x896) - mobile\n * - iPhone 14 Pro Max (430x932) - mobile\n * - iPad (768x1024) - tablet\n * - iPad Pro (1024x1366) - desktop\n * - Standard Desktop (1280x800) - desktop\n * - HD Display (1920x1080) - xlarge\n * - 4K Display (2560x1440) - xlarge\n * \n * @param width - Viewport width in pixels\n * @param height - Viewport height in pixels\n * @returns Complete responsive layout configuration\n * \n * @example\n * ```tsx\n * const layout = useResponsiveLayout(375, 667);\n * \n * <div style={{\n * padding: layout.spacing.md,\n * fontSize: layout.fontSize.body,\n * minHeight: layout.touchTarget.small,\n * transition: layout.transition,\n * }}>\n * Responsive content\n * </div>\n * ```\n * \n * @public\n * @korean 반응형레이아웃사용\n */\nexport function useResponsiveLayout(\n width: number,\n height: number\n): ResponsiveLayout {\n return useMemo(() => {\n // Determine screen size category using centralized scaling system\n const screenSize = getScreenSize(width);\n \n // Device type detection using robust device detection utility\n // Combines user-agent and screen size for reliable mobile detection\n const isMobile = shouldUseMobileControls();\n const isSmallMobile = width <= BREAKPOINTS.MOBILE_SMALL;\n const isTablet = screenSize === 'tablet';\n const isDesktop = screenSize === 'desktop' || screenSize === 'large' || screenSize === 'xlarge';\n const isLandscape = width > height;\n\n // Safe area insets from device detection utility\n const deviceInsets = getDeviceSafeAreaInsets();\n const safeArea: SafeAreaInsets = {\n top: deviceInsets.top,\n bottom: deviceInsets.bottom,\n left: isLandscape ? deviceInsets.left : 0,\n right: isLandscape ? deviceInsets.right : 0,\n };\n\n // Touch target sizes (iOS Human Interface Guidelines: 44x44px minimum)\n const touchTarget: TouchTargets = {\n small: 44, // Minimum iOS guideline\n medium: isSmallMobile ? 50 : 60,\n large: isSmallMobile ? 60 : 80,\n };\n\n // Calculate responsive values using centralized scaling system\n const responsiveValues = calculateResponsiveValues(width);\n\n // Map to existing font size structure for backward compatibility\n const fontSize: FontSizes = {\n small: responsiveValues.fontSize.small,\n body: responsiveValues.fontSize.body,\n title: responsiveValues.fontSize.title,\n hero: responsiveValues.fontSize.hero,\n hud: responsiveValues.fontSize.hud,\n };\n\n // Map to existing spacing structure for backward compatibility\n const spacing: Spacing = {\n xs: responsiveValues.spacing.xs,\n sm: responsiveValues.spacing.sm,\n md: responsiveValues.spacing.md,\n lg: responsiveValues.spacing.lg,\n xl: responsiveValues.spacing.xl,\n };\n\n // Get transition string for smooth resizing\n const transition = createTransitionString();\n\n return {\n isMobile,\n isSmallMobile,\n isTablet,\n isDesktop,\n isLandscape,\n screenSize,\n safeArea,\n touchTarget,\n fontSize,\n spacing,\n transition,\n viewport: { width, height },\n };\n }, [width, height]);\n}\n\n/**\n * Calculate available content area after safe areas and HUD\n * \n * @param layout - Responsive layout configuration\n * @param hudHeight - Height reserved for HUD elements\n * @param controlsHeight - Height reserved for control elements\n * @returns Available content dimensions\n */\nexport function useContentArea(\n layout: ResponsiveLayout,\n hudHeight: number = 0,\n controlsHeight: number = 0\n) {\n return useMemo(() => {\n const totalVerticalReserved =\n layout.safeArea.top +\n layout.safeArea.bottom +\n hudHeight +\n controlsHeight +\n layout.spacing.md * 2; // Padding\n\n const totalHorizontalReserved =\n layout.safeArea.left + layout.safeArea.right + layout.spacing.md * 2;\n\n return {\n width: layout.viewport.width - totalHorizontalReserved,\n height: layout.viewport.height - totalVerticalReserved,\n x: layout.safeArea.left + layout.spacing.md,\n y: layout.safeArea.top + hudHeight + layout.spacing.md,\n };\n }, [layout, hudHeight, controlsHeight]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,IAAa,cAAc;CAEzB,cAAc;CAEd,QAAQ,uBAAuB;CAE/B,QAAQ,uBAAuB;CAE/B,SAAS,uBAAuB;CACjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsID,SAAgB,oBACd,OACA,QACkB;AAClB,QAAO,cAAc;EAEnB,MAAM,aAAa,cAAc,MAAM;EAIvC,MAAM,WAAW,yBAAyB;EAC1C,MAAM,gBAAgB,SAAS,YAAY;EAC3C,MAAM,WAAW,eAAe;EAChC,MAAM,YAAY,eAAe,aAAa,eAAe,WAAW,eAAe;EACvF,MAAM,cAAc,QAAQ;EAG5B,MAAM,eAAe,mBAAyB;EAC9C,MAAM,WAA2B;GAC/B,KAAK,aAAa;GAClB,QAAQ,aAAa;GACrB,MAAM,cAAc,aAAa,OAAO;GACxC,OAAO,cAAc,aAAa,QAAQ;GAC3C;EAGD,MAAM,cAA4B;GAChC,OAAO;GACP,QAAQ,gBAAgB,KAAK;GAC7B,OAAO,gBAAgB,KAAK;GAC7B;EAGD,MAAM,mBAAmB,0BAA0B,MAAM;AAuBzD,SAAO;GACL;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA,UA7B0B;IAC1B,OAAO,iBAAiB,SAAS;IACjC,MAAM,iBAAiB,SAAS;IAChC,OAAO,iBAAiB,SAAS;IACjC,MAAM,iBAAiB,SAAS;IAChC,KAAK,iBAAiB,SAAS;IAChC;GAwBC,SArBuB;IACvB,IAAI,iBAAiB,QAAQ;IAC7B,IAAI,iBAAiB,QAAQ;IAC7B,IAAI,iBAAiB,QAAQ;IAC7B,IAAI,iBAAiB,QAAQ;IAC7B,IAAI,iBAAiB,QAAQ;IAC9B;GAgBC,YAbiB,wBAAwB;GAczC,UAAU;IAAE;IAAO;IAAQ;GAC5B;IACA,CAAC,OAAO,OAAO,CAAC;;;;;;;;;;AAWrB,SAAgB,eACd,QACA,YAAoB,GACpB,iBAAyB,GACzB;AACA,QAAO,cAAc;EACnB,MAAM,wBACJ,OAAO,SAAS,MAChB,OAAO,SAAS,SAChB,YACA,iBACA,OAAO,QAAQ,KAAK;EAEtB,MAAM,0BACJ,OAAO,SAAS,OAAO,OAAO,SAAS,QAAQ,OAAO,QAAQ,KAAK;AAErE,SAAO;GACL,OAAO,OAAO,SAAS,QAAQ;GAC/B,QAAQ,OAAO,SAAS,SAAS;GACjC,GAAG,OAAO,SAAS,OAAO,OAAO,QAAQ;GACzC,GAAG,OAAO,SAAS,MAAM,YAAY,OAAO,QAAQ;GACrD;IACA;EAAC;EAAQ;EAAW;EAAe,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useRoundTransition.js","names":[],"sources":["../../src/hooks/useRoundTransition.ts"],"sourcesContent":["/**\n * useRoundTransition Hook - Manages round transition state and timing\n *\n * Korean: 라운드 전환 훅 (Round Transition Hook)\n *\n * Handles the state machine for round transitions:\n * - idle: Normal combat state\n * - announcing: Showing round announcement\n * - countdown: Counting down to next round\n * - transitioning: Brief transition to next round\n *\n * @module hooks/useRoundTransition\n * @category Combat Hooks\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { PlayerState } from \"../systems\";\n\n/**\n * Round transition states\n *\n * Korean: 라운드 전환 상태\n */\nexport type RoundTransitionState =\n | \"idle\"\n | \"announcing\"\n | \"countdown\"\n | \"transitioning\";\n\n/**\n * Round transition configuration\n */\nexport interface RoundTransitionConfig {\n /** Duration of announcement display in seconds */\n readonly announcementDuration?: number;\n /** Duration of countdown in seconds */\n readonly countdownDuration?: number;\n /** Duration of transition phase in milliseconds */\n readonly transitionDuration?: number;\n}\n\n/**\n * Round transition hook state\n */\nexport interface UseRoundTransitionResult {\n /** Current transition state */\n readonly transitionState: RoundTransitionState;\n /** Whether announcement should be visible */\n readonly showAnnouncement: boolean;\n /** Start round transition sequence */\n readonly startTransition: (\n winner: PlayerState | null,\n roundNumber: number\n ) => void;\n /** Skip countdown and proceed immediately */\n readonly skipCountdown: () => void;\n /** Reset transition state to idle */\n readonly resetTransition: () => void;\n /** Round winner for current transition */\n readonly roundWinner: PlayerState | null;\n /** Round number for current transition */\n readonly currentRoundNumber: number;\n}\n\n/**\n * Default configuration values\n */\nconst DEFAULT_CONFIG: Required<RoundTransitionConfig> = {\n announcementDuration: 2,\n countdownDuration: 3,\n transitionDuration: 500,\n};\n\n/**\n * useRoundTransition Hook\n *\n * Manages the complete round transition flow:\n * 1. Idle state during normal combat\n * 2. Announcing state shows round results\n * 3. Countdown state counts down to next round\n * 4. Transitioning state briefly transitions to next round\n * 5. Returns to idle for next round\n *\n * @param config - Configuration for transition timings\n * @param onTransitionComplete - Callback when transition completes\n * @returns Round transition state and control functions\n *\n * @example\n * ```typescript\n * const {\n * transitionState,\n * showAnnouncement,\n * startTransition,\n * skipCountdown,\n * } = useRoundTransition(\n * { countdownDuration: 3 },\n * () => {\n * // Start next round\n * initializeNextRound();\n * }\n * );\n *\n * // When round ends\n * startTransition(winner, roundNumber);\n * ```\n */\nexport function useRoundTransition(\n config: RoundTransitionConfig = {},\n onTransitionComplete?: () => void\n): UseRoundTransitionResult {\n const mergedConfig = useMemo(\n () => ({ ...DEFAULT_CONFIG, ...config }),\n [config]\n );\n\n const [transitionState, setTransitionState] =\n useState<RoundTransitionState>(\"idle\");\n const [roundWinner, setRoundWinner] = useState<PlayerState | null>(null);\n const [currentRoundNumber, setCurrentRoundNumber] = useState(0);\n\n // Use ref to track if we should continue countdown\n const countdownActive = useRef(false);\n const countdownTimer = useRef<NodeJS.Timeout | null>(null);\n const transitionTimer = useRef<NodeJS.Timeout | null>(null);\n\n /**\n * Clear all active timers\n */\n const clearTimers = useCallback(() => {\n if (countdownTimer.current) {\n clearInterval(countdownTimer.current);\n countdownTimer.current = null;\n }\n if (transitionTimer.current) {\n clearTimeout(transitionTimer.current);\n transitionTimer.current = null;\n }\n }, []);\n\n /**\n * Start the transition sequence\n */\n const startTransition = useCallback(\n (winner: PlayerState | null, roundNumber: number) => {\n clearTimers();\n setRoundWinner(winner);\n setCurrentRoundNumber(roundNumber);\n setTransitionState(\"announcing\");\n\n // Move to countdown after announcement\n const announcementTimer = setTimeout(() => {\n setTransitionState(\"countdown\");\n countdownActive.current = true;\n\n // Countdown will be managed by the RoundAnnouncement component\n // After countdownDuration, move to transitioning state\n countdownTimer.current = setTimeout(() => {\n countdownActive.current = false;\n setTransitionState(\"transitioning\");\n\n // Complete transition after brief delay\n transitionTimer.current = setTimeout(() => {\n setTransitionState(\"idle\");\n onTransitionComplete?.();\n }, mergedConfig.transitionDuration);\n }, mergedConfig.countdownDuration * 1000);\n }, mergedConfig.announcementDuration * 1000);\n\n // Store timer ref for cleanup\n transitionTimer.current = announcementTimer;\n },\n [clearTimers, mergedConfig, onTransitionComplete]\n );\n\n /**\n * Skip countdown and proceed immediately to next round\n */\n const skipCountdown = useCallback(() => {\n if (transitionState === \"announcing\" || transitionState === \"countdown\") {\n clearTimers();\n countdownActive.current = false;\n setTransitionState(\"transitioning\");\n\n // Complete transition after brief delay\n transitionTimer.current = setTimeout(() => {\n setTransitionState(\"idle\");\n onTransitionComplete?.();\n }, mergedConfig.transitionDuration);\n }\n }, [\n transitionState,\n clearTimers,\n mergedConfig.transitionDuration,\n onTransitionComplete,\n ]);\n\n /**\n * Reset transition to idle state\n */\n const resetTransition = useCallback(() => {\n clearTimers();\n countdownActive.current = false;\n setTransitionState(\"idle\");\n setRoundWinner(null);\n setCurrentRoundNumber(0);\n }, [clearTimers]);\n\n /**\n * Cleanup on unmount\n */\n useEffect(() => {\n return () => {\n clearTimers();\n countdownActive.current = false;\n };\n }, [clearTimers]);\n\n return {\n transitionState,\n showAnnouncement:\n transitionState === \"announcing\" || transitionState === \"countdown\",\n startTransition,\n skipCountdown,\n resetTransition,\n roundWinner,\n currentRoundNumber,\n };\n}\n\nexport default useRoundTransition;\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmEA,IAAM,iBAAkD;CACtD,sBAAsB;CACtB,mBAAmB;CACnB,oBAAoB;CACrB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCD,SAAgB,mBACd,SAAgC,EAAE,EAClC,sBAC0B;CAC1B,MAAM,eAAe,eACZ;EAAE,GAAG;EAAgB,GAAG;EAAQ,GACvC,CAAC,OAAO,CACT;CAED,MAAM,CAAC,iBAAiB,sBACtB,SAA+B,OAAO;CACxC,MAAM,CAAC,aAAa,kBAAkB,SAA6B,KAAK;CACxE,MAAM,CAAC,oBAAoB,yBAAyB,SAAS,EAAE;CAG/D,MAAM,kBAAkB,OAAO,MAAM;CACrC,MAAM,iBAAiB,OAA8B,KAAK;CAC1D,MAAM,kBAAkB,OAA8B,KAAK;;;;CAK3D,MAAM,cAAc,kBAAkB;AACpC,MAAI,eAAe,SAAS;AAC1B,iBAAc,eAAe,QAAQ;AACrC,kBAAe,UAAU;;AAE3B,MAAI,gBAAgB,SAAS;AAC3B,gBAAa,gBAAgB,QAAQ;AACrC,mBAAgB,UAAU;;IAE3B,EAAE,CAAC;;;;CAKN,MAAM,kBAAkB,aACrB,QAA4B,gBAAwB;AACnD,eAAa;AACb,iBAAe,OAAO;AACtB,wBAAsB,YAAY;AAClC,qBAAmB,aAAa;AAsBhC,kBAAgB,UAnBU,iBAAiB;AACzC,sBAAmB,YAAY;AAC/B,mBAAgB,UAAU;AAI1B,kBAAe,UAAU,iBAAiB;AACxC,oBAAgB,UAAU;AAC1B,uBAAmB,gBAAgB;AAGnC,oBAAgB,UAAU,iBAAiB;AACzC,wBAAmB,OAAO;AAC1B,6BAAwB;OACvB,aAAa,mBAAmB;MAClC,aAAa,oBAAoB,IAAK;KACxC,aAAa,uBAAuB,IAAK;IAK9C;EAAC;EAAa;EAAc;EAAqB,CAClD;;;;CAKD,MAAM,gBAAgB,kBAAkB;AACtC,MAAI,oBAAoB,gBAAgB,oBAAoB,aAAa;AACvE,gBAAa;AACb,mBAAgB,UAAU;AAC1B,sBAAmB,gBAAgB;AAGnC,mBAAgB,UAAU,iBAAiB;AACzC,uBAAmB,OAAO;AAC1B,4BAAwB;MACvB,aAAa,mBAAmB;;IAEpC;EACD;EACA;EACA,aAAa;EACb;EACD,CAAC;;;;CAKF,MAAM,kBAAkB,kBAAkB;AACxC,eAAa;AACb,kBAAgB,UAAU;AAC1B,qBAAmB,OAAO;AAC1B,iBAAe,KAAK;AACpB,wBAAsB,EAAE;IACvB,CAAC,YAAY,CAAC;;;;AAKjB,iBAAgB;AACd,eAAa;AACX,gBAAa;AACb,mBAAgB,UAAU;;IAE3B,CAAC,YAAY,CAAC;AAEjB,QAAO;EACL;EACA,kBACE,oBAAoB,gBAAgB,oBAAoB;EAC1D;EACA;EACA;EACA;EACA;EACD"}
1
+ {"version":3,"file":"useRoundTransition.js","names":[],"sources":["../../src/hooks/useRoundTransition.ts"],"sourcesContent":["/**\n * useRoundTransition Hook - Manages round transition state and timing\n *\n * Korean: 라운드 전환 훅 (Round Transition Hook)\n *\n * Handles the state machine for round transitions:\n * - idle: Normal combat state\n * - announcing: Showing round announcement\n * - countdown: Counting down to next round\n * - transitioning: Brief transition to next round\n *\n * @module hooks/useRoundTransition\n * @category Combat Hooks\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { PlayerState } from \"../systems\";\n\n/**\n * Round transition states\n *\n * Korean: 라운드 전환 상태\n */\nexport type RoundTransitionState =\n | \"idle\"\n | \"announcing\"\n | \"countdown\"\n | \"transitioning\";\n\n/**\n * Round transition configuration\n */\nexport interface RoundTransitionConfig {\n /** Duration of announcement display in seconds */\n readonly announcementDuration?: number;\n /** Duration of countdown in seconds */\n readonly countdownDuration?: number;\n /** Duration of transition phase in milliseconds */\n readonly transitionDuration?: number;\n}\n\n/**\n * Round transition hook state\n */\nexport interface UseRoundTransitionResult {\n /** Current transition state */\n readonly transitionState: RoundTransitionState;\n /** Whether announcement should be visible */\n readonly showAnnouncement: boolean;\n /** Start round transition sequence */\n readonly startTransition: (\n winner: PlayerState | null,\n roundNumber: number\n ) => void;\n /** Skip countdown and proceed immediately */\n readonly skipCountdown: () => void;\n /** Reset transition state to idle */\n readonly resetTransition: () => void;\n /** Round winner for current transition */\n readonly roundWinner: PlayerState | null;\n /** Round number for current transition */\n readonly currentRoundNumber: number;\n}\n\n/**\n * Default configuration values\n */\nconst DEFAULT_CONFIG: Required<RoundTransitionConfig> = {\n announcementDuration: 2,\n countdownDuration: 3,\n transitionDuration: 500,\n};\n\n/**\n * useRoundTransition Hook\n *\n * Manages the complete round transition flow:\n * 1. Idle state during normal combat\n * 2. Announcing state shows round results\n * 3. Countdown state counts down to next round\n * 4. Transitioning state briefly transitions to next round\n * 5. Returns to idle for next round\n *\n * @param config - Configuration for transition timings\n * @param onTransitionComplete - Callback when transition completes\n * @returns Round transition state and control functions\n *\n * @example\n * ```typescript\n * const {\n * transitionState,\n * showAnnouncement,\n * startTransition,\n * skipCountdown,\n * } = useRoundTransition(\n * { countdownDuration: 3 },\n * () => {\n * // Start next round\n * initializeNextRound();\n * }\n * );\n *\n * // When round ends\n * startTransition(winner, roundNumber);\n * ```\n */\nexport function useRoundTransition(\n config: RoundTransitionConfig = {},\n onTransitionComplete?: () => void\n): UseRoundTransitionResult {\n const mergedConfig = useMemo(\n () => ({ ...DEFAULT_CONFIG, ...config }),\n [config]\n );\n\n const [transitionState, setTransitionState] =\n useState<RoundTransitionState>(\"idle\");\n const [roundWinner, setRoundWinner] = useState<PlayerState | null>(null);\n const [currentRoundNumber, setCurrentRoundNumber] = useState(0);\n\n // Use ref to track if we should continue countdown\n const countdownActive = useRef(false);\n const countdownTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const transitionTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n /**\n * Clear all active timers\n */\n const clearTimers = useCallback(() => {\n if (countdownTimer.current) {\n clearInterval(countdownTimer.current);\n countdownTimer.current = null;\n }\n if (transitionTimer.current) {\n clearTimeout(transitionTimer.current);\n transitionTimer.current = null;\n }\n }, []);\n\n /**\n * Start the transition sequence\n */\n const startTransition = useCallback(\n (winner: PlayerState | null, roundNumber: number) => {\n clearTimers();\n setRoundWinner(winner);\n setCurrentRoundNumber(roundNumber);\n setTransitionState(\"announcing\");\n\n // Move to countdown after announcement\n const announcementTimer = setTimeout(() => {\n setTransitionState(\"countdown\");\n countdownActive.current = true;\n\n // Countdown will be managed by the RoundAnnouncement component\n // After countdownDuration, move to transitioning state\n countdownTimer.current = setTimeout(() => {\n countdownActive.current = false;\n setTransitionState(\"transitioning\");\n\n // Complete transition after brief delay\n transitionTimer.current = setTimeout(() => {\n setTransitionState(\"idle\");\n onTransitionComplete?.();\n }, mergedConfig.transitionDuration);\n }, mergedConfig.countdownDuration * 1000);\n }, mergedConfig.announcementDuration * 1000);\n\n // Store timer ref for cleanup\n transitionTimer.current = announcementTimer;\n },\n [clearTimers, mergedConfig, onTransitionComplete]\n );\n\n /**\n * Skip countdown and proceed immediately to next round\n */\n const skipCountdown = useCallback(() => {\n if (transitionState === \"announcing\" || transitionState === \"countdown\") {\n clearTimers();\n countdownActive.current = false;\n setTransitionState(\"transitioning\");\n\n // Complete transition after brief delay\n transitionTimer.current = setTimeout(() => {\n setTransitionState(\"idle\");\n onTransitionComplete?.();\n }, mergedConfig.transitionDuration);\n }\n }, [\n transitionState,\n clearTimers,\n mergedConfig.transitionDuration,\n onTransitionComplete,\n ]);\n\n /**\n * Reset transition to idle state\n */\n const resetTransition = useCallback(() => {\n clearTimers();\n countdownActive.current = false;\n setTransitionState(\"idle\");\n setRoundWinner(null);\n setCurrentRoundNumber(0);\n }, [clearTimers]);\n\n /**\n * Cleanup on unmount\n */\n useEffect(() => {\n return () => {\n clearTimers();\n countdownActive.current = false;\n };\n }, [clearTimers]);\n\n return {\n transitionState,\n showAnnouncement:\n transitionState === \"announcing\" || transitionState === \"countdown\",\n startTransition,\n skipCountdown,\n resetTransition,\n roundWinner,\n currentRoundNumber,\n };\n}\n\nexport default useRoundTransition;\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmEA,IAAM,iBAAkD;CACtD,sBAAsB;CACtB,mBAAmB;CACnB,oBAAoB;CACrB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCD,SAAgB,mBACd,SAAgC,EAAE,EAClC,sBAC0B;CAC1B,MAAM,eAAe,eACZ;EAAE,GAAG;EAAgB,GAAG;EAAQ,GACvC,CAAC,OAAO,CACT;CAED,MAAM,CAAC,iBAAiB,sBACtB,SAA+B,OAAO;CACxC,MAAM,CAAC,aAAa,kBAAkB,SAA6B,KAAK;CACxE,MAAM,CAAC,oBAAoB,yBAAyB,SAAS,EAAE;CAG/D,MAAM,kBAAkB,OAAO,MAAM;CACrC,MAAM,iBAAiB,OAA6C,KAAK;CACzE,MAAM,kBAAkB,OAA6C,KAAK;;;;CAK1E,MAAM,cAAc,kBAAkB;AACpC,MAAI,eAAe,SAAS;AAC1B,iBAAc,eAAe,QAAQ;AACrC,kBAAe,UAAU;;AAE3B,MAAI,gBAAgB,SAAS;AAC3B,gBAAa,gBAAgB,QAAQ;AACrC,mBAAgB,UAAU;;IAE3B,EAAE,CAAC;;;;CAKN,MAAM,kBAAkB,aACrB,QAA4B,gBAAwB;AACnD,eAAa;AACb,iBAAe,OAAO;AACtB,wBAAsB,YAAY;AAClC,qBAAmB,aAAa;AAsBhC,kBAAgB,UAnBU,iBAAiB;AACzC,sBAAmB,YAAY;AAC/B,mBAAgB,UAAU;AAI1B,kBAAe,UAAU,iBAAiB;AACxC,oBAAgB,UAAU;AAC1B,uBAAmB,gBAAgB;AAGnC,oBAAgB,UAAU,iBAAiB;AACzC,wBAAmB,OAAO;AAC1B,6BAAwB;OACvB,aAAa,mBAAmB;MAClC,aAAa,oBAAoB,IAAK;KACxC,aAAa,uBAAuB,IAAK;IAK9C;EAAC;EAAa;EAAc;EAAqB,CAClD;;;;CAKD,MAAM,gBAAgB,kBAAkB;AACtC,MAAI,oBAAoB,gBAAgB,oBAAoB,aAAa;AACvE,gBAAa;AACb,mBAAgB,UAAU;AAC1B,sBAAmB,gBAAgB;AAGnC,mBAAgB,UAAU,iBAAiB;AACzC,uBAAmB,OAAO;AAC1B,4BAAwB;MACvB,aAAa,mBAAmB;;IAEpC;EACD;EACA;EACA,aAAa;EACb;EACD,CAAC;;;;CAKF,MAAM,kBAAkB,kBAAkB;AACxC,eAAa;AACb,kBAAgB,UAAU;AAC1B,qBAAmB,OAAO;AAC1B,iBAAe,KAAK;AACpB,wBAAsB,EAAE;IACvB,CAAC,YAAY,CAAC;;;;AAKjB,iBAAgB;AACd,eAAa;AACX,gBAAa;AACb,mBAAgB,UAAU;;IAE3B,CAAC,YAAY,CAAC;AAEjB,QAAO;EACL;EACA,kBACE,oBAAoB,gBAAgB,oBAAoB;EAC1D;EACA;EACA;EACA;EACA;EACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"useTechniqueSelection.js","names":[],"sources":["../../src/hooks/useTechniqueSelection.ts"],"sourcesContent":["/**\n * Custom hook for managing technique selection and execution.\n *\n * **Korean**: 기술 선택 관리 (Technique Selection Management)\n *\n * Handles technique selection state, keyboard shortcuts, cooldown tracking,\n * and validation of technique execution based on player resources and stance.\n *\n * @module hooks/useTechniqueSelection\n * @category Combat Hooks\n * @korean 기술선택훅\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { getTechniquesForStanceAndArchetype } from \"../data/techniques\";\nimport { PlayerState } from \"../systems/player\";\nimport {\n Technique,\n TechniqueCooldown,\n TechniqueValidation,\n} from \"../types\";\n\n/**\n * Configuration for technique selection hook.\n */\nexport interface UseTechniqueSelectionConfig {\n /** Player state with resources and stance */\n readonly player: PlayerState;\n\n /** Whether technique selection is enabled */\n readonly enabled?: boolean;\n\n /** Callback when technique is selected */\n readonly onTechniqueSelected?: (technique: Technique) => void;\n\n /** Callback when technique execution is attempted */\n readonly onTechniqueExecute?: (technique: Technique) => void;\n}\n\n/**\n * Technique selection state and actions.\n */\nexport interface UseTechniqueSelectionResult {\n /** Available techniques for player archetype */\n readonly availableTechniques: readonly Technique[];\n\n /** Currently selected technique index */\n readonly selectedIndex: number;\n\n /** Active cooldowns for techniques */\n readonly activeCooldowns: readonly TechniqueCooldown[];\n\n /** Select technique by index */\n readonly selectTechnique: (index: number) => void;\n\n /** Execute currently selected technique */\n readonly executeTechnique: (indexOverride?: number) => void;\n\n /** Check if technique can be executed */\n readonly validateTechnique: (technique: Technique) => TechniqueValidation;\n\n /** Check if technique is on cooldown */\n readonly isOnCooldown: (techniqueId: string) => boolean;\n\n /** Get remaining cooldown time in ms */\n readonly getRemainingCooldown: (techniqueId: string) => number;\n\n /** Check if player has sufficient resources */\n readonly hasResources: (technique: Technique) => boolean;\n}\n\n/**\n * Custom hook for managing technique selection and execution.\n *\n * @param config - Configuration options\n * @returns Technique selection state and actions\n *\n * @example\n * ```typescript\n * const techniqueSelection = useTechniqueSelection({\n * player: playerState,\n * enabled: !isPaused && combatActive,\n * onTechniqueExecute: (technique) => {\n * // Execute technique logic\n * executeCombatTechnique(playerState, opponent, technique);\n * }\n * });\n * ```\n *\n * @public\n */\nexport function useTechniqueSelection(\n config: UseTechniqueSelectionConfig\n): UseTechniqueSelectionResult {\n const {\n player,\n enabled = true,\n onTechniqueSelected,\n onTechniqueExecute,\n } = config;\n\n // Get available techniques based on player's current stance and archetype\n const availableTechniques = useMemo(\n () =>\n getTechniquesForStanceAndArchetype(\n player.currentStance,\n player.archetype\n ),\n [player.currentStance, player.archetype]\n );\n\n // Selected technique state\n const [selectedIndex, setSelectedIndex] = useState(0);\n\n // Cooldown tracking\n const [activeCooldowns, setActiveCooldowns] = useState<TechniqueCooldown[]>(\n []\n );\n\n // Ref for cleanup\n const cooldownUpdateIntervalRef = useRef<NodeJS.Timeout | null>(null);\n\n // Update cooldowns every 100ms\n useEffect(() => {\n if (activeCooldowns.length === 0) {\n // Clear any existing interval when no cooldowns\n if (cooldownUpdateIntervalRef.current) {\n clearInterval(cooldownUpdateIntervalRef.current);\n cooldownUpdateIntervalRef.current = null;\n }\n return;\n }\n\n let isMounted = true;\n cooldownUpdateIntervalRef.current = setInterval(() => {\n if (!isMounted) return;\n const now = Date.now();\n setActiveCooldowns((prev) => {\n return prev\n .map((cd) => ({\n ...cd,\n remaining: Math.max(0, cd.startTime + cd.duration - now),\n }))\n .filter((cd) => cd.remaining > 0);\n });\n }, 100);\n\n return () => {\n isMounted = false;\n if (cooldownUpdateIntervalRef.current) {\n clearInterval(cooldownUpdateIntervalRef.current);\n cooldownUpdateIntervalRef.current = null;\n }\n };\n }, [activeCooldowns.length]);\n\n // Check if technique is on cooldown\n const isOnCooldown = useCallback(\n (techniqueId: string): boolean => {\n return activeCooldowns.some(\n (cd) => cd.techniqueId === techniqueId && cd.remaining > 0\n );\n },\n [activeCooldowns]\n );\n\n // Get remaining cooldown time\n const getRemainingCooldown = useCallback(\n (techniqueId: string): number => {\n const cooldown = activeCooldowns.find(\n (cd) => cd.techniqueId === techniqueId\n );\n return cooldown?.remaining ?? 0;\n },\n [activeCooldowns]\n );\n\n // Check if player has sufficient resources\n const hasResources = useCallback(\n (technique: Technique): boolean => {\n return (\n player.stamina >= technique.staminaCost && player.ki >= technique.kiCost\n );\n },\n [player.stamina, player.ki]\n );\n\n // Validate technique execution\n const validateTechnique = useCallback(\n (technique: Technique): TechniqueValidation => {\n // Check stamina\n if (player.stamina < technique.staminaCost) {\n return {\n canExecute: false,\n reason: \"Insufficient stamina\",\n insufficientStamina: true,\n };\n }\n\n // Check Ki\n if (player.ki < technique.kiCost) {\n return {\n canExecute: false,\n reason: \"Insufficient Ki\",\n insufficientKi: true,\n };\n }\n\n // Check cooldown\n if (isOnCooldown(technique.id)) {\n return {\n canExecute: false,\n reason: \"Technique on cooldown\",\n onCooldown: true,\n };\n }\n\n // Check required stance\n if (\n technique.requiredStance &&\n player.currentStance !== technique.requiredStance\n ) {\n return {\n canExecute: false,\n reason: `Requires ${technique.requiredStance} stance`,\n wrongStance: true,\n };\n }\n\n return {\n canExecute: true,\n };\n },\n [player.stamina, player.ki, player.currentStance, isOnCooldown]\n );\n\n // Select technique by index\n const selectTechnique = useCallback(\n (index: number) => {\n if (index < 0 || index >= availableTechniques.length) return;\n if (!enabled) return;\n\n setSelectedIndex(index);\n const technique = availableTechniques[index];\n onTechniqueSelected?.(technique);\n },\n [availableTechniques, enabled, onTechniqueSelected]\n );\n\n // Execute currently selected technique\n const executeTechnique = useCallback(\n (indexOverride?: number) => {\n if (!enabled) return;\n\n const index = indexOverride ?? selectedIndex;\n const technique = availableTechniques[index];\n if (!technique) return;\n\n // Validate technique execution\n const validation = validateTechnique(technique);\n if (!validation.canExecute) {\n console.warn(`Cannot execute technique: ${validation.reason}`);\n return;\n }\n\n // Start cooldown\n const now = Date.now();\n const cooldown: TechniqueCooldown = {\n techniqueId: technique.id,\n startTime: now,\n duration: technique.cooldown,\n remaining: technique.cooldown,\n };\n setActiveCooldowns((prev) => [...prev, cooldown]);\n\n // Execute technique\n onTechniqueExecute?.(technique);\n },\n [\n enabled,\n availableTechniques,\n selectedIndex,\n validateTechnique,\n onTechniqueExecute,\n ]\n );\n\n // Keyboard shortcuts for technique selection (Q-E-R-T-Y-F-G-Z-X-C)\n useEffect(() => {\n if (!enabled) return;\n\n const handleKeyPress = (e: KeyboardEvent) => {\n // Ignore keypresses when typing in input fields\n const target = e.target as HTMLElement;\n if (\n target.tagName === \"INPUT\" ||\n target.tagName === \"TEXTAREA\" ||\n target.isContentEditable\n ) {\n return;\n }\n\n const key = e.key.toUpperCase();\n \n // Technique keys: Q, E, R, T, Y, F, G, Z, X, C (10 keys around WASD)\n const techniqueKeys = [\"Q\", \"E\", \"R\", \"T\", \"Y\", \"F\", \"G\", \"Z\", \"X\", \"C\"];\n\n // Prevent default for all technique keys during combat\n if (techniqueKeys.includes(key)) {\n e.preventDefault();\n }\n\n // Map keys to technique indices\n const techniqueIndex = techniqueKeys.indexOf(key);\n if (techniqueIndex !== -1 && techniqueIndex < availableTechniques.length) {\n selectTechnique(techniqueIndex);\n executeTechnique(techniqueIndex);\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyPress);\n return () => window.removeEventListener(\"keydown\", handleKeyPress);\n }, [enabled, availableTechniques, selectTechnique, executeTechnique]);\n\n return {\n availableTechniques,\n selectedIndex,\n activeCooldowns,\n selectTechnique,\n executeTechnique,\n validateTechnique,\n isOnCooldown,\n getRemainingCooldown,\n hasResources,\n };\n}\n\nexport default useTechniqueSelection;\n"],"mappings":"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"}
1
+ {"version":3,"file":"useTechniqueSelection.js","names":[],"sources":["../../src/hooks/useTechniqueSelection.ts"],"sourcesContent":["/**\n * Custom hook for managing technique selection and execution.\n *\n * **Korean**: 기술 선택 관리 (Technique Selection Management)\n *\n * Handles technique selection state, keyboard shortcuts, cooldown tracking,\n * and validation of technique execution based on player resources and stance.\n *\n * @module hooks/useTechniqueSelection\n * @category Combat Hooks\n * @korean 기술선택훅\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { getTechniquesForStanceAndArchetype } from \"../data/techniques\";\nimport { PlayerState } from \"../systems/player\";\nimport {\n Technique,\n TechniqueCooldown,\n TechniqueValidation,\n} from \"../types\";\n\n/**\n * Configuration for technique selection hook.\n */\nexport interface UseTechniqueSelectionConfig {\n /** Player state with resources and stance */\n readonly player: PlayerState;\n\n /** Whether technique selection is enabled */\n readonly enabled?: boolean;\n\n /** Callback when technique is selected */\n readonly onTechniqueSelected?: (technique: Technique) => void;\n\n /** Callback when technique execution is attempted */\n readonly onTechniqueExecute?: (technique: Technique) => void;\n}\n\n/**\n * Technique selection state and actions.\n */\nexport interface UseTechniqueSelectionResult {\n /** Available techniques for player archetype */\n readonly availableTechniques: readonly Technique[];\n\n /** Currently selected technique index */\n readonly selectedIndex: number;\n\n /** Active cooldowns for techniques */\n readonly activeCooldowns: readonly TechniqueCooldown[];\n\n /** Select technique by index */\n readonly selectTechnique: (index: number) => void;\n\n /** Execute currently selected technique */\n readonly executeTechnique: (indexOverride?: number) => void;\n\n /** Check if technique can be executed */\n readonly validateTechnique: (technique: Technique) => TechniqueValidation;\n\n /** Check if technique is on cooldown */\n readonly isOnCooldown: (techniqueId: string) => boolean;\n\n /** Get remaining cooldown time in ms */\n readonly getRemainingCooldown: (techniqueId: string) => number;\n\n /** Check if player has sufficient resources */\n readonly hasResources: (technique: Technique) => boolean;\n}\n\n/**\n * Custom hook for managing technique selection and execution.\n *\n * @param config - Configuration options\n * @returns Technique selection state and actions\n *\n * @example\n * ```typescript\n * const techniqueSelection = useTechniqueSelection({\n * player: playerState,\n * enabled: !isPaused && combatActive,\n * onTechniqueExecute: (technique) => {\n * // Execute technique logic\n * executeCombatTechnique(playerState, opponent, technique);\n * }\n * });\n * ```\n *\n * @public\n */\nexport function useTechniqueSelection(\n config: UseTechniqueSelectionConfig\n): UseTechniqueSelectionResult {\n const {\n player,\n enabled = true,\n onTechniqueSelected,\n onTechniqueExecute,\n } = config;\n\n // Get available techniques based on player's current stance and archetype\n const availableTechniques = useMemo(\n () =>\n getTechniquesForStanceAndArchetype(\n player.currentStance,\n player.archetype\n ),\n [player.currentStance, player.archetype]\n );\n\n // Selected technique state\n const [selectedIndex, setSelectedIndex] = useState(0);\n\n // Cooldown tracking\n const [activeCooldowns, setActiveCooldowns] = useState<TechniqueCooldown[]>(\n []\n );\n\n // Ref for cleanup\n const cooldownUpdateIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n // Update cooldowns every 100ms\n useEffect(() => {\n if (activeCooldowns.length === 0) {\n // Clear any existing interval when no cooldowns\n if (cooldownUpdateIntervalRef.current) {\n clearInterval(cooldownUpdateIntervalRef.current);\n cooldownUpdateIntervalRef.current = null;\n }\n return;\n }\n\n let isMounted = true;\n cooldownUpdateIntervalRef.current = setInterval(() => {\n if (!isMounted) return;\n const now = Date.now();\n setActiveCooldowns((prev) => {\n return prev\n .map((cd) => ({\n ...cd,\n remaining: Math.max(0, cd.startTime + cd.duration - now),\n }))\n .filter((cd) => cd.remaining > 0);\n });\n }, 100);\n\n return () => {\n isMounted = false;\n if (cooldownUpdateIntervalRef.current) {\n clearInterval(cooldownUpdateIntervalRef.current);\n cooldownUpdateIntervalRef.current = null;\n }\n };\n }, [activeCooldowns.length]);\n\n // Check if technique is on cooldown\n const isOnCooldown = useCallback(\n (techniqueId: string): boolean => {\n return activeCooldowns.some(\n (cd) => cd.techniqueId === techniqueId && cd.remaining > 0\n );\n },\n [activeCooldowns]\n );\n\n // Get remaining cooldown time\n const getRemainingCooldown = useCallback(\n (techniqueId: string): number => {\n const cooldown = activeCooldowns.find(\n (cd) => cd.techniqueId === techniqueId\n );\n return cooldown?.remaining ?? 0;\n },\n [activeCooldowns]\n );\n\n // Check if player has sufficient resources\n const hasResources = useCallback(\n (technique: Technique): boolean => {\n return (\n player.stamina >= technique.staminaCost && player.ki >= technique.kiCost\n );\n },\n [player.stamina, player.ki]\n );\n\n // Validate technique execution\n const validateTechnique = useCallback(\n (technique: Technique): TechniqueValidation => {\n // Check stamina\n if (player.stamina < technique.staminaCost) {\n return {\n canExecute: false,\n reason: \"Insufficient stamina\",\n insufficientStamina: true,\n };\n }\n\n // Check Ki\n if (player.ki < technique.kiCost) {\n return {\n canExecute: false,\n reason: \"Insufficient Ki\",\n insufficientKi: true,\n };\n }\n\n // Check cooldown\n if (isOnCooldown(technique.id)) {\n return {\n canExecute: false,\n reason: \"Technique on cooldown\",\n onCooldown: true,\n };\n }\n\n // Check required stance\n if (\n technique.requiredStance &&\n player.currentStance !== technique.requiredStance\n ) {\n return {\n canExecute: false,\n reason: `Requires ${technique.requiredStance} stance`,\n wrongStance: true,\n };\n }\n\n return {\n canExecute: true,\n };\n },\n [player.stamina, player.ki, player.currentStance, isOnCooldown]\n );\n\n // Select technique by index\n const selectTechnique = useCallback(\n (index: number) => {\n if (index < 0 || index >= availableTechniques.length) return;\n if (!enabled) return;\n\n setSelectedIndex(index);\n const technique = availableTechniques[index];\n onTechniqueSelected?.(technique);\n },\n [availableTechniques, enabled, onTechniqueSelected]\n );\n\n // Execute currently selected technique\n const executeTechnique = useCallback(\n (indexOverride?: number) => {\n if (!enabled) return;\n\n const index = indexOverride ?? selectedIndex;\n const technique = availableTechniques[index];\n if (!technique) return;\n\n // Validate technique execution\n const validation = validateTechnique(technique);\n if (!validation.canExecute) {\n console.warn(`Cannot execute technique: ${validation.reason}`);\n return;\n }\n\n // Start cooldown\n const now = Date.now();\n const cooldown: TechniqueCooldown = {\n techniqueId: technique.id,\n startTime: now,\n duration: technique.cooldown,\n remaining: technique.cooldown,\n };\n setActiveCooldowns((prev) => [...prev, cooldown]);\n\n // Execute technique\n onTechniqueExecute?.(technique);\n },\n [\n enabled,\n availableTechniques,\n selectedIndex,\n validateTechnique,\n onTechniqueExecute,\n ]\n );\n\n // Keyboard shortcuts for technique selection (Q-E-R-T-Y-F-G-Z-X-C)\n useEffect(() => {\n if (!enabled) return;\n\n const handleKeyPress = (e: KeyboardEvent) => {\n // Ignore keypresses when typing in input fields\n const target = e.target as HTMLElement;\n if (\n target.tagName === \"INPUT\" ||\n target.tagName === \"TEXTAREA\" ||\n target.isContentEditable\n ) {\n return;\n }\n\n const key = e.key.toUpperCase();\n \n // Technique keys: Q, E, R, T, Y, F, G, Z, X, C (10 keys around WASD)\n const techniqueKeys = [\"Q\", \"E\", \"R\", \"T\", \"Y\", \"F\", \"G\", \"Z\", \"X\", \"C\"];\n\n // Prevent default for all technique keys during combat\n if (techniqueKeys.includes(key)) {\n e.preventDefault();\n }\n\n // Map keys to technique indices\n const techniqueIndex = techniqueKeys.indexOf(key);\n if (techniqueIndex !== -1 && techniqueIndex < availableTechniques.length) {\n selectTechnique(techniqueIndex);\n executeTechnique(techniqueIndex);\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyPress);\n return () => window.removeEventListener(\"keydown\", handleKeyPress);\n }, [enabled, availableTechniques, selectTechnique, executeTechnique]);\n\n return {\n availableTechniques,\n selectedIndex,\n activeCooldowns,\n selectTechnique,\n executeTechnique,\n validateTechnique,\n isOnCooldown,\n getRemainingCooldown,\n hasResources,\n };\n}\n\nexport default useTechniqueSelection;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useThrottle.js","names":[],"sources":["../../src/hooks/useThrottle.ts"],"sourcesContent":["/**\n * useThrottle Hook\n * \n * Throttles a function to execute at most once per specified interval.\n * Useful for high-frequency events like scroll, resize, or touch move.\n * \n * Uses a ref pattern to ensure the latest callback is always called\n * without recreating the throttled function on every render.\n * \n * @module hooks/useThrottle\n * @category Performance\n * @korean 쓰로틀 훅\n */\n\nimport { useCallback, useRef, useLayoutEffect, useEffect } from 'react';\n\n/**\n * Hook to throttle a callback function\n * \n * @param callback - Function to throttle\n * @param delay - Minimum delay between executions in milliseconds\n * @returns Throttled function\n * \n * @example\n * ```tsx\n * const handleTouchMove = useThrottle((event: TouchEvent) => {\n * // Handle touch move\n * }, 16); // ~60fps\n * ```\n */\nexport function useThrottle<T extends (...args: never[]) => void>(\n callback: T,\n delay: number\n): T {\n const lastRunRef = useRef<number>(0);\n const timeoutRef = useRef<NodeJS.Timeout | null>(null);\n const callbackRef = useRef(callback);\n \n // Keep callback ref up to date\n useLayoutEffect(() => {\n callbackRef.current = callback;\n });\n\n // Cleanup pending timeout on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n return useCallback(\n (...args: Parameters<T>) => {\n const now = Date.now();\n const timeSinceLastRun = now - lastRunRef.current;\n\n if (timeSinceLastRun >= delay) {\n // Execute immediately if enough time has passed\n lastRunRef.current = now;\n callbackRef.current(...args);\n } else if (!timeoutRef.current) {\n // Schedule execution for later\n const timeUntilNext = delay - timeSinceLastRun;\n timeoutRef.current = setTimeout(() => {\n lastRunRef.current = Date.now();\n timeoutRef.current = null;\n callbackRef.current(...args);\n }, timeUntilNext);\n }\n },\n [delay]\n ) as T;\n}\n\nexport default useThrottle;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,YACd,UACA,OACG;CACH,MAAM,aAAa,OAAe,EAAE;CACpC,MAAM,aAAa,OAA8B,KAAK;CACtD,MAAM,cAAc,OAAO,SAAS;AAGpC,uBAAsB;AACpB,cAAY,UAAU;GACtB;AAGF,iBAAgB;AACd,eAAa;AACX,OAAI,WAAW,QACb,cAAa,WAAW,QAAQ;;IAGnC,EAAE,CAAC;AAEN,QAAO,aACJ,GAAG,SAAwB;EAC1B,MAAM,MAAM,KAAK,KAAK;EACtB,MAAM,mBAAmB,MAAM,WAAW;AAE1C,MAAI,oBAAoB,OAAO;AAE7B,cAAW,UAAU;AACrB,eAAY,QAAQ,GAAG,KAAK;aACnB,CAAC,WAAW,SAAS;GAE9B,MAAM,gBAAgB,QAAQ;AAC9B,cAAW,UAAU,iBAAiB;AACpC,eAAW,UAAU,KAAK,KAAK;AAC/B,eAAW,UAAU;AACrB,gBAAY,QAAQ,GAAG,KAAK;MAC3B,cAAc;;IAGrB,CAAC,MAAM,CACR"}
1
+ {"version":3,"file":"useThrottle.js","names":[],"sources":["../../src/hooks/useThrottle.ts"],"sourcesContent":["/**\n * useThrottle Hook\n * \n * Throttles a function to execute at most once per specified interval.\n * Useful for high-frequency events like scroll, resize, or touch move.\n * \n * Uses a ref pattern to ensure the latest callback is always called\n * without recreating the throttled function on every render.\n * \n * @module hooks/useThrottle\n * @category Performance\n * @korean 쓰로틀 훅\n */\n\nimport { useCallback, useRef, useLayoutEffect, useEffect } from 'react';\n\n/**\n * Hook to throttle a callback function\n * \n * @param callback - Function to throttle\n * @param delay - Minimum delay between executions in milliseconds\n * @returns Throttled function\n * \n * @example\n * ```tsx\n * const handleTouchMove = useThrottle((event: TouchEvent) => {\n * // Handle touch move\n * }, 16); // ~60fps\n * ```\n */\nexport function useThrottle<T extends (...args: never[]) => void>(\n callback: T,\n delay: number\n): T {\n const lastRunRef = useRef<number>(0);\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const callbackRef = useRef(callback);\n \n // Keep callback ref up to date\n useLayoutEffect(() => {\n callbackRef.current = callback;\n });\n\n // Cleanup pending timeout on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n return useCallback(\n (...args: Parameters<T>) => {\n const now = Date.now();\n const timeSinceLastRun = now - lastRunRef.current;\n\n if (timeSinceLastRun >= delay) {\n // Execute immediately if enough time has passed\n lastRunRef.current = now;\n callbackRef.current(...args);\n } else if (!timeoutRef.current) {\n // Schedule execution for later\n const timeUntilNext = delay - timeSinceLastRun;\n timeoutRef.current = setTimeout(() => {\n lastRunRef.current = Date.now();\n timeoutRef.current = null;\n callbackRef.current(...args);\n }, timeUntilNext);\n }\n },\n [delay]\n ) as T;\n}\n\nexport default useThrottle;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,YACd,UACA,OACG;CACH,MAAM,aAAa,OAAe,EAAE;CACpC,MAAM,aAAa,OAA6C,KAAK;CACrE,MAAM,cAAc,OAAO,SAAS;AAGpC,uBAAsB;AACpB,cAAY,UAAU;GACtB;AAGF,iBAAgB;AACd,eAAa;AACX,OAAI,WAAW,QACb,cAAa,WAAW,QAAQ;;IAGnC,EAAE,CAAC;AAEN,QAAO,aACJ,GAAG,SAAwB;EAC1B,MAAM,MAAM,KAAK,KAAK;EACtB,MAAM,mBAAmB,MAAM,WAAW;AAE1C,MAAI,oBAAoB,OAAO;AAE7B,cAAW,UAAU;AACrB,eAAY,QAAQ,GAAG,KAAK;aACnB,CAAC,WAAW,SAAS;GAE9B,MAAM,gBAAgB,QAAQ;AAC9B,cAAW,UAAU,iBAAiB;AACpC,eAAW,UAAU,KAAK,KAAK;AAC/B,eAAW,UAAU;AACrB,gBAAY,QAAQ,GAAG,KAAK;MAC3B,cAAc;;IAGrB,CAAC,MAAM,CACR"}
@@ -8,6 +8,14 @@ var globalContextLossCount = 0;
8
8
  var globalIsContextLost = false;
9
9
  var lastContextLossTime = 0;
10
10
  /**
11
+ * Get global WebGL context state
12
+ */
13
+ var getWebGLContextState = () => ({
14
+ isLost: globalIsContextLost,
15
+ lossCount: globalContextLossCount,
16
+ timeSinceLastLoss: Date.now() - lastContextLossTime
17
+ });
18
+ /**
11
19
  * Hook to handle WebGL context loss and restoration
12
20
  * Returns state information about the WebGL context
13
21
  */
@@ -113,7 +121,34 @@ var useWebGLContextLossHandler = (options = {}) => {
113
121
  ]);
114
122
  return contextState;
115
123
  };
124
+ /**
125
+ * Check if WebGL is available in the current browser
126
+ */
127
+ var isWebGLAvailable = () => {
128
+ try {
129
+ const canvas = document.createElement("canvas");
130
+ const gl = canvas.getContext("webgl") ?? canvas.getContext("experimental-webgl");
131
+ const available = gl !== null;
132
+ if (gl && "getExtension" in gl) gl.getExtension("WEBGL_lose_context")?.loseContext();
133
+ return available;
134
+ } catch {
135
+ return false;
136
+ }
137
+ };
138
+ /**
139
+ * Check if WebGL2 is available in the current browser
140
+ */
141
+ var isWebGL2Available = () => {
142
+ try {
143
+ const gl = document.createElement("canvas").getContext("webgl2");
144
+ const available = gl !== null;
145
+ if (gl && "getExtension" in gl) gl.getExtension("WEBGL_lose_context")?.loseContext();
146
+ return available;
147
+ } catch {
148
+ return false;
149
+ }
150
+ };
116
151
  //#endregion
117
- export { useWebGLContextLossHandler };
152
+ export { getWebGLContextState, isWebGL2Available, isWebGLAvailable, useWebGLContextLossHandler };
118
153
 
119
154
  //# sourceMappingURL=useWebGLContextLossHandler.js.map