baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
302
tests/MUnique.OpenMU.Tests/ExperienceRateSplitTest.cs
Normal file
302
tests/MUnique.OpenMU.Tests/ExperienceRateSplitTest.cs
Normal file
@@ -0,0 +1,302 @@
|
||||
// <copyright file="ExperienceRateSplitTest.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.Tests;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using MUnique.OpenMU.AttributeSystem;
|
||||
using MUnique.OpenMU.DataModel.Configuration;
|
||||
using MUnique.OpenMU.GameLogic;
|
||||
using MUnique.OpenMU.GameLogic.Attributes;
|
||||
using MUnique.OpenMU.GameServer;
|
||||
using MUnique.OpenMU.Interfaces;
|
||||
using MUnique.OpenMU.Persistence.InMemory;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for experience rate splitting between normal and master experience.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class ExperienceRateSplitTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that master classes receive master experience at the global master rate,
|
||||
/// while non-master classes receive normal experience.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask SoloKillUsesMasterExperienceRateForMasterClassesAsync()
|
||||
{
|
||||
var masterContext = this.CreateGameServerContext(
|
||||
normalExperienceRate: 1.0f,
|
||||
globalMasterExperienceRate: 5.0f,
|
||||
maximumLevel: 10,
|
||||
maximumMasterLevel: 200);
|
||||
var normalContext = this.CreateGameServerContext(
|
||||
normalExperienceRate: 1.0f,
|
||||
globalMasterExperienceRate: 5.0f,
|
||||
maximumLevel: 11,
|
||||
maximumMasterLevel: 200);
|
||||
|
||||
var masterPlayer = await this.CreatePlayerAsync(masterContext, level: 10, totalLevel: 10, isMasterClass: true).ConfigureAwait(false);
|
||||
var normalPlayer = await this.CreatePlayerAsync(normalContext, level: 10, totalLevel: 10, isMasterClass: false).ConfigureAwait(false);
|
||||
var killedObject = CreateKilledObject(level: 100);
|
||||
|
||||
var masterGained = await masterPlayer.AddExpAfterKillAsync(killedObject.Object).ConfigureAwait(false);
|
||||
var normalGained = await normalPlayer.AddExpAfterKillAsync(killedObject.Object).ConfigureAwait(false);
|
||||
|
||||
Assert.That(masterGained, Is.GreaterThan(0));
|
||||
Assert.That(normalGained, Is.GreaterThan(0));
|
||||
Assert.That(masterGained, Is.GreaterThan(normalGained * 3));
|
||||
Assert.That(masterPlayer.SelectedCharacter!.MasterExperience, Is.EqualTo(masterGained));
|
||||
Assert.That(normalPlayer.SelectedCharacter!.Experience, Is.EqualTo(normalGained));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the server experience rate is correctly applied to master experience gains.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask SoloKillAppliesServerExperienceRateToMasterExperienceAsync()
|
||||
{
|
||||
var highRateContext = this.CreateGameServerContext(
|
||||
normalExperienceRate: 3.0f,
|
||||
globalMasterExperienceRate: 2.0f,
|
||||
maximumLevel: 10,
|
||||
maximumMasterLevel: 200);
|
||||
var baseRateContext = this.CreateGameServerContext(
|
||||
normalExperienceRate: 1.0f,
|
||||
globalMasterExperienceRate: 2.0f,
|
||||
maximumLevel: 10,
|
||||
maximumMasterLevel: 200);
|
||||
|
||||
var highRatePlayer = await this.CreatePlayerAsync(highRateContext, level: 10, totalLevel: 10, isMasterClass: true).ConfigureAwait(false);
|
||||
var baseRatePlayer = await this.CreatePlayerAsync(baseRateContext, level: 10, totalLevel: 10, isMasterClass: true).ConfigureAwait(false);
|
||||
var killedObject = CreateKilledObject(level: 100);
|
||||
|
||||
var highRateGain = await highRatePlayer.AddExpAfterKillAsync(killedObject.Object).ConfigureAwait(false);
|
||||
var baseRateGain = await baseRatePlayer.AddExpAfterKillAsync(killedObject.Object).ConfigureAwait(false);
|
||||
|
||||
Assert.That(highRateGain, Is.GreaterThan(0));
|
||||
Assert.That(baseRateGain, Is.GreaterThan(0));
|
||||
Assert.That(highRateGain, Is.GreaterThan(baseRateGain * 2));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that party experience distribution applies master experience rates for master class members.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask PartyDistributionUsesMasterExperienceRateForMasterMembersAsync()
|
||||
{
|
||||
var context = this.CreateGameServerContext(
|
||||
normalExperienceRate: 1.0f,
|
||||
globalMasterExperienceRate: 4.0f,
|
||||
maximumLevel: 3,
|
||||
maximumMasterLevel: 200);
|
||||
|
||||
var masterPlayer = await this.CreatePlayerAsync(context, level: 3, totalLevel: 2, isMasterClass: true).ConfigureAwait(false);
|
||||
var normalPlayer = await this.CreatePlayerAsync(context, level: 2, totalLevel: 2, isMasterClass: false).ConfigureAwait(false);
|
||||
|
||||
var party = new Party(new PartyManager(5, new NullLogger<Party>()), 5, new NullLogger<Party>());
|
||||
await party.AddAsync(masterPlayer).ConfigureAwait(false);
|
||||
await party.AddAsync(normalPlayer).ConfigureAwait(false);
|
||||
await masterPlayer.AddObserverAsync(normalPlayer).ConfigureAwait(false);
|
||||
|
||||
var killedObject = CreateKilledObject(level: 5);
|
||||
_ = await party.DistributeExperienceAfterKillAsync(killedObject.Object, masterPlayer).ConfigureAwait(false);
|
||||
|
||||
var masterGained = masterPlayer.SelectedCharacter!.MasterExperience;
|
||||
var normalGained = normalPlayer.SelectedCharacter!.Experience;
|
||||
|
||||
Assert.That(masterGained, Is.GreaterThan(0));
|
||||
Assert.That(normalGained, Is.GreaterThan(0));
|
||||
Assert.That(masterGained, Is.GreaterThan(normalGained * 3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that concurrent normal experience gains cannot exceed the maximum level.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask ConcurrentNormalExperienceCantExceedMaximumLevelAsync()
|
||||
{
|
||||
var context = this.CreateGameServerContext(
|
||||
normalExperienceRate: 1.0f,
|
||||
globalMasterExperienceRate: 1.0f,
|
||||
maximumLevel: 2,
|
||||
maximumMasterLevel: 200);
|
||||
|
||||
var player = await this.CreatePlayerAsync(context, level: 1, totalLevel: 1, isMasterClass: false).ConfigureAwait(false);
|
||||
player.SelectedCharacter!.Experience = context.ExperienceTable[2] - 1;
|
||||
|
||||
var initialLevelUpPoints = player.SelectedCharacter.LevelUpPoints;
|
||||
var pointsPerLevelUp = (int)player.Attributes![Stats.PointsPerLevelUp];
|
||||
|
||||
await Task.WhenAll(
|
||||
player.AddExperienceAsync(10, null).AsTask(),
|
||||
player.AddExperienceAsync(10, null).AsTask()).ConfigureAwait(false);
|
||||
|
||||
Assert.That((int)player.Attributes[Stats.Level], Is.EqualTo(2));
|
||||
Assert.That(player.SelectedCharacter.LevelUpPoints, Is.EqualTo(initialLevelUpPoints + pointsPerLevelUp));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that concurrent master experience stays within configured maximum bounds.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask ConcurrentMasterExperienceStaysWithinConfiguredMaximumBoundsAsync()
|
||||
{
|
||||
var context = this.CreateGameServerContext(
|
||||
normalExperienceRate: 1.0f,
|
||||
globalMasterExperienceRate: 1.0f,
|
||||
maximumLevel: 400,
|
||||
maximumMasterLevel: 1);
|
||||
|
||||
var player = await this.CreatePlayerAsync(context, level: 400, totalLevel: 400, isMasterClass: true).ConfigureAwait(false);
|
||||
player.Attributes![Stats.MasterLevel] = 0;
|
||||
player.SelectedCharacter!.MasterExperience = context.MasterExperienceTable[1] - 1;
|
||||
var maxMasterExperience = context.MasterExperienceTable[context.Configuration.MaximumMasterLevel];
|
||||
|
||||
await Task.WhenAll(
|
||||
player.AddMasterExperienceAsync(10, null).AsTask(),
|
||||
player.AddMasterExperienceAsync(10, null).AsTask()).ConfigureAwait(false);
|
||||
|
||||
Assert.That((int)player.Attributes[Stats.MasterLevel], Is.LessThanOrEqualTo(context.Configuration.MaximumMasterLevel));
|
||||
Assert.That(player.SelectedCharacter.MasterExperience, Is.LessThanOrEqualTo(maxMasterExperience));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that experience overflow is applied below max when not prevented.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask OverflowIsAppliedBelowMaxWhenNotPreventedAsync()
|
||||
{
|
||||
var context = this.CreateGameServerContext(
|
||||
normalExperienceRate: 1.0f,
|
||||
globalMasterExperienceRate: 1.0f,
|
||||
maximumLevel: 10,
|
||||
maximumMasterLevel: 200);
|
||||
|
||||
var player = await this.CreatePlayerAsync(context, level: 1, totalLevel: 1, isMasterClass: false).ConfigureAwait(false);
|
||||
var requiredForLevel2 = context.ExperienceTable[2] - player.SelectedCharacter!.Experience;
|
||||
|
||||
await player.AddExperienceAsync((int)requiredForLevel2 + 10, null).ConfigureAwait(false);
|
||||
|
||||
Assert.That((int)player.Attributes![Stats.Level], Is.EqualTo(2));
|
||||
Assert.That(player.SelectedCharacter.Experience, Is.EqualTo(context.ExperienceTable[2] + 10));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that experience overflow is discarded below max when prevented.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async ValueTask OverflowIsDiscardedBelowMaxWhenPreventedAsync()
|
||||
{
|
||||
var context = this.CreateGameServerContext(
|
||||
normalExperienceRate: 1.0f,
|
||||
globalMasterExperienceRate: 1.0f,
|
||||
maximumLevel: 10,
|
||||
maximumMasterLevel: 200,
|
||||
preventExperienceOverflow: true);
|
||||
|
||||
var player = await this.CreatePlayerAsync(context, level: 1, totalLevel: 1, isMasterClass: false).ConfigureAwait(false);
|
||||
var requiredForLevel2 = context.ExperienceTable[2] - player.SelectedCharacter!.Experience;
|
||||
|
||||
await player.AddExperienceAsync((int)requiredForLevel2 + 10, null).ConfigureAwait(false);
|
||||
|
||||
Assert.That((int)player.Attributes![Stats.Level], Is.EqualTo(2));
|
||||
Assert.That(player.SelectedCharacter.Experience, Is.EqualTo(context.ExperienceTable[2]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that experience always stops at the maximum level regardless of the overflow setting.
|
||||
/// </summary>
|
||||
/// <param name="preventExperienceOverflow">Whether to prevent experience overflow.</param>
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public async ValueTask ExperienceAlwaysStopsAtMaximumLevelRegardlessOfOverflowSettingAsync(bool preventExperienceOverflow)
|
||||
{
|
||||
var context = this.CreateGameServerContext(
|
||||
normalExperienceRate: 1.0f,
|
||||
globalMasterExperienceRate: 1.0f,
|
||||
maximumLevel: 2,
|
||||
maximumMasterLevel: 200,
|
||||
preventExperienceOverflow);
|
||||
|
||||
var player = await this.CreatePlayerAsync(context, level: 1, totalLevel: 1, isMasterClass: false).ConfigureAwait(false);
|
||||
|
||||
await player.AddExperienceAsync(int.MaxValue, null).ConfigureAwait(false);
|
||||
await player.AddExperienceAsync(int.MaxValue, null).ConfigureAwait(false);
|
||||
|
||||
Assert.That((int)player.Attributes![Stats.Level], Is.EqualTo(2));
|
||||
}
|
||||
|
||||
private static Mock<IAttackable> CreateKilledObject(float level)
|
||||
{
|
||||
var attributes = new Mock<IAttributeSystem>();
|
||||
attributes.Setup(a => a[Stats.Level]).Returns(level);
|
||||
|
||||
var result = new Mock<IAttackable>();
|
||||
result.SetupGet(a => a.Attributes).Returns(attributes.Object);
|
||||
result.SetupGet(a => a.CurrentMap).Returns((GameMap?)null);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async ValueTask<Player> CreatePlayerAsync(IGameContext context, short level, float totalLevel, bool isMasterClass)
|
||||
{
|
||||
var player = await PlayerTestHelper.CreatePlayerAsync(context).ConfigureAwait(false);
|
||||
player.SelectedCharacter!.CharacterClass!.IsMasterClass = isMasterClass;
|
||||
player.Attributes![Stats.Level] = level;
|
||||
player.Attributes[Stats.MasterLevel] = 0;
|
||||
player.Attributes[Stats.PointsPerLevelUp] = 1;
|
||||
player.Attributes[Stats.MasterPointsPerLevelUp] = 1;
|
||||
player.Attributes.AddElement(new SimpleElement(1.0f, AggregateType.AddRaw), Stats.ExperienceRate);
|
||||
player.Attributes.AddElement(new SimpleElement(1.0f, AggregateType.AddRaw), Stats.MasterExperienceRate);
|
||||
player.Attributes.AddElement(new SimpleElement(totalLevel, AggregateType.AddRaw), Stats.TotalLevel);
|
||||
player.SelectedCharacter.Experience = 0;
|
||||
player.SelectedCharacter.MasterExperience = 0;
|
||||
return player;
|
||||
}
|
||||
|
||||
private IGameServerContext CreateGameServerContext(float normalExperienceRate, float globalMasterExperienceRate, short maximumLevel, short maximumMasterLevel, bool preventExperienceOverflow = false)
|
||||
{
|
||||
var contextProvider = new InMemoryPersistenceContextProvider();
|
||||
var gameConfiguration = contextProvider.CreateNewContext().CreateNew<GameConfiguration>();
|
||||
if (gameConfiguration.CharacterClasses is null)
|
||||
{
|
||||
typeof(GameConfiguration).GetProperty(nameof(GameConfiguration.CharacterClasses))?.SetValue(gameConfiguration, new List<CharacterClass>());
|
||||
}
|
||||
|
||||
gameConfiguration.RecoveryInterval = int.MaxValue;
|
||||
gameConfiguration.MaximumLevel = maximumLevel;
|
||||
gameConfiguration.MaximumMasterLevel = maximumMasterLevel;
|
||||
gameConfiguration.PreventExperienceOverflow = preventExperienceOverflow;
|
||||
gameConfiguration.MinimumMonsterLevelForMasterExperience = 0;
|
||||
gameConfiguration.ExperienceRate = 1.0f;
|
||||
gameConfiguration.MasterExperienceRate = globalMasterExperienceRate;
|
||||
var map = contextProvider.CreateNewContext().CreateNew<GameMapDefinition>();
|
||||
map.ExpMultiplier = 1.0f;
|
||||
gameConfiguration.Maps.Add(map);
|
||||
|
||||
var mapInitializer = new MapInitializer(gameConfiguration, new NullLogger<MapInitializer>(), NullDropGenerator.Instance, null);
|
||||
var gameServerContext = new GameServerContext(
|
||||
new GameServerDefinition
|
||||
{
|
||||
GameConfiguration = gameConfiguration,
|
||||
ServerConfiguration = new GameServerConfiguration(),
|
||||
ExperienceRate = normalExperienceRate,
|
||||
},
|
||||
new Mock<IGuildServer>().Object,
|
||||
new Mock<IEventPublisher>().Object,
|
||||
new Mock<ILoginServer>().Object,
|
||||
new Mock<IFriendServer>().Object,
|
||||
contextProvider,
|
||||
mapInitializer,
|
||||
new NullLoggerFactory(),
|
||||
new PlugInManager(new List<PlugInConfiguration>(), new NullLoggerFactory(), null, null),
|
||||
NullDropGenerator.Instance,
|
||||
new ConfigurationChangeMediator());
|
||||
mapInitializer.PlugInManager = gameServerContext.PlugInManager;
|
||||
mapInitializer.PathFinderPool = gameServerContext.PathFinderPool;
|
||||
return gameServerContext;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user