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
22 changes: 21 additions & 1 deletion src/GameLogic/Attributes/Stats.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <copyright file="Stats.cs" company="MUnique">
// <copyright file="Stats.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

Expand Down Expand Up @@ -111,6 +111,26 @@ public class Stats
/// </summary>
public static AttributeDefinition ExperienceRate { get; } = new(new Guid("1AD454D4-BEF9-416E-BC49-82A5B0277FC7"), "Experience Rate", "Defines the experience rate multiplier of a character. By default it's 1.0 and may be modified by seals or other stuff.");

/// <summary>
/// Gets the experience random min multiplier attribute definition.
/// </summary>
public static AttributeDefinition ExperienceRandomMinMultiplier { get; } = new(new Guid("536BF8B0-D24B-4314-95B7-5D651F5892DF"), "Experience Random Min Multiplier", "Defines the minimum multiplier for the randomized experience gain.");

/// <summary>
/// Gets the experience random max multiplier attribute definition.
/// </summary>
public static AttributeDefinition ExperienceRandomMaxMultiplier { get; } = new(new Guid("74CE26C6-6D59-4420-AF3F-457E138AE41C"), "Experience Random Max Multiplier", "Defines the maximum multiplier for the randomized experience gain.");

/// <summary>
/// Gets the experience rate per party member bonus attribute definition.
/// </summary>
public static AttributeDefinition ExperienceRatePerPartyMemberBonus { get; } = new(new Guid("851DE4C6-0F57-44BB-9A43-2FEE3751FCDE"), "Experience Rate Per Party Member Bonus", "Defines the bonus experience rate per party member. Default is 0.01.");

/// <summary>
/// Gets the experience rate bonus for set party attribute definition.
/// </summary>
public static AttributeDefinition ExperienceRateBonusForSetParty { get; } = new(new Guid("8FF5C2C7-51DE-4E0A-89D9-36CFDBB11775"), "Experience Rate Bonus For Set Party", "Defines the additional bonus experience rate for a set party (3+ different classes). Default is 0.02.");

/// <summary>
/// Gets the bonus experience rate attribute definition, which is added to <see cref="ExperienceRate"/> or <see cref="MasterExperienceRate"/>.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/GameLogic/IPartyMember.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ public interface IPartyMember : IWorldObserver, IObservable, IIdentifiable, ILoc
/// </summary>
IPartyMember? LastPartyRequester { get; set; }

/// <summary>
/// Gets the character class.
/// </summary>
CharacterClass? CharacterClass { get; }

/// <summary>
/// Gets the maximum health.
/// </summary>
Expand Down
4 changes: 4 additions & 0 deletions src/GameLogic/OfflinePartyMember.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public OfflinePartyMember(IPartyMember player)
this.MaximumHealth = player.MaximumHealth;
this.CurrentMap = player.CurrentMap;
this.Position = player.Position;
this.CharacterClass = player.CharacterClass;
this.Logger = player?.Logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
}

Expand All @@ -36,6 +37,9 @@ public OfflinePartyMember(IPartyMember player)
/// <inheritdoc />
public IPartyMember? LastPartyRequester { get; set; }

/// <inheritdoc />
public CharacterClass? CharacterClass { get; }

/// <inheritdoc />
public uint MaximumHealth { get; }

Expand Down
152 changes: 119 additions & 33 deletions src/GameLogic/Party.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public sealed class Party : AsyncDisposable
private CancellationTokenSource? _healthUpdateCts;

private IPartyMember[] _partyMembers = [];
private double _experienceBonus = 1.0;

/// <summary>
/// Initializes a new instance of the <see cref="Party"/> class.
Expand Down Expand Up @@ -92,6 +93,7 @@ public async ValueTask<bool> AddAsync(IPartyMember newMember)
newMember.Party = this;
this._partyManager.TrackMembership(newMember.Name, this);
this._partyMembers = [..this._partyMembers, newMember];
this.UpdateExperienceBonus();
}

