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.
- package/ARCHITECTURE.md +2404 -0
- package/COMBAT_ARCHITECTURE.md +3322 -0
- package/CONTROLS.md +639 -0
- package/CRA-ASSESSMENT.md +508 -0
- package/DATA_MODEL.md +675 -0
- package/ISMS_REFERENCE_MAPPING.md +513 -0
- package/README.md +1 -1
- package/SECURITY_ARCHITECTURE.md +1160 -0
- package/THREAT_MODEL.md +1163 -0
- package/lib/audio/AudioAssetLoader.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
- package/lib/components/screens/combat/hooks/useCombatAudio.js.map +1 -1
- package/lib/components/screens/intro/IntroScreen3D.js +1 -1
- package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
- package/lib/components/screens/training/components/HitFeedbackEffect3D.js.map +1 -1
- package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
- package/lib/components/shared/ui/SplashScreen.js +2 -2
- package/lib/data/archetypeClothing.js +1 -1
- package/lib/data/archetypePhysicalAttributes.js +158 -1
- package/lib/data/archetypePhysicalAttributes.js.map +1 -1
- package/lib/data/index.d.ts +14 -0
- package/lib/data/index.d.ts.map +1 -0
- package/lib/data/index.js +43 -0
- package/lib/data/index.js.map +1 -0
- package/lib/data/techniqueMappings.js +47 -2
- package/lib/data/techniqueMappings.js.map +1 -1
- package/lib/data/techniques.js +1 -1
- package/lib/hooks/index.d.ts +29 -0
- package/lib/hooks/index.d.ts.map +1 -0
- package/lib/hooks/index.js +53 -0
- package/lib/hooks/index.js.map +1 -0
- package/lib/hooks/useCombatTimer.js.map +1 -1
- package/lib/hooks/useDebounce.js +52 -0
- package/lib/hooks/useDebounce.js.map +1 -0
- package/lib/hooks/usePauseMenu.js +60 -0
- package/lib/hooks/usePauseMenu.js.map +1 -0
- package/lib/hooks/useResponsiveLayout.js +160 -0
- package/lib/hooks/useResponsiveLayout.js.map +1 -0
- package/lib/hooks/useRoundTransition.js.map +1 -1
- package/lib/hooks/useTechniqueSelection.js.map +1 -1
- package/lib/hooks/useThrottle.js.map +1 -1
- package/lib/hooks/useWebGLContextLossHandler.js +36 -1
- package/lib/hooks/useWebGLContextLossHandler.js.map +1 -1
- package/lib/hooks/useWindowSize.js +19 -1
- package/lib/hooks/useWindowSize.js.map +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -1
- package/package.json +22 -6
package/lib/data/techniques.js
CHANGED
|
@@ -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<
|
|
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<
|
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2FA,SAAgB,sBACd,QAC6B;CAC7B,MAAM,EACJ,QACA,UAAU,MACV,qBACA,uBACE;CAGJ,MAAM,sBAAsB,cAExB,mCACE,OAAO,eACP,OAAO,UACR,EACH,CAAC,OAAO,eAAe,OAAO,UAAU,CACzC;CAGD,MAAM,CAAC,eAAe,oBAAoB,SAAS,EAAE;CAGrD,MAAM,CAAC,iBAAiB,sBAAsB,SAC5C,EAAE,CACH;CAGD,MAAM,4BAA4B,OAA8C,KAAK;AAGrF,iBAAgB;AACd,MAAI,gBAAgB,WAAW,GAAG;AAEhC,OAAI,0BAA0B,SAAS;AACrC,kBAAc,0BAA0B,QAAQ;AAChD,8BAA0B,UAAU;;AAEtC;;EAGF,IAAI,YAAY;AAChB,4BAA0B,UAAU,kBAAkB;AACpD,OAAI,CAAC,UAAW;GAChB,MAAM,MAAM,KAAK,KAAK;AACtB,uBAAoB,SAAS;AAC3B,WAAO,KACJ,KAAK,QAAQ;KACZ,GAAG;KACH,WAAW,KAAK,IAAI,GAAG,GAAG,YAAY,GAAG,WAAW,IAAI;KACzD,EAAE,CACF,QAAQ,OAAO,GAAG,YAAY,EAAE;KACnC;KACD,IAAI;AAEP,eAAa;AACX,eAAY;AACZ,OAAI,0BAA0B,SAAS;AACrC,kBAAc,0BAA0B,QAAQ;AAChD,8BAA0B,UAAU;;;IAGvC,CAAC,gBAAgB,OAAO,CAAC;CAG5B,MAAM,eAAe,aAClB,gBAAiC;AAChC,SAAO,gBAAgB,MACpB,OAAO,GAAG,gBAAgB,eAAe,GAAG,YAAY,EAC1D;IAEH,CAAC,gBAAgB,CAClB;CAGD,MAAM,uBAAuB,aAC1B,gBAAgC;AAI/B,SAHiB,gBAAgB,MAC9B,OAAO,GAAG,gBAAgB,YAC5B,EACgB,aAAa;IAEhC,CAAC,gBAAgB,CAClB;CAGD,MAAM,eAAe,aAClB,cAAkC;AACjC,SACE,OAAO,WAAW,UAAU,eAAe,OAAO,MAAM,UAAU;IAGtE,CAAC,OAAO,SAAS,OAAO,GAAG,CAC5B;CAGD,MAAM,oBAAoB,aACvB,cAA8C;AAE7C,MAAI,OAAO,UAAU,UAAU,YAC7B,QAAO;GACL,YAAY;GACZ,QAAQ;GACR,qBAAqB;GACtB;AAIH,MAAI,OAAO,KAAK,UAAU,OACxB,QAAO;GACL,YAAY;GACZ,QAAQ;GACR,gBAAgB;GACjB;AAIH,MAAI,aAAa,UAAU,GAAG,CAC5B,QAAO;GACL,YAAY;GACZ,QAAQ;GACR,YAAY;GACb;AAIH,MACE,UAAU,kBACV,OAAO,kBAAkB,UAAU,eAEnC,QAAO;GACL,YAAY;GACZ,QAAQ,YAAY,UAAU,eAAe;GAC7C,aAAa;GACd;AAGH,SAAO,EACL,YAAY,MACb;IAEH;EAAC,OAAO;EAAS,OAAO;EAAI,OAAO;EAAe;EAAa,CAChE;CAGD,MAAM,kBAAkB,aACrB,UAAkB;AACjB,MAAI,QAAQ,KAAK,SAAS,oBAAoB,OAAQ;AACtD,MAAI,CAAC,QAAS;AAEd,mBAAiB,MAAM;EACvB,MAAM,YAAY,oBAAoB;AACtC,wBAAsB,UAAU;IAElC;EAAC;EAAqB;EAAS;EAAoB,CACpD;CAGD,MAAM,mBAAmB,aACtB,kBAA2B;AAC1B,MAAI,CAAC,QAAS;EAGd,MAAM,YAAY,oBADJ,iBAAiB;AAE/B,MAAI,CAAC,UAAW;EAGhB,MAAM,aAAa,kBAAkB,UAAU;AAC/C,MAAI,CAAC,WAAW,YAAY;AAC1B,WAAQ,KAAK,6BAA6B,WAAW,SAAS;AAC9D;;EAIF,MAAM,MAAM,KAAK,KAAK;EACtB,MAAM,WAA8B;GAClC,aAAa,UAAU;GACvB,WAAW;GACX,UAAU,UAAU;GACpB,WAAW,UAAU;GACtB;AACD,sBAAoB,SAAS,CAAC,GAAG,MAAM,SAAS,CAAC;AAGjD,uBAAqB,UAAU;IAEjC;EACE;EACA;EACA;EACA;EACA;EACD,CACF;AAGD,iBAAgB;AACd,MAAI,CAAC,QAAS;EAEd,MAAM,kBAAkB,MAAqB;GAE3C,MAAM,SAAS,EAAE;AACjB,OACE,OAAO,YAAY,WACnB,OAAO,YAAY,cACnB,OAAO,kBAEP;GAGF,MAAM,MAAM,EAAE,IAAI,aAAa;GAG/B,MAAM,gBAAgB;IAAC;IAAK;IAAK;IAAK;IAAK;IAAK;IAAK;IAAK;IAAK;IAAK;IAAI;AAGxE,OAAI,cAAc,SAAS,IAAI,CAC7B,GAAE,gBAAgB;GAIpB,MAAM,iBAAiB,cAAc,QAAQ,IAAI;AACjD,OAAI,mBAAmB,MAAM,iBAAiB,oBAAoB,QAAQ;AACxE,oBAAgB,eAAe;AAC/B,qBAAiB,eAAe;;;AAIpC,SAAO,iBAAiB,WAAW,eAAe;AAClD,eAAa,OAAO,oBAAoB,WAAW,eAAe;IACjE;EAAC;EAAS;EAAqB;EAAiB;EAAiB,CAAC;AAErE,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD"}
|
|
@@ -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<
|
|
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
|