diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs b/Assets/Scripts/ServerShared/DrawAhead.cs index 3cd1a4ea1..52c92fee8 100644 --- a/Assets/Scripts/ServerShared/DrawAhead.cs +++ b/Assets/Scripts/ServerShared/DrawAhead.cs @@ -14,13 +14,15 @@ public static class DrawAhead entity.Position + float3(entity.Velocity.x, 0, entity.Velocity.y) * lead; // The ship's rotation turned about its own up axis by the rate its last step turned, for lead seconds. - public static quaternion Rotation(Ship ship, float lead) + public static quaternion Rotation(Ship ship, float lead) => Turned(ship.Rotation, ship.TurnRate * lead); + + // q turned about its own up axis by the angle, in the sense Direction turns for a positive TurnRate. + public static quaternion Turned(quaternion q, float angle) { - var q = ship.Rotation; var u = float3(q.x, q.y, q.z); var up = float3(0, 1, 0); up += 2 * (q.w * cross(u, up) + cross(u, cross(u, up))); - var half = ship.TurnRate * lead * .5f; + var half = angle * .5f; var s = sin(half); var t = new quaternion(up.x * s, up.y * s, up.z * s, cos(half)); // Hamilton product t * q: CultMath carries no quaternion product (a gap in CultMath, not filled here). diff --git a/tests/Aetheria.Shared.Tests/SimClockTests.cs b/tests/Aetheria.Shared.Tests/SimClockTests.cs index 11cbc7a30..bc5bdd539 100644 --- a/tests/Aetheria.Shared.Tests/SimClockTests.cs +++ b/tests/Aetheria.Shared.Tests/SimClockTests.cs @@ -58,6 +58,11 @@ public sealed class SimClockTests Assert.InRange(elapsed, 10.0, 10.1); Assert.InRange(clock.Steps, Math.Round(elapsed * 60) - 1, Math.Round(elapsed * 60) + 1); + // A frame that pays for exactly one step runs it (binary-exact seconds, so nothing rounds). + var exact = new SimClock(.25f, 4); + Assert.Equal(1, exact.Advance(.25f, _ => { })); + Assert.Equal(0f, exact.Lead); + var slow = new SimClock(1f / 60f, 30); for (var frame = 0; frame < 600; frame++) slow.Advance(1f / 60, _ => { }); Assert.InRange(slow.Steps, 299, 301); @@ -270,4 +275,50 @@ public sealed partial class RunStartTests Assert.True(turn > .005f, "the step turns the ship enough to tell its sense"); Assert.True(error < .2f * turn, $"drawn one step ahead is where the sim puts the pose next (off by {error} of a {turn} turn)"); } + + // Thrusters turn the same way: a Longinus held in a turn, settled, turns each step by exactly its TurnRate. + [Fact] + public void ThrustersReportTheirTurn() + { + var (_, longinus, _) = EvFleet(); + EvSettle(longinus); + longinus.Turn = 1f; + var turned = 0; + for (var step = 0; step < 120; step++) + { + var before = longinus.Direction; + longinus.Update(ClockFrame); + Assert.InRange(abs(abs(SignedTurn(before, longinus.Direction)) - abs(longinus.TurnRate * ClockFrame)), 0f, 1e-6f); + if (abs(longinus.TurnRate) > 1e-4f) + { + turned++; + Assert.True(longinus.TurnRate > 0 == dot(longinus.Direction, before.Rotate(ItemRotation.Clockwise)) > 0); + } + } + Assert.True(turned > 30, "the thrusters turned the ship"); + } + + // The draw-ahead turn on a pose that is not flat: the up axis stays the pose's up, and the forward axis swings about + // it by the angle (Rodrigues' rotation, written out apart from DrawAhead's quaternion product). + [Fact] + public void TheDrawnTurnIsAboutThePosesOwnUp() + { + var up = normalize(float3(.3f, .8f, -.2f)); + var pose = quaternion.LookRotation(normalize(cross(float3(1, 0, .4f), up)), up); + const float angle = .6f; + var turned = DrawAhead.Turned(pose, angle); + + float3 Axis(quaternion q, float3 v) + { + var u = float3(q.x, q.y, q.z); + return v + 2 * (q.w * cross(u, v) + cross(u, cross(u, v))); + } + float3 Close(float3 a, float3 b) => float3(abs(a.x - b.x), abs(a.y - b.y), abs(a.z - b.z)); + var forward = Axis(pose, float3(0, 0, 1)); + var rodrigues = forward * cos(angle) + cross(Axis(pose, float3(0, 1, 0)), forward) * sin(angle); + var upBefore = Axis(pose, float3(0, 1, 0)); + var tolerance = float3(1e-5f, 1e-5f, 1e-5f); + Assert.True(all(Close(Axis(turned, float3(0, 1, 0)), upBefore) < tolerance), "the pose's up does not move"); + Assert.True(all(Close(Axis(turned, float3(0, 0, 1)), rodrigues) < tolerance), "the forward axis swings about it by the angle"); + } }