Skip to content
14 changes: 12 additions & 2 deletions 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,16 @@ 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 random experience min multiplier attribute definition.
/// </summary>
public static AttributeDefinition RandomExperienceMinMultiplier { get; } = new(new Guid("536BF8B0-D24B-4314-95B7-5D651F5892DF"), "Random Experience Min Multiplier", "Defines the minimum multiplier for the randomized experience gain.");

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

/// <summary>
/// Gets the bonus experience rate attribute definition, which is added to <see cref="ExperienceRate"/> or <see cref="MasterExperienceRate"/>.
/// </summary>
Expand Down Expand Up @@ -1525,4 +1535,4 @@ public Regeneration(AttributeDefinition regenerationMultiplier, AttributeDefinit
/// </summary>
public AttributeDefinition CurrentAttribute { get; }
}
}
}
39 changes: 30 additions & 9 deletions src/GameLogic/Party.cs
Original file line number Diff line number Diff line change
Expand Up @@ -342,18 +342,39 @@ protected override void Dispose(bool disposing)

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

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

var total = Rand.NextInt((int)(totalAvg * 0.8), (int)(totalAvg * 1.2));
var perLevel = (float)total / totalLevel;
var attributes = recipients[0].Attributes!;
var randomMinMultiplier = attributes[Stats.RandomExperienceMinMultiplier];
var randomMaxMultiplier = attributes[Stats.RandomExperienceMaxMultiplier];
var totalExperience = CalculateTotalExperience(totalBaseExperience, randomMinMultiplier, randomMaxMultiplier);
var perLevel = (float)totalExperience / totalLevel;

return (total, perLevel);
return (totalExperience, perLevel);
}

private static int CalculateTotalExperience(double totalBaseExperience, float randomMinMultiplier, float randomMaxMultiplier)
{
if (randomMinMultiplier <= 0 || randomMaxMultiplier <= 0)
{
return (int)totalBaseExperience;
}

var minimumExperience = (int)(totalBaseExperience * randomMinMultiplier);
var maximumExperience = (int)(totalBaseExperience * randomMaxMultiplier);
if (minimumExperience < maximumExperience)
{
return Rand.NextInt(minimumExperience, maximumExperience);
}

return (int)totalBaseExperience;
}
Comment thread
eduardosmaniotto marked this conversation as resolved.

private static async ValueTask AwardExperienceAsync(Player player, float perLevel, IAttackable killed)
Expand Down Expand Up @@ -535,4 +556,4 @@ private async Task HealthUpdateLoopAsync(CancellationToken cancellationToken)
// Expected during shutdown.
}
}
}
}
36 changes: 26 additions & 10 deletions src/GameLogic/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1190,9 +1190,11 @@ public async ValueTask<int> AddExpAfterKillAsync(IAttackable killedObject)
return 0;
}

var addMasterExperience = characterClass.IsMasterClass
&& (short)this.Attributes![Stats.Level] == this.GameContext.Configuration.MaximumLevel;
if (addMasterExperience)
var currentLevel = (short)this.Attributes![Stats.Level];
var isMaxLevel = currentLevel == this.GameContext.Configuration.MaximumLevel;
var isAddMasterExperience = characterClass.IsMasterClass && isMaxLevel;

if (isAddMasterExperience)
{
await this.AddMasterExperienceAsync(experience, killedObject).ConfigureAwait(false);
}
Expand Down Expand Up @@ -1223,23 +1225,37 @@ public int CalculateExpAfterKill(IAttackable killedObject)
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 currentLevel = (short)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(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));

var minMultiplier = attributes[Stats.RandomExperienceMinMultiplier];
var maxMultiplier = attributes[Stats.RandomExperienceMaxMultiplier];
if (minMultiplier > 0 && maxMultiplier > 0)
{
var minimumExperience = (int)(experience * minMultiplier);
var maximumExperience = (int)(experience * maxMultiplier);
if (minimumExperience < maximumExperience)
{
return Rand.NextInt(minimumExperience, maximumExperience);
}
}

return (int)experience;
Comment thread
eduardosmaniotto marked this conversation as resolved.
}

