[code] AC_Movement refactoring

main
Nikolay Petrov 2025-11-17 18:36:17 +05:00
parent 8ee0cba309
commit 9539f48a06
67 changed files with 4566 additions and 2195 deletions

View File

@ -108,13 +108,13 @@ module.exports = {
{ {
'selector': 'variable', 'selector': 'variable',
'format': ['camelCase', 'UPPER_CASE', 'PascalCase'], 'format': ['camelCase', 'UPPER_CASE', 'PascalCase'],
'prefix': ['_', 'BP_', 'AC_', 'WBP_', 'UBP_', 'ABP_', 'IMC_', 'IA_', 'DT_', 'BFL_', 'U', 'A', 'T', 'F'] 'prefix': ['_', 'BP_', 'AC_', 'WBP_', 'UBP_', 'ABP_', 'IMC_', 'IA_', 'DT_', 'BFL_', 'DA_', 'U', 'A', 'T', 'F']
}, },
{ {
'selector': 'variable', 'selector': 'variable',
'format': ['camelCase', 'UPPER_CASE', 'PascalCase'], 'format': ['camelCase', 'UPPER_CASE', 'PascalCase'],
'filter': { 'filter': {
'regex': '^(?!_|BP_|AC_|WBP_|UBP_|ABP_|IA_|DT_|IMC_|BFL_|U[A-Z]|A[A-Z]|T[A-Z]|F[A-Z])', 'regex': '^(?!_|BP_|AC_|WBP_|UBP_|ABP_|IA_|DT_|IMC_|BFL_|DA_|U[A-Z]|A[A-Z]|T[A-Z]|F[A-Z])',
'match': true 'match': true
} }
}, },
@ -122,14 +122,14 @@ module.exports = {
{ {
'selector': 'class', 'selector': 'class',
'format': ['PascalCase'], 'format': ['PascalCase'],
'prefix': ['_', 'BP_', 'AC_', 'WBP_', 'UBP_', 'ABP_', 'IMC_', 'IA_', 'DT_', 'FT_', 'BFL_', 'U', 'A', 'T', 'F'] 'prefix': ['_', 'BP_', 'AC_', 'WBP_', 'UBP_', 'ABP_', 'IMC_', 'IA_', 'DT_', 'FT_', 'BFL_', 'DA_', 'U', 'A', 'T', 'F']
}, },
// Regular classes without prefix // Regular classes without prefix
{ {
'selector': 'class', 'selector': 'class',
'format': ['PascalCase'], 'format': ['PascalCase'],
'filter': { 'filter': {
'regex': '^(?!_|BP_|AC_|WBP_|UBP_|ABP_|IA_|IMC_|BFL_|U[A-Z]|A[A-Z]|T[A-Z]|F[A-Z])', 'regex': '^(?!_|BP_|AC_|WBP_|UBP_|ABP_|IA_|IMC_|BFL_|DA_|U[A-Z]|A[A-Z]|T[A-Z]|F[A-Z])',
'match': true 'match': true
} }
}, },
@ -137,7 +137,7 @@ module.exports = {
{ {
'selector': 'interface', 'selector': 'interface',
'format': ['PascalCase'], 'format': ['PascalCase'],
'prefix': ['S_', 'I_', 'I'] 'prefix': ['S_', 'I_', 'I', 'DA_']
}, },
// Enums // Enums
{ {

View File

@ -4,7 +4,7 @@ import { AC_Camera } from '#root/Camera/Components/AC_Camera.ts';
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts'; import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts'; import { AC_InputDevice } from '#root/Input/Components/AC_InputDevice.ts';
import { IMC_Default } from '#root/Input/IMC_Default.ts'; import { IMC_Default } from '#root/Input/IMC_Default.ts';
import { AC_Movement } from '#root/Movement/Components/AC_Movement.ts'; import { AC_Movement } from '#root/Movement/AC_Movement.ts';
import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts'; import { AC_ToastSystem } from '#root/Toasts/Components/AC_ToastSystem.ts';
import { CapsuleComponent } from '#root/UE/CapsuleComponent.ts'; import { CapsuleComponent } from '#root/UE/CapsuleComponent.ts';
import { Cast } from '#root/UE/Cast.ts'; import { Cast } from '#root/UE/Cast.ts';
@ -201,8 +201,6 @@ export class BP_MainCharacter extends Pawn {
DeltaTime DeltaTime
); );
this.SetActorRotation(this.MovementComponent.GetCurrentRotation());
if (this.ShowDebugInfo) { if (this.ShowDebugInfo) {
this.MovementComponent.UpdateDebugPage(); this.MovementComponent.UpdateDebugPage();
this.InputDeviceComponent.UpdateDebugPage(); this.InputDeviceComponent.UpdateDebugPage();

BIN
Content/Blueprints/BP_MainCharacter.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Content/Camera/Components/AC_Camera.uasset (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Content/Debug/Tests/FT_DebugSystem.uasset (Stored with Git LFS)

Binary file not shown.

View File

@ -1,6 +1,6 @@
// Debug/UI/WBP_DebugHUD.ts // Debug/UI/WBP_DebugHUD.ts
import type { AC_Movement } from '#root/Movement/Components/AC_Movement.js'; import type { AC_Movement } from '#root/Movement/AC_Movement.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts'; import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
import type { Text } from '#root/UE/Text.ts'; import type { Text } from '#root/UE/Text.ts';
import { TextBlock } from '#root/UE/TextBlock.ts'; import { TextBlock } from '#root/UE/TextBlock.ts';

BIN
Content/Debug/UI/WBP_DebugHUD.uasset (Stored with Git LFS)

Binary file not shown.

View File

@ -10,8 +10,6 @@ import { FT_DebugNavigation } from '#root/Debug/Tests/FT_DebugNavigation.ts';
import { FT_DebugPageManagement } from '#root/Debug/Tests/FT_DebugPageManagement.ts'; import { FT_DebugPageManagement } from '#root/Debug/Tests/FT_DebugPageManagement.ts';
import { FT_DebugSystem } from '#root/Debug/Tests/FT_DebugSystem.ts'; import { FT_DebugSystem } from '#root/Debug/Tests/FT_DebugSystem.ts';
import { FT_InputDeviceDetection } from '#root/Input/Tests/FT_InputDeviceDetection.ts'; import { FT_InputDeviceDetection } from '#root/Input/Tests/FT_InputDeviceDetection.ts';
import { FT_BasicMovement } from '#root/Movement/Tests/FT_BasicMovement.ts';
import { FT_SurfaceClassification } from '#root/Movement/Tests/FT_SurfaceClassification.ts';
import { FT_ToastLimit } from '#root/Toasts/Tests/FT_ToastLimit.ts'; import { FT_ToastLimit } from '#root/Toasts/Tests/FT_ToastLimit.ts';
import { FT_ToastsDurationHandling } from '#root/Toasts/Tests/FT_ToastsDurationHandling.ts'; import { FT_ToastsDurationHandling } from '#root/Toasts/Tests/FT_ToastsDurationHandling.ts';
import { FT_ToastsEdgeCases } from '#root/Toasts/Tests/FT_ToastsEdgeCases.ts'; import { FT_ToastsEdgeCases } from '#root/Toasts/Tests/FT_ToastsEdgeCases.ts';
@ -46,13 +44,6 @@ const InputDeviceDetectionTest = new FT_InputDeviceDetection();
InputDeviceDetectionTest.EventStartTest(); InputDeviceDetectionTest.EventStartTest();
// Movement Tests
const BasicMovementTest = new FT_BasicMovement();
const SurfaceClassificationTest = new FT_SurfaceClassification();
BasicMovementTest.EventStartTest();
SurfaceClassificationTest.EventStartTest();
// Toasts Tests // Toasts Tests
const ToastLimitsTest = new FT_ToastLimit(); const ToastLimitsTest = new FT_ToastLimit();
const ToastsDurationHandlingTest = new FT_ToastsDurationHandling(); const ToastsDurationHandlingTest = new FT_ToastsDurationHandling();

BIN
Content/Levels/TestLevel.umap (Stored with Git LFS)

Binary file not shown.

View File

@ -0,0 +1,231 @@
// Movement/AC_Movement.ts
import type { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import { BFL_MovementProcessor } from '#root/Movement/Core/BFL_MovementProcessor.ts';
import type { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts';
import { DA_MovementConfigDefault } from '#root/Movement/Core/DA_MovementConfigDefault.ts';
import { E_MovementState } from '#root/Movement/Core/E_MovementState.ts';
import type { S_MovementState } from '#root/Movement/Core/S_MovementState.ts';
import { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
import { S_AngleThresholds } from '#root/Movement/Surface/S_AngleThresholds.ts';
import { ActorComponent } from '#root/UE/ActorComponent.ts';
import type { CapsuleComponent } from '#root/UE/CapsuleComponent.ts';
import type { Float } from '#root/UE/Float.ts';
import { HitResult } from '#root/UE/HitResult.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts';
import { Rotator } from '#root/UE/Rotator.ts';
import { StringLibrary } from '#root/UE/StringLibrary.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.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
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// Debug
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Update debug HUD with current movement info
* @category Debug
*/
public UpdateDebugPage(): void {
if (SystemLibrary.IsValid(this.DebugHUDComponent)) {
if (
this.DebugHUDComponent.ShouldUpdatePage(
this.DebugPageID,
SystemLibrary.GetGameTimeInSeconds()
)
) {
this.DebugHUDComponent.UpdatePageContent(
this.DebugPageID,
// Constants
`Max Speed: ${this.Config.MaxSpeed}\n` +
`Acceleration: ${this.Config.Acceleration}\n` +
`Friction: ${this.Config.Friction}\n` +
`Gravity: ${this.Config.Gravity}\n` +
`Initialized: ${this.IsInitialized}\n` +
`\n` +
// Current State
`Current Velocity: ${StringLibrary.ConvVectorToString(this.CurrentMovementState.Velocity)}\n` +
`Speed: ${this.CurrentMovementState.Speed}\n` +
`Is Grounded: ${this.CurrentMovementState.IsGrounded}\n` +
`Surface Type: ${this.CurrentMovementState.SurfaceType}\n` +
`Movement State: ${this.CurrentMovementState.MovementState}\n` +
`Input Magnitude: ${this.CurrentMovementState.InputMagnitude}` +
`\n` +
// Rotation
`Current Yaw: ${this.CurrentMovementState.Rotation.yaw}\n` +
`Rotation Delta: ${this.CurrentMovementState.RotationDelta}\` +
`Is Rotating: ${this.CurrentMovementState.IsRotating}\n` +
`Rotation Speed: ${this.Config.RotationSpeed}\` +
`Min Speed: ${this.Config.MinSpeedForRotation}` +
`\n` +
// Position
`Location: ${StringLibrary.ConvVectorToString(this.GetOwner().GetActorLocation())}` +
`\n` +
// Sweep Collision
`Collision Checks: ${this.CurrentMovementState.CollisionCount}/${this.Config.MaxCollisionChecks}\n` +
`Sweep Blocked: ${this.CurrentMovementState.IsBlocked}\n` +
`Ground Distance: ${this.Config.GroundTraceDistance} cm`
);
}
}
}
// ────────────────────────────────────────────────────────────────────────────────────────
// Default
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* 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 Default
*/
public ProcessMovementInput(InputVector: Vector, DeltaTime: Float): void {
if (this.IsInitialized) {
this.CurrentMovementState = BFL_MovementProcessor.ProcessMovement(
this.CurrentMovementState,
{
InputVector,
DeltaTime,
CapsuleComponent: this.CapsuleComponent,
Config: this.Config,
AngleThresholdsRads: this.AngleThresholdsRads,
},
SystemLibrary.IsValid(this.DebugHUDComponent)
? this.DebugHUDComponent.ShowVisualDebug
: false
);
this.GetOwner().SetActorLocation(this.CurrentMovementState.Location);
this.GetOwner().SetActorRotation(this.CurrentMovementState.Rotation);
}
}
/**
* Initialize movement system with angle conversion
* Converts degree thresholds to radians for runtime performance
* @category Default
*/
public InitializeMovementSystem(
CapsuleComponentRef: CapsuleComponent | null = null,
DebugHUDComponentRef: AC_DebugHUD | null = null
): void {
this.CapsuleComponent = CapsuleComponentRef;
this.DebugHUDComponent = DebugHUDComponentRef;
this.IsInitialized = true;
this.AngleThresholdsRads = {
Walkable: MathLibrary.DegreesToRadians(
this.Config.AngleThresholdsDegrees.Walkable
),
SteepSlope: MathLibrary.DegreesToRadians(
this.Config.AngleThresholdsDegrees.SteepSlope
),
Wall: MathLibrary.DegreesToRadians(
this.Config.AngleThresholdsDegrees.Wall
),
};
this.CurrentMovementState = BFL_MovementProcessor.CreateInitialState(
this.GetOwner().GetActorLocation(),
this.GetOwner().GetActorRotation()
);
if (SystemLibrary.IsValid(this.DebugHUDComponent)) {
this.DebugHUDComponent.AddDebugPage(
this.DebugPageID,
'Movement Info',
60
);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// Components
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Reference to debug HUD component for displaying camera info
* Optional, used for debugging purposes
* @category Components
*/
private DebugHUDComponent: AC_DebugHUD | null = null;
/**
* Reference to character's capsule component for collision detection
* @category Components
*/
private CapsuleComponent: CapsuleComponent | null = null;
// ────────────────────────────────────────────────────────────────────────────────────────
// Default
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Default movement state
* @category Default
*/
private CurrentMovementState: S_MovementState = {
Location: new Vector(0, 0, 0),
Rotation: new Rotator(0, 0, 0),
Velocity: new Vector(0, 0, 0),
Speed: 0.0,
IsGrounded: false,
GroundHit: new HitResult(),
SurfaceType: E_SurfaceType.None,
IsBlocked: false,
CollisionCount: 0,
IsRotating: false,
RotationDelta: 0.0,
MovementState: E_MovementState.Idle,
InputMagnitude: 0.0,
};
/**
* Default movement configuration
* @category Default
* @instanceEditable true
*/
private readonly Config: DA_MovementConfig = new DA_MovementConfigDefault();
/**
* Runtime cached angle thresholds in radians
* Converted from degrees during initialization for performance
* @category Default
*/
private AngleThresholdsRads: S_AngleThresholds = {
Walkable: 0.0,
SteepSlope: 0.0,
Wall: 0.0,
};
/**
* Debug page identifier for organizing debug output
* Used by debug HUD to categorize information
* @category Default
* @instanceEditable true
*/
private readonly DebugPageID: string = 'MovementInfo';
/**
* Flag indicating if movement system has been initialized
* Ensures angle thresholds are converted before use
* @category Debug
*/
private IsInitialized = false;
}

BIN
Content/Movement/AC_Movement.uasset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,325 @@
// Movement/Collision/BFL_CollisionResolver.ts
import type { S_SweepResult } from '#root/Movement/Collision/S_SweepResult.ts';
import { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.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 type { Integer } from '#root/UE/Integer.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts';
import { SystemLibrary } from '#root/UE/SystemLibrary.ts';
import { Vector } from '#root/UE/Vector.ts';
/**
* Collision Resolution System
*
* Handles swept collision detection and surface sliding
* Prevents tunneling through geometry with adaptive stepping
* Provides deterministic collision response
*
* @category Movement Collision
* @impure Uses SystemLibrary traces (reads world state)
*/
class BFL_CollisionResolverClass extends BlueprintFunctionLibrary {
// ════════════════════════════════════════════════════════════════════════════════════════
// SWEEP COLLISION
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Perform deterministic swept collision detection
* Breaks movement into adaptive steps to prevent tunneling
* Handles multiple collision iterations for smooth sliding
*
* @param StartLocation - Starting position for sweep
* @param DesiredDelta - Desired movement vector
* @param CapsuleComponent - Capsule for collision shape
* @param Config - Movement configuration with step sizes and iteration limits
* @param DeltaTime - Frame delta time for adaptive step calculation
* @param IsShowVisualDebug - Whether to draw debug traces in world
* @returns SweepResult with final location and collision info
*
* @example
* const result = CollisionResolver.PerformSweep(
* characterLocation,
* new Vector(100, 0, 0), // Move 1 meter forward
* capsuleComponent,
* config,
* 0.016
* );
* character.SetActorLocation(result.Location);
*
* @impure true - performs world traces
* @category Sweep Collision
*/
public PerformSweep(
StartLocation: Vector = new Vector(0, 0, 0),
DesiredDelta: Vector = new Vector(0, 0, 0),
CapsuleComponent: CapsuleComponent | null = null,
Config: DA_MovementConfig = new DA_MovementConfig(),
DeltaTime: Float = 0,
IsShowVisualDebug: boolean = false
): S_SweepResult {
// Validate capsule component
if (SystemLibrary.IsValid(CapsuleComponent)) {
// Calculate total distance to travel
const totalDistance = MathLibrary.VectorLength(DesiredDelta);
// Early exit if movement is negligible
if (totalDistance >= 0.01) {
// Calculate adaptive step size based on velocity
const stepSize = this.CalculateStepSize(
new Vector(
DesiredDelta.X / DeltaTime,
DesiredDelta.Y / DeltaTime,
DesiredDelta.Z / DeltaTime
),
DeltaTime,
Config
);
// Perform stepped sweep
let currentLocation = StartLocation;
let remainingDistance = totalDistance;
let collisionCount = 0;
let lastHit = new HitResult();
// Calculate number of steps (capped by max collision checks)
const CalculateNumSteps = (maxCollisionChecks: Integer): Integer =>
MathLibrary.Min(
MathLibrary.Ceil(totalDistance / stepSize),
maxCollisionChecks
);
for (let i = 0; i < CalculateNumSteps(Config.MaxCollisionChecks); i++) {
collisionCount++;
// Calculate step distance (last step might be shorter)
const currentStepSize = MathLibrary.Min(stepSize, remainingDistance);
const MathExpression = (desiredDelta: Vector): Vector =>
new Vector(
currentLocation.X +
MathLibrary.Normal(desiredDelta).X * currentStepSize,
currentLocation.Y +
MathLibrary.Normal(desiredDelta).Y * currentStepSize,
currentLocation.Z +
MathLibrary.Normal(desiredDelta).Z * currentStepSize
);
// Calculate target position for this step
const targetLocation = MathExpression(DesiredDelta);
// Perform capsule trace for this step
const { OutHit, ReturnValue } = SystemLibrary.CapsuleTraceByChannel(
currentLocation,
targetLocation,
CapsuleComponent.GetScaledCapsuleRadius(),
CapsuleComponent.GetScaledCapsuleHalfHeight(),
ETraceTypeQuery.Visibility,
false,
[],
IsShowVisualDebug
? EDrawDebugTrace.ForDuration
: EDrawDebugTrace.None
);
// Check if trace hit something
if (ReturnValue) {
// Collision detected - return hit location
lastHit = OutHit;
return {
Location: lastHit.Location,
Hit: lastHit,
Blocked: true,
CollisionCount: collisionCount,
};
} else {
// No collision - update position and continue
currentLocation = targetLocation;
remainingDistance = remainingDistance - currentStepSize;
// Check if reached destination
if (remainingDistance <= 0.01) {
break;
}
}
}
// Reached destination without blocking hit
return {
Location: currentLocation,
Hit: lastHit,
Blocked: false,
CollisionCount: collisionCount,
};
} else {
return {
Location: StartLocation,
Hit: new HitResult(),
Blocked: false,
CollisionCount: 0,
};
}
} else {
return {
Location: StartLocation,
Hit: new HitResult(),
Blocked: false,
CollisionCount: 0,
};
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// SURFACE SLIDING
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Project movement vector onto collision surface for sliding
* Removes component of movement that goes into surface
* Allows character to slide smoothly along walls
*
* @param MovementDelta - Desired movement vector
* @param SurfaceNormal - Normal of surface that was hit
* @returns Projected movement vector parallel to surface
*
* @example
* // Character hits wall at 45° angle
* const slideVector = CollisionResolver.ProjectOntoSurface(
* new Vector(100, 100, 0), // Moving diagonally
* new Vector(-0.707, 0, 0.707) // Wall normal (45° wall)
* );
* // Returns vector parallel to wall surface
*
* @pure true
* @category Surface Sliding
*/
public ProjectOntoSurface(
MovementDelta: Vector,
SurfaceNormal: Vector
): Vector {
// Project by removing normal component
// Formula: V' = V - (V·N)N
const MathExpression = (
movementDelta: Vector,
surfaceNormal: Vector
): Vector =>
new Vector(
MovementDelta.X -
SurfaceNormal.X * MathLibrary.Dot(movementDelta, surfaceNormal),
MovementDelta.Y -
SurfaceNormal.Y * MathLibrary.Dot(movementDelta, surfaceNormal),
MovementDelta.Z -
SurfaceNormal.Z * MathLibrary.Dot(movementDelta, surfaceNormal)
);
return MathExpression(MovementDelta, SurfaceNormal);
}
/**
* Calculate sliding vector after collision
* Combines sweep result with projection for smooth sliding
*
* @param SweepResult - Result from PerformSweep
* @param OriginalDelta - Original desired movement
* @param StartLocation - Starting location before sweep
* @returns Vector to apply for sliding movement
*
* @example
* const slideVector = CollisionResolver.CalculateSlideVector(
* sweepResult,
* desiredDelta,
* startLocation
* );
* if (slideVector.Length() > 0.01) {
* character.SetActorLocation(sweepResult.Location + slideVector);
* }
*
* @pure true
* @category Surface Sliding
*/
public CalculateSlideVector(
SweepResult: S_SweepResult,
OriginalDelta: Vector,
StartLocation: Vector
): Vector {
if (SweepResult.Blocked) {
const MathExpression = (
sweepLocation: Vector,
startLocation: Vector,
originalDelta: Vector
): Vector =>
new Vector(
originalDelta.X - (sweepLocation.X - startLocation.X),
originalDelta.Y - (sweepLocation.Y - startLocation.Y),
originalDelta.Z - (sweepLocation.Z - startLocation.Z)
);
// Project remaining movement onto collision surface
return this.ProjectOntoSurface(
MathExpression(SweepResult.Location, StartLocation, OriginalDelta),
SweepResult.Hit.ImpactNormal
);
} else {
// No sliding if no collision
return new Vector(0, 0, 0);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// ADAPTIVE STEPPING
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Calculate adaptive step size based on velocity
* Fast movement = smaller steps (more precise)
* Slow movement = larger steps (more performant)
*
* @param Velocity - Current movement velocity
* @param DeltaTime - Frame delta time
* @param Config - Movement configuration with min/max step sizes
* @returns Step size in cm, clamped between min and max
*
* @example
* const stepSize = CollisionResolver.CalculateStepSize(
* new Vector(1000, 0, 0), // Fast movement
* 0.016,
* config
* );
* // Returns small step size for precise collision detection
*
* @pure true
* @category Adaptive Stepping
*/
public CalculateStepSize(
Velocity: Vector = new Vector(0, 0, 0),
DeltaTime: Float = 0,
Config: DA_MovementConfig = new DA_MovementConfig()
): Float {
// Calculate distance traveled this frame
const frameDistance =
MathLibrary.VectorLength(
new Vector(Velocity.X, Velocity.Y, 0) // Horizontal distance only
) * DeltaTime;
// If moving very slowly, use max step size
if (frameDistance < Config.MinStepSize) {
return Config.MaxStepSize;
}
// Clamp between min and max
return MathLibrary.ClampFloat(
// Calculate adaptive step size (half of frame distance)
// This ensures at least 2 checks per frame
frameDistance * 0.5,
Config.MinStepSize,
Config.MaxStepSize
);
}
}
export const BFL_CollisionResolver = new BFL_CollisionResolverClass();

BIN
Content/Movement/Collision/BFL_CollisionResolver.uasset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,221 @@
// 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();

BIN
Content/Movement/Collision/BFL_GroundProbe.uasset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,12 @@
// Movement/Collision/S_SweepResult.ts
import type { HitResult } from '#root/UE/HitResult.ts';
import type { Integer } from '#root/UE/Integer.ts';
import type { Vector } from '#root/UE/Vector.ts';
export interface S_SweepResult {
Location: Vector;
Hit: HitResult;
Blocked: boolean;
CollisionCount: Integer;
}

BIN
Content/Movement/Collision/S_SweepResult.uasset (Stored with Git LFS) Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -0,0 +1,261 @@
// Movement/Core/BFL_MovementProcessor.ts
import { BFL_CollisionResolver } from '#root/Movement/Collision/BFL_CollisionResolver.ts';
import { BFL_GroundProbe } from '#root/Movement/Collision/BFL_GroundProbe.ts';
import { E_MovementState } from '#root/Movement/Core/E_MovementState.ts';
import type { S_MovementInput } from '#root/Movement/Core/S_MovementInput.ts';
import type { S_MovementState } from '#root/Movement/Core/S_MovementState.ts';
import { BFL_Kinematics } from '#root/Movement/Physics/BFL_Kinematics.ts';
import { BFL_RotationController } from '#root/Movement/Rotation/BFL_RotationController.ts';
import { BFL_MovementStateMachine } from '#root/Movement/State/BFL_MovementStateMachine.ts';
import { BFL_SurfaceClassifier } from '#root/Movement/Surface/BFL_SurfaceClassifier.ts';
import { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
import { BlueprintFunctionLibrary } from '#root/UE/BlueprintFunctionLibrary.ts';
import { HitResult } from '#root/UE/HitResult.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts';
import type { Rotator } from '#root/UE/Rotator.ts';
import { Vector } from '#root/UE/Vector.ts';
/**
* Movement Processor
*
* Unified movement processing system
* Takes current state + input, returns next state
* Pure functional approach - no side effects
*
* @category Movement Processing
* @impure Only collision traces (GroundProbe, CollisionResolver)
*/
class BFL_MovementProcessorClass extends BlueprintFunctionLibrary {
// ════════════════════════════════════════════════════════════════════════════════════════
// MAIN PROCESSING
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Process movement for one frame
*
* Main entry point - computes complete next state from current state + input
* Orchestrates all movement subsystems in correct order
*
* @param CurrentState - Current movement state
* @param Input - Movement input data (input vector, delta time, config, etc.)
* @param IsShowVisualDebug - Whether to show debug traces in the world
* @returns New movement state after processing
*
* @example
* const newState = BFL_MovementProcessor.ProcessMovement(
* this.CurrentMovementState,
* {
* InputVector: inputVector,
* DeltaTime: deltaTime,
* CapsuleComponent: this.CapsuleComponent,
* Config: this.Config,
* AngleThresholdsRads: this.AngleThresholdsRads
* }
* );
*
* // Apply results
* this.GetOwner().SetActorLocation(newState.Location);
* this.GetOwner().SetActorRotation(newState.Rotation);
*
* @impure true - performs collision traces
* @category Main Processing
*/
public ProcessMovement(
CurrentState: S_MovementState,
Input: S_MovementInput,
IsShowVisualDebug: boolean = false
): S_MovementState {
// ═══════════════════════════════════════════════════════════════════
// PHASE 1: INPUT & ROTATION
// ═══════════════════════════════════════════════════════════════════
const inputMagnitude = MathLibrary.VectorLength(Input.InputVector);
const rotationResult = BFL_RotationController.UpdateRotation(
CurrentState.Rotation,
Input.InputVector,
Input.Config,
Input.DeltaTime,
CurrentState.Speed
);
// ═══════════════════════════════════════════════════════════════════
// PHASE 2: GROUND DETECTION
// ═══════════════════════════════════════════════════════════════════
const groundHit = BFL_GroundProbe.CheckGround(
CurrentState.Location,
Input.CapsuleComponent,
Input.AngleThresholdsRads,
Input.Config
);
const isGrounded = groundHit.BlockingHit;
const surfaceType = BFL_GroundProbe.GetSurfaceType(
groundHit,
Input.AngleThresholdsRads
);
// ═══════════════════════════════════════════════════════════════════
// PHASE 3: PHYSICS CALCULATION
// ═══════════════════════════════════════════════════════════════════
let newVelocity = CurrentState.Velocity;
// Ground movement or air friction
if (BFL_SurfaceClassifier.IsWalkable(surfaceType) && isGrounded) {
newVelocity = BFL_Kinematics.CalculateGroundVelocity(
newVelocity,
Input.InputVector,
Input.DeltaTime,
Input.Config
);
} else {
newVelocity = BFL_Kinematics.CalculateFriction(
newVelocity,
Input.DeltaTime,
Input.Config
);
}
// Apply gravity
newVelocity = BFL_Kinematics.CalculateGravity(
newVelocity,
isGrounded,
Input.Config
);
const newSpeed = BFL_Kinematics.GetHorizontalSpeed(newVelocity);
// ═══════════════════════════════════════════════════════════════════
// PHASE 4: MOVEMENT APPLICATION (Sweep)
// ═══════════════════════════════════════════════════════════════════
const desiredDelta = new Vector(
newVelocity.X * Input.DeltaTime,
newVelocity.Y * Input.DeltaTime,
newVelocity.Z * Input.DeltaTime
);
const sweepResult = BFL_CollisionResolver.PerformSweep(
CurrentState.Location,
desiredDelta,
Input.CapsuleComponent,
Input.Config,
Input.DeltaTime,
IsShowVisualDebug
);
let finalLocation = sweepResult.Location;
// Handle collision sliding
if (sweepResult.Blocked) {
const slideVector = BFL_CollisionResolver.CalculateSlideVector(
sweepResult,
desiredDelta,
CurrentState.Location
);
if (
MathLibrary.VectorLength(slideVector) > 0.5 &&
MathLibrary.Dot(
MathLibrary.Normal(slideVector),
sweepResult.Hit.ImpactNormal
) >= -0.1
) {
finalLocation = new Vector(
sweepResult.Location.X + slideVector.X,
sweepResult.Location.Y + slideVector.Y,
sweepResult.Location.Z + slideVector.Z
);
}
}
// ═══════════════════════════════════════════════════════════════════
// PHASE 5: GROUND SNAPPING
// ═══════════════════════════════════════════════════════════════════
if (
BFL_GroundProbe.ShouldSnapToGround(newVelocity.Z, groundHit, isGrounded)
) {
finalLocation = BFL_GroundProbe.CalculateSnapLocation(
finalLocation,
groundHit,
Input.CapsuleComponent,
Input.Config.GroundTraceDistance
);
}
// ═══════════════════════════════════════════════════════════════════
// PHASE 6: STATE DETERMINATION
// ═══════════════════════════════════════════════════════════════════
const movementState = BFL_MovementStateMachine.DetermineState({
IsGrounded: isGrounded,
SurfaceType: surfaceType,
InputMagnitude: inputMagnitude,
CurrentSpeed: newSpeed,
VerticalVelocity: newVelocity.Z,
IsBlocked: sweepResult.Blocked,
});
// ═══════════════════════════════════════════════════════════════════
// RETURN NEW STATE
// ═══════════════════════════════════════════════════════════════════
return {
Location: finalLocation,
Rotation: rotationResult.Rotation,
Velocity: newVelocity,
Speed: newSpeed,
IsGrounded: isGrounded,
GroundHit: groundHit,
SurfaceType: surfaceType,
IsBlocked: sweepResult.Blocked,
CollisionCount: sweepResult.CollisionCount,
IsRotating: rotationResult.IsRotating,
RotationDelta: rotationResult.RemainingDelta,
MovementState: movementState,
InputMagnitude: inputMagnitude,
};
}
// ════════════════════════════════════════════════════════════════════════════════════════
// STATE UTILITIES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Create initial movement state
*
* @param Location - Starting location
* @param Rotation - Starting rotation
* @returns Initial movement state with defaults
*
* @pure true
* @category State Utilities
*/
public CreateInitialState(
Location: Vector,
Rotation: Rotator
): S_MovementState {
return {
Location,
Rotation,
Velocity: new Vector(0, 0, 0),
Speed: 0.0,
IsGrounded: false,
GroundHit: new HitResult(),
SurfaceType: E_SurfaceType.None,
IsBlocked: false,
CollisionCount: 0,
IsRotating: false,
RotationDelta: 0.0,
MovementState: E_MovementState.Idle,
InputMagnitude: 0.0,
};
}
}
export const BFL_MovementProcessor = new BFL_MovementProcessorClass();

BIN
Content/Movement/Core/BFL_MovementProcessor.uasset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,149 @@
// Movement/Core/DA_MovementConfig.ts
import { S_AngleThresholds } from '#root/Movement/Surface/S_AngleThresholds.ts';
import type { Float } from '#root/UE/Float.ts';
import { PrimaryDataAsset } from '#root/UE/PrimaryDataAsset.ts';
export class DA_MovementConfig extends PrimaryDataAsset {
// ════════════════════════════════════════════════════════════════════════════════════════
// MOVEMENT PHYSICS
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Maximum horizontal movement speed in UE units per second
* Character cannot exceed this speed through ground movement
* Used as target velocity cap in ProcessGroundMovement
*
* @category Movement Physics
* @instanceEditable true
* @unit cm/s
*/
public readonly MaxSpeed: Float = 800.0;
/**
* Speed of velocity interpolation towards target velocity
* Higher values = faster acceleration, more responsive feel
* Used with VInterpTo for smooth acceleration curves
* Value represents interpolation speed, not actual acceleration rate
*
* @category Movement Physics
* @instanceEditable true
*/
public readonly Acceleration: Float = 10.0;
/**
* Speed of velocity interpolation towards zero when no input
* Higher values = faster stopping, less sliding
* Used with VInterpTo for smooth deceleration curves
* Should typically be <= Acceleration for natural feel
*
* @category Movement Physics
* @instanceEditable true
*/
public readonly Friction: Float = 8.0;
/**
* Gravitational acceleration in UE units per second squared
* Applied to vertical velocity when character is airborne
* Standard Earth gravity 980 cm/s² in UE units
* Only affects Z-axis velocity, horizontal movement unaffected
*
* @category Movement Physics
* @instanceEditable true
* @unit cm/s^2
*/
public readonly Gravity: Float = 980.0;
// ════════════════════════════════════════════════════════════════════════════════════════
// SURFACE DETECTION
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Surface classification angle thresholds in degrees
* Walkable 50°, SteepSlope 85°, Wall 95°, Ceiling >95°
*
* @category Surface Detection
* @instanceEditable true
*/
public readonly AngleThresholdsDegrees: S_AngleThresholds = {
Walkable: 50,
SteepSlope: 85,
Wall: 95,
};
// ════════════════════════════════════════════════════════════════════════════════════════
// COLLISION SETTINGS
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Distance to trace downward for ground detection
* Should be slightly larger than capsule half-height
*
* @category Collision Settings
* @instanceEditable true
* @unit cm
*/
public readonly GroundTraceDistance: Float = 50.0;
/**
* Minimum step size for collision sweeps
* Smaller values = more precise but more expensive
*
* @category Collision Settings
* @instanceEditable true
* @unit cm
*/
public readonly MinStepSize: Float = 1.0;
/**
* Maximum step size for collision sweeps
* Larger values = less precise but cheaper
*
* @category Collision Settings
* @instanceEditable true
* @unit cm
*/
public readonly MaxStepSize: Float = 50.0;
/**
* Maximum collision checks allowed per frame
* Prevents infinite loops in complex geometry
*
* @category Collision Settings
* @instanceEditable true
*/
public readonly MaxCollisionChecks: Float = 25;
// ════════════════════════════════════════════════════════════════════════════════════════
// CHARACTER ROTATION
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Character rotation speed (degrees per second)
* How fast character turns toward movement direction
*
* @category Character Rotation
* @instanceEditable true
* @unit deg/s
*/
public RotationSpeed: Float = 360.0;
/**
* Minimum movement speed required to rotate character
* Prevents rotation jitter when nearly stationary
*
* @category Character Rotation
* @instanceEditable true
* @unit cm/s
*/
public MinSpeedForRotation: Float = 50.0;
/**
* Enable/disable character rotation toward movement
* Useful for debugging or special movement modes
*
* @category Character Rotation
* @instanceEditable true
*/
public ShouldRotateToMovement: boolean = true;
}

BIN
Content/Movement/Core/DA_MovementConfig.uasset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,41 @@
// Movement/Core/DA_MovementConfigDefault.ts
import { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts';
export class DA_MovementConfigDefault extends DA_MovementConfig {
// ════════════════════════════════════════════════════════════════════════════════════════
// MOVEMENT PHYSICS
// ════════════════════════════════════════════════════════════════════════════════════════
override MaxSpeed = 800.0;
override Acceleration = 10.0;
override Friction = 8.0;
override Gravity = 980.0;
// ════════════════════════════════════════════════════════════════════════════════════════
// SURFACE DETECTION
// ════════════════════════════════════════════════════════════════════════════════════════
override AngleThresholdsDegrees = {
Walkable: 50.0,
SteepSlope: 85.0,
Wall: 95.0,
};
// ════════════════════════════════════════════════════════════════════════════════════════
// COLLISION SETTINGS
// ════════════════════════════════════════════════════════════════════════════════════════
override GroundTraceDistance = 50.0;
override MinStepSize = 1.0;
override MaxStepSize = 50.0;
override MaxCollisionChecks = 25;
// ════════════════════════════════════════════════════════════════════════════════════════
// CHARACTER ROTATION
// ════════════════════════════════════════════════════════════════════════════════════════
override RotationSpeed = 360.0;
override MinSpeedForRotation = 50.0;
override ShouldRotateToMovement = true;
}

BIN
Content/Movement/Core/DA_MovementConfigDefault.uasset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,39 @@
// Movement/Core/E_MovementState.ts
/**
* Movement state enumeration
* Defines all possible character movement states
*
* @category Movement Enums
*/
export enum E_MovementState {
/**
* Character is stationary on ground
* No input, no movement
*/
Idle = 'Idle',
/**
* Character is moving on ground
* Has input and horizontal velocity
*/
Walking = 'Walking',
/**
* Character is in the air
* Not touching ground, affected by gravity
*/
Airborne = 'Airborne',
/**
* Character is sliding down steep slope
* On non-walkable surface (steep slope)
*/
Sliding = 'Sliding',
/**
* Character is blocked by collision
* Hitting wall or ceiling
*/
Blocked = 'Blocked',
}

BIN
Content/Movement/Core/E_MovementState.uasset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,40 @@
// Movement/Core/S_MovementInput.ts
import type { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts';
import type { S_AngleThresholds } from '#root/Movement/Surface/S_AngleThresholds.ts';
import type { CapsuleComponent } from '#root/UE/CapsuleComponent.ts';
import type { Float } from '#root/UE/Float.ts';
import type { Vector } from '#root/UE/Vector.ts';
/**
* Movement processing input data
* All data needed to compute next movement state
*
* @category Movement Input
*/
export interface S_MovementInput {
/**
* Player input vector (normalized XY direction)
*/
InputVector: Vector;
/**
* Frame delta time (seconds)
*/
DeltaTime: Float;
/**
* Character capsule component for collision
*/
CapsuleComponent: CapsuleComponent | null;
/**
* Movement configuration
*/
Config: DA_MovementConfig;
/**
* Angle thresholds in radians (for surface classification)
*/
AngleThresholdsRads: S_AngleThresholds;
}

BIN
Content/Movement/Core/S_MovementInput.uasset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,105 @@
// Movement/Core/S_MovementState.ts
import type { E_MovementState } from '#root/Movement/Core/E_MovementState.ts';
import type { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
import type { Float } from '#root/UE/Float.ts';
import type { HitResult } from '#root/UE/HitResult.ts';
import type { Rotator } from '#root/UE/Rotator.ts';
import type { Vector } from '#root/UE/Vector.ts';
/**
* Complete movement state snapshot
* Immutable data structure representing full character movement state
*
* @category Movement State
*/
export interface S_MovementState {
// ═══════════════════════════════════════════════════════════════════
// TRANSFORM
// ═══════════════════════════════════════════════════════════════════
/**
* Character world location
*/
Location: Vector;
/**
* Character rotation (yaw only)
*/
Rotation: Rotator;
// ═══════════════════════════════════════════════════════════════════
// VELOCITY & PHYSICS
// ═══════════════════════════════════════════════════════════════════
/**
* Current velocity vector (cm/s)
*/
Velocity: Vector;
/**
* Horizontal speed (cm/s)
*/
Speed: Float;
// ═══════════════════════════════════════════════════════════════════
// GROUND STATE
// ═══════════════════════════════════════════════════════════════════
/**
* Whether character is on walkable ground
*/
IsGrounded: boolean;
/**
* Ground trace hit result
*/
GroundHit: HitResult;
/**
* Current surface type
*/
SurfaceType: E_SurfaceType;
// ═══════════════════════════════════════════════════════════════════
// COLLISION STATE
// ═══════════════════════════════════════════════════════════════════
/**
* Whether movement was blocked by collision
*/
IsBlocked: boolean;
/**
* Number of collision checks this frame
*/
CollisionCount: number;
// ═══════════════════════════════════════════════════════════════════
// ROTATION STATE
// ═══════════════════════════════════════════════════════════════════
/**
* Whether character is actively rotating
*/
IsRotating: boolean;
/**
* Remaining angular distance to target (degrees)
*/
RotationDelta: Float;
// ═══════════════════════════════════════════════════════════════════
// MOVEMENT STATE
// ═══════════════════════════════════════════════════════════════════
/**
* Current movement state (Idle, Walking, Airborne, etc.)
*/
MovementState: E_MovementState;
/**
* Input magnitude (0-1)
*/
InputMagnitude: Float;
}

BIN
Content/Movement/Core/S_MovementState.uasset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,7 +0,0 @@
// Movement/Enums/E_MovementState.ts
export enum E_MovementState {
Idle = 'Idle',
Walking = 'Walking',
Airborne = 'Airborne',
}

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,179 @@
// Movement/Physics/BFL_Kinematics.ts
import type { DA_MovementConfig } from '#root/Movement/Core/DA_MovementConfig.ts';
import { BlueprintFunctionLibrary } from '#root/UE/BlueprintFunctionLibrary.ts';
import type { Float } from '#root/UE/Float.ts';
import { MathLibrary } from '#root/UE/MathLibrary.ts';
import { Vector } from '#root/UE/Vector.ts';
class BFL_KinematicsClass extends BlueprintFunctionLibrary {
// ════════════════════════════════════════════════════════════════════════════════════════
// GROUND MOVEMENT
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Calculate new velocity for ground-based movement with acceleration
* Uses VInterpTo for smooth acceleration towards target velocity
* Only affects horizontal (XY) components, preserves vertical (Z)
*
* @param CurrentVelocity - Current character velocity (cm/s)
* @param InputVector - Normalized input direction from player/AI
* @param DeltaTime - Frame delta time for frame-rate independence (s)
* @param Config - Movement configuration with MaxSpeed and Acceleration
* @returns New velocity vector with updated horizontal components
*
* @example
* // Character moving forward with input (1, 0, 0)
* const newVel = Kinematics.CalculateGroundVelocity(
* new Vector(400, 0, 0), // Current velocity
* new Vector(1, 0, 0), // Forward input
* 0.016, // 60 FPS delta
* config
* );
* // Returns: Vector(450, 0, 0) - accelerated towards MaxSpeed
*
* @pure true
* @category Ground Movement
*/
public CalculateGroundVelocity(
CurrentVelocity: Vector,
InputVector: Vector,
DeltaTime: Float,
Config: DA_MovementConfig
): Vector {
if (MathLibrary.VectorLength(InputVector) > 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
);
return MathLibrary.VInterpTo(
new Vector(CurrentVelocity.X, CurrentVelocity.Y, 0),
CalculateTargetVelocity(InputVector, Config.MaxSpeed),
DeltaTime,
Config.Acceleration
);
} else {
return this.CalculateFriction(CurrentVelocity, DeltaTime, Config);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// FRICTION
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Apply friction to horizontal velocity (deceleration when no input)
* Smoothly interpolates velocity towards zero using friction rate
* Only affects horizontal (XY) components, preserves vertical (Z)
*
* @param CurrentVelocity - Current character velocity (cm/s)
* @param DeltaTime - Frame delta time (s)
* @param Config - Movement configuration with Friction rate
* @returns New velocity vector with friction applied to horizontal components
*
* @example
* // Character sliding to stop after input released
* const newVel = Kinematics.ApplyFriction(
* new Vector(500, 0, 0), // Moving forward
* 0.016, // 60 FPS delta
* config // Friction = 8.0
* );
* // Returns: Vector(450, 0, 0) - smoothly decelerating
*
* @pure true
* @category Friction
*/
public CalculateFriction(
CurrentVelocity: Vector,
DeltaTime: Float,
Config: DA_MovementConfig
): Vector {
return MathLibrary.VInterpTo(
new Vector(CurrentVelocity.X, CurrentVelocity.Y, 0),
new Vector(0, 0, CurrentVelocity.Z),
DeltaTime,
Config.Friction
);
}
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAVITY
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Apply gravity to vertical velocity when airborne
* Only affects Z component, horizontal velocity unchanged
* Gravity is NOT applied when grounded (Z velocity set to 0)
*
* @param CurrentVelocity - Current character velocity (cm/s)
* @param IsGrounded - Whether character is on walkable surface
* @param Config - Movement configuration with Gravity force
* @returns New velocity vector with gravity applied to vertical component
*
* @example
* // Character falling (not grounded)
* const newVel = Kinematics.ApplyGravity(
* new Vector(500, 0, -200), // Moving forward and falling
* false, // Not grounded
* config // Gravity = 980 cm/s²
* );
* // Returns: Vector(500, 0, -216.8) - falling faster
*
* @example
* // Character on ground
* const newVel = Kinematics.ApplyGravity(
* new Vector(500, 0, -10), // Small downward velocity
* true, // Grounded
* config
* );
* // Returns: Vector(500, 0, 0) - vertical velocity zeroed
*
* @pure true
* @category Gravity
*/
public CalculateGravity(
CurrentVelocity: Vector,
IsGrounded: boolean,
Config: DA_MovementConfig
): Vector {
if (!IsGrounded) {
return new Vector(
CurrentVelocity.X,
CurrentVelocity.Y,
CurrentVelocity.Z - Config.Gravity
);
} else {
return new Vector(CurrentVelocity.X, CurrentVelocity.Y, 0);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VELOCITY QUERIES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Get horizontal speed (magnitude of XY velocity)
* Ignores vertical component, useful for animation and debug display
*
* @param Velocity - Velocity vector to measure
* @returns Speed in cm/s (horizontal plane only)
*
* @example
* const speed = Kinematics.GetHorizontalSpeed(new Vector(300, 400, -100));
* // Returns: 500.0 (sqrt(300² + 400²))
*
* @pure true
* @category Velocity Queries
*/
public GetHorizontalSpeed(Velocity: Vector): Float {
return MathLibrary.VectorLength(new Vector(Velocity.X, Velocity.Y, 0));
}
}
export const BFL_Kinematics = new BFL_KinematicsClass();

BIN
Content/Movement/Physics/BFL_Kinematics.uasset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,256 @@
// 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();

BIN
Content/Movement/Rotation/BFL_RotationController.uasset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,29 @@
// Movement/Rotation/S_RotationResult.ts
import type { Float } from '#root/UE/Float.ts';
import type { Rotator } from '#root/UE/Rotator.ts';
/**
* Rotation result data
* Contains updated rotation and metadata about rotation state
*
* @category Movement Rotation
*/
export interface S_RotationResult {
/**
* New rotation after interpolation
*/
Rotation: Rotator;
/**
* Whether character is actively rotating
* False if rotation is complete or speed too low
*/
IsRotating: boolean;
/**
* Angular distance remaining to target (degrees)
* Used for animations and debug
*/
RemainingDelta: Float;
}

BIN
Content/Movement/Rotation/S_RotationResult.uasset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,105 @@
// Movement/State/BFL_MovementStateMachine.ts
import { E_MovementState } from '#root/Movement/Core/E_MovementState.ts';
import type { S_MovementContext } from '#root/Movement/State/S_MovementContext.ts';
import { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
/**
* Movement State Machine
*
* Pure functional FSM for determining movement state
* Takes movement context and returns appropriate state
* No side effects - completely deterministic
*
* @category Movement State
* @pure All methods are pure functions
*/
class BFL_MovementStateMachineClass {
// ════════════════════════════════════════════════════════════════════════════════════════
// STATE DETERMINATION
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Determine movement state based on current Context
* Main entry point for state machine logic
*
* @param Context - Current movement context
* @returns Appropriate movement state
*
* @example
* const state = MovementStateMachine.DetermineState({
* IsGrounded: true,
* SurfaceType: E_SurfaceType.Walkable,
* InputMagnitude: 0.8,
* CurrentSpeed: 500,
* VerticalVelocity: 0,
* IsBlocked: false
* });
* // Returns: E_MovementState.Walking
*
* @pure true
* @category State Determination
*/
public DetermineState(Context: S_MovementContext): E_MovementState {
// Priority 1: Check if grounded
if (Context.IsGrounded) {
// Priority 2: Check surface type
if (Context.SurfaceType === E_SurfaceType.SteepSlope) {
return E_MovementState.Sliding;
} else if (
Context.SurfaceType === E_SurfaceType.Wall ||
Context.SurfaceType === E_SurfaceType.Ceiling ||
// Priority 3: Check if blocked by collision
Context.IsBlocked
) {
return E_MovementState.Blocked;
} else {
// Priority 4: Determine ground state based on input
return this.DetermineGroundedState(Context);
}
} else {
return this.DetermineAirborneState(Context);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// STATE HELPERS
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Determine state when character is airborne
* Distinguishes between jumping, falling, etc.
*
* @param Context - Current movement context
* @returns Airborne-specific state
*
* @pure true
* @category State Helpers
*/
private DetermineAirborneState(Context: S_MovementContext): E_MovementState {
// Could extend this to differentiate Jump vs Fall
// For now, just return Airborne
return E_MovementState.Airborne;
}
/**
* Determine state when character is on ground
* Distinguishes between idle, walking, running, etc.
*
* @param Context - Current movement context
* @returns Grounded-specific state
*
* @pure true
* @category State Helpers
*/
private DetermineGroundedState(Context: S_MovementContext): E_MovementState {
// Check if player is providing input
if (Context.InputMagnitude > 0.01 && Context.CurrentSpeed > 1.0) {
return E_MovementState.Walking;
} else {
return E_MovementState.Idle;
}
}
}
export const BFL_MovementStateMachine = new BFL_MovementStateMachineClass();

BIN
Content/Movement/State/BFL_MovementStateMachine.uasset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,43 @@
// Movement/State/S_MovementContext.ts
import type { E_SurfaceType } from '#root/Movement/Surface/E_SurfaceType.ts';
import type { Float } from '#root/UE/Float.ts';
/**
* Movement context data for state determination
* Contains all information needed to determine movement state
*
* @category Movement State
*/
export interface S_MovementContext {
/**
* Whether character is on walkable ground
*/
IsGrounded: boolean;
/**
* Type of surface character is on
*/
SurfaceType: E_SurfaceType;
/**
* Magnitude of player input (0-1)
*/
InputMagnitude: Float;
/**
* Current horizontal movement speed (cm/s)
*/
CurrentSpeed: Float;
/**
* Current vertical velocity (cm/s)
* Positive = moving up, Negative = falling
*/
VerticalVelocity: Float;
/**
* Whether character is blocked by collision
*/
IsBlocked: boolean;
}

BIN
Content/Movement/State/S_MovementContext.uasset (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

View File

@ -1,10 +0,0 @@
// Movement/Structs/S_SurfaceTestCase.ts
import type { E_SurfaceType } from '#root/Movement/Enums/E_SurfaceType.ts';
import type { Float } from '#root/UE/Float.ts';
export interface S_SurfaceTestCase {
AngleDegrees: Float;
ExpectedType: E_SurfaceType;
Description: string;
}

Binary file not shown.

View File

@ -0,0 +1,123 @@
// Movement/Surface/BFL_SurfaceClassifier.ts
import { BFL_Vectors } from '#root/Math/Libraries/BFL_Vectors.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 { Vector } from '#root/UE/Vector.ts';
class BFL_SurfaceClassifierClass extends BlueprintFunctionLibrary {
// ════════════════════════════════════════════════════════════════════════════════════════
// CLASSIFICATION
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Classify surface type based on normal vector and angle thresholds
*
* @param SurfaceNormal - Normalized surface normal vector (from hit result)
* @param AngleThresholdsRads - Angle thresholds in radians (pre-converted for performance)
* @returns Surface type classification
*
* @example
* // Flat ground (normal pointing up)
* const flat = SurfaceClassifier.Classify(new Vector(0, 0, 1), thresholds);
* // Returns: E_SurfaceType.Walkable
*
* @example
* // Steep slope (50° angle)
* const steep = SurfaceClassifier.Classify(BFL_Vectors.GetNormalFromAngle(50), thresholds);
* // Returns: E_SurfaceType.SteepSlope
*
* @pure true
* @category Classification
*/
public Classify(
SurfaceNormal: Vector,
AngleThresholdsRads: S_AngleThresholds
): E_SurfaceType {
// Calculate angle between surface normal and up vector
const surfaceAngle = BFL_Vectors.GetSurfaceAngle(SurfaceNormal);
// Classify based on angle thresholds
if (surfaceAngle <= AngleThresholdsRads.Walkable) {
return E_SurfaceType.Walkable;
} else if (surfaceAngle <= AngleThresholdsRads.SteepSlope) {
return E_SurfaceType.SteepSlope;
} else if (surfaceAngle <= AngleThresholdsRads.Wall) {
return E_SurfaceType.Wall;
} else {
return E_SurfaceType.Ceiling;
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// TYPE CHECKS
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Check if surface allows normal walking movement
*
* @param surfaceType - Surface type to check
* @returns True if surface is walkable
*
* @pure true
* @category Type Checks
*/
public IsWalkable(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 Type Checks
*/
public IsSteep(surfaceType: E_SurfaceType): boolean {
return surfaceType === E_SurfaceType.SteepSlope;
}
/**
* Check if surface blocks movement (collision wall)
*
* @param surfaceType - Surface type to check
* @returns True if surface is a wall
*
* @pure true
* @category Type Checks
*/
public IsWall(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 Type Checks
*/
public IsCeiling(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 Type Checks
*/
public IsNone(surfaceType: E_SurfaceType): boolean {
return surfaceType === E_SurfaceType.None;
}
}
export const BFL_SurfaceClassifier = new BFL_SurfaceClassifierClass();

BIN
Content/Movement/Surface/BFL_SurfaceClassifier.uasset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,4 +1,4 @@
// Movement/Enums/E_SurfaceType.ts // Movement/Surface/E_SurfaceType.ts
export enum E_SurfaceType { export enum E_SurfaceType {
None = 'None', None = 'None',

BIN
Content/Movement/Surface/E_SurfaceType.uasset (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,4 +1,4 @@
// Movement/Structs/S_AngleThresholds.ts // Movement/Surface/S_AngleThresholds.ts
import type { Float } from '#root/UE/Float.ts'; import type { Float } from '#root/UE/Float.ts';

BIN
Content/Movement/Surface/S_AngleThresholds.uasset (Stored with Git LFS) Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1,83 +0,0 @@
// Movement/Tests/FT_BasicMovement.ts
import { AC_DebugHUD } from '#root/Debug/Components/AC_DebugHUD.ts';
import { AC_Movement } from '#root/Movement/Components/AC_Movement.ts';
import { E_MovementState } from '#root/Movement/Enums/E_MovementState.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
import { StringLibrary } from '#root/UE/StringLibrary.ts';
/**
* Functional Test: Basic Movement System
* Tests fundamental movement mechanics: acceleration, friction, max speed
* Validates movement state transitions and input processing
*/
export class FT_BasicMovement extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test execution - validates basic movement functionality
* Tests initialization, input processing, state management
*/
EventStartTest(): void {
// Initialize movement system
this.MovementComponent.InitializeMovementSystem(
null,
this.DebugHUDComponent
);
// Test 1: Initialization
if (this.MovementComponent.GetIsInitialized()) {
// Test 2: Initial state should be Idle
if (this.MovementComponent.GetMovementState() === E_MovementState.Idle) {
// Test 3: Initial speed & velocity is zero
if (
this.MovementComponent.GetCurrentSpeed() === 0 &&
this.MovementComponent.GetCurrentVelocity().X === 0 &&
this.MovementComponent.GetCurrentVelocity().Y === 0 &&
this.MovementComponent.GetCurrentVelocity().Z === 0
) {
this.FinishTest(EFunctionalTestResult.Succeeded);
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Current Speed & Current velocity should be zero, got Speed: ${this.MovementComponent.GetCurrentSpeed()}, Velocity: ${StringLibrary.ConvVectorToString(this.MovementComponent.GetCurrentVelocity())}`
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Initial movement state should be Idle, got ${this.MovementComponent.GetMovementState()}`
);
}
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
'Movement system failed to initialize'
);
}
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Movement system component - component under test
* @category Components
*/
private MovementComponent = new AC_Movement();
/**
* Debug HUD system - displays test status and parameters
* @category Components
*/
private DebugHUDComponent = new AC_DebugHUD();
}

Binary file not shown.

View File

@ -1,114 +0,0 @@
// Movement/Tests/FT_SurfaceClassification.ts
import { BFL_Vectors } from '#root/Math/Libraries/BFL_Vectors.ts';
import { AC_Movement } from '#root/Movement/Components/AC_Movement.ts';
import { E_SurfaceType } from '#root/Movement/Enums/E_SurfaceType.ts';
import type { S_SurfaceTestCase } from '#root/Movement/Structs/S_SurfaceTestCase.ts';
import { EFunctionalTestResult } from '#root/UE/EFunctionalTestResult.ts';
import { FunctionalTest } from '#root/UE/FunctionalTest.ts';
/**
* Functional Test: Surface Classification System
* Tests angle-based surface type detection across all boundary conditions
* Validates Walkable/SteepSlope/Wall/Ceiling classification accuracy
*/
export class FT_SurfaceClassification extends FunctionalTest {
// ════════════════════════════════════════════════════════════════════════════════════════
// GRAPHS
// ════════════════════════════════════════════════════════════════════════════════════════
// ────────────────────────────────────────────────────────────────────────────────────────
// EventGraph
// ────────────────────────────────────────────────────────────────────────────────────────
/**
* Test execution - validates surface classification for all test cases
* Tests boundary conditions and edge cases for each surface type
*/
EventStartTest(): void {
this.TestCases.forEach(
({ AngleDegrees, ExpectedType, Description }, arrayIndex) => {
const surfaceType = this.MovementComponent.ClassifySurface(
BFL_Vectors.GetNormalFromAngle(AngleDegrees)
);
if (surfaceType === ExpectedType) {
this.FinishTest(EFunctionalTestResult.Succeeded);
} else {
this.FinishTest(
EFunctionalTestResult.Failed,
`Movement Component test ${arrayIndex + 1} FAIL: ${Description} (${AngleDegrees}°) expected ${ExpectedType}, got ${surfaceType}`
);
}
}
);
}
// ════════════════════════════════════════════════════════════════════════════════════════
// VARIABLES
// ════════════════════════════════════════════════════════════════════════════════════════
/**
* Movement system component - provides surface classification functionality
* @category Components
*/
private MovementComponent = new AC_Movement();
/**
* Comprehensive test cases covering all surface type boundaries
* Tests edge cases and typical angles for each classification
* @category Test Data
*/
private TestCases: S_SurfaceTestCase[] = [
{
AngleDegrees: 0.0,
ExpectedType: E_SurfaceType.Walkable,
Description: 'Flat surface',
},
{
AngleDegrees: 25.0,
ExpectedType: E_SurfaceType.Walkable,
Description: 'Gentle slope',
},
{
AngleDegrees: 49.0,
ExpectedType: E_SurfaceType.Walkable,
Description: 'Max walkable',
},
{
AngleDegrees: 51.0,
ExpectedType: E_SurfaceType.SteepSlope,
Description: 'Steep slope',
},
{
AngleDegrees: 70.0,
ExpectedType: E_SurfaceType.SteepSlope,
Description: 'Very steep',
},
{
AngleDegrees: 84.0,
ExpectedType: E_SurfaceType.SteepSlope,
Description: 'Max steep',
},
{
AngleDegrees: 90.0,
ExpectedType: E_SurfaceType.Wall,
Description: 'Vertical wall',
},
{
AngleDegrees: 94.0,
ExpectedType: E_SurfaceType.Wall,
Description: 'Max wall',
},
{
AngleDegrees: 120.0,
ExpectedType: E_SurfaceType.Ceiling,
Description: 'Overhang',
},
{
AngleDegrees: 180.0,
ExpectedType: E_SurfaceType.Ceiling,
Description: 'Ceiling',
},
];
}

Binary file not shown.

View File

@ -10,6 +10,10 @@ export class Actor extends UEObject {
super(outer, name); super(outer, name);
} }
public GetActorLocation(): Vector {
return new Vector(); // Placeholder implementation
}
public SetActorLocation( public SetActorLocation(
NewLocation: Vector = new Vector(), NewLocation: Vector = new Vector(),
Sweep: boolean = false, Sweep: boolean = false,
@ -19,6 +23,10 @@ export class Actor extends UEObject {
// Implementation for setting actor location // Implementation for setting actor location
} }
public GetActorRotation(): Rotator {
return new Rotator(); // Placeholder implementation
}
public SetActorRotation( public SetActorRotation(
NewRotation: Rotator = new Rotator(), NewRotation: Rotator = new Rotator(),
TeleportPhysics: boolean = false TeleportPhysics: boolean = false
@ -26,8 +34,4 @@ export class Actor extends UEObject {
console.log(NewRotation, TeleportPhysics); console.log(NewRotation, TeleportPhysics);
// Implementation for setting actor rotation // Implementation for setting actor rotation
} }
public GetActorLocation(): Vector {
return new Vector(); // Placeholder implementation
}
} }

View File

@ -76,15 +76,25 @@ class MathLibraryClass extends BlueprintFunctionLibrary {
} }
/** /**
* Calculate arctangent2 of Y and X * Calculate arctangent2 of Y and X in radians
* @param Y - Y coordinate * @param Y - Y coordinate
* @param X - X coordinate * @param X - X coordinate
* @returns Angle in radians (-π to π) * @returns Angle in radians (-π to π)
*/ */
public Atan2(Y: Float, X: Float): Float { public Atan2Radians(Y: Float, X: Float): Float {
return Math.atan2(Y, X); return Math.atan2(Y, X);
} }
/**
* Calculate arctangent2 of Y and X in degrees
* @param Y - Y coordinate
* @param X - X coordinate
* @returns Angle in degrees (-180 to 180)
*/
public Atan2Degrees(Y: Float, X: Float): Float {
return this.RadiansToDegrees(Math.atan2(Y, X));
}
/** /**
* Calculate dot product of two vectors * Calculate dot product of two vectors
* @param Vector1 - First vector * @param Vector1 - First vector

View File

@ -0,0 +1,11 @@
// UE/PrimaryDataAsset.ts
import { DataAsset } from '#root/UE/DataAsset.ts';
import { Name } from '#root/UE/Name.ts';
import { UEObject } from '#root/UE/UEObject.ts';
export class PrimaryDataAsset extends DataAsset {
constructor(outer: UEObject | null = null, name: Name | string = Name.NONE) {
super(outer, name);
}
}