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.
///