/// <summary>
/// Adds the master experience to the current character.
/// </summary>
/// <param name="experience">The experience which should be added.</param>
/// <param name="killedObject">The killed object which caused the experience gain.</param>
/// <param name="experience">The experience that should be added.</param>
/// <param name="killedObject">The killed object that caused the experience gain.</param>
public async ValueTask AddMasterExperienceAsync(int experience, IAttackable? killedObject)
{
using var d = await this._experienceLock.LockAsync().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
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 randomExperienceMinMultiplier = this.Context.CreateNew<ConstValueAttribute>(0.8f, Stats.RandomExperienceMinMultiplier.GetPersistent(this.GameConfiguration));
this.GameConfiguration.GlobalBaseAttributeValues.Add(randomExperienceMinMultiplier);

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


private long CalcNeededMasterExp(long lvl)
{
// f(x) = 505 * x^3 + 35278500 * x + 228045 * x^2
Expand Down Expand Up @@ -274,4 +281,4 @@ protected void AssignCharacterClassHomeMaps()
characterClass.HomeMap = this.GameConfiguration.Maps.First(map => map.Number == mapNumber);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// <copyright file="AddRandomExperienceConfigAttributesPlugIn075.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 random experience config attributes for version 0.75.
/// </summary>
[PlugIn]
[Display(Name = PlugInName, Description = PlugInDescription)]
[Guid("5F412933-CC0F-483B-B6AE-7B358A6257FD")]
public class AddRandomExperienceConfigAttributesPlugIn075 : AddRandomExperienceConfigAttributesPlugInBase
{
/// <inheritdoc />
public override UpdateVersion Version => UpdateVersion.AddRandomExperienceConfigAttributes075;

/// <inheritdoc />
public override string DataInitializationKey => Version075.DataInitialization.Id;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// <copyright file="AddRandomExperienceConfigAttributesPlugIn095d.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 random experience config attributes for version 0.95d.
/// </summary>
[PlugIn]
[Display(Name = PlugInName, Description = PlugInDescription)]
[Guid("9A166583-C3E7-4E04-924C-F01FF9840974")]
public class AddRandomExperienceConfigAttributesPlugIn095d : AddRandomExperienceConfigAttributesPlugInBase
{
/// <inheritdoc />
public override UpdateVersion Version => UpdateVersion.AddRandomExperienceConfigAttributes095d;

/// <inheritdoc />
public override string DataInitializationKey => Version095d.DataInitialization.Id;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// <copyright file="AddRandomExperienceConfigAttributesPlugInBase.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 MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic.Attributes;

/// <summary>
/// This update adds the random experience config attributes to the database
/// (RandomExperienceMinMultiplier, RandomExperienceMaxMultiplier)
/// and assigns their default values to global base attributes.
/// </summary>
public abstract class AddRandomExperienceConfigAttributesPlugInBase : UpdatePlugInBase
{
/// <summary>
/// The plugin name.
/// </summary>
internal const string PlugInName = "Add Random Experience Configuration Attributes";

/// <summary>
/// The plugin description.
/// </summary>
internal const string PlugInDescription = "Adds new random experience Configuration attributes to the game configuration.";

/// <inheritdoc />
public override string Name => PlugInName;

/// <inheritdoc />
public override string Description => PlugInDescription;

/// <inheritdoc />
public override bool IsMandatory => true;

/// <inheritdoc />
public override DateTime CreatedAt => new(2026, 04, 27, 20, 0, 0, DateTimeKind.Utc);

/// <inheritdoc />
protected override async ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration)
{
var attributesToAdd = new[]
{
Stats.RandomExperienceMinMultiplier,
Stats.RandomExperienceMaxMultiplier,
};

foreach (var attr in attributesToAdd)
{
if (!gameConfiguration.Attributes.Any(a => a.Id == attr.Id))
{
var newAttr = context.CreateNew<AttributeDefinition>(attr.Id, attr.Designation, attr.Description);
gameConfiguration.Attributes.Add(newAttr);
}
}

var randMin = gameConfiguration.Attributes.First(a => a.Id == Stats.RandomExperienceMinMultiplier.Id);
var randMax = gameConfiguration.Attributes.First(a => a.Id == Stats.RandomExperienceMaxMultiplier.Id);

if (!gameConfiguration.GlobalBaseAttributeValues.Any(a => a.Definition?.Id == randMin.Id))
{
gameConfiguration.GlobalBaseAttributeValues.Add(context.CreateNew<ConstValueAttribute>(0.8f, randMin));
}

if (!gameConfiguration.GlobalBaseAttributeValues.Any(a => a.Definition?.Id == randMax.Id))
{
gameConfiguration.GlobalBaseAttributeValues.Add(context.CreateNew<ConstValueAttribute>(1.2f, randMax));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// <copyright file="AddRandomExperienceConfigAttributesPlugInSeason6.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 random experience config attributes for season 6.
/// </summary>
[PlugIn]
[Display(Name = PlugInName, Description = PlugInDescription)]
[Guid("D1DC70A2-2614-4CC0-81C0-6C8253781019")]
public class AddRandomExperienceConfigAttributesPlugInSeason6 : AddRandomExperienceConfigAttributesPlugInBase
{
/// <inheritdoc />
public override UpdateVersion Version => UpdateVersion.AddRandomExperienceConfigAttributesSeason6;

/// <inheritdoc />
public override string DataInitializationKey => VersionSeasonSix.DataInitialization.Id;
}
17 changes: 16 additions & 1 deletion src/Persistence/Initialization/Updates/UpdateVersion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ public enum UpdateVersion
/// The version of the <see cref="FixAreaSkillsUpdatePlugIn"/>.
/// </summary>
FixAreaSkills = 78,

/// <summary>
/// The version of the <see cref="FinishDarkLordMasterTreePlugIn"/>.
/// </summary>
Expand All @@ -407,4 +407,19 @@ public enum UpdateVersion
/// The version of the <see cref="FixBloodCastleMonsterAttributesUpdatePlugIn"/>.
/// </summary>
FixBloodCastleMonsterAttributes = 80,

/// <summary>
/// The version of the <see cref="AddRandomExperienceConfigAttributesPlugIn075"/>.
/// </summary>
AddRandomExperienceConfigAttributes075 = 81,

/// <summary>
/// The version of the <see cref="AddRandomExperienceConfigAttributesPlugIn095d"/>.
/// </summary>
AddRandomExperienceConfigAttributes095d = 82,

/// <summary>
/// The version of the <see cref="AddRandomExperienceConfigAttributesPlugInSeason6"/>.
/// </summary>
AddRandomExperienceConfigAttributesSeason6 = 83,
}