XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;

namespace XFEEDToolBox.Test;

[TestFixture]
public class ScoopThermalTests
{
    private static readonly DateTimeOffset At=DateTimeOffset.Parse("2026-10-23T00:00:00Z");
    private static FlightFrame Frame(double heat,double fuel=4,double radius=150)=>new()
    {
        Sequence=1,ObservedAt=At,SystemAddress=1,CurrentStar=new(1,"G",Vector3d.Zero,100,115,200),Position=new(radius,0,0),
        HeatPercent=heat,FuelMain=fuel,FuelRequiredForJump=2,FuelNeededToNextRefuel=15.99,MinimumFuelToNextRefuel=5,
        ScoopActive=true,ScoopPowered=true,RefuellingToFull=true,PreferredOrbitRadius=145,SpeedMetresPerSecond=10
    };
    [Test] public void RisingHeatNearTheLimitOnlyRequestsASmallOrbitAdjustment()
    {
        var controller=new ScoopThermalController();controller.Observe(Frame(91));
        var adjustment=controller.Observe(Frame(92) with{ObservedAt=At.AddSeconds(.5),Sequence=2});
        Check.That(adjustment.MovingOutward&&adjustment.Interrupted);Check.That(adjustment.Radius>150&&adjustment.Radius<151.1);
        Check.That(!adjustment.CanSkipFullTank);
    }
    [Test] public void ARealFlatOrFallingTemperatureStopsOutwardTravelImmediately()
    {
        foreach(var heat in new[]{94d,93.9})
        {
            var controller=new ScoopThermalController();controller.Observe(Frame(94));
            var held=controller.Observe(Frame(heat,radius:150.4) with{ObservedAt=At.AddSeconds(.1),Sequence=2});
            Check.That(!held.MovingOutward);Check.Equal(150.4,held.Radius);
        }
    }
    [Test] public void InterruptedRefuellingCanDepartOnTheVerifiedRouteBudgetWithoutFillingTheTank()
    {
        var controller=new ScoopThermalController();var hot=controller.Observe(Frame(94,6));
        Check.That(hot.CanSkipFullTank);var cool=controller.Observe(Frame(84,6) with{ObservedAt=At.AddSeconds(.2),Sequence=2});
        Check.That(cool.CanSkipFullTank&&!cool.MovingOutward);Check.That(6<Frame(84).FuelNeededToNextRefuel);
        var insufficient=new ScoopThermalController().Observe(Frame(94,4.99));Check.That(!insufficient.CanSkipFullTank);
    }
    [Test] public void ANewStarDoesNotInheritThePreviousThermalInterruptionOrOrbitRadius()
    {
        var controller=new ScoopThermalController();controller.Observe(Frame(94,6));
        var next=controller.Observe(Frame(70,6) with{Sequence=2,ObservedAt=At.AddSeconds(2),SystemAddress=2,CurrentStar=new(2,"K",Vector3d.Zero,100,115,200)});
        Check.That(!next.Interrupted&&!next.CanSkipFullTank&&!next.MovingOutward);Check.Equal(145d,next.Radius);
    }
    [Test] public void RisingTemperatureDuringDepartureAlsoRequiresAnOutwardCorrection()
    {
        var controller=new ScoopThermalController();
        var frame=Frame(88,16) with{ScoopActive=false,RefuellingToFull=false,FuelNeededToNextRefuel=5};
        controller.Observe(frame);
        var hot=controller.Observe(frame with{Sequence=2,ObservedAt=At.AddSeconds(.25),HeatPercent=91});
        Check.That(hot.MovingOutward&&hot.Radius>150);
        // Even if the ship still drifts inward during the turn, a flat sample
        // must not reset the orbit back to the hotter, smaller radius.
        var flat=controller.Observe(frame with{Sequence=3,ObservedAt=At.AddSeconds(.5),HeatPercent=91,Position=new(149,0,0)});
        Check.That(!flat.MovingOutward&&flat.Radius>=150);
    }
    [Test] public void AnInwardSlowTurningShipStartsCoolingBeforeItsTurnWouldOverheat()
    {
        var controller=new ScoopThermalController();
        var frame=Frame(84,16) with{ScoopActive=false,RefuellingToFull=false,FuelNeededToNextRefuel=5,Forward=new(-.5,0,Math.Sqrt(.75)),AlignmentRateLowerDegreesPerSecond=3};
        controller.Observe(frame);
        var next=controller.Observe(frame with{ObservedAt=At.AddSeconds(1),Sequence=2,HeatPercent=85});
        Check.That(next.MovingOutward&&next.Radius>150);
    }
    [Test] public void StableTemperatureIgnoresAnOldLargeOrbitDemandThenAllowsRefuellingAfterCooling()
    {
        var controller=new ScoopThermalController();controller.Observe(Frame(94));
        var stable=controller.Observe(Frame(94,radius:150.4) with{ObservedAt=At.AddSeconds(.1),Sequence=2,PreferredOrbitRadius=180});
        Check.Equal(150.4,stable.Radius);Check.That(!stable.MovingOutward);
        var cool=controller.Observe(Frame(84,radius:150.4) with{ObservedAt=At.AddSeconds(.2),Sequence=3,PreferredOrbitRadius=145});
        Check.Equal(145d,cool.Radius);Check.That(!cool.CanSkipFullTank&&!cool.MovingOutward);
    }
    [Test] public void HotVisibleInwardDestinationCannotOverrideCoolingEvenWithEnoughFuel()
    {
        foreach(var fuel in new[]{4d,16d})
        {
            var frame=Frame(91,fuel) with{GameSha256="build",LoadoutFingerprint="fit",GeometryVerified=true,AdaptiveFlightControl=true,InSupercruise=true,
                Forward=new(-.5,0,Math.Sqrt(.75)),Up=new(0,1,0),TargetPosition=new(0,0,1000),TargetSystemAddress=2,Fsd=FsdPhase.Idle,
                CanBeginCharge=true,NativeAllowsCharging=true,NativeFullChargePeak=94,JumpCommitInterlockVerified=true,AlignmentRateLowerDegreesPerSecond=10,
                AngularSpeedDegreesPerSecond=0,ScoopActive=fuel<5,RefuellingToFull=fuel<5};
            Check.That(!FlightGeometry.Obscures(frame.CurrentStar!,frame.Position,frame.TargetPosition));
            var navigator=new FlightNavigator();var next=new NavWaypoint(2,"Visible inward next",new(1,0,0),"K");
            var hot=navigator.Decide(frame,next,null,At);
            Check.That(hot.Direction is {} direction&&Vector3d.Dot(direction,new(1,0,0))>.2);
            Check.Equal(0d,hot.Throttle);Check.That(!hot.BeginCharging&&!hot.PermitJump);
            var at=At.AddMilliseconds(100);
            var aligned=frame with{Sequence=2,ObservedAt=at,HeatPercent=91.1,Forward=hot.Direction!.Value,Position=new(149.8,0,0)};
            var continuing=navigator.Decide(aligned,next,null,at);
            Check.That(continuing.Throttle>0&&continuing.Throttle<=.4);
            Check.That(Vector3d.Dot(continuing.Direction!.Value,new(1,0,0))>.2);
        }
    }
    [Test] public void TheNavigatorWaitsNearbyThenChargesAfterCoolingWithEnoughFuel()
    {
        var frame=Frame(94,6) with{GameSha256="build",LoadoutFingerprint="fit",GeometryVerified=true,AdaptiveFlightControl=true,InSupercruise=true,
            Forward=new(1,0,0),Up=new(0,1,0),TargetPosition=new(10000,0,0),TargetSystemAddress=2,Fsd=FsdPhase.Idle,CanBeginCharge=true,
            NativeAllowsCharging=false,NativeFullChargePeak=104,JumpCommitInterlockVerified=true,AlignmentRateLowerDegreesPerSecond=10,AngularSpeedDegreesPerSecond=0};
        var model=new ShipThermalCalibration{GameSha256="build",LoadoutFingerprint="fit",StarBodyId=1,Verified=true,ExpiresAt=At.AddHours(1),
            MinimumHeat=0,MaximumHeat=150,ChargingGainUpperPerSecond=.1,ChargedHoldGainUpperPerSecond=.1,UncertaintyPercent=1,ChargeDurationUpperSeconds=10,
            DistanceBands=[new(105,1000,0,1)],Evidence="Independent offline thermal fixture"};
        var navigator=new FlightNavigator();var next=new NavWaypoint(2,"Next",new(10,0,0),"K");
        var hot=navigator.Decide(frame,next,model,At);
        Check.That(!hot.BeginCharging&&hot.Throttle<=.4&&hot.Direction is {} direction&&Vector3d.Dot(direction,new(1,0,0))<.9);
        var cool=frame with{Sequence=2,ObservedAt=At.AddSeconds(.1),HeatPercent=84,NativeAllowsCharging=true,NativeFullChargePeak=90};
        var ready=navigator.Decide(cool,next,model,cool.ObservedAt);Check.That(ready.BeginCharging&&!ready.CancelCharging);
    }
}