blacktrigram 0.7.19 → 0.7.20

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.
@@ -1 +1 @@
1
- {"version":3,"file":"usePlayerAnimation.js","names":[],"sources":["../../src/hooks/usePlayerAnimation.ts"],"sourcesContent":["/**\n * usePlayerAnimation - React hook for player animation state\n *\n * Provides a React interface to the PlayerAnimationStateMachine\n * with automatic cleanup and event handling.\n *\n * @module hooks/usePlayerAnimation\n * @category Hooks\n * @korean 플레이어애니메이션훅\n */\n\nimport { useCallback, useMemo, useRef, useState } from \"react\";\nimport {\n AnimationEvents,\n AnimationState,\n DEFAULT_ANIMATION_CONFIGS,\n PlayerAnimationStateMachine,\n} from \"../systems/animation\";\nimport type {\n AnimationConfig,\n AnimationUpdateResult,\n} from \"../systems/animation/core/types\";\nimport type { TrigramStance } from \"../types/common\";\n\n/**\n * Options for usePlayerAnimation hook\n *\n * @korean 플레이어애니메이션훅옵션\n */\nexport interface UsePlayerAnimationOptions {\n /**\n * Custom animation configurations\n * If not provided, uses DEFAULT_ANIMATION_CONFIGS\n *\n * @korean 커스텀애니메이션설정\n */\n readonly customConfigs?: Map<AnimationState, AnimationConfig>;\n\n /**\n * Animation event callbacks\n *\n * **IMPORTANT**: The events object should be stable (memoized) to prevent\n * unnecessary re-initialization of the animation system. Changes to event\n * callbacks after the hook is initialized will NOT be reflected in the\n * animation system. Use `useMemo` or define events outside the component\n * to ensure stability.\n *\n * @korean 이벤트콜백\n */\n readonly events?: AnimationEvents;\n\n /**\n * Initial animation state (defaults to \"idle\")\n *\n * @korean 초기상태\n */\n readonly initialState?: AnimationState;\n}\n\n/**\n * Return type for usePlayerAnimation hook\n *\n * @korean 플레이어애니메이션훅반환타입\n */\nexport interface UsePlayerAnimationReturn {\n /**\n * Current animation state\n *\n * @korean 현재상태\n */\n readonly currentState: AnimationState;\n\n /**\n * Current frame index\n *\n * @korean 현재프레임\n */\n readonly currentFrame: number;\n\n /**\n * Update animation state (call in useFrame)\n *\n * @param deltaTime - Time elapsed since last update in seconds\n * @returns Animation update result\n *\n * @korean 업데이트\n */\n readonly update: (deltaTime: number) => AnimationUpdateResult;\n\n /**\n * Transition to a new animation state\n *\n * @param newState - Target animation state\n * @returns Whether transition was successful\n *\n * @korean 상태전환\n */\n readonly transitionTo: (newState: AnimationState) => boolean;\n\n /**\n * Transition to ATTACK state with technique-specific duration.\n *\n * The default ATTACK config is 200ms (12 frames), but real techniques\n * range from 350ms to 1200ms. This method overrides the duration so\n * the state machine stays in ATTACK for the full technique animation.\n *\n * @param durationSeconds - The skeletal animation duration in seconds\n * @returns Whether transition was successful\n *\n * @korean 공격전환 (기술별 지속시간)\n */\n readonly transitionToAttack: (durationSeconds: number) => boolean;\n\n /**\n * Transition to stance-specific guard animation\n *\n * @param stance - Trigram stance\n * @returns Whether transition was successful\n *\n * @korean 자세가드전환\n */\n readonly transitionToStanceGuard: (stance: TrigramStance) => boolean;\n\n /**\n * Transition to stance_change animation with specific transition data\n *\n * **Korean**: 자세 전환 애니메이션 시작\n *\n * Initiates a stance change animation with the specific transition data\n * from the 64-transition matrix. This provides stance-specific keyframes\n * and blend weights for smooth interpolation.\n *\n * @param fromStance - Source trigram stance\n * @param toStance - Target trigram stance\n * @returns Whether transition was successful\n *\n * @korean 자세전환애니메이션시작\n */\n readonly transitionToStanceChange: (\n fromStance: TrigramStance,\n toStance: TrigramStance,\n ) => boolean;\n\n /**\n * Check if currently in a stance guard animation\n *\n * @returns True if in any stance guard state\n *\n * @korean 자세가드확인\n */\n readonly isInStanceGuard: () => boolean;\n\n /**\n * Get current guard stance (if in guard animation)\n *\n * @returns Trigram stance or null\n *\n * @korean 현재가드자세\n */\n readonly getCurrentGuardStance: () => TrigramStance | null;\n\n /**\n * Reset animation to idle state\n *\n * @korean 초기화\n */\n readonly reset: () => void;\n}\n\n/**\n * React hook for player animation state management\n *\n * Provides frame-accurate animation control with priority system\n * and event callbacks. Integrates seamlessly with useFrame for\n * 60fps updates.\n *\n * @param options - Animation options\n * @returns Animation control interface\n *\n * @example\n * ```typescript\n * // Basic usage\n * const { currentState, currentFrame, update, transitionTo } = usePlayerAnimation({\n * events: {\n * onAnimationStart: (state) => console.log(`Started ${state}`),\n * onAnimationComplete: (state) => console.log(`Completed ${state}`),\n * onFrame: (frame, state) => {\n * if (state === \"attack\" && frame === 6) {\n * // Execute attack at midpoint\n * executeAttack();\n * }\n * }\n * }\n * });\n *\n * // In useFrame callback\n * useFrame((state, delta) => {\n * const result = update(delta);\n * // Update visuals based on result.state and result.frame\n * });\n *\n * // Trigger animations\n * const handleAttackInput = () => {\n * transitionTo(\"attack\");\n * };\n *\n * const handleMovement = (isMoving: boolean) => {\n * transitionTo(isMoving ? \"walk\" : \"idle\");\n * };\n * ```\n *\n * @korean 플레이어애니메이션훅\n */\nexport function usePlayerAnimation(\n options: UsePlayerAnimationOptions = {},\n): UsePlayerAnimationReturn {\n const { customConfigs, events, initialState = \"idle\" } = options;\n\n // Force re-renders when state changes\n const [, forceUpdate] = useState(0);\n\n // Create animation configs (memoized)\n const configs = useMemo(\n () => customConfigs ?? DEFAULT_ANIMATION_CONFIGS,\n [customConfigs],\n );\n\n // Create animation state machine (persistent across renders via useMemo)\n const stateMachine = useMemo(() => {\n const machine = new PlayerAnimationStateMachine(configs, events);\n // Set initial state if not \"idle\"\n if (initialState !== \"idle\") {\n machine.transitionTo(initialState);\n }\n return machine;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []); // Empty deps - only create once\n\n // Track previous state to only update on actual changes\n const prevStateRef = useRef<AnimationState>(stateMachine.getCurrentState());\n const prevFrameRef = useRef<number>(stateMachine.getCurrentFrame());\n\n // Memoized callbacks with selective state updates\n const update = useCallback(\n (deltaTime: number) => {\n const result = stateMachine.update(deltaTime);\n const currentState = stateMachine.getCurrentState();\n const currentFrame = stateMachine.getCurrentFrame();\n\n // Only trigger re-render if state or frame changed\n if (\n currentState !== prevStateRef.current ||\n currentFrame !== prevFrameRef.current\n ) {\n prevStateRef.current = currentState;\n prevFrameRef.current = currentFrame;\n forceUpdate((n) => n + 1);\n }\n\n return result;\n },\n [stateMachine],\n );\n\n const transitionTo = useCallback(\n (newState: AnimationState) => {\n const success = stateMachine.transitionTo(newState);\n if (success) {\n prevStateRef.current = stateMachine.getCurrentState();\n prevFrameRef.current = stateMachine.getCurrentFrame();\n forceUpdate((n) => n + 1);\n }\n return success;\n },\n [stateMachine],\n );\n\n const reset = useCallback(() => {\n stateMachine.reset();\n prevStateRef.current = stateMachine.getCurrentState();\n prevFrameRef.current = stateMachine.getCurrentFrame();\n forceUpdate((n) => n + 1);\n }, [stateMachine]);\n\n const transitionToStanceGuard = useCallback(\n (stance: TrigramStance) => {\n const success = stateMachine.transitionToStanceGuard(stance);\n if (success) {\n prevStateRef.current = stateMachine.getCurrentState();\n prevFrameRef.current = stateMachine.getCurrentFrame();\n forceUpdate((n) => n + 1);\n }\n return success;\n },\n [stateMachine],\n );\n\n const transitionToAttack = useCallback(\n (durationSeconds: number) => {\n const success = stateMachine.transitionToAttack(durationSeconds);\n if (success) {\n prevStateRef.current = stateMachine.getCurrentState();\n prevFrameRef.current = stateMachine.getCurrentFrame();\n forceUpdate((n) => n + 1);\n }\n return success;\n },\n [stateMachine],\n );\n\n const transitionToStanceChange = useCallback(\n (fromStance: TrigramStance, toStance: TrigramStance) => {\n const success = stateMachine.transitionToStanceChange(\n fromStance,\n toStance,\n );\n if (success) {\n prevStateRef.current = stateMachine.getCurrentState();\n prevFrameRef.current = stateMachine.getCurrentFrame();\n forceUpdate((n) => n + 1);\n }\n return success;\n },\n [stateMachine],\n );\n\n const isInStanceGuard = useCallback(() => {\n return stateMachine.isInStanceGuard();\n }, [stateMachine]);\n\n const getCurrentGuardStance = useCallback(() => {\n return stateMachine.getCurrentGuardStance();\n }, [stateMachine]);\n\n // Memoize the return value to ensure stable reference unless state/frame changes\n // Note: Using ref.current as dependencies is intentional here - when state changes,\n // forceUpdate triggers a re-render, and the refs have new values which cause\n // the useMemo to recalculate\n // Return a fresh object each render to ensure currentState/currentFrame are always up-to-date\n // Mutable ref values don't trigger re-renders, so we can't rely on memoization here\n return {\n currentState: prevStateRef.current,\n currentFrame: prevFrameRef.current,\n update,\n transitionTo,\n transitionToAttack,\n transitionToStanceGuard,\n transitionToStanceChange,\n isInStanceGuard,\n getCurrentGuardStance,\n reset,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqNA,SAAgB,mBACd,UAAqC,EAAE,EACb;CAC1B,MAAM,EAAE,eAAe,QAAQ,eAAe,WAAW;CAGzD,MAAM,GAAG,eAAe,SAAS,EAAE;CAGnC,MAAM,UAAU,cACR,iBAAiB,2BACvB,CAAC,cAAc,CAChB;CAGD,MAAM,eAAe,cAAc;EACjC,MAAM,UAAU,IAAI,4BAA4B,SAAS,OAAO;AAEhE,MAAI,iBAAiB,OACnB,SAAQ,aAAa,aAAa;AAEpC,SAAO;IAEN,EAAE,CAAC;CAGN,MAAM,eAAe,OAAuB,aAAa,iBAAiB,CAAC;CAC3E,MAAM,eAAe,OAAe,aAAa,iBAAiB,CAAC;CAGnE,MAAM,SAAS,aACZ,cAAsB;EACrB,MAAM,SAAS,aAAa,OAAO,UAAU;EAC7C,MAAM,eAAe,aAAa,iBAAiB;EACnD,MAAM,eAAe,aAAa,iBAAiB;AAGnD,MACE,iBAAiB,aAAa,WAC9B,iBAAiB,aAAa,SAC9B;AACA,gBAAa,UAAU;AACvB,gBAAa,UAAU;AACvB,gBAAa,MAAM,IAAI,EAAE;;AAG3B,SAAO;IAET,CAAC,aAAa,CACf;CAED,MAAM,eAAe,aAClB,aAA6B;EAC5B,MAAM,UAAU,aAAa,aAAa,SAAS;AACnD,MAAI,SAAS;AACX,gBAAa,UAAU,aAAa,iBAAiB;AACrD,gBAAa,UAAU,aAAa,iBAAiB;AACrD,gBAAa,MAAM,IAAI,EAAE;;AAE3B,SAAO;IAET,CAAC,aAAa,CACf;CAED,MAAM,QAAQ,kBAAkB;AAC9B,eAAa,OAAO;AACpB,eAAa,UAAU,aAAa,iBAAiB;AACrD,eAAa,UAAU,aAAa,iBAAiB;AACrD,eAAa,MAAM,IAAI,EAAE;IACxB,CAAC,aAAa,CAAC;CAElB,MAAM,0BAA0B,aAC7B,WAA0B;EACzB,MAAM,UAAU,aAAa,wBAAwB,OAAO;AAC5D,MAAI,SAAS;AACX,gBAAa,UAAU,aAAa,iBAAiB;AACrD,gBAAa,UAAU,aAAa,iBAAiB;AACrD,gBAAa,MAAM,IAAI,EAAE;;AAE3B,SAAO;IAET,CAAC,aAAa,CACf;CAED,MAAM,qBAAqB,aACxB,oBAA4B;EAC3B,MAAM,UAAU,aAAa,mBAAmB,gBAAgB;AAChE,MAAI,SAAS;AACX,gBAAa,UAAU,aAAa,iBAAiB;AACrD,gBAAa,UAAU,aAAa,iBAAiB;AACrD,gBAAa,MAAM,IAAI,EAAE;;AAE3B,SAAO;IAET,CAAC,aAAa,CACf;CAED,MAAM,2BAA2B,aAC9B,YAA2B,aAA4B;EACtD,MAAM,UAAU,aAAa,yBAC3B,YACA,SACD;AACD,MAAI,SAAS;AACX,gBAAa,UAAU,aAAa,iBAAiB;AACrD,gBAAa,UAAU,aAAa,iBAAiB;AACrD,gBAAa,MAAM,IAAI,EAAE;;AAE3B,SAAO;IAET,CAAC,aAAa,CACf;CAED,MAAM,kBAAkB,kBAAkB;AACxC,SAAO,aAAa,iBAAiB;IACpC,CAAC,aAAa,CAAC;CAElB,MAAM,wBAAwB,kBAAkB;AAC9C,SAAO,aAAa,uBAAuB;IAC1C,CAAC,aAAa,CAAC;AAQlB,QAAO;EACL,cAAc,aAAa;EAC3B,cAAc,aAAa;EAC3B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD"}
1
+ {"version":3,"file":"usePlayerAnimation.js","names":[],"sources":["../../src/hooks/usePlayerAnimation.ts"],"sourcesContent":["/**\n * usePlayerAnimation - React hook for player animation state\n *\n * Provides a React interface to the PlayerAnimationStateMachine\n * with automatic cleanup and event handling.\n *\n * @module hooks/usePlayerAnimation\n * @category Hooks\n * @korean 플레이어애니메이션훅\n */\n\nimport { useCallback, useMemo, useState } from \"react\";\nimport {\n AnimationEvents,\n AnimationState,\n DEFAULT_ANIMATION_CONFIGS,\n PlayerAnimationStateMachine,\n} from \"../systems/animation\";\nimport type {\n AnimationConfig,\n AnimationUpdateResult,\n} from \"../systems/animation/core/types\";\nimport type { TrigramStance } from \"../types/common\";\n\n/**\n * Options for usePlayerAnimation hook\n *\n * @korean 플레이어애니메이션훅옵션\n */\nexport interface UsePlayerAnimationOptions {\n /**\n * Custom animation configurations\n * If not provided, uses DEFAULT_ANIMATION_CONFIGS\n *\n * @korean 커스텀애니메이션설정\n */\n readonly customConfigs?: Map<AnimationState, AnimationConfig>;\n\n /**\n * Animation event callbacks\n *\n * **IMPORTANT**: The events object should be stable (memoized) to prevent\n * unnecessary re-initialization of the animation system. Changes to event\n * callbacks after the hook is initialized will NOT be reflected in the\n * animation system. Use `useMemo` or define events outside the component\n * to ensure stability.\n *\n * @korean 이벤트콜백\n */\n readonly events?: AnimationEvents;\n\n /**\n * Initial animation state (defaults to \"idle\")\n *\n * @korean 초기상태\n */\n readonly initialState?: AnimationState;\n}\n\n/**\n * Return type for usePlayerAnimation hook\n *\n * @korean 플레이어애니메이션훅반환타입\n */\nexport interface UsePlayerAnimationReturn {\n /**\n * Current animation state\n *\n * @korean 현재상태\n */\n readonly currentState: AnimationState;\n\n /**\n * Current frame index\n *\n * @korean 현재프레임\n */\n readonly currentFrame: number;\n\n /**\n * Update animation state (call in useFrame)\n *\n * @param deltaTime - Time elapsed since last update in seconds\n * @returns Animation update result\n *\n * @korean 업데이트\n */\n readonly update: (deltaTime: number) => AnimationUpdateResult;\n\n /**\n * Transition to a new animation state\n *\n * @param newState - Target animation state\n * @returns Whether transition was successful\n *\n * @korean 상태전환\n */\n readonly transitionTo: (newState: AnimationState) => boolean;\n\n /**\n * Transition to ATTACK state with technique-specific duration.\n *\n * The default ATTACK config is 200ms (12 frames), but real techniques\n * range from 350ms to 1200ms. This method overrides the duration so\n * the state machine stays in ATTACK for the full technique animation.\n *\n * @param durationSeconds - The skeletal animation duration in seconds\n * @returns Whether transition was successful\n *\n * @korean 공격전환 (기술별 지속시간)\n */\n readonly transitionToAttack: (durationSeconds: number) => boolean;\n\n /**\n * Transition to stance-specific guard animation\n *\n * @param stance - Trigram stance\n * @returns Whether transition was successful\n *\n * @korean 자세가드전환\n */\n readonly transitionToStanceGuard: (stance: TrigramStance) => boolean;\n\n /**\n * Transition to stance_change animation with specific transition data\n *\n * **Korean**: 자세 전환 애니메이션 시작\n *\n * Initiates a stance change animation with the specific transition data\n * from the 64-transition matrix. This provides stance-specific keyframes\n * and blend weights for smooth interpolation.\n *\n * @param fromStance - Source trigram stance\n * @param toStance - Target trigram stance\n * @returns Whether transition was successful\n *\n * @korean 자세전환애니메이션시작\n */\n readonly transitionToStanceChange: (\n fromStance: TrigramStance,\n toStance: TrigramStance,\n ) => boolean;\n\n /**\n * Check if currently in a stance guard animation\n *\n * @returns True if in any stance guard state\n *\n * @korean 자세가드확인\n */\n readonly isInStanceGuard: () => boolean;\n\n /**\n * Get current guard stance (if in guard animation)\n *\n * @returns Trigram stance or null\n *\n * @korean 현재가드자세\n */\n readonly getCurrentGuardStance: () => TrigramStance | null;\n\n /**\n * Reset animation to idle state\n *\n * @korean 초기화\n */\n readonly reset: () => void;\n}\n\n/**\n * React hook for player animation state management\n *\n * Provides frame-accurate animation control with priority system\n * and event callbacks. Integrates seamlessly with useFrame for\n * 60fps updates.\n *\n * @param options - Animation options\n * @returns Animation control interface\n *\n * @example\n * ```typescript\n * // Basic usage\n * const { currentState, currentFrame, update, transitionTo } = usePlayerAnimation({\n * events: {\n * onAnimationStart: (state) => console.log(`Started ${state}`),\n * onAnimationComplete: (state) => console.log(`Completed ${state}`),\n * onFrame: (frame, state) => {\n * if (state === \"attack\" && frame === 6) {\n * // Execute attack at midpoint\n * executeAttack();\n * }\n * }\n * }\n * });\n *\n * // In useFrame callback\n * useFrame((state, delta) => {\n * const result = update(delta);\n * // Update visuals based on result.state and result.frame\n * });\n *\n * // Trigger animations\n * const handleAttackInput = () => {\n * transitionTo(\"attack\");\n * };\n *\n * const handleMovement = (isMoving: boolean) => {\n * transitionTo(isMoving ? \"walk\" : \"idle\");\n * };\n * ```\n *\n * @korean 플레이어애니메이션훅\n */\nexport function usePlayerAnimation(\n options: UsePlayerAnimationOptions = {},\n): UsePlayerAnimationReturn {\n const { customConfigs, events, initialState = \"idle\" } = options;\n\n // Create animation configs (memoized)\n const configs = useMemo(\n () => customConfigs ?? DEFAULT_ANIMATION_CONFIGS,\n [customConfigs],\n );\n\n // Create animation state machine (persistent across renders via useMemo)\n const stateMachine = useMemo(() => {\n const machine = new PlayerAnimationStateMachine(configs, events);\n // Set initial state if not \"idle\"\n if (initialState !== \"idle\") {\n machine.transitionTo(initialState);\n }\n return machine;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []); // Empty deps - only create once\n\n // Track current state/frame as React state so they can be safely returned\n // during render. Previously these were refs paired with a forceUpdate trick,\n // but react-hooks/refs forbids reading .current during render.\n const [currentState, setCurrentState] = useState<AnimationState>(() =>\n stateMachine.getCurrentState(),\n );\n const [currentFrame, setCurrentFrame] = useState<number>(() =>\n stateMachine.getCurrentFrame(),\n );\n\n // Memoized callbacks with selective state updates\n const update = useCallback(\n (deltaTime: number) => {\n const result = stateMachine.update(deltaTime);\n const nextState = stateMachine.getCurrentState();\n const nextFrame = stateMachine.getCurrentFrame();\n\n // Only trigger re-render if state or frame changed\n setCurrentState((prev) => (prev !== nextState ? nextState : prev));\n setCurrentFrame((prev) => (prev !== nextFrame ? nextFrame : prev));\n\n return result;\n },\n [stateMachine],\n );\n\n const transitionTo = useCallback(\n (newState: AnimationState) => {\n const success = stateMachine.transitionTo(newState);\n if (success) {\n setCurrentState(stateMachine.getCurrentState());\n setCurrentFrame(stateMachine.getCurrentFrame());\n }\n return success;\n },\n [stateMachine],\n );\n\n const reset = useCallback(() => {\n stateMachine.reset();\n setCurrentState(stateMachine.getCurrentState());\n setCurrentFrame(stateMachine.getCurrentFrame());\n }, [stateMachine]);\n\n const transitionToStanceGuard = useCallback(\n (stance: TrigramStance) => {\n const success = stateMachine.transitionToStanceGuard(stance);\n if (success) {\n setCurrentState(stateMachine.getCurrentState());\n setCurrentFrame(stateMachine.getCurrentFrame());\n }\n return success;\n },\n [stateMachine],\n );\n\n const transitionToAttack = useCallback(\n (durationSeconds: number) => {\n const success = stateMachine.transitionToAttack(durationSeconds);\n if (success) {\n setCurrentState(stateMachine.getCurrentState());\n setCurrentFrame(stateMachine.getCurrentFrame());\n }\n return success;\n },\n [stateMachine],\n );\n\n const transitionToStanceChange = useCallback(\n (fromStance: TrigramStance, toStance: TrigramStance) => {\n const success = stateMachine.transitionToStanceChange(\n fromStance,\n toStance,\n );\n if (success) {\n setCurrentState(stateMachine.getCurrentState());\n setCurrentFrame(stateMachine.getCurrentFrame());\n }\n return success;\n },\n [stateMachine],\n );\n\n const isInStanceGuard = useCallback(() => {\n return stateMachine.isInStanceGuard();\n }, [stateMachine]);\n\n const getCurrentGuardStance = useCallback(() => {\n return stateMachine.getCurrentGuardStance();\n }, [stateMachine]);\n\n // Return a fresh object each render so consumers see the latest state/frame\n return {\n currentState,\n currentFrame,\n update,\n transitionTo,\n transitionToAttack,\n transitionToStanceGuard,\n transitionToStanceChange,\n isInStanceGuard,\n getCurrentGuardStance,\n reset,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqNA,SAAgB,mBACd,UAAqC,EAAE,EACb;CAC1B,MAAM,EAAE,eAAe,QAAQ,eAAe,WAAW;CAGzD,MAAM,UAAU,cACR,iBAAiB,2BACvB,CAAC,cAAc,CAChB;CAGD,MAAM,eAAe,cAAc;EACjC,MAAM,UAAU,IAAI,4BAA4B,SAAS,OAAO;AAEhE,MAAI,iBAAiB,OACnB,SAAQ,aAAa,aAAa;AAEpC,SAAO;IAEN,EAAE,CAAC;CAKN,MAAM,CAAC,cAAc,mBAAmB,eACtC,aAAa,iBAAiB,CAC/B;CACD,MAAM,CAAC,cAAc,mBAAmB,eACtC,aAAa,iBAAiB,CAC/B;CAGD,MAAM,SAAS,aACZ,cAAsB;EACrB,MAAM,SAAS,aAAa,OAAO,UAAU;EAC7C,MAAM,YAAY,aAAa,iBAAiB;EAChD,MAAM,YAAY,aAAa,iBAAiB;AAGhD,mBAAiB,SAAU,SAAS,YAAY,YAAY,KAAM;AAClE,mBAAiB,SAAU,SAAS,YAAY,YAAY,KAAM;AAElE,SAAO;IAET,CAAC,aAAa,CACf;CAED,MAAM,eAAe,aAClB,aAA6B;EAC5B,MAAM,UAAU,aAAa,aAAa,SAAS;AACnD,MAAI,SAAS;AACX,mBAAgB,aAAa,iBAAiB,CAAC;AAC/C,mBAAgB,aAAa,iBAAiB,CAAC;;AAEjD,SAAO;IAET,CAAC,aAAa,CACf;CAED,MAAM,QAAQ,kBAAkB;AAC9B,eAAa,OAAO;AACpB,kBAAgB,aAAa,iBAAiB,CAAC;AAC/C,kBAAgB,aAAa,iBAAiB,CAAC;IAC9C,CAAC,aAAa,CAAC;CAElB,MAAM,0BAA0B,aAC7B,WAA0B;EACzB,MAAM,UAAU,aAAa,wBAAwB,OAAO;AAC5D,MAAI,SAAS;AACX,mBAAgB,aAAa,iBAAiB,CAAC;AAC/C,mBAAgB,aAAa,iBAAiB,CAAC;;AAEjD,SAAO;IAET,CAAC,aAAa,CACf;AAsCD,QAAO;EACL;EACA;EACA;EACA;EACA,oBAzCyB,aACxB,oBAA4B;GAC3B,MAAM,UAAU,aAAa,mBAAmB,gBAAgB;AAChE,OAAI,SAAS;AACX,oBAAgB,aAAa,iBAAiB,CAAC;AAC/C,oBAAgB,aAAa,iBAAiB,CAAC;;AAEjD,UAAO;KAET,CAAC,aAAa,CACf;EAgCC;EACA,0BA/B+B,aAC9B,YAA2B,aAA4B;GACtD,MAAM,UAAU,aAAa,yBAC3B,YACA,SACD;AACD,OAAI,SAAS;AACX,oBAAgB,aAAa,iBAAiB,CAAC;AAC/C,oBAAgB,aAAa,iBAAiB,CAAC;;AAEjD,UAAO;KAET,CAAC,aAAa,CACf;EAmBC,iBAjBsB,kBAAkB;AACxC,UAAO,aAAa,iBAAiB;KACpC,CAAC,aAAa,CAAC;EAgBhB,uBAd4B,kBAAkB;AAC9C,UAAO,aAAa,uBAAuB;KAC1C,CAAC,aAAa,CAAC;EAahB;EACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"inputSystem.d.ts","sourceRoot":"","sources":["../../src/utils/inputSystem.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"inputSystem.d.ts","sourceRoot":"","sources":["../../src/utils/inputSystem.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAK/C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAIhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAE3B;;;;;;;;OAQG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE;QAChB,4EAA4E;QAC5E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;QAClC,4EAA4E;QAC5E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;KACnC,CAAC;IAEF,yEAAyE;IACzE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;IAEzD,4EAA4E;IAC5E,QAAQ,CAAC,qBAAqB,CAAC,EAAE,QAAQ,CAAC;IAG1C,sDAAsD;IACtD,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IAEvC,iFAAiF;IACjF,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAElC,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAE7B,iEAAiE;IACjE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAGpC,0DAA0D;IAC1D,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAEnC,iEAAiE;IACjE,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CACxC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,oBAAoB;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,cAAc,EAAE,QAAQ,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAChD,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,qCAAqC;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,iBAAiB,GACxB,oBAAoB,CA6etB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,YAAY,GAAG,UAAU,CAAC;IACzE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,YAAY,CAAC,EAAE,aAAa,CAAC;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,eAAe,CAAqC;IAC5D,OAAO,CAAC,SAAS,CAAQ;;IAMzB,OAAO,CAAC,mBAAmB;IAK3B,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,WAAW;IAQnB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI;IAUnD,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI;IAgBtD,YAAY;IAIZ,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIvC,MAAM;IAIN,OAAO;IAIP,OAAO,CAAC,aAAa;IAOrB,OAAO;CAKR;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAQlE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,GAAG,WAAW,GAAG,IAAI,CA4B3E;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI;;;EAyBzE"}
@@ -68,7 +68,7 @@ function usePlayerMovement(config) {
68
68
  error: error instanceof Error ? error : new Error(String(error))
69
69
  };
70
70
  }
71
- }, [bounds?.worldWidthMeters, bounds?.worldDepthMeters]);
71
+ }, [bounds]);
72
72
  const arenaBounds = arenaBoundsResult.bounds;
