Derive each player's zen from the experience they actually gained
The money amount of a monster drop is computed as "gained experience + 7", but in a party it was neither derived from, nor distributed like, that experience. DistributeExperienceAfterKillAsync returned a party total without the game rate and the personal experience rates, while AddExpAfterKillAsync returned a value which had them applied. Both landed in the same variable in OnDeathAsync, so the party number was orders of magnitude smaller. That mismatch was worked around by recalculating the killer's solo experience for money purposes, which pinned the party pool to a solo-sized amount: with the pool then split by member count, a party of three received about a third of the solo zen each, while its experience pool grows with the party size. AwardExperienceAsync already computes the per member experience with all rates applied, it just discarded it. It now returns that value, the distribution returns the per member breakdown, and the money is split proportionally to it. The workaround is gone, the pool follows the experience, and each member's zen matches their own level, their own rates and their own master/normal branch. The units lost to the integer division are handed to the shares which were cut the most, one each, so no member is systematically favoured over many kills. Two defects around the money rate are fixed on the way: - MoneyAmountRate was applied for the killer when the drop was created and again for the receiver when a party picked it up, so a rate of 3.0 paid a party 9x. On the "money straight into the inventory" path for a solo killer it was not applied at all. It is now applied exactly once, for the receiver. - ClampMoneyOnPickup was only honoured for solo pick ups. A party member at MaximumInventoryMoney silently lost their share, because the drop was consumed as soon as any other member could take one. The clamp now runs on the shared payout path, after the money rate, so it clamps the amount actually credited. Money without a per player breakdown, such as the fixed amount of an item box, keeps being split equally. Shares of players who are no longer eligible are redistributed among the remaining ones instead of being lost. (cherry picked from commit 53f25aca08e690255571f2bb6c796bb7c10934a2)
This commit is contained in:
@@ -323,7 +323,9 @@ public abstract class AttackableNpcBase : NonPlayerCharacter, IAttackable
|
||||
var player = this.GetHitNotificationTarget(attacker);
|
||||
if (player is { })
|
||||
{
|
||||
int exp = await (player.Party?.DistributeExperienceAfterKillAsync(this, player) ?? player.AddExpAfterKillAsync(this)).ConfigureAwait(false);
|
||||
var experienceShares = player.Party is { } party
|
||||
? await party.DistributeExperienceAfterKillAsync(this, player).ConfigureAwait(false)
|
||||
: [new ExperienceShare(player, await player.AddExpAfterKillAsync(this).ConfigureAwait(false))];
|
||||
if (attacker == player)
|
||||
{
|
||||
await player.AfterKilledMonsterAsync().ConfigureAwait(false);
|
||||
@@ -341,7 +343,7 @@ public abstract class AttackableNpcBase : NonPlayerCharacter, IAttackable
|
||||
selectedCharacter.StateRemainingSeconds -= (int)this.Attributes[Stats.Level];
|
||||
}
|
||||
|
||||
_ = this.DropItemDelayedAsync(player, exp); // don't wait for completion.
|
||||
_ = this.DropItemDelayedAsync(player, experienceShares); // don't wait for completion.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -401,46 +403,44 @@ public abstract class AttackableNpcBase : NonPlayerCharacter, IAttackable
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask HandleMoneyDropAsync(uint amount, Player killer)
|
||||
private async ValueTask HandleMoneyDropAsync(uint amount, Player killer, IReadOnlyList<ExperienceShare> experienceShares)
|
||||
{
|
||||
// Each player gets the part of the money which matches the experience they gained from the kill,
|
||||
// so that money follows the same distribution as the experience it is derived from.
|
||||
var shares = MoneyDistribution.CreateShares(amount, experienceShares);
|
||||
|
||||
// We don't drop money in Devil Square, etc.
|
||||
var shouldDropMoney = killer.GameContext.Configuration.ShouldDropMoney && killer.CurrentMiniGame is null;
|
||||
if (!shouldDropMoney)
|
||||
{
|
||||
var party = killer.Party;
|
||||
if (party is null)
|
||||
if (killer.Party is { } party)
|
||||
{
|
||||
killer.TryAddMoney((int)amount);
|
||||
await party.DistributeMoneyAfterKillAsync(this, killer, shares).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await party.DistributeMoneyAfterKillAsync(this, killer, amount).ConfigureAwait(false);
|
||||
_ = MoneyDistribution.TryPay(killer, amount);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var droppedMoney = new DroppedMoney((uint)(amount * (killer.Attributes?[Stats.MoneyAmountRate] ?? 1.0f)), this.Position, this.CurrentMap);
|
||||
var droppedMoney = new DroppedMoney(amount, this.Position, this.CurrentMap, shares);
|
||||
await this.CurrentMap.AddAsync(droppedMoney).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask DropItemAsync(int exp, Player killer)
|
||||
private async ValueTask DropItemAsync(IReadOnlyList<ExperienceShare> experienceShares, Player killer)
|
||||
{
|
||||
// When the killer is in a party, DistributeExperienceAfterKillAsync returns a
|
||||
// total party experience that does NOT include game rate (ExperienceRate) or
|
||||
// personal experience rate multipliers. Since the money drop amount is
|
||||
// derived from this experience value, party money drops were dramatically
|
||||
// lower than solo drops. We recalculate the experience for money purposes
|
||||
// using the solo formula so money is consistent regardless of party state.
|
||||
if (killer.Party is not null)
|
||||
var exp = 0;
|
||||
foreach (var share in experienceShares)
|
||||
{
|
||||
exp = killer.CalculateExpAfterKill(this);
|
||||
exp += share.Experience;
|
||||
}
|
||||
|
||||
var (generatedItems, droppedMoney) = await this._dropGenerator.GenerateItemDropsAsync(this.Definition, exp, killer).ConfigureAwait(false);
|
||||
if (droppedMoney > 0)
|
||||
{
|
||||
await this.HandleMoneyDropAsync(droppedMoney.Value, killer).ConfigureAwait(false);
|
||||
await this.HandleMoneyDropAsync(droppedMoney.Value, killer, experienceShares).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var firstItem = !droppedMoney.HasValue;
|
||||
@@ -463,12 +463,12 @@ public abstract class AttackableNpcBase : NonPlayerCharacter, IAttackable
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask DropItemDelayedAsync(Player player, int gainedExp)
|
||||
private async ValueTask DropItemDelayedAsync(Player player, IReadOnlyList<ExperienceShare> experienceShares)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
await this.DropItemAsync(gainedExp, player).ConfigureAwait(false);
|
||||
await this.DropItemAsync(experienceShares, player).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user