Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 25 additions & 19 deletions src/GameLogic/DroppedMoney.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace MUnique.OpenMU.GameLogic;

using System.Diagnostics;
using System.Threading;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.Pathfinding;
using Nito.AsyncEx;

Expand Down Expand Up @@ -67,8 +68,7 @@ public DroppedMoney(uint amount, Point position, GameMap map)
public async ValueTask<bool> TryPickUpByAsync(Player player)
{
player.Logger.LogDebug("Player {0} tries to pick up {1}", player, this);
int amountToAdd = 0;
var clampMoneyOnPickup = player.GameContext?.Configuration?.ClampMoneyOnPickup ?? false;

using (await this._pickupLock.LockAsync())
{
if (!this._availableToPick)
Expand All @@ -77,20 +77,40 @@ public async ValueTask<bool> TryPickUpByAsync(Player player)
return false;
}

this._availableToPick = false;
}

if (player.Party is { } party)
{
var partyMembers = party.PartyList
.OfType<Player>()
.Where(p => p.CurrentMap == player.CurrentMap && !p.IsAtSafezone() && p.Attributes is { })
.ToList();

if (partyMembers.Count > 0)
{
var share = (int)(this.Amount / partyMembers.Count);
foreach (var member in partyMembers)
{
member.TryAddMoney((int)(share * member.Attributes![Stats.MoneyAmountRate]));
}
}
}
else
{
var clampMoneyOnPickup = player.GameContext?.Configuration?.ClampMoneyOnPickup ?? false;
if (clampMoneyOnPickup)
{
// Calculate how much can actually be added (clamp to max)
var maxMoney = player.GameContext?.Configuration?.MaximumInventoryMoney ?? int.MaxValue;
var currentMoney = player.Money;
amountToAdd = (int)Math.Min(this.Amount, (uint)Math.Max(0, maxMoney - currentMoney));
var amountToAdd = (int)Math.Min(this.Amount, (uint)Math.Max(0, maxMoney - currentMoney));

if (amountToAdd <= 0)
{
player.Logger.LogDebug("Player is at maximum money limit, Player {0}, Money {1}", player, this);
return false;
}

// Add the clamped amount
if (!player.TryAddMoney(amountToAdd))
{
player.Logger.LogDebug("Money could not be added to the inventory, Player {0}, Money {1}", player, this);
Expand All @@ -99,26 +119,12 @@ public async ValueTask<bool> TryPickUpByAsync(Player player)
}
else
{
// Original behavior: fail if it would exceed the maximum
if (!player.TryAddMoney((int)this.Amount))
{
player.Logger.LogDebug("Money could not be added to the inventory, Player {0}, Money {1}", player, this);
return false;
}

amountToAdd = (int)this.Amount;
}

this._availableToPick = false;
}

if (clampMoneyOnPickup && amountToAdd < this.Amount)
{
player.Logger.LogDebug("Money '{0}' was partially picked up by player '{1}' - added {2} out of {3} (player at max limit).", this, player, amountToAdd, this.Amount);
}
else
{
player.Logger.LogDebug("Money '{0}' was picked up by player '{1}' and added to his inventory.", this, player);
}

await this.DisposeAsync().ConfigureAwait(false);
Expand Down
13 changes: 12 additions & 1 deletion src/GameLogic/NPC/AttackableNpcBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,12 +373,23 @@ private async ValueTask HandleMoneyDropAsync(uint amount, Player killer)
return;
}

var droppedMoney = new DroppedMoney((uint)(amount * killer.Attributes![Stats.MoneyAmountRate]), this.Position, this.CurrentMap);
var droppedMoney = new DroppedMoney((uint)(amount * (killer.Attributes?[Stats.MoneyAmountRate] ?? 1.0f)), this.Position, this.CurrentMap);
await this.CurrentMap.AddAsync(droppedMoney).ConfigureAwait(false);
}

private async ValueTask DropItemAsync(int exp, 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)
{
exp = killer.CalculateExpAfterKill(this);
}

var (generatedItems, droppedMoney) = await this._dropGenerator.GenerateItemDropsAsync(this.Definition, exp, killer).ConfigureAwait(false);
if (droppedMoney > 0)
{
Expand Down
50 changes: 38 additions & 12 deletions src/GameLogic/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1184,29 +1184,55 @@ public async ValueTask<int> AddExpAfterKillAsync(IAttackable killedObject)
return 0;
}

var experience = this.CalculateExpAfterKill(killedObject);
if (experience == 0)
{
return 0;
}

var addMasterExperience = characterClass.IsMasterClass
&& (short)this.Attributes![Stats.Level] == this.GameContext.Configuration.MaximumLevel;
var expRateAttribute = addMasterExperience ? Stats.MasterExperienceRate : Stats.ExperienceRate;
var gameRate = addMasterExperience ? this.GameContext.MasterExperienceRate : this.GameContext.ExperienceRate;

var experience = killedObject.CalculateBaseExperience(this.Attributes![Stats.TotalLevel]);
experience *= gameRate;
experience *= this.Attributes[expRateAttribute] + this.Attributes[Stats.BonusExperienceRate];
experience *= this.CurrentMap?.Definition.ExpMultiplier ?? 1;
experience = Rand.NextInt((int)(experience * 0.8), (int)(experience * 1.2));

if (addMasterExperience)
{
await this.AddMasterExperienceAsync((int)experience, killedObject).ConfigureAwait(false);
await this.AddMasterExperienceAsync(experience, killedObject).ConfigureAwait(false);
}
else
{
await this.AddExperienceAsync((int)experience, killedObject).ConfigureAwait(false);
await this.AddExperienceAsync(experience, killedObject).ConfigureAwait(false);
}

await this.AddPetExperienceAsync(experience).ConfigureAwait(false);

return (int)experience;
return experience;
}

/// <summary>
/// Calculates the amount of experience gained after a kill, without applying it to the character.
/// </summary>
/// <param name="killedObject">The killed monster.</param>
/// <returns>The calculated experience amount.</returns>
public int CalculateExpAfterKill(IAttackable killedObject)
{
if (this.SelectedCharacter?.CharacterClass is not { } characterClass)
{
return 0;
}

if (this.Attributes is not { } attributes)
{
return 0;
}

var addMasterExperience = characterClass.IsMasterClass
&& (short)attributes[Stats.Level] == this.GameContext.Configuration.MaximumLevel;
var expRateAttribute = addMasterExperience ? Stats.MasterExperienceRate : Stats.ExperienceRate;
var gameRate = addMasterExperience ? this.GameContext.MasterExperienceRate : this.GameContext.ExperienceRate;

var experience = killedObject.CalculateBaseExperience(attributes[Stats.TotalLevel]);
experience *= gameRate;
experience *= attributes[expRateAttribute] + attributes[Stats.BonusExperienceRate];
experience *= this.CurrentMap?.Definition.ExpMultiplier ?? 1;
return Rand.NextInt((int)(experience * 0.8), (int)(experience * 1.2));
}

/// <summary>
Expand Down