// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // 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; /// /// Tests for : 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 , /// whose rules are covered by . /// [TestFixture] public class BotMasterHandlerTests { private IGameContext _gameContext = null!; /// /// Sets up a fresh game context with the usual maximum level before each test. /// [SetUp] public void SetUp() { this._gameContext = GameContextTestHelper.CreateGameContext(); this._gameContext.Configuration.MaximumLevel = 400; } /// /// Without the reset feature the evolution is due exactly at the game's maximum level. /// /// The character level. /// Whether the evolution is expected to be due. [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)); } /// /// A class which is already a master (or has no master target) never evolves again. /// [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); } /// /// 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 ). /// /// The configured reset limit; 0 means no limit. /// The bot's performed resets. /// Whether the evolution is expected to be due. [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)); } /// /// A due evolution assigns the master class - the same assignment the master quest performs. /// [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)); } /// /// The point spending loop learns the picked skill through the regular action and invests all /// available points. /// [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)); } /// /// A started skill is pushed to the rank-unlock level of 10 before anything new is learned. /// [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)); } /// /// 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. /// /// Whether the rank-2 skill shares the root of the learned rank-1 skill. [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)); } /// /// 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. /// [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)); } /// /// 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. /// [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)); } /// /// With everything learned at its maximum nothing is picked - the loop stops. /// [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); } /// /// Creates an offline test player whose class has a master class as next generation. /// private async ValueTask CreatePlayerWithMasterTargetAsync() { var player = await PlayerTestHelper.CreateOfflineLevelingPlayerAsync(this._gameContext).ConfigureAwait(false); GiveMasterTarget(player); return player; } /// /// Gives the player's character class a master class as next generation. /// 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(); 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()); var skill = new Mock(); skill.SetupAllProperties(); skill.Object.Number = number; skill.Setup(s => s.QualifiedCharacters).Returns(new List { qualifiedClass }); skill.Object.MasterDefinition = masterDefinition.Object; return skill.Object; } }