diff --git a/src/GameLogic/Party.cs b/src/GameLogic/Party.cs index 2bc71a5..e4c82b1 100644 --- a/src/GameLogic/Party.cs +++ b/src/GameLogic/Party.cs @@ -388,19 +388,20 @@ public sealed class Party : AsyncDisposable return exp; } + var normalExperience = (int)(perLevel + * attributes[Stats.Level] + * player.GameContext.ExperienceRate + * (attributes[Stats.ExperienceRate] + attributes[Stats.BonusExperienceRate])); + if (!isAtMaxLevel) { - var exp = (int)(perLevel - * attributes[Stats.Level] - * player.GameContext.ExperienceRate - * (attributes[Stats.ExperienceRate] + attributes[Stats.BonusExperienceRate])); - - await player.AddExperienceAsync(exp, killed).ConfigureAwait(false); - return exp; + await player.AddExperienceAsync(normalExperience, killed).ConfigureAwait(false); } - // Player is at max level but has not completed master quest. No experience awarded. - return 0; + // At the maximum level without the master quest no experience is awarded, but the amount is + // still returned: the money drop is derived from it, and a solo kill returns it as well + // (see Player.AddExpAfterKillAsync), so such a member must not end up without any money. + return normalExperience; } private async ValueTask ExitPartyAsync(IPartyMember member, byte index) diff --git a/tests/MUnique.OpenMU.Tests/ExperienceRateSplitTest.cs b/tests/MUnique.OpenMU.Tests/ExperienceRateSplitTest.cs index cac3382..6e03a30 100644 --- a/tests/MUnique.OpenMU.Tests/ExperienceRateSplitTest.cs +++ b/tests/MUnique.OpenMU.Tests/ExperienceRateSplitTest.cs @@ -113,6 +113,39 @@ public class ExperienceRateSplitTest Assert.That(masterGained, Is.GreaterThan(normalGained * 3)); } + /// + /// Verifies that a party member at the maximum level without the master quest still gets a + /// money basis. It gains no experience, but the money drop is derived from that value, and a + /// solo kill returns it too - so returning zero would leave it without any zen in a party. + /// + [Test] + public async ValueTask MaxLevelMemberWithoutMasterQuestStillHasMoneyBasisAsync() + { + var context = this.CreateGameServerContext( + normalExperienceRate: 1.0f, + globalMasterExperienceRate: 1.0f, + maximumLevel: 3, + maximumMasterLevel: 200); + + var maxedPlayer = await this.CreatePlayerAsync(context, level: 3, totalLevel: 3, isMasterClass: false).ConfigureAwait(false); + var levelingPlayer = await this.CreatePlayerAsync(context, level: 2, totalLevel: 2, isMasterClass: false).ConfigureAwait(false); + + var party = new Party(new PartyManager(5, new NullLogger()), 5, new NullLogger()); + await party.AddAsync(maxedPlayer).ConfigureAwait(false); + await party.AddAsync(levelingPlayer).ConfigureAwait(false); + await maxedPlayer.AddObserverAsync(levelingPlayer).ConfigureAwait(false); + + var killedObject = CreateKilledObject(level: 5); + var shares = await party.DistributeExperienceAfterKillAsync(killedObject.Object, maxedPlayer).ConfigureAwait(false); + + var maxedShare = shares.First(s => s.Player == maxedPlayer); + Assert.That(maxedShare.Experience, Is.GreaterThan(0)); + + // It still must not actually gain any experience. + Assert.That(maxedPlayer.SelectedCharacter!.Experience, Is.EqualTo(0)); + Assert.That(maxedPlayer.SelectedCharacter.MasterExperience, Is.EqualTo(0)); + } + /// /// Verifies that concurrent normal experience gains cannot exceed the maximum level. ///