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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

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

[TestFixture]
public class EngineeringTests
{
    private static readonly EngineeringCatalog Catalog = EngineeringCatalog.Default;
    private static FittedModule Module(ModuleEngineering? engineering = null) => new() { Slot = "LargeHardpoint1", Item = "hpt_multicannon_gimbal_large", Engineering = engineering };
    private static EngineerAccess Tod(int rank, double reputation) => new() { Id = 300260, Name = "Tod 'The Blaster' McQuinn", Rank = rank, Progress = "Unlocked", ReputationMin = reputation, ReputationMax = reputation };
    private static EngineeringPlan Plan(FittedModule m, EngineerAccess e, int grade = 5, double quality = 1, EngineeringRecipe? effect = null, bool remote = false) => EngineeringPlanner.Calculate(Catalog, m, "Overcharged Weapon", grade, quality, effect, e, [], remote);
    [Test] public void CapturedRulesPreserveOverflowAndValidateThresholds()
    {
        var r = EngineeringRules.Captured; Check.Equal(4, r.Rank(99.99)); Check.Equal(5, r.Rank(100)); Check.Equal(5.4, r.Reward(3)); Check.Equal(80d, r.ProgressPercent);
        Check.Equal(60.5, r.Absolute(4, 50));
    }
    [Test] public void RankFourPromotesWithinGradeAndReducesRemainingRolls()
    {
        var m = Module(new() { Blueprint = "Weapon_Overcharged", Level = 3, Quality = .2 });
        var p = Plan(m, Tod(4, 94.6), 3);
        Check.Equal(3, p.Rolls.Count); Check.Equal(4, p.Rolls[0].BeforeRank); Check.Equal(5, p.Rolls[0].AfterRank);
        Check.Equal(5, p.Rolls[1].BeforeRank); Check.That(p.Rolls[1].AfterQuality - p.Rolls[1].BeforeQuality > .33);
        Check.That(p.Complete && p.Exact); Check.Equal(3, p.Materials.Single(x => x.Key == "nickel").Required);
    }
    [Test] public void FreshEngineerLevelsDynamicallyToFiveDuringOneModule()
    {
        var p = Plan(Module(), Tod(1, 0)); Check.That(p.Complete && p.FinalRank == 5);
        Check.Equal(4, p.Rolls.Count(x => x.Grade == 1)); Check.Equal(5, p.Rolls.Count(x => x.Grade == 5));
        Check.That(p.Rolls.Any(x => x.BeforeRank == 4 && x.AfterRank == 5));
        Check.That(p.Rolls.Any(x => x.BeforeReputation < 1 && x.AfterReputation > 1));
    }
    [Test] public void MaxRankUsesOneTwoThreeFourFiveRolls()
    {
        var p = Plan(Module(), Tod(5, 100)); Check.Equal(15, p.Rolls.Count);
        for (int grade = 1; grade <= 5; grade++) Check.Equal(grade, p.Rolls.Count(x => x.Grade == grade));
    }
    [Test] public void PartialGradeCountsOnlyRemainingProgressAndExperimentalOnce()
    {
        var effect = Catalog.Recipes.Single(r => r.Type == "Multi-cannon" && r.Name == "Incendiary Rounds" && r.Grade == 0);
        var p = Plan(Module(new() { Blueprint = "Weapon_Overcharged", Level = 5, Quality = .6 }), Tod(5, 100), effect: effect);
        Check.Equal(2, p.Rolls.Count); Check.Equal(5, p.Materials.Single(x => x.Key == "phosphorus").Required);
        var already = Plan(Module(new() { Blueprint = "Weapon_Overcharged", Level = 5, Quality = 1, ExperimentalEffectId = "special_incendiary_rounds" }), Tod(5, 100), effect: effect);
        Check.Equal(0, already.Materials.Count);
    }
    [Test] public void RemoteCraftNeverRaisesReputationAndCannotApplyExperimental()
    {
        var p = Plan(Module(), Tod(4, 99), 4, remote: true);
        Check.Equal(4, p.FinalRank); Check.Equal(99d, p.FinalReputation!.Value); Check.That(p.Rolls.All(r => r.BeforeRank == r.AfterRank));
        var effect = Catalog.Recipes.First(r => r.Type == "Multi-cannon" && r.Grade == 0);
        Check.Throws<InvalidOperationException>(() => Plan(Module(), Tod(5, 100), effect: effect, remote: true));
    }
    [Test] public void ActualExistingEffectIsExplicitAndDoesNotRequireReapplyingItRemotely()
    {
        var effect=Catalog.Recipes.Single(r=>r.Type=="Multi-cannon"&&r.Name=="Incendiary Rounds"&&r.Grade==0);
        var module=Module(new(){Blueprint="Weapon_Overcharged",Level=5,Quality=1,ExperimentalEffect="Incendiary Rounds"});
        var current=Plan(module,Tod(5,100),effect:effect,remote:true);
        Check.Equal(0,current.Materials.Count);
        var none=EngineeringPlanner.Calculate(Catalog,module,"Overcharged Weapon",5,1,null,Tod(5,100),[],experimentalId:"");
        Check.Equal(0,none.Materials.Count);Check.That(none.Notes.Any(n=>n=="目标实验效果:无实验效果。"));
        Check.That(EngineeringExperimental.Same(Catalog,"Multi-cannon","special_incendiary_rounds","Incendiary Rounds"));
    }
    [Test] public void UnknownExistingEffectCanBeNamedWithoutInventingItsMaterialRecipe()
    {
        const string unknown="special_unlisted_effect";
        var module=Module(new(){Blueprint="Weapon_Overcharged",Level=5,Quality=1,ExperimentalEffectId=unknown});
        var unchanged=EngineeringPlanner.Calculate(Catalog,module,"Overcharged Weapon",5,1,null,Tod(5,100),[],remote:true,experimentalId:unknown);
        Check.Equal(0,unchanged.Materials.Count);
        Check.Throws<InvalidOperationException>(()=>EngineeringPlanner.Calculate(Catalog,Module(),"Overcharged Weapon",5,1,null,Tod(5,100),[],experimentalId:unknown));
    }
    [Test] public void ReputationIntervalProducesMaterialRanges()
    {
        var p = Plan(Module(), Tod(4, 21) with { ReputationMax = 99.99 });
        Check.That(!p.Exact && p.Complete); Check.That(p.Materials.Any(m => m.Minimum < m.Required));
    }
    [Test] public void ModuleCapabilityAndLockedEngineerAreEnforced()
    {
        Check.Throws<InvalidOperationException>(() => Plan(Module(), Tod(5, 100) with { Progress = "Known" }));
        Check.Throws<InvalidOperationException>(() => EngineeringPlanner.Calculate(Catalog, new() { Item = "int_hyperdrive_size5_class5" }, "Increased FSD Range", 5, 1, null, Tod(5, 100), []));
    }
    [Test] public void SnapshotAndCraftUpdateReputationInventoryAndModuleImmediately()
    {
        var p = new PlayerProjector();
        p.Apply(Check.Json("""{"event":"LoadGame","timestamp":"2026-10-21T00:00:00Z","FID":"test","ShipID":1}"""));
        p.Apply(Check.Json("""{"event":"Materials","timestamp":"2026-10-21T00:00:01Z","Raw":[{"Name":"nickel","Count":20}],"Encoded":[],"Manufactured":[]}"""));
        p.Apply(Check.Json("""{"event":"Loadout","timestamp":"2026-10-21T00:00:02Z","ShipID":1,"Modules":[{"Slot":"LargeHardpoint1","Item":"hpt_multicannon_gimbal_large","Engineering":{"BlueprintName":"Weapon_Overcharged","Level":3,"Quality":0.2}}]}"""));
        p.Apply(Check.Json("""{"event":"EngineerProgress","timestamp":"2026-10-21T00:00:03Z","Engineers":[{"Engineer":"Tod 'The Blaster' McQuinn","EngineerID":300260,"Progress":"Unlocked","Rank":4,"RankProgress":50}]}"""));
        p.Apply(Check.Json("""{"event":"EngineerCraft","timestamp":"2026-10-21T00:00:04Z","EngineerID":300260,"Engineer":"Tod 'The Blaster' McQuinn","Slot":"LargeHardpoint1","Module":"hpt_multicannon_gimbal_large","BlueprintName":"Weapon_Overcharged","Level":3,"Quality":0.45,"Ingredients":[{"Name":"nickel","Count":1}]}"""));
        Check.Equal(19, p.State.Materials.Single().Count); Check.Equal(.45, p.State.Ships.Single().Modules.Single().Engineering!.Quality!.Value);
        Check.That(Math.Abs(p.State.Engineers.Single().ReputationMin!.Value - 65.9) < .00001); Check.Equal(1, p.State.VerifiedEngineeringRolls);
        Check.That(!p.State.Ships.Single().ConfigurationChanged);
    }
    [Test] public void SparseRankUpdateKeepsUnlockAndDoesNotInventZeroProgress()
    {
        var p = new PlayerProjector();
        p.Apply(Check.Json("""{"event":"EngineerProgress","timestamp":"2026-10-21T00:00:00Z","Engineers":[{"Engineer":"Example","EngineerID":1,"Progress":"Unlocked","Rank":1,"RankProgress":0}]}"""));
        p.Apply(Check.Json("""{"event":"EngineerProgress","timestamp":"2026-10-21T00:00:01Z","Engineer":"Example","EngineerID":1,"Rank":4}"""));
        var e = p.State.Engineers.Single(); Check.That(e.Unlocked); Check.Equal(4, e.Rank); Check.That(e.RankProgress is null && e.ReputationMax > e.ReputationMin);
    }
    [Test] public void ModuleProgressJustBelowThresholdCannotSkipPaidRoll()
    {
        var p = Plan(Module(new() { Blueprint = "Weapon_Overcharged", Level = 3, Quality = .799 }), Tod(5, 100), 4);
        Check.That(p.Rolls.Any(r => r.Grade == 3));
    }
    [Test] public void ExperimentalAndRemoteJournalCraftDoNotIncreaseReputation()
    {
        var p = new PlayerProjector();
        p.Apply(Check.Json("""{"event":"EngineerProgress","timestamp":"2026-10-21T00:00:00Z","Engineers":[{"Engineer":"Example","EngineerID":1,"Progress":"Unlocked","Rank":4,"RankProgress":0}]}"""));
        p.Apply(Check.Json("""{"event":"EngineerCraft","timestamp":"2026-10-21T00:00:01Z","EngineerID":1,"Level":4,"ApplyExperimentalEffect":"special_auto_loader","Ingredients":[]}"""));
        p.Apply(Check.Json("""{"event":"EngineerCraft","timestamp":"2026-10-21T00:00:02Z","EngineerID":1,"Level":4,"IsRemote":true,"Ingredients":[]}"""));
        Check.Equal(21d, p.State.Engineers.Single().ReputationMin!.Value);
    }
    [Test] public void MissionRewardAndMaterialDonationMaintainInventory()
    {
        var p = new PlayerProjector();
        p.Apply(Check.Json("""{"event":"Materials","timestamp":"2026-10-21T00:00:00Z","Raw":[{"Name":"iron","Count":10}],"Encoded":[],"Manufactured":[]}"""));
        p.Apply(Check.Json("""{"event":"MissionCompleted","timestamp":"2026-10-21T00:00:01Z","MaterialsReward":[{"Name":"iron","Category":"Raw","Count":4}]}"""));
        p.Apply(Check.Json("""{"event":"EngineerContribution","timestamp":"2026-10-21T00:00:02Z","Type":"materials","Material":"iron","Quantity":5}"""));
        Check.Equal(9, p.State.Materials.Single().Count); Check.That(p.State.MaterialsReliable);
    }
    [Test] public void WorkshopTradeInvalidatesPreciseReputationWithoutFalselyRejectingLaterRoll()
    {
        var p = new EngineerProjector();
        var workshop = Catalog.EngineerMarkets[300260];
        p.Apply(Check.Json("""{"event":"EngineerProgress","timestamp":"2026-10-21T00:00:01Z","Engineers":[{"Engineer":"Tod 'The Blaster' McQuinn","EngineerID":300260,"Progress":"Unlocked","Rank":4,"RankProgress":0}]}"""), [], 1);
        p.Apply(Check.Json("""{"event":"MarketSell","timestamp":"2026-10-21T00:00:02Z"}"""), [], 1, workshop);
        Check.Equal(DateTimeOffset.Parse("2026-10-21T00:00:02Z"),p.UpdatedAt!.Value);
        Check.Equal(100d,p.Engineers.Single().ReputationMax!.Value);
        var module = Module(new(){Blueprint="Weapon_Overcharged",Level=3,Quality=.2});
        p.Apply(Check.Json("""{"event":"EngineerCraft","timestamp":"2026-10-21T00:00:03Z","EngineerID":300260,"Slot":"LargeHardpoint1","Module":"hpt_multicannon_gimbal_large","BlueprintName":"Weapon_Overcharged","Level":3,"Quality":0.5333333333}"""),[new(){ShipId=1,Modules=[module]}],1);
        Check.Equal(1,p.VerifiedQualityRolls);Check.Equal(0,p.MismatchedQualityRolls);
    }
}