using System.Numerics; using AcDream.App.Rendering; using Xunit; namespace AcDream.App.Tests.Rendering; /// /// Pure (no dat access) tests for /// — the port of gmCGAppearancePage::Rotate/DoRotation /// (0x0047CB50/0x0047CA80). /// public sealed class ChargenPreviewRotationControllerTests { /// /// CC6b-MOUNT: retail's OPERATIVE starting heading is 180, not the ctor's /// raw 0 — gmCGAppearancePage::gmCGAppearancePage @0x0047CDAC sets /// m_fCurHeading = 0f, but InitializePage @0x0047FDD0 always /// runs immediately afterward (before the page is ever visible) and /// overrides it to 180f at 0x00480235, pushed via /// SetPlayerHeading at 0x0048023F. No player-visible chargen /// Appearance frame is ever rendered at 0°. This is the seam a real mount /// site experiences (the parameterless constructor), pinned here so a /// future consumer can't silently regress to facing the character away /// from the camera. See /// for the full citation, including the cross-confirming /// gmCGSummaryPage/gmBarberUI sibling call sites. /// [Fact] public void DefaultConstructor_StartsAtRetailsOperative180DegreeHeading() { var controller = new ChargenPreviewRotationController(); Assert.Equal(180f, controller.HeadingDegrees); } [Fact] public void Toggle_StartsRotatingInTheGivenDirection() { var controller = new ChargenPreviewRotationController(0f); controller.Toggle(ChargenRotateDirection.Clockwise); Assert.True(controller.IsRotating); Assert.Equal(ChargenRotateDirection.Clockwise, controller.Direction); } [Fact] public void Toggle_SameDirectionWhileRotating_Stops() { var controller = new ChargenPreviewRotationController(0f); controller.Toggle(ChargenRotateDirection.Clockwise); controller.Toggle(ChargenRotateDirection.Clockwise); Assert.False(controller.IsRotating); } [Fact] public void Toggle_OppositeDirectionWhileRotating_SwitchesDirectionAndKeepsRotating() { var controller = new ChargenPreviewRotationController(0f); controller.Toggle(ChargenRotateDirection.Clockwise); controller.Toggle(ChargenRotateDirection.CounterClockwise); Assert.True(controller.IsRotating); Assert.Equal(ChargenRotateDirection.CounterClockwise, controller.Direction); } [Fact] public void Tick_WhileNotRotating_IsANoOp() { var controller = new ChargenPreviewRotationController(0f); controller.Tick(100.0); Assert.Equal(0f, controller.HeadingDegrees); } [Fact] public void Tick_FirstCallAfterToggle_ContributesZeroDelta() { // Rotate() invalidates m_dLastRotateTime so the very first DoRotation // tick resets it to "now" rather than computing a huge jump from a // stale/never-set timestamp. var controller = new ChargenPreviewRotationController(0f); controller.Toggle(ChargenRotateDirection.Clockwise); controller.Tick(1000.0); Assert.Equal(0f, controller.HeadingDegrees); } [Fact] public void Tick_ClockwiseAdvance_AddsTheExactPerTickFormula() { // deltaDegrees = ((now - last) / RotationSecondsPerRevolution) * 360. // Seed "now" nonzero (0.0 collides with the <= 0 reset-if-invalid // guard, same as retail's own sentinel check would if Timer::cur_time // could ever read exactly zero — never in practice, so tests avoid // it too). Explicit 0f baseline keeps the relative-delta assertion // below simple; the retail-default seam has its own dedicated test // above. var controller = new ChargenPreviewRotationController(0f); controller.Toggle(ChargenRotateDirection.Clockwise); controller.Tick(10.0); // seeds lastRotateTime = 10, zero delta. controller.Tick(11.5); // half a revolution at 3 s/rev. Assert.Equal(180f, controller.HeadingDegrees, 3); } [Fact] public void Tick_CounterClockwiseAdvance_SubtractsAndWrapsPositive() { var controller = new ChargenPreviewRotationController(0f); controller.Toggle(ChargenRotateDirection.CounterClockwise); controller.Tick(10.0); controller.Tick(11.5); // would go to -180, wraps to +180. Assert.Equal(180f, controller.HeadingDegrees, 3); } [Fact] public void Tick_AccumulatesAcrossMultipleTicks() { var controller = new ChargenPreviewRotationController(0f); controller.Toggle(ChargenRotateDirection.Clockwise); controller.Tick(10.0); controller.Tick(10.5); // +60 deg. controller.Tick(11.0); // +60 deg more. Assert.Equal(120f, controller.HeadingDegrees, 3); } /// /// F7: exercises the >360 -> -360 clamp arm (pseudo-C /// ~0x0047cb1e-0x0047cb31), the one with readable decomp polarity — /// unlike the CCW-branch FPU-stack artifact F3 documents, this branch's /// test/subtract shape is unambiguous. One large clockwise tick pushes /// heading past 360 in a single call. /// [Fact] public void Tick_ClockwiseAdvancePast360_ClampsBackBySubtracting360() { var controller = new ChargenPreviewRotationController(0f); controller.Toggle(ChargenRotateDirection.Clockwise); controller.Tick(10.0); // seeds lastRotateTime = 10, zero delta. controller.Tick(10.0 + 3.5); // 3.5s at 3s/rev = 420 deg -> 420, clamped to 60. Assert.Equal(60f, controller.HeadingDegrees, 3); } [Fact] public void ToOrientation_AtZeroHeading_IsIdentity() { var controller = new ChargenPreviewRotationController(0f); Quaternion orientation = controller.ToOrientation(); Assert.Equal(Quaternion.Identity.X, orientation.X, 4); Assert.Equal(Quaternion.Identity.Y, orientation.Y, 4); Assert.Equal(Quaternion.Identity.Z, orientation.Z, 4); Assert.Equal(Quaternion.Identity.W, orientation.W, 4); } }