using AcDream.Plugin.Abstractions; namespace AcDream.Plugins.MossTank.Tests; public sealed class PetAutomationTests { [Fact] public void Select_UsesHighestPriorityTargetsElementAndDensityRange() { var settings = Settings(MonsterDamageType.PlayerAuto, MonsterDamageType.Fire); settings.PetRangeMode = PetRangeMode.Custom; settings.PetCustomRange = 8f; settings.PetMonsterDensity = 2; settings.Rules.Insert(0, new MonsterRule( "name#Boss", new MonsterRuleActions { Priority = 4, DamageType = MonsterDamageType.Fire, PetDamageType = MonsterDamageType.PlayerAuto, })); PluginInventoryItem cold = Device(1, 49387, mastery: 3, level: 100); PluginInventoryItem fire = Device(2, 49380, mastery: 3, level: 100); PetAutomationChoice choice = PetAutomation.Select( [cold, fire], [Target(10, "Trash", 4), Target(11, "Boss", 7), Target(12, "Far", 9)], new Character(mastery: 3, summoning: 300), settings, activeOwnedPetCount: 0, allowRefill: true, allowSummon: true); Assert.Equal(PetAutomationActionKind.Summon, choice.Kind); Assert.Equal(2u, choice.Device.ObjectId); Assert.Equal(11u, choice.Target.ObjectId); Assert.Equal(MonsterDamageType.Fire, choice.DamageType); } [Fact] public void Select_RejectsWrongMasteryAndInsufficientSummoningSkill() { var settings = Settings(MonsterDamageType.Auto); PluginInventoryItem wrongMastery = Device(1, 49380, mastery: 2, level: 50); PluginInventoryItem tooDifficult = Device(2, 49387, mastery: 3, level: 400); PetAutomationChoice choice = PetAutomation.Select( [wrongMastery, tooDifficult], [Target(10, "Target", 4)], new Character(mastery: 3, summoning: 300), settings, 0, allowRefill: true, allowSummon: true); Assert.Equal(PetAutomationActionKind.None, choice.Kind); } [Fact] public void Select_ExplicitElementNeverFallsBackButPlayerAutoDoes() { PluginInventoryItem cold = Device(1, 49387, mastery: 3, level: 100); var explicitFire = Settings(MonsterDamageType.Fire); var playerAuto = Settings( MonsterDamageType.PlayerAuto, MonsterDamageType.Fire); var character = new Character(mastery: 3, summoning: 300); PluginCombatTarget[] targets = [Target(10, "Target", 4)]; Assert.Equal(PetAutomationActionKind.None, PetAutomation.Select( [cold], targets, character, explicitFire, 0, true, true).Kind); Assert.Equal(PetAutomationActionKind.Summon, PetAutomation.Select( [cold], targets, character, playerAuto, 0, true, true).Kind); } [Fact] public void Select_RefillsChosenDeviceWithEncapsulatedSpiritBeforeSummon() { var settings = Settings(MonsterDamageType.Cold); settings.PetRefillCountNormal = 5; PluginInventoryItem device = Device( 1, 49387, mastery: 3, level: 100, structure: 5, maximum: 50); PluginInventoryItem spirit = Item( 2, PetDeviceCatalog.EncapsulatedSpiritWeenieClassId); PetAutomationChoice choice = PetAutomation.Select( [device, spirit], [Target(10, "Target", 4)], new Character(mastery: 3, summoning: 300), settings, 0, allowRefill: true, allowSummon: true); Assert.Equal(PetAutomationActionKind.Refill, choice.Kind); Assert.Equal(spirit.ObjectId, choice.Tool.ObjectId); Assert.Equal(device.ObjectId, choice.Device.ObjectId); } [Fact] public void Select_DoesNothingWhileOwnedCombatPetIsActive() { var settings = Settings(MonsterDamageType.Cold); PetAutomationChoice choice = PetAutomation.Select( [Device(1, 49387, 3, 100)], [Target(10, "Target", 4)], new Character(3, 300), settings, activeOwnedPetCount: 1, allowRefill: true, allowSummon: true); Assert.Equal(PetAutomationActionKind.None, choice.Kind); } [Fact] public void Tick_WaitsForUseDoneAndHonorsRetailCooldown() { var items = new ItemAutomation { Items = [Device(1, 49387, 3, 100)], }; var automation = new PetAutomation(); var settings = Settings(MonsterDamageType.Cold); var character = new Character(3, 300); PluginCombatTarget[] targets = [Target(10, "Target", 4)]; Assert.True(automation.Tick( items, character, targets, settings, 1d, out _)); Assert.Equal(new[] { 1u }, items.Uses); Assert.True(automation.Tick( items, character, targets, settings, 1.1d, out string pending)); Assert.Contains("Summoning", pending, StringComparison.Ordinal); Assert.Single(items.Uses); items.Completion = new PluginItemUseCompletion(1, 1, 0, 0); Assert.False(automation.Tick( items, character, targets, settings, 1.2d, out _)); Assert.Single(items.Uses); Assert.False(automation.Tick( items, character, targets, settings, 46.1d, out _)); Assert.Single(items.Uses); Assert.True(automation.Tick( items, character, targets, settings, 46.2d, out _)); Assert.Equal(2, items.Uses.Count); } [Theory] [InlineData(48886u, (int)MonsterDamageType.Bludgeon)] [InlineData(49366u, (int)MonsterDamageType.Acid)] [InlineData(49380u, (int)MonsterDamageType.Fire)] [InlineData(49387u, (int)MonsterDamageType.Cold)] [InlineData(49373u, (int)MonsterDamageType.Electric)] public void Catalog_MapsRetailDeviceWcids( uint wcid, int expected) => Assert.Equal( (MonsterDamageType)expected, PetDeviceCatalog.DamageType(wcid)); private static CombatSettings Settings( MonsterDamageType pet, MonsterDamageType attack = MonsterDamageType.Auto) { var settings = new CombatSettings { SummonPets = true }; settings.Rules.Clear(); settings.Rules.Add(new MonsterRule( "DEFAULT", new MonsterRuleActions { DamageType = attack, PetDamageType = pet, })); for (uint id = 1; id <= 20; id++) settings.CombatItemObjectIds.Add(id); return settings; } private static PluginCombatTarget Target(uint id, string name, float distance) => new(id, name, 1, distance, 0, true, 1f); private static PluginInventoryItem Device( uint id, uint wcid, int mastery, int level, int structure = 50, int maximum = 50) => Item(id, wcid) with { PetClass = 49000, SummoningMastery = mastery, UseRequiresSkill = 54, UseRequiresSkillLevel = level, Structure = structure, MaximumStructure = maximum, }; private static PluginInventoryItem Item(uint id, uint wcid) => new( ObjectId: id, WeenieClassId: wcid, Name: $"Item {id}", ItemType: 0, ContainerObjectId: 1, WielderObjectId: 0, ValidLocations: 0, EquippedLocation: 0, Useability: 0, TargetType: 0, PublicFlags: 0, StackSize: 1, Structure: 1, MaximumStructure: 1, SpellId: 0, PetClass: 0, SummoningMastery: 0, ProcSpellId: 0, ProcSpellSelfTargeted: false, ProcSpellRate: 0, WeaponSkill: 0, DamageType: 0, Damage: 0, DamageVariance: 0, UseRequiresSkill: 0, UseRequiresSkillLevel: 0, UseRequiresSkillSpecialized: 0); private sealed class Character(int mastery, uint summoning) : ICharacterInfo { public bool IsInWorld => true; public uint ObjectId => 1; public uint CurrentHealth => 100; public uint MaxHealth => 100; public uint CurrentStamina => 100; public uint MaxStamina => 100; public uint CurrentMana => 100; public uint MaxMana => 100; public int SummoningMastery => mastery; public IReadOnlyList Skills => [new(54, "Summoning", PluginSkillTraining.Trained, summoning)]; public IReadOnlyList Attributes => []; public IReadOnlyList ActiveEnchantments => []; public bool TryGetSkill(uint skillId, out PluginSkillInfo skill) { if (skillId == 54) { skill = Skills[0]; return true; } skill = default; return false; } } private sealed class ItemAutomation : IItemAutomation { public bool IsAvailable => true; public bool IsBusy { get; set; } public int ActiveOwnedPetCount { get; set; } public PluginItemUseCompletion Completion { get; set; } public PluginItemUseCompletion LastCompletion => Completion; public IReadOnlyList Items { get; set; } = []; public List Uses { get; } = []; public List<(uint Source, uint Target)> Applies { get; } = []; public IReadOnlyList CaptureOwnedItems() => Items; public PluginItemCommandResult Use(uint objectId) { Uses.Add(objectId); return new(PluginItemCommandStatus.Started); } public PluginItemCommandResult Apply(uint objectId, uint targetObjectId) { Applies.Add((objectId, targetObjectId)); return new(PluginItemCommandStatus.Started); } } }