diff --git a/src/GameLogic/DroppedMoney.cs b/src/GameLogic/DroppedMoney.cs index 8973353..388facd 100644 --- a/src/GameLogic/DroppedMoney.cs +++ b/src/GameLogic/DroppedMoney.cs @@ -148,7 +148,12 @@ public sealed class DroppedMoney : AsyncDisposable, ILocateable return true; } - var shares = this._shares.Count > 0 + // Money has no owner - it can always be picked up, by strangers too. The recorded shares + // only apply when the party which picks it up is the one which earned it; then the money + // follows the experience. For anyone else there is no experience to follow, so it is split + // equally between the picking party, just like money without any shares (e.g. an item box). + var earnedByThisParty = this._shares.Any(share => party.IsEligibleForMoney(share.Player, player)); + var shares = earnedByThisParty ? this._shares : MoneyDistribution.CreateEqualShares(this.Amount, party.PartyList.OfType().ToList()); diff --git a/tests/MUnique.OpenMU.Tests/DroppedMoneyTest.cs b/tests/MUnique.OpenMU.Tests/DroppedMoneyTest.cs index 97b1010..d6f00d3 100644 --- a/tests/MUnique.OpenMU.Tests/DroppedMoneyTest.cs +++ b/tests/MUnique.OpenMU.Tests/DroppedMoneyTest.cs @@ -4,6 +4,7 @@ namespace MUnique.OpenMU.Tests; +using Microsoft.Extensions.Logging.Abstractions; using MUnique.OpenMU.GameLogic; using MUnique.OpenMU.Pathfinding; using NUnit.Framework; @@ -62,4 +63,76 @@ public class DroppedMoneyTest Assert.That(await money.TryPickUpByAsync(otherPlayer).ConfigureAwait(false), Is.False); Assert.That(otherPlayer.Money, Is.EqualTo(0)); } + + /// + /// Tests that money earned by one party can still be picked up by a stranger's party - money has no + /// owner - and is then split equally between the picking party, because there is no experience of theirs + /// to follow. The earner, who is not in the picking party, receives nothing. + /// + [Test] + public async Task StrangerPartySplitsPickedUpMoneyEquallyAsync() + { + var earner = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + var gameContext = earner.GameContext; + gameContext.Configuration.MaximumInventoryMoney = int.MaxValue; + + var strangerParty = CreateParty(); + var stranger1 = await AddPartyMemberAsync(gameContext, strangerParty).ConfigureAwait(false); + var stranger2 = await AddPartyMemberAsync(gameContext, strangerParty).ConfigureAwait(false); + + // The money was earned by 'earner', who is in neither of the picking party's members. + var shares = new[] { new MoneyShare(earner, DroppedAmount) }; + var money = new DroppedMoney(DroppedAmount, new Point(100, 100), stranger1.CurrentMap!, shares); + + Assert.That(await money.TryPickUpByAsync(stranger1).ConfigureAwait(false), Is.True); + Assert.That(earner.Money, Is.EqualTo(0)); + Assert.That(stranger1.Money + stranger2.Money, Is.EqualTo((int)DroppedAmount)); + Assert.That(stranger1.Money, Is.EqualTo((int)DroppedAmount / 2)); + Assert.That(stranger2.Money, Is.EqualTo((int)DroppedAmount / 2)); + } + + /// + /// Tests that when the earning party picks its own money up, it follows the recorded shares + /// (proportional to the experience each member gained) instead of being split equally. + /// + [Test] + public async Task EarningPartyReceivesMoneyByExperienceShareAsync() + { + var big = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + var gameContext = big.GameContext; + gameContext.Configuration.MaximumInventoryMoney = int.MaxValue; + + var party = CreateParty(); + await party.AddAsync(big).ConfigureAwait(false); + var small = await AddPartyMemberAsync(gameContext, party).ConfigureAwait(false); + + var shares = new[] + { + new MoneyShare(big, 800), + new MoneyShare(small, 200), + }; + var money = new DroppedMoney(DroppedAmount, new Point(100, 100), big.CurrentMap!, shares); + + Assert.That(await money.TryPickUpByAsync(big).ConfigureAwait(false), Is.True); + Assert.That(big.Money, Is.EqualTo(800)); + Assert.That(small.Money, Is.EqualTo(200)); + } + + private static Party CreateParty() + { + // The party manager of the lightweight test context has a maximum size of zero, which would + // reject every member. A stand-alone party with a real size is enough for the money distribution. + var partyManager = new PartyManager(5, new NullLogger()); + return new Party(partyManager, 5, new NullLogger()); + } + + private static async ValueTask AddPartyMemberAsync(IGameContext gameContext, Party party) + { + // The test helper already enters the player into the world, so it shares the live map of the + // context with the other members created from the same context - which is what the money + // distribution's eligibility check compares. + var player = await PlayerTestHelper.CreatePlayerAsync(gameContext).ConfigureAwait(false); + await party.AddAsync(player).ConfigureAwait(false); + return player; + } }