// Content/Movement/Components/AC_Movement.ts import type {S_MovementConstants} from "../Structs/S_MovementConstants.js"; import type {S_AngleThresholds} from "../Structs/S_AngleThresholds.js"; import type {Float, Vector} from "../../types.js"; import {acos, cos, D2R, Dot, Print, sin} from "../../functions.js"; import {E_SurfaceType} from "../Enums/E_SurfaceType.js"; import type {S_SurfaceTestCase} from "../Structs/S_SurfaceTestCase.js"; export class AC_Movement { // === Movement configuration exposed to designers === // Category: "Movement Config" // Instance editable: true public readonly MovementConstants: S_MovementConstants = { MaxSpeed: 600.0, Acceleration: 2000.0, Friction: 8.0, Gravity: 980.0, } // Category: "Movement Config" // Instance editable: true readonly AngleThresholdsDegrees: S_AngleThresholds = { Walkable: 50.0, // degrees SteepSlope: 85.0, // degrees Wall: 95.0, // degrees }; // ===================================================== // Category: "Debug" IsInitialized: boolean = false; // Category: "Debug" // Instance editable: true ShowDebugInfo: boolean = true; // === Runtime cached values for performance (radians) === // Category: "InternalCache" AngleThresholdsRads: S_AngleThresholds; // ===================================================== // Category: "Math" // Pure: true /** * Calculate angle between two normalized vectors * @param Vector1 - First normalized vector * @param Vector2 - Second normalized vector * @returns Angle between vectors in radians (0 to π) * @example * // 90° angle between X and Z axes * GetAngleBetweenVectors([1,0,0], [0,0,1]) // returns π/2 */ GetAngleBetweenVectors(Vector1: Vector, Vector2: Vector) { return acos(Dot(Vector1, Vector2)); } // Category: "Math" // Pure: true /** * Calculate angle between surface normal and up vector * @param SurfaceNormal - Normalized surface normal vector * @returns Angle from horizontal plane in radians (0 = flat, π/2 = vertical) * @example * // Flat surface * GetSurfaceAngle([0,0,1]) // returns 0 * // Vertical wall * GetSurfaceAngle([1,0,0]) // returns π/2 */ GetSurfaceAngle(SurfaceNormal: Vector): Float { return this.GetAngleBetweenVectors(SurfaceNormal, [0.0, 0.0, 1.0]); } // Category: "Math" // Pure: true /** * Generate surface normal vector from angle in degrees * @param AngleDegrees - Angle from horizontal in degrees (0-180) * @returns Normalized surface normal vector * @example * // Flat surface (0°) * GenerateNormalFromAngle(0) // returns [0,0,1] * // Vertical wall (90°) * GenerateNormalFromAngle(90) // returns [1,0,0] */ GenerateNormalFromAngle(AngleDegrees: Float): Vector { const AngleRads = D2R(AngleDegrees); return [sin(AngleRads), 0.0, cos(AngleRads)]; } // Category: "Surface Detection" // Pure: true /** * Classify surface type based on normal vector * @param SurfaceNormal - Normalized surface normal vector * @param AngleThresholds - Angle thresholds in radians * @returns Surface type classification * @example * // Classify flat ground * ClassifySurface([0,0,1], thresholds) // returns E_SurfaceType.Walkable */ ClassifySurface(SurfaceNormal: Vector, AngleThresholds: S_AngleThresholds): E_SurfaceType { const SurfaceAngle = this.GetSurfaceAngle(SurfaceNormal); if (SurfaceAngle <= AngleThresholds.Walkable) { return E_SurfaceType.Walkable; } else if (SurfaceAngle <= AngleThresholds.SteepSlope) { return E_SurfaceType.SteepSlope; } else if (SurfaceAngle <= AngleThresholds.Wall) { return E_SurfaceType.Wall; } else { return E_SurfaceType.Ceiling; } } // Category: "Surface Detection" // Pure: true /** * Check if surface allows normal walking movement * @param SurfaceType - Surface type to check * @returns True if surface is walkable */ IsSurfaceWalkable(SurfaceType: E_SurfaceType): boolean { return SurfaceType === E_SurfaceType.Walkable; } // Category: "Surface Detection" // Pure: true /** * Check if surface causes sliding behavior * @param SurfaceType - Surface type to check * @returns True if surface is steep slope */ IsSurfaceSteep(SurfaceType: E_SurfaceType): boolean { return SurfaceType === E_SurfaceType.SteepSlope; } // Category: "Surface Detection" // Pure: true /** * Check if surface blocks movement (collision) * @param SurfaceType - Surface type to check * @returns True if surface is a wall */ IsSurfaceWall(SurfaceType: E_SurfaceType): boolean { return SurfaceType === E_SurfaceType.Wall; } // Category: "Surface Detection" // Pure: true /** * Check if surface is overhead (ceiling) * @param SurfaceType - Surface type to check * @returns True if surface is ceiling */ IsSurfaceCeiling(SurfaceType: E_SurfaceType): boolean { return SurfaceType === E_SurfaceType.Ceiling; } // Category: "Surface Detection" // Pure: true /** * Check if no surface detected (airborne state) * @param SurfaceType - Surface type to check * @returns True if no surface contact */ IsSurfaceNone(SurfaceType: E_SurfaceType): boolean { return SurfaceType === E_SurfaceType.None; } // Category: "Debug" PrintDebugInfo() { Print('=== Movement System Initialized ==='); Print(`Walkable: ≤ ${this.AngleThresholdsDegrees.Walkable}°, Steep: ${this.AngleThresholdsDegrees.Walkable}°-${this.AngleThresholdsDegrees.SteepSlope}°, Wall: ${this.AngleThresholdsDegrees.SteepSlope}°-${this.AngleThresholdsDegrees.Wall}°, Ceiling: >${this.AngleThresholdsDegrees.Wall}°`); Print(`Max Speed: ${this.MovementConstants.MaxSpeed}, Acceleration: ${this.MovementConstants.Acceleration}, Friction: ${this.MovementConstants.Friction}, Gravity: ${this.MovementConstants.Gravity}`); Print('=================================='); } // Category: "Debug" /** * Run comprehensive surface classification test suite * @returns True if all tests pass */ TestSurfaceClassification() { let AllTestsPassed: boolean = true; const 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" } ]; for (let i = 0; i < TestCases.length; i++) { const testCase = TestCases[i]; const normal = this.GenerateNormalFromAngle(testCase.AngleDegrees); const result = this.ClassifySurface(normal, this.AngleThresholdsRads); if (result === testCase.ExpectedType) { Print(`✅ Test ${i+1} PASS: ${testCase.Description} (${testCase.AngleDegrees}°) = ${testCase.ExpectedType}`); } else { Print(`❌ Test ${i+1} FAIL: ${testCase.Description} (${testCase.AngleDegrees}°) expected ${testCase.ExpectedType}, got ${result}`); AllTestsPassed = false; } } return AllTestsPassed; } // Category: "System" /** * Initialize movement system with angle conversion and testing * Converts degree thresholds to radians for runtime performance * Runs automated tests if debug mode enabled */ InitializeMovementSystem() { this.IsInitialized = true; this.AngleThresholdsRads = { Walkable: D2R(this.AngleThresholdsDegrees.Walkable), SteepSlope: D2R(this.AngleThresholdsDegrees.SteepSlope), Wall: D2R(this.AngleThresholdsDegrees.Wall), }; if (this.ShowDebugInfo) { this.PrintDebugInfo(); this.TestSurfaceClassification(); } } }