blacktrigram 0.7.4 → 0.7.8
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/README.md +1 -1
- 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/hooks/useCombatTimer.js.map +1 -1
- 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/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTrainingActions.js","names":[],"sources":["../../../../../src/components/screens/training/hooks/useTrainingActions.ts"],"sourcesContent":["/**\n * useTrainingActions Hook - Training Action Handlers\n *\n * Custom hook for managing training action handlers.\n * Mirrors useCombatActions pattern for consistency.\n *\n * @korean 훈련액션훅 - 훈련 액션 핸들러 관리\n */\n\nimport { useCallback, useRef } from \"react\";\nimport {\n AudioBodyRegion,\n ImpactIntensity,\n} from \"../../../../audio/types\";\nimport type { AttackIntensity } from \"../../../screens/combat/hooks/useCombatAudio\";\nimport { getArchetypePhysicalAttributes } from \"../../../../data/archetypePhysicalAttributes\";\nimport {\n AnimationState,\n AnimationType,\n getAnimationForTechnique,\n} from \"../../../../systems/animation\";\nimport { physicalReachCalculator } from \"../../../../systems/physics\";\nimport { getTechniquesByStance } from \"../../../../systems/trigram/techniques\";\nimport { KoreanTechniquesSystem } from \"../../../../systems/trigram/KoreanTechniques\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../../systems/trigram/types\";\nimport type { KoreanTechnique } from \"../../../../systems/vitalpoint/types\";\nimport {\n CombatAttackType,\n DamageType,\n PlayerArchetype,\n Position,\n TrigramStance,\n} from \"../../../../types/common\";\nimport {\n DEFAULT_BODY_RADIUS_METERS,\n METERS_TO_TRAINING_UNITS,\n} from \"../../../../types/physicsConstants\";\nimport { calculateDistance3D } from \"../../../../utils/math\";\nimport { TrainingActions, TrainingScreenState } from \"./useTrainingState\";\n\nexport interface UseTrainingActionsConfig {\n readonly state: TrainingScreenState;\n readonly actions: TrainingActions;\n readonly playerPosition: Position;\n readonly player3DPosition: [number, number, number];\n readonly dummyPosition: [number, number, number];\n readonly playerArchetype: import(\"../../../../types/common\").PlayerArchetype;\n readonly playerStance: TrigramStance;\n /** Ref to current selected technique's animation type for distance-based hit detection */\n readonly currentTechniqueAnimationTypeRef: React.MutableRefObject<AnimationType>;\n readonly audio: {\n readonly playSFX: (sound: string, volume?: number) => Promise<void>;\n };\n /** Attack sound function from useCombatAudio for playing attack whoosh sounds */\n readonly playAttackSound?: (intensity: AttackIntensity) => Promise<void>;\n /** Bone impact audio function from useCombatAudio or similar hook */\n readonly playBoneImpactSound?: (options: {\n region?: AudioBodyRegion;\n intensity?: ImpactIntensity;\n damage?: number;\n remainingHealth?: number;\n vitalPoint?: boolean;\n hitPosition?: { x: number; y: number; z?: number };\n }) => Promise<void>;\n readonly onPlayerUpdate: (updates: {\n currentStance?: TrigramStance;\n lastActionTime?: number;\n position?: Position;\n }) => void;\n readonly playerAnimation: {\n readonly transitionTo: (state: AnimationState) => boolean;\n readonly transitionToStanceGuard: (stance: TrigramStance) => boolean;\n readonly currentState: string;\n };\n /** External ref to store pending attack data - shared with animation events */\n readonly pendingAttackRef: React.MutableRefObject<{\n accuracy: number;\n vitalPoint: string;\n animationType?: AnimationType;\n startTime?: number;\n techniqueId?: string;\n } | null>;\n /** Currently selected technique ID (from technique bar) */\n readonly selectedTechniqueId?: string;\n /** Callback to set the visual attack animation */\n readonly setAttackAnimation?: (animationName: string | undefined) => void;\n}\n\nexport interface UseTrainingActionsReturn {\n readonly handleStartTraining: () => void;\n readonly handleStopTraining: () => void;\n readonly handleDummyHit: (\n vitalPointId: string,\n attackContext?: {\n animationType?: AnimationType;\n techniqueId?: string;\n },\n ) => boolean;\n readonly handleDummyDefeated: () => void;\n readonly handleStanceChange: (stanceIndex: number) => void;\n readonly handleAttack: () => void;\n}\n\n/**\n * Get the best default technique for an archetype based on current stance.\n *\n * Each archetype has preferred combat styles:\n * - MUSA: Direct strikes, joint techniques (BLUNT/JOINT damage)\n * - AMSALJA: Nerve strikes, pressure points (NERVE/PRESSURE damage)\n * - HACKER: Precise calculated strikes (high accuracy)\n * - JEONGBO_YOWON: Psychological pressure, submissions (PRESSURE/JOINT)\n * - JOJIK_POKRYEOKBAE: High damage brutal strikes\n *\n * @korean 원형별 기본 기술 선택 - 8개 자세에 따른 최적 기술\n */\nfunction getDefaultTechniqueForArchetype(\n archetype: PlayerArchetype,\n stance: TrigramStance,\n): KoreanTechnique | undefined {\n const techniques = getTechniquesByStance(stance);\n if (techniques.length === 0) return undefined;\n\n // Score each technique based on archetype preference\n const scoredTechniques = techniques.map((tech) => {\n let score = 0;\n const damageType = tech.damageType;\n const attackType = tech.type;\n const isAdvanced =\n (tech.kiCost || 0) >= 10 || (tech.staminaCost || 0) >= 15;\n\n switch (archetype) {\n case PlayerArchetype.MUSA:\n // Musa prefers: strikes, blocks, counter-attacks (BLUNT/JOINT/CRUSHING)\n if (damageType === DamageType.JOINT) score += 30;\n if (damageType === DamageType.CRUSHING) score += 25;\n if (damageType === DamageType.BLUNT) score += 20;\n if (attackType === CombatAttackType.STRIKE) score += 15;\n if (attackType === CombatAttackType.PUNCH) score += 10;\n if (isAdvanced) score += 10;\n break;\n\n case PlayerArchetype.AMSALJA:\n // Amsalja prefers: nerve strikes, pressure points, thrusts\n if (damageType === DamageType.NERVE) score += 35;\n if (damageType === DamageType.PRESSURE) score += 30;\n if (attackType === CombatAttackType.NERVE_STRIKE) score += 25;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 25;\n if (attackType === CombatAttackType.THRUST) score += 15;\n if ((tech.accuracy || 0) >= 0.9) score += 10;\n break;\n\n case PlayerArchetype.HACKER:\n // Hacker prefers: high accuracy, calculated strikes\n if ((tech.accuracy || 0) >= 0.9) score += 30;\n if ((tech.accuracy || 0) >= 0.8) score += 15;\n if (damageType === DamageType.NERVE) score += 20;\n if (damageType === DamageType.INTERNAL) score += 20;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 15;\n break;\n\n case PlayerArchetype.JEONGBO_YOWON:\n // Jeongbo prefers: psychological pressure, submissions\n if (damageType === DamageType.PRESSURE) score += 30;\n if (damageType === DamageType.JOINT) score += 25;\n if (attackType === CombatAttackType.GRAPPLE) score += 20;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 15;\n break;\n\n case PlayerArchetype.JOJIK_POKRYEOKBAE:\n // Jojik prefers: high damage, brutal techniques\n if ((tech.damage || 0) >= 35) score += 35;\n if ((tech.damage || 0) >= 30) score += 20;\n if (damageType === DamageType.SLASHING) score += 20;\n if (damageType === DamageType.PIERCING) score += 15;\n if (damageType === DamageType.CRUSHING) score += 15;\n if (attackType === CombatAttackType.KICK) score += 10;\n break;\n }\n\n return { technique: tech, score };\n });\n\n // Sort by score descending, return highest scoring technique\n scoredTechniques.sort((a, b) => b.score - a.score);\n return scoredTechniques[0]?.technique;\n}\n\n/**\n * Calculate hit accuracy based on distance and effective reach.\n * Uses PhysicalReachCalculator for animation-aware reach calculation.\n *\n * Distance logic matches CombatSystem: if out of reach, guaranteed miss (accuracy 0).\n * Training scene coordinates are in meters, using METERS_TO_TRAINING_UNITS = 1.0.\n *\n * **IMPORTANT**: Distance calculation accounts for body radius.\n * Center-to-center distance is adjusted by subtracting target body radius\n * because attacks hit the body surface, not the center point.\n * The target body radius is calculated from physical attributes for archetypes,\n * or uses DEFAULT_BODY_RADIUS_METERS for training dummies.\n *\n * Example: If player center is 1.5m from dummy center, but dummy body\n * extends 0.23m from center, the effective distance to hit is 1.27m.\n *\n * @korean 거리 기반 명중률 계산 - 사정거리 밖이면 빗나감\n */\nfunction calculateHitAccuracy(\n playerPos: [number, number, number],\n dummyPos: [number, number, number],\n archetype: import(\"../../../../types/common\").PlayerArchetype,\n stance: TrigramStance,\n animationType?: AnimationType,\n reachConfig?: import(\"../../../../types/physics\").PhysicalReachConfig,\n): number {\n // Calculate 3D distance between player and dummy centers (in meters)\n const centerToCenterDistance = calculateDistance3D(playerPos, dummyPos);\n\n // Get player's physical attributes for reach calculation\n const playerPhysicalAttributes = getArchetypePhysicalAttributes(archetype);\n\n // Training dummy uses default body radius since it has no archetype\n // For combat between players, we would use calculateBodyRadius(targetPhysicalAttributes)\n // 훈련 더미는 원형이 없으므로 기본 몸체 반경 사용\n const targetBodyRadius = DEFAULT_BODY_RADIUS_METERS;\n\n // Effective distance = center-to-center minus target body radius only\n // Note: PhysicalReachCalculator already includes player body pivot/offset in reach calculation,\n // so we only subtract the target radius to avoid double-counting.\n // 유효 거리 = 중심간 거리 - 더미 몸체 반경 (플레이어 몸체 오프셋은 도달 거리에 포함됨)\n const effectiveDistance = Math.max(\n 0,\n centerToCenterDistance - targetBodyRadius,\n );\n\n // If animation type is available, use physics-based reach calculation\n // We use calculateMaxReach (peak time reach) because:\n // 1. Training hit detection happens at animation frame 6 (~100ms)\n // 2. But technique hit timings expect longer animations (e.g., roundhouse 200-480ms)\n // 3. Using max reach ensures the technique can hit if within peak reach range\n // 4. This matches intuitive behavior - if you're close enough to be hit by the kick, it hits\n // 훈련 타격 감지는 최대 도달 거리 사용 (애니메이션 타이밍과 기술 타이밍 불일치 보정)\n if (animationType !== undefined) {\n const maxReachMeters = physicalReachCalculator.calculateMaxReach(\n playerPhysicalAttributes,\n animationType,\n stance,\n reachConfig, // Use technique's designed reach if provided\n );\n\n // Convert reach from meters to training scene units.\n // Training scenes are authored in real-world meters, so we intentionally\n // use a 1:1 conversion here (METERS_TO_TRAINING_UNITS = 1.0).\n // Combat AI uses METERS_TO_PIXELS_SCALE (100) for pixel coordinates.\n const reachInUnits = maxReachMeters * METERS_TO_TRAINING_UNITS;\n\n // STRICT DISTANCE CHECK (matches CombatSystem behavior):\n // Out of reach = guaranteed miss with accuracy 0\n if (effectiveDistance > reachInUnits) {\n return 0;\n }\n\n // Within reach: accuracy based on how centered the hit is\n // Closer = higher accuracy (0.7 to 1.0 range)\n return Math.max(0.7, 1.0 - (effectiveDistance / reachInUnits) * 0.3);\n }\n\n // Fallback: use default punch reach (0.7 meters) for legacy behavior\n const defaultReach = 0.7 * METERS_TO_TRAINING_UNITS;\n if (effectiveDistance > defaultReach) {\n return 0; // Out of reach = miss\n }\n // Within default reach: linear accuracy based on distance\n return Math.max(0.5, 1.0 - (effectiveDistance / defaultReach) * 0.5);\n}\n\n/**\n * useTrainingActions hook\n * Provides training action handlers with proper memoization\n */\nexport function useTrainingActions(\n config: UseTrainingActionsConfig,\n): UseTrainingActionsReturn {\n const {\n state,\n actions,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n currentTechniqueAnimationTypeRef,\n audio,\n playBoneImpactSound,\n playAttackSound,\n onPlayerUpdate,\n playerAnimation,\n pendingAttackRef,\n selectedTechniqueId,\n setAttackAnimation,\n } = config;\n\n // Ref to store timeout for dummy reset\n const dummyResetTimeoutRef = useRef<NodeJS.Timeout | null>(null);\n\n const handleStartTraining = useCallback(() => {\n actions.startTraining();\n audio.playSFX(\"menu_select\");\n }, [actions, audio]);\n\n const handleStopTraining = useCallback(() => {\n // Clear any pending dummy reset timeout\n if (dummyResetTimeoutRef.current) {\n clearTimeout(dummyResetTimeoutRef.current);\n dummyResetTimeoutRef.current = null;\n }\n actions.stopTraining();\n audio.playSFX(\"menu_back\");\n }, [actions, audio]);\n\n const handleDummyDefeated = useCallback(() => {\n actions.setFeedback(\"훈련 더미 무력화! | Dummy Defeated!\");\n audio.playSFX(\"ki_release\");\n\n // Clear any existing timeout\n if (dummyResetTimeoutRef.current) {\n clearTimeout(dummyResetTimeoutRef.current);\n }\n\n // Reset dummy health after delay\n dummyResetTimeoutRef.current = setTimeout(() => {\n actions.resetDummy();\n }, 2000);\n }, [actions, audio]);\n\n const handleDummyHit = useCallback(\n (\n _vitalPointId: string,\n attackContext?: {\n animationType?: AnimationType;\n techniqueId?: string;\n },\n ): boolean => {\n // Get animation context from the passed attackContext parameter\n // (TrainingScreen3D.tsx should pass the attackData before clearing the ref)\n const animationType = attackContext?.animationType;\n\n // Get technique's reachConfig for accurate reach calculation\n // Priority: attackContext.techniqueId (resolved in handleAttack) > selectedTechniqueId\n // This ensures default techniques (chosen when no explicit selection) also get their reachConfig\n let reachConfig: import(\"../../../../types/physics\").PhysicalReachConfig | undefined;\n const resolvedTechniqueId =\n attackContext?.techniqueId ?? selectedTechniqueId;\n if (resolvedTechniqueId) {\n const technique = KoreanTechniquesSystem.getTechniqueById(resolvedTechniqueId);\n reachConfig = technique?.reachConfig;\n }\n\n const accuracy = calculateHitAccuracy(\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n animationType,\n reachConfig,\n );\n\n // Determine hit position (dummy center)\n const hitPosition: [number, number, number] = [\n dummyPosition[0],\n 1.5,\n dummyPosition[2],\n ];\n\n if (accuracy > 0.5) {\n const points = Math.round(accuracy * 100);\n const damage = Math.round(accuracy * 15); // 0-15 damage based on accuracy\n const isPerfect = accuracy > 0.9;\n\n // Register hit with state (only counts if training)\n if (state.isTraining) {\n actions.registerHit(points, damage, isPerfect);\n }\n\n // Play bone impact audio with anatomical feedback using proper audio system\n if (playBoneImpactSound) {\n void playBoneImpactSound({\n damage,\n remainingHealth: 100, // Dummy has 100 health\n vitalPoint: false,\n hitPosition: { x: hitPosition[0], y: hitPosition[1], z: hitPosition[2] },\n });\n }\n\n // Determine feedback and sound\n let effectType: \"success\" | \"perfect\";\n if (isPerfect) {\n actions.setFeedback(\"완벽한 타격! | Perfect Strike!\");\n audio.playSFX(\"ki_release\");\n effectType = \"perfect\";\n } else if (accuracy > 0.7) {\n actions.setFeedback(\"좋은 타격! | Good Strike!\");\n audio.playSFX(\"ki_charge\");\n effectType = \"success\";\n } else {\n actions.setFeedback(\"타격 성공 | Strike Success\");\n audio.playSFX(\"menu_click\");\n effectType = \"success\";\n }\n\n // Add hit effect\n actions.addHitEffect({\n position: hitPosition,\n type: effectType,\n visible: true,\n damage,\n });\n\n return true;\n } else {\n // Register miss (only counts if training)\n if (state.isTraining) {\n actions.registerMiss();\n }\n actions.setFeedback(\"빗나감 | Miss - Out of reach!\");\n audio.playSFX(\"menu_navigate\");\n\n // Add miss effect\n actions.addHitEffect({\n position: hitPosition,\n type: \"miss\",\n visible: true,\n });\n\n return false;\n }\n },\n [\n state.isTraining,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n actions,\n audio,\n playBoneImpactSound,\n selectedTechniqueId,\n ],\n );\n\n const handleStanceChange = useCallback(\n (stanceIndex: number) => {\n actions.setStanceIndex(stanceIndex);\n const stance = TRIGRAM_STANCES_ORDER[stanceIndex];\n if (stance) {\n // Directly transition to stance guard animation (skips transitional animation)\n // 자세 가드 애니메이션으로 직접 전환 (전환 애니메이션 생략)\n playerAnimation.transitionToStanceGuard(stance);\n onPlayerUpdate({ currentStance: stance });\n audio.playSFX(\"stance_change\");\n }\n },\n [actions, onPlayerUpdate, audio, playerAnimation],\n );\n\n const handleAttack = useCallback(() => {\n // Determine which technique to use:\n // 1. If a technique is explicitly selected from the TechniqueBar, use that\n // 2. Otherwise, use the best default technique for the archetype + stance\n // 사용할 기술 결정: 명시적 선택 또는 원형+자세 기반 기본 기술\n let techniqueToUse: KoreanTechnique | undefined;\n let techniqueId = selectedTechniqueId;\n\n if (!techniqueId) {\n // No technique explicitly selected - get default for archetype + stance\n // 명시적 선택 없음 - 원형과 자세에 맞는 기본 기술 사용\n const defaultTechnique = getDefaultTechniqueForArchetype(\n playerArchetype,\n playerStance,\n );\n if (defaultTechnique) {\n techniqueToUse = defaultTechnique;\n techniqueId = defaultTechnique.id;\n }\n }\n\n // Get the animation type from the technique\n // 기술에서 애니메이션 타입 가져오기\n let animationType = currentTechniqueAnimationTypeRef.current;\n if (techniqueToUse?.animationType) {\n animationType = techniqueToUse.animationType;\n currentTechniqueAnimationTypeRef.current = animationType;\n }\n\n const startTime = performance.now() / 1000; // Current time in seconds\n\n const accuracy = calculateHitAccuracy(\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n animationType,\n techniqueToUse?.reachConfig, // Pass technique's reachConfig for accurate reach\n );\n\n pendingAttackRef.current = {\n accuracy,\n vitalPoint: state.selectedVitalPoint ?? \"generic\",\n animationType,\n startTime,\n techniqueId, // Store resolved technique ID for handleDummyHit\n };\n\n // Set visual attack animation based on technique (AnimationRegistry lookup)\n // This ensures the 3D model plays the correct technique animation (kick vs punch vs elbow)\n // 기술에 따른 시각적 공격 애니메이션 설정 (발차기 vs 주먹 vs 팔꿈치)\n if (setAttackAnimation && techniqueId) {\n const animationName = getAnimationForTechnique(techniqueId);\n setAttackAnimation(animationName);\n }\n\n // Trigger attack animation - this will fire onFrame event at frame 6\n playerAnimation.transitionTo(AnimationState.ATTACK);\n\n // Play attack sound based on technique damage/intensity\n // Resolve technique data if we only have an ID (from TechniqueBar selection)\n if (!techniqueToUse && selectedTechniqueId) {\n // Technique selected from TechniqueBar but not yet resolved\n techniqueToUse = KoreanTechniquesSystem.getTechniqueById(selectedTechniqueId);\n }\n\n if (playAttackSound) {\n // Prefer explicit technique damage when available\n const damage = techniqueToUse?.damage ?? 10;\n const intensity: AttackIntensity =\n damage >= 40\n ? \"critical\"\n : damage >= 25\n ? \"heavy\"\n : damage >= 10\n ? \"medium\"\n : \"light\";\n void playAttackSound(intensity);\n } else {\n // Fallback to generic whoosh if playAttackSound not available\n audio.playSFX(\"whoosh\");\n }\n }, [\n state.selectedVitalPoint,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n currentTechniqueAnimationTypeRef,\n playerAnimation,\n audio,\n playAttackSound,\n pendingAttackRef,\n selectedTechniqueId,\n setAttackAnimation,\n ]);\n\n return {\n handleStartTraining,\n handleStopTraining,\n handleDummyHit,\n handleDummyDefeated,\n handleStanceChange,\n handleAttack,\n };\n}\n\nexport default useTrainingActions;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useTrainingActions.js","names":[],"sources":["../../../../../src/components/screens/training/hooks/useTrainingActions.ts"],"sourcesContent":["/**\n * useTrainingActions Hook - Training Action Handlers\n *\n * Custom hook for managing training action handlers.\n * Mirrors useCombatActions pattern for consistency.\n *\n * @korean 훈련액션훅 - 훈련 액션 핸들러 관리\n */\n\nimport { useCallback, useRef } from \"react\";\nimport {\n AudioBodyRegion,\n ImpactIntensity,\n} from \"../../../../audio/types\";\nimport type { AttackIntensity } from \"../../../screens/combat/hooks/useCombatAudio\";\nimport { getArchetypePhysicalAttributes } from \"../../../../data/archetypePhysicalAttributes\";\nimport {\n AnimationState,\n AnimationType,\n getAnimationForTechnique,\n} from \"../../../../systems/animation\";\nimport { physicalReachCalculator } from \"../../../../systems/physics\";\nimport { getTechniquesByStance } from \"../../../../systems/trigram/techniques\";\nimport { KoreanTechniquesSystem } from \"../../../../systems/trigram/KoreanTechniques\";\nimport { TRIGRAM_STANCES_ORDER } from \"../../../../systems/trigram/types\";\nimport type { KoreanTechnique } from \"../../../../systems/vitalpoint/types\";\nimport {\n CombatAttackType,\n DamageType,\n PlayerArchetype,\n Position,\n TrigramStance,\n} from \"../../../../types/common\";\nimport {\n DEFAULT_BODY_RADIUS_METERS,\n METERS_TO_TRAINING_UNITS,\n} from \"../../../../types/physicsConstants\";\nimport { calculateDistance3D } from \"../../../../utils/math\";\nimport { TrainingActions, TrainingScreenState } from \"./useTrainingState\";\n\nexport interface UseTrainingActionsConfig {\n readonly state: TrainingScreenState;\n readonly actions: TrainingActions;\n readonly playerPosition: Position;\n readonly player3DPosition: [number, number, number];\n readonly dummyPosition: [number, number, number];\n readonly playerArchetype: import(\"../../../../types/common\").PlayerArchetype;\n readonly playerStance: TrigramStance;\n /** Ref to current selected technique's animation type for distance-based hit detection */\n readonly currentTechniqueAnimationTypeRef: React.MutableRefObject<AnimationType>;\n readonly audio: {\n readonly playSFX: (sound: string, volume?: number) => Promise<void>;\n };\n /** Attack sound function from useCombatAudio for playing attack whoosh sounds */\n readonly playAttackSound?: (intensity: AttackIntensity) => Promise<void>;\n /** Bone impact audio function from useCombatAudio or similar hook */\n readonly playBoneImpactSound?: (options: {\n region?: AudioBodyRegion;\n intensity?: ImpactIntensity;\n damage?: number;\n remainingHealth?: number;\n vitalPoint?: boolean;\n hitPosition?: { x: number; y: number; z?: number };\n }) => Promise<void>;\n readonly onPlayerUpdate: (updates: {\n currentStance?: TrigramStance;\n lastActionTime?: number;\n position?: Position;\n }) => void;\n readonly playerAnimation: {\n readonly transitionTo: (state: AnimationState) => boolean;\n readonly transitionToStanceGuard: (stance: TrigramStance) => boolean;\n readonly currentState: string;\n };\n /** External ref to store pending attack data - shared with animation events */\n readonly pendingAttackRef: React.MutableRefObject<{\n accuracy: number;\n vitalPoint: string;\n animationType?: AnimationType;\n startTime?: number;\n techniqueId?: string;\n } | null>;\n /** Currently selected technique ID (from technique bar) */\n readonly selectedTechniqueId?: string;\n /** Callback to set the visual attack animation */\n readonly setAttackAnimation?: (animationName: string | undefined) => void;\n}\n\nexport interface UseTrainingActionsReturn {\n readonly handleStartTraining: () => void;\n readonly handleStopTraining: () => void;\n readonly handleDummyHit: (\n vitalPointId: string,\n attackContext?: {\n animationType?: AnimationType;\n techniqueId?: string;\n },\n ) => boolean;\n readonly handleDummyDefeated: () => void;\n readonly handleStanceChange: (stanceIndex: number) => void;\n readonly handleAttack: () => void;\n}\n\n/**\n * Get the best default technique for an archetype based on current stance.\n *\n * Each archetype has preferred combat styles:\n * - MUSA: Direct strikes, joint techniques (BLUNT/JOINT damage)\n * - AMSALJA: Nerve strikes, pressure points (NERVE/PRESSURE damage)\n * - HACKER: Precise calculated strikes (high accuracy)\n * - JEONGBO_YOWON: Psychological pressure, submissions (PRESSURE/JOINT)\n * - JOJIK_POKRYEOKBAE: High damage brutal strikes\n *\n * @korean 원형별 기본 기술 선택 - 8개 자세에 따른 최적 기술\n */\nfunction getDefaultTechniqueForArchetype(\n archetype: PlayerArchetype,\n stance: TrigramStance,\n): KoreanTechnique | undefined {\n const techniques = getTechniquesByStance(stance);\n if (techniques.length === 0) return undefined;\n\n // Score each technique based on archetype preference\n const scoredTechniques = techniques.map((tech) => {\n let score = 0;\n const damageType = tech.damageType;\n const attackType = tech.type;\n const isAdvanced =\n (tech.kiCost || 0) >= 10 || (tech.staminaCost || 0) >= 15;\n\n switch (archetype) {\n case PlayerArchetype.MUSA:\n // Musa prefers: strikes, blocks, counter-attacks (BLUNT/JOINT/CRUSHING)\n if (damageType === DamageType.JOINT) score += 30;\n if (damageType === DamageType.CRUSHING) score += 25;\n if (damageType === DamageType.BLUNT) score += 20;\n if (attackType === CombatAttackType.STRIKE) score += 15;\n if (attackType === CombatAttackType.PUNCH) score += 10;\n if (isAdvanced) score += 10;\n break;\n\n case PlayerArchetype.AMSALJA:\n // Amsalja prefers: nerve strikes, pressure points, thrusts\n if (damageType === DamageType.NERVE) score += 35;\n if (damageType === DamageType.PRESSURE) score += 30;\n if (attackType === CombatAttackType.NERVE_STRIKE) score += 25;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 25;\n if (attackType === CombatAttackType.THRUST) score += 15;\n if ((tech.accuracy || 0) >= 0.9) score += 10;\n break;\n\n case PlayerArchetype.HACKER:\n // Hacker prefers: high accuracy, calculated strikes\n if ((tech.accuracy || 0) >= 0.9) score += 30;\n if ((tech.accuracy || 0) >= 0.8) score += 15;\n if (damageType === DamageType.NERVE) score += 20;\n if (damageType === DamageType.INTERNAL) score += 20;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 15;\n break;\n\n case PlayerArchetype.JEONGBO_YOWON:\n // Jeongbo prefers: psychological pressure, submissions\n if (damageType === DamageType.PRESSURE) score += 30;\n if (damageType === DamageType.JOINT) score += 25;\n if (attackType === CombatAttackType.GRAPPLE) score += 20;\n if (attackType === CombatAttackType.PRESSURE_POINT) score += 15;\n break;\n\n case PlayerArchetype.JOJIK_POKRYEOKBAE:\n // Jojik prefers: high damage, brutal techniques\n if ((tech.damage || 0) >= 35) score += 35;\n if ((tech.damage || 0) >= 30) score += 20;\n if (damageType === DamageType.SLASHING) score += 20;\n if (damageType === DamageType.PIERCING) score += 15;\n if (damageType === DamageType.CRUSHING) score += 15;\n if (attackType === CombatAttackType.KICK) score += 10;\n break;\n }\n\n return { technique: tech, score };\n });\n\n // Sort by score descending, return highest scoring technique\n scoredTechniques.sort((a, b) => b.score - a.score);\n return scoredTechniques[0]?.technique;\n}\n\n/**\n * Calculate hit accuracy based on distance and effective reach.\n * Uses PhysicalReachCalculator for animation-aware reach calculation.\n *\n * Distance logic matches CombatSystem: if out of reach, guaranteed miss (accuracy 0).\n * Training scene coordinates are in meters, using METERS_TO_TRAINING_UNITS = 1.0.\n *\n * **IMPORTANT**: Distance calculation accounts for body radius.\n * Center-to-center distance is adjusted by subtracting target body radius\n * because attacks hit the body surface, not the center point.\n * The target body radius is calculated from physical attributes for archetypes,\n * or uses DEFAULT_BODY_RADIUS_METERS for training dummies.\n *\n * Example: If player center is 1.5m from dummy center, but dummy body\n * extends 0.23m from center, the effective distance to hit is 1.27m.\n *\n * @korean 거리 기반 명중률 계산 - 사정거리 밖이면 빗나감\n */\nfunction calculateHitAccuracy(\n playerPos: [number, number, number],\n dummyPos: [number, number, number],\n archetype: import(\"../../../../types/common\").PlayerArchetype,\n stance: TrigramStance,\n animationType?: AnimationType,\n reachConfig?: import(\"../../../../types/physics\").PhysicalReachConfig,\n): number {\n // Calculate 3D distance between player and dummy centers (in meters)\n const centerToCenterDistance = calculateDistance3D(playerPos, dummyPos);\n\n // Get player's physical attributes for reach calculation\n const playerPhysicalAttributes = getArchetypePhysicalAttributes(archetype);\n\n // Training dummy uses default body radius since it has no archetype\n // For combat between players, we would use calculateBodyRadius(targetPhysicalAttributes)\n // 훈련 더미는 원형이 없으므로 기본 몸체 반경 사용\n const targetBodyRadius = DEFAULT_BODY_RADIUS_METERS;\n\n // Effective distance = center-to-center minus target body radius only\n // Note: PhysicalReachCalculator already includes player body pivot/offset in reach calculation,\n // so we only subtract the target radius to avoid double-counting.\n // 유효 거리 = 중심간 거리 - 더미 몸체 반경 (플레이어 몸체 오프셋은 도달 거리에 포함됨)\n const effectiveDistance = Math.max(\n 0,\n centerToCenterDistance - targetBodyRadius,\n );\n\n // If animation type is available, use physics-based reach calculation\n // We use calculateMaxReach (peak time reach) because:\n // 1. Training hit detection happens at animation frame 6 (~100ms)\n // 2. But technique hit timings expect longer animations (e.g., roundhouse 200-480ms)\n // 3. Using max reach ensures the technique can hit if within peak reach range\n // 4. This matches intuitive behavior - if you're close enough to be hit by the kick, it hits\n // 훈련 타격 감지는 최대 도달 거리 사용 (애니메이션 타이밍과 기술 타이밍 불일치 보정)\n if (animationType !== undefined) {\n const maxReachMeters = physicalReachCalculator.calculateMaxReach(\n playerPhysicalAttributes,\n animationType,\n stance,\n reachConfig, // Use technique's designed reach if provided\n );\n\n // Convert reach from meters to training scene units.\n // Training scenes are authored in real-world meters, so we intentionally\n // use a 1:1 conversion here (METERS_TO_TRAINING_UNITS = 1.0).\n // Combat AI uses METERS_TO_PIXELS_SCALE (100) for pixel coordinates.\n const reachInUnits = maxReachMeters * METERS_TO_TRAINING_UNITS;\n\n // STRICT DISTANCE CHECK (matches CombatSystem behavior):\n // Out of reach = guaranteed miss with accuracy 0\n if (effectiveDistance > reachInUnits) {\n return 0;\n }\n\n // Within reach: accuracy based on how centered the hit is\n // Closer = higher accuracy (0.7 to 1.0 range)\n return Math.max(0.7, 1.0 - (effectiveDistance / reachInUnits) * 0.3);\n }\n\n // Fallback: use default punch reach (0.7 meters) for legacy behavior\n const defaultReach = 0.7 * METERS_TO_TRAINING_UNITS;\n if (effectiveDistance > defaultReach) {\n return 0; // Out of reach = miss\n }\n // Within default reach: linear accuracy based on distance\n return Math.max(0.5, 1.0 - (effectiveDistance / defaultReach) * 0.5);\n}\n\n/**\n * useTrainingActions hook\n * Provides training action handlers with proper memoization\n */\nexport function useTrainingActions(\n config: UseTrainingActionsConfig,\n): UseTrainingActionsReturn {\n const {\n state,\n actions,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n currentTechniqueAnimationTypeRef,\n audio,\n playBoneImpactSound,\n playAttackSound,\n onPlayerUpdate,\n playerAnimation,\n pendingAttackRef,\n selectedTechniqueId,\n setAttackAnimation,\n } = config;\n\n // Ref to store timeout for dummy reset\n const dummyResetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const handleStartTraining = useCallback(() => {\n actions.startTraining();\n audio.playSFX(\"menu_select\");\n }, [actions, audio]);\n\n const handleStopTraining = useCallback(() => {\n // Clear any pending dummy reset timeout\n if (dummyResetTimeoutRef.current) {\n clearTimeout(dummyResetTimeoutRef.current);\n dummyResetTimeoutRef.current = null;\n }\n actions.stopTraining();\n audio.playSFX(\"menu_back\");\n }, [actions, audio]);\n\n const handleDummyDefeated = useCallback(() => {\n actions.setFeedback(\"훈련 더미 무력화! | Dummy Defeated!\");\n audio.playSFX(\"ki_release\");\n\n // Clear any existing timeout\n if (dummyResetTimeoutRef.current) {\n clearTimeout(dummyResetTimeoutRef.current);\n }\n\n // Reset dummy health after delay\n dummyResetTimeoutRef.current = setTimeout(() => {\n actions.resetDummy();\n }, 2000);\n }, [actions, audio]);\n\n const handleDummyHit = useCallback(\n (\n _vitalPointId: string,\n attackContext?: {\n animationType?: AnimationType;\n techniqueId?: string;\n },\n ): boolean => {\n // Get animation context from the passed attackContext parameter\n // (TrainingScreen3D.tsx should pass the attackData before clearing the ref)\n const animationType = attackContext?.animationType;\n\n // Get technique's reachConfig for accurate reach calculation\n // Priority: attackContext.techniqueId (resolved in handleAttack) > selectedTechniqueId\n // This ensures default techniques (chosen when no explicit selection) also get their reachConfig\n let reachConfig: import(\"../../../../types/physics\").PhysicalReachConfig | undefined;\n const resolvedTechniqueId =\n attackContext?.techniqueId ?? selectedTechniqueId;\n if (resolvedTechniqueId) {\n const technique = KoreanTechniquesSystem.getTechniqueById(resolvedTechniqueId);\n reachConfig = technique?.reachConfig;\n }\n\n const accuracy = calculateHitAccuracy(\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n animationType,\n reachConfig,\n );\n\n // Determine hit position (dummy center)\n const hitPosition: [number, number, number] = [\n dummyPosition[0],\n 1.5,\n dummyPosition[2],\n ];\n\n if (accuracy > 0.5) {\n const points = Math.round(accuracy * 100);\n const damage = Math.round(accuracy * 15); // 0-15 damage based on accuracy\n const isPerfect = accuracy > 0.9;\n\n // Register hit with state (only counts if training)\n if (state.isTraining) {\n actions.registerHit(points, damage, isPerfect);\n }\n\n // Play bone impact audio with anatomical feedback using proper audio system\n if (playBoneImpactSound) {\n void playBoneImpactSound({\n damage,\n remainingHealth: 100, // Dummy has 100 health\n vitalPoint: false,\n hitPosition: { x: hitPosition[0], y: hitPosition[1], z: hitPosition[2] },\n });\n }\n\n // Determine feedback and sound\n let effectType: \"success\" | \"perfect\";\n if (isPerfect) {\n actions.setFeedback(\"완벽한 타격! | Perfect Strike!\");\n audio.playSFX(\"ki_release\");\n effectType = \"perfect\";\n } else if (accuracy > 0.7) {\n actions.setFeedback(\"좋은 타격! | Good Strike!\");\n audio.playSFX(\"ki_charge\");\n effectType = \"success\";\n } else {\n actions.setFeedback(\"타격 성공 | Strike Success\");\n audio.playSFX(\"menu_click\");\n effectType = \"success\";\n }\n\n // Add hit effect\n actions.addHitEffect({\n position: hitPosition,\n type: effectType,\n visible: true,\n damage,\n });\n\n return true;\n } else {\n // Register miss (only counts if training)\n if (state.isTraining) {\n actions.registerMiss();\n }\n actions.setFeedback(\"빗나감 | Miss - Out of reach!\");\n audio.playSFX(\"menu_navigate\");\n\n // Add miss effect\n actions.addHitEffect({\n position: hitPosition,\n type: \"miss\",\n visible: true,\n });\n\n return false;\n }\n },\n [\n state.isTraining,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n actions,\n audio,\n playBoneImpactSound,\n selectedTechniqueId,\n ],\n );\n\n const handleStanceChange = useCallback(\n (stanceIndex: number) => {\n actions.setStanceIndex(stanceIndex);\n const stance = TRIGRAM_STANCES_ORDER[stanceIndex];\n if (stance) {\n // Directly transition to stance guard animation (skips transitional animation)\n // 자세 가드 애니메이션으로 직접 전환 (전환 애니메이션 생략)\n playerAnimation.transitionToStanceGuard(stance);\n onPlayerUpdate({ currentStance: stance });\n audio.playSFX(\"stance_change\");\n }\n },\n [actions, onPlayerUpdate, audio, playerAnimation],\n );\n\n const handleAttack = useCallback(() => {\n // Determine which technique to use:\n // 1. If a technique is explicitly selected from the TechniqueBar, use that\n // 2. Otherwise, use the best default technique for the archetype + stance\n // 사용할 기술 결정: 명시적 선택 또는 원형+자세 기반 기본 기술\n let techniqueToUse: KoreanTechnique | undefined;\n let techniqueId = selectedTechniqueId;\n\n if (!techniqueId) {\n // No technique explicitly selected - get default for archetype + stance\n // 명시적 선택 없음 - 원형과 자세에 맞는 기본 기술 사용\n const defaultTechnique = getDefaultTechniqueForArchetype(\n playerArchetype,\n playerStance,\n );\n if (defaultTechnique) {\n techniqueToUse = defaultTechnique;\n techniqueId = defaultTechnique.id;\n }\n }\n\n // Get the animation type from the technique\n // 기술에서 애니메이션 타입 가져오기\n let animationType = currentTechniqueAnimationTypeRef.current;\n if (techniqueToUse?.animationType) {\n animationType = techniqueToUse.animationType;\n currentTechniqueAnimationTypeRef.current = animationType;\n }\n\n const startTime = performance.now() / 1000; // Current time in seconds\n\n const accuracy = calculateHitAccuracy(\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n animationType,\n techniqueToUse?.reachConfig, // Pass technique's reachConfig for accurate reach\n );\n\n pendingAttackRef.current = {\n accuracy,\n vitalPoint: state.selectedVitalPoint ?? \"generic\",\n animationType,\n startTime,\n techniqueId, // Store resolved technique ID for handleDummyHit\n };\n\n // Set visual attack animation based on technique (AnimationRegistry lookup)\n // This ensures the 3D model plays the correct technique animation (kick vs punch vs elbow)\n // 기술에 따른 시각적 공격 애니메이션 설정 (발차기 vs 주먹 vs 팔꿈치)\n if (setAttackAnimation && techniqueId) {\n const animationName = getAnimationForTechnique(techniqueId);\n setAttackAnimation(animationName);\n }\n\n // Trigger attack animation - this will fire onFrame event at frame 6\n playerAnimation.transitionTo(AnimationState.ATTACK);\n\n // Play attack sound based on technique damage/intensity\n // Resolve technique data if we only have an ID (from TechniqueBar selection)\n if (!techniqueToUse && selectedTechniqueId) {\n // Technique selected from TechniqueBar but not yet resolved\n techniqueToUse = KoreanTechniquesSystem.getTechniqueById(selectedTechniqueId);\n }\n\n if (playAttackSound) {\n // Prefer explicit technique damage when available\n const damage = techniqueToUse?.damage ?? 10;\n const intensity: AttackIntensity =\n damage >= 40\n ? \"critical\"\n : damage >= 25\n ? \"heavy\"\n : damage >= 10\n ? \"medium\"\n : \"light\";\n void playAttackSound(intensity);\n } else {\n // Fallback to generic whoosh if playAttackSound not available\n audio.playSFX(\"whoosh\");\n }\n }, [\n state.selectedVitalPoint,\n player3DPosition,\n dummyPosition,\n playerArchetype,\n playerStance,\n currentTechniqueAnimationTypeRef,\n playerAnimation,\n audio,\n playAttackSound,\n pendingAttackRef,\n selectedTechniqueId,\n setAttackAnimation,\n ]);\n\n return {\n handleStartTraining,\n handleStopTraining,\n handleDummyHit,\n handleDummyDefeated,\n handleStanceChange,\n handleAttack,\n };\n}\n\nexport default useTrainingActions;\n"],"mappings":"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"}
|
|
@@ -184,7 +184,7 @@ var SplashScreen = ({ onStart, width, height }) => {
|
|
|
184
184
|
}),
|
|
185
185
|
/* @__PURE__ */ jsxs("div", {
|
|
186
186
|
role: "contentinfo",
|
|
187
|
-
"aria-label": `Application version 0.7.
|
|
187
|
+
"aria-label": `Application version 0.7.8`,
|
|
188
188
|
style: {
|
|
189
189
|
position: "absolute",
|
|
190
190
|
bottom: "20px",
|
|
@@ -193,7 +193,7 @@ var SplashScreen = ({ onStart, width, height }) => {
|
|
|
193
193
|
fontSize: "10px",
|
|
194
194
|
zIndex: 1
|
|
195
195
|
},
|
|
196
|
-
children: ["v", "0.7.
|
|
196
|
+
children: ["v", "0.7.8"]
|
|
197
197
|
})
|
|
198
198
|
]
|
|
199
199
|
});
|
|
@@ -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"}
|
|
@@ -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":"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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<
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blacktrigram",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
4
4
|
"description": "Black Trigram (흑괘) - Korean Martial Arts Combat Simulator. Reusable game systems, combat mechanics, animation framework, and Korean martial arts data built with React, Three.js, and TypeScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
"three": "^0.183.0"
|
|
158
158
|
},
|
|
159
159
|
"devDependencies": {
|
|
160
|
-
"@aws-sdk/client-bedrock-runtime": "3.
|
|
160
|
+
"@aws-sdk/client-bedrock-runtime": "3.1020.0",
|
|
161
161
|
"@eslint/js": "10.0.1",
|
|
162
162
|
"@react-three/drei": "10.7.7",
|
|
163
163
|
"@react-three/fiber": "9.5.0",
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
"globals": "17.4.0",
|
|
186
186
|
"jest-axe": "10.0.0",
|
|
187
187
|
"jsdom": "29.0.1",
|
|
188
|
-
"knip": "6.
|
|
188
|
+
"knip": "6.1.1",
|
|
189
189
|
"license-compliance": "3.0.1",
|
|
190
190
|
"mocha-junit-reporter": "2.2.1",
|
|
191
191
|
"mochawesome": "7.1.4",
|
|
@@ -206,8 +206,8 @@
|
|
|
206
206
|
"typedoc-plugin-markdown": "4.11.0",
|
|
207
207
|
"typedoc-plugin-mermaid": "1.12.0",
|
|
208
208
|
"typedoc-plugin-missing-exports": "4.1.2",
|
|
209
|
-
"typescript": "
|
|
210
|
-
"typescript-eslint": "8.
|
|
209
|
+
"typescript": "6.0.2",
|
|
210
|
+
"typescript-eslint": "8.58.0",
|
|
211
211
|
"vite": "8.0.3",
|
|
212
212
|
"vite-bundle-analyzer": "1.3.6",
|
|
213
213
|
"vite-tsconfig-paths": "6.1.1",
|