// Movement/Collision/BFL_GroundProbe.ts import { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts'; import { BFL_SurfaceClassifier } from '#root/Movement/Surface/BFL_SurfaceClassifier.ts'; import { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts'; import type { S_AngleThresholds } from '#root/Movement/Surface/S_AngleThresholds.ts'; import { BlueprintFunctionLibrary } from '#root/UE/BlueprintFunctionLibrary.ts'; import type { CapsuleComponent } from '#root/UE/CapsuleComponent.ts'; import { EDrawDebugTrace } from '#root/UE/EDrawDebugTrace.ts'; import { ETraceTypeQuery } from '#root/UE/ETraceTypeQuery.ts'; import type { Float } from '#root/UE/Float.ts'; import { HitResult } from '#root/UE/HitResult.ts'; import { MathLibrary } from '#root/UE/MathLibrary.ts'; import { SystemLibrary } from '#root/UE/SystemLibrary.ts'; import { Vector } from '#root/UE/Vector.ts'; class BFL_GroundProbeClass extends BlueprintFunctionLibrary { // ════════════════════════════════════════════════════════════════════════════════════════ // GROUND DETECTION // ════════════════════════════════════════════════════════════════════════════════════════ /** * Check if character is standing on walkable ground * Performs line trace downward from capsule bottom * Validates surface type using SurfaceClassifier * * @param CharacterLocation - Current character world location * @param CapsuleComponent - Character's capsule component for trace setup * @param AngleThresholdsRads - Surface angle thresholds in radians * @param Config - Movement configuration with trace distance * @param IsShowVisualDebug - Whether to draw debug trace in world * @returns HitResult with ground information, or empty hit if not grounded * * @example * const groundHit = GroundProbe.CheckGround( * characterLocation, * capsuleComponent, * angleThresholdsRads, * config * ); * if (groundHit.BlockingHit) { * // Character is on walkable ground * } * * @impure true - performs world trace * @category Ground Detection */ public CheckGround( CharacterLocation: Vector = new Vector(0, 0, 0), CapsuleComponent: CapsuleComponent | null = null, AngleThresholdsRads: S_AngleThresholds = { Walkable: 0, SteepSlope: 0, Wall: 0, }, Config: DA_MovementConfig = new DA_MovementConfig(), IsShowVisualDebug: boolean = false ): HitResult { if (SystemLibrary.IsValid(CapsuleComponent)) { const CalculateEndLocation = ( currentZ: Float, halfHeight: Float, groundTraceDistance: Float ): Float => currentZ - halfHeight - groundTraceDistance; const { OutHit: groundHit, ReturnValue } = SystemLibrary.LineTraceByChannel( CharacterLocation, new Vector( CharacterLocation.X, CharacterLocation.Y, CalculateEndLocation( CharacterLocation.Z, CapsuleComponent.GetScaledCapsuleHalfHeight(), Config.GroundTraceDistance ) ), ETraceTypeQuery.Visibility, false, [], IsShowVisualDebug ? EDrawDebugTrace.ForDuration : EDrawDebugTrace.None ); // Check if trace hit something if (!ReturnValue) { return new HitResult(); } if ( BFL_SurfaceClassifier.IsWalkable( BFL_SurfaceClassifier.Classify( groundHit.ImpactNormal, AngleThresholdsRads ) ) ) { return groundHit; } else { return new HitResult(); } } else { return new HitResult(); } } // ════════════════════════════════════════════════════════════════════════════════════════ // GROUND SNAPPING // ════════════════════════════════════════════════════════════════════════════════════════ /** * Calculate snapped location to keep character on ground * Prevents character from floating slightly above ground * Only snaps if within reasonable distance threshold * * @param CurrentLocation - Current character location * @param GroundHit - Ground hit result from CheckGround * @param CapsuleComponent - Character's capsule component * @param SnapThreshold - Maximum distance to snap (default: 10 cm) * @returns Snapped location or original location if too far * * @example * const snappedLocation = GroundProbe.CalculateSnapLocation( * currentLocation, * groundHit, * capsuleComponent, * 10.0 * ); * character.SetActorLocation(snappedLocation); * * @pure true - only calculations, no side effects * @category Ground Snapping */ public CalculateSnapLocation( CurrentLocation: Vector, GroundHit: HitResult, CapsuleComponent: CapsuleComponent | null, SnapThreshold: Float = 10.0 ): Vector { if (GroundHit.BlockingHit) { if (SystemLibrary.IsValid(CapsuleComponent)) { const correctZ = GroundHit.Location.Z + CapsuleComponent.GetScaledCapsuleHalfHeight(); const CalculateZDifference = (currentLocZ: Float): Float => MathLibrary.abs(currentLocZ - correctZ); const zDifference = CalculateZDifference(CurrentLocation.Z); const ShouldSnap = (groundTraceDistance: Float): boolean => zDifference > 0.1 && zDifference < groundTraceDistance; if (ShouldSnap(SnapThreshold)) { return new Vector(CurrentLocation.X, CurrentLocation.Y, correctZ); } else { return CurrentLocation; } } else { return CurrentLocation; } } else { return CurrentLocation; } } /** * Check if ground snapping should be applied * Helper method to determine if conditions are right for snapping * * @param CurrentVelocityZ - Current vertical velocity * @param GroundHit - Ground hit result * @param IsGrounded - Whether character is considered grounded * @returns True if snapping should be applied * * @example * if (GroundProbe.ShouldSnapToGround(velocity.Z, groundHit, isGrounded)) { * const snappedLoc = GroundProbe.CalculateSnapLocation(...); * character.SetActorLocation(snappedLoc); * } * * @pure true * @category Ground Snapping */ public ShouldSnapToGround( CurrentVelocityZ: Float, GroundHit: HitResult, IsGrounded: boolean ): boolean { return IsGrounded && GroundHit.BlockingHit && CurrentVelocityZ <= 0; } // ════════════════════════════════════════════════════════════════════════════════════════ // UTILITIES // ════════════════════════════════════════════════════════════════════════════════════════ /** * Get surface type from ground hit * Convenience method combining trace result with classification * * @param GroundHit - Ground hit result * @param AngleThresholdsRads - Surface angle thresholds in radians * @returns Surface type classification * * @pure true * @category Utilities */ public GetSurfaceType( GroundHit: HitResult, AngleThresholdsRads: S_AngleThresholds ): E_SurfaceType { if (!GroundHit.BlockingHit) { return BFL_SurfaceClassifier.Classify( GroundHit.ImpactNormal, AngleThresholdsRads ); } else { return E_SurfaceType.None; } } } export const BFL_GroundProbe = new BFL_GroundProbeClass();