//
// 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;
}
}