257 lines
9.7 KiB
TypeScript
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();
|