tengri/Content/Movement/Collision/BFL_GroundProbe.ts

222 lines
8.4 KiB
TypeScript

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