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(() => 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(()=>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(() => Plan(Module(), Tod(5, 100) with { Progress = "Known" })); Check.Throws(() => 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); } }