// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.Tests; using Moq; using MUnique.OpenMU.DataModel; using MUnique.OpenMU.DataModel.Configuration.Items; using MUnique.OpenMU.DataModel.Entities; using MUnique.OpenMU.GameLogic; using MUnique.OpenMU.GameLogic.Bots; /// /// Tests the jewel usage policy of - which jewel a bot picks for which /// equipped item; the actual consumption goes through the regular consume handlers and is covered by /// . /// [TestFixture] public class BotJewelHandlerTest { private const byte FirstBackpackSlot = 12; /// /// A Bless in stock and an equipped piece below +6: the weakest piece is chosen for the Bless. /// [Test] public async ValueTask PrefersBlessOnWeakestUpgradeableItemAsync() { var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); // The stronger piece is the one the bot must NOT pick, so it only has to be there. await AddEquippedItemAsync(player, InventoryConstants.LeftHandSlot, 4).ConfigureAwait(false); var weakPiece = await AddEquippedItemAsync(player, InventoryConstants.RightHandSlot, 2).ConfigureAwait(false); var bless = await AddJewelAsync(player, FirstBackpackSlot, ItemConstants.JewelOfBless).ConfigureAwait(false); await AddJewelAsync(player, FirstBackpackSlot + 1, ItemConstants.JewelOfSoul).ConfigureAwait(false); var plan = BotJewelHandler.PlanNextUse(player, 2, 1, 1); Assert.That(plan, Is.Not.Null); Assert.That(plan!.Value.Jewel, Is.SameAs(bless)); Assert.That(plan.Value.Target, Is.SameAs(weakPiece)); } /// /// All gear at +6 or above: a Soul is only risked with a spare in stock. /// /// The number of souls in the backpack. /// Whether a jewel use is expected. [TestCase(1, false)] [TestCase(2, true)] public async ValueTask RisksSoulOnlyWithSpareStockAsync(int soulCount, bool expectsUse) { var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); await AddEquippedItemAsync(player, InventoryConstants.LeftHandSlot, 6).ConfigureAwait(false); for (var i = 0; i < soulCount; i++) { await AddJewelAsync(player, (byte)(FirstBackpackSlot + i), ItemConstants.JewelOfSoul).ConfigureAwait(false); } var plan = BotJewelHandler.PlanNextUse(player, 2, 1, 1); Assert.That(plan.HasValue, Is.EqualTo(expectsUse)); } /// /// The Soul prefers a lucky target (its success bonus) over a lower-level one without luck. /// [Test] public async ValueTask SoulPrefersLuckyItemAsync() { var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); await AddEquippedItemAsync(player, InventoryConstants.LeftHandSlot, 6).ConfigureAwait(false); var luckyPiece = await AddEquippedItemAsync(player, InventoryConstants.RightHandSlot, 7, withLuck: true).ConfigureAwait(false); await AddJewelAsync(player, FirstBackpackSlot, ItemConstants.JewelOfSoul).ConfigureAwait(false); await AddJewelAsync(player, FirstBackpackSlot + 1, ItemConstants.JewelOfSoul).ConfigureAwait(false); var plan = BotJewelHandler.PlanNextUse(player, 2, 1, 1); Assert.That(plan, Is.Not.Null); Assert.That(plan!.Value.Target, Is.SameAs(luckyPiece)); } /// /// Without luck the Soul risk stops at +6 (a failure from +7 on resets the item to +0), so only /// lucky items may be pushed further - up to the jewel ceiling of +9. /// /// The level of the equipped item. /// Whether the equipped item has luck. /// Whether a jewel use is expected. [TestCase(7, false, false)] [TestCase(7, true, true)] [TestCase(8, true, true)] [TestCase(9, true, false)] public async ValueTask RisksSoulAbovePlusSixOnlyWithLuckAsync(byte itemLevel, bool withLuck, bool expectsUse) { var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); await AddEquippedItemAsync(player, InventoryConstants.LeftHandSlot, itemLevel, withLuck).ConfigureAwait(false); await AddJewelAsync(player, FirstBackpackSlot, ItemConstants.JewelOfSoul).ConfigureAwait(false); await AddJewelAsync(player, FirstBackpackSlot + 1, ItemConstants.JewelOfSoul).ConfigureAwait(false); var plan = BotJewelHandler.PlanNextUse(player, 2, 1, 1); Assert.That(plan.HasValue, Is.EqualTo(expectsUse)); } /// /// Life is the last resort and is planned at most once per trip. /// /// Whether a life was already used within the trip. /// Whether a jewel use is expected. [TestCase(false, true)] [TestCase(true, false)] public async ValueTask UsesLifeAtMostOncePerTripAsync(bool lifeAlreadyUsed, bool expectsUse) { var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); var target = await AddEquippedItemAsync(player, InventoryConstants.LeftHandSlot, 9).ConfigureAwait(false); var life = await AddJewelAsync(player, FirstBackpackSlot, ItemConstants.JewelOfLife).ConfigureAwait(false); await AddJewelAsync(player, FirstBackpackSlot + 1, ItemConstants.JewelOfLife).ConfigureAwait(false); var plan = BotJewelHandler.PlanNextUse(player, 2, 1, lifeAlreadyUsed ? 0 : 1); Assert.That(plan.HasValue, Is.EqualTo(expectsUse)); if (expectsUse) { Assert.That(plan!.Value.Target, Is.SameAs(target)); Assert.That(plan.Value.Jewel, Is.SameAs(life)); } } /// /// Each jewel kind has its own budget: a spent Bless budget must not keep the Soul rule from /// being reached. With one shared budget the Bless rule - which has a target whenever any equipped /// piece is below +6 - consumed every use of every trip, and Souls and Lifes piled up unused. /// [Test] public async ValueTask SpentBlessBudgetDoesNotStarveSoulAsync() { var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); // A Bless target (below +6) and a Soul target (+6) are available at the same time. await AddEquippedItemAsync(player, InventoryConstants.RightHandSlot, 2).ConfigureAwait(false); var soulTarget = await AddEquippedItemAsync(player, InventoryConstants.LeftHandSlot, 6).ConfigureAwait(false); await AddJewelAsync(player, FirstBackpackSlot, ItemConstants.JewelOfBless).ConfigureAwait(false); var soul = await AddJewelAsync(player, FirstBackpackSlot + 1, ItemConstants.JewelOfSoul).ConfigureAwait(false); await AddJewelAsync(player, FirstBackpackSlot + 2, ItemConstants.JewelOfSoul).ConfigureAwait(false); var plan = BotJewelHandler.PlanNextUse(player, 0, 1, 1); Assert.That(plan, Is.Not.Null); Assert.That(plan!.Value.Jewel, Is.SameAs(soul)); Assert.That(plan.Value.Target, Is.SameAs(soulTarget)); } /// /// Without any applicable jewel or target, nothing is planned. /// [Test] public async ValueTask PlansNothingWithoutJewelsAsync() { var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); await AddEquippedItemAsync(player, InventoryConstants.LeftHandSlot, 2).ConfigureAwait(false); var plan = BotJewelHandler.PlanNextUse(player, 2, 1, 1); Assert.That(plan, Is.Null); } private static async ValueTask AddEquippedItemAsync(Player player, byte slot, byte level, bool withLuck = false) { var item = new Mock(); item.SetupAllProperties(); var itemOptions = new List(); item.Setup(i => i.ItemOptions).Returns(itemOptions); item.Setup(i => i.ItemSetGroups).Returns(new List()); var definition = new Mock(); definition.SetupAllProperties(); var itemSlot = new Mock(); itemSlot.Setup(s => s.ItemSlots).Returns(new List { slot }); definition.Setup(d => d.ItemSlot).Returns(itemSlot.Object); item.Object.Definition = definition.Object; item.Object.Definition.Width = 1; item.Object.Definition.Height = 1; item.Object.Definition.MaximumItemLevel = 15; item.Object.Definition.Durability = 1; item.Object.Durability = 1; item.Object.Level = level; if (withLuck) { var optionLink = new Mock(); optionLink.SetupAllProperties(); var option = new Mock(); option.SetupAllProperties(); option.Object.OptionType = ItemOptionTypes.Luck; optionLink.Object.ItemOption = option.Object; itemOptions.Add(optionLink.Object); } await player.Inventory!.AddItemAsync(slot, item.Object).ConfigureAwait(false); return item.Object; } private static async ValueTask AddJewelAsync(Player player, int slot, ItemIdentifier identifier) { var jewel = new Item { Definition = new ItemDefinition { Number = identifier.Number ?? 0, Group = identifier.Group, Width = 1, Height = 1, }, Durability = 1, }; await player.Inventory!.AddItemAsync((byte)slot, jewel).ConfigureAwait(false); return jewel; } }