Turn bound has one owner, the actuators; pin the turn rate and the 90 degree boundary
Ship.Update no longer clamps Turn: Thruster.Axis saturates and AetherDrive.Axis clamps, which is where the bound is enforced. TurnIsClampedToOne becomes TheActuatorsBoundATurnBeyondOne and asserts the actuator bounds directly. The turn rate is pinned against a value computed from each thruster's Torque, Thrust, TorqueMultiplier and the hull's Mass; the behind-the-hull rule is probed at 85 and 95 degrees. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
97adf07aec
commit
b644fceb24
2 changed files with 60 additions and 7 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>())
|
||||
{
|
||||
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<Thruster>().ToArray();
|
||||
var nose = normalize(ship.Direction);
|
||||
var right = Right(ship);
|
||||
|
||||
ship.Turn = turn;
|
||||
ship.Update(dt);
|
||||
|
||||
float Rate(Func<Thruster, bool> 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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue