// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.Tests; using Microsoft.Extensions.Logging.Abstractions; using MUnique.OpenMU.AttributeSystem; using MUnique.OpenMU.GameLogic; using MUnique.OpenMU.GameLogic.Attributes; using MUnique.OpenMU.Pathfinding; using NUnit.Framework; /// /// Tests for the distribution of a money drop between the players which earned it. /// [TestFixture] public class MoneyDistributionTest { private static readonly Point DropPosition = new(100, 100); /// /// Tests that the money is split proportionally to the experience each player gained, /// because the money amount is derived from that experience. /// [Test] public async Task SharesFollowExperienceProportionsAsync() { var (first, second) = await CreateTwoPlayersAsync().ConfigureAwait(false); var shares = MoneyDistribution.CreateShares(1000, [new ExperienceShare(first, 800), new ExperienceShare(second, 200)]); Assert.That(shares.Count, Is.EqualTo(2)); Assert.That(shares[0].Amount, Is.EqualTo(800u)); Assert.That(shares[1].Amount, Is.EqualTo(200u)); } /// /// Tests that the remainder of the integer division is not lost, so the shares always add up /// to the dropped amount. /// [Test] public async Task SharesAddUpToTheDroppedAmountAsync() { var (first, second) = await CreateTwoPlayersAsync().ConfigureAwait(false); var third = await PlayerTestHelper.CreatePlayerAsync(first.GameContext).ConfigureAwait(false); var shares = MoneyDistribution.CreateShares(10, [new ExperienceShare(first, 1), new ExperienceShare(second, 1), new ExperienceShare(third, 1)]); Assert.That(shares.Sum(s => (long)s.Amount), Is.EqualTo(10)); } /// /// Tests that the units lost to the integer division are spread over the shares which were cut /// the most, instead of always landing on the same player - which would favour them over many kills. /// [Test] public async Task RemainderIsSpreadInsteadOfAlwaysGoingToTheSamePlayerAsync() { var (first, second) = await CreateTwoPlayersAsync().ConfigureAwait(false); var third = await PlayerTestHelper.CreatePlayerAsync(first.GameContext).ConfigureAwait(false); // 52 split between three equal contributors: 17 each leaves a remainder of 1. var shares = MoneyDistribution.CreateShares(52, [new ExperienceShare(first, 9), new ExperienceShare(second, 9), new ExperienceShare(third, 9)]); Assert.That(shares.Sum(s => (long)s.Amount), Is.EqualTo(52)); Assert.That(shares.Max(s => s.Amount) - shares.Min(s => s.Amount), Is.LessThanOrEqualTo(1u)); } /// /// Tests that a player who gained no experience from the kill gets no money from it. /// [Test] public async Task PlayerWithoutExperienceGetsNoShareAsync() { var (first, second) = await CreateTwoPlayersAsync().ConfigureAwait(false); var shares = MoneyDistribution.CreateShares(8, [new ExperienceShare(first, 0), new ExperienceShare(second, 1)]); Assert.That(shares[0].Amount, Is.EqualTo(0u)); Assert.That(shares[1].Amount, Is.EqualTo(8u)); } /// /// Tests that a player without a party gets the money multiplied by their money rate. /// [Test] public async Task SoloPickUpAppliesMoneyRateAsync() { var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); player.GameContext.Configuration.MaximumInventoryMoney = int.MaxValue; player.Money = 0; SetMoneyRate(player, 2.0f); var money = new DroppedMoney(100, DropPosition, player.CurrentMap!); Assert.That(await money.TryPickUpByAsync(player).ConfigureAwait(false), Is.True); Assert.That(player.Money, Is.EqualTo(200)); } /// /// Tests that the pick up clamp is calculated on the amount *after* the money rate was applied. /// Clamping the raw amount would let the player exceed the maximum inventory money. /// [Test] public async Task SoloPickUpClampsAfterApplyingMoneyRateAsync() { var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); player.GameContext.Configuration.MaximumInventoryMoney = 150; player.GameContext.Configuration.ClampMoneyOnPickup = true; player.Money = 0; SetMoneyRate(player, 2.0f); var money = new DroppedMoney(100, DropPosition, player.CurrentMap!); Assert.That(await money.TryPickUpByAsync(player).ConfigureAwait(false), Is.True); Assert.That(player.Money, Is.EqualTo(150)); } /// /// Tests that every party member receives exactly the share which was reserved for them. /// [Test] public async Task PartyPickUpPaysReservedSharesAsync() { var (first, second) = await CreateTwoPlayersAsync().ConfigureAwait(false); await CreatePartyAsync(first, second).ConfigureAwait(false); var money = new DroppedMoney(1000, DropPosition, first.CurrentMap!, [new MoneyShare(first, 800), new MoneyShare(second, 200)]); Assert.That(await money.TryPickUpByAsync(first).ConfigureAwait(false), Is.True); Assert.That(first.Money, Is.EqualTo(800)); Assert.That(second.Money, Is.EqualTo(200)); } /// /// Tests that the money rate of a party member is applied exactly once, on their own share. /// It used to be applied twice: once for the killer when the drop was created, and once for /// the receiver when it was picked up. /// [Test] public async Task PartyPickUpAppliesEachMembersOwnRateOnceAsync() { var (first, second) = await CreateTwoPlayersAsync().ConfigureAwait(false); await CreatePartyAsync(first, second).ConfigureAwait(false); SetMoneyRate(first, 2.0f); var money = new DroppedMoney(200, DropPosition, first.CurrentMap!, [new MoneyShare(first, 100), new MoneyShare(second, 100)]); Assert.That(await money.TryPickUpByAsync(first).ConfigureAwait(false), Is.True); Assert.That(first.Money, Is.EqualTo(200)); Assert.That(second.Money, Is.EqualTo(100)); } /// /// Tests that the share of a player who is not eligible anymore - because they left the party, /// logged out or went to another map - is given to the remaining members instead of being lost. /// [Test] public async Task PartyPickUpRedistributesShareOfIneligiblePlayerAsync() { var (first, second) = await CreateTwoPlayersAsync().ConfigureAwait(false); await CreatePartyAsync(first).ConfigureAwait(false); // 'second' never joined the party, so it can't receive its share. var money = new DroppedMoney(200, DropPosition, first.CurrentMap!, [new MoneyShare(first, 100), new MoneyShare(second, 100)]); Assert.That(await money.TryPickUpByAsync(first).ConfigureAwait(false), Is.True); Assert.That(first.Money, Is.EqualTo(200)); Assert.That(second.Money, Is.EqualTo(0)); } /// /// Tests that the pick up clamp is honoured for each party member separately. A member at the /// money limit used to silently lose their share, because the clamp was only applied for solo pick ups. /// [Test] public async Task PartyPickUpHonoursClampPerMemberAsync() { var (first, second) = await CreateTwoPlayersAsync().ConfigureAwait(false); await CreatePartyAsync(first, second).ConfigureAwait(false); first.GameContext.Configuration.MaximumInventoryMoney = 150; first.GameContext.Configuration.ClampMoneyOnPickup = true; first.Money = 100; second.Money = 0; var money = new DroppedMoney(200, DropPosition, first.CurrentMap!, [new MoneyShare(first, 100), new MoneyShare(second, 100)]); Assert.That(await money.TryPickUpByAsync(first).ConfigureAwait(false), Is.True); Assert.That(first.Money, Is.EqualTo(150)); Assert.That(second.Money, Is.EqualTo(100)); } /// /// Tests that money without reserved shares - for example the fixed amount of an item box - /// is still split equally between the party members. /// [Test] public async Task MoneyWithoutSharesIsSplitEquallyAsync() { var (first, second) = await CreateTwoPlayersAsync().ConfigureAwait(false); await CreatePartyAsync(first, second).ConfigureAwait(false); var money = new DroppedMoney(200, DropPosition, first.CurrentMap!); Assert.That(await money.TryPickUpByAsync(first).ConfigureAwait(false), Is.True); Assert.That(first.Money, Is.EqualTo(100)); Assert.That(second.Money, Is.EqualTo(100)); } /// /// Tests that the drop is not consumed when no party member could take anything, so it stays /// available instead of being lost. /// [Test] public async Task PartyPickUpKeepsMoneyAvailableWhenNobodyCanTakeItAsync() { var (first, second) = await CreateTwoPlayersAsync().ConfigureAwait(false); await CreatePartyAsync(first, second).ConfigureAwait(false); first.GameContext.Configuration.MaximumInventoryMoney = 100; first.Money = 100; second.Money = 100; var money = new DroppedMoney(200, DropPosition, first.CurrentMap!, [new MoneyShare(first, 100), new MoneyShare(second, 100)]); Assert.That(await money.TryPickUpByAsync(first).ConfigureAwait(false), Is.False); Assert.That(first.Money, Is.EqualTo(100)); Assert.That(second.Money, Is.EqualTo(100)); } private static void SetMoneyRate(Player player, float rate) { player.Attributes!.AddElement(new SimpleElement(rate, AggregateType.Multiplicate), Stats.MoneyAmountRate); Assert.That( player.Attributes[Stats.MoneyAmountRate], Is.EqualTo(rate).Within(0.001f), "test setup: the money rate was not applied as expected"); } private static async ValueTask CreatePartyAsync(params Player[] members) { var party = new Party(new PartyManager(5, new NullLogger()), 5, new NullLogger()); foreach (var member in members) { await party.AddAsync(member).ConfigureAwait(false); } return party; } private static async ValueTask<(Player First, Player Second)> CreateTwoPlayersAsync() { var first = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); first.GameContext.Configuration.MaximumInventoryMoney = int.MaxValue; first.Money = 0; var second = await PlayerTestHelper.CreatePlayerAsync(first.GameContext).ConfigureAwait(false); second.Money = 0; Assert.That(first.CurrentMap, Is.Not.Null, "test setup: the players need a map"); Assert.That(second.CurrentMap, Is.SameAs(first.CurrentMap), "test setup: the players need to be on the same map"); Assert.That(first.IsAtSafezone(), Is.False, "test setup: the players must not be at a safezone"); return (first, second); } }