await this.SendPartyListAsync().ConfigureAwait(false);
Expand Down Expand Up @@ -135,6 +137,7 @@ public async ValueTask ReplaceMemberAsync(IPartyMember oldMember, IPartyMember n
}

this._partyMembers = updated;
this.UpdateExperienceBonus();
}

await this.SendPartyListAsync().ConfigureAwait(false);
Expand Down Expand Up @@ -340,22 +343,6 @@ protected override void Dispose(bool disposing)
base.Dispose(disposing);
}

private static (int Total, float PerLevel) CalculatePartyExperience(List<Player> recipients, IAttackable killed)
{
var count = recipients.Count;
var totalLevel = recipients.Sum(p => (int)p.Attributes![Stats.TotalLevel]);
var averageLevel = totalLevel / count;
var baseExp = killed.CalculateBaseExperience(averageLevel);

var totalAvg = baseExp * count * Math.Pow(1.05, count - 1);
totalAvg *= killed.CurrentMap?.Definition.ExpMultiplier ?? 1;

var total = Rand.NextInt((int)(totalAvg * 0.8), (int)(totalAvg * 1.2));
var perLevel = (float)total / totalLevel;

return (total, perLevel);
}

private static async ValueTask AwardExperienceAsync(Player player, float perLevel, IAttackable killed)
{
var attributes = player.Attributes!;
Expand Down Expand Up @@ -402,7 +389,13 @@ private async ValueTask ExitPartyAsync(IPartyMember member, byte index)
if (!shouldDispose)
{
this._partyMembers = this._partyMembers.Where(m => m != member).ToArray();
if (this.PartyMaster == member)
{
this.PartyMaster = this._partyMembers.FirstOrDefault();
}
}

this.UpdateExperienceBonus();
}

if (shouldDispose)
Expand Down Expand Up @@ -439,6 +432,27 @@ private void CleanupMember(IPartyMember member)
}
}

private async ValueTask UpdateNearbyCountAsync()
{
foreach (var member in this._partyMembers)
{
if (member is not Player player || player.Attributes is not { } attributes)
{
continue;
}

try
{
using var _ = await player.ObserverLock.ReaderLockAsync().ConfigureAwait(false);
attributes[Stats.NearbyPartyMemberCount] = this._partyMembers.Count(player.Observers.Contains);
}
catch (Exception ex)
{
this._logger.LogDebug(ex, "Error updating {Stat} for {Name}", nameof(Stats.NearbyPartyMemberCount), player.Name);
}
}
}

