blacktrigram 0.7.81 → 0.7.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/lib/components/screens/combat/CombatScreen3D.js +16 -11
  2. package/lib/components/screens/combat/CombatScreen3D.js.map +1 -1
  3. package/lib/components/screens/combat/components/effects/ParticleAudio3D.js +2 -1
  4. package/lib/components/screens/combat/components/effects/ParticleAudio3D.js.map +1 -1
  5. package/lib/components/screens/combat/hooks/useAICombat.js +2 -1
  6. package/lib/components/screens/combat/hooks/useAICombat.js.map +1 -1
  7. package/lib/components/screens/combat/hooks/useCombatActions.js +25 -9
  8. package/lib/components/screens/combat/hooks/useCombatActions.js.map +1 -1
  9. package/lib/components/screens/combat/hooks/useCombatAttackMovement.js +10 -6
  10. package/lib/components/screens/combat/hooks/useCombatAttackMovement.js.map +1 -1
  11. package/lib/components/screens/intro/IntroScreen3D.js +8 -4
  12. package/lib/components/screens/intro/IntroScreen3D.js.map +1 -1
  13. package/lib/components/screens/training/TrainingScreen3D.js +5 -3
  14. package/lib/components/screens/training/TrainingScreen3D.js.map +1 -1
  15. package/lib/components/screens/training/hooks/useAttackMovement.js +2 -1
  16. package/lib/components/screens/training/hooks/useAttackMovement.js.map +1 -1
  17. package/lib/components/screens/training/hooks/useTrainingActions.js +2 -1
  18. package/lib/components/screens/training/hooks/useTrainingActions.js.map +1 -1
  19. package/lib/components/shared/three/effects/ActionFeedback.js +2 -1
  20. package/lib/components/shared/three/effects/ActionFeedback.js.map +1 -1
  21. package/lib/components/shared/three/effects/DamageNumbers.js +2 -1
  22. package/lib/components/shared/three/effects/DamageNumbers.js.map +1 -1
  23. package/lib/components/shared/three/effects/HitEffects3D.js +2 -1
  24. package/lib/components/shared/three/effects/HitEffects3D.js.map +1 -1
  25. package/lib/components/shared/ui/SplashScreen.js +2 -2
  26. package/lib/hooks/useMuscleActivation.js +4 -1
  27. package/lib/hooks/useMuscleActivation.js.map +1 -1
  28. package/lib/hooks/useRoundTransition.js +2 -1
  29. package/lib/hooks/useRoundTransition.js.map +1 -1
  30. package/lib/hooks/useSkeletalAnimation.js +1 -1
  31. package/lib/index.js +2 -2
  32. package/lib/systems/VitalPointSystem.js +2 -1
  33. package/lib/systems/VitalPointSystem.js.map +1 -1
  34. package/lib/systems/animation/core/AnimationStateMachine.js +1 -1
  35. package/lib/systems/index.js +2 -2
  36. package/lib/utils/inputSystem.js +2 -1
  37. package/lib/utils/inputSystem.js.map +1 -1
  38. package/package.json +7 -7
@@ -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 // 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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"}
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.81",
3
+ "version": "0.7.83",
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",
@@ -189,7 +189,7 @@
189
189
  "three": "0.185.1"
190
190
  },
191
191
  "devDependencies": {
192
- "@aws-sdk/client-bedrock-runtime": "3.1081.0",
192
+ "@aws-sdk/client-bedrock-runtime": "3.1085.0",
193
193
  "@eslint/js": "10.0.1",
194
194
  "@react-three/drei": "10.7.7",
195
195
  "@react-three/fiber": "9.6.1",
@@ -201,7 +201,7 @@
201
201
  "@types/node": "26.1.1",
202
202
  "@types/react": "19.2.17",
203
203
  "@types/react-dom": "19.2.3",
204
- "@types/three": "0.185.0",
204
+ "@types/three": "0.185.1",
205
205
  "@vitejs/plugin-react": "6.0.3",
206
206
  "@vitest/coverage-v8": "4.1.10",
207
207
  "@vitest/ui": "4.1.10",
@@ -217,14 +217,14 @@
217
217
  "globals": "17.7.0",
218
218
  "jest-axe": "10.0.0",
219
219
  "jsdom": "29.1.1",
220
- "knip": "6.25.0",
220
+ "knip": "6.26.0",
221
221
  "license-compliance": "3.0.1",
222
222
  "mermaid": "11.16.0",
223
223
  "mocha-junit-reporter": "2.2.1",
224
224
  "mochawesome": "7.1.4",
225
225
  "mochawesome-merge": "5.1.1",
226
226
  "mochawesome-report-generator": "6.3.2",
227
- "openai": "6.45.0",
227
+ "openai": "6.46.0",
228
228
  "playwright": "1.61.1",
229
229
  "postprocessing": "6.39.2",
230
230
  "react": "19.2.7",
@@ -242,7 +242,7 @@
242
242
  "typescript": "npm:@typescript/typescript6@6.0.2",
243
243
  "typescript-7": "npm:typescript@7.0.1-rc",
244
244
  "typescript-eslint": "8.63.0",
245
- "vite": "8.1.3",
245
+ "vite": "8.1.4",
246
246
  "vite-bundle-analyzer": "1.3.8",
247
247
  "vite-tsconfig-paths": "6.1.1",
248
248
  "vitest": "4.1.10"
@@ -253,7 +253,7 @@
253
253
  },
254
254
  "glob": "13.0.1",
255
255
  "three": "0.185.1",
256
- "@types/three": "0.185.0",
256
+ "@types/three": "0.185.1",
257
257
  "joi": "17.13.4",
258
258
  "diff": "8.0.3",
259
259
  "serialize-javascript": "7.0.6",