tengri/Content/Movement/Rotation/BFL_RotationController.ts

257 lines
9.7 KiB
TypeScript

// 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();