diff --git a/Assets/Scripts/ServerShared/Ship.cs b/Assets/Scripts/ServerShared/Ship.cs index 875dd81be..e48bffba9 100644 --- a/Assets/Scripts/ServerShared/Ship.cs +++ b/Assets/Scripts/ServerShared/Ship.cs @@ -278,12 +278,11 @@ public class Ship : Entity foreach (var thruster in _forwardThrusters) thruster.Axis += MovementDirection.y; foreach (var thruster in _reverseThrusters) thruster.Axis += -MovementDirection.y; - var turn = clamp(Turn, -1f, 1f); - foreach (var thruster in _clockwiseThrusters) thruster.Axis += turn; - foreach (var thruster in _counterClockwiseThrusters) thruster.Axis += -turn; + foreach (var thruster in _clockwiseThrusters) thruster.Axis += Turn; + foreach (var thruster in _counterClockwiseThrusters) thruster.Axis += -Turn; foreach (var drive in _aetherDrives) - drive.Axis = float3(MovementDirection.y, MovementDirection.x, turn); + drive.Axis = float3(MovementDirection.y, MovementDirection.x, Turn); } var velocityMagnitude = length(Velocity); diff --git a/tests/Aetheria.Shared.Tests/SteeringTests.cs b/tests/Aetheria.Shared.Tests/SteeringTests.cs index 17daa889d..b87e91f71 100644 --- a/tests/Aetheria.Shared.Tests/SteeringTests.cs +++ b/tests/Aetheria.Shared.Tests/SteeringTests.cs @@ -228,6 +228,11 @@ public sealed class SteeringTests using var cache = RestoredHullsTests.OpenCatalog(); var ship = RestoredHullsTests.BuildThrustedShip(cache, Hull); + // Either side of the 90 degree boundary: sqrt shaping at 85, full demand at 95. + Assert.Equal(sqrt(sin(radians(85f))), Steering.Toward(ship, Heading(ship, 85f)), 5); + Assert.Equal(-sqrt(sin(radians(85f))), Steering.Toward(ship, Heading(ship, -85f)), 5); + Assert.Equal(1f, Steering.Toward(ship, Heading(ship, 95f))); + Assert.Equal(-1f, Steering.Toward(ship, Heading(ship, -95f))); Assert.Equal(1f, Steering.Toward(ship, Heading(ship, 100f))); Assert.Equal(-1f, Steering.Toward(ship, Heading(ship, -100f))); Assert.Equal(1f, Steering.Toward(ship, Heading(ship, 135f))); @@ -235,10 +240,10 @@ public sealed class SteeringTests Assert.Equal(-sqrt(sin(radians(80f))), Steering.Toward(ship, Heading(ship, -80f)), 4); } - // A Turn of 3 rotates the hull exactly as a Turn of 1 does. Ship.Update clamps it, and every consumer (Thruster.Axis, - // AetherDrive.Axis) clamps again, so removing the Ship clamp alone changes nothing observable. + // The turn bound has one owner, the actuators: Ship.Update hands Turn to them unclamped, Thruster.Axis saturates to + // 0..1 and AetherDrive.Axis clamps to -1..1. A Turn of 3 therefore rotates the hull as a Turn of 1. [Fact] - public void TurnIsClampedToOne() + public void TheActuatorsBoundATurnBeyondOne() { using var cache = RestoredHullsTests.OpenCatalog(); var one = RestoredHullsTests.BuildThrustedShip(cache, Hull); @@ -262,6 +267,55 @@ public sealed class SteeringTests Assert.True(Angle(start, one.Direction) > radians(.02f), "the reference turn is real"); Assert.True(Angle(one.Direction, three.Direction) < radians(.001f), $"{Angle(one.Direction, three.Direction)}"); Assert.True(Angle(minusOne.Direction, minusThree.Direction) < radians(.001f)); + + // The bound itself, at its owners. + foreach (var thruster in one.Equipment.SelectMany(i => i.Behaviors).OfType()) + { + thruster.Axis = 3f; + Assert.Equal(1f, thruster.Axis); + thruster.Axis = -3f; + Assert.Equal(0f, thruster.Axis); + } + + // Axis is a pure property; no catalog hull carries a drive, so the instance is made without its item. + var drive = (AetherDrive)System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(typeof(AetherDrive)); + drive.Axis = float3(3f, -3f, 3f); + Assert.Equal(float3(1f, -1f, 1f), drive.Axis); + drive.Axis = float3(0f, 0f, -3f); + Assert.Equal(-1f, drive.Axis.z); + } + + // The size of a turn, computed from the hull and not read back from the code under test: each rotation thruster + // at Axis 1 turns the hull by Torque * Thrust * TorqueMultiplier / Mass * dt per tick (Thruster.Execute's law, + // summed here from the thrusters' own stats over the sets whose Torque clears the floor). + [Theory] + [InlineData(1f)] + [InlineData(-1f)] + [InlineData(.5f)] + [InlineData(-.5f)] + public void ATurnRotatesAtTheRateTheHullsTorqueGives(float turn) + { + using var cache = RestoredHullsTests.OpenCatalog(); + var ship = RestoredHullsTests.BuildThrustedShip(cache, Hull); + var settings = RestoredHullsTests.Settings(); + const float dt = 1f / 60f; + var thrusters = ship.Equipment.SelectMany(i => i.Behaviors).OfType().ToArray(); + var nose = normalize(ship.Direction); + var right = Right(ship); + + ship.Turn = turn; + ship.Update(dt); + + float Rate(Func set, float axis) => + thrusters.Where(set).Sum(t => axis * t.Torque * t.Thrust * settings.TorqueMultiplier / ship.Mass * dt); + var clockwise = Rate(t => t.Torque > settings.TorqueFloor, max(turn, 0f)); + var counter = Rate(t => t.Torque < -settings.TorqueFloor, max(-turn, 0f)); + var expected = clockwise + counter; + + var after = normalize(ship.Direction); + var turned = atan2(dot(after, right), dot(after, nose)); // positive is clockwise + Assert.True(abs(expected) > radians(.01f), $"the expected turn is real: {expected}"); + Assert.InRange(abs(turned / expected), .98f, 1.02f); } // A state that writes a constant turn, standing for any piloting state.