Files
AdamuSw/tests/MUnique.OpenMU.Tests/Offline/BotMasterHandlerTests.cs
sven-n d04cbecae3 Merge pull request #820 from nolt/feature-bots
(cherry picked from commit b10de0645a869485fbb5771a739abd06b5708c2d)
2026-07-23 11:38:08 +03:00

268 lines
12 KiB
C#

// <copyright file="BotMasterHandlerTests.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Tests.Offline;
using Moq;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Bots;
using MUnique.OpenMU.GameLogic.Resets;
/// <summary>
/// Tests for <see cref="BotMasterHandler"/>: when a bot evolves into its master class (including the
/// iron rule of reset servers) and which master skill it invests its points into. The point investment
/// itself goes through the regular <see cref="MUnique.OpenMU.GameLogic.PlayerActions.Character.AddMasterPointAction"/>,
/// whose rules are covered by <see cref="MasterSystemTest"/>.
/// </summary>
[TestFixture]
public class BotMasterHandlerTests
{
private IGameContext _gameContext = null!;
/// <summary>
/// Sets up a fresh game context with the usual maximum level before each test.
/// </summary>
[SetUp]
public void SetUp()
{
this._gameContext = GameContextTestHelper.CreateGameContext();
this._gameContext.Configuration.MaximumLevel = 400;
}
/// <summary>
/// Without the reset feature the evolution is due exactly at the game's maximum level.
/// </summary>
/// <param name="level">The character level.</param>
/// <param name="expectsDue">Whether the evolution is expected to be due.</param>
[TestCase(399, false)]
[TestCase(400, true)]
public async ValueTask EvolutionIsDueAtMaximumLevelAsync(int level, bool expectsDue)
{
var player = await this.CreatePlayerWithMasterTargetAsync().ConfigureAwait(false);
player.Attributes![Stats.Level] = level;
Assert.That(BotMasterHandler.IsMasterEvolutionDue(player), Is.EqualTo(expectsDue));
}
/// <summary>
/// A class which is already a master (or has no master target) never evolves again.
/// </summary>
[Test]
public async ValueTask NoEvolutionWithoutMasterTargetAsync()
{
var player = await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
player.Attributes![Stats.Level] = 400;
Assert.That(BotMasterHandler.IsMasterEvolutionDue(player), Is.False);
}
/// <summary>
/// The iron rule of reset servers: the evolution is only due once the reset limit is exhausted,
/// and with no limit configured (resetting forever is the endgame) it is never due at all.
/// Uses the plain test context, where the added feature plugin is the effective one (see the
/// remarks at <see cref="BotResetHandlerTests.EffectiveLevelCountsResetsAsLevelSpansAsync"/>).
/// </summary>
/// <param name="resetLimit">The configured reset limit; 0 means no limit.</param>
/// <param name="resets">The bot's performed resets.</param>
/// <param name="expectsDue">Whether the evolution is expected to be due.</param>
[TestCase(3, 2, false)]
[TestCase(3, 3, true)]
[TestCase(0, 50, false)]
public async ValueTask EvolutionOnResetServersOnlyAfterLastResetAsync(int resetLimit, int resets, bool expectsDue)
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
player.GameContext.Configuration.MaximumLevel = 400;
GiveMasterTarget(player);
player.GameContext.FeaturePlugIns.AddPlugIn(
new ResetFeaturePlugIn { Configuration = new ResetConfiguration { RequiredLevel = 400, ResetLimit = resetLimit } },
true);
player.Attributes![Stats.Level] = 400;
player.Attributes[Stats.Resets] = resets;
Assert.That(BotMasterHandler.IsMasterEvolutionDue(player), Is.EqualTo(expectsDue));
}
/// <summary>
/// A due evolution assigns the master class - the same assignment the master quest performs.
/// </summary>
[Test]
public async ValueTask EvolutionAssignsMasterClassAsync()
{
var player = await this.CreatePlayerWithMasterTargetAsync().ConfigureAwait(false);
var masterClass = player.SelectedCharacter!.CharacterClass!.NextGenerationClass!;
player.Attributes![Stats.Level] = 400;
var evolved = await BotMasterHandler.TryEvolveAsync(player).ConfigureAwait(false);
Assert.That(evolved, Is.True);
Assert.That(player.SelectedCharacter!.CharacterClass, Is.SameAs(masterClass));
}
/// <summary>
/// The point spending loop learns the picked skill through the regular action and invests all
/// available points.
/// </summary>
[Test]
public async ValueTask SpendsPointsThroughRegularActionAsync()
{
// The plain test context is required here - its configuration accepts the mocked skills.
var contextDonor = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var player = await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(contextDonor.GameContext).ConfigureAwait(false);
player.SelectedCharacter!.CharacterClass!.IsMasterClass = true;
var skill = this.CreateMasterSkill(1, rank: 1, player.SelectedCharacter.CharacterClass);
player.GameContext.Configuration.Skills.Add(skill);
player.SelectedCharacter.MasterLevelUpPoints = 3;
await BotMasterHandler.TrySpendMasterPointsAsync(player).ConfigureAwait(false);
Assert.That(player.SelectedCharacter.MasterLevelUpPoints, Is.Zero);
var learned = player.SelectedCharacter.LearnedSkills.FirstOrDefault(l => l.Skill == skill);
Assert.That(learned, Is.Not.Null);
Assert.That(learned!.Level, Is.EqualTo(3));
}
/// <summary>
/// A started skill is pushed to the rank-unlock level of 10 before anything new is learned.
/// </summary>
[Test]
public async ValueTask FinishesStartedSkillBeforeLearningNewAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var characterClass = player.SelectedCharacter!.CharacterClass!;
var startedSkill = this.CreateMasterSkill(1, rank: 1, characterClass);
var otherSkill = this.CreateMasterSkill(2, rank: 1, characterClass);
player.GameContext.Configuration.Skills.Add(startedSkill);
player.GameContext.Configuration.Skills.Add(otherSkill);
player.SelectedCharacter.LearnedSkills.Add(new SkillEntry { Skill = startedSkill, Level = 5 });
player.SelectedCharacter.MasterLevelUpPoints = 1;
Assert.That(BotMasterHandler.PickNextMasterSkill(player), Is.SameAs(startedSkill));
}
/// <summary>
/// A skill of the next rank only becomes eligible once a skill of the previous rank of the same
/// root reached level 10; a next-rank skill of another root stays out of reach and the points go
/// into pumping the finished skill instead.
/// </summary>
/// <param name="sameRoot">Whether the rank-2 skill shares the root of the learned rank-1 skill.</param>
[TestCase(true)]
[TestCase(false)]
public async ValueTask RespectsRankGatePerRootAsync(bool sameRoot)
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var characterClass = player.SelectedCharacter!.CharacterClass!;
var rank1 = this.CreateMasterSkill(1, rank: 1, characterClass, rootId: 1);
var rank2 = this.CreateMasterSkill(2, rank: 2, characterClass, rootId: sameRoot ? (byte)1 : (byte)2);
player.GameContext.Configuration.Skills.Add(rank1);
player.GameContext.Configuration.Skills.Add(rank2);
player.SelectedCharacter.LearnedSkills.Add(new SkillEntry { Skill = rank1, Level = 10 });
player.SelectedCharacter.MasterLevelUpPoints = 1;
var pick = BotMasterHandler.PickNextMasterSkill(player);
Assert.That(pick, Is.SameAs(sameRoot ? rank2 : rank1));
}
/// <summary>
/// Among equally reachable new skills, a "useful" one (here: a passive boosting a stat) is
/// preferred even when a useless one comes first by number.
/// </summary>
[Test]
public async ValueTask PrefersUsefulSkillAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var characterClass = player.SelectedCharacter!.CharacterClass!;
var uselessSkill = this.CreateMasterSkill(1, rank: 1, characterClass);
var passiveSkill = this.CreateMasterSkill(2, rank: 1, characterClass);
passiveSkill.MasterDefinition!.TargetAttribute = Stats.MaximumHealth;
player.GameContext.Configuration.Skills.Add(uselessSkill);
player.GameContext.Configuration.Skills.Add(passiveSkill);
player.SelectedCharacter.MasterLevelUpPoints = 1;
Assert.That(BotMasterHandler.PickNextMasterSkill(player), Is.SameAs(passiveSkill));
}
/// <summary>
/// A bonus which only applies against other players does nothing for a bot: it spends its life
/// hunting monsters. It is picked last, after a passive which helps it there.
/// </summary>
[Test]
public async ValueTask PrefersPvmBonusOverPvpBonusAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var characterClass = player.SelectedCharacter!.CharacterClass!;
var pvpSkill = this.CreateMasterSkill(1, rank: 1, characterClass);
pvpSkill.MasterDefinition!.TargetAttribute = Stats.DefenseRatePvp;
var pvmSkill = this.CreateMasterSkill(2, rank: 1, characterClass);
pvmSkill.MasterDefinition!.TargetAttribute = Stats.MaximumHealth;
player.GameContext.Configuration.Skills.Add(pvpSkill);
player.GameContext.Configuration.Skills.Add(pvmSkill);
player.SelectedCharacter.MasterLevelUpPoints = 1;
Assert.That(BotMasterHandler.PickNextMasterSkill(player), Is.SameAs(pvmSkill));
}
/// <summary>
/// With everything learned at its maximum nothing is picked - the loop stops.
/// </summary>
[Test]
public async ValueTask PicksNothingWhenTreeIsFullAsync()
{
var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false);
var characterClass = player.SelectedCharacter!.CharacterClass!;
var skill = this.CreateMasterSkill(1, rank: 1, characterClass);
player.GameContext.Configuration.Skills.Add(skill);
player.SelectedCharacter.LearnedSkills.Add(new SkillEntry { Skill = skill, Level = 20 });
player.SelectedCharacter.MasterLevelUpPoints = 5;
Assert.That(BotMasterHandler.PickNextMasterSkill(player), Is.Null);
}
/// <summary>
/// Creates an offline test player whose class has a master class as next generation.
/// </summary>
private async ValueTask<GameLogic.Offline.OfflinePlayer> CreatePlayerWithMasterTargetAsync()
{
var player = await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false);
GiveMasterTarget(player);
return player;
}
/// <summary>
/// Gives the player's character class a master class as next generation.
/// </summary>
private static void GiveMasterTarget(Player player)
{
var masterClass = new CharacterClass
{
Name = "Test Master",
IsMasterClass = true,
};
Mock.Get(player.SelectedCharacter!.CharacterClass!)
.Setup(c => c.NextGenerationClass)
.Returns(masterClass);
}
private Skill CreateMasterSkill(short number, byte rank, CharacterClass qualifiedClass, byte rootId = 1)
{
var masterDefinition = new Mock<MasterSkillDefinition>();
masterDefinition.SetupAllProperties();
masterDefinition.Object.Rank = rank;
masterDefinition.Object.MaximumLevel = 20;
masterDefinition.Object.MinimumLevel = 1;
masterDefinition.Object.Root = new MasterSkillRoot { Id = new Guid(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, rootId) };
masterDefinition.Setup(m => m.RequiredMasterSkills).Returns(new List<Skill>());
var skill = new Mock<Skill>();
skill.SetupAllProperties();
skill.Object.Number = number;
skill.Setup(s => s.QualifiedCharacters).Returns(new List<CharacterClass> { qualifiedClass });
skill.Object.MasterDefinition = masterDefinition.Object;
return skill.Object;
}
}