using Sandbox.Movement;
namespace SceneTests.Components;
///
/// Pins MoveModeLadder: it scores -100 until its fixed-update scan finds a touched
/// trigger tagged with a climbable tag, at which point its Priority outranks the
/// walk mode - the controller starts climbing (gravity off, velocity zeroed),
/// ladders below the feet are ignored, the climb rotation faces the ladder from
/// either side, and removing the ladder hands control back to walking. Climb
/// movement itself reads Input.AnalogMove, which is silent headless, so actual
/// up/down motion is out of scope.
///
[TestClass]
public class LadderMoveModeTest
{
///
/// Creates a large static floor whose top surface sits at z = 0.
///
static GameObject CreateFloor( Scene scene )
{
var go = scene.CreateObject();
go.Name = "Floor";
go.WorldPosition = new Vector3( 0, 0, -50 );
var box = go.Components.Create();
box.Scale = new Vector3( 4000, 4000, 100 );
box.Static = true;
return go;
}
///
/// Creates a PlayerController with a MoveModeLadder at the given position,
/// configured for headless ticking: no input controls and no footstep sounds.
///
static PlayerController CreatePlayer( Scene scene, Vector3 position, out MoveModeLadder ladderMode )
{
var go = scene.CreateObject();
go.Name = "Player";
go.WorldPosition = position;
var pc = go.Components.Create();
pc.UseInputControls = false;
pc.EnableFootstepSounds = false;
ladderMode = go.Components.Create();
return pc;
}
///
/// Creates a static trigger box tagged "ladder" - the shape MoveModeLadder's
/// scan looks for in the body's touching triggers.
///
static GameObject CreateLadderTrigger( Scene scene, Vector3 position, Vector3 scale )
{
var go = scene.CreateObject();
go.Name = "Ladder";
go.WorldPosition = position;
go.Tags.Add( "ladder" );
var box = go.Components.Create();
box.Scale = scale;
box.IsTrigger = true;
box.Static = true;
return go;
}
///
/// Ticks the scene until the condition holds, bounded so a regression fails
/// fast instead of hanging. The caller asserts the condition afterwards.
///
static void TickUntil( Scene scene, System.Func condition, int maxTicks )
{
for ( int i = 0; i < maxTicks && !condition(); i++ )
{
scene.GameTick();
}
}
///
/// Pins the defaults (priority 5, speed 1, the built-in "ladder" climbable tag)
/// and the Score contract: -100 without a climbing object, the configured
/// Priority with one.
///
[TestMethod]
public void DefaultsAndScore()
{
var scene = new Scene();
using var sceneScope = scene.Push();
var pc = CreatePlayer( scene, Vector3.Zero, out var ladderMode );
Assert.AreEqual( 5, ladderMode.Priority );
Assert.AreEqual( 1f, ladderMode.Speed );
Assert.IsTrue( ladderMode.ClimbableTags.Has( "ladder" ), "the ladder tag is climbable out of the box" );
Assert.IsNull( ladderMode.ClimbingObject );
Assert.AreEqual( -100, ladderMode.Score( pc ), "without a ladder the mode rules itself out" );
ladderMode.ClimbingObject = scene.CreateObject();
Assert.AreEqual( 5, ladderMode.Score( pc ), "with a ladder the mode scores its priority" );
ladderMode.Priority = 9;
Assert.AreEqual( 9, ladderMode.Score( pc ) );
}
///
/// A tall "ladder" trigger overlapping the player makes the scan latch onto it:
/// the ladder mode outranks walking, the controller reports climbing, the
/// body's gravity is turned off and the climb rotation faces the ladder.
///
[TestMethod]
public void TaggedTriggerActivatesClimbing()
{
var scene = new Scene();
using var sceneScope = scene.Push();
CreateFloor( scene );
var ladder = CreateLadderTrigger( scene, new Vector3( 16, 0, 128 ), new Vector3( 20, 20, 256 ) );
var pc = CreatePlayer( scene, new Vector3( 0, 0, 10 ), out var ladderMode );
TickUntil( scene, () => pc.IsClimbing, 60 );
Assert.IsTrue( pc.IsClimbing, "the player should have latched onto the ladder" );
Assert.AreEqual( ladderMode, pc.Mode, "the ladder mode outranks the walk mode" );
Assert.AreEqual( ladder, ladderMode.ClimbingObject );
Assert.IsFalse( pc.IsAirborne, "climbing is not airborne" );
// the ladder is ahead of the player (+x) so the climb rotation is the
// ladder's own rotation, not the flipped one
Assert.IsTrue( ladderMode.ClimbingRotation.Distance( Rotation.Identity ) < 5f,
$"facing the ladder front should keep its rotation: {ladderMode.ClimbingRotation}" );
// climbing turns gravity off and brakes hard - the player hangs there
scene.GameTick();
Assert.IsFalse( pc.Body.Gravity, "the ladder mode disables body gravity" );
var heightWhenClimbing = pc.WorldPosition.z;
for ( int i = 0; i < 10; i++ )
{
scene.GameTick();
}
Assert.AreEqual( heightWhenClimbing, pc.WorldPosition.z, 5f,
$"without input the climber should hang in place: {pc.WorldPosition}" );
}
///
/// Approaching the ladder from its back side flips the climb rotation 180
/// degrees of yaw, so the climber always faces the ladder.
///
[TestMethod]
public void ClimbRotationFlipsOnBackSide()
{
var scene = new Scene();
using var sceneScope = scene.Push();
CreateFloor( scene );
CreateLadderTrigger( scene, new Vector3( 16, 0, 128 ), new Vector3( 20, 20, 256 ) );
// the player stands at +x of the ladder, behind its forward direction
var pc = CreatePlayer( scene, new Vector3( 32, 0, 10 ), out var ladderMode );
TickUntil( scene, () => pc.IsClimbing, 60 );
Assert.IsTrue( pc.IsClimbing );
Assert.IsTrue( ladderMode.ClimbingRotation.Distance( Rotation.FromYaw( 180 ) ) < 5f,
$"approaching from behind should flip the climb rotation: {ladderMode.ClimbingRotation}" );
}
///
/// A climbable trigger below the player's midpoint is never latched onto - the
/// scan refuses ladders beneath the character so walking over a low ladder
/// trigger doesn't yank the player into climb mode.
///
[TestMethod]
public void LadderBelowFeetIsIgnored()
{
var scene = new Scene();
using var sceneScope = scene.Push();
CreateFloor( scene );
CreateLadderTrigger( scene, new Vector3( 12, 0, 10 ), new Vector3( 20, 20, 20 ) );
var pc = CreatePlayer( scene, new Vector3( 0, 0, 10 ), out var ladderMode );
for ( int i = 0; i < 30; i++ )
{
scene.GameTick();
Assert.IsFalse( pc.IsClimbing, "a ladder below the feet must never be climbed" );
}
Assert.IsNull( ladderMode.ClimbingObject );
Assert.IsTrue( pc.Mode is MoveModeWalk, $"walking should stay in control: {pc.Mode}" );
}
///
/// When the ladder trigger goes away the scan clears the climbing object, the
/// mode scores itself out, walking takes back over and the player falls back
/// onto the floor.
///
[TestMethod]
public void RemovingLadderStopsClimbing()
{
var scene = new Scene();
using var sceneScope = scene.Push();
CreateFloor( scene );
var ladder = CreateLadderTrigger( scene, new Vector3( 16, 0, 128 ), new Vector3( 20, 20, 256 ) );
var pc = CreatePlayer( scene, new Vector3( 0, 0, 10 ), out var ladderMode );
TickUntil( scene, () => pc.IsClimbing, 60 );
Assert.IsTrue( pc.IsClimbing );
ladder.Enabled = false;
TickUntil( scene, () => !pc.IsClimbing, 20 );
Assert.IsFalse( pc.IsClimbing, "disabling the ladder should end the climb" );
Assert.IsTrue( pc.Mode is MoveModeWalk, $"walking should take back over: {pc.Mode}" );
Assert.IsFalse( ladderMode.ClimbingObject.IsValid(), "the climbing object should be cleared" );
TickUntil( scene, () => pc.IsOnGround, 60 );
Assert.IsTrue( pc.IsOnGround, $"gravity should return and land the player: {pc.WorldPosition}" );
}
}