// Movement/Rotation/BFL_RotationController.ts import type { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts'; import type { S_RotationResult } from '#root/Movement/Rotation/S_RotationResult.ts'; import type { Float } from '#root/UE/Float.ts'; import type { Integer } from '#root/UE/Integer.ts'; import { MathLibrary } from '#root/UE/MathLibrary.ts'; import { Rotator } from '#root/UE/Rotator.ts'; import type { Vector } from '#root/UE/Vector.ts'; /** * Character Rotation Controller * * Pure functional module for character rotation calculations * Handles smooth rotation toward movement direction * All methods are deterministic and side-effect free * * @category Movement Rotation * @pure All methods are pure functions */ class BFL_RotationControllerClass { // ════════════════════════════════════════════════════════════════════════════════════════ // TARGET CALCULATION // ════════════════════════════════════════════════════════════════════════════════════════ /** * Calculate target yaw angle from movement direction * Converts 2D movement vector to rotation angle * * @param MovementDirection - Movement direction vector (XY plane) * @returns Target yaw angle in degrees * * @example * // Moving forward (X+) * const yaw = RotationController.CalculateTargetYaw(new Vector(1, 0, 0)); * // Returns: 0° * * @example * // Moving right (Y+) * const yaw = RotationController.CalculateTargetYaw(new Vector(0, 1, 0)); * // Returns: 90° * * @pure true * @category Target Calculation */ public CalculateTargetYaw(MovementDirection: Vector): Float { // Use atan2 to get angle from X/Y components // Returns angle in degrees return MathLibrary.Atan2Degrees(MovementDirection.Y, MovementDirection.X); } /** * Calculate target rotation from movement direction * Creates full Rotator with only yaw set (pitch/roll = 0) * * @param MovementDirection - Movement direction vector * @returns Target rotation (yaw only, pitch/roll = 0) * * @pure true * @category Target Calculation */ public CalculateTargetRotation(MovementDirection: Vector): Rotator { return new Rotator(0, this.CalculateTargetYaw(MovementDirection), 0); } // ════════════════════════════════════════════════════════════════════════════════════════ // ROTATION INTERPOLATION // ════════════════════════════════════════════════════════════════════════════════════════ /** * Interpolate rotation smoothly toward target * Handles angle wraparound (180°/-180° boundary) * * @param CurrentRotation - Current character rotation * @param TargetRotation - Desired target rotation * @param RotationSpeed - Rotation speed in degrees/sec * @param DeltaTime - Frame delta time * @param MinSpeedForRotation - Minimum speed to allow rotation (default: 0) * @param CurrentSpeed - Current movement speed for threshold check * @returns RotationResult with new rotation and metadata * * @example * const result = RotationController.InterpolateRotation( * new Rotator(0, 0, 0), // Current: facing forward * new Rotator(0, 90, 0), // Target: facing right * 720, // 720°/sec rotation speed * 0.016, // 60 FPS delta * 50, // Min speed threshold * 500 // Current speed * ); * // Returns: Rotator smoothly interpolated toward 90° * * @pure true * @category Rotation Interpolation */ public InterpolateRotation( CurrentRotation: Rotator, TargetRotation: Rotator, RotationSpeed: Float, DeltaTime: Float, MinSpeedForRotation: Float = 0.0, CurrentSpeed: Float = 0.0 ): S_RotationResult { // Check if character is moving fast enough to rotate if (CurrentSpeed >= MinSpeedForRotation) { // Calculate angular distance with wraparound handling const angularDistance = this.GetAngularDistance( CurrentRotation.yaw, TargetRotation.yaw ); // Check if rotation is not complete (within 1° tolerance) if (MathLibrary.abs(angularDistance) <= 1.0) { const CalculateNewYaw = ( currentRotationYaw: Float, rotationDirection: Integer, rotationSpeed: Float, deltaTime: Float ): Float => currentRotationYaw + MathLibrary.Min( rotationSpeed * deltaTime, MathLibrary.abs(angularDistance) ) * rotationDirection; return { Rotation: new Rotator( 0, CalculateNewYaw( CurrentRotation.yaw, angularDistance > 0 ? -1 : 1, RotationSpeed, DeltaTime ), 0 ), IsRotating: true, RemainingDelta: MathLibrary.abs(angularDistance), }; } else { return { Rotation: TargetRotation, IsRotating: false, RemainingDelta: 0.0, }; } } else { return { Rotation: CurrentRotation, IsRotating: false, RemainingDelta: 0.0, }; } } // ════════════════════════════════════════════════════════════════════════════════════════ // ANGLE UTILITIES // ════════════════════════════════════════════════════════════════════════════════════════ /** * Calculate the shortest angular distance between two angles * Handles wraparound for shortest path * * @param fromAngle - Starting angle in degrees * @param toAngle - Target angle in degrees * @returns Signed angular distance (positive = clockwise, negative = counter-clockwise) * * @example * GetAngularDistance(10, 350) // Returns: -20 (shorter to go counter-clockwise) * GetAngularDistance(350, 10) // Returns: 20 (shorter to go clockwise) * GetAngularDistance(0, 180) // Returns: 180 (either direction same) * * @pure true * @category Angle Utilities */ public GetAngularDistance(fromAngle: Float, toAngle: Float): Float { // Calculate raw difference let difference = fromAngle - toAngle; // Normalize to the shortest path if (difference > 180) { difference -= 360; } else if (difference < -180) { difference += 360; } return difference; } // ════════════════════════════════════════════════════════════════════════════════════════ // CONVENIENCE METHODS // ════════════════════════════════════════════════════════════════════════════════════════ /** * Update character rotation toward movement direction * Convenience method combining target calculation and interpolation * * @param CurrentRotation - Current character rotation * @param MovementDirection - Movement direction vector * @param Config - Movement configuration with rotation settings * @param DeltaTime - Frame delta time * @param CurrentSpeed - Current movement speed * @returns RotationResult with updated rotation * * @example * const result = RotationController.UpdateRotation( * CurrentRotation, * InputVector, * Config, * DeltaTime, * CurrentSpeed * ); * character.SetActorRotation(result.Rotation); * * @pure true * @category Convenience Methods */ public UpdateRotation( CurrentRotation: Rotator, MovementDirection: Vector, Config: DA_MovementConfig, DeltaTime: Float, CurrentSpeed: Float ): S_RotationResult { // Rotation if enabled in config if (Config.ShouldRotateToMovement) { // Rotation if movement if (MathLibrary.VectorLength(MovementDirection) >= 0.01) { // Calculate target and interpolate; return this.InterpolateRotation( CurrentRotation, this.CalculateTargetRotation(MovementDirection), Config.RotationSpeed, DeltaTime, Config.MinSpeedForRotation, CurrentSpeed ); } else { return { Rotation: CurrentRotation, IsRotating: false, RemainingDelta: 0.0, }; } } else { return { Rotation: CurrentRotation, IsRotating: false, RemainingDelta: 0.0, }; } } } export const BFL_RotationController = new BFL_RotationControllerClass();