private async ValueTask<int> InternalDistributeExperienceAfterKillAsync(IAttackable killedObject, IObservable killer)
{
if (killedObject.IsSummonedMonster)
Expand All @@ -459,7 +473,7 @@ private async ValueTask<int> InternalDistributeExperienceAfterKillAsync(IAttacka
return 0;
}

var (total, perLevel) = CalculatePartyExperience(this._distributionList, killedObject);
var (total, perLevel) = this.CalculatePartyExperience(this._distributionList, killedObject);

foreach (var player in this._distributionList)
{
Expand All @@ -469,41 +483,113 @@ private async ValueTask<int> InternalDistributeExperienceAfterKillAsync(IAttacka
return total;
}

private async ValueTask UpdateNearbyCountAsync()
private async ValueTask SendPartyListAsync()
{
foreach (var member in this._partyMembers)
{
if (member is not Player player || player.Attributes is not { } attributes)
{
continue;
}

try
{
using var _ = await player.ObserverLock.ReaderLockAsync().ConfigureAwait(false);
attributes[Stats.NearbyPartyMemberCount] = this._partyMembers.Count(player.Observers.Contains);
await member.InvokeViewPlugInAsync<IUpdatePartyListPlugIn>(
p => p.UpdatePartyListAsync()).ConfigureAwait(false);
}
catch (Exception ex)
{
this._logger.LogDebug(ex, "Error updating {Stat} for {Name}", nameof(Stats.NearbyPartyMemberCount), player.Name);
this._logger.LogDebug(ex, "Error sending party list to {Name}", member.Name);
}
}
}

private async ValueTask SendPartyListAsync()
private (int Total, float PerLevel) CalculatePartyExperience(List<Player> recipients, IAttackable killed)
{
foreach (var member in this._partyMembers)
var (levelSum, maxLevel) = this.CalculatePartyLevels(recipients);
var baseExp = killed.CalculateBaseExperience(maxLevel);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please apply a randomizer, as described for the Player class.


if (recipients.FirstOrDefault()?.Attributes is { } attributes)
{
try
var minMultiplier = attributes[Stats.ExperienceRandomMinMultiplier];
var maxMultiplier = attributes[Stats.ExperienceRandomMaxMultiplier];
if (minMultiplier != 0 && maxMultiplier != 0)
{
await member.InvokeViewPlugInAsync<IUpdatePartyListPlugIn>(
p => p.UpdatePartyListAsync()).ConfigureAwait(false);
baseExp = Rand.NextInt((int)(baseExp * minMultiplier), (int)(baseExp * maxMultiplier));
}
catch (Exception ex)
}

var totalDistributed = baseExp
* this._experienceBonus
* (killed.CurrentMap?.Definition.ExpMultiplier ?? 1);
Comment thread
eduardosmaniotto marked this conversation as resolved.

return ((int)totalDistributed, (float)totalDistributed / levelSum);
}

private (int LevelSum, int MaxLevel) CalculatePartyLevels(List<Player> recipients)
{
int levelSum = 0;
int maxLevel = 0;
foreach (var player in recipients)
{
var level = (int)player.Attributes![Stats.TotalLevel];
levelSum += level;
if (level > maxLevel)
{
this._logger.LogDebug(ex, "Error sending party list to {Name}", member.Name);
maxLevel = level;
}
}

return (levelSum, maxLevel);
}

private void UpdateExperienceBonus()
{
var members = this._partyMembers;
var count = members.Length;
if (count < 2)
{
this._experienceBonus = 1.0;
return;
}

var attributes = (members.FirstOrDefault() as Player)?.Attributes;
var perPartyMemberBonus = attributes?[Stats.ExperienceRatePerPartyMemberBonus] ?? 0.01f;
var setPartyBonus = attributes?[Stats.ExperienceRateBonusForSetParty] ?? 0.02f;

if (perPartyMemberBonus == 0 && setPartyBonus == 0)
{
this._experienceBonus = 1.0;
return;
}

// General bonus: 2 players=1.02, 3 players=1.03, etc.
double bonus = 1.0 + (count * perPartyMemberBonus);

if (setPartyBonus != 0)
{
// Navigate to the last generation class to compare, since, currently, there is no way to look back.
var uniqueClasses = members
.Select(m => m.CharacterClass)
.Where(c => c != null)
.Select(c =>
{
var current = c!;
while (current.NextGenerationClass is { } next)
{
current = next;
}

return current;
})
.Distinct()
.Count();

// Set party adds an extra flat bonus on top of the general bonus.
bool isSetParty = uniqueClasses == count && count >= 3;
if (isSetParty)
{
bonus += setPartyBonus;
}
}

// Avoid negative xp values.
this._experienceBonus = Math.Max(0, bonus);
}

private async Task HealthUpdateLoopAsync(CancellationToken cancellationToken)
Expand Down
22 changes: 16 additions & 6 deletions src/GameLogic/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ public int Money
/// </summary>
public Character? SelectedCharacter => this._selectedCharacter;

/// <inheritdoc/>
public CharacterClass? CharacterClass => this.SelectedCharacter?.CharacterClass;

/// <summary>
/// Gets or sets the pose of the currently selected character.
/// </summary>
Expand Down Expand Up @@ -1184,18 +1187,25 @@ public async ValueTask<int> AddExpAfterKillAsync(IAttackable killedObject)
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 currentLevel = (short)this.Attributes![Stats.Level];
var isMaxLevel = currentLevel == this.GameContext.Configuration.MaximumLevel;
var isAddMasterExperience = characterClass.IsMasterClass && isMaxLevel;
var expRateAttribute = isAddMasterExperience ? Stats.MasterExperienceRate : Stats.ExperienceRate;
var gameRate = isAddMasterExperience ? 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));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert. You could add new Stats to be able to configure that:

  • Stats.ExperienceRandomMinMultiplier: 0.8
  • Stats.ExperienceRandomMaxMultiplier: 1.2
    added to the GameConfiguration.GlobalBaseAttributeValues.


