// Movement/Components/AC_Movement.ts import { BFL_Vectors } from '#root/Math/Libraries/BFL_Vectors.ts'; import { E_MovementState } from '#root/Movement/Enums/E_MovementState.ts'; import { E_SurfaceType } from '#root/Movement/Enums/E_SurfaceType.ts'; import { S_AngleThresholds } from '#root/Movement/Structs/S_AngleThresholds.ts'; import type { S_MovementConstants } from '#root/Movement/Structs/S_MovementConstants.ts'; import { ActorComponent } from '#root/UE/ActorComponent.ts'; import type { Float } from '#root/UE/Float.ts'; import { MathLibrary } from '#root/UE/MathLibrary.ts'; import { Rotator } from '#root/UE/Rotator.ts'; import { Vector } from '#root/UE/Vector.ts'; /** * Movement System Component * Core deterministic movement system for 3D platformer * Handles surface classification and movement physics calculations */ export class AC_Movement extends ActorComponent { // ════════════════════════════════════════════════════════════════════════════════════════ // FUNCTIONS // ════════════════════════════════════════════════════════════════════════════════════════ /** * Classify surface type based on normal vector * @param SurfaceNormal - Normalized surface normal vector * @param AngleThresholds - Angle thresholds in radians * @returns Surface type classification * @example * // Classify flat ground * ClassifySurface(new Vector(0,0,1), thresholds) // returns E_SurfaceType.Walkable * @pure true * @category Surface Detection */ public ClassifySurface( SurfaceNormal: Vector, AngleThresholds: S_AngleThresholds ): E_SurfaceType { const SurfaceAngle = BFL_Vectors.GetSurfaceAngle(SurfaceNormal); /** * Check if angle is within walkable range */ const IsWalkableAngle = (walkableAngle: Float): boolean => SurfaceAngle <= walkableAngle; /** * Check if angle is within steep slope range */ const IsSteepSlopeAngle = (steepSlopeAngle: Float): boolean => SurfaceAngle <= steepSlopeAngle; /** * Check if angle is within wall range */ const IsWallAngle = (wallAngle: Float): boolean => SurfaceAngle <= wallAngle; if (IsWalkableAngle(AngleThresholds.Walkable)) { return E_SurfaceType.Walkable; } else if (IsSteepSlopeAngle(AngleThresholds.SteepSlope)) { return E_SurfaceType.SteepSlope; } else if (IsWallAngle(AngleThresholds.Wall)) { return E_SurfaceType.Wall; } else { return E_SurfaceType.Ceiling; } } /** * Check if surface allows normal walking movement * @param SurfaceType - Surface type to check * @returns True if surface is walkable * @pure true * @category Surface Detection */ private IsSurfaceWalkable(SurfaceType: E_SurfaceType): boolean { return SurfaceType === E_SurfaceType.Walkable; } /** * Check if surface causes sliding behavior * @param SurfaceType - Surface type to check * @returns True if surface is steep slope * @pure true * @category Surface Detection */ private IsSurfaceSteep(SurfaceType: E_SurfaceType): boolean { return SurfaceType === E_SurfaceType.SteepSlope; } /** * Check if surface blocks movement (collision) * @param SurfaceType - Surface type to check * @returns True if surface is a wall * @pure true * @category Surface Detection */ private IsSurfaceWall(SurfaceType: E_SurfaceType): boolean { return SurfaceType === E_SurfaceType.Wall; } /** * Check if surface is overhead (ceiling) * @param SurfaceType - Surface type to check * @returns True if surface is ceiling * @pure true * @category Surface Detection */ private IsSurfaceCeiling(SurfaceType: E_SurfaceType): boolean { return SurfaceType === E_SurfaceType.Ceiling; } /** * Check if no surface detected (airborne state) * @param SurfaceType - Surface type to check * @returns True if no surface contact * @pure true * @category Surface Detection */ private IsSurfaceNone(SurfaceType: E_SurfaceType): boolean { return SurfaceType === E_SurfaceType.None; } /** * Process movement input from player controller * Normalizes input and calculates target velocity * @param InputVector - Raw input from WASD/gamepad stick * @param DeltaTime - Time since last frame for frame-rate independence * @category Movement Processing */ public ProcessMovementInput(InputVector: Vector, DeltaTime: Float): void { if (this.IsInitialized) { this.InputMagnitude = MathLibrary.VectorLength(InputVector); this.TargetRotation = this.CalculateTargetRotation(InputVector); this.UpdateCharacterRotation(DeltaTime); // Only process movement on walkable surfaces if (this.IsSurfaceWalkable(this.CurrentSurface) && this.IsGrounded) { this.ProcessGroundMovement(InputVector, DeltaTime); } else { this.ApplyFriction(DeltaTime); } // Always apply gravity this.ApplyGravity(DeltaTime); // Update movement state this.UpdateMovementState(); // Calculate current speed for debug this.UpdateCurrentSpeed(); } } /** * Process ground-based movement with acceleration and max speed limits * @param InputVector - Normalized input direction * @param DeltaTime - Frame delta time * @category Movement Processing */ private ProcessGroundMovement(InputVector: Vector, DeltaTime: Float): void { if (this.InputMagnitude > 0.01) { const CalculateTargetVelocity = ( inputVector: Vector, maxSpeed: Float ): Vector => new Vector( MathLibrary.Normal(inputVector).X * maxSpeed, MathLibrary.Normal(inputVector).Y * maxSpeed, MathLibrary.Normal(inputVector).Z * maxSpeed ); this.CurrentVelocity = MathLibrary.VInterpTo( new Vector(this.CurrentVelocity.X, this.CurrentVelocity.Y, 0), CalculateTargetVelocity(InputVector, this.MovementConstants.MaxSpeed), DeltaTime, this.MovementConstants.Acceleration ); } else { // Apply friction when no input this.ApplyFriction(DeltaTime); } } /** * Apply friction to horizontal velocity * @param DeltaTime - Frame delta time * @category Movement Processing */ private ApplyFriction(DeltaTime: Float): void { this.CurrentVelocity = MathLibrary.VInterpTo( new Vector(this.CurrentVelocity.X, this.CurrentVelocity.Y, 0), new Vector(0, 0, this.CurrentVelocity.Z), DeltaTime, this.MovementConstants.Friction ); } /** * Apply gravity to vertical velocity * @param DeltaTime - Frame delta time * @category Movement Processing */ private ApplyGravity(DeltaTime: Float): void { if (!this.IsGrounded) { const ApplyGravityForce = ( velocityZ: Float, gravity: Float, deltaTime: Float ): Float => velocityZ - gravity * deltaTime; // Apply gravity when airborne this.CurrentVelocity = new Vector( this.CurrentVelocity.X, this.CurrentVelocity.Y, ApplyGravityForce( this.CurrentVelocity.Z, this.MovementConstants.Gravity, DeltaTime ) ); } else { // Zero out vertical velocity when grounded this.CurrentVelocity = new Vector( this.CurrentVelocity.X, this.CurrentVelocity.Y, 0 ); } } /** * Update movement state based on current conditions * @category Movement Processing */ private UpdateMovementState(): void { if (!this.IsGrounded) { this.MovementState = E_MovementState.Airborne; } else if (this.InputMagnitude > 0.01) { this.MovementState = E_MovementState.Walking; } else { this.MovementState = E_MovementState.Idle; } } /** * Update current speed for debug display * @category Movement Processing */ private UpdateCurrentSpeed(): void { // Calculate horizontal speed only (ignore vertical component) this.CurrentSpeed = MathLibrary.VectorLength( new Vector(this.CurrentVelocity.X, this.CurrentVelocity.Y, 0) ); } /** * Calculate target rotation based on movement direction * Determines what rotation character should have based on movement * @param MovementDirection - Camera-relative movement direction from Blueprint * @returns Target rotation for character * @pure true * @category Character Rotation */ public CalculateTargetRotation(MovementDirection: Vector): Rotator { const TargetYaw = ( movementDirectionX: Float, movementDirectionY: Float ): Float => MathLibrary.RadiansToDegrees( MathLibrary.Atan2(movementDirectionY, movementDirectionX) ); if (MathLibrary.VectorLength(MovementDirection) < 0.01) { // No movement, maintain current target rotation return this.TargetRotation; } // Character should remain level (pitch = 0, roll = 0, only yaw changes) return new Rotator( 0, TargetYaw(MovementDirection.X, MovementDirection.Y), 0 ); } /** * Update character rotation towards target * Uses instant rotation approach similar to Zelda: BotW * @param DeltaTime - Time since last frame * @category Character Rotation */ public UpdateCharacterRotation(DeltaTime: Float): void { if (this.ShouldRotateToMovement) { const ShouldRotate = (): boolean => this.CurrentSpeed >= this.MinSpeedForRotation; if (ShouldRotate()) { let yawDifference = this.CurrentRotation.yaw - this.TargetRotation.yaw; if (yawDifference > 180) { yawDifference = yawDifference - 360; } else if (yawDifference < -180) { yawDifference = yawDifference + 360; } const NewYaw = ( currentYaw: Float, clampedRotationAmount: Float, coef: Float ): Float => currentYaw + clampedRotationAmount * coef; const ClampedRotationAmount = (deltaTime: Float): Float => MathLibrary.Min( this.RotationSpeed * deltaTime, MathLibrary.abs(yawDifference) ); this.RotationDelta = MathLibrary.abs(yawDifference); if (this.RotationDelta > 1) { this.IsCharacterRotating = true; this.CurrentRotation = new Rotator( 0, NewYaw( this.CurrentRotation.yaw, ClampedRotationAmount(DeltaTime), yawDifference > 0 ? -1 : 1 ), 0 ); } else { this.IsCharacterRotating = false; this.CurrentRotation = this.TargetRotation; } } else { this.IsCharacterRotating = false; this.RotationDelta = 0.0; } } } /** * Initialize movement system with angle conversion * Converts degree thresholds to radians for runtime performance * @category System */ public InitializeMovementSystem(): void { this.IsInitialized = true; this.AngleThresholdsRads = { Walkable: MathLibrary.DegreesToRadians( this.AngleThresholdsDegrees.Walkable ), SteepSlope: MathLibrary.DegreesToRadians( this.AngleThresholdsDegrees.SteepSlope ), Wall: MathLibrary.DegreesToRadians(this.AngleThresholdsDegrees.Wall), }; } // ════════════════════════════════════════════════════════════════════════════════════════ // VARIABLES // ════════════════════════════════════════════════════════════════════════════════════════ /** * Movement physics constants * Controls speed, acceleration, friction, and gravity values * @category Movement Config * @instanceEditable true */ public readonly MovementConstants: S_MovementConstants = { MaxSpeed: 600.0, Acceleration: 10.0, Friction: 8.0, Gravity: 980.0, }; /** * Surface classification angle thresholds in degrees * Walkable ≤50°, SteepSlope ≤85°, Wall ≤95°, Ceiling >95° * @category Movement Config * @instanceEditable true */ public readonly AngleThresholdsDegrees: S_AngleThresholds = { Walkable: 50.0, SteepSlope: 85.0, Wall: 95.0, }; /** * Runtime cached angle thresholds in radians * Converted from degrees during initialization for performance * @category Internal Cache */ private AngleThresholdsRads: S_AngleThresholds = { Walkable: 0.0, SteepSlope: 0.0, Wall: 0.0, }; /** * Flag indicating if movement system has been initialized * Ensures angle thresholds are converted before use * @category Debug */ public IsInitialized = false; /** * Current character velocity in world space * Updated every frame by movement calculations * @category Movement State */ public CurrentVelocity: Vector = new Vector(0, 0, 0); /** * Ground contact state - true when character is on walkable surface * Used for gravity application and movement restrictions * @category Movement State */ public IsGrounded: boolean = true; /** * Type of surface currently under character * Determines available movement options * @category Movement State */ public CurrentSurface: E_SurfaceType = E_SurfaceType.Walkable; /** * Current movement state of character * Used for animation and game logic decisions * @category Movement State */ public MovementState: E_MovementState = E_MovementState.Idle; /** * Magnitude of current movement input (0-1) * Used for determining if character should be moving * @category Movement State */ public InputMagnitude: Float = 0.0; /** * Current movement speed (magnitude of horizontal velocity) * Calculated from CurrentVelocity for debug display * @category Movement State */ public CurrentSpeed: Float = 0.0; /** * Rotation speed in degrees per second * Controls how fast character rotates towards movement direction * @category Character Rotation Config * @instanceEditable true */ public RotationSpeed: Float = 720.0; /** * Should character rotate when moving * Allows disabling rotation system if needed * @category Character Rotation Config * @instanceEditable true */ public ShouldRotateToMovement: boolean = true; /** * Minimum movement speed to trigger rotation * Prevents character from rotating during very slow movement * @category Character Rotation Config * @instanceEditable true */ public MinSpeedForRotation: Float = 50.0; /** * Current character rotation * Used for smooth interpolation to target rotation * @category Character Rotation State */ public CurrentRotation: Rotator = new Rotator(0, 0, 0); /** * Target character rotation * Calculated based on movement direction and camera orientation * @category Character Rotation State */ public TargetRotation: Rotator = new Rotator(0, 0, 0); /** * Whether character is currently rotating * Used for animation and debug purposes * @category Character Rotation State */ public IsCharacterRotating: boolean = false; /** * Angular difference between current and target rotation * Used for determining rotation completion and debug display * @category Character Rotation State */ public RotationDelta: Float = 0.0; }