From d7c589fc45132dc133d734c4afb096fec791fff0 Mon Sep 17 00:00:00 2001 From: nolt Date: Fri, 24 Jul 2026 14:48:40 +0200 Subject: [PATCH] Keep a money basis for party members at max level without master quest AwardExperienceAsync returned 0 for a character at the maximum level which has not completed the master quest: it gains neither normal nor master experience. Since the money of a kill is now derived from those returned values, such a member got no zen at all in a party, while the same character still earns zen when killing solo - AddExpAfterKillAsync returns the calculated amount there, regardless of whether it could be applied. The normal experience is now always calculated and returned, and only awarded when the character can still gain it. (cherry picked from commit 7cdb34b7c58f5924427b97ea17f26c3fe0f37719) --- src/GameLogic/Party.cs | 19 ++++++----- .../ExperienceRateSplitTest.cs | 33 +++++++++++++++++++ 2 files changed, 43 insertions(+), 9 deletions(-) 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. ///