if (addMasterExperience)
var minMultiplier = this.Attributes[Stats.ExperienceRandomMinMultiplier];
var maxMultiplier = this.Attributes[Stats.ExperienceRandomMaxMultiplier];
if (minMultiplier > 0 && maxMultiplier > 0)
{
experience = Rand.NextInt((int)(experience * minMultiplier), (int)(experience * maxMultiplier));
}

if (isAddMasterExperience)
{
await this.AddMasterExperienceAsync((int)experience, killedObject).ConfigureAwait(false);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <copyright file="CharacterClassInitialization.cs" company="MUnique">
// <copyright file="CharacterClassInitialization.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

Expand Down Expand Up @@ -160,6 +160,8 @@ private void AddCommonBaseAttributeValues(ICollection<ConstValueAttribute> baseA
baseAttributeValues.Add(this.CreateConstValueAttribute(1, Stats.DamageReceiveDecrement));
baseAttributeValues.Add(this.CreateConstValueAttribute(1, Stats.AttackDamageIncrease));
baseAttributeValues.Add(this.CreateConstValueAttribute(1, Stats.ExperienceRate));
baseAttributeValues.Add(this.CreateConstValueAttribute(0.01f, Stats.ExperienceRatePerPartyMemberBonus));
baseAttributeValues.Add(this.CreateConstValueAttribute(0.02f, Stats.ExperienceRateBonusForSetParty));
baseAttributeValues.Add(this.CreateConstValueAttribute(0.03f, Stats.PoisonDamageMultiplier));
baseAttributeValues.Add(this.CreateConstValueAttribute(1, Stats.ItemDurationIncrease));
baseAttributeValues.Add(this.CreateConstValueAttribute(2, Stats.AbilityRecoveryAbsolute));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <copyright file="GameConfigurationInitializerBase.cs" company="MUnique">
// <copyright file="GameConfigurationInitializerBase.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

Expand Down Expand Up @@ -238,8 +238,15 @@ private void AddGlobalBaseAttributeValues()
{
var moneyAmountRate = this.Context.CreateNew<ConstValueAttribute>(1f, Stats.MoneyAmountRate.GetPersistent(this.GameConfiguration));
this.GameConfiguration.GlobalBaseAttributeValues.Add(moneyAmountRate);

var experienceRandomMinMultiplier = this.Context.CreateNew<ConstValueAttribute>(0.8f, Stats.ExperienceRandomMinMultiplier.GetPersistent(this.GameConfiguration));
this.GameConfiguration.GlobalBaseAttributeValues.Add(experienceRandomMinMultiplier);

var experienceRandomMaxMultiplier = this.Context.CreateNew<ConstValueAttribute>(1.2f, Stats.ExperienceRandomMaxMultiplier.GetPersistent(this.GameConfiguration));
this.GameConfiguration.GlobalBaseAttributeValues.Add(experienceRandomMaxMultiplier);
}


private long CalcNeededMasterExp(long lvl)
{
// f(x) = 505 * x^3 + 35278500 * x + 228045 * x^2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// <copyright file="AddExperienceConfigAttributesPlugIn075.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.Persistence.Initialization.Updates;

using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;

/// <summary>
/// This update adds the experience config attributes for version 0.75.
/// </summary>
[PlugIn]
[Display(Name = PlugInName, Description = PlugInDescription)]
[Guid("5F412933-CC0F-483B-B6AE-7B358A6257FD")]
public class AddExperienceConfigAttributesPlugIn075 : AddExperienceConfigAttributesPlugInBase
{
/// <inheritdoc />
public override UpdateVersion Version => UpdateVersion.AddExperienceConfigAttributes075;

/// <inheritdoc />
public override string DataInitializationKey => Version075.DataInitialization.Id;
}
Loading