73
73
  useEffect(() => {
74
74
  if (arenaBoundsResult.error) if (bounds?.worldWidthMeters != null && bounds?.worldDepthMeters != null) console.warn("Failed to calculate arena bounds, using defaults:", arenaBoundsResult.error);
@@ -1 +1 @@
1
- {"version":3,"file":"inputSystem.js","names":[],"sources":["../../src/utils/inputSystem.ts"],"sourcesContent":["import { COMBAT_CONTROLS } from \"@/systems/types\";\nimport type { Position } from \"@/types/common\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport type { MovementInput } from \"../systems/physics/MovementPhysics\";\nimport { MovementPhysics } from \"../systems/physics/MovementPhysics\";\nimport { TrigramStance } from \"../types/common\";\nimport { calculateArenaBounds, DEFAULT_PHYSICS_ARENA_BOUNDS } from \"../types/PhysicsTypes\";\nimport type { MovementArenaBounds } from \"../types/PhysicsTypes\";\n\n/**\n * Configuration interface for the input system and player movement.\n * Uses physics-first approach: all positions and velocities are in meters.\n *\n * **Korean**: 입력 시스템 설정 (Input System Configuration)\n *\n * ## Physics-First Architecture\n *\n * This interface requires worldWidthMeters and worldDepthMeters to enable\n * the new physics-first coordinate system. Without these properties, the\n * movement system cannot properly convert between physics (meters) and\n * rendering (pixels).\n *\n * ### Migration Guide\n *\n * Existing code must be updated to pass world dimensions:\n *\n * ```typescript\n * // Before (incorrect):\n * const config = { bounds: { x: 0, y: 0, width: 960, height: 480 } };\n *\n * // After (correct):\n * const config = {\n * bounds: {\n * worldWidthMeters: 10, // From layout hook\n * worldDepthMeters: 10 // From layout hook\n * }\n * };\n * ```\n *\n * ### Fallback Behavior\n *\n * If worldWidthMeters/worldDepthMeters are not provided, the system falls back\n * to DEFAULT_PHYSICS_ARENA_BOUNDS (10m × 7.5m) to ensure movement stays bounded.\n * Callers SHOULD provide these values from their layout hooks (useCombatLayout, \n * useTrainingLayout) for proper arena sizing.\n */\nexport interface InputSystemConfig {\n /** Whether the input system is enabled and processing input */\n readonly enabled?: boolean;\n\n /**\n * Arena world dimensions in meters for physics calculations.\n *\n * **REQUIRED for physics-first coordinate system to work.**\n *\n * These values must come from layout hooks:\n * - CombatScreen3D: Use arenaBounds.worldWidthMeters/worldDepthMeters from useCombatLayout()\n * - TrainingScreen3D: Use trainingAreaBounds.worldWidthMeters/worldDepthMeters from useTrainingLayout()\n */\n readonly bounds?: {\n /** Physical arena width in meters (e.g., 6m mobile, 10m desktop, 14m 4K) */\n readonly worldWidthMeters: number;\n /** Physical arena depth in meters (e.g., 6m mobile, 10m desktop, 14m 4K) */\n readonly worldDepthMeters: number;\n };\n\n /** Callback invoked when player position changes (position in meters) */\n readonly onPositionChange?: (position: Position) => void;\n\n /** Initial player position in METERS (x = lateral, y = forward/backward) */\n readonly initialPositionMeters?: Position;\n\n // Physics-based movement parameters (always enabled)\n /** Current trigram stance affecting movement speed */\n readonly currentStance?: TrigramStance;\n\n /** Leg injury factor (0-1, where 1 is fully injured) affecting movement speed */\n readonly legInjuryFactor?: number;\n\n /** Whether player is running (sprint mode) */\n readonly isRunning?: boolean;\n\n /** Whether to use tactical step mode (30cm grid quantization) */\n readonly useTacticalSteps?: boolean;\n\n // Speed modifier overrides from SpeedModifierSystem\n /** Final calculated maximum speed in meters per second */\n readonly maxSpeedOverride?: number;\n\n /** Final calculated acceleration in meters per second squared */\n readonly accelerationOverride?: number;\n}\n\nexport interface MovementState {\n readonly up: boolean;\n readonly down: boolean;\n readonly left: boolean;\n readonly right: boolean;\n readonly position: Position;\n readonly isMoving: boolean; // Add isMoving to movement state\n}\n\nexport interface PlayerMovementResult {\n /** Player position in METERS (x = lateral, y = forward/backward in arena) */\n readonly playerPosition: Position;\n readonly movementState: MovementState;\n readonly isMoving: boolean;\n readonly isKeyPressed: (key: string) => boolean;\n /** Velocity in m/s (x = lateral, y = forward/backward) */\n readonly velocity?: { x: number; y: number };\n /** Current speed magnitude in m/s */\n readonly speed?: number;\n}\n\n/**\n * Hook for handling player movement with physics-first approach.\n * All positions and velocities are in METERS - no pixel conversions.\n *\n * **Korean**: 플레이어 이동 훅 (Player Movement Hook)\n *\n * @param config - Physics-first configuration with positions in meters\n * @returns Movement state and physics data (all in meters)\n */\nexport function usePlayerMovement(\n config: InputSystemConfig,\n): PlayerMovementResult {\n const {\n enabled = true,\n bounds,\n onPositionChange,\n initialPositionMeters = { x: 0, y: 0 },\n currentStance = TrigramStance.GEON,\n legInjuryFactor = 0,\n isRunning: isRunningProp = false,\n useTacticalSteps = false,\n maxSpeedOverride,\n accelerationOverride,\n } = config;\n\n // Position in METERS (x = lateral position, y = forward/backward position)\n const [playerPosition, setPlayerPosition] = useState<Position>(\n initialPositionMeters,\n );\n const [keyState, setKeyState] = useState({\n up: false,\n down: false,\n left: false,\n right: false,\n });\n // Physics state for render (velocity and speed in m/s)\n const [velocity, setVelocity] = useState<\n { x: number; y: number } | undefined\n >(undefined);\n const [speed, setSpeed] = useState<number | undefined>(undefined);\n\n // Auto-run detection: track how long movement keys have been held\n // After sustained movement, automatically transition from walking to running\n const movementStartTimeRef = useRef<number | null>(null);\n const AUTO_RUN_THRESHOLD_MS = 300; // Transition to run after 300ms of sustained movement\n\n // Physics-based movement state (always initialized for realistic combat)\n const physicsEngineRef = useRef<MovementPhysics | null>(null);\n const physicsStateRef = useRef<{\n position: THREE.Vector3;\n velocity: THREE.Vector3;\n acceleration: number;\n maxSpeed: number;\n currentStance: TrigramStance;\n legInjuryFactor: number;\n } | null>(null);\n\n // Initialize physics engine once on mount (always enabled)\n // All positions are in METERS - no pixel conversion needed\n useEffect(() => {\n if (!physicsEngineRef.current) {\n // Use arena width for physics-aware speed scaling\n // Validate and fall back to default if invalid\n const width = bounds?.worldWidthMeters;\n const arenaWidth =\n width != null && Number.isFinite(width) && width > 0\n ? width\n : DEFAULT_PHYSICS_ARENA_BOUNDS.worldWidthMeters;\n physicsEngineRef.current = new MovementPhysics(arenaWidth);\n // Initial position in meters (x = lateral, z = forward/backward)\n physicsStateRef.current = {\n position: new THREE.Vector3(\n initialPositionMeters.x,\n 0,\n initialPositionMeters.y,\n ),\n velocity: new THREE.Vector3(0, 0, 0),\n acceleration: 0,\n maxSpeed: 6.0, // Default to BASE_WALK_SPEED (6.0 m/s for responsive combat)\n currentStance,\n legInjuryFactor: legInjuryFactor ?? 0,\n };\n }\n }, []); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Compute arena bounds synchronously when bounds dimensions change\n // Uses useMemo to ensure bounds are available immediately (not after effect runs)\n // Falls back to default arena bounds if invalid or missing\n const arenaBoundsResult = useMemo<{\n bounds: MovementArenaBounds | undefined;\n error?: Error;\n }>(() => {\n if (bounds?.worldWidthMeters != null && bounds?.worldDepthMeters != null) {\n try {\n return {\n bounds: calculateArenaBounds(\n {\n worldWidthMeters: bounds.worldWidthMeters,\n worldDepthMeters: bounds.worldDepthMeters,\n },\n 0.3 // 0.3m character radius\n ),\n };\n } catch (error) {\n // If validation fails, fall back to default bounds\n // Error will be logged in useEffect to keep render pure\n return {\n bounds: undefined,\n error: error instanceof Error ? error : new Error(String(error)),\n };\n }\n }\n\n // Fallback: use default arena bounds to ensure movement stays bounded\n try {\n return {\n bounds: calculateArenaBounds(\n {\n worldWidthMeters: DEFAULT_PHYSICS_ARENA_BOUNDS.worldWidthMeters,\n worldDepthMeters: DEFAULT_PHYSICS_ARENA_BOUNDS.worldDepthMeters,\n },\n 0.3 // 0.3m character radius\n ),\n };\n } catch (error) {\n // Should never happen with default bounds, but handle gracefully\n // Error will be logged in useEffect to keep render pure\n return {\n bounds: undefined,\n error: error instanceof Error ? error : new Error(String(error)),\n };\n }\n }, [bounds?.worldWidthMeters, bounds?.worldDepthMeters]);\n\n const arenaBounds = arenaBoundsResult.bounds;\n\n // Log arena bounds calculation errors in an effect (not during render)\n useEffect(() => {\n if (arenaBoundsResult.error) {\n if (bounds?.worldWidthMeters != null && bounds?.worldDepthMeters != null) {\n // Custom bounds failed validation\n console.warn(\n \"Failed to calculate arena bounds, using defaults:\",\n arenaBoundsResult.error\n );\n } else {\n // Should never happen with default bounds\n console.error(\n \"Failed to calculate default arena bounds:\",\n arenaBoundsResult.error\n );\n }\n }\n }, [arenaBoundsResult.error, bounds?.worldWidthMeters, bounds?.worldDepthMeters]);\n\n // Update physics engine arena width when bounds change (legacy)\n useEffect(() => {\n if (!physicsEngineRef.current) {\n return;\n }\n\n const width = bounds?.worldWidthMeters;\n if (width == null) {\n return;\n }\n\n // Validate width before applying to physics engine to avoid runtime errors\n if (!Number.isFinite(width) || width <= 0) {\n console.warn(\n \"Ignoring invalid worldWidthMeters when updating arena width:\",\n width,\n );\n return;\n }\n\n try {\n physicsEngineRef.current.setArenaWidth(width);\n } catch (error) {\n console.warn(\"Failed to update physics arena width:\", error);\n }\n }, [bounds?.worldWidthMeters]);\n\n // Track pressed keys for combat system\n const pressedKeys = useRef<Set<string>>(new Set());\n // Use useState lazy initializer for performance.now() to avoid impure function during render\n const [initialTime] = useState(() => performance.now());\n const lastUpdateTime = useRef(initialTime);\n const animationFrameId = useRef<number | null>(null);\n\n // Refs to track last reported position/velocity to avoid useCallback dependency issues\n // This prevents the animation frame from being cancelled every frame due to callback recreation\n const lastReportedPositionRef = useRef<Position>(initialPositionMeters);\n const lastReportedVelocityRef = useRef<{ x: number; y: number } | undefined>(\n undefined,\n );\n const lastReportedSpeedRef = useRef<number | undefined>(undefined);\n\n // Ref to track keyState for physics loop - avoids recreating callback on key changes\n const keyStateRef = useRef({\n up: false,\n down: false,\n left: false,\n right: false,\n });\n\n // Calculate if currently moving\n const isMoving =\n keyState.up || keyState.down || keyState.left || keyState.right;\n\n // Create complete movement state\n const movementState: MovementState = {\n ...keyState,\n position: playerPosition,\n isMoving,\n };\n\n // Key press checker for combat system\n const isKeyPressed = useCallback((key: string): boolean => {\n return pressedKeys.current.has(key);\n }, []);\n\n // Enhanced keyboard event handlers\n const handleKeyDown = useCallback(\n (event: KeyboardEvent) => {\n if (!enabled) return;\n\n const key = event.key.toLowerCase();\n pressedKeys.current.add(key);\n\n // ✅ FIXED: Add all movement keys including WASD and arrows\n // Update both ref (for physics loop) and state (for React re-render)\n switch (key) {\n case \"w\":\n case \"arrowup\":\n keyStateRef.current.up = true;\n setKeyState((prev) => ({ ...prev, up: true }));\n event.preventDefault();\n break;\n case \"s\":\n case \"arrowdown\":\n keyStateRef.current.down = true;\n setKeyState((prev) => ({ ...prev, down: true }));\n event.preventDefault();\n break;\n case \"a\":\n case \"arrowleft\":\n keyStateRef.current.left = true;\n setKeyState((prev) => ({ ...prev, left: true }));\n event.preventDefault();\n break;\n case \"d\":\n case \"arrowright\":\n keyStateRef.current.right = true;\n setKeyState((prev) => ({ ...prev, right: true }));\n event.preventDefault();\n break;\n }\n },\n [enabled],\n );\n\n const handleKeyUp = useCallback(\n (event: KeyboardEvent) => {\n if (!enabled) return;\n\n const key = event.key.toLowerCase();\n pressedKeys.current.delete(key);\n\n // ✅ FIXED: Handle key release for all movement keys\n // Update both ref (for physics loop) and state (for React re-render)\n switch (key) {\n case \"w\":\n case \"arrowup\":\n keyStateRef.current.up = false;\n setKeyState((prev) => ({ ...prev, up: false }));\n break;\n case \"s\":\n case \"arrowdown\":\n keyStateRef.current.down = false;\n setKeyState((prev) => ({ ...prev, down: false }));\n break;\n case \"a\":\n case \"arrowleft\":\n keyStateRef.current.left = false;\n setKeyState((prev) => ({ ...prev, left: false }));\n break;\n case \"d\":\n case \"arrowright\":\n keyStateRef.current.right = false;\n setKeyState((prev) => ({ ...prev, right: false }));\n break;\n }\n },\n [enabled],\n );\n\n // ✅ FIXED: Proper movement calculation with correct bounds\n // Use a ref to store the callback to avoid reference before declaration issue\n const updatePositionRef = useRef<(() => void) | null>(null);\n\n const updatePosition = useCallback(() => {\n // Check if any movement keys are pressed using ref (not stale state)\n const keys = keyStateRef.current;\n const isCurrentlyMoving = keys.up || keys.down || keys.left || keys.right;\n\n if (!enabled || !isCurrentlyMoving) {\n animationFrameId.current = null;\n return;\n }\n\n const now = performance.now();\n const deltaTime = Math.min(now - (lastUpdateTime.current ?? now), 50);\n lastUpdateTime.current = now;\n\n if (deltaTime <= 0) {\n animationFrameId.current = requestAnimationFrame(() =>\n updatePositionRef.current?.(),\n );\n return;\n }\n\n // Physics-based movement (always enabled for realistic combat)\n if (physicsEngineRef.current && physicsStateRef.current) {\n // Apply speed modifiers if provided by SpeedModifierSystem\n // BUG FIX: Now properly passing maxSpeedOverride to physics engine\n if (maxSpeedOverride !== undefined) {\n physicsEngineRef.current.setMaxSpeed(maxSpeedOverride);\n }\n\n if (accelerationOverride !== undefined) {\n physicsEngineRef.current.setAcceleration(accelerationOverride);\n }\n\n // Convert key state to physics input (using ref to avoid callback recreation)\n // Screen coordinates: UP/W = toward top of screen, DOWN/S = toward bottom\n // Physics Z-axis: negative Z = toward top, positive Z = toward bottom\n const keys = keyStateRef.current;\n const forward = keys.up ? -1 : keys.down ? 1 : 0;\n const lateral = keys.right ? 1 : keys.left ? -1 : 0;\n const isCurrentlyMoving = forward !== 0 || lateral !== 0;\n\n // Auto-run detection: transition to running after sustained movement\n const now = performance.now();\n if (isCurrentlyMoving) {\n movementStartTimeRef.current ??= now;\n } else {\n movementStartTimeRef.current = null;\n }\n\n // Determine if player should be running (auto-run after threshold)\n const movementDuration = movementStartTimeRef.current\n ? now - movementStartTimeRef.current\n : 0;\n const shouldRun =\n isRunningProp || movementDuration > AUTO_RUN_THRESHOLD_MS;\n\n const physicsInput: MovementInput = {\n forward,\n lateral,\n isRunning: shouldRun,\n isMoving: isCurrentlyMoving,\n useTacticalSteps,\n };\n\n // Update physics state\n const state = physicsStateRef.current;\n state.currentStance = currentStance;\n state.legInjuryFactor = legInjuryFactor;\n\n // Clamp delta time to 1/30s (≈33.33ms) to match usePlayerMovement and prevent instability\n const clampedDeltaTimeMs = Math.min(deltaTime, 1000 / 30);\n\n // Use arena bounds computed via useMemo (available synchronously)\n physicsEngineRef.current.updateMovement(\n state,\n physicsInput,\n clampedDeltaTimeMs / 1000,\n arenaBounds, // Use memoized bounds\n );\n\n // Position in meters (x = lateral, y = forward/backward)\n const newPosition = { x: state.position.x, y: state.position.z };\n\n // Velocity in m/s (x = lateral, y = forward/backward)\n const newVelocity = { x: state.velocity.x, y: state.velocity.z };\n const newSpeed = state.velocity.length();\n\n // Use refs for comparison to avoid recreating callback on every frame\n // This prevents the animation frame from being cancelled due to useCallback recreation\n const lastPos = lastReportedPositionRef.current;\n if (newPosition.x !== lastPos.x || newPosition.y !== lastPos.y) {\n lastReportedPositionRef.current = newPosition;\n setPlayerPosition(newPosition);\n onPositionChange?.(newPosition);\n }\n\n // Update velocity and speed if changed (with epsilon tolerance for floating-point stability)\n const EPSILON = 0.001;\n const lastVel = lastReportedVelocityRef.current;\n const velocityChanged =\n !lastVel ||\n Math.abs(lastVel.x - newVelocity.x) > EPSILON ||\n Math.abs(lastVel.y - newVelocity.y) > EPSILON;\n if (velocityChanged) {\n lastReportedVelocityRef.current = newVelocity;\n setVelocity(newVelocity);\n }\n // Initialize speed when undefined, then update only on significant changes\n const lastSpd = lastReportedSpeedRef.current;\n if (lastSpd === undefined || Math.abs(lastSpd - newSpeed) > EPSILON) {\n lastReportedSpeedRef.current = newSpeed;\n setSpeed(newSpeed);\n }\n }\n\n // Continue animation if still moving (check ref, not stale closure)\n const stillMoving =\n keyStateRef.current.up ||\n keyStateRef.current.down ||\n keyStateRef.current.left ||\n keyStateRef.current.right;\n if (stillMoving) {\n animationFrameId.current = requestAnimationFrame(() =>\n updatePositionRef.current?.(),\n );\n } else {\n animationFrameId.current = null;\n }\n // NOTE: playerPosition, velocity, speed, keyState, isMoving intentionally excluded from deps\n // Using refs (lastReportedPositionRef, lastReportedVelocityRef, lastReportedSpeedRef, keyStateRef)\n // for comparison to prevent animation frame cancellation on every state update.\n // arenaBounds is computed from bounds and automatically updates when bounds changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n enabled,\n // playerPosition - excluded, using ref\n // keyState - excluded, using keyStateRef\n // isMoving - excluded, using keyStateRef for movement check\n // arenaBounds - excluded, derived from bounds (below)\n bounds,\n onPositionChange,\n currentStance,\n legInjuryFactor,\n isRunningProp,\n useTacticalSteps,\n // velocity - excluded, using ref\n // speed - excluded, using ref\n maxSpeedOverride,\n accelerationOverride,\n ]);\n\n // Keep updatePositionRef in sync via useEffect (not during render)\n useEffect(() => {\n updatePositionRef.current = updatePosition;\n }, [updatePosition]);\n\n // Handle keyboard input\n useEffect(() => {\n if (!enabled) return;\n\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n if (animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n }\n };\n }, [enabled, handleKeyDown, handleKeyUp]);\n\n // Start animation loop when movement begins\n useEffect(() => {\n if (isMoving && !animationFrameId.current) {\n lastUpdateTime.current = performance.now();\n // Use ref to avoid dependency on updatePosition callback\n animationFrameId.current = requestAnimationFrame(() => {\n updatePositionRef.current?.();\n });\n } else if (!isMoving && animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n animationFrameId.current = null;\n }\n\n return () => {\n if (animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n animationFrameId.current = null;\n }\n };\n // Only depend on isMoving - updatePositionRef is stable\n }, [isMoving]);\n\n return {\n playerPosition,\n movementState,\n isMoving,\n isKeyPressed,\n velocity,\n speed,\n };\n}\n\nexport interface InputEvent {\n readonly type: \"keydown\" | \"keyup\" | \"click\" | \"touchstart\" | \"touchend\";\n readonly key?: string;\n readonly target?: EventTarget | null;\n readonly timestamp: number;\n}\n\nexport interface CombatInput {\n readonly stanceChange?: TrigramStance;\n readonly attack?: boolean;\n readonly block?: boolean;\n readonly movement?: MovementState;\n readonly timestamp: number;\n}\n\n/**\n * Input system for combat controls\n */\nexport class InputSystem {\n private actionCallbacks = new Map<string, (() => void)[]>();\n private isEnabled = true;\n\n constructor() {\n this.setupEventListeners();\n }\n\n private setupEventListeners() {\n window.addEventListener(\"keydown\", this.handleKeyDown.bind(this));\n window.addEventListener(\"keyup\", this.handleKeyUp.bind(this));\n }\n\n private handleKeyDown(event: KeyboardEvent) {\n if (!this.isEnabled) return;\n\n const key = event.key;\n this.triggerAction(`keydown:${key}`);\n this.triggerAction(\"keydown\");\n }\n\n private handleKeyUp(event: KeyboardEvent) {\n if (!this.isEnabled) return;\n\n const key = event.key;\n this.triggerAction(`keyup:${key}`);\n this.triggerAction(\"keyup\");\n }\n\n registerAction(action: string, callback: () => void) {\n if (!this.actionCallbacks.has(action)) {\n this.actionCallbacks.set(action, []);\n }\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n callbacks.push(callback);\n }\n }\n\n unregisterAction(action: string, callback?: () => void) {\n if (!this.actionCallbacks.has(action)) return;\n\n if (callback) {\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n const index = callbacks.indexOf(callback);\n if (index > -1) {\n callbacks.splice(index, 1);\n }\n }\n } else {\n this.actionCallbacks.delete(action);\n }\n }\n\n clearActions() {\n this.actionCallbacks.clear();\n }\n\n isActionActive(action: string): boolean {\n return this.actionCallbacks.has(action);\n }\n\n enable() {\n this.isEnabled = true;\n }\n\n disable() {\n this.isEnabled = false;\n }\n\n private triggerAction(action: string) {\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n callbacks.forEach((callback) => callback());\n }\n }\n\n destroy() {\n window.removeEventListener(\"keydown\", this.handleKeyDown.bind(this));\n window.removeEventListener(\"keyup\", this.handleKeyUp.bind(this));\n this.clearActions();\n }\n}\n\n/**\n * Get stance from keyboard input\n */\nexport function getStanceFromKey(key: string): TrigramStance | null {\n const stanceKey = key as keyof typeof COMBAT_CONTROLS.stanceControls;\n\n if (stanceKey in COMBAT_CONTROLS.stanceControls) {\n return COMBAT_CONTROLS.stanceControls[stanceKey].stance;\n }\n\n return null;\n}\n\n/**\n * Process combat input and return structured combat data\n */\nexport function processCombatInput(event: KeyboardEvent): CombatInput | null {\n const key = event.key;\n const timestamp = performance.now();\n\n // Check for stance change (1-8 keys)\n const stance = getStanceFromKey(key);\n if (stance) {\n return {\n stanceChange: stance,\n timestamp,\n };\n }\n\n // Check for combat actions\n switch (key.toLowerCase()) {\n case \" \": // Space for attack\n return {\n attack: true,\n timestamp,\n };\n case \"shift\":\n return {\n block: true,\n timestamp,\n };\n default:\n return null;\n }\n}\n\n/**\n * Hook for combat input handling\n */\nexport function useCombatInput(onCombatInput: (input: CombatInput) => void) {\n const isEnabled = useRef<boolean>(true);\n\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (!isEnabled.current) return;\n\n const combatInput = processCombatInput(event);\n if (combatInput) {\n onCombatInput(combatInput);\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [onCombatInput]);\n\n return {\n enable: () => {\n isEnabled.current = true;\n },\n disable: () => {\n isEnabled.current = false;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"inputSystem.js","names":[],"sources":["../../src/utils/inputSystem.ts"],"sourcesContent":["import { COMBAT_CONTROLS } from \"@/systems/types\";\nimport type { Position } from \"@/types/common\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport type { MovementInput } from \"../systems/physics/MovementPhysics\";\nimport { MovementPhysics } from \"../systems/physics/MovementPhysics\";\nimport { TrigramStance } from \"../types/common\";\nimport { calculateArenaBounds, DEFAULT_PHYSICS_ARENA_BOUNDS } from \"../types/PhysicsTypes\";\nimport type { MovementArenaBounds } from \"../types/PhysicsTypes\";\n\n/**\n * Configuration interface for the input system and player movement.\n * Uses physics-first approach: all positions and velocities are in meters.\n *\n * **Korean**: 입력 시스템 설정 (Input System Configuration)\n *\n * ## Physics-First Architecture\n *\n * This interface requires worldWidthMeters and worldDepthMeters to enable\n * the new physics-first coordinate system. Without these properties, the\n * movement system cannot properly convert between physics (meters) and\n * rendering (pixels).\n *\n * ### Migration Guide\n *\n * Existing code must be updated to pass world dimensions:\n *\n * ```typescript\n * // Before (incorrect):\n * const config = { bounds: { x: 0, y: 0, width: 960, height: 480 } };\n *\n * // After (correct):\n * const config = {\n * bounds: {\n * worldWidthMeters: 10, // From layout hook\n * worldDepthMeters: 10 // From layout hook\n * }\n * };\n * ```\n *\n * ### Fallback Behavior\n *\n * If worldWidthMeters/worldDepthMeters are not provided, the system falls back\n * to DEFAULT_PHYSICS_ARENA_BOUNDS (10m × 7.5m) to ensure movement stays bounded.\n * Callers SHOULD provide these values from their layout hooks (useCombatLayout, \n * useTrainingLayout) for proper arena sizing.\n */\nexport interface InputSystemConfig {\n /** Whether the input system is enabled and processing input */\n readonly enabled?: boolean;\n\n /**\n * Arena world dimensions in meters for physics calculations.\n *\n * **REQUIRED for physics-first coordinate system to work.**\n *\n * These values must come from layout hooks:\n * - CombatScreen3D: Use arenaBounds.worldWidthMeters/worldDepthMeters from useCombatLayout()\n * - TrainingScreen3D: Use trainingAreaBounds.worldWidthMeters/worldDepthMeters from useTrainingLayout()\n */\n readonly bounds?: {\n /** Physical arena width in meters (e.g., 6m mobile, 10m desktop, 14m 4K) */\n readonly worldWidthMeters: number;\n /** Physical arena depth in meters (e.g., 6m mobile, 10m desktop, 14m 4K) */\n readonly worldDepthMeters: number;\n };\n\n /** Callback invoked when player position changes (position in meters) */\n readonly onPositionChange?: (position: Position) => void;\n\n /** Initial player position in METERS (x = lateral, y = forward/backward) */\n readonly initialPositionMeters?: Position;\n\n // Physics-based movement parameters (always enabled)\n /** Current trigram stance affecting movement speed */\n readonly currentStance?: TrigramStance;\n\n /** Leg injury factor (0-1, where 1 is fully injured) affecting movement speed */\n readonly legInjuryFactor?: number;\n\n /** Whether player is running (sprint mode) */\n readonly isRunning?: boolean;\n\n /** Whether to use tactical step mode (30cm grid quantization) */\n readonly useTacticalSteps?: boolean;\n\n // Speed modifier overrides from SpeedModifierSystem\n /** Final calculated maximum speed in meters per second */\n readonly maxSpeedOverride?: number;\n\n /** Final calculated acceleration in meters per second squared */\n readonly accelerationOverride?: number;\n}\n\nexport interface MovementState {\n readonly up: boolean;\n readonly down: boolean;\n readonly left: boolean;\n readonly right: boolean;\n readonly position: Position;\n readonly isMoving: boolean; // Add isMoving to movement state\n}\n\nexport interface PlayerMovementResult {\n /** Player position in METERS (x = lateral, y = forward/backward in arena) */\n readonly playerPosition: Position;\n readonly movementState: MovementState;\n readonly isMoving: boolean;\n readonly isKeyPressed: (key: string) => boolean;\n /** Velocity in m/s (x = lateral, y = forward/backward) */\n readonly velocity?: { x: number; y: number };\n /** Current speed magnitude in m/s */\n readonly speed?: number;\n}\n\n/**\n * Hook for handling player movement with physics-first approach.\n * All positions and velocities are in METERS - no pixel conversions.\n *\n * **Korean**: 플레이어 이동 훅 (Player Movement Hook)\n *\n * @param config - Physics-first configuration with positions in meters\n * @returns Movement state and physics data (all in meters)\n */\nexport function usePlayerMovement(\n config: InputSystemConfig,\n): PlayerMovementResult {\n const {\n enabled = true,\n bounds,\n onPositionChange,\n initialPositionMeters = { x: 0, y: 0 },\n currentStance = TrigramStance.GEON,\n legInjuryFactor = 0,\n isRunning: isRunningProp = false,\n useTacticalSteps = false,\n maxSpeedOverride,\n accelerationOverride,\n } = config;\n\n // Position in METERS (x = lateral position, y = forward/backward position)\n const [playerPosition, setPlayerPosition] = useState<Position>(\n initialPositionMeters,\n );\n const [keyState, setKeyState] = useState({\n up: false,\n down: false,\n left: false,\n right: false,\n });\n // Physics state for render (velocity and speed in m/s)\n const [velocity, setVelocity] = useState<\n { x: number; y: number } | undefined\n >(undefined);\n const [speed, setSpeed] = useState<number | undefined>(undefined);\n\n // Auto-run detection: track how long movement keys have been held\n // After sustained movement, automatically transition from walking to running\n const movementStartTimeRef = useRef<number | null>(null);\n const AUTO_RUN_THRESHOLD_MS = 300; // Transition to run after 300ms of sustained movement\n\n // Physics-based movement state (always initialized for realistic combat)\n const physicsEngineRef = useRef<MovementPhysics | null>(null);\n const physicsStateRef = useRef<{\n position: THREE.Vector3;\n velocity: THREE.Vector3;\n acceleration: number;\n maxSpeed: number;\n currentStance: TrigramStance;\n legInjuryFactor: number;\n } | null>(null);\n\n // Initialize physics engine once on mount (always enabled)\n // All positions are in METERS - no pixel conversion needed\n useEffect(() => {\n if (!physicsEngineRef.current) {\n // Use arena width for physics-aware speed scaling\n // Validate and fall back to default if invalid\n const width = bounds?.worldWidthMeters;\n const arenaWidth =\n width != null && Number.isFinite(width) && width > 0\n ? width\n : DEFAULT_PHYSICS_ARENA_BOUNDS.worldWidthMeters;\n physicsEngineRef.current = new MovementPhysics(arenaWidth);\n // Initial position in meters (x = lateral, z = forward/backward)\n physicsStateRef.current = {\n position: new THREE.Vector3(\n initialPositionMeters.x,\n 0,\n initialPositionMeters.y,\n ),\n velocity: new THREE.Vector3(0, 0, 0),\n acceleration: 0,\n maxSpeed: 6.0, // Default to BASE_WALK_SPEED (6.0 m/s for responsive combat)\n currentStance,\n legInjuryFactor: legInjuryFactor ?? 0,\n };\n }\n }, []); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Compute arena bounds synchronously when bounds dimensions change\n // Uses useMemo to ensure bounds are available immediately (not after effect runs)\n // Falls back to default arena bounds if invalid or missing\n // Depend on the whole `bounds` object so the compiler's inferred property-access\n // dependencies (bounds.worldWidthMeters / bounds.worldDepthMeters) are covered.\n const arenaBoundsResult = useMemo<{\n bounds: MovementArenaBounds | undefined;\n error?: Error;\n }>(() => {\n if (bounds?.worldWidthMeters != null && bounds?.worldDepthMeters != null) {\n try {\n return {\n bounds: calculateArenaBounds(\n {\n worldWidthMeters: bounds.worldWidthMeters,\n worldDepthMeters: bounds.worldDepthMeters,\n },\n 0.3 // 0.3m character radius\n ),\n };\n } catch (error) {\n // If validation fails, fall back to default bounds\n // Error will be logged in useEffect to keep render pure\n return {\n bounds: undefined,\n error: error instanceof Error ? error : new Error(String(error)),\n };\n }\n }\n\n // Fallback: use default arena bounds to ensure movement stays bounded\n try {\n return {\n bounds: calculateArenaBounds(\n {\n worldWidthMeters: DEFAULT_PHYSICS_ARENA_BOUNDS.worldWidthMeters,\n worldDepthMeters: DEFAULT_PHYSICS_ARENA_BOUNDS.worldDepthMeters,\n },\n 0.3 // 0.3m character radius\n ),\n };\n } catch (error) {\n // Should never happen with default bounds, but handle gracefully\n // Error will be logged in useEffect to keep render pure\n return {\n bounds: undefined,\n error: error instanceof Error ? error : new Error(String(error)),\n };\n }\n }, [bounds]);\n\n const arenaBounds = arenaBoundsResult.bounds;\n\n // Log arena bounds calculation errors in an effect (not during render)\n useEffect(() => {\n if (arenaBoundsResult.error) {\n if (bounds?.worldWidthMeters != null && bounds?.worldDepthMeters != null) {\n // Custom bounds failed validation\n console.warn(\n \"Failed to calculate arena bounds, using defaults:\",\n arenaBoundsResult.error\n );\n } else {\n // Should never happen with default bounds\n console.error(\n \"Failed to calculate default arena bounds:\",\n arenaBoundsResult.error\n );\n }\n }\n }, [arenaBoundsResult.error, bounds?.worldWidthMeters, bounds?.worldDepthMeters]);\n\n // Update physics engine arena width when bounds change (legacy)\n useEffect(() => {\n if (!physicsEngineRef.current) {\n return;\n }\n\n const width = bounds?.worldWidthMeters;\n if (width == null) {\n return;\n }\n\n // Validate width before applying to physics engine to avoid runtime errors\n if (!Number.isFinite(width) || width <= 0) {\n console.warn(\n \"Ignoring invalid worldWidthMeters when updating arena width:\",\n width,\n );\n return;\n }\n\n try {\n physicsEngineRef.current.setArenaWidth(width);\n } catch (error) {\n console.warn(\"Failed to update physics arena width:\", error);\n }\n }, [bounds?.worldWidthMeters]);\n\n // Track pressed keys for combat system\n const pressedKeys = useRef<Set<string>>(new Set());\n // Use useState lazy initializer for performance.now() to avoid impure function during render\n const [initialTime] = useState(() => performance.now());\n const lastUpdateTime = useRef(initialTime);\n const animationFrameId = useRef<number | null>(null);\n\n // Refs to track last reported position/velocity to avoid useCallback dependency issues\n // This prevents the animation frame from being cancelled every frame due to callback recreation\n const lastReportedPositionRef = useRef<Position>(initialPositionMeters);\n const lastReportedVelocityRef = useRef<{ x: number; y: number } | undefined>(\n undefined,\n );\n const lastReportedSpeedRef = useRef<number | undefined>(undefined);\n\n // Ref to track keyState for physics loop - avoids recreating callback on key changes\n const keyStateRef = useRef({\n up: false,\n down: false,\n left: false,\n right: false,\n });\n\n // Calculate if currently moving\n const isMoving =\n keyState.up || keyState.down || keyState.left || keyState.right;\n\n // Create complete movement state\n const movementState: MovementState = {\n ...keyState,\n position: playerPosition,\n isMoving,\n };\n\n // Key press checker for combat system\n const isKeyPressed = useCallback((key: string): boolean => {\n return pressedKeys.current.has(key);\n }, []);\n\n // Enhanced keyboard event handlers\n const handleKeyDown = useCallback(\n (event: KeyboardEvent) => {\n if (!enabled) return;\n\n const key = event.key.toLowerCase();\n pressedKeys.current.add(key);\n\n // ✅ FIXED: Add all movement keys including WASD and arrows\n // Update both ref (for physics loop) and state (for React re-render)\n switch (key) {\n case \"w\":\n case \"arrowup\":\n keyStateRef.current.up = true;\n setKeyState((prev) => ({ ...prev, up: true }));\n event.preventDefault();\n break;\n case \"s\":\n case \"arrowdown\":\n keyStateRef.current.down = true;\n setKeyState((prev) => ({ ...prev, down: true }));\n event.preventDefault();\n break;\n case \"a\":\n case \"arrowleft\":\n keyStateRef.current.left = true;\n setKeyState((prev) => ({ ...prev, left: true }));\n event.preventDefault();\n break;\n case \"d\":\n case \"arrowright\":\n keyStateRef.current.right = true;\n setKeyState((prev) => ({ ...prev, right: true }));\n event.preventDefault();\n break;\n }\n },\n [enabled],\n );\n\n const handleKeyUp = useCallback(\n (event: KeyboardEvent) => {\n if (!enabled) return;\n\n const key = event.key.toLowerCase();\n pressedKeys.current.delete(key);\n\n // ✅ FIXED: Handle key release for all movement keys\n // Update both ref (for physics loop) and state (for React re-render)\n switch (key) {\n case \"w\":\n case \"arrowup\":\n keyStateRef.current.up = false;\n setKeyState((prev) => ({ ...prev, up: false }));\n break;\n case \"s\":\n case \"arrowdown\":\n keyStateRef.current.down = false;\n setKeyState((prev) => ({ ...prev, down: false }));\n break;\n case \"a\":\n case \"arrowleft\":\n keyStateRef.current.left = false;\n setKeyState((prev) => ({ ...prev, left: false }));\n break;\n case \"d\":\n case \"arrowright\":\n keyStateRef.current.right = false;\n setKeyState((prev) => ({ ...prev, right: false }));\n break;\n }\n },\n [enabled],\n );\n\n // ✅ FIXED: Proper movement calculation with correct bounds\n // Use a ref to store the callback to avoid reference before declaration issue\n const updatePositionRef = useRef<(() => void) | null>(null);\n\n const updatePosition = useCallback(() => {\n // Check if any movement keys are pressed using ref (not stale state)\n const keys = keyStateRef.current;\n const isCurrentlyMoving = keys.up || keys.down || keys.left || keys.right;\n\n if (!enabled || !isCurrentlyMoving) {\n animationFrameId.current = null;\n return;\n }\n\n const now = performance.now();\n const deltaTime = Math.min(now - (lastUpdateTime.current ?? now), 50);\n lastUpdateTime.current = now;\n\n if (deltaTime <= 0) {\n animationFrameId.current = requestAnimationFrame(() =>\n updatePositionRef.current?.(),\n );\n return;\n }\n\n // Physics-based movement (always enabled for realistic combat)\n if (physicsEngineRef.current && physicsStateRef.current) {\n // Apply speed modifiers if provided by SpeedModifierSystem\n // BUG FIX: Now properly passing maxSpeedOverride to physics engine\n if (maxSpeedOverride !== undefined) {\n physicsEngineRef.current.setMaxSpeed(maxSpeedOverride);\n }\n\n if (accelerationOverride !== undefined) {\n physicsEngineRef.current.setAcceleration(accelerationOverride);\n }\n\n // Convert key state to physics input (using ref to avoid callback recreation)\n // Screen coordinates: UP/W = toward top of screen, DOWN/S = toward bottom\n // Physics Z-axis: negative Z = toward top, positive Z = toward bottom\n const keys = keyStateRef.current;\n const forward = keys.up ? -1 : keys.down ? 1 : 0;\n const lateral = keys.right ? 1 : keys.left ? -1 : 0;\n const isCurrentlyMoving = forward !== 0 || lateral !== 0;\n\n // Auto-run detection: transition to running after sustained movement\n const now = performance.now();\n if (isCurrentlyMoving) {\n movementStartTimeRef.current ??= now;\n } else {\n movementStartTimeRef.current = null;\n }\n\n // Determine if player should be running (auto-run after threshold)\n const movementDuration = movementStartTimeRef.current\n ? now - movementStartTimeRef.current\n : 0;\n const shouldRun =\n isRunningProp || movementDuration > AUTO_RUN_THRESHOLD_MS;\n\n const physicsInput: MovementInput = {\n forward,\n lateral,\n isRunning: shouldRun,\n isMoving: isCurrentlyMoving,\n useTacticalSteps,\n };\n\n // Update physics state\n const state = physicsStateRef.current;\n state.currentStance = currentStance;\n state.legInjuryFactor = legInjuryFactor;\n\n // Clamp delta time to 1/30s (≈33.33ms) to match usePlayerMovement and prevent instability\n const clampedDeltaTimeMs = Math.min(deltaTime, 1000 / 30);\n\n // Use arena bounds computed via useMemo (available synchronously)\n physicsEngineRef.current.updateMovement(\n state,\n physicsInput,\n clampedDeltaTimeMs / 1000,\n arenaBounds, // Use memoized bounds\n );\n\n // Position in meters (x = lateral, y = forward/backward)\n const newPosition = { x: state.position.x, y: state.position.z };\n\n // Velocity in m/s (x = lateral, y = forward/backward)\n const newVelocity = { x: state.velocity.x, y: state.velocity.z };\n const newSpeed = state.velocity.length();\n\n // Use refs for comparison to avoid recreating callback on every frame\n // This prevents the animation frame from being cancelled due to useCallback recreation\n const lastPos = lastReportedPositionRef.current;\n if (newPosition.x !== lastPos.x || newPosition.y !== lastPos.y) {\n lastReportedPositionRef.current = newPosition;\n setPlayerPosition(newPosition);\n onPositionChange?.(newPosition);\n }\n\n // Update velocity and speed if changed (with epsilon tolerance for floating-point stability)\n const EPSILON = 0.001;\n const lastVel = lastReportedVelocityRef.current;\n const velocityChanged =\n !lastVel ||\n Math.abs(lastVel.x - newVelocity.x) > EPSILON ||\n Math.abs(lastVel.y - newVelocity.y) > EPSILON;\n if (velocityChanged) {\n lastReportedVelocityRef.current = newVelocity;\n setVelocity(newVelocity);\n }\n // Initialize speed when undefined, then update only on significant changes\n const lastSpd = lastReportedSpeedRef.current;\n if (lastSpd === undefined || Math.abs(lastSpd - newSpeed) > EPSILON) {\n lastReportedSpeedRef.current = newSpeed;\n setSpeed(newSpeed);\n }\n }\n\n // Continue animation if still moving (check ref, not stale closure)\n const stillMoving =\n keyStateRef.current.up ||\n keyStateRef.current.down ||\n keyStateRef.current.left ||\n keyStateRef.current.right;\n if (stillMoving) {\n animationFrameId.current = requestAnimationFrame(() =>\n updatePositionRef.current?.(),\n );\n } else {\n animationFrameId.current = null;\n }\n // NOTE: playerPosition, velocity, speed, keyState, isMoving intentionally excluded from deps\n // Using refs (lastReportedPositionRef, lastReportedVelocityRef, lastReportedSpeedRef, keyStateRef)\n // for comparison to prevent animation frame cancellation on every state update.\n // arenaBounds is computed from bounds and automatically updates when bounds changes\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n enabled,\n // playerPosition - excluded, using ref\n // keyState - excluded, using keyStateRef\n // isMoving - excluded, using keyStateRef for movement check\n // arenaBounds - excluded, derived from bounds (below)\n bounds,\n onPositionChange,\n currentStance,\n legInjuryFactor,\n isRunningProp,\n useTacticalSteps,\n // velocity - excluded, using ref\n // speed - excluded, using ref\n maxSpeedOverride,\n accelerationOverride,\n ]);\n\n // Keep updatePositionRef in sync via useEffect (not during render)\n useEffect(() => {\n updatePositionRef.current = updatePosition;\n }, [updatePosition]);\n\n // Handle keyboard input\n useEffect(() => {\n if (!enabled) return;\n\n window.addEventListener(\"keydown\", handleKeyDown);\n window.addEventListener(\"keyup\", handleKeyUp);\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n window.removeEventListener(\"keyup\", handleKeyUp);\n if (animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n }\n };\n }, [enabled, handleKeyDown, handleKeyUp]);\n\n // Start animation loop when movement begins\n useEffect(() => {\n if (isMoving && !animationFrameId.current) {\n lastUpdateTime.current = performance.now();\n // Use ref to avoid dependency on updatePosition callback\n animationFrameId.current = requestAnimationFrame(() => {\n updatePositionRef.current?.();\n });\n } else if (!isMoving && animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n animationFrameId.current = null;\n }\n\n return () => {\n if (animationFrameId.current) {\n cancelAnimationFrame(animationFrameId.current);\n animationFrameId.current = null;\n }\n };\n // Only depend on isMoving - updatePositionRef is stable\n }, [isMoving]);\n\n return {\n playerPosition,\n movementState,\n isMoving,\n isKeyPressed,\n velocity,\n speed,\n };\n}\n\nexport interface InputEvent {\n readonly type: \"keydown\" | \"keyup\" | \"click\" | \"touchstart\" | \"touchend\";\n readonly key?: string;\n readonly target?: EventTarget | null;\n readonly timestamp: number;\n}\n\nexport interface CombatInput {\n readonly stanceChange?: TrigramStance;\n readonly attack?: boolean;\n readonly block?: boolean;\n readonly movement?: MovementState;\n readonly timestamp: number;\n}\n\n/**\n * Input system for combat controls\n */\nexport class InputSystem {\n private actionCallbacks = new Map<string, (() => void)[]>();\n private isEnabled = true;\n\n constructor() {\n this.setupEventListeners();\n }\n\n private setupEventListeners() {\n window.addEventListener(\"keydown\", this.handleKeyDown.bind(this));\n window.addEventListener(\"keyup\", this.handleKeyUp.bind(this));\n }\n\n private handleKeyDown(event: KeyboardEvent) {\n if (!this.isEnabled) return;\n\n const key = event.key;\n this.triggerAction(`keydown:${key}`);\n this.triggerAction(\"keydown\");\n }\n\n private handleKeyUp(event: KeyboardEvent) {\n if (!this.isEnabled) return;\n\n const key = event.key;\n this.triggerAction(`keyup:${key}`);\n this.triggerAction(\"keyup\");\n }\n\n registerAction(action: string, callback: () => void) {\n if (!this.actionCallbacks.has(action)) {\n this.actionCallbacks.set(action, []);\n }\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n callbacks.push(callback);\n }\n }\n\n unregisterAction(action: string, callback?: () => void) {\n if (!this.actionCallbacks.has(action)) return;\n\n if (callback) {\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n const index = callbacks.indexOf(callback);\n if (index > -1) {\n callbacks.splice(index, 1);\n }\n }\n } else {\n this.actionCallbacks.delete(action);\n }\n }\n\n clearActions() {\n this.actionCallbacks.clear();\n }\n\n isActionActive(action: string): boolean {\n return this.actionCallbacks.has(action);\n }\n\n enable() {\n this.isEnabled = true;\n }\n\n disable() {\n this.isEnabled = false;\n }\n\n private triggerAction(action: string) {\n const callbacks = this.actionCallbacks.get(action);\n if (callbacks) {\n callbacks.forEach((callback) => callback());\n }\n }\n\n destroy() {\n window.removeEventListener(\"keydown\", this.handleKeyDown.bind(this));\n window.removeEventListener(\"keyup\", this.handleKeyUp.bind(this));\n this.clearActions();\n }\n}\n\n/**\n * Get stance from keyboard input\n */\nexport function getStanceFromKey(key: string): TrigramStance | null {\n const stanceKey = key as keyof typeof COMBAT_CONTROLS.stanceControls;\n\n if (stanceKey in COMBAT_CONTROLS.stanceControls) {\n return COMBAT_CONTROLS.stanceControls[stanceKey].stance;\n }\n\n return null;\n}\n\n/**\n * Process combat input and return structured combat data\n */\nexport function processCombatInput(event: KeyboardEvent): CombatInput | null {\n const key = event.key;\n const timestamp = performance.now();\n\n // Check for stance change (1-8 keys)\n const stance = getStanceFromKey(key);\n if (stance) {\n return {\n stanceChange: stance,\n timestamp,\n };\n }\n\n // Check for combat actions\n switch (key.toLowerCase()) {\n case \" \": // Space for attack\n return {\n attack: true,\n timestamp,\n };\n case \"shift\":\n return {\n block: true,\n timestamp,\n };\n default:\n return null;\n }\n}\n\n/**\n * Hook for combat input handling\n */\nexport function useCombatInput(onCombatInput: (input: CombatInput) => void) {\n const isEnabled = useRef<boolean>(true);\n\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (!isEnabled.current) return;\n\n const combatInput = processCombatInput(event);\n if (combatInput) {\n onCombatInput(combatInput);\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [onCombatInput]);\n\n return {\n enable: () => {\n isEnabled.current = true;\n },\n disable: () => {\n isEnabled.current = false;\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blacktrigram",
3
- "version": "0.7.19",
3
+ "version": "0.7.20",
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",
@@ -183,7 +183,7 @@
183
183
  "postprocessing": "6.0.0",
184
184
  "react": "19.0.0",
185
185
  "react-dom": "19.0.0",
186
- "three": "0.183.0"
186
+ "three": "0.184.0"
187
187
  },
188
188
  "devDependencies": {
189
189
  "@aws-sdk/client-bedrock-runtime": "3.1031.0",
@@ -198,7 +198,7 @@
198
198
  "@types/node": "25.6.0",
199
199
  "@types/react": "19.2.14",
200
200
  "@types/react-dom": "19.2.3",
201
- "@types/three": "0.183.1",
201
+ "@types/three": "0.184.0",
202
202
  "@vitejs/plugin-react": "6.0.1",
203
203
  "@vitest/coverage-v8": "4.1.4",
204
204
  "@vitest/ui": "4.1.4",
@@ -209,7 +209,7 @@
209
209
  "dependency-cruiser": "17.3.10",
210
210
  "dotenv": "17.4.2",
211
211
  "eslint": "10.2.0",
212
- "eslint-plugin-react-hooks": "7.0.1",
212
+ "eslint-plugin-react-hooks": "7.1.0",
213
213
  "eslint-plugin-react-refresh": "0.5.2",
214
214
  "globals": "17.5.0",
215
215
  "jest-axe": "10.0.0",
@@ -226,7 +226,7 @@
226
226
  "react": "19.2.5",
227
227
  "react-dom": "19.2.5",
228
228
  "start-server-and-test": "3.0.2",
229
- "three": "0.183.2",
229
+ "three": "0.184.0",
230
230
  "ts-morph": "28.0.0",
231
231
  "ts-node": "10.9.2",
232
232
  "tsc-alias": "1.8.16",
@@ -235,7 +235,7 @@
235
235
  "typedoc-plugin-markdown": "4.11.0",
236
236
  "typedoc-plugin-mermaid": "1.12.0",
237
237
  "typedoc-plugin-missing-exports": "4.1.3",
238
- "typescript": "6.0.2",
238
+ "typescript": "6.0.3",
239
239
  "typescript-eslint": "8.58.2",
240
240
  "vite": "8.0.8",
241
241
  "vite-bundle-analyzer": "1.3.7",
@@ -246,6 +246,8 @@
246
246
  "eslint-plugin-react-hooks": {
247
247
  "eslint": "$eslint"
248
248
  },
249
- "glob": "13.0.1"
249
+ "glob": "13.0.1",
250
+ "three": "0.184.0",
251
+ "@types/three": "0.184.0"
250
252
  }
251
253
  }