tengri/Content/Movement/Components/AC_Movement.ts

236 lines
8.3 KiB
TypeScript

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